thincoder 0.12.59 → 0.12.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +62 -4
  2. package/README.md +10 -8
  3. package/bin/thincoder.mjs +99 -133
  4. package/package.json +6 -4
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +45 -21
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +59 -137
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +156 -359
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +27 -7
  17. package/src/agent/completion.mjs +17 -11
  18. package/src/agent/dispatch.mjs +145 -27
  19. package/src/agent/helpers.mjs +107 -13
  20. package/src/agent/record-results.mjs +55 -11
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +242 -0
  23. package/src/agent/setup-reminders.mjs +69 -0
  24. package/src/agent/setup.mjs +107 -127
  25. package/src/agent/spawn-child.mjs +55 -13
  26. package/src/agent-tools/advisor-async.mjs +346 -0
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +167 -116
  29. package/src/agent-tools/async-settle.mjs +191 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +139 -107
  32. package/src/agent-tools/design-token.mjs +117 -0
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +24 -29
  35. package/src/agent-tools/escalate-async.mjs +289 -0
  36. package/src/agent-tools/read-history.mjs +166 -32
  37. package/src/agent-tools/recent-changes.mjs +2 -1
  38. package/src/agent-tools/review-streak.mjs +93 -0
  39. package/src/agent-tools/settings.mjs +137 -34
  40. package/src/agent-tools/subagent-actions.mjs +180 -133
  41. package/src/agent-tools/subagent-async.mjs +184 -177
  42. package/src/agent-tools/subagent-panel.mjs +160 -0
  43. package/src/agent-tools/subagent-run.mjs +205 -0
  44. package/src/agent-tools/subagent-scheduler.mjs +100 -27
  45. package/src/agent-tools/subagent-spawn.mjs +453 -0
  46. package/src/agent-tools/subagent.mjs +256 -578
  47. package/src/agent-tools/verify.mjs +119 -292
  48. package/src/agent-tools.mjs +1 -0
  49. package/src/agent.mjs +89 -205
  50. package/src/cli/distill-command.mjs +12 -6
  51. package/src/cli/make-agent.mjs +26 -8
  52. package/src/cli/memory-command.mjs +4 -3
  53. package/src/cli/permission.mjs +2 -2
  54. package/src/cli/setup-wizard.mjs +42 -17
  55. package/src/completions.mjs +114 -0
  56. package/src/config-migrate.mjs +70 -0
  57. package/src/config.mjs +180 -63
  58. package/src/context.mjs +5 -147
  59. package/src/conventions.mjs +223 -0
  60. package/src/crash-reports.mjs +128 -0
  61. package/src/distill.mjs +11 -11
  62. package/src/expand-home.mjs +16 -0
  63. package/src/explore-distill.mjs +155 -0
  64. package/src/generate-title.mjs +1 -1
  65. package/src/hooks.mjs +7 -3
  66. package/src/memory/code-index.mjs +9 -3
  67. package/src/memory/code-sync.mjs +72 -32
  68. package/src/memory/core.mjs +6 -193
  69. package/src/memory/delete.mjs +236 -0
  70. package/src/memory/docs.mjs +68 -54
  71. package/src/memory/file-walk.mjs +109 -0
  72. package/src/memory/schema.mjs +15 -3
  73. package/src/memory.mjs +3 -1
  74. package/src/model-ref.mjs +66 -0
  75. package/src/model-specs.mjs +42 -8
  76. package/src/peer-domains.mjs +265 -0
  77. package/src/peer-instances.mjs +231 -0
  78. package/src/prompt-overlays.mjs +82 -0
  79. package/src/prompts/advisor-design.md +18 -75
  80. package/src/prompts/advisor-round1.md +14 -67
  81. package/src/prompts/advisor-round2.md +15 -51
  82. package/src/prompts/advisor-round3.md +15 -51
  83. package/src/prompts/common.md +115 -0
  84. package/src/prompts/consult-base.md +5 -23
  85. package/src/prompts/discipline-engineering.md +217 -0
  86. package/src/prompts/discipline-normal.md +179 -0
  87. package/src/prompts/persona-coder.md +21 -0
  88. package/src/prompts/persona-eng-coder.md +37 -0
  89. package/src/prompts/persona-eng-designer.md +55 -0
  90. package/src/prompts/persona-engineering.md +54 -0
  91. package/src/prompts/persona-explore.md +15 -0
  92. package/src/prompts/persona-normal.md +27 -0
  93. package/src/prompts/persona-plan.md +26 -0
  94. package/src/provider/anthropic.mjs +4 -4
  95. package/src/provider/core.mjs +18 -98
  96. package/src/provider/errors.mjs +101 -0
  97. package/src/provider/google.mjs +5 -6
  98. package/src/provider/index.mjs +2 -1
  99. package/src/provider/list-models.mjs +93 -0
  100. package/src/provider/rate.mjs +2 -1
  101. package/src/provider/responses.mjs +5 -3
  102. package/src/provider/retry.mjs +8 -45
  103. package/src/provider/sse.mjs +3 -4
  104. package/src/proxy.mjs +9 -14
  105. package/src/session-gc.mjs +214 -0
  106. package/src/session-guard.mjs +47 -0
  107. package/src/session-rename.mjs +38 -0
  108. package/src/session-slots.mjs +188 -60
  109. package/src/session.mjs +104 -124
  110. package/src/token-ttl.mjs +274 -0
  111. package/src/tools/{system.mjs → bash.mjs} +19 -221
  112. package/src/tools/checklist-sync.mjs +181 -0
  113. package/src/tools/checklist.mjs +52 -39
  114. package/src/tools/edit-batch.mjs +109 -10
  115. package/src/tools/edit-diff.mjs +110 -27
  116. package/src/tools/edit.md +17 -12
  117. package/src/tools/execute.mjs +31 -4
  118. package/src/tools/file.mjs +41 -16
  119. package/src/tools/git.md +1 -1
  120. package/src/tools/git.mjs +23 -34
  121. package/src/tools/glob-dialect.mjs +130 -0
  122. package/src/tools/glob.md +3 -3
  123. package/src/tools/grep.md +1 -1
  124. package/src/tools/index.mjs +9 -8
  125. package/src/tools/ops.mjs +188 -3
  126. package/src/tools/patch.mjs +3 -3
  127. package/src/tools/question.md +4 -0
  128. package/src/tools/question.mjs +26 -0
  129. package/src/tools/read.md +1 -2
  130. package/src/tools/read_image.md +1 -1
  131. package/src/tools/search.mjs +236 -0
  132. package/src/tools/shared.mjs +14 -13
  133. package/src/tools/wait_for.md +22 -0
  134. package/src/tui/agent-turn.mjs +36 -228
  135. package/src/tui/ansi.mjs +2 -0
  136. package/src/tui/clipboard.mjs +7 -1
  137. package/src/tui/cmd-advisor.mjs +3 -2
  138. package/src/tui/cmd-config.mjs +142 -30
  139. package/src/tui/cmd-eng.mjs +28 -40
  140. package/src/tui/cmd-exit.mjs +6 -8
  141. package/src/tui/cmd-mcp.mjs +8 -2
  142. package/src/tui/cmd-model.mjs +14 -12
  143. package/src/tui/cmd-new.mjs +3 -2
  144. package/src/tui/cmd-reindex.mjs +7 -0
  145. package/src/tui/cmd-session.mjs +19 -4
  146. package/src/tui/cmd-submodel.mjs +8 -5
  147. package/src/tui/cmd-think.mjs +10 -10
  148. package/src/tui/cmd-undo.mjs +4 -3
  149. package/src/tui/cmd-upgrade.mjs +19 -4
  150. package/src/tui/config-helpers.mjs +28 -16
  151. package/src/tui/distill-cmd.mjs +1 -1
  152. package/src/tui/index.mjs +40 -38
  153. package/src/tui/interaction.mjs +3 -3
  154. package/src/tui/key-handler.mjs +61 -17
  155. package/src/tui/key-modes.mjs +86 -8
  156. package/src/tui/layout.mjs +18 -10
  157. package/src/tui/model-catalog.mjs +89 -0
  158. package/src/tui/model-picker.mjs +498 -0
  159. package/src/tui/mouse.mjs +52 -9
  160. package/src/tui/pickers.mjs +28 -392
  161. package/src/tui/render-frame.mjs +32 -16
  162. package/src/tui/render-loop.mjs +2 -0
  163. package/src/tui/render-segments.mjs +12 -9
  164. package/src/tui/render.mjs +37 -5
  165. package/src/tui/slash-commands.mjs +2 -2
  166. package/src/tui/startup.mjs +4 -0
  167. package/src/tui/subagent-blocks.mjs +106 -295
  168. package/src/tui/subagent-children.mjs +162 -0
  169. package/src/tui/subagent-freeze.mjs +169 -0
  170. package/src/tui/subagent-panel.mjs +24 -31
  171. package/src/tui/suspension-drive.mjs +297 -0
  172. package/src/tui/tool-args.mjs +7 -5
  173. package/src/tui/tool-display.mjs +143 -0
  174. package/src/tui/tool-events.mjs +56 -185
  175. package/src/tui/tui-lifecycle.mjs +46 -4
  176. package/src/tui/update-notice.mjs +4 -0
  177. package/src/tui/wizard.mjs +61 -21
  178. package/src/tui/wrapped-spawn.mjs +38 -0
  179. package/src/prompts/coder.md +0 -56
  180. package/src/prompts/discipline.md +0 -102
  181. package/src/prompts/eng-coder.md +0 -44
  182. package/src/prompts/engineering-sub.md +0 -34
  183. package/src/prompts/engineering.md +0 -340
  184. package/src/prompts/explore.md +0 -21
  185. package/src/prompts/main.md +0 -56
  186. package/src/prompts/methodology-template.md +0 -58
  187. package/src/prompts/plan.md +0 -16
  188. package/src/prompts/system.md +0 -63
  189. package/src/tools/pdf-parse-text.mjs +0 -497
  190. package/src/tools/pdf-parse-xref.mjs +0 -499
  191. package/src/tools/pdf.mjs +0 -155
  192. package/src/tools/read_pdf.md +0 -21
