@engramx/client 0.15.0 → 0.16.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +262 -26
  2. package/dist/EngramClient.d.ts +235 -229
  3. package/dist/EngramClient.js +446 -1247
  4. package/dist/EngramClient.js.map +1 -1
  5. package/dist/MarketClient.d.ts +137 -0
  6. package/dist/MarketClient.js +228 -0
  7. package/dist/MarketClient.js.map +1 -0
  8. package/dist/ProfileManager.d.ts +4 -4
  9. package/dist/SessionManager.d.ts +3 -3
  10. package/dist/SessionManager.js +1 -1
  11. package/dist/SessionManager.js.map +1 -1
  12. package/dist/actionReceipt.d.ts +67 -0
  13. package/dist/actionReceipt.js +76 -2
  14. package/dist/actionReceipt.js.map +1 -1
  15. package/dist/auth.d.ts +16 -0
  16. package/dist/auth.js +38 -5
  17. package/dist/auth.js.map +1 -1
  18. package/dist/certifiedAudit.d.ts +32 -6
  19. package/dist/certifiedAudit.js +75 -14
  20. package/dist/certifiedAudit.js.map +1 -1
  21. package/dist/cli.js +100 -26
  22. package/dist/cli.js.map +1 -1
  23. package/dist/encryption.d.ts +30 -12
  24. package/dist/encryption.js +13 -15
  25. package/dist/encryption.js.map +1 -1
  26. package/dist/idl.d.ts +69 -0
  27. package/dist/idl.js +242 -0
  28. package/dist/idl.js.map +1 -0
  29. package/dist/index.d.ts +12 -18
  30. package/dist/index.js +58 -9
  31. package/dist/index.js.map +1 -1
  32. package/dist/ledgerfsHash.js +1 -1
  33. package/dist/ledgerfsHash.js.map +1 -1
  34. package/dist/lineage.js +8 -5
  35. package/dist/lineage.js.map +1 -1
  36. package/dist/memoryArchive.d.ts +428 -0
  37. package/dist/memoryArchive.js +504 -0
  38. package/dist/memoryArchive.js.map +1 -0
  39. package/dist/migrate.js +31 -4
  40. package/dist/migrate.js.map +1 -1
  41. package/dist/openEngram.d.ts +55 -0
  42. package/dist/openEngram.js +185 -0
  43. package/dist/openEngram.js.map +1 -0
  44. package/dist/pair.d.ts +33 -2
  45. package/dist/pair.js +56 -9
  46. package/dist/pair.js.map +1 -1
  47. package/dist/types.d.ts +246 -307
  48. package/dist/version.d.ts +8 -0
  49. package/dist/version.js +45 -0
  50. package/dist/version.js.map +1 -0
  51. package/dist/wire.d.ts +76 -0
  52. package/dist/wire.js +79 -0
  53. package/dist/wire.js.map +1 -0
  54. package/package.json +11 -19
  55. package/dist/bootstrap.d.ts +0 -21
  56. package/dist/bootstrap.js +0 -71
  57. package/dist/bootstrap.js.map +0 -1
