Merge origin/master into worktree/web-plugin-config

Three seams: the tsconfig path map gained a mapping on each side and keeps
both; the event-producer matrix is generated, so it was regenerated rather
than hand-merged row by row.
This commit is contained in:
Yichen Jiang
2026-08-11 18:27:53 +08:00
1460 changed files with 20030 additions and 19474 deletions

View File

@@ -0,0 +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/archived/bug-fix/2026-08-10-web-favicon-dark-mode.md
2026-08-10-web-favicon-dark-mode.md: 22e1d063a72b177e0e5c11f4bcfcbc233a81fdfc
2026-08-10-web-favicon-dark-mode.zh.md: dff90567337c6968220091c16c6cd55cf7dad2d1

View File

@@ -0,0 +1,26 @@
# Agent Note: Web favicon follows the color scheme
Status: implemented
Archived: 2026-08-10
English | [中文](2026-08-10-web-favicon-dark-mode.zh.md)
## Problem
`apps/web/public/favicon.svg` paints the DeepSeek mark solid black (`fill="#000"`), and `index.html` declares only that single SVG icon. Under an OS or browser dark color scheme the tab strip is dark too, so the black mark is effectively invisible. Safari versions before 26 do not render SVG favicons, so their users get no tab icon in any scheme.
## Decision
The favicon stays one file and adapts through the browser's own color-scheme signal: `favicon.svg` embeds `@media (prefers-color-scheme: dark) { path { fill: #fff } }`, switching the mark to white under a dark scheme while the light scheme keeps black. `index.html` and `manifest.webmanifest` also declare a 32×32 PNG fallback (`favicon-32x32.png`, DeepSeek brand blue `#4D6BFE`) that Safari versions before 26 render and that stays visible on both light and dark tab strips, extending the [web-install-manifest decision](../feature/2026-08-06-web-install-manifest.md).
The theme signal is the OS/browser scheme, not the GUI's in-app `dsh.theme` toggle: the favicon lives in browser chrome, whose background follows the browser scheme, so `prefers-color-scheme` is the correct semantic and needs no JavaScript. Known browser quirks — Chromium may not repaint the tab icon until reload after a scheme switch, and Safari versions before 26 ignore the SVG variant — are accepted and the PNG fallback covers the older-Safari case.
## Alternatives considered
- **A second `<link rel="icon" media="(prefers-color-scheme: dark)">` pointing at a separate dark SVG.** Rejected: the same scheme semantics with two files to keep in sync, and no benefit over the in-file media query.
- **A theme-presenter that swaps the icon href on `theme/change`.** Rejected: it would follow the in-app toggle rather than the browser scheme that actually colors the tab strip, and it adds client code and a presenter for a chrome asset.
- **No PNG fallback.** Rejected: Safari versions before 26 never render SVG favicons, so the fallback is the only way those versions get a tab icon at all.
## Consequences
Light scheme still shows the black mark, dark scheme shows white, and Safari versions before 26 show the blue PNG in both. `apps/web/tests/pwa-manifest.e2e.ts` pins the PNG link and its order before the SVG, both manifest icons, the shipped PNG's format and dimensions, and the dark media query inside the shipped SVG. The Chromium repaint quirk remains a browser behavior the app cannot fix.

View File

