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
@@ -1,10 +1,12 @@
1
1
  /**
2
- * subagent-async.mjs — async subagent 机械 + 共享 post-spawn 管线 + check/cancel 动作执行器
3
- * (AGENT-LOOP.md §19:subagent 单工具六动作 spawn/check/status/escalate/cancel/panel——
4
- * spawn 路径与工具面在 subagent.mjs;check/cancel 动作执行器与机械、管线在本模块)。
5
- * 内容:resolveChildProvider / async 常量(ASYNC_SUBAGENT_LIMIT/MAX_ASYNC_CHECKS)/
6
- * executeCheckAction / executeCancelAction + cancelAsyncSubagent(§19.5 D-M6——工具与
7
- * TUI ⏹ 共用)/ runChildPipeline / injectAsyncResult / buildChildRunOpts / mergeChildMutations。
2
+ * subagent-async.mjs — async subagent 机械 + 共享 post-spawn 管线 + cancel 动作执行器
3
+ * (AGENT-LOOP.md §19:subagent 单工具多动作——spawn/status/escalate/cancel/panel/
4
+ * consume-design + observe/send(SUBAGENT-OBSERVE-SEND)——§19.8 check 已删;spawn 路径与
5
+ * 工具面在 subagent.mjs;cancel 动作执行器与机械、管线在本模块——check 执行器随 §19.8 删除)。
6
+ * 内容:resolveChildProvider / async 池常量与域助手(ASYNC_POOL_LIMITS/poolDomainOf/
7
+ * resolvePoolLimits/poolLimitsFor/runningPoolCount——§24 D-24a)/ executeCancelAction +
8
+ * cancelAsyncSubagent(§19.5 D-M6——工具与 TUI ⏹ 共用)/ runChildPipeline /
9
+ * injectAsyncResult / buildChildRunOpts / mergeChildMutations。
8
10
  * 拆分(2026-09-05——Module Split Policy §20.9——纯迁移零行为变化):§20 调度器 + 文件域
9
11
  * 组 → ./subagent-scheduler.mjs(尾部 re-export 保测试动态 import 面——queueRunnable/
10
12
  * describeBlockers);status/panel/escalate 动作执行器 → ./subagent-actions.mjs。
@@ -17,13 +19,92 @@ import { runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
17
19
  import { pushReal } from "../context.mjs"
18
20
  import { offloadToolResult } from "../agent/helpers.mjs"
19
21
  import {
20
- describeBlockers, dependentLabels, detectStall, maybeRefillAsync, refreshQueuedTokens, STALL_NOTE,
22
+ dependentLabels, maybeRefillAsync, refreshQueuedTokens,
21
23
  } from "./subagent-scheduler.mjs"
24
+ // §24 D-24b (R13): advisor-pool cancel fallback + mutation logging for merged
25
+ // code(lazy function-level cycle——advisor-async → async-settle → scheduler →
26
+ // 本模块——全函数级绑定无求值期依赖,环安全)。
27
+ import { cancelAsyncAdvisor, noteMutations } from "./advisor-async.mjs"
22
28
 
23
- // Async subagent limits (AGENT-LOOP.md §15 D-A4): mechanical concurrency cap for
24
- // background spawns + the per-turn check budget (consult-style loop guard).
25
- export const ASYNC_SUBAGENT_LIMIT = 4
26
- export const MAX_ASYNC_CHECKS = 3
29
+ // Async pool limits per role domain (AGENT-LOOP.md §24 D-24a R14, 2026-09-06):
30
+ // the old single cap (ASYNC_SUBAGENT_LIMIT = 4, §15 D-A4) evolved into two
31
+ // independent pools eng-coder 4 / other roles 4 (user ruling "eng-coder 四路,
32
+ // 其他 4 路") — a full engCoder pool never blocks an explore spawn and vice versa
33
+ // (same-domain cap still 4, cross-domain total up to 8). The per-turn check
34
+ // budget was deleted with the check action (§19.8 — results arrive only via the
35
+ // auto channel; no loop guard needed).
36
+ // ⚠ 与 config.mjs DEFAULTS.agent.poolLimits 逐键同值(loadConfig/settings 默认源)——
37
+ // 耦合锚 T-24a4 断言锁住——勿单侧改默认。
38
+ export const ASYNC_POOL_LIMITS = { engCoder: 4, other: 4 }
39
+
40
+ /**
41
+ * Role → pool domain (single source of truth — §24 D-24a 修正 #8): the CLI role
42
+ * enum/assembly table is subagent.mjs's ROLES = { explore, plan, coder, eng-coder }
43
+ * (mode-filtered: normal → explore/plan/coder, engineering → explore/plan/eng-coder).
44
+ * eng-coder → engCoder pool; every other role (including unknown roles — fail-safe)
45
+ * → other pool. escalate spawns its expert internally (role "coder" — other pool).
46
+ */
47
+ export function poolDomainOf(role) {
48
+ return role === "eng-coder" ? "engCoder" : "other"
49
+ }
50
+
51
+ /**
52
+ * Per-domain limit validation (T-24a4): each key must be a positive integer ≥1 —
53
+ * invalid (0 / -1 / "abc") or absent keys fall back to that domain's default (4),
54
+ * independent per key (a partial config — e.g. settings set
55
+ * agent.poolLimits.engCoder — takes effect with the untouched domain at default).
56
+ * Non-object raw (missing / string / number / array) → both domains at default.
57
+ */
58
+ export function resolvePoolLimits(raw) {
59
+ const limits = { ...ASYNC_POOL_LIMITS }
60
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return limits
61
+ for (const key of Object.keys(ASYNC_POOL_LIMITS)) {
62
+ const v = raw[key]
63
+ if (Number.isInteger(v) && v >= 1) limits[key] = v
64
+ }
65
+ return limits
66
+ }
67
+
68
+ /** One-time fallback warn dedupe (warnedModels / warnedContextProviders precedent). */
69
+ const warnedPoolConfigs = new Set()
70
+
71
+ /**
72
+ * Effective per-domain limits — read at EVERY pool admission (D-24a 生效语义:
73
+ * 不缓存常驻——配置变更即生效下个 spawn; /config 与 settings 保存经 reloadConfig /
74
+ * 热应用替换 agent.config——本函数每次读最新值). Invalid present values emit ONE
75
+ * warning per value shape, then fall back (timeoutMs precedent).
76
+ */
77
+ export function poolLimitsFor(agent) {
78
+ const raw = agent?.config?.agent?.poolLimits
79
+ const limits = resolvePoolLimits(raw)
80
+ if (raw !== undefined && raw !== null && (typeof raw !== "object" || Array.isArray(raw))) {
81
+ warnPoolFallback(`poolLimits has invalid shape (${JSON.stringify(raw)})`, "shape:" + JSON.stringify(raw))
82
+ return limits
83
+ }
84
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
85
+ for (const key of Object.keys(ASYNC_POOL_LIMITS)) {
86
+ const v = raw[key]
87
+ if (v !== undefined && !(Number.isInteger(v) && v >= 1)) {
88
+ warnPoolFallback(`poolLimits.${key} = ${JSON.stringify(v)}`, `${key}:` + JSON.stringify(v))
89
+ }
90
+ }
91
+ }
92
+ return limits
93
+ }
94
+
95
+ function warnPoolFallback(what, sig) {
96
+ if (warnedPoolConfigs.has(sig)) return
97
+ warnedPoolConfigs.add(sig)
98
+ console.warn(`[config] agent.poolLimits: ${what} — must be a positive integer ≥1 — falling back to default ${JSON.stringify(ASYNC_POOL_LIMITS)} (AGENT-LOOP.md §24 D-24a)`)
99
+ }
100
+
101
+ /** Running count within ONE pool domain (口径同 §15 D-A1/T6: queued 与已完成不计入;
102
+ * 条目带 _pool 域字段——spawn 时 poolDomainOf(role) 落位;缺字段(手工/旧条目)按
103
+ * other——既有 coder 域测试语义不变)。 */
104
+ export function runningPoolCount(parent, pool) {
105
+ return [...(parent?._asyncSubagents?.values() ?? [])]
106
+ .filter((e) => e.status === "running" && (e._pool ?? "other") === pool).length
107
+ }
27
108
 
