@thincoder/core 0.9.4 → 0.10.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 (221) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +14 -14
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +23 -13
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +19 -15
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +90 -30
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
package/agent/setup.mjs CHANGED
@@ -10,14 +10,18 @@ import { search as memorySearch, docSearch } from "../memory.mjs"
10
10
  import { pushReal } from "../context.mjs"
11
11
  import { toOpenAISchema } from "../tools/index.mjs"
12
12
  import { loadSkills, formatSkillListing } from "../skills.mjs"
13
+ import { scopedRulesBlock } from "../rules.mjs"
13
14
  import { assemblePrompt } from "../prompt-overlays.mjs"
14
15
  import {
15
16
  escapeXml, repairHistory, listWorkDir,
16
17
  collectGitContext, loadProjectInstructions, OUTLINE_INJECT_PREFIX,
17
18
  DEFAULT_MAX_TURNS, ensureAutoReminder,
18
19
  } from "./helpers.mjs"
19
- import { pushEnvStateReminder, pushPeerReminder } from "./setup-reminders.mjs"
20
+ import { pushEnvStateReminder, pushPeerReminder, pushInjections } from "./setup-reminders.mjs"
20
21
  import { assembleFamilyTools } from "./family-tools.mjs"
22
+ import { normalizeOrigin } from "../memory/origin.mjs"
23
+ import { searchOrigins } from "../memory/code-search.mjs"
24
+ import { INDEX_ORIGIN_ROW_WARN } from "../memory/schema.mjs"
21
25
 
22
26
  const DEFAULT_COMPACT_THRESHOLD = 100_000
23
27
  const DOC_SEARCH_LIMIT = 5
@@ -41,6 +45,10 @@ const MEMORY_SEARCH_LIMIT = 3
41
45
  */