@@ -0,0 +1,504 @@
1
+ // The memory log's archive surface — the client half of the engram's export → verify → condense
2
+ // contract (ledgerfs build stage S4d: "condense refuses without an export receipt").
3
+ //
4
+ // WHY THIS IS MORE THAN FIVE PASSTHROUGHS. The canister's rule is that log entries may only be
5
+ // dropped after a chain-recorded export proved they were served. That makes the SDK, not the
6
+ // canister, the place where an owner actually ends up holding a verifiable archive — and it makes
7
+ // every shortcut here a way to destroy evidence. Three specific ones are closed below:
8
+ //
9
+ // 1. A SHORT ARCHIVE THAT REPORTS ITSELF COMPLETE. `getMemoryLogPage` clamps a page and
10
+ // discloses the clamp through `pageCap`; a client that assumed it got `max` would walk the
11
+ // chain in strides longer than the canister actually served and skip whatever fell between.
12
+ // assembleMemoryArchive advances by the window the canister CONFIRMED, and then asserts the
13
+ // range came back whole. It throws rather than return a gap.
14
+ // ⟨2026-10-07, beside the point above — the owner's decision of that day⟩ The engram serves
15
+ // `listMemoryLog` now, and its page states the window directly: `items.length + withheld`
16
+ // entries scanned, starting at `max(startSeq, firstAvailableSeq)`. The walk advances by that,
17
+ // as the owner's own archive page does. `pageCap` is not served and is not read.
18
+ // 2. A PAGE THE CANISTER CANNOT FIT IN ONE RESPONSE. A `#set`/`#restore` entry carries a full
19
+ // manifest — up to 40 chunk references, since maxFileBytes is 10 MiB at a frozen 256 KiB
20
+ // chunkSize (src/engram/LedgerFsInit.mo) — so a manifest-heavy page can exceed the 2 MiB
21
+ // response limit. The engram already clamps to 256 rows for this (MemoryRouter.LOG_PAGE_MAX,
22
+ // down from the library's 1024) and DISCLOSES the clamp as `pageCap`; the margin it leaves is
23
+ // an estimate, not a proof, so a rejection is retried at HALF the page size, down to a single
24
+ // entry, and is never allowed to end the walk early.
25
+ // 3. AN ARCHIVE NOBODY CHECKED. verifyMemoryArchive recomputes every entry hash and rebuilds
26
+ // the ops root, reusing ./ledgerfsHash.ts — the same file the `ledgerfs-goldens` cross-lock
27
+ // pins byte-for-byte against the Motoko encoder. Nothing here reimplements hashing.
28
+ //
29
+ // It also states a CEILING and refuses past it. Assembling an unbounded chain into one JS array
30
+ // is a real failure mode (see ARCHIVE_MAX_RANGE), and splitting is free: each part gets its own
31
+ // receipt, and receipts compose — condense only needs one that covers the prefix being dropped.
32
+ import { CanisterRefusal } from './wire.js';
33
+ import { eventHash, opsRoot as opsRootOverEventHashes, toHex, } from './ledgerfsHash.js';
34
+ // ⟨DELETED 2026-10-05⟩ `MemoryExportReceiptsPage` stood here: "One page of the
35
+ // receipts-and-checkpoints ledger. `next` is the `before` cursor, null = done", with the fields
36
+ // { receipts, checkpoints, firstAvailableSeq, totalEvents, scannedFromSeq, scanTruncated, next }.
37
+ // The 0.16.0 cut split that reply in two. `listMemoryExportReceipts` answers
38
+ // `ReceiptPage = { firstAvailableSeq; page : { items : vec ExportReceiptRow; total; withheld };
39
+ // scannedThroughSeq }`, paged forward by absolute seq, and `listCondenseCheckpoints` is the other
40
+ // half. Six of the seven fields were therefore served by nothing, and nothing in the workspace read
41
+ // the type. Removing the unserved fields would have left a one-field shell, so the type is gone.
42
+ // The rows survive (`MemoryExportReceiptRow`, `MemoryCheckpointRow` above) because they DO mirror
43
+ // the items both lists serve.
44
+ // ── Ceilings ───────────────────────────────────────────────────────────────────────────────────
45
+ /**
46
+ * How many log entries one assembled archive may span.
47
+ *
48
+ * A `#set` entry carries a full manifest, and maxFileBytes / chunkSize = 10 MiB / 256 KiB caps a
49
+ * manifest at 40 chunk references (src/engram/LedgerFsInit.mo) — several kilobytes of decoded JS
50
+ * objects for a single entry. Ten thousand of those is already past what a browser tab should
51
+ * hold, and an owner who runs out of memory mid-walk gets no archive AND no receipt.
52
+ *
53
+ * The ceiling is on the RANGE WIDTH, not on the entries kept, so an author-filtered archive is
54
+ * bounded by the same number: the range is what drives the round trips, and the range is what a
55
+ * receipt attests. Splitting costs nothing — each part gets its own receipt, and condense needs
56
+ * only one receipt covering the prefix it drops.
57
+ */
58
+ export const ARCHIVE_MAX_RANGE = 10000n;
59
+ /**
60
+ * Entries requested per page before any clamp.
61
+ *
62
+ * Set to the engram's own cap (`MemoryRouter.LOG_PAGE_MAX`, 256) so a full page is never left
63
+ * half-used — the engram clamps the ask to that number anyway and reports the result as
64
+ * `pageCap`. Asking for the cap is safe rather than optimistic because the fallback below it is
65
+ * real: a page that will not fit is retried at half the size, and a walk that had to shrink says
66
+ * so through `smallestPageUsed`.
67
+ * ⟨2026-10-07, beside "reports the result as `pageCap`"⟩ It reports the window it scanned as
68
+ * `items.length + withheld` now; `pageCap` is not served.
69
+ */
70
+ export const ARCHIVE_PAGE_EVENTS = 256n;
71
+ // ── Byte/shape normalization ───────────────────────────────────────────────────────────────────
72
+ // The agent decodes `vec nat8` as a Uint8Array, but a hand-built stub (or an older agent) can
73
+ // hand back a plain number[]. Copying once here means every hash input downstream is the shape
74
+ // ledgerfsHash expects, and a wrong shape fails at the boundary instead of producing a digest
75
+ // over the wrong bytes.
76
+ const asBytes = (v) => v instanceof Uint8Array ? v : Uint8Array.from(v);
77
+ const rawPrincipal = (p) => p.toUint8Array();
78
+ function toHashManifest(m) {
79
+ return {
80
+ chunks: m.chunks.map((c) => ({
81
+ hash: asBytes(c.hash),
82
+ size: c.size,
83
+ birthSeq: c.birthSeq,
84
+ })),
85
+ totalSize: m.totalSize,
86
+ contentRoot: asBytes(m.contentRoot),
87
+ };
88
+ }
89
+ function toHashImportNote(n) {
90
+ if ('genesis' in n) {
91
+ const b = n.genesis.boundTo;
92
+ return { kind: 'genesis', boundTo: b.length === 0 ? null : asBytes(b[0]) };
93
+ }
94
+ if ('legacyGap' in n)
95
+ return { kind: 'legacyGap', firstSurviving: n.legacyGap.firstSurviving };
96
+ if ('legacyTruncated' in n) {
97
+ return {
98
+ kind: 'legacyTruncated',
99
+ storedBytes: n.legacyTruncated.storedBytes,
100
+ prefixHash: asBytes(n.legacyTruncated.prefixHash),
101
+ };
102
+ }
103
+ if ('legacyDelete' in n)
104
+ return { kind: 'legacyDelete' };
105
+ if ('legacyUnreconstructable' in n) {
106
+ return {
107
+ kind: 'legacyUnreconstructable',
108
+ contentHash: asBytes(n.legacyUnreconstructable.contentHash),
109
+ reason: n.legacyUnreconstructable.reason,
110
+ };
111
+ }
112
+ // Not reachable through Candid: the decoder rejects a tag the IDL does not declare, so an
113
+ // unknown note here means the wire shape was hand-built. Refuse rather than hash a guess.
114
+ throw new Error(`Unrecognized import note in a log entry: ${JSON.stringify(Object.keys(n))}`);
115
+ }
116
+ function toHashOp(op) {
117
+ if ('append' in op) {
118
+ return {
119
+ kind: 'append',
120
+ deltaHash: asBytes(op.append.deltaHash),
121
+ deltaSize: op.append.deltaSize,
122
+ newSize: op.append.newSize,
123
+ };
124
+ }
125
+ if ('set' in op)
126
+ return { kind: 'set', manifest: toHashManifest(op.set.manifest) };
127
+ if ('restore' in op) {
128
+ return {
129
+ kind: 'restore',
130
+ toVersion: op.restore.toVersion,
131
+ manifest: toHashManifest(op.restore.manifest),
132
+ };
133
+ }
134
+ if ('tombstone' in op)
135
+ return { kind: 'tombstone' };
136
+ if ('evict' in op) {
137
+ return {
138
+ kind: 'evict',
139
+ fromVersion: op.evict.fromVersion,
140
+ toVersion: op.evict.toVersion,
141
+ chunkCount: op.evict.chunkCount,
142
+ byteCount: op.evict.byteCount,
143
+ evictedRoot: asBytes(op.evict.evictedRoot),
144
+ };
145
+ }
146
+ if ('condense' in op) {
147
+ return {
148
+ kind: 'condense',
149
+ fromSeq: op.condense.fromSeq,
150
+ toSeq: op.condense.toSeq,
151
+ opCount: op.condense.opCount,
152
+ opsRoot: asBytes(op.condense.opsRoot),
153
+ headAtBoundary: asBytes(op.condense.headAtBoundary),
154
+ };
155
+ }
156
+ if ('exported' in op) {
157
+ const f = op.exported.authorFilter;
158
+ return {
159
+ kind: 'exported',
160
+ fromSeq: op.exported.fromSeq,
161
+ toSeq: op.exported.toSeq,
162
+ opsRoot: asBytes(op.exported.opsRoot),
163
+ authorFilterRaw: f.length === 0 ? null : rawPrincipal(f[0]),
164
+ };
165
+ }
166
+ if ('imported' in op)
167
+ return { kind: 'imported', note: toHashImportNote(op.imported) };
168
+ throw new Error(`Unrecognized operation in a log entry: ${JSON.stringify(Object.keys(op))}`);
169
+ }
170
+ /**
171
+ * A decoded log entry in the form the frozen hash contract operates on.
172
+ *
173
+ * The store-global sentinel substitution (`#condense`/`#exported` hash with an empty path, zero
174
+ * version, zero size and a zero content root) is NOT applied here — ledgerfsHash owns it, in the
175
+ * one place the Motoko encoder is mirrored. Doing it twice is how the two drift.
176
+ */
177
+ export function toLedgerFsEvent(e) {
178
+ return {
179
+ seq: e.seq,
180
+ path: e.path,
181
+ version: e.version,
182
+ op: toHashOp(e.op),
183
+ byRaw: rawPrincipal(e.by),
184
+ at: e.atNs,
185
+ size: e.size,
186
+ contentRoot: asBytes(e.contentRoot),
187
+ prev: asBytes(e.prev),
188
+ };
189
+ }
190
+ /** The canonical hash of one log entry, recomputed from its decoded fields. */
191
+ export const memoryLogEventHash = (e) => eventHash(toLedgerFsEvent(e));
192
+ /**
193
+ * Whether a failed page looks like the engram could not fit the response, as opposed to refusing
194
+ * the call.
195
+ *
196
+ * Deliberately STRICT. Halving on every error would spend six round trips turning an
197
+ * authorization refusal into the same refusal, and would report it under a misleading heading;
198
+ * an unmatched error is rethrown with the cursor attached instead. If a new replica wording slips
199
+ * past this, the archive fails LOUDLY — the walk stops and throws — which is the safe direction:
200
+ * the alternative is an archive that is quietly short.
201
+ */
202
+ export function isResponseTooLarge(err) {
203
+ // A client-side allocation failure is the same class of problem from the owner's side: the
204
+ // page was too big to hold. Halving is the same remedy.
205
+ if (err instanceof RangeError)
206
+ return true;
207
+ const text = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
208
+ return /payload size|response size|message size|response is too large|too large|exceeds[^.]{0,60}\b(size|limit|maximum)\b|IC0504/i.test(text);
209
+ }
210
+ const asBig = (v) => (typeof v === 'bigint' ? v : BigInt(v));
211
+ /**
212
+ * Walk a range of the memory log into one archive, or throw.
213
+ *
214
+ * WHAT IT REFUSES TO DO, in order of how badly each would hurt:
215
+ * - report a short archive as whole (every seq in an unfiltered range is asserted present);
216
+ * - assume it received `max` entries (it advances by the window the engram confirmed via
217
+ * `pageCap`, and by the highest seq actually returned when that is further);
218
+ * - give up on a page the engram could not fit (halves and retries, down to one entry);
219
+ * - start below the retained floor (a condensed prefix is unattestable, so an explicit request
220
+ * for it is refused rather than silently raised);
221
+ * - assemble more than {@link ARCHIVE_MAX_RANGE} entries in one pass.
222
+ * ⟨2026-10-07, beside the second point — the owner's decision of that day⟩ The window the engram
223
+ * confirmed is `page.items.length + page.withheld` now — the entries it scanned for the page,
224
+ * starting at `max(startSeq, firstAvailableSeq)` — the rule the engram's own `listMemoryLog` doc
225
+ * states and the owner's archive page walks by. Two refusals join the list: a page whose window is
226
+ * EMPTY while the range still has entries to serve (the walk would spin, or end short), and a floor
227
+ * that rises above the cursor mid-walk (a condense ran during the walk, and what it dropped can no
228
+ * longer be served or attested).
229
+ *
230
+ * Termination is structural: the cursor advances by at least one seq per successful page, and the
231
+ * page size strictly halves per size failure until it reaches one.
232
+ */
233
+ export async function assembleMemoryArchive(fetchPage, options = {}) {
234
+ const authorFilter = options.authorFilter;
235
+ const filterArg = authorFilter === undefined ? [] : [authorFilter];
236
+ const explicitFrom = options.fromSeq === undefined ? null : asBig(options.fromSeq);
237
+ const explicitTo = options.toSeq === undefined ? null : asBig(options.toSeq);
238
+ if (explicitFrom !== null && explicitFrom < 1n) {
239
+ throw new Error('assembleMemoryArchive: fromSeq starts at 1 — the log has no entry 0.');
240
+ }
241
+ const requestedStart = explicitFrom ?? 1n;
242
+ let requested = options.pageEvents === undefined ? ARCHIVE_PAGE_EVENTS : asBig(options.pageEvents);
243
+ if (requested < 1n)
244
+ requested = 1n;
245
+ // Only ever decreases: the fallback shrinks the page and never grows it back, so one oversized
246
+ // page slows the rest of the walk rather than re-triggering the same rejection every stride.
247
+ let smallestPageUsed = requested;
248
+ const ceiling = (() => {
249
+ if (options.maxRange === undefined)
250
+ return ARCHIVE_MAX_RANGE;
251
+ const asked = asBig(options.maxRange);
252
+ if (asked < 1n)
253
+ return 1n;
254
+ return asked < ARCHIVE_MAX_RANGE ? asked : ARCHIVE_MAX_RANGE;
255
+ })();
256
+ const bySeq = new Map();
257
+ let cursor = requestedStart;
258
+ let fromSeq = requestedStart;
259
+ let toSeq = explicitTo;
260
+ let pagesFetched = 0;
261
+ // Set from the first page and refreshed by every one after it. No placeholder value: an
262
+ // uninitialized read would be a bug, and a stand-in zero head or zero total is exactly the
263
+ // fabricated reading this whole surface exists to avoid.
264
+ let head;
265
+ let totalEvents;
266
+ let firstAvailableSeq;
267
+ let firstPage = true;
268
+ for (;;) {
269
+ // Where THIS request started. Held separately from `cursor` because the first page can move
270
+ // `fromSeq` up to the retained floor, and the stride must still be measured from where the
271
+ // engram actually began scanning — not from a raised cursor, which would jump a whole window
272
+ // past entries the very page in hand did not reach.
273
+ const pageStart = cursor;
274
+ let reply;
275
+ try {
276
+ reply = await fetchPage(pageStart, requested, filterArg);
277
+ }
278
+ catch (err) {
279
+ if (!isResponseTooLarge(err)) {
280
+ const why = err instanceof Error ? err.message : String(err);
281
+ throw new Error(`assembleMemoryArchive: the engram refused the page at seq ${pageStart} (${requested} entries): ${why}. Nothing was assembled.`, { cause: err });
282
+ }
283
+ if (requested <= 1n) {
284
+ const why = err instanceof Error ? err.message : String(err);
285
+ throw new Error(`assembleMemoryArchive: log entry ${pageStart} could not be served even one at a time: ${why}. The archive stops here rather than skipping it — export the ranges either side of ${pageStart} separately and report this entry.`, { cause: err });
286
+ }
287
+ requested = requested / 2n; // bigint division floors; requested > 1 here, so it drops
288
+ if (requested < 1n)
289
+ requested = 1n;
290
+ if (requested < smallestPageUsed)
291
+ smallestPageUsed = requested;
292
+ continue;
293
+ }
294
+ // A typed refusal is the engram's answer, not a transport failure: it is never halved into
295
+ // (halving an authorization refusal would spend six round trips arriving at the same answer).
296
+ if ('Err' in reply) {
297
+ const refusal = new CanisterRefusal('listMemoryLog', reply.Err);
298
+ throw new Error(`assembleMemoryArchive: the engram refused the page at seq ${pageStart} (${requested} entries): ${refusal.message}. Nothing was assembled.`, { cause: refusal });
299
+ }
300
+ const page = reply.Ok;
301
+ pagesFetched += 1;
302
+ head = asBytes(page.head);
303
+ totalEvents = page.page.total;
304
+ firstAvailableSeq = page.firstAvailableSeq;
305
+ if (firstPage) {
306
+ firstPage = false;
307
+ if (page.firstAvailableSeq > pageStart) {
308
+ if (explicitFrom !== null) {
309
+ throw new Error(`assembleMemoryArchive: entries below seq ${page.firstAvailableSeq} were condensed away and can no longer be served, so a range starting at ${explicitFrom} cannot be attested. Start at ${page.firstAvailableSeq} or later.`);
310
+ }
311
+ // Only `fromSeq` moves. The engram began scanning at the floor, and the stride below is
312
+ // measured from there.
313
+ fromSeq = page.firstAvailableSeq;
314
+ }
315
+ if (explicitTo !== null && explicitTo > page.page.total) {
316
+ throw new Error(`assembleMemoryArchive: the log holds ${page.page.total} entries, so seq ${explicitTo} does not exist yet.`);
317
+ }
318
+ toSeq = explicitTo ?? page.page.total;
319
+ // Guard BEFORE subtracting. An empty log answers total = 0 with a floor of 1, so
320
+ // `toSeq - fromSeq` runs backwards there; in TypeScript that yields a negative BigInt
321
+ // rather than trapping, which would silently size the ceiling check the wrong way round.
322
+ const width = toSeq < fromSeq ? 0n : toSeq - fromSeq + 1n;
323
+ if (width === 0n) {
324
+ return {
325
+ events: [],
326
+ fromSeq,
327
+ toSeq,
328
+ authorFilter: authorFilter ?? null,
329
+ totalEvents,
330
+ firstAvailableSeq,
331
+ head,
332
+ pagesFetched,
333
+ smallestPageUsed,
334
+ };
335
+ }
336
+ if (width > ceiling) {
337
+ throw new Error(`assembleMemoryArchive: ${width} log entries (seq ${fromSeq}–${toSeq}) is past the ${ceiling}-entry ceiling for one archive. Export it in parts — each part gets its own receipt, and a condense only needs a receipt covering the prefix it drops.`);
338
+ }
339
+ }
340
+ else if (page.firstAvailableSeq > pageStart) {
341
+ // The floor rose above the cursor between two pages: a condense ran during the walk, and the
342
+ // engram clamped this page UP past entries the range still needs. They can no longer be
343
+ // served, so the range cannot be attested; a filtered walk would otherwise skip them without
344
+ // noticing. Refused, rather than returned short.
345
+ throw new Error(`assembleMemoryArchive: entries below seq ${page.firstAvailableSeq} were condensed away while the walk was at seq ${pageStart}, so seq ${pageStart}–${page.firstAvailableSeq - 1n} can no longer be served. Nothing is returned — walk the range again from ${page.firstAvailableSeq}.`);
346
+ }
347
+ // THE WINDOW IS THE CURSOR, and the page states it: the entries the engram scanned for this
348
+ // page, `items.length + withheld`, starting at max(pageStart, the floor). Never the rows alone
349
+ // — an author filter makes a page short or empty while the chain continues, and striding by
350
+ // the rows returned would skip every entry the filter removed and stall outright on an empty
351
+ // page; `withheld` is what the filter removed, so the sum is the scan.
352
+ const window = BigInt(page.page.items.length) + page.page.withheld;
353
+ if (window === 0n) {
354
+ // Nothing scanned at a cursor the range still covers (cursor <= toSeq <= total): the engram
355
+ // served nothing for entries that exist. Advancing would spin; stopping would return short.
356
+ throw new Error(`assembleMemoryArchive: the engram scanned no entries at seq ${pageStart} while the log holds ${page.page.total}, so seq ${pageStart}–${toSeq} came back empty. A short archive is not an archive — nothing is returned.`);
357
+ }
358
+ let highestSeen = null;
359
+ for (const e of page.page.items) {
360
+ if (highestSeen === null || e.seq > highestSeen)
361
+ highestSeen = e.seq;
362
+ if (e.seq < fromSeq || e.seq > toSeq)
363
+ continue; // an overshooting window, not an error
364
+ if (!bySeq.has(e.seq))
365
+ bySeq.set(e.seq, e);
366
+ }
367
+ // Advance past the window that was actually covered. A request below the retained floor is
368
+ // clamped UP by the engram, so the window began at max(pageStart, floor). The last clause is
369
+ // belt-and-braces: if a page ever returns an entry beyond the window it declared, the cursor
370
+ // still ends up past it. Every clause only raises the cursor, so the walk always moves.
371
+ const windowStart = pageStart < page.firstAvailableSeq ? page.firstAvailableSeq : pageStart;
372
+ let next = windowStart + window;
373
+ if (highestSeen !== null && highestSeen + 1n > next)
374
+ next = highestSeen + 1n;
375
+ cursor = next;
376
+ if (cursor > toSeq)
377
+ break;
378
+ }
379
+ const events = [...bySeq.values()].sort((a, b) => (a.seq < b.seq ? -1 : a.seq > b.seq ? 1 : 0));
380
+ // The anti-fail-to-empty assertion. Only an UNFILTERED archive can be checked this way: with a
381
+ // filter, the engram is expected to skip entries, and the client has no way to know which. What
382
+ // holds in both cases is that every window covering [fromSeq, toSeq] was requested, which the
383
+ // loop above guarantees by construction — and, since 0.16.0, that every window it covered was
384
+ // counted by the engram itself (`withheld`).
385
+ if (authorFilter === undefined) {
386
+ let expect = fromSeq;
387
+ for (const e of events) {
388
+ if (e.seq !== expect) {
389
+ throw new Error(`assembleMemoryArchive: the walk of seq ${fromSeq}–${toSeq} came back missing entry ${expect} (next entry served was ${e.seq}). A short archive is not an archive — nothing is returned.`);
390
+ }
391
+ expect += 1n;
392
+ }
393
+ if (expect !== toSeq + 1n) {
394
+ throw new Error(`assembleMemoryArchive: the walk of seq ${fromSeq}–${toSeq} stopped after entry ${expect - 1n}. A short archive is not an archive — nothing is returned.`);
395
+ }
396
+ }
397
+ return {
398
+ events,
399
+ fromSeq,
400
+ toSeq: toSeq,
401
+ authorFilter: authorFilter ?? null,
402
+ totalEvents,
403
+ firstAvailableSeq,
404
+ head,
405
+ pagesFetched,
406
+ smallestPageUsed,
407
+ };
408
+ }
409
+ const bytesEqual = (a, b) => {
410
+ if (a.length !== b.length)
411
+ return false;
412
+ for (let i = 0; i < a.length; i++)
413
+ if (a[i] !== b[i])
414
+ return false;
415
+ return true;
416
+ };
417
+ /**
418
+ * Rebuild the ops root over a set of log entries exactly as the engram does: the entries in
419
+ * `[fromSeq, toSeq]` that pass the author filter, in ascending seq order, each hashed with
420
+ * ledgerfsHash and wrapped as an ops leaf.
421
+ *
422
+ * An empty covered set has a DEFINED root — the honest attestation that this author wrote nothing
423
+ * in the range — so it is computed, never special-cased into a failure.
424
+ */
425
+ export function recomputeOpsRoot(events, range) {
426
+ const filter = range.authorFilter && range.authorFilter.length === 1 ? range.authorFilter[0] : null;
427
+ const filterText = filter === null ? null : filter.toText();
428
+ const covered = events
429
+ .filter((e) => e.seq >= range.fromSeq && e.seq <= range.toSeq)
430
+ .filter((e) => filterText === null || e.by.toText() === filterText)
431
+ .sort((a, b) => (a.seq < b.seq ? -1 : a.seq > b.seq ? 1 : 0));
432
+ return { root: opsRootOverEventHashes(covered.map(memoryLogEventHash)), covered };
433
+ }
434
+ /**
435
+ * Check an archive against the receipt (or checkpoint) that attests it.
436
+ *
437
+ * The hashing is ./ledgerfsHash.ts throughout — the file the `ledgerfs-goldens` cross-lock pins
438
+ * against the Motoko encoder. Nothing is reimplemented here, because a second implementation of
439
+ * the hash contract is exactly how an owner ends up being told their evidence is bad when the two
440
+ * copies merely drifted.
441
+ *
442
+ * Never throws on a mismatch: a mismatch is a RESULT to read, and the wording of
443
+ * {@link MemoryArchiveVerification.explanation} deliberately does not accuse the engram.
444
+ */
445
+ export function verifyMemoryArchive(archive, attestation) {
446
+ const notes = [];
447
+ const filtered = (attestation.authorFilter?.length ?? 0) === 1;
448
+ const { root, covered } = recomputeOpsRoot(archive.events, attestation);
449
+ const attested = asBytes(attestation.opsRoot);
450
+ const matches = bytesEqual(root, attested);
451
+ // Guard before subtracting: an inverted range (toSeq below fromSeq) is a malformed attestation,
452
+ // not a width of minus one.
453
+ const expected = filtered || attestation.toSeq < attestation.fromSeq
454
+ ? null
455
+ : Number(attestation.toSeq - attestation.fromSeq + 1n);
456
+ let rangeContiguous = true;
457
+ if (!filtered) {
458
+ let expect = attestation.fromSeq;
459
+ for (const e of covered) {
460
+ if (e.seq !== expect) {
461
+ rangeContiguous = false;
462
+ notes.push(`The archive jumps from seq ${expect - 1n} to ${e.seq}; entries ${expect}–${e.seq - 1n} are not in it.`);
463
+ break;
464
+ }
465
+ expect += 1n;
466
+ }
467
+ if (rangeContiguous && expected !== null && covered.length !== expected) {
468
+ rangeContiguous = false;
469
+ notes.push(`The attestation covers seq ${attestation.fromSeq}–${attestation.toSeq} (${expected} entries) but the archive holds ${covered.length} of them.`);
470
+ }
471
+ }
472
+ else {
473
+ notes.push('The attestation is author-filtered, so entry count and continuity cannot be checked from the archive alone — only the ops root can be.');
474
+ }
475
+ // Linkage between ADJACENT held entries only. The first entry's `prev` points at something the
476
+ // archive does not hold, and checking it against nothing would be theatre.
477
+ let chainLinked = true;
478
+ for (let i = 1; i < covered.length; i++) {
479
+ const prev = covered[i - 1];
480
+ const here = covered[i];
481
+ if (here.seq !== prev.seq + 1n)
482
+ continue; // not adjacent in the chain; nothing to link
483
+ if (!bytesEqual(asBytes(here.prev), memoryLogEventHash(prev))) {
484
+ chainLinked = false;
485
+ notes.push(`Entry ${here.seq} does not link back to entry ${prev.seq}.`);
486
+ break;
487
+ }
488
+ }
489
+ const explanation = matches
490
+ ? null
491
+ : "The ops root recomputed from this archive is not the one the receipt records. Read that as a question, not a verdict: the common causes sit on this side of the wire — an archive missing entries, an archive assembled with a different author filter than the receipt used, or a version skew between this SDK's copy of the hash rules and the engram's. Check the counts and notes above first. If they all line up and the roots still differ, keep the archive and report both roots as they stand rather than condensing anything.";
492
+ return {
493
+ matches,
494
+ computedOpsRoot: toHex(root),
495
+ attestedOpsRoot: toHex(attested),
496
+ eventsCovered: covered.length,
497
+ eventsExpected: expected,
498
+ rangeContiguous,
499
+ chainLinked,
500
+ notes,
501
+ explanation,
502
+ };
503
+ }
504
+ //# sourceMappingURL=memoryArchive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memoryArchive.js","sourceRoot":"","sources":["../src/memoryArchive.ts"],"names":[],"mappings":"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"}
package/dist/migrate.js CHANGED
@@ -2,6 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { execFileSync } from 'child_process';
4
4
  import { EngramClient } from './EngramClient.js';
