@cello-protocol/daemon 0.0.131 → 0.0.133

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/content-park.d.ts.map +1 -1
  2. package/dist/content-park.js +29 -11
  3. package/dist/content-park.js.map +1 -1
  4. package/dist/daemon.d.ts +11 -0
  5. package/dist/daemon.d.ts.map +1 -1
  6. package/dist/daemon.js +398 -18
  7. package/dist/daemon.js.map +1 -1
  8. package/dist/document-ack-inbound.d.ts +57 -0
  9. package/dist/document-ack-inbound.d.ts.map +1 -0
  10. package/dist/document-ack-inbound.js +174 -0
  11. package/dist/document-ack-inbound.js.map +1 -0
  12. package/dist/document-control-notifier.d.ts +61 -0
  13. package/dist/document-control-notifier.d.ts.map +1 -0
  14. package/dist/document-control-notifier.js +70 -0
  15. package/dist/document-control-notifier.js.map +1 -0
  16. package/dist/document-delivery-transport.d.ts +94 -0
  17. package/dist/document-delivery-transport.d.ts.map +1 -0
  18. package/dist/document-delivery-transport.js +179 -0
  19. package/dist/document-delivery-transport.js.map +1 -0
  20. package/dist/document-delivery.d.ts +181 -0
  21. package/dist/document-delivery.d.ts.map +1 -0
  22. package/dist/document-delivery.js +289 -0
  23. package/dist/document-delivery.js.map +1 -0
  24. package/dist/document-frame-router.d.ts +210 -0
  25. package/dist/document-frame-router.d.ts.map +1 -0
  26. package/dist/document-frame-router.js +396 -0
  27. package/dist/document-frame-router.js.map +1 -0
  28. package/dist/document-handlers.d.ts +47 -0
  29. package/dist/document-handlers.d.ts.map +1 -0
  30. package/dist/document-handlers.js +657 -0
  31. package/dist/document-handlers.js.map +1 -0
  32. package/dist/document-handshake.d.ts +156 -0
  33. package/dist/document-handshake.d.ts.map +1 -0
  34. package/dist/document-handshake.js +398 -0
  35. package/dist/document-handshake.js.map +1 -0
  36. package/dist/document-inbound.d.ts +91 -0
  37. package/dist/document-inbound.d.ts.map +1 -0
  38. package/dist/document-inbound.js +290 -0
  39. package/dist/document-inbound.js.map +1 -0
  40. package/dist/document-layer.d.ts +137 -0
  41. package/dist/document-layer.d.ts.map +1 -0
  42. package/dist/document-layer.js +255 -0
  43. package/dist/document-layer.js.map +1 -0
  44. package/dist/document-lifecycle.d.ts +125 -0
  45. package/dist/document-lifecycle.d.ts.map +1 -0
  46. package/dist/document-lifecycle.js +433 -0
  47. package/dist/document-lifecycle.js.map +1 -0
  48. package/dist/document-live-docs.d.ts +58 -0
  49. package/dist/document-live-docs.d.ts.map +1 -0
  50. package/dist/document-live-docs.js +126 -0
  51. package/dist/document-live-docs.js.map +1 -0
  52. package/dist/document-notify.d.ts +173 -0
  53. package/dist/document-notify.d.ts.map +1 -0
  54. package/dist/document-notify.js +438 -0
  55. package/dist/document-notify.js.map +1 -0
  56. package/dist/document-publish.d.ts +67 -0
  57. package/dist/document-publish.d.ts.map +1 -0
  58. package/dist/document-publish.js +149 -0
  59. package/dist/document-publish.js.map +1 -0
  60. package/dist/document-reachability.d.ts +42 -0
  61. package/dist/document-reachability.d.ts.map +1 -0
  62. package/dist/document-reachability.js +80 -0
  63. package/dist/document-reachability.js.map +1 -0
  64. package/dist/document-rejection.d.ts +240 -0
  65. package/dist/document-rejection.d.ts.map +1 -0
  66. package/dist/document-rejection.js +407 -0
  67. package/dist/document-rejection.js.map +1 -0
  68. package/dist/document-store.d.ts +154 -8
  69. package/dist/document-store.d.ts.map +1 -1
  70. package/dist/document-store.js +462 -4
  71. package/dist/document-store.js.map +1 -1
  72. package/dist/document-write-path.d.ts.map +1 -1
  73. package/dist/document-write-path.js +10 -43
  74. package/dist/document-write-path.js.map +1 -1
  75. package/dist/inbound-sessions.d.ts +25 -0
  76. package/dist/inbound-sessions.d.ts.map +1 -1
  77. package/dist/inbound-sessions.js +60 -0
  78. package/dist/inbound-sessions.js.map +1 -1
  79. package/dist/initiate-session-handler.d.ts +24 -1
  80. package/dist/initiate-session-handler.d.ts.map +1 -1
  81. package/dist/initiate-session-handler.js +35 -9
  82. package/dist/initiate-session-handler.js.map +1 -1
  83. package/dist/ipc-server.d.ts +11 -1
  84. package/dist/ipc-server.d.ts.map +1 -1
  85. package/dist/ipc-server.js +7 -1
  86. package/dist/ipc-server.js.map +1 -1
  87. package/dist/line-lcs.d.ts +51 -0
  88. package/dist/line-lcs.d.ts.map +1 -0
  89. package/dist/line-lcs.js +71 -0
  90. package/dist/line-lcs.js.map +1 -0
  91. package/dist/notification-handlers.d.ts +2 -1
  92. package/dist/notification-handlers.d.ts.map +1 -1
  93. package/dist/notification-handlers.js +11 -2
  94. package/dist/notification-handlers.js.map +1 -1
  95. package/dist/outbound-sessions.d.ts +2 -0
  96. package/dist/outbound-sessions.d.ts.map +1 -1
  97. package/dist/outbound-sessions.js +23 -2
  98. package/dist/outbound-sessions.js.map +1 -1
  99. package/dist/session-content-handlers.d.ts.map +1 -1
  100. package/dist/session-content-handlers.js +3 -2
  101. package/dist/session-content-handlers.js.map +1 -1
  102. package/dist/session-node-manager.d.ts +15 -0
  103. package/dist/session-node-manager.d.ts.map +1 -1
  104. package/dist/session-node-manager.js +188 -9
  105. package/dist/session-node-manager.js.map +1 -1
  106. package/dist/vocabulary.d.ts.map +1 -1
  107. package/dist/vocabulary.js +16 -0
  108. package/dist/vocabulary.js.map +1 -1
  109. package/dist/wire-content-hash.d.ts +27 -0
  110. package/dist/wire-content-hash.d.ts.map +1 -0
  111. package/dist/wire-content-hash.js +37 -0
  112. package/dist/wire-content-hash.js.map +1 -0
  113. 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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"}