@thincoder/core 0.9.5 → 0.10.1

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 (215) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-specs.mjs +16 -7
  119. package/notify-policy.mjs +51 -0
  120. package/package.json +1 -1
  121. package/peer-claims.mjs +2 -2
  122. package/peer-domains.mjs +7 -11
  123. package/peer-instances.mjs +9 -10
  124. package/process-probe-exec.mjs +187 -0
  125. package/process-probe.mjs +27 -175
  126. package/prompt-overlays.mjs +11 -10
  127. package/prompts/advisor-design.md +2 -2
  128. package/prompts/advisor-round2.md +1 -1
  129. package/prompts/advisor-round3.md +1 -1
  130. package/prompts/common.md +16 -8
  131. package/prompts/discipline-engineering.md +57 -16
  132. package/prompts/discipline-normal.md +6 -3
  133. package/prompts/persona-coder.md +0 -1
  134. package/prompts/persona-eng-coder.md +1 -5
  135. package/prompts/persona-eng-designer.md +2 -3
  136. package/prompts/persona-engineering.md +32 -11
  137. package/prompts/persona-explore.md +0 -1
  138. package/prompts/persona-normal.md +1 -1
  139. package/prompts/persona-plan.md +3 -4
  140. package/prompts/subagent-base.md +14 -0
  141. package/provider/core.mjs +11 -48
  142. package/provider/list-models.mjs +3 -0
  143. package/provider/responses-request.mjs +237 -0
  144. package/provider/responses.mjs +6 -228
  145. package/provider-flows.mjs +248 -0
  146. package/queued.mjs +90 -0
  147. package/rules.mjs +209 -2
  148. package/session-guard.mjs +27 -7
  149. package/session-lifecycle.mjs +90 -18
  150. package/session-rename.mjs +5 -1
  151. package/session-slot-claims.mjs +230 -0
  152. package/session-slot-scan.mjs +299 -0
  153. package/session-slot-verify.mjs +304 -0
  154. package/session-slot-write.mjs +74 -8
  155. package/session-slots-manifest.mjs +163 -241
  156. package/session-slots.mjs +99 -71
  157. package/session.mjs +30 -10
  158. package/shell-candidates.mjs +78 -0
  159. package/text-budget.mjs +2 -2
  160. package/think-off.mjs +49 -0
  161. package/token-ttl.mjs +15 -4
  162. package/tool-args.mjs +73 -0
  163. package/tool-docs/advisor.md +9 -0
  164. package/tool-docs/batch.md +1 -0
  165. package/tool-docs/code_search.md +1 -0
  166. package/tool-docs/consult_start.md +5 -0
  167. package/tool-docs/consult_stop.md +3 -0
  168. package/tool-docs/context.md +7 -0
  169. package/tool-docs/doc_search.md +1 -0
  170. package/tool-docs/edit.md +2 -13
  171. package/tool-docs/eng.md +1 -0
  172. package/tool-docs/execute.md +1 -9
  173. package/tool-docs/git.md +25 -51
  174. package/tool-docs/goal.md +6 -0
  175. package/tool-docs/ledger_add.md +1 -0
  176. package/tool-docs/ledger_close.md +1 -0
  177. package/tool-docs/ledger_count.md +1 -0
  178. package/tool-docs/ledger_query.md +1 -0
  179. package/tool-docs/ledger_update.md +1 -0
  180. package/tool-docs/main_history.md +1 -0
  181. package/tool-docs/memory.md +10 -0
  182. package/tool-docs/notify_parent.md +9 -0
  183. package/tool-docs/peer_instances.md +1 -0
  184. package/tool-docs/plan.md +5 -0
  185. package/tool-docs/process.md +7 -3
  186. package/tool-docs/read_history.md +7 -0
  187. package/tool-docs/recent_changes.md +1 -0
  188. package/tool-docs/repo_outline.md +1 -0
  189. package/tool-docs/settings.md +6 -0
  190. package/tool-docs/skill.md +5 -0
  191. package/tool-docs/subagent.md +31 -0
  192. package/tool-docs/task.md +7 -0
  193. package/tool-docs/timer.md +6 -0
  194. package/tool-docs/verify.md +1 -0
  195. package/tool-docs/wait_for.md +2 -1
  196. package/tools/bash.mjs +28 -4
  197. package/tools/edit-batch.mjs +4 -4
  198. package/tools/edit-diff.mjs +4 -23
  199. package/tools/exec-run.mjs +102 -4
  200. package/tools/execute.mjs +5 -2
  201. package/tools/file.mjs +4 -9
  202. package/tools/git-run.mjs +1 -1
  203. package/tools/git.mjs +0 -24
  204. package/tools/index.mjs +2 -1
  205. package/tools/linter.mjs +2 -1
  206. package/tools/node-child.mjs +21 -0
  207. package/tools/ops.mjs +50 -13
  208. package/tools/patch.mjs +4 -2
  209. package/tools/repomap.mjs +20 -9
  210. package/tools/shared.mjs +8 -6
  211. package/tools/web.mjs +5 -2
  212. package/tools/write-path.mjs +1 -1
  213. package/traces/trace-store.mjs +17 -14
  214. package/vision-reader.mjs +76 -0
  215. package/agent-tools/batch-segment.mjs +0 -18
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * subagent-actions.mjs — subagent 动作执行器(AGENT-LOOP-SUBAGENT.md §6.7——2026-09-05 自
3
- * subagent-async.mjs 拆分——Module Split Policy §20.9——纯迁移零行为变化)。
4
- * 内容:executeStatusAction(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M5——非阻塞池查询——touchedSummary/
5
- * shortTouchedPath/statusFields 摘要助手随行)/ executeObserveAction + executeSendAction
6
- * (SUBAGENT-OBSERVE-SEND——observe 只读/send 控制豁免注入)/ executeEscalateAction(AGENT-LOOP-SUBAGENT.md §6.7
7
- * D-M4——飞刀——touchedFilesNote 随行)。
3
+ * subagent-async.mjs 拆分——Module Split Policy——纯迁移零行为变化)。
4
+ * 内容:executeSendAction(SUBAGENT-OBSERVE-SEND——send 控制豁免注入)/ executeEscalateAction
5
+ * (AGENT-LOOP-SUBAGENT.md §6.7 D-M4——飞刀——touchedFilesNote 随行);status ∕ observe 两执行器与
6
+ * 摘要助手族(touchedSummary/shortTouchedPath/statusFields)随 2026-09-28 三次拆分迁出(见下)。
8
7
  * AGENT-LOOP-SUBAGENT.md §6.7.5 check 删除后仅 cancel 动作执行器与共享 post-spawn 管线留在 subagent-async.mjs;
