thincoder 0.12.54 → 0.12.59

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 (129) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +1 -1
  3. package/bin/thincoder.mjs +25 -3
  4. package/package.json +3 -7
  5. package/src/acp/bridge.mjs +132 -26
  6. package/src/advisor/messages.mjs +38 -3
  7. package/src/advisor/run.mjs +91 -53
  8. package/src/advisor.mjs +15 -7
  9. package/src/agent/dispatch.mjs +156 -39
  10. package/src/agent/helpers.mjs +46 -4
  11. package/src/agent/setup.mjs +102 -19
  12. package/src/agent/spawn-child.mjs +28 -1
  13. package/src/agent-tools/advisor.mjs +43 -11
  14. package/src/agent-tools/consult.mjs +37 -6
  15. package/src/agent-tools/eng.mjs +4 -1
  16. package/src/agent-tools/goal.mjs +11 -1
  17. package/src/agent-tools/read-history.mjs +160 -0
  18. package/src/agent-tools/settings.mjs +162 -0
  19. package/src/agent-tools/skill.mjs +2 -1
  20. package/src/agent-tools/subagent-actions.mjs +432 -0
  21. package/src/agent-tools/subagent-async.mjs +427 -0
  22. package/src/agent-tools/subagent-scheduler.mjs +319 -0
  23. package/src/agent-tools/subagent.mjs +565 -128
  24. package/src/agent-tools/task.mjs +4 -3
  25. package/src/agent-tools/timer.mjs +9 -4
  26. package/src/agent-tools/verify.mjs +161 -49
  27. package/src/agent-tools.mjs +1 -0
  28. package/src/agent.mjs +182 -81
  29. package/src/auto-think.mjs +14 -0
  30. package/src/cli/make-agent.mjs +27 -1
  31. package/src/cli/memory-command.mjs +28 -7
  32. package/src/cli/permission.mjs +8 -1
  33. package/src/config.mjs +125 -8
  34. package/src/context.mjs +115 -34
  35. package/src/distill.mjs +19 -1
  36. package/src/escape.mjs +82 -27
  37. package/src/log.mjs +195 -0
  38. package/src/mcp/transport-http.mjs +13 -1
  39. package/src/mcp.mjs +52 -7
  40. package/src/memory/code-sync.mjs +1 -1
  41. package/src/memory/core.mjs +204 -10
  42. package/src/memory/docs.mjs +197 -62
  43. package/src/memory.mjs +1 -1
  44. package/src/model-specs.mjs +38 -1
  45. package/src/prompts/advisor-design.md +46 -0
  46. package/src/prompts/advisor-round1.md +49 -2
  47. package/src/prompts/advisor-round2.md +47 -0
  48. package/src/prompts/advisor-round3.md +47 -0
  49. package/src/prompts/coder.md +22 -0
  50. package/src/prompts/consult-base.md +13 -0
  51. package/src/prompts/discipline.md +25 -6
  52. package/src/prompts/eng-coder.md +2 -2
  53. package/src/prompts/engineering-sub.md +23 -1
  54. package/src/prompts/engineering.md +157 -50
  55. package/src/prompts/explore.md +1 -2
  56. package/src/prompts/main.md +11 -5
  57. package/src/prompts/methodology-template.md +14 -0
  58. package/src/prompts/system.md +5 -2
  59. package/src/provider/anthropic.mjs +7 -5
  60. package/src/provider/core.mjs +104 -28
  61. package/src/provider/google.mjs +57 -24
  62. package/src/provider/normalize.mjs +1 -1
  63. package/src/provider/rate.mjs +0 -2
  64. package/src/provider/responses.mjs +8 -13
  65. package/src/provider/sse.mjs +20 -0
  66. package/src/session.mjs +15 -0
  67. package/src/tools/apply_patch.md +5 -1
  68. package/src/tools/bash.md +3 -3
  69. package/src/tools/delete.md +1 -0
  70. package/src/tools/edit-batch.mjs +92 -0
  71. package/src/tools/edit-diff.mjs +265 -0
  72. package/src/tools/edit.md +11 -6
  73. package/src/tools/execute.md +8 -8
  74. package/src/tools/execute.mjs +31 -35
  75. package/src/tools/file.mjs +26 -114
  76. package/src/tools/file_ops.md +3 -2
  77. package/src/tools/get_current_time.md +3 -1
  78. package/src/tools/git.md +1 -1
  79. package/src/tools/git.mjs +8 -16
  80. package/src/tools/hashline_edit.md +2 -0
  81. package/src/tools/index.mjs +3 -2
  82. package/src/tools/insert_after.md +2 -1
  83. package/src/tools/lint.md +3 -1
  84. package/src/tools/linter.mjs +9 -37
  85. package/src/tools/lsp.md +4 -1
  86. package/src/tools/patch.mjs +84 -13
  87. package/src/tools/pdf-parse-text.mjs +497 -0
  88. package/src/tools/pdf-parse-xref.mjs +499 -0
  89. package/src/tools/pdf.mjs +155 -0
  90. package/src/tools/question.md +2 -1
  91. package/src/tools/read.md +1 -0
  92. package/src/tools/read_pdf.md +21 -0
  93. package/src/tools/repomap.mjs +1 -1
  94. package/src/tools/shared.mjs +11 -32
  95. package/src/tools/system.mjs +6 -21
  96. package/src/tools/tree.md +2 -1
  97. package/src/tools/web.mjs +5 -3
  98. package/src/tools/websearch.md +2 -1
  99. package/src/tools/write.md +2 -0
  100. package/src/traces/trace-store.mjs +224 -0
  101. package/src/tui/agent-turn.mjs +387 -24
  102. package/src/tui/clipboard.mjs +17 -6
  103. package/src/tui/cmd-config.mjs +29 -9
  104. package/src/tui/cmd-eng.mjs +1 -0
  105. package/src/tui/cmd-extract.mjs +1 -1
  106. package/src/tui/cmd-mcp-form.mjs +197 -0
  107. package/src/tui/cmd-mcp.mjs +264 -114
  108. package/src/tui/cmd-think.mjs +1 -1
  109. package/src/tui/index.mjs +49 -95
  110. package/src/tui/interaction.mjs +41 -3
  111. package/src/tui/key-handler.mjs +105 -143
  112. package/src/tui/key-modes.mjs +215 -0
  113. package/src/tui/layout.mjs +22 -1
  114. package/src/tui/mouse.mjs +41 -1
  115. package/src/tui/pickers.mjs +73 -7
  116. package/src/tui/render-conversation.mjs +13 -161
  117. package/src/tui/render-frame.mjs +45 -20
  118. package/src/tui/render-loop.mjs +4 -1
  119. package/src/tui/render-segments.mjs +165 -0
  120. package/src/tui/render.mjs +4 -4
  121. package/src/tui/startup.mjs +40 -2
  122. package/src/tui/subagent-blocks.mjs +404 -111
  123. package/src/tui/subagent-panel.mjs +88 -13
  124. package/src/tui/tool-args.mjs +10 -2
  125. package/src/tui/tool-events.mjs +172 -95
  126. package/src/tui/update-notice.mjs +72 -0
  127. package/src/tui/wizard.mjs +36 -6
  128. package/src/agent-tools/escalate.mjs +0 -179
  129. package/src/tools/exec-prelude.mjs +0 -84
