diff --git a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.i18n.yaml index 90546ac5db..bd2626e850 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.i18n.yaml @@ -21,11 +21,11 @@ en: e550ae9892474276 zh: fb6a2d9e91ebd4d9 /agent-note-session-observations-and-projection-owned-client-state/decision/projection-execution-boundary: - en: b463e1854d2cc694 - zh: 84fb04c9ddeb2656 + en: 148de86941b90f41 + zh: c065a8e96e28b9d7 /agent-note-session-observations-and-projection-owned-client-state/decision/read-audiences: - en: c94cd4f6ed488c24 - zh: 12f3b223fdd72c4a + en: 888f62659f4d868d + zh: e74491e8c85046ee /agent-note-session-observations-and-projection-owned-client-state/decision/replayable-client-facts-are-projection-owned: en: efded09f0cd30a8e zh: 5e16580a39ff3359 @@ -33,8 +33,8 @@ en: fb8740be674e4708 zh: 7dc0c89558f51792 /agent-note-session-observations-and-projection-owned-client-state/decision/domain-applications: - en: c45a0429091f1cf4 - zh: a76c7fe66e22e410 + en: 4dcae9e00148b11e + zh: acfd1b6831cf50fe /agent-note-session-observations-and-projection-owned-client-state/decision/failure-and-readiness-boundaries: en: 0f90a8bccedf9e71 zh: 8452dfdf0af00fb3 @@ -45,11 +45,11 @@ en: 110ca8005f618ad1 zh: 8f3e83d59334b278 /agent-note-session-observations-and-projection-owned-client-state/decision/relationship-to-existing-decisions: - en: a794043297325db2 - zh: 6459213d8dc35b1b + en: 8d699dfc22ab5c5f + zh: 8dcd9fc6bd1594db /agent-note-session-observations-and-projection-owned-client-state/verification: - en: e1458c504be960a4 - zh: 12c565a2576450cb + en: df17217023668428 + zh: 88f7da4deffc6bf9 /agent-note-session-observations-and-projection-owned-client-state/alternatives-considered: en: a25138b4298cda16 zh: 92799a9bd8867d47 diff --git a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md index 8573ea89fa..523122c9ae 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md +++ b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md @@ -25,8 +25,7 @@ flowchart LR List["list / search"] --> Corpus["SessionQuery corpus"] Follow["follow"] --> Observe["observeSession"] Page["page / attachment / fork"] --> Observe - Subagent["subagent list / continuation"] --> Corpus - Subagent --> Observe + Subagent["subagent list / continuation"] --> Observe Corpus --> Cache["projection cache hints"] Cache --> ClientList["Client Session list"] Cache -->|"small miss"| Observe @@ -71,7 +70,7 @@ The registry owns fold state; each domain owns its `init`, `apply`, `view`, sche `view` remains an uncached synchronous conversion over folded state. Its cost is bounded by the registered projection units and is paid at snapshot publication; introducing a second cache would add invalidation states without reducing event replay. -Corpus listing remains a separate lightweight operation. `listSessions()` returns live-preferred headers without materializing every log. Session list and subagent list first use live projection state or durable projection-cache rows. Session list may take one complete observation for an individually stored artifact within its configured small-log limit when cached metadata cannot establish whether it is blank; a large or unreadable cache miss remains visible with unknown hints. +Corpus listing remains a separate lightweight operation. `listSessions()` returns live-preferred headers without materializing every log. Session list first uses live projection state or durable projection-cache rows. Subagent discovery reads parent-catalog projections through exact observations. Session list may take one complete observation for an individually stored artifact within its configured small-log limit when cached metadata cannot establish whether it is blank; a large or unreadable cache miss remains visible with unknown hints. `session.follow` publishes a required opening snapshot containing header, cursor, the initial event window, and a complete projection baseline. Reconnect replaces the previous generation from another complete snapshot. `session.page` is reserved for older-history reads and gap repair. Observation-only reads never activate an Agent; only an ordinary follow may retain its prepared observation and request promotion after the opening snapshot has been delivered. @@ -86,7 +85,7 @@ Each public operation chooses one query and projection policy. The choice is par | `session.follow` | One exact observation | All, carried in the opening snapshot | Ordinary cold Session only, after snapshot delivery | | `session.page` | One exact observation | None, except projection-backed subagent authorization | Never | | Attachment and fork source | One exact observation | None unless authorization requires it | Never for the source | -| Subagent list and continuation | Corpus plus live/cache/observation resolution | All on a cold fallback; audience consumes identity or inherited values | Never for listing; continuation follows its explicit command semantics | +| Subagent list and continuation | Parent-catalog observations for listing; target observations for continuation | All; listing consumes catalog values, continuation consumes identity and inherited values | Never for listing; continuation follows its explicit command semantics | ### Replayable Client facts are projection-owned @@ -123,7 +122,7 @@ Client-local interaction state also remains local: loading and error status, an - **Title and list metadata.** Cached projection hints may render an existing title and determine blankness or recency. Missing hints leave those facts unknown; only the bounded small-log policy may resolve them during listing. The Client reconciles list rows with the current metadata projection: nonblank evidence excludes a Session from blank reuse, and the later prompt timestamp supplies recency. Projection stores outlive lazy Client Session instances, so instantiation reads retained metadata even before a list row is available. A stale list response cannot override a newer history or control projection. - **Model selection.** `model/selection` records a complete provider, model, and optional reasoning effort. `modelSelection` distinguishes the last request's route from a later selection pending consumption by a request header. - **Agent preset.** The projection initializes from immutable Session metadata and advances on preset-selection events. A missing or `null` value is not replaced with the deployment default for an existing Session. -- **Subagent identity.** The `subagent` unit remains the sole descriptor interpreter. Listing obtains candidates from the shared corpus and resolves values through live state, projection cache, or an observation rather than scanning events itself. +- **Subagent identity.** The `subagent` unit remains the sole descriptor interpreter for continuation and history authorization. Listing derives membership, mode, and label from the `subagentCatalog` projection through parent observations; descendant discovery recursively reads reachable catalogs ([parent-catalog decision](2026-09-01-parent-owned-subagent-catalog.md)). - **Subagent presentation.** Opening projection values establish timing and identity before the Client declares the child interactive or offline, so transport loading does not masquerade as a durable state. These migrations remove special-case Client state without making projection own provider catalogs or interaction mechanics. A domain still owns mutations and commands; projection owns only their replayable Session result. @@ -167,14 +166,14 @@ These rules apply to new Session-derived Client state even when a direct event s - [Reusable Session preparation](../../archived/architecture/2026-08-05-session-preparation.md) owns cold materialization, repair, reservation, and publication. Observation adds a shared read lease over that prepared object; it does not move preparation into SessionQuery. - [Session history and Remote event transport](2026-08-18-session-history-and-event-transport.md) owns stream generations and replacement semantics. This decision supplies the exact snapshot that opens each journal generation. - [Projection state and Client views](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md) owns the distinction between Host fold state and Client values. This decision governs where those values are consumed and how partial list hints differ from a complete baseline. -- [Subagent identity projection](../../archived/architecture/2026-08-06-subagent-list-identity-projection.md) continues to own descriptor folding, the serializable `null` sentinel, and the own-suffix sequence check. This decision supersedes only its independent corpus merge and direct cold-inspection path: listing now uses SessionQuery's corpus and observation. +- [The parent-catalog decision](2026-09-01-parent-owned-subagent-catalog.md) owns subagent discovery. Descriptor folding, the serializable `null` sentinel, and the own-suffix sequence check remain relevant to continuation and history authorization, not catalog identity. - The broader [session projection and command-log proposal](../../proposed/architecture/2026-07-27-session-projection-and-command-log.md) remains proposed for the portions not represented by shipped code. This decision records the shipped observation and Client-ownership subset. ## Verification Persistence and SessionQuery tests pin shared cold loading, cancellation, live-source races, retained observations, disposal, and all-or-none projection calculation. Session Controller and Gateway tests pin snapshot-first opening, replacement reconnect, older-page reads, gap repair, list-cache hints, bounded small-log fallback, and promotion after snapshot delivery. -Client tests pin higher-sequence-wins projection storage, title updates, model catalog and selection readiness, preset roster refresh and Session-specific selection, and subagent loading without transient offline presentation. Subagent tests pin corpus enumeration, cache and observation fallback, lifecycle witnesses, bounded cold reads, and no Agent activation during listing. +Client tests pin higher-sequence-wins projection storage, title updates, model catalog and selection readiness, preset roster refresh and Session-specific selection, and subagent loading without transient offline presentation. Subagent tests pin parent-catalog order, recursive traversal, branch diagnostics, cancellation, fork isolation, and no Agent activation during listing. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md index 55c477e994..6639047731 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md @@ -25,8 +25,7 @@ flowchart LR List["list / search"] --> Corpus["SessionQuery corpus"] Follow["follow"] --> Observe["observeSession"] Page["page / attachment / fork"] --> Observe - Subagent["subagent list / continuation"] --> Corpus - Subagent --> Observe + Subagent["subagent list / continuation"] --> Observe Corpus --> Cache["projection cache hints"] Cache --> ClientList["Client Session list"] Cache -->|"small miss"| Observe @@ -71,7 +70,7 @@ Registry 拥有 fold state;各领域拥有自己的 `init`、`apply`、`view` `view` 保持为 folded state 上无 cache 的同步转换。其成本由已注册 projection unit 数量界定,并在 snapshot 发布时支付;引入第二层 cache 只会增加 invalidation 状态,无法减少 event replay。 -语料库 list 仍是独立的轻量操作。`listSessions()` 返回 live-preferred header,而不物化每份日志。Session list 与 subagent list 先读取 live projection 状态或持久 projection-cache row。当 cache 无法判断 Session 是否为空,且该 Session 拥有的独立产物未超过配置的小日志限制时,Session list 可以执行一次完整 observation;大型或不可读的 cache miss 仍以 hints 未知但 row 可见的方式返回。 +语料库 list 仍是独立的轻量操作。`listSessions()` 返回 live-preferred header,而不物化每份日志。Session list 先读取 live projection 状态或持久 projection-cache row。Subagent 发现通过精确 observation 读取父目录 projection。当 cache 无法判断 Session 是否为空,且该 Session 拥有的独立产物未超过配置的小日志限制时,Session list 可以执行一次完整 observation;大型或不可读的 cache miss 仍以 hints 未知但 row 可见的方式返回。 `session.follow` 发布必需的 opening snapshot,其中包含 header、cursor、首个事件窗口和完整 projection baseline。重连使用另一份完整 snapshot 替换上一 generation。`session.page` 仅用于旧历史读取与 gap repair。只读 observation 不激活 Agent;只有普通 follow 可以保留 prepared observation,并在 opening snapshot 已交付后请求 promotion。 @@ -86,7 +85,7 @@ Registry 拥有 fold state;各领域拥有自己的 `init`、`apply`、`view` | `session.follow` | 一份精确 observation | 全算,并由 opening snapshot 携带 | 仅普通 cold Session,且在 snapshot 交付后 | | `session.page` | 一份精确 observation | 不计算,但 projection-backed subagent 鉴权除外 | 从不 | | Attachment 与 fork source | 一份精确 observation | 鉴权不要求时不计算 | source 从不激活 | -| Subagent list 与 continuation | Corpus 加 live/cache/observation 解析 | cold fallback 全算;audience 只消费 identity 或继承值 | Listing 从不;continuation 遵循显式命令语义 | +| Subagent list 与 continuation | Listing 观察父目录;continuation 观察目标 | 全算;listing 消费目录值,continuation 消费身份与继承值 | Listing 从不;continuation 遵循显式命令语义 | ### 可回放的 Client 事实归 projection 所有 @@ -123,7 +122,7 @@ Client 本地交互状态也继续留在本地:loading 和 error 状态、打 - **Title 与 list metadata。** Cached projection hints 可以渲染已有 title,并判断 blankness 或 recency。Hints 缺失时这些事实保持未知;listing 期间只有有界小日志策略可以解析它们。Client 使用当前 metadata projection 对账列表行:非空白事实会将 Session 排除在空白复用之外,较晚的 prompt 时间用于最近活动排序。Projection store 的生命周期长于惰性创建的 Client Session 实例,因此实例化时即使尚无列表行,也会读取已保留的元数据。过期列表响应不能覆盖较新的 history 或 control projection。 - **Model selection。** `model/selection` 记录完整 provider、model 和可选 reasoning effort。`modelSelection` 区分上一请求使用的 route,以及等待 request header 消费的较晚 selection。 - **Agent preset。** Projection 从不可变 Session metadata 初始化,并随 preset-selection event 推进。对于现有 Session,缺失或 `null` 值不会替换成部署默认值。 -- **Subagent identity。** `subagent` unit 仍是唯一 descriptor interpreter。Listing 从共享 corpus 获得 candidate,并通过 live state、projection cache 或 observation 解析值,不自行扫描 event。 +- **Subagent identity。** `subagent` unit 仍是继续执行与历史授权的唯一 descriptor interpreter。Listing 通过父级 observation 从 `subagentCatalog` projection 派生成员关系、模式与标签;后代发现递归读取可达目录([父目录决策](2026-09-01-parent-owned-subagent-catalog.zh.md))。 - **Subagent presentation。** Opening projection value 在 Client 宣布 child 可交互或离线前建立 timing 与 identity,因此 transport loading 不会伪装成 durable state。 这些迁移删除特殊 Client state,但不会让 projection 接管 provider catalog 或交互机制。领域仍拥有 mutation 和 command;projection 只拥有其可回放 Session 结果。 @@ -167,14 +166,14 @@ Client 本地交互状态也继续留在本地:loading 和 error 状态、打 - [可复用 Session preparation](../../archived/architecture/2026-08-05-session-preparation.md)拥有冷物化、修复、reservation 和发布。Observation 在该 prepared object 之上增加共享读取 lease,并未把 preparation 移入 SessionQuery。 - [Session 历史与 Remote event transport](2026-08-18-session-history-and-event-transport.zh.md)拥有 stream generation 与 replacement 语义。本决策提供每个日志 generation 的精确 opening snapshot。 - [Projection state 与 Client view](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md)拥有 Host fold state 和 Client value 的区分。本决策规定这些值在哪里消费,以及部分 list hints 与完整 baseline 的差别。 -- [Subagent identity projection](../../archived/architecture/2026-08-06-subagent-list-identity-projection.md)继续拥有 descriptor folding、可序列化 `null` sentinel 和 own-suffix sequence 检查。本决策只取代其中独立 corpus merge 和直接 cold inspection 路径:listing 改为使用 SessionQuery corpus 和 observation。 +- [父目录决策](2026-09-01-parent-owned-subagent-catalog.zh.md)拥有 subagent 发现机制。Descriptor folding、可序列化 `null` sentinel 和 own-suffix sequence 检查仍用于继续执行与历史授权,不用于目录身份。 - 更广泛的 [session projection 与 command-log 提案](../../proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)仍为 proposed,其中尚未由已交付代码体现的部分不受影响。本决策记录已经交付的 observation 与 Client ownership 子集。 ## 验证 Persistence 与 SessionQuery 测试固定共享冷加载、取消、live-source race、retained observation、dispose 和 all-or-none projection 计算。Session Controller 与 Gateway 测试固定 snapshot-first opening、replacement reconnect、旧分页读取、gap repair、list-cache hints、小日志有界 fallback,以及 snapshot 交付后的 promotion。 -Client 测试固定 higher-sequence-wins projection store、title 更新、model catalog 与 selection readiness、preset roster refresh 与 Session 专属选择,以及不会短暂展示离线状态的 subagent loading。Subagent 测试固定 corpus 枚举、cache 与 observation fallback、lifecycle witness、有界冷读,以及 listing 期间不激活 Agent。 +Client 测试固定 higher-sequence-wins projection store、title 更新、model catalog 与 selection readiness、preset roster refresh 与 Session 专属选择,以及不会短暂展示离线状态的 subagent loading。Subagent 测试固定父目录顺序、递归遍历、分支诊断、取消、fork 隔离,以及 listing 期间不激活 Agent。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index 895e659b3a..dbb198ca69 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -15,5 +15,5 @@ en: 7a3d4b9f69c243d8 zh: 82b03d16ccc94dd2 /agent-note-parent-owned-subagent-catalog-events/consequences: - en: 97f2a8beb039a075 - zh: 80efa9f6dd12d438 + en: d582273d5102d617 + zh: 7f05590581b1df88 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index 536ec33148..27582937d8 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -44,6 +44,6 @@ Current-writer snapshot expectations include catalog facts even when replay inpu ## Consequences -Session observations and client snapshots expose the direct-child list through `projections.values.subagentCatalog`. The projection change feed publishes a complete list when catalog state changes. Each view costs O(D), so D creations can incur O(D²) cumulative view work; this follows the existing projection mechanism. `listChildren()` reads the parent projection through one live-preferred Session observation; descendant listing uses the Session corpus and child identity projection. [Web projection consumption](../simplification/2026-09-08-web-subagent-catalog-projections.md) owns browser loading and synchronization. +Session observations and client snapshots expose the direct-child list through `projections.values.subagentCatalog`. The projection change feed publishes a complete list when catalog state changes. Each view costs O(D), so D creations can incur O(D²) cumulative view work; this follows the existing projection mechanism. `listChildren()` reads the parent projection through one live-preferred Session observation; descendant listing recursively calls the same reader in catalog order. Reachable catalog facts supply identity and membership without a global Session scan or a separate child-identity cache path. Unreadable branches remain diagnostics; uncataloged Sessions are outside discovery. [Web projection consumption](../simplification/2026-09-08-web-subagent-catalog-projections.md) owns browser loading and synchronization. [Creation metadata exclusion](../simplification/2026-09-16-subagent-catalog-membership-only.md) records why model configuration is absent from catalog facts. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index ec78ff9e4b..982dab234e 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -44,6 +44,6 @@ snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clo ## 后果 -Session observation 与 client snapshot 通过 `projections.values.subagentCatalog` 暴露直接子级列表。目录状态变化时,projection change feed 发布完整列表。每次 view 的成本为 O(D),因此 D 次创建可能累计产生 O(D²) 的 view 工作量;这沿用现有 projection 机制。`listChildren()` 通过一次 live-preferred Session observation 读取父 projection;后代枚举使用 Session 语料库与子身份 projection。[Web projection 消费决策](../simplification/2026-09-08-web-subagent-catalog-projections.zh.md) 说明浏览器加载与同步。 +Session observation 与 client snapshot 通过 `projections.values.subagentCatalog` 暴露直接子级列表。目录状态变化时,projection change feed 发布完整列表。每次 view 的成本为 O(D),因此 D 次创建可能累计产生 O(D²) 的 view 工作量;这沿用现有 projection 机制。`listChildren()` 通过一次 live-preferred Session observation 读取父 projection;后代枚举按目录顺序递归调用同一读取函数。可达目录事实提供身份和成员关系,无需全局 Session 扫描或独立的子身份缓存路径。不可读分支保留为诊断;未进入目录的 Session 不在发现范围内。[Web projection 消费决策](../simplification/2026-09-08-web-subagent-catalog-projections.zh.md) 说明浏览器加载与同步。 [创建元数据排除决策](../simplification/2026-09-16-subagent-catalog-membership-only.zh.md) 说明目录事实为何不包含模型配置。 diff --git a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml index f664219cd9..d2b3a75106 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.i18n.yaml @@ -33,8 +33,8 @@ en: c46c9ca54ea9eaad zh: 918dc8eebff5edf3 /agent-note-client-session-references-reference-sources-and-ui-status/decision/presets-and-creation-flows: - en: 790eda215b64ee37 - zh: a4b5d3a61cc80cbc + en: 70ef26a3cd399b14 + zh: 5c81515ff896f91a /agent-note-client-session-references-reference-sources-and-ui-status/decision/host-event-context-ownership-and-typert: en: eb3be89cf9431c44 zh: bfc26899286b37ad diff --git a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.md b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.md index 162ef08875..ca4055bd9e 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.md +++ b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.md @@ -166,7 +166,7 @@ Workspace connection preserves its navigation guards and panel-switch invalidati Preset directories and deployment defaults may be shared. A bound Session's preset is read or changed through its Provider binding; preset controllers are cached by `SessionBinding` rather than managed by one root current-Session follower. The hero preset seat uses the `session-maybe` Provider: it shows the creation-flow choice without a Session and operates on the exact bound blank Session after one arrives. Header labels read the same Provider-bound Session projection. -A preset chosen before Session creation remains in the main Conversation's `session-maybe` preset surface. After Workspace creation or reuse establishes the main Provider over a blank Session, that surface applies the choice to its Provider-bound Session. When Settings changes the default preset or picker setting, the preset service selects the blank Session whose established Provider binding carries `mainView` ownership and updates that Session. Non-blank main Sessions, independent Sidebar Providers, and other background references remain unchanged; preset subtrees do not read the main reference or use a global current follower to find their target. +A preset chosen before Session creation remains in the main Conversation's `session-maybe` preset surface. After Workspace creation or reuse establishes the main Provider over a blank Session, that surface applies the choice to its Provider-bound Session. When Settings changes the default preset, the preset service selects the blank Session whose established Provider binding carries `mainView` ownership and updates that Session. Non-blank main Sessions, independent Sidebar Providers, and other background references remain unchanged; preset subtrees do not read the main reference or use a global current follower to find their target. ### Host-event Context ownership and Typert diff --git a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md index 3af8932f7c..b8c6c313f6 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-15-client-session-references.zh.md @@ -166,7 +166,7 @@ Workspace 连接保持既有导航检查与面板切换失效规则。侧边栏 预设目录与部署默认值可以共享。已绑定 Session 的预设通过 Provider 的 binding 读取或修改;预设控制器按 `SessionBinding` 缓存,不由根级 current-Session 跟随器管理。Hero 的 preset seat 使用 `session-maybe` Provider:没有 Session 时显示创建流程选择,绑定空白 Session 后操作该确切 Session。标题标签读取同一 Provider 绑定 Session 的投影。 -Session 创建前选择的 preset 保留在主 Conversation 的 `session-maybe` preset surface 中。Workspace 创建或复用空白 Session 并建立主 Provider 后,该 surface 将选择应用到 Provider 绑定的 Session。设置页修改默认 preset 或 picker 设置时,preset 服务从 Provider 已建立的 binding 缓存中选择带 `mainView` 所有权标记的空白 Session,并更新该 Session。非空白主 Session、Sidebar 的独立 Provider 与其他后台引用均不受该设置动作影响;preset 子树不读取主引用,也不通过全局 current follower 寻找目标。 +Session 创建前选择的 preset 保留在主 Conversation 的 `session-maybe` preset surface 中。Workspace 创建或复用空白 Session 并建立主 Provider 后,该 surface 将选择应用到 Provider 绑定的 Session。设置页修改默认 preset 时,preset 服务从 Provider 已建立的 binding 缓存中选择带 `mainView` 所有权标记的空白 Session,并更新该 Session。非空白主 Session、Sidebar 的独立 Provider 与其他后台引用均不受该设置动作影响;preset 子树不读取主引用,也不通过全局 current follower 寻找目标。 ### Host 事件 Context 所有权与 Typert diff --git a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml index 945359e6e7..159fee0bf9 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml @@ -15,5 +15,5 @@ en: 21d7ec9a9ff3cbf2 zh: 93d03b8b3012aa7e /agent-note-host-owned-scheduled-messages/consequences: - en: 5b8d0a26024f795e - zh: c4d6f36d54783c47 + en: 0358aab730e4bf56 + zh: 61e89e0374891f8a diff --git a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md index f063dba6e4..79a4f72071 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md +++ b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md @@ -58,7 +58,7 @@ Session inbox persistence and the task put occur in separate persistence systems **Keep the mock's hourly choice out of the stored vocabulary.** The approved mock lists 每小时整点 beside the daily, weekly and one-shot choices. Hourly and fixed-rate intervals are the same elapsed-time rule, so the card maps that menu slot onto the existing interval kind instead of adding a persisted kind. A dedicated hourly kind would need a new durable variant, its decoder, its target resolution, and its tool schema, all to express what an interval already expresses. -Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and the opt-in overlay remains responsible for enabling Schedule. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported. +Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and the Web bundle mounts Schedule in its host row list. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported. Required coverage includes startup from stored tasks, missing-status normalization, cold Session restoration, latest-only catch-up, inactive-task retention without repeat dispatch, real message receipts, deletion across restart, removal of a deleted task's saved deliveries, queue/persistence failures, and disposal. Wall-clock obligations include gaps and skipped dates, earlier-only overlaps, fractional seconds, UTC-range edges, committed-target and alias preservation, and post-restoration clock rollback. Weekly adds weekday normalization and validation, next-target selection across a gap and an overlap, and stored set preservation. Historical decoders must reject Daily, Weekly, and Cron while Host storage accepts them. Client coverage includes every recurring kind, exact rule time, zone, and weekday set, All/Enabled/Inactive status filters, retained inactive details and original-Session navigation, authoritative refresh after confirmed deletion and notification, empty lists, reconnect, and late responses. Presentation coverage includes the page's New action starting a Session, the transcript card's narrowing of persisted result JSON including an incomplete or replayed result, the header clock's direct open for exactly one task and its count label, the idle-only row mark with the hover-card two-task limit and omission line. History coverage includes legacy byte preservation, append and restart, immutable sent instructions, strict latest-receipt consistency, stable cursor pagination, binding checks, failed persistence, and late page replies during refresh or task changes. Cross-store crash duplication remains an explicit limitation rather than a completion claim. diff --git a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md index 433c1acb0a..393a773fa0 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md @@ -58,7 +58,7 @@ Session 收件箱持久化和任务 put 发生在两个持久化系统中。收 **不把 mock 的「每小时整点」引入持久词表。** 设计稿把它与每天、每周、仅一次并列。每小时整点与固定间隔在语义上是同一种按经过时间计量的规则,因此详情里的该项直接映射到既有的间隔类型,而不是新增一个持久种类。若真要新增,需要新的持久变体、解码器、目标解析与工具 schema,而它表达的能力现有间隔已经覆盖。 -任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,Schedule 仍由显式启用的 overlay 加载。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。 +任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,Web bundle 在其宿主行列表中挂载 Schedule。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。 必要覆盖范围包括从已存储任务启动、缺失状态规范化、冷 Session 恢复、仅补发最新发生时点、保留未运行任务且不重复投递、真实消息回执、重启后的删除结果、删除会移除已保存发送记录、排队或持久化失败,以及销毁。钟表规则的验证要求包括缺口和跳过的日期、仅选择较早时点的重叠、小数秒、UTC 范围边缘、已提交目标和别名保留,以及恢复后的时钟回拨。Weekly 另需覆盖星期集合的规范化与校验、跨缺口与重叠的下一目标选择,以及已存集合的保留。历史解码器必须拒绝 Daily、Weekly 与 Cron,而宿主存储接受它们。客户端覆盖范围包括每种重复规则、精确规则时间、时区和星期集合、全部/已开启/未运行状态筛选、保留未运行任务详情和原 Session 导航、确认删除和通知后的权威刷新、空列表、重新连接和晚到响应。呈现覆盖范围包括页面“新建”操作打开新会话、transcript 卡片从不完整或回放的结果 JSON 中收窄任务、页头时钟在恰好一个任务时直接打开并给出数量标签、仅空闲行标记及其悬浮卡片两个任务上限与遗漏行。历史覆盖范围包括旧文件字节保留、追加与重启、不可变的发送指令、最近回执一致性严格校验、稳定游标分页、绑定校验、持久化失败,以及刷新或切换任务期间晚到的分页响应。跨存储崩溃导致的重复投递仍是明确限制,不能被表述为完成保证。 diff --git a/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.i18n.yaml index a857ff2101..209838a431 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.i18n.yaml @@ -9,8 +9,8 @@ en: 9ca845902f1df3a2 zh: 41e3157a843a511e /agent-note-plugin-configuration-on-the-plugins-page/decision: - en: edce6321f96653c1 - zh: b9af8efeff5a05ed + en: ccee8068ea810fcb + zh: c87c4e6c44b33a67 /agent-note-plugin-configuration-on-the-plugins-page/consequences: en: d3d131d6c46bc32e zh: eb7c06f119dfbe8d diff --git a/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.md b/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.md index 700ada1538..35aaefd028 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.md +++ b/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.md @@ -20,7 +20,7 @@ The page passes `view: 'page'` for each form. Official plugin cards and details **The four host-plane pages register while the Host serves their namespaces.** Each page's package registers it through `ctx.slots.inject` when the shared settings mirror shows the namespace and disposes it when the namespace goes, so a deployment that does not compose the owning plugin shows no trace of it; since [settings pages as companion packages](2026-09-17-settings-pages-as-companion-packages.md) that is one companion package per page, and `ui-settings-plugins` keeps the Settings section as the **Built-in plugins** shell around the inventory tab. The `settings.plugin.item` slot is retired. -**The Official group.** The optional bundles the installation ships open the group, tagged **Beta** where the feature is one (Agent Teams), with no official tag; the configuration pages follow. Auto review is a published experimental package used as the install guide's example, not an `OPTIONAL_BUNDLES` entry or a CLI dependency. +**The Official group.** The optional bundles the installation ships open the group, tagged **Beta** where the package is experimental, with no official tag; the configuration pages follow. The [optional-bundle decision for experimental capabilities](2026-09-21-experimental-capabilities-as-optional-bundles.md) names the entries. ## Consequences diff --git a/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.zh.md b/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.zh.md index 23935312d2..ccdaf5629a 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.zh.md @@ -20,7 +20,7 @@ Status: implemented **四个宿主平面页面在 Host 服务其命名空间期间注册。** 每个页面所在的包在共享的 settings 镜像显示某个命名空间时通过 `ctx.slots.inject` 注册对应页面,命名空间消失时销毁,因此没有组装该插件的部署不会留下它的痕迹;自[设置页作为伴生包](2026-09-17-settings-pages-as-companion-packages.zh.md)起,每个页面各是一个伴生包,`ui-settings-plugins` 把设置分区保留为清单标签页外面的**内置插件**外壳。`settings.plugin.item` slot 退役。 -**官方分组。** 安装随附的可选组合包开启这个分组,属于 beta 功能的(Agent Teams)带 **Beta** 标签,没有官方标签;配置页排在其后。Auto review 是安装引导用作示例的已发布实验包,不属于 `OPTIONAL_BUNDLES` 或 CLI 依赖。 +**官方分组。** 安装随附的可选组合包开启这个分组,属于实验包的带 **Beta** 标签,没有官方标签;配置页排在其后。[实验能力的可选 bundle 决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)列出这些条目。 ## 后果 diff --git a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.i18n.yaml index 41b02cd5ae..1f07fe4f8b 100644 --- a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.i18n.yaml @@ -9,8 +9,8 @@ en: 725f773bb350218e zh: dd8fbc09ef48208d /agent-note-declarative-agent-presets-and-retained-revisions/decision: - en: fa2ac428de2ba3d0 - zh: c75da98e1cf076ef + en: 2a2489303fc76fc0 + zh: 1a760e9a5edfccc8 /agent-note-declarative-agent-presets-and-retained-revisions/alternatives-considered: en: 09f40eab4659e9f6 zh: 7a70da0d0ae6845b diff --git a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.md b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.md index 55ec173b33..bf900bc4dc 100644 --- a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.md +++ b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.md @@ -16,7 +16,7 @@ The registry owns each revision's scope and Loader tree. Agents link their own s The Web editor and `agent_preset` tool accept only child plugin YAML. Saving writes the active profile's user patch, preserving display metadata and unrelated configuration. Revision checks and profile locking prevent overwriting concurrent saves; validation rejects a higher-priority override that would defeat the edit. Persistence and activation outcomes are reported separately. Tool writes require approval or full access because configuration executes in the Host. -Session data continues to record the preset identity. Restart resolves that identity against current configuration and rejects a missing definition. Old executable revisions are process-local, not serialized. The existing chooser policy and blank-session selection remain: hiding the chooser uses the deployment default without deleting the saved user preference. +Session data continues to record the preset identity. Restart resolves that identity against current configuration and rejects a missing definition. Old executable revisions are process-local, not serialized. The saved user default and blank-session selection remain; `selectedDefault` is what an unnamed new session resolves, and the Client's Developer tools preference decides only whether the choice is shown. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.zh.md b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.zh.md index 60c81de4d9..21611da37f 100644 --- a/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-18-declarative-agent-presets.zh.md @@ -16,7 +16,7 @@ Preset 目录重复承担 Cordis 配置的所有权。独立的发现、元数 Web 编辑器与 `agent_preset` 工具只接受子插件 YAML。保存写入当前 profile 的用户补丁,保留显示元数据和无关配置。版本检查和 profile 锁防止覆盖并发保存;验证拒绝会使编辑失效的更高优先级覆盖。持久化与激活结果分别报告。配置在 Host 中执行,因此工具写入需要批准或完全访问权限。 -会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复。旧的可执行代际仅保留在进程内,不会序列化。现有选择器策略和空白会话选择保持不变:隐藏选择器使用部署默认值,但不删除保存的用户偏好。 +会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复。旧的可执行代际仅保留在进程内,不会序列化。保存的用户默认值与空白会话选择保持不变:未指定 preset 的新会话解析 `selectedDefault`,客户端的开发者工具偏好只决定是否展示这项选择。 ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.i18n.yaml index fa28db5307..938a007c31 100644 --- a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.i18n.yaml @@ -9,11 +9,11 @@ en: 2e7f99c81fbc941f zh: b0fac7cd5eb86ffe /agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/problem/mechanism: - en: faa32b20dc128571 - zh: d01263a543be675d + en: 36d24e92eedf8cea + zh: 5dafc5758ee0f380 /agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/problem/the-cache-s-two-uses-and-where-validation-lives: - en: 7bec01a054dffbad - zh: e158b106a079a770 + en: 055ead18162ca1cd + zh: 9046bf04229e45d1 /agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/problem/the-client-store-cannot-guarantee-that-connected-data-replaces-hints: en: 597b346459a9d8b0 zh: f7282a40061d645c @@ -27,8 +27,8 @@ en: 1138b58239be186b zh: cf5fc5f1d3f21bd3 /agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/decision/consumers: - en: 5b8c1e5eb06f7d59 - zh: 4018838c3390342a + en: 743f2dceda3072aa + zh: f4d53ddc13a4127e /agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/decision/the-client-store-separates-cached-and-sequenced-rows: en: 8a4e9e9ac2ada06f zh: b5884535cd934f72 @@ -42,8 +42,8 @@ en: d71031c0dc256d80 zh: 753c948a90a0c9ad /agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/consequences: - en: c95faf40ca336afb - zh: 95d6030671113056 + en: 525ea6842ad2e19a + zh: 595da1db20255617 /agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/testing: - en: 20aef0e3d10b7f98 - zh: 478b2d22eb3fcc2a + en: 8faa78adc442992c + zh: 29cb78232985fd47 diff --git a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md index 61b82e5003..bfdd8bda22 100644 --- a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md +++ b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.md @@ -14,13 +14,12 @@ This is neither cache corruption nor a version mismatch. The projcache record's A cache record (record format and predecessor recovery: [Projection-cache predecessor recovery and Session-format binding](2026-09-02-projcache-cross-version-read-compat.md)) is bound to a lifecycle identity: `formatVersion + createdAt + cwd + isSeeded + inheritedEventCount`, matched by `identityMatches` on full equality. Since #3346, `inheritedEventCount` (the length of the event prefix a fork inherits, the cut below) no longer appears in the logical header: the header keeps only the `isSeeded` bit and the exact cut follows the body. Since Session format v2 (#3398) the physical header line no longer stores `seedLength` either; the reader derives the cut from the seq of the `session/end-seed {inherited: true}` marker in the body. -A header-only read therefore cannot obtain the cut: the JSONL backend's `fromHeaderLine` hard-codes `inheritedEventCount: 0` for header-only reads, and `SessionPersistenceSnapshot` carries only the header, the revision, and an optional eventCount. All three header-only cache consumers grew the same guard: +A header-only read therefore cannot obtain the cut: the JSONL backend's `fromHeaderLine` hard-codes `inheritedEventCount: 0` for header-only reads, and `SessionPersistenceSnapshot` carries only the header, the revision, and an optional eventCount. The Session-list and reference consumers grew the same guard: | Consumer | Guard | Fallback | Consequence | |---|---|---|---| | `packages/api/session-controller/src/list.ts` `projectionsFor` | `header.isSeeded ? undefined : cachedSnapshot(header, 0) ?? cachedPredecessorTitle(header, 0)` | none | no title, no `sessionListMetadata`; blank falls back to `false`, ordering falls back to `createdAt` | | `packages/context/session-reference/src/index.ts` `projectedLabels` | same | none | `@` completion labels by id; the title cannot be searched | -| `packages/subagent/subagent/src/list-children.ts` `resolveColdIdentity` | same | `observeSession` reads the body | correct result, one body read per seeded child | When the guard landed (#3346, 2026-09-01), `list.ts` still had `probeSmallCold`: on a cache miss it read the body when the log file was at most `DEFAULT_COLD_BLANK_PROBE_MAX_BYTES = 1024` bytes. Its purpose was blank-session detection; it covered only logs under 1KB and never applied to an ordinary fork. #3400 (2026-09-03) removed it and the list returned to metadata plus cache only. #4320 (2026-09-19) made forking a first-class feature (`packages/core/session/src/fork.ts`, fork at any seq); every fork is `isSeeded: true` with an exact cut, seeded sessions went from a handful to dozens, all of them cold after a restart, and the gap became visible at once. @@ -33,7 +32,7 @@ The read and write faces of `SessionProjectionCache` and their callers: | `write(session)` | writes the checkpoint at three mandatory points plus a throttle | yes | the cache's own listeners | | `hydratePrepared(session, events)` | uses `rows` as the fold starting point and applies events from `row.seq + 1`; the result lands in live cells | the next checkpoint | `session-query/src/observation.ts` | | `coldSnapshot(meta, cut, events)` | fold starting point plus write-back | yes | no production caller | -| `cachedSnapshot(meta, cut, keys?)` | `viewCheckpoint`: each row passes `ver` and `stateSchema`, is `view`ed, and returned | no | `list.ts`, `session-reference`, `list-children` | +| `cachedSnapshot(meta, cut, keys?)` | `viewCheckpoint`: each row passes `ver` and `stateSchema`, is `view`ed, and returned | no | `list.ts`, `session-reference` | | `cachedPredecessorTitle(meta, cut)` | the same, `title` only, allowing an older `formatVersion` | no | `list.ts` | Identity validation lives entirely inside the cache (`recordFor` → `identityMatches`; `viewCheckpoint` checks `ver` and schema per row). `list.ts` validates nothing itself; it merely declines to call when it cannot supply a cut. @@ -70,7 +69,6 @@ In the block the read-only face returns, `asOfSeq` is the lowest watermark among |---|---| | `list.ts` `projectionsFor` | drop the `isSeeded` branch and `SessionLogOffset(0)`; every cold row reads `cachedSnapshot(header) ?? cachedPredecessorTitle(header)` | | `session-reference` `projectedLabels` | same symptom, same change | -| `list-children.ts` `resolveColdIdentity` | follows the signature only; the `!header.isSeeded` guard and the body fallback stay, because it needs the cut to classify a seq as inherited or owned, which is fold semantics | ### The client store separates cached and sequenced rows @@ -147,7 +145,7 @@ Bought: Paid: -- `cachedSnapshot` and `cachedPredecessorTitle` change signature; three callers change with them. +- `cachedSnapshot` and `cachedPredecessorTitle` change signature; the Session-list and reference callers use the header-only signatures. - `SessionProjectionHints` gains the required field `kind`; every producer of a list summary and every test fixture that builds one carries it. - A hand-crafted record with the same four fields and a different cut is displayed in the list until the session is opened. - Keys the baseline omits clear together with their hints: when a Host does not mount `schedule`, the schedule mark the list hinted disappears after the session opens. Under "connected data is the truth" this is the correct behavior. @@ -163,4 +161,3 @@ Paid: - `api/session-controller/tests/manager.client.spec.ts`: a `cached` `api-session/added` block is replaced by a control baseline at the same cursor; a `cached` list block does not displace an existing sequenced title. - `api/session-controller/tests/inbox-projection.client.spec.ts`: unchanged; a live session's `sequenced` list block still outranks a delayed control baseline at a lower cut. - `context/session-reference/tests/session-reference.spec.ts`: a seeded cold session is labeled and searchable by its cached title, a session without a cache record is still labeled by id, and neither reads a log. -- `subagent/subagent/tests/list-children.spec.ts`: unchanged; seeded children still go through body observation. diff --git a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md index ef49d90bc6..470c96385c 100644 --- a/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md @@ -14,13 +14,12 @@ Host 进程重启后,所有 fork 出来的会话(`SessionHeader.isSeeded === 缓存记录(记录格式与前代恢复见 [投影缓存前代恢复与 Session 格式绑定](2026-09-02-projcache-cross-version-read-compat.zh.md))绑定一份 lifecycle identity:`formatVersion + createdAt + cwd + isSeeded + inheritedEventCount`,`identityMatches` 做全等匹配。其中 `inheritedEventCount`(fork 继承的事件前缀长度,下称 cut)从 #3346 起不再出现在逻辑 header 里:header 只保留 `isSeeded` 这一位,精确 cut 跟随正文。Session 格式 v2 起(#3398),物理 header 行也不再存 `seedLength`,reader 从正文里 `session/end-seed {inherited: true}` 标记的 seq 推出 cut。 -于是 header-only 的读取拿不到 cut:JSONL 后端 `fromHeaderLine` 对 header-only 读取硬编码 `inheritedEventCount: 0`,`SessionPersistenceSnapshot` 只有 header、revision 和可选的 eventCount。三个只读 header 的缓存消费者都因此加了同一个守卫: +于是 header-only 的读取拿不到 cut:JSONL 后端 `fromHeaderLine` 对 header-only 读取硬编码 `inheritedEventCount: 0`,`SessionPersistenceSnapshot` 只有 header、revision 和可选的 eventCount。Session 列表与引用消费者都因此加了同一个守卫: | 消费者 | 守卫 | 兜底 | 后果 | |---|---|---|---| | `packages/api/session-controller/src/list.ts` `projectionsFor` | `header.isSeeded ? undefined : cachedSnapshot(header, 0) ?? cachedPredecessorTitle(header, 0)` | 无 | 无 title,无 `sessionListMetadata`,blank 判定退到 `false`,排序退到 `createdAt` | | `packages/context/session-reference/src/index.ts` `projectedLabels` | 同 | 无 | `@` 补全按 id 显示,搜不到 title | -| `packages/subagent/subagent/src/list-children.ts` `resolveColdIdentity` | 同 | `observeSession` 读正文 | 结果正确,每个 seeded 子代一次正文读 | 守卫存在时(#3346,2026-09-01),`list.ts` 还有 `probeSmallCold`:缓存 miss 且日志文件不超过 `DEFAULT_COLD_BLANK_PROBE_MAX_BYTES = 1024` 字节时读正文补全。它的目的是识别 blank 会话,只覆盖不到 1KB 的日志,对正常 fork 从未生效。#3400(2026-09-03)删掉它,列表回到纯元数据加缓存。#4320(2026-09-19)把 fork 变成一等功能(`packages/core/session/src/fork.ts`,任意 seq fork),每个 fork 都是 `isSeeded: true` 加精确 cut,seeded 会话数量从个位数变成几十条,重启后全部冷,问题集中显形。 @@ -33,7 +32,7 @@ Host 进程重启后,所有 fork 出来的会话(`SessionHeader.isSeeded === | `write(session)` | 三个强制点加节流写 checkpoint | 有 | 缓存内部监听 | | `hydratePrepared(session, events)` | 把 `rows` 当 fold 起点,从 `row.seq + 1` 继续 apply,结果进 live cell | 后续 checkpoint 写回 | `session-query/src/observation.ts` | | `coldSnapshot(meta, cut, events)` | fold 起点加回写 | 有 | 无生产调用方 | -| `cachedSnapshot(meta, cut, keys?)` | `viewCheckpoint`:每行过 `ver` 和 `stateSchema`,`view` 后返回 | 无 | `list.ts`、`session-reference`、`list-children` | +| `cachedSnapshot(meta, cut, keys?)` | `viewCheckpoint`:每行过 `ver` 和 `stateSchema`,`view` 后返回 | 无 | `list.ts`、`session-reference` | | `cachedPredecessorTitle(meta, cut)` | 同上,只看 `title`,允许更旧的 `formatVersion` | 无 | `list.ts` | 身份校验全部在缓存内部(`recordFor` → `identityMatches`;`viewCheckpoint` 逐行校验 `ver` 与 schema)。`list.ts` 自己不校验,只是给不出 cut 时不调。 @@ -70,7 +69,6 @@ spec.ts 与 README 描述身份检查的目的时用的都是同一个动词: |---|---| | `list.ts` `projectionsFor` | 删 `isSeeded` 分支和 `SessionLogOffset(0)`,冷会话统一 `cachedSnapshot(header) ?? cachedPredecessorTitle(header)` | | `session-reference` `projectedLabels` | 同一症状,同一改法 | -| `list-children.ts` `resolveColdIdentity` | 只跟随签名,`!header.isSeeded` 守卫和正文兜底原样保留;它需要 cut 做 inherited/owned 分类,属于 fold 语义 | ### 客户端 store 区分 cached 与 sequenced 行 @@ -147,7 +145,7 @@ seq 比较只在同一条 Host 连接内发生:`handleConnected` 先整表 `cl 付出的: -- `cachedSnapshot` 与 `cachedPredecessorTitle` 签名变化,三个调用方同步修改。 +- `cachedSnapshot` 与 `cachedPredecessorTitle` 签名变化,Session 列表与引用调用方使用仅需 header 的签名。 - `SessionProjectionHints` 新增必填字段 `kind`,所有产出列表摘要的地方和构造摘要的测试夹具都要带上。 - 手工构造的同四字段、不同 cut 的记录会在列表上显示到会话被打开为止。 - 基线没带的键连提示一起清掉:某个 Host 未挂载 `schedule` 时,打开会话后列表提示过的 schedule 标记消失。按"建连数据是真值"这是正确行为。 @@ -163,4 +161,3 @@ seq 比较只在同一条 Host 连接内发生:`handleConnected` 先整表 `cl - `api/session-controller/tests/manager.client.spec.ts`:`api-session/added` 的 `cached` block 被同 cursor 的 control 基线替换;`cached` 列表 block 不覆盖已有的 sequenced title。 - `api/session-controller/tests/inbox-projection.client.spec.ts`:不变,活会话的 `sequenced` 列表 block 仍压过延迟到达、cut 更低的 control 基线。 - `context/session-reference/tests/session-reference.spec.ts`:seeded 冷会话按缓存 title 标注并可按 title 搜到,没有缓存记录的会话仍按 id 标注,两者都不读日志。 -- `subagent/subagent/tests/list-children.spec.ts`:不变,seeded 子代仍走正文观察。 diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml new file mode 100644 index 0000000000..e8573b2e72 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-21-experimental-capabilities-as-optional-bundles.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md +/agent-note-ship-experimental-capabilities-as-optional-bundles: + en: 9ec3d64c70d601b6 + zh: c85722cfc7bf70df +/agent-note-ship-experimental-capabilities-as-optional-bundles/problem: + en: 2ba6c43b20a3d347 + zh: 5647179ddaa38ad9 +/agent-note-ship-experimental-capabilities-as-optional-bundles/decision: + en: 1c5a43b3ce2d3056 + zh: b59f549a12cf67d1 +/agent-note-ship-experimental-capabilities-as-optional-bundles/alternatives-considered: + en: a65d84004e48b3ef + zh: 17221f08e7a8a89b +/agent-note-ship-experimental-capabilities-as-optional-bundles/consequences: + en: d52ed0a248131a37 + zh: 779b8d415281c5da diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md new file mode 100644 index 0000000000..a2a83645c9 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md @@ -0,0 +1,25 @@ +# Agent Note: Ship experimental capabilities as optional bundles + +Status: implemented + +English | [中文](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md) + +## Problem + +The Web plugin page offered two optional bundles, Agent Teams and voice input. Auto review and the Inspector were published experimental packages that a person had to install by name or mount through a hand-written profile patch, although both already declared a bundle patch or shipped a mountable overlay. + +## Decision + +`OPTIONAL_BUNDLES` lists Agent Teams, voice input, and Auto review. Every optional bundle declares `icon` and exports `./locale/*.json` with `meta.title` and `meta.description`, so the Official group renders a localized title, description, and image; `verify-default-product-isolation` rejects an optional bundle without them. The Inspector's `cordis.patch.yml` names the package instead of a built-artifact path, so the same file is the bundle patch and the `--patch` overlay for a built Web launch. + +An optional bundle is a runtime dependency of the installation, so its dependency graph is downloaded by every `dsh` install. The list therefore admits only packages whose graph is already in the installation's closure; Auto review adds nothing. The Inspector stays available through explicit installation and does not appear in the default plugin list. The browser-use and computer-use providers stay explicit compositions: Playwright MCP, Chrome DevTools MCP, and the native Cua Driver runtime binaries add about 85 MB and 21 packages to every install whether or not the bundle is enabled, and a shipped Cua Driver MCP switch would offer a capability whose executable the installation does not carry. Those provider packages keep locale display metadata for their component rows. Two more packages stay out for other reasons: `ptc-runtime-python` replaces the PTC runtime, and `workflow-ptc` rejects a non-TypeScript runtime at load while the Web presets carry `workflow-ptc` rows a bundle patch cannot reach; `browser-use-stagehand-native` requires a native model name and API key at schema validation with no configuration form on the page. + +## Alternatives considered + +**Ship every provider as an optional bundle.** Composes and displays correctly, but grows every install by provider runtimes for a capability most installations never switch on; voice input's `sherpa-onnx-node` is the one accepted precedent. + +**Mount the registration-only `computer-use` and `browser-use` services in `dsh-base`.** The shared composition would carry rows that only an optional provider bundle needs; a provider bundle can insert the service row from its own patch and dependencies. + +## Consequences + +The Official group grows from two to three entries, each tagged experimental by its experimental name. The runtime dependency closure of the installation is unchanged. A browser-use or computer-use provider still needs its Service Definition and the provider in a profile patch or composition. diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md new file mode 100644 index 0000000000..62e248e5ae --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md @@ -0,0 +1,25 @@ +# Agent Note: 将实验能力作为可选 bundle 发布 + +Status: implemented + +[English](2026-09-21-experimental-capabilities-as-optional-bundles.md) | 中文 + +## 问题 + +Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto review 与 Inspector 都是已发布的实验包,用户必须按名称安装或手写 profile patch 才能挂载,尽管两者已经声明了 bundle patch 或附带可挂载的 overlay。 + +## 决策 + +`OPTIONAL_BUNDLES` 列出 Agent Teams、语音输入与 Auto review。每个可选 bundle 都声明 `icon` 并导出带 `meta.title` 与 `meta.description` 的 `./locale/*.json`,官方分组因此能渲染本地化标题、描述和图片;`verify-default-product-isolation` 会拒绝缺少这些声明的可选 bundle。Inspector 的 `cordis.patch.yml` 直接写包名而非构建产物路径,所以同一文件既是 bundle patch,也是已构建 Web 启动的 `--patch` overlay。 + +可选 bundle 是安装的运行时依赖,其依赖图会随每次 `dsh` 安装一起下载。因此名单只接纳依赖图已在安装闭包内的包;Auto review 不增加任何依赖。Inspector 保留显式安装方式,不在默认插件列表中显示。浏览器操作与电脑操作提供方仍保持显式组合:Playwright MCP、Chrome DevTools MCP 与原生 Cua Driver 运行时二进制会让每次安装多出约 85 MB、21 个包,无论 bundle 是否启用;而随附的 Cua Driver MCP 开关会提供一个安装本身并不携带其可执行文件的能力。这些提供方包保留 locale 显示元数据,供组件行使用。另有两个包因其他原因不纳入:`ptc-runtime-python` 会替换 PTC 运行时,而 `workflow-ptc` 在加载时拒绝非 TypeScript 运行时,且 Web preset 内含 bundle patch 无法触及的 `workflow-ptc` 行;`browser-use-stagehand-native` 在 schema 校验时就要求原生模型名称与 API 密钥,而插件页没有对应的配置表单。 + +## 考虑过的替代方案 + +**把所有提供方都做成可选 bundle。** 组合与展示都正确,但会为多数安装从不启用的能力把提供方运行时塞进每次安装;语音输入的 `sherpa-onnx-node` 是唯一被接受的先例。 + +**在 `dsh-base` 中挂载仅负责注册的 `computer-use` 与 `browser-use` 服务。** 共享组合将携带只有可选提供方 bundle 才需要的行;提供方 bundle 可以从自己的 patch 和依赖插入服务行。 + +## 后果 + +官方分组从两项增至三项,每项都因实验包名而带实验性标签。安装的运行时依赖闭包保持不变。浏览器操作或电脑操作提供方仍需在 profile patch 或组合中同时挂载其 Service Definition 与提供方。 diff --git a/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml new file mode 100644 index 0000000000..fec9a85467 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml @@ -0,0 +1,22 @@ +# Bilingual-pair consistency record for 2026-09-24-web-default-schedule-composition.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.md +/agent-note-schedule-in-the-shipped-web-composition: + en: 39229d7ab6ed433b + zh: 54287471d371a11c +/agent-note-schedule-in-the-shipped-web-composition/problem: + en: 2871fd5ed881c007 + zh: d03c78d402d186da +/agent-note-schedule-in-the-shipped-web-composition/decision: + en: bb19ad12b2ab0ffc + zh: 971f33e95474a36c +/agent-note-schedule-in-the-shipped-web-composition/recorded-sessions: + en: 5539a42052f4ddb7 + zh: 98cc190ae63b4549 +/agent-note-schedule-in-the-shipped-web-composition/alternatives-considered: + en: ca032a407c2ee8cb + zh: 748a4fcf2485a3da +/agent-note-schedule-in-the-shipped-web-composition/consequences: + en: 4f23bda8fda37987 + zh: 9f7c4a3538bd2979 diff --git a/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.md b/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.md new file mode 100644 index 0000000000..91f533950a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.md @@ -0,0 +1,39 @@ +# Agent Note: Schedule in the shipped Web composition + +Status: implemented + +English | [中文](2026-09-24-web-default-schedule-composition.zh.md) + +## Problem + +The Automation tasks page, the Session reminder catalog, and the `schedule_*` tools reached a Web deployment only through `--patch apps/cli/config/examples/schedule/cordis.yml`. `packages/bundle/web-app/cordis.patch.yml` carried the `ui-schedule` client row with `disabled: true`, and the Host rows `time-context` and `schedule` existed in that overlay alone. A person running the shipped `web` profile therefore saw no Automation tasks entry and no reminder tools, while every consumer that wanted them repeated the same three rows: the repository preview image kept its own overlay list, and two Web suites hard-coded the overlay path. + +## Decision + +`packages/bundle/web-app/cordis.patch.yml` inserts `time-context` and `schedule` in its Host row list and leaves `ui-schedule` enabled; `packages/bundle/web-app/package.json` declares both packages, which `verify-cordis-config` requires for a bare row name in a bundle patch. The `web` profile consequently ships the Automation tasks page, the Session-header reminder clock, the idle Session row's clock mark and hover list, the right-Sidebar task tab, and `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` on every live root Agent. + +`apps/cli/config/examples/schedule/cordis.yml` is deleted. `applyEntryPatches` appends an `insert` list without de-duplicating ids, so keeping the overlay would mount `time-context` and `schedule` a second time; the two Web suites and the preview packer that named the overlay compose the shipped profile alone. + +`time-context` ships with Schedule because a reminder request states a wall-clock target. The plugin appends one durable user message per eligible step carrying the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. The sampling instant is what lets the model turn a request such as "tomorrow at nine" into an offset-bearing `at` value; the [Schedule subsystem](../../../../docs/subsystems/schedule.md) owns that interpretation boundary and the explicit-zone requirement it feeds. + +The Host service, its storage domain, and the client half are unchanged. This decision moves which composition mounts them, and the [Host-owned scheduled messages decision](2026-09-16-host-schedule-storage.md) continues to own task storage, activation, dispatch, and delivery records. + +## Recorded sessions + +A Web scenario that drives a live step now logs one time-context reading per step, so its committed Session fixture and header sidecars were refreshed. A reading's sampled instant, browser zone, and elapsed duration are volatile, and `normalizeWebSessionVolatiles` in `apps/web/tests/scaffold.ts` replaces them with `{{timeContextTimestamp}}`, `{{clientTimeZone}}`, and `{{elapsed}}`. The turn and step numbers and the preceding-event baseline stay readable, so a fixture still shows what the model received. Refresh writes the current writer generation (`session.v4.jsonl`) beside the retained predecessors, which remain committed replay baselines. + +## Alternatives considered + +**Keep the overlay and change nothing.** The shipped surface would keep contradicting the product documentation that describes the Automation tasks entry, and each consumer would keep its own copy of the three rows. The overlay also cannot satisfy the request that the default `dsh web` process exposes the page without a flag. + +**Ship the Schedule rows without `time-context`.** The model would then have no sampling instant of its own, so an unqualified date or time in a reminder request would depend on a separate clock source; the overlay paired the two rows for that reason. + +**Reach for a separate optional bundle.** `OPTIONAL_BUNDLES` exists for bundles a person switches on from the plugin manager, which is the same opt-in shape as the overlay; the decision here is that a default Web surface owns the page and the tools. + +## Consequences + +- Every Web session's request header carries four additional tool schemas, and every eligible step appends one durable user message. A conversation that never creates a reminder pays that token cost. +- The clock reading is model-visible and durable, so it replays, compacts, and appears in exported Session logs like any other user message. +- A deployment that wants the previous behavior disables the `time-context`, `schedule`, and `ui-schedule` rows in its own profile patch layer; the capability itself is untouched. +- The `cordis_inspect_query` `listTools` answer for a full preset table now passes the base composition's 12,500-token inline budget, so the spill policy retains that answer's head and tail with a spill path instead of the complete JSON. +- The repository preview image and both Web suites drop their overlay arguments, so one composition change reaches every surface at once. diff --git a/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.zh.md b/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.zh.md new file mode 100644 index 0000000000..af561fe63e --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.zh.md @@ -0,0 +1,39 @@ +# Agent Note: 默认 Web 组合中的 Schedule + +Status: implemented + +[English](2026-09-24-web-default-schedule-composition.md) | 中文 + +## Problem + +自动化任务页面、Session 提醒目录以及 `schedule_*` 工具此前只能通过 `--patch apps/cli/config/examples/schedule/cordis.yml` 进入某个 Web 部署:`packages/bundle/web-app/cordis.patch.yml` 里的 `ui-schedule` 客户端行带 `disabled: true`,而宿主行 `time-context` 与 `schedule` 只存在于那个 overlay 中。因此运行发布版 `web` profile 的人既看不到自动化任务入口,也拿不到提醒工具;同时每个需要它们的消费者都要重复同样的三行:仓库预览镜像自行维护了一份 overlay 列表,两个 Web 测试套件各自硬编码了 overlay 路径。 + +## Decision + +`packages/bundle/web-app/cordis.patch.yml` 在其宿主行列表中插入 `time-context` 与 `schedule`,并让 `ui-schedule` 保持启用;`packages/bundle/web-app/package.json` 声明这两个包,这是 `verify-cordis-config` 对 bundle patch 中裸包名的要求。因此 `web` profile 会交付自动化任务页面、Session 页头的提醒时钟、空闲 Session 行的时钟标记与悬停列表、右侧栏任务页签,并在每个 live 根 Agent 上注册 `schedule_create`、`schedule_list`、`schedule_update` 与 `schedule_delete`。 + +`apps/cli/config/examples/schedule/cordis.yml` 已删除。`applyEntryPatches` 追加 `insert` 列表时不对 id 去重,保留该 overlay 会把 `time-context` 与 `schedule` 挂载两次;原本引用它的两个 Web 测试套件与预览打包器现在只组合发布版 profile。 + +`time-context` 随 Schedule 一起发布,因为提醒请求本身给出的是墙上时钟目标。该插件在每个符合条件的步骤追加一条持久 user 消息,包含采样瞬时、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。正是采样瞬时让模型能把「明天九点」这类请求落成带偏移量的 `at` 值;该解释边界及其要求的显式时区由 [Schedule 子系统](../../../../docs/subsystems/schedule.zh.md)负责。 + +宿主服务、其存储 domain 与客户端半边都没有改动。本决策改变的是由哪个组合挂载它们,任务存储、激活、投递与投递记录仍由[宿主定时消息决策](2026-09-16-host-schedule-storage.zh.md)负责。 + +## Recorded sessions + +驱动 live 步骤的 Web 场景现在每步记录一条 time-context 读数,因此其已提交的 Session 夹具与页头 sidecar 都已重刷。一条读数中的采样瞬时、浏览器时区与经过时长是易变值,`apps/web/tests/scaffold.ts` 的 `normalizeWebSessionVolatiles` 会把它们替换为 `{{timeContextTimestamp}}`、`{{clientTimeZone}}` 和 `{{elapsed}}`。turn 与 step 序号以及"前一条事件"的基线保持可读,因此夹具仍能显示模型收到的内容。重刷会在已保留的上一代旁写入当前 writer 代次(`session.v4.jsonl`),旧代次继续作为已提交的回放基线保留。 + +## Alternatives considered + +**保留 overlay,不做任何改动。** 发布版界面会继续与描述该自动化任务入口的产品文档相矛盾,而每个消费者都要各自保留那三行的副本。overlay 也无法满足「默认 `dsh web` 进程不带 flag 就暴露该页面」这一要求。 + +**只发布 Schedule 两行,不带 `time-context`。** 模型将没有自己的采样瞬时,于是提醒请求中未明确限定时区的日期或时间要依赖另一个时钟来源;overlay 当初正是为此把两行配对。 + +**另建一个可选 bundle。** `OPTIONAL_BUNDLES` 面向的是用户在插件管理器里主动开启的 bundle,与 overlay 是同一种 opt-in 形态;本决策要的是默认 Web 界面自己拥有该页面与这些工具。 + +## Consequences + +- 每个 Web 会话的请求头多出四个工具 schema,且每个符合条件的步骤追加一条持久 user 消息。从不创建提醒的对话也要承担这份 token 成本。 +- 时钟读数对模型可见且持久,因此它与其他 user 消息一样参与回放、压缩,并出现在导出的 Session 日志中。 +- 想要恢复旧行为的部署,可在自己的 profile patch 层禁用 `time-context`、`schedule` 与 `ui-schedule` 三行;能力本身没有改动。 +- 完整预设表的 `cordis_inspect_query` `listTools` 答案现在超过基础组合 12,500 token 的内联预算,因此 spill 策略改为保留该答案的首尾并给出 spill 路径,而不是完整 JSON。 +- 仓库预览镜像与两个 Web 测试套件都不再传 overlay 参数,一处组合改动即同时作用于所有界面。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.i18n.yaml index 1ca3737cda..270547a0cb 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.i18n.yaml @@ -21,8 +21,8 @@ en: 181653d75354df80 zh: 7984321c5a2715d5 /agent-note-session-opening-cache-identity-and-read-ordering/implementation-scope: - en: 84310b075ac5537b - zh: 6bf16dbf06353a39 + en: f3e37b63e2e813e3 + zh: 1c709bd0ee6cc3ff /agent-note-session-opening-cache-identity-and-read-ordering/alternatives-considered: en: 7d64c8151ffef2a0 zh: b899a22f4d070a78 diff --git a/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.md b/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.md index a4907df1d7..c00f8fa7a3 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.md +++ b/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.md @@ -51,7 +51,7 @@ A known address does not waive authorization checks. When opening child history, - Session formats, migrations, the Host observation's `all | none` policy, and the response fields of `session.projections` are unchanged. - There is no global cold-read singleflight, and Agent lookups are not universally replaced with read-only lookups. - `SessionManager.handleConnected()` still refreshes previously requested catalogs in bulk; this decision does not guarantee one cold read on reconnect. -- Tool-side `listDescendants` can still read child Sessions on cache misses, and expanding a child node in the Client can still cold-read that node. Ordinary main-session opening is not equivalent to every descendant-enumeration scenario. +- Tool-side `listDescendants` observes each reachable child catalog. An observation can reuse live state or a valid prepared Session; otherwise it reads the cold log. Expanding a child node in the Client can also cold-read that node. Ordinary main-session opening is not equivalent to every descendant-enumeration scenario. - `@` Session candidates still enumerate headers and obtain names from live projections or projection caches, falling back to ids on misses. Existing ranking, the default limit of 50, and direct-subagent grouping remain unchanged; discovery does not recursively traverse `subagentCatalog`. ## Alternatives considered diff --git a/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.zh.md b/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.zh.md index 759a45d8c5..ce9eac01db 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-09-20-session-open-read-coordination.zh.md @@ -51,7 +51,7 @@ Skill 保留共享目录请求自己的取消信号;`@` 查询将当前候选 - 没有修改 Session 格式、迁移、Host observation 的 `all | none` 策略或 `session.projections` 返回字段。 - 没有实现全局 cold-read singleflight,也没有把所有 Agent lookup 改成只读 lookup。 - 没有删除 `SessionManager.handleConnected()` 对此前请求目录的批量刷新;本决策不保证重连只有一次冷读。 -- 工具侧 `listDescendants` 的缓存缺失仍可能读取子会话;前端展开一个子节点也仍可能冷读该节点。普通主会话首开不等于所有后代枚举场景。 +- 工具侧 `listDescendants` 观察每个可达子级目录。观察可以复用实时状态或有效的 prepared Session,否则会读取冷日志。前端展开子节点也可能冷读该节点。普通主会话首开不等于所有后代枚举场景。 - `@` 会话候选仍枚举 header,并从 live projection 或 projection cache 取名称;缓存缺失回退到 id。原有排序、默认 50 条上限和直接 subagent 分组规则不变,没有改成沿 `subagentCatalog` 递归发现。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml index 6b8dc5e1c0..07e39db02d 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml @@ -18,8 +18,8 @@ en: a6d7884e4359af3a zh: e72588705942000a /agent-note-the-approval-seam-one-shot-permission-decisions-over-a-waterfall-of-answerers/decision/design-detail/the-seam-mechanism-and-policy-split: - en: 0aa6f3a90921d1ea - zh: c4a734f84800dae8 + en: 53da7800d8744d9e + zh: 316fef884853ca5d /agent-note-the-approval-seam-one-shot-permission-decisions-over-a-waterfall-of-answerers/decision/design-detail/ask-routing-in-dsh-tools: en: 1d4478059f608fd2 zh: 4a1d12e4ccb766b5 diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md index 03a4269dd7..bb44957b6d 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md @@ -55,7 +55,7 @@ After validation and a successful `approval/asked` append, the service resolves Answerers are `approval/request` waterfall listeners. Zero listeners fall through to `unavailable`; a recognizing listener occupies the first-wins decision slot, while an unrecognized agent must delegate with `next()`. Listeners dispose with their fibers, so an unloaded channel fails closed. Because sibling registration order is not deterministic, a deployment composes one terminal answerer and reserves `prepend` for decide-or-delegate gates. -`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, and optional `signal`. It uses the `ToolCallId` brand without importing `dsh-tools`, which depends on this seam. Channel adapters correlate any richer call state by `callId`; the approval request does not duplicate tool arguments. +`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, optional localized `displayReason`, and optional `signal`. It uses the `ToolCallId` brand without importing `dsh-tools`, which depends on this seam. Channel adapters correlate any richer call state by `callId`; the approval request does not duplicate tool arguments. #### Ask routing in dsh-tools diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md index f11e0956c9..b7e07a376c 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md @@ -55,7 +55,7 @@ tool/result "escalated" — this one call ran under the wider mode; the gra 应答者是 `approval/request` waterfall 监听器。零监听器会直接落到 `unavailable`;识别该 agent 的监听器占用先到先得的决策槽,而不识别的监听器必须调用 `next()` 委派。监听器会随其 fiber 一同 dispose(资源释放),因此卸载通道后,请求会在故障时默认被拒绝。由于兄弟插件的注册顺序不确定,部署应组合一个终端应答者,并保留 `prepend` 给「决策或委派」门禁。 -`ApprovalRequest` 携带发起请求的 `agent`、`toolName`、可选的精确 `callId`、人类可读的 `reason` 和可选的 `signal`。它使用 `ToolCallId` brand 而不导入依赖本 seam 的 `dsh-tools`。通道适配器可按 `callId` 关联任何更丰富的调用状态;审批请求本身不重复携带工具参数。 +`ApprovalRequest` 携带发起请求的 `agent`、`toolName`、可选的精确 `callId`、人类可读的 `reason`、可选的本地化 `displayReason` 和可选的 `signal`。它使用 `ToolCallId` brand 而不导入依赖本 seam 的 `dsh-tools`。通道适配器可按 `callId` 关联任何更丰富的调用状态;审批请求本身不重复携带工具参数。 #### dsh-tools 中的 Ask 路由 diff --git a/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.i18n.yaml b/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.i18n.yaml index a030f39da0..8862a5bc60 100644 --- a/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.i18n.yaml @@ -15,8 +15,8 @@ en: e7e953783cc4a332 zh: be5d47a503b47170 /agent-note-continuable-subagent-current-turn-interrupt/consequences: - en: 4a7ea28d02c66b63 - zh: bad8d6e58b45bd81 + en: 8e75516b07cdc842 + zh: 10e31033e14241e7 /agent-note-continuable-subagent-current-turn-interrupt/testing: en: e46413bc89f9238e zh: 4fb1639ae696e9a4 diff --git a/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.md b/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.md index e865415bbb..3da06ff860 100644 --- a/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.md +++ b/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.md @@ -41,7 +41,7 @@ The address-only RPC exposes one bit of live residency: an absent target is acce The Web surface keeps Send and Stop as independent actions for a running continuable child: the client `Session.cancel()` routes Stop through `subagent.interrupt` (one-shot addresses stay uncancellable, ordinary sessions keep their existing primary Send/Stop toggle through `session.cancel`), while Send delivers follow-ups through the busy-Enter setting's mode — Queue by default, Steer when selected — exactly as plain Enter does ([busy Send button](../bug-fix/2026-09-04-busy-send-button-follows-enter-setting.md)). A running parent-offline continuable child keeps the default composer with input and Send disabled but Stop reachable, returning to the read-only takeover once it stops ([Web subagent conversations](2026-07-27-web-subagent-conversations.md) owns the surrounding catalog and composer contract). -The model-facing `interrupt_agent(agent_id)` tool in `dsh-tool-subagent-control` passes `exec.agent` as the `ancestor` authority and adds none of its own: the core primitive verifies live registry identity and recorded lineage, so the tool can name a direct child or a deeper descendant with the same generic `agent_id` parameter — deliberately not `subagent_id`, which would imply direct children only. Discovery rides `list_agents({ scope: 'descendants' })` over the new `SubagentRuntime.listDescendants()` one-trace pre-order walk with verified `parentId`/`depth` per entry ([durable catalog note](../../archived/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md) owns the listing contract); discovery is a hint, never authority. `send_message` keeps its exact-direct-parent authority — only interrupt is ancestor-wide. +The model-facing `interrupt_agent(agent_id)` tool in `dsh-tool-subagent-control` passes `exec.agent` as the `ancestor` authority and adds none of its own: the core primitive verifies live registry identity and recorded lineage, so the tool can name a direct child or a deeper descendant with the same generic `agent_id` parameter — deliberately not `subagent_id`, which would imply direct children only. Discovery rides `list_agents({ scope: 'descendants' })` over the `SubagentRuntime.listDescendants()` recursive catalog walk with `parentId`/`depth` per entry ([parent-catalog note](../architecture/2026-09-01-parent-owned-subagent-catalog.md) owns discovery); discovery is a hint, never authority. `send_message` keeps its exact-direct-parent authority — only interrupt is ancestor-wide. ## Testing diff --git a/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.zh.md b/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.zh.md index ed4981bdac..0ef423905c 100644 --- a/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.zh.md +++ b/.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.zh.md @@ -41,7 +41,7 @@ Host RPC `subagent.interrupt` 接收 continuable 的 `SubagentAddress` 并返回 在 Web 侧,正在运行的 continuable child 使用相互独立的 Send 与 Stop 操作:客户端 `Session.cancel()` 将 Stop 路由到 `subagent.interrupt`(one-shot 地址保持不可取消,普通会话仍通过 `session.cancel` 保留既有的 primary Send/Stop 切换),同时 Send 按繁忙态 Enter 设置的模式投递后续消息——默认 Queue,选择后为 Steer——与 plain Enter 完全一致([繁忙态 Send 按钮](../bug-fix/2026-09-04-busy-send-button-follows-enter-setting.zh.md))。parent 离线但仍在运行的 continuable child 保留默认 composer,禁用输入区与 Send,但 Stop 仍然可达;停止后恢复为只读接管界面(周边目录与 composer 约定由 [Web subagent 对话](2026-07-27-web-subagent-conversations.zh.md)拥有)。 -`dsh-tool-subagent-control` 中面向模型的 `interrupt_agent(agent_id)` 工具把 `exec.agent` 作为 `ancestor` 授权传入,自身不增加任何权限:核心原语校验在线注册表身份与记录的 lineage,因此该工具可以用同一个通用 `agent_id` 参数指定直接 child 或更深的后代——刻意不用会暗示仅限直接 child 的 `subagent_id`。发现依赖 `list_agents({ scope: 'descendants' })`,其底层是新的 `SubagentRuntime.listDescendants()` 单次追踪 pre-order 遍历,每个条目带经校验的 `parentId`/`depth`(列表约定由[持久化目录 note](../../archived/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)拥有);发现只是提示,绝非权限。`send_message` 保持其确切直接 parent 权限——只有中断是 ancestor 级的。 +`dsh-tool-subagent-control` 中面向模型的 `interrupt_agent(agent_id)` 工具把 `exec.agent` 作为 `ancestor` 授权传入,自身不增加任何权限:核心原语校验在线注册表身份与记录的 lineage,因此该工具可以用同一个通用 `agent_id` 参数指定直接 child 或更深的后代——刻意不用会暗示仅限直接 child 的 `subagent_id`。发现依赖 `list_agents({ scope: 'descendants' })`,其底层是 `SubagentRuntime.listDescendants()` 递归目录遍历,每个条目带 `parentId`/`depth`(发现机制由[父目录 note](../architecture/2026-09-01-parent-owned-subagent-catalog.zh.md)拥有);发现只是提示,绝非权限。`send_message` 保持其确切直接 parent 权限——只有中断是 ancestor 级的。 ## 测试 diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml index 490c2cd775..68731414ce 100644 --- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml @@ -9,8 +9,8 @@ en: e22ee243b1503319 zh: b53c1a932d547f55 /agent-note-shared-developer-tool-settings/decision: - en: 3fc14af35b025970 - zh: 8359262a5c5d3805 + en: 62f71b49af7b9971 + zh: f0f12da4cb0c7fc0 /agent-note-shared-developer-tool-settings/alternatives-considered: en: df1b80e5b8d2a178 zh: 0136d16fc44d57bf diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md index 5ae8861efa..4d9f0fea89 100644 --- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md @@ -12,9 +12,9 @@ Diagnostic views, preset selection and changed-file summaries add complexity to The settings-owned `ctx.configForms.developerTools` preference uses a shared entry form and exposes one accepted `ui-settings.enabled` preference. The initial default-off choice reduces routine-task complexity; [Developer tools default on after preference resolution](2026-09-20-developer-tools-default-on.md) supersedes that default and owns startup enablement. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; loopback Web and desktop follow Host acceptance. Remote Web owns one browser-local observable shared by all consumers until reload, with no Host writes. -Off hides the Trajectory View, hides tool Inspect actions when no visible View declares tool-call inspection, hides the conversation View tab bar while fewer than two Views are available, hides the new-session preset chip and the settings row that allows switching Agent modes, and omits the changed-files card and its summary read. Views registered by other plugins stay available. Turning it off while Trajectory is active activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization. +Off hides the Trajectory View, hides tool Inspect actions when no visible View declares tool-call inspection, hides the conversation View tab bar while fewer than two Views are available, hides the new-session preset chip and refuses the Agent-preset cards' selection, and omits the changed-files card and its summary read. Views registered by other plugins stay available. Turning it off while Trajectory is active activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization. -Plugin assembly translates the shared preference into component-specific capabilities (`showCodeDiff`, `showPresetPicker`, and `interactivePreview`). Conversation supplies the inspection callback using the visible target's `toolCallFocus`; Chat does not name the target. The builtin HTML renderer uses DOMPurify for complete-document sanitization and parser-mutation defenses. Off preserves root styling, removes active documents, resource hints, SVG animation and all `href` attributes, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access. +Plugin assembly translates the shared preference into component-specific capabilities (`showCodeDiff`, the Agent-preset surfaces' `developerTools`, and `interactivePreview`). Conversation supplies the inspection callback using the visible target's `toolCallFocus`; Chat does not name the target. The builtin HTML renderer uses DOMPurify for complete-document sanitization and parser-mutation defenses. Off preserves root styling, removes active documents, resource hints, SVG animation and all `href` attributes, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md index d6e8e138a3..ac5829ed95 100644 --- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md @@ -12,9 +12,9 @@ Status: implemented 设置域内的 `ctx.configForms.developerTools` 偏好对象使用共享条目表单,提供一个已接受的 `ui-settings.enabled` 偏好。最初默认关闭的选择用于降低日常任务的复杂度;[偏好解析后默认开启开发者工具](2026-09-20-developer-tools-default-on.zh.md)取代该默认值并负责启动时的启用策略。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;回环 Web 和桌面端跟随 Host 接受的状态。远程 Web 持有一个所有消费者共享的浏览器本地可观察值,刷新后重置,不发送 Host 写入。 -关闭时隐藏 Trajectory View,在没有可见 View 声明工具调用检查能力时隐藏 Inspect 操作,可见 View 少于两个时隐藏会话 View 标签栏,隐藏新会话预设入口和「允许切换 agent 模式」设置行,并省略改动文件卡片及其摘要读取。其他插件注册的 View 保持可用。在 Trajectory 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。 +关闭时隐藏 Trajectory View,在没有可见 View 声明工具调用检查能力时隐藏 Inspect 操作,可见 View 少于两个时隐藏会话 View 标签栏,隐藏新会话预设入口并拒绝 Agent 预设卡片的选择操作,并省略改动文件卡片及其摘要读取。其他插件注册的 View 保持可用。在 Trajectory 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。 -插件组装层将共享偏好转换为组件自身的能力(`showCodeDiff`、`showPresetPicker` 和 `interactivePreview`)。Conversation 根据可见目标的 `toolCallFocus` 提供检查回调;Chat 不再指定目标名称。内置 HTML 渲染器使用 DOMPurify 清理完整文档并防护解析变异。关闭时保留根节点样式,移除主动文档、资源提示、SVG 动画及所有 `href` 属性,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。 +插件组装层将共享偏好转换为组件自身的能力(`showCodeDiff`、Agent 预设各界面读取的 `developerTools`,以及 `interactivePreview`)。Conversation 根据可见目标的 `toolCallFocus` 提供检查回调;Chat 不再指定目标名称。内置 HTML 渲染器使用 DOMPurify 清理完整文档并防护解析变异。关闭时保留根节点样式,移除主动文档、资源提示、SVG 动画及所有 `href` 属性,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。 ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.i18n.yaml b/.agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.i18n.yaml new file mode 100644 index 0000000000..0e8511d871 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-20-focus-ring-brand-primary.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.md +/agent-note-one-keyboard-focus-colour-for-every-control: + en: 967e19daff3df9bc + zh: 81e1a21d4204eb8a +/agent-note-one-keyboard-focus-colour-for-every-control/problem: + en: 2f3f77b1de539959 + zh: a4fa146b1d9ddecf +/agent-note-one-keyboard-focus-colour-for-every-control/decision: + en: 613ec184df6bf5d5 + zh: e4b3f95f4d33a54c +/agent-note-one-keyboard-focus-colour-for-every-control/alternatives-considered: + en: 392968102d520d1d + zh: e7b7b68f145cf3d3 +/agent-note-one-keyboard-focus-colour-for-every-control/consequences: + en: 25b9933c3d43ad76 + zh: 75d374ce3b7ec2eb diff --git a/.agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.md b/.agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.md new file mode 100644 index 0000000000..8212eaf529 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.md @@ -0,0 +1,43 @@ +# Agent Note: One keyboard-focus colour for every control + +Status: implemented + +English | [中文](2026-09-20-focus-ring-brand-primary.zh.md) + +## Problem + +Different explicit focus colours and browser-default outlines do not provide a consistent keyboard-focus cue. Browser defaults can also depend on the operating-system accent. + +Focus feedback has separate interaction and geometry constraints. A browser's `:focus-visible` heuristic can reveal a pointer-focused button after a key that does not move focus, while editable text controls need focus feedback on click. Containers that clip their contents can hide an outer ring. + +## Decision + +[The theme's focus stylesheet](../../../../packages/client/ui-theme/src/styles/focus.css) owns the shared ring colour and standard width. [Input modality](../../../../packages/client/ui-primitives/README.md#input-modality) distinguishes tooltip input from keyboard focus navigation. + +**One colour.** Global and component `:focus-visible` outlines and focus-ring shadows read `--dsw-focus-ring-color`, falling back to `--dsw-alias-state-business-primary`. The global fallback names the colour and the standard width but never the style, so it cannot create an outline where the component disables one. Naming the width is what keeps a control with no ring of its own at the standard geometry instead of Chromium's `auto 1px`. + +**Context-owned geometry.** `--dsw-focus-ring-width` carries the standard 2px width. Dense tables and toolbars may keep 1px so the ring does not dominate compact content. Components own their offsets and use an inset ring where an outer ring would be clipped. + +**Pointer focus does not imply keyboard navigation.** In pointer modality, `:focus-visible:not(:read-write)` controls use a transparent ring colour. Descendant and pseudo-element rings inherit it. Only focus-ring paint is suppressed: selected-state borders and elevation shadows remain, and editable text controls keep their own focus feedback on click. DOM focus stays on the control. + +Tooltips follow the last input and treat any key as keyboard input. Rings instead resume keyboard styling on non-composing navigation keys, or on focus reaching a different control after a non-composing key. Refocusing the same control is not navigation; pointer input, composition keys, and window blur clear the pending key. + +## Alternatives considered + +**Rewrite only the component declarations.** Explicit rings would agree, but controls using a browser-default outline would retain its colour. Requiring every control to declare its own ring would also leave new controls inconsistent until their authors add that rule. + +**Unify width and offset globally.** Positive offsets can be clipped by a scrolling ancestor, while a uniform thick ring can overwhelm dense content. Neither colour consistency nor input modality requires identical geometry. + +**Suppress by moving focus instead of by style.** Calling `blur()` on a pointer-focused button would hide its ring but also remove the target for subsequent Enter or Space activation. + +**Clear all shadows on pointer focus.** A `box-shadow` can contain elevation or selected-state paint as well as a focus ring. Suppressing its ring colour preserves those independent cues; exempting `:read-write` preserves text-entry feedback. + +**Replace every `outline: none` with a shared ring.** Some controls use an inset shadow or container-level focus treatment instead; some containers deliberately show no ring. Replacing those declarations would change their focus treatment, not merely its colour. + +**Give tooltips and rings the same modality rule.** A key can justify a tooltip on subsequent programmatic focus without justifying a ring on the same pointer-focused button. Keeping separate answers preserves both behaviours. + +## Consequences + +Focus rings share a theme colour while components retain their geometry and non-focus state cues. Ring visibility changes without changing the keyboard's active target. These rules do not guarantee identical browser rendering or eliminate component-specific clipping. + +[Stylesheet-order checks](../../../../packages/client/ui-theme/tests/client-styles.client.spec.ts), [focus-style checks](../../../../packages/client/ui-theme/tests/focus-ring-styles.client.spec.ts), and [input-modality tests](../../../../packages/client/ui-primitives/tests/input-modality.client.spec.ts) cover stylesheet declarations and modality transitions. Static declarations and synthetic events do not establish rendered ring visibility or cross-platform behaviour. diff --git a/.agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.zh.md b/.agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.zh.md new file mode 100644 index 0000000000..479e2d0efd --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-20-focus-ring-brand-primary.zh.md @@ -0,0 +1,43 @@ +# Agent Note:所有控件共用一个键盘焦点颜色 + +Status: implemented + +[English](2026-09-20-focus-ring-brand-primary.md) | 中文 + +## 问题 + +不同的显式焦点颜色与浏览器默认轮廓无法提供一致的键盘焦点提示。浏览器默认值还可能取决于操作系统强调色。 + +焦点反馈另有交互与几何约束。浏览器的 `:focus-visible` 启发式可能在不移动焦点的按键之后显出指针聚焦按钮的环,而可编辑文本控件在点击时需要焦点反馈。裁切内容的容器可能隐藏外扩环。 + +## 决策 + +[主题焦点样式表](../../../../packages/client/ui-theme/src/styles/focus.css) 拥有共享焦点环颜色与标准宽度。[输入模态](../../../../packages/client/ui-primitives/README.zh.md#input-modality) 区分 tooltip 输入与键盘焦点导航。 + +**一个颜色。** 全局与组件的 `:focus-visible` 轮廓和焦点环阴影读取 `--dsw-focus-ring-color`,回退到 `--dsw-alias-state-business-primary`。全局兜底声明颜色与标准宽度、但不声明样式,因此组件禁用轮廓时它不会创建轮廓。声明宽度可让没有自有焦点环的控件保持标准几何,而不是落到 Chromium 的 `auto 1px`。 + +**场景决定几何。** `--dsw-focus-ring-width` 提供标准的 2px 宽度。密集表格与工具栏可以保留 1px,避免焦点环压过紧凑内容。组件拥有自己的 offset,并在外扩环会被裁切时使用内嵌环。 + +**指针焦点不等于键盘导航。** 指针模态下,`:focus-visible:not(:read-write)` 控件使用透明的焦点环颜色。后代与伪元素上的环继承该值。只抑制焦点环绘制:选中态边框与 elevation 阴影保留,可编辑文本控件在点击时保留自身焦点反馈。DOM 焦点仍留在控件上。 + +Tooltip 跟随最近一次输入,将任意按键视为键盘输入。焦点环则在非组合输入的导航键到达时,或非组合输入按键之后焦点到达不同控件时,恢复键盘样式。重新聚焦同一控件不算导航;指针输入、组合输入按键和 window blur 会清除待处理按键。 + +## 考虑过的其他做法 + +**只重写组件声明。** 显式焦点环会一致,但使用浏览器默认轮廓的控件仍会保留其颜色。要求每个控件自行声明焦点环,也会让新控件在作者补上规则之前保持不一致。 + +**全局统一宽度与 offset。** 正 offset 可能被滚动祖先裁切,统一的粗环则可能压过密集内容。颜色一致性和输入模态都不要求相同的几何。 + +**用移动焦点取代样式抑制。** 对指针聚焦的按钮调用 `blur()` 可以隐藏焦点环,但也会移除后续 Enter 或 Space 激活的目标。 + +**指针聚焦时清除所有阴影。** 一个 `box-shadow` 除了焦点环,还可能包含 elevation 或选中态绘制。抑制其焦点环颜色可保留这些独立提示;豁免 `:read-write` 可保留文本输入反馈。 + +**把所有 `outline: none` 改为共享焦点环。** 一些控件改用内嵌阴影或容器级焦点处理;一些容器则刻意不显示焦点环。替换这些声明会改变焦点处理方式,而不只是颜色。 + +**让 tooltip 与焦点环共用同一条模态规则。** 按键可以让后续程序化聚焦显示 tooltip,却不一定需要在同一个指针聚焦按钮上显示焦点环。分别记录两个答案可保留两种行为。 + +## 影响 + +焦点环共享主题颜色,组件保留自己的几何与非焦点状态提示。焦点环可见性的变化不改变键盘的活动目标。这些规则不保证浏览器渲染完全相同,也不会消除组件特有的裁切。 + +[样式表顺序检查](../../../../packages/client/ui-theme/tests/client-styles.client.spec.ts)、[焦点样式检查](../../../../packages/client/ui-theme/tests/focus-ring-styles.client.spec.ts)与[输入模态测试](../../../../packages/client/ui-primitives/tests/input-modality.client.spec.ts)覆盖样式声明和模态转换。静态声明与合成事件不能确认焦点环的实际渲染可见性或跨平台行为。 diff --git a/.agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.i18n.yaml b/.agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.i18n.yaml new file mode 100644 index 0000000000..7ed8eaaa55 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-24-fixed-rate-floor-one-minute.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.md +/agent-note-one-minute-fixed-rate-floor: + en: d74434992e0ca252 + zh: cae88d61368d5555 +/agent-note-one-minute-fixed-rate-floor/problem: + en: a16373df16ee9126 + zh: 10e86f4cfe0e9a11 +/agent-note-one-minute-fixed-rate-floor/decision: + en: fb49d104c6539934 + zh: 04655695ccbb58c6 +/agent-note-one-minute-fixed-rate-floor/alternatives-considered: + en: 080ae22387dd35b7 + zh: 8247747dc444d7a4 +/agent-note-one-minute-fixed-rate-floor/consequences: + en: c11eac4968a2a707 + zh: c4a02ddeef3a5dff diff --git a/.agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.md b/.agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.md new file mode 100644 index 0000000000..77fffc7813 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.md @@ -0,0 +1,29 @@ +# Agent Note: One-minute fixed-rate floor + +Status: implemented + +English | [中文](2026-09-24-fixed-rate-floor-one-minute.zh.md) + +## Problem + +`every_seconds` refused every interval below 300 seconds, and the Web client restated that bound as its own per-unit floors of 1 hour, 5 minutes, or 300 seconds. The same product already offered a `cron` selector whose finest granularity is one minute, so two selectors that both describe a repeating cadence disagreed about the shortest one a person could ask for: a reminder that repeats every minute was expressible as `cron` and rejected as `every_seconds`. The floor also made a shipped default Web surface awkward to try, because the first repeating reminder anyone creates cannot come back in under five minutes. + +## Decision + +`MIN_EVERY_INTERVAL_SECONDS` in `packages/schedule/schedule/src/domain.ts` is 60. The Host decoder, the create and update validation, the `every_seconds` tool parameter description, and its refusal message all read that constant, so one change moves the bound everywhere the Host states it. + +The client keeps its own `MIN_INTERVAL_SECONDS = 60`, which the Run time card uses for the seconds row and to derive the per-unit floors: 1 hour, 1 minute, or 60 seconds. The localized hint and refusal copy states 1 minute and 60 seconds for those rows. + +One minute is the shortest cadence that does not need a new protocol decision: it matches the cron selector's granularity, and every accepted value remains a whole-second interval the durable record already stores. A stored record keeps whatever the Host accepted when it was created; a record written under the old bound (300 seconds or more) stays valid and replayable, and no migration runs because the previous bound rejected anything below it. + +## Alternatives considered + +**Keep the 300-second floor.** The two selectors would keep disagreeing about the shortest repeating cadence, and a person could still reach a one-minute cadence only through `cron`. + +**Lower the bound to one second.** Sub-minute intervals multiply durable follow-up messages and timer re-anchoring per reminder without a request behind them; the minute granularity already covers the shortest cadence the product names elsewhere. + +## Consequences + +- A fixed-rate reminder may now repeat every minute; the smallest accepted value is model-visible in the `every_seconds` description and in the client's hint copy. +- Delivery behavior, batching, catch-up, and the interval arithmetic are unchanged: an accepted interval still anchors to creation or to the accepted save, and each overdue record still contributes only its latest occurrence. +- A deployment cannot raise the floor through configuration; the bound is a protocol constant, so a deployment that needs a longer minimum enforces it in the interface that creates tasks. diff --git a/.agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.zh.md b/.agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.zh.md new file mode 100644 index 0000000000..6d103be09b --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-24-fixed-rate-floor-one-minute.zh.md @@ -0,0 +1,29 @@ +# Agent Note: 固定速率下限改为 1 分钟 + +Status: implemented + +[English](2026-09-24-fixed-rate-floor-one-minute.md) | 中文 + +## Problem + +`every_seconds` 会拒绝所有低于 300 秒的间隔,Web 客户端也把该下限复述为自己按单位计算的下限:1 小时、5 分钟或 300 秒。同一产品已经提供粒度为 1 分钟的 `cron` 选择器,于是两个都在描述重复节奏的选择器对「最短能填多少」给出了不同答案:每分钟重复一次的提醒用 `cron` 可以表达,用 `every_seconds` 会被拒绝。该下限也让默认发布 Schedule 的 Web 界面难以试用——任何人创建的第一条重复提醒都不可能在五分钟内回到。 + +## Decision + +`packages/schedule/schedule/src/domain.ts` 的 `MIN_EVERY_INTERVAL_SECONDS` 为 60。宿主解码器、创建与更新校验、`every_seconds` 工具参数描述及其拒绝消息都读取该常量,因此一处改动即可移动宿主陈述该下限的所有位置。 + +客户端保留自己的 `MIN_INTERVAL_SECONDS = 60`,「运行时间」卡片用它校验秒数行,并据此推导各单位的数量下限:1 小时、1 分钟或 60 秒。对应行的本地化提示与拒绝文案改为 1 分钟与 60 秒。 + +1 分钟是无需新增协议决策的最短节奏:它与 cron 选择器的粒度一致,且每个被接受的值仍是以整秒表示的间隔,持久记录本来就以整秒存储。已存储的记录保留宿主当初接受的值:按旧下限(300 秒及以上)写入的记录仍然有效、仍可回放;由于旧下限拒绝了低于它的值,因此不存在需要迁移的数据。 + +## Alternatives considered + +**保留下限 300 秒。** 两个选择器会继续对最短重复节奏给出不同答案,而每分钟一次的节奏仍只能通过 `cron` 达到。 + +**把下限降到 1 秒。** 低于分钟级的间隔会让每条提醒产生更多持久 follow-up 消息与 timer 重新对齐,而背后没有这样的需求;分钟粒度已经覆盖产品在别处命名的最短节奏。 + +## Consequences + +- 固定速率提醒现在可以每分钟重复一次;最小可接受值对模型可见,出现在 `every_seconds` 的描述与客户端的提示文案中。 +- 投递行为、批处理、补跑与间隔运算法则不变:被接受的间隔仍以创建时刻或接受保存的时刻为起算点,每条逾期记录仍只贡献最新一次发生时点。 +- 部署无法通过配置提高该下限;它是协议常量,需要更长最小间隔的部署应在创建任务的界面上自行约束。 diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml index dbe4c5aaab..f5e57341df 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml @@ -9,8 +9,8 @@ en: dc43ba84345e1405 zh: dbc03572ab7710d2 /agent-note-ship-optional-bundles-with-the-installation/decision: - en: 84777110aa68ecf8 - zh: b109275cfddb73b0 + en: d64020c4ca4957e1 + zh: 241b3b851234b6f0 /agent-note-ship-optional-bundles-with-the-installation/alternatives-considered: en: 05c19f616009bee8 zh: af318640481351f0 diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md index c9f560f5c0..059fe5bcce 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md @@ -14,7 +14,7 @@ The launcher names in `OPTIONAL_BUNDLES` (`packages/boot/app-boot/src/profile.ts Default-product isolation keeps its rules with one declared exception: an optional bundle's dependency graph is outside the default product. The static gate skips the `dependencies` edge from `@deepseek-ai/dsh` to a listed bundle and still rejects a runtime import, a shipped composition, a preset, or a default template that names it, an experimental dependency the list does not name, and a listed name that is not a runtime dependency or not a bundle. The workspace-constraints check accepts the same `dependencies` edges and no other runtime section, and the packed-install release check skips them from the installed entry package while requiring each listed bundle to be installed. -The [single-bundle decision](../architecture/2026-09-18-agent-teams-single-bundle.md) supersedes the two Agent Teams selections with one `@deepseek-ai/dsh-experimental-agent-team-profile` selection. This note retains the installation-owned optional-bundle policy. Auto review is a published experimental package the page's install guide names as its example, not an optional bundle. +The [single-bundle decision](../architecture/2026-09-18-agent-teams-single-bundle.md) supersedes the two Agent Teams selections with one `@deepseek-ai/dsh-experimental-agent-team-profile` selection. This note retains the installation-owned optional-bundle policy; the [optional-bundle decision for experimental capabilities](../architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md) names the current list and the display metadata each entry carries. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md index e91b6580e2..9847095601 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md @@ -14,7 +14,7 @@ Web 插件页只管理用户装进 profile 的组合包。像 Agent Teams 这样 默认产品隔离的规则保持不变,只声明一个例外:可选组合包的依赖图在默认产品之外。静态门禁跳过从 `@deepseek-ai/dsh` 到列表中组合包的 `dependencies` 边,仍然拒绝运行时 import、随附组合、preset 或默认模板对它的引用,拒绝列表没有点名的实验依赖,也拒绝不是运行时依赖或不是组合包的列表项。workspace 约束检查接受同样的 `dependencies` 边而不接受其他运行时依赖段;发布时的 packed-install 检查对已安装入口包跳过这些边,并要求列表中的每个组合包都已安装。 -[单组合包决策](../architecture/2026-09-18-agent-teams-single-bundle.zh.md)将两个 Agent Teams 选项替换为一个 `@deepseek-ai/dsh-experimental-agent-team-profile` 选项。本记录保留由安装方决定可选组合包的策略。Auto review 是一个已发布的实验包,插件页的安装引导拿它作示例,它不是可选组合包。 +[单组合包决策](../architecture/2026-09-18-agent-teams-single-bundle.zh.md)将两个 Agent Teams 选项替换为一个 `@deepseek-ai/dsh-experimental-agent-team-profile` 选项。本记录保留由安装方决定可选组合包的策略;[实验能力的可选 bundle 决策](../architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)列出当前名单以及每一项携带的显示元数据。 ## 考虑过的替代方案 diff --git a/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.i18n.yaml b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.i18n.yaml new file mode 100644 index 0000000000..737e671e9a --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.i18n.yaml @@ -0,0 +1,43 @@ +# Bilingual-pair consistency record for 2026-09-19-timed-user-question-two-settlements.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.md +/agent-note-timed-user-questions-as-two-settlements: + en: 15c55d3b120b8fd4 + zh: 6b86ac34f4a1e9cd +/agent-note-timed-user-questions-as-two-settlements/problem: + en: f665127a9b5b8225 + zh: b19845f62310f3a2 +/agent-note-timed-user-questions-as-two-settlements/proposal: + en: 56ca8c0f97976358 + zh: 69163bb5f064e069 +/agent-note-timed-user-questions-as-two-settlements/proposal/modes-and-scope: + en: d23e622ee4643b48 + zh: 3732c10c12344755 +/agent-note-timed-user-questions-as-two-settlements/proposal/ownership: + en: 0543678ad9d26fc5 + zh: aa6ba207e661c567 +/agent-note-timed-user-questions-as-two-settlements/proposal/foreground-wait-and-claim-lifetime: + en: 447713bbdf1ab4d8 + zh: 2757678ae47822aa +/agent-note-timed-user-questions-as-two-settlements/proposal/durable-state-and-late-answers: + en: b8729d06ab7814ed + zh: acaa8f4174701248 +/agent-note-timed-user-questions-as-two-settlements/proposal/card-state-and-delivery-races: + en: 8c55b693e7a5bca4 + zh: aea6e966ce3667e3 +/agent-note-timed-user-questions-as-two-settlements/proposal/local-countdown-and-panel-actions: + en: 4c0c9ec59fc51217 + zh: 322870eeb49176c7 +/agent-note-timed-user-questions-as-two-settlements/proposal/reply-node-and-grouping: + en: 10323b0e0340a40b + zh: a17fc3a4d4d820a7 +/agent-note-timed-user-questions-as-two-settlements/alternatives-considered: + en: ce8ff9d5bc4b85b7 + zh: 49024fe3a5b194b2 +/agent-note-timed-user-questions-as-two-settlements/acceptance-criteria: + en: 5a993484c73befe9 + zh: 38cd3209c68178c3 +/agent-note-timed-user-questions-as-two-settlements/risks: + en: aae260b0cd95554f + zh: 3d7fbd6cbe9e2d37 diff --git a/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.md b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.md new file mode 100644 index 0000000000..a4f4bf6e30 --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.md @@ -0,0 +1,113 @@ +# Agent Note: Timed user questions as two settlements + +Status: proposed + +English | [中文](2026-09-19-timed-user-question-two-settlements.zh.md) + +## Problem + +A question can block an agent even when useful work does not depend on the answer. Releasing the tool call after a wait must not discard the question: the user may answer after the agent has continued or after reopening the Session. A pending result is neither a skipped answer nor permission to proceed with work that requires approval. + +The foreground tool result and the eventual answer have different lifetimes. A `user-questions/request` waterfall settles once; keeping it alive after returning pending creates a second answer path that outlives its caller. Countdown, focus, and editing also have different owners from durable question state. + +## Proposal + +Keep foreground answers on the existing Remote Event waterfall. A timeout ends that request and returns pending; a later answer uses a business RPC that steers a durable user message. The claim stream controls the foreground wait only. It neither transports answers nor replaces the Remote Event. + +### Modes and scope + +- The default `mode: legacy` retains the blocking schema and `ask()` path. Explicit `mode: timed` adds `timeout`, using the configured default of 120 seconds when omitted. +- A positive integer timeout selects `askTimed()`; `-1` selects blocking `ask()`. A blocking call made with the timed schema still participates in timed-question projection and review; it is not identical to a legacy call. +- Plan review retains `ask()`, its `plan-review` intent, and `BAD_INTENT` validation. This decision does not move plan review to another package. +- Human interaction requires the exact live runtime root when an Agent is supplied. A stale instance fails with `CALLER_NOT_LIVE`; an owned child fails with `DELEGATED_CALLER`. Durable Session lineage does not prevent a resumed runtime root from asking. + +### Ownership + +| Owner | Responsibility | +|---|---| +| `tool-ask-user` | Model schema, timeout validation, service selection, and pending-result text. | +| `UserQuestionService` | Foreground waits and Client claims, unattended deadlines, late-answer validation and steering, and the durable question projection. | +| Gateway and Remote runtime | Existing request delivery, settlement, cancellation, and Remote stream transport; no question-specific timer or event-name branch. | +| `ui-user-questions` | One card per Session and call, answer-channel selection, local countdown and drafts, and the reply Definition and renderer. | +| `ui-chat` | Message-id-based presentation aggregation and ordinary process/Turn folding. | +| Question-specific tool row | Pending/answered presentation and panel actions, composed in `QuestionToolRow` without adding `rowAction` to generic `ToolRow`. | + +The [question service](../../../../packages/interaction/user-questions/README.md), [tool](../../../../packages/interaction/tool-ask-user/README.md), and [questions UI](../../../../packages/client/ui-user-questions/README.md) own their package contracts. + +### Foreground wait and claim lifetime + +`TimedQuestionWait` owns a private cancellation signal, the original Host deadline, and Client claims. The Host runs a timer only while no answer UI holds a claim. Delivery to a transport queue is not a claim. Losing the last claim restores the original deadline, with immediate expiry if it has passed. + +A timed request carries `wait: { callId, timed: true }`. Before attaching its answer channel, the Client opens `attachWait`, a business Remote stream that yields one Host-computed `remainingMs` frame and stays open for the claim's lifetime. Focus, edits, and heartbeats do not travel through this stream. + +The Client retains its claim through delivery of the waterfall outcome to the Host. Returning an answer locally is not delivery: releasing the claim then could let an overdue Host timer beat that answer. The Host closes the claim when the foreground request settles. Explicit delegation releases the claim before `next()`; plugin teardown delegates and cancels its stream. + +Client countdown expiry rejects the waterfall with `ASK_TIMED_OUT`. Unattended Host expiry aborts only the private wait signal with the same business error. `askTimed()` maps only that error to pending, maps parent cancellation to `ASK_ABORTED`, and propagates other failures. `NO_PROVIDER` waits on the same bounded wait lifetime, not a permanently unresolved promise. Foreground completion releases timers, claims, and the wait registry entry. + +### Durable state and late answers + +The `userQuestions` projection reconstructs question state from existing Session events. It recognises the timed schema by the `timeout` parameter recorded in `request/header`; legacy calls are not tracked. A valid `tool/call` opens a question. Its `tool/result` makes it continued for pending or `TOOL_OUTCOME_UNKNOWN`, settles it with a valid answer batch, or removes it for another result or failure. + +A continued question accepts `answer(agent, callId, answer)`. The method returns `false` for an unknown or non-continued call and throws `BAD_ANSWER` unless the batch names each question exactly once. Accepted answers use `agent.steer(createUserMessage(...))`, source `{ kind: 'user-question-reply', callId, outcome: 'answered' }`, and JSON text containing `answer_to_pending_question`, the call id, original questions, and answers. Remote Agent resolution from the Session id resumes a root when needed; the `answer()` body does not own that resume operation. + +The inbox message is the durable reply. `agent/inbox/spliced` settles the question when the message is queued; the later admitted `user/message` is idempotent for that settlement. Settled answer batches remain in the projection so the original tool row can show late answers even though its own result remains pending. Unusable recorded reply text settles with an empty batch. + +No new Session event type or wait-state log is needed. The message-source extension requires the repository's persistence-type acknowledgement. `dismissed` remains readable for historical records but has no producer: hiding a panel does not manufacture a reply. + +### Card state and delivery races + +`QuestionCards` shares one `PendingQuestion` between the live request and the continued projection, keyed by Session and `callId`. A blocking request without a call id has its own per-request card. An open card submits through its waterfall; without that channel it disables submission. A continued card uses the late-answer RPC. + +A waterfall submission has no delivery acknowledgement. The Gateway can discard an outcome that loses a settlement race. The card therefore retains its draft and stays busy until projection reconciliation. If the question becomes continued during submission, the controls re-arm with a resubmit hint; the user can submit the retained draft through RPC. This is not automatic retry or a guarantee that every local submission reaches the model. + +A tool-call-keyed card is removed when its call is absent from the active projection and no waterfall remains. Removal closes the card and clears its draft. Neither a local submission nor a transport cancel frame alone decides that the question is answered. An unkeyed blocking card ends with its waterfall; closing that blocking panel rejects with `ASK_CANCELLED`. + +### Local countdown and panel actions + +The Client computes `Date.now() + remainingMs`. Manual focus on a pristine answer surface pauses its countdown, and blur resumes the preserved remainder. Focus before the claim handshake remains effective when the deadline arrives; blur before the handshake does not create a timer. First edit or Take time freezes the countdown, with that disposition saved beside the browser-local draft. + +Autofocus applies only when the answer channel is ready, no countdown is active, and the card is not locked. Hiding a timed-call panel leaves its claim and countdown running; its tool row can reopen it. A settled call opens a read-only panel from recorded questions and answers, not another answer channel. + +### Reply node and grouping + +The questions UI identifies a late reply from `source.kind`, then parses its recorded JSON for question-and-answer presentation. Pasting identical JSON in an ordinary user message does not identify it as a reply. Unreadable recorded payloads retain their raw-text presentation. + +The ordinary message Definition and the question-reply Definition both match the appended message and retain separate Node Store projections with the same message id. Chat business grouping omits the `turn-trigger` presentation when a `question-reply` with that id is present. The duplicate trigger neither renders nor splits a group; the reply renders once. The same aggregation applies to unscoped historical input. + +The reply participates in ordinary process grouping and whole-Turn folding. It is not independently visible: reading it can require expanding both its Turn and process group. Turn disclosure stays before the group it controls, including a reply that opens the Turn. + +This decision adds no process-role field, independent-layout flag, user-message structure change, or target-local fallback dispatch. The independent-kind rules stay in Chat's existing grouping logic. A node-data-owned declaration of grouping and overlap semantics is deferred infrastructure, not part of the question feature. + +## Alternatives considered + +**Unconditional Host deadline or Gateway-owned question timers.** An unconditional deadline overrides local editing and Take time. Putting the exception into pending Remote Events or branching on the question event in Remote dispatch makes generic transport own business policy. Claims keep that policy in the question service. + +**Keep the waterfall alive after returning pending.** The request outlives the tool caller, and late waterfall answers compete with the RPC. Ending the foreground request gives each settlement one answer path. + +**Delegate every timed request immediately.** Dispatching to an intentionally empty answerer set wastes the existing blocking-answer path and requires special handling of `NO_PROVIDER`. Delegation is for an unavailable answerer, not the timed mode itself. + +**Persist a whole-value wait/focus/edit stream or use Host focus leases.** Durable question status is derivable from tool and inbox events. Focus and editing are local UI state; logging them or synchronising focus leases adds Host state without making multiple Clients share one editing session. + +**Reclassify JSON inside generic `MessageItem`, borrow the relay form, or use the default context row.** Parsing text to identify the business message confuses its source with its content; the relay form describes another agent's message, and a generic context row exposes JSON. A dedicated source, Definition, and renderer own the question presentation. + +**Independent reply layout through generic node metadata.** It broadens a question feature into a conversation-infrastructure change. Ordinary folding and Chat-owned aggregation are sufficient for this decision; independently declared node-data grouping semantics remain a separate design task. + +**A log-only close event or a manufactured dismissal message on panel close.** A log-only close hides the user's decision from the model and adds another durable mechanism. Manufacturing a reply treats hiding the panel as an answer. Panel close therefore leaves a timed question answerable. + +**A dedicated late-reply Session event beside the inbox message.** The inbox splice and admitted user message already record the reply. A third event duplicates the same fact and adds another persistence type. + +## Acceptance criteria + +- Service tests cover claimed and unclaimed waits, last-claim disconnect, cancellation, timeout error mapping, continued-only answers, and exact answer-id validation. +- Projection tests distinguish legacy and timed schemas and cover pending, answered, other failures, resume repair, and duplicate inbox/message settlement. +- Client tests cover claim retention through Host acceptance, focus before handshake, indefinite waits, draft retention, RPC resubmission, and panel hiding/reopening. +- Grouping and browser replay cover one rendered reply with both projections retained, ordinary collapsed placement, disclosure ordering, and the original tool row's read-only late-answer panel. +- Legacy question round trips, cancellation, plan-review intent, and Session remounts retain their existing behavior. Live multi-Client delivery races and real Host restart remain integration coverage gaps beyond isolated lifecycle and projection tests. + +## Risks + +- Take time affects only one Client. Another Client can answer or expire the shared waterfall; local focus does not establish a global hold. +- A terminal claim-stream failure can reject the foreground question rather than produce pending. No unattended-wait fallback is provided for that failure. +- Host and Client clocks need not align, but Client system-clock changes can move expiry and transport latency can delay the local countdown's start. +- The duplicate projections remain stored. Aggregation adds linear scans during structural grouping rebuilds; ordinary text-only streaming updates retain their incremental path. This is an algorithmic bound, not a measured latency claim. +- Unanswered continued questions can remain after reopening a Session. Panel close records nothing, and visibility follows normal folding rather than an always-visible reply guarantee. diff --git a/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.zh.md b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.zh.md new file mode 100644 index 0000000000..7f2d42f73b --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.zh.md @@ -0,0 +1,113 @@ +# Agent Note: 计时用户提问的两次结算 + +Status: proposed + +[English](2026-09-19-timed-user-question-two-settlements.md) | 中文 + +## Problem + +即使还有不依赖回答的工作可做,问题仍可能阻塞 agent(智能体)。等待后放行工具调用不能丢弃问题:用户可能在 agent 继续执行后,或重新打开 Session 后才回答。pending 结果既不是跳过回答,也不是开展需要批准的工作的授权。 + +前台工具结果与最终回答的生命周期不同。`user-questions/request` waterfall(瀑布式事件)只结算一次;返回 pending 后仍保留它,会产生一条活过调用方的第二回答路径。倒计时、聚焦和编辑的归属也不同于持久问题状态。 + +## Proposal + +前台回答继续使用现有 Remote Event waterfall。超时结束该请求并返回 pending;迟到回答通过业务 RPC steer 一条持久用户消息。接手 stream 只控制前台等待,既不传递回答,也不替代 Remote Event。 + +### 模式与范围 + +- 默认 `mode: legacy` 保留阻塞式 schema 和 `ask()` 路径。显式 `mode: timed` 增加 `timeout`,省略时使用配置默认值 120 秒。 +- 正整数超时选择 `askTimed()`;`-1` 选择阻塞式 `ask()`。timed schema 发出的阻塞调用仍参与计时问题投影和答案查看,并不等同于 legacy 调用。 +- 计划评审保留 `ask()`、`plan-review` 意图及 `BAD_INTENT` 校验。本决策不把计划评审迁移到其他包。 +- 提供 Agent 时,人机交互要求它是运行时中确切的存活根实例。陈旧实例以 `CALLER_NOT_LIVE` 失败;被其他 agent 拥有的子实例以 `DELEGATED_CALLER` 失败。持久 Session 的血缘关系不妨碍恢复后的运行时根实例提问。 + +### 职责归属 + +| 归属方 | 职责 | +|---|---| +| `tool-ask-user` | 模型 schema、超时校验、服务选择和 pending 结果文本。 | +| `UserQuestionService` | 前台等待与 Client 接手、无人接手时的 deadline、迟到回答校验与 steering(中途引导),以及持久问题投影。 | +| Gateway 与 Remote 运行时 | 现有请求投递、结算、取消及 Remote stream 传输;不包含问题专属计时器或按事件名分支。 | +| `ui-user-questions` | 每个 Session 和调用的一张卡片、回答通道选择、本地倒计时与草稿,以及回复 Definition 和渲染器。 | +| `ui-chat` | 基于消息 id 的呈现聚合,以及普通过程/轮次折叠。 | +| 问题专用工具行 | pending/已回答呈现与面板操作,在 `QuestionToolRow` 中组合,不向通用 `ToolRow` 添加 `rowAction`。 | + +[问题服务](../../../../packages/interaction/user-questions/README.zh.md)、[工具](../../../../packages/interaction/tool-ask-user/README.zh.md)和[问题 UI](../../../../packages/client/ui-user-questions/README.zh.md)分别拥有各包的约定。 + +### 前台等待与接手生命周期 + +`TimedQuestionWait` 拥有专属取消 signal、原始 Host deadline 和 Client 接手记录。Host 只在没有回答 UI 接手时运行计时器。投递到传输队列不算接手。最后一个接手方离开后恢复原 deadline,已经过期则立即到期。 + +计时请求携带 `wait: { callId, timed: true }`。Client 在挂接回答通道前打开业务 Remote stream `attachWait`,接收一帧由 Host 计算的 `remainingMs`,并在接手期间保持 stream 打开。聚焦、编辑和心跳不经过该 stream。 + +Client 将接手保留到 waterfall 结果交付给 Host。本地返回回答不等于交付:此时释放接手会让已经到期的 Host 计时器抢在回答之前结算。Host 在前台请求结算时关闭接手。显式委托在 `next()` 前释放接手;插件拆卸会委托请求并取消自己的 stream。 + +Client 倒计时到期以 `ASK_TIMED_OUT` 拒绝 waterfall。无人接手时,Host 到期只以同一业务错误中止等待专属 signal。`askTimed()` 只把该错误映射为 pending,把父级取消映射为 `ASK_ABORTED`,其余失败继续传播。`NO_PROVIDER` 等待同一个有界等待生命周期,而不是永不结算的 Promise。前台完成时释放计时器、接手记录及等待注册项。 + +### 持久状态与迟到回答 + +`userQuestions` 投影从现有 Session 事件重建问题状态。它通过 `request/header` 记录的 `timeout` 参数识别 timed schema,不跟踪 legacy 调用。有效的 `tool/call` 打开问题。其 `tool/result` 在 pending 或 `TOOL_OUTCOME_UNKNOWN` 时将问题标为已继续,在有效回答批次时结算并保存答案,其他结果或失败则移除问题。 + +已继续的问题接受 `answer(agent, callId, answer)`。未知或非已继续调用返回 `false`;回答批次没有恰好包含每个问题一次时抛出 `BAD_ANSWER`。接受的回答通过 `agent.steer(createUserMessage(...))` 送入 agent,来源为 `{ kind: 'user-question-reply', callId, outcome: 'answered' }`,JSON 文本包含 `answer_to_pending_question`、调用 id、原问题及答案。Remote 根据 Session id 解析 Agent 时按需恢复根实例;恢复操作不属于 `answer()` 方法体。 + +inbox 消息就是持久回复。`agent/inbox/spliced` 在消息入队时结算问题;随后接纳的 `user/message` 对该结算是幂等的。已结算答案批次保留在投影中,让原工具行即使自身结果仍是 pending,也能显示迟到回答。无法使用的已记录回复文本以空答案批次结算。 + +无需新增 Session 事件类型或等待状态日志。消息来源扩展需要仓库的持久化类型变更确认。`dismissed` 为历史记录保留可读性,但没有生产者:隐藏面板不会伪造回复。 + +### 卡片状态与投递竞争 + +`QuestionCards` 按 Session 和 `callId`,让实时请求与已继续投影共享一个 `PendingQuestion`。没有调用 id 的阻塞请求拥有独立的逐请求卡片。开放卡片通过 waterfall 提交;缺少该通道时禁用提交。已继续卡片使用迟到回答 RPC。 + +waterfall 提交没有送达确认。Gateway 可能丢弃输掉结算竞争的结果,因此卡片保留草稿并保持忙碌,等待投影同步。若提交期间问题变为已继续,控件带重提提示重新可用,用户可以通过 RPC 提交保留的草稿。这不是自动重试,也不保证每次本地提交都能到达模型。 + +按工具调用标识的卡片在调用不属于活动投影且没有 waterfall 时移除。移除会关闭卡片并清理草稿。本地提交和传输取消帧都不能单独决定问题已经回答。无调用标识的阻塞卡片随 waterfall 结束;关闭这种阻塞面板以 `ASK_CANCELLED` 拒绝请求。 + +### 本地倒计时与面板操作 + +Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会暂停倒计时,失焦后从保留的剩余时间继续。接手握手前的聚焦在 deadline 到达时仍生效;握手前失焦不会制造计时器。首次编辑或「慢慢回答」冻结倒计时,该状态与浏览器本地草稿一起保存。 + +只有回答通道就绪、没有活动倒计时且卡片未锁定时才自动聚焦。隐藏计时调用的面板会保留接手和倒计时,工具行可以重新打开面板。已结算调用依据记录的问题与答案打开只读面板,不产生另一条回答通道。 + +### 回复节点与分组 + +问题 UI 通过 `source.kind` 识别迟到回复,再解析已记录的 JSON 来呈现问答。普通用户消息粘贴相同 JSON 不会被识别为回复。无法读取的已记录载荷保留原始文本呈现。 + +普通消息 Definition 和问答回复 Definition 都匹配追加的消息,在 Node Store 中保留消息 id 相同的独立投影。存在同 id 的 `question-reply` 时,Chat 业务分组省略 `turn-trigger` 呈现。重复触发节点既不渲染,也不切开分组,回复只显示一次。相同聚合也适用于无轮次归属的历史输入。 + +回复参与普通过程分组和整轮折叠,不保证独立可见:阅读时可能需要展开所属轮次与过程分组。轮次折叠控件始终在所控制的分组之前,包括由回复开启轮次的情况。 + +本决策不添加 process-role 字段、独立布局标记、用户消息结构变更或目标内 fallback 派发。独立 kind 规则保留在 Chat 现有分组逻辑中。由节点 data 声明分组与重叠语义的机制是留待拆分的基础设施,不属于问题功能。 + +## Alternatives considered + +**无条件 Host deadline 或 Gateway 拥有问题计时器。** 无条件 deadline 会覆盖本地编辑和「慢慢回答」。把例外放入 pending Remote Events,或在 Remote 派发中按问题事件分支,会让通用传输拥有业务策略。接手机制将策略留在问题服务中。 + +**返回 pending 后继续保留 waterfall。** 请求活过工具调用方,迟到的 waterfall 回答与 RPC 竞争。结束前台请求让每次结算各有一条回答路径。 + +**立即委托所有计时请求。** 向故意留空的回答方集合派发请求,浪费现有阻塞回答路径,还要求特殊处理 `NO_PROVIDER`。委托表示回答方不可用,不代表计时模式本身。 + +**持久化整值等待/聚焦/编辑流,或使用 Host 聚焦租约。** 持久问题状态可从工具与 inbox 事件推导。聚焦和编辑属于本地 UI 状态;记录它们或同步聚焦租约会增加 Host 状态,却不能让多个 Client 共享同一编辑会话。 + +**在通用 `MessageItem` 内重新分类 JSON、借用 relay form,或使用默认 context 行。** 解析文本来识别业务消息混淆了来源与内容;relay form 表示其他 agent 的消息,通用 context 行则会暴露 JSON。专用来源、Definition 和渲染器拥有问题呈现。 + +**通过通用节点元数据让回复独立布局。** 这会把问题功能扩大为对话基础设施变更。普通折叠与 Chat 内聚合足以满足本决策;由节点 data 独立声明分组语义仍是单独的设计任务。 + +**面板关闭时写仅入日志的关闭事件,或伪造放弃消息。** 仅入日志会让模型看不到用户决定,还增加另一套持久机制。伪造回复则把隐藏面板当成回答。因此关闭面板仍让计时问题保持可回答。 + +**在 inbox 消息之外增加专用迟到回复 Session 事件。** inbox 插入与接纳的用户消息已经记录回复。第三个事件重复同一事实,并增加另一种持久化类型。 + +## Acceptance criteria + +- 服务测试覆盖有人与无人接手、最后接手方断开、取消、超时错误映射、仅接受已继续问题的回答,以及答案 id 的精确校验。 +- 投影测试区分 legacy 与 timed schema,并覆盖 pending、已回答、其他失败、恢复修复及 inbox/消息重复结算。 +- Client 测试覆盖接手保留到 Host 接受结果、握手前聚焦、无限期等待、草稿保留、RPC 重提及面板隐藏/重开。 +- 分组与浏览器回放覆盖保留两个投影但只渲染一次回复、普通折叠位置、折叠控件顺序,以及原工具行的迟到答案只读面板。 +- legacy 问答往返、取消、计划评审意图及 Session 重新挂载保留既有行为。真实多 Client 投递竞争与真实 Host 重启仍是独立生命周期和投影测试之外的集成覆盖缺口。 + +## Risks + +- 「慢慢回答」只影响一个 Client。其他 Client 可以回答或让共享 waterfall 到期,本地聚焦不构成全局暂停。 +- 接手 stream 的终止性失败可能让前台问题失败,而不是产生 pending。不为该失败提供无人接手等待的 fallback。 +- Host 与 Client 时钟不必对齐,但 Client 系统时钟变化可能移动到期时间,传输延迟可能推迟本地倒计时的开始。 +- 重复投影仍保留在存储中。聚合在结构性分组重建时增加线性扫描;普通纯文本 stream 更新保留增量路径。这是算法复杂度界限,不是实测延迟结论。 +- 无人回答的已继续问题可以在重新打开 Session 后继续存在。关闭面板不写记录,可见性遵循普通折叠,而不是保证回复始终可见。 diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index e585c5c6f2..b5afe5cdf1 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -15,8 +15,8 @@ en: 79cad979b0f7895c zh: b0f922a4b43accf8 /deepseek-ai-dsh/optional-overlays: - en: c1b3ea3b79a975e4 - zh: 61070ee19b59f7e6 + en: 3a40ddc44e55f79c + zh: 8d3949132e1ef93c /deepseek-ai-dsh/development: en: 99a36e4f7ea3f9c7 zh: 07ca2934de700caa diff --git a/apps/cli/README.md b/apps/cli/README.md index 7654404381..1ebada88ae 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -51,7 +51,7 @@ The [CLI behavior reference](reference/README.md) owns exact layer precedence, f ## Optional overlays -`config/examples/` ships opt-in overlays for GitHub review webhooks, session-local Schedule, memory MCP servers, and runtime Cordis tools. They are never part of a default profile; the [user guides](../../docs/user/guide/index.md) and [developer practice guides](../../docs/user/develop/practice/index.md) own setup and safety instructions. +`config/examples/` ships opt-in overlays for GitHub review webhooks, memory MCP servers, and runtime Cordis tools. They are never part of a default profile; the [user guides](../../docs/user/guide/index.md) and [developer practice guides](../../docs/user/develop/practice/index.md) own setup and safety instructions. ## Development diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index 08db105b16..ac8f61a24a 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -51,7 +51,7 @@ profile 目录包含一个 `package.json`,其中记录树外插件依赖,以 ## 可选覆盖层 -`config/examples/` 交付 GitHub 评审 webhook、会话内 Schedule、记忆 MCP 服务器与运行时 Cordis 工具的可选覆盖层。它们绝不属于默认 profile;设置与安全说明由[用户指南](../../docs/user/guide/index.zh.md)和[开发实战指南](../../docs/user/develop/practice/index.zh.md)负责。 +`config/examples/` 交付 GitHub 评审 webhook、记忆 MCP 服务器与运行时 Cordis 工具的可选覆盖层。它们绝不属于默认 profile;设置与安全说明由[用户指南](../../docs/user/guide/index.zh.md)和[开发实战指南](../../docs/user/develop/practice/index.zh.md)负责。 ## 开发 diff --git a/apps/cli/config/examples/schedule/cordis.yml b/apps/cli/config/examples/schedule/cordis.yml deleted file mode 100644 index eab335e0a5..0000000000 --- a/apps/cli/config/examples/schedule/cordis.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Opt-in host Schedule service and its Session task catalog. - -- insert: - - id: time-context - name: '@deepseek-ai/dsh-time-context' - - - id: schedule - name: '@deepseek-ai/dsh-schedule' - -- id: ui-schedule - disabled: false diff --git a/apps/cli/package.json b/apps/cli/package.json index fb5f762794..33eac62c0b 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -99,7 +99,8 @@ "@deepseek-ai/dsh-plugin-manager": "workspace:*", "@deepseek-ai/dsh-atomic-write": "workspace:*", "@deepseek-ai/dsh-agent-preset": "workspace:*", - "@deepseek-ai/dsh-experimental-voice-input-bundle": "workspace:*" + "@deepseek-ai/dsh-experimental-voice-input-bundle": "workspace:*", + "@deepseek-ai/dsh-experimental-auto-review": "workspace:*" }, "devDependencies": { "@agentclientprotocol/sdk": "1.4.0", diff --git a/apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts b/apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts index 14081ab1a1..063f578de9 100644 --- a/apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts +++ b/apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts @@ -3,12 +3,10 @@ import { randomUUID } from 'node:crypto' import { mkdir, readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' -import { fileURLToPath } from 'node:url' import type { DailyScheduleRecord, ScheduleCatalogEntry } from '@deepseek-ai/dsh-schedule' import { expect, it } from 'vitest' import { withDefaultWeb, webGet, webRequest } from './default-web-process.ts' -const overlay = fileURLToPath(new URL('../../../../config/examples/schedule/cordis.yml', import.meta.url)) const id = 'retained-remote-task' as DailyScheduleRecord['id'] const sessionId = 'unloaded-owner' as ScheduleCatalogEntry['sessionId'] const record: DailyScheduleRecord = { @@ -84,7 +82,7 @@ it('edits a retained task through built Remote bindings and preserves its identi expect(await rpc('schedule/update', { request: { sessionId: 'other-owner', id, expected: updated, change } })) .toEqual({ ok: true, value: { id, updated: false, code: 'schedule_not_found' } }) expect(await readFile(taskPath(root), 'utf8')).toBe(persisted) - }, { patches: [overlay], prepare: root => seed(root, persisted) }) + }, { prepare: root => seed(root, persisted) }) await withDefaultWeb(test, async ({ root, url }) => { const rpc = await connect(url, test.signal) @@ -94,7 +92,7 @@ it('edits a retained task through built Remote bindings and preserves its identi sessionId, id, expected: updated, change: { kind: 'daily', daily: { time: updated.time, time_zone: 'Etc/UTC' } }, } })).toEqual({ ok: true, value: { id, updated: false, record: updated } }) expect(await readFile(taskPath(root), 'utf8')).toBe(persisted) - }, { patches: [overlay], prepare: root => seed(root, persisted) }) + }, { prepare: root => seed(root, persisted) }) }) it('creates and idempotently adopts the explicit Session id bound to seeded task data without touching its task file', async (test) => { @@ -116,5 +114,5 @@ it('creates and idempotently adopts the explicit Session id bound to seeded task expect(await rpc('schedule/list', { request: { sessionId } })).toEqual(expectedList) expect(await rpc('schedule/history', { request: { sessionId, id, limit: 20 } })).toEqual(expectedHistory) expect(await readFile(taskPath(root), 'utf8')).toBe(persisted) - }, { patches: [overlay], prepare: root => seed(root, persisted) }) + }, { prepare: root => seed(root, persisted) }) }) diff --git a/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts b/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts index ecf19a7253..f98616f9a9 100644 --- a/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts +++ b/apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts @@ -24,6 +24,11 @@ it('boots default Web without experimental modules or an active built-in Browser expect(roster.entries).toEqual(expect.arrayContaining([ expect.objectContaining({ name: '@deepseek-ai/dsh-host-webserver', state: FiberState.ACTIVE }), expect.objectContaining({ name: '@deepseek-ai/dsh-client-modules', state: FiberState.ACTIVE }), + // The Loader rewrites an inserted row's id (`include:ui-schedule`), so + // the shipped rows are pinned by name and settled state. + expect.objectContaining({ name: '@deepseek-ai/dsh-time-context', state: FiberState.ACTIVE }), + expect.objectContaining({ name: '@deepseek-ai/dsh-schedule', state: FiberState.ACTIVE }), + expect.objectContaining({ name: '@deepseek-ai/dsh-client-ui-schedule', state: FiberState.ACTIVE }), ])) expect(roster.entries.some(entry => entry.name.endsWith('/runtime-roster-observer.js') && entry.state === FiberState.ACTIVE)).toBe(true) expect(roster.plugins.length).toBeGreaterThan(roster.entries.length) diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index d4dc6653f2..7d3a25f9af 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -23,6 +23,9 @@ import { dump, load } from 'js-yaml' import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' import { bundlePatchPaths, composeEntries } from '@deepseek-ai/dsh-app-boot' /** Profile entry ids whose volatile fields these scenarios edit through Settings. */ +/** Host Schedule tools every live root Agent receives from the shipped Web bundle. */ +const SCHEDULE_TOOLS = ['schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update'] + const SETTINGS_NAMESPACE = 'agent-preset-registry' const SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE = 'subagent-model-selection-settings' import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent' @@ -263,7 +266,8 @@ describe('the shipped Web composition', () => { // depend on ripgrep being present on the machine. expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([ 'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode', - 'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'present', 'read', 'read_image', 'send_message', 'skill', + 'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'present', 'read', 'read_image', 'schedule_create', + 'schedule_delete', 'schedule_list', 'schedule_update', 'send_message', 'skill', 'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write', ]) @@ -317,7 +321,7 @@ describe('the shipped Web composition', () => { expect(assembly.sections).toEqual([ { name: 'deployment:persona-prefix', text: MINIMAL_PROMPT }, ]) - expect(assembly.tools.map(tool => tool.name)).toEqual(['bash']) + expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', ...SCHEDULE_TOOLS]) expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION) expect(ctx.commands.find(handle.agent, 'goal')).toBeUndefined() // serviceFor reports preset-owned providers; unisolated consumers inherit the host fs. @@ -341,7 +345,7 @@ describe('the shipped Web composition', () => { setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined), }) try { - expect(toolNames(ctx, minimal.agent)).toEqual(['bash']) + expect(toolNames(ctx, minimal.agent)).toEqual(['bash', ...SCHEDULE_TOOLS]) expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10) await minimal.dispose() @@ -507,7 +511,7 @@ describe('the shipped Web composition', () => { // stays the preset's choice — minimal mounts no `tool-skill`, so its // tool table has no loader even though the global layer is readable. expect((await ctx.skills.list({ scope: handle.agent })).map(skill => skill.name)).toContain('dsh-badge') - expect(toolNames(ctx, handle.agent)).toEqual(['bash']) + expect(toolNames(ctx, handle.agent)).toEqual(['bash', ...SCHEDULE_TOOLS]) } finally { await handle.dispose() } @@ -681,10 +685,14 @@ describe('a user preset declared from the shipped cordis rows', () => { agent: copied.agent, }) expect(queried.isError).toBe(false) - const tools = (JSON.parse(resultText(queried)) as { data: { tools: Array<{ name: string }> } }).data.tools - expect(tools.map(tool => tool.name)).toEqual(expect.arrayContaining([ - 'bash', 'cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager', - ])) + // The whole-table answer can pass the inline token budget once the + // preset's tool table grows, and the spill policy then replaces its + // middle with a gap plus a spill footer. What this case proves is the + // provider's answer, so assert the names it must carry. + const queriedText = resultText(queried) + for (const toolName of ['bash', 'cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager']) { + expect(queriedText).toContain(`"name": "${toolName}"`) + } // The `Config` provider reads the booted profile tree: the shipped `tools` row declares a Config, // and the bootstrap include row is a carrier. The name filter keeps each page small. @@ -845,7 +853,6 @@ describe('a delegated child', () => { describe('the default preset as a user setting', () => { it('composes an unnamed session from the stored default, not the composed one', async () => { - expect((await ctx.agentPresets.remoteExportList()).modeSelectionEnabled).toBe(true) expect(ctx.agentPresets.defaultId).toBe('standard') await ctx.settings.update(SETTINGS_NAMESPACE, { selectedDefault: 'minimal' }) @@ -859,7 +866,7 @@ describe('the default preset as a user setting', () => { try { // `mount()` with no id resolves the effective default. One tool, not // `standard`'s catalog: the setting decided the composition. - expect(toolNames(ctx, handle.agent)).toEqual(['bash']) + expect(toolNames(ctx, handle.agent)).toEqual(['bash', ...SCHEDULE_TOOLS]) } finally { await handle.dispose() } @@ -874,6 +881,44 @@ describe('the default preset as a user setting', () => { }) }) +describe('a profile patch stored before Developer tools owned preset selection', () => { + let legacy: Context + let legacyHome: string + beforeAll(async () => { + legacyHome = await mkdtemp(join(tmpdir(), 'dsh-web-presets-legacy-')) + // A stored configuration from before the switch moved to Developer tools: + // it carries the retired key beside the default the user had saved. The + // Loader resolves the declared fields and leaves the extra one alone. + legacy = await bootWeb(legacyHome, [{ + id: SETTINGS_NAMESPACE, + config: { default: 'standard', selectedDefault: 'minimal', modeSelectionEnabled: false }, + }]) + }, 120_000) + afterAll(async () => { + await legacy?.fiber.dispose() + await rm(legacyHome, { recursive: true, force: true }) + }) + + it('starts, keeps the retired key inert and composes new sessions from the saved default', async () => { + expect(legacy.agentPresets.defaultId).toBe('minimal') + expect((await legacy.agentPresets.remoteExportList()).presets.find(row => row.id === 'minimal')?.isDefault).toBe(true) + // Settings projects the declared fields, so the retired key is neither + // shown nor rewritten; the user's saved default is. + expect(legacy.settings.describe().find(row => row.ns === SETTINGS_NAMESPACE)?.value) + .toEqual({ selectedDefault: 'minimal' }) + + const handle = await legacy.agents.create({ + sessionId: SessionId('preset-legacy-patch'), + setup: agentCtx => legacy.agentPresets.mount(agentCtx).then(() => undefined), + }) + try { + expect(toolNames(legacy, handle.agent)).toEqual(['bash', ...SCHEDULE_TOOLS]) + } finally { + await handle.dispose() + } + }) +}) + describe('a session keeps the preset it was created with', () => { it('refuses to adopt a live session under a different preset', async () => { const handle = await ctx.agents.create({ diff --git a/apps/desktop/src/preload-menu.ts b/apps/desktop/src/preload-menu.ts index aa2f54a423..f7b881278a 100644 --- a/apps/desktop/src/preload-menu.ts +++ b/apps/desktop/src/preload-menu.ts @@ -22,7 +22,8 @@ export function installWindowsMenu(): { update(): void; dispose(): void } { font: inherit; font-size: 14px; cursor: default; } button:hover, button[aria-expanded=true] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); } - button:focus-visible { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: -2px; } + button:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: -2px; } + :host-context(html[data-input-modality='pointer']) button:focus-visible { outline-color: transparent; } ` const bar = document.createElement('div') bar.setAttribute('role', 'menubar') diff --git a/apps/web/tests/agent-preset-selection.e2e.ts b/apps/web/tests/agent-preset-selection.e2e.ts index 911b763592..cfa4e81252 100644 --- a/apps/web/tests/agent-preset-selection.e2e.ts +++ b/apps/web/tests/agent-preset-selection.e2e.ts @@ -249,7 +249,7 @@ describe('web e2e: agent-preset selection', () => { await rm(fixtureRoot, { recursive: true, force: true }) }) - it('starts with mode selection shown on the Standard default', async () => { + it('starts on the Standard default with the roster editable in Settings', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-hero')) await connectFreshWorkspace(page, scaffold.workspaceCwd) await page.getByRole('button', { name: 'Standard mode', exact: true }).waitFor({ timeout: 10_000 }) @@ -257,9 +257,9 @@ describe('web e2e: agent-preset selection', () => { await openSettings(page, 'en') const dialog = page.getByRole('dialog', { name: 'Settings' }) await dialog.getByRole('button', { name: 'Agent presets' }).click() - const toggle = dialog.getByRole('switch', { name: 'Choose a mode for new tasks' }) await dialog.getByRole('button', { name: 'New task default: Standard mode' }).waitFor({ timeout: 10_000 }) - expect(await toggle.getAttribute('aria-checked')).toBe('true') + expect(await dialog.getByRole('switch').count()).toBe(0) + await dialog.getByRole('button', { name: 'Set as new task default: Minimal mode' }).waitFor({ timeout: 10_000 }) await dialog.getByRole('button', { name: 'Close' }).last().click() const snapshot = await captureStableAria(page, '[class*="heroWorkspaceRow"]', scaffold.workspaceCwd) @@ -343,7 +343,7 @@ describe('web e2e: agent-preset selection', () => { await writeComposerDraft(page, composer, '') }, 90_000) - it('aligns the current blank task and restores its saved default when re-enabled', async () => { + it('keeps the saved default composing sessions while Developer tools only gate the choice', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-disabled')) await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard') @@ -353,23 +353,23 @@ describe('web e2e: agent-preset selection', () => { await dialog.getByRole('button', { name: 'Set as new task default: Minimal mode' }).click() await dialog.getByRole('button', { name: 'New task default: Minimal mode' }).waitFor({ timeout: 10_000 }) await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('minimal') - const toggle = dialog.getByRole('switch', { name: 'Choose a mode for new tasks' }) - await toggle.click() - await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false') - await dialog.getByRole('button', { name: 'Application default: Standard mode' }).waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: 'General', exact: true }).click() + const developerTools = dialog.getByRole('switch', { name: 'Developer tools' }) + await developerTools.click() + await expect.poll(() => developerTools.getAttribute('aria-checked')).toBe('false') await dialog.getByRole('button', { name: 'Close' }).last().click() + // The gate hides the choice; the blank task keeps its saved composition. await expect.poll(() => page.getByRole('button', { name: / mode$/ }).count()).toBe(0) - await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard') + await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('minimal') - // The switch controls availability only: re-enabling restores the saved - // default and aligns this same still-blank task with it. await openSettings(page, 'en') const reopened = page.getByRole('dialog', { name: 'Settings' }) + await reopened.getByRole('button', { name: 'General', exact: true }).click() + const reopenedDeveloperTools = reopened.getByRole('switch', { name: 'Developer tools' }) + await reopenedDeveloperTools.click() + await expect.poll(() => reopenedDeveloperTools.getAttribute('aria-checked')).toBe('true') await reopened.getByRole('button', { name: 'Agent presets' }).click() - const reopenedToggle = reopened.getByRole('switch', { name: 'Choose a mode for new tasks' }) - await reopenedToggle.click() - await expect.poll(() => reopenedToggle.getAttribute('aria-checked')).toBe('true') await reopened.getByRole('button', { name: 'New task default: Minimal mode' }).waitFor({ timeout: 10_000 }) await reopened.getByRole('button', { name: 'Close' }).last().click() await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('minimal') diff --git a/apps/web/tests/assembled-remote.ts b/apps/web/tests/assembled-remote.ts index fd617ba974..7569106fc6 100644 --- a/apps/web/tests/assembled-remote.ts +++ b/apps/web/tests/assembled-remote.ts @@ -223,6 +223,10 @@ export function createAssembledRemote(options: AssembledRemoteOptions = {}): Ass return ok(undefined) }) mock.unary('session/list', () => ok({ items: structuredClone(sessions) })) + // The shipped Web composition mounts the Schedule client, whose catalog and + // per-Session list read empty here: these scenarios drive no reminder. + mock.unary('schedule/catalog', () => ok([])) + mock.unary('schedule/list', () => ok([])) mock.unary('session/projections', (request: unknown) => { const sessionId = recordString(recordValue(request, 'request'), 'sessionId') const summary = sessions.find(candidate => candidate.sessionId === sessionId) diff --git a/apps/web/tests/developer-tools-settings.e2e.ts b/apps/web/tests/developer-tools-settings.e2e.ts index 28bec282cc..aec863b432 100644 --- a/apps/web/tests/developer-tools-settings.e2e.ts +++ b/apps/web/tests/developer-tools-settings.e2e.ts @@ -25,16 +25,18 @@ it('persists developer tools in the Host settings document and restores the acce await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false') await page.getByRole('button', { name: 'Agent presets', exact: true }).click() - const pickerToggle = page.getByRole('switch', { name: 'Choose a mode for new tasks' }) await expect.poll(() => page.getByRole('heading', { name: 'Agent presets', exact: true }).count()).toBe(1) - expect(await pickerToggle.count()).toBe(0) + // The page owns no selection switch: only the General one gates the choice. + const section = page.locator('section') + .filter({ has: page.getByRole('heading', { name: 'Agent presets', exact: true }) }) + const refused = section.getByRole('button', { name: /^Turn on Developer tools in General settings to choose a default: / }) + expect(await section.getByRole('switch').count()).toBe(0) + await expect.poll(() => refused.count()).toBeGreaterThan(0) await scaffold.ctx.settings.update('ui-settings', { enabled: true }) - await expect.poll(() => pickerToggle.count()).toBe(1) - await expect.poll(() => pickerToggle.isEnabled()).toBe(true) - const pickerEnabled = await pickerToggle.getAttribute('aria-checked') + await expect.poll(() => section.getByRole('button', { name: 'Set as new task default: Minimal mode' }).count()).toBe(1) + await expect.poll(() => refused.count()).toBe(0) + expect(await section.getByRole('switch').count()).toBe(0) await scaffold.ctx.settings.update('ui-settings', { enabled: false }) - await expect.poll(() => pickerToggle.count()).toBe(0) - await scaffold.ctx.settings.update('ui-settings', { enabled: true }) - await expect.poll(() => pickerToggle.isEnabled()).toBe(true) - await expect.poll(() => pickerToggle.getAttribute('aria-checked')).toBe(pickerEnabled) + await expect.poll(() => refused.count()).toBeGreaterThan(0) + expect(await section.getByRole('switch').count()).toBe(0) }) diff --git a/apps/web/tests/expected/agent-preset-authoring/section.expected.md b/apps/web/tests/expected/agent-preset-authoring/section.expected.md index 4ce3d29216..a7d47106e1 100644 --- a/apps/web/tests/expected/agent-preset-authoring/section.expected.md +++ b/apps/web/tests/expected/agent-preset-authoring/section.expected.md @@ -9,9 +9,6 @@ - button "关闭" - heading "Agent 预设" [level=2] - paragraph: 选择 Agent 的工具和工作方式。日常任务用「标准模式」,扩展 DSH 的能力用「创造模式」。 - - text: 新任务可选择模式 实验性 - - paragraph: 开启后,可为每个新任务选择模式,并在这里设置默认值。关闭后,新任务使用应用配置的默认预设。已有任务不受影响。 - - switch "新任务可选择模式" [checked] - heading "内置" [level=3] - list: - listitem: diff --git a/apps/web/tests/expected/markdown-images/ui.expected.md b/apps/web/tests/expected/markdown-images/ui.expected.md index 31dee5c21a..e830dad629 100644 --- a/apps/web/tests/expected/markdown-images/ui.expected.md +++ b/apps/web/tests/expected/markdown-images/ui.expected.md @@ -27,6 +27,9 @@ - paragraph: - 'button "View full image: Encoded path"': - img "Encoded path" +- paragraph: + - 'button "View full image: Encoded filename"': + - img "Encoded filename" - paragraph: Image preview unavailable · Oversized image - paragraph: - 'button "View full image: Outside workspace image"': diff --git a/apps/web/tests/expected/plugin-config/official.expected.md b/apps/web/tests/expected/plugin-config/official.expected.md index dd8775171f..4e53ddd016 100644 --- a/apps/web/tests/expected/plugin-config/official.expected.md +++ b/apps/web/tests/expected/plugin-config/official.expected.md @@ -3,12 +3,16 @@ - button "刷新" - button "添加插件" - heading "官方" [level=3] -- text: "6" +- text: "7" - list: - listitem: - button "查看 智能体团队": 智能体团队 - text: 实验性 启用团队协作、团队工具、成员列表和共享任务看板。 - switch "启用 智能体团队" + - listitem: + - button "查看 自动授权审查": 自动授权审查 + - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 + - switch "启用 自动授权审查" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音;首次使用需安装依赖 diff --git a/apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md b/apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md index e9988fe58b..14d7992611 100644 --- a/apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md +++ b/apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md @@ -3,7 +3,7 @@ - button "关闭" - paragraph: 输入插件的包名、GitHub 仓库地址或本地目录路径。 - textbox "包名或地址": - - /placeholder: 例如 @deepseek-ai/dsh-experimental-auto-review + - /placeholder: 例如 @deepseek-ai/dsh-subagent-codex - text: slow-package - button "插件安装引导和示例" - button "安装源 默认安装源" diff --git a/apps/web/tests/expected/plugin-install-github/another-way.expected.md b/apps/web/tests/expected/plugin-install-github/another-way.expected.md index fa922891c7..b36f699bb5 100644 --- a/apps/web/tests/expected/plugin-install-github/another-way.expected.md +++ b/apps/web/tests/expected/plugin-install-github/another-way.expected.md @@ -2,14 +2,14 @@ - heading "添加插件" [level=2] - button "关闭" - textbox "插件包名": - - /placeholder: 例如 @deepseek-ai/dsh-experimental-auto-review + - /placeholder: 例如 @deepseek-ai/dsh-subagent-codex - button "收起引导" [expanded] - button "安装源 中国大陆镜像源" - list: - listitem: - text: 包名 插件包名即 npm 包名(如 dsh-xxx 或 @作者/插件名),社区插件的 README 安装命令中 dsh plugin add 或 pnpm add 之后的部分。 示例: - - code: "@deepseek-ai/dsh-experimental-auto-review" - - button "填入示例 @deepseek-ai/dsh-experimental-auto-review": 填入示例 + - code: "@deepseek-ai/dsh-subagent-codex" + - button "填入示例 @deepseek-ai/dsh-subagent-codex": 填入示例 - listitem: - text: GitHub 仓库地址 插件在 GitHub 上的开源仓库地址,也支持其他 Git 仓库。 示例: - code: https://github.com/author/dsh-plugin diff --git a/apps/web/tests/expected/plugin-install-github/mirror.expected.md b/apps/web/tests/expected/plugin-install-github/mirror.expected.md index fbdc9a5116..b649252d7d 100644 --- a/apps/web/tests/expected/plugin-install-github/mirror.expected.md +++ b/apps/web/tests/expected/plugin-install-github/mirror.expected.md @@ -2,7 +2,7 @@ - heading "添加插件" [level=2] - button "关闭" - textbox "插件包名": - - /placeholder: 例如 @deepseek-ai/dsh-experimental-auto-review + - /placeholder: 例如 @deepseek-ai/dsh-subagent-codex - button "插件安装引导和示例" - button "安装源 中国大陆镜像源" - button "安装" [disabled] diff --git a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md index f7d0ebbe50..e9b345be5f 100644 --- a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md +++ b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md @@ -3,12 +3,16 @@ - button "刷新" - button "添加插件" - heading "官方" [level=3] -- text: "6" +- text: "7" - list: - listitem: - button "查看 智能体团队": 智能体团队 - text: 实验性 启用团队协作、团队工具、成员列表和共享任务看板。 - switch "启用 智能体团队" + - listitem: + - button "查看 自动授权审查": 自动授权审查 + - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 + - switch "启用 自动授权审查" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音;首次使用需安装依赖 diff --git a/apps/web/tests/expected/plugin-manager/manager.expected.md b/apps/web/tests/expected/plugin-manager/manager.expected.md index 3f33e93302..978daa2f30 100644 --- a/apps/web/tests/expected/plugin-manager/manager.expected.md +++ b/apps/web/tests/expected/plugin-manager/manager.expected.md @@ -3,12 +3,16 @@ - button "刷新" - button "添加插件" - heading "官方" [level=3] -- text: "6" +- text: "7" - list: - listitem: - button "查看 智能体团队": 智能体团队 - text: 实验性 启用团队协作、团队工具、成员列表和共享任务看板。 - switch "启用 智能体团队" + - listitem: + - button "查看 自动授权审查": 自动授权审查 + - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 + - switch "启用 自动授权审查" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音;首次使用需安装依赖 diff --git a/apps/web/tests/fixtures/assembled-remote.fixture.json b/apps/web/tests/fixtures/assembled-remote.fixture.json index e375c6deb5..c939abbc92 100644 --- a/apps/web/tests/fixtures/assembled-remote.fixture.json +++ b/apps/web/tests/fixtures/assembled-remote.fixture.json @@ -181,8 +181,7 @@ "trust": "user", "isDefault": false } - ], - "modeSelectionEnabled": true + ] } }, "commands": { diff --git a/apps/web/tests/fixtures/time-context-every-step.patch.yml b/apps/web/tests/fixtures/time-context-every-step.patch.yml new file mode 100644 index 0000000000..22c6bd919f --- /dev/null +++ b/apps/web/tests/fixtures/time-context-every-step.patch.yml @@ -0,0 +1,4 @@ +# Browser-zone assertions need a fresh reading after the non-browser preparation turn. +- id: time-context + config: + refreshIntervalMs: 0 diff --git a/apps/web/tests/focus-rings.e2e.ts b/apps/web/tests/focus-rings.e2e.ts new file mode 100644 index 0000000000..3f7d10b666 --- /dev/null +++ b/apps/web/tests/focus-rings.e2e.ts @@ -0,0 +1,529 @@ +/** Real assembled UI plus source-compiled browser regressions for focus-ring paint. */ +import { join } from 'node:path' +import { build } from 'vite' +import { chromium, type Locator, type Page } from 'playwright' +import { beforeAll, describe, expect, it, onTestFinished } from 'vitest' +import { launchWebScaffold, watchConsole } from './scaffold.ts' +import { connectFreshWorkspace, newEnglishPage, REPO_ROOT } from './support.ts' + +/** Read rendered paint and browser focus, not just the modality publisher's attribute. */ +async function paint(target: Locator) { + return target.evaluate((element) => { + const style = getComputedStyle(element) + const resolveColor = (value: string): string => { + const probe = document.createElement('span') + probe.style.color = value + document.body.append(probe) + const color = getComputedStyle(probe).color + probe.remove() + return color + } + return { + active: document.activeElement === element, + focusVisible: element.matches(':focus-visible'), + modality: document.documentElement.getAttribute('data-input-modality'), + focusColor: resolveColor(style.getPropertyValue('--dsw-alias-state-business-primary').trim()), + hover: resolveColor(style.getPropertyValue('--dsw-alias-interactive-bg-hover').trim()), + outline: style.outlineColor, + outlineStyle: style.outlineStyle, + outlineWidth: style.outlineWidth, + shadow: style.boxShadow, + background: style.backgroundColor, + } + }) +} + +async function expectSilent(target: Locator): Promise { + const state = await paint(target) + expect(state.active).toBe(true) + expect(state.modality).toBe('pointer') + expect(state.focusColor).toMatch(/^rgb/) + expect(state.outlineStyle === 'none' || state.outline === 'rgba(0, 0, 0, 0)').toBe(true) + expect(state.shadow).not.toContain(state.focusColor) +} + +async function expectFocusRing(target: Locator, kind: 'outline' | 'shadow'): Promise { + await expect.poll(async () => { + const state = await paint(target) + return { + active: state.active, + visible: state.focusVisible, + modality: state.modality, + painted: kind === 'shadow' + ? state.shadow.includes(state.focusColor) + : state.outline === state.focusColor && state.outlineStyle === 'solid' && Number.parseFloat(state.outlineWidth) > 0, + } + }).toEqual({ active: true, visible: true, modality: 'keyboard', painted: true }) +} + +/** + * Name every ancestor that cuts the ring's painted box. A control flush with an + * `overflow: hidden` ancestor keeps its outline geometrically but loses the pixels that leave + * that ancestor, so compare the layout box grown by the ring against each clipping ancestor. + */ +async function ringCutBy(target: Locator): Promise { + return target.evaluate((element) => { + const style = getComputedStyle(element) + const width = Number.parseFloat(style.outlineWidth) || 0 + const offset = Number.parseFloat(style.outlineOffset) || 0 + const box = element.getBoundingClientRect() + const ring = { + left: box.left - width - offset, + top: box.top - width - offset, + right: box.right + width + offset, + bottom: box.bottom + width + offset, + } + const cuts: string[] = [] + for (let node = element.parentElement; node !== null; node = node.parentElement) { + const overflow = getComputedStyle(node) + if (overflow.overflow === 'visible' && overflow.overflowX === 'visible' && overflow.overflowY === 'visible') continue + const rect = node.getBoundingClientRect() + const cut = { + left: Math.max(0, rect.left - ring.left), + top: Math.max(0, rect.top - ring.top), + right: Math.max(0, ring.right - rect.right), + bottom: Math.max(0, ring.bottom - rect.bottom), + } + if (cut.left + cut.top + cut.right + cut.bottom > 0.5) { + cuts.push(`${node.tagName.toLowerCase()}${node.className === '' ? '' : `.${node.className}`} cuts ${JSON.stringify(cut)}`) + } + } + return cuts + }) +} + +it.each(['light', 'dark'] as const)('assembled app (%s): pointer keys stay silent; navigation and menu activation retain feedback', async (theme) => { + const scaffold = await launchWebScaffold() + onTestFinished(() => scaffold.close()) + const browser = await chromium.launch({ headless: true }) + onTestFinished(() => browser.close()) + const page = await newEnglishPage(browser) + const consoleWatch = watchConsole(page) + await page.emulateMedia({ colorScheme: theme }) + await page.goto(scaffold.authenticatedUrl) + await connectFreshWorkspace(page, scaffold.workspaceCwd) + await expect.poll(() => page.locator('body').getAttribute('data-ds-dark-theme')).toBe(theme === 'dark' ? '' : null) + const trigger = page.getByRole('button', { name: /^Access mode, current:/ }) + expect((await paint(trigger)).focusColor).toBe(theme === 'dark' ? 'rgb(122, 170, 255)' : 'rgb(65, 118, 230)') + + await trigger.click() + await page.getByRole('menu').waitFor() + await expectSilent(trigger) + await page.keyboard.press('Shift') + await expectSilent(trigger) + await page.keyboard.press('Escape') + await page.getByRole('menu').waitFor({ state: 'hidden' }) + await expectSilent(trigger) + + for (const key of ['Home', 'End']) { + await trigger.click() + await page.keyboard.press('Escape') + await expectSilent(trigger) + await page.keyboard.press(key) + await expectFocusRing(trigger, 'shadow') + } + + await trigger.click() + await page.keyboard.press('Escape') + await page.keyboard.press('Shift+Tab') + expect(await trigger.evaluate(element => element === document.activeElement)).toBe(false) + await page.keyboard.press('Tab') + await expectFocusRing(trigger, 'shadow') + for (const key of ['Enter', 'Space']) { + await page.keyboard.press(key) + await page.getByRole('menu').waitFor() + await expectFocusRing(trigger, 'shadow') + const menu = page.getByRole('menu') + for (const [navigation, index] of [['End', -1], ['Home', 0]] as const) { + await page.keyboard.press(navigation) + const row = menu.getByRole('menuitem').nth(index) + const state = await paint(row) + expect(state.active).toBe(true) + expect(state.focusVisible).toBe(true) + expect(state.modality).toBe('keyboard') + // Menu rows intentionally use the hover fill instead of a second outline. + expect(state.background).toBe(state.hover) + expect(state.background).not.toBe('rgba(0, 0, 0, 0)') + } + await page.keyboard.press('Escape') + await expectFocusRing(trigger, 'shadow') + } + expect(consoleWatch.warnings).toEqual([]) + expect(consoleWatch.pageErrors).toEqual([]) +}) + +it.each(['light', 'dark'] as const)('assembled app (%s): the sidebar toggle keeps its whole ring in the expanded and rail layouts', async (theme) => { + const scaffold = await launchWebScaffold() + onTestFinished(() => scaffold.close()) + const browser = await chromium.launch({ headless: true }) + onTestFinished(() => browser.close()) + const page = await newEnglishPage(browser) + await page.emulateMedia({ colorScheme: theme }) + await page.goto(scaffold.authenticatedUrl) + await connectFreshWorkspace(page, scaffold.workspaceCwd) + + const toggle = page.getByRole('button', { name: 'Collapse sidebar' }) + const rail = page.getByRole('button', { name: 'Open sidebar' }) + /** Sample the ring box once the collapse animation stops moving the control. */ + const settle = async (target: Locator, width: number): Promise => { + await expect.poll(() => target.evaluate(element => element.getBoundingClientRect().width)).toBe(width) + await target.evaluate(async (element) => { + for (let node: Element | null = element; node !== null; node = node.parentElement) { + await Promise.all(node.getAnimations().map(animation => animation.finished)) + } + }) + } + + await toggle.click() + await rail.waitFor() + // The rail only replaces the frozen wide layout once the collapse settles. + await settle(rail, 36) + await expectSilent(rail) + await page.keyboard.press('Home') + await expectFocusRing(rail, 'outline') + expect(await ringCutBy(rail)).toEqual([]) + + await rail.click() + await toggle.waitFor() + await settle(toggle, 28) + await expectSilent(toggle) + await page.keyboard.press('Home') + await expectFocusRing(toggle, 'outline') + expect(await ringCutBy(toggle)).toEqual([]) +}) + +/** + * Supplementary fixture, not an assembled app: Vite compiles the actual Menu, + * Switch, input-modality module and CSS Modules in memory. Only arrangement and + * state ownership belong to the fixture; no test writes the modality attribute. + */ +async function compileFixture(): Promise<{ script: string; css: string }> { + const source = (relative: string): string => JSON.stringify(join(REPO_ROOT, 'packages/client', relative).replaceAll('\\', '/')) + const entry = join(REPO_ROOT, 'apps/web/focus-rings-fixture.tsx').replaceAll('\\', '/') + const code = ` + import React, { useState } from 'react' + import { createRoot } from 'react-dom/client' + import { Menu } from ${source('ui-primitives/src/Menu.tsx')} + import { Switch } from ${source('ui-primitives/src/Switch.tsx')} + import ${source('ui-primitives/src/input-modality.ts')} + import ${source('ui-theme/src/styles/base.css')} + import ${source('ui-theme/src/styles/design-platform.css')} + import ${source('ui-theme/src/styles/focus.css')} + import ${source('ui-theme/src/styles/gradient-shadow-text.css')} + import triggerCss from ${source('ui-permission-presets/src/client/PermissionSelect.module.css')} + import feedbackCss from ${source('ui-message-feedback/src/client/FeedbackDialog.module.css')} + import workflowCss from ${source('ui-workflow-run/src/client/WorkflowRunPanel.module.css')} + import cardCss from ${source('ui-primitives/src/HoverCard.module.css')} + import pillCss from ${source('ui-primitives/src/Pill.module.css')} + import trajectoryCss from ${source('ui-trajectory/src/client/TrajectoryTable.module.css')} + import onboardingCss from ${source('ui-settings-account/src/client/DesktopOnboarding.module.css')} + function Fixture() { + const [checked, setChecked] = useState(false) + const [open, setOpen] = useState(false) + return
+
+ +
+ + + + setOpen(false)} + anchor={} + items={[{ id: 'first', label: 'First item' }, { id: 'last', label: 'Last item' }]} /> +