hippo-memory 1.72.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/README.md +6 -4
  2. package/dist/agent-memories/apply.js +5 -5
  3. package/dist/api/assemble.js +2 -1
  4. package/dist/api/audit.d.ts +7 -7
  5. package/dist/api/audit.js +19 -17
  6. package/dist/api/auth.d.ts +5 -6
  7. package/dist/api/auth.js +28 -33
  8. package/dist/api/conflicts.js +1 -1
  9. package/dist/api/context-auto.js +4 -2
  10. package/dist/api/context-select.js +8 -20
  11. package/dist/api/context.js +12 -10
  12. package/dist/api/drill-down.js +2 -1
  13. package/dist/api/forget.d.ts +2 -2
  14. package/dist/api/forget.js +6 -7
  15. package/dist/api/graph.js +2 -1
  16. package/dist/api/learn.d.ts +1 -1
  17. package/dist/api/learn.js +45 -44
  18. package/dist/api/memories.d.ts +4 -2
  19. package/dist/api/memories.js +16 -5
  20. package/dist/api/objects.js +2 -1
  21. package/dist/api/outcome.d.ts +5 -5
  22. package/dist/api/outcome.js +16 -15
  23. package/dist/api/predictions.js +2 -1
  24. package/dist/api/promote.d.ts +3 -3
  25. package/dist/api/promote.js +25 -27
  26. package/dist/api/prompt-hook.d.ts +3 -1
  27. package/dist/api/prompt-hook.js +1 -0
  28. package/dist/api/quarantine.js +2 -1
  29. package/dist/api/recall-core.js +8 -3
  30. package/dist/api/recall-pipeline.js +17 -17
  31. package/dist/api/recall-types.d.ts +3 -1
  32. package/dist/api/recall.js +32 -29
  33. package/dist/api/remember-local.d.ts +5 -2
  34. package/dist/api/remember-local.js +14 -6
  35. package/dist/api/remember.d.ts +5 -2
  36. package/dist/api/remember.js +16 -14
  37. package/dist/api/session-end.d.ts +24 -0
  38. package/dist/api/session-end.js +31 -1
  39. package/dist/api/sleep-run.js +5 -4
  40. package/dist/api/sleep.js +4 -3
  41. package/dist/api/tokens.js +1 -1
  42. package/dist/api/types.d.ts +1 -7
  43. package/dist/capture/command.js +1 -1
  44. package/dist/capture/compaction-spool.d.ts +3 -5
  45. package/dist/capture/compaction-spool.js +41 -46
  46. package/dist/capture/session-end-caller.js +1 -1
  47. package/dist/capture/session-worker.js +19 -6
  48. package/dist/cli/audit.js +10 -10
  49. package/dist/cli/auth.js +4 -4
  50. package/dist/cli/client.d.ts +1 -2
  51. package/dist/cli/client.js +13 -13
  52. package/dist/cli/context.js +1 -1
  53. package/dist/cli/curate.d.ts +1 -1
  54. package/dist/cli/curate.js +9 -9
  55. package/dist/cli/decisions.js +4 -5
  56. package/dist/cli/hook-runtime.d.ts +3 -1
  57. package/dist/cli/hook-runtime.js +2 -2
  58. package/dist/cli/init.d.ts +1 -1
  59. package/dist/cli/init.js +9 -9
  60. package/dist/cli/object-verbs.d.ts +2 -2
  61. package/dist/cli/object-verbs.js +4 -4
  62. package/dist/cli/predictions.js +2 -3
  63. package/dist/cli/quality-repair-once.d.ts +1 -1
  64. package/dist/cli/quality-repair-once.js +2 -2
  65. package/dist/cli/refine-llm.js +2 -1
  66. package/dist/cli/remember.js +5 -5
  67. package/dist/cli/serve.js +9 -4
  68. package/dist/cli/session-hooks.d.ts +0 -4
  69. package/dist/cli/session-hooks.js +57 -66
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -2
  72. package/dist/cli/shared.d.ts +1 -1
  73. package/dist/cli/shared.js +2 -2
  74. package/dist/cli/sleep.js +2 -2
  75. package/dist/cli/transfer.d.ts +3 -3
  76. package/dist/cli/transfer.js +34 -15
  77. package/dist/cli/usage.d.ts +1 -1
  78. package/dist/cli/usage.js +1 -1
  79. package/dist/cli/verb-row.d.ts +2 -0
  80. package/dist/cli/verbs/health.js +2 -0
  81. package/dist/cli/verbs/memory.js +43 -51
  82. package/dist/cli/verbs/objects.js +6 -3
  83. package/dist/cli/verbs/sessions.js +4 -2
  84. package/dist/cli.js +1 -2
  85. package/dist/connectors/dlq.js +2 -1
  86. package/dist/connectors/github/deletion.js +2 -1
  87. package/dist/connectors/github/ingest.js +2 -1
  88. package/dist/connectors/github/tenant-routing.js +2 -1
  89. package/dist/connectors/github/webhook.js +4 -2
  90. package/dist/connectors/ingest.js +2 -1
  91. package/dist/connectors/slack/deletion.js +2 -1
  92. package/dist/connectors/slack/ingest.js +2 -1
  93. package/dist/connectors/slack/tenant-routing.js +2 -1
  94. package/dist/consolidate/conflicts.js +1 -1
  95. package/dist/consolidate/dag.js +14 -9
  96. package/dist/consolidate/dedupe.js +1 -1
  97. package/dist/consolidate/memory-value.js +1 -1
  98. package/dist/consolidate/merge.js +6 -6
  99. package/dist/consolidate/sleep.js +1 -1
  100. package/dist/consolidate/traces.js +2 -2
  101. package/dist/core/config.js +15 -16
  102. package/dist/core/markdown.js +2 -2
  103. package/dist/core/memory.d.ts +4 -4
  104. package/dist/core/memory.js +10 -5
  105. package/dist/core/physics.d.ts +3 -1
  106. package/dist/core/physics.js +17 -2
  107. package/dist/core/project-identity.js +2 -1
  108. package/dist/core/search-types.d.ts +1 -1
  109. package/dist/dashboard/dashboard-actions.d.ts +2 -2
  110. package/dist/dashboard/dashboard-actions.js +5 -5
  111. package/dist/dashboard/dashboard-queries.js +2 -7
  112. package/dist/dashboard/dashboard-snapshot.js +1 -3
  113. package/dist/dashboard/dashboard.js +16 -5
  114. package/dist/db/connect.d.ts +2 -1
  115. package/dist/db/connect.js +2 -1
  116. package/dist/db/migrate.js +14 -7
  117. package/dist/db/migrations/v11.js +1 -0
  118. package/dist/db/migrations/v14.js +1 -0
  119. package/dist/db/physics-state.d.ts +0 -4
  120. package/dist/db/physics-state.js +5 -31
  121. package/dist/db/request-stores.d.ts +1 -0
  122. package/dist/db/request-stores.js +2 -1
  123. package/dist/db/vector-store.js +2 -1
  124. package/dist/doctor.d.ts +3 -0
  125. package/dist/doctor.js +7 -7
  126. package/dist/embeddings/local.js +2 -2
  127. package/dist/embeddings/provider.js +1 -10
  128. package/dist/entry/store-kit.d.ts +37 -0
  129. package/dist/entry/store-kit.js +25 -0
  130. package/dist/entry/testing.d.ts +6 -0
  131. package/dist/entry/testing.js +7 -0
  132. package/dist/eval/judgment.js +1 -1
  133. package/dist/hooks/json-hooks.js +6 -2
  134. package/dist/importers/core.js +3 -3
  135. package/dist/importers/vault.d.ts +1 -1
  136. package/dist/importers/vault.js +18 -18
  137. package/dist/index.d.ts +2 -2
  138. package/dist/index.js +1 -1
  139. package/dist/learn/autolearn.d.ts +6 -2
  140. package/dist/learn/autolearn.js +8 -6
  141. package/dist/learn/extract.js +1 -1
  142. package/dist/learn/invalidation.d.ts +2 -1
  143. package/dist/learn/invalidation.js +10 -5
  144. package/dist/mcp/memory-tools.d.ts +1 -1
  145. package/dist/mcp/memory-tools.js +6 -9
  146. package/dist/mcp/request.js +20 -19
  147. package/dist/mcp/stdio.js +13 -8
  148. package/dist/rerankers/cross-encoder.js +10 -3
  149. package/dist/search/embed-memories.d.ts +5 -2
  150. package/dist/search/embed-memories.js +40 -28
  151. package/dist/search/engine.d.ts +8 -0
  152. package/dist/search/engine.js +10 -0
  153. package/dist/search/vector.js +5 -5
  154. package/dist/server/auth.d.ts +1 -1
  155. package/dist/server/auth.js +7 -8
  156. package/dist/server/boot.d.ts +2 -1
  157. package/dist/server/boot.js +28 -19
  158. package/dist/server/client-ip.d.ts +9 -4
  159. package/dist/server/client-ip.js +35 -17
  160. package/dist/server/route-table.js +5 -6
  161. package/dist/server/routes/memories.js +4 -2
  162. package/dist/server/sleep-offload.js +3 -3
  163. package/dist/server/tls.d.ts +7 -2
  164. package/dist/server/tls.js +17 -5
  165. package/dist/server/types.d.ts +7 -2
  166. package/dist/server.d.ts +4 -41
  167. package/dist/server.js +3 -30
  168. package/dist/sharing/global-store.d.ts +4 -5
  169. package/dist/sharing/global-store.js +6 -11
  170. package/dist/sharing/global-sync.js +1 -1
  171. package/dist/sharing/search-both.js +9 -2
  172. package/dist/sharing/share.js +2 -4
  173. package/dist/store/ambient.js +2 -1
  174. package/dist/store/candidates.js +18 -8
  175. package/dist/store/cards.js +5 -4
  176. package/dist/store/compactions-record.js +8 -7
  177. package/dist/store/conflicts.d.ts +7 -1
  178. package/dist/store/conflicts.js +6 -5
  179. package/dist/store/connectors/dlq.js +2 -1
  180. package/dist/store/connectors/event-log.d.ts +13 -0
  181. package/dist/store/connectors/event-log.js +24 -0
  182. package/dist/store/connectors/github.js +6 -8
  183. package/dist/store/connectors/slack.js +6 -10
  184. package/dist/store/dashboard-reads.js +1 -0
  185. package/dist/store/delete-and-batch.js +5 -4
  186. package/dist/store/delivery-recorder.d.ts +2 -2
  187. package/dist/store/delivery-recorder.js +1 -6
  188. package/dist/store/descendant-page.js +2 -1
  189. package/dist/store/dormant.js +7 -3
  190. package/dist/store/entry-reads.d.ts +25 -3
  191. package/dist/store/entry-reads.js +43 -12
  192. package/dist/store/entry-row.js +9 -2
  193. package/dist/store/entry-writes.d.ts +10 -7
  194. package/dist/store/entry-writes.js +23 -11
  195. package/dist/store/goals.js +3 -2
  196. package/dist/store/graph-reads.js +5 -4
  197. package/dist/store/graph-view-rows.js +2 -1
  198. package/dist/store/half-life.js +2 -2
  199. package/dist/store/handoffs.d.ts +9 -3
  200. package/dist/store/handoffs.js +4 -3
  201. package/dist/store/index.d.ts +2 -2
  202. package/dist/store/index.js +1 -1
  203. package/dist/store/mirrors.js +9 -4
  204. package/dist/store/open.d.ts +2 -1
  205. package/dist/store/open.js +2 -1
  206. package/dist/store/port.d.ts +59 -3
  207. package/dist/store/quality-repair.js +1 -1
  208. package/dist/store/recall-trace.js +2 -1
  209. package/dist/store/rejected-values.js +14 -9
  210. package/dist/store/rows.js +2 -2
  211. package/dist/store/rule-sql.d.ts +2 -0
  212. package/dist/store/rule-sql.js +12 -6
  213. package/dist/store/search-rows.d.ts +5 -5
  214. package/dist/store/search-rows.js +9 -12
  215. package/dist/store/set-aside.js +1 -1
  216. package/dist/store/sqlite/connector-writes-group.js +3 -1
  217. package/dist/store/sqlite/entry-writes-group.d.ts +3 -3
  218. package/dist/store/sqlite/entry-writes-group.js +1 -1
  219. package/dist/store/sqlite/held-connection.d.ts +2 -0
  220. package/dist/store/sqlite/held-connection.js +1 -1
  221. package/dist/store/sqlite/last-recall-group.d.ts +13 -0
  222. package/dist/store/sqlite/last-recall-group.js +57 -0
  223. package/dist/store/sqlite/local.d.ts +0 -17
  224. package/dist/store/sqlite/local.js +0 -31
  225. package/dist/store/sqlite/maintenance-groups.d.ts +6 -0
  226. package/dist/store/sqlite/maintenance-groups.js +60 -0
  227. package/dist/store/sqlite/objects-group.js +14 -6
  228. package/dist/store/sqlite/store.d.ts +1 -8
  229. package/dist/store/sqlite/store.js +13 -24
  230. package/dist/store/summaries.d.ts +7 -0
  231. package/dist/store/summaries.js +49 -7
  232. package/dist/store/token-ledger-rows.js +3 -2
  233. package/dist/store/vector-index.js +5 -2
  234. package/dist/util/anthropic-messages.d.ts +3 -1
  235. package/dist/util/anthropic-messages.js +4 -2
  236. package/dist/util/atomic-write.d.ts +5 -2
  237. package/dist/util/atomic-write.js +12 -8
  238. package/dist/util/env.d.ts +10 -1
  239. package/dist/util/env.js +10 -1
  240. package/dist/util/format.d.ts +2 -0
  241. package/dist/util/format.js +4 -0
  242. package/dist/util/request-scope.d.ts +6 -2
  243. package/dist/util/request-scope.js +7 -3
  244. package/dist/util/spawn-argv.d.ts +9 -0
  245. package/dist/util/spawn-argv.js +57 -0
  246. package/dist/util/sql.d.ts +3 -0
  247. package/dist/util/sql.js +6 -0
  248. package/dist/util/vector-codec.d.ts +4 -0
  249. package/dist/util/vector-codec.js +15 -0
  250. package/dist/util/version.d.ts +1 -1
  251. package/dist/util/version.js +1 -1
  252. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  253. package/extensions/openclaw-plugin/package.json +1 -1
  254. package/openclaw.plugin.json +1 -1
  255. package/package.json +3 -1
  256. package/dist/api/on-store.d.ts +0 -16
  257. package/dist/api/on-store.js +0 -38
  258. package/dist/cli/lenient-id.d.ts +0 -2
  259. package/dist/cli/lenient-id.js +0 -13
