hippo-memory 1.63.2 → 1.64.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +11 -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/maintenance.d.ts +2 -1
  76. package/dist/cli/maintenance.js +17 -11
  77. package/dist/cli/quality-repair-once.d.ts +3 -0
  78. package/dist/cli/quality-repair-once.js +36 -0
  79. package/dist/cli/recall.js +2 -1
  80. package/dist/cli/remember.d.ts +2 -0
  81. package/dist/cli/remember.js +4 -1
  82. package/dist/cli/session-hooks.d.ts +0 -3
  83. package/dist/cli/session-hooks.js +12 -49
  84. package/dist/cli/setup.js +10 -2
  85. package/dist/cli/shared.d.ts +7 -14
  86. package/dist/cli/shared.js +19 -78
  87. package/dist/cli/sleep.js +6 -3
  88. package/dist/cli/usage.js +18 -9
  89. package/dist/cli.d.ts +1 -1
  90. package/dist/cli.js +3 -2
  91. package/dist/client.js +1 -1
  92. package/dist/compaction-record.d.ts +19 -1
  93. package/dist/compaction-record.js +61 -132
  94. package/dist/compaction-spool.d.ts +45 -0
  95. package/dist/compaction-spool.js +454 -0
  96. package/dist/config.d.ts +8 -0
  97. package/dist/config.js +49 -0
  98. package/dist/connectors/github/deletion.d.ts +1 -1
  99. package/dist/connectors/github/deletion.js +5 -17
  100. package/dist/consolidate/conflicts.js +4 -0
  101. package/dist/consolidate/flush-units.d.ts +9 -0
  102. package/dist/consolidate/flush-units.js +63 -0
  103. package/dist/consolidate/llm-passes.js +5 -4
  104. package/dist/consolidate/merge.js +5 -1
  105. package/dist/consolidate/run.d.ts +2 -1
  106. package/dist/consolidate/sleep.d.ts +2 -0
  107. package/dist/consolidate/sleep.js +82 -15
  108. package/dist/context-render.d.ts +14 -0
  109. package/dist/context-render.js +28 -0
  110. package/dist/dag.js +6 -0
  111. package/dist/dashboard-actions.js +3 -2
  112. package/dist/dashboard.js +4 -3
  113. package/dist/db/backup.d.ts +6 -0
  114. package/dist/db/backup.js +29 -0
  115. package/dist/db/busy.d.ts +9 -0
  116. package/dist/db/busy.js +35 -0
  117. package/dist/db/continuity.d.ts +1 -0
  118. package/dist/db/continuity.js +31 -4
  119. package/dist/db/meta.d.ts +2 -0
  120. package/dist/db/meta.js +7 -0
  121. package/dist/db/migrate.js +2 -3
  122. package/dist/db/migrations/index.d.ts +1 -1
  123. package/dist/db/migrations/index.js +5 -1
  124. package/dist/db/migrations/v39.js +2 -7
  125. package/dist/db/migrations/v53.d.ts +3 -0
  126. package/dist/db/migrations/v53.js +16 -0
  127. package/dist/db/migrations/v54.d.ts +3 -0
  128. package/dist/db/migrations/v54.js +25 -0
  129. package/dist/db/open.d.ts +11 -0
  130. package/dist/db/open.js +27 -0
  131. package/dist/db/tables.d.ts +2 -0
  132. package/dist/db/tables.js +6 -2
  133. package/dist/db.d.ts +2 -2
  134. package/dist/db.js +2 -2
  135. package/dist/doctor.d.ts +1 -1
  136. package/dist/doctor.js +31 -15
  137. package/dist/dormant.d.ts +16 -20
  138. package/dist/dormant.js +24 -30
  139. package/dist/embedding-provider.js +4 -3
  140. package/dist/entry/json-hooks.d.ts +3 -0
  141. package/dist/entry/json-hooks.js +4 -0
  142. package/dist/entry/project-identity.d.ts +2 -0
  143. package/dist/entry/project-identity.js +3 -0
  144. package/dist/entry/session-text.d.ts +7 -0
  145. package/dist/entry/session-text.js +8 -0
  146. package/dist/extract.js +6 -0
  147. package/dist/failure-log.d.ts +10 -2
  148. package/dist/failure-log.js +19 -2
  149. package/dist/gated-write.js +1 -1
  150. package/dist/graph/delta.d.ts +44 -0
  151. package/dist/graph/delta.js +109 -0
  152. package/dist/graph/types.d.ts +5 -0
  153. package/dist/graph/write.d.ts +20 -20
  154. package/dist/graph/write.js +103 -36
  155. package/dist/graph-extract.d.ts +43 -9
  156. package/dist/graph-extract.js +140 -157
  157. package/dist/graph-recall.d.ts +1 -0
  158. package/dist/graph-recall.js +2 -2
  159. package/dist/graph-view.d.ts +1 -0
  160. package/dist/graph-view.js +25 -2
  161. package/dist/half-life-migration.js +5 -1
  162. package/dist/handoff-evidence.d.ts +5 -0
  163. package/dist/handoff-evidence.js +30 -0
  164. package/dist/home-path.d.ts +5 -0
  165. package/dist/home-path.js +24 -0
  166. package/dist/hooks/json-hooks.d.ts +13 -2
  167. package/dist/hooks/json-hooks.js +246 -117
  168. package/dist/hooks/opencode.js +1 -1
  169. package/dist/hooks/shared.d.ts +2 -0
  170. package/dist/hooks/shared.js +6 -1
  171. package/dist/http-util.d.ts +16 -7
  172. package/dist/http-util.js +65 -20
  173. package/dist/importers/vault.js +2 -0
  174. package/dist/index.d.ts +2 -1
  175. package/dist/index.js +1 -0
  176. package/dist/json.d.ts +2 -0
  177. package/dist/json.js +7 -0
  178. package/dist/ledger-db.d.ts +10 -0
  179. package/dist/ledger-db.js +42 -0
  180. package/dist/local-embedding.d.ts +2 -0
  181. package/dist/local-embedding.js +20 -4
  182. package/dist/mcp/admin-tools.d.ts +2 -2
  183. package/dist/mcp/admin-tools.js +47 -5
  184. package/dist/mcp/memory-tools.js +7 -2
  185. package/dist/mcp/protocol.d.ts +6 -0
  186. package/dist/mcp/protocol.js +4 -2
  187. package/dist/mcp/recall-tools.js +19 -6
  188. package/dist/mcp/request.js +4 -0
  189. package/dist/mcp/session-state.d.ts +4 -0
  190. package/dist/mcp/session-state.js +15 -6
  191. package/dist/mcp/shared-gate.d.ts +4 -0
  192. package/dist/mcp/shared-gate.js +22 -0
  193. package/dist/mcp/tools.js +1 -0
  194. package/dist/memory-quality.d.ts +35 -0
  195. package/dist/memory-quality.js +191 -0
  196. package/dist/memory.d.ts +4 -4
  197. package/dist/physics-state.js +1 -1
  198. package/dist/pilot-arm.d.ts +14 -3
  199. package/dist/pilot-arm.js +32 -10
  200. package/dist/postinstall.js +2 -2
  201. package/dist/project-briefs.js +4 -4
  202. package/dist/project-identity.d.ts +14 -10
  203. package/dist/project-identity.js +29 -11
  204. package/dist/project-merge.d.ts +0 -2
  205. package/dist/project-merge.js +9 -13
  206. package/dist/prompt-hook.d.ts +65 -0
  207. package/dist/prompt-hook.js +213 -0
  208. package/dist/prompt-recall.js +1 -1
  209. package/dist/quality-repair.d.ts +27 -0
  210. package/dist/quality-repair.js +220 -0
  211. package/dist/rate-limit.d.ts +1 -0
  212. package/dist/rate-limit.js +5 -3
  213. package/dist/raw-archive.d.ts +1 -1
  214. package/dist/raw-archive.js +6 -16
  215. package/dist/recall-scope.d.ts +28 -5
  216. package/dist/recall-scope.js +52 -7
  217. package/dist/recall-trace.js +1 -1
  218. package/dist/reject-flow.d.ts +2 -0
  219. package/dist/reject-flow.js +20 -6
  220. package/dist/secret-detect.js +7 -8
  221. package/dist/server/auth.d.ts +8 -6
  222. package/dist/server/auth.js +85 -38
  223. package/dist/server/client-ip.d.ts +2 -0
  224. package/dist/server/client-ip.js +16 -1
  225. package/dist/server/mcp-http.d.ts +9 -3
  226. package/dist/server/mcp-http.js +56 -13
  227. package/dist/server/request.d.ts +1 -1
  228. package/dist/server/request.js +5 -5
  229. package/dist/server/routes/admin.js +12 -6
  230. package/dist/server/routes/memories.js +9 -1
  231. package/dist/server/routes/recall.js +14 -2
  232. package/dist/server/types.d.ts +42 -5
  233. package/dist/server/validation.d.ts +5 -2
  234. package/dist/server/validation.js +19 -2
  235. package/dist/server.d.ts +21 -4
  236. package/dist/server.js +207 -49
  237. package/dist/session-digest.d.ts +0 -1
  238. package/dist/session-digest.js +3 -12
  239. package/dist/session-owners.d.ts +7 -0
  240. package/dist/session-owners.js +54 -0
  241. package/dist/share-scrub.d.ts +3 -0
  242. package/dist/share-scrub.js +8 -0
  243. package/dist/shared.d.ts +1 -0
  244. package/dist/shared.js +26 -18
  245. package/dist/store/candidates.d.ts +14 -3
  246. package/dist/store/candidates.js +48 -26
  247. package/dist/store/conflicts.d.ts +15 -0
  248. package/dist/store/conflicts.js +68 -44
  249. package/dist/store/delete-and-batch.d.ts +25 -1
  250. package/dist/store/delete-and-batch.js +121 -69
  251. package/dist/store/entry-reads.d.ts +5 -6
  252. package/dist/store/entry-reads.js +24 -12
  253. package/dist/store/entry-row.d.ts +7 -10
  254. package/dist/store/entry-row.js +40 -18
  255. package/dist/store/entry-writes.d.ts +3 -3
  256. package/dist/store/entry-writes.js +9 -23
  257. package/dist/store/handoffs.d.ts +9 -6
  258. package/dist/store/handoffs.js +72 -59
  259. package/dist/store/index-and-stats.js +2 -2
  260. package/dist/store/open.d.ts +3 -1
  261. package/dist/store/open.js +3 -3
  262. package/dist/store/search-rows.d.ts +8 -5
  263. package/dist/store/search-rows.js +15 -18
  264. package/dist/store/sessions.d.ts +25 -6
  265. package/dist/store/sessions.js +61 -19
  266. package/dist/store/summaries.d.ts +1 -1
  267. package/dist/store/summaries.js +3 -18
  268. package/dist/store/tenant-lookup.d.ts +7 -3
  269. package/dist/store/tenant-lookup.js +9 -9
  270. package/dist/store-port.d.ts +13 -0
  271. package/dist/store-port.js +20 -0
  272. package/dist/version.d.ts +5 -1
  273. package/dist/version.js +5 -1
  274. package/dist/working-memory.js +1 -1
  275. package/dist/write-budget.d.ts +15 -0
  276. package/dist/write-budget.js +15 -0
  277. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  278. package/extensions/openclaw-plugin/package.json +1 -1
  279. package/openclaw.plugin.json +1 -1
  280. package/package.json +8 -4
