thincoder 0.12.58 → 0.12.60

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 (158) hide show
  1. package/CHANGELOG.md +78 -2
  2. package/README.md +3 -3
  3. package/bin/thincoder.mjs +88 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +135 -26
  6. package/src/advisor/messages.mjs +57 -4
  7. package/src/advisor/run.mjs +119 -79
  8. package/src/advisor.mjs +34 -7
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +182 -22
  11. package/src/agent/helpers.mjs +71 -4
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +107 -20
  16. package/src/agent/spawn-child.mjs +54 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +133 -109
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +154 -104
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +26 -30
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/goal.mjs +11 -1
  25. package/src/agent-tools/read-history.mjs +284 -0
  26. package/src/agent-tools/recent-changes.mjs +2 -1
  27. package/src/agent-tools/settings.mjs +152 -0
  28. package/src/agent-tools/skill.mjs +2 -1
  29. package/src/agent-tools/subagent-actions.mjs +470 -0
  30. package/src/agent-tools/subagent-async.mjs +382 -0
  31. package/src/agent-tools/subagent-panel.mjs +153 -0
  32. package/src/agent-tools/subagent-run.mjs +202 -0
  33. package/src/agent-tools/subagent-scheduler.mjs +343 -0
  34. package/src/agent-tools/subagent-spawn.mjs +406 -0
  35. package/src/agent-tools/subagent.mjs +203 -377
  36. package/src/agent-tools/task.mjs +4 -3
  37. package/src/agent-tools/timer.mjs +9 -4
  38. package/src/agent-tools/verify.mjs +198 -238
  39. package/src/agent-tools.mjs +1 -0
  40. package/src/agent.mjs +145 -242
  41. package/src/auto-think.mjs +14 -0
  42. package/src/cli/distill-command.mjs +10 -4
  43. package/src/cli/make-agent.mjs +4 -1
  44. package/src/cli/memory-command.mjs +2 -1
  45. package/src/cli/permission.mjs +8 -1
  46. package/src/cli/setup-wizard.mjs +17 -12
  47. package/src/config.mjs +61 -8
  48. package/src/context.mjs +81 -163
  49. package/src/crash-reports.mjs +123 -0
  50. package/src/distill.mjs +30 -12
  51. package/src/escape.mjs +6 -4
  52. package/src/explore-distill.mjs +155 -0
  53. package/src/log.mjs +195 -0
  54. package/src/memory/code-sync.mjs +2 -1
  55. package/src/memory/core.mjs +11 -72
  56. package/src/memory/delete.mjs +234 -0
  57. package/src/memory/docs.mjs +206 -87
  58. package/src/memory.mjs +3 -1
  59. package/src/model-specs.mjs +15 -1
  60. package/src/peer-domains.mjs +265 -0
  61. package/src/peer-instances.mjs +231 -0
  62. package/src/prompt-overlays.mjs +25 -0
  63. package/src/prompts/advisor-design.md +18 -39
  64. package/src/prompts/advisor-round1.md +20 -32
  65. package/src/prompts/advisor-round2.md +16 -16
  66. package/src/prompts/advisor-round3.md +16 -16
  67. package/src/prompts/coder.md +7 -28
  68. package/src/prompts/consult-base.md +4 -11
  69. package/src/prompts/discipline.md +31 -44
  70. package/src/prompts/eng-coder.md +9 -34
  71. package/src/prompts/engineering-sub.md +10 -8
  72. package/src/prompts/engineering.md +61 -264
  73. package/src/prompts/explore.md +4 -14
  74. package/src/prompts/main.md +18 -35
  75. package/src/prompts/methodology-template.md +32 -38
  76. package/src/prompts/plan.md +2 -9
  77. package/src/prompts/system.md +18 -35
  78. package/src/provider/core.mjs +62 -69
  79. package/src/provider/errors.mjs +76 -0
  80. package/src/provider/retry.mjs +8 -45
  81. package/src/session-gc.mjs +214 -0
  82. package/src/session-guard.mjs +47 -0
  83. package/src/session-rename.mjs +38 -0
  84. package/src/session-slots.mjs +181 -58
  85. package/src/session.mjs +48 -89
  86. package/src/token-ttl.mjs +273 -0
  87. package/src/tools/apply_patch.md +3 -1
  88. package/src/tools/bash.md +1 -1
  89. package/src/tools/checklist-sync.mjs +181 -0
  90. package/src/tools/checklist.mjs +52 -39
  91. package/src/tools/delete.md +1 -0
  92. package/src/tools/edit-batch.mjs +131 -44
  93. package/src/tools/edit-diff.mjs +348 -0
  94. package/src/tools/edit.md +20 -13
  95. package/src/tools/execute.md +7 -7
  96. package/src/tools/execute.mjs +55 -24
  97. package/src/tools/file.mjs +25 -70
  98. package/src/tools/file_ops.md +2 -1
  99. package/src/tools/get_current_time.md +3 -1
  100. package/src/tools/git.mjs +14 -6
  101. package/src/tools/glob-dialect.mjs +130 -0
  102. package/src/tools/glob.md +3 -3
  103. package/src/tools/grep.md +1 -1
  104. package/src/tools/hashline_edit.md +2 -0
  105. package/src/tools/index.mjs +3 -3
  106. package/src/tools/insert_after.md +2 -1
  107. package/src/tools/lint.md +2 -0
  108. package/src/tools/lsp.md +4 -1
  109. package/src/tools/ops.mjs +175 -3
  110. package/src/tools/patch.mjs +84 -13
  111. package/src/tools/question.md +5 -1
  112. package/src/tools/repomap.mjs +1 -1
  113. package/src/tools/shared.mjs +18 -25
  114. package/src/tools/system.mjs +50 -30
  115. package/src/tools/tree.md +2 -1
  116. package/src/tools/wait_for.md +22 -0
  117. package/src/tools/web.mjs +5 -3
  118. package/src/tools/websearch.md +2 -1
  119. package/src/tools/write.md +2 -0
  120. package/src/traces/trace-store.mjs +224 -0
  121. package/src/tui/agent-turn.mjs +179 -27
  122. package/src/tui/clipboard.mjs +15 -4
  123. package/src/tui/cmd-config.mjs +77 -16
  124. package/src/tui/cmd-eng.mjs +20 -16
  125. package/src/tui/cmd-extract.mjs +1 -1
  126. package/src/tui/cmd-mcp.mjs +17 -2
  127. package/src/tui/cmd-new.mjs +3 -2
  128. package/src/tui/cmd-session.mjs +19 -4
  129. package/src/tui/cmd-think.mjs +11 -11
  130. package/src/tui/cmd-upgrade.mjs +19 -4
  131. package/src/tui/config-helpers.mjs +28 -16
  132. package/src/tui/distill-cmd.mjs +1 -1
  133. package/src/tui/index.mjs +31 -96
  134. package/src/tui/interaction.mjs +13 -2
  135. package/src/tui/key-handler.mjs +105 -155
  136. package/src/tui/key-modes.mjs +215 -0
  137. package/src/tui/layout.mjs +22 -1
  138. package/src/tui/mouse.mjs +46 -0
  139. package/src/tui/pickers.mjs +51 -25
  140. package/src/tui/render-conversation.mjs +13 -161
  141. package/src/tui/render-frame.mjs +27 -10
  142. package/src/tui/render-loop.mjs +4 -1
  143. package/src/tui/render-segments.mjs +182 -0
  144. package/src/tui/startup.mjs +40 -0
  145. package/src/tui/subagent-blocks.mjs +272 -262
  146. package/src/tui/subagent-children.mjs +176 -0
  147. package/src/tui/subagent-freeze.mjs +172 -0
  148. package/src/tui/subagent-panel.mjs +125 -12
  149. package/src/tui/suspension-drive.mjs +351 -0
  150. package/src/tui/tool-args.mjs +10 -2
  151. package/src/tui/tool-display.mjs +142 -0
  152. package/src/tui/tool-events.mjs +127 -231
  153. package/src/tui/tui-lifecycle.mjs +29 -0
  154. package/src/tui/update-notice.mjs +76 -0
  155. package/src/tui/wizard.mjs +48 -12
  156. package/src/agent-tools/escalate.mjs +0 -179
  157. package/src/agent-tools/subagent-check.mjs +0 -107
  158. package/src/tools/exec-prelude.mjs +0 -84
