@thincoder/core 0.9.1

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 (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
@@ -0,0 +1,481 @@
1
+ /**
2
+ * advisor-async.mjs — async advisor reviews (AGENT-LOOP-SUBAGENT.md §6.10 — R13, 2026-09-06).
3
+ *
4
+ * The advisor tool runs reviews in a background pool at depth 0 (default async —
5
+ * ruling ②-3 A): the launch returns an ack, the turn ends naturally, the session
6
+ * suspends, the review settles into _pendingAsyncResults and a digest turn
7
+ * delivers the report (§17 consumption machinery — role-agnostic; entries carry
8
+ * role "advisor" and ride the subagent panel as a pseudo-role, ruling ②-4 A).
9
+ *
10
+ * This module owns:
11
+ * 1. the per-review INSTANCE registry (agent._advisorRuns — ruling ②-5 A:
12
+ * Map<reviewId, {reviewType, round, priorOutput, stale, open, docSetKey,
13
+ * designId}>) — rounds/prior are PER REVIEW (fix #4), no longer a run-global
14
+ * shared counter (design + code used to share _advisorRound);
15
+ * 2. launch resolution: design reviews continue the instance of the same
16
+ * document set (reviewId = the designId minted at the instance's first
17
+ * launch); code reviews continue the newest OPEN code instance (round 2+ of
18
+ * a fix loop) and CLOSE at normal user-run ends once no review is in flight
19
+ * (a converged/abandoned thread must not burn the cap of later tasks);
20
+ * 3. the async pool (agent._asyncAdvisors — ADVISOR_POOL_LIMIT = 4 default,
21
+ * agent.poolLimits.advisor configurable — POOL-CONFIG-UNIFIED; same-scope
22
+ * running launches are refused at once — never queued (F-5); over-limit +
23
+ * DIFFERENT scope launches QUEUE (ED-4 2026-09-16 — AGENT-LOOP-SUBAGENT.md §6.10:
24
+ * agent._asyncAdvisorQueue, ack {queued, position}, slot release auto-start,
25
+ * queued cancel dequeue+renumber — the former over-limit refusal ②-6a is
26
+ * retired); cancel (ruling ②-6b — directed abort → cancelled settle: no
27
+ * pending entry, no token, a "评审已取消——token 未签发" reminder);
28
+ * 4. SETTLE accounting (fixes #2/#4 — moved from the tool-result commit): a
29
+ * review whose targets mutated after launch settles STALE — it marks no
30
+ * _calledAdvisorThisRun and issues no token (the guard pushes back); a
31
+ * non-stale code review marks _calledAdvisorThisRun; a passing design review
32
+ * issues its token under the designId slot at settle time.
33
+ * 2026-09-11 第 11 批拆分:settle 记账 + 变更日志 + 陈旧/冻结判定迁 `advisor-settle.mjs`
34
+ * (本文件 500 行 = 硬帽在册);既有 import 面经本文件 re-export 保持不变。
35
+ *
36
+ * The sync path (depth>0 eng-coder self-review / explicit async:false) reuses
37
+ * the same instance resolution (rounds/prior/cap) — its accounting still lands
38
+ * in recordToolResults (agent.mjs run loop), marker-keyed by toolCall id.
39
+ */
40
+ import { randomUUID } from "node:crypto"
41
+ // design-token 工具组(2026-09-08 自本文件迁至 design-token.mjs——再越 500 行硬限)——
42
+ // 既有 import 面(advisor.mjs / 测试)不变:原导出全部经此 re-export 保留。
43
+ export {
44
+ buildApprovedSuffix, stripApprovedSuffix, effectiveTokenTtlMs, generateDesignToken,
45
+ validateDesignToken, makeDesignTokenRegex, settleDesignReview,
46
+ } from "./design-token.mjs"
47
+ // 第 11 批拆分(2026-09-11):settle 记账 + 变更日志(noteMutations)+ 陈旧判定
48
+ // (reviewIsStale)+ 冻结冲突(inflightDesignReviewConflict)迁 advisor-settle.mjs——
49
+ // 既有 import 面(dispatch / subagent-async / escalate-async / 测试)经此 re-export 不变。
50
+ import { normAbs, mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
51
+ export {
52
+ mutationSeqOf, noteMutations, reviewIsStale, settleAdvisorRun, inflightDesignReviewConflict,
53
+ } from "./advisor-settle.mjs"
54
+ // 第 33 批(§17.5):doc-set 键迁事实面档(实例续跑与陈旧判定同锚单源;原为私有——零 import 面)。
55
+ // 本文件继续在实例解析 / 池 entries 上消费它。
56
+ import { docSetKey } from "./review-facts.mjs"
57
+ // F31(2026-09-18 顾问面治理批):拒回文案对象标识行单源。
58
+ import { withIdentityLine, scopeSummary } from "../advisor/notice.mjs"
59
+ import { runAdvisorReview, resolveAdvisorProvider, ADVISOR_THINKING_PLACEHOLDER } from "../advisor/run.mjs"
60
+ import { stripEventToken } from "../agent/spawn-child.mjs"
61
+ import { escapeXml } from "../agent/helpers.mjs"
62
+ import { pushReal } from "../context.mjs"
63
+ import { logEvent } from "../log.mjs"
64
+ import { deathLine } from "../abort-provenance.mjs"
65
+ // ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
66
+ import { bindChildController, buildChildSignal, carrierField, getAsyncPool, settleAsyncEntry, tombstoneOf, writeTombstone } from "./async-settle.mjs"
67
+ import { nextSubagentId, consumeSubagentToken, assertPoolKeyFree, entryTerminal } from "./subagent-scheduler.mjs"
68
+
69
+ // ─────────────────────────────────────────────────────────────────────────────
70
+ // Review-instance registry (_advisorRuns — per-review rounds/prior/cap)
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+
73
+ /** 载体吸收后的只读读取(不创建):父对象字段优先、缺字段回退跨 run 载体
74
+ * (`history` 数组——agent 逐 run 重建的形态靠它跨 run 存活)。无 ⇒ null。 */
75
+ function advisorRunsRead(agent) {
76
+ const m = carrierField(agent, "_advisorRuns")
77
+ return m instanceof Map ? m : null
78
+ }
79
+
80
+ export function advisorRuns(agent) {
81
+ // 实例注册表读取吸收(与 #94 载体口径同源):既有形态(字段挂 agent)零变;
82
+ // 注册表已挂跨 run 载体的形态直接沿用(不另建分叉)。
83
+ const existing = advisorRunsRead(agent)
84
+ if (existing) return existing
85
+ // 缺省创建:落在跨 run 载体上(有 history 时)——与其余载体字段同口径。
86
+ const holder = agent?.history ?? agent
87
+ if (!holder) return new Map()
88
+ if (!(holder._advisorRuns instanceof Map)) holder._advisorRuns = new Map()
89
+ return holder._advisorRuns
90
+ }
91
+
92
+ /** The newest OPEN code instance (the thread a fix-round launch continues), or null. */
93
+ export function openCodeRun(agent) {
94
+ const runs = advisorRunsRead(agent)
95
+ if (!(runs instanceof Map) || runs.size === 0) return null
96
+ for (const r of [...runs.values()].reverse()) {
97
+ if (r.reviewType === "code" && r.open) return r
98
+ }
99
+ return null
100
+ }
101
+
102
+ /** The newest OPEN design instance for the given document set, or null. */
103
+ function openDesignRun(agent, key) {
104
+ const runs = advisorRunsRead(agent)
105
+ if (!(runs instanceof Map) || runs.size === 0) return null
106
+ for (const r of [...runs.values()].reverse()) {
107
+ if (r.reviewType === "design" && r.open && r.docSetKey === key) return r
108
+ }
109
+ return null
110
+ }
111
+
112
+ /**
113
+ * Resolve the review instance a launch continues (§11.2 ③ — per-review
114
+ * rounds/prior; reviewId = designId for design reviews, a random id for code).
115
+ * - design: continue the OPEN instance of the same document set (fix rounds of
116
+ * a review thread keep its designId and its round/prior); none → new instance.
117
+ * - code: continue the newest OPEN code instance (guard-driven fix loops);
118
+ * none → new instance.
119
+ * The legacy agent._advisorRound/_lastAdvisorOutput fields are SCOPED to the
120
+ * instance before the launch (message building + the run.mjs cap read them) —
121
+ * prior-less continuation degrades to round 1 semantics (fresh full review).
122
+ * @returns {{run: object, isNew: boolean, reviewId: string, designId: string|null}}
123
+ */
124
+ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {}) {
125
+ const runs = advisorRuns(agent)
126
+ let run = null
127
+ if (reviewType === "design") {
128
+ const key = docSetKey(documents, agent.cwd)
129
+ run = openDesignRun(agent, key)
130
+ if (!run) {
131
+ const reviewId = randomUUID()
132
+ // F2h (§29.1 2026-09-07): a same-scope re-review reuses the session's
133
+ // designId for this doc-set (any prior instance of the scope — open OR
134
+ // closed — the newest wins). A passed re-review overwrites the slot under
135
+ // the same id (no slot residue — T14: the old token is then rejected at
136
+ // the gate); old slots die at TTL only, kept as the fail-safe fallback.
137
+ const prior = [...runs.values()].reverse().find((r) => r.reviewType === "design" && r.docSetKey === key)
138
+ run = {
139
+ reviewId, reviewType, designId: prior?.designId ?? reviewId,
140
+ round: 0, priorOutput: null, stale: false, open: true, docSetKey: key,
141
+ }
142
+ runs.set(reviewId, run)
143
+ }
144
+ } else {
145
+ run = openCodeRun(agent)
146
+ if (!run) {
147
+ const reviewId = randomUUID()
148
+ run = {
149
+ reviewId, reviewType, designId: null,
150
+ round: 0, priorOutput: null, stale: false, open: true, docSetKey: null,
151
+ }
152
+ runs.set(reviewId, run)
153
+ }
154
+ }
155
+ // Scope the legacy mirror fields (message builder + run.mjs cap + displays).
156
+ agent._advisorRound = run.priorOutput ? run.round : 0
157
+ agent._lastAdvisorOutput = run.priorOutput
158
+ return { run, isNew: run.round === 0 && !run.priorOutput, reviewId: run.reviewId, designId: run.designId }
159
+ }
160
+
161
+ /** Guard round — the OPEN CODE instance only (2026-09-07 §8 F2): the design-written
162
+ * mirror never gates the code guard — no open code instance → 0. */
163
+ export function effectiveAdvisorRound(agent) {
164
+ return openCodeRun(agent)?.round ?? 0
165
+ }
166
+
167
+ // ─────────────────────────────────────────────────────────────────────────────
168
+ // Async pool (agent._asyncAdvisors — pool 上限 F-1/F-2:常量 = 运行时回退权威(2 → 4),
169
+ // config.mjs DEFAULTS.agent.poolLimits.advisor = config 层镜像——耦合锁 config-pool
170
+ // .test.mjs 逐键断言——勿单侧改默认)
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+
173
+ export const ADVISOR_POOL_LIMIT = 4
174
+
175
+ /** §11.2 advisor 池生效上限(纯——读 agent.poolLimits.advisor——合法 ≥1 整数生效——
176
+ * 非法/缺省回退 ADVISOR_POOL_LIMIT——与 subagent 域 resolvePoolLimits 独立不共享
177
+ * (subagent 两键表不含本键——键表语义不同——POOL-CONFIG-UNIFIED F-2))。 */
178
+ export function resolveAdvisorPoolLimit(raw) {
179
+ const v = raw?.advisor
180
+ return Number.isInteger(v) && v >= 1 ? v : ADVISOR_POOL_LIMIT
181
+ }
182
+
183
+ /** launch 判定点每次读 agent.config(/config 热应用——变更即生效下个 launch——文案
184
+ * 如实报本值)。 */
185
+ export function advisorPoolLimitFor(agent) {
186
+ return resolveAdvisorPoolLimit(agent?.config?.agent?.poolLimits)
187
+ }
188
+ /** §11.2 同 scope 守卫(F-5——用户裁①):同 reviewType+scope 有 running 评审 → true
189
+ * (拒——running 并行多实例歧义放大——settled 续跑语义不变)。design scope = 文档集键
190
+ * (docSetKey);code = 单 code 线程(记录不记路径键——与 openCodeRun 语义一致)。
191
+ * 与池容量守卫独立:容量 = 全局 ≤N——scope = 同 scope ≤1——两关都过才启动。 */
192
+ export function runningAdvisorOfScope(agent, reviewType, docSetKey) {
193
+ const pool = getAsyncPool(agent, "advisor")
194
+ if (!(pool instanceof Map)) return false
195
+ for (const e of pool.values()) {
196
+ if (e.status !== "running" || e.reviewType !== reviewType) continue
197
+ if (reviewType === "design" && e.run?.docSetKey !== docSetKey) continue
198
+ return true
199
+ }
200
+ return false
201
+ }
202
+
203
+ export function runningAdvisorCount(agent) {
204
+ return [...(getAsyncPool(agent, "advisor")?.values() ?? [])].filter((e) => e.status === "running").length
205
+ }
206
+
207
+ /** Any in-flight (non-done) async advisor review? — the completion guard skips
208
+ * its push-back while a review is pending (T-24b4: 未决不算未评审). */
209
+ export function advisorReviewPending(agent) {
210
+ return [...(getAsyncPool(agent, "advisor")?.values() ?? [])].some((e) => !e.done)
211
+ }
212
+
213
+ /** Directed cancel (ruling ②-6b — ⏹/cancel): abort the running review's
214
+ * controller (run.mjs signal chain) → the settle callback runs the cancelled
215
+ * branch (no pending entry / no token / "评审已取消——token 未签发" reminder).
216
+ * queued 目标(af 批 §6.11 第 3 条):出队 + 余位重编号 + 终态 cancelled + 机读线提醒
217
+ * + `⟦ev⟧cancelled`(唯一发射点——经调用方通道 `onToken`)+ `ev:cancelled` 日志;
218
+ * 无 abort(从未 start)⇒ 无 `⟦ev⟧stopped`。`onToken` 缺省 ⇒ 不发射(不另发)。
219
+ * 终态确认面(af 批 fix 轮):出队后 tombstone 在册 ⇒ 重复取消返回同一确认
220
+ * (`{id, status:"cancelled"}`——零重复注入 / 发射 / 日志);在册 `done` / 未知 id 文案不变。 */
221
+ export function cancelAsyncAdvisor(agent, id, onToken) {
222
+ const key = String(id)
223
+ const map = getAsyncPool(agent, "advisor") ?? new Map()
224
+ const entry = map.get(key)
225
+ if (!entry) {
226
+ // af 批 fix 轮(T-AF2 终态确认面——§6.11 第 3 条「幂等与池面」):queued 取消出队即出池
227
+ // ⇒ 重复取消经 cancelled 墓碑返回同一确认(本分支全早退——零重复注入 / 发射 / 日志)。
228
+ // 非取消墓碑(consumed / failed / discarded)与无墓碑 ⇒ 既有 unknown-id 文案不变;
229
+ // role 守卫:本条只答评审族(子代理族墓碑不在本面)。
230
+ const tomb = tombstoneOf(agent, key)
231
+ if (tomb?.status === "cancelled" && tomb.role === "advisor") return { id: key, status: "cancelled" }
232
+ return { id: key, status: "error", error: `unknown async advisor review id: ${key}` }
233
+ }
234
+ if (entry.done) {
235
+ return { id: key, status: "error", error: `async advisor review #${key} has already finished — nothing to cancel` }
236
+ }
237
+ if (entry.cancelled) return { id: key, status: "cancelled" } // abort already in flight — idempotent
238
+ // ED-4(§6.10 取消路由):queued 取消 = 出队 + 余位 position 重编号 + 终态 cancelled
239
+ // (无 abort——从未 start;was 记 queued 供 TUI 排队块移除——subagent 取消同式
240
+ // subagent-async.mjs)。排队条目取消不释放槽——不做补位(槽从未被占)。
241
+ // af 批收尾三面同址单点:机读线提醒(逐字同 running 面模板)/ 块面事件(唯一发射点)/ 日志面(queued 不经 settle ⇒ 出队点直记)。
242
+ if (entry.status === "queued") {
243
+ dequeueAdvisor(agent, entry)
244
+ entry.cancelled = true
245
+ entry.done = true
246
+ entry.status = "done"
247
+ map.delete(key)
248
+ writeTombstone(agent, key, "cancelled", "advisor")
249
+ pushReal(agent, {
250
+ role: "user",
251
+ content: `[System reminder: async advisor review #${escapeXml(String(entry.id))} cancelled — the review did not settle; token not issued (评审已取消——token 未签发)]`,
252
+ })
253
+ // 块面事件(零字段;relay 前缀 advisor#<id>/)——不发 ⟦ev⟧stopped(非 settle 通道)
254
+ try { onToken?.(`${entry.relayPrefix}⟦ev⟧cancelled\x1e`) } catch { /* relay 失败不影响池状态 */ }
255
+ logEvent("ev:cancelled", { id: `advisor#${key}` }) // 与 running 面同形(写点互斥 ⇒ 恰一条)
256
+ entry._settle?.()
257
+ return { id: key, status: "cancelled", was: "queued" }
258
+ }
259
+ entry.cancelled = true
260
+ // §20.3 站点 #11(第 24 批):定向中止 = cancel(reason 载荷)
261
+ entry.controller?.abort?.({ abortTrigger: "cancel", abortDetail: "advisor-cancel" })
262
+ return { id: key, status: "cancelled" }
263
+ }
264
+
265
+ /** Relay an advisor onOutput chunk into the subagent block channel (`advisor#N/`
266
+ * prefix — kind-preserving: think via onReasoning, tool via onToolOutput, text
267
+ * via onToken; the wait placeholder is stripped — it is a live indicator, not
268
+ * content, and the sync path strips it at freeze too). */
269
+ function relayAdvisorOutput(callbacks, prefix, chunk) {
270
+ if (!callbacks) return
271
+ const kind = typeof chunk === "string" ? "text" : (chunk?.kind ?? "text")
272
+ const text = typeof chunk === "string" ? chunk : String(chunk?.text ?? "")
273
+ if (!text) return
274
+ if (kind === "think") {
275
+ callbacks.onReasoning?.(prefix + text.replaceAll(ADVISOR_THINKING_PLACEHOLDER, ""))
276
+ } else if (kind === "tool") {
277
+ callbacks.onToolOutput?.(prefix + "tool", { kind: "tool", text })
278
+ } else {
279
+ callbacks.onToken?.(prefix + stripEventToken(text))
280
+ }
281
+ }
282
+
283
+ // ─── ED-4(2026-09-16 · AGENT-LOOP-SUBAGENT.md §6.10):评审池排队(槽释放自动起跑)────────
284
+
285
+ /** 排队出队单点(ED-4):`_asyncAdvisorQueue` 剔除目标条目 + 余项 position 按 `1..n`
286
+ * 重编号;读面经**载体吸收**(`carrierField`——#21⑤b):部分 parent(只携池 + `history`)
287
+ * 下不再 no-op(原直读 ⇒ 已取消评审被补位重启)。 */
288
+ function dequeueAdvisor(parent, entry) {
289
+ const queue = carrierField(parent, "_asyncAdvisorQueue")
290
+ if (!Array.isArray(queue)) return
291
+ const qi = queue.indexOf(entry)
292
+ if (qi < 0) return
293
+ queue.splice(qi, 1)
294
+ for (let i = 0; i < queue.length; i++) queue[i].position = i + 1
295
+ }
296
+
297
+ /** 排队块刷新(⟦ev⟧queued——slot 面:advisor 队列仅容量/scope 排队,无 dep/wait 面)——
298
+ * `_lastQueuedSig` 幂等去重;读面经**载体吸收**(部分 parent 不得 no-op——调用点含 VSC
299
+ * ⏹ → `executeCancelAction` 的合成 parent)。 */
300
+ export function refreshAdvisorQueuedTokens(parent, onToken) {
301
+ if (typeof onToken !== "function") return
302
+ const queue = carrierField(parent, "_asyncAdvisorQueue")
303
+ if (!Array.isArray(queue)) return
304
+ for (let i = 0; i < queue.length; i++) {
305
+ const e = queue[i]
306
+ const sig = `slot\x1e${i + 1}\x1e`
307
+ if (e._lastQueuedSig === sig) continue
308
+ e._lastQueuedSig = sig
309
+ try { onToken(`${e.relayPrefix}⟦ev⟧queued\x1eslot\x1e${i + 1}\x1equeued\x1e`) } catch { /* relay 失败不影响池状态 */ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * ED-4(§6.10):评审池排队补位单点——槽释放(任意族 settle 公共尾部)即按队首序补位
315
+ * 启动(出队复检同 scope 守卫——启动时刻同 scope 已被占 ⇒ 跳过留队)。与 subagent 队列
316
+ * 的先入者序不同:advisor 队列的阻塞仅来自 scope——被挡者不锁后入者(后入异 scope
317
+ * 排队者可越过被挡者先启动)。启动后余项 position 重编号 + ⟦ev⟧queued 刷新。
318
+ */
319
+ export function refillAdvisorQueue(parent, onToken) {
320
+ // 读面经**载体吸收**(`carrierField`——§6.10 ④「出队 / 补位 / 排队刷新三读面」同式):
321
+ // 部分 parent(只携池 + `history`)与出队 / 排队刷新读同一容器,不 no-op。
322
+ const queue = carrierField(parent, "_asyncAdvisorQueue")
323
+ if (!Array.isArray(queue) || queue.length === 0) return
324
+ const limit = advisorPoolLimitFor(parent)
325
+ let moved = false
326
+ for (;;) {
327
+ let pick = -1
328
+ for (let i = 0; i < queue.length; i++) {
329
+ const e = queue[i]
330
+ // §6.9 终态守卫(c1——谓词单点 entryTerminal):终态条目永不启动(幻影唯一燃料封死)
331
+ if (entryTerminal(e)) continue
332
+ // 出队复检(§6.10 交互):启动时刻同 scope 已被占 ⇒ 跳过留队(不等不排的反面面——
333
+ // 排队者从未占过 scope;scope 占用以 running 为准)。
334
+ if (runningAdvisorOfScope(parent, e.reviewType, e.run?.docSetKey ?? null)) continue
335
+ pick = i
336
+ break
337
+ }
338
+ if (pick < 0 || runningAdvisorCount(parent) >= limit) break
339
+ queue.splice(pick, 1)[0].start()
340
+ moved = true
341
+ }
342
+ if (!moved) return
343
+ for (let i = 0; i < queue.length; i++) queue[i].position = i + 1
344
+ refreshAdvisorQueuedTokens(parent, onToken)
345
+ }
346
+
347
+ /**
348
+ * Launch an async advisor review (pool entry + background runner + settle
349
+ * wiring — mirrors subagent-run.mjs's lifecycle, plus the ED-4 queue).
350
+ * The runner wraps runAdvisorReview (promise → report/error) — the subagent
351
+ * pipeline is untouched (ruling ②-2 A). The entry carries role "advisor" so the
352
+ * digest/panel/freeze consumers route it like any other settled block.
353
+ * @returns {{ok: true, id: string}} — ack; or {{ok: true, id: string, queued: true,
354
+ * position: number}} — ED-4 排队 ack(池满 + 异 scope——slot 释放自动起跑); or
355
+ * {{error: string}} — scope guard (same reviewType+scope running — §11.2 F-5)
356
+ * or invalid ctx.
357
+ */
358
+ export function launchAsyncAdvisor(parent, ctx, launch) {
359
+ const { reviewType, documents, paths, object, designToken, designId, run } = launch
360
+ const limit = advisorPoolLimitFor(parent)
361
+ // §11.2 same-scope guard(F-5——用户裁①):同 type+scope 有 running → 拒——与池容量
362
+ // 守卫独立两关都过才启动——同 scope 不等不排(settled 续跑不变;排队仅异 scope 面)。
363
+ if (runningAdvisorOfScope(parent, reviewType, run?.docSetKey ?? null)) {
364
+ const scopeNote = reviewType === "design"
365
+ ? "a design review of this document set is still running"
366
+ : "a code review is still running (code reviews are a single thread — launch the next one after it settles)"
367
+ // F31:既有稳定前缀逐字(行首)+ 标识块尾随(单源 notice.mjs);round = 本次发起将使用的轮次号。
368
+ const idFields = { type: reviewType, scope: scopeSummary(documents?.length ? documents : paths), round: `${(run?.round ?? 0) + 1}/uncapped`, criterion: "scope-in-flight" }
369
+ return { error: withIdentityLine(`Advisor: 此 scope 已有评审在跑——settle 后逐个发起 — ${scopeNote}; round/prior continuation would be ambiguous while it is in flight — wait for it to settle, then launch the next review.`, idFields) }
370
+ }
371
+ // ED-4(AGENT-LOOP-SUBAGENT.md §6.10):池满 + 异 scope ⇒ 排队(非拒)——ack 含 queued +
372
+ // position;running 计数只算 running 条目(排队不占槽)。原 ②-6a 拒发退役。
373
+ const queued = runningAdvisorCount(parent) >= limit
374
+ parent._asyncAdvisors ??= new Map()
375
+ // SUBAGENT-ID-COUNTER-AGENT(2026-09-09):取号统一走 nextSubagentId——池活续号兜底 + 跨池共号(§11.2)。
376
+ const id = nextSubagentId(parent)
377
+ // ED-5(AGENT-LOOP-SUBAGENT.md §6.21)一次性取号令牌消费(同步配对——本函数内取号 →
378
+ // 消费无 await 间隙);断言通过即置 undefined(防同一令牌跨站点复用)。
379
+ consumeSubagentToken(parent, id, "advisor launch", "advisor")
380
+ const entry = {
381
+ id, role: "advisor", reviewType, run,
382
+ reviewId: run.reviewId, designId, designToken, documents, paths, object,
383
+ launchSeq: mutationSeqOf(parent),
384
+ docAbs: reviewType === "design" && Array.isArray(documents)
385
+ ? documents.filter((d) => typeof d === "string").map((d) => normAbs(d, parent.cwd))
386
+ : [],
387
+ relayPrefix: `advisor#${id}/`,
388
+ status: queued ? "queued" : "running", position: undefined,
389
+ report: null, error: null, done: false, cancelled: false,
390
+ promise: null, _settle: null,
391
+ model: (() => { try { return resolveAdvisorProvider(parent).model ?? null } catch { return null } })(),
392
+ startedAt: queued ? null : Date.now(), turn: 0, maxTurns: 0,
393
+ controller: null,
394
+ }
395
+ // Entry controller chained to the session/run base signal (subagent-run parity):
396
+ // Ctrl+C / session abort propagates into the review's chat; a digest's own
397
+ // Ctrl+I must not orphan it (children hold the session signal while suspended).
398
+ // D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md)。
399
+ const ctrl = new AbortController()
400
+ entry.controller = ctrl
401
+ // §20.3 站点 #10(第 24 批):hop 逐跳保 reason;#98 链结单点(interrupt 豁免面——
402
+ // Ctrl+I 不逐链中止飞行评审)。
403
+ bindChildController(ctrl, buildChildSignal(parent, ctx))
404
+ entry.promise = new Promise((res) => { entry._settle = res })
405
+ entry.start = () => {
406
+ // ED-4(§6.10):补位启动锚点——queued → running(position 清位);async mark +
407
+ // [model] 在此后发出(入队不 paint async 块——启动才 paint)。
408
+ entry.status = "running"
409
+ entry.position = undefined
410
+ entry.startedAt = Date.now()
411
+ // Async mark + [model] — the TUI block opens at ACTUAL start (⏹ gating reads it).
412
+ ctx?.callbacks?.onToken?.(entry.relayPrefix + "⟦ev⟧async\x1e")
413
+ ctx?.callbacks?.onToken?.(entry.relayPrefix + "[model]" + (entry.model ?? ""))
414
+ runAdvisorReview(parent, reviewType, {
415
+ onOutput: (chunk) => relayAdvisorOutput(ctx?.callbacks, entry.relayPrefix, chunk),
416
+ signal: entry.controller.signal,
417
+ }, designToken, documents, paths, object, designId)
418
+ .then((report) => { entry.report = report })
419
+ // §20.3 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
420
+ .catch((err) => { entry.error = deathLine(err, entry.controller?.signal) })
421
+ .finally(() => {
422
+ // settle 公共收尾单点(ASYNC-RESULT-CONTAINER.md D3——settleAsyncEntry):日志三连
423
+ // /cancelled 分支(出池+墓碑+⟦ev⟧stopped+"评审已取消——token 未签发"提醒)/挂起分流
424
+ // (pending 单容器+出池——统一守卫 !parentAborted——D4)/公共尾部(settleSeq/_settle/
425
+ // 唤醒 waiter)统一走共享 helper。族特有 hook = settleAdvisorRun 记账(fix #2/#4——
426
+ // 陈旧判定/轮次/token D1 落盘——settle 分支输出写回 entry.report,digest 原样进);
427
+ // cancelled/parent-aborted 不调(不消费预算——settleAdvisorRun 自己的早退语义等价)。
428
+ settleAsyncEntry(parent, entry, {
429
+ pool: parent._asyncAdvisors,
430
+ ctx,
431
+ onAccounting: () => {
432
+ const settled = settleAdvisorRun(parent, entry)
433
+ // §29 fix B:settle 分支输出(清洗/未签发提示)写回 entry.report——digest 原样进。
434
+ if (settled.report != null) entry.report = settled.report
435
+ },
436
+ })
437
+ })
438
+ }
439
+ // ED-5(§6.21)入池键守卫:同 id 二次入池 = 覆写(静默丢报告 + status/cancel 错址)⇒ 抛错。
440
+ assertPoolKeyFree(parent._asyncAdvisors, id, "advisor")
441
+ parent._asyncAdvisors.set(String(id), entry)
442
+ if (queued) {
443
+ // ED-4(§6.10 排队面):独立评审队列入队 + 排队块 token(⟦ev⟧queued——slot 面)——
444
+ // 锚点 = 实际启动(入队不 paint async 块);ack 如实带 queued + position。
445
+ (parent._asyncAdvisorQueue ??= []).push(entry)
446
+ entry.position = parent._asyncAdvisorQueue.length
447
+ refreshAdvisorQueuedTokens(parent, ctx?.callbacks?.onToken)
448
+ logEvent("child:spawn", { role: "advisor", id: `advisor#${id}`, kind: "async", status: "queued", ms: 0 })
449
+ return { ok: true, id: String(id), queued: true, position: entry.position }
450
+ }
451
+ logEvent("child:spawn", { role: "advisor", id: `advisor#${id}`, kind: "async", status: "running", ms: 0 })
452
+ entry.start()
453
+ return { ok: true, id: String(id) }
454
+ }
455
+
456
+ /**
457
+ * Close every OPEN code instance at a normal non-auto run end (finalizeAgentTurn)
458
+ * when no review/child activity remains: a code thread that settled and whose
459
+ * digest was consumed must not burn the convergence budget of a later task — the
460
+ * next code review starts a fresh full review (round 1). Runs that END with a review
461
+ * or an eng-coder child in flight keep the thread open (fix-round continuation).
462
+ * @returns {boolean} whether any instance was closed
463
+ */
464
+ export function closeOpenCodeAdvisorRuns(agent) {
465
+ const advisors = getAsyncPool(agent, "advisor")
466
+ // Any pooled advisor entry — running OR settled-not-consumed — keeps the
467
+ // thread open: an in-run settle whose report has not reached the model (the
468
+ // suspension sweep / digest consumes it next) must still be continuable by
469
+ // the fix round that follows the disposition.
470
+ if ((advisors?.size ?? 0) > 0) return false
471
+ if ((getAsyncPool(agent, "subagent")?.size ?? 0) > 0) return false
472
+ if ((carrierField(agent, "_pendingAsyncResults") ?? []).some((e) => e.role === "advisor")) return false
473
+ let closed = false
474
+ const runs = advisorRunsRead(agent)
475
+ if (runs instanceof Map) {
476
+ for (const r of runs.values()) {
477
+ if (r.reviewType === "code" && r.open) { r.open = false; closed = true }
478
+ }
479
+ }
480
+ return closed
481
+ }