package/dist/api/sleep.js CHANGED
@@ -1,257 +1,10 @@
1
1
  // The sleep cycle: decay, consolidation, dedupe, graph extraction and the other maintenance passes.
2
- import { openHippoDb, closeHippoDb } from '../db.js';
3
- import { loadAllEntries } from '../store/entry-reads.js';
4
- import { deleteEntry, memoriesBackingObjects } from '../store/delete-and-batch.js';
5
- import { appendAuditEvent, reportAuditWriteFailure, auditMemories } from '../audit.js';
6
- import { autoShare } from '../shared.js';
7
- import { consolidate } from '../consolidate/sleep.js';
8
- import { loadConfig } from '../config.js';
9
- import { deduplicateStore } from '../dedupe.js';
10
- import { computeAmbientState } from '../ambient.js';
11
- import { loadPendingExtractionTenants } from '../graph/read.js';
12
- import { markPendingProcessedUpTo } from '../graph/write.js';
13
- import { extractGraph } from '../graph-extract.js';
14
- const DEFAULT_SLEEP_PHASES = {
15
- consolidate,
16
- deduplicateStore,
17
- auditMemories,
18
- autoShare,
19
- loadAllEntries,
20
- deleteEntry,
21
- computeAmbientState,
22
- loadConfig,
23
- loadPendingExtractionTenants,
24
- extractGraph,
25
- };
26
- /** Sleeps the WHOLE hippoRoot, every tenant, so /v1/sleep stays loopback-only; never auto-deletes pinned, raw, kept or object-backing rows. */
2
+ import { runSleep } from './sleep-run.js';
3
+ /** Sleeps the WHOLE hippoRoot, every tenant, so /v1/sleep stays loopback-only; SQLite only; never auto-deletes pinned, raw, kept or object-backing rows. */
27
4
  export async function sleep(ctx, opts = {}) {
28
- const dryRun = Boolean(opts.dryRun);
29
- // Resolve phase dependencies, allowing test-only `__phases`
30
- // override to inject deterministic throws for mid-phase failure coverage.
31
- const phases = { ...DEFAULT_SLEEP_PHASES, ...(opts.__phases ?? {}) };
32
- // Phase counters for the consolidate audit emit (in finally).
33
- // Accumulated as each phase completes so partial-failure paths still report
34
- // accurate "what got done before the failure" data.
35
- const counts = { consolidation: 0, dedup: 0, auditDeleted: 0, ambient: 0 };
36
- let phaseError = null;
37
- try {
38
- return await runSleepPhases(ctx, opts, phases, counts);
39
- }
40
- catch (err) {
41
- // SAFETY: phaseError is read via phaseError.message / (phaseError !==
42
- // null) below, both safe even if a non-Error was thrown; this mirrors
43
- // the existing lenient (err as Error) pattern used throughout this catch chain.
44
- phaseError = err;
45
- throw err;
46
- }
47
- finally {
48
- emitSleepAudit(ctx, opts, dryRun, counts, phaseError);
49
- }
50
- }
51
- async function runSleepPhases(ctx, opts, phases, counts) {
52
- const dryRun = Boolean(opts.dryRun);
53
- const snapshot = snapshotDirtyTenants(ctx, phases, dryRun);
54
- // Phase 1: Consolidation.
55
- const consolidateResult = await phases.consolidate(ctx.hippoRoot, { dryRun });
56
- counts.consolidation = consolidateResult.semanticCreated + consolidateResult.merged;
57
- const result = sleepResultFrom(consolidateResult, dryRun);
58
- // Phase 2: Dedup (post-consolidate near-duplicate cleanup).
59
- const dedupResult = phases.deduplicateStore(ctx.hippoRoot, { dryRun, actor: ctx.actor.subject });
60
- counts.dedup = dedupResult.removed;
61
- if (dedupResult.removed > 0)
62
- result.deduped = dedupSummary(dedupResult);
63
- // Phase 3: Quality audit (remove junk, report warnings; a dry run skips rows earlier phases would remove).
64
- counts.auditDeleted = runQualityAudit(ctx, phases, dryRun, consolidateResult, dedupResult, result);
65
- if (dryRun)
66
- return result;
67
- // Phase 4: Auto-share high-transfer-score memories to global.
68
- if (!opts.noShare)
69
- shareOnSleep(ctx, phases, result);
70
- // Phase 5: Post-sleep ambient state summary.
71
- counts.ambient = summarizeAmbient(ctx, phases, result);
72
- drainGraphQueue(ctx, phases, snapshot, result);
73
- return result;
74
- }
75
- // Taken before any memory-deleting phase: queue rows cascade-delete with their mirror, so a later read would miss
76
- // tenants dedup touched. Fail-soft: a failed read skips the graph refresh this sleep and never aborts core sleep.
77
- function snapshotDirtyTenants(ctx, phases, dryRun) {
78
- const snapshot = { dirtyTenants: [], error: null };
79
- if (!dryRun) {
80
- try {
81
- snapshot.dirtyTenants = phases.loadPendingExtractionTenants(ctx.hippoRoot);
82
- }
83
- catch (snapErr) {
84
- // SAFETY: this is a best-effort log message only; property access on
85
- // any JS value is safe (undefined if absent), preserving the existing
86
- // lenient formatting even when something non-Error was thrown.
87
- snapshot.error = snapErr.message;
88
- }
89
- }
90
- return snapshot;
91
- }
92
- function sleepResultFrom(consolidateResult, dryRun) {
93
- const result = {
94
- active: consolidateResult.decayed,
95
- removed: consolidateResult.removed,
96
- mergedEpisodic: consolidateResult.merged,
97
- newSemantic: consolidateResult.semanticCreated,
98
- dryRun,
99
- details: consolidateResult.details,
100
- };
101
- // Set only when non-zero, so a store without dormant memories gets a
102
- // byte-identical result (HTTP /v1/sleep, the CLI render snapshot).
103
- if (consolidateResult.dormant > 0) {
104
- result.dormant = consolidateResult.dormant;
105
- }
106
- if (consolidateResult.dormantExpired > 0) {
107
- result.dormantExpired = consolidateResult.dormantExpired;
108
- }
109
- return result;
110
- }
111
- function dedupSummary(dedupResult) {
112
- const semDups = dedupResult.pairs.filter((p) => p.keptLayer === 'semantic' && p.removedLayer === 'semantic').length;
113
- const epiDups = dedupResult.pairs.filter((p) => p.keptLayer === 'episodic' && p.removedLayer === 'episodic').length;
114
- const crossDups = dedupResult.pairs.filter((p) => p.keptLayer !== p.removedLayer).length;
115
- return {
116
- removed: dedupResult.removed,
117
- semDups,
118
- epiDups,
119
- crossDups,
120
- };
121
- }
122
- /** Returns how many audit errors were deleted, or would be under dryRun. */
123
- function runQualityAudit(ctx, phases, dryRun, consolidateResult, dedupResult, result) {
124
- const planned = new Set(dryRun ? [...(consolidateResult.removedIds ?? []), ...dedupResult.pairs.map((p) => p.removed)] : []);
125
- const allEntries = phases.loadAllEntries(ctx.hippoRoot).filter((e) => !planned.has(e.id));
126
- const auditOut = phases.auditMemories(allEntries, memoriesBackingObjects(ctx.hippoRoot));
127
- if (auditOut.issues.length === 0)
128
- return 0;
129
- const errors = auditOut.issues.filter((i) => i.severity === 'error');
130
- const warnings = auditOut.issues.filter((i) => i.severity === 'warning');
131
- let removed = 0;
132
- for (const issue of errors) {
133
- const reason = `sleep-audit: ${issue.reason}`;
134
- if (dryRun || phases.deleteEntry(ctx.hippoRoot, issue.memoryId, { actor: ctx.actor.subject, reason, automatic: true }))
135
- removed++;
136
- }
137
- if (removed > 0 || warnings.length > 0) {
138
- result.audit = {
139
- errorsRemoved: removed,
140
- warningCount: warnings.length,
141
- };
142
- }
143
- return removed;
144
- }
145
- function shareOnSleep(ctx, phases, result) {
146
- const sleepConfig = phases.loadConfig(ctx.hippoRoot);
147
- if (!sleepConfig.autoShareOnSleep)
148
- return;
149
- // Both skip counters are surfaced so the secret veto and the rejection tombstone are observable, not silent.
150
- const autoShareStats = { secretSkipped: 0, rejectedSkipped: 0 };
151
- const shared = phases.autoShare(ctx.hippoRoot, { minScore: 0.6, stats: autoShareStats });
152
- if (shared.length > 0) {
153
- result.shared = shared.length;
154
- }
155
- if (autoShareStats.secretSkipped > 0) {
156
- result.secretSkipped = autoShareStats.secretSkipped;
157
- }
158
- if (autoShareStats.rejectedSkipped > 0) {
159
- result.rejectedSkipped = autoShareStats.rejectedSkipped;
160
- }
161
- }
162
- /** Returns the ambient total the audit row reports: 0 when ambient is off or no current row is left. */
163
- function summarizeAmbient(ctx, phases, result) {
164
- const postSleepConfig = phases.loadConfig(ctx.hippoRoot);
165
- if (!postSleepConfig.ambient.enabled)
166
- return 0;
167
- const postSleepEntries = phases.loadAllEntries(ctx.hippoRoot).filter((e) => !e.superseded_by);
168
- if (postSleepEntries.length === 0)
169
- return 0;
170
- result.ambient = phases.computeAmbientState(postSleepEntries);
171
- return result.ambient.totalMemories;
172
- }
173
- // Phase 6: rebuild the graph of every tenant marked dirty since the last sleep, so graph recall runs on fresh data.
174
- // Fault-isolated: consolidation has already committed, so no failure here aborts sleep; a failed tenant stays pending.
175
- function drainGraphQueue(ctx, phases, snapshot, result) {
176
- try {
177
- if (snapshot.error) {
178
- // Core sleep already succeeded; surface the skipped graph refresh as a detail.
179
- result.details = [
180
- ...(result.details ?? []),
181
- `graph: dirty-tenant snapshot failed (skipped graph refresh): ${snapshot.error}`,
182
- ];
183
- }
184
- let gTenants = 0;
185
- let gEntities = 0;
186
- let gRelations = 0;
187
- // dirtyTenants was snapshotted before the memory-deleting phases above.
188
- for (const { tenantId, maxPendingId } of snapshot.dirtyTenants) {
189
- try {
190
- const ext = phases.extractGraph(ctx.hippoRoot, tenantId);
191
- // Count the rebuild as soon as it succeeds — it happened regardless of
192
- // the drain-mark below.
193
- gTenants += 1;
194
- gEntities += ext.entities;
195
- gRelations += ext.relations;
196
- // Watermark drain: only items enqueued before this rebuild started are marked; later arrivals stay pending.
197
- markPendingProcessedUpTo(ctx.hippoRoot, tenantId, maxPendingId);
198
- }
199
- catch (tenantErr) {
200
- // SAFETY: this is a best-effort log message only; property access
201
- // on any JS value is safe (undefined if absent), preserving the
202
- // existing lenient formatting even when something non-Error was thrown.
203
- result.details = [
204
- ...(result.details ?? []),
205
- `graph: extract failed for a dirty tenant (left pending): ${tenantErr.message}`,
206
- ];
207
- }
208
- }
209
- if (gTenants > 0) {
210
- result.graph = { tenants: gTenants, entities: gEntities, relations: gRelations };
211
- }
212
- }
213
- catch (graphErr) {
214
- // SAFETY: this is a best-effort log message only; property access on
215
- // any JS value is safe (undefined if absent), preserving the existing
216
- // lenient formatting even when something non-Error was thrown.
217
- result.details = [
218
- ...(result.details ?? []),
219
- `graph: drain phase failed (skipped): ${graphErr.message}`,
220
- ];
221
- }
222
- }
223
- // One 'consolidate' row per sleep, emitted from finally so a partial failure still reports what got done.
224
- // Its own handle; an audit failure is logged and never replaces the original phase error.
225
- function emitSleepAudit(ctx, opts, dryRun, counts, phaseError) {
226
- try {
227
- const db = openHippoDb(ctx.hippoRoot);
228
- try {
229
- const sleepAuditMetadata = {
230
- consolidationCount: counts.consolidation,
231
- dedupCount: counts.dedup,
232
- auditDeletedCount: counts.auditDeleted,
233
- ambientTotal: counts.ambient,
234
- dryRun,
235
- noShare: opts.noShare ?? false,
236
- partial: phaseError !== null,
237
- triggeredByTenant: ctx.tenantId, // preserve for audit forensics
238
- };
239
- if (phaseError)
240
- sleepAuditMetadata.errorMessage = phaseError.message;
241
- appendAuditEvent(db, {
242
- tenantId: '__host__',
243
- actor: ctx.actor.subject,
244
- op: 'consolidate',
245
- metadata: { ...sleepAuditMetadata },
246
- });
247
- }
248
- finally {
249
- closeHippoDb(db);
250
- }
251
- }
252
- catch (auditErr) {
253
- // Logged, never thrown: a second failure must not mask the original phaseError.
254
- reportAuditWriteFailure('consolidate', String(auditErr));
5
+ if (ctx.store && ctx.store.kind !== 'sqlite') {
6
+ throw new Error(`sleep supports only the sqlite store, not '${ctx.store.kind}'`);
255
7
  }
8
+ return runSleep(ctx, opts);
256
9
  }
257
10
  //# sourceMappingURL=sleep.js.map
@@ -1,4 +1,5 @@
1
1
  import { BadRequestError } from '../api-errors.js';
2
+ import type { HippoStore } from '../store-port.js';
2
3
  /**
3
4
  * Actor identity + authorization role for a Context.
4
5
  *
@@ -16,11 +17,13 @@ export interface Actor {
16
17
  viaAuthResolver?: true;
17
18
  /** The host's operator (CLI, stdio MCP, keyless loopback, host-tenant admin key): may act beyond its tenant. */
18
19
  hostAdmin?: true;
20
+ owner?: string;
19
21
  }
