Merge remote-tracking branch 'origin/master' into fix/web-ui-optimization

This commit is contained in:
imccyu
2026-07-28 15:06:17 +08:00
122 changed files with 1735 additions and 629 deletions

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
2026-07-22-slot-type-chain-implementation.md: 617524475f3da8af5d281efcfe8f79d500f31be8
2026-07-22-slot-type-chain-implementation.zh.md: 52edea30acea5989b3438cbcf4688df5a897f099
# 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: e88361701fc05c1ab30174dde147ae9558265ce6
2026-07-22-slot-type-chain-implementation.zh.md: 90473861c199f326f4b3635580c885517f0d612b

View File

@@ -45,7 +45,7 @@ Parity rule: **the declaring entry holds the exclusive right to render its child
| runtime | `PropsRuntime<K>` | SlotMap entry for K | `OwnerOf<K>` (render-site params) + session-scope standard `useSession`/`sessionId` + global `useSessions`/`useWorkspaces` |
| child render | `PropsRenderSlots<S>` | register's `children` keys | `renderSlot(key, owner)`, key statically narrowed to S; chain keys add `renderSlotChain` |
| store | `PropsStore<H>` | store factory return type | `useStore` selector hook + `actions.*` (draft-param stripped) |
| business | `I` | inject return type | plain data + callbacks (hooks banned) |
| business | `I` | inject return type | plain data + callbacks; a reserved `hooks` compartment of bare observables arrives bound as `use<Name>` selector hooks (`InjectFace<I>`) |
`sessionId` is framework-supplied wherever `scope: 'session'` is declared — owner params do not carry it. The register call site is the double-lock choke point: a component whose renderSlot keys exceed the `children` declaration, or that misses a declared face, or whose store/inject shapes drift, is a compile error on that line. Delegation is ordinary props passing (hand the `renderSlot` function down, optionally behind a narrower signature) — there is no whitelist face object and no minting API.
@@ -80,11 +80,11 @@ Store scope is **derived from the mounting entry's scope** (session slot → one
### inject: the registrant's business face, on its own ctx
An inject factory takes what its declarations earn it — `sessionId` for session slots, bound `actions` when a store is declared, nothing otherwise — and reads services through the **apply closure's own ctx**, so its capability boundary is the plugin's declared `inject` topology (the cordis property proxy applies natively; there is no assembly handle carrying a wider ctx). Its return value is plain data and callbacks only: the narrowed read/write face of the plugin's own services, cross-service orchestration (e.g. `send` = `actions.clearDraft()` + `ctx.conversation.send(...)`), and per-(entry×session) assembly side effects. No hooks, no ReactNode producers, no whole-service objects — narrowing is the value: what a component can do is exactly the factory's return shape.
An inject factory takes what its declarations earn it — `sessionId` for session slots, bound `actions` when a store is declared, nothing otherwise — and reads services through the **apply closure's own ctx**, so its capability boundary is the plugin's declared `inject` topology (the cordis property proxy applies natively; there is no assembly handle carrying a wider ctx). Its return value is plain data and callbacks, plus at most the reserved `hooks` compartment: a map of bare observable sources (getSnapshot+subscribe) the renderer binds into `use<Name>` selector hooks before the face reaches the component — the registrant-private twin of the provide channel's hooks compartment, for reactive facts too niche for the global standard kit (composer notices/lexicon, the settings nav rows). Components never receive the raw sources, so business code still contains no subscription machinery. Everything else stays plain: the narrowed read/write face of the plugin's own services, cross-service orchestration (e.g. `send` = `actions.clearDraft()` + `ctx.conversation.send(...)`), and per-(entry×session) assembly side effects. No hand-made hooks, no ReactNode producers, no whole-service objects — narrowing is the value: what a component can do is exactly the factory's return shape.
### Data-boundary discipline
Hooks are framework-made only: `useSession`, `useSessions`, `useWorkspaces`, `useStore`, `renderSlot` are the five seats, implemented once with framework-guaranteed correctness; business code passes plain data and callbacks between parent and child (a component's own behavioral hooks that subscribe to nothing external remain fine). Live data has exactly three channels: what the parent knows travels as owner props at the renderSlot site; what only the component knows is local state; what must be shared across entries or survive remounts is a declared store. Derivation is a pure function over framework-hook data (`useMemo`), never a subscription of its own.
Hooks are framework-made only: `useSession`, `useSessions`, `useWorkspaces`, `useStore`, `renderSlot` plus the hooks bound from provide contributions and inject `hooks` compartments — every one synthesized by the renderer's single binding machinery; business code passes plain data and callbacks between parent and child (a component's own behavioral hooks that subscribe to nothing external remain fine). Live data has exactly three channels: what the parent knows travels as owner props at the renderSlot site; what only the component knows is local state; what must be shared across entries or survive remounts is a declared store. Derivation is a pure function over framework-hook data (`useMemo`), never a subscription of its own.
### Tree context and the renderer seam
@@ -111,7 +111,7 @@ Render authority is enforceable rather than conventional: who renders what is a
| 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 |
| 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-defined hooks via inject | Every plugin becomes its own subscription machine; the framework store seat carries the same data with one audited machine |
| 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 |
| `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) |

View File

@@ -45,7 +45,7 @@ ctx.slots.register({
| 运行时 | `PropsRuntime<K>` | K 对应的 SlotMap entry | `OwnerOf<K>`(渲染现场传参)+ session scope 标配 `useSession`/`sessionId` + 全局 `useSessions`/`useWorkspaces` |
| 子坑渲染 | `PropsRenderSlots<S>` | register 的 `children` 键集 | `renderSlot(key, owner)`,键参静态收窄到 Schain 键另有 `renderSlotChain` |
| store | `PropsStore<H>` | store 工厂的返回类型 | `useStore` selector hook + `actions.*`(剥去 draft 形参) |
| 业务 | `I` | inject 的返回类型 | 普通数据+回调(禁 hook |
| 业务 | `I` | inject 的返回类型 | 普通数据+回调;保留键 `hooks` 格的裸 observable 经绑定以 `use<Name>` 选择器 hook 到达(`InjectFace<I>` |
凡声明 `scope: 'session'` 之处,`sessionId` 一律由框架供给——owner 传参不携带它。register 调用点是双向锁的收口:组件的 renderSlot 键集超出 `children` 声明、漏接某个已声明的面、store/inject 形状漂移,任何一条都在那一行上报编译错误。转授就是普通的 props 传递(把 `renderSlot` 函数递下去,可按需包一层更窄的签名)——不存在白名单面对象,也不存在铸面 API。
@@ -80,11 +80,11 @@ store 的 scope **从挂载 entry 的 scope 推导**session 坑→每个会
### inject注册方的业务面立足自己的 ctx
inject 工厂只收其声明挣来的形参——session 坑得 `sessionId`,声明了 store 的得绑定好的 `actions`,否则无参——取服务一律经 **apply 闭包自己的 ctx**,其能力边界因此就是本插件声明的 `inject` 拓扑cordis property proxy 原生生效;不存在携带更宽 ctx 的装配句柄)。返回值只含普通数据与回调:本插件自有服务的收窄读写面、跨服务编排(如 `send` = `actions.clearDraft()` + `ctx.conversation.send(...)`)、以及 per-(entry×session) 的装配副作用。禁 hook、禁 ReactNode 生产者、禁递整个服务对象——收窄本身就是价值:组件能做什么,恰由工厂返回值的形状圈定。
inject 工厂只收其声明挣来的形参——session 坑得 `sessionId`,声明了 store 的得绑定好的 `actions`,否则无参——取服务一律经 **apply 闭包自己的 ctx**,其能力边界因此就是本插件声明的 `inject` 拓扑cordis property proxy 原生生效;不存在携带更宽 ctx 的装配句柄)。返回值普通数据与回调,至多外加保留键 `hooks` 格:一张裸 observable sourcegetSnapshot+subscribe渲染器在业务面抵达组件前把每个 source 绑成 `use<Name>` 选择器 hook——即 provide 通道 hooks 格的注册方私有孪生供太小众、不该进全局标准件的响应式事实composer 的 notices/lexicon、settings 导航行)取用。组件永远收不到裸 source业务代码因此仍零订阅机械。其余保持普通:本插件自有服务的收窄读写面、跨服务编排(如 `send` = `actions.clearDraft()` + `ctx.conversation.send(...)`)、以及 per-(entry×session) 的装配副作用。禁手造 hook、禁 ReactNode 生产者、禁递整个服务对象——收窄本身就是价值:组件能做什么,恰由工厂返回值的形状圈定。
### 数据界线纪律
hook 只许框架造:`useSession`、`useSessions`、`useWorkspaces`、`useStore`、`renderSlot` 是仅有的五席,各实现一次、正确性由框架担保;业务代码在父子组件之间只传普通数据与回调(组件自用、不订阅任何外部数据源的行为 hook 不在此限)。活数据恰有三条通道:父知道的,作为 owner props 在 renderSlot 现场传入;只有组件自己知道的,是本地 state需要跨 entry 共享或跨重挂载存活的,是声明的 store。派生是对框架 hook 数据做纯函数(`useMemo`),绝不自成一路订阅。
hook 只许框架造:`useSession`、`useSessions`、`useWorkspaces`、`useStore`、`renderSlot` 五席,加上 provide 贡献与 inject `hooks` 格绑出的 hook——全部出自渲染器同一台绑定机械;业务代码在父子组件之间只传普通数据与回调(组件自用、不订阅任何外部数据源的行为 hook 不在此限)。活数据恰有三条通道:父知道的,作为 owner props 在 renderSlot 现场传入;只有组件自己知道的,是本地 state需要跨 entry 共享或跨重挂载存活的,是声明的 store。派生是对框架 hook 数据做纯函数(`useMemo`),绝不自成一路订阅。
### 树上语境与渲染器安装缝
@@ -111,7 +111,7 @@ register 签名里的两条硬化裁定之所以存在,是因为显然的替
| 白名单面对象(`ScopedSlots` + 收窄辅助件) | 白名单已在组件的 props 类型里,面可由机械推导;可铸造的面对象是第三个权威面,且只有运行时校验 |
| 装配句柄把 root ctx 带进 inject | 绕开声明的 inject 拓扑——每个工厂都摸得到每个服务package.json 的依赖声明就此失去意义 |
| `children` 用键数组形 | kind/scope 是运行时分派数据SlotMap 已被擦除,数组形必然逼出第二个 spec 注册 API——定义 API 复活 |
| 业务经 inject 自定义 hook | 每个插件都变成自己的订阅机械;框架 store 席位用一台受审计的机械承载同样的数据 |
| 业务手造 hook / 组件 props 里递裸 observable | 每个插件都变成自己的订阅机械inject `hooks` 格让同样的事实走那一台受审计的绑定机械 |
| 模块级 store 句柄 | 模块级句柄是跨插件重载与跨测试用例的单例;工厂形把身份圈定在单次 apply/测试调用内 |
| 组件直收 store 实例 | 渲染代码里能用 `update`/`set`,变更面就无从审计;声明的 actions 让「什么能变」保持为 register 现场的事实 |
| 注册位用 `FC` / 从组件推断 `I` | FC 静态位产生协变噪音、拒绝合法组件;组件侧推断静默吸收 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-25-web-client-session-scope-and-provide-channel.md
2026-07-25-web-client-session-scope-and-provide-channel.md: 09afe6d9e879ae7529d309c3b5e656be849fa543
2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 4d45d74c2e7c34601a5229fc0fc0780a23ec6fd5
2026-07-25-web-client-session-scope-and-provide-channel.md: 4496e3786ed4adb6e60dfd5cfad72e989657f649
2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 768dada95aacdb358115d496f45e7fc0eece0151

View File

@@ -90,7 +90,7 @@ The sole provisioning path by which session slot components fetch their own sess
Slot scope is the closed set `root | session-maybe | session`:
- `root` receives only the global standard kit, with no session identity or provisioning.
- `session-maybe` follows the current session, but the component instance does not change key when the id appears, disappears, or changes; with no session, `sessionId`, the results of `useSession`/`useInput`, and `inputActions` may all be absent. The unkeyed root `SessionMaybeProvider` drives these updates, while `SessionMaybeProvideInfo` uses the static key map to retain the complete hook/prop shape even with no session.
- `session-maybe` follows the current session, but the component instance does not change key when the id appears, disappears, or changes; 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.
- `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 composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch; `conversation.session` carries only the strict-session header/view, while the composer and every input slot also stay strict `session`. With no session, the composer stack places the presentation-only `DisabledInputBar` directly; once a session appears, the input body is swapped for the strictly bound InputBar; the textarea may be rebuilt, while the Hero and the layout skeleton are not. The blank → engaging/active transition stays inside the same strict-session subtree, and the InputBar is never rebuilt on a phase flip.