@@ -0,0 +1,319 @@
1
+ /**
2
+ * subagent-scheduler.mjs — §20 子 agent 任务调度器 + 文件域组(2026-09-05 自
3
+ * subagent-async.mjs 拆分——Module Split Policy §20.9——纯迁移零行为变化——
4
+ * AGENT-LOOP.md §20 D-SD1..SD5 + §21.1 D-SL1 环形死锁修正)。
5
+ * 内容:normalizeFileList / filesOverlap / depInfo / describeBlockers / detectStall(§21.1
6
+ * P-SL2 停滞机械检测)/ queueRunnable / assertNoDepCycle / dependentLabels /
7
+ * refreshQueuedTokens / maybeRefillAsync。
8
+ * ASYNC_SUBAGENT_LIMIT 常量回引自 subagent-async.mjs(主体保有——仅调用期使用——
9
+ * 与主体对此处调度符号的回引构成惰性环——无求值期依赖)。
10
+ */
11
+ import { isAbsolute, relative, resolve } from "node:path"
12
+ import { existsSync, statSync } from "node:fs"
13
+ import { ASYNC_SUBAGENT_LIMIT } from "./subagent-async.mjs"
14
+
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+ // §20 子 agent 任务调度器(AGENT-LOOP.md §20——D-SD1..SD5 + 20.4 处置注)
17
+ // 池条目域元数据(D-SD2:entry._files/_dependsOn——running ∪ queued 全带)、准入
18
+ // (D-SD3:域冲突/依赖未满足 → queued 等位)、补位扫描(D-SD4:最早可启动——
19
+ // 依赖全满足 + 域无冲突——waiting 越行不阻塞 slot 位)、释放规则(D-SD5——round2
20
+ // #3 锁定默认:依赖取消/失败 → 依赖者留 queued 标 dependency cancelled——仅父显式
21
+ // 处置或 AUTO 自动启动)、终态墓碑(round1 #8/T-SD14:check/注入消费与取消写墓碑——
22
+ // consumed 视为满足;非 consumed unknown id 才拒)。状态全部派生不存储(单点事实)。
23
+ // ═══════════════════════════════════════════════════════════════════════════
24
+
25
+ /** 文件域归一化(round1 #5——路径归一化再交集):相对 cwd 解析为绝对路径 + 去重;
26
+ * 非字符串/空项静默跳过(声明错误 = false-negative 明示风险——v1 边界)。
27
+ * §20.8 D-F1.1(2026-09-04):目录声明检测——fail-closed——尾斜杠形态 / 指向既有目录
28
+ * → throw(含路径——错误字符串英文定稿)——目录声明静默绕过冲突检测的通道闭合;
29
+ * 调用方(subagent.mjs spawn 入口)catch → 错误即工具结果(模型可见——无静默)。
30
+ * 已知限制(§20.8 未编号段——评审 #4):不存在的目录声明(无尾斜杠 + 目录未创建)仍通过——不处理。 */
31
+ export function normalizeFileList(files, cwd) {
32
+ const out = []
33
+ for (const f of Array.isArray(files) ? files : []) {
34
+ if (typeof f !== "string" || !f.trim()) continue
35
+ if (f.endsWith("/") || f.endsWith("\\")) {
36
+ throw new Error(`files must be file-level paths — directory declarations are not supported: ${f}`)
37
+ }
38
+ const abs = resolve(cwd ?? process.cwd(), f)
39
+ if (existsSync(abs) && statSync(abs).isDirectory()) {
40
+ throw new Error(`files must be file-level paths — directory declarations are not supported: ${f}`)
41
+ }
42
+ if (!out.includes(abs)) out.push(abs)
43
+ }
44
+ return out
45
+ }
46
+
47
+ /** 文件域相等比较键:Windows 大小写不敏感(vs Uri.fsPath 小写盘符差异同族——
48
+ * normalizeCwd 先例)——src/x vs ./src/X 在 win32 是同一文件。 */
49
+ const fileKey = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
50
+
51
+ /** 两文件域首个共同文件(比较键)——无交集 null。 */
52
+ export function filesOverlap(a, b) {
53
+ if (!a?.length || !b?.length) return null
54
+ const keys = new Set(b.map(fileKey))
55
+ const hit = a.map(fileKey).find((k) => keys.has(k))
56
+ return hit ?? null
57
+ }
58
+
59
+ /** 冲突文件的显示形态(优先相对 cwd——面板/返回文本可读)。 */
60
+ function showFile(parent, key) {
61
+ const cwd = parent.cwd ?? process.cwd()
62
+ const rel = relative(cwd, key)
63
+ return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : key
64
+ }
65
+
66
+ /**
67
+ * §20 依赖终态查询(单点事实——池条目 / pending(挂起期 settle 移交——注入前)/
68
+ * 终态墓碑(check/注入消费——consumed;取消/失败——D-SD5 分支)):
69
+ * - ok = settle 成功(报告已产出)/ consumed(check/注入消费——T-SD14 视为满足)
70
+ * - pending = running/queued 未终态(等启动/等完成)
71
+ * - failed / cancelled = 终态但非成功——依赖者走 dependency cancelled 分支(round2 #3)
72
+ * - unknown = 从未存在(spawn 时明确错误——非 consumed 的 unknown 拒——T-SD10)
73
+ */
74
+ export function depInfo(parent, id) {
75
+ const key = String(id)
76
+ const e = parent._asyncSubagents?.get(key)
77
+ if (e) {
78
+ if (e.cancelled) return { state: "cancelled", role: e.role }
79
+ if (e.done) return e.error != null ? { state: "failed", role: e.role } : { state: "ok", role: e.role }
80
+ return { state: "pending", role: e.role }
81
+ }
82
+ const pend = (parent._pendingAsyncResults ?? []).find((x) => String(x.id) === key)
83
+ if (pend) return pend.error != null ? { state: "failed", role: pend.role } : { state: "ok", role: pend.role }
84
+ const t = parent._asyncTombstones?.get(key)
85
+ if (t) return { state: t.status === "cancelled" || t.status === "failed" ? t.status : "ok", role: t.role }
86
+ return { state: "unknown", role: null }
87
+ }
88
+
89
+ /** §20 等待态派生(无存储——refill/status/面板/spawn 返回同一事实源)。kind:
90
+ * - slot = 无阻塞(依赖全满足 + 域无冲突)——纯槽满等位(可启动——等 slot)
91
+ * - wait = 依赖未完成 / 域冲突(running ∪ queued——D-SD3 同界——self 除外)
92
+ * - depc = 依赖取消/失败(round2 #3——非 AUTO 锁住——需父显式处置;AUTO 视为可启动)
93
+ * detail 为状态行共享文本(面板 waiting for 标注 / status reason / spawn reason)。 */
94
+ export function describeBlockers(parent, entry) {
95
+ const wait = []
96
+ const depc = []
97
+ for (const depId of entry._dependsOn ?? []) {
98
+ const info = depInfo(parent, String(depId))
99
+ if (info.state === "pending" || info.state === "unknown") {
100
+ wait.push(`${info.role ?? "sub"}#${depId}(依赖未完成)`)
101
+ } else if (info.state === "cancelled" || info.state === "failed") {
102
+ if (parent.autoApprove) continue // AUTO 档自动启动(D-SD5——父不在场由 digest 决策)
103
+ depc.push(`${info.role ?? "sub"}#${depId}`)
104
+ }
105
+ }
106
+ const myFiles = entry._files ?? []
107
+ if (myFiles.length > 0) {
108
+ for (const e of parent._asyncSubagents?.values() ?? []) {
109
+ if (e === entry) continue
110
+ if (e.status !== "running" && e.status !== "queued") continue
111
+ const hit = filesOverlap(myFiles, e._files ?? [])
112
+ if (!hit) continue
113
+ // §21.1 D-SL1.2(环形死锁修正——与 queueRunnable 同界——展示一致):后入
114
+ // 者(spawn 序晚于我——数字 id 比较)不列——只列"会真正阻断我的"(running
115
+ // 任意序 + 先入 queued);列后入者 = 误导"等一个其实等不到的人"。
116
+ if (e.status === "queued" && Number(e.id) > Number(entry.id)) continue
117
+ wait.push(`${e.role}#${e.id}(域冲突 ${showFile(parent, hit)})`)
118
+ }
119
+ }
120
+ // 长列表裁剪(块头宽度预算——细节 status 可查全量)
121
+ const cut = (arr) => (arr.length > 3 ? [...arr.slice(0, 3), `…(共 ${arr.length} 项)`] : arr)
122
+ if (depc.length > 0) {
123
+ const body = cut(depc).join("、")
124
+ return { kind: "depc", detail: wait.length > 0 ? `dependency cancelled: ${body};${cut(wait).join("、")}` : `dependency cancelled: ${body} — waiting for your decision (cancel this task to release, or AUTO starts it)` }
125
+ }
126
+ if (wait.length > 0) return { kind: "wait", detail: `waiting for: ${cut(wait).join("、")}` }
127
+ return { kind: "slot", detail: "" }
128
+ }
129
+
130
+ /** §21.1 P-SL2(D-SL2——混合边环形等待停滞机械检测——AGENT-LOOP.md §21.1 扩展注):
131
+ * 判据(收窄——零误报优先——宁可漏报不可误打断):池内 running = 0 && queued ≥ 2 &&
132
+ * 无 dependency-cancelled 标记条目 && 每 queued 的 blocker(files 冲突者 + 未 settle
133
+ * 依赖目标——与 describeBlockers/queueRunnable 同界——"真正会阻断我的"语义:files 只
134
+ * 算 running 任意序 + 先入 queued(D-SL1.1 序判定);依赖 pending = 池内未终态)⊆
135
+ * queued 集且非空(闭包无外逃 + 无 runnable 漏网)→ 停滞。返回 { chains }(每 queued
136
+ * 条目一条阻塞链——沿首个 blocker 走到首个重复节点(闭环)——每节点自带"等谁 + 为何等"
137
+ * 理由注(`X(reason) → Y` 形态——X 等 Y 因 reason);不满足 → null。
138
+ * 不报(F-SL2.2 收窄判据):running 锚点存在(依赖链/文件串行正常排队);depc 滞留
139
+ * (外部决策可解——§20 NF-SD 滞留有意——cancel 先入者即释放);单 queued;blocker
140
+ * 外逃(unknown 依赖目标/池外条目);blocker 空(runnable——settle/cancel 驱动 refill
141
+ * 会启动——非停滞)。自然流程中 wait 边恒指向先入者(依赖必须先前 spawn——unknown 拒 +
142
+ * 文件冲突只阻断后入者——id 序)——混合环仅人工注入可构造(T-SD5 同族防御断言)——
143
+ * 但状态既可能成环即机械可检——不依赖可达性论证。maybeRefillAsync 空转处不调用:refill
144
+ * 嵌在 settle/cancel 链上无模型可见输出通道(抛错即破坏池操作)——check 守卫(async
145
+ * 模块)+ status 视图(actions 模块)即 F-SL2 的报错落点。 */
146
+ export const STALL_NOTE = "cancel one task in the loop (action:'cancel') to break the cycle, then re-spawn it (AGENT-LOOP.md §21.1 P-SL2)"
147
+
148
+ export function detectStall(parent) {
149
+ const map = parent._asyncSubagents
150
+ if (!(map instanceof Map) || map.size === 0) return null
151
+ const all = [...map.values()]
152
+ if (all.some((e) => e.status === "running")) return null // ① running 锚点——正常排队不报
153
+ const queued = all.filter((e) => e.status === "queued" && !e.cancelled)
154
+ if (queued.length < 2) return null // ② 单 queued——非停滞不报
155
+ // ③ 每 queued 的结构化 blocker 表(阻塞边 + 理由——镜像 describeBlockers 同界:
156
+ // 依赖 pending/unknown 阻断(cancelled/failed 非 AUTO = depc 标记——整池不报);
157
+ // files 冲突 running 任意序 + 先入 queued——后入者不阻断(D-SL1.1 同判)。
158
+ const edgesOf = new Map()
159
+ for (const q of queued) {
160
+ const list = []
161
+ for (const depId of q._dependsOn ?? []) {
162
+ const st = depInfo(parent, String(depId)).state
163
+ if (st === "pending") {
164
+ list.push({ to: map.get(String(depId)), reason: `dependsOn ${String(depId)}` })
165
+ } else if (st === "unknown") {
166
+ return null // 依赖目标池外(unknown)——外逃 blocker——不报(保守)
167
+ } else if (!parent.autoApprove) {
168
+ return null // dep-cancelled/failed 标记条目——外部决策可解——不报(F-SL2.2)
169
+ }
170
+ }
171
+ const myFiles = q._files ?? []
172
+ if (myFiles.length > 0) {
173
+ for (const e of all) {
174
+ if (e === q) continue
175
+ if (e.status !== "running" && e.status !== "queued") continue
176
+ const hit = filesOverlap(myFiles, e._files ?? [])
177
+ if (!hit) continue
178
+ if (e.status === "queued" && Number(e.id) > Number(q.id)) continue // 后入不阻断
179
+ list.push({ to: e, reason: `files ${showFile(parent, hit)}——先入者` })
180
+ }
181
+ }
182
+ if (list.length === 0) return null // ④ 无 blocker = runnable——refill 会启动——非停滞
183
+ edgesOf.set(q, list)
184
+ }
185
+ // ⑤ 闭包无外逃:任一 blocker 落 queued 集外(含 cancelled/池外异常形态)→ 不报
186
+ for (const list of edgesOf.values()) {
187
+ for (const b of list) {
188
+ if (b.to == null || !queued.includes(b.to)) return null
189
+ }
190
+ }
191
+ // 链组装:每 queued 条目一条——沿首个 blocker 走到首个重复节点(闭环)——每节点
192
+ // 自带"我等谁 + 为何等"段(`X(reason) → Y`——本节点 blocker 理由注于本节点旁——
193
+ // 读取无歧义:X 等 Y 因为 reason——dependsOn 边不误读为目标属性——§21.1 扩展注
194
+ // 示例同信息形态——闭环以起始节点重复闭合(末节点不再注——闭合自明)。
195
+ const chains = []
196
+ for (const start of queued) {
197
+ const seen = []
198
+ let cur = start
199
+ for (;;) {
200
+ seen.push(cur)
201
+ const next = edgesOf.get(cur)?.[0]?.to
202
+ if (!next) break // 防御:预检保证不达(每节点 blocker 非空闭包内)
203
+ if (seen.includes(next)) { seen.push(next); break } // 闭环:重复节点入链闭合
204
+ cur = next
205
+ }
206
+ const frags = []
207
+ for (let i = 0; i < seen.length; i++) {
208
+ const edge = i < seen.length - 1 ? edgesOf.get(seen[i])?.[0] : null // 末节点=闭环重复——不再注
209
+ frags.push(`${seen[i].role}#${seen[i].id}${edge ? `(${edge.reason})` : ""}`)
210
+ }
211
+ chains.push({ task: start, text: frags.join(" → ") })
212
+ }
213
+ return { chains }
214
+ }
215
+
216
+
217
+ /** §20 D-SD4 补位判据:依赖全满足(AUTO 下 depc 放行)+ 域无冲突(running 任意序 +
218
+ * queued 先入者——§21.1 D-SL1.1 序判定:同文件串行 = 先入者先启动、后入者等先入者
219
+ * ——不自锁;先入者启动后以 running 身份继续挡住后入者——self 除外)。
220
+ * 已知限制(§21.1 评审 #4——与 §20 NF-SD 同语义——滞留有意义不静默):先入者被
221
+ * depc 锁定时(依赖取消/失败且非 AUTO——永不自动启动),后入者滞留等它——cancel
222
+ * 先入者即释放(父显式可清;AUTO 档 depc 视为可启动——不滞留)。 */
223
+ export function queueRunnable(parent, entry) {
224
+ for (const depId of entry._dependsOn ?? []) {
225
+ const state = depInfo(parent, String(depId)).state
226
+ if (state === "pending" || state === "unknown") return false
227
+ if ((state === "cancelled" || state === "failed") && !parent.autoApprove) return false
228
+ }
229
+ const myFiles = entry._files ?? []
230
+ if (myFiles.length > 0) {
231
+ for (const e of parent._asyncSubagents?.values() ?? []) {
232
+ if (e === entry) continue
233
+ if (e.status !== "running" && e.status !== "queued") continue
234
+ if (!filesOverlap(myFiles, e._files ?? [])) continue
235
+ // §21.1 D-SL1.1 序判定:queued 仅"先入者"(spawn 序早于我——数字 id 比较)阻断;
236
+ // 后入者不阻断——先入者先启动——两个 queued 同文件不再互等(环形死锁修正)。
237
+ // 防御(评审 #3——id 形态):池条目 id 为数字递增(_subAgentCounter——已核实);
238
+ // 异常形态 Number() 得 NaN → 比较 false → 不跳过 → 保守阻断(宁可多等——
239
+ // 不冒险并发——防 NaN 误放行)。
240
+ if (e.status === "queued" && Number(e.id) > Number(entry.id)) continue // 后入者不阻断——先入者先启动
241
+ return false
242
+ }
243
+ }
244
+ return true
245
+ }
246
+
247
+ /** §20 D-SD5 环防御(round2 #5——自然流程不可达:unknown id 拒 + spawn 序天然无环——
248
+ * 仅人工向池注入可构造——防御断言定位):从新 spawn 的依赖集出发沿池内条目
249
+ * _dependsOn 边做路径 DFS——路径上重复访问(可达环)→ 拒绝(A→B→A 永不自启——
250
+ * 错误明确——T-SD5)。运行/排队条目皆可成环节点;池小(≤4 槽 + 有限队列)深度有限。 */
251
+ export function assertNoDepCycle(parent, dependsOn) {
252
+ const edges = new Map()
253
+ for (const e of parent._asyncSubagents?.values() ?? []) {
254
+ if (e.status === "running" || e.status === "queued") {
255
+ edges.set(String(e.id), (e._dependsOn ?? []).map(String))
256
+ }
257
+ }
258
+ const onPath = new Set()
259
+ const visit = (id) => {
260
+ if (onPath.has(id)) {
261
+ throw new Error(`subagent dependsOn cycle detected: ${[...onPath, id].join(" → ")} — entries in a dependency loop can never start; cancel the dependents and restructure the chain (AGENT-LOOP.md §20 D-SD5)`)
262
+ }
263
+ onPath.add(id)
264
+ for (const dep of edges.get(id) ?? []) visit(dep)
265
+ onPath.delete(id)
266
+ }
267
+ for (const d of dependsOn) visit(String(d))
268
+ }
269
+
270
+ /** 依赖某 id 的 queued 条目显示标签(D-SD5 提醒/标注——依赖者列表)。 */
271
+ export function dependentLabels(parent, depId) {
272
+ const key = String(depId)
273
+ const out = []
274
+ for (const e of parent._asyncQueue ?? []) {
275
+ if ((e._dependsOn ?? []).some((d) => String(d) === key)) out.push(`${e.role}#${e.id}`)
276
+ }
277
+ return out
278
+ }
279
+
280
+ /** §20 D-SD3b 排队态面板刷新(⟦ev⟧queued 事件族——TUI routeSubToken 消费):对全部
281
+ * queued 条目重算等待态并发射变化(去重 sig——kind/position/detail 全变才发)——
282
+ * 调用点 = 一切队列突变与等待态变迁(spawn 入队 / settle 后补位与依赖转移 / cancel
283
+ * 出队 / check 消费)。position = 队列序(D-A1 既有——cancel 前移同源)。 */
284
+ export function refreshQueuedTokens(parent, onToken) {
285
+ if (typeof onToken !== "function") return
286
+ const queue = parent._asyncQueue ?? []
287
+ for (let i = 0; i < queue.length; i++) {
288
+ const e = queue[i]
289
+ const blk = describeBlockers(parent, e)
290
+ const sig = `${blk.kind}\x1e${i + 1}\x1e${blk.detail}`
291
+ if (e._lastQueuedSig === sig) continue
292
+ e._lastQueuedSig = sig
293
+ try {
294
+ onToken(`${e.relayPrefix}⟦ev⟧queued\x1e${blk.kind}\x1e${i + 1}\x1equeued\x1e${blk.detail}`)
295
+ } catch { /* relay 失败不影响池状态 */ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Slot-queue refill (AGENT-LOOP.md §15 D-A1/D-A6 + §20 D-SD4): start queue heads
301
+ * while a running slot is free — called from every settle (completion frees a slot)
302
+ * and from the turn-end collection's refill loop. §20:队列现可混合 waiting-deps 与
303
+ * slot-queued——扫描选"依赖全满足 + 域无冲突"的最早条目启动(waiting 越行不阻塞
304
+ * 槽位;多任务同时解除按 queued 序逐个启动到槽满——上限 4 不变)。纯 slot 队列的
305
+ * 行为与旧 shift 完全一致(全部条目可启动 → 最早 == 队首)。
306
+ */
307
+ export function maybeRefillAsync(parent) {
308
+ const queue = parent._asyncQueue ?? []
309
+ for (;;) {
310
+ const running = [...(parent._asyncSubagents?.values() ?? [])].filter((e) => e.status === "running").length
311
+ if (running >= ASYNC_SUBAGENT_LIMIT) return
312
+ let pick = -1
313
+ for (let i = 0; i < queue.length; i++) {
314
+ if (queueRunnable(parent, queue[i])) { pick = i; break }
315
+ }
316
+ if (pick < 0) return
317
+ queue.splice(pick, 1)[0].start()
318
+ }
319
+ }