20
22
  export interface Context {
21
23
  hippoRoot: string;
22
24
  tenantId: string;
23
25
  actor: Actor;
26
+ store?: HippoStore;
24
27
  }
25
28
  /**
26
29
  * Helper for building process-local Actor values (admin and host admin),
@@ -30,6 +33,8 @@ export interface Context {
30
33
  * buildContextWithAuth in src/server.ts.
31
34
  */
32
35
  export declare function adminActor(subject: string): Actor;
36
+ /** The per-person key: an unowned key gets its own bucket by key id rather than sharing one. */
37
+ export declare function ownerOrSubject(actor: Actor): string;
33
38
  /**
34
39
  * Thrown by `api.recall` when a caller's options violate a recall contract
35
40
  * that has been opted into via env. Carries a stable `code` field for HTTP /
package/dist/api/types.js CHANGED
@@ -10,6 +10,10 @@ import { BadRequestError } from '../api-errors.js';
10
10
  export function adminActor(subject) {
11
11
  return { subject, role: 'admin', hostAdmin: true };
12
12
  }
13
+ /** The per-person key: an unowned key gets its own bucket by key id rather than sharing one. */
14
+ export function ownerOrSubject(actor) {
15
+ return actor.owner ?? actor.subject;
16
+ }
13
17
  /**
14
18
  * Thrown by `api.recall` when a caller's options violate a recall contract
15
19
  * that has been opted into via env. Carries a stable `code` field for HTTP /
@@ -63,7 +63,7 @@ export function pruneAuditLog(db, opts) {
63
63
  count = Number(row.c);
64
64
  }
65
65
  else {
66
- db.exec('BEGIN');
66
+ db.exec('BEGIN IMMEDIATE');
67
67
  try {
68
68
  const result = db
69
69
  .prepare(`DELETE FROM audit_log WHERE tenant_id = ? AND ts < ?`)
package/dist/audit.d.ts CHANGED
@@ -14,12 +14,10 @@ export interface AuditResult {
14
14
  issues: AuditIssue[];
15
15
  clean: number;
16
16
  }
17
- export declare const STOP_WORDS: Set<string>;
18
17
  export declare function auditMemory(entry: MemoryEntry, backsObject?: boolean): AuditIssue | null;
19
18
  /** `backing`: ids of memories that back an object (store.memoriesBackingObjects). */