View File

@@ -90,7 +90,7 @@ session slot 组件「自己拿 session 数据」的唯一供数路径。插件
slot scope 是闭集 `root | session-maybe | session`
- `root` 只拿全局标准件,不接收 session 身份或供数。
- `session-maybe` 跟随 current session但组件实例不因 id 有无或切换而换 key无 session 时 `sessionId``useSession`/`useInput` 的选择结果及 `inputActions` 均可缺省。根部无 key 的 `SessionMaybeProvider` 驱动这条更新,`SessionMaybeProvideInfo` 靠静态键表在无 session 时仍保留完整 hook/prop 形状。
- `session-maybe` 跟随 current session但组件实例不因 id 有无或切换而换 key无 session 时 `sessionId``useSession`/`useInput` 的选择结果及 `inputActions` 均可缺省。根部无 key 的 `SessionMaybeProvider` 通过订阅 runtime 的原子 `currentProvide` 投影驱动这条更新——选择移动与 provider 名册变化经同一 source 发布current id 不变时的名册变化也会重发已挂载 bundle而不是把 entry 困在过期的 hook/prop 形状上——`SessionMaybeProvideInfo` 靠静态键表在无 session 时仍保留完整 hook/prop 形状。
- `session` 保证 `sessionId`、所有 hook source 与 props 均存在;每个严格 entry 的错误边界以 `sessionId` 为 key切换 session 会重建该 entry 及其 session store。
`conversation``session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、composer stack 与 overlay chain 的 fallback 外框在无 session → blank session 的切换中保持 React 实例;`conversation.session` 只承载严格 session 的 header/viewcomposer 与各输入 slot 也保持严格 `session`。无 session 时 composer stack 直接放纯展示的 `DisabledInputBar`session 出现后把输入体换成严格绑定的 InputBartextarea 允许重建Hero 与布局骨架不重建。blank → engaging/active 仍在同一严格 session subtree 内InputBar 不因 phase 翻转而重建。

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
2026-07-25-web-command-surfaces-and-assembly.md: 5188e8c17b31157b1c03203a8d7ba2d8e6a1496b
2026-07-25-web-command-surfaces-and-assembly.zh.md: 0134cc10cf4f49b7719d6a0dacb239389776d6ed
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md
2026-07-25-web-command-surfaces-and-assembly.md: 4c4a400abab940baebc1699fc15b709419865f0c
2026-07-25-web-command-surfaces-and-assembly.zh.md: c0acd1ecc5998a0ec488a1f13ed99ae4a93a240b

View File

@@ -28,8 +28,8 @@ The pipeline was ready but command knowledge had no landing spot: host-side `ctx
### Reference sources (seeing only projections plus their own apply closures, on the root ctx)
- **ui-skill**: `skill.list({sessionId})` addresses by session (the host resolves the project root from the session header); the directory cache is single-flight keyed by sessionId, prewarmed at birth by the `warm` hook and fully cleared by `connection/reset`. A pick produces a text outcome (the literal `/name ` text, Decision 21); `lexicon` supplies the roster from CatalogFetch's settled snapshot (`undefined` while not warm). No match hook (references never enter command adjudication). Skill references ride ordinary prompts as literal text (outside the command plane; tool-skill unchanged, with the session-prefix directory providing the cooperative association).
- **ui-subagent**: candidates are zero-RPC (the sessions.list snapshot filtered by parentId/running); a pick produces a text outcome (the literal `@name ` text); `lexicon` derives from the same snapshot (the model-side representation awaits its business workstream).
- **ui-skill**: `skill.list({sessionId})` addresses by session (the host resolves the project root from the session header); the directory cache is single-flight keyed by sessionId, prewarmed at birth by the `warm` hook and fully cleared by `connection/reset`. A pick produces a text outcome (the literal `/name ` text, Decision 21); `lexicon` supplies the roster from CatalogFetch's settled snapshot (`undefined` while not warm), and `subscribeLexicon` notifies per-session listeners on settle and on invalidation. No match hook (references never enter command adjudication). Skill references ride ordinary prompts as literal text (outside the command plane; tool-skill unchanged, with the session-prefix directory providing the cooperative association).
- **ui-subagent**: candidates are zero-RPC (the sessions.list snapshot filtered by parentId/running); a pick produces a text outcome (the literal `@name ` text); `lexicon` derives from the same snapshot and `subscribeLexicon` forwards the list store's change feed (the model-side representation awaits its business workstream).
### Fixture command routing and assembly

