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
@@ -2,7 +2,6 @@
2
2
  import * as path from 'path';
3
3
  import * as fs from 'fs';
4
4
  import { spawn } from 'child_process';
5
- import { defaultSleepLogPath } from '../hooks/shared.js';
6
5
  import { resolveCodexSessionTranscript } from '../hooks/codex-session.js';
7
6
  import { resolveCodexWrapperPaths } from '../hooks/codex-wrapper.js';
8
7
  import { isInitialized } from '../store/open.js';
@@ -17,45 +16,20 @@ import { summaryLine } from '../agent-memories/report.js';
17
16
  import { resolveProjectIdentity } from '../project-identity.js';
18
17
  import { getGlobalRoot } from '../shared.js';
19
18
  import { cmdCapture } from '../capture/command.js';
20
- import { cmdPreCompact, cmdPostCompact, transcriptWorkingState } from '../capture/compact.js';
19
+ import { cmdPreCompact, cmdPostCompact } from '../capture/compact.js';
20
+ import { transcriptWorkingState } from '../capture/working-state.js';
21
+ import { collectHandoffEvidence } from '../handoff-evidence.js';
21
22
  import { resolveLastSessionTranscript } from '../capture/transcript.js';
22
23
  import { truncateCodePointSafe } from '../transcript-tail.js';
23
24
  import { COMPACTION_DB_WAIT_MS } from '../compaction-record.js';
25
+ import { COMPACT_RESUME_EVENT_CONTENT_CAP, COMPACT_RESUME_MAX_AGE_MS, compactResumeText } from '../context-render.js';
24
26
  import { readStdinBounded } from '../stdin.js';
25
27
  import { resolveTenantId } from '../tenant.js';
26
- import { log } from '../log.js';
28
+ import { errorMessage, log } from '../log.js';
29
+ import { withLedgerDb } from '../ledger-db.js';
27
30
  import { printError } from './output.js';
28
- import { collectHandoffEvidence, logSessionEndImport, appendSessionEndCloseLog, printActiveTaskSnapshot, printSessionEvents, resetHookInjection, captureConsole, hookStoreRoot, withLedgerDb, runHookWithStores, inPilotHoldout, } from './shared.js';
29
- /** Prints the SessionEnd sleep log, then clears it. Stderr, because Claude Code adds
30
- * SessionStart stdout to the model's context and this log is for the user. */
31
- export function cmdLastSleep(flags) {
32
- const logPath = typeof flags['path'] === 'string'
33
- ? flags['path']
34
- : defaultSleepLogPath();
35
- if (!fs.existsSync(logPath))
36
- return;
37
- let content;
38
- try {
39
- content = fs.readFileSync(logPath, 'utf8');
40
- }
41
- catch {
42
- // Removed or locked since the exists check: there is nothing to show this session.
43
- return;
44
- }
45
- if (content.trim().length > 0) {
46
- printError('=== Previous session hippo consolidation ===');
47
- process.stderr.write(content);
48
- if (!content.endsWith('\n'))
49
- printError();
50
- printError('===========================================');
51
- }
52
- if (!flags['keep']) {
53
- try {
54
- fs.unlinkSync(logPath);
55
- }
56
- catch { /* non-fatal */ }
57
- }
58
- }
31
+ import { cmdLastSleep } from './last-sleep.js';
32
+ import { logSessionEndImport, appendSessionEndCloseLog, resetHookInjection, hookStoreRoot, runHookWithStores, inPilotHoldout, } from './shared.js';
59
33
  /**
60
34
  * SessionStart(compact) injector. Prints the active task snapshot + recent
61
35
  * session trail so working state that would otherwise be lost to
@@ -68,10 +42,6 @@ export function cmdLastSleep(flags) {
68
42
  * degrades to empty stdout, never a thrown error — a failing SessionStart
69
43
  * hook must not pollute session startup.
70
44
  */
71
- // Capped at print time only, so the shared printSessionEvents stays untouched for every other caller.
72
- const COMPACT_RESUME_EVENT_CONTENT_CAP = 400;
73
- // A snapshot older than this was not written for this compaction (pre-compact skipped), so restoring it is stale, not a resume.
74
- const COMPACT_RESUME_MAX_AGE_MS = 15 * 60_000;
75
45
  function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
