@thincoder/core 0.10.4 → 0.10.6

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 (153) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +1 -1
  3. package/advisor/compaction.mjs +11 -6
  4. package/advisor/run.mjs +4 -4
  5. package/agent/assemble.mjs +1 -1
  6. package/agent/chat-call.mjs +1 -1
  7. package/agent/child-marks.mjs +2 -2
  8. package/agent/digest-account.mjs +5 -4
  9. package/agent/dispatch-gates.mjs +8 -0
  10. package/agent/dispatch.mjs +39 -18
  11. package/agent/family-tools.mjs +22 -11
  12. package/agent/helpers.mjs +4 -3
  13. package/agent/light-round.mjs +66 -0
  14. package/agent/post-turn.mjs +8 -4
  15. package/agent/run-stages.mjs +5 -3
  16. package/agent/run-start.mjs +3 -2
  17. package/agent/setup-reminders.mjs +1 -1
  18. package/agent/setup.mjs +18 -3
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +8 -5
  21. package/agent/timers.mjs +56 -5
  22. package/agent/turn-loop.mjs +5 -3
  23. package/agent/write-gate.mjs +7 -7
  24. package/agent-tools/advisor-async.mjs +2 -2
  25. package/agent-tools/advisor-settle.mjs +2 -2
  26. package/agent-tools/advisor.mjs +1 -1
  27. package/agent-tools/async-discard.mjs +34 -0
  28. package/agent-tools/async-settle.mjs +3 -2
  29. package/agent-tools/audit-block.mjs +1 -1
  30. package/agent-tools/bash-async.mjs +1 -1
  31. package/agent-tools/batch-lifecycle.mjs +35 -14
  32. package/agent-tools/batch-paths.mjs +4 -4
  33. package/agent-tools/batch-skeleton.mjs +29 -11
  34. package/agent-tools/batch.mjs +28 -15
  35. package/agent-tools/browser-async.mjs +198 -0
  36. package/agent-tools/checkpoint.mjs +1 -1
  37. package/agent-tools/design-slots.mjs +232 -0
  38. package/agent-tools/digest-budget.mjs +1 -1
  39. package/agent-tools/manifest.mjs +193 -0
  40. package/agent-tools/parent-channel.mjs +7 -4
  41. package/agent-tools/review-facts.mjs +13 -13
  42. package/agent-tools/settings.mjs +1 -1
  43. package/agent-tools/skill.mjs +1 -1
  44. package/agent-tools/subagent-async.mjs +21 -12
  45. package/agent-tools/subagent-scheduler.mjs +8 -5
  46. package/agent-tools/subagent-spawn.mjs +29 -55
  47. package/agent-tools/subagent.mjs +21 -8
  48. package/agent-tools/timer.mjs +186 -12
  49. package/agent-tools/zero-write-watch.mjs +2 -2
  50. package/agent-tools.mjs +2 -1
  51. package/agent.mjs +1 -1
  52. package/attachments.mjs +1 -1
  53. package/browser/actions.mjs +163 -0
  54. package/browser/cdp.mjs +161 -0
  55. package/browser/clipboard.mjs +142 -0
  56. package/browser/input-actions.mjs +197 -0
  57. package/browser/input.mjs +256 -0
  58. package/browser/launch.mjs +143 -0
  59. package/browser/queue.mjs +171 -0
  60. package/browser/session.mjs +494 -0
  61. package/browser/snapshot.mjs +270 -0
  62. package/config-io.mjs +18 -13
  63. package/config-migrate.mjs +13 -29
  64. package/config-presets.mjs +33 -29
  65. package/config.mjs +68 -25
  66. package/context-degrade.mjs +1 -1
  67. package/context-echo.mjs +1 -1
  68. package/context-push.mjs +2 -2
  69. package/context.mjs +1 -1
  70. package/conventions.mjs +2 -2
  71. package/declaration.mjs +18 -2
  72. package/file-links.mjs +1 -1
  73. package/file-refs.mjs +1 -1
  74. package/history-window.mjs +1 -1
  75. package/i18n.mjs +2 -0
  76. package/ledger-cmd.mjs +62 -16
  77. package/ledger-db.mjs +6 -0
  78. package/ledger-surface.mjs +49 -5
  79. package/ledger-tools.mjs +16 -8
  80. package/ledger.mjs +9 -16
  81. package/log.mjs +1 -1
  82. package/manifest-discovery.mjs +1 -1
  83. package/manifest-schema.mjs +1 -1
  84. package/mcp/transport-stdio.mjs +1 -1
  85. package/mcp.mjs +1 -1
  86. package/memory/code-index.mjs +1 -1
  87. package/memory/code-search.mjs +1 -1
  88. package/memory/code-sync.mjs +1 -1
  89. package/memory/docs.mjs +6 -2
  90. package/memory/file-list.mjs +1 -1
  91. package/memory/file-walk.mjs +17 -5
  92. package/memory/maintain.mjs +410 -0
  93. package/memory/memory-tool.mjs +21 -6
  94. package/memory/schema.mjs +19 -7
  95. package/memory/sweep.mjs +8 -5
  96. package/memory/sync-tail.mjs +16 -10
  97. package/model-ref.mjs +16 -13
  98. package/model-specs.mjs +21 -9
  99. package/package.json +2 -1
  100. package/peer-instances.mjs +1 -1
  101. package/permission.mjs +33 -0
  102. package/process-probe.mjs +2 -2
  103. package/prompts/advisor-design.md +4 -2
  104. package/prompts/advisor-round1.md +1 -1
  105. package/prompts/advisor-round2.md +1 -1
  106. package/prompts/advisor-round3.md +1 -1
  107. package/prompts/common.md +36 -2
  108. package/prompts/consult-base.md +1 -1
  109. package/prompts/discipline-engineering.md +32 -15
  110. package/prompts/discipline-normal.md +46 -2
  111. package/prompts/persona-eng-designer.md +6 -6
  112. package/prompts/persona-engineering.md +14 -6
  113. package/prompts/persona-normal.md +2 -0
  114. package/prompts/subagent-base.md +3 -0
  115. package/provider/core.mjs +6 -0
  116. package/provider/errors.mjs +5 -3
  117. package/provider/google.mjs +9 -8
  118. package/provider/list-models.mjs +3 -2
  119. package/provider/sse.mjs +3 -0
  120. package/provider-flows.mjs +14 -15
  121. package/proxy-chunked.mjs +102 -0
  122. package/proxy-target.mjs +54 -0
  123. package/proxy-transport.mjs +298 -0
  124. package/proxy.mjs +17 -248
  125. package/session-gc.mjs +2 -2
  126. package/session-lifecycle.mjs +18 -14
  127. package/session-slot-claims.mjs +2 -2
  128. package/session-slots-manifest.mjs +1 -1
  129. package/session-slots.mjs +3 -3
  130. package/team-ledger.mjs +282 -0
  131. package/team.mjs +223 -0
  132. package/tool-docs/bash.md +1 -0
  133. package/tool-docs/batch.md +1 -1
  134. package/tool-docs/browser.md +48 -0
  135. package/tool-docs/ledger.md +3 -3
  136. package/tool-docs/manifest.md +16 -0
  137. package/tool-docs/memory.md +1 -0
  138. package/tool-docs/notify_parent.md +5 -0
  139. package/tool-docs/process.md +4 -4
  140. package/tool-docs/question.md +1 -0
  141. package/tool-docs/subagent.md +4 -3
  142. package/tool-docs/team_ledger.md +9 -0
  143. package/tool-docs/timer.md +6 -0
  144. package/tool-docs/wait_for.md +1 -0
  145. package/tools/browser.mjs +161 -0
  146. package/tools/git.mjs +1 -1
  147. package/tools/index.mjs +3 -0
  148. package/tools/ops.mjs +26 -10
  149. package/tools/repomap.mjs +1 -1
  150. package/tools/write-path.mjs +2 -2
  151. package/traces/trace-cleanup.mjs +1 -1
  152. package/traces/trace-store.mjs +1 -1
  153. package/vision-reader.mjs +10 -16