View File

@@ -28,8 +28,8 @@ Status: implemented
### 引用源(只见投影 + 自家 apply 闭包的 root ctx
- **ui-skill**`skill.list({sessionId})` 按会话寻址host 从会话 header 解析项目根);目录缓存按 sessionId 键控 single-flight`warm` 钩子出生预热、`connection/reset` 全清。pick 产出 text outcome`/name ` 原文,决策 21`lexicon` 从 CatalogFetch 的 settled 快照给名录(未热 `undefined`)。无 match 钩子引用不进命令裁决。skill 引用以原文随普通 prompt 走命令平面之外tool-skill 不变session-prefix 目录提供协作关联)。
- **ui-subagent**:候选零 RPCsessions.list 快照按 parentId/running 过滤pick 产出 text outcome`@name ` 原文);`lexicon` 同快照派生(模型侧表示待业务立项)。
- **ui-skill**`skill.list({sessionId})` 按会话寻址host 从会话 header 解析项目根);目录缓存按 sessionId 键控 single-flight`warm` 钩子出生预热、`connection/reset` 全清。pick 产出 text outcome`/name ` 原文,决策 21`lexicon` 从 CatalogFetch 的 settled 快照给名录(未热 `undefined``subscribeLexicon` 在 settle 与失效时按会话通知监听者。无 match 钩子引用不进命令裁决。skill 引用以原文随普通 prompt 走命令平面之外tool-skill 不变session-prefix 目录提供协作关联)。
- **ui-subagent**:候选零 RPCsessions.list 快照按 parentId/running 过滤pick 产出 text outcome`@name ` 原文);`lexicon` 同快照派生`subscribeLexicon` 转发 list store 的变更通道(模型侧表示待业务立项)。
### fixture 命令路由与装配

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
2026-07-25-web-input-machine-and-slash-pipeline.md: acbd132a5fdb97a4098064aae689dfca604ad4b7
2026-07-25-web-input-machine-and-slash-pipeline.zh.md: 158650a41b47f98037a1b3e610d9294694c55a8c
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md
2026-07-25-web-input-machine-and-slash-pipeline.md: 8cf3be7b3b7579d0c37898a58fb0ab4990fd71bf
2026-07-25-web-input-machine-and-slash-pipeline.zh.md: b1488893558d8b2bf9c104faca968435d46a9640

View File

@@ -81,10 +81,10 @@ A trigger/menu/pick pipeline with zero knowledge of "commands":
skill/@subagent references skip the placeholder + occurrence identity chain — a pick inserts the literal `/name ` `@name ` text straight into the draft, with the chip visual purely derived:
- PickOutcome gains a `{text}` arm; the new scoped bail event `slash/input-insert-text` `{text, span}` (the same contract as the other three: draftRev CAS, returning true ⟺ an actual rewrite); facade.insertText goes through setDraft concatenation — zero machine changes.
- Sources get an optional `lexicon?(session)` hook: a synchronous hot-snapshot name roster, with `undefined` = data not warm — zero decoration, never triggering a fetch (the render path stays synchronous and side-effect-free); the controller aggregates it into the `lexicon()` public surface.
- Sources get an optional `lexicon?(session)` hook: a synchronous hot-snapshot name roster, with `undefined` = data not warm — zero decoration, never triggering a fetch (the render path stays synchronous and side-effect-free); the paired optional `subscribeLexicon?(session, listener)` hook is the invalidation channel for rolls that change after warm (catalog settles, children spawn/exit). The controller aggregates the rolls into its `lexicon` snapshot store (re-polling on each source notification); sources registered after scope birth are warmed and folded in via the service's live-controller broadcast.
- `decorations.scanTextRefs`: a word-boundary scan of the draft (`/name`, `@name` at line start / after whitespace; `x/name` never hits) against the roster; a hit gets the `.textRef` mark (a pure range highlight on the backdrop, same as hlToken); an edit breaking the match shape simply disappears on the next scan.
- Sending is the literal text (no more `<skill>` serialization); on the bubble side MessageItem decorates both shapes (the legacy `<skill>` tag + plain-text tokens).
- The old occurrence/paste/serialize chain stays on disk in full, undeleted (additive; deletion is a separate future cut). Known limitation kept as-is: with the lexicon not warm at paste / cold start there is no decoration — it lights up only after typing `/` opens the menu once.
- The old occurrence/paste/serialize chain stays on disk in full, undeleted (additive; deletion is a separate future cut). Decoration reactivity: InputBar subscribes to the shell's lexicon source (uSES), so a roll that settles after the scope-birth prewarm lights existing draft tokens up without any menu interaction or unrelated re-render.
### Per-session provide contributions and the private keyboard surface

