@thincoder/core 0.9.5 → 0.10.2

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 (216) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +29 -12
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-ref.mjs +96 -3
  119. package/model-specs.mjs +16 -7
  120. package/notify-policy.mjs +51 -0
  121. package/package.json +1 -1
  122. package/peer-claims.mjs +2 -2
  123. package/peer-domains.mjs +7 -11
  124. package/peer-instances.mjs +9 -10
  125. package/process-probe-exec.mjs +187 -0
  126. package/process-probe.mjs +27 -175
  127. package/prompt-overlays.mjs +11 -10
  128. package/prompts/advisor-design.md +2 -2
  129. package/prompts/advisor-round2.md +1 -1
  130. package/prompts/advisor-round3.md +1 -1
  131. package/prompts/common.md +16 -8
  132. package/prompts/discipline-engineering.md +57 -16
  133. package/prompts/discipline-normal.md +6 -3
  134. package/prompts/persona-coder.md +0 -1
  135. package/prompts/persona-eng-coder.md +1 -5
  136. package/prompts/persona-eng-designer.md +2 -3
  137. package/prompts/persona-engineering.md +32 -11
  138. package/prompts/persona-explore.md +0 -1
  139. package/prompts/persona-normal.md +1 -1
  140. package/prompts/persona-plan.md +3 -4
  141. package/prompts/subagent-base.md +14 -0
  142. package/provider/core.mjs +11 -48
  143. package/provider/list-models.mjs +3 -0
  144. package/provider/responses-request.mjs +237 -0
  145. package/provider/responses.mjs +6 -228
  146. package/provider-flows.mjs +248 -0
  147. package/queued.mjs +90 -0
  148. package/rules.mjs +209 -2
  149. package/session-guard.mjs +27 -7
  150. package/session-lifecycle.mjs +108 -27
  151. package/session-rename.mjs +5 -1
  152. package/session-slot-claims.mjs +230 -0
  153. package/session-slot-scan.mjs +299 -0
  154. package/session-slot-verify.mjs +304 -0
  155. package/session-slot-write.mjs +74 -8
  156. package/session-slots-manifest.mjs +163 -241
  157. package/session-slots.mjs +99 -71
  158. package/session.mjs +30 -10
  159. package/shell-candidates.mjs +78 -0
  160. package/text-budget.mjs +2 -2
  161. package/think-off.mjs +49 -0
  162. package/token-ttl.mjs +15 -4
  163. package/tool-args.mjs +73 -0
  164. package/tool-docs/advisor.md +9 -0
  165. package/tool-docs/batch.md +1 -0
  166. package/tool-docs/code_search.md +1 -0
  167. package/tool-docs/consult_start.md +5 -0
  168. package/tool-docs/consult_stop.md +3 -0
  169. package/tool-docs/context.md +7 -0
  170. package/tool-docs/doc_search.md +1 -0
  171. package/tool-docs/edit.md +2 -13
  172. package/tool-docs/eng.md +1 -0
  173. package/tool-docs/execute.md +1 -9
  174. package/tool-docs/git.md +25 -51
  175. package/tool-docs/goal.md +6 -0
  176. package/tool-docs/ledger_add.md +1 -0
  177. package/tool-docs/ledger_close.md +1 -0
  178. package/tool-docs/ledger_count.md +1 -0
  179. package/tool-docs/ledger_query.md +1 -0
  180. package/tool-docs/ledger_update.md +1 -0
  181. package/tool-docs/main_history.md +1 -0
  182. package/tool-docs/memory.md +10 -0
  183. package/tool-docs/notify_parent.md +9 -0
  184. package/tool-docs/peer_instances.md +1 -0
  185. package/tool-docs/plan.md +5 -0
  186. package/tool-docs/process.md +7 -3
  187. package/tool-docs/read_history.md +7 -0
  188. package/tool-docs/recent_changes.md +1 -0
  189. package/tool-docs/repo_outline.md +1 -0
  190. package/tool-docs/settings.md +6 -0
  191. package/tool-docs/skill.md +5 -0
  192. package/tool-docs/subagent.md +31 -0
  193. package/tool-docs/task.md +7 -0
  194. package/tool-docs/timer.md +6 -0
  195. package/tool-docs/verify.md +1 -0
  196. package/tool-docs/wait_for.md +2 -1
  197. package/tools/bash.mjs +28 -4
  198. package/tools/edit-batch.mjs +4 -4
  199. package/tools/edit-diff.mjs +4 -23
  200. package/tools/exec-run.mjs +102 -4
  201. package/tools/execute.mjs +5 -2
  202. package/tools/file.mjs +4 -9
  203. package/tools/git-run.mjs +1 -1
  204. package/tools/git.mjs +0 -24
  205. package/tools/index.mjs +2 -1
  206. package/tools/linter.mjs +2 -1
  207. package/tools/node-child.mjs +21 -0
  208. package/tools/ops.mjs +50 -13
  209. package/tools/patch.mjs +4 -2
  210. package/tools/repomap.mjs +20 -9
  211. package/tools/shared.mjs +8 -6
  212. package/tools/web.mjs +5 -2
  213. package/tools/write-path.mjs +1 -1
  214. package/traces/trace-store.mjs +17 -14
  215. package/vision-reader.mjs +76 -0
  216. package/agent-tools/batch-segment.mjs +0 -18
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * 载体注入(端差面 = 池 / pending / 标志挂谁):`ctx.carrier` = 字段集按核内异步面
11
11
  * 现行口径的对象(`_asyncSubagents` · `_asyncAdvisors` · `_consultSessions` ·
12
- * `_pendingAsyncResults` · `_suspended`)——CLI 形 = `agent` 对象;VSC 形 = depth-0
12
+ * `_pendingAsyncResults` · `_suspended`;#9 后台 bash 任务池 `_bgTasks` 同列)
13
+ * ——CLI 形 = `agent` 对象;VSC 形 = depth-0
13
14
  * `history` 数组(附加属性不污染会话文件)。机制对载体零预设(空字段一律 `?.` 读)。
