thincoder 0.12.60 → 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 (152) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +27 -122
  4. package/package.json +3 -2
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +38 -17
  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 +36 -134
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +124 -329
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +3 -2
  17. package/src/agent/completion.mjs +1 -1
  18. package/src/agent/dispatch.mjs +47 -12
  19. package/src/agent/helpers.mjs +71 -13
  20. package/src/agent/record-results.mjs +13 -5
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +21 -6
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +26 -9
  26. package/src/agent-tools/advisor-async.mjs +70 -180
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +69 -20
  29. package/src/agent-tools/batch-segment.mjs +195 -0
  30. package/src/agent-tools/consult.mjs +23 -10
  31. package/src/agent-tools/design-token.mjs +14 -1
  32. package/src/agent-tools/digest-budget.mjs +76 -0
  33. package/src/agent-tools/eng.mjs +3 -3
  34. package/src/agent-tools/escalate-async.mjs +16 -13
  35. package/src/agent-tools/read-history.mjs +13 -3
  36. package/src/agent-tools/review-streak.mjs +93 -0
  37. package/src/agent-tools/settings.mjs +130 -17
  38. package/src/agent-tools/subagent-actions.mjs +15 -6
  39. package/src/agent-tools/subagent-async.mjs +66 -14
  40. package/src/agent-tools/subagent-panel.mjs +22 -15
  41. package/src/agent-tools/subagent-run.mjs +9 -6
  42. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  43. package/src/agent-tools/subagent-spawn.mjs +63 -16
  44. package/src/agent-tools/subagent.mjs +175 -49
  45. package/src/agent-tools/verify.mjs +13 -34
  46. package/src/agent-tools.mjs +1 -0
  47. package/src/agent.mjs +38 -21
  48. package/src/cli/distill-command.mjs +2 -2
  49. package/src/cli/make-agent.mjs +23 -7
  50. package/src/cli/memory-command.mjs +2 -2
  51. package/src/cli/setup-wizard.mjs +29 -9
  52. package/src/completions.mjs +114 -0
  53. package/src/config-migrate.mjs +70 -0
  54. package/src/config.mjs +132 -63
  55. package/src/conventions.mjs +223 -0
  56. package/src/crash-reports.mjs +7 -2
  57. package/src/expand-home.mjs +16 -0
  58. package/src/generate-title.mjs +1 -1
  59. package/src/hooks.mjs +7 -3
  60. package/src/memory/code-index.mjs +9 -3
  61. package/src/memory/code-sync.mjs +70 -31
  62. package/src/memory/delete.mjs +2 -0
  63. package/src/memory/docs.mjs +10 -6
  64. package/src/memory/file-walk.mjs +109 -0
  65. package/src/memory/schema.mjs +15 -3
  66. package/src/model-ref.mjs +66 -0
  67. package/src/model-specs.mjs +42 -8
  68. package/src/prompt-overlays.mjs +73 -16
  69. package/src/prompts/advisor-design.md +18 -8
  70. package/src/prompts/advisor-round1.md +7 -1
  71. package/src/prompts/advisor-round2.md +13 -2
  72. package/src/prompts/advisor-round3.md +13 -2
  73. package/src/prompts/common.md +115 -0
  74. package/src/prompts/consult-base.md +2 -0
  75. package/src/prompts/discipline-engineering.md +217 -0
  76. package/src/prompts/discipline-normal.md +179 -0
  77. package/src/prompts/persona-coder.md +21 -0
  78. package/src/prompts/persona-eng-coder.md +37 -0
  79. package/src/prompts/persona-eng-designer.md +55 -0
  80. package/src/prompts/persona-engineering.md +54 -0
  81. package/src/prompts/persona-explore.md +15 -0
  82. package/src/prompts/persona-normal.md +27 -0
  83. package/src/prompts/persona-plan.md +26 -0
  84. package/src/provider/anthropic.mjs +4 -4
  85. package/src/provider/core.mjs +13 -32
  86. package/src/provider/errors.mjs +26 -1
  87. package/src/provider/google.mjs +5 -6
  88. package/src/provider/index.mjs +2 -1
  89. package/src/provider/list-models.mjs +93 -0
  90. package/src/provider/rate.mjs +2 -1
  91. package/src/provider/responses.mjs +5 -3
  92. package/src/provider/sse.mjs +3 -4
  93. package/src/proxy.mjs +9 -14
  94. package/src/session-slots.mjs +7 -2
  95. package/src/session.mjs +59 -38
  96. package/src/token-ttl.mjs +2 -1
  97. package/src/tools/{system.mjs → bash.mjs} +6 -243
  98. package/src/tools/file.mjs +30 -10
  99. package/src/tools/git.md +1 -1
  100. package/src/tools/git.mjs +15 -34
  101. package/src/tools/index.mjs +4 -2
  102. package/src/tools/ops.mjs +20 -7
  103. package/src/tools/question.md +1 -0
  104. package/src/tools/question.mjs +26 -0
  105. package/src/tools/read.md +1 -1
  106. package/src/tools/read_image.md +1 -1
  107. package/src/tools/search.mjs +236 -0
  108. package/src/tui/agent-turn.mjs +32 -13
  109. package/src/tui/ansi.mjs +2 -0
  110. package/src/tui/clipboard.mjs +7 -1
  111. package/src/tui/cmd-advisor.mjs +3 -2
  112. package/src/tui/cmd-config.mjs +108 -37
  113. package/src/tui/cmd-eng.mjs +11 -27
  114. package/src/tui/cmd-exit.mjs +6 -8
  115. package/src/tui/cmd-model.mjs +14 -12
  116. package/src/tui/cmd-reindex.mjs +7 -0
  117. package/src/tui/cmd-submodel.mjs +8 -5
  118. package/src/tui/cmd-undo.mjs +4 -3
  119. package/src/tui/index.mjs +38 -37
  120. package/src/tui/key-handler.mjs +61 -17
  121. package/src/tui/key-modes.mjs +86 -8
  122. package/src/tui/layout.mjs +18 -10
  123. package/src/tui/model-catalog.mjs +89 -0
  124. package/src/tui/model-picker.mjs +498 -0
  125. package/src/tui/mouse.mjs +47 -10
  126. package/src/tui/pickers.mjs +28 -410
  127. package/src/tui/render-frame.mjs +32 -16
  128. package/src/tui/render-loop.mjs +2 -0
  129. package/src/tui/render-segments.mjs +5 -19
  130. package/src/tui/render.mjs +37 -5
  131. package/src/tui/slash-commands.mjs +2 -2
  132. package/src/tui/subagent-blocks.mjs +69 -90
  133. package/src/tui/subagent-children.mjs +50 -64
  134. package/src/tui/subagent-freeze.mjs +40 -43
  135. package/src/tui/subagent-panel.mjs +21 -66
  136. package/src/tui/suspension-drive.mjs +28 -82
  137. package/src/tui/tool-args.mjs +4 -2
  138. package/src/tui/tool-display.mjs +4 -3
  139. package/src/tui/tool-events.mjs +23 -16
  140. package/src/tui/tui-lifecycle.mjs +18 -5
  141. package/src/tui/wizard.mjs +52 -18
  142. package/src/tui/wrapped-spawn.mjs +38 -0
  143. package/src/prompts/coder.md +0 -13
  144. package/src/prompts/discipline.md +0 -84
  145. package/src/prompts/eng-coder.md +0 -19
  146. package/src/prompts/engineering-sub.md +0 -14
  147. package/src/prompts/engineering.md +0 -87
  148. package/src/prompts/explore.md +0 -12
  149. package/src/prompts/main.md +0 -34
  150. package/src/prompts/methodology-template.md +0 -38
  151. package/src/prompts/plan.md +0 -9
  152. package/src/prompts/system.md +0 -44