View File

@@ -81,10 +81,10 @@ occurrence 表与 chip 三投影:
skill/@subagent 引用不走占位符 + occurrence 身份链——pick 直接把 `/name ` `@name ` 原文插进 draftchip 视觉纯派生:
- PickOutcome 增 `{text}` arm新 scoped bail 事件 `slash/input-insert-text` `{text, span}`与另三个同契约draftRev CAS、返回 true ⟺ 实际改写facade.insertText 走 setDraft 拼接,机器零改动。
- source 可选 `lexicon?(session)` 钩子:同步热快照名录,`undefined` = 数据未热——零装饰、永不触发 fetch渲染路径保持同步无副作用controller 聚合为 `lexicon()` 公面
- source 可选 `lexicon?(session)` 钩子:同步热快照名录,`undefined` = 数据未热——零装饰、永不触发 fetch渲染路径保持同步无副作用配对的可选 `subscribeLexicon?(session, listener)` 钩子是名录在 warm 之后仍会变化(目录 settle、子代生灭时的失效通道。controller 把各名录聚合进自己的 `lexicon` snapshot store每次 source 通知重拉scope 出生后才注册的 source 由 service 广播给活 controller补 warm 并并入名录
- `decorations.scanTextRefs`:词边界扫描 draft行首/空白后的 `/name``@name``x/name` 永不命中)对照名录,命中即 `.textRef` markbackdrop 纯 range 高亮,同 hlToken编辑破坏匹配形状下次扫描自然消失。
- 发送即原文(不再 `<skill>` 序列化);气泡侧 MessageItem 双形状装饰legacy `<skill>` 标签 + 纯文本 token
- 旧 occurrence/paste/serialize 链全部保留在盘未删additive删除另成将来一刀已知局限维持现状:粘贴/冷启动时 lexicon 未热不装饰,输 `/` 开一次菜单后才亮
- 旧 occurrence/paste/serialize 链全部保留在盘未删additive删除另成将来一刀装饰响应性InputBar 以 uSES 订阅 shell 的 lexicon sourcescope 出生预热后才 settle 的名录会直接点亮已有 draft token无需菜单交互或无关重渲染
### per-session 供数贡献与键盘私面

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/implemented/bug-fix/2026-07-27-stable-snapshot-refresh-volatiles.md
2026-07-27-stable-snapshot-refresh-volatiles.md: e2e951cd9f78b319a701a3e60afba48786633f03
2026-07-27-stable-snapshot-refresh-volatiles.zh.md: 55302b509e28520f90f6cd820e4962be014318cc

View File

@@ -0,0 +1,31 @@
# Agent Note: Stable snapshot refresh volatiles
Status: implemented
English | [中文](2026-07-27-stable-snapshot-refresh-volatiles.zh.md)
## Problem
ACP snapshot comparison normalizes generated UUIDs, cwd aliases, spill locators, embedded event times, and omitted-byte counts, but refresh write-back persisted the fresh raw values. A behaviorally unchanged refresh therefore rewrote fixtures with new randomness or host-specific path spellings even though the comparison contract considered both logs equal.
## Decision
Refresh write-back uses `normalizeSessionLog` as its sole volatile-value authority. It normalizes the original harvested records with the fresh run's ids, cwd, and every cwd alias, while normalizing fixture records with the fixture header context; literal replacements affect only the raw values being written. After existing record alignment, it recursively compares fresh and existing leaves through those normalized records: normalized-equivalent leaves retain the existing raw value, while normalized-distinct leaves retain the fresh semantic value.
Before reuse, the complete logical-record layout must align, apart from the existing packed-chunk and inserted-title equivalences. Normalized-equivalent changed strings form a log-wide bijection: one fresh string maps to exactly one existing string and vice versa, so repeated IDs remain correlated across records. An unexplained record mismatch or conflicting mapping disables normalized string reuse for that log.
Object fields align by key. Array elements align only when all corresponding arrays have the same length; otherwise the fresh array wins. Strings remain atomic leaves. Existing packed-chunk timing alignment and inserted-title handling remain separate because they align logical events rather than values inside one record.
## Alternatives considered
**Use deterministic UUIDs and spill filenames in snapshot deployments.** Replacing production randomness would weaken the security shape under test or require test-only behavior in storage and approval implementations.
**Commit normalized fixtures.** Tokenized session logs would stop being raw replay inputs and would cause a broad fixture migration unrelated to the write-back defect.
**Preserve a whole record when its normalized form is unchanged.** This is simpler but churns a random field whenever another field in the same record changes semantically. Leaf-level preservation keeps those decisions independent.
## Consequences
Repeated refreshes no longer rewrite aligned fixture values solely because the normalizer classifies them as volatile, and new volatile categories added to the normalizer automatically inherit the write-back behavior. Structural ambiguity remains conservative: unmatched records, conflicting string mappings, resized arrays, and strings containing both semantic and volatile changes use fresh values rather than risk reusing misaligned data.
Focused unit coverage pins recursive object/array behavior, correlated IDs, ambiguous-layout fallback, conflicting mappings, fresh cwd aliases, volatile strings, and fresh semantic fields. Keyless refresh coverage proves approval UUIDs, cwd aliases, spill paths, and event-read volatility leave their committed fixtures byte-identical.

View File

