hippo-memory 1.52.9 → 1.53.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 (76) hide show
  1. package/README.md +47 -13
  2. package/dist/agent-memories/apply.d.ts +47 -0
  3. package/dist/agent-memories/apply.js +253 -0
  4. package/dist/agent-memories/claude-code.d.ts +11 -0
  5. package/dist/agent-memories/claude-code.js +113 -0
  6. package/dist/agent-memories/codex.d.ts +3 -0
  7. package/dist/agent-memories/codex.js +47 -0
  8. package/dist/agent-memories/copilot.d.ts +3 -0
  9. package/dist/agent-memories/copilot.js +125 -0
  10. package/dist/agent-memories/files.d.ts +37 -0
  11. package/dist/agent-memories/files.js +77 -0
  12. package/dist/agent-memories/folder-store.d.ts +17 -0
  13. package/dist/agent-memories/folder-store.js +44 -0
  14. package/dist/agent-memories/gemini.d.ts +3 -0
  15. package/dist/agent-memories/gemini.js +103 -0
  16. package/dist/agent-memories/git.d.ts +8 -0
  17. package/dist/agent-memories/git.js +11 -0
  18. package/dist/agent-memories/keys.d.ts +9 -0
  19. package/dist/agent-memories/keys.js +20 -0
  20. package/dist/agent-memories/legacy.d.ts +17 -0
  21. package/dist/agent-memories/legacy.js +45 -0
  22. package/dist/agent-memories/markdown.d.ts +13 -0
  23. package/dist/agent-memories/markdown.js +123 -0
  24. package/dist/agent-memories/openclaw.d.ts +3 -0
  25. package/dist/agent-memories/openclaw.js +42 -0
  26. package/dist/agent-memories/plan.d.ts +78 -0
  27. package/dist/agent-memories/plan.js +123 -0
  28. package/dist/agent-memories/qwen-code.d.ts +5 -0
  29. package/dist/agent-memories/qwen-code.js +50 -0
  30. package/dist/agent-memories/report.d.ts +52 -0
  31. package/dist/agent-memories/report.js +88 -0
  32. package/dist/agent-memories/source.d.ts +16 -0
  33. package/dist/agent-memories/source.js +32 -0
  34. package/dist/agent-memories/sync.d.ts +33 -0
  35. package/dist/agent-memories/sync.js +336 -0
  36. package/dist/agent-memories/tools.d.ts +33 -0
  37. package/dist/agent-memories/tools.js +19 -0
  38. package/dist/agent-memories/types.d.ts +42 -0
  39. package/dist/agent-memories/types.js +2 -0
  40. package/dist/api.d.ts +2 -2
  41. package/dist/api.js +28 -29
  42. package/dist/audit.d.ts +4 -3
  43. package/dist/audit.js +10 -6
  44. package/dist/capture.d.ts +11 -22
  45. package/dist/capture.js +76 -81
  46. package/dist/cli.d.ts +0 -2
  47. package/dist/cli.js +133 -170
  48. package/dist/compaction-items.d.ts +18 -0
  49. package/dist/compaction-items.js +60 -0
  50. package/dist/compaction-record.d.ts +94 -0
  51. package/dist/compaction-record.js +573 -0
  52. package/dist/config.d.ts +4 -0
  53. package/dist/config.js +13 -0
  54. package/dist/consolidate.d.ts +0 -2
  55. package/dist/consolidate.js +3 -36
  56. package/dist/db.d.ts +5 -1
  57. package/dist/db.js +40 -8
  58. package/dist/dedupe.js +3 -2
  59. package/dist/doctor.js +34 -2
  60. package/dist/dormant.d.ts +5 -3
  61. package/dist/dormant.js +9 -0
  62. package/dist/gated-write.d.ts +9 -0
  63. package/dist/gated-write.js +24 -0
  64. package/dist/hooks.d.ts +4 -2
  65. package/dist/hooks.js +8 -7
  66. package/dist/memory.d.ts +19 -2
  67. package/dist/memory.js +32 -3
  68. package/dist/shared.js +10 -7
  69. package/dist/store.d.ts +14 -2
  70. package/dist/store.js +75 -19
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  74. package/extensions/openclaw-plugin/package.json +1 -1
  75. package/openclaw.plugin.json +1 -1
  76. package/package.json +1 -1
