hippo-memory 1.61.0 → 1.62.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 (57) hide show
  1. package/README.md +28 -53
  2. package/dist/agent-memories/apply.d.ts +1 -1
  3. package/dist/agent-memories/claude-code.d.ts +3 -1
  4. package/dist/agent-memories/claude-code.js +53 -9
  5. package/dist/agent-memories/sync.d.ts +3 -3
  6. package/dist/agent-memories/sync.js +14 -6
  7. package/dist/agent-memories/types.d.ts +0 -2
  8. package/dist/api/assemble.js +55 -54
  9. package/dist/api/context-select.d.ts +49 -0
  10. package/dist/api/context-select.js +344 -0
  11. package/dist/api/context.d.ts +2 -2
  12. package/dist/api/context.js +195 -522
  13. package/dist/api/drill-down.js +36 -33
  14. package/dist/api/promote.js +55 -66
  15. package/dist/api/recall.js +303 -438
  16. package/dist/api/sleep.js +203 -218
  17. package/dist/capture/compact.d.ts +1 -1
  18. package/dist/capture/compact.js +2 -2
  19. package/dist/cli/briefs.js +324 -306
  20. package/dist/cli/context.js +44 -34
  21. package/dist/cli/continuity.js +283 -271
  22. package/dist/cli/curate.js +35 -34
  23. package/dist/cli/decisions.js +333 -333
  24. package/dist/cli/explain.js +66 -60
  25. package/dist/cli/maintenance.js +62 -51
  26. package/dist/cli/playbooks.js +387 -370
  27. package/dist/cli/projects.js +8 -5
  28. package/dist/cli/recall.js +28 -43
  29. package/dist/cli/remember.js +113 -70
  30. package/dist/cli/session-hooks.js +100 -90
  31. package/dist/cli/setup.js +267 -246
  32. package/dist/cli/status.js +73 -64
  33. package/dist/cli/transfer.js +85 -99
  34. package/dist/compaction-record.d.ts +0 -2
  35. package/dist/compaction-record.js +1 -1
  36. package/dist/customer-notes.js +77 -68
  37. package/dist/dag.js +222 -186
  38. package/dist/decisions.js +93 -76
  39. package/dist/doctor.js +11 -7
  40. package/dist/goals.js +99 -86
  41. package/dist/incidents.js +45 -38
  42. package/dist/policies.js +85 -68
  43. package/dist/processes.js +87 -71
  44. package/dist/project-briefs.js +135 -108
  45. package/dist/project-merge.d.ts +12 -4
  46. package/dist/project-merge.js +130 -45
  47. package/dist/shared.d.ts +9 -0
  48. package/dist/shared.js +10 -8
  49. package/dist/skills.js +81 -65
  50. package/dist/store/search-rows.d.ts +2 -2
  51. package/dist/store/search-rows.js +15 -9
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  55. package/extensions/openclaw-plugin/package.json +1 -1
  56. package/openclaw.plugin.json +1 -1
  57. package/package.json +1 -1
