Merge remote-tracking branch 'origin/master' into feat/loader-entry-disabled-interpolation

# Conflicts:
#	vendor/README.md
#	vendor/loader/src/config/entry.ts
This commit is contained in:
Huanqi Cao
2026-08-11 18:43:40 +08:00
993 changed files with 3516 additions and 16974 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-13-capability-seams.md
2026-06-13-capability-seams.md: ca5071cff1b26ba3b89537a73f40d7ecbde4b2bb
2026-06-13-capability-seams.md: b2dc124f7bf0b56598466e6521e0764af06075ab
2026-06-13-capability-seams.zh.md: 7790124b980e0525705c2db398802ea6a418685a

View File

@@ -6,7 +6,7 @@ English | [中文](2026-06-13-capability-seams.zh.md)
## Problem
The harness has swappable capabilities — bash execution today, sandboxed/remote executors and alternative model providers tomorrow. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer surface* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed.
The harness has swappable capabilities — bash execution today, sandboxed/remote executors and alternative model providers tomorrow. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer API* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed.
This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.bash`; a consumer declares `inject: ['bash']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this Agent Note does.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
2026-06-17-filesystem-capability-seam.md: 928ad4926e46ed5b6b35552b75882d565d8ad6db
2026-06-17-filesystem-capability-seam.md: 63628d592c17f000ae49f9558597bf8059f04942
2026-06-17-filesystem-capability-seam.zh.md: 02af77a3cc291eeba2be966a4bc08aa006675e7d

View File

@@ -12,7 +12,7 @@ That couples three concerns that change independently:
1. The filesystem contract: what operations plugins can ask for.
2. The backend: local disk now, sandboxed/remote/project-scoped filesystem later.
3. The consumer surface: model-facing `read` / `write` / `edit` schemas and result formatting.
3. The consumer API: model-facing `read` / `write` / `edit` schemas and result formatting.
Without a `ctx.fs` interface, swapping local filesystem access for a sandboxed or remote backend would churn the tool schemas, demos, and prompt guidance even when the model-facing contract should stay stable. It also makes permission/sandbox boundaries harder to reason about: a `cwd` option can look like a sandbox even though it is only a base path unless an explicit backend or `tools/execute` policy enforces containment.
@@ -139,7 +139,7 @@ The defensive-pattern classes this repo has been bitten by are pinned directly:
## Alternatives considered
- **Model-facing tools directly over `node:fs`** — the tool package would own execution policy, path resolution, atomic writes, text decoding, and edit semantics at once, coupling the three independently-changing concerns the Problem names and churning schemas on any backend swap.
- **One combined `dsh-fs-tools` package** — the pre-seam shape; rejected for the same Service Definition / Service provider / Consumer split as bash, and the combined name never became public surface.
- **One combined `dsh-fs-tools` package** — the pre-seam shape; rejected for the same Service Definition / Service provider / Consumer split as bash, and the combined name never became public API.
- **Observed-state on `ctx.fs`** — the shape this Agent Note first landed; superseded by [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [the event-gate Agent Note](2026-06-26-file-context-as-event-gate.md): a sandboxed/remote backend must not inherit model-facing observation policy, so the provider keeps only the version token and the optional version-guarded mutation.
## Consequences
@@ -160,4 +160,4 @@ The defensive-pattern classes this repo has been bitten by are pinned directly:
**Error codes become part of the seam.** `FsError` codes make stale-version and observation failures machine-routable through the existing structured error taxonomy. The cost is that `dsh-fs` imports the shared `HarnessError` base from `dsh-llm`; that dependency is intentional and stays limited to the error vocabulary.
**Package churn is front-loaded.** The three-package split adds boilerplate before there is more than one backend. This is intentional: filesystem access is a likely sandbox/remote boundary, and changing the package surface after shipping model-facing tools would be more expensive.
**Package churn is front-loaded.** The three-package split adds boilerplate before there is more than one backend. This is intentional: filesystem access is a likely sandbox/remote boundary, and changing the package API after shipping model-facing tools would be more expensive.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md
2026-06-18-agent-lifecycle-and-ownership-contracts.md: ad334d30afcaf1864a1ea3dd42f3c5b7d157603b
2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md: b9240969c9e952c1b38cbd4c088853005e156067
2026-06-18-agent-lifecycle-and-ownership-contracts.md: c3522d18dad2703664f12364e4d70cc057a3a945
2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md: 16781c8a5d1a1c67d1fe9296b0f34f2a00b11caf

View File

@@ -43,7 +43,7 @@ The bash owner-token comparison relies on the shared `Agent.id`/`SessionId` bein
- **A public `BashTask.owner` field** instead of the `BashExecutor.ownerOf(id)` Service Definition method — rejected: one read path, no redundant API.
- **Sibling cordis effects for the agent's session lifecycle** — rejected: a fiber unload disposes sibling effects concurrently (`Promise.all`), racing removal of the store-owned append publication hooks against the loop's closing `session/flush`; the single composite effect's ordered LIFO chain is what captures the closing `turn/end` on both disposal paths.
- **A separate step-only `abort()` beside `cancel()`** — shipped originally, then removed as unused; `cancel()` is the single public stop primitive ([the public-stop-surface Agent Note](../simplification/2026-06-20-public-agent-stop-surface.md)).
- **A separate step-only `abort()` beside `cancel()`** — shipped originally, then removed as unused; `cancel()` is the single public stop primitive ([the public-stop-API Agent Note](../simplification/2026-06-20-public-agent-stop-api.md)).
## Consequences

View File

@@ -43,7 +43,7 @@ bash 所有者 token 比较依赖共享的 `Agent.id`/`SessionId` 在存活 agen
- **公开的 `BashTask.owner` 字段**而非 `BashExecutor.ownerOf(id)` Service Definition 方法:否决。一条读取路径即可,无需冗余 API。
- **为 agent 的会话生命周期使用兄弟 Cordis effect**否决。fiber 卸载时并发释放兄弟 effect`Promise.all`store 拥有的 append 发布钩子的移除与循环的关闭 `session/flush` 产生竞争;单一复合 effect 的有序 LIFO 链才能在两条释放路径上都捕获关闭的 `turn/end`
- **在 `cancel()` 之外另设一个仅中止步骤的 `abort()`**:最初发布过,后因无人使用而移除;`cancel()` 是唯一的公开停止原语(见[公开停止接口 Agent Note](../simplification/2026-06-20-public-agent-stop-surface.md))。
- **在 `cancel()` 之外另设一个仅中止步骤的 `abort()`**:最初发布过,后因无人使用而移除;`cancel()` 是唯一的公开停止原语(见[公开停止接口 Agent Note](../simplification/2026-06-20-public-agent-stop-api.md))。
## 后果

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
2026-06-18-shared-persistence-write-coordinator.md: c376bb4a209a250d33274dfd60531475354ba363
2026-06-18-shared-persistence-write-coordinator.md: 93b6cd1bd058499e71948d3909de8e4c076b445e
2026-06-18-shared-persistence-write-coordinator.zh.md: 06a2dafc4d14c29bb7758fdc67d3f0f8136983e2

View File

@@ -40,7 +40,7 @@ The shared `runPersistenceContract` (public-API contract) runs for every backend
## Alternatives considered
- **A base class the backends extend** — rejected for composition: a backend exposes only the hooks, cannot reach the coordinator's private orchestration state, and a third-party backend may still implement the abstract service directly without the coordinator at all.
- **A wider hook surface** — each candidate hook folds away: there is no scope-specific live lookup because `loadStored` plus the coordinator's cwd check preserves the collision boundary, no storage-locator generic because validated JSONL metadata reproduces its path while SQLite is already id-bound, no separate `materialize` hook because the first batch must commit atomically with materialization, no separate create-collision probe because it is `loadStored(id) !== undefined`, and no coordinator pass-through for `list()` because listing needs none of the orchestration.
- **A wider hook API** — each candidate hook folds away: there is no scope-specific live lookup because `loadStored` plus the coordinator's cwd check preserves the collision boundary, no storage-locator generic because validated JSONL metadata reproduces its path while SQLite is already id-bound, no separate `materialize` hook because the first batch must commit atomically with materialization, no separate create-collision probe because it is `loadStored(id) !== undefined`, and no coordinator pass-through for `list()` because listing needs none of the orchestration.
## Consequences

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-branded-ids.md
2026-06-20-branded-ids.md: 78242226103fef0eac320cdfac0ccb82292fd9e8
2026-06-20-branded-ids.md: ded48409bcb3deb19e35029fa795b5ea28c9f6d7
2026-06-20-branded-ids.zh.md: 613ef81d198f10ec70ca5cb019776e607f4a8a79

View File

@@ -60,7 +60,7 @@ Kept deliberately narrow per the "not every string needs a brand" policy. Each o
## Verification
The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-bash` and threaded end-to-end (Service Definition, the `dsh-bash-local` generation site, the `dsh-tool-bash` model-facing surface) with no `dsh-bash` dependency on `dsh-session`; no collection keyed by an in-scope branded id (`CallId`/`SessionId`/`BashTaskId`) is keyed by bare `string`; public method params and exported signatures keep the brand; and brands are constructed via the cast factory at each boundary where a raw string enters (provider call id, ACP session id, model-supplied `task_id`), never as scattered `as` casts.
The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-bash` and threaded end-to-end (Service Definition, the `dsh-bash-local` generation site, the `dsh-tool-bash` model-facing tool) with no `dsh-bash` dependency on `dsh-session`; no collection keyed by an in-scope branded id (`CallId`/`SessionId`/`BashTaskId`) is keyed by bare `string`; public method params and exported signatures keep the brand; and brands are constructed via the cast factory at each boundary where a raw string enters (provider call id, ACP session id, model-supplied `task_id`), never as scattered `as` casts.
## Consequences

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
2026-06-20-generic-long-running-tool-runtime.md: 0dd49fe60f2c45081973657ff960b780d2d47257
2026-06-20-generic-long-running-tool-runtime.zh.md: 6e8458aea73536859bf4c81894679d31a5034d3a
2026-06-20-generic-long-running-tool-runtime.md: 457eaac7de92ea37287803349672271148d59185
2026-06-20-generic-long-running-tool-runtime.zh.md: 9a86c4cf2b49973212adfd4b9acdb54150af01d1

View File

@@ -25,7 +25,7 @@ Long-running tools are producers. `dsh-tool-bash` adapts a `BashProcess` into in
The literal types live on the [tasks subsystem page](../../../../docs/subsystems/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, optional positive `outputLimitBytes`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id.
`outputLimitBytes` is producer-owned presentation policy, not a registry buffer. The registry validates and projects it unchanged into `TaskSnapshot`; generic control surfaces apply the cap to complete model-facing output after adding their own status or notice metadata. Omitting it preserves the existing surface behavior, so the runtime does not impose a hidden default on unrelated producer families.
`outputLimitBytes` is producer-owned presentation policy, not a registry buffer. The registry validates and projects it unchanged into `TaskSnapshot`; generic control APIs apply the cap to complete model-facing output after adding their own status or notice metadata. Omitting it preserves the existing controller behavior, so the runtime does not impose a hidden default on unrelated producer families.
A model-facing producer exposes that committed id in its canonical success value, normally `{ kind: 'background', taskId }`; Native rendering may keep human-readable prose. A pre-aborted background call fails rather than returning a no-op because no task exists to satisfy the promised handle. Once registration publishes the id, cancellation belongs to the task's own controller and the task runtime: later cancellation of the producing tool call must not kill the published task. `task_kill`, owner disposal, and service teardown request cancellation; foreground execution remains coupled to the call's `exec.signal`.
@@ -39,7 +39,7 @@ Statuses are `running`, `stopping`, `completed`, `killed`, and `failed`. Produce
The runtime attaches one continuation to `done`, records the first terminal outcome, resolves waiters, and invokes completion listeners with per-listener error containment. First-wins settlement matters during teardown: if `cancel` throws, the runtime force-fails the record and warns that work may be orphaned rather than waiting forever for a promise that may never settle. A later producer outcome cannot overwrite that diagnosis or notify twice. A `cancel` that returns without eventually settling `done` still blocks teardown because the runtime cannot distinguish it from a slow, valid stop.
Task registrations are not effects of the producer tool fiber. Reloading a tool or control-surface plugin therefore does not kill work owned by an agent and backend. The task service's own disposal cancels all live tasks and awaits contract-compliant producers.
Task registrations are not effects of the producer tool fiber. Reloading a tool or controller plugin therefore does not kill work owned by an agent and backend. The task service's own disposal cancels all live tasks and awaits contract-compliant producers.
## Authorization and owner lifecycle
@@ -51,7 +51,7 @@ The first task for an owner attaches one asynchronous effect to `owner.ctx`. Age
For contract-compliant producers, `AgentHandle.dispose()` resolves only after owned background work has stopped. Work intended to outlive an agent must be started unowned; survival across runtime restarts requires a separate durable-job design.
## Service surface
## Service API
`TaskService` provides:
@@ -61,13 +61,13 @@ For contract-compliant producers, `AgentHandle.dispose()` resolves only after ow
- `kill(id, caller?, reason?)` for cancellation.
- `wait(id, timeoutMs, caller?, signal?)` for bounded terminal waiting.
- `onTaskDone(listener)` for effect-scoped observation with exact-owner delivery and listener containment.
- `attachSurface(name)` for the control-surface availability fence.
- `attachController(name)` for the task-controller availability fence.
`wait` returns the terminal snapshot when the task settles or the live snapshot when its timeout expires. Aborting a wait cancels only that wait. If settlement has already assigned terminal delivery to the waiter, the terminal snapshot still wins. Waiters unregister synchronously on abort so a same-tick settlement cannot suppress a completion notice on behalf of a reader that receives nothing.
A producer loaded without any control surface would let callers start work they cannot collect or stop. `dsh-tool-tasks` therefore calls `attachSurface()` for its lifetime, and `start()` fails before producer execution when no surface is attached. This check occurs at start rather than plugin load because sibling plugins may activate concurrently. Custom non-model surfaces can attach themselves without teaching the registry tool names.
A producer loaded without any controller would let callers start work they cannot collect or stop. `dsh-tool-tasks` therefore calls `attachController()` for its lifetime, and `start()` fails before producer execution when no controller is attached. This check occurs at start rather than plugin load because sibling plugins may activate concurrently. Custom non-model controllers can attach themselves without teaching the registry tool names.
## Model-facing control surface
## Model-facing control API
`dsh-tool-tasks` registers three kind-independent tools with generic UI cards:
@@ -79,13 +79,13 @@ Stream reads share one task-scoped consuming cursor because the owning model is
The system prompt tells the model to retain task ids, continue independent work instead of busy-polling or duplicating a running task, collect relevant tasks before its final answer, and kill work that no longer matters. Completion injects a logged `context/message` into the exact owner's session; it becomes durable context for the next request but does not wake an idle agent.
The runtime marks a terminal task `reported` when a read or wait delivers it, when a live waiter has claimed delivery at settlement, or when the model explicitly kills it. Reported tasks do not inject redundant completion notices. Listener failures are logged independently, do not stop later listeners, and are not awaited by waiters or teardown. When a snapshot carries `outputLimitBytes`, `dsh-tool-tasks` preserves UTF-8 boundaries and reuses an existing producer truncation marker rather than duplicating it. Reads reserve status suffixes and retain the output tail; completion notices reserve the stable `background task <id>` prefix and `task_output` instruction before truncating variable kind, label, status, detail, or the truncation marker itself, so the minimum PTY cap still identifies the task to collect. The task surface resolves the caller-visible producer cap in a prepended pre-execute listener before policy can deny or short-circuit dispatch, then applies it through the task definitions' last-mile `finalizeContent` callback so normalized tool errors, outer pipeline failures, and single-text policy results cannot escape the bound; deliberately structured multi-block policy results retain policy ownership of their shape and size.
The runtime marks a terminal task `reported` when a read or wait delivers it, when a live waiter has claimed delivery at settlement, or when the model explicitly kills it. Reported tasks do not inject redundant completion notices. Listener failures are logged independently, do not stop later listeners, and are not awaited by waiters or teardown. When a snapshot carries `outputLimitBytes`, `dsh-tool-tasks` preserves UTF-8 boundaries and reuses an existing producer truncation marker rather than duplicating it. Reads reserve status suffixes and retain the output tail; completion notices reserve the stable `background task <id>` prefix and `task_output` instruction before truncating variable kind, label, status, detail, or the truncation marker itself, so the minimum PTY cap still identifies the task to collect. The task controller resolves the caller-visible producer cap in a prepended pre-execute listener before policy can deny or short-circuit dispatch, then applies it through the task definitions' last-mile `finalizeContent` callback so normalized tool errors, outer pipeline failures, and single-text policy results cannot escape the bound; deliberately structured multi-block policy results retain policy ownership of their shape and size.
## Producer opt-in
Each producer owns whether its schema exposes `run_in_background` through defaulted config. `dsh-tool-bash`, `dsh-tool-pty`, and each `dsh-tool-subagent` instance use `enableRunInBackground`, defaulting to true. A disabled instance omits the parameter and also rejects a forced background argument at execution because the generic argument validator permits undeclared keys. Schema omission advertises the capability; the execution check enforces it.
`ctx.tasks` does not rewrite producer schemas. A bundle forwards configuration only for producers it owns. If a background call reaches `start()` without an attached surface, the runtime fence fails before execution.
`ctx.tasks` does not rewrite producer schemas. A bundle forwards configuration only for producers it owns. If a background call reaches `start()` without an attached controller, the runtime fence fails before execution.
## Producer integrations
@@ -107,7 +107,7 @@ The current `TaskStart.run()` contract passes in-process callbacks and exact `Ag
### Consumer-owned authorization or cleanup events
Consumer-owned checks invite inconsistent or missing isolation on each new surface. A broadcast cleanup event makes every listener filter every agent and provides no registration disposer. Central authorization plus one owner-scoped effect gives every consumer the same fence and an awaited, removable lifecycle hook.
Consumer-owned checks invite inconsistent or missing isolation on each new controller. A broadcast cleanup event makes every listener filter every agent and provides no registration disposer. Central authorization plus one owner-scoped effect gives every consumer the same fence and an awaited, removable lifecycle hook.
### Blocking output or a separate wait tool
@@ -125,7 +125,7 @@ Authorization, not unguessability, is the access boundary, and ids do not derive
## Testing
Unit coverage pins preflight atomicity, per-kind ids, output-limit validation and projection, complete UTF-8 result bounds, stream and final reads, wait timeout and abort races, cancellation, first-wins settlement, listener containment, notice suppression, owner isolation, stale owner instances, owner cleanup, service teardown, and the no-surface fence. Producer tests cover bash process mapping, subagent startup cancellation, terminal mapping, and disposal. Snapshot coverage pins the control-tool schemas and prompt guidance.
Unit coverage pins preflight atomicity, per-kind ids, output-limit validation and projection, complete UTF-8 result bounds, stream and final reads, wait timeout and abort races, cancellation, first-wins settlement, listener containment, notice suppression, owner isolation, stale owner instances, owner cleanup, service teardown, and the no-controller fence. Producer tests cover bash process mapping, subagent startup cancellation, terminal mapping, and disposal. Snapshot coverage pins the control-tool schemas and prompt guidance.
## Consequences

View File

@@ -25,7 +25,7 @@ Status: implemented
字面类型见[任务子系统页面](../../../../docs/subsystems/tasks.md)。生产方调用 `ctx.tasks.start()`,传入 kind、label、可选的所属 `Agent`、可选的正数 `outputLimitBytes` 与一个 `run()` 函数。运行时会在调用 `run()` 前完成所有可能失败的预检工作,并且只调用一次。`run()` 返回钩子后,注册过程不会再执行可能失败的步骤而直接提交;生产方无法启动没有可收集 task id 的工作。
`outputLimitBytes` 是生产方拥有的呈现策略,而非注册表缓冲区。注册表校验该值,并将其原样投影到 `TaskSnapshot`;通用控制接口添加自身的状态或通知元数据后,再将该上限应用于完整的面向模型输出。省略该值时保持现有接口行为,因此运行时不会向无关的生产方类别施加隐式默认值。
`outputLimitBytes` 是生产方拥有的呈现策略,而非注册表缓冲区。注册表校验该值,并将其原样投影到 `TaskSnapshot`;通用任务控制器添加自身的状态或通知元数据后,再将该上限应用于完整的面向模型输出。省略该值时保持现有控制器行为,因此运行时不会向无关的生产方类别施加隐式默认值。
面向模型的生产方会在规范成功值中暴露已提交的 id通常为 `{ kind: 'background', taskId }`Native 渲染仍可保留便于人类阅读的行文。预先被中止的后台调用会失败,而不是返回空操作,因为不存在可履行所承诺句柄的任务。一旦注册过程发布 id取消就归任务自身的控制器与任务运行时所有随后取消生产工具调用不得终止已发布的任务。`task_kill`、所有者资源释放和服务拆除会请求取消;前台执行仍与调用的 `exec.signal` 耦合。
@@ -39,7 +39,7 @@ Status: implemented
运行时为 `done` 附加一个 continuation记录第一个终止结果、解决等待方并逐个调用完成监听器同时隔离每个监听器的错误。首次结果优先的结算在资源销毁期间至关重要如果 `cancel` 抛出,运行时会强制将记录标为失败,并警告工作可能遗留,而不是永远等待一个可能永不完成的 promise。后续生产方结果不能覆盖该诊断也不能重复通知。`cancel` 返回后如果最终未使 `done` 完成,仍会阻塞资源销毁,因为运行时无法区分这种情况与缓慢但有效的停止。
任务注册不是生产方工具 fiber 的 effect。因此重新加载工具或控制接口插件不会终止由 agent智能体和后端拥有的工作。任务服务自身释放时会取消所有实时任务并等待遵守约定的生产方。
任务注册不是生产方工具 fiber 的 effect。因此重新加载工具或控制插件不会终止由 agent智能体和后端拥有的工作。任务服务自身释放时会取消所有实时任务并等待遵守约定的生产方。
## 授权与所有者生命周期
@@ -61,13 +61,13 @@ task id 在运行时全局可见且可预测,因此注册表会授权每次访
- `kill(id, caller?, reason?)`:取消。
- `wait(id, timeoutMs, caller?, signal?)`:有界的终止等待。
- `onTaskDone(listener)`effect 作用域内的观察,具有精确所有者投递和监听器隔离。
- `attachSurface(name)`控制接口可用性防线。
- `attachController(name)`任务控制器可用性防线。
`wait` 在任务完成时返回终止快照,在等待超时时返回实时快照。中止一次等待只取消该次等待。如果结算已经将终止投递分配给该等待方,终止快照仍然优先。等待方在中止时同步注销,因此同一 tick 内的结算无法代表一个什么也未收到的读取方压制完成通知。
如果生产方加载时没有任何控制接口,调用方就能启动无法收集或停止的工作。因此,`dsh-tool-tasks` 在其整个生命周期内调用 `attachSurface()`;没有附加接口时,`start()` 会在生产方开始执行前失败。该检查发生在启动时而非插件加载时,因为兄弟插件可能并发激活。自定义的非模型接口可以自行附加,无需让注册表了解工具名称。
如果生产方加载时没有任何任务控制器,调用方就能启动无法收集或停止的工作。因此,`dsh-tool-tasks` 在其整个生命周期内调用 `attachController()`;没有附加控制器时,`start()` 会在生产方开始执行前失败。该检查发生在启动时而非插件加载时,因为兄弟插件可能并发激活。自定义的非模型控制器可以自行附加,无需让注册表了解工具名称。
## 面向模型的控制接口
## 面向模型的控制
`dsh-tool-tasks` 注册三个与 kind 无关的工具,并使用通用 UI 卡片:
@@ -79,13 +79,13 @@ task id 在运行时全局可见且可预测,因此注册表会授权每次访
系统提示词要求模型保留 task id、在后台工作运行时继续处理独立工作而非忙轮询或重复启动同一任务、在给出最终答案前收集相关任务并终止不再重要的工作。完成时系统会向确切所有者的会话注入一条已记录的 `context/message`;它会成为下一个请求的持久上下文,但不会唤醒空闲的 agent。
当读取或等待交付终止任务、实时等待方在结算时认领了投递,或模型显式终止任务时,运行时将终止任务标为 `reported`。已报告的任务不会注入冗余的完成通知。监听器失败会独立记录,不会阻止后续监听器,也不会被等待方或资源销毁过程等待。当快照携带 `outputLimitBytes` 时,`dsh-tool-tasks` 会保持 UTF-8 边界,并复用生产方已有的截断标记,而不会重复添加。读取会为状态后缀预留空间并保留输出尾部;完成通知会先为稳定的 `background task <id>` 前缀与 `task_output` 指令预留空间,再截断可变的 kind、label、status、detail乃至截断标记本身因此 PTY 的最小上限仍能标识需要收集的任务。任务接口在策略有机会拒绝或短路分发之前,于最先执行的 pre-execute 监听器中解析调用方可见的生产方上限;随后通过任务定义最后一道的 `finalizeContent` 回调应用该上限,使规范化的工具错误、外层流水线失败与单文本策略结果都无法绕过该边界;经特意结构化的多块策略结果仍由策略拥有其形状与大小。
当读取或等待交付终止任务、实时等待方在结算时认领了投递,或模型显式终止任务时,运行时将终止任务标为 `reported`。已报告的任务不会注入冗余的完成通知。监听器失败会独立记录,不会阻止后续监听器,也不会被等待方或资源销毁过程等待。当快照携带 `outputLimitBytes` 时,`dsh-tool-tasks` 会保持 UTF-8 边界,并复用生产方已有的截断标记,而不会重复添加。读取会为状态后缀预留空间并保留输出尾部;完成通知会先为稳定的 `background task <id>` 前缀与 `task_output` 指令预留空间,再截断可变的 kind、label、status、detail乃至截断标记本身因此 PTY 的最小上限仍能标识需要收集的任务。任务控制器在策略有机会拒绝或短路分发之前,于最先执行的 pre-execute 监听器中解析调用方可见的生产方上限;随后通过任务定义最后一道的 `finalizeContent` 回调应用该上限,使规范化的工具错误、外层流水线失败与单文本策略结果都无法绕过该边界;经特意结构化的多块策略结果仍由策略拥有其形状与大小。
## 生产方显式启用
每个生产方通过带默认值的配置,自行决定其 schema 是否暴露 `run_in_background``dsh-tool-bash``dsh-tool-pty` 和每个 `dsh-tool-subagent` 实例都使用 `enableRunInBackground`,默认值为 true。禁用的实例会省略该参数由于通用参数校验器允许未声明的键它还会在执行时拒绝强制传入的后台参数。省略 schema 用于声明能力不可用;执行检查负责强制该约束。
`ctx.tasks` 不改写生产方 schema。bundle 只转发其所拥有生产方的配置。如果后台调用在没有附加接口的情况下到达 `start()`,运行时防线会在执行前使其失败。
`ctx.tasks` 不改写生产方 schema。bundle 只转发其所拥有生产方的配置。如果后台调用在没有附加控制器的情况下到达 `start()`,运行时防线会在执行前使其失败。
## 生产方集成
@@ -125,7 +125,7 @@ bash seam 暴露 `resolve`、`run` 和 `start`。`start(spec)` 返回一个 `Bas
## 测试
单元覆盖固定预检原子性、按 kind 分配的 id、输出上限的校验与投影、完整结果的 UTF-8 字节上限、流式与最终读取、等待超时与中止竞态、取消、首次结果优先的结算、监听器隔离、通知压制、所有者隔离、陈旧的所有者实例、所有者清理、服务资源销毁和无接口防线。生产方测试覆盖 bash 进程映射、subagent 启动取消、终止映射与释放。快照覆盖固定控制工具 schema 与提示词指导。
单元覆盖固定预检原子性、按 kind 分配的 id、输出上限的校验与投影、完整结果的 UTF-8 字节上限、流式与最终读取、等待超时与中止竞态、取消、首次结果优先的结算、监听器隔离、通知压制、所有者隔离、陈旧的所有者实例、所有者清理、服务资源销毁和无控制器防线。生产方测试覆盖 bash 进程映射、subagent 启动取消、终止映射与释放。快照覆盖固定控制工具 schema 与提示词指导。
## 后果

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
2026-06-24-web-capability-seam.md: ccf04420425555055ce3810d25bf764db17322bb
2026-06-24-web-capability-seam.md: 5df68ea1f0c32491f48d6da98a8fbce0e658e102
2026-06-24-web-capability-seam.zh.md: 73f7d0d9b82c03d41bdc768f22b22316e1bc907f

View File

@@ -8,7 +8,7 @@ English | [中文](2026-06-24-web-capability-seam.zh.md)
The harness needs model-facing web tools without binding the model contract to one vendor's API shape. Search is the immediate pressure point: supporting both Exa search and Perplexity search from the start — two deliberately different provider shapes (Exa returns a flat `results[]` of `{title, url, highlights, publishedDate}`; Perplexity returns a generated answer plus citations) — is what proves the normalized web contract does not just mirror one vendor. Fetch is a separate operation: an anonymous public HTTP(S) fetch backend has transport, security, redirect, decoding, and size-limit concerns that are not the same as provider-backed search.
The model-facing surface must stay stable while backends change. A search provider swap should not change how the model asks for a query, and a fetch implementation swap should not change how the model asks for a URL. Conversely, a provider package should not expose its own model-facing tool schema just because it has extra provider-specific knobs.
The model-facing API must stay stable while backends change. A search provider swap should not change how the model asks for a query, and a fetch implementation swap should not change how the model asks for a URL. Conversely, a provider package should not expose its own model-facing tool schema just because it has extra provider-specific knobs.
Putting search and fetch directly in `dsh-tool-web` would make the model-facing tool own provider selection, backend request mapping, transport policy, result normalization, prompt guidance, presentation, and schema registration at once. Letting each provider register its own tool has the opposite problem: tool availability, names, descriptions, and parameters would depend on whichever provider packages happen to load, and provider-specific fields would leak into the model contract.
@@ -74,7 +74,7 @@ Provider packages depend only on `dsh-web` and Cordis. They own credentials, end
## `ctx.web` contract
`ctx.web` is a provider registry plus a provider-selecting execution surface. The registry half stays close to `LlmService`: a `Map<id, provider>` per capability kind, `registerSearchProvider` / `registerFetchProvider` methods that return disposers, duplicate ids that throw `WebError`, and execution-time resolution that throws when the selected provider is absent or unusable. The authoritative signatures live in `packages/web/web/src/types.ts`; the seam's shape:
`ctx.web` is a provider registry plus a provider-selecting execution API. The registry half stays close to `LlmService`: a `Map<id, provider>` per capability kind, `registerSearchProvider` / `registerFetchProvider` methods that return disposers, duplicate ids that throw `WebError`, and execution-time resolution that throws when the selected provider is absent or unusable. The authoritative signatures live in `packages/web/web/src/types.ts`; the seam's shape:
```ts
import type { WebFetchRequest, WebFetchResult, WebSearchRequest, WebSearchResult } from '@deepseek-ai/dsh-web'
@@ -191,7 +191,7 @@ interface WebSearchSource {
}
```
`content` is optional provider-generated answer text, search context, or summary. `sources[]` is the portable citation surface. A source always has a URL; title, snippet, and `publishedAt` are optional because not every provider returns them. `title` is not required: Perplexity-style citations may provide only URLs, and forcing adapters to invent titles would make the seam lie. `dsh-tool-web` renders a `title ?? hostname(url)`-style fallback label for display. `publishedAt` is an optional publication/crawl timestamp as an ISO-8601 string — Exa returns it as `publishedDate` on each result and Perplexity returns a `date` on search results, so it is real provider data, not derived; the seam carries it as a string and leaves date parsing to the consumer.
`content` is optional provider-generated answer text, search context, or summary. `sources[]` is the portable citation shape. A source always has a URL; title, snippet, and `publishedAt` are optional because not every provider returns them. `title` is not required: Perplexity-style citations may provide only URLs, and forcing adapters to invent titles would make the seam lie. `dsh-tool-web` renders a `title ?? hostname(url)`-style fallback label for display. `publishedAt` is an optional publication/crawl timestamp as an ISO-8601 string — Exa returns it as `publishedDate` on each result and Perplexity returns a `date` on search results, so it is real provider data, not derived; the seam carries it as a string and leaves date parsing to the consumer.
Exa search maps each entry of the provider's flat `results[]` into a `WebSearchSource`: `url``url`, `title``title`, `snippet` ← the first `highlights[]` entry (an entry with no highlight has no portable snippet and is dropped), `publishedAt``publishedDate`. Exa returns no provider-generated answer, so `content` is omitted. Perplexity search maps `choices[0].message.content` to `content` and prefers the structured top-level `search_results[]` for `sources[]``url``url`, `title``title`, `snippet``snippet` (often empty), `publishedAt``date` — falling back to the URL-only `citations[]` array only when `search_results` is absent (those sources carry just a `url`). If a provider returns fewer structured fields than the seam supports, the adapter omits those optional fields.
@@ -294,7 +294,7 @@ This resembles OpenCode's local web search: one stable `websearch` tool dispatch
### Split search and fetch into two seams (`dsh-search`, `dsh-fetch`)
Tempting because the two halves share no request schema and no business logic, so each would map cleanly onto the bash/fs three-package template, and the `Search`/`Fetch` method-pair duplication on `WebService` would disappear. Rejected because the shared machinery — provider-id registry, registration-order-independent selection policy, abort propagation, the `WebError` taxonomy, and the product-facing "how this harness reaches the web" config surface — is real and would otherwise be duplicated across two near-identical seams. One `ctx.web` middle layer gives the product a single thing to inject and configure and gives provider selection one owner. The price is the parallel `searchX`/`fetchX` method pairs, which is accepted deliberately.
Tempting because the two halves share no request schema and no business logic, so each would map cleanly onto the bash/fs three-package template, and the `Search`/`Fetch` method-pair duplication on `WebService` would disappear. Rejected because the shared machinery — provider-id registry, registration-order-independent selection policy, abort propagation, the `WebError` taxonomy, and the product-facing "how this harness reaches the web" configuration API — is real and would otherwise be duplicated across two near-identical seams. One `ctx.web` middle layer gives the product a single thing to inject and configure and gives provider selection one owner. The price is the parallel `searchX`/`fetchX` method pairs, which is accepted deliberately.
### Choose the first registered provider

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md
2026-06-26-file-context-as-event-gate.md: 743c8f150a83f2452cb0ff672860b8462a56f762
2026-06-26-file-context-as-event-gate.md: 5f61201c8129b74233222bdc71eaf20443794760
2026-06-26-file-context-as-event-gate.zh.md: cd79b44dd3f3b75c5e5addc5be10c02bd1ce0151

View File

@@ -138,7 +138,7 @@ The tool passes `exec` (the tool-execution context) as the `actor` argument on e
An observed-state entry is the **prior-observation record**, but its discriminant matters. Successful read/write/edit records present at a version, allowing create-then-edit or edit-then-edit without an intervening read. A read/view that confirms absence replaces any old positive version with absent, allowing only a guarded create; a later successful create replaces it with the new present version. Missing entry alone means unseen and produces `FS_NOT_OBSERVED` for edit. The owner is derived structurally from `{ agent?: { session? } }`; disposal drops all state (HMR safety).
`dsh-fs-policy` is now a pure policy/recording plugin with no service surface — it influences the world only through the event gate. That is what removes the method coupling from `dsh-tool-fs`.
`dsh-fs-policy` is now a pure policy/recording plugin with no service API — it influences the world only through the event gate. That is what removes the method coupling from `dsh-tool-fs`.
## Bare-provider behavior (no `dsh-fs-policy`)

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md
2026-06-30-bash-stdin-env-trusted-plugin-surface.md: d8193b47ff16da7efb5f6bf1c3f34c0e1251572c
2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md: 7f5bb62d92f27c49b0f7f262dc64dce39c89f2cd
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md
2026-06-30-bash-stdin-env-trusted-plugin-api.md: 2087b2f7a9682ad5f554d4a7a2f521485d164432
2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md: d3ed6a86c7f2e356f50a918dddf3fe47ea7ae820

View File

@@ -2,7 +2,7 @@
Status: implemented
English | [中文](2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md)
English | [中文](2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md)
## Problem
@@ -30,4 +30,4 @@ Three deliberate choices:
## Consequences
Hook bridges pass JSON payloads and hook-specific variables through the existing bash seam, retaining its process-group, truncation, and spill behavior. The model surface remains unchanged, and the bash tool remains the sole owner of model-call request construction. The vocabulary lives in [the bash data-structure reference](../../../../docs/subsystems/bash.md).
Hook bridges pass JSON payloads and hook-specific variables through the existing bash seam, retaining its process-group, truncation, and spill behavior. The model-facing behavior remains unchanged, and the bash tool remains the sole owner of model-call request construction. The vocabulary lives in [the bash data-structure reference](../../../../docs/subsystems/bash.md).

View File

@@ -2,7 +2,7 @@
Status: implemented
[English](2026-06-30-bash-stdin-env-trusted-plugin-surface.md) | 中文
[English](2026-06-30-bash-stdin-env-trusted-plugin-api.md) | 中文
## 问题

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md
2026-06-30-event-domain-semantics.md: dbb3ef49979b7eeaa9a43b7db4a2d5a5839b5569
2026-06-30-event-domain-semantics.md: 70da718b5471ce309a090c8aade3e7290cc949dc
2026-06-30-event-domain-semantics.zh.md: 93fe14adeaa47d276219a316ff1250a9982f5f14

View File

@@ -1,4 +1,4 @@
# Agent Note: Event-domain semantics — session is the fact log, agent is the live surface
# Agent Note: Event-domain semantics — session is the fact log, agent is the live event channel
Status: implemented

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
2026-07-05-reconstructable-requests.md: d7b867d6e17c2dfe0e49b70bd0f5e7ff173fb572
2026-07-05-reconstructable-requests.md: e78284964ca85905524d3a0800b2de46c6964094
2026-07-05-reconstructable-requests.zh.md: c41e5f89558a9c7b09ccd2bf4fd5b7d9a6cde419

View File

@@ -37,7 +37,7 @@ Like MiniCode, the conversation advances append-only and resets only when model-
## Alternatives considered
- **Client as source of truth** (literal MiniCode): a second operative truth beside the log — the two drift and nothing notices; see the section above.
- **A stateful transmission client mirroring the log** — duplicates conversation state, needs rollback around listeners, leaves an unlogged edit surface, and still cannot reconstruct request headers. Session-owned caches plus logged headers avoid those split truths.
- **A stateful transmission client mirroring the log** — duplicates conversation state, needs rollback around listeners, leaves an unlogged edit path, and still cannot reconstruct request headers. Session-owned caches plus logged headers avoid those split truths.
- **Per-call request scalars** (a freely mutable config handed to each `agent/request` dispatch): a listener flips the model per call with zero accounting, silently abandoning the provider cache this design exists to protect. Config is per-conversation logged state; the waterfall proposes, the log records.
- **Detect-and-report** (compare consecutive requests, warn on divergence): catches violations after the fact; a violating request is still constructible and ships. Rejected for interface-level unrepresentability.
- **Event-driven assembly** (re-render only on change signals): a missed-signal bug class — a tool registered mid-session emits `tools/change`, not `system-prompt/change`, and a third-party provider may emit nothing. Per-step render + value compare is robust with zero signal discipline.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.md
2026-07-05-windows-jsonl-durable-publish.md: 38c4adc7a4f85d45e53e70fcac84073ab4e50775
2026-07-05-windows-jsonl-durable-publish.md: 546cde6086f3c84e22b4d4de144a48bb3425cd4e
2026-07-05-windows-jsonl-durable-publish.zh.md: 205460bcd374bd351cfdf541eb4041c09461229d

View File

@@ -16,7 +16,7 @@ The JSONL backend forks inside `materialize()` before any namespace mutation. Sh
POSIX keeps the existing protocol: create the root, project directory, and session directory with parent directory fsyncs, write and fsync a temp file, publish with `link()` so an existing final log is never overwritten, fsync the session directory, then remove the redundant temp hard link.
Windows creates missing directories through a durable staging publish: create a random sibling directory under the constant `.dsh-mkdir-` prefix, independent of the target basename, then publish it to the final directory name with `MoveFileExW(..., MOVEFILE_WRITE_THROUGH)` without `MOVEFILE_REPLACE_EXISTING` or `MOVEFILE_COPY_ALLOWED`. File materialization writes and fsyncs the temp log, then publishes that temp file to the final path with the same write-through `MoveFileExW` call and no replacement. `koffi` is the minimal Win32 bridge for this API surface; its install script is allowed in `pnpm-workspace.yaml` because the package ships the native loader and prebuilt platform modules.
Windows creates missing directories through a durable staging publish: create a random sibling directory under the constant `.dsh-mkdir-` prefix, independent of the target basename, then publish it to the final directory name with `MoveFileExW(..., MOVEFILE_WRITE_THROUGH)` without `MOVEFILE_REPLACE_EXISTING` or `MOVEFILE_COPY_ALLOWED`. File materialization writes and fsyncs the temp log, then publishes that temp file to the final path with the same write-through `MoveFileExW` call and no replacement. `koffi` is the minimal Win32 bridge for this API; its install script is allowed in `pnpm-workspace.yaml` because the package ships the native loader and prebuilt platform modules.
## Alternatives considered

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md
2026-07-06-timeout-deadline-library.md: b7c7ac07ce0acec819fa7e88b32b3b6d624f3e6e
2026-07-06-timeout-deadline-library.md: 7b252052c27fbf9f10716bd874a371b102abb614
2026-07-06-timeout-deadline-library.zh.md: e4ec6b9a9e07b55faf9f44f2b99b765620e626c4

View File

@@ -18,7 +18,7 @@ Each new external-process or network tool re-derived the same four things — cl
`@deepseek-ai/dsh-timeout` lives under `packages/util/` (peer to `dsh-brand`) and owns the *timing and classification* half of timeout; the *termination* half — the hard kill — stays in each capability's implementation. It is a library of pure functions, **not** a cordis service or plugin: it takes no `ctx`, registers nothing, holds no cross-call state, and emits no events. There is deliberately no central "timeout service" that would have to know how to stop every capability's work — that knowledge is exactly what a microkernel keeps out of shared layers, and what Codex's exec-only `ExecExpiration` scope demonstrates.
### The library surface
### The library API
Four functions, one watchdog interface, and one reason type:

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md
2026-07-06-tool-result-retention-library.md: 5e42660360e5a23b419c75b9c8006bec459bc322
2026-07-06-tool-result-retention-library.md: 1736d2dad98cbb8b67d570ce25742a84ea0ede59
2026-07-06-tool-result-retention-library.zh.md: 2a523e2eff36daf9100ac10bf5ae569eca30f830

View File

@@ -142,7 +142,7 @@ The formatter hook is deliberately small: a tool turns a `RetentionNotice` into
**Boundaries the library holds.** `truncated` means the retainer omitted otherwise-available content because of a budget; it never means the upstream was incomplete. Tool-specific states — `incomplete`, permission failures, provider partial failures, binary skips, bash spill-path recovery, invalid UTF-8 — stay in tool-domain fields, outside the retainer. When a future change migrates a tool, that package's README and tests must prove the model-facing result text is unchanged except for deliberate notice wording.
**Tradeoffs accepted.** The v1 surface deliberately supports only item `head` retention and text `head` / `tail` / `headTail`; windows, grouped budgets, sort-aware caps, and upstream-stop control wait until a second consumer proves the need. Text retention counts bytes for process/body safety, leaving character- and line-level preview budgets as separate tool-owned concerns.
**Tradeoffs accepted.** The v1 API deliberately supports only item `head` retention and text `head` / `tail` / `headTail`; windows, grouped budgets, sort-aware caps, and upstream-stop control wait until a second consumer proves the need. Text retention counts bytes for process/body safety, leaving character- and line-level preview budgets as separate tool-owned concerns.
## Alternatives considered

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md
2026-07-08-tool-output-spill-files.md: 0c8e5e25fc8a229db8fca36512ba781e548b5256
2026-07-08-tool-output-spill-files.md: 3c24bc9ed754726b70e833627a22167c8256162c
2026-07-08-tool-output-spill-files.zh.md: 771253335bf5822cb855c63e7dd0bbf0d517bab3

View File

@@ -10,7 +10,7 @@ Tool outputs need bounded model-facing previews, but some oversized results are
Before this change the behavior was uneven. `dsh-bash-local` already writes complete stdout/stderr streams to private temp spill files when its in-memory tail overflows, but ordinary text tool results were returned inline unless the tool hand-rolled its own cap. The [tool result retention library](2026-07-06-tool-result-retention-library.md) owns preview mechanics, but it does not own storage or an execution-pipeline policy that applies those mechanics to final tool results.
The shape matches the timeout policy design: a tool author declares a canonical value plus Native renderer, and a policy plugin enforces the deployment's default context budget on rendered content. Tool-specific early spill remains possible for provider acquisition bounds; tool-owned surface spill may retain a complete acquired canonical value while replacing only presentation. The [canonical tool-output contract](2026-07-20-canonical-tool-output-contract.md) owns that split.
The shape matches the timeout policy design: a tool author declares a canonical value plus Native renderer, and a policy plugin enforces the deployment's default context budget on rendered content. Tool-specific early spill remains possible for provider acquisition bounds; tool-owned presentation spill may retain a complete acquired canonical value while replacing only presentation. The [canonical tool-output contract](2026-07-20-canonical-tool-output-contract.md) owns that split.
## Decision

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
2026-07-10-single-file-executable-sdk-runtime-distribution.md: c2b6d9ff1825915e39738bf8f782c302ecfc1d0d
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d7758a77083e07b1d2cac99ae2be3f15e6edd2dc
2026-07-10-single-file-executable-sdk-runtime-distribution.md: 01081f1e0b8027420fedbc599f99c67e67a4639b
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: fadfc8cdcb0651665b965126ab0b25ed4218a533

View File

@@ -6,7 +6,7 @@ English | [中文](2026-07-10-single-file-executable-sdk-runtime-distribution.zh
## Problem
DeepSeek Harness needs a dedicated SDK distribution form for the Python library — no Node installation, runs directly on the target platform: a single-file executable (hereafter "the exe") that exposes a stdio JSON-RPC serving surface (`HarnessSdkServer`, the Python SDK's peer), where the plugins and configuration actually booted are decided entirely by a `cordis.yml` supplied from outside the exe.
DeepSeek Harness needs a dedicated SDK distribution form for the Python library — no Node installation, runs directly on the target platform: a single-file executable (hereafter "the exe") that exposes a stdio JSON-RPC serving interface (`HarnessSdkServer`, the Python SDK's peer), where the plugins and configuration actually booted are decided entirely by a `cordis.yml` supplied from outside the exe.
- The JSONRPC protocol for talking to the Python SDK is already validated
- A standardized way for cordis.yml to load every plugin (ESModule) is needed
@@ -23,11 +23,11 @@ The exe is packaged with the **`--sea` (enhanced SEA) mode** of [@yao-pkg/pkg](h
Terminology reminder: pkg's `/snapshot` VFS has nothing to do with this repo's testing-system "snapshot" (ACP replay expected outputs, `$DSH_SNAPSHOT`); this document says "VFS" for the former.
### The serving surface is a plugin: the two packages ui/jsonrpc + examples/jsonrpc-demo
### The serving interface is a plugin: the two packages sdk/server + examples/jsonrpc-demo
The deterministic protocol implementation (`server.ts` / `transport.ts`) lands as two packages on the existing `acp/acp` + `examples/acp-demo` pattern — the serving surface is itself a plugin:
- [`packages/scaffold/server`](../../../../packages/scaffold/server/README.md) (`@deepseek-ai/dsh-jsonrpc`): the pure protocol plugin; on apply it mounts `HarnessSdkServer` plus a line-delimited JSON-RPC transport on the process stdio, with disposal through `ctx.effect()`. Whether to serve is decided by `cordis.yml`; a yml that does not mount it is a legitimate process that does not serve. Protocol-level exit belongs to the plugin (after answering and flushing the `shutdown` response it disposes the root runtime so persistence drains, then `exit(0)`; an HMR-style unload only stops the service without exiting the process).
- [`packages/sdk/server`](../../../../packages/sdk/server/README.md) (`@deepseek-ai/dsh-jsonrpc`): the pure protocol plugin; on apply it mounts `HarnessSdkServer` plus a line-delimited JSON-RPC transport on the process stdio, with disposal through `ctx.effect()`. Whether to serve is decided by `cordis.yml`; a yml that does not mount it is a legitimate process that does not serve. Protocol-level exit belongs to the plugin (after answering and flushing the `shutdown` response it disposes the root runtime so persistence drains, then `exit(0)`; an HMR-style unload only stops the service without exiting the process).
- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md) (`@deepseek-ai/dsh-jsonrpc-demo`): a thin app bin — `installFailLoud` + `loadEnv` + config discovery + `boot()` from [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts), done once boot completes; the server is brought up by the `dsh-jsonrpc` entry in the yml. Its only dependency is app-boot. Process-level exit belongs to the bin (stdin EOF/SIGTERM → dispose then 0, SIGINT → 130).
Config discovery has two channels and fails loudly when both are missing: the `DSH_CORDIS_CONFIG` environment variable first (the SDK client convention), then an argv positional argument; no default path and no built-in fallback whatsoever — "the plugins actually booted are decided by an external cordis.yml" is a hard semantic.
@@ -80,6 +80,6 @@ Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disp
## Consequences
**Bought**: zero-dependency single-file distribution on target platforms; plugin semantics strictly identical to running from source (the same real package tree, no transpilation, no registry); the serving surface, the plugin set, and the configuration all converge on two sources of truth — `cordis.yml` plus one dependency manifest; the exe and node carriers share one tree and one semantics, so development verification never waits for packaging; official Node binaries remove the patched-binary supply-chain concern.
**Bought**: zero-dependency single-file distribution on target platforms; plugin semantics strictly identical to running from source (the same real package tree, no transpilation, no registry); the serving interface, the plugin set, and the configuration all converge on two sources of truth — `cordis.yml` plus one dependency manifest; the exe and node carriers share one tree and one semantics, so development verification never waits for packaging; official Node binaries remove the patched-binary supply-chain concern.
**Paid**: artifacts on the order of 174MB with source entering the blob as-is (no bytecode obfuscation; a closed-source distribution requirement needs a separate evaluation); pkg's VFS/module-hook layer remains community-maintained (the build script pins `@yao-pkg/pkg@6.21.0`; upgrading is an explicit change); `--sea` is one invocation per target (matching CI's one leg per platform; local multi-platform builds are serial).

View File

@@ -23,11 +23,11 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)vercel/pkg 归档后
术语提醒pkg 的 `/snapshot` VFS 与本仓库测试体系的「快照」ACP 回放预期输出、`$DSH_SNAPSHOT`无关本文用「VFS」指前者。
### 对外服务接口也是插件:ui/jsonrpc + examples/jsonrpc-demo 两个包
### 对外服务接口也是插件:sdk/server + examples/jsonrpc-demo 两个包
确定性协议实现(`server.ts` / `transport.ts`)按 `acp/acp` + `examples/acp-demo` 的既有模式落为两包——对外服务接口本身也是插件:
- [`packages/scaffold/server`](../../../../packages/scaffold/server/README.md)`@deepseek-ai/dsh-jsonrpc`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkServer` 与按行传输的 JSON-RPC 层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保 `shutdown` 响应发送完毕后,对根运行时执行 dispose资源释放让待处理的持久化操作完成再调用 `exit(0)`HMR 式卸载只停止服务,不退出进程)。
- [`packages/sdk/server`](../../../../packages/sdk/server/README.md)`@deepseek-ai/dsh-jsonrpc`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkServer` 与按行传输的 JSON-RPC 层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保 `shutdown` 响应发送完毕后,对根运行时执行 dispose资源释放让待处理的持久化操作完成再调用 `exit(0)`HMR 式卸载只停止服务,不退出进程)。
- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md)`@deepseek-ai/dsh-jsonrpc-demo`):轻量应用入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts) 的 `boot()``boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-jsonrpc` 条目启动。它只依赖 `app-boot`。进程级退出归 `bin` 所有stdin EOF/SIGTERM → dispose 后返回 0SIGINT → 130
配置发现有两个通道,均缺失时立即报错:优先使用 `DSH_CORDIS_CONFIG` 环境变量SDK 客户端约定),其次使用 argv 位置参数;没有默认路径或内置回退——「实际启动的插件由外部 `cordis.yml` 决定」是硬语义。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
2026-07-12-agent-scope-runtime-design.md: 8903a2fefa83dba042f8105b182e590783a9adde
2026-07-12-agent-scope-runtime-design.md: 5bee5f3f79be903848f8ecdf1ea84fe6fca60f6f
2026-07-12-agent-scope-runtime-design.zh.md: d86ab3d8cb9a051d4664aedac812b10b53521f22