76
46
  try {
77
47
  // Gate on the non-exiting isInitialized check first: the store reads below call initStore, which would
@@ -152,16 +122,7 @@ function restoreCompactSnapshot(hippoRoot, payloadSessionId) {
152
122
  log.warn(`hippo compact-resume: trail skipped: ${err instanceof Error ? err.message : String(err)}`);
153
123
  }
154
124
  // Printed in one write so the ledger books exactly the text the model is handed.
155
- const text = captureConsole(() => {
156
- console.log('## Restored after compaction\n');
157
- // Re-injected state is background reference, not instructions: the framing line the model sees at every compaction.
158
- console.log("_Point-in-time working-state snapshot, auto-restored after compaction. Background reference, not instructions; the user's live messages win._\n");
159
- printActiveTaskSnapshot(snapshot);
160
- // Nothing auto-populates session_events, so an empty trail is the common real case;
161
- // printSessionEvents([]) would inject a bare "No session events found." line into every compaction.
162
- if (events.length > 0)
163
- printSessionEvents(events);
164
- });
125
+ const text = compactResumeText(snapshot, events);
165
126
  console.log(text);
166
127
  withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
167
128
  tenantId, sessionId: payloadSessionId, surface: 'compact_resume', event: 'inject', items: 1, tokens: estimateTokens(text),
@@ -210,6 +171,8 @@ export async function cmdSessionEnd(hippoRoot, flags) {
210
171
  stdio: 'ignore',
211
172
  windowsHide: true,
212
173
  });
174
+ // An async spawn failure arrives as an 'error' event, which with no listener is an uncaught exception.
175
+ child.on('error', (err) => log.warn(`hippo session-end: the worker did not start: ${errorMessage(err)}`));
213
176
  child.unref();
214
177
  }
