@thincoder/core 0.10.4 → 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 (144) hide show
  1. package/CHANGELOG.md +31 -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/config-io.mjs +18 -13
  54. package/config-migrate.mjs +13 -29
  55. package/config-presets.mjs +33 -29
  56. package/config.mjs +68 -25
  57. package/context-degrade.mjs +1 -1
  58. package/context-echo.mjs +1 -1
  59. package/context-push.mjs +2 -2
  60. package/context.mjs +1 -1
  61. package/conventions.mjs +2 -2
  62. package/declaration.mjs +18 -2
  63. package/file-links.mjs +1 -1
  64. package/file-refs.mjs +1 -1
  65. package/history-window.mjs +1 -1
  66. package/i18n.mjs +2 -0
  67. package/ledger-cmd.mjs +62 -16
  68. package/ledger-db.mjs +6 -0
  69. package/ledger-surface.mjs +49 -5
  70. package/ledger-tools.mjs +16 -8
  71. package/ledger.mjs +9 -16
  72. package/log.mjs +1 -1
  73. package/manifest-discovery.mjs +1 -1
  74. package/manifest-schema.mjs +1 -1
  75. package/mcp/transport-stdio.mjs +1 -1
  76. package/mcp.mjs +1 -1
  77. package/memory/code-index.mjs +1 -1
  78. package/memory/code-search.mjs +1 -1
  79. package/memory/code-sync.mjs +1 -1
  80. package/memory/docs.mjs +6 -2
  81. package/memory/file-list.mjs +1 -1
  82. package/memory/file-walk.mjs +17 -5
  83. package/memory/maintain.mjs +410 -0
  84. package/memory/memory-tool.mjs +21 -6
  85. package/memory/schema.mjs +19 -7
  86. package/memory/sweep.mjs +8 -5
  87. package/memory/sync-tail.mjs +16 -10
  88. package/model-ref.mjs +16 -13
  89. package/model-specs.mjs +21 -9
  90. package/package.json +1 -1
  91. package/peer-instances.mjs +1 -1
  92. package/permission.mjs +33 -0
  93. package/process-probe.mjs +2 -2
  94. package/prompts/advisor-design.md +4 -2
  95. package/prompts/advisor-round1.md +1 -1
  96. package/prompts/advisor-round2.md +1 -1
  97. package/prompts/advisor-round3.md +1 -1
  98. package/prompts/common.md +36 -2
  99. package/prompts/consult-base.md +1 -1
  100. package/prompts/discipline-engineering.md +32 -15
  101. package/prompts/discipline-normal.md +46 -2
  102. package/prompts/persona-eng-designer.md +6 -6
  103. package/prompts/persona-engineering.md +14 -6
  104. package/prompts/persona-normal.md +2 -0
  105. package/prompts/subagent-base.md +3 -0
  106. package/provider/core.mjs +6 -0
  107. package/provider/errors.mjs +5 -3
  108. package/provider/google.mjs +9 -8
  109. package/provider/list-models.mjs +3 -2
  110. package/provider/sse.mjs +3 -0
  111. package/provider-flows.mjs +14 -15
  112. package/proxy-chunked.mjs +102 -0
  113. package/proxy-target.mjs +54 -0
  114. package/proxy-transport.mjs +298 -0
  115. package/proxy.mjs +17 -248
  116. package/session-gc.mjs +2 -2
  117. package/session-lifecycle.mjs +18 -14
  118. package/session-slot-claims.mjs +2 -2
  119. package/session-slots-manifest.mjs +1 -1
  120. package/session-slots.mjs +3 -3
  121. package/team-ledger.mjs +282 -0
  122. package/team.mjs +223 -0
  123. package/tool-docs/bash.md +1 -0
  124. package/tool-docs/batch.md +1 -1
  125. package/tool-docs/browser.md +48 -0
  126. package/tool-docs/ledger.md +3 -3
  127. package/tool-docs/manifest.md +16 -0
  128. package/tool-docs/memory.md +1 -0
  129. package/tool-docs/notify_parent.md +5 -0
  130. package/tool-docs/process.md +4 -4
  131. package/tool-docs/question.md +1 -0
  132. package/tool-docs/subagent.md +4 -3
  133. package/tool-docs/team_ledger.md +9 -0
  134. package/tool-docs/timer.md +6 -0
  135. package/tool-docs/wait_for.md +1 -0
  136. package/tools/browser.mjs +161 -0
  137. package/tools/git.mjs +1 -1
  138. package/tools/index.mjs +3 -0
  139. package/tools/ops.mjs +26 -10
  140. package/tools/repomap.mjs +1 -1
  141. package/tools/write-path.mjs +2 -2
  142. package/traces/trace-cleanup.mjs +1 -1
  143. package/traces/trace-store.mjs +1 -1
  144. package/vision-reader.mjs +10 -16
