supipowers 1.5.2 → 2.0.0

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 (340) hide show
  1. package/README.md +14 -8
  2. package/bin/install.mjs +20 -5
  3. package/bin/install.ts +95 -0
  4. package/package.json +8 -4
  5. package/skills/context-mode/SKILL.md +17 -10
  6. package/skills/harness/SKILL.md +94 -0
  7. package/skills/ui-design/SKILL.md +63 -0
  8. package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
  9. package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
  10. package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
  11. package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
  12. package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
  13. package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
  14. package/skills/ultraplan-discover/SKILL.md +96 -0
  15. package/skills/ultraplan-intake/SKILL.md +89 -0
  16. package/skills/ultraplan-research/SKILL.md +129 -0
  17. package/skills/ultraplan-review/SKILL.md +86 -0
  18. package/skills/ultraplan-review-scope/SKILL.md +111 -0
  19. package/skills/ultraplan-review-structure/SKILL.md +120 -0
  20. package/skills/ultraplan-review-tdd/SKILL.md +142 -0
  21. package/skills/ultraplan-scout/SKILL.md +110 -0
  22. package/skills/ultraplan-synthesize/SKILL.md +124 -0
  23. package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
  24. package/src/ai/schema-text.ts +129 -0
  25. package/src/ai/structured-output.ts +274 -0
  26. package/src/ai/template.ts +27 -0
  27. package/src/bootstrap.ts +63 -28
  28. package/src/commands/agents.ts +149 -45
  29. package/src/commands/ai-review.ts +251 -30
  30. package/src/commands/clear.ts +434 -0
  31. package/src/commands/commit.ts +1 -0
  32. package/src/commands/config.ts +242 -44
  33. package/src/commands/context.ts +55 -28
  34. package/src/commands/doctor.ts +234 -6
  35. package/src/commands/fix-pr.ts +306 -131
  36. package/src/commands/generate.ts +111 -21
  37. package/src/commands/memory.ts +192 -0
  38. package/src/commands/model-picker.ts +28 -21
  39. package/src/commands/model.ts +19 -9
  40. package/src/commands/optimize-context.ts +408 -29
  41. package/src/commands/plan.ts +2 -0
  42. package/src/commands/qa.ts +312 -137
  43. package/src/commands/release.ts +259 -76
  44. package/src/commands/review.ts +293 -59
  45. package/src/commands/status.ts +200 -13
  46. package/src/commands/supi.ts +3 -35
  47. package/src/commands/ui-design.ts +394 -0
  48. package/src/commands/ultraplan.ts +1518 -0
  49. package/src/commands/update.ts +86 -0
  50. package/src/config/defaults.ts +62 -0
  51. package/src/config/loader.ts +448 -60
  52. package/src/config/schema.ts +108 -2
  53. package/src/context/optimizer.ts +25 -33
  54. package/src/context/rule-renderer.ts +223 -0
  55. package/src/context/savings.ts +258 -0
  56. package/src/context/startup-check.ts +380 -0
  57. package/src/context/startup-optimizer.ts +355 -0
  58. package/src/context/tokenignore.ts +146 -0
  59. package/src/context-mode/cache-handle.ts +49 -0
  60. package/src/context-mode/cache-preview.ts +71 -0
  61. package/src/context-mode/cache-store.ts +738 -0
  62. package/src/context-mode/compressor.ts +131 -26
  63. package/src/context-mode/dedup.ts +108 -0
  64. package/src/context-mode/detector.ts +35 -4
  65. package/src/context-mode/event-extractor.ts +14 -12
  66. package/src/context-mode/event-store.ts +91 -36
  67. package/src/context-mode/hooks.ts +798 -56
  68. package/src/context-mode/knowledge/store.ts +255 -11
  69. package/src/context-mode/memory-store.ts +325 -0
  70. package/src/context-mode/metrics-recorder.ts +158 -0
  71. package/src/context-mode/metrics-store.ts +765 -0
  72. package/src/context-mode/model.ts +24 -0
  73. package/src/context-mode/processor-keys.ts +29 -0
  74. package/src/context-mode/processors/build.ts +66 -0
  75. package/src/context-mode/processors/docker.ts +57 -0
  76. package/src/context-mode/processors/git.ts +111 -0
  77. package/src/context-mode/processors/json.ts +112 -0
  78. package/src/context-mode/processors/k8s.ts +67 -0
  79. package/src/context-mode/processors/lint.ts +67 -0
  80. package/src/context-mode/processors/log.ts +86 -0
  81. package/src/context-mode/processors/registry.ts +116 -0
  82. package/src/context-mode/processors/test-runner.ts +102 -0
  83. package/src/context-mode/processors/types.ts +20 -0
  84. package/src/context-mode/repomap.ts +400 -0
  85. package/src/context-mode/routing.ts +97 -24
  86. package/src/context-mode/sandbox/runners.ts +5 -1
  87. package/src/context-mode/snapshot-builder.ts +106 -11
  88. package/src/context-mode/source-hash.ts +173 -0
  89. package/src/context-mode/tool-name.ts +11 -0
  90. package/src/context-mode/tools.ts +654 -22
  91. package/src/context-mode/web/fetcher.ts +31 -12
  92. package/src/debug/logger.ts +2 -1
  93. package/src/deps/registry.ts +1 -1
  94. package/src/discipline/failure-summarizer.ts +170 -0
  95. package/src/discipline/failure-taxonomy.ts +131 -0
  96. package/src/discipline/workflow-invariants.ts +125 -0
  97. package/src/discovery/index.ts +31 -0
  98. package/src/discovery/lsp.ts +87 -0
  99. package/src/discovery/rank.ts +144 -0
  100. package/src/discovery/sources.ts +89 -0
  101. package/src/discovery/workflow.ts +87 -0
  102. package/src/docs/contracts.ts +39 -0
  103. package/src/docs/drift.ts +117 -87
  104. package/src/fix-pr/assessment.ts +200 -0
  105. package/src/fix-pr/contracts.ts +47 -0
  106. package/src/fix-pr/fetch-comments.ts +80 -0
  107. package/src/fix-pr/prompt-builder.ts +58 -40
  108. package/src/fix-pr/scripts/exec.ts +34 -0
  109. package/src/fix-pr/scripts/trigger-review.ts +106 -0
  110. package/src/fix-pr/scripts/wait-and-check.ts +108 -0
  111. package/src/fix-pr/types.ts +4 -0
  112. package/src/git/branch-finish.ts +5 -0
  113. package/src/git/commit-contract.ts +83 -0
  114. package/src/git/commit.ts +121 -184
  115. package/src/git/status.ts +62 -8
  116. package/src/harness/anti_slop/architecture-parser.ts +210 -0
  117. package/src/harness/anti_slop/backend-factory.ts +30 -0
  118. package/src/harness/anti_slop/backend.ts +140 -0
  119. package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
  120. package/src/harness/anti_slop/fallow-adapter.ts +305 -0
  121. package/src/harness/anti_slop/installer.ts +227 -0
  122. package/src/harness/anti_slop/queue.ts +216 -0
  123. package/src/harness/anti_slop/recommend.ts +84 -0
  124. package/src/harness/anti_slop/score.ts +180 -0
  125. package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
  126. package/src/harness/artifacts/agents-md.ts +88 -0
  127. package/src/harness/artifacts/checks-wiring.ts +57 -0
  128. package/src/harness/artifacts/docs-tree.ts +79 -0
  129. package/src/harness/artifacts/lint-configs.ts +136 -0
  130. package/src/harness/artifacts/review-agents.ts +67 -0
  131. package/src/harness/bare-entry.ts +108 -0
  132. package/src/harness/command.ts +1010 -0
  133. package/src/harness/default-agents/design.md +23 -0
  134. package/src/harness/default-agents/discover.md +18 -0
  135. package/src/harness/default-agents/implement.md +24 -0
  136. package/src/harness/default-agents/plan.md +19 -0
  137. package/src/harness/default-agents/research.md +21 -0
  138. package/src/harness/default-agents/validate.md +22 -0
  139. package/src/harness/gc/reporter.ts +28 -0
  140. package/src/harness/gc/runner.ts +136 -0
  141. package/src/harness/hooks/layer-context-inject.ts +155 -0
  142. package/src/harness/hooks/post-session-sweep.ts +130 -0
  143. package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
  144. package/src/harness/hooks/register.ts +118 -0
  145. package/src/harness/model.ts +117 -0
  146. package/src/harness/pipeline.ts +348 -0
  147. package/src/harness/project-paths.ts +235 -0
  148. package/src/harness/stage-runner.ts +107 -0
  149. package/src/harness/stages/design.ts +386 -0
  150. package/src/harness/stages/discover.ts +454 -0
  151. package/src/harness/stages/implement.ts +162 -0
  152. package/src/harness/stages/plan.ts +335 -0
  153. package/src/harness/stages/research.ts +263 -0
  154. package/src/harness/stages/validate.ts +684 -0
  155. package/src/harness/storage.ts +467 -0
  156. package/src/harness/tools.ts +426 -0
  157. package/src/lsp/bridge.ts +56 -95
  158. package/src/lsp/capabilities.ts +108 -0
  159. package/src/lsp/contracts.ts +35 -0
  160. package/src/lsp/detector.ts +8 -12
  161. package/src/markdown-frontmatter.ts +68 -0
  162. package/src/mempalace/bridge.ts +129 -0
  163. package/src/mempalace/config.ts +75 -0
  164. package/src/mempalace/format.ts +163 -0
  165. package/src/mempalace/hooks.ts +370 -0
  166. package/src/mempalace/installer-helper.ts +194 -0
  167. package/src/mempalace/python/mempalace_bridge.py +440 -0
  168. package/src/mempalace/runtime.ts +565 -0
  169. package/src/mempalace/schema.ts +264 -0
  170. package/src/mempalace/session-summary.ts +198 -0
  171. package/src/mempalace/tool.ts +186 -0
  172. package/src/mempalace/uv.ts +256 -0
  173. package/src/migrate/runner.ts +354 -0
  174. package/src/planning/approval-flow.ts +206 -9
  175. package/src/planning/plan-writer-prompt.ts +4 -3
  176. package/src/planning/planning-ask-tool.ts +39 -0
  177. package/src/planning/render-markdown.ts +74 -0
  178. package/src/planning/spec.ts +42 -0
  179. package/src/planning/system-prompt.ts +11 -8
  180. package/src/planning/validate.ts +84 -0
  181. package/src/platform/omp.ts +15 -2
  182. package/src/platform/system-prompt.ts +37 -0
  183. package/src/platform/test-utils.ts +3 -0
  184. package/src/platform/types.ts +6 -1
  185. package/src/qa/config.ts +12 -6
  186. package/src/qa/detect-app-type.ts +13 -6
  187. package/src/qa/matrix.ts +12 -6
  188. package/src/qa/prompt-builder.ts +28 -30
  189. package/src/qa/scripts/dev-server-utils.ts +72 -0
  190. package/src/qa/scripts/run-e2e-tests.ts +226 -0
  191. package/src/qa/scripts/start-dev-server.ts +138 -0
  192. package/src/qa/scripts/stop-dev-server.ts +77 -0
  193. package/src/qa/session.ts +13 -7
  194. package/src/quality/ai-setup.ts +27 -25
  195. package/src/quality/contracts.ts +34 -0
  196. package/src/quality/gates/ai-review.ts +20 -58
  197. package/src/quality/gates/command.ts +249 -46
  198. package/src/quality/review-gates.ts +18 -2
  199. package/src/quality/runner.ts +63 -22
  200. package/src/quality/schemas.ts +37 -2
  201. package/src/quality/setup.ts +96 -16
  202. package/src/release/changelog.ts +1 -1
  203. package/src/release/channels/custom.ts +13 -3
  204. package/src/release/channels/types.ts +5 -0
  205. package/src/release/contracts.ts +90 -0
  206. package/src/release/executor.ts +122 -45
  207. package/src/release/prompt.ts +18 -2
  208. package/src/release/targets.ts +86 -0
  209. package/src/release/version.ts +96 -71
  210. package/src/review/agent-loader.ts +298 -127
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +115 -14
  213. package/src/review/output.ts +12 -139
  214. package/src/review/runner.ts +12 -6
  215. package/src/review/scope.ts +144 -24
  216. package/src/review/types.ts +11 -20
  217. package/src/review/validator.ts +12 -6
  218. package/src/storage/fix-pr-sessions.ts +21 -14
  219. package/src/storage/plans.ts +14 -5
  220. package/src/storage/qa-sessions.ts +25 -19
  221. package/src/storage/reliability-metrics.ts +180 -0
  222. package/src/storage/reports.ts +8 -7
  223. package/src/storage/review-sessions.ts +55 -20
  224. package/src/tool-catalog/active-tool-controller.ts +164 -0
  225. package/src/tool-catalog/active-tool-planner.ts +212 -0
  226. package/src/tool-catalog/tool-groups.ts +102 -0
  227. package/src/types.ts +1401 -5
  228. package/src/ui-design/backend-adapter.ts +78 -0
  229. package/src/ui-design/backends/local-html.ts +82 -0
  230. package/src/ui-design/backends/pencil-mcp.ts +111 -0
  231. package/src/ui-design/components-scanner.ts +124 -0
  232. package/src/ui-design/config.ts +55 -0
  233. package/src/ui-design/pen-scanner.ts +95 -0
  234. package/src/ui-design/pen-selector.ts +72 -0
  235. package/src/ui-design/prompt-builder.ts +73 -0
  236. package/src/ui-design/scanner.ts +136 -0
  237. package/src/ui-design/session.ts +974 -0
  238. package/src/ui-design/system-prompt.ts +312 -0
  239. package/src/ui-design/tokens-scanner.ts +181 -0
  240. package/src/ui-design/types.ts +96 -0
  241. package/src/ultraplan/agent-catalog.ts +522 -0
  242. package/src/ultraplan/authoring/agent-catalog.ts +310 -0
  243. package/src/ultraplan/authoring/authoring-tools.ts +552 -0
  244. package/src/ultraplan/authoring/command-handlers.ts +339 -0
  245. package/src/ultraplan/authoring/markdown.ts +510 -0
  246. package/src/ultraplan/authoring/model.ts +162 -0
  247. package/src/ultraplan/authoring/pipeline.ts +319 -0
  248. package/src/ultraplan/authoring/stage-runner.ts +141 -0
  249. package/src/ultraplan/authoring/stages/approve.ts +249 -0
  250. package/src/ultraplan/authoring/stages/discover.ts +289 -0
  251. package/src/ultraplan/authoring/stages/intake.ts +203 -0
  252. package/src/ultraplan/authoring/stages/research.ts +399 -0
  253. package/src/ultraplan/authoring/stages/review.ts +333 -0
  254. package/src/ultraplan/authoring/stages/scout.ts +188 -0
  255. package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
  256. package/src/ultraplan/authoring/storage.ts +594 -0
  257. package/src/ultraplan/authoring/synth-gate.ts +165 -0
  258. package/src/ultraplan/authoring-draft.ts +653 -0
  259. package/src/ultraplan/authoring-persist.ts +180 -0
  260. package/src/ultraplan/authoring-tool.ts +608 -0
  261. package/src/ultraplan/authoring-wizard.ts +587 -0
  262. package/src/ultraplan/batch/merge.ts +98 -0
  263. package/src/ultraplan/batch/planner.ts +150 -0
  264. package/src/ultraplan/batch/presenter.ts +97 -0
  265. package/src/ultraplan/batch/storage.ts +420 -0
  266. package/src/ultraplan/batch/supervisor.ts +317 -0
  267. package/src/ultraplan/batch/worker.ts +26 -0
  268. package/src/ultraplan/batch/worktree.ts +110 -0
  269. package/src/ultraplan/contracts.ts +1593 -0
  270. package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
  271. package/src/ultraplan/default-agents/authoring/intake.md +12 -0
  272. package/src/ultraplan/default-agents/authoring/planner.md +12 -0
  273. package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
  274. package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
  275. package/src/ultraplan/default-agents/authoring/scout.md +12 -0
  276. package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
  277. package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
  278. package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
  279. package/src/ultraplan/default-agents/backend-executor.md +10 -0
  280. package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
  281. package/src/ultraplan/default-agents/backend-tester.md +10 -0
  282. package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
  283. package/src/ultraplan/default-agents/frontend-executor.md +10 -0
  284. package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
  285. package/src/ultraplan/default-agents/frontend-tester.md +10 -0
  286. package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
  287. package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
  288. package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
  289. package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
  290. package/src/ultraplan/execution/contract.ts +71 -0
  291. package/src/ultraplan/execution/policy.ts +217 -0
  292. package/src/ultraplan/execution/runtime-tools.ts +107 -0
  293. package/src/ultraplan/execution/session-runner.ts +281 -0
  294. package/src/ultraplan/next-router.ts +85 -0
  295. package/src/ultraplan/presenter.ts +359 -0
  296. package/src/ultraplan/project-paths.ts +342 -0
  297. package/src/ultraplan/runtime/active-execution.ts +72 -0
  298. package/src/ultraplan/runtime/apply-mutation.ts +416 -0
  299. package/src/ultraplan/runtime/blockers.ts +243 -0
  300. package/src/ultraplan/runtime/hook-bridge.ts +486 -0
  301. package/src/ultraplan/runtime/launch-context.ts +207 -0
  302. package/src/ultraplan/runtime/migration.ts +524 -0
  303. package/src/ultraplan/runtime/normalize.ts +281 -0
  304. package/src/ultraplan/runtime/proof.ts +260 -0
  305. package/src/ultraplan/runtime/reducer.ts +416 -0
  306. package/src/ultraplan/runtime/repair.ts +251 -0
  307. package/src/ultraplan/runtime/tracker-storage.ts +368 -0
  308. package/src/ultraplan/session-selection.ts +291 -0
  309. package/src/ultraplan/storage.ts +374 -0
  310. package/src/utils/editor.ts +38 -0
  311. package/src/utils/executable.ts +80 -0
  312. package/src/utils/paths.ts +1 -20
  313. package/src/utils/shell.ts +31 -0
  314. package/src/visual/companion.ts +2 -1
  315. package/src/visual/scripts/frame-template.html +60 -0
  316. package/src/visual/scripts/index.js +59 -13
  317. package/src/visual/scripts/package.json +3 -0
  318. package/src/visual/start-server.ts +2 -1
  319. package/src/workspace/git-scope.ts +64 -0
  320. package/src/workspace/locks.ts +23 -0
  321. package/src/workspace/package-manager.ts +117 -0
  322. package/src/workspace/path-mapping.ts +75 -0
  323. package/src/workspace/project-slug.ts +92 -0
  324. package/src/workspace/repo-root.ts +137 -0
  325. package/src/workspace/selector.ts +115 -0
  326. package/src/workspace/state-paths.ts +118 -0
  327. package/src/workspace/targets.ts +313 -0
  328. package/src/fix-pr/scripts/diff-comments.sh +0 -33
  329. package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
  330. package/src/fix-pr/scripts/trigger-review.sh +0 -36
  331. package/src/fix-pr/scripts/wait-and-check.sh +0 -37
  332. package/src/qa/scripts/detect-app-type.sh +0 -68
  333. package/src/qa/scripts/discover-routes.sh +0 -143
  334. package/src/qa/scripts/run-e2e-tests.sh +0 -131
  335. package/src/qa/scripts/start-dev-server.sh +0 -46
  336. package/src/qa/scripts/stop-dev-server.sh +0 -36
  337. package/src/review/prompts/fix-output-schema.md +0 -18
  338. package/src/review/prompts/review-output-schema.md +0 -38
  339. package/src/review/template.ts +0 -15
  340. /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
