@agent-custody/state 0.1.8 → 0.2.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.
package/dist/ledger.js CHANGED
@@ -1,4 +1,4 @@
1
- // The fact ledger: an append-only JSONL log of events about what an agent believes.
1
+ // The fact ledger: an append-only log of events about what an agent believes, in a JSONL file, SQLite, or Postgres.
2
2
  // Bitemporal. Valid time is when a fact was true in the world; transaction time is when the ledger learned of it.
3
3
  // Nothing is ever edited in place. Correcting a belief is a new event, so "what did the agent believe at T" is always answerable.
4
4
  import { randomUUID } from "node:crypto";
@@ -6,19 +6,19 @@ import { createHash, createHmac } from "node:crypto";
6
6
  import { openStore } from "./storage.js";
7
7
  const RANK = { claimed: 0, attested: 1, verified: 2 };
8
8
  export class Ledger {
9
- events;
10
9
  store;
11
10
  now;
12
11
  forgetKey;
13
12
  /**
14
- * `location` is a path: JSONL by default, SQLite when it ends in .sqlite or .db; or pass a store.
15
- * forgetKey: a secret kept outside the store; with it, forgotten values leave an HMAC rather than a plain hash.
13
+ * `location` is a path (JSONL by default, SQLite when it ends in .sqlite or .db) or a postgres:// URL; or pass a
14
+ * store. The ledger holds no events itself: every question is a query to the store, so a shared store means a
15
+ * shared ledger. forgetKey: a secret kept outside the store; with it, forgotten values leave an HMAC rather than a
16
+ * plain hash.
16
17
  */
17
18
  constructor(location, opts = {}) {
18
19
  this.store = typeof location === "string" ? openStore(location) : location;
19
20
  this.now = opts.now ?? (() => new Date());
20
21
  this.forgetKey = opts.forgetKey ? Buffer.from(opts.forgetKey) : null;
21
- this.events = this.store.load();
22
22
  }
23
23
  /** Where the events live, for reports. */
24
24
  get location() {
@@ -26,31 +26,35 @@ export class Ledger {
26
26
  }
27
27
  /** Every event in order, for export. */
28
28
  export() {
29
- return [...this.events];
29
+ return this.store.events();
30
30
  }
31
31
  close() {
32
- this.store.close();
32
+ return this.store.close();
33
33
  }
34
- get size() {
35
- return this.events.length;
34
+ count() {
35
+ return this.store.count();
36
+ }
37
+ /** Every space with at least one fact. */
38
+ spaces() {
39
+ return this.store.spaces();
36
40
  }
37
41
  /** The checks assert makes, without appending. For callers that must do something irreversible before the append. */
38
- validateAssert(input) {
42
+ async validateAssert(input) {
39
43
  const validFrom = input.validFrom ?? this.now().toISOString();
40
44
  if (input.supersedes !== undefined) {
41
- const prior = this.factById(input.supersedes);
45
+ const prior = await this.factById(input.supersedes);
42
46
  if (!prior)
43
47
  throw new Error(`cannot supersede unknown fact ${input.supersedes}`);
44
48
  if (prior.fact.validTo !== null)
45
49
  throw new Error(`fact ${input.supersedes} is already superseded`);
46
- if (this.retractedAt(input.supersedes))
50
+ if (await this.retractedAt(input.supersedes))
47
51
  throw new Error(`fact ${input.supersedes} is retracted`);
48
52
  if (validFrom < prior.fact.validFrom)
49
53
  throw new Error(`replacement cannot start before the fact it supersedes`);
50
54
  }
51
55
  }
52
- assert(input) {
53
- this.validateAssert(input);
56
+ async assert(input) {
57
+ await this.validateAssert(input);
54
58
  const txTime = this.now().toISOString();
55
59
  const validFrom = input.validFrom ?? txTime;
56
60
  const event = {
@@ -73,13 +77,13 @@ export class Ledger {
73
77
  },
74
78
  supersedes: input.supersedes ?? null,
75
79
  };
76
- this.append(event);
80
+ await this.store.append(event);
77
81
  return event;
78
82
  }
79
- retract(input) {
80
- if (!this.factById(input.factId))
83
+ async retract(input) {
84
+ if (!(await this.factById(input.factId)))
81
85
  throw new Error(`cannot retract unknown fact ${input.factId}`);
82
- if (this.retractedAt(input.factId))
86
+ if (await this.retractedAt(input.factId))
83
87
  throw new Error(`fact ${input.factId} is already retracted`);
84
88
  const event = {
85
89
  eventId: randomUUID(),
@@ -90,95 +94,106 @@ export class Ledger {
90
94
  reason: input.reason,
91
95
  source: input.source ?? { receiptId: null },
92
96
  };
93
- this.append(event);
97
+ await this.store.append(event);
94
98
  return event;
95
99
  }
96
- confirm(input) {
97
- const prior = this.factById(input.factId);
100
+ async confirm(input) {
101
+ const prior = await this.factById(input.factId);
98
102
  if (!prior)
99
103
  throw new Error(`cannot confirm unknown fact ${input.factId}`);
100
- if (this.retractedAt(input.factId))
104
+ if (await this.retractedAt(input.factId))
101
105
  throw new Error(`fact ${input.factId} is retracted`);
102
- if (prior.fact.provenance !== "claimed" || this.confirmedAt(input.factId))
106
+ if (prior.fact.provenance !== "claimed" || (await this.confirmedAt(input.factId)))
103
107
  throw new Error(`fact ${input.factId} is already attested`);
104
108
  const event = { eventId: randomUUID(), kind: "confirm", txTime: this.now().toISOString(), factId: input.factId, actor: input.actor, source: input.source ?? { receiptId: null } };
105
- this.append(event);
109
+ await this.store.append(event);
106
110
  return event;
107
111
  }
108
- /**
109
- * Erases a fact's value from the ledger file, keeping its digest, and stops believing it. The file is rewritten in
110
- * place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about the fact
111
- * stays: who wrote it, when, from which receipt, and now who erased it and why.
112
- */
113
112
  /** Whether a legal hold currently stands on the fact. */
114
- held(factId) {
113
+ async held(factId) {
115
114
  let held = false;
116
- for (const e of this.events)
115
+ for (const e of await this.store.eventsFor(factId))
117
116
  if ((e.kind === "hold" || e.kind === "release") && e.factId === factId)
118
117
  held = e.kind === "hold";
119
118
  return held;
120
119
  }
121
- hold(input) {
122
- const prior = this.factById(input.factId);
120
+ async hold(input) {
121
+ const prior = await this.factById(input.factId);
123
122
  if (!prior)
124
123
  throw new Error(`cannot hold unknown fact ${input.factId}`);
125
124
  if (prior.fact.forgotten)
126
125
  throw new Error(`fact ${input.factId} is already forgotten`);
127
- if (this.held(input.factId))
126
+ if (await this.held(input.factId))
128
127
  throw new Error(`fact ${input.factId} is already on hold`);
129
128
  const event = { eventId: randomUUID(), kind: "hold", txTime: this.now().toISOString(), factId: input.factId, actor: input.actor, reason: input.reason, source: input.source ?? { receiptId: null } };
130
- this.append(event);
129
+ await this.store.append(event);
131
130
  return event;
132
131
  }
133
- release(input) {
134
- if (!this.held(input.factId))
132
+ async release(input) {
133
+ if (!(await this.held(input.factId)))
135
134
  throw new Error(`fact ${input.factId} is not on hold`);
136
135
  const event = { eventId: randomUUID(), kind: "release", txTime: this.now().toISOString(), factId: input.factId, actor: input.actor, reason: input.reason, source: input.source ?? { receiptId: null } };
137
- this.append(event);
136
+ await this.store.append(event);
138
137
  return event;
139
138
  }
139
+ /** The facts the ledger learned of before an instant, in one space or all, that have not been forgotten: what a retention sweep decides about. */
140
+ async learnedBefore(before, space) {
141
+ const events = await this.store.eventsAbout({ txBefore: before, ...(space === undefined ? {} : { space }) });
142
+ return events.filter((e) => e.kind === "assert" && e.txTime < before && (space === undefined || e.fact.space === space) && !e.fact.forgotten).map((e) => ({ ...e.fact }));
143
+ }
140
144
  /** Retention: forgets every fact the ledger learned of before the cutoff, in one space or all, skipping held and already-forgotten facts. Returns what it forgot and what it skipped. */
141
- sweep(input) {
145
+ async sweep(input) {
142
146
  const forgotten = [];
143
147
  const held = [];
144
- const targets = this.events.filter((e) => e.kind === "assert" && e.txTime < input.before && (input.space === undefined || e.fact.space === input.space) && !e.fact.forgotten);
145
- for (const e of targets) {
146
- if (this.held(e.fact.factId)) {
147
- held.push(e.fact.factId);
148
+ for (const f of await this.learnedBefore(input.before, input.space)) {
149
+ if (await this.held(f.factId)) {
150
+ held.push(f.factId);
148
151
  continue;
149
152
  }
150
- forgotten.push(this.forget({ factId: e.fact.factId, actor: input.actor, reason: input.reason, ...(input.source ? { source: input.source } : {}), ...(input.keepDigest === undefined ? {} : { keepDigest: input.keepDigest }) }));
153
+ forgotten.push(await this.forget({ factId: f.factId, actor: input.actor, reason: input.reason, ...(input.source ? { source: input.source } : {}), ...(input.keepDigest === undefined ? {} : { keepDigest: input.keepDigest }), compact: false }));
151
154
  }
155
+ if (forgotten.length > 0)
156
+ await this.store.compact();
152
157
  return { forgotten, held };
153
158
  }
154
- forget(input) {
155
- const prior = this.factById(input.factId);
159
+ /**
160
+ * Erases a fact's value from the store itself, keeping its digest, and stops believing it. The store rewrites the
161
+ * event in place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about
162
+ * the fact stays: who wrote it, when, from which receipt, and now who erased it and why. With compact false the
163
+ * store's reclaim step is left to the caller, for a batch of forgets followed by one compact().
164
+ */
165
+ async forget(input) {
166
+ const prior = await this.factById(input.factId);
156
167
  if (!prior)
157
168
  throw new Error(`cannot forget unknown fact ${input.factId}`);
158
169
  if (prior.fact.forgotten)
159
170
  throw new Error(`fact ${input.factId} is already forgotten`);
160
- if (this.held(input.factId))
171
+ if (await this.held(input.factId))
161
172
  throw new Error(`fact ${input.factId} is on legal hold; release it first`);
162
173
  const txTime = this.now().toISOString();
163
174
  const text = canonical(prior.fact.value);
164
175
  const digestKind = input.keepDigest === false ? "none" : this.forgetKey ? "hmac-sha256" : "sha256";
165
176
  const valueDigest = digestKind === "none" ? null : digestKind === "hmac-sha256" ? createHmac("sha256", this.forgetKey).update(text).digest("hex") : createHash("sha256").update(text).digest("hex");
166
- for (const e of this.events) {
177
+ for (const e of await this.store.eventsFor(input.factId)) {
167
178
  if (e.kind === "assert" && e.fact.factId === input.factId) {
168
- e.fact.value = null;
169
- e.fact.forgotten = { valueDigest, digestKind, at: txTime };
170
- this.store.replaceAssert(e);
179
+ await this.store.replaceAssert({ ...e, fact: { ...e.fact, value: null, forgotten: { valueDigest, digestKind, at: txTime } } });
171
180
  }
172
181
  }
173
182
  const event = { eventId: randomUUID(), kind: "forget", txTime, factId: input.factId, actor: input.actor, reason: input.reason, source: input.source ?? { receiptId: null }, valueDigest, digestKind };
174
- this.append(event);
183
+ await this.store.append(event);
184
+ if (input.compact !== false)
185
+ await this.store.compact();
175
186
  return event;
176
187
  }
188
+ /** Reclaims whatever the store may still hold of erased values; forget and sweep do this themselves unless told not to. */
189
+ compact() {
190
+ return this.store.compact();
191
+ }
177
192
  /** The facts believed at a moment. Valid time answers "was it true then"; transaction time answers "did the ledger know it then". */
178
- asOf(q = {}) {
193
+ async asOf(q = {}) {
179
194
  const validAt = q.validAt ?? this.now().toISOString();
180
195
  const txAt = q.txAt ?? this.now().toISOString();
181
- const known = this.events.filter((e) => e.txTime <= txAt);
196
+ const known = await this.store.eventsAbout({ txAtMost: txAt, ...(q.space === undefined ? {} : { space: q.space }), ...(q.subject === undefined ? {} : { subject: q.subject }), ...(q.predicate === undefined ? {} : { predicate: q.predicate }) });
182
197
  const retracted = new Set(known.filter((e) => e.kind === "retract" || e.kind === "forget").map((e) => e.factId));
183
198
  const confirmed = new Set(known.filter((e) => e.kind === "confirm").map((e) => e.factId));
184
199
  const facts = new Map();
@@ -198,41 +213,43 @@ export class Ledger {
198
213
  (q.predicate === undefined || f.predicate === q.predicate) &&
199
214
  (q.include === undefined || q.include === "all" || RANK[f.provenance] >= RANK[q.include]));
200
215
  }
216
+ /** One fact by id, whatever its state, with supersession applied; undefined when the ledger never held it. */
217
+ async get(factId) {
218
+ return (await this.factById(factId))?.fact;
219
+ }
201
220
  /** Every fact ever asserted, with supersession applied and retracted ones included, for audits that must see everything. */
202
- facts() {
221
+ async facts() {
222
+ const events = await this.store.events();
223
+ const retracted = new Set(events.filter((e) => e.kind === "retract" || e.kind === "forget").map((e) => e.factId));
203
224
  const out = new Map();
204
- for (const e of this.events) {
225
+ for (const e of events) {
205
226
  if (e.kind !== "assert")
206
227
  continue;
207
228
  out.set(e.fact.factId, { ...e.fact });
208
- if (e.supersedes && out.has(e.supersedes) && !this.retractedAt(e.fact.factId))
229
+ if (e.supersedes && out.has(e.supersedes) && !retracted.has(e.fact.factId))
209
230
  out.get(e.supersedes).validTo = e.fact.validFrom;
210
231
  }
211
232
  return [...out.values()];
212
233
  }
213
234
  /** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its confirmation, its retraction. */
214
235
  history(factId) {
215
- return this.events.filter((e) => (e.kind === "assert" ? e.fact.factId === factId || e.supersedes === factId : e.factId === factId));
236
+ return this.store.eventsFor(factId);
216
237
  }
217
- confirmedAt(factId) {
218
- return this.events.some((e) => e.kind === "confirm" && e.factId === factId);
238
+ async confirmedAt(factId) {
239
+ return (await this.store.eventsFor(factId)).some((e) => e.kind === "confirm" && e.factId === factId);
219
240
  }
220
- factById(factId) {
241
+ async factById(factId) {
221
242
  let found;
222
- for (const e of this.events) {
243
+ for (const e of await this.store.eventsFor(factId)) {
223
244
  if (e.kind === "assert" && e.fact.factId === factId)
224
245
  found = { ...e, fact: { ...e.fact } };
225
- else if (e.kind === "assert" && e.supersedes === factId && found && !this.retractedAt(e.fact.factId))
246
+ else if (e.kind === "assert" && e.supersedes === factId && found && !(await this.retractedAt(e.fact.factId)))
226
247
  found.fact.validTo = e.fact.validFrom;
227
248
  }
228
249
  return found;
229
250
  }
230
- retractedAt(factId) {
231
- return this.events.some((e) => (e.kind === "retract" || e.kind === "forget") && e.factId === factId);
232
- }
233
- append(event) {
234
- this.store.append(event);
235
- this.events.push(event);
251
+ async retractedAt(factId) {
252
+ return (await this.store.eventsFor(factId)).some((e) => (e.kind === "retract" || e.kind === "forget") && e.factId === factId);
236
253
  }
237
254
  }
238
255
  /** Canonical JSON: sorted keys, no whitespace, undefined dropped. The same encoding the receipts package uses. */
package/dist/pack.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { type Envelope, type KeyPair, type PublicKeyRef, type ReceiptBundle } from "@agent-custody/receipts";
2
+ import { type BlastRadius } from "./blast.ts";
3
+ import type { Fact, Ledger, LedgerEvent } from "./ledger.ts";
4
+ export declare const PACK_TYPE = "https://agent-custody.dev/custody-pack/v0.1";
5
+ export interface CustodyPack {
6
+ version: "0.1";
7
+ generatedAt: string;
8
+ factId: string;
9
+ fact: Fact | null;
10
+ /** every event that touched the fact, oldest first */
11
+ history: LedgerEvent[];
12
+ /** every receipt referenced by the history or the blast radius, by id, as the full verifiable bundle */
13
+ receipts: Record<string, ReceiptBundle>;
14
+ /** receipt ids the history or blast radius cited but no bundle was found for */
15
+ missingReceipts: string[];
16
+ blast: BlastRadius;
17
+ /** the forget event and what the forget receipt recorded, when the fact was forgotten */
18
+ forget: {
19
+ event: LedgerEvent;
20
+ receiptId: string | null;
21
+ verification: Record<string, string> | null;
22
+ removedFrom: string[] | null;
23
+ } | null;
24
+ /** hold and release events, in order */
25
+ holds: LedgerEvent[];
26
+ }
27
+ export declare function buildPack(ledger: Ledger, receiptsDir: string, factId: string): Promise<CustodyPack>;
28
+ export declare function signPack(pack: CustodyPack, key: KeyPair): Envelope;
29
+ export interface PackCheck {
30
+ name: string;
31
+ ok: boolean;
32
+ detail?: string;
33
+ }
34
+ export interface PackVerification {
35
+ ok: boolean;
36
+ checks: PackCheck[];
37
+ pack: CustodyPack | null;
38
+ }
39
+ /**
40
+ * Verifies the pack's own signature and digest, then every receipt inside it against the gateway's keys when given,
41
+ * that every event's source receipt is present, and that the forget receipt's result names this fact.
42
+ */
43
+ export declare function verifyPack(envelope: Envelope, packKeys: PublicKeyRef[], receiptKeys?: {
44
+ issuerKeys: PublicKeyRef[];
45
+ principalKeys?: PublicKeyRef[];
46
+ logKeys?: PublicKeyRef[];
47
+ upstreamKeys?: PublicKeyRef[];
48
+ }): PackVerification;
49
+ export declare function formatPack(p: CustodyPack): string;
package/dist/pack.js ADDED
@@ -0,0 +1,148 @@
1
+ // The custody pack: everything about one fact as a single artefact a reviewer can verify. Its history with the
2
+ // receipt that produced each event, its blast radius with every downstream receipt, its forget certificate and what
3
+ // the stores answered, all inside one signed statement. Hand it to counsel, an auditor, or the person who asked for
4
+ // the deletion; they need the signing key and the gateway's key, and nothing from you.
5
+ import { readdirSync, readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { digestOf, dsseSign, dsseVerify, verifyBundle } from "@agent-custody/receipts";
8
+ import { blastRadius, loadReceipts } from "./blast.js";
9
+ export const PACK_TYPE = "https://agent-custody.dev/custody-pack/v0.1";
10
+ function loadBundles(dir) {
11
+ const out = new Map();
12
+ for (const f of readdirSync(dir)) {
13
+ if (!f.endsWith(".json"))
14
+ continue;
15
+ try {
16
+ const bundle = JSON.parse(readFileSync(join(dir, f), "utf8"));
17
+ const st = JSON.parse(Buffer.from(bundle.envelope.payload, "base64").toString());
18
+ if (st.predicate?.receiptId)
19
+ out.set(st.predicate.receiptId, bundle);
20
+ }
21
+ catch {
22
+ // not a bundle; skip
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+ function receiptResult(bundle) {
28
+ try {
29
+ const st = JSON.parse(Buffer.from(bundle.envelope.payload, "base64").toString());
30
+ const text = st.predicate?.execution?.result?.content?.find((c) => c.type === "text")?.text;
31
+ if (!text)
32
+ return null;
33
+ try {
34
+ return JSON.parse(text);
35
+ }
36
+ catch {
37
+ return text;
38
+ }
39
+ }
40
+ catch {
41
+ return null; // a damaged bundle; the receipt check reports it
42
+ }
43
+ }
44
+ export async function buildPack(ledger, receiptsDir, factId) {
45
+ const bundles = loadBundles(receiptsDir);
46
+ const history = await ledger.history(factId);
47
+ const blast = await blastRadius(ledger, loadReceipts(receiptsDir), factId);
48
+ const wanted = new Set();
49
+ for (const e of history) {
50
+ const src = e.kind === "assert" ? e.fact.source.receiptId : e.source.receiptId;
51
+ if (src)
52
+ wanted.add(src);
53
+ }
54
+ for (const r of blast.receipts)
55
+ wanted.add(r.receiptId);
56
+ for (const f of blast.derivedFacts)
57
+ if (f.source.receiptId)
58
+ wanted.add(f.source.receiptId);
59
+ const receipts = {};
60
+ const missingReceipts = [];
61
+ for (const id of [...wanted].sort()) {
62
+ const b = bundles.get(id);
63
+ if (b)
64
+ receipts[id] = b;
65
+ else
66
+ missingReceipts.push(id);
67
+ }
68
+ const forgetEvent = history.find((e) => e.kind === "forget");
69
+ let forget = null;
70
+ if (forgetEvent && forgetEvent.kind === "forget") {
71
+ const rid = forgetEvent.source.receiptId;
72
+ const result = rid && receipts[rid] ? receiptResult(receipts[rid]) : null;
73
+ forget = { event: forgetEvent, receiptId: rid, verification: result?.verification ?? null, removedFrom: result?.removedFrom ?? null };
74
+ }
75
+ return {
76
+ version: "0.1",
77
+ generatedAt: new Date().toISOString(),
78
+ factId,
79
+ fact: blast.fact,
80
+ history,
81
+ receipts,
82
+ missingReceipts,
83
+ blast,
84
+ forget,
85
+ holds: history.filter((e) => e.kind === "hold" || e.kind === "release"),
86
+ };
87
+ }
88
+ export function signPack(pack, key) {
89
+ const statement = { _type: "https://in-toto.io/Statement/v1", subject: [{ name: `custody-pack:${pack.factId}`, digest: { sha256: digestOf(pack) } }], predicateType: PACK_TYPE, predicate: pack };
90
+ return dsseSign("application/vnd.in-toto+json", statement, key);
91
+ }
92
+ /**
93
+ * Verifies the pack's own signature and digest, then every receipt inside it against the gateway's keys when given,
94
+ * that every event's source receipt is present, and that the forget receipt's result names this fact.
95
+ */
96
+ export function verifyPack(envelope, packKeys, receiptKeys) {
97
+ const checks = [];
98
+ const add = (name, ok, detail) => {
99
+ checks.push(detail === undefined ? { name, ok } : { name, ok, detail });
100
+ return ok;
101
+ };
102
+ const v = dsseVerify(envelope, packKeys);
103
+ if (!v.ok) {
104
+ add("pack signature", false, v.error);
105
+ return { ok: false, checks, pack: null };
106
+ }
107
+ add("pack signature", true, `keyid ${v.keyid.slice(0, 12)}`);
108
+ const st = v.payload;
109
+ if (!add("pack type", st.predicateType === PACK_TYPE && !!st.predicate))
110
+ return { ok: false, checks, pack: null };
111
+ const pack = st.predicate;
112
+ add("pack digest", st.subject?.[0]?.digest?.sha256 === digestOf(pack));
113
+ add("no receipts missing", pack.missingReceipts.length === 0, pack.missingReceipts.length ? `${pack.missingReceipts.length} cited receipt(s) absent` : undefined);
114
+ for (const e of pack.history) {
115
+ const src = e.kind === "assert" ? e.fact.source.receiptId : e.source.receiptId;
116
+ if (src)
117
+ add(`${e.kind} event cites a receipt in the pack`, src in pack.receipts, src.slice(0, 8));
118
+ }
119
+ if (receiptKeys) {
120
+ for (const [id, bundle] of Object.entries(pack.receipts)) {
121
+ const r = verifyBundle(bundle, { issuerKeys: receiptKeys.issuerKeys, principalKeys: receiptKeys.principalKeys ?? [], ...(receiptKeys.logKeys ? { logKeys: receiptKeys.logKeys } : {}), ...(receiptKeys.upstreamKeys ? { upstreamKeys: receiptKeys.upstreamKeys } : {}) });
122
+ add(`receipt ${id.slice(0, 8)} verifies`, r.ok, r.ok ? undefined : r.checks.filter((c) => !c.ok).map((c) => c.name).join(", "));
123
+ if (r.ok && r.statement && r.statement.predicate.receiptId !== id)
124
+ add(`receipt ${id.slice(0, 8)} is the receipt it claims to be`, false);
125
+ }
126
+ }
127
+ else {
128
+ add("receipts checked against the gateway's keys", false, "no issuer key given; pass --issuer-key to check the receipts inside the pack");
129
+ }
130
+ if (pack.forget) {
131
+ const bundle = pack.forget.receiptId ? pack.receipts[pack.forget.receiptId] : undefined;
132
+ const result = bundle ? receiptResult(bundle) : null;
133
+ add("forget receipt names this fact and records the erasure", !!result && result.factId === pack.factId && result.erasedFromLedger === true, bundle ? undefined : "forget receipt not in the pack");
134
+ }
135
+ return { ok: checks.every((c) => c.ok), checks, pack };
136
+ }
137
+ export function formatPack(p) {
138
+ const lines = [];
139
+ lines.push(p.fact ? `fact ${p.factId}: ${p.fact.subject} ${p.fact.predicate}${p.fact.forgotten ? " = (erased)" : ` = ${JSON.stringify(p.fact.value)}`} (space ${p.fact.space}, by ${p.fact.actor}, ${p.fact.provenance})` : `fact ${p.factId}: not in this ledger`);
140
+ lines.push(`history: ${p.history.map((e) => e.kind).join(" → ")}`);
141
+ lines.push(`receipts in pack: ${Object.keys(p.receipts).length}${p.missingReceipts.length ? `, missing ${p.missingReceipts.length}` : ""}`);
142
+ if (p.holds.length)
143
+ lines.push(`holds: ${p.holds.map((e) => (e.kind === "hold" || e.kind === "release" ? `${e.kind} by ${e.actor} (${e.reason})` : e.kind)).join("; ")}`);
144
+ if (p.forget)
145
+ lines.push(`forgotten at ${p.forget.event.txTime} by ${p.forget.event.actor}; receipt ${p.forget.receiptId?.slice(0, 8) ?? "none"}; stores: ${p.forget.verification ? Object.entries(p.forget.verification).map(([k, v]) => `${k}=${v}`).join(", ") : "none"}`);
146
+ lines.push(`blast radius: ${p.blast.receipts.length} later call(s), ${p.blast.derivedFacts.length} derived belief(s), ${p.blast.stillBelieved.length} still believed`);
147
+ return lines.join("\n");
148
+ }
package/dist/server.d.ts CHANGED
@@ -21,6 +21,11 @@ export interface MemoryServerOptions {
21
21
  identity?: KeyPair;
22
22
  /** Retention windows by space pattern, ISO 8601 durations: { "org": "P365D", "team:*": "P90D" }. memory.sweep without `before` uses them. */
23
23
  retention?: Record<string, string>;
24
+ /** After a removal, how many times to ask a store's search whether the value is gone, and the first wait between asks (doubling). Default 3 and 200 ms. */
25
+ verify?: {
26
+ attempts?: number;
27
+ delayMs?: number;
28
+ };
24
29
  }
25
30
  /** Parses the ISO 8601 duration subset retention needs: P<n>W, P<n>D, PT<n>H, and combinations of D and H. */
26
31
  export declare function durationMs(iso: string): number;