docs(notes): archive low-value records

This commit is contained in:
Tianyi Cui
2026-08-07 22:52:19 +08:00
parent 1a7ac8fee1
commit db0133b4c7
122 changed files with 272 additions and 369 deletions

View File

@@ -1,6 +0,0 @@
# 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/rejected/architecture/2026-07-29-session-resumed-log-boundary.md
2026-07-29-session-resumed-log-boundary.md: 877b0c780f4c92983d2762243fac4e26d945887a
2026-07-29-session-resumed-log-boundary.zh.md: a6f6ecc0d3f3a349f1a438eec0a84f27f8f649b2

View File

@@ -1,51 +0,0 @@
# Agent Note: Record the resume process boundary in the session log
Status: rejected — the boundary belongs at the seeded-`Session` constructor, which also covers fork and replay; superseded by [the end-seed boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)
English | [中文](2026-07-29-session-resumed-log-boundary.zh.md)
## Problem
A session's durable log gave no evidence that it had changed processes. `session/created`, `session/disposed`, and `session/flush` are cordis runtime signals rather than `SessionEventMap` members, and `agent/session-start` carries a `SessionStartSource` but is emit-only and never logged. Reading a stored log therefore gave no hint that anything had been resumed.
That gap makes one class of question unanswerable. A plugin that owns a standalone open/close pair in the log — compaction's `compact/start``compact/end` is the only one today — must distinguish an unmatched opening marker left by a process that died mid-operation from one an operation is holding right now. Those two states are **byte-identical in stored history**. Without a boundary the owner has to choose between refusing forever (an unmatched marker wedges the operation permanently, and because automatic compaction failure is warn-and-continue the user-visible result is that compaction silently stops working until the context window overflows) and proceeding always (which defeats the point of holding a lock).
The pressure to fix this is immediate: moving `compact/start` to its real time point, before summarization, widens the crash window from a few microseconds of synchronous appends to the length of a whole model call, so orphaned brackets go from rare to routine.
## Proposal
`@deepseek-ai/dsh-session-persistence` declares one log-only `session/resumed` with an empty payload and appends exactly one at the end of every cold load, in the same `commitRepair` batch as any crash-repair closers and positioned after them — so every event before the boundary has a smaller seq and was written by a writer that is no longer tracking this log. Ownership lands narrowly on `loadCore()`, the cold-load path reached by `load()` and by `adopt()`. `loadLiveSnapshot()` appends nothing, and the non-mutating `inspect()`/`readFrom()` reads never write one.
The predicate a bracket owner evaluates is purely a function of the log: an unmatched opening marker with a `session/resumed` after it is stale, and one with no `session/resumed` after it is live.
`time` is `Date.now()` floored at the log's greatest `time`, deliberately unlike the synthetic closers, which reuse the last real event's timestamp so repair output stays a deterministic function of stored history. The wall clock is not monotonic — an NTP step, a VM restore, or a log copied from a machine that was ahead can put it behind events already stored — so the floor keeps every cross-boundary duration non-negative. The floor is durable, because the clamped boundary is stored and joins the log's maximum: one future-dated event pins every later boundary in that log to the same instant until wall time passes it.
**The predicate distinguishes process succession, not concurrent writers.** `load()`'s liveness guard is `ctx.sessions.get(id)`, which only sees sessions live in *this* runtime, and no backend takes a cross-process per-session lock. So process B cold-loading a session A currently owns writes a boundary after A's still-open bracket. A consumer that must tolerate concurrent writers still needs a liveness signal beyond the log.
## Why this was rejected
Two reasons, found while reviewing where the marker belonged.
**It covers no fork.** `sessions.fork()` and a subagent fork child construct a seeded session without touching persistence, so neither gets a boundary. A forked child inherits its parent's prefix verbatim — including an open `compact/start` the parent is still holding — which is the one case where the inherited bracket's owner is demonstrably alive. The predicate was unavailable exactly where it was most needed.
**Minting the marker at load made a read path a durable write.** Every consequence the review surfaced traced to that: a revision bump on every cold load, a `commitRepair` batch on a balanced log with nothing to repair, the durable time floor above, a load that fails against a read-only store, and a marked log after a resume the caller then cancelled. None of these are wrong given the placement; they are the placement's cost.
The successor keeps the problem statement and the concurrent-writer scope limit unchanged, and moves the write to `Session`'s constructor — the single waist all six seeded-start paths pass through, fork included. Because the marker then rides the ordinary seed-persistence path, the whole durable-write surface above disappears.
## Alternatives considered
**Use `Session.firstLiveSeq` as the staleness predicate.** Dismissed here on the grounds that it is documented as deliberately not persisted, so the same stored log yields different answers in different processes and a read-only reader cannot evaluate it at all. That reasoning was sound about the field and wrong about the conclusion: the fix is to persist a projection of it rather than to compute the boundary somewhere else. This is the alternative that became the successor.
**Declare the event in core (`dsh-session`).** Rejected here because "the constructor cannot distinguish resume from fork or replay." That is true and turned out not to matter — the distinction is not needed, since inherited history is dead history in all three cases.
**Teach `interruptedTurnClosers` to close `compact/*`.** Rejected: `compact/*` is plugin-owned vocabulary and core must not know it. Core closes turn, step, and tool boundaries — the relations it owns. The successor keeps this rejection.
**Lazy self-repair: the owner appends a synthetic closing marker when it finds an orphan.** A write inside a read-shaped check, and it needs an invariant exception for a numbered owner whose turn has already closed.
**A merge-extensible repair-contributor registry in core.** The right shape once a second consumer exists; with one consumer today, `packages/AGENTS.md` says not to split a seam preemptively.
**Write the boundary only when repair actually occurred.** Rejected: the predicate must hold for an orderly restart too, where there is nothing to repair. The successor keeps this rejection.
## Related
The cold-session `updatedAt` skew this proposal documented is scoped in [the last-activity-index Agent Note](../../proposed/architecture/2026-07-29-durable-last-activity-index.md). That defect predates this proposal and survives its rejection: it is caused by mtime counting every durable write, not by any one boundary.

