Merge origin/master into ihsiang/shortcut-sidebar-polish
This commit is contained in:
commit
eccfda04d6
346 files changed
+4579
-1957
No files matched your search
+10
-10
@@ -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
|
||||
|
||||
+6
-7
@@ -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
|
||||
|
||||
|
||||
+6
-7
@@ -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。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
+2
-2
@@ -15,5 +15,5 @@
|
||||
en: 7a3d4b9f69c243d8
|
||||
zh: 82b03d16ccc94dd2
|
||||
/agent-note-parent-owned-subagent-catalog-events/consequences:
|
||||
en: 97f2a8beb039a075
|
||||
zh: 80efa9f6dd12d438
|
||||
en: d582273d5102d617
|
||||
zh: 7f05590581b1df88
|
||||
@@ -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.
|
||||
+1
-1
@@ -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) 说明目录事实为何不包含模型配置。
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
en: 21d7ec9a9ff3cbf2
|
||||
zh: 93d03b8b3012aa7e
|
||||
/agent-note-host-owned-scheduled-messages/consequences:
|
||||
en: 5b8d0a26024f795e
|
||||
zh: c4d6f36d54783c47
|
||||
en: 0358aab730e4bf56
|
||||
zh: 61e89e0374891f8a
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 中收窄任务、页头时钟在恰好一个任务时直接打开并给出数量标签、仅空闲行标记及其悬浮卡片两个任务上限与遗漏行。历史覆盖范围包括旧文件字节保留、追加与重启、不可变的发送指令、最近回执一致性严格校验、稳定游标分页、绑定校验、持久化失败,以及刷新或切换任务期间晚到的分页响应。跨存储崩溃导致的重复投递仍是明确限制,不能被表述为完成保证。
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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)列出这些条目。
|
||||
|
||||
## 后果
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Preset 目录重复承担 Cordis 配置的所有权。独立的发现、元数
|
||||
|
||||
Web 编辑器与 `agent_preset` 工具只接受子插件 YAML。保存写入当前 profile 的用户补丁,保留显示元数据和无关配置。版本检查和 profile 锁防止覆盖并发保存;验证拒绝会使编辑失效的更高优先级覆盖。持久化与激活结果分别报告。配置在 Host 中执行,因此工具写入需要批准或完全访问权限。
|
||||
|
||||
会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复。旧的可执行代际仅保留在进程内,不会序列化。现有选择器策略和空白会话选择保持不变:隐藏选择器使用部署默认值,但不删除保存的用户偏好。
|
||||
会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复。旧的可执行代际仅保留在进程内,不会序列化。保存的用户默认值与空白会话选择保持不变:未指定 preset 的新会话解析 `selectedDefault`,客户端的开发者工具偏好只决定是否展示这项选择。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+10
-10
@@ -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
|
||||
+3
-6
@@ -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.
|
||||
+3
-6
@@ -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 子代仍走正文观察。
|
||||
+19
@@ -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
|
||||
+25
@@ -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.
|
||||
+25
@@ -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 与提供方。
|
||||
+22
@@ -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
|
||||
@@ -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.
|
||||
+39
@@ -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 参数,一处组合改动即同时作用于所有界面。
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` 递归发现。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
+2
-2
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 级的。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)覆盖样式声明和模态转换。静态声明与合成事件不能确认焦点环的实际渲染可见性或跨平台行为。
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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` 的描述与客户端的提示文案中。
|
||||
- 投递行为、批处理、补跑与间隔运算法则不变:被接受的间隔仍以创建时刻或接受保存的时刻为起算点,每条逾期记录仍只贡献最新一次发生时点。
|
||||
- 部署无法通过配置提高该下限;它是协议常量,需要更长最小间隔的部署应在创建任务的界面上自行约束。
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)列出当前名单以及每一项携带的显示元数据。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -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
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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)负责。
|
||||
|
||||
## 开发
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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) })
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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'])
|
||||
} 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({
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -9,9 +9,6 @@
|
||||
- button "关闭"
|
||||
- heading "Agent 预设" [level=2]
|
||||
- paragraph: 选择 Agent 的工具和工作方式。日常任务用「标准模式」,扩展 DSH 的能力用「创造模式」。
|
||||
- text: 新任务可选择模式 实验性
|
||||
- paragraph: 开启后,可为每个新任务选择模式,并在这里设置默认值。关闭后,新任务使用应用配置的默认预设。已有任务不受影响。
|
||||
- switch "新任务可选择模式" [checked]
|
||||
- heading "内置" [level=3]
|
||||
- list:
|
||||
- listitem:
|
||||
|
||||
@@ -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"':
|
||||
|
||||
@@ -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 转写录音;首次使用需安装依赖
|
||||
|
||||
@@ -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 "安装源 默认安装源"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
- dialog "添加插件":
|
||||
- heading "添加插件" [level=2]
|
||||
- button "关闭"
|
||||
- textbox "插件包名":
|
||||
- /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-subagent-codex"
|
||||
- button "填入示例 @deepseek-ai/dsh-subagent-codex": 填入示例
|
||||
- listitem:
|
||||
- text: GitHub 仓库地址 插件在 GitHub 上的开源仓库地址,也支持其他 Git 仓库。 示例:
|
||||
- code: https://github.com/author/dsh-plugin
|
||||
- button "填入示例 https://github.com/author/dsh-plugin": 填入示例
|
||||
- listitem:
|
||||
- text: 本地插件目录 本机上插件目录的绝对路径,适用于自行开发或已下载的插件。 示例:
|
||||
- code: /Users/name/my-plugin
|
||||
- button "填入示例 /Users/name/my-plugin": 填入示例
|
||||
- note: 请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。
|
||||
- button "安装" [disabled]
|
||||
@@ -0,0 +1,6 @@
|
||||
- dialog "无法访问 GitHub":
|
||||
- heading "无法访问 GitHub" [level=2]
|
||||
- button "关闭"
|
||||
- paragraph: 请尝试其他安装来源。
|
||||
- button "取消"
|
||||
- button "试试其他方式"
|
||||
@@ -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]
|
||||
@@ -0,0 +1,6 @@
|
||||
- dialog "连接 GitHub 超时":
|
||||
- heading "连接 GitHub 超时" [level=2]
|
||||
- button "关闭"
|
||||
- paragraph: 请尝试其他安装来源。
|
||||
- button "取消"
|
||||
- button "试试其他方式"
|
||||
@@ -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 转写录音;首次使用需安装依赖
|
||||
|
||||
@@ -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 转写录音;首次使用需安装依赖
|
||||
|
||||
+1
-2
@@ -181,8 +181,7 @@
|
||||
"trust": "user",
|
||||
"isDefault": false
|
||||
}
|
||||
],
|
||||
"modeSelectionEnabled": true
|
||||
]
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<void> => {
|
||||
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 <main style={{ padding: 40, display: 'grid', gap: 24, width: 420 }}>
|
||||
<div data-onboarding-card className={onboardingCss.card}>
|
||||
<label><input type="checkbox" aria-label="Onboarding purpose" />Purpose</label>
|
||||
</div>
|
||||
<Switch label="Toggle" checked={checked} onChange={setChecked} />
|
||||
<button id="default-outline">Default browser outline</button>
|
||||
<button id="outline-none" aria-label="Outline disabled" style={{ outline: 'none' }}>No outline</button>
|
||||
<Menu open={open} autoFocus onClose={() => setOpen(false)}
|
||||
anchor={<button className={triggerCss.trigger} onClick={() => setOpen(!open)}>Open menu</button>}
|
||||
items={[{ id: 'first', label: 'First item' }, { id: 'last', label: 'Last item' }]} />
|
||||
<textarea aria-label="Feedback" className={feedbackCss.detail} />
|
||||
<button aria-label="Workflow member" className={workflowCss.memberButton}>
|
||||
<span data-member-ring className={workflowCss.memberLabelWrap}>
|
||||
<span className={workflowCss.memberLabel}>Member</span>
|
||||
</span>
|
||||
</button>
|
||||
<button aria-label="Elevated card" className={cardCss.card + ' ' + cardCss.copyable}
|
||||
style={{ position: 'relative' }}>Card</button>
|
||||
<button aria-label="Selected pill" aria-pressed="true" className={pillCss.pill + ' ' + pillCss.active}>Selected</button>
|
||||
<table className={trajectoryCss.table} data-scroll-ready="true"><tbody>
|
||||
<tr aria-label="Trajectory row" tabIndex={0}><td>Row</td></tr>
|
||||
</tbody></table>
|
||||
<button aria-label="Selected request" aria-pressed="true"
|
||||
className={trajectoryCss.requestBoundaryControl + ' ' + trajectoryCss.requestBoundaryControlActive}
|
||||
style={{ position: 'relative', width: 40, height: 30 }} />
|
||||
</main>
|
||||
}
|
||||
createRoot(document.getElementById('root')).render(<Fixture />)
|
||||
`
|
||||
const result = await build({
|
||||
configFile: false,
|
||||
root: join(REPO_ROOT, 'apps/web'),
|
||||
logLevel: 'error',
|
||||
plugins: [{
|
||||
name: 'focus-rings-fixture',
|
||||
resolveId(id) { if (id === entry) return entry },
|
||||
load(id) { if (id === entry) return code },
|
||||
}],
|
||||
esbuild: { jsx: 'automatic', jsxDev: false },
|
||||
define: { 'process.env.NODE_ENV': JSON.stringify('production') },
|
||||
build: {
|
||||
write: false,
|
||||
minify: false,
|
||||
lib: { entry, name: 'FocusRingsFixture', formats: ['iife'] },
|
||||
},
|
||||
})
|
||||
if ('on' in result) throw new Error('expected an in-memory build, not a watcher')
|
||||
const outputs = (Array.isArray(result) ? result : [result]).flatMap(bundle => bundle.output)
|
||||
return {
|
||||
script: outputs.flatMap(output => output.type === 'chunk' ? [output.code] : []).join('\n'),
|
||||
css: outputs.flatMap((output) => {
|
||||
if (output.type !== 'asset' || !output.fileName.endsWith('.css')) return []
|
||||
return [typeof output.source === 'string' ? output.source : new TextDecoder().decode(output.source)]
|
||||
}).join('\n'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The painted ring's colour, style and width, plus its WCAG contrast against the surface the
|
||||
* ring sits on. A control that disables its outline reports `style: 'none'` and null contrast.
|
||||
*/
|
||||
async function ringReport(target: Locator) {
|
||||
return target.evaluate((element) => {
|
||||
const style = getComputedStyle(element)
|
||||
if (style.outlineStyle === 'none') return { color: null, style: style.outlineStyle, width: style.outlineWidth, offset: style.outlineOffset, contrast: null }
|
||||
const resolve = (value: string): { text: string; rgb: [number, number, number] } => {
|
||||
const probe = document.createElement('span')
|
||||
probe.style.color = value
|
||||
document.body.append(probe)
|
||||
const text = getComputedStyle(probe).color
|
||||
probe.remove()
|
||||
const parts = /([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/.exec(text)
|
||||
return { text, rgb: [Number(parts?.[1] ?? 0), Number(parts?.[2] ?? 0), Number(parts?.[3] ?? 0)] }
|
||||
}
|
||||
const ring = resolve(style.outlineColor)
|
||||
// The ring is painted outside the border box unless the offset is negative.
|
||||
const start = Number.parseFloat(style.outlineOffset) >= 0 ? element.parentElement : element
|
||||
let behind = null
|
||||
for (let node = start; node instanceof HTMLElement; node = node.parentElement) {
|
||||
const candidate = getComputedStyle(node).backgroundColor
|
||||
if (candidate && !/rgba\(0, 0, 0, 0\)/.test(candidate)) { behind = resolve(candidate); break }
|
||||
}
|
||||
if (behind === null) {
|
||||
return { color: ring.text, style: style.outlineStyle, width: style.outlineWidth, offset: style.outlineOffset, contrast: null }
|
||||
}
|
||||
const luminance = (rgb: [number, number, number]): number => {
|
||||
const channel = (value: number): number => { const c = value / 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4 }
|
||||
return 0.2126 * channel(rgb[0]) + 0.7152 * channel(rgb[1]) + 0.0722 * channel(rgb[2])
|
||||
}
|
||||
const a = luminance(ring.rgb); const b = luminance(behind.rgb)
|
||||
return {
|
||||
color: ring.text, style: style.outlineStyle, width: style.outlineWidth, offset: style.outlineOffset,
|
||||
behind: behind.text,
|
||||
contrast: Math.round(((Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05)) * 100) / 100,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('source-compiled supplementary focus paint', () => {
|
||||
let fixture: Awaited<ReturnType<typeof compileFixture>>
|
||||
beforeAll(async () => { fixture = await compileFixture() })
|
||||
|
||||
async function openFixture(): Promise<Page> {
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
onTestFinished(() => browser.close())
|
||||
const page = await newEnglishPage(browser)
|
||||
const consoleWatch = watchConsole(page)
|
||||
onTestFinished(() => { expect(consoleWatch.pageErrors).toEqual([]) })
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
await page.setContent('<!doctype html><html><body><div id="root"></div></body></html>')
|
||||
await page.addStyleTag({ content: fixture.css })
|
||||
await page.addScriptTag({ content: fixture.script })
|
||||
expect(consoleWatch.pageErrors).toEqual([])
|
||||
await page.getByRole('switch', { name: 'Toggle' }).waitFor()
|
||||
return page
|
||||
}
|
||||
|
||||
it.each(['light', 'dark'])('keeps ancestor card rings silent after pointer input in %s mode', async (theme) => {
|
||||
const page = await openFixture()
|
||||
await page.evaluate(dark => document.body.toggleAttribute('data-ds-dark-theme', dark), theme === 'dark')
|
||||
const checkbox = page.getByRole('checkbox', { name: 'Onboarding purpose' })
|
||||
const card = page.locator('[data-onboarding-card]')
|
||||
await checkbox.click()
|
||||
await page.keyboard.press('Shift')
|
||||
expect((await paint(checkbox)).active).toBe(true)
|
||||
expect((await paint(card)).outlineStyle).toBe('none')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Shift+Tab')
|
||||
const ring = await paint(card)
|
||||
expect((await paint(checkbox)).active).toBe(true)
|
||||
expect(ring.outlineStyle).toBe('solid')
|
||||
expect(ring.outline).toBe(ring.focusColor)
|
||||
})
|
||||
|
||||
it('keeps Shift/Escape silent while Tab, Home, End and paging reveal a blue outline', async () => {
|
||||
const page = await openFixture()
|
||||
const toggle = page.getByRole('switch', { name: 'Toggle' })
|
||||
await toggle.click()
|
||||
for (const key of ['Shift', 'Escape']) {
|
||||
await page.keyboard.press(key)
|
||||
await expectSilent(toggle)
|
||||
}
|
||||
expect((await paint(toggle)).focusVisible).toBe(true)
|
||||
for (const key of ['Home', 'End', 'PageUp', 'PageDown']) {
|
||||
await toggle.click()
|
||||
await page.keyboard.press(key)
|
||||
await expectFocusRing(toggle, 'outline')
|
||||
}
|
||||
await toggle.click()
|
||||
await page.keyboard.press('Tab')
|
||||
const fallback = page.locator('#default-outline')
|
||||
const state = await paint(fallback)
|
||||
expect(state.active).toBe(true)
|
||||
expect(state.focusVisible).toBe(true)
|
||||
expect(state.outline).toBe(state.focusColor)
|
||||
expect(Number.parseFloat(state.outlineWidth)).toBeGreaterThan(0)
|
||||
await page.keyboard.press('Shift+Tab')
|
||||
await expectFocusRing(toggle, 'outline')
|
||||
})
|
||||
|
||||
it.each(['Enter', 'Space'])('%s opens the real autoFocus Menu after a pointer-owned trigger', async (key) => {
|
||||
const page = await openFixture()
|
||||
const trigger = page.getByRole('button', { name: 'Open menu' })
|
||||
await trigger.click()
|
||||
await page.getByRole('menuitem', { name: 'First item' }).waitFor()
|
||||
await trigger.click()
|
||||
await page.getByRole('menu').waitFor({ state: 'hidden' })
|
||||
await expectSilent(trigger)
|
||||
await page.keyboard.press(key)
|
||||
const first = page.getByRole('menuitem', { name: 'First item' })
|
||||
await expect.poll(async () => (await paint(first)).active).toBe(true)
|
||||
const state = await paint(first)
|
||||
expect(state.modality).toBe('keyboard')
|
||||
expect(state.focusVisible).toBe(true)
|
||||
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')
|
||||
})
|
||||
|
||||
it('negative control: blanket shadow suppression destroys input and card paint and misses descendants', async () => {
|
||||
const page = await openFixture()
|
||||
const input = page.getByRole('textbox', { name: 'Feedback' })
|
||||
const card = page.getByRole('button', { name: 'Elevated card' })
|
||||
const elevation = (await paint(card)).shadow
|
||||
await input.click()
|
||||
const focusedInput = await paint(input)
|
||||
expect(focusedInput.active).toBe(true)
|
||||
expect(focusedInput.shadow).toContain(focusedInput.focusColor)
|
||||
// Restore direct-colour fallback and the rejected global eraser only in this page.
|
||||
await page.addStyleTag({ content: `
|
||||
html[data-input-modality='pointer'] body :focus-visible {
|
||||
--dsw-focus-ring-color: initial !important;
|
||||
outline-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
` })
|
||||
expect((await paint(input)).shadow).toBe('none')
|
||||
await card.click()
|
||||
await page.keyboard.press('Shift')
|
||||
expect((await paint(card)).active).toBe(true)
|
||||
expect((await paint(card)).shadow).toBe('none')
|
||||
expect((await paint(card)).shadow).not.toBe(elevation)
|
||||
const member = page.getByRole('button', { name: 'Workflow member' })
|
||||
await member.click()
|
||||
await page.keyboard.press('Shift')
|
||||
expect((await paint(member)).active).toBe(true)
|
||||
const descendant = await paint(member.locator('[data-member-ring]'))
|
||||
expect(descendant.outlineStyle).toBe('solid')
|
||||
expect(descendant.outline).toBe(descendant.focusColor)
|
||||
})
|
||||
|
||||
it('silences high-specificity table rings without hiding selected-state shadows', async () => {
|
||||
const page = await openFixture()
|
||||
const row = page.getByRole('row', { name: 'Trajectory row' })
|
||||
await row.click()
|
||||
await page.keyboard.press('Shift')
|
||||
await expectSilent(row)
|
||||
expect((await paint(row)).focusVisible).toBe(true)
|
||||
await page.keyboard.press('Home')
|
||||
await expectFocusRing(row, 'shadow')
|
||||
|
||||
for (const [name, pseudo] of [['Selected pill', null], ['Selected request', '::before']] as const) {
|
||||
const selected = page.getByRole('button', { name })
|
||||
const shadow = (): Promise<string> => selected.evaluate((element, pseudo) => getComputedStyle(element, pseudo).boxShadow, pseudo)
|
||||
await selected.click()
|
||||
// The request marker animates its border; sample after its transition settles.
|
||||
await selected.evaluate(async (element) => {
|
||||
await Promise.all(element.getAnimations({ subtree: true }).map(animation => animation.finished))
|
||||
})
|
||||
const before = await shadow()
|
||||
expect(before).not.toBe('none')
|
||||
expect(before).not.toContain('rgba(0, 0, 0, 0)')
|
||||
await page.keyboard.press('Shift')
|
||||
expect(await selected.getAttribute('aria-pressed')).toBe('true')
|
||||
expect((await paint(selected)).active).toBe(true)
|
||||
await expect.poll(shadow).toBe(before)
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves clicked text-input paint and card elevation while suppressing descendant rings', async () => {
|
||||
const page = await openFixture()
|
||||
const input = page.getByRole('textbox', { name: 'Feedback' })
|
||||
await input.click()
|
||||
await page.keyboard.type('Still focused')
|
||||
await expect.poll(async () => {
|
||||
const state = await paint(input)
|
||||
return { active: state.active, visible: state.focusVisible, modality: state.modality, ring: state.shadow.includes(state.focusColor) }
|
||||
}).toEqual({ active: true, visible: true, modality: 'pointer', ring: true })
|
||||
|
||||
const member = page.getByRole('button', { name: 'Workflow member' })
|
||||
const ring = member.locator('[data-member-ring]')
|
||||
await member.click()
|
||||
await page.keyboard.press('Shift')
|
||||
expect((await paint(member)).active).toBe(true)
|
||||
expect((await paint(member)).focusVisible).toBe(true)
|
||||
expect((await paint(ring)).outline).toBe('rgba(0, 0, 0, 0)')
|
||||
await page.keyboard.press('Home')
|
||||
const descendant = await paint(ring)
|
||||
expect((await paint(member)).active).toBe(true)
|
||||
expect(descendant.outline).toBe(descendant.focusColor)
|
||||
expect(descendant.outlineStyle).toBe('solid')
|
||||
expect(Number.parseFloat(descendant.outlineWidth)).toBeGreaterThan(0)
|
||||
|
||||
const card = page.getByRole('button', { name: 'Elevated card' })
|
||||
const elevation = (await paint(card)).shadow
|
||||
expect(elevation).not.toBe('none')
|
||||
await card.click()
|
||||
await page.keyboard.press('Shift')
|
||||
await expectSilent(card)
|
||||
expect((await paint(card)).focusVisible).toBe(true)
|
||||
expect((await paint(card)).shadow).toBe(elevation)
|
||||
await page.keyboard.press('Home')
|
||||
await expectFocusRing(card, 'outline')
|
||||
expect((await paint(card)).shadow).toBe(elevation)
|
||||
})
|
||||
it('gives an undeclared ring the standard width and legible contrast in both themes', async () => {
|
||||
const page = await openFixture()
|
||||
const button = page.locator('#default-outline')
|
||||
await page.keyboard.press('Shift')
|
||||
await button.focus()
|
||||
const light = await ringReport(button)
|
||||
expect(light.style).not.toBe('none')
|
||||
expect(light.width).toBe('2px')
|
||||
expect(light.color).toBe('rgb(65, 118, 230)')
|
||||
expect(light.contrast).not.toBeNull()
|
||||
expect(light.contrast!).toBeGreaterThanOrEqual(3)
|
||||
|
||||
await page.evaluate(() => { document.body.setAttribute('data-ds-dark-theme', '') })
|
||||
await page.keyboard.press('Shift')
|
||||
await button.focus()
|
||||
const dark = await ringReport(button)
|
||||
expect(dark.width).toBe('2px')
|
||||
expect(dark.color).toBe('rgb(122, 170, 255)')
|
||||
expect(dark.contrast).not.toBeNull()
|
||||
expect(dark.contrast!).toBeGreaterThanOrEqual(3)
|
||||
|
||||
// Naming the width must not create a ring where a control disables its outline.
|
||||
const disabled = page.locator('#outline-none')
|
||||
await page.keyboard.press('Shift')
|
||||
await disabled.focus()
|
||||
const none = await ringReport(disabled)
|
||||
expect(none.style).toBe('none')
|
||||
expect(none.contrast).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -118,6 +118,8 @@ function markdownImageFixture(remoteUrl: string, outsidePath: string): string {
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
``,
|
||||
@@ -173,6 +175,7 @@ describe('web e2e: Markdown image rendering', () => {
|
||||
await writeFile(join(scaffold.workspaceCwd, 'local-image.png'), PNG)
|
||||
await mkdir(join(scaffold.workspaceCwd, 'test workspace'))
|
||||
await writeFile(join(scaffold.workspaceCwd, 'test workspace/测试图.png'), PNG)
|
||||
await writeFile(join(scaffold.workspaceCwd, 'test workspace/测试#100%.png'), PNG)
|
||||
await writeFile(join(scaffold.workspaceCwd, 'corrupt.png'), 'invalid image')
|
||||
const oversized = await open(join(scaffold.workspaceCwd, 'oversized.png'), 'w')
|
||||
try {
|
||||
@@ -250,7 +253,7 @@ describe('web e2e: Markdown image rendering', () => {
|
||||
referrerPolicy: 'no-referrer',
|
||||
})
|
||||
await expect.poll(() => page.getByRole('img', { name: LOCAL_ALT }).evaluate(element => (element as HTMLImageElement).naturalWidth)).toBe(1)
|
||||
for (const name of ['Space path', 'Encoded path']) {
|
||||
for (const name of ['Space path', 'Encoded path', 'Encoded filename']) {
|
||||
await expect.poll(() => page.getByRole('img', { name, exact: true }).evaluate(element => (element as HTMLImageElement).naturalWidth)).toBe(1)
|
||||
}
|
||||
expect(imageOrigin.requests).toEqual([{ path: '/image.png', referer: undefined }])
|
||||
@@ -272,6 +275,7 @@ describe('web e2e: Markdown image rendering', () => {
|
||||
[join(scaffold.workspaceCwd, 'valid.png'), 200],
|
||||
[`${scaffold.workspaceCwd}/./local-image.png`, 200],
|
||||
[join(scaffold.workspaceCwd, 'test workspace/测试图.png'), 200],
|
||||
[join(scaffold.workspaceCwd, 'test workspace/测试#100%.png'), 200],
|
||||
[join(scaffold.workspaceCwd, 'oversized.png'), 413],
|
||||
[join(scaffold.persistenceRoot, 'outside.png'), 200],
|
||||
[join(scaffold.workspaceCwd, 'missing.png'), 404],
|
||||
|
||||
@@ -123,6 +123,10 @@ describe('minimal agent preset', () => {
|
||||
"prompt": "You are a helpful software engineer assistant.",
|
||||
"tools": [
|
||||
"bash",
|
||||
"schedule_create",
|
||||
"schedule_delete",
|
||||
"schedule_list",
|
||||
"schedule_update",
|
||||
],
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { OPTIONAL_BUNDLES } from '@deepseek-ai/dsh-app-boot'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
@@ -91,7 +92,7 @@ describe('web e2e: plugin configuration pages', () => {
|
||||
// the official bundles the installation ships switched off.
|
||||
await panel.getByRole('button', { name: '查看 网页搜索', exact: true }).waitFor({ timeout: 20_000 })
|
||||
const official = panel.locator('[data-plugin-group="official"]')
|
||||
expect(await official.locator('[data-plugin-package]').count()).toBe(2)
|
||||
expect(await official.locator('[data-plugin-package]').count()).toBe(OPTIONAL_BUNDLES.length)
|
||||
expect(await official.locator('[data-plugin-item]').count()).toBe(4)
|
||||
for (const title of ['终端', 'Agent 循环', 'Subagent', '网页搜索']) {
|
||||
expect(await official.getByRole('button', { name: `查看 ${title}`, exact: true }).count()).toBe(1)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Real Git reaches a controlled proxy failure or stall; the browser
|
||||
// offers a mirror for a replacement spec without retrying the failed address.
|
||||
// offers a mirror for a replacement spec, or another way once the install
|
||||
// already asks the mirror, without retrying the failed address.
|
||||
import { once } from 'node:events'
|
||||
import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises'
|
||||
import { createServer, type Socket } from 'node:net'
|
||||
@@ -11,7 +12,7 @@ import { expect, it, onTestFinished } from 'vitest'
|
||||
import { launchWebScaffold, captureStableAria, compareOrRefreshGolden, webSnapshotMode, watchConsole } from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
it.each(['network', 'timeout'] as const)('offers a mirror after a GitHub %s and waits for replacement input', async (failure) => {
|
||||
it.each(['network', 'timeout'] as const)('offers a mirror after a GitHub %s, then another way on the mirror, and waits for replacement input', async (failure) => {
|
||||
const scratch = await mkdtemp(join(tmpdir(), 'dsh-install-github-'))
|
||||
onTestFinished(() => rm(scratch, { recursive: true, force: true }))
|
||||
const sockets = new Set<Socket>()
|
||||
@@ -97,6 +98,23 @@ it.each(['network', 'timeout'] as const)('offers a mirror after a GitHub %s and
|
||||
await input.fill(spec)
|
||||
await dialog.getByRole('button', { name: '安装', exact: true }).click()
|
||||
dialog = page.getByRole('dialog', { name: title, exact: true })
|
||||
await dialog.waitFor()
|
||||
await compareOrRefreshGolden(
|
||||
fileURLToPath(new URL(`./expected/plugin-install-github/${failure === 'timeout' ? 'timeout' : 'failed'}-on-mirror.expected.md`, import.meta.url)),
|
||||
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode(),
|
||||
)
|
||||
await dialog.getByRole('button', { name: '试试其他方式', exact: true }).click()
|
||||
dialog = page.getByRole('dialog', { name: '添加插件', exact: true })
|
||||
await dialog.getByRole('button', { name: '收起引导', exact: true }).waitFor()
|
||||
expect(await input.inputValue()).toBe('')
|
||||
expect(await input.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
await compareOrRefreshGolden(
|
||||
fileURLToPath(new URL('./expected/plugin-install-github/another-way.expected.md', import.meta.url)),
|
||||
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode(),
|
||||
)
|
||||
await input.fill(spec)
|
||||
await dialog.getByRole('button', { name: '安装', exact: true }).click()
|
||||
dialog = page.getByRole('dialog', { name: title, exact: true })
|
||||
await dialog.getByRole('button', { name: '取消', exact: true }).click()
|
||||
await dialog.waitFor({ state: 'detached' })
|
||||
expect(await readFile(join(profile, '.attempts'), 'utf8')).toBe('')
|
||||
|
||||
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { FiberState } from '@deepseek-ai/cordis'
|
||||
import { OPTIONAL_BUNDLES } from '@deepseek-ai/dsh-app-boot'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
@@ -126,9 +127,11 @@ describe('web e2e: plugin manager', () => {
|
||||
// optional bundles open the Official group, followed by the official plugins that registered their
|
||||
// configuration, and its other bundles stay off the page.
|
||||
expect(await panel.locator('[data-plugin-group="bundles"] [data-plugin-package]').count()).toBe(2)
|
||||
expect(await panel.locator('[data-plugin-group="official"] [data-plugin-package]').count()).toBe(2)
|
||||
expect(await panel.locator('[data-plugin-group="official"] [data-plugin-package]').count()).toBe(OPTIONAL_BUNDLES.length)
|
||||
expect(await panel.locator('[data-plugin-group="official"] [data-plugin-item]').count()).toBe(4)
|
||||
expect(await panel.getByText('实验性', { exact: true }).count()).toBe(2)
|
||||
expect(await panel.getByText('实验性', { exact: true }).count())
|
||||
.toBe(OPTIONAL_BUNDLES.filter(name => name.startsWith('@deepseek-ai/dsh-experimental-')).length)
|
||||
expect(await panel.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-inspector"]').count()).toBe(0)
|
||||
expect(await panel.getByRole('switch', { name: '启用 语音输入', exact: true }).getAttribute('aria-checked')).toBe('false')
|
||||
// A bundle that is off still shows the rows its patch declares, without switches.
|
||||
await panel.getByRole('button', { name: '查看 @fixture/bundle' }).click()
|
||||
|
||||
@@ -1025,6 +1025,27 @@ function mapJsonStringValues(value: unknown, map: (value: string) => string): un
|
||||
return value
|
||||
}
|
||||
|
||||
/** Volatile fields inside one durable time-context reading, each replaced by a fixed token. */
|
||||
const TIME_CONTEXT_READING_FIELDS: readonly (readonly [RegExp, string])[] = [
|
||||
[/(Time sampled while preparing turn \d+, step \d+: )[^\n]*/, '$1{{timeContextTimestamp}}'],
|
||||
[/(Browser time zone for this request: )[^.]*\./, '$1{{clientTimeZone}}.'],
|
||||
[/(Elapsed since the preceding [^:]*: )[^\n]*/, '$1{{elapsed}}'],
|
||||
]
|
||||
|
||||
/** Replace the sampled instant, browser zone, and elapsed duration a time-context reading carries. */
|
||||
function tokenizeTimeContextReading(text: string): string {
|
||||
let normalized = text
|
||||
for (const [pattern, replacement] of TIME_CONTEXT_READING_FIELDS) normalized = normalized.replace(pattern, replacement)
|
||||
return normalized
|
||||
}
|
||||
|
||||
/** Whether one parsed Session record is a durable time-context reading. */
|
||||
function isTimeContextReading(value: unknown): boolean {
|
||||
if (value === null || typeof value !== 'object') return false
|
||||
const record = value as { type?: unknown; data?: { source?: { kind?: unknown } } }
|
||||
return record.type === 'user/message' && record.data?.source?.kind === 'time-context'
|
||||
}
|
||||
|
||||
/** Tokenize the browser timezone carried by user message sources. */
|
||||
function normalizeClientTimeZones(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(item => normalizeClientTimeZones(item))
|
||||
@@ -1102,7 +1123,7 @@ export function normalizeWebSessionVolatiles(log: string, workspaceCwd?: string)
|
||||
}))].sort((left, right) => right.length - left.length)
|
||||
return log.split(/\r?\n/).map((line) => {
|
||||
if (line.trim() === '') return line
|
||||
const record = normalizeClientTimeZones(mapJsonStringValues(JSON.parse(line), (value) => {
|
||||
let record = normalizeClientTimeZones(mapJsonStringValues(JSON.parse(line), (value) => {
|
||||
let normalized = value
|
||||
.replace(/Anonymous user: [0-9a-f-]{36}(?=\.$)/gi, 'Anonymous user: {{anonymousUserId}}')
|
||||
for (const cwd of cwdSpellings) normalized = replaceWebCwd(normalized, cwd)
|
||||
@@ -1111,6 +1132,9 @@ export function normalizeWebSessionVolatiles(log: string, workspaceCwd?: string)
|
||||
if (record.type === 'web/deepseek-search-llm-request' && typeof record.data?.endpoint === 'string') {
|
||||
record.data.endpoint = '{{webSearchEndpoint}}'
|
||||
}
|
||||
if (isTimeContextReading(record)) {
|
||||
record = mapJsonStringValues(record, tokenizeTimeContextReading) as typeof record
|
||||
}
|
||||
return JSON.stringify(record)
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ declare module '@deepseek-ai/dsh-llm' {
|
||||
}
|
||||
|
||||
const MODE = webSnapshotMode()
|
||||
const OVERLAY = fileURLToPath(new URL('../../cli/config/examples/schedule/cordis.yml', import.meta.url))
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/schedule-after', import.meta.url))
|
||||
const AFTER_EXPECTED = join(SNAPSHOT_DIR, 'conversation.expected.md')
|
||||
const AT_EXPECTED = join(SNAPSHOT_DIR, 'at-conversation.expected.md')
|
||||
@@ -272,7 +271,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
|
||||
const everyAdapter = new EveryReminderAdapter()
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY })
|
||||
scaffold = await launchWebScaffold()
|
||||
scaffold.ctx.effect(
|
||||
() => scaffold.ctx.llm.registerAdapter([AFTER_PROVIDER], afterAdapter),
|
||||
'Schedule Web After adapter',
|
||||
@@ -708,9 +707,7 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
const fixture = await readFile(CATALOG_FIXTURE, 'utf8')
|
||||
scaffold = await launchWebScaffold({
|
||||
extraOverlayPath: OVERLAY,
|
||||
})
|
||||
scaffold = await launchWebScaffold()
|
||||
await seedSession(scaffold, fixture, CATALOG_SESSION_ID, 'standard')
|
||||
const records = foldScheduleEvents(fixture.trim().split('\n').slice(1).map(line => JSON.parse(line) as SessionEvent)).active
|
||||
const domain = scaffold.ctx.storageDomain.get('schedule')
|
||||
@@ -835,23 +832,20 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
|
||||
|
||||
it('reads stored tasks without a live Session and deletes them through the catalog', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-schedule-catalog'))
|
||||
const base = composeEntries([
|
||||
loadOverlayPatches('Schedule catalog base roster', BASE_PATCH),
|
||||
...WEB_PATCHES.map(file => loadOverlayPatches('Schedule catalog base roster', file)),
|
||||
const shipped = composeEntries([
|
||||
loadOverlayPatches('Schedule catalog shipped roster', BASE_PATCH),
|
||||
...WEB_PATCHES.map(file => loadOverlayPatches('Schedule catalog shipped roster', file)),
|
||||
])
|
||||
const scheduled = composeEntries([
|
||||
loadOverlayPatches('Schedule catalog overlay roster', BASE_PATCH),
|
||||
...WEB_PATCHES.map(file => loadOverlayPatches('Schedule catalog overlay roster', file)),
|
||||
loadOverlayPatches('Schedule catalog overlay roster', OVERLAY),
|
||||
])
|
||||
expect(base.find(entry => entry.id === 'ui-schedule')).toMatchObject({
|
||||
expect(shipped.find(entry => entry.id === 'ui-schedule')).toMatchObject({
|
||||
name: '@deepseek-ai/dsh-client-ui-schedule',
|
||||
disabled: true,
|
||||
})
|
||||
expect(scheduled.find(entry => entry.id === 'ui-schedule')).toMatchObject({
|
||||
name: '@deepseek-ai/dsh-client-ui-schedule',
|
||||
disabled: false,
|
||||
})
|
||||
expect(shipped.find(entry => entry.id === 'ui-schedule')?.disabled).toBeUndefined()
|
||||
for (const row of [
|
||||
{ id: 'time-context', name: '@deepseek-ai/dsh-time-context' },
|
||||
{ id: 'schedule', name: '@deepseek-ai/dsh-schedule' },
|
||||
]) {
|
||||
expect(shipped.filter(entry => entry.id === row.id && entry.name === row.name)).toHaveLength(1)
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Automation tasks', exact: true }).click()
|
||||
const manager = page.getByTestId('task-manager-page')
|
||||
|
||||
@@ -489,6 +489,10 @@ const EXPECTED_TOOLS = [
|
||||
'present',
|
||||
'read',
|
||||
'read_image',
|
||||
'schedule_create',
|
||||
'schedule_delete',
|
||||
'schedule_list',
|
||||
'schedule_update',
|
||||
'send_message',
|
||||
'skill',
|
||||
'subagent',
|
||||
|
||||
@@ -410,7 +410,9 @@ describe('web e2e: shortcut reference', () => {
|
||||
const wasOpen = await page.getByRole('button', { name: 'Collapse sidebar', exact: true }).isVisible()
|
||||
await page.keyboard.press(sidebarKey)
|
||||
await page.getByRole('button', { name: wasOpen ? 'Open sidebar' : 'Collapse sidebar', exact: true }).waitFor()
|
||||
if (wasOpen) await page.getByRole('button', { name: 'Open sidebar', exact: true }).click()
|
||||
// The rail toggle moves during collapse; reopen with the binding this case exercises.
|
||||
if (wasOpen) await page.keyboard.press(sidebarKey)
|
||||
await page.getByRole('button', { name: 'Collapse sidebar', exact: true }).waitFor()
|
||||
await open()
|
||||
await page.getByRole('button', { name: 'Edit shortcut for Toggle left sidebar', exact: true }).click()
|
||||
await editor.getByRole('button', { name: 'Press a shortcut', exact: true }).focus()
|
||||
|
||||
@@ -51,7 +51,10 @@ describe.skipIf(MODE === 'record')('web e2e: compact Tool details', () => {
|
||||
|
||||
it('expands every recorded tool through its keyed renderer', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-tool-details'))
|
||||
const rows = page.locator('[data-tool]')
|
||||
// `[data-chat-call-id]` scopes the keyed tool cells: the Schedule client's
|
||||
// turn-tail create card carries the same `data-tool` marker without being
|
||||
// one of the keyed call views this case walks.
|
||||
const rows = page.locator('[data-chat-call-id] [data-tool]')
|
||||
expect(await rows.count()).toBe(45)
|
||||
for (const row of await rows.all()) {
|
||||
await row.getByRole('button', { expanded: false }).first().click()
|
||||
@@ -76,7 +79,7 @@ describe.skipIf(MODE === 'record')('web e2e: compact Tool details', () => {
|
||||
const snapshot = snapshots.join('\n')
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
await page.setViewportSize({ width: 360, height: 800 })
|
||||
const card = page.locator('[data-tool="schedule_create"]')
|
||||
const card = page.locator('[data-chat-call-id] [data-tool="schedule_create"]')
|
||||
expect(await card.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true)
|
||||
const reminders = page.locator('[data-tool="schedule_list"]')
|
||||
await scrollIntoView(reminders)
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"tests/plan-review.e2e.ts",
|
||||
"tests/steering.e2e.ts",
|
||||
"tests/navigation-panes.e2e.ts",
|
||||
"tests/focus-rings.e2e.ts",
|
||||
"tests/chat-scroll-fixture.ts",
|
||||
"tests/trajectory-virtualization.e2e.ts",
|
||||
"tests/lifecycle-chrome.e2e.ts",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
en: 41d772192075e133
|
||||
zh: 5ef3da69b4de1d8a
|
||||
/capability-seams-and-core-services:
|
||||
en: 35a8ab92922009f2
|
||||
zh: b2d0accdef6b94bd
|
||||
en: 0d14d210d41c6770
|
||||
zh: 964e92962b7cfa88
|
||||
@@ -161,7 +161,6 @@ flowchart LR
|
||||
svc_sessionProjections["ctx.sessionProjections<br/>Session projection units"]
|
||||
pkg_session_projection_cache["session-projection-cache"]
|
||||
svc_sessionProjectionCache["ctx.sessionProjectionCache<br/>Persisted projection cache"]
|
||||
pkg_subagent["subagent"]
|
||||
pkg_skill["skill"]
|
||||
svc_skills["ctx.skills<br/>Skill provider registry"]
|
||||
pkg_skill_badge["skill-badge"]
|
||||
@@ -222,6 +221,7 @@ flowchart LR
|
||||
pkg_fs_observation_policy["fs-observation-policy"]
|
||||
pkg_compaction["compaction"]
|
||||
svc_compaction["ctx.compaction<br/>Compaction seam"]
|
||||
pkg_subagent["subagent"]
|
||||
svc_subagents["ctx.subagents<br/>Subagent provider and continuation service"]
|
||||
pkg_subagent_spawn_in_process["subagent-spawn-in-process"]
|
||||
pkg_subagent_fork_in_process["subagent-fork-in-process"]
|
||||
@@ -493,7 +493,6 @@ flowchart LR
|
||||
svc_sessionProjectionCache --> pkg_api_session_controller
|
||||
svc_sessionProjectionCache --> pkg_session_query
|
||||
svc_sessionProjectionCache --> pkg_session_reference
|
||||
svc_sessionProjectionCache --> pkg_subagent
|
||||
svc_sessionProjections --> pkg_api_session_controller
|
||||
svc_sessionProjections --> pkg_session_title
|
||||
svc_sessionProjections --> pkg_tool_todo
|
||||
@@ -624,7 +623,7 @@ flowchart LR
|
||||
| `ctx.agentPresets` | `core` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | - | - | - | Eagerly mounts YAML-declared preset revisions, binds Agents and cold readers to scoped contributions, and retains retired revisions until their last user releases them. |
|
||||
| `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. |
|
||||
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session/session-projection) | - | [`api-session-controller`](../packages/api/session-controller), [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session/session-title) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and the Session controller serves baselines and pushes changed values. |
|
||||
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. |
|
||||
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points), serves cached projection views, and accelerates prepared-Session projection hydration. |
|
||||
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
|
||||
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
|
||||
|
||||
@@ -163,7 +163,6 @@ flowchart LR
|
||||
svc_sessionProjections["ctx.sessionProjections<br/>Session projection units"]
|
||||
pkg_session_projection_cache["session-projection-cache"]
|
||||
svc_sessionProjectionCache["ctx.sessionProjectionCache<br/>Persisted projection cache"]
|
||||
pkg_subagent["subagent"]
|
||||
pkg_skill["skill"]
|
||||
svc_skills["ctx.skills<br/>Skill provider registry"]
|
||||
pkg_skill_badge["skill-badge"]
|
||||
@@ -224,6 +223,7 @@ flowchart LR
|
||||
pkg_fs_observation_policy["fs-observation-policy"]
|
||||
pkg_compaction["compaction"]
|
||||
svc_compaction["ctx.compaction<br/>Compaction seam"]
|
||||
pkg_subagent["subagent"]
|
||||
svc_subagents["ctx.subagents<br/>Subagent provider and continuation service"]
|
||||
pkg_subagent_spawn_in_process["subagent-spawn-in-process"]
|
||||
pkg_subagent_fork_in_process["subagent-fork-in-process"]
|
||||
@@ -495,7 +495,6 @@ flowchart LR
|
||||
svc_sessionProjectionCache --> pkg_api_session_controller
|
||||
svc_sessionProjectionCache --> pkg_session_query
|
||||
svc_sessionProjectionCache --> pkg_session_reference
|
||||
svc_sessionProjectionCache --> pkg_subagent
|
||||
svc_sessionProjections --> pkg_api_session_controller
|
||||
svc_sessionProjections --> pkg_session_title
|
||||
svc_sessionProjections --> pkg_tool_todo
|
||||
@@ -626,7 +625,7 @@ flowchart LR
|
||||
| `ctx.agentPresets` | `core` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | - | - | - | 立即挂载 YAML 声明的 preset 版本,把 Agent 和冷读取绑定到作用域内的贡献,并保留已替换的版本,直到最后一个使用者释放它。 |
|
||||
| `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | 插件注册直接面向人的命令,而不会把调用发送给模型。 |
|
||||
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session/session-projection) | - | [`api-session-controller`](../packages/api/session-controller), [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session/session-title) | - | 各领域注册由状态驱动的折叠单元;主动驱动过程维护每个会话的水位状态,Session controller 提供 baseline 并推送发生变化的值。 |
|
||||
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent) | - | 按会话持久保存投影单元状态的检查点(节流检查点,以及轮次/结束/分离时的必选检查点),并提供冷读取阶梯:缓存行加持久化尾部回放,因此列表读取永远不需要加载完整日志。 |
|
||||
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | 按会话持久保存投影单元状态的检查点(节流检查点,以及轮次/结束/分离时的必选检查点),提供缓存投影视图,并加速 prepared Session 的投影恢复。 |
|
||||
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 |
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 |
|
||||
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
|
||||
|
||||
@@ -148,10 +148,8 @@ export type Config = PresetDefinition
|
||||
export interface Config {
|
||||
/** Deployment default when the caller omits a preset. */
|
||||
default: string
|
||||
/** User-selected default while the chooser is shown; edited through Settings. */
|
||||
/** User-selected default; edited through Settings. */
|
||||
selectedDefault: Volatile<string | undefined>
|
||||
/** Whether new-session surfaces expose preset selection and the saved default applies. */
|
||||
modeSelectionEnabled: Volatile<boolean>
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-agent-preset-registry -->
|
||||
|
||||
@@ -150,10 +150,8 @@ export type Config = PresetDefinition
|
||||
export interface Config {
|
||||
/** Deployment default when the caller omits a preset. */
|
||||
default: string
|
||||
/** User-selected default while the chooser is shown; edited through Settings. */
|
||||
/** User-selected default; edited through Settings. */
|
||||
selectedDefault: Volatile<string | undefined>
|
||||
/** Whether new-session surfaces expose preset selection and the saved default applies. */
|
||||
modeSelectionEnabled: Volatile<boolean>
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-agent-preset-registry -->
|
||||
|
||||
@@ -9,7 +9,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:71`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:71`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
|
||||
@@ -1093,7 +1093,6 @@ flowchart TD
|
||||
pkg_subagent --> pkg_session
|
||||
pkg_subagent --> pkg_session_persistence
|
||||
pkg_subagent --> pkg_session_projection
|
||||
pkg_subagent --> pkg_session_projection_cache
|
||||
pkg_subagent --> pkg_session_query
|
||||
pkg_subagent --> pkg_system_prompt
|
||||
pkg_subagent --> pkg_tools
|
||||
@@ -1641,7 +1640,7 @@ flowchart TD
|
||||
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
|
||||
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
|
||||
@@ -1095,7 +1095,6 @@ flowchart TD
|
||||
pkg_subagent --> pkg_session
|
||||
pkg_subagent --> pkg_session_persistence
|
||||
pkg_subagent --> pkg_session_projection
|
||||
pkg_subagent --> pkg_session_projection_cache
|
||||
pkg_subagent --> pkg_session_query
|
||||
pkg_subagent --> pkg_system_prompt
|
||||
pkg_subagent --> pkg_tools
|
||||
@@ -1643,7 +1642,7 @@ flowchart TD
|
||||
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
|
||||
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
|
||||
@@ -499,8 +499,8 @@ async register(definition: PresetDefinition): Promise<() => Promise<void>>
|
||||
*/
|
||||
async list(): Promise<AgentPreset[]>
|
||||
|
||||
/** Read the selection roster and chooser policy.
|
||||
* @returns Current presets, default and chooser policy.
|
||||
/** Read the selection roster.
|
||||
* @returns Current presets, each marked when it is the default.
|
||||
*/
|
||||
@Remote('list') async remoteExportList(): Promise<AgentPresetRoster>
|
||||
|
||||
|
||||
@@ -509,8 +509,8 @@ async register(definition: PresetDefinition): Promise<() => Promise<void>>
|
||||
*/
|
||||
async list(): Promise<AgentPreset[]>
|
||||
|
||||
/** Read the selection roster and chooser policy.
|
||||
* @returns Current presets, default and chooser policy.
|
||||
/** Read the selection roster.
|
||||
* @returns Current presets, each marked when it is the default.
|
||||
*/
|
||||
@Remote('list') async remoteExportList(): Promise<AgentPresetRoster>
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
en: 41cd197b5c74bfa3
|
||||
zh: 18ed11259036d726
|
||||
/host-wide-schedule/absolute-time-input:
|
||||
en: 9f814e5a1c9f93c1
|
||||
zh: 5f04bd46f43ebbf9
|
||||
en: e730fda52ba2854f
|
||||
zh: 01a6c682e327a133
|
||||
/host-wide-schedule/daily-wall-clock-input:
|
||||
en: 2d249edc3dfccb8a
|
||||
zh: 33fecda132b03b84
|
||||
/host-wide-schedule/fixed-rate-input-and-catch-up:
|
||||
en: 9057ff8733ca051a
|
||||
zh: c4bf9d86363808cd
|
||||
en: d2b5997cce3806dc
|
||||
zh: 2483ba9e38ad3cf3
|
||||
/host-wide-schedule/cron-wall-clock-input:
|
||||
en: 1ba9ae84f05d8919
|
||||
zh: 5b4abfdcddbbcae0
|
||||
@@ -27,8 +27,8 @@
|
||||
en: 0656173de50ee132
|
||||
zh: 54176540371d0f28
|
||||
/host-wide-schedule/web-catalog:
|
||||
en: 3526df6b736ae81c
|
||||
zh: 677a09e8ac367e56
|
||||
en: 39f24636dd788034
|
||||
zh: ba3ff541fee5854a
|
||||
/host-wide-schedule/timing-edits:
|
||||
en: ce5d2095a3ad2e82
|
||||
zh: 167b3715f8eaffc1
|
||||
|
||||
@@ -53,7 +53,7 @@ interface EveryScheduleRecord {
|
||||
readonly title: string
|
||||
/** Trimmed reminder content supplied at creation. */
|
||||
readonly prompt: string
|
||||
/** Fixed safe-integer interval, never below five minutes. */
|
||||
/** Fixed safe-integer interval, never below one minute. */
|
||||
readonly everySeconds: number
|
||||
/** Next anchor-aligned occurrence while active, or final occurrence when inactive. */
|
||||
readonly scheduledAt: string
|
||||
@@ -116,7 +116,7 @@ interface LocalAtInput {
|
||||
type AtInput = string | LocalAtInput
|
||||
```
|
||||
|
||||
The official Web overlay samples the browser's IANA zone for every prompt. Time-context tells the model to interpret otherwise-unqualified natural-language dates and times in that request-local zone when the open turn has one unambiguous browser zone; mixed or missing browser-zone records tell the model to ask. That guidance is not a durable Session default: the model must still pass an offset in the string form or `time_zone` in the local form, and Schedule never reads browser, Session, process, or model context.
|
||||
The shipped Web bundle mounts time-context, which samples the browser's IANA zone for every prompt. Time-context tells the model to interpret otherwise-unqualified natural-language dates and times in that request-local zone when the open turn has one unambiguous browser zone; mixed or missing browser-zone records tell the model to ask. That guidance is not a durable Session default: the model must still pass an offset in the string form or `time_zone` in the local form, and Schedule never reads browser, Session, process, or model context.
|
||||
|
||||
Schedule rejects invalid offsets and zones, offset-free strings, non-future targets, and local times inside daylight-saving gaps. A daylight-saving overlap chooses its first, earlier instant. Successful creation stores only canonical UTC `scheduledAt`, so replay never depends on ambient time-zone state.
|
||||
|
||||
@@ -141,11 +141,11 @@ A saved next target is a committed UTC instant. The Host decoder and restart pre
|
||||
|
||||
## Fixed-rate input and catch-up
|
||||
|
||||
`every_seconds` is a safe-integer interval of at least 300 seconds, aligned to creation time. It measures elapsed time, not daily wall-clock time: `every_seconds: 86400` cannot substitute for `daily` across offset changes. The protocol provides no shared cooldown or cross-record admission rule; the cron selector described below is wall-clock timing, not a fixed-rate interval.
|
||||
`every_seconds` is a safe-integer interval of at least 60 seconds, aligned to creation time. It measures elapsed time, not daily wall-clock time: `every_seconds: 86400` cannot substitute for `daily` across offset changes. The protocol provides no shared cooldown or cross-record admission rule; the cron selector described below is wall-clock timing, not a fixed-rate interval.
|
||||
|
||||
When the Host starts with an overdue Every, Daily, Weekly, or Cron record, it sends only the latest due occurrence. Every advances to the first target aligned to its current interval anchor after the delivery decision; Daily, Weekly, and Cron follow the local-date rules above and below. Missed occurrences do not accumulate. If no next UTC target is representable, delivery retains the record as inactive with its latest receipt.
|
||||
|
||||
One-shots produce individual follow-ups. Recurring records due in the same scan for the same Session share one follow-up rendered by `renderRecurringReminderBatchFraming`, with one latest occurrence per record. The five-minute minimum applies only to fixed-rate intervals; delivery does not wait for the target Agent to become idle.
|
||||
One-shots produce individual follow-ups. Recurring records due in the same scan for the same Session share one follow-up rendered by `renderRecurringReminderBatchFraming`, with one latest occurrence per record. The one-minute minimum applies only to fixed-rate intervals; delivery does not wait for the target Agent to become idle.
|
||||
|
||||
<a id="cron-wall-clock-input"></a>
|
||||
## Cron wall-clock input
|
||||
@@ -339,7 +339,7 @@ type ScheduleCatalogEntry = ScheduleRecord & {
|
||||
|
||||
The Remote `schedule.list({ sessionId })`, model `schedule_list`, and Session-header catalog return only active tasks. A model view's derived timing `state` remains distinct from stored lifecycle `status`. Deletion uses `schedule.delete({ sessionId, id })` with the entry's original binding; a mismatched binding returns not found. Model tools supply the current Agent's Session, whereas the global user interface supplies the selected task's binding. The binding check alone does not establish caller authorization. The payload-free `schedule/changed` event invalidates client lists; reconnecting clients fetch current state again.
|
||||
|
||||
The Web bundle keeps `ui-schedule` disabled by default; the Schedule overlay enables it with the Host capability. The [client package](../../packages/client/ui-schedule/README.md) owns the catalog, empty state, and deletion controls. The page separately filters all, active, and inactive tasks, retains inactive details and an original-Session control in the detail tab strip, and requires explicit confirmed deletion. Rules and Delivery records separate task settings from lazily paged saved receipts. Neither a receipt nor inactive status confirms model execution.
|
||||
The Web bundle mounts `ui-schedule` with the Host capability. The [client package](../../packages/client/ui-schedule/README.md) owns the catalog, empty state, and deletion controls. The page separately filters all, active, and inactive tasks, retains inactive details and an original-Session control in the detail tab strip, and requires explicit confirmed deletion. Rules and Delivery records separate task settings from lazily paged saved receipts. Neither a receipt nor inactive status confirms model execution.
|
||||
|
||||
## Timing edits
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ interface EveryScheduleRecord {
|
||||
readonly title: string
|
||||
/** Trimmed reminder content supplied at creation. */
|
||||
readonly prompt: string
|
||||
/** Fixed safe-integer interval, never below five minutes. */
|
||||
/** Fixed safe-integer interval, never below one minute. */
|
||||
readonly everySeconds: number
|
||||
/** Next anchor-aligned occurrence while active, or final occurrence when inactive. */
|
||||
readonly scheduledAt: string
|
||||
@@ -116,7 +116,7 @@ interface LocalAtInput {
|
||||
type AtInput = string | LocalAtInput
|
||||
```
|
||||
|
||||
官方 Web overlay 会为每条提示词采样浏览器的 IANA 时区。当 open turn 只有一个无歧义的浏览器时区时,Time-context 会告诉模型按该请求本地时区解释未明确限定时区的自然语言日期和时间;浏览器时区记录混合或缺失时,则告诉模型询问用户。该指引不是持久 Session 默认值:模型仍必须在字符串形式中传入偏移量,或在本地形式中传入 `time_zone`;Schedule 绝不会读取浏览器、Session、进程或模型上下文。
|
||||
发布的 Web bundle 会挂载 time-context,它为每条提示词采样浏览器的 IANA 时区。当 open turn 只有一个无歧义的浏览器时区时,Time-context 会告诉模型按该请求本地时区解释未明确限定时区的自然语言日期和时间;浏览器时区记录混合或缺失时,则告诉模型询问用户。该指引不是持久 Session 默认值:模型仍必须在字符串形式中传入偏移量,或在本地形式中传入 `time_zone`;Schedule 绝不会读取浏览器、Session、进程或模型上下文。
|
||||
|
||||
Schedule 会拒绝无效偏移量与时区、不带偏移量的字符串、非未来目标,以及落在夏令时缺口内的本地时间。遇到夏令时重叠时,会选择第一次出现的较早时点。创建成功后只存储规范化后的 UTC `scheduledAt`,因此回放绝不依赖环境时区状态。
|
||||
|
||||
@@ -141,11 +141,11 @@ interface DailyInput {
|
||||
|
||||
## 固定速率输入与补偿
|
||||
|
||||
`every_seconds` 是至少 300 秒的安全整数间隔,与创建时间对齐。它衡量经过的时间,而不是每日本地钟表时间:发生偏移量变化时,`every_seconds: 86400` 不能替代 `daily`。协议不提供共享冷却时间或跨记录准入规则;下文描述的 cron 选择器是钟表时间语义,不是固定速率间隔。
|
||||
`every_seconds` 是至少 60 秒的安全整数间隔,与创建时间对齐。它衡量经过的时间,而不是每日本地钟表时间:发生偏移量变化时,`every_seconds: 86400` 不能替代 `daily`。协议不提供共享冷却时间或跨记录准入规则;下文描述的 cron 选择器是钟表时间语义,不是固定速率间隔。
|
||||
|
||||
宿主启动时若 Every、Daily、Weekly 或 Cron 记录已逾期,只发送最新一次到期发生时点。Every 推进到投递判断时刻之后第一个与当前间隔起算点对齐的目标;Daily、Weekly 和 Cron 遵循上文与下文的本地日期规则。错过的发生时点不会累积。若没有可表示的下一 UTC 目标,投递后保留未运行记录及最近一次回执。
|
||||
|
||||
一次性提醒各自产生一条 follow-up。同一次扫描中属于同一 Session 的到期重复记录共用由 `renderRecurringReminderBatchFraming` 渲染的一条 follow-up,每条记录各包含最新一次触发。五分钟下限仅适用于固定速率间隔;投递不等待目标 Agent 空闲。
|
||||
一次性提醒各自产生一条 follow-up。同一次扫描中属于同一 Session 的到期重复记录共用由 `renderRecurringReminderBatchFraming` 渲染的一条 follow-up,每条记录各包含最新一次触发。一分钟下限仅适用于固定速率间隔;投递不等待目标 Agent 空闲。
|
||||
|
||||
<a id="cron-wall-clock-input"></a>
|
||||
## Cron 钟表时间输入
|
||||
@@ -339,7 +339,7 @@ type ScheduleCatalogEntry = ScheduleRecord & {
|
||||
|
||||
Remote 方法 `schedule.list({ sessionId })`、模型 `schedule_list` 和 Session 页头目录仅返回活动任务。模型视图派生的时间 `state` 与存储的生命周期 `status` 仍是不同概念。删除通过 `schedule.delete({ sessionId, id })` 使用条目的原始绑定;绑定不匹配时返回未找到。模型工具传入当前 Agent 的 Session,全局用户界面则传入所选任务的绑定。仅校验绑定并不构成调用者鉴权。无 payload 的 `schedule/changed` 事件通知客户端刷新列表;重新连接后,客户端再次获取当前状态。
|
||||
|
||||
Web bundle 默认禁用 `ui-schedule`;Schedule overlay 将它与宿主能力一起启用。[客户端包](../../packages/client/ui-schedule/README.zh.md) 负责目录、空状态与删除控件。页面单独筛选全部、活动和未运行任务,保留未运行任务详情及详情页签条内的原 Session 入口,并要求显式确认删除。“规则”和“发送记录”将任务设置与按需分页加载的已保存回执分开。回执与未运行状态均不确认模型执行。
|
||||
Web bundle 将 `ui-schedule` 与宿主能力一起挂载。[客户端包](../../packages/client/ui-schedule/README.zh.md) 负责目录、空状态与删除控件。页面单独筛选全部、活动和未运行任务,保留未运行任务详情及详情页签条内的原 Session 入口,并要求显式确认删除。“规则”和“发送记录”将任务设置与按需分页加载的已保存回执分开。回执与未运行状态均不确认模型执行。
|
||||
|
||||
## 修改时间
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/subagent.md
|
||||
/subagent:
|
||||
en: 4f8cfa6e59a1d98f
|
||||
zh: bd2b2fbabd418018
|
||||
en: 7ef8cafc02c317b5
|
||||
zh: 1ccf8273c706b436
|
||||
/subagent/two-kinds-of-capability-discovered-two-ways:
|
||||
en: 7be7cbc224948ed1
|
||||
zh: 6da4f61394ed900e
|
||||
@@ -12,11 +12,11 @@
|
||||
en: 37b7876d6f186642
|
||||
zh: 196a9df4fca5a4fd
|
||||
/subagent/continuable-children-and-activations:
|
||||
en: e011e4ac4227f4bc
|
||||
zh: b15cf25f519a116c
|
||||
en: 6cc3f373d6ad49b2
|
||||
zh: dd2cf41fbbc5fbe9
|
||||
/subagent/durable-enumeration-listchildren-listdescendants-and-their-entries:
|
||||
en: 3ad5d3831d5c5a5a
|
||||
zh: 26ca129a41ce7f77
|
||||
en: 5ff018408210d739
|
||||
zh: 909d4a1788d65629
|
||||
/subagent/the-terminal-result-subagentresult:
|
||||
en: 6070effb5f84f701
|
||||
zh: 2b8a6980ceb0b7b2
|
||||
@@ -27,5 +27,5 @@
|
||||
en: cb05cffdb837b7bc
|
||||
zh: a53133845d673f0a
|
||||
/subagent/in-process-backends-permission-depth-and-seed:
|
||||
en: ed6e94b122384d85
|
||||
zh: 5961582674b6af69
|
||||
en: 3290c070c97c284d
|
||||
zh: ebced35b081ea1c2
|
||||
+19
-23
@@ -4,7 +4,7 @@ English | [中文](subagent.zh.md)
|
||||
|
||||
The subagent seam lets an agent delegate work to a child agent. Like [bash](shell.md), it is **one optional capability**, not part of the agent loop, so its types live here rather than in [core.md](core.md). It differs from the other capability seams because **multiple provider implementations coexist** in one context, registered by name (`ctx.subagents`), while bash allows only one executor. Its registry follows the [LLM adapter registry](llm-streaming.md), not the single-service bash executor.
|
||||
|
||||
Service Definition: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Service Providers are sibling packages (`dsh-subagent-spawn-in-process`, `dsh-subagent-fork-in-process`, `dsh-subagent-acp`, `dsh-subagent-codex`, `dsh-subagent-claude-code`, `dsh-subagent-dsh-sdk`); the model-facing Consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation) and [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message`, `interrupt_agent`, and `list_agents` controls). The same `ctx.subagents` service owns continuable-child orchestration through an internal activation manager, direct-child discovery through the parent catalog, and complete descendant discovery through the Session corpus. Product-provider rationale lives in [the Codex and Claude Code Agent Note](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md); common-seam rationale lives in [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [the continuable subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), and [the adjacent-Agent messaging Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md); [the archived list-identity-projection record](../../.agents/notes/archived/architecture/2026-08-06-subagent-list-identity-projection.md) documents the original list-identity decision.
|
||||
Service Definition: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Service Providers are sibling packages (`dsh-subagent-spawn-in-process`, `dsh-subagent-fork-in-process`, `dsh-subagent-acp`, `dsh-subagent-codex`, `dsh-subagent-claude-code`, `dsh-subagent-dsh-sdk`); the model-facing Consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation) and [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message`, `interrupt_agent`, and `list_agents` controls). The same `ctx.subagents` service owns continuable-child orchestration through an internal activation manager, direct-child discovery through the parent catalog, and recursive descendant discovery through parent catalogs. Product-provider rationale lives in [the Codex and Claude Code Agent Note](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md); common-seam rationale lives in [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [the continuable subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), and [the adjacent-Agent messaging Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md); [the archived list-identity-projection record](../../.agents/notes/archived/architecture/2026-08-06-subagent-list-identity-projection.md) documents the original list-identity decision.
|
||||
|
||||
Sources: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts), [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts), and [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts)
|
||||
|
||||
@@ -262,24 +262,20 @@ interface ContinuableCreateSpec {
|
||||
|
||||
The descriptor (`SubagentDescriptorData` in [descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts)) is a mode-discriminated durable identity for every session-backed subagent. Both modes carry the provider name. A `one-shot` descriptor optionally carries a caller-owned display `label`; a `continuable` descriptor requires the delegation `description` as its durable creation label and additionally snapshots resolved child `agentOptions.provider`/`model`/`reasoningEffort` and optional `persona`/`toolFilter` for cold resume. It never snapshots the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation and a later composition input is a deliberate version change. It omits `subagentDepth` (cold resume trusts the persisted header's `delegationDepth` as the monotone floor) and `outputSchema` (one run or Activation's result contract, not durable identity).
|
||||
|
||||
A local one-shot provider appends the descriptor inside the child's initial turn before its first request. The continuation manager appends the descriptor after any provider-supplied lineage and before the initial prompt is admitted; `Session.inheritedEventCount` remains the fork-lineage boundary: resume-time descriptor authority reads the child's own suffix, while the list-serving identity projection folds `subagent/descriptor` last-wins so the child's own descriptor overrides a fork-seeded ancestor's. A seeded cold list skips a cache hint until an authoritative observation supplies that exact cut. The event is log-only: no `surfaceOp`, never in model history, and retained across compaction by the append-only log. Malformed current-version descriptors are corrupt; unsupported versions cannot be classified by this runtime.
|
||||
A local one-shot provider appends the descriptor inside the child's initial turn before its first request. The continuation manager appends the descriptor after any provider-supplied lineage and before the initial prompt is admitted; `Session.inheritedEventCount` remains the fork-lineage boundary: resume-time descriptor authority reads the child's own suffix, while the identity projection folds `subagent/descriptor` last-wins so the child's own descriptor overrides a fork-seeded ancestor's. The event is log-only: no `surfaceOp`, never in model history, and retained across compaction by the append-only log. Malformed current-version descriptors are corrupt; unsupported versions cannot be classified by this runtime.
|
||||
|
||||
## Durable enumeration: `listChildren()`, `listDescendants()`, and their entries
|
||||
|
||||
The model-facing `list_agents` adapter reports current activity as `running` or `inactive`. These values do not describe task completion or guarantee that `send_message` will succeed.
|
||||
|
||||
`SubagentRuntime.listChildren(parentSessionId, signal?)` reads the parent's `subagentCatalog` view through a live-preferred Session observation and releases that observation on success or failure. It returns direct-child entries in parent event order without reading child logs or enumerating the Session corpus. Query failures propagate; a missing catalog projection fails explicitly. Browser rows derive membership from the shared projection store and add activity from Session status; the control stream pushes complete catalog updates. `listDescendants()` retains corpus traversal, child-identity diagnostics, and complete `hasChildren` computation. [The parent-catalog Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns creation, fork isolation, ordering, and persistence costs.
|
||||
`SubagentRuntime.listChildren(parentSessionId, signal?)` reads the parent's `subagentCatalog` view through a live-preferred Session observation and releases that observation on success or failure. It returns direct-child entries in parent event order without reading child logs or enumerating the Session corpus. Query failures propagate; a missing catalog projection fails explicitly. Browser rows derive membership from the shared projection store and add activity from Session status; the control stream pushes complete catalog updates. `listDescendants()` recursively reads those catalogs and derives `hasChildren` from each child catalog. [The parent-catalog Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns creation, fork isolation, ordering, and persistence costs.
|
||||
|
||||
`SubagentRuntime.listDescendants(rootSessionId)` applies the same live-preferred corpus and projection-backed interpretation to the root's complete descendant tree in stable pre-order. Ordinary sessions and one-shot children remain traversal nodes, so continuable descendants below them are discovered; only `origin: 'subagent'` candidates produce rows. Each returned child or diagnostic adds its position from the enumerated durable header, while a cold inspection revalidates that complete lifecycle before serving identity:
|
||||
`SubagentRuntime.listDescendants(rootSessionId)` recursively calls the same catalog reader in stable pre-order, preserving each parent's event order. One-shot and unknown-mode entries remain traversal nodes; unknown modes produce `unsupported` diagnostics. An unreadable child catalog produces `corrupt` or `unavailable` and stops only that branch. Root read failures, missing services or projections, and cancellation reject the listing. Each reachable catalog is observed once and released before the next read; repeated ids and cycles are skipped. Sessions absent from reachable catalogs are not discovered, including ordinary Session forks and any subagents below those forks. Each row carries its catalog parent and root-relative depth:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One entry of a descendant listing: the interpreted subagent facts plus its
|
||||
* position in the complete session tree. `parentId` is the durable direct
|
||||
* parent from the enumerated header, and `depth` counts edges from the root.
|
||||
*/
|
||||
/** One catalog descendant with its direct parent and edge distance from the requested root. */
|
||||
type SubagentDescendantListEntry = SubagentListEntry & {
|
||||
/** Durable direct parent of this candidate in the enumerated tree. */
|
||||
/** Parent whose catalog contains this child. */
|
||||
readonly parentId: SessionId
|
||||
/** Edge distance from the requested root; direct children are `1`. */
|
||||
readonly depth: number
|
||||
@@ -465,7 +461,7 @@ The spawn and fork backends create an ordinary one-shot agent through `parent.ct
|
||||
- **Delegation depth** is durable `SessionHeader.delegationDepth` plus the merge-extensible runtime field `AgentOptions.subagentDepth`; absence means top-level depth zero, and the greater present value is authoritative. The seam owns both fields — the loop neither sets nor reads them — so an in-process child persists parent depth + 1, cold resume cannot lower it, and every start rejects a derived depth outside the safe-integer domain or above a defined absolute `request.maxDepth` cap.
|
||||
- **Fork seeding** uses [`CreateAgentOptions.seed`](core.md#creation-and-ownership) (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `ctx.agents.resume()` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and the [invariants](../../packages/runtime-diagnostics/invariants) replay accepts it (the in-flight, unbalanced turn is excluded).
|
||||
|
||||
`SubagentCatalogEntry` describes a direct child with complete or unknown-mode discovery information; `SubagentCatalogState` is the host-only projection state. `listChildren()` owns a live-preferred parent observation without opening child logs. Browser consumers read `subagentCatalog` through the shared Session projection store and combine membership with Session-list activity. `SubagentCatalogRow` belongs to complete-descendant listing. [The parent-catalog decision](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns the persistent facts and read semantics.
|
||||
`SubagentCatalogEntry` describes a direct child with complete or unknown-mode discovery information; `SubagentCatalogState` is the host-only projection state. `listChildren()` owns a live-preferred parent observation without opening child logs. Browser consumers read `subagentCatalog` through the shared Session projection store and combine membership with Session-list activity. `SubagentCatalogRow` belongs to recursive catalog listing. [The parent-catalog decision](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns the persistent facts and read semantics.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
@@ -585,19 +581,19 @@ async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): P
|
||||
listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentCatalogEntry[]>
|
||||
|
||||
/**
|
||||
* Enumerate the root's complete session-backed subagent tree in stable
|
||||
* pre-order from one live-preferred corpus, without loading or resuming an
|
||||
* Agent. Ordinary sessions and one-shot children remain traversal nodes so
|
||||
* continuable descendants below them are discovered; each returned entry
|
||||
* adds its durable `parentId` and root-relative `depth`. Identity resolution,
|
||||
* diagnostics, optional persistence, and cancellation use the registered
|
||||
* child identity projection and complete Session corpus.
|
||||
* @param rootSessionId - session whose complete descendant tree is listed.
|
||||
* @param signal - caller-owned cancellation forwarded to persistence reads
|
||||
* and observed around every read await.
|
||||
* @returns children and per-candidate diagnostics with tree position, in
|
||||
* stable pre-order.
|
||||
* Recursively list reachable parent catalogs in stable pre-order, preserving
|
||||
* each catalog's event order. Each row carries its catalog parent and depth;
|
||||
* one-shot and unknown-mode children remain traversal nodes. Unknown modes
|
||||
* produce unsupported diagnostics. Unreadable child catalogs produce corrupt
|
||||
* or unavailable diagnostics and stop only that branch. Root read failures,
|
||||
* missing services or projections, and cancellation reject the whole listing.
|
||||
* Each catalog is observed once and released before the next read. No Agent
|
||||
* is loaded or resumed; Sessions absent from reachable catalogs are omitted.
|
||||
* @param rootSessionId - session whose catalog starts descendant discovery.
|
||||
* @param signal - cancellation forwarded to and checked around each catalog read.
|
||||
* @returns children and branch diagnostics in parent-catalog pre-order.
|
||||
* @throws {@link SubagentError} when listing dependencies are unavailable or the caller cancels.
|
||||
* @throws SessionQueryError when the root catalog cannot be read.
|
||||
*/
|
||||
listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
subagent seam 让一个 agent(智能体)将工作委派给子 agent。与 [bash](shell.zh.md) 一样,它是**一项可选能力**,不属于 agent loop(智能体循环),因此其类型定义在此而非 [core.md](core.zh.md) 中。它不同于其他能力 seam,因为**同一上下文中可共存多个提供方实现**,并按名称注册(`ctx.subagents`),而 bash 只允许一个执行器。该注册表遵循 [LLM(大语言模型)适配器注册表](llm-streaming.zh.md),而非单服务的 bash 执行器。
|
||||
|
||||
Service Definition:[dsh-subagent](../../packages/subagent/subagent)(`ctx.subagents` + 下文词汇)。Service Provider 是六个兄弟包:`dsh-subagent-spawn-in-process`、`dsh-subagent-fork-in-process`、`dsh-subagent-acp`、`dsh-subagent-codex`、`dsh-subagent-claude-code`、`dsh-subagent-dsh-sdk`;面向模型的 Consumer 包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)和 [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message`、`interrupt_agent` 与 `list_agents` 控制工具)。同一个 `ctx.subagents` 服务通过内部激活管理器负责可继续子 agent 编排,通过 parent 目录发现直接 child,并通过 Session 语料库发现完整后代。产品提供方设计理由见 [Codex 与 Claude Code Agent Note](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md);通用 seam 的设计理由见 [subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md)、[可继续 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md)和[相邻 Agent 消息 Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md);[已归档的列表身份投影记录](../../.agents/notes/archived/architecture/2026-08-06-subagent-list-identity-projection.md)记录了最初的列表身份决策。
|
||||
Service Definition:[dsh-subagent](../../packages/subagent/subagent)(`ctx.subagents` + 下文词汇)。Service Provider 是六个兄弟包:`dsh-subagent-spawn-in-process`、`dsh-subagent-fork-in-process`、`dsh-subagent-acp`、`dsh-subagent-codex`、`dsh-subagent-claude-code`、`dsh-subagent-dsh-sdk`;面向模型的 Consumer 包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)和 [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message`、`interrupt_agent` 与 `list_agents` 控制工具)。同一个 `ctx.subagents` 服务通过内部激活管理器负责可继续子 agent 编排,通过 parent 目录发现直接 child,并通过父目录递归发现后代。产品提供方设计理由见 [Codex 与 Claude Code Agent Note](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md);通用 seam 的设计理由见 [subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md)、[可继续 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md)和[相邻 Agent 消息 Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md);[已归档的列表身份投影记录](../../.agents/notes/archived/architecture/2026-08-06-subagent-list-identity-projection.md)记录了最初的列表身份决策。
|
||||
|
||||
源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)、[`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts)和 [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts)
|
||||
|
||||
@@ -262,24 +262,20 @@ interface ContinuableCreateSpec {
|
||||
|
||||
描述符([descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts) 中的 `SubagentDescriptorData`)是每个由会话支撑的 subagent 所使用、按模式判别的持久化身份。两种模式都携带提供方名称。`one-shot` 描述符可以携带调用方拥有的可选显示 `label`;`continuable` 描述符要求以委派 `description` 作为持久化创建标签,并另外对已解析的子 agent `agentOptions.provider`/`model`/`reasoningEffort` 与可选的 `persona`/`toolFilter` 建立快照,用于冷恢复。它绝不会对可合并扩展的 `AgentOptions` 对象建立快照,因此无关的扩展值不会破坏继续执行,后续新增组合配置输入则是一次有意的版本更改。描述符省略 `subagentDepth`(冷恢复以持久化 header 中的 `delegationDepth` 作为单调下界)和 `outputSchema`(单次运行或 Activation 的结果约定,而非持久化身份)。
|
||||
|
||||
本地一次性提供方会在子 agent 的初始轮次内、首次请求前追加描述符。继续执行管理器会在任何提供方提供的谱系之后、初始提示词获准之前追加描述符;`Session.inheritedEventCount` 仍是 fork 谱系边界:恢复时的描述符权威读取子 agent 自身的后缀,而供列表使用的身份投影以 last-wins 折叠 `subagent/descriptor`,子 agent 自己的描述符会覆盖 fork seed 中祖先的描述符。seeded cold list 会跳过 cache hint,直到权威 observation 提供该精确 cut。该事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本描述符属于损坏;本运行时无法对不受支持的版本进行分类。
|
||||
本地一次性提供方会在子 agent 的初始轮次内、首次请求前追加描述符。继续执行管理器会在任何提供方提供的谱系之后、初始提示词获准之前追加描述符;`Session.inheritedEventCount` 仍是 fork 谱系边界:恢复时的描述符权威读取子 agent 自身的后缀,而身份投影以 last-wins 折叠 `subagent/descriptor`,子 agent 自己的描述符会覆盖 fork seed 中祖先的描述符。该事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本描述符属于损坏;本运行时无法对不受支持的版本进行分类。
|
||||
|
||||
## 持久化枚举:`listChildren()`、`listDescendants()` 与其条目
|
||||
|
||||
模型侧的 `list_agents` 适配器将当前活动表示为 `running` 或 `inactive`。这些值不描述任务完成情况,也不保证 `send_message` 会成功。
|
||||
|
||||
`SubagentRuntime.listChildren(parentSessionId, signal?)` 通过优先使用在线 Session 的观察读取父会话的 `subagentCatalog` 视图,并在成功或失败时释放观察。它按父会话事件顺序返回直接子级条目,不读取子级日志,也不枚举 Session 语料库。查询失败直接传播;缺少目录投影时显式失败。浏览器条目从共享 projection store 派生成员关系,并从 Session 状态补充活动状态;control stream 推送完整目录更新。`listDescendants()` 保留语料库遍历、子级身份诊断和完整的 `hasChildren` 计算。[父目录 Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明创建、fork 隔离、排序和持久化成本。
|
||||
`SubagentRuntime.listChildren(parentSessionId, signal?)` 通过优先使用在线 Session 的观察读取父会话的 `subagentCatalog` 视图,并在成功或失败时释放观察。它按父会话事件顺序返回直接子级条目,不读取子级日志,也不枚举 Session 语料库。查询失败直接传播;缺少目录投影时显式失败。浏览器条目从共享 projection store 派生成员关系,并从 Session 状态补充活动状态;control stream 推送完整目录更新。`listDescendants()` 递归读取这些目录,并从每个子级目录推导 `hasChildren`。[父目录 Agent Note](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明创建、fork 隔离、排序和持久化成本。
|
||||
|
||||
`SubagentRuntime.listDescendants(rootSessionId)` 将同一份实时优先语料与基于投影的解释应用到根的完整后代树,并按稳定 pre-order 输出。普通会话和一次性 child 仍作为遍历节点,因此其下的可继续后代仍可发现;只有 `origin: 'subagent'` 的候选会生成条目。每个返回的 child 或 diagnostic 都从枚举所得的持久 header 附加树位置;冷检查在提供身份前还会重新校验完整生命周期:
|
||||
`SubagentRuntime.listDescendants(rootSessionId)` 以稳定前序递归调用同一目录读取函数,保留每个父级的事件顺序。一次性和未知模式条目仍是遍历节点;未知模式产生 `unsupported` 诊断。无法读取的子级目录产生 `corrupt` 或 `unavailable`,只停止该分支。根读取失败、服务或投影缺失、取消会使整个查询失败。每个可达目录只观察一次,并在下一次读取前释放;重复 id 和循环引用会被跳过。可达目录中不存在的 Session 不会被发现,包括普通 Session fork 及其下的子 agent。每行携带目录中的父级和相对根的深度:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One entry of a descendant listing: the interpreted subagent facts plus its
|
||||
* position in the complete session tree. `parentId` is the durable direct
|
||||
* parent from the enumerated header, and `depth` counts edges from the root.
|
||||
*/
|
||||
/** One catalog descendant with its direct parent and edge distance from the requested root. */
|
||||
type SubagentDescendantListEntry = SubagentListEntry & {
|
||||
/** Durable direct parent of this candidate in the enumerated tree. */
|
||||
/** Parent whose catalog contains this child. */
|
||||
readonly parentId: SessionId
|
||||
/** Edge distance from the requested root; direct children are `1`. */
|
||||
readonly depth: number
|
||||
@@ -469,7 +465,7 @@ spawn 和 fork 后端通过 `parent.ctx` 创建一个普通的单次 agent,将
|
||||
- **委派深度**由持久 `SessionHeader.delegationDepth` 与可合并扩展的运行时字段 `AgentOptions.subagentDepth` 共同表示;缺失表示顶层深度为零,存在的较大值具有权威性。两个字段都归该 seam 所有——循环既不设置也不读取它们——因此进程内子 agent 会持久保存 parent 深度 + 1,冷恢复无法降低深度,而且每次 start 都会拒绝超出安全整数域、或高于已定义绝对 `request.maxDepth` 上限的派生深度。
|
||||
- **Fork 种子注入**使用 [`CreateAgentOptions.seed`](core.zh.md#creation-and-ownership)(一个 `SessionEvent[]` 前缀,经由 `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })` 传递,与 `ctx.agents.resume()` 使用的原语相同)。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*——父级事件直到并包括其最后一个 `turn/end`——因此种子从 0 连续,[invariants](../../packages/runtime-diagnostics/invariants) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。
|
||||
|
||||
`SubagentCatalogEntry` 描述一条完整或未知模式的直接子级目录记录;`SubagentCatalogState` 是仅 host 使用的 projection state。`listChildren()` 拥有一次 live-preferred 父 Session observation,不打开子级日志。浏览器消费者通过共享 Session projection store 读取 `subagentCatalog`,并将成员关系与 Session 列表活动状态组合。`SubagentCatalogRow` 属于完整后代列表。[父目录决策](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 规定持久事实与读取语义。
|
||||
`SubagentCatalogEntry` 描述一条完整或未知模式的直接子级目录记录;`SubagentCatalogState` 是仅 host 使用的 projection state。`listChildren()` 拥有一次 live-preferred 父 Session observation,不打开子级日志。浏览器消费者通过共享 Session projection store 读取 `subagentCatalog`,并将成员关系与 Session 列表活动状态组合。`SubagentCatalogRow` 属于递归目录列表。[父目录决策](../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 规定持久事实与读取语义。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
@@ -589,19 +585,19 @@ async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): P
|
||||
listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentCatalogEntry[]>
|
||||
|
||||
/**
|
||||
* Enumerate the root's complete session-backed subagent tree in stable
|
||||
* pre-order from one live-preferred corpus, without loading or resuming an
|
||||
* Agent. Ordinary sessions and one-shot children remain traversal nodes so
|
||||
* continuable descendants below them are discovered; each returned entry
|
||||
* adds its durable `parentId` and root-relative `depth`. Identity resolution,
|
||||
* diagnostics, optional persistence, and cancellation use the registered
|
||||
* child identity projection and complete Session corpus.
|
||||
* @param rootSessionId - session whose complete descendant tree is listed.
|
||||
* @param signal - caller-owned cancellation forwarded to persistence reads
|
||||
* and observed around every read await.
|
||||
* @returns children and per-candidate diagnostics with tree position, in
|
||||
* stable pre-order.
|
||||
* Recursively list reachable parent catalogs in stable pre-order, preserving
|
||||
* each catalog's event order. Each row carries its catalog parent and depth;
|
||||
* one-shot and unknown-mode children remain traversal nodes. Unknown modes
|
||||
* produce unsupported diagnostics. Unreadable child catalogs produce corrupt
|
||||
* or unavailable diagnostics and stop only that branch. Root read failures,
|
||||
* missing services or projections, and cancellation reject the whole listing.
|
||||
* Each catalog is observed once and released before the next read. No Agent
|
||||
* is loaded or resumed; Sessions absent from reachable catalogs are omitted.
|
||||
* @param rootSessionId - session whose catalog starts descendant discovery.
|
||||
* @param signal - cancellation forwarded to and checked around each catalog read.
|
||||
* @returns children and branch diagnostics in parent-catalog pre-order.
|
||||
* @throws {@link SubagentError} when listing dependencies are unavailable or the caller cancels.
|
||||
* @throws SessionQueryError when the root catalog cannot be read.
|
||||
*/
|
||||
listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 6cc2cd9ced70bb09
|
||||
zh: 1c0e6cb4ba2ed637
|
||||
/tool-schema-catalog/tool-package-map:
|
||||
en: ac9bc0329363007d
|
||||
zh: 0ef8b06296058cc6
|
||||
en: 72e2a375567f5850
|
||||
zh: b51b8ea5b52814fd
|
||||
/tool-schema-catalog/deepseek-ai-dsh-plugin-manager:
|
||||
en: e0143916bb634762
|
||||
zh: e0143916bb634762
|
||||
@@ -174,8 +174,8 @@
|
||||
en: 573da0cf49b1eed6
|
||||
zh: 573da0cf49b1eed6
|
||||
/tool-schema-catalog/deepseek-ai-dsh-schedule/schedule-create:
|
||||
en: ddd8148a0f9c849b
|
||||
zh: 8c930b69c46eab92
|
||||
en: 687b784279240ed2
|
||||
zh: a23fa1b08c15642b
|
||||
/tool-schema-catalog/deepseek-ai-dsh-schedule/schedule-delete:
|
||||
en: 3b5a084ce0d87d13
|
||||
zh: c1356d4b062f0e63
|
||||
@@ -183,8 +183,8 @@
|
||||
en: 4422ae2e2a4ca897
|
||||
zh: 56431645118d6ca0
|
||||
/tool-schema-catalog/deepseek-ai-dsh-schedule/schedule-update:
|
||||
en: d2cc5c77c73d6068
|
||||
zh: 639a86d6588e9d82
|
||||
en: a86e8751a34e9f06
|
||||
zh: 92a32fd4fc3657b4
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-lsp:
|
||||
en: 32982123b1f0e5bc
|
||||
zh: 32982123b1f0e5bc
|
||||
|
||||
@@ -32,7 +32,7 @@ This table connects model-visible tool names to the plugin package and service s
|
||||
| `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.subprocess`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. |
|
||||
| `@deepseek-ai/dsh-tool-terminal` | `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | `ctx.tools`, `ctx.terminals`, `ctx.systemPrompt`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. |
|
||||
| `@deepseek-ai/dsh-tool-goal` | `create_goal`, `get_goal`, `update_goal` | `ctx.tools`, `ctx.agents`, `ctx.goals`, `ctx.systemPrompt`, `a calling Agent in an authorized open turn` | `tool/call`, `goal/change for mutations`, `tool/result` | - | create, edit, pause, and resume require direct-human root authority; complete and blocked also accept the exact current goal round. The default blocked lower bound is three admitted rounds. |
|
||||
| `@deepseek-ai/dsh-schedule` | `schedule_create`, `schedule_delete`, `schedule_list`, `schedule_update` | `ctx.tools`, `ctx.schedule`, `a live root Agent` | `tool/call`, `Schedule storage domain create, update, or delete`, `tool/result` | - | Registered in live root Agent scopes while the opt-in Schedule service is loaded. Accepts after_seconds, explicit absolute at, bounded fixed-rate every_seconds, daily and weekly local times in an explicit IANA zone, and cron as a five-field expression. Management uses the Host storage domain; due messages resume the original Session. |
|
||||
| `@deepseek-ai/dsh-schedule` | `schedule_create`, `schedule_delete`, `schedule_list`, `schedule_update` | `ctx.tools`, `ctx.schedule`, `a live root Agent` | `tool/call`, `Schedule storage domain create, update, or delete`, `tool/result` | - | Registered in live root Agent scopes while the Schedule service is loaded. Accepts after_seconds, explicit absolute at, bounded fixed-rate every_seconds, daily and weekly local times in an explicit IANA zone, and cron as a five-field expression. Management uses the Host storage domain; due messages resume the original Session. |
|
||||
| `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.tools`, `ctx.lsp`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | The lsp tool keeps provider selection and language-server subprocesses behind ctx.lsp, so its model-visible schema stays stable across providers. Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-stdio`) at runtime; without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema. |
|
||||
| `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. |
|
||||
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, `user/message replacement catalogs via agent.inject()` | - | - |
|
||||
@@ -1382,7 +1382,7 @@ create, edit, pause, and resume require direct-human root authority; complete an
|
||||
|
||||
### `schedule_create`
|
||||
|
||||
Create one reminder in the current session. Supply a non-empty prompt, a title, and exactly one selector: a positive safe-integer after_seconds delay, at as a strict offset date-time or local date/time object, safe-integer every_seconds of at least 300, daily as {time: "23:00:00", time_zone: "Asia/Shanghai"}, weekly as {time: "09:00:00", time_zone: "Asia/Shanghai", weekdays: [1, 3]} with Monday 1 through Sunday 7, or cron as {expression: "*/15 9-17 * * 1-5", time_zone: "Asia/Shanghai"} with the five fields minute hour day-of-month month day-of-week. Every creation requires a title of at most 120 characters, non-empty after trimming; it names the task on its card, its detail heading, and in the task lists. Daily, weekly, and cron reminders retain that local time and zone; missing wall-clock times skip the date and repeated times use only the earlier instant. A cron day-of-month and day-of-week pair matches when either field matches once both are restricted. Fixed-rate targets stay creation-aligned until an interval edit establishes a new anchor. All four recurring kinds batch one latest occurrence per overdue rule. The Host restores this session when a reminder is due. After downtime, each recurring reminder delivers its latest missed occurrence once. Delivery can repeat after a crash.
|
||||
Create one reminder in the current session. Supply a non-empty prompt, a title, and exactly one selector: a positive safe-integer after_seconds delay, at as a strict offset date-time or local date/time object, safe-integer every_seconds of at least 60, daily as {time: "23:00:00", time_zone: "Asia/Shanghai"}, weekly as {time: "09:00:00", time_zone: "Asia/Shanghai", weekdays: [1, 3]} with Monday 1 through Sunday 7, or cron as {expression: "*/15 9-17 * * 1-5", time_zone: "Asia/Shanghai"} with the five fields minute hour day-of-month month day-of-week. Every creation requires a title of at most 120 characters, non-empty after trimming; it names the task on its card, its detail heading, and in the task lists. Daily, weekly, and cron reminders retain that local time and zone; missing wall-clock times skip the date and repeated times use only the earlier instant. A cron day-of-month and day-of-week pair matches when either field matches once both are restricted. Fixed-rate targets stay creation-aligned until an interval edit establishes a new anchor. All four recurring kinds batch one latest occurrence per overdue rule. The Host restores this session when a reminder is due. After downtime, each recurring reminder delivers its latest missed occurrence once. Delivery can repeat after a crash.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1402,7 +1402,7 @@ Create one reminder in the current session. Supply a non-empty prompt, a title,
|
||||
},
|
||||
"every_seconds": {
|
||||
"type": "number",
|
||||
"description": "Fixed-rate safe-integer interval in seconds, at least 300."
|
||||
"description": "Fixed-rate safe-integer interval in seconds, at least 60."
|
||||
},
|
||||
"daily": {
|
||||
"type": "object",
|
||||
@@ -1563,7 +1563,7 @@ Change one reminder in the current session in place, keeping its id and its save
|
||||
},
|
||||
"every_seconds": {
|
||||
"type": "number",
|
||||
"description": "Fixed-rate safe-integer interval in seconds, at least 300."
|
||||
"description": "Fixed-rate safe-integer interval in seconds, at least 60."
|
||||
},
|
||||
"daily": {
|
||||
"type": "object",
|
||||
@@ -1667,7 +1667,7 @@ Change one reminder in the current session in place, keeping its id and its save
|
||||
|
||||
Source: [`packages/schedule/schedule/src/tools.ts`](../packages/schedule/schedule/src/tools.ts)
|
||||
|
||||
Registered in live root Agent scopes while the opt-in Schedule service is loaded. Accepts after_seconds, explicit absolute at, bounded fixed-rate every_seconds, daily and weekly local times in an explicit IANA zone, and cron as a five-field expression. Management uses the Host storage domain; due messages resume the original Session.
|
||||
Registered in live root Agent scopes while the Schedule service is loaded. Accepts after_seconds, explicit absolute at, bounded fixed-rate every_seconds, daily and weekly local times in an explicit IANA zone, and cron as a five-field expression. Management uses the Host storage domain; due messages resume the original Session.
|
||||
|
||||
<a id="deepseek-aidsh-tool-lsp"></a>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
| `@deepseek-ai/dsh-tool-fs-search` | `glob`、`grep` | `ctx.tools`、`ctx.subprocess`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。 |
|
||||
| `@deepseek-ai/dsh-tool-terminal` | `terminal_close`、`terminal_list`、`terminal_open`、`terminal_read`、`terminal_send`、`terminal_signal` | `ctx.tools`、`ctx.terminals`、`ctx.systemPrompt`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | 这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.jobs`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。 |
|
||||
| `@deepseek-ai/dsh-tool-goal` | `create_goal`、`get_goal`、`update_goal` | `ctx.tools`、`ctx.agents`、`ctx.goals`、`ctx.systemPrompt`、`a calling Agent in an authorized open turn` | `tool/call`、`goal/change for mutations`、`tool/result` | - | create、edit、pause 和 resume 要求直接来自人类的根权限;complete 和 blocked 也接受确切的当前 Goal Round。blocked 的默认下限是 3 个获准的 Round。 |
|
||||
| `@deepseek-ai/dsh-schedule` | `schedule_create`、`schedule_delete`、`schedule_list`、`schedule_update` | `ctx.tools`、`ctx.schedule`、live 根 Agent | `tool/call`、Schedule storage domain 创建、更新或删除、`tool/result` | - | 选择启用的 Schedule 服务加载期间,在 live 根 Agent scope 内注册。接受 after_seconds、显式绝对 at、有界固定速率 every_seconds、带显式 IANA 时区的每日与每周本地时间,以及作为五字段表达式的 cron。管理使用宿主 storage domain;到期消息会恢复原 Session。 |
|
||||
| `@deepseek-ai/dsh-schedule` | `schedule_create`、`schedule_delete`、`schedule_list`、`schedule_update` | `ctx.tools`、`ctx.schedule`、live 根 Agent | `tool/call`、Schedule storage domain 创建、更新或删除、`tool/result` | - | Schedule 服务加载期间,在 live 根 Agent scope 内注册。接受 after_seconds、显式绝对 at、有界固定速率 every_seconds、带显式 IANA 时区的每日与每周本地时间,以及作为五字段表达式的 cron。管理使用宿主 storage domain;到期消息会恢复原 Session。 |
|
||||
| `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.tools`、`ctx.lsp`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,因此其模型可见 schema 在更换提供方时保持稳定。运行时要求已注册提供方,例如 `@deepseek-ai/dsh-lsp-stdio`;如果没有提供方,查询会返回结构化 `LSP_UNAVAILABLE` 错误,而不会改变 schema。 |
|
||||
| `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 |
|
||||
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - |
|
||||
@@ -1388,7 +1388,7 @@ create、edit、pause 和 resume 要求直接来自人类的根权限;complete
|
||||
|
||||
### `schedule_create`
|
||||
|
||||
在当前会话中创建一条提醒。请提供非空 prompt、标题,以及恰好一个 selector:正的安全整数 after_seconds 延时;作为严格带偏移日期时间或本地日期/时间对象的 at;不小于 300 的安全整数 every_seconds;形如 {time: "23:00:00", time_zone: "Asia/Shanghai"} 的 daily;形如 {time: "09:00:00", time_zone: "Asia/Shanghai", weekdays: [1, 3]} 的 weekly,其中周一为 1、周日为 7;或形如 {expression: "*/15 9-17 * * 1-5", time_zone: "Asia/Shanghai"} 的 cron,其五个字段为 minute hour day-of-month month day-of-week。每次创建都必须提供标题,最多 120 个字符且去除首尾空白后非空;该标题用于任务卡片、详情标题和任务列表。每日、每周与 cron 提醒保留指定的本地时间与时区;不存在的钟表时间会跳过该日期,重复时刻只取较早实例。cron 的 day-of-month 与 day-of-week 都受限时,匹配任一字段即算匹配。固定速率提醒在间隔编辑确立新起点前始终与创建时刻对齐。四类重复提醒都会将每条逾期规则的最新一个发生时点合并为一批投递。宿主会在提醒到期时恢复此会话。停机后,每条重复提醒只投递最新错过的一次。崩溃后可能重复投递。
|
||||
在当前会话中创建一条提醒。请提供非空 prompt、标题,以及恰好一个 selector:正的安全整数 after_seconds 延时;作为严格带偏移日期时间或本地日期/时间对象的 at;不小于 60 的安全整数 every_seconds;形如 {time: "23:00:00", time_zone: "Asia/Shanghai"} 的 daily;形如 {time: "09:00:00", time_zone: "Asia/Shanghai", weekdays: [1, 3]} 的 weekly,其中周一为 1、周日为 7;或形如 {expression: "*/15 9-17 * * 1-5", time_zone: "Asia/Shanghai"} 的 cron,其五个字段为 minute hour day-of-month month day-of-week。每次创建都必须提供标题,最多 120 个字符且去除首尾空白后非空;该标题用于任务卡片、详情标题和任务列表。每日、每周与 cron 提醒保留指定的本地时间与时区;不存在的钟表时间会跳过该日期,重复时刻只取较早实例。cron 的 day-of-month 与 day-of-week 都受限时,匹配任一字段即算匹配。固定速率提醒在间隔编辑确立新起点前始终与创建时刻对齐。四类重复提醒都会将每条逾期规则的最新一个发生时点合并为一批投递。宿主会在提醒到期时恢复此会话。停机后,每条重复提醒只投递最新错过的一次。崩溃后可能重复投递。
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1408,7 +1408,7 @@ create、edit、pause 和 resume 要求直接来自人类的根权限;complete
|
||||
},
|
||||
"every_seconds": {
|
||||
"type": "number",
|
||||
"description": "Fixed-rate safe-integer interval in seconds, at least 300."
|
||||
"description": "Fixed-rate safe-integer interval in seconds, at least 60."
|
||||
},
|
||||
"daily": {
|
||||
"type": "object",
|
||||
@@ -1573,7 +1573,7 @@ create、edit、pause 和 resume 要求直接来自人类的根权限;complete
|
||||
},
|
||||
"every_seconds": {
|
||||
"type": "number",
|
||||
"description": "Fixed-rate safe-integer interval in seconds, at least 300."
|
||||
"description": "Fixed-rate safe-integer interval in seconds, at least 60."
|
||||
},
|
||||
"daily": {
|
||||
"type": "object",
|
||||
@@ -1677,7 +1677,7 @@ create、edit、pause 和 resume 要求直接来自人类的根权限;complete
|
||||
|
||||
Source: [`packages/schedule/schedule/src/tools.ts`](../packages/schedule/schedule/src/tools.ts)
|
||||
|
||||
Registered in live root Agent scopes while the opt-in Schedule service is loaded. Accepts after_seconds, explicit absolute at, bounded fixed-rate every_seconds, daily and weekly local times in an explicit IANA zone, and cron as a five-field expression. Management uses the Host storage domain; due messages resume the original Session.
|
||||
Schedule 服务加载期间,在 live 根 Agent scope 内注册。接受 after_seconds、显式绝对 at、有界固定速率 every_seconds、带显式 IANA 时区的每日与每周本地时间,以及作为五字段表达式的 cron。管理使用宿主 storage domain;到期消息会恢复原 Session。
|
||||
|
||||
<a id="deepseek-aidsh-tool-lsp"></a>
|
||||
|
||||
|
||||
Loaded 100 of 346 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user