@cello-protocol/daemon 0.0.132 → 0.0.134
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/bin/cello-daemon.js +25 -0
- package/dist/bin/cello-daemon.js.map +1 -1
- package/dist/content-park.d.ts.map +1 -1
- package/dist/content-park.js +29 -11
- package/dist/content-park.js.map +1 -1
- package/dist/daemon.d.ts +11 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +455 -17
- package/dist/daemon.js.map +1 -1
- package/dist/document-ack-inbound.d.ts +57 -0
- package/dist/document-ack-inbound.d.ts.map +1 -0
- package/dist/document-ack-inbound.js +174 -0
- package/dist/document-ack-inbound.js.map +1 -0
- package/dist/document-control-notifier.d.ts +61 -0
- package/dist/document-control-notifier.d.ts.map +1 -0
- package/dist/document-control-notifier.js +70 -0
- package/dist/document-control-notifier.js.map +1 -0
- package/dist/document-delivery-transport.d.ts +94 -0
- package/dist/document-delivery-transport.d.ts.map +1 -0
- package/dist/document-delivery-transport.js +179 -0
- package/dist/document-delivery-transport.js.map +1 -0
- package/dist/document-delivery.d.ts +181 -0
- package/dist/document-delivery.d.ts.map +1 -0
- package/dist/document-delivery.js +289 -0
- package/dist/document-delivery.js.map +1 -0
- package/dist/document-frame-router.d.ts +210 -0
- package/dist/document-frame-router.d.ts.map +1 -0
- package/dist/document-frame-router.js +396 -0
- package/dist/document-frame-router.js.map +1 -0
- package/dist/document-handlers.d.ts +47 -0
- package/dist/document-handlers.d.ts.map +1 -0
- package/dist/document-handlers.js +657 -0
- package/dist/document-handlers.js.map +1 -0
- package/dist/document-handshake.d.ts +156 -0
- package/dist/document-handshake.d.ts.map +1 -0
- package/dist/document-handshake.js +398 -0
- package/dist/document-handshake.js.map +1 -0
- package/dist/document-inbound.d.ts +91 -0
- package/dist/document-inbound.d.ts.map +1 -0
- package/dist/document-inbound.js +290 -0
- package/dist/document-inbound.js.map +1 -0
- package/dist/document-layer.d.ts +137 -0
- package/dist/document-layer.d.ts.map +1 -0
- package/dist/document-layer.js +255 -0
- package/dist/document-layer.js.map +1 -0
- package/dist/document-lifecycle.d.ts +125 -0
- package/dist/document-lifecycle.d.ts.map +1 -0
- package/dist/document-lifecycle.js +433 -0
- package/dist/document-lifecycle.js.map +1 -0
- package/dist/document-live-docs.d.ts +58 -0
- package/dist/document-live-docs.d.ts.map +1 -0
- package/dist/document-live-docs.js +126 -0
- package/dist/document-live-docs.js.map +1 -0
- package/dist/document-notify.d.ts +173 -0
- package/dist/document-notify.d.ts.map +1 -0
- package/dist/document-notify.js +438 -0
- package/dist/document-notify.js.map +1 -0
- package/dist/document-publish.d.ts +67 -0
- package/dist/document-publish.d.ts.map +1 -0
- package/dist/document-publish.js +149 -0
- package/dist/document-publish.js.map +1 -0
- package/dist/document-reachability.d.ts +42 -0
- package/dist/document-reachability.d.ts.map +1 -0
- package/dist/document-reachability.js +80 -0
- package/dist/document-reachability.js.map +1 -0
- package/dist/document-rejection.d.ts +240 -0
- package/dist/document-rejection.d.ts.map +1 -0
- package/dist/document-rejection.js +407 -0
- package/dist/document-rejection.js.map +1 -0
- package/dist/document-store.d.ts +154 -8
- package/dist/document-store.d.ts.map +1 -1
- package/dist/document-store.js +462 -4
- package/dist/document-store.js.map +1 -1
- package/dist/document-write-path.d.ts.map +1 -1
- package/dist/document-write-path.js +10 -43
- package/dist/document-write-path.js.map +1 -1
- package/dist/inbound-sessions.d.ts.map +1 -1
- package/dist/inbound-sessions.js +4 -0
- package/dist/inbound-sessions.js.map +1 -1
- package/dist/initiate-session-handler.d.ts +24 -1
- package/dist/initiate-session-handler.d.ts.map +1 -1
- package/dist/initiate-session-handler.js +35 -9
- package/dist/initiate-session-handler.js.map +1 -1
- package/dist/ipc-server.d.ts +11 -1
- package/dist/ipc-server.d.ts.map +1 -1
- package/dist/ipc-server.js +7 -1
- package/dist/ipc-server.js.map +1 -1
- package/dist/line-lcs.d.ts +51 -0
- package/dist/line-lcs.d.ts.map +1 -0
- package/dist/line-lcs.js +71 -0
- package/dist/line-lcs.js.map +1 -0
- package/dist/notification-handlers.d.ts.map +1 -1
- package/dist/notification-handlers.js +31 -14
- package/dist/notification-handlers.js.map +1 -1
- package/dist/outbound-sessions.d.ts +2 -0
- package/dist/outbound-sessions.d.ts.map +1 -1
- package/dist/outbound-sessions.js +11 -1
- package/dist/outbound-sessions.js.map +1 -1
- package/dist/session-content-handlers.d.ts.map +1 -1
- package/dist/session-content-handlers.js +3 -2
- package/dist/session-content-handlers.js.map +1 -1
- package/dist/session-node-manager.d.ts +26 -4
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +201 -15
- package/dist/session-node-manager.js.map +1 -1
- package/dist/session-read-handlers.js +1 -1
- package/dist/session-read-handlers.js.map +1 -1
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +16 -0
- package/dist/vocabulary.js.map +1 -1
- package/dist/wire-content-hash.d.ts +27 -0
- package/dist/wire-content-hash.d.ts.map +1 -0
- package/dist/wire-content-hash.js +37 -0
- package/dist/wire-content-hash.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,174 @@
|
|
|
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 THIS document's peer.
|
|
59
|
+
if (ack.acker_agent_id !== doc.peerAgentId) {
|
|
60
|
+
this.#d.logger.warn("document.ack.not_peer", {
|
|
61
|
+
documentId: ack.document_id,
|
|
62
|
+
ackerAgentId: ack.acker_agent_id,
|
|
63
|
+
peerAgentId: doc.peerAgentId,
|
|
64
|
+
correlationId,
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
reason: "document_ack_not_peer",
|
|
69
|
+
// The peer's identity is not disclosed to a party that has not authenticated.
|
|
70
|
+
detail: "you are not a party to this document",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// 4. VERIFY, before any write.
|
|
74
|
+
if (!this.#d.verifySignature(ack.acker_agent_id, buildDocumentAckTbs(ack), ack.signature)) {
|
|
75
|
+
this.#d.logger.error("document.ack.signature_invalid", {
|
|
76
|
+
documentId: ack.document_id,
|
|
77
|
+
ackerAgentId: ack.acker_agent_id,
|
|
78
|
+
envelopeHash: ack.envelope_hash,
|
|
79
|
+
correlationId,
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
reason: "document_ack_signature_invalid",
|
|
84
|
+
detail: `the ack claims to come from ${ack.acker_agent_id} but its signature does not verify ` +
|
|
85
|
+
`against that agent — nothing was settled`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// 5. The envelope must be one WE authored, and it must exist.
|
|
89
|
+
const row = this.#d.store
|
|
90
|
+
.getEnvelopeLog(ownerAgentId, ack.document_id)
|
|
91
|
+
.find((e) => e.envelopeHash === ack.envelope_hash);
|
|
92
|
+
if (!row) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: "document_ack_envelope_unknown",
|
|
96
|
+
detail: `envelope ${ack.envelope_hash.slice(0, 16)}… is not in this document's log`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (row.senderAgentId !== ownerAgentId) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
reason: "document_ack_not_author",
|
|
103
|
+
detail: `envelope ${ack.envelope_hash.slice(0, 16)}… was authored by ${row.senderAgentId}, not by ` +
|
|
104
|
+
`you — there is no delivery of ours for this ack to settle`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// 6. SETTLE ONCE. The wire type's own header states this contract: nothing binds an ack to the
|
|
108
|
+
// acker's chain, so one acker can produce a valid ADMISSION and a valid REJECTION for the same
|
|
109
|
+
// envelope. Applying the later one would let a peer that admitted an envelope later claim it
|
|
110
|
+
// refused it — and the sender would roll back work the peer already holds. A second,
|
|
111
|
+
// CONTRADICTING ack is an error; a second identical one is an ordinary redelivery.
|
|
112
|
+
const alreadyAcked = row.ackedAtMs != null;
|
|
113
|
+
if (alreadyAcked) {
|
|
114
|
+
// ENVELOPE-scoped. The document-scoped reader answers a different question, and using it
|
|
115
|
+
// here would make a rejection of any OTHER envelope in this document read as a rejection of
|
|
116
|
+
// this one — so an honest redelivered admission would be refused as a contradiction the
|
|
117
|
+
// moment anything else in the document had ever been refused.
|
|
118
|
+
const previouslyRejected = this.#d.store.rejectionReceivedFor(ownerAgentId, ack.document_id, ack.envelope_hash);
|
|
119
|
+
const contradicts = ack.admitted === previouslyRejected;
|
|
120
|
+
if (contradicts) {
|
|
121
|
+
this.#d.logger.error("document.ack.contradiction", {
|
|
122
|
+
documentId: ack.document_id,
|
|
123
|
+
envelopeHash: ack.envelope_hash,
|
|
124
|
+
ackerAgentId: ack.acker_agent_id,
|
|
125
|
+
nowSays: ack.admitted ? "admitted" : "rejected",
|
|
126
|
+
correlationId,
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
reason: "document_ack_contradiction",
|
|
131
|
+
detail: `${ack.acker_agent_id} already settled envelope ${ack.envelope_hash.slice(0, 16)}… and ` +
|
|
132
|
+
`now says the opposite — this envelope stays settled as it was, and both claims are ` +
|
|
133
|
+
`retained`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// An identical redelivery. Nothing to do, and nothing to complain about.
|
|
137
|
+
return { ok: true, admitted: ack.admitted, envelopeHash: ack.envelope_hash };
|
|
138
|
+
}
|
|
139
|
+
// `markAcked` is idempotent and returns whether this was the first — a redelivered ack must not
|
|
140
|
+
// move the recorded time, or the delivery record says the peer confirmed at a moment it did not.
|
|
141
|
+
const first = this.#d.store.markAcked(ownerAgentId, ack.document_id, ack.envelope_hash, nowMs);
|
|
142
|
+
if (!ack.admitted) {
|
|
143
|
+
// Durable on the publishing side. A log line would not survive the restart after which the
|
|
144
|
+
// operator asks why their work never landed — and this is also what bounds the retry on the
|
|
145
|
+
// side that actually loops.
|
|
146
|
+
this.#d.rejections.recordIncomingRejection(ownerAgentId, ack.document_id, {
|
|
147
|
+
rejectionEnvelopeHash: ackRecordHash(ack.envelope_hash, ack.acked_at_ms),
|
|
148
|
+
rejectedEnvelopeHash: ack.envelope_hash,
|
|
149
|
+
reason: ack.rejection_reason ?? "document_rejected",
|
|
150
|
+
fromAgentId: ack.acker_agent_id,
|
|
151
|
+
});
|
|
152
|
+
return { ok: true, admitted: false, envelopeHash: ack.envelope_hash };
|
|
153
|
+
}
|
|
154
|
+
this.#d.logger.info("document.ack.admitted", {
|
|
155
|
+
documentId: ack.document_id,
|
|
156
|
+
envelopeHash: ack.envelope_hash,
|
|
157
|
+
firstAck: first,
|
|
158
|
+
correlationId,
|
|
159
|
+
});
|
|
160
|
+
return { ok: true, admitted: true, envelopeHash: ack.envelope_hash };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The identity of the RECEIVED-rejection row.
|
|
165
|
+
*
|
|
166
|
+
* Derived from the acked envelope and the acker's own timestamp — both signed — so a redelivered
|
|
167
|
+
* ack collapses onto the same row instead of advancing the round. Deriving it from anything local
|
|
168
|
+
* (a clock read here, a counter) would make every redelivery a new rejection, and three of those
|
|
169
|
+
* stall the document.
|
|
170
|
+
*/
|
|
171
|
+
function ackRecordHash(envelopeHash, ackedAtMs) {
|
|
172
|
+
return `${envelopeHash}:${ackedAtMs}`;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=document-ack-inbound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-ack-inbound.js","sourceRoot":"","sources":["../src/document-ack-inbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
import { type DocumentControlVerb } from "@cello-protocol/protocol-types";
|
|
31
|
+
import type { DocumentStore } from "./document-store.js";
|
|
32
|
+
export interface DocumentControlNotifierDeps {
|
|
33
|
+
store: DocumentStore;
|
|
34
|
+
/** The agents this daemon holds — name for signing and sending, owner key for scoping the store. */
|
|
35
|
+
owners(): ReadonlyArray<{
|
|
36
|
+
agentName: string;
|
|
37
|
+
ownerAgentId: string;
|
|
38
|
+
}>;
|
|
39
|
+
/** Sign as the named agent. A miss must REFUSE, not substitute another agent's key. */
|
|
40
|
+
sign(agentName: string, tbs: Uint8Array): Promise<Uint8Array>;
|
|
41
|
+
send(agentName: string, input: {
|
|
42
|
+
peerAgentId: string;
|
|
43
|
+
documentId: string;
|
|
44
|
+
bytes: Uint8Array;
|
|
45
|
+
correlationId: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
ok: true;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
reason: string;
|
|
51
|
+
}>;
|
|
52
|
+
now(): number;
|
|
53
|
+
}
|
|
54
|
+
export type NotifyPeer = (documentId: string, verb: DocumentControlVerb) => Promise<{
|
|
55
|
+
ok: true;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: string;
|
|
59
|
+
}>;
|
|
60
|
+
export declare function createDocumentControlNotifier(deps: DocumentControlNotifierDeps): NotifyPeer;
|
|
61
|
+
//# sourceMappingURL=document-control-notifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-control-notifier.d.ts","sourceRoot":"","sources":["../src/document-control-notifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;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,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;CACf;AAED,MAAM,MAAM,UAAU,GAAG,CACvB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,mBAAmB,KACtB,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE3D,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,2BAA2B,GAAG,UAAU,CAqC3F"}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
import { randomUUID } from "node:crypto";
|
|
31
|
+
import { encodeDocumentControl, buildDocumentControlTbs, DOCUMENT_CONTROL_VERSION, } from "@cello-protocol/protocol-types";
|
|
32
|
+
export function createDocumentControlNotifier(deps) {
|
|
33
|
+
return async (documentId, verb) => {
|
|
34
|
+
for (const { agentName, ownerAgentId } of deps.owners()) {
|
|
35
|
+
const doc = deps.store.getDocument(ownerAgentId, documentId);
|
|
36
|
+
if (!doc)
|
|
37
|
+
continue;
|
|
38
|
+
const control = {
|
|
39
|
+
type: "document_control",
|
|
40
|
+
control_version: DOCUMENT_CONTROL_VERSION,
|
|
41
|
+
document_id: documentId,
|
|
42
|
+
sender_agent_id: ownerAgentId,
|
|
43
|
+
verb,
|
|
44
|
+
sent_at_ms: deps.now(),
|
|
45
|
+
signature: new Uint8Array(0),
|
|
46
|
+
};
|
|
47
|
+
let signature;
|
|
48
|
+
try {
|
|
49
|
+
signature = await deps.sign(agentName, buildDocumentControlTbs(control));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// REFUSED, not skipped and not sent unsigned. The peer must reject an unsigned control
|
|
53
|
+
// frame, so shipping one would report a notification that cannot possibly land — and the
|
|
54
|
+
// caller reports `peerNotified` straight to the operator.
|
|
55
|
+
return { ok: false, reason: "document_control_unsigned" };
|
|
56
|
+
}
|
|
57
|
+
control.signature = signature;
|
|
58
|
+
return deps.send(agentName, {
|
|
59
|
+
peerAgentId: doc.peerAgentId,
|
|
60
|
+
documentId,
|
|
61
|
+
bytes: encodeDocumentControl(control),
|
|
62
|
+
correlationId: randomUUID(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// No agent on this daemon holds it. Named as such rather than reported as a transport failure:
|
|
66
|
+
// the two want different things from whoever reads the log.
|
|
67
|
+
return { ok: false, reason: "document_unknown" };
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=document-control-notifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-control-notifier.js","sourceRoot":"","sources":["../src/document-control-notifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,GAGzB,MAAM,gCAAgC,CAAC;AAqBxC,MAAM,UAAU,6BAA6B,CAAC,IAAiC;IAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE;QAChC,KAAK,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACxD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;YAC7D,IAAI,CAAC,GAAG;gBAAE,SAAS;YAEnB,MAAM,OAAO,GAAoB;gBAC/B,IAAI,EAAE,kBAAkB;gBACxB,eAAe,EAAE,wBAAwB;gBACzC,WAAW,EAAE,UAAU;gBACvB,eAAe,EAAE,YAAY;gBAC7B,IAAI;gBACJ,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;gBACtB,SAAS,EAAE,IAAI,UAAU,CAAC,CAAC,CAAC;aAC7B,CAAC;YACF,IAAI,SAAqB,CAAC;YAC1B,IAAI,CAAC;gBACH,SAAS,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC;YAC3E,CAAC;YAAC,MAAM,CAAC;gBACP,uFAAuF;gBACvF,yFAAyF;gBACzF,0DAA0D;gBAC1D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,2BAA2B,EAAE,CAAC;YAC5D,CAAC;YACD,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;YAE9B,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE;gBAC1B,WAAW,EAAE,GAAG,CAAC,WAAW;gBAC5B,UAAU;gBACV,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC;gBACrC,aAAa,EAAE,UAAU,EAAE;aAC5B,CAAC,CAAC;QACL,CAAC;QACD,+FAA+F;QAC/F,4DAA4D;QAC5D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IACnD,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-DELIVERY-2 — the transport behind `DocumentDeliveryTransport` (§16.4).
|
|
3
|
+
*
|
|
4
|
+
* DELIVERY-1 built the scheduler and the bookkeeping against an injected seam. This is the seam's
|
|
5
|
+
* implementation: the first NON-HANDLER consumer of the session machinery, which is what makes it
|
|
6
|
+
* interesting — every other caller of `SessionNegotiator` runs because an agent asked for
|
|
7
|
+
* something, and this one runs because a document has a pending envelope and nobody is watching.
|
|
8
|
+
*
|
|
9
|
+
* ── REUSE BEFORE OPEN, AND WHY THAT ORDER ─────────────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* §16.4: "the daemon uses the most recent active session with that peer or opens one". Reuse first,
|
|
12
|
+
* because opening is the expensive half — a directory negotiation, a dial, and a seal — and a
|
|
13
|
+
* backlog of pending envelopes for one peer would otherwise pay it per envelope. It is also the
|
|
14
|
+
* behaviour an operator expects when they are mid-conversation about the document: the change lands
|
|
15
|
+
* in the same sealed record as the discussion, without anyone passing a session hint.
|
|
16
|
+
*
|
|
17
|
+
* An explicit `sessionHint` overrides the choice but NOT the validation — a hint naming a session
|
|
18
|
+
* that is not active with this peer is refused rather than silently replaced by the daemon's own
|
|
19
|
+
* pick, because the one reason to pass a hint is to control which sealed record the change lands
|
|
20
|
+
* in, and quietly choosing a different one defeats exactly that.
|
|
21
|
+
*
|
|
22
|
+
* ── WHAT AN ACK IS HERE, AND WHAT IT IS NOT ───────────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* `sendContent` reports that the content left, or was PARKED at the relay for an offline peer. That
|
|
25
|
+
* is a transport fact. The DoD's ack is a different one — "the peer's daemon confirms admission (or
|
|
26
|
+
* rejection)" — and it can only come from the peer's inbound document handler, which is its own
|
|
27
|
+
* line. So this adapter returns `admitted: null`: SENT, not acked.
|
|
28
|
+
*
|
|
29
|
+
* That third state is not hedging; both two-valued answers are dishonest here. `true` would mark
|
|
30
|
+
* the envelope acknowledged in the log while the peer may never have applied it, and the log being
|
|
31
|
+
* right about what the peer holds is the entire reason pending is derived from it. `false` would
|
|
32
|
+
* count a send that WORKED as a failure and re-send content already in flight — the
|
|
33
|
+
* permanent-redelivery shape this milestone has already fixed once. The worker records the envelope
|
|
34
|
+
* as delivered, leaves it unacked, and asks again on the capped backoff.
|
|
35
|
+
*/
|
|
36
|
+
import type { DocumentDeliveryTransport } from "./document-delivery.js";
|
|
37
|
+
import type { DocumentEnvelopeRow } from "./document-store.js";
|
|
38
|
+
import { DiscoveryUnavailableError } from "./document-reachability.js";
|
|
39
|
+
import type { DiscoveryOutcome } from "./cross-node-negotiation.js";
|
|
40
|
+
import type { Logger } from "./types.js";
|
|
41
|
+
export interface DocumentTransportDeps {
|
|
42
|
+
/**
|
|
43
|
+
* Take this frame's position in OUR OWN session tree, after it has gone out — the `0x04` doc leaf.
|
|
44
|
+
*
|
|
45
|
+
* `cello_send` does this and the document path did not, and the consequence is not a missing
|
|
46
|
+
* audit record: the tree is the sequence space both sides count in, so a sender that skips it
|
|
47
|
+
* falls one behind per frame and the peer silently drops what it has already consumed.
|
|
48
|
+
*/
|
|
49
|
+
appendLeaf(agentName: string, sessionId: string, contentHash: Uint8Array, correlationId: string): void;
|
|
50
|
+
/** The agent this worker delivers for. One worker per attended agent. */
|
|
51
|
+
agentName: string;
|
|
52
|
+
/** `runDiscoveryLookup`, supplied by the composition root — it lives in a closure there. */
|
|
53
|
+
lookupPeer(peerAgentId: string, correlationId: string): Promise<DiscoveryOutcome>;
|
|
54
|
+
/**
|
|
55
|
+
* Close and SEAL a session this adapter opened. §16.4: the autonomous session "still happens
|
|
56
|
+
* because it carries signing, encryption, and the seal" — an opened session left running is a
|
|
57
|
+
* live node the operator did not start and that never produces the sealed record the whole
|
|
58
|
+
* design exists for.
|
|
59
|
+
*/
|
|
60
|
+
sealSession(agentName: string, sessionId: string, correlationId: string): Promise<void>;
|
|
61
|
+
/** Active sessions with this peer, most recent LAST. */
|
|
62
|
+
activeSessionsWith(agentName: string, peerAgentId: string): string[];
|
|
63
|
+
/** The existing initiate path: negotiate, dial, create the session node. */
|
|
64
|
+
openSession(agentName: string, peerAgentId: string, correlationId: string): Promise<{
|
|
65
|
+
ok: true;
|
|
66
|
+
sessionId: string;
|
|
67
|
+
} | {
|
|
68
|
+
ok: false;
|
|
69
|
+
reason: string;
|
|
70
|
+
guidance?: string;
|
|
71
|
+
}>;
|
|
72
|
+
/** `SessionNodeManager.sendContent`. */
|
|
73
|
+
sendContent(agentName: string, sessionId: string, content: Uint8Array, contentHash: Uint8Array, correlationId: string): Promise<{
|
|
74
|
+
ok: true;
|
|
75
|
+
delivered: true;
|
|
76
|
+
} | {
|
|
77
|
+
ok: true;
|
|
78
|
+
delivered: false;
|
|
79
|
+
parked: true;
|
|
80
|
+
} | {
|
|
81
|
+
ok: false;
|
|
82
|
+
reason: string;
|
|
83
|
+
error: string;
|
|
84
|
+
}>;
|
|
85
|
+
/** Encode a document envelope row onto the wire, and its content hash. */
|
|
86
|
+
encodeEnvelope(envelope: DocumentEnvelopeRow): {
|
|
87
|
+
bytes: Uint8Array;
|
|
88
|
+
hash: Uint8Array;
|
|
89
|
+
};
|
|
90
|
+
logger: Logger;
|
|
91
|
+
}
|
|
92
|
+
export declare function createDocumentDeliveryTransport(deps: DocumentTransportDeps): DocumentDeliveryTransport;
|
|
93
|
+
export { DiscoveryUnavailableError };
|
|
94
|
+
//# sourceMappingURL=document-delivery-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-delivery-transport.d.ts","sourceRoot":"","sources":["../src/document-delivery-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAExE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAA6B,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAClG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACvG,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAClF;;;;;OAKG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxF,wDAAwD;IACxD,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACrE,4EAA4E;IAC5E,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/F,wCAAwC;IACxC,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,UAAU,EACnB,WAAW,EAAE,UAAU,EACvB,aAAa,EAAE,MAAM,GACpB,OAAO,CACN;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE,GAC7B;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,IAAI,CAAA;KAAE,GAC5C;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAC/C,CAAC;IACF,0EAA0E;IAC1E,cAAc,CAAC,QAAQ,EAAE,mBAAmB,GAAG;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,UAAU,CAAA;KAAE,CAAC;IACvF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,qBAAqB,GAC1B,yBAAyB,CAqJ3B;AAED,OAAO,EAAE,yBAAyB,EAAE,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-DOC-DELIVERY-2 — the transport behind `DocumentDeliveryTransport` (§16.4).
|
|
3
|
+
*
|
|
4
|
+
* DELIVERY-1 built the scheduler and the bookkeeping against an injected seam. This is the seam's
|
|
5
|
+
* implementation: the first NON-HANDLER consumer of the session machinery, which is what makes it
|
|
6
|
+
* interesting — every other caller of `SessionNegotiator` runs because an agent asked for
|
|
7
|
+
* something, and this one runs because a document has a pending envelope and nobody is watching.
|
|
8
|
+
*
|
|
9
|
+
* ── REUSE BEFORE OPEN, AND WHY THAT ORDER ─────────────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* §16.4: "the daemon uses the most recent active session with that peer or opens one". Reuse first,
|
|
12
|
+
* because opening is the expensive half — a directory negotiation, a dial, and a seal — and a
|
|
13
|
+
* backlog of pending envelopes for one peer would otherwise pay it per envelope. It is also the
|
|
14
|
+
* behaviour an operator expects when they are mid-conversation about the document: the change lands
|
|
15
|
+
* in the same sealed record as the discussion, without anyone passing a session hint.
|
|
16
|
+
*
|
|
17
|
+
* An explicit `sessionHint` overrides the choice but NOT the validation — a hint naming a session
|
|
18
|
+
* that is not active with this peer is refused rather than silently replaced by the daemon's own
|
|
19
|
+
* pick, because the one reason to pass a hint is to control which sealed record the change lands
|
|
20
|
+
* in, and quietly choosing a different one defeats exactly that.
|
|
21
|
+
*
|
|
22
|
+
* ── WHAT AN ACK IS HERE, AND WHAT IT IS NOT ───────────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* `sendContent` reports that the content left, or was PARKED at the relay for an offline peer. That
|
|
25
|
+
* is a transport fact. The DoD's ack is a different one — "the peer's daemon confirms admission (or
|
|
26
|
+
* rejection)" — and it can only come from the peer's inbound document handler, which is its own
|
|
27
|
+
* line. So this adapter returns `admitted: null`: SENT, not acked.
|
|
28
|
+
*
|
|
29
|
+
* That third state is not hedging; both two-valued answers are dishonest here. `true` would mark
|
|
30
|
+
* the envelope acknowledged in the log while the peer may never have applied it, and the log being
|
|
31
|
+
* right about what the peer holds is the entire reason pending is derived from it. `false` would
|
|
32
|
+
* count a send that WORKED as a failure and re-send content already in flight — the
|
|
33
|
+
* permanent-redelivery shape this milestone has already fixed once. The worker records the envelope
|
|
34
|
+
* as delivered, leaves it unacked, and asks again on the capped backoff.
|
|
35
|
+
*/
|
|
36
|
+
import { wireContentHash } from "./wire-content-hash.js";
|
|
37
|
+
import { reachabilityFromDiscovery, DiscoveryUnavailableError } from "./document-reachability.js";
|
|
38
|
+
export function createDocumentDeliveryTransport(deps) {
|
|
39
|
+
/**
|
|
40
|
+
* Acquire a session with the peer — the hint, then the most recent active one, then a fresh dial.
|
|
41
|
+
*
|
|
42
|
+
* ONE implementation for every document frame kind. A proposal that opened its own session by a
|
|
43
|
+
* separate code path would drift on which session gets reused and on whether a session this
|
|
44
|
+
* daemon opened is sealed, and both are unrecoverable after the fact.
|
|
45
|
+
*/
|
|
46
|
+
async function acquireSession(peerAgentId, sessionHint, correlationId) {
|
|
47
|
+
const active = deps.activeSessionsWith(deps.agentName, peerAgentId);
|
|
48
|
+
if (sessionHint !== undefined) {
|
|
49
|
+
if (!active.includes(sessionHint)) {
|
|
50
|
+
// Refused, not replaced. The only reason to pass a hint is to control which sealed record
|
|
51
|
+
// the change lands in; quietly substituting the daemon's own pick would defeat exactly
|
|
52
|
+
// that, and it would do so silently.
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: "document_session_hint_invalid",
|
|
56
|
+
detail: `session ${sessionHint.slice(0, 16)}… is not an active session with ${peerAgentId}, ` +
|
|
57
|
+
`so the change cannot be placed in that record`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, sessionId: sessionHint, sessionOpened: false };
|
|
61
|
+
}
|
|
62
|
+
// Most recent LAST — activeSessionsWith is ordered oldest-first by the daemon's adapter.
|
|
63
|
+
if (active.length > 0)
|
|
64
|
+
return { ok: true, sessionId: active[active.length - 1], sessionOpened: false };
|
|
65
|
+
const opened = await deps.openSession(deps.agentName, peerAgentId, correlationId);
|
|
66
|
+
if (!opened.ok) {
|
|
67
|
+
// The upstream reason verbatim. `document_delivery_threw` is reserved for a genuine
|
|
68
|
+
// programming fault; a dial that was refused should say it was refused.
|
|
69
|
+
return { ok: false, reason: opened.reason, detail: opened.guidance };
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, sessionId: opened.sessionId, sessionOpened: true };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
async isPeerReachable(peerAgentId, correlationId) {
|
|
75
|
+
// The PASS's correlation id, threaded through. A per-peer constant was fabricated here, so
|
|
76
|
+
// every `directory.discovery.lookup` this worker ever emitted carried the same string and
|
|
77
|
+
// none of them joined to the pass — breaking the thread at exactly the hop an operator needs
|
|
78
|
+
// when nothing is syncing.
|
|
79
|
+
const outcome = await deps.lookupPeer(peerAgentId, correlationId);
|
|
80
|
+
// Throws on everything that is not an answer about the peer — see document-reachability.ts.
|
|
81
|
+
// The worker treats the throw as `lookup_failed` and keeps it out of the offline-peer count.
|
|
82
|
+
// The unknown-agent bit is RETURNED rather than logged here, so the worker can announce it
|
|
83
|
+
// against the document it belongs to.
|
|
84
|
+
return reachabilityFromDiscovery(outcome);
|
|
85
|
+
},
|
|
86
|
+
async sendBytes(input) {
|
|
87
|
+
const { peerAgentId, documentId, bytes, sessionHint, correlationId } = input;
|
|
88
|
+
const session = await acquireSession(peerAgentId, sessionHint, correlationId);
|
|
89
|
+
if (!session.ok)
|
|
90
|
+
return session;
|
|
91
|
+
// THE WIRE HASH, domain-separated. This was `sha256(bytes)`, and the receiver recomputes
|
|
92
|
+
// `sha256(0x00 || bytes)` for every frame — so the send reported success, `parked: false`,
|
|
93
|
+
// and the peer discarded it at the authenticity check before the document layer was ever
|
|
94
|
+
// consulted. Found by two real daemons; no in-process test could see it, because both sides
|
|
95
|
+
// of those compute the hash with the same function.
|
|
96
|
+
const hash = wireContentHash(bytes);
|
|
97
|
+
const sent = await deps.sendContent(deps.agentName, session.sessionId, bytes, hash, correlationId);
|
|
98
|
+
if (!sent.ok) {
|
|
99
|
+
// Sealed even on a send failure, for the same reason as below: a session this daemon opened
|
|
100
|
+
// and walked away from is a live node the operator never started. A failed send is exactly
|
|
101
|
+
// when that is most likely to happen.
|
|
102
|
+
if (session.sessionOpened)
|
|
103
|
+
await deps.sealSession(deps.agentName, session.sessionId, correlationId);
|
|
104
|
+
return { ok: false, reason: sent.reason, detail: sent.error };
|
|
105
|
+
}
|
|
106
|
+
// APPEND OUR OWN LEAF, exactly as `cello_send` does after a successful send.
|
|
107
|
+
//
|
|
108
|
+
// Not bookkeeping. The daemon's session tree is the sequence space both sides count in, and a
|
|
109
|
+
// sender that puts content on the wire without taking its leaf position leaves its own chain
|
|
110
|
+
// one behind for every frame it sends. The peer then sees a sequence it has already consumed
|
|
111
|
+
// and drops the frame — silently, because a duplicate is a normal event, so nothing is logged
|
|
112
|
+
// anywhere and the send reports success with `parked: false`.
|
|
113
|
+
//
|
|
114
|
+
// It hid behind an accident: with no prior traffic every frame is sequence 0, so the FIRST
|
|
115
|
+
// document frame in a fresh session arrives and everything after it does not. Adding one
|
|
116
|
+
// ordinary message before the exchange moved the failure earlier, which is what named the
|
|
117
|
+
// cause.
|
|
118
|
+
deps.appendLeaf(deps.agentName, session.sessionId, hash, correlationId);
|
|
119
|
+
deps.logger.info("document.frame.sent", {
|
|
120
|
+
documentId,
|
|
121
|
+
sessionId: session.sessionId,
|
|
122
|
+
sessionOpened: session.sessionOpened,
|
|
123
|
+
bytes: bytes.length,
|
|
124
|
+
parked: sent.delivered === false,
|
|
125
|
+
correlationId,
|
|
126
|
+
});
|
|
127
|
+
if (session.sessionOpened)
|
|
128
|
+
await deps.sealSession(deps.agentName, session.sessionId, correlationId);
|
|
129
|
+
return { ok: true, sessionId: session.sessionId, sessionOpened: session.sessionOpened };
|
|
130
|
+
},
|
|
131
|
+
async deliver(input) {
|
|
132
|
+
const { peerAgentId, documentId, envelope, sessionHint, correlationId } = input;
|
|
133
|
+
const session = await acquireSession(peerAgentId, sessionHint, correlationId);
|
|
134
|
+
if (!session.ok)
|
|
135
|
+
return session;
|
|
136
|
+
const { sessionId, sessionOpened } = session;
|
|
137
|
+
const { bytes, hash } = deps.encodeEnvelope(envelope);
|
|
138
|
+
const sent = await deps.sendContent(deps.agentName, sessionId, bytes, hash, correlationId);
|
|
139
|
+
if (sent.ok)
|
|
140
|
+
deps.appendLeaf(deps.agentName, sessionId, hash, correlationId);
|
|
141
|
+
if (!sent.ok) {
|
|
142
|
+
// SEAL WHAT WE OPENED, on the failure path too. This branch walked away from a session it
|
|
143
|
+
// had just dialled — a live node the operator never started, with no sealed record, which
|
|
144
|
+
// is precisely what the rule twenty lines below says the seal exists to prevent. A failed
|
|
145
|
+
// send is when it is most likely to happen, not least.
|
|
146
|
+
if (sessionOpened)
|
|
147
|
+
await deps.sealSession(deps.agentName, sessionId, correlationId);
|
|
148
|
+
return { ok: false, reason: sent.reason, detail: sent.error };
|
|
149
|
+
}
|
|
150
|
+
deps.logger.info("document.delivery.sent", {
|
|
151
|
+
documentId,
|
|
152
|
+
sessionId,
|
|
153
|
+
sessionOpened,
|
|
154
|
+
parked: sent.delivered === false,
|
|
155
|
+
correlationId,
|
|
156
|
+
});
|
|
157
|
+
// SEAL what we opened. §16.4: the autonomous session still happens because it carries
|
|
158
|
+
// signing, encryption and the seal — the ceremony goes to zero, the seal does not. A session
|
|
159
|
+
// this adapter opened and walked away from is a live node the operator never started, and
|
|
160
|
+
// the sealed record the design exists to produce is never produced. A session we REUSED is
|
|
161
|
+
// not ours to close: its owner decides when that conversation ends.
|
|
162
|
+
if (sessionOpened) {
|
|
163
|
+
await deps.sealSession(deps.agentName, sessionId, correlationId);
|
|
164
|
+
}
|
|
165
|
+
// SENT, NOT ACKED — `admitted: null`. The content left (or was parked for an offline peer),
|
|
166
|
+
// which is a transport fact; the DoD's ack is the peer's daemon confirming admission, and
|
|
167
|
+
// that answer comes from the inbound document handler, which is its own line.
|
|
168
|
+
//
|
|
169
|
+
// Neither of the two-valued answers is available honestly. `true` would mark the envelope
|
|
170
|
+
// acknowledged in the log while the peer may never have applied it — and the log being right
|
|
171
|
+
// about what the peer holds is the entire reason pending is derived from it. `false` would
|
|
172
|
+
// count a send that WORKED as a failure and re-send content already in flight, which is the
|
|
173
|
+
// permanent-redelivery shape this milestone already fixed once.
|
|
174
|
+
return { ok: true, sessionId, sessionOpened, admitted: null };
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export { DiscoveryUnavailableError };
|
|
179
|
+
//# sourceMappingURL=document-delivery-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-delivery-transport.js","sourceRoot":"","sources":["../src/document-delivery-transport.ts"],"names":[],"mappings":"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"}
|