@@ -1,11 +1,14 @@
1
- import { Database } from "bun:sqlite";
1
+ import { constants, Database } from "bun:sqlite";
2
2
  import fs from "node:fs";
3
+ import type { KnowledgeOwner, KnowledgeOwnerScope } from "../../types.js";
3
4
  import type { Chunk } from "./chunker.js";
4
5
 
5
6
  export interface SearchOptions {
6
7
  source?: string;
7
8
  contentType?: "code" | "prose";
8
9
  limit?: number;
10
+ owner?: KnowledgeOwner;
11
+ includeAllSessions?: boolean;
9
12
  }
10
13
 
11
14
  export interface SearchResult {
@@ -14,6 +17,8 @@ export interface SearchResult {
14
17
  source: string;
15
18
  contentType: string;
16
19
  score: number;
20
+ ownerScope: KnowledgeOwnerScope;
21
+ ownerId: string;
17
22
  }
18
23
 
19
24
  export interface QueryGroupedResults {
@@ -27,13 +32,22 @@ export interface StoreStats {
27
32
  dbSizeBytes: number;
28
33
  }
29
34
 
35
+ export interface KnowledgeClearResult {
36
+ chunksDeleted: number;
37
+ urlCacheDeleted: number;
38
+ }
39
+
40
+ const SCHEMA_VERSION = 2;
41
+
30
42
  const SCHEMA = `
31
43
  CREATE TABLE IF NOT EXISTS content_chunks (
32
44
  id INTEGER PRIMARY KEY AUTOINCREMENT,
33
45
  source TEXT NOT NULL,
34
46
  title TEXT NOT NULL DEFAULT '',
35
47
  body TEXT NOT NULL,
36
- content_type TEXT NOT NULL DEFAULT 'prose'
48
+ content_type TEXT NOT NULL DEFAULT 'prose',
49
+ owner_scope TEXT NOT NULL DEFAULT 'project',
50
+ owner_id TEXT NOT NULL DEFAULT ''
37
51
  );
38
52
 
39
53
  CREATE VIRTUAL TABLE IF NOT EXISTS content_chunks_fts USING fts5(
@@ -52,42 +66,145 @@ CREATE TRIGGER IF NOT EXISTS content_chunks_ad AFTER DELETE ON content_chunks BE
52
66
  INSERT INTO content_chunks_fts(content_chunks_fts, rowid, title, body) VALUES ('delete', old.id, old.title, old.body);
53
67
  END;
54
68
 
69
+ CREATE INDEX IF NOT EXISTS idx_content_chunks_owner ON content_chunks(owner_scope, owner_id);
70
+ CREATE INDEX IF NOT EXISTS idx_content_chunks_source_owner ON content_chunks(source, owner_scope, owner_id);
71
+
55
72
  CREATE TABLE IF NOT EXISTS url_cache (
56
73
  url TEXT NOT NULL,
57
74
  source TEXT NOT NULL,
75
+ owner_scope TEXT NOT NULL DEFAULT 'project',
76
+ owner_id TEXT NOT NULL DEFAULT '',
58
77
  fetched_at INTEGER NOT NULL,
59
- PRIMARY KEY (url, source)
78
+ PRIMARY KEY (url, source, owner_scope, owner_id)
60
79
  );
80
+
81
+ CREATE INDEX IF NOT EXISTS idx_url_cache_owner ON url_cache(owner_scope, owner_id);
61
82
  `;
62
83
 
84
+ const PROJECT_OWNER: Required<KnowledgeOwner> = { ownerScope: "project", ownerId: "" };
85
+
63
86
  export class KnowledgeStore {
64
87
  private _db: Database;
65
88
  private dbPath: string;
89
+ #closed = false;
66
90
 
67
91
  /** Public accessor for direct SQL on extension tables (e.g. url_cache). */
68
92
  get db(): Database {
69
93
  return this._db;
70
94
  }
71
95
 
96
+ get path(): string {
97
+ return this.dbPath;
98
+ }
99
+
72
100
  constructor(dbPath: string) {
73
101
  this.dbPath = dbPath;
74
102
  this._db = new Database(dbPath);
75
- this._db.exec("PRAGMA journal_mode=WAL");
76
103
  }
77
104
 
78
105
  init(): void {
106
+ this.#ensureDeleteJournalMode();
107
+ this.#migrate();
79
108
  this._db.exec(SCHEMA);
109
+ this._db.exec("INSERT INTO content_chunks_fts(content_chunks_fts) VALUES('rebuild')");
110
+ }
111
+
112
+ #ensureDeleteJournalMode(): void {
113
+ const journalMode = this.#getJournalMode();
114
+ if (journalMode === "delete") return;
115
+
116
+ if (journalMode === "wal") {
117
+ this.#cleanupWalSidecars();
118
+ }
119
+
120
+ try {
121
+ this._db.exec("PRAGMA journal_mode = DELETE");
122
+ } catch {
123
+ // Older WAL-backed databases can stay on WAL for this process.
124
+ // close() still checkpoints them so teardown and the next reopen succeed.
125
+ }
126
+ }
127
+
128
+ #getJournalMode(): string {
129
+ const { journal_mode } = this._db.prepare("PRAGMA journal_mode").get() as {
130
+ journal_mode: string;
131
+ };
132
+ return journal_mode.toLowerCase();
80
133
  }
81
- index(chunks: Chunk[], source: string): void {
82
- const del = this._db.prepare("DELETE FROM content_chunks WHERE source = ?");
134
+
135
+ #cleanupWalSidecars(): void {
136
+ try {
137
+ this._db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);
138
+ this._db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
139
+ } catch {
140
+ // Best effort only: close() still releases the handle in finally.
141
+ }
142
+ }
143
+
144
+ #migrate(): void {
145
+ const { user_version } = this._db.prepare("PRAGMA user_version").get() as {
146
+ user_version: number;
147
+ };
148
+ if (user_version >= SCHEMA_VERSION) return;
149
+
150
+ const hasContentChunks = tableExists(this._db, "content_chunks");
151
+ if (hasContentChunks) {
152
+ addColumnIfMissing(this._db, "content_chunks", "owner_scope", "TEXT NOT NULL DEFAULT 'legacy'");
153
+ addColumnIfMissing(this._db, "content_chunks", "owner_id", "TEXT NOT NULL DEFAULT ''");
154
+ this._db.prepare(
155
+ `UPDATE content_chunks
156
+ SET owner_scope = 'legacy'
157
+ WHERE owner_scope IS NULL OR owner_scope = ''`,
158
+ ).run();
159
+ }
160
+
161
+ const hasUrlCache = tableExists(this._db, "url_cache");
162
+ if (hasUrlCache && !columnExists(this._db, "url_cache", "owner_scope")) {
163
+ this._db.exec(`
164
+ CREATE TABLE IF NOT EXISTS url_cache_v2 (
165
+ url TEXT NOT NULL,
166
+ source TEXT NOT NULL,
167
+ owner_scope TEXT NOT NULL DEFAULT 'legacy',
168
+ owner_id TEXT NOT NULL DEFAULT '',
169
+ fetched_at INTEGER NOT NULL,
170
+ PRIMARY KEY (url, source, owner_scope, owner_id)
171
+ );
172
+ INSERT OR REPLACE INTO url_cache_v2 (url, source, owner_scope, owner_id, fetched_at)
173
+ SELECT url, source, 'legacy', '', fetched_at FROM url_cache;
174
+ DROP TABLE url_cache;
175
+ ALTER TABLE url_cache_v2 RENAME TO url_cache;
176
+ `);
177
+ }
178
+
179
+ this._db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
180
+ }
181
+
182
+ index(chunks: Chunk[], source: string, owner?: KnowledgeOwner): void {
183
+ const resolvedOwner = normalizeOwner(owner);
184
+ const del = this._db.prepare(
185
+ `DELETE FROM content_chunks
186
+ WHERE source = ?
187
+ AND (
188
+ (owner_scope = ? AND owner_id = ?)
189
+ OR (? = 'project' AND owner_scope = 'legacy')
190
+ )`,
191
+ );
83
192
  const ins = this._db.prepare(
84
- "INSERT INTO content_chunks (source, title, body, content_type) VALUES (?, ?, ?, ?)",
193
+ `INSERT INTO content_chunks (source, title, body, content_type, owner_scope, owner_id)
194
+ VALUES (?, ?, ?, ?, ?, ?)`,
85
195
  );
86
196
 
87
197
  this._db.transaction(() => {
88
- del.run(source);
198
+ del.run(source, resolvedOwner.ownerScope, resolvedOwner.ownerId, resolvedOwner.ownerScope);
89
199
  for (const chunk of chunks) {
90
- ins.run(source, chunk.title, chunk.body, chunk.contentType);
200
+ ins.run(
201
+ source,
202
+ chunk.title,
203
+ chunk.body,
204
+ chunk.contentType,
205
+ resolvedOwner.ownerScope,
206
+ resolvedOwner.ownerId,
207
+ );
91
208
  }
92
209
  })();
93
210
  }