package/dist/audit.js CHANGED
@@ -109,11 +109,12 @@ function hasNoSpecificity(text) {
109
109
  return false;
110
110
  return words.length < 8 && VAGUE_ONLY.test(text);
111
111
  }
112
- export function auditMemory(entry) {
112
+ export function auditMemory(entry, backsObject = false) {
113
113
  const issue = classifyMemory(entry);
114
- // Error means "auto-remove"; a pinned or raw row is never auto-removed, so it can only warn.
115
- if (issue?.severity === 'error' && !canAutoDelete(entry)) {
116
- return { ...issue, severity: 'warning', reason: `${issue.reason} (${entry.pinned ? 'pinned' : 'raw'}, kept)` };
114
+ // Error means "auto-remove"; a pinned, raw, kept-for-good or object-backing row is never auto-removed, so it can only warn.
115
+ if (issue?.severity === 'error' && (!canAutoDelete(entry) || backsObject)) {
116
+ const why = entry.pinned ? 'pinned' : entry.kind === 'raw' ? 'raw' : backsObject ? 'backs an object' : 'keep rule';
117
+ return { ...issue, severity: 'warning', reason: `${issue.reason} (${why}, kept)` };
117
118
  }
118
119
  return issue;
119
120
  }
@@ -140,10 +141,11 @@ function classifyMemory(entry) {
140
141
  }
141
142
  return null;
142
143
  }
143
- export function auditMemories(entries) {
144
+ /** `backing`: ids of memories that back an object (store.memoriesBackingObjects). */
145
+ export function auditMemories(entries, backing) {
144
146
  const issues = [];
145
147
  for (const entry of entries) {
146
- const issue = auditMemory(entry);
148
+ const issue = auditMemory(entry, backing.has(entry.id));
147
149
  if (issue)
148
150
  issues.push(issue);
149
151
  }
@@ -229,6 +231,8 @@ export const AUDIT_OPS = [
229
231
  'quarantine', // emitted by recordQuarantine inside remember's write transaction
230
232
  'quarantine_approve', // emitted by api.quarantineApprove
231
233
  'quarantine_reject', // emitted by api.quarantineReject
234
+ 'agent_memory_restore', // emitted by the agent memory sync when a deleted note comes back
235
+ 'agent_memory_set_aside', // emitted by the agent memory sync when a note is deleted or refused
232
236
  ];
233
237
  function isBigIntValue(value) {
234
238
  return typeof value === 'bigint';
package/dist/capture.d.ts CHANGED
@@ -89,11 +89,13 @@ export declare function summariseTranscript(jsonl: string): string;
89
89
  * Priority, where the first source present is the only one tried:
90
90
  * 1. Explicit `transcriptPath` option (from `--transcript <path>`)
91
91
  * 2. Stdin JSON payload (Claude Code / OpenCode SessionEnd hook shape)
92
- * 3. Most recent `.jsonl` under `~/.claude/projects/<any>/`, only on a proven manual run (no path, no stdin text, no timed-out read), because this scan spans every project on the box
92
+ * 3. Most recent `.jsonl` under `~/.claude/projects/<any>/`, only when the caller passes `mayScan` (only the caller knows it is not a hook) and there is no path and no stdin text, because this scan spans every project on the box
93
93
  *
94
94
  * Returns null when nothing resolves, a named transcript or payload whose file is missing included. Never throws.
95
95
  */
96
- export declare function resolveLastSessionTranscript(explicit: string | undefined, stdinText: string | undefined, stdinTimedOut?: boolean): string | null;
96
+ export declare function resolveLastSessionTranscript(explicit: string | undefined, stdinText: string | undefined, opts: {
97
+ mayScan: boolean;
98
+ }): string | null;
97
99
  export declare function cmdCapture(hippoRoot: string, options: CaptureOptions): void;
98
100
  /** Never read the whole transcript — PreCompact fires exactly when it's largest. */
99
101
  export declare const PRE_COMPACT_TAIL_BYTES: number;
@@ -140,19 +142,6 @@ export declare function readTranscriptTail(transcriptPath: string, capBytes?: nu
140
142
  * guard, not per-file copies.
141
143
  */
142
144
  export declare function sanitizeLogMessage(message: string): string;
143
- /** What one pre-compact run saved, reported to the user after compaction. */
144
- export interface PreCompactReport {
145
- snapshotSaved: boolean;
146
- /** Claude Code session the run belonged to, when the payload named one. */
147
- sessionId: string | null;
148
- }
149
- /** Line shown after compaction, or null when nothing was saved (Claude Code's own generic message is the fallback). */
150
- export declare function preCompactMessage(report: Pick<PreCompactReport, 'snapshotSaved'>): string | null;
151
- /**
152
- * Where pre-compact leaves its report for `hippo post-compact`: next to the
153
- * pre-compact log, so both hooks find it from the same `--log-file`.
154
- */
155
- export declare function preCompactReportPath(logFile: string): string;
156
145
  /** A session's task, summary and next step from its transcript tail, secrets scrubbed and capped, '' where none; null with a logged reason when nothing is derivable. */
157
146
  export declare function transcriptWorkingState(transcriptPath: string, log: (message: string) => void): Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null;
158
147
  export interface PreCompactOptions {
@@ -168,12 +157,12 @@ export interface PreCompactOptions {
168
157
  * could turn a caught-and-logged failure back into a non-zero exit.
169
158
  */
170
159
  export declare function cmdPreCompact(hippoRoot: string, options: PreCompactOptions): Promise<void>;
171
- /**
172
- * The message for the PostCompact hook, from the report pre-compact left,
173
- * or null when there is nothing to say. Consumes the report, so the message
174
- * shows once. A report from another session, or an old one, is dropped.
175
- * Never throws.
176
- */
177
- export declare function postCompactMessage(stdinText: string | undefined, logFile?: string, now?: Date): string | null;
160
+ export interface PostCompactOptions {
161
+ stdinText?: string;
162
+ logFile?: string;
163
+ afterSave?: (transcriptPath: string, originProject: string, log: (message: string) => void) => void;
164
+ }
165
+ /** PostCompact entry point: saves the summary and its items, replays earlier leftovers, returns the one line to show. Never throws, so the hook exits 0. */
166
+ export declare function cmdPostCompact(hippoRoot: string, options: PostCompactOptions): string | null;
178
167
  export {};
179
168
  //# sourceMappingURL=capture.d.ts.map
package/dist/capture.js CHANGED
@@ -13,7 +13,9 @@ import * as path from 'path';
13
13
  import { createMemory, Layer } from './memory.js';
14
14
  import { duplicateKey, storedTextKeys } from './same-text.js';
15
15
  import { isContentWorthStoring } from './audit.js';
16
- import { isInitialized, writeEntry, loadAllEntries, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, } from './store.js';
16
+ import { isInitialized, openStore, stampOriginProject, writeEntryMirrors, loadAllEntries, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, } from './store.js';
17
+ import { gatedWrite } from './gated-write.js';
18
+ import { PRE_COMPACT_INSTRUCTION, compactionOrigin, parsePostCompactPayload, postCompactLine, recordCompactionStart, recordSnapshotSaved, saveCompaction, replayCompactionsAt, COMPACTION_DB_WAIT_MS, } from './compaction-record.js';
17
19
  import { getGlobalRoot, initGlobal } from './shared.js';
18
20
  import { embedMemory } from './embeddings.js';
19
21
  import { resolveTenantId } from './tenant.js';
@@ -627,11 +629,11 @@ function summariseSessionTurns(turns) {
627
629
  * Priority, where the first source present is the only one tried:
628
630
  * 1. Explicit `transcriptPath` option (from `--transcript <path>`)
629
631
  * 2. Stdin JSON payload (Claude Code / OpenCode SessionEnd hook shape)
630
- * 3. Most recent `.jsonl` under `~/.claude/projects/<any>/`, only on a proven manual run (no path, no stdin text, no timed-out read), because this scan spans every project on the box
632
+ * 3. Most recent `.jsonl` under `~/.claude/projects/<any>/`, only when the caller passes `mayScan` (only the caller knows it is not a hook) and there is no path and no stdin text, because this scan spans every project on the box
631
633
  *
632
634
  * Returns null when nothing resolves, a named transcript or payload whose file is missing included. Never throws.
633
635
  */
634
- export function resolveLastSessionTranscript(explicit, stdinText, stdinTimedOut = false) {
636
+ export function resolveLastSessionTranscript(explicit, stdinText, opts) {
635
637
  if (explicit)
636
638
  return fs.existsSync(explicit) ? explicit : null;
637
639
  if (stdinText && stdinText.trim() !== '') {
@@ -648,7 +650,7 @@ export function resolveLastSessionTranscript(explicit, stdinText, stdinTimedOut
648
650
  }
649
651
  return null;
650
652
  }
651
- if (stdinTimedOut)
653
+ if (!opts.mayScan)
652
654
  return null;
653
655
  const home = process.env.HOME || process.env.USERPROFILE;
654
656
  if (!home)
@@ -788,7 +790,7 @@ function cmdCaptureCore(hippoRoot, options) {
788
790
  case 'last-session': {
789
791
  let turns = options.sessionTurns;
790
792
  if (!turns) {
791
- const resolved = resolveLastSessionTranscript(options.transcriptPath, options.stdinText, options.stdinTimedOut);
793
+ const resolved = resolveLastSessionTranscript(options.transcriptPath, options.stdinText, { mayScan: !options.stdinTimedOut });
792
794
  if (!resolved) {
793
795
  console.log('No transcript found. Pass --transcript <path> or run from a SessionEnd hook.');
794
796
  return;
@@ -832,6 +834,7 @@ function cmdCaptureCore(hippoRoot, options) {
832
834
  // once, run the same checkRejectionGuard the real write path uses via
833
835
  // writeEntry, never write anything.
834
836
  const dryRunDb = options.dryRun ? openHippoDb(targetRoot) : null;
837
+ const writeDb = options.dryRun ? null : openStore(targetRoot);
835
838
  try {
836
839
  for (const item of extracted) {
837
840
  if (keys.has(duplicateKey(item.content))) {
@@ -876,19 +879,20 @@ function cmdCaptureCore(hippoRoot, options) {
876
879
  }
877
880
  console.log(` [capture] (${item.category}) ${item.content}`);
878
881
  }
879
- else {
882
+ else if (writeDb !== null) {
880
883
  // AT1 (plan §3 containment): one rejected item must not abort the
881
884
  // rest of this capture's items.
882
- try {
883
- writeEntry(targetRoot, entry);
885
+ const stamped = stampOriginProject(targetRoot, entry);
886
+ const outcome = gatedWrite(writeDb, targetRoot, stamped);
887
+ if (outcome === 'skipped:rejected') {
888
+ rejected++;
889
+ continue;
884
890
  }
885
- catch (err) {
886
- if (err instanceof RejectedValueError) {
887
- rejected++;
888
- continue;
889
- }
890
- throw err;
891
+ if (outcome !== 'written') {
892
+ skipped++;
893
+ continue;
891
894
  }
895
+ writeEntryMirrors(targetRoot, stamped);
892
896
  updateStats(targetRoot, { remembered: 1 });
893
897
  keys.add(duplicateKey(item.content)); // within-batch dedup
894
898
  void embedMemory(targetRoot, entry);
@@ -899,6 +903,8 @@ function cmdCaptureCore(hippoRoot, options) {
899
903
  finally {
900
904
  if (dryRunDb)
901
905
  closeHippoDb(dryRunDb);
906
+ if (writeDb)
907
+ closeHippoDb(writeDb);
902
908
  }
903
909
  const prefix = options.dryRun ? '[dry-run] ' : '';
904
910
  const globalPrefix = useGlobal ? '[global] ' : '';
@@ -1098,18 +1104,14 @@ function isReadableFile(filePath) {
1098
1104
  return false;
1099
1105
  }
1100
1106
  }
1101
- /** Line shown after compaction, or null when nothing was saved (Claude Code's own generic message is the fallback). */
1102
- export function preCompactMessage(report) {
1103
- if (!report.snapshotSaved)
1104
- return null;
1105
- return 'Hippo saved your task snapshot before compacting. The snapshot is restored into the new context.';
1106
- }
1107
- /**
1108
- * Where pre-compact leaves its report for `hippo post-compact`: next to the
1109
- * pre-compact log, so both hooks find it from the same `--log-file`.
1110
- */
1111
- export function preCompactReportPath(logFile) {
1112
- return path.join(path.dirname(logFile), 'pre-compact-last.json');
1107
+ /** PreCompact stdout is the summariser's instructions; sent before the snapshot work because a locked store can run the hook past its 30 s limit, and via writeSync because process.exit drops buffered pipe output. */
1108
+ function printPreCompactInstruction(logFile) {
1109
+ try {
1110
+ fs.writeSync(1, `${PRE_COMPACT_INSTRUCTION}\n`);
1111
+ }
1112
+ catch (err) {
1113
+ appendPreCompactLog(logFile, `instruction not printed: ${errorMessage(err)}`);
1114
+ }
1113
1115
  }
1114
1116
  /** A session's task, summary and next step from its transcript tail, secrets scrubbed and capped, '' where none; null with a logged reason when nothing is derivable. */
1115
1117
  export function transcriptWorkingState(transcriptPath, log) {
@@ -1150,8 +1152,8 @@ export function transcriptWorkingState(transcriptPath, log) {
1150
1152
  next_step: nextStep.trim() ? truncateCodePointSafe(nextStep, PRE_COMPACT_NEXT_STEP_CAP) : '',
1151
1153
  };
1152
1154
  }
1153
- /** Runs the PreCompact producer: saves a working-state snapshot into `report`. Never extracts memories; SessionEnd capture owns that. */
1154
- function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
1155
+ /** Runs the PreCompact producer: records the compaction, asks the summariser for memories, saves a working-state snapshot. Never extracts memories itself; SessionEnd capture owns that. */
1156
+ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
1155
1157
  // X3: the PreCompact hook fires in every Claude Code project, including
1156
1158
  // ones that never ran `hippo init`, so gate before any store-opening call
1157
1159
  // (saveActiveTaskSnapshot etc. call initStore, which would create one).
@@ -1175,6 +1177,8 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
1175
1177
  const manualInvocation = !stdinText || stdinText.trim() === '';
1176
1178
  let sessionId = null;
1177
1179
  let payloadTranscriptPath = null;
1180
+ let payloadCwd = null;
1181
+ let payloadTrigger = null;
1178
1182
  if (!manualInvocation) {
1179
1183
  let payload;
1180
1184
  try {
@@ -1193,8 +1197,9 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
1193
1197
  }
1194
1198
  if ('session_id' in payload && isStringValue(payload.session_id))
1195
1199
  sessionId = payload.session_id;
1196
- report.sessionId = sessionId;
1197
1200
  payloadTranscriptPath = payload.transcript_path;
1201
+ payloadCwd = 'cwd' in payload && isStringValue(payload.cwd) ? payload.cwd : null;
1202
+ payloadTrigger = 'trigger' in payload && isStringValue(payload.trigger) ? payload.trigger : null;
1198
1203
  }
1199
1204
  // X11: payload transcript_path must end .jsonl. No directory-containment
1200
1205
  // check is applied on top of this — CLAUDE_CONFIG_DIR can relocate the
@@ -1207,6 +1212,12 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
1207
1212
  appendPreCompactLog(logFile, `skip: payload transcript_path is not a .jsonl file: ${payloadTranscriptPath}`);
1208
1213
  return;
1209
1214
  }
1215
+ // The record is the "something saved before every compaction", so it lands even when no snapshot is derivable below.
1216
+ let recordId = null;
1217
+ if (sessionId !== null && sessionId !== '') {
1218
+ recordId = recordCompactionStart(hippoRoot, { sessionId, trigger: payloadTrigger, cwd: payloadCwd, transcriptPath: payloadTranscriptPath }, (message) => appendPreCompactLog(logFile, message));
1219
+ printPreCompactInstruction(logFile);
1220
+ }
1210
1221
  // A payload transcript_path is EXCLUSIVE: never fall back to
1211
1222
  // newest-transcript auto-discovery when it's missing/unreadable. That
1212
1223
  // fallback would snapshot a DIFFERENT session's transcript under THIS
@@ -1224,7 +1235,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
1224
1235
  }
1225
1236
  }
1226
1237
  else {
1227
- transcriptPath = resolveLastSessionTranscript(undefined, stdinText);
1238
+ transcriptPath = resolveLastSessionTranscript(undefined, stdinText, { mayScan: true });
1228
1239
  }
1229
1240
  if (!transcriptPath) {
1230
1241
  appendPreCompactLog(logFile, 'skip: no transcript resolved');
@@ -1275,7 +1286,8 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
1275
1286
  session_id: sessionId,
1276
1287
  });
1277
1288
  appendPreCompactLog(logFile, 'snapshot saved');
1278
- report.snapshotSaved = true;
1289
+ if (recordId !== null)
1290
+ recordSnapshotSaved(hippoRoot, recordId, (message) => appendPreCompactLog(logFile, message));
1279
1291
  }
1280
1292
  catch (err) {
1281
1293
  appendPreCompactLog(logFile, `snapshot save failed: ${errorMessage(err)}`);
@@ -1291,68 +1303,51 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
1291
1303
  */
1292
1304
  export async function cmdPreCompact(hippoRoot, options) {
1293
1305
  const logFile = options.logFile ?? defaultPreCompactLogPath();
1294
- const report = { snapshotSaved: false, sessionId: null };
1295
1306
  try {
1296
- runPreCompact(hippoRoot, options.stdinText, options.stdinTimedOut ?? false, logFile, report);
1307
+ runPreCompact(hippoRoot, options.stdinText, options.stdinTimedOut ?? false, logFile);
1297
1308
  }
1298
1309
  catch (err) {
1299
1310
  appendPreCompactLog(logFile, `pre-compact failed: ${errorMessage(err)}`);
1300
1311
  }
1301
- // Nothing goes to stdout: Claude Code passes PreCompact stdout to the
1302
- // summarising model as extra instructions. The PostCompact hook
1303
- // (`hippo post-compact`) tells the user instead, from this report.
1304
- if (report.snapshotSaved) {
1305
- try {
1306
- fs.writeFileSync(preCompactReportPath(logFile), JSON.stringify({ ...report, at: new Date().toISOString() }));
1307
- }
1308
- catch {
1309
- // Losing the message must never fail the hook.
1310
- }
1311
- }
1312
1312
  process.exit(0);
1313
1313
  }
1314
- /** How old a pre-compact report may be and still describe this compaction. */
1315
- const POST_COMPACT_REPORT_MAX_AGE_MS = 10 * 60_000;
1316
- /**
1317
- * The message for the PostCompact hook, from the report pre-compact left,
1318
- * or null when there is nothing to say. Consumes the report, so the message
1319
- * shows once. A report from another session, or an old one, is dropped.
1320
- * Never throws.
1321
- */
1322
- export function postCompactMessage(stdinText, logFile = defaultPreCompactLogPath(), now = new Date()) {
1323
- const reportFile = preCompactReportPath(logFile);
1324
- let raw;
1325
- try {
1326
- raw = fs.readFileSync(reportFile, 'utf8');
1327
- fs.rmSync(reportFile, { force: true });
1328
- }
1329
- catch {
1330
- return null;
1331
- }
1314
+ /** A PostCompact hook has 10 s in all; replay stops starting new records after this. */
1315
+ const POST_COMPACT_REPLAY_BUDGET_MS = 6000;
1316
+ /** PostCompact entry point: saves the summary and its items, replays earlier leftovers, returns the one line to show. Never throws, so the hook exits 0. */
1317
+ export function cmdPostCompact(hippoRoot, options) {
1318
+ const logFile = options.logFile ?? defaultPreCompactLogPath();
1319
+ const log = (message) => appendPreCompactLog(logFile, `post-compact: ${message}`);
1320
+ const deadline = Date.now() + POST_COMPACT_REPLAY_BUDGET_MS;
1332
1321
  try {
1333
- const report = JSON.parse(raw);
1334
- if (!isObjectLike(report))
1335
- return null;
1336
- const at = 'at' in report && isStringValue(report.at) ? Date.parse(report.at) : Number.NaN;
1337
- if (Number.isNaN(at) || now.getTime() - at > POST_COMPACT_REPORT_MAX_AGE_MS)
1322
+ if (!isInitialized(hippoRoot)) {
1323
+ log('skip: no hippo store');
1338
1324
  return null;
1339
- let payloadSession = null;
1340
- try {
1341
- const payload = JSON.parse((stdinText ?? '').trim() || 'null');
1342
- if (isObjectLike(payload) && 'session_id' in payload && isStringValue(payload.session_id))
1343
- payloadSession = payload.session_id;
1344
1325
  }
1345
- catch {
1346
- // No usable payload: fall back to the age check alone.
1326
+ const payload = parsePostCompactPayload(options.stdinText);
1327
+ let line = null;
1328
+ let storeBusy = false;
1329
+ if (payload === null) {
1330
+ log('skip: no PostCompact payload naming a session');
1347
1331
  }
1348
- const reportSession = 'sessionId' in report && isStringValue(report.sessionId) ? report.sessionId : null;
1349
- if (payloadSession !== null && reportSession !== null && payloadSession !== reportSession)
1350
- return null;
1351
- return preCompactMessage({
1352
- snapshotSaved: 'snapshotSaved' in report && report.snapshotSaved === true,
1353
- });
1332
+ else {
1333
+ const saved = saveCompaction(hippoRoot, payload, log);
1334
+ line = postCompactLine(saved);
1335
+ storeBusy = saved.deferred;
1336
+ if (!storeBusy && payload.transcriptPath !== null && options.afterSave) {
1337
+ try {
1338
+ options.afterSave(payload.transcriptPath, compactionOrigin(hippoRoot, payload.cwd), log);
1339
+ }
1340
+ catch (err) {
1341
+ log(`agent memory import failed: ${errorMessage(err)}`);
1342
+ }
1343
+ }
1344
+ }
1345
+ if (!storeBusy)
1346
+ replayCompactionsAt(hippoRoot, log, { busyWaitMs: COMPACTION_DB_WAIT_MS, deadline });
1347
+ return line;
1354
1348
  }
1355
- catch {
1349
+ catch (err) {
1350
+ log(`failed: ${errorMessage(err)}`);
1356
1351
  return null;
1357
1352
  }
1358
1353
  }
package/dist/cli.d.ts CHANGED
@@ -43,8 +43,6 @@ export declare function parseArgs(argv: string[]): {
43
43
  args: string[];
44
44
  flags: Record<string, string | boolean | string[]>;
45
45
  };
46
- /** Import new entries from the Claude Code auto memory of the store's project, the frontmatter .md files in ~/.claude/projects/<project>/memory/. */
47
- export declare function learnFromMemoryMd(hippoRoot: string, homeDir?: string): number;
48
46
  /**
49
47
  * Render an api.sleep result as console output, byte-identical to the
50
48
  * pre-extraction inline implementation in cmdSleepCore.