28
109
  /**
29
110
  * Resolve the sub-agent's provider from a model override string (shared with the
@@ -60,160 +141,6 @@ export function resolveChildProvider(parent, modelArg) {
60
141
  return { ...parent.provider, model: modelArg }
61
142
  }
62
143
 
63
- /** Wait for an async entry to settle (or the parent signal to abort), parked on
64
- * the agent's waiter list — the entry settle finally wakes every waiter (same
65
- * pattern as consult_check's session waiters). Returns "aborted" on signal. */
66
- function wakeOnAsyncSettle(agent, ctx) {
67
- return new Promise((resolve) => {
68
- const cleanup = () => {
69
- const i = (agent._asyncWaiters ?? []).indexOf(w)
70
- if (i >= 0) agent._asyncWaiters.splice(i, 1)
71
- ctx.signal?.removeEventListener("abort", onAbort)
72
- }
73
- const w = () => { cleanup(); resolve("settled") }
74
- const onAbort = () => { cleanup(); resolve("aborted") }
75
- ;(agent._asyncWaiters ??= []).push(w)
76
- if (ctx.signal) {
77
- if (ctx.signal.aborted) { onAbort(); return }
78
- ctx.signal.addEventListener("abort", onAbort, { once: true })
79
- }
80
- })
81
- }
82
-
83
- /**
84
- * subagent action:"check" (§19 — the retired subagent_check's semantics verbatim,
85
- * AGENT-LOOP.md §15 D-A2): fetch async subagent results.
86
- * - id omitted → the next completed child in ARRIVAL order (first finished first)
87
- * - id given → block until THAT child finishes (queued items wait for their start)
88
- * - n (required) → 1-based read counter, strictly incrementing per run (loop
89
- * guard); capped at MAX_ASYNC_CHECKS per turn — beyond that, use the turn-end
90
- * auto-wait. Errors never consume results.
91
- * Consumed entries are deleted from the map — a re-check of the same id is the
92
- * same "unknown async subagent id" error (T12).
93
- */
94
- export async function executeCheckAction(args, ctx) {
95
- const agent = ctx.agent
96
- const map = agent._asyncSubagents ?? new Map()
97
- const { id, n } = args ?? {}
98
- // Strict 1-based incrementing read counter (D-A2, review #1): out-of-order /
99
- // repeated n is rejected WITHOUT consuming a result (T14).
100
- const lastN = agent._asyncCheckLastN ?? 0
101
- if (!Number.isInteger(n) || n !== lastN + 1) {
102
- return JSON.stringify({ status: "error", error: "invalid read counter — pass n = lastN+1" })
103
- }
104
- if (n > MAX_ASYNC_CHECKS) {
105
- return JSON.stringify({ status: "error", error: "check limit exceeded — use turn-end auto-wait for the rest" })
106
- }
107
- agent._asyncCheckLastN = n
108
-
109
- let target = null
110
- if (id !== undefined && id !== null && String(id) !== "") {
111
- target = map.get(String(id))
112
- // Unknown OR already-consumed ids (consumed entries are deleted) — same error (T12).
113
- if (!target) return JSON.stringify({ id: String(id), status: "error", error: `unknown async subagent id: ${id}` })
114
- }
115
-
116
- // Block until the target settles (specific id / next completed in arrival order).
117
- // §20(advisor code review 🟡 处置):不可启动的 queued 条目会让阻塞等待永久悬挂
118
- // (check 是同步工具调用——模型回合被钉死——仅 Ctrl+C 可解)。补位(refill)只由
119
- // settle/cancel/spawn 事件驱动:**池内无 running 条目 = 无未来 settle 事件 = queued
120
- // 条目永不启动**(槽满等位的 slot 条目在 running 归零时已被最后一次 settle 的 refill
121
- // 启动——此时仍 queued 者必为不可启动:depc 锁定或阻塞源本身不可启动)。
122
- for (;;) {
123
- if (target) {
124
- if (target.done) break
125
- // 守卫 ①(target 级):queued 目标在无 running 池中不可启动 → 立即返回(无论
126
- // wait/depc——阻塞源链底为 depc 的 wait 条目同样无 settle 可期)。
127
- if (target.status === "queued" && !target.cancelled) {
128
- const blk = describeBlockers(agent, target)
129
- if (blk.kind === "depc") {
130
- return JSON.stringify({
131
- id: String(target.id), status: "queued", waiting: "dependency-cancelled",
132
- reason: blk.detail,
133
- note: "check would block forever — this task is locked by a cancelled/failed dependency and will not start on its own; cancel it (action:'cancel') or run an AUTO session to release it (AGENT-LOOP.md §20 round2 #3)",
134
- })
135
- }
136
- if (![...map.values()].some((e) => e.status === "running")) {
137
- const qi = (agent._asyncQueue ?? []).indexOf(target)
138
- const out = { id: String(target.id), status: "queued", position: qi >= 0 ? qi + 1 : undefined }
139
- if (blk.detail) out.reason = blk.detail
140
- // §21.1 P-SL2(D-SL2):停滞机械检测——目标处于不可自行解除的等待闭包(无
141
- // running + 每 queued 的 blocker 均闭包内 + 无 depc)→ 明确报错列本任务
142
- // 阻塞链 + 引导 cancel 破环重派(F-SL2——非静默);不满足停滞判据的形态
143
- // (单 queued/depc 滞留/外逃 blocker)维持下方原守卫文本——零行为变化。
144
- const stall = detectStall(agent)
145
- if (stall) {
146
- const chain = stall.chains.find((c) => c.task === target)?.text ?? stall.chains[0]?.text
147
- out.stall = true
148
- out.note = `scheduler stall — the queued tasks block each other in a closed wait loop that can never settle on its own:\n${chain}\n${STALL_NOTE}`
149
- } else {
150
- out.note = "check would block indefinitely — this queued task cannot start while the pool has no running task (starts are settle-driven); cancel it (action:'cancel') or make pool progress (AUTO session starts it on the next settle/refill)"
151
- }
152
- return JSON.stringify(out)
153
- }
154
- }
155
- const woke = await wakeOnAsyncSettle(agent, ctx)
156
- if (woke === "aborted") return JSON.stringify({ done: true, stopped: true })
157
- continue
158
- }
159
- const completed = [...map.values()].filter((e) => e.done)
160
- if (completed.length > 0) {
161
- target = completed.sort((a, b) => (a._settleSeq ?? 0) - (b._settleSeq ?? 0))[0]
162
- break
163
- }
164
- if (map.size === 0) return JSON.stringify({ done: true })
165
- // 守卫 ②(arrival-order):池内无 running(completed 已空 → 无 done)且仍有条目 →
166
- // 全为 queued 且永不启动 → 立即返回明确错误(防无界悬挂——补 cancel 引导)。
167
- if (![...map.values()].some((e) => e.status === "running")) {
168
- // §21.1 P-SL2(D-SL2):停滞机械检测——全 queued 且每 blocker 闭包内(无 depc 等
169
- // 外部可解形态)→ 明确错误逐条列阻塞链(F-SL2——列链 + 引导 cancel 破环重派);
170
- // 不满足停滞判据的形态(单 queued/depc 滞留/外逃 blocker)维持原守卫文本——零变化。
171
- const stall = detectStall(agent)
172
- if (stall) {
173
- return JSON.stringify({
174
- status: "error",
175
- error: `scheduler stall — the pool holds only queued tasks that block each other in closed wait loops; nothing can ever settle on its own:\n${stall.chains.map((c) => c.text).join("\n")}\n${STALL_NOTE}`,
176
- })
177
- }
178
- const stuck = [...map.values()]
179
- .map((e) => `${e.role}#${e.id}(${describeBlockers(agent, e).kind === "depc" ? "dependency-cancelled" : "blocked"})`)
180
- .join(", ")
181
- return JSON.stringify({
182
- status: "error",
183
- error: `nothing will settle — the pool holds only queued task(s) that cannot start without a running task: ${stuck}; cancel them (action:'cancel') or make pool progress (AUTO session starts them on the next settle/refill)`,
184
- })
185
- }
186
- const woke = await wakeOnAsyncSettle(agent, ctx)
187
- if (woke === "aborted") return JSON.stringify({ done: true, stopped: true })
188
- }
189
-
190
- map.delete(String(target.id))
191
- // §20(advisor code review 🟡 处置):check 消费与挂起期 settle 的竞态——settle 在挂起
192
- // 分支先把条目移交 _pendingAsyncResults(本 check 在途等待期间发生——digest 回合)——
193
- // 消费时若条目已被移入 pending,反向清除(两消费点互斥——防下轮 prepareRun 重复注入
194
- // ——D-S3"只注入一次"不变式——同一报告双送达违例)。
195
- const pend = agent._pendingAsyncResults
196
- if (Array.isArray(pend)) {
197
- const pendIdx = pend.findIndex((x) => String(x.id) === String(target.id))
198
- if (pendIdx >= 0) pend.splice(pendIdx, 1)
199
- }
200
- // §20 D-SD5 终态墓碑(T-SD14):消费即终态——dependsOn 引用该 id 的条目视其终态
201
- // 满足/标注(consumed-ok = 已满足;failed/cancelled = 依赖取消/失败分支)。写于
202
- // 观察 cancelled/error 之后——取消/失败条目不被误记 consumed。
203
- if (target.cancelled) {
204
- const tombstones = (agent._asyncTombstones ??= new Map())
205
- tombstones.set(String(target.id), { status: "cancelled", role: target.role })
206
- // Cancelled entries are removed from the pool at their cancel — an in-flight
207
- // check that held the entry object observes `cancelled` and reports the same
208
- // unknown-id error a fresh check gets (nothing to consume; §19.5 T-M27).
209
- return JSON.stringify({ id: String(target.id), status: "error", error: `unknown async subagent id: ${target.id}` })
210
- }
211
- const tombstones = (agent._asyncTombstones ??= new Map())
212
- tombstones.set(String(target.id), { status: target.error ? "failed" : "consumed", role: target.role })
213
- if (target.error) return JSON.stringify({ id: String(target.id), status: "error", error: target.error })
214
- return JSON.stringify({ id: String(target.id), role: target.role, status: "done", report: target.report ?? "" })
215
- }
216
-
217
144
  /**
218
145
  * §19.5 D-M6 cancel 核心(工具路径与 TUI ⏹ 共用):定向中止单个后台子代理,id 必填。
219
146
  * - queued 目标(未启动无 controller):出队 + 后续 position 前移 + settle waiter
@@ -235,7 +162,7 @@ export function cancelAsyncSubagent(agent, id) {
235
162
  }
236
163
  if (entry.cancelled) return { id: key, status: "cancelled" } // abort already in flight — idempotent
237
164
  if (entry.status === "queued") {
238
- // 出队 + position 释放/前移;settle 使在途 check waiter 终止(观察 cancelled
165
+ // 出队 + position 释放/前移(§19.5 D-M6——无 abort
239
166
  const queue = agent._asyncQueue ?? []
240
167
  const qi = queue.indexOf(entry)
241
168
  if (qi >= 0) {
@@ -251,7 +178,6 @@ export function cancelAsyncSubagent(agent, id) {
251
178
  const tombstones = (agent._asyncTombstones ??= new Map())
252
179
  tombstones.set(key, { status: "cancelled", role: entry.role })
253
180
  entry._settle?.()
254
- for (const w of agent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
255
181
  return { id: key, status: "cancelled", was: "queued" }
256
182
  }
257
183
  // running:标记 + 条目 abort——settle finally 跑 cancelled 分支(移除 + stopped + 提醒)
@@ -276,6 +202,12 @@ export function executeCancelAction(args, ctx) {
276
202
  const key = String(id)
277
203
  const agent = ctx.agent
278
204
  const entry = agent._asyncSubagents?.get(key)
205
+ // §24 D-24b (②-6b): an id that names no async SUBAGENT falls through to the
206
+ // async ADVISOR pool (the background reviews share the cancel surface — ⏹ on
207
+ // an advisor block / action:'cancel' with an advisor id abort that review).
208
+ if (!entry && agent?._asyncAdvisors?.has(key)) {
209
+ return JSON.stringify(cancelAsyncAdvisor(agent, key))
210
+ }
279
211
  const wasQueued = entry?.status === "queued"
280
212
  // 依赖者快照(出队前——用于 AUTO 分支判定"是否有依赖者被本次取消波及";note 组装在
281
213
  // refill 后按**仍 queued** 的实况重算——防 AUTO 已自动启动后文案称 "stay queued")
@@ -350,22 +282,40 @@ export async function runChildPipeline(child, input, childOpts, childRunOpts, {
350
282
 
351
283
  /**
352
284
  * Inject one settled async entry into the parent history as a user-role reminder
353
- * (§17 D-S3 — single shared form for BOTH consumption points: turn-end collection
354
- * (collectSettledAsync, agent.mjs) and the run-start _pendingAsyncResults injection;
355
- * the message shape is identical to the §15 collector's). Consumed = the caller
356
- * removes the entry from its container; no double-inject across the two paths.
285
+ * (§17 D-S3 — the auto channel, sole consumption path since §19.8: turn-end
286
+ * collection (collectSettledAsync, agent.mjs) and the run-start _pendingAsyncResults
287
+ * injection; the message shape is identical to the §15 collector's). Consumed =
288
+ * the caller removes the entry from its container; no double-inject across the
289
+ * two paths (D-S3 "inject once" invariant).
357
290
  */