@@ -29,11 +29,6 @@ const BAD = /^([\w-]+)\.(?:\w+\.)?bad$/;
29
29
  const BAD_TMP = /^[\w-]+\.\w+\.bad\.[0-9a-f]{8}\.tmp$/;
30
30
  const NEW_STEM = /^(\d{13})-[0-9a-f]{8}$/;
31
31
  const LEGACY_STEM_TIME = /-(\d+)$/;
32
- let fsx = fs;
33
- /** Test-only seam, so a test can make one fs call fail; null restores the real fs. */
34
- export function _setSpoolFsForTests(next) {
35
- fsx = next ?? fs;
36
- }
37
32
  const stamp = (ms) => String(Math.trunc(ms)).padStart(STAMP_DIGITS, '0');
38
33
  const isTime = (value) => typeof value === 'number' && Number.isFinite(value);
39
34
  const idle = new Int32Array(new SharedArrayBuffer(4));
@@ -99,7 +94,7 @@ function claimOf(name) {
99
94
  function spoolFile(hippoRoot) {
100
95
  return path.join(hippoRoot, SPOOL_DIR, `${stamp(Date.now())}-${randomBytes(4).toString('hex')}.a0.json`);
101
96
  }
102
- export function spool(hippoRoot, tenantId, payload, text, at) {
97
+ export function spool(hippoRoot, tenantId, payload, text, at, fsx = fs) {
103
98
  const file = spoolFile(hippoRoot);
104
99
  fsx.mkdirSync(path.dirname(file), { recursive: true });
105
100
  const body = {
@@ -160,7 +155,7 @@ function parseLock(text) {
160
155
  return { pid, at: raw.at, token };
161
156
  }
162
157
  /** The lock's body; its mtime stands in for the time when the body does not parse. */
163
- function readLock(lock, log) {
158
+ function readLock(fsx, lock, log) {
164
159
  let text = '';
165
160
  const read = settle(() => { text = fsx.readFileSync(lock, 'utf8'); }, log, 'spool lock not read');
166
161
  if (read !== 'done')
@@ -172,7 +167,7 @@ function readLock(lock, log) {
172
167
  const stat = settle(() => { mtime = fsx.statSync(lock).mtimeMs; }, log, 'spool lock not read');
173
168
  return stat === 'done' ? { pid: null, at: mtime, token: null } : stat;
174
169
  }
175
- function createLock(lock, body, log) {
170
+ function createLock(fsx, lock, body, log) {
176
171
  try {
177
172
  fsx.writeFileSync(lock, body, { encoding: 'utf8', flag: 'wx' });
178
173
  return 'taken';
@@ -188,18 +183,18 @@ function createLock(lock, body, log) {
188
183
  }
189
184
  }
190
185
  /** This run's token and take time once it holds `replay.lock`, else null: another replayer holds it, or it could not be made. */
191
- function takeLock(dir, log) {
186
+ function takeLock(fsx, dir, log) {
192
187
  const lock = path.join(dir, LOCK);
193
188
  const token = randomBytes(8).toString('hex');
194
189
  const at = Date.now();
195
190
  const body = JSON.stringify({ pid: process.pid, at, token });
196
191
  for (let tries = 0; tries < 2; tries++) {
197
- const made = createLock(lock, body, log);
192
+ const made = createLock(fsx, lock, body, log);
198
193
  if (made === 'taken')
199
194
  return { token, at };
200
195
  if (made === 'skip')
201
196
  break;
202
- const held = readLock(lock, log);
197
+ const held = readLock(fsx, lock, log);
203
198
  if (held === 'gone')
204
199
  continue;
205
200
  if (held === 'busy')
@@ -216,24 +211,24 @@ function takeLock(dir, log) {
216
211
  return null;
217
212
  }
218
213
  /** False, after saying so, once `replay.lock` no longer holds this run's token or cannot be read. */
219
- function ownsLock(dir, token, log) {
220
- const held = readLock(path.join(dir, LOCK), log);
214
+ function ownsLock(fsx, dir, token, log) {
215
+ const held = readLock(fsx, path.join(dir, LOCK), log);
221
216
  if (held !== 'gone' && held !== 'busy' && held.token === token)
222
217
  return true;
223
218
  log(held === 'busy' ? 'spool lock could not be read, stopping' : 'spool left to another replayer (lock taken over)');
224
219
  return false;
225
220
  }
226
221
  /** Removes `replay.lock` only while it holds this run's token, so a run never deletes a successor's lock. Never throws. */
227
- function releaseLock(dir, mine, log) {
222
+ function releaseLock(fsx, dir, mine, log) {
228
223
  const lock = path.join(dir, LOCK);
229
- const held = readLock(lock, log);
224
+ const held = readLock(fsx, lock, log);
230
225
  // A takeover needs a lock over STALE_MS old, so a busy read of one this run took more recently is still this run's.
231
226
  const ours = held === 'busy' ? Date.now() - mine.at < STALE_MS : held !== 'gone' && held.token === mine.token;
232
227
  if (ours)
233
228
  settle(() => fsx.unlinkSync(lock), log, 'spool lock not released');
234
229
  }
235
230
  /** Claim time sits in the claim's name, so a claim over STALE_MS away from now belongs to a replayer that never finished. True when one was put back. */
236
- function recoverStaleClaims(dir, names, log) {
231
+ function recoverStaleClaims(fsx, dir, names, log) {
237
232
  const now = Date.now();
238
233
  // A claim whose unlink stayed busy after its `.bad` landed is a spare copy, not a second failure.
239
234
  const setAside = new Set(names.flatMap((name) => BAD.exec(name)?.[1] ?? []));
@@ -256,21 +251,21 @@ function recoverStaleClaims(dir, names, log) {
256
251
  putBackOne = true;
257
252
  }
258
253
  }
259
- else if (moveBad(dir, file, { stem: claim.stem, attempt: claim.attempt, cause: 'interrupted', log })) {
254
+ else if (moveBad(fsx, dir, file, { stem: claim.stem, attempt: claim.attempt, cause: 'interrupted', log })) {
260
255
  log(`spool file ${claim.name} was claimed by a replayer that never finished ${MAX_ATTEMPTS} times, set aside as .bad`);
261
256
  }
262
257
  }
263
258
  return putBackOne;
264
259
  }
265
260
  /** Returns a claim to the waiting files as `<stem>.a<attempt>.json`; one that stays busy is left for stale recovery. */
266
- function putBack(dir, claim, stem, attempt, log) {
261
+ function putBack(fsx, dir, claim, stem, attempt, log) {
267
262
  const name = path.basename(claim);
268
263
  if (settle(() => fsx.renameSync(claim, path.join(dir, `${stem}.a${attempt}.json`)), log, `spool file ${name} not put back`) === 'busy') {
269
264
  log(`spool file ${name} could not be put back; it returns ${STALE_MS / MINUTE_MS} minutes after its claim`);
270
265
  }
271
266
  }
272
267
  /** Writes `<stem>.<cause>.bad` through a random temp file, so the rename replaces any torn `.bad` a crash left. */
273
- function writeBad(dir, options) {
268
+ function writeBad(fsx, dir, options) {
274
269
  const { stem, cause, text, log, what } = options;
275
270
  const tmp = path.join(dir, `${stem}.${cause}.bad.${randomBytes(4).toString('hex')}.tmp`);
276
271
  const written = settle(() => fsx.writeFileSync(tmp, text, { encoding: 'utf8', flag: 'wx' }), log, what);
@@ -280,21 +275,21 @@ function writeBad(dir, options) {
280
275
  return moved === 'done';
281
276
  }
282
277
  /** Sets a claim aside as `.bad` before the claim goes, so a crash between leaves a whole copy; a busy write puts the claim back. */
283
- function moveBad(dir, claim, options) {
278
+ function moveBad(fsx, dir, claim, options) {
284
279
  const { stem, attempt, cause, log, raw } = options;
285
280
  const what = `spool file ${path.basename(claim)} not set aside`;
286
281
  let text = raw ?? '';
287
282
  if (raw === undefined && settle(() => { text = fsx.readFileSync(claim, 'utf8'); }, log, what) !== 'done')
288
283
  return false;
289
- if (!writeBad(dir, { stem, cause, text, log, what })) {
290
- putBack(dir, claim, stem, attempt, log);
284
+ if (!writeBad(fsx, dir, { stem, cause, text, log, what })) {
285
+ putBack(fsx, dir, claim, stem, attempt, log);
291
286
  return false;
292
287
  }
293
288
  settle(() => fsx.unlinkSync(claim), log, what);
294
289
  return true;
295
290
  }
296
291
  /** A tmp over STALE_MS old belongs to a spool that never renamed it: a whole one joins the waiting files, a torn one is set aside. True when one joined. */
297
- function promoteTmp(dir, names, tenantId, log) {
292
+ function promoteTmp(fsx, dir, names, tenantId, log) {
298
293
  const now = Date.now();
299
294
  let promoted = false;
300
295
  for (const name of names) {
@@ -320,7 +315,7 @@ function promoteTmp(dir, names, tenantId, log) {
320
315
  promoted = true;
321
316
  }
322
317
  }
323
- else if (writeBad(dir, { stem: m[1], cause: 'unreadable', text, log, what })) {
318
+ else if (writeBad(fsx, dir, { stem: m[1], cause: 'unreadable', text, log, what })) {
324
319
  settle(() => fsx.unlinkSync(file), log, what);
325
320
  log(`spool file ${name} was never finished, set aside as .bad`);
326
321
  }
@@ -328,13 +323,13 @@ function promoteTmp(dir, names, tenantId, log) {
328
323
  return promoted;
329
324
  }
330
325
  /** Removes a claim once the store holds its summary; a claim left behind is imported again, so the log says so. */
331
- function removeClaim(claim, name, log) {
326
+ function removeClaim(fsx, claim, name, log) {
332
327
  const removed = settleCode(() => fsx.unlinkSync(claim), log, `spool file ${name} claim not removed`);
333
328
  if (removed.settled === 'busy')
334
329
  log(`spool file ${name} saved; its claim could not be removed (${removed.code}); it will be imported again`);
335
330
  }
336
331
  /** After an importer throw: a busy store stops the run with the count unchanged, any other error counts one try toward .bad. */
337
- function importFailed(dir, claim, entry, options) {
332
+ function importFailed(fsx, dir, claim, entry, options) {
338
333
  const { text, recorded, cause, log } = options;
339
334
  const { name } = entry;
340
335
  const busy = isSqliteBusy(cause);
@@ -346,22 +341,22 @@ function importFailed(dir, claim, entry, options) {
346
341
  return busy ? 'stop' : 'skipped';
347
342
  }
348
343
  if (busy) {
349
- putBack(dir, claim, entry.stem, entry.attempt, log);
344
+ putBack(fsx, dir, claim, entry.stem, entry.attempt, log);
350
345
  log(`spool file ${name} waits for the next run: the store is busy`);
351
346
  return 'stop';
352
347
  }
353
348
  const next = entry.attempt + 1;
354
349
  if (next < MAX_ATTEMPTS) {
355
- putBack(dir, claim, entry.stem, next, log);
350
+ putBack(fsx, dir, claim, entry.stem, next, log);
356
351
  log(`spool file ${name} failed to import (try ${next} of ${MAX_ATTEMPTS}): ${errorMessage(cause)}`);
357
352
  }
358
- else if (moveBad(dir, claim, { stem: entry.stem, attempt: entry.attempt, cause: 'failed', log, raw: text })) {
353
+ else if (moveBad(fsx, dir, claim, { stem: entry.stem, attempt: entry.attempt, cause: 'failed', log, raw: text })) {
359
354
  log(`spool file ${name} set aside as .bad after ${MAX_ATTEMPTS} tries: ${errorMessage(cause)}`);
360
355
  }
361
356
  return 'skipped';
362
357
  }
363
358
  /** Claims, reads and imports one waiting file; `stop` ends the run because the store is busy. */
364
- function importFile(dir, entry, tenantId, log, importOne) {
359
+ function importFile(fsx, dir, entry, tenantId, log, importOne) {
365
360
  const { name } = entry;
366
361
  const claim = path.join(dir, `${entry.stem}.a${entry.attempt}.claim-${stamp(Date.now())}`);
367
362
  const taken = settleCode(() => fsx.renameSync(path.join(dir, name), claim), log, `spool file ${name} not claimed`);
@@ -374,12 +369,12 @@ function importFile(dir, entry, tenantId, log, importOne) {
374
369
  if (read === 'gone')
375
370
  log(`spool file ${name} vanished`);
376
371
  if (read === 'busy')
377
- putBack(dir, claim, entry.stem, entry.attempt, log);
372
+ putBack(fsx, dir, claim, entry.stem, entry.attempt, log);
378
373
  if (read !== 'done')
379
374
  return 'skipped';
380
375
  const spooled = parseSpooled(text, tenantId);
381
376
  if (spooled === null) {
382
- if (moveBad(dir, claim, { stem: entry.stem, attempt: entry.attempt, cause: 'unreadable', log, raw: text })) {
377
+ if (moveBad(fsx, dir, claim, { stem: entry.stem, attempt: entry.attempt, cause: 'unreadable', log, raw: text })) {
383
378
  log(`spool file ${name} is not readable, set aside as .bad`);
384
379
  }
385
380
  return 'skipped';
@@ -389,7 +384,7 @@ function importFile(dir, entry, tenantId, log, importOne) {
389
384
  if (recorded)
390
385
  return;
391
386
  recorded = true;
392
- removeClaim(claim, name, log);
387
+ removeClaim(fsx, claim, name, log);
393
388
  };
394
389
  try {
395
390
  importOne(spooled, markRecorded);
@@ -397,10 +392,10 @@ function importFile(dir, entry, tenantId, log, importOne) {
397
392
  return 'imported';
398
393
  }
399
394
  catch (err) {
400
- return importFailed(dir, claim, entry, { text, recorded, cause: err, log });
395
+ return importFailed(fsx, dir, claim, entry, { text, recorded, cause: err, log });
401
396
  }
402
397
  }
403
- function mtimeOf(file) {
398
+ function mtimeOf(fsx, file) {
404
399
  try {
405
400
  return fsx.statSync(file).mtimeMs;
406
401
  }
@@ -413,7 +408,7 @@ function mtimeOf(file) {
413
408
  }
414
409
  }
415
410
  /** What doctor reports, read only: files a replay should have taken by now, claims and tmp files a dead process left, and `.bad` files. */
416
- export function spoolCounts(hippoRoot, now, waitingAfterMs) {
411
+ export function spoolCounts(hippoRoot, now, waitingAfterMs, fsx = fs) {
417
412
  const counts = { waiting: 0, stale: 0, bad: 0 };
418
413
  const dir = path.join(hippoRoot, SPOOL_DIR);
419
414
  if (!fsx.existsSync(dir))
@@ -426,12 +421,12 @@ export function spoolCounts(hippoRoot, now, waitingAfterMs) {
426
421
  if (name.endsWith('.bad'))
427
422
  counts.bad++;
428
423
  else if (waiting !== null) {
429
- const spooled = NEW_STEM.test(waiting[1]) ? created(waiting[1]) : mtimeOf(file);
424
+ const spooled = NEW_STEM.test(waiting[1]) ? created(waiting[1]) : mtimeOf(fsx, file);
430
425
  if (spooled !== null && at - spooled > waitingAfterMs)
431
426
  counts.waiting++;
432
427
  }
433
428
  else if (claim !== null || TMP.test(name)) {
434
- const since = claim?.claimedAt ?? mtimeOf(file);
429
+ const since = claim?.claimedAt ?? mtimeOf(fsx, file);
435
430
  if (since !== null && Math.abs(at - since) > STALE_MS)
436
431
  counts.stale++;
437
432
  }
@@ -439,25 +434,25 @@ export function spoolCounts(hippoRoot, now, waitingAfterMs) {
439
434
  return counts;
440
435
  }
441
436
  /** One replayer at a time holds `replay.lock`, so only one process claims a file even where Windows lets two renames of it both land. */
442
- export function importSpool(hippoRoot, tenantId, log, deadline, importOne) {
437
+ export function importSpool(hippoRoot, tenantId, log, deadline, importOne, fsx = fs) {
443
438
  const dir = path.join(hippoRoot, SPOOL_DIR);
444
439
  if (!fsx.existsSync(dir))
445
440
  return 0;
446
- const mine = takeLock(dir, log);
441
+ const mine = takeLock(fsx, dir, log);
447
442
  if (mine === null)
448
443
  return 0;
449
444
  try {
450
- if (!ownsLock(dir, mine.token, log))
445
+ if (!ownsLock(fsx, dir, mine.token, log))
451
446
  return 0;
452
447
  const names = fsx.readdirSync(dir);
453
- const promoted = promoteTmp(dir, names, tenantId, log);
454
- const recovered = recoverStaleClaims(dir, names, log);
448
+ const promoted = promoteTmp(fsx, dir, names, tenantId, log);
449
+ const recovered = recoverStaleClaims(fsx, dir, names, log);
455
450
  let finished = 0;
456
451
  // Only promotion and recovery add waiting names, so the folder is listed again only after one of them did.
457
452
  for (const entry of waitingFiles(promoted || recovered ? fsx.readdirSync(dir) : names)) {
458
- if (Date.now() > deadline || !ownsLock(dir, mine.token, log))
453
+ if (Date.now() > deadline || !ownsLock(fsx, dir, mine.token, log))
459
454
  break;
460
- const outcome = importFile(dir, entry, tenantId, log, importOne);
455
+ const outcome = importFile(fsx, dir, entry, tenantId, log, importOne);
461
456
  if (outcome === 'imported')
462
457
  finished++;
463
458
  if (outcome === 'stop')
@@ -466,7 +461,7 @@ export function importSpool(hippoRoot, tenantId, log, deadline, importOne) {
466
461
  return finished;
467
462
  }
468
463
  finally {
469
- releaseLock(dir, mine, log);
464
+ releaseLock(fsx, dir, mine, log);
470
465
  }
471
466
  }
472
467
  //# sourceMappingURL=compaction-spool.js.map
@@ -24,7 +24,7 @@ export function sessionEndHandoffForCaller(ctx, req) {
24
24
  const state = checkedWorkingState(req.workingState);
25
25
  const evidence = checkedEvidence(req.evidence);
26
26
  const key = bindCaller(ctx, req.sessionId, req.project);
27
- const handoff = writeSessionEndHandoff(ctx.hippoRoot, ctx.tenantId, req.sessionId, evidence, state, key);
27
+ const handoff = writeSessionEndHandoff(ctx.hippoRoot, { tenantId: ctx.tenantId, sessionId: req.sessionId, evidence, derived: state, key });
28
28
  const snapshotsClosed = closeTaskSnapshotsForSession(ctx.hippoRoot, ctx.tenantId, req.sessionId, 'session-ended', key);
29
29
  return { handoffWritten: handoff !== null, snapshotsClosed };
30
30
  }
@@ -3,7 +3,7 @@
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import { homeDir } from '../util/agent-homes.js';
6
- import { errorMessage, log as logger } from '../util/log.js';
6
+ import { errorCode, errorMessage, log as logger } from '../util/log.js';
7
7
  import { isObjectLike, isStringValue } from '../core/capture-contract.js';
8
8
  import { SESSION_ID_RE } from './copilot-transcript.js';
9
9
  import { blockHash } from '../util/token-text.js';
@@ -78,10 +78,16 @@ function releaseSessionLock(lock) {
78
78
  logger.debug(`session lock not removed: ${errorMessage(err)}`);
79
79
  }
80
80
  }
81
- /** False when the sessions folder cannot be written, so the caller runs unlocked as before. */
81
+ /** False when the request could not be queued; the caller then refuses the run, since an unlocked worker could race a live one. */
82
82
  function queueRun(files, mode) {
83
83
  try {
84
84
  fs.mkdirSync(path.dirname(files.queued), { recursive: true });
85
+ }
86
+ catch (err) {
87
+ logger.warn(`session worker: cannot create ${path.dirname(files.queued)}, so the ${mode} close is not run: ${errorMessage(err)}`);
88
+ return false;
89
+ }
90
+ try {
85
91
  // A full close outranks a turn close: it overwrites a queued turn, and a turn never replaces a queued full close.
86
92
  if (mode === 'full')
87
93
  fs.writeFileSync(files.queued, 'full');
@@ -90,7 +96,10 @@ function queueRun(files, mode) {
90
96
  return true;
91
97
  }
92
98
  catch (err) {
93
- return err instanceof Error && 'code' in err && err.code === 'EEXIST';
99
+ if (errorCode(err) === 'EEXIST')
100
+ return true;
101
+ logger.warn(`session worker: cannot queue the ${mode} close in ${files.queued}, so it is not run: ${errorMessage(err)}`);
102
+ return false;
94
103
  }
95
104
  }
96
105
  /** Takes the queued request by renaming it, so one queued while this reads it lands in a fresh file. */
@@ -99,8 +108,10 @@ function takeQueued(queued) {
99
108
  try {
100
109
  fs.renameSync(queued, taken);
101
110
  }
102
- catch {
103
- // Nothing queued, or a rename refused for a moment; the request stays queued for the next round.
111
+ catch (err) {
112
+ // ENOENT means nothing is queued; any other refusal leaves the request queued for the next round, but is told.
113
+ if (errorCode(err) !== 'ENOENT')
114
+ logger.warn(`session worker: cannot take the queued request ${queued}: ${errorMessage(err)}`);
104
115
  return null;
105
116
  }
106
117
  try {
@@ -113,8 +124,10 @@ function takeQueued(queued) {
113
124
  /** Runs `work` unless a live worker holds this session, which then runs the request after its own so the last reply is never dropped. */
114
125
  export async function runSessionWorker(sessionId, mode, work) {
115
126
  const files = sessionStateFiles(sessionId);
116
- if (files === null || !queueRun(files, mode))
127
+ if (files === null)
117
128
  return work(mode);
129
+ if (!queueRun(files, mode))
130
+ return;
118
131
  for (let take = takeSessionLock(files.lock); take !== 'held'; take = takeSessionLock(files.lock)) {
119
132
  if (take === 'unusable')
120
133
  return work(mode);
package/dist/cli/audit.js CHANGED
@@ -72,7 +72,7 @@ async function cmdAuditList(hippoRoot, tenantId, flags) {
72
72
  console.log(formatAuditRow(ev));
73
73
  }
74
74
  }
75
- function cmdAuditPrune(hippoRoot, ctxTenantId, flags) {
75
+ async function cmdAuditPrune(hippoRoot, ctxTenantId, flags) {
76
76
  const olderThanRaw = stringFlag(flags, 'older-than') ?? '';
77
77
  if (!olderThanRaw) {
78
78
  printError('Usage: hippo audit prune --older-than <Nd> [--dry-run] [--tenant <t>]');
@@ -89,7 +89,7 @@ function cmdAuditPrune(hippoRoot, ctxTenantId, flags) {
89
89
  const tenantId = stringFlag(flags, 'tenant')?.trim() || ctxTenantId;
90
90
  const dryRun = flagIsTrue(flags, 'dry-run');
91
91
  const asJson = boolFlag(flags, 'json');
92
- const result = api.pruneAuditLog(cliApiContext(hippoRoot, tenantId), { olderThanDays, dryRun });
92
+ const result = await api.pruneAuditLog(cliApiContext(hippoRoot, tenantId), { olderThanDays, dryRun });
93
93
  if (asJson) {
94
94
  console.log(JSON.stringify(result));
95
95
  return;
@@ -107,15 +107,15 @@ async function cmdAuditLog(hippoRoot, tenantId, args, flags) {
107
107
  return;
108
108
  }
109
109
  if (sub === 'prune') {
110
- cmdAuditPrune(hippoRoot, tenantId, flags);
110
+ await cmdAuditPrune(hippoRoot, tenantId, flags);
111
111
  return;
112
112
  }
113
113
  printError(`Unknown audit subcommand: ${sub}. Expected: list | prune.`);
114
114
  throw new CliExit(1);
115
115
  }
116
- function auditRepair(hippoRoot, tenantId, flags) {
116
+ async function auditRepair(hippoRoot, tenantId, flags) {
117
117
  const apply = flagIsTrue(flags, 'apply') && flags['dry-run'] !== true;
118
- const result = api.repairMemoryQuality(cliApiContext(flags['global'] ? getGlobalRoot() : hippoRoot, tenantId), apply);
118
+ const result = await api.repairMemoryQuality(cliApiContext(flags['global'] ? getGlobalRoot() : hippoRoot, tenantId), apply);
119
119
  if (flags['json']) {
120
120
  console.log(JSON.stringify(result));
121
121
  return;
@@ -137,7 +137,7 @@ function auditRepair(hippoRoot, tenantId, flags) {
137
137
  if (!apply)
138
138
  console.log('Preview only. Add --apply to move set-aside memories to dormant storage. Pin a review memory to keep it.');
139
139
  }
140
- function fixAuditErrors(hippoRoot, tenantId, result, flags) {
140
+ async function fixAuditErrors(hippoRoot, tenantId, result, flags) {
141
141
  const errors = result.issues.filter(i => i.severity === 'error');
142
142
  if (errors.length > 0 && flagIsTrue(flags, 'dry-run')) {
143
143
  console.log(`\nWould remove ${errors.length} error-severity memories (dry run, nothing deleted).`);
@@ -145,7 +145,7 @@ function fixAuditErrors(hippoRoot, tenantId, result, flags) {
145
145
  }
146
146
  else if (errors.length > 0) {
147
147
  const ctx = cliApiContext(hippoRoot, tenantId);
148
- const removedCount = api.removeAuditErrors(ctx, 'audit --fix', errors).length;
148
+ const removedCount = (await api.removeAuditErrors(ctx, 'audit --fix', errors)).length;
149
149
  console.log(`\nRemoved ${removedCount} error-severity memories.`);
150
150
  console.log(`${result.issues.length - errors.length} warnings remain (review manually).`);
151
151
  }
@@ -155,7 +155,7 @@ function fixAuditErrors(hippoRoot, tenantId, result, flags) {
155
155
  }
156
156
  export async function handleAudit({ hippoRoot, tenantId, args, flags }) {
157
157
  if (args[0] === 'repair') {
158
- auditRepair(hippoRoot, tenantId, flags);
158
+ await auditRepair(hippoRoot, tenantId, flags);
159
159
  return;
160
160
  }
161
161
  // `audit list` and `audit prune` are the audit-log subcommands; other forms (no sub, --fix) keep the memory-quality auditor.
@@ -164,7 +164,7 @@ export async function handleAudit({ hippoRoot, tenantId, args, flags }) {
164
164
  return;
165
165
  }
166
166
  requireInit(hippoRoot);
167
- const result = api.auditMemoryQuality(cliApiContext(hippoRoot, tenantId));
167
+ const result = await api.auditMemoryQuality(cliApiContext(hippoRoot, tenantId));
168
168
  const shouldFix = boolFlag(flags, 'fix');
169
169
  if (result.issues.length === 0) {
170
170
  console.log(`All ${result.total} memories passed quality checks.`);
@@ -177,7 +177,7 @@ export async function handleAudit({ hippoRoot, tenantId, args, flags }) {
177
177
  console.log(` "${truncateWithEllipsis(issue.content, 80)}"`);
178
178
  }
179
179
  if (shouldFix) {
180
- fixAuditErrors(hippoRoot, tenantId, result, flags);
180
+ await fixAuditErrors(hippoRoot, tenantId, result, flags);
181
181
  }
182
182
  else {
183
183
  console.log(`\nRun with --fix to auto-remove error-severity issues.`);
package/dist/cli/auth.js CHANGED
@@ -28,7 +28,7 @@ function noteMintDefaults(roleGiven, expiryGiven) {
28
28
  if (notes.length > 0)
29
29
  printError(`hippo auth create: ${notes.join('; ')}.`);
30
30
  }
31
- function cmdAuthCreate(hippoRoot, defaultTenantId, flags) {
31
+ async function cmdAuthCreate(hippoRoot, defaultTenantId, flags) {
32
32
  const root = resolveAuthRoot(hippoRoot, flags);
33
33
  const tenantFlag = stringFlag(flags, 'tenant');
34
34
  const labelFlag = stringFlag(flags, 'label');
@@ -45,7 +45,7 @@ function cmdAuthCreate(hippoRoot, defaultTenantId, flags) {
45
45
  const ctx = cliApiContext(root, tenantFlag ?? defaultTenantId);
46
46
  let result;
47
47
  try {
48
- result = api.authCreate(ctx, { label: labelFlag, role, ttlDays, noExpiry: noExpiry || undefined });
48
+ result = await api.authCreate(ctx, { label: labelFlag, role, ttlDays, noExpiry: noExpiry || undefined });
49
49
  }
50
50
  catch (err) {
51
51
  if (!(err instanceof api.BadRequestError))
@@ -112,7 +112,7 @@ async function cmdAuthRevoke(hippoRoot, tenantId, keyId, flags) {
112
112
  const ctx = await keyContext(resolveAuthRoot(hippoRoot, flags), tenantId, keyId);
113
113
  let revokedAt;
114
114
  try {
115
- revokedAt = api.authRevoke(ctx, keyId).revokedAt;
115
+ revokedAt = (await api.authRevoke(ctx, keyId)).revokedAt;
116
116
  }
117
117
  catch (err) {
118
118
  printError(`Error: ${errorMessage(err)}`);
@@ -152,7 +152,7 @@ export async function handleAuth({ hippoRoot, tenantId, args, flags }) {
152
152
  const subArgs = args.slice(1);
153
153
  switch (sub) {
154
154
  case 'create':
155
- cmdAuthCreate(hippoRoot, tenantId, flags);
155
+ await cmdAuthCreate(hippoRoot, tenantId, flags);
156
156
  return;
157
157
  case 'list':
158
158
  cmdAuthList(hippoRoot, tenantId, flags);
@@ -1,8 +1,7 @@
1
1
  /** HTTP client wrapper for `hippo serve`: only the routed writes (remember, forget, archive, promote); every other command opens the store directly.
2
2
  * Server errors rethrow with the message verbatim so CLI substring matches still work; network errors propagate as the native fetch failure. */
3
3
  import type { RememberOpts, RememberResult } from '../api/index.js';
4
- /** An error the server answered with, not one the transport raised: the refused-connection classifier sniffs message text,
5
- * and a server message quotes the caller's own id or content back verbatim. */
4
+ /** An error the server answered with, not one the transport raised, so the transport classifier can skip it by class. */
6
5
  export declare class HttpResponseError extends Error {
7
6
  readonly status: number;
8
7
  constructor(message: string, status: number);
@@ -3,6 +3,7 @@
3
3
  import { fetchWithRetry } from '../util/http-retry.js';
4
4
  import { readCappedJson } from '../util/capped-json.js';
5
5
  import { isJsonObject, isJsonString } from '../util/json.js';
6
+ import { isObjectLike } from '../core/capture-contract.js';
6
7
  /** A write the local server has not answered in this long is stuck; the caller treats it as delivery-unknown. */
7
8
  const SERVER_TIMEOUT_MS = 30_000;
8
9
  /** Five tries at the server's 1 s Retry-After keeps the roughly 5 s wait the CLI had before the server answered busy with 503. */
@@ -26,8 +27,7 @@ function buildHeaders(apiKey, withBody) {
26
27
  headers['authorization'] = `Bearer ${apiKey}`;
27
28
  return headers;
28
29
  }
29
- /** An error the server answered with, not one the transport raised: the refused-connection classifier sniffs message text,
30
- * and a server message quotes the caller's own id or content back verbatim. */
30
+ /** An error the server answered with, not one the transport raised, so the transport classifier can skip it by class. */
31
31
  export class HttpResponseError extends Error {
32
32
  status;
33
33
  constructor(message, status) {
@@ -109,29 +109,29 @@ export async function archiveRaw(serverUrl, apiKey, id, reason) {
109
109
  await throwForStatus(res);
110
110
  return readReply(res, 'POST /v1/memories/:id/archive', (b) => isJsonString(b.archivedAt));
111
111
  }
112
- function hasObjectCause(e) {
113
- // Node's fs/net system errors (ECONNREFUSED, ECONNRESET) attach the syscall code on a non-null object `cause`;
114
- // the strict-equality checks below validate it before use.
115
- return typeof e.cause === 'object' && e.cause !== null;
112
+ /** The syscall code Node puts on a system error itself (`http` errors) or on the `cause` of a fetch `TypeError`; '' when there is none. */
113
+ function transportCode(err) {
114
+ if ('code' in err)
115
+ return String(err.code);
116
+ // Node's fetch attaches the syscall code (ECONNREFUSED, ECONNRESET) on a non-null object `cause`.
117
+ const cause = err.cause;
118
+ return isObjectLike(cause) && 'code' in cause ? String(cause.code) : '';
116
119
  }
117
120
  export function classifyTransportFailure(err) {
118
121
  // The server answered, so the transport worked, whatever the message says.
119
122
  if (err instanceof HttpResponseError)
120
123
  return 'none';
121
- const message = err.message.toLowerCase();
122
- const code = hasObjectCause(err) ? err.cause.code : undefined;
124
+ const code = transportCode(err);
123
125
  // Connect-phase failures: no request bytes ever left the client, so a stale
124
126
  // pidfile can be healed and the call replayed on the direct path.
125
127
  if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EAI_AGAIN')
126
128
  return 'never-sent';
127
- if (message.includes('econnrefused'))
128
- return 'never-sent';
129
- // The socket died mid-exchange. Node fetch surfaces this as a bare
130
- // 'fetch failed' with the syscall code on the cause, so check both shapes.
129
+ // The socket died mid-exchange.
131
130
  if (code === 'ECONNRESET' || code === 'ECONNABORTED' || code === 'EPIPE' || code === 'UND_ERR_SOCKET') {
132
131
  return 'delivery-unknown';
133
132
  }
134
- if (message.includes('socket hang up') || message.includes('fetch failed'))
133
+ // Fetch rejects every network failure as a TypeError, with a cause only when the syscall is known, so no code means the conservative answer.
134
+ if (err instanceof TypeError)
135
135
  return 'delivery-unknown';
136
136
  // The request was sent and the server went quiet, so it may have committed.
137
137
  if (err.name === 'TimeoutError')
@@ -54,7 +54,7 @@ async function renderContext(hippoRoot, resolvedTenant, args, flags, stdinText,
54
54
  const session = resolveContextSession(stdinText);
55
55
  const { currentSessionId, ledgerSessionId, payloadSessionId } = session;
56
56
  // The pilot arm is booked at the first hook call whatever the flags, so the holdout sees no budget or content branch.
57
- if (inPilotHoldout(hippoRoot, resolvedTenant, currentSessionId, payloadSessionId !== undefined)) {
57
+ if (inPilotHoldout(hippoRoot, resolvedTenant, currentSessionId, { write: payloadSessionId !== undefined })) {
58
58
  rec?.disabled();
59
59
  return;
60
60
  }
@@ -1,5 +1,5 @@
1
1
  import { type CommandContext } from './flag-values.js';
2
- export declare function handleOutcome({ hippoRoot, tenantId, flags }: CommandContext): void;
2
+ export declare function handleOutcome({ hippoRoot, tenantId, flags }: CommandContext): Promise<void>;
3
3
  export declare function handleConflicts({ hippoRoot, tenantId, flags }: CommandContext): void;
4
4
  export declare function handleResolve({ hippoRoot, tenantId, args, flags }: CommandContext): Promise<void>;
5
5
  export declare function handleReject({ hippoRoot, tenantId, args, flags }: CommandContext): void;
@@ -21,7 +21,7 @@ const CONFLICT_PREVIEW_CHARS = 120;
21
21
  const REJECTED_DIGEST_LIST_CHARS = 16;
22
22
  const DORMANT_PREVIEW_CHARS = 100;
23
23
  const STRENGTH_DECIMALS = 3;
24
- export function handleOutcome({ hippoRoot, tenantId, flags }) {
24
+ export async function handleOutcome({ hippoRoot, tenantId, flags }) {
25
25
  requireInit(hippoRoot);
26
26
  const good = boolFlag(flags, 'good');
27
27
  const bad = boolFlag(flags, 'bad');
@@ -34,10 +34,10 @@ export function handleOutcome({ hippoRoot, tenantId, flags }) {
34
34
  const specificId = flags['id'] ? String(flags['id']) : null;
35
35
  let updated;
36
36
  if (specificId) {
37
- updated = api.outcome(ctx, [specificId], good).applied;
37
+ updated = (await api.outcome(ctx, [specificId], good)).applied;
38
38
  }
39
39
  else {
40
- const r = api.outcomeForLastRecall(ctx, good);
40
+ const r = await api.outcomeForLastRecall(ctx, good);
41
41
  if (r.ids.length === 0) {
42
42
  console.log('No recent recall to apply outcome to. Use --id <id> to target a specific memory.');
43
43
  return;
@@ -48,17 +48,17 @@ export function handleOutcome({ hippoRoot, tenantId, flags }) {
48
48
  }
49
49
  // Shared between the forget dispatch and cmdForget so the message can't drift.
50
50
  const ARCHIVE_REASON_REQUIRED = 'hippo forget --archive requires --reason "<why>" (recorded on the archive).';
51
- function cmdForget(hippoRoot, tenantId, id, flags) {
51
+ async function cmdForget(hippoRoot, tenantId, id, flags) {
52
52
  requireInit(hippoRoot);
53
53
  const ctx = cliApiContext(hippoRoot, tenantId);
54
54
  // Raw memories (connector ingestion) are append-only: a BEFORE-DELETE trigger aborts any delete,
55
55
  // so archiveRaw is the sanctioned removal path and records ctx.actor as the archiver.
56
56
  if (flagIsTrue(flags, 'archive')) {
57
- archiveForgottenRaw(ctx, id, stringFlag(flags, 'reason') ?? null);
57
+ await archiveForgottenRaw(ctx, id, stringFlag(flags, 'reason') ?? null);
58
58
  return;
59
59
  }
60
60
  try {
61
- api.forget(ctx, id);
61
+ await api.forget(ctx, id);
62
62
  console.log(`Forgot ${id}`);
63
63
  }
64
64
  catch (err) {
@@ -66,13 +66,13 @@ function cmdForget(hippoRoot, tenantId, id, flags) {
66
66
  throw new CliExit(1);
67
67
  }
68
68
  }
69
- function archiveForgottenRaw(ctx, id, reason) {
69
+ async function archiveForgottenRaw(ctx, id, reason) {
70
70
  if (!reason) {
71
71
  printError(ARCHIVE_REASON_REQUIRED);
72
72
  throw new CliExit(1);
73
73
  }
74
74
  try {
75
- api.archiveRaw(ctx, id, reason);
75
+ await api.archiveRaw(ctx, id, reason);
76
76
  console.log(`Archived ${id}`);
77
77
  }
78
78
  catch (err) {
@@ -453,7 +453,7 @@ export async function handleForget({ hippoRoot, tenantId, args, flags }) {
453
453
  });
454
454
  if (routed)
455
455
  return;
456
- cmdForget(hippoRoot, tenantId, id, flags);
456
+ await cmdForget(hippoRoot, tenantId, id, flags);
457
457
  }
458
458
  function invalidateChurn(hippoRoot, tenantId, args, flags) {
459
459
  if (args[0] || flags['id'] !== undefined) {