View File

@@ -1,51 +0,0 @@
# Agent Note: 在会话日志中记录恢复的进程边界
Status: rejected — 边界应当落在带种子 `Session` 的构造函数上,那里同时覆盖 fork 与回放;由[种子结束边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)取代
[English](2026-07-29-session-resumed-log-boundary.md) | 中文
## Problem
会话的持久日志此前无法证明它换过进程。`session/created``session/disposed``session/flush` 是 cordis 运行时信号,而不是 `SessionEventMap` 成员;`agent/session-start` 虽然携带 `SessionStartSource`,却只用于 emit从不记录。因此读取一份已存储日志得不到任何关于「曾经发生过恢复」的线索。
这一空缺让一类问题无法回答。在日志中拥有独立开始/结束事件对的插件必须区分两种未匹配的起始标记:一种由某个在操作中途死亡的进程留下,另一种正被当前某项操作持有;今天符合这一形态的只有压缩的 `compact/start``compact/end`。这两种状态**在已存储历史中逐字节相同**。没有边界,所有方只能在两种做法之间选择:永远拒绝(一个未匹配的标记会永久卡住该操作,而自动压缩失败采取警告并继续的策略,因此用户可见的结果是压缩静默停止工作,直到上下文窗口溢出),或者始终继续(这让持有锁失去了意义)。
修复它的压力是即刻的:把 `compact/start` 移到摘要生成之前这个真实的时间点,会把崩溃窗口从几微秒的同步追加扩大为一整次模型调用的时长,孤儿括号也就从罕见变为常态。
## Proposal
`@deepseek-ai/dsh-session-persistence` 声明唯一一个纯日志事件 `session/resumed`,其载荷为空,并在每次冷加载结束时恰好追加一条:与崩溃修复产生的 closers 同处一个 `commitRepair` 批次,且排在它们之后。因此,该边界之前的每个事件都有更小的 seq并且都是由一个不再追踪这份日志的写入方写下的。所有权狭窄地落在 `loadCore()`,也就是 `load()``adopt()` 到达的冷加载路径。`loadLiveSnapshot()` 不追加任何内容,非变更性的 `inspect()``readFrom()` 读取也从不写入。
括号所有方求值的谓词纯粹是日志的函数:未匹配的起始标记之后有 `session/resumed` 的就是陈旧的,之后没有的就是存活的。
`time``Date.now()` 并以日志的最大 `time` 为下限,刻意区别于合成 closers——后者复用最后一个真实事件的时间戳以便修复输出始终是已存储历史的确定性函数。挂钟并非单调一次 NTP 跳变、一次虚拟机恢复,或一份从走快的机器上拷来的日志,都可能让它落在已存储事件之后,因此这个下限让跨边界的时长都非负。该下限是持久的,因为被钳制的边界本身会被存储并加入日志的最大值:一个未来时间的事件会把该日志中之后的每个边界都钉在同一时刻,直到挂钟时间越过它。
**该谓词区分的是进程接替,不是并发写入方。** `load()` 的存活性守卫是 `ctx.sessions.get(id)`,它只看到*本*运行时中存活的会话,而且没有任何后端会取跨进程的按会话锁。因此,进程 B 冷加载一个 A 当前拥有的会话时,会在 A 仍然开放的括号之后写入一个边界。必须容忍并发写入方的消费方仍然需要日志之外的存活信号。
## 为什么被否决
两个原因,都是在复审标记应当落在何处时发现的。
**它完全覆盖不到 fork。** `sessions.fork()` 与子代理 fork 子会话在不触及持久化的情况下构造带种子会话因此两者都拿不到边界。fork 子会话会逐字节继承父会话的前缀——包括父会话仍然持有的开放 `compact/start`——而这恰恰是继承括号的所有方明显还活着的唯一情形。谓词偏偏在最需要它的地方不可用。
**在加载时铸造标记,把读取路径变成了持久写入。** 复审暴露出的每一项后果都源于此:每次冷加载都递增 revision、对一份无需修复的平衡日志也要走 `commitRepair`、上文那个持久时间下限、加载在只读存储上会失败,以及调用方随后取消的恢复也已留下标记。这些在该放置方式下都不算错,它们就是该放置方式的成本。
取代方案保留问题陈述与并发写入方的适用范围限制不变,并把写入移到 `Session` 的构造函数——全部六条带种子启动路径(含 fork必经的唯一收窄处。由于标记随后走普通的种子持久化路径上述整个持久写入面就消失了。
## Alternatives considered
**用 `Session.firstLiveSeq` 作为陈旧性谓词。** 此处以「文档明确它有意不做持久化,因此同一份已存储日志在不同进程中会给出不同答案,而只读读取方根本无法对它求值」为理由否决。这个推理对字段本身是成立的,但结论错了:正确的修法是持久化它的一个投影,而不是把边界挪到别处去算。这条替代方案正是后来的取代方案。
**在核心(`dsh-session`)中声明该事件。** 此处以「构造函数无法把恢复与 fork 或回放区分开」为理由否决。这句话是对的,但事实证明它无关紧要——并不需要这种区分,因为在这三种情形下继承历史都是死历史。
**教 `interruptedTurnClosers` 关闭 `compact/*`。** 否决:`compact/*` 是插件所属词汇,核心不得知道它。核心只关闭轮次、步骤和工具边界,也就是它自己拥有的关系。取代方案保留这条否决。
**惰性自修复:所有方发现孤儿时自行追加一条合成的关闭标记。** 这是在一次形似读取的检查中执行写入,而且需要为一个带轮次编号、其轮次却已经关闭的所有方开一个不变式例外。
**在核心中建一个可合并扩展的修复贡献方注册表。** 一旦出现第二个消费方,这就是正确的形状;今天只有一个消费方,而 `packages/AGENTS.md` 要求不要预先拆分 seam。
**仅在确实发生了修复时才写入边界。** 否决:该谓词对有序重启同样必须成立,而那时没有任何东西需要修复。取代方案保留这条否决。
## 相关
本提案记录过的冷会话 `updatedAt` 偏斜,范围界定在[最后活动索引 Agent Note](../../proposed/architecture/2026-07-29-durable-last-activity-index.md)。该缺陷早于本提案存在,并且在本提案被否决后依然存在:它的成因是 mtime 会计入每一次持久写入,而不是某一个边界。

View File

@@ -1,6 +0,0 @@
# 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/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md
2026-07-04-prune-unimplemented-subagent-vocabulary.md: 276e832af695acbcf70103def8b51fb8c6e1033f
2026-07-04-prune-unimplemented-subagent-vocabulary.zh.md: 81b79c77a055f97785c6a96b7b17802878ded623

View File

@@ -1,39 +0,0 @@
# Agent Note: Prune the unimplemented subagent seam vocabulary
Status: rejected — the deferred capability vocabulary (`outputSchema`/`structured`, `toolFilter`, `sendMessage`/`resume`) is intentionally reserved surface: the seam advertises the full intended contract ahead of its implementations by design, so providers and consumers grow into a stable shape rather than re-negotiating it per capability. The consumer-evidence analysis below records the decision-time state.
English | [中文](2026-07-04-prune-unimplemented-subagent-vocabulary.zh.md)
## Problem
The [subagent seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) shipped a two-tier capability design: start-time capability flags checked by the service, and optional runtime methods on `SubagentRun`. Three start-time features and both optional runtime methods have zero implementations and zero callers:
- **`outputSchema`/`structured` and `toolFilter`** (`SubagentCapabilities`, `SubagentStartRequest`, `SubagentResult` in `packages/subagent/subagent/src/types.ts`): at the decision point, every real provider declared `outputSchema: false, toolFilter: false` (`packages/subagent/subagent-spawn/src/index.ts`, `packages/subagent/subagent-fork/src/index.ts`, `packages/subagent/subagent-acp/src/index.ts`); the sole production `ctx.subagents.start` caller (`packages/subagent/tool-subagent/src/index.ts`) built `{ prompt, parent, signal?, agentOptions? }` and structurally could not set either; `structured` appeared only in the scripted test fixture. The service's capability check carried two assert rows whose only exercisers were the rejection tests.
- **`SubagentRun.sendMessage` / `SubagentRun.resume`** (same file): implemented by NO provider — not even the mock; the spawn spec asserts their *absence*.
The only reason `dsh-subagent` depended on `dsh-tools` at the decision point was `outputSchema`'s schema type (now `ObjectJsonSchema`). Three subsequent subagent workstreams (per-session snapshot replay, the fork seed boundary, the ACP backend) landed around this surface without growing a single consumer.
## Proposal
Remove `outputSchema`/`structured`, `toolFilter`, `sendMessage`, and `resume` from the seam; shrink `SubagentCapabilities` to `{ depthLimit }`; drop the two capability-assert rows, the all-false flags on the three providers, the scripted fixture's structured branch and capability knobs, and the tests that exist to pin the removed surface. Drop the `dsh-tools` peer/dev dependency from `packages/subagent/subagent/package.json`. Update the [subagent.md](../../../../docs/core-data-structures/subagent.md) pastes and the type-equiv manifest, plus the affected provider READMEs. The implementing PR amends the seam Agent Note's capability catalog per [implemented/AGENTS.md](../../implemented/AGENTS.md).
**Keep** `depthLimit`/`maxDepth` and capability checks. The in-process backend enforces the limit, although the shipping tool does not yet set it. Recursion is a known seam risk, so the appropriate follow-up is to supply a tool default rather than delete working enforcement.
Adjacent surface examined and deliberately left alone: `SubagentService.getProvider()`/`list()` have test-harness consumers only, but the [prune-dead-seam-methods implementation note](../../archived/simplification/2026-06-20-prune-dead-seam-methods.md) records precisely this shape being removed from the bash executor and reverted — a test harness IS a consumer for a one-line accessor over an already-tracked map. `SubagentRunEndInfo.lastAssistantMessage` is a recorded keep (the [subagent-observe-enrich Agent Note](../../archived/feature/2026-06-30-subagent-observe-enrich.md)'s review dropped `agentType` and kept it deliberately, as the only final-message channel for out-of-process children); its currently-unwired bridge forwarding is a gap to close or a consumer to document, not surface for this Agent Note to cut.
This is the seam-vocabulary echo of [prune dead methods from the persistence seam](../../archived/simplification/2026-06-20-prune-dead-seam-methods.md): members every implementation must declare for nobody — weaker even, since here zero implementations exist.
## Alternatives considered
### Why not keep it?
The two-kinds-of-capability design is the seam Agent Note's headline, and re-adding `outputSchema` later touches several files. But the design survives with `depthLimit` as its live example and the Agent Notes as its record, and the seam Agent Note itself concedes the shipped `toolFilter` shape is wrong (real enforcement needs a `tools/pre-execute` deny in the child's context, not schema filtering) — that deny primitive exists on the interception seams, so re-adding against a real implementing provider will pin a better contract than the current speculative one.
## Acceptance criteria
- The removed spellings appear only in this Agent Note and the amended seam Agent Notes; `SubagentCapabilities` is `{ depthLimit: boolean }`; the `dsh-tools` dependency edge is gone (`hygiene` green).
- Depth-enforcement tests are unchanged and green.
## Risks
The subagent lifecycle events carry `lastAssistantMessage` on the end payload — that enrichment lives in the service module, not the seam vocabulary this Agent Note shrinks, and the observe-enrich Agent Note records dropping an `agentType` sibling for lacking a consumer: the judgment this Agent Note extends. The CC hooks bridge, the first outside consumer of those lifecycle events, reads only the event payloads and touches none of the surface removed here; the observe-enrich Agent Note's deferred control-flow redesign names implementing `resume` as its own future work — exactly the re-add trigger this Agent Note's pattern anticipates.

View File

@@ -1,39 +0,0 @@
# Agent Note: 裁剪未实现的 subagent seam 词汇
Status: rejected — 延后的能力词汇(`outputSchema`/`structured``toolFilter``sendMessage`/`resume`)是有意保留的接口面:该 seam 按设计先于实现声明完整的预期契约,使提供方与消费方沿稳定形状演进,而非针对每项能力重新协商。下方的消费方证据分析记录了决策时的状态。
[English](2026-07-04-prune-unimplemented-subagent-vocabulary.md) | 中文
## 问题
[subagent seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 交付了一套两层能力设计:启动时由服务检查的能力 flag以及 `SubagentRun` 上的可选运行时方法。三个启动时功能和两个可选运行时方法的实现数与调用数均为零:
- **`outputSchema`/`structured``toolFilter`**`SubagentCapabilities``SubagentStartRequest``SubagentResult`,位于 `packages/subagent/subagent/src/types.ts`):在作出决策时,每个真实提供方都声明 `outputSchema: false, toolFilter: false``packages/subagent/subagent-spawn/src/index.ts``packages/subagent/subagent-fork/src/index.ts``packages/subagent/subagent-acp/src/index.ts`);唯一的生产环境 `ctx.subagents.start` 调用方(`packages/subagent/tool-subagent/src/index.ts`)构造 `{ prompt, parent, signal?, agentOptions? }`,结构上无法设置这两个字段;`structured` 仅出现在脚本化测试 fixture测试前置数据中。服务的能力检查包含两行 assert其唯一执行者是拒绝测试。
- **`SubagentRun.sendMessage` / `SubagentRun.resume`**(同一文件):没有任何提供方实现——包括 mock 也没有spawn spec 断言的正是它们的*缺失*。
在作出决策时,`dsh-subagent` 依赖 `dsh-tools` 的唯一原因是 `outputSchema` 的 schema 类型(现为 `ObjectJsonSchema`)。三项后续 subagent 工作按会话快照回放、fork seed 边界、ACPAgent Client Protocol后端都围绕这块接口面落地却连一个消费方都没有产生。
## 提案
从 seam 中移除 `outputSchema`/`structured``toolFilter``sendMessage``resume`;将 `SubagentCapabilities` 缩减为 `{ depthLimit }`;删除两行能力 assert、三个提供方上的 all-false flag、脚本化 fixture 的 structured 分支和能力旋钮,以及为固定被移除接口面而存在的测试。`dsh-tools` 的对等依赖peer dependency和开发依赖应从 `packages/subagent/subagent/package.json` 中删除。更新 [subagent.md](../../../../docs/core-data-structures/subagent.md) 中的粘贴内容与 type-equiv manifest元数据清单以及受影响的提供方 README。实现 PRPull Request按照 [implemented/AGENTS.md](../../implemented/AGENTS.md) 修订 seam Agent Note 的能力目录。
**保留** `depthLimit`/`maxDepth` 与能力检查。进程内后端已强制执行该限制,尽管当前发布的工具尚未设置它。递归是已知的 seam 风险,因此恰当的后续工作是提供一个工具默认值,而非删除正在工作的强制逻辑。
审视过但有意不动的相邻接口面:`SubagentService.getProvider()`/`list()` 仅有测试 harness 消费方,但 [prune-dead-seam-methods 实现说明](../../archived/simplification/2026-06-20-prune-dead-seam-methods.md)恰好记录了这种形态从 bash 执行器中被移除后又被回退的经过——对于一个基于已跟踪 map 的单行访问器而言,测试 harness 就是消费方。`SubagentRunEndInfo.lastAssistantMessage` 是一个已记录的保留项([subagent 观测/丰富化 Agent Note](../../archived/feature/2026-06-30-subagent-observe-enrich.md)的评审删除了 `agentType` 但有意保留了它,因为它是进程外子 agent智能体唯一的最终消息通道它当前未接通的桥接转发是一个待补的缺口或待记录的消费方不是本 Agent Note 要裁剪的接口面。
这是[从持久化 seam 裁剪死方法](../../archived/simplification/2026-06-20-prune-dead-seam-methods.md)在 seam 词汇层面的回响:每个实现都必须声明、却无人使用的成员,甚至更弱,因为这里连一个实现都没有。
## 曾考虑的替代方案
### 为什么不保留?
两类能力的设计是 seam Agent Note 的核心亮点,日后重新添加 `outputSchema` 会涉及多个文件。但该设计以 `depthLimit` 作为活跃示例、以 Agent Note 作为记录仍然成立;而且 seam Agent Note 本身承认已交付的 `toolFilter` 形态是错误的(真正的强制需要在子 agent 上下文中实施 `tools/pre-execute` deny而非 schema 过滤)——该 deny 原语已存在于拦截 seam 上,因此在由真实提供方实现并重新添加时,将确定一份比当前推测性契约更好的契约。
## 验收标准
- 被移除的拼写仅出现在本 Agent Note 和修订后的 seam Agent Note 中;`SubagentCapabilities``{ depthLimit: boolean }``dsh-tools` 依赖边已消除(`hygiene` 绿色)。
- 深度强制测试不变且绿色。
## 风险
subagent 生命周期事件在结束载荷上携带 `lastAssistantMessage`——该增强位于服务模块中,不在本 Agent Note 缩减的 seam 词汇范围内observe-enrich Agent Note 记录了因缺少消费方而删除 `agentType` 兄弟字段的判断,本 Agent Note 延续了这一判断。CC 钩子桥接是这些生命周期事件的第一个外部消费方它只读取事件载荷不涉及本文移除的任何接口面observe-enrich Agent Note 推迟的控制流重设计将实现 `resume` 列为自身的未来工作——恰好是本 Agent Note 模式所预期的重新添加触发点。

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/rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.md
2026-07-26-dependency-swaps-rejected-by-nih-audit.md: 421ce93a20c567cac4d6a96f806949348dff3e6b
2026-07-26-dependency-swaps-rejected-by-nih-audit.zh.md: 8ae9eb83269a910160aef3a563dce6d83dbc7ad8
2026-07-26-dependency-swaps-rejected-by-nih-audit.md: 538f6a41b4d2db72f867e98810e8e9382cbdac98
2026-07-26-dependency-swaps-rejected-by-nih-audit.zh.md: b3f7a1d9717397ee9ed50890bc024fba7d79fa9a

View File

@@ -18,7 +18,7 @@ Adopt the following dependency swaps. Rejected — per-item evidence below; a fu
- **`vscode-languageserver-types` for lsp-local's wire-type subset**: ~80 type lines and ~45 guard lines, but upstream guards differ in both directions (accept `uri: undefined` the repo must reject; require `targetRange` the repo tolerates absent), and the initialize-result shapes live in `vscode-languageserver-protocol`, dragging `vscode-jsonrpc` in as a runtime dep — ~1 MB for 80 spec-exact lines.
- **`json-rpc-2.0` for `dsh-jsonrpc`**: deletable correlation/dispatch is real (~100130 lines) but the NDJSON wire must stay bit-identical for the hand-rolled Python SDK client, the package is single-maintainer, and the [GUI RPC note](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md) already treats this package as a frozen narrow surface. `vscode-jsonrpc` is a worse fit still (Content-Length framing, cancellation vocabulary the protocol lacks).
- **`jsonrpcclient` for the Python SDK client**: v4 builds/parses messages only — ~20 lines — while the 500 lines that matter (subprocess lifecycle, threaded reader, id correlation, bidirectional server-role responses) stay; the library is in low-maintenance mode.
- **`eventsource-parser` for apiproxy's `readSse`**: only ~15 lines of framing are deletable, both wire ends are in-repo so spec conformance is moot, and it would add a dep to a browser-safe package. (Contrast with the [llm-deepseek proposal](../../implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md), where a real provider sits across the wire.)
- **`eventsource-parser` for apiproxy's `readSse`**: only ~15 lines of framing are deletable, both wire ends are in-repo so spec conformance is moot, and it would add a dep to a browser-safe package. (Contrast with the [archived llm-deepseek dependency decision](../../archived/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md), where a real provider sits across the wire.)
**Retry, timers, async:**
@@ -46,7 +46,7 @@ Adopt the following dependency swaps. Rejected — per-item evidence below; a fu
- **`shell-quote` for POSIX single-quoting**: two 1-line quoting helpers with exhaustive tests versus a maintenance-mode package with a CVE history and different escaping output — a safety boundary is the wrong place to save one line.
- **`strip-ansi` for pty sanitization**: the pty sanitizer is a streaming state machine with split-sequence carry across chunks and OSC `133;D` prompt-marker extraction (the shell-readiness signal); stateless strippers replace ~20 inner lines while all state machinery stays. `stripVTControlCharacters` also demonstrably leaks unterminated-OSC payloads the session-title normalizer must strip (anti-spoofing).
- **`pidtree`/`ps-tree` for the pty process inspector**: bare PID trees; the code needs start-time identity against PID reuse plus `/proc` stdin-wait detection no package does.
- **`execa` for the subagent-subprocess dispose ladder**: `forceKillAfterDelay` covers SIGTERM→SIGKILL but not the stdin-EOF-first cooperative tier or the reject-if-no-exit-edge contract; adopting it here rewrites spawn sites while keeping the ladder. (Test-infrastructure spawn plumbing is different — see the [execa Agent Note](../../implemented/testing/2026-07-26-execa-for-test-subprocess-plumbing.md).)
- **`execa` for the subagent-subprocess dispose ladder**: `forceKillAfterDelay` covers SIGTERM→SIGKILL but not the stdin-EOF-first cooperative tier or the reject-if-no-exit-edge contract; adopting it here rewrites spawn sites while keeping the ladder. (Test-infrastructure spawn plumbing is different — see the [archived execa test-infrastructure decision](../../archived/testing/2026-07-26-execa-for-test-subprocess-plumbing.md).)
- **`tree-kill` for acp-snapshot teardown and lsp process kill**: the lines are drain-ordering/error-propagation, not tree traversal; lsp/bash already use detached process groups + taskkill.
- **node-pty everywhere for the TUI test driver**: the archived [Windows-TUI note](../../archived/feature/2026-07-20-windows-tui-support.md) explicitly rejected node-pty-on-every-host; it was already the Windows leg.

View File

@@ -18,7 +18,7 @@ Status: rejected — 下列每一项替换在证据上都未达到净简化门
- **以 `vscode-languageserver-types` 承担 lsp-local 的协议类型子集**:约 80 行类型加约 45 行守卫,但上游守卫在两个方向上都与本仓库不一致(接受本仓库必须拒绝的 `uri: undefined`;强制要求本仓库容忍缺失的 `targetRange`),而且 initialize 结果的形状住在 `vscode-languageserver-protocol` 里,会把 `vscode-jsonrpc` 拖成运行时依赖——为 80 行严格贴合规范的代码付出约 1 MB。
- **以 `json-rpc-2.0` 替换 `dsh-jsonrpc`**:可删除的关联/分发代码确实存在(约 100130 行),但 NDJSON 协议格式wire format必须与手写的 Python SDK 客户端逐位一致,该包只有单一维护者,且 [GUI RPC 决策](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)已把这个包当作冻结的窄接口面对待。`vscode-jsonrpc` 更不合适Content-Length 分帧、该协议并不具备的取消词汇)。
- **以 `jsonrpcclient` 承担 Python SDK 客户端**v4 只做消息的构造/解析——约 20 行——而真正要紧的 500 行子进程生命周期、线程化读取器、id 关联、双向的服务端角色应答)全都保留;该库处于低维护模式。
- **以 `eventsource-parser` 替换 apiproxy 的 `readSse`**:可删除的分帧只有约 15 行,线路两端都在仓库内,规范符合性无关紧要,而且这会给一个浏览器安全的包添加依赖。(对比 [llm-deepseek 提案](../../implemented/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md):那里线路对面是真实的提供方。)
- **以 `eventsource-parser` 替换 apiproxy 的 `readSse`**:可删除的分帧只有约 15 行,线路两端都在仓库内,规范符合性无关紧要,而且这会给一个浏览器安全的包添加依赖。(对比[已归档的 llm-deepseek 依赖决策](../../archived/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md):那里线路对面是真实的提供方。)
**重试、定时器与异步:**
@@ -46,7 +46,7 @@ Status: rejected — 下列每一项替换在证据上都未达到净简化门
- **以 `shell-quote` 承担 POSIX 单引号包裹**:两个各 1 行、测试详尽的引号辅助函数,对上一个处于维护模式、有 CVE 历史、转义输出还不一样的包——安全边界不是省一行代码的地方。
- **以 `strip-ansi` 承担 pty 净化**pty 净化器是一台流式状态机,带跨分片的断裂序列续接和 OSC `133;D` 提示符标记提取shell 就绪信号);无状态的剥离器只能替掉约 20 行内层代码,全部状态机构件原样保留。`stripVTControlCharacters` 还被实证会泄漏未终止的 OSC 载荷,会话标题归一化器必须剥除它们(反欺骗)。
- **以 `pidtree`/`ps-tree` 承担 pty 进程巡检器**:它们只给裸 PID 树;这段代码需要对抗 PID 复用的启动时间身份校验,加上 `/proc` stdin 等待检测,没有包做这些。
- **以 `execa` 承担 subagent-subprocess 的 dispose资源释放阶梯**`forceKillAfterDelay` 覆盖 SIGTERM→SIGKILL但覆盖不了先发 stdin EOF 的协作层级,也覆盖不了「无退出沿即 reject」契约在这里采用它意味着重写各 spawn 调用点、同时阶梯照旧保留。(测试基础设施的 spawn 管线是另一回事——见 [execa Agent Note](../../implemented/testing/2026-07-26-execa-for-test-subprocess-plumbing.md)。)
- **以 `execa` 承担 subagent-subprocess 的 dispose资源释放阶梯**`forceKillAfterDelay` 覆盖 SIGTERM→SIGKILL但覆盖不了先发 stdin EOF 的协作层级,也覆盖不了「无退出沿即 reject」契约在这里采用它意味着重写各 spawn 调用点、同时阶梯照旧保留。(测试基础设施的 spawn 管线是另一回事——见[已归档的 execa 测试基础设施决策](../../archived/testing/2026-07-26-execa-for-test-subprocess-plumbing.md)。)
- **以 `tree-kill` 承担 acp-snapshot 拆除与 lsp 进程终止**那些代码行做的是排空顺序与错误传播不是进程树遍历lsp/bash 已经使用分离的进程组加 taskkill。
- **在 TUI 测试驱动器上到处使用 node-pty**:已归档的 [Windows TUI 决策](../../archived/feature/2026-07-20-windows-tui-support.md)明确否决了在每个宿主上都使用 node-pty它当时已经是 Windows 那一条腿。