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
@@ -7,63 +7,7 @@ import { loadContentsWithTag } from './store/entry-reads.js';
7
7
  import { loadConfig } from './config.js';
8
8
  import { closeHippoDb, openHippoDb } from './db.js';
9
9
  import { recordFailure } from './failure-log.js';
10
- import { blockHash } from './token-ledger.js';
11
- import { redactSecretsStrict } from './secret-detect.js';
12
- const MAX_LEN = 200;
13
- /** The user, a permission prompt or a hook said no: routine, not a lesson. */
14
- const DECLINED = /user (?:doesn't|does not) want|denied by (?:the )?user|user (?:rejected|declined|denied)|permission to use|was blocked by (?:a )?hook/i;
15
- /** The OS or a remote host refused access (EACCES, SSH publickey): routine too, but nobody declined anything. */
16
- const OS_PERMISSION = /permission denied/i;
17
- /** Leading `cd` or `pushd` steps say where a command ran, not what it ran. */
18
- const LEADING_CD = /^\s*(?:(?:cd|pushd)\b[^;&|]*(?:&&|\|\||;)\s*)+/;
19
- const NO_MATCH = /\bno (?:matches|files|results) found\b/i;
20
- /** Shell commands whose exit code 1 means "nothing found" or "differs", not an error. */
21
- const QUIET_EXIT_1 = /^\s*(?:grep|rg|egrep|fgrep|find|test|\[|diff|cmp|git diff|git grep)\b/;
22
- function isString(v) {
23
- return v !== undefined && v !== null && v.constructor === String;
24
- }
25
- function isObject(v) {
26
- return v !== undefined && v !== null && !Array.isArray(v) && v.constructor === Object;
27
- }
28
- function payloadString(payload, key) {
29
- if (!isObject(payload))
30
- return null;
31
- const value = payload[key];
32
- return isString(value) && value.trim() !== '' ? value : null;
33
- }
34
- /** Normalised form used to spot repeats. The failure log keeps only its hash, so changing it breaks repeat counts. */
35
- export function failureSignature(text) {
36
- return text.toLowerCase().replace(/[0-9a-f]{7,}/g, '#').replace(/\d+/g, '#').replace(/\s+/g, ' ').trim();
37
- }
38
- /** The memory text for a failure payload, or why it is not stored; a routine skip keeps its text for the log. Pure. */
39
- export function lessonFromFailure(payload) {
40
- if (!isObject(payload))
41
- return { skip: 'skipped-invalid', text: null, detail: null };
42
- // SAFETY: isObject narrowed payload to a plain JSON object; the fields read are all optional.
43
- const p = payload;
44
- if (p.is_interrupt === true)
45
- return { skip: 'skipped-interrupt', text: null, detail: null };
46
- if (!isString(p.error) || p.error.trim().length < 12)
47
- return { skip: 'skipped-invalid', text: null, detail: null };
48
- const tool = isString(p.tool_name) ? p.tool_name : 'tool';
49
- const error = redactSecretsStrict(p.error.replace(/\s+/g, ' ').trim());
50
- const text = `${tool}: ${error}`.slice(0, MAX_LEN);
51
- const command = isObject(p.tool_input) && isString(p.tool_input['command']) ? p.tool_input['command'].replace(LEADING_CD, '') : '';
52
- const head = command.trim().split(/\s+/).slice(0, 2).join(' ');
53
- const detail = `${tool}${head ? ` ${head}` : ''}: ${error}`;
54
- const routine = (rule) => ({ skip: 'skipped-routine', rule, text, detail });
55
- if (DECLINED.test(error))
56
- return routine('declined');
57
- if (OS_PERMISSION.test(error))
58
- return routine('os-permission');
59
- if (NO_MATCH.test(error))
60
- return routine('no-match');
61
- if (tool === 'Grep' || tool === 'Glob')
62
- return routine('search-tool');
63
- if (tool === 'Bash' && QUIET_EXIT_1.test(command) && /exit code 1\b/i.test(error))
64
- return routine('quiet-exit');
65
- return { text, detail };
66
- }
10
+ import { failureHash, failureSignature, lessonFromFailure, payloadString, } from './capture/failure-reading.js';
67
11
  /** Store a failure as an error memory unless it is routine or a repeat, and log it either way, even when storing throws. */
68
12
  export function captureToolFailure(hippoRoot, tenantId, payload) {
69
13
  const lesson = lessonFromFailure(payload);
@@ -77,7 +21,7 @@ export function captureToolFailure(hippoRoot, tenantId, payload) {
77
21
  }
78
22
  }
79
23
  function logFailure(hippoRoot, tenantId, payload, lesson, outcome) {
80
- const hash = (s) => (s === null ? null : blockHash(failureSignature(s)));
24
+ const hash = (s) => (s === null ? null : failureHash(s));
81
25
  const db = openHippoDb(hippoRoot);
82
26
  try {
83
27
  recordFailure(db, {
@@ -94,9 +38,9 @@ function logFailure(hippoRoot, tenantId, payload, lesson, outcome) {
94
38
  closeHippoDb(db);
95
39
  }
96
40
  }
97
- function storeLesson(hippoRoot, tenantId, text) {
41
+ export function storeLesson(hippoRoot, tenantId, text, caller) {
98
42
  const sig = failureSignature(text);
99
- const repeat = loadContentsWithTag(hippoRoot, tenantId, 'auto-captured').some((content) => failureSignature(content) === sig);
43
+ const repeat = loadContentsWithTag(hippoRoot, tenantId, 'auto-captured', caller?.origins).some((content) => failureSignature(content) === sig);
100
44
  if (repeat)
101
45
  return 'duplicate';
102
46
  const entry = createMemory(text, {
@@ -106,7 +50,11 @@ function storeLesson(hippoRoot, tenantId, text) {
106
50
  tenantId,
107
51
  baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
108
52
  });
109
- writeEntry(hippoRoot, entry);
53
+ // A caller's project is its checked one; the store's own folder would name the server.
54
+ if (caller === undefined)
55
+ writeEntry(hippoRoot, entry);
56
+ else
57
+ writeEntry(hippoRoot, { ...entry, origin_project: caller.originProject }, { actor: caller.actor });
110
58
  return 'stored';
111
59
  }
112
60
  //# sourceMappingURL=capture-error.js.map
package/dist/cli/audit.js CHANGED
@@ -8,6 +8,8 @@ import { resolveTenantId } from '../tenant.js';
8
8
  import { pruneAuditLog, parseOlderThanFlag } from '../audit-prune.js';
9
9
  import { printError } from './output.js';
10
10
  import { requireInit, resolveAuthRoot } from './shared.js';
11
+ import { repairAutomaticMemories } from '../quality-repair.js';
12
+ import { getGlobalRoot } from '../shared.js';
11
13
  // ---------------------------------------------------------------------------
12
14
  // Audit log subcommands (`hippo audit list`)
13
15
  // ---------------------------------------------------------------------------
@@ -114,6 +116,29 @@ function cmdAuditLog(hippoRoot, args, flags) {
114
116
  process.exit(1);
115
117
  }
116
118
  export function handleAudit({ hippoRoot, args, flags }) {
119
+ if (args[0] === 'repair') {
120
+ const apply = flags['apply'] === true && flags['dry-run'] !== true;
121
+ const result = repairAutomaticMemories(flags['global'] ? getGlobalRoot() : hippoRoot, { tenantId: resolveTenantId({}), apply });
122
+ if (flags['json']) {
123
+ console.log(JSON.stringify(result));
124
+ return;
125
+ }
126
+ console.log(`Quality repair ${apply ? 'apply' : 'preview'}: ${result.issues.length} issue(s) across ${result.total} memories.`);
127
+ for (const issue of result.issues)
128
+ console.log(` [${issue.disposition}] ${issue.id}: ${issue.reason}${issue.protection ? ` (${issue.protection})` : ''}`);
129
+ for (const blocker of result.blockers)
130
+ console.log(` Blocked: ${blocker}. Repair never upgrades a store; any other hippo command does, then run repair again.`);
131
+ const restore = `hippo dormant restore <id>${flags['global'] ? ' --global' : ''}`;
132
+ if (result.backup)
133
+ console.log(`Backup: ${result.backup}\nMoved ${result.appliedIds.length} memories to dormant storage. Recovery: ${restore}.`);
134
+ else if (apply && result.supported)
135
+ console.log('Nothing moved: no unprotected memory has a certain defect.');
136
+ for (const warning of result.warnings)
137
+ console.log(`Warning: ${warning}`);
138
+ if (!apply)
139
+ console.log('Preview only. Add --apply to move set-aside memories to dormant storage. Pin a review memory to keep it.');
140
+ return;
141
+ }
117
142
  // `audit list` and `audit prune` -> audit-log subcommands.
118
143
  // Other forms (no sub, --fix) keep the existing memory-quality auditor
119
144
  // for backwards compatibility.
package/dist/cli/auth.js CHANGED
@@ -54,8 +54,9 @@ function cmdAuthCreate(hippoRoot, flags) {
54
54
  function formatKeyRow(item) {
55
55
  const label = item.label ?? '-';
56
56
  const created = item.createdAt;
57
+ const expires = item.expiresAt ?? '-';
57
58
  const revoked = item.revokedAt ?? '-';
58
- return `${item.keyId} ${item.tenantId} ${item.role} ${label} ${created} ${revoked}`;
59
+ return `${item.keyId} ${item.tenantId} ${item.role} ${label} ${created} ${expires} ${revoked}`;
59
60
  }
60
61
  function cmdAuthList(hippoRoot, flags) {
61
62
  const root = resolveAuthRoot(hippoRoot, flags);
@@ -74,10 +75,10 @@ function cmdAuthList(hippoRoot, flags) {
74
75
  return;
75
76
  }
76
77
  if (items.length === 0) {
77
- console.log(includeRevoked ? 'No API keys.' : 'No active API keys. (Use --all to include revoked.)');
78
+ console.log(includeRevoked ? 'No API keys.' : 'No active API keys. (Use --all to include revoked and expired.)');
78
79
  return;
79
80
  }
80
- console.log('key_id tenant role label created revoked');
81
+ console.log('key_id tenant role label created expires revoked');
81
82
  for (const item of items) {
82
83
  console.log(formatKeyRow(item));
83
84
  }
@@ -1,23 +1,20 @@
1
1
  // The `hippo context` verb, which the per-prompt hook also runs; main() loads it lazily from the command table.
2
- import { evalNow } from '../ablation.js';
3
2
  import * as path from 'path';
4
- import { isInitialized } from '../store/open.js';
5
- import { writeDeliveryEventAtRoot, writeDeliveryEventOnHandle } from '../recall-trace.js';
6
3
  import { createDeliveryRecorder } from '../delivery-recorder.js';
7
4
  import { loadConfig } from '../config.js';
8
- import { isSqliteBusy, noteStoreBusy } from '../db.js';
9
- import { blockHash, estimateTokens, isSubagentPayload, lastSentState, recordTokenUse, shouldSkipUnchanged, } from '../token-ledger.js';
5
+ import { blockHash, estimateTokens, isSubagentPayload, recordTokenUse } from '../token-ledger.js';
10
6
  import { isGlobalStoreRoot } from '../project-identity.js';
11
7
  import { autoDetectContext } from '../context-auto.js';
12
8
  import { detectScope } from '../scope.js';
13
- import { getGlobalRoot } from '../shared.js';
9
+ import { ledgerRoot, withLedgerDb } from '../ledger-db.js';
14
10
  import { readStdinBounded } from '../stdin.js';
15
11
  import * as api from '../api.js';
16
12
  import { resolveTenantId } from '../tenant.js';
17
13
  import { renderAmbientSummary } from '../ambient.js';
18
- import { contextCost, contextHeading, contextLine, crossProjectHeading, crossProjectLine, settleTokens, } from '../context-render.js';
14
+ import { contextBlockLines, contextCost, crossProjectLines, settleTokens } from '../context-render.js';
15
+ import { additionalContextOutput, flushDeliveryRecorder, hasContextData, toRenderItems, } from '../prompt-hook.js';
19
16
  import { printError } from './output.js';
20
- import { parseLimitFlag, parseCountFlag, parseBudgetFlag, requireInit, printActiveTaskSnapshot, printSessionEvents, printHandoff, hostSessionId, captureConsole, hookStoreRoot, withLedgerDb, runHookWithStores, inPilotHoldout, } from './shared.js';
17
+ import { parseLimitFlag, parseCountFlag, parseBudgetFlag, requireInit, printActiveTaskSnapshot, printSessionEvents, printHandoff, hostSessionId, captureConsole, hookStoreRoot, runHookWithStores, inPilotHoldout, } from './shared.js';
21
18
  export async function cmdContext(hippoRoot, args, flags, stdinText) {
22
19
  const rec = startDeliveryRecorder(hippoRoot, flags, stdinText);
23
20
  // No try/finally: a render throw keeps its own exit code and writes no event.
@@ -30,7 +27,7 @@ function startDeliveryRecorder(hippoRoot, flags, stdinText) {
30
27
  return null;
31
28
  try {
32
29
  // The same store withLedgerDb writes the token ledger to, so its config governs both.
33
- const root = isInitialized(hippoRoot) ? hippoRoot : isInitialized(getGlobalRoot()) ? getGlobalRoot() : null;
30
+ const root = ledgerRoot(hippoRoot);
34
31
  if (root === null || !loadConfig(root).deliveryLedger.enabled)
35
32
  return null;
36
33
  return createDeliveryRecorder({
@@ -48,18 +45,6 @@ function startDeliveryRecorder(hippoRoot, flags, stdinText) {
48
45
  return null;
49
46
  }
50
47
  }
51
- /** With `db`, writes on the token ledger's handle (same store); without it, opens its own. A second flush is a no-op. */
52
- function flushDeliveryRecorder(rec, db) {
53
- if (rec === null)
54
- return;
55
- try {
56
- rec.flush((input) => (db ? writeDeliveryEventOnHandle(db, input) : writeDeliveryEventAtRoot(rec.root, input)));
57
- }
58
- catch (error) {
59
- // Pinned stderr text, as in the recorder build above.
60
- printError(`[hippo] delivery ledger write failed:${error instanceof Error ? error.message : String(error)}`);
61
- }
62
- }
63
48
  /** The hook payload's session id and raw prompt; a malformed or empty payload yields neither. */
64
49
  function readHookPayload(stdinText) {
65
50
  let sessionId;
@@ -122,7 +107,9 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
122
107
  renderContextJson(view, query);
123
108
  }
124
109
  else if (format === 'additional-context') {
125
- renderAdditionalContext(view);
110
+ const stdout = additionalContextOutput(view);
111
+ if (stdout)
112
+ process.stdout.write(stdout);
126
113
  }
127
114
  else {
128
115
  renderContextMarkdown(view);
@@ -162,15 +149,6 @@ function buildContextOpts(flags, input) {
162
149
  deliveryObserver: input.rec ?? undefined,
163
150
  };
164
151
  }
165
- function hasContextData(result) {
166
- return Boolean(result.entries.length > 0 ||
167
- result.activeSnapshot ||
168
- result.sessionHandoff ||
169
- (result.recentEvents && result.recentEvents.length > 0));
170
- }
171
- function toRenderItems(entries) {
172
- return entries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
173
- }
174
152
  function renderContextJson(view, query) {
175
153
  const { result, rec } = view;
176
154
  const output = result.entries.map((r) => ({
@@ -202,122 +180,6 @@ function renderContextJson(view, query) {
202
180
  flushDeliveryRecorder(rec, db);
203
181
  });
204
182
  }
205
- /** The hook format: a skippable static block (snapshot, handoff, events, pins, recent-N) and a never-skipped recall block. */
206
- function renderAdditionalContext(view) {
207
- const { result, rec, framing } = view;
208
- const staticEntries = result.entries.filter((r) => r.category !== 'cross-project' && !r.promptRecall);
209
- const staticCrossEntries = result.entries.filter((r) => r.category === 'cross-project' && !r.promptRecall);
210
- const staticItems = toRenderItems(staticEntries);
211
- const recallItems = toRenderItems(result.entries.filter((r) => r.promptRecall));
212
- const staticBlock = settleTokens((t) => captureConsole(() => {
213
- if (result.activeSnapshot)
214
- printActiveTaskSnapshot(result.activeSnapshot);
215
- if (result.sessionHandoff)
216
- printHandoff(result.sessionHandoff);
217
- if (result.recentEvents && result.recentEvents.length > 0) {
218
- printSessionEvents(result.recentEvents);
219
- }
220
- // No live strength percentage, so an unchanged set of memories renders byte-identically turn after turn.
221
- if (staticItems.length > 0)
222
- printContextMarkdown(staticItems, t, framing, { showStrength: false });
223
- printCrossProjectSection(staticCrossEntries);
224
- }));
225
- const recallBlock = recallItems.length > 0
226
- ? settleTokens((t) => captureConsole(() => printContextMarkdown(recallItems, t, framing, { showStrength: false, heading: 'Prompt-Relevant Memory' })))
227
- : '';
228
- if (!staticBlock.trim() && !recallBlock.trim()) {
229
- rec?.delivered({ state: 'empty' });
230
- return;
231
- }
232
- const surface = view.pinnedOnly ? 'hook' : 'context';
233
- const sendStatic = staticBlock.trim().length > 0 && !skipUnchangedStatic(view, surface, staticBlock, recallBlock, staticItems.length);
234
- const finalStatic = sendStatic ? staticBlock : '';
235
- const additionalContext = finalStatic && recallBlock
236
- ? `${finalStatic}\n\n${recallBlock}`
237
- : finalStatic || recallBlock;
238
- const staticReused = !sendStatic && staticBlock.trim().length > 0;
239
- if (!additionalContext.trim()) {
240
- rec?.delivered({ state: 'reused', staticHash: blockHash(staticBlock), staticReused });
241
- return;
242
- }
243
- const payload = {
244
- hookSpecificOutput: {
245
- hookEventName: 'UserPromptSubmit',
246
- additionalContext,
247
- },
248
- };
249
- process.stdout.write(JSON.stringify(payload));
250
- rec?.delivered({
251
- state: staticReused ? 'reused-recall-sent' : 'sent',
252
- staticHash: staticBlock.trim() ? blockHash(staticBlock) : null,
253
- recallHash: recallBlock ? blockHash(recallBlock) : null,
254
- emittedText: additionalContext,
255
- staticReused,
256
- });
257
- if (finalStatic || recallBlock) {
258
- recordAdditionalContextRows(view, surface, { text: finalStatic, items: staticItems.length }, { text: recallBlock, items: recallItems.length });
259
- }
260
- }
261
- /** True, after booking the skip row, when the hook may omit a static block this session already holds. */
262
- function skipUnchangedStatic(view, surface, staticBlock, recallBlock, staticCount) {
263
- const { hippoRoot, payloadSessionId, rec } = view;
264
- if (!view.pinnedOnly || payloadSessionId === undefined)
265
- return false;
266
- const injectCfg = loadConfig(hippoRoot).pinnedInject;
267
- if (injectCfg.skipUnchanged === false)
268
- return false;
269
- const refreshTurns = Number.isFinite(injectCfg.refreshTurns) && injectCfg.refreshTurns >= 0
270
- ? injectCfg.refreshTurns
271
- : 10;
272
- // Hashed on the static text alone so an unchanged pin set still skips while recall varies.
273
- const staticHash = blockHash(staticBlock);
274
- const last = withLedgerDb(hippoRoot, (db) => lastSentState(db, view.tenantId, payloadSessionId, surface));
275
- if (!shouldSkipUnchanged(last ?? null, staticHash, refreshTurns))
276
- return false;
277
- withLedgerDb(hippoRoot, (db) => {
278
- recordTokenUse(db, {
279
- tenantId: view.tenantId, sessionId: payloadSessionId, surface, event: 'skip',
280
- items: staticCount, tokens: estimateTokens(staticBlock), hash: staticHash,
281
- });
282
- if (recallBlock.trim())
283
- return;
284
- rec?.delivered({ state: 'reused', staticHash, staticReused: true });
285
- flushDeliveryRecorder(rec, db);
286
- });
287
- return true;
288
- }
289
- /** One connection for both rows; each insert in its own try so one failing doesn't skip the other. */
290
- function recordAdditionalContextRows(view, surface, staticPart, recallPart) {
291
- withLedgerDb(view.hippoRoot, (db) => {
292
- if (staticPart.text) {
293
- try {
294
- recordTokenUse(db, {
295
- tenantId: view.tenantId, sessionId: view.ledgerSessionId, surface, event: 'inject',
296
- items: staticPart.items, tokens: estimateTokens(staticPart.text), hash: blockHash(staticPart.text),
297
- });
298
- // Best-effort row: only a busy store is actionable, and a ledger failure must not break the hook.
299
- }
300
- catch (error) {
301
- if (isSqliteBusy(error))
302
- noteStoreBusy('token ledger row skipped');
303
- }
304
- }
305
- if (recallPart.text) {
306
- try {
307
- recordTokenUse(db, {
308
- tenantId: view.tenantId, sessionId: view.ledgerSessionId, surface: 'hook_recall', event: 'inject',
309
- items: recallPart.items, tokens: estimateTokens(recallPart.text), hash: blockHash(recallPart.text),
310
- });
311
- // Same best-effort rule as the inject row above.
312
- }
313
- catch (error) {
314
- if (isSqliteBusy(error))
315
- noteStoreBusy('token ledger row skipped');
316
- }
317
- }
318
- flushDeliveryRecorder(view.rec, db);
319
- });
320
- }
321
183
  /** The default format; the header figure counts the whole block, sections included, as the ledger does. */
322
184
  function renderContextMarkdown(view) {
323
185
  const { result, rec, framing } = view;
@@ -352,21 +214,14 @@ function renderContextMarkdown(view) {
352
214
  flushDeliveryRecorder(rec, db);
353
215
  });
354
216
  }
355
- /** An explicit header lets agents and humans tell borrowed context from project memory. */
356
217
  function printCrossProjectSection(items) {
357
- if (items.length === 0)
358
- return;
359
- console.log(crossProjectHeading(items.length));
360
- for (const item of items)
361
- console.log(crossProjectLine(item));
218
+ for (const line of crossProjectLines(items))
219
+ console.log(line);
362
220
  }
363
221
  /** @internal Exported for the render snapshot test; not a stable public API. */
364
222
  export function printContextMarkdown(items, totalTokens, framing = 'observe', opts = {}) {
365
- const now = evalNow();
366
- const showStrength = opts.showStrength !== false;
367
- console.log(contextHeading(opts.heading ?? 'Project Memory', items.length, totalTokens));
368
- for (const item of items)
369
- console.log(contextLine(item, framing, showStrength, now));
223
+ for (const line of contextBlockLines(items, totalTokens, framing, opts))
224
+ console.log(line);
370
225
  }
371
226
  export async function handleContext({ hippoRoot, args, flags }) {
372
227
  // Bounded, not a TTY guard: the hot stdin path and a manual run share this one command.
@@ -4,8 +4,9 @@ import { saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOut
4
4
  import { isHandoffOutcome, formatHandoffEvidenceLine } from '../handoff.js';
5
5
  import { resolveTenantId } from '../tenant.js';
6
6
  import { wmPush, wmRead, wmClear, wmFlush } from '../working-memory.js';
7
+ import { collectHandoffEvidence } from '../handoff-evidence.js';
7
8
  import { printError } from './output.js';
8
- import { requireInit, collectHandoffEvidence, printActiveTaskSnapshot, printSessionEvents, printHandoff, } from './shared.js';
9
+ import { requireInit, printActiveTaskSnapshot, printSessionEvents, printHandoff, } from './shared.js';
9
10
  export function cmdSnapshot(hippoRoot, args, flags) {
10
11
  requireInit(hippoRoot);
11
12
  const subcommand = args[0] ?? 'show';
package/dist/cli/init.js CHANGED
@@ -14,7 +14,7 @@ import { currentMachine, importForStore, importProjectMemories, importUserMemori
14
14
  import { emptyReport, mergeReports } from '../agent-memories/report.js';
15
15
  import { getGlobalRoot, initGlobal } from '../shared.js';
16
16
  import { registerWorkspace } from '../scheduler.js';
17
- import { printAgentImport, installCodexMemoryHooks, setupDailySchedule, learnFromRepo } from './shared.js';
17
+ import { printAgentImport, installCodexMemoryHooks, setupDailySchedule, learnFromRepo, skipLearnOnSharedStore, warnClaudeSettingsUnusable } from './shared.js';
18
18
  import { HOOK_MARKERS, HOOKS, hippoBlock } from './hook-blocks.js';
19
19
  function scanForGitRepos(rootDir, maxDepth = 2) {
20
20
  const repos = [];
@@ -150,8 +150,9 @@ export function cmdInit(hippoRoot, flags) {
150
150
  if (!flags['no-schedule'] && !flags['global']) {
151
151
  setupDailySchedule(globalRoot);
152
152
  }
153
+ const learn = !flags['no-learn'] && !skipLearnOnSharedStore(hippoRoot);
153
154
  // Seed with git history on first init (unless --no-learn)
154
- if (!alreadyExists && !flags['no-learn'] && !flags['global']) {
155
+ if (!alreadyExists && learn && !flags['global']) {
155
156
  if (isGitRepo(process.cwd())) {
156
157
  const seedDays = 30;
157
158
  console.log(`\n Seeding memories from last ${seedDays} days of git history...`);
@@ -165,7 +166,7 @@ export function cmdInit(hippoRoot, flags) {
165
166
  }
166
167
  }
167
168
  // Every run, not only the first: an agent's notes change between inits.
168
- if (!flags['no-learn'])
169
+ if (learn)
169
170
  printAgentImport(importForStore(hippoRoot, { machine: currentMachine() }));
170
171
  }
171
172
  /** Every write init makes into agent config (instruction blocks, hooks, plugins) is an automatic integration, so one switch skips them all. */
@@ -254,6 +255,7 @@ function installUserLevelHooks(agents, codexHint) {
254
255
  // claude-code` and `hippo setup`.
255
256
  if (hook === 'claude-code') {
256
257
  const result = installJsonHooks(hook);
258
+ warnClaudeSettingsUnusable(result, ' ');
257
259
  if (result.installedSessionEnd) {
258
260
  console.log(` Auto-installed hippo session-end SessionEnd hook in ${hook} settings`);
259
261
  }
@@ -0,0 +1,6 @@
1
+ /** The line the user sees when the last sleep failed, a spool step hit an unexpected error, or the spool holds `.bad` files; null when none did. */
2
+ export declare function sleepProblems(logText: string, bad: number): string | null;
3
+ /** Prints the SessionEnd sleep log on stderr, then clears it. In a hook, stdout carries only `systemMessage`,
4
+ * because Claude Code shows that to the user and adds any other SessionStart stdout to the model's context. */
5
+ export declare function cmdLastSleep(hippoRoot: string, flags: Record<string, string | boolean | string[]>, out?: 'hook' | 'terminal'): void;
6
+ //# sourceMappingURL=last-sleep.d.ts.map
@@ -0,0 +1,68 @@
1
+ // `hippo last-sleep`: the previous session's sleep log for the debug log, and one problems line for the user.
2
+ import * as fs from 'fs';
3
+ import { isStringValue } from '../capture-contract.js';
4
+ import { REPLAY_AFTER_MS } from '../compaction-record.js';
5
+ import { SPOOL_PROBLEM, spoolCounts } from '../compaction-spool.js';
6
+ import { defaultSleepLogPath } from '../hooks/shared.js';
7
+ import { errorMessage } from '../log.js';
8
+ import { truncateCodePointSafe } from '../transcript-tail.js';
9
+ import { printError } from './output.js';
10
+ import { hookStoreRoot } from './shared.js';
11
+ const SLEEP_FAILED = '[hippo] sleep failed: ';
12
+ const FAILURE_CHARS = 120;
13
+ /** The line the user sees when the last sleep failed, a spool step hit an unexpected error, or the spool holds `.bad` files; null when none did. */
14
+ export function sleepProblems(logText, bad) {
15
+ const lines = logText.split(/\r?\n/);
16
+ const failed = lines.find((line) => line.startsWith(SLEEP_FAILED));
17
+ const errors = lines.filter((line) => line.includes(SPOOL_PROBLEM)).length;
18
+ const parts = [
19
+ failed === undefined ? '' : `the last sleep failed (${truncateCodePointSafe(failed.slice(SLEEP_FAILED.length).trim(), FAILURE_CHARS)})`,
20
+ errors === 0 ? '' : `${errors} error${errors === 1 ? '' : 's'} while saving waiting compaction summaries`,
21
+ bad === 0 ? '' : `${bad} compaction ${bad === 1 ? 'summary' : 'summaries'} set aside as .bad`,
22
+ ].filter((part) => part !== '');
23
+ return parts.length === 0 ? null : `Hippo: ${parts.join('; ')}. Run hippo doctor for details.`;
24
+ }
25
+ function badSpoolFiles(hippoRoot) {
26
+ try {
27
+ return spoolCounts(hookStoreRoot(hippoRoot), new Date(), REPLAY_AFTER_MS).bad;
28
+ }
29
+ catch (err) {
30
+ printError(`hippo: spool not counted: ${errorMessage(err)}`);
31
+ return 0;
32
+ }
33
+ }
34
+ /** Prints the SessionEnd sleep log on stderr, then clears it. In a hook, stdout carries only `systemMessage`,
35
+ * because Claude Code shows that to the user and adds any other SessionStart stdout to the model's context. */
36
+ export function cmdLastSleep(hippoRoot, flags, out = process.stdout.isTTY ? 'terminal' : 'hook') {
37
+ const pathFlag = flags['path'];
38
+ const logPath = isStringValue(pathFlag) ? pathFlag : defaultSleepLogPath();
39
+ if (!fs.existsSync(logPath))
40
+ return;
41
+ let content;
42
+ try {
43
+ content = fs.readFileSync(logPath, 'utf8');
44
+ }
45
+ catch {
46
+ // Removed or locked since the exists check: there is nothing to show this session.
47
+ return;
48
+ }
49
+ if (content.trim().length > 0) {
50
+ printError('=== Previous session hippo consolidation ===');
51
+ process.stderr.write(content);
52
+ if (!content.endsWith('\n'))
53
+ printError();
54
+ printError('===========================================');
55
+ }
56
+ const problems = sleepProblems(content, badSpoolFiles(hippoRoot));
57
+ if (problems !== null && out === 'hook')
58
+ process.stdout.write(`${JSON.stringify({ systemMessage: problems })}\n`);
59
+ if (problems !== null && out === 'terminal')
60
+ printError(problems);
61
+ if (!flags['keep']) {
62
+ try {
63
+ fs.unlinkSync(logPath);
64
+ }
65
+ catch { /* non-fatal */ }
66
+ }
67
+ }
68
+ //# sourceMappingURL=last-sleep.js.map
@@ -1,4 +1,5 @@
1
+ import { type EmbeddingProvider } from '../embedding-provider.js';
1
2
  export declare function cmdRefine(hippoRoot: string, flags: Record<string, string | boolean | string[]>): Promise<void>;
2
3
  export declare function cmdDedup(hippoRoot: string, flags: Record<string, string | boolean | string[]>): void;
3
- export declare function cmdEmbed(hippoRoot: string, flags: Record<string, string | boolean | string[]>): Promise<void>;
4
+ export declare function cmdEmbed(hippoRoot: string, flags: Record<string, string | boolean | string[]>, given?: EmbeddingProvider): Promise<void>;
4
5
  //# sourceMappingURL=maintenance.d.ts.map
@@ -3,7 +3,6 @@ import { envAnthropicApiKey } from '../env.js';
3
3
  import { loadAllEntries } from '../store/entry-reads.js';
4
4
  import { deduplicateStore } from '../dedupe.js';
5
5
  import { embedAll, loadEmbeddingIndex } from '../embeddings.js';
6
- import { resolveEmbeddingModel } from '../local-embedding.js';
7
6
  import { resolveEmbeddingProvider } from '../embedding-provider.js';
8
7
  import { resetAllPhysicsState } from '../physics-state.js';
9
8
  import { loadConfig } from '../config.js';
@@ -93,7 +92,7 @@ export function cmdDedup(hippoRoot, flags) {
93
92
  // ---------------------------------------------------------------------------
94
93
  // Embed command
95
94
  // ---------------------------------------------------------------------------
96
- export async function cmdEmbed(hippoRoot, flags) {
95
+ export async function cmdEmbed(hippoRoot, flags, given) {
97
96
  // --global mirrors resolveAuthRoot (cli.ts:6900): initGlobal() + the global
98
97
  // root, skipping the local requireInit entirely, so this is the healing
99
98
  // path for pre-1.27.0 global stores from a directory with no local .hippo.
@@ -109,12 +108,13 @@ export async function cmdEmbed(hippoRoot, flags) {
109
108
  printEmbedStatus(root);
110
109
  return;
111
110
  }
112
- if (!embedProviderReady(root))
111
+ const provider = readyEmbedProvider(root, given);
112
+ if (!provider)
113
113
  return;
114
114
  console.log('Embedding all memories (this may take a moment on first run to download model)...');
115
115
  let count;
116
116
  try {
117
- count = await embedAll(root, resolveEmbeddingModel(root));
117
+ count = await embedAll(root, undefined, provider);
118
118
  }
119
119
  catch (err) {
120
120
  printError(`Embedding failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -126,6 +126,11 @@ export async function cmdEmbed(hippoRoot, flags) {
126
126
  const entriesAfter = loadAllEntries(root);
127
127
  const embIndexAfter = loadEmbeddingIndex(root);
128
128
  console.log(`Done. ${count} new embeddings created. ${Object.keys(embIndexAfter).length}/${entriesAfter.length} total.`);
129
+ const unembedded = entriesAfter.filter((e) => !embIndexAfter[e.id]).length;
130
+ if (unembedded > 0) {
131
+ printError(`${unembedded} memories are still not embedded (the warnings above name them). Re-run \`hippo embed\` to retry.`);
132
+ process.exitCode = 1;
133
+ }
129
134
  }
130
135
  function resetPhysics(root) {
131
136
  const entries = loadAllEntries(root);
@@ -154,10 +159,10 @@ function printEmbedStatus(root) {
154
159
  console.log(` ${missing.length} memories need embedding (run \`hippo embed\` to embed them)`);
155
160
  }
156
161
  }
157
- /** False, after saying why, when no usable provider exists. */
158
- function embedProviderReady(root) {
162
+ /** The provider to embed with, or null after saying why none is usable. */
163
+ function readyEmbedProvider(root, given) {
159
164
  // Embedding (unlike status/reset) needs an available provider.
160
- const embedProvider = (() => {
165
+ const embedProvider = given ?? (() => {
161
166
  try {
162
167
  return resolveEmbeddingProvider(root);
163
168
  }
@@ -168,23 +173,24 @@ function embedProviderReady(root) {
168
173
  })();
169
174
  if (!embedProvider) {
170
175
  process.exitCode = 1;
171
- return false;
176
+ return null;
172
177
  }
173
178
  if (embedProvider.isAvailable())
174
- return true;
179
+ return embedProvider;
175
180
  if (loadConfig(root).embeddings.enabled === false) {
176
181
  console.log('Embeddings are disabled in config (embeddings.enabled = false). Set it to true or "auto" to enable.');
177
- return false;
182
+ return null;
178
183
  }
179
184
  if (embedProvider.kind === 'local') {
180
185
  console.log('Embeddings not available. Install @huggingface/transformers to enable:');
181
186
  console.log(' npm install @huggingface/transformers');
187
+ process.exitCode = 1;
182
188
  }
183
189
  else {
184
190
  printError(`Embedding provider '${embedProvider.kind}' is configured but ${embedProvider.keyEnv} is not set.`);
185
191
  printError(`Export ${embedProvider.keyEnv}, or set config.embeddings.provider back to 'local'.`);
186
192
  process.exitCode = 1;
187
193
  }
188
- return false;
194
+ return null;
189
195
  }
190
196
  //# sourceMappingURL=maintenance.js.map
@@ -0,0 +1,3 @@
1
+ /** Fault-isolated like the project tag repair: a failure warns, leaves the store unmarked and runs again next time. */
2
+ export declare function repairQualityOnceAt(root: string): void;
3
+ //# sourceMappingURL=quality-repair-once.d.ts.map