hippo-memory 1.28.0 → 1.30.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 (91) hide show
  1. package/README.md +15 -0
  2. package/dist/api.d.ts +25 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +101 -2
  5. package/dist/api.js.map +1 -1
  6. package/dist/audit.d.ts +1 -1
  7. package/dist/audit.d.ts.map +1 -1
  8. package/dist/audit.js.map +1 -1
  9. package/dist/capture.d.ts +49 -2
  10. package/dist/capture.d.ts.map +1 -1
  11. package/dist/capture.js +464 -1
  12. package/dist/capture.js.map +1 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +196 -3
  15. package/dist/cli.js.map +1 -1
  16. package/dist/config.d.ts +8 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +21 -0
  19. package/dist/config.js.map +1 -1
  20. package/dist/consolidate.d.ts.map +1 -1
  21. package/dist/consolidate.js +195 -22
  22. package/dist/consolidate.js.map +1 -1
  23. package/dist/db.d.ts.map +1 -1
  24. package/dist/db.js +75 -1
  25. package/dist/db.js.map +1 -1
  26. package/dist/hooks.d.ts +10 -0
  27. package/dist/hooks.d.ts.map +1 -1
  28. package/dist/hooks.js +64 -1
  29. package/dist/hooks.js.map +1 -1
  30. package/dist/mcp/server.d.ts.map +1 -1
  31. package/dist/mcp/server.js +14 -1
  32. package/dist/mcp/server.js.map +1 -1
  33. package/dist/memory-value-weights.d.ts +22 -0
  34. package/dist/memory-value-weights.d.ts.map +1 -0
  35. package/dist/memory-value-weights.js +31 -0
  36. package/dist/memory-value-weights.js.map +1 -0
  37. package/dist/memory-value.d.ts +123 -0
  38. package/dist/memory-value.d.ts.map +1 -0
  39. package/dist/memory-value.js +255 -0
  40. package/dist/memory-value.js.map +1 -0
  41. package/dist/recall-trace.d.ts +117 -0
  42. package/dist/recall-trace.d.ts.map +1 -0
  43. package/dist/recall-trace.js +179 -0
  44. package/dist/recall-trace.js.map +1 -0
  45. package/dist/secret-detect.d.ts +8 -0
  46. package/dist/secret-detect.d.ts.map +1 -1
  47. package/dist/secret-detect.js +25 -0
  48. package/dist/secret-detect.js.map +1 -1
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +1 -0
  51. package/dist/server.js.map +1 -1
  52. package/dist/src/api.js +101 -2
  53. package/dist/src/api.js.map +1 -1
  54. package/dist/src/audit.js.map +1 -1
  55. package/dist/src/capture.js +464 -1
  56. package/dist/src/capture.js.map +1 -1
  57. package/dist/src/cli.js +196 -3
  58. package/dist/src/cli.js.map +1 -1
  59. package/dist/src/config.js +21 -0
  60. package/dist/src/config.js.map +1 -1
  61. package/dist/src/consolidate.js +195 -22
  62. package/dist/src/consolidate.js.map +1 -1
  63. package/dist/src/db.js +75 -1
  64. package/dist/src/db.js.map +1 -1
  65. package/dist/src/hooks.js +64 -1
  66. package/dist/src/hooks.js.map +1 -1
  67. package/dist/src/mcp/server.js +14 -1
  68. package/dist/src/mcp/server.js.map +1 -1
  69. package/dist/src/memory-value-weights.js +31 -0
  70. package/dist/src/memory-value-weights.js.map +1 -0
  71. package/dist/src/memory-value.js +255 -0
  72. package/dist/src/memory-value.js.map +1 -0
  73. package/dist/src/recall-trace.js +179 -0
  74. package/dist/src/recall-trace.js.map +1 -0
  75. package/dist/src/secret-detect.js +25 -0
  76. package/dist/src/secret-detect.js.map +1 -1
  77. package/dist/src/server.js +1 -0
  78. package/dist/src/server.js.map +1 -1
  79. package/dist/src/store.js +55 -4
  80. package/dist/src/store.js.map +1 -1
  81. package/dist/src/version.js +1 -1
  82. package/dist/store.d.ts +15 -0
  83. package/dist/store.d.ts.map +1 -1
  84. package/dist/store.js +55 -4
  85. package/dist/store.js.map +1 -1
  86. package/dist/version.d.ts +1 -1
  87. package/dist/version.js +1 -1
  88. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  89. package/extensions/openclaw-plugin/package.json +1 -1
  90. package/openclaw.plugin.json +1 -1
  91. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-value-weights.js","sourceRoot":"","sources":["../../src/memory-value-weights.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,uEAAuE;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAAqC,MAAM,CAAC,MAAM,CAAC;IAClF,QAAQ,EAAE,CAAC,kBAAkB;IAC7B,cAAc,EAAE,mBAAmB;IACnC,QAAQ,EAAE,CAAC,mBAAmB;IAC9B,eAAe,EAAE,CAAC,kBAAkB;IACpC,gBAAgB,EAAE,kBAAkB;IACpC,gBAAgB,EAAE,mBAAmB;IACrC,aAAa,EAAE,kBAAkB;IACjC,cAAc,EAAE,CAAC,kBAAkB;CACpC,CAAC,CAAC;AAEH;yDACyD;AACzD,MAAM,CAAC,MAAM,sBAAsB,GACjC,kEAAkE,CAAC"}
