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.
- package/CHANGELOG.md +38 -3
- package/README.md +2 -2
- package/bin/thincoder.mjs +80 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +7 -4
- package/src/advisor/messages.mjs +24 -4
- package/src/advisor/run.mjs +35 -33
- package/src/advisor.mjs +25 -6
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +102 -19
- package/src/agent/helpers.mjs +36 -0
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +18 -2
- package/src/agent/spawn-child.mjs +29 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +110 -108
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +121 -102
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/read-history.mjs +155 -31
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +7 -17
- package/src/agent-tools/subagent-actions.mjs +168 -130
- package/src/agent-tools/subagent-async.mjs +129 -174
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +45 -21
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +107 -555
- package/src/agent-tools/verify.mjs +118 -270
- package/src/agent.mjs +57 -190
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +3 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +56 -8
- package/src/context.mjs +5 -147
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +11 -11
- package/src/explore-distill.mjs +155 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +58 -48
- package/src/memory.mjs +3 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +9 -76
- package/src/prompts/advisor-round1.md +9 -68
- package/src/prompts/advisor-round2.md +7 -54
- package/src/prompts/advisor-round3.md +7 -54
- package/src/prompts/coder.md +7 -50
- package/src/prompts/consult-base.md +4 -24
- package/src/prompts/discipline.md +26 -44
- package/src/prompts/eng-coder.md +7 -32
- package/src/prompts/engineering-sub.md +3 -23
- package/src/prompts/engineering.md +53 -306
- package/src/prompts/explore.md +3 -12
- package/src/prompts/main.md +10 -32
- package/src/prompts/methodology-template.md +28 -48
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +16 -35
- package/src/provider/core.mjs +6 -67
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +11 -6
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +5 -6
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +3 -0
- package/src/tools/read.md +0 -1
- package/src/tools/shared.mjs +14 -13
- package/src/tools/system.mjs +44 -9
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +17 -228
- package/src/tui/cmd-config.mjs +48 -7
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +3 -2
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/mouse.mjs +7 -1
- package/src/tui/pickers.mjs +40 -22
- package/src/tui/render-segments.mjs +27 -10
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +95 -263
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +61 -23
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +3 -3
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +37 -173
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +12 -6
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-run.mjs — async spawn 执行器 executeAsyncSpawn(2026-09-05 module-split:
|
|
3
|
+
* subagent.mjs 726 > 500 硬限——execute 的 async 分支 verbatim 迁入(闭包变量参数化:
|
|
4
|
+
* parent/ctx/role/args + buildSpawnChild 产物),语义零变——subagent.mjs execute 调
|
|
5
|
+
* 用(只此一处);§20 补位/排队刷新仍来自 subagent-scheduler.mjs。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ASYNC_POOL_LIMITS, poolDomainOf, poolLimitsFor, runningPoolCount } from "./subagent-async.mjs"
|
|
9
|
+
import { runChildPipeline } from "./subagent-async.mjs"
|
|
10
|
+
import { logEvent } from "../log.mjs"
|
|
11
|
+
import { pushReal } from "../context.mjs"
|
|
12
|
+
import {
|
|
13
|
+
describeBlockers, refreshQueuedTokens,
|
|
14
|
+
} from "./subagent-scheduler.mjs"
|
|
15
|
+
// ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
|
|
16
|
+
import { buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SUBAGENT-OBSERVE-SEND D2:注入队列回合边界消费核心——把 entry._injected 全部消息按普通
|
|
20
|
+
* user 回合推入子代理历史(pushReal → 下轮 chat 即含该指令)并清空队列。独立导出供测试
|
|
21
|
+
* 直接锁该接缝(AC2——"子下回合边界收到并作普通 user 指令"的历史落点)。consumeInjected
|
|
22
|
+
* 闭包即调用本函数。N2:不打断在跑工具——本函数只在回合边界(agent.mjs 循环头)被调。
|
|
23
|
+
* @returns {number} 本次投递条数(空队列 0)
|
|
24
|
+
*/
|
|
25
|
+
export function drainInjectedQueue(entry, agent) {
|
|
26
|
+
const q = entry?._injected
|
|
27
|
+
if (!Array.isArray(q) || q.length === 0) return 0
|
|
28
|
+
const msgs = q.splice(0)
|
|
29
|
+
for (const m of msgs) pushReal(agent, { role: "user", content: String(m) })
|
|
30
|
+
return msgs.length
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Async branch (AGENT-LOOP.md §15 D-A1/D-A6): spawn without waiting.
|
|
35
|
+
* The child runs the EXACT blocking pipeline (runChildPipeline — relay /
|
|
36
|
+
* turn-cap / permission / MIN_REPORT_CHARS / mergeChildMutations all unchanged),
|
|
37
|
+
* but the parent does not await it: the promise is parked in _asyncSubagents and
|
|
38
|
+
* consumed by the auto channel (§19.8 — turn-end collection / suspension digest;
|
|
39
|
+
* the check action is gone). Slot queue (§24 D-24a/R14 — 分域): the entry carries a
|
|
40
|
+
* pool domain (_pool = poolDomainOf(role)); running count < limit[its domain]
|
|
41
|
+
* (agent.poolLimits — default engCoder 4 / other 4) → start now; ≥ → enqueue
|
|
42
|
+
* (status "queued", position = queue index) — never rejected, never requiring the
|
|
43
|
+
* model to batch. Domains never block each other (engCoder pool full ≠ explore queued).
|
|
44
|
+
* @returns {string} JSON ack {id, role, status: running|queued[, position][, waiting][, reason]}
|
|
45
|
+
*/
|
|
46
|
+
export function executeAsyncSpawn(parent, ctx, role, args, child, input, childOpts, childRunOpts, relayPrefix, childProvider, files, dependsOn) {
|
|
47
|
+
if ((ctx.depth ?? 0) > 0) {
|
|
48
|
+
throw new Error("async spawn only available at the top level")
|
|
49
|
+
}
|
|
50
|
+
parent._asyncSubagents ??= new Map()
|
|
51
|
+
parent._asyncQueue ??= []
|
|
52
|
+
const id = parent._subAgentCounter
|
|
53
|
+
const entry = {
|
|
54
|
+
id, role, relayPrefix,
|
|
55
|
+
// §24 D-24a/R14:池域字段(域判定单一事实源——poolDomainOf——role 枚举见
|
|
56
|
+
// subagent.mjs ROLES;未知角色归 other)——running 计数/补位按域过滤。
|
|
57
|
+
_pool: poolDomainOf(role),
|
|
58
|
+
status: "queued", // 下面按等待态/槽位重定(避免两处判断漂移)
|
|
59
|
+
position: undefined,
|
|
60
|
+
report: null, error: null, done: false, cancelled: false,
|
|
61
|
+
promise: null, _settle: null, _settleSeq: 0,
|
|
62
|
+
// §19.5 D-M5 可决策字段(status 数据装配锚点):model 在 spawn 时记录;
|
|
63
|
+
// startedAt 在 ACTUAL start(queued 等待不计 elapsed);turn/maxTurns 由
|
|
64
|
+
// 下方 onToken 拦截层从子代理 ⟦ev⟧turn 事件镜像(T-M18 正确性断言)。
|
|
65
|
+
model: childProvider?.model ?? null,
|
|
66
|
+
startedAt: null,
|
|
67
|
+
turn: 0, maxTurns: 0,
|
|
68
|
+
// §19.5 D-M6 (round2 #2):条目级 AbortController——cancel 定向 abort 本
|
|
69
|
+
// 条目(runAgent signal 链);Ctrl+C 全停语义不变(基信号 abort 逐链传播)。
|
|
70
|
+
controller: null,
|
|
71
|
+
// §20 D-SD2 域元数据(AGENT-LOOP.md §20):running ∪ queued 条目全带——
|
|
72
|
+
// _files(归一化绝对路径)/ _dependsOn(字符串 id)——冲突检测与补位判据
|
|
73
|
+
// 的事实源;无调度参数 spawn 两字段皆空(legacy——不参与冲突检测——零改动)。
|
|
74
|
+
_files: files,
|
|
75
|
+
_dependsOn: dependsOn,
|
|
76
|
+
_lastQueuedSig: null, // ⟦ev⟧queued 去重 sig(refreshQueuedTokens)
|
|
77
|
+
// SUBAGENT-OBSERVE-SEND D2:父侧 send 注入队列——父 action:'send' push 消息,子
|
|
78
|
+
// 回合边界经 consumeInjected 回调消费(drain + pushReal 成 user 回合);settle 收尾
|
|
79
|
+
// 时仍残留 = 未投递(消息入队后子在下一回合边界前 settle)→ 附 settle 报告提示。
|
|
80
|
+
_injected: [],
|
|
81
|
+
}
|
|
82
|
+
// §20 D-SD3 准入落点:等待态(依赖未满足/域冲突/depc)→ queued(waiting-deps——
|
|
83
|
+
// 不占槽不启动——即使槽空);纯槽满(kind slot)→ 按域计数判定(§24 D-24a:
|
|
84
|
+
// runningIn(domain) < limit(domain)——跨域互不阻塞——每次入池判定时读配置)。
|
|
85
|
+
const blockers = describeBlockers(parent, entry)
|
|
86
|
+
if (blockers.kind === "slot") {
|
|
87
|
+
const limits = poolLimitsFor(parent) // 运行期读 + 校验(非法键回退默认——T-24a4)
|
|
88
|
+
entry.status = runningPoolCount(parent, entry._pool) >= (limits[entry._pool] ?? ASYNC_POOL_LIMITS[entry._pool])
|
|
89
|
+
? "queued" : "running"
|
|
90
|
+
} else {
|
|
91
|
+
entry.status = "queued" // waiting-deps / dependency-cancelled——slot 空也不启动
|
|
92
|
+
}
|
|
93
|
+
// The settle signal — resolves when the run chain settles (never rejects).
|
|
94
|
+
entry.promise = new Promise((res) => { entry._settle = res })
|
|
95
|
+
// §19.5 D-M6:条目 controller 链到会话/回合基信号(_sessionSignal 优先——
|
|
96
|
+
// §17 挂起会话内 children 持会话 signal,digest 自身 Ctrl+C 不误伤)。
|
|
97
|
+
// D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md)。
|
|
98
|
+
const ctrl = new AbortController()
|
|
99
|
+
entry.controller = ctrl
|
|
100
|
+
const baseSignal = buildChildSignal(parent, ctx)
|
|
101
|
+
if (baseSignal) {
|
|
102
|
+
if (baseSignal.aborted) ctrl.abort()
|
|
103
|
+
else baseSignal.addEventListener("abort", () => ctrl.abort(), { once: true })
|
|
104
|
+
}
|
|
105
|
+
// §19.5 D-M5:turn 镜像拦截层(callbacks 包装层——选改动最小方案:在既有
|
|
106
|
+
// wrapChildCallbacks 之外再包一层,只解析 ⟦ev⟧turn 更新条目,其余原样转发)。
|
|
107
|
+
const trackOpts = { ...childOpts }
|
|
108
|
+
const parentOnToken = trackOpts.onToken
|
|
109
|
+
if (parentOnToken) {
|
|
110
|
+
trackOpts.onToken = (t) => {
|
|
111
|
+
const ev = String(t).match(/^⟦ev⟧turn\x1e(\d+)\x1e(\d+)\x1e/)
|
|
112
|
+
if (ev) {
|
|
113
|
+
entry.turn = Number(ev[1]) || 0
|
|
114
|
+
entry.maxTurns = Number(ev[2]) || 0
|
|
115
|
+
}
|
|
116
|
+
return parentOnToken(t)
|
|
117
|
+
}
|
|
118
|
+
// §27 R23 D-R23c1:嵌套 wrapper 标记随镜像层透传(wrapChildCallbacks 产物带
|
|
119
|
+
// _relayPrefix——本层重包后丢失会让异步子代理(默认 async——depth-0)内嵌 spawn
|
|
120
|
+
// 的生成侧补发射失效(emitNestedChildEvent 判据)——同步复制标记保语义)。
|
|
121
|
+
trackOpts.onToken._relayPrefix = parentOnToken._relayPrefix ?? null
|
|
122
|
+
}
|
|
123
|
+
entry.start = () => {
|
|
124
|
+
entry.status = "running"
|
|
125
|
+
entry.position = undefined
|
|
126
|
+
entry.startedAt = Date.now()
|
|
127
|
+
// §19.5.6 D-SF1(round3 #1):绑定子代理对象引用——绑定时刻 = 实际启动时
|
|
128
|
+
// (queued 条目 spawn-ack 时刻尚无子代理对象——§20 D-SD3b);绑定对象 = 子代理
|
|
129
|
+
// 对象(不是 _touchedFiles 数组引用——per-run 记账在 prepareRun 重置——数组
|
|
130
|
+
// 引用会陈旧——对象引用保证 status 查询时实时读——杀前一刻最新)。
|
|
131
|
+
entry.childAgent = child
|
|
132
|
+
// SUBAGENT-OBSERVE-SEND D2:子侧输入源贯通(设计"硬缺口"闭合)——把"注入队列消费
|
|
133
|
+
// 回调"经 childRunOpts 塞进 runAgent opts(runChildPipeline → runWithContinue →
|
|
134
|
+
// runAgent),子回合边界(agent.mjs 每轮开头的 consumeInjected?.() 点)消费
|
|
135
|
+
// entry._injected → pushReal 成 user 回合进子历史——当作普通用户指令处理。回调闭包
|
|
136
|
+
// 持 entry + child;空队列 no-op(每轮尝试——零开销)。N2:不打断在跑工具——入队
|
|
137
|
+
// 消息只在下一回合边界(当前工具完成后的下轮 chat 前)进上下文。
|
|
138
|
+
const consumeInjected = (ag) => { drainInjectedQueue(entry, ag ?? child) }
|
|
139
|
+
// §19.5 D-M7b ①: async 标记事件——零字段 ⟦ev⟧async token(sync 不发)。
|
|
140
|
+
// 锚点 = 实际启动(与 [model] 同步——queued 入队不 paint,补位启动才发);
|
|
141
|
+
// 先于 [model] 发出——区块创建即知 sub.async(routeSubToken 解析——
|
|
142
|
+
// ⏹ 门控与头标 async 的判定源)。父级直接 emit(depth-0 专属路径——
|
|
143
|
+
// 不经子代理文本 strip 白名单——与 ⟦ev⟧stopped/settled 同族)。
|
|
144
|
+
ctx.callbacks?.onToken?.(relayPrefix + "⟦ev⟧async\x1e")
|
|
145
|
+
// Deferred [model] emit: the TUI block is created at ACTUAL start.
|
|
146
|
+
ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (childProvider.model ?? ""))
|
|
147
|
+
// Turn-cap on background children NEVER pops the continue panel (D-A3):
|
|
148
|
+
// §15 D-A3 exception (2026-09-02 unified rule, AGENT-LOOP.md §2): in an
|
|
149
|
+
// engineering && AUTO session the child auto-resumes — the user authorized
|
|
150
|
+
// unattended runs, no one is at the panel. Every other tier auto-declines
|
|
151
|
+
// and the partial-work report carries the cap reason. §18 D-E2 relies on
|
|
152
|
+
// this exception as the turn-cap fallback for the default-async eng-coder
|
|
153
|
+
// delivery (the internal protocol does not raise the 100-turn cap).
|
|
154
|
+
runChildPipeline(child, input, trackOpts, { ...childRunOpts, signal: entry.controller.signal, consumeInjected }, {
|
|
155
|
+
parent, role, args,
|
|
156
|
+
askContinue: () => Promise.resolve(Boolean(parent.config?.agent?.engineering && parent.autoApprove)),
|
|
157
|
+
})
|
|
158
|
+
.then((report) => { entry.report = report })
|
|
159
|
+
.catch((err) => { entry.error = err?.message ?? String(err) })
|
|
160
|
+
.finally(() => {
|
|
161
|
+
// SUBAGENT-OBSERVE-SEND D3(send→settle 竞态):settle 收尾时 _injected 仍残留
|
|
162
|
+
// = 消息入队后子代理在下一回合边界前 settle——未投递——附 settle 报告/错误提示
|
|
163
|
+
// (防父误以为引导已落地)。error 路径附 error;报告路径附 report 尾。
|
|
164
|
+
const undelivered = Array.isArray(entry._injected) ? entry._injected.length : 0
|
|
165
|
+
if (undelivered > 0) {
|
|
166
|
+
const note = `\n[note: ${undelivered} message(s) queued via subagent action:'send' were NOT delivered — the subagent settled before its next turn boundary; re-spawn with the direction if it still applies.]`
|
|
167
|
+
if (entry.error != null) entry.error += note
|
|
168
|
+
else entry.report = `${entry.report ?? ""}${note}`
|
|
169
|
+
entry._injected = [] // 子已 settle——消费面终——清空防重复提示(提示已随报告携带)
|
|
170
|
+
}
|
|
171
|
+
// settle 公共收尾单点(ASYNC-RESULT-CONTAINER.md D3——settleAsyncEntry):日志三连
|
|
172
|
+
// (ev:cancelled/child:done|error/ev:settled)/cancelled 分支(出池+墓碑+⟦ev⟧stopped
|
|
173
|
+
// +提醒)/挂起分流(pending 单容器+出池——统一守卫 !parentAborted——D4)/公共尾部
|
|
174
|
+
// (settleSeq/_settle/唤醒 waiter + 腾槽补位——helper 尾部恒补——见 async-settle.mjs)。
|
|
175
|
+
settleAsyncEntry(parent, entry, {
|
|
176
|
+
pool: parent._asyncSubagents,
|
|
177
|
+
ctx,
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
parent._asyncSubagents.set(String(id), entry)
|
|
182
|
+
// LOGGING(LOGGING.md):child:spawn(async——注册即事件;status 记 queued/running 分流;
|
|
183
|
+
// 实际启动由补位 start() 触发——运行中由子内 llm/tool 事件可见)
|
|
184
|
+
logEvent("child:spawn", { role, id: `${role}#${id}`, kind: "async", status: entry.status, ms: 0 })
|
|
185
|
+
if (entry.status === "queued") {
|
|
186
|
+
parent._asyncQueue.push(entry)
|
|
187
|
+
entry.position = parent._asyncQueue.length
|
|
188
|
+
// §20 D-SD3b:排队 spawn 返回即建面板 waiting 块(⟦ev⟧queued 事件——spawn 侧
|
|
189
|
+
// 发——TUI routeSubToken 消费建块/更新头;启动后 ⟦ev⟧async 转 running——同 key
|
|
190
|
+
// 不重建)。refreshQueuedTokens 同时校正既有排队条目的位置/等待态头。
|
|
191
|
+
refreshQueuedTokens(parent, ctx.callbacks?.onToken)
|
|
192
|
+
const blk = describeBlockers(parent, entry)
|
|
193
|
+
const out = { id: String(id), role, status: "queued", position: entry.position }
|
|
194
|
+
if (blk.kind !== "slot") {
|
|
195
|
+
out.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
|
|
196
|
+
out.reason = blk.detail
|
|
197
|
+
}
|
|
198
|
+
return JSON.stringify(out)
|
|
199
|
+
}
|
|
200
|
+
entry.start()
|
|
201
|
+
return JSON.stringify({ id: String(id), role, status: "running" })
|
|
202
|
+
}
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* 内容:normalizeFileList / filesOverlap / depInfo / describeBlockers / detectStall(§21.1
|
|
6
6
|
* P-SL2 停滞机械检测)/ queueRunnable / assertNoDepCycle / dependentLabels /
|
|
7
7
|
* refreshQueuedTokens / maybeRefillAsync。
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* ASYNC_POOL_LIMITS/poolLimitsFor/runningPoolCount 回引自 subagent-async.mjs(主体保有——
|
|
9
|
+
* 仅调用期使用——与主体对此处调度符号的回引构成惰性环——无求值期依赖)。
|
|
10
10
|
*/
|
|
11
|
-
import { isAbsolute, relative, resolve } from "node:path"
|
|
11
|
+
import { basename, isAbsolute, relative, resolve } from "node:path"
|
|
12
12
|
import { existsSync, statSync } from "node:fs"
|
|
13
|
-
import {
|
|
13
|
+
import { poolLimitsFor, runningPoolCount, ASYNC_POOL_LIMITS } from "./subagent-async.mjs"
|
|
14
|
+
// ASYNC-RESULT-CONTAINER.md D1:池 accessor——async-settle.mjs 反向 import 本模块
|
|
15
|
+
// (dependentLabels)构成同款惰性环(函数级绑定——无求值期依赖)。
|
|
16
|
+
import { getAsyncPool } from "./async-settle.mjs"
|
|
14
17
|
|
|
15
18
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
19
|
// §20 子 agent 任务调度器(AGENT-LOOP.md §20——D-SD1..SD5 + 20.4 处置注)
|
|
@@ -18,7 +21,7 @@ import { ASYNC_SUBAGENT_LIMIT } from "./subagent-async.mjs"
|
|
|
18
21
|
// (D-SD3:域冲突/依赖未满足 → queued 等位)、补位扫描(D-SD4:最早可启动——
|
|
19
22
|
// 依赖全满足 + 域无冲突——waiting 越行不阻塞 slot 位)、释放规则(D-SD5——round2
|
|
20
23
|
// #3 锁定默认:依赖取消/失败 → 依赖者留 queued 标 dependency cancelled——仅父显式
|
|
21
|
-
// 处置或 AUTO 自动启动)、终态墓碑(round1 #8/T-SD14
|
|
24
|
+
// 处置或 AUTO 自动启动)、终态墓碑(round1 #8/T-SD14:自动通道注入消费与取消写墓碑——
|
|
22
25
|
// consumed 视为满足;非 consumed unknown id 才拒)。状态全部派生不存储(单点事实)。
|
|
23
26
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
27
|
|
|
@@ -27,9 +30,14 @@ import { ASYNC_SUBAGENT_LIMIT } from "./subagent-async.mjs"
|
|
|
27
30
|
* §20.8 D-F1.1(2026-09-04):目录声明检测——fail-closed——尾斜杠形态 / 指向既有目录
|
|
28
31
|
* → throw(含路径——错误字符串英文定稿)——目录声明静默绕过冲突检测的通道闭合;
|
|
29
32
|
* 调用方(subagent.mjs spawn 入口)catch → 错误即工具结果(模型可见——无静默)。
|
|
30
|
-
* 已知限制(§20.8 未编号段——评审 #4):不存在的目录声明(无尾斜杠 + 目录未创建)仍通过——不处理。
|
|
33
|
+
* 已知限制(§20.8 未编号段——评审 #4):不存在的目录声明(无尾斜杠 + 目录未创建)仍通过——不处理。
|
|
34
|
+
* §28 R26 F-R26b(2026-09-07):父侧维护文件黑名单——归一化后 basename 全名匹配 +
|
|
35
|
+
* 大小写不敏感(todo.md/changelog.md/checklist.md 精确 + checklist* 前缀家族——路径任意
|
|
36
|
+
* 层含 docs/、根、.thincoder/)——命中 → throw(fail-closed——先于调度器准入——无排队
|
|
37
|
+
* 残留——错误即工具结果);提示列出全部违规条目——英文模板逐字定稿(AGENT-LOOP.md §28)。 */
|
|
31
38
|
export function normalizeFileList(files, cwd) {
|
|
32
39
|
const out = []
|
|
40
|
+
const violations = [] // §28 R26 F-R26b:父侧维护文件命中(保留声明原样——提示可读)
|
|
33
41
|
for (const f of Array.isArray(files) ? files : []) {
|
|
34
42
|
if (typeof f !== "string" || !f.trim()) continue
|
|
35
43
|
if (f.endsWith("/") || f.endsWith("\\")) {
|
|
@@ -39,8 +47,17 @@ export function normalizeFileList(files, cwd) {
|
|
|
39
47
|
if (existsSync(abs) && statSync(abs).isDirectory()) {
|
|
40
48
|
throw new Error(`files must be file-level paths — directory declarations are not supported: ${f}`)
|
|
41
49
|
}
|
|
50
|
+
// 黑名单 basename 推导前做分隔符归一(\ → /——反斜杠变体跨平台同拒——
|
|
51
|
+
// win32 resolve 本就兼容双分隔符——POSIX 需显式归一——advisor 🔵2 处置)
|
|
52
|
+
const base = basename(resolve(cwd ?? process.cwd(), f.replace(/\\/g, "/"))).toLowerCase()
|
|
53
|
+
if (base === "todo.md" || base === "changelog.md" || base.startsWith("checklist")) violations.push(f)
|
|
42
54
|
if (!out.includes(abs)) out.push(abs)
|
|
43
55
|
}
|
|
56
|
+
if (violations.length > 0) {
|
|
57
|
+
throw new Error(violations
|
|
58
|
+
.map((f) => `Parent-side maintained file ${f} must not be listed in files — reconciliation is the parent's duty; use the design-doc path if you need to edit a design doc`)
|
|
59
|
+
.join("\n"))
|
|
60
|
+
}
|
|
44
61
|
return out
|
|
45
62
|
}
|
|
46
63
|
|
|
@@ -65,20 +82,22 @@ function showFile(parent, key) {
|
|
|
65
82
|
|
|
66
83
|
/**
|
|
67
84
|
* §20 依赖终态查询(单点事实——池条目 / pending(挂起期 settle 移交——注入前)/
|
|
68
|
-
*
|
|
69
|
-
* - ok = settle 成功(报告已产出)/ consumed
|
|
85
|
+
* 终态墓碑(自动通道注入消费——consumed;取消/失败——D-SD5 分支)):
|
|
86
|
+
* - ok = settle 成功(报告已产出)/ consumed(自动通道注入消费——T-SD14 视为满足)
|
|
70
87
|
* - pending = running/queued 未终态(等启动/等完成)
|
|
71
88
|
* - failed / cancelled = 终态但非成功——依赖者走 dependency cancelled 分支(round2 #3)
|
|
72
89
|
* - unknown = 从未存在(spawn 时明确错误——非 consumed 的 unknown 拒——T-SD10)
|
|
73
90
|
*/
|
|
74
91
|
export function depInfo(parent, id) {
|
|
75
92
|
const key = String(id)
|
|
76
|
-
const e = parent
|
|
93
|
+
const e = getAsyncPool(parent, "subagent")?.get(key)
|
|
77
94
|
if (e) {
|
|
78
95
|
if (e.cancelled) return { state: "cancelled", role: e.role }
|
|
79
96
|
if (e.done) return e.error != null ? { state: "failed", role: e.role } : { state: "ok", role: e.role }
|
|
80
97
|
return { state: "pending", role: e.role }
|
|
81
98
|
}
|
|
99
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一停靠——依赖目标挂起期 settle
|
|
100
|
+
// 移交也在此——注入前视为已消费)。
|
|
82
101
|
const pend = (parent._pendingAsyncResults ?? []).find((x) => String(x.id) === key)
|
|
83
102
|
if (pend) return pend.error != null ? { state: "failed", role: pend.role } : { state: "ok", role: pend.role }
|
|
84
103
|
const t = parent._asyncTombstones?.get(key)
|
|
@@ -141,8 +160,8 @@ export function describeBlockers(parent, entry) {
|
|
|
141
160
|
* 会启动——非停滞)。自然流程中 wait 边恒指向先入者(依赖必须先前 spawn——unknown 拒 +
|
|
142
161
|
* 文件冲突只阻断后入者——id 序)——混合环仅人工注入可构造(T-SD5 同族防御断言)——
|
|
143
162
|
* 但状态既可能成环即机械可检——不依赖可达性论证。maybeRefillAsync 空转处不调用:refill
|
|
144
|
-
* 嵌在 settle/cancel 链上无模型可见输出通道(抛错即破坏池操作)——
|
|
145
|
-
*
|
|
163
|
+
* 嵌在 settle/cancel 链上无模型可见输出通道(抛错即破坏池操作)——status 视图(actions
|
|
164
|
+
* 模块)即 F-SL2 的报错/标记落点(§19.8:check 守卫随 check 删除)。 */
|
|
146
165
|
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
166
|
|
|
148
167
|
export function detectStall(parent) {
|
|
@@ -280,7 +299,7 @@ export function dependentLabels(parent, depId) {
|
|
|
280
299
|
/** §20 D-SD3b 排队态面板刷新(⟦ev⟧queued 事件族——TUI routeSubToken 消费):对全部
|
|
281
300
|
* queued 条目重算等待态并发射变化(去重 sig——kind/position/detail 全变才发)——
|
|
282
301
|
* 调用点 = 一切队列突变与等待态变迁(spawn 入队 / settle 后补位与依赖转移 / cancel
|
|
283
|
-
* 出队 /
|
|
302
|
+
* 出队 / 自动通道消费)。position = 队列序(D-A1 既有——cancel 前移同源)。 */
|
|
284
303
|
export function refreshQueuedTokens(parent, onToken) {
|
|
285
304
|
if (typeof onToken !== "function") return
|
|
286
305
|
const queue = parent._asyncQueue ?? []
|
|
@@ -297,21 +316,26 @@ export function refreshQueuedTokens(parent, onToken) {
|
|
|
297
316
|
}
|
|
298
317
|
|
|
299
318
|
/**
|
|
300
|
-
* Slot-queue refill (AGENT-LOOP.md §15 D-A1/D-A6 + §20 D-SD4
|
|
301
|
-
* while a running slot is free — called from every settle
|
|
302
|
-
* and from the turn-end collection's refill loop. §20
|
|
303
|
-
* slot-queued——扫描选"依赖全满足 + 域无冲突"
|
|
304
|
-
*
|
|
305
|
-
*
|
|
319
|
+
* Slot-queue refill (AGENT-LOOP.md §15 D-A1/D-A6 + §20 D-SD4 + §24 D-24a/R14 分域):
|
|
320
|
+
* start queue heads while a running slot is free — called from every settle
|
|
321
|
+
* (completion frees a slot) and from the turn-end collection's refill loop. §20:
|
|
322
|
+
* 队列现可混合 waiting-deps 与 slot-queued——扫描选"依赖全满足 + 域无冲突"的最早
|
|
323
|
+
* 条目启动(waiting 越行不阻塞槽位;多任务同时解除按 queued 序逐个启动)。
|
|
324
|
+
* §24 D-24a(R14):槽位判定按域——条目 _pool 域内 running 数 < 该域上限才启动
|
|
325
|
+
* (同域仍 4——纯单域队列行为与旧 shift 完全一致;跨域总量 8——各域独立腾槽,
|
|
326
|
+
* 互不阻塞)。配置每次补位时读(poolLimitsFor——变更即生效下个补位)。
|
|
306
327
|
*/
|
|
307
328
|
export function maybeRefillAsync(parent) {
|
|
308
329
|
const queue = parent._asyncQueue ?? []
|
|
309
330
|
for (;;) {
|
|
310
|
-
const running = [...(parent._asyncSubagents?.values() ?? [])].filter((e) => e.status === "running").length
|
|
311
|
-
if (running >= ASYNC_SUBAGENT_LIMIT) return
|
|
312
331
|
let pick = -1
|
|
313
332
|
for (let i = 0; i < queue.length; i++) {
|
|
314
|
-
|
|
333
|
+
const e = queue[i]
|
|
334
|
+
if (!queueRunnable(parent, e)) continue
|
|
335
|
+
const pool = e._pool ?? "other" // 缺域字段(手工/旧条目)按 other——既有测试语义不变
|
|
336
|
+
if (runningPoolCount(parent, pool) >= (poolLimitsFor(parent)[pool] ?? ASYNC_POOL_LIMITS[pool])) continue
|
|
337
|
+
pick = i
|
|
338
|
+
break
|
|
315
339
|
}
|
|
316
340
|
if (pick < 0) return
|
|
317
341
|
queue.splice(pick, 1)[0].start()
|