thincoder 0.12.59 → 0.12.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. package/src/tools/read_pdf.md +0 -21
@@ -0,0 +1,172 @@
1
+ /**
2
+ * subagent-freeze.mjs — 子agent 区块完成/冻结族(2026-09-05 module-split:
3
+ * subagent-blocks.mjs 625 > 500 硬限——§19.6 面板镜像 syncPanelSnapshot + finish/
4
+ * freeze 家族 verbatim 迁入,语义零变;subagent-blocks.mjs import 回(routeSub* 与
5
+ * compression panel 调用点)+ re-export 保外部 import 面(index.mjs 等)。
6
+ */
7
+
8
+ import { C } from "./ansi.mjs"
9
+ import { closeOpenSubChildren } from "./subagent-children.mjs"
10
+
11
+ // ─── §19.6 D-P1 面板镜像(subagent panel 检查工具)───
12
+ // TUI 装配处(index.mjs startTUI)state._agent = agent——块级状态变更点经
13
+ // syncPanelSnapshot 刷新 agent._panelSnapshot(区块数组快照:{key, role,
14
+ // status: running|done|awaitingDigest, startedAt}——状态变更即刷——O(n) 小——
15
+ // 不逐 token——NF-P)。agent 层 subagent action:"panel" 读此镜像(视图与用户
16
+ // 所见一致)+ 冻结门控(D-P3)。未挂载(headless/mock——无 state._agent)=
17
+ // 无镜像——no-op(降级路径由 agent 侧判定)。
18
+
19
+ /** 刷新面板镜像(调用点 = 本模块全部 state.subTasks 块级变更点末尾——map 已稳定)。 */
20
+ export function syncPanelSnapshot(state) {
21
+ const agent = state._agent
22
+ if (!agent) return // 未挂载(headless/mock 无 TUI 装配)——无镜像
23
+ const snap = []
24
+ for (const sub of Object.values(state.subTasks ?? {})) {
25
+ // §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(镜像与面板
26
+ // 所见一致——action:'panel' 视图 + freeze 门控读此态)。
27
+ const status = sub.done ? (sub.awaitingDigest ? "awaitingDigest" : "done") : (sub.queued ? "queued" : "running")
28
+ snap.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
29
+ }
30
+ agent._panelSnapshot = snap
31
+ }
32
+
33
+ /** Mark the earliest running child of the given role(s) as done (✓ header, frozen elapsed). */
34
+ export function finishSubTask(state, roles, lastError = null) {
35
+ state.subTasks ??= {}
36
+ const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
37
+ const running = Object.values(state.subTasks)
38
+ .filter((s) => !s.done && roleSet.has(s.role))
39
+ .sort((a, b) => a.started - b.started)
40
+ if (running.length === 0) return
41
+ const sub = running[0]
42
+ sub.done = true
43
+ sub.doneAt = Date.now()
44
+ sub.currentTool = null
45
+ sub.approval = null
46
+ if (lastError) sub.lastError = lastError
47
+ sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
48
+ syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
49
+ }
50
+
51
+ /** §7.2.3 sync spawn 完成精确冻结(2026-09-03):按 relay key 精确 settle。finishSubTask
52
+ * 的"最早 started"启发式只在面板单 running 块时成立——§15 async 化后 async eng-coder
53
+ * (先启动)与 sync explore 并存时 explore 完成会误冻先启动的 eng-coder 块(7.2.3.1)。
54
+ * dispatch 沿 ctx._subagentKey(relayPrefix 去尾 = `role#N`)把 key 传进 onToolResult——
55
+ * 有 key 即精确标 done(冻结载体进流 + 删条目由调用方 freezeDoneSubTasks 承接——与
56
+ * finishSubTask 同一契约);无匹配块(已冻结 tombstone/不存在)返回 null——调用方不得
57
+ * 落启发式兜底(精确 key 无匹配 = 无从归属——不误冻他块)。 */
58
+ export function finishSubTaskKey(state, key, lastError = null) {
59
+ const sub = state.subTasks?.[key]
60
+ if (!sub) return null
61
+ sub.done = true
62
+ sub.doneAt = Date.now()
63
+ sub.currentTool = null
64
+ sub.approval = null
65
+ if (lastError) sub.lastError = lastError
66
+ sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
67
+ syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
68
+ return sub
69
+ }
70
+
71
+ /** 冻结完成/中断区块进 state.lines(§7.2 D4——_frozenSubTask 载体行,渲染端
72
+ * render-conversation 识别;折叠交互 key = sub-${key} 与运行面板同源跨冻结延续)。
73
+ * §27 R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层子块未收尾(外层 abort/
74
+ * 中断)→ 子块随外层冻结定格 stopped(closeOpenSubChildren——不悬空)。
75
+ * 锚点插入(2026-09-03 修复轮):settled 块带 _freezeAt(settle 时刻流位置)——
76
+ * splice 落位使挂起期补发冻结块位于其 digest 总览文本之前;无锚点尾推不变;
77
+ * 多锚点批量冻结按降序(绝对位置 splice——先插小锚点会移走大锚点目标)。 */
78
+ export function freezeSubTaskLines(state, sub) {
79
+ if (!sub) return
80
+ closeOpenSubChildren(sub) // R23 D-R23c2——开子块随外层冻结定格 stopped
81
+ state._frozenSubKeys ??= new Set()
82
+ state._frozenSubKeys.add(sub.key)
83
+ sub.done = true
84
+ sub.doneAt = sub.doneAt ?? Date.now()
85
+ const anchor = sub._freezeAt ?? state.lines.length
86
+ state.lines.splice(Math.min(anchor, state.lines.length), 0, {
87
+ text: `subagent activity: ${sub.key}`, color: C.dim, _frozenSubTask: sub,
88
+ })
89
+ }
90
+
91
+ /** 头裁锚点校正(index.mjs pushLine 调用):裁 removedCount 补 1 标记行 = 净位移
92
+ * removedCount−1(code review round1 #3);在途锚点前移,min 0 兜底。 */
93
+ export function shiftFreezeAnchors(state, removedCount) {
94
+ const shift = removedCount - 1
95
+ for (const sub of Object.values(state.subTasks ?? {})) {
96
+ if (sub._freezeAt !== undefined) sub._freezeAt = Math.max(0, sub._freezeAt - shift)
97
+ }
98
+ }
99
+
100
+ /** 冻结 + 释放全部已 done 块(工具结果清扫路径)。锚点降序(后 settle 先插——
101
+ * 绝对位置 splice 语义);无锚点(-1)排末尾推;sort 稳定。 */
102
+ export function freezeDoneSubTasks(state) {
103
+ const subs = Object.values(state.subTasks ?? {})
104
+ .filter((s) => s.done)
105
+ .sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
106
+ for (const sub of subs) {
107
+ freezeSubTaskLines(state, sub)
108
+ delete state.subTasks[sub.key]
109
+ }
110
+ syncPanelSnapshot(state) // §19.6 D-P1: 删除后刷镜(块移出面板)
111
+ }
112
+
113
+ /** 按角色整组标记 done——consult N 并行 children 的会话级 settle(单发
114
+ * finishSubTask 只结最早一个——consult 残项 2026-08-30)。 */
115
+ export function finishSubTasksByRole(state, roles, lastError = null) {
116
+ state.subTasks ??= {}
117
+ const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
118
+ for (const sub of Object.values(state.subTasks)) {
119
+ if (!sub.done && roleSet.has(sub.role)) {
120
+ sub.done = true
121
+ sub.doneAt = Date.now()
122
+ sub.currentTool = null
123
+ sub.approval = null
124
+ if (lastError) sub.lastError = lastError
125
+ sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
126
+ }
127
+ }
128
+ syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
129
+ }
130
+
131
+ /** 回合尾/挂起退出清扫(runAgentTurn finally / suspensionSession finally):冻结全部
132
+ * 剩余块——中断(Ctrl+C/错误)不留下钉住输入框的 ghost;未 done 者
133
+ * lastError="interrupted"(Ready 态跳过)。§17.5.5:挂起自然退出时本函数只兜底
134
+ * **未消化残项**(已消化块由 freezeReclaimDigestedBlocks 逐条先行回收——块回收与
135
+ * 池空解耦)。锚点降序同 freezeDoneSubTasks(挂起期 settle 锚点交错批次各按其
136
+ * settle 位置落位)。 */
137
+ export function freezeAllSubTasks(state) {
138
+ const subs = Object.values(state.subTasks ?? {})
139
+ .sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1))
140
+ for (const sub of subs) {
141
+ if (!sub.done) {
142
+ sub.done = true
143
+ sub.doneAt = Date.now()
144
+ if (!sub.lastError && state.status !== "Ready") sub.lastError = "interrupted"
145
+ }
146
+ freezeSubTaskLines(state, sub)
147
+ delete state.subTasks[sub.key]
148
+ }
149
+ syncPanelSnapshot(state) // §19.6 D-P1: 冻结回收后刷镜(块移出面板)
150
+ }
151
+
152
+ /** §17.5.5 消化完成逐条冻结回收(2026-09-03 实测修订 + round1 #1 位置裁定):digest/会话内
153
+ * 用户回合消化完 pending 条目(run 首行已注入)后调用——把"已消化但仍驻留面板"的
154
+ * awaitingDigest 块立即冻结进流(不等池空——块回收与池空解耦;池空 freezeAllSubTasks
155
+ * 仅兜底未消化残项)。归属不变式:会话内任何 run 开始前 pinned 块的条目必在 pending
156
+ * (settle 即移交);run 消费后条目不在 pending 的 pinned 块 = 本 run 消化者——无需
157
+ * 快照即精确归属。位置(round1 #1 裁定——与 17.5.5 早版文本的矛盾已消解,见
158
+ * AGENT-LOOP.md §17.5.5):**settle 锚点 splice 落位——digest 总览文本之前**——同
159
+ * §7.2 D4 修复轮/D-S8 锚点语义(T-S6/T-S14 位置断言同口径)——锚点降序逐块冻结
160
+ * (splice 绝对位互不位移)。@returns {number} 回收块数 */
161
+ export function freezeReclaimDigestedBlocks(state, pendingList) {
162
+ const pend = pendingList ?? []
163
+ const targets = Object.values(state.subTasks ?? {})
164
+ .filter((s) => s.awaitingDigest && !pend.some((e) => `${e.role}#${e.id}` === s.key))
165
+ .sort((a, b) => (b._freezeAt ?? -1) - (a._freezeAt ?? -1)) // 锚点降序(同 freezeAllSubTasks)
166
+ for (const sub of targets) {
167
+ freezeSubTaskLines(state, sub) // splice 落 settle 锚点(digest 总览文本之前)
168
+ delete state.subTasks[sub.key]
169
+ }
170
+ syncPanelSnapshot(state) // §19.6 D-P1: 逐条回收后刷镜(块移出面板)
171
+ return targets.length
172
+ }
@@ -12,7 +12,8 @@
12
12
  *
