thincoder 0.12.59 → 0.12.61
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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- 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 +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- 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/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- 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 +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/session-slots.mjs
CHANGED
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createHash } from "node:crypto"
|
|
12
|
-
import { mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, existsSync
|
|
13
|
-
import { join, dirname } from "node:path"
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, existsSync } from "node:fs"
|
|
13
|
+
import { join, dirname, basename } from "node:path"
|
|
14
14
|
import { execSync } from "node:child_process"
|
|
15
15
|
import { configDir } from "./config.mjs"
|
|
16
16
|
import { migrateHashLength } from "./session-migrate.mjs"
|
|
17
|
+
// §10 D-2(2026-09-05):resumeSlot 决策归本文件(slot/claim 层);data 层读(loadSlotFile/
|
|
18
|
+
// legacy 过滤)属 session.mjs(500 行内不迁移)——静态环仅此一处:函数声明实例化期已初始化、
|
|
19
|
+
// 只函数体内运行时使用(环安全);VS Code 端 session-slots ↔ session-io 同构镜像。
|
|
20
|
+
import { loadSlotFile, isLegacyTransient } from "./session.mjs"
|
|
17
21
|
|
|
18
22
|
let currentSessionId = null
|
|
19
23
|
|
|
@@ -36,16 +40,57 @@ function cwdHash(cwd) {
|
|
|
36
40
|
return createHash("sha1").update(normalizeCwd(cwd)).digest("hex")
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
/** sessions 根目录隔离缝(2026-09-08 DESIGN-TOKEN-SETTLEMENT 测试——VSC 端
|
|
44
|
+
* _setSessionsDirForTest 同构):默认 null 走真实 configDir;测试指向临时目录
|
|
45
|
+
* (beforeEach 设、afterEach 复位)。migrateHashLength 的 legacy 迁移仍查真实
|
|
46
|
+
* 目录(temp 哈希无 legacy 文件——no-op 无害)。 */
|
|
47
|
+
let sessionsDirOverride = null
|
|
48
|
+
export function _setSessionsDirForTest(dir) { sessionsDirOverride = dir }
|
|
49
|
+
export function _resetSessionsDirForTest() { sessionsDirOverride = null }
|
|
50
|
+
function sessionsBase() { return sessionsDirOverride ?? join(configDir, "sessions") }
|
|
51
|
+
|
|
39
52
|
/** Derive base session path from cwd hash. Migrates legacy short-hash files on first access. */
|
|
40
53
|
export function sessionPath(cwd) {
|
|
41
54
|
const hash = cwdHash(cwd)
|
|
42
55
|
migrateHashLength(cwd, hash)
|
|
43
|
-
return join(
|
|
56
|
+
return join(sessionsBase(), `${hash}.json`)
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
export function slotPath(cwd, n) { return sessionPath(cwd) + "." + n }
|
|
47
60
|
export function manifestPath(cwd) { return sessionPath(cwd) + ".manifest" }
|
|
48
61
|
|
|
62
|
+
// ========== end marker(SESSION.md §10 端分离恢复——本端"最后使用槽位"记录)==========
|
|
63
|
+
|
|
64
|
+
/** 端常量:本端记录文件后缀——CLI 写 .cli、VS Code 写 .vscode,互不触碰
|
|
65
|
+
* (NF1:本端记录是本端单写者文件——不新增跨端共享可变字段)。 */
|
|
66
|
+
export const END = "cli"
|
|
67
|
+
|
|
68
|
+
/** 本端记录路径:{manifest}.{END}(manifest 旁的独立小文件,非内嵌字段——NF1)。 */
|
|
69
|
+
export function endMarkerPath(cwd) { return `${manifestPath(cwd)}.${END}` }
|
|
70
|
+
|
|
71
|
+
/** 读本端记录:返回 { slot: <number|null>, updatedAt } 或 null。三态语义(D-1):
|
|
72
|
+
* 文件缺失 = 从未记录(升级/首用迁移窗口——可能触发一次性继承);
|
|
73
|
+
* JSON 解析失败/结构非法(损坏)= 按"缺失"降级——不 rename 不 unlink(幂等、不误伤,
|
|
74
|
+
* T-M13);`slot: null` = 显式置空(删过本端记录槽——绝不触发继承,T-M4)。 */
|
|
75
|
+
export function readEndMarker(cwd) {
|
|
76
|
+
try {
|
|
77
|
+
const p = endMarkerPath(cwd)
|
|
78
|
+
if (!existsSync(p)) return null
|
|
79
|
+
const m = JSON.parse(readFileSync(p, "utf8"))
|
|
80
|
+
if (m && m.slot === null) return { slot: null, updatedAt: m.updatedAt ?? null }
|
|
81
|
+
if (m && Number.isInteger(m.slot) && m.slot >= 1) return { slot: m.slot, updatedAt: m.updatedAt ?? null }
|
|
82
|
+
return null
|
|
83
|
+
} catch { return null }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 写本端记录(原子 .tmp+rename,复用 writeSessionFile):slot = 目标槽号或 null(显式置空)。
|
|
87
|
+
* 失败容忍(NF2):写失败按无记录路径降级——不抛错,不影响会话数据(T-M14)。 */
|
|
88
|
+
export function writeEndMarker(cwd, slot) {
|
|
89
|
+
try {
|
|
90
|
+
writeSessionFile(endMarkerPath(cwd), { slot, updatedAt: Date.now() })
|
|
91
|
+
} catch { /* NF2:失败容忍 */ }
|
|
92
|
+
}
|
|
93
|
+
|
|
49
94
|
/** Path to the active slot's file */
|
|
50
95
|
export function activePath(cwd) {
|
|
51
96
|
return slotPath(cwd, activeSlot(cwd))
|
|
@@ -90,9 +135,11 @@ function extractSlotMeta(history, activeProvider, updatedAt, title = "") {
|
|
|
90
135
|
}
|
|
91
136
|
}
|
|
92
137
|
|
|
93
|
-
/** Extract preview summary from session data for manifest storage (with current timestamp)
|
|
138
|
+
/** Extract preview summary from session data for manifest storage (with current timestamp)
|
|
139
|
+
* MODEL-MERGE-SESSION:摘要升级带 activeModel——listSlots 合成显 "p:m"(双端同规则)。 */
|
|
94
140
|
export function slotDigest(data) {
|
|
95
141
|
const meta = extractSlotMeta(data.history ?? [], data.activeProvider, data.updatedAt, data.title ?? "")
|
|
142
|
+
if (data.activeModel) meta.activeModel = data.activeModel
|
|
96
143
|
return { ts: Date.now(), ...meta }
|
|
97
144
|
}
|
|
98
145
|
|
|
@@ -143,27 +190,14 @@ export function saveManifest(cwd, m, deletions = null, opts = {}) {
|
|
|
143
190
|
writeSessionFile(manifestPath(cwd), m)
|
|
144
191
|
}
|
|
145
192
|
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
|
|
153
|
-
* see which slots are taken and avoid them.
|
|
154
|
-
*/
|
|
155
|
-
function ensureActive(cwd, m) {
|
|
193
|
+
/** 死主条目清理(2026-08-31 会诊 F4 + 2026-09-01 会诊 deepseek/kimi 🔴 语义原样抽取——
|
|
194
|
+
* ensureActive/resumeSlot 共用):删除 owner 进程已死的 slotSessions 条目。死主判定必须跑
|
|
195
|
+
* isProcessAlive——不能以"文件缺失"短路(活进程在"认领 → 首次保存"窗口文件暂缺,误删会
|
|
196
|
+
* 致双进程同槽)。返回 deletions 计算函数(调用时按 m 当前状态过滤刚重新认领的槽——防删
|
|
197
|
+
* 掉自己的新属主),无清理返回 null。删除须经 saveManifest 的 deletions 显式落盘——条目级
|
|
198
|
+
* 合并会把磁盘死条目从 fresh 复活回写(N1 + 会诊 🔴)。 */
|
|
199
|
+
function cleanDeadOwners(m) {
|
|
156
200
|
const mySessionId = getSessionId()
|
|
157
|
-
if (!m.slotSessions) m.slotSessions = {}
|
|
158
|
-
|
|
159
|
-
// 2026-08-31 会诊 F4:顺手清理死主条目——owner 进程已死的 slot 标记不再占用
|
|
160
|
-
// (原实现死项永不清理,空闲 slot 越来越少 → 新号滥发;且每次 save 对每 slot 跑
|
|
161
|
-
// tasklist,延迟随 slot 数增长)。
|
|
162
|
-
// advisor round2 #10:不能加 "文件缺失即删" 的短路——活进程在"认领 → 首次保存"窗口
|
|
163
|
-
// (新项目首跑:启动认领 slot、回合末才落盘)文件暂缺,误删其条目会被另一进程当
|
|
164
|
-
// 空闲认领 → 双进程永久写同一槽(F2 轮转互旋)。死主判定必须跑 tasklist;
|
|
165
|
-
// ensureActive 因 F1 粘性每次进程只跑几次,全量 tasklist 成本可接受。
|
|
166
|
-
let cleaned = false
|
|
167
201
|
const deadSlots = []
|
|
168
202
|
for (const [slot, owner] of Object.entries(m.slotSessions)) {
|
|
169
203
|
if (owner && owner !== mySessionId) {
|
|
@@ -171,24 +205,33 @@ function ensureActive(cwd, m) {
|
|
|
171
205
|
if (!pid || !isProcessAlive(pid)) {
|
|
172
206
|
delete m.slotSessions[slot]
|
|
173
207
|
deadSlots.push(slot)
|
|
174
|
-
cleaned = true
|
|
175
208
|
}
|
|
176
209
|
}
|
|
177
210
|
}
|
|
211
|
+
return deadSlots.length === 0 ? null : () => ({ slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId) })
|
|
212
|
+
}
|
|
178
213
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Claim a slot for this process and set it as active. Idempotent.
|
|
216
|
+
* Preference order:
|
|
217
|
+
* 1. The current active slot, if it is unowned / ours / its owner is dead — reuse it.
|
|
218
|
+
* 2. Any slot that is unowned or owned by a dead process (reclaim). → allocateFresh
|
|
219
|
+
* 3. A brand-new slot when all are owned by live processes. → allocateFresh
|
|
220
|
+
* The owner is recorded in m.slotSessions so other processes (CLI ↔ VS Code) can
|
|
221
|
+
* see which slots are taken and avoid them.
|
|
222
|
+
*/
|
|
223
|
+
function ensureActive(cwd, m) {
|
|
224
|
+
const mySessionId = getSessionId()
|
|
225
|
+
if (!m.slotSessions) m.slotSessions = {}
|
|
226
|
+
// 顺手清理死主条目(见 cleanDeadOwners)。ensureActive 因 F1 粘性每次进程只跑几次,
|
|
227
|
+
// 全量 tasklist 成本可接受;清理结果必须落盘——早退 + 分支 1/2/3 全部传 deletions。
|
|
228
|
+
const deadParam = cleanDeadOwners(m)
|
|
183
229
|
|
|
184
230
|
// Already own the active slot — nothing to do.
|
|
185
231
|
// 2026-08-31 advisor round2 🔵:清理结果此时落盘(否则死项清理只在内存生效,早退
|
|
186
232
|
// 路径永不持久化——死条目一直滞留到其他路径保存才消失)。
|
|
187
|
-
// advisor round3 N1 + 2026-09-01 会诊 deepseek/kimi 🔴:必须传 deletions——saveManifest
|
|
188
|
-
// 的条目级合并({...fresh, ...m})会把磁盘上仍存在的死条目从 fresh 复活回写,仅传 m
|
|
189
|
-
// 等于没删。N1 当时只修了早退路径,分支 1/2/3 漏了(认领主路径上死项清理是死代码)。
|
|
190
233
|
if (m.active && m.slotSessions[m.active] === mySessionId) {
|
|
191
|
-
if (
|
|
234
|
+
if (deadParam) saveManifest(cwd, m, deadParam())
|
|
192
235
|
return
|
|
193
236
|
}
|
|
194
237
|
|
|
@@ -201,20 +244,40 @@ function ensureActive(cwd, m) {
|
|
|
201
244
|
// 1. Prefer the current active slot if we can take it (preserves "resume where you left off").
|
|
202
245
|
if (m.active && m.slots[m.active] && isFree(m.active)) {
|
|
203
246
|
m.slotSessions[m.active] = mySessionId
|
|
204
|
-
saveManifest(cwd, m, deadParam(), { setActive: true })
|
|
247
|
+
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
205
248
|
return
|
|
206
249
|
}
|
|
207
250
|
|
|
251
|
+
// 2/3. 分支 2/3 已抽取为 allocateFresh(2026-09-05 §10 D-2——resumeSlot 全新分配复用)。
|
|
252
|
+
allocateFresh(cwd, m, deadParam)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Allocate a slot with ensureActive 分支 2/3 语义(2026-09-05 §10 D-2 抽取——resumeSlot
|
|
257
|
+
* 步骤③"其余一切 → 全新分配"复用;与 newSession 选号语义对齐):
|
|
258
|
+
* 2. Reclaim a manifest slot whose FILE does not exist (never held a session);
|
|
259
|
+
* 3. A brand-new slot when none is free / all numbers are taken.
|
|
260
|
+
* 记录所有权 + active 指针(setActive)并落盘。返回分配的槽号。
|
|
261
|
+
*/
|
|
262
|
+
export function allocateFresh(cwd, m, deadParam = null) {
|
|
263
|
+
const mySessionId = getSessionId()
|
|
264
|
+
if (!m.slotSessions) m.slotSessions = {}
|
|
265
|
+
const isFree = (slot) => {
|
|
266
|
+
const owner = m.slotSessions[slot]
|
|
267
|
+
if (!owner || owner === mySessionId) return true
|
|
268
|
+
return !isProcessAlive(parseInt(owner.split("-")[0]))
|
|
269
|
+
}
|
|
270
|
+
|
|
208
271
|
// 2. Reclaim a slot whose FILE does not exist (never held a session). 2026-08-31 会诊 F4:
|
|
209
272
|
// 原实现认领"编号最小的空闲 slot"——死主的旧 slot 文件仍在,新进程会 resume 进
|
|
210
273
|
// 陌生会话("会话乱了"实锤)且退出时覆盖它。只有文件缺失的空 slot 才允许回收。
|
|
211
|
-
const allSlots = Object.keys(m.slots).filter(n => /^\d+$/.test(n)).map(Number).sort((a, b) => a - b)
|
|
274
|
+
const allSlots = Object.keys(m.slots).filter((n) => /^\d+$/.test(n)).map(Number).sort((a, b) => a - b)
|
|
212
275
|
for (const slot of allSlots) {
|
|
213
276
|
if (isFree(slot) && !existsSync(slotPath(cwd, slot))) {
|
|
214
277
|
m.active = slot
|
|
215
278
|
m.slotSessions[slot] = mySessionId
|
|
216
|
-
saveManifest(cwd, m, deadParam(), { setActive: true })
|
|
217
|
-
return
|
|
279
|
+
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
280
|
+
return slot
|
|
218
281
|
}
|
|
219
282
|
}
|
|
220
283
|
|
|
@@ -234,7 +297,20 @@ function ensureActive(cwd, m) {
|
|
|
234
297
|
while (liveClaimed(newSlot) || existsSync(slotPath(cwd, newSlot))) newSlot++
|
|
235
298
|
m.active = newSlot
|
|
236
299
|
m.slotSessions[newSlot] = mySessionId
|
|
237
|
-
saveManifest(cwd, m, deadParam(), { setActive: true })
|
|
300
|
+
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
301
|
+
return newSlot
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 认领指定槽为本进程所有并置为 active(2026-09-05 §10 D-2 resumeSlot 认领路径——调用方
|
|
305
|
+
* 已按"属主 空/死/本进程"判据校验可用性;幂等)。manifest active 保留为共享指针(D-6:
|
|
306
|
+
* 旧版端/ACP 恢复依据 + 无记录端一次性继承源 + 列表回退高亮——不再作本端恢复第一依据)。
|
|
307
|
+
* deadParam 在写入所有权之后求值(防 deletions 删掉本调用刚认领的槽——ensureActive
|
|
308
|
+
* deadParam 同型过滤)。 */
|
|
309
|
+
export function claimSlot(cwd, slot, m = loadManifest(cwd), deadParam = null) {
|
|
310
|
+
m.slotSessions ??= {}
|
|
311
|
+
m.slotSessions[slot] = getSessionId()
|
|
312
|
+
m.active = slot // 认领即翻共享指针(setActive 写 m.active——不更新则落快照旧值)
|
|
313
|
+
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
238
314
|
}
|
|
239
315
|
|
|
240
316
|
/**
|
|
@@ -282,6 +358,7 @@ function loadSlotMeta(cwd, slot, v) {
|
|
|
282
358
|
const data = JSON.parse(readFileSync(p, "utf8"))
|
|
283
359
|
const history = data.history ?? []
|
|
284
360
|
const meta = extractSlotMeta(history, data.activeProvider, data.updatedAt ?? ts, data.title ?? "")
|
|
361
|
+
if (data.activeModel) meta.activeModel = data.activeModel
|
|
285
362
|
return { ts, ...meta }
|
|
286
363
|
} catch {
|
|
287
364
|
return { ts }
|
|
@@ -307,7 +384,9 @@ export function listSlots(cwd) {
|
|
|
307
384
|
messageCount: meta.messageCount ?? 0,
|
|
308
385
|
turnCount: meta.turnCount ?? 0,
|
|
309
386
|
firstMessage: meta.firstMessage ?? "",
|
|
310
|
-
|
|
387
|
+
// MODEL-MERGE-SESSION 摘要 "p:m":activeProvider 保持裸渠道名——列表消费面显复合
|
|
388
|
+
// (旧摘要无 activeModel → 回退裸渠道名——新老兼容;cmd-session/VSC sessions 行免改)
|
|
389
|
+
activeProvider: meta.activeProvider ? (meta.activeModel ? `${meta.activeProvider}:${meta.activeModel}` : meta.activeProvider) : "",
|
|
311
390
|
updatedAt: meta.updatedAt ?? meta.ts,
|
|
312
391
|
updatedDate: new Date(meta.updatedAt ?? meta.ts).toLocaleString(),
|
|
313
392
|
title: meta.title ?? "",
|
|
@@ -330,32 +409,81 @@ export function deleteSlot(cwd, slot) {
|
|
|
330
409
|
// setActive: true —— 显式表达"删到 active 时 active 置空"的意图(saveManifest 默认
|
|
331
410
|
// 保留 fresh.active,2026-09-01 会诊三家 🟡)
|
|
332
411
|
saveManifest(cwd, m, { slots: [n], slotSessions: [n] }, { setActive: true })
|
|
412
|
+
// 2026-09-05 §10 D-4/F4:删到本端记录槽 → 记录显式置空(文件保留 + slot:null——下次全新起步,不复活——T-M4/T-M8)
|
|
413
|
+
if (readEndMarker(cwd)?.slot === n) writeEndMarker(cwd, null)
|
|
333
414
|
return true
|
|
334
415
|
}
|
|
335
416
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (!existsSync(
|
|
344
|
-
|
|
417
|
+
// renameSlot 已拆至 session-rename.mjs(2026-09-06 §12.2.5 契约改使本文件超 500 行硬限,
|
|
418
|
+
// §12.3 授权拆分);session.mjs re-export 保持调用点不变。
|
|
419
|
+
|
|
420
|
+
// ========== resumeSlot(SESSION.md §10 D-2——端分离恢复决策)==========
|
|
421
|
+
|
|
422
|
+
/** 本端记录槽可用判据(D-2):slot ∈ m.slots + 槽文件在盘 + 属主 空/死/本进程。 */
|
|
423
|
+
function usableSlot(cwd, m, slot) {
|
|
424
|
+
if (!m.slots[slot] || !existsSync(slotPath(cwd, slot))) return false
|
|
425
|
+
const owner = m.slotSessions?.[slot]
|
|
426
|
+
if (!owner || owner === getSessionId()) return true
|
|
427
|
+
const pid = parseInt(owner.split("-")[0])
|
|
428
|
+
return !pid || !isProcessAlive(pid)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** legacy 单文件兜底(v1/v2 单会话 {hash}.json——迁移前残留;仅 data 层——2026-09-05 §10
|
|
432
|
+
* D-2:恢复数据兜底不改变 claim 落点,T-M10)。纪律与 loadSlotFile 一致:cwd 不匹配(别人的文件)
|
|
433
|
+
* 直接 null 不改名;坏结构改名 .unreadable 保留(version>2 不动);解析失败 .corrupted 保留。 */
|
|
434
|
+
function loadLegacyFile(cwd) {
|
|
435
|
+
const legacy = sessionPath(cwd)
|
|
345
436
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
437
|
+
if (existsSync(legacy)) {
|
|
438
|
+
const data = JSON.parse(readFileSync(legacy, "utf8"))
|
|
439
|
+
// cwd 不匹配是别人的文件——与 loadSlotFile 一致直接 return null 不改名
|
|
440
|
+
// ("别人的文件不动"原则,2026-08-31 advisor round2 🟡)
|
|
441
|
+
if (data.cwd && data.cwd.toLowerCase() !== cwd.toLowerCase()) return null
|
|
442
|
+
if ((data?.version === 1 || data?.version === 2) && Array.isArray(data.history)) {
|
|
443
|
+
data.history = data.history.filter((m) => !isLegacyTransient(m))
|
|
444
|
+
return data
|
|
445
|
+
}
|
|
446
|
+
// 结构不匹配:保留现场(version>2 的新版文件不动)
|
|
447
|
+
if (!(typeof data?.version === "number" && data.version > 2)) {
|
|
448
|
+
try { renameSync(legacy, `${legacy}.unreadable`) } catch {}
|
|
449
|
+
console.error(`[session] legacy file ${legacy}: invalid structure — preserved as ${basename(legacy)}.unreadable`)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (e) {
|
|
453
|
+
console.error(`[session] failed to load legacy ${legacy}: ${e.message}`)
|
|
454
|
+
try { renameSync(legacy, `${legacy}.corrupted`) } catch {}
|
|
349
455
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
456
|
+
return null
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* 恢复决策(SESSION.md §10 D-2)——TUI 启动的本端恢复入口(VS Code 面板同构镜像)。
|
|
461
|
+
* 返回 { slot, data }(data 可为 null——全新起步或读槽失败)。判据:
|
|
462
|
+
* ① 本端记录可用(slot ≠ null 且 ∈ m.slots 且槽文件在盘 且属主 空/死/本进程)→ claimSlot;
|
|
463
|
+
* ② 记录缺失(从未记录 = 升级/首用迁移窗口):②a active 属主 = 本进程(同进程重入——
|
|
464
|
+
* ensureActive 早退语义镜像:认领后尚未写 slots 条目/文件的窗口)→ 直接沿用;
|
|
465
|
+
* ②b 否则一次性继承 manifest.active(同判据;活属主绝不继承——全新槽起步,T-M3);
|
|
466
|
+
* ③ 其余一切(slot:null 显式置空 / 槽被删 / 属主为活外人 / 继承失败)→ allocateFresh。
|
|
467
|
+
* 每次落点都写本端记录;claim 后读槽失败(.corrupted/.unreadable——loadSlotFile 既有改名
|
|
468
|
+
* 保全语义)→ 保持已 claim 槽 + data:null——不改 marker——下次保存原地重建(T-M15)。
|
|
469
|
+
*/
|
|
470
|
+
export function resumeSlot(cwd) {
|
|
355
471
|
const m = loadManifest(cwd)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
472
|
+
m.slotSessions ??= {}
|
|
473
|
+
// 与 ensureActive 同型的死主清理(认领路径持久化——F5a 纪律:仅传 m 等于没删)
|
|
474
|
+
const deadParam = cleanDeadOwners(m)
|
|
475
|
+
const rec = readEndMarker(cwd) // null = 缺失/损坏;{slot:null|N} = 文件在(D-1)
|
|
476
|
+
let slot = null
|
|
477
|
+
if (rec?.slot != null && usableSlot(cwd, m, rec.slot)) slot = rec.slot // ① 本端记录可用
|
|
478
|
+
else if (rec === null) {
|
|
479
|
+
if (m.active && m.slotSessions?.[m.active] === getSessionId()) slot = m.active // ②a 同进程重入
|
|
480
|
+
else if (m.active && usableSlot(cwd, m, m.active)) slot = m.active // ②b 一次性继承
|
|
359
481
|
}
|
|
360
|
-
|
|
482
|
+
if (slot !== null) claimSlot(cwd, slot, m, deadParam)
|
|
483
|
+
else slot = allocateFresh(cwd, m, deadParam) // ③ 全新分配(slot:null 绝不继承——T-M4)
|
|
484
|
+
// data 层:读已认领槽(loadSlotFile 自 session.mjs——环 import 见文件头);读失败/槽文件不在 → legacy 单文件兜底(仅 data)
|
|
485
|
+
let data = loadSlotFile(cwd, slot)
|
|
486
|
+
if (!data) data = loadLegacyFile(cwd)
|
|
487
|
+
writeEndMarker(cwd, slot)
|
|
488
|
+
return { slot, data }
|
|
361
489
|
}
|