20
19
  export declare function auditMemories(entries: MemoryEntry[], backing: ReadonlySet<string>): AuditResult;
21
- export declare function isContentWorthStoring(content: string): boolean;
22
- export declare const AUDIT_OPS: readonly ["remember", "recall", "promote", "supersede", "forget", "archive_raw", "auth_revoke", "auth_create", "outcome", "consolidate", "audit_prune", "summary_marked_dirty", "summary_marked_clean", "summary_rebuilt", "predict_create", "predict_close", "predict_baserate", "recall_autodebias_hint", "recall_autodebias_hint_no_class_match", "recall_autodebias_hint_tiebreak", "recall_anchor_detected_query_repeat", "recall_anchor_detected_memory_dominance", "recall_anchor_skipped_no_session", "recall_availability_detected", "decision_create", "decision_supersede", "decision_close", "incident_open", "incident_resolve", "incident_close", "process_create", "process_supersede", "process_close", "policy_create", "policy_supersede", "policy_close", "skill_create", "skill_supersede", "skill_close", "project_brief_create", "project_brief_supersede", "project_brief_close", "customer_note_create", "customer_note_supersede", "customer_note_close", "mv_rescue", "reject_value", "reject_refusal", "unreject_value", "conflict_resolve", "half_life_migrate", "dormant_restore", "auth_grant", "auth_ungrant", "quarantine", "quarantine_approve", "quarantine_reject", "agent_memory_restore", "agent_memory_set_aside", "project_merge", "project_repair"];
20
+ export declare const AUDIT_OPS: readonly ["remember", "recall", "promote", "supersede", "forget", "archive_raw", "auth_revoke", "auth_create", "outcome", "consolidate", "audit_prune", "summary_marked_dirty", "summary_marked_clean", "summary_rebuilt", "predict_create", "predict_close", "predict_baserate", "recall_autodebias_hint", "recall_autodebias_hint_no_class_match", "recall_autodebias_hint_tiebreak", "recall_anchor_detected_query_repeat", "recall_anchor_detected_memory_dominance", "recall_anchor_skipped_no_session", "recall_availability_detected", "decision_create", "decision_supersede", "decision_close", "incident_open", "incident_resolve", "incident_close", "process_create", "process_supersede", "process_close", "policy_create", "policy_supersede", "policy_close", "skill_create", "skill_supersede", "skill_close", "project_brief_create", "project_brief_supersede", "project_brief_close", "customer_note_create", "customer_note_supersede", "customer_note_close", "mv_rescue", "reject_value", "reject_refusal", "unreject_value", "conflict_resolve", "half_life_migrate", "dormant_restore", "auth_grant", "auth_ungrant", "quarantine", "quarantine_approve", "quarantine_reject", "agent_memory_restore", "agent_memory_set_aside", "project_merge", "project_repair", "quality_repair"];
23
21
  export type AuditOp = (typeof AUDIT_OPS)[number];