@@ -1,82 +1,74 @@
1
1
  /**
2
- * subagent-children.mjs — 嵌套子代理子块载体数据层(AGENT-LOOP.md §27 R23 D-R23a/c2/NFR)。
2
+ * subagent-children.mjs — 嵌套子代理子块载体数据层(显示契约:docs/design/TUI.md §6
3
+ * 「内层活动并入外层流(SUBAGENT-TAIL)」节——2026-09-11 批)。
3
4
  *
4
5
  * 子块 = 外层 state.subTasks 块内的嵌套载体(外层 .children[]——每层一子块——D-R23d):
5
6
  * carrier = { key(父内段标 role#N), role, model, started, done, doneAt, stopped,
6
7
  * blocks, currentTool, toolArgs, approval, lastError, dropped,
7
- * blockEpoch, children: [], _lineCount }——与外层块同形(渲染复用
8
- * renderBlockTimeline/renderExpandedBlock 公共组件)。
9
- * 内容/工具/模型全部归属子块(D-R23b——不再混外层 blocks——T-R23a.2 输出全在子块)。
8
+ * blockEpoch, children: [], _lineCount }
9
+ * SUBAGENT-TAIL(D-ST1)后内层 relay 行**不再存子块**——append 目标上移外层块 blocks
10
+ * (单存储单账本;子块 blocks 恒空)。子块载体降为**守护元数据**,职责 = ① done 后
11
+ * 迟到 chunk 丢弃(F2 语义不回退)② 外层冻结时未收尾子块不悬空(closeOpenSubChildren
12
+ * ——写路径保留,无正读者)③ 内层工具 fresh 判别(leaf.currentTool)。
10
13
  *
11
- * N2 共享配额(R23 NFR——评审 #6):子块行数计入**外层** 500 行环形上限——树级 trim
12
- * (子块批量输出先丢、外层叙述保留优先——见 trimSubTree)——子块不独立扩容——面板
13
- * 不无限增长。
14
+ * N2 单环配额(D-ST4):全部内容行(含内层并入行)就地计入外层 500 **显示行**环——
15
+ * trim = 单载体最旧先行(原树级 trim「子块先丢 / done 豁免」随子块内容层退役)。
16
+ * 省略计数真值(N6——D-ST5):`…(已省略 N 行)` 的 N 只随内容移除增长;标记自身
17
+ * 不占额度、不被丢弃、不计入 N。
14
18
  */
15
19
  export const SUB_BLOCK_LINE_LIMIT = 500
16
20
 