@@ -1,7 +1,10 @@
1
1
  /**
2
- * subagent.mjs — subagent tool(ONE tool, EIGHT actions + spawn 路径驱动器)。
2
+ * subagent.mjs — subagent tool(ONE tool, NINE actions + spawn 路径驱动器)。
3
3
  * 2026-09-07 token 链终消费制:+action: consume-design(ENGINEERING-MODE.md §2.6 F1——
4
4
  * 父侧链终核销消费 designId 槽——执行器 executeConsumeDesignAction 在 subagent-spawn.mjs)。
5
+ * 2026-10-07 批 ledger-tool(#927 并入):+action: design-slots(只读清点面——
6
+ * DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1)+ consume-design 选择器扩展(designIds / expired /
7
+ * olderThanDays)——实现在 `design-slots.mjs`(subagent-spawn.mjs re-export 保 import 面)。
5
8
  *
6
9
  * 2026-09-03 拆分轮: subagent.mjs 超 500 硬顶——async 常量、共享 post-spawn 管线
7
10
  *(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
@@ -31,6 +34,7 @@ import {
31
34
  import { buildChildSignal } from "./async-settle.mjs"
32
35
  import { executeStatusAction, executeEscalateAction, executePanelAction, executeObserveAction, executeSendAction } from "./subagent-actions.mjs"
33
36
  import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
37
+ import { executeDesignSlotsAction } from "./design-slots.mjs"
34
38
  import { executeAsyncSpawn } from "./subagent-run.mjs"
35
39
  import { ROUND_VALUES } from "./spawn-gates.mjs"
36
40
  // #15 描述外置:描述文本单点 = tool-docs/subagent.md(DESC 单解析面,缺档抛错语义不变)
@@ -102,14 +106,16 @@ export function buildSyncStoppedReport(role, capturedOutput, designId) {
102
106
  }
103
107
 
104
108
  /**
105
- * subagent tool — ONE tool, EIGHT actions (AGENT-LOOP-SUBAGENT.md §6.7 +
109
+ * subagent tool — ONE tool, NINE actions (AGENT-LOOP-SUBAGENT.md §6.7 +
106
110
  * SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
107
111
  * (inspect a running/queued/done async child's recent activity + current tool — docs/cli/design/TUI.md §6.8) /
108
112
  * send (inject a direction into a RUNNING async child — consumed at its next turn
109
113
  * boundary as an ordinary instruction — §6.8) / escalate (飞刀 — hand implementation to
110
114
  * a stronger model) / cancel (stop ONE background subagent — AGENT-LOOP-SUBAGENT.md §6.7.2) / panel (view + fix
111
115
  * the live subagent panel — AGENT-LOOP-SUBAGENT.md §6.7.2) / consume-design (parent-side chain-terminal token
112
- * consumption — ENGINEERING-MODE.md §2.6, 2026-09-07). The check
116
+ * consumption — ENGINEERING-MODE.md §2.6, 2026-09-07; selectors designId / designIds / expired /
117
+ * olderThanDays — DESIGN-TOKEN-SETTLEMENT.md §10 F-SL2) / design-slots (read-only slot inventory —
118
+ * DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1, 2026-10-07). The check
113
119
  * action was deleted (AGENT-LOOP-SUBAGENT.md §6.7.5): async results reach the model only via the auto channel.
114
120
  * - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
115
121
  * (suitable for codebase exploration); "coder" — full tool set, self-contained
@@ -130,7 +136,7 @@ export const subagentTool = {
130
136
  parameters: {
131
137
  type: "object",
132
138
  properties: {
133
- action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design; semantics per action in the tool description." },
139
+ action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send", "design-slots"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design / design-slots; semantics per action in the tool description." },
134
140
  view: { type: "boolean", description: "action:'panel' only: true (default) = the live panel blocks as the user sees them; false without freeze = error. Mutually exclusive with freeze (freeze wins)." },
135
141
  freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck block — reclaims it into the conversation; refused for running/done/unknown blocks or a still-pending report; works on the CLI TUI panel and on the desktop renderer panel (live readback of the blocks as the user sees them)." },
136
142
  task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria)." },
@@ -138,8 +144,11 @@ export const subagentTool = {
138
144
  role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the role matrix in the tool description. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
139
145
  model: { type: "string", description: "action:'spawn': provider/model override ('provider:model', a provider, or a model name on the parent's provider) — defaults config.agent.subagentModels[role] → config.agent.subagentModel → the parent's provider; \"default\" inherits. action:'escalate': a consult candidate (default = the first)." },
140
146
  designToken: { type: "string", description: "Required when role='eng-coder': the token from advisor(type='design') after the review passed — without it, eng-coder cannot modify files." },
141
- designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the slot to close out (required to pick between several; the gate refuses to guess)." },
142
- batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the task book this spawn implements/writes. Refused when absent or when the path does not resolve to a readable file; the content is never validated." },
147
+ designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the single-slot selector (required to pick between several; the gate refuses to guess); see designIds / expired / olderThanDays for batch consumption." },
148
+ designIds: { type: "array", items: { type: "string" }, description: "action:'consume-design' (batch): an explicit non-empty list of design ids to close out — pass exactly ONE selector per call (designId / designIds / expired / olderThanDays)." },
149
+ expired: { type: "boolean", description: "action:'consume-design' (batch): true = close out every expired slot (exactly ONE selector per call)." },
150
+ olderThanDays: { type: "number", description: "action:'consume-design' (batch): close out every slot older than N days (age > N; exactly ONE selector per call)." },
151
+ batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the task book this spawn implements/writes. Refused when absent or when the path does not resolve to a readable file; the content is never validated. role='coder' (normal mode): optional — binds this spawn's §5 record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file." },
143
152
  round: { type: "string", enum: ROUND_VALUES, description: "REQUIRED for role='eng-coder'/'eng-designer': initial (first round for this design) or fix (a correction round reusing the same designId+designToken; docs FIRST). No default — missing = refused." },
144
153
  async: { type: "boolean", description: "action:'spawn'/'escalate': default at depth 0 = true (async; the report arrives automatically); depth>0 always sync. async:false forces the blocking flight." },
145
154
  files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares — file-level paths only (cwd-relative or absolute; directories rejected — they bypass the conflict detector). Overlapping-file tasks serialize automatically. Omit = no conflict detection." },
@@ -165,7 +174,7 @@ export const subagentTool = {
165
174
  // coder+WRITE child (violates explore-only intent) and status/panel/observe/send
166
175
  // have no async pool / panel mirror to query in a child context.
167
176
  if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
168
- throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/observe/send are not`)
177
+ throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/design-slots/observe/send are not`)
169
178
  }
170
179
  // AGENT-LOOP-ASYNC-POOL.md §6.8 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
171
180
  // async OR blocking — the digest must stay organize-only. The escalate
@@ -183,6 +192,10 @@ export const subagentTool = {
183
192
  // 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
184
193
  // 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
185
194
  if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
195
+ // 2026-10-07 批 ledger-tool(#927 并入——DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1):design-slots =
196
+ // 只读清点(工程模式父侧限定——门在执行器内);dispatch 分类 = readonly(planMode 放行 /
197
+ // 免审批 / digest 放行——同 status / observe)。
198
+ if (action === "design-slots") return executeDesignSlotsAction(args, ctx)
186
199
  // AGENT-LOOP-SUBAGENT.md §6.7.2 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
187
200
  // (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
188
201
  // CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction 经 ctx.state
@@ -195,7 +208,7 @@ export const subagentTool = {
195
208
  // send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
196
209
  if (action === "observe") return executeObserveAction(args, ctx)
197
210
  if (action === "send") return executeSendAction(args, ctx)
198
- throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send.`)
211
+ throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send, design-slots.`)
199
212
  }
200
213
 
201
214
  const parent = ctx.agent
@@ -1,10 +1,90 @@
1
- import { TIMER_MAX_PENDING } from "../agent/timers.mjs"
1
+ import { TIMER_MAX_PENDING, TIMER_MIN_EVERY_MS, nextFireAt } from "../agent/timers.mjs"
2
2
  import { DESC } from "../tools/shared.mjs"
3
3
 
4
4
  /**
5
5
  * timer tool: set a time budget for thinking before the agent insists on action.
6
- * 描述面 = `tool-docs/timer.md`(DESC() 加载;机制 = AGENT-LOOP-ASYNC-POOL.md §6.30.3 D-TW8)。
6
+ * 描述面 = `tool-docs/timer.md`(DESC() 加载;机制 = AGENT-LOOP-ASYNC-POOL.md §6.30.3 D-TW8 ∥ §6.30.18
7
+ * 四式 + `cancel`/`list`)。取时单点 = 时钟缝 `ctx.now ?? Date.now()`(§6.30.18——生产 `toolCtx`
8
+ * (`agent/dispatch-run.mjs`)不携 `now` ⇒ 缺省回落 = 现行为零变;单元测试档以假 ctx 直驱)。
7
9
  */
