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.
- package/CHANGELOG.md +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
|
83
|
-
mutate
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/tui/cmd-eng.mjs
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
69
|
-
* the CURRENT session slot
|
|
70
|
-
*
|
|
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(
|
|
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 —
|
|
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
|
}
|
package/src/tui/cmd-extract.mjs
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/tui/cmd-mcp.mjs
CHANGED
|
@@ -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 }
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -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
|
|
13
|
-
//
|
|
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 = []
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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.
|
|
71
|
+
const marker = s.slot === highlightSlot ? " ●" : ""
|
|
57
72
|
return {
|
|
58
73
|
type: "item",
|
|
59
74
|
text: `Slot ${s.slot} │ ${turns} │ ${when} │ ${label}${model}${marker}`,
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -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
|
}
|
package/src/tui/cmd-upgrade.mjs
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
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(
|
|
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
|
-
|
|
7
|
-
|
|
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 {
|
|
10
|
-
const
|
|
11
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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 */
|
package/src/tui/distill-cmd.mjs
CHANGED
|
@@ -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} (
|
|
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 })
|