hippo-memory 1.63.1 → 1.64.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 (275) hide show
  1. package/README.md +13 -5
  2. package/dist/agent-memories/claude-code.js +3 -3
  3. package/dist/ambient-store.d.ts +2 -0
  4. package/dist/ambient-store.js +4 -3
  5. package/dist/api/assemble.d.ts +2 -0
  6. package/dist/api/assemble.js +11 -7
  7. package/dist/api/auth.d.ts +15 -11
  8. package/dist/api/auth.js +116 -22
  9. package/dist/api/context-select.d.ts +5 -3
  10. package/dist/api/context-select.js +6 -11
  11. package/dist/api/context-types.d.ts +2 -1
  12. package/dist/api/context.d.ts +1 -1
  13. package/dist/api/context.js +79 -55
  14. package/dist/api/dormant.d.ts +5 -5
  15. package/dist/api/dormant.js +13 -9
  16. package/dist/api/drill-down.d.ts +2 -0
  17. package/dist/api/drill-down.js +12 -7
  18. package/dist/api/forget.d.ts +1 -1
  19. package/dist/api/forget.js +8 -4
  20. package/dist/api/learn.js +5 -3
  21. package/dist/api/outcome.d.ts +1 -1
  22. package/dist/api/outcome.js +2 -1
  23. package/dist/api/promote.js +8 -5
  24. package/dist/api/recall-types.d.ts +4 -2
  25. package/dist/api/recall.js +51 -36
  26. package/dist/api/remember.d.ts +7 -0
  27. package/dist/api/remember.js +26 -2
  28. package/dist/api/sleep-run.d.ts +28 -0
  29. package/dist/api/sleep-run.js +267 -0
  30. package/dist/api/sleep.d.ts +5 -45
  31. package/dist/api/sleep.js +5 -252
  32. package/dist/api/types.d.ts +5 -0
  33. package/dist/api/types.js +4 -0
  34. package/dist/audit-prune.js +1 -1
  35. package/dist/audit.d.ts +1 -3
  36. package/dist/audit.js +7 -103
  37. package/dist/auth.d.ts +48 -4
  38. package/dist/auth.js +111 -42
  39. package/dist/autolearn.js +2 -2
  40. package/dist/capture/caller-session.d.ts +16 -0
  41. package/dist/capture/caller-session.js +58 -0
  42. package/dist/capture/command.d.ts +19 -1
  43. package/dist/capture/command.js +28 -21
  44. package/dist/capture/compact-resume-caller.d.ts +11 -0
  45. package/dist/capture/compact-resume-caller.js +35 -0
  46. package/dist/capture/compact.d.ts +0 -14
  47. package/dist/capture/compact.js +7 -160
  48. package/dist/capture/compaction-items-caller.d.ts +16 -0
  49. package/dist/capture/compaction-items-caller.js +32 -0
  50. package/dist/capture/extract.d.ts +2 -14
  51. package/dist/capture/extract.js +135 -346
  52. package/dist/capture/failure-caller.d.ts +21 -0
  53. package/dist/capture/failure-caller.js +74 -0
  54. package/dist/capture/failure-reading.d.ts +44 -0
  55. package/dist/capture/failure-reading.js +78 -0
  56. package/dist/capture/pre-compact-caller.d.ts +16 -0
  57. package/dist/capture/pre-compact-caller.js +39 -0
  58. package/dist/capture/session-end-caller.d.ts +21 -0
  59. package/dist/capture/session-end-caller.js +30 -0
  60. package/dist/capture/session-texts.d.ts +14 -0
  61. package/dist/capture/session-texts.js +42 -0
  62. package/dist/capture/transcript.d.ts +6 -1
  63. package/dist/capture/transcript.js +20 -16
  64. package/dist/capture/working-state.d.ts +21 -0
  65. package/dist/capture/working-state.js +154 -0
  66. package/dist/capture-error.d.ts +8 -22
  67. package/dist/capture-error.js +9 -61
  68. package/dist/cli/audit.js +25 -0
  69. package/dist/cli/auth.js +4 -3
  70. package/dist/cli/context.js +13 -158
  71. package/dist/cli/continuity.js +2 -1
  72. package/dist/cli/init.js +5 -3
  73. package/dist/cli/last-sleep.d.ts +6 -0
  74. package/dist/cli/last-sleep.js +68 -0
  75. package/dist/cli/quality-repair-once.d.ts +3 -0
  76. package/dist/cli/quality-repair-once.js +36 -0
  77. package/dist/cli/recall.js +2 -1
  78. package/dist/cli/remember.d.ts +2 -0
  79. package/dist/cli/remember.js +4 -1
  80. package/dist/cli/session-hooks.d.ts +0 -3
  81. package/dist/cli/session-hooks.js +12 -49
  82. package/dist/cli/setup.js +10 -2
  83. package/dist/cli/shared.d.ts +7 -14
  84. package/dist/cli/shared.js +19 -78
  85. package/dist/cli/sleep.js +6 -3
  86. package/dist/cli/usage.js +18 -9
  87. package/dist/cli.d.ts +1 -1
  88. package/dist/cli.js +3 -2
  89. package/dist/client.js +1 -1
  90. package/dist/compaction-record.d.ts +19 -1
  91. package/dist/compaction-record.js +61 -132
  92. package/dist/compaction-spool.d.ts +45 -0
  93. package/dist/compaction-spool.js +454 -0
  94. package/dist/config.d.ts +8 -0
  95. package/dist/config.js +49 -0
  96. package/dist/connectors/github/deletion.d.ts +1 -1
  97. package/dist/connectors/github/deletion.js +5 -17
  98. package/dist/consolidate/conflicts.js +4 -0
  99. package/dist/consolidate/flush-units.d.ts +9 -0
  100. package/dist/consolidate/flush-units.js +63 -0
  101. package/dist/consolidate/llm-passes.js +5 -4
  102. package/dist/consolidate/merge.js +5 -1
  103. package/dist/consolidate/run.d.ts +2 -1
  104. package/dist/consolidate/sleep.d.ts +2 -0
  105. package/dist/consolidate/sleep.js +82 -15
  106. package/dist/context-render.d.ts +14 -0
  107. package/dist/context-render.js +28 -0
  108. package/dist/dag.js +6 -0
  109. package/dist/dashboard-actions.js +3 -2
  110. package/dist/dashboard.js +4 -3
  111. package/dist/db/backup.d.ts +6 -0
  112. package/dist/db/backup.js +29 -0
  113. package/dist/db/busy.d.ts +9 -0
  114. package/dist/db/busy.js +35 -0
  115. package/dist/db/continuity.d.ts +1 -0
  116. package/dist/db/continuity.js +31 -4
  117. package/dist/db/meta.d.ts +2 -0
  118. package/dist/db/meta.js +7 -0
  119. package/dist/db/migrate.js +2 -3
  120. package/dist/db/migrations/index.d.ts +1 -1
  121. package/dist/db/migrations/index.js +5 -1
  122. package/dist/db/migrations/v39.js +2 -7
  123. package/dist/db/migrations/v53.d.ts +3 -0
  124. package/dist/db/migrations/v53.js +16 -0
  125. package/dist/db/migrations/v54.d.ts +3 -0
  126. package/dist/db/migrations/v54.js +25 -0
  127. package/dist/db/open.d.ts +11 -0
  128. package/dist/db/open.js +27 -0
  129. package/dist/db/tables.d.ts +2 -0
  130. package/dist/db/tables.js +6 -2
  131. package/dist/db.d.ts +2 -2
  132. package/dist/db.js +2 -2
  133. package/dist/doctor.d.ts +1 -1
  134. package/dist/doctor.js +31 -15
  135. package/dist/dormant.d.ts +16 -20
  136. package/dist/dormant.js +24 -30
  137. package/dist/entry/json-hooks.d.ts +3 -0
  138. package/dist/entry/json-hooks.js +4 -0
  139. package/dist/entry/project-identity.d.ts +2 -0
  140. package/dist/entry/project-identity.js +3 -0
  141. package/dist/entry/session-text.d.ts +7 -0
  142. package/dist/entry/session-text.js +8 -0
  143. package/dist/extract.js +6 -0
  144. package/dist/failure-log.d.ts +10 -2
  145. package/dist/failure-log.js +19 -2
  146. package/dist/gated-write.js +1 -1
  147. package/dist/graph/delta.d.ts +44 -0
  148. package/dist/graph/delta.js +109 -0
  149. package/dist/graph/types.d.ts +5 -0
  150. package/dist/graph/write.d.ts +20 -20
  151. package/dist/graph/write.js +103 -36
  152. package/dist/graph-extract.d.ts +43 -9
  153. package/dist/graph-extract.js +140 -157
  154. package/dist/graph-recall.d.ts +1 -0
  155. package/dist/graph-recall.js +2 -2
  156. package/dist/graph-view.d.ts +1 -0
  157. package/dist/graph-view.js +25 -2
  158. package/dist/half-life-migration.js +5 -1
  159. package/dist/handoff-evidence.d.ts +5 -0
  160. package/dist/handoff-evidence.js +30 -0
  161. package/dist/home-path.d.ts +5 -0
  162. package/dist/home-path.js +24 -0
  163. package/dist/hooks/json-hooks.d.ts +13 -2
  164. package/dist/hooks/json-hooks.js +246 -117
  165. package/dist/hooks/opencode.js +1 -1
  166. package/dist/hooks/shared.d.ts +2 -0
  167. package/dist/hooks/shared.js +6 -1
  168. package/dist/http-util.d.ts +16 -7
  169. package/dist/http-util.js +65 -20
  170. package/dist/importers/vault.js +2 -0
  171. package/dist/index.d.ts +2 -1
  172. package/dist/index.js +1 -0
  173. package/dist/json.d.ts +2 -0
  174. package/dist/json.js +7 -0
  175. package/dist/ledger-db.d.ts +10 -0
  176. package/dist/ledger-db.js +42 -0
  177. package/dist/mcp/admin-tools.d.ts +2 -2
  178. package/dist/mcp/admin-tools.js +47 -5
  179. package/dist/mcp/memory-tools.js +7 -2
  180. package/dist/mcp/protocol.d.ts +6 -0
  181. package/dist/mcp/protocol.js +4 -2
  182. package/dist/mcp/recall-tools.js +19 -6
  183. package/dist/mcp/request.js +4 -0
  184. package/dist/mcp/session-state.d.ts +4 -0
  185. package/dist/mcp/session-state.js +15 -6
  186. package/dist/mcp/shared-gate.d.ts +4 -0
  187. package/dist/mcp/shared-gate.js +22 -0
  188. package/dist/mcp/tools.js +1 -0
  189. package/dist/memory-quality.d.ts +35 -0
  190. package/dist/memory-quality.js +191 -0
  191. package/dist/memory.d.ts +4 -4
  192. package/dist/physics-state.js +1 -1
  193. package/dist/pilot-arm.d.ts +14 -3
  194. package/dist/pilot-arm.js +32 -10
  195. package/dist/postinstall.js +2 -2
  196. package/dist/project-briefs.js +4 -4
  197. package/dist/project-identity.d.ts +14 -10
  198. package/dist/project-identity.js +29 -11
  199. package/dist/project-merge.d.ts +0 -2
  200. package/dist/project-merge.js +9 -13
  201. package/dist/prompt-hook.d.ts +65 -0
  202. package/dist/prompt-hook.js +213 -0
  203. package/dist/prompt-recall.js +1 -1
  204. package/dist/quality-repair.d.ts +27 -0
  205. package/dist/quality-repair.js +220 -0
  206. package/dist/rate-limit.d.ts +1 -0
  207. package/dist/rate-limit.js +5 -3
  208. package/dist/raw-archive.d.ts +1 -1
  209. package/dist/raw-archive.js +6 -16
  210. package/dist/recall-scope.d.ts +28 -5
  211. package/dist/recall-scope.js +52 -7
  212. package/dist/recall-trace.js +1 -1
  213. package/dist/reject-flow.d.ts +2 -0
  214. package/dist/reject-flow.js +20 -6
  215. package/dist/secret-detect.js +7 -8
  216. package/dist/server/auth.d.ts +8 -6
  217. package/dist/server/auth.js +85 -38
  218. package/dist/server/client-ip.d.ts +2 -0
  219. package/dist/server/client-ip.js +16 -1
  220. package/dist/server/mcp-http.d.ts +9 -3
  221. package/dist/server/mcp-http.js +56 -13
  222. package/dist/server/request.d.ts +1 -1
  223. package/dist/server/request.js +5 -5
  224. package/dist/server/routes/admin.js +12 -6
  225. package/dist/server/routes/memories.js +9 -1
  226. package/dist/server/routes/recall.js +14 -2
  227. package/dist/server/types.d.ts +42 -5
  228. package/dist/server/validation.d.ts +5 -2
  229. package/dist/server/validation.js +19 -2
  230. package/dist/server.d.ts +21 -4
  231. package/dist/server.js +207 -49
  232. package/dist/session-digest.d.ts +0 -1
  233. package/dist/session-digest.js +3 -12
  234. package/dist/session-owners.d.ts +7 -0
  235. package/dist/session-owners.js +54 -0
  236. package/dist/share-scrub.d.ts +3 -0
  237. package/dist/share-scrub.js +8 -0
  238. package/dist/shared.d.ts +1 -0
  239. package/dist/shared.js +26 -18
  240. package/dist/store/candidates.d.ts +14 -3
  241. package/dist/store/candidates.js +48 -26
  242. package/dist/store/conflicts.d.ts +15 -0
  243. package/dist/store/conflicts.js +68 -44
  244. package/dist/store/delete-and-batch.d.ts +25 -1
  245. package/dist/store/delete-and-batch.js +121 -69
  246. package/dist/store/entry-reads.d.ts +5 -6
  247. package/dist/store/entry-reads.js +24 -12
  248. package/dist/store/entry-row.d.ts +7 -10
  249. package/dist/store/entry-row.js +40 -18
  250. package/dist/store/entry-writes.d.ts +3 -3
  251. package/dist/store/entry-writes.js +9 -23
  252. package/dist/store/handoffs.d.ts +9 -6
  253. package/dist/store/handoffs.js +72 -59
  254. package/dist/store/index-and-stats.js +2 -2
  255. package/dist/store/open.d.ts +3 -1
  256. package/dist/store/open.js +3 -3
  257. package/dist/store/search-rows.d.ts +8 -5
  258. package/dist/store/search-rows.js +15 -18
  259. package/dist/store/sessions.d.ts +25 -6
  260. package/dist/store/sessions.js +61 -19
  261. package/dist/store/summaries.d.ts +1 -1
  262. package/dist/store/summaries.js +3 -18
  263. package/dist/store/tenant-lookup.d.ts +7 -3
  264. package/dist/store/tenant-lookup.js +9 -9
  265. package/dist/store-port.d.ts +13 -0
  266. package/dist/store-port.js +20 -0
  267. package/dist/version.d.ts +5 -1
  268. package/dist/version.js +5 -1
  269. package/dist/working-memory.js +1 -1
  270. package/dist/write-budget.d.ts +15 -0
  271. package/dist/write-budget.js +15 -0
  272. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  273. package/extensions/openclaw-plugin/package.json +1 -1
  274. package/openclaw.plugin.json +1 -1
  275. package/package.json +8 -4