@@ -0,0 +1,255 @@
1
+ /**
2
+ * LC2-E3 — learned memory-value scorer, wired into the sleep decay pass as a
3
+ * rescue-only veto (design D1/D2, docs/plans/2026-08-10-lc2-e3-mv-wiring.md).
4
+ *
5
+ * computeMvFeatures mirrors benchmarks/memory-value/extract.mjs's
6
+ * computeFeatures for the 8 live dims the E2 fitter optimized over
7
+ * (FIT_DIMS) — the only dims MEMORY_VALUE_WEIGHTS carries a weight for.
8
+ * Any future edit to either side must keep them byte-equivalent; the parity
9
+ * test in tests/memory-value-wiring.test.ts enforces this.
10
+ *
11
+ * scoreEntries mirrors benchmarks/memory-value/evaluate.mjs's per-store
12
+ * min-max normalization + weighted scorer (no additional orientation
13
+ * multiply — the frozen weights already encode sign/orientation).
14
+ *
15
+ * rescueSet implements D1's rescue-only semantics: a condemned entry is
16
+ * rescued iff its learned score ranks in the top 30% (RESCUE_BUDGET, the E2
17
+ * keep-budget operating point) of its own tenant's non-pinned candidate set
18
+ * (D2). Deletes(flag-on) subset Deletes(flag-off) by construction — this
19
+ * function can only ever shrink the condemned set, never grow it.
20
+ */
21
+ import { calculateStrength } from './memory.js';
22
+ import { compareEntryIdentity } from './compare.js';
23
+ import { MEMORY_VALUE_WEIGHTS, SOURCE_ARTIFACT_SHA256 } from './memory-value-weights.js';
24
+ /** The 8 live feature dims (FIT_DIMS) — canonical order for iteration. */
25
+ export const MV_FEATURE_NAMES = [
26
+ 'age_days',
27
+ 'half_life_days',
28
+ 'strength',
29
+ 'retrieval_count',
30
+ 'outcome_positive',
31
+ 'outcome_negative',
32
+ 'outcome_ratio',
33
+ 'content_length',
34
+ ];
35
+ /** The E2 keep-budget operating point (the only point with measured
36
+ * evidence) — a code constant tied to that evidence, not user-tunable. */
37
+ const RESCUE_BUDGET = 0.3;
38
+ /**
39
+ * Review-round F1 (small-tenant degeneracy): below this per-tenant
40
+ * non-pinned candidate-set size, a rank statistic is noise — E2's evidence
41
+ * says nothing about tiny scale — and the floor prevents immortal-entry
42
+ * convergence: keepN=ceil(0.3*N) guarantees >=1 rescue at N=1, so without a
43
+ * floor a condemned-only 1-entry tenant would be rescued every single sleep
44
+ * forever. A condemned-only tenant below the floor instead drains normally
45
+ * as entries are deleted: the surviving rescued subset only ever shrinks
46
+ * toward 0, never regrows past 10 to regain eligibility on its own. Code
47
+ * constant tied to that reasoning, not user-tunable (same posture as
48
+ * RESCUE_BUDGET).
49
+ */
50
+ const MIN_RESCUE_GROUP = 10;
51
+ /**
52
+ * Blind v1 feature dict for one MemoryEntry, restricted to the 8 dims the
53
+ * frozen weights carry (mirrors extract.mjs's computeFeatures).
54
+ *
55
+ * CRITICAL: `strength` is CLOCK-BASIS `calculateStrength(entry, now)` with
56
+ * NO DecayOptions — that is how the frozen weights' training features were
57
+ * computed (extract.mjs never passes decayOpts). Passing the production
58
+ * decay basis (config.decayBasis via consolidate.ts's decayOpts) into this
59
+ * feature would silently break parity with the frozen weight vector. This
60
+ * is an intentional divergence from the condemnation TRIGGER in
61
+ * consolidate.ts, which keeps using decayOpts as today — only the rescue
62
+ * FEATURE is clock-basis.
63
+ */
64
+ export function computeMvFeatures(entry, now) {
65
+ const ageDays = (now.getTime() - Date.parse(entry.created)) / (1000 * 60 * 60 * 24);
66
+ const pos = entry.outcome_positive ?? 0;
67
+ const neg = entry.outcome_negative ?? 0;
68
+ return {
69
+ age_days: ageDays,
70
+ half_life_days: entry.half_life_days,
71
+ strength: calculateStrength(entry, now), // clock-basis, never decayOpts — see doc comment above
72
+ retrieval_count: entry.retrieval_count,
73
+ outcome_positive: pos,
74
+ outcome_negative: neg,
75
+ outcome_ratio: (pos - neg) / (pos + neg + 1), // same formula as extract.mjs / calculateRewardFactor's ratio term
76
+ content_length: entry.content.length,
77
+ };
78
+ }
79
+ /**
80
+ * Throws if the weight constant is malformed: fewer/extra dims, a
81
+ * non-finite weight value, or a missing source digest. Flag-on + a broken
82
+ * constant must THROW, never silently behave as flag-off.
83
+ *
84
+ * Parameterized (defaults to the real frozen singleton) so the throw
85
+ * conditions are directly unit-testable without mutating the frozen
86
+ * MEMORY_VALUE_WEIGHTS export — mirrors fit.mjs's verifyFrozenWeights
87
+ * (see tests/memory-value-fit.test.ts's `describe('verifyFrozenWeights')`).
88
+ */
89
+ export function validateWeights(weights = MEMORY_VALUE_WEIGHTS, digest = SOURCE_ARTIFACT_SHA256) {
90
+ for (const key of MV_FEATURE_NAMES) {
91
+ const v = weights[key];
92
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
93
+ throw new Error(`memory-value: weights constant is malformed — "${key}" is not a finite number ` +
94
+ `(src/memory-value-weights.ts)`);
95
+ }
96
+ }
97
+ if (typeof digest !== 'string' || digest.length === 0) {
98
+ throw new Error('memory-value: weights constant is malformed — SOURCE_ARTIFACT_SHA256 digest missing (src/memory-value-weights.ts)');
99
+ }
100
+ }
101
+ /**
102
+ * Min-max normalize each of the 8 features over the given entry set
103
+ * (constant feature -> 0, matching evaluate.mjs), then score = dot(weights,
104
+ * normalized). The normalization context is exactly the entries passed in —
105
+ * callers control the bounded scope (D2: per-tenant, non-pinned).
106
+ *
107
+ * `weights` defaults to the real frozen singleton; parameterized (like
108
+ * validateWeights) so callers/tests can score against an explicit vector
109
+ * without touching the module singleton.
110
+ *
111
+ * Review-round F2 (non-finite features): Date.parse on a malformed `created`
112
+ * string yields NaN, and NaN would silently corrupt every OTHER entry's
113
+ * min-max in the same group. An entry with ANY non-finite computed feature
114
+ * is excluded from the normalization context entirely (its raw values never
115
+ * touch min/max) and always scores -Infinity — the lowest possible score, so
116
+ * it sorts to the bottom of its tenant deterministically and (via rescueSet's
117
+ * explicit finite-score guard below) can never be rescued. Conservative
118
+ * direction: deletes(flag-on) subset deletes(flag-off) still holds.
119
+ */
120
+ export function scoreEntries(entries, now, weights = MEMORY_VALUE_WEIGHTS) {
121
+ const raw = new Map();
122
+ for (const e of entries)
123
+ raw.set(e.id, computeMvFeatures(e, now));
124
+ const nonFiniteIds = new Set();
125
+ for (const e of entries) {
126
+ const nf = raw.get(e.id);
127
+ if (MV_FEATURE_NAMES.some((f) => !Number.isFinite(nf[f])))
128
+ nonFiniteIds.add(e.id);
129
+ }
130
+ const normContextEntries = entries.filter((e) => !nonFiniteIds.has(e.id));
131
+ const minMax = new Map();
132
+ for (const f of MV_FEATURE_NAMES) {
133
+ let min = Infinity;
134
+ let max = -Infinity;
135
+ for (const e of normContextEntries) {
136
+ const v = raw.get(e.id)[f];
137
+ if (v < min)
138
+ min = v;
139
+ if (v > max)
140
+ max = v;
141
+ }
142
+ minMax.set(f, { min, max });
143
+ }
144
+ const norm = (f, v) => {
145
+ const { min, max } = minMax.get(f);
146
+ return max === min ? 0 : (v - min) / (max - min);
147
+ };
148
+ const scores = new Map();
149
+ for (const e of entries) {
150
+ if (nonFiniteIds.has(e.id)) {
151
+ scores.set(e.id, -Infinity);
152
+ continue;
153
+ }
154
+ const nf = raw.get(e.id);
155
+ let sum = 0;
156
+ for (const f of MV_FEATURE_NAMES)
157
+ sum += weights[f] * norm(f, nf[f]);
158
+ scores.set(e.id, sum);
159
+ }
160
+ return scores;
161
+ }
162
+ /**
163
+ * Groups non-pinned entries by tenantId (D2), scores + ranks each tenant's
164
+ * group independently, and returns per-entry rank context for every
165
+ * non-pinned entry (not just condemned ones) — the shared basis for both
166
+ * rescueSet's rescue decision and consolidate.ts's audit-row rank context,
167
+ * so the two never compute the ranking differently.
168
+ *
169
+ * `weights`/`digest` default to the real frozen singleton — parameterized
170
+ * (like validateWeights/scoreEntries) purely for direct unit-testability of
171
+ * the fail-loud path, never overridden by production callers.
172
+ */
173
+ export function rankNonPinnedByTenant(entries, now, weights = MEMORY_VALUE_WEIGHTS, digest = SOURCE_ARTIFACT_SHA256) {
174
+ validateWeights(weights, digest);
175
+ const byTenant = new Map();
176
+ for (const e of entries) {
177
+ if (e.pinned)
178
+ continue; // D2: pinned entries never compete for rescue (never condemned)
179
+ // F9: guard undefined tenantId the same way dag.ts:341 does — the
180
+ // MemoryEntry type says `string`, but a raw/legacy row can still carry
181
+ // undefined at runtime, and grouping it under the literal key
182
+ // "undefined" would silently split it into its own singleton tenant.
183
+ const tenantId = e.tenantId ?? 'default';
184
+ const list = byTenant.get(tenantId);
185
+ if (list)
186
+ list.push(e);
187
+ else
188
+ byTenant.set(tenantId, [e]);
189
+ }
190
+ const result = new Map();
191
+ for (const [tenantId, group] of byTenant) {
192
+ const scores = scoreEntries(group, now, weights);
193
+ // score DESC -> compareEntryIdentity (content asc -> id asc), the shared
194
+ // deterministic tie-break used by every score-primary sort site in this
195
+ // codebase (src/compare.ts). F2: `-Infinity - -Infinity` is NaN, not 0 —
196
+ // two non-finite-feature entries tied at -Infinity would otherwise fall
197
+ // through to `diff` (NaN), which Array.sort treats as "no preference"
198
+ // and leaves insertion-order-dependent. Route NaN through the same
199
+ // deterministic tie-break as an exact-zero diff.
200
+ const sorted = [...group].sort((a, b) => {
201
+ const diff = scores.get(b.id) - scores.get(a.id);
202
+ return diff === 0 || Number.isNaN(diff) ? compareEntryIdentity(a, b) : diff;
203
+ });
204
+ // F1: tenants smaller than MIN_RESCUE_GROUP never rescue (keepN 0) — see
205
+ // that constant's doc comment.
206
+ const keepN = sorted.length < MIN_RESCUE_GROUP
207
+ ? 0
208
+ : Math.min(sorted.length, Math.ceil(RESCUE_BUDGET * sorted.length));
209
+ sorted.forEach((e, i) => {
210
+ result.set(e.id, {
211
+ tenantId,
212
+ score: scores.get(e.id),
213
+ rank: i + 1,
214
+ totalNonPinned: sorted.length,
215
+ keepN,
216
+ });
217
+ });
218
+ }
219
+ return result;
220
+ }
221
+ /**
222
+ * D1 rescue decision: a condemned entry is rescued iff it ranks in the top
223
+ * 30% of its tenant's non-pinned candidate set by learned score. Returns the
224
+ * subset of condemnedIds that are rescued — the caller filters commits
225
+ * (rescued -> survivors) and threads the same set into detectConflicts.
226
+ *
227
+ * `weights`/`digest` default to the real frozen singleton; production
228
+ * callers (consolidate.ts) never pass overrides — the params exist purely so
229
+ * "flag on + a broken constant throws" is directly testable end-to-end
230
+ * through this function without mutating the frozen module singleton.
231
+ *
232
+ * `precomputedRanks` (round-2 code-review P2-2): when the caller has already
233
+ * computed the per-tenant ranking (e.g. consolidate.ts needs it separately
234
+ * for detail/audit rank context), pass it here to skip the internal
235
+ * rankNonPinnedByTenant call — the whole-store ranking pass then runs
236
+ * exactly once per sleep instead of twice. Omitted (the default), rescueSet
237
+ * computes it internally as before — existing callers/tests are unaffected.
238
+ */
239
+ export function rescueSet(entries, condemnedIds, now, weights = MEMORY_VALUE_WEIGHTS, digest = SOURCE_ARTIFACT_SHA256, precomputedRanks) {
240
+ validateWeights(weights, digest); // fail loud before any rescue computation (constraint 5)
241
+ const ranked = precomputedRanks ?? rankNonPinnedByTenant(entries, now, weights, digest);
242
+ const rescued = new Set();
243
+ for (const id of condemnedIds) {
244
+ const info = ranked.get(id);
245
+ // F2: Number.isFinite(info.score) is an explicit, absolute guard — not
246
+ // just reliance on -Infinity naturally sorting last. In the degenerate
247
+ // case where every entry in a tenant is non-finite-scored (a tie at
248
+ // -Infinity), rank position alone could otherwise place one inside
249
+ // keepN; this makes "never rescued" hold regardless.
250
+ if (info && info.rank <= info.keepN && Number.isFinite(info.score))
251
+ rescued.add(id);
252
+ }
253
+ return rescued;
254
+ }
255
+ //# sourceMappingURL=memory-value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-value.js","sourceRoot":"","sources":["../../src/memory-value.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * LC1 — retrieval-trace persistence
3
+ * (docs/plans/2026-08-02-lc1-recall-trace-persistence.md).
4
+ *
5
+ * Single producer for the `recall_traces` / `recall_trace_results` /
6
+ * `recall_trace_outcomes` tables (schema v40). Every recall on the three
7
+ * wired paths (api.recall, api.getContext, CLI cmdRecall) writes a trace
8
+ * row: the ids + ranks + scores actually returned. Outcome events that
9
+ * resolve their targets from last-retrieval state link back to the trace
10
+ * they judge via `recordTraceOutcome`. This is the (query, shown, outcome)
11
+ * training triple every Track LC learned component needs.
12
+ *
13
+ * All writes here are fail-soft: a broken trace write must never break the
14
+ * surrounding recall or outcome call. Failures are logged to stderr and
15
+ * swallowed (matches the api.ts ~2843 "audit emit failed" precedent — no
16
+ * new debug env var).
17
+ */
18
+ import { createHash } from 'node:crypto';
19
+ import { openHippoDb, closeHippoDb } from './db.js';
20
+ /**
21
+ * Strip a RerankStep down to {stage, multiplier, scoreBefore, scoreAfter}
22
+ * before persisting (F3 privacy fix, codex cross-model finding). `note` is
23
+ * free-form human text — the CLI's goal-boost step embeds matched goal tag
24
+ * text there, so persisting it verbatim would leak raw user content into
25
+ * training data via `rerank_json`. Only the four structured fields survive;
26
+ * any other/future free-form field is dropped by construction (allowlist,
27
+ * not a denylist).
28
+ */
29
+ function sanitizeRerankSteps(steps) {
30
+ return steps.map((s) => ({
31
+ stage: s.stage,
32
+ multiplier: s.multiplier,
33
+ scoreBefore: s.scoreBefore,
34
+ scoreAfter: s.scoreAfter,
35
+ }));
36
+ }
37
+ /**
38
+ * Insert a `recall_traces` row + its `recall_trace_results` rows in ONE
39
+ * transaction, on the connection handed in. Fail-soft: never throws —
40
+ * logs to stderr and returns null on any failure.
41
+ *
42
+ * Connection policy (per the plan): api.recall calls this directly on its
43
+ * own already-open handle. api.getContext and CLI cmdRecall go through
44
+ * `writeRecallTraceAtRoot` instead, since their audit handles are already
45
+ * closed by the time tracing runs.
46
+ */
47
+ export function writeRecallTrace(db, input) {
48
+ try {
49
+ const queryHash = createHash('sha256').update(input.query).digest('hex').slice(0, 16);
50
+ const ts = new Date().toISOString();
51
+ db.exec('BEGIN');
52
+ try {
53
+ const insertTrace = db.prepare(`
54
+ INSERT INTO recall_traces (ts, tenant_id, session_id, pipeline, query_hash, query_length, result_count, explain_mode)
55
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
56
+ `);
57
+ const traceResult = insertTrace.run(ts, input.tenantId, input.sessionId ?? null, input.pipeline, queryHash, input.query.length, input.results.length, input.explainMode ? 1 : 0);
58
+ const traceId = Number(traceResult.lastInsertRowid);
59
+ const insertResult = db.prepare(`
60
+ INSERT INTO recall_trace_results (trace_id, tenant_id, memory_id, result_rank, score, rerank_json)
61
+ VALUES (?, ?, ?, ?, ?, ?)
62
+ `);
63
+ input.results.forEach((r, i) => {
64
+ insertResult.run(traceId, input.tenantId, r.memoryId, i + 1, r.score, r.rerankSteps && r.rerankSteps.length > 0 ? JSON.stringify(sanitizeRerankSteps(r.rerankSteps)) : null);
65
+ });
66
+ db.exec('COMMIT');
67
+ return traceId;
68
+ }
69
+ catch (error) {
70
+ db.exec('ROLLBACK');
71
+ throw error;
72
+ }
73
+ }
74
+ catch (error) {
75
+ // eslint-disable-next-line no-console
76
+ console.error(`[hippo] recall trace write failed: ${error.message}`);
77
+ return null;
78
+ }
79
+ }
80
+ /**
81
+ * Convenience wrapper: opens a fresh short-lived connection at `root`,
82
+ * writes the trace, and closes. Returns the new trace id, or null on any
83
+ * failure (fail-soft).
84
+ *
85
+ * Used at api.getContext and CLI cmdRecall — sites where the block's own
86
+ * convention is per-call handles (writeEntry, saveIndex) and the earlier
87
+ * audit handles are already closed. NOT used by api.recall, which must
88
+ * reuse the caller's open handle (v1.11.5 no-side-effects contract,
89
+ * tests/api-recall-no-side-effects.test.ts).
90
+ *
91
+ * F1 structural fix (replaces the earlier stamp-then-clear design): this
92
+ * function does NOT touch the `last_trace_id` meta key. Stamping lived here
93
+ * originally, on its own connection, separate from the `last_retrieval_ids`
94
+ * write in `saveIndex` — two connections meant two commits, so a crash or
95
+ * a failed second write could advance one without the other. LOCKSTEP
96
+ * INVARIANT: `last_trace_id` must only ever advance in the SAME write as
97
+ * `last_retrieval_ids`. The caller now does: call this function FIRST, set
98
+ * `localIndex.last_trace_id` from the returned id, THEN call `saveIndex`
99
+ * once — `saveIndex` persists both meta keys in one transaction
100
+ * (store.ts). Call sites that trace WITHOUT advancing `last_retrieval_ids`
101
+ * (CLI cmdRecall's zero-result path, getContext's empty-result path) simply
102
+ * never touch `localIndex` at all — they can't desync by construction.
103
+ *
104
+ * Fail-soft: never throws, including on connection failure.
105
+ */
106
+ export function writeRecallTraceAtRoot(root, input) {
107
+ let db;
108
+ try {
109
+ db = openHippoDb(root);
110
+ }
111
+ catch (error) {
112
+ // eslint-disable-next-line no-console
113
+ console.error(`[hippo] recall trace connection failed: ${error.message}`);
114
+ return null;
115
+ }
116
+ try {
117
+ return writeRecallTrace(db, input);
118
+ }
119
+ finally {
120
+ closeHippoDb(db);
121
+ }
122
+ }
123
+ /**
124
+ * Record an outcome event against a trace, linking the (query, shown,
125
+ * outcome) triple. Called ONLY where the credited ids actually come from
126
+ * the last-retrieval mechanism (api.outcomeForLastRecall and any outcome
127
+ * flow that resolves its targets from last-retrieval state) or from an
128
+ * SDK caller's explicit `traceId` opt — never unconditionally from
129
+ * api.outcome, which would mislink an explicit-id caller to a stale,
130
+ * unrelated trace.
131
+ *
132
+ * Lives in its own append-only table, not audit_log metadata: audit_log is
133
+ * pruned by `pruneAuditLog`, and pruning must never erase training data.
134
+ *
135
+ * F4 validation (codex cross-model finding): `traceId`/`memoryIds` reach
136
+ * this function from caller-side state (`last_trace_id` / applied outcome
137
+ * ids) that can go stale relative to the trace it names — a forgotten
138
+ * memory, a tenant switch mid-session, or a race between two callers. Two
139
+ * checks run before the insert, both skip silently (console.error one
140
+ * line) rather than throw:
141
+ * 1. The named trace must exist and belong to `input.tenantId` — a
142
+ * tenant mismatch or a dangling id (deleted trace) skips.
143
+ * 2. `input.memoryIds` is intersected against the trace's OWN
144
+ * `recall_trace_results.memory_id` set — only ids that trace actually
145
+ * returned are recorded. An id that was never in this trace's result
146
+ * set (stale caller state) is silently dropped rather than recorded
147
+ * as a false credit. If the intersection is empty, no row is written.
148
+ *
149
+ * Fail-soft: never throws.
150
+ */
151
+ export function recordTraceOutcome(db, input) {
152
+ try {
153
+ const trace = db.prepare(`SELECT tenant_id FROM recall_traces WHERE id = ?`).get(input.traceId);
154
+ if (!trace || trace.tenant_id !== input.tenantId) {
155
+ // eslint-disable-next-line no-console
156
+ console.error(`[hippo] recall trace outcome skipped: trace ${input.traceId} missing or tenant mismatch`);
157
+ return;
158
+ }
159
+ const memberRows = db
160
+ .prepare(`SELECT memory_id FROM recall_trace_results WHERE trace_id = ?`)
161
+ .all(input.traceId);
162
+ const members = new Set(memberRows.map((r) => r.memory_id));
163
+ const credited = input.memoryIds.filter((id) => members.has(id));
164
+ if (credited.length === 0) {
165
+ // eslint-disable-next-line no-console
166
+ console.error(`[hippo] recall trace outcome skipped: no credited ids intersect trace ${input.traceId}'s results`);
167
+ return;
168
+ }
169
+ db.prepare(`
170
+ INSERT INTO recall_trace_outcomes (trace_id, ts, tenant_id, outcome, memory_ids_json)
171
+ VALUES (?, ?, ?, ?, ?)
172
+ `).run(input.traceId, new Date().toISOString(), input.tenantId, input.outcome, JSON.stringify(credited));
173
+ }
174
+ catch (error) {
175
+ // eslint-disable-next-line no-console
176
+ console.error(`[hippo] recall trace outcome write failed: ${error.message}`);
177
+ }
178
+ }
179
+ //# sourceMappingURL=recall-trace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recall-trace.js","sourceRoot":"","sources":["../../src/recall-trace.ts"],"names":[],"mappings":"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"}
@@ -67,4 +67,29 @@ export function detectSecret(entry) {
67
67
  }