5
+ import { NPX_CLIENT } from './version.js';
5
6
  function getOpenClawDir() {
6
7
  return process.env['OPENCLAW_CONFIG_DIR'] || path.join(process.env['HOME'] || '', '.openclaw');
7
8
  }
@@ -107,8 +108,34 @@ export async function hasValidPairing(options) {
107
108
  sessionKeyPath: options.sessionKeyPath,
108
109
  host: options.host,
109
110
  });
110
- // Lightweight authenticated query — succeeds only if the operator is registered
111
- await client.listBackups();
111
+ // Ask the engram directly whether it recognises this session key, rather than inferring it
112
+ // from whether some gated read happened to come back.
113
+ //
114
+ // This probe used to call listBackups() and read any resolved promise as "paired". It reached
115
+ // the right verdict only via a carve-out: every unpaired caller — anonymous, revoked,
116
+ // unrelated — already trapped, while a PAIRED agent that merely lacked canReadBackup
117
+ // was handed `[]` and so looked like a paired agent with no backups. That carve-out closed on
118
+ // 2026-08-16; the same agent now gets a refusal, the SDK throws it, and the old probe
119
+ // would call a genuinely paired agent UNPAIRED and send the user back through pairing.
120
+ //
121
+ // ⟨0.16.0, L2.2 — fixed 2026-08-25⟩ getOwnerConsole(), NOT `status().authorized`.
122
+ //
123
+ // The paragraph this replaces was true of v0.15.0 and is false in BOTH halves at HEAD. The
124
+ // engram's `status()` is gone (it split into public `about()` + gated `getOwnerConsole()`),
125
+ // and `About` carries no `authorized` — `grep authorized src/engram/engram.did` finds only
126
+ // prose and an unrelated `authorizedBy`. So the read DECODED FINE and answered `undefined`,
127
+ // and every caller — owner, agent, guardian alike — was told they were UNPAIRED
128
+ // immediately after pairing successfully. This shipped: dist/migrate.js carried it too.
129
+ //
130
+ // `getOwnerConsole` is the gated half of that split, and its gate is #anyRole DESPITE the name
131
+ // (main.mo: `#anyRole, "getOwnerConsole"`; Gated.mo: "owner | active agent | active
132
+ // guardian") — exactly the audience the old flag answered `true` for. Reaching the record AT
133
+ // ALL is the authorization, so an `Ok` IS the recognition and the SDK throws on `#Err`, which
134
+ // the catch below turns into `false`. No sibling flag survives for a caller to misread.
135
+ //
136
+ // This is still a question put to the CANISTER rather than a string-match on refusal copy,
137
+ // which is what the replaced paragraph was rightly defending.
138
+ await client.getOwnerConsole();
112
139
  return true;
113
140
  }
114
141
  catch {
@@ -300,7 +327,7 @@ export function listCheckpoints() {
300
327
  export function rollbackCheckpoint(checkpointId) {
301
328
  const checkpoints = listCheckpoints();
302
329
  if (checkpoints.length === 0) {
303
- throw new Error('No checkpoints to roll back. Run "npx @engramx/client pair" first to create a checkpoint.');
330
+ throw new Error(`No checkpoints to roll back. Run "${NPX_CLIENT} pair" first to create a checkpoint.`);
304
331
  }
305
332
  let target;
306
333
  if (checkpointId) {
@@ -861,7 +888,7 @@ export async function recoverOpenClaw(options) {
861
888
  const latestByType = new Map();
862
889
  for (const backup of finalized) {
863
890
  const existing = latestByType.get(backup.dbType);
864
- if (!existing || backup.createdAt > existing.createdAt) {
891
+ if (!existing || backup.createdAtNs > existing.createdAtNs) {
865
892
  latestByType.set(backup.dbType, backup);
866
893
  }
867
894
  }