@@ -0,0 +1,31 @@
# Agent Note: 稳定快照刷新中的易变值
Status: implemented
[English](2026-07-27-stable-snapshot-refresh-volatiles.md) | 中文
## 问题
ACPAgent Client Protocol快照比较会归一化生成的 UUID、cwd 别名、spill locator、嵌入的事件时间和省略字节数但刷新写回会持久化本次生成的原始值。因此即使比较契约将两份日志视为相等一次行为未发生变化的刷新仍会用新的随机值或宿主特有的路径写法改写 fixture测试前置数据
## 决策
刷新写回以 `normalizeSessionLog` 作为易变值的唯一判定依据。系统使用本次运行的 id、cwd 及全部 cwd 别名归一化原始收集记录,并使用 fixture header 上下文归一化 fixture 记录;字面量替换只影响要写入的原始值。现有记录完成对齐后,系统基于这些归一化记录,递归比较本次生成记录与现有记录的叶节点:归一化后等价的叶节点保留现有原始值,归一化后不同的叶节点则保留本次生成的语义值。
复用前必须确保完整逻辑记录布局对齐,现有的打包分片与插入标题等价情形除外。归一化后等价但发生变化的字符串在整份日志范围内形成双射:一个本次生成的字符串只映射到一个现有字符串,反向亦然,因此跨记录重复出现的 ID 仍保持关联。出现无法解释的记录不匹配或映射冲突时,该日志会停用规范化字符串复用。
对象字段按键对齐。只有所有对应数组长度相同时,才对齐其元素;否则以本次生成的数组为准。字符串始终作为不可拆分的叶节点。现有的打包分片计时对齐与插入标题处理仍保持独立,因为它们对齐的是逻辑事件,而非单条记录内的值。
## 考虑过的替代方案
**在快照部署中使用确定性的 UUID 和 spill 文件名。** 替换生产环境使用的随机性会削弱测试所要验证的安全属性,或者要求存储与审批实现引入仅用于测试的行为。
**提交归一化后的 fixture。** token 化的会话日志将不再是原始回放输入,并会引发与写回缺陷无关的大范围 fixture 迁移。
**当整条记录的归一化形式未变时保留整条记录。** 这种做法更简单,但同一记录中的另一个字段发生语义变化时,也会改写其中的随机字段。按叶节点保留可使这些决策彼此独立。
## 后果
重复刷新不再仅仅因为规范化器将已对齐的 fixture 值归类为易变值,就改写这些值;以后加入规范化器的新易变值类别也会自动继承该写回行为。结构有歧义时仍采取保守策略:记录无法匹配、字符串映射冲突、数组尺寸发生变化,或字符串同时包含语义变化与易变变化时,均使用本次生成的值,避免冒险复用未对齐的数据。
聚焦的单元测试固定了递归处理对象与数组的行为、关联 ID、有歧义布局时的回退、映射冲突、本次运行的 cwd 别名、易变字符串以及本次生成的语义字段。无密钥刷新测试证明,审批 UUID、cwd 别名、spill 路径和事件读取中的易变值不会改变已提交 fixture 的任何字节。

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/feature/2026-07-21-cross-session-references.md
2026-07-21-cross-session-references.md: 18dab5fb85f2201258e2f15c3c069d2668ae80d3
2026-07-21-cross-session-references.zh.md: 44e33ab1ed762b08b6be30d66b82396a5b519c02
2026-07-21-cross-session-references.md: a6c7c3b3e89e032d6d807a25a7661a875f9f7bec
2026-07-21-cross-session-references.zh.md: ef78cd7ecee3e2625b446139d7f22e16d0bf1dfa

View File

@@ -14,7 +14,7 @@ TUI users need to bring relevant work from another conversation into one new mes
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. TUI renders that URI inside `@[label](uri)`; text-only clients may use the same inline mention. Explicit Markdown mentions reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
The service uses `ctx.sessionQuery.readSurface(sessionId)`, which loads one live-preferred corpus observation, folds it with the session package's canonical surface algorithm, and returns a detached header, capture seq, and current nodes. FTS is not a dependency: v1 discovery filters only id and cwd, and future title/body search can replace the candidate layer without changing reference identity or preparation.
The service uses `ctx.sessionQuery.readSurface(sessionId)`, which loads one live-preferred corpus observation, folds it with the session package's canonical surface algorithm, and returns a detached header, capture seq, and current nodes. FTS is not a dependency: discovery matches id, cwd, or the latest folded title, while message bodies remain outside the candidate layer. Non-empty queries batch title observations across the visible corpus with bounded persisted-log concurrency and cancellation; a dedicated title index can replace that discovery path without changing reference identity or preparation.
## Snapshot and projection
@@ -32,7 +32,7 @@ Reference preparation is not a new steering protocol and does not create a turn
## Host adapters
TUI combines session candidates with the existing `@` file provider. Each candidate displays the latest folded session title and falls back to the session id; lookup follows the editor's cancellation signal, and session id, cwd, and mention labels escape external terminal controls while the canonical URI retains the original id. TUI prepares only submissions containing structured mentions, disables duplicate submit while awaiting snapshots, restores failed input, renders the readable direct content as the user message, and renders session-reference source metadata as a compact source list instead of exposing the complete JSON in the terminal.
TUI combines session candidates with the existing `@` file provider. Candidate lookup matches case-insensitive substrings of the session id, cwd, or latest folded title, displays that title, and falls back to the session id when a title observation is absent or fails. Lookup follows the editor's cancellation signal, and session id, cwd, and mention labels escape external terminal controls while the canonical URI retains the original id. TUI prepares only submissions containing structured mentions, disables duplicate submit while awaiting snapshots, restores failed input, renders the readable direct content as the user message, and renders session-reference source metadata as a compact source list instead of exposing the complete JSON in the terminal.
The [automation-only ACP transport](../simplification/2026-07-23-acp-automation-only-protocol.md) deliberately does not mount session-query or session-reference services.
@@ -53,7 +53,7 @@ Each of at most three references is independently capped at 65,536 UTF-8 bytes b
## Verification
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, title-aware candidate ranking, terminal-control escaping, projection exclusions, non-recursive snapshot projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, prompt cancellation against a non-settling storage read, independent per-source byte retention, prompt blocking, admission-time staging, send/steer placement, title isolation, missing capability, and compact TUI replay. A keyless TUI snapshot runs the real agent loop: the source surface replaces old user/assistant history with a compact checkpoint, the target submits a mention, and the captured model request contains a sourced snapshot message followed by the readable current prompt, without either shadowed string.
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, id/cwd/title candidate matching and ranking, failed title-observation fallback, candidate cancellation, terminal-control escaping, projection exclusions, non-recursive snapshot projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, prompt cancellation against a non-settling storage read, independent per-source byte retention, prompt blocking, admission-time staging, send/steer placement, title isolation, missing capability, and compact TUI replay. One keyless terminal snapshot types a title-only substring against an opaque session id and pins the rendered candidate. Another keyless TUI snapshot runs the real agent loop: the source surface replaces old user/assistant history with a compact checkpoint, the target submits a mention, and the captured model request contains a sourced snapshot message followed by the readable current prompt, without either shadowed string.
## Consequences

View File

