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
|
@@ -2,67 +2,24 @@
|
|
|
2
2
|
* agent-tools/advisor.mjs — advisor tool wrapper.
|
|
3
3
|
* The agent calls this explicitly to get an independent review.
|
|
4
4
|
* type="design" for design doc review, type="code" for code review (default).
|
|
5
|
+
* §24 D-24b (R13 — async advisor): at depth 0 the review launches into the
|
|
6
|
+
* background pool by DEFAULT (async:true / omitted; async:false forces the
|
|
7
|
+
* blocking review); depth>0 (eng-coder self-review) stays synchronous always.
|
|
5
8
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { runAdvisorReview } from "../advisor/run.mjs"
|
|
9
|
+
import { runAdvisorReview, MAX_ADVISOR_ROUNDS, buildCapMessage } from "../advisor/run.mjs"
|
|
8
10
|
import { isDocFile } from "../advisor/repos.mjs"
|
|
11
|
+
import {
|
|
12
|
+
generateDesignToken,
|
|
13
|
+
settleDesignReview,
|
|
14
|
+
resolveAdvisorLaunch,
|
|
15
|
+
launchAsyncAdvisor,
|
|
16
|
+
stripApprovedSuffix,
|
|
17
|
+
} from "./advisor-async.mjs"
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* invalid values fall back to the default, never silently disable the ceiling). */
|
|
15
|
-
function effectiveTokenTtlMs(agent) {
|
|
16
|
-
const cfg = agent?.config?.agent?.engTokenTtlMs
|
|
17
|
-
return (Number.isFinite(cfg) && cfg > 0) ? cfg : TOKEN_TTL_DEFAULT_MS
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Generate a signed design token with expiration */
|
|
21
|
-
function generateDesignToken(agent) {
|
|
22
|
-
const uuid = randomUUID()
|
|
23
|
-
const expiresAt = Date.now() + effectiveTokenTtlMs(agent)
|
|
24
|
-
const payload = `${uuid}:${expiresAt}`
|
|
25
|
-
const signature = createHmac("sha256", TOKEN_SECRET).update(payload).digest("hex").slice(0, 16)
|
|
26
|
-
return `${payload}:${signature}`
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Validate design token: format, expiration, signature — ALL fail-closed (v2 2026-08-25).
|
|
30
|
-
* The two legacy fail-open branches (parts!=3 → true, NaN expiry → true) were pass-through
|
|
31
|
-
* backdoors: any malformed string bypassed validation. Only exact signed tokens pass now.
|
|
32
|
-
* Format must be exactly uuid:expiresAt:hmacSig. */
|
|
33
|
-
export function validateDesignToken(token) {
|
|
34
|
-
if (!token || typeof token !== "string") return false
|
|
35
|
-
|
|
36
|
-
// New format: uuid:expiresAt:signature (3 parts separated by ':')
|
|
37
|
-
const parts = token.split(":")
|
|
38
|
-
if (parts.length !== 3) return false // fail-closed (was: return true)
|
|
39
|
-
|
|
40
|
-
const [uuid, expiresAt, signature] = parts
|
|
41
|
-
const expTime = parseInt(expiresAt, 10)
|
|
42
|
-
if (isNaN(expTime)) return false // fail-closed (was: return true)
|
|
43
|
-
|
|
44
|
-
// Check expiration
|
|
45
|
-
if (Date.now() > expTime) return false
|
|
46
|
-
|
|
47
|
-
// Check signature
|
|
48
|
-
const payload = `${uuid}:${expiresAt}`
|
|
49
|
-
const expectedSig = createHmac("sha256", TOKEN_SECRET).update(payload).digest("hex").slice(0, 16)
|
|
50
|
-
return signature === expectedSig
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Build a [DESIGN-TOKEN:...] regex; escapes special chars as a safety net.
|
|
54
|
-
* Matches the FULL token (uuid:expiresAt:signature) — prompt tells advisor
|
|
55
|
-
* to echo the complete token verbatim, not just the UUID segment.
|
|
56
|
-
* Flexible matching: allows token to be on its own line, in a code block,
|
|
57
|
-
* or surrounded by whitespace. */
|
|
58
|
-
const makeDesignTokenRegex = (token, flags = "") => {
|
|
59
|
-
// Escape the entire token, not just UUID — advisor echoes [DESIGN-TOKEN:uuid:expiresAt:signature]
|
|
60
|
-
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
61
|
-
return new RegExp(
|
|
62
|
-
`(?:^|\\s|\`|\\*)\\[DESIGN-TOKEN:\\s*${escaped}\\s*\\](?:\\s|$|\`|\\*)`,
|
|
63
|
-
flags + "ms"
|
|
64
|
-
)
|
|
65
|
-
}
|
|
19
|
+
// Design-token utilities moved to advisor-async.mjs (the async settle shares
|
|
20
|
+
// them — no wrapper↔runner module cycle); validateDesignToken stays exported
|
|
21
|
+
// here for the tests' import surface (implementation re-exported).
|
|
22
|
+
export { validateDesignToken } from "./advisor-async.mjs"
|
|
66
23
|
|
|
67
24
|
export const advisorTool = {
|
|
68
25
|
name: "advisor",
|
|
@@ -81,11 +38,21 @@ export const advisorTool = {
|
|
|
81
38
|
"Optionally pass object={type,target,status,reason,exclude} to anchor the review target " +
|
|
82
39
|
"(AGENT-LOOP.md §18.8 — the review-object declaration is mechanically injected into the review message); " +
|
|
83
40
|
"absent → legacy behavior (no injection). " +
|
|
41
|
+
"ASYNC (AGENT-LOOP.md §24 D-24b): at depth 0 the review runs in the BACKGROUND by default " +
|
|
42
|
+
"(async:true or omitted) — the call returns an ack immediately, the turn ends, and the report " +
|
|
43
|
+
"arrives automatically in a digest turn when the review finishes; at most 2 reviews run in " +
|
|
44
|
+
"parallel (excess launches are refused — launch one at a time). Pass async:false to force the " +
|
|
45
|
+
"blocking review (result returned inline). Inside a child (depth>0 — eng-coder self-review) " +
|
|
46
|
+
"reviews are always synchronous; async:true is rejected there. " +
|
|
84
47
|
"Returns the review report — the advisor's findings verdict: all-clear (call verify) or a findings list to fix.",
|
|
85
48
|
parameters: {
|
|
86
49
|
type: "object",
|
|
87
50
|
properties: {
|
|
88
51
|
type: { type: "string", enum: ["code", "design"], description: "Review type: 'design' for design doc review, 'code' for code review (default)" },
|
|
52
|
+
async: {
|
|
53
|
+
type: "boolean",
|
|
54
|
+
description: "Background review: default at depth 0 = true (async — ack now, report via digest); async:false forces the blocking review. depth>0 → always sync (async:true rejected).",
|
|
55
|
+
},
|
|
89
56
|
object: {
|
|
90
57
|
type: "object",
|
|
91
58
|
properties: {
|
|
@@ -129,6 +96,7 @@ export const advisorTool = {
|
|
|
129
96
|
|
|
130
97
|
// Code review must have a scope — no implicit fallback.
|
|
131
98
|
if (reviewType !== "design" && !paths && !documents) {
|
|
99
|
+
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
132
100
|
return "Advisor: no review scope specified. Provide paths (files/directories to review) or documents (acceptance criteria context)."
|
|
133
101
|
}
|
|
134
102
|
|
|
@@ -140,69 +108,103 @@ export const advisorTool = {
|
|
|
140
108
|
return !isDocFile(doc)
|
|
141
109
|
})
|
|
142
110
|
if (invalidDocs.length > 0) {
|
|
111
|
+
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
143
112
|
return `Advisor: design review documents must be in docs/ directory or be recognized doc files. Invalid: ${invalidDocs.join(", ")}`
|
|
144
113
|
}
|
|
145
114
|
}
|
|
146
115
|
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
116
|
+
// §24 D-24b (R13 — ruling ②-3 A): async gate. Depth-0 defaults to the
|
|
117
|
+
// background pool; depth>0 (eng-coder internal self-review) is ALWAYS sync —
|
|
118
|
+
// an explicit async:true there is rejected, the default never flips.
|
|
119
|
+
const depth = ctx?.depth
|
|
120
|
+
if (args.async === true && depth !== 0) {
|
|
121
|
+
// 拒发登记(与 cap/池满拒同款):评审未跑——不置 called/不耗轮次(record-results
|
|
122
|
+
// 的 REFUSED 契约——advisor 评审发现 #1:拒发不得静默满足 guard)。
|
|
123
|
+
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
124
|
+
return "Advisor: async reviews are only available at depth 0 — the top-level session owns the background pool (AGENT-LOOP.md §24 D-24b); inside a child (eng-coder self-review) reviews run synchronously. Call advisor again without async:true (or with async:false)."
|
|
125
|
+
}
|
|
126
|
+
const isAsync = args.async === true || (depth === 0 && args.async !== false)
|
|
127
|
+
// (ctx.depth undefined = direct callers/tests without a dispatch context —
|
|
128
|
+
// legacy sync semantics.)
|
|
151
129
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
//
|
|
130
|
+
// Per-review instance resolution (§24 D-24b ③ — ruling ②-5 A): fix rounds
|
|
131
|
+
// continue the same reviewId (design = the doc-set instance's designId —
|
|
132
|
+
// slot/spawn continuity; code = the newest OPEN instance). The resolution
|
|
133
|
+
// scopes agent._advisorRound/_lastAdvisorOutput so the message builder and
|
|
134
|
+
// the run.mjs cap read THIS instance's round/prior (multi-review isolation).
|
|
135
|
+
const resolved = resolveAdvisorLaunch(agent, reviewType, { documents })
|
|
136
|
+
// Design token minted for EVERY design round — the reviewer echoes it only on
|
|
137
|
+
// a clean pass; on pass it is slotted under the instance's designId at settle
|
|
138
|
+
// (sync: right here; async: the settle callback — fix #2). A NEW instance
|
|
139
|
+
// gets a fresh designId; a continued fix round keeps the original one — and a
|
|
140
|
+
// same-scope re-review after a pass REUSES the session's id (F2h §29.1).
|
|
159
141
|
const designToken = reviewType === "design" ? generateDesignToken(agent) : null
|
|
160
|
-
const designId = reviewType === "design" ?
|
|
142
|
+
const designId = reviewType === "design" ? resolved.designId : null
|
|
143
|
+
|
|
144
|
+
// Cap pre-check (T-24b11 — per-review ≤5 rounds, CODE REVIEWS ONLY): a 6th
|
|
145
|
+
// launch of a capped CODE instance is refused synchronously — the review
|
|
146
|
+
// never starts (sync and async alike; runAdvisorReview's own cap check stays
|
|
147
|
+
// for legacy direct callers). DESIGN reviews are EXEMPT (2026-09-07 §8
|
|
148
|
+
// ruling): their rounds keep advancing (ROUND2/3 convergence prompts + TUI
|
|
149
|
+
// round display) but the cap never refuses them. The refusal marks no
|
|
150
|
+
// called/round state (guard keeps pushing only while a review can still run
|
|
151
|
+
// — at the cap the round check stops it).
|
|
152
|
+
if (resolved.run.reviewType !== "design" && resolved.run.round >= MAX_ADVISOR_ROUNDS) {
|
|
153
|
+
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
154
|
+
return buildCapMessage(agent)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isAsync) {
|
|
158
|
+
const ack = launchAsyncAdvisor(agent, ctx, {
|
|
159
|
+
reviewType, documents, paths, object: reviewObject,
|
|
160
|
+
designToken, designId, run: resolved.run,
|
|
161
|
+
})
|
|
162
|
+
if (ack.error) {
|
|
163
|
+
// Pool-full refusal (②-6a — no queueing): the review did NOT launch — the
|
|
164
|
+
// model must not count it as "advisor called" (the guard keeps pushing).
|
|
165
|
+
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
166
|
+
return ack.error
|
|
167
|
+
}
|
|
168
|
+
// Async-ack marker: recordToolResults must NOT do the launch-time
|
|
169
|
+
// accounting (called/round) for this call — the settle owns it.
|
|
170
|
+
if (ctx._toolCallId !== undefined) {
|
|
171
|
+
(agent._advisorAsyncAcks ??= new Set()).add(ctx._toolCallId)
|
|
172
|
+
}
|
|
173
|
+
return JSON.stringify({
|
|
174
|
+
id: ack.id, kind: "advisor", status: "running",
|
|
175
|
+
reviewId: resolved.reviewId,
|
|
176
|
+
note: "评审已后台启动——完成自动回来 (review started in the background — the report arrives in a digest turn automatically; pass this id to cancel if needed)",
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Sync path (depth>0 / explicit async:false / direct callers without depth):
|
|
181
|
+
// legacy blocking review. The design token/echo handling runs below; the
|
|
182
|
+
// instance accounting (round++ per completed attempt) lands in
|
|
183
|
+
// recordToolResults, marker-keyed by this tool call's id.
|
|
184
|
+
if (ctx._toolCallId !== undefined) {
|
|
185
|
+
(agent._advisorSyncCalls ??= new Map()).set(ctx._toolCallId, resolved.reviewId)
|
|
186
|
+
}
|
|
161
187
|
const result = await runAdvisorReview(agent, reviewType, {
|
|
162
188
|
onOutput: ctx.onOutput,
|
|
163
189
|
signal: ctx.signal,
|
|
164
|
-
}, designToken, documents, paths, reviewObject)
|
|
190
|
+
}, designToken, documents, paths, reviewObject, designId)
|
|
165
191
|
|
|
166
192
|
if (reviewType === "design") {
|
|
167
|
-
//
|
|
168
|
-
// The token IS the verdict — the advisor echoes it only on approval (prompt-enforced);
|
|
193
|
+
// Design pass/fail settlement — token echo IS the verdict (prompt-enforced);
|
|
169
194
|
// no findings-table heuristics: a design with issues never carries the token.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// an eng-coder whose own design review passed may write files without the
|
|
182
|
-
// parent spawn-time authorization. NOTE: unreachable today — eng-coder.md
|
|
183
|
-
// tells the child not to re-run the design review, and spawn already sets
|
|
184
|
-
// _engDesignReviewed (subagent.mjs). Kept as defense-in-depth for a future
|
|
185
|
-
// eng-coder autonomous design-revision entry. Parent agents (role undefined)
|
|
186
|
-
// don't use this flag — their runAgent resets it anyway; they are trusted
|
|
187
|
-
// via the engineering prompt.
|
|
188
|
-
if (agent._role === "eng-coder") agent._engDesignReviewed = true
|
|
189
|
-
// Strip the bracketed token so only ONE unambiguous format (plain UUID) reaches the main agent
|
|
190
|
-
const cleanResult = result.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
191
|
-
// designId rides the Approved block (review #1): the parent needs it to aim the FIRST
|
|
192
|
-
// eng-coder spawn when several designs live in the same session.
|
|
193
|
-
return `${cleanResult}\n\nApproved. Pass this exact token to eng-coder (designToken parameter): ${designToken}\ndesignId: ${designId} (pass as the designId parameter when spawning eng-coder; optional while this session holds a single design)`
|
|
194
|
-
}
|
|
195
|
-
// Review failed (or advisor chose not to pass) → do NOT touch ANY slot (方案 ②, review #2:
|
|
196
|
-
// a failed RE-review leaves the previously approved token alive until TTL; the failed call's
|
|
197
|
-
// own designId was never stored, so there is nothing to clear). Isolation (2026-08-30,
|
|
198
|
-
// extended to the multi-slot Map 2026-09-01): a network glitch must not clear / other
|
|
199
|
-
// designs' slots must not be affected — only a COMPLETED non-passing review lands here,
|
|
200
|
-
// and it revokes nothing.
|
|
201
|
-
// Strip every dead token occurrence from the raw output so the main agent can't grab an invalid one
|
|
202
|
-
if (result) {
|
|
203
|
-
const stripped = result.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
204
|
-
return stripped || "Advisor: design review did not pass."
|
|
195
|
+
// (session cleanup for design reviews is owned by runAdvisorReview)
|
|
196
|
+
// Multi-design slots (2026-09-01): store under this review's designId.
|
|
197
|
+
// DESIGN-TOKEN-SETTLEMENT D3 (2026-09-08): the single `_engDesignToken` mirror is
|
|
198
|
+
// retired — no mirror write here; the dispatch/spawn gates read the authoritative
|
|
199
|
+
// multi-slot Map (+ slot-file re-read). Slotting moved into settleDesignReview
|
|
200
|
+
// (shared with the async settle — fix #2).
|
|
201
|
+
const settled = settleDesignReview(agent, resolved.run, designToken, result)
|
|
202
|
+
// F2e (§29.1): the sync prior mirror must not carry the raw echo the runner
|
|
203
|
+
// stored — overwrite with the clean settled form (exact-suffix truncation).
|
|
204
|
+
if (settled.passed) {
|
|
205
|
+
agent._lastAdvisorOutput = stripApprovedSuffix(settled.output, resolved.run.approvedSuffix)
|
|
205
206
|
}
|
|
207
|
+
return settled.output
|
|
206
208
|
}
|
|
207
209
|
return result
|
|
208
210
|
},
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* async-settle.mjs — async 结果容器统一共享 helper(ASYNC-RESULT-CONTAINER.md D1-D6,
|
|
3
|
+
* 2026-09-08——CLI 端落地;VS Code 同名机制各自独立实现)。根治四族 settle 记账逐字
|
|
4
|
+
* 重复(subagent/advisor/escalate/consult)+ pending 三族分叉(_pendingEscalateResults/
|
|
5
|
+
* _pendingConsultResults 独立族废弃)+ done-in-pool 三表示 + _sessionSignal 兜底抄 3 处
|
|
6
|
+
* (consult 无兜底):
|
|
7
|
+
* - D1 池 accessor:`getAsyncPool(parent, role)`——role "advisor" 返 `_asyncAdvisors`,
|
|
8
|
+
* 其余返 `_asyncSubagents`——底层保留双池(advisor 无队列独立调度),accessor 吸收差异。
|
|
9
|
+
* - D2 pending 单容器:全族统一 `_pendingAsyncResults`,条目带 role 字段(consult 升格
|
|
10
|
+
* 完整 entry);`parkAsyncPending` 统一停靠(`_inPending` 标记保留防重复移交——settle
|
|
11
|
+
* 回调与 sweepSettledToPending 同一表示,done-in-pool 统一:留池 done:true + pending
|
|
12
|
+
* 单容器)。
|
|
13
|
+
* - D3 settle 共享 helper:`settleAsyncEntry(parent, entry, {pool, ctx, onAccounting})`——
|
|
14
|
+
* 公共收尾单点(日志三连(ev:cancelled / child:done|error + ev:settled)→ cancelled /
|
|
15
|
+
* parentAborted / 挂起分流 → settleSeq/`_settle` 唤醒 waiter)。族特有段作 onAccounting
|
|
16
|
+
* hook 注入(advisor 陈旧判定/token D1 落盘记账;escalate 三分类 merge 决策 + 腾槽补位)。
|
|
17
|
+
* - D4 守卫统一:`parentAborted`(= `ctx.signal?.aborted || entry.controller?.signal?.
|
|
18
|
+
* aborted`——escalate 严格版)——日志三连与挂起分流共用同一守卫。
|
|
19
|
+
* - D6 buildChildSignal:`parent._sessionSignal ?? ctx.signal ?? null` 单点(原 3 处
|
|
20
|
+
* 逐字抄;consult 补 _sessionSignal 兜底——D5)。
|
|
21
|
+
* 模块图:单向 import subagent-scheduler.mjs(dependentLabels——cancelled 提醒依赖者
|
|
22
|
+
* 列表)+ log/context/helpers/spawn-child——四族 settle 回调(subagent-run/advisor-async/
|
|
23
|
+
* escalate-async/consult)单向 import 本模块;scheduler 反向 import 本模块(getAsyncPool)
|
|
24
|
+
* 与既有 scheduler ↔ subagent-async 同款惰性环——无求值期依赖。
|
|
25
|
+
*/
|
|
26
|
+
import { logEvent, errText } from "../log.mjs"
|
|
27
|
+
import { pushReal } from "../context.mjs"
|
|
28
|
+
import { escapeXml } from "../agent/helpers.mjs"
|
|
29
|
+
import { TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
30
|
+
import { dependentLabels, maybeRefillAsync, refreshQueuedTokens } from "./subagent-scheduler.mjs"
|
|
31
|
+
|
|
32
|
+
// ─── D1 池 accessor(吸收双池)────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** 池 accessor(D1):role "advisor" → `_asyncAdvisors`(独立评审池——无队列独立调度);
|
|
35
|
+
* 其余角色 → `_asyncSubagents`(子代理/飞刀共享槽位队列池)。未初始化返 null——调用方
|
|
36
|
+
* 以 `?? new Map()` / 可选链处置。 */
|
|
37
|
+
export function getAsyncPool(parent, role) {
|
|
38
|
+
if (role === "advisor") return parent?._asyncAdvisors ?? null
|
|
39
|
+
return parent?._asyncSubagents ?? null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── D2 pending 单容器(+role)───────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** pending 单容器停靠(settle 挂起分流 / sweep 补扫共用——统一表示:条目置 `_inPending`
|
|
45
|
+
* 防重复移交(sweep 幂等判据),includes 去重兜底)。 */
|
|
46
|
+
export function parkAsyncPending(parent, entry) {
|
|
47
|
+
const pend = (parent._pendingAsyncResults ??= [])
|
|
48
|
+
if (pend.includes(entry)) return
|
|
49
|
+
entry._inPending = true
|
|
50
|
+
pend.push(entry)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── D4 守卫统一(严格版)+ D6 信号兜底单点 ──────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/** D4 settle 守卫统一(escalate 严格版):父侧中止 = 回合 signal aborted 或条目
|
|
56
|
+
* controller aborted(取消先行——cancelled 分支在守卫之前分流,定向 cancel 不算父中止)。
|
|
57
|
+
* 日志三连与挂起分流共用本守卫。 */
|
|
58
|
+
export function parentAborted(ctx, entry) {
|
|
59
|
+
return Boolean(ctx?.signal?.aborted || entry?.controller?.signal?.aborted)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** D6 child signal 构建单点(D5——consult 补 _sessionSignal 兜底):挂起会话内的回合
|
|
63
|
+
* (digest/用户回合)子代理持会话 signal(`parent._sessionSignal`——会话 Stop 逐链中止,
|
|
64
|
+
* 消化轮自身 Ctrl+I/Ctrl+C 不误伤);回合级 `ctx.signal` 兜底。原 subagent/advisor/
|
|
65
|
+
* escalate 3 处逐字抄 + consult 补上统一入此。 */
|
|
66
|
+
export function buildChildSignal(parent, ctx) {
|
|
67
|
+
return parent?._sessionSignal ?? ctx?.signal ?? null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── D3 settle 共享 helper ───────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* D3 settle 共享 helper——四族 settle 回调公共收尾单点(ASYNC-RESULT-CONTAINER.md
|
|
74
|
+
* AC1:settle 记账单点,无逐字重复)。流程:
|
|
75
|
+
* ① 落 done/status="done"(settle 即翻——不再占槽/持文件域;report/error 由各族
|
|
76
|
+
* 在调用前落好——subagent 的未投递注记先于本调用附加);
|
|
77
|
+
* ② 日志三连(统一守卫 !parentAborted——cancelled → ev:cancelled;非中止 →
|
|
78
|
+
* child:done|:error(族形态:advisor 合并单事件;escalate 撞 cap 归 partial)+
|
|
79
|
+
* 挂起期 ev:settled;consult 族跳过——per-model 日志已在子代理 settle 时记录);
|
|
80
|
+
* ③ 分流:cancelled(出池 + cancelled 墓碑 + ⟦ev⟧stopped + 族提醒——不入 pending)→
|
|
81
|
+
* parentAborted(守卫抑制——不落事件不入流——中止清池在回合尾/挂起中止统一做)→
|
|
82
|
+
* settled(族 onAccounting hook 记账 → 挂起期(或 consult 族恒停靠)移交 pending
|
|
83
|
+
* 单容器 + 出池 + ⟦ev⟧settled;回合内 ⟦ev⟧done 留池 done:true);
|
|
84
|
+
* ④ 公共尾部:settleSeq 递增 + `_settle` 唤醒 waiter + 腾槽补位(subagent/escalate 族
|
|
85
|
+
* 恒补——settle/cancel 释放槽 → maybeRefillAsync + refreshQueuedTokens——AGENT-LOOP.md
|
|
86
|
+
* §10 "settle/cancel 释放槽后…启动到槽满";advisor(独立评审池——无队列)/consult
|
|
87
|
+
* (会话池)豁免)。
|
|
88
|
+
* opts(设计签名 `{pool, onAccounting}` + 实现参数 ctx):
|
|
89
|
+
* - pool:条目所在池 Map(出池 delete 目标;consult 传 null——会话池无条目)。
|
|
90
|
+
* - ctx:回合上下文(守卫读 ctx.signal;⟦ev⟧ 事件经 ctx.callbacks.onToken;腾槽补位
|
|
91
|
+
* 的 refreshQueuedTokens 也经 ctx.callbacks.onToken;consult 传 null——无 TUI 事件
|
|
92
|
+
* 无守卫)。
|
|
93
|
+
* - onAccounting(parent, entry):族特有 hook——advisor 调 settleAdvisorRun 记账(D1
|
|
94
|
+
* 落盘保留);escalate 调 classifyEscalateSettle(三分类 merge 决策)。
|
|
95
|
+
* 设计偏差注(advisor code review 发现——交付报告同步):设计 D3 把 maybeRefillAsync
|
|
96
|
+
* 归入 onAccounting hook(仅 settled 分支执行)——running 取消的 cancelled 分支将不再
|
|
97
|
+
* 补位(槽释放但 queued 头停滞——挂起会话无回合尾补位覆盖),违背既有不变式
|
|
98
|
+
* (AGENT-LOOP.md §10 + subagent-scheduler maybeRefillAsync 文档 "called from every
|
|
99
|
+
* settle")与 VSC 镜像(无条件 refill)。修正:腾槽补位移入公共尾部(subagent/escalate
|
|
100
|
+
* 族恒补——旧行为零回归;advisor/consult 豁免——同 VSC refill!==false 语义)。
|
|
101
|
+
*/
|
|
102
|
+
export function settleAsyncEntry(parent, entry, opts = {}) {
|
|
103
|
+
const { pool = null, ctx = null, onAccounting = null } = opts
|
|
104
|
+
const role = entry.role ?? "subagent"
|
|
105
|
+
entry.done = true
|
|
106
|
+
entry.status = "done"
|
|
107
|
+
const childLogId = `${role}#${entry.id}`
|
|
108
|
+
const childMs = entry.startedAt ? Date.now() - entry.startedAt : 0
|
|
109
|
+
const aborted = parentAborted(ctx, entry)
|
|
110
|
+
// ② 日志三连(D4 统一守卫——中止不落错误事件——ev:stopped 已在中止清池点表达;
|
|
111
|
+
// 定向 cancel 走 ev:cancelled)。族形态差异(LOGGING.md):
|
|
112
|
+
// - advisor:合并单 child:done(kind error/ok);
|
|
113
|
+
// - escalate:撞 cap 归 child:done partial(error 且非 capPartial 才 child:error);
|
|
114
|
+
// - 其余(subagent 族):error → child:error;done kind 按报告撞 cap 判 partial。
|
|
115
|
+
if (role !== "consult") {
|
|
116
|
+
if (entry.cancelled) {
|
|
117
|
+
logEvent("ev:cancelled", { id: childLogId })
|
|
118
|
+
} else if (!aborted) {
|
|
119
|
+
if (role === "advisor") {
|
|
120
|
+
const err = entry.error != null ? errText(entry.error, 200) : null
|
|
121
|
+
logEvent("child:done", { role, id: childLogId, ms: childMs, kind: err ? "error" : "ok" })
|
|
122
|
+
} else if (role === "escalate") {
|
|
123
|
+
const capPartial = String(entry.error ?? entry.report ?? "").includes(TURN_CAP_MARK)
|
|
124
|
+
if (entry.error != null && !capPartial) logEvent("child:error", { role, id: childLogId, ms: childMs, err: errText(entry.error, 200) })
|
|
125
|
+
else logEvent("child:done", { role, id: childLogId, ms: childMs, kind: capPartial ? "partial" : "ok" })
|
|
126
|
+
} else {
|
|
127
|
+
if (entry.error != null) logEvent("child:error", { role, id: childLogId, ms: childMs, err: errText(entry.error, 200) })
|
|
128
|
+
else logEvent("child:done", { role, id: childLogId, ms: childMs, kind: String(entry.report ?? "").includes(TURN_CAP_MARK) ? "partial" : "ok" })
|
|
129
|
+
}
|
|
130
|
+
if (parent._suspended) logEvent("ev:settled", { id: childLogId, kind: "suspended" })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// ③ 分流
|
|
134
|
+
if (entry.cancelled) {
|
|
135
|
+
// §19.5 cancelled settle 分支(D-M6):不入 pending、不参与回合尾直注入——清池 +
|
|
136
|
+
// 终态墓碑(dependsOn 取消语义)+ ⟦ev⟧stopped 冻结 + 族提醒(半成品警示对模型可见)。
|
|
137
|
+
pool?.delete(String(entry.id))
|
|
138
|
+
const tombstones = (parent._asyncTombstones ??= new Map())
|
|
139
|
+
tombstones.set(String(entry.id), { status: "cancelled", role })
|
|
140
|
+
ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
|
|
141
|
+
if (role === "advisor") {
|
|
142
|
+
// ②-6b:评审取消——token 未签发提醒(settleAdvisorRun 不消费预算)。
|
|
143
|
+
pushReal(parent, {
|
|
144
|
+
role: "user",
|
|
145
|
+
content: `[System reminder: async advisor review #${escapeXml(String(entry.id))} cancelled — the review did not settle; token not issued (评审已取消——token 未签发)]`,
|
|
146
|
+
})
|
|
147
|
+
} else if (role === "escalate") {
|
|
148
|
+
pushReal(parent, {
|
|
149
|
+
role: "user",
|
|
150
|
+
content: `[System reminder: async escalate #${entry.id} (${entry.tag}) cancelled by user — partial changes not merged/audited]`,
|
|
151
|
+
})
|
|
152
|
+
} else {
|
|
153
|
+
// §20 D-SD5:running 依赖取消——提醒列出依赖者(供模型决策)。
|
|
154
|
+
const dependents = dependentLabels(parent, String(entry.id))
|
|
155
|
+
const autoNote = parent.autoApprove
|
|
156
|
+
? " — AUTO session: they auto-start on slot availability (round2 #3)"
|
|
157
|
+
: " — they stay queued until you cancel them or an AUTO session starts them"
|
|
158
|
+
pushReal(parent, {
|
|
159
|
+
role: "user",
|
|
160
|
+
content: `[System reminder: subagent ${escapeXml(role)}#${entry.id} cancelled by user — partial changes not merged/audited${dependents.length > 0 ? `; queued dependents ${dependents.join(", ")} marked "dependency cancelled"${autoNote}` : ""}]`,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
} else if (!aborted) {
|
|
164
|
+
// 族记账 hook(advisor 陈旧判定/token D1 落盘;escalate 三分类 merge 决策;subagent/
|
|
165
|
+
// escalate 腾槽补位)——可改写 entry.report(settle 分支输出——digest 原样进)。
|
|
166
|
+
onAccounting?.(parent, entry)
|
|
167
|
+
// 挂起分流(§17 D-S8 + D-S3 记账——以读取时刻为准):挂起期 settle → 移交 pending
|
|
168
|
+
// 单容器(digest 注入)+ 出池 + ⟦ev⟧settled 驻留;回合内 → ⟦ev⟧done 立即冻结
|
|
169
|
+
// (条目留池——done-in-pool 统一表示——回合尾 collectSettledAsync 注入)。
|
|
170
|
+
// consult 族恒停靠(settle 即出会话池入 pending——无 TUI 冻结事件——子块各自冻结)。
|
|
171
|
+
if (parent._suspended || role === "consult") {
|
|
172
|
+
parkAsyncPending(parent, entry)
|
|
173
|
+
pool?.delete(String(entry.id))
|
|
174
|
+
ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
|
|
175
|
+
} else {
|
|
176
|
+
ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ④ 公共尾部:settleSeq 递增 + _settle 唤醒(never rejects)+ 唤醒挂起驱动 waiter
|
|
180
|
+
entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
|
|
181
|
+
entry._settle?.()
|
|
182
|
+
for (const w of parent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
|
|
183
|
+
// §20 D-SD4 释放点:settle/cancel 释放槽 + 依赖终态转移 → 补位(依赖满足者/域冲突
|
|
184
|
+
// 解除者自动启动——槽 ≤4)→ 排队态面板刷新(等待块头标注随终态更新——dependency
|
|
185
|
+
// cancelled / 位置前移)。subagent/escalate 族恒补(旧行为零回归——cancelled 分支
|
|
186
|
+
// 同样补位);advisor(独立评审池——无队列)/consult(会话池)豁免。
|
|
187
|
+
if (role !== "advisor" && role !== "consult") {
|
|
188
|
+
maybeRefillAsync(parent)
|
|
189
|
+
refreshQueuedTokens(parent, ctx?.callbacks?.onToken)
|
|
190
|
+
}
|
|
191
|
+
}
|