hippo-memory 1.29.0 → 1.31.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 (124) hide show
  1. package/README.md +13 -0
  2. package/dist/api.d.ts +49 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +78 -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.map +1 -1
  10. package/dist/capture.js +79 -23
  11. package/dist/capture.js.map +1 -1
  12. package/dist/cli.d.ts +3 -0
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +234 -13
  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/connectors/github/ingest.d.ts.map +1 -1
  21. package/dist/connectors/github/ingest.js +17 -0
  22. package/dist/connectors/github/ingest.js.map +1 -1
  23. package/dist/connectors/slack/ingest.d.ts.map +1 -1
  24. package/dist/connectors/slack/ingest.js +15 -0
  25. package/dist/connectors/slack/ingest.js.map +1 -1
  26. package/dist/consolidate.d.ts.map +1 -1
  27. package/dist/consolidate.js +623 -312
  28. package/dist/consolidate.js.map +1 -1
  29. package/dist/dag.d.ts +10 -0
  30. package/dist/dag.d.ts.map +1 -1
  31. package/dist/dag.js +32 -3
  32. package/dist/dag.js.map +1 -1
  33. package/dist/db.d.ts.map +1 -1
  34. package/dist/db.js +43 -1
  35. package/dist/db.js.map +1 -1
  36. package/dist/extract.d.ts.map +1 -1
  37. package/dist/extract.js +18 -1
  38. package/dist/extract.js.map +1 -1
  39. package/dist/importers.d.ts +10 -0
  40. package/dist/importers.d.ts.map +1 -1
  41. package/dist/importers.js +242 -159
  42. package/dist/importers.js.map +1 -1
  43. package/dist/mcp/server.d.ts.map +1 -1
  44. package/dist/mcp/server.js +41 -5
  45. package/dist/mcp/server.js.map +1 -1
  46. package/dist/memory-value-weights.d.ts +22 -0
  47. package/dist/memory-value-weights.d.ts.map +1 -0
  48. package/dist/memory-value-weights.js +31 -0
  49. package/dist/memory-value-weights.js.map +1 -0
  50. package/dist/memory-value.d.ts +123 -0
  51. package/dist/memory-value.d.ts.map +1 -0
  52. package/dist/memory-value.js +255 -0
  53. package/dist/memory-value.js.map +1 -0
  54. package/dist/reject-flow.d.ts +66 -0
  55. package/dist/reject-flow.d.ts.map +1 -0
  56. package/dist/reject-flow.js +207 -0
  57. package/dist/reject-flow.js.map +1 -0
  58. package/dist/rejection.d.ts +100 -0
  59. package/dist/rejection.d.ts.map +1 -0
  60. package/dist/rejection.js +157 -0
  61. package/dist/rejection.js.map +1 -0
  62. package/dist/server.d.ts.map +1 -1
  63. package/dist/server.js +5 -0
  64. package/dist/server.js.map +1 -1
  65. package/dist/shared.d.ts +9 -0
  66. package/dist/shared.d.ts.map +1 -1
  67. package/dist/shared.js +57 -6
  68. package/dist/shared.js.map +1 -1
  69. package/dist/sleep-redact.d.ts +2 -0
  70. package/dist/sleep-redact.d.ts.map +1 -1
  71. package/dist/sleep-redact.js +2 -0
  72. package/dist/sleep-redact.js.map +1 -1
  73. package/dist/src/api.js +78 -2
  74. package/dist/src/api.js.map +1 -1
  75. package/dist/src/audit.js.map +1 -1
  76. package/dist/src/capture.js +79 -23
  77. package/dist/src/capture.js.map +1 -1
  78. package/dist/src/cli.js +234 -13
  79. package/dist/src/cli.js.map +1 -1
  80. package/dist/src/config.js +21 -0
  81. package/dist/src/config.js.map +1 -1
  82. package/dist/src/connectors/github/ingest.js +17 -0
  83. package/dist/src/connectors/github/ingest.js.map +1 -1
  84. package/dist/src/connectors/slack/ingest.js +15 -0
  85. package/dist/src/connectors/slack/ingest.js.map +1 -1
  86. package/dist/src/consolidate.js +623 -312
  87. package/dist/src/consolidate.js.map +1 -1
  88. package/dist/src/dag.js +32 -3
  89. package/dist/src/dag.js.map +1 -1
  90. package/dist/src/db.js +43 -1
  91. package/dist/src/db.js.map +1 -1
  92. package/dist/src/extract.js +18 -1
  93. package/dist/src/extract.js.map +1 -1
  94. package/dist/src/importers.js +242 -159
  95. package/dist/src/importers.js.map +1 -1
  96. package/dist/src/mcp/server.js +41 -5
  97. package/dist/src/mcp/server.js.map +1 -1
  98. package/dist/src/memory-value-weights.js +31 -0
  99. package/dist/src/memory-value-weights.js.map +1 -0
  100. package/dist/src/memory-value.js +255 -0
  101. package/dist/src/memory-value.js.map +1 -0
  102. package/dist/src/reject-flow.js +207 -0
  103. package/dist/src/reject-flow.js.map +1 -0
  104. package/dist/src/rejection.js +157 -0
  105. package/dist/src/rejection.js.map +1 -0
  106. package/dist/src/server.js +5 -0
  107. package/dist/src/server.js.map +1 -1
  108. package/dist/src/shared.js +57 -6
  109. package/dist/src/shared.js.map +1 -1
  110. package/dist/src/sleep-redact.js +2 -0
  111. package/dist/src/sleep-redact.js.map +1 -1
  112. package/dist/src/store.js +498 -35
  113. package/dist/src/store.js.map +1 -1
  114. package/dist/src/version.js +1 -1
  115. package/dist/store.d.ts +107 -3
  116. package/dist/store.d.ts.map +1 -1
  117. package/dist/store.js +498 -35
  118. package/dist/store.js.map +1 -1
  119. package/dist/version.d.ts +1 -1
  120. package/dist/version.js +1 -1
  121. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  122. package/extensions/openclaw-plugin/package.json +1 -1
  123. package/openclaw.plugin.json +1 -1
  124. package/package.json +1 -1
