thincoder 0.12.58 → 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 (158) hide show
  1. package/CHANGELOG.md +78 -2
  2. package/README.md +3 -3
  3. package/bin/thincoder.mjs +88 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +135 -26
  6. package/src/advisor/messages.mjs +57 -4
  7. package/src/advisor/run.mjs +119 -79
  8. package/src/advisor.mjs +34 -7
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +182 -22
  11. package/src/agent/helpers.mjs +71 -4
  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 +107 -20
  16. package/src/agent/spawn-child.mjs +54 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +133 -109
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +154 -104
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +26 -30
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/goal.mjs +11 -1
  25. package/src/agent-tools/read-history.mjs +284 -0
  26. package/src/agent-tools/recent-changes.mjs +2 -1
  27. package/src/agent-tools/settings.mjs +152 -0
  28. package/src/agent-tools/skill.mjs +2 -1
  29. package/src/agent-tools/subagent-actions.mjs +470 -0
  30. package/src/agent-tools/subagent-async.mjs +382 -0
  31. package/src/agent-tools/subagent-panel.mjs +153 -0
  32. package/src/agent-tools/subagent-run.mjs +202 -0
  33. package/src/agent-tools/subagent-scheduler.mjs +343 -0
  34. package/src/agent-tools/subagent-spawn.mjs +406 -0
  35. package/src/agent-tools/subagent.mjs +203 -377
  36. package/src/agent-tools/task.mjs +4 -3
  37. package/src/agent-tools/timer.mjs +9 -4
  38. package/src/agent-tools/verify.mjs +198 -238
  39. package/src/agent-tools.mjs +1 -0
  40. package/src/agent.mjs +145 -242
  41. package/src/auto-think.mjs +14 -0
  42. package/src/cli/distill-command.mjs +10 -4
  43. package/src/cli/make-agent.mjs +4 -1
  44. package/src/cli/memory-command.mjs +2 -1
  45. package/src/cli/permission.mjs +8 -1
  46. package/src/cli/setup-wizard.mjs +17 -12
  47. package/src/config.mjs +61 -8
  48. package/src/context.mjs +81 -163
  49. package/src/crash-reports.mjs +123 -0
  50. package/src/distill.mjs +30 -12
  51. package/src/escape.mjs +6 -4
  52. package/src/explore-distill.mjs +155 -0
  53. package/src/log.mjs +195 -0
  54. package/src/memory/code-sync.mjs +2 -1
  55. package/src/memory/core.mjs +11 -72
  56. package/src/memory/delete.mjs +234 -0
  57. package/src/memory/docs.mjs +206 -87
  58. package/src/memory.mjs +3 -1
  59. package/src/model-specs.mjs +15 -1
  60. package/src/peer-domains.mjs +265 -0
  61. package/src/peer-instances.mjs +231 -0
  62. package/src/prompt-overlays.mjs +25 -0
  63. package/src/prompts/advisor-design.md +18 -39
  64. package/src/prompts/advisor-round1.md +20 -32
  65. package/src/prompts/advisor-round2.md +16 -16
  66. package/src/prompts/advisor-round3.md +16 -16
  67. package/src/prompts/coder.md +7 -28
  68. package/src/prompts/consult-base.md +4 -11
  69. package/src/prompts/discipline.md +31 -44
  70. package/src/prompts/eng-coder.md +9 -34
  71. package/src/prompts/engineering-sub.md +10 -8
  72. package/src/prompts/engineering.md +61 -264
  73. package/src/prompts/explore.md +4 -14
  74. package/src/prompts/main.md +18 -35
  75. package/src/prompts/methodology-template.md +32 -38
  76. package/src/prompts/plan.md +2 -9
  77. package/src/prompts/system.md +18 -35
  78. package/src/provider/core.mjs +62 -69
  79. package/src/provider/errors.mjs +76 -0
  80. package/src/provider/retry.mjs +8 -45
  81. package/src/session-gc.mjs +214 -0
  82. package/src/session-guard.mjs +47 -0
  83. package/src/session-rename.mjs +38 -0
  84. package/src/session-slots.mjs +181 -58
  85. package/src/session.mjs +48 -89
  86. package/src/token-ttl.mjs +273 -0
  87. package/src/tools/apply_patch.md +3 -1
  88. package/src/tools/bash.md +1 -1
  89. package/src/tools/checklist-sync.mjs +181 -0
  90. package/src/tools/checklist.mjs +52 -39
  91. package/src/tools/delete.md +1 -0
  92. package/src/tools/edit-batch.mjs +131 -44
  93. package/src/tools/edit-diff.mjs +348 -0
  94. package/src/tools/edit.md +20 -13
  95. package/src/tools/execute.md +7 -7
  96. package/src/tools/execute.mjs +55 -24
  97. package/src/tools/file.mjs +25 -70
  98. package/src/tools/file_ops.md +2 -1
  99. package/src/tools/get_current_time.md +3 -1
  100. package/src/tools/git.mjs +14 -6
  101. package/src/tools/glob-dialect.mjs +130 -0
  102. package/src/tools/glob.md +3 -3
  103. package/src/tools/grep.md +1 -1
  104. package/src/tools/hashline_edit.md +2 -0
  105. package/src/tools/index.mjs +3 -3
  106. package/src/tools/insert_after.md +2 -1
  107. package/src/tools/lint.md +2 -0
  108. package/src/tools/lsp.md +4 -1
  109. package/src/tools/ops.mjs +175 -3
  110. package/src/tools/patch.mjs +84 -13
  111. package/src/tools/question.md +5 -1
  112. package/src/tools/repomap.mjs +1 -1
  113. package/src/tools/shared.mjs +18 -25
  114. package/src/tools/system.mjs +50 -30
  115. package/src/tools/tree.md +2 -1
  116. package/src/tools/wait_for.md +22 -0
  117. package/src/tools/web.mjs +5 -3
  118. package/src/tools/websearch.md +2 -1
  119. package/src/tools/write.md +2 -0
  120. package/src/traces/trace-store.mjs +224 -0
  121. package/src/tui/agent-turn.mjs +179 -27
  122. package/src/tui/clipboard.mjs +15 -4
  123. package/src/tui/cmd-config.mjs +77 -16
  124. package/src/tui/cmd-eng.mjs +20 -16
  125. package/src/tui/cmd-extract.mjs +1 -1
  126. package/src/tui/cmd-mcp.mjs +17 -2
  127. package/src/tui/cmd-new.mjs +3 -2
  128. package/src/tui/cmd-session.mjs +19 -4
  129. package/src/tui/cmd-think.mjs +11 -11
  130. package/src/tui/cmd-upgrade.mjs +19 -4
  131. package/src/tui/config-helpers.mjs +28 -16
  132. package/src/tui/distill-cmd.mjs +1 -1
  133. package/src/tui/index.mjs +31 -96
  134. package/src/tui/interaction.mjs +13 -2
  135. package/src/tui/key-handler.mjs +105 -155
  136. package/src/tui/key-modes.mjs +215 -0
  137. package/src/tui/layout.mjs +22 -1
  138. package/src/tui/mouse.mjs +46 -0
  139. package/src/tui/pickers.mjs +51 -25
  140. package/src/tui/render-conversation.mjs +13 -161
  141. package/src/tui/render-frame.mjs +27 -10
  142. package/src/tui/render-loop.mjs +4 -1
  143. package/src/tui/render-segments.mjs +182 -0
  144. package/src/tui/startup.mjs +40 -0
  145. package/src/tui/subagent-blocks.mjs +272 -262
  146. package/src/tui/subagent-children.mjs +176 -0
  147. package/src/tui/subagent-freeze.mjs +172 -0
  148. package/src/tui/subagent-panel.mjs +125 -12
  149. package/src/tui/suspension-drive.mjs +351 -0
  150. package/src/tui/tool-args.mjs +10 -2
  151. package/src/tui/tool-display.mjs +142 -0
  152. package/src/tui/tool-events.mjs +127 -231
  153. package/src/tui/tui-lifecycle.mjs +29 -0
  154. package/src/tui/update-notice.mjs +76 -0
  155. package/src/tui/wizard.mjs +48 -12
  156. package/src/agent-tools/escalate.mjs +0 -179
  157. package/src/agent-tools/subagent-check.mjs +0 -107
  158. package/src/tools/exec-prelude.mjs +0 -84
