@engramx/client 0.16.0-rc.2 → 0.16.0-rc.20

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 (95) hide show
  1. package/README.md +69 -8
  2. package/dist/EngramClient.d.ts +60 -16
  3. package/dist/EngramClient.js +139 -70
  4. package/dist/EngramClient.js.map +1 -1
  5. package/dist/MarketClient.js.map +1 -1
  6. package/dist/ProfileManager.d.ts +3 -19
  7. package/dist/ProfileManager.js +1 -58
  8. package/dist/ProfileManager.js.map +1 -1
  9. package/dist/SessionManager.d.ts +1 -1
  10. package/dist/SessionManager.js.map +1 -1
  11. package/dist/actionReceipt.d.ts +8 -0
  12. package/dist/actionReceipt.js +8 -0
  13. package/dist/actionReceipt.js.map +1 -1
  14. package/dist/birthChain.d.ts +87 -0
  15. package/dist/birthChain.js +233 -0
  16. package/dist/birthChain.js.map +1 -0
  17. package/dist/cache.js.map +1 -1
  18. package/dist/certifiedAudit.d.ts +31 -0
  19. package/dist/certifiedAudit.js +124 -0
  20. package/dist/certifiedAudit.js.map +1 -1
  21. package/dist/claude-setup.d.ts +15 -6
  22. package/dist/claude-setup.js +30 -17
  23. package/dist/claude-setup.js.map +1 -1
  24. package/dist/cli.d.ts +7 -1
  25. package/dist/cli.js +212 -187
  26. package/dist/cli.js.map +1 -1
  27. package/dist/connect-config.d.ts +109 -0
  28. package/dist/connect-config.js +251 -0
  29. package/dist/connect-config.js.map +1 -0
  30. package/dist/encryption.d.ts +4 -0
  31. package/dist/encryption.js +7 -1
  32. package/dist/encryption.js.map +1 -1
  33. package/dist/evidence.d.ts +94 -0
  34. package/dist/evidence.js +352 -0
  35. package/dist/evidence.js.map +1 -0
  36. package/dist/idl.d.ts +21 -3
  37. package/dist/idl.js +34 -4
  38. package/dist/idl.js.map +1 -1
  39. package/dist/index.d.ts +13 -6
  40. package/dist/index.js +22 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/ledgerfsHash.d.ts +0 -2
  43. package/dist/ledgerfsHash.js +0 -2
  44. package/dist/ledgerfsHash.js.map +1 -1
  45. package/dist/lineage.d.ts +7 -1
  46. package/dist/lineage.js +7 -1
  47. package/dist/lineage.js.map +1 -1
  48. package/dist/memoryArchive.d.ts +77 -98
  49. package/dist/memoryArchive.js +58 -31
  50. package/dist/memoryArchive.js.map +1 -1
  51. package/dist/migrate.d.ts +18 -30
  52. package/dist/migrate.js +42 -136
  53. package/dist/migrate.js.map +1 -1
  54. package/dist/openEngram.d.ts +7 -0
  55. package/dist/openEngram.js +27 -3
  56. package/dist/openEngram.js.map +1 -1
  57. package/dist/openclaw-local.d.ts +86 -0
  58. package/dist/openclaw-local.js +332 -0
  59. package/dist/openclaw-local.js.map +1 -0
  60. package/dist/openclaw-setup.d.ts +82 -3
  61. package/dist/openclaw-setup.js +162 -50
  62. package/dist/openclaw-setup.js.map +1 -1
  63. package/dist/pair.js.map +1 -1
  64. package/dist/path-utils.d.ts +0 -16
  65. package/dist/path-utils.js +4 -27
  66. package/dist/path-utils.js.map +1 -1
  67. package/dist/publicRecord.d.ts +5 -0
  68. package/dist/publicRecord.js +52 -0
  69. package/dist/publicRecord.js.map +1 -0
  70. package/dist/recordChain.d.ts +69 -0
  71. package/dist/recordChain.js +145 -0
  72. package/dist/recordChain.js.map +1 -0
  73. package/dist/schema.d.ts +11 -22
  74. package/dist/schema.js +6 -28
  75. package/dist/schema.js.map +1 -1
  76. package/dist/types.d.ts +67 -451
  77. package/dist/verify.d.ts +8 -0
  78. package/dist/verify.js +19 -0
  79. package/dist/verify.js.map +1 -0
  80. package/dist/version.d.ts +22 -0
  81. package/dist/version.js +22 -0
  82. package/dist/version.js.map +1 -1
  83. package/package.json +27 -12
  84. package/dist/generated/engram.idl.d.ts +0 -6
  85. package/dist/generated/engram.idl.js +0 -1366
  86. package/dist/generated/engram.idl.js.map +0 -1
  87. package/dist/generated/market.idl.d.ts +0 -6
  88. package/dist/generated/market.idl.js +0 -557
  89. package/dist/generated/market.idl.js.map +0 -1
  90. package/dist/generated/registry.idl.d.ts +0 -6
  91. package/dist/generated/registry.idl.js +0 -510
  92. package/dist/generated/registry.idl.js.map +0 -1
  93. package/dist/offline.d.ts +0 -21
  94. package/dist/offline.js +0 -104
  95. package/dist/offline.js.map +0 -1
package/README.md CHANGED
@@ -40,8 +40,11 @@ console.log(`loaded ${ctx.includedFiles}/${ctx.totalFiles} files`);
40
40
 
41
41
  // AS THE AGENT WORKS: append (append-only; the canister concatenates the delta)
42
42
  const receipt = await engram.appendMemory('memory/daily.md', '## Notes\nSomething happened.');
43
- // receipt = { version, seq, head }. `head` is the audit-chain head this write advanced to, hex —
44
- // the evidence the write landed, verifiable by a third party against the chain and its anchor.
43
+ // receipt = { version, seq, head }. `head` is the memory file's chain head after this write, hex —
44
+ // the evidence the write landed. It is not the audit-chain head: this write's audit-log entry
45
+ // records it, so the audit head that getCertifiedAuditHead() certifies, and the registry anchors,
46
+ // covers it without being equal to it. A verifier goes from `head` to that entry, then along the
47
+ // audit chain to a certified head. ⟨2026-10-13: this called `head` the audit-chain head.⟩
45
48
  // Carry it forward: reading the file back later returns content, not proof.