24
22
  export interface AppendAuditOpts {
25
23
  tenantId: string;
package/dist/audit.js CHANGED
@@ -2,94 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { canAutoDelete } from './memory.js';
3
3
  import { log } from './log.js';
4
4
  import { keysetAfter } from './keyset.js';
5
- export const STOP_WORDS = new Set([
6
- 'the', 'a', 'an', 'is', 'was', 'are', 'were', 'be', 'been', 'being',
7
- 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'it',
8
- 'this', 'that', 'and', 'or', 'but', 'not', 'no', 'so', 'if', 'do',
9
- 'did', 'does', 'has', 'had', 'have', 'will', 'would', 'could', 'should',
10
- 'may', 'might', 'can', 'shall', 'we', 'i', 'you', 'they', 'he', 'she',
11
- 'my', 'our', 'your', 'its', 'his', 'her', 'their', 'up', 'out', 'just',
12
- 'also', 'then', 'than', 'some', 'all', 'any', 'each', 'very', 'too',
13
- ]);
14
- const VAGUE_ONLY = /^[\w\s,.'"-]+$/;
15
- // Han, Hiragana and Katakana carry no whitespace word boundaries, so a plain
16
- // \s+ split scores
17
- // an entire sentence as one "word" and the gate rejects real sentences as junk.
18
- // CJK words average ~2 characters, so approximate substantive units as one per
19
- // 2 CJK LETTERS.
20
- //
21
- // Two properties this must hold:
22
- // 1. Letters only, enforced by construction: a Katakana BLOCK range counts the
23
- // middle dot and prolonged sound mark, and `\p{Script=Han}` alone counts
24
- // Han NON-letters (Kangxi radicals), so the `(?=\p{L})` lookahead makes
25
- // "letters only" true by definition; tests/df3-cjk-quality-floor.test.ts sweeps it.
26
- //
27
- // SCOPE, stated precisely because the constant name says "CJK": this covers
28
- // Han, Hiragana and Katakana only. Hangul is absent (Korean largely survives
29
- // the whitespace split already) and other spaceless scripts - Thai, Khmer,
30
- // Burmese, Lao - still hit the original one-word failure.
31
- // 2. ADD to the latin count, never strip before it. Stripping CJK first can
32
- // REDUCE the count for short mixed tokens (`UI<han> DB<han> QA<han>` leaves
33
- // three 2-char latin fragments that fail the `> 2` filter), which would
34
- // reject content this gate previously accepted and make capture silently
35
- // drop it. Adding keeps the change strictly more permissive - the property
36
- // that makes it safe in a predicate shared with capture's write gate.
37
- const CJK_LETTERS = /(?=\p{L})(?:\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana})/gu;
38
- function substantiveWordCount(text) {
39
- const cjkLetterCount = (text.match(CJK_LETTERS) ?? []).length;
40
- const latinWordCount = text
41
- .toLowerCase()
42
- .split(/\s+/)
43
- .filter(w => w.length > 2 && !STOP_WORDS.has(w))
44
- .length;
45
- return latinWordCount + Math.floor(cjkLetterCount / 2);
46
- }
47
- function isVersionBump(text) {
48
- const t = text.trim();
49
- // release/bump/prep/tag + version
50
- if (/^(?:bump|release|prep|tag)\s+(?:to\s+)?v?\d+\.\d+/i.test(t))
51
- return true;
52
- // bare semver ("0.24.1", "v1.2.3")
53
- if (/^v?\d+\.\d+\.\d+\s*$/i.test(t))
54
- return true;
55
- // chore: release 1.2.3 / chore(ci): bump v1.2.3
56
- if (/^chore(?:\([^)]+\))?:\s*(?:release|bump|version|tag|prep)\b/i.test(t))
57
- return true;
58
- // Merge commits
59
- if (/^(?:Merge branch|Merge pull request)\b/i.test(t))
60
- return true;
61
- // WIP sentinels
62
- if (/^WIP\b/i.test(t))
63
- return true;
64
- return false;
65
- }
66
- function isFragment(text) {
67
- const trimmed = text.trim();
68
- if (trimmed.startsWith('to ') && trimmed.length < 50)
69
- return true;
70
- if (trimmed.startsWith('for ') && trimmed.length < 50)
71
- return true;
72
- if (trimmed.startsWith('and ') && trimmed.length < 50)
73
- return true;
74
- return false;
75
- }
76
- function hasNoSpecificity(text) {
77
- const words = text.toLowerCase().split(/\s+/);
78
- const hasNumber = /\d/.test(text);
79
- const hasProperNoun = /[A-Z][a-z]{2,}/.test(text);
80
- // An ACRONYM is specificity too: the proper-noun pattern needs lowercase after the capital, so
81
- // "PR", "CI", "DB", "API", "S3" (the densest domain tokens in a technical memory) all read as vague.
82
- // It counts only against ordinary prose (the lowercase test), else a shouted phrase bypasses the gate,
83
- // and CHAT acronyms never count: the gate is shared, so "LGTM ship it" would reach recent-context slots.
84
- const CHAT_ACRONYMS = /^(?:TODO|FYI|LGTM|IIRC|IMO|IMHO|FWIW|TBD|BTW|ASAP|AFAIK|WIP|NB|PS)$/;
85
- const domainAcronyms = (text.match(/\b[A-Z]{2,6}\b/g) ?? []).filter(a => !CHAT_ACRONYMS.test(a));
86
- const hasAcronym = domainAcronyms.length > 0 && /[a-z]/.test(text);
87
- const hasPath = /[/\\.]/.test(text);
88
- const hasCode = /[`_{}()\[\]]/.test(text);
89
- if (hasNumber || hasProperNoun || hasPath || hasCode || hasAcronym)
90
- return false;
91
- return words.length < 8 && VAGUE_ONLY.test(text);
92
- }
5
+ import { automaticDefect, hasNoSpecificity, isFragment, isReleaseCommitNoise, substantiveWordCount, } from './memory-quality.js';
93
6
  export function auditMemory(entry, backsObject = false) {
94
7
  const issue = classifyMemory(entry);
95
8
  // Error means "auto-remove"; a pinned, raw, kept-for-good or object-backing row is never auto-removed, so it can only warn.
@@ -107,7 +20,7 @@ function classifyMemory(entry) {
107
20
  if (content.length < 10) {
108
21
  return { memoryId: entry.id, content, severity: 'error', reason: 'too short (< 10 chars)' };
109
22
  }
110
- if (isVersionBump(content)) {
23
+ if (isReleaseCommitNoise(content)) {
111
24
  return { memoryId: entry.id, content, severity: 'error', reason: 'release/commit noise, not a useful memory' };
112
25
  }
113
26
  if (isFragment(content)) {
@@ -120,6 +33,10 @@ function classifyMemory(entry) {
120
33
  if (content.length < 40 && hasNoSpecificity(content)) {
121
34
  return { memoryId: entry.id, content, severity: 'warning', reason: 'no specific details (names, paths, numbers, code)' };
122
35
  }
36
+ const reason = automaticDefect(entry);
37
+ if (reason !== null) {
38
+ return { memoryId: entry.id, content, severity: 'warning', reason: `automatic memory defect: ${reason}` };
39
+ }
123
40
  return null;
124
41
  }
125
42
  /** `backing`: ids of memories that back an object (store.memoriesBackingObjects). */
@@ -136,20 +53,6 @@ export function auditMemories(entries, backing) {
136
53
  clean: entries.length - issues.length,
137
54
  };
138
55
  }
139
- export function isContentWorthStoring(content) {
140
- const trimmed = content.trim();
141
- if (trimmed.length < 10)
142
- return false;
143
- if (isVersionBump(trimmed))
144
- return false;
145
- if (isFragment(trimmed))
146
- return false;
147
- if (substantiveWordCount(trimmed) < 2)
148
- return false;
149
- if (trimmed.length < 40 && hasNoSpecificity(trimmed))
150
- return false;
151
- return true;
152
- }
153
56
  // ---------------------------------------------------------------------------
154
57
  // Audit log primitives (append-only mutation trail)
155
58
  // ---------------------------------------------------------------------------
@@ -216,6 +119,7 @@ export const AUDIT_OPS = [
216
119
  'agent_memory_set_aside', // emitted by the agent memory sync when a note is deleted or refused
217
120
  'project_merge', // emitted by `hippo projects merge --apply` with every id it touched
218
121
  'project_repair', // emitted by `hippo projects repair --apply` with every id it touched
122
+ 'quality_repair', // emitted by `hippo audit repair --apply` for each memory it moved to dormant storage
219
123
  ];
220
124
  function isBigIntValue(value) {
221
125
  return typeof value === 'bigint';
package/dist/auth.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { type DatabaseSyncLike } from './db.js';
1
+ import type { DatabaseSyncLike } from './db.js';
2
2
  import { type KeysetPosition } from './keyset.js';
3
+ import type { HippoStore } from './store-port.js';
3
4
  /** Every minted API key starts with this, so the server can route a bearer token by shape. */
4
5
  export declare const API_KEY_PREFIX = "hk_";
5
6
  /** Counts since process start, so a flood of bad tokens shows as scrypt work, not just as 401s. */
@@ -13,6 +14,10 @@ export interface CreateApiKeyOpts {
13
14
  label?: string;
14
15
  /** 'admin' | 'member'. Defaults to 'admin' (backward-compat for callers that don't specify). */
15
16
  role?: 'admin' | 'member';
17
+ /** The auth-resolver subject a self-service key belongs to; unset for keys an admin or the CLI mints. */
18
+ ownerSubject?: string;
19
+ /** ISO time the key stops working; unset means it never expires. */
20
+ expiresAt?: string;
16
21
  }
17
22
  export interface CreateApiKeyResult {
18
23
  keyId: string;
@@ -34,6 +39,27 @@ export interface VerifiedApiKey {
34
39
  keyId: string;
35
40
  role: 'admin' | 'member';
36
41
  scopes: string[];
42
+ ownerSubject?: string | null;
43
+ }
44
+ /** One api_keys row and its scope grants as a store returns it, revoked or not; the core checks the secret, the role and the expiry. */
45
+ export interface ApiKeyRecord {
46
+ keyHash: string;
47
+ tenantId: string;
48
+ revokedAt: string | null;
49
+ role: string;
50
+ scopes: string[];
51
+ /** ISO time the key stops working; null when it never expires. */
52
+ expiresAt: string | null;
53
+ /** Who minted the key; optional because a store that omits it fails safe to an unowned key. */
54
+ ownerSubject?: string | null;
55
+ }
56
+ /** The api_keys row for `keyId` with its scope grants; null when no row matches. */
57
+ export declare function readApiKeyRecord(db: DatabaseSyncLike, keyId: string): ApiKeyRecord | null;
58
+ /** A key that passed every check, with the time it stops working so a cache entry never outlives it. */
59
+ export interface CheckedApiKey {
60
+ key: VerifiedApiKey;
61
+ expiresAtMs: number;
62
+ keyHash: string;
37
63
  }
38
64
  export declare function validateApiKey(db: DatabaseSyncLike, plaintext: string): ValidateResult;
39
65
  /** How long a verified key is trusted without a store read; also the ceiling on a revoke made by another process. */
@@ -43,14 +69,21 @@ export declare class VerifiedKeyCache {
43
69
  private readonly capacity;
44
70
  private readonly ttlMs;
45
71
  private readonly entries;
72
+ private deletes;
46
73
  constructor(capacity: number, ttlMs: number);
47
74
  get size(): number;
75
+ /** Moves on every delete, so a store read that straddled a revoke or grant is not cached. */
76
+ get epoch(): number;
77
+ /** The entry this exact token was verified into, fresh or lapsed. */
78
+ private match;
48
79
  get(hippoRoot: string, keyId: string, plaintext: string, now: number): VerifiedApiKey | undefined;
49
- set(hippoRoot: string, keyId: string, plaintext: string, key: VerifiedApiKey, now: number): void;
80
+ /** The key hash this exact token was last verified against, kept past the TTL so a re-check can skip scrypt. */
81
+ verifiedHash(hippoRoot: string, keyId: string, plaintext: string): string | undefined;
82
+ set(hippoRoot: string, keyId: string, plaintext: string, found: CheckedApiKey, now: number): void;
50
83
  delete(keyId: string): void;
51
84
  }
52
- /** Verify a bearer API key for `hippoRoot`; a cache hit skips both scrypt and the DB open. Null when invalid. */
53
- export declare function verifyApiKeyCached(hippoRoot: string, plaintext: string): VerifiedApiKey | null;
85
+ /** Verify a bearer API key against `store`, served at `hippoRoot`; a cache hit skips both scrypt and the store. Null when invalid. A token whose cache entry lapsed re-reads its record without scrypt; a throw from `beforeScrypt` refuses before scrypt runs. */
86
+ export declare function verifyApiKeyCached(hippoRoot: string, plaintext: string, store: HippoStore, beforeScrypt?: () => void): Promise<VerifiedApiKey | null>;
54
87
  export declare function revokeApiKey(db: DatabaseSyncLike, keyId: string): void;
55
88
  /** Grant `keyId` read access to one restricted `scope`. Idempotent. */
56
89
  export declare function grantScope(db: DatabaseSyncLike, keyId: string, scope: string): void;
@@ -71,11 +104,20 @@ export interface ApiKeyListItem {
71
104
  role: 'admin' | 'member';
72
105
  /** Restricted scopes this key may read. */
73
106
  scopes: string[];
107
+ /** ISO time the key stops working; null when it never expires. */
108
+ expiresAt: string | null;
109
+ /** The auth-resolver subject that minted this key for itself; null for keys an admin or the CLI minted. */
110
+ ownerSubject: string | null;
74
111
  }
75
112
  export interface ListApiKeysOpts {
113
+ /** Only keys that still work: unrevoked and unexpired. */
76
114
  active: boolean;
77
115
  /** Only this tenant's keys; omit for every tenant (the CLI's single-tenant view). */
78
116
  tenantId?: string;
117
+ /** Only keys minted by this auth-resolver subject. */
118
+ ownerSubject?: string;
119
+ /** Only this one key. */
120
+ keyId?: string;
79
121
  /** Resume after this row: the position the previous page ended on (key and id are both the row id). */
80
122
  after?: KeysetPosition;
81
123
  limit?: number;
@@ -88,4 +130,6 @@ export interface ApiKeyListRow {
88
130
  /** Keys newest first, with their row ids, filtered and limited in SQL. */
89
131
  export declare function listApiKeyRows(db: DatabaseSyncLike, opts: ListApiKeysOpts): ApiKeyListRow[];
90
132
  export declare function listApiKeys(db: DatabaseSyncLike, opts: ListApiKeysOpts): ApiKeyListItem[];
133
+ /** Ids of the unrevoked, unexpired keys `ownerSubject` minted in `tenantId`, oldest first. */
134
+ export declare function listLiveOwnedKeyIds(db: DatabaseSyncLike, tenantId: string, ownerSubject: string, now: number): string[];
91
135
  //# sourceMappingURL=auth.d.ts.map