@@ -0,0 +1,382 @@
1
+ /**
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。
10
+ * 拆分(2026-09-05——Module Split Policy §20.9——纯迁移零行为变化):§20 调度器 + 文件域
11
+ * 组 → ./subagent-scheduler.mjs(尾部 re-export 保测试动态 import 面——queueRunnable/
12
+ * describeBlockers);status/panel/escalate 动作执行器 → ./subagent-actions.mjs。
13
+ */
14
+ import {
15
+ runAgent, escapeXml,
16
+ MIN_REPORT_CHARS, REPORT_CONTINUATION, DEFAULT_SUBAGENT_TURNS,
17
+ } from "../agent.mjs"
18
+ import { runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
19
+ import { pushReal } from "../context.mjs"
20
+ import { offloadToolResult } from "../agent/helpers.mjs"
21
+ import {
22
+ dependentLabels, maybeRefillAsync, refreshQueuedTokens,
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"
28
+
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
+ }
108
+
109
+ /**
110
+ * Resolve the sub-agent's provider from a model override string (shared with the
111
+ * VS Code port). Forms accepted:
112
+ * "provider:model" → the named provider with the named model
113
+ * "provider" → the named provider's configured model
114
+ * "model" → same provider as the parent, different model
115
+ * "default" → same as null: the parent's provider unchanged (explicit
116
+ * "use the default model" — 2026-09-05 user ruling; matched
117
+ * case-insensitively — parser-layer alias guard)
118
+ * null → parent's provider unchanged.
119
+ * API keys come from config.json only (env vars are not a key source).
120
+ */
121
+ export function resolveChildProvider(parent, modelArg) {
122
+ if (!modelArg) return { ...parent.provider }
123
+ // "default" alias (2026-09-05 user ruling — ARCHITECTURE.md 子 agent 模型指定):
124
+ // the literal "default", matched case-insensitively, declares "no override →
125
+ // inherit the default model" — equivalent to null/omission (parent provider
126
+ // unchanged). Checked BEFORE the provider-name lookup so the alias is reserved
127
+ // (≡ omission — providers are not consulted) and never falls through to the
128
+ // model-name swap branch. Any other single-segment value keeps the legacy
129
+ // semantics below.
130
+ if (String(modelArg).toLowerCase() === "default") return { ...parent.provider }
131
+ const providers = parent.config?.providersList ?? []
132
+ const withKey = (p) => (p.apiKey?.trim() ? { ...p, apiKey: p.apiKey.trim() } : { ...p })
133
+ if (modelArg.includes(":")) {
134
+ const [pname, mname] = modelArg.split(":")
135
+ const p = providers.find((x) => x.name === pname)
136
+ if (!p) throw new Error(`subagent model: unknown provider "${pname}" (available: ${providers.map((x) => x.name).join(", ") || "none"})`)
137
+ return { ...withKey(p), model: mname || p.model }
138
+ }
139
+ const byName = providers.find((x) => x.name === modelArg)
140
+ if (byName) return withKey(byName)
141
+ return { ...parent.provider, model: modelArg }
142
+ }
143
+
144
+ /**
145
+ * §19.5 D-M6 cancel 核心(工具路径与 TUI ⏹ 共用):定向中止单个后台子代理,id 必填。
146
+ * - queued 目标(未启动无 controller):出队 + 后续 position 前移 + settle waiter
147
+ * → { id, status:"cancelled", was:"queued" }——无 abort(T-M27)
148
+ * - running 目标:置 entry.cancelled + 条目 controller abort(child runAgent signal)
149
+ * → settle finally 跑 cancelled 分支(⟦ev⟧stopped 冻结 + 模型提醒;不入 pending/
150
+ * 不直注入)→ { id, status:"cancelled" }(T-M19)
151
+ * - 未知/已完成 id → error(T-M20);重复 cancel 幂等(abort 已在途)
152
+ */
153
+ export function cancelAsyncSubagent(agent, id) {
154
+ const key = String(id)
155
+ const map = agent._asyncSubagents ?? new Map()
156
+ const entry = map.get(key)
157
+ if (!entry) {
158
+ return { id: key, status: "error", error: `unknown async subagent id: ${key}` }
159
+ }
160
+ if (entry.done) {
161
+ return { id: key, status: "error", error: `async subagent #${key} has already finished — nothing to cancel` }
162
+ }
163
+ if (entry.cancelled) return { id: key, status: "cancelled" } // abort already in flight — idempotent
164
+ if (entry.status === "queued") {
165
+ // 出队 + position 释放/前移(§19.5 D-M6——无 abort)
166
+ const queue = agent._asyncQueue ?? []
167
+ const qi = queue.indexOf(entry)
168
+ if (qi >= 0) {
169
+ queue.splice(qi, 1)
170
+ for (let i = 0; i < queue.length; i++) queue[i].position = i + 1
171
+ }
172
+ entry.cancelled = true
173
+ entry.done = true
174
+ entry.status = "done"
175
+ map.delete(key)
176
+ // §20 D-SD5 终态墓碑:queued 取消(无 settle 事件——出队即终态)——依赖者经
177
+ // 墓碑查得 cancelled 分支(round1 #4——cancel 返回时即重估标注)。
178
+ const tombstones = (agent._asyncTombstones ??= new Map())
179
+ tombstones.set(key, { status: "cancelled", role: entry.role })
180
+ entry._settle?.()
181
+ return { id: key, status: "cancelled", was: "queued" }
182
+ }
183
+ // running:标记 + 条目 abort——settle finally 跑 cancelled 分支(移除 + stopped + 提醒)
184
+ entry.cancelled = true
185
+ entry.controller?.abort?.()
186
+ return { id: key, status: "cancelled" }
187
+ }
188
+
189
+ /** subagent action:"cancel" (§19.5 D-M6): depth-0 main-session control only.
190
+ * §20 D-SD5/round1 #4(queued 依赖取消——无 settle 事件):出队后**返回即**重估
191
+ * 依赖者——依赖者留 queued 标 dependency cancelled(refreshQueuedTokens 发更新块头
192
+ * token)+ 工具结果内注依赖者(模型可见——工具结果内——round1 #4 明示通道)+ 补位
193
+ * (AUTO 档依赖者自动启动/槽位竞态释放)。被取消条目自身发 ⟦ev⟧cancelled 移除等待块。 */
194
+ export function executeCancelAction(args, ctx) {
195
+ if ((ctx.depth ?? 0) > 0) {
196
+ return JSON.stringify({ status: "error", error: "cancel is only available at depth 0 — a child agent has no async pool of its own (AGENT-LOOP.md §19.5 D-M6)" })
197
+ }
198
+ const id = args?.id
199
+ if (id === undefined || id === null || String(id) === "") {
200
+ return JSON.stringify({ status: "error", error: "cancel requires the id of the async subagent to stop — omitting it would mean a blanket cancel (Ctrl+C stops everything; AGENT-LOOP.md §19.5 D-M6)" })
201
+ }
202
+ const key = String(id)
203
+ const agent = ctx.agent
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
+ }
211
+ const wasQueued = entry?.status === "queued"
212
+ // 依赖者快照(出队前——用于 AUTO 分支判定"是否有依赖者被本次取消波及";note 组装在
213
+ // refill 后按**仍 queued** 的实况重算——防 AUTO 已自动启动后文案称 "stay queued")
214
+ const hadDependents = entry ? dependentLabels(agent, key).length > 0 : false
215
+ const result = cancelAsyncSubagent(agent, key)
216
+ if (wasQueued && result.status === "cancelled" && result.was === "queued" && entry) {
217
+ // §20 D-SD3b:取消/出队 → 移除等待块(不冻结——TUI routeSubToken cancelled 分支)
218
+ ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧cancelled\x1e`)
219
+ // 依赖者标注 + 位置前移 + AUTO 自动启动(round2 #3:AUTO 档才启动——手动留 queued)
220
+ maybeRefillAsync(agent)
221
+ refreshQueuedTokens(agent, ctx.callbacks?.onToken)
222
+ if (hadDependents) {
223
+ // refill 后重算——AUTO 下依赖者已启动者不再列(文案与实际状态一致——code review 🔵)
224
+ const dependents = dependentLabels(agent, key)
225
+ if (dependents.length > 0) {
226
+ result.dependents = dependents
227
+ result.note = `queued dependents ${dependents.join(", ")} marked "dependency cancelled" — they stay queued until you cancel them (this action again with their id) or an AUTO session starts them (AGENT-LOOP.md §20 D-SD5)`
228
+ } else if (agent.autoApprove) {
229
+ result.note = `dependents of the cancelled task auto-started (AUTO session — round2 #3: an AUTO session starts dependency-cancelled dependents on slot availability)`
230
+ }
231
+ }
232
+ }
233
+ return JSON.stringify(result)
234
+ }
235
+
236
+ /**
237
+ * 共享 post-spawn 管线(阻塞与 async 同一条——§15 D-A1 "全不变"):turn-cap continue
238
+ * 循环 → 拒绝降级 partial 返回 → MIN_REPORT_CHARS 扩写 → eng-coder mutation merge →
239
+ * designId 后缀。onDeclined 在此(两路同一形态)——只有 askContinue 不同:阻塞经权限
240
+ * 面板询问;async 永不弹面板(自动拒绝;engineering && AUTO 自动 resume——§15 D-A3)。
241
+ */
242
+ export async function runChildPipeline(child, input, childOpts, childRunOpts, { parent, role, args, askContinue }) {
243
+ const declined = { partial: null }
244
+ let report = await runWithContinue(
245
+ (child, input, cbs, opts) => runAgent(child, input, cbs, opts), // opts = childRunOpts + resume (由管线管理)
246
+ child, input, childOpts, childRunOpts,
247
+ {
248
+ askContinue,
249
+ onDeclined: (e, output) => {
250
+ if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
251
+ // 拒绝 = 早退语义:不带 MIN_REPORT_CHARS 扩写(对已 cap 的 child 追问长报告是错的);
252
+ // review #2:用管线捕获的 output(此时 report 仍 ""——runWithContinue 未返回)
253
+ declined.partial = `Subagent (${role}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output || ""}`
254
+ },
255
+ },
256
+ )
257
+ if (declined.partial !== null) {
258
+ // 被拒的 eng-coder 交付仍带 designId(fix round 以同 slot 重 spawn——2026-09-01)
259
+ if (role === "eng-coder") declined.partial += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
260
+ return declined.partial
261
+ }
262
+
263
+ // 报告过短 = 交接不完整:送回扩写一次(kimi-code summaryPolicy: min 200 chars 同源)
264
+ if (report.length < MIN_REPORT_CHARS) {
265
+ report = await runAgent(child, REPORT_CONTINUATION, childOpts, childRunOpts)
266
+ }
267
+
268
+ // 工程模式机械码门:委托的文件改动不得绕过父侧 advisor/verify guard——eng-coder ONLY
269
+ // 有意为之(review #8):普通 coder 自带自评纪律;normal 模式无父侧 gate 可喂。
270
+ // 只在实际 mutated 时 merge(防 runAgent 写前抛错时传播空 mutation 声明——纵深防御)
271
+ if (role === "eng-coder" && child._mutatedThisRun) {
272
+ mergeChildMutations(parent, child)
273
+ }
274
+
275
+ // designId 随交付报告(2026-09-01):divergence audit fix round 用同一 designId+token 重 spawn
276
+ if (role === "eng-coder") {
277
+ report += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse this designId with the same designToken (from the approved advisor type='design' review) when re-spawning this eng-coder for an audit fix round.`
278
+ }
279
+
280
+ return report
281
+ }
282
+
283
+ /**
284
+ * Inject one settled async entry into the parent history as a user-role reminder
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).
290
+ */
291
+ export async function injectAsyncResult(agent, entry) {
292
+ const body = entry.error ?? entry.report ?? "(no report)"
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
+ }
311
+ pushReal(agent, {
312
+ role: "user",
313
+ content: label,
314
+ })
315
+ // §20 D-SD5 终态墓碑:本函数是全部自动注入路径的共享形态(回合尾 collect + 挂起
316
+ // digest 首行注入)——注入即消费(调用方随即从容器移除)——dependsOn 引用该 id 的
317
+ // 后续 spawn 视为已满足(T-SD14 消费终态语义——§19.8 后 check 消费路径删除,本自动
318
+ // 通道为唯一消费方;error 条目记 failed——依赖取消/失败分支照旧,不误标成功)。
319
+ const tombstones = (agent._asyncTombstones ??= new Map())
320
+ tombstones.set(String(entry.id), { status: entry.error != null ? "failed" : "consumed", role: entry.role })
321
+ }
322
+
323
+ /**
324
+ * Child agent run options — the parent's abort signal MUST propagate to the
325
+ * child: without it, Ctrl+C aborts the parent's controller but the child keeps
326
+ * running its full turn budget (up to subagentTurns) while the parent awaits —
327
+ * the interrupt appears to do nothing.
328
+ * §17 D-S9: during a suspension session children share the SESSION signal instead
329
+ * (agent._sessionSignal) — a digest's own Ctrl+I/Ctrl+C must not abort the whole
330
+ * pool; the session driver aborts the session controller to stop everything.
331
+ */
332
+ export function buildChildRunOpts(ctx) {
333
+ return {
334
+ depth: (ctx.depth ?? 0) + 1,
335
+ maxTurns: ctx.agent?.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS,
336
+ signal: ctx.agent?._sessionSignal ?? ctx.signal ?? null,
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Merge an eng-coder child's mutations into the parent agent's bookkeeping.
342
+ * The parent must stay aware of delegated file changes: `_touchedFiles` enables
343
+ * the advisor guard (completion.mjs) to detect that code was modified and
344
+ * pushback for review. Prior verify/advisor state is invalidated because it
345
+ * judged an older state.
346
+ *
347
+ * `_advisorRound` is NOT reset: merged code enters the CURRENT convergence
348
+ * cycle. Resetting here would break the review→fix→re-review loop (the parent
349
+ * reviews, spawns an eng-coder to fix, merges, reviews again — every merge
350
+ * would restart at round 1 and the 5-round cap could never be reached).
351
+ * `_calledAdvisorThisRun` IS cleared so the merged code triggers a fresh
352
+ * advisor call (the guard demands review of new mutations).
353
+ *
354
+ * Returns true when mutations were merged (kept for future caller checks).
355
+ */
356
+ export function mergeChildMutations(parent, child) {
357
+ // A child claiming mutations without any touched file is a misbehaving
358
+ // child (or a bookkeeping bug) — do not propagate an empty mutation claim
359
+ // to the parent's guard state.
360
+ if (!child._mutatedThisRun || !(child._touchedFiles?.length)) return false
361
+ parent._mutatedThisRun = true
362
+ const merged = []
363
+ for (const abs of child._touchedFiles ?? []) {
364
+ if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
365
+ merged.push(abs)
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)
370
+ if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
371
+ if (parent._verifiedThisRun) {
372
+ parent._verifiedThisRun = false
373
+ parent._verifyPassed = undefined
374
+ }
375
+ // Stale session cleanup only — the round counter survives (see above).
376
+ parent._advisorSession = null
377
+ return true
378
+ }
379
+
380
+ // Re-export shim (2026-09-05 拆分轮): §20 调度符号迁至 ./subagent-scheduler.mjs——本面
381
+ // 保留供 test/subagent-scheduler.test.mjs 动态 import(测试零改动——freeze.mjs 同款转发链)。
382
+ export { queueRunnable, describeBlockers } from "./subagent-scheduler.mjs"
@@ -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
+ }