@thincoder/core 0.10.3 → 0.10.4

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 (68) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/advisor/citations.mjs +29 -3
  3. package/advisor/compaction.mjs +21 -2
  4. package/advisor/history.mjs +8 -6
  5. package/advisor/loop.mjs +29 -27
  6. package/advisor/messages.mjs +2 -1
  7. package/advisor/run.mjs +5 -1
  8. package/advisor/timeline.mjs +26 -0
  9. package/advisor.mjs +4 -2
  10. package/agent/completion.mjs +3 -1
  11. package/agent/digest-account.mjs +188 -0
  12. package/agent/family-tools.mjs +11 -11
  13. package/agent/helpers.mjs +8 -0
  14. package/agent/record-results.mjs +4 -0
  15. package/agent/run-stages.mjs +27 -4
  16. package/agent/run-start.mjs +49 -3
  17. package/agent/suspension.mjs +30 -8
  18. package/agent/turn-loop.mjs +25 -2
  19. package/agent/write-gate.mjs +13 -25
  20. package/agent-tools/advisor-async.mjs +14 -6
  21. package/agent-tools/advisor.mjs +34 -33
  22. package/agent-tools/async-settle.mjs +29 -5
  23. package/agent-tools/batch-lifecycle.mjs +26 -5
  24. package/agent-tools/batch-paths.mjs +22 -2
  25. package/agent-tools/batch-skeleton.mjs +31 -33
  26. package/agent-tools/batch.mjs +11 -3
  27. package/agent-tools/design-token.mjs +20 -0
  28. package/agent-tools/escalate-async.mjs +4 -2
  29. package/agent-tools/review-facts.mjs +143 -16
  30. package/agent-tools/subagent-actions-query.mjs +13 -6
  31. package/agent-tools/subagent-async.mjs +10 -3
  32. package/agent-tools/subagent-panel.mjs +2 -1
  33. package/agent-tools/subagent-spawn.mjs +1 -1
  34. package/agent-tools/subagent.mjs +1 -1
  35. package/agent-tools/zero-write-watch.mjs +77 -0
  36. package/agent.mjs +1 -1
  37. package/conventions.mjs +26 -6
  38. package/escape.mjs +1 -1
  39. package/i18n.mjs +5 -0
  40. package/ledger-tools.mjs +148 -85
  41. package/ledger-variant-notice.mjs +47 -0
  42. package/ledger.mjs +2 -2
  43. package/manifest-schema.mjs +6 -4
  44. package/manifest.mjs +5 -3
  45. package/package.json +1 -1
  46. package/prompts/common.md +2 -2
  47. package/prompts/persona-engineering.md +4 -3
  48. package/provider/anthropic.mjs +14 -2
  49. package/provider/errors.mjs +23 -0
  50. package/provider/google.mjs +13 -2
  51. package/provider/responses-request.mjs +3 -1
  52. package/provider/responses.mjs +38 -4
  53. package/shell-identity.mjs +107 -0
  54. package/tool-docs/advisor.md +1 -1
  55. package/tool-docs/bash.md +1 -1
  56. package/tool-docs/batch.md +1 -1
  57. package/tool-docs/delete.md +7 -5
  58. package/tool-docs/ledger.md +6 -0
  59. package/tool-docs/subagent.md +10 -0
  60. package/tools/bash.mjs +9 -4
  61. package/tools/index.mjs +1 -4
  62. package/tools/patch.mjs +8 -2
  63. package/tools/write-path.mjs +6 -4
  64. package/tool-docs/ledger_add.md +0 -1
  65. package/tool-docs/ledger_close.md +0 -1
  66. package/tool-docs/ledger_count.md +0 -1
  67. package/tool-docs/ledger_query.md +0 -1
  68. package/tool-docs/ledger_update.md +0 -1
@@ -13,7 +13,9 @@ import { pushManifestStateReminder } from "./setup-reminders.mjs"
13
13
  import { cleanupConsultSessions } from "../agent-tools/consult.mjs"
14
14
  import { logEvent, errText } from "../log.mjs"
15
15
  // ASYNC-RESULT-CONTAINER.md D1:池 accessor(absorb 双池——advisor 独立池无队列)
16
- import { getAsyncPool, releaseSettledEntry } from "../agent-tools/async-settle.mjs"
16
+ import { carrierField, getAsyncPool, releaseSettledEntry } from "../agent-tools/async-settle.mjs"
17
+ // §6.31 消化账务(批 digest-accounting · 2026-10-05 · 台账 #930):见账 pass ∥ 回合分类单点
18
+ import { accountDigestRound, isDigestRound } from "./digest-account.mjs"
17
19
  // 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-ASYNC-POOL.md §6.20):中止分支「只清已死」收尾单点
18
20
  // (#9:+ 后台 bash 任务族——逐条杀树 + 出池 + 墓碑)
19
21
  import { discardAbortedPool, discardAbortedAdvisors, discardAbortedBgTasks } from "../agent-tools/async-discard.mjs"
@@ -23,10 +25,15 @@ import { flushPeerDomains } from "../peer-domains.mjs"
23
25
  // 第 30 批 D1(Stop 钩子——AGENT-LOOP.md §6.13):主会话 run 终止事件(触发块见 finalizeAgentTurn)
24
26
  import { runHooks } from "../hooks.mjs"
25
27
 
28
+ /** 结束信号缺席提醒(§6.31.7 F-DA5——逐字 = 父侧定稿;与「非 stop 异常」同面)。 */
29
+ const FINISH_MISSING_REMINDER =
30
+ "[System reminder: the previous turn ended without a finish signal — the provider did not report how generation ended. If the response appears incomplete, continue from where it left off.]"
31
+
26
32
  /**
27
33
  * 响应后置提醒注入(2026-09-05 实践轮——自 runAgent 响应处理链提取,verbatim):
28
34
  * 流规则 warning(warn 动作——流已完成——去重注入,模型下轮可见)+ 异常 finish
29
- * reason 警告(响应可能不完整/截断——reasonMap 人类化描述注入)。
35
+ * reason 警告(响应可能不完整/截断——reasonMap 人类化描述注入)+ 结束信号缺席提醒
36
+ * (§6.31.7 F-DA5——clean end 无 finishReason ⇒ 同面提醒 + 留痕)。
30
37
  */
31
38
  export function injectResponseReminders(agent, response) {
32
39
  // Stream rule warnings (action: "warn"): stream completed; inject de-duplicated
@@ -39,6 +46,14 @@ export function injectResponseReminders(agent, response) {
39
46
  })