46
49
 
47
50
  // Point reads: one file, or several at once
@@ -54,6 +57,19 @@ const files = await engram.readMemoryBatch(['MEMORY.md', 'memory/daily.md']);
54
57
  // const balance = await engram.walletBalance();
55
58
  ```
56
59
 
60
+ > ⟨2026-10-15⟩ The receipt is wider now: `{ version, seq, head, prev, atNs, contentRoot, size,
61
+ > recordId, recordPrev, sent }`. `prev`, `atNs`, `contentRoot` and `size` are the rest of the memory-file
62
+ > event, so with `sent` (the bytes this write sent) you recompute `head` yourself; `recordId` and
63
+ > `recordPrev` place the write's entry in the engram's record. Capture the evidence right after the
64
+ > write, and anyone can check it later — no key, no owner:
65
+ >
66
+ > ```typescript
67
+ > const bundle = await engram.captureEvidence(receipt, 'memory/daily.md');
68
+ > // fs.writeFileSync('bundle.json', JSON.stringify(bundle));
69
+ > // npx @engramx/client@0.16.0-rc.20 verify bundle.json → each link, the signed time, the entries walked
70
+ > ```
71
+
72
+
57
73
  ### What you can actually call today (2026-08-17)
58
74
 
59
75
  Two things in the snippet above are worth stating plainly rather than letting you discover them.
@@ -72,7 +88,7 @@ in-repo consumer as a reason to report what breaks, not as a warning that it doe
72
88
 
73
89
  The 2026-08-17 charter diet (below) deleted a lot of caller-less methods and **deliberately kept
74
90
  these two.** Zero in-repo callers was the reason to look at them; it was not the reason to decide.
75
- Three facts decided it. They are what `npx @engramx/mcp connect` *instructs* whoever builds a new
91
+ Three facts decided it. They are what `npx @engramx/mcp@0.16.0-rc.20 connect` *instructs* whoever builds a new
76
92
  SDK-lane agent to call, verbatim, in the guide the dashboard's connect page renders — deleting a method
77
93
  while still telling people to call it is the failure this whole exercise exists to prevent.
78
94
  `startSession()` is the only read on the **update** path, so it is the only one the canister can
@@ -98,13 +114,15 @@ Pair your agent with an engram:
98
114
 
99
115
  ```bash
100
116
  # Pair the agent on this machine with the engram and install the OpenClaw plugin
101
- pnpm dlx @engramx/client@0.16.0-rc.2 pair <invite-code> --engram <canister-id> --target openclaw
117
+ pnpm dlx @engramx/client@0.16.0-rc.20 pair <invite-code> --engram <canister-id> --target openclaw
102
118
 
103
119
  # Replacement machine — after pairing, restore memory from the engram
104
- pnpm dlx @engramx/client migrate --engram <canister-id> --recover
120
+ pnpm dlx @engramx/client@0.16.0-rc.20 migrate --engram <canister-id> --recover
105
121
  ```
106
122
 
107
- `pair` generates the key for this engram at `~/.engramx/session.key` (and, on the first pairing, the agent's lasting key at `~/.engramx/root.key`), pairs the agent — the CLI prints `Paired as an agent` — and installs the OpenClaw plugin. `migrate` then moves memory, saving a local checkpoint first — run `npx @engramx/client rollback` to undo. Its default direction pushes local memory files to the engram; `--recover` instead restores database backups, and memory files sync automatically on next OpenClaw startup.
123
+ `pair` generates the key for this engram at `~/.engramx/session.key` (and, on the first pairing, the agent's lasting key at `~/.engramx/root.key`), pairs the agent — the CLI prints `Paired as an agent` — and installs the OpenClaw plugin. `migrate` then moves memory, saving a local checkpoint first — run `npx @engramx/client@0.16.0-rc.20 rollback` to undo. Its default direction pushes local memory files to the engram; `--recover` instead restores database backups, and memory files sync automatically on next OpenClaw startup.
124
+
125
+ ⟨2026-10-13⟩ `pair` moves no memory unless you add `--migrate`, and `--yes` never adds it. Without it, `pair` leaves any OpenClaw memory files on this machine where they are and prints one line: how many it found, and the `migrate` command that moves them. With `--migrate`, it runs that migration after pairing — each file is uploaded, then deleted here, and `rollback` restores them. Through 0.16.0-rc.11, `pair` ran the migration on every pairing, whatever the `--target` (Claude too), and answered its own question under `--yes`.
108
126
 
109
127
  ⟨2026-09-29⟩ From code, pairing is one call: `EngramClient.pairAgent(inviteCode, root)`, or sign the
110
128
  statement with `signEnrolment(root, engramId, key, inviteCode)` and call the engram's `pairAgent`
@@ -118,7 +136,7 @@ If the owner of the wallet allows it (a standing allowance they set for this age
118
136
  page), the agent can open an engram of its own through the engram it works in. The owner may be a program. Whoever runs that program is the owner for every purpose on these pages.
119
137
 
120
138
  ```bash