13
13
  * §19.5 D-M7 ⏹:运行中(非 done)折叠头右缘停止标记(dim,仅折叠头)——点击 =
14
14
  * cancel(mouse.mjs 列级命中 _stopCol——不触发折叠翻转)。
15
- * §19.5 D-M8:本模块同时承载子标行 dim 样式(styleSubLabelRow——面板与冻结渲染共用)。
15
+ * §27 R23(supersede §19.5 D-M8 子标方案):嵌套子代理渲染为子块段(renderSubChildSections
16
+ * ——子块头 + 独立折叠键——面板与冻结渲染共用——styleSubLabelRow 已随子标机制退役)。
16
17
  *
17
18
  * 中立模块(D1 评审 #6):layout.mjs 调 renderSubagentPanel 预计算面板高度
18
19
  * (subagentLines → subagentH),render-frame.mjs 直接 put 预计算行(不重复
@@ -30,26 +31,61 @@ import { sliceByWidth, stringWidth } from "./render.mjs"
30
31
  import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } from "./fold-block.mjs"
31
32
  import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
32
33
 
33
- /** 子标行行首标记(`explore#1 · `——sublabelLine 写入的字面形态)。 */
34
- const SUB_LABEL_RE = /^([\w-]+#\d+(?:\/[\w-]+#\d+)* · )/
34
+ /**
35
+ * §27 R23 子块(嵌套子代理)段渲染——面板与冻结渲染共用(中立纯函数模块):
36
+ * 每子块 = 工具式头行(`❯ explore#N · model · elapsed`——D-R23b,dim ❯ + role#N 亮 +
37
+ * model/elapsed dim——与 C 工具头同排式;done/stopped 定格动词——D-R23c2)+ 折叠态
38
+ * tail 2 / 展开态全量窗口(60% 封顶 + 块内滚动——fold-block 公共组件);孙块递归
39
+ * (任意 inner 深度——D-R23d——每层独立折叠键)。折叠键 = `sub-{outerKey}/{innerPath}`
40
+ * (D-R23e——与既有 sub-{key} 同族——_foldToggle 命中映射复用 fold-block 通用通道——
41
+ * mouse.mjs 零改动)。行数配额在数据层(子块计入外层 500 行环——NFR)。
42
+ */
35
43
 
36
- /** §19.5 D-M8 子标行 dim 样式(最终渲染点注入——行对象单色外 wrap 无法表达行内
37
- * 双色):行首 `explore#1 · ` 子标转 gray(90m),其后内容恢复该行 kind 色
38
- * (tool=cyan / text=白 / think=默认 fg——dim 属性本就由 C.reason 施加,不受
39
- * fg 切换影响)。折叠 tail / dim 行不需调用(整行已 dim)。返回新行对象。 */
40
- export function styleSubLabelRow(row) {
41
- const content = String(row.text).replace(/^│ /, "")
42
- const m = content.match(SUB_LABEL_RE)
43
- if (!m) return row
44
- const restore = row.color === C.tool ? ansi.fg(6)
45
- : row.color === C.text ? ansi.fg(7)
46
- : ansi.fg(9) // 默认 fg(think 行 C.reason=dim 属性——90m 后切回默认即还原 dim 白)
47
- return {
48
- ...row,
49
- text: `│ ${ansi.gray}${m[1]}${restore}${content.slice(m[1].length)}`,
50
- }
44
+ /** 子块折叠键(D-R23e):sub-{outerKey}/{innerPath}。 */
45
+ export function subChildFoldKey(rootKey, innerPath) {
46
+ return `sub-${rootKey}/${innerPath}`
51
47
  }
52
48
 
49
+ /** 子块头行(D-R23b/c2)——返回带 _foldToggle 的行(可点击折叠/展开——fold-block 通道)。 */
50
+ function subChildHeadRow(rootKey, child, innerPath, cols) {
51
+ const foldKey = subChildFoldKey(rootKey, innerPath)
52
+ const end = child.done ? (child.doneAt ?? Date.now()) : Date.now()
53
+ const secs = Math.max(0, Math.floor((end - (child.started ?? end)) / 1000))
54
+ const verb = child.done ? (child.stopped ? `stopped ${secs}s` : `done ${secs}s`) : `${secs}s`
55
+ const raw = `❯ ${child.key}${child.model ? ` · ${child.model}` : ""} · ${verb}`
56
+ let text = sliceByWidth(raw, Math.max(1, cols - 2))
57
+ // dim 注入(截断后——自闭合:行末由打印端 reset 兜底;灰色段间 restore 恢复行基色)。
58
+ const restore = ansi.fg(6) // 行基色 C.tool(cyan)
59
+ if (text.startsWith("❯")) text = `${ansi.gray}❯${restore}${text.slice(1)}`
60
+ const sep = text.indexOf(" · ")
61
+ if (sep > 0) text = `${text.slice(0, sep)}${ansi.gray}${text.slice(sep)}`
62
+ return { text, color: C.tool, _foldToggle: foldKey }
63
+ }
64
+
65
+ /** 子块段行(head + 折叠 tail2 / 展开窗口)——递归孙块。root = 外层块(key 为折叠前缀源)。 */
66
+ export function renderSubChildSections(state, root, cols, maxRows) {
67
+ const out = []
68
+ const walk = (carrier, path) => {
69
+ for (const child of carrier.children ?? []) {
70
+ const innerPath = path ? `${path}/${child.key}` : child.key
71
+ out.push(subChildHeadRow(root.key, child, innerPath, cols))
72
+ const foldKey = subChildFoldKey(root.key, innerPath)
73
+ if (isExpanded(state, foldKey)) {
74
+ // 展开:全量时间线窗口化(per-kind 着色,60% 封顶 + 块内滚动——公共组件)
75
+ const body = renderBlockTimeline(child.blocks, cols)
76
+ out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: `${child.key} activity` }))
77
+ } else {
78
+ // 折叠:tail 2 非空行(最近活动)——dim(与既有外层折叠 tail 同型)
79
+ for (const line of foldTailLines(child.blocks, 2)) {
80
+ out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim, _skipDimFold: true })
81
+ }
82
+ }
83
+ walk(child, innerPath)
84
+ }
85
+ }
86
+ walk(root, "")
87
+ return out
88
+ }
53
89
  /**
54
90
  * 面板行构建(纯函数):顶部分隔线 `─` + 各运行中区块(折叠头 + tail 3 /
55
91
  * 展开全量)。maxRows = 终端行数(展开态 60% 封顶窗口化);省略 = 不封顶
@@ -71,14 +107,15 @@ export function renderSubagentPanel(state, cols, maxRows) {
71
107
  const icon = sub.approval ? "⏸" : sub.done ? "✓" : "▶"
72
108
  const elapsed = Math.floor(((sub.done ? (sub.doneAt ?? Date.now()) : Date.now()) - sub.started) / 1000)
73
109
  // §19.5 D-M7b ②: sync/async 显式头标(B 形态——不靠"没标推断")——async 由
74
- // ⟦ev⟧async 标记置位;sync 区块(无标记)显式标 sync。仅真实 subagent 角色
110
+ // ⟦ev⟧async 标记置位;sync 区块(无标记)显式标 sync。真实 subagent 角色
75
111
  // (escalate/consult/compress 等复用面板槽的条目无语义——非 spawn 角色豁免);
112
+ // §24 D-24b:role "advisor" 伪角色同面板(块/⏹/冻结复用——ruling ②-4 A)。
76
113
  // 冻结后保留(与 model 标识同生命周期——render-conversation frozenSubTaskLines
77
114
  // 同款 modePart)。**颜色后置注入**(code review 🔵#4):bracket 宽度预算用纯文
78
115
  // 本(dim ANSI 内嵌会被 sliceByWidth 截断在 restore 之前 → 行尾残留 dim)——
79
116
  // 截断后对完整存活的 mode word 单独套 dim + 恢复行色(自闭合——截断落在词内
80
117
  // 则 replace 不命中 → 无 ANSI 泄漏,词以行色显示)。
81
- const isSubRole = SUBAGENT_ROLES.includes(sub.role)
118
+ const isSubRole = SUBAGENT_ROLES.includes(sub.role) || sub.role === "advisor"
82
119
  // §20 D-SD3b waiting 块(sub.queued——排队 spawn 返回即建——未启动无 relay 流):
83
120
  // 括号状态词 = waiting(依赖/域冲突等位——detail 即原因)或 queued(槽满等位——
84
121
  // 状态区显示 position);不显示 sync/async 词(尚未启动——无 async 标记可言——
@@ -130,7 +167,7 @@ export function renderSubagentPanel(state, cols, maxRows) {
130
167
  // 🟡#1——glyph 在 cols−1、最右列留 margin——避免终端最末列点击不可靠/全角字形
131
168
  // 顶格被裁;命中区 = col ≥ _stopCol = cols−1——含 glyph 与其右 margin,左邻
132
169
  // padding 空格仍走折叠)。整行 ≤ cols:内容先按 cols−3 截断。
133
- if (!sub.done && sub.async === true && SUBAGENT_ROLES.includes(sub.role)) {
170
+ if (!sub.done && sub.async === true && (SUBAGENT_ROLES.includes(sub.role) || sub.role === "advisor")) {
134
171
  const cut = sliceByWidth(headText, Math.max(0, cols - 3))
135
172
  headText = cut + " ".repeat(Math.max(0, cols - 3 - stringWidth(cut)))
136
173
  line.text = `${headText} ${ansi.dim}⏹${ansi.reset} `
@@ -142,8 +179,7 @@ export function renderSubagentPanel(state, cols, maxRows) {
142
179
  out.push(line)
143
180
  if (isExpanded(state, foldKey)) {
144
181
  // 展开态:全量活动时间线(per-kind 着色,60% 屏封顶 + 块内滚动——公共组件)。
145
- // §19.5 D-M8:时间线行过 styleSubLabelRow——行首子标 dim、内容恢复 kind 色。
146
- const body = renderBlockTimeline(sub.blocks, cols).map(styleSubLabelRow)
182
+ const body = renderBlockTimeline(sub.blocks, cols)
147
183
  out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
148
184
  } else {
149
185
  // 折叠态:tail 3 非空 block 行(最近活动),dim。
@@ -151,6 +187,8 @@ export function renderSubagentPanel(state, cols, maxRows) {
151
187
  out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim })
152
188
  }
153
189
  }
190
+ // §27 R23:子块段(嵌套子代理——头 + tail2/展开窗口)随外层块渲染(面板与冻结共用)。
191
+ out.push(...renderSubChildSections(state, sub, cols, maxRows))
154
192
  }
155
193
  return out
156
194
  }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * suspension-drive.mjs — §17 挂起会话驱动器(2026-09-05 module-split:agent-turn.mjs
3
+ * 535 > 500 硬限——poolCounts + §17 挂起会话段 verbatim 迁入,语义零变;agent-turn.mjs
4
+ * re-import(runAgentTurn ↔ suspensionSession 函数级静态环——模块求值期无顶层调用,
5
+ * 环安全——session-slots ↔ session.mjs 同款先例)。
6
+ *
7
+ * §17(2026-09-02,AGENT-LOOP.md §17 D-S1..S9):回合尾后台池非空 → 不阻塞等待,
8
+ * 进入挂起态——输入放开(Enter = 新回合 / digest 中 Enter 排队 pendingInput)、
9
+ * settle 事件驱动 auto-turn 消化(手动档 organize-only / AUTO 档全语义)、池空 + 无
10
+ * 待处理输入 → 补发 done 冻结自然退出。状态机行表见 AGENT-LOOP.md §17 D-S9。
11
+ */
12
+
13
+ // 函数级静态环(2026-09-05):drive 的 digestTurn/用户回合经 runAgentTurn 递归进入
14
+ // agent-turn;agent-turn 的回合尾经 suspensionSession 进入本文件——互相 import。
15
+ import { runAgentTurn } from "./agent-turn.mjs"
16
+ import { freezeAllSubTasks, freezeReclaimDigestedBlocks } from "./subagent-blocks.mjs"
17
+ import { sweepToolBlocks } from "./tool-events.mjs"
18
+ import { logEvent } from "../log.mjs"
19
+ import { C } from "./ansi.mjs"
20
+ // ASYNC-RESULT-CONTAINER.md D1/D2:池 accessor(双池 absorb)+ pending 单容器停靠
21
+ import { getAsyncPool, parkAsyncPending } from "../agent-tools/async-settle.mjs"
22
+
23
+ // ── §24 D-24c(R15——2026-09-06)排队用户指令合并常量:单批 ≤8 条且合并注入
24
+ // ≤2000 字符(双端逐字一致——§22 D-Q3 常量先例);超限截批先行(余下下批——不丢
25
+ // 不截断单条);单条 >2000 字符直发不进批;/cmd 不进合并(逐条保序即时)。──
26
+ export const MAX_MERGE_ITEMS = 8
27
+ export const MAX_MERGE_CHARS = 2000
28
+
29
+ /** 合并注入形态(编号逐条——模型一次处理全部——设计措辞锚)。 */
30
+ export function formatMergedMessages(items) {
31
+ return `你排队了 ${items.length} 条消息:\n${items.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n——一次处理`
32
+ }
33
+
34
+ /**
35
+ * R15 攒批计划(纯函数——消费点共享同一事实源):按队列顺序产出动作序列。
36
+ * - { kind:"slash", text, count:1 }——/cmd 逐条即时执行(保序——不进合并缓冲);
37
+ * - { kind:"turn", text, count, merged }——文本批次:连续非 / 条目攒批(单批 ≤8 条;
38
+ * 合并注入(格式化后)≤2000 字符;count ≥2 → merged 编号注入;count === 1 → 原文
39
+ * 直发(含单条 >2000 字符——不进批);超限截批先行——余下条目留待下批(不丢)。
40
+ * 消费者每次取动作 [0] 并按 count 从源队列移除——下轮重新计划余项(边界幂等)。
41
+ */
42
+ export function planQueuedInput(items) {
43
+ const actions = []
44
+ const n = items.length
45
+ let i = 0
46
+ while (i < n) {
47
+ const cur = String(items[i])
48
+ if (cur.startsWith("/")) { actions.push({ kind: "slash", text: cur, count: 1 }); i++; continue }
49
+ let j = i
50
+ for (;;) {
51
+ if (j - i >= MAX_MERGE_ITEMS || j >= n) break
52
+ const it = String(items[j])
53
+ if (it.startsWith("/")) break
54
+ if (it.length > MAX_MERGE_CHARS) break // 单条超长——不进批(留作直发)
55
+ const cand = items.slice(i, j + 1)
56
+ if (cand.length > 1 && formatMergedMessages(cand).length > MAX_MERGE_CHARS) break // 批总长超限——截批
57
+ j++
58
+ }
59
+ // 超长条目堵头(内环一步未进)→ 该条直发(不进批——也不与前后合并)
60
+ if (j === i) { actions.push({ kind: "turn", text: String(items[i]), count: 1, merged: false }); i++; continue }
61
+ const taken = items.slice(i, j)
62
+ if (taken.length === 1) {
63
+ actions.push({ kind: "turn", text: String(taken[0]), count: 1, merged: false })
64
+ } else {
65
+ actions.push({ kind: "turn", text: formatMergedMessages(taken), count: taken.length, merged: true })
66
+ }
67
+ i = j
68
+ }
69
+ return actions
70
+ }
71
+
72
+ /** 后台池计数(LOGGING susp/digest 事件字段——pendingN/poolN)。
73
+ * poolN = _asyncSubagents + _asyncAdvisors(§24 D-24b advisor 池同面板计数——queued
74
+ * 条目同样在 map 内——2026-09-03 code review #2:不再 +queue.length 双计)。
75
+ * R17(§25 D-R17a/b):pendingN = pending 单容器 `_pendingAsyncResults` 条数(
76
+ * ASYNC-RESULT-CONTAINER.md D2——四族统一停靠——digest 驱动判据同单容器)。 */
77
+ export function poolCounts(agent) {
78
+ const map = agent?._asyncSubagents
79
+ const adv = agent?._asyncAdvisors
80
+ const running = (map ? [...map.values()].filter((e) => e.status === "running").length : 0)
81
+ + (adv ? [...adv.values()].filter((e) => e.status === "running").length : 0)
82
+ + consultRunningChildren(agent)
83
+ return {
84
+ poolN: (map?.size ?? 0) + (adv?.size ?? 0),
85
+ pendingN: pendingFamilyCount(agent),
86
+ runningN: running,
87
+ }
88
+ }
89
+
90
+ /** 运行中 consult children(会诊会话已跨回合——挂起判据/状态行计数同源)。 */
91
+ function consultRunningChildren(agent) {
92
+ let n = 0
93
+ for (const s of agent?._consultSessions?.values() ?? []) {
94
+ if (!s.stopped) n += Math.max(0, s.pending ?? 0)
95
+ }
96
+ return n
97
+ }
98
+
99
+ /** pending 单容器条数(§25 D-R17a 消费驱动判据——T-R17j——ASYNC-RESULT-CONTAINER.md
100
+ * D2:四族统一 `_pendingAsyncResults` +role,不再分族三容器)。 */
101
+ export function pendingFamilyCount(agent) {
102
+ return agent?._pendingAsyncResults?.length ?? 0
103
+ }
104
+
105
+ /** pending 单容器非空(digest 触发判据——D-S2/D-S9——§25 D-R17a)。 */
106
+ export function pendingFamiliesNonEmpty(agent) {
107
+ return pendingFamilyCount(agent) > 0
108
+ }
109
+
110
+ /** pending 单容器条目(freezeReclaimDigestedBlocks 的归属比对表——四族同容器)。 */
111
+ export function allPendingEntries(agent) {
112
+ return [...(agent?._pendingAsyncResults ?? [])]
113
+ }
114
+
115
+ // ─── §17 挂起会话(AGENT-LOOP.md §17 D-S2/D-S9 状态机行表)────────────────
116
+
117
+ /** 后台池存活判据(D-S2/F5 口径):running/queued 子代理或后台评审,或已 settle 未注入结果
118
+ * (pending 单容器非空 = D-S3 "未注入"),或 running consult 会话(会诊跨回合——
119
+ * §25 D-R17a 挂起活度钩子——consult 启动回合尾即入挂起态)。回合尾与每次轮末都用它
120
+ * 评估退出。§24 D-24b:_asyncAdvisors(后台评审池)与子代理池同判——评审飞行中挂起
121
+ * 会话必须存活。 */
122
+ export function poolLive(agent) {
123
+ const map = agent._asyncSubagents
124
+ const adv = agent._asyncAdvisors
125
+ return (map && map.size > 0) || (adv && adv.size > 0)
126
+ || pendingFamilyCount(agent) > 0
127
+ || consultRunningChildren(agent) > 0
128
+ }
129
+
130
+ /** D-S3 ③ 记账清扫:回合边界竞态落下的已 settle 项(settle 回调未及移交——发生在
131
+ * 回合刚结束、_suspended 尚未置位的窗口)补入 pending。幂等:回调已移交的条目已从
132
+ * map 删除并带 _inPending 标记,不会重复入列。
133
+ * ASYNC-RESULT-CONTAINER.md D2:pending 单容器(parkAsyncPending——四族统一停靠
134
+ * +role;escalate 不再走独立流)。 */
135
+ function sweepSettledToPending(agent) {
136
+ const maps = [getAsyncPool(agent, "subagent"), getAsyncPool(agent, "advisor")].filter((m) => m instanceof Map && m.size > 0)
137
+ if (maps.length === 0) return
138
+ for (const map of maps) {
139
+ for (const e of [...map.values()]) {
140
+ if (e.done && !e._inPending) {
141
+ parkAsyncPending(agent, e)
142
+ map.delete(String(e.id))
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ /** 等待下一次 settle(running 子代理 promise 完成)或用户唤醒(Enter 入队 / Ctrl+C /
149
+ * 会话 abort)。唤醒器经 state._suspWake 单槽注入;abort 监听兜底。 */
150
+ function waitForSettleOrWake(agent, state) {
151
+ return new Promise((resolve) => {
152
+ let finished = false
153
+ const cleanup = () => {
154
+ state._suspWake = null
155
+ const i = (agent._asyncWaiters ?? []).indexOf(w)
156
+ if (i >= 0) agent._asyncWaiters.splice(i, 1)
157
+ agent._sessionAbort?.signal.removeEventListener("abort", onAbort)
158
+ }
159
+ const finish = (why) => {
160
+ if (finished) return
161
+ finished = true
162
+ cleanup()
163
+ resolve(why)
164
+ }
165
+ const w = () => finish("settle")
166
+ const wake = () => finish("wake")
167
+ const onAbort = () => finish("aborted")
168
+ ;(agent._asyncWaiters ??= []).push(w)
169
+ state._suspWake = wake
170
+ if (agent._sessionAbort?.signal.aborted) { onAbort(); return }
171
+ agent._sessionAbort?.signal.addEventListener("abort", onAbort, { once: true })
172
+ })
173
+ }
174
+
175
+ /** 后台模式状态行文本(D-S8;17.5.4 #6 顺手对齐):"后台 N 子代理运行中 · M 完成待消化"
176
+ * ——"运行中" = running + queued(含后台评审——§24 D-24b 同面板计数)+ running consult
177
+ * children(R17——会诊跨回合);"完成待消化" = pending 任一族移交项 + §17.5 回合尾留池
178
+ * 的 settled 未消费项(挂起会话 sweep 前的可见窗口)。 */
179
+ function backgroundStatusText(agent) {
180
+ const map = agent._asyncSubagents
181
+ const adv = agent._asyncAdvisors
182
+ const running = (map ? [...map.values()].filter((e) => e.status === "running").length : 0)
183
+ + (adv ? [...adv.values()].filter((e) => e.status === "running").length : 0)
184
+ + consultRunningChildren(agent)
185
+ const queued = agent._asyncQueue?.length ?? 0
186
+ const pending = pendingFamilyCount(agent)
187
+ const doneInPool = (map ? [...map.values()].filter((e) => e.done).length : 0)
188
+ + (adv ? [...adv.values()].filter((e) => e.done).length : 0) // §17.5 留池未消费
189
+ const awaiting = pending + doneInPool
190
+ const active = running + queued
191
+ return active > 0 || awaiting > 0
192
+ ? `后台 ${active} 子代理运行中${awaiting ? ` · ${awaiting} 完成待消化` : ""}`
193
+ : "后台子代理收尾…"
194
+ }
195
+
196
+ /** 消化轮:系统驱动的 auto-turn(D-S6)。手动档不传权限/问答 handler(D-S7 装配
197
+ * 契约——denied 不弹面板、不悬挂);AUTO 档沿用普通回调(autoApprove 短路自动
198
+ * 执行)。_suspended 保持 true:消化中 settle 延迟冻结 + 移交 pending。R17:pending
199
+ * 计数/消化触发 = 任一 pending 族(T-R17j——consult/escalate 空闲 settle 也触发消化轮)。 */
200
+ async function digestTurn(ctx) {
201
+ const { agent, pushLine } = ctx
202
+ const manual = !agent.autoApprove
203
+ pushLine(manual
204
+ ? "[auto-turn: digesting finished subagent reports…]"
205
+ : "[auto-turn: continuing background work…]", C.dim)
206
+ const digestCtx = manual
207
+ ? { ...ctx, askPermission: null, askBatchPermission: null, askQuestion: null }
208
+ : ctx
209
+ // LOGGING:digest:* 事件(D-S9 消化轮边界——LOGGING.md F-L4 挂起态覆盖)
210
+ const d0 = Date.now()
211
+ const pend0 = pendingFamilyCount(agent)
212
+ logEvent("digest:start", { pendingN: pend0 })
213
+ await runAgentTurn(digestCtx, "", { autoTurn: true, skipSession: true })
214
+ logEvent("digest:end", { pendingN: pendingFamilyCount(agent), ms: Date.now() - d0 })
215
+ }
216
+
217
+ /**
218
+ * §17 挂起会话驱动(D-S9 行表;由 runAgentTurn 回合尾进入,池空自然退出):
219
+ * - suspension:池项 settle → 入 pending → 开 auto-turn(合并消化近邻 settle);
220
+ * 用户 Enter → pendingInput(digest 运行中排队,D-S5)——用户输入优先于 digest;
221
+ * - auto-turn:消化中 settle 不并发开新轮(单 runAgent 循环),轮末按 pending/池态
222
+ * 续开合并消化轮或回挂起;pendingInput 非空 → 以该消息开新回合(不触发新 digest);
223
+ * - §17.5.5:每次消化/会话内用户回合消费 pending 后 → freezeReclaimDigestedBlocks
224
+ * 逐条冻结回收(消化完成块不滞留面板——不等池空;settle 锚点 splice——digest 总览
225
+ * 文本之前——round1 #1 裁定);
226
+ * - 退出:池空 + pending 空 + 无待处理输入 → freezeAllSubTasks 补发冻结(仅兜底
227
+ * 未消化残项——17.5.5 块回收与池空解耦)→ idle。
228
+ * _suspended 翻转:会话期 true(settle 回调据此延迟冻结 + 移交 pending);会话内
229
+ * 用户回合执行期翻 false(普通回合语义:settle 即冻结 + 回合尾直注入 ①)。
230
+ */
231
+ export async function suspensionSession(ctx) {
232
+ const { agent, state, render, pushLine } = ctx
233
+ state.pendingInput ??= []
234
+ state._suspAborted = false
235
+ agent._suspended = true
236
+ agent._sessionSignal = agent._sessionAbort.signal // 会话内 spawn 的 children 共享(subagent.mjs)
237
+ state.suspended = true
238
+ state._suspPending = false // 偏差 #1:进入真正挂起态——标志只在释放窗口期有效(此后由 state.suspended 分流)
239
+ const suspTick = setInterval(() => {
240
+ if (state.suspended && !state.processing) {
241
+ state.status = backgroundStatusText(agent)
242
+ render()
243
+ }
244
+ }, 1000)
245
+ state.status = backgroundStatusText(agent)
246
+ render()
247
+ // LOGGING:susp:* 事件(挂起态进入/退出——F-L4;挂起期输入事件 v1 不记——refinement #1)
248
+ const s0 = Date.now()
249
+ logEvent("susp:enter", poolCounts(agent))
250
+ try {
251
+ while (!state._suspAborted && !agent._sessionAbort.signal.aborted) {
252
+ sweepSettledToPending(agent)
253
+ // 1. 用户输入优先(D-S5):pendingInput(digest 期排队)→ queue(处理期排队)。
254
+ // §24 D-24c(R15):空闲消费攒批合并——同源连续文本合成单条注入(编号逐条
255
+ // ——单批 ≤8 条 / ≤2000 字符;单条超长直发不进批;余下留待下批——不丢);
256
+ // /cmd 不进合并——逐条保序即时(位置 = 源内队列序)。每次取计划动作 [0]——
257
+ // 单回合消费后回环续取(新到消息自然并进下一回合——不设延迟窗口)。
258
+ const fromPending = state.pendingInput.length > 0
259
+ const srcLen = fromPending ? state.pendingInput.length : state.queue.length
260
+ if (srcLen > 0) {
261
+ const src = fromPending ? state.pendingInput : state.queue.map((q) => q.text)
262
+ const head = planQueuedInput(src)[0]
263
+ if (fromPending) state.pendingInput.splice(0, head.count)
264
+ else state.queue.splice(0, head.count)
265
+ if (head.kind === "slash") {
266
+ await ctx.handleSlash?.(head.text)
267
+ render()
268
+ continue
269
+ }
270
+ agent._suspended = false // 用户回合 = 普通回合语义(① 直注入 + settle 即冻结)
271
+ await runAgentTurn(ctx, head.text, { skipSession: true })
272
+ agent._suspended = true
273
+ // §17.5.5:该回合消化完 pending(run 首行注入)→ 逐条冻结回收驻留块
274
+ // (不等池空——settle 锚点 splice——digest 总览文本之前;与 digest 回收同规则)
275
+ // R17:回收比对 = pending 单容器(四族统一——ASYNC-RESULT-CONTAINER.md D2)
276
+ freezeReclaimDigestedBlocks(state, allPendingEntries(agent))
277
+ state.status = backgroundStatusText(agent)
278
+ continue
279
+ }
280
+ // 2. pending 任一族非空 → 合并消化轮(注入由 runAgent 首行统一完成——D-S3 单注入点;
281
+ // R17 判据推广——consult/escalate 族同样触发——T-R17j)
282
+ if (pendingFamiliesNonEmpty(agent)) {
283
+ await digestTurn(ctx)
284
+ // §17.5.5 实测修订(2026-09-03):digest 消化完成(pending 条目已注入)→ 逐条补发
285
+ // done 冻结回收——不等池空——块从面板移除进流(settle 锚点 splice 落位——digest
286
+ // 总览文本之前——round1 #1 裁定);池空 freeze-out 仅兜底未消化残项(挂起会话
287
+ // 结束统一清场)——块回收与池空解耦(T-H7/AC-H5)。
288
+ // 归属不变式:会话内任何 run 开始前 pinned 块(awaitingDigest)的条目必在 pending
289
+ // 任一族——run 消费后不在 pending 的 pinned 块即本 run 消化者(无需快照即精确归属)。
290
+ freezeReclaimDigestedBlocks(state, allPendingEntries(agent))
291
+ state.status = backgroundStatusText(agent)
292
+ continue
293
+ }
294
+ // 3. 池空(无 running/queued/未注入)→ 自然退出回 idle(补发冻结在 finally)
295
+ if (!poolLive(agent)) break
296
+ // 4. 等下一 settle / 用户唤醒(Enter 入队、Ctrl+C)
297
+ await waitForSettleOrWake(agent, state)
298
+ }
299
+ } finally {
300
+ clearInterval(suspTick)
301
+ const aborted = state._suspAborted || agent._sessionAbort.signal.aborted
302
+ if (aborted && (agent._asyncSubagents?.size ?? 0) > 0) logEvent("ev:stopped", { poolN: agent._asyncSubagents?.size ?? 0, where: "suspension-abort" })
303
+ logEvent("susp:exit", { ...poolCounts(agent), ms: Date.now() - s0, reason: aborted ? "aborted" : "idle" })
304
+ agent._suspended = false
305
+ agent._sessionSignal = null
306
+ agent._sessionAbort = null
307
+ agent._sessionAbortAll = null // 偏差 #3:会话期 controller 集合随句柄一并释放
308
+ state.suspended = false
309
+ state._suspWake = null
310
+ state.suspAbortArmed = false // round2 偏差 #4:会话退出即解除挂起中止武装(防跨会话粘滞)
311
+ if (aborted) {
312
+ // §15 abort 语义:清池不注入(用户显式停——不注入陈旧错误)
313
+ agent._asyncSubagents?.clear()
314
+ agent._asyncAdvisors?.clear()
315
+ agent._asyncQueue = []
316
+ // ASYNC-RESULT-CONTAINER.md D2:pending 单容器——中止清容器不注入陈旧结果(四族
317
+ // 统一一处清;consult 会话标记 stopped——settle 不入 digest 流——T-R17c)+
318
+ // children abort。
319
+ agent._pendingAsyncResults = []
320
+ const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
321
+ cleanupConsultSessions(agent)
322
+ // §17 round2 偏差 #2-CLI(code review round2 #2-CLI):中止时不静默丢弃挂起期
323
+ // 排队的用户消息——Enter 已清空输入框并入 pendingInput(用户视为已发送),
324
+ // 残余转回 state.queue({text} 条目,下个普通回合的队列循环续发——零丢失)
325
+ // + 提示行明示去向(不静默丢)。
326
+ const queuedN = state.pendingInput?.length ?? 0
327
+ if (queuedN > 0) {
328
+ state.queue.push(...state.pendingInput.splice(0).map((t) => ({ text: String(t) })))
329
+ pushLine(`[background work stopped — ${queuedN} queued message${queuedN > 1 ? "s" : ""} will run as a normal turn]`, C.warn)
330
+ }
331
+ } else {
332
+ // D-S3 ③ 兜底:退出前残余(极端竞态)直注入再退——结果零丢失(AC-S2;
333
+ // ASYNC-RESULT-CONTAINER.md D2:pending 单容器一处清——注入器按 role 分发
334
+ // (consult → injectConsultResult;其余 → injectAsyncResult——四族同容器)。
335
+ const { injectAsyncResult } = await import("../agent-tools/subagent.mjs")
336
+ const { injectConsultResult } = await import("../agent-tools/consult.mjs")
337
+ const residual = agent._pendingAsyncResults
338
+ if (residual?.length) {
339
+ for (const e of residual.splice(0)) {
340
+ if (e.role === "consult") await injectConsultResult(agent, e)
341
+ else await injectAsyncResult(agent, e)
342
+ }
343
+ }
344
+ }
345
+ // 补发 done 冻结:驻留面板的 awaiting-digest 块随池空冻结进流(T-S14)
346
+ freezeAllSubTasks(state)
347
+ sweepToolBlocks(state)
348
+ state.status = "Ready"
349
+ render()
350
+ }
351
+ }