358
291
  export async function injectAsyncResult(agent, entry) {
359
292
  const body = entry.error ?? entry.report ?? "(no report)"
360
293
  const preview = await offloadToolResult(String(body), `async-subagent-${entry.id}`)
294
+ // §24 D-24b: advisor entries label themselves (role "advisor") — the digest
295
+ // reminder says "async advisor review #N finished" (T-24b2 shape); subagent
296
+ // entries keep the legacy wording verbatim.
297
+ // §25 D-R17b (R17): escalate entries (role "escalate" — async 飞刀) label
298
+ // themselves the same way — the entry report body carries the merge/overlap
299
+ // notes composed at settle (done/error classification — digest 指令语义 = 已
300
+ // merge 报告可继续处置——动作域仍按消费回合档位——无族例外).
301
+ let label
302
+ if (entry.role === "advisor") {
303
+ label = `[System reminder: async advisor review #${entry.id} finished]\n${escapeXml(preview)}`
304
+ } else if (entry.role === "escalate") {
305
+ label = entry.error != null
306
+ ? `[System reminder: async escalate #${entry.id} ended with an error]\n${escapeXml(preview)}`
307
+ : `[System reminder: async escalate #${entry.id} finished — post-op report (mutations merged)]\n${escapeXml(preview)}`
308
+ } else {
309
+ label = `[System reminder: async subagent #${entry.id} (${entry.role}) finished]\n${escapeXml(preview)}`
310
+ }
361
311
  pushReal(agent, {
362
312
  role: "user",
363
- content: `[System reminder: async subagent #${entry.id} (${entry.role}) finished]\n${escapeXml(preview)}`,
313
+ content: label,
364
314
  })
365
315
  // §20 D-SD5 终态墓碑:本函数是全部自动注入路径的共享形态(回合尾 collect + 挂起
366
316
  // digest 首行注入)——注入即消费(调用方随即从容器移除)——dependsOn 引用该 id 的
367
- // 后续 spawn 视为已满足(T-SD14 check 消费语义;error 条目记 failed——依赖取消/
368
- // 失败分支照旧,不误标成功)。
317
+ // 后续 spawn 视为已满足(T-SD14 消费终态语义——§19.8 check 消费路径删除,本自动
318
+ // 通道为唯一消费方;error 条目记 failed——依赖取消/失败分支照旧,不误标成功)。
369
319
  const tombstones = (agent._asyncTombstones ??= new Map())
370
320
  tombstones.set(String(entry.id), { status: entry.error != null ? "failed" : "consumed", role: entry.role })
371
321
  }