View File

@@ -25,7 +25,7 @@ The design can be skimmed as seven choices:
| Coordinate create/resume | One `AgentCreationTransaction` |
| Protect durable, queued, model, or wire data | Materialize once at that boundary |
| Pass typed values inside one process | Readonly borrowed contract |
| Compose the model-visible prompt and tool surface | One shared tool view plus the authoritative assembly-waterfall result |
| Compose the model-visible prompt and tool set | One shared tool view plus the authoritative assembly-waterfall result |
| Coordinate subagent, worker, and process shutdown | One cancellation signal plus the independent terminal/quiescence facts of that boundary |
The rest of this Agent Note expands those choices in dependency order: Cordis mechanics, scope routing, creation and session commit, tools and prompts, subagents and workflows, then executable checks.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md
2026-07-12-scoped-layers-store.md: c5186d1652bca617eed62ec02937f2d055ea727c
2026-07-12-scoped-layers-store.md: 91df72cf18c3f28d49534793bee85094b299c9a5
2026-07-12-scoped-layers-store.zh.md: f5084426beef836d71e55c0898ec1edbdc7725f3

View File

@@ -111,11 +111,11 @@ All seven facades keep validation and diagnostics in their owning registry and c
## Consequences
- Scope-aware registries express one aggregate layer and reuse the same construction, ownership, rollback, notification, and reclamation choreography. Domain-specific validation, diagnostics, filtering, evaluation, and observer policy remain in each registry.
- The public read surface stays narrow: direct table iteration preserves explicitly live behavior, while `merge()` is the one shared materialized shadowing operation. A heterogeneous `ScopeLayer` has no layer-wide `values()` contract.
- The public read API stays narrow: direct table iteration preserves explicitly live behavior, while `merge()` is the one shared materialized shadowing operation. A heterogeneous `ScopeLayer` has no layer-wide `values()` contract.
- The helper is deliberately synchronous. A future registration that needs asynchronous setup or several independently owned undos must identify its ownership and settlement boundaries before widening this contract.
- An action must throw before retaining a contribution or return an undo for everything it retained; the helper cannot repair mutation outside that contract. The provided entry operations are atomic, and migrated registries perform fallible validation before insertion.
- A scoped layer remains allocated until every table in its aggregate is empty. Disposing one facade therefore cannot discard sibling contributions owned by the same scope.
- The four public symbols become a reusable package contract. Keeping `EntryValues` internal and consumer policy outside the helper limits the compatibility surface.
- The four public symbols become a reusable package contract. Keeping `EntryValues` internal and consumer policy outside the helper limits the compatibility API.
- The migration changes no public registry behavior and no model-, human-, wire-, persistence-, configuration-, or dependency-graph output.
## Verification

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
2026-07-14-provider-routed-llm-adapters.md: 8bd34126d05140e486cd170bfc9255f0962dc413
2026-07-14-provider-routed-llm-adapters.zh.md: 09d9032da5845d3e416dcfa047fde8e6527025b4
2026-07-14-provider-routed-llm-adapters.md: df152a4436226ac5cb1941ce060a35bd4a0d496d
2026-07-14-provider-routed-llm-adapters.zh.md: 181323c136a4f953d64928a8698720e164dd5ad1

View File

