diff --git a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml index 24f76c7cb0..f391fe518e 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml @@ -6,14 +6,14 @@ en: 41f30d473ff53ce2 zh: 6851c4414f5331c1 /agent-note-session-projections-as-a-required-reader-seam/problem: - en: c0c779c717cad546 - zh: 5d2916ac86bf5d84 + en: fca00eeb7a8d9abd + zh: d7a9766f5322dff9 /agent-note-session-projections-as-a-required-reader-seam/decision: - en: a1c50d2f43334a88 - zh: d7736994733454b4 + en: dd550c8f4c2aa263 + zh: 9e60b0e465697e77 /agent-note-session-projections-as-a-required-reader-seam/alternatives-considered: en: aea3da939c90d240 zh: 21e91c481cdd23b7 /agent-note-session-projections-as-a-required-reader-seam/consequences: - en: 73325513ec70fbb2 - zh: 610bf34b9f9db420 + en: ab45eb87f7c22cdb + zh: e65a97991a45164f diff --git a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md index e1c6835311..5ab32c0b2c 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md +++ b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md @@ -6,19 +6,17 @@ English | [中文](2026-08-19-session-projection-mandatory-seam.zh.md) ## Problem -An optional projection registry lets a plugin whose host behavior reads projected state activate without that state. Unless the reader rejects the missing registry or key, the host behavior or subagent catalog fields can silently disappear. Batch-only reads also materialize every client view when a consumer needs one host value. Some contributor sites intentionally retain optional `ctx.inject` registration, so their readers need an explicit missing-state rule. +An optional projection registry lets a plugin whose host behavior reads projected state activate without that state. Unless the reader rejects the missing registry or key, the host behavior or subagent catalog fields can silently disappear. Batch-only reads also materialize every client view when a consumer needs one host value. ## Decision This decision builds on the split between host projection state and client views in [Session projection state and client views](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md). -Every host reader treats the projection registry and its required key as mandatory state. A plugin either declares `sessionProjections` as a required injection or resolves the registry and key explicitly and throws on the first dependent access. Official compositions mount the registry before those plugins. `ApiProxyService` follows the required-injection form; the lower-level `createApiProxy` factory remains tolerant for isolated tests and diagnostics. +Every host reader treats the projection registry and its required key as mandatory state. Plugins declare `sessionProjections` as a required injection and await acquisition before exposing readers. Official compositions mount the registry before those plugins. `ApiProxyService` follows the required-injection form; the lower-level `createApiProxy` factory remains tolerant for isolated tests and diagnostics. -A domain contributor may register its unit through `ctx.inject(['sessionProjections'], ...)`. Optional registration controls the child lifecycle only; it does not authorize a reader to substitute a default when the registry or key is absent. +Readers acquire state through [awaited registration and retained handles](2026-10-12-session-projection-registration-handles.md). That decision supersedes optional first-access acquisition and returning a bare disposer. It preserves the required-state rule here: a missing registry cannot silently produce default host state. Client views still contain only consumed fields; typed handle reads provide richer host state. -The registry provides `stateOf(session, key)` for one typed host state and keeps `snapshot()` for batch carriers. Client views contain only consumed fields; host readers use `stateOf` for richer state. - -`onChanged` publishes client-visible value changes only. Unit registration and removal remain effect-scoped registry lifecycle; `register()` returns the exact Cordis disposer so a composite domain owner can finish cleanup against projected state before removing its unit. Registration changes do not create a second Host event stream or client tombstone protocol. A later authoritative history or list baseline reflects the active key set. +`onChanged` publishes client-visible value changes only. Registration changes do not create a second Host event stream or client tombstone protocol. A later authoritative history or list baseline reflects the active key set. ## Alternatives considered @@ -30,6 +28,6 @@ The registry provides `stateOf(session, key)` for one typed host state and keeps ## Consequences -- Missing projection composition fails either during activation or at the first dependent host access; it never degrades to a default value. +- Missing projection composition fails during activation; it never degrades to a default value. - Host consumers avoid repeated whole-registry snapshots and log scans. - Wire payloads exclude host-only fields and per-key watermark wrappers; ordinary baselines communicate the active key set. diff --git a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md index 5d6a5169a5..0ab43fd5c0 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md @@ -6,19 +6,17 @@ Status: implemented ## 问题 -可选的投影注册表会让 host 行为读取投影状态的插件在缺少该状态时仍然激活。除非读取方拒绝缺失的注册表或 key,否则 host 行为或 subagent 目录字段可能静默消失。只有批量读取也会在消费方只需要一个 host 值时物化每个客户端 view。部分贡献位置有意保留可选的 `ctx.inject` 注册形式,因此其读取方需要明确的缺失状态规则。 +可选的投影注册表会让 host 行为读取投影状态的插件在缺少该状态时仍然激活。除非读取方拒绝缺失的注册表或 key,否则 host 行为或 subagent 目录字段可能静默消失。只有批量读取也会在消费方只需要一个 host 值时物化每个客户端 view。 ## 决策 本决策建立在[会话投影的 host 状态与客户端视图](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md)所定义的拆分之上。 -每个 host 读取方都把投影注册表及其所需 key 视为必需状态。插件要么把 `sessionProjections` 声明为必需注入,要么显式解析注册表与 key,并在第一次依赖它们的访问时抛错。正式组合在这些插件之前挂载注册表。`ApiProxyService` 采用必需注入形式;较低层的 `createApiProxy` factory 对隔离测试和诊断保持容错。 +每个 host 读取方都把投影注册表及其所需 key 视为必需状态。插件将 `sessionProjections` 声明为必需注入,并在暴露读取方前等待获取完成。正式组合在这些插件之前挂载注册表。`ApiProxyService` 采用必需注入形式;较低层的 `createApiProxy` factory 对隔离测试和诊断保持容错。 -领域贡献方可以通过 `ctx.inject(['sessionProjections'], ...)` 注册单元。可选注册只控制子功能生命周期;它并不允许读取方在注册表或 key 缺失时替换为默认值。 +读取方通过[等待注册与保留句柄](2026-10-12-session-projection-registration-handles.zh.md)获取状态。该决策取代可选首次访问获取和返回裸释放函数的方式,保留这里的必需状态规则:缺少注册表不能静默产生默认 host 状态。客户端视图仍只包含消费字段;类型化句柄读取提供更丰富的 host 状态。 -注册表提供 `stateOf(session, key)` 来读取一个类型化 host 状态,并为批量 carrier 保留 `snapshot()`。客户端 view 只包含消费方使用的字段;host 读取方通过 `stateOf` 取得更丰富的状态。 - -`onChanged` 只发布客户端可见值的变化。单元注册和移除仍是绑定 effect 的注册表生命周期;`register()` 返回 Cordis 的原始 disposer,使组合式领域 owner 可以先依据投影状态完成清理,再移除自身单元。注册变化不会创建第二条 Host 事件流或客户端 tombstone 协议。后续权威 history 或 list 基线会反映活跃 key 集。 +`onChanged` 只发布客户端可见值的变化。注册变化不会创建第二条 Host 事件流或客户端 tombstone 协议。后续权威 history 或 list 基线会反映活跃 key 集。 ## 考虑过的替代方案 @@ -30,6 +28,6 @@ Status: implemented ## 后果 -- 缺少投影组合会在激活期间或第一次依赖它的 host 访问时失败,绝不会降级为默认值。 +- 缺少投影组合会在激活期间失败,绝不会降级为默认值。 - host 消费方避免重复的全注册表快照和日志扫描。 - 协议负载排除 host 内部字段和逐 key 水位包装;普通基线会传达活跃 key 集。 diff --git a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml new file mode 100644 index 0000000000..c22458657b --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-12-session-projection-registration-handles.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md +/agent-note-asynchronous-projection-registration-and-retained-readers: + en: 693349255afbaec8 + zh: 13bcbf3f40b93b2b +/agent-note-asynchronous-projection-registration-and-retained-readers/problem: + en: 47ba548b869be190 + zh: c2e5038a56fe741c +/agent-note-asynchronous-projection-registration-and-retained-readers/decision: + en: 19064b5aecb26f57 + zh: 075bb87c6a5299ae +/agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered: + en: ee5b0cd715ed1fef + zh: 6b1478dcde21916d +/agent-note-asynchronous-projection-registration-and-retained-readers/consequences: + en: 4b5e4bb446872545 + zh: 9fb2cc8ae77fb823 diff --git a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md new file mode 100644 index 0000000000..ea16e10828 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md @@ -0,0 +1,31 @@ +# Agent Note: Asynchronous projection registration and retained readers + +Status: implemented + +English | [中文](2026-10-12-session-projection-registration-handles.zh.md) + +## Problem + +Host readers coupled to registry key lookup lose access when a plugin unloads, although an Agent may still need projected state while its asynchronous cleanup drains. Repeated batch snapshots materialize unrelated client views. A synchronous registration API also makes every consumer assume that acquisition can never require asynchronous initialization. + +## Decision + +`SessionProjectionRegistry.register` returns a promise for a typed handle. Consumers await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Definitions remain with their domain owner, and each reader registers the required definition instead of looking up a possibly absent key. 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. + +AgentLoop acquires inbox and turn-boundary handles once before publishing its factory. Each Agent receives those readers during synchronous construction. Cleanup can use them after the loop fiber removes global registrations, so projection release need not wait for every Agent teardown. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state. + +## Alternatives considered + +**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. + +**Register the same units for each Agent.** The definitions already contain per-Session cells. Additional references and asynchronous Agent initialization add a creation race without providing independent data ownership. + +**Convert function plugins to classes.** Cordis already awaits promises returned by `apply`. Preserving namespace exports avoids a published entry-point and diagnostic-name break. + +## Consequences + +Consumers must migrate acquisition and retain handles. 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. diff --git a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md new file mode 100644 index 0000000000..28974a237b --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 异步投影注册与保留读取句柄 + +Status: implemented + +[English](2026-10-12-session-projection-registration-handles.md) | 中文 + +## Problem + +依赖注册表键查询的 Host 读取方会在插件卸载时失去访问能力,但 Agent 可能仍需在异步清理排空期间读取投影状态。重复的批量快照会具象化无关 Client 视图。同步注册 API 也使每个消费者假定获取过程永远不需要异步初始化。 + +## Decision + +`SessionProjectionRegistry.register` 返回类型化句柄的 Promise。消费者在激活期间等待它,再暴露依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。定义仍归领域拥有方,每个读取方注册所需定义,不再查询可能不存在的键。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。 + +句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。 + +AgentLoop 在发布工厂前只获取一次 inbox 与轮次边界句柄。每个 Agent 在同步构造时收到这些读取句柄。loop fiber 移除全局注册后,清理过程仍可使用它们,因此投影释放无需等待每个 Agent teardown。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。 + +## Alternatives considered + +**保留注册表级查询并延迟注销。** 这使无关插件的释放顺序依赖每个读取方的最后一次访问。保留读取句柄使清理不再依赖该顺序。 + +**返回同步句柄。** 这保留消费者的立即读取,却将获取 API 固定为同步初始化。等待获取过程使时序显式,同时就绪后的状态读取仍同步。 + +**为每个 Agent 注册相同单元。** 定义已经包含逐 Session 单元。额外引用和异步 Agent 初始化只会引入创建竞态,不提供独立数据所有权。 + +**将函数插件改为类。** Cordis 已等待 `apply` 返回的 Promise。保留命名空间导出可避免发布入口与诊断名称的破坏性变更。 + +## Consequences + +消费者必须迁移获取方式并保留句柄。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。 diff --git a/AGENTS.md b/AGENTS.md index 724faca086..f830b6d8ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,7 +130,7 @@ Real-API tests/demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and - Packages use `@deepseek-ai/dsh-`; vendor is [rescoped](docs/rescope.md) and `private: true`. Harness packages declare `@deepseek-ai/cordis` in `peerDependencies`/`devDependencies`. Workspace dependency sections use DSH `workspace:*`, vendor/native `workspace:~` ([rules](.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)). - ESM everywhere (`"type": "module"`). Use package names across packages and `.ts` in local relative imports. Config subprocesses run built `lib/` under plain Node; source regressions use their declared launcher ([testing policy](docs/testing.md#test-subprocess-launch-modes)). The `dsh` CLI source launch runs through tsx's ESM-only hook (`node --import tsx/esm`); modules it reaches must stay ESM (no CJS-only exports) — Node's native TypeScript modes are unavailable across the engines range ([source-launch contract](.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md)). Raw/Web `cordis.yml` bare plugins must appear in their resolver manifest's `dependencies`; `verify-cordis-config` enforces it. -- **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer. +- **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer, directly or through its documented registration handle. - **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`. Public service methods document parameters and non-void returns. `SessionEventMap` members are required-on-read unless the envelope carries `ignorable: true`. Bump `SESSION_FORMAT_VERSION` only when existing discriminators cannot prevent unsafe interpretation; same-version changes require safe reader behavior ([review](.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.md)). - **Switch on discriminant tags.** Closed unions end in `assertNever`; merge-extensible unions fall through a documented default. - **Waterfall listeners MUST call `next()`** to delegate; returning without it short-circuits the chain ([semantics](docs/cordis-primer.md#cordis-waterfall-semantics)). diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 8206f3e6dc..5b82618b52 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -27,8 +27,8 @@ en: e6402b28c0ed15fa zh: 6c07be9047b42056 /deepseek-harness-architecture/session-log: - en: a2e35c43438fec6d - zh: e27bdac8e9feff32 + en: 35fcd01052084a07 + zh: e16743dac147cb6e /deepseek-harness-architecture/capability-seams: en: a5bc20c3f2a1b370 zh: 91773a281410b2f4 diff --git a/docs/architecture.md b/docs/architecture.md index 03d88148c7..b821b9fa92 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -123,7 +123,7 @@ Session consumers know only the current logical format. Header-only `stat` and ` **Model-visible means logged.** Every model request must be reconstructable from the log. New model-visible inputs require session events. Plugins changing existing message content register [pure message projections](subsystems/session.md#plugin-owned-message-projections); detached readers supply the same definitions explicitly. Tool changes are capability-independent; [Session tool history](../packages/core/session/README.md) supplies provider declarations. -**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, host consumers read one typed state with `stateOf()`, and carriers batch cropped client views with `snapshot()`. A host reader either requires this service during activation or fails explicitly when the registry or required key is absent. Contributors may retain `ctx.inject(['sessionProjections'], ...)` registration without silently defaulting a missing host value. The agent loop registers shared `turnBoundary` state for its readers ([decision](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md)). +**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, and carriers batch cropped client views with `snapshot()`. Host readers require the registry during activation, await registration, and retain a handle for synchronous state reads. Releasing registration removes global participation; retained readers continue folding Session events. The shared `turnBoundary` definition belongs to `dsh-agent` ([registration lifecycle](../packages/session/session-projection/README.md)). [Working directories](subsystems/working-directory.md) supply user context and execution paths without changing original project identity, sandbox write roots, or existing process directories. diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 870fa1c996..403c4f3063 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -127,7 +127,7 @@ Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list` **模型可见即已记录。** 每个模型请求都必须能从日志重建。新增模型可见输入需要会话事件。修改现有消息内容的插件注册[纯消息投影](subsystems/session.zh.md#plugin-owned-message-projections),独立读取器显式传入相同的处理器。 工具变更不依赖能力;[Session 工具历史](../packages/core/session/README.zh.md)提供提供方声明。 -**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,host 消费方通过 `stateOf()` 读取单个类型化状态,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方要么在激活时要求该服务,要么在注册表或必需 key 缺席时明确失败。贡献方可以保留 `ctx.inject(['sessionProjections'], ...)` 注册,但不能为缺失的 host 值静默提供默认值。agent loop 为读取方注册共享的 `turnBoundary` 状态([决策](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md))。 +**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方在激活时要求注册表,等待注册完成,并保留句柄以同步读取状态。释放注册会移除全局参与关系;已有读取方仍可继续折叠 Session 事件。共享的 `turnBoundary` 定义归 `dsh-agent` 所有([注册生命周期](../packages/session/session-projection/README.zh.md))。 [工作目录](subsystems/working-directory.zh.md) 提供用户上下文和执行路径,不改变原始项目标识、沙箱写入根目录或已有进程目录。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 00bad2b532..eb2a6a1ea2 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -92,7 +92,7 @@ export interface Config { - `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md) · `Volatile` (`@deepseek-ai/cosmokit`) -- `source`: [`packages/core/agent-loop/src/index.ts:303`](../packages/core/agent-loop/src/index.ts) +- `source`: [`packages/core/agent-loop/src/index.ts:254`](../packages/core/agent-loop/src/index.ts) ```ts config-catalog /** Agent-loop plugin configuration. */ @@ -220,7 +220,7 @@ export interface Config { ## `@deepseek-ai/dsh-api-session-controller` - `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry` -- `source`: [`packages/api/session-controller/src/index.ts:80`](../packages/api/session-controller/src/index.ts) +- `source`: [`packages/api/session-controller/src/index.ts:79`](../packages/api/session-controller/src/index.ts) ```ts config-catalog /** Session Controller deployment policy. */ @@ -950,7 +950,7 @@ export interface StagehandModelConfig { ## `@deepseek-ai/dsh-experimental-claude-code-mods` -- `inject`: `workingDirectory` +- `inject`: `workingDirectory` · `sessionProjections` - `source`: [`packages/experimental/claude-code-mods/src/index.ts:53`](../packages/experimental/claude-code-mods/src/index.ts) ```ts config-catalog @@ -1458,7 +1458,7 @@ export interface HmrConfig extends ChokidarOptions { ## `@deepseek-ai/dsh-hooks-claude-code` - `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:51`](../packages/experimental/hooks-claude-code/src/index.ts) +- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:53`](../packages/experimental/hooks-claude-code/src/index.ts) ```ts config-catalog /** Plugin config: where the CC hook config lives + substitution roots. */ @@ -1497,7 +1497,7 @@ export interface Config { ## `@deepseek-ai/dsh-hooks-codex` - `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-codex/src/index.ts:50`](../packages/experimental/hooks-codex/src/index.ts) +- `source`: [`packages/experimental/hooks-codex/src/index.ts:53`](../packages/experimental/hooks-codex/src/index.ts) ```ts config-catalog /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ @@ -2098,7 +2098,7 @@ export interface ReplayModelConfig { ## `@deepseek-ai/dsh-llm-retry` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts) +- `source`: [`packages/llm/llm-retry/src/index.ts:26`](../packages/llm/llm-retry/src/index.ts) ```ts config-catalog /** This policy executor has no config; providers own `retryPolicy`. */ @@ -2372,7 +2372,7 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` - `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts) +- `source`: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -3259,7 +3259,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' ## `@deepseek-ai/dsh-subagent` -- `inject`: `workingDirectory` +- `inject`: `workingDirectory` · `sessionProjections` - `refs`: `Volatile` (`@deepseek-ai/cordis`) - `source`: [`packages/subagent/subagent/src/index.ts:184`](../packages/subagent/subagent/src/index.ts) @@ -3511,7 +3511,7 @@ export interface Config { ## `@deepseek-ai/dsh-terminal-bash` -- `inject`: `terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess` · `workingDirectory` +- `inject`: `terminals` · `sandboxPolicy` · `subprocess` · `workingDirectory` - `source`: [`packages/terminal/terminal-bash/src/config.ts:10`](../packages/terminal/terminal-bash/src/config.ts) ```ts config-catalog @@ -3572,7 +3572,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:55`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ @@ -3591,7 +3591,7 @@ export interface Config { ## `@deepseek-ai/dsh-tmux-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts) +- `source`: [`packages/context/tmux-context/src/index.ts:45`](../packages/context/tmux-context/src/index.ts) ```ts config-catalog /** Per-turn tmux-location scheduling. Invalid values fail plugin load. */ @@ -3753,7 +3753,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-goal` - `inject`: `agents` · `goals` · `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts) +- `source`: [`packages/goal/tool-goal/src/index.ts:33`](../packages/goal/tool-goal/src/index.ts) ```ts config-catalog /** Model policy and hard lower bounds for goal-state updates. */ @@ -3921,7 +3921,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` - `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/experimental/tool-session-query/src/index.ts:28`](../packages/experimental/tool-session-query/src/index.ts) +- `source`: [`packages/experimental/tool-session-query/src/index.ts:26`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ @@ -3977,7 +3977,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:49`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -4054,7 +4054,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-todo` - `inject`: `tools` · `sessionProjections` -- `source`: [`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts) +- `source`: [`packages/todo/tool-todo/src/index.ts:30`](../packages/todo/tool-todo/src/index.ts) ```ts config-catalog /** Model-facing todo tool configuration. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 3d17bcb06a..aacdae8e58 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -94,7 +94,7 @@ export interface Config { - `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md) · `Volatile` (`@deepseek-ai/cosmokit`) -- `source`: [`packages/core/agent-loop/src/index.ts:303`](../packages/core/agent-loop/src/index.ts) +- `source`: [`packages/core/agent-loop/src/index.ts:254`](../packages/core/agent-loop/src/index.ts) ```ts config-catalog /** Agent-loop plugin configuration. */ @@ -222,7 +222,7 @@ export interface Config { ## `@deepseek-ai/dsh-api-session-controller` - `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry` -- `source`: [`packages/api/session-controller/src/index.ts:80`](../packages/api/session-controller/src/index.ts) +- `source`: [`packages/api/session-controller/src/index.ts:79`](../packages/api/session-controller/src/index.ts) ```ts config-catalog /** Session Controller deployment policy. */ @@ -952,7 +952,7 @@ export interface StagehandModelConfig { ## `@deepseek-ai/dsh-experimental-claude-code-mods` -- `inject`: `workingDirectory` +- `inject`: `workingDirectory` · `sessionProjections` - `source`: [`packages/experimental/claude-code-mods/src/index.ts:53`](../packages/experimental/claude-code-mods/src/index.ts) ```ts config-catalog @@ -1460,7 +1460,7 @@ export interface HmrConfig extends ChokidarOptions { ## `@deepseek-ai/dsh-hooks-claude-code` - `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:51`](../packages/experimental/hooks-claude-code/src/index.ts) +- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:53`](../packages/experimental/hooks-claude-code/src/index.ts) ```ts config-catalog /** Plugin config: where the CC hook config lives + substitution roots. */ @@ -1499,7 +1499,7 @@ export interface Config { ## `@deepseek-ai/dsh-hooks-codex` - `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-codex/src/index.ts:50`](../packages/experimental/hooks-codex/src/index.ts) +- `source`: [`packages/experimental/hooks-codex/src/index.ts:53`](../packages/experimental/hooks-codex/src/index.ts) ```ts config-catalog /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ @@ -2100,7 +2100,7 @@ export interface ReplayModelConfig { ## `@deepseek-ai/dsh-llm-retry` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts) +- `source`: [`packages/llm/llm-retry/src/index.ts:26`](../packages/llm/llm-retry/src/index.ts) ```ts config-catalog /** This policy executor has no config; providers own `retryPolicy`. */ @@ -2374,7 +2374,7 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` - `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts) +- `source`: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -3261,7 +3261,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' ## `@deepseek-ai/dsh-subagent` -- `inject`: `workingDirectory` +- `inject`: `workingDirectory` · `sessionProjections` - `refs`: `Volatile` (`@deepseek-ai/cordis`) - `source`: [`packages/subagent/subagent/src/index.ts:184`](../packages/subagent/subagent/src/index.ts) @@ -3513,7 +3513,7 @@ export interface Config { ## `@deepseek-ai/dsh-terminal-bash` -- `inject`: `terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess` · `workingDirectory` +- `inject`: `terminals` · `sandboxPolicy` · `subprocess` · `workingDirectory` - `source`: [`packages/terminal/terminal-bash/src/config.ts:10`](../packages/terminal/terminal-bash/src/config.ts) ```ts config-catalog @@ -3574,7 +3574,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:55`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ @@ -3593,7 +3593,7 @@ export interface Config { ## `@deepseek-ai/dsh-tmux-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts) +- `source`: [`packages/context/tmux-context/src/index.ts:45`](../packages/context/tmux-context/src/index.ts) ```ts config-catalog /** Per-turn tmux-location scheduling. Invalid values fail plugin load. */ @@ -3755,7 +3755,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-goal` - `inject`: `agents` · `goals` · `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts) +- `source`: [`packages/goal/tool-goal/src/index.ts:33`](../packages/goal/tool-goal/src/index.ts) ```ts config-catalog /** Model policy and hard lower bounds for goal-state updates. */ @@ -3923,7 +3923,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` - `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/experimental/tool-session-query/src/index.ts:28`](../packages/experimental/tool-session-query/src/index.ts) +- `source`: [`packages/experimental/tool-session-query/src/index.ts:26`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ @@ -3979,7 +3979,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:49`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -4056,7 +4056,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-todo` - `inject`: `tools` · `sessionProjections` -- `source`: [`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts) +- `source`: [`packages/todo/tool-todo/src/index.ts:30`](../packages/todo/tool-todo/src/index.ts) ```ts config-catalog /** Model-facing todo tool configuration. */ diff --git a/docs/defensive-patterns/remote-and-api.md b/docs/defensive-patterns/remote-and-api.md index 25281e96fb..df98cdde2a 100644 --- a/docs/defensive-patterns/remote-and-api.md +++ b/docs/defensive-patterns/remote-and-api.md @@ -92,16 +92,18 @@ The owning package decides and registers its unit, and the controller only reads ```ts ignore-check // packages/workspace//src/index.ts: the rule lives with the feature it serves. -ctx.sessionProjections.register({ +export const durablePageProjectionDefinition = { key: 'durablePage', stateSchema: durablePageState, init: () => ({ durable: false }), apply: applyDurablePage, stateVersion: 1, -}) +} +await ctx.sessionProjections.register(durablePageProjectionDefinition) // packages/api/session-controller: generic orchestration reads the projected value. -const { durable } = ctx.sessionProjections.stateOf(session, 'durablePage') +const durablePage = await ctx.sessionProjections.register(durablePageProjectionDefinition) +const { durable } = durablePage.stateOf(session) ``` The wrong version decides the rule inside the shared layer: diff --git a/docs/defensive-patterns/remote-and-api.zh.md b/docs/defensive-patterns/remote-and-api.zh.md index 1a9c291d9c..79691a940a 100644 --- a/docs/defensive-patterns/remote-and-api.zh.md +++ b/docs/defensive-patterns/remote-and-api.zh.md @@ -92,16 +92,18 @@ type SessionRemote = { list(): ...; rename(id: string): ... } // hand-written ```ts ignore-check // packages/workspace//src/index.ts: the rule lives with the feature it serves. -ctx.sessionProjections.register({ +export const durablePageProjectionDefinition = { key: 'durablePage', stateSchema: durablePageState, init: () => ({ durable: false }), apply: applyDurablePage, stateVersion: 1, -}) +} +await ctx.sessionProjections.register(durablePageProjectionDefinition) // packages/api/session-controller: generic orchestration reads the projected value. -const { durable } = ctx.sessionProjections.stateOf(session, 'durablePage') +const durablePage = await ctx.sessionProjections.register(durablePageProjectionDefinition) +const { durable } = durablePage.stateOf(session) ``` 反例把这条规则的决定放进共享层: diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 0f0d3dbdcf..fd1fcb14a8 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -8,7 +8,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:200`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | | `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 16b47c76cb..ad79919624 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -10,7 +10,7 @@ | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:200`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | | `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index e61666b542..342e85df6d 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -144,8 +144,8 @@ en: 4294f78d26ed985b zh: 4294f78d26ed985b /session-persistence-event-catalog/events/plan/plan-mode-log-only: - en: 862175bfe8692ef7 - zh: a8ab123bf674cf77 + en: 9ae5537eeae238de + zh: 6be761c7fd83fdce /session-persistence-event-catalog/events/request: en: a8df82440ebdf5f8 zh: a8df82440ebdf5f8 @@ -1347,8 +1347,8 @@ en: a74e3dfa51c987af zh: 6c3c87974d7a7260 /session-persistence-event-catalog/resolved-persistence-types/active~2: - en: a3de6a6fe9672c7a - zh: a357481e27b3b81f + en: c861888d6239b700 + zh: a9b2271d3c6b8737 /session-persistence-event-catalog/resolved-persistence-types/agentpreset: en: b8a0f8aac9708056 zh: f5f004bd205b41e1 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 3d8b0b77db..eb4ebe099b 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -700,7 +700,7 @@ Source: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/ 'plan/mode': { active: boolean } ``` -Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -5556,7 +5556,7 @@ Array of `unknown` (opaque). SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -Sources: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +Sources: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 76af401f6b..5b1def70af 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -702,7 +702,7 @@ export type SessionEvent = { 'plan/mode': { active: boolean } ``` -来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -5558,7 +5558,7 @@ SHA-256: `b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7` SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 729e58948d..1da9491a5f 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -22518,7 +22518,7 @@ "digest": "20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5", "names": [], "sources": [ - "packages/plan/plan-mode/src/index.ts:52" + "packages/plan/plan-mode/src/index.ts:53" ] }, { diff --git a/docs/subsystems/plan.i18n.yaml b/docs/subsystems/plan.i18n.yaml index e5ce0f0902..a55599bd51 100644 --- a/docs/subsystems/plan.i18n.yaml +++ b/docs/subsystems/plan.i18n.yaml @@ -6,8 +6,8 @@ en: 234f6e4c49714997 zh: add95d36252b572d /plan-mode/logged-state-and-recovery: - en: dd1e8ba31e6fd535 - zh: a22dbccc60c73bc9 + en: 4c83897d7a6d8a67 + zh: 22dcc171a439ed19 /plan-mode/pending-selections-and-the-pre-step-append: en: 8d7e5b7824921ed5 zh: 910fad43593f29f4 diff --git a/docs/subsystems/plan.md b/docs/subsystems/plan.md index beba872f61..56af5f14d2 100644 --- a/docs/subsystems/plan.md +++ b/docs/subsystems/plan.md @@ -8,7 +8,7 @@ Source: [`packages/plan/plan-mode/src/index.ts`](../../packages/plan/plan-mode/s ## Logged state and recovery -`plan/mode` (`{ active: boolean }`) is a log-only, whole-value-replace [session event](session.md): durable and replayable, never in the model transcript. The optionally registered `plan` unit folds committed mode, command settlement, and the mode recorded at the latest request header. `ctx.planMode` reads that state through `stateOf()`; the first dependent access fails if the registry, `plan` key, or `turnBoundary` key is absent. Clients receive only `{ active, pending }`; resume, fork, and compaction recover both from the log. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md). +`plan/mode` (`{ active: boolean }`) is a log-only, whole-value-replace [session event](session.md): durable and replayable, never in the model transcript. The `plan` unit folds committed mode, command settlement, and the mode recorded at the latest request header. `ctx.planMode` requires the registry and awaits registration of `plan` and `turnBoundary` during activation, then reads through their retained handles. Clients receive only `{ active, pending }`; resume, fork, and compaction recover both from the log. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md). ## Pending selections and the pre-step append diff --git a/docs/subsystems/plan.zh.md b/docs/subsystems/plan.zh.md index 63463f0105..f0bb4a0d99 100644 --- a/docs/subsystems/plan.zh.md +++ b/docs/subsystems/plan.zh.md @@ -8,7 +8,7 @@ ## 已记录状态与恢复 -`plan/mode`(`{ active: boolean }`)是仅记日志、整值替换的[会话事件](session.zh.md):持久且可回放,绝不进入模型 transcript(文本记录)。可选注册的 `plan` 单元折叠已提交模式、命令结算结果和最近一次请求头记录的模式。`ctx.planMode` 通过 `stateOf()` 读取该状态;注册表、`plan` key 或 `turnBoundary` key 缺失时,第一次依赖它们的访问会失败。客户端只接收 `{ active, pending }`;恢复、fork 与压缩(compaction)都能从日志恢复两者。完整事件声明见[持久化日志事件目录](../persistence-catalog.zh.md)。 +`plan/mode`(`{ active: boolean }`)是仅记日志、整值替换的[会话事件](session.zh.md):持久且可回放,绝不进入模型 transcript(文本记录)。`plan` 单元折叠已提交模式、命令结算结果和最近一次请求头记录的模式。`ctx.planMode` 要求注册表存在,并在激活期间等待 `plan` 和 `turnBoundary` 注册完成,之后通过保留的句柄读取状态。客户端只接收 `{ active, pending }`;恢复、fork 与压缩(compaction)都能从日志恢复两者。完整事件声明见[持久化日志事件目录](../persistence-catalog.zh.md)。 ## 待生效选择与 pre-step 追加 diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index 8d53d7ed36..5f008d2f02 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/docs/subsystems/session-projection.i18n.yaml @@ -3,14 +3,14 @@ # 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: a6ab5134ca1771fd - zh: 523039a9d84e3f97 + en: e9330151d3eb579a + zh: 53e47acd1d810da9 /session-projections/the-unit: en: fad897af8b8839ff zh: 3c68c5256973bc39 /session-projections/the-snapshot-and-the-change-feed: - en: a49a57c600c3cbd0 - zh: c92f00b8a9d71bad + en: 630c1041f41f89bf + zh: a3361bdead116cd7 /session-projections/the-registry-ctx-sessionprojections: - en: 50e79c1bc16abb63 - zh: 200ecbf0e4ab19d2 + en: 67fe0f49dee90e25 + zh: c23de3a2803040f0 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index 8580b94c4d..5da8044825 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -2,10 +2,27 @@ English | [中文](session-projection.zh.md) -The session-projection seam — a [capability seam](../capability-seams.md) through which domain host plugins serve whole current values of log-derived per-session state to client carriers: the Service Definition and registry ([dsh-session-projection](../../packages/session/session-projection), `ctx.sessionProjections`), domain contributors (each registering one pure unit), and carriers ([dsh-session-controller](../../packages/api/session-controller)'s history tail page and `session/projection` push frame). It is one optional capability, not part of the agent-loop spine. The framework drives, the domain computes: the registry subscribes to `session/event` once and folds every committed event through every unit; domains hold no subscriptions and clients never fold domain events — they receive finished values. Design authority: the [session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md); drive/cache/feed contracts: the [package README](../../packages/session/session-projection/README.md). +The session-projection seam — a [capability seam](../capability-seams.md) through which domain host plugins serve whole current values of log-derived per-session state to client carriers: the Service Definition and registry ([dsh-session-projection](../../packages/session/session-projection), `ctx.sessionProjections`), domain contributors (each registering one pure unit), and carriers ([dsh-session-controller](../../packages/api/session-controller)'s history tail page and `session/projection` push frame). Host state readers require the registry during activation. The framework drives, the domain computes: the registry subscribes to `session/event` once and folds every committed event through every unit; domains hold no subscriptions and clients never fold domain events — they receive finished values. Design authority: the [session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md); drive/cache/feed contracts: the [package README](../../packages/session/session-projection/README.md). 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). + +```ts type-equiv +/** An effect-owned registration with a reader retained independently of registration. */ +interface ProjectionHandle { + /** + * Read the current state, including after unregistering, by folding new + * Session events with this handle's original unit. Do not mutate the live value. + * @param session - Session whose committed events are folded. + * @returns the projection state at the Session's current cursor. + */ + stateOf(session: Session): S + /** Unregister from global drives and snapshots without invalidating this reader. */ + dispose(): void | Promise +} +``` + ## The unit `SessionProjectionStateMap` is the merge-extensible table of host fold states, while `SessionProjectionMap` retains the client-visible whole values. A domain contributes one `ProjectionDefinition` per state key; a `wire` block makes that key client-visible, and rendering belongs to the slot system, never this layer: @@ -99,11 +116,11 @@ type ProjectionChangeListener = ( ) => void ``` -`snapshot(session)` is fully synchronous: a carrier reads it in the same tick as its page slice, so `asOfSeq` covers both reads at one sequence number. It returns only client views, and every value passes its unit's `viewSchema` before return. `stateOf(session, key)` reads one live host state without computing unrelated views; callers must not mutate the borrowed reference. A state-reference change computes one cached raw view, and the change feed fires only when that result changes by `Object.is`; an object-valued view must preserve its reference to suppress publication across internal-only state changes. +`snapshot(session)` is fully synchronous: a carrier reads it in the same tick as its page slice, so `asOfSeq` covers both reads at one sequence number. It returns only client views, and every value passes its unit's `viewSchema` before return. `handle.stateOf(session)` reads one live host state without computing unrelated views; callers must not mutate the borrowed reference. A state-reference change computes one cached raw view, and the change feed fires only when that result changes by `Object.is`; an object-valued view must preserve its reference to suppress publication across internal-only state changes. ## 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). Registration is an effect whose disposer rides the calling fiber: an unloaded domain plugin's key (with its cached cells) disappears from subsequent drives and snapshots, and clients read that as capability absence; a duplicate key with a different `stateVersion` throws, while same-version registrants share one unit and are counted. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. +`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. @@ -204,26 +221,27 @@ 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). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. A host reader either declares `sessionProjections` in its plugin `inject` or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through `ctx.inject(['sessionProjections'], ...)`. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads. +`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. ```ts cordis-catalog /** - * Register one domain's unit. The registration is an effect on the calling - * context's fiber: disposing the fiber (or calling the returned disposer) - * removes the key — and the unit's cached cells — from subsequent drives - * and snapshots. + * Register one domain's unit on the calling context's fiber. Fiber or handle + * disposal releases one reference; the final release removes live participation. + * Retained handles remain readable after release. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ -register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'> & { wire: NonNullable['wire']> }, ): () => void +register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'> & { wire: NonNullable['wire']> }, ): Promise> /** * Register one host-only unit. Its state is omitted from client snapshots * and always checkpointed like every other unit. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ -register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'>, ): () => void +register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'>, ): Promise> /** * Subscribe to the change feed. The registration is an effect on the @@ -233,16 +251,6 @@ register< K extends Exclude void -/** - * Read one unit's current host state after materializing every registered - * unit at the Session cursor. Unrelated wire views are not produced. - * The returned value is live; callers must not mutate it. - * @param session - the session whose state is read. - * @param key - the registered unit key. - * @returns current state, or `undefined` when the key is not registered. - */ -stateOf( session: Session, key: K, ): SessionProjectionStateMap[K] | undefined - /** * 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). diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index b98d348a36..c2c952b8fc 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -2,10 +2,27 @@ [English](session-projection.md) | 中文 -会话投影 seam 是一项[能力 seam](../capability-seams.zh.md):领域 host 插件经由它向客户端载体供给按会话的日志派生状态的当前全量值;三方分别是 Service Definition 与注册表([dsh-session-projection](../../packages/session/session-projection),`ctx.sessionProjections`)、领域贡献方(每个领域注册一个纯单元)与载体([dsh-session-controller](../../packages/api/session-controller) 的历史尾页与 `session/projection` 推送帧)。它是一项可选能力,不属于 agent loop(智能体循环)主干。框架负责驱动,领域负责计算:注册表只订阅一次 `session/event`,并把每个已提交事件折叠进每个单元;领域不持有任何订阅,客户端也从不折叠领域事件——它们收到的是成品值。设计权威:[session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md);驱动、缓存与变更流约定:[包 README](../../packages/session/session-projection/README.zh.md)。 +会话投影 seam 是一项[能力 seam](../capability-seams.zh.md):领域 host 插件经由它向客户端载体供给按会话的日志派生状态的当前全量值;三方分别是 Service Definition 与注册表([dsh-session-projection](../../packages/session/session-projection),`ctx.sessionProjections`)、领域贡献方(每个领域注册一个纯单元)与载体([dsh-session-controller](../../packages/api/session-controller) 的历史尾页与 `session/projection` 推送帧)。Host 状态读取方在激活期间要求注册表存在。框架负责驱动,领域负责计算:注册表只订阅一次 `session/event`,并把每个已提交事件折叠进每个单元;领域不持有任何订阅,客户端也从不折叠领域事件——它们收到的是成品值。设计权威:[session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md);驱动、缓存与变更流约定:[包 README](../../packages/session/session-projection/README.zh.md)。 源码:[`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)。 + +```ts type-equiv +/** An effect-owned registration with a reader retained independently of registration. */ +interface ProjectionHandle { + /** + * Read the current state, including after unregistering, by folding new + * Session events with this handle's original unit. Do not mutate the live value. + * @param session - Session whose committed events are folded. + * @returns the projection state at the Session's current cursor. + */ + stateOf(session: Session): S + /** Unregister from global drives and snapshots without invalidating this reader. */ + dispose(): void | Promise +} +``` + ## 投影单元 `SessionProjectionStateMap` 是 host 侧折叠状态的 merge-extensible 类型表,`SessionProjectionMap` 则继续表示客户端可见的全量值。领域为每个状态 key 贡献一个 `ProjectionDefinition`;`wire` 块使该 key 对客户端可见,渲染归 slot 体系管,永远不归本层: @@ -99,11 +116,11 @@ type ProjectionChangeListener = ( ) => void ``` -`snapshot(session)` 完全同步:载体在切出页面切片的同一 tick 内读取它,因此 `asOfSeq` 使两次读取使用同一个序号。它只返回客户端视图,并在返回前通过各单元的 `viewSchema` 校验。`stateOf(session, key)` 可在不计算无关视图的情况下读取一份实时 host 状态;调用方不得修改这一借用引用。state 引用变化时,注册表计算并缓存一次原始 view;只有该结果通过 `Object.is` 判定为变化时才触发变更流,对象 view 若要在仅内部 state 变化时抑制发布就必须保留引用。 +`snapshot(session)` 完全同步:载体在切出页面切片的同一 tick 内读取它,因此 `asOfSeq` 使两次读取使用同一个序号。它只返回客户端视图,并在返回前通过各单元的 `viewSchema` 校验。`handle.stateOf(session)` 可在不计算无关视图的情况下读取一份实时 host 状态;调用方不得修改这一借用引用。state 引用变化时,注册表计算并缓存一次原始 view;只有该结果通过 `Object.is` 判定为变化时才触发变更流,对象 view 若要在仅内部 state 变化时抑制发布就必须保留引用。 ## 注册表:`ctx.sessionProjections` -`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动权:一份 `session/event` 订阅、对每个已注册单元即时调用 `apply`,以及每会话每单元的水位线(watermark)cell。cell 惰性构建:在事件流过之后才注册的单元,或比注册表更早的会话,都在首次触达(事件或读取)时从 `init` 出发在内存日志上折叠。注册是一个 effect,其 disposer 随调用方 fiber 走:领域插件卸载后,其 key(连同缓存的 cell)从后续驱动与快照中消失,客户端将其读作能力缺失;key 以不同 `stateVersion` 重复时直接 throw,同版本注册方则共享一个单元并被计数。领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响。 +`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动逻辑:一个 `session/event` 订阅、对所有已注册单元立即执行的 `apply`,以及按会话、按单元保存的水位线缓存。缓存单元惰性构建:若单元在事件产生后才注册,或会话早于注册表存在,第一次访问(事件或读取)会从 `init` 开始折叠内存日志。领域在激活期间等待 `register(definition)`,并保留返回的 handle(句柄)。注册归调用 fiber 所有:最后一次释放会从全局驱动和快照中移除该 key,已有句柄仍可读取。同 key、同版本的注册者共享一个单元;版本不兼容时注册失败。 @@ -204,26 +221,27 @@ 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). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. A host reader either declares `sessionProjections` in its plugin `inject` or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through `ctx.inject(['sessionProjections'], ...)`. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads. +`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. ```ts cordis-catalog /** - * Register one domain's unit. The registration is an effect on the calling - * context's fiber: disposing the fiber (or calling the returned disposer) - * removes the key — and the unit's cached cells — from subsequent drives - * and snapshots. + * Register one domain's unit on the calling context's fiber. Fiber or handle + * disposal releases one reference; the final release removes live participation. + * Retained handles remain readable after release. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ -register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'> & { wire: NonNullable['wire']> }, ): () => void +register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'> & { wire: NonNullable['wire']> }, ): Promise> /** * Register one host-only unit. Its state is omitted from client snapshots * and always checkpointed like every other unit. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ -register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'>, ): () => void +register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'>, ): Promise> /** * Subscribe to the change feed. The registration is an effect on the @@ -233,16 +251,6 @@ register< K extends Exclude void -/** - * Read one unit's current host state after materializing every registered - * unit at the Session cursor. Unrelated wire views are not produced. - * The returned value is live; callers must not mutate it. - * @param session - the session whose state is read. - * @param key - the registered unit key. - * @returns current state, or `undefined` when the key is not registered. - */ -stateOf( session: Session, key: K, ): SessionProjectionStateMap[K] | undefined - /** * 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). diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.i18n.yaml new file mode 100644 index 0000000000..6d6f45fd15 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.i18n.yaml @@ -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.1-alpha.1/claude-code-mods-projections/guide.md +/: + en: 2ca1b8bc4d168c3f + zh: 2257728847687363 +/compose-session-projections-for-claude-code-mods: + en: 58c229d213b33d7b + zh: d6d539f921e59af2 +/compose-session-projections-for-claude-code-mods/change: + en: 3a76b4c79776f847 + zh: e9c7d258a2b76611 +/compose-session-projections-for-claude-code-mods/migration: + en: 6b9ecb1a635ccb44 + zh: 978dad0bdcd69d57 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md new file mode 100644 index 0000000000..3c97f66652 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md @@ -0,0 +1,17 @@ +--- +kind: upgrade-guide +description: "The Claude Code mods bridge requires the session projection registry at initialization." +--- + +# Compose session projections for Claude Code mods + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-experimental-claude-code-mods` requires `sessionProjections` to initialize. Custom compositions that omit this service no longer start the bridge. The bridge registers the turn and context-pressure projections before serving mods. + +## Migration + +1. Add `@deepseek-ai/dsh-session-projection` to the `cordis.yml` or profile overlay that composes the bridge, unless it is already composed. +2. Start the profile and confirm the bridge and its mod plugins load successfully. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md new file mode 100644 index 0000000000..753d5ca266 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md @@ -0,0 +1,17 @@ +--- +kind: upgrade-guide +description: "Claude Code 模组桥接在初始化时要求会话投影注册表。" +--- + +# 为 Claude Code 模组组合会话投影 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-experimental-claude-code-mods` 需要 `sessionProjections` 才能初始化。未提供此服务的自定义组合不再启动桥接。桥接在服务模组前注册轮次和上下文压力投影。 + +## 迁移 + +1. 在组合桥接的 `cordis.yml` 或配置覆盖文件中添加 `@deepseek-ai/dsh-session-projection`;已组合此服务时无需重复添加。 +2. 启动配置,确认桥接及其模组插件成功加载。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml index c1938c0f53..9407c83aff 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml @@ -9,8 +9,8 @@ en: fcf5f06084f989d9 zh: 5e3e6a05786390dd /migrate-delegation-to-managed-subagent-activations/change: - en: 17add29d9f80674e - zh: 4d4050e99f019cca + en: e56eb4dee1cf246a + zh: f5c5c62c815c140d /migrate-delegation-to-managed-subagent-activations/migration: - en: d4269b2c58484a3d - zh: 0ae39476ecd330e6 + en: e8455cb8d6ecf66b + zh: 1b48e06d6aac7d9f diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md index 5a88ff2ee3..1ef7d07119 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md @@ -11,7 +11,9 @@ English | [中文](guide.zh.md) `@deepseek-ai/dsh-tool-subagent` removes `backgroundMode`, `enableRunInBackground`, and the model argument `run_in_background`. Every delegation now returns `{ kind: 'activation', subagentId }` after accepting the child, and the runtime delivers its final answer in a parent completion notice. Foreground results and `{ kind: 'background', jobId }` results are removed; subagents are no longer collected or stopped through `job_output` or `job_kill`. -The published `@deepseek-ai/dsh-subagent-in-process-driver` package is removed. Local providers contribute creation inputs through `prepareContinuable()`; the subagent service owns execution. Spawn and Fork children can receive later messages, including after settlement when persistence and Session query are available. External providers execute once and do not accept follow-up messages. +`@deepseek-ai/dsh-subagent-in-process-driver` is removed. Local providers contribute creation inputs through `prepareContinuable()`; the subagent service owns execution. Spawn and Fork children can receive later messages, including after settlement when persistence and Session query are available. External providers execute once and do not accept follow-up messages. + +Subagent initialization requires `@deepseek-ai/dsh-session-projection`. ## Migration @@ -24,7 +26,7 @@ The published `@deepseek-ai/dsh-subagent-in-process-driver` package is removed. toolName: subagent ``` - Local tool delegation requires Session persistence. Compose it with the [subagent service and provider](../../../../packages/subagent/subagent/README.md#use-this-package). + Mount `@deepseek-ai/dsh-session-projection`. Local tool delegation requires Session persistence. Compose it with the [subagent service and provider](../../../../packages/subagent/subagent/README.md#use-this-package). 2. Remove `run_in_background` from tool calls and update result consumers to read `subagentId`. Collect the completion notice instead of foreground output or a Job result. Use `interrupt_agent` to stop the current execution; use `send_message` only for local children when those control tools are composed. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md index aa83610496..1f2e80d7cf 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md @@ -11,7 +11,9 @@ description: "受管理的子 agent activation 替代前台与 Job 后台委派 `@deepseek-ai/dsh-tool-subagent` 移除了 `backgroundMode`、`enableRunInBackground` 和模型参数 `run_in_background`。每次委派现在都会在接纳子 agent(智能体)后返回 `{ kind: 'activation', subagentId }`,运行时通过父级完成通知交付最终答案。前台结果和 `{ kind: 'background', jobId }` 结果已移除;子 agent 不再通过 `job_output` 或 `job_kill` 收集结果或停止。 -已发布的 `@deepseek-ai/dsh-subagent-in-process-driver` 包被移除。本地提供方通过 `prepareContinuable()` 提供创建输入,由子 agent 服务拥有执行过程。Spawn 和 Fork 子级可以接收后续消息;组合了持久化和 Session 查询服务时,结算后也能接收。外部提供方只执行一次,不接受后续消息。 +`@deepseek-ai/dsh-subagent-in-process-driver` 被移除。本地提供方通过 `prepareContinuable()` 提供创建输入,由子 agent 服务拥有执行过程。Spawn 和 Fork 子级可以接收后续消息;组合了持久化和 Session 查询服务时,结算后也能接收。外部提供方只执行一次,不接受后续消息。 + +子 agent 初始化需要 `@deepseek-ai/dsh-session-projection`。 ## 迁移 @@ -24,7 +26,7 @@ description: "受管理的子 agent activation 替代前台与 Job 后台委派 toolName: subagent ``` - 本地工具委派需要 Session 持久化。将其与[子 agent 服务及提供方](../../../../packages/subagent/subagent/README.zh.md#use-this-package)一起组合。 + 挂载 `@deepseek-ai/dsh-session-projection`。本地工具委派需要 Session 持久化。将其与[子 agent 服务及提供方](../../../../packages/subagent/subagent/README.zh.md#use-this-package)一起组合。 2. 从工具调用中删除 `run_in_background`,并将结果消费方改为读取 `subagentId`。收集完成通知,替代前台输出或 Job 结果。组合了相应控制工具时,使用 `interrupt_agent` 停止当前执行;`send_message` 仅用于本地子级。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml new file mode 100644 index 0000000000..7d864c5be2 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml @@ -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.1-alpha.2/session-projection-handles/guide.md +/: + en: b3f54a7eadb3398a + zh: 6815b996b2cebc6f +/session-projection-registration-handles: + en: 727968e582c6eee6 + zh: e01dc83384747012 +/session-projection-registration-handles/change: + en: 06c3d4ee9031fb7e + zh: cf2b664bfdafe837 +/session-projection-registration-handles/migration: + en: 0619d4d16272fa0c + zh: c3b0bb2c9d3ecb57 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md new file mode 100644 index 0000000000..f746d9170d --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Session projection registration is asynchronous and returns a retained reader instead of a disposer." +--- +# Session projection registration handles + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-session-projection` changes `SessionProjectionRegistry.register(definition)` from a synchronous disposer to `Promise>`. Registry-level `stateOf(session, key)` is removed. Registration errors now reject the returned promise. After the last registration is disposed, the unit leaves snapshots, checkpoints, and live notifications, but retained handles can still fold Session events on demand. + +`@deepseek-ai/dsh-subagent` now requires `sessionProjections`. `@deepseek-ai/dsh-api-session-controller` removes `installModelSelectionProjection`; consumers register `modelSelectionProjection` instead. The controller also registers `agentPreset`, whose null value means the Session has no recorded preset; that key does not establish that a preset registry is available. `turnBoundaryProjectionDefinition` is exported by `@deepseek-ai/dsh-agent`; its `agent-loop` re-export remains available. + +## Migration + +1. Declare `sessionProjections` as a required plugin injection and mount `SessionProjectionRegistry` before consumers, including `SubagentRuntime` in custom compositions. +2. In an asynchronous function-plugin `apply()` or service `[Service.init]()`, await `ctx.sessionProjections.register(definition)` and retain the returned handle before publishing dependent operations. Keep existing function-plugin namespace exports. +3. Replace `registry.stateOf(session, key)` with `handle.stateOf(session)`. Import the owning package's projection definition when a reader needs to acquire a handle itself. Replace stored unregister callbacks with `handle.dispose`; fiber disposal still releases registrations automatically. Await registration when handling initialization failures. +4. Replace `installModelSelectionProjection(ctx)` with `await ctx.sessionProjections.register(modelSelectionProjection)`, importing the definition from `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts`. +5. Typecheck custom plugins, then verify activation, a Session state read, and owner disposal. A released reader remains usable, while the released unit disappears from new snapshots once its final registration is gone. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md new file mode 100644 index 0000000000..d6a19c5631 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Session 投影注册改为异步,并返回保留读取能力的句柄而不是释放函数。" +--- +# Session 投影注册句柄 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-session-projection` 的 `SessionProjectionRegistry.register(definition)` 从同步释放函数改为返回 `Promise>`。注册表级 `stateOf(session, key)` 已移除,注册错误现在使返回的 Promise 拒绝。最后一次注册释放后,该投影单元不再参与快照、检查点和实时通知,但保留的句柄仍可按需折叠 Session 事件。 + +`@deepseek-ai/dsh-subagent` 现在必需注入 `sessionProjections`。`@deepseek-ai/dsh-api-session-controller` 移除了 `installModelSelectionProjection`;消费者改为注册 `modelSelectionProjection`。控制器也注册 `agentPreset`,其 null 值表示 Session 未记录预设;该键不代表预设注册表可用。`turnBoundaryProjectionDefinition` 由 `@deepseek-ai/dsh-agent` 导出;`agent-loop` 的再导出仍然可用。 + +## 迁移 + +1. 将 `sessionProjections` 声明为插件的必需注入,并在消费者之前挂载 `SessionProjectionRegistry`,包括自定义组合中的 `SubagentRuntime`。 +2. 在函数插件的异步 `apply()` 或服务的 `[Service.init]()` 中,等待 `ctx.sessionProjections.register(definition)`,在发布依赖操作前保留返回的句柄。保留既有的函数插件命名空间导出。 +3. 将 `registry.stateOf(session, key)` 替换为 `handle.stateOf(session)`。读取方需要自行取得句柄时,导入归属包的投影定义。将保存的注销回调替换为 `handle.dispose`;fiber 释放仍会自动解除注册。处理初始化错误时须等待注册完成。 +4. 将 `installModelSelectionProjection(ctx)` 替换为 `await ctx.sessionProjections.register(modelSelectionProjection)`,从 `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts` 导入该定义。 +5. 对自定义插件执行类型检查,再验证激活、Session 状态读取与 owner 释放。已释放的读取句柄仍可使用,而投影单元在最后一次注册解除后会从新快照中消失。 diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 7da873bea6..d2ded420f8 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -2204,19 +2204,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ { key: 'sessionProjections', summary: '`ctx.sessionProjections`: the projection unit table and its drive.', - description: '`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). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin\'s key disappears from snapshots and clients read it as capability absence. A host reader either declares `sessionProjections` in its plugin `inject` or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through `ctx.inject([\'sessionProjections\'], ...)`. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads.', + description: '`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.', methods: [ { - signature: 'register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, \'wire\'> & { wire: NonNullable[\'wire\']> }, ): () => void', - description: 'Register one domain\'s unit. The registration is an effect on the calling context\'s fiber: disposing the fiber (or calling the returned disposer) removes the key — and the unit\'s cached cells — from subsequent drives and snapshots.', + signature: 'register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, \'wire\'> & { wire: NonNullable[\'wire\']> }, ): Promise>', + description: 'Register one domain\'s unit on the calling context\'s fiber. Fiber or handle disposal releases one reference; the final release removes live participation. Retained handles remain readable after release. Callers must await registration before publishing consumers; registration may perform asynchronous work.', parameters: [{ name: 'definition', description: 'key, state schema, pure unit functions, and stateVersion.' }], - returns: 'the exact disposer that unregisters this unit.', + returns: 'a ready handle with synchronous state reads and the effect disposer.', }, { - signature: 'register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, \'wire\'>, ): () => void', - description: 'Register one host-only unit. Its state is omitted from client snapshots and always checkpointed like every other unit.', + signature: 'register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, \'wire\'>, ): Promise>', + description: 'Register one host-only unit. Its state is omitted from client snapshots and always checkpointed like every other unit. Callers must await registration before publishing consumers; registration may perform asynchronous work.', parameters: [{ name: 'definition', description: 'key, state schema, pure unit functions, and stateVersion.' }], - returns: 'the exact disposer that unregisters this unit.', + returns: 'a ready handle with synchronous state reads and the effect disposer.', }, { signature: 'onChanged(listener: ProjectionChangeListener): () => void', @@ -2224,12 +2224,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'listener', description: 'called once per client-visible unit whose raw view changed by `Object.is`, per committed event.' }], returns: 'the exact disposer that unsubscribes.', }, - { - signature: 'stateOf( session: Session, key: K, ): SessionProjectionStateMap[K] | undefined', - description: 'Read one unit\'s current host state after materializing every registered unit at the Session cursor. Unrelated wire views are not produced. The returned value is live; callers must not mutate it.', - parameters: [{ name: 'session', description: 'the session whose state is read.' }, { name: 'key', description: 'the registered unit key.' }], - returns: 'current state, or `undefined` when the key is not registered.', - }, { signature: 'snapshot( session: Session, keys?: readonly Extract[], ): ProjectionSnapshot', description: '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). Fully synchronous — every value and `asOfSeq` reflect the same log position. Each value passes its unit\'s `viewSchema` before leaving.', @@ -6336,6 +6330,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'ProjectionDefinition', declaration: 'export interface ProjectionDefinition {\n key: K;\n stateSchema: ZodType;\n init(header: SessionHeader, inheritedEventCount: SessionLogOffset): NoInfer;\n apply(state: NoInfer, event: SessionEvent): NoInfer;\n wire?: K extends keyof SessionProjectionMap ? {\n viewSchema: ZodType;\n view(state: NoInfer): SessionProjectionMap[K];\n } : never;\n stateVersion: number;\n}', }, + { + name: 'ProjectionHandle', + declaration: 'export interface ProjectionHandle {\n stateOf(session: Session): S;\n dispose(): void | Promise;\n}', + }, { name: 'ProjectionSnapshot', declaration: 'export interface ProjectionSnapshot {\n asOfSeq: SessionSeqCursor;\n values: Partial;\n}', diff --git a/scripts/gen-config-catalog.spec.ts b/scripts/gen-config-catalog.spec.ts index 7248df7b98..4aed5f9782 100644 --- a/scripts/gen-config-catalog.spec.ts +++ b/scripts/gen-config-catalog.spec.ts @@ -97,6 +97,31 @@ export function apply(ctx: unknown, config: Config): void {} expect(new Set(provider?.schemaKeys)).toEqual(new Set(['mode', 'headless', 'endpoint'])) }) + it.each([false, true])('checks a class Config imported from a sibling module (invalid field: %s)', (invalid) => { + const { root, write } = fixture() + write('packages/test/provider/src/config.ts', ` +import Schema from '@deepseek-ai/schemastery' +export interface Config { + /** Maximum result count. */ + limit: number +} +export const Config = Schema.object({ ${invalid ? 'hidden' : 'limit'}: Schema.number() }) +`) + write('packages/test/provider/src/index.ts', ` +import { Config } from './config.ts' +export default class Provider { + static Config = Config + constructor(ctx: unknown, config: Config) {} +} +`) + if (invalid) { + expect(() => collectConfigCatalog(root)).toThrow("schema validates key 'hidden' but config type 'Config' declares no such member") + } else { + const provider = collectConfigCatalog(root).find(entry => entry.pkg === '@test/provider') + expect(provider?.schemaKeys).toEqual(['limit']) + } + }) + it('rejects type-only imports used as runtime schemas', () => { const { root, write } = fixture() write('packages/test/provider/src/index.ts', ` diff --git a/scripts/gen-config-catalog.ts b/scripts/gen-config-catalog.ts index 60a31c9fdb..df0ac078d4 100644 --- a/scripts/gen-config-catalog.ts +++ b/scripts/gen-config-catalog.ts @@ -504,7 +504,7 @@ function unwrapExpr(expr: ts.Expression): ts.Expression { * packages whose schemas an intersect composes. A key path is the top-level * key or a nested path through object/array compositions (`agents[].id`). * Handles object/union calls, chained refinements, named const schema imports - * through public workspace source paths, and `z.intersect([X.Config, …])`; errors on + * through package-relative or public workspace source paths, and `z.intersect([X.Config, …])`; errors on * anything else, so a schema the walk cannot see fails the gate instead of * silently thinning it. Nested values that are neither `object` nor `array` * compositions (primitives, unions, dynamic-key dicts) contribute no paths. diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 89b1432bc4..0113d16160 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -805,6 +805,7 @@ export const LINK_MAP: Readonly> = { WorkflowRunInfo: 'workflow.md', WorkflowStartRequest: 'workflow.md', ProjectionDefinition: 'session-projection.md', + ProjectionHandle: 'session-projection.md', SessionProjectionMap: 'session-projection.md', SessionProjectionStateMap: 'session-projection.md', ProjectionChangeListener: 'session-projection.md', diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 37f21e08c1..4218ec3c8c 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -2116,6 +2116,11 @@ "symbol": "PresetSpec", "source": "packages/interaction/permission-presets/src/index.ts" }, + { + "doc": "docs/subsystems/session-projection.md", + "symbol": "ProjectionHandle", + "source": "packages/session/session-projection/src/index.ts" + }, { "doc": "docs/subsystems/session-projection.md", "symbol": "ProjectionDefinition",