121
- npx @engramx/client open-engram --engram <canister-id>
139
+ npx @engramx/client@0.16.0-rc.20 open-engram --engram <canister-id>
122
140
  ```
123
141
 
124
142
  The engram it works in pays to open it, from its own balance, and the agent's lasting key
@@ -127,7 +145,7 @@ paid in is the agent's from the moment the new engram exists, and nothing is tak
127
145
  reads the allowance first and stops, with nothing spent, when there is none; it never creates a
128
146
  lasting key; and it prints the engram's own sentence when the engram refuses. It then prints the
129
147
  owner kit's command that acts as the new engram's owner with the same file —
130
- `npx @engramx/owner --connect --key-file <that file> --registry <id>`, with both filled in — which
148
+ `npx @engramx/owner@0.16.0-rc.20 --connect --key-file <that file> --registry <id>`, with both filled in — which
131
149
  mints an invite for an agent to pair into it. The agent's lasting key is an owner key for the engram
132
150
  it opened only; on the engram it works in it stays an agent. The agent's work in an engram its own
133
151
  key owns is never counted on its track record. From code: `EngramClient.getChildAllowance(key)`,
@@ -193,6 +211,13 @@ wrapper went. *(16 names)*
193
211
  `getAgentWriteReview`, `markAgentWritesReviewed` and `getEngramCard` (the vocabulary change: owner,
194
212
  agent, guardian).
195
213
 
214
+ ⟨2026-10-07⟩ The memory-log walk is still exported, as a module function rather than a class method:
215
+ `assembleMemoryArchive(fetchPage, options)` takes the engram's own `listMemoryLog` as `fetchPage` —
216
+ an owner-authenticated actor's, bound (`actor.listMemoryLog.bind(actor)`) — reads the page exactly as
217
+ the engram serves it (`{ firstAvailableSeq; head; page : { items; total; withheld } }`, advancing by
218
+ `items.length + withheld`), and `verifyMemoryArchive` checks the archive against an export receipt.
219
+ The `assembleMemoryArchive` in group 4 below is the class method that left.
220
+
196
221
  **2. Renamed on the engram — the capability survives under a new name.** *(4 names)*
197
222
 
198
223
  | you called | call instead |
@@ -271,6 +296,10 @@ does not expose. They stayed because the encryption runbook instructs whoever mi
271
296
  Deleting a method a runbook tells people to call, while the runbook still tells them to call it, is
272
297
  worse than the blemish. Moving the family to `@engramx/owner` is a two-repo change: the doc rider
273
298
  first, then the methods.
299
+ ⟨2026-10-13⟩ The owner ruled to move it. `git-revert`'s engram step went first: the CLI now reverts
300
+ the local memory repo and prints the owner kit's `--restore` command, because replacing a file is the
301
+ owner's act (`@engramx/owner`, `restoreMemory`). The encryption trio follows with its runbook; it
302
+ needs the owner kit to take this package's encryption module (`@engramx/client/encryption`).
274
303
 
275
304
  **The removals are a break.** A method this class no longer has fails at the call site, loudly,
276
305
  with a `TypeError`; a method the SDK no longer *declares* to the canister fails as a catchable
@@ -283,6 +312,9 @@ approval-request reads, an agent's key-hash rotation, the whole backup lane, and
283
312
  marketplace: `listServices` → `submitServiceRequestWithPayment` → `pollJobResult`/`getJob` →
284
313
  `getJobResult`, with `confirmJob`/`disputeJob` to settle. From here, no method returns to this class
285
314
  without a named caller. "A seller might want it" is how the last two dozen got here.
315
+ ⟨CORRECTED 2026-10-13⟩ The buyer half is not on this class: it moved to `MarketClient` (the market
316
+ is its own canister) when `payAndCall` was deleted on 2026-09-30 — `listServices` → `createJob` →
317
+ `pollJobStatus`/`getJob` → `getJobResult`, with `confirmJob`/`disputeJob` to settle.
286
318
 
287
319
  ## Backup API
288
320
 
@@ -348,10 +380,39 @@ ever empty again, such a call would report `'indeterminate'` by design — the S
348
380
  canonicity.) Pass `{ registryId }` to ask a different fleet (e.g. staging); the answer is then
349
381
  _that fleet's_ testimony, not canonicity.
350
382
 
383
+ ## Verify a birth chain ⟨2026-10-26⟩
384
+
385
+ Every engram a registry opens gets a **birth record**: its id, the engram it was opened from (none
386
+ for a first engram), its engram number, its birth time, a random secret, and the fingerprint of its
387
+ parent's record. The engram holds its own record and copies of its ancestors' records, and serves
388
+ them to its **owner only** (`getBirthChain`) — the owner decides whom to show them to. The registry
389
+ certifies each record's fingerprint, and nothing else, in its certified tree (`getCertifiedBirth`,
390
+ public: it reveals only whether a fingerprint is present). A record holds no person's identity,
391
+ memory or wallet data.
392
+
393
+ ```typescript
394
+ import { verifyBirthChain, fromCandidBirthRecord } from '@engramx/client/verify';
395
+
396
+ const reply = await engram.getBirthChain(); // as the engram's owner
397
+ if ('Err' in reply) throw new Error(JSON.stringify(reply.Err));
398
+ const records = reply.Ok.map(fromCandidBirthRecord);
399
+ // The registry's witnesses are fetched for you (`registryBirthWitness`); pass `fetchWitness` to
400
+ // supply your own.
401
+ const verdict = await verifyBirthChain(agent, registryId, records, { expectChild: engramId });
402
+ // verdict.verified — every record certified against the IC root key AND every link holds
403
+ // verdict.complete — the chain starts at a first engram (a part-way run is still verifiable)
404
+ // verdict.links — per record: certified or not, and why
405
+ ```
406
+
407
+ Fingerprints are recomputed from the canonical encoding (`encodeBirthRecord`), never read off the
408
+ wire. There is no certified proof of absence: an uncertified link is "not proven", not "proven false".
409
+
351
410
  ## OpenClaw Integration
352
411
 
353
412
  The `--target openclaw` option on `pair` handles plugin installation and `openclaw.json` configuration automatically. For manual setup or customization, see the README of the `@engramx/openclaw` package on npm.
354
413
 
414
+ ⟨2026-10-13⟩ It installs the plugin released with this package, at the same version. If a different version is already installed, `pair` stops before it pairs, names that version, and prints the command that upgrades it; the invite is not used, so the same command runs again afterwards. When it installs nothing (OpenClaw's gateway is not running, or both installers fail), it says so and why, and prints the command that installs the plugin. Until this day it printed "Installed EngramX plugin for OpenClaw" in every case.
415
+
355
416
  ## Maturity of the payment and identity surfaces
356
417
 
357
418
  Verified against the canister interface on 2026-08-16. "Stubbed" was doing too much work here, so
@@ -2,7 +2,8 @@ import { type CertifiedAuditHead, type VerifiedAuditHead } from './certifiedAudi
2
2
  import { Ed25519KeyIdentity } from '@icp-sdk/core/identity';
3
3
  import { Principal } from '@icp-sdk/core/principal';
4
4
  import { idlFactory } from './idl.js';
5
- import type { MemoryHistory, MemoryRangePage, MemoryFile, MemoryFileInfo, SessionContext, Pairing, AuditPage, AgentRecord, ChildAllowanceRow, EngramStatus, OwnerConsole, GuardianRecord, BackupMetadata, BackupType, ApprovalRequest } from './types.js';
5
+ import { type EvidenceBundle } from './evidence.js';
6
+ import type { MemoryHistory, MemoryRangePage, MemoryFile, MemoryFileInfo, SessionContext, Pairing, AuditPage, AgentRecord, AgentGrant, ChildAllowanceRow, EngramStatus, OwnerConsole, GuardianRecord, BackupMetadata, BackupRetention, BackupType, ApprovalRequest } from './types.js';
6
7
  /**
7
8
  * Source class declared on an append — recorded permanently in the engram's audit chain beside
8
9
  * this agent's principal. Declared by the caller, NOT verified by the canister; a false
@@ -37,6 +38,21 @@ export interface MemoryReceipt {
37
38
  seq: bigint;
38
39
  /** Chain head after this write, hex. The thing to hand a verifier. */
39
40
  head: string;
41
+ /** The memory-file chain head before this write, hex. */
42
+ prev: string;
43
+ /** The write's time (nanoseconds), as the event committed it. */
44
+ atNs: bigint;
45
+ /** The file's content root after the write, hex. */
46
+ contentRoot: string;
47
+ /** The file's size after the write, in bytes. */
48
+ size: bigint;
49
+ /** The id of this write's entry in the engram's chained record. */
50
+ recordId: bigint;
51
+ /** The record head just before that entry, hex (empty for the record's first entry). */
52
+ recordPrev: string;
53
+ /** The bytes this write sent: the appended delta (separator and any encryption included), or the
54
+ * whole new content of a revise. What the memory-file event commits to. */
55
+ sent: Uint8Array;
40
56
  }
41
57
  export interface EngramClientConfig {
42
58
  /** The engram canister ID */
@@ -51,8 +67,6 @@ export interface EngramClientConfig {
51
67
  cacheMaxAge?: number;
52
68
  /** Max cache entries. Default: 100 */
53
69
  cacheMaxSize?: number;
54
- /** Path for offline queue. Default: null (disabled) */
55
- offlineQueuePath?: string;
56
70
  /** Encrypt memory content client-side via vetKeys. Default: false (plaintext).
57
71
  * Enable for new/migrated engrams; the canister must support vetKD. Reads of
58
72
  * legacy plaintext keep working regardless. */
@@ -73,7 +87,6 @@ export declare class EngramClient {
73
87
  private agent;
74
88
  private actor;
75
89
  private cache;
76
- private offlineQueue?;
77
90
  private identity;
78
91
  private canisterId;
79
92
  private encryption;
@@ -109,13 +122,12 @@ export declare class EngramClient {
109
122
  /**
110
123
  * Append `content` to a memory file (append-only; the canister concatenates the delta).
111
124
  *
112
- * @returns the write {@link MemoryReceipt}. **Special case:** if an `offlineQueue` is configured
113
- * and the network call fails with a transport error, the encoded delta is queued for a later
114
- * `syncOfflineQueue()` and this returns `null` — there is no receipt for a write that has not
115
- * happened, and a caller that surfaces one must say "queued", not report a version. Deterministic
116
- * errors (validation, permission, writes-paused, non-framed base in encrypted mode) always throw.
125
+ * @returns the write {@link MemoryReceipt}. Every failure throws — a transport error, and the
126
+ * deterministic ones (validation, permission, writes-paused, non-framed base in encrypted mode).
127
+ * ⟨2026-10-13, the owner's 4A⟩ It returned `MemoryReceipt | null` until then, null meaning
128
+ * "deferred to the offline queue"; the queue had no consumer that could switch it on and went.
117
129
  */
118
- appendMemory(path: string, content: string, source?: MemorySource): Promise<MemoryReceipt | null>;
130
+ appendMemory(path: string, content: string, source?: MemorySource): Promise<MemoryReceipt>;
119
131
  /**
120
132
  * The engram's file listing.
121
133
  *
@@ -234,7 +246,10 @@ export declare class EngramClient {
234
246
  * Same class as the readAuditLog envelope defect corrected 2026-08-20 two methods below,
235
247
  * and caught the same way: by a live call, because tsc has nothing to check here.
236
248
  * This method is the CYCLES projection its name and both MCP catalogs promise.
237
- * Corrected 2026-08-25. */
249
+ * Corrected 2026-08-25.
250
+ * ⟨2026-10-13, the owner's 3A⟩ tsc has something to check now: the actor is typed with the
251
+ * generated interface (idl.ts · SdkEngramService) and unwrap returns the reply's own Ok type, so
252
+ * this defect would not compile today. */
238
253
  walletBalance(): Promise<bigint>;
239
254
  /** ⟨0.16.0, L1 + L3⟩ Paged and enveloped. The wire method moved to `listAuditEntries` and the
240
255
  * CALL followed it, but the RESULT HANDLING did not: this returned the raw
@@ -244,11 +259,15 @@ export declare class EngramClient {
244
259
  * `this.actor` is `ActorSubclass<any>`, so the declared type is an unchecked assertion and tsc
245
260
  * had nothing to catch it with: the rename compiled and looked done.
246
261
  * Corrected 2026-08-20 to the idiom every sibling here already uses.
262
+ * ⟨2026-10-13⟩ The actor is typed now (idl.ts · SdkEngramService); see walletBalance above.
247
263
  *
248
264
  * ⟨CORRECTED 2026-09-29, owner ruling follow-up 16 (B)⟩ Returns the PAGE, not `.items`. An agent
249
265
  * now reads only the entries it caused, so a window of the record comes back with `withheld`
250
266
  * counting what it was not shown; handing back the bare array would let an agent mistake its part
251
- * of the record for the whole. `limit` is the window's span, not a row count. */
267
+ * of the record for the whole. `limit` is the window's span, not a row count.
268
+ * ⟨2026-10-18⟩ Since the owner's ruling 6B of 2026-10-14 the rest of the window comes back in
269
+ * `sealed` as hash-only rows (id, digest, entry hash) in place of the `withheld` count, so
270
+ * `items + sealed` is the window examined; see `AuditPage`. */
252
271
  readAuditLog(fromIndex?: number, limit?: number): Promise<AuditPage>;
253
272
  /** How many entries the chained audit log currently holds — the page bound for
254
273
  * {@link readAuditLog}.
@@ -270,6 +289,21 @@ export declare class EngramClient {
270
289
  /** The chained audit-log head plus the IC certificate over it (raw, unverified). Use
271
290
  * getVerifiedAuditHead() to verify it against the IC root key. */
272
291
  getCertifiedAuditHead(): Promise<CertifiedAuditHead>;
292
+ /**
293
+ * ⟨2026-10-15, the owner's ruling 6B of 2026-10-14⟩ Capture the evidence of a write this client
294
+ * just made, as a bundle anyone can check later with `engramx verify` — with no owner, no key and
295
+ * no account.
296
+ *
297
+ * Call it right after the write: it reads the certified record head NOW (the certificate's signed
298
+ * time is the time the bundle proves), reads this writer's own record entry back (an agent reads
299
+ * the entries it caused), and the hash-only rows from that entry to the captured head. A head that
300
+ * does not yet cover the write (a lagging replica) is read again, up to five times.
301
+ *
302
+ * THROWS when the evidence cannot be made — never a bundle that would fail its own check: when the
303
+ * entry cannot be read back in full by this caller (a write made through a paid session is logged
304
+ * under the session's payer), or the head never covers the write.
305
+ */
306
+ captureEvidence(receipt: MemoryReceipt, path: string): Promise<EvidenceBundle>;
273
307
  /** Fetch the certified audit head and VERIFY it against the IC root key — returns the certified
274
308
  * head bytes and whether the replica's claim agrees, WITHOUT trusting this replica. */
275
309
  getVerifiedAuditHead(): Promise<VerifiedAuditHead>;
@@ -300,6 +334,13 @@ export declare class EngramClient {
300
334
  * Terminal for this principal on this engram: pairing is one-shot, so working for it again
301
335
  * needs a new principal and a fresh invite. Throws on a refusal (not an agent, already ended). */
302
336
  resignAgent(): Promise<void>;
337
+ /** ⟨2026-10-21, round 11 g⟩ THIS agent's own grant, as the engram enforces it against the key
338
+ * this client signs with: what it may read, where it may write (`permissions.scopes`, the
339
+ * `memory.append:<path>` rules — none means anywhere it could before), its rate
340
+ * (`permissions.callsPerMinute`, 0 = no limit) and its payment limits. Read it before writing to
341
+ * avoid a refusal the owner then has to explain. Only the caller's own grant — never another
342
+ * agent's. Throws on a refusal: the owner, a revoked or resigned agent, a guardian or a stranger. */
343
+ myGrant(): Promise<AgentGrant>;
303
344
  /** ⟨2026-09-29, the owner's ruling of that date (follow-up 10, option A)⟩ Step one of replacing
304
345
  * THIS agent's key on the engram: the key this client signs with nominates `newKey`. Nothing
305
346
  * moves until the new key accepts (`acceptAgentKeyNomination`, called by a client that signs with
@@ -357,6 +398,13 @@ export declare class EngramClient {
357
398
  * chunk, then check the reassembled sha256 against it before applying anything. A restore that
358
399
  * cannot state the hash it expected is not a restore. */
359
400
  getBackupMetadata(backupId: string): Promise<BackupMetadata>;
401
+ /** ⟨2026-10-21, the owner's round-11 answers h and m⟩ How many backups of `dbType` the engram
402
+ * keeps for EACH creator — this agent's own included — and whether that number is the engram's
403
+ * default (`isDefault`, the owner never set one). Since that day the cap is per creator: this
404
+ * agent's finalize prunes only this agent's own oldest backups beyond it, never the owner's or
405
+ * another agent's. Needs the upload permission (`canAppendMemory`); throws on a refusal — a
406
+ * reader-only agent, a revoked or resigned one, a guardian, a stranger — never a made-up number. */
407
+ getBackupRetention(dbType: string): Promise<BackupRetention>;
360
408
  latestBackupMatchesHash(dbType: string, sha256: string): Promise<BackupMetadata | null>;
361
409
  /** Encrypt a backup payload IFF this engram is in Private mode — the SAME toggle that governs
362
410
  * memory files governs backups (encrypt files ⇒ encrypt backups). Produces a whole-blob
@@ -417,10 +465,6 @@ export declare class EngramClient {
417
465
  total: number;
418
466
  failed: string[];
419
467
  }>;
420
- syncOfflineQueue(): Promise<{
421
- synced: number;
422
- failed: number;
423
- }>;
424
468
  /**
425
469
  * Redeem an invite as the key for this engram, enrolled under the agent's lasting key (`root`).
426
470
  *
@@ -3,20 +3,28 @@ import { verifyCertifiedAuditHead, } from './certifiedAudit.js';
3
3
  import { Principal } from '@icp-sdk/core/principal';
4
4
  import { LRUCache } from './cache.js';
5
5
  import { idlFactory } from './idl.js';
6
- import { OfflineQueue } from './offline.js';
7
6
  import { loadSessionKey, identityFromSeed } from './auth.js';
8
7
  import { signEnrolment } from './actionReceipt.js';
9
8
  import { unwrapResult, errorMessage } from './wire.js';
10
9
  import { EncryptionManager } from './encryption.js';
10
+ import { EVIDENCE_FORMAT } from './evidence.js';
11
+ import { sealedRowOf } from './recordChain.js';
11
12
  const memorySourceArg = (s) => s === undefined ? [] : [{ [s]: null }];
12
- const receiptOf = (r) => ({
13
+ const hexOf = (b) => Array.from(b)
14
+ .map((x) => x.toString(16).padStart(2, '0'))
15
+ .join('');
16
+ const receiptOf = (r, sent) => ({
13
17
  version: r.version,
14
18
  seq: r.seq,
15
- head: Array.from(r.head)
16
- .map((b) => b.toString(16).padStart(2, '0'))
17
- .join(''),
19
+ head: hexOf(r.head),
20
+ prev: hexOf(r.prev),
21
+ atNs: r.atNs,
22
+ contentRoot: hexOf(r.contentRoot),
23
+ size: r.size,
24
+ recordId: r.recordId,
25
+ recordPrev: hexOf(r.recordPrev),
26
+ sent,
18
27
  });
19
- const ALLOWED_OFFLINE_METHODS = new Set(['appendMemory']);
20
28
  function validateMemoryPath(p) {
21
29
  if (p.includes('\0'))
22
30
  throw new Error('Path contains null byte');
@@ -155,9 +163,6 @@ export class EngramClient {
155
163
  });
156
164
  // If the caller didn't force a mode, sync it from the canister's flag lazily.
157
165
  this.encryptionModeSynced = config.encryptMemory !== undefined;
158
- if (config.offlineQueuePath) {
159
- this.offlineQueue = new OfflineQueue(config.offlineQueuePath);
160
- }
161
166
  }
162
167
  async readMemory(path) {
163
168
  validateMemoryPath(path);
@@ -198,11 +203,10 @@ export class EngramClient {
198
203
  /**
199
204
  * Append `content` to a memory file (append-only; the canister concatenates the delta).
200
205
  *
201
- * @returns the write {@link MemoryReceipt}. **Special case:** if an `offlineQueue` is configured
202
- * and the network call fails with a transport error, the encoded delta is queued for a later
203
- * `syncOfflineQueue()` and this returns `null` — there is no receipt for a write that has not
204
- * happened, and a caller that surfaces one must say "queued", not report a version. Deterministic
205
- * errors (validation, permission, writes-paused, non-framed base in encrypted mode) always throw.
206
+ * @returns the write {@link MemoryReceipt}. Every failure throws — a transport error, and the
207
+ * deterministic ones (validation, permission, writes-paused, non-framed base in encrypted mode).
208
+ * ⟨2026-10-13, the owner's 4A⟩ It returned `MemoryReceipt | null` until then, null meaning
209
+ * "deferred to the offline queue"; the queue had no consumer that could switch it on and went.
206
210
  */
207
211
  async appendMemory(path, content, source) {
208
212
  validateMemoryPath(path);
@@ -246,32 +250,7 @@ export class EngramClient {
246
250
  const deltaText = nonEmpty ? `\n${content}` : content;
247
251
  // Plaintext: raw UTF-8 delta. Encrypted: a length-framed, individually-sealed segment.
248
252
  const delta = await this.encryption.encodeAppendDelta(this.actor, deltaText);
249
- // Only the NETWORK call is wrapped for offline deferral. Everything above (validation, encryption
250
- // mode + framing checks, delta encoding) and the unwrap below throw DETERMINISTIC / logic errors
251
- // (bad path, non-framed base, writes-paused, permission denied) that MUST surface — they must never
252
- // be swallowed into the offline queue and reported as a fake success.
253
- let rawResult;
254
- try {
255
- rawResult = await this.actor.appendMemory(path, delta, memorySourceArg(source));
256
- }
257
- catch (err) {
258
- if (this.offlineQueue) {
259
- // Queue the ALREADY-ENCODED delta bytes (separator + framing baked in) so the replay sends a
260
- // valid Candid `vec nat8`, not a raw string, and does not re-run the read-dependent framing.
261
- this.offlineQueue.enqueue('appendMemory', [
262
- path,
263
- Array.from(delta),
264
- memorySourceArg(source),
265
- ]);
266
- // null, NOT a zeroed receipt. The old sentinel was BigInt(0) — a version number that never
267
- // existed — and under the receipt shape the equivalent would be a record with an empty
268
- // head, which is indistinguishable from a real receipt to a caller that does not check.
269
- // There is no receipt for a write that has not happened; null says exactly that.
270
- return null;
271
- }
272
- throw err;
273
- }
274
- const receipt = receiptOf(this.unwrap(rawResult));
253
+ const receipt = receiptOf(this.unwrap(await this.actor.appendMemory(path, delta, memorySourceArg(source))), Uint8Array.from(delta));
275
254
  this.cache.invalidate(`mem:${path}`);
276
255
  return receipt;
277
256
  }
@@ -304,7 +283,7 @@ export class EngramClient {
304
283
  // errorMessage, NOT `new Error(r.Err)`: Err is a six-armed VARIANT, and stringifying it
305
284
  // yields "[object Object]" — it compiles, it runs, and it destroys the message. Same
306
285
  // defect the shared unwrap was written for; this call site had been missed.
307
- if (r.Err !== undefined)
286
+ if ('Err' in r)
308
287
  return [p, new Error(errorMessage(r.Err))];
309
288
  const raw = r.Ok;
310
289
  try {
@@ -524,7 +503,7 @@ export class EngramClient {
524
503
  const bytes = this.encryption.isEnabled()
525
504
  ? await this.encryption.encodeFramedSegmentBytes(this.actor, raw)
526
505
  : raw;
527
- const receipt = receiptOf(this.unwrap(await this.actor.reviseMemory(path, bytes)));
506
+ const receipt = receiptOf(this.unwrap(await this.actor.reviseMemory(path, bytes)), Uint8Array.from(bytes));
528
507
  // Revise REPLACES content, so a stale cached read is not merely short — it is the wrong
529
508
  // content entirely, served confidently for the rest of the TTL.
530
509
  this.cache.invalidate(`mem:${path}`);
@@ -553,7 +532,10 @@ export class EngramClient {
553
532
  * Same class as the readAuditLog envelope defect corrected 2026-08-20 two methods below,
554
533
  * and caught the same way: by a live call, because tsc has nothing to check here.
555
534
  * This method is the CYCLES projection its name and both MCP catalogs promise.
556
- * Corrected 2026-08-25. */
535
+ * Corrected 2026-08-25.
536
+ * ⟨2026-10-13, the owner's 3A⟩ tsc has something to check now: the actor is typed with the
537
+ * generated interface (idl.ts · SdkEngramService) and unwrap returns the reply's own Ok type, so
538
+ * this defect would not compile today. */
557
539
  async walletBalance() {
558
540
  return this.unwrap(await this.actor.getWalletBalances(), 'getWalletBalances').cycles;
559
541
  }
@@ -569,11 +551,15 @@ export class EngramClient {
569
551
  * `this.actor` is `ActorSubclass<any>`, so the declared type is an unchecked assertion and tsc
570
552
  * had nothing to catch it with: the rename compiled and looked done.
571
553
  * Corrected 2026-08-20 to the idiom every sibling here already uses.
554
+ * ⟨2026-10-13⟩ The actor is typed now (idl.ts · SdkEngramService); see walletBalance above.
572
555
  *
573
556
  * ⟨CORRECTED 2026-09-29, owner ruling follow-up 16 (B)⟩ Returns the PAGE, not `.items`. An agent
574
557
  * now reads only the entries it caused, so a window of the record comes back with `withheld`
575
558
  * counting what it was not shown; handing back the bare array would let an agent mistake its part
576
- * of the record for the whole. `limit` is the window's span, not a row count. */
559
+ * of the record for the whole. `limit` is the window's span, not a row count.
560
+ * ⟨2026-10-18⟩ Since the owner's ruling 6B of 2026-10-14 the rest of the window comes back in
561
+ * `sealed` as hash-only rows (id, digest, entry hash) in place of the `withheld` count, so
562
+ * `items + sealed` is the window examined; see `AuditPage`. */
577
563
  async readAuditLog(fromIndex = 0, limit = 50) {
578
564
  return this.unwrap(await this.actor.listAuditEntries(BigInt(fromIndex), BigInt(limit)), 'listAuditEntries');
579
565
  }
@@ -607,6 +593,94 @@ export class EngramClient {
607
593
  certificate: r.certificate.length ? new Uint8Array(r.certificate[0]) : null,
608
594
  };
609
595
  }
596
+ /**
597
+ * ⟨2026-10-15, the owner's ruling 6B of 2026-10-14⟩ Capture the evidence of a write this client
598
+ * just made, as a bundle anyone can check later with `engramx verify` — with no owner, no key and
599
+ * no account.
600
+ *
601
+ * Call it right after the write: it reads the certified record head NOW (the certificate's signed
602
+ * time is the time the bundle proves), reads this writer's own record entry back (an agent reads
603
+ * the entries it caused), and the hash-only rows from that entry to the captured head. A head that
604
+ * does not yet cover the write (a lagging replica) is read again, up to five times.
605
+ *
606
+ * THROWS when the evidence cannot be made — never a bundle that would fail its own check: when the
607
+ * entry cannot be read back in full by this caller (a write made through a paid session is logged
608
+ * under the session's payer), or the head never covers the write.
609
+ */
610
+ async captureEvidence(receipt, path) {
611
+ let head = await this.getCertifiedAuditHead();
612
+ for (let tries = 0; head.total <= receipt.recordId && tries < 5; tries++) {
613
+ await new Promise((r) => setTimeout(r, 1000));
614
+ head = await this.getCertifiedAuditHead();
615
+ }
616
+ if (head.total <= receipt.recordId) {
617
+ throw new Error(`captureEvidence: the certified head covers ${head.total} entries and this write's entry is ${receipt.recordId} — the replica has not caught up; try again`);
618
+ }
619
+ if (!head.certificate)
620
+ throw new Error('captureEvidence: the head came back without a certificate');
621
+ const own = this.unwrap(await this.actor.listAuditEntries(receipt.recordId, 1n), 'listAuditEntries');
622
+ const entry = own.items.find((e) => e.id === receipt.recordId);
623
+ if (!entry) {
624
+ throw new Error(`captureEvidence: this caller cannot read record entry ${receipt.recordId} in full (it was logged under another principal), so it cannot make the bundle; the owner can`);
625
+ }
626
+ const rows = [];
627
+ let next = receipt.recordId + 1n;
628
+ while (next < head.total) {
629
+ const page = this.unwrap(await this.actor.listAuditEntries(next, head.total - next), 'listAuditEntries');
630
+ const got = page.sealed.map((r) => ({
631
+ id: r.id,
632
+ digest: Uint8Array.from(r.digest),
633
+ entryHash: Uint8Array.from(r.entryHash),
634
+ }));
635
+ for (const e of page.items)
636
+ got.push(await sealedRowOf(e));
637
+ got.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
638
+ if (got.length === 0)
639
+ break;
640
+ for (const r of got) {
641
+ if (r.id >= head.total)
642
+ break;
643
+ rows.push({ id: r.id.toString(), digest: hexOf(r.digest), entryHash: hexOf(r.entryHash) });
644
+ }
645
+ next = got[got.length - 1].id + 1n;
646
+ }
647
+ return {
648
+ format: EVIDENCE_FORMAT,
649
+ canisterId: this.canisterId,
650
+ entry: {
651
+ id: entry.id.toString(),
652
+ timestampNs: entry.timestampNs.toString(),
653
+ caller: entry.caller.toText(),
654
+ callerRole: Object.keys(entry.callerRole)[0] ?? '',
655
+ operation: entry.operation,
656
+ details: entry.details,
657
+ success: entry.success,
658
+ entryHash: hexOf(entry.entryHash),
659
+ },
660
+ recordPrev: receipt.recordPrev,
661
+ hashFirstFrom: own.hashFirstFrom.toString(),
662
+ certified: {
663
+ certificate: Buffer.from(head.certificate).toString('base64'),
664
+ headHash: hexOf(head.headHash),
665
+ total: head.total.toString(),
666
+ },
667
+ rows,
668
+ write: {
669
+ path,
670
+ bytes: Buffer.from(receipt.sent).toString('base64'),
671
+ by: this.identity.getPrincipal().toText(),
672
+ receipt: {
673
+ version: receipt.version.toString(),
674
+ seq: receipt.seq.toString(),
675
+ head: receipt.head,
676
+ prev: receipt.prev,
677
+ atNs: receipt.atNs.toString(),
678
+ contentRoot: receipt.contentRoot,
679
+ size: receipt.size.toString(),
680
+ },
681
+ },
682
+ };
683
+ }
610
684
  /** Fetch the certified audit head and VERIFY it against the IC root key — returns the certified
611
685
  * head bytes and whether the replica's claim agrees, WITHOUT trusting this replica. */