@@ -14,7 +14,7 @@ TUI 用户需要把另一场对话中的相关工作带入一条新消息,但
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返不会因分隔符产生歧义。TUI 把该 URI 渲染到 `@[label](uri)` 中;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
该服务使用 `ctx.sessionQuery.readSurface(sessionId)`它优先从实时会话加载一次语料观察结果使用会话包的规范表层算法执行折叠并返回与源数据分离的会话头、捕获序号和当前节点。FTS 不是功能依赖:v1 的候选发现只按 id 和 cwd 过滤;未来的标题或正文搜索可以替换候选层,而无需改变引用标识或准备过程。
该服务使用 `ctx.sessionQuery.readSurface(sessionId)`它优先从实时会话加载一次语料观察结果使用会话包的规范表层算法执行折叠并返回与源数据分离的会话头、捕获序号和当前节点。FTS 不是功能依赖:候选发现会匹配 id、cwd 或最新折叠后的标题,而消息主体不进入候选层。非空查询会对可见语料中的标题观察结果执行批处理,以有界并发读取持久化日志,并支持取消;专用标题索引可以替换这条发现路径,而无需改变引用标识或准备过程。
## 快照与投影
@@ -32,7 +32,7 @@ TUI 负责快照/直接消息事务,不扩展通用收件箱记录。在 nex
## 宿主适配器
TUI 把会话候选与现有 `@` 文件提供方组合在一起。每个候选项显示最新折叠后的会话标题,没有标题时回退到 session id。候选查询遵循编辑器的取消信号session id、cwd 和提及标签中的外部终端控制字符会被转义,但规范 URI 仍保留原始 id。TUI 只准备包含结构化提及标记的提交;等待快照时禁用重复提交;失败时恢复输入;它把可读的直接内容渲染为用户消息,并把会话引用来源元数据渲染为精简的来源列表,不在终端中暴露完整 JSON。
TUI 把会话候选与现有 `@` 文件提供方组合在一起。候选查询会对 session id、cwd 或最新折叠后的标题执行不区分大小写的子串匹配,显示该标题,并在没有标题观察结果或标题观察失败时回退到 session id。候选查询遵循编辑器的取消信号session id、cwd 和提及标签中的外部终端控制字符会被转义,但规范 URI 仍保留原始 id。TUI 只准备包含结构化提及标记的提交;等待快照时禁用重复提交;失败时恢复输入;它把可读的直接内容渲染为用户消息,并把会话引用来源元数据渲染为精简的来源列表,不在终端中暴露完整 JSON。
[仅面向自动化的 ACPAgent Client Protocol传输层](../simplification/2026-07-23-acp-automation-only-protocol.md)有意不挂载会话查询或会话引用服务。
@@ -53,7 +53,7 @@ TUI 把会话候选与现有 `@` 文件提供方组合在一起。每个候选
## 验证
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、会考虑标题的候选排序、终端控制字符转义、投影排除规则、快照的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消提示词、逐源独立字节保留、提示词阻止、准入期间的暂存、sendsteer 放置方式、标题隔离、功能缺失和精简的 TUI 回放。无密钥 TUI 快照会运行真实的 agent loop智能体循环源表层用一个压缩检查点替换旧的用户assistant 历史,目标会话提交一个提及标记,捕获到的模型请求包含一条带来源的快照消息,后面跟随可读的当前提示词,并且不包含任一被遮蔽的字符串。
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、按 idcwd标题进行候选匹配与排序、标题观察失败时的回退、候选查询取消、终端控制字符转义、投影排除规则、快照的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消提示词、逐源独立字节保留、提示词阻止、准入期间的暂存、sendsteer 放置方式、标题隔离、功能缺失和精简的 TUI 回放。一个无密钥终端快照会在会话 id 不透明的情况下输入一个只与标题匹配的子串,并固定渲染出的候选项。另一个无密钥 TUI 快照会运行真实的 agent loop智能体循环源表层用一个压缩检查点替换旧的用户assistant 历史,目标会话提交一个提及标记,捕获到的模型请求包含一条带来源的快照消息,后面跟随可读的当前提示词,并且不包含任一被遮蔽的字符串。
## 后果

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/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: a1481bb9e2c3abfc3111dce8a1c38835436e3e13
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 64a55f6aa0cb4b9a4a5efc625ece6a0800217f52
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: 96ffd772810a7908ff452967aa0be0e540bc2d8c
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: a7062bd356eb77c76799a64c980198200d140e8f

View File

