@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.
- package/README.md +69 -8
- package/dist/EngramClient.d.ts +60 -16
- package/dist/EngramClient.js +139 -70
- package/dist/EngramClient.js.map +1 -1
- package/dist/MarketClient.js.map +1 -1
- package/dist/ProfileManager.d.ts +3 -19
- package/dist/ProfileManager.js +1 -58
- package/dist/ProfileManager.js.map +1 -1
- package/dist/SessionManager.d.ts +1 -1
- package/dist/SessionManager.js.map +1 -1
- package/dist/actionReceipt.d.ts +8 -0
- package/dist/actionReceipt.js +8 -0
- package/dist/actionReceipt.js.map +1 -1
- package/dist/birthChain.d.ts +87 -0
- package/dist/birthChain.js +233 -0
- package/dist/birthChain.js.map +1 -0
- package/dist/cache.js.map +1 -1
- package/dist/certifiedAudit.d.ts +31 -0
- package/dist/certifiedAudit.js +124 -0
- package/dist/certifiedAudit.js.map +1 -1
- package/dist/claude-setup.d.ts +15 -6
- package/dist/claude-setup.js +30 -17
- package/dist/claude-setup.js.map +1 -1
- package/dist/cli.d.ts +7 -1
- package/dist/cli.js +212 -187
- package/dist/cli.js.map +1 -1
- package/dist/connect-config.d.ts +109 -0
- package/dist/connect-config.js +251 -0
- package/dist/connect-config.js.map +1 -0
- package/dist/encryption.d.ts +4 -0
- package/dist/encryption.js +7 -1
- package/dist/encryption.js.map +1 -1
- package/dist/evidence.d.ts +94 -0
- package/dist/evidence.js +352 -0
- package/dist/evidence.js.map +1 -0
- package/dist/idl.d.ts +21 -3
- package/dist/idl.js +34 -4
- package/dist/idl.js.map +1 -1
- package/dist/index.d.ts +13 -6
- package/dist/index.js +22 -5
- package/dist/index.js.map +1 -1
- package/dist/ledgerfsHash.d.ts +0 -2
- package/dist/ledgerfsHash.js +0 -2
- package/dist/ledgerfsHash.js.map +1 -1
- package/dist/lineage.d.ts +7 -1
- package/dist/lineage.js +7 -1
- package/dist/lineage.js.map +1 -1
- package/dist/memoryArchive.d.ts +77 -98
- package/dist/memoryArchive.js +58 -31
- package/dist/memoryArchive.js.map +1 -1
- package/dist/migrate.d.ts +18 -30
- package/dist/migrate.js +42 -136
- package/dist/migrate.js.map +1 -1
- package/dist/openEngram.d.ts +7 -0
- package/dist/openEngram.js +27 -3
- package/dist/openEngram.js.map +1 -1
- package/dist/openclaw-local.d.ts +86 -0
- package/dist/openclaw-local.js +332 -0
- package/dist/openclaw-local.js.map +1 -0
- package/dist/openclaw-setup.d.ts +82 -3
- package/dist/openclaw-setup.js +162 -50
- package/dist/openclaw-setup.js.map +1 -1
- package/dist/pair.js.map +1 -1
- package/dist/path-utils.d.ts +0 -16
- package/dist/path-utils.js +4 -27
- package/dist/path-utils.js.map +1 -1
- package/dist/publicRecord.d.ts +5 -0
- package/dist/publicRecord.js +52 -0
- package/dist/publicRecord.js.map +1 -0
- package/dist/recordChain.d.ts +69 -0
- package/dist/recordChain.js +145 -0
- package/dist/recordChain.js.map +1 -0
- package/dist/schema.d.ts +11 -22
- package/dist/schema.js +6 -28
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +67 -451
- package/dist/verify.d.ts +8 -0
- package/dist/verify.js +19 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +22 -0
- package/dist/version.js +22 -0
- package/dist/version.js.map +1 -1
- package/package.json +27 -12
- package/dist/generated/engram.idl.d.ts +0 -6
- package/dist/generated/engram.idl.js +0 -1366
- package/dist/generated/engram.idl.js.map +0 -1
- package/dist/generated/market.idl.d.ts +0 -6
- package/dist/generated/market.idl.js +0 -557
- package/dist/generated/market.idl.js.map +0 -1
- package/dist/generated/registry.idl.d.ts +0 -6
- package/dist/generated/registry.idl.js +0 -510
- package/dist/generated/registry.idl.js.map +0 -1
- package/dist/offline.d.ts +0 -21
- package/dist/offline.js +0 -104
- 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
|
|
44
|
-
// the evidence the write landed
|
|
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.
|
|
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
|
package/dist/EngramClient.d.ts
CHANGED
|
@@ -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
|
|
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}.
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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
|
|
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
|
*
|
package/dist/EngramClient.js
CHANGED
|
@@ -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
|
|
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:
|
|
16
|
-
|
|
17
|
-
|
|
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}.
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
*
|