@@ -0,0 +1,26 @@
# Agent Note: 网页图标随配色方案切换
Status: implemented
Archived: 2026-08-10
[English](2026-08-10-web-favicon-dark-mode.md) | 中文
## 问题
`apps/web/public/favicon.svg` 把 DeepSeek 图标绘制为纯黑色(`fill="#000"`),而 `index.html` 只声明了这一个 SVG 图标。当操作系统或浏览器处于暗色配色方案时标签栏同样是深色黑色图标实际上不可见。Safari 26 之前的版本不渲染 SVG favicon因此这些版本的 Safari 用户无论何种配色方案都看不到标签页图标。
## 决策
favicon 保持单一文件,并通过浏览器自身的配色方案信号自适应:`favicon.svg` 内嵌 `@media (prefers-color-scheme: dark) { path { fill: #fff } }`,在暗色方案下把图标切换为白色,浅色方案保持黑色。`index.html``manifest.webmanifest` 同时声明 32×32 PNG 兜底(`favicon-32x32.png`DeepSeek 品牌蓝 `#4D6BFE`Safari 26 之前的版本会渲染该 PNG且它在浅色与深色标签栏上都清晰可见这是对 [Web 安装 manifest 决策](../feature/2026-08-06-web-install-manifest.md) 的扩展。
主题信号取操作系统/浏览器方案,而不是 GUI 应用内 `dsh.theme` 开关favicon 位于浏览器 chrome 中,其背景跟随浏览器方案,因此 `prefers-color-scheme` 是正确语义,无需任何 JavaScript。已知的浏览器怪癖——Chromium 在切换方案后可能要到刷新页面才重绘标签图标Safari 26 之前的版本忽略 SVG 变体——均被接受,旧版 Safari 场景由 PNG 兜底覆盖。
## 曾考虑的替代方案
- **新增指向独立暗色 SVG 的第二个 `<link rel="icon" media="(prefers-color-scheme: dark)">`。** 不予采纳:语义相同却要多维护一个文件,相比文件内媒体查询没有任何收益。
- **由主题 presenter 在 `theme/change` 时替换图标 href。** 不予采纳:它会跟随应用内开关,而不是真正决定标签栏颜色的浏览器方案,并且为一个 chrome 资源引入客户端代码和 presenter。
- **不提供 PNG 兜底。** 不予采纳Safari 26 之前的版本从不渲染 SVG favicon兜底是这些版本获得标签图标的唯一途径。
## 后果
浅色方案仍显示黑色图标暗色方案显示白色Safari 26 之前的版本两种方案都显示蓝色 PNG。`apps/web/tests/pwa-manifest.e2e.ts` 固定断言 PNG 链接及其位于 SVG 之前的顺序、manifest 中的两个图标、交付 PNG 的格式与尺寸,以及交付 SVG 内部的暗色媒体查询。Chromium 的重绘怪癖仍是浏览器行为,应用无法修复。

View File

@@ -94,6 +94,9 @@
"bug-fix/2026-08-03-tui-long-session-render-costs.i18n.yaml": "sha256:f65f7bf8fc84c7a1f022ee393c8d969c06d9bde8bed3a0206de86fb35b246ac6",
"bug-fix/2026-08-03-tui-long-session-render-costs.md": "sha256:6ecf2ef831f527f361ade18a882d79bc6eccf15cc676d05728e7753f41cde051",
"bug-fix/2026-08-03-tui-long-session-render-costs.zh.md": "sha256:5f44e707b332e13fa06d625212173ea055c1c3c0aee60888435a0ff099ec6037",
"bug-fix/2026-08-10-web-favicon-dark-mode.i18n.yaml": "sha256:859c4399f9a017a68ba89552fdafa05e73c0599d94cee9551c84ea5b749a14f3",
"bug-fix/2026-08-10-web-favicon-dark-mode.md": "sha256:4d17e247abd76ae3aed5fb4e075fd66a2838292f89f7021c82a79fe37ed905e6",
"bug-fix/2026-08-10-web-favicon-dark-mode.zh.md": "sha256:7bbff8a3b7061c127afcc75cd2a8043b02a999b78c0180edd8f7e4807fcfe71d",
"feature/2026-06-14-acp-agent-client-protocol.i18n.yaml": "sha256:006795baa43ae962a8d125cc0f1e9f134bc2ee9fb758b6e7669e3fa0126e1918",
"feature/2026-06-14-acp-agent-client-protocol.md": "sha256:6828c0af74bb3fb96206ca6b21c0e56a000b50e4744aad4bc2c05092f3a5a31b",
"feature/2026-06-14-acp-agent-client-protocol.zh.md": "sha256:ba104e841a1fb84edbd3b6c8119d50445b7785255a7a8d13bb9ac8a2cb4d2e69",
@@ -298,12 +301,18 @@
"process/2026-07-23-browser-demo-gif-recording.i18n.yaml": "sha256:808ccdda39e540645b440e40a2124baed737b98636265d8f6d8cf036a70f0d50",
"process/2026-07-23-browser-demo-gif-recording.md": "sha256:4d3a3dc829c75b66f4f57a6a763b4b9562ce10efb90a19308142f598c5ea8524",
"process/2026-07-23-browser-demo-gif-recording.zh.md": "sha256:409e5d31ea87f35c5227fcd1bd105167f580a8a24d29a223ce390b29eb31639a",
"process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml": "sha256:cbd5c32f2997713339699f4c12876b1950895fcb21594927c7509e3cb4c5b8d5",
"process/2026-07-23-personal-staging-maintenance-skills.md": "sha256:a1ef960ddc47c8bc14578d8432ed0ac4272e31178937780989e4e3b3e3f4d49f",
"process/2026-07-23-personal-staging-maintenance-skills.zh.md": "sha256:c0f3fcf0914af88cafcc7e094a2131bd1f764003defc6eaa446b743319e73ec1",
"process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.i18n.yaml": "sha256:89b6dc255cb0dd9d97ae6f34f37d185412d4260d7bf2f87d6b25e340f01dc26e",
"process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.md": "sha256:1742e09435ade4a09349c8843eb381e870a2be2d51b74449f08260422c8096b3",
"process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.zh.md": "sha256:220bd53a88617b09ee8970627c46540fa8951724b102acfe296a7ed7d0f7b5fe",
"process/2026-07-27-wine-windows-gates-experiment.i18n.yaml": "sha256:6f4cbc12ee9cddbb297bf7e138ccabcd204f66898a0f7411b1633f03d5a9eab5",
"process/2026-07-27-wine-windows-gates-experiment.md": "sha256:8d37dcdab058098c7de3da1de00ce61bef92bbc8d6ee71add959474c6fb3e936",
"process/2026-07-27-wine-windows-gates-experiment.zh.md": "sha256:77fbf04df36af09e55007a93bd6b22d08ff99869efe8de3e97dac5b4701e0a9e",
"process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml": "sha256:e7d072856cd3df3b717d94647be4ea05db087b114df4b11d68a171677b94c970",
"process/2026-07-31-installer-adopts-existing-checkout.md": "sha256:d97e39d07bde30d534417a406fc6c56ee6765dbc7b671888d17ef2cac9a7c6dc",
"process/2026-07-31-installer-adopts-existing-checkout.zh.md": "sha256:ab445493c1aacf3f8dc091bdbd765ad56c551ae613e0d8156610f0c4614a7431",
"process/2026-08-04-forward-only-pr-issue-status.i18n.yaml": "sha256:af23e203a66a95674154899410e2f420d1d0685dbf856c24cfccdaa547a17925",
"process/2026-08-04-forward-only-pr-issue-status.md": "sha256:2d31077da47d95ab3ddf64d5efc6b1b8fb7c7709d39aca4a825ef9e9d382d501",
"process/2026-08-04-forward-only-pr-issue-status.zh.md": "sha256:b61f865b7a8a0ac901250a3edbb92ea73177067c4c25448c7088925c2caeccd7",

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/process/2026-07-23-personal-staging-maintenance-skills.md
2026-07-23-personal-staging-maintenance-skills.md: a7ccc5b1e0f13e880c58a93d2e4c2cd4f06e2a93
2026-07-23-personal-staging-maintenance-skills.zh.md: 8593291f510352429cbb29679e7d033589312e41
2026-07-23-personal-staging-maintenance-skills.md: 86654be1afe986107e7dae4440b497a266636012
2026-07-23-personal-staging-maintenance-skills.zh.md: 7593a2e8cafc93df02c9418c94f84bbd3f154a0c

View File

@@ -1,6 +1,7 @@
# Agent Note: Personal staging maintenance skills
Status: implemented
Archived: 2026-08-10
English | [中文](2026-07-23-personal-staging-maintenance-skills.zh.md)

View File

@@ -1,6 +1,7 @@
# Agent Note: 个人集成分支维护 skill
Status: implemented
Archived: 2026-08-10
[English](2026-07-23-personal-staging-maintenance-skills.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/process/2026-07-31-installer-adopts-existing-checkout.md
2026-07-31-installer-adopts-existing-checkout.md: 1d45a14273095610e3ed8047da2ce9f4ca95cbb6
2026-07-31-installer-adopts-existing-checkout.zh.md: 971bc3b389c341b314872b8e45ab20ebd2ed5b2c
2026-07-31-installer-adopts-existing-checkout.md: 08eec47a1cf1254409df93ab83ddf503b4d7d171
2026-07-31-installer-adopts-existing-checkout.zh.md: dc3cc5ee8dbbe287860ad02f4bdba66afb4fbeaf

View File

@@ -1,6 +1,7 @@
# Agent Note: the installer adopts an existing checkout into the managed layout
Status: implemented
Archived: 2026-08-10
English | [中文](2026-07-31-installer-adopts-existing-checkout.zh.md)

View File

@@ -1,6 +1,7 @@
# Agent Note: 安装器把已有检出接管进受管布局
Status: implemented
Archived: 2026-08-10
[English](2026-07-31-installer-adopts-existing-checkout.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-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-21-mandatory-app-attribution-headers.md
2026-06-21-mandatory-app-attribution-headers.md: 12482ca80d19e5cd1e62b8860865db6cccea61ab
2026-06-21-mandatory-app-attribution-headers.zh.md: 43f356887948b88d34ba41d46ed1cd2f1c89a1e2
2026-06-21-mandatory-app-attribution-headers.md: 39050a53ec76e8c5a6cac4d8e31fa15b992c406e
2026-06-21-mandatory-app-attribution-headers.zh.md: bfd6aa2540022f68cf9f69ccc2c12b3bc0978960

View File

@@ -32,7 +32,7 @@ The provider-neutral identity is owned by `dsh-llm` (`packages/llm/llm/src/attri
- product token for `User-Agent`: `deepseek-harness` (continuity with the pre-Agent Note wire value and the repo/org identity)
- version: read from the owning package's manifest via `createRequire`, never a hand-copied constant
- app URL: `https://github.com/deepseek-ai/deepseek-harness-sdk` - the planned public home, which must exist before release
- app URL: `https://github.com/deepseek-ai/deepseek-harness` - the repository home
The default is mandatory and non-empty. White-label deployments pass their own `AppIdentity` to `attributionHeaders(identity)` - the override hook is the function parameter, with no deployment config plumbing until a consumer needs it - and omission falls back to the harness default rather than suppressing attribution. There is no per-request API for the model, user prompt, session id, cwd, user email, API key owner, or local machine identity to influence these fields.
@@ -77,8 +77,6 @@ The landed contract:
**Providers see that traffic comes from the harness.** That is the point, but it means deployments that previously blended into generic SDK traffic become identifiable. Mitigation: send only static public product data and let forks/white-label deployments pass their own `AppIdentity`.
**The app URL points at a repository that does not exist yet.** `deepseek-ai/deepseek-harness-sdk` is the planned public home; until it is created the URL is a dangling promise that blocks release.
**Header support differs by client library.** The hand-rolled adapter sets headers directly; the pi-ai-backed adapter depends on pi-ai continuing to honor `StreamOptions.headers` (merged last over provider defaults). The wire-level mock-server tests are the guard: if a pi-ai upgrade stops delivering the header, the suite goes red. This is useful pressure on the abstraction: a provider adapter that cannot set mandatory headers cannot fully implement the harness LLM contract.
**OpenRouter rankings do not benefit yet.** `User-Agent` is the correct baseline for provider-neutral HTTP identity, but it will not create OpenRouter app pages or rankings because OpenRouter requires `HTTP-Referer` for that product feature. That is deliberate: public app marketplace participation is a separate product decision, not a prerequisite for mandatory request attribution.

View File

@@ -32,7 +32,7 @@ OpenRouter 应用归属刻意未实现。`HTTP-Referer`、`X-OpenRouter-Title`
- `User-Agent` 的产品 token`deepseek-harness`(与 Agent Note 之前的线路值及仓库/组织身份保持连续性)
- 版本:通过 `createRequire` 从所属包的 manifest元数据清单读取绝不手动复制常量
- 应用 URL`https://github.com/deepseek-ai/deepseek-harness-sdk`——计划中的公开主页,且必须在发布前实际存在
- 应用 URL`https://github.com/deepseek-ai/deepseek-harness`——仓库主页
默认值是强制的且非空。白标部署通过向 `attributionHeaders(identity)` 传入自己的 `AppIdentity` 来覆盖——覆盖钩子就是函数参数,在有消费方需要之前不做部署配置管道——省略时回退到 harness 默认值而非抑制归属。没有逐请求 API 允许模型、用户提示词、会话 id、cwd、用户邮箱、API key 所有者或本地机器身份影响这些字段。
@@ -77,8 +77,6 @@ OpenRouter 应用归属刻意未实现。`HTTP-Referer`、`X-OpenRouter-Title`
**提供方看到流量来自 harness。** 这正是目的,但意味着此前混在通用 SDK 流量中的部署变得可识别。缓解措施:仅发送静态公开产品数据,并允许 fork/白标部署传入自己的 `AppIdentity`
**应用 URL 指向一个尚不存在的仓库。** `deepseek-ai/deepseek-harness-sdk` 是计划中的公开主页;在它创建之前,该 URL 是一个阻塞发布的悬空承诺。
**不同客户端库的头部支持有差异。** 手写适配器直接设置头部;基于 pi-ai 的适配器依赖 pi-ai 继续尊重 `StreamOptions.headers`(最后合并覆盖提供方默认值)。线路级 mock 服务器测试是守卫:如果 pi-ai 升级后不再投递该头部,套件会变红。这对抽象施加了有益的压力:一个无法设置强制头部的提供方适配器不能完整实现 harness 的 LLM 约定。
**OpenRouter 排名尚未受益。** `User-Agent` 是提供方无关的 HTTP 身份的正确基线,但它不会创建 OpenRouter 应用页面或排名,因为 OpenRouter 要求 `HTTP-Referer` 来实现该产品功能。这是有意为之:公开应用市场参与是一个独立的产品决策,不是强制请求归属的前提。

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: 860186294059facf17d1eba38423092acf7a4a7d
2026-07-23-client-plugin-loading-model.zh.md: 68f2e70253485d4e215c981d3b338d5046390247

View File

@@ -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).
@@ -122,9 +122,9 @@ The support boundary, stated honestly. Reload is coarse by design: fresh fiber,
## 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 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.
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

@@ -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)`(丢弃工厂与记录,下次到达即重新加载)。
@@ -122,7 +122,7 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
## Consequences
wire 两侧跑着同一份治理实现;浏览器特有的表面只是一套模块系统一个重载插件。插件包只有一种形态纯度门禁因此覆盖全部插件。依赖边与启动档位都与其所有者——manifest——同住负责组合的 app 只握名册与 `--dev` 开关。各漂移缺陷类被结构性关死:共享清单人肉同步、装载顺序耦合、跨插件 import、名册/档位双重记账。浏览器原生脚本装载使插件网络资源、生成 bundle 与 TypeScript/TSX 源码保持标准映射,模块系统也只保留一个可替换的 `loadBundle` 钩子。
wire 两侧跑着同一份治理实现;浏览器特有层只包含一套模块系统一个重载插件。插件包只有一种形态纯度门禁因此覆盖全部插件。依赖边与启动档位都与其所有者——manifest——同住负责组合的 app 只握名册与 `--dev` 开关。各漂移缺陷类被结构性关死:共享清单人肉同步、装载顺序耦合、跨插件 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.md: a730a14ac29b84c97c3f9c5b122eb907def4d389
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: 4fe315bb7673ba219286b176123ccbbe08f02f0d

View File

@@ -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

@@ -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: ff411e1b387dcd83b04f8c02d5dde3c70a815932
2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 593e194654bb3552ad3cac4534847fba11cf3694
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

@@ -93,7 +93,7 @@ Slot scope is the closed set `root | session-maybe | session`:
- `session-maybe` follows the current session with ADOPTION identity (the only behavior — there is no hold-identity-forever mode): an incarnation born session-less keeps its React instance across the arrival of the FIRST session (the blank shell adopts it — no remount, the DOM survives), and from then on behaves exactly like a strict session entry — switching to a different session remounts, and dropping back to no-session remounts into a fresh blank incarnation that will adopt again. Component-local per-session state therefore clears by construction; state that must survive a switch belongs in session-bound sources (machine, store, hooks). With no session, `sessionId`, the results of `useSession`/`useInput`, and `inputActions` may all be absent. The unkeyed root `SessionMaybeProvider` drives these updates by subscribing to the runtime's atomic `currentProvide` projection — selection moves and provider-roster changes publish through the same source, so a roster change under a stable current id republishes the mounted bundle instead of stranding entries on an obsolete hook/prop schema — while `SessionMaybeProvideInfo` uses the static key map to retain the complete hook/prop shape even with no session; the per-entry adoption bookkeeping (incarnation-counter key) lives in the renderer's `SessionMaybeEntry`.
- `session` guarantees that `sessionId`, every hook source, and every prop exist; each strict entry's error boundary is keyed by `sessionId`, so switching sessions recreates that entry and its session store.
`conversation` is the resident `session-maybe` shell: `ConversationRoot`, HeroShell, the Workspace picker, the root-owned scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no session it renders inert (machine faces absent, `disabled` owner prop), and the same instance — textarea included — goes live when a session appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
`conversation` is the resident `session-maybe` shell: `ConversationRoot`, HeroShell, the Workspace picker, the root-owned scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no session its machine faces and message actions are inert, while the whole dashed card opens the existing Workspace picker by pointer and its read-only textarea does the same through Enter or Space. The same instance — textarea included — goes live when a session appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
- The runtime's first built-in entry: the `'session'` hook — `useSession` itself rides the same mechanism, no special-casing.
- Concurrent discipline: the render plane reads only from the hooks compartment (uSES consistency guarantee); props-compartment callbacks are used only in event-handler space; descriptor resolution is render-safe (idempotent caching, with prune reaping residue from abandoned renders).
@@ -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 |
@@ -132,5 +132,5 @@ Slot scope is the closed set `root | session-maybe | session`:
- Plugins gain session context isomorphic to the host's: per-session state hangs on the actx and mounts/tears down in one piece with the scope fiber, making leaks structurally impossible; two-session isolation is structurally guaranteed by the scope filter.
- The client object layer converges to a wire mirror: session identity, lifecycle, and capability adjudication all defer to the host entity — the input system (the next layer) always faces a session with a real Agent, and providers like slash/skill uniformly address by sessionId directly.
- Blank-session governance takes zero dedicated mechanisms: state rides one derived bit, visibility rides the unified list projection (only the current blank shows, as `New Session`), reclamation rides lazy persistence's existing contract (evaporation on restart), and the ordinary ceiling rides same-Workspace reuse.
- The cost: the id→ctx handoff discipline and provide's Concurrent discipline are conventions rather than type-enforced, pinned by review and tests; fully disabled input while no workspace is picked is an experience cost the product surface accepts (the price of the single state axis).
- The cost: the id→ctx handoff discipline and provide's Concurrent discipline are conventions rather than type-enforced, pinned by review and tests. The single state axis still withholds machine faces until a Session exists; the resident card routes activation to the Workspace picker during that interval ([decision](../feature/2026-08-07-workspace-picker-composer-entry.md)).
- Known gaps: approval/question recovery across prune (TODO); model selection returns in live-mutation shape (the host `selectModel` trio is ready-made, its client consumer not yet built).

View File

@@ -93,7 +93,7 @@ slot scope 是闭集 `root | session-maybe | session`
- `session-maybe` 以**收养adoption身份语义**跟随 current session唯一行为——不存在「永久保持实例」模式空态出生的化身在**第一个** session 到来时保持 React 实例空壳收养它——不重挂DOM 存活);此后行为与严格 session entry 完全一致——切到不同 session 重挂,跌回无 session 也重挂为崭新的空态化身(之后再次收养)。因此组件本地的 per-session 状态**由构造保证**随切换清零;需要活过切换的状态必须住 session 绑定的源machine、store、hooks。无 session 时 `sessionId``useSession`/`useInput` 的选择结果及 `inputActions` 均可缺省。根部无 key 的 `SessionMaybeProvider` 通过订阅 runtime 的原子 `currentProvide` 投影驱动这条更新——选择移动和提供方名册变化经同一 source 发布current id 不变时的名册变化也会重发已挂载 bundle而不是把 entry 困在过期的钩子/prop 形状上——`SessionMaybeProvideInfo` 靠静态键表在无 session 时仍保留完整钩子/prop 形状;逐 entry 的收养记账(化身计数 key住在 renderer 的 `SessionMaybeEntry`
- `session` 保证 `sessionId`、所有钩子 source 与 props 均存在;每个严格 entry 的错误边界以 `sessionId` 为 key切换 session 会重建该 entry 及其 session store。
`conversation``session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、root 持有的 scrollport 与 composer stack以及 overlay chain 的 fallback 外框,在无 session → blank session 的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumbtabaction`conversation.session` 在其内部承载 view ring 与 draft mirror二者共享同一个 session scope chat store。composer bar`conversation.composer.bar`)本身即为 `session-maybe`:无 session 时以惰性态渲染(machine face 缺席、`disabled` owner propsession 出现后同一实例(含 textarea转为 live其余输入 slot 保持严格 `session`在此之前不分发任何条目。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
`conversation``session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、root 持有的 scrollport 与 composer stack以及 overlay chain 的 fallback 外框,在无 session → blank session 的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumbtabaction`conversation.session` 在其内部承载 view ring 与 draft mirror二者共享同一个 session scope chat store。composer bar`conversation.composer.bar`)本身即为 `session-maybe`:无 session 时,其 machine face 和消息操作保持惰性,整张虚线卡片可经指针打开现有 Workspace picker只读 textarea 也可通过 Enter 或 Space 打开。session 出现后同一实例(含 textarea转为 live其余输入 slot 保持严格 `session`在此之前不分发任何条目。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
- 运行时内建第一条:`'session'` 钩子——`useSession` 本身走同一机制,无特判。
- Concurrent 纪律:渲染平面只从 hooks 格读uSES 一致性保证props 格回调只在事件 handler 空间用;描述符解析 render-safe幂等缓存、废弃渲染残留由 prune 收尸)。
@@ -132,5 +132,5 @@ slot scope 是闭集 `root | session-maybe | session`
- 插件获得与 host 同构的会话上下文:逐会话状态挂 actx、随 scope fiber 一次拆装,泄漏结构性不可能;双会话隔离由 scope filter 结构性保证。
- client 对象层收敛为 wire 镜像:会话身份、生命周期、能力判别全部以 host 实体为准——输入体系(下一层)面对的永远是「有真 Agent 的会话」slash/skill 等提供方一律以 sessionId 直接寻址。
- 空会话治理零专用机制:状态靠一个派生位,可见性靠统一列表投影(仅 current blank 以 `New Session` 展示),回收靠 lazy persistence 的既有约定(重启蒸发),常规上限靠同 Workspace 复用。
- 代价id→ctx 换乘纪律、provide 的 Concurrent 纪律都是约定而非类型强制,靠 review 与测试钉住;「未选 workspace」期间输入全禁是产品面接受的体验代价单一状态轴换来的)。
- 代价id→ctx 换乘纪律、provide 的 Concurrent 纪律都是约定而非类型强制,靠 review 与测试钉住。单一状态轴仍会在 Session 存在前隐藏 machine face这段时间内常驻卡片会把激活操作转到 Workspace picker[决策](../feature/2026-08-07-workspace-picker-composer-entry.md))。
- 已知欠账approval/question 跨 prune 恢复TODO模型选择以 live-mutation 形状回归host `selectModel` 三件套现成,其 client 消费方尚未构建)。

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-dsh-source-launch-tsx-esm.md
2026-07-29-dsh-source-launch-tsx-esm.md: 5cf4a227f388a1ac8315594af4e0256864ef17f5
2026-07-29-dsh-source-launch-tsx-esm.zh.md: b5a52b3d01840337c0091310e50d8fac34245519
2026-07-29-dsh-source-launch-tsx-esm.md: b2428602a780f2880f0f803ba59b16a76b39790e
2026-07-29-dsh-source-launch-tsx-esm.zh.md: 866bc8886725788e6e619f9507f5c8ccab63f82d

View File

@@ -14,11 +14,11 @@ Startup latency also mattered: the off-thread `module.register()` hooks worker s
## Decision
The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. `bin/dsh`, the root `dsh`/`demo:tui`/`demo:web` scripts, and the Code Mode TUI overlay use the same vector; `bin/dsh` references the hook and tsconfig by absolute checkout paths (bare `tsx/esm` does not resolve from an arbitrary cwd) and pins `TSX_TSCONFIG_PATH` to the root tsconfig. The CJS hook stays off because the CLI source graph is ESM-only; measured TUI time-to-banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain.
The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. The root `dsh` script completes the repository build, then uses that vector from the repository root. The CJS hook stays off because the CLI source graph is ESM-only; measured runtime launch to the TUI banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain.
`scripts/tspath-loader.ts` and `apps/cli/src/tsconfig-paths-loader.ts` are deleted. With them went the loader's runtime rule of mapping a workspace import only for declared runtime dependencies — tsx applies the `paths` map unconditionally. Declaration completeness now rests on the static gates alone: `verify-cordis-config` for configured bare plugins, and workspace constraints for manifests. (That runtime rule found real bugs: `dsh-plan-mode` and `dsh-tool-tasks` imported `@deepseek-ai/dsh-llm` while declaring it only in devDependencies; since fixed.)
The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`): a keyless piped-stdio launch of the exact production vector asserting the non-zero-exit TTY refusal. Any future Node change to module hooks or TypeScript handling turns this gate red instead of breaking developers' `pnpm dsh`.
The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`): a keyless piped-stdio launch of the exact production runtime vector asserting the non-zero-exit TTY refusal. Any future Node change to module hooks or TypeScript handling turns this gate red instead of breaking developers' `pnpm dsh`.
## Alternatives considered
@@ -35,4 +35,4 @@ The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`
- One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line.
- TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode.
- The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures.
- Startup improves ~0.4s over the full tsx default (`demo:headless` now aliases the same `dsh --profile headless` source launch; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path).
- Runtime launch improves ~0.4s over the full tsx default; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path.

View File

@@ -14,11 +14,11 @@ Status: implemented
## 决策
`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。`bin/dsh`、根目录的 `dsh`/`demo:tui`/`demo:web` 脚本以及 Code Mode TUI overlay 使用同一向量;`bin/dsh` 以 checkout 的绝对路径引用钩子与 tsconfig裸的 `tsx/esm` 无法从任意 cwd 解析),并将 `TSX_TSCONFIG_PATH` 固定到根 tsconfig。CJS 钩子保持关闭,因为 CLI 源码图是纯 ESM实测 TUI banner 约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。
`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。根目录的 `dsh` 脚本先完成仓库构建,然后从仓库根目录使用同一启动方式。CJS 钩子保持关闭,因为 CLI(命令行界面)源码图是纯 ESM实测运行时启动至 TUI banner 耗时约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。
`scripts/tspath-loader.ts``apps/cli/src/tsconfig-paths-loader.ts` 已删除。随之消失的还有该 loader「仅为已声明运行时依赖映射 workspace import」的运行时规则——tsx 无条件应用 `paths` 映射。声明完整性现在仅由静态门禁保障:配置的裸插件走 `verify-cordis-config`manifest元数据清单走 workspace constraints。该运行时规则确实发现过真实缺陷`dsh-plan-mode``dsh-tool-tasks` 导入 `@deepseek-ai/dsh-llm` 却只声明在 devDependencies后已修复。
node-compat CI 矩阵Node 22.19 与 26新增 `dsh-source-launch-smoke``apps/cli/tests/source-launch.compat.spec.ts`):以精确的生产启动向量做 keyless 管道 stdio 启动,断言非零退出的 TTY 拒绝。未来 Node 对模块钩子或 TypeScript 处理的任何改动都会让该门禁变红,而不是破坏开发者的 `pnpm dsh`
node-compat CI 矩阵Node 22.19 与 26新增 `dsh-source-launch-smoke``apps/cli/tests/source-launch.compat.spec.ts`):以精确的生产运行时启动向量做 keyless 管道 stdio 启动,断言非零退出的 TTY 拒绝。未来 Node 对模块钩子或 TypeScript 处理的任何改动都会让该门禁变红,而不是破坏开发者的 `pnpm dsh`
## 备选方案
@@ -35,4 +35,4 @@ node-compat CI 矩阵Node 22.19 与 26新增 `dsh-source-launch-smoke``
- 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。
- TypeScript 转换重新委托给 tsx/esbuild逆转了前一篇 Agent Note「证明 Node 原生转换可用」的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。
- 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。
- 启动相比完整 tsx 默认形态快约 0.4s`demo:headless` 现为同一条 `dsh --profile headless` 源码启动命令的别名ACP 保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上
- 运行时启动相比完整 tsx 默认形态快约 0.4sACPAgent Client Protocol保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上。

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

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