612
686
  async getVerifiedAuditHead() {
@@ -649,6 +723,15 @@ export class EngramClient {
649
723
  async resignAgent() {
650
724
  this.unwrap(await this.actor.resignAgent(), 'resignAgent');
651
725
  }
726
+ /** ⟨2026-10-21, round 11 g⟩ THIS agent's own grant, as the engram enforces it against the key
727
+ * this client signs with: what it may read, where it may write (`permissions.scopes`, the
728
+ * `memory.append:<path>` rules — none means anywhere it could before), its rate
729
+ * (`permissions.callsPerMinute`, 0 = no limit) and its payment limits. Read it before writing to
730
+ * avoid a refusal the owner then has to explain. Only the caller's own grant — never another
731
+ * agent's. Throws on a refusal: the owner, a revoked or resigned agent, a guardian or a stranger. */
732
+ async myGrant() {
733
+ return this.unwrap(await this.actor.myGrant(), 'myGrant');
734
+ }
652
735
  /** ⟨2026-09-29, the owner's ruling of that date (follow-up 10, option A)⟩ Step one of replacing
653
736
  * THIS agent's key on the engram: the key this client signs with nominates `newKey`. Nothing
654
737
  * moves until the new key accepts (`acceptAgentKeyNomination`, called by a client that signs with
@@ -761,7 +844,9 @@ export class EngramClient {
761
844
  return dbTypeFilter ? page.items.filter((b) => b.dbType === dbTypeFilter) : page.items;
762
845
  }
763
846
  async pullBackupChunk(backupId, chunkIndex) {
764
- return this.unwrap(await this.actor.backupPullChunk(backupId, BigInt(chunkIndex)));
847
+ const chunk = this.unwrap(await this.actor.backupPullChunk(backupId, BigInt(chunkIndex)));
848
+ // A blob is typed `Uint8Array | number[]` by the generator; the agent decodes it to a Uint8Array.
849
+ return chunk instanceof Uint8Array ? chunk : Uint8Array.from(chunk);
765
850
  }
766
851
  /** One backup's metadata by id. Kept alongside listBackups/pullBackupChunk because
767
852
  * RECOVERY-RUNBOOK §"Restore" names exactly that chain: list or fetch the metadata, pull each
@@ -770,6 +855,15 @@ export class EngramClient {
770
855
  async getBackupMetadata(backupId) {
771
856
  return this.unwrap(await this.actor.getBackupMetadata(backupId));
772
857
  }
858
+ /** ⟨2026-10-21, the owner's round-11 answers h and m⟩ How many backups of `dbType` the engram
859
+ * keeps for EACH creator — this agent's own included — and whether that number is the engram's
860
+ * default (`isDefault`, the owner never set one). Since that day the cap is per creator: this
861
+ * agent's finalize prunes only this agent's own oldest backups beyond it, never the owner's or
862
+ * another agent's. Needs the upload permission (`canAppendMemory`); throws on a refusal — a
863
+ * reader-only agent, a revoked or resigned one, a guardian, a stranger — never a made-up number. */
864
+ async getBackupRetention(dbType) {
865
+ return this.unwrap(await this.actor.getBackupRetention(dbType), 'getBackupRetention');
866
+ }
773
867
  // deleteBackup and setBackupRetention are owner-only; deleteBackup left in the 2026-08-17
774
868
  // charter diet. An agent that can destroy the owner's backups is the append-only
775
869
  // guarantee inverted — the whole point of the lane is that a compromised agent cannot erase.
@@ -932,31 +1026,6 @@ export class EngramClient {
932
1026
  }
933
1027
  return { migrated, total: files.length, failed };
934
1028
  }
935
- async syncOfflineQueue() {
936
- if (!this.offlineQueue)
937
- return { synced: 0, failed: 0 };
938
- return this.offlineQueue.flush(async (method, args) => {
939
- if (!ALLOWED_OFFLINE_METHODS.has(method)) {
940
- console.error(`engramx: offline queue contains unknown method "${method}" — operation dropped`);
941
- return;
942
- }
943
- const fn = this.actor[method];
944
- if (!fn)
945
- throw new Error(`Unknown method: ${method}`);
946
- // Queue entries persisted by a pre-source-class SDK carry 2 args; the 3-arg IDL would
947
- // throw on arity at encode. Pad with opt-none so an old queue replays as UNTAGGED —
948
- // the same thing it was when written.
949
- if (method === 'appendMemory' && args.length === 2)
950
- args = [...args, []];
951
- const result = await fn(...args);
952
- if (result && typeof result === 'object' && 'Err' in result) {
953
- // Same variant-stringification defect as readMemoryBatch's row mapper: `Err` is a
954
- // six-armed record, and `new Error(obj)` renders "[object Object]" over the one sentence
955
- // a replay failure needs to carry.
956
- throw new Error(errorMessage(result.Err));
957
- }
958
- });
959
- }
960
1029
  /**
961
1030
  * Redeem an invite as the key for this engram, enrolled under the agent's lasting key (`root`).
962
1031
  *