9
- * §20 调度器 + 文件域组在 ./subagent-scheduler.mjs(describeBlockers 由此导入)。
8
+ * AGENT-LOOP-SUBAGENT.md §6.9 调度器 + 文件域组在 ./subagent-scheduler.mjs(describeBlockers 由此导入)。
10
9
  * 2026-09-08 二次拆分:AGENT-LOOP-SUBAGENT.md §6.7.2 面板段迁至 ./subagent-panel.mjs(601 > 500 硬限跨档——
11
10
  * 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面)+ ASYNC-RESULT-
12
11
  * CONTAINER.md D1 落地(池访问点改 getAsyncPool accessor)。
12
+ * 2026-09-28 三次拆分(Module Split Policy——498/499 贴 500 硬限):**查询面**(status ∕ observe
13
+ * 两执行器 + 摘要助手族)迁至 ./subagent-actions-query.mjs(verbatim);本档保留 send ∕ escalate
14
+ * 执行器与 panel re-export,并 re-export 上述两执行器保 subagent.mjs 既有 import 面。
13
15
  */
14
16
  import { isAbsolute, relative } from "node:path"
15
17
  import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS, excludeSubagentTools } from "../agent.mjs"
@@ -18,268 +20,20 @@ import {
18
20
  ensureChildApiKey, clampEffort,
19
21
  } from "../agent/spawn-child.mjs"
20
22
  import { logEvent, errText } from "../log.mjs"
21
- import { describeBlockers, detectStall, STALL_NOTE } from "./subagent-scheduler.mjs"
22
23
  import { resolveChildProvider, mergeChildMutations } from "./subagent-async.mjs"
23
24
  import { launchEscalateAsync } from "./escalate-async.mjs"
24
- // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口常量(单源——store 零依赖)。
25
+ // TUI-OOM-ROOTCAUSE·AGENT-LOOP.md §6.15:子代理人读线窗口常量(单源——store 零依赖)。
25
26
  import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
26
27
  // ASYNC-RESULT-CONTAINER.md D1:池 accessor(absorb 双池——advisor 独立池无队列)
27
28
  import { getAsyncPool } from "./async-settle.mjs"