@@ -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,66 @@
1
+ /**
2
+ * AT1 rejected-value tombstone — shared reject/unreject/list flow.
3
+ * docs/plans/2026-08-15-at1-rejected-value-tombstone.md (T2, plan §4).
4
+ *
5
+ * The CLI (`hippo reject`/`rejections`/`unreject`) and the Context-based
6
+ * `api.reject`/`api.unreject`/`api.listRejections` surfaces both need the
7
+ * SAME multi-step transaction + post-commit mirror-purge flow. Extracted
8
+ * here (leaf module) so neither duplicates it.
9
+ *
10
+ * Module direction: this file imports from store.ts, rejection.ts, and
11
+ * raw-archive.ts. Nothing imports FROM this file except cli.ts and api.ts,
12
+ * so it introduces no cycle.
13
+ */
14
+ import { type RejectedValueRow } from './rejection.js';
15
+ export interface RejectFlowOpts {
16
+ hippoRoot: string;
17
+ tenantId: string;
18
+ actor: string;
19
+ reason: string;
20
+ /** By-id form: reject the CURRENT content of an existing memory. */
21
+ memoryId?: string;
22
+ /** Pre-emptive form: reject a value not currently stored (or already gone). */
23
+ value?: string;
24
+ }
25
+ export interface RejectFlowResult {
26
+ digest: string;
27
+ /** The rejected content, for the CLI's at-reject-time echo (plan §2: the
28
+ * tombstone itself stores no content — this is the only place it's seen
29
+ * again after this call returns). */
30
+ content: string;
31
+ /** Every live row removed this call (all tenant rows whose normalized
32
+ * digest matched — not just the id passed, per the K1/R7 duplicate
33
+ * lesson). */
34
+ removedIds: string[];
35
+ /** Subset of removedIds that were kind='raw' (archived, not deleted). */
36
+ removedRawIds: string[];
37
+ }
38
+ /**
39
+ * `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
40
+ * insert the tombstone, enumerate + remove every live tenant row whose
41
+ * normalized digest matches (kind-aware), one aggregate `reject_value`
42
+ * audit, COMMIT. Then post-commit (mirrors the existing purge+reaper
43
+ * pattern verbatim from api.archiveRaw, api.ts:1913-1938): best-effort
44
+ * mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids, one
45
+ * index mirror rewrite.
46
+ */
47
+ export declare function rejectValue(opts: RejectFlowOpts): RejectFlowResult;
48
+ export type UnrejectOutcome = {
49
+ status: 'ok';
50
+ digest: string;
51
+ reason: string | null;
52
+ } | {
53
+ status: 'not_found';
54
+ } | {
55
+ status: 'ambiguous';
56
+ candidates: RejectedValueRow[];
57
+ };
58
+ /**
59
+ * `hippo unreject` / `api.unreject` — resolve a unique tombstone by digest
60
+ * (or prefix), delete it, audit `unreject_value`. The only v1 escape hatch
61
+ * (plan §4): no per-write force flag.
62
+ */
63
+ export declare function unrejectValue(hippoRoot: string, tenantId: string, digestOrPrefix: string, actor: string): UnrejectOutcome;
64
+ /** `hippo rejections` / `api.listRejections` — list tombstones for a tenant. */
65
+ export declare function listRejectionsForTenant(hippoRoot: string, tenantId: string): RejectedValueRow[];
66
+ //# sourceMappingURL=reject-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reject-flow.d.ts","sourceRoot":"","sources":["../src/reject-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,OAAO,EAML,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf;;0CAEsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB;;mBAEe;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,yEAAyE;IACzE,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,GAAG,gBAAgB,CAgIlE;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAC;AAE5D;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,MAAM,GACZ,eAAe,CAmCjB;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAQ/F"}
@@ -0,0 +1,207 @@
1
+ /**
2
+ * AT1 rejected-value tombstone — shared reject/unreject/list flow.
3
+ * docs/plans/2026-08-15-at1-rejected-value-tombstone.md (T2, plan §4).
4
+ *
5
+ * The CLI (`hippo reject`/`rejections`/`unreject`) and the Context-based
6
+ * `api.reject`/`api.unreject`/`api.listRejections` surfaces both need the
7
+ * SAME multi-step transaction + post-commit mirror-purge flow. Extracted
8
+ * here (leaf module) so neither duplicates it.
9
+ *
10
+ * Module direction: this file imports from store.ts, rejection.ts, and
11
+ * raw-archive.ts. Nothing imports FROM this file except cli.ts and api.ts,
12
+ * so it introduces no cycle.
13
+ */
14
+ import { openHippoDb, closeHippoDb } from './db.js';
15
+ import { appendAuditEvent } from './audit.js';
16
+ import { archiveRawMemory } from './raw-archive.js';
17
+ import { initStore, deleteEntryCore, purgeMirrorBestEffort, writeIndexMirror, buildIndexFromDb, } from './store.js';
18
+ import { rejectionDigest, normalizeValueForRejection, insertRejectedValue, deleteRejectedValue, listRejectedValues, } from './rejection.js';
19
+ /**
20
+ * `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
21
+ * insert the tombstone, enumerate + remove every live tenant row whose
22
+ * normalized digest matches (kind-aware), one aggregate `reject_value`
23
+ * audit, COMMIT. Then post-commit (mirrors the existing purge+reaper
24
+ * pattern verbatim from api.archiveRaw, api.ts:1913-1938): best-effort
25
+ * mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids, one
26
+ * index mirror rewrite.
27
+ */
28
+ export function rejectValue(opts) {
29
+ if (!opts.reason.trim()) {
30
+ throw new Error('reject requires a non-empty --reason (the tombstone stores no content; reason is its only identity).');
31
+ }
32
+ if (opts.memoryId === undefined && opts.value === undefined) {
33
+ throw new Error('reject requires either a memory id or --value.');
34
+ }
35
+ if (opts.memoryId !== undefined && opts.value !== undefined) {
36
+ // P2 fix: the CLI's flag parser already refuses both forms together;
37
+ // the shared flow itself didn't enforce it, so a direct api caller
38
+ // passing both silently got the memoryId path with `value` ignored —
39
+ // surprising for a caller who thought they were rejecting `value`.
40
+ throw new Error('reject accepts either a memory id or --value, not both.');
41
+ }
42
+ if (opts.value !== undefined && normalizeValueForRejection(opts.value).length === 0) {
43
+ // Direct api callers can pass strings the CLI flag parser would have
44
+ // refused; an empty-normalized tombstone would refuse nothing meaningful
45
+ // and pollute the listing.
46
+ throw new Error('reject --value requires non-empty content.');
47
+ }
48
+ initStore(opts.hippoRoot);
49
+ const db = openHippoDb(opts.hippoRoot);
50
+ try {
51
+ let content;
52
+ if (opts.memoryId !== undefined) {
53
+ const row = db
54
+ .prepare(`SELECT content, tenant_id FROM memories WHERE id = ?`)
55
+ .get(opts.memoryId);
56
+ if (!row || row.tenant_id !== opts.tenantId) {
57
+ throw new Error(`memory not found: ${opts.memoryId}`);
58
+ }
59
+ content = row.content;
60
+ }
61
+ else {
62
+ content = opts.value;
63
+ }
64
+ const digest = rejectionDigest(content);
65
+ const now = new Date().toISOString();
66
+ const removedIds = [];
67
+ const removedRawIds = [];
68
+ db.exec('BEGIN');
69
+ try {
70
+ insertRejectedValue(db, {
71
+ tenantId: opts.tenantId,
72
+ digest,
73
+ reason: opts.reason,
74
+ rejectedBy: opts.actor,
75
+ rejectedAt: now,
76
+ sourceMemoryId: opts.memoryId ?? null,
77
+ normalizedChars: normalizeValueForRejection(content).length,
78
+ });
79
+ // O(N) scan over the tenant's rows (plan §4): human-triggered command
80
+ // on ~1-5k-row stores — acceptable, documented. A digest column on
81
+ // memories is the escape if stores grow 100x; not needed now.
82
+ const rows = db
83
+ .prepare(`SELECT id, kind, content FROM memories WHERE tenant_id = ?`)
84
+ .all(opts.tenantId);
85
+ for (const row of rows) {
86
+ if (rejectionDigest(row.content) !== digest)
87
+ continue;
88
+ if (row.kind === 'raw') {
89
+ // Append-only trigger respected — archiveRawMemory is the only
90
+ // legitimate removal path for kind='raw', and its inner SAVEPOINT
91
+ // composes safely inside this BEGIN/COMMIT.
92
+ archiveRawMemory(db, row.id, { reason: opts.reason, who: opts.actor });
93
+ removedRawIds.push(row.id);
94
+ }
95
+ else {
96
+ // suppressForgetAudit: the aggregate reject_value row below is the
97
+ // trail for these removals, not N individual forget rows (plan
98
+ // §4, round-3 advisory 2 — mirrors api.ts:1873-1877).
99
+ deleteEntryCore(db, row.id, { actor: opts.actor, suppressForgetAudit: true });
100
+ }
101
+ removedIds.push(row.id);
102
+ }
103
+ try {
104
+ appendAuditEvent(db, {
105
+ tenantId: opts.tenantId,
106
+ actor: opts.actor,
107
+ op: 'reject_value',
108
+ targetId: opts.memoryId,
109
+ metadata: { digest, removedIds, count: removedIds.length },
110
+ });
111
+ }
112
+ catch {
113
+ // Best-effort — mirrors store.ts's private audit() semantics. This
114
+ // runs INSIDE the still-open transaction (COMMIT is the next
115
+ // statement): a swallowed audit failure lets the tombstone +
116
+ // removals commit without the trail row, rather than rolling the
117
+ // whole reject back over bookkeeping.
118
+ }
119
+ db.exec('COMMIT');
120
+ }
121
+ catch (err) {
122
+ try {
123
+ db.exec('ROLLBACK');
124
+ }
125
+ catch {
126
+ // already rolled back
127
+ }
128
+ throw err;
129
+ }
130
+ // Post-commit, db handle still open (same pattern as api.archiveRaw):
131
+ // best-effort mirror purge per removed id, reaper-backstop stamp for
132
+ // raw ids, one index mirror rewrite.
133
+ for (const id of removedIds) {
134
+ // AT1 fix: purgeMirrorBestEffort retries once, then — for non-raw ids,
135
+ // which cleanupArchivedMirrors' reaper never scans — reports the
136
+ // EXPLICIT leftover path(s) instead of the false "will retry via
137
+ // reaper" claim. See its own doc comment (store.ts, near
138
+ // removeEntryMirrors) for the full rationale.
139
+ const mirrorOk = purgeMirrorBestEffort(opts.hippoRoot, id, removedRawIds.includes(id), 'hippo reject');
140
+ if (mirrorOk && removedRawIds.includes(id)) {
141
+ db.prepare(`UPDATE raw_archive SET mirror_cleaned_at = ? WHERE memory_id = ?`).run(new Date().toISOString(), id);
142
+ }
143
+ }
144
+ if (removedIds.length > 0) {
145
+ writeIndexMirror(opts.hippoRoot, buildIndexFromDb(db));
146
+ }
147
+ return { digest, content, removedIds, removedRawIds };
148
+ }
149
+ finally {
150
+ closeHippoDb(db);
151
+ }
152
+ }
153
+ /**
154
+ * `hippo unreject` / `api.unreject` — resolve a unique tombstone by digest
155
+ * (or prefix), delete it, audit `unreject_value`. The only v1 escape hatch
156
+ * (plan §4): no per-write force flag.
157
+ */
158
+ export function unrejectValue(hippoRoot, tenantId, digestOrPrefix, actor) {
159
+ // P2 fix: an empty/blank prefix startsWith-matches EVERY digest (every
160
+ // string starts with ''), which would previously fall through to the
161
+ // ambiguous-candidates branch and list the whole tombstone set instead of
162
+ // failing loud on the actually-invalid input. Reject before the DB round
163
+ // trip.
164
+ if (digestOrPrefix.trim().length === 0) {
165
+ return { status: 'not_found' };
166
+ }
167
+ initStore(hippoRoot);
168
+ const db = openHippoDb(hippoRoot);
169
+ try {
170
+ const all = listRejectedValues(db, tenantId);
171
+ const matches = all.filter((r) => r.digest.startsWith(digestOrPrefix));
172
+ if (matches.length === 0)
173
+ return { status: 'not_found' };
174
+ if (matches.length > 1)
175
+ return { status: 'ambiguous', candidates: matches };
176
+ const target = matches[0];
177
+ deleteRejectedValue(db, tenantId, target.digest);
178
+ try {
179
+ appendAuditEvent(db, {
180
+ tenantId,
181
+ actor,
182
+ op: 'unreject_value',
183
+ targetId: target.sourceMemoryId ?? undefined,
184
+ metadata: { digest: target.digest, reason: target.reason },
185
+ });
186
+ }
187
+ catch {
188
+ // Best-effort, same as reject's audit call above.
189
+ }
190
+ return { status: 'ok', digest: target.digest, reason: target.reason };
191
+ }
192
+ finally {
193
+ closeHippoDb(db);
194
+ }
195
+ }
196
+ /** `hippo rejections` / `api.listRejections` — list tombstones for a tenant. */
197
+ export function listRejectionsForTenant(hippoRoot, tenantId) {
198
+ initStore(hippoRoot);
199
+ const db = openHippoDb(hippoRoot);
200
+ try {
201
+ return listRejectedValues(db, tenantId);
202
+ }
203
+ finally {
204
+ closeHippoDb(db);
205
+ }
206
+ }
207
+ //# sourceMappingURL=reject-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reject-flow.js","sourceRoot":"","sources":["../src/reject-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * AT1 rejected-value tombstone — core invariant.
3
+ * docs/plans/2026-08-15-at1-rejected-value-tombstone.md
4
+ *
5
+ * Exact-normalized-value semantics: a human who rejects a fact can refuse
6
+ * byte-stable re-ingestion of the same value across remember/capture/import/
7
+ * sync surfaces. Paraphrase/semantic matching is explicitly out of scope
8
+ * (documented limitation, plan "Design").
9
+ *
10
+ * Kept db-agnostic (helpers take a `DatabaseSyncLike` handle) and does NOT
11
+ * import from store.ts — store.ts imports from here, and the reverse would
12
+ * be a cycle.
13
+ */
14
+ import type { DatabaseSyncLike } from './db.js';
15
+ /**
16
+ * Normalize content for rejection-digest comparisons: Unicode NFC →
17
+ * lowercase → collapse whitespace runs to a single space → trim. No
18
+ * punctuation stripping — over-normalization creates false refusals, which
19
+ * are worse than misses (plan §1).
20
+ */
21
+ export declare function normalizeValueForRejection(content: string): string;
22
+ /**
23
+ * Full sha256 hex (64 chars) of the normalized content. Reuses the strong-
24
+ * identity convention (importers.ts:814 content-hash tag), NOT the privacy-
25
+ * lossy 16-char convention (recall-trace query hashing) — a tombstone lookup
26
+ * key needs collision resistance, not redaction (plan §1).
27
+ */
28
+ export declare function rejectionDigest(content: string): string;
29
+ /**
30
+ * Thrown by the write-path guard (checkRejectionGuard, called from
31
+ * upsertEntryRow) when an incoming write would introduce a value matching a
32
+ * tombstoned digest. Carries enough context for the transaction-owner catch
33
+ * blocks (writeEntry, api.supersede) to write a post-rollback
34
+ * `reject_refusal` audit row via `auditRejectionRefusal` (plan §3).
35
+ */
36
+ export declare class RejectedValueError extends Error {
37
+ readonly digest: string;
38
+ readonly tenantId: string;
39
+ readonly entryId: string;
40
+ readonly reason: string | null;
41
+ readonly rejectedAt: string;
42
+ constructor(opts: {
43
+ digest: string;
44
+ tenantId: string;
45
+ entryId: string;
46
+ reason: string | null;
47
+ rejectedAt: string;
48
+ });
49
+ }
50
+ export interface RejectedValueRow {
51
+ tenantId: string;
52
+ digest: string;
53
+ reason: string | null;
54
+ rejectedBy: string | null;
55
+ rejectedAt: string;
56
+ sourceMemoryId: string | null;
57
+ normalizedChars: number | null;
58
+ }
59
+ /**
60
+ * Look up a tombstone by tenant + digest. One indexed point query — the
61
+ * guard's common-case cost, a miss ends the guard (plan §3).
62
+ */
63
+ export declare function findRejectedValue(db: DatabaseSyncLike, tenantId: string, digest: string): RejectedValueRow | null;
64
+ /**
65
+ * Insert (or refresh) a tombstone row. Caller owns the transaction — used by
66
+ * the T2 `reject` verb and `resolveConflict`'s `rejectLoserValue` path.
67
+ */
68
+ export declare function insertRejectedValue(db: DatabaseSyncLike, opts: {
69
+ tenantId: string;
70
+ digest: string;
71
+ reason: string;
72
+ rejectedBy: string;
73
+ rejectedAt: string;
74
+ sourceMemoryId?: string | null;
75
+ normalizedChars: number;
76
+ }): void;
77
+ /**
78
+ * Delete a tombstone by tenant + exact digest — the T2 `unreject` verb, the
79
+ * only v1 escape hatch (plan §4).
80
+ */
81
+ export declare function deleteRejectedValue(db: DatabaseSyncLike, tenantId: string, digest: string): boolean;
82
+ /** List tombstones for a tenant, newest first — the T2 `rejections` verb. */
83
+ export declare function listRejectedValues(db: DatabaseSyncLike, tenantId: string): RejectedValueRow[];
84
+ /**
85
+ * The write-path guard's check helper, called from `upsertEntryRow`
86
+ * (store.ts). Fires when the incoming content's digest matches a tombstone
87
+ * AND the write *introduces* that content: the row is new, OR the stored
88
+ * row's content digest differs from the incoming one — an UPSERT editing a
89
+ * same-id row TO a rejected value is a content introduction and must be
90
+ * refused. Unchanged same-id re-persists (recall boost, decay, star toggle)
91
+ * are exempt by construction (plan §3).
92
+ *
93
+ * Ordering minimizes queries on the common (miss) path: (a) caller has
94
+ * already computed nothing yet — this does the digest + point lookup first;
95
+ * (b) a miss returns immediately (ONE indexed point query); (c) only on a
96
+ * tombstone hit does it SELECT the stored row's content to classify
97
+ * new-row vs content-introduction (+1 query, rare path).
98
+ */
99
+ export declare function checkRejectionGuard(db: DatabaseSyncLike, tenantId: string, entryId: string, content: string): void;
100
+ //# sourceMappingURL=rejection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rejection.d.ts","sourceRoot":"","sources":["../src/rejection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,IAAI,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;KACpB;CAYF;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAwBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,gBAAgB,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,gBAAgB,GAAG,IAAI,CAQzB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,gBAAgB,EACpB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,eAAe,EAAE,MAAM,CAAC;CACzB,GACA,IAAI,CAmBN;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAGnG;AAED,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAQ7F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,gBAAgB,EACpB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,IAAI,CAmCN"}