@cello-protocol/daemon 0.0.167 → 0.0.169
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +115 -254
- package/dist/daemon.js.map +1 -1
- package/dist/document-amendment-store.d.ts +94 -46
- package/dist/document-amendment-store.d.ts.map +1 -1
- package/dist/document-amendment-store.js +217 -102
- package/dist/document-amendment-store.js.map +1 -1
- package/dist/document-delivery-transport.d.ts +41 -52
- package/dist/document-delivery-transport.d.ts.map +1 -1
- package/dist/document-delivery-transport.js +6 -156
- package/dist/document-delivery-transport.js.map +1 -1
- package/dist/document-frame-router.d.ts +13 -17
- package/dist/document-frame-router.d.ts.map +1 -1
- package/dist/document-frame-router.js +21 -93
- package/dist/document-frame-router.js.map +1 -1
- package/dist/document-handlers.d.ts +1 -1
- package/dist/document-handlers.d.ts.map +1 -1
- package/dist/document-handlers.js +471 -466
- package/dist/document-handlers.js.map +1 -1
- package/dist/document-handshake.d.ts +4 -1
- package/dist/document-handshake.d.ts.map +1 -1
- package/dist/document-handshake.js +3 -1
- package/dist/document-handshake.js.map +1 -1
- package/dist/document-inbound.d.ts +18 -5
- package/dist/document-inbound.d.ts.map +1 -1
- package/dist/document-inbound.js +94 -110
- package/dist/document-inbound.js.map +1 -1
- package/dist/document-layer.d.ts +28 -67
- package/dist/document-layer.d.ts.map +1 -1
- package/dist/document-layer.js +670 -565
- package/dist/document-layer.js.map +1 -1
- package/dist/document-lifecycle.d.ts +11 -157
- package/dist/document-lifecycle.d.ts.map +1 -1
- package/dist/document-lifecycle.js +26 -561
- package/dist/document-lifecycle.js.map +1 -1
- package/dist/document-publish.d.ts +13 -0
- package/dist/document-publish.d.ts.map +1 -1
- package/dist/document-publish.js +19 -11
- package/dist/document-publish.js.map +1 -1
- package/dist/document-reconcile-engine.d.ts +66 -0
- package/dist/document-reconcile-engine.d.ts.map +1 -0
- package/dist/document-reconcile-engine.js +225 -0
- package/dist/document-reconcile-engine.js.map +1 -0
- package/dist/document-reconcile-scheduler.d.ts +91 -0
- package/dist/document-reconcile-scheduler.d.ts.map +1 -0
- package/dist/document-reconcile-scheduler.js +153 -0
- package/dist/document-reconcile-scheduler.js.map +1 -0
- package/dist/document-rejection.d.ts +2 -0
- package/dist/document-rejection.d.ts.map +1 -1
- package/dist/document-rejection.js +5 -1
- package/dist/document-rejection.js.map +1 -1
- package/dist/document-store.d.ts +18 -220
- package/dist/document-store.d.ts.map +1 -1
- package/dist/document-store.js +95 -717
- package/dist/document-store.js.map +1 -1
- package/dist/initiate-session-handler.d.ts +5 -0
- package/dist/initiate-session-handler.d.ts.map +1 -1
- package/dist/initiate-session-handler.js +1 -0
- package/dist/initiate-session-handler.js.map +1 -1
- package/package.json +5 -5
- package/dist/delivery-sweep-bound.d.ts +0 -79
- package/dist/delivery-sweep-bound.d.ts.map +0 -1
- package/dist/delivery-sweep-bound.js +0 -109
- package/dist/delivery-sweep-bound.js.map +0 -1
- package/dist/document-ack-inbound.d.ts +0 -71
- package/dist/document-ack-inbound.d.ts.map +0 -1
- package/dist/document-ack-inbound.js +0 -210
- package/dist/document-ack-inbound.js.map +0 -1
- package/dist/document-control-notifier.d.ts +0 -107
- package/dist/document-control-notifier.d.ts.map +0 -1
- package/dist/document-control-notifier.js +0 -137
- package/dist/document-control-notifier.js.map +0 -1
- package/dist/document-delivery.d.ts +0 -229
- package/dist/document-delivery.d.ts.map +0 -1
- package/dist/document-delivery.js +0 -449
- package/dist/document-delivery.js.map +0 -1
- package/dist/document-join-store.d.ts +0 -80
- package/dist/document-join-store.d.ts.map +0 -1
- package/dist/document-join-store.js +0 -197
- package/dist/document-join-store.js.map +0 -1
package/dist/document-store.js
CHANGED
|
@@ -30,9 +30,7 @@
|
|
|
30
30
|
* display label and reusable after retirement. The M7 session tables join on `agent_name`; that
|
|
31
31
|
* is a known defect (`DOD-AGENT-ID-JOINKEY-1`), not a precedent to copy.
|
|
32
32
|
*/
|
|
33
|
-
import {
|
|
34
|
-
import { DOCUMENT_AMENDMENTS_CREATE_SQL, walkMembership } from "./document-amendment-store.js";
|
|
35
|
-
import { decodeDocumentAmendment } from "@cello-protocol/protocol-types";
|
|
33
|
+
import { DOCUMENT_ENTRIES_CREATE_SQL, dropLegacyEpochColumn, dropLegacyColumns, } from "./document-amendment-store.js";
|
|
36
34
|
/** Thrown when a read path refuses to materialize over a chain that does not verify. */
|
|
37
35
|
export class DocumentChainError extends Error {
|
|
38
36
|
reason;
|
|
@@ -63,10 +61,12 @@ const CREATE_ENVELOPES_SQL = `
|
|
|
63
61
|
envelope_hash TEXT NOT NULL,
|
|
64
62
|
sender_agent_id TEXT NOT NULL,
|
|
65
63
|
doc_prev_hash TEXT,
|
|
66
|
-
epoch_id INTEGER NOT NULL,
|
|
67
64
|
signature BLOB NOT NULL,
|
|
68
65
|
state_vector BLOB NOT NULL,
|
|
69
66
|
payload BLOB,
|
|
67
|
+
-- SYNC-G1: the author's governance frontier at authoring (JSON array of entry hashes) —
|
|
68
|
+
-- content's causal link to the governance that made it admissible. Inside the signed TBS.
|
|
69
|
+
governance_parents TEXT NOT NULL DEFAULT '[]',
|
|
70
70
|
kind TEXT NOT NULL CHECK (kind IN ('update', 'withdrawal', 'rejection')),
|
|
71
71
|
-- The Yjs clientID the sender SIGNED for (ENVELOPE-1 puts it inside the TBS). Recorded so the
|
|
72
72
|
-- gate's authorship rule has a binding derived from authenticated data rather than from a seam
|
|
@@ -76,33 +76,6 @@ const CREATE_ENVELOPES_SQL = `
|
|
|
76
76
|
references_hash TEXT,
|
|
77
77
|
created_at INTEGER NOT NULL,
|
|
78
78
|
log_index INTEGER NOT NULL,
|
|
79
|
-
-- DELIVERY-1. Pending outbound is DERIVED from these columns rather than held in a queue:
|
|
80
|
-
-- a queue in memory does not survive a restart, and a queue in its own table is a second
|
|
81
|
-
-- source of truth that can disagree with the log about what was sent. "Unacknowledged
|
|
82
|
-
-- envelopes I authored" is the whole definition, and it is a WHERE clause.
|
|
83
|
-
-- TWO facts, and they are genuinely different: delivered_at is when the envelope LEFT (or was
|
|
84
|
-
-- parked for an offline peer), acked_at is when the peer's daemon said it admitted or rejected
|
|
85
|
-
-- it. An earlier version had delivered_at with no real writer — its only assignment was a
|
|
86
|
-
-- COALESCE inside the ack, so it always equalled acked_at and the distinction was one the
|
|
87
|
-
-- schema could not express. It has a writer now (markDelivered), so the distinction is real:
|
|
88
|
-
-- "sent, awaiting confirmation" is exactly the state a store-and-forward transport leaves an
|
|
89
|
-
-- envelope in, and an operator asking why something has not landed needs to tell it from
|
|
90
|
-
-- "never sent".
|
|
91
|
-
delivered_at INTEGER,
|
|
92
|
-
acked_at INTEGER,
|
|
93
|
-
-- A THIRD fact, and it is not either of the two above. abandoned_at is when WE STOPPED TRYING
|
|
94
|
-
-- (the unacked ceiling) — a local decision, and NOT a claim about the peer.
|
|
95
|
-
--
|
|
96
|
-
-- It exists because the ceiling first stopped delivery by calling markAcked, and acked_at means
|
|
97
|
-
-- "the peer's daemon said it admitted or rejected it". Overloading it made withdraw tell the
|
|
98
|
-
-- operator "your peer holds it, so it cannot be withdrawn" about an envelope the peer may never
|
|
99
|
-
-- have seen. Same class as any other false claim of confirmation, and a column is cheaper.
|
|
100
|
-
abandoned_at INTEGER,
|
|
101
|
-
-- How many times delivery has been attempted, and when the next attempt is due. On the row,
|
|
102
|
-
-- because a backoff that resets on restart is not a backoff — a daemon restarting in a
|
|
103
|
-
-- reconnect loop would hammer an unreachable peer at full rate forever.
|
|
104
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
105
|
-
next_attempt_at INTEGER,
|
|
106
79
|
PRIMARY KEY (owner_agent_id, document_id, envelope_hash),
|
|
107
80
|
-- A duplicate index would make ORDER BY log_index non-deterministic, and this log's entire
|
|
108
81
|
-- value is deterministic replay. Two daemons on one DB file (the orphan-process case this
|
|
@@ -153,6 +126,10 @@ const CREATE_QUARANTINE_SQL = `
|
|
|
153
126
|
limit_name TEXT,
|
|
154
127
|
limit_value INTEGER,
|
|
155
128
|
limit_actual INTEGER,
|
|
129
|
+
-- SYNC-R35: the EXACT signed refusal frame, so the refusal can travel by the ordinary
|
|
130
|
+
-- exchange (re-encoding from columns cannot reproduce the signed bytes). Nullable: rows
|
|
131
|
+
-- born before R35 hold no wire, and the exchange simply has nothing to attach for them.
|
|
132
|
+
rejection_wire BLOB,
|
|
156
133
|
created_at INTEGER NOT NULL,
|
|
157
134
|
PRIMARY KEY (owner_agent_id, document_id, rejection_envelope_hash),
|
|
158
135
|
FOREIGN KEY (owner_agent_id, document_id) REFERENCES documents (owner_agent_id, document_id)
|
|
@@ -182,16 +159,6 @@ const CREATE_REJECTIONS_RECEIVED_SQL = `
|
|
|
182
159
|
FOREIGN KEY (owner_agent_id, document_id) REFERENCES documents (owner_agent_id, document_id)
|
|
183
160
|
);
|
|
184
161
|
`;
|
|
185
|
-
/** Mirrors `DocumentLifecycle`'s definition exactly — see the note at the exec site. */
|
|
186
|
-
const CREATE_WITHDRAWALS_SQL = `
|
|
187
|
-
CREATE TABLE IF NOT EXISTS document_withdrawals (
|
|
188
|
-
owner_agent_id TEXT NOT NULL,
|
|
189
|
-
document_id TEXT NOT NULL,
|
|
190
|
-
envelope_hash TEXT NOT NULL,
|
|
191
|
-
created_at INTEGER NOT NULL,
|
|
192
|
-
PRIMARY KEY (owner_agent_id, document_id, envelope_hash)
|
|
193
|
-
);
|
|
194
|
-
`;
|
|
195
162
|
const CREATE_SNAPSHOTS_SQL = `
|
|
196
163
|
CREATE TABLE IF NOT EXISTS document_snapshots (
|
|
197
164
|
owner_agent_id TEXT NOT NULL,
|
|
@@ -219,86 +186,61 @@ export class DocumentStore {
|
|
|
219
186
|
this.#db = db;
|
|
220
187
|
this.#logger = logger;
|
|
221
188
|
this.#db.exec(CREATE_DOCUMENTS_SQL);
|
|
189
|
+
// SYNC-G1 birth-gated column (the consent-migration precedent): a database created before
|
|
190
|
+
// the causal-anchor column exists gains it here, defaulted to the empty frontier — CREATE
|
|
191
|
+
// IF NOT EXISTS cannot add columns, and a client-side migration that fails is unrecoverable
|
|
192
|
+
// on an operator machine, so the ALTER is guarded by the actual table shape.
|
|
193
|
+
const envelopeColumns = this.#db
|
|
194
|
+
.prepare(`PRAGMA table_info(document_envelopes)`)
|
|
195
|
+
.all();
|
|
196
|
+
if (envelopeColumns.length > 0 &&
|
|
197
|
+
!envelopeColumns.some((c) => c.name === "governance_parents")) {
|
|
198
|
+
this.#db.exec(`ALTER TABLE document_envelopes ADD COLUMN governance_parents TEXT NOT NULL DEFAULT '[]'`);
|
|
199
|
+
}
|
|
222
200
|
// M14B / DOD-MP-AMEND-1 — the amendments table this store READS (currentDocumentEpoch);
|
|
223
201
|
// DocumentAmendmentStore owns writes. Shared definition, whichever constructs first wins.
|
|
224
|
-
this.#db.exec(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// bookkeeping over the log — the envelope is the truth, a row here is one holder's
|
|
228
|
-
// outstanding confirmation. Restart-survivable by construction.
|
|
229
|
-
this.#db.exec(`
|
|
230
|
-
CREATE TABLE IF NOT EXISTS document_deliveries (
|
|
231
|
-
owner_agent_id TEXT NOT NULL,
|
|
232
|
-
document_id TEXT NOT NULL,
|
|
233
|
-
envelope_hash TEXT NOT NULL,
|
|
234
|
-
holder_agent_id TEXT NOT NULL,
|
|
235
|
-
delivered_at INTEGER,
|
|
236
|
-
acked_at INTEGER,
|
|
237
|
-
abandoned_at INTEGER,
|
|
238
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
239
|
-
-- SENDS is the ceiling's counter; ATTEMPTS is the schedule's. Conflating them was the
|
|
240
|
-
-- old worker's defect: five quiet deferrals to an offline peer counted as five unacked
|
|
241
|
-
-- sends, and the first real send after they returned abandoned the envelope.
|
|
242
|
-
sends INTEGER NOT NULL DEFAULT 0,
|
|
243
|
-
next_attempt_at INTEGER,
|
|
244
|
-
created_at INTEGER NOT NULL,
|
|
245
|
-
PRIMARY KEY (owner_agent_id, document_id, envelope_hash, holder_agent_id)
|
|
246
|
-
);
|
|
247
|
-
`);
|
|
248
|
-
// DOD-MP-INVITE-FANOUT-1 — the admitting amendment gets the SAME durability a content edit
|
|
249
|
-
// has. It used to be sent best-effort over direct transport: one shot, no row, no retry, no
|
|
250
|
-
// restart survival, so a single failed send lost a membership change permanently while every
|
|
251
|
-
// surface reported success. The governance act that decides who is a party to the document is
|
|
252
|
-
// the last thing that should be less durable than a typo fix.
|
|
253
|
-
//
|
|
254
|
-
// A SEPARATE table rather than a `payload_kind` column on `document_deliveries`: that query is
|
|
255
|
-
// the hot path every content edit rides, and ordering — the only reason to share a queue — is
|
|
256
|
-
// preserved explicitly instead, by draining amendments FIRST in every pass (see
|
|
257
|
-
// DocumentDelivery#run). A holder must apply the amendment before any edit authored at the new
|
|
258
|
-
// epoch, or they refuse that edit as coming from a non-participant, which is the observed
|
|
259
|
-
// symptom.
|
|
260
|
-
this.#db.exec(`
|
|
261
|
-
CREATE TABLE IF NOT EXISTS document_amendment_deliveries (
|
|
262
|
-
owner_agent_id TEXT NOT NULL,
|
|
263
|
-
document_id TEXT NOT NULL,
|
|
264
|
-
amendment_hash TEXT NOT NULL,
|
|
265
|
-
holder_agent_id TEXT NOT NULL,
|
|
266
|
-
-- SENT is 'the bytes left this daemon'. ACKED is 'the holder demonstrably applied it'.
|
|
267
|
-
-- Conflating them was the review's HIGH-2: a successful send means the frame reached their
|
|
268
|
-
-- daemon, and the receiver can still REFUSE to record it (recordAmendment throws on a chain
|
|
269
|
-
-- gap or a failed derivation, and the router logs it and answers nothing). Acking on that
|
|
270
|
-
-- reproduced the original defect through the new machinery.
|
|
271
|
-
sent_at INTEGER,
|
|
272
|
-
acked_at INTEGER,
|
|
273
|
-
-- RETIRED is not delivered. A holder the chain no longer contains is owed nothing, and
|
|
274
|
-
-- recording that as an ack would make the durable record claim a delivery that never
|
|
275
|
-
-- happened.
|
|
276
|
-
retired_at INTEGER,
|
|
277
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
278
|
-
next_attempt_at INTEGER,
|
|
279
|
-
created_at INTEGER NOT NULL,
|
|
280
|
-
PRIMARY KEY (owner_agent_id, document_id, amendment_hash, holder_agent_id)
|
|
281
|
-
);
|
|
282
|
-
`);
|
|
202
|
+
this.#db.exec(`DROP TABLE IF EXISTS document_amendments`);
|
|
203
|
+
this.#db.exec(DOCUMENT_ENTRIES_CREATE_SQL);
|
|
204
|
+
dropLegacyEpochColumn(this.#db, "document_entries");
|
|
283
205
|
this.#db.exec(CREATE_ENVELOPES_SQL);
|
|
206
|
+
dropLegacyEpochColumn(this.#db, "document_envelopes");
|
|
207
|
+
// SYNC-AC3 residue. P4 deleted the delivery worker and its ledgers but left DELIVERY-1's
|
|
208
|
+
// bookkeeping columns standing on the envelope log — no writer, no reader, and exactly the
|
|
209
|
+
// per-recipient DEBT the pivot exists to abolish ("X still owes Y this envelope"). Dead
|
|
210
|
+
// columns are not harmless here: the next author to see `next_attempt_at` in the schema
|
|
211
|
+
// reasonably concludes retry state belongs on the row and rebuilds the machine. Dropped on
|
|
212
|
+
// open, birth-gated like the epoch column beside it.
|
|
213
|
+
dropLegacyColumns(this.#db, "document_envelopes", [
|
|
214
|
+
"delivered_at", "acked_at", "abandoned_at", "attempts", "next_attempt_at",
|
|
215
|
+
]);
|
|
284
216
|
this.#db.exec(CREATE_QUARANTINE_SQL);
|
|
217
|
+
// SYNC-R35, birth-gated like the epoch drops: a database born earlier lacks the column.
|
|
218
|
+
{
|
|
219
|
+
const cols = this.#db.prepare(`PRAGMA table_info(document_quarantine)`).all();
|
|
220
|
+
if (!cols.some((c) => c.name === "rejection_wire")) {
|
|
221
|
+
this.#db.exec(`ALTER TABLE document_quarantine ADD COLUMN rejection_wire BLOB`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
285
224
|
this.#db.exec(CREATE_REJECTIONS_RECEIVED_SQL);
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
225
|
+
// D9 residue sweep (review F5): withdrawals have no reader or writer left.
|
|
226
|
+
this.#db.exec(`DROP TABLE IF EXISTS document_withdrawals`);
|
|
227
|
+
// SYNC-P5 (spec §9): the per-party DISPLAY CACHE — last successful exchange and the
|
|
228
|
+
// position that party last claimed. Non-authoritative by construction: no correctness
|
|
229
|
+
// decision reads it (R44); it exists so `cello_doc_list` can say in_sync|behind|unseen
|
|
230
|
+
// without initiating an exchange per row.
|
|
231
|
+
this.#db.exec(`
|
|
232
|
+
CREATE TABLE IF NOT EXISTS document_party_view (
|
|
233
|
+
owner_agent_id TEXT NOT NULL,
|
|
234
|
+
document_id TEXT NOT NULL,
|
|
235
|
+
party_agent_id TEXT NOT NULL,
|
|
236
|
+
last_exchange_ms INTEGER NOT NULL,
|
|
237
|
+
gov_seqs TEXT NOT NULL,
|
|
238
|
+
content_counts TEXT NOT NULL,
|
|
239
|
+
refused TEXT NOT NULL,
|
|
240
|
+
PRIMARY KEY (owner_agent_id, document_id, party_agent_id)
|
|
241
|
+
);
|
|
242
|
+
`);
|
|
292
243
|
this.#db.exec(CREATE_SNAPSHOTS_SQL);
|
|
293
|
-
// COLUMN BIRTH. A daemon that already holds an envelope log must gain the column without
|
|
294
|
-
// losing the log. `ALTER TABLE ... ADD COLUMN` throws when it is already there, which is the
|
|
295
|
-
// guard — the same pattern `document-handshake.ts` uses, rather than a version number nobody
|
|
296
|
-
// maintains.
|
|
297
|
-
addColumnIfMissing(this.#db, this.#logger, {
|
|
298
|
-
table: "document_envelopes",
|
|
299
|
-
column: "abandoned_at",
|
|
300
|
-
sql: "ALTER TABLE document_envelopes ADD COLUMN abandoned_at INTEGER",
|
|
301
|
-
});
|
|
302
244
|
// Reading the log in arrival order is the only access pattern that matters.
|
|
303
245
|
this.#db.exec("CREATE INDEX IF NOT EXISTS idx_document_envelopes_order ON document_envelopes (owner_agent_id, document_id, log_index)");
|
|
304
246
|
}
|
|
@@ -321,16 +263,6 @@ export class DocumentStore {
|
|
|
321
263
|
.get(ownerAgentId, documentId);
|
|
322
264
|
return r ? toDocumentRow(r) : null;
|
|
323
265
|
}
|
|
324
|
-
/**
|
|
325
|
-
* Does this daemon hold ANY document, under any owner?
|
|
326
|
-
*
|
|
327
|
-
* Deliberately unscoped, and that is the point: it is the only question that can tell a delivery
|
|
328
|
-
* sweep visiting zero agents apart from a daemon that simply has no documents. Without it the two
|
|
329
|
-
* look identical in the log, which is what let an owner-key mismatch hide.
|
|
330
|
-
*/
|
|
331
|
-
anyDocumentExists() {
|
|
332
|
-
return this.#db.prepare("SELECT 1 FROM documents LIMIT 1").get() !== undefined;
|
|
333
|
-
}
|
|
334
266
|
listDocuments(ownerAgentId) {
|
|
335
267
|
const rows = this.#db
|
|
336
268
|
.prepare("SELECT * FROM documents WHERE owner_agent_id = ? ORDER BY created_at ASC")
|
|
@@ -367,14 +299,14 @@ export class DocumentStore {
|
|
|
367
299
|
try {
|
|
368
300
|
info = this.#db
|
|
369
301
|
.prepare(`INSERT INTO document_envelopes
|
|
370
|
-
(owner_agent_id, document_id, envelope_hash, sender_agent_id, doc_prev_hash,
|
|
371
|
-
signature, state_vector, payload, kind, references_hash,
|
|
372
|
-
created_at, log_index)
|
|
302
|
+
(owner_agent_id, document_id, envelope_hash, sender_agent_id, doc_prev_hash,
|
|
303
|
+
signature, state_vector, payload, governance_parents, kind, references_hash,
|
|
304
|
+
sender_client_id, created_at, log_index)
|
|
373
305
|
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
374
306
|
COALESCE((SELECT MAX(log_index) + 1 FROM document_envelopes
|
|
375
307
|
WHERE owner_agent_id = ? AND document_id = ?), 0)
|
|
376
308
|
ON CONFLICT (owner_agent_id, document_id, envelope_hash) DO NOTHING`)
|
|
377
|
-
.run(ownerAgentId, envelope.documentId, envelope.envelopeHash, envelope.senderAgentId, envelope.docPrevHash,
|
|
309
|
+
.run(ownerAgentId, envelope.documentId, envelope.envelopeHash, envelope.senderAgentId, envelope.docPrevHash, Buffer.from(envelope.signature), Buffer.from(envelope.stateVector), envelope.payload === null ? null : Buffer.from(envelope.payload), JSON.stringify(envelope.governanceParents ?? []), envelope.kind, envelope.referencesEnvelopeHash ?? null, envelope.senderClientId ?? null, envelope.createdAtMs, ownerAgentId, envelope.documentId);
|
|
378
310
|
}
|
|
379
311
|
catch (err) {
|
|
380
312
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -439,9 +371,9 @@ export class DocumentStore {
|
|
|
439
371
|
documentId: q.documentId,
|
|
440
372
|
senderAgentId: q.rejectedSenderAgentId,
|
|
441
373
|
docPrevHash: q.rejectedDocPrevHash,
|
|
374
|
+
governanceParents: [],
|
|
442
375
|
// EXEMPT from current-epoch stamping (M14B Entry 5): a stub is a SYNTHETIC verification
|
|
443
376
|
// node — never on the wire, never replayed — and the chain walk checks hash linkage only.
|
|
444
|
-
epochId: 0,
|
|
445
377
|
signature: new Uint8Array(0),
|
|
446
378
|
stateVector: new Uint8Array(0),
|
|
447
379
|
payload: null,
|
|
@@ -555,10 +487,10 @@ export class DocumentStore {
|
|
|
555
487
|
.prepare(`INSERT INTO document_quarantine
|
|
556
488
|
(owner_agent_id, document_id, rejection_envelope_hash, rejected_envelope_hash,
|
|
557
489
|
rejected_sender_agent_id, rejected_doc_prev_hash, payload, reason, detail,
|
|
558
|
-
rule, limit_name, limit_value, limit_actual, created_at)
|
|
559
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
490
|
+
rule, limit_name, limit_value, limit_actual, rejection_wire, created_at)
|
|
491
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
560
492
|
ON CONFLICT (owner_agent_id, document_id, rejection_envelope_hash) DO NOTHING`)
|
|
561
|
-
.run(ownerAgentId, row.documentId, row.rejectionEnvelopeHash, row.rejectedEnvelopeHash, row.rejectedSenderAgentId, row.rejectedDocPrevHash, Buffer.from(row.payload), row.reason, row.detail ?? null, row.rule ?? null, row.limitName ?? null, row.limitValue ?? null, row.limitActual ?? null, row.createdAtMs);
|
|
493
|
+
.run(ownerAgentId, row.documentId, row.rejectionEnvelopeHash, row.rejectedEnvelopeHash, row.rejectedSenderAgentId, row.rejectedDocPrevHash, Buffer.from(row.payload), row.reason, row.detail ?? null, row.rule ?? null, row.limitName ?? null, row.limitValue ?? null, row.limitActual ?? null, row.rejectionWire ? Buffer.from(row.rejectionWire) : null, row.createdAtMs);
|
|
562
494
|
return Number(info.changes) > 0;
|
|
563
495
|
}
|
|
564
496
|
listQuarantined(ownerAgentId, documentId) {
|
|
@@ -584,6 +516,7 @@ export class DocumentStore {
|
|
|
584
516
|
limitName: r["limit_name"] ?? undefined,
|
|
585
517
|
limitValue: r["limit_value"] ?? undefined,
|
|
586
518
|
limitActual: r["limit_actual"] ?? undefined,
|
|
519
|
+
rejectionWire: r["rejection_wire"] == null ? undefined : toU8(r["rejection_wire"]),
|
|
587
520
|
createdAtMs: r["created_at"],
|
|
588
521
|
}));
|
|
589
522
|
}
|
|
@@ -723,597 +656,46 @@ export class DocumentStore {
|
|
|
723
656
|
*/
|
|
724
657
|
/**
|
|
725
658
|
* The document's CURRENT epoch: the head of its recorded amendment chain, 0 at genesis.
|
|
726
|
-
* Trustworthy for stamping because every append site
|
|
659
|
+
* Trustworthy for stamping because every append site rules on admissibility before
|
|
727
660
|
* recording — a row in document_amendments is post-validation by invariant (M14B Entry 5).
|
|
728
661
|
*/
|
|
729
|
-
/**
|
|
730
|
-
|
|
731
|
-
* chain, never stored: a status column would need a CHECK-constraint rebuild on every operator
|
|
732
|
-
* DB, and a stored flag can drift from the chain that actually governs. Forward-only by
|
|
733
|
-
* construction — nothing here touches content.
|
|
734
|
-
*/
|
|
735
|
-
removedFromArrangement(ownerAgentId, documentId) {
|
|
736
|
-
return this.memberRemoved(ownerAgentId, documentId, ownerAgentId);
|
|
737
|
-
}
|
|
738
|
-
/** The same walk for ANY agent — the delivery worker asks it about the TARGET (F1). */
|
|
739
|
-
memberRemoved(ownerAgentId, documentId, agentId) {
|
|
662
|
+
/** SYNC-P5 (review F6) — per-sender envelope counts without hauling payload blobs. */
|
|
663
|
+
envelopeCountsBySender(ownerAgentId, documentId) {
|
|
740
664
|
const rows = this.#db
|
|
741
|
-
.prepare(`SELECT
|
|
665
|
+
.prepare(`SELECT sender_agent_id, COUNT(*) AS n FROM document_envelopes
|
|
742
666
|
WHERE owner_agent_id = ? AND document_id = ?
|
|
743
|
-
|
|
667
|
+
GROUP BY sender_agent_id`)
|
|
744
668
|
.all(ownerAgentId, documentId);
|
|
745
|
-
|
|
746
|
-
// on bytes this build cannot read, and this runs once PER ROW inside `list` — so one
|
|
747
|
-
// undecodable chain took down the operator's whole document list. Reporting not-removed
|
|
748
|
-
// here is not a health claim: the same row carries `arrangementUnavailable` naming the
|
|
749
|
-
// undecodable chain, so the surface says it cannot answer rather than answering wrongly.
|
|
750
|
-
let verdict;
|
|
751
|
-
try {
|
|
752
|
-
verdict = walkMembership(rows.map((r) => decodeDocumentAmendment(new Uint8Array(r.received_bytes))), agentId);
|
|
753
|
-
}
|
|
754
|
-
catch (err) {
|
|
755
|
-
this.#logger.error("document.membership.undecodable", {
|
|
756
|
-
documentId,
|
|
757
|
-
agentId,
|
|
758
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
759
|
-
});
|
|
760
|
-
return { removed: false, epochId: null };
|
|
761
|
-
}
|
|
762
|
-
return { removed: verdict.state === "removed", epochId: verdict.epochId };
|
|
763
|
-
}
|
|
764
|
-
currentDocumentEpoch(ownerAgentId, documentId) {
|
|
765
|
-
const r = this.#db
|
|
766
|
-
.prepare(`SELECT MAX(epoch_id) AS max_epoch FROM document_amendments
|
|
767
|
-
WHERE owner_agent_id = ? AND document_id = ?`)
|
|
768
|
-
.get(ownerAgentId, documentId);
|
|
769
|
-
return r?.max_epoch ?? 0;
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Append an envelope AND seed its per-holder delivery rows in ONE transaction (review M5):
|
|
773
|
-
* a crash between the two left an envelope in the log with zero rows, and the bilateral
|
|
774
|
-
* backfill would re-seed only the genesis peer — the fan-out holders silently starved.
|
|
775
|
-
* Returns the append verdict unchanged.
|
|
776
|
-
*/
|
|
777
|
-
appendEnvelopeWithDeliveries(ownerAgentId, envelope, holderAgentIds, nowMs) {
|
|
778
|
-
this.#db.exec("BEGIN IMMEDIATE");
|
|
779
|
-
try {
|
|
780
|
-
const appended = this.appendEnvelope(ownerAgentId, envelope);
|
|
781
|
-
if (appended) {
|
|
782
|
-
this.seedDeliveries(ownerAgentId, envelope.documentId, envelope.envelopeHash, holderAgentIds, nowMs);
|
|
783
|
-
}
|
|
784
|
-
this.#db.exec("COMMIT");
|
|
785
|
-
return appended;
|
|
786
|
-
}
|
|
787
|
-
catch (err) {
|
|
788
|
-
this.#db.exec("ROLLBACK");
|
|
789
|
-
throw err;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
/** DOD-MP-FANOUT-1 — one pending row per CURRENT holder for a freshly published envelope. */
|
|
793
|
-
seedDeliveries(ownerAgentId, documentId, envelopeHash, holderAgentIds, nowMs) {
|
|
794
|
-
const insert = this.#db.prepare(`INSERT INTO document_deliveries
|
|
795
|
-
(owner_agent_id, document_id, envelope_hash, holder_agent_id, created_at)
|
|
796
|
-
VALUES (?, ?, ?, ?, ?)
|
|
797
|
-
ON CONFLICT (owner_agent_id, document_id, envelope_hash, holder_agent_id) DO NOTHING`);
|
|
798
|
-
for (const holder of holderAgentIds) {
|
|
799
|
-
insert.run(ownerAgentId, documentId, envelopeHash, holder, nowMs);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* DOD-MP-INVITE-FANOUT-1 — one owed row per holder who must learn about this amendment.
|
|
804
|
-
*
|
|
805
|
-
* The inviter and the subject are excluded by the CALLER, not here: this records what is owed,
|
|
806
|
-
* and who is owed it is a governance question the handler has already derived.
|
|
807
|
-
*/
|
|
808
|
-
seedAmendmentDeliveries(ownerAgentId, documentId, amendmentHash, holderAgentIds, nowMs) {
|
|
809
|
-
const insert = this.#db.prepare(`INSERT INTO document_amendment_deliveries
|
|
810
|
-
(owner_agent_id, document_id, amendment_hash, holder_agent_id, created_at)
|
|
811
|
-
VALUES (?, ?, ?, ?, ?)
|
|
812
|
-
ON CONFLICT (owner_agent_id, document_id, amendment_hash, holder_agent_id) DO NOTHING`);
|
|
813
|
-
for (const holder of holderAgentIds) {
|
|
814
|
-
insert.run(ownerAgentId, documentId, amendmentHash, holder, nowMs);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
/**
|
|
818
|
-
* Amendments still owed to a holder, with the exact bytes to send. Joined to
|
|
819
|
-
* `document_amendments` — the recorded chain IS the payload, so a redelivery can never send
|
|
820
|
-
* something the local chain does not contain.
|
|
821
|
-
*/
|
|
822
|
-
pendingAmendmentDeliveries(ownerAgentId, nowMs, opts = {}) {
|
|
823
|
-
// HEAD-OF-LINE, PER (DOCUMENT, HOLDER) — and this is a correctness rule, not a nicety.
|
|
824
|
-
//
|
|
825
|
-
// Ordering by `created_at` among only the rows that are DUE inverts the chain: epoch N fails
|
|
826
|
-
// once and takes a backoff, epoch N+1 is seeded due immediately, so the next pass sends N+1
|
|
827
|
-
// alone. The receiver then refuses it with `document_amendment_chain_gap` — "an out-of-order
|
|
828
|
-
// arrival is retried by its sender, never buffered silently" — and under the old ack-on-send
|
|
829
|
-
// there was no retry, so N+1 was lost permanently and the one message that would have explained
|
|
830
|
-
// it said the opposite. Proven by the unit review.
|
|
831
|
-
//
|
|
832
|
-
// So: order by the CHAIN's epoch, and never offer an amendment while an earlier one is still
|
|
833
|
-
// outstanding for that same holder. Per (document, holder), never globally — one holder's stuck
|
|
834
|
-
// chain must not hold up another's.
|
|
835
|
-
const rows = this.#db
|
|
836
|
-
.prepare(`SELECT holder_agent_id, document_id, amendment_hash, epoch_id, attempts, received_bytes
|
|
837
|
-
FROM (
|
|
838
|
-
SELECT d.holder_agent_id, d.document_id, d.amendment_hash, d.attempts,
|
|
839
|
-
a.epoch_id, a.received_bytes,
|
|
840
|
-
d.next_attempt_at,
|
|
841
|
-
ROW_NUMBER() OVER (
|
|
842
|
-
PARTITION BY d.document_id, d.holder_agent_id
|
|
843
|
-
ORDER BY a.epoch_id ASC
|
|
844
|
-
) AS rn
|
|
845
|
-
FROM document_amendment_deliveries d
|
|
846
|
-
JOIN document_amendments a
|
|
847
|
-
ON a.owner_agent_id = d.owner_agent_id
|
|
848
|
-
AND a.document_id = d.document_id
|
|
849
|
-
AND a.amendment_hash = d.amendment_hash
|
|
850
|
-
WHERE d.owner_agent_id = ?
|
|
851
|
-
AND d.acked_at IS NULL
|
|
852
|
-
AND d.retired_at IS NULL
|
|
853
|
-
)
|
|
854
|
-
WHERE rn = 1
|
|
855
|
-
AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
|
|
856
|
-
ORDER BY document_id ASC, holder_agent_id ASC
|
|
857
|
-
LIMIT ?`)
|
|
858
|
-
.all(ownerAgentId, nowMs, opts.limit ?? 50);
|
|
859
|
-
return rows.map((r) => ({
|
|
860
|
-
holderAgentId: r["holder_agent_id"],
|
|
861
|
-
documentId: r["document_id"],
|
|
862
|
-
amendmentHash: r["amendment_hash"],
|
|
863
|
-
epochId: r["epoch_id"] ?? 0,
|
|
864
|
-
attempts: r["attempts"] ?? 0,
|
|
865
|
-
bytes: new Uint8Array(r["received_bytes"]),
|
|
866
|
-
}));
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* Owed amendment rows with NO matching row in the recorded chain.
|
|
870
|
-
*
|
|
871
|
-
* The pending query INNER JOINs the chain, which is right — a redelivery must never send bytes
|
|
872
|
-
* this daemon's own chain does not contain — but an inner join also makes such a row invisible
|
|
873
|
-
* AND owed forever. Surfaced so the worker can say so instead of scanning past it every tick.
|
|
874
|
-
*/
|
|
875
|
-
amendmentDeliveriesWithoutChainRow(ownerAgentId) {
|
|
876
|
-
const rows = this.#db
|
|
877
|
-
.prepare(`SELECT d.document_id, d.amendment_hash, d.holder_agent_id
|
|
878
|
-
FROM document_amendment_deliveries d
|
|
879
|
-
LEFT JOIN document_amendments a
|
|
880
|
-
ON a.owner_agent_id = d.owner_agent_id
|
|
881
|
-
AND a.document_id = d.document_id
|
|
882
|
-
AND a.amendment_hash = d.amendment_hash
|
|
883
|
-
WHERE d.owner_agent_id = ?
|
|
884
|
-
AND d.acked_at IS NULL
|
|
885
|
-
AND d.retired_at IS NULL
|
|
886
|
-
AND a.amendment_hash IS NULL`)
|
|
887
|
-
.all(ownerAgentId);
|
|
888
|
-
return rows.map((r) => ({
|
|
889
|
-
documentId: r["document_id"],
|
|
890
|
-
amendmentHash: r["amendment_hash"],
|
|
891
|
-
holderAgentId: r["holder_agent_id"],
|
|
892
|
-
}));
|
|
893
|
-
}
|
|
894
|
-
/**
|
|
895
|
-
* The bytes left this daemon. NOT a confirmation — see the `sent_at` note on the table.
|
|
896
|
-
*
|
|
897
|
-
* The row stays owed and is re-offered after `retryAfterMs`, because the only thing that has
|
|
898
|
-
* happened is that their daemon received a frame. Whether it RECORDED it is a separate fact, and
|
|
899
|
-
* the receiver can refuse.
|
|
900
|
-
*/
|
|
901
|
-
markAmendmentSent(ownerAgentId, documentId, amendmentHash, holderAgentId, nowMs, retryAfterMs) {
|
|
902
|
-
this.#db
|
|
903
|
-
.prepare(`UPDATE document_amendment_deliveries
|
|
904
|
-
SET sent_at = ?, next_attempt_at = ?
|
|
905
|
-
WHERE owner_agent_id = ? AND document_id = ? AND amendment_hash = ? AND holder_agent_id = ?`)
|
|
906
|
-
.run(nowMs, nowMs + retryAfterMs, ownerAgentId, documentId, amendmentHash, holderAgentId);
|
|
907
|
-
}
|
|
908
|
-
/** The holder demonstrably HAS it. Nothing further is owed for this (amendment, holder). */
|
|
909
|
-
ackAmendmentDelivery(ownerAgentId, documentId, amendmentHash, holderAgentId, nowMs) {
|
|
910
|
-
this.#db
|
|
911
|
-
.prepare(`UPDATE document_amendment_deliveries
|
|
912
|
-
SET acked_at = ?
|
|
913
|
-
WHERE owner_agent_id = ? AND document_id = ? AND amendment_hash = ? AND holder_agent_id = ?`)
|
|
914
|
-
.run(nowMs, ownerAgentId, documentId, amendmentHash, holderAgentId);
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* PROOF BY EPOCH — settle every amendment a holder has demonstrably applied.
|
|
918
|
-
*
|
|
919
|
-
* There is no amendment ack frame, and inventing one is wire work. But there is already a fact on
|
|
920
|
-
* the wire that PROVES receipt: a holder that acks an envelope authored at epoch E must hold every
|
|
921
|
-
* amendment up to E, because the inbound epoch gate refuses anything whose epoch does not match
|
|
922
|
-
* its own derived arrangement. So their ack of the content IS their ack of the governance that
|
|
923
|
-
* made the content admissible.
|
|
924
|
-
*
|
|
925
|
-
* Returns how many rows it settled, so the caller can say so.
|
|
926
|
-
*/
|
|
927
|
-
ackAmendmentsThroughEpoch(ownerAgentId, documentId, holderAgentId, epochId, nowMs) {
|
|
928
|
-
const owed = this.#db
|
|
929
|
-
.prepare(`SELECT d.amendment_hash
|
|
930
|
-
FROM document_amendment_deliveries d
|
|
931
|
-
JOIN document_amendments a
|
|
932
|
-
ON a.owner_agent_id = d.owner_agent_id
|
|
933
|
-
AND a.document_id = d.document_id
|
|
934
|
-
AND a.amendment_hash = d.amendment_hash
|
|
935
|
-
WHERE d.owner_agent_id = ? AND d.document_id = ? AND d.holder_agent_id = ?
|
|
936
|
-
AND d.acked_at IS NULL AND d.retired_at IS NULL
|
|
937
|
-
AND a.epoch_id <= ?`)
|
|
938
|
-
.all(ownerAgentId, documentId, holderAgentId, epochId);
|
|
939
|
-
for (const row of owed) {
|
|
940
|
-
this.ackAmendmentDelivery(ownerAgentId, documentId, row.amendment_hash, holderAgentId, nowMs);
|
|
941
|
-
}
|
|
942
|
-
return owed.length;
|
|
943
|
-
}
|
|
944
|
-
/**
|
|
945
|
-
* The holder is no longer in the chain. Distinct from acked ON PURPOSE: an ack here would make
|
|
946
|
-
* the durable record claim a delivery that never happened.
|
|
947
|
-
*/
|
|
948
|
-
retireAmendmentDelivery(ownerAgentId, documentId, amendmentHash, holderAgentId, nowMs) {
|
|
949
|
-
this.#db
|
|
950
|
-
.prepare(`UPDATE document_amendment_deliveries
|
|
951
|
-
SET retired_at = ?
|
|
952
|
-
WHERE owner_agent_id = ? AND document_id = ? AND amendment_hash = ? AND holder_agent_id = ?`)
|
|
953
|
-
.run(nowMs, ownerAgentId, documentId, amendmentHash, holderAgentId);
|
|
954
|
-
}
|
|
955
|
-
/** Record one failed attempt and when to try again; returns THEIR attempt count. */
|
|
956
|
-
recordAmendmentAttempt(ownerAgentId, documentId, amendmentHash, holderAgentId, nextAttemptAtMs) {
|
|
957
|
-
this.#db
|
|
958
|
-
.prepare(`UPDATE document_amendment_deliveries
|
|
959
|
-
SET attempts = attempts + 1, next_attempt_at = ?
|
|
960
|
-
WHERE owner_agent_id = ? AND document_id = ? AND amendment_hash = ? AND holder_agent_id = ?`)
|
|
961
|
-
.run(nextAttemptAtMs, ownerAgentId, documentId, amendmentHash, holderAgentId);
|
|
962
|
-
const row = this.#db
|
|
963
|
-
.prepare(`SELECT attempts FROM document_amendment_deliveries
|
|
964
|
-
WHERE owner_agent_id = ? AND document_id = ? AND amendment_hash = ? AND holder_agent_id = ?`)
|
|
965
|
-
.get(ownerAgentId, documentId, amendmentHash, holderAgentId);
|
|
966
|
-
return row?.attempts ?? 0;
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Everything due for delivery, PER HOLDER, joined to its envelope. The window is bounded PER
|
|
970
|
-
* HOLDER (`ROW_NUMBER` over holder partitions) — one holder's backlog must never evict
|
|
971
|
-
* another's rows from the pass, which is the no_peer starvation shape multiplied by N.
|
|
972
|
-
*/
|
|
973
|
-
pendingHolderDeliveries(ownerAgentId, nowMs, opts = {}) {
|
|
974
|
-
const limit = opts.perHolderLimit ?? 50;
|
|
975
|
-
const rows = this.#db
|
|
976
|
-
.prepare(
|
|
977
|
-
// holder_attempts ALIASED: e.* carries the envelope row's LEGACY bilateral attempts
|
|
978
|
-
// column, which would silently shadow the per-holder count in the result map.
|
|
979
|
-
`SELECT d.holder_agent_id, d.attempts AS holder_attempts, d.document_id AS delivery_document_id, e.*
|
|
980
|
-
FROM (
|
|
981
|
-
SELECT *, ROW_NUMBER() OVER (
|
|
982
|
-
PARTITION BY holder_agent_id
|
|
983
|
-
ORDER BY created_at ASC, envelope_hash ASC
|
|
984
|
-
) AS rn
|
|
985
|
-
FROM document_deliveries
|
|
986
|
-
WHERE owner_agent_id = ?
|
|
987
|
-
AND acked_at IS NULL AND abandoned_at IS NULL
|
|
988
|
-
AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
|
|
989
|
-
) d
|
|
990
|
-
JOIN document_envelopes e
|
|
991
|
-
ON e.owner_agent_id = d.owner_agent_id
|
|
992
|
-
AND e.document_id = d.document_id
|
|
993
|
-
AND e.envelope_hash = d.envelope_hash
|
|
994
|
-
WHERE d.rn <= ?
|
|
995
|
-
ORDER BY d.holder_agent_id ASC, d.created_at ASC`)
|
|
996
|
-
.all(ownerAgentId, nowMs, limit);
|
|
997
|
-
return rows.map((r) => ({
|
|
998
|
-
holderAgentId: r["holder_agent_id"],
|
|
999
|
-
documentId: r["delivery_document_id"],
|
|
1000
|
-
attempts: r["holder_attempts"] ?? 0,
|
|
1001
|
-
envelope: toEnvelopeRow(r),
|
|
1002
|
-
}));
|
|
1003
|
-
}
|
|
1004
|
-
/** Record one holder's attempt; returns THEIR attempt count. */
|
|
1005
|
-
recordHolderAttempt(ownerAgentId, documentId, envelopeHash, holderAgentId, nextAttemptAtMs) {
|
|
1006
|
-
this.#db
|
|
1007
|
-
.prepare(`UPDATE document_deliveries
|
|
1008
|
-
SET attempts = attempts + 1, next_attempt_at = ?
|
|
1009
|
-
WHERE owner_agent_id = ? AND document_id = ? AND envelope_hash = ? AND holder_agent_id = ?`)
|
|
1010
|
-
.run(nextAttemptAtMs, ownerAgentId, documentId, envelopeHash, holderAgentId);
|
|
1011
|
-
const r = this.#db
|
|
1012
|
-
.prepare(`SELECT attempts FROM document_deliveries
|
|
1013
|
-
WHERE owner_agent_id = ? AND document_id = ? AND envelope_hash = ? AND holder_agent_id = ?`)
|
|
1014
|
-
.get(ownerAgentId, documentId, envelopeHash, holderAgentId);
|
|
1015
|
-
return r?.attempts ?? 0;
|
|
1016
|
-
}
|
|
1017
|
-
/** Settle one holder's confirmation. True exactly when THIS call settled it. */
|
|
1018
|
-
ackHolderDelivery(ownerAgentId, documentId, envelopeHash, holderAgentId, nowMs) {
|
|
1019
|
-
const r = this.#db
|
|
1020
|
-
.prepare(`UPDATE document_deliveries
|
|
1021
|
-
SET acked_at = ?, delivered_at = COALESCE(delivered_at, ?)
|
|
1022
|
-
WHERE owner_agent_id = ? AND document_id = ? AND envelope_hash = ? AND holder_agent_id = ?
|
|
1023
|
-
AND acked_at IS NULL AND abandoned_at IS NULL`)
|
|
1024
|
-
.run(nowMs, nowMs, ownerAgentId, documentId, envelopeHash, holderAgentId);
|
|
1025
|
-
return (r.changes ?? 0) > 0;
|
|
1026
|
-
}
|
|
1027
|
-
/**
|
|
1028
|
-
* Retire EVERY outstanding row for one holder — our decision, announced by the caller.
|
|
1029
|
-
* Returns the affected envelope hashes so the caller can reconcile the envelope-level record
|
|
1030
|
-
* (an envelope whose every holder-row is settled-with-abandonment is itself abandoned).
|
|
1031
|
-
*/
|
|
1032
|
-
abandonHolderDeliveries(ownerAgentId, documentId, holderAgentId, nowMs) {
|
|
1033
|
-
const rows = this.#db
|
|
1034
|
-
.prepare(`SELECT envelope_hash FROM document_deliveries
|
|
1035
|
-
WHERE owner_agent_id = ? AND document_id = ? AND holder_agent_id = ?
|
|
1036
|
-
AND acked_at IS NULL AND abandoned_at IS NULL`)
|
|
1037
|
-
.all(ownerAgentId, documentId, holderAgentId);
|
|
1038
|
-
this.#db
|
|
1039
|
-
.prepare(`UPDATE document_deliveries
|
|
1040
|
-
SET abandoned_at = ?
|
|
1041
|
-
WHERE owner_agent_id = ? AND document_id = ? AND holder_agent_id = ?
|
|
1042
|
-
AND acked_at IS NULL AND abandoned_at IS NULL`)
|
|
1043
|
-
.run(nowMs, ownerAgentId, documentId, holderAgentId);
|
|
1044
|
-
return rows.map((r) => r.envelope_hash);
|
|
669
|
+
return new Map(rows.map((r) => [r.sender_agent_id, r.n]));
|
|
1045
670
|
}
|
|
1046
|
-
/**
|
|
1047
|
-
|
|
671
|
+
/** SYNC-P5 — record what a party's exchange just showed us (display cache; see the table). */
|
|
672
|
+
recordPartyView(ownerAgentId, documentId, partyAgentId, view, nowMs) {
|
|
1048
673
|
this.#db
|
|
1049
|
-
.prepare(`
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
this.#db
|
|
1062
|
-
.prepare(`UPDATE document_deliveries
|
|
1063
|
-
SET delivered_at = COALESCE(delivered_at, ?)
|
|
1064
|
-
WHERE owner_agent_id = ? AND document_id = ? AND envelope_hash = ? AND holder_agent_id = ?`)
|
|
1065
|
-
.run(nowMs, ownerAgentId, documentId, envelopeHash, holderAgentId);
|
|
1066
|
-
}
|
|
1067
|
-
/** Has ANY holder's copy of this envelope left the machine? */
|
|
1068
|
-
envelopeEverSent(ownerAgentId, envelopeHash) {
|
|
1069
|
-
const r = this.#db
|
|
1070
|
-
.prepare(`SELECT 1 FROM document_deliveries
|
|
1071
|
-
WHERE owner_agent_id = ? AND envelope_hash = ? AND delivered_at IS NOT NULL
|
|
1072
|
-
LIMIT 1`)
|
|
1073
|
-
.get(ownerAgentId, envelopeHash);
|
|
1074
|
-
return r !== undefined;
|
|
1075
|
-
}
|
|
1076
|
-
/** Reschedule one holder's retry WITHOUT counting an attempt — waiting is not sending. */
|
|
1077
|
-
scheduleHolderRetry(ownerAgentId, documentId, envelopeHash, holderAgentId, nextAttemptAtMs) {
|
|
1078
|
-
this.#db
|
|
1079
|
-
.prepare(`UPDATE document_deliveries
|
|
1080
|
-
SET next_attempt_at = ?
|
|
1081
|
-
WHERE owner_agent_id = ? AND document_id = ? AND envelope_hash = ? AND holder_agent_id = ?`)
|
|
1082
|
-
.run(nextAttemptAtMs, ownerAgentId, documentId, envelopeHash, holderAgentId);
|
|
1083
|
-
}
|
|
1084
|
-
/** Retire ONE (envelope, holder) row — the per-holder unacked ceiling's teeth. */
|
|
1085
|
-
abandonSingleHolderDelivery(ownerAgentId, documentId, envelopeHash, holderAgentId, nowMs) {
|
|
1086
|
-
this.#db
|
|
1087
|
-
.prepare(`UPDATE document_deliveries
|
|
1088
|
-
SET abandoned_at = ?
|
|
1089
|
-
WHERE owner_agent_id = ? AND document_id = ? AND envelope_hash = ? AND holder_agent_id = ?
|
|
1090
|
-
AND acked_at IS NULL AND abandoned_at IS NULL`)
|
|
1091
|
-
.run(nowMs, ownerAgentId, documentId, envelopeHash, holderAgentId);
|
|
1092
|
-
}
|
|
1093
|
-
/**
|
|
1094
|
-
* No holder still owes a confirmation on this envelope. TRUE with zero rows — the bilateral
|
|
1095
|
-
* legacy case, where the envelope-row columns remain the record and this table has nothing to
|
|
1096
|
-
* say.
|
|
1097
|
-
*/
|
|
1098
|
-
envelopeFullySettled(ownerAgentId, envelopeHash) {
|
|
1099
|
-
const r = this.#db
|
|
1100
|
-
.prepare(`SELECT 1 FROM document_deliveries
|
|
1101
|
-
WHERE owner_agent_id = ? AND envelope_hash = ?
|
|
1102
|
-
AND acked_at IS NULL AND abandoned_at IS NULL
|
|
1103
|
-
LIMIT 1`)
|
|
1104
|
-
.get(ownerAgentId, envelopeHash);
|
|
1105
|
-
return r === undefined;
|
|
1106
|
-
}
|
|
1107
|
-
/**
|
|
1108
|
-
* Seed per-holder rows for LEGACY bilateral envelopes: anything this agent authored that is
|
|
1109
|
-
* still unacknowledged on the envelope row and has no per-holder rows yet gets one for the
|
|
1110
|
-
* genesis peer. Idempotent (the PK refuses duplicates); run per pass, cheap, and journaled —
|
|
1111
|
-
* pre-fan-out backlogs are near-empty at alpha, this is insurance not migration.
|
|
1112
|
-
*/
|
|
1113
|
-
backfillBilateralDeliveries(ownerAgentId, senderAgentId, nowMs) {
|
|
1114
|
-
this.#db
|
|
1115
|
-
.prepare(`INSERT INTO document_deliveries
|
|
1116
|
-
(owner_agent_id, document_id, envelope_hash, holder_agent_id, attempts, created_at)
|
|
1117
|
-
SELECT e.owner_agent_id, e.document_id, e.envelope_hash, d.peer_agent_id, e.attempts, ?
|
|
1118
|
-
FROM document_envelopes e
|
|
1119
|
-
JOIN documents d
|
|
1120
|
-
ON d.owner_agent_id = e.owner_agent_id AND d.document_id = e.document_id
|
|
1121
|
-
WHERE e.owner_agent_id = ? AND e.sender_agent_id = ?
|
|
1122
|
-
AND e.acked_at IS NULL AND e.abandoned_at IS NULL AND e.kind = 'update'
|
|
1123
|
-
AND NOT EXISTS (
|
|
1124
|
-
SELECT 1 FROM document_deliveries dd
|
|
1125
|
-
WHERE dd.owner_agent_id = e.owner_agent_id AND dd.envelope_hash = e.envelope_hash
|
|
1126
|
-
)
|
|
1127
|
-
ON CONFLICT (owner_agent_id, document_id, envelope_hash, holder_agent_id) DO NOTHING`)
|
|
1128
|
-
.run(nowMs, ownerAgentId, senderAgentId);
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* THIS holder's settlement of THIS envelope — the per-acker twin of `envelopeSettlement`,
|
|
1132
|
-
* with rejection attribution taken from the rejections-received rows' `from_agent_id`.
|
|
1133
|
-
* Null when the holder has not answered (or has no row — the bilateral-legacy case, where the
|
|
1134
|
-
* envelope-level read is the record).
|
|
1135
|
-
*/
|
|
1136
|
-
holderSettlement(ownerAgentId, envelopeHash, holderAgentId) {
|
|
674
|
+
.prepare(`INSERT INTO document_party_view
|
|
675
|
+
(owner_agent_id, document_id, party_agent_id, last_exchange_ms, gov_seqs,
|
|
676
|
+
content_counts, refused)
|
|
677
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
678
|
+
ON CONFLICT (owner_agent_id, document_id, party_agent_id) DO UPDATE SET
|
|
679
|
+
last_exchange_ms = excluded.last_exchange_ms,
|
|
680
|
+
gov_seqs = excluded.gov_seqs,
|
|
681
|
+
content_counts = excluded.content_counts,
|
|
682
|
+
refused = excluded.refused`)
|
|
683
|
+
.run(ownerAgentId, documentId, partyAgentId, nowMs, JSON.stringify(view.govSeqs), JSON.stringify(view.contentCounts), JSON.stringify(view.refused));
|
|
684
|
+
}
|
|
685
|
+
partyView(ownerAgentId, documentId, partyAgentId) {
|
|
1137
686
|
const r = this.#db
|
|
1138
|
-
.prepare(`SELECT
|
|
1139
|
-
WHERE owner_agent_id = ? AND
|
|
1140
|
-
.get(ownerAgentId,
|
|
1141
|
-
if (!r
|
|
687
|
+
.prepare(`SELECT last_exchange_ms, gov_seqs, content_counts, refused FROM document_party_view
|
|
688
|
+
WHERE owner_agent_id = ? AND document_id = ? AND party_agent_id = ?`)
|
|
689
|
+
.get(ownerAgentId, documentId, partyAgentId);
|
|
690
|
+
if (!r)
|
|
1142
691
|
return null;
|
|
1143
692
|
return {
|
|
1144
|
-
|
|
693
|
+
lastExchangeMs: r.last_exchange_ms,
|
|
694
|
+
govSeqs: JSON.parse(r.gov_seqs),
|
|
695
|
+
contentCounts: JSON.parse(r.content_counts),
|
|
696
|
+
refused: JSON.parse(r.refused),
|
|
1145
697
|
};
|
|
1146
698
|
}
|
|
1147
|
-
/** Was a rejection of this envelope received FROM this specific holder? */
|
|
1148
|
-
rejectionReceivedFromFor(ownerAgentId, documentId, envelopeHash, fromAgentId) {
|
|
1149
|
-
const r = this.#db
|
|
1150
|
-
.prepare(`SELECT 1 FROM document_rejections_received
|
|
1151
|
-
WHERE owner_agent_id = ? AND document_id = ? AND rejected_envelope_hash = ?
|
|
1152
|
-
AND from_agent_id = ?
|
|
1153
|
-
LIMIT 1`)
|
|
1154
|
-
.get(ownerAgentId, documentId, envelopeHash, fromAgentId);
|
|
1155
|
-
return r !== undefined;
|
|
1156
|
-
}
|
|
1157
|
-
/**
|
|
1158
|
-
* ONE deterministic envelope-level terminal rule (FANOUT-1 review M3), applied by every
|
|
1159
|
-
* reconciliation site: once no holder is outstanding, the envelope is ACKED if at least one
|
|
1160
|
-
* holder acked, ABANDONED only when nobody ever answered. Without one rule, identical final
|
|
1161
|
-
* holder states landed as opposite envelope-level facts depending on event order — and
|
|
1162
|
-
* `withdraw`'s "your peer holds it" answered differently for the same reality.
|
|
1163
|
-
*/
|
|
1164
|
-
reconcileEnvelopeSettlement(ownerAgentId, documentId, envelopeHash, nowMs) {
|
|
1165
|
-
if (!this.envelopeFullySettled(ownerAgentId, envelopeHash))
|
|
1166
|
-
return;
|
|
1167
|
-
const anyAcked = this.#db
|
|
1168
|
-
.prepare(`SELECT 1 FROM document_deliveries
|
|
1169
|
-
WHERE owner_agent_id = ? AND envelope_hash = ? AND acked_at IS NOT NULL
|
|
1170
|
-
LIMIT 1`)
|
|
1171
|
-
.get(ownerAgentId, envelopeHash);
|
|
1172
|
-
if (anyAcked !== undefined) {
|
|
1173
|
-
this.markAcked(ownerAgentId, documentId, envelopeHash, nowMs);
|
|
1174
|
-
}
|
|
1175
|
-
else {
|
|
1176
|
-
const anyRows = this.#db
|
|
1177
|
-
.prepare(`SELECT 1 FROM document_deliveries WHERE owner_agent_id = ? AND envelope_hash = ? LIMIT 1`)
|
|
1178
|
-
.get(ownerAgentId, envelopeHash);
|
|
1179
|
-
// Zero rows = bilateral legacy — the envelope-level columns are already the record.
|
|
1180
|
-
if (anyRows !== undefined) {
|
|
1181
|
-
this.markAbandoned(ownerAgentId, documentId, envelopeHash, nowMs);
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
/** Does this holder still owe any confirmation on this document? */
|
|
1186
|
-
holderHasPending(ownerAgentId, documentId, holderAgentId) {
|
|
1187
|
-
const r = this.#db
|
|
1188
|
-
.prepare(`SELECT 1 FROM document_deliveries
|
|
1189
|
-
WHERE owner_agent_id = ? AND document_id = ? AND holder_agent_id = ?
|
|
1190
|
-
AND acked_at IS NULL AND abandoned_at IS NULL
|
|
1191
|
-
LIMIT 1`)
|
|
1192
|
-
.get(ownerAgentId, documentId, holderAgentId);
|
|
1193
|
-
return r !== undefined;
|
|
1194
|
-
}
|
|
1195
|
-
pendingDeliveries(ownerAgentId, nowMs,
|
|
1196
|
-
/**
|
|
1197
|
-
* OUR OWN sender id on the wire — the author's pubkey hex (M14-D5).
|
|
1198
|
-
*
|
|
1199
|
-
* Kept as a separate parameter, and separately named, even though the daemon now scopes the
|
|
1200
|
-
* store by that same pubkey hex so the two coincide. They are different FACTS: the owner key
|
|
1201
|
-
* says whose local store this row is in, the sender id says who signed the envelope. An
|
|
1202
|
-
* earlier version of the daemon scoped by agent name, and the mismatch returned nothing
|
|
1203
|
-
* pending — every published update sitting in the log undelivered, with no error anywhere.
|
|
1204
|
-
* Collapsing them into one argument makes that class of bug unrepresentable in the call and
|
|
1205
|
-
* invisible in the query.
|
|
1206
|
-
*/
|
|
1207
|
-
senderAgentId = ownerAgentId, limit = 100) {
|
|
1208
|
-
const rows = this.#db
|
|
1209
|
-
.prepare(`SELECT * FROM document_envelopes
|
|
1210
|
-
WHERE owner_agent_id = ? AND sender_agent_id = ? AND acked_at IS NULL
|
|
1211
|
-
-- ABANDONED rows are not pending. This is what actually stops the worker at the
|
|
1212
|
-
-- unacked ceiling: the selection reads no document STATUS, so setting the document
|
|
1213
|
-
-- stalled changed what the surface said and nothing about what the worker did, and one
|
|
1214
|
-
-- envelope went out 74 times against a cap of 5.
|
|
1215
|
-
AND abandoned_at IS NULL
|
|
1216
|
-
-- UPDATES only. A withdrawal record is local audit — the update it concerns was never
|
|
1217
|
-
-- delivered, so there is nothing for the peer to act on — and a rejection reaches the
|
|
1218
|
-
-- peer through the rejection protocol, not this worker. Without the scope the worker
|
|
1219
|
-
-- would ship both, and the withdrawal would arrive as a reference to an envelope the
|
|
1220
|
-
-- peer has never seen.
|
|
1221
|
-
AND kind = 'update'
|
|
1222
|
-
-- An ENDED document does not deliver. A killed or closed document that kept shipping
|
|
1223
|
-
-- would contradict the verb the operator just used, and the peer would receive updates
|
|
1224
|
-
-- on a collaboration they were told had stopped.
|
|
1225
|
-
AND EXISTS (
|
|
1226
|
-
SELECT 1 FROM documents d
|
|
1227
|
-
WHERE d.owner_agent_id = document_envelopes.owner_agent_id
|
|
1228
|
-
AND d.document_id = document_envelopes.document_id
|
|
1229
|
-
AND d.status NOT IN ('killed', 'closed')
|
|
1230
|
-
)
|
|
1231
|
-
AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
|
|
1232
|
-
-- A WITHDRAWN update is not pending. Derived from the withdrawal record rather than a
|
|
1233
|
-
-- flag on the row, so there is one fact in one place: without this the delivery worker
|
|
1234
|
-
-- ships the very update the operator just withdrew.
|
|
1235
|
-
--
|
|
1236
|
-
-- The table is created by DocumentLifecycle, which may not have run — a store used
|
|
1237
|
-
-- without it is a legitimate configuration — so the reference is guarded rather than
|
|
1238
|
-
-- assumed. A missing table would otherwise throw here and take the whole delivery pass
|
|
1239
|
-
-- down with it.
|
|
1240
|
-
AND NOT EXISTS (
|
|
1241
|
-
SELECT 1 FROM document_withdrawals w
|
|
1242
|
-
WHERE w.owner_agent_id = document_envelopes.owner_agent_id
|
|
1243
|
-
AND w.document_id = document_envelopes.document_id
|
|
1244
|
-
AND w.envelope_hash = document_envelopes.envelope_hash
|
|
1245
|
-
)
|
|
1246
|
-
-- log_index is PER DOCUMENT, so it alone is not a total order across documents and the
|
|
1247
|
-
-- bounded window could be filled by one document's backlog forever. The tiebreaks make
|
|
1248
|
-
-- the window deterministic; the no-peer branch scheduling its rows is what stops one
|
|
1249
|
-
-- document monopolising it.
|
|
1250
|
-
ORDER BY log_index ASC, document_id ASC, envelope_hash ASC LIMIT ?`)
|
|
1251
|
-
.all(ownerAgentId, senderAgentId, nowMs, limit);
|
|
1252
|
-
return rows.map(toEnvelopeRow);
|
|
1253
|
-
}
|
|
1254
|
-
/** Record that the peer acknowledged. Idempotent — a redelivered ack must not move the clock. */
|
|
1255
|
-
/**
|
|
1256
|
-
* How many of our envelopes for this document were ABANDONED — the unacked ceiling fired and they
|
|
1257
|
-
* will never be retried.
|
|
1258
|
-
*
|
|
1259
|
-
* Surfaced because an abandoned envelope leaves every pending counter, so a document that
|
|
1260
|
-
* permanently dropped an update is otherwise indistinguishable from one that delivered everything.
|
|
1261
|
-
*/
|
|
1262
|
-
abandonedCount(ownerAgentId, documentId) {
|
|
1263
|
-
const row = this.#db
|
|
1264
|
-
.prepare(`SELECT COUNT(*) AS n FROM document_envelopes
|
|
1265
|
-
WHERE owner_agent_id = ? AND document_id = ? AND sender_agent_id = ? AND abandoned_at IS NOT NULL`)
|
|
1266
|
-
.get(ownerAgentId, documentId, ownerAgentId);
|
|
1267
|
-
return row?.n ?? 0;
|
|
1268
|
-
}
|
|
1269
|
-
/**
|
|
1270
|
-
* Is this envelope SETTLED — the peer answered it, admitted or rejected?
|
|
1271
|
-
*
|
|
1272
|
-
* Keyed by envelope hash ALONE, without the document, because the caller waiting on it is the
|
|
1273
|
-
* delivery transport, which holds a session open and knows the envelope it sent but has no reason
|
|
1274
|
-
* to carry the document id through the wait. The hash is a sha256 over the envelope's own
|
|
1275
|
-
* preimage, so it identifies one envelope across every document this owner holds.
|
|
1276
|
-
*/
|
|
1277
|
-
isEnvelopeAcked(ownerAgentId, envelopeHash) {
|
|
1278
|
-
return this.envelopeSettlement(ownerAgentId, envelopeHash) !== null;
|
|
1279
|
-
}
|
|
1280
|
-
/**
|
|
1281
|
-
* HOW an envelope was settled, or null if the peer has not answered it.
|
|
1282
|
-
*
|
|
1283
|
-
* `admitted` is derived from whether a rejection was RECEIVED for it, because `acked_at` records
|
|
1284
|
-
* only that the peer answered — a rejection is an ack for delivery purposes, so the two states
|
|
1285
|
-
* share that column deliberately (see `DocumentAckInbound`). Reading the answer, not just its
|
|
1286
|
-
* existence, is what lets the delivery worker report `delivered` and `rejected` truthfully rather
|
|
1287
|
-
* than counting every answered envelope as still in flight.
|
|
1288
|
-
*/
|
|
1289
|
-
envelopeSettlement(ownerAgentId, envelopeHash) {
|
|
1290
|
-
const row = this.#db
|
|
1291
|
-
.prepare(`SELECT document_id FROM document_envelopes
|
|
1292
|
-
WHERE owner_agent_id = ? AND envelope_hash = ? AND acked_at IS NOT NULL LIMIT 1`)
|
|
1293
|
-
.get(ownerAgentId, envelopeHash);
|
|
1294
|
-
if (row?.document_id === undefined)
|
|
1295
|
-
return null;
|
|
1296
|
-
return { admitted: !this.rejectionReceivedFor(ownerAgentId, row.document_id, envelopeHash) };
|
|
1297
|
-
}
|
|
1298
|
-
/**
|
|
1299
|
-
* WE GAVE UP — the unacked ceiling. Deliberately not `markAcked`: this records a decision of
|
|
1300
|
-
* ours, and says nothing about whether the peer holds the envelope, because we do not know.
|
|
1301
|
-
*/
|
|
1302
|
-
markAbandoned(ownerAgentId, documentId, envelopeHash, nowMs) {
|
|
1303
|
-
const info = this.#db
|
|
1304
|
-
.prepare(`UPDATE document_envelopes SET abandoned_at = ?
|
|
1305
|
-
WHERE owner_agent_id = ? AND document_id = ? AND envelope_hash = ?
|
|
1306
|
-
AND acked_at IS NULL AND abandoned_at IS NULL`)
|
|
1307
|
-
.run(nowMs, ownerAgentId, documentId, envelopeHash);
|
|
1308
|
-
return Number(info.changes) > 0;
|
|
1309
|
-
}
|
|
1310
|
-
markAcked(ownerAgentId, documentId, envelopeHash, nowMs) {
|
|
1311
|
-
const info = this.#db
|
|
1312
|
-
.prepare(`UPDATE document_envelopes SET acked_at = ?, delivered_at = COALESCE(delivered_at, ?)
|
|
1313
|
-
WHERE owner_agent_id = ? AND document_id = ? AND envelope_hash = ? AND acked_at IS NULL`)
|
|
1314
|
-
.run(nowMs, nowMs, ownerAgentId, documentId, envelopeHash);
|
|
1315
|
-
return Number(info.changes) > 0;
|
|
1316
|
-
}
|
|
1317
699
|
/** Record a rejection the PEER sent us. Returns whether a row was written (idempotent by leaf). */
|
|
1318
700
|
recordRejectionReceived(ownerAgentId, row) {
|
|
1319
701
|
const info = this.#db
|
|
@@ -1517,19 +899,15 @@ function toEnvelopeRow(r) {
|
|
|
1517
899
|
documentId: r["document_id"],
|
|
1518
900
|
senderAgentId: r["sender_agent_id"],
|
|
1519
901
|
docPrevHash: r["doc_prev_hash"] ?? null,
|
|
1520
|
-
epochId: r["epoch_id"],
|
|
1521
902
|
signature: toU8(r["signature"]),
|
|
1522
903
|
stateVector: toU8(r["state_vector"]),
|
|
1523
904
|
payload: payload === null || payload === undefined ? null : toU8(payload),
|
|
905
|
+
governanceParents: JSON.parse(r["governance_parents"] ?? "[]"),
|
|
1524
906
|
kind: r["kind"],
|
|
1525
907
|
senderClientId: r["sender_client_id"] ?? null,
|
|
1526
908
|
referencesEnvelopeHash: r["references_hash"] ?? null,
|
|
1527
909
|
createdAtMs: r["created_at"],
|
|
1528
910
|
logIndex: r["log_index"],
|
|
1529
|
-
deliveredAtMs: r["delivered_at"] ?? null,
|
|
1530
|
-
ackedAtMs: r["acked_at"] ?? null,
|
|
1531
|
-
attempts: r["attempts"] ?? 0,
|
|
1532
|
-
nextAttemptAtMs: r["next_attempt_at"] ?? null,
|
|
1533
911
|
};
|
|
1534
912
|
}
|
|
1535
913
|
/**
|