17
- const countBlockLines = (text) => text.split("\n").length
18
-
19
- /** 载体树总行数(根自身 + 全部子孙块)——共享配额记账。 */
20
- export function carrierTreeLines(carrier) {
21
- let n = carrier._lineCount ?? 0
22
- for (const c of carrier.children ?? []) n += carrierTreeLines(c)
23
- return n
21
+ /** 显示行计数(N6 口径):块尾 `\n` 的空元素不计(行结束符非空行)。 */
22
+ const countBlockLines = (text) => {
23
+ const lines = String(text ?? "").split("\n")
24
+ return lines[lines.length - 1] === "" ? lines.length - 1 : lines.length
24
25
  }
25
26
 
26
- /** 载体级丢行(原 trimSubBlocks 逐块丢弃数学——整块 shift、末块按行裁切);返回实际
27
- * 丢弃行数。每载体独立 dropped 计数 + meta 省略标记(同既有 trimSubBlocks 语义)。 */
27
+ /** 省略标记块判定(trim 产物——不占额度/不可被丢;N6)。 */
28
+ const isTrimMarker = (b) => b?.kind === "meta" && b._trimMarker === true
29
+
30
+ /** 载体级丢行(D-ST4 单环最旧先行):从头部内容块 FIFO 丢行;省略标记跳过(不占额度/
31
+ * 不被丢弃/不计入 N——N6)。返回实际丢弃行数(= N 增量——无幽灵行)。 */
28
32
  function dropCarrierLines(carrier, want) {
29
- if (want <= 0 || carrier.blocks.length === 0) return 0
33
+ if (want <= 0) return 0
30
34
  let dropped = 0
31
- while (want > dropped && carrier.blocks.length > 0) {
32
- const first = carrier.blocks[0]
35
+ while (want > dropped) {
36
+ const idx = carrier.blocks.findIndex((b) => !isTrimMarker(b))
37
+ if (idx === -1) break // 仅余省略标记(内容耗尽)——不再产生幽灵行
38
+ const first = carrier.blocks[idx]
33
39
  const lines = countBlockLines(first.text)
40
+ if (lines <= 0) { carrier.blocks.splice(idx, 1); continue } // 空块无显示行:移除不计数
34
41
  const take = Math.min(lines, want - dropped)
35
42
  if (take >= lines) {
36
- carrier.blocks.shift()
43
+ carrier.blocks.splice(idx, 1)
37
44
  dropped += lines
38
45
  } else {
39
46
  first.text = first.text.split("\n").slice(take).join("\n")
40
47
  dropped += take
41
48
  }
42
49
  }
43
- carrier._lineCount = (carrier._lineCount ?? 0) - dropped
50
+ carrier._lineCount = Math.max(0, (carrier._lineCount ?? 0) - dropped)
44
51
  carrier.dropped = (carrier.dropped ?? 0) + dropped
45
- const marker = `…(已省略 ${carrier.dropped} 行)`
46
- const first = carrier.blocks[0]
47
- if (first && first.kind === "meta") first.text = marker
48
- else { carrier.blocks.unshift({ kind: "meta", text: marker }); carrier._lineCount = (carrier._lineCount ?? 0) + 1 }
52
+ if (dropped > 0) {
53
+ const marker = `…(已省略 ${carrier.dropped} 行)`
54
+ const head = carrier.blocks[0]
55
+ if (head && isTrimMarker(head)) head.text = marker // 既有标记原位更新(不重复 unshift)
56
+ else carrier.blocks.unshift({ kind: "meta", text: marker, _trimMarker: true }) // 标记不占额度(N6)
57
+ }
49
58
  return dropped
50
59
  }
51
60
 
52
- /** R23 NFR:树级配额 trim——总行(根 + 子块树)超限丢行。丢序 = **子树递归先于
53
- * 根载体**(后序:叶子/子块先、根最后)——子块承载批量输出(explore 工具流),外层
54
- * 叙述行(折叠头 tail/状态区的展示源)保留优先;每载体内部仍最旧行先行(dropCarrierLines
55
- * 逐块 FIFO)。单载体(无子块)情形与既有 trimSubBlocks 行为逐字同效。
56
- * §27.1 F1(2026-09-07 三缺陷修复批——缺陷③主修):丢行遍历**跳过 done 子块**——
57
- * done = 定格快照——配额压力不再蚕食已定格子块行,丢行落点移到外层自身行(live
58
- * 块「已省略 N 行」增长语义正确)。边界(评审 #6):live 行耗尽(全树皆 done)→
59
- * 允许超限(定格快照优先于配额——N2 上限对 done 存量 best-effort)。 */
60
- export function trimSubTree(root) {
61
- let over = carrierTreeLines(root) - SUB_BLOCK_LINE_LIMIT
62
- if (over <= 0) return
63
- const walk = (carrier) => {
64
- for (const c of carrier.children ?? []) {
65
- if (over <= 0) return
66
- if (c.done) continue // §27.1 F1: done 子块 = 定格快照——丢行遍历跳过(豁免)
67
- walk(c)
68
- }
69
- if (over <= 0) return
70
- over -= dropCarrierLines(carrier, over)
71
- }
72
- walk(root)
61
+ /** 单载体 trim(原树级 trimSubTree 收窄——D-ST4):超 500 显示行 最旧先行丢行。 */
62
+ function trimSubCarrier(carrier) {
63
+ const over = (carrier._lineCount ?? 0) - SUB_BLOCK_LINE_LIMIT
64
+ if (over > 0) dropCarrierLines(carrier, over)
73
65
  }
74
66
 
75
- /** kind 合并追加(载体级)——既有 appendSubBlock 数学(净增行记账——P1)。 */
67
+ /** kind 合并追加(载体级)——净增行记账(显示行口径;省略标记不参与合并——N6)。 */
76
68
  function pushBlock(carrier, kind, text, fresh) {
77
69
  if (!text) return
78
70
  const last = carrier.blocks.at(-1)
79
- if (!fresh && last && last.kind === kind) {
71
+ if (!fresh && last && last.kind === kind && !isTrimMarker(last)) {
80
72
  const before = countBlockLines(last.text)
81
73
  last.text += text
82
74
  carrier._lineCount = (carrier._lineCount ?? 0) + countBlockLines(last.text) - before
@@ -87,27 +79,18 @@ function pushBlock(carrier, kind, text, fresh) {
87
79
  carrier.blockEpoch = (carrier.blockEpoch ?? 0) + 1
88
80
  }
89
81
 
90
- /** 追加到根载体(外层块/压缩面板——无父载体者自身即配额树根)。树级 trim——
91
- * 无子块时与既有 trimSubBlocks 同效(N2 既有测试口径不变)。 */
82
+ /** 追加到块载体(外层块/压缩面板——自身即配额环):单载体 trim——无子块时与既有
83
+ * trimSubBlocks 同效(N2 既有测试口径不变)。SUBAGENT-TAIL:内层行经 routeSub* 也
84
+ * 走本函数(append 目标上移——单一配额环)。 */
92
85
  export function appendSubBlock(sub, kind, text, { fresh = false } = {}) {
93
86
  if (!text) return
94
87
  pushBlock(sub, kind, text, fresh)
95
- trimSubTree(sub)
96
- }
97
-
98
- /** R23:追加到子块载体——行数计入根(外层)树配额(NFR——同配额不独立扩容)。
99
- * §27.1 F2(2026-09-07):child.done 守卫——done 后完全定格,迟到 chunk 丢弃
100
- * (与 §7.2 D4 完成态冻结语义统一——数据层 tombstone 拒绝)。 */
101
- export function appendSubChild(root, child, kind, text, { fresh = false } = {}) {
102
- if (!text) return
103
- if (child.done) return
104
- pushBlock(child, kind, text, fresh)
105
- root.blockEpoch = (root.blockEpoch ?? 0) + 1
106
- trimSubTree(root)
88
+ trimSubCarrier(sub)
107
89
  }
108
90
 
109
91
  /** R23 D-R23a:子块载体创建/定位——按段(role#N)在父载体 children 查找,缺失即建
110
- * (每层一子块——任意 inner 深度同路径——D-R23d)。@returns 子块载体 */
92
+ * (每层一子块——任意 inner 深度同路径——D-R23d)。SUBAGENT-TAIL 后子块只承载守护
93
+ * 元数据(done/currentTool/children——内容行归外层 blocks)。@returns 子块载体 */
111
94
  export function ensureSubChild(carrier, seg) {
112
95
  carrier.children ??= []
113
96
  let c = carrier.children.find((x) => x.key === seg)
@@ -142,6 +125,7 @@ export function descendSubChild(root, inner, { create = true } = {}) {
142
125
 
143
126
  /** R23 D-R23c2 子块完成定格(内层 ⟦ev⟧done/stopped → 子块尾定格——不落 preview、不冻结
144
127
  * 外层);stopped = interrupted(外层 abort 传播/内层错误收尾——T-R23a.3 不悬空)。
128
+ * SUBAGENT-TAIL 后 done 为 F2 正读者(迟到 chunk 丢弃守卫);currentTool 为 fresh 判别源。
145
129
  * 外层块头 currentTool 若指向该子块(innerPath 前缀——评审 #8 全路径现状保持)→ 清空
146
130
  * (子块已定格——工具不再是 current)。 */
147
131
  export function closeSubChild(root, child, stopped, innerPath) {
@@ -158,7 +142,9 @@ export function closeSubChild(root, child, stopped, innerPath) {
158
142
  }
159
143
 
160
144
  /** R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层未收尾(外层 abort/中断)→
161
- * 内层子块随外层冻结时定格 stopped——不悬空。freezeSubTaskLines 冻结前调用。 */
145
+ * 内层子块随外层冻结时定格 stopped——不悬空。SUBAGENT-TAIL:② 语义照旧保留(防御性
146
+ * 写路径——定格后无正读者、不设断言;见 docs/design/TUI.md §6 守护字段类目 A)。
147
+ * freezeSubTaskLines 冻结前调用。 */
162
148
  export function closeOpenSubChildren(sub) {
163
149
  const walk = (carrier) => {
164
150
  for (const c of carrier.children ?? []) {
@@ -1,51 +1,52 @@
1
1
  /**
2
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 等)。
3
+ * subagent-blocks.mjs 625 > 500 硬限——§19.6 面板现算 computePanelBlocks + finish/
4
+ * freeze 家族迁入;subagent-blocks.mjs import 回(routeSub* 与 compression panel
5
+ * 调用点)+ re-export 保外部 import 面(index.mjs 等)。
6
+ * CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):§19.6 面板**手工镜像退役**(镜像读写
7
+ * 全删)——改读时现算 computePanelBlocks(state):四字段 key/
8
+ * role/status/startedAt 均为 state.subTasks 活值纯推导(评审 #2 确证),读时现算
9
+ * 与 sync 时刻镜像等价——双账本结构性漂移根治(单账本)。完成/冻结族不再逐点刷镜
10
+ * (无镜像可刷——调用点全清)。
6
11
  */
7
12
 
8
13
  import { C } from "./ansi.mjs"
9
14
  import { closeOpenSubChildren } from "./subagent-children.mjs"
10
15
 
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 侧判定)。
16
+ // ─── §19.6 D-P1 面板视图(subagent panel 检查工具)───
17
+ // CLI-ACTIVITY-DEBLOAT F-3:面板区块列表由消费面**读时现算**(action:"panel"
18
+ // ctx.state = agent._tuiState(index.mjs startTUI 装配)取 TUI state——subTasks
19
+ // 活值纯推导——视图与用户所见一致(同一数据源单账本))。未挂载(headless/VSC/
20
+ // 子代理——agent._tuiState 缺省)→ null = 无面板——消费面降级池视图/freeze 报
21
+ // 不可用(T-P5 语义不变)。status 三态映射(纯函数):done+awaitingDigest →
22
+ // "awaitingDigest";done "done";queued(waiting/等位未启动)→ "queued";
23
+ // 其余 → "running"(§20 D-SD3b queued 态入镜口径保留)。
18
24
 
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 态入镜(镜像与面板
25
+ /** 现算面板块列表(state.subTasks 活值纯推导——无时点快照语义)。 */
26
+ export function computePanelBlocks(state) {
27
+ const subs = state?.subTasks
28
+ if (!subs) return null // 未挂载(headless/mock 无 TUI state)——无面板
29
+ const blocks = []
30
+ for (const sub of Object.values(subs)) {
31
+ // §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(视图与面板
26
32
  // 所见一致——action:'panel' 视图 + freeze 门控读此态)。
27
33
  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() })
34
+ blocks.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
29
35
  }
30
- agent._panelSnapshot = snap
36
+ return blocks
31
37
  }
32
38
 
33
- /** Mark the earliest running child of the given role(s) as done (✓ header, frozen elapsed). */
39
+ /** §7.2.3 完成冻结——CLI-ACTIVITY-DEBLOAT F-2(2026-09-10)收窄:本函数是历史启发式
40
+ * 入口("最早 started"角色匹配——§15 async 化后的 7.2.3.1 实测误冻源:async eng-coder
41
+ * 先启动时 explore 完成会误冻其块)。评审 #4 路线定死:签名保留、函数体收窄为精确
42
+ * 匹配校验——本签名无 key 参数,无从精确归属 → 恒 no-op 返 null,不再猜任何块
43
+ * (宁可 no-op 不误冻——被否决备选"保留兜底加告警日志":冻错块是错动作,日志救不回)。
44
+ * finishSubTaskKey 为唯一完成路径;roles/lastError 仅签名兼容保留(老调用面 import
45
+ * 不断——误冻面归零,无 key 窗口的块由回合尾 freezeAllSubTasks 兜底清场)。
46
+ * @returns {null} 恒 null(无精确 key 命中面——无从归属不归属)。 */
34
47
  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 状态变更刷镜
48
+ void state; void roles; void lastError // 收窄后零副作用——参数仅签名兼容
49
+ return null
49
50
  }
50
51
 
51
52
  /** §7.2.3 sync spawn 完成精确冻结(2026-09-03):按 relay key 精确 settle。finishSubTask
@@ -64,20 +65,20 @@ export function finishSubTaskKey(state, key, lastError = null) {
64
65
  sub.approval = null
65
66
  if (lastError) sub.lastError = lastError
66
67
  sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
67
- syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
68
68
  return sub
69
69
  }
70
70
 
71
71
  /** 冻结完成/中断区块进 state.lines(§7.2 D4——_frozenSubTask 载体行,渲染端
72
72
  * render-conversation 识别;折叠交互 key = sub-${key} 与运行面板同源跨冻结延续)。
73
73
  * §27 R23 D-R23c2 收尾语义(T-R23c.2a):外层先冻结而内层子块未收尾(外层 abort/
74
- * 中断)→ 子块随外层冻结定格 stopped(closeOpenSubChildren——不悬空)。
74
+ * 中断)→ 子块随外层冻结定格 stopped(closeOpenSubChildren——不悬空)。SUBAGENT-TAIL:
75
+ * ② 语义照旧(防御性保留——无正读者、不设断言;显示契约 docs/design/TUI.md §6)。
75
76
  * 锚点插入(2026-09-03 修复轮):settled 块带 _freezeAt(settle 时刻流位置)——
76
77
  * splice 落位使挂起期补发冻结块位于其 digest 总览文本之前;无锚点尾推不变;
77
78
  * 多锚点批量冻结按降序(绝对位置 splice——先插小锚点会移走大锚点目标)。 */
78
79
  export function freezeSubTaskLines(state, sub) {
79
80
  if (!sub) return
80
- closeOpenSubChildren(sub) // R23 D-R23c2——开子块随外层冻结定格 stopped
81
+ closeOpenSubChildren(sub) // R23 D-R23c2——开子块随外层冻结定格 stopped(SUBAGENT-TAIL:② 防御性保留)
81
82
  state._frozenSubKeys ??= new Set()
82
83
  state._frozenSubKeys.add(sub.key)
83
84
  sub.done = true
@@ -107,11 +108,10 @@ export function freezeDoneSubTasks(state) {
107
108
  freezeSubTaskLines(state, sub)
108
109
  delete state.subTasks[sub.key]
109
110
  }
110
- syncPanelSnapshot(state) // §19.6 D-P1: 删除后刷镜(块移出面板)
111
111
  }
112
112
 
113
- /** 按角色整组标记 done——consult N 并行 children 的会话级 settle(单发
114
- * finishSubTask 只结最早一个——consult 残项 2026-08-30)。 */
113
+ /** 按角色整组标记 done——consult N 并行 children 的会话级 settle(2026-08-30;
114
+ * F-2 收窄后 finishSubTask 已退役 no-op——本函数为唯一按角色完成面)。 */
115
115
  export function finishSubTasksByRole(state, roles, lastError = null) {
116
116
  state.subTasks ??= {}
117
117
  const roleSet = new Set(Array.isArray(roles) ? roles : [roles])
@@ -125,7 +125,6 @@ export function finishSubTasksByRole(state, roles, lastError = null) {
125
125
  sub.blockEpoch = (sub.blockEpoch ?? 0) + 1
126
126
  }
127
127
  }
128
- syncPanelSnapshot(state) // §19.6 D-P1: done 状态变更刷镜
129
128
  }
130
129
 
131
130
  /** 回合尾/挂起退出清扫(runAgentTurn finally / suspensionSession finally):冻结全部
@@ -146,7 +145,6 @@ export function freezeAllSubTasks(state) {
146
145
  freezeSubTaskLines(state, sub)
147
146
  delete state.subTasks[sub.key]
148
147
  }
149
- syncPanelSnapshot(state) // §19.6 D-P1: 冻结回收后刷镜(块移出面板)
150
148
  }
151
149
 
152
150
  /** §17.5.5 消化完成逐条冻结回收(2026-09-03 实测修订 + round1 #1 位置裁定):digest/会话内
@@ -167,6 +165,5 @@ export function freezeReclaimDigestedBlocks(state, pendingList) {
167
165
  freezeSubTaskLines(state, sub) // splice 落 settle 锚点(digest 总览文本之前)
168
166
  delete state.subTasks[sub.key]
169
167
  }
170
- syncPanelSnapshot(state) // §19.6 D-P1: 逐条回收后刷镜(块移出面板)
171
168
  return targets.length
172
169
  }
@@ -12,8 +12,10 @@
12
12
  *
13
13
  * §19.5 D-M7 ⏹:运行中(非 done)折叠头右缘停止标记(dim,仅折叠头)——点击 =
14
14
  * cancel(mouse.mjs 列级命中 _stopCol——不触发折叠翻转)。
15
- * §27 R23(supersede §19.5 D-M8 子标方案):嵌套子代理渲染为子块段(renderSubChildSections
16
- * ——子块头 + 独立折叠键——面板与冻结渲染共用——styleSubLabelRow 已随子标机制退役)。
15
+ * SUBAGENT-TAIL(supersede §27 R23 子块段方案;显示契约 docs/design/TUI.md §6):嵌套
16
+ * 子代理内容行并入本块 blocks(数据层——append 目标上移)——面板与冻结渲染只读
17
+ * sub.blocks(无子块段/无子块头/无每层折叠键);折叠键只剩 `sub-${key}`(面板 ↔
18
+ * 冻结同键——D5 无缝衔接不变)。
17
19
  *
18
20
  * 中立模块(D1 评审 #6):layout.mjs 调 renderSubagentPanel 预计算面板高度
19
21
  * (subagentLines → subagentH),render-frame.mjs 直接 put 预计算行(不重复
@@ -31,61 +33,6 @@ import { sliceByWidth, stringWidth } from "./render.mjs"
31
33
  import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } from "./fold-block.mjs"
32
34
  import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
33
35
 
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
- */
43
-
44
- /** 子块折叠键(D-R23e):sub-{outerKey}/{innerPath}。 */
45
- export function subChildFoldKey(rootKey, innerPath) {
46
- return `sub-${rootKey}/${innerPath}`
47
- }
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
- }
89
36
  /**
90
37
  * 面板行构建(纯函数):顶部分隔线 `─` + 各运行中区块(折叠头 + tail 3 /
91
38
  * 展开全量)。maxRows = 终端行数(展开态 60% 封顶窗口化);省略 = 不封顶
@@ -109,7 +56,7 @@ export function renderSubagentPanel(state, cols, maxRows) {
109
56
  // §19.5 D-M7b ②: sync/async 显式头标(B 形态——不靠"没标推断")——async 由
110
57
  // ⟦ev⟧async 标记置位;sync 区块(无标记)显式标 sync。真实 subagent 角色
111
58
  // (escalate/consult/compress 等复用面板槽的条目无语义——非 spawn 角色豁免);
112
- // §24 D-24b:role "advisor" 伪角色同面板(块/⏹/冻结复用——ruling ②-4 A)。
59
+ // §11.2 D-24b:role "advisor" 伪角色同面板(块/⏹/冻结复用——ruling ②-4 A)。
113
60
  // 冻结后保留(与 model 标识同生命周期——render-conversation frozenSubTaskLines
114
61
  // 同款 modePart)。**颜色后置注入**(code review 🔵#4):bracket 宽度预算用纯文
115
62
  // 本(dim ANSI 内嵌会被 sliceByWidth 截断在 restore 之前 → 行尾残留 dim)——
@@ -119,7 +66,9 @@ export function renderSubagentPanel(state, cols, maxRows) {
119
66
  // §20 D-SD3b waiting 块(sub.queued——排队 spawn 返回即建——未启动无 relay 流):
120
67
  // 括号状态词 = waiting(依赖/域冲突等位——detail 即原因)或 queued(槽满等位——
121
68
  // 状态区显示 position);不显示 sync/async 词(尚未启动——无 async 标记可言——
122
- // sync 词会误导:async spawn 排队的块不是 sync);⏹ 门控不变(async 启动后才置)。
69
+ // sync 词会误导:async spawn 排队的块不是 sync);⏹ 门控(QUEUED-VISIBILITY F-2——
70
+ // 2026-09-09):queued 块头即置取消 ⏹(slot/wait/depc 三态——用户可撤销排队决策——
71
+ // 点击 = 出队——覆盖 SESSION-ACTIVITY-REVISED F-6“接受无取消”旧裁)。
123
72
  const queued = sub.queued ?? null
124
73
  const statusWord = queued ? (queued.kind === "slot" ? "queued" : "waiting") : null
125
74
  const modeWord = isSubRole && !queued ? (sub.async === true ? "async" : "sync") : null
@@ -160,14 +109,22 @@ export function renderSubagentPanel(state, cols, maxRows) {
160
109
  : ""
161
110
  let headText = `${bracket} ${sliceByWidth(statePart + argSummary, Math.max(0, cols - 2 - bracketWidth))}`
162
111
  const line = { color: C.tool, _foldToggle: foldKey }
163
- // §19.5 D-M7 ⏹ + D-M7b ③ 门控:⏹ 只对 async 区块(running && SUBAGENT_ROLES &&
164
- // sub.async——sync 区块无 ⏹——杜绝"可见但不可中止"误导——用户裁定 B 形态)。
165
- // done/awaitingDigest/压缩面板(role compress)/consult 无标记(点击只对池内
166
- // async 子代理有意义):dim 停止标记钉在折叠头右缘**内收一列**(code review
112
+ // §19.5 D-M7 ⏹ + D-M7b ③ 门控 + SYNC-CANCEL F3(2026-09-09):⏹ 只对 **live 可中止**
113
+ // 区块——async(running && sub.async——⟦ev⟧async 置位)∪ **sync registry live**
114
+ // (state._agent?._syncChildAborts?.has(sub.key)——阻塞 spawn 运行期注册——成功/折叠/
115
+ // 整回合停后 finally 注销——门控自动放下)——杜绝"可见但不可中止"误导。真实 subagent
116
+ // 角色(SUBAGENT_ROLES)+ advisor;escalate/consult/compress 无 registry 无 ⏹。
117
+ // **headless/测试无 _agent → sync 不钉 ⏹**(registry miss——零回归——AC6);async
118
+ // 区块不依赖 _agent(sub.async 置位即钉)。done/awaitingDigest 无标记(点击只对池内/
119
+ // registry 内 live 子代理有意义):dim 停止标记钉在折叠头右缘**内收一列**(code review
167
120
  // 🟡#1——glyph 在 cols−1、最右列留 margin——避免终端最末列点击不可靠/全角字形
168
121
  // 顶格被裁;命中区 = col ≥ _stopCol = cols−1——含 glyph 与其右 margin,左邻
169
122
  // padding 空格仍走折叠)。整行 ≤ cols:内容先按 cols−3 截断。
170
- if (!sub.done && sub.async === true && (SUBAGENT_ROLES.includes(sub.role) || sub.role === "advisor")) {
123
+ const registryLive = state._agent?._syncChildAborts?.has(sub.key) === true
124
+ // F-2(QUEUED-VISIBILITY——2026-09-09):门控扩 queued——排队块(sub.queued——
125
+ // slot/wait/depc 三态)头置取消 ⏹(用户可撤销排队决策)——queued 恒先于 async 置位
126
+ // (subagent-blocks queued 分支守卫)——async/registry-live 判定不变(running 停面)。
127
+ if (!sub.done && (sub.async === true || registryLive || !!queued) && (SUBAGENT_ROLES.includes(sub.role) || sub.role === "advisor")) {
171
128
  const cut = sliceByWidth(headText, Math.max(0, cols - 3))
172
129
  headText = cut + " ".repeat(Math.max(0, cols - 3 - stringWidth(cut)))
173
130
  line.text = `${headText} ${ansi.dim}⏹${ansi.reset} `
@@ -187,8 +144,6 @@ export function renderSubagentPanel(state, cols, maxRows) {
187
144
  out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim })
188
145
  }
189
146
  }
190
- // §27 R23:子块段(嵌套子代理——头 + tail2/展开窗口)随外层块渲染(面板与冻结共用)。
191
- out.push(...renderSubChildSections(state, sub, cols, maxRows))
192
147
  }
193
148
  return out
194
149
  }
@@ -4,10 +4,11 @@
4
4
  * re-import(runAgentTurn ↔ suspensionSession 函数级静态环——模块求值期无顶层调用,
5
5
  * 环安全——session-slots ↔ session.mjs 同款先例)。
6
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。
7
+ * §17(2026-09-02,AGENT-LOOP.md §9 D-S1..S9):回合尾后台池非空 → 不阻塞等待,
8
+ * 进入挂起态——挂起空闲输入开放(Enter = 新回合填单槽 + 唤醒)、busy(processing
9
+ * digest)输入禁用(INPUT-LOCK-ASYNC C'——提交吞——2026-09-09)、settle 事件驱动
10
+ * auto-turn 消化(手动档 organize-only / AUTO 档全语义)、池空 + 无待处理输入 → 补发
11
+ * done 冻结自然退出。状态机行表见 AGENT-LOOP.md §9.2。
11
12
  */
12
13
 
13
14
  // 函数级静态环(2026-09-05):drive 的 digestTurn/用户回合经 runAgentTurn 递归进入
@@ -20,57 +21,13 @@ import { C } from "./ansi.mjs"
20
21
  // ASYNC-RESULT-CONTAINER.md D1/D2:池 accessor(双池 absorb)+ pending 单容器停靠
21
22
  import { getAsyncPool, parkAsyncPending } from "../agent-tools/async-settle.mjs"
22
23
 
23
- // ── §24 D-24cR15——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
- }
24
+ // INPUT-LOCK-ASYNCC'——2026-09-09,本档 INPUT-LOCK-ASYNC.md):R15 排队
25
+ // 用户指令合并(§11.3 D-24c——攒批计划/合并文案/上限常量)整批废弃
26
+ // ——busy 提交吞 + pendingInput 单槽化(至多一条待交接——单消息逐发不攒批)。
27
+ // 废弃记录见 AGENT-LOOP.md §11.3。
71
28
 
72
29
  /** 后台池计数(LOGGING susp/digest 事件字段——pendingN/poolN)。
73
- * poolN = _asyncSubagents + _asyncAdvisors(§24 D-24b advisor 池同面板计数——queued
30
+ * poolN = _asyncSubagents + _asyncAdvisors(§11.2 D-24b advisor 池同面板计数——queued
74
31
  * 条目同样在 map 内——2026-09-03 code review #2:不再 +queue.length 双计)。
75
32
  * R17(§25 D-R17a/b):pendingN = pending 单容器 `_pendingAsyncResults` 条数(
76
33
  * ASYNC-RESULT-CONTAINER.md D2——四族统一停靠——digest 驱动判据同单容器)。 */
@@ -117,7 +74,7 @@ export function allPendingEntries(agent) {
117
74
  /** 后台池存活判据(D-S2/F5 口径):running/queued 子代理或后台评审,或已 settle 未注入结果
118
75
  * (pending 单容器非空 = D-S3 "未注入"),或 running consult 会话(会诊跨回合——
119
76
  * §25 D-R17a 挂起活度钩子——consult 启动回合尾即入挂起态)。回合尾与每次轮末都用它
120
- * 评估退出。§24 D-24b:_asyncAdvisors(后台评审池)与子代理池同判——评审飞行中挂起
77
+ * 评估退出。§11.2 D-24b:_asyncAdvisors(后台评审池)与子代理池同判——评审飞行中挂起
121
78
  * 会话必须存活。 */
122
79
  export function poolLive(agent) {
123
80
  const map = agent._asyncSubagents
@@ -173,7 +130,7 @@ function waitForSettleOrWake(agent, state) {
173
130
  }
174
131
 
175
132
  /** 后台模式状态行文本(D-S8;17.5.4 #6 顺手对齐):"后台 N 子代理运行中 · M 完成待消化"
176
- * ——"运行中" = running + queued(含后台评审——§24 D-24b 同面板计数)+ running consult
133
+ * ——"运行中" = running + queued(含后台评审——§11.2 D-24b 同面板计数)+ running consult
177
134
  * children(R17——会诊跨回合);"完成待消化" = pending 任一族移交项 + §17.5 回合尾留池
178
135
  * 的 settled 未消费项(挂起会话 sweep 前的可见窗口)。 */
179
136
  function backgroundStatusText(agent) {
@@ -217,7 +174,8 @@ async function digestTurn(ctx) {
217
174
  /**
218
175
  * §17 挂起会话驱动(D-S9 行表;由 runAgentTurn 回合尾进入,池空自然退出):
219
176
  * - suspension:池项 settle → 入 pending → 开 auto-turn(合并消化近邻 settle);
220
- * 用户 Enter → pendingInputdigest 运行中排队,D-S5)——用户输入优先于 digest;
177
+ * 挂起空闲用户 Enter → pendingInput 单槽(busy 含 digest 提交吞——INPUT-LOCK-ASYNC)
178
+ * ——用户输入优先于 digest;
221
179
  * - auto-turn:消化中 settle 不并发开新轮(单 runAgent 循环),轮末按 pending/池态
222
180
  * 续开合并消化轮或回挂起;pendingInput 非空 → 以该消息开新回合(不触发新 digest);
223
181
  * - §17.5.5:每次消化/会话内用户回合消费 pending 后 → freezeReclaimDigestedBlocks
@@ -250,25 +208,14 @@ export async function suspensionSession(ctx) {
250
208
  try {
251
209
  while (!state._suspAborted && !agent._sessionAbort.signal.aborted) {
252
210
  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
- }
211
+ // 1. 用户输入优先(D-S5):pendingInput 单槽(INPUT-LOCK-ASYNC C'——busy(processing
212
+ // digest)提交吞——Enter 只可能落在挂起空闲/释放窗口——至多一条待交接——driver
213
+ // 消费清槽即开新回合)。R15 攒批/queue 双源已随排队机制废弃收敛(2026-09-09——
214
+ // 单消息交接——无合并无 /cmd 分流——key-handler 只收非斜杠文本——单消息逐发)。
215
+ if ((state.pendingInput?.length ?? 0) > 0) {
216
+ const head = String(state.pendingInput.shift())
270
217
  agent._suspended = false // 用户回合 = 普通回合语义(① 直注入 + settle 即冻结)
271
- await runAgentTurn(ctx, head.text, { skipSession: true })
218
+ await runAgentTurn(ctx, head, { skipSession: true })
272
219
  agent._suspended = true
273
220
  // §17.5.5:该回合消化完 pending(run 首行注入)→ 逐条冻结回收驻留块
274
221
  // (不等池空——settle 锚点 splice——digest 总览文本之前;与 digest 回收同规则)
@@ -319,14 +266,13 @@ export async function suspensionSession(ctx) {
319
266
  agent._pendingAsyncResults = []
320
267
  const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
321
268
  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)
269
+ // §17 round2 偏差 #2-CLI(code review round2 #2-CLI)+ INPUT-LOCK 单槽化:中止时
270
+ // 不静默丢弃挂起期输入——Enter 已清空输入框并入 pendingInput(用户视为已发送)——
271
+ // 单槽语义下残余至多一条——转回 state.queue({text} 单条目,下个普通回合的队列
272
+ // 循环续发——零丢失)+ 提示行明示去向(不静默丢)。
273
+ if ((state.pendingInput?.length ?? 0) > 0) {
274
+ state.queue.push({ text: String(state.pendingInput.shift()) })
275
+ pushLine(`[background work stopped the message you entered will run as a normal turn]`, C.warn)
330
276
  }
331
277
  } else {
332
278
  // D-S3 ③ 兜底:退出前残余(极端竞态)直注入再退——结果零丢失(AC-S2;