berry-agent 0.1.0-alpha.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.
Files changed (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,706 @@
1
+ /**
2
+ * persist — SQLite 物理层 Store(05 篇 §6 物理存储 / §9 schema 的落码面)。
3
+ *
4
+ * 职责边界:本文件只做「表的读写与库的治理」——事件批写(含 FTS 同批对账与
5
+ * sessions 行 upsert)/ 读原语(loadEvents 撕裂尾 heal、queryEvents 过滤维)/
6
+ * store_state·credentials·model_catalog·incidents 四小面 / FTS 重建与抽样审计 /
7
+ * 开库门禁序(版本链 + 备份 + 权限)。队列编舞(节流/退避/毒丸分类)在
8
+ * write-behind.ts——两文件经 WriteTarget 窄接口耦合。
9
+ */
10
+ import Database from 'better-sqlite3';
11
+ import { copyFileSync, mkdirSync } from 'node:fs';
12
+ import { dirname } from 'node:path';
13
+ import { BaseError, getEventTypeMeta } from '../contracts/index.js';
14
+ import { snapshotJsonValue } from '../session/index.js';
15
+ import { normalizeMigrations } from './migrations.js';
16
+ import { ensureDataDir, MEMORY_DB_PATH, repairFileMode, resolveDataDir } from './paths.js';
17
+ import { decryptSecret, encryptSecret, ephemeralSecretKey, loadOrCreateSecretKey } from './secret-box.js';
18
+ import { APPLICATION_ID, CANONICAL_DDL, SCHEMA_VERSION } from './schema.js';
19
+ /** queryEvents 缺省页帽与硬帽 */
20
+ const QUERY_LIMIT_DEFAULT = 1000;
21
+ const QUERY_LIMIT_MAX = 10000;
22
+ /** store_state LRU 帽(05 §6.2) */
23
+ const STORE_STATE_LRU_CAP = 256;
24
+ /**
25
+ * 打开库(门禁序,05 §6.4/§6.5/§6.6):
26
+ * 目录确保(数据目录 0700 + 库父目录存在)→ 开库 → WAL 编舞(busy_timeout +
27
+ * 探测 + 同步退避)→ 库文件 0600 自检修复 → 版本门禁(全新库单事务建链 /
28
+ * 高于 head 拒开 / 低于 head 备份后逐版迁移)→ 密钥装配。
29
+ * @returns 就绪的 Store(调用方负责 close——退出 checkpoint 编舞在 close 内)
30
+ */
31
+ export function openStore(options = {}) {
32
+ const warn = options.warn ?? ((message) => console.error(message));
33
+ const dbPath = options.dbPath ?? MEMORY_DB_PATH;
34
+ const dataDir = options.dataDir ?? resolveDataDir();
35
+ const inMemory = dbPath === MEMORY_DB_PATH;
36
+ const chain = normalizeMigrations(options.migrations ?? [], SCHEMA_VERSION);
37
+ const headVersion = chain.length > 0 ? chain[chain.length - 1].version : SCHEMA_VERSION;
38
+ if (!inMemory) {
39
+ // 库父目录须在场(better-sqlite3 不代建目录);0700 治理只打数据目录——
40
+ // tier-2(DB_PATH 指库文件到别处)时库父目录不是数据目录,不越权改权限
41
+ mkdirSync(dirname(dbPath), { recursive: true });
42
+ }
43
+ ensureDataDir(dataDir, warn);
44
+ const db = new Database(dbPath);
45
+ prepareWal(db, warn);
46
+ if (!inMemory) {
47
+ // 库文件 0600 自检修复(05 §6.6——credentials 表所在库文件)
48
+ repairFileMode(dbPath, '库文件', warn);
49
+ }
50
+ // 版本门禁(先读后判;全新库单事务 = 建链原子性,05 §6.5)
51
+ let version = readUserVersion(db);
52
+ if (version > headVersion) {
53
+ db.close();
54
+ throw new BaseError('PERSIST_SCHEMA_TOO_NEW', `库 ${dbPath} 的 user_version=${version} 高于宿主迁移链 head=${headVersion}(降级运行拒开——先升级宿主再开此库)`);
55
+ }
56
+ if (version === 0) {
57
+ if (!databaseIsEmpty(db)) {
58
+ db.close();
59
+ throw new BaseError('PERSIST_SCHEMA_UNRECOGNIZED', `库 ${dbPath} 非空但 user_version=0(外来 SQLite 文件或损坏残卷)——宁拒绝不误读`);
60
+ }
61
+ const bootstrap = db.transaction(() => {
62
+ db.exec(CANONICAL_DDL);
63
+ db.pragma(`application_id = ${APPLICATION_ID}`);
64
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
65
+ });
66
+ bootstrap();
67
+ version = SCHEMA_VERSION;
68
+ }
69
+ if (version < headVersion) {
70
+ // 低于 head:迁移前备份库文件(用户数据主权,05 §6.4)——内存库无从备份
71
+ if (!inMemory) {
72
+ checkpointTruncate(db, warn);
73
+ const backupPath = `${dbPath}.bak-v${version}`;
74
+ copyFileSync(dbPath, backupPath);
75
+ warn(`[persist] 迁移前备份:${dbPath} → ${backupPath}(从 v${version} 升到 v${headVersion})`);
76
+ }
77
+ for (const migration of chain) {
78
+ if (migration.version <= version)
79
+ continue;
80
+ const step = db.transaction(() => {
81
+ db.exec(migration.sql);
82
+ db.pragma(`user_version = ${migration.version}`);
83
+ });
84
+ step();
85
+ }
86
+ }
87
+ // 凭证密钥:注入位 > 数据目录自举(文件库)/ 临时密钥(内存库——库亡密亡,
88
+ // 不在盘上留无主密钥文件)
89
+ const secretKey = options.secretKey ?? (inMemory ? ephemeralSecretKey() : loadOrCreateSecretKey(dataDir, warn));
90
+ return new Store(db, dbPath, headVersion, secretKey, warn, options.clock ?? (() => Date.now()));
91
+ }
92
+ /** 读 user_version(PRAGMA 读 simple 形态直回数值) */
93
+ function readUserVersion(db) {
94
+ const row = db.pragma('user_version', { simple: true });
95
+ return typeof row === 'number' ? row : 0;
96
+ }
97
+ /** 空库判定:无用户表(sqlite_% 内部对象不算;影子表属用户域但新库必无) */
98
+ function databaseIsEmpty(db) {
99
+ const row = db
100
+ .prepare(`SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`)
101
+ .get();
102
+ return row.n === 0;
103
+ }
104
+ /**
105
+ * WAL 编舞(承 berry prepareWalConnection 三拍):
106
+ * ① busy_timeout 5000(跨进程短锁等待面)→ ② journal_mode=WAL 幂等探测
107
+ * (换模被并发连接占住时 SQLITE_BUSY——同步退避重试 5 轮)→ ③ synchronous=FULL
108
+ * (批落事务的落盘语义是「flush 返回 = 已持久」)。
109
+ *
110
+ * 件内导出(非公开面):aux.ts 开派生库复用同一卫生拍——主库与派生库
111
+ * 连接治理单源(批 18b openAuxDatabase 消费位)。
112
+ */
113
+ export function prepareWal(db, warn) {
114
+ db.pragma('busy_timeout = 5000');
115
+ for (let attempt = 0;; attempt++) {
116
+ try {
117
+ db.pragma('journal_mode = WAL');
118
+ break;
119
+ }
120
+ catch (err) {
121
+ // 结构化判别 SQLITE_BUSY(@types 的 SqliteError instanceof 收窄面失真——
122
+ // 稳定契约是 code 字符串)
123
+ const code = typeof err === 'object' && err !== null && 'code' in err ? err.code : undefined;
124
+ if (code !== 'SQLITE_BUSY' || attempt >= 5) {
125
+ warn(`[persist] journal_mode=WAL 设置失败(继续以缺省日志模式运行):${String(err)}`);
126
+ break;
127
+ }
128
+ syncSleep(Math.min(20 * 3 ** attempt, 500));
129
+ }
130
+ }
131
+ db.pragma('synchronous = FULL');
132
+ }
133
+ /** 同步睡眠(主线程 Atomics.wait——busy 退避等锁场景专用,勿作通用 sleep 用) */
134
+ function syncSleep(ms) {
135
+ const cell = new Int32Array(new SharedArrayBuffer(4));
136
+ Atomics.wait(cell, 0, 0, ms);
137
+ }
138
+ /** 退出前 checkpoint(TRUNCATE——干净退出不留 -wal 残卷,05 §6.5) */
139
+ function checkpointTruncate(db, warn) {
140
+ try {
141
+ db.pragma('wal_checkpoint(TRUNCATE)');
142
+ }
143
+ catch (err) {
144
+ warn(`[persist] wal_checkpoint(TRUNCATE) 失败(残卷由下次打开自动恢复——WAL 设计内行为):${String(err)}`);
145
+ }
146
+ }
147
+ /**
148
+ * Store——库级单例(进程内多会话共用,05 §6.1 律 2)。
149
+ *
150
+ * 写路径 per-session 游标(cursors Map)做连续性断言:期望 seq = 游标 + 1,
151
+ * 不符即 PERSIST_DATA_CORRUPT(入队序 = 事务序的结构性违约——bug 指示器,
152
+ * fail-loud 不猜)。游标首遇从 sessions.last_seq 初始化(seeded 会话续写衔接)。
153
+ */
154
+ export class Store {
155
+ dbPath;
156
+ headVersion;
157
+ db;
158
+ secretKey;
159
+ warn;
160
+ clock;
161
+ /** per-session 已落账最大 seq(含毒丸 accountDropped 的跳记账) */
162
+ cursors = new Map();
163
+ /** 预编译语句缓存(热路径免重复 prepare) */
164
+ statements = new Map();
165
+ closed = false;
166
+ constructor(db, dbPath, headVersion, secretKey, warn, clock) {
167
+ this.db = db;
168
+ this.dbPath = dbPath;
169
+ this.headVersion = headVersion;
170
+ this.secretKey = secretKey;
171
+ this.warn = warn;
172
+ this.clock = clock;
173
+ }
174
+ /** 语句缓存取用(同 SQL 全生命期一个 prepared 对象) */
175
+ stmt(sql) {
176
+ let s = this.statements.get(sql);
177
+ if (!s) {
178
+ s = this.db.prepare(sql);
179
+ this.statements.set(sql, s);
180
+ }
181
+ return s;
182
+ }
183
+ /**
184
+ * 同实例 better-sqlite3 句柄窄面(core: 插件 DAO 接线位——批 15a 起生效;
185
+ * 03 §3.2 `berry-agent/sqlite` SqliteFace 同源同律:宿主 core: 件建自有表走
186
+ * 同一实例、better-sqlite3 裸导入仍只准 persist——消费侧以类型导入取得
187
+ * `SqliteDatabase` 形、零运行时依赖)。
188
+ *
189
+ * 调用方纪律:只建自有表族/只读写自有表——主库七表(schema.ts CANONICAL_DDL)
190
+ * 的读写恒走 Store 方法面,本面不为绕开写链门禁而开。
191
+ */
192
+ sqlite() {
193
+ this.ensureOpen();
194
+ return this.db;
195
+ }
196
+ /** 关库守卫(调用序 bug——编程错误面,非注册码语义) */
197
+ ensureOpen() {
198
+ if (this.closed)
199
+ throw new Error('persist Store 已关闭(close 后不得再读写——调用序 bug)');
200
+ }
201
+ // ── 写路径(write-behind 消费面)──────────────────────────────────────────
202
+ /** 批写:单事务(events + FTS 同批 + sessions upsert + 连续性断言) */
203
+ writeEvents(writes) {
204
+ this.ensureOpen();
205
+ if (writes.length === 0)
206
+ return;
207
+ const now = this.clock();
208
+ const tx = this.db.transaction(() => {
209
+ for (const write of writes)
210
+ this.writeTuple(write, now);
211
+ });
212
+ tx();
213
+ }
214
+ /** 行写:毒丸诊断模式(单条独立事务——失败原样抛,分类归 write-behind) */
215
+ writeEventSingle(write) {
216
+ this.ensureOpen();
217
+ const now = this.clock();
218
+ const tx = this.db.transaction(() => {
219
+ this.writeTuple(write, now);
220
+ });
221
+ tx();
222
+ }
223
+ /** 单事件元组写(批/行两模式共用体:events 行 + fts 对账 + sessions 推进) */
224
+ writeTuple(write, now) {
225
+ const { sessionId, event, registration } = write;
226
+ const expected = this.cursorFor(sessionId) + 1;
227
+ if (event.seq !== expected) {
228
+ throw new BaseError('PERSIST_DATA_CORRUPT', `会话 ${sessionId} 写序违约:期望 seq=${expected} 实得 ${event.seq}(入队序 = 事务序被破坏——bug 指示器)`);
229
+ }
230
+ this.stmt(`INSERT INTO events (session_id, seq, type, time, data, ignorable, surface_op, source_event_seqs)
231
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(sessionId, event.seq, event.type, event.time, JSON.stringify(event.data), event.ignorable ? 1 : 0, event.surfaceOp ? JSON.stringify(event.surfaceOp) : null, event.sourceEventSeqs ? JSON.stringify(event.sourceEventSeqs) : null);
232
+ // FTS 写入对账(同批同事务):surface 类别入索引;遮蔽指令同步删区间行
233
+ const body = ftsBodyOf(event);
234
+ if (body !== null) {
235
+ this.stmt(`INSERT INTO session_fts (session_id, seq, body) VALUES (?, ?, ?)`).run(sessionId, event.seq, body);
236
+ }
237
+ if (event.surfaceOp) {
238
+ this.stmt(`DELETE FROM session_fts WHERE session_id = ? AND seq BETWEEN ? AND ?`).run(sessionId, event.surfaceOp.start, event.surfaceOp.end);
239
+ }
240
+ // sessions 行:身份列首登为准(冲突只推进),updated_at/last_seq 批写推进
241
+ this.stmt(`INSERT INTO sessions (id, title, origin, parent_id, seed_length, workspace_root, created_at, updated_at, last_seq)
242
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
243
+ ON CONFLICT(id) DO UPDATE SET updated_at = excluded.updated_at, last_seq = MAX(last_seq, excluded.last_seq)`).run(sessionId, registration.title ?? null, registration.origin, registration.parentId ?? null, registration.seedLength, registration.workspaceRoot ?? null, now, now, event.seq);
244
+ this.cursors.set(sessionId, event.seq);
245
+ }
246
+ /** 毒丸记账:不落行但推进游标(后续事件 seq 续账不撞连续性断言) */
247
+ accountDropped(sessionId, seq) {
248
+ const current = this.cursorFor(sessionId);
249
+ if (seq > current)
250
+ this.cursors.set(sessionId, seq);
251
+ }
252
+ /** 游标取用(首遇从库内 sessions.last_seq 初始化) */
253
+ cursorFor(sessionId) {
254
+ let cursor = this.cursors.get(sessionId);
255
+ if (cursor === undefined) {
256
+ const row = this.stmt(`SELECT last_seq FROM sessions WHERE id = ?`).get(sessionId);
257
+ cursor = row ? row.last_seq : -1;
258
+ this.cursors.set(sessionId, cursor);
259
+ }
260
+ return cursor;
261
+ }
262
+ /** 毒丸 durable 标记落账(只增不删——审计面 select 即阅) */
263
+ recordIncident(entry) {
264
+ this.ensureOpen();
265
+ this.stmt(`INSERT INTO persist_incidents (time, session_id, seq, type, reason) VALUES (?, ?, ?, ?, ?)`).run(entry.time, entry.sessionId ?? null, entry.seq ?? null, entry.type ?? null, entry.reason);
266
+ }
267
+ /** 审计读面(诊断/CLI 消费——倒序最新在前) */
268
+ listIncidents(limit = 100) {
269
+ this.ensureOpen();
270
+ const rows = this.stmt(`SELECT id, time, session_id, seq, type, reason FROM persist_incidents ORDER BY id DESC LIMIT ?`).all(limit);
271
+ return rows.map((row) => ({
272
+ id: row.id,
273
+ time: row.time,
274
+ sessionId: row.session_id ?? undefined,
275
+ seq: row.seq ?? undefined,
276
+ type: row.type ?? undefined,
277
+ reason: row.reason,
278
+ }));
279
+ }
280
+ // ── events 读原语 ──────────────────────────────────────────────────────────
281
+ /**
282
+ * 全量读会话事件(恢复重放面):
283
+ * - 中段损坏(坏位之后仍有好行)→ PERSIST_DATA_CORRUPT fail-loud(宁拒勿删);
284
+ * - 撕裂尾(坏位之后无行 = 尾部残卷)→ heal:删除坏位起的 events+fts 行、
285
+ * sessions.last_seq 回退 + warn,返回干净前缀(§4 步 1)。
286
+ */
287
+ loadEvents(sessionId) {
288
+ this.ensureOpen();
289
+ const rows = this.stmt(`SELECT seq, type, time, data, ignorable, surface_op, source_event_seqs FROM events WHERE session_id = ? ORDER BY seq`).all(sessionId);
290
+ const events = [];
291
+ let firstBadSeq = null;
292
+ for (const row of rows) {
293
+ if (row.seq !== events.length) {
294
+ firstBadSeq = events.length; // 洞:期望位缺行
295
+ break;
296
+ }
297
+ try {
298
+ events.push(parseEventRow(row));
299
+ }
300
+ catch {
301
+ firstBadSeq = row.seq; // 坏行:JSON 残卷
302
+ break;
303
+ }
304
+ }
305
+ if (firstBadSeq !== null) {
306
+ if (rows.some((row) => row.seq > firstBadSeq)) {
307
+ throw new BaseError('PERSIST_DATA_CORRUPT', `会话 ${sessionId} 日志中段损坏(首个坏位 seq=${firstBadSeq},其后仍有行)——宁拒勿删,请人工检视`);
308
+ }
309
+ // 撕裂尾 heal(§2.5 例外一:尾部残卷截断)
310
+ const heal = this.db.transaction(() => {
311
+ this.stmt(`DELETE FROM events WHERE session_id = ? AND seq >= ?`).run(sessionId, firstBadSeq);
312
+ this.stmt(`DELETE FROM session_fts WHERE session_id = ? AND seq >= ?`).run(sessionId, firstBadSeq);
313
+ this.stmt(`UPDATE sessions SET last_seq = ? WHERE id = ? AND last_seq >= ?`).run(firstBadSeq - 1, sessionId, firstBadSeq);
314
+ });
315
+ heal();
316
+ this.cursors.set(sessionId, firstBadSeq - 1);
317
+ this.warn(`[persist] 会话 ${sessionId} 撕裂尾截断:seq>=${firstBadSeq} 的 ${rows.length - events.length} 行残卷已清(恢复按干净前缀重放)`);
318
+ }
319
+ return events;
320
+ }
321
+ /** 跨会话事件查询(05 §3.4——过滤维 + 游标分页;坏行跳过 + warn) */
322
+ queryEvents(filter) {
323
+ this.ensureOpen();
324
+ const limit = Math.min(Math.max(1, filter.limit ?? QUERY_LIMIT_DEFAULT), QUERY_LIMIT_MAX);
325
+ const clauses = [];
326
+ const params = [];
327
+ if (filter.sessionId !== undefined) {
328
+ clauses.push('session_id = ?');
329
+ params.push(filter.sessionId);
330
+ }
331
+ if (filter.types !== undefined && filter.types.length > 0) {
332
+ clauses.push(`type IN (${filter.types.map(() => '?').join(', ')})`);
333
+ params.push(...filter.types);
334
+ }
335
+ if (filter.sinceMs !== undefined) {
336
+ clauses.push('time >= ?');
337
+ params.push(filter.sinceMs);
338
+ }
339
+ if (filter.untilMs !== undefined) {
340
+ clauses.push('time <= ?');
341
+ params.push(filter.untilMs);
342
+ }
343
+ if (filter.fromSeq !== undefined) {
344
+ clauses.push('seq >= ?');
345
+ params.push(filter.fromSeq);
346
+ }
347
+ if (filter.toSeq !== undefined) {
348
+ clauses.push('seq <= ?');
349
+ params.push(filter.toSeq);
350
+ }
351
+ if (filter.cursor) {
352
+ // 游标 = 上一页末行 (time, session_id, seq) 的不透明令牌——行值比较续页
353
+ clauses.push('(time, session_id, seq) > (?, ?, ?)');
354
+ params.push(...decodeCursor(filter.cursor));
355
+ }
356
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
357
+ const rows = this.stmt(`SELECT seq, session_id, type, time, data, ignorable, surface_op, source_event_seqs
358
+ FROM events ${where} ORDER BY time, session_id, seq LIMIT ?`).all(...params, limit + 1);
359
+ const hasMore = rows.length > limit;
360
+ const page = hasMore ? rows.slice(0, limit) : rows;
361
+ const events = [];
362
+ let last;
363
+ for (const row of page) {
364
+ try {
365
+ events.push(parseEventRow(row));
366
+ last = row;
367
+ }
368
+ catch (err) {
369
+ // 查询面不做 heal(那是 loadEvents 的恢复语义)——坏行跳过 + warn
370
+ this.warn(`[persist] queryEvents 跳过坏行(${row.session_id}#${row.seq}):${String(err)}`);
371
+ }
372
+ }
373
+ return {
374
+ events,
375
+ nextCursor: hasMore && last ? encodeCursor(last.time, last.session_id, last.seq) : null,
376
+ };
377
+ }
378
+ // ── sessions 行面 ──────────────────────────────────────────────────────────
379
+ /** 会话行查询(loadSession 的登记信息源) */
380
+ getSessionRow(sessionId) {
381
+ this.ensureOpen();
382
+ const row = this.stmt(`SELECT id, title, origin, parent_id, seed_length, workspace_root, created_at, updated_at, last_seq
383
+ FROM sessions WHERE id = ?`).get(sessionId);
384
+ return row ? parseSessionRow(row) : undefined;
385
+ }
386
+ /** 会话列表(updated_at 倒序;workspaceRoot 过滤 = 「按 cwd 取最新会话」选取面) */
387
+ listSessions(options = {}) {
388
+ this.ensureOpen();
389
+ const limit = options.limit ?? 100;
390
+ const rows = options.workspaceRoot !== undefined
391
+ ? this.stmt(`SELECT id, title, origin, parent_id, seed_length, workspace_root, created_at, updated_at, last_seq
392
+ FROM sessions WHERE workspace_root = ? ORDER BY updated_at DESC LIMIT ?`).all(options.workspaceRoot, limit)
393
+ : this.stmt(`SELECT id, title, origin, parent_id, seed_length, workspace_root, created_at, updated_at, last_seq
394
+ FROM sessions ORDER BY updated_at DESC LIMIT ?`).all(limit);
395
+ return rows.map(parseSessionRow);
396
+ }
397
+ /** 会话登记先行落行(空种子形态——last_seq=-1 无事件语义,与游标起点一致;
398
+ * 有事件走写路径。0 是「seq 0 已落」——空档若记 0,首事件 seq 0 会被连续性
399
+ * 断言当跳号拒写,恰成毒丸) */
400
+ registerSessionRow(sessionId, registration) {
401
+ this.ensureOpen();
402
+ const now = this.clock();
403
+ this.stmt(`INSERT INTO sessions (id, title, origin, parent_id, seed_length, workspace_root, created_at, updated_at, last_seq)
404
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, -1)
405
+ ON CONFLICT(id) DO NOTHING`).run(sessionId, registration.title ?? null, registration.origin, registration.parentId ?? null, registration.seedLength, registration.workspaceRoot ?? null, now, now);
406
+ }
407
+ /** 标题独立更新(auto-title 面——非事件路径,同步小写) */
408
+ updateSessionTitle(sessionId, title) {
409
+ this.ensureOpen();
410
+ return this.stmt(`UPDATE sessions SET title = ? WHERE id = ?`).run(title, sessionId).changes > 0;
411
+ }
412
+ /** 会话删除(§2.5 物理删除:events + fts + 行 三删同事务——FTS 删除对账第二档) */
413
+ deleteSession(sessionId) {
414
+ this.ensureOpen();
415
+ const tx = this.db.transaction(() => {
416
+ const gone = this.stmt(`DELETE FROM events WHERE session_id = ?`).run(sessionId).changes > 0;
417
+ this.stmt(`DELETE FROM session_fts WHERE session_id = ?`).run(sessionId);
418
+ this.stmt(`DELETE FROM sessions WHERE id = ?`).run(sessionId);
419
+ return gone;
420
+ });
421
+ const gone = tx();
422
+ this.cursors.delete(sessionId);
423
+ return gone;
424
+ }
425
+ // ── session_fts 面(对账三档:写入档在写路径 / 删除档在 deleteSession)──────
426
+ /** 会话内全文检索(首发口径:查询面限定 session_id——06 §10 引此为源) */
427
+ searchSessionFts(sessionId, pattern, limit = 50) {
428
+ this.ensureOpen();
429
+ // 检索式消毒:整体作为 FTS5 字符串字面量(内嵌引号双写)——任意用户输入
430
+ // 都成合法查询且语义 = 子串匹配(trigram);<3 字符 trigram 无从匹配返回空
431
+ const literal = `"${pattern.replace(/"/g, '""')}"`;
432
+ const rows = this.stmt(`SELECT seq FROM session_fts WHERE session_fts MATCH ? AND session_id = ? ORDER BY seq LIMIT ?`).all(literal, sessionId, limit);
433
+ return rows.map((row) => row.seq);
434
+ }
435
+ /** 跨会话全文检索(05 §9 追记①——查询面限定解除、索引面同源同索引;bm25 序;
436
+ * 返回命中行原文供消费侧切 snippet;消毒同会话内变体(字符串字面量引号双写)) */
437
+ searchFtsGlobal(pattern, limit = 50) {
438
+ this.ensureOpen();
439
+ const literal = `"${pattern.replace(/"/g, '""')}"`;
440
+ return this.stmt(`SELECT session_id AS sessionId, seq, body FROM session_fts
441
+ WHERE session_fts MATCH ? ORDER BY rank LIMIT ?`).all(literal, limit);
442
+ }
443
+ /** 全量重建(派生物不修不补——重建即修复;CLI 手动命令与审计缺口共用腿) */
444
+ rebuildFts() {
445
+ this.ensureOpen();
446
+ let sessions = 0;
447
+ let events = 0;
448
+ const tx = this.db.transaction(() => {
449
+ this.stmt(`DELETE FROM session_fts`).run();
450
+ const rows = this.stmt(`SELECT session_id, seq, type, data FROM events ORDER BY session_id, seq`).all();
451
+ const insert = this.stmt(`INSERT INTO session_fts (session_id, seq, body) VALUES (?, ?, ?)`);
452
+ let currentSession;
453
+ for (const row of rows) {
454
+ if (row.session_id !== currentSession) {
455
+ currentSession = row.session_id;
456
+ sessions++;
457
+ }
458
+ const body = ftsBodyOfRaw(row.type, row.data);
459
+ if (body !== null) {
460
+ insert.run(row.session_id, row.seq, body);
461
+ events++;
462
+ }
463
+ }
464
+ });
465
+ tx();
466
+ return { sessions, events };
467
+ }
468
+ /** 启动抽样对账(随机 N 会话行数比对——发现缺口由调用方触发全量重建) */
469
+ auditFts(sampleCount = 8) {
470
+ this.ensureOpen();
471
+ const sampled = this.stmt(`SELECT DISTINCT session_id FROM events ORDER BY RANDOM() LIMIT ?`).all(sampleCount);
472
+ const mismatches = [];
473
+ for (const { session_id } of sampled) {
474
+ const rows = this.stmt(`SELECT seq, type, data FROM events WHERE session_id = ? ORDER BY seq`).all(session_id);
475
+ let expected = 0;
476
+ for (const row of rows) {
477
+ if (ftsBodyOfRaw(row.type, row.data) !== null)
478
+ expected++;
479
+ }
480
+ const actual = this.stmt(`SELECT count(*) AS n FROM session_fts WHERE session_id = ?`).get(session_id).n;
481
+ if (expected !== actual)
482
+ mismatches.push({ sessionId: session_id, expected, actual });
483
+ }
484
+ return { checked: sampled.length, mismatches };
485
+ }
486
+ // ── store_state 面(插件持久键值统一面——受理执法在装载层,此处只供表与治理)──
487
+ /** 键值读(过期即视为缺 + 顺手清扫;读触达刷新 last_accessed_at——LRU 依据) */
488
+ getStoreState(key) {
489
+ this.ensureOpen();
490
+ this.sweepExpiredState();
491
+ const row = this.stmt(`SELECT key, value, expires_at, kind FROM store_state WHERE key = ?`).get(key);
492
+ if (!row)
493
+ return undefined;
494
+ if (row.expires_at !== null && row.expires_at <= this.clock()) {
495
+ // 竞速兜底(清扫窗口后又到点——读路径当场删)
496
+ this.stmt(`DELETE FROM store_state WHERE key = ?`).run(key);
497
+ return undefined;
498
+ }
499
+ this.stmt(`UPDATE store_state SET last_accessed_at = ? WHERE key = ?`).run(this.clock(), key);
500
+ return {
501
+ key: row.key,
502
+ value: JSON.parse(row.value),
503
+ kind: row.kind,
504
+ expiresAt: row.expires_at ?? undefined,
505
+ };
506
+ }
507
+ /** 键值写(upsert + LRU 帽执法 + 过期清扫三治一体,05 §6.2) */
508
+ setStoreState(key, value, options = {}) {
509
+ this.ensureOpen();
510
+ const safeValue = snapshotJsonValue(value, 'store_state.value');
511
+ const now = this.clock();
512
+ const expiresAt = options.ttlMs !== undefined ? now + options.ttlMs : null;
513
+ this.stmt(`INSERT INTO store_state (key, value, expires_at, kind, last_accessed_at)
514
+ VALUES (?, ?, ?, ?, ?)
515
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at,
516
+ kind = excluded.kind, last_accessed_at = excluded.last_accessed_at`).run(key, JSON.stringify(safeValue), expiresAt, options.kind ?? 'kv', now);
517
+ this.sweepExpiredState();
518
+ // LRU 帽:超帽即逐出最久未触达(新写键永不在逐出集——刚 touch 过)
519
+ const total = this.stmt(`SELECT count(*) AS n FROM store_state`).get().n;
520
+ if (total > STORE_STATE_LRU_CAP) {
521
+ this.stmt(`DELETE FROM store_state WHERE key IN (
522
+ SELECT key FROM store_state ORDER BY last_accessed_at ASC, key ASC LIMIT ?
523
+ ) AND key != ?`).run(total - STORE_STATE_LRU_CAP, key);
524
+ }
525
+ }
526
+ /** 键值删(终态删除面——job 条目结算即删的物理腿) */
527
+ deleteStoreState(key) {
528
+ this.ensureOpen();
529
+ return this.stmt(`DELETE FROM store_state WHERE key = ?`).run(key).changes > 0;
530
+ }
531
+ /** 过期清扫(顺手腿——读写路径捎带,无独立定时器) */
532
+ sweepExpiredState() {
533
+ this.stmt(`DELETE FROM store_state WHERE expires_at IS NOT NULL AND expires_at <= ?`).run(this.clock());
534
+ }
535
+ // ── credentials 面(密文盒消费——api_key 列只存密文;namespace 归属维
536
+ // 2026-09-08 c-2 扩容——03 §10.9/05 §9;'host' 宿主域 = 模型 key 与
537
+ // 静态人面凭证,'plugin:<id>' 插件域经 c-3 读腿受理位写入)────────────
538
+ /** 凭证写(加密 upsert;meta 须纯 JSON;namespace 归属域显式传) */
539
+ setCredential(namespace, provider, entry) {
540
+ this.ensureOpen();
541
+ const meta = entry.meta !== undefined ? snapshotJsonValue(entry.meta, 'credentials.meta') : null;
542
+ this.stmt(`INSERT INTO credentials (namespace, provider, api_key, meta, updated_at) VALUES (?, ?, ?, ?, ?)
543
+ ON CONFLICT(namespace, provider) DO UPDATE SET api_key = excluded.api_key, meta = excluded.meta,
544
+ updated_at = excluded.updated_at`).run(namespace, provider, encryptSecret(this.secretKey, entry.apiKey), meta === null ? null : JSON.stringify(meta), this.clock());
545
+ }
546
+ /** 凭证读(解密——密钥丢失/不匹配 fail-loud PERSIST_SECRET_UNREADABLE) */
547
+ getCredential(namespace, provider) {
548
+ this.ensureOpen();
549
+ const row = this.stmt(`SELECT namespace, provider, api_key, meta, updated_at FROM credentials
550
+ WHERE namespace = ? AND provider = ?`).get(namespace, provider);
551
+ if (!row)
552
+ return undefined;
553
+ return {
554
+ namespace: row.namespace,
555
+ provider: row.provider,
556
+ apiKey: decryptSecret(this.secretKey, row.api_key),
557
+ meta: row.meta === null ? undefined : JSON.parse(row.meta),
558
+ updatedAt: row.updated_at,
559
+ };
560
+ }
561
+ /** 凭证删(撤销唯一路径 = 人面 rm / 插件 revoke——保留律的对面) */
562
+ deleteCredential(namespace, provider) {
563
+ this.ensureOpen();
564
+ return (this.stmt(`DELETE FROM credentials WHERE namespace = ? AND provider = ?`).run(namespace, provider).changes > 0);
565
+ }
566
+ /** 凭证清单(不回 api_key——枚举面零密文零明文;全域列示:人面命令恒可
567
+ * 列示/撤销一切域——03 §10.9,namespace 分权不进治理面。meta 随行回
568
+ * 〔parsed JSON——c-5 人面 list 来源列 / expired 呈现位消费;写侧
569
+ * snapshotJsonValue 保证纯 JSON 可解析〕) */
570
+ listCredentialProviders() {
571
+ this.ensureOpen();
572
+ return this.stmt(`SELECT namespace, provider, meta, updated_at FROM credentials ORDER BY namespace, provider`).all().map((row) => ({
573
+ namespace: row.namespace,
574
+ provider: row.provider,
575
+ meta: row.meta === null ? undefined : JSON.parse(row.meta),
576
+ updatedAt: row.updated_at,
577
+ }));
578
+ }
579
+ // ── model_catalog 面 ────────────────────────────────────────────────────────
580
+ /** 模型目录写(06/07 篇消费的物理 CRUD 面) */
581
+ upsertModel(model) {
582
+ this.ensureOpen();
583
+ const meta = model.meta !== undefined ? snapshotJsonValue(model.meta, 'model.meta') : null;
584
+ this.stmt(`INSERT INTO model_catalog (id, provider, label, meta, updated_at) VALUES (?, ?, ?, ?, ?)
585
+ ON CONFLICT(id) DO UPDATE SET provider = excluded.provider, label = excluded.label,
586
+ meta = excluded.meta, updated_at = excluded.updated_at`).run(model.id, model.provider, model.label ?? null, meta === null ? null : JSON.stringify(meta), this.clock());
587
+ }
588
+ /** 模型目录读(provider 过滤可选) */
589
+ listModels(provider) {
590
+ this.ensureOpen();
591
+ const rows = provider !== undefined
592
+ ? this.stmt(`SELECT id, provider, label, meta, updated_at FROM model_catalog WHERE provider = ? ORDER BY id`).all(provider)
593
+ : this.stmt(`SELECT id, provider, label, meta, updated_at FROM model_catalog ORDER BY id`).all();
594
+ return rows.map((row) => ({
595
+ id: row.id,
596
+ provider: row.provider,
597
+ label: row.label ?? undefined,
598
+ meta: row.meta === null ? undefined : JSON.parse(row.meta),
599
+ updatedAt: row.updated_at,
600
+ }));
601
+ }
602
+ /** 模型目录删 */
603
+ deleteModel(id) {
604
+ this.ensureOpen();
605
+ return this.stmt(`DELETE FROM model_catalog WHERE id = ?`).run(id).changes > 0;
606
+ }
607
+ // ── 生命周期 ────────────────────────────────────────────────────────────────
608
+ /** 关库(flush 由调用方先行——persistence.close 编舞;此处只管收尾) */
609
+ close() {
610
+ if (this.closed)
611
+ return;
612
+ this.closed = true;
613
+ if (this.dbPath !== MEMORY_DB_PATH) {
614
+ checkpointTruncate(this.db, this.warn);
615
+ }
616
+ this.db.close();
617
+ }
618
+ /** 原生连接(宿主内消费位 = assembly createAuditFace〔audit_events 单写者〕;跨件勿散用——走 Store 方法面) */
619
+ get connection() {
620
+ return this.db;
621
+ }
622
+ /** 内存库判别(:memory: 形态的执法位消费——createSeededSession 拒绝面) */
623
+ get inMemory() {
624
+ return this.dbPath === MEMORY_DB_PATH;
625
+ }
626
+ }
627
+ /** 事件行 → 事件信封(JSON 列解析——失败即抛,调用方决定截断/跳过) */
628
+ function parseEventRow(row) {
629
+ const data = JSON.parse(row.data);
630
+ const surfaceOp = row.surface_op === null ? undefined : JSON.parse(row.surface_op);
631
+ const sourceEventSeqs = row.source_event_seqs === null ? undefined : JSON.parse(row.source_event_seqs);
632
+ return {
633
+ type: row.type,
634
+ seq: row.seq,
635
+ time: row.time,
636
+ data,
637
+ ...(row.ignorable ? { ignorable: true } : {}),
638
+ ...(surfaceOp ? { surfaceOp } : {}),
639
+ ...(sourceEventSeqs ? { sourceEventSeqs } : {}),
640
+ };
641
+ }
642
+ /** sessions 原始行 → 读形态(NULL 列归一 undefined) */
643
+ function parseSessionRow(row) {
644
+ return {
645
+ id: row.id,
646
+ title: row.title ?? undefined,
647
+ origin: row.origin,
648
+ parentId: row.parent_id ?? undefined,
649
+ seedLength: row.seed_length,
650
+ workspaceRoot: row.workspace_root ?? undefined,
651
+ createdAt: row.created_at,
652
+ updatedAt: row.updated_at,
653
+ lastSeq: row.last_seq,
654
+ };
655
+ }
656
+ // ── FTS body 抽取(索引面:全部 surface 类别事件——词汇注册表 category 判据)──
657
+ /** 事件信封 → FTS body(非 surface 类别返回 null 不入索引) */
658
+ function ftsBodyOf(event) {
659
+ return ftsBodyOfRaw(event.type, JSON.stringify(event.data));
660
+ }
661
+ /** 行形态直达抽取(重建/审计面复用——避免先 parse 整信封再 stringify) */
662
+ function ftsBodyOfRaw(type, dataJson) {
663
+ const meta = getEventTypeMeta(type);
664
+ if (!meta || meta.category !== 'surface')
665
+ return null;
666
+ const parts = [];
667
+ collectStringValues(JSON.parse(dataJson), parts, 0);
668
+ return parts.length > 0 ? parts.join(' ') : null;
669
+ }
670
+ /** 递归收集字符串值(只收值不收键——键名污染匹配面;深度帽防恶意嵌套) */
671
+ function collectStringValues(value, out, depth) {
672
+ if (depth > 8)
673
+ return;
674
+ if (typeof value === 'string') {
675
+ if (value.length > 0)
676
+ out.push(value);
677
+ return;
678
+ }
679
+ if (Array.isArray(value)) {
680
+ for (const item of value)
681
+ collectStringValues(item, out, depth + 1);
682
+ return;
683
+ }
684
+ if (value !== null && typeof value === 'object') {
685
+ for (const item of Object.values(value))
686
+ collectStringValues(item, out, depth + 1);
687
+ }
688
+ }
689
+ // ── 游标编解码(queryEvents 分页令牌——不透明:base64url(JSON))──────────────
690
+ /** 游标编码(上一页末行三元组) */
691
+ function encodeCursor(time, sessionId, seq) {
692
+ return Buffer.from(JSON.stringify({ t: time, s: sessionId, q: seq }), 'utf8').toString('base64url');
693
+ }
694
+ /** 游标解码(坏令牌 = 面向调用方的输入错误,fail-loud) */
695
+ function decodeCursor(cursor) {
696
+ try {
697
+ const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
698
+ if (typeof parsed.t !== 'number' || typeof parsed.s !== 'string' || typeof parsed.q !== 'number') {
699
+ throw new Error('shape');
700
+ }
701
+ return [parsed.t, parsed.s, parsed.q];
702
+ }
703
+ catch {
704
+ throw new BaseError('PERSIST_DATA_CORRUPT', `queryEvents 游标不可解码:${cursor.slice(0, 32)}…`);
705
+ }
706
+ }