68
68
  return { flagged: false, reason: null };
69
69
  }
70
+ /**
71
+ * Replace secret-shaped substrings in free text with a redaction marker.
72
+ * Reuses the same `SECRET_PATTERNS` / co-occurrence guard as `detectSecret`
73
+ * (which only flags whole-entry content) so callers that must persist raw
74
+ * text that never passes through the normal capture content gate — e.g. the
75
+ * CS1 pre-compact snapshot fields — can scrub it in place instead.
76
+ */
77
+ export function redactSecrets(text) {
78
+ if (!text)
79
+ return text;
80
+ let result = text;
81
+ // PEM/OpenSSH blocks first: the pattern-table entry matches only the
82
+ // BEGIN delimiter, which is fine for detectSecret's flag-or-not decision
83
+ // but would leave the base64 payload behind here. Consume through the
84
+ // matching END delimiter; a truncated block with no END is redacted to
85
+ // the end of the text (codex round 3).
86
+ result = result.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----(?:[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----|[\s\S]*$)/g, '[REDACTED]');
87
+ for (const { name, re } of SECRET_PATTERNS) {
88
+ if (CO_OCCURRENCE_GUARDED.has(name) && !KEYISH_CONTEXT_RE.test(text))
89
+ continue;
90
+ const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
91
+ result = result.replace(new RegExp(re.source, flags), '[REDACTED]');
92
+ }
93
+ return result;
94
+ }
70
95
  //# sourceMappingURL=secret-detect.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"secret-detect.js","sourceRoot":"","sources":["../../src/secret-detect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAQH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC;IAC1B,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa;IAC1D,OAAO,EAAE,UAAU,EAAE,aAAa;CACnC,CAAC,CAAC;AAEH,MAAM,eAAe,GAAgD;IACnE,EAAE,IAAI,EAAE,gBAAgB,EAAE,EAAE,EAAE,sBAAsB,EAAE;IACtD,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,6CAA6C,EAAE;IAC3E,EAAE,IAAI,EAAE,yBAAyB,EAAE,EAAE,EAAE,kCAAkC,EAAE;IAC3E,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,EAAE,kCAAkC,EAAE;IAC/D,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,0CAA0C,EAAE;IACtE,EAAE,IAAI,EAAE,gBAAgB,EAAE,EAAE,EAAE,2BAA2B,EAAE;IAC3D,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,EAAE,oCAAoC,EAAE;IACvE,2EAA2E;IAC3E,yEAAyE;IACzE,yEAAyE;IACzE,uEAAuE;IACvE,QAAQ;IACR,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,2BAA2B,EAAE;IACzD,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,EAAE,sCAAsC,EAAE;IACzE,0EAA0E;IAC1E,oEAAoE;IACpE,wEAAwE;IACxE,oEAAoE;IACpE,wEAAwE;IACxE,yEAAyE;IACzE,kEAAkE;IAClE,mCAAmC;IACnC,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,EAAE,uGAAuG,EAAE;CAC3I,CAAC;AAEF,MAAM,iBAAiB,GAAG,mDAAmD,CAAC;AAC9E,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAE7E;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAA0C;IACrE,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAG,CAAC,WAAW,EAAE,EAAE,EAAE,CAAC;QAC/D,CAAC;IACH,CAAC;IACD,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,eAAe,EAAE,CAAC;QAC3C,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,SAAS;QACtC,IAAI,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,SAAS;QACxF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,IAAI,EAAE,EAAE,CAAC;IACtD,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1C,CAAC"}