28
-
29
- /**
30
- * subagent action:"status" (AGENT-LOOP-SUBAGENT.md §6.7 D-M2, new): NON-BLOCKING async-pool query —
31
- * returns immediately and never consumes a result (results belong to the auto
32
- * channel — AGENT-LOOP-SUBAGENT.md §6.7.5: no check action any more). Source of truth = the pool
33
- * (_asyncSubagents): entries moved to _pendingAsyncResults during a suspension
34
- * (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S3 ② — injected at the next run start) are no longer in the pool and
35
- * are NOT counted as done-waiting.
36
- * - id given → { id, role, status, model?, elapsedSec?, turn?, maxTurns?,
37
- * touchedFiles?/touchedMore?/touched? ... } for that entry; unknown id → error
38
- * (T12 semantics — unknown async-subagent id error, same wording as the pool).
39
- * - id omitted → { overview: { running: [{id, role, model, elapsedSec, turn,
40
- * maxTurns, touchedFiles?/touched?}], queued: [{id, role, position, touched?}],
41
- * done: [{id, role}] } } — live queue positions (index in _asyncQueue + 1).
42
- * AGENT-LOOP-SUBAGENT.md §6.7.2: running 条目带 touched files 摘要(touchedFiles 前 5 + touchedMore 超出
43
- * 计数——相对查询方 cwd;0 改动 → touched 占位);queued 条目带 touched 占位
44
- * "—(未启动)";done/error/取消条目无 touched 字段(round3 #9)。
45
- * A settled-but-unconsumed entry (settled during a NORMAL turn) reports done
46
- * with a "not yet consumed" note — the auto channel (turn-end collection / the
47
- * suspension digest) still delivers it afterwards (AGENT-LOOP-SUBAGENT.md §6.7.5: sole consumption path).
48
- */
49
- /** AGENT-LOOP-SUBAGENT.md §6.7.2 D-M5 decision-field assembly (F9): running entries report
50
- * {id, role, model, elapsedSec, turn, maxTurns} — the data needed to decide
51
- * WHO to cancel. Model is recorded at spawn (childProvider), startedAt at
52
- * ACTUAL start (queued waits don't count), turn/maxTurns mirrored from the
53
- * child's ⟦ev⟧turn events at the callbacks-wrap layer (subagent.mjs tracker).
54
- * elapsedSec computed at call time from startedAt. */
55
- /** AGENT-LOOP-SUBAGENT.md §6.7.2 D-SF2/N-SF1 摘要形态:status 调用时实时读 entry.childAgent._touchedFiles
56
- * (绝对路径——per-run 记账——§18.12)→ 相对查询方 cwd;cwd 之外保留绝对形态 +
57
- * "../" 前缀;>80 字符截尾(不超行);前 5 个 + 独立截断字段 touchedMore(超出
58
- * 计数——不混入数组——消费方按类型区分)。占位:running 0 改动 → "—(尚无改动)"
59
- * (T-SF2a);queued 未启动 → "—(未启动)"(T-SF2b);done/error/取消条目不含
60
- * 本摘要(round3 #9 明示——本批只做 running/queued)。 */
61
- function touchedSummary(entry, cwd) {
62
- if (entry.status !== "running") return { touched: "—(未启动)" }
63
- const files = entry.childAgent?._touchedFiles ?? []
64
- if (files.length === 0) return { touched: "—(尚无改动)" }
65
- const shown = files.slice(0, 5).map((f) => shortTouchedPath(f, cwd))
66
- const out = { touchedFiles: shown }
67
- if (files.length > 5) out.touchedMore = files.length - 5
68
- return out
69
- }
70
-
71
- /** N-SF1 单路径显示形态:cwd 内 → 相对路径;cwd 外 → "../" + 绝对路径;>80 截尾。 */
72
- function shortTouchedPath(f, cwd) {
73
- let p = f
74
- if (cwd) {
75
- const rel = relative(cwd, f)
76
- p = rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : `../${f}`
77
- }
78
- return p.length > 80 ? `${p.slice(0, 79)}…` : p
79
- }
80
-
81
- function statusFields(entry, cwd) {
82
- // §18.3 #1(第 10 批——双池并表):评审池条目专属字段面——role 区分两池;子代理条目
83
- // 字段面零变化(NFR-B2)。reviewType(design|code) / round / elapsedSec——与子代理
84
- // 的 turn/maxTurns 而非(评审无子回合预算语义)。
85
- if (entry.role === "advisor") {
86
- const base = { id: String(entry.id), role: entry.role, reviewType: entry.reviewType ?? null, round: entry.round ?? entry.run?.round ?? null }
87
- if (entry.model != null) base.model = entry.model
88
- if (entry.startedAt) base.elapsedSec = Math.max(0, Math.round((Date.now() - entry.startedAt) / 1000))
89
- return base
90
- }
91
- const base = { id: String(entry.id), role: entry.role }
92
- if (entry.status === "running") {
93
- base.model = entry.model ?? null
94
- base.elapsedSec = entry.startedAt ? Math.max(0, Math.floor((Date.now() - entry.startedAt) / 1000)) : 0
95
- base.turn = entry.turn ?? 0
96
- base.maxTurns = entry.maxTurns ?? 0
97
- // AGENT-LOOP-SUBAGENT.md §6.7.2:touched files 摘要(T-SF1..4——新字段追加——既有字段零破坏)
98
- Object.assign(base, touchedSummary(entry, cwd))
99
- } else if (entry.status === "queued") {
100
- // AGENT-LOOP-SUBAGENT.md §6.7.2 T-SF2b:未启动——确定性占位(不崩;无对象可读)
101
- base.touched = "—(未启动)"
102
- }
103
- return base
104
- }
105
-
106
- export function executeStatusAction(args, ctx) {
107
- // #46(批 TOOLFACE-FIXES ①——设计 AGENT-LOOP-SUBAGENT.md §6.25 ①):七动作族 depth 门收口——
108
- // status 与 observe/send/escalate/cancel/panel freeze 同为 depth-0 专有(子代无自有异步池);
109
- // 缺门时子代静默空 overview(把「不可用」读成「无在飞」)。族锚取现行可解析节号(§6.7.2)。
110
- if ((ctx.depth ?? 0) > 0) {
111
- return JSON.stringify({ status: "error", error: "status is only available at depth 0 — a child agent has no async pool of its own" })
112
- }
113
- const agent = ctx.agent
114
- const map = getAsyncPool(agent, "subagent") ?? new Map()
115
- const advisors = getAsyncPool(agent, "advisor") ?? new Map()
116
- const queue = agent._asyncQueue ?? []
117
- const queuedPosition = (id) => {
118
- const i = queue.findIndex((e) => String(e.id) === id)
119
- return i >= 0 ? i + 1 : undefined
120
- }
121
- const { id } = args ?? {}
122
- if (id !== undefined && id !== null && String(id) !== "") {
123
- const key = String(id)
124
- // §11.2 D-24b: by-id queries fall through to the advisor pool (shared counter —
125
- // ids are unique across both pools; role identifies the kind).
126
- const entry = map.get(key) ?? advisors.get(key)
127
- if (!entry) {
128
- // Wording kept exact — locked by subagent-async/tool tests (unknown-id error
129
- // semantics); an unknown id simply names neither pool (the pools share the
130
- // id counter, so the message stays unambiguous).
131
- return JSON.stringify({ id: key, status: "error", error: `unknown async subagent id: ${key}` })
132
- }
133
- const target = statusFields(entry, agent.cwd)
134
- if (entry.status === "running") return JSON.stringify({ ...target, status: "running" })
135
- if (entry.status === "queued") {
136
- // §20 F-SD4/D-SD3b:waiting 语义对模型可见——排队原因(冲突对象/依赖对象)随
137
- // status 返回;纯槽满等位(kind slot)无 waiting 字段(position 已足够)。
138
- const blk = describeBlockers(agent, entry)
139
- const out = { ...target, status: "queued", position: queuedPosition(key) ?? entry.position }
140
- if (blk.kind !== "slot") {
141
- out.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
142
- out.reason = blk.detail
143
- }
144
- // §21.1 P-SL2(D-SL2):停滞机械标记——池停滞时本任务所在闭环阻塞链随 status
145
- // 返回(F-SL2——status 视图可显示停滞——模型可见——零正常路径字段变化)。
146
- const stall = detectStall(agent)
147
- if (stall) {
148
- const chain = stall.chains.find((c) => c.task === entry)?.text
149
- if (chain) out.stall = { chain, note: STALL_NOTE }
150
- }
151
- return JSON.stringify(out)
152
- }
153
- // done = settled during this turn and not yet consumed — the auto channel still
154
- // delivers it (AGENT-LOOP-SUBAGENT.md §6.7.5: turn-end collection / the suspension digest — no fetch action).
155
- target.status = "done"
156
- target.done = true
157
- if (entry.error) target.error = entry.error
158
- target.note = "settled, not yet consumed — delivered by the auto channel (turn-end collection or the suspension digest injects it)"
159
- return JSON.stringify(target)
160
- }
161
- const overview = { running: [], queued: [], done: [] }
162
- const mapEntries = [...map.values(), ...advisors.values()]
163
- for (const entry of mapEntries) {
164
- if (entry.status === "running") overview.running.push(statusFields(entry, agent.cwd))
165
- else if (entry.status === "queued") {
166
- // §20:queued 条目补 waiting/reason(F-SD4——依赖/冲突原因模型可见);
167
- // AGENT-LOOP-SUBAGENT.md §6.7.2 T-SF2b:未启动占位(确定性——不崩)。
168
- const blk = describeBlockers(agent, entry)
169
- const row = statusFields(entry, agent.cwd)
170
- row.position = queuedPosition(String(entry.id)) ?? entry.position
171
- if (blk.kind !== "slot") {
172
- row.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
173
- row.reason = blk.detail
174
- }
175
- overview.queued.push(row)
176
- }
177
- else if (entry.done) overview.done.push({ id: String(entry.id), role: entry.role })
178
- }
179
- // §21.1 P-SL2(D-SL2):停滞机械标记——overview 级标注(可显示停滞——F-SL2)——
180
- // 链文本随视图返回(逐条——每 queued 条目一行——闭环可破环引导同挂)。
181
- const stall = detectStall(agent)
182
- if (stall) {
183
- overview.stall = {
184
- chains: stall.chains.map((c) => c.text),
185
- note: STALL_NOTE,
186
- }
187
- }
188
- return JSON.stringify({ overview })
189
- }
29
+ import { settleConsultCheckpoint, settleTurnCheckpoint, resumedSendResult } from "./checkpoint.mjs"
190
30
 