@@ -1,4 +1,5 @@
1
1
  import { META_TABLE_DDL } from './meta.js';
2
+ import { tableExists } from './tables.js';
2
3
  // Shared by migration v48 and the stamped-store self-heal below.
3
4
  export const MEMORY_QUARANTINE_DDL = `
4
5
  CREATE TABLE IF NOT EXISTS memory_quarantine (
@@ -27,7 +28,9 @@ const TASK_SNAPSHOTS_DDL = `
27
28
  updated_at TEXT NOT NULL,
28
29
  session_id TEXT,
29
30
  tenant_id TEXT NOT NULL DEFAULT 'default',
30
- scope TEXT
31
+ scope TEXT,
32
+ owner_subject TEXT,
33
+ origin_project TEXT
31
34
  )
32
35
  `;
33
36
  const SESSION_EVENTS_DDL = `
@@ -60,7 +63,19 @@ const SESSION_HANDOFFS_DDL = `
60
63
  evidence_json TEXT,
61
64
  outcome TEXT,
62
65
  target_runtime TEXT,
63
- card_id TEXT
66
+ card_id TEXT,
67
+ owner_subject TEXT,
68
+ origin_project TEXT
69
+ )
70
+ `;
71
+ // Shared by migration v54 and the self-heal: a session id belongs to the first owner that binds it.
72
+ export const SESSION_OWNERS_DDL = `
73
+ CREATE TABLE IF NOT EXISTS session_owners (
74
+ tenant_id TEXT NOT NULL,
75
+ session_id TEXT NOT NULL,
76
+ owner_subject TEXT NOT NULL,
77
+ created_at TEXT NOT NULL,
78
+ PRIMARY KEY (tenant_id, session_id)
64
79
  )
65
80
  `;
