@thincoder/core 0.10.2 → 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 (106) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/advisor/citations.mjs +40 -4
  3. package/advisor/compaction.mjs +21 -2
  4. package/advisor/convergence.mjs +1 -1
  5. package/advisor/history.mjs +8 -6
  6. package/advisor/loop.mjs +29 -27
  7. package/advisor/messages.mjs +5 -4
  8. package/advisor/run.mjs +5 -1
  9. package/advisor/timeline.mjs +26 -0
  10. package/advisor.mjs +4 -2
  11. package/agent/completion.mjs +3 -1
  12. package/agent/digest-account.mjs +188 -0
  13. package/agent/dispatch-run.mjs +29 -12
  14. package/agent/family-tools.mjs +11 -11
  15. package/agent/helpers.mjs +8 -0
  16. package/agent/record-results.mjs +10 -1
  17. package/agent/run-stages.mjs +27 -4
  18. package/agent/run-start.mjs +49 -3
  19. package/agent/spawn-child.mjs +2 -1
  20. package/agent/subagent-event.mjs +71 -0
  21. package/agent/suspension.mjs +60 -12
  22. package/agent/turn-loop.mjs +31 -2
  23. package/agent/write-gate.mjs +13 -25
  24. package/agent-tools/advisor-async.mjs +25 -7
  25. package/agent-tools/advisor-settle.mjs +7 -9
  26. package/agent-tools/advisor.mjs +34 -33
  27. package/agent-tools/async-settle.mjs +29 -5
  28. package/agent-tools/batch-lifecycle.mjs +29 -7
  29. package/agent-tools/batch-paths.mjs +22 -2
  30. package/agent-tools/batch-read.mjs +36 -0
  31. package/agent-tools/batch-skeleton.mjs +59 -33
  32. package/agent-tools/batch.mjs +16 -6
  33. package/agent-tools/design-token.mjs +76 -16
  34. package/agent-tools/escalate-async.mjs +4 -2
  35. package/agent-tools/review-facts.mjs +143 -16
  36. package/agent-tools/subagent-actions-query.mjs +13 -6
  37. package/agent-tools/subagent-async.mjs +16 -4
  38. package/agent-tools/subagent-panel.mjs +121 -37
  39. package/agent-tools/subagent-spawn.mjs +1 -1
  40. package/agent-tools/subagent.mjs +5 -3
  41. package/agent-tools/verify.mjs +5 -0
  42. package/agent-tools/zero-write-watch.mjs +77 -0
  43. package/agent.mjs +1 -1
  44. package/auto-think.mjs +26 -2
  45. package/config-presets.mjs +8 -0
  46. package/config.mjs +30 -0
  47. package/conventions.mjs +26 -6
  48. package/declaration.mjs +26 -2
  49. package/embedding.mjs +35 -3
  50. package/escape.mjs +1 -1
  51. package/i18n.mjs +5 -0
  52. package/ledger-cmd.mjs +13 -21
  53. package/ledger-db.mjs +34 -1
  54. package/ledger-migrate.mjs +4 -3
  55. package/ledger-read.mjs +94 -0
  56. package/ledger-surface.mjs +15 -46
  57. package/ledger-tools.mjs +148 -85
  58. package/ledger-variant-notice.mjs +47 -0
  59. package/ledger.mjs +38 -64
  60. package/manifest-schema.mjs +19 -7
  61. package/manifest.mjs +32 -11
  62. package/mcp/transport-http.mjs +52 -4
  63. package/mcp/transport-stdio.mjs +33 -8
  64. package/mcp.mjs +13 -5
  65. package/memory/code-search.mjs +5 -3
  66. package/memory/core.mjs +6 -4
  67. package/memory/docs.mjs +5 -3
  68. package/memory/schema.mjs +2 -0
  69. package/package.json +1 -1
  70. package/prompts/advisor-design.md +3 -2
  71. package/prompts/advisor-round1.md +1 -0
  72. package/prompts/advisor-round2.md +8 -3
  73. package/prompts/advisor-round3.md +7 -2
  74. package/prompts/common.md +2 -2
  75. package/prompts/discipline-normal.md +2 -1
  76. package/prompts/persona-engineering.md +7 -4
  77. package/provider/anthropic.mjs +14 -2
  78. package/provider/core.mjs +10 -4
  79. package/provider/errors.mjs +23 -0
  80. package/provider/google.mjs +31 -4
  81. package/provider/normalize.mjs +57 -0
  82. package/provider/responses-request.mjs +3 -1
  83. package/provider/responses.mjs +38 -4
  84. package/provider/sse.mjs +45 -13
  85. package/proxy.mjs +13 -3
  86. package/session.mjs +13 -2
  87. package/shell-identity.mjs +107 -0
  88. package/stream-destroy.mjs +62 -0
  89. package/tool-docs/advisor.md +1 -1
  90. package/tool-docs/bash.md +1 -1
  91. package/tool-docs/batch.md +1 -1
  92. package/tool-docs/delete.md +7 -5
  93. package/tool-docs/ledger.md +6 -0
  94. package/tool-docs/subagent.md +10 -0
  95. package/tool-docs/verify.md +1 -1
  96. package/tools/bash.mjs +9 -4
  97. package/tools/edit-batch.mjs +7 -2
  98. package/tools/index.mjs +1 -4
  99. package/tools/patch.mjs +8 -2
  100. package/tools/write-path.mjs +6 -4
  101. package/undo-stack.mjs +23 -9
  102. package/tool-docs/ledger_add.md +0 -1
  103. package/tool-docs/ledger_close.md +0 -1
  104. package/tool-docs/ledger_count.md +0 -1
  105. package/tool-docs/ledger_query.md +0 -1
  106. package/tool-docs/ledger_update.md +0 -1