@@ -1,10 +1,14 @@
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——§11.1)/ executeCancelAction +
8
+ * cancelAsyncSubagent(§19.5 D-M6——工具与 TUI ⏹ 共用)/ runChildPipeline /
9
+ * injectAsyncResult / buildChildRunOpts / mergeChildMutations / enqueueAsk(批 6——
10
+ * _permQueue 审批/继续弹窗串行收 helper——消费位 subagent.mjs/subagent-spawn.mjs/
11
+ * escalate-async.mjs)。
8
12
  * 拆分(2026-09-05——Module Split Policy §20.9——纯迁移零行为变化):§20 调度器 + 文件域
9
13
  * 组 → ./subagent-scheduler.mjs(尾部 re-export 保测试动态 import 面——queueRunnable/
10
14
  * describeBlockers);status/panel/escalate 动作执行器 → ./subagent-actions.mjs。
@@ -16,14 +20,106 @@ import {
16
20
  import { runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
17
21
  import { pushReal } from "../context.mjs"
18
22
  import { offloadToolResult } from "../agent/helpers.mjs"
23
+ import { digestBudgetOver, persistOverflowReport } from "./digest-budget.mjs"
24
+ // 群 B 批 B5(§22 D-DG4):预算单源迁出(digest-budget.mjs)——原处 re-export 保测试
25
+ // 导入面零改(DIGEST_INJECT_BUDGET / _setDigestOffloadDirForTest)。
26
+ export { DIGEST_INJECT_BUDGET, _setDigestOffloadDirForTest } from "./digest-budget.mjs"
19
27
  import {
20
- describeBlockers, dependentLabels, detectStall, maybeRefillAsync, refreshQueuedTokens, STALL_NOTE,
28
+ dependentLabels, maybeRefillAsync, refreshQueuedTokens,
21
29
  } from "./subagent-scheduler.mjs"
30
+ // §11.2 (R13): advisor-pool cancel fallback + mutation logging for merged
31
+ // code(lazy function-level cycle——advisor-async → async-settle → scheduler →
32
+ // 本模块——全函数级绑定无求值期依赖,环安全)。
33
+ import { cancelAsyncAdvisor, noteMutations } from "./advisor-async.mjs"
22
34
 
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
35
+ // agent-tools 共享:并行子代理的审批/继续弹窗经 owner 上命名 promise 链串行——
36
+ // 永不叠弹窗(返回链供调用方 .then 续接)。
37
+ export function enqueueAsk(owner, key, ask) {
38
+ const chain = (owner[key] ?? Promise.resolve()).then(ask, ask)
39
+ owner[key] = chain
40
+ return chain
41
+ }
42
+
43
+ // Async pool limits per role domain (AGENT-LOOP.md §11.1 — R14, 2026-09-06):
44
+ // the old single cap (ASYNC_SUBAGENT_LIMIT = 4, §15 D-A4) evolved into two
45
+ // independent pools — eng-coder 4 / other roles 4 (user ruling "eng-coder 四路,
46
+ // 其他 4 路") — a full engCoder pool never blocks an explore spawn and vice versa
47
+ // (same-domain cap still 4, cross-domain total up to 8). The per-turn check
48
+ // budget was deleted with the check action (§19.8 — results arrive only via the
49
+ // auto channel; no loop guard needed).
50
+ // ⚠ 与 config.mjs DEFAULTS.agent.poolLimits 的 SUBAGENT 两键(engCoder/other)逐键同值
51
+ // (loadConfig/settings 默认源)——耦合锚 T-24a4 断言锁住——勿单侧改默认。advisor 第三键
52
+ // 不属本域(POOL-CONFIG-UNIFIED 2026-09-09——由 advisor-async 读取器消费——见该文件)。
53
+ export const ASYNC_POOL_LIMITS = { engCoder: 4, other: 4 }
54
+
55
+ /**
56
+ * Role → pool domain (single source of truth — §11.1 修正 #8): the CLI role
57
+ * enum/assembly table is subagent.mjs's ROLES = { explore, plan, coder, eng-coder }
58
+ * (mode-filtered: normal → explore/plan/coder, engineering → explore/plan/eng-coder).
59
+ * eng-coder → engCoder pool; every other role (including unknown roles — fail-safe)
60
+ * → other pool. escalate spawns its expert internally (role "coder" — other pool).
61
+ */
62
+ export function poolDomainOf(role) {
63
+ return role === "eng-coder" ? "engCoder" : "other"
64
+ }
65
+
66
+ /**
67
+ * Per-domain limit validation (T-24a4): each key must be a positive integer ≥1 —
68
+ * invalid (0 / -1 / "abc") or absent keys fall back to that domain's default (4),
69
+ * independent per key (a partial config — e.g. settings set
70
+ * agent.poolLimits.engCoder — takes effect with the untouched domain at default).
71
+ * Non-object raw (missing / string / number / array) → both domains at default.
72
+ */
73
+ export function resolvePoolLimits(raw) {
74
+ const limits = { ...ASYNC_POOL_LIMITS }
75
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return limits
76
+ for (const key of Object.keys(ASYNC_POOL_LIMITS)) {
77
+ const v = raw[key]
78
+ if (Number.isInteger(v) && v >= 1) limits[key] = v
79
+ }
80
+ return limits
81
+ }
82
+
83
+ /** One-time fallback warn dedupe (warnedModels / warnedContextProviders precedent). */
84
+ const warnedPoolConfigs = new Set()
85
+
86
+ /**
87
+ * Effective per-domain limits — read at EVERY pool admission (D-24a 生效语义:
88
+ * 不缓存常驻——配置变更即生效下个 spawn; /config 与 settings 保存经 reloadConfig /
89
+ * 热应用替换 agent.config——本函数每次读最新值). Invalid present values emit ONE
90
+ * warning per value shape, then fall back (timeoutMs precedent).
91
+ */
92
+ export function poolLimitsFor(agent) {
93
+ const raw = agent?.config?.agent?.poolLimits
94
+ const limits = resolvePoolLimits(raw)
95
+ if (raw !== undefined && raw !== null && (typeof raw !== "object" || Array.isArray(raw))) {
96
+ warnPoolFallback(`poolLimits has invalid shape (${JSON.stringify(raw)})`, "shape:" + JSON.stringify(raw))
97
+ return limits
98
+ }
99
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
100
+ for (const key of Object.keys(ASYNC_POOL_LIMITS)) {
101
+ const v = raw[key]
102
+ if (v !== undefined && !(Number.isInteger(v) && v >= 1)) {
103
+ warnPoolFallback(`poolLimits.${key} = ${JSON.stringify(v)}`, `${key}:` + JSON.stringify(v))
104
+ }
105
+ }
106
+ }
107
+ return limits
108
+ }
109
+
110
+ function warnPoolFallback(what, sig) {
111
+ if (warnedPoolConfigs.has(sig)) return
112
+ warnedPoolConfigs.add(sig)
113
+ console.warn(`[config] agent.poolLimits: ${what} — must be a positive integer ≥1 — falling back to default ${JSON.stringify(ASYNC_POOL_LIMITS)} (AGENT-LOOP.md §11.1)`)
114
+ }
115
+
116
+ /** Running count within ONE pool domain (口径同 §15 D-A1/T6: queued 与已完成不计入;
117
+ * 条目带 _pool 域字段——spawn 时 poolDomainOf(role) 落位;缺字段(手工/旧条目)按
118
+ * other——既有 coder 域测试语义不变)。 */
119
+ export function runningPoolCount(parent, pool) {
120
+ return [...(parent?._asyncSubagents?.values() ?? [])]
121
+ .filter((e) => e.status === "running" && (e._pool ?? "other") === pool).length
122
+ }
27
123
 
28
124
  /**
29
125
  * Resolve the sub-agent's provider from a model override string (shared with the
@@ -56,164 +152,13 @@ export function resolveChildProvider(parent, modelArg) {
56
152
  return { ...withKey(p), model: mname || p.model }
57
153
  }
58
154
  const byName = providers.find((x) => x.name === modelArg)
59
- if (byName) return withKey(byName)
155
+ // F-2c (MODEL-400-FIX):裸渠道名克隆须重派生 model——渠道裸克隆会丢 model 键 → 无 model 请求
156
+ // → serde 400。MODEL-SELECTION v2(M3④):取渠道默认模型(`provider.model` 单值——2026-09-10
157
+ // 起;老 models[] 首候选形态已退场);无默认模型 → 主 provider 的 model 兜底(T28)。
158
+ if (byName) return { ...withKey(byName), model: byName.model ?? parent.provider?.model }
60
159
  return { ...parent.provider, model: modelArg }
61
160
  }
62
161
 
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
162
  /**
218
163
  * §19.5 D-M6 cancel 核心(工具路径与 TUI ⏹ 共用):定向中止单个后台子代理,id 必填。
219
164
  * - queued 目标(未启动无 controller):出队 + 后续 position 前移 + settle waiter
@@ -235,7 +180,7 @@ export function cancelAsyncSubagent(agent, id) {
235
180
  }
236
181
  if (entry.cancelled) return { id: key, status: "cancelled" } // abort already in flight — idempotent
237
182
  if (entry.status === "queued") {
238
- // 出队 + position 释放/前移;settle 使在途 check waiter 终止(观察 cancelled
183
+ // 出队 + position 释放/前移(§19.5 D-M6——无 abort
239
184
  const queue = agent._asyncQueue ?? []
240
185
  const qi = queue.indexOf(entry)
241
186
  if (qi >= 0) {
@@ -251,15 +196,39 @@ export function cancelAsyncSubagent(agent, id) {
251
196
  const tombstones = (agent._asyncTombstones ??= new Map())
252
197
  tombstones.set(key, { status: "cancelled", role: entry.role })
253
198
  entry._settle?.()
254
- for (const w of agent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
255
199
  return { id: key, status: "cancelled", was: "queued" }
256
200
  }
257
201
  // running:标记 + 条目 abort——settle finally 跑 cancelled 分支(移除 + stopped + 提醒)
258
202
  entry.cancelled = true
259
- entry.controller?.abort?.()
203
+ // §20.3 站点 #11(第 24 批):定向中止 = cancel(reason 载荷——下游可判「谁杀的」)
204
+ entry.controller?.abort?.({ abortTrigger: "cancel", abortDetail: "subagent-cancel" })
260
205
  return { id: key, status: "cancelled" }
261
206
  }
262
207
 
208
+ /**
209
+ * SYNC-CANCEL(L52——2026-09-09):sync spawn(深度>0 或阻塞调用——阻塞路径)定向中止
210
+ * 核心(TUI ⏹ 直连——不经模型回合)。registry `agent._syncChildAborts`(key =
211
+ * relayPrefix 去尾 `role#N`——subagent.mjs 阻塞路径注册 { ctrl, stopped:false },
212
+ * try/finally 三路径注销——R7)——与 async 的池条目 controller 存 agent 分层一致
213
+ * (subagent.mjs 装配注):
214
+ * - 有 live ctrl(未 stopped)→ 置 stopped 旗标 + ctrl.abort()(child runAgent 的
215
+ * childRunOpts.signal = ctrl.signal——N4 无轮询直连 abort——AbortError 解绕)→
216
+ * { id, status:"cancelled" }——catch 三分支②折叠 stopped partial 报告(父回合继续)
217
+ * - 无 live ctrl(registry miss——未注册/已注销——或已 stopped——stop 在途/已完成)→
218
+ * error 文案(mouse :199 同款——"finished or stop no longer applies")
219
+ * 幂等边界:stopped 后再调恒 error(不重复 abort——无副作用);重复 ⏹ 由调用方文案兜底。
220
+ */
221
+ export function cancelSyncChild(agent, key) {
222
+ const k = String(key)
223
+ const entry = agent?._syncChildAborts?.get(k)
224
+ if (!entry || entry.stopped) {
225
+ return { id: k, status: "error", error: `sync child ${k} has no live stop control — it has finished or stop no longer applies` }
226
+ }
227
+ entry.stopped = true
228
+ entry.ctrl.abort({ abortTrigger: "cancel", abortDetail: "sync-child-cancel" })
229
+ return { id: k, status: "cancelled" }
230
+ }
231
+
263
232
  /** subagent action:"cancel" (§19.5 D-M6): depth-0 main-session control only.
264
233
  * §20 D-SD5/round1 #4(queued 依赖取消——无 settle 事件):出队后**返回即**重估
265
234
  * 依赖者——依赖者留 queued 标 dependency cancelled(refreshQueuedTokens 发更新块头
@@ -276,6 +245,12 @@ export function executeCancelAction(args, ctx) {
276
245
  const key = String(id)
277
246
  const agent = ctx.agent
278
247
  const entry = agent._asyncSubagents?.get(key)
248
+ // §11.2 (②-6b): an id that names no async SUBAGENT falls through to the
249
+ // async ADVISOR pool (the background reviews share the cancel surface — ⏹ on
250
+ // an advisor block / action:'cancel' with an advisor id abort that review).
251
+ if (!entry && agent?._asyncAdvisors?.has(key)) {
252
+ return JSON.stringify(cancelAsyncAdvisor(agent, key))
253
+ }
279
254
  const wasQueued = entry?.status === "queued"
280
255
  // 依赖者快照(出队前——用于 AUTO 分支判定"是否有依赖者被本次取消波及";note 组装在
281
256
  // refill 后按**仍 queued** 的实况重算——防 AUTO 已自动启动后文案称 "stay queued")
@@ -348,24 +323,51 @@ export async function runChildPipeline(child, input, childOpts, childRunOpts, {
348
323
  return report
349
324
  }
350
325
 
326
+ // ─── BATCH-3-STRUCTURE F-2:digest 注入批量预算(2026-09-09)───
327
+ // §22 D-DG1(群 B 批 B5):常量 / 判超 / 记账 / 落盘四处合一入 digest-budget.mjs 叶子模块
328
+ // (四族共享单源——D2);本文件保留 re-export(顶部)与经本入口的三族注入器接线。
329
+
351
330
  /**
352
331
  * 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.
332
+ * (§17 D-S3 — the auto channel, sole consumption path since §19.8: turn-end
333
+ * collection (collectSettledAsync, agent.mjs) and the run-start _pendingAsyncResults
334
+ * injection; the message shape is identical to the §15 collector's). Consumed =
335
+ * the caller removes the entry from its container; no double-inject across the
336
+ * two paths (D-S3 "inject once" invariant).
357
337
  */
358
338
  export async function injectAsyncResult(agent, entry) {
359
339
  const body = entry.error ?? entry.report ?? "(no report)"
360
- const preview = await offloadToolResult(String(body), `async-subagent-${entry.id}`)
340
+ // F-2(BATCH-3-STRUCTURE)+ §22 D-DG2(群 B 批 B5):注入前查轮累计(单源)——超限条目不
341
+ // inline 全文,改清单行(全文经 persistOverflowReport 落盘——path 随行)。首条豁免
342
+ // (used===0 不判超):单条大报告 >64K offload 预览照旧——轮预算只约束累计。
343
+ const raw = String(body)
344
+ const over = digestBudgetOver(agent, raw.length)
345
+ const preview = (over && await persistOverflowReport(raw, { tag: `async-subagent-${entry.id}` })) || await offloadToolResult(raw, `async-subagent-${entry.id}`)
346
+ // §11.2: advisor entries label themselves (role "advisor") — the digest
347
+ // reminder says "async advisor review #N finished" (T-24b2 shape); subagent
348
+ // entries keep the legacy wording verbatim.
349
+ // §25 D-R17b (R17): escalate entries (role "escalate" — async 飞刀) label
350
+ // themselves the same way — the entry report body carries the merge/overlap
351
+ // notes composed at settle (done/error classification — digest 指令语义 = 已
352
+ // merge 报告可继续处置——动作域仍按消费回合档位——无族例外).
353
+ let label
354
+ if (entry.role === "advisor") {
355
+ label = `[System reminder: async advisor review #${entry.id} finished]\n${escapeXml(preview)}`
356
+ } else if (entry.role === "escalate") {
357
+ label = entry.error != null
358
+ ? `[System reminder: async escalate #${entry.id} ended with an error]\n${escapeXml(preview)}`
359
+ : `[System reminder: async escalate #${entry.id} finished — post-op report (mutations merged)]\n${escapeXml(preview)}`
360
+ } else {
361
+ label = `[System reminder: async subagent #${entry.id} (${entry.role}) finished]\n${escapeXml(preview)}`
362
+ }
361
363
  pushReal(agent, {
362
364
  role: "user",
363
- content: `[System reminder: async subagent #${entry.id} (${entry.role}) finished]\n${escapeXml(preview)}`,
365
+ content: label,
364
366
  })
365
367
  // §20 D-SD5 终态墓碑:本函数是全部自动注入路径的共享形态(回合尾 collect + 挂起
366
368
  // digest 首行注入)——注入即消费(调用方随即从容器移除)——dependsOn 引用该 id 的
367
- // 后续 spawn 视为已满足(T-SD14 check 消费语义;error 条目记 failed——依赖取消/
368
- // 失败分支照旧,不误标成功)。
369
+ // 后续 spawn 视为已满足(T-SD14 消费终态语义——§19.8 check 消费路径删除,本自动
370
+ // 通道为唯一消费方;error 条目记 failed——依赖取消/失败分支照旧,不误标成功)。
369
371
  const tombstones = (agent._asyncTombstones ??= new Map())
370
372
  tombstones.set(String(entry.id), { status: entry.error != null ? "failed" : "consumed", role: entry.role })
371
373
  }
@@ -409,9 +411,14 @@ export function mergeChildMutations(parent, child) {
409
411
  // to the parent's guard state.
410
412
  if (!child._mutatedThisRun || !(child._touchedFiles?.length)) return false
411
413
  parent._mutatedThisRun = true
414
+ const merged = []
412
415
  for (const abs of child._touchedFiles ?? []) {
413
416
  if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
417
+ merged.push(abs)
414
418
  }
419
+ // §11.2: merged code mutates the parent's state — log it for the stale
420
+ // scan of in-flight reviews (a review whose code changed under it is stale).
421
+ noteMutations(parent, merged)
415
422
  if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
416
423
  if (parent._verifiedThisRun) {
417
424
  parent._verifiedThisRun = false
@@ -0,0 +1,160 @@
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
+ * CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):手工面板镜像退役——视图面与门控改读时现算
9
+ * computePanelBlocks(ctx.state)(ctx.state = agent._tuiState——TUI 装配处接线——单账本);
10
+ * 未挂载(headless/VSC/子代理)→ 现算返 null → 降级/报不可用照旧
11
+ * (T-P5 语义零变)。门控语义零动(F-4)——仅数据源从镜像换现算。
12
+ * subagent-actions.mjs 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面。
13
+ */
14
+ import { getAsyncPool } from "./async-settle.mjs"
15
+ import { computePanelBlocks } from "../tui/subagent-freeze.mjs" // F-3:面板视图读时现算(单账本——tui→core 单向依赖,无环)
16
+
17
+ /** 面板块 key(role#N)在池(Map——条目值)/pending 单容器(数组)中的归属判定。 */
18
+ function blockKeyIn(container, key) {
19
+ const entries = container instanceof Map ? [...container.values()] : (container ?? [])
20
+ return entries.some((e) => `${e.role}#${e.id}` === key)
21
+ }
22
+
23
+ /**
24
+ * §19.6 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents/
25
+ * _asyncAdvisors——经 getAsyncPool accessor——D1)无对应运行条目 + pending 单容器
26
+ * (_pendingAsyncResults——D2)无对应条目的块(= 已消化驻留块——报告已入模型上下文
27
+ * ——pending 已消费——状态滞后——补发冻结不破坏任何顺序)。
28
+ * - pending 仍有对应(报告未达模型)→ 拒绝(提前回收破坏消化顺序——T-P3)
29
+ * - 不存在的 key / 仍 running / done 的块 → 拒绝(T-P4——running 块 settle 时自冻)
30
+ * - 无面板(现算返 null)→ 拒绝(headless/VS Code——freeze 不可用——T-P5)
31
+ * 错误信息明确(模型可解释 + 自助修正)。返回 { ok:true } 或 { err }。
32
+ */
33
+ function panelFreezeGate(ctx, key) {
34
+ // F-3(2026-09-10):镜像退役——经 ctx.state 读时现算(CLI-ACTIVITY-DEBLOAT)。
35
+ // 门控语义零动(F-4):awaitingDigest 限定/池归属查/pending 查逐字保留——仅数据源换现算。
36
+ const snap = computePanelBlocks(ctx.state)
37
+ if (!Array.isArray(snap)) {
38
+ return { err: "panel unavailable — no CLI TUI panel in this session (headless / VS Code / subagent contexts — freeze unavailable; panel is CLI-TUI-only, AC-P4)" }
39
+ }
40
+ const block = snap.find((b) => b.key === key)
41
+ if (!block) {
42
+ const live = snap.map((b) => `${b.key}(${b.status})`).join(", ")
43
+ return { err: `unknown panel block key: ${key} — the live panel holds: ${live || "(no blocks)"}` }
44
+ }
45
+ if (block.status !== "awaitingDigest") {
46
+ if (block.status === "done") {
47
+ 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)` }
48
+ }
49
+ if (block.status === "running") {
50
+ 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)` }
51
+ }
52
+ return { err: `block ${key} is in state ${block.status} — freeze only reclaims awaitingDigest blocks whose report is already digested` }
53
+ }
54
+ if (blockKeyIn(getAsyncPool(ctx.agent, "subagent"), key) || blockKeyIn(getAsyncPool(ctx.agent, "advisor"), key)) {
55
+ 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
+ }
57
+ // ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
58
+ // 独立族退役)。
59
+ if (blockKeyIn(ctx.agent._pendingAsyncResults, key)) {
60
+ 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)` }
61
+ }
62
+ return { ok: true }
63
+ }
64
+
65
+ /**
66
+ * §19.6 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
67
+ * freeze 优先):
68
+ * - view(缺省——返回面板块列表):ctx.state(= agent._tuiState——CLI TUI 装配)经
69
+ * computePanelBlocks **读时现算**(F-3 单账本——与用户所见一致)。awaitingDigest 条目
70
+ * **读时交叉**
71
+ * _pendingAsyncResults/池 标注 digested(round1 #3——digested:true
72
+ * = 报告已消化但块仍驻留——异常块——freeze 候选;模型可定位解释 UI 怪相)。
73
+ * - freeze:key(D-P3 门控通过 → 发 key + "/" + ⟦ev⟧done 哨兵字面 token——
74
+ * onToken——TUI routeSubToken 冻结回收——落位复用 sub._freezeAt settle 锚点
75
+ * splice,无锚点尾推兜底——§17.5.5 同口径——round1 #2)。
76
+ * 无 TUI state(headless/VS Code——D-P2 round1 #1:webview 无 state.subTasks 对应物——
77
+ * 7.2.3.2 #8 先例)→ view 恒降级池视图(双池经 getAsyncPool + pending 单容器
78
+ * 合成)+ no panel 注;freeze 报不可用。CLI-only 完整能力(AC-P4)。
79
+ */
80
+ export function executePanelAction(args, ctx) {
81
+ const agent = ctx.agent
82
+ const freezeKey = (args?.freeze !== undefined && args?.freeze !== null && String(args.freeze) !== "")
83
+ ? String(args.freeze)
84
+ : null
85
+ // ── freeze 面(优先——D-P2 单动作双参互斥)──
86
+ if (freezeKey) {
87
+ if ((ctx.depth ?? 0) > 0) {
88
+ 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)" })
89
+ }
90
+ const gate = panelFreezeGate(ctx, freezeKey)
91
+ if (gate.err) return JSON.stringify({ status: "error", error: gate.err })
92
+ if (!ctx.callbacks?.onToken) {
93
+ return JSON.stringify({ status: "error", error: `panel present but no token relay in this context — the freeze of ${freezeKey} cannot reach the TUI` })
94
+ }
95
+ // 门控通过 → 发 done 冻结事件(settle 同机制字面格式——TUI routeSubToken done
96
+ // 分支冻结回收——落位 _freezeAt settle 锚点 splice;无锚点(旧会话残留)时
97
+ // freezeSubTaskLines 尾推兜底——注明两种落位,模型不被误导(advisor 🟡1)。
98
+ ctx.callbacks.onToken(`${freezeKey}/⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
99
+ return JSON.stringify({
100
+ key: freezeKey,
101
+ status: "frozen",
102
+ 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)",
103
+ })
104
+ }
105
+ // ── view 面(缺省——readonly)──
106
+ // 双参互斥(D-P2):freeze 优先;显式 view:false 且无 freeze = 无请求可执行——报错。
107
+ if (args?.view === false) {
108
+ 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)" })
109
+ }
110
+ const snap = computePanelBlocks(ctx.state)
111
+ if (!Array.isArray(snap)) {
112
+ // F-P3 降级:无 TUI state(headless/VS Code/子代理上下文——CLI TUI-only 完整能力)→
113
+ // 池视图(双池经 getAsyncPool——运行/排队条目 + pending 单容器待消化条目)
114
+ const blocks = []
115
+ const queue = agent._asyncQueue ?? []
116
+ for (const e of [...(getAsyncPool(agent, "subagent")?.values() ?? []), ...(getAsyncPool(agent, "advisor")?.values() ?? [])]) {
117
+ const b = { key: `${e.role}#${e.id}`, role: e.role }
118
+ if (e.status === "running") {
119
+ b.status = "running"
120
+ b.elapsedSec = e.startedAt ? Math.max(0, Math.floor((Date.now() - e.startedAt) / 1000)) : 0
121
+ } else if (e.status === "queued") {
122
+ b.status = "queued"
123
+ const qi = queue.indexOf(e)
124
+ b.position = qi >= 0 ? qi + 1 : (e.position ?? null)
125
+ } else {
126
+ b.status = "done" // 回合内 settle 未取——自动通道(回合尾 collect/挂起 digest)送达
127
+ }
128
+ blocks.push(b)
129
+ }
130
+ // ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
131
+ // 独立族退役)——族注记按 role 保留。
132
+ for (const e of agent._pendingAsyncResults ?? []) {
133
+ const note = e.role === "consult"
134
+ ? "consultation digest pending — injected at the next run start (§25 D-R17a)"
135
+ : e.role === "escalate"
136
+ ? "report pending — injected at the next run start (§25 D-R17b)"
137
+ : "report pending — injected at the next run start (§17)"
138
+ blocks.push({ key: `${e.role}#${e.id}`, role: e.role, status: "awaitingDigest", note })
139
+ }
140
+ return JSON.stringify({
141
+ degraded: true,
142
+ note: "no panel — this session has no CLI TUI panel (headless / VS Code / subagent context — panel view is CLI-TUI-only, AC-P4); pool-derived view below; action:'status' shows the full pool",
143
+ panel: blocks,
144
+ })
145
+ }
146
+ const panel = snap.map((b) => {
147
+ const out = { key: b.key, role: b.role, status: b.status }
148
+ if (b.status === "running") {
149
+ out.elapsedSec = b.startedAt ? Math.max(0, Math.floor((Date.now() - b.startedAt) / 1000)) : 0
150
+ } else if (b.status === "awaitingDigest") {
151
+ // 读时交叉(round1 #3):pending/池均无对应 = 报告已消化(注入即从两者移除)——
152
+ // 块驻留 = 状态滞后——digested:true(freeze 候选——模型可定位异常块)。
153
+ // 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)
156
+ }
157
+ return out
158
+ })
159
+ return JSON.stringify({ panel })
160
+ }