10
+
11
+ /** `at` = 本地刻 `YYYY-MM-DDTHH:MM[:SS]`(严格正则+日历回读校验——如 `2026-02-30` 拒)。 */
12
+ const AT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/
13
+ /** `daily` = 本地 `HH:MM`(与核 `DAILY_RE` 同形——工具面 = 唯一生产者,核守卫为防御面)。 */
14
+ const DAILY_RE = /^([01]\d|2[0-3]):([0-5]\d)$/
15
+
16
+ /** 默认 `message` 槽尾(逐式槽 = §6.30.18 默认表;一次性槽逐字零变)。 */
17
+ const DEFAULT_TAIL = ". Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing."
18
+
19
+ /** 本地刻 `YYYY-MM-DD HH:MM`(宿主本地时区——回执 ∥ 默认文案槽共用)。 */
20
+ function localText(ms) {
21
+ const d = new Date(ms)
22
+ const p = (n) => String(n).padStart(2, "0")
23
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
24
+ }
25
+
26
+ /** `at` 解算(宿主本地时区):形不合 ∥ 日历回读不符(`2026-02-30` ∥ 时位越界等)⇒ `null`。 */
27
+ function atEpochMs(raw) {
28
+ const m = AT_RE.exec(raw)
29
+ if (m === null) return null
30
+ const [y, mo, d, hh, mi, ss] = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6] ?? 0)]
31
+ const date = new Date(y, mo - 1, d, hh, mi, ss, 0)
32
+ const back = date.getFullYear() === y && date.getMonth() === mo - 1 && date.getDate() === d
33
+ && date.getHours() === hh && date.getMinutes() === mi && date.getSeconds() === ss
34
+ return back ? date.getTime() : null
35
+ }
36
+
37
+ /** 时长形(§6.30.18):`<mm:ss>`(< 1h)∥ `<h:mm:ss>`(≥ 1h——h 不补零;与 `/timers` 行同判)。 */
38
+ function durText(ms) {
39
+ const sec = Math.max(0, Math.ceil(ms / 1000))
40
+ const h = Math.floor(sec / 3600)
41
+ const mm = String(Math.floor((sec % 3600) / 60)).padStart(2, "0")
42
+ const ss = String(sec % 60).padStart(2, "0")
43
+ return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`
44
+ }
45
+
46
+ /** kind(`list` 行——§6.30.18):`every <N>s` ∕ `daily <HH:MM>` ∕ `at <刻>`(`at: true` 来源标记位——
47
+ * 父侧裁定 2026-10-12);余(一次性)⇒ `one-shot`。 */
48
+ function kindText(t) {
49
+ const everyMs = t?.everyMs
50
+ if (typeof everyMs === "number" && Number.isFinite(everyMs) && everyMs > 0) return `every ${everyMs / 1000}s`
51
+ const daily = t?.daily
52
+ if (typeof daily === "string" && DAILY_RE.test(daily)) return `daily ${daily}`
53
+ if (t?.at === true) return `at ${localText(t.expiresAt)}`
54
+ return "one-shot"
55
+ }
56
+
57
+ /** `list` 行第三槽:到期未投 ⇒ `due now`;否则 `next <dur>`(剩余现算)。 */
58
+ function nextText(t, now) {
59
+ const at = Number(t?.expiresAt)
60
+ return at > now ? `next ${durText(at - now)}` : "due now"
61
+ }
62
+
63
+ /** `message` 首行(`list` 行第四槽——逐字原样,不截断)。 */
64
+ function firstLine(message) {
65
+ const text = String(message ?? "")
66
+ const i = text.indexOf("\n")
67
+ return i < 0 ? text : text.slice(0, i)
68
+ }
69
+
70
+ /** `cancel`(§6.30.18):按 id 移除(`String` 宽松比对);零匹配 ⇒ 显式抛错(文案含 `list` 指引)。 */
71
+ function cancelAction(agent, wanted) {
72
+ const pending = agent?._pendingTimers ?? []
73
+ const idx = pending.findIndex((t) => String(t?.id) === String(wanted))
74
+ if (idx < 0) {
75
+ throw new Error(`timer cancel: no pending timer with id ${String(wanted)} — use list: true to see pending timers`)
76
+ }
77
+ const [hit] = pending.splice(idx, 1)
78
+ return `Timer cancelled (id ${String(hit.id)}).`
79
+ }
80
+
81
+ /** `list`(只读枚举——剩余现算;到期未投 ⇒ `due now` 态照出)。 */
82
+ function listAction(agent, now) {
83
+ const pending = agent?._pendingTimers ?? []
84
+ if (pending.length === 0) return "No pending timers."
85
+ return pending.map((t) => `${String(t.id)} · ${kindText(t)} · ${nextText(t, now)} · ${firstLine(t.message)}`).join("\n")
86
+ }
87
+
8
88
  export const timerTool = {
9
89
  name: "timer",
10
90
  description: DESC("timer"),
@@ -15,6 +95,26 @@ export const timerTool = {
15
95
  type: "number",
16
96
  description: "Thinking budget in seconds (default 180). Longer for complex reasoning, shorter for simple tasks.",
17
97
  },
98
+ at: {
99
+ type: "string",
100
+ description: 'Fire once at a local clock time, e.g. "2026-10-13T09:00" (YYYY-MM-DDTHH:MM[:SS]). A time already past fires immediately.',
101
+ },
102
+ every: {
103
+ type: "number",
104
+ description: "Fixed interval in seconds (minimum 60): the first reminder comes N seconds after setting, then every N seconds until cancelled.",
105
+ },
106
+ daily: {
107
+ type: "string",
108
+ description: 'Local "HH:MM" — repeats every day at that time (a time already past today fires tomorrow), until cancelled.',
109
+ },
110
+ cancel: {
111
+ type: "string",
112
+ description: "Id of a pending timer to stop (from the set confirmation; see also list).",
113
+ },
114
+ list: {
115
+ type: "boolean",
116
+ description: "List pending timers with their next due time (read-only).",
117
+ },
18
118
  message: {
19
119
  type: "string",
20
120
  description: "Custom reminder message to show when time is up. Default: a suggestion to add debug logs or run the code.",
@@ -25,22 +125,96 @@ export const timerTool = {
25
125
  readonly: true,
26
126
  sideEffectExempt: true,
27
127
  execute(args, ctx) {
28
- // D15.3#1:默认 180;非法值(非数字/非正数)显式报错——静默 NaN 定时器永不触发的坑
29
- const seconds = Number(args.seconds ?? 180)
30
- if (!Number.isFinite(seconds) || seconds <= 0) {
31
- throw new Error(`timer seconds must be a positive number, got: ${args.seconds}`)
128
+ const now = ctx.now ?? Date.now() // 时钟缝单点(§6.30.18):at 过时判定 ∥ every/daily 首投换算 ∥ id 撞号基准
129
+ const agent = ctx.agent
130
+
131
+ // 动作解析 + 互斥(至多一式;cancel ∥ list 与一切设置式互斥、与彼此互斥)——违例显式拒。
132
+ const hasSeconds = args.seconds !== undefined && args.seconds !== null
133
+ const hasAt = args.at !== undefined && args.at !== null
134
+ const hasEvery = args.every !== undefined && args.every !== null
135
+ const hasDaily = args.daily !== undefined && args.daily !== null
136
+ const hasCancel = args.cancel !== undefined && args.cancel !== null
137
+ const wantsList = args.list === true
138
+ const forms = [hasSeconds && "seconds", hasAt && "at", hasEvery && "every", hasDaily && "daily"].filter((v) => v !== false)
139
+ if (forms.length > 1 || (forms.length > 0 && (hasCancel || wantsList)) || (hasCancel && wantsList)) {
140
+ const given = [...forms, hasCancel && "cancel", wantsList && "list"].filter((v) => v !== false).join(" / ")
141
+ throw new Error(
142
+ `timer: pass at most one action per call — got ${given}. Forms: { seconds } one-shot ∥ { at: "YYYY-MM-DDTHH:MM[:SS]" } absolute ∥ { every: <seconds ≥ 60> } interval ∥ { daily: "HH:MM" } daily ∥ { cancel: <id> } stop ∥ { list: true } list`,
143
+ )
144
+ }
145
+ if (hasCancel) return cancelAction(agent, args.cancel)
146
+ if (wantsList) return listAction(agent, now)
147
+
148
+ // 逐式解算(校验逐条显式抛错——不静默钳制;拒点时在途表零变动)
149
+ let form = "seconds"
150
+ let expiresAt = null
151
+ let message = null
152
+ let everySec = null
153
+ let everyMs = null
154
+ let oneShotSec = null
155
+ if (hasAt) {
156
+ form = "at"
157
+ const raw = args.at
158
+ const ts = typeof raw === "string" ? atEpochMs(raw) : null
159
+ if (ts === null) {
160
+ throw new Error(`timer at must be a local clock time "YYYY-MM-DDTHH:MM[:SS]" (e.g. "2026-10-13T09:00"), got: ${String(raw)}`)
161
+ }
162
+ expiresAt = ts // 过时点 ⇒ 所给刻(delay ≤ 0 ⇒ 即时投递——AC-45① 口径;不重排)
163
+ message = `⏰ Time's up (at ${localText(ts)})${DEFAULT_TAIL}`
164
+ } else if (hasEvery) {
165
+ form = "every"
166
+ everySec = Number(args.every)
167
+ if (!Number.isFinite(everySec) || everySec * 1000 < TIMER_MIN_EVERY_MS) {
168
+ throw new Error(`timer every must be a number of seconds ≥ ${TIMER_MIN_EVERY_MS / 1000} (got: ${String(args.every)})`)
169
+ }
170
+ everyMs = everySec * 1000
171
+ expiresAt = nextFireAt({ everyMs, expiresAt: now }, now) // 首投 = 设时 + N(非立即——防「设置即投」双投感)
172
+ message = `⏰ Time's up (every ${everySec} seconds)${DEFAULT_TAIL}`
173
+ } else if (hasDaily) {
174
+ form = "daily"
175
+ if (typeof args.daily !== "string" || !DAILY_RE.test(args.daily)) {
176
+ throw new Error(`timer daily must be a local "HH:MM" time (24-hour, e.g. "09:00"), got: ${String(args.daily)}`)
177
+ }
178
+ expiresAt = nextFireAt({ daily: args.daily }, now) // 今日该刻未过 ⇒ 今日;已过 ⇒ 明日(非即时)
179
+ message = `⏰ Time's up (daily at ${args.daily})${DEFAULT_TAIL}`
180
+ } else {
181
+ // D15.3#1:默认 180;非法值(非数字/非正数)显式报错——静默 NaN 定时器永不触发的坑
182
+ const seconds = Number(args.seconds ?? 180)
183
+ if (!Number.isFinite(seconds) || seconds <= 0) {
184
+ throw new Error(`timer seconds must be a positive number, got: ${args.seconds}`)
185
+ }
186
+ expiresAt = now + seconds * 1000
187
+ message = `⏰ Time's up (${seconds}s)${DEFAULT_TAIL}`
188
+ oneShotSec = seconds
32
189
  }
