thincoder 0.12.59 → 0.12.60

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 (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. package/src/tools/read_pdf.md +0 -21
@@ -2,11 +2,14 @@
2
2
  * subagent-actions.mjs — subagent 动作执行器(§19/§19.5/§19.6——2026-09-05 自
3
3
  * subagent-async.mjs 拆分——Module Split Policy §20.9——纯迁移零行为变化)。
4
4
  * 内容:executeStatusAction(§19.5 D-M5——非阻塞池查询——touchedSummary/
5
- * shortTouchedPath/statusFields 摘要助手随行)/ executePanelAction(§19.6——面板镜像
6
- * view + 门控 freeze——panelFreezeGate/blockKeyIn 随行)/ executeEscalateAction(§19
5
+ * shortTouchedPath/statusFields 摘要助手随行)/ executeObserveAction + executeSendAction
6
+ * (SUBAGENT-OBSERVE-SEND——observe 只读/send 控制豁免注入)/ executeEscalateAction(§19
7
7
  * D-M4——飞刀——touchedFilesNote 随行)。
8
- * check/cancel 动作执行器与共享 post-spawn 管线留在 subagent-async.mjs;
8
+ * §19.8 check 删除后仅 cancel 动作执行器与共享 post-spawn 管线留在 subagent-async.mjs;
9
9
  * §20 调度器 + 文件域组在 ./subagent-scheduler.mjs(describeBlockers 由此导入)。
10
+ * 2026-09-08 二次拆分:§19.6 面板段迁至 ./subagent-panel.mjs(601 > 500 硬限跨档——
11
+ * 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面)+ ASYNC-RESULT-
12
+ * CONTAINER.md D1 落地(池访问点改 getAsyncPool accessor)。
10
13
  */
11
14
  import { isAbsolute, relative } from "node:path"
12
15
  import { runAgent, createAgent, CODER_OVERLAY, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
@@ -17,16 +20,20 @@ import {
17
20
  import { logEvent, errText } from "../log.mjs"
18
21
  import { describeBlockers, detectStall, STALL_NOTE } from "./subagent-scheduler.mjs"
19
22
  import { resolveChildProvider, mergeChildMutations } from "./subagent-async.mjs"
23
+ import { launchEscalateAsync } from "./escalate-async.mjs"
24
+ // ASYNC-RESULT-CONTAINER.md D1:池 accessor(absorb 双池——advisor 独立池无队列)
25
+ import { getAsyncPool } from "./async-settle.mjs"
20
26
 
21
27
  /**
22
28
  * subagent action:"status" (§19 D-M2, new): NON-BLOCKING async-pool query —
23
- * returns immediately, never consumes a result and never touches the check read
24
- * counter (T-M10). Source of truth = the pool (_asyncSubagents): entries moved
25
- * to _pendingAsyncResults during a suspension (§17 D-S3 ② — injected at the next
26
- * run start) are no longer in the pool and are NOT counted as done-waiting.
29
+ * returns immediately and never consumes a result (results belong to the auto
30
+ * channel — §19.8: no check action any more). Source of truth = the pool
31
+ * (_asyncSubagents): entries moved to _pendingAsyncResults during a suspension
32
+ * (§17 D-S3 ② — injected at the next run start) are no longer in the pool and
33
+ * are NOT counted as done-waiting.
27
34
  * - id given → { id, role, status, model?, elapsedSec?, turn?, maxTurns?,
28
35
  * touchedFiles?/touchedMore?/touched? ... } for that entry; unknown id → error
29
- * (same wording as check — T12 semantics)
36
+ * (T12 semantics — unknown async-subagent id error, same wording as the pool).
30
37
  * - id omitted → { overview: { running: [{id, role, model, elapsedSec, turn,
31
38
  * maxTurns, touchedFiles?/touched?}], queued: [{id, role, position, touched?}],
32
39
  * done: [{id, role}] } } — live queue positions (index in _asyncQueue + 1).
@@ -34,7 +41,8 @@ import { resolveChildProvider, mergeChildMutations } from "./subagent-async.mjs"
34
41
  * 计数——相对查询方 cwd;0 改动 → touched 占位);queued 条目带 touched 占位
35
42
  * "—(未启动)";done/error/取消条目无 touched 字段(round3 #9)。
36
43
  * A settled-but-unconsumed entry (settled during a NORMAL turn) reports done
37
- * with a "not yet consumed" note — check still retrieves it afterwards.
44
+ * with a "not yet consumed" note — the auto channel (turn-end collection / the
45
+ * suspension digest) still delivers it afterwards (§19.8: sole consumption path).
38
46
  */
39
47
  /** §19.5 D-M5 decision-field assembly (F9): running entries report
40
48
  * {id, role, model, elapsedSec, turn, maxTurns} — the data needed to decide
@@ -86,7 +94,8 @@ function statusFields(entry, cwd) {
86
94
 
87
95
  export function executeStatusAction(args, ctx) {
88
96
  const agent = ctx.agent
89
- const map = agent._asyncSubagents ?? new Map()
97
+ const map = getAsyncPool(agent, "subagent") ?? new Map()
98
+ const advisors = getAsyncPool(agent, "advisor") ?? new Map()
90
99
  const queue = agent._asyncQueue ?? []
91
100
  const queuedPosition = (id) => {
92
101
  const i = queue.findIndex((e) => String(e.id) === id)
@@ -95,8 +104,13 @@ export function executeStatusAction(args, ctx) {
95
104
  const { id } = args ?? {}
96
105
  if (id !== undefined && id !== null && String(id) !== "") {
97
106
  const key = String(id)
98
- const entry = map.get(key)
107
+ // §24 D-24b: by-id queries fall through to the advisor pool (shared counter —
108
+ // ids are unique across both pools; role identifies the kind).
109
+ const entry = map.get(key) ?? advisors.get(key)
99
110
  if (!entry) {
111
+ // Wording kept exact — locked by subagent-async/tool tests (unknown-id error
112
+ // semantics); an unknown id simply names neither pool (the pools share the
113
+ // id counter, so the message stays unambiguous).
100
114
  return JSON.stringify({ id: key, status: "error", error: `unknown async subagent id: ${key}` })
101
115
  }
102
116
  const target = statusFields(entry, agent.cwd)
@@ -119,16 +133,17 @@ export function executeStatusAction(args, ctx) {
119
133
  }
120
134
  return JSON.stringify(out)
121
135
  }
122
- // done = settled during this turn and not yet consumed — check still retrieves it
123
- // (§17.5: at a driven turn end it stays pooled → the suspension digest consumes it).
136
+ // done = settled during this turn and not yet consumed — the auto channel still
137
+ // delivers it 19.8: turn-end collection / the suspension digest no fetch action).
124
138
  target.status = "done"
125
139
  target.done = true
126
140
  if (entry.error) target.error = entry.error
127
- target.note = "settled, not yet consumed — retrieve via check or the suspension digest injects it"
141
+ target.note = "settled, not yet consumed — delivered by the auto channel (turn-end collection or the suspension digest injects it)"
128
142
  return JSON.stringify(target)
129
143
  }
130
144
  const overview = { running: [], queued: [], done: [] }
131
- for (const entry of map.values()) {
145
+ const mapEntries = [...map.values(), ...advisors.values()]
146
+ for (const entry of mapEntries) {
132
147
  if (entry.status === "running") overview.running.push(statusFields(entry, agent.cwd))
133
148
  else if (entry.status === "queued") {
134
149
  // §20:queued 条目补 waiting/reason(F-SD4——依赖/冲突原因模型可见);
@@ -157,145 +172,158 @@ export function executeStatusAction(args, ctx) {
157
172
  }
158
173
 
159
174
  // ═══════════════════════════════════════════════════════════════════════════
160
- // §19.6 subagent panel 检查工具(AGENT-LOOP.md §19.6——F-P1..P3/D-P1..P4)
175
+ // SUBAGENT-OBSERVE-SEND(docs/design/SUBAGENT-OBSERVE-SEND.md——CLI 端)
176
+ // observe(D1——readonly 查询)+ send(D2——控制类豁免注入引导)——动作执行器。
177
+ // 目标 = 父自身 spawn 的异步子代理池条目(_asyncSubagents——非 advisor/escalate——
178
+ // 后者共享 id 计数但非"读写子代理",错误路径明示)。
161
179
  // ═══════════════════════════════════════════════════════════════════════════
162
180
 
163
- /** 面板块 keyrole#N)在池(Map——条目值)/pending(数组)中的归属判定。 */
164
- function blockKeyIn(container, key) {
165
- const entries = container instanceof Map ? [...container.values()] : (container ?? [])
166
- return entries.some((e) => `${e.role}#${e.id}` === key)
181
+ /** D1/N2 摘要形态:从子代理 _fullHistoryreal 消息——pushReal 累积)倒序收最近 N
182
+ * assistant 回合的一行描述(newest-first;assistant 消息 = 一回合——带 tool_calls =
183
+ * 工具轮、独立 content = 回复首行)。返回纯摘要非全量(N2 隔离——不把子代理噪音灌父)。 */
184
+ function recentTurnLines(child, limit) {
185
+ const hist = child?._fullHistory
186
+ if (!Array.isArray(hist)) return []
187
+ const out = []
188
+ for (let i = hist.length - 1; i >= 0 && out.length < limit; i--) {
189
+ const m = hist[i]
190
+ if (m?.role !== "assistant") continue
191
+ const tcs = Array.isArray(m.tool_calls)
192
+ ? m.tool_calls.filter((t) => typeof t?.function?.name === "string")
193
+ : []
194
+ if (tcs.length) out.push(`tools: ${tcs.map((t) => t.function.name).join(", ")}`)
195
+ else if (typeof m.content === "string" && m.content.trim()) {
196
+ const first = m.content.trim().split(/\r?\n/).find((l) => l.trim())
197
+ out.push((first ?? m.content.trim()).slice(0, 120))
198
+ } else out.push("(assistant — no content)")
199
+ }
200
+ return out
201
+ }
202
+
203
+ function queuedPositionOf(agent, key) {
204
+ const i = (agent?._asyncQueue ?? []).findIndex((e) => String(e.id) === key)
205
+ return i >= 0 ? i + 1 : undefined
206
+ }
207
+
208
+ /** recent 参数钳制(默认 ~5 条摘要——可参数;钳到 1..20 防超长 N2 失控)。 */
209
+ function clampRecent(n) {
210
+ const v = Number.parseInt(n, 10)
211
+ if (!Number.isFinite(v)) return 5
212
+ return Math.max(1, Math.min(20, v))
167
213
  }
168
214
 
169
215
  /**
170
- * §19.6 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents)
171
- * 无对应运行条目 + pending_pendingAsyncResults)无对应条目的块(= 已消化驻留块
172
- * ——报告已入模型上下文——pending 已消费——状态滞后——补发冻结不破坏任何顺序)。
173
- * - pending 仍有对应(报告未达模型)→ 拒绝(提前回收破坏消化顺序——T-P3)
174
- * - 不存在的 key / 仍 running / done 的块 → 拒绝(T-P4——running 块 settle 时自冻)
175
- * - 无镜像 拒绝(headless/VS Code——freeze 不可用——T-P5)
176
- * 错误信息明确(模型可解释 + 自助修正)。返回 { ok:true } 或 { err }。
216
+ * subagent action:"observe"(D1——READONLY 查询,父回合内):按 id 拉运行中异步子代理的
217
+ * 最近活动快照——从 entry.childAgentsubagent-run.mjs start() 绑定)读**已落回合**摘要
218
+ * (_fullHistory 最近 N 条)+ _touchedFiles + turn/maxTurns + status + **in-flight 当前
219
+ * 工具**(评审 #1——从 dispatch runOne 维护的 child._inflightTools 读——非只读已落
220
+ * history:卡在长工具调用时 history 无新回合、恰是 observe 要检测的卡死态)。返摘要非
221
+ * 全量(N2)。observe = readonly——running/queued/done 均可查(done 终报 / queued 占位)。
177
222
  */
178
- function panelFreezeGate(agent, key) {
179
- const snap = agent._panelSnapshot
180
- if (!Array.isArray(snap)) {
181
- return { err: "panel unavailable — no CLI TUI panel mirror in this session (headless / VS Code / subagent contexts — freeze unavailable; panel is CLI-TUI-only, AC-P4)" }
223
+ export function executeObserveAction(args, ctx) {
224
+ if ((ctx.depth ?? 0) > 0) {
225
+ return JSON.stringify({ status: "error", error: "observe is only available at depth 0 — a child agent has no async pool of its own (AGENT-LOOP.md §7.2)" })
182
226
  }
183
- const block = snap.find((b) => b.key === key)
184
- if (!block) {
185
- const live = snap.map((b) => `${b.key}(${b.status})`).join(", ")
186
- return { err: `unknown panel block key: ${key} — the live panel holds: ${live || "(no blocks)"}` }
227
+ const agent = ctx.agent
228
+ const id = args?.id
229
+ if (id === undefined || id === null || String(id) === "") {
230
+ return JSON.stringify({ status: "error", error: "observe requires the id of the async subagent to inspect (from the async spawn return) pass the id; omitting it observes nothing (SUBAGENT-OBSERVE-SEND)" })
187
231
  }
188
- if (block.status !== "awaitingDigest") {
189
- if (block.status === "done") {
190
- return { err: `block ${key} is already done — nothing to freeze; it was (or is about to be) reclaimed into the conversation by the freeze sweep at the turn end / settle (only digested-stuck awaitingDigest blocks need a manual freeze)` }
232
+ const key = String(id)
233
+ const entry = getAsyncPool(agent, "subagent")?.get(key)
234
+ if (!entry) {
235
+ if (getAsyncPool(agent, "advisor")?.has(key)) {
236
+ return JSON.stringify({ status: "error", error: `id ${key} is an async ADVISOR review — observe is for async subagents (read/edit children); track an advisor with action:'status' or wait for its auto-delivered report` })
191
237
  }
192
- if (block.status === "running") {
193
- return { err: `block ${key} is still running — freeze only reclaims awaitingDigest blocks whose report is already digested; a running block freezes on its own settle (or stop it with action:'cancel' if it is a background async child)` }
194
- }
195
- return { err: `block ${key} is in state ${block.status} — freeze only reclaims awaitingDigest blocks whose report is already digested` }
196
- }
197
- if (blockKeyIn(agent._asyncSubagents, key)) {
198
- return { err: `block ${key} still has a live pool entry — it is NOT a digested-stuck block (freeze refused; status action shows the pool)` }
238
+ return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
199
239
  }
200
- if (blockKeyIn(agent._pendingAsyncResults, key)) {
201
- return { err: `block ${key} is still genuinely awaiting digestion — its report is still in _pendingAsyncResults and has NOT reached the model yet; freezing now would break the digestion order (wait for the digest run, which reclaims it automatically — §17.5.5)` }
240
+ const child = entry.childAgent
241
+ const out = { id: key, role: entry.role, status: entry.status }
242
+ if (entry.status === "running") {
243
+ out.turn = entry.turn ?? 0
244
+ out.maxTurns = entry.maxTurns ?? 0
245
+ Object.assign(out, touchedSummary(entry, agent.cwd))
246
+ // in-flight 当前工具(评审 #1):child._inflightTools Set——dispatch runOne 在工具
247
+ // 执行前后维护——LLM 生成/工具间空隙为空;子代理 await 长工具调用时父回合可见它。
248
+ const inflight = child?._inflightTools
249
+ if (inflight instanceof Set && inflight.size > 0) out.currentTool = [...inflight]
250
+ const n = clampRecent(args?.recent)
251
+ out.recentTurns = recentTurnLines(child, n)
252
+ out.note = `observing running ${entry.role}#${key} — recentTurns newest-first (cap ${n}); currentTool shown only while a tool is executing (stuck detection); sent directions consume at the next turn boundary`
253
+ } else if (entry.status === "queued") {
254
+ out.position = queuedPositionOf(agent, key) ?? entry.position ?? null
255
+ out.note = "queued — not started yet; no activity to observe (starts when the slot frees / dependencies clear; action:'send' targets running only)"
256
+ } else {
257
+ // done = settled this turn, not yet consumed — final report rides the auto channel.
258
+ out.done = true
259
+ out.turn = entry.turn ?? 0
260
+ out.maxTurns = entry.maxTurns ?? 0
261
+ out.recentTurns = child ? recentTurnLines(child, clampRecent(args?.recent)) : []
262
+ out.note = "settled — the final report is delivered by the auto channel (turn-end collection or the suspension digest); observe returns the activity summary, not the full report (N2)"
202
263
  }
203
- return { ok: true }
264
+ return JSON.stringify(out)
204
265
  }
205
266
 
206
267
  /**
207
- * §19.6 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
208
- * freeze 优先):
209
- * - view(缺省——返回镜像区块列表):agent._panelSnapshot = TUI 面板镜像(块级
210
- * 状态变更点由 subagent-blocks syncPanelSnapshot 同步刷新——与用户所见一致——
211
- * index.mjs 装配 state._agent)。awaitingDigest 条目**读时交叉**
212
- * _pendingAsyncResults/_asyncSubagents 标注 digested(round1 #3——digested:true
213
- * = 报告已消化但块仍驻留——异常块——freeze 候选;模型可定位解释 UI 怪相)。
214
- * - freeze:key(D-P3 门控通过 → 发 key + "/" + ⟦ev⟧done 哨兵字面 token——
215
- * onToken——TUI routeSubToken 冻结回收——落位复用 sub._freezeAt settle 锚点
216
- * splice,无锚点尾推兜底——§17.5.5 同口径——round1 #2)。
217
- * 无镜像(headless/VS Code——D-P2 round1 #1:webview 无 state.subTasks 对应物——
218
- * 7.2.3.2 #8 先例)→ view 恒降级池视图(_asyncSubagents + _pendingAsyncResults
219
- * 合成)+ no panel 注;freeze 报不可用。CLI-only 完整能力(AC-P4)。
268
+ * subagent action:"send"(D2——控制类豁免,同 cancel/panel-freeze——父回合内显式调用即
269
+ * 授权):按 id 向运行中异步子代理注入一条引导消息——push 进 entry._injected(仅 running
270
+ * 异步可 send);子回合边界经 consumeInjected 回调消费 → pushReal 成 user 回合进子历史
271
+ * 当作普通指令处理(注入不等同偏离豁免——子收敛/审计纪律不变)。settle/cancel/unknown
272
+ * /queued 明确错误。send→settle 竞态:入队后子未及下回合边界即 settle → 消息未投递
273
+ * settle 收尾附报告提示(不在此报错——send 返回时无法预知)。
220
274
  */
221
- export function executePanelAction(args, ctx) {
275
+ export function executeSendAction(args, ctx) {
276
+ if ((ctx.depth ?? 0) > 0) {
277
+ return JSON.stringify({ status: "error", error: "send is only available at depth 0 — a child agent has no async pool of its own (AGENT-LOOP.md §7.2)" })
278
+ }
222
279
  const agent = ctx.agent
223
- const freezeKey = (args?.freeze !== undefined && args?.freeze !== null && String(args.freeze) !== "")
224
- ? String(args.freeze)
225
- : null
226
- // ── freeze 面(优先——D-P2 单动作双参互斥)──
227
- if (freezeKey) {
228
- if ((ctx.depth ?? 0) > 0) {
229
- return JSON.stringify({ status: "error", error: "panel freeze is only available at depth 0 — a child agent has no panel of its own (AGENT-LOOP.md §19.6 D-P2)" })
230
- }
231
- const gate = panelFreezeGate(agent, freezeKey)
232
- if (gate.err) return JSON.stringify({ status: "error", error: gate.err })
233
- if (!ctx.callbacks?.onToken) {
234
- return JSON.stringify({ status: "error", error: `panel mirror present but no token relay in this context — the freeze of ${freezeKey} cannot reach the TUI` })
235
- }
236
- // 门控通过 → 发 done 冻结事件(settle 同机制字面格式——TUI routeSubToken done
237
- // 分支冻结回收——落位 _freezeAt settle 锚点 splice;无锚点(旧会话残留)时
238
- // freezeSubTaskLines 尾推兜底——注明两种落位,模型不被误导(advisor 🟡1)。
239
- ctx.callbacks.onToken(`${freezeKey}/⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
240
- return JSON.stringify({
241
- key: freezeKey,
242
- status: "frozen",
243
- note: "done freeze event issued — the TUI reclaimed the block into the conversation (spliced at its settle anchor when one is recorded, else appended at the current stream end — §17.5.5 same-rule position)",
244
- })
280
+ const id = args?.id
281
+ if (id === undefined || id === null || String(id) === "") {
282
+ return JSON.stringify({ status: "error", error: "send requires the id of the running async subagent to direct (from the async spawn return) — omitting it means an unspecified target (SUBAGENT-OBSERVE-SEND)" })
245
283
  }
246
- // ── view 面(缺省——readonly)──
247
- // 双参互斥(D-P2):freeze 优先;显式 view:false 且无 freeze = 无请求可执行——报错。
248
- if (args?.view === false) {
249
- return JSON.stringify({ status: "error", error: "panel has nothing to do — view:false with no freeze key; pass freeze:'role#N' to reclaim a digested-stuck block, or omit view (defaults to true)" })
284
+ const message = args?.message
285
+ if (typeof message !== "string" || !message.trim()) {
286
+ return JSON.stringify({ status: "error", error: "send requires the message to inject — the direction the running subagent should treat as an ordinary user instruction at its next turn boundary" })
250
287
  }
251
- const snap = agent._panelSnapshot
252
- if (!Array.isArray(snap)) {
253
- // F-P3 降级:无镜像(headless/VS Code/子代理上下文——CLI TUI-only 完整能力)→
254
- // 池视图(_asyncSubagents 运行/排队条目 + _pendingAsyncResults 待消化条目)
255
- const blocks = []
256
- const queue = agent._asyncQueue ?? []
257
- for (const e of [...(agent._asyncSubagents?.values() ?? [])]) {
258
- const b = { key: `${e.role}#${e.id}`, role: e.role }
259
- if (e.status === "running") {
260
- b.status = "running"
261
- b.elapsedSec = e.startedAt ? Math.max(0, Math.floor((Date.now() - e.startedAt) / 1000)) : 0
262
- } else if (e.status === "queued") {
263
- b.status = "queued"
264
- const qi = queue.indexOf(e)
265
- b.position = qi >= 0 ? qi + 1 : (e.position ?? null)
266
- } else {
267
- b.status = "done" // 回合内 settle 未取——status action 可查/check 可取回
268
- }
269
- blocks.push(b)
288
+ const key = String(id)
289
+ const entry = getAsyncPool(agent, "subagent")?.get(key)
290
+ if (!entry) {
291
+ if (getAsyncPool(agent, "advisor")?.has(key)) {
292
+ return JSON.stringify({ status: "error", error: `id ${key} is an async ADVISOR review — send is for async subagents; you cannot inject direction into a running review (AGENT-LOOP.md §7.2)` })
270
293
  }
271
- for (const e of agent._pendingAsyncResults ?? []) {
272
- blocks.push({ key: `${e.role}#${e.id}`, role: e.role, status: "awaitingDigest", note: "report pending — injected at the next run start (§17)" })
273
- }
274
- return JSON.stringify({
275
- degraded: true,
276
- note: "no panel — this session has no CLI TUI panel mirror (headless / VS Code / subagent context — panel view is CLI-TUI-only, AC-P4); pool-derived view below; action:'status' shows the full pool",
277
- panel: blocks,
278
- })
294
+ return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
279
295
  }
280
- const panel = snap.map((b) => {
281
- const out = { key: b.key, role: b.role, status: b.status }
282
- if (b.status === "running") {
283
- out.elapsedSec = b.startedAt ? Math.max(0, Math.floor((Date.now() - b.startedAt) / 1000)) : 0
284
- } else if (b.status === "awaitingDigest") {
285
- // 读时交叉(round1 #3):pending/池均无对应 = 报告已消化(注入即从两者移除)——
286
- // 块驻留 = 状态滞后——digested:true(freeze 候选——模型可定位异常块)。
287
- out.digested = !blockKeyIn(agent._pendingAsyncResults, b.key) && !blockKeyIn(agent._asyncSubagents, b.key)
288
- }
289
- return out
296
+ if (entry.done || entry.cancelled) {
297
+ return JSON.stringify({ status: "error", error: `async subagent #${key} has ${entry.cancelled ? "been cancelled" : "already settled"} — nothing to send (you cannot inject into a finished subagent; re-spawn with the direction instead)` })
298
+ }
299
+ if (entry.status !== "running") {
300
+ return JSON.stringify({ status: "error", error: `async subagent #${key} is ${entry.status} (not running) — send only targets a RUNNING async subagent; a queued one has not started its turn loop yet` })
301
+ }
302
+ entry._injected ??= []
303
+ entry._injected.push(String(message).trim())
304
+ return JSON.stringify({
305
+ id: key,
306
+ status: "delivered",
307
+ queued: entry._injected.length,
308
+ note: `message queued for ${entry.role}#${key} — consumed as an ordinary user instruction at its next turn boundary (after its current tool finishes — non-interrupting). If it settles first, its report carries an "undelivered" note.`,
290
309
  })
291
- return JSON.stringify({ panel })
292
310
  }
293
311
 
312
+ // ═══════════════════════════════════════════════════════════════════════════
313
+ // §19.6 subagent panel 检查工具(AGENT-LOOP.md §19.6——F-P1..P3/D-P1..P4)
314
+ // ═══════════════════════════════════════════════════════════════════════════
315
+ // 2026-09-08 拆分(Module Split Policy——601 > 500 硬限跨档):§19.6 面板段迁至
316
+ // ./subagent-panel.mjs(executePanelAction/panelFreezeGate/blockKeyIn——verbatim +
317
+ // ASYNC-RESULT-CONTAINER D1/D2 落地);本面保留 re-export 保 subagent.mjs 既有
318
+ // import 面(subagent-async 尾部 re-export 先例)。
319
+ export { executePanelAction } from "./subagent-panel.mjs"
320
+
294
321
  /**
295
- * subagent action:"escalate"(§19 D-M4——退役 escalate 工具语义原样,ESCALATE.md):
296
- * 飞刀——交给 consultModels 池里更强模型(WRITE + 术后报告)。约束全保留:depth-0
297
- * only / 工程模式拒 / consultModels 空拒 / relay 前缀 `escalate#N/`(与既有前缀同名
298
- * ——TUI 路由零改动)/ permQueue(continue 直达用户)/ mutations merge 回父。
322
+ * subagent action:"escalate"(§19 D-M4——退役 escalate 工具语义原样,ESCALATE.md
323
+ * §25 D-R17b——R17:缺省 async——后台飞刀 + settle 三分类 digest——async:false 保
324
+ * 同步旧路径)。飞刀——交给 consultModels 池里更强模型(WRITE + 术后报告)。约束全
325
+ * 保留:depth-0 only / 工程模式拒 / consultModels 空拒 / relay 前缀 `escalate#N/`
326
+ * (与既有前缀同名——TUI 路由零改动)/ mutations merge 回父(async 路径在 settle 分类)。
299
327
  */
300
328
  export async function executeEscalateAction(args, ctx) {
301
329
  const parent = ctx.agent
@@ -334,6 +362,16 @@ export async function executeEscalateAction(args, ctx) {
334
362
  }
335
363
 
336
364
  const tag = label(pick)
365
+
366
+ // §25 D-R17b (R17 — 决策点 ③): escalate 缺省 async — the launch returns an ack
367
+ // {id, role:"escalate", status:"running"|"queued"} and the flight runs in the
368
+ // background (shared other pool; settle 三分类 → pending 单容器 digest——
369
+ // ASYNC-RESULT-CONTAINER.md D2)。
370
+ // `async: false` keeps the legacy synchronous flight below (backward compat).
371
+ if (args?.async !== false) {
372
+ return launchEscalateAsync(parent, ctx, { task: String(task), provider, tag, effortNote })
373
+ }
374
+
337
375
  const relayPrefix = makeRelay(parent, "escalate", ctx.callbacks?.onToken, provider.model ?? tag)
338
376
 
339
377
  // 无墙钟 watchdog——turn cap 即成本预算(2026-08-16 rationale:固定墙钟会误杀正常慢速