thincoder 0.12.59 → 0.12.61

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 (192) hide show
  1. package/CHANGELOG.md +62 -4
  2. package/README.md +10 -8
  3. package/bin/thincoder.mjs +99 -133
  4. package/package.json +6 -4
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +45 -21
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +59 -137
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +156 -359
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +27 -7
  17. package/src/agent/completion.mjs +17 -11
  18. package/src/agent/dispatch.mjs +145 -27
  19. package/src/agent/helpers.mjs +107 -13
  20. package/src/agent/record-results.mjs +55 -11
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +242 -0
  23. package/src/agent/setup-reminders.mjs +69 -0
  24. package/src/agent/setup.mjs +107 -127
  25. package/src/agent/spawn-child.mjs +55 -13
  26. package/src/agent-tools/advisor-async.mjs +346 -0
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +167 -116
  29. package/src/agent-tools/async-settle.mjs +191 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +139 -107
  32. package/src/agent-tools/design-token.mjs +117 -0
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +24 -29
  35. package/src/agent-tools/escalate-async.mjs +289 -0
  36. package/src/agent-tools/read-history.mjs +166 -32
  37. package/src/agent-tools/recent-changes.mjs +2 -1
  38. package/src/agent-tools/review-streak.mjs +93 -0
  39. package/src/agent-tools/settings.mjs +137 -34
  40. package/src/agent-tools/subagent-actions.mjs +180 -133
  41. package/src/agent-tools/subagent-async.mjs +184 -177
  42. package/src/agent-tools/subagent-panel.mjs +160 -0
  43. package/src/agent-tools/subagent-run.mjs +205 -0
  44. package/src/agent-tools/subagent-scheduler.mjs +100 -27
  45. package/src/agent-tools/subagent-spawn.mjs +453 -0
  46. package/src/agent-tools/subagent.mjs +256 -578
  47. package/src/agent-tools/verify.mjs +119 -292
  48. package/src/agent-tools.mjs +1 -0
  49. package/src/agent.mjs +89 -205
  50. package/src/cli/distill-command.mjs +12 -6
  51. package/src/cli/make-agent.mjs +26 -8
  52. package/src/cli/memory-command.mjs +4 -3
  53. package/src/cli/permission.mjs +2 -2
  54. package/src/cli/setup-wizard.mjs +42 -17
  55. package/src/completions.mjs +114 -0
  56. package/src/config-migrate.mjs +70 -0
  57. package/src/config.mjs +180 -63
  58. package/src/context.mjs +5 -147
  59. package/src/conventions.mjs +223 -0
  60. package/src/crash-reports.mjs +128 -0
  61. package/src/distill.mjs +11 -11
  62. package/src/expand-home.mjs +16 -0
  63. package/src/explore-distill.mjs +155 -0
  64. package/src/generate-title.mjs +1 -1
  65. package/src/hooks.mjs +7 -3
  66. package/src/memory/code-index.mjs +9 -3
  67. package/src/memory/code-sync.mjs +72 -32
  68. package/src/memory/core.mjs +6 -193
  69. package/src/memory/delete.mjs +236 -0
  70. package/src/memory/docs.mjs +68 -54
  71. package/src/memory/file-walk.mjs +109 -0
  72. package/src/memory/schema.mjs +15 -3
  73. package/src/memory.mjs +3 -1
  74. package/src/model-ref.mjs +66 -0
  75. package/src/model-specs.mjs +42 -8
  76. package/src/peer-domains.mjs +265 -0
  77. package/src/peer-instances.mjs +231 -0
  78. package/src/prompt-overlays.mjs +82 -0
  79. package/src/prompts/advisor-design.md +18 -75
  80. package/src/prompts/advisor-round1.md +14 -67
  81. package/src/prompts/advisor-round2.md +15 -51
  82. package/src/prompts/advisor-round3.md +15 -51
  83. package/src/prompts/common.md +115 -0
  84. package/src/prompts/consult-base.md +5 -23
  85. package/src/prompts/discipline-engineering.md +217 -0
  86. package/src/prompts/discipline-normal.md +179 -0
  87. package/src/prompts/persona-coder.md +21 -0
  88. package/src/prompts/persona-eng-coder.md +37 -0
  89. package/src/prompts/persona-eng-designer.md +55 -0
  90. package/src/prompts/persona-engineering.md +54 -0
  91. package/src/prompts/persona-explore.md +15 -0
  92. package/src/prompts/persona-normal.md +27 -0
  93. package/src/prompts/persona-plan.md +26 -0
  94. package/src/provider/anthropic.mjs +4 -4
  95. package/src/provider/core.mjs +18 -98
  96. package/src/provider/errors.mjs +101 -0
  97. package/src/provider/google.mjs +5 -6
  98. package/src/provider/index.mjs +2 -1
  99. package/src/provider/list-models.mjs +93 -0
  100. package/src/provider/rate.mjs +2 -1
  101. package/src/provider/responses.mjs +5 -3
  102. package/src/provider/retry.mjs +8 -45
  103. package/src/provider/sse.mjs +3 -4
  104. package/src/proxy.mjs +9 -14
  105. package/src/session-gc.mjs +214 -0
  106. package/src/session-guard.mjs +47 -0
  107. package/src/session-rename.mjs +38 -0
  108. package/src/session-slots.mjs +188 -60
  109. package/src/session.mjs +104 -124
  110. package/src/token-ttl.mjs +274 -0
  111. package/src/tools/{system.mjs → bash.mjs} +19 -221
  112. package/src/tools/checklist-sync.mjs +181 -0
  113. package/src/tools/checklist.mjs +52 -39
  114. package/src/tools/edit-batch.mjs +109 -10
  115. package/src/tools/edit-diff.mjs +110 -27
  116. package/src/tools/edit.md +17 -12
  117. package/src/tools/execute.mjs +31 -4
  118. package/src/tools/file.mjs +41 -16
  119. package/src/tools/git.md +1 -1
  120. package/src/tools/git.mjs +23 -34
  121. package/src/tools/glob-dialect.mjs +130 -0
  122. package/src/tools/glob.md +3 -3
  123. package/src/tools/grep.md +1 -1
  124. package/src/tools/index.mjs +9 -8
  125. package/src/tools/ops.mjs +188 -3
  126. package/src/tools/patch.mjs +3 -3
  127. package/src/tools/question.md +4 -0
  128. package/src/tools/question.mjs +26 -0
  129. package/src/tools/read.md +1 -2
  130. package/src/tools/read_image.md +1 -1
  131. package/src/tools/search.mjs +236 -0
  132. package/src/tools/shared.mjs +14 -13
  133. package/src/tools/wait_for.md +22 -0
  134. package/src/tui/agent-turn.mjs +36 -228
  135. package/src/tui/ansi.mjs +2 -0
  136. package/src/tui/clipboard.mjs +7 -1
  137. package/src/tui/cmd-advisor.mjs +3 -2
  138. package/src/tui/cmd-config.mjs +142 -30
  139. package/src/tui/cmd-eng.mjs +28 -40
  140. package/src/tui/cmd-exit.mjs +6 -8
  141. package/src/tui/cmd-mcp.mjs +8 -2
  142. package/src/tui/cmd-model.mjs +14 -12
  143. package/src/tui/cmd-new.mjs +3 -2
  144. package/src/tui/cmd-reindex.mjs +7 -0
  145. package/src/tui/cmd-session.mjs +19 -4
  146. package/src/tui/cmd-submodel.mjs +8 -5
  147. package/src/tui/cmd-think.mjs +10 -10
  148. package/src/tui/cmd-undo.mjs +4 -3
  149. package/src/tui/cmd-upgrade.mjs +19 -4
  150. package/src/tui/config-helpers.mjs +28 -16
  151. package/src/tui/distill-cmd.mjs +1 -1
  152. package/src/tui/index.mjs +40 -38
  153. package/src/tui/interaction.mjs +3 -3
  154. package/src/tui/key-handler.mjs +61 -17
  155. package/src/tui/key-modes.mjs +86 -8
  156. package/src/tui/layout.mjs +18 -10
  157. package/src/tui/model-catalog.mjs +89 -0
  158. package/src/tui/model-picker.mjs +498 -0
  159. package/src/tui/mouse.mjs +52 -9
  160. package/src/tui/pickers.mjs +28 -392
  161. package/src/tui/render-frame.mjs +32 -16
  162. package/src/tui/render-loop.mjs +2 -0
  163. package/src/tui/render-segments.mjs +12 -9
  164. package/src/tui/render.mjs +37 -5
  165. package/src/tui/slash-commands.mjs +2 -2
  166. package/src/tui/startup.mjs +4 -0
  167. package/src/tui/subagent-blocks.mjs +106 -295
  168. package/src/tui/subagent-children.mjs +162 -0
  169. package/src/tui/subagent-freeze.mjs +169 -0
  170. package/src/tui/subagent-panel.mjs +24 -31
  171. package/src/tui/suspension-drive.mjs +297 -0
  172. package/src/tui/tool-args.mjs +7 -5
  173. package/src/tui/tool-display.mjs +143 -0
  174. package/src/tui/tool-events.mjs +56 -185
  175. package/src/tui/tui-lifecycle.mjs +46 -4
  176. package/src/tui/update-notice.mjs +4 -0
  177. package/src/tui/wizard.mjs +61 -21
  178. package/src/tui/wrapped-spawn.mjs +38 -0
  179. package/src/prompts/coder.md +0 -56
  180. package/src/prompts/discipline.md +0 -102
  181. package/src/prompts/eng-coder.md +0 -44
  182. package/src/prompts/engineering-sub.md +0 -34
  183. package/src/prompts/engineering.md +0 -340
  184. package/src/prompts/explore.md +0 -21
  185. package/src/prompts/main.md +0 -56
  186. package/src/prompts/methodology-template.md +0 -58
  187. package/src/prompts/plan.md +0 -16
  188. package/src/prompts/system.md +0 -63
  189. package/src/tools/pdf-parse-text.mjs +0 -497
  190. package/src/tools/pdf-parse-xref.mjs +0 -499
  191. package/src/tools/pdf.mjs +0 -155
  192. package/src/tools/read_pdf.md +0 -21
