thincoder 0.12.62 → 0.12.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +46 -49
  3. package/bin/thincoder.mjs +68 -34
  4. package/package.json +7 -6
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +257 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +60 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +35 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-plan.mjs +9 -0
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +24 -1
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +33 -11
  38. package/src/tui/index.mjs +18 -10
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-modes.mjs +9 -4
  41. package/src/tui/ledger-surface.mjs +22 -59
  42. package/src/tui/model-catalog.mjs +4 -4
  43. package/src/tui/model-picker.mjs +8 -7
  44. package/src/tui/mouse.mjs +11 -6
  45. package/src/tui/pickers.mjs +15 -2
  46. package/src/tui/render-conversation.mjs +1 -1
  47. package/src/tui/render-frame.mjs +15 -6
  48. package/src/tui/render-loop.mjs +1 -1
  49. package/src/tui/render-segments.mjs +3 -1
  50. package/src/tui/slash-commands.mjs +1 -1
  51. package/src/tui/startup.mjs +14 -14
  52. package/src/tui/subagent-blocks.mjs +20 -3
  53. package/src/tui/subagent-freeze.mjs +73 -2
  54. package/src/tui/suspension-drive.mjs +57 -23
  55. package/src/tui/tool-events.mjs +11 -8
  56. package/src/tui/tui-lifecycle.mjs +9 -0
  57. package/src/tui/wizard.mjs +3 -3
  58. package/src/tui/wrapped-spawn.mjs +6 -2
  59. package/src/abort-provenance.mjs +0 -116
  60. package/src/advisor/citations.mjs +0 -139
  61. package/src/advisor/compaction.mjs +0 -174
  62. package/src/advisor/convergence.mjs +0 -80
  63. package/src/advisor/history.mjs +0 -77
  64. package/src/advisor/loop.mjs +0 -293
  65. package/src/advisor/messages.mjs +0 -299
  66. package/src/advisor/project-context.mjs +0 -194
  67. package/src/advisor/repos.mjs +0 -150
  68. package/src/advisor/run.mjs +0 -293
  69. package/src/advisor/truncate.mjs +0 -57
  70. package/src/advisor.mjs +0 -290
  71. package/src/agent/completion.mjs +0 -146
  72. package/src/agent/dispatch.mjs +0 -489
  73. package/src/agent/helpers.mjs +0 -384
  74. package/src/agent/post-turn.mjs +0 -70
  75. package/src/agent/record-results.mjs +0 -174
  76. package/src/agent/relay-prefix.mjs +0 -39
  77. package/src/agent/run-stages.mjs +0 -244
  78. package/src/agent/setup-reminders.mjs +0 -69
  79. package/src/agent/setup.mjs +0 -354
  80. package/src/agent/spawn-child.mjs +0 -243
  81. package/src/agent-tools/advisor-async.mjs +0 -346
  82. package/src/agent-tools/advisor-settle.mjs +0 -231
  83. package/src/agent-tools/advisor.mjs +0 -260
  84. package/src/agent-tools/async-settle.mjs +0 -204
  85. package/src/agent-tools/batch-segment.mjs +0 -195
  86. package/src/agent-tools/consult.mjs +0 -473
  87. package/src/agent-tools/design-token.mjs +0 -117
  88. package/src/agent-tools/digest-budget.mjs +0 -76
  89. package/src/agent-tools/eng.mjs +0 -67
  90. package/src/agent-tools/escalate-async.mjs +0 -295
  91. package/src/agent-tools/goal.mjs +0 -119
  92. package/src/agent-tools/plan.mjs +0 -81
  93. package/src/agent-tools/read-history.mjs +0 -309
  94. package/src/agent-tools/recent-changes.mjs +0 -24
  95. package/src/agent-tools/review-streak.mjs +0 -93
  96. package/src/agent-tools/settings.mjs +0 -265
  97. package/src/agent-tools/skill.mjs +0 -47
  98. package/src/agent-tools/subagent-actions.mjs +0 -482
  99. package/src/agent-tools/subagent-async.mjs +0 -434
  100. package/src/agent-tools/subagent-panel.mjs +0 -160
  101. package/src/agent-tools/subagent-run.mjs +0 -205
  102. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  103. package/src/agent-tools/subagent-spawn.mjs +0 -459
  104. package/src/agent-tools/subagent.mjs +0 -404
  105. package/src/agent-tools/task.mjs +0 -87
  106. package/src/agent-tools/timer.mjs +0 -46
  107. package/src/agent-tools/verify.mjs +0 -271
  108. package/src/agent-tools.mjs +0 -17
  109. package/src/agent.mjs +0 -417
  110. package/src/auto-think.mjs +0 -115
  111. package/src/config-migrate.mjs +0 -70
  112. package/src/config.mjs +0 -496
  113. package/src/context.mjs +0 -392
  114. package/src/conventions.mjs +0 -223
  115. package/src/embedding.mjs +0 -120
  116. package/src/escape.mjs +0 -152
  117. package/src/expand-home.mjs +0 -16
  118. package/src/explore-distill.mjs +0 -155
  119. package/src/generate-title.mjs +0 -88
  120. package/src/git/checkpoint.mjs +0 -448
  121. package/src/git/gitmem.mjs +0 -100
  122. package/src/hooks.mjs +0 -97
  123. package/src/ledger.mjs +0 -227
  124. package/src/log.mjs +0 -195
  125. package/src/markdown.mjs +0 -106
  126. package/src/mcp/helpers.mjs +0 -51
  127. package/src/mcp/transport-http.mjs +0 -248
  128. package/src/mcp/transport-stdio.mjs +0 -140
  129. package/src/mcp/transport-ws.mjs +0 -122
  130. package/src/mcp.mjs +0 -295
  131. package/src/memory/code-index.mjs +0 -219
  132. package/src/memory/code-sync.mjs +0 -415
  133. package/src/memory/core.mjs +0 -299
  134. package/src/memory/delete.mjs +0 -236
  135. package/src/memory/docs.mjs +0 -419
  136. package/src/memory/file-walk.mjs +0 -109
  137. package/src/memory/scan.mjs +0 -95
  138. package/src/memory/schema.mjs +0 -452
  139. package/src/memory.mjs +0 -21
  140. package/src/model-ref.mjs +0 -66
  141. package/src/model-specs.mjs +0 -179
  142. package/src/peer-domains.mjs +0 -265
  143. package/src/peer-instances.mjs +0 -231
  144. package/src/prompt-overlays.mjs +0 -82
  145. package/src/prompts/advisor-design.md +0 -41
  146. package/src/prompts/advisor-round1.md +0 -41
  147. package/src/prompts/advisor-round2.md +0 -46
  148. package/src/prompts/advisor-round3.md +0 -42
  149. package/src/prompts/common.md +0 -115
  150. package/src/prompts/consult-base.md +0 -19
  151. package/src/prompts/discipline-engineering.md +0 -258
  152. package/src/prompts/discipline-normal.md +0 -185
  153. package/src/prompts/persona-coder.md +0 -21
  154. package/src/prompts/persona-eng-coder.md +0 -37
  155. package/src/prompts/persona-eng-designer.md +0 -60
  156. package/src/prompts/persona-engineering.md +0 -55
  157. package/src/prompts/persona-explore.md +0 -15
  158. package/src/prompts/persona-normal.md +0 -27
  159. package/src/prompts/persona-plan.md +0 -26
  160. package/src/provider/anthropic.mjs +0 -225
  161. package/src/provider/core.mjs +0 -476
  162. package/src/provider/errors.mjs +0 -101
  163. package/src/provider/google.mjs +0 -257
  164. package/src/provider/index.mjs +0 -7
  165. package/src/provider/list-models.mjs +0 -93
  166. package/src/provider/normalize.mjs +0 -81
  167. package/src/provider/rate.mjs +0 -108
  168. package/src/provider/responses.mjs +0 -495
  169. package/src/provider/retry.mjs +0 -88
  170. package/src/provider/sse.mjs +0 -264
  171. package/src/proxy.mjs +0 -261
  172. package/src/rules.mjs +0 -53
  173. package/src/session-gc.mjs +0 -221
  174. package/src/session-guard.mjs +0 -59
  175. package/src/session-migrate.mjs +0 -48
  176. package/src/session-rename.mjs +0 -38
  177. package/src/session-segments.mjs +0 -100
  178. package/src/session-slots.mjs +0 -492
  179. package/src/session-store.mjs +0 -441
  180. package/src/session.mjs +0 -492
  181. package/src/skills.mjs +0 -153
  182. package/src/text-budget.mjs +0 -46
  183. package/src/token-ttl.mjs +0 -274
  184. package/src/tools/apply_patch.md +0 -15
  185. package/src/tools/bash.md +0 -37
  186. package/src/tools/bash.mjs +0 -268
  187. package/src/tools/checklist-sync.mjs +0 -181
  188. package/src/tools/checklist.md +0 -13
  189. package/src/tools/checklist.mjs +0 -299
  190. package/src/tools/delete.md +0 -13
  191. package/src/tools/edit-batch.mjs +0 -191
  192. package/src/tools/edit-diff.mjs +0 -348
  193. package/src/tools/edit.md +0 -30
  194. package/src/tools/execute.md +0 -21
  195. package/src/tools/execute.mjs +0 -228
  196. package/src/tools/fetch.md +0 -12
  197. package/src/tools/file.mjs +0 -469
  198. package/src/tools/file_ops.md +0 -17
  199. package/src/tools/get_current_time.md +0 -8
  200. package/src/tools/git-checkpoint.mjs +0 -143
  201. package/src/tools/git-ext.mjs +0 -173
  202. package/src/tools/git.md +0 -54
  203. package/src/tools/git.mjs +0 -356
  204. package/src/tools/glob-dialect.mjs +0 -130
  205. package/src/tools/glob.md +0 -11
  206. package/src/tools/grep.md +0 -19
  207. package/src/tools/hashline_edit.md +0 -14
  208. package/src/tools/index.mjs +0 -36
  209. package/src/tools/insert_after.md +0 -15
  210. package/src/tools/lint.md +0 -10
  211. package/src/tools/linter.mjs +0 -128
  212. package/src/tools/ls.md +0 -12
  213. package/src/tools/lsp.md +0 -10
  214. package/src/tools/lsp.mjs +0 -316
  215. package/src/tools/ops.mjs +0 -299
  216. package/src/tools/patch.mjs +0 -282
  217. package/src/tools/process.md +0 -10
  218. package/src/tools/question.md +0 -16
  219. package/src/tools/question.mjs +0 -26
  220. package/src/tools/read.md +0 -20
  221. package/src/tools/read_image.md +0 -8
  222. package/src/tools/repomap.mjs +0 -314
  223. package/src/tools/search.mjs +0 -236
  224. package/src/tools/shared.mjs +0 -446
  225. package/src/tools/tree.md +0 -14
  226. package/src/tools/tree.mjs +0 -66
  227. package/src/tools/wait_for.md +0 -22
  228. package/src/tools/web.mjs +0 -224
  229. package/src/tools/websearch.md +0 -16
  230. package/src/tools/write.md +0 -11
  231. package/src/traces/trace-store.mjs +0 -355
@@ -1,441 +0,0 @@
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
- }