40
47
  }
41
48
 
49
+ // §6.31.7 F-DA5(#929——批 digest-accounting):结束信号缺席(clean end 且无 finishReason)
50
+ // ⇒ 不得静默当正常——同面异常提醒注入 + `ev:finish-missing` 留痕;partial / interrupted /
51
+ // ruleTriggered 径零触(网络断流既有 `_warnings` 已覆盖;ruleTriggered 在调用点之前分流)。
52
+ if (response.finishReason == null && !response.partial && !response.interrupted) {
53
+ agent.history.push({ role: "user", content: FINISH_MISSING_REMINDER })
54
+ logEvent("ev:finish-missing", { turn: agent._currentTurn ?? 0, auto: agent._inAutoTurn === true, child: agent._logId })
55
+ }
56
+
42
57
  // Warn on abnormal finish reasons — the response may be incomplete/truncated.
43
58
  if (response.finishReason && response.finishReason !== "stop" && response.finishReason !== "tool_calls") {
44
59
  const reasonMap = {
@@ -145,11 +160,12 @@ export async function injectTurnReminders(agent, ctx) {
145
160
  * 回合收尾(2026-09-05 实践轮——自 runAgent finally 提取,verbatim + 签名化):写足迹
146
161
  * flush → Stop 钩子(第 30 批 D1——ctx.depth === 0 的主会话 run 终止事件,
147
162
  * fire-and-forget)→ consult 清理(无孤儿烧 token 的 consult children)→ async 池回合尾
148
- * 分流(Ctrl+C 清池不注入 / ContinueError 留池续跑 / 其余 collectSettledAsync)→ auto-turn
163
+ * 分流(Ctrl+C 清池不注入 / ContinueError 留池续跑 / 其余 collectSettledAsync)→
164
+ * **见账 pass(§6.31.5——消化轮收尾逐条机检账目覆盖;消化账务批)** → auto-turn
149
165
  * guard 标记继承(对象字段清单——正常结束才继承;中止丢弃;ContinueError 由续跑快照)。
150
166
  */
151
167
  export async function finalizeAgentTurn(agent, ctx) {
152
- const { signal, autoTurn, suspDriven, thrownError, depth } = ctx
168
+ const { signal, autoTurn, upstreamTurn, timerTurn, suspDriven, thrownError, depth } = ctx
153
169
  // R10 L3(MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末登记 flush——本回合写足迹整写
154
170
  // 一次(首行执行:收尾链后续任何异常都不吞登记);无写入 → 跳过(hot 窗口自然衰减)。
155
171
  flushPeerDomains(agent)
@@ -204,6 +220,7 @@ export async function finalizeAgentTurn(agent, ctx) {
204
220
  // (原 _pendingConsultResults/_pendingEscalateResults 同口径——VSC 同语义 filter);
205
221
  // subagent/advisor 停靠保留(挂起期 settle 的已完成结果不随中止丢)。
206
222
  agent._pendingAsyncResults = (agent._pendingAsyncResults ?? []).filter((e) => e.role !== "consult" && e.role !== "escalate")
223
+ agent._unsettledDigests = [] // §6.31.5 清账面:会话中止 ⇒ 升级账本随 pending 一并清(用户全停 = 旧账不续)
207
224
  } else if (thrownError instanceof ContinueError) {
208
225
  // keep _asyncSubagents/_asyncAdvisors/_consultSessions — the resumed run continues them
209
226
  } else {
@@ -220,6 +237,12 @@ export async function finalizeAgentTurn(agent, ctx) {
220
237
  closeOpenCodeAdvisorRuns(agent)
221
238
  }
222
239
  }
240
+ // §6.31.5 见账面(消化轮收尾——finalize 单点;消化账务批 · 2026-10-05 · 台账 #930):
241
+ // 恒达窗 = 正常 ∥ Abort-continue ∥ 异常抛三径同达;会话停止径不判(清场语义归 finishSuspension)。
242
+ if (isDigestRound({ autoTurn, upstreamTurn, timerTurn }) && carrierField(agent, "_daSession") === true
243
+ && !(signal?.aborted && !signal?.reason?.interrupt)) {
244
+ accountDigestRound(agent)
245
+ }
223
246
  agent._inAutoTurn = false
224
247
  // AGENT-LOOP-ASYNC-POOL.md §6.8 D-S6: auto-turn guard marks survive into the next USER run (restored at its
225
248
  // !resume reset above). Normal ends only — abort discards; ContinueError lets the
@@ -30,14 +30,55 @@ export async function beginRun(agent, input, callbacks, {
30
30
  // +role——四族(subagent/advisor/escalate/consult)统一停靠;注入器按 role 分发
31
31
  // (consult → injectConsultResult;其余 → injectAsyncResult——族分支同 CONSULTATION.md §6.2 D-R17a/b)
32
32
  // ——单容器一处清,不再逐族三段。
33
+ // §6.31 消化账务(批 digest-accounting · 2026-10-05 · 台账 #930):**投递 ≠ 销账**——
34
+ // 会话态(`_daSession`)投递留容器(覆盖才离容器 / 未见账经消化轮回路重投);fallback
35
+ // (headless / 直连 runAgent)缺省条逐字沿用取尽语义(先离容后注入 ∥ `releaseSettledEntry` 面;
36
+ // 零账目动作——不销账 / 不重投),已投递未销账条同判据跳过且留容器(§6.31 出口缝② · 父侧裁定 2026-10-05)。
33
37
  const pendingAsync = agent._pendingAsyncResults
34
- if (pendingAsync?.length) {
38
+ const { carrierField, releaseChildHold, releaseSettledEntry } = await import("../agent-tools/async-settle.mjs")
39
+ if (carrierField(agent, "_daSession") === true) {
40
+ const { armAccountRound, isDigestRound, shouldDeliverEntry, digestAccountRequirement } = await import("./digest-account.mjs")
41
+ armAccountRound(agent) // §6.31.5 落痕清位:会话态起跑即清(缺痕 ⇒ 全条未覆盖——安全方向)
42
+ if (pendingAsync?.length) {
43
+ const { injectAsyncResult } = await import("../agent-tools/subagent.mjs")
44
+ const { injectConsultResult } = await import("../agent-tools/consult.mjs")
45
+ const digestRound = isDigestRound({ autoTurn, upstreamTurn, timerTurn })
46
+ // 投递门控两态(§6.31.4):首投任意回合 ∥ 重投仅消化轮;注入后**留容器**(不 splice)。
47
+ // 注入抛错 ⇒ 未投条目(`_daDelivered` 未置)留容器待下轮——收窄既有「splice 先取尽」的丢失窗。
48
+ for (const e of [...pendingAsync]) {
49
+ if (!shouldDeliverEntry(e, digestRound)) continue
50
+ if (e.role === "consult") await injectConsultResult(agent, e)
51
+ else await injectAsyncResult(agent, e)
52
+ e._daDelivered = true
53
+ e._daRetry = false
54
+ releaseChildHold(e) // 仅释放 `childAgent`——`report` 保留至销账 / 升级(重投需原文)
55
+ }
56
+ // 账目要求行(§6.31.3):仅会话态消化轮且未销账清单非空时注入(先投递后拼清单——
57
+ // 含本回合首投 / 重投);两档同发(手动 / AUTO);transient(机器线独有)。
58
+ if (digestRound) {
59
+ const ids = pendingAsync.filter((e) => e._daDelivered === true).map((e) => e.id)
60
+ if (ids.length > 0) {
61
+ agent.history.push({ role: "user", content: digestAccountRequirement(ids), transient: true })
62
+ }
63
+ }
64
+ }
65
+ } else if (pendingAsync?.length) {
35
66
  const { injectAsyncResult } = await import("../agent-tools/subagent.mjs")
36
67
  const { injectConsultResult } = await import("../agent-tools/consult.mjs")
37
68
  // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §6.15 消费点②——run 起始 pending 注入):
38
69
  // 注入完成后释放条目对子代理对象的持有(childAgent/report 置空——幂等 helper)。
39
- const { releaseSettledEntry } = await import("../agent-tools/async-settle.mjs")
40
- for (const e of pendingAsync.splice(0)) {
70
+ // §6.31 出口缝②(#44 披露①扩散面 · 父侧裁定 2026-10-05):fallback 支同判据延伸——
71
+ // 注入前按投递账分区:已投递未销账条(`_daDelivered === true`——内容已在历史)不注入且
72
+ // 留容器(与 idle 清场同义——防重复 / 条目不离账务面);`_daDelivered` 缺省条逐字沿用
73
+ // (先离容后注入的取尽窗 ∥ 注入顺序 ∥ `releaseSettledEntry` 面)。
74
+ const taken = []
75
+ let kept = 0
76
+ for (const e of pendingAsync) {
77
+ if (e?._daDelivered === true) pendingAsync[kept++] = e
78
+ else taken.push(e)
79
+ }
80
+ pendingAsync.length = kept
81
+ for (const e of taken) {
41
82
  if (e.role === "consult") await injectConsultResult(agent, e)
42
83
  else await injectAsyncResult(agent, e)
43
84
  releaseSettledEntry(e)
@@ -80,6 +121,11 @@ export async function beginRun(agent, input, callbacks, {
80
121
  agent._verifyPassed = undefined
81
122
  agent._calledAdvisorThisRun = false
82
123
  agent._touchedFiles = []
124
+ // #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32.6 载体重置):streak / 闩与
125
+ // `_touchedFiles = []`(本块)同点复位——链起点;续跑(resume)不执行 ⇒ 「连续」
126
+ // 跨段存活(锚与分支差别注见 §6.32.2 ①)。
127
+ agent._zeroWriteStreak = 0
128
+ agent._zeroWriteAlerted = false
83
129
  agent._verifyRetries = 0
84
130
  agent._advisorRound = 0
85
131
  agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
@@ -5,11 +5,14 @@
5
5
  * 回合尾后台池仍 live → 挂起会话——池项 settle → 入 pending;pending 非空 **或存在未 drain 的
6
6
  * 上行 ask**(`upstreamWaiting`——F-UC7 / §6.27.12.4 ①)→ 合并消化轮 / 唤醒轮(auto-turn;旗标
7
7
  * `upstreamTurn` 仅供域文本选择);挂起空闲用户输入优先于消化轮。池空 + pending 空 + 无待处理
8
- * 输入 → 自然退出(idle)。退出清场:abort = 清池不注入(陈旧结果不注入);idle = 残余直注入(结果零丢失)。
8
+ * 输入 → 自然退出(idle)。退出清场:abort = 清池不注入(陈旧结果不注入);idle = 残余直注入
9
+ * (结果零丢失;已投递未销账条按投递账过滤——留容器不注入,§6.31 出口缝)。
9
10
  *
10
11
  * 载体注入(端差面 = 池 / pending / 标志挂谁):`ctx.carrier` = 字段集按核内异步面
11
12
  * 现行口径的对象(`_asyncSubagents` · `_asyncAdvisors` · `_consultSessions` ·
12
- * `_pendingAsyncResults` · `_suspended`;#9 后台 bash 任务池 `_bgTasks` 同列)
13
+ * `_pendingAsyncResults` · `_suspended`;#9 后台 bash 任务池 `_bgTasks` 同列;
14
+ * 消化账务批 · 2026-10-05 同列 `_daSession`(挂起会话标记——置位 / 复位单点)与
15
+ * `_unsettledDigests`(升级账本——中止清场))
13
16
  * ——CLI 形 = `agent` 对象;VSC 形 = depth-0
14
17
  * `history` 数组(附加属性不污染会话文件)。机制对载体零预设(空字段一律 `?.` 读)。
15
18
  *
@@ -112,12 +115,15 @@ function consultLiveCount(carrier) {
112
115
  * - abort:**只清已死**(§6.20 口径——增补 D:`discardAbortedPool` ∕ `discardAbortedAdvisors`
113
116
  * 单点,与核 run-stages 回合尾中止同式;存活 ∕ 已 settle 条目留池——settled 报告沿后续
114
117
  * 回合边界注入到达,不静默丢)+ pending 单容器清 + 会诊会话清理标记;
115
- * - idle:残余直注入(极端竞态残项——结果零丢失;注入器由宿主注入,缺省 no-op 保残项)。
118
+ * - idle:残余直注入(极端竞态残项——结果零丢失;注入器由宿主注入,缺省 no-op 保残项);
119
+ * **已投递未销账条(`_daDelivered === true`)按投递账过滤**——内容已在历史(投递起跑窗):
120
+ * 不注入且留容器(消化账务批出口缝 · #41 披露① · 父侧裁定 2026-10-05——状态面可见 / 可续账)。
116
121
  *
117
- * 返回 `{ injected, left, error }`(子代理面板批 · 2026-10-04 —— `hooks.reclaim` 恒达窗的退出半):
118
- * `left` = **离容全量**(splice 全取——注入失败条不缩水),供调用点 `reclaim(left)` 补发
119
- * (「消费 ⇒ 必发 done」不变量——C2 零发射口);注入循环 try/catch 收口 ⇒ `injected` = 成功
120
- * 前缀、`error` = 首错(调用点补发毕后重抛——fail-loud 保持);abort ∥ 无注入器 ∥ 无残差 ⇒ 三键空值。
122
+ * 返回 `{ injected, left, error }`(子代理面板批 · 2026-10-04 —— `hooks.reclaim` 恒达窗的退出半;
123
+ * 消化账务批出口缝——`left` = **注入集离容全量**(离容全取——注入失败条不缩水;已投递未销账
124
+ * 跳过条不取、留容器),供调用点 `reclaim(left)` 补发(「消费 ⇒ 必发 done」不变量——C2 零发射口;
125
+ * 跳过条未消费、其 done 已在投递起跑窗落——不入补发面);注入循环 try/catch 收口 ⇒ `injected` =
126
+ * 成功前缀、`error` = 首错(调用点补发毕后重抛——fail-loud 保持);abort ∥ 无注入器 ∥ 无残差 ⇒ 三键空值。
121
127
  * 既有 C3/C4 快照径(中止冻结快照)零改。
122
128
  */
123
129
  export async function finishSuspension(carrier, { aborted = false, injectResidual = null } = {}) {
@@ -127,6 +133,7 @@ export async function finishSuspension(carrier, { aborted = false, injectResidua
127
133
  discardAbortedAdvisors(carrier)
128
134
  discardAbortedBgTasks(carrier) // #9 收尾档②:后台 bash 任务逐条杀树 + 出池 + 墓碑(杀 ⟺ 控制器已中止)
129
135
  carrier._pendingAsyncResults = []
136
+ carrier._unsettledDigests = [] // §6.31.5 清账面(消化账务批):会话中止 ⇒ 升级账本随 pending 一并清(旧账不续)
130
137
  if (carrier._consultSessions instanceof Map) {
131
138
  const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
132
139
  cleanupConsultSessions(carrier)
@@ -135,7 +142,18 @@ export async function finishSuspension(carrier, { aborted = false, injectResidua
135
142
  }
136
143
  const residual = carrier._pendingAsyncResults
137
144
  if (!residual?.length || typeof injectResidual !== "function") return { injected: [], left: [], error: null }
138
- const left = residual.splice(0)
145
+ // §6.31 出口缝(#41 披露① · 父侧裁定 2026-10-05):清场按投递账过滤——已投递未销账条
146
+ // (`_daDelivered === true`——内容已在历史)**不注入且不动容器**(状态面可见 / 可续账——防重复);
147
+ // 判据 = 条目既有字段(调用窗 `_daSession` 已复位——不以其为门);其余条逐字沿用。
148
+ // 单遍分区写法(#44 披露④ · 父侧裁定 2026-10-05):一次遍历同出 `left`(离容集)与留容器集——
149
+ // 消除 `indexOf` 未命中 ⇒ `splice(-1,1)` 静默删尾的失败类;离容顺序 ∥ `left` 语义不缩水。
150
+ const left = []
151
+ let kept = 0
152
+ for (const e of residual) {
153
+ if (e?._daDelivered === true) residual[kept++] = e
154
+ else left.push(e)
155
+ }
156
+ residual.length = kept
139
157
  const injected = []
140
158
  for (const e of left) {
141
159
  try { await injectResidual(e) } catch (error) { return { injected, left, error } } // 首错收口(前缀 = injected)
@@ -217,6 +235,9 @@ export function startSuspension(ctx) {
217
235
 
218
236
  const done = (async () => {
219
237
  carrier._suspended = true
238
+ // §6.31.2 会话标记(消化账务批 · 2026-10-05 · 台账 #930):挂起会话期 true——投递留容器 /
239
+ // 见账 / 重投回路的唯一判据;退出(finally)置 false ⇒ 会话外回 fallback 语义。
240
+ carrier._daSession = true
220
241
  hooks.onCounts?.(backgroundCounts(carrier))
221
242
  let aborted = false
222
243
  try {
@@ -298,6 +319,7 @@ export function startSuspension(ctx) {
298
319
  if (!finished) {
299
320
  finished = true
300
321
  carrier._suspended = false
322
+ carrier._daSession = false // §6.31.2 会话标记复位(退出即回 fallback 语义)
301
323
  latch.wake = null
302
324
  // §2.3 退出清场:abort 判定与实时信号合并(两端同式——Stop 落在 runTurn 内时
303
325
  // abort 以 AbortError 抛出、循环顶检查不会再执行——不合并会把清场误走 idle 注入)。
@@ -49,8 +49,13 @@ export async function runTurnLoop(agent, {
49
49
 
50
50
  // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 消费点):子 → 父在飞消息的
51
51
  // 回合边界注入单点取用一次(模块缓存 ⇒ 每 run 一次代价);动态 import = 零新增静态边
52
- // (先例 = 上方 injectAsyncResult :113-117)。
52
+ // (同款先例 = run-start.mjs:43 ∥ :66——injectAsyncResult 动态 import)。
53
53
  const { drainChildUpstream } = await import("../agent-tools/parent-channel.mjs")
54
+ // #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32):连续零写越阈 ⇒ 自动上行提醒(只推——
55
+ // 不杀 / 不转向);单源叶 = zero-write-watch.mjs,动态 import 沿上条同款(零新增静态边)。
56
+ const { maybeZeroWriteAlert } = await import("../agent-tools/zero-write-watch.mjs")
57
+ // §6.31.5 落痕写点门(#939):载体吸收经 `carrierField` 单点(§6.31.4 同式);动态 import = 零新增静态边(沿上方两条同款)。
58
+ const { carrierField } = await import("../agent-tools/async-settle.mjs")
54
59
 
55
60
  // #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值 / 零自动动作):计数基线——回合环逐轮
56
61
  // 比对 `_touchedFiles` 增量(轮顶对上一轮结账;撞帽收尾轮在环外单结)。
@@ -70,7 +75,18 @@ export async function runTurnLoop(agent, {
70
75
  agent._maxTurns = frame.maxTurns
71
76
  // #417(撞帽载荷「本段零落盘轮数」——采集点与 `_turnSeq` 同源面):上一轮至今 `_touchedFiles`
72
77
  // 零增 ⇒ 上一轮零落盘,计数 +1;mark 前移(收尾轮在环外结账——见下方 ContinueError 前)。
73
- if (turn > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
78
+ // #934(零落笔看门狗——§6.32.2 ① 单点采集):streak 与越阈判定同点——上轮零写 ⇒ +1 并判越阈;
79
+ // 有写 ⇒ streak 归零 + 清闩(写后重臂——每连续零写段恰一条)。#417 计数语义逐字保持。
80
+ if (turn > 0) {
81
+ if (agent._touchedFiles.length === agent._turnFilesMark) {
82
+ agent._zeroWriteTurns += 1
83
+ agent._zeroWriteStreak += 1
84
+ maybeZeroWriteAlert(agent) // #934:连续零写越阈 ⇒ 自动上行提醒(只推——§6.32)
85
+ } else {
86
+ agent._zeroWriteStreak = 0
87
+ agent._zeroWriteAlerted = false // 写后重臂
88
+ }
89
+ }
74
90
  agent._turnFilesMark = agent._touchedFiles.length
75
91
  // D2 (AGENT-LOOP-SUBAGENT.md §6.7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
76
92
  // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
@@ -109,6 +125,13 @@ export async function runTurnLoop(agent, {
109
125
  systemPrompt, toolSchemas, streamOutput, callbacks, signal, streamRuleFired, autoTurn, depth, turn,
110
126
  })
111
127
 
128
+ // §6.31.5 落痕(消化账务批 · 2026-10-05 · 台账 #930 ∥ #939 收正):本 run 内**逐轮追加**——
129
+ // 每模型轮返回即写(会话态);首轮签收不为收口轮覆盖(见账取件读累积痕)。
130
+ // 落父字段(抗 `history` 整体替换——`context.mjs` `applyCompression`);非会话零动作。
131
+ if (carrierField(agent, "_daSession") === true && response.content) {
132
+ agent._lastRunOutput = `${agent._lastRunOutput ?? ""}${response.content}\n`
133
+ }
134
+
112
135
  // 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
113
136
  // 服务端 item id 是 msg_xxx 非 web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
114
137
  // 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
@@ -6,8 +6,10 @@
6
6
  * (KD-M4-3)——本档承载三个单点:
7
7
  *
8
8
  * 1. `resolveReviewTargetPaths(agent)` —— 评审目标解析单点:读 manifest `docRoot`
9
- * 产出评审对象 / 被审文件的绝对路径集合(目录级;子键值形态 = 串 | 多根数组,
10
- * 展开走 `docRootPaths`)。缺 `docRoot` 键 → `readManifest`
9
+ * 产出评审对象 / 被审文件的绝对路径集合(目录级;子键值形态 = 串 | 多根数组)。
10
+ * 判定本体(`REVIEW_ROOT_KEYS` 键集 + `resolveReviewRootsFor(dir)`——2026-10-02 收口 ·
11
+ * 台账 #828 按用点形)自本档迁 `review-facts.mjs`(2026-10-04 · #921 · KD-2——本档同名
12
+ * 再出口保既有 import 面);缺 `docRoot` 键 → `readManifest`
11
13
  * 的 fillDefaults 已补默认(架构 §2.3 E2 便利 fallback 落点);整档缺失 / 非法 JSON →
12
14
  * `{ok:false}` → 回退 DEFAULT_MANIFEST.docRoot(本函数是壳门之下的便利层——E2 的
13
15
  * 「整档缺失」由壳面 requireManifest 按用点处置(判 + 报明 / 建档,**不抛**——
@@ -15,8 +17,6 @@
15
17
  * KD-M1-12):CLI make-agent.mjs / VSC setup.mjs 同判);读错(权限等)→ 上抛不伪装成缺失(manifest.mjs 契约)。
16
18
  * M6 的 `advisor.mjs` design-review 分类分支消费本导出
17
19
  * (KD-M6-1——同源不重复实现;落 dispatch.mjs 会让 advisor 反向 import 门禁簇成环)。
18
- * **按用点形(2026-10-02 收口 · 台账 #828)**:`resolveReviewRootsFor(dir)`——同单点
19
- * 非第二实现(按目标目录 ∥ 所属项目根解析;多档锚下各文档按各自项目判定)。
20
20
  * 2. `freezeWindowConflict(agent, absPaths)` —— D5 冻结窗口判据组装:被审文件集 =
21
21
  * 声明文档集(`inflightDesignReviewConflict` 同源 docAbs 腿)+ 批次档(`run.batchDoc`
22
22
  * 腿——评审绑定批档在途时,写批档同样致 stale)。dispatch.mjs
@@ -30,7 +30,6 @@
30
30
  * 代码路径判定(conventions.mjs 单一权威分类——KD-M4-1 保留为代码路径判定用)。
31
31
  */
32
32
  import { basename, dirname, resolve } from "node:path"
33
- import { DEFAULT_MANIFEST, docRootPaths, readManifest } from "../manifest.mjs"
34
33
  // advisor-settle 无上游依赖本簇(dispatch → write-gate → advisor-settle 单向)。normAbs
35
34
  // 权威本体 = review-facts.mjs(第 33 批迁入),advisor-settle re-export;本档再 re-export
36
35
  // (指针链非副本),M6 经本档一行 import 取齐。
@@ -38,13 +37,17 @@ import { inflightDesignReviewConflict, normAbs } from "../agent-tools/advisor-se
38
37
  // 批次档路径单源(叶档——`BATCH-RECORD.md` §4.15):批次档写门基底集取 `batchDocBases`。
39
38
  import { batchDocBases } from "../agent-tools/batch-paths.mjs"
40
39
  export { normAbs }
41
-
42
- /** 评审目标解析读的 docRoot 键集(评审对象/被审文件可住的所有文档层——M6 分类据此判定)。 */
43
- const REVIEW_ROOT_KEYS = ["requirements", "specs", "design", "modules", "batches"]
40
+ // 2026-10-04(工具路径基面根治批 · #921 · KD-2 同名再出口):REVIEW_ROOT_KEYS +
41
+ // resolveReviewRootsFor 判定本体迁 `review-facts.mjs`(评审事实面——advisor-async 既有
42
+ // import 面零新模块边);本档同名再出口(指针非副本——normAbs 同款先例)——既有 import 面
43
+ // (advisor.mjs / 测试)零改;本档消费点 `resolveReviewTargetPaths` 薄委托同名函数。
44
+ export { REVIEW_ROOT_KEYS, resolveReviewRootsFor } from "../agent-tools/review-facts.mjs"
45
+ import { resolveReviewRootsFor } from "../agent-tools/review-facts.mjs"
44
46
 
45
47
  /**
46
- * 评审目标解析单点(M4 §2.1#1):读 manifest `docRoot` → 评审对象 / 被审文件的绝对路径
47
- * 集合(目录级,反斜杠归一;子键值形态 = 串 | 多根数组,逐键经 `docRootPaths` 展开)。
48
+ * 评审目标解析单点(M4 §2.1#1):评审对象 / 被审文件的绝对路径集合(目录级)——
49
+ * 判定本体(manifest `docRoot` 读面 / 缺档回退 / 读错上抛语义)2026-10-04 迁
50
+ * `review-facts.mjs` 单源(KD-2——本档薄委托;详述见该档 resolveReviewRootsFor JSDoc)。
48
51
  * 缺 `docRoot` 键 → readManifest 已补默认;整档缺失 / 非法 →
49
52
  * `{ok:false}` → 回退 DEFAULT_MANIFEST.docRoot(便利层——E2 的「整档缺失」由壳面
50
53
  * requireManifest 按用点处置(判 + 报明 / 建档,**不抛**——启动侧恒不拒,KD-M1-25;**工程模式会话**口径——KD-M1-12),本函数不重复拦);
@@ -56,21 +59,6 @@ export function resolveReviewTargetPaths(agent) {
56
59
  return resolveReviewRootsFor(agent?.cwd ?? process.cwd())
57
60
  }
58
61
 
59
- /** 同单点之**按用点解析**增量(2026-09-21「参数按用点解析」之裁收口 · 台账 #828):按**目标目录**
60
- * (其所属项目根——最近带档祖先)解析评审目标根集——多档锚下各文档按各自项目判定。
61
- * @param {string} dir 目标目录(项目根——最近带档祖先)
62
- * @returns {string[]} 评审目标目录绝对路径(去重)
63
- */
64
- export function resolveReviewRootsFor(dir) {
65
- const man = readManifest(dir)
66
- const root = man.ok && man.manifest?.docRoot && typeof man.manifest.docRoot === "object"
67
- ? man.manifest.docRoot
68
- : DEFAULT_MANIFEST.docRoot
69
- const out = []
70
- for (const key of REVIEW_ROOT_KEYS) out.push(...docRootPaths(root?.[key], dir))
71
- return [...new Set(out)]
72
- }
73
-
74
62
  /**
75
63
  * D5 冻结窗口判据组装(M4 §2.1#2):评审在途(点火 → 结算)期间父侧对被审文件集
76
64
  * (声明文档集 + 批次档)的写入 → 返回冲突 { id, path };无冲突 → null。
@@ -47,13 +47,13 @@ export {
47
47
  // 第 11 批拆分(2026-09-11):settle 记账 + 变更日志(noteMutations)+ 陈旧判定
48
48
  // (reviewIsStale)+ 冻结冲突(inflightDesignReviewConflict)迁 advisor-settle.mjs——
49
49
  // 既有 import 面(dispatch / subagent-async / escalate-async / 测试)经此 re-export 不变。
50
- import { normAbs, mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
50
+ import { mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
51
51
  export {
52
52
  mutationSeqOf, noteMutations, reviewIsStale, settleAdvisorRun, inflightDesignReviewConflict,
53
53
  } from "./advisor-settle.mjs"
54
54
  // 第 33 批(评审失败护栏):doc-set 键迁事实面档(实例续跑与陈旧判定同锚单源;原为私有——零 import 面)。
55
55
  // 本文件继续在实例解析 / 池 entries 上消费它。
56
- import { docSetKey } from "./review-facts.mjs"
56
+ import { docSetKey, resolveReviewDocPaths, scopeAdvisorMirror } from "./review-facts.mjs"
57
57
  // F31(2026-09-18 顾问面治理批):拒回文案对象标识行单源。
58
58
  import { withIdentityLine, scopeSummary } from "../advisor/notice.mjs"
59
59
  import { runAdvisorReview, resolveAdvisorProvider, ADVISOR_THINKING_PLACEHOLDER } from "../advisor/run.mjs"
@@ -65,6 +65,8 @@ import { deathLine } from "../abort-provenance.mjs"
65
65
  // ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
66
66
  import { bindChildController, buildChildSignal, carrierField, getAsyncPool, settleAsyncEntry, tombstoneOf, writeTombstone } from "./async-settle.mjs"
67
67
  import { nextSubagentId, consumeSubagentToken, assertPoolKeyFree, entryTerminal } from "./subagent-scheduler.mjs"
68
+ // #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32.6 触发接线②):评审族推送单源(叶档——恒零抛)。
69
+ import { pushAdvisorAlert } from "./zero-write-watch.mjs"
68
70
 
69
71
  // ─────────────────────────────────────────────────────────────────────────────
70
72
  // Review-instance registry (_advisorRuns — per-review rounds/prior/cap)
@@ -161,9 +163,9 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
161
163
  runs.set(reviewId, run)
162
164
  }
163
165
  }
164
- // Scope the legacy mirror fields (message builder + run.mjs cap + displays).
165
- agent._advisorRound = run.priorOutput ? run.round : 0
166
- agent._lastAdvisorOutput = run.priorOutput
166
+ // Scope the mirror fields to THIS instance (message builder + run.mjs cap +
167
+ // displays; #949 ① — the queued-launch window is re-secured at entry.start).
168
+ scopeAdvisorMirror(agent, run)
167
169
  return { run, isNew: run.round === 0 && !run.priorOutput, reviewId: run.reviewId, designId: run.designId }
168
170
  }
169
171
 
@@ -390,8 +392,11 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
390
392
  id, role: "advisor", reviewType, run,
391
393
  reviewId: run.reviewId, designId, designToken, documents, paths, object,
392
394
  launchSeq: mutationSeqOf(parent),
395
+ // docAbs 产出面同源归一(2026-10-04 · #921 · A 随件):四腿单源 resolved(裸形/前缀形
396
+ // 命中候选项目根即归一为绝对路径)——冻结窗/陈旧判定对**裸形受理档**照护成立(否则 A
397
+ // 开冻结窗保护洞);绝对形与旧 normAbs 面逐字等价。
393
398
  docAbs: reviewType === "design" && Array.isArray(documents)
394
- ? documents.filter((d) => typeof d === "string").map((d) => normAbs(d, parent.cwd))
399
+ ? [...resolveReviewDocPaths(documents, parent.cwd).resolved.values()]
395
400
  : [],
396
401
  relayPrefix: `advisor#${id}/`,
397
402
  status: queued ? "queued" : "running", position: undefined,
@@ -420,9 +425,12 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
420
425
  // Async mark + [model] — the TUI block opens at ACTUAL start (⏹ gating reads it).
421
426
  ctx?.callbacks?.onToken?.(entry.relayPrefix + "⟦ev⟧async\x1e")
422
427
  ctx?.callbacks?.onToken?.(entry.relayPrefix + "[model]" + (entry.model ?? ""))
428
+ // #949 ①:排队 / 延迟窗口封死——构建前按本评审实例复核镜像(消费点定域;幂等)。
429
+ scopeAdvisorMirror(parent, run)
423
430
  runAdvisorReview(parent, reviewType, {
424
431
  onOutput: (chunk) => relayAdvisorOutput(ctx?.callbacks, entry.relayPrefix, chunk),
425
432
  signal: entry.controller.signal,
433
+ onStall: (rounds) => pushAdvisorAlert(parent, `advisor#${id}`, rounds), // #934:连续无产出越阈 ⇒ 自动上行提醒(只推——§6.32)
426
434
  }, designToken, documents, paths, object, designId)
427
435
  .then((report) => { entry.report = report })
428
436
  // §6.12 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
@@ -9,12 +9,11 @@
9
9
  */
10
10
  import { runAdvisorReview, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
11
11
  import { resolveBatchDocPath } from "./batch.mjs"
12
- // M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot(声明面)——复用 M4 的
13
- // write-gate.mjs 单一权威源(KD-M6-1),替代 v1 的逐档分类面(isDocPath);
14
- // normAbs 同源 re-export(指针非副本)。不 import dispatch.mjs(簇间回边,环风险)。
15
- import { resolveReviewTargetPaths, resolveReviewRootsFor, normAbs } from "../agent/write-gate.mjs"
16
- import { owningProject } from "../manifest-discovery.mjs"
17
- import { sep } from "node:path"
12
+ // M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot(声明面)——单一权威源 =
13
+ // review-facts.mjs 的 resolveReviewDocPaths(2026-10-04 · #921 · KD-2——分类四腿 + B 文案
14
+ // 诊断数据单源;write-gate 同名再出口保 normAbs 面)。不 import dispatch.mjs(簇间回边,环风险)。
15
+ import { normAbs } from "../agent/write-gate.mjs"
16
+ import { resolveReviewDocPaths } from "./review-facts.mjs"
18
17
  import {
19
18
  generateDesignToken,
20
19
  settleDesignReview,
@@ -63,11 +62,11 @@ export const advisorTool = {
63
62
  documents: {
64
63
  type: "array",
65
64
  items: { type: "string" },
66
- description: "Doc paths to review (design / requirements / referenced docs). The advisor reviews ONLY these — no git diff scan. Use for both review types to pass the task's docs list.",
65
+ description: "Doc paths to review (design / requirements / referenced docs). Relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/…, or absolute). The advisor reviews ONLY these — no git diff scan. Use for both review types to pass the task's docs list.",
67
66
  },
68
67
  batchDoc: {
69
68
  type: "string",
70
- description: "Design review only: the in-flight batch record path — validated whenever passed (unreadable ⇒ refused, not ignored); the reviewer then also gets the `batch` write channel to record its findings + verdict. Omit when no batch record is in flight (no write channel — zero regression).",
69
+ description: "Design review only: the in-flight batch record path — validated whenever passed (unreadable ⇒ refused, not ignored); the reviewer then also gets the `batch` write channel to record its findings + verdict. Relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/batches/…, or absolute). Omit when no batch record is in flight (no write channel — zero regression).",
71
70
  },
72
71
  },
73
72
  required: ["type"],
@@ -112,34 +111,36 @@ export const advisorTool = {
112
111
  )
113
112
  }
114
113
 
115
- // Design review: the review scope must be documentation files. Classification
116
- // comes from the manifest-declared review-target roots (M4 write-gate.mjs single
117
- // authority — FR12: no directory-name hardcoding; a project whose docs live
118
- // elsewhere declares them in PROJECT-MANIFEST.json docRoot).
114
+ // Design review: the review scope must be documentation files. Classification =
115
+ // resolveReviewDocPaths (2026-10-04 · #921 · the four-leg single authority in
116
+ // review-facts.mjs): legs ① session-root set / ② owning-project root set keep the
117
+ // legacy behavior verbatim; leg ③ probes candidate project roots for bare
118
+ // project-root-relative forms; leg ④ disambiguates by readable-file uniqueness
119
+ // (fail-closed — criterion=scope-doc-ambiguous when ≥2 candidates are readable).
119
120
  if (reviewType === "design" && documents) {
120
- const roots = resolveReviewTargetPaths(agent).map((r) => r.replace(/[\\/]/g, sep))
121
- // 按用点解析(2026-09-21 之裁收口 · 台账 #828):逐文档按**其所属项目**(最近带档祖先)
122
- // 的 docRoot 判定;无主档文档回退会话根集判定(原行为零变)。
123
- const ownerRootsCache = new Map()
124
- const ownRoots = (abs) => {
125
- const owner = owningProject(abs)
126
- if (!owner) return []
127
- let r = ownerRootsCache.get(owner)
128
- if (!r) {
129
- r = resolveReviewRootsFor(owner).map((x) => x.replace(/[\\/]/g, sep))
130
- ownerRootsCache.set(owner, r)
131
- }
132
- return r
133
- }
134
- const invalidDocs = documents.filter((doc) => {
135
- const n = normAbs(doc, agent.cwd).replace(/[\\/]/g, sep)
136
- const under = (r) => n === r || n.startsWith(r + sep)
137
- return !roots.some(under) && !ownRoots(n).some(under)
138
- })
139
- if (invalidDocs.length > 0) {
121
+ const { resolved, invalid, ambiguous, candidates } = resolveReviewDocPaths(documents, agent.cwd)
122
+ if (invalid.length > 0 || ambiguous) {
140
123
  if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
124
+ if (ambiguous) {
125
+ // 歧义拒(KD-4 独立判据——不混入 not-doc;列全部可读命中——fail-closed 不静默挑一)。
126
+ return withIdentityLine(
127
+ `Advisor: design review document is ambiguous — "${ambiguous.doc}" matches several candidate projects: ${ambiguous.hits.join(", ")}. Pass a project-root-relative or absolute path.`,
128
+ { type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-doc-ambiguous" },
129
+ )
130
+ }
131
+ // not-doc 拒:既有稳定前缀逐字(行首)+ 解析诊断尾(B 面根治——逐文档「解析后绝对
132
+ // 路径 ← 基面 = 会话 cwd」+ 候选项目根提示 cap 5 + 重试指引——纠正「误报因」缺陷)。
133
+ const invalidList = invalid.map((v) => v.doc).join(", ")
134
+ const attemptedList = invalid
135
+ .map((v) => `${v.doc} → ${v.attempted.length > 0 ? v.attempted.join(" | ") : normAbs(v.doc, agent.cwd)}`)
136
+ .join("; ")
137
+ const shown = candidates.slice(0, 5)
138
+ const more = candidates.length - shown.length
139
+ const candidateNote = shown.length > 0
140
+ ? `(candidates: ${shown.join(", ")}${more > 0 ? ` +${more} more` : ""}) — pass project-root-relative (e.g. ${shown[0]}/docs/…) or absolute paths`
141
+ : "— pass project-root-relative or absolute paths"
141
142
  return withIdentityLine(
142
- `Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidDocs.join(", ")}`,
143
+ `Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidList}\nDiagnosis: resolved against session cwd ${agent.cwd} → ${attemptedList}; no candidate project docRoot contains it ${candidateNote}.`,
143
144
  { type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-not-doc" },
144
145
  )
145
146
  }
@@ -134,6 +134,17 @@ export function parkAsyncPending(parent, entry) {
134
134
  pend.push(entry)
135
135
  }
136
136
 
137
+ /**
138
+ * §6.31.5 升级账本写助手(消化账务批):`_unsettledDigests` 追加一行
139
+ * (`{ id, role, failures, report, at }`)。载体吸收(父字段优先 / history 借用——形态同
140
+ * `parkAsyncPending`);行序 = 升级序(读面 = `digest-account.mjs` `unsettledRows`)。
141
+ */
142
+ export function appendUnsettledDigest(parent, row) {
143
+ const existing = carrierField(parent, "_unsettledDigests")
144
+ const list = (parent._unsettledDigests ??= Array.isArray(existing) ? existing : [])
145
+ list.push(row)
146
+ }
147
+
137
148
  // ─── D4 守卫统一(严格版)+ D6 信号兜底单点 ──────────────────────────────────
138
149
 
139
150
  /** D4 settle 守卫统一(escalate 严格版):父侧中止 = 回合 signal aborted(#98 VSC 侧
@@ -161,7 +172,9 @@ export function buildChildSignal(parent, ctx) {
161
172
  * 对子代理对象的引用(`childAgent`/`report`——条目前此从不显式释放,挂起期 = 分钟级驻留)。
162
173
  * 幂等(重入不抛);池内/挂起未消化窗口语义零变(settle 时刻不释放——表 2 候选 2 否决:
163
174
  * status/observe 在窗口内仍读 child 摘要)。三消费点(回合尾收集 / run 起始 pending 注入 /
164
- * 挂起残差注入)在注入完成后调本 helper。
175
+ * 挂起残差注入)——fallback 面(无 `_daSession`)注入完成后调本 helper(逐字沿用);会话态
176
+ * (`_daSession`)注入后改调 `releaseChildHold`(仅释放 `childAgent`——`report` 保留至销账 /
177
+ * 升级;§6.31.4 ∥ KD-DA7)。
165
178
  */
166
179
  export function releaseSettledEntry(entry) {
167
180
  if (!entry || typeof entry !== "object") return
@@ -169,6 +182,16 @@ export function releaseSettledEntry(entry) {
169
182
  entry.report = null
170
183
  }
171
184
 
185
+ /**
186
+ * §6.31.4 投递面(消化账务批 · 2026-10-05 · 台账 #930):会话态注入后**仅**释放 `childAgent`——
187
+ * `report` 保留至销账 / 升级(重投需原文;驻留增量以报告量级 ∥ 重投上限封顶)。fallback 面零调用
188
+ * (既有 `releaseSettledEntry` 逐字沿用——注入即释放)。
189
+ */
190
+ export function releaseChildHold(entry) {
191
+ if (!entry || typeof entry !== "object") return
192
+ entry.childAgent = null
193
+ }
194
+
172
195
  /**
173
196
  * D3 settle 共享 helper——四族 settle 回调公共收尾单点(ASYNC-RESULT-CONTAINER.md
174
197
  * AC1:settle 记账单点,无逐字重复)。流程:
@@ -273,10 +296,11 @@ export function settleAsyncEntry(parent, entry, opts = {}) {
273
296
  parkAsyncPending(parent, entry)
274
297
  pool?.delete(String(entry.id))
275
298
  }
276
- // 发射单点(块到达时点归位批 · #746|§6.8「settle 延迟冻结(两态统一)」):settle 一律 ⟦ev⟧settled
277
- // (区块头恒中间态「done · awaiting digestion」驻留面板);⟦ev⟧done = 消费面补发(消费窗清单单源
278
- // = §6.8)⇒ 冻结 ∥ 归档恒落消费时点——回合运行中零归档触发器(「S 不夹运行中回合」结构不变式)。
279
- ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
299
+ // 发射单点(块到达时点归位批 · #746|§6.8「settle 延迟冻结(两态统一)」):settle 一律 ⟦ev⟧settled
300
+ // (区块头恒中间态「done · awaiting digestion」驻留面板);⟦ev⟧done = 消费面补发(消费窗清单单源
301
+ // = §6.8)⇒ 冻结 ∥ 归档恒落投递时点(消化账务批 · 2026-10-05——归档 ≠ 销账;销账后移见账 = §6.31);
302
+ // 回合运行中零归档触发器(「S 不夹运行中回合」结构不变式)。
303
+ ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
280
304
  }
281
305
  // ④ 公共尾部:settleSeq 递增 + _settle 唤醒(never rejects)+ 唤醒挂起驱动 waiter
282
306
  entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)