33
- const expiresAt = Date.now() + seconds * 1000
34
- const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
35
190
 
36
191
  // §6.30.3 门三件②(D-TW6):在途帽 8——超限显式拒(不静默丢 / 不静默清;形态先例 = 队满
37
- // 「拒 + 提示 + 保留」)。拒点时在途表零变动(本次不推入)。
38
- const pending = (ctx.agent._pendingTimers = ctx.agent._pendingTimers ?? [])
192
+ // 「拒 + 提示 + 保留」)。周期项同占坑(1 条 1 坑——§6.30.18 D-TW15;重排净零不触帽)。
193
+ const pending = (agent._pendingTimers = agent._pendingTimers ?? [])
39
194
  if (pending.length >= TIMER_MAX_PENDING) {
40
195
  throw new Error(`timer: ${pending.length} timers already pending (max ${TIMER_MAX_PENDING}) — wait for one to fire before setting another`)
41
196
  }
42
- pending.push({ id: Date.now(), expiresAt, message })
197
+ // id 唯一化(取消可寻址·§6.30.18):基准读时钟缝单点,与在途撞号 ⇒ `+1` 递增至空闲(零新载体字段)。
198
+ let id = now
199
+ while (pending.some((t) => String(t?.id) === String(id))) id += 1
200
+ const entry = { id, expiresAt, message: args.message || message }
201
+ if (everyMs !== null) entry.everyMs = everyMs
202
+ if (form === "daily") entry.daily = args.daily
203
+ if (form === "at") entry.at = true // 来源标记位(非载重——时刻单源 = `expiresAt`;父侧裁定 2026-10-12)
204
+ pending.push(entry)
43
205
 