@@ -12,8 +12,8 @@ The stdio JSON-RPC serving surface (`@deepseek-ai/dsh-jsonrpc`, the [single-exe
Three packages, layered exactly like the existing Python stack, plus one seam registration:
- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/sdk/sdk-protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-jsonrpc` (which now imports it), and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. One behavioral change: an error response now rejects with `JsonRpcResponseError` carrying the wire `code`/`data` (the Python client already preserved these; the old transport threw a bare `Error` with only the message).
- **`@deepseek-ai/dsh-sdk-client`** (`packages/sdk/sdk-client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Session-tree scoping from `subagent.started` lineage edges is client-side, mirroring `client.py`. Deliberate asymmetries with Python: the launch spec is explicit `command`/`args` (no bundled-runtime resolution — that is a distribution concern with no TS consumer yet); `env` replaces rather than merges (callers own credential policy; `scrubbedParentEnv` from the subprocess seam is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit (the client runs outside any harness context, so it cannot ride `ctx.subprocess`).
- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/sdk/sdk-protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-jsonrpc` (which now imports it), and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. The package root explicitly exports that complete interface and provides no source-module deep imports. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. One behavioral change: an error response now rejects with `JsonRpcResponseError` carrying the wire `code`/`data` (the Python client already preserved these; the old transport threw a bare `Error` with only the message).
- **`@deepseek-ai/dsh-sdk-client`** (`packages/sdk/sdk-client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Its package-root consumer interface explicitly exports both client layers, caller-facing types, and the protocol-owned `JsonRpcResponseError`; source modules, normalization helpers, and the notification producer stay internal. `TurnResult.events` contains only the root session's typed events, while `notifications` retains session ids across the root and descendants discovered from `subagent.started`; session-tree scoping is client-side, mirroring `client.py`. Deliberate asymmetries with Python: the launch spec is explicit `command`/`args` (no bundled-runtime resolution — that is a distribution concern with no TS consumer yet); `env` replaces rather than merges (callers own credential policy; `scrubbedParentEnv` from the subprocess seam is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit (the client runs outside any harness context, so it cannot ride `ctx.subprocess`).
- **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Its `provider`/`model` config feeds the child's `initialize`; `env` is where deployments pass the child's own key and `DSH_CORDIS_CONFIG`.
- **The subagent seam grows `out-of-process.ts`**: the provider-side vocabulary both out-of-process backends share — `NO_START_CAPABILITIES`, timing-bound validation, child cwd resolution (config override, else the delegating parent session's workspace), the never-reject `settleRunResult`, and the `subprocessRunHandle` publication. Process mechanics (spawn, env scrub, tree-scoped teardown) live in the `dsh-subprocess` seam; `subagent-acp` spawns through `ctx.subprocess`, while this backend spawns through the SDK client (the subprocess README's documented exception for SDK-managed transports) and applies the seam's `scrubbedParentEnv()` itself.
@@ -38,10 +38,12 @@ Four tiers, per [testing policy](../../../../docs/testing.md):
**Give the TS SDK bundled-runtime resolution parity with Python.** Python's carrier resolution exists to ship wheels to users without Node. A TypeScript consumer definitionally has Node and (in-repo) the workspace; inventing a distribution story with no consumer violates the require-current-need rule. Deferred until a real npm-distribution consumer appears.
**Export source modules, normalization helpers, and subscription producer operations.** These are implementation seams with no caller need; exposing them would make callers learn how the client validates and distributes wire input. The package roots instead enumerate the supported client and protocol interfaces, and the client re-exports the one protocol error callers must distinguish.
**Reuse `dsh-acp-snapshot`'s `runScenario` for the SDK snapshots.** That harness speaks ACP (`ClientSideConnection`, `InputStep` scripts). The SDK suite's whole point is to drive the *SDK client* as the entry surface; it reuses the normalize/refresh library layer (`normalizeSessionLog`, `refreshFixtureReplacements`, …) and leaves the ACP driver alone.
## Consequences
**Bought**: the SDK runtime protocol now has named, compiler-checked types shared by its server and both client SDKs; TypeScript consumers get the same subprocess-driving capability Python has, with typed errors and structured turn reasons; the subagent seam gains a harness-native out-of-process backend whose children are full peers (own config, persistence, tools) — the recursive-composition story the seam note anticipated; the jsonrpc example finally has snapshot coverage, through the SDK path itself.
**Bought**: the SDK runtime protocol now has named, compiler-checked types shared by its server and both client SDKs; TypeScript consumers get the same subprocess-driving capability Python has, with typed errors, structured turn reasons, and package roots that expose only caller-owned operations; the subagent seam gains a harness-native out-of-process backend whose children are full peers (own config, persistence, tools) — the recursive-composition story the seam note anticipated; the jsonrpc example finally has snapshot coverage, through the SDK path itself.
**Paid**: a third package in the `sdk/` group and a fourth subagent backend to keep current; the SDK backend boots a complete plugin tree per child (heavier per-run than an ACP child; pooling remains future work, same as ACP); the wire still has no cancel method, so both the SDK's `RequestTimeoutError` and the backend's dispose settle locally while the server-side turn runs on until process teardown; fixtures for the snapshot suite were recorded against `deepseek-v4-flash` and re-record on model-behavior drift like every other recorded corpus.

View File

@@ -12,8 +12,8 @@ stdio JSON-RPC 服务表面(`@deepseek-ai/dsh-jsonrpc`,见[单文件可执
三个包,分层与既有 Python 栈完全一致,外加一个接缝注册:
- **`@deepseek-ai/dsh-sdk-protocol`**`packages/sdk/sdk-protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport``dsh-jsonrpc` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result``SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data``JsonRpcResponseError` 拒绝Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。
- **`@deepseek-ai/dsh-sdk-client`**`packages/sdk/sdk-client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(生成、分帧、通知扇出、有类型的错误表面、经共享处置阶梯关闭至静止)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize``run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费者的发行问题);`env` 整体替换而非合并凭据策略归调用方subprocess 接缝的 `scrubbedParentEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`Python 只暴露 `status`);拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出(客户端运行在任何 harness 上下文之外,无法搭乘 `ctx.subprocess`)。
- **`@deepseek-ai/dsh-sdk-protocol`**`packages/sdk/sdk-protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport``dsh-jsonrpc` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result``SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。该包根显式导出这一完整接口,且不提供指向源模块的深层导入。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data``JsonRpcResponseError` 拒绝Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。
- **`@deepseek-ai/dsh-sdk-client`**`packages/sdk/sdk-client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(生成、分帧、通知扇出、有类型的错误表面、经共享处置阶梯关闭至静止)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize``run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。其包根消费方接口显式导出两层客户端、面向调用方的类型,以及协议包所拥有的 `JsonRpcResponseError`;源模块、规范化辅助函数和通知投递端都保留为内部实现。`TurnResult.events` 只包含根会话的类型化事件,而 `notifications` 则保留根会话及从 `subagent.started` 发现的后代各自的会话 id基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费者的发行问题);`env` 整体替换而非合并凭据策略归调用方subprocess 接缝的 `scrubbedParentEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`Python 只暴露 `status`);拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出(客户端运行在任何 harness 上下文之外,无法搭乘 `ctx.subprocess`)。
- **`@deepseek-ai/dsh-subagent-dsh-sdk`**`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,以 `subagent-acp` 的同胞结构组织:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样的经 `onError` 汇把结果压平为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未跑回合就尘埃落定的子进程,都是 `error`)。其 `provider`/`model` 配置喂给子进程的 `initialize``env` 是部署传入子进程自有密钥与 `DSH_CORDIS_CONFIG` 的地方。
- **subagent 接缝增长出 `out-of-process.ts`**:两个进程外后端共享的 provider 侧词汇——`NO_START_CAPABILITIES`、时限校验、子进程 cwd 解析(配置覆盖、否则发起委托的父会话工作区)、绝不拒绝的 `settleRunResult`、以及 `subprocessRunHandle` 发布。进程机制spawn、环境擦除、进程树拆除属于 `dsh-subprocess` 接缝;`subagent-acp``ctx.subprocess` 生成子进程,本后端则经 SDK 客户端生成subprocess README 记载的 SDK 托管传输例外)并自行应用接缝的 `scrubbedParentEnv()`
@@ -38,10 +38,12 @@ stdio JSON-RPC 服务表面(`@deepseek-ai/dsh-jsonrpc`,见[单文件可执
**给 TS SDK 与 Python 对等的捆绑运行时解析。** Python 的载体解析是为了给没有 Node 的用户发 wheel。TypeScript 消费者定义上就有 Node 且(仓库内)有工作区;为不存在的消费者发明发行故事违反"要求当前需求"规则。推迟到真实 npm 发行消费者出现。
**导出源模块、规范化辅助函数和订阅投递端操作。** 这些都是调用方不需要的实现接缝;暴露它们会让调用方不得不理解客户端如何校验与分发线输入。各包根转而枚举受支持的客户端接口与协议接口,客户端则只重新导出调用方必须区分的那一种协议错误。
**复用 `dsh-acp-snapshot` 的 `runScenario` 做 SDK 快照。** 那个 harness 说 ACP`ClientSideConnection``InputStep` 脚本。SDK 套件的全部意义就是以 *SDK 客户端*为入口表面;它复用 normalize/refresh 库层(`normalizeSessionLog``refreshFixtureReplacements`……),不动 ACP 驱动器。
## Consequences
**买到**SDK 运行时协议现在拥有服务器与两个客户端 SDK 共享的、编译器校验的具名类型TypeScript 消费者获得与 Python 相同的子进程驱动能力且带类型化错误与结构化回合原因subagent 接缝获得一个 harness 原生的进程外后端,其子进程是完整对等体(自有配置、持久化、工具)——正是接缝 Note 预期的递归组合故事jsonrpc 示例终于有了快照覆盖,而且走的就是 SDK 路径本身。
**买到**SDK 运行时协议现在拥有服务器与两个客户端 SDK 共享的、编译器校验的具名类型TypeScript 消费者获得与 Python 相同的子进程驱动能力,且带类型化错误与结构化回合原因,包根也只暴露归调用方所有的操作subagent 接缝获得一个 harness 原生的进程外后端,其子进程是完整对等体(自有配置、持久化、工具)——正是接缝 Note 预期的递归组合故事jsonrpc 示例终于有了快照覆盖,而且走的就是 SDK 路径本身。
**付出**`sdk/` 组多了第三个包、subagent 多了第四个要保持最新的后端SDK 后端每个子进程启动完整插件树(单次成本高于 ACP 子进程;池化与 ACP 一样留作未来工作线上仍无取消方法SDK 的 `RequestTimeoutError` 与后端的 dispose 都只在本地定格、服务器侧回合继续跑到进程拆除为止;快照夹具录制于 `deepseek-v4-flash`,与其他录制语料一样随模型行为漂移而重录。

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-27-explicit-change-scope-report.md
2026-07-27-explicit-change-scope-report.md: 2cce567940a142ed1f4699f4dc67322ed69565e9
2026-07-27-explicit-change-scope-report.zh.md: cc08df3ee4f3d681bc4ae8b2b0eab3588dfe1a73
2026-07-27-explicit-change-scope-report.md: ed09ffc44252e1e571d50537b3471cad8d68d8f9
2026-07-27-explicit-change-scope-report.zh.md: e13092395d6708b1ec030d13365853a5eaecba55

View File

@@ -12,13 +12,13 @@ An incorrect range undermines evidence selection because it can omit affected pa
## Decision
The root `change-scope` command requires `--base <ref>`, accepts `--head <ref>` with `HEAD` as the default, and offers a versioned `--json` form. It resolves both inputs to commits with ambiguity detection and requires one merge base before writing output. The report records the repository root without normalizing legal path whitespace, current branch, configured upstream, input refs, resolved base, head, and merge-base commit IDs, plus sorted committed, staged, unstaged, and untracked path sets. Path records are split at raw NUL bytes; the repository root, branch, upstream, and every path are decoded as strict UTF-8. An invalid value aborts the report before output instead of substituting characters or collapsing distinct values.
The root `change-scope` command requires `--base <ref>`, accepts `--head <ref>` with `HEAD` as the default, and writes one versioned JSON report. It resolves both inputs to commits with ambiguity detection and requires one merge base before rendering. The report records the repository root without normalizing legal path whitespace, input refs, resolved base, head, and merge-base commit IDs, plus sorted committed, staged, unstaged, and untracked path sets. Path records are split at raw NUL bytes; the repository root and every path are decoded as strict UTF-8. An invalid value aborts the report instead of substituting characters or collapsing distinct values.
Committed paths compare the resolved merge base with the resolved head. Dirty path sets always describe the current worktree and index, even when `--head` names another commit. Every Git probe disables configured filesystem monitors and optional lock-taking; diff configuration cannot hide submodules or invoke external diff or text-conversion drivers, and rename detection is disabled so both sides of a rename remain visible.
The command never guesses or fetches a base, queries a hosting provider, or selects tests. Each calling workflow verifies current remote or stack state, supplies the base explicitly, and uses the factual report as input to semantic review or evidence selection.
Focused temporary-repository tests cover a fresh branch tracking `origin/master` without a same-name remote, its post-push upstream, a worktree path ending in legal whitespace, a stacked non-master base, every dirty layer, a configured filesystem monitor remaining unexecuted, distinct non-UTF-8 POSIX paths and branch or upstream names failing without partial output, invalid, ambiguous, and non-commit refs, deterministic human/JSON parity, and unchanged refs, index, config, and status after reporting.
Focused temporary-repository tests cover explicit and stacked refs, every dirty layer, legal path whitespace, strict path decoding, inert probes, invalid refs, the deterministic schema, and unchanged refs, index, config, and status after reporting.
## Alternatives considered
@@ -30,9 +30,11 @@ Focused temporary-repository tests cover a fresh branch tracking `origin/master`
**Generate required tests from changed paths.** Paths cannot establish behavior reached through configuration, dynamic loading, subprocesses, workers, built artifacts, or providers. Evidence selection remains judgment under the pre-push workflow.
**Report current branch and upstream and maintain a parallel human renderer.** Callers already verify branch and base state before invocation, no consumer uses those fields, and formatted prose duplicates the JSON schema without improving path completeness.
## Consequences
The explicit input makes an incorrect base possible but visible: both input refs and all three resolved commit IDs appear in either output form. Callers pay the small cost of verifying and fetching the live base before running the command.
The explicit input makes an incorrect base possible but visible: both input refs and all three resolved commit IDs appear in the report. Callers pay the small cost of verifying and fetching the live base before running the command.
The string schema deliberately cannot represent non-UTF-8 path bytes. A repository containing them must rename those paths before it can produce a report, preserving exact scope instead of returning a lossy one.

View File

@@ -12,13 +12,13 @@ Status: implemented
## 决策
根目录的 `change-scope` 命令要求提供 `--base <ref>`,接受可选的 `--head <ref>`(默认为 `HEAD`),并提供带版本号的 `--json` 输出格式。该命令会检测歧义,将两个输入解析为 commit并要求二者恰好有一个合并基点之后才会输出结果。报告记录仓库根目录(不对路径中的合法空白字符作规范化处理)、当前分支、配置的上游、输入引用、解析后的基准、头部与合并基点 commit ID以及排序后的已提交、已暂存、未暂存和未跟踪路径集合。路径记录先按原始 NUL 字节切分;仓库根目录、分支、上游和每条路径都以严格 UTF-8 解码。遇到无效值时,命令会在写出任何结果前失败,不会用替换字符代替无效字节或把不同值合并为一条。
根目录的 `change-scope` 命令要求提供 `--base <ref>`,接受可选的 `--head <ref>`(默认为 `HEAD`),并写出一份带版本号的 JSON 报告。该命令会检测歧义,将两个输入解析为 commit并要求二者恰好有一个合并基点之后才会呈现报告。报告记录仓库根目录(不对路径中的合法空白字符作规范化处理)、输入引用、解析后的基准、头部与合并基点 commit ID以及排序后的已提交、已暂存、未暂存和未跟踪路径集合。路径记录先按原始 NUL 字节切分;仓库根目录和每条路径都以严格 UTF-8 解码。遇到无效值时,报告会中止,不会用替换字符代替无效字节或把不同值合并为一条。
已提交路径由解析后的合并基点与头部之间的比较得出。即使 `--head` 指定其他 commit各类未提交路径集合仍始终描述当前 worktree 与索引。每次 Git 探测都会禁用配置的文件系统监视器和可选加锁diff 配置不能隐藏子模块,也不能调用外部 diff 或文本转换驱动;系统禁用重命名检测,因此重命名前后的路径都会保留在结果中。
该命令从不猜测或获取基准,不查询代码托管提供方,也不选择测试。调用该命令的每个工作流都会验证当前远端或堆叠状态、显式提供基准,并将这份事实报告作为语义评审或证据选择的输入。
聚焦的临时仓库测试覆盖以下情形:新分支跟踪 `origin/master` 但没有同名远端分支;同一分支推送后的上游配置;以合法空白字符结尾的 worktree 路径;堆叠分支以非 master 分支为基准;所有未提交改动层;配置的文件系统监视器不会执行;互异的非 UTF-8 POSIX 路径、分支名或上游名会使报告失败且不产生部分输出;无效、有歧义及不指向 commit 的引用;人类可读输出与 JSON 输出保持确定性一致。测试还确认生成报告前后引用、索引、配置与状态均不发生变化
聚焦的临时仓库测试覆盖显式引用与堆叠引用、所有未提交改动层、合法路径空白、严格路径解码、无副作用的探测、无效引用、确定性 schema以及报告前后不变的引用、索引、配置与状态。
## 考虑过的替代方案
@@ -30,9 +30,11 @@ Status: implemented
**根据变更路径生成必需的测试。** 变更路径无法揭示经由配置、动态加载、子进程、worker、构建产物或提供方触达的行为。pre-push 工作流仍须通过判断来选择证据。
**报告当前分支与上游,并维护并行的人类可读渲染器。** 调用方在调用前已经验证分支和基准状态,没有消费方使用这些字段,而格式化文字只会重复 JSON schema并不能提高路径完整性。
## 结果
显式输入仍可能指定错误的基准,但这种错误是可见的:两种输出格式都会显示输入引用与解析出的三个 commit ID。调用方需要付出少量成本在运行该命令前验证实时基准并从远端获取它。
显式输入仍可能指定错误的基准,但这种错误是可见的:报告中会显示输入引用与解析出的三个 commit ID。调用方需要付出少量成本在运行该命令前验证实时基准并从远端获取它。
字符串 schema 有意不表示非 UTF-8 路径字节。含有这类路径的仓库必须先重命名这些路径才能生成报告,以此保持范围精确,而非返回有损结果。