@cello-protocol/daemon 0.0.168 → 0.0.170
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/agent-id-migration.d.ts.map +1 -1
- package/dist/agent-id-migration.js +16 -0
- package/dist/agent-id-migration.js.map +1 -1
- package/dist/away-detection.d.ts +62 -15
- package/dist/away-detection.d.ts.map +1 -1
- package/dist/away-detection.js +77 -20
- package/dist/away-detection.js.map +1 -1
- package/dist/close-session-handler.d.ts.map +1 -1
- package/dist/close-session-handler.js +69 -3
- package/dist/close-session-handler.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +406 -303
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-open-registry.d.ts +92 -0
- package/dist/delivery-open-registry.d.ts.map +1 -0
- package/dist/delivery-open-registry.js +121 -0
- package/dist/delivery-open-registry.js.map +1 -0
- 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 +51 -53
- package/dist/document-delivery-transport.d.ts.map +1 -1
- package/dist/document-delivery-transport.js +16 -157
- 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 +462 -511
- 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 +49 -67
- package/dist/document-layer.d.ts.map +1 -1
- package/dist/document-layer.js +688 -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 -568
- 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 +124 -0
- package/dist/document-reconcile-scheduler.d.ts.map +1 -0
- package/dist/document-reconcile-scheduler.js +226 -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 -247
- package/dist/document-store.d.ts.map +1 -1
- package/dist/document-store.js +95 -827
- package/dist/document-store.js.map +1 -1
- package/dist/inbound-sessions.d.ts +7 -0
- package/dist/inbound-sessions.d.ts.map +1 -1
- package/dist/inbound-sessions.js +27 -6
- package/dist/inbound-sessions.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/dist/notification-handlers.d.ts.map +1 -1
- package/dist/notification-handlers.js +39 -2
- package/dist/notification-handlers.js.map +1 -1
- package/dist/session-content-handlers.d.ts.map +1 -1
- package/dist/session-content-handlers.js +134 -8
- package/dist/session-content-handlers.js.map +1 -1
- package/dist/session-node-manager.d.ts +153 -8
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +1376 -35
- package/dist/session-node-manager.js.map +1 -1
- package/dist/types.d.ts +64 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.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 -124
- package/dist/document-control-notifier.d.ts.map +0 -1
- package/dist/document-control-notifier.js +0 -176
- package/dist/document-control-notifier.js.map +0 -1
- package/dist/document-delivery.d.ts +0 -250
- package/dist/document-delivery.d.ts.map +0 -1
- package/dist/document-delivery.js +0 -608
- 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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-M12B-DELIVERY-QUIET-1 — which sessions this daemon's DOCUMENT DELIVERY worker is opening.
|
|
3
|
+
*
|
|
4
|
+
* ── THE LOOP THIS EXISTS TO BREAK ────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Two reachability triggers fire when a session comes up, and both are correct when a PEER caused
|
|
7
|
+
* the session:
|
|
8
|
+
*
|
|
9
|
+
* 1. The initiator half — `onSessionOpened` in `initiate-session-handler.ts`, fired by the side
|
|
10
|
+
* that dialled.
|
|
11
|
+
* 2. The inbound half — `dispatchSessionStateChangedWithTelegram` on `state === "created"`,
|
|
12
|
+
* fired by the side that accepted, which ALSO rings the conversation doorbell and pushes a
|
|
13
|
+
* Telegram notification.
|
|
14
|
+
*
|
|
15
|
+
* Either one calls `ReconcileScheduler.onReachable`, and that is not a nudge: it sets
|
|
16
|
+
* `failures = 0` and `nextAttemptMs = 0` and sweeps every shared document immediately. The stated
|
|
17
|
+
* rationale is sound — "the backoff modeled 'they do not answer', and here they demonstrably just
|
|
18
|
+
* did" (SYNC-P5 R39 trigger 2) — but it is only sound when the peer caused the session.
|
|
19
|
+
*
|
|
20
|
+
* When the DELIVERY WORKER opened it, the "signal" is our own outbound act reflected back at us. We
|
|
21
|
+
* learn nothing about the peer, and we wipe a backoff that a refusal — possibly that same peer's
|
|
22
|
+
* refusal, seconds earlier — had just set. Then:
|
|
23
|
+
*
|
|
24
|
+
* the sweep needs a session → it opens one → creation fires the trigger → the trigger zeroes the
|
|
25
|
+
* backoff and sweeps every document → the sweep needs sessions → …
|
|
26
|
+
*
|
|
27
|
+
* Measured 2026-08-17: 321 reconcile attempts in 85 minutes driving 53 sessions and 63 standing-
|
|
28
|
+
* receiver rebuilds. After `DOD-SYNC-REFUSAL-BACKOFF-1` removed the refusal storm, still 55
|
|
29
|
+
* attempts in 20 minutes with 0 refusals — the volume that fix did not reach is this trigger.
|
|
30
|
+
*
|
|
31
|
+
* ── WHY AN INTENT, KEYED ON THE PEER, AND NOT A SESSION ID ───────────────────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* The obvious design is to record the session id the delivery worker opened. It does not work: the
|
|
34
|
+
* id is minted by the DIRECTORY and only reaches the opener at the end of negotiation, whereas the
|
|
35
|
+
* directory has already pushed the assignment to the counterparty — which, when both agents live on
|
|
36
|
+
* one daemon (the measured configuration), is this same process. The inbound doorbell therefore
|
|
37
|
+
* fires BEFORE the opener ever learns the id, so a session-id registry is always too late.
|
|
38
|
+
*
|
|
39
|
+
* So what is registered is the INTENT — "this local agent's delivery worker is opening a session
|
|
40
|
+
* with this peer right now" — before negotiation starts, and released when the open returns.
|
|
41
|
+
*
|
|
42
|
+
* ── THE KNOWN IMPRECISION, STATED RATHER THAN HIDDEN ─────────────────────────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* If a delivery open to peer P is in flight for agent A at the same instant A's OPERATOR opens a
|
|
45
|
+
* session to the same P, the inbound half cannot tell the two assignments apart and will suppress
|
|
46
|
+
* the operator's doorbell too. It is narrow (same local agent, same peer, overlapping windows) and
|
|
47
|
+
* it costs one missed notification for a session the operator started themselves and is therefore
|
|
48
|
+
* already watching. The alternative — suppressing nothing — is the storm. This is the same class of
|
|
49
|
+
* accepted, journaled imprecision as the away-ack coalescing guard in `daemon.ts`.
|
|
50
|
+
*
|
|
51
|
+
* Counting, not a boolean: two documents can be delivering to the same peer concurrently, and a
|
|
52
|
+
* boolean cleared by whichever finishes first would re-open the loop for the other.
|
|
53
|
+
*/
|
|
54
|
+
/** Who caused a session to exist. Decides whether its creation is a signal about the peer. */
|
|
55
|
+
export type SessionOpenedBy = "operator" | "document_delivery";
|
|
56
|
+
export interface DeliveryOpenRegistry {
|
|
57
|
+
/**
|
|
58
|
+
* Register that `openerPubkey`'s delivery worker is opening a session with `targetPubkey`.
|
|
59
|
+
* Returns the release function.
|
|
60
|
+
*/
|
|
61
|
+
begin(openerPubkey: string, targetPubkey: string): () => void;
|
|
62
|
+
/**
|
|
63
|
+
* True while a delivery open from `openerPubkey` toward `targetPubkey` is in flight.
|
|
64
|
+
*
|
|
65
|
+
* ORDER IS THE WHOLE THING. The two halves that ask this question stand at opposite ends of the
|
|
66
|
+
* same dial, so one of them must reverse its own arguments:
|
|
67
|
+
*
|
|
68
|
+
* initiator half — WE opened it: isDeliveryOpening(us, them)
|
|
69
|
+
* inbound half — THEY opened it: isDeliveryOpening(them, us)
|
|
70
|
+
*
|
|
71
|
+
* The first version of this registry keyed on (agent NAME, peer PUBKEY) and both halves passed
|
|
72
|
+
* their own local agent first. The initiator half matched; the inbound half asked
|
|
73
|
+
* `(receiver-name, initiator-pubkey)` against a registry holding `(initiator-name, target-pubkey)`
|
|
74
|
+
* — two tuples that can never be equal — so the doorbell guard read as protection and was
|
|
75
|
+
* unreachable in production, and the log line meant to prove it worked never fired either.
|
|
76
|
+
* Pubkeys on BOTH ends is what lets the two halves name the same pair at all.
|
|
77
|
+
*/
|
|
78
|
+
isDeliveryOpening(openerPubkey: string, targetPubkey: string): boolean;
|
|
79
|
+
/** In-flight count. Exposed for assertions and for the leak check a test can make. */
|
|
80
|
+
inFlight(): number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* How long an entry may claim to be "in flight" before it is ignored.
|
|
84
|
+
*
|
|
85
|
+
* `openSessionAs` carries no deadline of its own, so a dial that never settles would leave the
|
|
86
|
+
* intent registered and silence that peer's doorbell for the life of the process — a worse outage
|
|
87
|
+
* than the storm this exists to stop. A dial that has been in flight for two minutes is not in
|
|
88
|
+
* flight; it is stuck, and a stuck dial must not be able to mute anything.
|
|
89
|
+
*/
|
|
90
|
+
export declare const DELIVERY_OPEN_STALE_MS = 120000;
|
|
91
|
+
export declare function createDeliveryOpenRegistry(now?: () => number): DeliveryOpenRegistry;
|
|
92
|
+
//# sourceMappingURL=delivery-open-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-open-registry.d.ts","sourceRoot":"","sources":["../src/delivery-open-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,8FAA8F;AAC9F,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,mBAAmB,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,IAAI,CAAC;IAC9D;;;;;;;;;;;;;;;OAeG;IACH,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;IACvE,sFAAsF;IACtF,QAAQ,IAAI,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,SAAU,CAAC;AAE9C,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,MAAiB,GAAG,oBAAoB,CAoD7F"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-M12B-DELIVERY-QUIET-1 — which sessions this daemon's DOCUMENT DELIVERY worker is opening.
|
|
3
|
+
*
|
|
4
|
+
* ── THE LOOP THIS EXISTS TO BREAK ────────────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Two reachability triggers fire when a session comes up, and both are correct when a PEER caused
|
|
7
|
+
* the session:
|
|
8
|
+
*
|
|
9
|
+
* 1. The initiator half — `onSessionOpened` in `initiate-session-handler.ts`, fired by the side
|
|
10
|
+
* that dialled.
|
|
11
|
+
* 2. The inbound half — `dispatchSessionStateChangedWithTelegram` on `state === "created"`,
|
|
12
|
+
* fired by the side that accepted, which ALSO rings the conversation doorbell and pushes a
|
|
13
|
+
* Telegram notification.
|
|
14
|
+
*
|
|
15
|
+
* Either one calls `ReconcileScheduler.onReachable`, and that is not a nudge: it sets
|
|
16
|
+
* `failures = 0` and `nextAttemptMs = 0` and sweeps every shared document immediately. The stated
|
|
17
|
+
* rationale is sound — "the backoff modeled 'they do not answer', and here they demonstrably just
|
|
18
|
+
* did" (SYNC-P5 R39 trigger 2) — but it is only sound when the peer caused the session.
|
|
19
|
+
*
|
|
20
|
+
* When the DELIVERY WORKER opened it, the "signal" is our own outbound act reflected back at us. We
|
|
21
|
+
* learn nothing about the peer, and we wipe a backoff that a refusal — possibly that same peer's
|
|
22
|
+
* refusal, seconds earlier — had just set. Then:
|
|
23
|
+
*
|
|
24
|
+
* the sweep needs a session → it opens one → creation fires the trigger → the trigger zeroes the
|
|
25
|
+
* backoff and sweeps every document → the sweep needs sessions → …
|
|
26
|
+
*
|
|
27
|
+
* Measured 2026-08-17: 321 reconcile attempts in 85 minutes driving 53 sessions and 63 standing-
|
|
28
|
+
* receiver rebuilds. After `DOD-SYNC-REFUSAL-BACKOFF-1` removed the refusal storm, still 55
|
|
29
|
+
* attempts in 20 minutes with 0 refusals — the volume that fix did not reach is this trigger.
|
|
30
|
+
*
|
|
31
|
+
* ── WHY AN INTENT, KEYED ON THE PEER, AND NOT A SESSION ID ───────────────────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* The obvious design is to record the session id the delivery worker opened. It does not work: the
|
|
34
|
+
* id is minted by the DIRECTORY and only reaches the opener at the end of negotiation, whereas the
|
|
35
|
+
* directory has already pushed the assignment to the counterparty — which, when both agents live on
|
|
36
|
+
* one daemon (the measured configuration), is this same process. The inbound doorbell therefore
|
|
37
|
+
* fires BEFORE the opener ever learns the id, so a session-id registry is always too late.
|
|
38
|
+
*
|
|
39
|
+
* So what is registered is the INTENT — "this local agent's delivery worker is opening a session
|
|
40
|
+
* with this peer right now" — before negotiation starts, and released when the open returns.
|
|
41
|
+
*
|
|
42
|
+
* ── THE KNOWN IMPRECISION, STATED RATHER THAN HIDDEN ─────────────────────────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* If a delivery open to peer P is in flight for agent A at the same instant A's OPERATOR opens a
|
|
45
|
+
* session to the same P, the inbound half cannot tell the two assignments apart and will suppress
|
|
46
|
+
* the operator's doorbell too. It is narrow (same local agent, same peer, overlapping windows) and
|
|
47
|
+
* it costs one missed notification for a session the operator started themselves and is therefore
|
|
48
|
+
* already watching. The alternative — suppressing nothing — is the storm. This is the same class of
|
|
49
|
+
* accepted, journaled imprecision as the away-ack coalescing guard in `daemon.ts`.
|
|
50
|
+
*
|
|
51
|
+
* Counting, not a boolean: two documents can be delivering to the same peer concurrently, and a
|
|
52
|
+
* boolean cleared by whichever finishes first would re-open the loop for the other.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* How long an entry may claim to be "in flight" before it is ignored.
|
|
56
|
+
*
|
|
57
|
+
* `openSessionAs` carries no deadline of its own, so a dial that never settles would leave the
|
|
58
|
+
* intent registered and silence that peer's doorbell for the life of the process — a worse outage
|
|
59
|
+
* than the storm this exists to stop. A dial that has been in flight for two minutes is not in
|
|
60
|
+
* flight; it is stuck, and a stuck dial must not be able to mute anything.
|
|
61
|
+
*/
|
|
62
|
+
export const DELIVERY_OPEN_STALE_MS = 120_000;
|
|
63
|
+
export function createDeliveryOpenRegistry(now = Date.now) {
|
|
64
|
+
// Pubkeys reach this code from a wire assignment on one side and from a caller's parameter on the
|
|
65
|
+
// other, and those two do not agree on case. Lowercased at the boundary so a comparison here can
|
|
66
|
+
// never be the reason the suppression silently stops working.
|
|
67
|
+
//
|
|
68
|
+
// NUL separator, written as an escape rather than a raw byte: a literal U+0000 in the source made
|
|
69
|
+
// this file binary to git, so `git show` printed no diff for it at all and the one new module in
|
|
70
|
+
// the unit could not be reviewed through the normal path.
|
|
71
|
+
const key = (openerPubkey, targetPubkey) => `${openerPubkey.toLowerCase()}\u0000${targetPubkey.toLowerCase()}`;
|
|
72
|
+
// Start times, newest last. An array rather than a count so a stale entry can be dropped
|
|
73
|
+
// individually — a bare counter cannot tell which of two in-flight opens is the wedged one.
|
|
74
|
+
const opens = new Map();
|
|
75
|
+
const live = (k) => {
|
|
76
|
+
const all = opens.get(k);
|
|
77
|
+
if (all === undefined)
|
|
78
|
+
return [];
|
|
79
|
+
const cutoff = now() - DELIVERY_OPEN_STALE_MS;
|
|
80
|
+
const fresh = all.filter((startedAt) => startedAt > cutoff);
|
|
81
|
+
if (fresh.length === 0)
|
|
82
|
+
opens.delete(k);
|
|
83
|
+
else if (fresh.length !== all.length)
|
|
84
|
+
opens.set(k, fresh);
|
|
85
|
+
return fresh;
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
begin(openerPubkey, targetPubkey) {
|
|
89
|
+
const k = key(openerPubkey, targetPubkey);
|
|
90
|
+
const startedAt = now();
|
|
91
|
+
opens.set(k, [...live(k), startedAt]);
|
|
92
|
+
let released = false;
|
|
93
|
+
return () => {
|
|
94
|
+
// Idempotent: a caller that releases twice must not drop someone else's entry and leave the
|
|
95
|
+
// doorbell suppressed for a peer whose open has not finished.
|
|
96
|
+
if (released)
|
|
97
|
+
return;
|
|
98
|
+
released = true;
|
|
99
|
+
const remaining = opens.get(k);
|
|
100
|
+
if (remaining === undefined)
|
|
101
|
+
return;
|
|
102
|
+
const at = remaining.indexOf(startedAt);
|
|
103
|
+
const next = at === -1 ? remaining : [...remaining.slice(0, at), ...remaining.slice(at + 1)];
|
|
104
|
+
if (next.length === 0)
|
|
105
|
+
opens.delete(k);
|
|
106
|
+
else
|
|
107
|
+
opens.set(k, next);
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
isDeliveryOpening(openerPubkey, targetPubkey) {
|
|
111
|
+
return live(key(openerPubkey, targetPubkey)).length > 0;
|
|
112
|
+
},
|
|
113
|
+
inFlight() {
|
|
114
|
+
let total = 0;
|
|
115
|
+
for (const k of [...opens.keys()])
|
|
116
|
+
total += live(k).length;
|
|
117
|
+
return total;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=delivery-open-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-open-registry.js","sourceRoot":"","sources":["../src/delivery-open-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,72 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SYNC-P1 (daemon half) — the fork-tolerant entry store.
|
|
3
3
|
*
|
|
4
|
-
* One row per
|
|
5
|
-
* re-encode. The bytes are the truth: `chain` decodes them on
|
|
6
|
-
* exactly what was signed, and a codec asymmetry can never
|
|
7
|
-
* (the class of defect TRACE-1 found in the proposal codec).
|
|
4
|
+
* One row per ENTRY (keyed by entry hash — never by an epoch slot), holding the entry AS
|
|
5
|
+
* RECEIVED — the wire bytes, never a re-encode. The bytes are the truth: `chain` decodes them on
|
|
6
|
+
* read, so what the fold consumes is exactly what was signed, and a codec asymmetry can never
|
|
7
|
+
* silently rewrite an agreed record (the class of defect TRACE-1 found in the proposal codec).
|
|
8
8
|
*
|
|
9
|
-
* The store owns persistence and
|
|
10
|
-
* - **
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
9
|
+
* The store owns persistence and ANCESTRY CLOSURE:
|
|
10
|
+
* - **Held until whole (R14)** — an entry lands in `document_entries` only when every parent has
|
|
11
|
+
* landed. Otherwise it waits in `document_entries_pending`: recorded, never applied, never in a
|
|
12
|
+
* watermark, promoted the moment its ancestry completes — cascade included. The invariant this
|
|
13
|
+
* buys: everything in `document_entries` has its FULL ancestry there too.
|
|
14
|
+
* - **Fork tolerance** — two entries claiming the same epoch or the same author seq are BOTH
|
|
15
|
+
* stored. Ruling on conflicts is the causal fold's job (`deriveDocumentState`), not the
|
|
16
|
+
* store's; the old chain-gap and epoch-conflict refusals are gone WITH the epoch spine.
|
|
17
|
+
* - **Idempotent redelivery** — the same bytes again is `recorded: false`, not an error and not
|
|
18
|
+
* a second row.
|
|
19
|
+
* - **Watermarks** — per author, the highest CONTIGUOUS seq with its head hash(es). Two heads at
|
|
20
|
+
* one seq is an equivocation made visible for the exchange to resolve, never silently picked.
|
|
18
21
|
*
|
|
19
|
-
* What the store does NOT judge: signatures, policy, subject semantics.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* What the store does NOT judge: signatures, policy, subject semantics. The fold rules on those
|
|
23
|
+
* at every consumption, and the inbound path rules BEFORE appending. A row in these tables is a
|
|
24
|
+
* claim to be folded, not an admitted fact.
|
|
22
25
|
*/
|
|
23
26
|
import { type DocumentAmendmentEnvelope } from "@cello-protocol/protocol-types";
|
|
24
27
|
import type { DaemonDatabase } from "./sqlcipher-db.js";
|
|
25
28
|
import type { Logger } from "./types.js";
|
|
26
29
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
30
|
+
* The PRE-PIVOT table, epoch-keyed. Still created because `DocumentStore` execs this too and the
|
|
31
|
+
* pre-pivot readers live until P4 deletes them (SYNC-D2); the entry store neither reads nor
|
|
32
|
+
* writes it. Rows predating the pivot stay untouched — old-shape bytes are not decodable by the
|
|
33
|
+
* v2 codec and are not migrated (no compatibility owed; essentially no documents exist).
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Shared with `DocumentStore` (the :352 shared-definition precedent): its membership walk and
|
|
37
|
+
* epoch read consume `document_entries`, so the tables must exist whichever module constructs
|
|
38
|
+
* first. Keep both consumers on THIS string.
|
|
31
39
|
*/
|
|
32
|
-
export declare const DOCUMENT_AMENDMENTS_CREATE_SQL = "\n CREATE TABLE IF NOT EXISTS document_amendments (\n owner_agent_id TEXT NOT NULL,\n document_id TEXT NOT NULL,\n epoch_id INTEGER NOT NULL,\n amendment_hash TEXT NOT NULL,\n received_bytes BLOB NOT NULL,\n recorded_at INTEGER NOT NULL,\n PRIMARY KEY (owner_agent_id, document_id, epoch_id)\n );\n";
|
|
33
|
-
export interface MembershipVerdict {
|
|
34
|
-
state: "holder" | "removed" | "untouched";
|
|
35
|
-
epochId: number | null;
|
|
36
|
-
}
|
|
37
40
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
+
* D7 — the epoch spine is deleted. A database born before that carries a NOT NULL `epoch_id`
|
|
42
|
+
* with no default, which would refuse every insert from this build. Dropped in place (SQLite
|
|
43
|
+
* ≥3.35 / SQLCipher 4.5); a fresh database never has it.
|
|
41
44
|
*/
|
|
42
|
-
|
|
45
|
+
type ColumnDropper = {
|
|
46
|
+
exec(sql: string): void;
|
|
47
|
+
prepare(sql: string): {
|
|
48
|
+
all(...a: unknown[]): unknown[];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Drop columns a previous milestone left behind, birth-gated: only what the table actually still
|
|
53
|
+
* has is touched, so this is safe on a database created yesterday and on one created before the
|
|
54
|
+
* column existed. Dead schema is not free — it is the design a future author reads and rebuilds.
|
|
55
|
+
*/
|
|
56
|
+
export declare function dropLegacyColumns(db: ColumnDropper, table: string, columns: readonly string[]): void;
|
|
57
|
+
export declare function dropLegacyEpochColumn(db: ColumnDropper, table: string): void;
|
|
58
|
+
export declare const DOCUMENT_ENTRIES_CREATE_SQL = "\n CREATE TABLE IF NOT EXISTS document_entries (\n owner_agent_id TEXT NOT NULL,\n document_id TEXT NOT NULL,\n entry_hash TEXT NOT NULL,\n author_agent_id TEXT NOT NULL,\n author_seq INTEGER NOT NULL,\n received_bytes BLOB NOT NULL,\n recorded_at INTEGER NOT NULL,\n PRIMARY KEY (owner_agent_id, document_id, entry_hash)\n );\n CREATE INDEX IF NOT EXISTS idx_document_entries_author\n ON document_entries (owner_agent_id, document_id, author_agent_id, author_seq);\n CREATE TABLE IF NOT EXISTS document_entries_pending (\n owner_agent_id TEXT NOT NULL,\n document_id TEXT NOT NULL,\n entry_hash TEXT NOT NULL,\n author_agent_id TEXT NOT NULL,\n received_bytes BLOB NOT NULL,\n recorded_at INTEGER NOT NULL,\n PRIMARY KEY (owner_agent_id, document_id, entry_hash)\n );\n";
|
|
43
59
|
export interface AmendmentAppendResult {
|
|
44
|
-
/** False on an idempotent redelivery — the
|
|
60
|
+
/** False on an idempotent redelivery — the entry (or its pending row) already existed. */
|
|
45
61
|
recorded: boolean;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
/** True when the entry is waiting on missing parents (R14) — recorded but not yet applied. */
|
|
63
|
+
held: boolean;
|
|
64
|
+
/** Hex of the entry's TBS hash. */
|
|
65
|
+
entryHash: string;
|
|
66
|
+
/** Previously-held entries this arrival completed, now applied — envelopes included, so the
|
|
67
|
+
* caller can run the same post-apply surfacing it runs for a direct arrival (review F2: the
|
|
68
|
+
* held path silently dropped removal notices and lifecycle completion). */
|
|
69
|
+
promoted: PromotedEntry[];
|
|
70
|
+
}
|
|
71
|
+
export interface PromotedEntry {
|
|
72
|
+
entryHash: string;
|
|
73
|
+
envelope: DocumentAmendmentEnvelope;
|
|
74
|
+
}
|
|
75
|
+
export interface PendingEntry {
|
|
76
|
+
entryHash: string;
|
|
77
|
+
/** The parents not yet held — the exchange asks for exactly these. */
|
|
78
|
+
missingParents: string[];
|
|
79
|
+
recordedAtMs: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Ceiling on HELD entries per (document, author). An honest author's pending set is a short gap
|
|
83
|
+
* awaiting one delivery; a hostile known author fabricating parents could otherwise grow the
|
|
84
|
+
* pending table without bound (review F4).
|
|
85
|
+
*/
|
|
86
|
+
export declare const MAX_PENDING_PER_AUTHOR = 64;
|
|
87
|
+
export interface AuthorWatermark {
|
|
88
|
+
/** Highest CONTIGUOUS seq held from this author. */
|
|
89
|
+
seq: number;
|
|
90
|
+
/** Entry hash(es) at that seq — more than one is an equivocation, visible by design. */
|
|
91
|
+
headHashes: string[];
|
|
49
92
|
}
|
|
50
93
|
export declare class DocumentAmendmentStore {
|
|
51
94
|
#private;
|
|
52
95
|
constructor(db: DaemonDatabase, logger: Logger);
|
|
53
96
|
/**
|
|
54
|
-
* Append one
|
|
55
|
-
* decoder's named reason and nothing is stored.
|
|
97
|
+
* Append one entry from its wire bytes. Decodes first — malformed bytes refuse with the
|
|
98
|
+
* decoder's named reason and nothing is stored. An entry with missing parents is HELD; an
|
|
99
|
+
* arrival that completes held ancestry promotes the whole cascade.
|
|
56
100
|
*/
|
|
57
101
|
append(ownerAgentId: string, documentId: string, receivedBytes: Uint8Array, nowMs: number): AmendmentAppendResult;
|
|
58
|
-
/**
|
|
102
|
+
/** Every applied entry, decoded from the stored received bytes. The set is ancestry-closed. */
|
|
59
103
|
chain(ownerAgentId: string, documentId: string): DocumentAmendmentEnvelope[];
|
|
104
|
+
/** Held entries and exactly which parents they still wait on (R37's receiver-side record). */
|
|
105
|
+
pending(ownerAgentId: string, documentId: string): PendingEntry[];
|
|
106
|
+
/**
|
|
107
|
+
* This author's applied entries with seq strictly beyond `afterSeq`, as WIRE BYTES in seq
|
|
108
|
+
* order — what a reconcile reply carries to a peer whose watermark for this author is behind
|
|
109
|
+
* ours (R10 step 2/3). Forked seqs both ship: the peer's fold rules on them like ours did.
|
|
110
|
+
*/
|
|
111
|
+
entriesByAuthorAfter(ownerAgentId: string, documentId: string, authorAgentId: string, afterSeq: number): Uint8Array[];
|
|
60
112
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* remove_holder, with the epoch it happened at, so a refusal can name the removal rather than
|
|
65
|
-
* a generic condition. Reads the recorded chain only — validity was ruled before append
|
|
66
|
-
* (the standing invariant).
|
|
113
|
+
* Per-author position: the highest CONTIGUOUS seq held and the head hash(es) at it. A held
|
|
114
|
+
* (pending) entry is not counted — a gap ends the walk (R13: report contiguous, never highest
|
|
115
|
+
* received).
|
|
67
116
|
*/
|
|
68
|
-
|
|
69
|
-
/** The highest recorded epoch, or 0 — genesis — when no amendment exists. */
|
|
70
|
-
currentEpoch(ownerAgentId: string, documentId: string): number;
|
|
117
|
+
watermarks(ownerAgentId: string, documentId: string): Map<string, AuthorWatermark>;
|
|
71
118
|
}
|
|
119
|
+
export {};
|
|
72
120
|
//# sourceMappingURL=document-amendment-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-amendment-store.d.ts","sourceRoot":"","sources":["../src/document-amendment-store.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"document-amendment-store.d.ts","sourceRoot":"","sources":["../src/document-amendment-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAGL,KAAK,yBAAyB,EAC/B,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;;GAKG;AACH;;;;GAIG;AACH;;;;GAIG;AACH,KAAK,aAAa,GAAG;IAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAA;KAAE,CAAA;CAAE,CAAC;AAE5G;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAOpG;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE5E;AAED,eAAO,MAAM,2BAA2B,63BAsBvC,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,0FAA0F;IAC1F,QAAQ,EAAE,OAAO,CAAC;IAClB,8FAA8F;IAC9F,IAAI,EAAE,OAAO,CAAC;IACd,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB;;gFAE4E;IAC5E,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,yBAAyB,CAAC;CACrC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,wFAAwF;IACxF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,qBAAa,sBAAsB;;gBAIrB,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM;IAW9C;;;;OAIG;IACH,MAAM,CACJ,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,UAAU,EACzB,KAAK,EAAE,MAAM,GACZ,qBAAqB;IA0DxB,+FAA+F;IAC/F,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,yBAAyB,EAAE;IAW5E,8FAA8F;IAC9F,OAAO,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,YAAY,EAAE;IAwBjE;;;;OAIG;IACH,oBAAoB,CAClB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,UAAU,EAAE;IAaf;;;;OAIG;IACH,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC;CAqInF"}
|