@thincoder/core 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
package/session-gc.mjs
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-gc.mjs — 会话目录残留 GC + 冷 cwd 报告/删除(SESSION.md §6.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(§6.12 D-V1)+残留;删除前重校验冷态——
|
|
15
|
+
* TOCTOU 防护 T12)。
|
|
16
|
+
*
|
|
17
|
+
* 拆分理由(§6.12 review #1):session-slots.mjs 已在 500 行硬限零余量,GC 是独立决策
|
|
18
|
+
* ("何时清何种残留"),独立成模块;原语(sessionPath / ownerPid)自 session-slots
|
|
19
|
+
* import,启动钩子(session.mjs resumeSlot 包装)只一行调用。
|
|
20
|
+
*
|
|
21
|
+
* F-MI7(2026-09-18):判活 = **核探测束**(`process-probe.mjs` `probeOwnersAsync`)+ `ownerState`
|
|
22
|
+
* 三态查表——本档**零逐 pid 探测**;**未知 ⇒ 保留 / 不判冷**(D-MI10 同向);入口面异步(启动钩子
|
|
23
|
+
* 延后执行,不阻塞启动路径)。死主数据文件 / 活跃槽判定与认领面同源判据。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readdirSync, readFileSync, statSync, unlinkSync, existsSync, rmSync } from "node:fs"
|
|
27
|
+
import { join, dirname, basename } from "node:path"
|
|
28
|
+
import { configDir } from "./config.mjs"
|
|
29
|
+
import { sessionPath, ownerPid } from "./session-slots.mjs"
|
|
30
|
+
import { probeOwnersAsync, ownerState } from "./process-probe.mjs"
|
|
31
|
+
|
|
32
|
+
/** 保留期(§6.12):损坏现场(.corrupted/.unreadable/.manifest.corrupted)与并发轮转
|
|
33
|
+
* 备份(.bak-*)30 天;孤儿 .tmp 7 天(崩溃现场恢复窗口)。 */
|
|
34
|
+
export const RESIDUE_RETENTION_MS = 30 * 24 * 3600 * 1000
|
|
35
|
+
export const ORPHAN_TMP_RETENTION_MS = 7 * 24 * 3600 * 1000
|
|
36
|
+
/** 冷 cwd 阈值(§6.12):manifest mtime 距今 > 90 天(保守——正常开发会频繁触碰)。 */
|
|
37
|
+
export const COLD_CWD_RETENTION_MS = 90 * 24 * 3600 * 1000
|
|
38
|
+
|
|
39
|
+
function sessionsDir() { return join(configDir, "sessions") }
|
|
40
|
+
|
|
41
|
+
/** 残留分类(§6.12 后缀表):name 须以 `${prefix}.` 开头(prefix = `${hash}.json`)。
|
|
42
|
+
* 返回 { retention, slot, tmp } 或 null(主文件/end marker/他前缀——不动)。
|
|
43
|
+
* slot = 关联槽号(`.N.` 段),无则 null(如 .manifest.corrupted)。 */
|
|
44
|
+
function classifyResidue(name, prefix) {
|
|
45
|
+
if (!name.startsWith(prefix + ".")) return null
|
|
46
|
+
const slotMatch = name.slice(prefix.length).match(/^\.(\d+)\./)
|
|
47
|
+
const slot = slotMatch ? Number(slotMatch[1]) : null
|
|
48
|
+
if (name.endsWith(".corrupted") || name.endsWith(".unreadable")) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false }
|
|
49
|
+
if (/\.bak-\d+$/.test(name)) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false }
|
|
50
|
+
if (name.endsWith(".tmp")) return { retention: ORPHAN_TMP_RETENTION_MS, slot, tmp: true }
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 活跃槽集合(§6.12 T3/T12 操作定义):.json.N 主文件存在 且 manifest slotSessions[N]
|
|
55
|
+
* 属主进程存活。manifest 缺失/损坏 → 空集(无活跃槽——不阻碍残留清理)。
|
|
56
|
+
* 探测 = **入口一次异步束**(零逐 pid exec);三态消费:活 ⇒ 活跃;死 ⇒ 非活跃;
|
|
57
|
+
* **未知(探测失败 / 缺行)⇒ 计为活跃**(现场保留——D-MI10 同向)。 */
|
|
58
|
+
async function liveSlots(dir, prefix, probeFn) {
|
|
59
|
+
const active = new Set()
|
|
60
|
+
let m
|
|
61
|
+
try { m = JSON.parse(readFileSync(join(dir, `${prefix}.manifest`), "utf8")) } catch { return active }
|
|
62
|
+
const entries = []
|
|
63
|
+
for (const [n, owner] of Object.entries(m.slotSessions ?? {})) {
|
|
64
|
+
if (!/^\d+$/.test(n) || !owner) continue
|
|
65
|
+
if (!existsSync(join(dir, `${prefix}.${n}`))) continue
|
|
66
|
+
const pid = ownerPid(owner)
|
|
67
|
+
if (pid) entries.push([Number(n), pid])
|
|
68
|
+
}
|
|
69
|
+
if (!entries.length) return active
|
|
70
|
+
const bundle = await probeFn([...new Set(entries.map(([, pid]) => pid))])
|
|
71
|
+
for (const [n, pid] of entries) {
|
|
72
|
+
if (ownerState(pid, bundle) !== "dead") active.add(n)
|
|
73
|
+
}
|
|
74
|
+
return active
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 残留 GC(§6.12——单 cwd 前缀,不跨 cwd 扫描):返回 { candidates, deleted }
|
|
79
|
+
* (dryRun 时 candidates 照列、deleted 为空——只列不删,N2 可预览)。
|
|
80
|
+
* 扫描先按后缀预过滤(N4——只 stat 残留候选),再对候选做活跃槽/孤儿/保留期判定;
|
|
81
|
+
* 无残留候选 ⇒ **零探测早退**(不发起探测束)。probeFn = 测试注入缝(缺省核异步束)。
|
|
82
|
+
*/
|
|
83
|
+
export async function gcResidue({ dir = sessionsDir(), prefix, now = Date.now(), dryRun = false, probeFn = probeOwnersAsync } = {}) {
|
|
84
|
+
const result = { candidates: [], deleted: [] }
|
|
85
|
+
let entries
|
|
86
|
+
try { entries = readdirSync(dir) } catch { return result }
|
|
87
|
+
const candidates = []
|
|
88
|
+
for (const name of entries) {
|
|
89
|
+
const c = classifyResidue(name, prefix)
|
|
90
|
+
if (c) candidates.push({ name, ...c })
|
|
91
|
+
}
|
|
92
|
+
if (!candidates.length) return result
|
|
93
|
+
const active = await liveSlots(dir, prefix, probeFn)
|
|
94
|
+
for (const c of candidates) {
|
|
95
|
+
if (c.slot !== null && active.has(c.slot)) continue // N1:活跃槽现场一律保留(T3)
|
|
96
|
+
const p = join(dir, c.name)
|
|
97
|
+
if (c.tmp && existsSync(p.slice(0, -".tmp".length))) continue // 非孤儿 .tmp(主文件在——写中/回退候选)
|
|
98
|
+
let st
|
|
99
|
+
try { st = statSync(p) } catch { continue }
|
|
100
|
+
if (st.mtimeMs >= now - c.retention) continue // 边界:older-than 才删,等于保留期保留(§6.12)
|
|
101
|
+
result.candidates.push(c.name)
|
|
102
|
+
if (!dryRun) {
|
|
103
|
+
try { unlinkSync(p); result.deleted.push(c.name) } catch { /* 占用/竞态——跳过 */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return result
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const scheduledPrefixes = new Set()
|
|
110
|
+
|
|
111
|
+
/** 启动钩子(§6.12 review #8——N4):GC 延后到进程启动完成后空闲执行(setImmediate),
|
|
112
|
+
* 不阻塞启动路径;每进程每前缀一次(Set 去重——resumeSlot 可多次进入)。 */
|
|
113
|
+
export function scheduleSessionGC(cwd) {
|
|
114
|
+
let base
|
|
115
|
+
try { base = sessionPath(cwd) } catch { return }
|
|
116
|
+
if (scheduledPrefixes.has(base)) return
|
|
117
|
+
scheduledPrefixes.add(base)
|
|
118
|
+
setImmediate(() => {
|
|
119
|
+
gcResidue({ dir: dirname(base), prefix: basename(base) }).catch(() => { /* 清理失败静默——不影响主流程 */ })
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 冷 cwd 枚举(§6.12 步骤 1/2——跨 cwd 报告面,仅手动命令调用):候选 =
|
|
125
|
+
* manifest mtime 距今 > 90 天 且 无活跃数据文件(.json.N 主文件不存在或全部属死主进程)。
|
|
126
|
+
* 返回 [{ hash, prefix, manifestMtime, dataFiles, files }](files = 整前缀全部文件——
|
|
127
|
+
* 含裸 v1 `{hash}.json`,§6.12 D-V1——dataFiles 仅 .json.N,v1 不参与冷态判定)。
|
|
128
|
+
* 探测 = **全部候选前缀一次异步束**(两遍:先筛候选并收 pid,再一次判活 + 三态);
|
|
129
|
+
* **未知 ⇒ 非冷(保留)**——探测失败不得判冷(D-MI10 同向)。
|
|
130
|
+
*/
|
|
131
|
+
export async function listColdCwds({ dir = sessionsDir(), now = Date.now(), probeFn = probeOwnersAsync } = {}) {
|
|
132
|
+
let entries
|
|
133
|
+
try { entries = readdirSync(dir) } catch { return [] }
|
|
134
|
+
const cands = []
|
|
135
|
+
for (const name of entries) {
|
|
136
|
+
if (!name.endsWith(".json.manifest")) continue // end marker(.manifest.cli/.vscode)与 .manifest.corrupted 天然排除
|
|
137
|
+
const prefix = name.slice(0, -".manifest".length) // `${hash}.json`
|
|
138
|
+
let st
|
|
139
|
+
try { st = statSync(join(dir, name)) } catch { continue }
|
|
140
|
+
if (st.mtimeMs >= now - COLD_CWD_RETENTION_MS) continue // 近期活跃(<90 天阈值——T10)
|
|
141
|
+
let m
|
|
142
|
+
try { m = JSON.parse(readFileSync(join(dir, name), "utf8")) } catch { continue } // 损坏 manifest 不判冷(.corrupted 归残留 GC)
|
|
143
|
+
const dataFiles = entries.filter((e) => e.startsWith(prefix + ".") && /^\d+$/.test(e.slice(prefix.length + 1)))
|
|
144
|
+
const owners = dataFiles.map((e) => {
|
|
145
|
+
const slotName = e.slice(prefix.length + 1)
|
|
146
|
+
const pid = ownerPid(m.slotSessions?.[slotName] ?? "")
|
|
147
|
+
return pid || 0
|
|
148
|
+
})
|
|
149
|
+
cands.push({ prefix, manifestMtime: st.mtimeMs, dataFilesLen: dataFiles.length, owners })
|
|
150
|
+
}
|
|
151
|
+
if (!cands.length) return []
|
|
152
|
+
const pids = [...new Set(cands.flatMap((c) => c.owners).filter(Boolean))]
|
|
153
|
+
const bundle = await probeFn(pids) // 空清单 ⇒ 探测面零 exec 早退
|
|
154
|
+
const cold = []
|
|
155
|
+
for (const c of cands) {
|
|
156
|
+
// 死主数据文件判定:三态——活 / 未知 ⇒ 非冷(保守保留);无非自身属主 ⇒ 无活跃数据文件
|
|
157
|
+
const hasLive = c.owners.some((pid) => pid && ownerState(pid, bundle) !== "dead")
|
|
158
|
+
if (hasLive) continue // 有活跃数据文件 → 非冷(T12);探测失败 ⇒ 未知 ⇒ 同向保留
|
|
159
|
+
cold.push({
|
|
160
|
+
hash: c.prefix.slice(0, -".json".length),
|
|
161
|
+
prefix: c.prefix,
|
|
162
|
+
manifestMtime: c.manifestMtime,
|
|
163
|
+
dataFiles: c.dataFilesLen,
|
|
164
|
+
files: entries.filter((e) => e.startsWith(c.prefix + ".") || e === c.prefix), // 整前缀清空含裸 v1 {hash}.json(§6.12 D-V1)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
return cold
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 删除指定冷 cwd 的整个前缀(§6.12 步骤 3——manifest+end marker+.json.N 死主数据+
|
|
172
|
+
* 裸 v1 {hash}.json(§6.12 D-V1)+残留;只删 manifest 留数据文件会制造孤儿数据,整前缀清空才真正释放)。
|
|
173
|
+
* 删除前重跑冷 cwd 判定(TOCTOU 防护,T12——期间变活跃则拒绝)。
|
|
174
|
+
*/
|
|
175
|
+
export async function deleteColdCwd(hash, { dir = sessionsDir(), now = Date.now(), probeFn = probeOwnersAsync } = {}) {
|
|
176
|
+
const target = (await listColdCwds({ dir, now, probeFn })).find((c) => c.hash === hash)
|
|
177
|
+
if (!target) return { ok: false, reason: "not-cold", deleted: [] }
|
|
178
|
+
const deleted = []
|
|
179
|
+
for (const name of target.files) {
|
|
180
|
+
const p = join(dir, name)
|
|
181
|
+
try {
|
|
182
|
+
// §6.14(TUI-OOM-ROOTCAUSE):目录项(记录存储 sidecar `{prefix}.N.d`)递归删——
|
|
183
|
+
// 现 unlinkSync 对目录静默跳过(旧实现漏删 sidecar);数据文件维持 unlink。
|
|
184
|
+
if (statSync(p).isDirectory()) rmSync(p, { recursive: true, force: true })
|
|
185
|
+
else unlinkSync(p)
|
|
186
|
+
deleted.push(name)
|
|
187
|
+
} catch { /* 占用/竞态——跳过 */ }
|
|
188
|
+
}
|
|
189
|
+
return { ok: true, deleted }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* ④ 端差段 · 手动执行面(§6.12)——仅命令行壳提供(另一形态无 shell 子命令通道);
|
|
194
|
+
* 本段**核内零消费方**(结构机检③):核内保存实现(取一侧),命令接线属壳侧(S2)。
|
|
195
|
+
*
|
|
196
|
+
* `thincoder session gc` 子命令分发(§6.12 手动面——F2 执行入口仅命令行壳,
|
|
197
|
+
* 另一形态无 shell 子命令通道,review #7):
|
|
198
|
+
* --dry-run 报告当前 cwd 残留候选 + 跨 cwd 冷候选(只列不删,N2 预览)
|
|
199
|
+
* --confirm <hash> 删除指定冷 cwd 整前缀(警告 + 文件清单 + TOCTOU 重校验)
|
|
200
|
+
* --confirm --all 逐冷 cwd 同型警告删除
|
|
201
|
+
* 返回进程退出码(0/1)。dir/prefix/now/out/err/probeFn 为测试注入缝(默认生产行为)。
|
|
202
|
+
*/
|
|
203
|
+
export async function runSessionGc(args, { dir = sessionsDir(), prefix = null, cwd = process.cwd(), now = Date.now(), out = console.log, err = console.error, probeFn = probeOwnersAsync } = {}) {
|
|
204
|
+
const dryRun = args.includes("--dry-run")
|
|
205
|
+
const confirmIdx = args.indexOf("--confirm")
|
|
206
|
+
const hasConfirm = confirmIdx >= 0
|
|
207
|
+
const confirmTarget = hasConfirm ? args[confirmIdx + 1] : null
|
|
208
|
+
// --dry-run 与 --confirm 互斥(review 🔵#4——同给时不得静默忽略 confirm;无值 --confirm 同为用法错误)
|
|
209
|
+
if (args[0] !== "gc" || (!dryRun && !hasConfirm) || (dryRun && hasConfirm) || (hasConfirm && !confirmTarget)) {
|
|
210
|
+
err("Usage: thincoder session gc --dry-run | --confirm <hash> | --confirm --all")
|
|
211
|
+
return 1
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (dryRun) {
|
|
215
|
+
out("Session GC dry-run — no files will be deleted.")
|
|
216
|
+
// 当前 cwd 残留(§6.12 删除面——自动 GC 的预览)
|
|
217
|
+
let p = prefix
|
|
218
|
+
if (!p) { try { p = basename(sessionPath(cwd)) } catch { p = null } }
|
|
219
|
+
const residue = p ? await gcResidue({ dir, prefix: p, now, dryRun: true, probeFn }) : { candidates: [] }
|
|
220
|
+
out(`Residue candidates for current project (${p ?? "unknown"}): ${residue.candidates.length}`)
|
|
221
|
+
for (const name of residue.candidates) out(` ${name}`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const cold = await listColdCwds({ dir, now, probeFn })
|
|
225
|
+
if (dryRun) {
|
|
226
|
+
out(`Cold project candidates (manifest idle > 90 days, no live data files): ${cold.length}`)
|
|
227
|
+
for (const c of cold) {
|
|
228
|
+
out(` ${c.hash} manifest mtime ${new Date(c.manifestMtime).toISOString()} data files ${c.dataFiles} total files ${c.files.length}`)
|
|
229
|
+
}
|
|
230
|
+
if (cold.length) out('Run "thincoder session gc --confirm <hash>" (or --confirm --all) to permanently delete a cold project prefix.')
|
|
231
|
+
return 0
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --confirm:删除前警告(N2 可逆——文件清单 + 永久删除提示);deleteColdCwd 内部重校验冷态(TOCTOU)
|
|
235
|
+
const targets = confirmTarget === "--all" ? cold : cold.filter((c) => c.hash === confirmTarget)
|
|
236
|
+
if (!targets.length) {
|
|
237
|
+
err(`Refused: ${confirmTarget} is not a cold project (active, recent, or unknown) — nothing deleted.`)
|
|
238
|
+
return 1
|
|
239
|
+
}
|
|
240
|
+
for (const t of targets) {
|
|
241
|
+
out(`WARNING: 此操作永久删除该 cwd 的全部会话历史 (hash ${t.hash}, ${t.files.length} files):`)
|
|
242
|
+
for (const name of t.files) out(` ${name}`)
|
|
243
|
+
const r = await deleteColdCwd(t.hash, { dir, now, probeFn })
|
|
244
|
+
if (!r.ok) { err(`Refused: ${t.hash} is no longer cold (became active) — skipped.`) ; continue }
|
|
245
|
+
out(`Deleted ${r.deleted.length} files for ${t.hash}.`)
|
|
246
|
+
}
|
|
247
|
+
return 0
|
|
248
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-lifecycle.mjs — 会话生命周期面(init-block 批 · F-MI7 拆分——`session.mjs` 500 行
|
|
3
|
+
* 硬限;各函数语义原样外提,SESSION.md §6.10 D-2 / §6.12 / §6.11 注记随迁):
|
|
4
|
+
* 本端恢复入口包装(resumeSlot——残留 GC 钩子)/ 槽数据应用(applySession)/ 新建槽
|
|
5
|
+
* (newSession)/ 运行态清空(resetSessionState)/ 槽切换(switchToSlot)/ 占用查询
|
|
6
|
+
* (slotOccupancy)/ v1 老文件清扫(stripTruncatedToolArgs)。
|
|
7
|
+
*
|
|
8
|
+
* 静态环(与 session-slots.mjs ↔ session.mjs 同形):本档引 `session.mjs` 的
|
|
9
|
+
* `loadSlotFile`(data 层读留 session.mjs——2026-09-05 §6.10 D-2 拆分点),session.mjs re-export
|
|
10
|
+
* 本档函数保持既有 import 面 —— 函数声明在实例化期已初始化、仅函数体内运行时使用(环安全;
|
|
11
|
+
* 同 session-slots.mjs 头注)。
|
|
12
|
+
*
|
|
13
|
+
* 探测纪律(F-MI7 · SESSION.md §6.2):本档**零同步 exec 扫描**——`newSession` 走入口一次
|
|
14
|
+
* **异步束**(`probeOwnersAsync` + `ownerState` 三态查表);`slotOccupancy` = **有界同步例外**
|
|
15
|
+
* (`probeOwnersSync`,每 exec ≤ SYNC_PROBE_MS;探测失败 ⇒ `{ occupied: true, unknown: true }`
|
|
16
|
+
* 保守占位——未知不认领,D-MI10)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync } from "node:fs"
|
|
20
|
+
import { resolveCompactThreshold } from "./config.mjs"
|
|
21
|
+
import {
|
|
22
|
+
slotPath, loadManifest, saveManifest, slotDigest, writeSessionFile, getSessionId,
|
|
23
|
+
writeEndMarker, ownerPid, ownerPids,
|
|
24
|
+
resumeSlot as slotsResumeSlot,
|
|
25
|
+
} from "./session-slots.mjs"
|
|
26
|
+
import { probeOwnersSync, probeOwnersAsync, ownerState } from "./process-probe.mjs"
|
|
27
|
+
// 环 import:见头注(loadSlotFile 属 session.mjs 的 data 层读面)。
|
|
28
|
+
import { loadSlotFile } from "./session.mjs"
|
|
29
|
+
import { mergeAdjacentAssistantEchoes } from "./context.mjs"
|
|
30
|
+
import { scheduleSessionGC } from "./session-gc.mjs"
|
|
31
|
+
import { restoreEngTokens } from "./token-ttl.mjs"
|
|
32
|
+
import { bindRecordStore, unbindRecordStore, RECORD_WINDOW_MESSAGES } from "./session-store.mjs"
|
|
33
|
+
|
|
34
|
+
/** 恢复决策包装(SESSION.md §6.12 启动钩子,2026-09-06):本端恢复入口触发一次残留 GC——
|
|
35
|
+
* scheduleSessionGC 内部 setImmediate 空闲执行 + 每进程每前缀去重,不阻塞启动路径(N4)。
|
|
36
|
+
* **async**(F-MI7——整链异步:探测不阻塞事件循环;调用面必须 await)。 */
|
|
37
|
+
export async function resumeSlot(cwd) {
|
|
38
|
+
scheduleSessionGC(cwd)
|
|
39
|
+
return slotsResumeSlot(cwd)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** slimForDisplay 截断的 arguments 以 U+2026(…)结尾——不是合法 JSON 的完整值。
|
|
43
|
+
* v1 老文件回退播种机器线时置为 {}(合法空参数),防止半截 \\uXXXX 毒化发送载荷(会诊 F6)。 */
|
|
44
|
+
function stripTruncatedToolArgs(m) {
|
|
45
|
+
if (m?.role !== "assistant" || !Array.isArray(m.tool_calls)) return m
|
|
46
|
+
let changed = false
|
|
47
|
+
const tool_calls = m.tool_calls.map((tc) => {
|
|
48
|
+
const args = tc?.function?.arguments
|
|
49
|
+
if (typeof args === "string" && args.endsWith("…")) {
|
|
50
|
+
changed = true
|
|
51
|
+
return { ...tc, function: { ...tc.function, arguments: "{}" } }
|
|
52
|
+
}
|
|
53
|
+
return tc
|
|
54
|
+
})
|
|
55
|
+
return changed ? { ...m, tool_calls } : m
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Apply loaded session data onto an agent object; returns true if the runtime composite
|
|
59
|
+
* was switched. 2026-08-31 会诊 F1:清空 _slot/_slotMtime 缓存——切换后下次保存重新认领。
|
|
60
|
+
*
|
|
61
|
+
* MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
|
|
62
|
+
* "三支"误读):
|
|
63
|
+
* ① 槽 provider 存在 → provider/model 按槽值设(双字段恒非空——模型取 data.activeModel,
|
|
64
|
+
* legacy 槽 null/空串回该渠道默认模型 `providers[].model`(单值——2026-09-10 MODEL-SELECTION
|
|
65
|
+
* v2;老 models[] 首候选形态已退场))+ 重算
|
|
66
|
+
* compactThreshold(auto 时——按恢复后模型)——不看 config(defaultModel 只是新会话起点);
|
|
67
|
+
* ② 槽 provider 没了 → D-S3 保留——静默保持现状(config 有效则有效——两方都无效由调用侧
|
|
68
|
+
* 复验 validateProvider 弹重选)。
|
|
69
|
+
* 删旧支:"activeModel==null 清 stale override 回渠道默认"——前提 = 渠道默认字段——已随
|
|
70
|
+
* 三旧层删除消失——双字段恒非空故不可能触发(评审 #7)。 */
|
|
71
|
+
export function applySession(agent, data, opts = {}) {
|
|
72
|
+
// 机读线语义(历史注释保留):data.history = FULL 未压缩记录(人读线);data.contextHistory =
|
|
73
|
+
// 压缩后机器线——恢复各从其源(保留压缩收益——从完整 history 重建会塞回中间过程——实测
|
|
74
|
+
// prompt 膨胀 283%)。v1 老文件(无 contextHistory)回退播种,剥离被 slimForDisplay 截断的
|
|
75
|
+
// tool_calls.arguments(… 结尾 → 置 {}——截断可劈断 \\uXXXX 产生 400 毒载荷)。length > 0 才
|
|
76
|
+
// 当机读线(contextHistory: [] 是"无机读线"而非空机器线——空机器线静默丢全部上下文)。
|
|
77
|
+
agent.config ??= {} // ACP test mocks may omit config; be defensive like the ??= below
|
|
78
|
+
const full = Array.isArray(data.history) ? data.history : []
|
|
79
|
+
// SESSION.md §6.11(2026-09-08——F2/F3):恢复事件按当前 data 重定——载入历史非空 = 会话
|
|
80
|
+
// 恢复 → 下个 depth-0 回合 env-state resumed:yes 一次(pushEnvStateReminder 读即清——每次恢复
|
|
81
|
+
// 一次)。/new 与空历史槽切换不武装(显式清 false)——无恢复事件恒 no(F3 伪触发消除——不再靠
|
|
82
|
+
// _sessionStart != null 推断;已武装 agent 切到空历史槽也不残留上次恢复事件——评审 round2 🟡)。
|
|
83
|
+
// _processRestartPending 不在此清——由启动路径(bin/thincoder.mjs)设、prepareRun 发句清
|
|
84
|
+
// (N6——双信号独立:切槽不报进程重启)。
|
|
85
|
+
// 跨端异名互指(结构债批 5 N7——双端同语义各自独立实现、命名不统一是刻意——SESSION.md
|
|
86
|
+
// §6.11):VSC 端同机制载体异名 = agent._resumedPending(src/agent/setup.mjs hydrateRun——
|
|
87
|
+
// restore:true 工厂 + fullHistory 非空时武装——每槽恢复一次)+ 模块级 restartDetectionDone
|
|
88
|
+
// 一次性闸(src/agent/setup-reminders.mjs——process restarted 句的进程级信号)。
|
|
89
|
+
agent._envResumed = full.length > 0
|
|
90
|
+
const ch = data.contextHistory
|
|
91
|
+
const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
|
|
92
|
+
// D-CC19 恢复面回声归并(CONTEXT-COMPACTION §6.10 #8):已落盘机读线原样装回会复活
|
|
93
|
+
// D-CC18 病态形态(无 reasoning_content 的 assistant 紧邻 assistant ⇒ 首请求 400)——
|
|
94
|
+
// machine 定线后、装线前扫描归并(干净输入返回同一引用——零拷贝零回归)。
|
|
95
|
+
const machineMerged = mergeAdjacentAssistantEchoes(machine)
|
|
96
|
+
// 绑定态(opts.slot 在场):人读线 = 记录存储尾窗(下方绑定块填充)——不复制全量 JSON 数组
|
|
97
|
+
agent._fullHistory = opts.slot != null ? [] : [...full]
|
|
98
|
+
agent.history = [...machineMerged]
|
|
99
|
+
agent.title = data.title ?? ""
|
|
100
|
+
agent.tasks = data.tasks ?? []
|
|
101
|
+
agent.planMode = data.planMode ?? false
|
|
102
|
+
agent.autoApprove = data.autoApprove ?? false
|
|
103
|
+
agent.goal = data.goal ?? null
|
|
104
|
+
agent._pendingReminders = data.pendingReminders ?? []
|
|
105
|
+
agent._sessionStart = data.sessionStart ?? null
|
|
106
|
+
// R16 token 字段恢复(过期过滤——见 token-ttl.mjs restoreEngTokens)
|
|
107
|
+
restoreEngTokens(agent, data)
|
|
108
|
+
// engineering is session-level (2026-08-29): the slot value is the CLI session's authority
|
|
109
|
+
// — config.json is only the initial default / cross-end mirror. A legacy slot without the
|
|
110
|
+
// field keeps whatever config.json seeded (unchanged behavior).
|
|
111
|
+
if (data.engineering !== undefined) {
|
|
112
|
+
agent.config.agent ??= {}
|
|
113
|
+
agent.config.agent.engineering = data.engineering === true
|
|
114
|
+
}
|
|
115
|
+
if (data.advisor) {
|
|
116
|
+
agent.config.advisor = { ...data.advisor }
|
|
117
|
+
}
|
|
118
|
+
// Reset stall/compaction state on session switch
|
|
119
|
+
agent._compressFailures = 0
|
|
120
|
+
agent._verifyRetries = 0
|
|
121
|
+
agent._verifyPassed = false
|
|
122
|
+
agent._slot = null // 粘性缓存清空——切换后重新认领(F1b)
|
|
123
|
+
agent._slotMtime = null
|
|
124
|
+
// ── 记录存储绑定(TUI-OOM-ROOTCAUSE · SESSION.md §6.14 绑定与对账 / 追加时点)──
|
|
125
|
+
// opts.slot 在场(启动恢复 / 非占用的 /session 切换 / ACP 钉槽)→ 绑定(身份核验 + 对账 +
|
|
126
|
+
// 窗口)——人读线 = store.tail(200);未传(模式 F:被他人活进程占用的槽 / 测试 / ACP fork)
|
|
127
|
+
// → 解绑——人读线维持全量数组(D-SE26 模式 F 零回归)。
|
|
128
|
+
if (opts.slot != null) {
|
|
129
|
+
const store = bindRecordStore(agent, {
|
|
130
|
+
slotFile: slotPath(agent.cwd, opts.slot),
|
|
131
|
+
identity: data.sessionStart ?? null,
|
|
132
|
+
baseHistory: full,
|
|
133
|
+
})
|
|
134
|
+
agent._fullHistory = store.tail(RECORD_WINDOW_MESSAGES)
|
|
135
|
+
} else {
|
|
136
|
+
unbindRecordStore(agent)
|
|
137
|
+
}
|
|
138
|
+
// ── MODEL-MERGE-SESSION 恢复(F-4——两支)──
|
|
139
|
+
const slotProvider = data.activeProvider ? agent.providers?.find((pr) => pr.name === data.activeProvider) : null
|
|
140
|
+
if (slotProvider) {
|
|
141
|
+
// ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空——legacy 槽 activeModel
|
|
142
|
+
// null/空串 = 无 override——回该渠道默认模型(`providers[].model` 单值——M3)。
|
|
143
|
+
// F-2d(MODEL-400-FIX):`||` 非 `??`——空串也兜(activeModel="" 的槽会让 `??` 不落链 →
|
|
144
|
+
// 空槽 model 恒有值,provider.model 键不缺失)
|
|
145
|
+
const prevName = agent.activeProvider
|
|
146
|
+
const prevModel = agent.activeModel ?? null
|
|
147
|
+
const slotModel = data.activeModel || slotProvider.model
|
|
148
|
+
const switched = prevName !== slotProvider.name || prevModel !== slotModel
|
|
149
|
+
agent.activeProvider = slotProvider.name
|
|
150
|
+
agent.activeModel = slotModel
|
|
151
|
+
agent.provider = { ...slotProvider }
|
|
152
|
+
if (slotModel) agent.provider.model = slotModel
|
|
153
|
+
// 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched 分支——收拢本处)
|
|
154
|
+
if (switched && agent.config?.agent?.compactThresholdAuto && agent.provider.model) {
|
|
155
|
+
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
156
|
+
}
|
|
157
|
+
return switched
|
|
158
|
+
}
|
|
159
|
+
// ② 槽 provider 没了 → D-S3 保留——静默保持现状(不报错不纠正——config 有效则静默用
|
|
160
|
+
// config/defaultModel 运行时;两方都无效由调用侧复验 validateProvider 弹重选)
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a new session slot: allocate a free slot number,
|
|
166
|
+
* write an empty session, and mark it as the active slot.
|
|
167
|
+
* No limit on the number of sessions.
|
|
168
|
+
* **async**(F-MI7:入口一次异步束——探测不阻塞事件循环;调用面必须 await)。
|
|
169
|
+
*/
|
|
170
|
+
export async function newSession(cwd) {
|
|
171
|
+
const m = loadManifest(cwd)
|
|
172
|
+
|
|
173
|
+
// 2026-09-01 advisor 🔵:先清理死主条目(与 ensureActive 分支2 语义一致)——否则
|
|
174
|
+
// "死主 + 文件缺失"的空槽(m.slots 有条目、无文件、属主已死)永不复用,槽号持续
|
|
175
|
+
// 增长。死主且无文件 = 该会话从未落盘(进程死了没保存),条目可安全删除回收。
|
|
176
|
+
// 2026-09-01 会诊 deepseek/glm 🟡:清理必须经 deletions 显式落盘——saveManifest 条目级
|
|
177
|
+
// 合并会把磁盘死条目从 fresh 复活回写,仅传 m 等于没删(VS Code newSlot 已修,CLI 对称)。
|
|
178
|
+
// F-MI7:判活来自**入口一次异步束**(零自有 exec / 零逐 pid exec);判据查表——
|
|
179
|
+
// unknown(探测失败 / 缺行)⇒ 保守保留条目(D-MI10)。
|
|
180
|
+
const mySessionId = getSessionId()
|
|
181
|
+
const bundle = await probeOwnersAsync(ownerPids(m, mySessionId))
|
|
182
|
+
const deadSlots = []
|
|
183
|
+
for (const [s, owner] of Object.entries(m.slotSessions ?? {})) {
|
|
184
|
+
if (!owner || owner === mySessionId) continue
|
|
185
|
+
if (ownerState(ownerPid(owner), bundle) !== "dead") continue
|
|
186
|
+
delete m.slotSessions[s]
|
|
187
|
+
if (!existsSync(slotPath(cwd, Number(s)))) delete m.slots[s]
|
|
188
|
+
deadSlots.push(s)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Find next available slot number — 2026-08-31 会诊 deepseek 🟡:不能只看 manifest
|
|
192
|
+
// 条目(丢失更新可能让条目消失而文件仍在)——复用该号会直接覆写真实会话
|
|
193
|
+
// (F2 防护不覆盖 newSession 的空数据直写)。
|
|
194
|
+
// 2026-08-31 advisor round2 🟡:同时跳过"已被另一活进程认领但尚未落盘"的号
|
|
195
|
+
// (slotSessions 有条目、slots 无条目、文件不存在)——否则双进程会认领同一号。
|
|
196
|
+
// F-MI7:活 / 未知属主 ⇒ 视为已占(跳号——保守),判据同查束。
|
|
197
|
+
const liveClaimed = (n) => {
|
|
198
|
+
const owner = m.slotSessions?.[n]
|
|
199
|
+
if (!owner || owner === mySessionId) return false
|
|
200
|
+
return ownerState(ownerPid(owner), bundle) !== "dead"
|
|
201
|
+
}
|
|
202
|
+
let slot = 1
|
|
203
|
+
while (m.slots[slot] || existsSync(slotPath(cwd, slot)) || liveClaimed(slot)) slot++
|
|
204
|
+
|
|
205
|
+
// Write empty session — 2026-09-01 advisor 🔵:补 contextHistory/planMode 字段与
|
|
206
|
+
// VS Code newSlot 对齐(SESSION.md §6.3 v2 格式双端一致;两端读侧均有兜底,功能等价)
|
|
207
|
+
const data = { version: 2, cwd, title: "", updatedAt: Date.now(), history: [], contextHistory: [], tasks: [], planMode: false, goal: null, autoApprove: false, advisor: null, pendingReminders: [], sessionStart: null }
|
|
208
|
+
writeSessionFile(slotPath(cwd, slot), data)
|
|
209
|
+
m.slots[slot] = slotDigest(data)
|
|
210
|
+
m.active = slot
|
|
211
|
+
// 2026-08-31 advisor round1 🟡:与 ensureActive 认领模型一致——立即记录新 slot 所有权,
|
|
212
|
+
// 否则 /new 后到首次保存之间并发方(VS Code/另一 CLI)会把新 active 槽当"空闲可恢复"
|
|
213
|
+
// 认领 → 双进程写同一槽(F2 轮转互旋)。
|
|
214
|
+
m.slotSessions ??= {}
|
|
215
|
+
m.slotSessions[slot] = mySessionId
|
|
216
|
+
// 2026-09-01 会诊三家 🟡:显式翻 active 的调用点传 setActive(saveManifest 默认保留
|
|
217
|
+
// 磁盘 fresh.active,避免把并发方刚翻的指针回滚)
|
|
218
|
+
// 2026-09-01 会诊 deepseek/glm 🟡:deletions 过滤掉本调用刚重新认领的槽(防删掉自己的
|
|
219
|
+
// 新属主)——与 ensureActive deadParam / VS Code newSlot 同型。
|
|
220
|
+
const deletions = deadSlots.length
|
|
221
|
+
? {
|
|
222
|
+
slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId),
|
|
223
|
+
slots: deadSlots.filter((s) => !m.slots[s]),
|
|
224
|
+
}
|
|
225
|
+
: null
|
|
226
|
+
saveManifest(cwd, m, deletions, { setActive: true })
|
|
227
|
+
// 2026-09-05 §6.10 D-4:/new 落点写本端记录(显式切换跟随——T-M8)
|
|
228
|
+
writeEndMarker(cwd, slot)
|
|
229
|
+
return slot
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 清空会话运行态(/new 用,2026-08-31 会诊 F3):_fullHistory/title/_sessionStart 等
|
|
233
|
+
* 全部会话级状态 + 一次性注入标志必须全清——否则新会话首次落盘把旧会话完整人类线 +
|
|
234
|
+
* 旧标题写进新槽(实锤 .19/.3 双副本);注入标志不清则 /new 后新会话永不注入
|
|
235
|
+
* OS/cwd reminder(2026-09-01 会诊 glm 🟡)。autoApprove 是用户偏好,跨会话保留(有意)。 */
|
|
236
|
+
export function resetSessionState(agent) {
|
|
237
|
+
agent._fullHistory = []
|
|
238
|
+
agent.history = []
|
|
239
|
+
agent.title = ""
|
|
240
|
+
agent.tasks = []
|
|
241
|
+
agent._sessionStart = null
|
|
242
|
+
// DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——不再维护
|
|
243
|
+
agent._engDesignTokens = new Map() // multi-design slots die with the session (2026-09-01 fix #2)
|
|
244
|
+
agent._compressFailures = 0
|
|
245
|
+
agent._verifyRetries = 0
|
|
246
|
+
agent._verifyPassed = undefined
|
|
247
|
+
agent._runStartHistoryLen = 0
|
|
248
|
+
agent._lastPromptTokens = null
|
|
249
|
+
agent._usageAtLen = null
|
|
250
|
+
agent.planMode = false
|
|
251
|
+
agent.goal = null
|
|
252
|
+
agent._pendingReminders = []
|
|
253
|
+
agent._slot = null
|
|
254
|
+
agent._slotMtime = null
|
|
255
|
+
agent._osReminderInjected = false
|
|
256
|
+
// §6.11(2026-09-08):一次性注入标志换名——_restartReminderInjected 推断退役(F3——
|
|
257
|
+
// _sessionStart != null 伪触发);/new 清零新标记——恢复武装/启动句标志不跨会话复活
|
|
258
|
+
agent._envResumed = false
|
|
259
|
+
agent._processRestartPending = false
|
|
260
|
+
agent._lastEngState = false
|
|
261
|
+
// /new 或切换:记录存储解绑(新槽由 cmd-new / 首保存补绑重新挂载——§6.14 绑定与对账)
|
|
262
|
+
unbindRecordStore(agent)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Switch the manifest active pointer to a slot. Returns the slot's session data
|
|
266
|
+
* (null if the slot doesn't exist / can't be read). 2026-08-31 会诊 deepseek 🔴:
|
|
267
|
+
* 原实现经 loadSession → activeSlot 有认领副作用——目标槽被活进程占用时 ensureActive
|
|
268
|
+
* 分支 3 会把 active 拨到新空槽并读回 null + 劫持对方指针。现改为 loadSlotFile 直接读
|
|
269
|
+
* (无认领副作用);目标槽空闲则一并认领、被另一活进程占用则不认领(slotOccupancy——
|
|
270
|
+
* 下次保存经 activeSlot 自然 fork 到新槽);只改 manifest 指针(setActive 意图)。
|
|
271
|
+
* 2026-09-01 会诊三家 🟡:saveManifest 条目级合并 + setActive(不把并发方刚翻的指针回滚)。 */
|
|
272
|
+
export function switchToSlot(cwd, slot) {
|
|
273
|
+
const m = loadManifest(cwd)
|
|
274
|
+
if (!m.slots[slot]) return null
|
|
275
|
+
const data = loadSlotFile(cwd, slot)
|
|
276
|
+
if (!data) return null
|
|
277
|
+
m.active = slot
|
|
278
|
+
const occ = slotOccupancy(cwd, slot)
|
|
279
|
+
if (!occ.occupied) {
|
|
280
|
+
m.slotSessions ??= {}
|
|
281
|
+
m.slotSessions[slot] = getSessionId()
|
|
282
|
+
}
|
|
283
|
+
saveManifest(cwd, m, null, { setActive: true })
|
|
284
|
+
// 2026-09-05 §6.10 D-4:/session N 跟随"最后查看的槽"(成功切换才写)
|
|
285
|
+
writeEndMarker(cwd, slot)
|
|
286
|
+
return data
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 目标槽是否被另一活进程占用(2026-09-01 会诊/advisor 🟡):排除本进程属主——
|
|
290
|
+
* /session 重选当前槽不误报;同进程双会话防护由 ACP sameProcessPinned 承担。
|
|
291
|
+
* F-MI7(**有界同步例外**——D-MI14 登记):零探测早退(无属主 / 本进程属主);冷路径单 pid
|
|
292
|
+
* **同步束**(每 exec ≤ SYNC_PROBE_MS);探测失败 ⇒ 未知 ⇒ `{ occupied: true, unknown: true }`
|
|
293
|
+
* (保守占位——未知不认领,D-MI10)。 */
|
|
294
|
+
export function slotOccupancy(cwd, slot) {
|
|
295
|
+
const m = loadManifest(cwd)
|
|
296
|
+
const owner = m.slotSessions?.[slot]
|
|
297
|
+
if (!owner) return { occupied: false }
|
|
298
|
+
if (owner === getSessionId()) return { occupied: false }
|
|
299
|
+
const pid = ownerPid(owner)
|
|
300
|
+
if (!pid) return { occupied: false }
|
|
301
|
+
const st = ownerState(pid, probeOwnersSync([pid]))
|
|
302
|
+
if (st === "alive") return { occupied: true, owner }
|
|
303
|
+
if (st === "unknown") return { occupied: true, unknown: true, owner }
|
|
304
|
+
return { occupied: false }
|
|
305
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
}
|