@a9i5k4/dsh-auto-memory 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +30 -13
  2. package/README.zh-CN.md +30 -13
  3. package/docs/FRONTEND-CO-CREATION.md +191 -0
  4. package/docs/GM53-HOMEPAGE-PROMPT.md +323 -0
  5. package/docs/HANDBOOK.md +88 -52
  6. package/docs/HOMEPAGE-CONTENT-FOR-GM53.md +299 -0
  7. package/docs/PROMO-PROMPT-3.0.md +100 -0
  8. package/docs/USER-GUIDE.en.md +11 -11
  9. package/docs/USER-GUIDE.zh-CN.md +11 -11
  10. package/docs/WHITEPAPER.md +207 -0
  11. package/docs/screenshots/promo/promo-0-banner-v3.png +0 -0
  12. package/docs/screenshots/promo/promo-0-banner-v4.png +0 -0
  13. package/docs/screenshots/promo/promo-1b-auto-recall.png +0 -0
  14. package/lib/activation-host.js +69 -10
  15. package/lib/board-mode.js +1 -1
  16. package/lib/client.js +1697 -285
  17. package/lib/config-io.js +156 -0
  18. package/lib/context-bridge.js +3 -0
  19. package/lib/context-host.js +23 -10
  20. package/lib/degrade.js +385 -0
  21. package/lib/dsh-home.js +143 -0
  22. package/lib/episodic-store.js +142 -18
  23. package/lib/evidence-store.js +8 -1
  24. package/lib/fact-store.js +484 -43
  25. package/lib/hub-io.js +217 -0
  26. package/lib/index-sync.js +13 -1
  27. package/lib/index.js +1730 -202
  28. package/lib/intent-clean-safe.js +258 -40
  29. package/lib/l0-extract.js +231 -16
  30. package/lib/m4-corpus.js +8 -2
  31. package/lib/m7-index-sync-host.js +8 -1
  32. package/lib/memory-envelope.js +6 -1
  33. package/lib/memory-hub.js +164 -17
  34. package/lib/memory-index.js +4 -2
  35. package/lib/note-status-apply.js +118 -0
  36. package/lib/note-status.js +204 -0
  37. package/lib/procedure-store.js +333 -31
  38. package/lib/procedure-switch.js +38 -0
  39. package/lib/python-sidecar-client.js +314 -11
  40. package/lib/recall-fusion.js +83 -12
  41. package/lib/rules-edit.js +159 -0
  42. package/lib/semantic-decide.js +41 -8
  43. package/lib/semantic-js.js +51 -6
  44. package/lib/shadow-host.js +3 -5
  45. package/lib/skill-export-host.js +153 -0
  46. package/lib/skill-export.js +239 -0
  47. package/lib/storage-manage.js +6 -0
  48. package/lib/temporal-parse.js +191 -159
  49. package/lib/tier0-catalog.js +45 -3
  50. package/lib/wb-contract.js +198 -2
  51. package/lib/wb-sidecar.js +54 -3
  52. package/package.json +6 -2
  53. package/docs/internal/ACCEPT-35-LIVE.md +0 -143
  54. package/docs/internal/ACCEPTANCE-20260914.md +0 -90
  55. package/docs/internal/ARCH-REVIEW-BRIEF.md +0 -411
  56. package/docs/internal/ARCH-REVIEW-REQUEST.md +0 -201
  57. package/docs/internal/ARCH-REVIEW-ROUND2.md +0 -169
  58. package/docs/internal/ARCH-REVIEW-ROUND3.md +0 -206
  59. package/docs/internal/ART-DIRECTION-WIREFRAME.md +0 -181
  60. package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +0 -314
  61. package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +0 -192
  62. package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +0 -72
  63. package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +0 -131
  64. package/docs/internal/CUA-VISION-FIX-NOTES.md +0 -78
  65. package/docs/internal/DECISIONS-20260914-SESSION.md +0 -269
  66. package/docs/internal/DESIGN-OVERHAUL-PRE-RESEARCH.md +0 -292
  67. package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +0 -219
  68. package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +0 -132
  69. package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +0 -13
  70. package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +0 -74
  71. package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +0 -352
  72. package/docs/internal/GPT-REVIEW-PROMPT.md +0 -216
  73. package/docs/internal/GROUP-DIGEST-SETUP.md +0 -62
  74. package/docs/internal/GROUP-LISTENER-SETUP.md +0 -49
  75. package/docs/internal/GROUP-WEBHOOK-SETUP.md +0 -93
  76. package/docs/internal/HANDOFF-TO-ZCODE.md +0 -168
  77. package/docs/internal/KICKOFF-P0.md +0 -254
  78. package/docs/internal/MASTER-PLAN-3.0.md +0 -411
  79. package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +0 -85
  80. package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +0 -222
  81. package/docs/internal/NEXT-VERSION-TODO.md +0 -95
  82. package/docs/internal/OFFICIAL-DISCUSSION-DRAFT.md +0 -80
  83. package/docs/internal/PENDING-FIXES-20260916.md +0 -289
  84. package/docs/internal/RAG-KARPATHY-PROGRAM.md +0 -229
  85. package/docs/internal/RELEASE-PROCESS.md +0 -99
  86. package/docs/internal/REPORT-P0-NIGHTLY.md +0 -212
  87. package/docs/internal/REPORT-P5-ACCEPTANCE.md +0 -31
  88. package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +0 -153
  89. package/docs/internal/REVIEW-WB-GRAPH-SELF.md +0 -81
  90. package/docs/internal/ROADMAP-20260917-WEEK.md +0 -305
  91. package/docs/internal/ROADMAP.md +0 -106
  92. package/docs/internal/RUN-P0-NIGHTLY.md +0 -227
  93. package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +0 -175
  94. package/docs/internal/S10-GAPS-PLAIN-20260917.md +0 -125
  95. package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +0 -360
  96. package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +0 -90
  97. package/docs/internal/SUBAGENT-REPORT-ROUTING-PRE-RESEARCH.md +0 -261
  98. package/docs/internal/THREE-LAYER-CONTRACT.md +0 -210
  99. package/docs/internal/TODO-BACKLOG.md +0 -263
  100. package/docs/internal/TODO-GRAPH.html +0 -715
  101. package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +0 -493
  102. package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +0 -710
  103. package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +0 -710
  104. package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +0 -703
  105. package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +0 -710
  106. package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +0 -715
  107. package/docs/internal/WB-FORMAT-CONVENTION.md +0 -112
  108. package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +0 -71
  109. package/docs/internal/WB-GRAPH-INTEGRATION-PLAN.md +0 -386
  110. package/docs/internal/WB-GRAPH-RESEARCH-BRIEF.md +0 -118
  111. package/docs/internal/WB-GRAPH-RESEARCH-EXTERNAL.md +0 -228
  112. package/docs/internal/WB-GRAPH-RESEARCH-LOCAL.md +0 -190
  113. package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +0 -56
  114. package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +0 -787
  115. package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +0 -112
  116. package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +0 -230
