@thincoder/core 0.10.3 → 0.10.5

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 (172) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +1 -1
  3. package/advisor/citations.mjs +29 -3
  4. package/advisor/compaction.mjs +31 -7
  5. package/advisor/history.mjs +8 -6
  6. package/advisor/loop.mjs +29 -27
  7. package/advisor/messages.mjs +2 -1
  8. package/advisor/run.mjs +9 -5
  9. package/advisor/timeline.mjs +26 -0
  10. package/advisor.mjs +4 -2
  11. package/agent/assemble.mjs +1 -1
  12. package/agent/chat-call.mjs +1 -1
  13. package/agent/child-marks.mjs +2 -2
  14. package/agent/completion.mjs +3 -1
  15. package/agent/digest-account.mjs +189 -0
  16. package/agent/dispatch-gates.mjs +8 -0
  17. package/agent/dispatch.mjs +39 -18
  18. package/agent/family-tools.mjs +27 -16
  19. package/agent/helpers.mjs +12 -3
  20. package/agent/light-round.mjs +66 -0
  21. package/agent/post-turn.mjs +8 -4
  22. package/agent/record-results.mjs +4 -0
  23. package/agent/run-stages.mjs +32 -7
  24. package/agent/run-start.mjs +50 -3
  25. package/agent/setup-reminders.mjs +1 -1
  26. package/agent/setup.mjs +18 -3
  27. package/agent/subagent-event.mjs +1 -1
  28. package/agent/suspension.mjs +37 -12
  29. package/agent/timers.mjs +56 -5
  30. package/agent/turn-loop.mjs +30 -5
  31. package/agent/write-gate.mjs +20 -32
  32. package/agent-tools/advisor-async.mjs +15 -7
  33. package/agent-tools/advisor-settle.mjs +2 -2
  34. package/agent-tools/advisor.mjs +34 -33
  35. package/agent-tools/async-discard.mjs +34 -0
  36. package/agent-tools/async-settle.mjs +32 -7
  37. package/agent-tools/audit-block.mjs +1 -1
  38. package/agent-tools/bash-async.mjs +1 -1
  39. package/agent-tools/batch-lifecycle.mjs +59 -17
  40. package/agent-tools/batch-paths.mjs +24 -4
  41. package/agent-tools/batch-skeleton.mjs +59 -43
  42. package/agent-tools/batch.mjs +39 -18
  43. package/agent-tools/browser-async.mjs +198 -0
  44. package/agent-tools/checkpoint.mjs +1 -1
  45. package/agent-tools/design-slots.mjs +232 -0
  46. package/agent-tools/design-token.mjs +20 -0
  47. package/agent-tools/digest-budget.mjs +1 -1
  48. package/agent-tools/escalate-async.mjs +4 -2
  49. package/agent-tools/manifest.mjs +193 -0
  50. package/agent-tools/parent-channel.mjs +7 -4
  51. package/agent-tools/review-facts.mjs +143 -16
  52. package/agent-tools/settings.mjs +1 -1
  53. package/agent-tools/skill.mjs +1 -1
  54. package/agent-tools/subagent-actions-query.mjs +13 -6
  55. package/agent-tools/subagent-async.mjs +31 -15
  56. package/agent-tools/subagent-panel.mjs +2 -1
  57. package/agent-tools/subagent-scheduler.mjs +8 -5
  58. package/agent-tools/subagent-spawn.mjs +30 -56
  59. package/agent-tools/subagent.mjs +21 -8
  60. package/agent-tools/timer.mjs +186 -12
  61. package/agent-tools/zero-write-watch.mjs +77 -0
  62. package/agent-tools.mjs +2 -1
  63. package/agent.mjs +2 -2
  64. package/attachments.mjs +1 -1
  65. package/config-io.mjs +18 -13
  66. package/config-migrate.mjs +13 -29
  67. package/config-presets.mjs +33 -29
  68. package/config.mjs +68 -25
  69. package/context-degrade.mjs +1 -1
  70. package/context-echo.mjs +1 -1
  71. package/context-push.mjs +2 -2
  72. package/context.mjs +1 -1
  73. package/conventions.mjs +28 -8
  74. package/declaration.mjs +18 -2
  75. package/escape.mjs +1 -1
  76. package/file-links.mjs +1 -1
  77. package/file-refs.mjs +1 -1
  78. package/history-window.mjs +1 -1
  79. package/i18n.mjs +7 -0
  80. package/ledger-cmd.mjs +62 -16
  81. package/ledger-db.mjs +6 -0
  82. package/ledger-surface.mjs +49 -5
  83. package/ledger-tools.mjs +158 -87
  84. package/ledger-variant-notice.mjs +47 -0
  85. package/ledger.mjs +11 -18
  86. package/log.mjs +1 -1
  87. package/manifest-discovery.mjs +1 -1
  88. package/manifest-schema.mjs +7 -5
  89. package/manifest.mjs +5 -3
  90. package/mcp/transport-stdio.mjs +1 -1
  91. package/mcp.mjs +1 -1
  92. package/memory/code-index.mjs +1 -1
  93. package/memory/code-search.mjs +1 -1
  94. package/memory/code-sync.mjs +1 -1
  95. package/memory/docs.mjs +6 -2
  96. package/memory/file-list.mjs +1 -1
  97. package/memory/file-walk.mjs +17 -5
  98. package/memory/maintain.mjs +410 -0
  99. package/memory/memory-tool.mjs +21 -6
  100. package/memory/schema.mjs +19 -7
  101. package/memory/sweep.mjs +8 -5
  102. package/memory/sync-tail.mjs +16 -10
  103. package/model-ref.mjs +16 -13
  104. package/model-specs.mjs +21 -9
  105. package/package.json +1 -1
  106. package/peer-instances.mjs +1 -1
  107. package/permission.mjs +33 -0
  108. package/process-probe.mjs +2 -2
  109. package/prompts/advisor-design.md +4 -2
  110. package/prompts/advisor-round1.md +1 -1
  111. package/prompts/advisor-round2.md +1 -1
  112. package/prompts/advisor-round3.md +1 -1
  113. package/prompts/common.md +38 -4
  114. package/prompts/consult-base.md +1 -1
  115. package/prompts/discipline-engineering.md +32 -15
  116. package/prompts/discipline-normal.md +46 -2
  117. package/prompts/persona-eng-designer.md +6 -6
  118. package/prompts/persona-engineering.md +18 -9
  119. package/prompts/persona-normal.md +2 -0
  120. package/prompts/subagent-base.md +3 -0
  121. package/provider/anthropic.mjs +14 -2
  122. package/provider/core.mjs +6 -0
  123. package/provider/errors.mjs +28 -3
  124. package/provider/google.mjs +22 -10
  125. package/provider/list-models.mjs +3 -2
  126. package/provider/responses-request.mjs +3 -1
  127. package/provider/responses.mjs +38 -4
  128. package/provider/sse.mjs +3 -0
  129. package/provider-flows.mjs +14 -15
  130. package/proxy-chunked.mjs +102 -0
  131. package/proxy-target.mjs +54 -0
  132. package/proxy-transport.mjs +298 -0
  133. package/proxy.mjs +17 -248
  134. package/session-gc.mjs +2 -2
  135. package/session-lifecycle.mjs +18 -14
  136. package/session-slot-claims.mjs +2 -2
  137. package/session-slots-manifest.mjs +1 -1
  138. package/session-slots.mjs +3 -3
  139. package/shell-identity.mjs +107 -0
  140. package/team-ledger.mjs +282 -0
  141. package/team.mjs +223 -0
  142. package/tool-docs/advisor.md +1 -1
  143. package/tool-docs/bash.md +2 -1
  144. package/tool-docs/batch.md +1 -1
  145. package/tool-docs/browser.md +48 -0
  146. package/tool-docs/delete.md +7 -5
  147. package/tool-docs/ledger.md +6 -0
  148. package/tool-docs/manifest.md +16 -0
  149. package/tool-docs/memory.md +1 -0
  150. package/tool-docs/notify_parent.md +5 -0
  151. package/tool-docs/process.md +4 -4
  152. package/tool-docs/question.md +1 -0
  153. package/tool-docs/subagent.md +14 -3
  154. package/tool-docs/team_ledger.md +9 -0
  155. package/tool-docs/timer.md +6 -0
  156. package/tool-docs/wait_for.md +1 -0
  157. package/tools/bash.mjs +9 -4
  158. package/tools/browser.mjs +161 -0
  159. package/tools/git.mjs +1 -1
  160. package/tools/index.mjs +4 -4
  161. package/tools/ops.mjs +26 -10
  162. package/tools/patch.mjs +8 -2
  163. package/tools/repomap.mjs +1 -1
  164. package/tools/write-path.mjs +8 -6
  165. package/traces/trace-cleanup.mjs +1 -1
  166. package/traces/trace-store.mjs +1 -1
  167. package/vision-reader.mjs +10 -16
  168. package/tool-docs/ledger_add.md +0 -1
  169. package/tool-docs/ledger_close.md +0 -1
  170. package/tool-docs/ledger_count.md +0 -1
  171. package/tool-docs/ledger_query.md +0 -1
  172. package/tool-docs/ledger_update.md +0 -1