66
81
  const CARDS_DDL = `
@@ -128,6 +143,7 @@ const CONTINUITY_TABLE_DDL = [
128
143
  CARD_RUNS_DDL,
129
144
  CARD_COMMENTS_DDL,
130
145
  MEMORY_QUARANTINE_DDL,
146
+ SESSION_OWNERS_DDL,
131
147
  ];
132
148
  const CONTINUITY_INDEX_DDL = [
133
149
  `CREATE INDEX IF NOT EXISTS idx_task_snapshots_status_updated ON task_snapshots(status, updated_at DESC, id DESC)`,
@@ -145,19 +161,30 @@ const CONTINUITY_INDEX_DDL = [
145
161
  `CREATE INDEX IF NOT EXISTS idx_card_runs_tenant_card ON card_runs(tenant_id, card, started DESC)`,
146
162
  `CREATE INDEX IF NOT EXISTS idx_card_comments_tenant_card ON card_comments(tenant_id, card_id, created_at DESC)`,
147
163
  `CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_card ON session_handoffs(tenant_id, card_id, created_at DESC)`,
164
+ `CREATE INDEX IF NOT EXISTS idx_task_snapshots_owner ON task_snapshots(tenant_id, owner_subject, origin_project, status, updated_at DESC)`,
165
+ `CREATE INDEX IF NOT EXISTS idx_session_handoffs_owner ON session_handoffs(tenant_id, owner_subject, origin_project, created_at DESC)`,
148
166
  ];
167
+ // A client retry carries the same request id, so a repeat finds the earlier row instead of writing again.
168
+ const REQUEST_INDEX_DDL = {
169
+ compactions: `CREATE UNIQUE INDEX IF NOT EXISTS idx_compactions_request ON compactions(tenant_id, request_id) WHERE request_id IS NOT NULL`,
170
+ failure_log: `CREATE UNIQUE INDEX IF NOT EXISTS idx_failure_log_request ON failure_log(tenant_id, request_id) WHERE request_id IS NOT NULL`,
171
+ };
149
172
  // Lives with the other re-asserted DDL so the required-object list below derives from one place.
150
173
  export const MEMORIES_FTS_DDL = `CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(id UNINDEXED, content, tags)`;
151
- const CREATED_NAME = /CREATE\s+(?:VIRTUAL\s+)?(?:TABLE|INDEX)\s+IF\s+NOT\s+EXISTS\s+(\w+)/gi;
174
+ const CREATED_NAME = /CREATE\s+(?:VIRTUAL\s+|UNIQUE\s+)?(?:TABLE|INDEX)\s+IF\s+NOT\s+EXISTS\s+(\w+)/gi;
152
175
  /** Names of every table and index a `CREATE ... IF NOT EXISTS` in `ddl` makes. */
153
176
  export function createdObjectNames(ddl) {
154
177
  return [...ddl.matchAll(CREATED_NAME)].map((m) => m[1]);
155
178
  }
156
179
  /** Every object runMigrations re-asserts on a stamped store; the open fast path requires all of them. */
157
- export const REQUIRED_SCHEMA_OBJECTS = Object.freeze([META_TABLE_DDL, ...CONTINUITY_TABLE_DDL, ...CONTINUITY_INDEX_DDL, MEMORIES_FTS_DDL].flatMap(createdObjectNames));
180
+ export const REQUIRED_SCHEMA_OBJECTS = Object.freeze([META_TABLE_DDL, ...CONTINUITY_TABLE_DDL, ...CONTINUITY_INDEX_DDL, ...Object.values(REQUEST_INDEX_DDL), MEMORIES_FTS_DDL].flatMap(createdObjectNames));
158
181
  // After the loop: tenant_id (v16) and scope (v23) do not exist yet on a genuine old store.
159
182
  export function ensureContinuityIndexes(db) {
160
183
  for (const ddl of CONTINUITY_INDEX_DDL)
161
184
  db.exec(ddl);
185
+ // A store that lost one of these tables still opens, so its hooks can log the failed step and run the rest.
186
+ for (const [table, ddl] of Object.entries(REQUEST_INDEX_DDL))
187
+ if (tableExists(db, table))
188
+ db.exec(ddl);
162
189
  }
163
190
  //# sourceMappingURL=continuity.js.map
package/dist/db/meta.d.ts CHANGED
@@ -5,5 +5,7 @@ export declare function getSchemaVersion(db: DatabaseSyncLike): number;
5
5
  export declare function setSchemaVersion(db: DatabaseSyncLike, version: number): void;
6
6
  export declare function getMeta(db: DatabaseSyncLike, key: string, fallback?: string): string;
7
7
  export declare function setMeta(db: DatabaseSyncLike, key: string, value: string): void;
8
+ /** Raise min_compatible_binary to `version`; never lowers it, since an older floor would let back in a binary a newer write already shut out. */
9
+ export declare function raiseMinBinary(db: DatabaseSyncLike, version: string): void;
8
10
  export declare function isFtsAvailable(db: DatabaseSyncLike): boolean;
9
11
  //# sourceMappingURL=meta.d.ts.map
package/dist/db/meta.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { compareSemver } from '../version.js';
1
2
  import { tableExists } from './tables.js';
2
3
  export const META_TABLE_DDL = `