@@ -1,4 +1,3 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
1
  import { ansi, C } from "./ansi.mjs"
3
2
  /** Merge an embedding-key save into the raw config, backfilling baseURL/model from defaults.
4
3
  * Keeps existing custom values (Ollama/local embedding); defaults are the single source
@@ -17,8 +16,9 @@ export function embeddingPatch(raw, embKey, defaults) {
17
16
  export async function handleConfigCommand(ctx, args = []) {
18
17
  const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey, pickModelForSlot } = ctx
19
18
  const { configPath, DEFAULTS } = await import("../config.mjs")
20
- const ac = agent.config?.agent ?? {}
21
- const ec = agent.config?.embedding ?? {}
19
+ let ac = agent.config?.agent ?? {}
20
+ let ec = agent.config?.embedding ?? {}
21
+ let tc = agent.config?.traces ?? {}
22
22
 
23
23
  // agent.config.proxy 已被 loadConfig 归一化为 { uri, web, model } | undefined
24
24
  function proxySummary() {
@@ -30,9 +30,10 @@ export async function handleConfigCommand(ctx, args = []) {
30
30
  async function setEmbedKey() {
31
31
  const embKey = await askQuestion("Enter embedding API key (default: SiliconFlow bge-m3):")
32
32
  if (!embKey) return false
33
+ // D-F5b 语义先盘后存(embeddingPatch 在磁盘 fresh raw 上合并——冲突放弃不留 ghost)
34
+ await persistRaw((raw) => { raw.embedding = embeddingPatch(raw, embKey, DEFAULTS.embedding) })
33
35
  agent.config.embedding ??= {}
34
36
  agent.config.embedding.apiKey = embKey
35
- await persistRaw((raw) => { raw.embedding = embeddingPatch(raw, embKey, DEFAULTS.embedding) })
36
37
  if (agent.memory) {
37
38
  const { createEmbedder } = await import("../embedding.mjs")
38
39
  agent.memory.embedder = createEmbedder(agent.config.embedding)
@@ -77,13 +78,15 @@ export async function handleConfigCommand(ctx, args = []) {
77
78
  }
78
79
  }
79
80
 
80
- /** 保存 config(mutate 改 raw)→ reloadConfig(provider 代理无需重启即生效) */
81
+ /** 保存 config(mutate 改 raw)→ reloadConfig(provider 代理无需重启即生效)。
82
+ * D-F5b:磁盘新鲜读 → mutate → 写前 mtime 门控(writeConfigAtomic——冲突放弃 +
83
+ * .bak 留现场);冲突时先 reloadConfig 采纳磁盘新值再 throw——调用方 try/catch
84
+ * 统一展示 "Save failed: config changed on disk concurrently — retry"。 */
81
85
  async function saveProxy(mutate) {
82
- const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
83
- mutate(raw)
84
- const { saveConfig } = await import("../config.mjs")
85
- saveConfig(raw)
86
+ const { writeConfigAtomic, configPath } = await import("../config.mjs")
87
+ const r = writeConfigAtomic(configPath, mutate)
86
88
  await reloadConfig()
89
+ if (!r.ok) throw new Error("config changed on disk concurrently — retry")
87
90
  }