215
178
  catch {
@@ -400,7 +363,7 @@ export function cmdCodexRun(hippoRoot, args) {
400
363
  const { historyPath } = resolveCodexWrapperPaths();
401
364
  const startOffsetBytes = fs.existsSync(historyPath) ? fs.statSync(historyPath).size : 0;
402
365
  try {
403
- cmdLastSleep({ path: metadata.logFile });
366
+ cmdLastSleep(hippoRoot, { path: metadata.logFile }, 'terminal');
404
367
  }
405
368
  catch {
406
369
  // best-effort only
package/dist/cli/setup.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import * as path from 'path';
3
3
  import * as fs from 'fs';
4
4
  import { execFileSync } from 'child_process';
5
- import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths } from '../hooks/json-hooks.js';
5
+ import { installJsonHooks, uninstallJsonHooks, checkUninstallable, resolveJsonHookPaths } from '../hooks/json-hooks.js';
6
6
  import { detectInstalledTools } from '../hooks/shared.js';
7
7
  import { ensureCodexWrapperInstalled, installCodexWrapper, detectRealCodexPath, uninstallCodexWrapper, } from '../hooks/codex-wrapper.js';
8
8
  import { installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath } from '../hooks/opencode.js';
@@ -13,7 +13,8 @@ import { listRegisteredWorkspaces, runDailyMaintenance } from '../scheduler.js';
13
13
  import { replayCompactionsAt } from '../compaction-record.js';
14
14
  import { log } from '../log.js';
15
15
  import { printError } from './output.js';
16
- import { printAgentImport, installCodexMemoryHooks, setupDailySchedule } from './shared.js';
16
+ import { printAgentImport, installCodexMemoryHooks, setupDailySchedule, warnClaudeSettingsUnusable } from './shared.js';
17
+ import { repairQualityOnceAt } from './quality-repair-once.js';
17
18
  import { HOOK_MARKERS, HOOKS, hippoBlock } from './hook-blocks.js';
18
19
  import { escapeRegex } from '../escape.js';
19
20
  // ---------------------------------------------------------------------------
@@ -101,6 +102,7 @@ function patchAgentFile(hook, target) {
101
102
  }
102
103
  }
103
104
  function printClaudeHookInstall(result) {
105
+ warnClaudeSettingsUnusable(result, '');
104
106
  if (result.installedSessionEnd) {
105
107
  console.log(`Installed hippo session-end SessionEnd hook in ${result.target} settings`);
106
108
  }
@@ -159,6 +161,9 @@ function hookUninstall(target) {
159
161
  if (uninstallJsonHooks(target)) {
160
162
  console.log(`Removed hippo hooks from ${target} settings`);
161
163
  }
164
+ else {
165
+ warnClaudeSettingsUnusable(checkUninstallable(target), '', 'uninstall');
166
+ }
162
167
  }
163
168
  else if (target === 'opencode') {
164
169
  // opencode uses a TS plugin; uninstall removes the plugin file AND
@@ -280,6 +285,8 @@ function setupJsonTool(tool, dryRun) {
280
285
  return;
281
286
  }
282
287
  const result = installJsonHooks(tool.name);
288
+ if (warnClaudeSettingsUnusable(result, ` ${tool.name.padEnd(14)} `))
289
+ return;
283
290
  const bits = [];
284
291
  if (result.installedSessionEnd)
285
292
  bits.push('SessionEnd (session-end)');
@@ -362,6 +369,7 @@ export function cmdDailyRunner() {
362
369
  const finished = replayCompactionsAt(globalRoot, (message) => log.warn(`compaction replay: ${message}`));
363
370
  if (finished > 0)
364
371
  console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over in the global store.`);
372
+ repairQualityOnceAt(globalRoot);
365
373
  }
366
374
  printAgentImport(importUserMemories(globalRoot, { machine: currentMachine() }), '');
367
375
  const workspaces = listRegisteredWorkspaces(globalRoot);
@@ -1,8 +1,8 @@
1
+ import { type InstallResult } from '../hooks/json-hooks.js';
1
2
  import { TaskSnapshot, SessionEvent } from '../store/rows.js';
2
- import type { HandoffEvidence, SessionHandoff } from '../handoff.js';
3
+ import type { SessionHandoff } from '../handoff.js';
3
4
  import type { SearchResult } from '../search/types.js';
4
5
  import { type HippoConfig } from '../config.js';
5
- import { openHippoDb } from '../db.js';
6
6
  import { type ImportReport } from '../agent-memories/report.js';
7
7
  import { type ChurnStaleResult } from '../invalidation.js';
8
8
  import { type AuditOp } from '../audit.js';
@@ -44,8 +44,12 @@ export declare function recallEntryText(r: SearchResult, query: string, showWhy:
44
44
  export declare function recallHeading(entries: number, tokens: number, query: string): string;
45
45
  /** One line when an agent memory import moved anything; its warnings go to stderr. */
46
46
  export declare function printAgentImport(report: ImportReport, indent?: string): void;
47
+ /** True, after one line, on a shared store: this account's commits and agent notes are not its members' memories. */
48
+ export declare function skipLearnOnSharedStore(hippoRoot: string): boolean;
47
49
  /** Adds hippo's two Codex hooks and says what changed; each install ends on the trust reminder, since Codex skips an untrusted hook. */
48
50
  export declare function installCodexMemoryHooks(indent: string): void;
51
+ /** The one line init, hook install, hook uninstall and setup print when Claude Code's settings.json is not JSON hippo can edit and so was left unchanged; true when it printed. */
52
+ export declare function warnClaudeSettingsUnusable(result: Pick<InstallResult, 'settingsPath' | 'invalidJson'>, indent: string, action?: 'install' | 'uninstall'): boolean;
49
53
  /**
50
54
  * Set up a machine-level daily runner that sweeps all registered Hippo
51
55
  * workspaces.
@@ -70,7 +74,6 @@ export declare function engineFlags(flags: CliFlags, config: HippoConfig): Engin
70
74
  * `__session-end-worker` subcommand (not user-facing). Failures in one stage
71
75
  * do not block the other.
72
76
  */
73
- export declare function collectHandoffEvidence(cwd: string, testStatus: HandoffEvidence['testStatus']): HandoffEvidence;
74
77
  /** A folder without its own store never sleeps at session end, so its project's agent notes go to the global store here. */
75
78
  export declare function logSessionEndImport(logFile: string | null, transcriptPath: string | undefined): void;
76
79
  /**
@@ -110,13 +113,6 @@ export declare function captureConsole(fn: () => void): string;
110
113
  * is looked for in the other.
111
114
  */
112
115
  export declare function hookStoreRoot(hippoRoot: string): string;
113
- /**
114
- * Run `fn` against the token ledger's store: the local store when it is
115
- * initialized, else the global one (the per-prompt hook runs in directories
116
- * without a local store). Best-effort: returns undefined and never throws,
117
- * because a ledger failure must not break context or recall.
118
- */
119
- export declare function withLedgerDb<T>(hippoRoot: string, fn: (db: ReturnType<typeof openHippoDb>) => T): T | undefined;
120
116
  export declare function learnFromRepo(hippoRoot: string, repoPath: string, days: number, label?: string): {
121
117
  added: number;
122
118
  skipped: number;
@@ -125,9 +121,6 @@ export declare function learnFromRepo(hippoRoot: string, repoPath: string, days:
125
121
  export declare function resolveAuthRoot(hippoRoot: string, flags: Record<string, string | boolean | string[]>): string;
126
122
  /** Hook commands share one handle per store and wait at most HOOK_DB_WAIT_MS for a lock; a store still busy after that skips the hook's work with one warning, exit 0. */
127
123
  export declare function runHookWithStores<T>(fn: () => T | Promise<T>): Promise<T | undefined>;
128
- /**
129
- * Whether this session sits in the pilot's holdout arm (src/pilot-arm.ts). Off at rate 0 and with no session id.
130
- * `write` books the arm row; a read-only caller (env-only id, sub-agent) follows the stored arm, else the hash.
131
- */
124
+ /** Whether this session sits in the pilot's holdout arm; off at rate 0 and with no session id. */
132
125
  export declare function inPilotHoldout(hippoRoot: string, tenantId: string, sessionId: string | undefined, write: boolean): boolean;
133
126
  //# sourceMappingURL=shared.d.ts.map
@@ -9,9 +9,10 @@ import { CODEX_TRUST_LINE } from '../hooks/shared.js';
9
9
  import { confidenceLabel } from '../memory.js';
10
10
  import { isInitialized } from '../store/open.js';
11
11
  import { explainMatch } from '../search/explain.js';
12
- import { loadConfig } from '../config.js';
12
+ import { isSharedStore } from '../config.js';
13
13
  import { openHippoDb, closeHippoDb, isSqliteBusy, noteStoreBusy, withSharedStoreHandles, HOOK_DB_WAIT_MS } from '../db.js';
14
- import { ensurePilotArm, hashArm, readPilotArm } from '../pilot-arm.js';
14
+ import { withLedgerDb } from '../ledger-db.js';
15
+ import { sessionPilotArm } from '../pilot-arm.js';
15
16
  import { hookPayloadSessionId, isSubagentPayload, recordTokenUse } from '../token-ledger.js';
16
17
  import { importAtSessionEnd, currentMachine } from '../agent-memories/sync.js';
17
18
  import { summaryLine } from '../agent-memories/report.js';
@@ -218,6 +219,13 @@ export function printAgentImport(report, indent = ' ') {
218
219
  for (const warning of report.warnings)
219
220
  printError(`hippo: agent memories: ${warning}`);
220
221
  }
222
+ /** True, after one line, on a shared store: this account's commits and agent notes are not its members' memories. */
223
+ export function skipLearnOnSharedStore(hippoRoot) {
224
+ if (!isSharedStore(hippoRoot))
225
+ return false;
226
+ console.log("Shared store: skipped learning from this account's git commits and coding agents' own memories.");
227
+ return true;
228
+ }
221
229
  /** Adds hippo's two Codex hooks and says what changed; each install ends on the trust reminder, since Codex skips an untrusted hook. */
222
230
  export function installCodexMemoryHooks(indent) {
223
231
  const result = installJsonHooks('codex');
@@ -234,6 +242,13 @@ export function installCodexMemoryHooks(indent) {
234
242
  : `${indent}hippo's Codex memory hooks already in ${result.settingsPath}`);
235
243
  console.log(`${indent}${CODEX_TRUST_LINE}`);
236
244
  }
245
+ /** The one line init, hook install, hook uninstall and setup print when Claude Code's settings.json is not JSON hippo can edit and so was left unchanged; true when it printed. */
246
+ export function warnClaudeSettingsUnusable(result, indent, action = 'install') {
247
+ if (!result.invalidJson)
248
+ return false;
249
+ console.log(`${indent}WARNING: ${result.settingsPath} is not a JSON object hippo can merge into, so it was left unchanged; fix it, then run \`hippo hook ${action} claude-code\`.`);
250
+ return true;
251
+ }
237
252
  /**
238
253
  * Set up a machine-level daily runner that sweeps all registered Hippo
239
254
  * workspaces.
@@ -320,32 +335,6 @@ export function engineFlags(flags, config) {
320
335
  * `__session-end-worker` subcommand (not user-facing). Failures in one stage
321
336
  * do not block the other.
322
337
  */
323
- // Best-effort git state; a missing git, non-repo cwd, or the timeout all
324
- // yield null fields rather than throw (autolearn.ts execFileSync shape).
325
- export function collectHandoffEvidence(cwd, testStatus) {
326
- let gitRef = null;
327
- try {
328
- gitRef = execFileSync('git', ['rev-parse', 'HEAD'], {
329
- cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
330
- }).trim() || null;
331
- }
332
- catch {
333
- // No git, not a repo, or timed out: evidence is optional, so the field stays null.
334
- gitRef = null;
335
- }
336
- let dirtyTree = null;
337
- try {
338
- const status = execFileSync('git', ['status', '--porcelain'], {
339
- cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
340
- });
341
- dirtyTree = status.trim().length > 0;
342
- }
343
- catch {
344
- // Same as gitRef: unknown tree state is reported as null, never as an error.
345
- dirtyTree = null;
346
- }
347
- return { gitRef, dirtyTree, testStatus };
348
- }
349
338
  /** A folder without its own store never sleeps at session end, so its project's agent notes go to the global store here. */
350
339
  export function logSessionEndImport(logFile, transcriptPath) {
351
340
  try {
@@ -452,42 +441,6 @@ export function hookStoreRoot(hippoRoot) {
452
441
  const globalRoot = getGlobalRoot();
453
442
  return isInitialized(globalRoot) ? globalRoot : hippoRoot;
454
443
  }
455
- /**
456
- * Run `fn` against the token ledger's store: the local store when it is
457
- * initialized, else the global one (the per-prompt hook runs in directories
458
- * without a local store). Best-effort: returns undefined and never throws,
459
- * because a ledger failure must not break context or recall.
460
- */
461
- export function withLedgerDb(hippoRoot, fn) {
462
- let root = null;
463
- try {
464
- if (isInitialized(hippoRoot))
465
- root = hippoRoot;
466
- else if (isInitialized(getGlobalRoot()))
467
- root = getGlobalRoot();
468
- }
469
- catch {
470
- // An unreadable store root means no ledger write; the ledger must never break context or recall.
471
- return undefined;
472
- }
473
- if (root === null)
474
- return undefined;
475
- let db;
476
- try {
477
- db = openHippoDb(root);
478
- return fn(db);
479
- }
480
- catch (error) {
481
- // Best effort, but a busy store is the one failure an operator can act on, so it warns once.
482
- if (isSqliteBusy(error))
483
- noteStoreBusy('token ledger row skipped');
484
- return undefined;
485
- }
486
- finally {
487
- if (db)
488
- closeHippoDb(db);
489
- }
490
- }
491
444
  export function learnFromRepo(hippoRoot, repoPath, days, label) {
492
445
  const prefix = label ? `[${label}] ` : '';
493
446
  const ctx = { hippoRoot, tenantId: resolveTenantId({}), actor: adminActor('cli') };
@@ -530,20 +483,8 @@ export async function runHookWithStores(fn) {
530
483
  return undefined;
531
484
  }
532
485
  }
533
- /**
534
- * Whether this session sits in the pilot's holdout arm (src/pilot-arm.ts). Off at rate 0 and with no session id.
535
- * `write` books the arm row; a read-only caller (env-only id, sub-agent) follows the stored arm, else the hash.
536
- */
486
+ /** Whether this session sits in the pilot's holdout arm; off at rate 0 and with no session id. */
537
487
  export function inPilotHoldout(hippoRoot, tenantId, sessionId, write) {
538
- if (sessionId === undefined || sessionId.trim() === '')
539
- return false;
540
- const root = isInitialized(hippoRoot) ? hippoRoot : isInitialized(getGlobalRoot()) ? getGlobalRoot() : null;
541
- if (root === null)
542
- return false;
543
- const rate = loadConfig(root).pilot.holdoutRateBp;
544
- if (rate <= 0)
545
- return false;
546
- const arm = withLedgerDb(hippoRoot, (db) => write ? ensurePilotArm(db, tenantId, sessionId, rate) : readPilotArm(db, sessionId) ?? hashArm(sessionId, rate));
547
- return (arm ?? hashArm(sessionId, rate)) === 'holdout';
488
+ return sessionPilotArm(hippoRoot, tenantId, sessionId, write) === 'holdout';
548
489
  }
549
490
  //# sourceMappingURL=shared.js.map
package/dist/cli/sleep.js CHANGED
@@ -11,7 +11,8 @@ import { renderAmbientSummary } from '../ambient.js';
11
11
  import { errorMessage, log } from '../log.js';
12
12
  import { closeHippoDb, openHippoDb } from '../db.js';
13
13
  import { repairOnceOnSleep } from '../project-merge.js';
14
- import { requireInit, learnFromRepo, runChurnStaleForRepo, printAgentImport } from './shared.js';
14
+ import { requireInit, learnFromRepo, runChurnStaleForRepo, printAgentImport, skipLearnOnSharedStore } from './shared.js';
15
+ import { repairQualityOnceAt } from './quality-repair-once.js';
15
16
  import { printError } from './output.js';
16
17
  /** Runs `hippo sleep`; with `--log-file` it also tees its output to that file. */
17
18
  export async function cmdSleep(hippoRoot, flags) {
@@ -156,10 +157,11 @@ async function cmdSleepCore(hippoRoot, flags) {
156
157
  requireInit(hippoRoot);
157
158
  // Phase 1: Auto-learn from git and every coding agent's own memories (CLI-only, uses process.cwd() / os.homedir()).
158
159
  // Stays in cli.ts; api.sleep covers Phase 2-6 only.
159
- if (!flags['no-learn'] && flags['dry-run']) {
160
+ const learn = !flags['no-learn'] && !skipLearnOnSharedStore(hippoRoot);
161
+ if (learn && flags['dry-run']) {
160
162
  console.log("Dry run: skipped learning from git commits and coding agents' own memories (`hippo import --agents --dry-run` previews those).");
161
163
  }
162
- else if (!flags['no-learn']) {
164
+ else if (learn) {
163
165
  const config = loadConfig(hippoRoot);
164
166
  if (config.autoLearnOnSleep && isGitRepo(process.cwd())) {
165
167
  const { added } = learnFromRepo(hippoRoot, process.cwd(), 1);
@@ -183,6 +185,7 @@ async function cmdSleepCore(hippoRoot, flags) {
183
185
  if (finished > 0)
184
186
  console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over from earlier sessions.`);
185
187
  repairProjectTagsOnce(hippoRoot);
188
+ repairQualityOnceAt(hippoRoot);
186
189
  }
187
190
  // Phase 2-6: Pure-storage pipeline (consolidate + dedup + audit + share + ambient).
188
191
  const ctx = {
package/dist/cli/usage.js CHANGED
@@ -248,7 +248,7 @@ export const VERB_USAGE = {
248
248
  --no-learn Skip auto git-learn and the agent memory import before consolidation
249
249
  --no-share Skip auto-sharing to global store`],
250
250
  'last-sleep': [`
251
- last-sleep Print the last 'hippo sleep --log-file' output to stderr and clear it
251
+ last-sleep Show the last sleep log on stderr, one problems line to the user, and clear it
252
252
  --path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
253
253
  --keep Print without clearing`],
254
254
  'session-end': [`
@@ -287,7 +287,7 @@ export const VERB_USAGE = {
287
287
  --json Output as JSON
288
288
  --global Operate on the global store
289
289
  auth list List API keys (active by default)
290
- --all Include revoked keys
290
+ --all Include revoked and expired keys
291
291
  --json Output as JSON
292
292
  --global Operate on the global store
293
293
  auth revoke <key_id> Revoke an API key (subsequent validate fails)
@@ -333,7 +333,15 @@ export const VERB_USAGE = {
333
333
  dlq replay <id> [--force]
334
334
  Re-ingest a DLQ entry (--force skips sig check)`],
335
335
  audit: [`
336
- audit [--fix] Check memory quality (--fix removes junk)`, `
336
+ audit [--fix] Check memory quality (--fix removes junk)
337
+ audit repair [--apply] Preview repair of memories hippo wrote itself; --apply moves certain
338
+ defects to dormant storage. Sleep and the daily runner apply it once
339
+ per store after an upgrade
340
+ --json Report ids, reasons, protections and schema blockers as JSON
341
+ --global Operate on the global store without changing its schema
342
+ Writes a database backup first; undo with hippo dormant restore <id>,
343
+ which marks the memory verified so repair leaves it alone. Sleep's
344
+ dormant retention still applies; the backup stays until you remove it.`, `
337
345
  audit <sub> Query the append-only audit log (A5 stub auth)
338
346
  audit list List audit events for the active tenant
339
347
  --op <op> Filter by op (remember | recall | promote |
@@ -542,17 +550,18 @@ export const VERB_USAGE = {
542
550
  hook <sub> [target] Manage framework integrations
543
551
  hook list Show available hooks
544
552
  hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
545
- claude-code adds 7 hooks to ~/.claude/settings.json;
546
- opencode installs a plugin; codex adds 2 hooks to
547
- $CODEX_HOME/hooks.json (trust them once in /hooks) and
553
+ claude-code adds 7 hooks to $CLAUDE_CONFIG_DIR/settings.json
554
+ (~/.claude by default); opencode installs a plugin; codex
555
+ adds 2 hooks to $CODEX_HOME/hooks.json (trust them once in /hooks) and
548
556
  wraps the detected launcher in place; all but claude-code
549
557
  also patch an existing AGENTS.md
550
558
  hook uninstall <target> Remove hook`],
551
559
  setup: [`
552
560
  setup One-shot: detect installed AI tools and install their hooks:
553
- claude-code gets 7 hooks in ~/.claude/settings.json, opencode
554
- a plugin, codex 2 hooks in its hooks.json plus a launcher
555
- wrapper; other tools get a hint. Then imports each agent's
561
+ claude-code gets 7 hooks in $CLAUDE_CONFIG_DIR/settings.json
562
+ (~/.claude by default), opencode a plugin, codex 2 hooks in
563
+ its hooks.json plus a launcher wrapper; other tools get a
564
+ hint. Then imports each agent's
556
565
  user-level memories into the global store
557
566
  --all Install for every JSON-hook tool, even if not detected
558
567
  --dry-run Show what would be installed without writing
package/dist/cli.d.ts CHANGED
@@ -89,7 +89,7 @@ export declare const COMMANDS: {
89
89
  usage: string[];
90
90
  };
91
91
  'last-sleep': {
92
- run: ({ flags }: CommandContext) => Promise<void>;
92
+ run: ({ hippoRoot, flags }: CommandContext) => Promise<void>;
93
93
  usage: string[];
94
94
  };
95
95
  'session-end': {
package/dist/cli.js CHANGED
@@ -41,6 +41,7 @@ import { repairCodexWrapperIfInstalled } from './hooks/codex-wrapper.js';
41
41
  import { getHippoRoot } from './store/open.js';
42
42
  import { cmdGithub, printGithubBackfillUsage } from './connectors/github/cli-impl.js';
43
43
  import { printError } from './cli/output.js';
44
+ import { isStoreBusy, STORE_BUSY_MESSAGE } from './db/busy.js';
44
45
  import { VERB_USAGE, USAGE_HEADER, USAGE_EXAMPLES, printAuditPruneUsage, printSlackBackfillUsage, printSlackWorkspacesUsage } from './cli/usage.js';
45
46
  // ---------------------------------------------------------------------------
46
47
  // Helpers
@@ -243,7 +244,7 @@ export const COMMANDS = {
243
244
  usage: VERB_USAGE.sleep,
244
245
  },
245
246
  'last-sleep': {
246
- run: async ({ flags }) => { (await import('./cli/session-hooks.js')).cmdLastSleep(flags); },
247
+ run: async ({ hippoRoot, flags }) => { (await import('./cli/last-sleep.js')).cmdLastSleep(hippoRoot, flags); },
247
248
  usage: VERB_USAGE['last-sleep'],
248
249
  },
249
250
  'session-end': {
@@ -637,7 +638,7 @@ export async function runCli(argv = process.argv) {
637
638
  await main(command, args, flags, getHippoRoot(process.cwd()));
638
639
  }
639
640
  catch (err) {
640
- printError('Error:', err instanceof Error ? err.message : err);
641
+ printError('Error:', isStoreBusy(err) ? STORE_BUSY_MESSAGE : err instanceof Error ? err.message : err);
641
642
  process.exit(1);
642
643
  }
643
644
  }
package/dist/client.js CHANGED
@@ -22,7 +22,7 @@ const STORE_BUSY_ATTEMPTS = 5;
22
22
  function isStoreBusy(res) {
23
23
  return res.status === 503 && res.headers.has('retry-after');
24
24
  }
25
- /** Replaying is safe because each routed write commits in one transaction and anything after it is best-effort, so a busy 503 means nothing landed. */
25
+ /** Replaying is safe because each routed write commits in one transaction and anything after it is best-effort, so a busy 503 means nothing landed; a sleep is safe too, as a re-run finishes a partial one. */
26
26
  function sendWrite(url, init) {
27
27
  return fetchWithRetry(url, init, { timeoutMs: SERVER_TIMEOUT_MS, attempts: STORE_BUSY_ATTEMPTS, retryOn: isStoreBusy });
28
28
  }
@@ -33,6 +33,8 @@ interface ScrubbedSummary {
33
33
  }
34
34
  /** What the record keeps of a compact_summary: the scrubbed body, and every parsed item scrubbed. */
35
35
  export declare function readCompactionText(compactSummary: string): ScrubbedSummary;
36
+ /** A caller's items scrubbed as readCompactionText scrubs a summary's, since another machine's scrub is not trusted; cut after it, as a mask can run longer than what it hides. */
37
+ export declare function scrubCompactionItems(items: readonly string[]): string[];
36
38
  export interface CompactionStart {
37
39
  sessionId: string;
38
40
  originProject: string;
@@ -43,13 +45,24 @@ export interface CompactionStart {
43
45
  export declare function startCompaction(db: DatabaseSyncLike, tenantId: string, start: CompactionStart, at?: Date): string;
44
46
  /** A session's newest record, or null. */
45
47
  export declare function latestCompaction(db: DatabaseSyncLike, tenantId: string, sessionId: string): CompactionRecord | null;
48
+ /** The record a caller's request made, whatever state it reached, so a retry neither writes its items twice nor starts a second record; another session's id is a ConflictError. */
49
+ export declare function compactionByRequest(db: DatabaseSyncLike, tenantId: string, requestId: string, sessionId: string): CompactionRecord | null;
46
50
  /** Best effort, never throws: a compaction must not fail because its record could not be written. */
47
51
  export declare function recordCompactionStart(hippoRoot: string, start: Omit<CompactionStart, 'originProject'>, log: Log): string | null;
48
- export declare function recordSnapshotSaved(hippoRoot: string, recordId: string, log: Log): void;
52
+ /** On the caller's handle and tenant, so a server marks the record under the caller's tenant with its own wait. */
53
+ export declare function markSnapshotSaved(db: DatabaseSyncLike, tenantId: string, recordId: string): void;
54
+ export declare function recordSnapshotSaved(hippoRoot: string, tenantId: string, recordId: string, log: Log): void;
49
55
  export interface CompactionText {
50
56
  summary: string;
51
57
  items: string[];
52
58
  }
59
+ /** A caller's checked project, and the id its retries carry so a retry finds this record. */
60
+ export interface SummaryCaller {
61
+ originProject?: string;
62
+ requestId?: string;
63
+ }
64
+ /** Puts the summary on the session's `started` record, or inserts a `summarised` one when pre-compact wrote none. One statement each. */
65
+ export declare function recordSummary(db: DatabaseSyncLike, hippoRoot: string, tenantId: string, meta: Omit<CompactionStart, 'originProject'>, text: CompactionText, at: Date, caller?: SummaryCaller): CompactionRecord;
53
66
  export interface ItemContext {
54
67
  tenantId: string;
55
68
  /** null when the record step failed: the items still go in, with no record to close. */
@@ -59,6 +72,11 @@ export interface ItemContext {
59
72
  /** Where the session ran, so rows held under the project's older folder name count too. */
60
73
  cwd: string | null;
61
74
  items: string[];
75
+ /** Set when another machine sent the items: its audit actor and its project's names, since the server's folder is neither. */
76
+ caller?: {
77
+ actor: string;
78
+ origins: readonly string[];
79
+ };
62
80
  }
63
81
  /** Items that become rows, then the record `done`, in one transaction so two sessions cannot both insert one text. Mirrors after commit. */
64
82
  export declare function saveItems(db: DatabaseSyncLike, hippoRoot: string, ctx: ItemContext, log: Log): number;