191
31
  // ═══════════════════════════════════════════════════════════════════════════
192
- // SUBAGENT-OBSERVE-SEND(SUBAGENT-OBSERVE-SEND.md——CLI 端)
193
- // observe(D1——readonly 查询)+ send(D2——控制类豁免注入引导)——动作执行器。
194
- // 目标 = 父自身 spawn 的异步子代理池条目(_asyncSubagents——非 advisor/escalate——
195
- // 后者共享 id 计数但非"读写子代理",错误路径明示)。
32
+ // 查询面(status ∕ observe + 摘要助手族):2026-09-28 拆分(Module Split Policy——498/499
33
+ // 贴 500 硬限跨档)迁至 ./subagent-actions-query.mjs(verbatim);本面 re-export 两执行器
34
+ // 保 subagent.mjs 既有 import 面(subagent-panel 尾部 re-export 先例)。
196
35
  // ═══════════════════════════════════════════════════════════════════════════
197
-
198
- /** D1/N2 摘要形态:从子代理 _fullHistory(real 消息——pushReal 累积)倒序收最近 N 个
199
- * assistant 回合的一行描述(newest-first;assistant 消息 = 一回合——带 tool_calls =
200
- * 工具轮、独立 content = 回复首行)。返回纯摘要非全量(N2 隔离——不把子代理噪音灌父)。 */
201
- function recentTurnLines(child, limit) {
202
- const hist = child?._fullHistory
203
- if (!Array.isArray(hist)) return []
204
- const out = []
205
- for (let i = hist.length - 1; i >= 0 && out.length < limit; i--) {
206
- const m = hist[i]
207
- if (m?.role !== "assistant") continue
208
- const tcs = Array.isArray(m.tool_calls)
209
- ? m.tool_calls.filter((t) => typeof t?.function?.name === "string")
210
- : []
211
- if (tcs.length) out.push(`tools: ${tcs.map((t) => t.function.name).join(", ")}`)
212
- else if (typeof m.content === "string" && m.content.trim()) {
213
- const first = m.content.trim().split(/\r?\n/).find((l) => l.trim())
214
- out.push((first ?? m.content.trim()).slice(0, 120))
215
- } else out.push("(assistant — no content)")
216
- }
217
- return out
218
- }
219
-
220
- function queuedPositionOf(agent, key) {
221
- const i = (agent?._asyncQueue ?? []).findIndex((e) => String(e.id) === key)
222
- return i >= 0 ? i + 1 : undefined
223
- }
224
-
225
- /** recent 参数钳制(默认 ~5 条摘要——可参数;钳到 1..20 防超长 N2 失控)。 */
226
- function clampRecent(n) {
227
- const v = Number.parseInt(n, 10)
228
- if (!Number.isFinite(v)) return 5
229
- return Math.max(1, Math.min(20, v))
230
- }
231
-
232
- /**
233
- * subagent action:"observe"(D1——READONLY 查询,父回合内):按 id 拉运行中异步子代理的
234
- * 最近活动快照——从 entry.childAgent(subagent-run.mjs start() 绑定)读**已落回合**摘要
235
- * (_fullHistory 最近 N 条)+ _touchedFiles + turn/maxTurns + status + **in-flight 当前
236
- * 工具**(评审 #1——从 dispatch runOne 维护的 child._inflightTools 读——非只读已落
237
- * history:卡在长工具调用时 history 无新回合、恰是 observe 要检测的卡死态)。返摘要非
238
- * 全量(N2)。observe = readonly——running/queued/done 均可查(done 终报 / queued 占位)。
239
- */
240
- export function executeObserveAction(args, ctx) {
241
- if ((ctx.depth ?? 0) > 0) {
242
- return JSON.stringify({ status: "error", error: "observe is only available at depth 0 — a child agent has no async pool of its own" })
243
- }
244
- const agent = ctx.agent
245
- const id = args?.id
246
- if (id === undefined || id === null || String(id) === "") {
247
- 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" })
248
- }
249
- const key = String(id)
250
- const entry = getAsyncPool(agent, "subagent")?.get(key)
251
- if (!entry) {
252
- if (getAsyncPool(agent, "advisor")?.has(key)) {
253
- 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` })
254
- }
255
- return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
256
- }
257
- const child = entry.childAgent
258
- const out = { id: key, role: entry.role, status: entry.status }
259
- if (entry.status === "running") {
260
- out.turn = entry.turn ?? 0
261
- out.maxTurns = entry.maxTurns ?? 0
262
- Object.assign(out, touchedSummary(entry, agent.cwd))
263
- // in-flight 当前工具(评审 #1):child._inflightTools Set——dispatch runOne 在工具
264
- // 执行前后维护——LLM 生成/工具间空隙为空;子代理 await 长工具调用时父回合可见它。
265
- const inflight = child?._inflightTools
266
- if (inflight instanceof Set && inflight.size > 0) out.currentTool = [...inflight]
267
- const n = clampRecent(args?.recent)
268
- out.recentTurns = recentTurnLines(child, n)
269
- 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`
270
- } else if (entry.status === "queued") {
271
- out.position = queuedPositionOf(agent, key) ?? entry.position ?? null
272
- out.note = "queued — not started yet; no activity to observe (starts when the slot frees / dependencies clear; action:'send' targets running only)"
273
- } else {
274
- // done = settled this turn, not yet consumed — final report rides the auto channel.
275
- out.done = true
276
- out.turn = entry.turn ?? 0
277
- out.maxTurns = entry.maxTurns ?? 0
278
- out.recentTurns = child ? recentTurnLines(child, clampRecent(args?.recent)) : []
279
- 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)"
280
- }
281
- return JSON.stringify(out)
282
- }
36
+ export { executeStatusAction, executeObserveAction } from "./subagent-actions-query.mjs"
283
37
 