88
91
 
89
92
  // ── Proxy sub-menu loop:每轮重建 entries 显示最新状态,defaultIndex 记住上次位置 ──
@@ -228,17 +231,56 @@ export async function handleConfigCommand(ctx, args = []) {
228
231
  }
229
232
  }
230
233
 
234
+ // ── 并发池子菜单(§24 D-24a/R14——agent.poolLimits——读/改两域;保存经 saveProxy
235
+ // 落盘 + reloadConfig 热应用——下个 spawn 生效)──
236
+ async function poolMenu() {
237
+ let poolIdx = 0
238
+ for (;;) {
239
+ const pl = agent.config?.agent?.poolLimits ?? {}
240
+ // 显示回退与默认同源(DEFAULTS.agent.poolLimits——配置 DEFAULTS 与运行时回退常量
241
+ // ASYNC_POOL_LIMITS 的耦合由 T-24a4 锚定断言锁住——防默认值单侧漂移)
242
+ const cur = (k) => (Number.isInteger(pl[k]) && pl[k] >= 1 ? pl[k] : DEFAULTS.agent.poolLimits[k])
243
+ const entries = [
244
+ { type: "header", text: `Async pools: eng-coder ${cur("engCoder")} / other ${cur("other")}(默认 4/4——分域互不阻塞——超限排队)` },
245
+ { type: "item", text: `eng-coder 池上限 = ${cur("engCoder")}`, action: "engCoder" },
246
+ { type: "item", text: `其他角色池上限 = ${cur("other")}`, action: "other" },
247
+ ]
248
+ const c = await showPicker("并发池(agent.poolLimits)", entries, { defaultIndex: poolIdx })
249
+ if (!c) return // Esc 返回主菜单
250
+ poolIdx = Math.max(0, entries.filter((e) => e.type === "item").indexOf(c))
251
+ const val = await askQuestion(`${c.action} pool limit (current: ${cur(c.action)} — positive integer ≥1):`)
252
+ if (!val) continue // 空输入不改动
253
+ const num = Number(val)
254
+ if (!Number.isInteger(num) || num < 1) { pushLine("Pool limit must be a positive integer (≥1)", C.error); continue }
255
+ const next = { engCoder: cur("engCoder"), other: cur("other"), [c.action]: num }
256
+ try {
257
+ await saveProxy((raw) => { raw.agent ??= {}; raw.agent.poolLimits = { engCoder: next.engCoder, other: next.other } })
258
+ pushLabel("❯ Config", ansi.bold + C.tool)
259
+ pushLine(`agent.poolLimits = { engCoder: ${next.engCoder}, other: ${next.other} }(下个 spawn 生效——分域互不阻塞)`, C.tool)
260
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
261
+ }
262
+ }
263
+
231
264
  // ── Main config loop ──
232
265
  let running = true
233
266
  let mainIdx = 0 // 记住上次选中位置,改完一项回主菜单时恢复
234
267
  while (running) {
268
+ // 每轮刷新——保存分支经 reloadConfig 替换了 agent.config 对象——回菜单显示新值
269
+ ac = agent.config?.agent ?? {}
270
+ ec = agent.config?.embedding ?? {}
271
+ tc = agent.config?.traces ?? {}
235
272
  const consultCount = (ac.consultModels ?? []).length
273
+ const pl = ac.poolLimits ?? {}
274
+ const cur = (k) => (Number.isInteger(pl[k]) && pl[k] >= 1 ? pl[k] : DEFAULTS.agent.poolLimits[k])
236
275
  const mainEntries = [
237
- { type: "header", text: `proxy=${proxySummary()} | maxTurns=${ac.maxTurns ?? 200} | compactThreshold=${ac.compactThreshold ?? 100000} | verifyGuard=${ac.verifyGuard === true ? "on" : "off"} | consult=${consultCount} model(s) | embedding=${agent.memory?.embedder ? "on" : "off"}` },
276
+ { type: "header", text: `proxy=${proxySummary()} | maxTurns=${ac.maxTurns ?? 200} | compactThreshold=${ac.compactThreshold ?? 100000} | verifyGuard=${ac.verifyGuard === true ? "on" : "off"} | consult=${consultCount} model(s) | embedding=${agent.memory?.embedder ? "on" : "off"} | traces=${tc.enabled === false ? "off" : "on"}` },
238
277
  { type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 200}`, action: "agent.maxTurns" },
239
278
  { type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
279
+ { type: "item", text: `并发池 agent.poolLimits = engCoder ${cur("engCoder")} / other ${cur("other")}(async 分域上限)`, action: "pool" },
240
280
  { type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
241
281
  { type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
282
+ { type: "item", text: `traces.enabled = ${tc.enabled === false ? "off" : "on"}(轨迹存档——发布默认关——隐私)`, action: "traces.enabled" },
283
+ { type: "item", text: `traces.retentionHours = ${tc.retentionHours ?? 24} h(超期文件启动时清理)`, action: "traces.retentionHours" },
242
284
  { type: "item", text: `agent.consultModels = ${consultCount} model(s)${consultCount ? ` (${(ac.consultModels ?? []).map((m) => m.provider + ":" + m.model).join(", ")})` : ""}`, action: "consult" },
243
285
  { type: "item", text: `agent.consultTurns = ${ac.consultTurns ?? 40}`, action: "agent.consultTurns" },
244
286
  { type: "item", text: `agent.consultTimeoutMs = ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, action: "agent.consultTimeoutMs" },
@@ -259,14 +301,16 @@ export async function handleConfigCommand(ctx, args = []) {
259
301
  pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
260
302
  pushLine(`agent.compactThreshold: ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, C.dim)
261
303
  pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
304
+ pushLine(`traces.enabled: ${tc.enabled === false ? "off" : "on"}(默认 off——发布隐私——本地分析可开)`, C.dim)
305
+ pushLine(`traces.retentionHours: ${tc.retentionHours ?? 24}(超期文件启动清理——D-TR10)`, C.dim)
262
306
  pushLine(`agent.consultModels: ${(ac.consultModels ?? []).map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ") || "(none)"}`, C.dim)
307
+ pushLine(`agent.poolLimits: { engCoder: ${cur("engCoder")}, other: ${cur("other")} }(async 分域上限——默认 4/4——agent.poolLimits 可配)`, C.dim)
263
308
  pushLine(`agent.consultTurns: ${ac.consultTurns ?? 40}`, C.dim)
264
309
  pushLine(`agent.consultTimeoutMs: ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, C.dim)
265
310
  pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
266
311
  pushLine(`proxy: ${proxySummary()}`, C.dim)
267
312
  pushLine(`Config file: ${configPath}`, C.dim)
268
- running = false
269
- continue
313
+ continue // 回主菜单(Esc 退出)
270
314
  }
271
315
 
272
316
  if (choice.action === "proxy") {
@@ -279,8 +323,13 @@ export async function handleConfigCommand(ctx, args = []) {
279
323
  continue
280
324
  }
281
325
 
326
+ if (choice.action === "pool") {
327
+ await poolMenu()
328
+ continue
329
+ }
330
+
282
331
  if (choice.action === "embedkey") {
283
- if (await setEmbedKey()) running = false
332
+ await setEmbedKey() // 保存成功/取消都回主菜单(Esc 退出)
284
333
  continue
285
334
  }
286
335
 
@@ -293,9 +342,20 @@ export async function handleConfigCommand(ctx, args = []) {
293
342
  })
294
343
  pushLabel("❯ Config", ansi.bold + C.tool)
295
344
  pushLine(`agent.verifyGuard = ${newVal ? "on" : "off"}`, C.tool)
296
- running = false
297
345
  } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
298
- continue
346
+ continue // 回主菜单(Esc 退出)
347
+ }
348
+
349
+ if (choice.action === "traces.enabled") {
350
+ const newVal = tc.enabled === false // 显式写布尔(默认 off——DEFAULTS 合并后必有值)
351
+ try {
352
+ await saveProxy((raw) => {
353
+ raw.traces = { ...(raw.traces ?? {}), enabled: newVal }
354
+ })
355
+ pushLabel("❯ Config", ansi.bold + C.tool)
356
+ pushLine(`traces.enabled = ${newVal ? "on" : "off"}${newVal ? "(轨迹落盘 ~/.thincoder/traces/——保留 " + (tc.retentionHours ?? 24) + "h)" : ""}`, C.tool)
357
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
358
+ continue // 回主菜单(Esc 退出)
299
359
  }
300
360
 
301
361
  // Embedding model is fixed (BAAI/bge-m3, SiliconFlow) — no picker; it's over-engineering
@@ -308,6 +368,7 @@ export async function handleConfigCommand(ctx, args = []) {
308
368
  : label === "agent.subagentTurns" ? (ac.subagentTurns ?? 100)
309
369
  : label === "agent.compactThreshold" ? (ac.compactThreshold ?? 100000)
310
370
  : label === "agent.consultTurns" ? (ac.consultTurns ?? 40)
371
+ : label === "traces.retentionHours" ? (tc.retentionHours ?? 24)
311
372
  : isTimeout ? Math.round((ac.consultTimeoutMs ?? 600000) / 60000)
312
373
  : ""
313
374
  const val = await askQuestion(`${label} (current: ${current}${isTimeout ? " min" : ""}):`)
@@ -325,7 +386,7 @@ export async function handleConfigCommand(ctx, args = []) {
325
386
  pushLabel("❯ Config", ansi.bold + C.tool)
326
387
  pushLine(`${label} = ${isTimeout ? `${val} min (${stored} ms)` : val}`, C.tool)
327
388
  pushLine("(restart to apply)", C.dim)
328
- running = false
329
389
  } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
390
+ // 回主菜单(Esc 退出)——数值输入 Enter 空值也已 continue 于此
330
391
  }
331
392
  }
@@ -1,11 +1,12 @@
1
1
  /** /eng command: toggle engineering mode.
2
2
  * Requires METHODOLOGY.md in project root. Offers to create one if missing.
3
- * ctx: { agent, pushLine, pushLabel, persistRaw, showPicker } */
3
+ * ctx: { agent, pushLine, pushLabel, showPicker } */
4
4
  import { existsSync, copyFileSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs"
5
5
  import { join, dirname } from "node:path"
6
6
  import { fileURLToPath } from "node:url"
7
7
  import { ansi, C } from "./ansi.mjs"
8
8
  import { activeSlot, slotPath } from "../session.mjs"
9
+ import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
9
10
 
10
11
  const templateDir = join(fileURLToPath(import.meta.url), "..", "..", "prompts")
11
12
  import { ENG_OFF_REMINDER } from "../agent.mjs"
@@ -25,7 +26,7 @@ export function writeSessionFile(p, data) {
25
26
  }
26
27
 
27
28
  export async function handleEngCommand(ctx) {
28
- const { agent, pushLine, pushLabel, persistRaw, showPicker } = ctx
29
+ const { agent, pushLine, pushLabel, showPicker } = ctx
29
30
  agent.config.agent ??= {}
30
31
  const methodologyPath = join(agent.cwd, "METHODOLOGY.md")
31
32
 
@@ -47,31 +48,40 @@ export async function handleEngCommand(ctx) {
47
48
  }
48
49
 
49
50
  agent.config.agent.engineering = !agent.config.agent.engineering
51
+ // §24 D-24b: per-review instances die with the mode (fresh convergence cycles
52
+ // on the next toggle).
53
+ agent._advisorRuns = new Map()
50
54
  if (!agent.config.agent.engineering) {
51
- agent._engDesignToken = null // invalidate stale token
52
- agent._engDesignTokens = new Map() // multi-design slots die with the mode (2026-09-01 fix #2)
55
+ // R16 (2026-09-06 F-R16a): OFF 不清 token——有效 token 跨模式存活(仅 TTL 过期
56
+ // 在三清理时机删:恢复过滤 / 开模式清过期 / spawn 门禁拒时删槽)。
53
57
  // OFF must reach the model too (2026-08-25): /auto pushes a reminder on toggle — the
54
58
  // mode flip is invisible to the agent otherwise. (ON needs none here: the injector
55
59
  // in agent.mjs already announces ON transitions on the next turn.)
56
60
  agent._pendingReminders = agent._pendingReminders ?? []
57
61
  agent._pendingReminders.push(ENG_OFF_REMINDER)
58
62
  }
59
- await persistEngineering(ctx, agent)
63
+ // 开工程模式(真实 OFF→ON 转换)→ 清过期 token(有效保留——用户裁定"打开工程
64
+ // 模式时应该清理"——F-R16b ②——与 eng tool enter 同语义)。
65
+ const clearedExpired = agent.config.agent.engineering ? purgeExpiredDesignTokens(agent) : 0
66
+ await persistEngineering(agent)
60
67
  pushLabel("❯ Eng", ansi.bold + C.tool)
61
68
  pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"} (session)`, C.tool)
62
69
  if (agent.config.agent.engineering) {
63
70
  pushLine(` → strictly following ${methodologyPath}`, C.dim)
71
+ if (clearedExpired > 0) {
72
+ pushLine(` → cleared ${clearedExpired} expired design token${clearedExpired === 1 ? "" : "s"}; valid tokens from prior reviews stay usable`, C.dim)
73
+ }
64
74
  }
65
75
  }
66
76
 
67
77
  /**
68
- * Dual persistence (2026-08-29engineering is session-level): write the flipped flag into
69
- * the CURRENT session slot first (slot authority shared with VS Code, per-session), then
70
- * the config.json mirror (CLI visibility/compat; no longer the cross-session source of truth).
78
+ * Slot-only persistence (2026-09-08 — ENG-SESSION-PROVIDER-CLEANUP D1.1): the flipped flag
79
+ * goes into the CURRENT session slot only the slot is the sole authority (shared with
80
+ * VS Code, per-session; config.json is just the initial default, no mirror write).
71
81
  * The in-memory agent.config.agent.engineering (already flipped) stays the live authority for
72
82
  * this process; saveSession also round-trips it on every turn-end write.
73
83
  */
74
- async function persistEngineering(ctx, agent) {
84
+ async function persistEngineering(agent) {
75
85
  const slot = activeSlot(agent.cwd)
76
86
  try {
77
87
  const p = slotPath(agent.cwd, slot)
@@ -80,11 +90,5 @@ async function persistEngineering(ctx, agent) {
80
90
  data.engineering = agent.config.agent.engineering
81
91
  writeSessionFile(p, data)
82
92
  }
83
- } catch { /* slot missing/unreadable — config mirror still written */ }
84
- if (ctx.persistRaw) {
85
- await ctx.persistRaw((raw) => {
86
- raw.agent ??= {}
87
- raw.agent.engineering = agent.config.agent.engineering
88
- })
89
- }
93
+ } catch { /* slot missing/unreadable — in-memory flag already flipped; saveSession persists at turn end */ }
90
94
  }
@@ -7,7 +7,7 @@ export async function handleExtractCommand(ctx) {
7
7
  pushLine("[extract] Analyzing session...", C.dim)
8
8
  const count = await runDistill()
9
9
  const msg = count > 0
10
- ? `Knowledge extracted: ${count} candidate(s) saved to memory (agent will recall them via memory_search)`
10
+ ? `Knowledge extracted: ${count} candidate(s) saved to memory (agent will recall them via the memory tool)`
11
11
  : "No new knowledge found in this session."
12
12
  pushLine(msg, count > 0 ? C.tool : C.dim)
13
13
  }
@@ -82,9 +82,15 @@ export async function handleMcpCommand(ctx, args = []) {
82
82
  }
83
83
 
84
84
  async function removeServer(name) {
85
+ // D-F5a(#7)先盘后存:磁盘 fresh raw.mcp.servers 上只删目标条目——不整节写回内存
86
+ // agent.config.mcp.servers(内存含 reloadMcpFromDisk 的 keptConnected 尾巴——
87
+ // 整节写回会把对端磁盘上其他 server 的新改动一起抹掉);冲突放弃不留内存 ghost
88
+ await persistRaw((raw) => {
89
+ raw.mcp ??= { servers: [] }
90
+ if (!Array.isArray(raw.mcp.servers)) return
91
+ raw.mcp.servers = raw.mcp.servers.filter((s) => s?.name !== name)
92
+ })
85
93
  agent.config.mcp.servers = getServers().filter((s) => s.name !== name)
86
- // 评审 #1:磁盘无 mcp 段时(T23 场景——mcp 段被整体删除而连接保留)raw.mcp 为 undefined → 先建段再写
87
- await persistRaw((raw) => { raw.mcp ??= { servers: [] }; raw.mcp.servers = agent.config.mcp.servers })
88
94
  // Remove from tool list
89
95
  const { removeMcpTools } = await import("../mcp.mjs")
90
96
  removeMcpTools(agent, name)
@@ -174,6 +180,15 @@ Return ONLY the JSON object:`,
174
180
  }],
175
181
  tools: [],
176
182
  signal: AbortSignal.timeout(15_000),
183
+ // §18.6 D-TR4/D-TR6(2026-09-04 fix round1):/mcp ai 生成调用经 chat() 唯一
184
+ // 采集点——补轨迹元数据 + traces 开关透传(agent.config.traces.enabled——
185
+ // 关=不落盘必须全覆盖——与 agent.mjs/context.mjs 同模式)
186
+ logCtx: {
187
+ stage: "mcp", kind: "mcp",
188
+ role: agent?._role ?? null,
189
+ session: agent?._sessionStart ?? null, cwd: agent?.cwd ?? process.cwd(),
190
+ traces: agent?.config?.traces?.enabled !== false,
191
+ },
177
192
  })
178
193
  const jsonMatch = (res.content ?? "").match(/\{[\s\S]*\}/)
179
194
  if (!jsonMatch) { pushLine("[mcp] AI response not valid JSON", C.error); return }
@@ -9,8 +9,9 @@ export async function handleNewCommand(ctx) {
9
9
  const doNewSession = () => {
10
10
  const slot = newSession(agent.cwd)
11
11
  // 2026-08-31 会诊 F3:resetSessionState 清全量会话态(_fullHistory/title/_sessionStart/
12
- // _engDesignToken/压缩与验证计数等)——原实现只清 agent.history,新会话首次落盘把旧
13
- // 会话完整人类线 + 旧标题写进新 slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
12
+ // 多槽设计凭证(_engDesignTokens——单值镜像 _engDesignToken 已退役 D3)/压缩与验证计数
13
+ // 等)——原实现只清 agent.history,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
14
+ // slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
14
15
  resetSessionState(agent)
15
16
  agent._slot = slot
16
17
  state.tasks = []
@@ -1,4 +1,4 @@
1
- import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy } from "../session.mjs"
1
+ import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker } from "../session.mjs"
2
2
  import { ansi, C } from "./ansi.mjs"
3
3
  import { restoreLines } from "./startup.mjs"
4
4
  import { stringWidth, sliceByWidth } from "./render.mjs"
@@ -19,8 +19,16 @@ export async function handleRenameCommand(ctx, args) {
19
19
  pushLine(`Title too long (max 80 chars)`, C.error)
20
20
  return
21
21
  }
22
- if (!renameSlot(agent.cwd, slot, title)) {
23
- pushLine(`Rename failed active session (slot ${slot}) not found`, C.error)
22
+ // §12.2.5:renameSlot 契约 { ok, reason }——失败原因对用户可见(F3)
23
+ const r = renameSlot(agent.cwd, slot, title)
24
+ if (!r.ok) {
25
+ const why = {
26
+ "file-missing": `active session (slot ${slot}) not found`,
27
+ "parse-failure": `session file (slot ${slot}) is corrupted`,
28
+ "mtime-conflict": "session changed on disk concurrently — retry",
29
+ "invalid-slot": `invalid slot ${slot}`,
30
+ }[r.reason] ?? r.reason
31
+ pushLine(`Rename failed — ${why}`, C.error)
24
32
  return
25
33
  }
26
34
  agent.title = title
@@ -37,6 +45,13 @@ export async function handleSessionCommand(ctx) {
37
45
  pushLine("No sessions (use /new to start a new session)", C.dim)
38
46
  return
39
47
  }
48
+ // 2026-09-05 §10 D-5:本端高亮按端记录(● = 记录槽 ∈ 列表 ? 记录槽 : manifest active
49
+ // 回退——含"记录槽已被对端删除"的守卫);listSlots 的 manifest active 语义不变
50
+ // (ACP session/list 零变化——manifest active 保留为列表回退高亮)
51
+ const rec = readEndMarker(agent.cwd)
52
+ const highlightSlot = (rec?.slot != null && slots.some((s) => s.slot === rec.slot))
53
+ ? rec.slot
54
+ : (slots.find((s) => s.isActive)?.slot ?? null)
40
55
  const shortDate = (d) => {
41
56
  const dt = new Date(d)
42
57
  return `${dt.getMonth() + 1}/${dt.getDate()} ${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`
@@ -53,7 +68,7 @@ export async function handleSessionCommand(ctx) {
53
68
  const turns = s.turnCount > 0 ? `${s.turnCount} turns` : "0 turns"
54
69
  const when = shortDate(s.updatedAt)
55
70
  const model = s.activeProvider ? ` — ${s.activeProvider}` : ""
56
- const marker = s.isActive ? " ●" : ""
71
+ const marker = s.slot === highlightSlot ? " ●" : ""
57
72
  return {
58
73
  type: "item",
59
74
  text: `Slot ${s.slot} │ ${turns} │ ${when} │ ${label}${model}${marker}`,
@@ -76,7 +76,7 @@ export async function handleThinkCommand(ctx, args = []) {
76
76
  if (e.action === "auto") {
77
77
  const newAuto = agent.config?.agent?.autoThink === true
78
78
  pushLine(`Auto-think: ${newAuto ? "ON" : "OFF"}`, C.tool)
79
- return // exit loop — no useful actions remain when auto mode just changed
79
+ return // exit loop — no useful actions remain when auto mode just changed (T8b 锁定——toggle 即退是已批准语义——2026-09-05 曾误改 OFF 回菜单——测试抓回归——已撤销)
80
80
  } else if (e.action === "effort") {
81
81
  pushLine(`Reasoning effort: ${e.level}`, C.tool)
82
82
  } else {
@@ -97,16 +97,16 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
97
97
  // 开 auto = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null
98
98
  // 会让 auto 每轮写入的 reasoning_effort 与 enable_thinking:false 矛盾同发(F2 违约)。
99
99
  // 仅清 null:thinking-type 模型的 {type:"disabled"} 不在评审 #1 范围,保持既有语义。
100
- if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
100
+ if (cur.thinking === null) { delete cur.thinking; await syncProviderField(agent.activeProvider, "thinking", undefined) }
101
101
  delete cur.reasoningEffort
102
- await syncProviderField("reasoningEffort", undefined)
102
+ await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
103
103
  }
104
104
  } else if (e.action === "effort") {
105
105
  // 选档位 = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null 会让
106
106
  // enable_thinking:false 与 reasoning_effort 矛盾同发(F2 违约)。仅清 null(同上注释)。
107
- if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
107
+ if (cur.thinking === null) { delete cur.thinking; await syncProviderField(agent.activeProvider, "thinking", undefined) }
108
108
  cur.reasoningEffort = e.level
109
- await syncProviderField("reasoningEffort", e.level)
109
+ await syncProviderField(agent.activeProvider, "reasoningEffort", e.level)
110
110
  } else {
111
111
  const enable = e.action === "on"
112
112
  if (isEffortOnly) {
@@ -116,9 +116,9 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
116
116
  // "on" 默认 effort 取 spec 枚举首值(交付评审 #2):硬编码 "high" 对 qwen3.8-max
117
117
  // (enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置)
118
118
  else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.[0] ?? "high" }
119
- await syncProviderField("thinking", cur.thinking)
120
- if (!enable) await syncProviderField("reasoningEffort", undefined)
121
- else await syncProviderField("reasoningEffort", cur.reasoningEffort)
119
+ await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
120
+ if (!enable) await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
121
+ else await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
122
122
  } else {
123
123
  if (enable) {
124
124
  cur.thinking = { type: thinkOnValue }
@@ -127,11 +127,11 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
127
127
  cur.thinking = isCustomThink ? undefined : { type: "disabled" }
128
128
  delete cur.reasoningEffort
129
129
  }
130
- await syncProviderField("thinking", cur.thinking)
130
+ await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
131
131
  if (enable) {
132
- await syncProviderField("reasoningEffort", cur.reasoningEffort)
132
+ await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
133
133
  } else {
134
- await syncProviderField("reasoningEffort", undefined)
134
+ await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
135
135
  }
136
136
  }
137
137
  }
@@ -1,9 +1,10 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
+ import { upgradeFailureText } from "./update-notice.mjs"
2
3
 
3
4
  /** /upgrade command: check for updates and optionally upgrade.
4
5
  * ctx: { agent, pushLine, pushLabel, showPicker } */
5
6
  export async function handleUpgradeCommand(ctx) {
6
- const { pushLine, pushLabel, showPicker } = ctx
7
+ const { pushLine, pushLabel, showPicker, render } = ctx
7
8
  const { checkForUpdate } = await import("../upgrade.mjs")
8
9
  const { readFileSync } = await import("node:fs")
9
10
 
@@ -31,13 +32,27 @@ export async function handleUpgradeCommand(ctx) {
31
32
  pushLine(`Upgrading to ${result.latest}...`, C.tool)
32
33
  const { exec } = await import("node:child_process")
33
34
  const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
34
- cp.stdout?.on("data", () => {})
35
- cp.stderr?.on("data", () => {})
35
+ let stdout = ""
36
+ let done = false
37
+ cp.stdout?.on("data", (d) => { stdout += d })
38
+ cp.stderr?.on("data", (d) => { stdout += d })
39
+ // 2026-09-05(advisor 🟡#5/#7):异步回调必须显式 render(pushLine 只写 lines 不绘制——
40
+ // handleSlash 返回后的统一 render 先于 exec 完成,结论行要等下次输入才可见);失败文案
41
+ // 复用 upgradeFailureText(附 npm 输出尾 ≤3 行——与 update-notice 后台路径同诊断能力)
42
+ cp.on("error", (e) => {
43
+ if (done) return
44
+ done = true
45
+ pushLine(`✗ Upgrade failed (${e.message}). Run \`thincoder upgrade\` manually.`, C.error)
46
+ render?.()
47
+ })
36
48
  cp.on("close", (code) => {
49
+ if (done) return
50
+ done = true
37
51
  if (code === 0) {
38
52
  pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
39
53
  } else {
40
- pushLine(`✗ Upgrade failed (exit ${code}). Run \`thincoder upgrade\` manually.`, C.error)
54
+ pushLine(upgradeFailureText(code, stdout), C.error)
41
55
  }
56
+ render?.()
42
57
  })
43
58
  }
@@ -1,26 +1,38 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
-
3
1
  /** persistRaw / syncProviderField / maskKey: config read/write helpers.
4
2
  * Extracted from index.mjs for shared use by slash-commands, wizard, and pickers.
5
- * createConfigHelpers(agent) returns { persistRaw, syncProviderField, maskKey } */
6
- export function createConfigHelpers(agent) {
7
- /** Read config.json mutate persist to disk */
3
+ * createConfigHelpers(agent) returns { persistRaw, syncProviderField, maskKey }
4
+ * R10 F5(D-F5b):persistRaw 全链(磁盘新鲜读→mutate→写)收口进 config.mjs
5
+ * writeConfigAtomic——mtime 门控 + .bak + 冲突放弃(见 MULTI-INSTANCE-COLLAB.md §2a.2)。
6
+ * createConfigHelpers(agent, opts):opts.configPath 为测试注入缝(默认 configPath——
7
+ * 生产 index.mjs 单参调用零变化)。 */
8
+ export function createConfigHelpers(agent, opts = {}) {
9
+ const cfgPath = opts.configPath ?? null // null → 默认 configPath(动态 import 解)
10
+
11
+ /** 磁盘新鲜读 → mutate(单操作)→ mtime 门控写;冲突时 throw(提示语见下——TUI
12
+ * slash 分发层统一 [error] 行展示;调用方若自带 try/catch 展示 Save failed 同文案)。 */
8
13
  async function persistRaw(mutate) {
9
- const { saveConfig, configPath } = await import("../config.mjs")
10
- const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
11
- mutate(raw)
12
- saveConfig(raw)
14
+ const { writeConfigAtomic, configPath } = await import("../config.mjs")
15
+ const r = writeConfigAtomic(cfgPath ?? configPath, mutate)
16
+ if (!r.ok) throw new Error("config changed on disk concurrently — retry")
13
17
  }
14
18
 
15
- /** Sync a field of the current provider to the providers array and persist to disk */
16
- async function syncProviderField(field, value) {
17
- const target = agent.providers.find((p) => p.name === agent.activeProvider)
18
- if (!target) return
19
- if (value === undefined) delete target[field]
20
- else target[field] = value
19
+ /** Sync a field of a provider (by name) to the providers array and persist to disk.
20
+ * D-F5a(#1):磁盘 fresh raw.providers 上单字段补丁——不取 agent.providers 整数组
21
+ * (长跑进程的内存 providers 快照不得整体写回——会抹掉对端对磁盘其他改动的保留)。
22
+ * 内存 agent.providers 目标项同步镜像(旧行为:改内存 + 整节写盘——镜像保留内存
23
+ * 一致性,磁盘写只剩单字段)。value === undefined 删除该字段。 */
24
+ async function syncProviderField(name, field, value) {
25
+ const mem = agent.providers?.find((p) => p?.name === name)
26
+ if (!mem) return // 内存无此 provider(旧语义 early return——不产生空写)
21
27
  await persistRaw((raw) => {
22
- raw.providers = agent.providers
28
+ raw.providers ??= []
29
+ const target = raw.providers.find((p) => p?.name === name)
30
+ if (!target) return // 磁盘目标已被对端删除 → 只做内存镜像,不落盘
31
+ if (value === undefined) delete target[field]
32
+ else target[field] = value
23
33
  })
34
+ if (value === undefined) delete mem[field]
35
+ else mem[field] = value
24
36
  }
25
37
 
26
38
  /** Mask API key for display */
@@ -22,7 +22,7 @@ export async function runDistill(ctx) {
22
22
  }
23
23
  let saved = 0
24
24
  for (const c of candidates) {
25
- pushLine(`── Candidate [${c.type}] ${c.title} (scope: ${c.scope ?? "personal"})`, C.warn)
25
+ pushLine(`── Candidate [${c.type}] ${c.title} (layer: ${c.layer ?? "personal"})`, C.warn)
26
26
  for (const line of c.content.split("\n").slice(0, 6)) pushLine(` ${line}`, C.dim)
27
27
  if (c.type === "rule") pushLine(" (rule type — consider writing manually; press y to extract)", C.warn)
28
28
  const accept = await askPermission("distill-save", { title: c.title })