thincoder 0.12.62 → 0.12.64
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 +27 -0
- package/README.md +46 -49
- package/bin/thincoder.mjs +68 -34
- package/package.json +7 -6
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +257 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -379
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +60 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +1 -1
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +1 -1
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +35 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +6 -5
- package/src/tui/cmd-plan.mjs +9 -0
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +24 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +33 -11
- package/src/tui/index.mjs +18 -10
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +22 -59
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +15 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +14 -14
- package/src/tui/subagent-blocks.mjs +20 -3
- package/src/tui/subagent-freeze.mjs +73 -2
- package/src/tui/suspension-drive.mjs +57 -23
- package/src/tui/tool-events.mjs +11 -8
- package/src/tui/tui-lifecycle.mjs +9 -0
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +6 -2
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -244
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -243
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -204
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -473
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -295
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -309
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -482
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -459
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -417
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -392
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -88
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/ledger.mjs +0 -227
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -415
- package/src/memory/core.mjs +0 -299
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -419
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/scan.mjs +0 -95
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -258
- package/src/prompts/discipline-normal.md +0 -185
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -60
- package/src/prompts/persona-engineering.md +0 -55
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -221
- package/src/session-guard.mjs +0 -59
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-segments.mjs +0 -100
- package/src/session-slots.mjs +0 -492
- package/src/session-store.mjs +0 -441
- package/src/session.mjs +0 -492
- package/src/skills.mjs +0 -153
- package/src/text-budget.mjs +0 -46
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -355
package/src/session-gc.mjs
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-gc.mjs — 会话目录残留 GC + 冷 cwd 报告/删除(SESSION.md §12,2026-09-06)。
|
|
3
|
-
*
|
|
4
|
-
* F1 残留 GC(自动):进程启动时对当前 cwd hash 前缀做一次轻量清理——.corrupted /
|
|
5
|
-
* .unreadable / .manifest.corrupted / .bak-* 保留 30 天,孤儿 .tmp 保留 7 天
|
|
6
|
-
*(mtime < now − retention 才删,等于保留期保留——older-than 边界语义)。
|
|
7
|
-
* 安全(N1):活跃槽(主文件在 + manifest slotSessions[N] 属主活)的现场一律保留;
|
|
8
|
-
* manifest 主文件 / end marker / 数据主文件永不进入候选(后缀预过滤只匹配残留后缀)。
|
|
9
|
-
*
|
|
10
|
-
* F2 冷 cwd(手动——12.2.4 三步,v1 不自动删 manifest):
|
|
11
|
-
* 判定:manifest mtime 距今 > 90 天 且 无任何活跃数据文件(.json.N 不存在或全部属死主);
|
|
12
|
-
* 报告:thincoder session gc --dry-run(跨 cwd 枚举,不删除);
|
|
13
|
-
* 删除:thincoder session gc --confirm <hash|--all>(整前缀清空——manifest+marker+
|
|
14
|
-
* .json.N 死主数据+裸 v1 {hash}.json(§12.7 D-V1)+残留;删除前重校验冷态——
|
|
15
|
-
* TOCTOU 防护 T12)。
|
|
16
|
-
*
|
|
17
|
-
* 拆分理由(§12.3 review #1):session-slots.mjs 已在 500 行硬限零余量,GC 是独立决策
|
|
18
|
-
* ("何时清何种残留"),独立成模块;原语(sessionPath/isProcessAlive)自 session-slots
|
|
19
|
-
* import,启动钩子(session.mjs resumeSlot 包装)只一行调用。
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { readdirSync, readFileSync, statSync, unlinkSync, existsSync, rmSync } from "node:fs"
|
|
23
|
-
import { join, dirname, basename } from "node:path"
|
|
24
|
-
import { configDir } from "./config.mjs"
|
|
25
|
-
import { sessionPath, isProcessAlive } from "./session-slots.mjs"
|
|
26
|
-
|
|
27
|
-
/** 保留期(§12.2.2):损坏现场(.corrupted/.unreadable/.manifest.corrupted)与并发轮转
|
|
28
|
-
* 备份(.bak-*)30 天;孤儿 .tmp 7 天(崩溃现场恢复窗口)。 */
|
|
29
|
-
export const RESIDUE_RETENTION_MS = 30 * 24 * 3600 * 1000
|
|
30
|
-
export const ORPHAN_TMP_RETENTION_MS = 7 * 24 * 3600 * 1000
|
|
31
|
-
/** 冷 cwd 阈值(§12.2.4):manifest mtime 距今 > 90 天(保守——正常开发会频繁触碰)。 */
|
|
32
|
-
export const COLD_CWD_RETENTION_MS = 90 * 24 * 3600 * 1000
|
|
33
|
-
|
|
34
|
-
function sessionsDir() { return join(configDir, "sessions") }
|
|
35
|
-
|
|
36
|
-
/** 残留分类(§12.2.2 后缀表):name 须以 `${prefix}.` 开头(prefix = `${hash}.json`)。
|
|
37
|
-
* 返回 { retention, slot, tmp } 或 null(主文件/end marker/他前缀——不动)。
|
|
38
|
-
* slot = 关联槽号(`.N.` 段),无则 null(如 .manifest.corrupted)。 */
|
|
39
|
-
function classifyResidue(name, prefix) {
|
|
40
|
-
if (!name.startsWith(prefix + ".")) return null
|
|
41
|
-
const slotMatch = name.slice(prefix.length).match(/^\.(\d+)\./)
|
|
42
|
-
const slot = slotMatch ? Number(slotMatch[1]) : null
|
|
43
|
-
if (name.endsWith(".corrupted") || name.endsWith(".unreadable")) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false }
|
|
44
|
-
if (/\.bak-\d+$/.test(name)) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false }
|
|
45
|
-
if (name.endsWith(".tmp")) return { retention: ORPHAN_TMP_RETENTION_MS, slot, tmp: true }
|
|
46
|
-
return null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** 活跃槽集合(§12.4 T3/T12 操作定义):.json.N 主文件存在 且 manifest slotSessions[N]
|
|
50
|
-
* 属主进程存活。manifest 缺失/损坏 → 空集(无活跃槽——不阻碍残留清理)。 */
|
|
51
|
-
function liveSlots(dir, prefix, aliveFn) {
|
|
52
|
-
const active = new Set()
|
|
53
|
-
try {
|
|
54
|
-
const m = JSON.parse(readFileSync(join(dir, `${prefix}.manifest`), "utf8"))
|
|
55
|
-
for (const [n, owner] of Object.entries(m.slotSessions ?? {})) {
|
|
56
|
-
if (!/^\d+$/.test(n) || !owner) continue
|
|
57
|
-
if (!existsSync(join(dir, `${prefix}.${n}`))) continue
|
|
58
|
-
const pid = parseInt(String(owner).split("-")[0])
|
|
59
|
-
if (pid && aliveFn(pid)) active.add(Number(n))
|
|
60
|
-
}
|
|
61
|
-
} catch { /* manifest 不可读 → 无活跃槽 */ }
|
|
62
|
-
return active
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* 残留 GC(§12.2.3——单 cwd 前缀,不跨 cwd 扫描):返回 { candidates, deleted }
|
|
67
|
-
* (dryRun 时 candidates 照列、deleted 为空——只列不删,N2 可预览)。
|
|
68
|
-
* 扫描先按后缀预过滤(N4——只 stat 残留候选),再对候选做活跃槽/孤儿/保留期判定。
|
|
69
|
-
*/
|
|
70
|
-
export function gcResidue({ dir = sessionsDir(), prefix, now = Date.now(), dryRun = false, aliveFn = isProcessAlive } = {}) {
|
|
71
|
-
const result = { candidates: [], deleted: [] }
|
|
72
|
-
let entries
|
|
73
|
-
try { entries = readdirSync(dir) } catch { return result }
|
|
74
|
-
const candidates = []
|
|
75
|
-
for (const name of entries) {
|
|
76
|
-
const c = classifyResidue(name, prefix)
|
|
77
|
-
if (c) candidates.push({ name, ...c })
|
|
78
|
-
}
|
|
79
|
-
if (!candidates.length) return result
|
|
80
|
-
const active = liveSlots(dir, prefix, aliveFn)
|
|
81
|
-
for (const c of candidates) {
|
|
82
|
-
if (c.slot !== null && active.has(c.slot)) continue // N1:活跃槽现场一律保留(T3)
|
|
83
|
-
const p = join(dir, c.name)
|
|
84
|
-
if (c.tmp && existsSync(p.slice(0, -".tmp".length))) continue // 非孤儿 .tmp(主文件在——写中/回退候选)
|
|
85
|
-
let st
|
|
86
|
-
try { st = statSync(p) } catch { continue }
|
|
87
|
-
if (st.mtimeMs >= now - c.retention) continue // 边界:older-than 才删,等于保留期保留(§12.4)
|
|
88
|
-
result.candidates.push(c.name)
|
|
89
|
-
if (!dryRun) {
|
|
90
|
-
try { unlinkSync(p); result.deleted.push(c.name) } catch { /* 占用/竞态——跳过 */ }
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return result
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const scheduledPrefixes = new Set()
|
|
97
|
-
|
|
98
|
-
/** 启动钩子(§12 review #8——N4):GC 延后到进程启动完成后空闲执行(setImmediate),
|
|
99
|
-
* 不阻塞启动路径;每进程每前缀一次(Set 去重——resumeSlot 可多次进入)。 */
|
|
100
|
-
export function scheduleSessionGC(cwd) {
|
|
101
|
-
let base
|
|
102
|
-
try { base = sessionPath(cwd) } catch { return }
|
|
103
|
-
if (scheduledPrefixes.has(base)) return
|
|
104
|
-
scheduledPrefixes.add(base)
|
|
105
|
-
setImmediate(() => {
|
|
106
|
-
try { gcResidue({ dir: dirname(base), prefix: basename(base) }) } catch { /* 清理失败静默——不影响主流程 */ }
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 冷 cwd 枚举(§12.2.4 步骤 1/2——跨 cwd 报告面,仅手动命令调用):候选 =
|
|
112
|
-
* manifest mtime 距今 > 90 天 且 无活跃数据文件(.json.N 主文件不存在或全部属死主进程)。
|
|
113
|
-
* 返回 [{ hash, prefix, manifestMtime, dataFiles, files }](files = 整前缀全部文件——
|
|
114
|
-
* 含裸 v1 `{hash}.json`,§12.7 D-V1——dataFiles 仅 .json.N,v1 不参与冷态判定)。
|
|
115
|
-
*/
|
|
116
|
-
export function listColdCwds({ dir = sessionsDir(), now = Date.now(), aliveFn = isProcessAlive } = {}) {
|
|
117
|
-
let entries
|
|
118
|
-
try { entries = readdirSync(dir) } catch { return [] }
|
|
119
|
-
const cold = []
|
|
120
|
-
for (const name of entries) {
|
|
121
|
-
if (!name.endsWith(".json.manifest")) continue // end marker(.manifest.cli/.vscode)与 .manifest.corrupted 天然排除
|
|
122
|
-
const prefix = name.slice(0, -".manifest".length) // `${hash}.json`
|
|
123
|
-
let st
|
|
124
|
-
try { st = statSync(join(dir, name)) } catch { continue }
|
|
125
|
-
if (st.mtimeMs >= now - COLD_CWD_RETENTION_MS) continue // 近期活跃(<90 天阈值——T10)
|
|
126
|
-
let m
|
|
127
|
-
try { m = JSON.parse(readFileSync(join(dir, name), "utf8")) } catch { continue } // 损坏 manifest 不判冷(.corrupted 归残留 GC)
|
|
128
|
-
const dataFiles = entries.filter((e) => e.startsWith(prefix + ".") && /^\d+$/.test(e.slice(prefix.length + 1)))
|
|
129
|
-
const hasLive = dataFiles.some((e) => {
|
|
130
|
-
const owner = m.slotSessions?.[e.slice(prefix.length + 1)]
|
|
131
|
-
const pid = parseInt(String(owner ?? "").split("-")[0])
|
|
132
|
-
return !!(pid && aliveFn(pid))
|
|
133
|
-
})
|
|
134
|
-
if (hasLive) continue // 有活跃数据文件 → 非冷(T12)
|
|
135
|
-
cold.push({
|
|
136
|
-
hash: prefix.slice(0, -".json".length),
|
|
137
|
-
prefix,
|
|
138
|
-
manifestMtime: st.mtimeMs,
|
|
139
|
-
dataFiles: dataFiles.length,
|
|
140
|
-
files: entries.filter((e) => e.startsWith(prefix + ".") || e === prefix), // 整前缀清空含裸 v1 {hash}.json(§12.7 D-V1)
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
return cold
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 删除指定冷 cwd 的整个前缀(§12.2.4 步骤 3——manifest+end marker+.json.N 死主数据+
|
|
148
|
-
* 裸 v1 {hash}.json(§12.7 D-V1)+残留;只删 manifest 留数据文件会制造孤儿数据,整前缀清空才真正释放)。
|
|
149
|
-
* 删除前重跑冷 cwd 判定(TOCTOU 防护,T12——期间变活跃则拒绝)。
|
|
150
|
-
*/
|
|
151
|
-
export function deleteColdCwd(hash, { dir = sessionsDir(), now = Date.now(), aliveFn = isProcessAlive } = {}) {
|
|
152
|
-
const target = listColdCwds({ dir, now, aliveFn }).find((c) => c.hash === hash)
|
|
153
|
-
if (!target) return { ok: false, reason: "not-cold", deleted: [] }
|
|
154
|
-
const deleted = []
|
|
155
|
-
for (const name of target.files) {
|
|
156
|
-
const p = join(dir, name)
|
|
157
|
-
try {
|
|
158
|
-
// §14.3.8(TUI-OOM-ROOTCAUSE):目录项(记录存储 sidecar `{prefix}.N.d`)递归删——
|
|
159
|
-
// 现 unlinkSync 对目录静默跳过(旧实现漏删 sidecar);数据文件维持 unlink。
|
|
160
|
-
if (statSync(p).isDirectory()) rmSync(p, { recursive: true, force: true })
|
|
161
|
-
else unlinkSync(p)
|
|
162
|
-
deleted.push(name)
|
|
163
|
-
} catch { /* 占用/竞态——跳过 */ }
|
|
164
|
-
}
|
|
165
|
-
return { ok: true, deleted }
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* `thincoder session gc` 子命令分发(§12.2.3/12.2.4 手动面——F2 执行入口仅 CLI,
|
|
170
|
-
* VS Code 端无 shell 子命令通道,review #7):
|
|
171
|
-
* --dry-run 报告当前 cwd 残留候选 + 跨 cwd 冷候选(只列不删,N2 预览)
|
|
172
|
-
* --confirm <hash> 删除指定冷 cwd 整前缀(警告 + 文件清单 + TOCTOU 重校验)
|
|
173
|
-
* --confirm --all 逐冷 cwd 同型警告删除
|
|
174
|
-
* 返回进程退出码(0/1)。dir/prefix/now/out/err/aliveFn 为测试注入缝(默认生产行为)。
|
|
175
|
-
*/
|
|
176
|
-
export function runSessionGc(args, { dir = sessionsDir(), prefix = null, cwd = process.cwd(), now = Date.now(), out = console.log, err = console.error, aliveFn = isProcessAlive } = {}) {
|
|
177
|
-
const dryRun = args.includes("--dry-run")
|
|
178
|
-
const confirmIdx = args.indexOf("--confirm")
|
|
179
|
-
const hasConfirm = confirmIdx >= 0
|
|
180
|
-
const confirmTarget = hasConfirm ? args[confirmIdx + 1] : null
|
|
181
|
-
// --dry-run 与 --confirm 互斥(review 🔵#4——同给时不得静默忽略 confirm;无值 --confirm 同为用法错误)
|
|
182
|
-
if (args[0] !== "gc" || (!dryRun && !hasConfirm) || (dryRun && hasConfirm) || (hasConfirm && !confirmTarget)) {
|
|
183
|
-
err("Usage: thincoder session gc --dry-run | --confirm <hash> | --confirm --all")
|
|
184
|
-
return 1
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (dryRun) {
|
|
188
|
-
out("Session GC dry-run — no files will be deleted.")
|
|
189
|
-
// 当前 cwd 残留(§12.2.3 删除面——自动 GC 的预览)
|
|
190
|
-
let p = prefix
|
|
191
|
-
if (!p) { try { p = basename(sessionPath(cwd)) } catch { p = null } }
|
|
192
|
-
const residue = p ? gcResidue({ dir, prefix: p, now, dryRun: true, aliveFn }) : { candidates: [] }
|
|
193
|
-
out(`Residue candidates for current project (${p ?? "unknown"}): ${residue.candidates.length}`)
|
|
194
|
-
for (const name of residue.candidates) out(` ${name}`)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const cold = listColdCwds({ dir, now, aliveFn })
|
|
198
|
-
if (dryRun) {
|
|
199
|
-
out(`Cold project candidates (manifest idle > 90 days, no live data files): ${cold.length}`)
|
|
200
|
-
for (const c of cold) {
|
|
201
|
-
out(` ${c.hash} manifest mtime ${new Date(c.manifestMtime).toISOString()} data files ${c.dataFiles} total files ${c.files.length}`)
|
|
202
|
-
}
|
|
203
|
-
if (cold.length) out('Run "thincoder session gc --confirm <hash>" (or --confirm --all) to permanently delete a cold project prefix.')
|
|
204
|
-
return 0
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// --confirm:删除前警告(N2 可逆——文件清单 + 永久删除提示);deleteColdCwd 内部重校验冷态(TOCTOU)
|
|
208
|
-
const targets = confirmTarget === "--all" ? cold : cold.filter((c) => c.hash === confirmTarget)
|
|
209
|
-
if (!targets.length) {
|
|
210
|
-
err(`Refused: ${confirmTarget} is not a cold project (active, recent, or unknown) — nothing deleted.`)
|
|
211
|
-
return 1
|
|
212
|
-
}
|
|
213
|
-
for (const t of targets) {
|
|
214
|
-
out(`WARNING: 此操作永久删除该 cwd 的全部会话历史 (hash ${t.hash}, ${t.files.length} files):`)
|
|
215
|
-
for (const name of t.files) out(` ${name}`)
|
|
216
|
-
const r = deleteColdCwd(t.hash, { dir, now, aliveFn })
|
|
217
|
-
if (!r.ok) { err(`Refused: ${t.hash} is no longer cold (became active) — skipped.`) ; continue }
|
|
218
|
-
out(`Deleted ${r.deleted.length} files for ${t.hash}.`)
|
|
219
|
-
}
|
|
220
|
-
return 0
|
|
221
|
-
}
|
package/src/session-guard.mjs
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-guard.mjs — F2 轮转守卫(2026-08-31 会诊 F2 🔴;2026-09-08 DESIGN-TOKEN-
|
|
3
|
-
* SETTLEMENT D1 自 session-slots.mjs 提取——session-slots 再越 500 行硬限,本守卫约 41
|
|
4
|
-
* 行随迁,verbatim 零语义变):saveSession 与 token-ttl persistEngTokens 共用同一份。
|
|
5
|
-
* 写前校验磁盘槽文件是否本进程会话的现场——磁盘文件 sessionStart 与本进程会话不符
|
|
6
|
-
* (另一进程/会话的现场)/版本更新/异 cwd → 先轮转 .bak 保留再写。检查按 mtime 缓存
|
|
7
|
-
* (agent._slotMtime):文件自上次检查/自写未变就跳过全量解析——每次保存都
|
|
8
|
-
* readFileSync+JSON.parse 多 MB 会话 → O(n²) 退化。文件存在但不可读(损坏/半写)→
|
|
9
|
-
* 改名 .corrupted 保留现场。返回轮转的 .bak 路径或 null。
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { existsSync, readFileSync, renameSync, statSync } from "node:fs"
|
|
13
|
-
import { basename } from "node:path"
|
|
14
|
-
// §14.3.8①(修正轮 #1):.bak 轮转对**非本会话活动绑定面**联动改名 sidecar(`{p}.d → {bak}.d`)。
|
|
15
|
-
import { RECORD_DIR_SUFFIX } from "./session-store.mjs"
|
|
16
|
-
|
|
17
|
-
/** 轮转时随迁记录存储(.bak 路径联动)——本会话活动绑定面跳过(防拔掉活动存储)。 */
|
|
18
|
-
function moveSidecar(src, dst, agent) {
|
|
19
|
-
const dir = src + RECORD_DIR_SUFFIX
|
|
20
|
-
if (agent?._recordStore?.dir === dir) return // 本会话活动绑定面——原地保留(§14.3.8①)
|
|
21
|
-
try {
|
|
22
|
-
if (existsSync(dir)) renameSync(dir, dst + RECORD_DIR_SUFFIX)
|
|
23
|
-
} catch { /* 随迁失败不阻断轮转(现场原地保留——读侧身份核验兜底) */ }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function guardForeignSlotFile(agent, p, slot) {
|
|
27
|
-
try {
|
|
28
|
-
if (!existsSync(p)) return null
|
|
29
|
-
const st = statSync(p)
|
|
30
|
-
if (st.mtimeMs === (agent._slotMtime ?? -1)) return null
|
|
31
|
-
const disk = JSON.parse(readFileSync(p, "utf8"))
|
|
32
|
-
// version>2 的新版文件无论 sessionStart 一律轮转(loadSlotFile 对 v3 返回 null 不动
|
|
33
|
-
// 文件——若其 sessionStart 为 null,旧版首次保存会静默覆盖;轮转 .bak 保证新版文件
|
|
34
|
-
// 保留)。磁盘文件 cwd 不匹配(异项目文件误落本路径)同样轮转——与 loadSlotFile/
|
|
35
|
-
// legacy 读的"别人的文件不动"原则对齐。
|
|
36
|
-
const diskIsNewer = typeof disk?.version === "number" && disk.version > 2
|
|
37
|
-
const diskStart = disk?.sessionStart ?? null
|
|
38
|
-
const myStart = agent._sessionStart ?? null
|
|
39
|
-
const diskForeign = typeof disk?.cwd === "string" && disk.cwd.toLowerCase() !== agent.cwd.toLowerCase()
|
|
40
|
-
if (diskIsNewer || diskForeign || (diskStart && diskStart !== myStart)) {
|
|
41
|
-
const bak = `${p}.bak-${Date.now()}`
|
|
42
|
-
renameSync(p, bak)
|
|
43
|
-
moveSidecar(p, bak, agent)
|
|
44
|
-
console.error(`[session] slot ${slot} holds ${diskIsNewer ? `a newer-version file (v${disk.version})` : diskForeign ? `a foreign-cwd file (${disk.cwd})` : `another session (start ${diskStart}, ours ${myStart})`} — preserved as ${basename(bak)}`)
|
|
45
|
-
agent._slotMtime = st.mtimeMs
|
|
46
|
-
return bak
|
|
47
|
-
}
|
|
48
|
-
agent._slotMtime = st.mtimeMs
|
|
49
|
-
return null
|
|
50
|
-
} catch {
|
|
51
|
-
// 文件存在但不可读(损坏/半写):改名 .corrupted 保留现场(2026-09-01 advisor 🔵——
|
|
52
|
-
// 与自身 loadSlotFile 的 .corrupted 约定 + VS Code saveSessionToSlot 对齐;.bak 保留
|
|
53
|
-
// 给 F2 轮转路径,损坏现场不再混入轮转后缀)。
|
|
54
|
-
if (existsSync(p)) {
|
|
55
|
-
try { renameSync(p, `${p}.corrupted`) } catch {}
|
|
56
|
-
}
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
59
|
-
}
|
package/src/session-migrate.mjs
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-migrate.mjs — one-time migration of legacy short-hash session files to the
|
|
3
|
-
* full 40-char sha1 hash. Extracted from session.mjs (file-size split).
|
|
4
|
-
*/
|
|
5
|
-
import { createHash } from "node:crypto"
|
|
6
|
-
import { renameSync, existsSync } from "node:fs"
|
|
7
|
-
import { join } from "node:path"
|
|
8
|
-
import { configDir } from "./config.mjs"
|
|
9
|
-
|
|
10
|
-
/** One-time migration: rename legacy short-hash session files to the full 40-char hash.
|
|
11
|
-
* Idempotent; runs on first access per cwd.
|
|
12
|
-
* Historical hash algorithms (all sha1, none normalized the drive letter):
|
|
13
|
-
* - CLI: sha1(cwd).slice(0, 12) — cwd comes from process.cwd() (uppercase drive on Windows)
|
|
14
|
-
* - VS Code: sha1(cwd).slice(0, 16) — cwd comes from uri.fsPath (LOWERCASE drive on Windows)
|
|
15
|
-
* Plus the previous migration attempt's assumption (normalized 12 = first 12 of the full hash).
|
|
16
|
-
* Every combination is tried — a migration that only checks one candidate misses real
|
|
17
|
-
* legacy files (drive-letter case differs between CLI and VS Code historical paths). */
|
|
18
|
-
/** 2026-09-01 advisor 🔵(VS Code 侧已修,CLI 对称补齐):已迁移/确认无 legacy 的 hash
|
|
19
|
-
* 记录在 Set 中短路——否则每次 sessionPath() 都重跑 5 候选 × 3 existsSync 的系统调用。 */
|
|
20
|
-
const migratedHashes = new Set() // full 40-char hash → migration already attempted (found none or done)
|
|
21
|
-
|
|
22
|
-
export function migrateHashLength(cwd, fullHash) {
|
|
23
|
-
if (migratedHashes.has(fullHash)) return false
|
|
24
|
-
const dir = join(configDir, "sessions")
|
|
25
|
-
const lower = cwd.replace(/^([A-Z]):/, (_, d) => d.toLowerCase() + ":")
|
|
26
|
-
const candidates = [
|
|
27
|
-
createHash("sha1").update(cwd).digest("hex").slice(0, 12),
|
|
28
|
-
createHash("sha1").update(cwd).digest("hex").slice(0, 16),
|
|
29
|
-
createHash("sha1").update(lower).digest("hex").slice(0, 12),
|
|
30
|
-
createHash("sha1").update(lower).digest("hex").slice(0, 16),
|
|
31
|
-
fullHash.slice(0, 12),
|
|
32
|
-
]
|
|
33
|
-
const newBase = join(dir, `${fullHash}.json`)
|
|
34
|
-
let migrated = false
|
|
35
|
-
for (const short of new Set(candidates)) {
|
|
36
|
-
const legacyBase = join(dir, `${short}.json`)
|
|
37
|
-
if (!existsSync(legacyBase) && !existsSync(`${legacyBase}.manifest`) && !existsSync(`${legacyBase}.1`)) continue
|
|
38
|
-
migrated = true
|
|
39
|
-
try {
|
|
40
|
-
for (const suffix of ["", ".manifest", ...Array.from({ length: 64 }, (_, i) => `.${i + 1}`)]) {
|
|
41
|
-
const from = legacyBase + suffix
|
|
42
|
-
if (existsSync(from) && !existsSync(newBase + suffix)) renameSync(from, newBase + suffix)
|
|
43
|
-
}
|
|
44
|
-
} catch { /* best-effort; leave files in place on failure */ }
|
|
45
|
-
}
|
|
46
|
-
migratedHashes.add(fullHash)
|
|
47
|
-
return migrated
|
|
48
|
-
}
|
package/src/session-rename.mjs
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-rename.mjs — 槽位重命名(标题写路径)。2026-09-06 自 session-slots.mjs 拆出:
|
|
3
|
-
* §12.2.5 契约改(boolean → { ok, reason })使 session-slots.mjs 超 500 行硬限
|
|
4
|
-
*(§12.3 授权——"若 >500 则按 §10.6 先例拆文件,一并拆出 renameSlot")。
|
|
5
|
-
* 原语(slotPath/manifest 读写)自 session-slots.mjs import;session.mjs re-export
|
|
6
|
-
* renameSlot 保持既有调用点(cmd-session.mjs / 测试经 session.mjs)不变。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { readFileSync, existsSync, statSync } from "node:fs"
|
|
10
|
-
import { slotPath, loadManifest, saveManifest, slotDigest, writeSessionFile } from "./session-slots.mjs"
|
|
11
|
-
|
|
12
|
-
/** Rename a slot: update the slot file's title + the manifest metadata (shared with VS Code).
|
|
13
|
-
* 2026-09-01 会诊 glm 🟡:写回前按 mtime 门控重读——原实现读全量→改 title→整文件写回,
|
|
14
|
-
* 窗口内并发方的最新保存会被旧数据覆盖(丢消息);mtime 变了即放弃本次重命名。
|
|
15
|
-
* 2026-09-06 §12.2.5:契约 boolean → { ok, reason? }(file-missing/parse-failure/mtime-conflict/invalid-slot,F3 失败可见);_stat = mtime-conflict 测试缝(默认 statSync)。 */
|
|
16
|
-
export function renameSlot(cwd, slot, title, _stat = statSync) {
|
|
17
|
-
const n = Number(slot)
|
|
18
|
-
if (!Number.isInteger(n) || n < 1) return { ok: false, reason: "invalid-slot" }
|
|
19
|
-
const p = slotPath(cwd, n)
|
|
20
|
-
if (!existsSync(p)) return { ok: false, reason: "file-missing" }
|
|
21
|
-
let data
|
|
22
|
-
try {
|
|
23
|
-
data = JSON.parse(readFileSync(p, "utf8"))
|
|
24
|
-
} catch {
|
|
25
|
-
return { ok: false, reason: "parse-failure" }
|
|
26
|
-
}
|
|
27
|
-
const t0 = _stat(p).mtimeMs
|
|
28
|
-
data.title = title
|
|
29
|
-
// 读与写之间文件被并发方改过 → 放弃(保留并发内容,重命名下次重试)
|
|
30
|
-
if (_stat(p).mtimeMs !== t0) return { ok: false, reason: "mtime-conflict" }
|
|
31
|
-
writeSessionFile(p, data)
|
|
32
|
-
const m = loadManifest(cwd)
|
|
33
|
-
if (m.slots[n]) {
|
|
34
|
-
m.slots[n] = slotDigest(data)
|
|
35
|
-
saveManifest(cwd, m)
|
|
36
|
-
}
|
|
37
|
-
return { ok: true }
|
|
38
|
-
}
|
package/src/session-segments.mjs
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-segments.mjs — 记录存储的段文件原语 + 人读线条目形态(TUI-OOM-ROOTCAUSE 批)。
|
|
3
|
-
*
|
|
4
|
-
* 机制契约全文 = docs/design/SESSION.md §14.3(D2 单一权威源);本文件 = 「段 IO + 条目
|
|
5
|
-
* 形态」叶子层(`session-store.mjs` 拆分产物——503+ 行越 500 硬限,§14.5 表列模块按职责
|
|
6
|
-
* 拆分;store 继续 re-export 全部公开名,调用面零改)。零项目内依赖(仅 `node:`)。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { readdirSync, readFileSync } from "node:fs"
|
|
10
|
-
|
|
11
|
-
/** 常量单源(§14.3.2):每段消息数 / sidecar 目录后缀。 */
|
|
12
|
-
export const RECORD_SEG_MESSAGES = 100
|
|
13
|
-
export const RECORD_DIR_SUFFIX = ".d"
|
|
14
|
-
|
|
15
|
-
/** 观测计数(测试缝——AC-RS9「恢复只读末段」以段读次数断言)。 */
|
|
16
|
-
export const _storeStats = { segmentReads: 0, segmentWrites: 0, appendedEntries: 0, catchUpRetries: 0 }
|
|
17
|
-
|
|
18
|
-
const LEGACY_TRANSIENT_PREFIXES = [
|
|
19
|
-
"[System reminder: working directory snapshot:",
|
|
20
|
-
"[Relevant memories from previous sessions",
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
/** legacy transient 判定(旧进程写进人读线的机器注入——落盘/展示均跳过)。 */
|
|
24
|
-
export function isLegacyTransient(m) {
|
|
25
|
-
return (
|
|
26
|
-
m?.role === "user" &&
|
|
27
|
-
typeof m.content === "string" &&
|
|
28
|
-
LEGACY_TRANSIENT_PREFIXES.some((p) => m.content.startsWith(p))
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Slim the HUMAN line for storage(自 session.mjs 迁出——§14.5):机器线(contextHistory)
|
|
33
|
-
* 逐字节保持。Copy-on-write ONLY(两线经 pushReal 共享对象引用):tool args → 300 字符头
|
|
34
|
-
* 截断 / tool content → 500 / 多模态 user → 保留 text、丢 image_url。 */
|
|
35
|
-
export function slimForDisplay(m) {
|
|
36
|
-
if (m && Array.isArray(m.content)) {
|
|
37
|
-
const textParts = m.content.filter((p) => p?.type !== "image_url")
|
|
38
|
-
if (textParts.length === m.content.length) return m
|
|
39
|
-
return { ...m, content: textParts }
|
|
40
|
-
}
|
|
41
|
-
if (m && m.role === "assistant" && Array.isArray(m.tool_calls)) {
|
|
42
|
-
let changed = false
|
|
43
|
-
const tool_calls = m.tool_calls.map((tc) => {
|
|
44
|
-
const args = tc.function?.arguments
|
|
45
|
-
if (typeof args === "string" && args.length > 300) {
|
|
46
|
-
changed = true
|
|
47
|
-
return { ...tc, function: { ...tc.function, arguments: args.slice(0, 300) + "…" } }
|
|
48
|
-
}
|
|
49
|
-
return tc
|
|
50
|
-
})
|
|
51
|
-
return changed ? { ...m, tool_calls } : m
|
|
52
|
-
}
|
|
53
|
-
if (m && m.role === "tool" && typeof m.content === "string" && m.content.length > 500) {
|
|
54
|
-
return { ...m, content: m.content.slice(0, 500) + "\n… (truncated for storage)" }
|
|
55
|
-
}
|
|
56
|
-
return m
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 记录条目过滤(与 saveSession 同源):transient 机器注入与 legacy-transient 前缀不进记录。 */
|
|
60
|
-
export function shouldAppend(m) {
|
|
61
|
-
return !!m && typeof m === "object" && !m.transient && !isLegacyTransient(m)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** 真实 user 消息(turnCount/firstMessage 口径——同 session-slots.extractSlotMeta)。 */
|
|
65
|
-
export function isRealUserMsg(m) {
|
|
66
|
-
return m?.role === "user" && typeof m.content === "string" && !m.content.startsWith("[System reminder:")
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** sidecar 目录路径(`{slot 文件路径}.d`——同目录同前缀)。 */
|
|
70
|
-
export function recordDirOf(slotFile) { return slotFile + RECORD_DIR_SUFFIX }
|
|
71
|
-
|
|
72
|
-
/** 段文件名(六位零填充)。 */
|
|
73
|
-
export function segName(n) { return `seg-${String(n).padStart(6, "0")}.jsonl` }
|
|
74
|
-
|
|
75
|
-
/** 段号升序表(readdir——不读内容)。 */
|
|
76
|
-
export function listSegments(dir) {
|
|
77
|
-
let names
|
|
78
|
-
try { names = readdirSync(dir) } catch { return [] }
|
|
79
|
-
const out = []
|
|
80
|
-
for (const n of names) {
|
|
81
|
-
const m = /^seg-(\d+)\.jsonl$/.exec(n)
|
|
82
|
-
if (m) out.push(Number(m[1]))
|
|
83
|
-
}
|
|
84
|
-
return out.sort((a, b) => a - b)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** 读段(原始行——去尾空行);读失败按空段容忍(读路径尽力面)。 */
|
|
88
|
-
export function readSegmentLines(path) {
|
|
89
|
-
_storeStats.segmentReads++
|
|
90
|
-
let text
|
|
91
|
-
try { text = readFileSync(path, "utf8") } catch { return [] }
|
|
92
|
-
const lines = text.split("\n")
|
|
93
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop()
|
|
94
|
-
return lines
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** 逐行容错解析:半行/损坏行 → undefined(调用方跳过——不抛、不自动修复)。 */
|
|
98
|
-
export function tryParse(line) {
|
|
99
|
-
try { return JSON.parse(line) } catch { return undefined }
|
|
100
|
-
}
|