thincoder 0.12.60 → 0.12.62
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 +49 -2
- package/README.md +8 -6
- package/bin/thincoder.mjs +38 -124
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +19 -6
- 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 +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +24 -7
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +43 -11
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/async-settle.mjs +13 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +28 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +22 -13
- package/src/agent-tools/read-history.mjs +31 -6
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +18 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +10 -7
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +69 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +42 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/context.mjs +12 -1
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +35 -8
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +9 -4
- package/src/heap-watch.mjs +88 -0
- package/src/hooks.mjs +7 -3
- package/src/ledger.mjs +227 -0
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +77 -36
- package/src/memory/core.mjs +8 -9
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +17 -11
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/scan.mjs +95 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +19 -9
- package/src/prompts/advisor-round1.md +8 -2
- package/src/prompts/advisor-round2.md +14 -3
- package/src/prompts/advisor-round3.md +14 -3
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +258 -0
- package/src/prompts/discipline-normal.md +185 -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 +60 -0
- package/src/prompts/persona-engineering.md +55 -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 +13 -32
- package/src/provider/errors.mjs +26 -1
- 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/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +9 -2
- package/src/session-guard.mjs +12 -0
- package/src/session-segments.mjs +100 -0
- package/src/session-slots.mjs +10 -2
- package/src/session-store.mjs +441 -0
- package/src/session.mjs +137 -99
- package/src/text-budget.mjs +46 -0
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/traces/trace-store.mjs +195 -64
- package/src/tui/agent-turn.mjs +32 -13
- 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-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +5 -1
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +8 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/display-budget.mjs +184 -0
- package/src/tui/index.mjs +69 -40
- package/src/tui/key-handler-search.mjs +9 -1
- 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/ledger-surface.mjs +69 -0
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +38 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +45 -13
- package/src/tui/subagent-blocks.mjs +70 -90
- package/src/tui/subagent-children.mjs +125 -67
- package/src/tui/subagent-freeze.mjs +48 -45
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +31 -83
- package/src/tui/tool-args.mjs +9 -4
- package/src/tui/tool-display.mjs +20 -5
- package/src/tui/tool-events.mjs +76 -30
- package/src/tui/tui-lifecycle.mjs +27 -7
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +54 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-store.mjs — 人读线记录存储:磁盘为准 + 内存窗口(TUI-OOM-ROOTCAUSE 批)。
|
|
3
|
+
* 机制契约全文 = docs/design/SESSION.md §14.3(D2 单一权威源);本文件 = 实现。
|
|
4
|
+
* 形态:`{slot 文件路径}.d/` + `meta.json` + `seg-000001.jsonl`(行 = slimForDisplay 后的
|
|
5
|
+
* 消息 JSON——与槽 JSON `history` 元素逐字节同形;投影零转换,§14.3.1)。
|
|
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(调用面零改——§14.3.3 接口面)
|
|
23
|
+
export {
|
|
24
|
+
RECORD_SEG_MESSAGES, RECORD_DIR_SUFFIX, recordDirOf, isLegacyTransient, slimForDisplay, _storeStats,
|
|
25
|
+
} from "./session-segments.mjs"
|
|
26
|
+
|
|
27
|
+
/** 内存窗口(条数——与首屏 INITIAL_HISTORY_MESSAGES 同值——单一概念,§14.3.2)。 */
|
|
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 // 追加失败即停(§14.4 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
|
+
/** 绑定对账(§14.3.4):**先验身份、后比计数**。返回 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>` 保留现场,按现场重建(§14.3.8 ②③)——身份回到现场身份
|
|
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 目录 → 重新物化——§14.3.4 规则 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 页沿(§14.3.6——页沿上下文供渲染层跨页回合标签/工具结果配对)。 */
|
|
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
|
+
/** 追加一条真实消息(§14.3.5 单点 = 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 (SESSION.md §14.4 D-R4)`)
|
|
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 (SESSION.md §14.4 D-R4)")
|
|
356
|
+
this.syncMeta()
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 绑定记录存储到 agent(`agent._recordStore`)+ 身份核验/对账 + 窗口置位(§14.3.4)。 */
|
|
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 投影(流式拼接——§14.3.3 表 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
|
+
// 半行容忍(§14.3.2——读路径逐行容错):崩溃在 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
|
+
}
|