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,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,157 @@
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 { createHash } from 'node:crypto';
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 function normalizeValueForRejection(content) {
22
+ return content.normalize('NFC').toLowerCase().replace(/\s+/g, ' ').trim();
23
+ }
24
+ /**
25
+ * Full sha256 hex (64 chars) of the normalized content. Reuses the strong-
26
+ * identity convention (importers.ts:814 content-hash tag), NOT the privacy-
27
+ * lossy 16-char convention (recall-trace query hashing) — a tombstone lookup
28
+ * key needs collision resistance, not redaction (plan §1).
29
+ */
30
+ export function rejectionDigest(content) {
31
+ return createHash('sha256').update(normalizeValueForRejection(content)).digest('hex');
32
+ }
33
+ /**
34
+ * Thrown by the write-path guard (checkRejectionGuard, called from
35
+ * upsertEntryRow) when an incoming write would introduce a value matching a
36
+ * tombstoned digest. Carries enough context for the transaction-owner catch
37
+ * blocks (writeEntry, api.supersede) to write a post-rollback
38
+ * `reject_refusal` audit row via `auditRejectionRefusal` (plan §3).
39
+ */
40
+ export class RejectedValueError extends Error {
41
+ digest;
42
+ tenantId;
43
+ entryId;
44
+ reason;
45
+ rejectedAt;
46
+ constructor(opts) {
47
+ super(`Memory value refused: matches a rejected value (digest ${opts.digest.slice(0, 12)}..., ` +
48
+ `reason: ${opts.reason ?? 'none given'}). Run "hippo unreject" to allow it again.`);
49
+ this.name = 'RejectedValueError';
50
+ this.digest = opts.digest;
51
+ this.tenantId = opts.tenantId;
52
+ this.entryId = opts.entryId;
53
+ this.reason = opts.reason;
54
+ this.rejectedAt = opts.rejectedAt;
55
+ }
56
+ }
57
+ function rowToRejectedValue(row) {
58
+ return {
59
+ tenantId: row.tenant_id,
60
+ digest: row.digest,
61
+ reason: row.reason,
62
+ rejectedBy: row.rejected_by,
63
+ rejectedAt: row.rejected_at,
64
+ sourceMemoryId: row.source_memory_id,
65
+ normalizedChars: row.normalized_chars,
66
+ };
67
+ }
68
+ /**
69
+ * Look up a tombstone by tenant + digest. One indexed point query — the
70
+ * guard's common-case cost, a miss ends the guard (plan §3).
71
+ */
72
+ export function findRejectedValue(db, tenantId, digest) {
73
+ const row = db
74
+ .prepare(`SELECT tenant_id, digest, reason, rejected_by, rejected_at, source_memory_id, normalized_chars
75
+ FROM rejected_values WHERE tenant_id = ? AND digest = ?`)
76
+ .get(tenantId, digest);
77
+ return row ? rowToRejectedValue(row) : null;
78
+ }
79
+ /**
80
+ * Insert (or refresh) a tombstone row. Caller owns the transaction — used by
81
+ * the T2 `reject` verb and `resolveConflict`'s `rejectLoserValue` path.
82
+ */
83
+ export function insertRejectedValue(db, opts) {
84
+ db.prepare(`INSERT INTO rejected_values(tenant_id, digest, reason, rejected_by, rejected_at, source_memory_id, normalized_chars)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?)
86
+ ON CONFLICT(tenant_id, digest) DO UPDATE SET
87
+ reason = excluded.reason,
88
+ rejected_by = excluded.rejected_by,
89
+ rejected_at = excluded.rejected_at,
90
+ source_memory_id = excluded.source_memory_id,
91
+ normalized_chars = excluded.normalized_chars`).run(opts.tenantId, opts.digest, opts.reason, opts.rejectedBy, opts.rejectedAt, opts.sourceMemoryId ?? null, opts.normalizedChars);
92
+ }
93
+ /**
94
+ * Delete a tombstone by tenant + exact digest — the T2 `unreject` verb, the
95
+ * only v1 escape hatch (plan §4).
96
+ */
97
+ export function deleteRejectedValue(db, tenantId, digest) {
98
+ const result = db.prepare(`DELETE FROM rejected_values WHERE tenant_id = ? AND digest = ?`).run(tenantId, digest);
99
+ return (result.changes ?? 0) > 0;
100
+ }
101
+ /** List tombstones for a tenant, newest first — the T2 `rejections` verb. */
102
+ export function listRejectedValues(db, tenantId) {
103
+ const rows = db
104
+ .prepare(`SELECT tenant_id, digest, reason, rejected_by, rejected_at, source_memory_id, normalized_chars
105
+ FROM rejected_values WHERE tenant_id = ? ORDER BY rejected_at DESC, digest ASC`)
106
+ .all(tenantId);
107
+ return rows.map(rowToRejectedValue);
108
+ }
109
+ /**
110
+ * The write-path guard's check helper, called from `upsertEntryRow`
111
+ * (store.ts). Fires when the incoming content's digest matches a tombstone
112
+ * AND the write *introduces* that content: the row is new, OR the stored
113
+ * row's content digest differs from the incoming one — an UPSERT editing a
114
+ * same-id row TO a rejected value is a content introduction and must be
115
+ * refused. Unchanged same-id re-persists (recall boost, decay, star toggle)
116
+ * are exempt by construction (plan §3).
117
+ *
118
+ * Ordering minimizes queries on the common (miss) path: (a) caller has
119
+ * already computed nothing yet — this does the digest + point lookup first;
120
+ * (b) a miss returns immediately (ONE indexed point query); (c) only on a
121
+ * tombstone hit does it SELECT the stored row's content to classify
122
+ * new-row vs content-introduction (+1 query, rare path).
123
+ */
124
+ export function checkRejectionGuard(db, tenantId, entryId, content) {
125
+ const incomingDigest = rejectionDigest(content);
126
+ const tombstone = findRejectedValue(db, tenantId, incomingDigest);
127
+ if (!tombstone)
128
+ return; // miss ends the guard — the overwhelmingly common case
129
+ // Deliberately id-only (no tenant filter): memory ids are globally unique
130
+ // ULIDs, this lookup only classifies new-row vs same-id re-persist for the
131
+ // id the caller is already writing, and the tombstone lookup above is the
132
+ // tenant-scoped decision. Matches deleteEntry's own by-id SELECT.
133
+ //
134
+ // P2 fix: also read tenant_id. Content-digest-only comparison let a
135
+ // same-id upsert that ONLY changes tenantId slip through as an "unchanged
136
+ // re-persist" — content C sitting quietly (never rejected) in tenant A
137
+ // could be re-tagged into tenant B, and since C's digest already matched
138
+ // this row's stored digest, the guard exempted it even though B is the
139
+ // tenant that rejected C (that is WHY `tombstone` above is non-null: the
140
+ // lookup already ran under the INCOMING/destination tenantId). A tenant
141
+ // change on the SAME id is therefore always a content introduction into
142
+ // the destination tenant, exactly as if the row were new there.
143
+ const storedRow = db.prepare(`SELECT content, tenant_id FROM memories WHERE id = ?`).get(entryId);
144
+ const isNewRow = storedRow === undefined;
145
+ const tenantChanged = !isNewRow && storedRow.tenant_id !== tenantId;
146
+ const isContentIntroduction = isNewRow || tenantChanged || rejectionDigest(storedRow.content) !== incomingDigest;
147
+ if (!isContentIntroduction)
148
+ return; // unchanged same-id re-persist — exempt by construction
149
+ throw new RejectedValueError({
150
+ digest: incomingDigest,
151
+ tenantId,
152
+ entryId,
153
+ reason: tombstone.reason,
154
+ rejectedAt: tombstone.rejectedAt,
155
+ });
156
+ }
157
+ //# sourceMappingURL=rejection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rejection.js","sourceRoot":"","sources":["../../src/rejection.ts"],"names":[],"mappings":"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"}
@@ -108,6 +108,11 @@ 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
112
+ 'reject_value', // AT1 — emitted by `hippo reject`; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
113
+ 'reject_refusal', // AT1 — emitted when the rejection guard refuses a write; lockstep
114
+ 'unreject_value', // AT1 — emitted by `hippo unreject`; lockstep
115
+ 'conflict_resolve', // AT1 — emitted by resolveConflict on every resolution path; lockstep
111
116
  ]);
112
117
  // Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
113
118
  // enough to dump a small deployment's full audit log without paginating, but