package/dist/api/sleep.js CHANGED
@@ -31,184 +31,10 @@ export async function sleep(ctx, opts = {}) {
31
31
  // v1.11.5: phase counters for the consolidate audit emit (in finally).
32
32
  // Accumulated as each phase completes so partial-failure paths still report
33
33
  // accurate "what got done before the failure" data.
34
- let consolidationCount = 0;
35
- let dedupCount = 0;
36
- let auditDeletedCount = 0;
37
- let ambientTotal = 0;
34
+ const counts = { consolidation: 0, dedup: 0, auditDeleted: 0, ambient: 0 };
38
35
  let phaseError = null;
39
- let graphSnapshotError = null;
40
- let result = null;
41
36
  try {
42
- // Snapshot dirty tenants BEFORE any memory-deleting phase (consolidate /
43
- // dedup / audit). The graph_extraction_queue rows are FK'd to mirror
44
- // memories with ON DELETE CASCADE, so a phase that deletes a queued mirror
45
- // (e.g. dedup removing a near-duplicate superseding decision) would drop the
46
- // tenant from a drain-time load and leave its graph stale (codex P1). The
47
- // MAX(id) watermark captured here stays valid: arrivals during sleep get a
48
- // higher id and remain pending.
49
- //
50
- // Fail-soft (codex P2): a queue-read failure here must NOT abort core sleep
51
- // (consolidation / dedup / audit run regardless). On failure, skip graph
52
- // refresh this sleep (recovered next sleep) and surface a detail once
53
- // `result` exists (Phase 6).
54
- let dirtyTenants = [];
55
- if (!dryRun) {
56
- try {
57
- dirtyTenants = phases.loadPendingExtractionTenants(ctx.hippoRoot);
58
- }
59
- catch (snapErr) {
60
- // SAFETY: this is a best-effort log message only; property access on
61
- // any JS value is safe (undefined if absent), preserving the existing
62
- // lenient formatting even when something non-Error was thrown.
63
- graphSnapshotError = snapErr.message;
64
- }
65
- }
66
- // Phase 1: Consolidation.
67
- const consolidateResult = await phases.consolidate(ctx.hippoRoot, { dryRun });
68
- consolidationCount = consolidateResult.semanticCreated + consolidateResult.merged;
69
- result = {
70
- active: consolidateResult.decayed,
71
- removed: consolidateResult.removed,
72
- mergedEpisodic: consolidateResult.merged,
73
- newSemantic: consolidateResult.semanticCreated,
74
- dryRun,
75
- details: consolidateResult.details,
76
- };
77
- // Set only when non-zero, so a store without dormant memories gets a
78
- // byte-identical result (HTTP /v1/sleep, the CLI render snapshot).
79
- if (consolidateResult.dormant > 0) {
80
- result.dormant = consolidateResult.dormant;
81
- }
82
- if (consolidateResult.dormantExpired > 0) {
83
- result.dormantExpired = consolidateResult.dormantExpired;
84
- }
85
- // Phase 2: Dedup (post-consolidate near-duplicate cleanup).
86
- const dedupResult = phases.deduplicateStore(ctx.hippoRoot, { dryRun, actor: ctx.actor.subject });
87
- dedupCount = dedupResult.removed;
88
- if (dedupResult.removed > 0) {
89
- const semDups = dedupResult.pairs.filter((p) => p.keptLayer === 'semantic' && p.removedLayer === 'semantic').length;
90
- const epiDups = dedupResult.pairs.filter((p) => p.keptLayer === 'episodic' && p.removedLayer === 'episodic').length;
91
- const crossDups = dedupResult.pairs.filter((p) => p.keptLayer !== p.removedLayer).length;
92
- result.deduped = {
93
- removed: dedupResult.removed,
94
- semDups,
95
- epiDups,
96
- crossDups,
97
- };
98
- }
99
- // Phase 3: Quality audit (remove junk, report warnings; a dry run skips rows earlier phases would remove).
100
- const planned = new Set(dryRun ? [...(consolidateResult.removedIds ?? []), ...dedupResult.pairs.map((p) => p.removed)] : []);
101
- const allEntries = phases.loadAllEntries(ctx.hippoRoot).filter((e) => !planned.has(e.id));
102
- const auditOut = phases.auditMemories(allEntries, memoriesBackingObjects(ctx.hippoRoot));
103
- if (auditOut.issues.length > 0) {
104
- const errors = auditOut.issues.filter((i) => i.severity === 'error');
105
- const warnings = auditOut.issues.filter((i) => i.severity === 'warning');
106
- let removed = 0;
107
- for (const issue of errors) {
108
- const reason = `sleep-audit: ${issue.reason}`;
109
- if (dryRun || phases.deleteEntry(ctx.hippoRoot, issue.memoryId, { actor: ctx.actor.subject, reason, automatic: true }))
110
- removed++;
111
- }
112
- auditDeletedCount = removed;
113
- if (removed > 0 || warnings.length > 0) {
114
- result.audit = {
115
- errorsRemoved: removed,
116
- warningCount: warnings.length,
117
- };
118
- }
119
- }
120
- if (dryRun)
121
- return result;
122
- // Phase 4: Auto-share high-transfer-score memories to global.
123
- if (!opts.noShare) {
124
- const sleepConfig = phases.loadConfig(ctx.hippoRoot);
125
- if (sleepConfig.autoShareOnSleep) {
126
- // v1.25.0: surface the secret-veto skip count (v39 follow-up #2) so
127
- // the veto is observable instead of silent.
128
- // AT1: rejectedSkipped is autoShare's sibling counter for candidates
129
- // the global store's rejection tombstone refused (threaded the same
130
- // way as secretSkipped just below).
131
- const autoShareStats = { secretSkipped: 0, rejectedSkipped: 0 };
132
- const shared = phases.autoShare(ctx.hippoRoot, { minScore: 0.6, stats: autoShareStats });
133
- if (shared.length > 0) {
134
- result.shared = shared.length;
135
- }
136
- if (autoShareStats.secretSkipped > 0) {
137
- result.secretSkipped = autoShareStats.secretSkipped;
138
- }
139
- if (autoShareStats.rejectedSkipped > 0) {
140
- result.rejectedSkipped = autoShareStats.rejectedSkipped;
141
- }
142
- }
143
- }
144
- // Phase 5: Post-sleep ambient state summary.
145
- const postSleepConfig = phases.loadConfig(ctx.hippoRoot);
146
- if (postSleepConfig.ambient.enabled) {
147
- const postSleepEntries = phases.loadAllEntries(ctx.hippoRoot).filter((e) => !e.superseded_by);
148
- if (postSleepEntries.length > 0) {
149
- result.ambient = phases.computeAmbientState(postSleepEntries);
150
- ambientTotal = result.ambient.totalMemories;
151
- }
152
- }
153
- // Phase 6: Graph extraction drain (E3 sleep enqueue-hook). Rebuild the
154
- // entity/relation graph for every tenant marked dirty (by markGraphDirty)
155
- // since the last sleep, so `recall --hops` + cross-object `references` edges
156
- // run on fresh data without a manual `hippo graph extract`. Fully
157
- // fault-isolated: the consolidation work above has already committed, so a
158
- // failure here must never abort sleep; a per-tenant extract failure leaves
159
- // that tenant's queue items pending for the next sleep. (Skipped under
160
- // dryRun via the early return above.)
161
- try {
162
- if (graphSnapshotError) {
163
- // The dirty-tenant snapshot failed (codex P2 fail-soft). Core sleep
164
- // already succeeded; surface the skipped graph refresh as a detail.
165
- result.details = [
166
- ...(result.details ?? []),
167
- `graph: dirty-tenant snapshot failed (skipped graph refresh): ${graphSnapshotError}`,
168
- ];
169
- }
170
- let gTenants = 0;
171
- let gEntities = 0;
172
- let gRelations = 0;
173
- // dirtyTenants was snapshotted before the memory-deleting phases above.
174
- for (const { tenantId, maxPendingId } of dirtyTenants) {
175
- try {
176
- const ext = phases.extractGraph(ctx.hippoRoot, tenantId);
177
- // Count the rebuild as soon as it succeeds — it happened regardless of
178
- // the drain-mark below.
179
- gTenants += 1;
180
- gEntities += ext.entities;
181
- gRelations += ext.relations;
182
- // Watermark drain: mark processed only items enqueued before this
183
- // rebuild started (id <= maxPendingId). Arrivals during the rebuild
184
- // keep pending status and are caught next sleep; rows whose mirror was
185
- // cascade-deleted earlier this sleep are already gone (no-op).
186
- markPendingProcessedUpTo(ctx.hippoRoot, tenantId, maxPendingId);
187
- }
188
- catch (tenantErr) {
189
- // SAFETY: this is a best-effort log message only; property access
190
- // on any JS value is safe (undefined if absent), preserving the
191
- // existing lenient formatting even when something non-Error was thrown.
192
- result.details = [
193
- ...(result.details ?? []),
194
- `graph: extract failed for a dirty tenant (left pending): ${tenantErr.message}`,
195
- ];
196
- }
197
- }
198
- if (gTenants > 0) {
199
- result.graph = { tenants: gTenants, entities: gEntities, relations: gRelations };
200
- }
201
- }
202
- catch (graphErr) {
203
- // SAFETY: this is a best-effort log message only; property access on
204
- // any JS value is safe (undefined if absent), preserving the existing
205
- // lenient formatting even when something non-Error was thrown.
206
- result.details = [
207
- ...(result.details ?? []),
208
- `graph: drain phase failed (skipped): ${graphErr.message}`,
209
- ];
210
- }
211
- return result;
37
+ return await runSleepPhases(ctx, opts, phases, counts);
212
38
  }
213
39
  catch (err) {
214
40
  // SAFETY: phaseError is read via phaseError.message / (phaseError !==
@@ -218,54 +44,213 @@ export async function sleep(ctx, opts = {}) {
218
44
  throw err;
219
45
  }
220
46
  finally {
221
- // v1.11.5: emit one 'consolidate' audit_log row per api.sleep invocation,
222
- // with phase counters in metadata. Closes the CLI/MCP parity gap that T6
223
- // fixed for cmdOutcome (Episode A follow-up). In finally so partial-failure
224
- // paths still emit; `partial: true` + errorMessage flag the failure.
225
- // Dedicated handle for this emit only (phase helpers above each open their
226
- // own handle via hippoRoot — SQLite single-writer makes parallel handles
227
- // safe for the read-heavy phases).
228
- //
229
- // TODO(v1.12.0 + A5 v2): the audit row is tagged with ctx.tenantId but
230
- // api.sleep is host-wide (cross-tenant dedup is intentional). When
231
- // /v1/sleep moves off loopback-only, either tag with a synthetic "host"
232
- // tenant or scope api.sleep per-tenant. Independent-review-critic flag,
233
- // v1.11.5 ship.
234
- //
235
- // Error preservation: if openHippoDb or appendAuditEvent throws here, we
236
- // do NOT let it replace the original phaseError (independent-review HIGH:
237
- // would mask the underlying consolidation failure). Audit emit failure
238
- // is logged to stderr but the original throw wins.
47
+ emitSleepAudit(ctx, opts, dryRun, counts, phaseError);
48
+ }
49
+ }
50
+ async function runSleepPhases(ctx, opts, phases, counts) {
51
+ const dryRun = Boolean(opts.dryRun);
52
+ const snapshot = snapshotDirtyTenants(ctx, phases, dryRun);
53
+ // Phase 1: Consolidation.
54
+ const consolidateResult = await phases.consolidate(ctx.hippoRoot, { dryRun });
55
+ counts.consolidation = consolidateResult.semanticCreated + consolidateResult.merged;
56
+ const result = sleepResultFrom(consolidateResult, dryRun);
57
+ // Phase 2: Dedup (post-consolidate near-duplicate cleanup).
58
+ const dedupResult = phases.deduplicateStore(ctx.hippoRoot, { dryRun, actor: ctx.actor.subject });
59
+ counts.dedup = dedupResult.removed;
60
+ if (dedupResult.removed > 0)
61
+ result.deduped = dedupSummary(dedupResult);
62
+ // Phase 3: Quality audit (remove junk, report warnings; a dry run skips rows earlier phases would remove).
63
+ counts.auditDeleted = runQualityAudit(ctx, phases, dryRun, consolidateResult, dedupResult, result);
64
+ if (dryRun)
65
+ return result;
66
+ // Phase 4: Auto-share high-transfer-score memories to global.
67
+ if (!opts.noShare)
68
+ shareOnSleep(ctx, phases, result);
69
+ // Phase 5: Post-sleep ambient state summary.
70
+ counts.ambient = summarizeAmbient(ctx, phases, result);
71
+ drainGraphQueue(ctx, phases, snapshot, result);
72
+ return result;
73
+ }
74
+ // Taken before any memory-deleting phase: queue rows cascade-delete with their mirror, so a later read would miss
75
+ // tenants dedup touched. Fail-soft: a failed read skips the graph refresh this sleep and never aborts core sleep.
76
+ function snapshotDirtyTenants(ctx, phases, dryRun) {
77
+ const snapshot = { dirtyTenants: [], error: null };
78
+ if (!dryRun) {
239
79
  try {
240
- const db = openHippoDb(ctx.hippoRoot);
80
+ snapshot.dirtyTenants = phases.loadPendingExtractionTenants(ctx.hippoRoot);
81
+ }
82
+ catch (snapErr) {
83
+ // SAFETY: this is a best-effort log message only; property access on
84
+ // any JS value is safe (undefined if absent), preserving the existing
85
+ // lenient formatting even when something non-Error was thrown.
86
+ snapshot.error = snapErr.message;
87
+ }
88
+ }
89
+ return snapshot;
90
+ }
91
+ function sleepResultFrom(consolidateResult, dryRun) {
92
+ const result = {
93
+ active: consolidateResult.decayed,
94
+ removed: consolidateResult.removed,
95
+ mergedEpisodic: consolidateResult.merged,
96
+ newSemantic: consolidateResult.semanticCreated,
97
+ dryRun,
98
+ details: consolidateResult.details,
99
+ };
100
+ // Set only when non-zero, so a store without dormant memories gets a
101
+ // byte-identical result (HTTP /v1/sleep, the CLI render snapshot).
102
+ if (consolidateResult.dormant > 0) {
103
+ result.dormant = consolidateResult.dormant;
104
+ }
105
+ if (consolidateResult.dormantExpired > 0) {
106
+ result.dormantExpired = consolidateResult.dormantExpired;
107
+ }
108
+ return result;
109
+ }
110
+ function dedupSummary(dedupResult) {
111
+ const semDups = dedupResult.pairs.filter((p) => p.keptLayer === 'semantic' && p.removedLayer === 'semantic').length;
112
+ const epiDups = dedupResult.pairs.filter((p) => p.keptLayer === 'episodic' && p.removedLayer === 'episodic').length;
113
+ const crossDups = dedupResult.pairs.filter((p) => p.keptLayer !== p.removedLayer).length;
114
+ return {
115
+ removed: dedupResult.removed,
116
+ semDups,
117
+ epiDups,
118
+ crossDups,
119
+ };
120
+ }
121
+ /** Returns how many audit errors were deleted, or would be under dryRun. */
122
+ function runQualityAudit(ctx, phases, dryRun, consolidateResult, dedupResult, result) {
123
+ const planned = new Set(dryRun ? [...(consolidateResult.removedIds ?? []), ...dedupResult.pairs.map((p) => p.removed)] : []);
124
+ const allEntries = phases.loadAllEntries(ctx.hippoRoot).filter((e) => !planned.has(e.id));
125
+ const auditOut = phases.auditMemories(allEntries, memoriesBackingObjects(ctx.hippoRoot));
126
+ if (auditOut.issues.length === 0)
127
+ return 0;
128
+ const errors = auditOut.issues.filter((i) => i.severity === 'error');
129
+ const warnings = auditOut.issues.filter((i) => i.severity === 'warning');
130
+ let removed = 0;
131
+ for (const issue of errors) {
132
+ const reason = `sleep-audit: ${issue.reason}`;
133
+ if (dryRun || phases.deleteEntry(ctx.hippoRoot, issue.memoryId, { actor: ctx.actor.subject, reason, automatic: true }))
134
+ removed++;
135
+ }
136
+ if (removed > 0 || warnings.length > 0) {
137
+ result.audit = {
138
+ errorsRemoved: removed,
139
+ warningCount: warnings.length,
140
+ };
141
+ }
142
+ return removed;
143
+ }
144
+ function shareOnSleep(ctx, phases, result) {
145
+ const sleepConfig = phases.loadConfig(ctx.hippoRoot);
146
+ if (!sleepConfig.autoShareOnSleep)
147
+ return;
148
+ // Both skip counters are surfaced so the secret veto and the rejection tombstone are observable, not silent.
149
+ const autoShareStats = { secretSkipped: 0, rejectedSkipped: 0 };
150
+ const shared = phases.autoShare(ctx.hippoRoot, { minScore: 0.6, stats: autoShareStats });
151
+ if (shared.length > 0) {
152
+ result.shared = shared.length;
153
+ }
154
+ if (autoShareStats.secretSkipped > 0) {
155
+ result.secretSkipped = autoShareStats.secretSkipped;
156
+ }
157
+ if (autoShareStats.rejectedSkipped > 0) {
158
+ result.rejectedSkipped = autoShareStats.rejectedSkipped;
159
+ }
160
+ }
161
+ /** Returns the ambient total the audit row reports: 0 when ambient is off or no current row is left. */
162
+ function summarizeAmbient(ctx, phases, result) {
163
+ const postSleepConfig = phases.loadConfig(ctx.hippoRoot);
164
+ if (!postSleepConfig.ambient.enabled)
165
+ return 0;
166
+ const postSleepEntries = phases.loadAllEntries(ctx.hippoRoot).filter((e) => !e.superseded_by);
167
+ if (postSleepEntries.length === 0)
168
+ return 0;
169
+ result.ambient = phases.computeAmbientState(postSleepEntries);
170
+ return result.ambient.totalMemories;
171
+ }
172
+ // Phase 6: rebuild the graph of every tenant marked dirty since the last sleep, so graph recall runs on fresh data.
173
+ // Fault-isolated: consolidation has already committed, so no failure here aborts sleep; a failed tenant stays pending.
174
+ function drainGraphQueue(ctx, phases, snapshot, result) {
175
+ try {
176
+ if (snapshot.error) {
177
+ // Core sleep already succeeded; surface the skipped graph refresh as a detail.
178
+ result.details = [
179
+ ...(result.details ?? []),
180
+ `graph: dirty-tenant snapshot failed (skipped graph refresh): ${snapshot.error}`,
181
+ ];
182
+ }
183
+ let gTenants = 0;
184
+ let gEntities = 0;
185
+ let gRelations = 0;
186
+ // dirtyTenants was snapshotted before the memory-deleting phases above.
187
+ for (const { tenantId, maxPendingId } of snapshot.dirtyTenants) {
241
188
  try {
242
- const sleepAuditMetadata = {
243
- consolidationCount,
244
- dedupCount,
245
- auditDeletedCount,
246
- ambientTotal,
247
- dryRun,
248
- noShare: opts.noShare ?? false,
249
- partial: phaseError !== null,
250
- triggeredByTenant: ctx.tenantId, // preserve for audit forensics
251
- };
252
- if (phaseError)
253
- sleepAuditMetadata.errorMessage = phaseError.message;
254
- appendAuditEvent(db, {
255
- tenantId: '__host__',
256
- actor: ctx.actor.subject,
257
- op: 'consolidate',
258
- metadata: { ...sleepAuditMetadata },
259
- });
189
+ const ext = phases.extractGraph(ctx.hippoRoot, tenantId);
190
+ // Count the rebuild as soon as it succeeds — it happened regardless of
191
+ // the drain-mark below.
192
+ gTenants += 1;
193
+ gEntities += ext.entities;
194
+ gRelations += ext.relations;
195
+ // Watermark drain: only items enqueued before this rebuild started are marked; later arrivals stay pending.
196
+ markPendingProcessedUpTo(ctx.hippoRoot, tenantId, maxPendingId);
260
197
  }
261
- finally {
262
- closeHippoDb(db);
198
+ catch (tenantErr) {
199
+ // SAFETY: this is a best-effort log message only; property access
200
+ // on any JS value is safe (undefined if absent), preserving the
201
+ // existing lenient formatting even when something non-Error was thrown.
202
+ result.details = [
203
+ ...(result.details ?? []),
204
+ `graph: extract failed for a dirty tenant (left pending): ${tenantErr.message}`,
205
+ ];
263
206
  }
264
207
  }
265
- catch (auditErr) {
266
- // Logged, never thrown: a second failure must not mask the original phaseError.
267
- reportAuditWriteFailure('consolidate', String(auditErr));
208
+ if (gTenants > 0) {
209
+ result.graph = { tenants: gTenants, entities: gEntities, relations: gRelations };
268
210
  }
269
211
  }
212
+ catch (graphErr) {
213
+ // SAFETY: this is a best-effort log message only; property access on
214
+ // any JS value is safe (undefined if absent), preserving the existing
215
+ // lenient formatting even when something non-Error was thrown.
216
+ result.details = [
217
+ ...(result.details ?? []),
218
+ `graph: drain phase failed (skipped): ${graphErr.message}`,
219
+ ];
220
+ }
221
+ }
222
+ // One 'consolidate' row per sleep, emitted from finally so a partial failure still reports what got done.
223
+ // Its own handle; an audit failure is logged and never replaces the original phase error.
224
+ function emitSleepAudit(ctx, opts, dryRun, counts, phaseError) {
225
+ try {
226
+ const db = openHippoDb(ctx.hippoRoot);
227
+ try {
228
+ const sleepAuditMetadata = {
229
+ consolidationCount: counts.consolidation,
230
+ dedupCount: counts.dedup,
231
+ auditDeletedCount: counts.auditDeleted,
232
+ ambientTotal: counts.ambient,
233
+ dryRun,
234
+ noShare: opts.noShare ?? false,
235
+ partial: phaseError !== null,
236
+ triggeredByTenant: ctx.tenantId, // preserve for audit forensics
237
+ };
238
+ if (phaseError)
239
+ sleepAuditMetadata.errorMessage = phaseError.message;
240
+ appendAuditEvent(db, {
241
+ tenantId: '__host__',
242
+ actor: ctx.actor.subject,
243
+ op: 'consolidate',
244
+ metadata: { ...sleepAuditMetadata },
245
+ });
246
+ }
247
+ finally {
248
+ closeHippoDb(db);
249
+ }
250
+ }
251
+ catch (auditErr) {
252
+ // Logged, never thrown: a second failure must not mask the original phaseError.
253
+ reportAuditWriteFailure('consolidate', String(auditErr));
254
+ }
270
255
  }
271
256
  //# sourceMappingURL=sleep.js.map
@@ -37,7 +37,7 @@ export declare function cmdPreCompact(hippoRoot: string, options: PreCompactOpti
37
37
  export interface PostCompactOptions {
38
38
  stdinText?: string;
39
39
  logFile?: string;
40
- afterSave?: (transcriptPath: string, originProject: string, log: (message: string) => void) => void;
40
+ afterSave?: (transcriptPath: string, cwd: string | null, log: (message: string) => void) => void;
41
41
  }
42
42
  /** 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. */
43
43
  export declare function cmdPostCompact(hippoRoot: string, options: PostCompactOptions): string | null;
@@ -2,7 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { isInitialized } from '../store/open.js';
4
4
  import { saveActiveTaskSnapshot, loadActiveTaskSnapshot } from '../store/sessions.js';
5
- import { PRE_COMPACT_INSTRUCTION, compactionOrigin, parsePostCompactPayload, postCompactLine, recordCompactionStart, recordSnapshotSaved, saveCompaction, replayCompactionsAt, COMPACTION_DB_WAIT_MS, } from '../compaction-record.js';
5
+ import { PRE_COMPACT_INSTRUCTION, parsePostCompactPayload, postCompactLine, recordCompactionStart, recordSnapshotSaved, saveCompaction, replayCompactionsAt, COMPACTION_DB_WAIT_MS, } from '../compaction-record.js';
6
6
  import { resolveTenantId } from '../tenant.js';
7
7
  import { defaultPreCompactLogPath } from '../hooks/shared.js';
8
8
  import { maskEmails, redactSecretsStrict } from '../secret-detect.js';
@@ -335,7 +335,7 @@ export function cmdPostCompact(hippoRoot, options) {
335
335
  storeBusy = saved.deferred;
336
336
  if (!storeBusy && payload.transcriptPath !== null && options.afterSave) {
337
337
  try {
338
- options.afterSave(payload.transcriptPath, compactionOrigin(hippoRoot, payload.cwd), log);
338
+ options.afterSave(payload.transcriptPath, payload.cwd, log);
339
339
  }
340
340
  catch (err) {
341
341
  log(`agent memory import failed: ${errorMessage(err)}`);