1
+ {"version":3,"file":"secret-detect.js","sourceRoot":"","sources":["../../src/secret-detect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAQH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC;IAC1B,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa;IAC1D,OAAO,EAAE,UAAU,EAAE,aAAa;CACnC,CAAC,CAAC;AAEH,MAAM,eAAe,GAAgD;IACnE,EAAE,IAAI,EAAE,gBAAgB,EAAE,EAAE,EAAE,sBAAsB,EAAE;IACtD,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,6CAA6C,EAAE;IAC3E,EAAE,IAAI,EAAE,yBAAyB,EAAE,EAAE,EAAE,kCAAkC,EAAE;IAC3E,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,EAAE,kCAAkC,EAAE;IAC/D,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,0CAA0C,EAAE;IACtE,EAAE,IAAI,EAAE,gBAAgB,EAAE,EAAE,EAAE,2BAA2B,EAAE;IAC3D,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,EAAE,oCAAoC,EAAE;IACvE,2EAA2E;IAC3E,yEAAyE;IACzE,yEAAyE;IACzE,uEAAuE;IACvE,QAAQ;IACR,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,2BAA2B,EAAE;IACzD,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,EAAE,sCAAsC,EAAE;IACzE,0EAA0E;IAC1E,oEAAoE;IACpE,wEAAwE;IACxE,oEAAoE;IACpE,wEAAwE;IACxE,yEAAyE;IACzE,kEAAkE;IAClE,mCAAmC;IACnC,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,EAAE,uGAAuG,EAAE;CAC3I,CAAC;AAEF,MAAM,iBAAiB,GAAG,mDAAmD,CAAC;AAC9E,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAE7E;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAA0C;IACrE,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAG,CAAC,WAAW,EAAE,EAAE,EAAE,CAAC;QAC/D,CAAC;IACH,CAAC;IACD,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,eAAe,EAAE,CAAC;QAC3C,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,SAAS;QACtC,IAAI,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,SAAS;QACxF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,IAAI,EAAE,EAAE,CAAC;IACtD,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,qEAAqE;IACrE,yEAAyE;IACzE,sEAAsE;IACtE,uEAAuE;IACvE,uCAAuC;IACvC,MAAM,GAAG,MAAM,CAAC,OAAO,CACrB,0FAA0F,EAC1F,YAAY,CACb,CAAC;IACF,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,eAAe,EAAE,CAAC;QAC3C,IAAI,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QAC/E,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,GAAG,CAAC;QACjE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -108,6 +108,7 @@ const VALID_AUDIT_OPS = new Set([
108
108
  'customer_note_create', // E2 customer_note first-class object — emitted by saveCustomerNote
109
109
  'customer_note_supersede', // E2 — emitted by saveCustomerNote on a supersession
110
110
  'customer_note_close', // E2 — emitted by closeCustomerNote
111
+ 'mv_rescue', // LC2-E3 — emitted by consolidate() per rescue; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
111
112
  ]);
112
113
  // Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
113
114
  // enough to dump a small deployment's full audit log without paginating, but