@@ -13,6 +13,11 @@ import { snapshotForUndo } from "../undo-stack.mjs"
13
13
  import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites, markClaimNoted } from "../peer-domains.mjs"
14
14
  import { logToolError, noteExecutedMutation } from "./dispatch-gates.mjs"
15
15
 
16
+ /** console 回显预算(AGENT-LOOP.md §6.4 · #863):采集端 cap = 与 helpers.mjs `TOOL_RESULT_OFFLOAD_LIMIT`
17
+ * 同值(65536 字符)——累计到限停收(超出丢弃);发生丢弃 ⇒ 回显段尾恰一行截断标记(逐字——两路径同款)。 */
18
+ const CONSOLE_CAPTURE_LIMIT = 64 * 1024
19
+ const CONSOLE_TRUNCATED_MARK = "[console truncated at 65536 chars]"
20
+
16
21
  /** 单条 prepared 项的执行(Phase 2 体——原 `dispatch.mjs` runOne 闭包;自由变量 agent ∕ depth ∕
17
22
  * signal ∕ callbacks 经形参代入,语义逐字保持:console 捕获 ∕ routed 短路 ∕ mutation 记账 ∕
18
23
  * 错误收口零改)。 */
@@ -39,6 +44,16 @@ export async function runPreparedItem(item, agent, depth, signal, callbacks) {
39
44
  // 捕获分离(父恢复原始后子的拦截期间父捕获停止、子恢复后父继续)——正确。
40
45
  // 声明在 try 之外:catch 块(异常路径)也要访问(报错前的探查输出回显)。
41
46
  const capturedConsole = []
47
+ // 采集端有界(#863):累计达限停收——超出丢弃并置截断位(段尾恰一行标记的判据;采集端单点)。
48
+ let capturedChars = 0
49
+ let capturedTruncated = false
50
+ const capturedPush = (t) => {
51
+ const room = CONSOLE_CAPTURE_LIMIT - capturedChars
52
+ if (room <= 0) { capturedTruncated = true; return }
53
+ if (t.length + 1 > room) { t = t.slice(0, room - 1); capturedTruncated = true }
54
+ capturedChars += t.length + 1
55
+ capturedConsole.push(t)
56
+ }
42
57
  // LOGGING(LOGGING.md):tool:* 事件——仅真实执行(pre-gate 拦截项在下方早退分支不入事件)。
43
58
  // 参数值永不落盘(NF-L3——工具事件不记 args);child=子代理 id(agent._logId,spawn 时 stamp)。
44
59
  const toolT0 = Date.now()
@@ -72,8 +87,8 @@ export async function runPreparedItem(item, agent, depth, signal, callbacks) {
72
87
  }
73
88
  const origConsoleLog = console.log
74
89
  const origConsoleErr = console.error
75
- console.log = (...a) => capturedConsole.push(a.map(String).join(" "))
76
- console.error = (...a) => capturedConsole.push("[err] " + a.map(String).join(" "))
90
+ console.log = (...a) => capturedPush(a.map(String).join(" "))
91
+ console.error = (...a) => capturedPush("[err] " + a.map(String).join(" "))
77
92
  let rawResult
78
93
  // ctx 对象提升为变量(docs/cli/design/TUI.md §6.8):subagent 阻塞 execute 返回前在 ctx 上留
79
94
  // _subagentKey(relayPrefix 去尾)——runOne 在 execute 返回后读它作 onToolResult
@@ -120,20 +135,22 @@ export async function runPreparedItem(item, agent, depth, signal, callbacks) {
120
135
  if (FILE_MUTATORS.has(toolName) && !raw.startsWith("Error:")) {
121
136
  noteExecutedMutation(agent, item.tool, item.args)
122
137
  }
138
+ // 2026-08-31 工具输出回显 ∥ #863:console 段 ∥ 结果本体拼接先于 offload——并入 64K 判定
139
+ // (超限 ⇒ 落盘 + 预览);采集端发生过丢弃 ⇒ 段尾恰一行截断标记(逐字——错误路径同款)。
140
+ const consoleSegment = capturedConsole.length > 0
141
+ ? `\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}${capturedTruncated ? `\n${CONSOLE_TRUNCATED_MARK}` : ""}`
142
+ : ""
143
+ const spliced = raw + consoleSegment
123
144
  // Multimodal tools keep the raw result (base64 images ride the multimodal
124
- // channel); everything else offloads oversized text to disk. Flag-driven, not
125
- // name-driven (consult P3, 2026-08-30).
126
- const result = item.tool?.multimodal ? raw : await offloadToolResult(raw, item.toolCall.id)
127
- // 2026-08-31:工具执行期间捕获的 console 输出附在结果后回显(模型视野)
128
- const resultWithConsole = capturedConsole.length > 0
129
- ? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
130
- : result
145
+ // channel); everything else offloads oversized text to disk. Flag-driven, not
146
+ // name-driven (consult P3, 2026-08-30).
147
+ const result = item.tool?.multimodal ? spliced : await offloadToolResult(spliced, item.toolCall.id)
131
148
  // R10 L3:冲突 / 认领软提示附在工具结果末尾(模型可见——不阻止写;提示附加成功才落去重标记)
132
149
  const notedOk = Boolean(peerNote) && !raw.startsWith("Error:")
133
150
  if (notedOk) markClaimNoted(agent, peerNote.keys)
134
151
  const resultForModel = notedOk
135
- ? `${resultWithConsole}\n${peerNote.text}`
136
- : resultWithConsole
152
+ ? `${result}\n${peerNote.text}`
153
+ : result
137
154
  callbacks.onToolResult?.(item.toolCall.name, resultForModel, item.toolCall.id, toolCtx._subagentKey)
138
155
  // PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
139
156
  runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
@@ -160,7 +177,7 @@ export async function runPreparedItem(item, agent, depth, signal, callbacks) {
160
177
  const ctx = ctxParts.length > 0 ? ` [${ctxParts.join(", ")}]` : ""
161
178
  // 2026-08-31:异常路径同样回显捕获的 console(工具报错前的探查输出最有价值)
162
179
  const consolePart = capturedConsole.length > 0
163
- ? `\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
180
+ ? `\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}${capturedTruncated ? `\n${CONSOLE_TRUNCATED_MARK}` : ""}`
164
181
  : ""
165
182
  return { ...item, result: `Error: ${error.message}${ctx}${consolePart}`, ok: false }
166
183
  }
@@ -27,9 +27,9 @@ export async function assembleFamilyTools({
27
27
  } = {}) {
28
28
  // CORE-UNIFICATION TOOLS #83:consult 家族随统一登记册自 `../agent-tools.mjs` 取用(单一来源)
29
29
  const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, contextTool, batchTool, consultStartTool, consultStopTool, parentChannelTool } = await import("../agent-tools.mjs")
30
- // 写命令(主 agent 专用)——动态 import 且**仅 depth===0 载入**(ledger 链静态达 node:sqlite——
31
- // W8 契约②;子代理路径不注册 = 零载入——depth>0 解构得空、不引用即无副作用)
32
- const { ledgerAddTool, ledgerUpdateTool, ledgerCloseTool } = depth === 0 ? await import("../ledger.mjs") : {}
30
+ // 台账统一入口(2026-10-05 统一入口批——台账 #923):全量变体 = depth-0 段 ∥ 只读变体 = 全部
31
+ // depth>0 段。动态 import——ledger 链静态达 node:sqlite(W8 契约②);两变体同名单对象、schema 窄化。
32
+ const { ledgerTool, ledgerReadTool } = await import("../ledger.mjs")
33
33
 
34
34
  // withPool: decorate the consult_start description with the CURRENT candidate pool
35
35
  // so the model knows which models it can pick (CLI parity with the plugin). The
@@ -141,9 +141,8 @@ export async function assembleFamilyTools({
141
141
  const depthOnly = depth === 0
142
142
  ? [decorate?.subagent ?? filteredSubagent, skillTool, goalTool, engTool, verifyTool, recentChangesTool, readHistoryTool, contextTool, advisorTool, batchTool(null),
143
143
  ...consultTools,
144
- // 台账写命令(M2——仅主 agent;查询面 ledger_count 住基础集 tools/index.mjs)。
145
- // fail-closed:子代理不挂载 = 写面机械不可达。
146
- ledgerAddTool, ledgerUpdateTool, ledgerCloseTool,
144
+ // 台账全量变体(五 action——仅主 agent 装配;fail-closed:子代理段只挂读变体 = 写面机械不可达)
145
+ ledgerTool,
147
146
  // 端差(decorate.settings——VSC depth-0 主 agent 面;缺省不追加):核默认形态里
148
147
  // settings 住**基础集**(`tools/index.mjs` `assembleBuiltinTools`),端侧自持清单
149
148
  // 无该面 ⇒ 端以 decorate 补位(收敛通道 = 将来去 decorate 项即归核位)。
@@ -168,11 +167,12 @@ export async function assembleFamilyTools({
168
167
  // (未列名 depth>0 role 落同一兜底段 ⇒ 亦装配;语义 =「depth>0 且非 consult 皆装配」);
169
168
  // 计数口径:`consult` 分支不入 ⇒ 「5 个插入点」读法已作废(实读 `thincoder-core/agent/family-tools.mjs:165-169`)。
170
169
  // consult 段不入(其角色语义 = 父发起的一次性会诊,父在其 settle 前不期望中途对话)。
171
- : engChildRole === "eng-coder" ? [parentChannelTool, advisorTool, verifyTool, batchTool(batchDoc), ...(engChildSubagent ? [engChildSubagent] : [])]
172
- : engChildRole === "eng-designer" ? [parentChannelTool, batchTool(batchDoc), ...(engChildSubagent ? [engChildSubagent] : [])]
173
- : role === "coder" ? [parentChannelTool, verifyTool, advisorTool]
174
- : role === "consult" ? [recentChangesTool]
175
- : [parentChannelTool]
170
+ // 台账只读变体(query / count)——全部 depth>0 角色段(含 consult ∕ 兜底;写三 schema 级不可达)
171
+ : engChildRole === "eng-coder" ? [parentChannelTool, ledgerReadTool, advisorTool, verifyTool, batchTool(batchDoc), ...(engChildSubagent ? [engChildSubagent] : [])]
172
+ : engChildRole === "eng-designer" ? [parentChannelTool, ledgerReadTool, batchTool(batchDoc), ...(engChildSubagent ? [engChildSubagent] : [])]
173
+ : role === "coder" ? [parentChannelTool, ledgerReadTool, verifyTool, advisorTool]
174
+ : role === "consult" ? [recentChangesTool, ledgerReadTool]
175
+ : [parentChannelTool, ledgerReadTool]
176
176
 
177
177
  // 固定段(装配序 = agent.tools → 固定段 → 家族段 → extraTools):timer 全模式全深度;
178
178
  // plan 随模式位——ENG-PLAN-EXCLUSION(FR31 ① · KD8 卸载而非「注册 + 报错」· KD11 全深度两端);
package/agent/helpers.mjs CHANGED
@@ -468,6 +468,14 @@ export const UPSTREAM_TURN_DOMAIN =
468
468
  export const TIMER_TURN_DOMAIN =
469
469
  "[System reminder: auto-turn — a timer you set earlier has expired (reminder above). No user message is waiting. Act on what the timer was for now: pull the data or run the check rather than continuing to reason in the abstract. Stay within the work that was in flight when the timer was set; do not open unrelated new work. Wrap up as soon as the action is done or blocked.]"
470
470
 
471
+ /** Digest-accounting requirement line (AGENT-LOOP-ASYNC-POOL.md §6.31.3 · batch digest-accounting ·
472
+ * 2026-10-05):消化轮账目要求——两档同发(手动 / AUTO);manual 域文本只覆盖手动档 ⇒ 要求行
473
+ * 独立常量承载,避免 AUTO 轮无要求。`<ids>` = 未销账 id 清单占位(动态拼 = digest-account.mjs
474
+ * `digestAccountRequirement`);注入面 = transient(机器线独有)。Verbatim from the parent-side
475
+ * final text (single line — no newlines); content authority = parent side. */
476
+ export const DIGEST_ACCOUNT_DOMAIN =
477
+ "[System reminder: digest accounting — for every report listed below as not yet accounted, emit exactly one line: \"[digest-ack #<id>] digested — <key points>\" (handled) or \"[digest-ack #<id>] deferred — <reason>\" (not handled). Not yet accounted: <ids>. Do not omit any.]"
478
+
471
479
  /** Engineering-mode status injection — one reminder on EVERY transition (2026-08-25:
472
480
  * OFF is announced too — the model must know the gates lifted; silence after /eng-off
473
481
  * left it guessing. Covers TUI /eng, resume, and any path bypassing the eng tool.) */
@@ -26,9 +26,12 @@
26
26
  import { pushReal } from "../context.mjs"
27
27
  import { specForModel } from "../config.mjs"
28
28
  import { FILE_MUTATORS, toolTouchPaths } from "./helpers.mjs"
29
+ import { capHistoryImages } from "../provider/normalize.mjs"
29
30
  import { resolve } from "node:path"
30
31
  import { advisorRuns, stripApprovedSuffix } from "../agent-tools/advisor-async.mjs"
31
32
  import { looksLikeReviewOutput, advisorIncompleteMarker } from "../advisor/run.mjs"
33
+ // #949(评审 history 串台面):同步面投递水印单点(叶档——零环)。
34
+ import { noteReviewDelivered } from "../agent-tools/review-facts.mjs"
32
35
 
33
36
  let _reindexFile = null
34
37
 
@@ -117,6 +120,8 @@ export async function recordToolResults(agent, toolByName, results) {
117
120
  } else {
118
121
  const reviewId = agent._advisorSyncCalls?.get(toolCall.id)
119
122
  const run = reviewId !== undefined ? advisorRuns(agent).get(reviewId) : undefined
123
+ // #949:工具结果已入史(上方 pushReal)⇒ 本评审响应表取件下界落定(run 缺省零写)。
124
+ if (run) noteReviewDelivered(agent, run)
120
125
  // F16 同步面(第 13 批——ADVISOR-GUARDS.md §6):未完成尾 + 非设计面
121
126
  // (含类型不可判——run 缺失的 legacy 直调)⇒ 不置「已覆盖」(与 settle 面
122
127
  // 逐条 parity;设计评审保持计入——无代码面)。
@@ -130,7 +135,9 @@ export async function recordToolResults(agent, toolByName, results) {
130
135
  // F2e: the sync settle's engine-approved suffix (stored on the
131
136
  // run by settleDesignReview) is stripped with exact truncation — the
132
137
  // prior never carries the raw token / designId.
133
- if (looksLikeReviewOutput(result)) {
138
+ // #863 关闭点轻量化(父侧批·五裁定㈡——与 settle 面同族守卫):closed 实例零消费面
139
+ // (续跑仅取 open)——关闭后不写回 priorOutput(否则关闭点释放被覆盖)。
140
+ if (run.open && looksLikeReviewOutput(result)) {
134
141
  run.priorOutput = stripApprovedSuffix(result, run.approvedSuffix)
135
142
  }
136
143
  } else {
@@ -174,4 +181,6 @@ export async function recordToolResults(agent, toolByName, results) {
174
181
  if (real) pushReal(agent, msg)
175
182
  else agent.history.push(msg)
176
183
  }
184
+ // #853 D-PR32:注入面同函数同预算——history 本体驱逐最老图片(防无界膨胀;发送前另有副本面同函数)。
185
+ if (deferredUserMsgs.some((d) => d.real)) agent.history = capHistoryImages(agent.history)
177
186
  }
@@ -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
@@ -25,7 +25,8 @@ import { appendCappedText } from "../text-budget.mjs"
25
25
  export { RELAY_PREFIX_RE, parseRelayPath, relayPrefixOf } from "./relay-prefix.mjs"
26
26
 
27
27
  /** 事件 token 哨兵串(D1)——LLM 正常内容混淆概率极低;字段分隔用 RS (\x1e)。 */
28
- export const EVENT_SENTINEL = "⟦ev⟧"
28
+ import { EVENT_SENTINEL } from "./subagent-event.mjs" // 2026-10-04 批(ACP-CLIENT.md §12.4):定义位迁入零依赖叶 `subagent-event.mjs`;再导出 ⇒ 既有 import 面零改(先例 = 同档 `child-marks.mjs`)
29
+ export { EVENT_SENTINEL }
29
30
  const RS = "\x1e"
30
31
 
31
32
  // 报告文本锚点(turn-cap / stopped-by-user)2026-09-20 下沉零依赖叶 `child-marks.mjs`(端壳
@@ -0,0 +1,71 @@
1
+ /**
2
+ * subagent-event.mjs — 子代理事件 token 文法机读单源(`⟦ev⟧<小写名>` + RS 分隔字段)。
3
+ *
4
+ * 收编自 `spawn-child.mjs`(2026-10-04 ACP 协议面补全批——设计档 `docs/cli/design/
5
+ * ACP-CLIENT.md` §12.4):哨兵定义位迁入本叶 + 形态判据与逐事件投影单源——消费方
6
+ * (本批 = ACP 桥 `thincoder-cli/src/acp/bridge.mjs`)经本叶判据剥离/投影,不再自持
7
+ * 平行表达式(同族先例 = `relay-prefix.mjs` / `child-marks.mjs`;`spawn-child.mjs`
8
+ * import + 再导出哨兵 ⇒ 既有 import 面零改)。
9
+ *
10
+ * 零 import——任意层可引、无环。
11
+ *
12
+ * 文法:`⟦ev⟧<小写名>\x1e<字段…>`(字段以 RS 分隔)。终止符约束 load-bearing:正文含
13
+ * `⟦ev⟧` 而无 RS 终止符 ⇒ 非事件形态 ⇒ null ⇒ 消费方照常转发(不吞真实正文)。
14
+ * 逐事件字段序 = 发射侧单源(各发射点既有构造——本批零改发射侧);投影映射以样本逐字
15
+ * 钉住(批内件 T-862-1…6)——发射侧改字段序 = 契约面变更(设计档 §12.3)。
16
+ */
17
+
18
+ /** 事件 token 哨兵串(D1)——LLM 正常内容混淆概率极低;字段分隔用 RS (\x1e)。 */
19
+ export const EVENT_SENTINEL = "⟦ev⟧"
20
+
21
+ /** 形态判据(**单源——唯一表达式**):`⟦ev⟧<小写名>` + RS 终止符同现。
22
+ * D13 负向边界(load-bearing):放宽为无终止符形态会吞真实正文(先例教训见
23
+ * `thincoder-cli/src/tui/render.mjs:250-252`)。形态域 = 开集——未知名同判(客户端
24
+ * SHOULD 容忍未知 state;新增事件名不破坏契约)。 */
25
+ const EVENT_FORM_RE = /^⟦ev⟧([a-z]+)\x1e/
26
+ const RS = "\x1e"
27
+
28
+ /** 数值字段解析(§12.2):无值(缺省 ∥ 空串)⇒ 键省略;`Number` 转换非有限值 ⇒ 键省略
29
+ * (防御面——发射侧恒为整数)。 */
30
+ const num = (v) => {
31
+ if (v == null || v === "") return undefined
32
+ const n = Number(v)
33
+ return Number.isFinite(n) ? n : undefined
34
+ }
35
+
36
+ /**
37
+ * 解析子代理事件 token(**形态命中必非 null**——剥离判据与投影载荷由此同一调用决定)。
38
+ * @param {string} text 事件 token 原文(relay 前缀已由消费方剥离——前缀解析非本叶职责)
39
+ * @returns {{ name: string, progress?: { turn?: number, maxTurns?: number, position?: number }, detail?: string } | null}
40
+ * null = 非事件形态(消费方照常转发)
41
+ */
42
+ export function parseSubagentEvent(text) {
43
+ const m = EVENT_FORM_RE.exec(String(text))
44
+ if (!m) return null
45
+ const name = m[1]
46
+ const fields = String(text).slice(m[0].length).split(RS)
47
+ // 逐态投影(字段序 = 发射侧单源——样本映射见设计档 §12.3):
48
+ // turn / approval ⟦ev⟧turn\x1e{turn}\x1e{maxTurns}\x1e{phase}\x1e[detail]
49
+ // queued ⟦ev⟧queued\x1e{kind}\x1e{position}\x1equeued\x1e[detail](kind 不承载)
50
+ // async / cancelled / stopped / settled / done = 零字段态(stopped 等第 2/3 位 0 系
51
+ // 发射侧填充,不承载——state 逐字即全部载荷)。
52
+ if (name === "turn" || name === "approval") {
53
+ const ev = { name }
54
+ const progress = {}
55
+ const turn = num(fields[0])
56
+ const maxTurns = num(fields[1])
57
+ if (turn !== undefined) progress.turn = turn
58
+ if (maxTurns !== undefined) progress.maxTurns = maxTurns
59
+ if (Object.keys(progress).length > 0) ev.progress = progress
60
+ if (name === "approval" && fields[3]) ev.detail = fields[3]
61
+ return ev
62
+ }
63
+ if (name === "queued") {
64
+ const ev = { name }
65
+ const position = num(fields[1])
66
+ if (position !== undefined) ev.progress = { position }
67
+ if (fields[3]) ev.detail = fields[3]
68
+ return ev
69
+ }
70
+ return { name } // 零字段态 ∥ 未知名(形态域开集)
71
+ }
@@ -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
  *
@@ -29,7 +32,8 @@
29
32
  * - abortSignal 会话中止信号(兜底监听 + abort 清场判据)
30
33
  * - hooks.onCounts(counts) 计数变化通知({ running, queued, pending, done })
31
34
  * - hooks.onDigest(phase, counts) 消化轮边界(start / end)
32
- * - hooks.reclaim(consumed) 消化完成条目逐条回收(不等池空)
35
+ * - hooks.reclaim(consumed) 回收(消化完成 ∥ 残差离容 ∥ 丢弃三类离容逐条补发 done——
36
+ * 恒达窗:正常 ∥ Abort-continue ∥ 异常 ∥ 退出残差四窗同达)
33
37
  * - hooks.freezeAll() 退出冻结(兜底残项)
34
38
  * - injectResidual(entry) 残余注入面(idle 退出清场;宿主装配端注入器——CLI =
35
39
  * 逐族注入分发 / VSC = pending 单容器注入器)
@@ -111,7 +115,16 @@ function consultLiveCount(carrier) {
111
115
  * - abort:**只清已死**(§6.20 口径——增补 D:`discardAbortedPool` ∕ `discardAbortedAdvisors`
112
116
  * 单点,与核 run-stages 回合尾中止同式;存活 ∕ 已 settle 条目留池——settled 报告沿后续
113
117
  * 回合边界注入到达,不静默丢)+ pending 单容器清 + 会诊会话清理标记;
114
- * - idle:残余直注入(极端竞态残项——结果零丢失;注入器由宿主注入,缺省 no-op 保残项)。
118
+ * - idle:残余直注入(极端竞态残项——结果零丢失;注入器由宿主注入,缺省 no-op 保残项);
119
+ * **已投递未销账条(`_daDelivered === true`)按投递账过滤**——内容已在历史(投递起跑窗):
120
+ * 不注入且留容器(消化账务批出口缝 · #41 披露① · 父侧裁定 2026-10-05——状态面可见 / 可续账)。
121
+ *
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 ∥ 无注入器 ∥ 无残差 ⇒ 三键空值。
127
+ * 既有 C3/C4 快照径(中止冻结快照)零改。
115
128
  */
116
129
  export async function finishSuspension(carrier, { aborted = false, injectResidual = null } = {}) {
117
130
  if (aborted) {
@@ -120,16 +133,33 @@ export async function finishSuspension(carrier, { aborted = false, injectResidua
120
133
  discardAbortedAdvisors(carrier)
121
134
  discardAbortedBgTasks(carrier) // #9 收尾档②:后台 bash 任务逐条杀树 + 出池 + 墓碑(杀 ⟺ 控制器已中止)
122
135
  carrier._pendingAsyncResults = []
136
+ carrier._unsettledDigests = [] // §6.31.5 清账面(消化账务批):会话中止 ⇒ 升级账本随 pending 一并清(旧账不续)
123
137
  if (carrier._consultSessions instanceof Map) {
124
138
  const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
125
139
  cleanupConsultSessions(carrier)
126
140
  }
127
- return
141
+ return { injected: [], left: [], error: null }
128
142
  }
129
143
  const residual = carrier._pendingAsyncResults
130
- if (residual?.length && typeof injectResidual === "function") {
131
- for (const e of residual.splice(0)) await injectResidual(e)
144
+ if (!residual?.length || typeof injectResidual !== "function") return { injected: [], left: [], error: null }
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
157
+ const injected = []
158
+ for (const e of left) {
159
+ try { await injectResidual(e) } catch (error) { return { injected, left, error } } // 首错收口(前缀 = injected)
160
+ injected.push(e)
132
161
  }
162
+ return { injected, left, error: null }
133
163
  }
134
164
 
135
165
  /** 等待下一次 settle(池 waiter——核内异步面 settle 尾部唤醒)/ 宿主唤醒(wake——用户输入)/
@@ -179,7 +209,9 @@ function waitForSettleOrWake(carrier, abortSignal, latch, { deadline = null, tim
179
209
  * 注入由宿主 runTurn 首行完成——单注入点);
180
210
  * 3. 池空(无 running / queued / 未注入)→ 自然退出;
181
211
  * 4. 等下一 settle / 宿主唤醒(handle.wake)/ timer 到期(opt-in——兑现真 ⇒ timer 轮)。
182
- * 每轮消化 / 用户回合后:hooks.reclaim(consumed) 逐条回收(不等池空)。
212
+ * 每轮消化 / 用户回合后:hooks.reclaim(consumed) 逐条回收(不等池空)——恒达窗形(子代理面板批 ·
213
+ * 2026-10-04):正常 ∥ Abort-continue ∥ 异常抛三径同达,退出残差另经 `finishSuspension` 返回面补发
214
+ * (「消费 ⇒ 必发 done」不变量;发射不去重,归档/记录恰一次住幂等层)。
183
215
  */
184
216
  export function startSuspension(ctx) {
185
217
  const {
@@ -203,6 +235,9 @@ export function startSuspension(ctx) {
203
235
 
204
236
  const done = (async () => {
205
237
  carrier._suspended = true
238
+ // §6.31.2 会话标记(消化账务批 · 2026-10-05 · 台账 #930):挂起会话期 true——投递留容器 /
239
+ // 见账 / 重投回路的唯一判据;退出(finally)置 false ⇒ 会话外回 fallback 语义。
240
+ carrier._daSession = true
206
241
  hooks.onCounts?.(backgroundCounts(carrier))
207
242
  let aborted = false
208
243
  try {
@@ -221,8 +256,10 @@ export function startSuspension(ctx) {
221
256
  await runTurn(item)
222
257
  } finally {
223
258
  carrier._suspended = true
259
+ // 回收恒达窗(子代理面板批 · 2026-10-04 · §2.2):正常 ∥ 异常 ∥ 中止三径同达
260
+ // (「消费 ⇒ 必发 done」不变量)。
261
+ hooks.reclaim?.(afterRun())
224
262
  }
225
- hooks.reclaim?.(afterRun())
226
263
  hooks.onCounts?.(backgroundCounts(carrier))
227
264
  continue
228
265
  }
@@ -242,9 +279,12 @@ export function startSuspension(ctx) {
242
279
  continue
243
280
  }
244
281
  throw e
282
+ } finally {
283
+ // 回收恒达窗(子代理面板批 · 2026-10-04 · §2.2):正常 ∥ Abort-continue ∥ 异常抛三径同达;
284
+ // 中止径 catch 的 `onDigest("end")` 先行(continue 触 finally——既有语义序保持)。
285
+ hooks.reclaim?.(afterRun())
245
286
  }
246
287
  hooks.onDigest?.("end", backgroundCounts(carrier))
247
- hooks.reclaim?.(afterRun())
248
288
  hooks.onCounts?.(backgroundCounts(carrier))
249
289
  continue
250
290
  }
@@ -267,8 +307,10 @@ export function startSuspension(ctx) {
267
307
  // 中止路径零边界、不补发轮后序钩子;池空 / pending 空自然退出——与消化支同判,§6.30.10)。
268
308
  if (e?.name === "AbortError" && !abortSignal?.aborted) continue
269
309
  throw e
310
+ } finally {
311
+ // 回收恒达窗(子代理面板批 · 2026-10-04 · §2.2):正常 ∥ Abort-continue ∥ 异常抛三径同达。
312
+ hooks.reclaim?.(afterRun())
270
313
  }
271
- hooks.reclaim?.(afterRun())
272
314
  hooks.onCounts?.(backgroundCounts(carrier))
273
315
  continue
274
316
  }
@@ -277,13 +319,19 @@ export function startSuspension(ctx) {
277
319
  if (!finished) {
278
320
  finished = true
279
321
  carrier._suspended = false
322
+ carrier._daSession = false // §6.31.2 会话标记复位(退出即回 fallback 语义)
280
323
  latch.wake = null
281
324
  // §2.3 退出清场:abort 判定与实时信号合并(两端同式——Stop 落在 runTurn 内时
282
325
  // abort 以 AbortError 抛出、循环顶检查不会再执行——不合并会把清场误走 idle 注入)。
283
326
  const abortedNow = aborted || Boolean(abortSignal?.aborted)
284
327
  aborted = abortedNow
285
- await finishSuspension(carrier, { aborted: abortedNow, injectResidual })
328
+ const flushed = await finishSuspension(carrier, { aborted: abortedNow, injectResidual })
329
+ // 残差离容补发(子代理面板批 · 2026-10-04 · §2.2——C2 零发射口):补发面 = 离容全量
330
+ // (抛错径同覆);与 freezeAll 双探针幂等(归档/记录恰一次住幂等层)。
331
+ hooks.reclaim?.(flushed.left)
286
332
  hooks.freezeAll?.()
333
+ // 注入器首错重抛(fail-loud 保持——清场两探针已行毕)。
334
+ if (flushed.error != null) throw flushed.error
287
335
  }
288
336
  }
289
337
  return { reason: aborted ? "aborted" : "idle", residualInput: pendingInput.splice(0) }