@@ -409,9 +359,14 @@ export function mergeChildMutations(parent, child) {
409
359
  // to the parent's guard state.
410
360
  if (!child._mutatedThisRun || !(child._touchedFiles?.length)) return false
411
361
  parent._mutatedThisRun = true
362
+ const merged = []
412
363
  for (const abs of child._touchedFiles ?? []) {
413
364
  if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
365
+ merged.push(abs)
414
366
  }
367
+ // §24 D-24b: merged code mutates the parent's state — log it for the stale
368
+ // scan of in-flight reviews (a review whose code changed under it is stale).
369
+ noteMutations(parent, merged)
415
370
  if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
416
371
  if (parent._verifiedThisRun) {
417
372
  parent._verifiedThisRun = false
@@ -0,0 +1,153 @@
1
+ /**
2
+ * subagent-panel.mjs — subagent action:"panel" 执行器(§19.6——2026-09-08 自
3
+ * subagent-actions.mjs 拆分——Module Split Policy:601 > 500 硬限跨档,§19.6 面板段
4
+ * verbatim 迁入 + ASYNC-RESULT-CONTAINER.md D1/D2 落地(池 accessor getAsyncPool +
5
+ * pending 单容器 _pendingAsyncResults 四族统一——escalate/consult 独立族退役))。
6
+ * 内容:executePanelAction(D-P2——readonly 视图面 + 门控 freeze)+ panelFreezeGate
7
+ * (D-P3 冻结门控)+ blockKeyIn(块归属判定随行)。
8
+ * subagent-actions.mjs 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面。
9
+ */
10
+ import { getAsyncPool } from "./async-settle.mjs"
11
+
12
+ /** 面板块 key(role#N)在池(Map——条目值)/pending 单容器(数组)中的归属判定。 */
13
+ function blockKeyIn(container, key) {
14
+ const entries = container instanceof Map ? [...container.values()] : (container ?? [])
15
+ return entries.some((e) => `${e.role}#${e.id}` === key)
16
+ }
17
+
18
+ /**
19
+ * §19.6 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents/
20
+ * _asyncAdvisors——经 getAsyncPool accessor——D1)无对应运行条目 + pending 单容器
21
+ * (_pendingAsyncResults——D2)无对应条目的块(= 已消化驻留块——报告已入模型上下文
22
+ * ——pending 已消费——状态滞后——补发冻结不破坏任何顺序)。
23
+ * - pending 仍有对应(报告未达模型)→ 拒绝(提前回收破坏消化顺序——T-P3)
24
+ * - 不存在的 key / 仍 running / done 的块 → 拒绝(T-P4——running 块 settle 时自冻)
25
+ * - 无镜像 → 拒绝(headless/VS Code——freeze 不可用——T-P5)
26
+ * 错误信息明确(模型可解释 + 自助修正)。返回 { ok:true } 或 { err }。
27
+ */
28
+ function panelFreezeGate(agent, key) {
29
+ const snap = agent._panelSnapshot
30
+ if (!Array.isArray(snap)) {
31
+ 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)" }
32
+ }
33
+ const block = snap.find((b) => b.key === key)
34
+ if (!block) {
35
+ const live = snap.map((b) => `${b.key}(${b.status})`).join(", ")
36
+ return { err: `unknown panel block key: ${key} — the live panel holds: ${live || "(no blocks)"}` }
37
+ }
38
+ if (block.status !== "awaitingDigest") {
39
+ if (block.status === "done") {
40
+ 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)` }
41
+ }
42
+ if (block.status === "running") {
43
+ 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)` }
44
+ }
45
+ return { err: `block ${key} is in state ${block.status} — freeze only reclaims awaitingDigest blocks whose report is already digested` }
46
+ }
47
+ if (blockKeyIn(getAsyncPool(agent, "subagent"), key) || blockKeyIn(getAsyncPool(agent, "advisor"), key)) {
48
+ return { err: `block ${key} still has a live pool entry — it is NOT a digested-stuck block (freeze refused; status action shows the pool)` }
49
+ }
50
+ // ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
51
+ // 独立族退役)。
52
+ if (blockKeyIn(agent._pendingAsyncResults, key)) {
53
+ return { err: `block ${key} is still genuinely awaiting digestion — its report is still pending 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)` }
54
+ }
55
+ return { ok: true }
56
+ }
57
+
58
+ /**
59
+ * §19.6 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
60
+ * freeze 优先):
61
+ * - view(缺省——返回镜像区块列表):agent._panelSnapshot = TUI 面板镜像(块级
62
+ * 状态变更点由 subagent-blocks syncPanelSnapshot 同步刷新——与用户所见一致——
63
+ * index.mjs 装配 state._agent)。awaitingDigest 条目**读时交叉**
64
+ * _pendingAsyncResults/池 标注 digested(round1 #3——digested:true
65
+ * = 报告已消化但块仍驻留——异常块——freeze 候选;模型可定位解释 UI 怪相)。
66
+ * - freeze:key(D-P3 门控通过 → 发 key + "/" + ⟦ev⟧done 哨兵字面 token——
67
+ * onToken——TUI routeSubToken 冻结回收——落位复用 sub._freezeAt settle 锚点
68
+ * splice,无锚点尾推兜底——§17.5.5 同口径——round1 #2)。
69
+ * 无镜像(headless/VS Code——D-P2 round1 #1:webview 无 state.subTasks 对应物——
70
+ * 7.2.3.2 #8 先例)→ view 恒降级池视图(双池经 getAsyncPool + pending 单容器
71
+ * 合成)+ no panel 注;freeze 报不可用。CLI-only 完整能力(AC-P4)。
72
+ */
73
+ export function executePanelAction(args, ctx) {
74
+ const agent = ctx.agent
75
+ const freezeKey = (args?.freeze !== undefined && args?.freeze !== null && String(args.freeze) !== "")
76
+ ? String(args.freeze)
77
+ : null
78
+ // ── freeze 面(优先——D-P2 单动作双参互斥)──
79
+ if (freezeKey) {
80
+ if ((ctx.depth ?? 0) > 0) {
81
+ 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)" })
82
+ }
83
+ const gate = panelFreezeGate(agent, freezeKey)
84
+ if (gate.err) return JSON.stringify({ status: "error", error: gate.err })
85
+ if (!ctx.callbacks?.onToken) {
86
+ return JSON.stringify({ status: "error", error: `panel mirror present but no token relay in this context — the freeze of ${freezeKey} cannot reach the TUI` })
87
+ }
88
+ // 门控通过 → 发 done 冻结事件(settle 同机制字面格式——TUI routeSubToken done
89
+ // 分支冻结回收——落位 _freezeAt settle 锚点 splice;无锚点(旧会话残留)时
90
+ // freezeSubTaskLines 尾推兜底——注明两种落位,模型不被误导(advisor 🟡1)。
91
+ ctx.callbacks.onToken(`${freezeKey}/⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
92
+ return JSON.stringify({
93
+ key: freezeKey,
94
+ status: "frozen",
95
+ 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)",
96
+ })
97
+ }
98
+ // ── view 面(缺省——readonly)──
99
+ // 双参互斥(D-P2):freeze 优先;显式 view:false 且无 freeze = 无请求可执行——报错。
100
+ if (args?.view === false) {
101
+ 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)" })
102
+ }
103
+ const snap = agent._panelSnapshot
104
+ if (!Array.isArray(snap)) {
105
+ // F-P3 降级:无镜像(headless/VS Code/子代理上下文——CLI TUI-only 完整能力)→
106
+ // 池视图(双池经 getAsyncPool——运行/排队条目 + pending 单容器待消化条目)
107
+ const blocks = []
108
+ const queue = agent._asyncQueue ?? []
109
+ for (const e of [...(getAsyncPool(agent, "subagent")?.values() ?? []), ...(getAsyncPool(agent, "advisor")?.values() ?? [])]) {
110
+ const b = { key: `${e.role}#${e.id}`, role: e.role }
111
+ if (e.status === "running") {
112
+ b.status = "running"
113
+ b.elapsedSec = e.startedAt ? Math.max(0, Math.floor((Date.now() - e.startedAt) / 1000)) : 0
114
+ } else if (e.status === "queued") {
115
+ b.status = "queued"
116
+ const qi = queue.indexOf(e)
117
+ b.position = qi >= 0 ? qi + 1 : (e.position ?? null)
118
+ } else {
119
+ b.status = "done" // 回合内 settle 未取——自动通道(回合尾 collect/挂起 digest)送达
120
+ }
121
+ blocks.push(b)
122
+ }
123
+ // ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
124
+ // 独立族退役)——族注记按 role 保留。
125
+ for (const e of agent._pendingAsyncResults ?? []) {
126
+ const note = e.role === "consult"
127
+ ? "consultation digest pending — injected at the next run start (§25 D-R17a)"
128
+ : e.role === "escalate"
129
+ ? "report pending — injected at the next run start (§25 D-R17b)"
130
+ : "report pending — injected at the next run start (§17)"
131
+ blocks.push({ key: `${e.role}#${e.id}`, role: e.role, status: "awaitingDigest", note })
132
+ }
133
+ return JSON.stringify({
134
+ degraded: true,
135
+ 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",
136
+ panel: blocks,
137
+ })
138
+ }
139
+ const panel = snap.map((b) => {
140
+ const out = { key: b.key, role: b.role, status: b.status }
141
+ if (b.status === "running") {
142
+ out.elapsedSec = b.startedAt ? Math.max(0, Math.floor((Date.now() - b.startedAt) / 1000)) : 0
143
+ } else if (b.status === "awaitingDigest") {
144
+ // 读时交叉(round1 #3):pending/池均无对应 = 报告已消化(注入即从两者移除)——
145
+ // 块驻留 = 状态滞后——digested:true(freeze 候选——模型可定位异常块)。
146
+ // ASYNC-RESULT-CONTAINER.md D2:pending 单容器参与比对(四族统一)。
147
+ out.digested = !blockKeyIn(agent._pendingAsyncResults, b.key)
148
+ && !blockKeyIn(getAsyncPool(agent, "subagent"), b.key) && !blockKeyIn(getAsyncPool(agent, "advisor"), b.key)
149
+ }
150
+ return out
151
+ })
152
+ return JSON.stringify({ panel })
153
+ }