@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.
Files changed (111) hide show
  1. package/dist/agent-id-migration.d.ts.map +1 -1
  2. package/dist/agent-id-migration.js +16 -0
  3. package/dist/agent-id-migration.js.map +1 -1
  4. package/dist/away-detection.d.ts +62 -15
  5. package/dist/away-detection.d.ts.map +1 -1
  6. package/dist/away-detection.js +77 -20
  7. package/dist/away-detection.js.map +1 -1
  8. package/dist/close-session-handler.d.ts.map +1 -1
  9. package/dist/close-session-handler.js +69 -3
  10. package/dist/close-session-handler.js.map +1 -1
  11. package/dist/daemon.d.ts.map +1 -1
  12. package/dist/daemon.js +406 -303
  13. package/dist/daemon.js.map +1 -1
  14. package/dist/delivery-open-registry.d.ts +92 -0
  15. package/dist/delivery-open-registry.d.ts.map +1 -0
  16. package/dist/delivery-open-registry.js +121 -0
  17. package/dist/delivery-open-registry.js.map +1 -0
  18. package/dist/document-amendment-store.d.ts +94 -46
  19. package/dist/document-amendment-store.d.ts.map +1 -1
  20. package/dist/document-amendment-store.js +217 -102
  21. package/dist/document-amendment-store.js.map +1 -1
  22. package/dist/document-delivery-transport.d.ts +51 -53
  23. package/dist/document-delivery-transport.d.ts.map +1 -1
  24. package/dist/document-delivery-transport.js +16 -157
  25. package/dist/document-delivery-transport.js.map +1 -1
  26. package/dist/document-frame-router.d.ts +13 -17
  27. package/dist/document-frame-router.d.ts.map +1 -1
  28. package/dist/document-frame-router.js +21 -93
  29. package/dist/document-frame-router.js.map +1 -1
  30. package/dist/document-handlers.d.ts +1 -1
  31. package/dist/document-handlers.d.ts.map +1 -1
  32. package/dist/document-handlers.js +462 -511
  33. package/dist/document-handlers.js.map +1 -1
  34. package/dist/document-handshake.d.ts +4 -1
  35. package/dist/document-handshake.d.ts.map +1 -1
  36. package/dist/document-handshake.js +3 -1
  37. package/dist/document-handshake.js.map +1 -1
  38. package/dist/document-inbound.d.ts +18 -5
  39. package/dist/document-inbound.d.ts.map +1 -1
  40. package/dist/document-inbound.js +94 -110
  41. package/dist/document-inbound.js.map +1 -1
  42. package/dist/document-layer.d.ts +49 -67
  43. package/dist/document-layer.d.ts.map +1 -1
  44. package/dist/document-layer.js +688 -565
  45. package/dist/document-layer.js.map +1 -1
  46. package/dist/document-lifecycle.d.ts +11 -157
  47. package/dist/document-lifecycle.d.ts.map +1 -1
  48. package/dist/document-lifecycle.js +26 -568
  49. package/dist/document-lifecycle.js.map +1 -1
  50. package/dist/document-publish.d.ts +13 -0
  51. package/dist/document-publish.d.ts.map +1 -1
  52. package/dist/document-publish.js +19 -11
  53. package/dist/document-publish.js.map +1 -1
  54. package/dist/document-reconcile-engine.d.ts +66 -0
  55. package/dist/document-reconcile-engine.d.ts.map +1 -0
  56. package/dist/document-reconcile-engine.js +225 -0
  57. package/dist/document-reconcile-engine.js.map +1 -0
  58. package/dist/document-reconcile-scheduler.d.ts +124 -0
  59. package/dist/document-reconcile-scheduler.d.ts.map +1 -0
  60. package/dist/document-reconcile-scheduler.js +226 -0
  61. package/dist/document-reconcile-scheduler.js.map +1 -0
  62. package/dist/document-rejection.d.ts +2 -0
  63. package/dist/document-rejection.d.ts.map +1 -1
  64. package/dist/document-rejection.js +5 -1
  65. package/dist/document-rejection.js.map +1 -1
  66. package/dist/document-store.d.ts +18 -247
  67. package/dist/document-store.d.ts.map +1 -1
  68. package/dist/document-store.js +95 -827
  69. package/dist/document-store.js.map +1 -1
  70. package/dist/inbound-sessions.d.ts +7 -0
  71. package/dist/inbound-sessions.d.ts.map +1 -1
  72. package/dist/inbound-sessions.js +27 -6
  73. package/dist/inbound-sessions.js.map +1 -1
  74. package/dist/initiate-session-handler.d.ts +5 -0
  75. package/dist/initiate-session-handler.d.ts.map +1 -1
  76. package/dist/initiate-session-handler.js +1 -0
  77. package/dist/initiate-session-handler.js.map +1 -1
  78. package/dist/notification-handlers.d.ts.map +1 -1
  79. package/dist/notification-handlers.js +39 -2
  80. package/dist/notification-handlers.js.map +1 -1
  81. package/dist/session-content-handlers.d.ts.map +1 -1
  82. package/dist/session-content-handlers.js +134 -8
  83. package/dist/session-content-handlers.js.map +1 -1
  84. package/dist/session-node-manager.d.ts +153 -8
  85. package/dist/session-node-manager.d.ts.map +1 -1
  86. package/dist/session-node-manager.js +1376 -35
  87. package/dist/session-node-manager.js.map +1 -1
  88. package/dist/types.d.ts +64 -2
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js.map +1 -1
  91. package/package.json +5 -5
  92. package/dist/delivery-sweep-bound.d.ts +0 -79
  93. package/dist/delivery-sweep-bound.d.ts.map +0 -1
  94. package/dist/delivery-sweep-bound.js +0 -109
  95. package/dist/delivery-sweep-bound.js.map +0 -1
  96. package/dist/document-ack-inbound.d.ts +0 -71
  97. package/dist/document-ack-inbound.d.ts.map +0 -1
  98. package/dist/document-ack-inbound.js +0 -210
  99. package/dist/document-ack-inbound.js.map +0 -1
  100. package/dist/document-control-notifier.d.ts +0 -124
  101. package/dist/document-control-notifier.d.ts.map +0 -1
  102. package/dist/document-control-notifier.js +0 -176
  103. package/dist/document-control-notifier.js.map +0 -1
  104. package/dist/document-delivery.d.ts +0 -250
  105. package/dist/document-delivery.d.ts.map +0 -1
  106. package/dist/document-delivery.js +0 -608
  107. package/dist/document-delivery.js.map +0 -1
  108. package/dist/document-join-store.d.ts +0 -80
  109. package/dist/document-join-store.d.ts.map +0 -1
  110. package/dist/document-join-store.js +0 -197
  111. 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