284
38
  /**
285
39
  * subagent action:"send"(D2——控制类豁免,同 cancel/panel-freeze——父回合内显式调用即
@@ -308,6 +62,7 @@ export function executeSendAction(args, ctx) {
308
62
  if (getAsyncPool(agent, "advisor")?.has(key)) {
309
63
  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 — track it with action:'status' (role:"advisor") or wait for its report to arrive automatically` })
310
64
  }
65
+ if (settleConsultCheckpoint(agent, key, message)) return resumedSendResult(key, "consult") // 检查点兑现 ②(会诊会话)
311
66
  return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
312
67
  }
313
68
  if (entry.done || entry.cancelled) {
@@ -318,6 +73,7 @@ export function executeSendAction(args, ctx) {
318
73
  }
319
74
  entry._injected ??= []
320
75
  entry._injected.push(String(message).trim())
76
+ if (settleTurnCheckpoint(entry)) return resumedSendResult(key, entry.role ?? "subagent") // 检查点兑现 ①(池条目)
321
77
  return JSON.stringify({
322
78
  id: key,
323
79
  status: "delivered",
@@ -337,7 +93,7 @@ export { executePanelAction } from "./subagent-panel.mjs"
337
93
 
338
94
  /**
339
95
  * subagent action:"escalate"(AGENT-LOOP-SUBAGENT.md §6.7 D-M4——退役 escalate 工具语义原样,ESCALATE.md;
340
- * §25 D-R17b——R17:缺省 async——后台飞刀 + settle 三分类 digest——async:false 保
96
+ * ESCALATE.md §5 D-R17b——R17:缺省 async——后台飞刀 + settle 三分类 digest——async:false 保
341
97
  * 同步旧路径)。飞刀——交给 consultModels 池里更强模型(WRITE + 术后报告)。约束全
342
98
  * 保留:depth-0 only / 工程模式拒 / consultModels 空拒 / relay 前缀 `escalate#N/`
343
99
  * (与既有前缀同名——TUI 路由零改动)/ mutations merge 回父(async 路径在 settle 分类)。
@@ -380,7 +136,7 @@ export async function executeEscalateAction(args, ctx) {
380
136
 
381
137
  const tag = label(pick)
382
138
 
383
- // §25 D-R17b (R17 — 决策点 ③): escalate 缺省 async — the launch returns an ack
139
+ // ESCALATE.md §5 D-R17b (R17 — 决策点 ③): escalate 缺省 async — the launch returns an ack
384
140
  // {id, role:"escalate", status:"running"|"queued"} and the flight runs in the
385
141
  // background (shared other pool; settle 三分类 → pending 单容器 digest——
386
142
  // ASYNC-RESULT-CONTAINER.md D2)。
@@ -414,7 +170,7 @@ export async function executeEscalateAction(args, ctx) {
414
170
  memory: parent.memory,
415
171
  role: "coder",
416
172
  })
417
- child._historyWindow = RECORD_WINDOW_MESSAGES // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口(四处创建点同置)
173
+ child._historyWindow = RECORD_WINDOW_MESSAGES // TUI-OOM-ROOTCAUSE·AGENT-LOOP.md §6.15:子代理人读线窗口(四处创建点同置)
418
174
  child._logId = escId // LOGGING:子内事件归属(escalate#N)
419
175
  // SUBAGENT-UPSTREAM-CHANNEL(§6.27.4 W2——escalate **sync**:单向;工具返回注走同步形)。
420
176
  child._upstream = { parent, label: escId, sync: true }
@@ -428,7 +184,7 @@ export async function executeEscalateAction(args, ctx) {
428
184
  // §2.5 #78 并入:escalate 子代理输出流式(VSC subagent-escalate 同款豁免)。
429
185
  streamOutput: true,
430
186
  }
431
- // Continue 经 runWithContinue(§7.2 D3,主会话同等 y/n 面板):resume:true 不重注入
187
+ // Continue 经 runWithContinue(docs/cli/design/TUI.md §6.8 D3,主会话同等 y/n 面板):resume:true 不重注入
432
188
  // task 文本(setup 跳 input)且保留 child history + mutation 记账,刷新 turn 预算;
433
189
  // 无权限 handler(headless)或拒绝 → 部分工作返回;continue 次数无限(每轮可拒)。
434
190
  // 残环批(2026-09-16):飞刀询问名同规包装(`escalate#<id>/<tool>`——端侧键形解析的
@@ -465,7 +221,7 @@ export async function executeEscalateAction(args, ctx) {
465
221
  mergeChildMutations(parent, child)
466
222
  if (escErr) logEvent("child:error", { role: "escalate", id: escId, ms: Date.now() - escT0, err: errText(escErr.err, 200) })
467
223
  else logEvent("child:done", { role: "escalate", id: escId, ms: Date.now() - escT0, kind: String(report).includes(TURN_CAP_MARK) ? "partial" : "ok" })
468
- // §7.2.3(round1 #2):escalate 与 spawn 同享 ctx._subagentKey——同步完成精确冻
224
+ // docs/cli/design/TUI.md §6.8(round1 #2):escalate 与 spawn 同享 ctx._subagentKey——同步完成精确冻
469
225
  // (relayPrefix 去尾 = `escalate#N`)。仅成功路径(escErr = 运行中途失败——不设
470
226
  // key——TUI 回落 escalate 角色启发式:escalate 串行 + 角色限定,天然精确——legacy
471
227
  // 行为不变——错误路径不触发冻结 round1 #1)。
@@ -4,7 +4,7 @@
4
4
  * consume-design + observe/send(SUBAGENT-OBSERVE-SEND)——AGENT-LOOP-SUBAGENT.md §6.7.5 check 已删;spawn 路径与
5
5
  * 工具面在 subagent.mjs;cancel 动作执行器与机械、管线在本模块——check 执行器随 AGENT-LOOP-SUBAGENT.md §6.7.5 删除)。
6
6
  * 内容:resolveChildProvider / async 池常量与域助手(ASYNC_POOL_LIMITS/poolDomainOf/
7
- * resolvePoolLimits/poolLimitsFor/runningPoolCount——§11.1)/ executeCancelAction +
7
+ * resolvePoolLimits/poolLimitsFor/runningPoolCount——AGENT-LOOP-ASYNC-POOL.md §6.10)/ executeCancelAction +
8
8
  * cancelAsyncSubagent(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6——工具与 TUI ⏹ 共用)/ runChildPipeline /
9
9
  * injectAsyncResult / buildChildRunOpts / mergeChildMutations / enqueueAsk(批 6——
10
10
  * _permQueue 审批/继续弹窗串行收 helper——消费位 subagent.mjs/subagent-spawn.mjs/
@@ -22,18 +22,20 @@ import { pushReal } from "../context.mjs"
22
22
  import { offloadToolResult } from "../agent/helpers.mjs"
23
23
  import { logEvent } from "../log.mjs"
24
24
  import { digestBudgetOver, persistOverflowReport } from "./digest-budget.mjs"
25
- // 群 B 批 B5(§22 D-DG4):预算单源迁出(digest-budget.mjs)——原处 re-export 保测试
25
+ // 群 B 批 B5(AGENT-LOOP.md §6.14 D-DG4):预算单源迁出(digest-budget.mjs)——原处 re-export 保测试
26
26
  // 导入面零改(DIGEST_INJECT_BUDGET / _setDigestOffloadDirForTest)。
27
27
  export { DIGEST_INJECT_BUDGET, _setDigestOffloadDirForTest } from "./digest-budget.mjs"
28
28
  import {
29
29
  dependentLabels, maybeRefillAsync, refreshQueuedTokens,
30
30
  } from "./subagent-scheduler.mjs"
31
- // §11.2 (R13): advisor-pool cancel fallback + mutation logging for merged
31
+ // §6.10 (R13): advisor-pool cancel fallback + mutation logging for merged
32
32
  // code(lazy function-level cycle——advisor-async → async-settle → scheduler →
33
33
  // 本模块——全函数级绑定无求值期依赖,环安全)。
34
34
  import { cancelAsyncAdvisor, noteMutations, refreshAdvisorQueuedTokens } from "./advisor-async.mjs"
35
35
  // #94(VSC 侧并入——异步机械族):池读取载体吸收 + 墓碑写入单点(原 inline 写收口)。
36
36
  import { getAsyncPool, tombstoneOf, writeTombstone } from "./async-settle.mjs"
37
+ // #9(TOOLS.md §6.19):后台 bash 任务注入器(digest 单点族按 role="bg" 分发到本档)。
38
+ import { injectBgResult } from "./bash-async.mjs"
37
39
 
38
40
  // agent-tools 共享:并行子代理的审批/继续弹窗经 owner 上命名 promise 链串行——
39
41
  // 永不叠弹窗(返回链供调用方 .then 续接)。
@@ -44,7 +46,7 @@ export function enqueueAsk(owner, key, ask) {
44
46
  }
45
47
 
46
48
  // Async pool limits per role domain (AGENT-LOOP-ASYNC-POOL.md §6.10 — R14, 2026-09-06):
47
- // the old single cap (ASYNC_SUBAGENT_LIMIT = 4, §15 D-A4) evolved into two
49
+ // the old single cap (ASYNC_SUBAGENT_LIMIT = 4, AGENT-LOOP-SUBAGENT.md §6.7.3 D-A4) evolved into two
48
50
  // independent pools — eng-coder 4 / other roles 4 (user ruling "eng-coder 四路,
49
51
  // 其他 4 路") — a full engCoder pool never blocks an explore spawn and vice versa
50
52
  // (same-domain cap still 4, cross-domain total up to 8). The per-turn check
@@ -56,7 +58,7 @@ export function enqueueAsk(owner, key, ask) {
56
58
  export const ASYNC_POOL_LIMITS = { engCoder: 4, other: 4 }
57
59
 
58
60
  /**
59
- * Role → pool domain (single source of truth — §11.1 修正 #8): the CLI role
61
+ * Role → pool domain (single source of truth — §6.10 修正 #8): the CLI role
60
62
  * whitelist is subagent.mjs's ROLES = { explore, plan, coder, eng-coder, eng-designer }
61
63
  * (mode-filtered: normal → explore/plan/coder, engineering → explore/eng-designer/eng-coder).
62
64
  * eng-coder → engCoder pool; every other role (including unknown roles — fail-safe)
@@ -116,7 +118,7 @@ function warnPoolFallback(what, sig) {
116
118
  console.warn(`[config] agent.poolLimits: ${what} — must be a positive integer ≥1 — falling back to default ${JSON.stringify(ASYNC_POOL_LIMITS)}`)
117
119
  }
118
120
 
119
- /** Running count within ONE pool domain (口径同 §15 D-A1/T6: queued 与已完成不计入;
121
+ /** Running count within ONE pool domain (口径同 AGENT-LOOP-SUBAGENT.md §6.7.3 D-A1/T6: queued 与已完成不计入;
120
122
  * 条目带 _pool 域字段——spawn 时 poolDomainOf(role) 落位;缺字段(手工/旧条目)按
121
123
  * other——既有 coder 域测试语义不变)。 */