@@ -13,8 +13,8 @@
13
13
  * 段标题定位(sectionHeaderRe)同住本档:append 段尾定位与 status/close 段内状态行定位
14
14
  * 消费**同一个段头正则**——`## §N` 形态单源(`## §20` 不误命中 §2)。
15
15
  *
16
- * F11-C(本批):**骨架死占位枚举**(TEMPLATE_PLACEHOLDERS)与残留扫描纯函数
17
- * (findPlaceholderResidue)同住本档——与骨架模板共址即判据不会与模板脱节。
16
+ * F11-C:**骨架死占位枚举**(TEMPLATE_PLACEHOLDERS)与残留扫描纯函数(findPlaceholderResidue——
17
+ * 判定域 = 档头结构行,2026-10-05 判据收正)同住本档——与骨架模板共址即判据不会与模板脱节。
18
18
  */
19
19
  /**
20
20
  * 段标题定位(`## §N` 独立标题——`## §20` 不误命中 §2)。
@@ -29,7 +29,7 @@ export const sectionHeaderRe = (seg) => new RegExp(`^## §${seg}(?=\\s|$)`, "m")
29
29
  * 内嵌另一冻结词的值(含「进行中…已收口」双词 ⇒ 已收口优先误冻结——2026-09-20 词面纪律)。
30
30
  * emoji / 括注 / 日期后缀容忍(**读侧** gate 判定 = 子串包含,同 gate——写入面 value 谓词
31
31
  * 自本批起收紧为「剥装饰白名单后余核 = 关键词」,见 batch-lifecycle.mjs);
32
- * 全词表并集 = 各段项去重集合(note 字段零命中的判据面——括注永不误触 gate)。
32
+ * 全词表并集 = 各段项去重集合(note 字段零命中的判据——括注永不误触 gate)。
33
33
  */
