@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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-store.mjs — 人读线记录存储:磁盘为准 + 内存窗口(TUI-OOM-ROOTCAUSE 批)。
|
|
3
|
+
* 机制契约全文 = docs/core/design/SESSION.md §6.14(D2 单一权威源);本文件 = 实现。
|
|
4
|
+
* 形态:`{slot 文件路径}.d/` + `meta.json` + `seg-000001.jsonl`(行 = slimForDisplay 后的
|
|
5
|
+
* 消息 JSON——与槽 JSON `history` 元素逐字节同形;投影零转换,§6.14)。
|
|
6
|
+
* 依赖:零项目内依赖(仅 `node:`)——路径由 `slotFile` 传入;session.mjs / session-slots.mjs /
|
|
7
|
+
* session-guard.mjs 单向引本模块。窗口驱逐在 pushReal(context.mjs)、追加单点同 pushReal。
|
|
8
|
+
* 段 IO 原语与条目形态(`slimForDisplay` 等)拆住 `session-segments.mjs`(越 500 硬限的
|
|
9
|
+
* 职责拆分——本档 re-export 全部公开名,调用面零改)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync,
|
|
14
|
+
renameSync, rmSync, truncateSync, unlinkSync, writeFileSync, writeSync,
|
|
15
|
+
} from "node:fs"
|
|
16
|
+
import { basename, dirname, join } from "node:path"
|
|
17
|
+
import {
|
|
18
|
+
RECORD_SEG_MESSAGES, recordDirOf, segName, listSegments, readSegmentLines, tryParse,
|
|
19
|
+
isLegacyTransient, slimForDisplay, shouldAppend, isRealUserMsg, _storeStats,
|
|
20
|
+
} from "./session-segments.mjs"
|
|
21
|
+
|
|
22
|
+
// 公开名 re-export(调用面零改——§6.14 接口面)
|
|
23
|
+
export {
|
|
24
|
+
RECORD_SEG_MESSAGES, RECORD_DIR_SUFFIX, recordDirOf, isLegacyTransient, slimForDisplay, _storeStats,
|
|
25
|
+
} from "./session-segments.mjs"
|
|
26
|
+
|
|
27
|
+
/** 内存窗口(条数——与首屏 INITIAL_HISTORY_MESSAGES 同值——单一概念,§6.14)。 */
|
|
28
|
+
export const RECORD_WINDOW_MESSAGES = 200
|
|
29
|
+
|
|
30
|
+
const META_NAME = "meta.json"
|
|
31
|
+
|
|
32
|
+
class RecordStore {
|
|
33
|
+
constructor(slotFile) {
|
|
34
|
+
this.slotFile = slotFile
|
|
35
|
+
this.dir = recordDirOf(slotFile)
|
|
36
|
+
this.agent = null
|
|
37
|
+
this.identity = null // meta.identity(会话身份锚 = sessionStart)
|
|
38
|
+
this.degraded = false // 追加失败即停(§6.14 D-R4①)
|
|
39
|
+
this.memTotal = 0 // pushReal 计数(含未落盘条——缺口 = memTotal − total,D-R4②)
|
|
40
|
+
this._segCount = this._lastSegN = this._lastSegLines = 0 // 末段段号(= 段文件数)+ 行数
|
|
41
|
+
this._metaReady = false
|
|
42
|
+
this._lastSegCache = null // { n, lines }——bind 期复用(恢复只读末段:不重复读同一段)
|
|
43
|
+
this._partialSeg = 0 // 带未写完半行的段号(崩溃现场——首写前先补行终止符,防粘连)
|
|
44
|
+
this._counts = { turnCount: 0, firstMessage: "" }
|
|
45
|
+
this._sawRealUser = false
|
|
46
|
+
this._firstUser = undefined // undefined = 未扫;null = 无(首扫一次并缓存)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 绑定对账(§6.14):**先验身份、后比计数**。返回 this。 */
|
|
50
|
+
bind(agent, identity, baseHistory) {
|
|
51
|
+
this.agent = agent
|
|
52
|
+
const base = (Array.isArray(baseHistory) ? baseHistory : []).filter(shouldAppend)
|
|
53
|
+
const site = identity ?? null
|
|
54
|
+
let meta = this._readMeta()
|
|
55
|
+
this._scan()
|
|
56
|
+
const hasSidecar = meta !== null || this._segCount > 0
|
|
57
|
+
if (hasSidecar) {
|
|
58
|
+
this.identity = meta?.identity ?? null
|
|
59
|
+
const bothEmpty = !this.identity && !site
|
|
60
|
+
const same = !!(this.identity && site && this.identity === site)
|
|
61
|
+
if (!bothEmpty && !same) {
|
|
62
|
+
// 陈旧/孤儿 sidecar——不得采纳(槽号回收/轮转/改名/对端占用同槽的跨会话污染面):
|
|
63
|
+
// 改名 `.stale-<epochms>` 保留现场,按现场重建(§6.14 ②③)——身份回到现场身份
|
|
64
|
+
// (否则空 base 路径不重物化时,新建 sidecar 会把旧会话身份写进 meta——内审 #1)。
|
|
65
|
+
this._quarantine()
|
|
66
|
+
this.identity = site
|
|
67
|
+
meta = null
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
this.identity = site // 无 sidecar:创建时写入现场身份(可 null——待固化)
|
|
71
|
+
}
|
|
72
|
+
const storeTotal = this.total()
|
|
73
|
+
if (meta?.degraded === true || storeTotal < base.length) {
|
|
74
|
+
// ② 计数对账——以 JSON 为准重建(降级标记 / JSON 更长:VSC 追加、sidecar 缺失、首次迁移)
|
|
75
|
+
this.identity = site
|
|
76
|
+
this._materialize(base)
|
|
77
|
+
} else {
|
|
78
|
+
// 以段为准(崩溃后未保存消息可见)——计数 = JSON 全量 + JSON 之后的已落盘尾部
|
|
79
|
+
this._countOver(base)
|
|
80
|
+
if (storeTotal > base.length) this._countOver(this._range(base.length, storeTotal))
|
|
81
|
+
}
|
|
82
|
+
this.memTotal = this.total()
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_readMeta() {
|
|
87
|
+
try {
|
|
88
|
+
const p = join(this.dir, META_NAME)
|
|
89
|
+
if (!existsSync(p)) return null
|
|
90
|
+
const m = JSON.parse(readFileSync(p, "utf8"))
|
|
91
|
+
return m && typeof m === "object" ? m : null
|
|
92
|
+
} catch { return null }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_writeMeta() {
|
|
96
|
+
const meta = { v: 1, segSize: RECORD_SEG_MESSAGES, identity: this.identity ?? null }
|
|
97
|
+
if (this.degraded) meta.degraded = true
|
|
98
|
+
try {
|
|
99
|
+
mkdirSync(this.dir, { recursive: true })
|
|
100
|
+
const tmp = join(this.dir, META_NAME) + ".tmp"
|
|
101
|
+
writeFileSync(tmp, JSON.stringify(meta), "utf8")
|
|
102
|
+
renameSync(tmp, join(this.dir, META_NAME))
|
|
103
|
+
} catch {
|
|
104
|
+
try { writeFileSync(join(this.dir, META_NAME), JSON.stringify(meta), "utf8") } catch { /* 尽力面 */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** meta 同步(身份补写 / 降级置位时重写——小文件原子写)。 */
|
|
109
|
+
syncMeta() {
|
|
110
|
+
try { this._writeMeta(); this._metaReady = true } catch { /* 尽力面 */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 陈旧/孤儿现场保留:目录改名 `.stale-<epochms>`(与 .bak/.corrupted 同族)后按现场重建。 */
|
|
114
|
+
_quarantine() {
|
|
115
|
+
const dst = `${this.dir}.stale-${Date.now()}`
|
|
116
|
+
try {
|
|
117
|
+
renameSync(this.dir, dst)
|
|
118
|
+
console.error(`[session] stale record store preserved as ${basename(dst)} (identity ${this.identity ?? "null"} ≠ session)`)
|
|
119
|
+
} catch { /* 改名失败(占用)——仍按现场重建(materialize 覆盖) */ }
|
|
120
|
+
this._resetState()
|
|
121
|
+
this.identity = null // 隔离后不再持有旧身份(新 sidecar 待写现场身份——bind 内随即置回 site)
|
|
122
|
+
this.degraded = false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 定位态 + 计数态归零(改名/重建/扫描共用;counts=false 保留计数——扫描路径)。 */
|
|
126
|
+
_resetState(counts = true) {
|
|
127
|
+
this._segCount = this._lastSegN = this._lastSegLines = 0
|
|
128
|
+
this._metaReady = false
|
|
129
|
+
this._lastSegCache = null
|
|
130
|
+
this._partialSeg = 0
|
|
131
|
+
if (!counts) return
|
|
132
|
+
this._counts = { turnCount: 0, firstMessage: "" }
|
|
133
|
+
this._sawRealUser = false
|
|
134
|
+
this._firstUser = undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 目录扫描:段数 + 末段行数(半行 = 末段最后一行解析失败 → 视为未写完、忽略)。 */
|
|
138
|
+
_scan() {
|
|
139
|
+
const segs = listSegments(this.dir)
|
|
140
|
+
this._resetState(false)
|
|
141
|
+
if (segs.length === 0) return
|
|
142
|
+
this._segCount = this._lastSegN = segs[segs.length - 1]
|
|
143
|
+
const lines = readSegmentLines(join(this.dir, segName(this._lastSegN)))
|
|
144
|
+
const complete = lines.length > 0 && tryParse(lines[lines.length - 1]) === undefined ? lines.length - 1 : lines.length
|
|
145
|
+
this._lastSegLines = complete
|
|
146
|
+
// 末尾半行(崩溃现场):记段号——下一次追加先补行终止符(不自动修复内容,仅不粘连新记录)
|
|
147
|
+
this._partialSeg = lines.length > complete ? this._lastSegN : 0
|
|
148
|
+
this._lastSegCache = { n: this._lastSegN, lines: lines.slice(0, complete) }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 末段缓存取段(bind 期不重复读同一段)。 */
|
|
152
|
+
_segmentLines(n) {
|
|
153
|
+
if (this._lastSegCache && this._lastSegCache.n === n) return this._lastSegCache.lines
|
|
154
|
+
return readSegmentLines(join(this.dir, segName(n)))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_countOver(msgs) {
|
|
158
|
+
for (const m of msgs) this._countLine(m)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 以 JSON 为准重建整个 sidecar(rm 目录 → 重新物化——§6.14 规则 2)。 */
|
|
162
|
+
_materialize(base) {
|
|
163
|
+
try { rmSync(this.dir, { recursive: true, force: true }) } catch { /* 尽力面 */ }
|
|
164
|
+
this._resetState()
|
|
165
|
+
this.degraded = false
|
|
166
|
+
this.memTotal = 0
|
|
167
|
+
if (base.length === 0) return // 两者皆空 → 新目录懒创建(首 append 建目录/写 meta)
|
|
168
|
+
mkdirSync(this.dir, { recursive: true })
|
|
169
|
+
this._writeMeta()
|
|
170
|
+
this._metaReady = true
|
|
171
|
+
const buf = []
|
|
172
|
+
for (const m of base) {
|
|
173
|
+
this._countLine(m)
|
|
174
|
+
buf.push(JSON.stringify(slimForDisplay(m)))
|
|
175
|
+
if (buf.length >= RECORD_SEG_MESSAGES) this._flushSeg(buf)
|
|
176
|
+
}
|
|
177
|
+
this._flushSeg(buf)
|
|
178
|
+
this.memTotal = this.total()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 段落盘(物化路径——整段一次写出)。 */
|
|
182
|
+
_flushSeg(lines) {
|
|
183
|
+
if (lines.length === 0) return
|
|
184
|
+
const n = this._lastSegN + 1
|
|
185
|
+
writeFileSync(join(this.dir, segName(n)), lines.join("\n") + "\n", "utf8")
|
|
186
|
+
this._lastSegN = n
|
|
187
|
+
this._lastSegLines = lines.length
|
|
188
|
+
this._segCount = n
|
|
189
|
+
this._lastSegCache = null
|
|
190
|
+
_storeStats.segmentWrites++
|
|
191
|
+
lines.length = 0
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 已落盘条数:(段数−1) × segSize + 末段行数(末段空文件按 0 行计)。 */
|
|
195
|
+
total() {
|
|
196
|
+
if (this._segCount <= 0) return 0
|
|
197
|
+
return (this._segCount - 1) * RECORD_SEG_MESSAGES + this._lastSegLines
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 绝对区间 [lo, hi) 的连续切片(逐段读——只触碰覆盖区间段)。 */
|
|
201
|
+
_range(lo, hi) {
|
|
202
|
+
const out = []
|
|
203
|
+
const total = this.total()
|
|
204
|
+
if (hi <= lo || total <= 0) return out
|
|
205
|
+
const firstSeg = Math.floor(lo / RECORD_SEG_MESSAGES) + 1
|
|
206
|
+
const lastSeg = Math.floor((hi - 1) / RECORD_SEG_MESSAGES) + 1
|
|
207
|
+
for (let s = firstSeg; s <= lastSeg; s++) {
|
|
208
|
+
const lines = this._segmentLines(s)
|
|
209
|
+
const segBase = (s - 1) * RECORD_SEG_MESSAGES
|
|
210
|
+
for (let k = 0; k < lines.length; k++) {
|
|
211
|
+
const idx = segBase + k
|
|
212
|
+
if (idx < lo || idx >= hi || idx >= total) continue
|
|
213
|
+
const msg = tryParse(lines[k])
|
|
214
|
+
if (msg !== undefined) out.push(msg)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return out
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 尾部 n 条(恢复窗口:只读末段及其前置段)。 */
|
|
221
|
+
tail(n) {
|
|
222
|
+
const total = this.total()
|
|
223
|
+
return this._range(Math.max(0, total - n), total)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 区间取页 + ±1 页沿(§6.14——页沿上下文供渲染层跨页回合标签/工具结果配对)。 */
|
|
227
|
+
page(start, end, { margin = 1 } = {}) {
|
|
228
|
+
const total = this.total()
|
|
229
|
+
const lo = Math.max(0, start - margin)
|
|
230
|
+
const hi = Math.min(total, end + margin)
|
|
231
|
+
return { messages: this._range(lo, hi), base: lo }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 方向流式迭代(read_history 用——'newest' 自尾向前 / 'oldest' 自首向后)。 */
|
|
235
|
+
*iterate(direction = "newest") {
|
|
236
|
+
const total = this.total()
|
|
237
|
+
if (total <= 0) return
|
|
238
|
+
const segs = []
|
|
239
|
+
for (let s = 1; s <= this._segCount; s++) segs.push(s)
|
|
240
|
+
const order = direction === "oldest" ? segs : [...segs].reverse()
|
|
241
|
+
for (const s of order) {
|
|
242
|
+
const lines = this._segmentLines(s)
|
|
243
|
+
const segBase = (s - 1) * RECORD_SEG_MESSAGES
|
|
244
|
+
for (let k = 0; k < lines.length; k++) {
|
|
245
|
+
const idx = direction === "oldest" ? segBase + k : segBase + (lines.length - 1 - k)
|
|
246
|
+
if (idx >= total) continue // 半行(末段未写完)不计
|
|
247
|
+
const msg = tryParse(lines[idx - segBase])
|
|
248
|
+
if (msg !== undefined) yield msg
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 首条真实 user 消息(标题回退——首扫一次并缓存;生成器惰性:命中即停,不扫后续段)。 */
|
|
254
|
+
firstUserMessage() {
|
|
255
|
+
if (this._firstUser !== undefined) return this._firstUser
|
|
256
|
+
this._firstUser = null
|
|
257
|
+
for (const m of this.iterate("oldest")) {
|
|
258
|
+
if (isRealUserMsg(m)) { this._firstUser = m; break }
|
|
259
|
+
}
|
|
260
|
+
return this._firstUser
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 槽摘要计数(manifest digest——不物化 history):{total, userReal, firstMessage}。 */
|
|
264
|
+
counters() {
|
|
265
|
+
return { total: this.total(), userReal: this._counts.turnCount, firstMessage: this._counts.firstMessage }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 追加一条真实消息(§6.14 单点 = pushReal;同步;独立 try/catch——失败置 degraded + 停写)。 */
|
|
269
|
+
append(msg) {
|
|
270
|
+
if (!shouldAppend(msg)) return false
|
|
271
|
+
this.memTotal++
|
|
272
|
+
if (this.degraded) return false
|
|
273
|
+
try {
|
|
274
|
+
this._writeLine(msg)
|
|
275
|
+
return true
|
|
276
|
+
} catch (e) {
|
|
277
|
+
this.degraded = true
|
|
278
|
+
console.error(`[session] record store append failed (${e.message}) — writing stopped`)
|
|
279
|
+
return false
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 半行现场修复(写路径,代码评审 #1):截掉末尾未写完的半行——恢复「物理行序 = 逻辑
|
|
284
|
+
* 序」不变式(读侧按计数索引;半行占位会使后续记录的物理位置与计数错位)。半行对应
|
|
285
|
+
* 的消息从未被确认(崩溃中止的写入)——丢弃即读侧「视为未写完、忽略」的落盘面。 */
|
|
286
|
+
_dropPartialTail() {
|
|
287
|
+
if (!this._partialSeg || this._partialSeg !== this._lastSegN) return
|
|
288
|
+
this._partialSeg = 0
|
|
289
|
+
this._lastSegCache = null
|
|
290
|
+
try {
|
|
291
|
+
const p = join(this.dir, segName(this._lastSegN))
|
|
292
|
+
const buf = readFileSync(p)
|
|
293
|
+
truncateSync(p, buf.lastIndexOf(0x0a) + 1) // −1(无完整行)→ 截为 0
|
|
294
|
+
} catch { /* 尽力面:截断失败仍按读侧半行容错跳过 */ }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 单条落盘(成功路径——失败向上抛,由 append/catchUp 统一处置)。 */
|
|
298
|
+
_writeLine(msg) {
|
|
299
|
+
mkdirSync(this.dir, { recursive: true })
|
|
300
|
+
this._dropPartialTail() // 先清末尾半行(若在)——防新记录与半行错位/粘连
|
|
301
|
+
// 身份固化:创建时为空 → 现场身份补写(只在跃迁时重写 meta——逐条追加不重复写)
|
|
302
|
+
const hadIdentity = this.identity != null
|
|
303
|
+
if (!hadIdentity && this.agent?._sessionStart) this.identity = this.agent._sessionStart
|
|
304
|
+
if (!this._metaReady || (!hadIdentity && this.identity != null)) {
|
|
305
|
+
this._writeMeta()
|
|
306
|
+
this._metaReady = true
|
|
307
|
+
}
|
|
308
|
+
if (this._lastSegN === 0) {
|
|
309
|
+
this._lastSegN = 1
|
|
310
|
+
this._lastSegLines = 0
|
|
311
|
+
this._segCount = 1
|
|
312
|
+
} else if (this._lastSegLines >= RECORD_SEG_MESSAGES) {
|
|
313
|
+
this._lastSegN++ // 轮转:末段满 segSize → 下一次追加新建下一段
|
|
314
|
+
this._lastSegLines = 0
|
|
315
|
+
this._segCount = this._lastSegN
|
|
316
|
+
}
|
|
317
|
+
appendFileSync(join(this.dir, segName(this._lastSegN)), JSON.stringify(slimForDisplay(msg)) + "\n", "utf8")
|
|
318
|
+
_storeStats.appendedEntries++
|
|
319
|
+
this._lastSegLines++
|
|
320
|
+
this._lastSegCache = null
|
|
321
|
+
this._countLine(msg)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_countLine(m) {
|
|
325
|
+
if (!isRealUserMsg(m)) return
|
|
326
|
+
this._counts.turnCount++
|
|
327
|
+
if (!this._sawRealUser) {
|
|
328
|
+
this._sawRealUser = true
|
|
329
|
+
this._counts.firstMessage = String(m.content).slice(0, 80)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** D-R4② 追赶重试(保存时点):缺口 ⊆ 窗口 → 重试追加;窗口已滑过缺口 → 不补写(不写洞)。 */
|
|
334
|
+
catchUp() {
|
|
335
|
+
if (!this.degraded) return true
|
|
336
|
+
_storeStats.catchUpRetries++
|
|
337
|
+
const gap = this.memTotal - this.total()
|
|
338
|
+
if (gap <= 0) { this.degraded = false; return true }
|
|
339
|
+
const win = (this.agent?._fullHistory ?? []).filter(shouldAppend)
|
|
340
|
+
if (gap > win.length) return false // 缺口已滑出窗口——保持降级(不写洞)
|
|
341
|
+
const missing = win.slice(win.length - gap)
|
|
342
|
+
try {
|
|
343
|
+
for (const m of missing) this._writeLine(m)
|
|
344
|
+
} catch (e) {
|
|
345
|
+
console.error(`[session] record store catch-up failed: ${e.message}`)
|
|
346
|
+
return false
|
|
347
|
+
}
|
|
348
|
+
this.degraded = false
|
|
349
|
+
return true
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** 降级置位(追赶失败——投影照 store 前缀 + meta.degraded 标记 + stderr 一行诊断)。 */
|
|
353
|
+
markDegraded() {
|
|
354
|
+
this.degraded = true
|
|
355
|
+
console.error("[session] record store degraded — projection written from the stored prefix; unflushed messages beyond the window are unrecoverable")
|
|
356
|
+
this.syncMeta()
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 绑定记录存储到 agent(`agent._recordStore`)+ 身份核验/对账 + 窗口置位(§6.14)。 */
|
|
361
|
+
export function bindRecordStore(agent, { slotFile, identity = null, baseHistory = [] } = {}) {
|
|
362
|
+
const store = new RecordStore(slotFile)
|
|
363
|
+
store.bind(agent, identity, baseHistory)
|
|
364
|
+
agent._recordStore = store
|
|
365
|
+
agent._historyWindow = RECORD_WINDOW_MESSAGES
|
|
366
|
+
return store
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 解绑(模式 F / /new 重置——内存窗口随之关闭,人读线回到全量数组语义)。 */
|
|
370
|
+
export function unbindRecordStore(agent) {
|
|
371
|
+
agent._recordStore = null
|
|
372
|
+
agent._historyWindow = null
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** 删槽联动(deleteSlot / 冷前缀清理调用)。 */
|
|
376
|
+
export function unlinkRecordStore(slotFile) {
|
|
377
|
+
try { rmSync(recordDirOf(slotFile), { recursive: true, force: true }) } catch { /* 尽力面 */ }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* 槽 JSON 投影(流式拼接——§6.14 表 3 候选 1):`history` = 各段原文拼接(峰值 O(单段));
|
|
382
|
+
* 原子写(.tmp + rename——与 writeSessionFile 同族)。降级态先追赶(D-R4②)。
|
|
383
|
+
*/
|
|
384
|
+
export function saveProjectedSlot(agent, p, fields, contextHistory) {
|
|
385
|
+
const store = agent?._recordStore
|
|
386
|
+
if (!store) return false
|
|
387
|
+
store.catchUp()
|
|
388
|
+
if (store.degraded) {
|
|
389
|
+
store.markDegraded()
|
|
390
|
+
} else {
|
|
391
|
+
if (store.identity == null && agent._sessionStart) store.identity = agent._sessionStart // 身份固化(规则 3)
|
|
392
|
+
store.syncMeta()
|
|
393
|
+
}
|
|
394
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
395
|
+
const tmp = `${p}.tmp`
|
|
396
|
+
const fd = openSync(tmp, "w")
|
|
397
|
+
const w = (s) => writeSync(fd, s)
|
|
398
|
+
try {
|
|
399
|
+
w("{")
|
|
400
|
+
let first = true
|
|
401
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
402
|
+
const encoded = v === undefined ? undefined : JSON.stringify(v)
|
|
403
|
+
if (encoded === undefined) continue // JSON.stringify 语义:undefined 字段省略(不产非法 JSON)
|
|
404
|
+
if (!first) w(",")
|
|
405
|
+
first = false
|
|
406
|
+
w(JSON.stringify(k) + ":" + encoded)
|
|
407
|
+
}
|
|
408
|
+
if (!first) w(",")
|
|
409
|
+
w(`${JSON.stringify("history")}:[`)
|
|
410
|
+
let wrote = false
|
|
411
|
+
for (let s = 1; s <= store._segCount; s++) {
|
|
412
|
+
let lines
|
|
413
|
+
try { lines = readSegmentLines(join(store.dir, segName(s))) } catch { lines = [] }
|
|
414
|
+
for (const line of lines) {
|
|
415
|
+
// 半行容忍(§6.14——读路径逐行容错):崩溃在 append 中途留下的末尾半行/损坏行
|
|
416
|
+
// 不得拼进投影——否则槽 JSON 变成非法 JSON(下次 loadSlotFile 整体读不出)。
|
|
417
|
+
if (line === "" || tryParse(line) === undefined) continue
|
|
418
|
+
if (wrote) w(",")
|
|
419
|
+
wrote = true
|
|
420
|
+
w(line)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
w("]")
|
|
424
|
+
w("," + JSON.stringify("contextHistory") + ":" + JSON.stringify(contextHistory ?? []))
|
|
425
|
+
w("}")
|
|
426
|
+
} finally {
|
|
427
|
+
try { closeSync(fd) } catch { /* ignore */ }
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
renameSync(tmp, p)
|
|
431
|
+
} catch {
|
|
432
|
+
try { unlinkSync(p) } catch { /* missing is fine */ }
|
|
433
|
+
try {
|
|
434
|
+
renameSync(tmp, p)
|
|
435
|
+
try { unlinkSync(tmp) } catch { /* ignore */ }
|
|
436
|
+
} catch {
|
|
437
|
+
writeFileSync(p, readFileSync(tmp, "utf8"), "utf8")
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return true
|
|
441
|
+
}
|
package/session.mjs
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session.mjs — session persistence (slot-based model)
|
|
3
|
+
* Each project (keyed by cwd hash) keeps unlimited session slots.
|
|
4
|
+
* Every session lives in a numbered slot; the manifest tracks the SHARED active pointer
|
|
5
|
+
* (2026-09-05 §6.10 D-6 修订:active = 旧版端/ACP 的恢复依据 + 无记录端的一次性继承源 +
|
|
6
|
+
* 列表回退高亮——本端恢复依据 = end marker `{manifest}.cli`,见 session-slots.mjs
|
|
7
|
+
* resumeSlot/readEndMarker,SESSION.md §6.1/§6.10)。
|
|
8
|
+
*
|
|
9
|
+
* File layout: {hash}.json.N (slots), {hash}.json.manifest (slot metadata + active pointer).
|
|
10
|
+
* Legacy {hash}.json is migrated to a slot on first access.
|
|
11
|
+
*
|
|
12
|
+
* 2026-08-31 advisor round1 🔴:slot/清单管理拆至 session-slots.mjs(本文件曾超 500 行
|
|
13
|
+
* 硬限);本文件只保留核心读写 + re-export 全部 slot 导出(既有 import 路径不变)。
|
|
14
|
+
* init-block 批 · F-MI7:会话生命周期面(resumeSlot 包装/applySession/newSession/
|
|
15
|
+
* resetSessionState/switchToSlot/slotOccupancy)拆至 session-lifecycle.mjs(再越 500 行)——
|
|
16
|
+
* 同样 re-export 保面。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, renameSync, existsSync, statSync } from "node:fs"
|
|
20
|
+
import { basename } from "node:path"
|
|
21
|
+
import {
|
|
22
|
+
slotPath, writeSessionFile, loadManifest, saveManifest, slotDigest,
|
|
23
|
+
activeSlot, writeEndMarker,
|
|
24
|
+
} from "./session-slots.mjs"
|
|
25
|
+
// F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
|
|
26
|
+
import { guardForeignSlotFile } from "./session-guard.mjs"
|
|
27
|
+
import { engTokenSlotFields } from "./token-ttl.mjs"
|
|
28
|
+
// TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14):人读线记录存储(磁盘为准 + 内存窗口)。
|
|
29
|
+
// slimForDisplay/isLegacyTransient 迁入 store 模块(§6.14 记录存储)——本文件 re-export 保持
|
|
30
|
+
// 既有 import 面(session-slots.mjs 引 isLegacyTransient)。依赖方向单向:store 零项目内依赖。
|
|
31
|
+
import {
|
|
32
|
+
bindRecordStore, unbindRecordStore, saveProjectedSlot, isLegacyTransient, slimForDisplay,
|
|
33
|
+
RECORD_WINDOW_MESSAGES,
|
|
34
|
+
} from "./session-store.mjs"
|
|
35
|
+
export { isLegacyTransient, slimForDisplay, bindRecordStore, unbindRecordStore }
|
|
36
|
+
|
|
37
|
+
// re-export slot 管理(保持既有 import session.mjs 的调用点不变;resumeSlot 见生命周期 re-export)
|
|
38
|
+
export {
|
|
39
|
+
getSessionId, normalizeCwd, sessionPath, slotPath, manifestPath, activePath,
|
|
40
|
+
writeSessionFile, slotDigest, loadManifest, saveManifest, activeSlot, listSlots,
|
|
41
|
+
deleteSlot, isProcessAlive, END, endMarkerPath,
|
|
42
|
+
readEndMarker, writeEndMarker, claimSlot, allocateFresh,
|
|
43
|
+
} from "./session-slots.mjs"
|
|
44
|
+
// §6.12 标题写契约使 session-slots.mjs 超 500 行硬限 → renameSlot 拆至 session-rename.mjs(§6.12 模块与实现约束)
|
|
45
|
+
export { renameSlot } from "./session-rename.mjs"
|
|
46
|
+
// 人读线惰性窗口面(`history-window.mjs`)——re-export 保调用方单一路径。
|
|
47
|
+
export { historyWindow, HISTORY_PAGE_SIZE, isRealUserMsg } from "./history-window.mjs"
|
|
48
|
+
// 会话生命周期面(init-block 批 · F-MI7 拆分——语义原样外提):本端恢复入口包装(含残留 GC
|
|
49
|
+
// 钩子)/ 槽数据应用 / 新建 / 运行态清空 / 槽切换 / 占用查询。**resumeSlot/newSession 为 async**
|
|
50
|
+
// (入口一次异步探测束——调用面必须 await)。
|
|
51
|
+
export {
|
|
52
|
+
resumeSlot, applySession, newSession, resetSessionState, switchToSlot, slotOccupancy,
|
|
53
|
+
} from "./session-lifecycle.mjs"
|
|
54
|
+
import { resumeSlot as lifecycleResumeSlot } from "./session-lifecycle.mjs"
|
|
55
|
+
|
|
56
|
+
// ========== core read/write ==========
|
|
57
|
+
// (legacy transient 判定 + slimForDisplay 自本区迁至 session-store.mjs——§6.14;
|
|
58
|
+
// 上方 re-export 保持既有 import 面。)
|
|
59
|
+
|
|
60
|
+
/** 未绑定路径的人读线全量物化(模式 F——既有语义逐字保留)。 */
|
|
61
|
+
function legacyHistory(agent) {
|
|
62
|
+
return (agent._fullHistory ?? agent.history)
|
|
63
|
+
.filter((m) => !m.transient && !isLegacyTransient(m))
|
|
64
|
+
.map(slimForDisplay)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 恢复描述符(§6.14 TUI 分页契约——两调用点统一形态):`{ history, total, base }`。
|
|
68
|
+
* 绑定态:history = 尾窗 + ±1 页沿头一条(跨页回合标签判定用——不渲染)、total = 绝对
|
|
69
|
+
* 总条数(「N messages」标签口径)、base = history[0] 的绝对序号;未绑定(模式 F):
|
|
70
|
+
* 回退全量数组(total = 数组长)。 */
|
|
71
|
+
export function sessionDescriptor(agent, data) {
|
|
72
|
+
const store = agent?._recordStore
|
|
73
|
+
if (store) {
|
|
74
|
+
const total = store.total()
|
|
75
|
+
const win = store.page(Math.max(0, total - RECORD_WINDOW_MESSAGES), total, { margin: 1 })
|
|
76
|
+
return { history: win.messages, total, base: win.base }
|
|
77
|
+
}
|
|
78
|
+
const full = Array.isArray(data?.history) ? data.history : []
|
|
79
|
+
return { history: full, total: full.length, base: 0 }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 槽摘要(记录存储绑定路径——history 不物化):与 slotDigest(extractSlotMeta) 同形。 */
|
|
83
|
+
function digestFromStore(fields, counters) {
|
|
84
|
+
const meta = {
|
|
85
|
+
messageCount: counters.total,
|
|
86
|
+
turnCount: counters.userReal,
|
|
87
|
+
firstMessage: counters.firstMessage,
|
|
88
|
+
activeProvider: fields.activeProvider ?? "",
|
|
89
|
+
updatedAt: fields.updatedAt ?? Date.now(),
|
|
90
|
+
title: fields.title ?? "",
|
|
91
|
+
}
|
|
92
|
+
if (fields.activeModel) meta.activeModel = fields.activeModel
|
|
93
|
+
return { ts: Date.now(), ...meta }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Save agent state to the active slot file (atomic write). `display` (the old
|
|
97
|
+
* WYSIWYG render snapshot) is DEPRECATED — it drifted out of sync with history
|
|
98
|
+
* whenever VS Code wrote the slot, and the TUI resumed from a stale snapshot.
|
|
99
|
+
* Restore now always rebuilds from history (lazy, see startup.mjs).
|
|
100
|
+
* 2026-08-31 会诊 F1:slot 粘性——首次认领后缓存 agent._slot,永不重跑 ensureActive
|
|
101
|
+
* (原实现每次保存重推,manifest active 被并发方翻动时会话静默迁移)。
|
|
102
|
+
* 返回轮转的 .bak 路径或 null。 */
|
|
103
|
+
export function saveSession(agent) {
|
|
104
|
+
// _fullHistory is written at the source via pushReal — no flush needed here.
|
|
105
|
+
// history = FULL, never-compacted (human-readable; VS Code panel & CLI resume read this)
|
|
106
|
+
// contextHistory = machine context (possibly compacted) so CLI resume keeps the token savings
|
|
107
|
+
// Human line: transient machine injections never enter the readable record.
|
|
108
|
+
// TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14 追加时点):绑定记录存储后 `history` 字段由
|
|
109
|
+
// saveProjectedSlot 流式拼接段原文生成(磁盘为准——不物化全量数组);未绑定
|
|
110
|
+
// (模式 F:thincoder chat/测试/未覆盖路径)→ 既有全量物化路径逐字保留(D-SE26 模式 F 零回归)。
|
|
111
|
+
// Machine line (contextHistory): KEEP transient messages — resume must rebuild the
|
|
112
|
+
// machine line byte-identical to what the provider cache saw. Dropping them made every
|
|
113
|
+
// process restart diverge at the first injection position (git/OS/time reminders are
|
|
114
|
+
// re-injected with FRESH content) → whole-prefix cache miss on the CLI's very first
|
|
115
|
+
// request of each session (2026-08-16 cache-hit report; Kimi review).
|
|
116
|
+
const contextHistory = agent.history.filter((m) => !isLegacyTransient(m))
|
|
117
|
+
// fields = 槽 JSON 数据面(history 由投影插入——键序与既有 data 形态同序,history/contextHistory 居后)
|
|
118
|
+
const fields = {
|
|
119
|
+
version: 2,
|
|
120
|
+
cwd: agent.cwd,
|
|
121
|
+
title: agent.title ?? "",
|
|
122
|
+
// MODEL-MERGE-SESSION 槽双字段恒非空(裁定 a):有 provider 即携带具体复合值——
|
|
123
|
+
// 恢复不看 config(F-4)——无渠道默认可回落 ""/null(全新未配态——读侧容忍)
|
|
124
|
+
activeProvider: agent.activeProvider ?? agent.provider?.name ?? "",
|
|
125
|
+
activeModel: agent.activeModel ?? agent.provider?.model ?? null,
|
|
126
|
+
updatedAt: Date.now(),
|
|
127
|
+
tasks: agent.tasks ?? [],
|
|
128
|
+
planMode: agent.planMode ?? false,
|
|
129
|
+
autoApprove: agent.autoApprove ?? false,
|
|
130
|
+
engineering: agent.config?.agent?.engineering ?? false,
|
|
131
|
+
...engTokenSlotFields(agent),
|
|
132
|
+
goal: agent.goal ?? null,
|
|
133
|
+
advisor: agent.config?.advisor ?? null,
|
|
134
|
+
pendingReminders: agent._pendingReminders ?? [],
|
|
135
|
+
sessionStart: agent._sessionStart ?? null,
|
|
136
|
+
}
|
|
137
|
+
// 2026-09-05 §6.10 D-4:首认领(_slot 为 null——如 /session 切换后首保存、“查看对方活槽 →
|
|
138
|
+
// 保存 fork 新槽”的落盘槽跟随)写本端记录——marker = 端内最后认领者;粘性 _slot 期间
|
|
139
|
+
// 的日常保存不写(F1:保存永不参与竞争)
|
|
140
|
+
const claimedNow = agent._slot == null
|
|
141
|
+
const slot = agent._slot ??= activeSlot(agent.cwd)
|
|
142
|
+
if (claimedNow) writeEndMarker(agent.cwd, slot)
|
|
143
|
+
const p = slotPath(agent.cwd, slot)
|
|
144
|
+
// 首保存补绑(§6.14 绑定与对账——兜底所有未覆盖路径):未绑定且有槽 → 绑定记录存储
|
|
145
|
+
// (baseHistory = 当前 _fullHistory 全量;identity = 当前 _sessionStart)
|
|
146
|
+
if (!agent._recordStore) {
|
|
147
|
+
bindRecordStore(agent, { slotFile: p, identity: agent._sessionStart ?? null, baseHistory: agent._fullHistory ?? [] })
|
|
148
|
+
}
|
|
149
|
+
// 2026-08-31 会诊 F2 🔴:写前校验磁盘文件的 sessionStart——与本进程会话不符(另一
|
|
150
|
+
// 进程/会话的现场)→ 先轮转 .bak 保留再写(11311 条历史被新进程覆盖的实锤场景)。
|
|
151
|
+
// 守卫自 2026-09-08 提取为 guardForeignSlotFile(session-guard.mjs——DESIGN-TOKEN-
|
|
152
|
+
// SETTLEMENT D1 二次拆分:先入 session-slots、再因 500 行硬限迁 session-guard)——
|
|
153
|
+
// saveSession 与 token-ttl persistEngTokens(settle 当场落盘)共用同一份(检查按
|
|
154
|
+
// mtime 缓存 _slotMtime:自写未变跳过全量解析)。
|
|
155
|
+
const rotated = guardForeignSlotFile(agent, p, slot)
|
|
156
|
+
// 绑定态 → 流式投影(段原文拼接——VSC 兼容面逐字同形);未绑定 → 既有全量物化写
|
|
157
|
+
if (agent._recordStore) saveProjectedSlot(agent, p, fields, contextHistory)
|
|
158
|
+
else writeSessionFile(p, { ...fields, history: legacyHistory(agent), contextHistory })
|
|
159
|
+
// 记录我们刚写的 mtime——下次保存跳过重复解析
|
|
160
|
+
try { agent._slotMtime = statSync(p).mtimeMs } catch {}
|
|
161
|
+
// Update slot metadata in manifest
|
|
162
|
+
try {
|
|
163
|
+
const m = loadManifest(agent.cwd)
|
|
164
|
+
m.slots[slot] = agent._recordStore
|
|
165
|
+
? digestFromStore(fields, agent._recordStore.counters())
|
|
166
|
+
: slotDigest({ ...fields, history: legacyHistory(agent) })
|
|
167
|
+
saveManifest(agent.cwd, m)
|
|
168
|
+
} catch (e) {
|
|
169
|
+
// Manifest update failure is non-fatal — data is safe, metadata will lazy-recover on next listSlots
|
|
170
|
+
console.error(`[session] manifest metadata update failed for slot ${slot}: ${e.message}`)
|
|
171
|
+
}
|
|
172
|
+
return rotated
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Load slot file with shared validation — 2026-08-31 会诊 deepseek 🟡 抽取:
|
|
176
|
+
* loadSession(active 槽)/switchToSlot/ACP session/load 共享。无认领副作用。
|
|
177
|
+
* version 1/2 + history 数组 + cwd 匹配(2026-09-01 会诊 kimi 🔵:cwd 先行——"别人的
|
|
178
|
+
* 文件不动"优先于结构校验,异 cwd + 坏 version 不得改名);结构不符改名 .unreadable、
|
|
179
|
+
* 解析失败 .tmp 回退成功后提升为正主(损坏主文件改名 .corrupted 保留)、主文件缺失
|
|
180
|
+
* 时恢复孤儿 .tmp(rename 前崩溃现场)。 */
|
|
181
|
+
export function loadSlotFile(cwd, slot) {
|
|
182
|
+
const p = slotPath(cwd, slot)
|
|
183
|
+
const tryLoad = (path) => {
|
|
184
|
+
try {
|
|
185
|
+
if (!existsSync(path)) return null
|
|
186
|
+
const data = JSON.parse(readFileSync(path, "utf8"))
|
|
187
|
+
if (data.cwd && data.cwd.toLowerCase() !== cwd.toLowerCase()) return null // 别人的文件,不动
|
|
188
|
+
if (data?.version !== 1 && data?.version !== 2) {
|
|
189
|
+
if (typeof data?.version === "number" && data.version > 2) return null // 新版 CLI 的文件,不动(F2 轮转兜底)
|
|
190
|
+
try { renameSync(path, `${path}.unreadable`) } catch {}
|
|
191
|
+
console.error(`[session] unsupported version ${data?.version} at ${path} — preserved as ${basename(path)}.unreadable`)
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(data.history)) {
|
|
195
|
+
try { renameSync(path, `${path}.unreadable`) } catch {}
|
|
196
|
+
console.error(`[session] slot file ${path}: history is not an array — preserved as ${basename(path)}.unreadable`)
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
data.history = data.history.filter((m) => !isLegacyTransient(m))
|
|
200
|
+
return data
|
|
201
|
+
} catch (e) {
|
|
202
|
+
return { _error: e }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let result = tryLoad(p)
|
|
207
|
+
if (result && !result._error) return result
|
|
208
|
+
if (result?._error) {
|
|
209
|
+
console.error(`[session] failed to load slot ${slot}: ${result._error.message}. Trying .tmp fallback...`)
|
|
210
|
+
const tmpResult = tryLoad(`${p}.tmp`)
|
|
211
|
+
if (tmpResult && !tmpResult._error) {
|
|
212
|
+
console.error(`[session] recovered from .tmp fallback`)
|
|
213
|
+
// 2026-09-01 会诊 🟢:.tmp 提升为正主(损坏主文件改名 .corrupted 保留现场)——
|
|
214
|
+
// 否则主文件留在原地,每次加载都重复"恢复",且保存侧 F2 会持续轮转它。
|
|
215
|
+
try {
|
|
216
|
+
renameSync(p, `${p}.corrupted`)
|
|
217
|
+
renameSync(`${p}.tmp`, p)
|
|
218
|
+
} catch {}
|
|
219
|
+
return tmpResult
|
|
220
|
+
}
|
|
221
|
+
console.error(`[session] .tmp fallback also failed — session lost.`)
|
|
222
|
+
try { renameSync(p, `${p}.corrupted`) } catch {}
|
|
223
|
+
} else if (!result) {
|
|
224
|
+
// 2026-09-01 advisor 🔵:主文件缺失但孤儿 .tmp 存在(原子写 rename 前崩溃)——
|
|
225
|
+
// 恢复并提升为正主(SESSION.md §6.4 的 .tmp 回退语义应覆盖此场景)。
|
|
226
|
+
const orphan = tryLoad(`${p}.tmp`)
|
|
227
|
+
if (orphan && !orphan._error) {
|
|
228
|
+
console.error(`[session] slot ${slot} main file missing — recovered orphan .tmp`)
|
|
229
|
+
try { renameSync(`${p}.tmp`, p) } catch {}
|
|
230
|
+
return orphan
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Load session data for this cwd — 2026-09-05 §6.10 D-2:平移为 resumeSlot 的数据包装
|
|
237
|
+
* (签名不变——测试兼容;读槽校验/.tmp 回退/legacy 兜底语义原样保留在 resumeSlot 的
|
|
238
|
+
* data 层)。恢复目标选择按本端记录(end marker)——manifest active 仅作无记录端的
|
|
239
|
+
* 一次性继承源(D-6),不再作本端恢复第一依据。
|
|
240
|
+
* **async**(init-block 批 · F-MI7:resumeSlot 整链异步——入口一次异步探测束;调用面
|
|
241
|
+
* 必须 await)。 */
|
|
242
|
+
export async function loadSession(cwd) {
|
|
243
|
+
return (await lifecycleResumeSlot(cwd)).data
|
|
244
|
+
}
|