@@ -1,261 +0,0 @@
1
- # 子代理完成报告与窗口接续 · 技术预研
2
-
3
- > 状态:**只做预研,不出码**(用户 2026-09-10 指示「下一版再改」)。
4
- > 研究范围:DSH live 版本 0.1.5-rc.1 的官方包源码,逐条带 `文件:行号`。
5
- > 代码基准目录:`C:\Users\JH Z\AppData\Roaming\npm\node_modules\@deepseek-ai\dsh\node_modules\@deepseek-ai\`
6
- > 插件侧基准:`D:\dsh-auto-memory\lib\index.js`
7
-
8
- ---
9
-
10
- ## 0 结论摘要
11
-
12
- 1. **子代理完成后把报告投给"父",在 DSH 里是双重固定绑定,没有官方接口可以改投。**「re-parent / 改归属」在本版本不存在(`dsh-subagent` 的服务方法清单里没有任何写 `parentSession` 的方法)。
13
- 2. 想达成目标只有三条路:**①延后接续(避开问题)**、**②桥接转发(不改归属,改投递)**、**③双会话协同(交接材料声明 + 新会话自查)**。
14
- 3. 有一把**现成的官方钥匙**:DSH 的 Agent 句柄支持 `followup / steer / inject`,且 `ctx.get('agents').get(sessionId)` 按会话取句柄 —— 插件可以把任意消息投进**任意 live 会话并唤醒它**。转发路线就建立在这上面。
15
- 4. **另有一个比"投错窗口"更严重的隐患**:父会话不在 registry 时,结算通知**被静默丢弃**(`dsh-subagent:1249` 直接 `return`)。接续之后如果旧会话被销毁,在跑的子代理报告会**直接消失**——这条必须优先兜住。
16
- 5. 推荐顺序:**A 延后接续 → D 材料声明 → B 转发兜底 → C 可选**。
17
-
18
- ---
19
-
20
- ## 1 投递链:报告是怎么走到"父窗口"的
21
-
22
- 子代理分两类,**投递路径不同**。**本机默认走第二类**(`:1.2`)——`tool-subagent` 的 `backgroundMode` 在 standard/cordis preset 里是 `continuable`,所以用户看到的"报告"绝大多数是 **settlement notice**,不是 job 通知。
23
-
24
- ### 1.1 一次性后台子代理(走 job)
25
-
26
- - `dsh-tool-subagent/lib/index.js:537` 起一个 `kind: "background"` 的 job(带 `run_in_background` 的工具调用走这条;插件自己的 `auto-memory-fold/consolidate/greet` 也都是 one-shot)。
27
- - 完成通知由 **jobs 插件**发出:
28
- `dsh-tool-jobs/lib/index.js:206-227`
29
- ```js
30
- ctx.jobs.onJobDone((snapshot, owner) => {
31
- if (snapshot.reported || owner === void 0) return
32
- const message = createUserMessage({ content: [...], source: {...form: 'notice'} })
33
- if (delivery === 'wakeup' && owner.status === 'idle' && spent < wakeBudget) { owner.followup(message); return }
34
- owner.inject(message)
35
- })
36
- ```
37
- 官方模板:有界唤醒 `maxConsecutiveWakes: 3` + 超预算降级为 `inject`(不唤醒)。
38
- - **`owner` 是 spawn 时捕获的 Agent 句柄,不是 session id** —— 没有任何字符串层可以替换。
39
- - job 的所有权围栏同样按 owner 走:`dsh-jobs/lib/index.js:51-55`(「access is fenced by the owner's session id」)、`dsh-jobs-local/lib/index.js:179-180`(`job.owner.id === session`)。
40
-
41
- ### 1.2 可续子代理(走 activation;**本机默认**)
42
-
43
- - 结算投递:`dsh-subagent/lib/index.js:1244-1259`
44
- ```js
45
- notifySettlement(activation, terminal) {
46
- if (!activation.announced) return
47
- const parent = this.ctx.agents.get(activation.parentSession) // ← 结算时按 session id 现取
48
- if (parent === void 0) return // ← 父不在 ⇒ 静默丢弃
49
- const message = createSettlementMessage(activation.childId, terminal)
50
- if (this.closingTeardownFor(parent) !== undefined) { parent.inject(message); return }
51
- this.sendWaking(parent, message, parent.status === 'idle' ? 'queue' : 'steer')
52
- }
53
- ```
54
- - 投递原语:`dsh-subagent/lib/index.js:873-885` —— 父自身是 resident 激活时走它的 inbox 并唤醒,否则退化为 `parent.steer(message)` / `parent.followup(message)`。
55
- - 通知正文形态:`dsh-subagent/lib/index.js:641-681` —— 首行 `Background subagent <childId> finished...`,附「Its closing message:」+ 子代理最后一条消息;`source = { kind: 'subagent-settled', form: 'notice', senderSessionId: childId }`。
56
-
57
- ### 1.3 三处绑定 + 一处内存快照
58
-
59
- | 绑定 | 位置 | 性质 |
60
- |---|---|---|
61
- | `child.header.parentSession` | 子会话 header(持久化) | 只用于**查询过滤**(`dsh-subagent:2073`、`2180`)与**授权校验**(`:862`、`:968`) |
62
- | `activation.parentSession` | `dsh-subagent:1089` 建立激活时写入 | 内存快照;**通知投递读它**(`:1248`) |
63
- | `job.owner` | job 记录 | 一次性后台子代理的投递目标 |
64
-
65
- ### 1.4 我们真正能用的投递面(关键)
66
-
67
- `dsh-agent-loop/lib/index.js:773-797`:
68
- ```js
69
- get status() { return this.phase.kind === 'idle' || this.phase.kind === 'maintenance' ? 'idle' : 'running' }
70
- followup(input) { this.send(input, 'next-turn', true) } // 排队 + 唤醒
71
- steer(input) { this.send(input, 'next-step', true) } // 插到最近一步 + 唤醒
72
- inject(input) { this.send(input, 'next-step', false) } // 只排队,不唤醒
73
- ```
74
- 配合 `ctx.get('agents').get(sessionId)`(Agent 注册表按会话 id 取),**插件可以往任意 live 会话投递并选择是否唤醒**。这是转发方案的全部基础;插件目前已经在用同一注册表(`lib/index.js:6693` `engine._subagents = ctx.get('subagents')`,`_lastAgent` 也来自 `ctx.get('agents')`)。
75
-
76
- ### 1.5 实机取证(本机 2026-09-10 18:31)
77
-
78
- 从 DSH 的会话投影缓存直接读出(路径:`~/.dsh/storages/session_projcache/sessions/<sessionId>.json` → `record.rows.*`):
79
-
80
- **① 每个会话都有一行 `subagentCatalog`,内容就是它的直接子代理清单:**
81
- ```json
82
- {"inheritedEventCount":0,"head":{"values":[
83
- {"version":0,"childId":"…","childCreatedAt":1789035230558,"mode":"one-shot","label":"auto-memory-fold"},
84
- {"version":0,"childId":"…","childCreatedAt":1789035713789,"mode":"continuable","label":"文档体系审计"}
85
- ]}}
86
- ```
87
- 本机实测:旧会话 `40727a84` 的 catalog 有 13 条(含本预研刚 spawn 的两个研究子代理,`childCreatedAt=1789036077798/99`),新会话 `9cc01f76` 的 catalog 有 5 条(含 18:21:53 spawn 的三条设计审计)。
88
- → **归属是按父会话分行的,一行不会串到另一行** —— 这正是问题所在,也说明"哪个孩子属于谁"随时可查、成本极低。
89
-
90
- **② 接续之后两条会话同时 live**:18:31:38 与 18:31:28 两个时刻,`40727a84`(6.2MB)与 `9cc01f76`(1.4MB)的 `session.v3.jsonl.zstd` **都在持续写入**。
91
- → 旧会话并没有因为接续而退出,它仍然在跑回合、仍在收孩子的报告。这解释了用户看到的现象,也让"以旧父身份继续管孩子"(路线 C)在当前进程内成立。
92
-
93
- **③ 由此得到一条更省事的数据源**:判定"某会话是否还有孩子在跑",不必非走 `listChildren`,用 `agents.get(childId)?.status === 'running'` 就行(`status` 语义见 §1.4),孩子的 id 从 `subagentCatalog` 投影行拿。`agents` 服务插件已经在用。
94
- ⚠️ 但 `subagentCatalog` 本身**没有 activity 字段**(`SA/lib/types/projection-types.d.ts:8-17`:只有 `{id, createdAt, mode, label}`),而且 `listChildren` 的 `activity` 走的是 Session store 口径(在内存= running),**只有浏览器面 `remoteExportList` 才会重新采样 Agent driver**(`SA/lib/index.js:74-83`)。所以要拿到真实"在跑"状态,host 侧应当自己 `agents.get(id)?.status` 复核。
95
-
96
- ### 1.6 可用的官方投递面(实现时会用到的原语清单)
97
-
98
- | 原语 | 位置 | 语义 |
99
- |---|---|---|
100
- | `agent.followup(msg)` | `dsh-agent-loop:789` | 排到 next-turn **并唤醒**(空闲会话立刻开新回合) |
101
- | `agent.steer(msg)` | `:792` | 插到 next-step **并唤醒** |
102
- | `agent.inject(msg)` | `:795` | 插到 next-step **不唤醒**;空闲会话可能永远不醒,`cancel`/dispose 会丢弃 |
103
- | `agent.status` | `:773` | `idle` / `running` |
104
- | `ctx.get('agents').get(sessionId)` | Agent 注册表 | **只返回 live 的** Agent |
105
- | `sessionController.resolveAgent(sessionId)` | host-only | 冷会话也能**现场拉起**再投递 |
106
- | `sessionController.prompt({requestId, sessionId, mode:'queue'\|'steer', content})` | `SC/lib/index.js:773-774` | `queue`=新回合、`steer`=当前回合下一步;两者都不打断 |
107
- | `ctx.on('subagent/end', info)` | `SA/lib/types/index.d.ts:94` | 子代理结束事件:`{runId, provider, id, stopReason, lastAssistantMessage?}` |
108
- | `ctx.on('agent/turn-stopping')` | `AG/lib/types/runtime-types.d.ts:396` | 回合将关;listener 可 `agent.steer()` 让回合继续 |
109
- | `ctx.on('agent/status')` | `:247` | `{agent, status}` 状态跃迁(可做"子代理由 running→idle"的触发) |
110
-
111
- **两条硬边界**(必须记住):
112
- - 子代理子会话(`header.origin === 'subagent'`)不能走 `sessionController.prompt`,一律 `session/agent-busy` + `use subagent delivery for this child session`。
113
- - `sessionController` **没有** session 级 ACL:`dsh-authorization` 与越权无关;host 插件直调不需要 approval。换句话说,该做的闸门由插件自己把关(只对"已被接续过的旧会话"做转发)。
114
-
115
- ---
116
-
117
- ## 2 「能不能直接改投」— 逐条否证
118
-
119
- | 设想的办法 | 结论 | 证据 |
120
- |---|---|---|
121
- | 官方有没有 re-parent / transfer 接口 | **没有**。服务方法清单只有 `list / listChildren / listDescendants / remoteExportList / prompt / interrupt / drainContinuableChildren / start / startContinuable / sendMessage` | `dsh-subagent:2981 / 2999 / 3015 / 3040 / 3082 / 2959` |
122
- | 让新会话直接接管旧会话的子代理 | **被授权拒绝**:控制面按 `header.parentSession` 校验,跨父报 `subagent "X" belongs to another parent session` | `dsh-subagent:862`、`:968`、`:945` |
123
- | 改子会话 header 的 `parentSession` | **不建议**:官方持久化格式字段 + 内存快照(activation)与 job.owner 各自独立,改一处不足以重定向 job 那条路,还会让 `authorizeLineage` 与新父不一致 | `:1001`、`:945`、`:968` |
124
- | 反射式改 `activation.parentSession` / `job.owner` | 理论可行、**强烈不建议**:跨版本立即碎,且 `ancestry` 校验(`:863`、`:945`)按建立时的谱系判定,改完控制面全废 | 同上 |
125
-
126
- **唯一"正解"是把投递改写在自己的层上** —— 见第 3 节 B。
127
-
128
- ---
129
-
130
- ## 3 四条可行路线
131
-
132
- ### A. 延后接续(治本,先做)
133
-
134
- **原理**:问题的本质是「旧会话还有在跑的孩子,却被接续了」。把空闲判据从「回合边界」扩到「回合边界 **且** 无 running 直接子代理」。
135
-
136
- - **接续时记名**(最小改动、收益最大):在 `hostAutoContinue` / 一键接续组装材料前,用 `listChildren(oldSid)` 取一次"未完成子代理清单",把**子代理 id + 标签 + 起始时间**写进交接材料(路线 D)。即使它们后来报告到了旧窗口,新会话也知道"有谁在外面跑、用哪个 id 去问"。
137
- - 数据源:`ctx.get('subagents').listChildren(parentSessionId, signal)` → `resolveCandidateRows(...)` 按 `header.parentSession === parentSessionId && origin === 'subagent'` 过滤(`dsh-subagent:2073`);`remoteExportList` 额外给 `activity: running | inactive`(用 live Agent 注册表判定,`:74-83`)。
138
- - ⚠️ **只有用旧会话 id 查得到这些孩子**:`listChildren(newSid)` 查不到(`:2073` 按 `header.parentSession` 过滤)——这也是"新会话无法接管"的同一个根因。
139
- - 落点:插件 `lib/index.js:2143-2150` 的 `tickAutoContinue` → `awaitIdle` 分支,`busy` 判据追加「有 running 直接子代理」,沿用现有 `deferCount` 上限机制(最多 5 次 × 20s)。
140
- - 必须带三条保险:①**硬上限**(例如最多推迟 5 分钟或 5 次),超时照常接续,避免长任务里永不接续;②**用户手动接续不受限**(一键接续直接走);③service 不可用时 **fail-open**(`subagent/projections-unavailable` 等错误照常接续)。
141
- - 代价:约 10-15 行 + 一处 service 调用。风险:低。
142
-
143
- ### B. 桥接转发(真正把报告送进新窗口)
144
-
145
- **原理**:不改变归属,插件自己把「结算事实」再投一份到新会话。
146
-
147
- #### B-0 触发器:**官方有专门的事件,插件直接订阅即可**
148
-
149
- `dsh-subagent/lib/types/index.d.ts:85,94` 提供两个事件:**`subagent/start`** 与 **`subagent/end`**;
150
- `SubagentRunEndInfo = { runId, provider, id: SessionId, local, stopReason, lastAssistantMessage?: ContentBlock[] }`(`types.d.ts:93-110`),`stopReason ∈ {completed, aborted, error, 'max-tokens', refusal}`。
151
-
152
- 官方现成消费范例(实现 Claude Code 的 `SubagentStop` 钩子):
153
- ```js
154
- // dsh-hooks-claude-code/lib/index.js:309-329
155
- ctx.on("subagent/start", (info) => { const child = ctx.get("agents")?.get(info.id) ... })
156
- ctx.on("subagent/end", (info) => { const child = subagentChildren.get(info.runId) ?? ctx.get("agents")?.get(info.id) ... })
157
- ```
158
- ⇒ **转发只要在插件自己的 ctx 上 `ctx.on('subagent/end', info => ...)`**,payload 里已经带了子代理 id、结束原因与最后一条消息 —— 不需要枚举孩子、不需要轮询、不需要读日志。
159
-
160
- ⚠️ 但 **payload 里没有 parentId**,而且该事件是在 `notifySettlement` **之后**才 emit 的(`dsh-subagent:1239 → :1241`:看到事件时原通知已经入队)。所以"这条报告原本要发给谁"要在事件里自己解一次:
161
- ```js
162
- ctx.on('subagent/end', (info) => {
163
- const child = ctx.get('agents')?.get(info.id) // 官方范例就是这么取的
164
- const fromSid = child?.session?.header?.parentSession // 子会话自己的 durable 头
165
- ... // 再用闩锁表把 fromSid 映射到 toSid
166
- })
167
- ```
168
- (官方另有一个反解范例:`dsh-sdk-jsonrpc-server/lib/index.js:35-37 subagentParentOf`。)
169
-
170
- ⚠️ **不要用"打补丁 steer/followup"的方式拦截**:当父本身是常驻可续子代理时,通知走的是父自己的 activation inbox(`dsh-subagent:874-881`),根本不经过 `agent.steer/followup` —— 补丁会漏。正确做法是**旁听**(`subagent/end` 或 `agent/inbox/inserted`),把文本**复制**一份给新会话。
171
-
172
- **兜底触发器**(覆盖 `subagent/end` 看不到的路径,例如 job 通知未走 activation):插件今天已经在监听原始事件流 `ctx.on('session/event', ...)`(`lib/index.js:6826-6828`),并已在里面按 `event.type === 'user/message'` 分流、读 `source.kind` 与插件名(`:1182-1186`、`:6109-6110`)。两类通知最终都是父会话里的一条 `user/message`:
173
- - 可续子代理:`source = { kind: 'subagent-settled', form: 'notice', senderSessionId: <childId> }`(`dsh-subagent:674-679`)
174
- - job:`source = { kind: 'plugin', plugin: 'tool-jobs', form: 'notice' }`(`dsh-tool-jobs:213-218`)
175
-
176
- #### B-1 投递目标
177
-
178
- 后继会话 id 从已接续闩锁文件 `~/.dsh/memory/auto-continue-done.json`(`{sessions:[{from,to,at}]}`)取 —— 现成数据,`from` 命中事件所属会话即可。
179
-
180
- #### B-2 投递方式
181
-
182
- ```js
183
- const agents = ctx.get('agents')
184
- let target = agents?.get(toSid) // 只返回 live 的
185
- if (!target) target = await ctx.sessionController?.resolveAgent(toSid) // 冷会话现场拉起
186
- target.status === 'idle' ? target.followup(msg) : target.inject(msg)
187
- ```
188
- 与官方 `sendWaking` 同款策略(`dsh-subagent:1255`):空闲则排队并唤醒,忙则只排队不打断。
189
-
190
- ⚠️ 三个必须避开的坑(均来自官方明文):
191
- 1. **不要用 `sessionController.prompt` 投给子代理子会话**:`header.origin === 'subagent'` 一律被拒为 `session/agent-busy`,reason `use subagent delivery for this child session`(`dsh-api-session-controller/lib/index.js:124-138`)。
192
- 2. **`requestId` 是幂等键**:撞车时服务**静默返回 `accepted:true` 但不投递**(`SC/lib/index.js:741`)。转发每次都要现铸新 id,否则报告会丢。
193
- 3. **必须自带唤醒预算**:照抄 `maxConsecutiveWakes` 思路,否则每个子代理结算都开一个模型回合(烧钱且噪音大);超预算降级为 `inject`。
194
- 另:`prompt` 的兜底 catch 会把一切 admission 异常包成 `session/agent-busy` / `"prompt rejected"`,真因只在 `reason` —— 日志必须打 `reason`,否则失败不可见(这正是 2.4.0 修过的那条故障线)。
195
-
196
- #### B-3 消息形态建议
197
-
198
- ```
199
- 【上游子代理结算 · 来自已被接续的会话 <oldSid8>】
200
- Background subagent <childId> finished...(原文首行)
201
- 摘要:<...>
202
- 回读:job_output(<jobId>) / 该子会话 <childId> 末尾若干条
203
- ```
204
-
205
- #### B-4 遗留问题
206
-
207
- - 旧会话里的原通知**无法撤回**,会与新会话里的转发件并存(可接受;若嫌吵,可在旧会话侧做"已转发"标记,但这需要改官方渲染面,不做)。
208
- - 转发件是 user 角色消息,新会话可能当真用户输入 —— 故正文必须显式标注来源(见 B-3)。
209
- - 退避:同一 `senderSessionId` + 同一内容指纹去重,避免重复投递。
210
-
211
- ### C. 以旧父身份继续管那些子代理(可选)
212
-
213
- 插件是 host 侧、持有原始 service,可以直接:
214
- ```js
215
- subagents.prompt({ parentSessionId: oldSid, childSessionId, mode: 'continuable', delivery: 'queue', requestId, content })
216
- subagents.interrupt({ parentSessionId: oldSid, childSessionId, mode: 'continuable' })
217
- ```
218
- 前提:旧会话 agent 仍 live(否则 `subagent/parent-unavailable`,`dsh-subagent:3046`);子代理必须是 continuable(`subagent/not-resumable`)。**新会话自己不能管**(授权按 header.parentSession)。
219
-
220
- 用途:续问/收拢/中断"遗留孩子"。不是必须项。
221
-
222
- ### D. 交接材料声明 + 新会话可查(顺手做,零风险)
223
-
224
- - 接续时把「旧会话在跑的子代理清单」(id + 标题 + 起始时间 + 是否 continuable)写进交接材料(carry),让新会话一开始就知道"还有哪些活在外面跑"。
225
- - 插件再暴露一个**只读**查询面(HTTP 端点或给新会话用的工具),回答"旧会话的孩子现在什么状态、输出在哪"。
226
- - 代价:小。把「完全看不见」变成「看得见、可回读」。
227
-
228
- ---
229
-
230
- ## 4 需要实机验证的点(改之前先测)
231
-
232
- 1. 接续之后,旧会话的 agent 是否仍 live(`ctx.get('agents').get(oldSid)`)?—— 决定 A 能否等待、B/C 能否读到孩子输出。**已初步取证:是**(18:31 两份 v3 日志同时在写,见 §1.5)。
233
- 2. `listChildren(oldSid)` 在本机能否返回(取决于 projections / sessionQuery 是否挂载),错误码分别是什么。
234
- 3. 一次性后台子代理的 job `snapshot` 里有没有可判别"这是子代理 job"的字段(`kind` / `label`),用于 B 的过滤。
235
- 4. **`subagent/end` 在插件 ctx 上的可见范围**:能否收到别的作用域下的子代理结束(官方 `dsh-hooks-claude-code` 用它实现全局 SubagentStop,倾向可以);payload **不含 parentId**,需自己从子会话 header 反解。
236
- 5. **`announced` 门**:通知只在 `activation.announced === true` 时发出,而它只在父**真的投递过消息**给子代理才置位(`dsh-subagent:1246`、`:1809/1929`)—— 存在"结算了但从不产生通知"的子代理,这类只能靠 A/D 兜。
237
- 6. **本机 `backgroundMode` 已确认为 `continuable`**(`dsh-agent-presets/presets/standard/agent.cordis.yml:181-187`),故 live 路径是 settlement notice;job 那条路只对显式 `run_in_background` 的 one-shot 生效。
238
- 7. 新会话被 `followup` 唤醒时的打扰程度(用户正在输入时是否该退化为 `inject` 只排队)。
239
- 8. 父会话销毁后通知是否真的丢弃(`:1249`)—— 若会丢,B 必须"落盘优先",否则报告永久消失。
240
-
241
- ---
242
-
243
- ## 5 明确不做的事
244
-
245
- - ❌ 不改 DSH 内部对象(`activation.parentSession`、`job.owner`)。
246
- - ❌ 不重写子会话 header 的 `parentSession`(官方持久化格式 + 校验,跨版本必碎)。
247
- - ❌ 不改官方 UI 的 subagent 面板;所有改动留在插件自己的 layer。
248
- - ❌ 不在本轮出码 —— 本文只是预研。
249
-
250
- ---
251
-
252
- ## 6 与现有插件的接口对照(落点清单)
253
-
254
- | 需要的东西 | 现状 | 缺什么 |
255
- |---|---|---|
256
- | subagents service | `lib/index.js:117` 已 `inject: ['subagents']`,`:6693` 已缓存 `_subagents` | 直接用 `listChildren` / `remoteExportList`(尚未调用) |
257
- | agents 注册表 | `_lastAgent` / `inheritPermission*` 已在用 | 把它当"投递口"用(`followup/inject`)是新增语义 |
258
- | 接续映射表 | `~/.dsh/memory/auto-continue-done.json`(`from → to`) | 正好可作为 B 的判据来源 |
259
- | job 完成事件 | 未订阅 | 新增 `ctx.jobs.onJobDone` 监听器 |
260
- | 空闲判据 | `tickAutoContinue` 的 `awaitIdle`(`:2143-2150`) | 追加"无 running 子代理" |
261
- | 交接材料 | `buildContinueCarry` | 追加"在外子代理清单"一节 |
@@ -1,210 +0,0 @@
1
- # 三层检索契约(Tier-0 / Tier-1 / Tier-2)v2
2
-
3
- > 2026-09-14 定稿(v2 重整:补 RAG 管线要素、用实测数据定预算)。
4
- > 背景:三层 OpenViking 式架构**验收"以为过了、实际没过"**——只验了声明项,没验能力可达。
5
- > 本文件是施工与验收的**唯一依据**。相关:`ROADMAP.md`(主线)、`TODO-GRAPH.html`(P0-② 阶段门)、`MEMORY-MUTATION-AND-INDEX-DESIGN.md`(索引侧)。
6
-
7
- ---
8
-
9
- # 第一部分 · 三层安排(Arrangement)
10
-
11
- ## 1.1 为什么是三层
12
-
13
- 三层的本质是**把"要不要用"和"内容是什么"分开**:上层负责**判断与缩窄**(便宜、常驻、永远在场),下层负责**给出证据**(贵、按需、可溯源)。因此判定顺序是"上层先出 → 搜索空间缩窄 → 才下探",而不是把三层一起灌进上下文。
14
-
15
- ## 1.2 三层总表
16
-
17
- | 维度 | **Tier-0 · 目录(指引层)** | **Tier-1 · 摘要(候选层)** | **Tier-2 · 原文块(证据层)** |
18
- | --- | --- | --- | --- |
19
- | 回答的问题 | 要不要用某条记忆? | 该下探哪一条? | 原文到底怎么说的? |
20
- | 输入 | 项目笔记 + 用户级记忆 + 当日日志的**结论行** + 白板 | 语料记录(日志 / 反思 / 笔记 / 用户记忆 / 白板) | 命中记录的 chunk(`chunkId = hash(记忆ID, 内容摘要, 序号)`) |
21
- | 产出 | 每条 **1 行**:`标题 · 一句结论 · layer · status · 日期` | 每条 1 段摘要(≤`L1` 字符)+ `id` + 得分 + 匹配原因 | 命中块原文 + `文件:行号` + `digest` |
22
- | 常驻性 | **每轮都注入**(不依赖命中) | 命中后按需(top-`K`) | 需要证据时下探 |
23
- | 预算 | ≤ `B0` = **800 token** | ≤ `L1` = **140 字符** × `K` = **8** | ≤ `B2` = **2400 字符** / 次 |
24
- | 失效方式 | 陈旧即被上游覆盖(无状态) | 状态过滤(`superseded`/`retracted`) | 随快照版本(`miv`)失效 |
25
-
26
- ## 1.3 数据流
27
-
28
- ```
29
- 源文件(日志 / 反思 / 笔记 / 用户记忆 / 白板)
30
- │ ①抽取:标题 → 首句 → 截断(三级降级) ← Tier-1 的产出
31
- ├──────────────► Tier-1 摘要(≤140 字符/条)
32
- │ ②分块:chunkId = hash(记忆ID, 内容摘要, 序号) ← Tier-2 的存储单位
33
- ├──────────────► Tier-2 原文块(≤1500 字符,实测)
34
- │ ③目录:从"当前认知"里抽 1 行/条 ← Tier-0 的产出
35
- └──────────────► Tier-0 目录(≤800 token,常驻)
36
-
37
- 检索时:Tier-0 先出 → 缩窄 → 命中不足才下探 Tier-1 → 需要证据才取 Tier-2
38
- ```
39
-
40
- ---
41
-
42
- # 第二部分 · 每层的要素清单
43
-
44
- ## 2.1 Tier-0(目录)要素
45
-
46
- | 要素 | 要求 |
47
- | --- | --- |
48
- | 内容来源 | **只取"当前认知"**:项目笔记 / 用户记忆 / 白板的标题与结论行;**禁止原始转储** |
49
- | 每行字段 | `标题`、`一句结论`、`layer`、`status`、`日期`(+ 内部 `id` 供下探) |
50
- | 优先级 | `project > whiteboard > user > log`;同层按日期倒序(超预算时按此裁剪) |
51
- | **per-layer 配额(必须有)** | 纯严格优先级会让 `project` **吃满预算**——真实语料实测:project 77 块把 800 token 全占,`whiteboard`/`user`/`log` **一条都进不来**,于是"分层"退化成单层。**规定:`project ≤ 60%·B0`;`whiteboard`、`user` 各**保底 10%**;剩余额度再按优先级填充** |
52
- | 刷新时机 | 每轮或笔记变更后重建;重建**必须廉价**(纯文本处理,不涉嵌入) |
53
- | 状态位 | 带 `status`;被 `superseded`/`retracted` 的**不出现在目录**(I5) |
54
- | 降级 | 目录为空/来源缺失 → 输出显式提示,不静默给空(I7) |
55
-
56
- ## 2.2 Tier-1(摘要层)要素 —— 第二层要做好的事
57
-
58
- | # | 要素 | 说明与要求 | 现状 |
59
- | --- | --- | --- | --- |
60
- | 1 | **抽取规则** | 标题 → 首句 → 截断(三级降级),保证**任何记录都有摘要** | ✅ 已有(`l0-extract-pre.js`) |
61
- | 2 | **必须保留"判定信息"** | 摘要里要留下**结论句**(例:"已拍板 X""根因是 Y");只留主题词等于没用 | ❌ 缺(决策句常被截掉) |
62
- | 3 | **长度上限** | ≤`L1`=140 字符;超限按"信息密度"裁,不是简单截断 | ⚠️ 现约 93 字符,无规则 |
63
- | 4 | **层级与状态** | 每条带 `layer`(五值)与 `status`(三值) | ❌ 缺(C1 在做) |
64
- | 5 | **得分与匹配原因** | 返回 `词法×n / 语义×score` 与命中词,便于调参与解释 | ✅ 已有 |
65
- | 6 | **多路融合** | 词法 + 语义 → RRF 融合;任一路不可用要标注(I7) | ⚠️ 语义臂会静默消失 |
66
- | 7 | **同源去重 / 合并** | 同一文件多个块命中 → 合并为一条显示,避免刷屏占预算 | ❌ 缺 |
67
- | 8 | **排序体现当前认知** | 笔记/结论优先于历史日志;被替代项降权或过滤 | ❌ 缺(现为纯 top-K) |
68
-
69
- ## 2.3 Tier-2(原文块)要素 —— 第三层要做好的事
70
-
71
- | # | 要素 | 说明与要求 | 现状 |
72
- | --- | --- | --- | --- |
73
- | 1 | **分块粒度** | 按**记录/小节**切(现有 `chunkOrdinal/chunkCount`);实测 p50=418、p90=1186 字符 | ✅ 已有 |
74
- | 2 | **块边界与重叠** | 边界不切断结论句;跨块语义断裂处应有**少量重叠**(或把结论句并入首块) | ❌ 未见重叠 |
75
- | 3 | **块元数据** | `chunkId`、`memoryId`、`sourceRef`、`recordDigest`、`chunkOrdinal/Count` | ✅ 已有(15 字段) |
76
- | 4 | **按块取,不取整篇** | 只回命中块(I3);长文档禁止整篇灌入 | ⚠️ 契约新定 |
77
- | 5 | **可溯源** | 返回 `文件:行号` + `digest`,便于核验与回滚 | ✅ expand 已返回 |
78
- | 6 | **超长块处理** | 单块 > `B2` 时:先截结论句 + 尾注"(已截断,全文 N 字符)",不静默丢 | ❌ 缺 |
79
- | 7 | **保真** | 原文不改写、不摘要(引用必须逐字) | ✅ |
80
- | 8 | **编码与 BOM** | 读回时剥离 BOM,保持 UTF-8 | ⚠️ 需断言 |
81
-
82
- ---
83
-
84
- # 第三部分 · 一条好 RAG 管线的关键部件(对照表)
85
-
86
- | # | 部件 | 我们要做到什么 | 现状 |
87
- | --- | --- | --- | --- |
88
- | 1 | **分块(Chunking)** | 语义完整、带元数据、必要时重叠 | ✅ 主体已有;重叠缺 |
89
- | 2 | **嵌入(Embedding)** | 引擎身份入键(模型/维度/归一化);换引擎=全量重建 | ⚠️ OR 双引擎要写清切换语义 |
90
- | 3 | **索引(Index)** | 向量 + 词法双路;**缓存键=chunkId**(内容寻址) | ❌ 现按整份语料哈希(P0-④) |
91
- | 4 | **查询理解** | 多键提取、时间意图("最近/上次") | ⚠️ 部分 |
92
- | 5 | **召回与融合** | 词法 + 语义 RRF;分数可解释 | ✅ 融合已有;语义臂不稳 |
93
- | 6 | **重排(Rerank)** | 现在靠融合排序;有需求再上轻量重排 | ⚠️ 够用 |
94
- | 7 | **过滤与权限** | `layer`/`status` 过滤、来源白名单、**检索+注入双层** | ❌ 缺(C1/C2) |
95
- | 8 | **上下文组装** | 预算分配、排序(相关性 vs 时序)、去重、provenance 标注 | ❌ 缺(C5) |
96
- | 9 | **可溯源(Grounding)** | 每条注入带出处(文件+行号+digest) | ✅ 部分 |
97
- | 10 | **评估(Evaluation)** | ground truth 集 + 指标(命中率/答案可达率/噪声比/token) | ⚠️ 只有词法基线 |
98
- | 11 | **容错与降级** | 跳过+计数+quarantine;未就绪显式标注(I7);fail-open | ❌ 现在 fail-closed 且静默 |
99
- | 12 | **新鲜度与增量** | 写后防抖触发、快照 epoch、差量同步 | ❌ 缺(P0-④c/④e) |
100
-
101
- ---
102
-
103
- # 第四部分 · 预算(Budget):要,而且要算出来
104
-
105
- ## 4.1 结论先说
106
-
107
- **需要预算,并且它必须是三层一起核对过的总量。** 三层若同时全给,会直接顶穿注入预算——这不是理论,是实测:
108
-
109
- ```
110
- Tier-0 800 token(≈1600 字符) + Tier-1 8×140(=1120 字符) + Tier-2 2400 字符
111
- = 5120 字符,而注入预算 injectBudgetChars = 8000 字符(**可配置项**;见 SPEC §0.2)
112
- → 三层仍不同时给:本契约规定"逐层下探",与预算是否宽裕无关
113
- ```
114
-
115
- **所以本契约规定:三层不是"同时给",而是"逐层下探"**(见 §5 闸门)。默认只给 Tier-0;命中不足才给 Tier-1;要证据才给 Tier-2。
116
-
117
- ## 4.2 预算的实测依据(E2,2026-09-14 重启后复跑)
118
-
119
- 来源:`~/.dsh/memory/semantic-pre/derived-corpus.json`(重启后重建,**34 条记录**,字段含 `text`):
120
-
121
- | 指标 | 实测值 |
122
- | --- | --- |
123
- | 单条原文长度 | 最小 **144** / p50 **760** / p90 **1196** / p99 **1692** / 最大 **1692** 字符 |
124
- | 合计 | 24,254 字符(均值 713) |
125
- | 超过 1000 字符 | 7 条(20.6%) |
126
- | 超过 2000 / 5000 / 10000 字符 | **0 / 0 / 0 条** |
127
-
128
- **对账(证明"语料 text 长度"这个代理指标可信)**:抽 2 条真实 `expand` 与语料长度比对 —— `mem_d55f8e8e`:expand 报 **1499 字符** ↔ 语料 1499 ✅;`mem_5a7f779a`:expand 报 **1403 字符** ↔ 语料 1403 ✅。
129
-
130
- ## 4.3 由此定出的预算(初值)
131
-
132
- | 参数 | 取值 | 依据 |
133
- | --- | --- | --- |
134
- | `B2`(Tier-2 单次) | **2400 字符**(≈1200 token) | E2 实测 max=1692;语料仍在增长(条均 713),留 ~42% 余量;超出者按 §2.3-6 截断(截结论句 + 标注"已截断,全文 N 字符") |
135
- | `L1`(Tier-1 每条) | **140 字符**(≈70 token) | 原文 p50=760 → 压到 ~1/5 保留事实;现摘要约 93 字符偏短、缺结论句 |
136
- | `K`(Tier-1 条数) | **8** | 8×140=1120 字符 ≈ 560 token,占注入预算 ~1/4 |
137
- | `B0`(Tier-0 常驻) | **800 token**(≈1200 字符) | 占注入预算约 1/4;目录"每条 1 行"约 12 字 → 可容纳 ~30 条当前认知 |
138
- | 总量上限 | 逐层下探,**不同时给**;单轮注入总长 ≤ `injectBudgetChars`(默认 8000) | §4.1 的实测校验 |
139
-
140
- > **口径(与 SPEC §0.2 一致,别再当矛盾)**:Tier-0 常驻有两层门 —— `tier0MaxTokens` **默认 400**(**可配置项**),硬上限 `B0` = **800 token**;属「默认值 vs 上限」之别。
141
-
142
- ## 4.4 中间"概览层"要不要?——**本数据下不需要**
143
-
144
- OpenViking 是 L0(~100 token) → L1(~2k) → L2(原文)。我们的实测是:**原文本身 p90 只有 1196 字符、max 1692**,L0(140 字符) 直接跳到原文块(≤2400)**跨度可接受**。
145
- 判定规则(写进契约,可复测):**当出现单块 > 5000 字符的源(如整篇 PLAN.md、超长日志)时,才需要"概览层"或更细的分块**;当前 **0 条**命中该条件(最近复核:34 条,max 1692)。
146
-
147
- ## 4.5 预算不是拍脑袋(校准流程)
148
-
149
- 1. 跑 **E1**(预算—召回曲线):扫 `L1 ∈ {60, 90, 140, 220, 400}`、`B0 ∈ {200, 400, 800, 1600}`,记录命中率与**答案可达率**;
150
- 2. 跑 **E3**(直接灌 vs 逐层):三策略的正确率 / token / 噪声比;
151
- 3. **选值规则:在满足"答案可达率 ≥ 0.9"的前提下取最小预算**(膝盖点);
152
- 4. 校准结果回写本表,并同步 `TODO-GRAPH.html` 的 P1-⑯。
153
-
154
- ## 4.6 token 计量口径(不统一,I1 就无法验证)
155
-
156
- 仓库既有 `estimateSessionTokens = ceil(chars/4)+4`(`lib/index.js:2471`)**对 CJK 低估 2–4 倍**,直接拿它当预算门会**静默突破 I1**。
157
-
158
- - **契约规定:预算门取 `max(ceil(chars/2), 仓库口径)`**(保守值恒 ≥ 仓库值);
159
- - C4 的 Tier-0 模块已按此实现并锁进测试;`estimateMode:'repo'` 可复算仓库口径对照;
160
- - 换算:`B0 = 800 token` ≈ **1600 字符**目录容量。
161
-
162
- ---
163
-
164
- # 第五部分 · 递进闸门
165
-
166
- ```
167
- 默认(常态):只注入 Tier-0(≤ B0 = 800 token)
168
- ↓ 当(Tier-0 命中 < 2 条)或(问题含「为什么 / 怎么 / 具体 / 复现」语义)时
169
- 下探 Tier-1:top-K(K=8,每条 ≤ L1 = 140 字符)
170
- ↓ 当(需要引用 / 行号 / 复现命令 / 判定"原文怎么说")时
171
- 下探 Tier-2:取该条的命中块(≤ B2 = 2400 字符)
172
- ```
173
-
174
- - 允许**升层**(下层命中不足→回上层重选),**禁止**一次性三层全灌;
175
- - 阈值(2 条 / 8 条 / 140 字符)为初值,由 E1/E3 校准。
176
-
177
- ---
178
-
179
- # 第六部分 · 不变量(违反即回归)
180
-
181
- - **I1** Tier-0 常驻且 ≤ `B0`;**I2** Tier-1 每条 ≤ `L1`、条数 ≤ `K`;**I3** Tier-2 按块且 ≤ `B2`;
182
- - **I4** 每层条目必须带 `layer` + `status`;
183
- - **I5** 非 `current` 的条目在**检索结果与注入内容两处**都被过滤;
184
- - **I6** 三层来自同一份快照(同一 `miv`),混版视为错误;
185
- - **I7** 索引未就绪 / 语义臂不可用**必须显式降级标注**(例:`[语义索引未就绪 · 已降级为词法]`),禁止静默丢弃注入。
186
-
187
- `layer` ∈ { `user` | `project` | `log` | `reflection` | `whiteboard` };`status` ∈ { `current` | `superseded` | `retracted` }。
188
-
189
- ---
190
-
191
- # 第七部分 · 施工清单与验收
192
-
193
- | # | 改动 | 文件 | 状态 |
194
- | --- | --- | --- | --- |
195
- | **C1** | 抽取层补 `layer` + `status` | `lib/l0-extract-pre.js` | ✅ **完成**(+100/−2;`classifyLayerPre`/`L0_LAYERS`/`L0_STATUSES`/`L0_DEFAULT_LAYER`;21 断言绿) |
196
- | **C2** | 召回返回带 `layer/status` + **检索侧过滤**(I5) | `lib/index.js` | ✅ **完成**(l0Mode 与语义臂两处;导出 `isCurrentPre`;21 断言绿;p2 回归已修复) |
197
- | **C4** | Tier-0 目录生成器(每条 1 行,≤ `B0`,按优先级 + 配额裁剪) | 新 `lib/tier0-catalog-pre.js` | ✅ **完成**(33 断言绿;真实语料 788 token ≤ 800;**配额已随 C5 落地**:`allocateTier0QuotaPre` 为 opt-in,关闭时逐字节保持旧行为) |
198
- | **C3** | 接线 `l0-index-pre.js`(L0 自己的向量索引,增量;**需显式落 layer/status 两列**) | `lib/l0-index-pre.js` + 新 `lib/l0-index-sync-pre.js` + `lib/index.js` | ✅ **完成**(模块侧显式落两列并把层/状态计入索引身份;接线经 `createL0IndexSyncPre` 的 `update({layer})` 显式传层;**按层各一份**索引文件;开关 `l0IndexEnabled` **默认开**(用户 2026-09-14 裁定)、5 分钟节流、全 fail-soft;**待宿主重启复核**) |
199
- | **C5** | 注入层改造:Tier-0 常驻 + 按闸门下探 + **per-layer 配额** + 降级标注(I7) | 新 `lib/tier-layer-inject-pre.js` + `lib/index.js` + `lib/context-host-pre.js` + `lib/activation-host-pre.js` | ✅ **完成**(83 断言绿 + 5 处定向变异报红;**入口更正:每轮 `<memory_system>` 块由 `renderMemoryDynamic` 产出,`injectionText` 是零引用死代码**;`index-not-ready` 由静默丢弃改为降级仍注入;**待宿主重启复核**) |
200
- | **C6** | 验收套件(每条能力一个**能失败**的断言) | `tests/smoke/smoke-test-three-layer-pre.mjs` | ✅ **完成**(**122 断言**:Tier-0 预算 19 / layer+status 27 / supersede 双层 14 / expand 回归 9 / 源文件损坏降级 11 / C3 接线 32 / C7 回归 10;7 处定向变异全部报红) |
201
- | **C7** | 注入侧可见性:块内 `Score: 0.xx (rank n/m)` + reason 串带 `intent/dense/margin`(P1-⑮) | `lib/activation-inbox-pre.js` + `lib/context-host-pre.js` + `python/worker_semantic_pre_v1.py` | ✅ **完成**(29 断言绿:分值在、降序排名、乱序必重排、预算计入、无分省略;**待宿主重启后在真实注入里复核**) |
202
-
203
- **验收判据("真过"的定义)**:
204
- 1. Tier-0 常驻且 ≤ `B0`,内容为"当前认知"而非原始转储;
205
- 2. L0 列表**每条**带 `layer` + `status`;
206
- 3. 造一条被 supersede 的记忆 → 断言它在**结果与注入**两处都不出现,但审计视图可见;
207
- 4. 造一条命中 → 断言 `expand` 取回原文(**已通,作为回归钉子**);
208
- 5. **故意破坏一个源文件** → 断言检索仍返回词法命中 + 明确降级标注(不是整条失败);
209
- 6. 上列每条都在 `tests/smoke/` 有对应套件,且**故意改坏实现时确实会红**。
210
- 7. **注入侧看得见相似度**(C7):注入块每条带 `Score: 0.xx (rank n/m)`,块序按分值严格降序;分值非法整行省略(不写 NaN);这几行的字节计入预算(不得静默超预算)。