@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
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOD-DOC-INBOUND-2 — consuming an arriving ACK. This is what closes DELIVERY-2's loop.
|
|
3
|
-
*
|
|
4
|
-
* Without it a sent envelope's outcome is `admitted: null` forever: the worker knows the content
|
|
5
|
-
* left and nothing more, so it re-sends on the ack timeout and eventually stalls the document at
|
|
6
|
-
* the unacked ceiling.
|
|
7
|
-
*
|
|
8
|
-
* ── EVERY CHECK HERE GUARDS ONE OF TWO IRREVERSIBLE CONSEQUENCES ──────────────────────────────
|
|
9
|
-
*
|
|
10
|
-
* An ack SETTLES an envelope. Admitted, it stops being redelivered; rejected, the sender rolls back
|
|
11
|
-
* local work and supersedes. Both are things an unauthenticated party must not be able to cause, so
|
|
12
|
-
* the order is:
|
|
13
|
-
*
|
|
14
|
-
* 1. DECODE — refuse malformed input before interpreting any of it.
|
|
15
|
-
* 2. KNOW the document — an ack for a document we do not hold settles nothing that exists.
|
|
16
|
-
* 3. ACKER is the peer — a document is a pairwise agreement; nobody else's answer settles it.
|
|
17
|
-
* 4. VERIFY — before any write. An unsigned ack that silenced a delivery would drop
|
|
18
|
-
* the content from the pending set with neither operator ever learning it
|
|
19
|
-
* was never applied — the silent divergence the two-layer design exists to
|
|
20
|
-
* prevent.
|
|
21
|
-
* 5. THE ENVELOPE IS OURS — we authored it, and it is in the log. An ack for a peer-authored
|
|
22
|
-
* envelope settles a delivery that was never ours to make; an ack for an
|
|
23
|
-
* envelope that is not there is a bug or a probe, and recording it would
|
|
24
|
-
* put a claim about a nonexistent delivery into a permanent record.
|
|
25
|
-
* 6. SETTLE — mark acked; on a rejection also record it durably on the publishing side,
|
|
26
|
-
* because an operator needs to know WHY their work was refused after a
|
|
27
|
-
* restart, not merely that it was.
|
|
28
|
-
*/
|
|
29
|
-
import { decodeDocumentAck, buildDocumentAckTbs } from "@cello-protocol/protocol-types";
|
|
30
|
-
export class DocumentAckInbound {
|
|
31
|
-
#d;
|
|
32
|
-
constructor(deps) {
|
|
33
|
-
this.#d = deps;
|
|
34
|
-
}
|
|
35
|
-
receive(ownerAgentId, wire, nowMs, correlationId = "ack") {
|
|
36
|
-
// 1. DECODE.
|
|
37
|
-
let ack;
|
|
38
|
-
try {
|
|
39
|
-
ack = decodeDocumentAck(wire);
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
43
|
-
this.#d.logger.warn("document.ack.malformed", { detail, correlationId });
|
|
44
|
-
// One reason code with the upstream message in the detail — the decoder's named refusals and
|
|
45
|
-
// raw CBOR errors both arrive here, and building a code out of the message turns the second
|
|
46
|
-
// kind into English prose nothing can match on.
|
|
47
|
-
return { ok: false, reason: "document_ack_malformed", detail };
|
|
48
|
-
}
|
|
49
|
-
// 2. KNOW the document.
|
|
50
|
-
const doc = this.#d.store.getDocument(ownerAgentId, ack.document_id);
|
|
51
|
-
if (!doc) {
|
|
52
|
-
return {
|
|
53
|
-
ok: false,
|
|
54
|
-
reason: "document_unknown",
|
|
55
|
-
detail: `no document ${ack.document_id.slice(0, 16)}… for this agent`,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
// 3. The acker must be a party. When the chain DERIVES, derived membership is the WHOLE
|
|
59
|
-
// gate — a removed genesis peer's ack refuses like any outsider's (review H2). Only when the
|
|
60
|
-
// chain cannot answer (bilateral legacy, no genesis record) does the genesis-peer column
|
|
61
|
-
// stand in.
|
|
62
|
-
const holders = this.#d.currentHolders(ownerAgentId, ack.document_id);
|
|
63
|
-
const isParty = holders === null
|
|
64
|
-
? ack.acker_agent_id === doc.peerAgentId
|
|
65
|
-
: holders.includes(ack.acker_agent_id);
|
|
66
|
-
if (!isParty) {
|
|
67
|
-
this.#d.logger.warn("document.ack.not_peer", {
|
|
68
|
-
documentId: ack.document_id,
|
|
69
|
-
ackerAgentId: ack.acker_agent_id,
|
|
70
|
-
peerAgentId: doc.peerAgentId,
|
|
71
|
-
correlationId,
|
|
72
|
-
});
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
reason: "document_ack_not_peer",
|
|
76
|
-
// The peer's identity is not disclosed to a party that has not authenticated.
|
|
77
|
-
detail: "you are not a party to this document",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
// 4. VERIFY, before any write.
|
|
81
|
-
if (!this.#d.verifySignature(ack.acker_agent_id, buildDocumentAckTbs(ack), ack.signature)) {
|
|
82
|
-
this.#d.logger.error("document.ack.signature_invalid", {
|
|
83
|
-
documentId: ack.document_id,
|
|
84
|
-
ackerAgentId: ack.acker_agent_id,
|
|
85
|
-
envelopeHash: ack.envelope_hash,
|
|
86
|
-
correlationId,
|
|
87
|
-
});
|
|
88
|
-
return {
|
|
89
|
-
ok: false,
|
|
90
|
-
reason: "document_ack_signature_invalid",
|
|
91
|
-
detail: `the ack claims to come from ${ack.acker_agent_id} but its signature does not verify ` +
|
|
92
|
-
`against that agent — nothing was settled`,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
// 5. The envelope must be one WE authored, and it must exist.
|
|
96
|
-
const row = this.#d.store
|
|
97
|
-
.getEnvelopeLog(ownerAgentId, ack.document_id)
|
|
98
|
-
.find((e) => e.envelopeHash === ack.envelope_hash);
|
|
99
|
-
if (!row) {
|
|
100
|
-
return {
|
|
101
|
-
ok: false,
|
|
102
|
-
reason: "document_ack_envelope_unknown",
|
|
103
|
-
detail: `envelope ${ack.envelope_hash.slice(0, 16)}… is not in this document's log`,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
if (row.senderAgentId !== ownerAgentId) {
|
|
107
|
-
return {
|
|
108
|
-
ok: false,
|
|
109
|
-
reason: "document_ack_not_author",
|
|
110
|
-
detail: `envelope ${ack.envelope_hash.slice(0, 16)}… was authored by ${row.senderAgentId}, not by ` +
|
|
111
|
-
`you — there is no delivery of ours for this ack to settle`,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
// 6. SETTLE ONCE. The wire type's own header states this contract: nothing binds an ack to the
|
|
115
|
-
// acker's chain, so one acker can produce a valid ADMISSION and a valid REJECTION for the same
|
|
116
|
-
// envelope. Applying the later one would let a peer that admitted an envelope later claim it
|
|
117
|
-
// refused it — and the sender would roll back work the peer already holds. A second,
|
|
118
|
-
// CONTRADICTING ack is an error; a second identical one is an ordinary redelivery.
|
|
119
|
-
// SETTLE-ONCE, PER ACKER (review M4). The envelope-level column stays null until ALL
|
|
120
|
-
// holders answer, so an envelope-scoped check let a holder that ADMITTED later send a
|
|
121
|
-
// REJECTION mid-fan-out — processed, rounds advanced, work rolled back that they already
|
|
122
|
-
// hold. The acker's own per-holder row is the record; the envelope-level check remains only
|
|
123
|
-
// for the zero-row bilateral legacy.
|
|
124
|
-
const holderPrior = this.#d.store.holderSettlement(ownerAgentId, ack.envelope_hash, ack.acker_agent_id);
|
|
125
|
-
const alreadyAcked = holderPrior !== null || row.ackedAtMs != null;
|
|
126
|
-
if (alreadyAcked) {
|
|
127
|
-
// ENVELOPE-scoped. The document-scoped reader answers a different question, and using it
|
|
128
|
-
// here would make a rejection of any OTHER envelope in this document read as a rejection of
|
|
129
|
-
// this one — so an honest redelivered admission would be refused as a contradiction the
|
|
130
|
-
// moment anything else in the document had ever been refused.
|
|
131
|
-
const previouslyRejected = holderPrior !== null
|
|
132
|
-
? !holderPrior.admitted
|
|
133
|
-
: this.#d.store.rejectionReceivedFor(ownerAgentId, ack.document_id, ack.envelope_hash);
|
|
134
|
-
const contradicts = ack.admitted === previouslyRejected;
|
|
135
|
-
if (contradicts) {
|
|
136
|
-
this.#d.logger.error("document.ack.contradiction", {
|
|
137
|
-
documentId: ack.document_id,
|
|
138
|
-
envelopeHash: ack.envelope_hash,
|
|
139
|
-
ackerAgentId: ack.acker_agent_id,
|
|
140
|
-
nowSays: ack.admitted ? "admitted" : "rejected",
|
|
141
|
-
correlationId,
|
|
142
|
-
});
|
|
143
|
-
return {
|
|
144
|
-
ok: false,
|
|
145
|
-
reason: "document_ack_contradiction",
|
|
146
|
-
detail: `${ack.acker_agent_id} already settled envelope ${ack.envelope_hash.slice(0, 16)}… and ` +
|
|
147
|
-
`now says the opposite — this envelope stays settled as it was, and both claims are ` +
|
|
148
|
-
`retained`,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
// An identical redelivery. Nothing to do, and nothing to complain about.
|
|
152
|
-
return { ok: true, admitted: ack.admitted, envelopeHash: ack.envelope_hash };
|
|
153
|
-
}
|
|
154
|
-
// FANOUT-1: the ACKING HOLDER settles THEIR row; the envelope-level record follows the ONE
|
|
155
|
-
// deterministic terminal rule (`reconcileEnvelopeSettlement` — review M3), identical from
|
|
156
|
-
// every reconciliation site. Bilateral legacy (zero rows) stays byte-identical: the
|
|
157
|
-
// reconcile no-ops and markAcked below still runs through it.
|
|
158
|
-
this.#d.store.ackHolderDelivery(ownerAgentId, ack.document_id, ack.envelope_hash, ack.acker_agent_id, nowMs);
|
|
159
|
-
const before = row.ackedAtMs != null;
|
|
160
|
-
this.#d.store.reconcileEnvelopeSettlement(ownerAgentId, ack.document_id, ack.envelope_hash, nowMs);
|
|
161
|
-
const legacy = this.#d.store.holderSettlement(ownerAgentId, ack.envelope_hash, ack.acker_agent_id) === null;
|
|
162
|
-
const first = legacy
|
|
163
|
-
? this.#d.store.markAcked(ownerAgentId, ack.document_id, ack.envelope_hash, nowMs)
|
|
164
|
-
: !before;
|
|
165
|
-
// ANNOUNCE THE SETTLE, before the admitted/rejected split — both outcomes end the delivery, and
|
|
166
|
-
// a waiter that only heard about admissions would keep a session open through every rejection.
|
|
167
|
-
this.#d.onSettled?.(ownerAgentId, ack.envelope_hash, ack.acker_agent_id, ack.admitted);
|
|
168
|
-
if (!ack.admitted) {
|
|
169
|
-
// Durable on the publishing side. A log line would not survive the restart after which the
|
|
170
|
-
// operator asks why their work never landed — and this is also what bounds the retry on the
|
|
171
|
-
// side that actually loops.
|
|
172
|
-
this.#d.rejections.recordIncomingRejection(ownerAgentId, ack.document_id, {
|
|
173
|
-
rejectionEnvelopeHash: ackRecordHash(ack.envelope_hash),
|
|
174
|
-
rejectedEnvelopeHash: ack.envelope_hash,
|
|
175
|
-
reason: ack.rejection_reason ?? "document_rejected",
|
|
176
|
-
fromAgentId: ack.acker_agent_id,
|
|
177
|
-
});
|
|
178
|
-
return { ok: true, admitted: false, envelopeHash: ack.envelope_hash };
|
|
179
|
-
}
|
|
180
|
-
this.#d.logger.info("document.ack.admitted", {
|
|
181
|
-
documentId: ack.document_id,
|
|
182
|
-
envelopeHash: ack.envelope_hash,
|
|
183
|
-
firstAck: first,
|
|
184
|
-
correlationId,
|
|
185
|
-
});
|
|
186
|
-
return { ok: true, admitted: true, envelopeHash: ack.envelope_hash };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* The identity of the RECEIVED-rejection row: the REFUSED ENVELOPE, and nothing else.
|
|
191
|
-
*
|
|
192
|
-
* ONE REFUSED ENVELOPE IS ONE ROUND, however many acks for it arrive.
|
|
193
|
-
*
|
|
194
|
-
* This used to mix in the acker's `acked_at_ms` on the reasoning that both fields are signed, so a
|
|
195
|
-
* redelivered ack would collapse onto the same row. That reasoning had a false premise: acks are
|
|
196
|
-
* not stored and redelivered — `sendAck` builds a fresh `DocumentAck` with `acked_at_ms: Date.now()`
|
|
197
|
-
* every time it answers. So two acks for the SAME envelope carried two different timestamps and
|
|
198
|
-
* became two rows, advancing the round twice.
|
|
199
|
-
*
|
|
200
|
-
* That is not hypothetical. Delivery re-sends on a 1s first backoff, so an ordinary in-flight
|
|
201
|
-
* overlap — the sender re-sends while the first ack is still on the wire, both land — produced two
|
|
202
|
-
* rounds from one refusal. `MAX_REJECTED_ROUNDS` is 3, so routine delivery overlap could stall a
|
|
203
|
-
* shared document with no hostility involved. That is the same denial of service the 5b
|
|
204
|
-
* already-rejected guard in `document-inbound.ts` was written to close, arriving through the other
|
|
205
|
-
* side of the same loop.
|
|
206
|
-
*/
|
|
207
|
-
function ackRecordHash(envelopeHash) {
|
|
208
|
-
return envelopeHash;
|
|
209
|
-
}
|
|
210
|
-
//# sourceMappingURL=document-ack-inbound.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"document-ack-inbound.js","sourceRoot":"","sources":["../src/document-ack-inbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOD-DOC-TOOLS-1 — telling the peer a document has ended.
|
|
3
|
-
*
|
|
4
|
-
* `DocumentLifecycle` ends a document locally without asking anyone, deliberately: a kill is a
|
|
5
|
-
* safety verb, and a safety verb that needs the counterparty's cooperation is not one. But a peer
|
|
6
|
-
* who is never told keeps publishing into a document that will never answer — their updates refused
|
|
7
|
-
* at the far end forever, with nothing on their screen explaining why. So the notification is
|
|
8
|
-
* REQUIRED to be attempted, ALLOWED to fail, and the operator is told which happened.
|
|
9
|
-
*
|
|
10
|
-
* ── WHY THIS IS A MODULE AND NOT A CLOSURE IN THE COMPOSITION ROOT ────────────────────────────
|
|
11
|
-
*
|
|
12
|
-
* It was one, and that is precisely how the surface tests passed while the feature did nothing.
|
|
13
|
-
* `createDocumentLayer` takes `notifyPeer` as an injected seam, so the two-party test wired it to
|
|
14
|
-
* `async () => ({ ok: true })` — which reported success, sent nothing, and agreed with whatever the
|
|
15
|
-
* near side did. Kill "worked" on both sides of the assertion and on neither side of the wire.
|
|
16
|
-
*
|
|
17
|
-
* That is the same shape as the two defects the DELIVERY-2 review found: a stub on the far side
|
|
18
|
-
* cannot disagree with you. So the construction lives here, and both the daemon and the test build
|
|
19
|
-
* it from this function — the test can still substitute the TRANSPORT, which is the part that has
|
|
20
|
-
* to be substituted, without also substituting the logic under test.
|
|
21
|
-
*
|
|
22
|
-
* ── WHY THE DOCUMENT IS FOUND BY SCANNING ─────────────────────────────────────────────────────
|
|
23
|
-
*
|
|
24
|
-
* `notifyPeer`'s signature carries no owner: lifecycle calls it knowing only the document id, and
|
|
25
|
-
* widening that interface would push a multi-agent daemon concern into a unit that deliberately has
|
|
26
|
-
* none. So the owner is resolved here, by asking each agent this daemon holds whether the document
|
|
27
|
-
* is theirs. A document id is a hash committing to both parties and a nonce, so at most one agent
|
|
28
|
-
* can answer.
|
|
29
|
-
*
|
|
30
|
-
* ── DOD-MP-CONTROL-N-1 — WHY THE TARGET IS DERIVED, NOT `peerAgentId` ─────────────────────────
|
|
31
|
-
*
|
|
32
|
-
* This addressed `doc.peerAgentId` and returned after one send. That column is the GENESIS
|
|
33
|
-
* counterparty, frozen at creation, and multiplayer made it wrong in two ways at once: with three
|
|
34
|
-
* holders only one was told, and after a removal the genesis counterparty can BE the removed
|
|
35
|
-
* holder — so the frame went to the one party it must not reach while the remaining co-author was
|
|
36
|
-
* never told, under a reported `peerNotified: true`. Found by the M14B live fleet smoke.
|
|
37
|
-
*
|
|
38
|
-
* The target is now the DERIVED participant set, the same source FANOUT-1 established for update
|
|
39
|
-
* envelopes; control frames were simply never migrated to it. Deriving needs the genesis proposal,
|
|
40
|
-
* the amendment chain and signature verification — all of which live above this module, so it
|
|
41
|
-
* arrives as an injected `holders()` rather than by widening this unit into the replay engine.
|
|
42
|
-
*
|
|
43
|
-
* A derivation failure REFUSES. It must never fall back to `peerAgentId`: that is the old bug, and
|
|
44
|
-
* it aims the frame at exactly the wrong party in the case that motivated the change.
|
|
45
|
-
*/
|
|
46
|
-
import { type DocumentControlVerb } from "@cello-protocol/protocol-types";
|
|
47
|
-
import type { DocumentStore } from "./document-store.js";
|
|
48
|
-
export interface DocumentControlNotifierDeps {
|
|
49
|
-
store: DocumentStore;
|
|
50
|
-
/** The agents this daemon holds — name for signing and sending, owner key for scoping the store. */
|
|
51
|
-
owners(): ReadonlyArray<{
|
|
52
|
-
agentName: string;
|
|
53
|
-
ownerAgentId: string;
|
|
54
|
-
}>;
|
|
55
|
-
/**
|
|
56
|
-
* The CURRENT holders of this document OTHER than the owner, derived from the genesis proposal
|
|
57
|
-
* plus the amendment chain — the same set that governs update delivery. A failure to derive is
|
|
58
|
-
* returned, not swallowed: this unit refuses rather than guessing a recipient.
|
|
59
|
-
*/
|
|
60
|
-
holders(ownerAgentId: string, documentId: string): {
|
|
61
|
-
ok: true;
|
|
62
|
-
holders: readonly string[];
|
|
63
|
-
} | {
|
|
64
|
-
ok: false;
|
|
65
|
-
reason: string;
|
|
66
|
-
};
|
|
67
|
-
/** Sign as the named agent. A miss must REFUSE, not substitute another agent's key. */
|
|
68
|
-
sign(agentName: string, tbs: Uint8Array): Promise<Uint8Array>;
|
|
69
|
-
send(agentName: string, input: {
|
|
70
|
-
peerAgentId: string;
|
|
71
|
-
documentId: string;
|
|
72
|
-
bytes: Uint8Array;
|
|
73
|
-
correlationId: string;
|
|
74
|
-
}): Promise<{
|
|
75
|
-
ok: true;
|
|
76
|
-
} | {
|
|
77
|
-
ok: false;
|
|
78
|
-
reason: string;
|
|
79
|
-
}>;
|
|
80
|
-
now(): number;
|
|
81
|
-
/**
|
|
82
|
-
* Optional, and only for reporting a LOCAL fault. This module used to swallow the signing error
|
|
83
|
-
* entirely, so the one description of "your key could not be loaded" existed nowhere — not in the
|
|
84
|
-
* return value, not in a log line — and the operator was sent to wait for a peer who was already
|
|
85
|
-
* there.
|
|
86
|
-
*/
|
|
87
|
-
logger?: {
|
|
88
|
-
warn(event: string, ctx: Record<string, unknown>): void;
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
export type NotifyPeer = (documentId: string, verb: DocumentControlVerb) => Promise<{
|
|
92
|
-
ok: true;
|
|
93
|
-
holdersNotified: Record<string, boolean>;
|
|
94
|
-
/**
|
|
95
|
-
* WHY each holder did not get it, per holder. The boolean alone throws the cause away, and
|
|
96
|
-
* the operator's sentence then has to guess — it guessed "most likely offline" for a
|
|
97
|
-
* `session_sealed`, which waiting can never fix. That is the exact substitution
|
|
98
|
-
* `notifyGuidance` exists to prevent, reintroduced one layer up.
|
|
99
|
-
*/
|
|
100
|
-
holderFailures: Record<string, string>;
|
|
101
|
-
} | {
|
|
102
|
-
ok: false;
|
|
103
|
-
reason: string;
|
|
104
|
-
detail?: string;
|
|
105
|
-
}>;
|
|
106
|
-
export declare function createDocumentControlNotifier(deps: DocumentControlNotifierDeps): NotifyPeer;
|
|
107
|
-
//# sourceMappingURL=document-control-notifier.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"document-control-notifier.d.ts","sourceRoot":"","sources":["../src/document-control-notifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAGH,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,aAAa,CAAC;IACrB,oGAAoG;IACpG,MAAM,IAAI,aAAa,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrE;;;;OAIG;IACH,OAAO,CACL,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,uFAAuF;IACvF,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9D,IAAI,CACF,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,UAAU,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,GAC3F,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzD,GAAG,IAAI,MAAM,CAAC;IACd;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC;CACtE;AAED,MAAM,MAAM,UAAU,GAAG,CACvB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,mBAAmB,KACtB,OAAO,CACR;IACE,EAAE,EAAE,IAAI,CAAC;IACT,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC,GACD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CACjD,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,2BAA2B,GAAG,UAAU,CAwF3F"}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOD-DOC-TOOLS-1 — telling the peer a document has ended.
|
|
3
|
-
*
|
|
4
|
-
* `DocumentLifecycle` ends a document locally without asking anyone, deliberately: a kill is a
|
|
5
|
-
* safety verb, and a safety verb that needs the counterparty's cooperation is not one. But a peer
|
|
6
|
-
* who is never told keeps publishing into a document that will never answer — their updates refused
|
|
7
|
-
* at the far end forever, with nothing on their screen explaining why. So the notification is
|
|
8
|
-
* REQUIRED to be attempted, ALLOWED to fail, and the operator is told which happened.
|
|
9
|
-
*
|
|
10
|
-
* ── WHY THIS IS A MODULE AND NOT A CLOSURE IN THE COMPOSITION ROOT ────────────────────────────
|
|
11
|
-
*
|
|
12
|
-
* It was one, and that is precisely how the surface tests passed while the feature did nothing.
|
|
13
|
-
* `createDocumentLayer` takes `notifyPeer` as an injected seam, so the two-party test wired it to
|
|
14
|
-
* `async () => ({ ok: true })` — which reported success, sent nothing, and agreed with whatever the
|
|
15
|
-
* near side did. Kill "worked" on both sides of the assertion and on neither side of the wire.
|
|
16
|
-
*
|
|
17
|
-
* That is the same shape as the two defects the DELIVERY-2 review found: a stub on the far side
|
|
18
|
-
* cannot disagree with you. So the construction lives here, and both the daemon and the test build
|
|
19
|
-
* it from this function — the test can still substitute the TRANSPORT, which is the part that has
|
|
20
|
-
* to be substituted, without also substituting the logic under test.
|
|
21
|
-
*
|
|
22
|
-
* ── WHY THE DOCUMENT IS FOUND BY SCANNING ─────────────────────────────────────────────────────
|
|
23
|
-
*
|
|
24
|
-
* `notifyPeer`'s signature carries no owner: lifecycle calls it knowing only the document id, and
|
|
25
|
-
* widening that interface would push a multi-agent daemon concern into a unit that deliberately has
|
|
26
|
-
* none. So the owner is resolved here, by asking each agent this daemon holds whether the document
|
|
27
|
-
* is theirs. A document id is a hash committing to both parties and a nonce, so at most one agent
|
|
28
|
-
* can answer.
|
|
29
|
-
*
|
|
30
|
-
* ── DOD-MP-CONTROL-N-1 — WHY THE TARGET IS DERIVED, NOT `peerAgentId` ─────────────────────────
|
|
31
|
-
*
|
|
32
|
-
* This addressed `doc.peerAgentId` and returned after one send. That column is the GENESIS
|
|
33
|
-
* counterparty, frozen at creation, and multiplayer made it wrong in two ways at once: with three
|
|
34
|
-
* holders only one was told, and after a removal the genesis counterparty can BE the removed
|
|
35
|
-
* holder — so the frame went to the one party it must not reach while the remaining co-author was
|
|
36
|
-
* never told, under a reported `peerNotified: true`. Found by the M14B live fleet smoke.
|
|
37
|
-
*
|
|
38
|
-
* The target is now the DERIVED participant set, the same source FANOUT-1 established for update
|
|
39
|
-
* envelopes; control frames were simply never migrated to it. Deriving needs the genesis proposal,
|
|
40
|
-
* the amendment chain and signature verification — all of which live above this module, so it
|
|
41
|
-
* arrives as an injected `holders()` rather than by widening this unit into the replay engine.
|
|
42
|
-
*
|
|
43
|
-
* A derivation failure REFUSES. It must never fall back to `peerAgentId`: that is the old bug, and
|
|
44
|
-
* it aims the frame at exactly the wrong party in the case that motivated the change.
|
|
45
|
-
*/
|
|
46
|
-
import { randomUUID } from "node:crypto";
|
|
47
|
-
import { encodeDocumentControl, buildDocumentControlTbs, DOCUMENT_CONTROL_VERSION, } from "@cello-protocol/protocol-types";
|
|
48
|
-
export function createDocumentControlNotifier(deps) {
|
|
49
|
-
return async (documentId, verb) => {
|
|
50
|
-
for (const { agentName, ownerAgentId } of deps.owners()) {
|
|
51
|
-
const doc = deps.store.getDocument(ownerAgentId, documentId);
|
|
52
|
-
if (!doc)
|
|
53
|
-
continue;
|
|
54
|
-
const targets = deps.holders(ownerAgentId, documentId);
|
|
55
|
-
if (!targets.ok) {
|
|
56
|
-
deps.logger?.warn("document.control.holders_underivable", {
|
|
57
|
-
documentId, verb, agentName, reason: targets.reason,
|
|
58
|
-
});
|
|
59
|
-
return { ok: false, reason: targets.reason };
|
|
60
|
-
}
|
|
61
|
-
if (targets.holders.length === 0) {
|
|
62
|
-
// Distinct from a failed send, and reported rather than returned as a vacuous success: an
|
|
63
|
-
// empty map behind `ok: true` reads as "everyone was told" to a caller that checks only ok.
|
|
64
|
-
return { ok: false, reason: "document_no_holders" };
|
|
65
|
-
}
|
|
66
|
-
const control = {
|
|
67
|
-
type: "document_control",
|
|
68
|
-
control_version: DOCUMENT_CONTROL_VERSION,
|
|
69
|
-
document_id: documentId,
|
|
70
|
-
sender_agent_id: ownerAgentId,
|
|
71
|
-
verb,
|
|
72
|
-
sent_at_ms: deps.now(),
|
|
73
|
-
signature: new Uint8Array(0),
|
|
74
|
-
};
|
|
75
|
-
let signature;
|
|
76
|
-
try {
|
|
77
|
-
signature = await deps.sign(agentName, buildDocumentControlTbs(control));
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
// REFUSED, not skipped and not sent unsigned. The peer must reject an unsigned control
|
|
81
|
-
// frame, so shipping one would report a notification that cannot possibly land — and the
|
|
82
|
-
// caller reports `peerNotified` straight to the operator.
|
|
83
|
-
//
|
|
84
|
-
// THE MESSAGE IS KEPT. A bare `catch {}` here threw away the only description of a LOCAL
|
|
85
|
-
// fault — a key file that moved, a locked keychain, an agent with no provider — and the
|
|
86
|
-
// operator was then told their peer had not heard them and to try again when the peer was
|
|
87
|
-
// back. Waiting cannot fix a signing failure. The reason travels up so the caller can say
|
|
88
|
-
// which of the two it is.
|
|
89
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
90
|
-
deps.logger?.warn("document.control.unsigned", { documentId, verb, agentName, reason: detail });
|
|
91
|
-
return { ok: false, reason: "document_control_unsigned", detail };
|
|
92
|
-
}
|
|
93
|
-
control.signature = signature;
|
|
94
|
-
// Signed ONCE. The TBS commits to the document, the sender and the verb — never to a
|
|
95
|
-
// recipient — so every holder receives byte-identical evidence and cannot be handed a
|
|
96
|
-
// frame that differs from their co-holders'.
|
|
97
|
-
const bytes = encodeDocumentControl(control);
|
|
98
|
-
const holdersNotified = {};
|
|
99
|
-
const holderFailures = {};
|
|
100
|
-
for (const holder of targets.holders) {
|
|
101
|
-
// Sequential and independent: one unreachable holder must neither block nor abort the
|
|
102
|
-
// others (availability is a first-class protocol concern), and a throw from the transport
|
|
103
|
-
// is that holder's failure alone, not the fan-out's.
|
|
104
|
-
try {
|
|
105
|
-
const sent = await deps.send(agentName, {
|
|
106
|
-
peerAgentId: holder,
|
|
107
|
-
documentId,
|
|
108
|
-
bytes,
|
|
109
|
-
correlationId: randomUUID(),
|
|
110
|
-
});
|
|
111
|
-
holdersNotified[holder] = sent.ok;
|
|
112
|
-
if (!sent.ok) {
|
|
113
|
-
holderFailures[holder] = sent.reason;
|
|
114
|
-
deps.logger?.warn("document.control.holder_not_notified", {
|
|
115
|
-
documentId, verb, holderAgentId: holder, reason: sent.reason,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
holdersNotified[holder] = false;
|
|
121
|
-
holderFailures[holder] = err instanceof Error ? err.message : String(err);
|
|
122
|
-
deps.logger?.warn("document.control.holder_not_notified", {
|
|
123
|
-
documentId, verb, holderAgentId: holder,
|
|
124
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// `ok` means SIGNED AND ATTEMPTED TO EVERY CURRENT HOLDER. Who actually took it is the map's
|
|
129
|
-
// job — collapsing that into one boolean is how a partial fan-out reads as a clean one.
|
|
130
|
-
return { ok: true, holdersNotified, holderFailures };
|
|
131
|
-
}
|
|
132
|
-
// No agent on this daemon holds it. Named as such rather than reported as a transport failure:
|
|
133
|
-
// the two want different things from whoever reads the log.
|
|
134
|
-
return { ok: false, reason: "document_unknown" };
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
//# sourceMappingURL=document-control-notifier.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"document-control-notifier.js","sourceRoot":"","sources":["../src/document-control-notifier.ts"],"names":[],"mappings":"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"}
|