34
34
  export const STATUS_WORDS = Object.freeze({
35
35
  1: Object.freeze({ open: "进行中", closed: "已收口" }),
@@ -39,16 +39,29 @@ export const STATUS_WORDS = Object.freeze({
39
39
  })
40
40
 
41
41
  /** 身份 → 可写段号(append 段白名单的**子代理面**——原 batch-segment.mjs 逐字迁移;主 agent
42
- * 的 depth-0 白名单(append = §1/§4/§6 · status = §1——轮 2 #3 裁定②)由身份判据在
43
- * batch.mjs / batch-lifecycle.mjs 分支,不经本表。旧 batch-segment.mjs 导出面等价保持——
44
- * 本符号经 batch.mjs re-export(过渡 shim 面零触)。 */
42
+ * 的 depth-0 白名单(模式感知——工程 §1/§4/§6 · §1 ∥ 普通 §1/§2/§4/§5/§6 · §1/§2/§5,§4.16)
43
+ * 由身份判据在 batch.mjs / batch-lifecycle.mjs 分支,不经本表。旧 batch-segment.mjs 导出面等价
44
+ * 保持——本符号经 batch.mjs re-export(过渡 shim 面零触)。**表零字面改**——普通模式另立增表
45
+ * `SEGMENT_BY_ROLE_NORMAL`(合并查只在普通模式分支);工程面「逐字不变」判据据此成立。 */
45
46
  export const SEGMENT_BY_ROLE = { "eng-designer": 2, "eng-coder": 5 }
46
47
 
48
+ /** 普通模式增表(§4.16——**合并查只在普通模式分支**,工程模式分支不消费本表):coder 承办 ⇒ §5
49
+ * 实施记录(可选绑定自写——未绑定由主 agent 代记、行内注明来源)。 */
50
+ export const SEGMENT_BY_ROLE_NORMAL = { "coder": 5 }
51
+
52
+ /** depth-0 段白名单(§4.16——**模式键两集**;工程面判定字面逐字保持、普通面新增):
53
+ * append:工程 = §1/§4/§6 ∥ 普通 = §1/§2/§4/§5/§6(§2 = 设计师席并入主 agent;§5 = 直做实施记录);
54
+ * status:工程 = §1 ∥ 普通 = §1/§2/§5(写域仍「调用者自己段」;`segment` 缺省 = §1)。 */
55
+ export const DEPTH0_SEGMENTS = Object.freeze({
56
+ append: Object.freeze({ engineering: Object.freeze([1, 4, 6]), normal: Object.freeze([1, 2, 4, 5, 6]) }),
57
+ status: Object.freeze({ engineering: Object.freeze([1]), normal: Object.freeze([1, 2, 5]) }),
58
+ })
59
+
47
60
  /** §1 状态行前缀形态(`**状态行**:` 独立行——表格行 / 块引用行不命中;判定只认关键字)。 */
48
61
  export const STATUS_LINE_RE = /^\s*\*\*状态行\*\*[::]\s*(.*)$/
49
62
 
50
63
  /**
51
- * 骨架死占位**枚举单源**(F11-C 判据面——create 骨架产出过的全部死占位字面):
64
+ * 骨架死占位**枚举单源**(F11-C 判据——create 骨架产出过的全部死占位字面):
52
65
  * - `#<编号>` / `<板块>` = 现行骨架仍在产出的两处(台账行——填充人 = 主 agent,时点 =
53
66
  * 建档后、本档首个 append/status 之前);
54
67
  * - `<BATCH-ID>` / `<讨论来源>` = 改形**删除**的两处旧占位(create 不再产出;旧档残留同判)。
@@ -58,46 +71,42 @@ export const STATUS_LINE_RE = /^\s*\*\*状态行\*\*[::]\s*(.*)$/
58
71
  */
59
72
  export const TEMPLATE_PLACEHOLDERS = ["<BATCH-ID>", "<讨论来源>", "#<编号>", "<板块>"]
60
73
 
74
+ /** 档头结构行值形(骨架三类——标题行 `# …` / 编制行 `> 编制` / 台账行 `> 台账`):判据可辨性收正
75
+ * (`TOOLS.md` §6.20)后只有这三类行参与死占位判定;骨架↔结构行不变量 = `batchSkeleton` 产出中
76
+ * 枚举字面 ⊆ 本三类行(骨架改形与判据同拍——结构行外新增占位 = 静默漏检)。 */
77
+ const HEADER_STRUCTURAL_RE = [/^#\s/, /^>\s*编制/, /^>\s*台账/]
78
+
61
79
  /**
62
80
  * 死占位残留扫描(F11-C——append/status 落笔前的机检;**纯函数 · 零 fs**)。
63
- * 判定域 = **档头**(档首行起至首个 `## §N` 段头前)+ **本次目标段**段内——他段模板占位不归
64
- * 本段作者管(一段一作者:§2 作者扫 §5 的占位 = 越权)。
81
+ * 判定域 = **档头结构行**(区域 = 档首至首个 `## §N` 段头前;值形 = 骨架三类结构行:标题行
82
+ * `# …` / 编制行 `> 编制` / 台账行 `> 台账`)——行内出现枚举字面 ⇒ 残留;段体(首段头起)
83
+ * 任意位置出现字面 = 正文引用 ⇒ **豁免**(引用非未填——2026-10-05 判据可辨性收正,`TOOLS.md`
84
+ * §6.20;原「档头 + 目标段」全域子串扫描分不清「未填占位」与「正文引用」——本批 §1 首投实害)。
65
85
  * @param {string} src — 档全文
66
- * @param {number|string} seg — 本次目标段号(`2` / `"2"`——同 sectionHeaderRe 消费形)
67
- * @returns {Array<{line: number, text: string, where: "header"|"section"}>} 残留列表(空 = 无残留);
68
- * `line` = 所在判定域内 1-based 行号(档头域自档首行起 · 段域自 `## §N` 标题行起——便于定位),
69
- * `text` = 命中行 trim 原文,`where` = 命中域(档头 / 目标段——错误句标注位置用)
86
+ * @returns {Array<{line: number, text: string}>} 残留列表(空 = 无残留);
87
+ * `line` = 档头域内 1-based 行号(档头域自档首行起——便于定位),`text` = 命中行 trim 原文
70
88
  */
71
- export function findPlaceholderResidue(src, seg) {
72
- const out = []
73
- const scan = (text, where) => {
74
- text.split("\n").forEach((line, i) => {
75
- if (TEMPLATE_PLACEHOLDERS.some((ph) => line.includes(ph))) out.push({ line: i + 1, text: line.trim(), where })
76
- })
77
- }
89
+ export function findPlaceholderResidue(src) {
78
90
  const firstSection = /^## §\d/m.exec(src)
79
- scan(src.slice(0, firstSection ? firstSection.index : 0), "header")
80
- const hdr = sectionHeaderRe(seg).exec(src)
81
- if (hdr) {
82
- const nextRe = /^## §\d/gm
83
- nextRe.lastIndex = hdr.index + hdr[0].length
84
- const next = nextRe.exec(src)
85
- scan(src.slice(hdr.index, next ? next.index : src.length), "section")
86
- }
91
+ const out = []
92
+ src.slice(0, firstSection ? firstSection.index : 0).split("\n").forEach((line, i) => {
93
+ if (!HEADER_STRUCTURAL_RE.some((re) => re.test(line))) return
94
+ if (TEMPLATE_PLACEHOLDERS.some((ph) => line.includes(ph))) out.push({ line: i + 1, text: line.trim() })
95
+ })
87
96
  return out
88
97
  }
89
98
 
90
99
  /**
91
- * 占位残留拒绝句(F11-C——append / status 两个挂点共用单源文案;逐行列残留:位置标注 + 行号 + 原文)。
100
+ * 占位残留拒绝句(F11-C——append / status 两个挂点共用单源文案;逐行列残留:行号 + 原文)。
92
101
  * 错误句前缀 = `batch:`(本批新增错误面——与 append 迁移面的 `batch_segment:` 旧面不混)。
93
- * @param {Array<{line:number,text:string,where:string}>} residues — findPlaceholderResidue 的返回值(非空)
102
+ * 逐字 = `TOOLS.md` §6.20 拒句表(「or your target section」半句随判定域收正退场)。
103
+ * @param {Array<{line:number,text:string}>} residues — findPlaceholderResidue 的返回值(非空)
94
104
  * @returns {string} 完整拒绝句(调用方 throw new Error(...))
95
105
  */
96
106
  export function placeholderResidueError(residues) {
97
- const where = (r) => (r.where === "header" ? "档头" : "目标段")
98
- const list = residues.map((r) => ` ${where(r)} line ${r.line}: ${r.text}`).join("\n")
99
- return "batch: 骨架死占位残留 — the batch record still carries skeleton placeholders (档头 or your target section). " +
100
- "Fill them before writing: create ⇒ the main agent fills the header's 台账编号 / 板块 placeholders ⇒ the record's first append/status opens. " +
107
+ const list = residues.map((r) => ` 档头 line ${r.line}: ${r.text}`).join("\n")
108
+ return "batch: 骨架死占位残留 — the batch record's header still carries skeleton placeholders. " +
109
+ "Fill them before writing: create with ledger / board fills the 台账 line at creation; otherwise the main agent fills the header by file edit ⇒ the record's first append/status opens. " +
101
110
  `Nothing was written. Residue:\n${list}`
102
111
  }
103
112
 
@@ -177,25 +186,32 @@ export function sectionHasStatusLine(src, seg) {
177
186
  * 编制行 + 台账/前情指针行)+ §1 段内**占位状态行**(含 gate 合法关键字「进行中」——建档即过
178
187
  * gate;整行不含「已收口」——gate 已收口优先,占位行不得携带冻结词)+ 各段模板子标题占位。
179
188
  * 已知实参替换:date / topic / **source** / prev(§4.11 参数面 + F11-B——source 必填、prev 传
180
- * 入值过幂等剥前缀,归一化在 lifecycle 面);档头 `#<编号>` / `<板块>` 仍保持占位字面(台账
181
- * 登记 = 主 agent 既有义务,create 不代建——填充时点 = 建档后、本档首个 append/status 之前)。
189
+ * 入值过幂等剥前缀,归一化在 lifecycle 面);`ledger` / `board` = 可选参(2026-10-05 建即填——
190
+ * 给则台账行实参化、未给保持占位字面;4 参旧调用形输出零变)。档头 `#<编号>` / `<板块>` 占位
191
+ * 留存时(未给参)由主 agent 文件编辑填(台账登记 = 主 agent 既有义务,create 不代建——填充
192
+ * 时点 = 建档后、本档首个 append/status 之前)。
182
193
  * 本批改形删除(F11-B):旧 `# …(<BATCH-ID>)` 后缀与 `来源 = <讨论来源>` 占位实参化。
194
+ * 模式取形(§4.16——普通模式变体**四处替换**、余行逐字同;`mode` 缺省 = "engineering" ⇒
195
+ * 工程产出逐字不变):① 汇总行 §2/§5 标签;② `## §2` 段头;③ §2 状态占位句;④ `## §5` 段头。
183
196
  */
184
- export function batchSkeleton({ date, topic, source, prev }) {
197
+ export function batchSkeleton({ date, topic, source, prev, ledger = null, board = null, mode = "engineering" }) {
198
+ const normal = mode === "normal"
199
+ const designerSeat = normal ? "主 agent" : "eng-designer" // ① 汇总行 §2 标签 ∥ ② §2 段头
200
+ const implementerSeat = normal ? "实施者:主 agent ∥ coder" : "eng-coder" // ① 汇总行 §5 标签 ∥ ④ §5 段头
185
201
  return [
186
202
  `# ${date} · ${topic}`,
187
- "> 六段 append-only,一段一作者:§1 讨论(主 agent)· §2 批次任务与设计(eng-designer)· §3 设计评审(评审子代理)· §4 用户批准(主 agent)· §5 实施记录(eng-coder)· §6 验证与收口(父代理)。",
203
+ `> 六段 append-only,一段一作者:§1 讨论(主 agent)· §2 批次任务与设计(${designerSeat})· §3 设计评审(评审子代理)· §4 用户批准(主 agent)· §5 实施记录(${implementerSeat})· §6 验证与收口(父代理)。`,
188
204
  `> 编制:主 agent · ${date} · 来源 = ${source}。`,
189
- `> 台账 = #<编号>(<板块> · 归批)。前情 = ${prev}。`,
205
+ `> 台账 = ${ledger ?? "#<编号>"}(${board ?? "<板块>"} · 归批)。前情 = ${prev}。`,
190
206
  "## §1 讨论(主 agent)",
191
207
  `**状态行**:🔄 ${STATUS_WORDS[1].open}(…)`,
192
208
  "<§1 模板占位:本批条目 / 关键判据 / 授权口径>",
193
- "## §2 批次任务与设计(eng-designer)",
194
- "**状态行**:(eng-designer 写入时更新)",
195
- "<§2 模板占位:本批条目(覆盖) / 设计档落点 / 机制设计 / 受影响文件与测试面 / 验收对照 / 关键决策 / 上抛项>",
209
+ `## §2 批次任务与设计(${designerSeat})`,
210
+ `**状态行**:(${normal ? "主 agent" : "eng-designer"} 写入时更新)`, // ③ §2 状态占位句
211
+ "<§2 模板占位:本批条目(覆盖) / 设计档落点 / 机制设计 / 受影响文件与测试覆盖 / 验收对照 / 关键决策 / 上抛项>",
196
212
  "## §3 设计评审(评审子代理)",
197
213
  "## §4 用户批准(主 agent)",
198
- "## §5 实施记录(eng-coder)",
214
+ `## §5 实施记录(${implementerSeat})`,
199
215
  "## §6 验证与收口(父代理)",
200
216
  "",
201
217
  ].join("\n")
@@ -12,9 +12,10 @@
12
12
  * eng 子代理 `depth > 0`(`agent._role` 定段);评审实例 ctx 无 depth 但工具以 `review: true`
13
13
  * 绑定。create/close 仅 depth-0 放行;depth-0 的 append/status/close 可选 `path`(D-BR21:
14
14
  * 缺省 = findInFlightBatch 取唯一在飞批,0/复数 ⇒ throw);子代理/评审传 path ⇒ 拒(目标 =
15
- * spawn/实例注入,语法上写不到别处——运行期门拒)。append 段白名单:depth-0 → §1/§4/§6 ·
16
- * eng-designer → §2 · 评审 → §3 · eng-coder → §5;status 写域 = 调用者自己段(主 agent 仅 §1
17
- * ——轮 2 #3 裁定②)。append 迁移面错误串**逐字保持 "batch_segment:" 前缀**(§4.1 全表锚断言);
15
+ * spawn/实例注入,语法上写不到别处——运行期门拒)。append 段白名单(**模式感知**——§4.16):
16
+ * depth-0 → 工程 §1/§4/§6 ∥ 普通 §1/§2/§4/§5/§6 · eng-designer → §2 · 评审 → §3 · eng-coder → §5 ·
17
+ * coder(普通模式分支增表——SEGMENT_BY_ROLE_NORMAL)→ §5;status 写域 = 调用者自己段(模式感知
18
+ * ——见 batch-lifecycle.mjs)。append 迁移面错误串**逐字保持 "batch_segment:" 前缀**(§4.1 全表锚断言);
18
19
  * "batch:" 前缀 = 本批新增错误面(create/close/status/findInFlight/path 门/dispatch)。
19
20
  *
20
21
  * 导出面:`batchTool`(主名)· `resolveBatchDocPath` · `batchDocBases` · `batchDocForReview` ·
@@ -29,7 +30,7 @@ import { readFileSync, writeFileSync } from "node:fs"
29
30
 
30
31
  // 描述面 = `tool-docs/batch.md`(#15 描述外置统一——工厂形描述经 DESC 单一解析面,工厂调用期取值)
31
32
  import { DESC } from "../tools/shared.mjs"
32
- import { SEGMENT_BY_ROLE, readBatchStatusLine, sectionHeaderRe, findPlaceholderResidue, placeholderResidueError } from "./batch-skeleton.mjs"
33
+ import { SEGMENT_BY_ROLE, SEGMENT_BY_ROLE_NORMAL, DEPTH0_SEGMENTS, readBatchStatusLine, sectionHeaderRe, findPlaceholderResidue, placeholderResidueError } from "./batch-skeleton.mjs"
33
34
  import { batchDocBases, resolveBatchDocPath } from "./batch-paths.mjs"
34
35
  import { closeBatchRecord, createBatchRecord, findInFlightBatch, statusBatchRecord } from "./batch-lifecycle.mjs"
35
36
 
@@ -93,10 +94,12 @@ function segmentNumber(raw) {
93
94
  }
94
95
 
95
96
  /** 身份 → 可写段号;无写权身份 → null。评审实例绑定优先(评审者的身份即"设计评审"。)
96
- * depth-0 的段白名单(append = §1/§4/§6)由 appendBatchRecord 分支,不经本表。 */
97
- function allowedSegment(agent, review) {
97
+ * depth-0 的段白名单(append——模式感知,§4.16)由 appendBatchRecord 分支,不经本表;
98
+ * 普通模式分支额外合并 coder 增表(`SEGMENT_BY_ROLE_NORMAL`——**合并查只在普通模式分支**)。 */
99
+ function allowedSegment(agent, review, engineering) {
98
100
  if (review) return 3
99
- return SEGMENT_BY_ROLE[agent?._role] ?? null
101
+ const role = agent?._role
102
+ return SEGMENT_BY_ROLE[role] ?? (engineering ? null : SEGMENT_BY_ROLE_NORMAL[role]) ?? null
100
103
  }
101
104
 
102
105
  /**
@@ -146,8 +149,9 @@ function insertIntoSection(src, seg, text) {
146
149
 
147
150
  /**
148
151
  * append 执行体(action=append——原 batch_segment execute 迁移 + depth-0 分支):
149
- * - 段白名单:depth-0 → §1/§4/§6(新错误面,"batch:" 前缀);eng-designer → §2 · 评审 → §3 ·
150
- * eng-coder → §5(迁移错误面,"batch_segment:" 前缀**逐字保持**);
152
+ * - 段白名单(**模式感知**——§4.16):depth-0 工程 → §1/§4/§6(新错误面,"batch:" 前缀)∥
153
+ * depth-0 普通 → §1/§2/§4/§5/§6(模式化新串);eng-designer → §2 · 评审 → §3 ·
154
+ * eng-coder → §5 · coder(普通模式分支增表)→ §5(迁移错误面,"batch_segment:" 前缀**逐字保持**);
151
155
  * - 目标定位(D-BR21):depth-0 path 可选(缺省 = findInFlightBatch 唯一在飞批);子代理/评审
152
156
  * 传 path ⇒ 拒,目标 = spawn/实例绑定;
153
157
  * - 其余(gate / text 校验 / 剥凭证 / 骨架保护 / **死占位机检(F11-C)** / 插入 / 记账 / 回执)逐字保持原语义。
@@ -156,20 +160,29 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
156
160
  const agent = ctx?.agent ?? {}
157
161
  const cwd = agent.cwd ?? process.cwd()
158
162
  const depth0 = ctx?.depth === 0
159
- const seg = allowedSegment(agent, review)
163
+ // 模式位执行期实读(§4.16——同款先例 = task.mjs execute 门;翻转即时生效——D-BR26)
164
+ const engineering = !!agent?.config?.agent?.engineering
165
+ const seg = allowedSegment(agent, review, engineering)
160
166
  if (!depth0 && seg === null) {
161
- throw new Error("batch_segment: no segment is writable by this caller — the channel exists for eng-designer (§2), eng-coder (§5) and design reviews bound to a batch record (§3); the parent agent writes §1/§4/§6 through ordinary document writes.")
167
+ throw new Error(engineering
168
+ ? "batch_segment: no segment is writable by this caller — the channel exists for eng-designer (§2), eng-coder (§5) and design reviews bound to a batch record (§3); the parent agent writes §1/§4/§6 through ordinary document writes."
169
+ : "batch_segment: no segment is writable by this caller — the channel exists for eng-designer (§2), eng-coder (§5) and design reviews bound to a batch record (§3); in normal mode the parent agent writes §1/§2/§4/§5/§6 through ordinary document writes.")
162
170
  }
163
171
  const n = segmentNumber(args?.segment)
164
172
  if (n === null) {
165
173
  if (depth0) {
166
- throw new Error(`batch: unknown segment ${JSON.stringify(args?.segment ?? null)} — depth-0 append writes §1/§4/§6 of the in-flight record (pass the section number you write, e.g. "§4").`)
174
+ throw new Error(engineering
175
+ ? `batch: unknown segment ${JSON.stringify(args?.segment ?? null)} — depth-0 append writes §1/§4/§6 of the in-flight record (pass the section number you write, e.g. "§4").`
176
+ : `batch: unknown segment ${JSON.stringify(args?.segment ?? null)} — in normal mode depth-0 append writes §1/§2/§4/§5/§6 of the in-flight record (pass the section number you write, e.g. "§4").`)
167
177
  }
168
178
  throw new Error(`batch_segment: unknown segment ${JSON.stringify(args?.segment ?? null)} — pass the section number you write (e.g. "§${seg}").`)
169
179
  }
170
180
  if (depth0) {
171
- if (n !== 1 && n !== 4 && n !== 6) {
172
- throw new Error(`batch: §${n} is not yours to write — depth-0 append writes §1/§4/§6 only (一段一作者: §2 = eng-designer, §3 = design review, §5 = eng-coder).`)
181
+ const domain = engineering ? DEPTH0_SEGMENTS.append.engineering : DEPTH0_SEGMENTS.append.normal
182
+ if (!domain.includes(n)) {
183
+ throw new Error(engineering
184
+ ? `batch: §${n} is not yours to write — depth-0 append writes §1/§4/§6 only (一段一作者: §2 = eng-designer, §3 = design review, §5 = eng-coder).`
185
+ : `batch: §${n} is not yours to write — in normal mode depth-0 append writes §1/§2/§4/§5/§6 (一段一作者: §3 = design review only). Nothing was written.`)
173
186
  }
174
187
  } else if (n !== seg) {
175
188
  throw new Error(`batch_segment: §${n} is not yours to write — this caller writes §${seg} only (一段一作者: eng-designer → §2, design review → §3, eng-coder → §5).`)
@@ -211,9 +224,9 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
211
224
  if (!body.trim()) {
212
225
  throw new Error("batch_segment: nothing to append — the text is empty after credential stripping (credential values never reach the record).")
213
226
  }
214
- // F11-C(挂点:gate / text 校验后 · insertIntoSection 前):档头 + 目标段死占位残留 ⇒ 拒
215
- // (他段占位不拦——一段一作者;骨架枚举单源 = skeleton 档 TEMPLATE_PLACEHOLDERS)。
216
- const residues = findPlaceholderResidue(src, n)
227
+ // F11-C(挂点:gate / text 校验后 · insertIntoSection 前):档头结构行死占位残留 ⇒ 拒
228
+ // (段体引用豁免——判据可辨性收正,`TOOLS.md` §6.20;骨架枚举单源 = skeleton 档 TEMPLATE_PLACEHOLDERS)。
229
+ const residues = findPlaceholderResidue(src)
217
230
  if (residues.length) throw new Error(placeholderResidueError(residues))
218
231
  const { written, roundN } = insertIntoSection(src, n, body)
219
232
  writeFileSync(abs, written)
@@ -246,7 +259,7 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
246
259
  },
247
260
  segment: {
248
261
  type: "string",
249
- description: "append/status only — the batch-record section you are writing (e.g. \"§4\"). Declares the section number only; your identity decides what is actually writable (main agent: append §1/§4/§6 · status §1; eng-designer §2; design review §3; eng-coder §5).",
262
+ description: "append/status only — the batch-record section you are writing (e.g. \"§4\"). Declares the section number only; your identity decides what is actually writable (main agent: append §1/§4/§6 · status §1 (engineering) / append §1/§2/§4/§5/§6 · status §1/§2/§5 (normal); eng-designer §2; eng-coder §5; normal-mode coder with a batchDoc binding §5; design review §3).",
250
263
  },
251
264
  text: {
252
265
  type: "string",
@@ -268,6 +281,14 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
268
281
  type: "string",
269
282
  description: "create only — REQUIRED: the record's origin line (来源 = …; single line). It fills the header's 编制 line; without it the skeleton's placeholder would block the record's first append/status (topic-style fail-closed). Nothing is written when it is missing.",
270
283
  },
284
+ ledger: {
285
+ type: "string",
286
+ description: "create only — optional: the ledger entry number for the header's 台账 line (e.g. \"#938\"; a leading # is added if missing). Passed together with board, it fills the header at creation; when omitted, the placeholder stays and must be filled by file edit before the record's first append/status.",
287
+ },
288
+ board: {
289
+ type: "string",
290
+ description: "create only — optional: the owning board for the header's 台账 line (e.g. \"core\"). See ledger — both fill the header at creation.",
291
+ },
271
292
  date: {
272
293
  type: "string",
273
294
  description: "create only — the record date (YYYY-MM-DD); defaults to today.",
@@ -0,0 +1,198 @@
1
+ /**
2
+ * browser-async.mjs — browser 后台动作任务桥(池 ∥ 起跑 ∥ 结算 ∥ 注入 ∥ 杀单点——复用 bg 族单点,D2)。
3
+ * 设计权威 = `docs/core/design/BROWSER-TOOL.md` §2.11(异步通道——F-BT18)∥ §2.9(后台动作仍受动作
4
+ * 预算——timeoutMs 可覆写)∥ §2.10(外部关闭在飞 ⇒ 同因由结算);批档 = `docs/batches/2026-10-07-browser-async-fix.md` §2。
5
+ *
6
+ * - 形态:`async:true`(depth-0 专项——schema 删参 + 运行期第二道)⇒ ack
7
+ * `browser#<N> started (running) — <action> <subject>` 即返 ⇒ 后台同队执行(同一串行队列——单页
8
+ * 不引入并发)⇒ 结算**恒停靠** `_pendingAsyncResults` + 唤醒挂起驱动(bg 同式——消化轮注入)。
9
+ * - 池:`_browserTasks`(角色 "browser";帽 `BROWSER_TASK_MAX = 4` 对齐单域 4,第 5 条显式拒——零静默丢)。
10
+ * - 取消:`process action=kill id:N`(id 路由先 bg 后 browser)⇒ queued 丢队(从未运行)∥ running
11
+ * abort(动作展开)⇒ 结算 cancelled(墓碑 + 无摘要;重复幂等);**不杀浏览器**(重置经 `close`)。
12
+ * 取消柄 = 条目控制器信号(`browser/session.mjs` `runAction` 的 `{signal}` 缝——一条机制两个因由)。
13
+ * - 收尾:Stop ∥ 会话中止 ⇒ `async-discard.mjs` `BROWSER_SPEC`(dispose = 条目控制器 abort)+ 墓碑
14
+ * `discarded` 出池 + 整批一次提醒(模型可见凭据);回合中断(Ctrl+I)经 `bindChildController` 豁免
15
+ * (池保留——bg 同款)。竞态窗(条目先自行结算、收尾面未及见)⇒ 按 bg 族兜底:正常两态照常停靠
16
+ * (摘要必达——零静默丢失),**不**自落「仅墓碑」降级支(墓碑不是模型可见凭据)。
17
+ * - 注入:`injectBrowserResult`(digest 单点族成员——`injectAsyncResult` 按 role="browser" 分发);
18
+ * 回执正文受动作面既有上限(≤20,000 字符)⇒ 无需 offload 径。
19
+ * - 等待:`wait_for "browser id:N done"`(`browserTaskDone`——池内 done ∥ 出池即 done)。
20
+ * 入径 = `tools/browser.mjs` ∥ `tools/ops.mjs` **动态 import**(W8 契约②);模块图:→ browser/session.mjs
21
+ * (入队面)· agent-tools/async-settle.mjs · subagent-scheduler.mjs(取号 / 令牌 / 键守卫)·
22
+ * agent/helpers.mjs(escapeXml)· context.mjs(pushReal)——叶子向、无环。
23
+ */
24
+ import { escapeXml } from "../agent/helpers.mjs"
25
+ import { pushReal } from "../context.mjs"
26
+ import { runAction } from "../browser/session.mjs"
27
+ import {
28
+ bindChildController, buildChildSignal, carrierField, getAsyncPool, parkAsyncPending,
29
+ tombstoneOf, wakeAsyncWaiters, writeTombstone,
30
+ } from "./async-settle.mjs"
31
+ import { assertPoolKeyFree, consumeSubagentToken, nextSubagentId } from "./subagent-scheduler.mjs"
32
+
33
+ /** 后台动作帽(对齐 `ASYNC_POOL_LIMITS` 单域 4——KD-22)。超限显式拒。 */
34
+ export const BROWSER_TASK_MAX = 4
35
+
36
+ /** 池写侧(绑定不变式:主容器落父字段;缺省借用 / 别名 `history` 侧——同 `bash-async.mjs` 同式)。 */
37
+ function ensureBrowserPool(parent) {
38
+ if (parent._browserTasks instanceof Map) return parent._browserTasks
39
+ const borrowed = carrierField(parent, "_browserTasks")
40
+ if (borrowed instanceof Map) { parent._browserTasks = borrowed; return borrowed }
41
+ parent._browserTasks = new Map()
42
+ if (parent?.history && typeof parent.history === "object") parent.history._browserTasks = parent._browserTasks
43
+ return parent._browserTasks
44
+ }
45
+
46
+ /** 条目读取(键形单源:写侧 `set(String(id))` ⇒ 读一律 String 归一;双形键兜底照 bg 族)。 */
47
+ export function browserTaskEntry(parent, id) {
48
+ const pool = getAsyncPool(parent, "browser")
49
+ if (!(pool instanceof Map)) return null
50
+ const key = String(id)
51
+ return pool.get(key) ?? pool.get(Number(key)) ?? null
52
+ }
53
+
54
+ /** `wait_for` 条件 `browser id:N done` 判据(同式 bg):池内 done ∥ **出池即 done**(结算恒出池——
55
+ * pending 停靠 / 杀 / 丢弃皆已结束);未知 id 亦按 done-by-vacuity。 */
56
+ export function browserTaskDone(parent, id) {
57
+ const entry = browserTaskEntry(parent, id)
58
+ if (!entry) return true
59
+ return entry.done === true || entry.status === "done"
60
+ }
61
+
62
+ /** 条目中止(同一机制两处调用:kill 径 `killBrowserTask` ∥ discard 收尾面 `BROWSER_SPEC.dispose`):
63
+ * 条目控制器 abort ⇒ `runAction` 信号径传导(queued 丢队 ∥ running 动作展开);重复幂等
64
+ * (`AbortController.abort` 幂等)。 */
65
+ function abortBrowserEntry(entry) {
66
+ try { entry?.controller?.abort?.({ abortTrigger: "browser-kill" }) } catch { /* 已中止 */ }
67
+ }
68
+
69
+ /** subject 标签(ack ∥ 摘要共用):动作对象头——navigate = url ∥ ref / selector / url / key / op /
70
+ * from / networkIdle ∥ 坐标 ∥ expression 头 ∥ text 头;全空 ⇒ 空串。 */
71
+ export function browserSubject(action, args = {}) {
72
+ const pick = action === "navigate" ? (args.url ?? null)
73
+ : (args.ref ?? args.selector ?? args.url ?? args.key ?? args.op ?? args.from
74
+ ?? (args.networkIdle === true ? "networkIdle" : null))
75
+ if (pick != null && String(pick) !== "") return String(pick)
76
+ if (String(args.x ?? "") !== "" && String(args.y ?? "") !== "" && Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y))) {
77
+ return `${args.x},${args.y}`
78
+ }
79
+ if (typeof args.expression === "string" && args.expression) return args.expression.slice(0, 60)
80
+ if (typeof args.text === "string" && args.text) return args.text.slice(0, 40)
81
+ return ""
82
+ }
83
+
84
+ /**
85
+ * 起跑(ack 即返——调用方不被动作阻塞)。失败面 = 显式 `{ error }`(帽 / 深度 / 上下文——零静默丢)。
86
+ * @returns {{ id: number, ack: string, entry: object } | { error: string }}
87
+ */
88
+ export async function launchBrowserTask(parent, ctx, args = {}) {
89
+ if (!parent) return { error: "no agent context — background browser tasks run in a depth-0 session only" }
90
+ if ((ctx?.depth ?? 0) > 0) {
91
+ return { error: "browser async is depth-0 only — a child agent has no background task pool (run the action synchronously)" }
92
+ }
93
+ const action = String(args.action ?? "")
94
+ const pool = ensureBrowserPool(parent)
95
+ const running = [...pool.values()].filter((e) => e.done !== true)
96
+ if (running.length >= BROWSER_TASK_MAX) {
97
+ return {
98
+ error: `browser task cap reached (${running.length}/${BROWSER_TASK_MAX} running: ${running.map((e) => `browser#${e.id}`).join(", ")})` +
99
+ ` — wait for one to settle (wait_for "browser id:N done") or kill one (process action='kill' id:N)`,
100
+ }
101
+ }
102
+ // ED-5:取号 → 消费同步配对(无 await 间隙)+ 入池键守卫(同 bash-async 同式——守卫前置于起跑,命中即抛零副作用)。
103
+ const id = nextSubagentId(parent)
104
+ consumeSubagentToken(parent, id, "browser async launch", "browser")
105
+ assertPoolKeyFree(pool, id, "browser")
106
+ const { async: _asyncFlag, ...actionArgs } = args // `async` 不入动作参数面(零语义泄漏)
107
+ const subject = browserSubject(action, actionArgs)
108
+ const ctrl = new AbortController()
109
+ const entry = {
110
+ id, role: "browser", action, args: actionArgs, subject,
111
+ status: "running", done: false, cancelled: false, discarded: false,
112
+ controller: ctrl, startedAt: Date.now(), endedAt: null,
113
+ receipt: null, error: null, failed: null, _settle: null, promise: null,
114
+ }
115
+ entry.promise = new Promise((res) => { entry._settle = res })
116
+ // 会话 / 回合信号链(同 bg):非 interrupt 中止 ⇒ 控制器中止 ⇒ 动作展开 + discard 收尾面;
117
+ // 回合中断(Ctrl+I)⇒ 不动(池保留)。
118
+ bindChildController(ctrl, buildChildSignal(parent, ctx))
119
+ entry.promiseRun = Promise.resolve()
120
+ .then(() => runAction(action, actionArgs, ctx, { signal: ctrl.signal }))
121
+ .then(
122
+ (receipt) => { entry.receipt = String(receipt) },
123
+ (e) => { entry.error = e?.message ?? String(e) },
124
+ )
125
+ .finally(() => finalizeBrowserEntry(parent, entry))
126
+ pool.set(String(id), entry)
127
+ return { id, ack: `browser#${id} started (running) — ${action}${subject ? ` ${subject}` : ""}`, entry }
128
+ }
129
+
130
+ /**
131
+ * 结算单点(回执 / 拒绝两径合流;幂等:entry.done 即返)。取消 / 丢弃两态 ⇒ 只落终态与唤醒(无摘要——
132
+ * 凭据 = 杀点确认 ∥ 收尾整批提醒);其余(含会话中止竞态窗——收尾面未及见)⇒ 恒停靠 pending + 唤醒
133
+ * 挂起驱动(消化轮注入——零静默丢失;同 `bash-async.mjs` finalize 同式,**不**自落「仅墓碑」支)。
134
+ */
135
+ function finalizeBrowserEntry(parent, entry) {
136
+ if (entry.done) return
137
+ entry.done = true
138
+ entry.status = "done"
139
+ entry.endedAt = Date.now()
140
+ getAsyncPool(parent, "browser")?.delete(String(entry.id))
141
+ if (entry.cancelled === true || entry.discarded === true) {
142
+ entry._settle?.() // 定向杀 ∥ 会话中止收尾——不注入(墓碑已由杀点 / 收尾面落定)
143
+ return
144
+ }
145
+ const receipt = entry.receipt ?? ""
146
+ if (entry.error == null && receipt.startsWith("Error:")) {
147
+ entry.failed = receipt.split("\n")[0].replace(/^Error:\s*/, "")
148
+ }
149
+ parkAsyncPending(parent, entry)
150
+ entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
151
+ entry._settle?.()
152
+ wakeAsyncWaiters(parent)
153
+ }
154
+
155
+ /** 状态词截断(摘要首行单行可读——超长因由截 200 字符)。 */
156
+ function clip(text, max = 200) {
157
+ const s = String(text)
158
+ return s.length > max ? `${s.slice(0, max)}…` : s
159
+ }
160
+
161
+ /** 注入器(digest 单点族成员——`injectAsyncResult` 按 role="browser" 分发;消费即墓碑)。 */
162
+ export async function injectBrowserResult(parent, entry) {
163
+ const seconds = (Math.max(0, (entry.endedAt ?? Date.now()) - entry.startedAt) / 1000).toFixed(1)
164
+ const label = `${entry.action}${entry.subject ? ` ${entry.subject}` : ""}`
165
+ const status = entry.error != null ? `failed: ${clip(entry.error)}`
166
+ : entry.failed != null ? `failed: ${clip(entry.failed)}`
167
+ : "ok"
168
+ const head = `[System reminder: background browser#${entry.id} finished — ${label} (${status}, ${seconds}s)]`
169
+ const body = escapeXml(String(entry.receipt ?? entry.error ?? "(no result)"))
170
+ pushReal(parent, { role: "user", content: `${head}\n${body}` })
171
+ writeTombstone(parent, entry.id, entry.error != null || entry.failed != null ? "failed" : "consumed", "browser")
172
+ }
173
+
174
+ /**
175
+ * 杀单点(id 靶——`process` action='kill'):queued ⇒ 丢队(从未运行)∥ running ⇒ abort(动作展开)
176
+ * ⇒ 结算 cancelled(墓碑 + 无摘要);**不杀浏览器**;重复 = 幂等(经终态墓碑返回同一确认——bg 同式)。
177
+ * @returns {{ id: string, status: "cancelled" | "error", error?: string }}
178
+ */
179
+ export function killBrowserTask(parent, id) {
180
+ const key = String(id)
181
+ if (!key || key === "undefined" || key === "null") {
182
+ return { id: key, status: "error", error: "process kill id: requires a background browser task id (browser#N from the async ack)" }
183
+ }
184
+ const pool = getAsyncPool(parent, "browser")
185
+ const entry = browserTaskEntry(parent, key)
186
+ if (!entry) {
187
+ const tomb = tombstoneOf(parent, key)
188
+ if (tomb?.status === "cancelled" && tomb.role === "browser") return { id: key, status: "cancelled" } // 幂等确认
189
+ return { id: key, status: "error", error: `unknown background browser task id: ${key} — it has finished, was killed, or was never started` }
190
+ }
191
+ if (entry.done) return { id: key, status: "error", error: `browser#${key} has already finished — nothing to kill` }
192
+ if (entry.cancelled) return { id: key, status: "cancelled" } // 杀已在途——幂等
193
+ entry.cancelled = true
194
+ abortBrowserEntry(entry)
195
+ pool?.delete(key)
196
+ writeTombstone(parent, key, "cancelled", "browser")
197
+ return { id: key, status: "cancelled" }
198
+ }
@@ -59,7 +59,7 @@ export function registerTurnCapCheckpoint(child, carrier, { turn = 0, signal } =
59
59
  if (signal?.aborted) return Promise.resolve(false) // ② 已中止 ⇒ 立即降级(不进挂起)
60
60
  const key = String(carrier.id ?? "")
61
61
  const rec = { child, carrier, turn }
62
- // 先臂后报请(顺序即防御):`rec.resolve` 与中止监听在入队**前**就位——否则
62
+ // 先布防、后报请(顺序即防御):`rec.resolve` 与中止监听在入队**前**就位——否则
63
63
  // `pushChildUpstream` 同步唤醒等待栓的窗口里「登记已写、resolve 未挂」,兑现假成功而挂起不收敛。
64
64
  let abortedNow = signal?.aborted === true
65
65
  const promise = new Promise((resolve) => {