3
4
  CREATE TABLE IF NOT EXISTS meta (
@@ -30,6 +31,12 @@ export function getMeta(db, key, fallback = '') {
30
31
  export function setMeta(db, key, value) {
31
32
  db.prepare(`INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(key, value);
32
33
  }
34
+ /** Raise min_compatible_binary to `version`; never lowers it, since an older floor would let back in a binary a newer write already shut out. */
35
+ export function raiseMinBinary(db, version) {
36
+ const existing = getMeta(db, 'min_compatible_binary');
37
+ if (!existing || compareSemver(version, existing) > 0)
38
+ setMeta(db, 'min_compatible_binary', version);
39
+ }
33
40
  export function isFtsAvailable(db) {
34
41
  return getMeta(db, 'fts5_available', '0') === '1';
35
42
  }
@@ -179,13 +179,12 @@ export function repairFtsDrift(db) {
179
179
  const counts = ftsRowCounts(db);
180
180
  if (counts === null || counts.memories === counts.fts)
181
181
  return false;
182
+ // NOT IN reads the UNINDEXED id column once, not once per memory; one NULL id would make NOT IN match nothing.
182
183
  db.exec(`
183
184
  INSERT INTO memories_fts(id, content, tags)
184
185
  SELECT m.id, m.content, m.tags_json
185
186
  FROM memories m
186
- WHERE NOT EXISTS (
187
- SELECT 1 FROM memories_fts f WHERE f.id = m.id
188
- )
187
+ WHERE m.id NOT IN (SELECT id FROM memories_fts WHERE id IS NOT NULL)
189
188
  `);
190
189
  db.exec(`
191
190
  DELETE FROM memories_fts
@@ -1,5 +1,5 @@
1
1
  import type { Migration } from './types.js';
2
- export declare const CURRENT_SCHEMA_VERSION = 52;
2
+ export declare const CURRENT_SCHEMA_VERSION = 54;
3
3
  /** Every schema migration in version order; runMigrations applies each one above the stored version. */
4
4
  export declare const MIGRATIONS: Migration[];
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -50,7 +50,9 @@ import { v49 } from './v49.js';
50
50
  import { v50 } from './v50.js';
51
51
  import { v51 } from './v51.js';
52
52
  import { v52 } from './v52.js';
53
- export const CURRENT_SCHEMA_VERSION = 52;
53
+ import { v53 } from './v53.js';
54
+ import { v54 } from './v54.js';
55
+ export const CURRENT_SCHEMA_VERSION = 54;
54
56
  /** Every schema migration in version order; runMigrations applies each one above the stored version. */
55
57
  export const MIGRATIONS = [
56
58
  v01,
@@ -105,5 +107,7 @@ export const MIGRATIONS = [
105
107
  v50,
106
108
  v51,
107
109
  v52,
110
+ v53,
111
+ v54,
108
112
  ];
109
113
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  import * as os from 'os';
2
2
  import * as path from 'path';
3
- import { compareSemver } from '../../version.js';
4
3
  import { deriveOriginProject, originFromSource, isGlobalStoreRoot } from '../../project-identity.js';
4
+ import { raiseMinBinary } from '../meta.js';
5
5
  import { tableHasColumn } from '../tables.js';
6
6
  export const v39 = {
7
7
  version: 39,
@@ -48,12 +48,7 @@ export const v39 = {
48
48
  // this DB would ignore origin_project and the secret veto and resume
49
49
  // injecting cross-project rows. 1.24.0 is the first version with the
50
50
  // isolation behavior. Forward-only - never lower an existing minimum.
51
- // SAFETY: this get() result's shape matches the single `value` column
52
- // named in the SELECT above.
53
- const existingMin = db.prepare(`SELECT value FROM meta WHERE key = 'min_compatible_binary'`).get()?.value;
54
- if (!existingMin || compareSemver('1.24.0', existingMin) > 0) {
55
- db.prepare(`INSERT INTO meta(key, value) VALUES('min_compatible_binary', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run('1.24.0');
56
- }
51
+ raiseMinBinary(db, '1.24.0');
57
52
  },
58
53
  };
59
54
  //# sourceMappingURL=v39.js.map
@@ -0,0 +1,3 @@
1
+ import type { Migration } from './types.js';
2
+ export declare const v53: Migration;
3
+ //# sourceMappingURL=v53.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { tableHasColumn, tableExists } from '../tables.js';
2
+ export const v53 = {
3
+ version: 53,
4
+ up: (db) => {
5
+ // Self-service keys record who minted them and when they stop working; both stay null for keys minted before this.
6
+ // No floor raise here: a store with no expiring key is safe for older binaries, so the first expiring key raises it.
7
+ if (tableExists(db, 'api_keys')) {
8
+ if (!tableHasColumn(db, 'api_keys', 'owner_subject'))
9
+ db.exec(`ALTER TABLE api_keys ADD COLUMN owner_subject TEXT`);
10
+ if (!tableHasColumn(db, 'api_keys', 'expires_at'))
11
+ db.exec(`ALTER TABLE api_keys ADD COLUMN expires_at TEXT`);
12
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_live_owner ON api_keys(tenant_id, owner_subject) WHERE revoked_at IS NULL`);
13
+ }
14
+ },
15
+ };
16
+ //# sourceMappingURL=v53.js.map
@@ -0,0 +1,3 @@
1
+ import type { Migration } from './types.js';
2
+ export declare const v54: Migration;
3
+ //# sourceMappingURL=v54.d.ts.map
@@ -0,0 +1,25 @@
1
+ import { SESSION_OWNERS_DDL } from '../continuity.js';
2
+ import { tableHasColumn, tableExists } from '../tables.js';
3
+ const OWNER_COLUMNS = [
4
+ ['task_snapshots', 'owner_subject'],
5
+ ['task_snapshots', 'origin_project'],
6
+ ['session_handoffs', 'owner_subject'],
7
+ ['session_handoffs', 'origin_project'],
8
+ ['failure_log', 'owner_subject'],
9
+ ['failure_log', 'origin_project'],
10
+ ['compactions', 'request_id'],
11
+ ['failure_log', 'request_id'],
12
+ ];
13
+ export const v54 = {
14
+ version: 54,
15
+ up: (db) => {
16
+ // Rows from before this stay NULL: no backfill, so they never match an owner's key.
17
+ // No floor raise here: the first session bind or owner snapshot raises it, so an unused store stays open to older binaries.
18
+ for (const [t, c] of OWNER_COLUMNS) {
19
+ if (tableExists(db, t) && !tableHasColumn(db, t, c))
20
+ db.exec(`ALTER TABLE ${t} ADD COLUMN ${c} TEXT`);
21
+ }
22
+ db.exec(SESSION_OWNERS_DDL);
23
+ },
24
+ };
25
+ //# sourceMappingURL=v54.js.map
package/dist/db/open.d.ts CHANGED
@@ -11,8 +11,19 @@ export declare function withSharedStoreHandles<T>(fn: () => T | Promise<T>, opts
11
11
  }): Promise<T>;
12
12
  /** Lock wait inside an HTTP request: SQLite waits synchronously, so a long wait would stall every other request on the event loop. */
13
13
  export declare const SERVER_DB_WAIT_MS = 250;
14
+ /** Lock wait for each of sleep's short write transactions, even inside a server request: a run stops only on a writer that holds the lock longer. */
15
+ export declare const SLEEP_DB_WAIT_MS = 5000;
14
16
  /** Runs `fn` so that every store opened inside it, across awaits, waits at most `busyWaitMs` for a lock unless the open passes its own. */
15
17
  export declare function withBusyWait<T>(busyWaitMs: number, fn: () => T): T;
18
+ /** The lock wait an open here would get without its own `busyWaitMs`, in openHippoDb's order; undefined outside both scopes. */
19
+ export declare function scopedBusyWait(): number | undefined;
20
+ /** Thrown by a hippo.db open inside a request served from another store: the code path is not ported to the store port yet. */
21
+ export declare class SqliteBlockedError extends Error {
22
+ readonly storeKind: string;
23
+ constructor(storeKind: string);
24
+ }
25
+ /** Runs `fn` so that every hippo.db open inside it, across awaits, throws; otherwise a missed port would create and write a hippo.db nobody reads. */
26
+ export declare function withSqliteBlocked<T>(storeKind: string, fn: () => T): T;
16
27
  /** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
17
28
  export declare function openHippoDb(hippoRoot: string, opts?: {
18
29
  busyWaitMs?: number;
package/dist/db/open.js CHANGED
@@ -54,13 +54,39 @@ export async function withSharedStoreHandles(fn, opts) {
54
54
  }
55
55
  /** Lock wait inside an HTTP request: SQLite waits synchronously, so a long wait would stall every other request on the event loop. */
56
56
  export const SERVER_DB_WAIT_MS = 250;
57
+ /** Lock wait for each of sleep's short write transactions, even inside a server request: a run stops only on a writer that holds the lock longer. */
58
+ export const SLEEP_DB_WAIT_MS = 5000;
57
59
  const scopedBusyWaitMs = new AsyncLocalStorage();
58
60
  /** Runs `fn` so that every store opened inside it, across awaits, waits at most `busyWaitMs` for a lock unless the open passes its own. */
59
61
  export function withBusyWait(busyWaitMs, fn) {
60
62
  return scopedBusyWaitMs.run(busyWaitMs, fn);
61
63
  }
64
+ /** The lock wait an open here would get without its own `busyWaitMs`, in openHippoDb's order; undefined outside both scopes. */
65
+ export function scopedBusyWait() {
66
+ return shareBusyWaitMs ?? scopedBusyWaitMs.getStore();
67
+ }
68
+ /** Thrown by a hippo.db open inside a request served from another store: the code path is not ported to the store port yet. */
69
+ export class SqliteBlockedError extends Error {
70
+ storeKind;
71
+ constructor(storeKind) {
72
+ super(`hippo.db is not opened while the '${storeKind}' store serves this request; this code path is not ported to the store yet`);
73
+ this.storeKind = storeKind;
74
+ this.name = 'SqliteBlockedError';
75
+ }
76
+ }
77
+ const sqliteBlockedBy = new AsyncLocalStorage();
78
+ /** Runs `fn` so that every hippo.db open inside it, across awaits, throws; otherwise a missed port would create and write a hippo.db nobody reads. */
79
+ export function withSqliteBlocked(storeKind, fn) {
80
+ return sqliteBlockedBy.run(storeKind, fn);
81
+ }
82
+ function assertSqliteAllowed() {
83
+ const storeKind = sqliteBlockedBy.getStore();
84
+ if (storeKind !== undefined)
85
+ throw new SqliteBlockedError(storeKind);
86
+ }
62
87
  /** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
63
88
  export function openHippoDb(hippoRoot, opts) {
89
+ assertSqliteAllowed();
64
90
  if (shareDepth === 0)
65
91
  return openOwnHippoDb(hippoRoot, { busyWaitMs: opts?.busyWaitMs ?? scopedBusyWaitMs.getStore() });
66
92
  const busyWaitMs = opts?.busyWaitMs ?? shareBusyWaitMs ?? scopedBusyWaitMs.getStore();
@@ -121,6 +147,7 @@ function openOwnHippoDb(hippoRoot, opts) {
121
147
  }
122
148
  /** Open an existing store without changing it: no mkdir, WAL switch, migration or mirror cleanup. Throws when hippo.db is missing. */
123
149
  export function openHippoDbReadOnly(hippoRoot) {
150
+ assertSqliteAllowed();
124
151
  const db = new DatabaseSync(getHippoDbPath(hippoRoot), { readOnly: true });
125
152
  try {
126
153
  db.exec('PRAGMA busy_timeout = 5000');
@@ -1,4 +1,6 @@
1
1
  import type { DatabaseSyncLike } from './sqlite.js';
2
+ /** Column names of one table; empty when the table is missing. */
3
+ export declare function tableColumns(db: DatabaseSyncLike, tableName: string): Set<string>;
2
4
  export declare function tableHasColumn(db: DatabaseSyncLike, tableName: string, columnName: string): boolean;
3
5
  export declare function tableExists(db: DatabaseSyncLike, tableName: string): boolean;
4
6
  /** Row count of one table; null when the table is missing or unreadable, which callers show as unknown. */
package/dist/db/tables.js CHANGED
@@ -1,9 +1,13 @@
1
- export function tableHasColumn(db, tableName, columnName) {
1
+ /** Column names of one table; empty when the table is missing. */
2
+ export function tableColumns(db, tableName) {
2
3
  if (!/^[a-z_]+$/i.test(tableName))
3
4
  throw new Error(`Invalid table name: ${tableName}`);
4
5
  // SAFETY: rows' shape matches PRAGMA table_info's documented `name` column.
5
6
  const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
6
- return rows.some((row) => row.name === columnName);
7
+ return new Set(rows.flatMap((row) => (row.name === undefined ? [] : [row.name])));
8
+ }
9
+ export function tableHasColumn(db, tableName, columnName) {
10
+ return tableColumns(db, tableName).has(columnName);
7
11
  }
8
12
  export function tableExists(db, tableName) {
9
13
  if (!/^[a-z_]+$/i.test(tableName))
package/dist/db.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { DatabaseSyncLike } from './db/sqlite.js';
2
- export { isSqliteBusy, STORE_BUSY_MESSAGE, execWithBusyRetry } from './db/busy.js';
2
+ export { isSqliteBusy, isStoreBusy, StoreBusyError, STORE_BUSY_MESSAGE, execWithBusyRetry, withWriteScope } from './db/busy.js';
3
3
  export { getSchemaVersion, getMeta, setMeta, isFtsAvailable } from './db/meta.js';
4
4
  export { countTableRows, pruneConsolidationRuns } from './db/tables.js';
5
5
  export { getCurrentSchemaVersion, IncompatibleBinaryError, ftsRowCounts, repairFtsDrift } from './db/migrate.js';
6
- export { getHippoDbPath, HOOK_DB_WAIT_MS, noteStoreBusy, withSharedStoreHandles, SERVER_DB_WAIT_MS, withBusyWait, openHippoDb, openHippoDbReadOnly, closeHippoDb, } from './db/open.js';
6
+ export { getHippoDbPath, HOOK_DB_WAIT_MS, noteStoreBusy, withSharedStoreHandles, SERVER_DB_WAIT_MS, SLEEP_DB_WAIT_MS, withBusyWait, scopedBusyWait, SqliteBlockedError, withSqliteBlocked, openHippoDb, openHippoDbReadOnly, closeHippoDb, } from './db/open.js';
7
7
  //# sourceMappingURL=db.d.ts.map
package/dist/db.js CHANGED
@@ -1,6 +1,6 @@
1
- export { isSqliteBusy, STORE_BUSY_MESSAGE, execWithBusyRetry } from './db/busy.js';
1
+ export { isSqliteBusy, isStoreBusy, StoreBusyError, STORE_BUSY_MESSAGE, execWithBusyRetry, withWriteScope } from './db/busy.js';
2
2
  export { getSchemaVersion, getMeta, setMeta, isFtsAvailable } from './db/meta.js';
3
3
  export { countTableRows, pruneConsolidationRuns } from './db/tables.js';
4
4
  export { getCurrentSchemaVersion, IncompatibleBinaryError, ftsRowCounts, repairFtsDrift } from './db/migrate.js';
5
- export { getHippoDbPath, HOOK_DB_WAIT_MS, noteStoreBusy, withSharedStoreHandles, SERVER_DB_WAIT_MS, withBusyWait, openHippoDb, openHippoDbReadOnly, closeHippoDb, } from './db/open.js';
5
+ export { getHippoDbPath, HOOK_DB_WAIT_MS, noteStoreBusy, withSharedStoreHandles, SERVER_DB_WAIT_MS, SLEEP_DB_WAIT_MS, withBusyWait, scopedBusyWait, SqliteBlockedError, withSqliteBlocked, openHippoDb, openHippoDbReadOnly, closeHippoDb, } from './db/open.js';
6
6
  //# sourceMappingURL=db.js.map
package/dist/doctor.d.ts CHANGED
@@ -19,7 +19,7 @@ export interface DoctorReport {
19
19
  /** Inputs for {@link runDoctor}; defaults come from the process. */
20
20
  export interface DoctorOpts {
21
21
  cwd?: string;
22
- /** Home directory used to find agent configuration (~/.claude, ~/.codex). */
22
+ /** Home directory used to find agent configuration (~/.claude unless CLAUDE_CONFIG_DIR is set, ~/.codex unless CODEX_HOME is). */
23
23
  home?: string;
24
24
  version: string;
25
25
  nodeVersion?: string;
package/dist/doctor.js CHANGED
@@ -13,10 +13,12 @@ import { isInitialized } from './store/open.js';
13
13
  import { loadConfig } from './config.js';
14
14
  import { openHippoDbReadOnly, closeHippoDb, getSchemaVersion, getCurrentSchemaVersion, countTableRows, ftsRowCounts, IncompatibleBinaryError } from './db.js';
15
15
  import { REPLAY_AFTER_MS, TRANSCRIPT_FILL_WINDOW_MS } from './compaction-record.js';
16
+ import { SPOOL_DIR, spoolCounts } from './compaction-spool.js';
16
17
  import { isEmbeddingAvailable } from './local-embedding.js';
17
- import { CODEX_TRUST_LINE, codexHomeDir, isCodexPresent, isJsonObject } from './hooks/shared.js';
18
+ import { CODEX_TRUST_LINE, claudeConfigDir, codexHomeDir, isCodexPresent, isJsonObject } from './hooks/shared.js';
18
19
  import { planProjectRepair } from './project-merge.js';
19
20
  import { resolveTenantId } from './tenant.js';
21
+ import { readJsonFile } from './json.js';
20
22
  /** Minimum Node.js version hippo supports (package.json engines). */
21
23
  export const MIN_NODE = '22.16.0';
22
24
  function versionAtLeast(actual, min) {
@@ -32,8 +34,7 @@ function versionAtLeast(actual, min) {
32
34
  }
33
35
  function readJson(file) {
34
36
  try {
35
- // SAFETY: JSON.parse returns a JSON value by definition.
36
- return JSON.parse(fs.readFileSync(file, 'utf8'));
37
+ return readJsonFile(file);
37
38
  }
38
39
  catch {
39
40
  // A missing or corrupt file is the finding doctor reports, so null is the answer.
@@ -101,8 +102,17 @@ function sleepCheck(db, now) {
101
102
  return { id: 'sleep', status: 'info', detail: `sleep history unavailable (${message})` };
102
103
  }
103
104
  }
104
- /** Compaction records the PostCompact hook left unfinished, which `hippo sleep` replays. */
105
- function compactionsCheck(db, now) {
105
+ /** Spool counts in their own try, so a spool that cannot be read never hides the database counts. */
106
+ function readSpool(store, now) {
107
+ try {
108
+ return { counts: spoolCounts(store, now, REPLAY_AFTER_MS), unread: '' };
109
+ }
110
+ catch (err) {
111
+ return { counts: { waiting: 0, stale: 0, bad: 0 }, unread: `; spool not read: ${err instanceof Error ? err.message : String(err)}` };
112
+ }
113
+ }
114
+ /** Compaction records the PostCompact hook left unfinished and spool files a replay has not finished, which `hippo sleep` replays. */
115
+ function compactionsCheck(db, store, now) {
106
116
  const stuckBefore = new Date(now.getTime() - REPLAY_AFTER_MS).toISOString();
107
117
  const transcriptFloor = new Date(now.getTime() - TRANSCRIPT_FILL_WINDOW_MS).toISOString();
108
118
  try {
@@ -115,14 +125,20 @@ function compactionsCheck(db, now) {
115
125
  const summarised = Number(row?.summarised ?? 0);
116
126
  const started = Number(row?.started ?? 0);
117
127
  const stuck = summarised + started;
118
- if (stuck === 0)
119
- return { id: 'compactions', status: 'pass', detail: `${total} compaction${total === 1 ? '' : 's'} recorded, none stuck` };
120
- return {
121
- id: 'compactions',
122
- status: 'warn',
123
- detail: `${stuck} compaction${stuck === 1 ? '' : 's'} unfinished after 10 minutes (${summarised} with a summary whose memories are not saved yet, ${started} with no summary yet)`,
124
- fix: 'hippo sleep (replays them)',
125
- };
128
+ const { counts: spool, unread } = readSpool(store, now);
129
+ const spooled = spool.waiting + spool.stale + spool.bad > 0;
130
+ const recorded = `${total} compaction${total === 1 ? '' : 's'} recorded, none stuck`;
131
+ if (stuck === 0 && !spooled && unread === '')
132
+ return { id: 'compactions', status: 'pass', detail: recorded };
133
+ const fix = [
134
+ stuck + spool.waiting + spool.stale > 0 ? 'hippo sleep (replays them)' : '',
135
+ spool.bad > 0 ? `open the .bad files in ${path.join(store, SPOOL_DIR)}, save what you still need with hippo remember, then delete them` : '',
136
+ ].filter((s) => s !== '');
137
+ const unfinished = stuck === 0
138
+ ? `${recorded} in the store`
139
+ : `${stuck} compaction${stuck === 1 ? '' : 's'} unfinished after 10 minutes (${summarised} with a summary whose memories are not saved yet, ${started} with no summary yet)`;
140
+ const note = spooled ? `; spool: ${spool.waiting} waiting, ${spool.stale} left by a replay that stopped, ${spool.bad} .bad` : '';
141
+ return { id: 'compactions', status: 'warn', detail: `${unfinished}${note}${unread}`, fix: fix.join('; ') };
126
142
  }
127
143
  catch (err) {
128
144
  const message = err instanceof Error ? err.message : String(err);
@@ -241,7 +257,7 @@ function databaseChecks(store, now) {
241
257
  checks.push(tokensCheck(db, since));
242
258
  checks.push(failuresCheck(db, since, have));
243
259
  checks.push(sleepCheck(db, now));
244
- checks.push(compactionsCheck(db, now));
260
+ checks.push(compactionsCheck(db, store, now));
245
261
  }
246
262
  catch (err) {
247
263
  checks.push({
@@ -267,7 +283,7 @@ const CLAUDE_CODE_HOOKS = [
267
283
  ['hippo capture-error', 'failed-tool capture'],
268
284
  ];
269
285
  function claudeCodeCheck(home) {
270
- const claudeDir = path.join(home, '.claude');
286
+ const claudeDir = claudeConfigDir(home);
271
287
  if (!fs.existsSync(claudeDir)) {
272
288
  return { id: 'claude-code', status: 'info', detail: 'Claude Code not found; other agents can use hippo over MCP (hippo mcp)' };
273
289
  }
package/dist/dormant.d.ts CHANGED
@@ -19,8 +19,9 @@
19
19
  */
20
20
  import type { DatabaseSyncLike } from './db.js';
21
21
  import type { MemoryEntry } from './memory.js';
22
- /** Why a memory went dormant: sleep's decay pass, an imported agent memory whose note was deleted, or `hippo projects repair` splitting a two-project merge. */
23
- export type DormantReason = 'decay' | 'source-deleted' | 'project-repair';
22
+ import type { SqlFragment } from './recall-scope.js';
23
+ /** Why a memory went dormant: sleep's decay pass, an imported agent memory whose note was deleted, `hippo projects repair` splitting a two-project merge, or `hippo audit repair` setting aside an automatic memory with a certain defect. */
24
+ export type DormantReason = 'decay' | 'source-deleted' | 'project-repair' | 'quality-repair';
24
25
  /** One memory that sleep is moving out of active memory into the dormant store. */
25
26
  export interface DormantMove {
26
27
  /** The memory as it stood when it faded; restored verbatim apart from its recall clock. */
@@ -39,7 +40,7 @@ export interface DormantMemory {
39
40
  tags: string[];
40
41
  /** Live strength when it went dormant. */
41
42
  strength: number;
42
- /** Why it went dormant (`decay` or `source-deleted`). */
43
+ /** Why it went dormant: one of the {@link DormantReason} values. */
43
44
  reason: string;
44
45
  /** ISO time it went dormant. */
45
46
  dormantAt: string;
@@ -57,8 +58,8 @@ export interface ListDormantOpts {
57
58
  * memory is never in both places or in neither.
58
59
  */
59
60
  export declare function insertDormantRow(db: DatabaseSyncLike, move: DormantMove): void;
60
- /** A tenant's dormant memories, newest first, optionally filtered by search terms. */
61
- export declare function listDormantRows(db: DatabaseSyncLike, tenantId: string, opts?: ListDormantOpts): DormantMemory[];
61
+ /** A tenant's dormant memories whose scope `admit` passes, newest first, optionally filtered by search terms. */
62
+ export declare function listDormantRows(db: DatabaseSyncLike, tenantId: string, opts?: ListDormantOpts, admit?: SqlFragment): DormantMemory[];
62
63
  /** A dormant memory's stored snapshot plus when and why it went dormant. */
63
64
  export interface DormantSnapshot {
64
65
  entry: MemoryEntry;
@@ -77,21 +78,16 @@ export declare function listDormantSnapshots(db: DatabaseSyncLike, tenantId: str
77
78
  export declare function dormantSnapshotsBySourcePrefix(db: DatabaseSyncLike, tenantId: string, prefix: string): DormantSnapshot[];
78
79
  /** Put `entry` in place of a tenant's dormant memory `id`, keeping when and why that one went dormant. */
79
80
  export declare function replaceDormantEntry(db: DatabaseSyncLike, tenantId: string, id: string, entry: MemoryEntry): void;
80
- /** Whether a tenant has a dormant memory with this id (snapshot readable or not). */
81
- export declare function hasDormantRow(db: DatabaseSyncLike, tenantId: string, id: string): boolean;
81
+ /** Whether a tenant has a dormant memory with this id whose scope `admit` passes (snapshot readable or not). */
82
+ export declare function hasDormantRow(db: DatabaseSyncLike, tenantId: string, id: string, admit?: SqlFragment): boolean;
82
83
  /** Delete a tenant's dormant memory. Returns false when there was none. */
83
84
  export declare function deleteDormantRow(db: DatabaseSyncLike, tenantId: string, id: string): boolean;
84
- /**
85
- * Delete every dormant memory in the tenant whose content has this rejection
86
- * digest, so a rejected value cannot linger in dormant storage. Returns the
87
- * ids removed. Same O(N) scan as the live-row sweep in reject-flow.ts.
88
- */
89
- export declare function purgeDormantByDigest(db: DatabaseSyncLike, tenantId: string, digest: string): string[];
90
- /** How many dormant memories went dormant before `cutoffIso` (a dry-run count). */
91
- export declare function countExpiredDormant(db: DatabaseSyncLike, cutoffIso: string): number;
92
- /**
93
- * Delete every dormant memory (all tenants) that went dormant before
94
- * `cutoffIso`: the `dormant.retentionDays` window. Returns how many went.
95
- */
96
- export declare function purgeExpiredDormant(db: DatabaseSyncLike, cutoffIso: string): number;
85
+ /** Deletes the tenant's dormant copies of a rejected digest whose snapshot scope `inReach` admits, so the value cannot linger; returns their ids. */
86
+ export declare function purgeDormantByDigest(db: DatabaseSyncLike, tenantId: string, digest: string, inReach: (scope: string | null) => boolean): string[];
87
+ export interface DormantKey {
88
+ readonly tenantId: string;
89
+ readonly id: string;
90
+ }
91
+ export declare function expiredDormantKeys(db: DatabaseSyncLike, cutoffIso: string): DormantKey[];
92
+ export declare function deleteExpiredDormantRow(db: DatabaseSyncLike, key: DormantKey, cutoffIso: string): number;
97
93
  //# sourceMappingURL=dormant.d.ts.map
package/dist/dormant.js CHANGED
@@ -49,8 +49,11 @@ function rowToDormantMemory(row) {
49
49
  dormantAt: row.dormant_at,
50
50
  };
51
51
  }
52
- /** A tenant's dormant memories, newest first, optionally filtered by search terms. */
53
- export function listDormantRows(db, tenantId, opts = {}) {
52
+ // The snapshot's scope as a `scope` column, so recall-scope's SQL fragments read it as they read memories.scope.
53
+ const DORMANT_WITH_SCOPE = `(SELECT *, CASE WHEN json_valid(entry_json) THEN json_extract(entry_json, '$.scope') END AS scope FROM dormant_memories)`;
54
+ const EVERY_SCOPE = { sql: '1', params: [] };
55
+ /** A tenant's dormant memories whose scope `admit` passes, newest first, optionally filtered by search terms. */
56
+ export function listDormantRows(db, tenantId, opts = {}, admit = EVERY_SCOPE) {
54
57
  const terms = (opts.query ?? '').trim().split(/\s+/).filter((t) => t.length > 0);
55
58
  const limit = opts.limit !== undefined && Number.isFinite(opts.limit) && opts.limit >= 1
56
59
  ? Math.floor(opts.limit)
@@ -58,10 +61,10 @@ export function listDormantRows(db, tenantId, opts = {}) {
58
61
  const termClauses = terms.map(() => ` AND content LIKE ? ESCAPE '\\'`).join('');
59
62
  // SAFETY: rows' shape matches the seven columns named in the SELECT.
60
63
  const rows = db.prepare(`SELECT tenant_id, id, content, entry_json, reason, strength, dormant_at
61
- FROM dormant_memories
62
- WHERE tenant_id = ?${termClauses}
64
+ FROM ${DORMANT_WITH_SCOPE}
65
+ WHERE tenant_id = ? AND ${admit.sql}${termClauses}
63
66
  ORDER BY dormant_at DESC, id ASC
64
- LIMIT ?`).all(tenantId, ...terms.map((t) => `%${escapeLike(t)}%`), limit);
67
+ LIMIT ?`).all(tenantId, ...admit.params, ...terms.map((t) => `%${escapeLike(t)}%`), limit);
65
68
  return rows.map(rowToDormantMemory);
66
69
  }
67
70
  /**
@@ -99,45 +102,36 @@ export function replaceDormantEntry(db, tenantId, id, entry) {
99
102
  db.prepare(`UPDATE dormant_memories SET id = ?, content = ?, entry_json = ? WHERE tenant_id = ? AND id = ?`)
100
103
  .run(entry.id, entry.content, JSON.stringify(entry), tenantId, id);
101
104
  }
102
- /** Whether a tenant has a dormant memory with this id (snapshot readable or not). */
103
- export function hasDormantRow(db, tenantId, id) {
104
- return db.prepare(`SELECT 1 FROM dormant_memories WHERE tenant_id = ? AND id = ?`).get(tenantId, id) !== undefined;
105
+ /** Whether a tenant has a dormant memory with this id whose scope `admit` passes (snapshot readable or not). */
106
+ export function hasDormantRow(db, tenantId, id, admit = EVERY_SCOPE) {
107
+ return db.prepare(`SELECT 1 FROM ${DORMANT_WITH_SCOPE} WHERE tenant_id = ? AND id = ? AND ${admit.sql}`).get(tenantId, id, ...admit.params) !== undefined;
105
108
  }
106
109
  /** Delete a tenant's dormant memory. Returns false when there was none. */
107
110
  export function deleteDormantRow(db, tenantId, id) {
108
111
  const result = db.prepare(`DELETE FROM dormant_memories WHERE tenant_id = ? AND id = ?`).run(tenantId, id);
109
112
  return Number(result.changes ?? 0) > 0;
110
113
  }
111
- /**
112
- * Delete every dormant memory in the tenant whose content has this rejection
113
- * digest, so a rejected value cannot linger in dormant storage. Returns the
114
- * ids removed. Same O(N) scan as the live-row sweep in reject-flow.ts.
115
- */
116
- export function purgeDormantByDigest(db, tenantId, digest) {
117
- // SAFETY: rows' shape matches the two columns named in the SELECT.
118
- const rows = db.prepare(`SELECT id, content FROM dormant_memories WHERE tenant_id = ?`)
119
- .all(tenantId);
114
+ /** Deletes the tenant's dormant copies of a rejected digest whose snapshot scope `inReach` admits, so the value cannot linger; returns their ids. */
115
+ export function purgeDormantByDigest(db, tenantId, digest, inReach) {
116
+ // SAFETY: rows' shape matches the three columns named in the SELECT.
117
+ const rows = db.prepare(`SELECT id, content, CASE WHEN json_valid(entry_json) THEN json_extract(entry_json, '$.scope') END AS scope
118
+ FROM dormant_memories WHERE tenant_id = ?`).all(tenantId);
120
119
  const removed = [];
121
120
  for (const row of rows) {
122
- if (rejectionDigest(row.content) !== digest)
121
+ if (rejectionDigest(row.content) !== digest || !inReach(row.scope))
123
122
  continue;
124
123
  deleteDormantRow(db, tenantId, row.id);
125
124
  removed.push(row.id);
126
125
  }
127
126
  return removed;
128
127
  }
129
- /** How many dormant memories went dormant before `cutoffIso` (a dry-run count). */
130
- export function countExpiredDormant(db, cutoffIso) {
131
- // SAFETY: row's shape matches the single aliased COUNT column in the SELECT.
132
- const row = db.prepare(`SELECT COUNT(*) AS n FROM dormant_memories WHERE dormant_at < ?`).get(cutoffIso);
133
- return Number(row.n);
128
+ export function expiredDormantKeys(db, cutoffIso) {
129
+ const sql = `SELECT tenant_id AS tenantId, id FROM dormant_memories WHERE dormant_at < ?`;
130
+ // SAFETY: rows' shape matches the two columns named in the SELECT.
131
+ return db.prepare(sql).all(cutoffIso);
134
132
  }
135
- /**
136
- * Delete every dormant memory (all tenants) that went dormant before
137
- * `cutoffIso`: the `dormant.retentionDays` window. Returns how many went.
138
- */
139
- export function purgeExpiredDormant(db, cutoffIso) {
140
- const result = db.prepare(`DELETE FROM dormant_memories WHERE dormant_at < ?`).run(cutoffIso);
141
- return Number(result.changes ?? 0);
133
+ export function deleteExpiredDormantRow(db, key, cutoffIso) {
134
+ const sql = `DELETE FROM dormant_memories WHERE tenant_id = ? AND id = ? AND dormant_at < ?`;
135
+ return Number(db.prepare(sql).run(key.tenantId, key.id, cutoffIso).changes ?? 0);
142
136
  }
143
137
  //# sourceMappingURL=dormant.js.map
@@ -0,0 +1,3 @@
1
+ export { installJsonHooks, resolveJsonHookPaths, uninstallJsonHooks, writeSettingsFile, type InstallResult, type JsonHookPaths } from '../hooks/json-hooks.js';
2
+ export { readJsonFile } from '../json.js';
3
+ //# sourceMappingURL=json-hooks.d.ts.map
@@ -0,0 +1,4 @@
1
+ // The hippo-memory/json-hooks subpath: only what the changelog publishes, so the module's other exports stay internal.
2
+ export { installJsonHooks, resolveJsonHookPaths, uninstallJsonHooks, writeSettingsFile } from '../hooks/json-hooks.js';
3
+ export { readJsonFile } from '../json.js';
4
+ //# sourceMappingURL=json-hooks.js.map
@@ -0,0 +1,2 @@
1
+ export { MAX_PROJECT_ALIASES, MCP_PROJECT_SCOPED_HEADER, resolveProjectIdentity, type ProjectIdentity, type ResolveProjectIdentityOpts } from '../project-identity.js';
2
+ //# sourceMappingURL=project-identity.d.ts.map
@@ -0,0 +1,3 @@
1
+ // The hippo-memory/project-identity subpath: only what the changelog publishes, so the module's other exports stay internal.
2
+ export { MAX_PROJECT_ALIASES, MCP_PROJECT_SCOPED_HEADER, resolveProjectIdentity } from '../project-identity.js';
3
+ //# sourceMappingURL=project-identity.js.map