42
46
  export async function prepareRun(agent, input, callbacks, {
43
47
  depth = 0, signal, overrideTurns, resume, extraTools = null,
48
+ // P4-I(批次档 §2.5 A1 + 父侧 2026-09-29 裁定①):端壳装配缝——注入串 ∕ 工具装饰;
49
+ // 两项缺省(null ∕ undefined)⇒ 现行行为逐字零变(CLI ∕ desktop 不传即零变)。
50
+ // (A2 提示尾块载位已退役——B7 3a:核自持 `.cursor/rules` 尾块,见下方 depth-0 块。)
51
+ injections = null, toolDecorate = undefined,
44
52
  } = {}) {
45
53
  const maxTurns = overrideTurns ?? agent.config?.agent?.maxTurns ?? DEFAULT_MAX_TURNS
46
54
  const threshold = agent.config?.agent?.compactThreshold ?? DEFAULT_COMPACT_THRESHOLD
@@ -91,7 +99,8 @@ export async function prepareRun(agent, input, callbacks, {
91
99
  if (agent.memory && !agent.history.some((m) => typeof m.content === "string" && m.content.startsWith(OUTLINE_INJECT_PREFIX))) {
92
100
  try {
93
101
  const { buildSummary } = await import("../tools/repomap.mjs")
94
- const summary = await buildSummary(agent.memory.db, agent.cwd)
102
+ // §6.14 B4:origin 限定 + 文件数上界(越界 ⇒ 提示行 + 跳过大纲构建——见 repomap.mjs)
103
+ const summary = await buildSummary(agent.memory.db, agent.cwd, { origin: normalizeOrigin(agent.memory.codeOrigin) })
95
104
  if (summary && !summary.startsWith("(no indexed")) {
96
105
  agent.history.push({ role: "user", content: `${OUTLINE_INJECT_PREFIX}\n${summary}]`, transient: true })
97
106
  }
@@ -103,8 +112,16 @@ export async function prepareRun(agent, input, callbacks, {
103
112
  if (agent.memory && depth === 0) {
104
113
  const docs = await docSearch(agent.memory, input, { limit: DOC_SEARCH_LIMIT })
105
114
  if (docs.length > 0) {
106
- const count = agent.memory.db.prepare(`SELECT COUNT(*) AS n FROM doc_chunks`).get()?.n ?? 0
107
- const more = count > docs.length ? ` (${count} chunks indexed total — call doc_search if you need more)` : ""
115
+ // §6.14 B3 + §6.15:origin 集限定 + 有界计数(子查询 LIMIT = WARN+1 形——越界回「20000+」;跨 origin 全扫不再回升)
116
+ const origins = searchOrigins(agent.memory)
117
+ const bound = INDEX_ORIGIN_ROW_WARN + 1
118
+ const countFilter = origins.length === 1 ? " WHERE origin = ?" : origins.length > 1 ? ` WHERE origin IN (${origins.map(() => "?").join(", ")})` : ""
119
+ const countRow = agent.memory.db.prepare(
120
+ `SELECT COUNT(*) AS n FROM (SELECT 1 FROM doc_chunks${countFilter} LIMIT ${bound})`,
121
+ ).get(...origins)
122
+ const count = Number(countRow?.n ?? 0)
123
+ const countLabel = count >= bound ? `${INDEX_ORIGIN_ROW_WARN}+` : String(count)
124
+ const more = count > docs.length ? ` (${countLabel} chunks indexed total — call doc_search if you need more)` : ""
108
125
  agent.history.push({
109
126
  role: "user",
110
127
  content:
@@ -137,6 +154,9 @@ export async function prepareRun(agent, input, callbacks, {
137
154
  pushEnvStateReminder(agent)
138
155
  await pushPeerReminder(agent)
139
156
  }
157
+ // A1(§2.5——端壳编辑器上下文注入,序位 = env/peer 之后、time 之前):端采集内容经
158
+ // `pushInjections` 单点落机器线(transient + 同文去重);缺省 null ⇒ 零行为。
159
+ pushInjections(agent.history, injections)
140
160
 
141
161
  // Time grounding for EVERY agent depth AND every resume, pushed LAST (after the user
142
162
  // input): transient on the HUMAN line — dropped on persist; on the MACHINE line — kept
@@ -165,6 +185,9 @@ export async function prepareRun(agent, input, callbacks, {
165
185
  engineering: agent.config?.agent?.engineering === true,
166
186
  consultModels: agent.config?.agent?.consultModels ?? [],
167
187
  batchDoc: agent._batchDoc ?? null,
188
+ // 裁定①(P4-I · 父侧 2026-09-29):端壳装饰体透传(`assembleFamilyTools` 既有 `decorate`
189
+ // 参数面——缺省 undefined ⇒ 核默认形态逐字零变,CLI ∕ desktop 不传即零变)。
190
+ decorate: toolDecorate,
168
191
  })
169
192
  // 两段式:绑定值(基础集)→ 家族段 → caller 注入面(§2.3F 不相交义务在注入方)。
170
193
  const tools = [...agent.tools, ...familyTools, ...(Array.isArray(extraTools) ? extraTools : [])]
@@ -232,6 +255,11 @@ export async function prepareRun(agent, input, callbacks, {
232
255
  const skills = await loadSkills(agent.cwd)
233
256
  const listing = formatSkillListing(skills)
234
257
  if (listing) systemPrompt += `\n\n${listing}`
258
+ // B7 3a:`.cursor/rules` 常驻集尾块(原端壳 A2 载位退役——核自持;序位 = skills 清单之后)。
259
+ // 尾块 ∕ JIT 两缝同门 `depth === 0`(子代理不携——B1 §2.5-A2 在册态);同点缓存 `agent._rules`
260
+ // (JIT 判定只读该缓存——`loadScopedRules` 本 run 唯一一次目录读取)。
261
+ const scopedBlock = scopedRulesBlock(agent, agent.cwd)
262
+ if (scopedBlock) systemPrompt += scopedBlock
235
263
  }
236
264
 
237
265
  ensureAutoReminder(agent)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * spawn-child.mjs — 生成侧统一子代理管线(AGENT-LOOP.md §7.2 D3)。
2
+ * spawn-child.mjs — 生成侧统一子代理管线(AGENT-LOOP-SUBAGENT.md §6.7.2)。
3
3
  *
4
4
  * 收编 subagent/escalate/consult 三份机械同构的重复实现:
5
5
  * - allocRelay / emitRelayModel: relay 取号段 ∥ 出生声明(`[model]`)发射段——两段分离
@@ -20,8 +20,8 @@ import { specForModel } from "../config.mjs"
20
20
  import { ContinueError } from "../agent.mjs"
21
21
  import { relayPrefixOf } from "./relay-prefix.mjs"
22
22
  import { appendCappedText } from "../text-budget.mjs"
23
- // 第 27 批 §12.3①:relay 前缀文法单一权威模块(`src/agent/relay-prefix.mjs`)——
24
- // 生成侧枢纽再导出(TUI/ACP 消费方可经此导入;消费方按 §12.3① 直连模块亦可)。
23
+ // 第 27 批:relay 前缀文法单一权威模块(`src/agent/relay-prefix.mjs`)——
24
+ // 生成侧枢纽再导出(TUI/ACP 消费方可经此导入;消费方直连模块亦可)。
25
25
  export { RELAY_PREFIX_RE, parseRelayPath, relayPrefixOf } from "./relay-prefix.mjs"
26
26
 
27
27
  /** 事件 token 哨兵串(D1)——LLM 正常内容混淆概率极低;字段分隔用 RS (\x1e)。 */
@@ -35,7 +35,7 @@ const RS = "\x1e"
35
35
  export { TURN_CAP_MARK, STOPPED_MARK } from "./child-marks.mjs"
36
36
 
37
37
  /**
38
- * §18 D-E3 工程子代理内部 spawn 机械门(AGENT-LOOP.md §18 D-E2 round5 #2 后备 +
38
+ * D-E3 工程子代理内部 spawn 机械门(AGENT-LOOP-SUBAGENT.md §6.7.6 后备 +
39
39
  * ENGINEERING-MODE.md §2.15 D):eng-coder(偏差审计)与 **eng-designer(自己勘察)**
40
40
  * 同为受限通道——depth>0 且父角色 ∈ 集合时,内部 spawn 只允许 role='explore'、
41
41
  * async 强制同步。
@@ -109,7 +109,7 @@ export function makeRelay(parent, label, emit, model) {
109
109
  // Single source for the event grammar branch lists (consult P3, 2026-08-30):
110
110
  // stripEventToken (display) and stripEventTokensForCapture (capture) shared them
111
111
  // literally — extending the event set meant touching both regexes.
112
- // "done" = §15 D-A3 async-child completion event (emitted by the parent's
112
+ // "done" = AGENT-LOOP-SUBAGENT.md §6.7.3 D-A3 async-child completion event (emitted by the parent's
113
113
  // turn-end collection, not by children — listed so the grammar stays honest).
114
114
  const EVENT_PHASE = "turn|approval|done"
115
115
  const EVENT_TYPE = "llm|tool|approval|done"
@@ -139,7 +139,7 @@ export function stripEventTokensForCapture(text) {
139
139
  * - onToolOutput 带**已加前缀**的 name 走父 onToolOutput(name 形如
140
140
  * "coder#1/bash",消费端剥前缀路由进对应区块;chunk 对象/裸串原样透传)。
141
141
  * 父回调缺省时不包装(headless 嵌入)。
142
- * §27 R23 D-R23c1:包装产物在 onToken 上留 `_relayPrefix` 标记——同步收尾段据此
142
+ * docs/cli/design/TUI.md §6.8.2 R23 D-R23c1:包装产物在 onToken 上留 `_relayPrefix` 标记——同步收尾段据此
143
143
  * 判定"ctx.callbacks 已是嵌套 wrapper"(本 spawn 处于更深一层——eng-coder 内
144
144
  * explore)→ emitNestedChildEvent 补发射内层完成事件(T-R23c.2b 断言源)。
145
145
  */
@@ -163,7 +163,7 @@ export function wrapChildCallbacks(relayPrefix, parentCallbacks = {}) {
163
163
  }
164
164
 
165
165
  /**
166
- * §27 R23 D-R23c1(生成侧补发射——评审 #1 🅰):sync spawn 同步收尾时若父回调已是
166
+ * §6.8.2 R23 D-R23c1(生成侧补发射——评审 #1 🅰):sync spawn 同步收尾时若父回调已是
167
167
  * 嵌套 wrapper(onToken 带 `_relayPrefix` 标记——即本 spawn 的父本身是子代理,如
168
168
  * eng-coder 内 explore 审计)→ 发内层 done/stopped 事件(带完整嵌套前缀——wrapper
169
169
  * 链自动补外层前缀)→ 主 TUI routeSubToken 路由到子块定格。非嵌套(depth-0 直连主
@@ -178,7 +178,7 @@ export function emitNestedChildEvent(ctx, relayPrefix, kind) {
178
178
  return true
179
179
  }
180
180
 
181
- /** 捕获额度(TUI-OOM-ROOTCAUSE——AGENT-LOOP.md §23.3.1 表 1 候选 1:滞后水位截断):
181
+ /** 捕获额度(TUI-OOM-ROOTCAUSE——AGENT-LOOP.md §6.15:滞后水位截断):
182
182
  * `_capturedOutput` 是子代理流式文本的第二份全量拷贝(原无上限——勘察 C2);超 hard 即
183
183
  * 裁至头 16K + 标记 + 尾 48K(摊还 O(1))——消费面读时已各自 slice(0, 2000/4000),头尾
184
184
  * 保真覆盖;停止报告内联场景出现截断标记(可断言——D-SM1)。纯函数本体住
@@ -235,7 +235,7 @@ export async function runWithContinue(runner, child, input, callbacks, runOpts,
235
235
  // Review #4 fix: strip sentinel/control chars from the capture — `output` feeds
236
236
  // onDeclined's partial-output return, which lands in the PARENT LLM history where
237
237
  // the display-layer sanitizeDisplay backstop does not apply.
238
- // TUI-OOM-ROOTCAUSE(§23.3.1):捕获滞后水位截断——超 CAPTURE_CAP_OPTS.hard 裁至
238
+ // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §6.15):捕获滞后水位截断——超 CAPTURE_CAP_OPTS.hard 裁至
239
239
  // 头 + 标记 + 尾(_capturedOutput 有界;续跑同闭包累积,语义一致)。
240
240
  const capture = callbacks?.onToken
241
241
  ? (t) => { output = appendCappedText(output, stripEventTokensForCapture(String(t)), CAPTURE_CAP_OPTS); child._capturedOutput = output; callbacks.onToken(t) }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * agent/suspension.mjs — §17 挂起 / 唤醒驱动(核内形态——AGENT-LOOP.md §2.3 #184 定案)。
2
+ * agent/suspension.mjs — 挂起 / 唤醒驱动(核内形态——AGENT-LOOP.md §2.3 #184 定案;机制面 = AGENT-LOOP-ASYNC-POOL.md §6.8)。
3
3
  *
4
4
  * 语义(两端同源,逐字承两端现行挂起驱动——CLI 侧 tui 面 / VSC 侧 extension 面,同语义移植):
5
5
  * 回合尾后台池仍 live → 挂起会话——池项 settle → 入 pending;pending 非空 **或存在未 drain 的
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * 载体注入(端差面 = 池 / pending / 标志挂谁):`ctx.carrier` = 字段集按核内异步面
11
11
  * 现行口径的对象(`_asyncSubagents` · `_asyncAdvisors` · `_consultSessions` ·
12
- * `_pendingAsyncResults` · `_suspended`)——CLI 形 = `agent` 对象;VSC 形 = depth-0
12
+ * `_pendingAsyncResults` · `_suspended`;#9 后台 bash 任务池 `_bgTasks` 同列)
13
+ * ——CLI 形 = `agent` 对象;VSC 形 = depth-0
13
14
  * `history` 数组(附加属性不污染会话文件)。机制对载体零预设(空字段一律 `?.` 读)。
14
15
  *
15
16
  * 核内零文案、零渲染、零端名分支(契约 5 / 10):状态行文本 / 提示行 / 消息族全在宿主侧,
@@ -18,7 +19,13 @@
18
19
  * ctx 接口(注入面):
19
20
  * - carrier 池 / pending / 标志载体(见上)
20
21
  * - runTurn(text, opts) 回合执行器(digest = `("", { autoTurn: true })`;上行唤醒轮 =
21
- * `("", { autoTurn: true, upstreamTurn: true })`——旗标仅供域文本选择)
22
+ * `("", { autoTurn: true, upstreamTurn: true })`;timer 轮 =
23
+ * `("", { autoTurn: true, timerTurn: true })`——旗标仅供域文本选择)
24
+ * - inputQueue **opt-in** 宿主既有输入队列数组(增补 C):驱动就地消费该数组——宿主
25
+ * 其余入队 ∕ 读取面与驱动同一数组(零同步面);缺省核内新建(既有宿主零变)
26
+ * - takeInput(queue) **opt-in** 取项缝(增补 C):批次策略(富条目 / 合并批 / 消费回执行)
27
+ * 归宿主;缺省 `shift`;返回 null/undefined ⇒ 本步零动作、落第 2 步
28
+ * (首动作不可消费的防御面——条目不消费留队)。可异步(宿主取项含动态 import)
22
29
  * - abortSignal 会话中止信号(兜底监听 + abort 清场判据)
23
30
  * - hooks.onCounts(counts) 计数变化通知({ running, queued, pending, done })
24
31
  * - hooks.onDigest(phase, counts) 消化轮边界(start / end)
@@ -26,6 +33,11 @@
26
33
  * - hooks.freezeAll() 退出冻结(兜底残项)
27
34
  * - injectResidual(entry) 残余注入面(idle 退出清场;宿主装配端注入器——CLI =
28
35
  * 逐族注入分发 / VSC = pending 单容器注入器)
36
+ * - timerFace **opt-in** timer 面(§6.30.10):`{ deadline(), deliver() }`——`deadline()`
37
+ * 每轮步骤 4 现算(number|null;缺省 / null ⇒ 零注册,等待仍三态);
38
+ * `deliver()` 到期兑现面(返回布尔 = 是否已交付;交付真 ⇒ timer 轮)。
39
+ * 缺省不传 ⇒ 既有行为逐字等价(现宿主零传)。
40
+ * - timer / clear 等待原语注入缝(缺省 `setTimeout` / `clearTimeout`——用例零真实等待)
29
41
  */
30
42
 
31
43
  import { parkAsyncPending } from "../agent-tools/async-settle.mjs"
@@ -42,11 +54,13 @@ function consultRunningChildren(carrier) {
42
54
 
43
55
  /** 后台池存活判据(D-S2/F5 口径——CLI 为准):running/queued 的池条目,或已 settle 未注入
44
56
  * (pending 非空 = D-S3「未注入」),或运行中 consult children(会诊跨回合)。回合尾与每次
45
- * 轮末都评估退出。 */
57
+ * 轮末都评估退出。**#9 三域并入**:`_bgTasks`(后台 bash 任务)在途亦为 live——settle 经
58
+ * pending 停靠驱动消化轮(`bash-async.mjs` 恒停靠)。 */
46
59
  export function poolLive(carrier) {
47
60
  const sub = carrier?._asyncSubagents
48
61
  const adv = carrier?._asyncAdvisors
49
- return (sub && sub.size > 0) || (adv && adv.size > 0)
62
+ const bg = carrier?._bgTasks
63
+ return (sub && sub.size > 0) || (adv && adv.size > 0) || (bg && bg.size > 0)
50
64
  || (carrier?._pendingAsyncResults?.length ?? 0) > 0
51
65
  || consultRunningChildren(carrier) > 0
52
66
  }
@@ -94,13 +108,17 @@ function consultLiveCount(carrier) {
94
108
 
95
109
  /**
96
110
  * 退出清场(单点——驱动 finally 与单测共用):
97
- * - abort:清池不注入(用户显式停——陈旧结果不回灌)+ 会诊会话清理标记;
111
+ * - abort:**只清已死**(§6.20 口径——增补 D:`discardAbortedPool` ∕ `discardAbortedAdvisors`
112
+ * 单点,与核 run-stages 回合尾中止同式;存活 ∕ 已 settle 条目留池——settled 报告沿后续
113
+ * 回合边界注入到达,不静默丢)+ pending 单容器清 + 会诊会话清理标记;
98
114
  * - idle:残余直注入(极端竞态残项——结果零丢失;注入器由宿主注入,缺省 no-op 保残项)。
99
115
  */
100
116
  export async function finishSuspension(carrier, { aborted = false, injectResidual = null } = {}) {
101
117
  if (aborted) {
102
- carrier._asyncSubagents?.clear()
103
- carrier._asyncAdvisors?.clear()
118
+ const { discardAbortedPool, discardAbortedAdvisors, discardAbortedBgTasks } = await import("../agent-tools/async-discard.mjs")
119
+ discardAbortedPool(carrier)
120
+ discardAbortedAdvisors(carrier)
121
+ discardAbortedBgTasks(carrier) // #9 收尾档②:后台 bash 任务逐条杀树 + 出池 + 墓碑(杀 ⟺ 控制器已中止)
104
122
  carrier._pendingAsyncResults = []
105
123
  if (carrier._consultSessions instanceof Map) {
106
124
  const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
@@ -114,13 +132,19 @@ export async function finishSuspension(carrier, { aborted = false, injectResidua
114
132
  }
115
133
  }
116
134
 
117
- /** 等待下一次 settle(池 waiter——核内异步面 settle 尾部唤醒)或宿主唤醒(wake——用户输入)。
118
- * 双路单次兑现(settle / wake / aborted 三态,先到先得);cleanup 摘除全部注册。 */
119
- function waitForSettleOrWake(carrier, abortSignal, latch) {
135
+ /** 等待下一次 settle(池 waiter——核内异步面 settle 尾部唤醒)/ 宿主唤醒(wake——用户输入)/
136
+ * **timer 到期**(opt-in——§6.30.10 等待第四态,与 settle / wake 同槽先到先得)。
137
+ * `deadline != null` ⇒ 一次性注册(`unref()` 尽力——不阻断宿主退出);缺省 / null ⇒ 三态逐字等价。
138
+ * cleanup 摘除全部注册(含未触发句柄——尽力清);到期兑现交循环支(本原语零交付语义)。 */
139
+ function waitForSettleOrWake(carrier, abortSignal, latch, { deadline = null, timer = setTimeout, clear = clearTimeout } = {}) {
120
140
  return new Promise((resolve) => {
121
141
  let finished = false
142
+ let handle = null
122
143
  const cleanup = () => {
123
144
  latch.wake = null
145
+ const h = handle
146
+ handle = null
147
+ if (h !== null) { try { clear(h) } catch { /* 已触发 / 不可清——尽力面 */ } }
124
148
  const i = (carrier._asyncWaiters ?? []).indexOf(onSettle)
125
149
  if (i >= 0) carrier._asyncWaiters.splice(i, 1)
126
150
  abortSignal?.removeEventListener("abort", onAbort)
@@ -133,8 +157,14 @@ function waitForSettleOrWake(carrier, abortSignal, latch) {
133
157
  }
134
158
  const onSettle = () => finish("settle")
135
159
  const onAbort = () => finish("aborted")
160
+ const onTimer = () => finish("timer")
136
161
  ;(carrier._asyncWaiters ??= []).push(onSettle)
137
162
  latch.wake = () => finish("wake")
163
+ // opt-in deadline(§6.30.10 等待第四态):无在途 / 未传 ⇒ 零注册(等待仍三态)。宿主不变式 = 到期即出列 / 关即 null(否则「已到期 ∧ 交付假」会 0ms 反复重注册)。
164
+ if (deadline != null) {
165
+ handle = timer(onTimer, Math.max(0, deadline - Date.now()))
166
+ try { handle?.unref?.() } catch { /* unref 失败不阻断 */ }
167
+ }
138
168
  if (abortSignal?.aborted) { onAbort(); return }
139
169
  abortSignal?.addEventListener("abort", onAbort, { once: true })
140
170
  })
@@ -143,17 +173,23 @@ function waitForSettleOrWake(carrier, abortSignal, latch) {
143
173
  /**
144
174
  * 启动挂起会话(同步返回句柄;宿主 `await handle.done` ⇒ `{ reason, residualInput }`)。
145
175
  *
146
- * 状态机行表(承 AGENT-LOOP.md §9/§7 行表):
176
+ * 状态机行表(承 AGENT-LOOP-ASYNC-POOL.md §6.8 行表):
147
177
  * 1. 用户输入优先(D-S5):pendingInput 非空 → 以该消息开普通回合(`_suspended=false`);
148
178
  * 2. pending 非空 或 存在未 drain 的 ask(`upstreamWaiting`)→ 合并消化轮 / 唤醒轮(auto-turn;
149
179
  * 注入由宿主 runTurn 首行完成——单注入点);
150
180
  * 3. 池空(无 running / queued / 未注入)→ 自然退出;
151
- * 4. 等下一 settle / 宿主唤醒(handle.wake)。
181
+ * 4. 等下一 settle / 宿主唤醒(handle.wake)/ timer 到期(opt-in——兑现真 ⇒ timer 轮)。
152
182
  * 每轮消化 / 用户回合后:hooks.reclaim(consumed) 逐条回收(不等池空)。
153
183
  */
154
184
  export function startSuspension(ctx) {
155
- const { carrier, runTurn, abortSignal = null, hooks = {}, injectResidual = null } = ctx
156
- const pendingInput = []
185
+ const {
186
+ carrier, runTurn, abortSignal = null, hooks = {}, injectResidual = null,
187
+ timerFace = null, timer = setTimeout, clear = clearTimeout,
188
+ inputQueue = null, takeInput = null,
189
+ } = ctx
190
+ // 输入队列(增补 C):宿主既有数组(VSC ∕ CLI 形——宿主其余读者与驱动同一数组)优先;
191
+ // 缺省核内新建(desktop 形——经 `pushInput` 入槽)。
192
+ const pendingInput = Array.isArray(inputQueue) ? inputQueue : []
157
193
  const latch = { wake: null }
158
194
  let finished = false
159
195
 
@@ -173,19 +209,23 @@ export function startSuspension(ctx) {
173
209
  while (true) {
174
210
  if (abortSignal?.aborted) { aborted = true; break }
175
211
  sweepSettledToPending(carrier)
176
- // 1. 用户输入优先:单槽(至多一条待交接——宿主在 busy 期拒收提交)。
212
+ // 1. 用户输入优先(D-S5):队列非空 → 取项缝(增补 C——缺省 `shift`;富条目 ∕ 合并批
213
+ // 由宿主 `takeInput` 实现)开普通回合(`_suspended=false`);首动作不可消费
214
+ // (`takeInput ⇒ null`)⇒ 本步零动作、条目不消费,落第 2 步。
177
215
  if (pendingInput.length > 0) {
178
- const head = String(pendingInput.shift())
179
- const afterRun = consumedByRun()
180
- carrier._suspended = false // 用户回合 = 普通回合语义(settle 即冻结 + 回合尾直注入)
181
- try {
182
- await runTurn(head)
183
- } finally {
184
- carrier._suspended = true
216
+ const item = takeInput ? await takeInput(pendingInput) : pendingInput.shift()
217
+ if (item != null) {
218
+ const afterRun = consumedByRun()
219
+ carrier._suspended = false // 用户回合 = 普通回合语义(settle 即冻结 + 回合尾直注入)
220
+ try {
221
+ await runTurn(item)
222
+ } finally {
223
+ carrier._suspended = true
224
+ }
225
+ hooks.reclaim?.(afterRun())
226
+ hooks.onCounts?.(backgroundCounts(carrier))
227
+ continue
185
228
  }
186
- hooks.reclaim?.(afterRun())
187
- hooks.onCounts?.(backgroundCounts(carrier))
188
- continue
189
229
  }
190
230
  // 2. pending 非空 或 未 drain 的 ask → 合并消化轮 / 上行唤醒轮(单注入点由 runTurn
191
231
  // 首行完成:pending 经 run 起始注入器,ask 经回合头 `drainChildUpstream`)。
@@ -210,9 +250,28 @@ export function startSuspension(ctx) {
210
250
  }
211
251
  // 3. 池空 → 自然退出。
212
252
  if (!poolLive(carrier)) break
213
- // 4. 等下一 settle / 宿主唤醒。
214
- const why = await waitForSettleOrWake(carrier, abortSignal, latch)
253
+ // 4. 等下一 settle / 宿主唤醒 / timer 到期(opt-in——§6.30.10 等待第四态;deadline 每轮现算)。
254
+ const why = await waitForSettleOrWake(carrier, abortSignal, latch, {
255
+ deadline: timerFace?.deadline?.() ?? null, timer, clear,
256
+ })
215
257
  if (why === "aborted") { aborted = true; break }
258
+ // 4b. 窗内到期兑现(§6.30.10 循环兑现支):交付真 ⇒ timer 轮(轮后序同消化轮形 = reclaim +
259
+ // onCounts 两钩,不发 digest 边界);交付面契约 = 同步 · 严格布尔(`=== true`——非布尔返值
260
+ // 即静默零轮);池空窗退交宿主空闲闩(到期件已出列——零重复投递);交付假 ⇒ 循环重入。
261
+ if (why === "timer" && timerFace?.deliver?.() === true) {
262
+ const afterRun = consumedByRun()
263
+ try {
264
+ await runTurn("", { autoTurn: true, timerTurn: true })
265
+ } catch (e) {
266
+ // timer 轮自身的回合级中止不是会话停止——容纳并重入循环(timer 轮不发 digest 边界 ⇒
267
+ // 中止路径零边界、不补发轮后序钩子;池空 / pending 空自然退出——与消化支同判,§6.30.10)。
268
+ if (e?.name === "AbortError" && !abortSignal?.aborted) continue
269
+ throw e
270
+ }
271
+ hooks.reclaim?.(afterRun())
272
+ hooks.onCounts?.(backgroundCounts(carrier))
273
+ continue
274
+ }
216
275
  }
217
276
  } finally {
218
277
  if (!finished) {
@@ -231,8 +290,9 @@ export function startSuspension(ctx) {
231
290
  })()
232
291
 
233
292
  return {
234
- /** 挂起空闲期入槽(单槽语义:宿主 busy 期拒收;至多一条待交接)。 */
235
- pushInput(msg) { pendingInput.push(String(msg)) },
293
+ /** 挂起空闲期入槽(宿主 busy 期拒收——条数策略归宿主)。**保留原值**(增补 C:富条目
294
+ * ——VSC ∕ CLI 载体对象;消费面 = 宿主 `takeInput`;`String()` 化归宿主需要时自做)。 */
295
+ pushInput(msg) { pendingInput.push(msg) },
236
296
  /** 宿主唤醒(用户输入落槽后 / 宿主 settle 回调路径——settle 由池 waiter 另一路唤醒)。 */
237
297
  wake() { latch.wake?.() },
238
298
  done,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * agent/timers.mjs — timer 到期件(载体①:在途 timer 的单一权威读面)+ 空闲唤醒机制(闩 ∕ 派发 ∕ 火策略)。
3
+ *
4
+ * 机制单源 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.30.2(载体定形 D-TW1):
5
+ * 读面三件纯函数住本档——最近到期时点 / 到期出列(幂等)/ 历史注入单点。消费面三处共用
6
+ * 同一读点(零第二份过滤语义):① 回合边界轮询(`agent/post-turn.mjs`——行为零变);
7
+ * ② 空闲闩 deadline;③ 挂起窗 deadline;显示面(状态行标记 / `/timers`)同读源。
8
+ *
9
+ * 载体仍在 agent 字段 `_pendingTimers`(条目形 `{ id, expiresAt, message }`)——**唯一写点** =
10
+ * timer 工具 `thincoder-core/agent-tools/timer.mjs`;跨 run 存活 = 规范语义(D-TW3,无 per-run 复位)。
11
+ *
12
+ * 空闲唤醒族(2026-09-28 流程批 R4 **上提核件** · 纯搬 + 转口 · 零语义改;上提源 = VSC
13
+ * `thincoder-vscode/src/extension/timer-watch.mjs:46` ∕ `:73` ∕ `:94`——as-of 2026-09-28 R4 读数;两端现址(B3 收编后,实读 2026-09-29)=
14
+ * VSC `:63`(闩 ∕ 装配)· `:29`(派发)· `:48`(火)∥ CLI `:41`(闩转口)· `:34`(派发转口)· `:57`(火)):`createTimerWatch` = 一次性最近截止闩
15
+ * (起 ∕ 停 ∕ 重臂 ∕ `unref`)· `deliverExpiredTimers` = 到期批派发(出列幂等 ⇒ 注入单点 ⇒ 落流缝)·
16
+ * `fireTimerWake` = 火策略(非空闲零动作 ∕ 零交付零开轮)。端面表壳留端(桌面键面 ∕ `ev:timer` 出词;
17
+ * VSC ∕ CLI 自持副本已随 B3 迁移轮退场(2026-09-29——双写窗口收口),本档 = 单源)。
18
+ */
19
+
20
+ /** 在途条数帽(§6.30.3 门三件②成本闸):超限 ⇒ timer 工具**显式拒**(不静默丢 / 不静默清;
21
+ * 形态先例 = 队满「拒 + 提示 + 保留」)。与 CLI 排队容量 `QUEUED_MAX_ITEMS`(8)同值同形。 */
22
+ export const TIMER_MAX_PENDING = 8
23
+
24
+ /** 最近到期时点(毫秒)或 `null`(空在途)——端闩 / 挂起窗 deadline / 显示面共用同源(每帧活读)。 */
25
+ export function pendingTimerDeadline(agent) {
26
+ const timers = agent?._pendingTimers ?? []
27
+ let min = null
28
+ for (const t of timers) {
29
+ const at = t?.expiresAt
30
+ if (typeof at !== "number" || !Number.isFinite(at)) continue
31
+ if (min === null || at < min) min = at
32
+ }
33
+ return min
34
+ }
35
+
36
+ /** 到期出列(**幂等**):取走 `expiresAt <= now` 的条目并返回(在途表内移除)——二次调用零返
37
+ * (§6.30.3 成本闸②「到期即消费」——绝不重复投递)。未到期项原序保留。 */
38
+ export function takeExpiredTimers(agent, now = Date.now()) {
39
+ const timers = agent?._pendingTimers ?? []
40
+ const expired = timers.filter((t) => t.expiresAt <= now)
41
+ if (expired.length === 0) return []
42
+ agent._pendingTimers = timers.filter((t) => t.expiresAt > now)
43
+ return expired
44
+ }
45
+
46
+ /** 历史注入单点(逐字形态 = 既有注入单源):逐条 `[System reminder: ⏰ timer — <message>]` 落历史,
47
+ * 返回注入的原文(显示面复用同一串——零第二份字面)。 */
48
+ export function injectTimerReminders(agent, entries) {
49
+ const lines = []
50
+ for (const t of entries ?? []) {
51
+ const line = `[System reminder: ⏰ timer — ${t.message}]`
52
+ agent.history.push({ role: "user", content: line })
53
+ lines.push(line)
54
+ }
55
+ return lines
56
+ }
57
+
58
+ /** 开关判据(§6.30.3 门三件③ D-TW5 · 上提源 = VSC `timer-watch.mjs:33`——as-of 2026-09-28 R4 读数,B3 收编后该端副本退场)
59
+ * (现转口 = CLI `timer-watch.mjs:18`):默认开——只有显式 `false` 才关
60
+ * (键 `agent.timerWake`;与 CLI 同口径 `!== false`)。关 ⇒ 闩零注册 ⇒ 回仅回合边界投递(既有语义)。 */
61
+ export function timerWakeEnabled(agent) {
62
+ return agent?.config?.agent?.timerWake !== false
63
+ }
64
+
65
+ /** 到期批派发(**两路共用**:空闲闩 fire ∥ 挂起窗 `timerFace.deliver` · 上提源 = VSC `timer-watch.mjs:73`
66
+ * 核心体——as-of 2026-09-28 R4 读数;现址 = VSC `:29` · CLI `:34`):出列(幂等——绝不重复投递)→ 注入单点 → 逐条落流缝(`onLine`——端侧 `ev:timer` ∕ webview ∕
67
+ * TUI 行)。返回交付原文(核注入单源逐字;零到期 ∕ 无载体 ⇒ 空数组——在途不动 = 零静默丢)。
68
+ * `now` = 时点值(默认现刻;与 `takeExpiredTimers` 同形——调用方传钟、本档取时点)。 */
69
+ export function deliverExpiredTimers(agent, { now = Date.now(), onLine = null } = {}) {
70
+ const lines = injectTimerReminders(agent, takeExpiredTimers(agent, now))
71
+ for (const line of lines) onLine?.(line)
72
+ return lines
73
+ }
74
+
75
+ /** 一次性最近截止闩(单槽 · 上提源 = VSC `timer-watch.mjs:46` 逐字——as-of 2026-09-28 R4 读数;现址 = VSC `:63`(闩 ∕ 装配)· CLI `:41`(闩转口)):`sync()` = 按当前在途最近到期时点
76
+ * (重)武装——无在途 ∕ 开关关 ∕ 无载体 ⇒ 撤旧零注册;`disarm()` = 撤闩清点。到点自撤 · `unref()`(尽力面)。
77
+ * `getAgent` / `onFire` 缺省宽容沿用源(未接线 = 惰性零注册);`timer` ∕ `clear` ∕ `now` = 注入缝(默认平台全局)。
78
+ * @returns {{ sync: Function, disarm: Function }}——`sync()` ⇒ 武装延迟(毫秒)或 `null`(未武装) */
79
+ export function createTimerWatch({ getAgent, onFire, timer = setTimeout, clear = clearTimeout, now = Date.now } = {}) {
80
+ let handle = null
81
+ const disarm = () => {
82
+ const h = handle
83
+ handle = null
84
+ if (h !== null) { try { clear(h) } catch { /* 已触发 / 不可清——尽力面 */ } }
85
+ }
86
+ const sync = () => {
87
+ const agent = getAgent?.()
88
+ const deadline = timerWakeEnabled(agent) ? pendingTimerDeadline(agent) : null
89
+ if (deadline == null) { disarm(); return null } // 无在途 / 开关关 / 无载体——撤旧,零注册
90
+ disarm() // 单槽:重复武装 = 撤旧立新(延迟按最近到期重算)
91
+ const delay = Math.max(0, deadline - now())
92
+ handle = timer(() => { handle = null; onFire?.() }, delay)
93
+ try { handle?.unref?.() } catch { /* unref 失败不阻断(一次性闩自然退出) */ }
94
+ return delay
95
+ }
96
+ return { sync, disarm }
97
+ }
98
+
99
+ /** 空闲唤醒火策略(上提源 = VSC `timer-watch.mjs:94`——as-of 2026-09-28 R4 读数;现址 = VSC `:48` · CLI `:57`;桌面同形):
100
+ * **非空闲零动作**——回合在飞 ∕ 挂起窗
101
+ * 由各自既有路径接管,在途表零触碰(链尾重同步再武装);空闲 ⇒ 派发;交付真 ⇒ 开 timer 轮(开轮面 =
102
+ * `openTurn` 缝,承担 `{ autoTurn: true, timerTurn: true }` 语义)。`deliver` 契约 = 同步 · 严格布尔
103
+ * (判定 `=== true`——同 §6.30.10 `timerFace.deliver`);缝缺 ⇒ 抛(fail-loud——未接线不得静默零开轮)。
104
+ * `busy` = 非空闲判据(宿主现算:在飞回合 ∕ 窗内 ⇒ true);返回是否开轮。 */
105
+ export async function fireTimerWake({ busy = false, deliver, openTurn } = {}) {
106
+ if (typeof deliver !== "function" || typeof openTurn !== "function") {
107
+ throw new Error("[timers] fireTimerWake requires deliver/openTurn seams (unwired seams must not pass silently)")
108
+ }
109
+ if (busy) return false
110
+ if (deliver() !== true) return false
111
+ await openTurn()
112
+ return true
113
+ }