@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,143 @@
1
+ /**
2
+ * async-discard.mjs — 中止清池的「只清已死」收尾单点(批 4 CLI-ASYNC-DISCARD——CLI 侧对称;
3
+ * 设计权威 = `docs/core/design/AGENT-LOOP-SUBAGENT.md` §6.20,需求 = `docs/core/requirements/AGENT-LOOP.md` §4.10)。
4
+ *
5
+ * 原作 = 两接线点中止分支的无差别清池(`thincoder-core/agent/run-stages.mjs` 回合尾中止 ·
6
+ * `thincoder-cli/src/tui/suspension-drive.mjs` 挂起会话中止)——`_asyncSubagents.clear()` +
7
+ * `_asyncAdvisors.clear()` + `_asyncQueue = []`:被清出的条目不留终态、无提醒、无事件 ⇒
8
+ * 模型只能猜「后台报告到底到没到」。对侧基准 = `thincoder-vscode/src/agent-tools/async-discard.mjs`
9
+ * (本批 VSC 零写入;同机制两实现 = D-AD7 已知重复登记,收敛另案)。
10
+ *
11
+ * 单不变量(§6.20.3):**清池 ⟺ 该条目已死且其报告不可达**。
12
+ * - 丢弃 ⟺ `entry.done !== true ∧ entry.cancelled !== true ∧ parentAborted(ctx, entry)`;
13
+ * - 保留 ⟺ 其余——存活条目(controller 未中止——报告沿自动通道到达)/ done-in-pool(回合尾
14
+ * 收集 / 挂起 sweep 消化)/ cancelled(settle cancelled 分支收尾)。
15
+ *
16
+ * 判据口径(D-AD6):两接线点**均不传 `ctx`** ⇒ 实走 controller 支(`parentAborted(null, entry)`)
17
+ * ——与对侧有效判据同判(对侧传入的 `ctx` 为死参);`ctx` 保留为签名备用面(直调用例 / 对侧收敛)。
18
+ * 「controller 已中止 = 条目真死」由 `bindChildController` 单点保证(`async-settle.mjs:88-98`
19
+ * 双路:基信号已中止 ⇒ 立即 abort;未来中止 ⇒ 带活监听逐链传播;interrupt 不逐链)。
20
+ *
21
+ * 动作序(§6.20.3):每条目 = 写 `discarded` 墓碑(**载体吸收单点** `writeTombstone`——父对象无
22
+ * 自有 `_asyncTombstones` 而 `history` 有 ⇒ 借用同一 Map,不另建分叉)→ 出池 → 队列剔除(存活
23
+ * 条目 `position` 重编号——与 cancel 出队 `subagent-async.mjs:190` 同式;ED-4 后 advisor 族
24
+ * 同样剔除其独立队列 `_asyncAdvisorQueue`)→ 汇总;整批(有丢弃才发生)= **一次** `pushReal`
25
+ * 模型可见提醒(user 角色——escapeXml 转义插值)+ **一条** `ev:discarded`(零丢弃 ⇒ 零注入、
26
+ * 零事件)。
27
+ *
28
+ * 端差登记(D-AD8a/b):注入走核 `pushReal`(机器线 + 人读线——对侧直 `history.push`);转义只
29
+ * 覆盖插值(对侧整条转义)——文案骨架无 XML 特殊字符 ⇒ 可观察输出等价。
30
+ *
31
+ * 模块图:单向 import 核单点 `async-settle.mjs`(`parentAborted` / `getAsyncPool` /
32
+ * `writeTombstone`)+ `../context.mjs`(`pushReal`)/ `../agent/helpers.mjs`(`escapeXml`)/
33
+ * `../log.mjs`——叶子向、无环。
34
+ */
35
+ import { carrierField, getAsyncPool, parentAborted, writeTombstone } from "./async-settle.mjs"
36
+ import { pushReal } from "../context.mjs"
37
+ import { escapeXml } from "../agent/helpers.mjs"
38
+ import { logEvent } from "../log.mjs"
39
+
40
+ /** 提醒模板(verbatim 同源 = 对侧 `async-discard.mjs:37-44`;测试断关键子串,不逐行断)。 */
41
+ const subagentReminderText = (n, list) =>
42
+ `[System reminder: ${n} background subagent(s) were discarded by the user's Stop — their reports will NOT arrive: ${list}. ` +
43
+ "Partial changes from discarded children stay unmerged/unaudited; re-spawn if the work is still needed.]"
44
+
45
+ /** 评审族提醒模板(verbatim——advisor 池同构面;整批一次注入)。 */
46
+ const advisorReminderText = (n, list) =>
47
+ `[System reminder: ${n} background advisor review(s) were discarded by the user's Stop — their reports will NOT arrive: ${list}.\n` +
48
+ "No design token was issued for a discarded design review; launch the review again if it is still needed.]"
49
+
50
+ /** 列表词(wasStatus 数据源):queued → "(was queued — never started)";其余 "(was running)"。 */
51
+ const wasPhrase = (wasStatus) => (wasStatus === "queued" ? " (was queued — never started)" : " (was running)")
52
+
53
+ /** 丢弃判定(单点复用——零新谓词):`parentAborted` 复用核守卫单点(interrupt 豁免内建)。 */
54
+ function discardable(entry, ctx) {
55
+ return entry.done !== true && entry.cancelled !== true && parentAborted(ctx, entry)
56
+ }
57
+
58
+ /** 出池(键形单源:写侧 `Map.set(String(id))` ⇒ 读删一律 String 归一)。 */
59
+ function removeFromPool(map, id) {
60
+ if (!map) return
61
+ map.delete(String(id))
62
+ }
63
+
64
+ /** 队列剔除(D-AD4——接线点原 `agent._asyncQueue = []` 行由本模块接管):剔除已丢弃 id,存活
65
+ * 条目 `position` 按 `1..n` 重编号(面板 / status 的 queue position 与队列内容保持一致)。
66
+ * ED-4(2026-09-16):queueKey 参数化——subagent 族 `_asyncQueue`、advisor 族
67
+ * `_asyncAdvisorQueue`(评审池有排队语义后同面剔除)。 */
68
+ function pruneQueue(parent, ids, queueKey) {
69
+ const queue = carrierField(parent, queueKey) // #43-①:第四读面同式吸收(载体形队列)
70
+ if (!Array.isArray(queue) || queue.length === 0) return
71
+ for (let i = queue.length - 1; i >= 0; i--) {
72
+ if (ids.has(String(queue[i]?.id))) queue.splice(i, 1)
73
+ }
74
+ for (let i = 0; i < queue.length; i++) queue[i].position = i + 1
75
+ }
76
+
77
+ /**
78
+ * 私有共享核(族差异经 spec 注入:池键 / 摘要 / 列表词 / 文案 / 角色名 / 队列剔除面)。
79
+ * @returns {{discarded: object[], kept: number}} discarded = 丢弃条目摘要(族形态);kept = 判定后仍在池数。
80
+ */
81
+ function discardRole(parent, spec, ctx) {
82
+ const map = getAsyncPool(parent, spec.pool)
83
+ const out = { discarded: [], kept: 0 }
84
+ if (!map || map.size === 0) return out
85
+ const ids = new Set()
86
+ for (const entry of [...map.values()]) {
87
+ if (!discardable(entry, ctx)) continue
88
+ out.discarded.push(spec.describe(entry))
89
+ ids.add(String(entry.id))
90
+ writeTombstone(parent, entry.id, "discarded", spec.roleOf(entry))
91
+ removeFromPool(map, entry.id)
92
+ }
93
+ out.kept = map.size
94
+ if (out.discarded.length === 0) return out // 零丢弃 = 零噪音(零注入 / 零事件)
95
+ spec.pruneQueue?.(parent, ids)
96
+ const list = out.discarded.map(spec.listPhrase).join(", ")
97
+ pushReal(parent, { role: "user", content: spec.reminder(out.discarded.length, escapeXml(list)) })
98
+ logEvent("ev:discarded", { n: out.discarded.length, ids: out.discarded.map((d) => `${d.role}#${d.id}`).join(", ") })
99
+ return out
100
+ }
101
+
102
+ /** subagent 族 spec(对侧 `:90-96` 同形;队列剔除 = 本族 `_asyncQueue`——queueKey 参数化面)。 */
103
+ const SUBAGENT_SPEC = {
104
+ pool: "subagent",
105
+ describe: (entry) => ({ id: entry.id, role: entry.role, wasStatus: entry.status === "queued" ? "queued" : "running" }),
106
+ roleOf: (entry) => entry.role,
107
+ listPhrase: (d) => `${d.role}#${d.id}${wasPhrase(d.wasStatus)}`,
108
+ reminder: subagentReminderText,
109
+ pruneQueue: (parent, ids) => pruneQueue(parent, ids, "_asyncQueue"),
110
+ }
111
+
112
+ /** 评审族 spec(对侧 `:99-105` 同形;ED-4 后含排队面:wasStatus 读条目 status——queued →
113
+ * "(was queued — never started)";队列剔除 = 评审独立队列 `_asyncAdvisorQueue`)。 */
114
+ const ADVISOR_SPEC = {
115
+ pool: "advisor",
116
+ describe: (entry) => ({ id: entry.id, role: "advisor", reviewType: entry.reviewType, wasStatus: entry.status === "queued" ? "queued" : "running" }),
117
+ roleOf: () => "advisor",
118
+ listPhrase: (d) => `${d.role}#${d.id} (${d.reviewType})${wasPhrase(d.wasStatus)}`,
119
+ reminder: advisorReminderText,
120
+ pruneQueue: (parent, ids) => pruneQueue(parent, ids, "_asyncAdvisorQueue"),
121
+ }
122
+
123
+ /**
124
+ * 中止收尾(F1–F3):只清已死子代理条目——写 `discarded` 墓碑 + 出池 + 队列剔除 + 整批一次提醒。
125
+ * @param parent agent 形态(载体双形经 `getAsyncPool` / `writeTombstone` 吸收)
126
+ * @param ctx 判据 ctx(**接线点不传**——controller 支,D-AD6;仅直调用例使用)
127
+ * @returns {{discarded: Array<{id, role, wasStatus: "running"|"queued"}>, kept: number}}
128
+ * discarded = 丢弃条目摘要;kept = 判定后仍在池的条目数。
129
+ */
130
+ export function discardAbortedPool(parent, ctx = null) {
131
+ return discardRole(parent, SUBAGENT_SPEC, ctx)
132
+ }
133
+
134
+ /**
135
+ * 中止收尾(F3——评审池同构面):只清已死评审条目——写 `discarded` 墓碑(role "advisor")+
136
+ * 出池 + 整批一次提醒;done-in-pool(已完成待收集)与存活评审留池。
137
+ * @param parent agent 形态
138
+ * @param ctx 判据 ctx(接线点不传——同上)
139
+ * @returns {{discarded: Array<{id, role: "advisor", reviewType, wasStatus: "running"|"queued"}>, kept: number}}
140
+ */
141
+ export function discardAbortedAdvisors(parent, ctx = null) {
142
+ return discardRole(parent, ADVISOR_SPEC, ctx)
143
+ }
@@ -0,0 +1,299 @@
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
+ * S1 续轮第二批(2026-09-14——异步机械族 VSC 侧并入,AGENT-LOOP.md §2.3 载体口径):
27
+ * - #94 载体吸收:`carrierField`(父对象字段优先 / 回退 `history`——VSC 形跨 runAgent
28
+ * 载体)+ 墓碑三函数单点(`writeTombstoneTo` / `writeTombstone` / `tombstoneOf`,
29
+ * 原核内 3 处 inline 写全部收口);`getAsyncPool` / `parkAsyncPending` 走吸收。
30
+ * - #98 interrupt 豁免面:`parentAborted` 豁免 `reason.interrupt`(Ctrl+I 不是全停);
31
+ * `bindChildController` = 链结单点(原 4 处逐字同构,interrupt 不逐链中止)。
32
+ */
33
+ import { logEvent, errText } from "../log.mjs"
34
+ import { pushReal } from "../context.mjs"
35
+ import { escapeXml } from "../agent/helpers.mjs"
36
+ import { TURN_CAP_MARK } from "../agent/spawn-child.mjs"
37
+ import { dependentLabels, maybeRefillAsync, refreshQueuedTokens } from "./subagent-scheduler.mjs"
38
+ // ED-4(2026-09-16):advisor 排队补位——同 scheduler ↔ 本模块惰性环(函数声明提升——无求值期依赖)
39
+ import { refillAdvisorQueue } from "./advisor-async.mjs"
40
+
41
+ // ─── #94 载体吸收(VSC 侧并入——AGENT-LOOP.md §2.3 载体五字段)───────────────
42
+
43
+ /**
44
+ * 载体字段读取吸收(#94——「VSC 的跨 runAgent 存活载体面按核内结构归一」):
45
+ * §2.3 载体口径 = `_asyncSubagents` · `_asyncAdvisors` · `_consultSessions` ·
46
+ * `_pendingAsyncResults` · `_suspended`(AGENT-LOOP.md §2.3 seam 表)。
47
+ * CLI 形:字段直接挂 agent(跨 run 存活);VSC 形:字段挂 depth-0 `history`
48
+ * (agent per-run 重建——run 起始把 history 容器绑到 agent 字段,绑定不变式)。
49
+ * 本函数吸收两形为同一读取:父对象字段优先,缺字段回退 `parent.history`。
50
+ * 写侧**不变**(仍以父对象字段为入口——绑定不变式下与 history 同一容器)。
51
+ */
52
+ export function carrierField(parent, field) {
53
+ const own = parent?.[field]
54
+ if (own !== undefined && own !== null) return own
55
+ return parent?.history?.[field] ?? undefined
56
+ }
57
+
58
+ /**
59
+ * 终态墓碑写入单点(#94——VSC 侧并入:`writeTombstoneTo` / `writeTombstone` 单点收口;
60
+ * 原核内 3 处 inline `(x._asyncTombstones ??= new Map()); .set(...)` 全数经此)。
61
+ * holder 向 = 拥有载体的对象(agent / history 数组——跨 runAgent 存活口径同 getAsyncPool)。
62
+ */
63
+ export function writeTombstoneTo(holder, id, status, role) {
64
+ if (!holder) return
65
+ if (!(holder._asyncTombstones instanceof Map)) holder._asyncTombstones = new Map()
66
+ holder._asyncTombstones.set(String(id), { status, role })
67
+ }
68
+
69
+ /** parent 形态墓碑写入(载体吸收:父对象无 Map 而 history 有 ⇒ 借用同一 Map——不另建分叉)。
70
+ * #43-② 借用规则扩张(设计 §6.25 ③):父无自有 Map 且载体无 ⇒ **主容器落父字段**(今日落点——
71
+ * CLI 零回归;`history` 数组会被整体替换、非稳定载体)+ **载体侧写同一容器**——合成 parent 形
72
+ * (VSC ⏹ 取消路径)跨调用存活。 */
73
+ export function writeTombstone(parent, id, status, role) {
74
+ if (!(parent?._asyncTombstones instanceof Map)) {
75
+ const existing = carrierField(parent, "_asyncTombstones")
76
+ if (existing instanceof Map) parent._asyncTombstones = existing // 借用(既有)
77
+ else if (parent?.history && typeof parent.history === "object") {
78
+ parent._asyncTombstones = new Map() // 主容器(父字段——今日落点)
79
+ parent.history._asyncTombstones = parent._asyncTombstones // 载体别名(合成 parent 跨调用存活)
80
+ }
81
+ }
82
+ writeTombstoneTo(parent, id, status, role)
83
+ }
84
+
85
+ /** 终态墓碑读取(parent 形态——载体吸收;无记录 ⇒ null)。 */
86
+ export function tombstoneOf(parent, id) {
87
+ const m = carrierField(parent, "_asyncTombstones")
88
+ return (m instanceof Map ? m.get(String(id)) : undefined) ?? null
89
+ }
90
+
91
+ /**
92
+ * 条目 controller 链到基信号单点(#98 VSC 侧并入——interrupt 豁免面;原 subagent-run /
93
+ * advisor-async / escalate-async / consult 四处逐字同构):
94
+ * - 基信号**已 aborted** 且非 interrupt ⇒ 立即 abort(reason 逐跳保留——§20.3 站点 #10);
95
+ * - interrupt aborted(Ctrl+I——中断消息注入后同回合续跑)⇒ **不逐链中止**(池保留;
96
+ * 子代理 / 评审继续跑完,settle 照常注入——「Ctrl+I keeps the pool」意图对齐);
97
+ * - 未来 abort ⇒ 非 interrupt 才逐链传播(Stop / 会话中止全停语义不变)。
98
+ */
99
+ export function bindChildController(ctrl, baseSignal) {
100
+ if (!ctrl || !baseSignal) return
101
+ if (baseSignal.aborted) {
102
+ if (baseSignal.reason?.interrupt !== true) ctrl.abort(baseSignal.reason)
103
+ return
104
+ }
105
+ baseSignal.addEventListener("abort", () => {
106
+ if (baseSignal.reason?.interrupt === true) return
107
+ ctrl.abort(baseSignal.reason)
108
+ }, { once: true })
109
+ }
110
+
111
+ // ─── D1 池 accessor(吸收双池)────────────────────────────────────────────────
112
+
113
+ /** 池 accessor(D1):role "advisor" → `_asyncAdvisors`(独立评审池——无队列独立调度)·
114
+ * role "consult" → `_consultSessions`(会话池)· 其余 → `_asyncSubagents`(子代理/飞刀
115
+ * 共享槽位队列池)。载体经 `carrierField` 吸收(#94);未初始化返 null——调用方以
116
+ * `?? new Map()` / 可选链处置。 */
117
+ export function getAsyncPool(parent, role) {
118
+ const field = role === "advisor" ? "_asyncAdvisors" : role === "consult" ? "_consultSessions" : "_asyncSubagents"
119
+ return carrierField(parent, field) ?? null
120
+ }
121
+
122
+ // ─── D2 pending 单容器(+role)───────────────────────────────────────────────
123
+
124
+ /** pending 单容器停靠(settle 挂起分流 / sweep 补扫共用——统一表示:条目置 `_inPending`
125
+ * 防重复移交(sweep 幂等判据),includes 去重兜底)。载体经 `carrierField` 吸收(#94——
126
+ * VSC 形 pending 挂 history);写侧仍落父对象字段(无则借用 history 的数组)。 */
127
+ export function parkAsyncPending(parent, entry) {
128
+ const existing = carrierField(parent, "_pendingAsyncResults")
129
+ const pend = (parent._pendingAsyncResults ??= Array.isArray(existing) ? existing : [])
130
+ if (pend.includes(entry)) return
131
+ entry._inPending = true
132
+ pend.push(entry)
133
+ }
134
+
135
+ // ─── D4 守卫统一(严格版)+ D6 信号兜底单点 ──────────────────────────────────
136
+
137
+ /** D4 settle 守卫统一(escalate 严格版):父侧中止 = 回合 signal aborted(#98 VSC 侧
138
+ * 并入——interrupt 豁免:Ctrl+I `reason.interrupt` 不是全停,回合续跑、池保留 ⇒ 不判
139
+ * 父中止)或条目 controller aborted(取消先行——cancelled 分支在守卫之前分流,定向
140
+ * cancel 不算父中止)。日志三连与挂起分流共用本守卫。 */
141
+ export function parentAborted(ctx, entry) {
142
+ const base = ctx?.signal
143
+ if (base?.aborted === true && base?.reason?.interrupt !== true) return true
144
+ return Boolean(entry?.controller?.signal?.aborted)
145
+ }
146
+
147
+ /** D6 child signal 构建单点(D5——consult 补 _sessionSignal 兜底):挂起会话内的回合
148
+ * (digest/用户回合)子代理持会话 signal(`parent._sessionSignal`——会话 Stop 逐链中止,
149
+ * 消化轮自身 Ctrl+I/Ctrl+C 不误伤);回合级 `ctx.signal` 兜底。原 subagent/advisor/
150
+ * escalate 3 处逐字抄 + consult 补上统一入此。 */
151
+ export function buildChildSignal(parent, ctx) {
152
+ return parent?._sessionSignal ?? ctx?.signal ?? null
153
+ }
154
+
155
+ // ─── D3 settle 共享 helper ───────────────────────────────────────────────────
156
+
157
+ /**
158
+ * §23.3.1 消化窗口持有释放(TUI-OOM-ROOTCAUSE——D-SM2 表 2 候选 1):注入完成后置空条目
159
+ * 对子代理对象的引用(`childAgent`/`report`——条目前此从不显式释放,挂起期 = 分钟级驻留)。
160
+ * 幂等(重入不抛);池内/挂起未消化窗口语义零变(settle 时刻不释放——表 2 候选 2 否决:
161
+ * status/observe 在窗口内仍读 child 摘要)。三消费点(回合尾收集 / run 起始 pending 注入 /
162
+ * 挂起残差注入)在注入完成后调本 helper。
163
+ */
164
+ export function releaseSettledEntry(entry) {
165
+ if (!entry || typeof entry !== "object") return
166
+ entry.childAgent = null
167
+ entry.report = null
168
+ }
169
+
170
+ /**
171
+ * D3 settle 共享 helper——四族 settle 回调公共收尾单点(ASYNC-RESULT-CONTAINER.md
172
+ * AC1:settle 记账单点,无逐字重复)。流程:
173
+ * ① 落 done/status="done"(settle 即翻——不再占槽/持文件域;report/error 由各族
174
+ * 在调用前落好——subagent 的未投递注记先于本调用附加);
175
+ * ② 日志三连(统一守卫 !parentAborted——cancelled → ev:cancelled;非中止 →
176
+ * child:done|:error(族形态:advisor 合并单事件;escalate 撞 cap 归 partial)+
177
+ * 挂起期 ev:settled;consult 族跳过——per-model 日志已在子代理 settle 时记录);
178
+ * ③ 分流:cancelled(出池 + cancelled 墓碑 + ⟦ev⟧stopped + 族提醒——不入 pending)→
179
+ * parentAborted(守卫抑制——不落事件不入流——中止清池在回合尾/挂起中止统一做)→
180
+ * settled(族 onAccounting hook 记账 → 挂起期(或 consult 族恒停靠)移交 pending
181
+ * 单容器 + 出池 + ⟦ev⟧settled;回合内 ⟦ev⟧done 留池 done:true);
182
+ * ④ 公共尾部:settleSeq 递增 + `_settle` 唤醒 waiter + 腾槽补位(subagent/escalate 族
183
+ * 恒补——settle/cancel 释放槽 → maybeRefillAsync + refreshQueuedTokens——AGENT-LOOP.md
184
+ * §10 "settle/cancel 释放槽后…启动到槽满";advisor 经 refillAdvisorQueue 补位
185
+ * (ED-4——评审池有排队语义后不再豁免);consult(会话池)豁免)。
186
+ * opts(设计签名 `{pool, onAccounting}` + 实现参数 ctx):
187
+ * - pool:条目所在池 Map(出池 delete 目标;consult 传 null——会话池无条目)。
188
+ * - ctx:回合上下文(守卫读 ctx.signal;⟦ev⟧ 事件经 ctx.callbacks.onToken;腾槽补位
189
+ * 的 refreshQueuedTokens 也经 ctx.callbacks.onToken;consult 传 null——无 TUI 事件
190
+ * 无守卫)。
191
+ * - onAccounting(parent, entry):族特有 hook——advisor 调 settleAdvisorRun 记账(D1
192
+ * 落盘保留);escalate 调 classifyEscalateSettle(三分类 merge 决策)。
193
+ * 设计偏差注(advisor code review 发现——交付报告同步):设计 D3 把 maybeRefillAsync
194
+ * 归入 onAccounting hook(仅 settled 分支执行)——running 取消的 cancelled 分支将不再
195
+ * 补位(槽释放但 queued 头停滞——挂起会话无回合尾补位覆盖),违背既有不变式
196
+ * (AGENT-LOOP.md §10 + subagent-scheduler maybeRefillAsync 文档 "called from every
197
+ * settle")与 VSC 镜像(无条件 refill)。修正:腾槽补位移入公共尾部(subagent/escalate
198
+ * 族恒补——旧行为零回归;advisor/consult 豁免——同 VSC refill!==false 语义)。
199
+ */
200
+ export function settleAsyncEntry(parent, entry, opts = {}) {
201
+ const { pool = null, ctx = null, onAccounting = null } = opts
202
+ const role = entry.role ?? "subagent"
203
+ entry.done = true
204
+ entry.status = "done"
205
+ const childLogId = `${role}#${entry.id}`
206
+ const childMs = entry.startedAt ? Date.now() - entry.startedAt : 0
207
+ const aborted = parentAborted(ctx, entry)
208
+ // 挂起分流判定单点(#94 载体吸收——VSC 形 `_suspended` 挂 history)。
209
+ const suspended = carrierField(parent, "_suspended") === true
210
+ // ② 日志三连(D4 统一守卫——中止不落错误事件——ev:stopped 已在中止清池点表达;
211
+ // 定向 cancel 走 ev:cancelled)。族形态差异(LOGGING.md):
212
+ // - advisor:合并单 child:done(kind error/ok);
213
+ // - escalate:撞 cap 归 child:done partial(error 且非 capPartial 才 child:error);
214
+ // - 其余(subagent 族):error → child:error;done kind 按报告撞 cap 判 partial。
215
+ if (role !== "consult") {
216
+ if (entry.cancelled) {
217
+ logEvent("ev:cancelled", { id: childLogId })
218
+ } else if (!aborted) {
219
+ if (role === "advisor") {
220
+ const err = entry.error != null ? errText(entry.error, 200) : null
221
+ logEvent("child:done", { role, id: childLogId, ms: childMs, kind: err ? "error" : "ok" })
222
+ } else if (role === "escalate") {
223
+ const capPartial = String(entry.error ?? entry.report ?? "").includes(TURN_CAP_MARK)
224
+ if (entry.error != null && !capPartial) logEvent("child:error", { role, id: childLogId, ms: childMs, err: errText(entry.error, 200) })
225
+ else logEvent("child:done", { role, id: childLogId, ms: childMs, kind: capPartial ? "partial" : "ok" })
226
+ } else {
227
+ if (entry.error != null) logEvent("child:error", { role, id: childLogId, ms: childMs, err: errText(entry.error, 200) })
228
+ else logEvent("child:done", { role, id: childLogId, ms: childMs, kind: String(entry.report ?? "").includes(TURN_CAP_MARK) ? "partial" : "ok" })
229
+ }
230
+ if (suspended) logEvent("ev:settled", { id: childLogId, kind: "suspended" })
231
+ }
232
+ }
233
+ // ③ 分流
234
+ if (entry.cancelled) {
235
+ // §19.5 cancelled settle 分支(D-M6):不入 pending、不参与回合尾直注入——清池 +
236
+ // 终态墓碑(dependsOn 取消语义)+ ⟦ev⟧stopped 冻结 + 族提醒(半成品警示对模型可见)。
237
+ pool?.delete(String(entry.id))
238
+ writeTombstone(parent, entry.id, "cancelled", role) // §20 D-SD5:单点写入(#94 载体吸收)
239
+ ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
240
+ if (role === "advisor") {
241
+ // ②-6b:评审取消——token 未签发提醒(settleAdvisorRun 不消费预算)。
242
+ pushReal(parent, {
243
+ role: "user",
244
+ content: `[System reminder: async advisor review #${escapeXml(String(entry.id))} cancelled — the review did not settle; token not issued (评审已取消——token 未签发)]`,
245
+ })
246
+ } else if (role === "escalate") {
247
+ pushReal(parent, {
248
+ role: "user",
249
+ content: `[System reminder: async escalate #${entry.id} (${entry.tag}) cancelled by user — partial changes not merged/audited]`,
250
+ })
251
+ } else {
252
+ // §20 D-SD5:running 依赖取消——提醒列出依赖者(供模型决策)。
253
+ const dependents = dependentLabels(parent, String(entry.id))
254
+ const autoNote = parent.autoApprove
255
+ ? " — AUTO session: they auto-start on slot availability (round2 #3)"
256
+ : " — they stay queued until you cancel them or an AUTO session starts them"
257
+ pushReal(parent, {
258
+ role: "user",
259
+ 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}` : ""}]`,
260
+ })
261
+ }
262
+ } else if (!aborted) {
263
+ // 族记账 hook(advisor 陈旧判定/token D1 落盘;escalate 三分类 merge 决策;subagent/
264
+ // escalate 腾槽补位)——可改写 entry.report(settle 分支输出——digest 原样进)。
265
+ onAccounting?.(parent, entry)
266
+ // 挂起分流(§17 D-S8 + D-S3 记账——以读取时刻为准):挂起期 settle → 移交 pending
267
+ // 单容器(digest 注入)+ 出池 + ⟦ev⟧settled 驻留;回合内 → ⟦ev⟧done 立即冻结
268
+ // (条目留池——done-in-pool 统一表示——回合尾 collectSettledAsync 注入)。
269
+ // consult 族恒停靠(settle 即出会话池入 pending——无 TUI 冻结事件——子块各自冻结)。
270
+ if (suspended || role === "consult") {
271
+ parkAsyncPending(parent, entry)
272
+ pool?.delete(String(entry.id))
273
+ ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
274
+ } else {
275
+ ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
276
+ }
277
+ }
278
+ // ④ 公共尾部:settleSeq 递增 + _settle 唤醒(never rejects)+ 唤醒挂起驱动 waiter
279
+ entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
280
+ entry._settle?.()
281
+ wakeAsyncWaiters(parent) // §6.27.12.4 ①:挂起驱动唤醒单点(settle 尾 / 上行 ask 入队尾两处调用)
282
+ // §20 D-SD4 释放点:settle/cancel 释放槽 + 依赖终态转移 → 补位(依赖满足者/域冲突
283
+ // 解除者自动启动——槽 ≤4)→ 排队态面板刷新(等待块头标注随终态更新——dependency
284
+ // cancelled / 位置前移)。subagent/escalate 族恒补(旧行为零回归——cancelled 分支
285
+ // 同样补位);advisor(ED-4——独立评审池的排队语义)经 refillAdvisorQueue 补位;
286
+ // consult(会话池)豁免。
287
+ if (role === "advisor") {
288
+ refillAdvisorQueue(parent, ctx?.callbacks?.onToken)
289
+ } else if (role !== "consult") {
290
+ maybeRefillAsync(parent)
291
+ refreshQueuedTokens(parent, ctx?.callbacks?.onToken)
292
+ }
293
+ }
294
+ /** 唤醒挂起驱动单点(§6.27.12.4 ①——settle 尾 / 上行 ask 入队尾两处调用):循环体逐字自 settle 公共尾
295
+ * 抽入(唯一差分 = 读径经 `carrierField` 吸收——父字段在场路径零差分 = 严格超集);缺省 / 空 ⇒ no-op。 */
296
+ export function wakeAsyncWaiters(parent) {
297
+ const list = carrierField(parent, "_asyncWaiters")
298
+ for (const w of list?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
299
+ }