@@ -48,7 +48,7 @@ This state is model-visible replay input and therefore follows the existing [rec
### Propagate the target through every request producer
Every model-selection surface carries provider and model together: declarative agents, ACP and stdio app config, the JSON-RPC initialize request, subagent overrides and inheritance, workflow child overrides, and direct compaction summarization. Subagents inherit both fields from their parent before applying request overrides. The system-prompt variable set gains `provider` beside `model`.
Every model-selection path carries provider and model together: declarative agents, ACP and stdio app config, the JSON-RPC initialize request, subagent overrides and inheritance, workflow child overrides, and direct compaction summarization. Subagents inherit both fields from their parent before applying request overrides. The system-prompt variable set gains `provider` beside `model`.
Compaction configuration gains `summarizationProvider` beside `summarizationModel`. Both are empty to inherit, or both are non-empty to select an explicit target; a half-configured pair fails load. Inheritance uses the last logged request target when one exists and falls back to the agent's creation options. `compact/summary` records both fields with the existing model-call envelope.
@@ -66,7 +66,7 @@ The on-disk session format remains the pre-release pinned version `0`, with no c
**Let `dsh-llm-pi-ai` automatically register every pi-ai provider.** This would claim ambient credentials and provider names the deployment never intended to expose, and would conflict with native adapters such as `dsh-llm-deepseek`. Explicit profiles make capability and credential scope reviewable.
**Mount one pi-ai plugin instance per provider.** Separate instances isolate config but repeat plugin declarations and cannot make profile registration atomic. One adapter already receives provider on every request, so a validated profile map is the smaller lifecycle surface.
**Mount one pi-ai plugin instance per provider.** Separate instances isolate config but repeat plugin declarations and cannot make profile registration atomic. One adapter already receives provider on every request, so a validated profile map is the smaller lifecycle API.
**Accept arbitrary inline pi-ai model descriptors.** This would support catalog-external private model ids, but it exposes pi-ai's model and compatibility schema as Harness configuration and makes the adapter responsible for validating protocol-specific combinations. The first version supports custom endpoints by overriding `baseURL` on catalog models; custom descriptors require a separate decision after a real catalog-external deployment is identified.

View File

@@ -48,7 +48,7 @@ pi-ai 回放状态是其成功 `AssistantMessage` 的带版本最小投影,包
### 在所有请求生产方中传播目标
模型选择接口都同时携带 provider 与 model声明式 agent、ACPAgent Client Protocol和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 `model` 之外增加 `provider`
模型选择路径都同时携带 provider 与 model声明式 agent、ACPAgent Client Protocol和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 `model` 之外增加 `provider`
压缩配置在 `summarizationModel` 之外增加 `summarizationProvider`。两个值均为空时继承,均非空时选择显式目标;只配置其中一个会导致加载失败。继承优先使用最近一次记录的请求目标,没有时回退到 agent 创建选项。`compact/summary` 使用现有模型调用 envelope 记录两个字段。
@@ -60,7 +60,7 @@ JSON-RPC 运行时显式接收 provider 与 model。仅当 `deepseek` 提供方
**继续以模型名称作为注册表键,并增加通配适配器。** 通配机制会在精确注册与兜底插件之间引入回退顺序,使重复所有权取决于监听器顺序;若不再增加其他约定,仍无法区分不同提供方中相同的模型 ID。
**将提供方与模型编码到一个字符串中。** OpenRouter 的 `openai/gpt-*` 等值已经包含类似提供方的前缀和斜杠。分隔符约定会把路由语法泄漏到每个模型选择接口,并需要转义规则;两个显式字段更清晰,也可以分别记录日志。
**将提供方与模型编码到一个字符串中。** OpenRouter 的 `openai/gpt-*` 等值已经包含类似提供方的前缀和斜杠。分隔符约定会把路由语法泄漏到每个模型选择,并需要转义规则;两个显式字段更清晰,也可以分别记录日志。
**增加 `backend + provider + model`。** backend 键可以让 `dsh-llm-deepseek` 与 pi-ai 的 DeepSeek 实现共存,并按请求切换。最终采用的部署规则是一个提供方对应一个适配器所有者:同一上游的不同实现属于由插件组合选定的替代项。第三个路由维度会增加每个请求与配置的负担,却没有当前消费方。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
2026-07-15-lsp-capability-seam.md: a461432fe5f218ea6c3ff3e031b52f7ebffbd7a5
2026-07-15-lsp-capability-seam.md: c407de5275da0a8323b3c6186e178c8b2fafdc31
2026-07-15-lsp-capability-seam.zh.md: bfbd8d04f71b154d2833c86f2a6dc84d7e13fff4

View File

@@ -137,7 +137,7 @@ Navigation maps `Location` directly and `LocationLink` from `targetUri` plus `ta
Abort reaches every query phase and sends `$/cancelRequest` once an id exists. An unresponsive server is terminated and awaited without collateral active work because the instance is serialized. Disposal rejects and cancels work, attempts graceful shutdown, escalates through bounded termination, and awaits quiescence.
## Deliberately deferred surface
## Deliberately deferred API
Symbols are deferred because they need different schemas and overlap read/search; a future workspace-symbol tool must accept a search query. Call hierarchy is deferred because support is uneven, and `prepareCallHierarchy` remains an internal prerequisite rather than a model operation.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md
2026-07-16-explicit-turn-cancellation.md: 5d7b5856ceebd3dc49564c1800004188da162fd8
2026-07-16-explicit-turn-cancellation.md: 86faad9929d3eb5b00e66bb1c46a2e35b135d954
2026-07-16-explicit-turn-cancellation.zh.md: ba20b80f3c280087d71688ef4596fb075fa5782b

View File

@@ -40,7 +40,7 @@ Initiator-scope tests assert that every hook still observes the exact Agent and
**Persist a free-form string reason.** Strings admit spelling drift, prevent exhaustive switching, and encourage consumers to parse presentation text. The runtime uses a closed discriminated union, while the terminal record needs only the stable aborted outcome.
**Persist the typed caller cause in `turn/end`.** No production replay, UI, ACP, telemetry, or workflow consumer distinguishes `user` from `parent`. Copying the request source into the terminal result would conflate two facts and add Session-specific validation without a consumer; a future audit surface can record a separate cancellation-request event.
**Persist the typed caller cause in `turn/end`.** No production replay, UI, ACP, telemetry, or workflow consumer distinguishes `user` from `parent`. Copying the request source into the terminal result would conflate two facts and add Session-specific validation without a consumer; a future audit trail can record a separate cancellation-request event.
**Define speculative `superseded`, `timeout`, and `shutdown` variants now.** No current Agent cancellation producer implements those semantics. `shutdown` is already lifecycle disposal, and timeout or supersession should enter the union only with an owning policy and unique terminal meaning.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
2026-07-19-gui-layering-and-rpc-protocol.md: da96ae97f2a2d64aeef7794bd82ccbd86602b1ad
2026-07-19-gui-layering-and-rpc-protocol.zh.md: 36dc7391bc3f9bb0d5105fea14a2763d0b7159a1
2026-07-19-gui-layering-and-rpc-protocol.md: deba5e81c35e1572e2e6dc68b48234414ac9a7d5
2026-07-19-gui-layering-and-rpc-protocol.zh.md: e506c6242794b1b065136c8190f5c46a46e0d069

View File

@@ -30,7 +30,7 @@ Directories layer as follows:
- **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dsh.client` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
- **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dsh.client` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
- `apps/` holds the externally exported applications, assembled from Client / Host mixtures.
- `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`.
- `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell API exported by `dsh-client-web`.
- `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh --profile headless` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
- A future Electron application reuses the same web client packages over an IPC fetch carrier.
@@ -116,7 +116,7 @@ Domain interface signatures perceive only the narrow forms: `RpcRequest<P> = { r
### RpcReceipt: the carrier receipt
The HTTP response body of a `ClientResponse` is `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }` — a carrier-layer receipt, **not** an RpcMessage (a response has no response); late/duplicate answers get `not-pending`, and the logical convergence surface is the `*/resolved` frames.
The HTTP response body of a `ClientResponse` is `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }` — a carrier-layer receipt, **not** an RpcMessage (a response has no response); late/duplicate answers get `not-pending`, and the logical convergence point is the `*/resolved` frames.
## The type system: signatures are the source of truth

View File

@@ -28,7 +28,7 @@ Status: implemented
- **静态到达 entry 包**`connection``runtime``ui-theme``i18n``hmr`):无 `dsh.client` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。
- **fetch 到达插件包**`ui-layout``ui-sidebar``ui-conversation``ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dsh.client` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布tsdown 闭包工厂 bundle。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
- `apps/` 作为对外导出的应用入口,可以由 Client / Host 混合组装。
- `apps/web``dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts`
- `apps/web``dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳 API 之上的一层薄 `main.ts`
- `apps/cli``@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist`dsh --profile headless` = [直接使用核心 AgentSession 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
- 将来的 Electron 应用经由 IPC fetch 载体复用同一套 web client 包。
@@ -114,7 +114,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
### RpcReceipt载体回执
`ClientResponse` 的 HTTP 应答体是 `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }`——载体层回执,**不是** RpcMessageresponse 不再有 response迟到/重复应答收 `not-pending`,逻辑收敛`*/resolved` 帧。
`ClientResponse` 的 HTTP 应答体是 `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }`——载体层回执,**不是** RpcMessageresponse 不再有 response迟到/重复应答收 `not-pending`,逻辑收敛`*/resolved` 帧。
## 类型体系:函数签名即事实源

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
2026-07-19-gui-web-client-architecture.md: bc61aab894d587820ef4cb568b6439993a27d30d
2026-07-19-gui-web-client-architecture.md: 070b857f14007f429826ab83b17b5c8fbd3d3d0b
2026-07-19-gui-web-client-architecture.zh.md: 1f5bafe1dff878b5ca5ffcbdb9ed8ca38a863c9f

View File

@@ -42,7 +42,7 @@ Implementation homes: registry core and the props-share types in `packages/clien
## Services and scope addressing
A service is a plugin's only API surface toward other plugins (UI components and injection faces are not APIs; a plugin nobody calls mounts no service — ui-trajectory is the minimal-plugin exemplar: no ctx service, only view-slot registrations). The roster: `ctx.connection` (api client + stream handles), `ctx.slots` (registry wrapper emitting `slots/changed`, render entry, renderer installation contract), `ctx.sessions` (list store, current-session state, scope tree), `ctx.loader`, `ctx.theme`, `ctx.i18n`, `ctx.layout` (cross-plugin view navigation), `ctx.conversation` (send/cancel/startSession). Viewing state that used to live in service stores (panel widths, selection, drafts) now lives in entry-declared stores per the [slot system standard](2026-07-22-slot-type-chain-implementation.md).
A service is a plugin's only API toward other plugins (UI components and injection faces are not APIs; a plugin nobody calls mounts no service — ui-trajectory is the minimal-plugin exemplar: no ctx service, only view-slot registrations). The roster: `ctx.connection` (api client + stream handles), `ctx.slots` (registry wrapper emitting `slots/changed`, render entry, renderer installation contract), `ctx.sessions` (list store, current-session state, scope tree), `ctx.loader`, `ctx.theme`, `ctx.i18n`, `ctx.layout` (cross-plugin view navigation), `ctx.conversation` (send/cancel/startSession). Viewing state that used to live in service stores (panel widths, selection, drafts) now lives in entry-declared stores per the [slot system standard](2026-07-22-slot-type-chain-implementation.md).
There is no component registration model besides slots — the former view and tool rings both dissolved into it. Conversation views are entries of the `'conversation.view'` list slot ui-conversation declares, tab metadata rides the registration options (`id`/`order`/`label`), and per-view chrome lives inside the view components themselves. Final Chat business Nodes dispatch through the keyed/session `'conversation.chat.node'` slot; ui-tool owns its `tool-call` entry, recursively renders the supplied `subCalls`, and declares the keyed/session `'tool.call.toolview'` child slot. The key space stays runtime-open (SlotMap declares slots, never keys), and roots and descendants dispatch by `entryKey: toolName` with `GenericToolCard` as the fallback. Business packages register atomic views through `ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: '<tool>' }, Row))`; the declaration is the load and reload dependency ([decision](2026-08-05-slot-declaration-injection.md)). ui-conversation separately delegates the selected call's details body through `'conversation.details.tool'`, so ui-tool's card models remain the single presentation owner without making conversation import Tool components. The target-neutral event and view registries are data assembly seams rather than parallel component registries ([decision](2026-08-09-client-conversation-node-assembly.md)).

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md
2026-07-19-package-invariant-runtime-contracts.md: c1a5aa1e55965b07b34ce307375d77e75cdeb9be
2026-07-19-package-invariant-runtime-contracts.zh.md: 29fc2ecf2937b7557e16d972ad71230c0e304617
2026-07-19-package-invariant-runtime-contracts.md: 6e6516c2fa629aa736c178be4acbc8b42fd9a0b9
2026-07-19-package-invariant-runtime-contracts.zh.md: a7432ca0320646c57b29fbfd64c679b94c9861af

View File

@@ -73,6 +73,6 @@ Vitest mounts `InvariantService` with `{ enabled: true }` for every package test
- Every package has visible ownership and publication wiring, but only packages with a plausible runtime relation add listeners or trace state.
- Empty companions remain reviewable decisions with package-specific explanations and fail the gate if the explanation is removed.
- Type declarations, Cordis loadability, plugin metadata, service method surfaces, and pure algebra remain covered by their owning compile, load, unit, or integration gates.
- Type declarations, Cordis loadability, plugin metadata, service method APIs, and pure algebra remain covered by their owning compile, load, unit, or integration gates.
- Runtime failures identify the owning npm package and point to an inconsistent observation rather than restating a required API shape.
- The original selection, blocklist precedence, duplicate ownership, rollback, disposal, and HMR service contracts remain unchanged.

View File