@@ -107,6 +224,8 @@ export class KnowledgeStore {
107
224
 
108
225
  let sql = `
109
226
  SELECT c.title, c.body, c.source, c.content_type AS contentType,
227
+ c.owner_scope AS ownerScope,
228
+ c.owner_id AS ownerId,
110
229
  bm25(content_chunks_fts, 5.0, 1.0) AS score
111
230
  FROM content_chunks_fts f
112
231
  JOIN content_chunks c ON c.id = f.rowid
@@ -123,6 +242,12 @@ export class KnowledgeStore {
123
242
  params.push(options.contentType);
124
243
  }
125
244
 
245
+ const visibility = buildVisibilityClause(options);
246
+ if (visibility) {
247
+ sql += ` AND ${visibility.sql}`;
248
+ params.push(...visibility.params);
249
+ }
250
+
126
251
  sql += " ORDER BY score LIMIT ?";
127
252
  params.push(limit);
128
253
 
@@ -149,6 +274,73 @@ export class KnowledgeStore {
149
274
  return count;
150
275
  }
151
276
 
277
+ listSessions(): { session_id: string; chunk_count: number; url_cache_count: number }[] {
278
+ const merged = new Map<string, { session_id: string; chunk_count: number; url_cache_count: number }>();
279
+ const chunkRows = this._db.prepare(
280
+ `SELECT owner_id AS session_id, COUNT(*) AS chunk_count
281
+ FROM content_chunks
282
+ WHERE owner_scope = 'session'
283
+ GROUP BY owner_id`,
284
+ ).all() as Array<{ session_id: string; chunk_count: number }>;
285
+ const urlRows = this._db.prepare(
286
+ `SELECT owner_id AS session_id, COUNT(*) AS url_cache_count
287
+ FROM url_cache
288
+ WHERE owner_scope = 'session'
289
+ GROUP BY owner_id`,
290
+ ).all() as Array<{ session_id: string; url_cache_count: number }>;
291
+
292
+ for (const row of chunkRows) {
293
+ merged.set(row.session_id, {
294
+ session_id: row.session_id,
295
+ chunk_count: row.chunk_count,
296
+ url_cache_count: 0,
297
+ });
298
+ }
299
+ for (const row of urlRows) {
300
+ const existing = merged.get(row.session_id);
301
+ if (existing) {
302
+ existing.url_cache_count = row.url_cache_count;
303
+ } else {
304
+ merged.set(row.session_id, {
305
+ session_id: row.session_id,
306
+ chunk_count: 0,
307
+ url_cache_count: row.url_cache_count,
308
+ });
309
+ }
310
+ }
311
+ return [...merged.values()].sort((a, b) => a.session_id.localeCompare(b.session_id));
312
+ }
313
+
314
+ clearSession(ownerId: string): KnowledgeClearResult {
315
+ const chunks = this._db.prepare(
316
+ "SELECT COUNT(*) AS cnt FROM content_chunks WHERE owner_scope = 'session' AND owner_id = ?",
317
+ ).get(ownerId) as { cnt: number };
318
+ const urls = this._db.prepare(
319
+ "SELECT COUNT(*) AS cnt FROM url_cache WHERE owner_scope = 'session' AND owner_id = ?",
320
+ ).get(ownerId) as { cnt: number };
321
+ this._db.prepare(
322
+ "DELETE FROM content_chunks WHERE owner_scope = 'session' AND owner_id = ?",
323
+ ).run(ownerId);
324
+ this._db.prepare(
325
+ "DELETE FROM url_cache WHERE owner_scope = 'session' AND owner_id = ?",
326
+ ).run(ownerId);
327
+ this._db.exec("INSERT INTO content_chunks_fts(content_chunks_fts) VALUES('rebuild')");
328
+ return { chunksDeleted: chunks.cnt, urlCacheDeleted: urls.cnt };
329
+ }
330
+
331
+ clearProject(): KnowledgeClearResult {
332
+ const chunks = this._db.prepare("SELECT COUNT(*) AS cnt FROM content_chunks").get() as {
333
+ cnt: number;
334
+ };
335
+ const urls = this._db.prepare("SELECT COUNT(*) AS cnt FROM url_cache").get() as {
336
+ cnt: number;
337
+ };
338
+ this._db.exec("DELETE FROM content_chunks");
339
+ this._db.exec("DELETE FROM url_cache");
340
+ this._db.exec("INSERT INTO content_chunks_fts(content_chunks_fts) VALUES('rebuild')");
341
+ return { chunksDeleted: chunks.cnt, urlCacheDeleted: urls.cnt };
342
+ }
343
+
152
344
  getStats(): StoreStats {
153
345
  const countRow = this._db.prepare("SELECT COUNT(*) AS cnt FROM content_chunks").get() as {
154
346
  cnt: number;
@@ -168,14 +360,66 @@ export class KnowledgeStore {
168
360
  pruneExpiredUrls(ttlHours = 24): number {
169
361
  const cutoff = Math.floor(Date.now() / 1000) - ttlHours * 3600;
170
362
  const result = this._db.prepare("DELETE FROM url_cache WHERE fetched_at < ?").run(cutoff);
171
- return result.changes;
363
+ return result.changes as number;
172
364
  }
173
365
 
174
366
  close(): void {
175
- this._db.close();
367
+ if (this.#closed) return;
368
+
369
+ try {
370
+ try {
371
+ if (this.#getJournalMode() === "wal") {
372
+ this.#cleanupWalSidecars();
373
+ }
374
+ } catch {
375
+ // The DB path may already be gone during teardown; still close the handle.
376
+ }
377
+ } finally {
378
+ this._db.close();
379
+ this.#closed = true;
380
+ }
176
381
  }
177
382
  }
178
383
 
384
+ function normalizeOwner(owner: KnowledgeOwner | undefined): Required<KnowledgeOwner> {
385
+ if (!owner) return PROJECT_OWNER;
386
+ return {
387
+ ownerScope: owner.ownerScope,
388
+ ownerId: owner.ownerScope === "session" ? owner.ownerId ?? "" : owner.ownerId ?? "",
389
+ };
390
+ }
391
+
392
+ function buildVisibilityClause(options: SearchOptions | undefined): { sql: string; params: string[] } | null {
393
+ if (options?.includeAllSessions) return null;
394
+
395
+ const owner = options?.owner;
396
+ if (owner?.ownerScope === "session") {
397
+ return {
398
+ sql: "(c.owner_scope IN ('project', 'legacy') OR (c.owner_scope = 'session' AND c.owner_id = ?))",
399
+ params: [owner.ownerId ?? ""],
400
+ };
401
+ }
402
+
403
+ return {
404
+ sql: "c.owner_scope IN ('project', 'legacy')",
405
+ params: [],
406
+ };
407
+ }
408
+
409
+ function tableExists(db: Database, table: string): boolean {
410
+ return db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(table) != null;
411
+ }
412
+
413
+ function columnExists(db: Database, table: string, column: string): boolean {
414
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
415
+ return rows.some((row) => row.name === column);
416
+ }
417
+
418
+ function addColumnIfMissing(db: Database, table: string, column: string, definition: string): void {
419
+ if (columnExists(db, table, column)) return;
420
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
421
+ }
422
+
179
423
  /** Strip FTS5 special operators to prevent syntax errors. Keep alphanumeric + spaces. */
180
424
  function sanitizeFtsQuery(query: string): string {
181
425
  // Remove characters that have special meaning in FTS5: ^, *, ", (, ), {, }, +, -
@@ -0,0 +1,325 @@
1
+ // src/context-mode/memory-store.ts
2
+ //
3
+ // L5 cross-session memory store. Keeps durable observations/decisions/tasks
4
+ // per project under <projectStateDir>/sessions/memory.db. Mirrors the
5
+ // event-store/metrics-store conventions: DELETE journal mode, idempotent
6
+ // migration, best-effort failures, content-addressed dedup, and bounded
7
+ // retrieval.
8
+ //
9
+ // Privacy contract:
10
+ // - Every row carries an explicit `owner_scope` (`session` | `project`) and
11
+ // `owner_id`. Session clear deletes session-owned rows for the active
12
+ // session and stamps a clear epoch so project-owned rows that pre-date the
13
+ // epoch are filtered out of subsequent injection for that session.
14
+ // - Project clear deletes every row and every epoch for the project.
15
+
16
+ import { Database } from "bun:sqlite";
17
+ import { createHash } from "node:crypto";
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+
21
+ export const SCHEMA_VERSION = 1;
22
+
23
+ export type MemoryOwnerScope = "session" | "project";
24
+ export type MemoryType = "observation" | "decision" | "task";
25
+
26
+ export interface MemoryStoreOptions {
27
+ dbPath: string;
28
+ projectSlug: string;
29
+ }
30
+
31
+ export interface MemoryRow {
32
+ id: number;
33
+ ownerScope: MemoryOwnerScope;
34
+ ownerId: string;
35
+ type: MemoryType;
36
+ body: string;
37
+ bodyHash: string;
38
+ priority: number;
39
+ createdAt: number;
40
+ }
41
+
42
+ export interface MemoryPutInput {
43
+ ownerScope: MemoryOwnerScope;
44
+ ownerId?: string;
45
+ type: MemoryType;
46
+ body: string;
47
+ priority?: number;
48
+ now?: number;
49
+ }
50
+
51
+ export interface MemoryRetrieveOptions {
52
+ sessionId: string;
53
+ byteBudget?: number;
54
+ limit?: number;
55
+ now?: number;
56
+ }
57
+
58
+ const DEFAULT_PRIORITY = 3;
59
+ const DEFAULT_BYTE_BUDGET = 4 * 1024;
60
+ const DEFAULT_LIMIT = 25;
61
+ const RETENTION_DAYS = 30;
62
+
63
+ const SCHEMA = `
64
+ CREATE TABLE IF NOT EXISTS memory_entries (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ owner_scope TEXT NOT NULL,
67
+ owner_id TEXT NOT NULL DEFAULT '',
68
+ type TEXT NOT NULL,
69
+ body TEXT NOT NULL,
70
+ body_hash TEXT NOT NULL,
71
+ priority INTEGER NOT NULL DEFAULT 3,
72
+ created_at INTEGER NOT NULL,
73
+ UNIQUE(owner_scope, owner_id, type, body_hash)
74
+ );
75
+
76
+ CREATE INDEX IF NOT EXISTS idx_memory_owner ON memory_entries(owner_scope, owner_id);
77
+ CREATE INDEX IF NOT EXISTS idx_memory_priority ON memory_entries(priority, created_at);
78
+
79
+ CREATE TABLE IF NOT EXISTS memory_clear_epochs (
80
+ session_id TEXT PRIMARY KEY,
81
+ cleared_at INTEGER NOT NULL
82
+ );
83
+
84
+ CREATE TABLE IF NOT EXISTS memory_meta (
85
+ project_slug TEXT PRIMARY KEY,
86
+ last_prune_at INTEGER,
87
+ last_project_clear_at INTEGER
88
+ );
89
+ `;
90
+
91
+ export class MemoryStore {
92
+ readonly #dbPath: string;
93
+ readonly #projectSlug: string;
94
+ #db: Database;
95
+ #closed = false;
96
+
97
+ constructor(opts: MemoryStoreOptions) {
98
+ this.#dbPath = opts.dbPath;
99
+ this.#projectSlug = opts.projectSlug;
100
+ fs.mkdirSync(path.dirname(opts.dbPath), { recursive: true });
101
+ this.#db = new Database(opts.dbPath);
102
+ }
103
+
104
+ get dbPath(): string {
105
+ return this.#dbPath;
106
+ }
107
+
108
+ get projectSlug(): string {
109
+ return this.#projectSlug;
110
+ }
111
+
112
+ init(): void {
113
+ try {
114
+ this.#db.exec("PRAGMA journal_mode = DELETE;");
115
+ } catch {
116
+ // Older WAL-backed databases stay on WAL until close().
117
+ }
118
+ this.#db.exec(SCHEMA);
119
+ this.#migrate();
120
+ }
121
+
122
+ #migrate(): void {
123
+ const { user_version } = this.#db.prepare("PRAGMA user_version").get() as {
124
+ user_version: number;
125
+ };
126
+ if (user_version === SCHEMA_VERSION) return;
127
+ if (user_version > SCHEMA_VERSION) {
128
+ throw new Error(
129
+ `memory-store: unknown schema version ${user_version} (max supported: ${SCHEMA_VERSION})`,
130
+ );
131
+ }
132
+ this.#db.exec(`PRAGMA user_version = ${SCHEMA_VERSION};`);
133
+ }
134
+
135
+ put(input: MemoryPutInput): MemoryRow | null {
136
+ this.#assertOpen();
137
+ const body = input.body.trim();
138
+ if (!body) return null;
139
+ const ownerId = input.ownerScope === "session" ? input.ownerId ?? "" : "";
140
+ const bodyHash = sha256(body);
141
+ const now = input.now ?? Date.now();
142
+ const priority = clampPriority(input.priority ?? DEFAULT_PRIORITY);
143
+
144
+ this.#db.prepare(
145
+ `INSERT INTO memory_entries (owner_scope, owner_id, type, body, body_hash, priority, created_at)
146
+ VALUES (?, ?, ?, ?, ?, ?, ?)
147
+ ON CONFLICT(owner_scope, owner_id, type, body_hash) DO UPDATE SET
148
+ priority = MIN(memory_entries.priority, excluded.priority),
149
+ created_at = excluded.created_at`,
150
+ ).run(input.ownerScope, ownerId, input.type, body, bodyHash, priority, now);
151
+
152
+ return this.#readRowByKey(input.ownerScope, ownerId, input.type, bodyHash);
153
+ }
154
+
155
+ retrieve(options: MemoryRetrieveOptions): MemoryRow[] {
156
+ this.#assertOpen();
157
+ const byteBudget = Math.max(0, options.byteBudget ?? DEFAULT_BYTE_BUDGET);
158
+ if (byteBudget === 0) return [];
159
+ const limit = Math.max(1, options.limit ?? DEFAULT_LIMIT);
160
+ const epoch = this.#readClearEpoch(options.sessionId) ?? -1;
161
+
162
+ const sql = `
163
+ SELECT id, owner_scope AS ownerScope, owner_id AS ownerId, type, body,
164
+ body_hash AS bodyHash, priority, created_at AS createdAt
165
+ FROM memory_entries
166
+ WHERE (
167
+ (owner_scope = 'session' AND owner_id = ?)
168
+ OR (owner_scope = 'project' AND created_at > ?)
169
+ )
170
+ ORDER BY priority ASC, created_at DESC
171
+ LIMIT ?
172
+ `;
173
+ const rows = this.#db.prepare(sql).all(options.sessionId, epoch, limit) as MemoryRow[];
174
+
175
+ const collected: MemoryRow[] = [];
176
+ let used = 0;
177
+ for (const row of rows) {
178
+ const cost = byteLength(row.body);
179
+ if (used + cost > byteBudget) continue;
180
+ used += cost;
181
+ collected.push(row);
182
+ }
183
+ return collected;
184
+ }
185
+
186
+ recordClearEpoch(sessionId: string, now = Date.now()): void {
187
+ this.#assertOpen();
188
+ this.#db.prepare(
189
+ `INSERT INTO memory_clear_epochs (session_id, cleared_at)
190
+ VALUES (?, ?)
191
+ ON CONFLICT(session_id) DO UPDATE SET cleared_at = excluded.cleared_at`,
192
+ ).run(sessionId, now);
193
+ }
194
+
195
+ clearSession(sessionId: string, now = Date.now()): { deleted: number } {
196
+ this.#assertOpen();
197
+ const deleted = this.#db.prepare(
198
+ `DELETE FROM memory_entries WHERE owner_scope = 'session' AND owner_id = ?`,
199
+ ).run(sessionId).changes as number;
200
+ this.recordClearEpoch(sessionId, now);
201
+ return { deleted };
202
+ }
203
+
204
+ clearProject(now = Date.now()): { deleted: number } {
205
+ this.#assertOpen();
206
+ const stats = this.#db.prepare("SELECT COUNT(*) AS cnt FROM memory_entries").get() as {
207
+ cnt: number;
208
+ };
209
+ this.#db.exec("DELETE FROM memory_entries");
210
+ this.#db.exec("DELETE FROM memory_clear_epochs");
211
+ this.#db.prepare(
212
+ `INSERT INTO memory_meta (project_slug, last_project_clear_at)
213
+ VALUES (?, ?)
214
+ ON CONFLICT(project_slug) DO UPDATE SET last_project_clear_at = excluded.last_project_clear_at`,
215
+ ).run(this.#projectSlug, now);
216
+ return { deleted: stats.cnt };
217
+ }
218
+
219
+ pruneOld(retentionDays = RETENTION_DAYS, now = Date.now()): number {
220
+ this.#assertOpen();
221
+ const cutoff = now - retentionDays * 24 * 60 * 60 * 1000;
222
+ const deleted = this.#db.prepare(
223
+ `DELETE FROM memory_entries WHERE created_at < ?`,
224
+ ).run(cutoff).changes as number;
225
+ this.#db.prepare(
226
+ `INSERT INTO memory_meta (project_slug, last_prune_at)
227
+ VALUES (?, ?)
228
+ ON CONFLICT(project_slug) DO UPDATE SET last_prune_at = excluded.last_prune_at`,
229
+ ).run(this.#projectSlug, now);
230
+ return deleted;
231
+ }
232
+
233
+ getStats(): { totalRows: number; sessionRows: number; projectRows: number } {
234
+ this.#assertOpen();
235
+ const total = this.#db.prepare(
236
+ "SELECT COUNT(*) AS cnt FROM memory_entries",
237
+ ).get() as { cnt: number };
238
+ const sessionRows = this.#db.prepare(
239
+ "SELECT COUNT(*) AS cnt FROM memory_entries WHERE owner_scope = 'session'",
240
+ ).get() as { cnt: number };
241
+ const projectRows = this.#db.prepare(
242
+ "SELECT COUNT(*) AS cnt FROM memory_entries WHERE owner_scope = 'project'",
243
+ ).get() as { cnt: number };
244
+ return {
245
+ totalRows: total.cnt,
246
+ sessionRows: sessionRows.cnt,
247
+ projectRows: projectRows.cnt,
248
+ };
249
+ }
250
+
251
+ countSessionRows(sessionId: string): number {
252
+ this.#assertOpen();
253
+ const row = this.#db.prepare(
254
+ "SELECT COUNT(*) AS cnt FROM memory_entries WHERE owner_scope = 'session' AND owner_id = ?",
255
+ ).get(sessionId) as { cnt: number };
256
+ return row.cnt;
257
+ }
258
+
259
+ listSessions(): { session_id: string; row_count: number }[] {
260
+ this.#assertOpen();
261
+ return this.#db.prepare(
262
+ `SELECT owner_id AS session_id, COUNT(*) AS row_count
263
+ FROM memory_entries
264
+ WHERE owner_scope = 'session'
265
+ GROUP BY owner_id
266
+ ORDER BY owner_id`,
267
+ ).all() as Array<{ session_id: string; row_count: number }>;
268
+ }
269
+
270
+ close(): void {
271
+ if (this.#closed) return;
272
+ this.#closed = true;
273
+ this.#db.close();
274
+ }
275
+
276
+ #readRowByKey(
277
+ ownerScope: MemoryOwnerScope,
278
+ ownerId: string,
279
+ type: MemoryType,
280
+ bodyHash: string,
281
+ ): MemoryRow | null {
282
+ return (this.#db.prepare(
283
+ `SELECT id, owner_scope AS ownerScope, owner_id AS ownerId, type, body,
284
+ body_hash AS bodyHash, priority, created_at AS createdAt
285
+ FROM memory_entries
286
+ WHERE owner_scope = ? AND owner_id = ? AND type = ? AND body_hash = ?`,
287
+ ).get(ownerScope, ownerId, type, bodyHash) as MemoryRow | undefined) ?? null;
288
+ }
289
+
290
+ #readClearEpoch(sessionId: string): number | null {
291
+ const row = this.#db.prepare(
292
+ `SELECT cleared_at AS clearedAt FROM memory_clear_epochs WHERE session_id = ?`,
293
+ ).get(sessionId) as { clearedAt: number } | undefined;
294
+ return row?.clearedAt ?? null;
295
+ }
296
+
297
+ #assertOpen(): void {
298
+ if (this.#closed) {
299
+ throw new Error("memory-store: cannot use a closed store");
300
+ }
301
+ }
302
+ }
303
+
304
+ function sha256(value: string): string {
305
+ return createHash("sha256").update(value).digest("hex");
306
+ }
307
+
308
+ function byteLength(value: string): number {
309
+ return new TextEncoder().encode(value).byteLength;
310
+ }
311
+
312
+ function clampPriority(priority: number): number {
313
+ if (!Number.isFinite(priority)) return DEFAULT_PRIORITY;
314
+ return Math.min(5, Math.max(1, Math.floor(priority)));
315
+ }
316
+
317
+ let _memoryStoreRef: MemoryStore | null = null;
318
+
319
+ export function getMemoryStore(): MemoryStore | null {
320
+ return _memoryStoreRef;
321
+ }
322
+
323
+ export function _setMemoryStoreRef(store: MemoryStore | null): void {
324
+ _memoryStoreRef = store;
325
+ }