- * DOD-MP-AMEND-1 (daemon half) — the append-only amendment store.
2
+ * SYNC-P1 (daemon half) — the fork-tolerant entry store.
3
3
  *
4
- * One row per epoch per document, holding the amendment AS RECEIVED the wire bytes, never a
5
- * re-encode. The bytes are the truth: `chain` decodes them on read, so what replay consumes is
6
- * exactly what was signed, and a codec asymmetry can never silently rewrite an agreed record
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 chain SHAPE:
10
- * - **Contiguity** epoch N appends only onto N-1 rows. An out-of-order arrival is refused by
11
- * name, not buffered; amendment-lag buffering is DOD-MP-INBOUND-N-1's design, and until it
12
- * lands the honest behaviour is a loud refusal the sender retries.
13
- * - **Fork refusal** a different amendment at an occupied epoch is `document_amendment_conflict`,
14
- * first record kept. Two rivals for one epoch is a governance fork; absorbing either silently
15
- * would have two holders replay different arrangements with no error anywhere.
16
- * - **Idempotent redelivery** — the same bytes again is `recorded: false`, not an error and not a
17
- * second row.
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. `deriveArrangement`
20
- * (protocol-types) rules on those at every consumption, and the inbound path rules BEFORE
21
- * appending. A row in this table is a claim to be replayed, not an admitted fact.
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
- * Shared with `DocumentStore`, which also execs this (the :353 shared-definition precedent there):
28
- * every epoch producer already holds a DocumentStore, and `currentDocumentEpoch` reads this table
29
- * through it so the table must exist whichever module constructs first. Keep the two consumers
30
- * on THIS one string.
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
- * The last membership event naming this agent in an ordered chain ONE implementation, because
39
- * two walks (the inbound refusal's and the publish gate's) disagreeing about whether someone was
40
- * removed is two daemons disagreeing about the arrangement.
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
- export declare function walkMembership(chain: readonly DocumentAmendmentEnvelope[], agentId: string): MembershipVerdict;
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 row already existed with the same hash. */
60
+ /** False on an idempotent redelivery — the entry (or its pending row) already existed. */
45
61
  recorded: boolean;
46
- epochId: number;
47
- /** Hex of the amendment's TBS hash. */
48
- amendmentHash: string;
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 amendment from its wire bytes. Decodes first — malformed bytes refuse with the
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
- /** The ordered chain, decoded from the stored received bytes. */
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
- * DOD-MP-REMOVE-1 this agent's LAST membership event in the recorded chain.
62
- * "holder" = admitted (or never touched by any membership amendment genesis membership is
63
- * the CALLER's fact, not this table's); "removed" = the last event naming them was
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
- membershipOf(ownerAgentId: string, documentId: string, agentId: string): MembershipVerdict;
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;;;;;;;;;;;;;;;;;;;;;GAqBG;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,eAAO,MAAM,8BAA8B,oWAU1C,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;IAC1C,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,yBAAyB,EAAE,EAC3C,OAAO,EAAE,MAAM,GACd,iBAAiB,CAcnB;AAED,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,sBAAsB;;gBAIrB,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM;IAM9C;;;OAGG;IACH,MAAM,CACJ,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,UAAU,EACzB,KAAK,EAAE,MAAM,GACZ,qBAAqB;IAmExB,iEAAiE;IACjE,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,yBAAyB,EAAE;IAW5E;;;;;;;OAOG;IACH,YAAY,CACV,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,iBAAiB;IAIpB,6EAA6E;IAC7E,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;CAS/D"}
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"}