122
124
  export function runningPoolCount(parent, pool) {
@@ -194,7 +196,7 @@ export function cancelAsyncSubagent(agent, id) {
194
196
  entry.done = true
195
197
  entry.status = "done"
196
198
  map.delete(key)
197
- // §20 D-SD5 终态墓碑:queued 取消(无 settle 事件——出队即终态)——依赖者经
199
+ // AGENT-LOOP-SUBAGENT.md §6.9 D-SD5 终态墓碑:queued 取消(无 settle 事件——出队即终态)——依赖者经
198
200
  // 墓碑查得 cancelled 分支(round1 #4——cancel 返回时即重估标注)。
199
201
  writeTombstone(agent, key, "cancelled", entry.role)
200
202
  // af 批 F-6(§6.11 日志面):queued 取消不经 settle ⇒ 出队点直记一条 ev:cancelled
@@ -205,7 +207,7 @@ export function cancelAsyncSubagent(agent, id) {
205
207
  }
206
208
  // running:标记 + 条目 abort——settle finally 跑 cancelled 分支(移除 + stopped + 提醒)
207
209
  entry.cancelled = true
208
- // §20.3 站点 #11(第 24 批):定向中止 = cancel(reason 载荷——下游可判「谁杀的」)
210
+ // AGENT-LOOP-SUBAGENT.md §6.12 站点 #11(第 24 批):定向中止 = cancel(reason 载荷——下游可判「谁杀的」)
209
211
  entry.controller?.abort?.({ abortTrigger: "cancel", abortDetail: "subagent-cancel" })
210
212
  return { id: key, status: "cancelled" }
211
213
  }
@@ -238,7 +240,7 @@ export function cancelSyncChild(agent, key) {
238
240
  }
239
241
 
240
242
  /** subagent action:"cancel" (AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6): depth-0 main-session control only.
241
- * §20 D-SD5/round1 #4(queued 依赖取消——无 settle 事件):出队后**返回即**重估
243
+ * §6.9 D-SD5/round1 #4(queued 依赖取消——无 settle 事件):出队后**返回即**重估
242
244
  * 依赖者——依赖者留 queued 标 dependency cancelled(refreshQueuedTokens 发更新块头
243
245
  * token)+ 工具结果内注依赖者(模型可见——工具结果内——round1 #4 明示通道)+ 补位
244
246
  * (AUTO 档依赖者自动启动/槽位竞态释放)。被取消条目自身发 ⟦ev⟧cancelled 移除等待块。 */
@@ -253,7 +255,7 @@ export function executeCancelAction(args, ctx) {
253
255
  const key = String(id)
254
256
  const agent = ctx.agent
255
257
  const entry = getAsyncPool(agent, "subagent")?.get(key)
256
- // §11.2 (②-6b): an id that names no async SUBAGENT falls through to the
258
+ // §6.10 (②-6b): an id that names no async SUBAGENT falls through to the
257
259
  // async ADVISOR pool (the background reviews share the cancel surface — ⏹ on
258
260
  // an advisor block / action:'cancel' with an advisor id abort that review).
259
261
  // af 批 fix 轮(T-AF2 终态确认面 · §6.11 第 3 条「幂等」):已出队取消的 advisor id
@@ -277,7 +279,7 @@ export function executeCancelAction(args, ctx) {
277
279
  const hadDependents = entry ? dependentLabels(agent, key).length > 0 : false
278
280
  const result = cancelAsyncSubagent(agent, key)
279
281
  if (wasQueued && result.status === "cancelled" && result.was === "queued" && entry) {
280
- // §20 D-SD3b:取消/出队 → 移除等待块(不冻结——TUI routeSubToken cancelled 分支)
282
+ // §6.9 D-SD3b:取消/出队 → 移除等待块(不冻结——TUI routeSubToken cancelled 分支)
281
283
  ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧cancelled\x1e`)
282
284
  // 依赖者标注 + 位置前移 + AUTO 自动启动(round2 #3:AUTO 档才启动——手动留 queued)
283
285
  maybeRefillAsync(agent)
@@ -297,10 +299,10 @@ export function executeCancelAction(args, ctx) {
297
299
  }
298
300
 
299
301
  /**
300
- * 共享 post-spawn 管线(阻塞与 async 同一条——§15 D-A1 "全不变"):turn-cap continue
302
+ * 共享 post-spawn 管线(阻塞与 async 同一条——AGENT-LOOP-SUBAGENT.md §6.7.3 D-A1 "全不变"):turn-cap continue
301
303
  * 循环 → 拒绝降级 partial 返回 → MIN_REPORT_CHARS 扩写 → eng-coder mutation merge →
302
304
  * designId 后缀。onDeclined 在此(两路同一形态)——只有 askContinue 不同:阻塞经权限
303
- * 面板询问;async 永不弹面板(自动拒绝;engineering && AUTO 自动 resume——§15 D-A3)。
305
+ * 面板询问;async 永不弹面板(自动拒绝;engineering && AUTO 自动 resume——§6.7.3 D-A3)。
304
306
  */
305
307
  export async function runChildPipeline(child, input, childOpts, childRunOpts, { parent, role, args, askContinue }) {
306
308
  const declined = { partial: null }
@@ -344,29 +346,32 @@ export async function runChildPipeline(child, input, childOpts, childRunOpts, {
344
346
  }
345
347
 
346
348
  // ─── BATCH-3-STRUCTURE F-2:digest 注入批量预算(2026-09-09)───
347
- // §22 D-DG1(群 B 批 B5):常量 / 判超 / 记账 / 落盘四处合一入 digest-budget.mjs 叶子模块
349
+ // AGENT-LOOP.md §6.14 D-DG1(群 B 批 B5):常量 / 判超 / 记账 / 落盘四处合一入 digest-budget.mjs 叶子模块
348
350
  // (四族共享单源——D2);本文件保留 re-export(顶部)与经本入口的三族注入器接线。
349
351
 
350
352
  /**
351
353
  * Inject one settled async entry into the parent history as a user-role reminder
352
354
  * (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S3 — the auto channel, sole consumption path since AGENT-LOOP-SUBAGENT.md §6.7.5: turn-end
353
355
  * collection (collectSettledAsync, agent.mjs) and the run-start _pendingAsyncResults
354
- * injection; the message shape is identical to the §15 collector's). Consumed =
356
+ * injection; the message shape is identical to the §6.7.3 collector's). Consumed =
355
357
  * the caller removes the entry from its container; no double-inject across the
356
358
  * two paths (D-S3 "inject once" invariant).
357
359
  */
358
360
  export async function injectAsyncResult(agent, entry) {
361
+ // #9:后台 bash 任务族(role="bg")单列——专用注入器(状态行 + 尾截 + 全量在 log);
362
+ // 本函为 digest 注入单点族成员,消费面(run-start pending / 端壳残余注入)零改。
363
+ if (entry.role === "bg") return injectBgResult(agent, entry)
359
364
  const body = entry.error ?? entry.report ?? "(no report)"
360
- // F-2(BATCH-3-STRUCTURE)+ §22 D-DG2(群 B 批 B5):注入前查轮累计(单源)——超限条目不
365
+ // F-2(BATCH-3-STRUCTURE)+ AGENT-LOOP.md §6.14 D-DG2(群 B 批 B5):注入前查轮累计(单源)——超限条目不
361
366
  // inline 全文,改清单行(全文经 persistOverflowReport 落盘——path 随行)。首条豁免
362
367
  // (used===0 不判超):单条大报告 >64K offload 预览照旧——轮预算只约束累计。
363
368
  const raw = String(body)
364
369
  const over = digestBudgetOver(agent, raw.length)
365
370
  const preview = (over && await persistOverflowReport(raw, { tag: `async-subagent-${entry.id}` })) || await offloadToolResult(raw, `async-subagent-${entry.id}`)
366
- // §11.2: advisor entries label themselves (role "advisor") — the digest
371
+ // §6.10: advisor entries label themselves (role "advisor") — the digest
367
372
  // reminder says "async advisor review #N finished" (T-24b2 shape); subagent
368
373
  // entries keep the legacy wording verbatim.
369
- // §25 D-R17b (R17): escalate entries (role "escalate" — async 飞刀) label
374
+ // ESCALATE.md §5 D-R17b (R17): escalate entries (role "escalate" — async 飞刀) label
370
375
  // themselves the same way — the entry report body carries the merge/overlap
371
376
  // notes composed at settle (done/error classification — digest 指令语义 = 已
372
377
  // merge 报告可继续处置——动作域仍按消费回合档位——无族例外).
@@ -384,7 +389,7 @@ export async function injectAsyncResult(agent, entry) {
384
389
  role: "user",
385
390
  content: label,
386
391
  })
387
- // §20 D-SD5 终态墓碑:本函数是全部自动注入路径的共享形态(回合尾 collect + 挂起
392
+ // §6.9 D-SD5 终态墓碑:本函数是全部自动注入路径的共享形态(回合尾 collect + 挂起
388
393
  // digest 首行注入)——注入即消费(调用方随即从容器移除)——dependsOn 引用该 id 的
389
394
  // 后续 spawn 视为已满足(T-SD14 消费终态语义——AGENT-LOOP-SUBAGENT.md §6.7.5 后 check 消费路径删除,本自动
390
395
  // 通道为唯一消费方;error 条目记 failed——依赖取消/失败分支照旧,不误标成功)。
@@ -438,7 +443,7 @@ export function mergeChildMutations(parent, child) {
438
443
  if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
439
444
  merged.push(abs)
440
445
  }
441
- // §11.2: merged code mutates the parent's state — log it for the stale
446
+ // §6.10: merged code mutates the parent's state — log it for the stale
442
447
  // scan of in-flight reviews (a review whose code changed under it is stale).
443
448
  noteMutations(parent, merged)
444
449
  if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
@@ -20,6 +20,27 @@ function blockKeyIn(container, key) {
20
20
  return entries.some((e) => `${e.role}#${e.id}` === key)
21
21
  }
22
22
 
23
+ /** consult 子块键形态(#748):`consult#<N>`(N = 子块 relay 号——会话条目 `childIds` 表元素同形)。 */
24
+ function consultChildKey(key) {
25
+ const k = String(key ?? "")
26
+ return k.startsWith("consult#") && !k.includes("/") && k.length > "consult#".length
27
+ }
28
+
29
+ /** consult 子块消费判读(#748——与回收 ∥ 补发判据同源):会话在跑(`_consultSessions` 内未 stopped
30
+ * 会话携该 relay 号)∥ 会话条目在 pending(`childIds` 携该号)⇒ **未消化**(报告未达模型——
31
+ * 冻结 ∥ 回收即破坏消化顺序)。非 consult 子块键 ⇒ 恒 false。 */
32
+ function consultChildUndigested(agent, key) {
33
+ if (!consultChildKey(key)) return false
34
+ const id = String(key).slice("consult#".length)
35
+ for (const s of agent?._consultSessions?.values() ?? []) {
36
+ if (s.stopped !== true && (s.childIds ?? []).includes(id)) return true
37
+ }
38
+ for (const e of agent?._pendingAsyncResults ?? []) {
39
+ if (e?.role === "consult" && (e.childIds ?? []).includes(id)) return true
40
+ }
41
+ return false
42
+ }
43
+
23
44
  /**
24
45
  * AGENT-LOOP-SUBAGENT.md §6.7.2 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents/
25
46
  * _asyncAdvisors——经 getAsyncPool accessor——D1)无对应运行条目 + pending 单容器
@@ -28,6 +49,7 @@ function blockKeyIn(container, key) {
28
49
  * - pending 仍有对应(报告未达模型)→ 拒绝(提前回收破坏消化顺序——T-P3)
29
50
  * - 不存在的 key / 仍 running / done 的块 → 拒绝(T-P4——running 块 settle 时自冻)
30
51
  * - 无面板(现算返 null)→ 拒绝(headless/VS Code——freeze 不可用——T-P5)
52
+ * - consult 子块(#748):会话在跑 ∥ 会话条目在 pending(`childIds` 携该号)→ 拒绝(同判据——与回收 ∥ 补发同源)
31
53
  * 错误信息明确(模型可解释 + 自助修正)。返回 { ok:true } 或 { err }。
32
54
  */
33
55
  function panelFreezeGate(ctx, key) {
@@ -55,10 +77,14 @@ function panelFreezeGate(ctx, key) {
55
77
  return { err: `block ${key} still has a live pool entry — it is NOT a digested-stuck block (freeze refused; status action shows the pool)` }
56
78
  }
57
79
  // ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
58
- // 独立族退役)。
59
- if (blockKeyIn(ctx.agent._pendingAsyncResults, key)) {
80
+ // 独立族退役)。#748:consult 子块键不经裸键比对(会话条目键 `consult#<sessionId>` 与
81
+ // 子块键可数字撞键)——改判消费判据(会话在跑 ∥ 会话条目在 pending——与回收 ∥ 补发同源)。
82
+ if (!consultChildKey(key) && blockKeyIn(ctx.agent._pendingAsyncResults, key)) {
60
83
  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)` }
61
84
  }
85
+ if (consultChildUndigested(ctx.agent, key)) {
86
+ return { err: `block ${key} belongs to a consultation whose digest has not been consumed — the session is still running, or its session entry is still pending (freeze refused; the block is reclaimed at the consumption window)` }
87
+ }
62
88
  return { ok: true }
63
89
  }
64
90
 
@@ -151,8 +177,11 @@ export function executePanelAction(args, ctx) {
151
177
  // 读时交叉(round1 #3):pending/池均无对应 = 报告已消化(注入即从两者移除)——
152
178
  // 块驻留 = 状态滞后——digested:true(freeze 候选——模型可定位异常块)。
153
179
  // ASYNC-RESULT-CONTAINER.md D2:pending 单容器参与比对(四族统一)。
154
- out.digested = !blockKeyIn(agent._pendingAsyncResults, b.key)
155
- && !blockKeyIn(getAsyncPool(agent, "subagent"), b.key) && !blockKeyIn(getAsyncPool(agent, "advisor"), b.key)
180
+ // #748:consult 子块按消费判据(会话在跑 ∥ 会话条目在 pending ⇒ 未消化)——与门控同源。
181
+ out.digested = consultChildKey(b.key)
182
+ ? !consultChildUndigested(agent, b.key)
183
+ : !blockKeyIn(agent._pendingAsyncResults, b.key)
184
+ && !blockKeyIn(getAsyncPool(agent, "subagent"), b.key) && !blockKeyIn(getAsyncPool(agent, "advisor"), b.key)
156
185
  }
157
186
  return out
158
187
  })