@@ -73,6 +73,6 @@ Vitest 为每个包测试拓扑使用 `{ enabled: true }` 挂载 `InvariantServi
- 每个包都有可见的所有权与发布 wiring但只有具备合理运行时关系的包才会增加 listener 或 trace 状态。
- 空 companion 是带包专属说明、可评审的决策;删除说明后门禁会失败。
- 类型声明、Cordis 可加载性、插件 metadata、服务方法形状和纯代数继续由所属的编译、加载、单元或集成门禁覆盖。
- 类型声明、Cordis 可加载性、插件 metadata、服务方法 API 和纯代数继续由所属的编译、加载、单元或集成门禁覆盖。
- 运行时失败会标明所属 npm 包,并指出不一致的观测,而不是复述必要的 API 形状。
- 原有 selection、blocklist 优先级、重复所有权、回滚、dispose 和 HMR热模块替换服务约定保持不变。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md
2026-07-19-package-owned-invariant-service.md: 296c55b21b947d32412acff54715d3687cf9c43b
2026-07-19-package-owned-invariant-service.zh.md: 8aa776feabdef84a4007dc317115e6d72f6bbc50
2026-07-19-package-owned-invariant-service.md: f1918ec1d31f9d91538b6b92070d98217567c5c5
2026-07-19-package-owned-invariant-service.zh.md: 71354b3ceabfc102eae6004399644a308d78c253

View File

@@ -49,11 +49,11 @@ Blocklist matches override allowlist matches. Each list entry is a case-sensitiv
The public registration boundary is `ctx.invariants.register(packageName, installer)`. It reserves one active registration per full npm package name even when filters disable installation, and returns the effect disposer. Disposing the companion or service releases the reservation and all contribution state.
An enabled installer runs in a dedicated child Cordis fiber owned by the service. `InvariantInstaller.inject` declares the child fiber's service surface explicitly; the registry carries no product-specific dependency metadata. The service joins a returned installer promise before registration succeeds, so asynchronous startup checks remain transactional. The installer receives a bound `fail(message)` reporter. Calling it throws an `Error` subclass named `InvariantError` with stable code `INVARIANT` and the registering `packageName`; it does not extend a product-package error base.
An enabled installer runs in a dedicated child Cordis fiber owned by the service. `InvariantInstaller.inject` declares the child fiber's service API explicitly; the registry carries no product-specific dependency metadata. The service joins a returned installer promise before registration succeeds, so asynchronous startup checks remain transactional. The installer receives a bound `fail(message)` reporter. Calling it throws an `Error` subclass named `InvariantError` with stable code `INVARIANT` and the registering `packageName`; it does not extend a product-package error base.
Registration setup is transactional. If an installer fails after registering listeners, the child fiber is disposed completely and the name reservation is released before the failure escapes. Filtered registrations create no child but retain their reservation until disposal. Reloading a companion therefore begins with one clean installer state; stateful contributions rebuild baselines from their owning services.
The former functional-plugin entrypoint and one-argument `InvariantError` constructor are not retained as compatibility surfaces. The repository is pre-release and all call sites move to the service and package-attributed error together.
The former functional-plugin entry point and one-argument `InvariantError` constructor are not retained as compatibility APIs. The repository is pre-release and all call sites move to the service and package-attributed error together.
### Initial stateful companions and exhaustive ownership
@@ -76,7 +76,7 @@ The generated scoped-event subject resolver lives in `dsh-scope`, beside the con
The example agent spine mounts the service and all four stateful companion subpaths, forwarding `enabled`, `package_allowlist`, and `package_blocklist` to the service. Generated SDK Cordis composition emits the same entries. A subpath entry adds its installable root npm package rather than treating the subpath as a package name. The shipped `dsh` TUI and Web config trees omit the service and companions under the [shipped-config decision](../simplification/2026-08-03-omit-invariants-from-shipped-config.md).
Workspace constraints recognize the separate invariant bundle, and package exports, project references, build configuration, dependency declarations, and the lockfile describe the same publication surface. Generated config catalogs, module graphs, and API documentation derive from those sources.
Workspace constraints recognize the separate invariant bundle, and package exports, project references, build configuration, dependency declarations, and the lockfile describe the same publication metadata. Generated config catalogs, module graphs, and API documentation derive from those sources.
## Testing

View File

@@ -49,11 +49,11 @@ blocklist 匹配优先于 allowlist 匹配。每个条目都是区分大小写
公开注册边界是 `ctx.invariants.register(packageName, installer)`。即使过滤器禁止安装,它也会为每个完整 npm 包名保留唯一的活跃注册,并返回 effect disposer。卸载伴随插件或服务都会释放注册名及全部贡献状态。
启用的 installer 在服务拥有的独立 Cordis 子 fiber 中运行。`InvariantInstaller.inject` 显式声明该子 fiber 的服务表面;注册服务不携带产品专用依赖元数据。服务会在注册成功前等待 installer 返回的 promise因此异步启动检查仍具有事务性。installer 接收绑定后的 `fail(message)` 报告器。调用它会抛出名为 `InvariantError``Error` 子类,保留稳定代码 `INVARIANT` 并记录注册方 `packageName`;该错误不继承产品包中的错误基类。
启用的 installer 在服务拥有的独立 Cordis 子 fiber 中运行。`InvariantInstaller.inject` 显式声明该子 fiber 的服务 API;注册服务不携带产品专用依赖元数据。服务会在注册成功前等待 installer 返回的 promise因此异步启动检查仍具有事务性。installer 接收绑定后的 `fail(message)` 报告器。调用它会抛出名为 `InvariantError``Error` 子类,保留稳定代码 `INVARIANT` 并记录注册方 `packageName`;该错误不继承产品包中的错误基类。
注册启动是事务性的。如果 installer 在注册监听器后失败,子 fiber 会完整释放,并在失败向外传播前解除包名占用。被过滤的注册不创建子 fiber但会保留占用直到 dispose。伴随插件重载时总会从干净的 installer 状态开始;有状态贡献从其所属服务重建基线。
原有函数式插件入口与单参数 `InvariantError` 构造函数不作为兼容表面保留。仓库尚未发布,所有调用方会一起迁移到服务和带包归属的错误。
原有函数式插件入口与单参数 `InvariantError` 构造函数不作为兼容 API 保留。仓库尚未发布,所有调用方会一起迁移到服务和带包归属的错误。
### 首批有状态伴随插件与完整所有权
@@ -76,7 +76,7 @@ blocklist 匹配优先于 allowlist 匹配。每个条目都是区分大小写
示例 agent spine 会挂载服务和四个有状态伴随子路径,并把 `enabled``package_allowlist``package_blocklist` 转发给服务。生成的 SDK Cordis 组合输出相同条目。子路径条目添加可安装的根 npm 包,而不会把子路径误当成包名。根据[交付配置决策](../simplification/2026-08-03-omit-invariants-from-shipped-config.md),交付的 `dsh` TUI 与 Web 配置树会省略该服务及其伴随插件。
Workspace 约束识别独立的不变式 bundle包 exports、项目引用、构建配置、依赖声明和 lockfile 描述同一发布表面。生成的配置目录、模块图和 API 文档都从这些源派生。
Workspace 约束识别独立的不变式 bundle包 exports、项目引用、构建配置、依赖声明和 lockfile 描述同一发布元数据。生成的配置目录、模块图和 API 文档都从这些源派生。
## 测试

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md
2026-07-22-slot-type-chain-implementation.md: 9930aaa62bd4b1f37dac9736517524b7e01b1746
2026-07-22-slot-type-chain-implementation.zh.md: 2778d223cd89b4394bd629661dbb58105e6655cb
2026-07-22-slot-type-chain-implementation.md: 41a5c4592b22cc66d2f717794534305972c89eb3
2026-07-22-slot-type-chain-implementation.zh.md: 16b923376c7e6de63cb556cb666a0e35bfd9f080

View File

@@ -78,7 +78,7 @@ export function createChatStore() {
One factory, three consumption points: (a) `register` — pass the factory for an exclusive store, or call it once in `apply` and pass the same handle to several registers to share the instance (cross-plugin sharing is constructively impossible: the handle never leaves the package); (b) `PropsStore<ReturnType<typeof createChatStore>>` derives the component's store share with zero hand-written members; (c) tests call the factory and `.create()` a real engine instance, feeding `useSelector`/`actions` straight in as props — production outlets run the very same `create` path, so there is no second machinery.
Store scope is **derived from the mounting entry's scope** (session slot → one instance per session, living and dying with the session; root slot → one per entry). Read = `props.useStore`; write = `props.actions.*` only — the raw instance (with `update`/`set`) never reaches a component, so the declared actions are the complete, auditable mutation surface. Production code never calls the factory or `create` outside `apply`.
Store scope is **derived from the mounting entry's scope** (session slot → one instance per session, living and dying with the session; root slot → one per entry). Read = `props.useStore`; write = `props.actions.*` only — the raw instance (with `update`/`set`) never reaches a component, so the declared actions are the complete, auditable mutation API. Production code never calls the factory or `create` outside `apply`.
### inject: the registrant's business face, on its own ctx
@@ -103,19 +103,19 @@ Two hardening decisions in the register signature exist because the obvious alte
## Consequences
Render authority is enforceable rather than conventional: who renders what is a load-time fact, and auditing the UI structure = reading the register calls; for chain slots, WHO renders is additionally a render-time fact, but the deciding selectors are register-site declarations, so the audit surface stays the register calls. Every props surface is statically derived from one source (SlotMap entry, children keys, store factory, inject return), so a schema change propagates by compiler rather than by grep. Plugins carry no subscription machinery of their own — store lifecycle (per-session instances, disposal, persistence) is framework semantics keyed to the entry axis. Costs: registration options are dense (children spec objects); the framework carries real inference machinery (`defineStore`'s init/actions same-round inference may need a curried fallback); and the compile-time double locks mean prototype-stage drift is a hard error, not a warning.
Render authority is enforceable rather than conventional: who renders what is a load-time fact, and auditing the UI structure = reading the register calls; for chain slots, WHO renders is additionally a render-time fact, but the deciding selectors are register-site declarations, so the audit scope stays the register calls. Every props API is statically derived from one source (SlotMap entry, children keys, store factory, inject return), so a schema change propagates by compiler rather than by grep. Plugins carry no subscription machinery of their own — store lifecycle (per-session instances, disposal, persistence) is framework semantics keyed to the entry axis. Costs: registration options are dense (children spec objects); the framework carries real inference machinery (`defineStore`'s init/actions same-round inference may need a curried fallback); and the compile-time double locks mean prototype-stage drift is a hard error, not a warning.
## Alternatives considered
| Rejected | One-line reason |
|---|---|
| Separate define/register two-step API | The split leaves render authority unenforced and invites ordering bugs; children-in-register settles declaration, authorization, and spec in one visible place |
| Whitelist face objects (`ScopedSlots` + narrowing helpers) | With the whitelist already in the component's props type, the face is derivable by machinery; a mintable face object is a third authority surface with runtime-only checks |
| Whitelist face objects (`ScopedSlots` + narrowing helpers) | With the whitelist already in the component's props type, the face is derivable by machinery; a mintable face object is a third authority API with runtime-only checks |
| Assembly handles carrying root ctx into inject | Bypasses declared inject topology — every factory could reach every service, so package.json dependency declarations stop meaning anything |
| `children` as a key array | kind/scope are runtime dispatch data; SlotMap is erased, so an array forces a second spec-registration API — a definition API reborn |
| Business hand-made hooks / raw observables in component props | Every plugin becomes its own subscription machine; the inject `hooks` compartment carries the same facts through the one audited binding machinery |
| Module-level store handles | A module-scope handle is a singleton across plugin reloads and test cases; the factory form scopes identity to apply/test invocation |
| Components receiving the store instance | `update`/`set` in render code makes the mutation surface unauditable; declared actions keep "what can change" a register-site fact |
| Components receiving the store instance | `update`/`set` in render code makes the mutation API unauditable; declared actions keep "what can change" a register-site fact |
| `FC` at the register position / inferring `I` from the component | FC statics generate covariant noise that rejects valid components; component-side inference absorbs props drift silently (see rulings above) |
| Keyed dispatch with owner-side routing for takeover slots | The owner accumulates per-entry contracts and a hardcoded routing table (`find` + `entryKey` per takeover); the chain currency keeps new takeover registrations at zero owner edits |
| Components declining by rendering null | Declining requires mounting first — hooks and effects run for nothing, and mount/unmount churn breaks memoization and key semantics; a pure selector decides without a component instance |

View File

@@ -110,12 +110,12 @@ register 签名里的两条硬化裁定之所以存在,是因为显然的替
| Rejected | One-line reason |
|---|---|
| 独立的 define/register 两步式 API | 拆分让渲染权威无从强制、招来时序 bugchildren 进 register 让声明、授权、spec 在同一个可见位置结清 |
| 白名单面对象(`ScopedSlots` + 收窄辅助件) | 白名单已在组件的 props 类型里,可由机械推导;可铸造的面对象是第三权威,且只有运行时校验 |
| 白名单面对象(`ScopedSlots` + 收窄辅助件) | 白名单已在组件的 props 类型里,该对象可由机械推导;可铸造的面对象是第三权威 API,且只有运行时校验 |
| 装配句柄把 root ctx 带进 inject | 绕开声明的 inject 拓扑——每个工厂都摸得到每个服务package.json 的依赖声明就此失去意义 |
| `children` 用键数组形 | kind/scope 是运行时分派数据SlotMap 已被擦除,数组形必然逼出第二个 spec 注册 API——定义 API 复活 |
| 业务手造 hook / 组件 props 里递裸 observable | 每个插件都变成自己的订阅机械inject `hooks` 格让同样的事实走那一台受审计的绑定机械 |
| 模块级 store 句柄 | 模块级句柄是跨插件重载与跨测试用例的单例;工厂形把身份圈定在单次 apply/测试调用内 |
| 组件直收 store 实例 | 渲染代码里能用 `update`/`set`,变更就无从审计;声明的 actions 让「什么能变」保持为 register 现场的事实 |
| 组件直收 store 实例 | 渲染代码里能用 `update`/`set`,变更 API 就无从审计;声明的 actions 让「什么能变」保持为 register 现场的事实 |
| 注册位用 `FC` / 从组件推断 `I` | FC 静态位产生协变噪音、拒绝合法组件;组件侧推断静默吸收 props 漂移(见上文裁定) |
| 接管 slot 用 keyed 分派 + owner 侧路由 | owner 会不断攒下逐 entry 约定与硬编码路由表(每种接管一份 `find` + `entryKey`chain 货币让新增接管注册保持 owner 零改动 |
| 组件靠渲染 null 表示不接 | 不接也得先挂载——hook 与 effect 白跑,挂载/卸载抖动破坏 memo 化与 key 语义;纯选择器无需组件实例即可裁决 |

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md
2026-07-23-client-plugin-loading-model.md: 21289c5dcebc7244e98c602e9f10bac7eb365bc3
2026-07-23-client-plugin-loading-model.zh.md: c3c4ef598c1d92d4ebec7b9691d31cf33c7c62a2
2026-07-23-client-plugin-loading-model.md: 3347bdac95eb8e06be3e7c20d24319103f738149
2026-07-23-client-plugin-loading-model.zh.md: d52409e167b536162a8efb9a9ecc3092f6e5d1d2

View File

@@ -31,7 +31,7 @@ What makes a package a plugin? One rule: **a package is a plugin package once it
- **Plain packages** are the absolute base the module system itself needs, plus libraries not yet converted to DI: the react family, cordis, `@deepseek-ai/dsh-client-modules` (the module system itself — it can never be a plugin, because modules precede all modules), the web shell kernel, and — for now — ui-slots, web-react, ui-primitives. Plain packages are shell-bundled, seeded into the module table, and invisible to the host graph.
- **Plugin packages** are everything else. Each one carries a `dsh.client` manifest declaration (`{ platform, inject, immediately? }`) and one uniform shape: the shared tsdown preset emits `lib/client.js`, and `exports["./client"]` points at that bundle. Each is a governed entry of the host-authored graph. The current set is connection, runtime, ui-theme, i18n, hmr (dev graphs only), ui-layout, ui-sidebar, ui-conversation, ui-model-selector, ui-question, and ui-trajectory.
The manifest owns the package's loading contract: its `inject` dependency edges, plus the optional `immediately` prefetch mark (absent means lazy). The composing app owns only the roster and the `--dev` switch.
The manifest owns the package's loading contract: its `inject` dependency edges, plus the optional `immediately` prefetch mark (absent means lazy). The composing app owns only the roster.
To add a plugin package: declare `dsh.client`, emit the `./client` bundle through the shared preset, add the name to the composing app's roster. Nothing else changes hands.
@@ -46,7 +46,7 @@ Four edge rules govern imports across the two kinds. None of them depends on any
### One module system, one plugin governor
The browser mirrors the host's division of labor. `dsh-client-modules` (`ClientModuleSystem`) takes the module-system seat that Node's internal ESM loader holds host-side; the same vendored `@cordisjs/plugin-loader` keeps the governance seat on both sides. The line between them in one sentence: **the module system owns module identity and bytes — how code arrives, registers, and becomes an export surface; the Loader owns plugin lifecycle — when a plugin mounts, what it waits for, and how it is torn down.**
The browser mirrors the host's division of labor. `dsh-client-modules` (`ClientModuleSystem`) takes the module-system seat that Node's internal ESM loader holds host-side; the same vendored `@cordisjs/plugin-loader` keeps the governance seat on both sides. The line between them in one sentence: **the module system owns module identity and bytes — how code arrives, registers, and becomes an exports; the Loader owns plugin lifecycle — when a plugin mounts, what it waits for, and how it is torn down.**
`ClientModuleSystem` is a lazy CJS table. Executing a bundle only **registers** its factory — the bundle calls `window.__ModuleLoader__.load({ id, factory })` and nothing else happens. Every module body side effect, CSS injection included, lives inside the factory closure and runs at materialization: the first `require`/import of that id, memoized after that. A factory that requires a registered-but-unmaterialized sibling materializes it recursively, so no sort order exists anywhere. When asked to import an id, the table resolves through a fixed branch order: seed word → memoized record → static registration (shell-own modules, e.g. app-shell) → registered factory → graph-row external classic-script load → loud throw. That final throw is the runtime mirror of the build-time purity gate. The system also keeps per-module bookkeeping — owned `<style data-plugin>` tag ids, observed require edges — and exposes the two verbs HMR needs: `prefetch(id)` (load the script and register its factory; concurrent calls share one in-flight task) and `invalidate(id)` (drop the factory and record so the next arrival reloads it).
@@ -66,7 +66,7 @@ What happens between `dsh web` starting and the UI appearing? Three stages: the
**Host side — compose the graph.**
1. The composing app (`apps/cli`) ships the roster as ordinary rows in its `cordis.yml` config tree — client plugin packages are entry rows like every host plugin, and `--dev` appends the `client-hmr` row in code (`AppCLIEntry`) before the host activation audit so the same check covers it. A roster row that fails to import is caught by `assertEntriesLoaded`; a row whose fiber rejects is reported with its original stack by `assertEntriesActivated` ([host boot decision](2026-07-24-web-config-tree-boot-and-transport-layering.md)).
1. The composing app (`apps/cli`) ships the roster as ordinary rows in its `cordis.yml` config tree — client plugin packages are entry rows like every host plugin, including the always-mounted `client-hmr` row. A roster row that fails to import is caught by `assertEntriesLoaded`; a row whose fiber rejects is reported with its original stack by `assertEntriesActivated` ([host boot decision](2026-07-24-web-config-tree-boot-and-transport-layering.md)).
2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) scans loader entries' package.json `dsh.client` declarations and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately? }] }`. The `inject` edges and the `immediately` mark come from manifests, never hand-copied. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the host audit reports either error from the FAILED fiber.
3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per name forever and bundle re-hashing reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Each bundle's content hash is its `rev` (cache busting + HMR diff anchor), the row set hashes into `graph.rev`, and every row is served as a script resource at `/plugins/<id>/client.js?rev=…`, with its source map at the same path plus `.map`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph (it is a plain route-registration plugin; modules registers the bundle route and taps the index render itself).
@@ -84,7 +84,7 @@ Why is the roster yml rows and not a scan? Because which plugins compose into a
### Hot reload: one driver plugin, self-watched bundles
Whether hot reload is active is a composition decision: dev compositions mount the `client-hmr` row (a normal plugin package, appended by `--dev`) whose node half brings the bundle watch and the SSE channel; prod compositions mount nothing and have neither.
Hot reload is a composition decision: the web bundle mounts the `client-hmr` row (a normal plugin package) unconditionally; its node half brings the bundle watch and the SSE channel, and the chain stays idle until a rebuild watcher rewrites client bundles. A composition that must not expose it disables the row.
How does a rebuilt bundle become a reload signal? The hmr node half observes it itself — no builder tells it. It reads bundle paths from `ctx.clientModuleHost.clientPath(id)`, and one HMR-owned interval stat-polls every current graph row. Adding a row is ordered as synchronous stat baseline, then immediate `clientModuleHost.rebuilt(id)`: a write after the module host's graph hash but before that baseline is caught by the immediate re-hash, while a write after the baseline leaves a stat delta for the next poll. This avoids `fs.watchFile`, whose asynchronous first baseline can silently absorb a construction-time rebuild. Watch membership follows `onGraphChanged`; vanished rows drop out, and a bundle missing at poll time keeps its row dirty so reappearance forces a re-hash even with identical metadata. On a mtime/size delta or dirty row, `clientModuleHost.rebuilt(id)` is the single re-hash entry point; when the `rev` actually changed, the node half broadcasts a `rebuilt` frame on `GET /plugins/events` — a system SSE channel that sends the full graph on connect and `rebuilt` frames on change, presentation-only wire that never enters the session log. Polling is deliberate because inotify does not fire on the weka network mount, the same reason the build-side watcher needs `--poll`; the interval is a validated config field (default 500ms), and disposal clears the one timer. Rebuilding bundles is any tsdown watch process's business — `scripts/dev-web.ts` remains the watch-build entry point, discovering its package list through `dsh.client` while scanning `packages/*/*/package.json` at startup — and builder and host share zero protocol. A torn read self-heals: stats keep changing while the write completes, so the next poll re-hashes and broadcasts the final rev.
@@ -117,14 +117,14 @@ The support boundary, stated honestly. Reload is coarse by design: fresh fiber,
| `dsh-client-runtime` | session object layer + slots service + store engine | plugin, declares `immediately` | keeps shrinking toward a pure session object layer |
| `dsh-client-ui-theme` | theme tokens/service | plugin, declares `immediately`, plus the `./styles/*` source channel | Theme Registry (separate ruling) |
| `dsh-client-i18n` | I18nService | plugin, declares `immediately` | per-deployment locale composition |
| `dsh-client-hmr` | hot reload driver | plugin, declares `immediately`; dev graphs only | rollback; reconnect handshake |
| `dsh-client-hmr` | hot reload driver | plugin, declares `immediately` | rollback; reconnect handshake |
| ui-layout / ui-sidebar / ui-conversation / ui-trajectory | UI features | plugins, on-demand | conversation domain split; trajectory real implementation |
## Consequences
One governance implementation runs on both sides of the wire; the browser-specific surface is one module system plus one reload plugin. Plugin packages have one shape, so the purity gate covers them all. Dependency edges and the boot tier live with their owners — the manifests — while the composing app holds only the roster and the `--dev` switch. The drift classes stay structurally closed: share-list hand-sync, load-order coupling, cross-plugin imports, roster/tier double bookkeeping. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps only one replaceable `loadBundle` hook.
One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Plugin packages have one shape, so the purity gate covers them all. Dependency edges and the boot tier live with their owners — the manifests — while the composing app holds only the roster. The drift classes stay structurally closed: share-list hand-sync, load-order coupling, cross-plugin imports, roster/tier double bookkeeping. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps only one replaceable `loadBundle` hook.
Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows are informational — activation truth is service-level — so a mismatch surfaces at the settled sweep, not at graph validation; the three not-yet-promoted libraries keep their static-import export surface until their DI conversions land; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch.
Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows are informational — activation truth is service-level — so a mismatch appears at the settled sweep, not at graph validation; the three not-yet-promoted libraries keep their static-import exports until their DI conversions land; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch.
Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer moved from a webserver-side registry into the `dsh-client-modules` node half (the package upgraded to dual-face per this note's promotion rule — its consumer now reaches it through cordis DI), and the transport split landed alongside: the webserver became a plain route-registration plugin, `/api/*` binding moved to the connection node half over the upgraded `api-gateway` plugin (`dsh-host-apiproxy` providing `ctx.apiProxy`), and the dev bundle watch + SSE channel moved to the hmr node half.

View File

@@ -31,7 +31,7 @@ host 侧cordis 插件装载站在 Node 的模块机制之上——require cac
- **普通包**是模块系统自身所需的绝对基座,加上尚未转成 DI 的库react 家族、cordis、`@deepseek-ai/dsh-client-modules`模块系统本身——它永远不可能是插件因为模块先于一切模块、web 壳内核以及——暂时——ui-slots、web-react、ui-primitives。普通包打进壳 bundle、播种进模块表、对 host 图不可见。
- **插件包**是其余一切。每个都携带 `dsh.client` manifest元数据清单声明`{ platform, inject, immediately? }`)和同一种统一形态:共享 tsdown 预设产出 `lib/client.js``exports["./client"]` 指向该 bundle。每个都是 host 独家撰写的图里受治理的 entry。当前包括connection、runtime、ui-theme、i18n、hmr仅进 dev 图、ui-layout、ui-sidebar、ui-conversation、ui-model-selector、ui-question、ui-trajectory。
manifest 拥有包的装载约定:它的 `inject` 依赖边,加可选的 `immediately` 预取标记(缺省即 lazy。负责组合的 app 只拥有名册`--dev` 开关
manifest 拥有包的装载约定:它的 `inject` 依赖边,加可选的 `immediately` 预取标记(缺省即 lazy。负责组合的 app 只拥有名册。
新增一个插件包:声明 `dsh.client`,经共享预设产出 `./client` bundle把包名加进负责组合的 app 的名册。除此之外无需任何交接。
@@ -46,7 +46,7 @@ manifest 拥有包的装载约定:它的 `inject` 依赖边,加可选的 `im
### 一套模块系统,一个插件治理器
浏览器复刻 host 侧的分工。`dsh-client-modules``ClientModuleSystem`)坐上 host 侧由 Node 内部 ESM loader 占据的模块系统席位;同一份 vendored `@cordisjs/plugin-loader` 在两侧都坐治理席。二者的分界线一句话说尽:**模块系统拥有模块身份与字节——代码怎么到达、怎么登记、怎么变成导出Loader 拥有插件生命周期——插件何时挂载、等待什么、如何拆除。**
浏览器复刻 host 侧的分工。`dsh-client-modules``ClientModuleSystem`)坐上 host 侧由 Node 内部 ESM loader 占据的模块系统席位;同一份 vendored `@cordisjs/plugin-loader` 在两侧都坐治理席。二者的分界线一句话说尽:**模块系统拥有模块身份与字节——代码怎么到达、怎么登记、怎么变成导出内容Loader 拥有插件生命周期——插件何时挂载、等待什么、如何拆除。**
`ClientModuleSystem` 是一张 lazy CJS 表。执行 bundle 只**登记**其工厂——bundle 调用 `window.__ModuleLoader__.load({ id, factory })`,此外什么都不发生。模块体的一切副作用(包括 CSS 注入)都住在工厂闭包里,在物化时运行:物化即该 id 的首次 `require`/import此后记忆化。工厂若 require 一个已登记未物化的同伴,就递归物化它,因此任何地方都不存在排序。被要求 import 一个 id 时,表按固定分支顺序解析:种子词条 → 记忆化的记录 → 静态登记(壳自有模块,如 app-shell→ 已登记的工厂 → 图行外部 classic script 加载 → 大声抛错。最后这一抛是构建期纯度门禁在运行期的镜像。系统还保管逐模块的簿记——名下 `<style data-plugin>` 标签 id、观测到的 require 边——并暴露 HMR热模块替换需要的两个动词`prefetch(id)`(加载脚本、只登记工厂;并发调用共享同一在途任务)与 `invalidate(id)`(丢弃工厂与记录,下次到达即重新加载)。
@@ -66,7 +66,7 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
**host 侧——组合这张图。**
1. 负责组合的 app`apps/cli`)把名册作为普通行放进它的 `cordis.yml` 配置树——client 插件包与每个 host 插件一样是 entry 行,`--dev` 由代码(`AppCLIEntry`)在 host 激活检查之前追加 `client-hmr`,使同一项检查覆盖它。名册行 import 失败由 `assertEntriesLoaded` 捕获fiber reject 的行则由 `assertEntriesActivated` 报告原始 stack[host boot 决策](2026-07-24-web-config-tree-boot-and-transport-layering.md))。
1. 负责组合的 app`apps/cli`)把名册作为普通行放进它的 `cordis.yml` 配置树——client 插件包与每个 host 插件一样是 entry 行,包括无条件挂载的 `client-hmr` 行。名册行 import 失败由 `assertEntriesLoaded` 捕获fiber reject 的行则由 `assertEntriesActivated` 报告原始 stack[host boot 决策](2026-07-24-web-config-tree-boot-and-transport-layering.md))。
2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)扫描 loader entry 的 package.json `dsh.client` 声明,组合出 `window.__DSH_BOOT__``{ rev, entries: [{ id, url, rev, inject?, immediately? }] }``inject` 边与 `immediately` 标记都来自 manifest永不人肉抄写。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下畸形声明字段同样会让激活失败host 检查会从 FAILED fiber 报告这两类错误。
3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries包元数据含「非 client 包」的否定结论按名永久缓存bundle 重哈希只经 `rebuilt(id)` 可达。激活趟从当前 entries 灌同一脏集合并同步 flush初扫与稳态共享一条实现。每个 bundle 的内容哈希是其 `rev`(缓存失效 + HMR diff 锚点),行集合哈希进 `graph.rev`,每一行都作为脚本资源供给:`/plugins/<id>/client.js?rev=…`,对应 sourcemap 位于同一路径加 `.map`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知它是朴素路由注册插件bundle 路由和 index 渲染 tap 都由 modules 自己注册)。
@@ -84,7 +84,7 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
### 热重载:一个驱动插件,自行监视的 bundle
热重载是否启用是一项组合决策:dev 组合挂载 `client-hmr` 行(一个常规的插件包,由 `--dev` 追加),其 node 半带来 bundle 监视与 SSEServer-Sent Events通道prod 组合不挂载,两者皆无
热重载是一项组合决策:web 组合包无条件挂载 `client-hmr` 行(一个常规的插件包),其 node 半带来 bundle 监视与 SSEServer-Sent Events通道没有重建 watcher 改写客户端 bundle 时链路保持空闲。不得暴露它的组合可在 patch 层禁用该行
重建好的 bundle 怎么变成重载信号hmr 的 node 半自己观察——没有构建器来通知它。它从 `ctx.clientModuleHost.clientPath(id)` 读取图上各行的 bundle 路径,由 HMR 自持的单个定时器对当前图上的每一行做 stat 轮询。新增图行时,顺序固定为先同步取得 stat 基线,再立即调用 `clientModuleHost.rebuilt(id)`:在模块 host 算出图哈希之后、取得基线之前发生的写入会被这次立即重哈希捕获;取得基线之后发生的写入则会留下 stat 差异,供下一次轮询捕获。这避开了 `fs.watchFile`:它以异步首次 stat 建立基线,可能把构造期间的重建静默吸收进基线。监视集合的成员随 `onGraphChanged` 更新;消失的行撤下监视,轮询时缺失的 bundle 则让对应行保持标脏状态文件重现时即使元数据相同也强制重哈希。mtime/size 变化或行处于标脏状态时,`clientModuleHost.rebuilt(id)` 是重哈希的唯一入口;当 `rev` 真的变了node 半才在 `GET /plugins/events` 上广播 `rebuilt` 帧——这是一条系统级 SSE 通道,连接即发全量图,变更时发 `rebuilt` 帧,仅供呈现的 wire永不进会话日志。轮询是刻意选择inotify 在 weka 网络挂载上不触发,构建侧监视器需要 `--poll` 也是同一原因;轮询间隔是一个经校验的配置字段(默认 500msdispose资源释放会清掉那一个定时器。重建 bundle 则是任意一个 tsdown watch 进程的事——`scripts/dev-web.ts` 仍作为 watch 构建入口保留,其包清单在启动时扫描 `packages/*/*/package.json` 按 dsh.client 发现——构建器与 host 共享零协议。写一半的 bundle 被撕裂读取会自愈:写入完成期间 stat 持续变化,下一个轮询节拍会再次重哈希并广播最终的 rev。
@@ -117,12 +117,12 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
| `dsh-client-runtime` | 会话对象层 + slots 服务 + store 引擎 | 插件,声明 `immediately` | 持续缩向纯会话对象层 |
| `dsh-client-ui-theme` | 主题 token/服务 | 插件,声明 `immediately`,外加 `./styles/*` 源码通道 | Theme Registry另行裁定 |
| `dsh-client-i18n` | I18nService | 插件,声明 `immediately` | 按部署组合语言包 |
| `dsh-client-hmr` | 热重载驱动 | 插件,声明 `immediately`;仅进 dev 图 | 回滚;重连握手 |
| `dsh-client-hmr` | 热重载驱动 | 插件,声明 `immediately` | 回滚;重连握手 |
| ui-layout / ui-sidebar / ui-conversation / ui-trajectory | UI 功能 | 插件,按需到达 | conversation 域拆分trajectory 真实现 |
## Consequences
wire 两侧跑着同一份治理实现;浏览器特有的表面只是一套模块系统一个重载插件。插件包只有一种形态纯度门禁因此覆盖全部插件。依赖边与启动档位都与其所有者——manifest——同住负责组合的 app 只握名册`--dev` 开关。各漂移缺陷类被结构性关死:共享清单人肉同步、装载顺序耦合、跨插件 import、名册/档位双重记账。浏览器原生脚本装载使插件网络资源、生成 bundle 与 TypeScript/TSX 源码保持标准映射,模块系统也只保留一个可替换的 `loadBundle` 钩子。
wire 两侧跑着同一份治理实现;浏览器特有层只包含一套模块系统一个重载插件。插件包只有一种形态纯度门禁因此覆盖全部插件。依赖边与启动档位都与其所有者——manifest——同住负责组合的 app 只握名册。各漂移缺陷类被结构性关死:共享清单人肉同步、装载顺序耦合、跨插件 import、名册/档位双重记账。浏览器原生脚本装载使插件网络资源、生成 bundle 与 TypeScript/TSX 源码保持标准映射,模块系统也只保留一个可替换的 `loadBundle` 钩子。
接受的代价vendored Loader 在浏览器里背着闲置机件EntryTree 持久化是 no-op分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;图中 `inject` 行仅是信息性说明——激活的真相在服务层——因此不匹配会在 settled 扫描时浮出,而不是在图校验时被拦下;三个尚未升格的库在各自的 DI 转换落地之前保持静态 import 的导出面;每个 bundle 多出一份 sourcemap 产物,外部脚本失败也只能给出粗粒度的 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md
2026-07-23-toolview-dissolution.md: be1bd9d161714194855988d76a632c667fae8c84
2026-07-23-toolview-dissolution.zh.md: c3f301002aac4c443dedc76fa747d8b45be6577a
2026-07-23-toolview-dissolution.md: 173af01ff6cd5dc74f2f0916fd49ad278636d1bb
2026-07-23-toolview-dissolution.zh.md: 8a4048fbb6ca22acd0b790e0855e4dd63d0b7006

View File

@@ -26,7 +26,7 @@ Four behavioral deltas were accepted deliberately, not overlooked. Cross-view ap
**Promote `renderToolView` into the standard kit and move the registry into the runtime package.** Rejected: Tool presentation is Client UI vocabulary; hoisting it into runtime would leak presentation into the data object layer and still leave two registration models.
**Derive slot declarations from subscription refCounts** (declare the slot implicitly when the first registrant subscribes). Rejected for implicit coupling and debounce complexity; noted as a possible revisit only if a genuinely multi-viewer surface appears.
**Derive slot declarations from subscription refCounts** (declare the slot implicitly when the first registrant subscribes). Rejected for implicit coupling and debounce complexity; noted as a possible revisit only if a genuinely multi-viewer UI appears.
**A thin `registerToolView` facade over slots.register.** Deferred, not rejected: after dissolution the facade would carry only compile-time sugar (slot-name literal narrowing, tool→key vocabulary, props pre-composition) with zero runtime. Per "enforce at the operation boundary" (a facade is not an enforcement point), it stays unbuilt; the useful type composition ships as the exported Tool view props alias. A later facade can be added without disturbing direct registration if repeated registration ceremony justifies it.

View File

@@ -26,7 +26,7 @@ Status: implemented
**把 `renderToolView` 提进标配 kit、注册表迁入 runtime 包。** 拒绝Tool 展示是 Client UI 词汇;上提进 runtime 会把展示概念泄漏进数据对象层,且依然留着两套注册模型。
**以订阅 refCount 推导槽声明**(首个注册方订阅时隐式声明槽)。拒绝:隐式耦合加去抖复杂度;记为将来真出现多观看面时的备选。
**以订阅 refCount 推导槽声明**(首个注册方订阅时隐式声明槽)。拒绝:隐式耦合加去抖复杂度;记为将来真出现多视图 UI 时的备选。
**slots.register 之上的薄 `registerToolView` 门面。** 缓建而非拒绝溶解后该门面只剩编译期语法糖slot 名字面量收窄、tool→key 词汇翻译、props 预组合运行时为零。按「enforce at the operation boundary」门面不是强制点保持不建有用的类型组合以导出的 Tool view props 别名兑现。若重复注册仪式今后足以证明其价值,可在不扰动直接注册的前提下补充门面。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md
2026-07-24-single-harness-home-resolver.md: 1e128911dce0b50dc95722c6edd82cb322d18f3d
2026-07-24-single-harness-home-resolver.zh.md: 258a16e6ca139da191db564aae40d23d05ca46b7
2026-07-24-single-harness-home-resolver.md: 27db7e23b1ae3d2c9a05259bfb3047591589627d
2026-07-24-single-harness-home-resolver.zh.md: 5eec850c9c7ecc70b2327eacca422662ec39a9cc

View File

@@ -6,13 +6,12 @@ English | [中文](2026-07-24-single-harness-home-resolver.zh.md)
## Problem
The harness had three inconsistent conventions for "where does DeepSeek Harness user data live":
The harness had two inconsistent conventions for "where does DeepSeek Harness user data live":
- `@deepseek-ai/dsh-home` resolved `configured ?? $DSH_HOME ?? ~/.dsh`.
- `@deepseek-ai/dsh-paths` shipped a **second** `resolveDshHome` with the same precedence plus tilde expansion — a near-duplicate of `dsh-home` that no gate flagged because the two lived in different packages and had already drifted (only one expanded tildes).
- `@deepseek-ai/dsh-telemetry`'s `globalConfigDir` used a *different* policy entirely: `DSH_CONFIG_HOME > $XDG_CONFIG_HOME/deepseek-harness > %APPDATA%/deepseek-harness > ~/.config/deepseek-harness`.
So most of the product parked everything under one `~/.dsh` root while telemetry alone stored its anonymous id elsewhere, under a `deepseek-harness` namespace that contradicts the repo-wide `dsh` shorthand (`DSH_HOME`, `@deepseek-ai/dsh-*`, `~/.dsh`). Two resolvers plus a divergent third policy means no single home fact.
Two resolvers for the same cross-cutting fact meant there was no single home policy.
## Decision
@@ -22,20 +21,18 @@ One resolver owns the harness home, in `@deepseek-ai/dsh-paths`, single-root:
explicit configured path > $DSH_HOME > ~/.dsh
```
An empty or whitespace-only `$DSH_HOME` is treated as unset, matching the guard telemetry's old resolver carried: without it `resolve('')` would silently place the home at the current working directory. The harness keeps all user data under one root; there is no XDG config/data/cache split. `dshHomePath(...segments)` joins deployment-owned children onto that root, and `dsh-app-boot` exposes it to Loader `!!js` config expressions before mounting entries, so shipped compositions derive `sessions` and `storages` without copying the resolver. `dshHomeDisplay()` names a resolved root symbolically for user-facing paths — `~/.dsh` for the default home, `$DSH_HOME` for any configured home — so the user-global `AGENTS.md` label never leaks an absolute machine path. It replaces workspace-context's bespoke default-vs-`$DSH_HOME` check.
An empty or whitespace-only `$DSH_HOME` is treated as unset; otherwise `resolve('')` would silently place the home at the current working directory. The harness keeps all user data under one root; there is no XDG config/data/cache split. `dshHomePath(...segments)` joins deployment-owned children onto that root, and `dsh-app-boot` exposes it to Loader `!!js` config expressions before mounting entries, so shipped compositions derive `sessions` and `storages` without copying the resolver. `dshHomeDisplay()` names a resolved root symbolically for user-facing paths — `~/.dsh` for the default home, `$DSH_HOME` for any configured home — so the user-global `AGENTS.md` label never leaks an absolute machine path. It replaces workspace-context's bespoke default-vs-`$DSH_HOME` check.
`@deepseek-ai/dsh-home` is deleted. Its three importers (`dsh-tool-bash`, `dsh-skill-local`, `dsh-agent-spine-demo`) now import `resolveDshHome` from `dsh-paths`. `dsh-telemetry`'s `globalConfigDir` delegates to `resolveDshHome`, dropping its second resolver, the `DSH_CONFIG_HOME` override, the XDG/`%APPDATA%` branches, and the `deepseek-harness` namespace; the anonymous id now lives directly under the harness home.
`@deepseek-ai/dsh-home` is deleted. Its three importers (`dsh-tool-bash`, `dsh-skill-local`, `dsh-agent-spine-demo`) import `resolveDshHome` from `dsh-paths`.
`dsh-telemetry` and its separate home policy are absent under the [SDK project toolchain removal](../simplification/2026-08-11-remove-sdk-project-toolchain.md), leaving this resolver as the sole home policy.
## Alternatives considered
**Leave the two `resolveDshHome` copies in place.** They had already drifted (one expands tildes, one didn't) and encode the same cross-cutting fact twice. Consolidation is the point of the `util/` layer; a duplicate resolver is a latent divergence bug.
**Adopt XDG (honor `$XDG_CONFIG_HOME`, or split config/data/cache into separate trees).** Considered and dropped in favor of one obvious root. A single `$DSH_HOME || ~/.dsh` ground truth matches `~/.claude` / `~/.aws`, needs no per-kind reclassification of every `~/.dsh` consumer, and leaves no resolver asymmetry to reconcile. Telemetry aligning onto the same root — rather than keeping its own XDG path — is precisely the divergence this removes.
**Keep telemetry's own config dir.** Its `deepseek-harness` namespace and separate XDG policy were the lone exception to the `dsh`/`~/.dsh` convention. Folding it onto the shared resolver is what makes "one home fact" true. The cost is that the anonymous id becomes scoped to `$DSH_HOME` rather than the machine: a project that points `DSH_HOME` at a repo-local path (or a command that loads a project `.env` before telemetry) gets a home-local id, so the id counts harness homes, not machines. This is accepted as the intended meaning of single-root — a relocated `$DSH_HOME` moves *all* harness state, telemetry identity included — and the module contract is stated as per-harness-home rather than per-machine. A machine-global identity that ignored `$DSH_HOME` would reintroduce exactly the second home policy this decision removes.
**Adopt XDG (honor `$XDG_CONFIG_HOME`, or split config/data/cache into separate trees).** Considered and dropped in favor of one obvious root. A single `$DSH_HOME || ~/.dsh` ground truth matches `~/.claude` / `~/.aws`, needs no per-kind reclassification of every `~/.dsh` consumer, and leaves no resolver asymmetry to reconcile.
## Consequences
- One home fact, one resolver. `dsh-paths` is the sole owner; the `util/` group loses the `home` package.
- Telemetry's anonymous id moves from `~/.config/deepseek-harness/telemetry.json` to the harness home (`~/.dsh/telemetry.json` by default). Under the pre-release "backends reject old formats" stance this needs no migration: an orphaned old id simply regenerates once, and the id is anonymous by construction.
- Telemetry drops Windows `%APPDATA%` handling. `resolveDshHome` uses `os.homedir()`, which is correct on Windows; the harness does not special-case `%APPDATA%` for its single root.

View File

@@ -6,13 +6,12 @@ Status: implemented
## 问题
对于"DeepSeek Harness 用户数据存放在哪里"harness 里存在套互不一致的约定:
对于"DeepSeek Harness 用户数据存放在哪里"harness 里存在套互不一致的约定:
- `@deepseek-ai/dsh-home``configured ?? $DSH_HOME ?? ~/.dsh` 解析。
- `@deepseek-ai/dsh-paths` 又提供了**第二个** `resolveDshHome`,优先级相同但额外做了波浪号展开——它几乎是 `dsh-home` 的重复实现,却没有任何门禁发现,因为两者分属不同的包,而且早已漂移(只有一个会展开波浪号)。
- `@deepseek-ai/dsh-telemetry``globalConfigDir` 采用了*完全不同*的策略:`DSH_CONFIG_HOME > $XDG_CONFIG_HOME/deepseek-harness > %APPDATA%/deepseek-harness > ~/.config/deepseek-harness`
于是产品的大部分内容都停放在同一个 `~/.dsh` 根目录下,唯独 telemetry 把匿名 id 存到别处,落在一个 `deepseek-harness` 命名空间里,这与全仓库通行的 `dsh` 简写(`DSH_HOME``@deepseek-ai/dsh-*``~/.dsh`)相冲突。两个解析器再加上一个各行其是的第三套策略,意味着不存在单一的 home 事实
同一条横切事实有两个解析器,意味着不存在单一的 home 策略
## 决策
@@ -22,20 +21,18 @@ Status: implemented
explicit configured path > $DSH_HOME > ~/.dsh
```
空或仅含空白的 `$DSH_HOME` 被当作未设置处理,这与 telemetry 旧解析器所带的保护一致:若无此保护`resolve('')` 会悄悄把 home 落在当前工作目录。harness 把所有用户数据都放在同一个根目录下;不存在 XDG 的 config/data/cache 拆分。`dshHomePath(...segments)` 将部署负责的子路径拼接到该根目录下,`dsh-app-boot` 在挂载条目前向 Loader `!!js` 配置表达式暴露它,因此出厂组合无需复制解析器即可派生 `sessions``storages``dshHomeDisplay()` 为面向用户的路径以符号形式命名已解析的根目录——默认 home 显示为 `~/.dsh`,任何已配置的 home 显示为 `$DSH_HOME`——这样面向用户全局的 `AGENTS.md` 标签就绝不会泄露机器上的绝对路径。它取代了 workspace-context 中自定义的"默认值 vs `$DSH_HOME`"判断。
空或仅含空白的 `$DSH_HOME` 被当作未设置处理;否则`resolve('')` 会悄悄把 home 落在当前工作目录。harness 把所有用户数据都放在同一个根目录下;不存在 XDG 的 config/data/cache 拆分。`dshHomePath(...segments)` 将部署负责的子路径拼接到该根目录下,`dsh-app-boot` 在挂载条目前向 Loader `!!js` 配置表达式暴露它,因此出厂组合无需复制解析器即可派生 `sessions``storages``dshHomeDisplay()` 为面向用户的路径以符号形式命名已解析的根目录——默认 home 显示为 `~/.dsh`,任何已配置的 home 显示为 `$DSH_HOME`——这样面向用户全局的 `AGENTS.md` 标签就绝不会泄露机器上的绝对路径。它取代了 workspace-context 中自定义的"默认值 vs `$DSH_HOME`"判断。
`@deepseek-ai/dsh-home` 被删除。它的三个引用方(`dsh-tool-bash``dsh-skill-local``dsh-agent-spine-demo`现在`dsh-paths` 导入 `resolveDshHome``dsh-telemetry``globalConfigDir` 转而委托给 `resolveDshHome`,去掉了它的第二个解析器、`DSH_CONFIG_HOME` 覆盖项、XDG/`%APPDATA%` 分支以及 `deepseek-harness` 命名空间;匿名 id 现在直接存放在 harness home 之下。
`@deepseek-ai/dsh-home` 被删除。它的三个引用方(`dsh-tool-bash``dsh-skill-local``dsh-agent-spine-demo`)从 `dsh-paths` 导入 `resolveDshHome`
`dsh-telemetry` 及其独立 home 策略已随 [SDK 项目工具链移除](../simplification/2026-08-11-remove-sdk-project-toolchain.md)一并消失,因此该解析器是唯一的 home 策略。
## 备选方案
**保留两份 `resolveDshHome` 副本。** 它们早已漂移(一个展开波浪号,一个不展开),并把同一条横切事实编码了两遍。`util/` 层的意义正是在于合并,重复的解析器是一个潜在的分歧 bug。
**采用 XDG遵从 `$XDG_CONFIG_HOME`,或把 config/data/cache 拆分到各自的目录树)。** 经过考虑后放弃,转而采用一个显而易见的根目录。单一的 `$DSH_HOME || ~/.dsh` 基准事实与 `~/.claude` / `~/.aws` 一致,无需对每个 `~/.dsh` 消费方按类别重新归类,也不留下任何需要协调的解析器不对称。telemetry 对齐到同一根目录——而不是保留自己的 XDG 路径——正是本决策所要消除的那种分歧。
**保留 telemetry 自己的 config 目录。** 它的 `deepseek-harness` 命名空间和独立的 XDG 策略是唯一违背 `dsh`/`~/.dsh` 约定的例外。把它折叠到共享解析器上,才让"单一 home 事实"成真。代价是匿名 id 的作用域从机器变成了 `$DSH_HOME`:若某个项目把 `DSH_HOME` 指向仓库本地路径(或某条命令在 telemetry 之前加载了项目的 `.env`),得到的就是 home 本地的 id因此该 id 统计的是 harness home而非机器。这被接受为单一根目录的应有含义——重定位 `$DSH_HOME` 会移动*全部* harness 状态telemetry 身份也在其中——模块约定据此表述为 per-harness-home 而非 per-machine。一个忽略 `$DSH_HOME` 的机器级全局身份,恰恰会重新引入本决策所要消除的那第二套 home 策略。
**采用 XDG遵从 `$XDG_CONFIG_HOME`,或把 config/data/cache 拆分到各自的目录树)。** 经过考虑后放弃,转而采用一个显而易见的根目录。单一的 `$DSH_HOME || ~/.dsh` 基准事实与 `~/.claude` / `~/.aws` 一致,无需对每个 `~/.dsh` 消费方按类别重新归类,也不留下任何需要协调的解析器不对称。
## 影响
- 单一 home 事实,单一解析器。`dsh-paths` 是唯一归属方;`util/` 组失去了 `home` 包。
- telemetry 的匿名 id 从 `~/.config/deepseek-harness/telemetry.json` 移到 harness home默认为 `~/.dsh/telemetry.json`)。在预发布的"后端拒绝旧格式"立场下,这无需迁移:一个遗留的旧 id 只会重新生成一次,而且该 id 本就是匿名构造的。
- telemetry 去掉了 Windows `%APPDATA%` 处理。`resolveDshHome` 使用 `os.homedir()`,这在 Windows 上是正确的harness 不会为它的单一根目录对 `%APPDATA%` 做特殊处理。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.md
2026-07-24-web-config-tree-boot-and-transport-layering.md: c00d0c544cfd04927d23eac53720cb969a44e044
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: 4fe315bb7673ba219286b176123ccbbe08f02f0d
2026-07-24-web-config-tree-boot-and-transport-layering.md: be0a75d98c3d0004e49ad574e7e0e37ba35259bb
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: 316cb0bc2ef23b440f52b35774c38a47f1e0570b

View File

@@ -12,7 +12,7 @@ English | [中文](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)
## Decision
**Composition is one flat assembled tree.** `apps/cli/config/base.cordis.yml` plus `apps/cli/config/web.cordis.yml` holds every row — the host runtime (32 rows), the `api-gateway` row, the `webserver` row, and the `dsh.client` rows (the browser roster; the modules row is simultaneously a host row). No spine bundle: every plugin is one row and every config field is yml-editable. That stance later became repository-wide, with the rows both surfaces share factored into `apps/cli/config/base.cordis.yml` and each surface reduced to an overlay ([shared-base overlays](../simplification/2026-07-29-shared-base-config-overlays.md)). `--dev` appends the `dsh-client-hmr` row in code before the settle audit — prod and dev differ by exactly that row. Row order carries no load semantics; activation is service-availability driven. The shared audit rejects imports with no fiber, awaits only failed fibers to recover original activation errors, and reports services that leave a fiber `PENDING`; before throwing, it marks those exact rejection reasons through one process checkpoint so `installFailLoud` coalesces Loader's duplicate notification while unrelated unhandled rejections remain fatal. The Node app-boot artifact embeds `@cordisjs/plugin-include` while leaving `@cordisjs/plugin-loader` external, so the include's `EntryTree` and the host bind to one Loader peer instead of splitting a config tree across two Loader implementations.
**Composition is one flat assembled tree.** `apps/cli/config/base.cordis.yml` plus `apps/cli/config/web.cordis.yml` holds every row — the host runtime (32 rows), the `api-gateway` row, the `webserver` row, and the `dsh.client` rows (the browser roster; the modules row is simultaneously a host row). No spine bundle: every plugin is one row and every config field is yml-editable. That stance later became repository-wide, with the rows both surfaces share factored into `apps/cli/config/base.cordis.yml` and each surface reduced to an overlay ([shared-base overlays](../simplification/2026-07-29-shared-base-config-overlays.md)). The `dsh-client-hmr` row is an ordinary always-on bundle row (originally appended in code by `--dev`; the flag is retired). Row order carries no load semantics; activation is service-availability driven. The shared audit rejects imports with no fiber, awaits only failed fibers to recover original activation errors, and reports services that leave a fiber `PENDING`; before throwing, it marks those exact rejection reasons through one process checkpoint so `installFailLoud` coalesces Loader's duplicate notification while unrelated unhandled rejections remain fatal. The Node app-boot artifact embeds `@cordisjs/plugin-include` while leaving `@cordisjs/plugin-loader` external, so the include's `EntryTree` and the host bind to one Loader peer instead of splitting a config tree across two Loader implementations.
**Boot glue is a class pair.** `AppCLIEntry` (apps/cli) and `AppWebEntry` (the shell kernel) hold only what must exist independently of cordis: argv facts, the composed patch set, the parsed boot manifest, the module system instance, loading-page handles — everything else lives in plugins. `AppCLIEntry.run()` is three stages: layered env (ambient > cwd `.env` > `$DSH_HOME/.env`, closing the defect above) → patch composition → Loader include boot plus the activation audit. `AppWebEntry.run()` mirrors it browser-side: parse `window.__DSH_BOOT__` into a `BootManifest` (two views: npm-package rows for the module table, cordis-plugin rows for entry composition; malformed wire throws), build the module system, render the loading page, prefetch the `immediately` tier in parallel with Context/Loader setup, **await the prefetch before creating entries** (materialization is `tree.import`'s synchronous require, unprotected by fiber inject waiting; cross-package require edges such as i18n → runtime/client need every immediately-tier factory registered first — an empirically found 1025% boot race otherwise), adopt the modules entry, create the graph rows, settle, sweep.
@@ -35,7 +35,7 @@ English | [中文](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)
| Dedicated `dsh-host-profile` receiver package | User model state belongs to the Settings-backed `ctx.agentDefaultModel`; an extra Host receiver would duplicate ownership and exclude direct entry points |
| Runtime `assembly` shim plugin providing an `apiHandler` service | Existed only because `createApiProxy` lived in runtime; moving it into apiproxy made the gateway self-hosting, and `toFetchHandler` is a pure function the binding side calls |
| Full-rescan + incremental scan coexisting | Two implementations, two semantics; the single per-package path covers the activation pass too |
| A bespoke `./impl` export on the modules package | Non-uniform export surface; the standard `./client` carries the whole browser half |
| A bespoke `./impl` export on the modules package | Non-uniform exports; the standard `./client` carries the whole browser half |
| dev overlay / `cordis.dev.yml` | One yml; `!!js` cannot conditionalize row existence, and `--dev` appending one row is the entire difference |
| env vars in the mapping table | The same field would gain env/json double sourcing and need an invented precedence |
| Unbarriered create-after-prefetch (`arrive()` dedup as safety) | Disproved by a 1025% boot race: in-flight dedup covers same-package double-fetch, not cross-package synchronous require edges |

View File

@@ -12,7 +12,7 @@ Status: implemented
## 决策
**组合结果是一棵平铺配置树。** `apps/cli/config/base.cordis.yml``apps/cli/config/web.cordis.yml` 共同持有全部行——host 运行时32 行)、`api-gateway` 行、`webserver` 行、`dsh.client` 行(浏览器 rostermodules 行同时是 host 行)。不做 spine bundle每插件一行、每个 config 字段 yml 可改。这一立场后来推广到全仓:两个 surface 共享的配置项被抽取进 `apps/cli/config/base.cordis.yml`,各 surface 则收敛为一份 overlay[共享 base overlay](../simplification/2026-07-29-shared-base-config-overlays.md))。`--dev` 在 settle audit 之前由代码追加 `dsh-client-hmr` 行——prod 与 dev 的全部差异就是这一行。行序无装载语义;激活由服务可用性驱动。共享 audit 会拒绝没有 fiber 的 import、仅等待失败的 fiber 以恢复原始激活错误,并报告让 fiber 停在 `PENDING` 的服务;抛出错误前,审计会通过一个进程级检查点标记这些 rejection 的确切原因,从而让 `installFailLoud` 将 Loader 的重复通知合并为一次,而无关的未处理 rejection 仍然致命。Node app-boot 产物内嵌 `@cordisjs/plugin-include`,但将 `@cordisjs/plugin-loader` 保持为外部依赖,因此 include 的 `EntryTree` 与 host 会绑定到同一个 Loader peer而不会让一棵配置树横跨两个 Loader 实现。
**组合结果是一棵平铺配置树。** `apps/cli/config/base.cordis.yml``apps/cli/config/web.cordis.yml` 共同持有全部行——host 运行时32 行)、`api-gateway` 行、`webserver` 行、`dsh.client` 行(浏览器 rostermodules 行同时是 host 行)。不做 spine bundle每插件一行、每个 config 字段 yml 可改。这一立场后来推广到全仓:两个 surface 共享的配置项被抽取进 `apps/cli/config/base.cordis.yml`,各 surface 则收敛为一份 overlay[共享 base overlay](../simplification/2026-07-29-shared-base-config-overlays.md))。`dsh-client-hmr` 行是普通的常开组合包行(最初由 `--dev` 在代码中追加;该旗标已废除)。行序无装载语义;激活由服务可用性驱动。共享 audit 会拒绝没有 fiber 的 import、仅等待失败的 fiber 以恢复原始激活错误,并报告让 fiber 停在 `PENDING` 的服务;抛出错误前,审计会通过一个进程级检查点标记这些 rejection 的确切原因,从而让 `installFailLoud` 将 Loader 的重复通知合并为一次,而无关的未处理 rejection 仍然致命。Node app-boot 产物内嵌 `@cordisjs/plugin-include`,但将 `@cordisjs/plugin-loader` 保持为外部依赖,因此 include 的 `EntryTree` 与 host 会绑定到同一个 Loader peer而不会让一棵配置树横跨两个 Loader 实现。
**boot 胶水由两个类组成。** `AppCLIEntry`apps/cli`AppWebEntry`(壳内核)只持有那些必须独立于 cordis、提前存在的东西argv 事实、合成的 patch 集、解析出的 boot manifest元数据清单、模块系统实例、loading 页句柄——其余一律进插件。`AppCLIEntry.run()` 三段:分层 envambient > cwd `.env` > `$DSH_HOME/.env`,顺手关掉上述缺陷)→ patch 合成 → Loader include boot 加 activation audit。`AppWebEntry.run()` 在浏览器侧镜像它:把 `window.__DSH_BOOT__` 解析成 `BootManifest`双视角npm 包行给模块表、cordis 插件行给 entry 组合;畸形 wire 大声抛)、建模块系统、渲染 loading 页、immediately 层预取与 Context/Loader 准备并行、**create entry 之前等预取齐**(物化是 `tree.import` 的同步 require不受 fiber inject 等待保护i18n → runtime/client 这类跨包 require 边要求 immediately 层工厂全部注册完——否则有实测 1025% 的 boot 竞态)、收编 modules entry、逐一创建图行、settle、sweep。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
2026-07-25-web-client-session-scope-and-provide-channel.md: f371b93ccf6cb3ba10cbdb73baa670cbf889f393
2026-07-25-web-client-session-scope-and-provide-channel.md: 561f5792e40ef740cdac56af5efb2d08fa15333c
2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 78440131c4e09c9a458009fbe5a2a34a707a481c

View File

@@ -122,7 +122,7 @@ Slot scope is the closed set `root | session-maybe | session`:
| A `scopeTarget` carrier + fused dispatcher (mirroring the host `agentEvents`) | The host wrapper layer guards the business Agent subject against drifting from the scope key; client events have no subject to guard — the filter on the actx plus cordis primitives covers every need |
| Sessions not holding a ctx (a cordis-free object layer) | A red line born only so the filtering unit tests avoid importing cordis, at the cost of two-hop contribute callbacks plus mutable public fields; the host Agent already holds loopCtx |
| Resident Session instances (resident-instance) | The host session log is the durable truth; residency is mere identity convenience, and its misalignment with the scope lifecycle is a source of complexity |
| Components receiving wiring-callback bundles (two-layer inject→props pass-down) | The standard-kit channel lets components fetch their own; the public surface converges to hooks + stable props |
| Components receiving wiring-callback bundles (two-layer inject→props pass-down) | The standard-kit channel lets components fetch their own; the public API converges to hooks + stable props |
| Swapping the no-session Hero view for the entire session Conversation | Even with the outer layout unchanged, the Hero, picker, and composer subtrees would remount together, making the whole UI region jump |
| Making InputBar itself `session-maybe` | The input state machine, keyboard command surface, and actions would all have to accept absent values; replacing only the disabled input body keeps optionality at the shell boundary |
| A dedicated conversion frame | `session-status(running:true)` semantically implies conversion (a blank session never runs); adding a frame buys zero information for one more wire type |

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md
2026-07-26-subprocess-seam.md: 7899af56259d39a4e3f924bcba673c8efa99c682
2026-07-26-subprocess-seam.zh.md: d9cbc0ebf33bc785984b0a9bd58fa8ebc6aab436
2026-07-26-subprocess-seam.md: f43b55b0b760c2aabe317abacdb800b755de059b
2026-07-26-subprocess-seam.zh.md: df8c3ddab80933882bf3587a9f01e019eb721a87

View File

@@ -17,11 +17,11 @@ A new `subprocess/` capability family owns "run and manage a process"; the bash
- **`dsh-bash-local` (Consumer)** — `inject: ['subprocess']`; maps each resolved `BashExecSpec` onto a `SubprocessSpawnSpec` (`['bash', '-c', command]`), keeps its config, `resolve()` defaulting, fused-deadline `timedOut`/`aborted` classification, the `[stderr]`-marked background read merge with its consuming cursor, and the `onProcessDone` subclass hook. `dsh-bash-sandbox` is unchanged apart from redeclaring the inherited inject; it still wraps at the command-string level and re-enters the inherited spawn path.
- **`dsh-bash` (Service Definition)** — re-exports the moved vocabulary from `dsh-subprocess`, so no bash Consumer changes an import; `BashExecRequest`/`BashExecSpec`/`BashProcess` and the sandbox facts remain bash-owned.
Every composition that loads a bash executor now also loads `@deepseek-ai/dsh-subprocess-local` (CLI, examples, python bundled runtime, create-sdk's bash feature resources, inline test configs).
Every composition that loads a bash executor also loads `@deepseek-ai/dsh-subprocess-local` (CLI, examples, the Python bundled runtime, and inline test configs).
Background-process lifetime moved from the executor to the subprocess service: the executor no longer retains a live-process set, so an executor reload leaves background work running and readable, and composition teardown (the service's disposal) remains the kill-and-join boundary. One behavioral contract shifted with it: a background spawn failure can no longer be buffered as fake stderr inside the plumbing (the service rejects `done` and buffers nothing for a process that never ran), so the executor injects the `spawn failed: …` note into exactly one `readOutput()` delta.
Observed stream and lifecycle needs then moved the eligible process consumers onto the seam: LSP uses piped protocol streams plus a collected stderr tail; the ACP backend uses piped ndjson, inherited stderr, and a consumer-owned stdin-EOF disposal ladder; PTY uses `spawnTerminal()` while keeping readiness and terminal policy. `dsh-subagent-subprocess` and the private LSP tree helpers were deleted. MCP transport spawning, the SDK package-manager runner, synchronous TUI Git probing, and dependency-light test-support launchers remain outside by ownership or execution shape; their production callers share the scrub where applicable.
Observed stream and lifecycle needs then moved the eligible process consumers onto the seam: LSP uses piped protocol streams plus a collected stderr tail; the ACP backend uses piped ndjson, inherited stderr, and a consumer-owned stdin-EOF disposal ladder; PTY uses `spawnTerminal()` while keeping readiness and terminal policy. `dsh-subagent-subprocess` and the private LSP tree helpers were deleted. MCP transport spawning and dependency-light test-support launchers remain outside by ownership or execution shape; their production callers share the scrub where applicable.
## Alternatives considered
@@ -31,7 +31,7 @@ Observed stream and lifecycle needs then moved the eligible process consumers on
**Use one `stdio: 'pipe' | 'inherit' | 'collect'` mode for all streams.** Rejected because real consumers mix modes per stream: LSP uses pipe/pipe/collect, ACP uses pipe/pipe/inherit, and Bash uses data/collect/collect.
**Route every process launch through `ctx.subprocess`.** Rejected because the MCP SDK owns its transport spawn, the SDK wizard has no Cordis context and needs inherited redirection, the TUI probe is synchronous, and support launchers deliberately stay independent of product seams. PTY allocation did move behind `spawnTerminal()` because the provider, not the consumer, owns that substrate-specific primitive.
**Route every process launch through `ctx.subprocess`.** Rejected because the MCP SDK owns its transport spawn and support launchers deliberately stay independent of product seams. PTY allocation did move behind `spawnTerminal()` because the provider, not the consumer, owns that substrate-specific primitive.
**Put `run_in_background`/task semantics into the subprocess capability seam instead.** Rejected: that boundary already exists — `ctx.tasks` owns ids, ownership, and notices, and the bash tool adapts a `BashProcess` into task hooks. The subprocess seam sits *below* the bash executor, not beside the task registry.

View File

@@ -17,11 +17,11 @@ Status: implemented
- **`dsh-bash-local`Consumer**——`inject: ['subprocess']`;把每个解析后的 `BashExecSpec` 映射为一个 `SubprocessSpawnSpec``['bash', '-c', command]`),并保留自身配置、`resolve()` 默认值补全、基于融合 deadline 的 `timedOut`/`aborted` 分类、带 `[stderr]` 标记的后台读取合并及其消费游标,以及 `onProcessDone` 子类钩子。`dsh-bash-sandbox` 除了重新声明继承来的 inject 之外没有变化;它仍在命令字符串层面做包装,并重新进入继承的 spawn 路径。
- **`dsh-bash`Service Definition**——把迁走的词汇从 `dsh-subprocess` 重导出,因此没有任何 bash Consumer 需要改动导入;`BashExecRequest`/`BashExecSpec`/`BashProcess` 与沙箱事实仍归 bash 所有。
如今,每个加载 bash 执行器的组合都同时加载 `@deepseek-ai/dsh-subprocess-local`CLI命令行界面、各示例、Python 捆绑运行时、create-sdk 的 bash 功能资源,以及各内联测试配置。
每个加载 bash 执行器的组合都同时加载 `@deepseek-ai/dsh-subprocess-local`CLI命令行界面、各示例、Python 捆绑运行时以及各内联测试配置。
后台进程的存续期从执行器移到了管理器:执行器不再保有存活进程集合,于是重载执行器后,后台工作会继续运行且仍可读取,而组合拆除(管理器的 dispose仍是先终止再等待退出的边界。一条行为约定随之挪动后台 spawn 失败不再能在管道内部被缓冲成伪造的 stderr对一个从未真正运行的进程管理器会 reject `done`,且不缓冲任何内容),因此执行器把 `spawn failed: …` 提示注入恰好一个 `readOutput()` 增量。
基于已观察到的流与生命周期需求,具备条件的进程消费方随后迁到该 seamLSP 使用管道化协议流加收集式 stderr 尾部ACPAgent Client Protocol后端使用管道化 ndjson、继承式 stderr 和消费方拥有的 stdin-EOF dispose 阶梯PTY 使用 `spawnTerminal()`,同时保留就绪与终端策略。`dsh-subagent-subprocess` 与 LSP 私有进程树辅助函数均被删除。MCP 传输 spawn、SDK 包管理器运行器、同步 TUI Git 探测和刻意保持轻依赖的 test-support 启动器因所有权或执行形状仍留在外部;适用的生产调用方共享凭据清除。
基于已观察到的流与生命周期需求,具备条件的进程消费方随后迁到该 seamLSP 使用管道化协议流加收集式 stderr 尾部ACPAgent Client Protocol后端使用管道化 ndjson、继承式 stderr 和消费方拥有的 stdin-EOF dispose 阶梯PTY 使用 `spawnTerminal()`,同时保留就绪与终端策略。`dsh-subagent-subprocess` 与 LSP 私有进程树辅助函数均被删除。MCP 传输 spawn 和刻意保持轻依赖的 test-support 启动器因所有权或执行形状仍留在外部;适用的生产调用方共享凭据清除。
## 曾考虑的替代方案
@@ -31,7 +31,7 @@ Status: implemented
**用单个 `stdio: 'pipe' | 'inherit' | 'collect'` 模式统一全部流。**否决真实消费方按流混用模式——LSP 使用 pipe/pipe/collectACP 使用 pipe/pipe/inheritBash 使用 data/collect/collect。
**把每一次进程启动都路由到 `ctx.subprocess`。**否决MCP SDK 拥有其传输 spawnSDK 向导没有 Cordis 上下文且需要继承式重定向TUI 探测是同步的,support 启动器则刻意独立于产品 seam。PTY 分配迁到 `spawnTerminal()`,因为这项底层专用原语归提供方而非消费方所有。
**把每一次进程启动都路由到 `ctx.subprocess`。**否决MCP SDK 拥有其传输 spawnsupport 启动器则刻意独立于产品 seam。PTY 分配迁到 `spawnTerminal()`,因为这项底层专用原语归提供方而非消费方所有。
**改把 `run_in_background`/任务语义放进 subprocess 能力 seam。**否决:那条边界已经存在。`ctx.tasks` 拥有 id、所有权与通知bash 工具则把 `BashProcess` 适配成任务钩子。subprocess seam 位于 bash 执行器*之下*,而不是与任务注册表并列。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md
2026-07-26-task-registry-seam.md: 4487bd9c53595fa8b4eed588b294ceafe3ab58dc
2026-07-26-task-registry-seam.zh.md: 6195dc809e84852c7e0f63ac101ba0ed6a46853e
2026-07-26-task-registry-seam.md: d5864d86577839c77ab27d70c9dc1c6a79685d56
2026-07-26-task-registry-seam.zh.md: 096cf41c614d1e9e15b6421412e9dc8160e38099

View File

@@ -6,23 +6,23 @@ English | [中文](2026-07-26-task-registry-seam.zh.md)
## Problem
The [background-task runtime](2026-06-20-generic-long-running-tool-runtime.md) shipped `TaskService` as one concrete package: `@deepseek-ai/dsh-tasks` owned both the `ctx.tasks` contract every producer and control surface programs against and the process-local provider (the in-memory store, settlement bookkeeping, owner-cleanup effects, teardown). That bundling recouples the two rates of change the repository's [capability-seam rule](2026-06-13-capability-seams.md) separates: swapping the registry's storage or lifecycle backend would churn the same package whose types and `ctx.tasks` surface producers (`dsh-tool-bash`, `dsh-tool-pty`, `dsh-tool-subagent`), the control surface (`dsh-tool-tasks`), and `TaskKindMap` extenders import. Every other swappable capability in the harness — bash, pty, fs, skill, subagent, web, session persistence — already carries the Service Definition / Service provider / Consumer split; the task registry was the remaining `core`-mode exception, guarded only by a `TODO(task-service-backend)` comment.
The [background-task runtime](2026-06-20-generic-long-running-tool-runtime.md) shipped `TaskService` as one concrete package: `@deepseek-ai/dsh-tasks` owned both the `ctx.tasks` contract every producer and controller programs against and the process-local provider (the in-memory store, settlement bookkeeping, owner-cleanup effects, teardown). That bundling recouples the two rates of change the repository's [capability-seam rule](2026-06-13-capability-seams.md) separates: swapping the registry's storage or lifecycle backend would churn the same package whose types and `ctx.tasks` API producers (`dsh-tool-bash`, `dsh-tool-pty`, `dsh-tool-subagent`), the controller (`dsh-tool-tasks`), and `TaskKindMap` extenders import. Every other swappable capability in the harness — bash, pty, fs, skill, subagent, web, session persistence — already carries the Service Definition / Service provider / Consumer split; the task registry was the remaining `core`-mode exception, guarded only by a `TODO(task-service-backend)` comment.
## Decision
`tasks/` is now a three-package capability family in the bash-trio shape:
- **`@deepseek-ai/dsh-tasks` (Service Definition)** — the abstract `TaskService extends Service` owning `ctx.tasks`, the eight-method contract (`start`, `list`, `get`, `read`, `kill`, `wait`, `onTaskDone`, `attachSurface`), all vocabulary types (`TaskId`, `TaskKindMap`, `TaskStart`, `TaskHooks`, `TaskOutcome`, `TaskSnapshot`, `TaskRead`, `TaskDoneListener`), and the snapshot invariant companion. The class-level JSDoc states the semantics every Service provider owes: registrations outlive producer and surface fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no attached control surface serves the spec's owner (surfaces and listeners are scope-layered, so one process-wide registry answers both questions per owner).
- **`@deepseek-ai/dsh-tasks` (Service Definition)** — the abstract `TaskService extends Service` owning `ctx.tasks`, the eight-method contract (`start`, `list`, `get`, `read`, `kill`, `wait`, `onTaskDone`, `attachController`), all vocabulary types (`TaskId`, `TaskKindMap`, `TaskStart`, `TaskHooks`, `TaskOutcome`, `TaskSnapshot`, `TaskRead`, `TaskDoneListener`), and the snapshot invariant companion. The class-level JSDoc states the semantics every Service provider owes: registrations outlive producer and controller fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no attached task controller serves the spec's owner (controllers and listeners are scope-layered, so one process-wide registry answers both questions per owner).
- **`@deepseek-ai/dsh-tasks-local` (Service provider)** — `LocalTaskService`, the process-local registry moved verbatim: the in-memory store, per-kind counters, waiter bookkeeping, `TASK_WAIT_TIMEOUT` deadline code, owner-cleanup effects, and force-fail teardown. The `dsh-timeout` dependency moves here with it; the Service Definition package has no provider dependencies.
- **`@deepseek-ai/dsh-tool-tasks` (Consumer)** — unchanged; it injects `'tasks'` and never imports provider types.
Compositions load `dsh-tasks-local` where they previously loaded `dsh-tasks` (the CLI cordis.yml row, `agent-spine-demo`, test harnesses, the tool-catalog generator boot). Producer misconfiguration diagnostics ("background tasks unavailable: load …") name `dsh-tasks` — the Service Definition package that declares the absent `ctx.tasks` service — and the Service Definition package's own surfaces (its README and the direct-mount fence) point at Service providers, so the producer message stays correct when another backend becomes the recommended default. Producers, `TaskKindMap` declaration merges, and the control surface keep importing `@deepseek-ai/dsh-tasks` only.
Compositions load `dsh-tasks-local` where they previously loaded `dsh-tasks` (the CLI cordis.yml row, `agent-spine-demo`, test harnesses, the tool-catalog generator boot). Producer misconfiguration diagnostics ("background tasks unavailable: load …") name `dsh-tasks` — the Service Definition package that declares the absent `ctx.tasks` service — and the Service Definition package's own APIs (its README and the direct-mount fence) point at Service providers, so the producer message stays correct when another backend becomes the recommended default. Producers, `TaskKindMap` declaration merges, and the controller keep importing `@deepseek-ai/dsh-tasks` only.
The seam keeps the in-process contract semantics unchanged: `TaskStart.run()` still passes callbacks and exact `Agent` objects, so a durable or cross-process backend still has design work to do before it can satisfy this Service Definition (identity, restart, ownership, observation). The split moves that future work out of every Consumer's dependency graph; it does not pre-design the backend.
## Alternatives considered
**Keep the concrete service until a second backend exists (status quo).** This was the original runtime note's position: extracting a Service Definition before a second provider risks freezing the wrong boundary. It lost because the boundary is no longer speculative — the eight service methods and their semantics have been stable across every producer integration since introduction, they are exactly the surface `dsh-tool-tasks` and the producers already program against, and the repository convention treats swappable capabilities as three packages by default. The residual risk (a durable backend needing contract changes) is unchanged by the split: those changes would land in the Service Definition package either way, and today they would also churn every Consumer's provider dependency.
**Keep the concrete service until a second backend exists (status quo).** This was the original runtime note's position: extracting a Service Definition before a second provider risks freezing the wrong boundary. It lost because the boundary is no longer speculative — the eight service methods and their semantics have been stable across every producer integration since introduction, they are exactly the API `dsh-tool-tasks` and the producers already program against, and the repository convention treats swappable capabilities as three packages by default. The residual risk (a durable backend needing contract changes) is unchanged by the split: those changes would land in the Service Definition package either way, and today they would also churn every Consumer's provider dependency.
**Service-Definition-only extraction inside one package (export an abstract class beside the concrete one).** Rejected because it separates nothing operationally: Consumers still depend on the package that carries the provider and its dependencies, and a replacement backend still cannot ship without the local one in its graph. The package boundary is the unit of independent evolution here.
@@ -30,6 +30,6 @@ The seam keeps the in-process contract semantics unchanged: `TaskStart.run()` st
## Consequences
Bought: the task registry now matches the repository-wide seam shape; a durable, remote, or instrumented registry is a sibling Service provider implementing eight abstract methods, and no producer, control surface, or `TaskKindMap` extender changes when one lands. The Service Definition README states the contract; the provider README owns the lifecycle bookkeeping facts. The registry behavior suite (owner cleanup, settlement, waits, teardown) lives with `dsh-tasks-local`; the Service Definition package keeps a stub-subclass test pinning registration under `ctx.tasks` and single-service duplication behavior, plus the probe-based invariant suite.
Bought: the task registry now matches the repository-wide seam shape; a durable, remote, or instrumented registry is a sibling Service provider implementing eight abstract methods, and no producer, controller, or `TaskKindMap` extender changes when one lands. The Service Definition README states the contract; the provider README owns the lifecycle bookkeeping facts. The registry behavior suite (owner cleanup, settlement, waits, teardown) lives with `dsh-tasks-local`; the Service Definition package keeps a stub-subclass test pinning registration under `ctx.tasks` and single-service duplication behavior, plus the probe-based invariant suite.
Cost: one more package (manifest, tsconfig, README, invariant companion), and compositions must name the Service provider package. `abstract` erases at runtime and this package name used to be the mountable registry, so the Service Definition constructor fails loudly when mounted directly — a stale composition row gets "load a Service provider such as @deepseek-ai/dsh-tasks-local" at load time instead of a half-registered `ctx.tasks` failing far from the misconfiguration.

View File

@@ -6,17 +6,17 @@ Status: implemented
## 问题
[后台任务运行时](2026-06-20-generic-long-running-tool-runtime.md)交付时把 `TaskService` 做成了单个具体包:`@deepseek-ai/dsh-tasks` 既拥有每个生产方和控制接口面向编程的 `ctx.tasks` 约定,也拥有进程内 Service provider内存存储、结算簿记、所有者清理 effect、拆除。这种捆绑重新耦合了仓库[能力 seam 规则](2026-06-13-capability-seams.md)本要分离的两种变化速率:一旦替换注册表的存储或生命周期后端,被搅动的就是同一个包,而生产方(`dsh-tool-bash``dsh-tool-pty``dsh-tool-subagent`)、控制接口`dsh-tool-tasks`)和 `TaskKindMap` 扩展方正是从这个包导入类型与 `ctx.tasks` 接口。harness 中其余每项可替换能力——bash、pty、fs、skill技能、subagent、web、会话持久化——都已具备 Service Definition / Service provider / Consumer 三分;任务注册表曾是仅剩的 `core` 模式例外,仅由一条 `TODO(task-service-backend)` 注释把守。
[后台任务运行时](2026-06-20-generic-long-running-tool-runtime.md)交付时把 `TaskService` 做成了单个具体包:`@deepseek-ai/dsh-tasks` 既拥有每个生产方和控制面向编程的 `ctx.tasks` 约定,也拥有进程内 Service provider内存存储、结算簿记、所有者清理 effect、拆除。这种捆绑重新耦合了仓库[能力 seam 规则](2026-06-13-capability-seams.md)本要分离的两种变化速率:一旦替换注册表的存储或生命周期后端,被搅动的就是同一个包,而生产方(`dsh-tool-bash``dsh-tool-pty``dsh-tool-subagent`)、控制`dsh-tool-tasks`)和 `TaskKindMap` 扩展方正是从这个包导入类型与 `ctx.tasks` API。harness 中其余每项可替换能力——bash、pty、fs、skill技能、subagent、web、会话持久化——都已具备 Service Definition / Service provider / Consumer 三分;任务注册表曾是仅剩的 `core` 模式例外,仅由一条 `TODO(task-service-backend)` 注释把守。
## 决策
`tasks/` 如今是一个 bash 三件套形态的三包能力家族:
- **`@deepseek-ai/dsh-tasks`Service Definition**——抽象的 `TaskService extends Service`,拥有 `ctx.tasks`、八个方法的约定(`start``list``get``read``kill``wait``onTaskDone``attachSurface`)、全部词汇类型(`TaskId``TaskKindMap``TaskStart``TaskHooks``TaskOutcome``TaskSnapshot``TaskRead``TaskDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个 Service provider 都必须兑现的语义:注册的存续期长于生产方与控制接口的 fiber有所有者的访问以会话为界结算遵循首次结果优先且监听器错误被隔离并且当没有任何已附加的控制接口服务于 spec 的所有者时 `start` 拒绝启动工作(控制接口与监听器按 scope 分层,因此一个进程级注册表能逐所有者地回答这两个问题)。
- **`@deepseek-ai/dsh-tasks`Service Definition**——抽象的 `TaskService extends Service`,拥有 `ctx.tasks`、八个方法的约定(`start``list``get``read``kill``wait``onTaskDone``attachController`)、全部词汇类型(`TaskId``TaskKindMap``TaskStart``TaskHooks``TaskOutcome``TaskSnapshot``TaskRead``TaskDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个 Service provider 都必须兑现的语义:注册的存续期长于生产方与控制的 fiber有所有者的访问以会话为界结算遵循首次结果优先且监听器错误被隔离并且当没有任何已附加的任务控制器服务于 spec 的所有者时 `start` 拒绝启动工作(控制与监听器按 scope 分层,因此一个进程级注册表能逐所有者地回答这两个问题)。
- **`@deepseek-ai/dsh-tasks-local`Service provider**——`LocalTaskService`,即原样迁移的进程内注册表:内存存储、按 kind 划分的计数器、等待方簿记、`TASK_WAIT_TIMEOUT` deadline 代码、所有者清理 effect以及强制失败的拆除。`dsh-timeout` 依赖随之迁入此包Service Definition 包不含任何提供方依赖。
- **`@deepseek-ai/dsh-tool-tasks`Consumer**——保持不变;它注入 `'tasks'`,从不导入提供方类型。
各组合在原先加载 `dsh-tasks` 的位置改为加载 `dsh-tasks-local`CLI命令行界面的 cordis.yml 配置项、`agent-spine-demo`、各测试 harness以及工具目录生成器的启动流程。生产方的配置错误诊断信息「background tasks unavailable: load …」)点名 `dsh-tasks`——即声明缺失的 `ctx.tasks` 服务的 Service Definition 包Service Definition 包自身的对外呈现(其 README 与直接挂载防线)会指向各 Service provider因此当另一个后端日后成为推荐默认时生产方的消息依旧正确。生产方、`TaskKindMap` 声明合并和控制接口仍然只导入 `@deepseek-ai/dsh-tasks`
各组合在原先加载 `dsh-tasks` 的位置改为加载 `dsh-tasks-local`CLI命令行界面的 cordis.yml 配置项、`agent-spine-demo`、各测试 harness以及工具目录生成器的启动流程。生产方的配置错误诊断信息「background tasks unavailable: load …」)点名 `dsh-tasks`——即声明缺失的 `ctx.tasks` 服务的 Service Definition 包Service Definition 包自身的 API(其 README 与直接挂载防线)会指向各 Service provider因此当另一个后端日后成为推荐默认时生产方的消息依旧正确。生产方、`TaskKindMap` 声明合并和控制仍然只导入 `@deepseek-ai/dsh-tasks`
该 seam 保持进程内约定语义不变:`TaskStart.run()` 仍然传入回调和确切的 `Agent` 对象,因此持久化或跨进程后端在能满足此 Service Definition 之前仍有设计工作要做(身份、重启、所有权、观察)。这次拆分把该项未来工作移出了每个 Consumer 的依赖图;它并不预先设计后端。
@@ -30,6 +30,6 @@ Status: implemented
## 后果
换来的是:任务注册表如今与全仓库通行的 seam 形态一致;持久化、远程或带插桩的注册表将是一个实现八个抽象方法的同级 Service provider这样的注册表落地时任何生产方、控制接口`TaskKindMap` 扩展方都无需改动。Service Definition 的 README 陈述约定;生命周期簿记方面的事实归 Service provider 的 README 所有。注册表行为测试套件(所有者清理、结算、等待、拆除)随 `dsh-tasks-local` 存放Service Definition 包保留一个桩子类stub subclass测试固定 `ctx.tasks` 下的注册行为与单一服务的重复注册行为,外加基于探针的不变式测试套件。
换来的是:任务注册表如今与全仓库通行的 seam 形态一致;持久化、远程或带插桩的注册表将是一个实现八个抽象方法的同级 Service provider这样的注册表落地时任何生产方、控制`TaskKindMap` 扩展方都无需改动。Service Definition 的 README 陈述约定;生命周期簿记方面的事实归 Service provider 的 README 所有。注册表行为测试套件(所有者清理、结算、等待、拆除)随 `dsh-tasks-local` 存放Service Definition 包保留一个桩子类stub subclass测试固定 `ctx.tasks` 下的注册行为与单一服务的重复注册行为,外加基于探针的不变式测试套件。
代价是:多出一个包,即多一份 manifest元数据清单、tsconfig、README 与不变式配套插件;同时各组合必须点名 Service provider 包。`abstract` 在运行时会被擦除,而这个包名过去正是可挂载的具体注册表,因此直接挂载 Service Definition 时其构造函数会明确报错——一条陈旧的组合配置行会在加载时得到「load a Service provider such as @deepseek-ai/dsh-tasks-local」而不是一个未完整注册的 `ctx.tasks` 在远离错误配置处才失败。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md
2026-07-28-api-browser-trust-boundary.md: 7c1615fedf0f18f5e2c341b2246415057a6ee172
2026-07-28-api-browser-trust-boundary.md: f4d14201d052299bb6063553e962a5d7c74fdb4b
2026-07-28-api-browser-trust-boundary.zh.md: fc210921918f2e790f3f1fe36edd66e73f19d74c

View File

@@ -1,4 +1,4 @@
# Agent Note: One carrier-level browser-trust boundary for the whole /api surface
# Agent Note: One carrier-level browser-trust boundary for all `/api` routes
Status: implemented

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-package-regrouping.md
2026-07-29-package-regrouping.md: 30fc45a122263350b4a2ad1998850f631c20f9b8
2026-07-29-package-regrouping.zh.md: a3a9a11ec71b7f894dcea7c733eb39a80b71ac50
2026-07-29-package-regrouping.md: 6b85fafaebd75b051d239966a30bb81bdaf5522f
2026-07-29-package-regrouping.zh.md: 06eb210e318833d9651af2c4f0e4d792799a2018

View File

@@ -10,29 +10,26 @@ The two-level `packages/<group>/<pkg>` hierarchy ([original decision](../../arch
- `ui/` mixed four unrelated planes: the human terminal channel (`tui`), the SDK's JSON-RPC server half (`jsonrpc`, whose peer dependency on `dsh-sdk-protocol` binds it to the SDK wire stack), the human-interaction seams (`user-interaction`, `user-approval`, `permission`, `tool-ask-user`, `commands`), and channel-neutral boot glue (`app-boot`). Its own README narrated the mixture instead of stating a role.
- The session family was fragmented across five groups — `session-persistence/`, `session-projection/`, `session-query/`, `session-title/`, and `telemetry/` — although the measured dependency edges tie them together (query → persistence, title → projection, projection → persistence; see [docs/module-graph.md](../../../../docs/module-graph.md)).
- Two group names collided with unrelated packages: `telemetry/` (session reporting) vs `dsh-telemetry` (launcher-side SDK telemetry), and `timeout/` (a tool-call guard) vs `util/timeout` (the generic promise utility).
- The `timeout/` group for a tool-call guard collided with `util/timeout`, the generic promise utility.
- `cordis/` named its group after the framework every package is built on, so the name discriminated nothing; its single package `tool-cordis` is the runtime self-modification toolset.
- The old `sdk/` folder names were inconsistent: `sdk/sdk-client` and `sdk/sdk-protocol` repeated the group name while `sdk/telemetry`, `sdk/helper`, and `sdk/scripts` did not.
The north star for the regrouping: **closely clustered packages share a group.** A cluster is measured — peer-dependency edges and co-change — not thematic. An isolated seam family may stand alone as a small group; the failure mode to avoid is the grab-bag whose name describes no single role.
## Decision
Six groups are recomposed; every other group keeps its prior boundary and contents (the dependency analysis confirmed the capability families — `bash/`, `pty/`, `code-runtime/`, `sandbox/`, `subprocess/`, `fs/`, `lsp/`, `web/`, `skill/`, and the rest — were already drawn correctly). npm package names did not change; the folder tree carries the whole change.
Five regrouping decisions remain current; every other group keeps its prior boundary and contents (the dependency analysis confirmed the capability families — `bash/`, `pty/`, `code-runtime/`, `sandbox/`, `subprocess/`, `fs/`, `lsp/`, `web/`, `skill/`, and the rest — were already drawn correctly). The original sixth decision collected the SDK project initializer, launcher tooling, and runtime JSON-RPC packages under `scaffold/`; [removing that unreleased toolchain](../simplification/2026-08-11-remove-sdk-project-toolchain.md) superseded it by deleting the project tooling and moving the surviving runtime trio to `sdk/`.
| Group | Members (folder names) | From |
|---|---|---|
| `session/` | session-persistence, session-persistence-jsonl, session-persistence-sqlite, session-checkpoint-policy, session-projection, session-projection-cache, session-title, session-title-llm, session-title-first-message-llm, session-title-all-messages-llm, session-telemetry, session-telemetry-otel | `session-persistence/` + `session-projection/` + `session-title/` + `telemetry/` |
| `interaction/` | user-interaction, user-approval, permission, tool-ask-user, commands, tui | `ui/` |
| `boot/` | app-boot | `ui/` |
| `scaffold/` | helper, scripts, create-sdk, protocol, client, server, telemetry | `sdk/` + `ui/jsonrpc` |
| `guard/` | repeat-tool-guard, timeout-policy | `guard/` + `timeout/` |
| `self-modification/` | tool-cordis | `cordis/` |
- **`session/`** is the durable session data plane: the persistence seam with its backends and checkpoint policy, the projection fold that serves whole values from that log, log-backed titles, and OTel reporting. The title fold is itself load-bearing for the read side (`session-query` peer-depends on `dsh-session-title`), so titles belong with the data plane, not in a derived-services annex. The plain name is deliberate (prefer names a human would say); the nearby `core/session` package remains the live in-memory service, while this group is the durable family around it. `session-query/` stays a standalone group — the read/tool surface has its own model tools and SQLite FTS backend and is consumed independently of persistence internals. Absorbing `telemetry/` ended the group-name collision with `dsh-telemetry`.
- **`session/`** is the durable session data plane: the persistence seam with its backends and checkpoint policy, the projection fold that serves whole values from that log, log-backed titles, and OTel reporting. The title fold is itself load-bearing for the read side (`session-query` peer-depends on `dsh-session-title`), so titles belong with the data plane, not in a derived-services annex. The plain name is deliberate (prefer names a human would say); the nearby `core/session` package remains the live in-memory service, while this group is the durable family around it. `session-query/` stays a standalone group — the read/tool surface has its own model tools and SQLite FTS backend and is consumed independently of persistence internals.
- **`interaction/`** is the human-collaboration plane plus the terminal channel that answers it: the question/approval seams, the permission preset, the model-facing `ask_user_question` tool, the human-command registry (`plan-mode` and `command-goal` already consume `commands` together with the interaction seams), and `tui` — the interactive channel is the plane's richest provider and consumer (peer edges to `commands` and `user-interaction`), and a one-package `tui/` group would spend a top-level name on one plugin.
- **`boot/`** is a role-complete single-package group: the shared bin boot glue that belongs to no channel and no assembly (consumed by `apps/cli`, the `scaffold/` launcher, and the `examples/` demo bins).
- **`scaffold/`** is the developer-tooling family: project helper, launcher, initializer, wire protocol with both ends (`server` is the former `ui/jsonrpc`), and launcher telemetry. Renamed from `sdk/`: the whole `packages/` tree *is* the SDK, so a group named `sdk/` inside it said nothing; `scaffold/` names the create/launch/drive-a-project role. Folders drop the legacy `sdk-` prefix (`protocol`, `client`, `server`), matching the `client/`/`host/` role-named folder style; the three affected npm names are mapped explicitly beside the group wildcard in `tsconfig.base.json` until the deferred renames land.
- **`boot/`** is a role-complete single-package group: the shared bin boot glue that belongs to no channel and no assembly (consumed by `apps/cli` and the `examples/` demo bins).
- **`guard/`** keeps its documented role, loop-hygiene guards, and gains the tool-call timeout enforcer, dissolving the one-package `timeout/` group whose name collided with `util/timeout`.
- **`self-modification/`** names the role `cordis/` obscured: the toolset with which the agent inspects and mounts plugins in its own live runtime, and the landing zone for future self-modification packages.
@@ -40,21 +37,16 @@ Six groups are recomposed; every other group keeps its prior boundary and conten
## Deferred renames (FIXME markers)
Five npm names should eventually change, but renaming inside the reorganization would have turned a pure-move PR into an import-churn PR. Instead, each affected package's module JSDoc carries a `FIXME` naming the intended new name. `FIXME` blocks a tagged release ([marker semantics](../../../../docs/development.md)), which is the wanted forcing function: these renames are only free while nothing external consumes the packages.
Two npm names should eventually change, but renaming inside the reorganization would have turned a pure-move PR into an import-churn PR. Each affected package's module JSDoc carries a `FIXME` naming the intended new name. `FIXME` blocks a tagged release ([marker semantics](../../../../docs/development.md)), which is the wanted forcing function: these renames are only free while nothing external consumes the packages. Removing the unreleased SDK project toolchain deleted the other three packages and their rename markers instead of preserving names for code that no longer exists.
| Current npm name | Intended name | Why |
|---|---|---|
| `@deepseek-ai/dsh-jsonrpc` | `@deepseek-ai/dsh-sdk-server` | Names the wire encoding, not the role; it is the server half of the SDK protocol |
| `@deepseek-ai/dsh-telemetry` | `@deepseek-ai/dsh-sdk-telemetry` | Collides with the `dsh-session-telemetry` family; it is launcher-side SDK telemetry |
| `@deepseek-ai/dsh-helper` | `@deepseek-ai/dsh-sdk-helper` | Indefensibly generic as a published name |
| `@deepseek-ai/dsh-scripts` | `@deepseek-ai/dsh-sdk-scripts` | Same |
| `@deepseek-ai/dsh-timeout-policy` | `@deepseek-ai/dsh-timeout-guard` | Suggestion, not settled: aligns the name with its `guard/` home; decide at resolution time |
The first four are settled intent; resolving them converges the SDK wire stack's npm names on `dsh-sdk-*` (the npm prefix names the product stack; the `scaffold/` folder names the role). `@deepseek-ai/create-sdk` keeps its documented npm-initializer exception.
## What the move touched
The moves landed as pure `git mv` moves, so rename detection carries the history. A group move touched: the moved package's `tsconfig.json` relative `references` and every dependent's entry (including the `apps/cli` project references), the tsconfig aggregate and path maps, group READMEs (five new bilingual triplets, deletions for dissolved groups, the [packages/README.md](../../../../packages/README.md) hierarchy table, the root `AGENTS.md` layout map), regenerated artifacts (`docs/module-graph.md`, path-embedding catalogs, the lockfile's importer keys), and root-relative `packages/...` citations in prose and gate scripts. Remaining group-path referents (workspace configs, test globs, lint keys) were found mechanically by the acceptance gates failing loud — the repository's own misconfiguration rule.
The moves landed as pure `git mv` moves, so rename detection carries the history. A group move touched: the moved package's `tsconfig.json` relative `references` and every dependent's entry (including the `apps/cli` project references), the tsconfig aggregate and path maps, group READMEs, the [packages/README.md](../../../../packages/README.md) hierarchy table, the root `AGENTS.md` layout map, regenerated artifacts (`docs/module-graph.md`, path-embedding catalogs, and the lockfile's importer keys), and root-relative `packages/...` citations in prose and gate scripts. Remaining group-path referents (workspace configs, test globs, lint keys) were found mechanically by the acceptance gates failing loud — the repository's own misconfiguration rule.
A group move did not touch: npm names, imports, `cordis.yml` configs, snapshot fixtures, the `pnpm-workspace.yaml`/`tsdown` globs (both `packages/*/*`), or the Python runtime manifest — all reference packages by npm name.
@@ -62,13 +54,13 @@ A group move did not touch: npm names, imports, `cordis.yml` configs, snapshot f
## Alternatives considered
**Coarse domain buckets** (`exec/` = subprocess+sandbox+bash+pty+code-runtime, `workspace/` = fs+lsp+workspace, `orchestration/` = subagent+workflow+tasks, `knowledge/` = web+skill, `collab/` = plan+todo+goal; ~16 groups). Rejected: the measured graph contradicts the merges. `sandbox` and `subprocess` are shared infrastructure consumed across families (bash ×5, fs ×5, pty, lsp, mcp, subagent, scaffold edges), `web``skill` have zero edges, and a large bucket reproduces the `ui/` grab-bag at a larger scale.
**Coarse domain buckets** (`exec/` = subprocess+sandbox+bash+pty+code-runtime, `workspace/` = fs+lsp+workspace, `orchestration/` = subagent+workflow+tasks, `knowledge/` = web+skill, `collab/` = plan+todo+goal; ~16 groups). Rejected: the measured graph contradicts the merges. `sandbox` and `subprocess` are shared infrastructure consumed across families (bash ×5, fs ×5, pty, lsp, mcp, and subagent edges), `web``skill` have zero edges, and a large bucket reproduces the `ui/` grab-bag at a larger scale.
**Abstract layer names** (`capability/`, `policy/`, `extension/`, `provider/`). Rejected: they describe every plugin equally badly, and a `capability/` bucket would hold ~50 packages.
**A full npm rename sweep** (`dsh-<group>-<pkg>` for every package). Rejected: npm names are flat, so group-prefixing adds churn across imports, configs, and fixtures with no disambiguation gain; targeted FIXME-tracked renames cover the actual collisions.
**Performing the five renames inside the reorganization.** Rejected: renames multiply open-PR conflicts and destroy the pure-move review property. The FIXME markers keep them visible release blockers to resolve as small follow-up PRs.
**Performing the deferred renames inside the reorganization.** Rejected: renames multiply open-PR conflicts and destroy the pure-move review property. The remaining FIXME markers keep them visible release blockers to resolve as small follow-up PRs.
**A two-way session split** (`session-core/` + `session-utils/`). Rejected: query belongs to neither side cleanly, and `session-core` invites confusion with `core/session` (`dsh-session`, the live in-memory service, which stays in `core/`).
@@ -78,9 +70,7 @@ A group move did not touch: npm names, imports, `cordis.yml` configs, snapshot f
**A standalone one-package `tui/` group.** Rejected: `tui` is the interaction plane's primary provider/consumer (peer edges to `commands`, `user-interaction`), and a top-level name spent on one plugin adds a group without adding information; it folds into `interaction/`.
**Keeping the group name `sdk/`.** Rejected: the whole `packages/` tree is the SDK, so an `sdk/` group inside it discriminates nothing — the same disease as `cordis/`. `scaffold/` names the actual role (create, launch, and drive projects from outside).
**Moving `app-boot` to `apps/`.** Rejected: `apps/` is the assembly tier over the package tier, and `dsh-app-boot` is a library that package-tier code imports (`scaffold/scripts`' launcher peer-depends on it) — placing it in `apps/` would invert the tiers and put a workspace library outside the `packages/*/*` build globs. It stays a package; `boot/` is its role-complete home.
**Moving `app-boot` to `apps/`.** Rejected: `apps/` is the assembly tier over the package tier, and `dsh-app-boot` is a package-tier library — placing it in `apps/` would invert the tiers and put a workspace library outside the `packages/*/*` build globs. It stays a package; `boot/` is its role-complete home.
**Moving `tool-cordis` into `core/`.** Rejected: self-modification is its own product seam, expected to grow; the spine stays minimal. The group was first named `self-evolve/`; the name settled on `self-modification/` as the plainer term.
@@ -88,9 +78,9 @@ A group move did not touch: npm names, imports, `cordis.yml` configs, snapshot f
## Consequences
- The tree matches the map: the six recomposed groups hold exactly the listed members; the groups `ui/`, `sdk/`, `telemetry/`, `timeout/`, `cordis/`, `session-persistence/`, `session-projection/`, and `session-title/` no longer exist; every other group's contents are unchanged. The workspace package-name set is identical before and after (zero npm renames), and the five FIXME markers pin the deferred ones. A FIXME that later proves wrong must be removed explicitly with rationale, never silently dropped.
- The five still-current regrouped families hold the listed members; the groups `ui/`, `telemetry/`, `timeout/`, `cordis/`, `session-persistence/`, `session-projection/`, and `session-title/` no longer exist. The regrouping itself changed no npm names. The later SDK toolchain removal intentionally changed the package set and restored `sdk/` as the precise home of the runtime SDK trio. Two FIXME markers pin the remaining deferred renames; a FIXME that later proves wrong must be removed explicitly with rationale, never silently dropped.
- What pins the result: `pnpm run typecheck`, the unit suites of every moved group, `verify-package-paths`, `verify-md-links`, and the corpus-wide translation pairing all pass on the moved tree; the group-scoped test globs in `vitest.snapshot.config.ts` were rewritten with the moves so the suites collect the same test files as before (a fail-open glob would silently drop coverage).
- Every open PR touching a moved file rebases across the move once; rename detection resolves most hunks mechanically.
- Single-package groups remain (`boot/`, `self-modification/`, and existing ones such as `acp/`). Accepted deliberately: each is role-complete rather than a fragment of a family, and a truthful small group beats a nominal merge.
- The `scaffold/` folders diverge from their npm names until the deferred renames land — the one transitional asymmetry, carried by three explicit `paths` entries in `tsconfig.base.json` and resolved by the FIXME renames.
- The `sdk/` role folders map explicitly to their npm names in `tsconfig.base.json`; the `server/` mapping remains transitional until `dsh-jsonrpc` is renamed.
- What this gave up: nothing functional — the change is navigational. Muscle memory and external links to old GitHub paths break, which is acceptable pre-release with no external consumers.

View File

@@ -10,29 +10,26 @@ Status: implemented
- `ui/` 混杂了四个互不相关的平面:人类终端通道(`tui`、SDK 的 JSON-RPC 服务端一半(`jsonrpc`,它对 `dsh-sdk-protocol` 的对等依赖peer dependency把它绑在 SDK 通信栈上)、人机交互 seam`user-interaction``user-approval``permission``tool-ask-user``commands`),以及与通道无关的 boot 胶水(`app-boot`)。它自己的 README 只能逐一叙述这堆混杂,说不出一个统一职责。
- 会话家族被割裂在五个组里——`session-persistence/``session-projection/``session-query/``session-title/``telemetry/`——而实测依赖边明明把它们连成一体query → persistence、title → projection、projection → persistence见 [docs/module-graph.md](../../../../docs/module-graph.md))。
- 两个组名与不相干的包撞名:`telemetry/`(会话上报)撞上 `dsh-telemetry`(启动器侧 SDK telemetry`timeout/`(一个工具调用守卫)撞上 `util/timeout`(通用 promise 工具)
- 用于工具调用守卫的 `timeout/` 组与通用 promise 工具 `util/timeout` 撞名
- `cordis/` 拿所有包共同依托的框架给自己的组命名,这个名字因此毫无区分度;组里唯一的包 `tool-cordis` 是运行时自我修改工具集。
-`sdk/` 的目录命名不一致:`sdk/sdk-client``sdk/sdk-protocol` 重复了组名,而 `sdk/telemetry``sdk/helper``sdk/scripts` 没有。
这次重新分组的指导准则:**聚类紧密的包同处一组。**聚类以实测为准(对等依赖边与 co-change而非按主题归类。孤立的 seam 家族可以自成一个小组;要避免的失败形态,是名字概括不出单一职责的大杂烩组。
## Decision
重组六个组;其余每个组都保持先前的边界与内容不变(依赖分析确认各能力家族——`bash/``pty/``code-runtime/``sandbox/``subprocess/``fs/``lsp/``web/``skill/` 及其余——本来就划得正确)。npm 包名一个未改;整个变更全部由目录树承载
五项重组决策仍然有效;其余每个组都保持先前的边界与内容不变(依赖分析确认各能力家族——`bash/``pty/``code-runtime/``sandbox/``subprocess/``fs/``lsp/``web/``skill/` 及其余——本来就划得正确)。原本的第六项决策把 SDK 项目初始化器、启动器工具与运行时 JSON-RPC 包汇集到 `scaffold/`[移除这套未发布工具链](../simplification/2026-08-11-remove-sdk-project-toolchain.md)的决策删除了项目工具,并将存留的运行时三包移到 `sdk/`,从而取代了该项决策
| 组 | 成员(目录名) | 来源 |
|---|---|---|
| `session/` | session-persistence、session-persistence-jsonl、session-persistence-sqlite、session-checkpoint-policy、session-projection、session-projection-cache、session-title、session-title-llm、session-title-first-message-llm、session-title-all-messages-llm、session-telemetry、session-telemetry-otel | `session-persistence/` + `session-projection/` + `session-title/` + `telemetry/` |
| `interaction/` | user-interaction、user-approval、permission、tool-ask-user、commands、tui | `ui/` |
| `boot/` | app-boot | `ui/` |
| `scaffold/` | helper、scripts、create-sdk、protocol、client、server、telemetry | `sdk/` + `ui/jsonrpc` |
| `guard/` | repeat-tool-guard、timeout-policy | `guard/` + `timeout/` |
| `self-modification/` | tool-cordis | `cordis/` |
- **`session/`** 是持久会话数据平面:持久化 seam 连同其各后端与检查点策略、从该日志折叠fold出全量值对外供值的投影、日志兜底的标题以及 OTel 上报。标题折叠本身就是读取侧的承重构件(`session-query``dsh-session-title` 声明对等依赖),所以标题属于数据平面,而非某个「派生服务」附属区。用这个朴素的名字是有意为之(名字要像人起的);旁边的 `core/session` 包仍是常驻内存的实时服务,本组则是围绕它的持久家族。`session-query/` 保持独立成组:这个读取/工具面自带模型工具和 SQLite FTS 后端,其消费不依赖持久化内部实现。吸收 `telemetry/` 之后,与 `dsh-telemetry` 的组名冲突就此终结。
- **`session/`** 是持久会话数据平面:持久化 seam 连同其各后端与检查点策略、从该日志折叠fold出全量值对外供值的投影、日志兜底的标题以及 OTel 上报。标题折叠本身就是读取侧的承重构件(`session-query``dsh-session-title` 声明对等依赖),所以标题属于数据平面,而非某个「派生服务」附属区。用这个朴素的名字是有意为之(名字要像人起的);旁边的 `core/session` 包仍是常驻内存的实时服务,本组则是围绕它的持久家族。`session-query/` 保持独立成组:这个读取/工具面自带模型工具和 SQLite FTS 后端,其消费不依赖持久化内部实现。
- **`interaction/`** 是人机协作平面加上应答它的终端通道:提问/批准 seam、权限预设、面向模型的 `ask_user_question` 工具、人类命令注册表(`plan-mode``command-goal` 已经把 `commands` 和各交互 seam 放在一起消费),以及 `tui`——这个交互通道是该平面最重的提供方与消费方(对 `commands``user-interaction` 均有对等依赖边),而一个单包 `tui/` 组会把一个顶层名字花在一个插件上。
- **`boot/`** 是角色完备的单包组:不归属任何通道也不归属任何组装的共享 bin boot 胶水(被 `apps/cli``scaffold/` 的启动器和 `examples/` 各演示 bin 消费)。
- **`scaffold/`** 是开发者工具家族:项目 helper、启动器、初始化器、连同两端的通信协议`server` 即原先的 `ui/jsonrpc`),以及启动器侧 telemetry。从 `sdk/` 改名:整个 `packages/` 树本身就是 SDK树里再放一个叫 `sdk/` 的组等于什么都没说;`scaffold/` 说出了「创建/启动/驱动项目」这一实际角色。目录去掉遗留的 `sdk-` 前缀(`protocol``client``server`),与 `client/`/`host/` 的角色命名风格一致;在推迟的改名落地之前,受影响的三个 npm 名在 `tsconfig.base.json` 里于组通配符旁显式映射。
- **`boot/`** 是角色完备的单包组:不归属任何通道也不归属任何组装的共享 bin boot 胶水(被 `apps/cli` `examples/` 各演示 bin 消费)。
- **`guard/`** 保留其文档记载的角色(循环卫生守卫),并新纳入强制执行工具调用超时的包;那个与 `util/timeout` 撞名的单包组 `timeout/` 随之解散。
- **`self-modification/`** 把 `cordis/` 遮蔽掉的角色说了出来:它是 agent智能体检查并挂载自身实时运行时中插件所用的工具集也是未来自我修改类包的落点。
@@ -40,21 +37,16 @@ Status: implemented
## Deferred renames (FIXME markers)
个 npm 名最终应当改掉,但在这次重组内部改名,会把一个纯移动的 PRPull Request变成大量翻改 import 的 PR。因此每个受影响包的模块 JSDoc 里带有一条 `FIXME`,写明意图中的新名字。`FIXME` 会阻塞打 tag 的发布([标记语义](../../../../docs/development.md)),这正是想要的倒逼机制:只有趁还没有外部消费方使用这些包时,这些改名才是零成本的。
个 npm 名最终应当改掉,但在这次重组内部改名,会把一个纯移动的 PRPull Request变成大量翻改 import 的 PR。每个受影响包的模块 JSDoc 里带有一条 `FIXME`,写明意图中的新名字。`FIXME` 会阻塞打 tag 的发布([标记语义](../../../../docs/development.md)),这正是想要的倒逼机制:只有趁还没有外部消费方使用这些包时,这些改名才是零成本的。移除未发布的 SDK 项目工具链时,另外三个包及其改名标记一并删除,没有为已经不存在的代码保留包名。
| 当前 npm 名 | 目标名 | 原因 |
|---|---|---|
| `@deepseek-ai/dsh-jsonrpc` | `@deepseek-ai/dsh-sdk-server` | 名字说的是协议编码而非角色;它是 SDK 协议的服务端一半 |
| `@deepseek-ai/dsh-telemetry` | `@deepseek-ai/dsh-sdk-telemetry` | 与 `dsh-session-telemetry` 家族撞名;它是启动器侧 SDK telemetry |
| `@deepseek-ai/dsh-helper` | `@deepseek-ai/dsh-sdk-helper` | 作为公开发布名空泛得站不住脚 |
| `@deepseek-ai/dsh-scripts` | `@deepseek-ai/dsh-sdk-scripts` | 同上 |
| `@deepseek-ai/dsh-timeout-policy` | `@deepseek-ai/dsh-timeout-guard` | 仅为建议、尚未定案:使名字与其 `guard/` 归属对齐;到解决时再定 |
前四个是已定的意图兑现之后SDK 通信栈的 npm 名随之收敛为 `dsh-sdk-*`npm 前缀指产品栈,`scaffold/` 目录名指角色)。`@deepseek-ai/create-sdk` 保留其文档记载的 npm 初始化器特例。
## What the move touched
移动以纯 `git mv` 形式落地,历史由重命名检测承载。组移动触及了:被移动包的 `tsconfig.json` 相对 `references` 及每个依赖方的对应条目(含 `apps/cli` 的 project referencestsconfig 聚合与路径映射;各组 README(五组新的双语三文件配对、被解散组的 README 删除、[packages/README.md](../../../../packages/README.md) 的层级结构表`AGENTS.md` 的布局图;重新生成的产物(`docs/module-graph.md`、内嵌路径的目录锁文件的 importer 键);以及散文与门禁脚本中以仓库根为基准的 `packages/...` 引用。其余每一处组路径引用workspace 配置、测试 glob、lint 键)都由验收门禁的响亮失败机械地找了出来——这正是本仓库自己的「配置错误必须响亮失败」规则。
移动以纯 `git mv` 形式落地,历史由重命名检测承载。组移动触及了:被移动包的 `tsconfig.json` 相对 `references` 及每个依赖方的对应条目(含 `apps/cli` 的 project referencestsconfig 聚合与路径映射;各组 README[packages/README.md](../../../../packages/README.md) 的层级结构表`AGENTS.md` 的布局图;重新生成的产物(`docs/module-graph.md`、内嵌路径的目录以及锁文件的 importer 键);以及散文与门禁脚本中以仓库根为基准的 `packages/...` 引用。其余每一处组路径引用workspace 配置、测试 glob、lint 键)都由验收门禁的响亮失败机械地找了出来——这正是本仓库自己的「配置错误必须响亮失败」规则。
组移动未触及npm 包名、import、`cordis.yml` 配置、快照 fixture测试前置数据`pnpm-workspace.yaml``tsdown` 的 glob都是 `packages/*/*`),以及 Python 运行时 manifest元数据清单——它们全部按 npm 包名引用包。
@@ -62,13 +54,13 @@ Status: implemented
## Alternatives considered
**粗粒度领域桶**`exec/` = subprocess+sandbox+bash+pty+code-runtime`workspace/` = fs+lsp+workspace`orchestration/` = subagent+workflow+tasks`knowledge/` = web+skill`collab/` = plan+todo+goal约 16 个组)。不予采纳:实测依赖图与这些合并相矛盾。`sandbox``subprocess` 是被各家族跨界消费的共享基础设施(与 bash ×5、fs ×5、pty、lsp、mcpsubagent、scaffold 均有依赖边),`web``skill` 之间零依赖边,而大桶只会在更大尺度上复现 `ui/` 式大杂烩。
**粗粒度领域桶**`exec/` = subprocess+sandbox+bash+pty+code-runtime`workspace/` = fs+lsp+workspace`orchestration/` = subagent+workflow+tasks`knowledge/` = web+skill`collab/` = plan+todo+goal约 16 个组)。不予采纳:实测依赖图与这些合并相矛盾。`sandbox``subprocess` 是被各家族跨界消费的共享基础设施(与 bash ×5、fs ×5、pty、lsp、mcpsubagent 均有依赖边),`web``skill` 之间零依赖边,而大桶只会在更大尺度上复现 `ui/` 式大杂烩。
**抽象分层名**`capability/``policy/``extension/``provider/`)。不予采纳:这些名字对每个插件都同样地不达意,而且一个 `capability/` 桶会装下约 50 个包。
**一轮全量 npm 重命名**(每个包都改为 `dsh-<group>-<pkg>`。不予采纳npm 包名是扁平的,加组前缀只会在 import、配置和 fixture 之间制造改动,却换不来任何消歧收益;用 FIXME 跟踪的定点改名足以覆盖真正的撞名。
**在重组内部一并完成那五个改名。** 不予采纳:改名会成倍放大开放 PR 的冲突并破坏纯移动的评审属性。FIXME 标记让这些改名保持为可见的发布阻塞项,留待以小型后续 PR 逐一解决。
**在重组内部一并完成推迟的改名。** 不予采纳:改名会成倍放大开放 PR 的冲突,并破坏纯移动的评审属性。剩余的 FIXME 标记让这些改名保持为可见的发布阻塞项,留待以小型后续 PR 逐一解决。
**会话两分法**`session-core/` + `session-utils/`。不予采纳query 放哪一侧都不干净,而且 `session-core` 容易与 `core/session` 混淆(后者是 `dsh-session`,常驻内存的实时服务,留在 `core/` 不动)。
@@ -78,9 +70,7 @@ Status: implemented
**独立的单包 `tui/` 组。** 不予采纳:`tui` 是交互平面最重的提供方/消费方(对 `commands``user-interaction` 有对等依赖边),把一个顶层名字花在一个插件上只添组不添信息;它折入 `interaction/`
**保留组名 `sdk/`。** 不予采纳:整个 `packages/` 树本身就是 SDK树里的 `sdk/` 组毫无区分度——与 `cordis/` 同病。`scaffold/` 说出了实际角色(从外部创建、启动、驱动项目)
**把 `app-boot` 挪到 `apps/`。** 不予采纳:`apps/` 是包层之上的组装层,而 `dsh-app-boot` 是被包层代码 import 的库(`scaffold/scripts` 的启动器对它声明对等依赖)——放进 `apps/` 会颠倒层级,并把一个 workspace 库放到 `packages/*/*` 构建 glob 之外。它仍是一个包;`boot/` 是它角色完备的家。
**把 `app-boot` 挪到 `apps/`。** 不予采纳:`apps/` 是包层之上的组装层,而 `dsh-app-boot` 是包层的库——放进 `apps/` 会颠倒层级,并把一个 workspace 库放到 `packages/*/*` 构建 glob 之外。它仍是一个包;`boot/` 是它角色完备的家
**把 `tool-cordis` 挪进 `core/`。** 不予采纳:自我修改是独立的产品 seam预期还会生长主干保持精简。该组最初命名为 `self-evolve/`;名字最终定为更朴素的 `self-modification/`
@@ -88,9 +78,9 @@ Status: implemented
## Consequences
- 目录树与映射表一致:重组的六个组恰好持有所列成员;`ui/``sdk/``telemetry/``timeout/``cordis/``session-persistence/``session-projection/``session-title/` 这些组不复存在其余每个组的内容不变。workspace 的包名集合在前后完全相同npm 改名为零),五条 FIXME 标记钉住推迟改名日后若某条 FIXME 被证明不对,必须连同理由显式移除,绝不允许无声消失。
- 五个仍然有效的重组家族持有所列成员;`ui/``telemetry/``timeout/``cordis/``session-persistence/``session-projection/``session-title/` 这些组不复存在。重组本身没有更改 npm 名。后续移除 SDK 工具链的决策有意改变包集合,并恢复 `sdk/` 作为运行时 SDK 三包的精确归属。两条 FIXME 标记钉住剩余的推迟改名日后若某条 FIXME 被证明不对,必须连同理由显式移除,绝不允许无声消失。
- 结果由以下检查钉住:`pnpm run typecheck`、每个被移动组的单元测试套件、`verify-package-paths``verify-md-links` 与全语料翻译配对在移动后的树上全部通过;`vitest.snapshot.config.ts` 中按组划定的测试 glob 随移动一并改写套件收集到与移动前相同的测试文件glob 匹配为空会无声地丢失覆盖)。
- 每个触碰被移动文件的开放 PR 都跨过这次移动做一次变基;重命名检测可机械化解决大多数改动块。
- 单包组依然存在(`boot/``self-modification/`,以及 `acp/` 等既有单包组)。这是有意接受的:每个都是角色完备的整体而非某个家族的碎片,一个名实相符的小组胜过一次徒有其名的合并。
- 在推迟的改名落地之前,`scaffold/` 的目录名与其 npm 名并不一致——这是唯一的过渡性不对称,由 `tsconfig.base.json` 里三条显式 `paths` 映射承载,并由 FIXME 改名最终消除
- `sdk/` 的角色目录在 `tsconfig.base.json` 中显式映射到各自的 npm 名;在 `dsh-jsonrpc` 完成改名之前,`server/` 的映射仍是过渡性的
- **这次变更放弃了什么:** 功能上一无所失——变更只关乎导航。肌肉记忆和指向旧 GitHub 路径的外部链接会失效;在 pre-release、尚无外部消费者的前提下这可以接受。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md
2026-07-30-followup-enqueue-and-owned-runs.md: e0561f8cfc975dd60e69307b160070c292efff8e
2026-07-30-followup-enqueue-and-owned-runs.zh.md: a807b3ebe47399cae95c38bc2232e20e00e839f5
2026-07-30-followup-enqueue-and-owned-runs.md: 9978b3a8ab8678fe98e000476505cee9dcaa1bc6
2026-07-30-followup-enqueue-and-owned-runs.zh.md: 6501266d32b5ae884a478bfae9e4f1e3d5d85575

View File

@@ -16,7 +16,7 @@ Keep `Agent.followup(message): void` as an enqueue-only operation. `Agent.whenId
The low-level SDK protocol answers `session/prompt` as soon as enqueue succeeds with `{ messageId }`. It streams durable facts through `session.event`, publishes whole-agent transitions through `session.status`, and has no `session.finished`. A low-level client may observe that receipt and later idleness, but receives no prompt result.
High-level automation APIs return a `RunResult` only when they explicitly own an activity interval. The TypeScript and Python SDK `run()` methods collect from the submitted message's durable inbox receipt through the next whole-agent `idle`; their `finalResponse` is the last committed assistant message in that interval, not a response causally attributed to the submitted prompt. The one-shot CLI owns the analogous idle-to-idle interval. An isolated child-agent run may report a result because its caller owns the complete child lifecycle and any steering belongs to that run.
High-level automation APIs return a `RunResult` only when they explicitly own an activity interval. The TypeScript and Python SDK `run()` methods collect from the submitted message's durable inbox receipt through the next whole-agent `idle`; their final response is the last committed assistant message in that interval, not a response causally attributed to the submitted prompt. The Python SDK also reports the last root turn's reason kind as the run-level [`finish_reason`](../bug-fix/2026-08-11-owned-run-finish-reason.md), without attributing it to the submitted prompt. The one-shot CLI owns the analogous idle-to-idle interval. An isolated child-agent run may report a result because its caller owns the complete child lifecycle and any steering belongs to that run.
ACP must return a protocol `stopReason`. Its bridge serializes one in-flight prompt per ACP session, waits for whole-agent idle, and otherwise reports the generic `end_turn`. Token-limit endings are not attributed to the prompt: they settle as `end_turn`. A model error on the prompt's correlated turn does reject the prompt immediately (the error is attributed by its owning turn), and a turnless slot (admission discarded the prompt) settles as `cancelled` at idle alongside explicit ACP cancellation or disposal.
@@ -33,10 +33,10 @@ Goal continuation retains `MessageId` only to recognize its durable queued and a
## Verification
- Agent and inbox tests pin enqueue-only follow-up, durable admission or cancellation, and whole-agent idle observation.
- SDK protocol, TypeScript SDK, and Python SDK tests pin the `{ messageId }` receipt, `session.status`, the absence of `session.finished`, and receipt-to-idle `RunResult` collection without prompt-level `status` or `reason`.
- SDK protocol, TypeScript SDK, and Python SDK tests pin the `{ messageId }` receipt, `session.status`, the absence of `session.finished`, and receipt-to-idle `RunResult` collection without prompt-level `status` or `reason`; Python SDK tests separately pin its run-level `finish_reason` observation.
- ACP, one-shot CLI, goal continuation, and subagent tests pin the distinct activity ownership each integration possesses.
- Consumer tests pin that no production integration derives a follow-up result by correlating `MessageId` with `turn/end`.
## Consequences
An owned activity interval can include steering, injected context, or other work submitted before idleness, so its final response and events are deliberately broader than the initiating message. Prompt-level model error and token-limit classifications disappear from SDK and ACP results; callers that need those facts must inspect the durable event stream without claiming causal attribution. Concurrent automation on one session requires an explicit serialization or ownership policy rather than an implicit per-prompt result.
An owned activity interval can include steering, injected context, or other work submitted before idleness, so its final response, finish reason, and events are deliberately broader than the initiating message. Prompt-level model error and token-limit classifications remain absent from SDK and ACP results; callers may inspect run-level or durable event facts without claiming causal attribution. Concurrent automation on one session requires an explicit serialization or ownership policy rather than an implicit per-prompt result.

View File

@@ -16,7 +16,7 @@ Status: implemented
底层 SDK 协议在入队成功后立即以 `{ messageId }` 响应 `session/prompt`。它通过 `session.event` 流式传输持久事实,通过 `session.status` 发布整个 agent 的状态转换,且不包含 `session.finished`。底层客户端可以观察该回执和之后的 idle但不会收到提示词结果。
只有明确拥有一个活动区间时,高层自动化 API 才返回 `RunResult`。TypeScript 和 Python SDK 的 `run()` 方法从已提交消息的持久 inbox 回执开始收集,直至整个 agent 下一次进入 `idle`;其 `finalResponse` 是该区间内最后一条已提交的 assistant 消息,而不是按因果关系归属于已提交提示词的响应。单次 CLI命令行界面拥有相应的 idle 到 idle 区间。隔离的子 agent 运行可以报告结果,因为调用方拥有完整的子级生命周期,任何 steering 都属于该运行。
只有明确拥有一个活动区间时,高层自动化 API 才返回 `RunResult`。TypeScript 和 Python SDK 的 `run()` 方法从已提交消息的持久 inbox 回执开始收集,直至整个 agent 下一次进入 `idle`;其最终响应是该区间内最后一条已提交的 assistant 消息,而不是按因果关系归属于已提交提示词的响应。Python SDK 还把根会话最后一个轮次的结束原因 kind 作为运行级 [`finish_reason`](../bug-fix/2026-08-11-owned-run-finish-reason.md) 返回,但不会将其归因于已提交的提示词。单次 CLI命令行界面拥有相应的 idle 到 idle 区间。隔离的子 agent 运行可以报告结果,因为调用方拥有完整的子级生命周期,任何 steering 都属于该运行。
ACPAgent Client Protocol必须返回协议规定的 `stopReason`。其桥接层串行处理每个 ACP 会话中唯一一个正在处理的提示词,等待整个 agent 进入 idle其他情况均报告通用的 `end_turn`。token 上限的轮次结束不归因于提示词:它们以 `end_turn` 结算。与该提示词关联的轮次上的模型错误会立即以该错误拒绝提示词(错误按其所属轮次归因),而 turnless 槽位(准入已丢弃提示词)会在 idle 时以 `cancelled` 结算,与显式 ACP 取消或 dispose资源释放并列。
@@ -33,10 +33,10 @@ Goal 续行只保留 `MessageId`,用于识别持久排队和已准入的 goal
## 验证
- Agent 与 inbox 测试固定 follow-up 仅入队、持久准入或取消以及整个 agent 的 idle 观测。
- SDK 协议、TypeScript SDK 和 Python SDK 测试固定 `{ messageId }` 回执、`session.status`、不存在 `session.finished`,以及不含提示词级 `status``reason` 的回执到 idle `RunResult` 收集。
- SDK 协议、TypeScript SDK 和 Python SDK 测试固定 `{ messageId }` 回执、`session.status`、不存在 `session.finished`,以及不含提示词级 `status``reason` 的回执到 idle `RunResult` 收集Python SDK 测试另行固定其运行级 `finish_reason` 观测
- ACP、单次 CLI、goal 续行和 subagent 测试固定各集成实际拥有的不同活动边界。
- 消费方测试固定生产集成都不会通过关联 `MessageId``turn/end` 来推导 follow-up 结果。
## 后果
自有活动区间可以包含进入 idle 前提交的 steering、注入上下文或其他工作因此其最终响应和事件有意比初始消息涵盖更广。SDK 和 ACP 结果不包含提示词级模型错误和 token 上限分类;需要这些事实的调用方必须检查持久事件,但不能声称这些事实具有因果归属。在同一会话上并发执行自动化操作时,必须采用显式串行或所有权策略,不能依赖隐式的按提示词结果。
自有活动区间可以包含进入 idle 前提交的 steering、注入上下文或其他工作因此其最终响应、结束原因和事件有意比初始消息涵盖更广。SDK 和 ACP 结果不包含提示词级模型错误和 token 上限分类;调用方可以检查运行级或持久事件事实,但不能声称这些事实具有因果归属。在同一会话上并发执行自动化操作时,必须采用显式串行或所有权策略,不能依赖隐式的按提示词结果。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-04-declaring-a-provider-from-the-models-page.md
2026-08-04-declaring-a-provider-from-the-models-page.md: 44441da5427dbcd3f0651c2c137a5132ff6d968b
2026-08-04-declaring-a-provider-from-the-models-page.zh.md: 974c4fa5be0893e8fdf6a9142877a8b966708dab
2026-08-04-declaring-a-provider-from-the-models-page.md: 3e7ff9c3bd58b73185c669a224a59cd017ecfb42
2026-08-04-declaring-a-provider-from-the-models-page.zh.md: a4255f8e29b696e84ff3accf636714cdfc0855f1

View File

@@ -22,22 +22,30 @@ Fetching asks about the endpoint **the form currently shows** — a base URL edi
The protocol choices come from the namespace's **own schema**, read through the settings descriptor the page already fetches (`providers.*.api` is a union of the adapter's `supportedProtocols()`). No new wire field, no constant in the client, and no way for the offered choices to drift from the accepted ones.
The editor reaches the two fields a route the directory reports as **declared** names for itself — its display name and that protocol. A create card asking for a field no editor can change leaves that field reachable only through `settings.yaml`, which is the posture this note set out to end. Both render in the fold beside the endpoint, the protocol from the same schema read. Clearing the name unsets it, and what the route falls back to is the layer beneath the one the field edits — a `cordis.yml` may pin a name for a route the catalog does not ship, so the placeholder reads the composition layer and names the route id only when nothing pins one. The protocol has no fallback to clear to. Because an apply can now rename the route, the saved notice names it as the refreshed directory reports it rather than as the target captured when the card opened. A catalog route gets neither: it defaults its name from its catalog entry, and each of its models carries its own protocol, so a route-level one could only override every one of them.
The **Provider ID** is the one create-card field that stays fixed, and not for want of a control. It is the `providers.<route>` dict key, so changing it is a move rather than an edit, and the editor is addressed by the `settingsPath` that move would invalidate. It is referenced from outside this namespace — `agent-default-model` stores a `provider` string, and every `request/header` in every session log already records one — so a rename would silently strip meaning from referents this page cannot see. And it is the stem of the derived credential reference: the page writes keys but can never read one back, so it cannot move `OLD_API_KEY` to `NEW_API_KEY`, leaving a rename to either orphan the stored key or point the profile at a reference under the previous name. Declaring the new route and deleting the old one does all three explicitly, and the page already offers both halves.
## Alternatives considered
**Declare a provider through `ProviderEditor` with extra fields.** One card instead of two, but the editor is addressed by `settingsPath`, and a route being named has no path yet. Recomputing the path per keystroke would remount the card and discard the draft; deferring it would mean the editor's whole write path no longer described what it was editing.
**Add a wire field for the protocol list.** Explicit. But the settings schema already crosses the wire and already contains the union, so a second copy could disagree with the first — and the one the adapter enforces is the schema.
**Let the Provider ID be edited, with the page performing the move.** The card would unset the old key and set the new profile in one `settings.mutate`, and the rest is a rename. But the credential cannot travel with it — the page holds a redacted descriptor, never a value — and the referents in other namespaces and in logged sessions have no rename path at all, so the honest version of this feature is the create-then-delete the page already has.
**Offer the protocol on every pi-ai route, with an inherit choice.** Symmetric with the base URL beside it, and repointing a catalog route at a gateway speaking another wire protocol is a real thing to want. But no consumer asks for it, one wrong pick silently repoints every model on the route, and the inherit choice would be the only way to write a declared route into a profile the adapter refuses. `settings.yaml` still expresses the repoint for a deployment that means it.
**Fetch against the stored profile instead of the live form.** No key would leave the form for an unsaved provider. But the flow that needs fetching most is the one where nothing is stored yet, and a form whose endpoint was edited would quietly interrogate the old one.
**Write adopted candidates straight into the list.** Fewer clicks, but a fetch would then overwrite capacities the user had corrected, and a listing that discloses only ids would replace real numbers with nothing.
## Consequences
A gateway, a self-hosted server, or a model newer than the installed catalog is now configurable without leaving the browser, and the endpoint itself supplies the model ids where it can. The page grew two components and one shared list editor; the editor card's pi-ai fold grew from two fields to a list.
A gateway, a self-hosted server, or a model newer than the installed catalog is now configurable without leaving the browser, and the endpoint itself supplies the model ids where it can. The page grew two components and one shared list editor; the editor card's pi-ai fold grew from two fields to a list, plus a name and a protocol on a declared route.
What it costs: only pi-ai routes can be hand-declared, because `llm-pi-ai` is the one namespace whose profiles describe a whole provider — a `llm-deepseek` route stays a composition fact. Interrogation reaches only OpenAI-compatible endpoints, so a gateway speaking another protocol reports that it cannot be asked and its models are typed in. And the page now holds a key in component state for the duration of a fetch, which is the same exposure `credentials.set` already has and no longer than the card lives.
## Testing
`packages/client/ui-models/tests/provider-form.spec.tsx` drives the rendered page over a scripted wire face: adding, editing, and removing rows; a cleared optional field leaving the profile and a non-integer capacity never entering it; the interrogation carrying the edited endpoint, the unsaved key, and the profile's protocol; the picker's default selection, toggling, cancel, and adopt-keeps-tuned-rows; the empty, refused, and rejected-transport paths; the create writing one profile plus its credential; every gate on the create button; and the read-only posture. `protocolChoices` is covered against a schema that declares the union and one that does not.
`packages/client/ui-models/tests/provider-form.spec.tsx` drives the rendered page over a scripted wire face: adding, editing, and removing rows; a cleared optional field leaving the profile and a non-integer capacity never entering it; the interrogation carrying the edited endpoint, the unsaved key, and the profile's protocol; the picker's default selection, toggling, cancel, and adopt-keeps-tuned-rows; the empty, refused, and rejected-transport paths; the create writing one profile plus its credential; every gate on the create button; and the read-only posture. `protocolChoices` is covered against a schema that declares the union and one that does not. The stylesheet gate reads the package's own sources and fails any `<select>` that takes `.input` without `.selectInput`, because the OS arrow it would otherwise keep sits flush inside the 240px cap `select.input` imposes. The editor's own field inventory is asserted per route kind — a catalog route stops at the key and the endpoint, a declared one also carries the protocol — along with the protocol edit travelling as a single `api` path op, a rename travelling as a single `displayName` one, a cleared name unsetting rather than storing the empty string the adapter refuses, and a declared profile naming no protocol selecting nothing rather than the first choice. `apps/web/tests/models-settings.e2e.ts` reopens the declared route through the real wire, captures the card, and asserts the chosen protocol and the new name both reach `settings.yaml` and the row re-registers under the rename.

View File

@@ -22,22 +22,30 @@ Status: implemented
协议选项来自该 namespace **自己的 schema**,经页面本就会获取的 settings 描述符读出(`providers.*.api` 是适配器 `supportedProtocols()` 的一个 union。没有新增协议字段客户端里没有常量提供的选项也无从与被接受的集合发生漂移。
对于目录报告为**已声明**的路由,编辑器够得着它为自己命名的那两个字段——显示名称与该协议。创建卡片索要一个编辑器改不了的字段,等于把该字段留在只有 `settings.yaml` 才能触及的位置,而那正是本记录要终结的姿态。两者都渲染在折叠区里、紧挨着端点,协议读的是同一份 schema。清空名称即取消设置而路由退回的是该字段所编辑层之下的那一层——`cordis.yml` 可以为目录未提供的路由钉一个名称,因此占位符读组合层,只有没人钉名称时才报路由 id。协议没有可退回的兜底。既然一次保存现在可以改名保存回执便按刷新后的目录来点名这条路由而不是按卡片打开时捕获的 target。内置目录路由两个都不给它的名称由目录条目兜底它的每个模型各自带着自己的协议路由级协议只可能把它们全部覆盖掉。
**Provider ID** 是创建卡片上唯一保持固定的字段,原因不是没做控件。它是 `providers.<route>` 这个字典键,因此改它是一次搬移而非一次编辑,而编辑器正是由那次搬移会作废的 `settingsPath` 寻址的。它还被本 namespace 之外引用——`agent-default-model` 存着一个 `provider` 字符串,每条会话日志里的每个 `request/header` 也都已经记下了一个——因此重命名会悄悄抽空这个页面看不见的那些引用。它同时是派生凭据引用的词干:页面写得了密钥却永远读不回来,因此无法把 `OLD_API_KEY` 搬到 `NEW_API_KEY`,重命名要么让已存密钥成为孤儿,要么让 profile 指向一个仍带旧名的引用。声明新路由再删掉旧的,把这三件事都显式做了一遍,而页面本就提供这两半。
## Alternatives considered
**在 `ProviderEditor` 上加字段来声明提供方。** 两张卡片变一张,但编辑器由 `settingsPath` 寻址,而正在被命名的路由还没有路径。逐次按键重算路径会让卡片重新挂载并丢掉草稿;推迟计算则意味着编辑器的整条写入路径不再描述它正在编辑的东西。
**为协议列表新增一个协议字段。** 显式。但 settings schema 本来就会跨越协议层、本来就含有那个 union因此第二份副本可能与第一份不一致——而适配器强制执行的是 schema 那一份。
**开放 Provider ID 编辑,由页面来完成这次搬移。** 卡片可以在一次 `settings.mutate` 里取消旧键、设置新 profile剩下的就只是改名。但凭据没法跟着走——页面手里只有脱敏描述符从来没有值——而其他 namespace 与已记录会话里的引用根本没有重命名通路,因此这个功能诚实的版本,就是页面已经具备的「先建后删」。
**给每条 pi-ai 路由都提供协议,并附一个「继承」选项。** 与紧挨着的 API 地址对称,而且把内置目录路由指向讲另一种协议的网关确实是有人会想要的事。但目前没有消费方提出这个诉求,一次选错就会静默地把该路由上每个模型都重指,而「继承」选项还会成为把已声明路由写成适配器拒绝的 profile 的唯一途径。真要这么做的部署,`settings.yaml` 仍然表达得了。
**针对已存 profile 而非实时表单发起获取。** 对尚未保存的提供方来说,密钥就不会离开表单。但最需要获取的恰恰是「什么都还没存」的那条流程,而端点已修改的表单会悄悄去询问旧地址。
**把采纳的候选直接写进列表。** 点击更少,但一次获取就会覆盖用户已更正的容量,而只公布 id 的列表会把真实数字替换成空。
## Consequences
网关、自建服务,或比已安装 catalog 更新的模型,如今无需离开浏览器就能配置,而模型 id 在端点能提供时由端点自己给出。页面多了两个组件和一个共用的列表编辑器;编辑卡片的 pi-ai 折叠区从两个字段长成了一个列表。
网关、自建服务,或比已安装 catalog 更新的模型,如今无需离开浏览器就能配置,而模型 id 在端点能提供时由端点自己给出。页面多了两个组件和一个共用的列表编辑器;编辑卡片的 pi-ai 折叠区从两个字段长成了一个列表,已声明路由上还多了一个名称输入框和一个协议选择框
代价是:只有 pi-ai 路由可以手工声明,因为 `llm-pi-ai` 是唯一一个其 profile 描述整个提供方的 namespace——`llm-deepseek` 路由仍是组合面的事实。询问只覆盖 OpenAI 兼容端点,因此讲其他协议的网关会报告自己无法被询问,其模型需手工键入。另外,页面在一次获取期间会把密钥保存在组件状态里,这与 `credentials.set` 已有的暴露面相同,且不长于卡片的存活时间。
## Testing
`packages/client/ui-models/tests/provider-form.spec.tsx` 在脚本化的协议面之上驱动渲染后的页面:添加、编辑与移除行;被清空的可选字段离开 profile、非整数容量从不进入询问携带已修改的端点、未保存的密钥以及 profile 自身的协议;选择框的默认选中、勾选切换、取消,以及「采纳保留已调优的行」;空列表、被拒、传输被拒三条路径;创建写入一份 profile 加其凭据;创建按钮上的每一道门控;以及只读姿态。`protocolChoices` 针对「声明了该 union」与「没有声明」两种 schema 都有覆盖。
`packages/client/ui-models/tests/provider-form.spec.tsx` 在脚本化的协议面之上驱动渲染后的页面:添加、编辑与移除行;被清空的可选字段离开 profile、非整数容量从不进入询问携带已修改的端点、未保存的密钥以及 profile 自身的协议;选择框的默认选中、勾选切换、取消,以及「采纳保留已调优的行」;空列表、被拒、传输被拒三条路径;创建写入一份 profile 加其凭据;创建按钮上的每一道门控;以及只读姿态。`protocolChoices` 针对「声明了该 union」与「没有声明」两种 schema 都有覆盖。样式 gate 读取本包自己的源码,任何只取 `.input` 而不取 `.selectInput``<select>` 都会失败——否则它保留的系统箭头会紧贴 `select.input` 所设 240px 上限的右边缘。编辑器自身的字段清单按路由种类各有断言——内置目录路由止于密钥与端点,已声明路由还带着协议——同时覆盖协议改动只以单条 `api` path op 传出、改名只以单条 `displayName` path op 传出、清空名称是取消设置而不是存入适配器会拒绝的空串,以及不写协议的已声明 profile 什么都不选中、而非选中第一个候选。`apps/web/tests/models-settings.e2e.ts` 经真实协议层重新打开这条已声明路由,捕获该卡片,并断言选定的协议与新名称都抵达了 `settings.yaml`、该行也以新名重新注册。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
2026-08-05-profile-plugin-bundles.md: 54626e3f48a2ba7db19813e6e883f0e77499d0e2
2026-08-05-profile-plugin-bundles.zh.md: 357e0f63d4eba0f0985c9e14aad54595c7b41c77
2026-08-05-profile-plugin-bundles.md: ffef7b67a11617599674e0515e16bfd5283d8445
2026-08-05-profile-plugin-bundles.zh.md: b190d5e834ba3ce8b719a4a3e61f256eb2c0d82b

View File

@@ -22,7 +22,7 @@ Two supporting refactors: the webserver's built-in static dist serving became th
- **Dependency-scan plus partial `patchOrder`** (the original sketch): scanning `dependencies` for bundles and ordering unlisted ones alphabetically has two sources of truth and an implicit tie-break; one explicit ordered `dsh.profile.bundles` list is smaller and fully deterministic. A raw `pnpm add` inside the profile installs a library without activating any patch — explicit, no spooky scan.
- **`link:` entries for in-box bundles**: pnpm cannot version, install, or update a `link:` into the installation, it embeds a machine path in a user file, and it breaks when the installation moves. The two-anchor resolution plus healed symlink fallback gives the same guarantee ("bundles come from the installation") without ceremony.
- **A pre-boot `context` module in the bundle manifest** for boot-time values (dist path, flag facts): rejected in favor of pure plugins — the glue is ordinary rows and app-owned startup services, so the composition stays fully dumpable and the manifest stays data-only. The launcher-owned `ctx.headlessIo` host hook is the one host-provided slot, and it is provided in `boot()`'s `prepare` hook, before any config-tree entry mounts.
- **A pre-boot `context` module in the bundle manifest** for boot-time values (dist path, flag facts): rejected in favor of pure plugins — the glue is ordinary rows and app-owned startup services, so the composition stays fully dumpable and the manifest stays data-only. The launcher-provided host slots (`ctx.cmdlineArgs`, `ctx.appExit`, and the environment snapshot) are provided in `boot()`'s `prepare` hook, before any config-tree entry mounts.
- **Transitive bundle auto-application**: only direct `dsh.profile.bundles` entries contribute layers; a meta-bundle wanting to re-export another bundle's patch must do so explicitly in its own patch file.
## Consequences

View File

@@ -22,7 +22,7 @@ Status: implemented
- **依赖扫描加部分 `patchOrder`**(最初的草案):扫描 `dependencies` 找出组合包、未列出者按字母序排列,会产生两个真源和一条隐式决胜规则;一份显式有序的 `dsh.profile.bundles` 列表更小、完全确定。在 profile 内直接 `pnpm add` 只会安装一个库,不激活任何 patch——行为显式没有暗中扫描。
- **内置组合包使用 `link:` 条目**pnpm 无法对指向安装目录的 `link:` 做版本管理、安装或更新,它会把机器路径嵌进用户文件,并且在安装目录移动后失效。双锚点解析加上每次启动修复的符号链接回退提供了同样的保证(「组合包来自安装目录」),且没有这些繁文缛节。
- **在组合包 manifest 中放一个启动前 `context` 模块**承载启动期取值dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是普通配置行和由应用持有的启动服务,因此组合始终可完整 dumpmanifest 保持纯数据。启动器持有的 `ctx.headlessIo` 宿主钩子是唯一由宿主提供的 slot在任何配置树条目挂载之前,于 `boot()``prepare` 钩子中提供。
- **在组合包 manifest 中放一个启动前 `context` 模块**承载启动期取值dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是普通配置行和由应用持有的启动服务,因此组合始终可完整 dumpmanifest 保持纯数据。启动器提供的宿主 slot`ctx.cmdlineArgs``ctx.appExit` 与环境快照)在任何配置树条目挂载之前,于 `boot()``prepare` 钩子中提供。
- **组合包的传递式自动应用**:只有直接列在 `dsh.profile.bundles` 中的条目才贡献层;想重新导出另一个组合包 patch 的元组合包,必须在自己的 patch 文件中显式完成。
## Consequences

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.md
2026-08-06-app-owned-command-line.md: 4a05cac5ed7f44fb55c2d4498bf28a43befdb073
2026-08-06-app-owned-command-line.zh.md: 86a37f416d17c4615152b29d73f171803f24c4c3
2026-08-06-app-owned-command-line.md: 2480775f654fd5c2fecebc8d59e311acee878920
2026-08-06-app-owned-command-line.zh.md: d754c125d5bc683156f5ac3f285e2cd711e6773b

View File

@@ -16,18 +16,17 @@ The new `@deepseek-ai/dsh-cmdline` package owns the handoff. A launcher calls `p
The boot mounts the composition once. Cordis holds each row until its injections are active; Loader then interpolates that row's `!!js` against the injection-ready plugin context immediately before activation. Include keeps nested row expressions raw until their target row reaches this point. `--help` leaves the provider's service absent, so dependent rows never activate, and a live patch reload interpolates again against the service that remains active, so a served port cannot be silently reset.
The shipped apps moved their flags into their bundles: `dsh-web-app` owns the Web family (and enables the `client-hmr` row it now ships disabled, for `--dev`), and `dsh-headless` owns the task positional and rejects a missing task as a usage error. `apps/cli/src/web.ts` is gone; `runProfile` no longer knows any flag-target row id. Out of tree, turtle-ui gained `--resume <session>` / `--session <id>` the same way, which is the design's real validation: an installed plugin added a flag with no launcher change.
The shipped apps moved their flags into their bundles: `dsh-web-app` owns the Web family, and `dsh-headless` owns the task positional and rejects a missing task as a usage error. `apps/cli/src/web.ts` is gone; `runProfile` no longer knows any flag-target row id. Out of tree, turtle-ui gained `--resume <session>` / `--session <id>` the same way, which is the design's real validation: an installed plugin added a flag with no launcher change.
Two further consequences. Loader mounts sibling rows concurrently, so one row can activate while another still mounts or while the whole boot is rolling back; the Web bundle therefore publishes its URL only after its own Loader tree settles. The Web bundle's runtime plugin owns the harness-source prompt section too, so `dsh web` and `dsh --profile web` boot identically without Web-specific launcher setup.
## Why Loader owns the ordering
Four framework facts shape the mechanism:
Three framework facts shape the mechanism:
- **A profile's rows arrive inside the root include's `patches` option.** Include is an entry-tree owner, so its static entry-config resolver interpolates Include's own options while preserving nested `!!js` nodes for their target rows instead of recursively evaluating them in the Include context.
- **A profile's rows arrive inside the root include's `patches` option.** Include declares the `EntryGroup.key` tree-carrier marker (as Group does), so Loader keeps its config — entry and patch lists, including Include's own `path` — literal instead of recursively evaluating nested `!!js` nodes in the Include context; each expression resolves in its target row's fiber.
- **Cordis activates a fiber only after all declared injections are active.** Immediately before each activation, Cordis runs the `internal/config` waterfall against the fiber's own context; Loader's listener interpolates the raw config after Cordis snapshots its injected services.
- **Provider replacement and HMR must preserve the same contract.** Fiber reactivation re-runs the waterfall, HMR carries the raw config to the replacement fiber, and a pending row accepts option changes without prematurely evaluating expressions against absent services.
- **A row cannot be inserted from inside a mounting plugin** — `tree.create` returns a prefixed id it then fails to resolve — so a conditional row ships `disabled: true` and an active row enables it (`dsh web --dev` and its reload chain). Enablement is an in-memory Loader override rather than an options rewrite, so Include reapplication cannot silently disable it. The Web bundle also starts client discovery only after enabling the optional row, ensuring the first browser graph already contains its HMR receiver.
This leaves dependency ordering in Cordis activation and Loader interpolation, which own it. Rows keep their `inject` and config, Loader mounts the composition once, and the launcher only provides argv and process-lifecycle services.
@@ -43,7 +42,7 @@ This leaves dependency ordering in Cordis activation and Loader interpolation, w
## Consequences
- An app's flags, help text, and usage errors live with the rows they configure; adding a flag to an installed plugin needs no launcher change.
- The launcher still recognizes the headless runner for one-shot process lifetime and the telemetry row for its environment switch; neither path interprets app arguments.
- The launcher recognizes no app row at all: the telemetry row remains its only composition probe (for the environment switch), SIGTERM exits 0 on every surface, every boot watches its user patch layers, and the one-shot runner exits through `ctx.appExit` like any other app.
- `--help` leaves every row that depends on the provider's service pending and requests bounded exit; unrelated rows may activate concurrently before teardown.
- An app-owned service has no statically declared provider: a bundle shipping consumer rows without that provider fails at settlement with pending entries naming the service, not at load.
- A user patch that replaces a row's whole `config` drops its expressions, and with them the flag's precedence for that row.

View File

@@ -16,18 +16,17 @@ profile 落地之后,组合可以安装,命令行却不能。`apps/cli` 仍
boot 只挂载一次整套组合。Cordis 让每一行等待其注入激活Loader 随后在激活前一刻,基于已注入就绪的插件上下文插值该行的 `!!js`。Include 会保留嵌套的行表达式,直到目标行到达这一时点。`--help` 会让提供方服务保持缺失,因此依赖行永不激活;活动 patch 重载会针对仍然在线的服务再次插值,所以已经服务中的端口不会被悄悄重置。
已交付的各应用把自己的 flag 搬进了组合包:`dsh-web-app` 持有 Web 家族(并为 `--dev` 启用它如今以禁用状态交付的 `client-hmr` 行)`dsh-headless` 持有任务位置参数,缺少任务时按用法错误拒绝。`apps/cli/src/web.ts` 已删除;`runProfile` 不再知道任何 flag 目标行 id。在树外turtle-ui 以同样的方式获得了 `--resume <session>` / `--session <id>`,这才是这套设计的真正验证:一个已安装的插件加上了一个 flag启动器毫无改动。
已交付的各应用把自己的 flag 搬进了组合包:`dsh-web-app` 持有 Web 家族,`dsh-headless` 持有任务位置参数,缺少任务时按用法错误拒绝。`apps/cli/src/web.ts` 已删除;`runProfile` 不再知道任何 flag 目标行 id。在树外turtle-ui 以同样的方式获得了 `--resume <session>` / `--session <id>`,这才是这套设计的真正验证:一个已安装的插件加上了一个 flag启动器毫无改动。
还有两条后果。Loader 会并发挂载兄弟行,因此一行可能已经激活,而另一行仍在挂载,或整次 boot 正在回滚;所以 Web 组合包只会在自身的 Loader 配置树结算后公布 URL。另外Web 组合包的运行时插件也持有 harness 源码提示词段,因此 `dsh web``dsh --profile web` 无需 Web 专用启动器设置即可按完全相同的方式启动。
## 为什么由 Loader 持有顺序
条框架事实塑造了这套机制:
条框架事实塑造了这套机制:
- **profile 的各行位于根 include 的 `patches` 选项内部。** Include 是条目树所有者,因此它的静态条目配置解析器会插值 Include 自身的选项,同时为目标行保留嵌套的 `!!js` 节点,而不是在 Include 上下文中递归求值
- **profile 的各行位于根 include 的 `patches` 选项内部。** Include 声明了 `EntryGroup.key` 树载体标记(与 Group 相同),因此 Loader 让它的配置——条目与 patch 列表,包括 Include 自己的 `path`——保持字面值,而不是在 Include 上下文中递归求值嵌套的 `!!js` 节点;每个表达式都在其目标行的 fiber 中解析
- **Cordis 只在所有声明的注入都已激活后才激活 fiber。** 每次激活前一刻Cordis 会基于 fiber 自身上下文运行 `internal/config` waterfallCordis 快照注入服务之后Loader 的监听器再插值原始配置。
- **提供方替换与 HMR 必须保持相同契约。** fiber 重新激活时会重跑 waterfallHMR 会把原始配置带给替换 fiber而待处理行可以接受选项变更不会针对缺失服务提前求值表达式。
- **不能从正在挂载的插件内部插入一行**——`tree.create` 返回一个带前缀的 id随后它自己解析不出来——因此条件性的行以 `disabled: true` 交付,再由活跃行启用(`dsh web --dev` 及其重载链路)。启用采用 Loader 的内存覆盖而非改写选项,因此 Include 重新应用配置时不会悄然将其禁用。Web 组合包还会在启用可选行之后才启动客户端发现,确保首份浏览器图中已经包含 HMR 接收端。
这样,依赖顺序仍由负责它的 Cordis 激活与 Loader 插值流程处理。各行保留自己的 `inject` 和配置Loader 只挂载一次组合,启动器只提供 argv 与进程生命周期服务。
@@ -43,7 +42,7 @@ boot 只挂载一次整套组合。Cordis 让每一行等待其注入激活Lo
## 后果
- 应用的 flag、help 文本和用法错误与它们所配置的行放在一起;给已安装的插件加一个 flag 不需要改动启动器。
- 启动器仍会识别 headless runner 以管理一次性进程生命周期,并识别 telemetry 行以应用环境开关;两条路径都不解析应用参数
- 启动器完全不识别任何应用行telemetry 行仍是它唯一的组合探测用于环境开关SIGTERM 在所有 surface 上以 0 退出,每次启动都监视用户 patch 层,一次性 runner 像任何应用一样经 `ctx.appExit` 退出
- `--help` 会让所有依赖提供方服务的行保持待处理并请求有边界的退出;无关行可能在拆除前并发激活。
- 应用自有服务没有静态声明的提供方:交付了消费行却缺少对应提供方的组合包会在结算时失败,报出指向该服务的待处理条目,而不是在加载时失败。
- 用户 patch 若整体替换某行的 `config`,会连同其中的表达式一起丢掉,该行上 flag 的优先级也随之消失。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-07-shared-feedback-telemetry-user-id.md
2026-08-07-shared-feedback-telemetry-user-id.md: 6d4020828cb1f2ab3de0328c8959a18a0fcfe6c4
2026-08-07-shared-feedback-telemetry-user-id.zh.md: 892fa0f848d656609885d008ab36e3ebbe09b992
2026-08-07-shared-feedback-telemetry-user-id.md: 5ef487646c94808177e1263b94b8c23a9a045d97
2026-08-07-shared-feedback-telemetry-user-id.zh.md: 1ee433396f83a8caf5e35c9cfd1b4f3c04d6c59f

View File

@@ -14,7 +14,7 @@ The earlier [anonymous-user-id decision](../feature/2026-07-31-telemetry-anonymo
`@deepseek-ai/dsh-user-id` owns `getOrCreateAnonymousUserId()` and the `$DSH_HOME/.userid` storage contract. `session-telemetry-otel` uses the returned id as OpenTelemetry Resource `user.id`; the `/feedback` success acknowledgement reports `Feedback recorded for session {sessionId}` followed by `User: {userId}` on a second line, which keeps both identifiers available through the generic command row's expandable body. Invalid feedback is rejected before resolving the id, so an empty command does not create `.userid`.
The extraction preserves the existing random UUID, home resolution, process memo, exclusive-create concurrency, corruption replacement, and best-effort write semantics. It does not unify the dsh-sdk launcher's separate `telemetry.json` identity.
The extraction preserves the existing random UUID, home resolution, process memo, exclusive-create concurrency, corruption replacement, and best-effort write semantics.
## Alternatives considered
@@ -23,7 +23,6 @@ The extraction preserves the existing random UUID, home resolution, process memo
| Import the helper from `session-telemetry-otel` | Couples feedback to an optional exporter backend and forms a reverse dependency cycle once telemetry exports feedback |
| Duplicate the persistence helper in feedback | Two implementations of one file contract can drift and race with different validation or failure semantics |
| Generate a separate feedback user id | The acknowledgement could not correlate with the OTel Resource and would not satisfy the reporting purpose |
| Move the launcher telemetry id too | The launcher feed is not a consumer of `.userid`; unifying unrelated stores remains out of scope |
## Consequences

Some files were not shown because too many files have changed in this diff Show More