14
15
  *
15
16
  * 核内零文案、零渲染、零端名分支(契约 5 / 10):状态行文本 / 提示行 / 消息族全在宿主侧,
@@ -18,7 +19,13 @@
18
19
  * ctx 接口(注入面):
19
20
  * - carrier 池 / pending / 标志载体(见上)
20
21
  * - runTurn(text, opts) 回合执行器(digest = `("", { autoTurn: true })`;上行唤醒轮 =
21
- * `("", { autoTurn: true, upstreamTurn: true })`——旗标仅供域文本选择)
22
+ * `("", { autoTurn: true, upstreamTurn: true })`;timer 轮 =
23
+ * `("", { autoTurn: true, timerTurn: true })`——旗标仅供域文本选择)
24
+ * - inputQueue **opt-in** 宿主既有输入队列数组(增补 C):驱动就地消费该数组——宿主
25
+ * 其余入队 ∕ 读取面与驱动同一数组(零同步面);缺省核内新建(既有宿主零变)
26
+ * - takeInput(queue) **opt-in** 取项缝(增补 C):批次策略(富条目 / 合并批 / 消费回执行)
27
+ * 归宿主;缺省 `shift`;返回 null/undefined ⇒ 本步零动作、落第 2 步
28
+ * (首动作不可消费的防御面——条目不消费留队)。可异步(宿主取项含动态 import)
22
29
  * - abortSignal 会话中止信号(兜底监听 + abort 清场判据)
23
30
  * - hooks.onCounts(counts) 计数变化通知({ running, queued, pending, done })
24
31
  * - hooks.onDigest(phase, counts) 消化轮边界(start / end)
@@ -26,6 +33,11 @@
26
33
  * - hooks.freezeAll() 退出冻结(兜底残项)
27
34
  * - injectResidual(entry) 残余注入面(idle 退出清场;宿主装配端注入器——CLI =
28
35
  * 逐族注入分发 / VSC = pending 单容器注入器)
36
+ * - timerFace **opt-in** timer 面(§6.30.10):`{ deadline(), deliver() }`——`deadline()`
37
+ * 每轮步骤 4 现算(number|null;缺省 / null ⇒ 零注册,等待仍三态);
38
+ * `deliver()` 到期兑现面(返回布尔 = 是否已交付;交付真 ⇒ timer 轮)。
39
+ * 缺省不传 ⇒ 既有行为逐字等价(现宿主零传)。
40
+ * - timer / clear 等待原语注入缝(缺省 `setTimeout` / `clearTimeout`——用例零真实等待)
29
41
  */
30
42
 
31
43
  import { parkAsyncPending } from "../agent-tools/async-settle.mjs"
