mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
Merge pull request #7734 from deepseek-harness/worktree-sync-dsh-0.2.2-to-master
release(dsh): sync release/dsh-0.2.2 back to master
This commit is contained in:
+6
-6
@@ -9,11 +9,11 @@
|
||||
en: 47ba548b869be190
|
||||
zh: c2e5038a56fe741c
|
||||
/agent-note-asynchronous-projection-registration-and-retained-readers/decision:
|
||||
en: be0d17bfbd1b59d4
|
||||
zh: 7a23bccdf95c21ff
|
||||
en: a6f9ae8a53af5e4b
|
||||
zh: 0aece791574ef060
|
||||
/agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered:
|
||||
en: e7c66403fab991d2
|
||||
zh: 1580ec29116c7d42
|
||||
en: 12e0f5fc9638acc6
|
||||
zh: 24213550da27b70b
|
||||
/agent-note-asynchronous-projection-registration-and-retained-readers/consequences:
|
||||
en: 31ce6f02dd3bf39d
|
||||
zh: bcd29b2146f689b5
|
||||
en: f5c294608d87844f
|
||||
zh: 24488e439a04d81a
|
||||
|
||||
+6
-2
@@ -12,7 +12,7 @@ Host readers coupled to registry key lookup lose access when a plugin unloads, a
|
||||
|
||||
`SessionProjectionRegistry.register` returns an awaitable registration for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules.
|
||||
|
||||
A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives, snapshots, and checkpoints. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle.
|
||||
A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives and snapshots. Checkpoints keep each Session's last cell of the released unit until its key registers again: the registry no longer advances that cell, while a retained handle read still advances it forward, so a writer that runs after the owner unloads still stores the state already folded. Under the Cordis rc.11 teardown order, Web-layer plugins release their units before the Sessions they describe detach, and the final projection-cache write would otherwise drop those rows. Stored rows can therefore lag the log; the cache promises only possibly stale rows. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle.
|
||||
|
||||
AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. AgentLoop acquires its inbox handle once before publishing its factory, reads the last turn before creating an Agent scope, and passes an inbox callback plus that turn number into synchronous Agent construction. The callback retains its handle through cleanup after the loop fiber removes global registrations. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state.
|
||||
|
||||
@@ -20,6 +20,10 @@ Plugins compiled against the synchronous API keep running during migration. The
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Detach owned Sessions before unregistering.** Other Web-layer plugins unload in earlier fibers under the rc.11 teardown order, and plugin reload or disable reaches the same timing, so ordering one owner cannot keep every row.
|
||||
|
||||
**Checkpoint a released unit only while its cell is at the Session cursor.** A Session with a running turn at shutdown appends teardown events after its units are released, so its rows would be dropped again.
|
||||
|
||||
**Keep registry-level lookup and delay unregistering.** This couples unrelated plugin disposal order to every reader's final access. Retained readers make cleanup independent of that order.
|
||||
|
||||
**Return a synchronous handle.** This preserves callers' immediate reads but commits the acquisition API to synchronous initialization. Awaited acquisition leaves that timing explicit while state reads remain synchronous after readiness.
|
||||
@@ -36,4 +40,4 @@ Plugins compiled against the synchronous API keep running during migration. The
|
||||
|
||||
## Consequences
|
||||
|
||||
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. Compatibility tests and a real Loader subprocess verify the deprecated calls.
|
||||
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. The registry also retains a released predecessor, with its definition closures and weakly keyed cells, until the same key registers again, so a key that a reloaded plugin renames or stops registering keeps the old definition referenced for the process lifetime. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. Compatibility tests and a real Loader subprocess verify the deprecated calls.
|
||||
|
||||
+6
-2
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
`SessionProjectionRegistry.register` 返回可等待的注册结果,用于获取类型化句柄。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。
|
||||
|
||||
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
|
||||
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进和快照中移除单元。检查点仍保留每个 Session 中已释放单元的最后一个 cell,直到它的 key 再次注册:注册表不再推进这个 cell,但已保留的 handle 读取时仍会将其向前推进,因此 owner 卸载后才运行的写入方仍会存下已经折叠出的状态。按 Cordis rc.11 的卸载顺序,Web 层插件会在它们所描述的 Session detach 之前释放单元,否则 projection cache 的最终写入会丢掉这些行。因此存储的行可能落后于日志;cache 只承诺行可能过时。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
|
||||
|
||||
AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`。AgentLoop 在发布工厂前只获取一次 inbox 句柄,在创建 Agent 作用域前读取最后轮次,并将 inbox 回调与该轮次编号传入同步 Agent 构造过程。loop fiber 移除全局注册后,回调仍在清理期间保留其句柄。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。
|
||||
|
||||
@@ -20,6 +20,10 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**卸载前先 detach 自己拥有的 Session。** 按 rc.11 的卸载顺序,其他 Web 层插件在更早卸载的 fiber 里,插件重载或禁用也会碰上同样的时机,调整单个 owner 的顺序保不住所有行。
|
||||
|
||||
**只在已释放单元的 cell 位于 Session 游标时才写入检查点。** 关停时正在运行一轮的会话会在单元释放后追加 teardown 事件,因此它的这些行会再次被丢掉。
|
||||
|
||||
**保留注册表级查询并延迟注销。** 这使无关插件的释放顺序依赖每个读取方的最后一次访问。保留读取句柄使清理不再依赖该顺序。
|
||||
|
||||
**返回同步句柄。** 这保留消费者的立即读取,却将获取 API 固定为同步初始化。等待获取过程使时序显式,同时就绪后的状态读取仍同步。
|
||||
@@ -36,4 +40,4 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
|
||||
|
||||
## Consequences
|
||||
|
||||
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
|
||||
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册表自身也会保留已释放的前任及其定义闭包和弱键单元,直到同一 key 再次注册;因此重载后改名或不再注册的 key 会在进程生命周期内一直引用旧定义。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 9f84c9bd85b6aace
|
||||
zh: 8ebb10962c219624
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/decision:
|
||||
en: 6e8f43d7a53c35fd
|
||||
zh: a6e2bccc20474515
|
||||
en: aada3ca481120bff
|
||||
zh: 1fa45a276a47e921
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/alternatives-considered:
|
||||
en: ce8320f1032018ed
|
||||
zh: 4c5fad5e605bf524
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ A Session-list row carries every registered client view. Four of those views are
|
||||
The Session summary is every registered client view except the keys whose `SessionProjectionMap` entry carries the summary-exclusion mark.
|
||||
|
||||
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) declares the type-only mark `SessionProjectionSummaryExcluded<T>` over a unique symbol that is never exported and never present at runtime. `SessionProjectionViewValue<T>` strips it distributively, so a nullable entry keeps its `null`; `SessionProjectionSummaryExcludedKey` tests the non-nullish part; `SessionProjectionSummaryValues` is every view value minus those keys.
|
||||
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), and `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
|
||||
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares), and `contextPressure` and `contextBreakdown` (token-meter). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
|
||||
- Registration carries the same decision as a plain boolean: `wire.summary` says whether a Session summary includes the unit, and omitting it means `true`. A key whose map entry carries the mark declares `false` so the runtime key set and the client view type agree. The mark itself has no runtime representation — `wire.view` and `viewSchema` are typed with `SessionProjectionViewValue`, which strips it, so the erase in `register` only widens the state type.
|
||||
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) exposes `summaryKeys(): ReadonlySet<key>`, derived from the live registrations and rebuilt only when a new key registers or a key's last registrant disposes. `cachedSnapshot`, `viewCheckpoint`, and `snapshot` accept `ProjectionKeySelection` (`readonly Key[] | ReadonlySet<Key>`) and use a passed set as-is.
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live and cold cached faces. `cachedPredecessorTitle` keeps its title-only read.
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live registry's `snapshot` and to the cold cache read. `cachedPredecessorTitle` keeps its title-only read.
|
||||
- Client types only: `SessionSummary.projectionValues` narrows to `Partial<SessionProjectionSummaryValues>`, while `SessionProjectionSnapshot.values` keeps every view value. No wire field, function, or UI name changes, and an opened Session still receives every key through `session/follow`, the control baseline, and `session.projections`.
|
||||
|
||||
Estimated effect: with logs whose outlines dominate the projection payload, the four keys are estimated at roughly two thirds of a Session-list response for a few hundred Sessions — the outline alone near 57%, the figures and todo list near 5% together, and the shared image limits near 5% — and at roughly 85% of the same response after transport compression, where the outline's unique text barely compresses while the remaining keys do.
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ Session 列表行携带每个已注册的客户端视图。其中四个视图从
|
||||
Session 摘要就是每个已注册客户端视图,减去 `SessionProjectionMap` 条目带摘要排除标记的键。
|
||||
|
||||
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) 声明类型层标记 `SessionProjectionSummaryExcluded<T>`,它基于一个从不导出、运行时从不存在的 unique symbol。`SessionProjectionViewValue<T>` 以分发方式去掉标记,可空条目因此保留 `null`;`SessionProjectionSummaryExcludedKey` 取非空部分判断;`SessionProjectionSummaryValues` 是全部视图值减去这些键。
|
||||
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo),以及 `imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
|
||||
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo)、`imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享),以及 `contextPressure` 和 `contextBreakdown`(token-meter)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
|
||||
- 注册用普通布尔值承载同一决定:`wire.summary` 表示 Session 摘要是否包含该单元,省略即 `true`。map 条目带标记的 key 声明 `false`,让运行时键集合与客户端视图类型一致。标记本身没有运行时表示 —— `wire.view` 与 `viewSchema` 的签名已用 `SessionProjectionViewValue` 去标记,因此 `register` 里的擦除只是放宽状态类型。
|
||||
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) 提供 `summaryKeys(): ReadonlySet<key>`,从实时注册派生,只有新键注册或某键最后一个注册者注销时才重建。`cachedSnapshot`、`viewCheckpoint`、`snapshot` 接受 `ProjectionKeySelection`(`readonly Key[] | ReadonlySet<Key>`),传入 Set 时直接使用。
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给 live 与 cold 两个缓存读取面。`cachedPredecessorTitle` 保持只读 title。
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给实时注册表的 `snapshot` 与冷会话的缓存读取。`cachedPredecessorTitle` 保持只读 title。
|
||||
- 客户端只有类型变化:`SessionSummary.projectionValues` 收窄为 `Partial<SessionProjectionSummaryValues>`,而 `SessionProjectionSnapshot.values` 保留全部视图值。wire 字段、函数名与 UI 名称均不变;打开的 Session 仍通过 `session/follow`、控制基线与 `session.projections` 收到每个键。
|
||||
|
||||
估算效果:当 outline 在投影负载中占主导时,四个键估计占几百个 Session 时列表响应的约三分之二 —— outline 单项约 57%,统计与 todo 合计约 5%,共享的图片上限约 5% —— 占传输压缩后同一响应的约 85%,因为 outline 的唯一文本几乎无法压缩,而其余键可以。
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 344bcc7e9825bbc8
|
||||
zh: c1d0f4b54f5b3786
|
||||
/agent-note-change-request-panel-and-worktree-sessions/decision:
|
||||
en: 6a0dc836205863ba
|
||||
zh: 3d4feb0385953dda
|
||||
en: 66bde24e7c601eb6
|
||||
zh: 5091591f3dc1ac85
|
||||
/agent-note-change-request-panel-and-worktree-sessions/deferred:
|
||||
en: 32ef8af85f4d2f66
|
||||
zh: b82b67f94346399e
|
||||
@@ -18,5 +18,5 @@
|
||||
en: e471e16b6b6450ca
|
||||
zh: 5fbc9242edc8d112
|
||||
/agent-note-change-request-panel-and-worktree-sessions/consequences:
|
||||
en: 81790c448f0a7d3b
|
||||
zh: ec63ccb2ad315ff8
|
||||
en: 0b785a2204963c4f
|
||||
zh: be4bd8db40b801a1
|
||||
|
||||
@@ -10,7 +10,9 @@ English | [中文](2026-10-09-forge-panel.zh.md)
|
||||
|
||||
## Decision
|
||||
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.md) is a two-face package. Its Host half, the `forgePanel` Remote, assembles everything the panel shows for one Session into one streamed state; the forge registry and the binding service keep only their domain operations. Its Client half is a right-sidebar tab, a Session header button that shows or hides it, a New Session option, a creation card above the composer, a toast, and the [Session list icons and hover card section](2026-10-20-forge-session-list.md). Every surface calls a bound item a change request, the forge registry's term, whatever its code host calls it. The [forge bundle](../../../../packages/experimental/forge-bundle/README.md) ships the panel, the forge registry, the GitHub provider, the bindings, and an isolated worktree service as one default-disabled Plugins page entry.
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.md) is a two-face package. Its Host half, the `forgePanel` Remote, assembles everything the panel shows for one Session into one streamed state; the forge registry and the binding service keep only their domain operations. Its Client half is a right-sidebar tab, a Session header button that shows or hides it, a New Session option, a creation card above the composer, a toast, and the [Session list icons and hover card section](2026-10-20-forge-session-list.md). Every surface calls a bound item a change request, the forge registry's term, whatever its code host calls it. The [forge bundle](../../../../packages/experimental/forge-bundle/README.md) ships the panel, the forge registry, the GitHub provider, the bindings, the worktree model tool, and an isolated worktree service for the panel as one default-disabled Plugins page entry.
|
||||
|
||||
Review and worktree controls serve one code-change workflow. One Plugins entry lets users enable that workflow without having to distinguish the underlying package split. Packages and the two worktree services remain independent so each owns its before-Session or in-conversation creation flow.
|
||||
|
||||
The Remote inspects the checkout through the Session's subprocess provider under its [resolved permissions and query restrictions](../bug-fix/2026-10-28-forge-git-session-permissions.md), so it reads what the Session's commands see. Inspection reads the top-level, Git, and common Git directories, the branch or the abbreviated HEAD commit, the upstream, the remotes, and the size of the branch's changes against its fork point. It runs when a Session is followed, after each `working-directory/change`, `tool/result`, and `sandbox/mode` event, and on refresh, one at a time with one trailing rerun.
|
||||
|
||||
@@ -89,4 +91,4 @@ The changes view offers no scope for one commit or for the last turn yet; both w
|
||||
|
||||
## Consequences
|
||||
|
||||
The panel shows current status only while it is open; checks that finish while nothing is mounted produce no notification. Changes made outside the Session appear after Refresh or the next tool result. Every inspection spawns up to twelve short Git processes in the Session's execution world with one remote, and two more for each further remote it tries for a fork point. A worktree exists only for a Session that requested it before its first step, and a failed or cancelled attempt can leave its branch or checkout behind. A message that enters while the Agent runs a turn or other maintenance gets no hold, so its worktree is created within the first turn and a removed worktree is not checked. While a message waits, the Agent stays idle in maintenance: the composer offers no stop, and callers waiting for the Agent to become idle wait as well. Restoring loses changes that were never committed. A base branch that requires a merge queue cannot merge from the panel. With Git Worktrees also enabled, two independent worktree services exist; the model's tool never sees the panel's service. A local or change request diff over `maxDiffBytes`, or one the host refuses, shows no files. Expanding unchanged lines of a change request reads each file from the code host once.
|
||||
The panel shows current status only while it is open; checks that finish while nothing is mounted produce no notification. Changes made outside the Session appear after Refresh or the next tool result. Every inspection spawns up to twelve short Git processes in the Session's execution world with one remote, and two more for each further remote it tries for a fork point. A worktree exists only for a Session that requested it before its first step, and a failed or cancelled attempt can leave its branch or checkout behind. A message that enters while the Agent runs a turn or other maintenance gets no hold, so its worktree is created within the first turn and a removed worktree is not checked. While a message waits, the Agent stays idle in maintenance: the composer offers no stop, and callers waiting for the Agent to become idle wait as well. Restoring loses changes that were never committed. A base branch that requires a merge queue cannot merge from the panel. The bundle mounts two independent worktree services; the model's tool never sees the panel's service. A local or change request diff over `maxDiffBytes`, or one the host refuses, shows no files. Expanding unchanged lines of a change request reads each file from the code host once.
|
||||
|
||||
@@ -10,7 +10,9 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.zh.md) 是一个双端包。宿主端的 `forgePanel` Remote 把面板为一个 Session 展示的全部内容组装成一条流式状态;forge 注册表与绑定服务只保留各自的领域操作。客户端包括一个右侧边栏标签页、一个显示或隐藏它的 Session 标题栏按钮、一个新会话选项、一张位于输入框上方的创建卡片、一个 toast,以及[会话列表中的图标与悬浮卡片部分](2026-10-20-forge-session-list.zh.md)。所有界面都把已绑定的条目称为变更请求(forge 注册表的术语),不随代码托管平台的叫法变化。[forge 套件](../../../../packages/experimental/forge-bundle/README.zh.md)把面板、forge 注册表、GitHub 提供方、绑定服务与一个隔离的 worktree 服务作为插件页中默认禁用的一个条目发布。
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.zh.md) 是一个双端包。宿主端的 `forgePanel` Remote 把面板为一个 Session 展示的全部内容组装成一条流式状态;forge 注册表与绑定服务只保留各自的领域操作。客户端包括一个右侧边栏标签页、一个显示或隐藏它的 Session 标题栏按钮、一个新会话选项、一张位于输入框上方的创建卡片、一个 toast,以及[会话列表中的图标与悬浮卡片部分](2026-10-20-forge-session-list.zh.md)。所有界面都把已绑定的条目称为变更请求(forge 注册表的术语),不随代码托管平台的叫法变化。[forge 套件](../../../../packages/experimental/forge-bundle/README.zh.md)把面板、forge 注册表、GitHub 提供方、绑定服务、工作树模型工具与面板使用的一个隔离 worktree 服务作为插件页中默认禁用的一个条目发布。
|
||||
|
||||
评审与工作树控件服务于同一个代码修改流程。统一插件入口让用户可以一次启用这些能力,无需根据底层包的拆分判断应该开启哪张卡片。包与两套工作树服务仍保持独立,分别负责会话开始前和对话中创建工作树的流程。
|
||||
|
||||
Remote 通过 Session 的子进程提供方,按其[解析出的权限与查询限制](../bug-fix/2026-10-28-forge-git-session-permissions.zh.md)检查检出,因此它读到的与 Session 命令看到的相同。检查读取顶层目录、Git 目录与公共 Git 目录、分支或缩写的 HEAD 提交、上游与远端,以及分支相对于其分叉点的变更规模。它在 Session 被关注时、每个 `working-directory/change`、`tool/result` 与 `sandbox/mode` 事件之后以及刷新时运行,一次只运行一个,最多再追加一次。
|
||||
|
||||
@@ -89,4 +91,4 @@ Session 所在的 worktree 可能在 Session 之外被删除。每次检查都
|
||||
|
||||
## Consequences
|
||||
|
||||
面板只在打开时显示当前状态;没有任何界面挂载时检查结束不会有通知。Session 之外的改动要等到刷新或下一个工具结果才会出现。只有一个远端时,每次检查最多在 Session 的执行环境中启动十二个短暂的 Git 进程;为寻找分叉点每多尝试一个远端,就多启动两个。只有在第一步之前提出请求的 Session 才会有 worktree,失败或被取消的尝试可能留下它的分支或检出。Agent 正在运行一轮或其他维护任务时进入的消息不会被暂留,因此它的 worktree 在第一轮之内创建,已删除的 worktree 也不会被检查。消息等待期间,Agent 以空闲状态处于维护任务中:输入框不提供停止,等待 Agent 空闲的调用方也会一并等待。恢复会丢失从未提交的修改。要求合并队列的基准分支无法从面板合并。同时启用 Git Worktrees 时会存在两个互不影响的 worktree 服务;模型的工具看不到面板的服务。本地或变更请求的 diff 超过 `maxDiffBytes`,或被托管平台拒绝时,不显示任何文件。展开变更请求中未变更的行时,每个文件只从托管平台读取一次。
|
||||
面板只在打开时显示当前状态;没有任何界面挂载时检查结束不会有通知。Session 之外的改动要等到刷新或下一个工具结果才会出现。只有一个远端时,每次检查最多在 Session 的执行环境中启动十二个短暂的 Git 进程;为寻找分叉点每多尝试一个远端,就多启动两个。只有在第一步之前提出请求的 Session 才会有 worktree,失败或被取消的尝试可能留下它的分支或检出。Agent 正在运行一轮或其他维护任务时进入的消息不会被暂留,因此它的 worktree 在第一轮之内创建,已删除的 worktree 也不会被检查。消息等待期间,Agent 以空闲状态处于维护任务中:输入框不提供停止,等待 Agent 空闲的调用方也会一并等待。恢复会丢失从未提交的修改。要求合并队列的基准分支无法从面板合并。组合包挂载两个互不影响的 worktree 服务;模型的工具看不到面板的服务。本地或变更请求的 diff 超过 `maxDiffBytes`,或被托管平台拒绝时,不显示任何文件。展开变更请求中未变更的行时,每个文件只从托管平台读取一次。
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 3e229ecf646af866
|
||||
zh: d358507b1e1d381f
|
||||
/agent-note-wine-and-native-windows-ci/decision:
|
||||
en: 1125b44a170f70fb
|
||||
zh: 37838b9da2003651
|
||||
en: 9038773113cf3553
|
||||
zh: 67875d393093c105
|
||||
/agent-note-wine-and-native-windows-ci/alternatives-considered:
|
||||
en: 0e9a322cfc38d313
|
||||
zh: f7bdeb86ba4fea1a
|
||||
|
||||
@@ -30,7 +30,7 @@ Native watchers use `canonicalizeWatchPath()` to realpath the deepest existing a
|
||||
|
||||
Windows durable JSONL paths keep drive roots in native spelling and apply the extended-length namespace only to descendants and staging paths. The ACP teardown ladder uses real Node children, proves graceful and forced tiers with host-appropriate outcomes, and avoids claiming POSIX signal delivery on Windows. Executable fixtures provide `.cmd` shims and `PATHEXT` where the product accepts a bare command. Repository-cache helpers live inside the selected Git subpath so their declared `file:` dependencies expose command shims identically on Windows. The bundled installer exports pnpm's own workspace-ignore configuration, retains `PNPM_HOME` for pnpm data while removing that directory from lifecycle `PATH`, and prioritizes `.CMD` in `PATHEXT`; nested Git-package installation therefore cannot rejoin the enclosing workspace or let an inherited Windows pnpm executable outrank the transaction-owned wrapper.
|
||||
|
||||
Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../../../../apps/cli/src/process-shutdown.ts) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include serializes debounced writes, retries only transient access or busy failures with bounded backoff, and observes every timer rejection. A terminal persistence failure remains on the queue and is rethrown to the teardown owner, while successful teardown drains the latest write.
|
||||
Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../../../../apps/cli/src/process-shutdown.ts) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include journals runtime changes and writes them through an atomic rename, retrying only transient access or busy failures with bounded backoff. `flush()` and `stop()` reject while changes remain unsaved, and a later successful flush clears the previous error.
|
||||
|
||||
Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.153.4 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Wine 在 Linux 内核与区分大小写的 ext4 之上采用 hoisted 依赖布
|
||||
|
||||
Windows 的持久 JSONL 路径会保留驱动器根目录的原生写法,并仅对后代路径与暂存路径应用扩展长度命名空间。ACP(Agent Client Protocol)拆卸阶梯使用真实 Node 子进程,以符合宿主语义的结果证明优雅终止与强制终止两个层级,并避免声称 Windows 会交付 POSIX 信号。产品接受裸命令时,可执行 fixture 会提供 `.cmd` 包装脚本与 `PATHEXT`。repository-cache 辅助包位于所选 Git 子路径内,因此它们声明的 `file:` 依赖会在 Windows 上以相同方式暴露命令包装脚本。随附的安装器会导出 pnpm 自有的 workspace-ignore 配置,保留 `PNPM_HOME` 作为 pnpm 数据配置,同时从生命周期 `PATH` 中移除该目录,并在 `PATHEXT` 中优先选择 `.CMD`;因此,嵌套 Git 包安装既不会重新加入外层 workspace,也不会让继承的 Windows pnpm 可执行文件抢在事务持有的 wrapper 之前。
|
||||
|
||||
启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../../../../apps/cli/src/process-shutdown.ts)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 会串行化防抖写入,只对瞬时访问或忙碌故障执行有界退避重试,并确保每个由计时器触发的拒绝都得到观察。持久化最终失败后,该故障会保留在队列中,并重新抛给拆卸责任方;成功拆卸则会排空最新写入。
|
||||
启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../../../../apps/cli/src/process-shutdown.ts)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 通过 journal 记录运行时变更,并以原子 rename 写入,只对瞬时访问或忙碌故障执行有界退避重试。仍有未保存变更时,`flush()` 和 `stop()` 会拒绝;之后一次成功的 flush 会清除先前的错误。
|
||||
|
||||
Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.153.4 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。
|
||||
|
||||
|
||||
+4
-4
@@ -9,11 +9,11 @@
|
||||
en: d4ab65a39cf4fd7c
|
||||
zh: 62144be9b4fd7956
|
||||
/agent-note-keep-loader-mutations-non-transactional/decision:
|
||||
en: 9b661159de63d421
|
||||
zh: 15697f468a90edb9
|
||||
en: 83268169e9436127
|
||||
zh: ce740df03e9fe24a
|
||||
/agent-note-keep-loader-mutations-non-transactional/alternatives-considered:
|
||||
en: 72b871064c7ae315
|
||||
zh: a09e5168f8124c4c
|
||||
en: 645db43a48b9cfc1
|
||||
zh: d03fabd1e82dadbd
|
||||
/agent-note-keep-loader-mutations-non-transactional/consequences:
|
||||
en: 100f687b9d498308
|
||||
zh: 1c4bc2d989e43ec7
|
||||
|
||||
@@ -12,19 +12,19 @@ Transactional config reload preserves an old plugin generation after a failed ed
|
||||
|
||||
Revert the five commits in #932, resolving package moves and retaining independent later behavior. The reported merge commit belongs to the larger #936 dependency chain; reverting its first-parent diff would remove unrelated repository-plugin support. The [vendor ledger](../../../../vendor/README.md#local-modifications) records every retained source change against the unchanged pins.
|
||||
|
||||
Loader changes entry options eagerly. EntryGroup starts siblings concurrently and logs application failures; EntryTree waits for outstanding work without rejecting failed fibers. Neither restores a previous plugin or configuration. Include retains parse validation and patch reapplication, but plugin failures can leave a partially applied tree.
|
||||
Loader changes entry options eagerly. EntryGroup starts siblings concurrently and logs application failures; EntryTree waits for outstanding work without rejecting failed fibers. Neither restores a previous plugin or configuration. Include keeps the running tree after an invalid file, but plugin failures can leave a partially applied tree.
|
||||
|
||||
Application consumers own their completion checks. The CLI waits for its fallback HMR service before installing live patch watchers. The directory chooser checks the entries it mounts. The chooser and browser package runner capture the first fiber-disposal result before removing the entry, then await it before reporting teardown complete. Preset mounting waits for its subtree and reports import, activation, and missing-service failures. [App boot](../../../../packages/boot/app-boot/README.md) owns exact patch-file watching, activation audits, and partial-context cleanup. Web startup audits activation before printing a URL or opening the browser. These adaptations preserve existing consumer behavior after the reverse patch.
|
||||
|
||||
Fiber, Entry, and isolate keep their upstream update return behavior. App boot observes discarded restart promises through the existing `internal/update` waterfall and waits for fibers before auditing a patch reload. The detached import-completion observer handles both fiber outcomes; the fiber still retains its failure for an explicit audit. Durable Include writes drain before and after child removal so a later teardown write cannot erase an earlier terminal write failure.
|
||||
Fiber, Entry, and isolate keep their upstream update return behavior. App boot observes discarded restart promises through the existing `internal/update` waterfall and waits for fibers before auditing a patch reload. The detached import-completion observer handles both fiber outcomes; the fiber still retains its failure for an explicit audit. Include persistence follows the journal adaptation in the vendor ledger: `flush()` and `stop()` reject while changes remain unsaved.
|
||||
|
||||
Two #932-specific vendor changes remain: awaited initial-file creation and forced rereading in Include, and Schemastery conditional exports. Restoring the pre-#932 debounced write/read sequence reproduces `ENOENT` in the missing-file initialization test. Keeping these two lines preserves the existing `initial` option without an application-side file writer or a second YAML serializer. Removing Schemastery exports reproduces `ERR_REQUIRE_ESM_RACE_CONDITION` while the Web preset suite boots: Node falls back to the CJS entry during concurrent ESM imports. The HMR injection decorators, conditional patch cloning, and update return values use the pre-#932 behavior. Explicit `workspace:` dependencies make the #932 workspace-link switch and dedicated lockfile check unnecessary.
|
||||
One #932-specific vendor change remains: Schemastery conditional exports. Include 1.1.1 writes the `initial` file before reading it, so missing-file initialization no longer needs a local change. Removing Schemastery exports reproduces `ERR_REQUIRE_ESM_RACE_CONDITION` while the Web preset suite boots: Node falls back to the CJS entry during concurrent ESM imports. The HMR injection decorators and update return values use the pre-#932 behavior; patch cloning follows upstream Include. Explicit `workspace:` dependencies make the #932 workspace-link switch and dedicated lockfile check unnecessary.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep transactional Loader updates.** They provide automatic recovery from a rejected plugin candidate, but retain the vendored lifecycle machinery being removed. Parse failures can be contained without plugin rollback.
|
||||
|
||||
**Restore every vendored file verbatim.** This would also remove lazy injected config evaluation, conditional disabled entries, lifecycle disposal fixes, durable writes, and module-loader compatibility. Those changes have independent consumers and remain recorded in the vendor ledger.
|
||||
**Restore every vendored file verbatim.** This would also remove lazy injected config evaluation, conditional disabled entries, lifecycle disposal fixes, and module-loader compatibility. Those changes have independent consumers and remain recorded in the vendor ledger.
|
||||
|
||||
**Move generic rollback into app boot.** This would retain the same candidate-generation and restoration obligations under another owner. Applications instead report failures and allow a later valid edit to recover.
|
||||
|
||||
|
||||
@@ -12,19 +12,19 @@ Status: implemented
|
||||
|
||||
撤销 #932 中的五个提交,解决包移动冲突并保留后续独立行为。记录的合并提交属于更大的 #936 依赖链;撤销其第一父提交差异还会删除无关的仓库插件支持。[Vendor 修改记录](../../../../vendor/README.md#local-modifications) 按不变的固定来源记录每项保留的源码更改。
|
||||
|
||||
Loader 立即更改条目选项。EntryGroup 并发启动同级条目并记录应用失败;EntryTree 等待未完成的工作,但不因失败的 fiber 而拒绝。两者均不恢复旧插件或配置。Include 保留解析校验和 patch 重应用,但插件失败可能留下部分应用的配置树。
|
||||
Loader 立即更改条目选项。EntryGroup 并发启动同级条目并记录应用失败;EntryTree 等待未完成的工作,但不因失败的 fiber 而拒绝。两者均不恢复旧插件或配置。Include 在文件无效时保留正在运行的配置树,但插件失败可能留下部分应用的配置树。
|
||||
|
||||
应用消费者负责完成检查。CLI 在安装实时 patch 监视器前等待其回退 HMR 服务。目录选择器检查其挂载的条目。选择器和浏览器包运行器在移除条目前取得首次 fiber 释放的结果,并等待该结果后才报告拆卸完成。预设挂载等待其子树,并报告导入、激活和缺少服务的失败。[应用启动](../../../../packages/boot/app-boot/README.zh.md) 负责精确 patch 文件监视、激活检查和部分上下文清理。Web 启动在打印 URL 或打开浏览器前检查激活状态。这些适配维持应用反向补丁后已有的消费者行为。
|
||||
|
||||
Fiber、Entry 和 isolate 保持上游的更新返回行为。App boot 通过现有的 `internal/update` waterfall 观察被丢弃的重启 promise,并在检查 patch 重载前等待 fiber。游离的导入完成观察器处理 fiber 的两种结果;fiber 仍保留失败信息供显式检查。Include 的持久写入在删除子条目前后均排空,防止后续拆卸写入掩盖更早的终止性写入失败。
|
||||
Fiber、Entry 和 isolate 保持上游的更新返回行为。App boot 通过现有的 `internal/update` waterfall 观察被丢弃的重启 promise,并在检查 patch 重载前等待 fiber。游离的导入完成观察器处理 fiber 的两种结果;fiber 仍保留失败信息供显式检查。Include 的持久化遵循 vendor 修改记录中的 journal 适配:仍有未保存变更时,`flush()` 和 `stop()` 会拒绝。
|
||||
|
||||
保留两项 #932 专属 vendor 改动:Include 中等待初始文件创建并强制重新读取,以及 Schemastery 条件导出。恢复 #932 前的防抖写入和读取顺序,会在缺失文件初始化测试中复现 `ENOENT`。保留这两行可以维持已有 `initial` 选项,而无需在应用侧增加文件写入器或另一份 YAML 序列化逻辑。移除 Schemastery exports 后,Web preset 测试在启动时复现 `ERR_REQUIRE_ESM_RACE_CONDITION`:并发 ESM 导入使 Node 回退到 CJS 入口。HMR 注入装饰器、条件 patch 克隆及更新返回值采用 #932 前的行为。显式 `workspace:` 依赖使 #932 的 workspace 链接开关与专用锁文件检查不再必要。
|
||||
保留一项 #932 专属 vendor 改动:Schemastery 条件导出。Include 1.1.1 会先写入 `initial` 文件再读取,因此缺失文件的初始化不再需要本地改动。移除 Schemastery exports 后,Web preset 测试在启动时复现 `ERR_REQUIRE_ESM_RACE_CONDITION`:并发 ESM 导入使 Node 回退到 CJS 入口。HMR 注入装饰器及更新返回值采用 #932 前的行为;patch 克隆遵循上游 Include。显式 `workspace:` 依赖使 #932 的 workspace 链接开关与专用锁文件检查不再必要。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留事务化 Loader 更新。** 它们能从被拒绝的插件候选自动恢复,但会保留本次删除的 vendor 生命周期机制。解析失败可以独立于插件回滚进行处理。
|
||||
|
||||
**逐字恢复所有 vendor 文件。** 这还会删除延迟注入配置求值、条件禁用条目、生命周期释放修复、持久写入和模块加载器兼容性。这些更改具有独立消费者,并继续记录在 vendor 修改记录中。
|
||||
**逐字恢复所有 vendor 文件。** 这还会删除延迟注入配置求值、条件禁用条目、生命周期释放修复和模块加载器兼容性。这些更改具有独立消费者,并继续记录在 vendor 修改记录中。
|
||||
|
||||
**将通用回滚移入应用启动。** 这会在另一归属下保留相同的候选代次与恢复义务。应用改为报告失败,并允许后续有效编辑恢复。
|
||||
|
||||
|
||||
@@ -40,3 +40,8 @@
|
||||
- id: session-log-deepseek
|
||||
config:
|
||||
enabled: false
|
||||
|
||||
# The recorded tool catalog includes the optional patch tool.
|
||||
- id: tool-fs
|
||||
config:
|
||||
applyPatch: true
|
||||
|
||||
@@ -57,6 +57,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
{ label: 'reports max-token turns with mapping enabled through env', envValue: 'true', editorEnabled: false },
|
||||
{ label: 'reports max-token turns with mapping disabled through env', envValue: 'false', editorEnabled: false },
|
||||
{ label: 'allows an explicit patch to enable str_replace_editor', envValue: undefined, editorEnabled: true },
|
||||
{ label: 'allows an explicit patch to enable apply_patch', envValue: undefined, editorEnabled: false, patchEnabled: true },
|
||||
{ label: 'uses the PTC executor overlay despite the retired environment selector', envValue: undefined, editorEnabled: false, ptc: true },
|
||||
// The shipped sdk profile mounts no observation policy: an edit of a file the model never read lands on disk.
|
||||
{ label: 'edits a file the model never read through the shipped profile', envValue: undefined, editorEnabled: false, unreadEdit: true },
|
||||
@@ -64,11 +65,14 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
const { envValue, editorEnabled } = scenario
|
||||
const ptc = 'ptc' in scenario && scenario.ptc
|
||||
const unreadEdit = 'unreadEdit' in scenario && scenario.unreadEdit
|
||||
const patchEnabled = 'patchEnabled' in scenario && scenario.patchEnabled
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-smoke-'))
|
||||
const unreadFile = join(root, 'notes.txt')
|
||||
if (unreadEdit) await writeFile(unreadFile, 'alpha\n')
|
||||
const editorPatch = join(root, 'editor.patch.yml')
|
||||
const fsPatch = join(root, 'fs.patch.yml')
|
||||
if (patchEnabled) await writeFile(fsPatch, '- {id: tool-fs, config: {applyPatch: true}}\n')
|
||||
if (editorEnabled) await writeFile(editorPatch, [
|
||||
'- insert:',
|
||||
' - id: tool-str-replace-editor',
|
||||
@@ -103,6 +107,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
'--profile',
|
||||
'sdk',
|
||||
...(editorEnabled ? ['--patch', editorPatch] : []),
|
||||
...(patchEnabled ? ['--patch', fsPatch] : []),
|
||||
...(ptc ? ['--patch', join(repoRoot, 'apps/cli/config/examples/ptc.yml')] : []),
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
@@ -189,16 +194,16 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
const toolNames = tools.map(tool => tool.name)
|
||||
expect(modelRequests[0]?.output_config).toEqual({ effort: 'max' })
|
||||
expect(modelRequests[0]?.max_tokens).toBe(1234)
|
||||
const expectedTools = ['read', 'write', 'edit', 'apply_patch', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
|
||||
const expectedTools = ['read', 'write', 'edit', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
|
||||
'present', 'ask_user_question', 'schedule_create', 'schedule_edit', 'schedule_list', 'schedule_view', 'plugin_manager']
|
||||
const requestText = JSON.stringify(modelRequests[0])
|
||||
if (ptc) {
|
||||
expect(toolNames).toEqual(['run_code'])
|
||||
// The PTC overlay keeps the pre-apply_patch tool set, as the Web ptc preset does.
|
||||
for (const tool of expectedTools.filter(tool => tool !== 'apply_patch')) expect(requestText.includes(`${tool}:`), tool).toBe(true)
|
||||
for (const tool of expectedTools) expect(requestText.includes(`${tool}:`), tool).toBe(true)
|
||||
expect(requestText.includes('apply_patch:')).toBe(false)
|
||||
} else {
|
||||
expect(toolNames).toEqual(expect.arrayContaining(expectedTools))
|
||||
expect(toolNames.includes('apply_patch')).toBe(patchEnabled)
|
||||
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
|
||||
expect(toolNames).not.toContain('list_subagent_models')
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ describe('the shipped Web composition', () => {
|
||||
// excluded for the reason the TUI composition e2e excludes them — they
|
||||
// depend on ripgrep being present on the machine.
|
||||
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
|
||||
'apply_patch', 'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_subagent_models', 'plugin_manager', 'present', 'read', 'read_image',
|
||||
'schedule_create', 'schedule_edit', 'schedule_list', 'schedule_view',
|
||||
'send_message', 'skill',
|
||||
|
||||
@@ -59,3 +59,37 @@ it.each(languages)('selects %s from the browser, localizes metadata and persists
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
const forgePreviews = [
|
||||
['de', 'de-DE', 'Plugins', 'Eine Unterhaltung mit verknüpftem GitHub-Pull-Request, Prüfungen, Reviews, Codeunterschieden und einem separaten Arbeitsbaum'],
|
||||
['es', 'es-MX', 'Complementos', 'Una conversación vinculada a un pull request de GitHub, con comprobaciones, revisiones, diferencias de código y un árbol de trabajo aislado'],
|
||||
['fr', 'fr-CA', 'Plugins', 'Une conversation liée à une pull request GitHub, avec vérifications, revues, différences de code et un arbre de travail isolé'],
|
||||
['ja', 'ja-JP', 'プラグイン', 'GitHub のプルリクエストに関連付けられた会話、チェックとレビューの状況、コードの差分、独立したワークツリー'],
|
||||
['pt', 'pt-BR', 'Plug-ins', 'Uma conversa vinculada a um pull request do GitHub, com verificações, revisões, diferenças de código e uma árvore de trabalho isolada'],
|
||||
['ru', 'ru-RU', 'Плагины', 'Беседа, связанная с pull request GitHub: проверки, рецензии, различия в коде и отдельное рабочее дерево'],
|
||||
] as const
|
||||
|
||||
it.each(forgePreviews)('reads the Forge preview in %s before activation', async (id, tag, plugins, alt) => {
|
||||
const resources: { scaffold?: WebScaffold; browser?: Browser } = {}
|
||||
onTestFinished(async () => {
|
||||
try { await resources.browser?.close() } finally { await resources.scaffold?.close() }
|
||||
})
|
||||
const scaffold = await launchWebScaffold()
|
||||
resources.scaffold = scaffold
|
||||
const browser = await chromium.launch()
|
||||
resources.browser = browser
|
||||
const page = await browser.newPage({ locale: tag, viewport: { width: 1440, height: 960 } })
|
||||
const tripwire = watchConsole(page)
|
||||
onTestFailed(() => saveFailureShot(page, `forge-preview-${id}`))
|
||||
await page.goto(scaffold.authenticatedUrl)
|
||||
await expect.poll(() => page.locator('html').getAttribute('lang')).toBe(id)
|
||||
await page.getByRole('button', { name: plugins, exact: true }).click()
|
||||
const card = page.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-forge-bundle"]')
|
||||
expect(await card.getByRole('switch').getAttribute('aria-checked')).toBe('false')
|
||||
await card.getByRole('button').click()
|
||||
const image = page.locator('[data-detail-document]').getByRole('img', { name: alt, exact: true })
|
||||
await image.waitFor()
|
||||
await image.evaluate(async (node: HTMLImageElement) => { await node.decode() })
|
||||
expect(await image.evaluate((node: HTMLImageElement) => [node.naturalWidth, node.naturalHeight])).toEqual([640, 300])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
- switch "启用 自动授权审查"
|
||||
- listitem:
|
||||
- button "查看 变更请求与工作树": 变更请求与工作树
|
||||
- text: 实验性 跟踪每个会话的变更请求及其检查与评审,新会话也可以在 Git 工作树中开始;GitHub 通过 gh 命令行访问。
|
||||
- text: 实验性 在会话侧栏集中跟踪关联的 GitHub PR:查看检查状态、评审意见与代码差异,并把修改建议交给智能体继续处理。也可查看本地改动,用独立工作树隔离任务。GitHub 功能需要本机安装 gh 并登录。
|
||||
- status
|
||||
- switch "启用 变更请求与工作树"
|
||||
- heading "已安装插件" [level=3]
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
- button "返回插件": 插件
|
||||
- status
|
||||
- switch "启用 变更请求与工作树"
|
||||
- heading "变更请求与工作树" [level=3]
|
||||
- text: v{{version}} 实验性
|
||||
- paragraph:
|
||||
- code: "@deepseek-ai/dsh-experimental-forge-bundle"
|
||||
- paragraph: 在会话侧栏集中跟踪关联的 GitHub PR:查看检查状态、评审意见与代码差异,并把修改建议交给智能体继续处理。也可查看本地改动,用独立工作树隔离任务。GitHub 功能需要本机安装 gh 并登录。
|
||||
- region "详细介绍":
|
||||
- paragraph:
|
||||
- img "会话关联 GitHub PR,跟踪检查、评审与代码差异,并用工作树隔离任务改动"
|
||||
- heading "包含的组件" [level=4]
|
||||
- text: 共 7 个 · 7 已停用
|
||||
- list:
|
||||
- listitem:
|
||||
- text: 工作树服务 创建命名的 Git 工作树,并切换当前会话的工作目录。
|
||||
- code: worktree
|
||||
- code: "@deepseek-ai/dsh-experimental-worktree"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: Git 工作树 要求在新分支上工作时,智能体会创建单独的 Git 工作树并在其中继续,你当前的工作树保持原样。
|
||||
- code: tool-worktree
|
||||
- code: "@deepseek-ai/dsh-experimental-tool-worktree"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 变更请求 通过一个与提供方无关的服务注册代码托管提供方,并轮询变更请求状态。
|
||||
- code: forge
|
||||
- code: "@deepseek-ai/dsh-experimental-forge"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: GitHub 变更请求 通过宿主上的 GitHub CLI 为 forge 注册表读取 GitHub 变更请求。
|
||||
- code: forge-github
|
||||
- code: "@deepseek-ai/dsh-experimental-forge-github"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 会话变更请求 把变更请求绑定到创建、推送或跟踪它们的会话,并监听其状态。
|
||||
- code: forge-session
|
||||
- code: "@deepseek-ai/dsh-experimental-forge-session"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 工作树服务 创建命名的 Git 工作树,并切换当前会话的工作目录。
|
||||
- code: forge-worktree
|
||||
- code: "@deepseek-ai/dsh-experimental-worktree"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 变更请求面板 在侧栏标签页中显示会话的变更请求,并让新会话可以在工作树中开始。
|
||||
- code: forge-panel
|
||||
- code: "@deepseek-ai/dsh-experimental-client-ui-forge"
|
||||
- text: 已关闭
|
||||
- heading "来源信息" [level=4]
|
||||
- term: 代码来源
|
||||
- definition: 内置
|
||||
- term: 当前版本
|
||||
- definition: {{version}}
|
||||
@@ -39,7 +39,7 @@
|
||||
- switch "启用 自动授权审查"
|
||||
- listitem:
|
||||
- button "查看 变更请求与工作树": 变更请求与工作树
|
||||
- text: 实验性 跟踪每个会话的变更请求及其检查与评审,新会话也可以在 Git 工作树中开始;GitHub 通过 gh 命令行访问。
|
||||
- text: 实验性 在会话侧栏集中跟踪关联的 GitHub PR:查看检查状态、评审意见与代码差异,并把修改建议交给智能体继续处理。也可查看本地改动,用独立工作树隔离任务。GitHub 功能需要本机安装 gh 并登录。
|
||||
- status
|
||||
- switch "启用 变更请求与工作树"
|
||||
- heading "已安装插件" [level=3]
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
- switch "启用 自动授权审查"
|
||||
- listitem:
|
||||
- button "查看 变更请求与工作树": 变更请求与工作树
|
||||
- text: 实验性 跟踪每个会话的变更请求及其检查与评审,新会话也可以在 Git 工作树中开始;GitHub 通过 gh 命令行访问。
|
||||
- text: 实验性 在会话侧栏集中跟踪关联的 GitHub PR:查看检查状态、评审意见与代码差异,并把修改建议交给智能体继续处理。也可查看本地改动,用独立工作树隔离任务。GitHub 功能需要本机安装 gh 并登录。
|
||||
- status
|
||||
- switch "启用 变更请求与工作树"
|
||||
- heading "已安装插件" [level=3]
|
||||
|
||||
@@ -447,6 +447,8 @@ it.skipIf(MODE === 'record')('automatically shows a numbered PR when gh JSON omi
|
||||
const { settled } = await sendPrompt(page, scaffold)
|
||||
const sessionId = await settled
|
||||
const events = await readPersistedEvents(scaffold, sessionId)
|
||||
const request = events.find(event => event.type === 'request/header')
|
||||
expect(request?.data.header.tools?.filter(tool => tool.name === 'create_worktree')).toHaveLength(1)
|
||||
const toolOutput = events.flatMap(event => event.type === 'tool/result'
|
||||
? event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []) : []).join('\n')
|
||||
expect(toolOutput).toContain('Add the pull request panel')
|
||||
|
||||
@@ -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 { HIDDEN_BUNDLES } from '@deepseek-ai/dsh-official-bundles'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
@@ -115,17 +116,14 @@ describe('web e2e: plugin configuration pages', () => {
|
||||
// The More list omits the shipped bundles whose own rows do not run (the visibility rule lives in
|
||||
// ui-plugin-manager), so it holds fewer cards than the installed optional-bundle list.
|
||||
const bundles = await scaffold.ctx.pluginManager.listBundles()
|
||||
const hiddenBundles = new Set([
|
||||
'@deepseek-ai/dsh-experimental-terminal-bundle',
|
||||
'@deepseek-ai/dsh-experimental-liveshare',
|
||||
])
|
||||
expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package]').count())
|
||||
.toBe(bundles.filter(bundle => !hiddenBundles.has(bundle.name)
|
||||
.toBe(bundles.filter(bundle => !HIDDEN_BUNDLES.has(bundle.name)
|
||||
&& bundle.name !== '@deepseek-ai/dsh-experimental-liveshare'
|
||||
&& bundle.official && (bundle.listedByDefault !== false || bundle.enabled || bundle.installed)).length)
|
||||
expect(await basic.count()).toBe(0)
|
||||
expect(await extensions.count()).toBe(0)
|
||||
expect(await panel.locator('[data-plugin-group="bundles"]').count()).toBe(0)
|
||||
await panel.getByRole('button', { name: '查看 Git 工作树', exact: true }).click()
|
||||
await panel.getByRole('button', { name: '查看 开发者工具', exact: true }).click()
|
||||
await panel.locator('[data-plugin-detail]').waitFor()
|
||||
await panel.getByRole('button', { name: '返回 实验性插件', exact: true }).click()
|
||||
await panel.getByRole('heading', { name: '实验性插件', exact: true, level: 1 }).waitFor()
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { FiberState } from '@deepseek-ai/cordis'
|
||||
import { readProfileManifest } from '@deepseek-ai/dsh-app-boot'
|
||||
import { HIDDEN_BUNDLES } from '@deepseek-ai/dsh-official-bundles'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished, vi } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
@@ -852,7 +853,7 @@ describe('web e2e: plugin manager', () => {
|
||||
// the eligible installed optional-bundle list.
|
||||
expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package="@deepseek-ai/dsh-experimental-liveshare"]').count()).toBe(0)
|
||||
expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package]').count())
|
||||
.toBe(bundles.filter(bundle => bundle.name !== '@deepseek-ai/dsh-experimental-terminal-bundle'
|
||||
.toBe(bundles.filter(bundle => !HIDDEN_BUNDLES.has(bundle.name)
|
||||
&& bundle.name !== '@deepseek-ai/dsh-experimental-liveshare'
|
||||
&& bundle.official && (bundle.listedByDefault !== false || bundle.enabled || bundle.installed)).length)
|
||||
// Computer Use ships with the installation and is listed only on the macOS Hosts it supports.
|
||||
@@ -878,7 +879,6 @@ describe('web e2e: plugin manager', () => {
|
||||
['@deepseek-ai/dsh-personalized-recommendations', 1],
|
||||
['@deepseek-ai/dsh-experimental-inspector-profile', 1],
|
||||
...cuaSupported ? [['@deepseek-ai/dsh-cua-bundle', 1]] : [],
|
||||
['@deepseek-ai/dsh-experimental-tool-worktree', 1],
|
||||
['@deepseek-ai/dsh-experimental-cot-translation-bundle', 1],
|
||||
['@deepseek-ai/dsh-experimental-session-titles-bundle', 1],
|
||||
['@deepseek-ai/dsh-experimental-badge-skill-bundle', 1],
|
||||
@@ -1065,6 +1065,51 @@ describe('web e2e: plugin manager', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it('introduces code review and worktrees with localized artwork before activation', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-forge-preview'))
|
||||
const panel = await openPluginsPanel()
|
||||
await panel.getByRole('button', { name: '查看 变更请求与工作树', exact: true }).click()
|
||||
const detail = panel.locator('[data-plugin-detail]')
|
||||
expect(await detail.getByRole('switch').getAttribute('aria-checked')).toBe('false')
|
||||
const sources: (string | null)[] = []
|
||||
let english = false
|
||||
try {
|
||||
for (const [language, alt] of [
|
||||
['zh', '会话关联 GitHub PR,跟踪检查、评审与代码差异,并用工作树隔离任务改动'],
|
||||
['en', 'A conversation linked to a GitHub pull request, with checks, reviews, code diffs, and an isolated worktree'],
|
||||
] as const) {
|
||||
if (language === 'en') {
|
||||
await setLanguage(language)
|
||||
english = true
|
||||
}
|
||||
const document = detail.locator('[data-detail-document]')
|
||||
const image = document.getByRole('img', { name: alt, exact: true })
|
||||
await image.waitFor()
|
||||
await image.evaluate(async (node: HTMLImageElement) => { await node.decode() })
|
||||
expect(await image.evaluate((node: HTMLImageElement) => [node.naturalWidth, node.naturalHeight])).toEqual([640, 300])
|
||||
sources.push(await image.getAttribute('src'))
|
||||
for (const width of [1680, 390]) {
|
||||
await page.setViewportSize({ width, height: 1000 })
|
||||
expect(await image.evaluate((node: HTMLImageElement) => {
|
||||
const bounds = node.getBoundingClientRect()
|
||||
return bounds.width > 0 && bounds.left >= 0 && bounds.right <= window.innerWidth
|
||||
&& window.document.documentElement.scrollWidth <= window.innerWidth
|
||||
})).toBe(true)
|
||||
}
|
||||
await page.setViewportSize({ width: 1680, height: 1000 })
|
||||
}
|
||||
expect(sources[0]).not.toBe(sources[1])
|
||||
} finally {
|
||||
await page.setViewportSize({ width: 1680, height: 1000 })
|
||||
if (english) await setLanguage('zh')
|
||||
}
|
||||
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'forge-preview.expected.md'),
|
||||
await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd, {
|
||||
replacements: [[badgeVersion, '{{version}}']],
|
||||
}), MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it('localizes independent exports and falls back per field without activating the plugins', async () => {
|
||||
onTestFinished(closeSettings)
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-exports'))
|
||||
@@ -1494,7 +1539,7 @@ describe('web e2e: plugin manager', () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, [
|
||||
'manager.expected.md', 'live-enabled.expected.md', 'missing-bundle.expected.md', 'exports.expected.md', 'exports-en.expected.md', 'icons.expected.md', 'loading.expected.md', 'refresh.expected.md',
|
||||
'ime-enter.expected.md', 'header-responsive.expected.md', 'header-natural-motion.expected.md', 'preview.expected.md',
|
||||
'touch-tooltips.expected.md',
|
||||
'touch-tooltips.expected.md', 'forge-preview.expected.md',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
type NormalizeContext,
|
||||
} from '@deepseek-ai/dsh-session-snapshot'
|
||||
import type { Profile, ProfileContext } from '@deepseek-ai/dsh-app-boot'
|
||||
import type { PresetDefinition } from '@deepseek-ai/dsh-agent-preset-registry'
|
||||
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type {
|
||||
@@ -713,6 +714,20 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
{ id: 'llm-deepseek', disabled: mode !== 'record' && !maskDeepSeekCredential },
|
||||
]
|
||||
|
||||
// Recorded standard sessions pin the optional patch tool in their request headers.
|
||||
if (options.replayFixture !== undefined || options.compareReplaySession === true) {
|
||||
const config = composedRows.find(row => row.id === 'preset-standard')!.config as PresetDefinition
|
||||
overlayPatches.push({
|
||||
id: 'preset-standard',
|
||||
config: {
|
||||
...config,
|
||||
plugins: config.plugins.map(plugin => plugin.id === 'tool-fs'
|
||||
? { ...plugin, config: { ...plugin.config, applyPatch: true } }
|
||||
: plugin),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Live fields use a shared deployment layer; process-specific ports and roots stay in CLI overlays.
|
||||
const formEntries = new Set(['agent-default-model', 'agent-preset-registry', 'llm-deepseek', 'llm-pi-ai',
|
||||
'web', 'web-search-deepseek', 'web-deepseek-api', 'agent-loop', 'subagent', 'bash-sandbox', 'pwsh-sandbox',
|
||||
|
||||
@@ -1,7 +1,51 @@
|
||||
/** Manual task creation and editing through the shipped Web composition. */
|
||||
/** Schedule creation, prompt prefill, and editing through the shipped Web composition. */
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it } from 'vitest'
|
||||
import { launchWebScaffold, watchConsole } from './scaffold.ts'
|
||||
import { writeComposerDraft } from './support.ts'
|
||||
|
||||
it.each([
|
||||
{ locale: 'en', panel: 'Automation tasks', create: 'New', menu: 'Choose a creation method', chat: 'Create in a session',
|
||||
prompt: 'Please create an automation task based on the requirements.' },
|
||||
{ locale: 'zh', panel: '自动化任务', create: '新建', menu: '选择创建方式', chat: '在会话中创建',
|
||||
prompt: '请根据需求创建一个自动化任务。' },
|
||||
])('prefills an unsent Schedule creation draft in $locale through both conversation entries', async (copy) => {
|
||||
const scaffold = await launchWebScaffold({ firstUse: true })
|
||||
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined
|
||||
try {
|
||||
browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: copy.locale })
|
||||
await page.addInitScript((locale) => { localStorage.setItem('dsh.locale', locale) }, copy.locale)
|
||||
const tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
const input = page.locator('[data-composer-input][contenteditable="true"]').first()
|
||||
for (const { entry, draft } of [
|
||||
{ entry: 'primary', draft: '' },
|
||||
{ entry: 'primary', draft: 'Existing unsent draft' },
|
||||
{ entry: 'menu', draft: '' },
|
||||
{ entry: 'menu', draft: 'Existing unsent draft' },
|
||||
]) {
|
||||
await writeComposerDraft(page, input, draft)
|
||||
await page.getByRole('button', { name: copy.panel, exact: true }).click()
|
||||
const manager = page.getByTestId('task-manager-page')
|
||||
if (entry === 'primary') await manager.getByRole('button', { name: copy.create, exact: true }).click()
|
||||
else {
|
||||
await manager.getByRole('button', { name: copy.menu, exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: copy.chat, exact: true }).click()
|
||||
}
|
||||
await manager.waitFor({ state: 'detached' })
|
||||
await input.getByText(copy.prompt, { exact: true }).waitFor()
|
||||
expect(await input.evaluate(element => [...element.children].map(paragraph => paragraph.textContent).join('\n')))
|
||||
.toBe(draft === '' ? copy.prompt : `${draft}\n${copy.prompt}`)
|
||||
const events = scaffold.ctx.agents.list().flatMap(agent => agent.session.snapshotEvents())
|
||||
expect(events.filter(event => event.type === 'user/message' || event.type === 'turn/start')).toEqual([])
|
||||
expect(await scaffold.ctx.schedule.catalog()).toEqual([])
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
} finally {
|
||||
try { await browser?.close() } finally { await scaffold.close() }
|
||||
}
|
||||
})
|
||||
|
||||
it('creates from a paused empty state, selects the task, and saves an inline edit without starting an Agent', async () => {
|
||||
// First use registers the default Workspace, which a new task's directory starts with.
|
||||
|
||||
@@ -84,6 +84,24 @@ class RestartableServer {
|
||||
logs(): string { return this.output.replace(/([?&]token=)[A-Za-z0-9_-]+/gu, '$1<redacted>') }
|
||||
}
|
||||
|
||||
type HostRpc = <T>(endpoint: string, args: object) => Promise<T>
|
||||
|
||||
/** Authenticate against one server process and return its Host RPC caller. */
|
||||
async function hostRpc(url: string): Promise<HostRpc> {
|
||||
const authenticated = await fetch(url, { redirect: 'manual' })
|
||||
const cookie = authenticated.headers.get('set-cookie')?.split(';', 1)[0]
|
||||
if (cookie === undefined) throw new Error('Server did not issue an authentication cookie')
|
||||
return async <T>(endpoint: string, args: object): Promise<T> => {
|
||||
const response = await fetch(new URL('/api/' + endpoint, url), {
|
||||
method: 'POST', headers: { 'content-type': 'application/json', cookie },
|
||||
body: JSON.stringify({ type: 'client-request', rpcId: randomUUID(), method: endpoint, payload: { args } }),
|
||||
})
|
||||
const result = await response.json() as { result: { ok: boolean; value: T; error?: { message: string } } }
|
||||
if (!result.result.ok) throw new Error(`${endpoint}: ${String(result.result.error?.message)}`)
|
||||
return result.result.value
|
||||
}
|
||||
}
|
||||
|
||||
it.each([false, true])('keeps the same revision, Session and page across a server restart (delayed startup: %s)', async (holdStartup) => {
|
||||
const world = await mkdtemp(join(tmpdir(), 'dsh-server-restart-'))
|
||||
onTestFinished(() => rm(world, { recursive: true, force: true }))
|
||||
@@ -118,18 +136,7 @@ it.each([false, true])('keeps the same revision, Session and page across a serve
|
||||
|
||||
const url = await server.start(0)
|
||||
const port = Number(new URL(url).port)
|
||||
const authenticated = await fetch(url, { redirect: 'manual' })
|
||||
const cookie = authenticated.headers.get('set-cookie')?.split(';', 1)[0]
|
||||
if (cookie === undefined) throw new Error('Server did not issue an authentication cookie')
|
||||
const rpc = async <T>(endpoint: string, args: object): Promise<T> => {
|
||||
const response = await fetch(new URL('/api/' + endpoint, url), {
|
||||
method: 'POST', headers: { 'content-type': 'application/json', cookie },
|
||||
body: JSON.stringify({ type: 'client-request', rpcId: randomUUID(), method: endpoint, payload: { args } }),
|
||||
})
|
||||
const result = await response.json() as { result: { ok: boolean; value: T; error?: { message: string } } }
|
||||
if (!result.result.ok) throw new Error(`${endpoint}: ${String(result.result.error?.message)}`)
|
||||
return result.result.value
|
||||
}
|
||||
const rpc = await hostRpc(url)
|
||||
await rpc('workspace/create', { request: { path: workspacePath } })
|
||||
const scenarioId = randomUUID()
|
||||
|
||||
@@ -234,3 +241,30 @@ it.each([false, true])('keeps the same revision, Session and page across a serve
|
||||
expect(streamErrors).toEqual([])
|
||||
expect(navigations).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps a live blank Session blank across a graceful server restart', async () => {
|
||||
const world = await mkdtemp(join(tmpdir(), 'dsh-server-restart-'))
|
||||
onTestFinished(() => rm(world, { recursive: true, force: true }))
|
||||
// The scenario runs no model turn, so the model endpoint is never contacted.
|
||||
const server = new RestartableServer(world, 'http://127.0.0.1:9')
|
||||
onTestFinished(() => server.stop())
|
||||
const workspacePath = join(world, 'workspace')
|
||||
await mkdir(workspacePath)
|
||||
onTestFailed(async () => {
|
||||
await writeFile(join(REPO_ROOT, '.artifacts', 'server-restart-blank-' + randomUUID() + '.json'), JSON.stringify({ server: server.logs() }, null, 2))
|
||||
})
|
||||
|
||||
const before = await hostRpc(await server.start(0))
|
||||
const { workspace } = await before<{ workspace: { id: string } }>('workspace/create', { request: { path: workspacePath } })
|
||||
const { sessionId } = await before<{ sessionId: string }>('session/create', { request: { workspaceId: workspace.id } })
|
||||
const listed = async (rpc: HostRpc) => (await rpc<{
|
||||
items: { sessionId: string; blank: boolean; agentAvailable: boolean }[]
|
||||
}>('session/list', { _request: {} })).items.find(item => item.sessionId === sessionId)
|
||||
expect(await listed(before)).toMatchObject({ blank: true, agentAvailable: true })
|
||||
|
||||
// SIGTERM unloads the Web layer, including the list projection, before its live Session detaches;
|
||||
// stopping right after creation also precedes the throttled checkpoint of the Session's setup events.
|
||||
await server.stop()
|
||||
const after = await hostRpc(await server.start(0))
|
||||
expect(await listed(after)).toMatchObject({ blank: true, agentAvailable: false })
|
||||
})
|
||||
|
||||
@@ -473,7 +473,6 @@ function assertLeanChildRecord(agent: Agent): void {
|
||||
* Agent Note owns the rationale and its sources.
|
||||
*/
|
||||
const EXPECTED_TOOLS = [
|
||||
'apply_patch',
|
||||
'ask_user_question',
|
||||
'bash',
|
||||
'create_goal',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Git Worktrees opt-in through the shipped plugin manager, profile, and Loader. */
|
||||
/** Unified review and worktree opt-in through the shipped plugin manager, profile, and Loader. */
|
||||
|
||||
import { execFile } from 'node:child_process'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
@@ -6,16 +6,19 @@ import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import { FiberState } from '@deepseek-ai/cordis'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-experimental-worktree'
|
||||
import type {} from '@deepseek-ai/dsh-experimental-client-ui-forge'
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it, onTestFailed, onTestFinished } from 'vitest'
|
||||
import { launchWebScaffold, watchConsole } from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, PLUGIN_TOGGLE_SETTLE_MS, saveFailureShot } from './support.ts'
|
||||
|
||||
const BUNDLE = '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
const BUNDLE = '@deepseek-ai/dsh-experimental-forge-bundle'
|
||||
const STANDALONE = '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
const exec = promisify(execFile)
|
||||
|
||||
it('persists the Git Worktrees switch and removes its capability while retaining checkouts', async () => {
|
||||
it('persists the unified review and worktree switch and retains checkouts after disabling', async () => {
|
||||
const scaffold = await launchWebScaffold({
|
||||
extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
|
||||
})
|
||||
@@ -70,13 +73,15 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
}
|
||||
let panel = await openPlugins()
|
||||
let card = panel.locator(`[data-plugin-group="more"] [data-plugin-package="${BUNDLE}"]`)
|
||||
await card.getByRole('button', { name: 'View Git worktrees', exact: true }).waitFor()
|
||||
await card.getByRole('button', { name: 'View Change requests & worktrees', exact: true }).waitFor()
|
||||
expect(await panel.locator(`[data-plugin-package="${STANDALONE}"]`).count()).toBe(0)
|
||||
expect(await card.getByText('Experimental', { exact: true }).count()).toBe(1)
|
||||
let toggle = card.getByRole('switch', { name: 'Enable Git worktrees', exact: true })
|
||||
let toggle = card.getByRole('switch', { name: 'Enable Change requests & worktrees', exact: true })
|
||||
expect(await toggle.getAttribute('aria-checked')).toBe('false')
|
||||
expect(await selectedBundles()).not.toContain(BUNDLE)
|
||||
expect(rows()).toEqual([])
|
||||
expect(scaffold.ctx.get('worktrees')).toBeUndefined()
|
||||
expect(scaffold.ctx.get('forgePanel')).toBeUndefined()
|
||||
expect(tools()).toContain('working_directory')
|
||||
expect(tools()).not.toContain('create_worktree')
|
||||
|
||||
@@ -85,14 +90,21 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
|
||||
expect(await selectedBundles()).toContain(BUNDLE)
|
||||
expect(tools()).toContain('create_worktree')
|
||||
const created = await scaffold.ctx.worktrees.create(agent, { name: 'gui-opt-in' })
|
||||
expect(scaffold.ctx.get('forgePanel')).toBeDefined()
|
||||
const created = { path: join(repository, '.agents/worktrees/gui-opt-in'), baseCommit: (await git('rev-parse', 'HEAD')).stdout.trim() }
|
||||
const result = await scaffold.ctx.tools.execute({
|
||||
agent, signal: new AbortController().signal,
|
||||
calls: [{ type: 'tool-call', id: ToolCallId('gui-opt-in'), name: 'create_worktree', arguments: JSON.stringify({ name: 'gui-opt-in' }) }],
|
||||
})
|
||||
expect(result.results[0]?.result.isError).not.toBe(true)
|
||||
expect(result.results[0]?.result.value).toEqual({ ...created, branch: 'gui-opt-in', repositoryRoot: repository })
|
||||
expect(await readFile(join(created.path, 'retained.txt'), 'utf8')).toBe('retained checkout\n')
|
||||
expect(scaffold.ctx.workingDirectory.get(agent.session)).toBe(created.path)
|
||||
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
panel = await openPlugins()
|
||||
card = panel.locator(`[data-plugin-group="more"] [data-plugin-package="${BUNDLE}"]`)
|
||||
toggle = card.getByRole('switch', { name: 'Enable Git worktrees', exact: true })
|
||||
toggle = card.getByRole('switch', { name: 'Enable Change requests & worktrees', exact: true })
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
|
||||
expect(tools()).toContain('create_worktree')
|
||||
await toggle.click()
|
||||
@@ -100,6 +112,7 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
|
||||
expect(await selectedBundles()).not.toContain(BUNDLE)
|
||||
expect(scaffold.ctx.get('worktrees')).toBeUndefined()
|
||||
expect(scaffold.ctx.get('forgePanel')).toBeUndefined()
|
||||
expect(tools()).not.toContain('create_worktree')
|
||||
expect(tools()).toContain('working_directory')
|
||||
expect(await readFile(join(created.path, 'retained.txt'), 'utf8')).toBe('retained checkout\n')
|
||||
@@ -108,7 +121,7 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
panel = await openPlugins()
|
||||
await expect.poll(() => panel.getByRole('switch', { name: 'Enable Git worktrees', exact: true })
|
||||
await expect.poll(() => panel.getByRole('switch', { name: 'Enable Change requests & worktrees', exact: true })
|
||||
.getAttribute('aria-checked')).toBe('false')
|
||||
expect(tools()).not.toContain('create_worktree')
|
||||
expect(await selectedBundles()).not.toContain(BUNDLE)
|
||||
|
||||
@@ -23,11 +23,11 @@ Application listeners are outside this package matrix. Desktop Host subscribes t
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:700`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:680`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:707`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:686`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:693`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:701`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:681`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:708`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:687`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:694`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:700`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:680`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:707`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:686`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:693`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:701`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:681`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:708`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:687`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:694`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
|
||||
|
||||
@@ -1362,8 +1362,8 @@
|
||||
en: 4147b170f52814af
|
||||
zh: e78991ac1c41ebec
|
||||
/session-persistence-event-catalog/resolved-persistence-types/string:
|
||||
en: c178efefdf01cade
|
||||
zh: 200b1ab00ed8bfd3
|
||||
en: caa52cd38cec3aee
|
||||
zh: 3ff0f1fceaac2eb1
|
||||
/session-persistence-event-catalog/resolved-persistence-types/string~2:
|
||||
en: 0089620bf4638263
|
||||
zh: 1922bef1713094bb
|
||||
@@ -1884,8 +1884,8 @@
|
||||
en: 4e102391d685eff5
|
||||
zh: 9762243a13014f3d
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-user:
|
||||
en: 984cac7296874653
|
||||
zh: 13b4ee707a8ce305
|
||||
en: eaecebdbcd5cc4e4
|
||||
zh: f519d7ade666f48a
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-user-approval:
|
||||
en: a52d2dfbaf844127
|
||||
zh: 4c5306ca40f20873
|
||||
|
||||
@@ -5639,7 +5639,7 @@ Array of `number`.
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
Sources: [`packages/api/session-controller/src/types.ts:389`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -7759,7 +7759,7 @@ Sources: [`packages/core/tools/src/index.ts:28`](../packages/core/tools/src/inde
|
||||
|
||||
SHA-256: `a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts)
|
||||
Sources: [`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
|
||||
@@ -5641,7 +5641,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
来源:[`packages/api/session-controller/src/types.ts:389`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -7761,7 +7761,7 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
|
||||
|
||||
SHA-256: `a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts)
|
||||
来源:[`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
|
||||
Generated
+2
-2
@@ -253,7 +253,7 @@
|
||||
{"digest":"4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731","names":["packages/llm/llm/src/message.ts#ToolResultMessage"],"sources":["packages/llm/llm/src/message.ts:173"]},
|
||||
{"digest":"4b7ddaf3710b88ff9b8df66304f5f97bf03688fd19730eedc2e9535203c820ea","names":[],"sources":[]},
|
||||
{"digest":"4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db","names":[],"sources":["packages/core/session/src/types.ts:198"]},
|
||||
{"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/team/agent-team/src/types.ts#TeamId","packages/team/agent-team/src/types.ts#TeamMessageId","packages/team/agent-team/src/types.ts#TeamTaskId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:388","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/team/agent-team/src/types.ts:20","packages/team/agent-team/src/types.ts:32","packages/team/agent-team/src/types.ts:8","packages/workflow/workflow/src/types.ts:13"]},
|
||||
{"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/team/agent-team/src/types.ts#TeamId","packages/team/agent-team/src/types.ts#TeamMessageId","packages/team/agent-team/src/types.ts#TeamTaskId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:389","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/team/agent-team/src/types.ts:20","packages/team/agent-team/src/types.ts:32","packages/team/agent-team/src/types.ts:8","packages/workflow/workflow/src/types.ts:13"]},
|
||||
{"digest":"4d60dddaa65d3da8d4cd014d85ce3d2c2875664dd0433e331d8e9d395289686b","names":["packages/experimental/liveshare/src/host/viewer-principal.ts#ShareViewerPrincipal"],"sources":["packages/experimental/liveshare/src/host/viewer-principal.ts:24"]},
|
||||
{"digest":"4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159","names":[],"sources":[]},
|
||||
{"digest":"4e44c9118152f6829634bfd508e47669ab0497caffa380c94b4be91f9390d35d","names":[],"sources":[]},
|
||||
@@ -474,7 +474,7 @@
|
||||
{"digest":"a3c6f1ea38e9a61d7a9706057ba65aabe02dfe6d9dd90d4736aa62325d874531","names":[],"sources":[]},
|
||||
{"digest":"a3edd1efd1fef21b72c55e213c8a8b24196b091abde2b89db52be0b2aaae3cfa","names":[],"sources":[]},
|
||||
{"digest":"a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c","names":[],"sources":["packages/team/agent-team/src/types.ts:175"]},
|
||||
{"digest":"a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302","names":[],"sources":["packages/api/session-controller/src/types.ts:397"]},
|
||||
{"digest":"a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302","names":[],"sources":["packages/api/session-controller/src/types.ts:398"]},
|
||||
{"digest":"a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc","names":[],"sources":[]},
|
||||
{"digest":"a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb","names":["packages/experimental/hook-protocol/src/types.ts#HookDialect"],"sources":["packages/experimental/hook-protocol/src/types.ts:48"]},
|
||||
{"digest":"a6da9ad5d42ae83a6833a8288b076688e8e0801633ea36cbdd8a30a6d9f3160c","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunStartData"],"sources":["packages/workflow/tool-workflow/src/types.ts:14"]},
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
en: c8aa4d3384ea1306
|
||||
zh: 5aa030587d5b634d
|
||||
/vendored-package-rescope/name-mapping:
|
||||
en: 239fa7993825ac62
|
||||
zh: df35fa9f5ef42a05
|
||||
en: 50a68b9b3724eff8
|
||||
zh: 9facad930deeb014
|
||||
/vendored-package-rescope/what-the-rename-does-not-touch:
|
||||
en: 9bb7d09f8cccf695
|
||||
zh: 2a8f3f60ae5116f3
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ The Cordis framework and its foundation libraries are vendored under [`vendor/`]
|
||||
| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.11 | Framework core: `Context`, `Service`, `Fiber`, events |
|
||||
| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | Shared utilities the framework and Schemastery build on |
|
||||
| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | Config schemas (`Schema`) behind every plugin's `Config` |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` loading, plugin resolution, repository cache |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | Config includes and patch overlays |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.8 | `cordis.yml` loading, plugin resolution, repository cache |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.1.1 | Config includes and patch overlays |
|
||||
| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | Nested plugin groups |
|
||||
| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.4 | Disposal-aware timers on `ctx` |
|
||||
| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | Hot module replacement for plugins and config |
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ Cordis 框架及其基础库以源码形式 vendored 在 [`vendor/`](../vendor/R
|
||||
| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.11 | 框架核心:`Context`、`Service`、`Fiber`、事件 |
|
||||
| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | 框架与 Schemastery 共用的基础工具 |
|
||||
| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | 配置 schema(`Schema`),每个插件的 `Config` 都基于它 |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` 装载、插件解析、repository 缓存 |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | 配置包含与 patch 叠加 |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.8 | `cordis.yml` 装载、插件解析、repository 缓存 |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.1.1 | 配置包含与 patch 叠加 |
|
||||
| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 嵌套插件分组 |
|
||||
| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.4 | `ctx` 上随 disposal 回收的定时器 |
|
||||
| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | 插件与配置的热替换 |
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/forge.md
|
||||
/forges:
|
||||
en: 1b51f9d385dafe76
|
||||
zh: 4bb73627aa69d652
|
||||
en: 3ee521b77b4601e2
|
||||
zh: 4170c33e49248f8f
|
||||
/forges/providers-and-change-requests:
|
||||
en: 4ec7d1892b4067db
|
||||
zh: 6bb70d21ae3d7a89
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](forge.zh.md)
|
||||
|
||||
The experimental forge capability seam reads change requests, meaning pull requests and merge requests, from code hosts. [`dsh-experimental-forge`](../../packages/experimental/forge/README.md) is the Service Definition and provides `ctx.forge`; [`dsh-experimental-forge-github`](../../packages/experimental/forge-github/README.md) is the Service Provider for GitHub hosts; [`dsh-experimental-forge-session`](../../packages/experimental/forge-session/README.md) is the Consumer that binds change requests to Sessions and provides `ctx.forgeSession`; [`dsh-experimental-client-ui-forge`](../../packages/experimental/client-ui-forge/README.md) is the GUI Consumer that provides `ctx.forgePanel`. The default-disabled [forge bundle](../../packages/experimental/forge-bundle/README.md) composes all four, with an isolated [worktree service](../../packages/experimental/worktree/README.md) for the panel, from the Plugins page; otherwise they load only through explicit composition.
|
||||
The experimental forge capability seam reads change requests, meaning pull requests and merge requests, from code hosts. [`dsh-experimental-forge`](../../packages/experimental/forge/README.md) is the Service Definition and provides `ctx.forge`; [`dsh-experimental-forge-github`](../../packages/experimental/forge-github/README.md) is the Service Provider for GitHub hosts; [`dsh-experimental-forge-session`](../../packages/experimental/forge-session/README.md) is the Consumer that binds change requests to Sessions and provides `ctx.forgeSession`; [`dsh-experimental-client-ui-forge`](../../packages/experimental/client-ui-forge/README.md) is the GUI Consumer that provides `ctx.forgePanel`. The default-disabled [forge bundle](../../packages/experimental/forge-bundle/README.md) composes all four, with the [worktree model tool](../../packages/experimental/tool-worktree/README.md) and an isolated [worktree service](../../packages/experimental/worktree/README.md) for the panel, from the Plugins page; otherwise they load only through explicit composition.
|
||||
|
||||
## Providers and change requests
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](forge.md) | 中文
|
||||
|
||||
实验性的 forge 能力 seam 从代码托管平台读取变更请求(change request,即 pull request 与 merge request)。[`dsh-experimental-forge`](../../packages/experimental/forge/README.zh.md) 是 Service Definition,提供 `ctx.forge`;[`dsh-experimental-forge-github`](../../packages/experimental/forge-github/README.zh.md) 是服务 GitHub 主机的 Service Provider;[`dsh-experimental-forge-session`](../../packages/experimental/forge-session/README.zh.md) 是把变更请求绑定到 Session 的 Consumer,提供 `ctx.forgeSession`;[`dsh-experimental-client-ui-forge`](../../packages/experimental/client-ui-forge/README.zh.md) 是提供 `ctx.forgePanel` 的 GUI Consumer。默认禁用的 [forge Bundle](../../packages/experimental/forge-bundle/README.zh.md) 可从插件页面组合这四个包,并为面板附带一个隔离的 [worktree 服务](../../packages/experimental/worktree/README.zh.md);除此之外,它们只能通过显式组合加载。
|
||||
实验性的 forge 能力 seam 从代码托管平台读取变更请求(change request,即 pull request 与 merge request)。[`dsh-experimental-forge`](../../packages/experimental/forge/README.zh.md) 是 Service Definition,提供 `ctx.forge`;[`dsh-experimental-forge-github`](../../packages/experimental/forge-github/README.zh.md) 是服务 GitHub 主机的 Service Provider;[`dsh-experimental-forge-session`](../../packages/experimental/forge-session/README.zh.md) 是把变更请求绑定到 Session 的 Consumer,提供 `ctx.forgeSession`;[`dsh-experimental-client-ui-forge`](../../packages/experimental/client-ui-forge/README.zh.md) 是提供 `ctx.forgePanel` 的 GUI Consumer。默认禁用的 [forge Bundle](../../packages/experimental/forge-bundle/README.zh.md) 可从插件页面组合这四个包,同时加载[工作树模型工具](../../packages/experimental/tool-worktree/README.zh.md),并为面板附带一个隔离的 [worktree 服务](../../packages/experimental/worktree/README.zh.md);除此之外,它们只能通过显式组合加载。
|
||||
|
||||
## 提供方与变更请求
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md
|
||||
/session-projections:
|
||||
en: b2789f5257a935c5
|
||||
zh: 12d430a9cd454592
|
||||
en: 1804bc8fc5f226e4
|
||||
zh: 2f31d0f014508d29
|
||||
/session-projections/the-unit:
|
||||
en: d48814d3ab0e997f
|
||||
zh: d0337d8eba097a63
|
||||
@@ -12,11 +12,11 @@
|
||||
en: bc9d438b90fc2baa
|
||||
zh: df7cbea2dad3786b
|
||||
/session-projections/summary-reads:
|
||||
en: d2bc9bcc775acfb7
|
||||
zh: b1d2ba74496305b8
|
||||
en: fcc9d07e5c41abb7
|
||||
zh: 798004ad1c66787b
|
||||
/session-projections/summary-reads/summary-vocabulary:
|
||||
en: 6a93a4e1682fd6e6
|
||||
zh: 28729129c3d8a3b1
|
||||
/session-projections/the-registry-ctx-sessionprojections:
|
||||
en: 67fe0f49dee90e25
|
||||
zh: c23de3a2803040f0
|
||||
en: 33e4aa5276d4c0fa
|
||||
zh: 18019b4990734b82
|
||||
|
||||
@@ -6,7 +6,7 @@ The session-projection seam — a [capability seam](../capability-seams.md) thro
|
||||
|
||||
Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)
|
||||
|
||||
A `ProjectionHandle` retains its synchronous reader after unregistration. `stateOf(session)` folds newly committed events with the original unit; it does not rejoin global snapshots, checkpoints, or notifications. Registration must still finish before the handle can be used. See the [registration lifecycle](../../packages/session/session-projection/README.md).
|
||||
A `ProjectionHandle` retains its synchronous reader after unregistration. `stateOf(session)` folds newly committed events with the original unit; it does not rejoin global snapshots or notifications. Registration must still finish before the handle can be used. See the [registration lifecycle](../../packages/session/session-projection/README.md).
|
||||
|
||||
```ts type-equiv
|
||||
/** An effect-owned registration with a reader retained independently of registration. */
|
||||
@@ -14,6 +14,8 @@ interface ProjectionHandle<S> {
|
||||
/**
|
||||
* Read the current state, including after unregistering, by folding new
|
||||
* Session events with this handle's original unit. Do not mutate the live value.
|
||||
* After the final release, a read also advances the released cell that
|
||||
* `checkpoint(session)` serves, until the key registers again.
|
||||
* @param session - Session whose committed events are folded.
|
||||
* @returns the projection state at the Session's current cursor.
|
||||
*/
|
||||
@@ -156,9 +158,9 @@ type ProjectionEventChangeListener = (
|
||||
|
||||
A Session summary carries every registered client view except the keys whose `SessionProjectionMap` entry carries the `SessionProjectionSummaryExcluded<T>` mark. The mark has no runtime representation: it decides the client view type, while `wire.summary` decides the runtime key set, with omission meaning `true`. A key whose entry carries the mark declares `wire.summary: false` so the two agree; the field is a plain boolean, so the type does not enforce it.
|
||||
|
||||
`summaryKeys()` derives the key set from the live registrations and keeps returning the same instance until a registration or its disposal changes the key table. A summary consumer passes it to the existing key-taking faces — `cachedSnapshot(session, keys)` for cached cells and `viewCheckpoint(rows, keys)` for checkpoint rows, both keeping their watermark rules and both using a set without copying — so there is no summary-specific read face and no second definition of the set.
|
||||
`summaryKeys()` derives the key set from the live registrations and keeps returning the same instance until a registration or its disposal changes the key table. A summary consumer passes it to the existing key-taking faces — `snapshot(session, keys)` for live Sessions and `viewCheckpoint(rows, keys)` for checkpoint rows, both using a set without copying — so there is no summary-specific read face and no second definition of the set.
|
||||
|
||||
A list row carries that summary projection, never the full projection: it promises the summary key set and nothing more. `snapshot`, `cachedSnapshot`, and `viewCheckpoint` remain complete when called without a key subset, which is how an opened Session, its follow baseline, and the control baseline keep every projected value; the `@` reference reads only its label keys through the same key-taking faces.
|
||||
A list row carries that summary projection, never the full projection: it promises the summary key set and nothing more. `snapshot(session, keys)` computes only the units it returns. Without a key subset, `snapshot` computes every client-visible unit and no Host-only unit, and `viewCheckpoint` serves every wire key, so an opened Session, its follow baseline, and the control baseline carry every projected value; the `@` reference reads only its label keys through the same key-taking faces. The registry's `cachedSnapshot(session, keys)` is deprecated: it returns only values another read already computed and may omit keys.
|
||||
|
||||
### Summary vocabulary
|
||||
|
||||
@@ -230,7 +232,7 @@ type ProjectionKeySelection =
|
||||
|
||||
## The registry: `ctx.sessionProjections`
|
||||
|
||||
`SessionProjectionRegistry` ([signatures](#ctxsessionprojections--sessionprojectionregistry)) owns the drive: one `session/event` subscription, eager `apply` over every registered unit, and per-session per-unit watermark cells. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Domains await `register(definition)` during activation and retain its handle. Registration belongs to the calling fiber: its last release removes the key from global drives and snapshots, while retained handles remain readable. Same-key, same-version registrants share one unit; incompatible versions reject registration.
|
||||
`SessionProjectionRegistry` ([signatures](#ctxsessionprojections--sessionprojectionregistry)) owns the drive: one `session/event` subscription, eager `apply` over every registered unit, and per-session per-unit watermark cells. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Domains await `register(definition)` during activation and retain its handle. Registration belongs to the calling fiber: its last release removes the key from global drives and snapshots, checkpoints keep each Session's last state of it until the key registers again, and retained handles remain readable. Same-key, same-version registrants share one unit; incompatible versions reject registration.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
@@ -331,7 +333,7 @@ Source: [`packages/session/session-projection-cache/src/index.ts`](../../package
|
||||
|
||||
### `ctx.sessionProjections` — `SessionProjectionRegistry`
|
||||
|
||||
`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Consumers declare `sessionProjections` as a required injection, await registration during activation, and retain its ready handle. Registrants sharing a key share one unit and are counted. The last release removes the unit from live drives, checkpoints, and client snapshots; retained handles still fold new events on demand using the original unit.
|
||||
`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Consumers declare `sessionProjections` as a required injection, await registration during activation, and retain its ready handle. Registrants sharing a key share one unit and are counted. The last release removes the unit from live drives, restores, and client snapshots, while checkpoints keep each Session's last cell of it until the key registers again and the new unit refolds the log; retained handles still fold new events on demand using the original unit.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -376,30 +378,22 @@ onEventChanged(listener: ProjectionEventChangeListener): () => void
|
||||
/**
|
||||
* One consistent cut over every registered client-visible unit for one session, read from
|
||||
* the watermark cache (missing cells fold lazily over the in-memory log).
|
||||
* Only the returned units are computed: other units, including Host-only
|
||||
* units without `wire`, are neither folded nor advanced by this read.
|
||||
* Fully synchronous — every value and `asOfSeq` reflect the same log
|
||||
* position. Each value passes its unit's `viewSchema` before leaving.
|
||||
* @param session - the session whose projection values are read.
|
||||
* @param keys - optional client-visible outputs; state materialization remains complete.
|
||||
* @param keys - optional client-visible outputs; only these units are computed.
|
||||
* @returns the snapshot; `values` is empty when no selected client-visible unit is registered.
|
||||
*/
|
||||
snapshot( session: Session, keys?: ProjectionKeySelection, ): ProjectionSnapshot
|
||||
|
||||
/**
|
||||
* Read only already-materialized client-visible cells without folding history.
|
||||
* Values may trail the live Session and are therefore hints, not a complete
|
||||
* baseline. Missing cells are omitted.
|
||||
* @param session - attached Session whose cached cells are inspected.
|
||||
* @param keys - optional wire keys to view.
|
||||
* @returns the lowest common cached cut, or `undefined` when no wire cell exists.
|
||||
*/
|
||||
cachedSnapshot( session: Session, keys?: ProjectionKeySelection, ): ProjectionSnapshot | undefined
|
||||
|
||||
/**
|
||||
* The keys a Session summary carries: every registered client-visible key
|
||||
* whose unit does not declare `summary: false`. The returned set is stable —
|
||||
* the same instance on every call — until a registration or its disposal
|
||||
* changes the key table, so a consumer may hold it across rows. Callers pass
|
||||
* it to a key-taking read face such as `cachedSnapshot(session, summaryKeys())`
|
||||
* it to a key-taking read face such as `snapshot(session, summaryKeys())`
|
||||
* or `viewCheckpoint(rows, summaryKeys())`; those faces use the set as-is.
|
||||
* @returns the current summary key set; do not mutate it.
|
||||
*/
|
||||
@@ -416,8 +410,13 @@ summaryKeys(): ReadonlySet<Extract<keyof SessionProjectionMap, string>>
|
||||
* mutable state, and a caller reaching the live reference could corrupt
|
||||
* every subsequent snapshot and frame through it (plain JSON by the unit
|
||||
* contract, so the clone is total).
|
||||
* A released unit contributes the Session's last cell. The registry no
|
||||
* longer advances that cell, while a retained handle read still advances it
|
||||
* forward, so a writer running after a plugin unloads keeps that unit's
|
||||
* latest state; a Session that has no cell of it gets no row. {@link restore}
|
||||
* folds only live units, so its refreshed rows omit released units.
|
||||
* @param session - the session whose unit states are checkpointed.
|
||||
* @returns one row per registered key.
|
||||
* @returns one row per registered key, plus one per released key with a cell for this Session.
|
||||
*/
|
||||
checkpoint(session: Session): ProjectionCheckpoint
|
||||
|
||||
@@ -434,7 +433,7 @@ checkpoint(session: Session): ProjectionCheckpoint
|
||||
* re-read.
|
||||
* @param checkpoint - persisted rows for one session (possibly stale or empty).
|
||||
* @returns the offset for the stored-log suffix read (`SessionHandle.read`),
|
||||
* or `undefined` when no unit is registered (no read needed —
|
||||
* or `undefined` when no live unit is registered (released predecessors do not count; no read needed —
|
||||
* {@link restore} would serve empty values regardless).
|
||||
*/
|
||||
restoreFloor(checkpoint: ProjectionCheckpoint): SessionLogOffset | undefined
|
||||
@@ -475,6 +474,8 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: ProjectionKeySelection,
|
||||
* @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
|
||||
* supplied event's seq, `baseSeq - 1` for an empty tail) plus the
|
||||
* refreshed checkpoint rows at that cut, ready for a durable write-back.
|
||||
* The rows cover live units only: a released unit has no cell for a log
|
||||
* folded without a live `Session`, so a write-back of them omits it.
|
||||
*/
|
||||
restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
源码:[`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)
|
||||
|
||||
`ProjectionHandle` 在注销后仍保留同步 reader。`stateOf(session)` 用原有单元折叠新提交的事件,不会重新加入全局快照、检查点或通知。必须等待注册完成才能使用 handle。参见[注册生命周期](../../packages/session/session-projection/README.zh.md)。
|
||||
`ProjectionHandle` 在注销后仍保留同步 reader。`stateOf(session)` 用原有单元折叠新提交的事件,不会重新加入全局快照或通知。必须等待注册完成才能使用 handle。参见[注册生命周期](../../packages/session/session-projection/README.zh.md)。
|
||||
|
||||
```ts type-equiv
|
||||
/** An effect-owned registration with a reader retained independently of registration. */
|
||||
@@ -14,6 +14,8 @@ interface ProjectionHandle<S> {
|
||||
/**
|
||||
* Read the current state, including after unregistering, by folding new
|
||||
* Session events with this handle's original unit. Do not mutate the live value.
|
||||
* After the final release, a read also advances the released cell that
|
||||
* `checkpoint(session)` serves, until the key registers again.
|
||||
* @param session - Session whose committed events are folded.
|
||||
* @returns the projection state at the Session's current cursor.
|
||||
*/
|
||||
@@ -156,9 +158,9 @@ type ProjectionEventChangeListener = (
|
||||
|
||||
Session 摘要包含每个已注册客户端视图,但排除 `SessionProjectionMap` 条目带 `SessionProjectionSummaryExcluded<T>` 标记的 key。标记没有运行时表示:它决定客户端视图类型,`wire.summary` 决定运行时键集合,省略即 `true`。map 条目带标记的 key 应声明 `wire.summary: false` 以保持一致;该字段是普通布尔值,类型层不做强制。
|
||||
|
||||
`summaryKeys()` 从实时注册派生该集合,并在注册或注销改变 key 表之前持续返回同一实例。摘要消费方把它传给既有的带 key 读取面 —— 缓存 cell 用 `cachedSnapshot(session, keys)`,checkpoint 行用 `viewCheckpoint(rows, keys)`,两者保持各自的水位规则且直接使用 Set、不做复制 —— 因此不存在摘要专用读取面,集合也只有一处定义。
|
||||
`summaryKeys()` 从实时注册派生该集合,并在注册或注销改变 key 表之前持续返回同一实例。摘要消费方把它传给既有的带 key 读取面 —— 实时 Session 用 `snapshot(session, keys)`,checkpoint 行用 `viewCheckpoint(rows, keys)`,两者都直接使用 Set、不做复制 —— 因此不存在摘要专用读取面,集合也只有一处定义。
|
||||
|
||||
列表行携带的是这个摘要投影,绝不是全量投影:它只承诺摘要键集合,不承诺更多。`snapshot`、`cachedSnapshot` 与 `viewCheckpoint` 在不传 key 子集时仍是完整的,打开的 Session、其 follow 基线与控制基线因此保留每个投影值;`@` 引用只通过同一组带 key 的读取面读取其标签 key。
|
||||
列表行携带的是这个摘要投影,绝不是全量投影:它只承诺摘要键集合,不承诺更多。`snapshot(session, keys)` 只计算它返回的单元。不传 key 子集时,`snapshot` 计算每个客户端可见单元、不计算 Host 专用单元,`viewCheckpoint` 返回每个 wire key,因此打开的 Session、其 follow 基线与控制基线携带每个投影值;`@` 引用只通过同一组带 key 的读取面读取其标签 key。注册表的 `cachedSnapshot(session, keys)` 已废弃:它只返回其他读取已经算好的值,可能缺少 key。
|
||||
|
||||
### 摘要词汇
|
||||
|
||||
@@ -230,7 +232,7 @@ type ProjectionKeySelection =
|
||||
|
||||
## 注册表:`ctx.sessionProjections`
|
||||
|
||||
`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动逻辑:一个 `session/event` 订阅、对所有已注册单元立即执行的 `apply`,以及按会话、按单元保存的水位线缓存。缓存单元惰性构建:若单元在事件产生后才注册,或会话早于注册表存在,第一次访问(事件或读取)会从 `init` 开始折叠内存日志。领域在激活期间等待 `register(definition)`,并保留返回的 handle(句柄)。注册归调用 fiber 所有:最后一次释放会从全局驱动和快照中移除该 key,已有句柄仍可读取。同 key、同版本的注册者共享一个单元;版本不兼容时注册失败。
|
||||
`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动逻辑:一个 `session/event` 订阅、对所有已注册单元立即执行的 `apply`,以及按会话、按单元保存的水位线缓存。缓存单元惰性构建:若单元在事件产生后才注册,或会话早于注册表存在,第一次访问(事件或读取)会从 `init` 开始折叠内存日志。领域在激活期间等待 `register(definition)`,并保留返回的 handle(句柄)。注册归调用 fiber 所有:最后一次释放会从全局驱动和快照中移除该 key,检查点仍保留每个会话中它的最后状态,直到该 key 再次注册,已有句柄仍可读取。同 key、同版本的注册者共享一个单元;版本不兼容时注册失败。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
@@ -331,7 +333,7 @@ Source: [`packages/session/session-projection-cache/src/index.ts`](../../package
|
||||
|
||||
### `ctx.sessionProjections` — `SessionProjectionRegistry`
|
||||
|
||||
`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Consumers declare `sessionProjections` as a required injection, await registration during activation, and retain its ready handle. Registrants sharing a key share one unit and are counted. The last release removes the unit from live drives, checkpoints, and client snapshots; retained handles still fold new events on demand using the original unit.
|
||||
`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Consumers declare `sessionProjections` as a required injection, await registration during activation, and retain its ready handle. Registrants sharing a key share one unit and are counted. The last release removes the unit from live drives, restores, and client snapshots, while checkpoints keep each Session's last cell of it until the key registers again and the new unit refolds the log; retained handles still fold new events on demand using the original unit.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -376,30 +378,22 @@ onEventChanged(listener: ProjectionEventChangeListener): () => void
|
||||
/**
|
||||
* One consistent cut over every registered client-visible unit for one session, read from
|
||||
* the watermark cache (missing cells fold lazily over the in-memory log).
|
||||
* Only the returned units are computed: other units, including Host-only
|
||||
* units without `wire`, are neither folded nor advanced by this read.
|
||||
* Fully synchronous — every value and `asOfSeq` reflect the same log
|
||||
* position. Each value passes its unit's `viewSchema` before leaving.
|
||||
* @param session - the session whose projection values are read.
|
||||
* @param keys - optional client-visible outputs; state materialization remains complete.
|
||||
* @param keys - optional client-visible outputs; only these units are computed.
|
||||
* @returns the snapshot; `values` is empty when no selected client-visible unit is registered.
|
||||
*/
|
||||
snapshot( session: Session, keys?: ProjectionKeySelection, ): ProjectionSnapshot
|
||||
|
||||
/**
|
||||
* Read only already-materialized client-visible cells without folding history.
|
||||
* Values may trail the live Session and are therefore hints, not a complete
|
||||
* baseline. Missing cells are omitted.
|
||||
* @param session - attached Session whose cached cells are inspected.
|
||||
* @param keys - optional wire keys to view.
|
||||
* @returns the lowest common cached cut, or `undefined` when no wire cell exists.
|
||||
*/
|
||||
cachedSnapshot( session: Session, keys?: ProjectionKeySelection, ): ProjectionSnapshot | undefined
|
||||
|
||||
/**
|
||||
* The keys a Session summary carries: every registered client-visible key
|
||||
* whose unit does not declare `summary: false`. The returned set is stable —
|
||||
* the same instance on every call — until a registration or its disposal
|
||||
* changes the key table, so a consumer may hold it across rows. Callers pass
|
||||
* it to a key-taking read face such as `cachedSnapshot(session, summaryKeys())`
|
||||
* it to a key-taking read face such as `snapshot(session, summaryKeys())`
|
||||
* or `viewCheckpoint(rows, summaryKeys())`; those faces use the set as-is.
|
||||
* @returns the current summary key set; do not mutate it.
|
||||
*/
|
||||
@@ -416,8 +410,13 @@ summaryKeys(): ReadonlySet<Extract<keyof SessionProjectionMap, string>>
|
||||
* mutable state, and a caller reaching the live reference could corrupt
|
||||
* every subsequent snapshot and frame through it (plain JSON by the unit
|
||||
* contract, so the clone is total).
|
||||
* A released unit contributes the Session's last cell. The registry no
|
||||
* longer advances that cell, while a retained handle read still advances it
|
||||
* forward, so a writer running after a plugin unloads keeps that unit's
|
||||
* latest state; a Session that has no cell of it gets no row. {@link restore}
|
||||
* folds only live units, so its refreshed rows omit released units.
|
||||
* @param session - the session whose unit states are checkpointed.
|
||||
* @returns one row per registered key.
|
||||
* @returns one row per registered key, plus one per released key with a cell for this Session.
|
||||
*/
|
||||
checkpoint(session: Session): ProjectionCheckpoint
|
||||
|
||||
@@ -434,7 +433,7 @@ checkpoint(session: Session): ProjectionCheckpoint
|
||||
* re-read.
|
||||
* @param checkpoint - persisted rows for one session (possibly stale or empty).
|
||||
* @returns the offset for the stored-log suffix read (`SessionHandle.read`),
|
||||
* or `undefined` when no unit is registered (no read needed —
|
||||
* or `undefined` when no live unit is registered (released predecessors do not count; no read needed —
|
||||
* {@link restore} would serve empty values regardless).
|
||||
*/
|
||||
restoreFloor(checkpoint: ProjectionCheckpoint): SessionLogOffset | undefined
|
||||
@@ -475,6 +474,8 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: ProjectionKeySelection,
|
||||
* @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
|
||||
* supplied event's seq, `baseSeq - 1` for an empty tail) plus the
|
||||
* refreshed checkpoint rows at that cut, ready for a durable write-back.
|
||||
* The rows cover live units only: a released unit has no cell for a log
|
||||
* folded without a live `Session`, so a write-back of them omits it.
|
||||
*/
|
||||
restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/worktrees.md
|
||||
/git-worktrees:
|
||||
en: 21ec4a3b825d7e81
|
||||
zh: c841c5ca4b4e8959
|
||||
en: 3a3d69d2b12a78f9
|
||||
zh: a7a7d12487968bbc
|
||||
/git-worktrees/creation-and-retention:
|
||||
en: 53f643c5d30e00b2
|
||||
zh: ba9e52efc62c7656
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](worktrees.zh.md)
|
||||
|
||||
The experimental [`dsh-experimental-worktree`](../../packages/experimental/worktree/README.md) service provides `ctx.worktrees`. Its [`create_worktree`](../../packages/experimental/tool-worktree/README.md) consumer creates a new Git branch and checkout, then selects it through the [working-directory service](working-directory.md). The [Git Worktrees optional bundle](../../packages/experimental/tool-worktree/README.md#use-this-package) loads both packages through the GUI plugin manager. It ships switched off; explicit compositions can also mount the two packages.
|
||||
The experimental [`dsh-experimental-worktree`](../../packages/experimental/worktree/README.md) service provides `ctx.worktrees`. Its [`create_worktree`](../../packages/experimental/tool-worktree/README.md) consumer creates a new Git branch and checkout, then selects it through the [working-directory service](working-directory.md). The [Change requests & worktrees bundle](../../packages/experimental/forge-bundle/README.md#use-this-package) loads both packages through the GUI plugin manager and ships switched off. Other profiles can select the [standalone worktree bundle](../../packages/experimental/tool-worktree/README.md#use-this-package) or mount the two packages explicitly.
|
||||
|
||||
## Creation and retention
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](worktrees.md) | 中文
|
||||
|
||||
实验性的 [`dsh-experimental-worktree`](../../packages/experimental/worktree/README.zh.md) 服务提供 `ctx.worktrees`。其消费方 [`create_worktree`](../../packages/experimental/tool-worktree/README.zh.md) 创建新的 Git 分支与检出目录,再通过[工作目录服务](working-directory.zh.md)选中它。[Git 工作树可选 bundle](../../packages/experimental/tool-worktree/README.zh.md#use-this-package)通过 GUI 插件管理器一起加载这两个包。它随安装提供,但默认关闭;显式组合也可以挂载这两个包。
|
||||
实验性的 [`dsh-experimental-worktree`](../../packages/experimental/worktree/README.zh.md) 服务提供 `ctx.worktrees`。其消费方 [`create_worktree`](../../packages/experimental/tool-worktree/README.zh.md) 创建新的 Git 分支与检出目录,再通过[工作目录服务](working-directory.zh.md)选中它。[变更请求与工作树组合包](../../packages/experimental/forge-bundle/README.zh.md#use-this-package)通过 GUI 插件管理器一起加载这两个包,随安装提供但默认关闭。其他 profile 可以选择[独立工作树组合包](../../packages/experimental/tool-worktree/README.zh.md#use-this-package),或显式挂载这两个包。
|
||||
|
||||
## 创建与保留
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 6cc2cd9ced70bb09
|
||||
zh: 1c0e6cb4ba2ed637
|
||||
/tool-schema-catalog/tool-package-map:
|
||||
en: 6e8419f195bc96a1
|
||||
zh: 564c4f04420d329a
|
||||
en: 17d0b1d1a3c15c7f
|
||||
zh: 1a9d90fc99e0e848
|
||||
/tool-schema-catalog/deepseek-ai-dsh-plugin-manager:
|
||||
en: e0143916bb634762
|
||||
zh: e0143916bb634762
|
||||
@@ -309,8 +309,8 @@
|
||||
en: d5f5679abe86e316
|
||||
zh: d5f5679abe86e316
|
||||
/tool-schema-catalog/deepseek-ai-dsh-experimental-tool-worktree/create-worktree:
|
||||
en: d8fe3c566ba2147c
|
||||
zh: baf372cb9242670c
|
||||
en: 41950f76db71e7f7
|
||||
zh: 654d2e5505ff3d3c
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-workflow:
|
||||
en: bb38a9d29f46563c
|
||||
zh: bb38a9d29f46563c
|
||||
|
||||
@@ -46,7 +46,7 @@ This table connects model-visible tool names to the plugin package and service s
|
||||
| `@deepseek-ai/dsh-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | Four task-board tools share the subagent tree’s root Session. Enabling Team mode on the root Session adds task-board guidance alongside standard subagent delegation and control tools. Standard subagent controls provide required identity independently of Team mode. |
|
||||
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. |
|
||||
| `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | Reads or changes the calling Session directory. Existing shells and processes retain their own directories; origin metadata and permission roots stay fixed. |
|
||||
| `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | Explicit experimental composition only. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout. |
|
||||
| `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | Enabled through the Change requests & worktrees bundle on Web and Desktop; other profiles can select the standalone bundle. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout. |
|
||||
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflowEngine`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-workspace-dependencies` | `load_workspace_dependencies` | `ctx.tools` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. |
|
||||
@@ -2521,7 +2521,7 @@ Create a new Git branch and worktree from a local commit, branch, or tag, then c
|
||||
|
||||
Source: [`packages/experimental/tool-worktree/src/index.ts`](../packages/experimental/tool-worktree/src/index.ts)
|
||||
|
||||
Explicit experimental composition only. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout.
|
||||
Enabled through the Change requests & worktrees bundle on Web and Desktop; other profiles can select the standalone bundle. Creates a new branch and checkout from a pinned local commit under existing write permissions, then changes the Session directory. Leaving retains the branch and checkout.
|
||||
|
||||
<a id="deepseek-aidsh-tool-workflow"></a>
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
| `@deepseek-ai/dsh-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | 四个任务板工具共享 subagent 树的根 Session。在根 Session 开启 Team 模式,会在标准 subagent 委派和控制工具之外增加任务板指导。标准 subagent 控制工具提供必需身份,不受 Team 模式影响。 |
|
||||
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 |
|
||||
| `@deepseek-ai/dsh-tool-working-directory` | `working_directory` | `ctx.tools`, `ctx.workingDirectory` | `tool/call`, `working-directory/change`, `user context for directory changes`, `tool/result` | - | 读取或改变调用方 Session 的目录。已有 shell 和进程保留自己的目录;原始元数据和权限根目录保持不变。 |
|
||||
| `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | 仅通过显式实验组合加载。在现有写权限下从固定的本地提交创建新分支与检出目录,再改变 Session 工作目录。离开时保留分支与检出目录。 |
|
||||
| `@deepseek-ai/dsh-experimental-tool-worktree` | `create_worktree` | `ctx.tools`, `ctx.worktrees`, `a calling Agent` | `tool/call`, `Git branch and checkout`, `working-directory/change`, `tool/result` | - | 在 Web 和 Desktop 上通过变更请求与工作树组合包启用;其他 profile 可以选择独立组合包。在现有写权限下从固定的本地提交创建新分支与检出目录,再改变 Session 工作目录。离开时保留分支与检出目录。 |
|
||||
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`、`ctx.workflowEngine`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents the script children)` | `tool/call`、`tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-workspace-dependencies` | `load_workspace_dependencies` | `ctx.tools` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-web` | `web_fetch`、`web_search` | `ctx.tools`、`ctx.web`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | web_search 和 web_fetch 将提供方选择置于 ctx.web 之后,使模型可见 schema 在更换后端时保持稳定。 |
|
||||
@@ -2527,7 +2527,7 @@ todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为
|
||||
|
||||
来源:[`packages/experimental/tool-worktree/src/index.ts`](../packages/experimental/tool-worktree/src/index.ts)
|
||||
|
||||
仅通过显式实验组合加载。在现有写权限下从固定的本地提交创建新分支与检出目录,再改变 Session 工作目录。离开时保留分支与检出目录。
|
||||
在 Web 和 Desktop 上通过变更请求与工作树组合包启用;其他 profile 可以选择独立组合包。在现有写权限下从固定的本地提交创建新分支与检出目录,再改变 Session 工作目录。离开时保留分支与检出目录。
|
||||
|
||||
<a id="deepseek-aidsh-tool-workflow"></a>
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.2-alpha.2/cordis-loader-entry-trees/guide.md
|
||||
/:
|
||||
en: 02dfa517303f3206
|
||||
zh: abf9a22603299cc7
|
||||
/loader-entry-tree-reports-and-include-persistence:
|
||||
en: dc27815908c90cd9
|
||||
zh: e9cc077d4d3fdbec
|
||||
/loader-entry-tree-reports-and-include-persistence/change:
|
||||
en: 6bbe6a3a14e182d1
|
||||
zh: 4256bd997293f8f2
|
||||
/loader-entry-tree-reports-and-include-persistence/migration:
|
||||
en: 574f30ad53da1e66
|
||||
zh: 1b9914c4ead412f7
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Loader entry trees report mutations through commit(change), and Include persists them through a journal."
|
||||
---
|
||||
|
||||
# Loader entry tree reports and Include persistence
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`@deepseek-ai/cordis-plugin-loader` follows Loader 1.0.0-rc.8, and `@deepseek-ai/cordis-plugin-include` follows Include 1.1.1. This affects `cordis.yml` and patch files, code that subclasses `EntryTree`, and code that calls `applyEntryPatches` or relies on Include writes.
|
||||
|
||||
- `EntryTree.write()` is removed. Subclasses implement `commit(change: EntryChange)`, which receives each create, update, move, removal and explicit Fiber save synchronously. In-memory trees implement a no-op.
|
||||
- `applyEntryPatches(data, patches, warn)` always returns cloned entries, including without patches, and clones inserted rows. Callers can no longer mutate the input through the result.
|
||||
- Include persists runtime changes through a journal reconciled with the latest file. File edits win conflicting fields and are logged as conflicts. Changes to patch-owned fields and inserted rows update the parent Include's patches. `flush()` waits for pending writes and rejects while changes remain unsaved; `refresh()` and `stop()` return the same result.
|
||||
- Include writes generated ids back into file rows that had none. Runtime changes to rows inserted by patches, including bundle rows, update the parent's patches instead of the included file. The parent of the `dsh` profile's root Include is the in-memory Loader, so such changes, for example a bundle row's self-disposal `disabled: true`, no longer survive a restart; previously the whole patched tree was written into `cordis.yml`.
|
||||
|
||||
## Migration
|
||||
|
||||
1. In `EntryTree` subclasses, replace `write()` with `commit(change)`. Persist from `change.options` and `change.legacy`: a missing `legacy` is a creation and a missing `options` is a removal.
|
||||
2. Treat the `applyEntryPatches` result as the only copy to modify.
|
||||
3. Where code needs a runtime change on disk, await `include.flush()`; for patch-owned changes, flush the child Include and then its parent.
|
||||
4. Start the application and confirm that runtime edits appear in the configuration file after `flush()`.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Loader 条目树通过 commit(change) 报告变更,Include 通过 journal 持久化这些变更。"
|
||||
---
|
||||
|
||||
# Loader 条目树变更报告与 Include 持久化
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
`@deepseek-ai/cordis-plugin-loader` 跟随 Loader 1.0.0-rc.8,`@deepseek-ai/cordis-plugin-include` 跟随 Include 1.1.1。受影响的是 `cordis.yml` 与 patch 文件、继承 `EntryTree` 的代码,以及调用 `applyEntryPatches` 或依赖 Include 写盘的代码。
|
||||
|
||||
- 移除了 `EntryTree.write()`。子类改为实现 `commit(change: EntryChange)`,它会同步收到每一次创建、更新、移动、删除和显式 Fiber 保存。内存树实现为空操作。
|
||||
- `applyEntryPatches(data, patches, warn)` 总是返回克隆后的条目,没有 patch 时也一样,插入行同样会被克隆。调用方不能再通过结果修改输入。
|
||||
- Include 通过与最新文件协调的 journal 持久化运行时变更。文件编辑在字段冲突时优先,并记录为冲突。对 patch 所拥有字段和插入行的修改会写入父 Include 的 patch。`flush()` 等待待处理的写入,并在仍有未保存变更时拒绝;`refresh()` 和 `stop()` 返回相同的结果。
|
||||
- Include 会把生成的 id 写回原本没有 id 的文件行。对 patch 插入行(包括 bundle 行)的运行时修改会写入父级的 patch,而不是被 include 的文件。`dsh` profile 根 Include 的父级是内存中的 Loader,因此这类修改(例如 bundle 行自行释放时写入的 `disabled: true`)重启后不再保留;此前整棵已应用 patch 的树会被写入 `cordis.yml`。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 在 `EntryTree` 子类中把 `write()` 换成 `commit(change)`。根据 `change.options` 和 `change.legacy` 持久化:缺少 `legacy` 表示创建,缺少 `options` 表示删除。
|
||||
2. 只修改 `applyEntryPatches` 返回的结果。
|
||||
3. 需要运行时变更落盘时,await `include.flush()`;对于 patch 所拥有的变更,先 flush 子 Include,再 flush 其父 Include。
|
||||
4. 启动应用,确认 `flush()` 之后运行时编辑已出现在配置文件中。
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.2-alpha.2/forge-worktree-entry/guide.md
|
||||
/:
|
||||
en: e25e3adac59e3584
|
||||
zh: 1a16ac4c16ab3a0d
|
||||
/one-plugin-entry-for-change-requests-and-worktrees:
|
||||
en: 2eb2f0805d09f0c4
|
||||
zh: a7b02646515b39df
|
||||
/one-plugin-entry-for-change-requests-and-worktrees/change:
|
||||
en: b814d167dea20941
|
||||
zh: a8e3c416acd4fb65
|
||||
/one-plugin-entry-for-change-requests-and-worktrees/migration:
|
||||
en: 08b0e3bae9e9697e
|
||||
zh: 62f66d48328f1037
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "The Plugins page replaces the standalone Git worktrees card with Change requests & worktrees."
|
||||
---
|
||||
|
||||
# One plugin entry for change requests and worktrees
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
Web and Desktop use **Change requests & worktrees** to enable the review panel, the new-Session worktree option, and the model's `create_worktree` tool together. The separate **Git worktrees** card is hidden. Profiles that already enabled the Forge bundle also gain the model tool.
|
||||
|
||||
Existing profiles are not migrated automatically. A selected `@deepseek-ai/dsh-experimental-tool-worktree` bundle stays active even though its card is hidden, so turning off Forge does not disable that standalone tool. Selecting both it and `@deepseek-ai/dsh-experimental-forge-bundle` inserts duplicate `worktree` and `tool-worktree` ids, making those component controls read-only. Remove the standalone selection before using the unified switch.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Stop Web or quit Desktop. Back up `$DSH_HOME/profiles/<name>/package.json` and its sibling `cordis.patch.yml`. Use `~/.dsh` when `DSH_HOME` is unset; the standard profile names are `web` and `desktop`. For a custom profile, use its name.
|
||||
2. Edit `dsh.profile.bundles` in that `package.json`. Remove every `@deepseek-ai/dsh-experimental-tool-worktree` entry. To keep worktree support through the unified switch, add `@deepseek-ai/dsh-experimental-forge-bundle` once; if it is already selected, keep that entry. To disable both features, remove both bundle names. Preserve all other entries and fields.
|
||||
3. Keep existing `cordis.patch.yml` overrides: the `worktree`, `tool-worktree`, and `optional-forge-panel` ids are unchanged. To use the Forge panel without exposing the model tool, add or update this entry in the existing patch list:
|
||||
|
||||
```yaml
|
||||
- id: tool-worktree
|
||||
disabled: true
|
||||
```
|
||||
|
||||
4. Restart the application. In Plugins, confirm that **Change requests & worktrees** reflects your selection. With no disabling row override, enabling it provides the panel and `create_worktree`; turning it off removes both. Confirm that the profile lists no standalone worktree bundle. Existing worktrees, branches, and Session records are retained.
|
||||
|
||||
Headless profiles and custom compositions that need only the standalone tool can keep it; do not also select the Forge bundle. In Web or Desktop, that standalone selection remains hidden and must be managed through the profile file.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "插件页将独立的 Git 工作树卡片合入变更请求与工作树。"
|
||||
---
|
||||
|
||||
# 变更请求与工作树使用同一个插件入口
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
Web 和 Desktop 通过**变更请求与工作树**同时启用评审面板、新会话的工作树选项和模型的 `create_worktree` 工具。独立的 **Git 工作树**卡片被隐藏。已经启用 Forge 组合包的 profile 也会新增模型工具。
|
||||
|
||||
已有 profile 不会自动迁移。选中的 `@deepseek-ai/dsh-experimental-tool-worktree` 组合包仍会运行,即使其卡片已隐藏,因此关闭 Forge 不会停用这个独立工具。同时选择它和 `@deepseek-ai/dsh-experimental-forge-bundle` 会插入重复的 `worktree` 与 `tool-worktree` id,使对应组件控件变为只读。使用统一开关前,请移除独立组合包的选择。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 停止 Web 或退出 Desktop。备份 `$DSH_HOME/profiles/<name>/package.json` 及同目录下的 `cordis.patch.yml`。未设置 `DSH_HOME` 时使用 `~/.dsh`;标准 profile 名称为 `web` 和 `desktop`。自定义 profile 使用其自身名称。
|
||||
2. 编辑该 `package.json` 的 `dsh.profile.bundles`。移除所有 `@deepseek-ai/dsh-experimental-tool-worktree` 条目。若要通过统一开关保留工作树功能,添加一次 `@deepseek-ai/dsh-experimental-forge-bundle`;若已选中它,则保留该条目。若要关闭两项功能,则移除这两个组合包名称。保留其他所有条目和字段。
|
||||
3. 保留现有的 `cordis.patch.yml` 覆盖配置:`worktree`、`tool-worktree` 和 `optional-forge-panel` 的 id 均未改变。若只使用 Forge 面板,不向模型提供工作树工具,在现有 patch 列表中添加或更新以下条目:
|
||||
|
||||
```yaml
|
||||
- id: tool-worktree
|
||||
disabled: true
|
||||
```
|
||||
|
||||
4. 重启应用。在插件页确认**变更请求与工作树**的状态符合你的选择。没有禁用插件行的覆盖配置时,启用它会提供面板与 `create_worktree`,关闭后两者均移除。确认 profile 不再列出独立的工作树组合包。已有工作树、分支和 Session 记录均保留。
|
||||
|
||||
仅需要独立工具的 Headless profile 和自定义组合可以继续保留它,但不要同时选择 Forge 组合包。在 Web 或 Desktop 中,该独立选择仍然隐藏,需要通过 profile 文件管理。
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.2-alpha.2/projection-released-checkpoint/guide.md
|
||||
/:
|
||||
en: 738f2376c5a8ebe5
|
||||
zh: 078f2a08d8b6ad4b
|
||||
/released-projection-units-stay-in-checkpoints:
|
||||
en: 9355d9de711acc7d
|
||||
zh: eb89da6b886a7556
|
||||
/released-projection-units-stay-in-checkpoints/change:
|
||||
en: 5944b6c7b72e15e4
|
||||
zh: 051d68e2eb5151d7
|
||||
/released-projection-units-stay-in-checkpoints/migration:
|
||||
en: 9f24ecf12748eb41
|
||||
zh: c07649e79a0174f9
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "`ctx.sessionProjections.checkpoint(session)` keeps a released unit's last row instead of omitting it."
|
||||
---
|
||||
|
||||
# Released projection units stay in checkpoints
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
Previously, when the last registrant of a projection key released it, through `handle.dispose()` or by unloading the owning fiber, the registry deleted the unit and `ctx.sessionProjections.checkpoint(session)` stopped returning its row.
|
||||
|
||||
Now the released unit stays in the registry as a predecessor. It leaves event drives, `snapshot()`, `summaryKeys()`, `restoreFloor()`, `restore()`, `viewCheckpoint()`, and `hydrate()`, and Sessions created afterward get no cell for it. `checkpoint(session)` still returns the last row the unit folded for each existing Session, and the registry no longer advances that row. Registering the same key again replaces the predecessor and refolds the log; see the [registration lifecycle](../../../../packages/session/session-projection/README.md).
|
||||
|
||||
This affects plugin code and tests that read `checkpoint()` after a projection's owner unloads, such as HMR tests that assert a released key is absent. `@deepseek-ai/dsh-session-projection-cache` stores these rows, so a graceful shutdown no longer drops the values that Web-layer plugins folded before their Sessions detach.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Find reads of `sessionProjections.checkpoint(...)` that run after a projection's owner unloads, including calls through an alias of `ctx.sessionProjections`.
|
||||
2. Replace an assertion that the released key is absent from an existing Session with one that its row keeps the pre-unload value:
|
||||
|
||||
```ts ignore-check
|
||||
const row = ctx.sessionProjections.checkpoint(session).myKey
|
||||
await owner.dispose()
|
||||
expect(ctx.sessionProjections.checkpoint(session)).toHaveProperty('myKey', row)
|
||||
```
|
||||
|
||||
3. To show that the unit left live participation, assert that a Session created after the unload has no row, `expect(ctx.sessionProjections.checkpoint(ctx.sessions.create())).not.toHaveProperty('myKey')`, or that an event appended after the unload leaves the row unchanged. A client-visible key is also absent from `snapshot(session).values`.
|
||||
4. Run the affected tests; they pass when no assertion expects a released key to disappear from an existing Session's checkpoint.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "`ctx.sessionProjections.checkpoint(session)` 保留已释放单元的最后一行,而不再省略它。"
|
||||
---
|
||||
|
||||
# 已释放的投影单元保留在检查点中
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
此前,投影 key 的最后一个注册方通过 `handle.dispose()` 或卸载所属 fiber 释放它时,注册表会删除该单元,`ctx.sessionProjections.checkpoint(session)` 也不再返回它的行。
|
||||
|
||||
现在,已释放的单元作为前任留在注册表中。它退出事件推进、`snapshot()`、`summaryKeys()`、`restoreFloor()`、`restore()`、`viewCheckpoint()` 和 `hydrate()`,此后新建的 Session 也不会为它建立 cell。`checkpoint(session)` 仍为每个已有 Session 返回该单元最后折叠出的行,注册表不再推进这一行。同一 key 再次注册时会替换前任并重新折叠日志;参见[注册生命周期](../../../../packages/session/session-projection/README.zh.md)。
|
||||
|
||||
这会影响在投影所属方卸载后读取 `checkpoint()` 的插件代码和测试,例如断言已释放 key 不存在的 HMR 测试。`@deepseek-ai/dsh-session-projection-cache` 会存储这些行,因此正常关停不再丢掉 Web 层插件在其 Session detach 之前已折叠出的值。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 找出在投影所属方卸载后执行的 `sessionProjections.checkpoint(...)` 读取,包括通过 `ctx.sessionProjections` 别名的调用。
|
||||
2. 把「已有 Session 中不存在已释放 key」的断言,改为断言它的行保持卸载前的值:
|
||||
|
||||
```ts ignore-check
|
||||
const row = ctx.sessionProjections.checkpoint(session).myKey
|
||||
await owner.dispose()
|
||||
expect(ctx.sessionProjections.checkpoint(session)).toHaveProperty('myKey', row)
|
||||
```
|
||||
|
||||
3. 如需证明该单元已退出实时参与,可断言卸载后新建的 Session 没有这一行,即 `expect(ctx.sessionProjections.checkpoint(ctx.sessions.create())).not.toHaveProperty('myKey')`,或断言卸载后追加的事件不会改变这一行。客户端可见的 key 也不会出现在 `snapshot(session).values` 中。
|
||||
4. 运行受影响的测试;当没有断言要求已释放 key 从已有 Session 的检查点中消失时,测试即通过。
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.2-alpha.2/projection-snapshot-scope/guide.md
|
||||
/:
|
||||
en: 82a6e0261297097c
|
||||
zh: c86e496c258e72b2
|
||||
/projection-snapshot-scope:
|
||||
en: c8b08322a943d6b2
|
||||
zh: 31862a259b7738bb
|
||||
/projection-snapshot-scope/change:
|
||||
en: d95cd644b01419a1
|
||||
zh: 89038a6de9b74e8a
|
||||
/projection-snapshot-scope/migration:
|
||||
en: 408d88a4ea6fe3a9
|
||||
zh: 397de0dc2d8318df
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Projection snapshot reads compute only the returned units, the registry cached snapshot read is deprecated, and Session list summaries omit context pressure and breakdown."
|
||||
---
|
||||
|
||||
# Projection snapshot scope
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`ctx.sessionProjections.snapshot(session, keys)` from `@deepseek-ai/dsh-session-projection` computes only the units it returns: with `keys` it computes only the selected units, and without `keys` it computes every unit that defines `wire` and no Host-only unit. A plugin can no longer rely on other units being computed after it calls `snapshot`. The registry's `cachedSnapshot(session, keys)` is deprecated; it returns only values another read already computed and may omit keys. The persisted cache read `ctx.sessionProjectionCache.cachedSnapshot(header, keys)` is unchanged. `session.list` summary `projections` no longer carry `contextBreakdown` or `contextPressure`; the `@deepseek-ai/dsh-token-meter` entries are now `SessionProjectionSummaryExcluded`. Plugins and SDK or wire clients that read these values are affected.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Read Host state of your own unit through the handle that `await ctx.sessionProjections.register(definition)` returns, with `handle.stateOf(session)`, instead of calling `snapshot` first and reading cells afterwards.
|
||||
2. Replace calls to the registry's `cachedSnapshot(session, keys)` with `snapshot(session, keys)`.
|
||||
3. Read `contextBreakdown` and `contextPressure` from an opened Session, for example the Session projection snapshot or follow baseline, instead of from `session.list` rows.
|
||||
4. Rebuild your plugins and clients, confirm that type checks pass without `no-deprecated` findings for `cachedSnapshot`, and confirm that an opened Session still shows its context meter.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "投影 snapshot 读取只计算返回的单元,注册表的 cached snapshot 读取已废弃,Session 列表摘要不再包含 context pressure 和 breakdown。"
|
||||
---
|
||||
|
||||
# 投影 snapshot 的计算范围
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
`@deepseek-ai/dsh-session-projection` 的 `ctx.sessionProjections.snapshot(session, keys)` 只计算它返回的单元:传 `keys` 时只计算选中的单元,不传时计算每个定义了 `wire` 的单元、不计算 Host 专用单元。插件不能再依赖调用 `snapshot` 之后其他单元已经算好。注册表的 `cachedSnapshot(session, keys)` 已废弃,它只返回其他读取已经算好的值,可能缺少 key。持久化缓存的读取 `ctx.sessionProjectionCache.cachedSnapshot(header, keys)` 不变。`session.list` 摘要的 `projections` 不再携带 `contextBreakdown` 和 `contextPressure`,`@deepseek-ai/dsh-token-meter` 的这两项改为 `SessionProjectionSummaryExcluded`。读取这些值的插件以及 SDK 或 wire 客户端受影响。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 通过 `await ctx.sessionProjections.register(definition)` 返回的句柄,用 `handle.stateOf(session)` 读取自己单元的 Host 状态,不要先调用 `snapshot` 再读取 cell。
|
||||
2. 把对注册表 `cachedSnapshot(session, keys)` 的调用替换为 `snapshot(session, keys)`。
|
||||
3. 从打开的 Session 读取 `contextBreakdown` 和 `contextPressure`,例如 Session 投影 snapshot 或 follow 基线,不要从 `session.list` 行读取。
|
||||
4. 重新构建插件和客户端,确认类型检查通过、`cachedSnapshot` 没有 `no-deprecated` 告警,并确认打开的 Session 仍显示上下文用量。
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 1fdf707322fb6996
|
||||
zh: 0204f7e9d4aa3cce
|
||||
/session-controller/use-this-package:
|
||||
en: 25c9c54037dc7971
|
||||
zh: 7fa87a380b046afb
|
||||
en: 300f02be359a3170
|
||||
zh: ac7fb4abbe78eddf
|
||||
/session-controller/client-references:
|
||||
en: e8979a803ec3e194
|
||||
zh: c567efcd971728ba
|
||||
|
||||
@@ -33,7 +33,7 @@ History pages and follow opening snapshots carry one `{ type: 'event', event: Se
|
||||
|
||||
The Client journal validates current Session event envelopes before publishing follow snapshots, live entries, or history pages. It reuses the browser-safe Session validators for required surface markers, exact replacement endpoints, earlier unique source seqs, embedded Assistant provider metadata, request-header omissions, and tool-error consistency. Invalid records fail without field stripping or normalization; range membership and source existence remain durable-log checks on the Host.
|
||||
|
||||
Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. A receipt names a staged file or a staged image, and an image receipt subjects the prompt to the same image-input check as an encoded image. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and the subagent ownership fence determined by `origin: 'subagent'` to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent's effective working directory and scoped registry when present. Cold listing uses the projected current directory, falling back to the recorded origin and then the configured directory default. It retains the preset standing scope without activating an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` normally reads an exact baseline, including current-format cold logs, and returns `kind: sequenced`. Only a read requiring migration returns `kind: migration-required` hints without `asOfSeq`; missing keys remain absent. It returns null for a missing Session. Fork callers may set `allowMigration: false` to receive `session/migration-required` instead of starting source migration; omission retains automatic migration. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Sequenced reads and live projection frames use the same sequence ordering; cached reads fill hints without replacing sequenced values.
|
||||
Each endpoint states its activation policy. For a cold Session, list reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. An attached Session is summarized from its in-memory log. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. A receipt names a staged file or a staged image, and an image receipt subjects the prompt to the same image-input check as an encoded image. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and the subagent ownership fence determined by `origin: 'subagent'` to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent's effective working directory and scoped registry when present. Cold listing uses the projected current directory, falling back to the recorded origin and then the configured directory default. It retains the preset standing scope without activating an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` normally reads an exact baseline, including current-format cold logs, and returns `kind: sequenced`. Only a read requiring migration returns `kind: migration-required` hints without `asOfSeq`; missing keys remain absent. It returns null for a missing Session. Fork callers may set `allowMigration: false` to receive `session/migration-required` instead of starting source migration; omission retains automatic migration. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Sequenced reads and live projection frames use the same sequence ordering; cached reads fill hints without replacing sequenced values.
|
||||
|
||||
Every Remote endpoint is access-controlled: a Peer whose principal is not the owner reaches an endpoint only when, on every resource the call touches, a binding of that principal kind's policy in [`dsh-api-remote-access`](../remote-access/README.md) grants a role listing the action ([Peer principals Agent Note](../../../.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.md)). The controller injects `remoteAccess` and registers a default resolver for the `session` resource kind through `typert.resources`. It reads a named `sessionId` parameter, otherwise the first object argument's `sessionId` or `address`; unrelated string parameters name no Session. An object containing both `sessionId` and `address` is invalid, even when they name the same Session. `session/events`, the deprecated `session/page`, `session/observeEvent`, and `session/follow` use this resolver to read `address.sessionId` for an ordinary Session address and `address.childSessionId` for a subagent address; whether access to the parent Session covers its children is the policy's rule. `session/projections`, `session/observeProjections`, `session/state`, `session/observeState`, and `session/wait` read the Session resource from `request.sessionId`, so each call and subscription names only the Sessions it may read. `session/list`, `session/search`, and `session/control` read no resource, so their calls touch the `global` resource and need a `global` role; for a non-owner caller, `session/list` returns only the rows, `session/search` only the hits, and `session/control` only the baseline projections and projection frames, whose Session `ctx.remoteAccess.allows()` admits for the same action, so that action also belongs to a `session` role; owner results are unfiltered. `session/prompt` builds the durable `user-rpc` message source in its Remote method and records `principal` only for a non-owner caller, read through `principalOf(this.ctx.invocation)`; a source without `principal` belongs to the owner.
|
||||
|
||||
@@ -41,7 +41,7 @@ Every Remote endpoint is access-controlled: a Peer whose principal is not the ow
|
||||
|
||||
List rows carry `formatStatus` when available, so Clients can identify migration-required Sessions without requesting their projections.
|
||||
|
||||
Host list generation yields between complete rows only when its configured work slice is exhausted; cheap remaining work returns without a forced yield. Request cancellation is checked per row and after each yield and rejects without partial results. Each row is synchronous; the list does not promise one cross-Session snapshot. A Session already queued as cold remains a cached row if it attaches during a yield: Client in-flight mutation replay corrects its availability, and sequenced projections supersede cached hints.
|
||||
Host list generation yields between complete rows only when its configured work slice is exhausted; cheap remaining work returns without a forced yield. Request cancellation is checked per row and after each yield and rejects without partial results. Each row is synchronous; the list does not promise one cross-Session snapshot. A Session already queued as cold remains a cached row if it attaches during a yield: Client in-flight mutation replay corrects its availability, and sequenced projections supersede cached hints. An attached Session's summary reads its blankness and last-prompt recency through the `sessionListMetadata` projection handle, and its projection hints carry every summary key computed through `snapshot`. The first summary of an attached Session computes these projections over its whole in-memory log, and later summaries advance them incrementally; this computation runs inside one synchronous row, so `listWorkSliceMs` cannot split it.
|
||||
|
||||
Client list rows and resident Sessions use the current `sessionListMetadata` projection to reject stale blank-session hints; recency is the later of the summary timestamp and the projected last user prompt. When SessionManager creates an instance before its list row arrives, it reconciles blankness with metadata already retained for that Session. An opened conversation is therefore not reused by New Session even when an older list response still marks it blank.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ kind: "package-reference"
|
||||
|
||||
Client journal 在发布 follow 快照、live entry 或历史页之前验证当前 Session 事件 envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 提供方元数据、request header 可选字段的省略规则以及工具错误一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。
|
||||
|
||||
每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。凭证指向一个暂存文件或一张暂存图片;图片凭证与编码图片一样要经过图片输入检查。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和由 `origin: 'subagent'` 确定的 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent 的有效工作目录与限定范围内的注册表。冷态列表使用投影中的当前目录,依次回退到记录的起始目录与配置的默认目录。它保留 preset 常驻 scope,不会激活 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 正常读取准确基线,包括当前格式的冷日志,返回 `kind: sequenced`。只有需要迁移时才返回不带 `asOfSeq` 的 `kind: migration-required` 提示,缺少的键保持缺席。Session 不存在时返回 null。Fork 调用者可设置 `allowMigration: false`,在源历史需要迁移时收到 `session/migration-required`;省略则保持自动迁移。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。有序读取与实时 projection 帧使用相同的序号排序规则;缓存读取只补充提示,不替换有序值。
|
||||
每个 endpoint 都声明自己的激活策略。对冷 Session,列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。已接入 Session 从其内存日志生成摘要。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。凭证指向一个暂存文件或一张暂存图片;图片凭证与编码图片一样要经过图片输入检查。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和由 `origin: 'subagent'` 确定的 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent 的有效工作目录与限定范围内的注册表。冷态列表使用投影中的当前目录,依次回退到记录的起始目录与配置的默认目录。它保留 preset 常驻 scope,不会激活 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 正常读取准确基线,包括当前格式的冷日志,返回 `kind: sequenced`。只有需要迁移时才返回不带 `asOfSeq` 的 `kind: migration-required` 提示,缺少的键保持缺席。Session 不存在时返回 null。Fork 调用者可设置 `allowMigration: false`,在源历史需要迁移时收到 `session/migration-required`;省略则保持自动迁移。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。有序读取与实时 projection 帧使用相同的序号排序规则;缓存读取只补充提示,不替换有序值。
|
||||
|
||||
每个 Remote 端点都受访问控制:principal 不是 owner 的 Peer 只有在本次调用访问的每个资源上,该 principal kind 在 [`dsh-api-remote-access`](../remote-access/README.zh.md) 中的 policy 都有某个 binding 授予列出该 action 的 role 时,才能调用该端点([Peer 身份属性 Agent Note](../../../.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md))。控制器注入 `remoteAccess`,并经 `typert.resources` 为 `session` 资源种类登记默认 resolver。它读取具名 `sessionId` 参数,否则读取第一个对象参数的 `sessionId` 或 `address`;无关字符串参数不指明 Session。对象同时包含 `sessionId` 和 `address` 时无效,即使两者指向同一 Session。`session/events`、已弃用的 `session/page`、`session/observeEvent` 与 `session/follow` 使用该 resolver:对普通 Session 地址读 `address.sessionId`,对 subagent 地址读 `address.childSessionId`;父会话的访问权是否覆盖其子会话由 policy 决定。`session/projections`、`session/observeProjections`、`session/state`、`session/observeState` 与 `session/wait` 从 `request.sessionId` 读取会话资源,因此每次调用与订阅都只点名它可读的会话。`session/list`、`session/search` 与 `session/control` 不读取资源,其调用访问 `global` 资源,需要 `global` role;对非 owner 调用方,`session/list` 只返回其 Session 被 `ctx.remoteAccess.allows()` 按同一 action 允许的行,`session/search` 只返回这类 Session 的命中,`session/control` 也只返回这类 Session 的 baseline projection 与 projection 帧,因此该 action 也要列在 `session` role 中;owner 的结果不过滤。`session/prompt` 在其 Remote 方法内构造持久的 `user-rpc` 消息 source,且只为非 owner 调用方记录经 `principalOf(this.ctx.invocation)` 读取的 `principal`;不带 `principal` 的 source 属于 owner。
|
||||
|
||||
@@ -41,7 +41,7 @@ Client journal 在发布 follow 快照、live entry 或历史页之前验证当
|
||||
|
||||
列表行在状态可用时携带 `formatStatus`,Client 无需请求其投影即可识别需要迁移的 Session。
|
||||
|
||||
Host 仅在配置的工作时间片耗尽时,于完整条目之间让出事件循环;剩余工作较少时直接返回,不强制让出。每行及每次让出之后检查请求取消,取消时拒绝返回部分结果。每行同步计算;列表不承诺跨 Session 的统一快照。已经进入冷态队列的 Session 若在让出期间接入,仍返回缓存行:Client 对进行中请求的变更重放会纠正其可用性,带序号的投影会覆盖缓存提示。
|
||||
Host 仅在配置的工作时间片耗尽时,于完整条目之间让出事件循环;剩余工作较少时直接返回,不强制让出。每行及每次让出之后检查请求取消,取消时拒绝返回部分结果。每行同步计算;列表不承诺跨 Session 的统一快照。已经进入冷态队列的 Session 若在让出期间接入,仍返回缓存行:Client 对进行中请求的变更重放会纠正其可用性,带序号的投影会覆盖缓存提示。已接入 Session 的摘要通过 `sessionListMetadata` 投影句柄读取是否为空和最后一次提示词的最近活动时间,其投影提示携带通过 `snapshot` 计算的每个摘要 key。已接入 Session 的首次摘要在其完整内存日志上计算这些投影,之后的摘要增量推进;这次计算在单行的同步工作内进行,`listWorkSliceMs` 无法切分它。
|
||||
|
||||
Client 列表行和驻留 Session 使用当前 `sessionListMetadata` 投影纠正过期的空白会话提示;最近活动时间取摘要时间戳与投影中最后一次用户提示词时间的较晚值。当 SessionManager 在列表行到达前创建实例时,会使用已保留的该 Session 元数据对账空白状态。因此,即使旧列表响应仍将已有对话标为空白,新会话操作也不会复用已经打开过的对话。
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-agent-preset-registry'
|
||||
import type { ImageAttachmentLimits } from '@deepseek-ai/dsh-attachment'
|
||||
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection'
|
||||
import type { ProjectionHandle, ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection'
|
||||
import type {} from '@deepseek-ai/dsh-session-projection-cache'
|
||||
import { SessionQueryError, type SessionRecord, type SessionSearchCursor } from '@deepseek-ai/dsh-session-query'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
@@ -76,6 +76,8 @@ export function truncateUnicodeCodePoints(value: string, maximum: number): strin
|
||||
|
||||
/** Owns list projection registration, bounded cold summaries, and authorized search. */
|
||||
export class ApiSessionList {
|
||||
private listMetadata!: ProjectionHandle<SessionListMetadata>
|
||||
|
||||
/**
|
||||
* @param ctx - Host context carrying Session, query, persistence, and projection services.
|
||||
* @param workSliceMs - Resolved positive integral list-work budget in milliseconds.
|
||||
@@ -87,7 +89,7 @@ export class ApiSessionList {
|
||||
* @returns when all required registrations are ready.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
await this.ctx.sessionProjections.register<'sessionListMetadata', SessionListMetadata>({
|
||||
this.listMetadata = await this.ctx.sessionProjections.register<'sessionListMetadata', SessionListMetadata>({
|
||||
key: 'sessionListMetadata',
|
||||
stateSchema: sessionListMetadataSchema,
|
||||
init: () => ({ blank: true, lastPromptAt: null }),
|
||||
@@ -111,19 +113,25 @@ export class ApiSessionList {
|
||||
|
||||
/**
|
||||
* Build one current attached-Session summary.
|
||||
* Blankness and recency come from the `sessionListMetadata` handle; projection
|
||||
* hints carry every summary key computed through `snapshot`. The first call
|
||||
* for a Session computes these projections over its whole in-memory log, and
|
||||
* later calls advance them incrementally. The handle read and the hint with
|
||||
* the same key come from one cell; the handle read does not depend on any
|
||||
* other unit's view.
|
||||
* @param session - attached Session to summarize.
|
||||
* @returns current list metadata and available projections.
|
||||
*/
|
||||
summaryFor(session: Session): SessionSummary {
|
||||
const metadata = this.listMetadata.stateOf(session)
|
||||
const projections = this.projectionsFor(session.header, session)
|
||||
const metadata = projections?.values.sessionListMetadata
|
||||
return {
|
||||
sessionId: session.id,
|
||||
updatedAt: updatedAt(session.header, metadata),
|
||||
agentAvailable: this.ctx.agents.get(session.id)?.session === session,
|
||||
formatStatus: 'current',
|
||||
running: this.ctx.agents.get(session.id)?.status === 'running',
|
||||
blank: metadata?.blank ?? session.seq === 0,
|
||||
blank: metadata.blank,
|
||||
...listFields(session.header),
|
||||
...(projections === undefined ? {} : { projections }),
|
||||
}
|
||||
@@ -304,9 +312,9 @@ export class ApiSessionList {
|
||||
// itself summary-excluded.
|
||||
const keys = this.ctx.sessionProjections.summaryKeys()
|
||||
if (session !== undefined) {
|
||||
// The live registry computed the block for this Session: its watermark
|
||||
// The live registry computes the block for this Session: its watermark
|
||||
// shares the sequence space of the Session's baselines and frames.
|
||||
return hintsOf('sequenced', this.ctx.sessionProjections.cachedSnapshot(session, keys))
|
||||
return hintsOf('sequenced', this.ctx.sessionProjections.snapshot(session, keys))
|
||||
}
|
||||
// A cold row reads the persisted cache by header alone; the cache serves
|
||||
// seeded and unseeded lifecycles alike because a listing never seeds a
|
||||
@@ -334,8 +342,8 @@ function hintsOf(
|
||||
block: ProjectionSnapshot | undefined,
|
||||
): SessionProjectionHints | undefined {
|
||||
if (block === undefined || Object.keys(block.values).length === 0) return undefined
|
||||
// Listing hints contain every summary wire value the source currently holds
|
||||
// but remain partial: missing cells and cache rows are never materialized here.
|
||||
// Listing hints contain every summary wire value the source holds; cached
|
||||
// hints remain partial because missing cache rows are never folded here.
|
||||
return { kind, asOfSeq: block.asOfSeq, values: block.values }
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,10 @@ export interface SessionListMetadata {
|
||||
}
|
||||
|
||||
/**
|
||||
* Every available summary wire value a Session-list row carries as partial,
|
||||
* possibly stale hints. The block omits the keys whose unit declares
|
||||
* `summary: false`. `kind` and `asOfSeq` are independent facts: `kind` says
|
||||
* Summary wire values a Session-list row carries. A `sequenced` block carries
|
||||
* every summary key computed at its `asOfSeq`; a `cached` block carries the
|
||||
* values the persisted cache holds and may omit keys or trail the Session.
|
||||
* The block omits the keys whose unit declares `summary: false`. `kind` and `asOfSeq` are independent facts: `kind` says
|
||||
* which sequence space `asOfSeq` belongs to, and therefore how a client may
|
||||
* merge the block; `asOfSeq` is the producer's watermark in that space.
|
||||
*/
|
||||
|
||||
@@ -275,7 +275,8 @@ describe('attached updatedAt tracks human prompts', () => {
|
||||
const listed = await remote.list(request({}))
|
||||
if (!listed.ok) throw new Error('list failed')
|
||||
const summary = listed.value.items.find(item => item.sessionId === 'resumed-untouched')
|
||||
expect(summary?.updatedAt).toBe(500)
|
||||
// The resumed log's last human prompt, not its creation time or the later pickup.
|
||||
expect(summary?.updatedAt).toBe(worked)
|
||||
|
||||
// A lifecycle boundary is not a human update.
|
||||
resumed.append('turn/start', { turn: 2 })
|
||||
|
||||
@@ -5,21 +5,40 @@
|
||||
* fresh session keeps it list-hidden and reusable, while the first accepted
|
||||
* prompt's turn/start clears it. The host/session-added frame shares the
|
||||
* same predicate function (covered by the workspace spec's frame assertion).
|
||||
* A resumed Session computes its own list metadata (blankness and last-prompt
|
||||
* recency) when it is published, independent of which other projections were
|
||||
* read before publication.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionSeq } from '@deepseek-ai/dsh-session'
|
||||
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
// Side-effect type imports: the configuration-event SessionEventMap merges.
|
||||
import type {} from '@deepseek-ai/dsh-permission-presets'
|
||||
import type {} from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import type { SessionSummary } from '../src/types.ts'
|
||||
import { createSessionTestRemote, type TestSessionRemote } from './test-remote.ts'
|
||||
|
||||
const ownedContexts = new Set<Context>()
|
||||
const ownedRoots = new Set<string>()
|
||||
afterEach(async () => {
|
||||
await Promise.all([...ownedContexts].map(ctx => ctx.fiber.dispose()))
|
||||
ownedContexts.clear()
|
||||
await Promise.all([...ownedRoots].map(root => rm(root, { recursive: true, force: true })))
|
||||
ownedRoots.clear()
|
||||
})
|
||||
|
||||
async function harness(): Promise<{ ctx: Context; remote: TestSessionRemote; attach: (session: Session) => Promise<void> }> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
@@ -74,3 +93,110 @@ describe('summary blank = conversation not started', () => {
|
||||
expect(await listBlank(remote, session.id)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Mount the real AgentLoop over JSONL persistence without the projection
|
||||
* cache, whose creation checkpoint would otherwise compute every unit before
|
||||
* the Session Controller observes the published Session.
|
||||
*/
|
||||
async function resumeHarness(): Promise<{ ctx: Context; remote: TestSessionRemote; cwd: string }> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-list-blank-'))
|
||||
ownedRoots.add(root)
|
||||
const ctx = new Context()
|
||||
ownedContexts.add(ctx)
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await ctx.plugin(JsonlSessionPersistence, { root })
|
||||
const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: root })
|
||||
await mountAgentLoopTestHarness(ctx)
|
||||
return { ctx, remote, cwd: root }
|
||||
}
|
||||
|
||||
/** Store one Session through persistence so a later resume seeds it from disk. */
|
||||
async function storeSession(ctx: Context, id: SessionId, cwd: string, events: readonly SessionEvent[]): Promise<void> {
|
||||
const prepared = ctx.sessions.prepare(id, { meta: { cwd } })
|
||||
const handle = await ctx.sessionPersistence.create(prepared.header)
|
||||
await handle.append(events)
|
||||
await handle.close()
|
||||
}
|
||||
|
||||
/** Configuration events a Session records at creation, before any prompt. */
|
||||
const configurationEvents = [
|
||||
{ type: 'permission/preset', seq: SessionSeq(0), time: 1, data: { preset: 'danger-full-access' } },
|
||||
{ type: 'sandbox/mode', seq: SessionSeq(1), time: 2, data: { mode: 'danger-full-access' } },
|
||||
{ type: 'approval/policy', seq: SessionSeq(2), time: 3, data: { policy: 'never' } },
|
||||
] as SessionEvent[]
|
||||
|
||||
/** Resume one stored Session and collect the summaries published for it. */
|
||||
async function resumeAndObserve(ctx: Context, remote: TestSessionRemote, id: SessionId): Promise<{
|
||||
added: SessionSummary[]
|
||||
listed: SessionSummary | undefined
|
||||
}> {
|
||||
const added: SessionSummary[] = []
|
||||
ctx.on('api-session/added', (summary) => {
|
||||
if (summary.sessionId === id) added.push(summary)
|
||||
})
|
||||
await ctx.agents.resume({ resumeSessionId: id })
|
||||
const listed = await remote.list({})
|
||||
if (!listed.ok) throw new Error('list failed')
|
||||
return { added, listed: listed.value.items.find(item => item.sessionId === id) }
|
||||
}
|
||||
|
||||
describe('resumed Session summaries', () => {
|
||||
it('publish a turnless resumed Session as blank', async () => {
|
||||
const { ctx, remote, cwd } = await resumeHarness()
|
||||
const id = 'session-resumed-blank' as SessionId
|
||||
await storeSession(ctx, id, cwd, configurationEvents)
|
||||
|
||||
const { added, listed } = await resumeAndObserve(ctx, remote, id)
|
||||
|
||||
expect(added.length).toBeGreaterThan(0)
|
||||
expect(added.map(summary => summary.blank)).toEqual(added.map(() => true))
|
||||
expect(listed?.blank).toBe(true)
|
||||
})
|
||||
|
||||
it('publish a resumed conversation as non-blank', async () => {
|
||||
const { ctx, remote, cwd } = await resumeHarness()
|
||||
const id = 'session-resumed-conversation' as SessionId
|
||||
await storeSession(ctx, id, cwd, [
|
||||
...configurationEvents,
|
||||
{ type: 'turn/start', seq: SessionSeq(3), time: 4, data: { turn: 1 } },
|
||||
{
|
||||
type: 'user/message', seq: SessionSeq(4), time: 4,
|
||||
data: createUserMessage({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }),
|
||||
surfaceOp: 'append',
|
||||
},
|
||||
{ type: 'turn/end', seq: SessionSeq(5), time: 5, data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
] as SessionEvent[])
|
||||
|
||||
const { added, listed } = await resumeAndObserve(ctx, remote, id)
|
||||
|
||||
expect(added.length).toBeGreaterThan(0)
|
||||
expect(added.map(summary => summary.blank)).toEqual(added.map(() => false))
|
||||
expect(listed?.blank).toBe(false)
|
||||
})
|
||||
|
||||
it('publish a resumed conversation with its last prompt time from the first frame', async () => {
|
||||
const { ctx, remote, cwd } = await resumeHarness()
|
||||
const id = 'session-resumed-recency' as SessionId
|
||||
const promptAt = Date.now() + 60_000
|
||||
await storeSession(ctx, id, cwd, [
|
||||
...configurationEvents,
|
||||
{ type: 'turn/start', seq: SessionSeq(3), time: promptAt, data: { turn: 1 } },
|
||||
{
|
||||
type: 'user/message', seq: SessionSeq(4), time: promptAt,
|
||||
data: createUserMessage({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }),
|
||||
surfaceOp: 'append',
|
||||
},
|
||||
{ type: 'turn/end', seq: SessionSeq(5), time: promptAt + 1, data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
] as SessionEvent[])
|
||||
|
||||
const { added, listed } = await resumeAndObserve(ctx, remote, id)
|
||||
|
||||
expect(added.length).toBeGreaterThan(0)
|
||||
expect(added.map(summary => [summary.updatedAt, summary.blank])).toEqual(added.map(() => [promptAt, false]))
|
||||
expect(Object.keys(added[0]?.projections?.values ?? {})).toContain('sessionListMetadata')
|
||||
expect(Object.keys(added[0]?.projections?.values ?? {})).toEqual(expect.arrayContaining([...ctx.sessionProjections.summaryKeys()]))
|
||||
expect(listed?.updatedAt).toBe(promptAt)
|
||||
expect(listed?.blank).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, Sess
|
||||
import type { Session, SessionEvent, SessionHeader, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
|
||||
import type { SessionProjectionSummaryExcluded } from '@deepseek-ai/dsh-session-projection/types'
|
||||
import SessionProjectionCache, { projectionCacheDomainSpec } from '@deepseek-ai/dsh-session-projection-cache'
|
||||
import { titleProjectionDefinition } from '@deepseek-ai/dsh-session-title'
|
||||
import Storage from '@deepseek-ai/dsh-storage'
|
||||
@@ -43,6 +44,7 @@ let nextHarnessSession = 1
|
||||
declare module '@deepseek-ai/dsh-session-projection/types' {
|
||||
interface SessionProjectionMap {
|
||||
'test/last-user': { text: string } | null
|
||||
'test/summary-excluded': SessionProjectionSummaryExcluded<number>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,7 +470,7 @@ describe('session.history projections block', () => {
|
||||
})
|
||||
|
||||
describe('session.list projections column', () => {
|
||||
it('serves every already-materialized wire value from the live registry without folding', async () => {
|
||||
it('serves every summary wire value from the live registry', async () => {
|
||||
const { ctx, session } = await harness(true)
|
||||
await ctx.sessionProjections.register(lastUserUnit())
|
||||
const gateway = await remote(ctx)
|
||||
@@ -502,29 +504,38 @@ describe('session.list projections column', () => {
|
||||
expect(row?.projections?.values.agentPreset).toBe('minimal')
|
||||
})
|
||||
|
||||
it('omits an unmaterialized live projection instead of folding history for listing', async () => {
|
||||
it('computes every summary key for a live row without computing summary-excluded units', async () => {
|
||||
const { ctx, session } = await harness(true)
|
||||
seedMessages(session, 1)
|
||||
const unit = lastUserUnit()
|
||||
const apply = vi.fn(unit.apply)
|
||||
await ctx.sessionProjections.register({ ...unit, apply })
|
||||
await ctx.sessionProjections.register(lastUserUnit())
|
||||
const excludedApply = vi.fn((state: number) => state + 1)
|
||||
await ctx.sessionProjections.register({
|
||||
key: 'test/summary-excluded',
|
||||
stateSchema: z.number().int().nonnegative(),
|
||||
init: () => 0,
|
||||
apply: excludedApply,
|
||||
wire: { viewSchema: z.number().int().nonnegative(), view: state => state, summary: false },
|
||||
stateVersion: 1,
|
||||
})
|
||||
|
||||
const response = await (await remote(ctx)).list(request({}))
|
||||
if (!response.ok) throw new Error('unreachable')
|
||||
const row = response.value.items.find(item => item.sessionId === session.id)
|
||||
expect(row).toBeDefined()
|
||||
expect('test/last-user' in (row?.projections?.values ?? {})).toBe(false)
|
||||
expect(apply).not.toHaveBeenCalled()
|
||||
expect(row?.projections?.values['test/last-user']).toEqual({ text: 'm0' })
|
||||
expect(row?.projections?.asOfSeq).toBe(session.seq - 1)
|
||||
expect('test/summary-excluded' in (row?.projections?.values ?? {})).toBe(false)
|
||||
expect(excludedApply).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('omits the column entirely when no registry is mounted', async () => {
|
||||
it('carries every registered summary key, which is only its own list metadata when no other unit is registered', async () => {
|
||||
const { ctx, session } = await harness(false)
|
||||
seedMessages(session, 1)
|
||||
const response = await (await remote(ctx)).list(request({}))
|
||||
if (!response.ok) throw new Error('unreachable')
|
||||
const row = response.value.items.find(item => item.sessionId === session.id)
|
||||
expect(row).toBeDefined()
|
||||
expect(row !== undefined && 'projections' in row).toBe(false)
|
||||
expect(row?.projections?.kind).toBe('sequenced')
|
||||
expect(Object.keys(row?.projections?.values ?? {})).toEqual([...ctx.sessionProjections.summaryKeys()])
|
||||
expect(Object.keys(row?.projections?.values ?? {})).toEqual(['sessionListMetadata'])
|
||||
})
|
||||
|
||||
it('serves the summary projection hints from the cache with zero log loads', async () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Session-list summary payloads: a `session.list` row carries every registered
|
||||
* wire projection except the units that declare `summary: false`
|
||||
* (`turnOutline`, `sessionStats`, `todos`, `imageLimits`), while an opened Session's
|
||||
* (`turnOutline`, `sessionStats`, `todos`, `imageLimits`, `contextPressure`,
|
||||
* `contextBreakdown`), while an opened Session's
|
||||
* `session/follow` opening snapshot keeps serving them.
|
||||
*/
|
||||
|
||||
@@ -21,9 +22,10 @@ import {
|
||||
import {
|
||||
apply as toolTodoApply, Config as toolTodoConfig, inject as toolTodoInject, name as toolTodoName,
|
||||
} from '@deepseek-ai/dsh-tool-todo'
|
||||
import TokenMeter from '@deepseek-ai/dsh-token-meter'
|
||||
import { createSessionTestRemote } from './test-remote.ts'
|
||||
|
||||
const SUMMARY_EXCLUDED_KEYS = ['turnOutline', 'sessionStats', 'todos', 'imageLimits'] as const
|
||||
const SUMMARY_EXCLUDED_KEYS = ['turnOutline', 'sessionStats', 'todos', 'imageLimits', 'contextPressure', 'contextBreakdown'] as const
|
||||
|
||||
const ownedContexts = new Set<Context>()
|
||||
afterEach(async () => {
|
||||
@@ -51,7 +53,7 @@ async function opening(
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount the registry plus the four summary-excluded units and one ordinary
|
||||
* Mount the registry plus the summary-excluded units and one ordinary
|
||||
* wire unit. The tool plugin needs a tool registry; the list path never
|
||||
* executes a tool, so a registration sink is enough.
|
||||
*/
|
||||
@@ -71,6 +73,7 @@ async function harness(): Promise<{ ctx: Context; sessionContext: Context }> {
|
||||
Config: toolTodoConfig,
|
||||
apply: toolTodoApply,
|
||||
}, { allowParallelInProgress: false })
|
||||
await ctx.plugin(TokenMeter)
|
||||
// Build the remote (and with it the list owner that registers
|
||||
// `sessionListMetadata`) before any Session exists, so every registered unit
|
||||
// seeds a cell at `session/created`.
|
||||
|
||||
@@ -413,7 +413,7 @@ async function recompose(
|
||||
// Ascending original positions put each row back where it was.
|
||||
for (const { group, options, index } of stopped.toSorted((left, right) => left.index - right.index)) {
|
||||
// `create` starts the entry but leaves the group's option list as it is; that list is what the next update
|
||||
// compares against to remove the row and what the Include writes back.
|
||||
// compares against to remove the row.
|
||||
group.data.splice(index, 0, options)
|
||||
await group.create(options)
|
||||
}
|
||||
@@ -656,10 +656,9 @@ export function renderConfigDump(
|
||||
const base = parsed as Parameters<typeof applyEntryPatches>[0]
|
||||
// snapshot_k = ONE application of layers 1..k flattened, using the exact
|
||||
// arguments boot passes for that prefix. snapshot_N is the mounted composition.
|
||||
// The patches are cloned per call: applyEntryPatches detaches the entry
|
||||
// list but pushes `insert` rows by reference from the patch list, so
|
||||
// sharing patch objects across snapshot calls would leak a later
|
||||
// snapshot's mutations into an earlier one's result.
|
||||
// The patches are cloned per call: applyEntryPatches clones entries and
|
||||
// inserted rows but assigns override values such as `config` by reference,
|
||||
// so each snapshot keeps its own copies of those values.
|
||||
const snapshot = (count: number, warnings: string[]): ReturnType<typeof applyEntryPatches> => {
|
||||
const flattened = structuredClone(layers.slice(0, count).flatMap(layer => layer.patches))
|
||||
return applyEntryPatches(base, flattened, (message: string, ...args: unknown[]) => {
|
||||
@@ -804,8 +803,8 @@ export interface FailLoudProcess {
|
||||
exit(code: number): void
|
||||
}
|
||||
|
||||
// Loader rc.5 derives and drops a rejected promise after a fiber fails. Keep
|
||||
// exact reasons already folded into the boot diagnostic visible through the
|
||||
// An activation rejection already folded into the boot diagnostic can also
|
||||
// reach the process rejection handler. Keep exact reasons visible through the
|
||||
// next process rejection checkpoint so the process guard can coalesce them.
|
||||
const assembledActivationRejections = new Map<unknown, number>()
|
||||
|
||||
|
||||
@@ -469,7 +469,10 @@ class Hmr extends Service {
|
||||
loader.exit()
|
||||
return
|
||||
}
|
||||
for (const include of includes) await include.refresh()
|
||||
for (const include of includes) {
|
||||
// A rejected refresh reports unsaved runtime changes; Include keeps its running tree.
|
||||
try { await include.refresh() } catch (error) { this.ctx.logger.warn(error) }
|
||||
}
|
||||
if (this.stashed.size > 0) {
|
||||
try { await this.partialReload() } finally { this.stashed.clear() }
|
||||
}
|
||||
|
||||
@@ -135,6 +135,31 @@ it('refreshes an Include through the queue and skips registered exact paths', as
|
||||
expect(refresh).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('continues the batch when an Include refresh reports unsaved changes', async () => {
|
||||
const { ctx, dir } = await fixture()
|
||||
const moduleWatcher = watchers.at(-1)!
|
||||
const imported = vi.spyOn(ctx.loader, 'import').mockResolvedValue(Include)
|
||||
onTestFinished(() => { imported.mockRestore() })
|
||||
const includes = []
|
||||
for (const name of ['first.yml', 'second.yml']) {
|
||||
const file = join(dir, name)
|
||||
writeFileSync(file, '[]\n')
|
||||
const id = await ctx.loader.create({ name: 'include', config: { path: pathToFileURL(file).href } })
|
||||
includes.push({ file, id })
|
||||
}
|
||||
await ctx.loader.await()
|
||||
const [first, second] = includes.map(({ id }) => ctx.loader.resolve(id).subtree as Include)
|
||||
const failure = new Error('config file has 1 unsaved change(s)')
|
||||
const rejected = vi.spyOn(first!, 'refresh').mockRejectedValue(failure)
|
||||
const refreshed = vi.spyOn(second!, 'refresh')
|
||||
const warn = vi.spyOn(ctx.logger, 'warn')
|
||||
moduleWatcher.emit('change', includes[0]!.file)
|
||||
moduleWatcher.emit('change', includes[1]!.file)
|
||||
await vi.waitFor(() => { expect(refreshed).toHaveBeenCalledOnce() })
|
||||
expect(rejected).toHaveBeenCalledOnce()
|
||||
expect(warn).toHaveBeenCalledWith(failure)
|
||||
})
|
||||
|
||||
it('queues cached module replacements behind configuration mutations and recovers after failure', async () => {
|
||||
const { ctx, dir, hmr } = await fixture()
|
||||
const file = join(dir, 'source.mjs')
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
name: '@deepseek-ai/dsh-tool-fs'
|
||||
config:
|
||||
# Set false to drop the apply_patch tool beside edit.
|
||||
applyPatch: true
|
||||
applyPatch: false
|
||||
|
||||
- id: tool-fs-search
|
||||
name: '@deepseek-ai/dsh-tool-fs-search'
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
name: '@deepseek-ai/dsh-tool-fs'
|
||||
config:
|
||||
# Set false to drop the apply_patch tool beside edit.
|
||||
applyPatch: true
|
||||
applyPatch: false
|
||||
- id: tool-fs-search
|
||||
name: '@deepseek-ai/dsh-tool-fs-search'
|
||||
config:
|
||||
|
||||
@@ -300,30 +300,30 @@ describe('PluginManagerPage', () => {
|
||||
})
|
||||
|
||||
it('keeps the Installed tab panel through loading and the More list out of the tabs, and returns a More plugin opened from Discover to Discover', () => {
|
||||
const worktree = '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
const inspector = '@deepseek-ai/dsh-experimental-inspector-profile'
|
||||
const { navigation, set } = renderTab({ status: 'loading', packages: [] }, {}, {
|
||||
'plugins.tab:discover': (_view, owner) => (
|
||||
<button type="button" onClick={() => { (owner as PluginTabProps).openPackage(worktree) }}>Discover open</button>
|
||||
<button type="button" onClick={() => { (owner as PluginTabProps).openPackage(inspector) }}>Discover open</button>
|
||||
),
|
||||
}, {}, [{ id: 'discover', label: 'Discover' }])
|
||||
const installed = screen.getByRole('tab', { name: en.installedTab })
|
||||
expect(document.getElementById(installed.getAttribute('aria-controls') as string)?.getAttribute('role')).toBe('tabpanel')
|
||||
set({ status: 'ready', packages: [] })
|
||||
expect(screen.getByRole('tabpanel', { name: en.installedTab }).textContent).toBe(en.empty)
|
||||
set({ packages: [pkg({ name: worktree, official: true, optional: true, installed: false, enabled: false })] })
|
||||
set({ packages: [pkg({ name: inspector, official: true, optional: true, installed: false, enabled: false })] })
|
||||
act(() => { navigation.actions.setView({ kind: 'more' }) })
|
||||
expect(screen.queryByRole('tablist')).toBeNull()
|
||||
expect(screen.queryByRole('tabpanel')).toBeNull()
|
||||
act(() => { navigation.actions.setView({ kind: 'list' }) })
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Discover' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Discover open' }))
|
||||
expect(document.querySelector(`[data-plugin-detail="${worktree}"]`)).not.toBeNull()
|
||||
expect(document.querySelector(`[data-plugin-detail="${inspector}"]`)).not.toBeNull()
|
||||
// An official plugin of the More list opened from Discover returns to Discover, not to the More list.
|
||||
fireEvent.click(screen.getByRole('button', { name: en.backToList }))
|
||||
expect(navigation.getSnapshot()).toEqual({ tab: 'discover', view: { kind: 'list' } })
|
||||
fireEvent.click(screen.getByRole('tab', { name: en.installedTab }))
|
||||
act(() => { navigation.actions.setView({ kind: 'more' }) })
|
||||
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', worktree) }))
|
||||
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', inspector) }))
|
||||
fireEvent.click(screen.getByRole('button', { name: en.backToPackage.replace('{name}', en.moreTitle) }))
|
||||
expect(screen.getByRole('heading', { name: en.moreTitle, level: 1 })).toBeTruthy()
|
||||
})
|
||||
@@ -1237,7 +1237,7 @@ describe('PluginManagerPage', () => {
|
||||
.toEqual(['more'])
|
||||
const more = document.querySelector('[data-plugin-group="more"]') as HTMLElement
|
||||
expect(within(more).getAllByRole('listitem').map(card => card.getAttribute('data-plugin-package')))
|
||||
.toEqual([team, '@acme/new-official', '@deepseek-ai/dsh-experimental-tool-worktree', '@deepseek-ai/dsh-experimental-badge-skill-bundle', '@deepseek-ai/dsh-subagent-codex', '@deepseek-ai/dsh-subagent-claude-code'])
|
||||
.toEqual([team, '@acme/new-official', '@deepseek-ai/dsh-experimental-badge-skill-bundle', '@deepseek-ai/dsh-subagent-codex', '@deepseek-ai/dsh-subagent-claude-code'])
|
||||
expect(document.querySelector('[data-plugin-package="dsh-better-sidebar"]')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', team) }))
|
||||
fireEvent.click(screen.getByRole('button', { name: en.backToPackage.replace('{name}', en.moreTitle) }))
|
||||
@@ -1255,6 +1255,20 @@ describe('PluginManagerPage', () => {
|
||||
expect(screen.getByRole('heading', { name: en.basicTitle })).toBeTruthy()
|
||||
})
|
||||
|
||||
it.each([false, true])('shows one forge card and hides the standalone worktree card when enabled is %s', (enabled) => {
|
||||
const forge = '@deepseek-ai/dsh-experimental-forge-bundle'
|
||||
const worktree = '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
renderTab({ packages: [
|
||||
pkg({ name: forge, official: true, enabled }),
|
||||
pkg({ name: worktree, official: true, enabled }),
|
||||
] })
|
||||
expect(document.querySelectorAll(`[data-plugin-package="${forge}"]`)).toHaveLength(1)
|
||||
expect(document.querySelector(`[data-plugin-package="${worktree}"]`)).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: en.moreAction }))
|
||||
expect(document.querySelectorAll(`[data-plugin-package="${forge}"]`)).toHaveLength(1)
|
||||
expect(document.querySelector(`[data-plugin-package="${worktree}"]`)).toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
'@deepseek-ai/dsh-experimental-session-search',
|
||||
'@deepseek-ai/dsh-experimental-ralph-bundle',
|
||||
@@ -1360,7 +1374,7 @@ describe('PluginManagerPage', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
'@deepseek-ai/dsh-experimental-tool-worktree',
|
||||
'@deepseek-ai/dsh-experimental-inspector-profile',
|
||||
'@deepseek-ai/dsh-personalized-recommendations',
|
||||
])('pins enabled %s onto the main list and removes it when disabled', (first) => {
|
||||
const team = '@deepseek-ai/dsh-experimental-voice-input-bundle'
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 405b30feaf48c087
|
||||
/deepseek-ai-dsh-client-ui-schedule/use-this-package:
|
||||
en: 928ae699661089c5
|
||||
zh: 9895b75c5a30771b
|
||||
en: 5cd34ed47dea2d59
|
||||
zh: c77b078e9e2c2197
|
||||
/deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source:
|
||||
en: 4005b800e930f581
|
||||
zh: ccb9318dad7f7e8f
|
||||
@@ -27,8 +27,8 @@
|
||||
en: 2fd968202916b197
|
||||
zh: f1ab66194846562b
|
||||
/deepseek-ai-dsh-client-ui-schedule/model-experience:
|
||||
en: 064b67705e5da4ca
|
||||
zh: 2163217c14878c59
|
||||
en: 6f528a72fe45af1b
|
||||
zh: 8272529f6e324315
|
||||
/deepseek-ai-dsh-client-ui-schedule/model-experience/kv-cache-effect:
|
||||
en: 52239e5688dfebe1
|
||||
zh: 37343f5dbde010e8
|
||||
|
||||
@@ -27,7 +27,7 @@ The Automation tasks page and right Sidebar task tab use the same editor for tas
|
||||
|
||||
Below 768px, the task list uses 20px horizontal insets, matching the Plugins page.
|
||||
|
||||
The shipped Web composition mounts `ui-schedule` with Host Schedule. The Automation tasks sidebar entry opens the task list without selecting or activating a Session. New retains its existing behavior: open a new conversation and let the user enter an instruction. Its adjacent menu offers Create in conversation and Create manually. Manual creation opens the same editor as an existing task, validates required fields, and saves a task without starting an Agent or sending the instruction. A failed request leaves the form and its values visible. The empty state shows a decorative document-and-clock illustration whose colors follow the theme.
|
||||
The shipped Web composition mounts `ui-schedule` with Host Schedule. The Automation tasks sidebar entry opens the task list without selecting or activating a Session. New and the adjacent menu's Create in a session action both open a new conversation with a short task-creation prompt in the current UI language, ready for the user to complete and send. When a blank conversation is reused, the prompt is appended on a new line, preserving its text, inline references, and attachments. The same menu offers Create manually. Manual creation opens the same editor as an existing task, validates required fields, and saves a task without starting an Agent or sending the instruction. A failed request leaves the form and its values visible. The empty state shows a decorative document-and-clock illustration whose colors follow the theme.
|
||||
|
||||
Enabled, Paused, Ended, and All filters combine with text search over the title, prompt, and linked Session id; the page starts on Enabled. The search-row menu clears all ended tasks after confirmation, preserving active and paused tasks. Selecting a task opens its editable Rules / Records page. Save is enabled only for valid, meaningful changes and sends only changed fields with `expectedUpdatedAt`, or `null` when no stored revision exists. The fields stay editable after saving. Unsaved drafts survive navigation between tasks and away from the page. A conflict retains the whole draft and disables Save. Review with my edits reapplies the changed field groups to the latest values for review before saving; Discard replaces the draft with the latest stored values. For a new task, Discard closes the editor. Late responses cannot change another task's detail. Coarse-pointer search, instruction, and timing fields use 16px text to prevent focus zoom on iOS Safari.
|
||||
|
||||
@@ -72,7 +72,7 @@ Sidebar layout records omit opener parameters. `TaskTabBindings` therefore persi
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
None, as this browser package registers no model tools or messages.
|
||||
None, as this browser package registers no model tools or automatic messages; the creation prompt stays an unsent draft.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ kind: "package-reference"
|
||||
|
||||
宽度小于 768px 时,任务列表使用 20px 的左右内边距,与插件页一致。
|
||||
|
||||
发布的 Web 组合将 `ui-schedule` 与宿主 Schedule 一起挂载。自动化任务侧栏入口打开任务列表,不选择或激活会话。“新建”保留现有行为:进入新会话,由用户输入指令。相邻下拉提供“在对话中创建”和“手动创建”。手动创建与已有任务打开同一编辑页,校验必填项并保存任务,不启动 Agent 或发送指令。请求失败时保留表单和已填内容。空态显示装饰性的文档与时钟插图,其颜色随主题变化。
|
||||
发布的 Web 组合将 `ui-schedule` 与宿主 Schedule 一起挂载。自动化任务侧栏入口打开任务列表,不选择或激活会话。“新建”和相邻菜单的“在会话中创建”都会进入新会话,按当前界面语言预填简短的任务创建提示,供用户补充并发送。若复用空白会话,提示会另起一行追加,保留原有文本、行内引用和附件。该菜单还提供“手动创建”。手动创建与已有任务打开同一编辑页,校验必填项并保存任务,不启动 Agent 或发送指令。请求失败时保留表单和已填内容。空态显示装饰性的文档与时钟插图,其颜色随主题变化。
|
||||
|
||||
“已开启”“已暂停”“已结束”“全部”筛选结合标题、指令和关联会话 id 搜索,默认显示已开启任务。搜索行菜单在确认后清除全部已结束任务,保留已开启和已暂停任务。选择任务直接打开可编辑的“规则 / 任务运行记录”页面。只有字段有效且有实际修改时才能保存;保存只发送变更字段及 `expectedUpdatedAt`,存储版本缺失时发送 `null`。保存后字段保持可编辑。切换任务或离开页面时保留未保存的草稿。冲突保留整份草稿并禁用保存。“保留修改并复核”将改过的字段组应用到最新值,供保存前复核;“放弃”用最新存储值替换草稿。对于新任务,“放弃”关闭编辑页。迟到响应不会改变其他任务详情。粗指针设备的搜索、指令和时间字段使用 16px 字号,避免 iOS Safari 聚焦时缩放。
|
||||
|
||||
@@ -72,7 +72,7 @@ Peer 可读取 `schedule/catalog` 时,插件注册现有 `schedules` 主面板
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
无:此浏览器包不注册模型工具或消息,Schedule 拥有提醒投递。
|
||||
无:本浏览器包不注册模型工具或自动消息;创建提示保留为未发送的草稿。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ export function apply(ctx: ClientContext): void {
|
||||
store: createEditorStore(),
|
||||
inject: (): TaskManagerInjected => ({
|
||||
...detail,
|
||||
onNewTask: () => { ctx.uiWorkspace.startSession() },
|
||||
onNewTask: () => { ctx.uiWorkspace.startSession(undefined, { prompt: t('host.createPrompt'), whenDraftExists: 'append' }) },
|
||||
onClearEnded: async () => {
|
||||
const outcome = await clearEnded()
|
||||
deleteToast.reportClear(outcome)
|
||||
|
||||
@@ -6,6 +6,7 @@ export const en = {
|
||||
'host.create': 'Create',
|
||||
'host.createMethod': 'Choose a creation method',
|
||||
'host.chatCreate': 'Create in a session',
|
||||
'host.createPrompt': 'Please create an automation task based on the requirements.',
|
||||
'host.manualCreate': 'Create manually',
|
||||
'host.closeCreate': 'Close creation form',
|
||||
'host.creating': 'Creating…',
|
||||
@@ -164,6 +165,7 @@ export const zh: Record<TaskManagerKey, string> = {
|
||||
'host.create': '创建',
|
||||
'host.createMethod': '选择创建方式',
|
||||
'host.chatCreate': '在会话中创建',
|
||||
'host.createPrompt': '请根据需求创建一个自动化任务。',
|
||||
'host.manualCreate': '手动创建',
|
||||
'host.closeCreate': '关闭创建页',
|
||||
'host.creating': '创建中…',
|
||||
@@ -318,6 +320,7 @@ export const ja = {
|
||||
'host.create': '作成',
|
||||
'host.createMethod': '作成方法を選択',
|
||||
'host.chatCreate': '会話で作成',
|
||||
'host.createPrompt': '要件に基づいて自動化タスクを作成してください。',
|
||||
'host.manualCreate': '手動で作成',
|
||||
'host.closeCreate': '作成フォームを閉じる',
|
||||
'host.creating': '作成中…',
|
||||
@@ -573,6 +576,7 @@ export const es = {
|
||||
'card.openLabel': 'Abrir detalles de la tarea: {title}',
|
||||
'card.deleted': 'Eliminado',
|
||||
'host.chatCreate': 'Crear en una conversación',
|
||||
'host.createPrompt': 'Crea una tarea automatizada según los requisitos.',
|
||||
'host.conflict': 'Esta tarea cambió en otro lugar. Revisa los valores actuales con tus cambios o descártalos.',
|
||||
'host.reapply': 'Revisar con mis cambios',
|
||||
'host.create': 'Crear',
|
||||
@@ -726,6 +730,7 @@ export const fr = {
|
||||
'card.openLabel': 'Détails de la tâche ouverte : {title}',
|
||||
'card.deleted': 'Supprimé',
|
||||
'host.chatCreate': 'Créer dans une conversation',
|
||||
'host.createPrompt': 'Crée une tâche automatisée selon les exigences.',
|
||||
'host.conflict': 'Cette tâche a été modifiée ailleurs. Vérifiez les valeurs actuelles avec vos modifications, ou abandonnez-les.',
|
||||
'host.reapply': 'Vérifier avec mes modifications',
|
||||
'host.create': 'Créer',
|
||||
@@ -879,6 +884,7 @@ export const de = {
|
||||
'card.openLabel': 'Aufgabendetails öffnen: {title}',
|
||||
'card.deleted': 'Gelöscht',
|
||||
'host.chatCreate': 'In einer Unterhaltung erstellen',
|
||||
'host.createPrompt': 'Erstelle eine automatisierte Aufgabe anhand der Anforderungen.',
|
||||
'host.conflict': 'Diese Aufgabe wurde andernorts geändert. Prüfe die aktuellen Werte mit deinen Änderungen oder verwirf sie.',
|
||||
'host.reapply': 'Mit meinen Änderungen prüfen',
|
||||
'host.create': 'Erstellen',
|
||||
@@ -1032,6 +1038,7 @@ export const ru = {
|
||||
'card.openLabel': 'Открыть сведения о задаче: {title}.',
|
||||
'card.deleted': 'Удалено',
|
||||
'host.chatCreate': 'Создать в беседе',
|
||||
'host.createPrompt': 'Создай автоматизированную задачу на основе требований.',
|
||||
'host.conflict': 'Задача изменена в другом месте. Проверьте актуальные значения со своими правками или отмените изменения.',
|
||||
'host.reapply': 'Проверить со своими правками',
|
||||
'host.create': 'Создать',
|
||||
@@ -1185,6 +1192,7 @@ export const pt = {
|
||||
'card.openLabel': 'Detalhes da tarefa aberta: {title}',
|
||||
'card.deleted': 'Excluído',
|
||||
'host.chatCreate': 'Criar em uma conversa',
|
||||
'host.createPrompt': 'Crie uma tarefa automatizada com base nos requisitos.',
|
||||
'host.conflict': 'Esta tarefa foi alterada em outro lugar. Revise os valores atuais com suas alterações ou descarte-as.',
|
||||
'host.reapply': 'Revisar com minhas alterações',
|
||||
'host.create': 'Criar',
|
||||
|
||||
@@ -28,6 +28,7 @@ import { SessionScheduleHover } from '../src/client/SessionScheduleHover.tsx'
|
||||
import { SessionScheduleMark } from '../src/client/SessionScheduleMark.tsx'
|
||||
import { ScheduleTurnCard, type ScheduleTurnCardInjected } from '../src/client/ScheduleTurnCard.tsx'
|
||||
import { en, NS, zh } from '../src/client/locales.ts'
|
||||
import * as managerLocales from '../src/client/task-manager-locales.ts'
|
||||
|
||||
const Empty = () => null
|
||||
|
||||
@@ -425,8 +426,16 @@ describe('ui-schedule browser half', () => {
|
||||
expect(chip.taskBindings).toBe(body.taskBindings)
|
||||
expect(chip.hooks.catalog).toBe(body.hooks.catalog)
|
||||
const panel = injectedFace(ctx.slots.entries('main')[0]!, undefined as never) as TaskManagerInjected
|
||||
panel.onNewTask()
|
||||
expect(startSession).toHaveBeenCalledOnce()
|
||||
expect(Object.keys(managerLocales).sort()).toEqual(['de', 'en', 'es', 'fr', 'ja', 'pt', 'ru', 'zh'])
|
||||
for (const [language, dictionary] of Object.entries(managerLocales)) {
|
||||
expect(dictionary['host.createPrompt']).not.toContain('\n')
|
||||
ctx.locale.setLocale(language)
|
||||
panel.onNewTask()
|
||||
expect(startSession).toHaveBeenLastCalledWith(undefined, {
|
||||
prompt: dictionary['host.createPrompt'], whenDraftExists: 'append',
|
||||
})
|
||||
}
|
||||
expect(startSession).toHaveBeenCalledTimes(8)
|
||||
} finally {
|
||||
await fiber.dispose()
|
||||
owner()
|
||||
|
||||
@@ -455,7 +455,7 @@ describe('time-context projection fold edges', () => {
|
||||
expect(contextTexts(cold).at(-1)).toContain('mixed ["America/New_York","Asia/Shanghai"]')
|
||||
})
|
||||
|
||||
it('rebuilds an old checkpoint from the full journal and removes the unit on unload', async () => {
|
||||
it('rebuilds an old checkpoint from the full journal and freezes the unit on unload', async () => {
|
||||
const { ctx, fiber } = await mount()
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
const session = Session.create(SessionId('browser-old-row'))
|
||||
@@ -474,8 +474,12 @@ describe('time-context projection fold edges', () => {
|
||||
expect(ctx.sessionProjections.checkpoint(cold).timeContext).toEqual({
|
||||
ver: 3, seq: cold.seq - 1, val: rows.timeContext!.val,
|
||||
})
|
||||
const row = ctx.sessionProjections.checkpoint(cold).timeContext
|
||||
await fiber.dispose()
|
||||
expect(ctx.sessionProjections.checkpoint(cold).timeContext).toBeUndefined()
|
||||
openMessageTurn(cold, 2, 'UTC')
|
||||
// The released unit keeps the hydrated row and folds no event after the unload.
|
||||
expect(ctx.sessionProjections.checkpoint(cold)).toHaveProperty('timeContext', row)
|
||||
expect(ctx.sessionProjections.checkpoint(Session.create(SessionId('browser-after-unload')))).not.toHaveProperty('timeContext')
|
||||
})
|
||||
|
||||
it('retains reported invalid zones without validating rejected or throttled requests', async () => {
|
||||
|
||||
@@ -107,9 +107,11 @@ describe('ReactLoopInbox', () => {
|
||||
'next-turn': [pending],
|
||||
'next-step': [],
|
||||
})
|
||||
const row = ctx.sessionProjections.checkpoint(session).inbox
|
||||
|
||||
await projection.dispose()
|
||||
expect(ctx.sessionProjections.checkpoint(session).inbox?.val).toBeUndefined()
|
||||
expect(ctx.sessionProjections.checkpoint(session)).toHaveProperty('inbox', row)
|
||||
expect(ctx.sessionProjections.checkpoint(ctx.sessions.create())).not.toHaveProperty('inbox')
|
||||
expect(first.nextTurn).toEqual([pending])
|
||||
first.clear()
|
||||
expect(second.nextTurn).toEqual([])
|
||||
|
||||
@@ -71,7 +71,9 @@ it.each([0, 1, 2])('retains Agent resources until all work exits with %i failed
|
||||
expect(scopeDisposedBeforeWorkExit).toBe(false)
|
||||
expect(scopeDisposed).toBe(true)
|
||||
expect(abortHandled).toBe(1)
|
||||
expect(projections.checkpoint(agent.session).inbox).toBeUndefined()
|
||||
// The released inbox unit keeps this Session's last row; a Session created after the unload gets none.
|
||||
expect(projections.checkpoint(agent.session)).toHaveProperty('inbox')
|
||||
expect(projections.checkpoint(ctx.sessions.create())).not.toHaveProperty('inbox')
|
||||
expect(projections.checkpoint(agent.session).turnBoundary).toBeDefined()
|
||||
expect(ctx.agents.get(agent.id)).toBeUndefined()
|
||||
expect(ctx.sessions.get(agent.session.id)).toBeUndefined()
|
||||
|
||||
@@ -306,8 +306,14 @@ describe('agent scope lifecycle', () => {
|
||||
expect(ctx.sessionProjections.checkpoint(second.agent.session).inbox?.val).toBeDefined()
|
||||
|
||||
await Promise.all([first.dispose(), second.dispose()])
|
||||
const row = ctx.sessionProjections.checkpoint(second.agent.session).inbox
|
||||
const coldRow = ctx.sessionProjections.checkpoint(cold).inbox
|
||||
await loopFiber.dispose()
|
||||
expect(ctx.sessionProjections.checkpoint(second.agent.session).inbox?.val).toBeUndefined()
|
||||
cold.append('turn/start', { turn: 1 })
|
||||
// The released unit keeps each Session's last row and folds no event after the unload.
|
||||
expect(ctx.sessionProjections.checkpoint(second.agent.session)).toHaveProperty('inbox', row)
|
||||
expect(ctx.sessionProjections.checkpoint(cold)).toHaveProperty('inbox', coldRow)
|
||||
expect(ctx.sessionProjections.checkpoint(ctx.sessions.create(SessionId('projection-after-unload')))).not.toHaveProperty('inbox')
|
||||
})
|
||||
|
||||
it('agent.ctx listeners hear only their own agent (scoped dispatch end to end)', async () => {
|
||||
|
||||
@@ -61,9 +61,13 @@ describe('AgentRegistry', () => {
|
||||
})
|
||||
session.append('turn/end', { turn: 3, reason: { kind: 'completed' } })
|
||||
expect(ctx.agents.turnBoundaryOf(session)).toMatchObject({ openTurnStartSeq: null, lastTurn: 3 })
|
||||
expect(ctx.sessionProjections.checkpoint(session).turnBoundary).toBeDefined()
|
||||
const row = ctx.sessionProjections.checkpoint(session).turnBoundary
|
||||
expect(row).toBeDefined()
|
||||
await fiber.dispose()
|
||||
expect(ctx.sessionProjections.checkpoint(session).turnBoundary).toBeUndefined()
|
||||
session.append('turn/start', { turn: 4 })
|
||||
// The released unit keeps its last row and folds no event after the unload.
|
||||
expect(ctx.sessionProjections.checkpoint(session)).toHaveProperty('turnBoundary', row)
|
||||
expect(ctx.sessionProjections.checkpoint(Session.create(SessionId('turn-positions-after-unload')))).not.toHaveProperty('turnBoundary')
|
||||
})
|
||||
|
||||
it('contributes Agent lookup and scoped Context providers while Typert is live', async () => {
|
||||
|
||||
@@ -1671,11 +1671,13 @@ describe('forgePanel lifecycle', () => {
|
||||
const opened = await open(repo, {}, { worktrees: true })
|
||||
await opened.ctx.forgePanel.requestWorktree(opened.session, null, signal)
|
||||
const proposed = prompt('go')
|
||||
const row = opened.ctx.sessionProjections.checkpoint(opened.session).forgeWorktree
|
||||
|
||||
await opened.panel.dispose()
|
||||
|
||||
expect(opened.ctx.get('forgePanel')).toBeUndefined()
|
||||
expect(opened.ctx.sessionProjections.checkpoint(opened.session)).not.toHaveProperty('forgeWorktree')
|
||||
expect(opened.ctx.sessionProjections.checkpoint(opened.session)).toHaveProperty('forgeWorktree', row)
|
||||
expect(opened.ctx.sessionProjections.checkpoint(opened.ctx.sessions.create())).not.toHaveProperty('forgeWorktree')
|
||||
sendPrompt(opened.agent, 'go')
|
||||
await expect(proposeStep(opened.ctx, opened.agent, { turn: 1, step: 1, messages: [proposed] })).resolves.toEqual({ kind: 'enter', messages: [proposed] })
|
||||
expect(opened.ctx.workingDirectory.get(opened.session)).toBe(repo)
|
||||
|
||||
@@ -9,26 +9,26 @@
|
||||
en: 567570af44a77fa2
|
||||
zh: 3d7722fe3ade65d6
|
||||
/deepseek-ai-dsh-experimental-forge-bundle/summary:
|
||||
en: f6ce7db50e10af0b
|
||||
zh: 415f509876e88d8a
|
||||
en: 8d8189bb9a91f049
|
||||
zh: 27f02215d6919b3d
|
||||
/deepseek-ai-dsh-experimental-forge-bundle/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: 2f8a621b991260a1
|
||||
/deepseek-ai-dsh-experimental-forge-bundle/use-this-package:
|
||||
en: 227f8ec88513be9e
|
||||
zh: be16cbbb4f3912e2
|
||||
en: 33679bed06be4e74
|
||||
zh: 77dcfac9570d1fbc
|
||||
/deepseek-ai-dsh-experimental-forge-bundle/understand-the-implementation:
|
||||
en: 6effcbf40264388b
|
||||
zh: b4f093955671c650
|
||||
en: 5cb4136ef2058890
|
||||
zh: 5142828996eef6c7
|
||||
/deepseek-ai-dsh-experimental-forge-bundle/further-exploration:
|
||||
en: ab1f524b8ad79c55
|
||||
zh: 137cfcb6d192c269
|
||||
/deepseek-ai-dsh-experimental-forge-bundle/model-experience:
|
||||
en: 0e8d555c9ba51844
|
||||
zh: 1aeda456da13a9bd
|
||||
en: 48e871ea5f1238c8
|
||||
zh: 6d40dddf8de9d610
|
||||
/deepseek-ai-dsh-experimental-forge-bundle/model-experience/kv-cache-effect:
|
||||
en: 06c8b68589e27fc7
|
||||
zh: 092fd7649b34bc04
|
||||
en: 0192ab064b8000a7
|
||||
zh: 1a1de04f87631b9a
|
||||
/deepseek-ai-dsh-experimental-forge-bundle/known-limitations-and-deferred-work:
|
||||
en: ab489084b24ecb16
|
||||
zh: 6be1686e9741dffa
|
||||
|
||||
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Show the change requests of each Session with their checks and reviews, and let a new Session start in its own Git worktree. Enable Change requests & worktrees on the Web or Desktop Plugins page; shipped profiles leave it disabled. GitHub status comes from the `gh` CLI signed in on the Host; without it, the panel still shows the branch and the worktree option still works.
|
||||
Review each Session's change requests, checks, and diffs, start a new Session in a Git worktree, or ask the model to create one during a conversation. Enable Change requests & worktrees on the Web or Desktop Plugins page; shipped profiles leave it disabled. GitHub status comes from the Host's signed-in `gh` CLI; local worktree creation does not require GitHub authentication.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -27,6 +27,8 @@ Show the change requests of each Session with their checks and reviews, and let
|
||||
|
||||
Open Plugins and enable Change requests & worktrees in the Official group. In a Git workspace, check Worktree in the composer before the first message to start the Session in a new worktree. In a Session whose directory is inside a Git checkout, the header button opens the panel. GitHub pull requests that the Session creates with `gh pr create`, opens with `gh pr view`, `gh pr edit`, or `gh pr checkout`, or works on as its branch or updates with a later `git push`, appear as change requests without further steps. Run `gh auth login --hostname <host>` in a terminal to sign in; the panel shows the command when sign-in is missing. The [panel package](../client-ui-forge/README.md#use-this-package) describes each control.
|
||||
|
||||
The same switch enables the model's [`create_worktree`](../tool-worktree/README.md) tool. Turning it off removes both the panel and the tool while retaining existing checkouts and branches. For profiles that already selected the standalone worktree bundle, follow the [upgrade guide](../../../docs/upgrade-guide/v0.2.2-alpha.2/forge-worktree-entry/guide.md) before enabling this bundle.
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
@@ -35,7 +37,7 @@ Open Plugins and enable Change requests & worktrees in the Official group. In a
|
||||
<details>
|
||||
<summary>Maintainer details — click to expand</summary>
|
||||
|
||||
The bundle patch inserts the forge registry, the GitHub provider, and the Session bindings as Host rows, and a group that isolates the `worktrees` service, holding a worktree service row and the panel's two-face package, whose Client half the Web and Desktop clients load while the row is enabled. The isolation lets the bundle coexist with the Git Worktrees bundle, whose model tool keeps its own service. The rows need the Session projection registry, working-directory, filesystem, subprocess, and sandbox services that every base-backed profile composes.
|
||||
The bundle mounts the forge registry, GitHub provider, Session bindings, and the worktree model tool. The tool uses the top-level `worktree` service; the panel keeps its isolated `forge-worktree` service inside `optional-forge-panel`. Their configuration ids and creation flows stay independent. The panel records worktrees requested before a Session starts; model-created worktrees do not acquire the panel's recovery records. The rows use the Session projection, working-directory, filesystem, subprocess, and sandbox services from base-backed profiles.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -51,11 +53,11 @@ The bundle patch inserts the forge registry, the GitHub provider, and the Sessio
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
None, as the composed packages add no model tool or prompt; the directory notice of a Session that starts in a worktree reaches the model through the working-directory service, as described by the [panel](../client-ui-forge/README.md#model-experience).
|
||||
Indirectly, through the [`create_worktree`](../tool-worktree/README.md#model-experience) tool and the [panel’s directory notice](../client-ui-forge/README.md#model-experience) for Sessions that start in a worktree.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None from the bundle; see the [panel](../client-ui-forge/README.md#model-experience) for the worktree notice.
|
||||
Enabling or disabling the bundle changes the model's tool schema list. Calls append tool results and directory context; see the [tool's cache behavior](../tool-worktree/README.md#model-experience).
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-bundle"
|
||||
|
||||
## 概述
|
||||
|
||||
展示每个 Session 的变更请求及其检查与评审状态,并让新 Session 在自己的 Git worktree 中开始。在 Web 或 Desktop 的插件页面启用“变更请求与工作树”;发布的 profile 默认禁用该功能。GitHub 状态来自宿主上已登录的 `gh` 命令行;没有它时,面板仍会显示分支,工作树选项也照常可用。
|
||||
查看每个 Session 的变更请求、检查与代码差异,在 Git 工作树中开始新 Session,或在对话中让模型创建工作树。在 Web 或 Desktop 的插件页面启用“变更请求与工作树”;发布的 profile 默认禁用该功能。GitHub 状态来自宿主上已登录的 `gh` 命令行;创建本地工作树无需 GitHub 认证。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -27,6 +27,8 @@ kind: "package-bundle"
|
||||
|
||||
打开插件页面,在官方分组中启用“变更请求与工作树”。在 Git 工作区中,发出第一条消息之前在输入框中勾选“工作树”,Session 就会在新的 worktree 中开始。Session 的目录位于 Git 检出中时,标题栏按钮会打开面板。Session 用 `gh pr create` 创建、用 `gh pr view`、`gh pr edit` 或 `gh pr checkout` 打开的 GitHub pull request,以及 Session 所在分支上或之后 `git push` 更新的 pull request,无需额外操作即会作为变更请求出现。在终端运行 `gh auth login --hostname <host>` 登录;缺少登录时,面板会显示这条命令。[面板包](../client-ui-forge/README.zh.md#use-this-package)说明了每个控件。
|
||||
|
||||
同一个开关也启用模型的 [`create_worktree`](../tool-worktree/README.zh.md) 工具。关闭后面板和工具一起移除,已有检出目录与分支仍然保留。已选中独立工作树组合包的 profile,请在启用本组合包前按[升级指南](../../../docs/upgrade-guide/v0.2.2-alpha.2/forge-worktree-entry/guide.zh.md)调整配置。
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
@@ -35,7 +37,7 @@ kind: "package-bundle"
|
||||
<details>
|
||||
<summary>维护者细节——点击展开</summary>
|
||||
|
||||
Bundle patch 以宿主行插入 forge 注册表、GitHub 提供方与 Session 绑定,并插入一个隔离 `worktrees` 服务的分组,其中包含一个 worktree 服务行与面板的双端包;该行启用期间,Web 与 Desktop 客户端会加载其客户端部分。隔离让本套件可以与 Git Worktrees 套件共存,后者的模型工具保有自己的服务。这些行依赖每个基于 base 的 profile 都会组合的 Session 投影注册表、工作目录、文件系统、子进程与沙箱服务。
|
||||
组合包挂载 Forge 注册表、GitHub 提供方、Session 绑定和工作树模型工具。工具使用顶层 `worktree` 服务;面板在 `optional-forge-panel` 内保留隔离的 `forge-worktree` 服务。两者的配置 id 和创建流程各自独立。面板记录 Session 开始前请求的工作树;模型创建的工作树不会获得面板的恢复记录。这些行使用基于 base 的 profile 提供的 Session 投影、工作目录、文件系统、子进程和沙箱服务。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -51,11 +53,11 @@ Bundle patch 以宿主行插入 forge 注册表、GitHub 提供方与 Session
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
无:组合的包不增加模型工具或提示词;以 worktree 开始的 Session 的目录通知通过工作目录服务告知模型,详见[面板](../client-ui-forge/README.zh.md#model-experience)。
|
||||
间接影响:通过 [`create_worktree`](../tool-worktree/README.zh.md#model-experience) 工具,以及以工作树开始的 Session 的[面板目录通知](../client-ui-forge/README.zh.md#model-experience)。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
Bundle 本身没有影响;worktree 通知见[面板](../client-ui-forge/README.zh.md#model-experience)。
|
||||
启用或关闭组合包会改变模型的工具 schema 列表。调用追加工具结果和目录上下文;参见[工具的缓存行为](../tool-worktree/README.zh.md#model-experience)。
|
||||
|
||||
## 已知限制与待办工作
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
- insert:
|
||||
- id: worktree
|
||||
name: '@deepseek-ai/dsh-experimental-worktree'
|
||||
- id: tool-worktree
|
||||
name: '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
- id: forge
|
||||
name: '@deepseek-ai/dsh-experimental-forge'
|
||||
- id: forge-github
|
||||
|
||||
@@ -0,0 +1 @@
|
||||

|
||||
@@ -0,0 +1 @@
|
||||

|
||||
@@ -0,0 +1 @@
|
||||

|
||||
@@ -0,0 +1 @@
|
||||

|
||||
@@ -0,0 +1 @@
|
||||

|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user