44
- return `Timer set for ${seconds} seconds. A reminder will appear when time is up.`
206
+ if (form === "at") {
207
+ const when = localText(expiresAt)
208
+ return expiresAt > now
209
+ ? `Timer set (id ${id}) for ${when} local. Stop it with cancel id ${id}.`
210
+ : `Timer set (id ${id}) for ${when} local — that time has passed, so the reminder appears immediately.`
211
+ }
212
+ if (form === "every") {
213
+ return `Repeating timer set (id ${id}): first reminder in ${everySec} seconds, then every ${everySec} seconds. Stop it with cancel id ${id}.`
214
+ }
215
+ if (form === "daily") {
216
+ return `Daily timer set (id ${id}): next reminder ${localText(expiresAt)} local, then every day at ${args.daily}. Stop it with cancel id ${id}.`
217
+ }
218
+ return `Timer set for ${oneShotSec} seconds. A reminder will appear when time is up.`
45
219
  },
46
220
  }
@@ -36,7 +36,7 @@ export function advisorAlertText(from, rounds) {
36
36
  * 命中 ⇒ `pushChildUpstream({parent, from: _upstream.label, kind: "note", message})` + 置闩。
37
37
  * **恒零抛**(兜底捕获兑现——自动源无模型可报错;失败面静默跳过)。
38
38
  * @param {object} child 子 agent(eng-coder / eng-designer 形态;`_upstream` 由 spawn 站点装配)。
39
- * @returns {boolean} 是否命中推送(测试面)。
39
+ * @returns {boolean} 是否命中推送(测试)。
40
40
  */
41
41
  export function maybeZeroWriteAlert(child) {
42
42
  try {
@@ -65,7 +65,7 @@ export function maybeZeroWriteAlert(child) {
65
65
  * @param {object} parent 父 agent(接收方——`pushChildUpstream` 载体吸收同款)。
66
66
  * @param {string} from 来源标签(`advisor#<entry id>`)。
67
67
  * @param {number} rounds 连续无产出轮数。
68
- * @returns {boolean} 是否推送成功(测试面)。
68
+ * @returns {boolean} 是否推送成功(测试)。
69
69
  */
70
70
  export function pushAdvisorAlert(parent, from, rounds) {
71
71
  try {
package/agent-tools.mjs CHANGED
@@ -13,13 +13,14 @@ export { recentChangesTool } from "./agent-tools/recent-changes.mjs"
13
13
  export { timerTool } from "./agent-tools/timer.mjs"
14
14
  export { advisorTool } from "./agent-tools/advisor.mjs"
15
15
  export { engTool } from "./agent-tools/eng.mjs"
16
+ export { manifestTool } from "./agent-tools/manifest.mjs"
16
17
  export { readHistoryTool } from "./agent-tools/read-history.mjs"
17
18
  // 模型主动整理上下文(CONTEXT-COMPACTION.md §6.16 · F-CC5):单工具三操作 stats / prune / compact
18
19
  // ——depth-0 段挂载(子代理面裁见 §6.16.6 `family-tools.mjs` `depthOnly`)。
19
20
  export { contextTool } from "./agent-tools/context.mjs"
20
21
  export { batchTool } from "./agent-tools/batch.mjs"
21
22
  // 批次档生命周期工具(BATCH-RECORD §4.3 挂载表——主名 `batch` 单工具四 action:depth-0 主 agent
22
- // create/close + append §1/§4/§6 + status §1(轮 2 裁定②:§4/§6 状态面走普通文档写)(D-BR18);
23
+ // create/close + append/status 域随模式(工程面 = append §1/§4/§6 ∥ status §1;普通面 = append §1/§2/§4/§5/§6 ∥ status §1/§2/§5)(轮 2 裁定②:§4/§6 状态面走普通文档写)(D-BR18);
23
24
  // eng 子代理 eng 分支绑定段写入;评审 §3。
24
25
  // CORE-UNIFICATION TOOLS #83(统一登记册——VSC `agent-tools/index.mjs:15` 含 consult 家族;
25
26
  // CLI 原把 consult 另挂 `agent/setup.mjs:173` ⇒ 归位:登记册即单一来源,装配方只读本档)。
package/agent.mjs CHANGED
@@ -91,7 +91,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
91
91
  try {
92
92
  return await runTurnLoop(agent, {
93
93
  maxTurns, threshold, toolSchemas, toolByName, systemPrompt,
94
- depth, signal, autoTurn, streamOutput, consumeInjected, consumeQueuedInput, callbacks, distillSignal,
94
+ depth, signal, autoTurn, suspDriven, streamOutput, consumeInjected, consumeQueuedInput, callbacks, distillSignal,
95
95
  })
96
96
  } catch (e) {
97
97
  thrownError = e
package/attachments.mjs CHANGED
@@ -64,7 +64,7 @@ export function isNonVisionModel(model) {
64
64
  */
65
65
  export function savePastedImages(dataUrls, cwd, { fs } = {}) {
66
66
  const list = Array.isArray(dataUrls) ? dataUrls : []
67
- if (list.length === 0) return { paths: [], dropped: 0 } // 空表先于缝校验早退(零判据面 —— 缝不检)
67
+ if (list.length === 0) return { paths: [], dropped: 0 } // 空表先于缝校验早退(零判据 —— 缝不检)
68
68
  if (typeof fs?.mkdirSync !== "function" || typeof fs?.writeFileSync !== "function") {
69
69
  throw new TypeError("savePastedImages: fs seam missing — inject { mkdirSync, writeFileSync } (the write face must not silently drop every image)")
70
70
  }
package/config-io.mjs CHANGED
@@ -141,7 +141,7 @@ const warnedContext = new Set()
141
141
  /**
142
142
  * Resolve providers list + active name from disk(VSC `config-io.mjs` 逐字随迁)。BaseURL
143
143
  * trailing slashes normalized. `activeProvider` 语义 = config.defaultModel 的渠道;缺失/失效
144
- * → 回退首 provider。渠道单值 `model` 归一:非字符串/空串删除。
144
+ * → 回退首 provider。渠道单值 `model` 一律删除(2026-10-09 清除批——渠道不携模型,零回落)。
145
145
  */
146
146
  export function resolveProviders() {
147
147
  const raw = loadRaw()
@@ -150,8 +150,8 @@ export function resolveProviders() {
150
150
  : []
151
151
  for (const p of providers) {
152
152
  if (typeof p.baseURL === "string") p.baseURL = p.baseURL.replace(/\/+$/, "")
153
- if (typeof p.model === "string" && p.model.trim()) p.model = p.model.trim()
154
- else delete p.model
153
+ // 2026-10-09 清除批(PROVIDER.md §6.16 M3):渠道不携模型——一律删,不落任何回退
154
+ delete p.model
155
155
  // PROVIDER.md §15 D-C1: providers[].context must be a positive integer (K units).
156
156
  if (p.context != null) {
157
157
  const n = Number(p.context)
@@ -218,9 +218,11 @@ export function removeProviderKeyFromConfig(name) {
218
218
 
219
219
  const FORMATS = ["openai", "anthropic", "google"]
220
220
 
221
- /** 新增渠道。payload: { preset?: name, custom?: { name, baseURL, model, format }, key? }
221
+ /** 新增渠道。payload: { preset?: name, custom?: { name, baseURL, format }, key?, proxy? }
222
+ * `proxy === true` ⇒ 同批落 `proxy: true`(缺 ∥ 非真 ⇒ 零键——与行面 `handleSetProviderProxy` 同判据)。
223
+ * 2026-10-09 清除批:custom 不携 `model`——必填判 ∥ 落键双退(渠道不携模型——R3)。
222
224
  * 返回错误串或 null。 */
223
- export function addProviderEntry({ preset, custom, key } = {}) {
225
+ export function addProviderEntry({ preset, custom, key, proxy } = {}) {
224
226
  let providers
225
227
  try {
226
228
  ({ providers } = resolveProviders())
@@ -240,22 +242,25 @@ export function addProviderEntry({ preset, custom, key } = {}) {
240
242
  if (existing.has(name) || PROVIDER_PRESETS[name]) return `Name "${name}" is already in use`
241
243
  const baseURL = (custom.baseURL || "").trim().replace(/\/+$/, "")
242
244
  if (!baseURL) return "Base URL is required"
243
- const model = (custom.model || "").trim()
244
- if (!model) return "Model is required"
245
245
  const format = (custom.format || "openai").trim()
246
246
  if (!FORMATS.includes(format)) return `Unknown API format: ${format} (expected ${FORMATS.join("/")})`
247
- entry = { name, baseURL, model } // MODEL-SELECTION:渠道单值默认模型(候选清单字段已退场)
247
+ entry = { name, baseURL } // 2026-10-09 清除批:渠道不携模型——单值 model 双退(R3 零读写)
248
248
  if (format !== "openai") entry.format = format
249
249
  } else {
250
250
  return "Add provider needs a preset or a custom config"
251
251
  }
252
252
 
253
- const r = persistRaw((raw) => { (raw.providers ??= []).push(entry) })
254
- const err = conflictError(r)
255
- if (err) return err // F5b:config 被并发方改过——放弃 + 提示重试(决策① A)
253
+ // #1027(三端对齐批):走 proxy 旗——仅真值落键(运行期 = 逐渠 `proxy: true` ∧ 代理 `uri` 在案——无全局闸;2026-10-08)
254
+ if (proxy === true) entry.proxy = true
255
+
256
+ // #1062(PROVIDER.md §6.23):落条与置钥合为**一次**写——单写原子(不被两段写撕开 ⇒ 半状态
257
+ // 不可达);F5b 冲突 ⇒ 提示串且零落盘(条目 ∥ 钥俱不落)。
256
258
  const k = (key || "").trim()
257
- if (k) setProviderKey(entry.name, k)
258
- return null
259
+ const r = persistRaw((raw) => {
260
+ (raw.providers ??= []).push(entry)
261
+ if (k) entry.apiKey = k
262
+ })
263
+ return conflictError(r)
259
264
  }
260
265
 
261
266
  /** 移除渠道。激活渠道受保护(CLI parity)。返回错误串或 null。 */
@@ -5,17 +5,14 @@
5
5
  * the caller (CLI: config.mjs loadConfig; VSC: config-io.mjs loadRaw — same rule, 双端各自
6
6
  * 独立实现——不做同步依赖).
7
7
  *
8
- * 目标形态 C:每渠道恰好一个默认模型(`providers[].model` 单值)+ 顶层 `defaultModel` 复合。
8
+ * 目标形态 C(2026-10-09 清除批:渠道单值模型退场——M7 v2):渠道**不携模型**(`providers[].model` /
9
+ * `models[]` 一律删)+ 顶层 `defaultModel` 复合(新会话起点)。
9
10
  * 老形态迁移(v2):
10
- * - A:`providers[].model` + `activeProvider/activeModel` → `p.model` **保留**(不再搬入 models);
11
- * `activeProvider/activeModel` → `defaultModel` 复合(`activeModel` 优先;AP 不存在/AM 空 → 首渠道回退)
12
- * - B:`providers[].models[]` → `p.model = defaultModel 属本渠道的模型段 ?? 现有 p.model(非空字符串)
13
- * ?? models[] 首个非空字符串`,然后 `delete p.models`
14
- * - 混合/垃圾:非数组 `p.models` / 非字符串(空串)`p.model` → 删除(清理)
15
- * - 顺序:先构造/读取有效 `defaultModel` 值(含老 active* 转换),再按它给各渠道播种 `p.model`
16
- * - 幂等:无老字段(无 `models` / 无 active* / 无垃圾值)→ 返回 false 不动;slot 旧字段
11
+ * - A:`activeProvider/activeModel` → `defaultModel` 复合(`activeModel` 优先;AP 不存在/AM 空 → 首渠道回退)
12
+ * - B:`providers[].models[]` / `providers[].model` → 一律删除(清理)
13
+ * - 顺序:先构造/读取有效 `defaultModel` 值(含老 active* 转换),再清各渠道 `model` / `models` 键
14
+ * - 幂等:无老字段(无 `models` / 无 `model` / 无 active*)→ 返回 false 不动;slot 旧字段
17
15
  * (会话文件内)不迁移——读侧容忍
18
- * - 空结果合法:渠道无模型来源 → `p.model` 不设(模型选择经 `/models` 拉取候选——M8/M9)
19
16
  *
20
17
  * ── 核内合并(CORE-UNIFICATION §2.5 #79 · CONFIG 组「取并集」)──
21
18
  * ② `migrateCore`(VS Code 旧设置 / 密钥库迁移遍——VSC 侧逐字随迁 + 依赖面收核为注入参数):
@@ -46,25 +43,12 @@ export function migrateLegacyModelFields(raw) {
46
43
  changed = true
47
44
  }
48
45
 
49
- // ── ② 渠道播种单值 `p.model` + 清 models ──
50
- const dm = typeof raw.defaultModel === "string" && raw.defaultModel.trim() ? raw.defaultModel : null
51
- const dmSep = dm ? dm.indexOf(":") : -1
52
- const dmProvider = dmSep > 0 ? dm.slice(0, dmSep) : null
53
- const dmModel = dmSep > 0 && dmSep < dm.length - 1 ? dm.slice(dmSep + 1) : null
46
+ // ── ② 渠道模型键清除(2026-10-09 清除批——M7 v2:渠道不携模型,`model` / `models` 一律删;
47
+ // 幂等 = 无键零改)──
54
48
  for (const p of providers) {
55
49
  if (!p || typeof p !== "object") continue
56
- if (Array.isArray(p.models)) {
57
- const next = (p.name === dmProvider && dmModel)
58
- ? dmModel
59
- : (typeof p.model === "string" && p.model.trim() ? p.model : seedModel(p))
60
- if (next == null) delete p.model
61
- else p.model = next
62
- delete p.models
63
- changed = true
64
- continue
65
- }
66
- if (p.models !== undefined) { delete p.models; changed = true } // 非数组 models —— 垃圾清理
67
- if (p.model !== undefined && !(typeof p.model === "string" && p.model.trim())) { delete p.model; changed = true }
50
+ if (p.models !== undefined) { delete p.models; changed = true }
51
+ if (p.model !== undefined) { delete p.model; changed = true }
68
52
  }
69
53
  return changed
70
54
  }
@@ -93,7 +77,7 @@ function seedModel(p) {
93
77
  *
94
78
  * Never overwrites an apiKey already present in config.json (the CLI may have written it first).
95
79
  * Builtin preset names missing from providers[] are created from PROVIDER_PRESETS; unknown
96
- * names are only created when baseURL/model metadata is recoverable.
80
+ * names are only created when baseURL metadata is recoverable (渠道不携模型——2026-10-09 清除批).
97
81
  */
98
82
  export async function migrateCore(deps) {
99
83
  const { secrets, flags, legacySettings, clearLegacySettings } = deps
@@ -122,8 +106,8 @@ export async function migrateCore(deps) {
122
106
  entry = presetToEntry(name)
123
107
  } else {
124
108
  const meta = legacyMeta[name]
125
- if (!meta?.baseURL || !meta?.model) return // orphan key, no way to reconstruct
126
- entry = { name, baseURL: meta.baseURL, model: meta.model }
109
+ if (!meta?.baseURL) return // orphan key, no way to reconstruct(渠道不携模型——2026-10-09 清除批:门只认 baseURL,model 零读零落)
110
+ entry = { name, baseURL: meta.baseURL }
127
111
  // PROVIDER.md §15 D-C4: migrate-settings is the same source — a legacy
128
112
  // settings entry carrying context (K units) rides along into config.json.
129
113
  if (meta.context != null) entry.context = meta.context