@@ -42,11 +54,13 @@ function consultRunningChildren(carrier) {
42
54
 
43
55
  /** 后台池存活判据(D-S2/F5 口径——CLI 为准):running/queued 的池条目,或已 settle 未注入
44
56
  * (pending 非空 = D-S3「未注入」),或运行中 consult children(会诊跨回合)。回合尾与每次
45
- * 轮末都评估退出。 */
57
+ * 轮末都评估退出。**#9 三域并入**:`_bgTasks`(后台 bash 任务)在途亦为 live——settle 经
58
+ * pending 停靠驱动消化轮(`bash-async.mjs` 恒停靠)。 */
46
59
  export function poolLive(carrier) {
47
60
  const sub = carrier?._asyncSubagents
48
61
  const adv = carrier?._asyncAdvisors
49
- return (sub && sub.size > 0) || (adv && adv.size > 0)
62
+ const bg = carrier?._bgTasks
63
+ return (sub && sub.size > 0) || (adv && adv.size > 0) || (bg && bg.size > 0)
50
64
  || (carrier?._pendingAsyncResults?.length ?? 0) > 0
51
65
  || consultRunningChildren(carrier) > 0
52
66
  }
@@ -94,13 +108,17 @@ function consultLiveCount(carrier) {
94
108
 
95
109
  /**
96
110
  * 退出清场(单点——驱动 finally 与单测共用):
97
- * - abort:清池不注入(用户显式停——陈旧结果不回灌)+ 会诊会话清理标记;
111
+ * - abort:**只清已死**(§6.20 口径——增补 D:`discardAbortedPool` ∕ `discardAbortedAdvisors`
112
+ * 单点,与核 run-stages 回合尾中止同式;存活 ∕ 已 settle 条目留池——settled 报告沿后续
113
+ * 回合边界注入到达,不静默丢)+ pending 单容器清 + 会诊会话清理标记;
98
114
  * - idle:残余直注入(极端竞态残项——结果零丢失;注入器由宿主注入,缺省 no-op 保残项)。
99
115
  */
100
116
  export async function finishSuspension(carrier, { aborted = false, injectResidual = null } = {}) {
101
117
  if (aborted) {
102
- carrier._asyncSubagents?.clear()
103
- carrier._asyncAdvisors?.clear()
118
+ const { discardAbortedPool, discardAbortedAdvisors, discardAbortedBgTasks } = await import("../agent-tools/async-discard.mjs")
119
+ discardAbortedPool(carrier)
120
+ discardAbortedAdvisors(carrier)
121
+ discardAbortedBgTasks(carrier) // #9 收尾档②:后台 bash 任务逐条杀树 + 出池 + 墓碑(杀 ⟺ 控制器已中止)
104
122
  carrier._pendingAsyncResults = []
105
123
  if (carrier._consultSessions instanceof Map) {
106
124
  const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
@@ -114,13 +132,19 @@ export async function finishSuspension(carrier, { aborted = false, injectResidua
114
132
  }
115
133
  }
116
134
 
117
- /** 等待下一次 settle(池 waiter——核内异步面 settle 尾部唤醒)或宿主唤醒(wake——用户输入)。
118
- * 双路单次兑现(settle / wake / aborted 三态,先到先得);cleanup 摘除全部注册。 */
119
- function waitForSettleOrWake(carrier, abortSignal, latch) {
135
+ /** 等待下一次 settle(池 waiter——核内异步面 settle 尾部唤醒)/ 宿主唤醒(wake——用户输入)/
136
+ * **timer 到期**(opt-in——§6.30.10 等待第四态,与 settle / wake 同槽先到先得)。
137
+ * `deadline != null` ⇒ 一次性注册(`unref()` 尽力——不阻断宿主退出);缺省 / null ⇒ 三态逐字等价。
138
+ * cleanup 摘除全部注册(含未触发句柄——尽力清);到期兑现交循环支(本原语零交付语义)。 */
139
+ function waitForSettleOrWake(carrier, abortSignal, latch, { deadline = null, timer = setTimeout, clear = clearTimeout } = {}) {
120
140
  return new Promise((resolve) => {
121
141
  let finished = false
142
+ let handle = null
122
143
  const cleanup = () => {
123
144
  latch.wake = null
145
+ const h = handle
146
+ handle = null
147
+ if (h !== null) { try { clear(h) } catch { /* 已触发 / 不可清——尽力面 */ } }
124
148
  const i = (carrier._asyncWaiters ?? []).indexOf(onSettle)
125
149
  if (i >= 0) carrier._asyncWaiters.splice(i, 1)
126
150
  abortSignal?.removeEventListener("abort", onAbort)
@@ -133,8 +157,14 @@ function waitForSettleOrWake(carrier, abortSignal, latch) {
133
157
  }
134
158
  const onSettle = () => finish("settle")
135
159
  const onAbort = () => finish("aborted")
160
+ const onTimer = () => finish("timer")
136
161
  ;(carrier._asyncWaiters ??= []).push(onSettle)
137
162
  latch.wake = () => finish("wake")
163
+ // opt-in deadline(§6.30.10 等待第四态):无在途 / 未传 ⇒ 零注册(等待仍三态)。宿主不变式 = 到期即出列 / 关即 null(否则「已到期 ∧ 交付假」会 0ms 反复重注册)。
164
+ if (deadline != null) {
165
+ handle = timer(onTimer, Math.max(0, deadline - Date.now()))
166
+ try { handle?.unref?.() } catch { /* unref 失败不阻断 */ }
167
+ }
138
168
  if (abortSignal?.aborted) { onAbort(); return }
139
169
  abortSignal?.addEventListener("abort", onAbort, { once: true })
140
170
  })
@@ -143,17 +173,23 @@ function waitForSettleOrWake(carrier, abortSignal, latch) {
143
173
  /**
144
174
  * 启动挂起会话(同步返回句柄;宿主 `await handle.done` ⇒ `{ reason, residualInput }`)。
145
175
  *
146
- * 状态机行表(承 AGENT-LOOP.md §9/§7 行表):
176
+ * 状态机行表(承 AGENT-LOOP-ASYNC-POOL.md §6.8 行表):
147
177
  * 1. 用户输入优先(D-S5):pendingInput 非空 → 以该消息开普通回合(`_suspended=false`);
148
178
  * 2. pending 非空 或 存在未 drain 的 ask(`upstreamWaiting`)→ 合并消化轮 / 唤醒轮(auto-turn;
149
179
  * 注入由宿主 runTurn 首行完成——单注入点);
150
180
  * 3. 池空(无 running / queued / 未注入)→ 自然退出;
151
- * 4. 等下一 settle / 宿主唤醒(handle.wake)。
181
+ * 4. 等下一 settle / 宿主唤醒(handle.wake)/ timer 到期(opt-in——兑现真 ⇒ timer 轮)。
152
182
  * 每轮消化 / 用户回合后:hooks.reclaim(consumed) 逐条回收(不等池空)。
153
183
  */
154
184
  export function startSuspension(ctx) {
155
- const { carrier, runTurn, abortSignal = null, hooks = {}, injectResidual = null } = ctx
156
- const pendingInput = []
185
+ const {
186
+ carrier, runTurn, abortSignal = null, hooks = {}, injectResidual = null,
187
+ timerFace = null, timer = setTimeout, clear = clearTimeout,
188
+ inputQueue = null, takeInput = null,
189
+ } = ctx
190
+ // 输入队列(增补 C):宿主既有数组(VSC ∕ CLI 形——宿主其余读者与驱动同一数组)优先;
191
+ // 缺省核内新建(desktop 形——经 `pushInput` 入槽)。
192
+ const pendingInput = Array.isArray(inputQueue) ? inputQueue : []
157
193
  const latch = { wake: null }
158
194
  let finished = false
159
195
 
@@ -173,19 +209,23 @@ export function startSuspension(ctx) {
173
209
  while (true) {
174
210
  if (abortSignal?.aborted) { aborted = true; break }
175
211
  sweepSettledToPending(carrier)
176
- // 1. 用户输入优先:单槽(至多一条待交接——宿主在 busy 期拒收提交)。
212
+ // 1. 用户输入优先(D-S5):队列非空 → 取项缝(增补 C——缺省 `shift`;富条目 ∕ 合并批
213
+ // 由宿主 `takeInput` 实现)开普通回合(`_suspended=false`);首动作不可消费
214
+ // (`takeInput ⇒ null`)⇒ 本步零动作、条目不消费,落第 2 步。
177
215
  if (pendingInput.length > 0) {
178
- const head = String(pendingInput.shift())
179
- const afterRun = consumedByRun()
180
- carrier._suspended = false // 用户回合 = 普通回合语义(settle 即冻结 + 回合尾直注入)
181
- try {
182
- await runTurn(head)
183
- } finally {
184
- carrier._suspended = true
216
+ const item = takeInput ? await takeInput(pendingInput) : pendingInput.shift()
217
+ if (item != null) {
218
+ const afterRun = consumedByRun()
219
+ carrier._suspended = false // 用户回合 = 普通回合语义(settle 即冻结 + 回合尾直注入)
220
+ try {
221
+ await runTurn(item)
222
+ } finally {
223
+ carrier._suspended = true
224
+ }
225
+ hooks.reclaim?.(afterRun())
226
+ hooks.onCounts?.(backgroundCounts(carrier))
227
+ continue
185
228
  }
186
- hooks.reclaim?.(afterRun())
187
- hooks.onCounts?.(backgroundCounts(carrier))
188
- continue
189
229
  }
190
230
  // 2. pending 非空 或 未 drain 的 ask → 合并消化轮 / 上行唤醒轮(单注入点由 runTurn
191
231
  // 首行完成:pending 经 run 起始注入器,ask 经回合头 `drainChildUpstream`)。
@@ -210,9 +250,28 @@ export function startSuspension(ctx) {
210
250
  }
211
251
  // 3. 池空 → 自然退出。
212
252
  if (!poolLive(carrier)) break
213
- // 4. 等下一 settle / 宿主唤醒。
214
- const why = await waitForSettleOrWake(carrier, abortSignal, latch)
253
+ // 4. 等下一 settle / 宿主唤醒 / timer 到期(opt-in——§6.30.10 等待第四态;deadline 每轮现算)。
254
+ const why = await waitForSettleOrWake(carrier, abortSignal, latch, {
255
+ deadline: timerFace?.deadline?.() ?? null, timer, clear,
256
+ })
215
257
  if (why === "aborted") { aborted = true; break }
258
+ // 4b. 窗内到期兑现(§6.30.10 循环兑现支):交付真 ⇒ timer 轮(轮后序同消化轮形 = reclaim +
259
+ // onCounts 两钩,不发 digest 边界);交付面契约 = 同步 · 严格布尔(`=== true`——非布尔返值
260
+ // 即静默零轮);池空窗退交宿主空闲闩(到期件已出列——零重复投递);交付假 ⇒ 循环重入。
261
+ if (why === "timer" && timerFace?.deliver?.() === true) {
262
+ const afterRun = consumedByRun()
263
+ try {
264
+ await runTurn("", { autoTurn: true, timerTurn: true })
265
+ } catch (e) {
266
+ // timer 轮自身的回合级中止不是会话停止——容纳并重入循环(timer 轮不发 digest 边界 ⇒
267
+ // 中止路径零边界、不补发轮后序钩子;池空 / pending 空自然退出——与消化支同判,§6.30.10)。
268
+ if (e?.name === "AbortError" && !abortSignal?.aborted) continue
269
+ throw e
270
+ }
271
+ hooks.reclaim?.(afterRun())
272
+ hooks.onCounts?.(backgroundCounts(carrier))
273
+ continue
274
+ }
216
275
  }
217
276
  } finally {
218
277
  if (!finished) {
@@ -231,8 +290,9 @@ export function startSuspension(ctx) {
231
290
  })()
232
291
 
233
292
  return {
234
- /** 挂起空闲期入槽(单槽语义:宿主 busy 期拒收;至多一条待交接)。 */
235
- pushInput(msg) { pendingInput.push(String(msg)) },
293
+ /** 挂起空闲期入槽(宿主 busy 期拒收——条数策略归宿主)。**保留原值**(增补 C:富条目
294
+ * ——VSC ∕ CLI 载体对象;消费面 = 宿主 `takeInput`;`String()` 化归宿主需要时自做)。 */
295
+ pushInput(msg) { pendingInput.push(msg) },
236
296
  /** 宿主唤醒(用户输入落槽后 / 宿主 settle 回调路径——settle 由池 waiter 另一路唤醒)。 */
237
297
  wake() { latch.wake?.() },
238
298
  done,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * agent/timers.mjs — timer 到期件(载体①:在途 timer 的单一权威读面)+ 空闲唤醒机制(闩 ∕ 派发 ∕ 火策略)。
3
+ *
4
+ * 机制单源 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.30.2(载体定形 D-TW1):
5
+ * 读面三件纯函数住本档——最近到期时点 / 到期出列(幂等)/ 历史注入单点。消费面三处共用
6
+ * 同一读点(零第二份过滤语义):① 回合边界轮询(`agent/post-turn.mjs`——行为零变);
7
+ * ② 空闲闩 deadline;③ 挂起窗 deadline;显示面(状态行标记 / `/timers`)同读源。
8
+ *
9
+ * 载体仍在 agent 字段 `_pendingTimers`(条目形 `{ id, expiresAt, message }`)——**唯一写点** =
10
+ * timer 工具 `thincoder-core/agent-tools/timer.mjs`;跨 run 存活 = 规范语义(D-TW3,无 per-run 复位)。
11
+ *
12
+ * 空闲唤醒族(2026-09-28 流程批 R4 **上提核件** · 纯搬 + 转口 · 零语义改;上提源 = VSC
13
+ * `thincoder-vscode/src/extension/timer-watch.mjs:46` ∕ `:73` ∕ `:94`——as-of 2026-09-28 R4 读数;两端现址(B3 收编后,实读 2026-09-29)=
14
+ * VSC `:63`(闩 ∕ 装配)· `:29`(派发)· `:48`(火)∥ CLI `:41`(闩转口)· `:34`(派发转口)· `:57`(火)):`createTimerWatch` = 一次性最近截止闩
15
+ * (起 ∕ 停 ∕ 重臂 ∕ `unref`)· `deliverExpiredTimers` = 到期批派发(出列幂等 ⇒ 注入单点 ⇒ 落流缝)·
16
+ * `fireTimerWake` = 火策略(非空闲零动作 ∕ 零交付零开轮)。端面表壳留端(桌面键面 ∕ `ev:timer` 出词;
17
+ * VSC ∕ CLI 自持副本已随 B3 迁移轮退场(2026-09-29——双写窗口收口),本档 = 单源)。
18
+ */
19
+
20
+ /** 在途条数帽(§6.30.3 门三件②成本闸):超限 ⇒ timer 工具**显式拒**(不静默丢 / 不静默清;
21
+ * 形态先例 = 队满「拒 + 提示 + 保留」)。与 CLI 排队容量 `QUEUED_MAX_ITEMS`(8)同值同形。 */
22
+ export const TIMER_MAX_PENDING = 8
23
+
24
+ /** 最近到期时点(毫秒)或 `null`(空在途)——端闩 / 挂起窗 deadline / 显示面共用同源(每帧活读)。 */
25
+ export function pendingTimerDeadline(agent) {
26
+ const timers = agent?._pendingTimers ?? []
27
+ let min = null
28
+ for (const t of timers) {
29
+ const at = t?.expiresAt
30
+ if (typeof at !== "number" || !Number.isFinite(at)) continue
31
+ if (min === null || at < min) min = at
32
+ }
33
+ return min
34
+ }
35
+
36
+ /** 到期出列(**幂等**):取走 `expiresAt <= now` 的条目并返回(在途表内移除)——二次调用零返
37
+ * (§6.30.3 成本闸②「到期即消费」——绝不重复投递)。未到期项原序保留。 */
38
+ export function takeExpiredTimers(agent, now = Date.now()) {
39
+ const timers = agent?._pendingTimers ?? []
40
+ const expired = timers.filter((t) => t.expiresAt <= now)
41
+ if (expired.length === 0) return []
42
+ agent._pendingTimers = timers.filter((t) => t.expiresAt > now)
43
+ return expired
44
+ }
45
+
46
+ /** 历史注入单点(逐字形态 = 既有注入单源):逐条 `[System reminder: ⏰ timer — <message>]` 落历史,
47
+ * 返回注入的原文(显示面复用同一串——零第二份字面)。 */
48
+ export function injectTimerReminders(agent, entries) {
49
+ const lines = []
50
+ for (const t of entries ?? []) {
51
+ const line = `[System reminder: ⏰ timer — ${t.message}]`
52
+ agent.history.push({ role: "user", content: line })
53
+ lines.push(line)
54
+ }
55
+ return lines
56
+ }
57
+
58
+ /** 开关判据(§6.30.3 门三件③ D-TW5 · 上提源 = VSC `timer-watch.mjs:33`——as-of 2026-09-28 R4 读数,B3 收编后该端副本退场)
59
+ * (现转口 = CLI `timer-watch.mjs:18`):默认开——只有显式 `false` 才关
60
+ * (键 `agent.timerWake`;与 CLI 同口径 `!== false`)。关 ⇒ 闩零注册 ⇒ 回仅回合边界投递(既有语义)。 */
61
+ export function timerWakeEnabled(agent) {
62
+ return agent?.config?.agent?.timerWake !== false
63
+ }
64
+
65
+ /** 到期批派发(**两路共用**:空闲闩 fire ∥ 挂起窗 `timerFace.deliver` · 上提源 = VSC `timer-watch.mjs:73`
66
+ * 核心体——as-of 2026-09-28 R4 读数;现址 = VSC `:29` · CLI `:34`):出列(幂等——绝不重复投递)→ 注入单点 → 逐条落流缝(`onLine`——端侧 `ev:timer` ∕ webview ∕
67
+ * TUI 行)。返回交付原文(核注入单源逐字;零到期 ∕ 无载体 ⇒ 空数组——在途不动 = 零静默丢)。
68
+ * `now` = 时点值(默认现刻;与 `takeExpiredTimers` 同形——调用方传钟、本档取时点)。 */
69
+ export function deliverExpiredTimers(agent, { now = Date.now(), onLine = null } = {}) {
70
+ const lines = injectTimerReminders(agent, takeExpiredTimers(agent, now))
71
+ for (const line of lines) onLine?.(line)
72
+ return lines
73
+ }
74
+
75
+ /** 一次性最近截止闩(单槽 · 上提源 = VSC `timer-watch.mjs:46` 逐字——as-of 2026-09-28 R4 读数;现址 = VSC `:63`(闩 ∕ 装配)· CLI `:41`(闩转口)):`sync()` = 按当前在途最近到期时点
76
+ * (重)武装——无在途 ∕ 开关关 ∕ 无载体 ⇒ 撤旧零注册;`disarm()` = 撤闩清点。到点自撤 · `unref()`(尽力面)。
77
+ * `getAgent` / `onFire` 缺省宽容沿用源(未接线 = 惰性零注册);`timer` ∕ `clear` ∕ `now` = 注入缝(默认平台全局)。
78
+ * @returns {{ sync: Function, disarm: Function }}——`sync()` ⇒ 武装延迟(毫秒)或 `null`(未武装) */
79
+ export function createTimerWatch({ getAgent, onFire, timer = setTimeout, clear = clearTimeout, now = Date.now } = {}) {
80
+ let handle = null
81
+ const disarm = () => {
82
+ const h = handle
83
+ handle = null
84
+ if (h !== null) { try { clear(h) } catch { /* 已触发 / 不可清——尽力面 */ } }
85
+ }
86
+ const sync = () => {
87
+ const agent = getAgent?.()
88
+ const deadline = timerWakeEnabled(agent) ? pendingTimerDeadline(agent) : null
89
+ if (deadline == null) { disarm(); return null } // 无在途 / 开关关 / 无载体——撤旧,零注册
90
+ disarm() // 单槽:重复武装 = 撤旧立新(延迟按最近到期重算)
91
+ const delay = Math.max(0, deadline - now())
92
+ handle = timer(() => { handle = null; onFire?.() }, delay)
93
+ try { handle?.unref?.() } catch { /* unref 失败不阻断(一次性闩自然退出) */ }
94
+ return delay
95
+ }
96
+ return { sync, disarm }
97
+ }
98
+
99
+ /** 空闲唤醒火策略(上提源 = VSC `timer-watch.mjs:94`——as-of 2026-09-28 R4 读数;现址 = VSC `:48` · CLI `:57`;桌面同形):
100
+ * **非空闲零动作**——回合在飞 ∕ 挂起窗
101
+ * 由各自既有路径接管,在途表零触碰(链尾重同步再武装);空闲 ⇒ 派发;交付真 ⇒ 开 timer 轮(开轮面 =
102
+ * `openTurn` 缝,承担 `{ autoTurn: true, timerTurn: true }` 语义)。`deliver` 契约 = 同步 · 严格布尔
103
+ * (判定 `=== true`——同 §6.30.10 `timerFace.deliver`);缝缺 ⇒ 抛(fail-loud——未接线不得静默零开轮)。
104
+ * `busy` = 非空闲判据(宿主现算:在飞回合 ∕ 窗内 ⇒ true);返回是否开轮。 */
105
+ export async function fireTimerWake({ busy = false, deliver, openTurn } = {}) {
106
+ if (typeof deliver !== "function" || typeof openTurn !== "function") {
107
+ throw new Error("[timers] fireTimerWake requires deliver/openTurn seams (unwired seams must not pass silently)")
108
+ }
109
+ if (busy) return false
110
+ if (deliver() !== true) return false
111
+ await openTurn()
112
+ return true
113
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * agent/turn-loop.mjs — runAgent 回合环(2026-09-29 P2 三拆:agent.mjs runAgent 段 2 ∕ 3 ∕ 5 ∕ 6
3
+ * verbatim 迁入——循环局部 ∕ `_ctxBasis` ∕ drainChildUpstream ∕ for 循环本体 ∕ 零落盘结账 +
4
+ * ContinueError;模型调用段经 callModelTurn 外提 agent/chat-call.mjs)。
5
+ * 接口 = runTurnLoop(agent, ctx);控制流零转换(`continue` ∕ `return cr.content` ∕ `throw` 全留环内)。
6
+ */
7
+ import { resolve } from "node:path"
8
+ import { pushReal, summarizeRunExplorations } from "../context.mjs"
9
+ import { abortError, annotateAbort } from "../abort-provenance.mjs"
10
+ import { specForModel, assistantToolCallMessage } from "../config.mjs"
11
+ import { executeToolCalls } from "./dispatch.mjs"
12
+ import { recordToolResults } from "./record-results.mjs"
13
+ import { injectPostTurn } from "./post-turn.mjs"
14
+ import { handleCompletion } from "./completion.mjs"
15
+ import { runCompactionCheck, injectTurnReminders, injectResponseReminders } from "./run-stages.mjs"
16
+ import { callModelTurn } from "./chat-call.mjs"
17
+ import {
18
+ turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 TURN-CAP-CONTINUE.md §4)
19
+ ContinueError,
20
+ FILE_MUTATORS, toolTouchPaths,
21
+ } from "./helpers.mjs"
22
+
23
+ export async function runTurnLoop(agent, {
24
+ maxTurns, threshold, toolSchemas, toolByName, systemPrompt,
25
+ depth, signal, autoTurn, streamOutput,
26
+ consumeInjected, consumeQueuedInput, callbacks, distillSignal,
27
+ }) {
28
+ let guardPushbacks = 0
29
+ let advisorPushbacks = 0
30
+ let honestReminderInjected = false
31
+ const recentCallSigs = []
32
+ // "once" stream rules fire at most once per runAgent call; the set survives across
33
+ // chat() calls (rule abort-retry, tool loop) within the turn.
34
+ const streamRuleFired = new Set()
35
+
36
+ // Compaction overhead for the pure-estimation path: system prompt + tools schema are
37
+ // in every request but not in history — without them the first-turn/just-compacted
38
+ // estimate under-counts and may never trigger. Measured path already includes both.
39
+ const compactionOverhead = {
40
+ systemPrompt,
41
+ tools: toolSchemas,
42
+ // TRACES.md §6.1 D-TR4:compress 轨迹 depth 元数据(runAgent 的 depth 在此作用域——
43
+ // context.mjs compressIfNeeded 经 extras 透出到 logCtx)
44
+ traceDepth: depth,
45
+ }
46
+ // F-CC1(§6.16.4 阈值单源接线):本回合压缩判定所用的阈值 + 固定开销面暂存——
47
+ // `context` 工具的 stats 报**同一口径**(不自行重算第二口径;VSC checkAndCompact 同款暂存)。
48
+ agent._ctxBasis = { threshold, overhead: compactionOverhead }
49
+
50
+ // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 消费点):子 → 父在飞消息的
51
+ // 回合边界注入单点取用一次(模块缓存 ⇒ 每 run 一次代价);动态 import = 零新增静态边
52
+ // (先例 = 上方 injectAsyncResult :113-117)。
53
+ const { drainChildUpstream } = await import("../agent-tools/parent-channel.mjs")
54
+
55
+ // #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值 / 零自动动作):计数基线——回合环逐轮
56
+ // 比对 `_touchedFiles` 增量(轮顶对上一轮结账;撞帽收尾轮在环外单结)。
57
+ agent._turnFilesMark = agent._touchedFiles.length
58
+
59
+ for (let turn = 0; turn < maxTurns; turn++) {
60
+ // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §4):编号帧——
61
+ // `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
62
+ // (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
63
+ // 公式 = helpers.mjs turnFrame)。段内帽判定不读帧(下行循环条件只读段内
64
+ // turn / maxTurns——N6)。
65
+ const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
66
+ agent._currentTurn = frame.turn
67
+ agent._maxTurns = frame.maxTurns
68
+ // #417(撞帽载荷「本段零落盘轮数」——采集点与 `_turnSeq` 同源面):上一轮至今 `_touchedFiles`
69
+ // 零增 ⇒ 上一轮零落盘,计数 +1;mark 前移(收尾轮在环外结账——见下方 ContinueError 前)。
70
+ if (turn > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
71
+ agent._turnFilesMark = agent._touchedFiles.length
72
+ // D2 (AGENT-LOOP-SUBAGENT.md §6.7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
73
+ // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
74
+ // the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
75
+ // (agent._currentTurn / _maxTurns——字段形态 / 字段数 / phase 零变化,N5)。
76
+ if (depth > 0 && callbacks.onToken) {
77
+ callbacks.onToken(`⟦ev⟧turn\x1e${agent._currentTurn}\x1e${agent._maxTurns}\x1ellm\x1e`)
78
+ }
79
+ // §2.5 #78 并入(VSC 帧回调):每轮帧另经结构化回调发出(池条目 entry.turn 的
80
+ // VSC 等价通道——⟦ev⟧turn token 解析的宿主面随时可用;签名扩展向后兼容)。
81
+ callbacks.onAgentTurn?.(frame.turn, frame.maxTurns)
82
+
83
+ // SUBAGENT-OBSERVE-SEND D2: 子代理回合边界消费点——每轮开头把父侧经 subagent
84
+ // action:'send' 注入队列(entry._injected)的消息按普通 user 回合推入子历史
85
+ // (pushReal → 下一轮 chat 即含该指令)。由 executeAsyncSpawn 经 childRunOpts 贯通的
86
+ // consumeInjected 回调承载(异步子代理专属——缺省 null:主会话/阻塞子代理零开销)。
87
+ consumeInjected?.(agent)
88
+ // 子代理在飞消息(子 → 父;§6.27):空队列 no-op;非空 ⇒ 恰一条合并 user 消息注入。
89
+ drainChildUpstream(agent)
90
+ // ③ 用户 → 主会话投递(步边界 pickup——queue-visible 批 fix 轮 2026-09-24 · F16):循环头
91
+ // 回合边界缝(核只给缝、不给策略——取批 / pushReal / 呈现归端侧闭包);缺省 null,系统轮不传
92
+ // (`AGENT-LOOP-ASYNC-POOL.md` §6.8「步边界 pickup」· 适用面 = 用户回合)。
93
+ consumeQueuedInput?.(agent)
94
+
95
+ const lastRole = agent.history.at(-1)?.role
96
+ if (lastRole === "user" || lastRole === "tool") {
97
+ // 2026-09-05 实践轮:压缩检查/降级计数提为 runCompactionCheck(agent/run-stages.mjs——
98
+ // CLI 对位 VS run-stages)——循环骨架此处只剩检查调用(recentCallSigs 对象引用回流)。
99
+ await runCompactionCheck(agent, { threshold, callbacks, compactionOverhead, signal, recentCallSigs })
100
+ }
101
+
102
+ // 2026-09-05 实践轮:plan cadence + eng 状态注入提为 injectTurnReminders(run-stages)
103
+ await injectTurnReminders(agent, { depth })
104
+
105
+ const response = await callModelTurn(agent, {
106
+ systemPrompt, toolSchemas, streamOutput, callbacks, signal, streamRuleFired, autoTurn, depth, turn,
107
+ })
108
+
109
+ // 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
110
+ // 服务端 item id 是 msg_xxx 非 web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
111
+ // 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
112
+ for (const btr of response.builtinToolResults ?? []) {
113
+ if (!btr?.id) continue
114
+ pushReal(agent, {
115
+ role: "tool",
116
+ tool_call_id: `web_search_call_${btr.id}`,
117
+ content: JSON.stringify({ id: btr.id, query: btr.query ?? "", sources: btr.sources ?? [], status: btr.status ?? "completed" }),
118
+ })
119
+ }
120
+
121
+ // Stream rule triggered mid-generation (action: "abort"): halt, inject the rule's
122
+ // message as a reminder, retry from the same context.
123
+ if (response.ruleTriggered) {
124
+ if (response.content) {
125
+ pushReal(agent, { role: "assistant", content: response.content })
126
+ }
127
+ const label = response.ruleName ? ` — stream rule "${response.ruleName}"` : ""
128
+ agent.history.push({
129
+ role: "user",
130
+ content: `[System reminder${label}: ${response.ruleMessage}]`,
131
+ })
132
+ continue
133
+ }
134
+
135
+ // Stream rule warnings / finish-reason 警告(2026-09-05 实践轮——提为
136
+ // injectResponseReminders,agent/run-stages.mjs——verbatim,语义零变)
137
+ injectResponseReminders(agent, response)
138
+
139
+ // User interrupted mid-generation (Ctrl+I): commit partial output + inject the
140
+ // message, then signal the outer loop to recreate the controller and resume.
141
+ if (response.interrupted) {
142
+ if (response.content) {
143
+ pushReal(agent, { role: "assistant", content: response.content })
144
+ }
145
+ agent.history.push({
146
+ role: "user",
147
+ content: `[User interrupt: ${response.interruptMessage}]`,
148
+ })
149
+ // AGENT-LOOP-SUBAGENT.md §6.12 站点 #8(第 24 批):错误对象已自带 name/message——只补来源标注(缺 abortInfo 才补)
150
+ throw annotateAbort(Object.assign(new Error("User interrupted"), { name: "AbortError" }), signal, "agent", "interrupted-response")
151
+ }
152
+
153
+ if (response.usage) {
154
+ callbacks.onUsage?.(response.usage)
155
+ if (response.usage.prompt_tokens != null) {
156
+ agent._lastPromptTokens = response.usage.prompt_tokens
157
+ agent._usageAtLen = agent.history.length
158
+ }
159
+ }
160
+
161
+ // Warn on abnormal finish reasons — the response may be incomplete/truncated.
162
+ // 2026-09-05 实践轮:finish-reason 注入随流规则警告提为 injectResponseReminders。
163
+
164
+ if (response.toolCalls.length === 0) {
165
+ const cr = handleCompletion(agent, response, depth, turn, guardPushbacks, honestReminderInjected, advisorPushbacks, callbacks)
166
+ guardPushbacks = cr.guardPushbacks
167
+ honestReminderInjected = cr.honestReminderInjected
168
+ advisorPushbacks = cr.advisorPushbacks
169
+ if (cr.action === "continue") continue
170
+ if (depth === 0) {
171
+ // End-of-run exploration distillation (CONTEXT-COMPACTION §5 + SEND-STALL-DISTILL
172
+ // §2.1): async — the promise hangs on _pendingDistill, settling at the next run's
173
+ // start or the TUI exit flush. Silent (N3): failure never blocks return/history.
174
+ // TRACES.md §6.1 D-TR4:depth 透传(distill 轨迹元数据——与 compress 同通道);extras = 会话续写前缀面(§6.15)
175
+ // §2.5-B(P4-I):端壳经 `opts.distillSignal` 供蒸馏专用中止信号(与运行 signal 分离——
176
+ // 运行 controller 重建不杀蒸馏);缺省(null)⇒ `signal` 逐字零变。
177
+ const distill = summarizeRunExplorations(agent, callbacks, distillSignal ?? signal, depth,
178
+ { systemPrompt, tools: toolSchemas }).catch(() => {}) // 与回合请求同源(无第二构造点)
179
+ agent._pendingDistill = distill
180
+ }
181
+ return cr.content
182
+ }
183
+
184
+ // abort after chat completes, before committing history: don't commit a half-finished turn
185
+ if (signal?.aborted) throw abortError(signal, "agent", "post-chat")
186
+
187
+ pushReal(agent, assistantToolCallMessage(response, specForModel(agent.provider.model)))
188
+
189
+ const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
190
+
191
+ // Ctrl+I interrupt during tool execution: skip committing partial results — inject
192
+ // the interrupt and retry (placeholder results keep strict providers pairable).
193
+ if (signal?.reason?.interrupt) {
194
+ // 中断变更记账(2026-08-31 评审 #4):此分支的工具已全部执行完成(磁盘已变,execute 已完成),
195
+ // 真实结果按语义不进历史(placeholder 替代)——但变更必须记账:否则 guard 看到
196
+ // "本轮未改代码" 放行,评审/verify 门禁被绕过(文件改了却没评审)。
197
+ // §29 fix A(2026-09-07):mutation-seq 记账已收敛到 dispatch runOne 执行成功即刻
198
+ // (唯一记账点——本分支不再 noteMutations——不双计——中断+同批 launch seq 单计
199
+ // 回归断言见 §29 T-A1i);此处仅剩 guard 标志 + touchedFiles 记账。
200
+ for (const { toolCall, ok } of results) {
201
+ const tool = toolByName.get(toolCall.name)
202
+ if (!ok || !tool || !FILE_MUTATORS.has(toolCall.name)) continue
203
+ agent._mutatedThisRun = true
204
+ agent._calledAdvisorThisRun = false
205
+ agent._verifiedThisRun = false
206
+ agent._verifyPassed = undefined
207
+ try {
208
+ const args = JSON.parse(toolCall.arguments)
209
+ const paths = toolTouchPaths(tool, args)
210
+ for (const p of paths) {
211
+ const abs = resolve(agent.cwd, p)
212
+ if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
213
+ }
214
+ } catch { /* 畸形 args 不影响记账(touchedFiles 尽力而为) */ }
215
+ }
216
+ // The assistant tool_calls were committed above — synthesize placeholder tool
217
+ // results BEFORE the interrupt message (strict providers 400 on dangling
218
+ // tool_calls; consult P1, 2026-08-30).
219
+ for (const tc of response.toolCalls) {
220
+ agent.history.push({ role: "tool", tool_call_id: tc.id, content: "[Tool execution interrupted — results discarded]" })
221
+ }
222
+ agent.history.push({
223
+ role: "user",
224
+ content: `[User interrupt: ${signal.reason.message}]`,
225
+ })
226
+ callbacks.onTurnEnd?.(agent, turn)
227
+ continue
228
+ }
229
+
230
+ // Model is executing tools → real work: reset guard pushback counters
231
+ guardPushbacks = 0
232
+ advisorPushbacks = 0
233
+
234
+ // Commit tool results (pairing, multimodal deferral, mutation accounting, reindex)
235
+ await recordToolResults(agent, toolByName, results)
236
+
237
+ injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
238
+ }
239
+
240
+ // #417(撞帽载荷「本段零落盘轮数」):收尾轮(撞帽轮)若零落盘同样计入——轮顶计数只
241
+ // 覆盖到倒数第二轮(maxTurns ≤ 0 时环未跑,不虚计)。
242
+ if (maxTurns > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
243
+ throw new ContinueError(maxTurns)
244
+ }