@@ -2,14 +2,17 @@
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
- import { runAgent, createAgent, CODER_OVERLAY, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
15
+ import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
13
16
  import {
14
17
  runWithContinue, TURN_CAP_MARK, makeRelay, wrapChildCallbacks,
15
18
  ensureChildApiKey, clampEffort,
@@ -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
@@ -69,6 +77,15 @@ function shortTouchedPath(f, cwd) {
69
77
  }
70
78
 
71
79
  function statusFields(entry, cwd) {
80
+ // §18.3 #1(第 10 批——双池并表):评审池条目专属字段面——role 区分两池;子代理条目
81
+ // 字段面零变化(NFR-B2)。reviewType(design|code) / round / elapsedSec——与子代理
82
+ // 的 turn/maxTurns 而非(评审无子回合预算语义)。
83
+ if (entry.role === "advisor") {
84
+ const base = { id: String(entry.id), role: entry.role, reviewType: entry.reviewType ?? null, round: entry.round ?? entry.run?.round ?? null }
85
+ if (entry.model != null) base.model = entry.model
86
+ if (entry.startedAt) base.elapsedSec = Math.max(0, Math.round((Date.now() - entry.startedAt) / 1000))
87
+ return base
88
+ }
72
89
  const base = { id: String(entry.id), role: entry.role }
73
90
  if (entry.status === "running") {
74
91
  base.model = entry.model ?? null
@@ -86,7 +103,8 @@ function statusFields(entry, cwd) {
86
103
 
87
104
  export function executeStatusAction(args, ctx) {
88
105
  const agent = ctx.agent
89
- const map = agent._asyncSubagents ?? new Map()
106
+ const map = getAsyncPool(agent, "subagent") ?? new Map()
107
+ const advisors = getAsyncPool(agent, "advisor") ?? new Map()
90
108
  const queue = agent._asyncQueue ?? []
91
109
  const queuedPosition = (id) => {
92
110
  const i = queue.findIndex((e) => String(e.id) === id)
@@ -95,8 +113,13 @@ export function executeStatusAction(args, ctx) {
95
113
  const { id } = args ?? {}
96
114
  if (id !== undefined && id !== null && String(id) !== "") {
97
115
  const key = String(id)
98
- const entry = map.get(key)
116
+ // §11.2 D-24b: by-id queries fall through to the advisor pool (shared counter —
117
+ // ids are unique across both pools; role identifies the kind).
118
+ const entry = map.get(key) ?? advisors.get(key)
99
119
  if (!entry) {
120
+ // Wording kept exact — locked by subagent-async/tool tests (unknown-id error
121
+ // semantics); an unknown id simply names neither pool (the pools share the
122
+ // id counter, so the message stays unambiguous).
100
123
  return JSON.stringify({ id: key, status: "error", error: `unknown async subagent id: ${key}` })
101
124
  }
102
125
  const target = statusFields(entry, agent.cwd)
@@ -119,16 +142,17 @@ export function executeStatusAction(args, ctx) {
119
142
  }
120
143
  return JSON.stringify(out)
121
144
  }
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).
145
+ // done = settled during this turn and not yet consumed — the auto channel still
146
+ // delivers it 19.8: turn-end collection / the suspension digest no fetch action).
124
147
  target.status = "done"
125
148
  target.done = true
126
149
  if (entry.error) target.error = entry.error
127
- target.note = "settled, not yet consumed — retrieve via check or the suspension digest injects it"
150
+ target.note = "settled, not yet consumed — delivered by the auto channel (turn-end collection or the suspension digest injects it)"
128
151
  return JSON.stringify(target)
129
152
  }
130
153
  const overview = { running: [], queued: [], done: [] }
131
- for (const entry of map.values()) {
154
+ const mapEntries = [...map.values(), ...advisors.values()]
155
+ for (const entry of mapEntries) {
132
156
  if (entry.status === "running") overview.running.push(statusFields(entry, agent.cwd))
133
157
  else if (entry.status === "queued") {
134
158
  // §20:queued 条目补 waiting/reason(F-SD4——依赖/冲突原因模型可见);
@@ -157,145 +181,158 @@ export function executeStatusAction(args, ctx) {
157
181
  }
158
182
 
159
183
  // ═══════════════════════════════════════════════════════════════════════════
160
- // §19.6 subagent panel 检查工具(AGENT-LOOP.md §19.6——F-P1..P3/D-P1..P4)
184
+ // SUBAGENT-OBSERVE-SEND(SUBAGENT-OBSERVE-SEND.md——CLI 端)
185
+ // observe(D1——readonly 查询)+ send(D2——控制类豁免注入引导)——动作执行器。
186
+ // 目标 = 父自身 spawn 的异步子代理池条目(_asyncSubagents——非 advisor/escalate——
187
+ // 后者共享 id 计数但非"读写子代理",错误路径明示)。
161
188
  // ═══════════════════════════════════════════════════════════════════════════
162
189
 
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)
190
+ /** D1/N2 摘要形态:从子代理 _fullHistoryreal 消息——pushReal 累积)倒序收最近 N
191
+ * assistant 回合的一行描述(newest-first;assistant 消息 = 一回合——带 tool_calls =
192
+ * 工具轮、独立 content = 回复首行)。返回纯摘要非全量(N2 隔离——不把子代理噪音灌父)。 */
193
+ function recentTurnLines(child, limit) {
194
+ const hist = child?._fullHistory
195
+ if (!Array.isArray(hist)) return []
196
+ const out = []
197
+ for (let i = hist.length - 1; i >= 0 && out.length < limit; i--) {
198
+ const m = hist[i]
199
+ if (m?.role !== "assistant") continue
200
+ const tcs = Array.isArray(m.tool_calls)
201
+ ? m.tool_calls.filter((t) => typeof t?.function?.name === "string")
202
+ : []
203
+ if (tcs.length) out.push(`tools: ${tcs.map((t) => t.function.name).join(", ")}`)
204
+ else if (typeof m.content === "string" && m.content.trim()) {
205
+ const first = m.content.trim().split(/\r?\n/).find((l) => l.trim())
206
+ out.push((first ?? m.content.trim()).slice(0, 120))
207
+ } else out.push("(assistant — no content)")
208
+ }
209
+ return out
210
+ }
211
+
212
+ function queuedPositionOf(agent, key) {
213
+ const i = (agent?._asyncQueue ?? []).findIndex((e) => String(e.id) === key)
214
+ return i >= 0 ? i + 1 : undefined
215
+ }
216
+
217
+ /** recent 参数钳制(默认 ~5 条摘要——可参数;钳到 1..20 防超长 N2 失控)。 */
218
+ function clampRecent(n) {
219
+ const v = Number.parseInt(n, 10)
220
+ if (!Number.isFinite(v)) return 5
221
+ return Math.max(1, Math.min(20, v))
167
222
  }
168
223
 
169
224
  /**
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 }。
225
+ * subagent action:"observe"(D1——READONLY 查询,父回合内):按 id 拉运行中异步子代理的
226
+ * 最近活动快照——从 entry.childAgentsubagent-run.mjs start() 绑定)读**已落回合**摘要
227
+ * (_fullHistory 最近 N 条)+ _touchedFiles + turn/maxTurns + status + **in-flight 当前
228
+ * 工具**(评审 #1——从 dispatch runOne 维护的 child._inflightTools 读——非只读已落
229
+ * history:卡在长工具调用时 history 无新回合、恰是 observe 要检测的卡死态)。返摘要非
230
+ * 全量(N2)。observe = readonly——running/queued/done 均可查(done 终报 / queued 占位)。
177
231
  */
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)" }
232
+ export function executeObserveAction(args, ctx) {
233
+ if ((ctx.depth ?? 0) > 0) {
234
+ 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
235
  }
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)"}` }
236
+ const agent = ctx.agent
237
+ const id = args?.id
238
+ if (id === undefined || id === null || String(id) === "") {
239
+ 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
240
  }
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)` }
191
- }
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)` }
241
+ const key = String(id)
242
+ const entry = getAsyncPool(agent, "subagent")?.get(key)
243
+ if (!entry) {
244
+ if (getAsyncPool(agent, "advisor")?.has(key)) {
245
+ 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` })
194
246
  }
195
- return { err: `block ${key} is in state ${block.status} — freeze only reclaims awaitingDigest blocks whose report is already digested` }
247
+ return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
196
248
  }
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)` }
199
- }
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)` }
249
+ const child = entry.childAgent
250
+ const out = { id: key, role: entry.role, status: entry.status }
251
+ if (entry.status === "running") {
252
+ out.turn = entry.turn ?? 0
253
+ out.maxTurns = entry.maxTurns ?? 0
254
+ Object.assign(out, touchedSummary(entry, agent.cwd))
255
+ // in-flight 当前工具(评审 #1):child._inflightTools Set——dispatch runOne 在工具
256
+ // 执行前后维护——LLM 生成/工具间空隙为空;子代理 await 长工具调用时父回合可见它。
257
+ const inflight = child?._inflightTools
258
+ if (inflight instanceof Set && inflight.size > 0) out.currentTool = [...inflight]
259
+ const n = clampRecent(args?.recent)
260
+ out.recentTurns = recentTurnLines(child, n)
261
+ 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`
262
+ } else if (entry.status === "queued") {
263
+ out.position = queuedPositionOf(agent, key) ?? entry.position ?? null
264
+ out.note = "queued — not started yet; no activity to observe (starts when the slot frees / dependencies clear; action:'send' targets running only)"
265
+ } else {
266
+ // done = settled this turn, not yet consumed — final report rides the auto channel.
267
+ out.done = true
268
+ out.turn = entry.turn ?? 0
269
+ out.maxTurns = entry.maxTurns ?? 0
270
+ out.recentTurns = child ? recentTurnLines(child, clampRecent(args?.recent)) : []
271
+ 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
272
  }
203
- return { ok: true }
273
+ return JSON.stringify(out)
204
274
  }
205
275
 
206
276
  /**
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)。
277
+ * subagent action:"send"(D2——控制类豁免,同 cancel/panel-freeze——父回合内显式调用即
278
+ * 授权):按 id 向运行中异步子代理注入一条引导消息——push 进 entry._injected(仅 running
279
+ * 异步可 send);子回合边界经 consumeInjected 回调消费 → pushReal 成 user 回合进子历史
280
+ * 当作普通指令处理(注入不等同偏离豁免——子收敛/审计纪律不变)。settle/cancel/unknown
281
+ * /queued 明确错误。send→settle 竞态:入队后子未及下回合边界即 settle → 消息未投递
282
+ * settle 收尾附报告提示(不在此报错——send 返回时无法预知)。
220
283
  */
221
- export function executePanelAction(args, ctx) {
284
+ export function executeSendAction(args, ctx) {
285
+ if ((ctx.depth ?? 0) > 0) {
286
+ 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)" })
287
+ }
222
288
  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
- })
289
+ const id = args?.id
290
+ if (id === undefined || id === null || String(id) === "") {
291
+ 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
292
  }
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)" })
293
+ const message = args?.message
294
+ if (typeof message !== "string" || !message.trim()) {
295
+ 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
296
  }
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)
270
- }
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)" })
297
+ const key = String(id)
298
+ const entry = getAsyncPool(agent, "subagent")?.get(key)
299
+ if (!entry) {
300
+ if (getAsyncPool(agent, "advisor")?.has(key)) {
301
+ 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) — track it with action:'status' (role:"advisor") or wait for its report to arrive automatically` })
273
302
  }
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
- })
303
+ return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
279
304
  }
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
305
+ if (entry.done || entry.cancelled) {
306
+ 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)` })
307
+ }
308
+ if (entry.status !== "running") {
309
+ 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` })
310
+ }
311
+ entry._injected ??= []
312
+ entry._injected.push(String(message).trim())
313
+ return JSON.stringify({
314
+ id: key,
315
+ status: "delivered",
316
+ queued: entry._injected.length,
317
+ 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
318
  })
291
- return JSON.stringify({ panel })
292
319
  }
293
320
 
321
+ // ═══════════════════════════════════════════════════════════════════════════
322
+ // §19.6 subagent panel 检查工具(AGENT-LOOP.md §19.6——F-P1..P3/D-P1..P4)
323
+ // ═══════════════════════════════════════════════════════════════════════════
324
+ // 2026-09-08 拆分(Module Split Policy——601 > 500 硬限跨档):§19.6 面板段迁至
325
+ // ./subagent-panel.mjs(executePanelAction/panelFreezeGate/blockKeyIn——verbatim +
326
+ // ASYNC-RESULT-CONTAINER D1/D2 落地);本面保留 re-export 保 subagent.mjs 既有
327
+ // import 面(subagent-async 尾部 re-export 先例)。
328
+ export { executePanelAction } from "./subagent-panel.mjs"
329
+
294
330
  /**
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 回父。
331
+ * subagent action:"escalate"(§19 D-M4——退役 escalate 工具语义原样,ESCALATE.md
332
+ * §25 D-R17b——R17:缺省 async——后台飞刀 + settle 三分类 digest——async:false 保
333
+ * 同步旧路径)。飞刀——交给 consultModels 池里更强模型(WRITE + 术后报告)。约束全
334
+ * 保留:depth-0 only / 工程模式拒 / consultModels 空拒 / relay 前缀 `escalate#N/`
335
+ * (与既有前缀同名——TUI 路由零改动)/ mutations merge 回父(async 路径在 settle 分类)。
299
336
  */
300
337
  export async function executeEscalateAction(args, ctx) {
301
338
  const parent = ctx.agent
@@ -334,6 +371,16 @@ export async function executeEscalateAction(args, ctx) {
334
371
  }
335
372
 
336
373
  const tag = label(pick)
374
+
375
+ // §25 D-R17b (R17 — 决策点 ③): escalate 缺省 async — the launch returns an ack
376
+ // {id, role:"escalate", status:"running"|"queued"} and the flight runs in the
377
+ // background (shared other pool; settle 三分类 → pending 单容器 digest——
378
+ // ASYNC-RESULT-CONTAINER.md D2)。
379
+ // `async: false` keeps the legacy synchronous flight below (backward compat).
380
+ if (args?.async !== false) {
381
+ return launchEscalateAsync(parent, ctx, { task: String(task), provider, tag, effortNote })
382
+ }
383
+
337
384
  const relayPrefix = makeRelay(parent, "escalate", ctx.callbacks?.onToken, provider.model ?? tag)
338
385
 
339
386
  // 无墙钟 watchdog——turn cap 即成本预算(2026-08-16 rationale:固定墙钟会误杀正常慢速
@@ -350,13 +397,13 @@ export async function executeEscalateAction(args, ctx) {
350
397
  let escT0 = Date.now()
351
398
  try {
352
399
  // 全写路径(role "coder"):权限经父 onPermissionRequest,mutations merge 回父
400
+ // G3(施工②):overlay 摘除——coder 人格槽由 assemblePrompt 场景表承载(G3 映射)。
353
401
  child = createAgent({
354
402
  provider,
355
403
  tools: parent.tools,
356
404
  config: parent.config,
357
405
  cwd: parent.cwd,
358
406
  memory: parent.memory,
359
- overlay: CODER_OVERLAY,
360
407
  role: "coder",
361
408
  })
362
409
  child._logId = escId // LOGGING:子内事件归属(escalate#N)
@@ -385,7 +432,7 @@ export async function executeEscalateAction(args, ctx) {
385
432
  child, task, { ...childCallbacks, onPermissionRequest: parent.autoApprove ? async () => true : (ctx.onPermissionRequest ?? null) },
386
433
  runOpts,
387
434
  {
388
- // escalate has NO permQueue: prompts go straight to the user (T-L spec).
435
+ // sync escalate has NO permQueue——async 飞行权限走 _permQueue(escalate-async.mjs): prompts go straight to the user (T-L spec).
389
436
  askContinue: (e) => (ctx.onPermissionRequest
390
437
  ? ctx.onPermissionRequest("continue", { turns: e.turn, agent: tag })
391
438
  : Promise.resolve(false)),