@@ -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).`)
@@ -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",
@@ -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) => {
@@ -0,0 +1,232 @@
1
+ /**
2
+ * design-slots.mjs — design-token 槽位清点 + 消费核(2026-10-07 批 ledger-tool · 台账 #927 并入)。
3
+ * 设计单源 = `docs/core/design/DESIGN-TOKEN-SETTLEMENT.md` §10(F-SL1 清点面 / F-SL2 批量消费)。
4
+ *
5
+ * 面一 `executeDesignSlotsAction`(`subagent` action `design-slots`——只读):
6
+ * 数据源 = 槽文件权威表(`readEngTokensFromSlot`——无 TTL 过滤原样)∪ 内存 Map(并集去重,
7
+ * 冲突以槽为准——D-S3 同源);**零写**(不 reconcile 回写、不落盘)。列表面 = 逐槽一行
8
+ * (designId ∥ live/expired ∥ ageDays ∥ expiresAt ∥ 批;龄序 = 最老在前)+ 计数尾行;
9
+ * 「批」= `_advisorRuns` 按 designId 命中 run 的 `docSetKey` 中 `batches/` 段档取 basename
10
+ * (KD-SL2——零新存储;不可得 ⇒ `—`)。
11
+ * 面二 `executeConsumeDesignAction`(`subagent` action `consume-design` 消费核——自
12
+ * `subagent-spawn.mjs` 迁入):选择器恰一(`designId` ∥ `designIds` ∥ `expired` ∥
13
+ * `olderThanDays`——KD-SL3 fail-closed 不猜);目标集 = 并集视图按选择器解析 → 逐枚移出
14
+ * (`removeDesignTokenSlot` 同款 token 值守护)→ **单次** `persistEngTokens` 落盘(D-S6 单点);
15
+ * 落盘失败 ⇒ 内存快照整体回滚 + 抛(KD-SL4——不留半消费态)。单枚形态(designId / 无选择器
16
+ * 单槽):语义 / 拒文 / 回执**现行逐字不变**。
17
+ *
18
+ * 签发链行为零改(mint / settle / TTL / persist 存储形——#868 面另论);工程模式父侧(depth-0)限定
19
+ * (门 = 本档两执行器;depth 面由装配与受限变体门承载)。
20
+ */
21
+ import { persistEngTokens, readEngTokensFromSlot, reconcileEngTokensFromSlot, removeDesignTokenSlot, tokenExpired, tokenExpiryMs } from "../token-ttl.mjs"
22
+ import { effectiveTokenTtlMs } from "./design-token.mjs"
23
+ import { advisorRuns } from "./advisor-async.mjs"
24
+
25
+ const DAY_MS = 86400000
26
+
27
+ /** 槽并集视图(F-SL1 数据源 / F-SL2 目标集解析):内存 Map 先行、槽文件权威表覆盖(同 id 冲突以槽
28
+ * 为准——D-S3 同源);纯读——零回写、零落盘。 */
29
+ function unionSlots(agent) {
30
+ const merged = new Map()
31
+ const mem = agent?._engDesignTokens
32
+ if (mem instanceof Map) for (const [id, t] of mem) merged.set(id, t)
33
+ const file = readEngTokensFromSlot(agent)
34
+ if (file) for (const [id, t] of Object.entries(file)) merged.set(id, t)
35
+ return merged
36
+ }
37
+
38
+ /** 「批」列派生(KD-SL2——零新存储):会话内评审实例注册表 `_advisorRuns` 按 designId 命中的**最新**
39
+ * run ⇒ `docSetKey`(被审文档绝对路径集 JSON)中 `batches/` 段档取 basename;不可得(跨重启 /
40
+ * 模式重进 / 注册表回收)⇒ `—`(如实降级)。 */
41
+ function batchOf(agent, designId) {
42
+ const runs = advisorRuns(agent)
43
+ if (!(runs instanceof Map)) return "—"
44
+ for (const run of [...runs.values()].reverse()) {
45
+ if (run?.designId !== designId || typeof run.docSetKey !== "string") continue
46
+ const name = batchBasename(run.docSetKey)
47
+ if (name) return name
48
+ }
49
+ return "—"
50
+ }
51
+
52
+ /** docSetKey(JSON 串)→ `batches/` 段档 basename(首命中);不可解析 / 无命中 ⇒ null。 */
53
+ function batchBasename(docSetKey) {
54
+ let list = null
55
+ try { list = JSON.parse(docSetKey) } catch { return null }
56
+ if (!Array.isArray(list)) return null
57
+ for (const p of list) {
58
+ if (typeof p !== "string") continue
59
+ const parts = p.split(/[\\/]/)
60
+ const i = parts.lastIndexOf("batches")
61
+ if (i >= 0 && i < parts.length - 1 && parts[i + 1]) return parts[i + 1]
62
+ }
63
+ return null
64
+ }
65
+
66
+ /** 槽事实(清点列与选择器共用):状态 = `tokenExpired` 单源;`ageDays` = now − (expiresAt −
67
+ * `effectiveTokenTtlMs`) 派生(TTL 配置曾改时为近似——如实注);`expiresAt` 精确;畸形 / 格式外
68
+ * token ⇒ 两项 `—`(不可判龄)。 */
69
+ function slotFacts(agent, designId, token, now, ttlMs) {
70
+ const expiry = typeof token === "string" ? tokenExpiryMs(token) : null
71
+ return {
72
+ designId, token,
73
+ expired: typeof token === "string" && tokenExpired(token, now),
74
+ ageDays: expiry === null ? null : (now - (expiry - ttlMs)) / DAY_MS,
75
+ expiresAt: expiry,
76
+ batch: batchOf(agent, designId),
77
+ }
78
+ }
79
+
80
+ /** 龄序比较(最老在前——剩余有效期升序;不可判龄(畸形串)排尾——不冒充最老)。 */
81
+ const byOldest = (a, b) => (b.ageDays ?? -Infinity) - (a.ageDays ?? -Infinity)
82
+
83
+ /**
84
+ * `subagent` action `design-slots`(F-SL1——只读清点面):工程模式父侧限定(与 consume-design
85
+ * 同门)。输出 = 行文本(逐槽行 + 计数尾行);字段 / 序 / 降级标记 = 契约,行内措辞 = 实现面。
86
+ * 分类 = 只读(planMode 放行 / 免审批 / digest 放行——谓词在 `agent/dispatch-gates.mjs`)。
87
+ */
88
+ export function executeDesignSlotsAction(args, ctx) {
89
+ const parent = ctx?.agent
90
+ if (!parent?.config?.agent?.engineering) {
91
+ throw new Error("Engineering mode is not active — design-slots applies only to engineering-mode design tokens (spawn 同门).")
92
+ }
93
+ const now = Date.now()
94
+ const ttlMs = effectiveTokenTtlMs(parent)
95
+ const entries = [...unionSlots(parent)].map(([id, t]) => slotFacts(parent, id, t, now, ttlMs))
96
+ entries.sort(byOldest)
97
+ const lines = entries.map((e) =>
98
+ `- designId=${e.designId} status=${e.expired ? "expired" : "live"} ageDays=${e.ageDays === null ? "—" : e.ageDays.toFixed(1)} expiresAt=${e.expiresAt === null ? "—" : new Date(e.expiresAt).toISOString()} batch=${e.batch}`)
99
+ const live = entries.filter((e) => !e.expired).length
100
+ lines.push(`design-slots: ${entries.length} slot(s) — ${live} live / ${entries.length - live} expired`)
101
+ return lines.join("\n")
102
+ }
103
+
104
+ /**
105
+ * `subagent` action `consume-design` 消费核(F-SL2;自 `subagent-spawn.mjs` 迁入——2026-10-07):
106
+ * 选择器恰一——`designId`(现行 · 单枚)∥ `designIds`(显式非空清单)∥ `expired:true`(全部过期项)
107
+ * ∥ `olderThanDays:N`(ageDays > N);多选择器同给 / 选择器缺失且多槽 ⇒ 拒(fail-closed——不猜)。
108
+ * 单枚形态走 `consumeSingle`(拒文 / 回执逐字不变);批量走 `consumeBatch`(逐枚行 + `consumed N` 计数行)。
109
+ */
110
+ export function executeConsumeDesignAction(args, ctx) {
111
+ const parent = ctx?.agent
112
+ if (!parent?.config?.agent?.engineering) {
113
+ throw new Error("Engineering mode is not active — consume-design applies only to engineering-mode design tokens (spawn 同门).")
114
+ }
115
+ const slots = parent?._engDesignTokens
116
+ const hasSlots = slots instanceof Map && slots.size > 0
117
+ const designId = args?.designId ? String(args.designId) : undefined
118
+ const selectors = []
119
+ if (designId !== undefined) selectors.push("designId")
120
+ if (args?.designIds !== undefined && args?.designIds !== null) selectors.push("designIds")
121
+ if (args?.expired !== undefined && args?.expired !== null) selectors.push("expired")
122
+ if (args?.olderThanDays !== undefined && args?.olderThanDays !== null) selectors.push("olderThanDays")
123
+ if (selectors.length > 1) {
124
+ throw new Error(`consume-design: pass exactly ONE selector — designId / designIds / expired / olderThanDays (got: ${selectors.join(" + ")}); a batch consume never guesses its target set.`)
125
+ }
126
+ if (selectors.length === 0) {
127
+ // 无选择器 = 单枚兼容形态(现行语义零变):缺 designId 且多槽 ⇒ 拒(不误消费任一槽)
128
+ if (hasSlots && slots.size > 1) {
129
+ throw new Error(`consume-design: Multiple approved designs in this session (${slots.size}) — pass the designId parameter (echoed with each token) to choose which design to close out.`)
130
+ }
131
+ return consumeSingle(parent, undefined)
132
+ }
133
+ if (selectors[0] === "designId") return consumeSingle(parent, designId)
134
+ return consumeBatch(parent, selectors[0], args)
135
+ }
136
+
137
+ /** 单枚消费(现行形态逐字保留——读内存槽;未知 / 已消费 ⇒ 幂等 no-op;落盘失败 ⇒ 槽回滚 + 抛)。 */
138
+ function consumeSingle(parent, designId) {
139
+ const slots = parent?._engDesignTokens
140
+ const hasSlots = slots instanceof Map && slots.size > 0
141
+ // 读槽值:给定 designId → 精确槽(未知/已消费 → undefined);缺省 → 唯一槽
142
+ let token = null
143
+ if (designId && hasSlots) token = slots.get(designId) ?? null
144
+ else if (!designId && hasSlots) token = [...slots.values()][0]
145
+ // 未知 designId / 已消费 / 无任何槽 → 幂等 no-op 提示(不报错——评审 #3 定死)
146
+ if (!token) {
147
+ return `consume-design: no live slot${designId ? ` for designId ${designId}` : ""} (already consumed or never issued) — idempotent no-op, nothing changed.`
148
+ }
149
+ const slotId = designId ?? [...slots.keys()][0]
150
+ removeDesignTokenSlot(parent, designId, token)
151
+ try {
152
+ persistEngTokens(parent)
153
+ } catch (e) {
154
+ if (slotId) parent._engDesignTokens.set(slotId, token)
155
+ throw new Error(`consume-design: the slot was removed in memory but could NOT be durably deleted from the slot file (${e.message}) — the slot is restored in memory; retry consume-design.`)
156
+ }
157
+ return `design slot consumed — designId ${designId ?? "(single-design session)"} is closed out; a further eng-coder spawn for this design is mechanically rejected, and any new work (including deviation fixes) requires a fresh advisor(type='design') review and token.`
158
+ }
159
+
160
+ /** 批量消费(F-SL2 / KD-SL3 / KD-SL4):目标集 = 并集视图按选择器解析;逐枚移出(token 值守护)→
161
+ * 单次 persist;落盘失败 ⇒ 内存快照整体回滚 + 抛。回执 = 逐枚行 + 计数行(`consumed N`)。
162
+ * 落盘前先经 D2 权威表 reconcile(槽 = 权威;未选中的**活**槽因此并入内存、随 persist 保住——
163
+ * 过期项则随本次落盘一并出清,即「落盘携带(如实)」注)。 */
164
+ function consumeBatch(parent, selector, args) {
165
+ const now = Date.now()
166
+ const ttlMs = effectiveTokenTtlMs(parent)
167
+ const snapshot = parent._engDesignTokens instanceof Map ? new Map(parent._engDesignTokens) : null
168
+ reconcileEngTokensFromSlot(parent)
169
+ const union = unionSlots(parent)
170
+ const targets = []
171
+ const seen = new Set()
172
+ if (selector === "designIds") {
173
+ if (!Array.isArray(args.designIds) || args.designIds.length === 0) {
174
+ throw new Error("consume-design: designIds must be a non-empty array of design ids — pass an explicit list, or use expired / olderThanDays.")
175
+ }
176
+ for (const raw of args.designIds) {
177
+ const id = typeof raw === "string" ? raw.trim() : ""
178
+ if (!id) throw new Error(`consume-design: designIds entries must be non-empty strings (got ${JSON.stringify(raw)}).`)
179
+ if (seen.has(id)) continue // 重复条目去重(同一 id 只计一次——回执计数不虚增)
180
+ seen.add(id)
181
+ const token = union.get(id) ?? null
182
+ targets.push({ designId: id, token, consumed: token !== null }) // 未知 / 已消费 ⇒ 逐枚 no-op(幂等)
183
+ }
184
+ } else if (selector === "expired") {
185
+ if (args.expired !== true) {
186
+ throw new Error("consume-design: expired must be true — it selects every expired slot; pass designIds / olderThanDays for other shapes.")
187
+ }
188
+ for (const [id, t] of union) {
189
+ if (typeof t === "string" && tokenExpired(t, now)) {
190
+ const expiry = tokenExpiryMs(t)
191
+ targets.push({ designId: id, token: t, consumed: true, ageDays: expiry === null ? null : (now - (expiry - ttlMs)) / DAY_MS })
192
+ }
193
+ }
194
+ targets.sort(byOldest)
195
+ } else {
196
+ const n = args.olderThanDays
197
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 0) {
198
+ throw new Error("consume-design: olderThanDays must be a non-negative number — pass the day threshold (e.g. olderThanDays: 30).")
199
+ }
200
+ for (const [id, t] of union) {
201
+ const expiry = typeof t === "string" ? tokenExpiryMs(t) : null
202
+ if (expiry === null) continue // 畸形串不可判龄——不被龄选择器选中(清点面照列)
203
+ const ageDays = (now - (expiry - ttlMs)) / DAY_MS
204
+ if (ageDays > n) targets.push({ designId: id, token: t, consumed: true, ageDays })
205
+ }
206
+ targets.sort(byOldest)
207
+ }
208
+ const removals = targets.filter((t) => t.consumed)
209
+ for (const t of removals) removeDesignTokenSlot(parent, t.designId, t.token)
210
+ if (removals.length > 0) {
211
+ try {
212
+ persistEngTokens(parent)
213
+ } catch (e) {
214
+ // KD-SL4:内存快照整体回滚 + 抛(不留半消费态——可重试)
215
+ if (snapshot) {
216
+ const map = parent._engDesignTokens instanceof Map ? parent._engDesignTokens : (parent._engDesignTokens = new Map())
217
+ map.clear()
218
+ for (const [id, v] of snapshot) map.set(id, v)
219
+ } else {
220
+ delete parent._engDesignTokens
221
+ }
222
+ throw new Error(`consume-design: ${removals.length} slot(s) were removed in memory but could NOT be durably deleted from the slot file (${e.message}) — the slots are restored in memory; retry consume-design.`)
223
+ }
224
+ }
225
+ const lines = targets.map((t) => (t.consumed
226
+ ? `- designId ${t.designId} — consumed`
227
+ : `- designId ${t.designId} — no live slot (already consumed or never issued) — idempotent no-op, nothing changed.`))
228
+ lines.push(removals.length > 0
229
+ ? `consumed ${removals.length} design slot(s) — a further eng-coder spawn for these designs is mechanically rejected; any new work (including deviation fixes) requires a fresh advisor(type='design') review and token.`
230
+ : "consumed 0 design slot(s) — nothing changed (idempotent no-op).")
231
+ return lines.join("\n")
232
+ }
@@ -55,7 +55,7 @@ export function digestBudgetOver(agent, size) {
55
55
  return over
56
56
  }
57
57
 
58
- /** Test seam: 落盘目录重定向(单测沙箱——生产从不调用)。 */
58
+ /** Test seam: 落盘目录重定向(单元测试沙箱——生产从不调用)。 */
59
59
  let _digestOffloadDirOverride = null
60
60
  export function _setDigestOffloadDirForTest(dir) { _digestOffloadDirOverride = dir }
61
61