thincoder 0.12.59 → 0.12.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. package/src/tools/read_pdf.md +0 -21
@@ -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 { randomUUID, createHmac } from "node:crypto"
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
- const TOKEN_TTL_DEFAULT_MS = 7 * 24 * 3600 * 1000 // 7-day ceiling (v2 2026-08-25): multi-batch delivery must not re-review an unchanged design within a week; agent.engTokenTtlMs overrides
11
- const TOKEN_SECRET = process.env.THINCODER_TOKEN_SECRET || "thincoder-default-secret"
12
-
13
- /** Effective token TTL: config override with runtime validation (advisor timeoutMs precedent —
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
- // Design review: NO round reset design reviews share the 5-round convergence
148
- // budget with code reviews (round advances in agent.mjs; cap in run.mjs).
149
- // Session reset also removed: design rounds 2+ continue the advisor session
150
- // like code reviews (fix claims + round-aware prompts).
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
- // Generate the design token BEFORE the review and inject it into the advisor's prompt.
153
- // The advisor (LLM) decides pass/fail itself and echoes the token only on approval
154
- // the gate is a mechanical string match, not fragile semantics parsing.
155
- // A random designId is minted for EVERY design-review call (2026-09-01 multi-design
156
- // slots): on pass the token is stored in parent._engDesignTokens keyed by this id and
157
- // the id is echoed to the parent; on failure the id is dropped — never stored, so it
158
- // cannot clobber any other design's slot. Not a document anchor (rejected 2026-08-31).
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" ? randomUUID() : null
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
- // Whitespace-tolerant match (LLM may add spaces or wrap in fences).
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
- const tokenPattern = makeDesignTokenRegex(designToken)
171
- if (designToken && result && tokenPattern.test(result)) {
172
- // Advisor echoed the token review passed. Issue it to the parent for eng-coder.
173
- // (session cleanup for design reviews is owned by runAdvisorReview)
174
- // Multi-design slots (2026-09-01): store under this review's designId; the single
175
- // `_engDesignToken` mirror stays for the legacy boolean gates (dispatch "has token",
176
- // session persistence) key decision ② of ENGINEERING-MODE.md §7 2026-09-01.
177
- agent._engDesignTokens ??= new Map()
178
- agent._engDesignTokens.set(designId, designToken)
179
- agent._engDesignToken = designToken
180
- // Unlock the dispatch design gate (dispatch.mjs) for eng-coder SELF-review:
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
+ }