@cello-protocol/daemon 0.0.196 → 0.0.197

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 (39) hide show
  1. package/dist/held-content.d.ts +7 -3
  2. package/dist/held-content.d.ts.map +1 -1
  3. package/dist/held-content.js.map +1 -1
  4. package/dist/session-content-context.d.ts +144 -0
  5. package/dist/session-content-context.d.ts.map +1 -0
  6. package/dist/session-content-context.js +2 -0
  7. package/dist/session-content-context.js.map +1 -0
  8. package/dist/session-content-ingest.d.ts +208 -0
  9. package/dist/session-content-ingest.d.ts.map +1 -0
  10. package/dist/session-content-ingest.js +2216 -0
  11. package/dist/session-content-ingest.js.map +1 -0
  12. package/dist/session-content-send.d.ts +184 -0
  13. package/dist/session-content-send.d.ts.map +1 -0
  14. package/dist/session-content-send.js +1300 -0
  15. package/dist/session-content-send.js.map +1 -0
  16. package/dist/session-lifecycle.d.ts +303 -0
  17. package/dist/session-lifecycle.d.ts.map +1 -0
  18. package/dist/session-lifecycle.js +1643 -0
  19. package/dist/session-lifecycle.js.map +1 -0
  20. package/dist/session-node-manager.d.ts +67 -766
  21. package/dist/session-node-manager.d.ts.map +1 -1
  22. package/dist/session-node-manager.js +966 -7940
  23. package/dist/session-node-manager.js.map +1 -1
  24. package/dist/session-node-types.d.ts +22 -0
  25. package/dist/session-node-types.d.ts.map +1 -1
  26. package/dist/session-node-types.js.map +1 -1
  27. package/dist/session-relay.d.ts +361 -0
  28. package/dist/session-relay.d.ts.map +1 -0
  29. package/dist/session-relay.js +1471 -0
  30. package/dist/session-relay.js.map +1 -0
  31. package/dist/session-salts.d.ts +13 -0
  32. package/dist/session-salts.d.ts.map +1 -1
  33. package/dist/session-salts.js +13 -0
  34. package/dist/session-salts.js.map +1 -1
  35. package/dist/session-seal.d.ts +336 -0
  36. package/dist/session-seal.d.ts.map +1 -0
  37. package/dist/session-seal.js +948 -0
  38. package/dist/session-seal.js.map +1 -0
  39. package/package.json +5 -5
@@ -21,27 +21,29 @@ import { type CapacityReason } from "./refusal-reasons.js";
21
21
  import { type SaltAgreementFrame } from "./session-salt-agreement.js";
22
22
  import { type DaemonDatabase } from "./sqlcipher-db.js";
23
23
  import { type RefusalKind } from "./refusal-reasons.js";
24
- import type { Stream } from "@libp2p/interface";
25
- import type { Logger, SessionRecord, SealReadinessView } from "./types.js";
24
+ import type { Logger, SessionRecord } from "./types.js";
26
25
  import { SessionTree, type WritableSessionTreeLeafKind } from "./session-tree.js";
27
26
  import { type CelloNode, type IAutoNatService } from "@cello-protocol/transport";
28
27
  import type { KeyProvider } from "@cello-protocol/crypto";
29
28
  import { type SessionEphemeral } from "@cello-protocol/crypto";
30
29
  import { type ParkEnvelope } from "./park-envelope.js";
31
30
  import { AgentRelayClient, type RelayAuthRefusal, type RelayWitnessAlert } from "./session-relay-client.js";
32
- import { RelayReceiptStore, type RelayReceipt } from "./relay-receipt-store.js";
33
- import { SessionSealLeafStore, type SealCarryLeaf } from "./session-seal-leaf-store.js";
31
+ import { SessionContentSender } from "./session-content-send.js";
32
+ import { SessionContentIngest } from "./session-content-ingest.js";
33
+ import { SessionSeal } from "./session-seal.js";
34
+ import { SessionRelay } from "./session-relay.js";
35
+ import { SessionLifecycle } from "./session-lifecycle.js";
36
+ import { RelayReceiptStore } from "./relay-receipt-store.js";
37
+ import { SessionSealLeafStore } from "./session-seal-leaf-store.js";
34
38
  import { SessionOwnChainStore } from "./session-own-chain-store.js";
35
- import type { SealUpgradeReadiness } from "./seal-upgrade.js";
36
39
  import type { SealFrontierLeaf } from "./seal-frontier-verify.js";
37
- import { type QuarantineFrameMeta } from "./quarantine-framing.js";
38
40
  import { type SecurityGatewayClient } from "@cello-protocol/gateway";
39
41
  /**
40
42
  * One row in an agent's witness-alert list — DOD-M15-CORROBORATE-1 review F1. Deduped on
41
43
  * `(witness relay, session)`, so a repeated observation raises `occurrences` rather than taking
42
44
  * another slot in a bounded list.
43
45
  */
44
- import { type AbandonNoticeResult, type CreateSessionResult, type ISessionNodeFactory, type ParkedDrainReason, type QuarantinedRecord, type ReceivedContentEntry, type RefusalNotice, type RelayConnectParams, type SentAuthorship, type SessionImpairment, type TranscriptEntry, type WitnessAlertNotice } from "./session-node-types.js";
46
+ import { type ISessionNodeFactory, type ParkedDrainReason, type QuarantinedRecord, type RefusalNotice, type SessionImpairment, type TranscriptEntry, type WitnessAlertNotice } from "./session-node-types.js";
45
47
  export { ABUSE_MAX_SESSIONS_PER_UNKNOWN_SENDER, ABUSE_MAX_SESSION_RECEIVED_BYTES, ABUSE_MAX_UNKNOWN_SESSIONS_GLOBAL, type AbandonNoticeResult, type AckHashReason, CAP_INTERRUPTED_TTL_MS, type ISessionNodeFactory, LEAF_FETCH_GRACE_MS, MAX_TERMINAL_REFUSALS_PER_SESSION, type ParkedDrainReason, type QuarantinedRecord, RELAY_QUARANTINE_MS, REVIVAL_BOUND_SWEEP_MS, REVIVAL_WINDOW_MS, REVIVE_RESERVATION_CANDIDATES, REVIVE_RESERVATION_TIMEOUT_MS, type RefusalNotice, type RelayConnectParams, SR_RESERVATION_MAX_RETRIES, type SentAuthorship, type SessionImpairment, type SessionNodeConfig, TERMINAL_REFUSAL_READ_RETRY_MS, TERMINAL_REFUSAL_REASONS, type TranscriptEntry, type WitnessAlertNotice, } from "./session-node-types.js";
46
48
  export declare class SessionNodeManager {
47
49
  #private;
@@ -199,6 +201,21 @@ export declare class SessionNodeManager {
199
201
  wasSessionRefused(agentName: string, sessionId: string): boolean;
200
202
  countReceivedMessages(agentName: string, sessionId: string): number;
201
203
  markInterruptedByCounterpartyForTest(agentName: string, sessionId: string): void;
204
+ /**
205
+ * M7-SESSION-004 (AC-005): persist the seal certificate's legibility object with the
206
+ * sealed record. Stored as a JSON string (hex-encoded pubkeys) so it round-trips a
207
+ * daemon restart and is returned intact on the cert-read surface. The caller normalises
208
+ * the raw wire legibility (Uint8Array pubkeys) into a JSON-safe shape before storing.
209
+ * Best-effort: a session row may not yet exist (the seal arrived before the row was
210
+ * persisted); in that case we no-op rather than throw — the cert still flows through the
211
+ * live return path. The legibility content is identical regardless of delivery timing.
212
+ *
213
+ * ⚠️ THIS BLOCK WAS STRANDED WHEN THE METHOD BECAME A DELEGATOR. It stayed behind and ended up
214
+ * stacked on top of `markSealed`'s own doc block, so that method showed two descriptions and the
215
+ * first one described a different method entirely — and the seal split then carried it into
216
+ * `session-seal.ts`, where `recordSealCertificate` does not appear at all. Returned to the call
217
+ * it describes. The row-writing itself lives in `session-queries.ts`.
218
+ */
202
219
  recordSealCertificate(agentName: string, sessionId: string, sealedRootHex: string, legibilityJson: string): void;
203
220
  recordCounterpartyPrimary(agentName: string, sessionId: string, primaryPubkeyHex: string): void;
204
221
  /**
@@ -435,76 +452,6 @@ export declare class SessionNodeManager {
435
452
  * encrypted DB file.
436
453
  */
437
454
  getDb(): DaemonDatabase;
438
- /**
439
- * RELAYSIG-1: the durably-stored, signature-verified relay ordering-record receipts for an agent
440
- * (optionally a single session). Empty when no receipts have been recorded yet. Read-only.
441
- */
442
- getRelayReceipts(agentPubkeyHex: string, sessionIdHex?: string): RelayReceipt[];
443
- /**
444
- * FED-OPTIONB-SEAL-001: the complete ordered leaf chain (both parties) a UNILATERAL seal carries to the
445
- * directory for the OFFLINE tree rebuild. Empty when no leaves were logged (e.g. a direct-only session
446
- * with no relay witness) — the caller then has nothing to carry and the seal stays bilateral/pending.
447
- */
448
- /**
449
- * REBUILD THE CERTIFIED ROOT FROM THIS DAEMON'S OWN LEAVES — `DOD-M15-SEALWIRE-1` bullet 2.
450
- *
451
- * The receipt used to prove only that the directory signed SOMETHING: the client took the sealed
452
- * root off the wire, confirmed the directory's signature over those bytes, stored it, and threw
453
- * away the root it had computed a step earlier. At co-signing time that means **your key signs a
454
- * root you never checked.**
455
- *
456
- * Bullet 1 moved the certified root into the content-hash domain, which is the domain this daemon
457
- * can actually rebuild — each carry leaf's `content_hash` is the leaf hash (RFC 6962 §2.1 "hash"
458
- * leaves are used as-is), and the carry is ordered by the relay's canonical `sequence_number`,
459
- * which is the order the directory rebuilds in.
460
- *
461
- * ─── Why this returns "cannot judge" instead of always answering ───────────────────────────
462
- *
463
- * A root comparison that is WRONG makes every session unsealable, and force-abandon — no receipt —
464
- * becomes the only exit. That failure is worse than the one being guarded, and this file already
465
- * carries two comments saying so about other gates.
466
- *
467
- * The carry is this daemon's view, and it is not guaranteed complete at the instant a certificate
468
- * arrives: the counterparty's SEAL ctrl leaf is what TRIGGERS the seal, so it may not have been
469
- * witnessed here yet. So completeness is checked FIRST, against the certificate's own leaf count.
470
- * A short carry means this daemon cannot judge — which is a different answer from "the roots
471
- * disagree", and conflating them would turn a local timing gap into an accusation.
472
- */
473
- verifyCertifiedRoot(agentPubkeyHex: string, sessionIdHex: string, certifiedRoot: Uint8Array, certifiedLeafCount: number): {
474
- verdict: "match";
475
- } | {
476
- verdict: "mismatch";
477
- ownRootHex: string | null;
478
- detail: string;
479
- } | {
480
- verdict: "cannot_judge";
481
- reason: string;
482
- };
483
- /**
484
- * WHERE THE MUTUALLY-SIGNED PREFIX ENDS, DERIVED FROM THIS DAEMON'S OWN LEAVES —
485
- * `DOD-M15-UNILATERAL-1`, review F2.
486
- *
487
- * ⚠️ **THE FIRST VERSION COMPUTED THIS FROM THE CERTIFICATE'S OWN PARTICIPANT LIST, AND CALLED
488
- * THAT "recomputed, cannot be steered".** It could be steered. On the SOLO path the certificate's
489
- * TBS binds no legibility at all, and the client verifies only the *live* party's frontier — so
490
- * the absent party's `content_frontier_seq` and every `last_authored_seq` arrived unchecked. One
491
- * directory node could publish the absent party's frontier as 3 and the receipt would say
492
- * "mutually signed through 3" over a transcript that party never signed for. That is the precise
493
- * conflation this field exists to prevent, reintroduced by the field itself.
494
- *
495
- * The carry answers it without trusting anybody. This daemon holds the counterparty's own leaves,
496
- * each carrying, inside the bytes THEY signed, both the sequence they authored and the
497
- * `last_seen_seq` they acknowledged. So a party's commitment reaches
498
- * `max(highest sequence they authored, highest sequence they acknowledged)`, and the transcript is
499
- * mutually signed only as far as the LEAST-committed party reaches.
500
- *
501
- * Fewer than two distinct authors ⇒ `0`: nobody countersigned anything, which is the honest floor
502
- * for a conversation where the other side only ever received. `null` when the carry is empty or
503
- * unreadable — the caller must then publish NO boundary rather than fall back to a number
504
- * somebody else supplied.
505
- */
506
- countersignedThroughSeqFromCarry(agentPubkeyHex: string, sessionIdHex: string): number | null;
507
- getSealCarry(agentPubkeyHex: string, sessionIdHex: string): SealCarryLeaf[];
508
455
  /** DOD-TIER-4: the DISPLAY/relationship check — is this counterparty a genuine contact (KNOWN or
509
456
  * above)? Replaces the old binary `isContact` for behaviour that keyed on "we have a relationship"
510
457
  * (e.g. the away-response wording). An UNKNOWN-tier contact (a mere row) is NOT known. */
@@ -520,22 +467,6 @@ export declare class SessionNodeManager {
520
467
  * four-level resolution TOTAL. A pure read; the resolved text is screened on the outbound path by
521
468
  * the caller like any content (SI — it does not bypass the gateway). */
522
469
  resolveAwayMessage(agentName: string, pubkey: string): string | null;
523
- /**
524
- * DOD-M15-REFUSEDEVIDENCE-1 — retain a message refused OUTSIDE `ingestReceivedContent`.
525
- *
526
- * Review F6. The park drain terminally-blocks a message that arrived for an already-committed
527
- * session, then confirm-deletes the relay copy — the one other route in the tree that discarded
528
- * refused content, and the highest-suspicion combination in the product: hostile bytes aimed at a
529
- * conversation somebody has already sealed. Shipped guidance now tells every operator that
530
- * refused messages are kept, so this is made true rather than the promise narrowed.
531
- *
532
- * A thin delegate, not a second implementation: the bound, the dedup, the sequence allocation and
533
- * the logging are the ones every other refusal uses.
534
- */
535
- quarantineRefusedInbound(agentName: string, sessionId: string, reason: string, content: Uint8Array, contentHashHex: string, senderPubkeyHex: string | null, correlationId?: string): number | null;
536
- /** The metadata half of a framed quarantine read — everything known ABOUT the message, none of it
537
- * taken from the message. Split out so the framing module never touches the database. */
538
- quarantineFrameMeta(agentName: string, sessionId: string, rec: QuarantinedRecord): QuarantineFrameMeta;
539
470
  /** M8C-ABUSE-1 + DOD-TIER-2/3: is a NEW inbound session from this counterparty within the
540
471
  * acceptance bounds? The per-sender cap is now the sender's TIER cap (DEFAULT_TIER_BOUNDS), not a
541
472
  * flat "3 for strangers, unbounded for contacts". This is where DOD-TIER-3 falls out for free: a
@@ -550,43 +481,11 @@ export declare class SessionNodeManager {
550
481
  ok: false;
551
482
  reason: CapacityReason;
552
483
  };
553
- /**
554
- * The current standing receiver node's session-transport coordinates (peer id +
555
- * listen multiaddrs), or null if it is not ready. These are the addresses a local
556
- * SessionNegotiator advertises as this node's counterparty endpoint so the initiator
557
- * can dial it, and the value an inbound session_assignment carries in its
558
- * counterparty_session_* fields. Read-only — does NOT consume the standing receiver
559
- * (unlike acceptSession, which hands it off).
560
- */
561
- /**
562
- * 032-RELAYSPREAD — would this receiver ADMIT an inbound dial from this relay?
563
- *
564
- * The gater's inbound carve-out is the security-sensitive half of the spread: only relays whose
565
- * own reservation is confirmed held earn it, so a directory that merely NAMES a relay cannot dial
566
- * in behind the gate. Nothing could observe that from outside the manager, and the review found
567
- * the consequence: substituting the CANDIDATE list for the held list at the `setReservedRelayPeers`
568
- * call kept every test in the unit green while shipping exactly that hole. A guard whose wiring
569
- * cannot be observed is a guard nothing can test.
570
- *
571
- * Reads the live gater rather than a copy, so it cannot drift from what the gate actually does.
572
- */
573
- isRelayCarvedOutInbound(agentName: string, relayPeerId: string): boolean;
574
- /**
575
- * The libp2p Peer ID of an active session's node (N_A for an initiated session), or
576
- * null if no active node exists for it. This is the initiator's session peer id that an
577
- * inbound session_assignment must carry to the counterparty (so the counterparty gates
578
- * its handed-off receiver to it). Read-only.
579
- */
580
- getSessionNodePeerId(agentName: string, sessionId: string): string | null;
581
484
  /**
582
485
  * M7-SESSION-001 (M-1 PUSH): register the session-state-change callback.
583
486
  * Called by the composition root (daemon.ts) after the NotificationDispatcher
584
487
  * exists. Setter injection avoids a construction-order/circular dependency.
585
488
  */
586
- /** Fix #1 EXTENSION: inject the broker-connection opener. Setter injection, same construction-order reason. */
587
- setEnsureSealBroker(cb: (agentName: string, sessionId: string) => Promise<{
588
- stop: (reason: string) => Promise<void>;
589
- } | null>): void;
590
489
  setOnSessionStateChanged(cb: (agentName: string, sessionId: string, state: string, counterpartyPubkey: string | null) => void): void;
591
490
  /**
592
491
  * M8C-MSGWAKE-1: inject the content-arrival callback (daemon.ts → NotificationDispatcher.
@@ -607,17 +506,6 @@ export declare class SessionNodeManager {
607
506
  * an unresolvable name — see #requireAgentId for why null is not an option.
608
507
  */
609
508
  resolveAgentId(agentName: string): string;
610
- /**
611
- * Create a new outbound session node.
612
- * Called during cello_initiate_session.
613
- *
614
- * @param sessionId Unique session ID (hex string)
615
- * @param agentName Name of the initiating agent
616
- * @param counterpartyPubkey Counterparty's K_local public key (hex)
617
- * @param counterpartyPeerId Counterparty's session-layer Peer ID (for gater)
618
- * @param correlationId Correlation ID minted at session initiation
619
- */
620
- createSessionNode(sessionId: string, agentName: string, counterpartyPubkey: string, counterpartyPeerId: string, correlationId: string, reuseStandingReceiver?: boolean, relay?: RelayConnectParams): Promise<CreateSessionResult>;
621
509
  /**
622
510
  * DOD-M12B-ACK-1 — live `/cello/content/1.0.0` stream counts on a session's direct path, or null
623
511
  * when the session has no active node.
@@ -631,40 +519,6 @@ export declare class SessionNodeManager {
631
519
  inbound: number;
632
520
  outbound: number;
633
521
  } | null;
634
- /**
635
- * DOD-M12B-ABANDON-NOTIFY-1 — drive the REAL inbound content handler with one framed message and
636
- * a claimed peer identity.
637
- *
638
- * The handler is registered on a live libp2p node, so without this the only way to reach its
639
- * branches is a full two-node transport fixture — which is why the session-abandoned branch and
640
- * its peer pinning had no coverage at all. This feeds the same function the protocol handler
641
- * calls, including the authentication check, rather than a copy of its logic.
642
- */
643
- handleContentFrameForTest(agentName: string, sessionId: string, framedBytes: Uint8Array, remotePeerId?: string): Promise<void>;
644
- /**
645
- * Hand the standing receiver to an inbound session.
646
- * Called during cello_await_session.
647
- *
648
- * CRITICAL (AC-015): gater.setAllowedPeer() is called BEFORE returning
649
- * the node's multiaddr to the caller. This closes the window where an
650
- * unexpected peer could connect during the hand-off.
651
- */
652
- acceptSession(sessionId: string, agentName: string, counterpartyPubkey: string, initiatorPeerId: string, correlationId: string, relay?: RelayConnectParams): Promise<CreateSessionResult>;
653
- /**
654
- * Destroy a session node after seal or on error teardown.
655
- * Status written to SQLite.
656
- */
657
- destroySessionNode(agentName: string, sessionId: string, reason: "sealed" | "interrupted" | "error"): Promise<void>;
658
- /**
659
- * round-2 finding #5: retire a session's live libp2p node WITHOUT changing its
660
- * DB status. Used after the active-session bilateral seal commitment has already
661
- * advanced the row to 'seal_interrupted_pending': the session is frozen, so we
662
- * stop the node and unregister its /cello/content handler (no more inbound leaves,
663
- * no leaked node per active close) but must NOT overwrite the pending/sealed status
664
- * the way destroySessionNode would. The durable tree stays in SQLite (getSessionTree
665
- * reloads it); the in-memory plaintext buffer is evicted.
666
- */
667
- retireSessionNode(agentName: string, sessionId: string): Promise<void>;
668
522
  gracefulShutdown(): Promise<void>;
669
523
  /**
670
524
  * DOD-M12B-REVIVAL-BOUND-1 — close every session the revival window has expired.
@@ -687,88 +541,46 @@ export declare class SessionNodeManager {
687
541
  * cello_list_sessions). Classification + filtering + the count limit are applied by the caller.
688
542
  */
689
543
  getAllSessions(): SessionRecord[];
690
- /**
691
- * M7-SESSION-004 (AC-005): persist the seal certificate's legibility object with the
692
- * sealed record. Stored as a JSON string (hex-encoded pubkeys) so it round-trips a
693
- * daemon restart and is returned intact on the cert-read surface. The caller normalises
694
- * the raw wire legibility (Uint8Array pubkeys) into a JSON-safe shape before storing.
695
- * Best-effort: a session row may not yet exist (the seal arrived before the row was
696
- * persisted); in that case we no-op rather than throw — the cert still flows through the
697
- * live return path. The legibility content is identical regardless of delivery timing.
698
- */
699
- /**
700
- * DOD-M12B-INTERRUPTED-ESCALATE-1 — flip a session to `sealed`, synchronously, without needing a
701
- * live node.
702
- *
703
- * **`destroySessionNode(…, "sealed")` cannot be relied on to do this.** It returns early at
704
- * `if (!entry) return`, and the status write lives 26 lines BELOW that guard — so it flips the
705
- * status only for a session that still has an `#activeNodes` entry. An interrupted session has
706
- * none by construction: every producer of that status deletes the entry. Before this method, a
707
- * unilateral seal on an interrupted session stored the notarized root and the certificate and
708
- * left the row saying `interrupted` — the receipt landed and nothing that represents it moved.
709
- * `cello_sessions` still showed it stuck, `cello_close_session` still refused it by name, and the
710
- * restart-seal resolver re-selected it on the next boot to run the whole ceremony again against a
711
- * session that already held a receipt.
712
- *
713
- * STATUS FIRST AND SYNCHRONOUS, teardown second — the order `abandonSession` uses and the one
714
- * `retireSession` documents. The flip is the load-bearing half; the teardown makes memory agree
715
- * with it. `#updateSessionStatus` also runs the terminal disposition hooks (held content is
716
- * annexed, not stranded), which the early return skipped entirely.
717
- */
718
- markSealed(agentName: string, sessionId: string): boolean;
719
- /**
720
- * M8B FINDING-6 (cascade-2): persist a seal certificate for a session that may have NO local
721
- * `sessions` row. recordSealCertificate above is an `UPDATE ... WHERE` — a SILENT no-op when the
722
- * row is absent (the exact trap the cascade-2 reviewer flagged). The ABSENT party (B), learning of
723
- * a seal on reconnect via seal_unilateral_notification, may never have persisted a row for this
724
- * session. This ensures a minimal stub row first (INSERT OR IGNORE — a no-op if a row already
725
- * exists, e.g. an 'interrupted' row after a restart) so B's receipt is actually durable + retrievable
726
- * via cello_get_sealed_receipt. The counterparty pubkey is required by the schema (NOT NULL); B
727
- * derives it from the notification's present_pubkey.
728
- */
729
- recordSealCertificateEnsuringRow(agentName: string, sessionId: string, counterpartyPubkeyHex: string, sealedRootHex: string, legibilityJson: string): void;
730
- /**
731
- * M7-SESSION-001: Mark a session as interrupted with message count and timestamp.
732
- * Called when a relay session_interrupted frame arrives or a relay stream closes.
733
- * Also tears down the in-memory session node if one exists for this sessionId.
734
- *
735
- * @param sessionId The hex session ID from the relay frame
736
- * @param messageCount Number of message leaves at interruption
737
- * @param source 'relay_frame' | 'stream_close'
738
- */
739
- markInterruptedWithDetails(agentName: string, sessionId: string, messageCount: number,
740
- /**
741
- * WHAT ACTUALLY HAPPENED, and it is written to the row — review F3.
742
- *
743
- * `key_refused` is its own source rather than a borrowed `stream_close`, because the row's
744
- * `interrupted_by` is what an operator reads days later: labelling a key-authentication refusal
745
- * `relay_stream_close` sends them to the relay fleet for a fault in the payload.
746
- */
747
- source: "relay_frame" | "stream_close" | "key_refused"): Promise<boolean>;
748
- /**
749
- * M7-SESSION-001 (H-1): persist a verified bilateral SEAL-INTERRUPTED
750
- * commitment and transition the session to 'seal_interrupted_pending'.
751
- *
752
- * This is NOT a seal. It records that both parties produced and exchanged
753
- * K_local-signed SEAL-INTERRUPTED leaves over the same {leafCount, merkleRoot}.
754
- * The FROST threshold notarization is a separate, currently-unwired step (see
755
- * daemon.ts handleSealInterruptedFlow H-1 note), which is precisely why the
756
- * status is 'seal_interrupted_pending' and never 'sealed'.
757
- *
758
- * The status update is guarded so it only advances a session out of the
759
- * 'interrupted' state — it will not overwrite a 'sealed' row.
760
- *
761
- * @returns true if the session row was advanced to seal_interrupted_pending.
762
- */
763
- persistSealInterruptedCommitment(opts: {
764
- agentName: string;
765
- sessionId: string;
766
- role: "initiator" | "responder";
767
- ownLeaf: unknown;
768
- counterpartyLeaf: unknown;
769
- merkleRoot: string;
770
- nonce: string;
771
- }): boolean;
544
+ createSessionNode(...args: Parameters<SessionLifecycle["createSessionNode"]>): ReturnType<SessionLifecycle["createSessionNode"]>;
545
+ acceptSession(...args: Parameters<SessionLifecycle["acceptSession"]>): ReturnType<SessionLifecycle["acceptSession"]>;
546
+ destroySessionNode(...args: Parameters<SessionLifecycle["destroySessionNode"]>): ReturnType<SessionLifecycle["destroySessionNode"]>;
547
+ retireSessionNode(...args: Parameters<SessionLifecycle["retireSessionNode"]>): ReturnType<SessionLifecycle["retireSessionNode"]>;
548
+ markInterruptedWithDetails(...args: Parameters<SessionLifecycle["markInterruptedWithDetails"]>): ReturnType<SessionLifecycle["markInterruptedWithDetails"]>;
549
+ connectToCounterparty(...args: Parameters<SessionLifecycle["connectToCounterparty"]>): ReturnType<SessionLifecycle["connectToCounterparty"]>;
550
+ notifyCounterpartyAbandon(...args: Parameters<SessionLifecycle["notifyCounterpartyAbandon"]>): ReturnType<SessionLifecycle["notifyCounterpartyAbandon"]>;
551
+ retireOnCounterpartyAbandon(...args: Parameters<SessionLifecycle["retireOnCounterpartyAbandon"]>): ReturnType<SessionLifecycle["retireOnCounterpartyAbandon"]>;
552
+ reviveSessionNode(...args: Parameters<SessionLifecycle["reviveSessionNode"]>): ReturnType<SessionLifecycle["reviveSessionNode"]>;
553
+ reviveIfNeededForSend(...args: Parameters<SessionLifecycle["reviveIfNeededForSend"]>): ReturnType<SessionLifecycle["reviveIfNeededForSend"]>;
554
+ reviveIfNeededForRead(...args: Parameters<SessionLifecycle["reviveIfNeededForRead"]>): ReturnType<SessionLifecycle["reviveIfNeededForRead"]>;
555
+ abandonSession(...args: Parameters<SessionLifecycle["abandonSession"]>): ReturnType<SessionLifecycle["abandonSession"]>;
556
+ getSessionNodePeerId(...args: Parameters<SessionLifecycle["getSessionNodePeerId"]>): ReturnType<SessionLifecycle["getSessionNodePeerId"]>;
557
+ setDirectoryRelayEndpoints(...args: Parameters<SessionRelay["setDirectoryRelayEndpoints"]>): ReturnType<SessionRelay["setDirectoryRelayEndpoints"]>;
558
+ registerRelayStream(...args: Parameters<SessionRelay["registerRelayStream"]>): ReturnType<SessionRelay["registerRelayStream"]>;
559
+ isRelayCarvedOutInbound(...args: Parameters<SessionRelay["isRelayCarvedOutInbound"]>): ReturnType<SessionRelay["isRelayCarvedOutInbound"]>;
560
+ isRelayQuarantined(...args: Parameters<SessionRelay["isRelayQuarantined"]>): ReturnType<SessionRelay["isRelayQuarantined"]>;
561
+ quarantineRefusedInbound(...args: Parameters<SessionRelay["quarantineRefusedInbound"]>): ReturnType<SessionRelay["quarantineRefusedInbound"]>;
562
+ quarantineFrameMeta(...args: Parameters<SessionRelay["quarantineFrameMeta"]>): ReturnType<SessionRelay["quarantineFrameMeta"]>;
563
+ getRelayReceipts(...args: Parameters<SessionRelay["getRelayReceipts"]>): ReturnType<SessionRelay["getRelayReceipts"]>;
564
+ patchRelayClientForTest(...args: Parameters<SessionRelay["patchRelayClientForTest"]>): ReturnType<SessionRelay["patchRelayClientForTest"]>;
565
+ getSessionRelayForTest(...args: Parameters<SessionRelay["getSessionRelayForTest"]>): ReturnType<SessionRelay["getSessionRelayForTest"]>;
566
+ submitSealLeaf(...args: Parameters<SessionSeal["submitSealLeaf"]>): ReturnType<SessionSeal["submitSealLeaf"]>;
567
+ sealReadiness(...args: Parameters<SessionSeal["sealReadiness"]>): ReturnType<SessionSeal["sealReadiness"]>;
568
+ sealReadinessView(...args: Parameters<SessionSeal["sealReadinessView"]>): ReturnType<SessionSeal["sealReadinessView"]>;
569
+ verifyCertifiedRoot(...args: Parameters<SessionSeal["verifyCertifiedRoot"]>): ReturnType<SessionSeal["verifyCertifiedRoot"]>;
570
+ getSealCarry(...args: Parameters<SessionSeal["getSealCarry"]>): ReturnType<SessionSeal["getSealCarry"]>;
571
+ markSealed(...args: Parameters<SessionSeal["markSealed"]>): ReturnType<SessionSeal["markSealed"]>;
572
+ recordSealCertificateEnsuringRow(...args: Parameters<SessionSeal["recordSealCertificateEnsuringRow"]>): ReturnType<SessionSeal["recordSealCertificateEnsuringRow"]>;
573
+ persistSealInterruptedCommitment(...args: Parameters<SessionSeal["persistSealInterruptedCommitment"]>): ReturnType<SessionSeal["persistSealInterruptedCommitment"]>;
574
+ getSealUpgradeReadiness(...args: Parameters<SessionSeal["getSealUpgradeReadiness"]>): ReturnType<SessionSeal["getSealUpgradeReadiness"]>;
575
+ countersignedThroughSeqFromCarry(...args: Parameters<SessionSeal["countersignedThroughSeqFromCarry"]>): ReturnType<SessionSeal["countersignedThroughSeqFromCarry"]>;
576
+ setEnsureSealBroker(...args: Parameters<SessionSeal["setEnsureSealBroker"]>): ReturnType<SessionSeal["setEnsureSealBroker"]>;
577
+ ingestReceivedContent(...args: Parameters<SessionContentIngest["ingestReceivedContent"]>): ReturnType<SessionContentIngest["ingestReceivedContent"]>;
578
+ sendContent(...args: Parameters<SessionContentSender["sendContent"]>): ReturnType<SessionContentSender["sendContent"]>;
579
+ placeOwnLeaf(...args: Parameters<SessionContentSender["placeOwnLeaf"]>): ReturnType<SessionContentSender["placeOwnLeaf"]>;
580
+ takeReceivedContent(...args: Parameters<SessionContentIngest["takeReceivedContent"]>): ReturnType<SessionContentIngest["takeReceivedContent"]>;
581
+ recordWitnessedSequence(...args: Parameters<SessionContentIngest["recordWitnessedSequence"]>): ReturnType<SessionContentIngest["recordWitnessedSequence"]>;
582
+ getUndeliverableSeqs(...args: Parameters<SessionContentIngest["getUndeliverableSeqs"]>): ReturnType<SessionContentIngest["getUndeliverableSeqs"]>;
583
+ handleContentFrameForTest(...args: Parameters<SessionContentIngest["handleContentFrameForTest"]>): ReturnType<SessionContentIngest["handleContentFrameForTest"]>;
772
584
  /** Loaded from SQLite on first access so it survives a restart (AC-007). NEVER null: an unknown session yields an EMPTY tree. */
773
585
  getSessionTree(agentName: string, sessionId: string): SessionTree;
774
586
  /** Current daemon-owned tree root for a session, as hex. */
@@ -783,319 +595,9 @@ export declare class SessionNodeManager {
783
595
  leafIndex: number;
784
596
  newRootHex: string;
785
597
  };
786
- /**
787
- * SEAM 1b (dialer ⇄ session-node reconciliation): dial the counterparty THROUGH
788
- * this session's OWN node, so the session node N_A holds the connection its content
789
- * newStream actually rides. TRANSPORT-001's transport selector dialed on a separate
790
- * (composition-root) node whose connection N_A could not use — the per-session node
791
- * must be the dialer. Direct mode only here (the default content path, Part 4 D-a);
792
- * relay-circuit + dcutr strategy via N_A is a later seam. Tries each addr in turn;
793
- * succeeds on the first connection, returns a named failure if none connect.
794
- */
795
- connectToCounterparty(agentName: string, sessionId: string, addrs: string[]): Promise<{
796
- ok: true;
797
- } | {
798
- ok: false;
799
- reason: string;
800
- error: string;
801
- }>;
802
- /**
803
- * Send content over the session node's direct P2P content stream.
804
- * On a dead/missing stream this returns a NAMED, diagnosable failure — never a silent success
805
- * (which desyncs the two sides). Do not swallow a send error here.
806
- *
807
- * SCOPE / findings #3 + #4 — what this send path does and does NOT do today:
808
- * - #4: it delivers the content over the direct /cello/content/1.0.0 P2P
809
- * stream only. It does NOT also submit a K_local-SIGNED content_hash leaf to
810
- * the RELAY on /cello/relay/1.0.0 (EARS behavior #1). That relay hash-submit
811
- * is MSG-001's scope; AC-001's "relay log shows a hash_submit" evidence is
812
- * produced once MSG-001 lands.
813
- * - #3: because there is no relay yet, the sequence number cello_send returns
814
- * is the LOCAL leaf index, not a relay-assigned canonical global sequence.
815
- * Each daemon appends leaves in its own LOCAL observation order, so two
816
- * daemons' roots agree only under perfectly ping-ponged traffic. Canonical
817
- * cross-process ordering (and thus AC-002 root agreement under concurrent
818
- * bidirectional traffic) requires the relay-assigned sequence from MSG-001.
819
- */
820
- sendContent(agentName: string, sessionId: string, content: Uint8Array, contentHash: Uint8Array,
821
- /**
822
- * Required alongside the two below — every production caller already passes one, and an optional
823
- * parameter in front of a required one is what TypeScript refuses. Making it explicit costs
824
- * nothing and removes the last place a positional argument can silently shift.
825
- */
826
- correlationId: string | undefined,
827
- /**
828
- * The DOMAIN this content belongs to, as the relay and the directory will see it. Defaults to
829
- * MESSAGE so `cello_send` is unchanged; the document path passes 0x04/0x05. Not cosmetic — the
830
- * directory computes `final_message` and `answered` from the witnessed kind, and both of its
831
- * document exclusions were dead while every document leaf arrived here as a message.
832
- *
833
- * ⚠️ ALSO REQUIRED NOW, and for the same reason as `contentHashAlg` below — this parameter is the
834
- * precedent, not a bystander. It defaulted to MESSAGE, the document adapter in `daemon.ts`
835
- * silently dropped it, and the wire was wrong for a whole release: *"0.0.145 shipped the fix
836
- * everywhere except here."* A default that matches the common case makes the omission invisible
837
- * at every call site and at typecheck. Every caller states its kind now.
838
- */
839
- leafKind: number,
840
- /**
841
- * `DOD-M15-SEALWIRE-1` part B2b — the algorithm `contentHash` was produced under, taken from
842
- * `contentHashForSession` by the caller that computed the hash.
843
- *
844
- * Passed rather than re-derived HERE, deliberately: re-deriving would ask "how would this
845
- * session hash something now?", and the answer can differ from how THIS message was actually
846
- * hashed. A hash and its label must travel together or the peer refuses a message nobody touched.
847
- *
848
- * ⚠️ REQUIRED, NOT DEFAULTED — review B2b-1 F4, and the default is what made four mutants
849
- * unfalsifiable. It was `= CONTENT_HASH_ALGS.SHA256`, which equals the only value in play today,
850
- * so DROPPING THE ARGUMENT AT ANY OF THE FIVE HOPS produced byte-identical output and the whole
851
- * 2,800-test daemon suite stayed green. Measured at all four send sites individually.
852
- *
853
- * A default that equals the current value makes every threading edit invisible until the value
854
- * changes — and the day it changes, the dropped argument mislabels the message and every peer
855
- * refuses it as a tamper. Required makes a dropped argument a TYPECHECK failure instead of a
856
- * test question nobody can answer.
857
- */
858
- contentHashAlg: string): Promise<{
859
- ok: true;
860
- delivered: true;
861
- sequenceNumber?: number;
862
- authorship?: SentAuthorship;
863
- } | {
864
- ok: true;
865
- delivered: false;
866
- parked: true;
867
- sequenceNumber?: number;
868
- authorship?: SentAuthorship;
869
- }
870
- /**
871
- * ⚠️ `authorship` RIDES THE FAILURE PATH TOO — review pass 2, and it is the same reasoning that
872
- * already put `sequenceNumber` here.
873
- *
874
- * A DURABLY QUEUED message was witnessed and SIGNED before delivery was attempted; only the
875
- * direct hand-off failed. Omitting the proof here meant every relay-degraded-but-alive send —
876
- * the common case — wrote a transcript row indistinguishable from one the relay never saw, while
877
- * the signature for it sat in scope and was discarded. The position survives a failed delivery
878
- * for exactly this reason; so does the proof.
879
- */
880
- | {
881
- ok: false;
882
- reason: string;
883
- error: string;
884
- durable: boolean;
885
- cause?: string;
886
- guidance?: string;
887
- sequenceNumber?: number;
888
- authorship?: SentAuthorship;
889
- }>;
890
- /**
891
- * M7 DOD-SPINE-7: submit THIS party's SEAL ctrl leaf (0x02) to the relay witness.
892
- * Structure: content_hash = SHA-256(0x02 || encodeSealPayload({session_id, final_root,
893
- * close_timestamp, "PENDING"})), where final_root is the daemon's OWN tree root. Two
894
- * distinct-sender SEAL leaves in the relay's log trigger the relay's #maybeProcessSeal
895
- * → directory processSeal (rebuild + verify the signed chain) → FROST notarization →
896
- * session_sealed. Requires an active relay client; the caller falls back to the
897
- * directory-mediated path when this returns relay_unavailable.
898
- */
899
- submitSealLeaf(agentName: string, sessionId: string, correlationId?: string): Promise<{
900
- ok: true;
901
- sequenceNumber: number;
902
- reportedRootHex: string;
903
- } | {
904
- ok: false;
905
- reason: string;
906
- reportedRootHex?: string;
907
- sequenceNumber?: number;
908
- }>;
909
- /**
910
- * CELLO-M7-UPGRADE-001 (DOD-UP-1): readiness of a session for B to RATIFY a unilateral seal
911
- * (the returning absent party). This is the SAME verifiability bar as the UP-2 auto-ack gate:
912
- *
913
- * - `known`: the session exists locally with its content (B has a transcript to ratify). After a
914
- * restart B reloads it from SQLite, and autoRecoverForAgent re-pulls any parked content first.
915
- * - `tampered`: the content cross-check flagged a content_hash mismatch (#contentDesynced) — B
916
- * must NEVER ratify content it could not integrity-verify (the KERNEL refusal, AC-003).
917
- *
918
- * The directory separately verifies B's ack signature is genuine; B separately verifies the
919
- * unilateral cert signature (R1 is authentic). NOTE: a full "B's frontier covers R1's tail"
920
- * completeness check (the `desynced` reason) requires the deferred MSG-001-3b canonical-sequence
921
- * reconciliation — same documented limitation as the UP-2 gate above.
922
- */
923
- getSealUpgradeReadiness(agentName: string, sessionId: string): SealUpgradeReadiness;
924
- ingestReceivedContent(agentName: string, sessionId: string, content: Uint8Array, contentHash: Uint8Array, correlationId?: string,
925
- /**
926
- * DOD-FRONTIER-STRAND-1 AC1: the relay-assigned canonical position for THIS message, taken from
927
- * the verified ordering record by the caller. Passed EXPLICITLY rather than recovered from
928
- * `#witnessedSeq`, because that map is keyed by content hash — so two byte-identical messages
929
- * collapse in it before dedup is ever consulted, which is the whole defect. Absent when the
930
- * session has no relay witness (relay-degraded): see the announced fallback below.
931
- */
932
- canonicalSeqIn?: number,
933
- /**
934
- * DOD-M15-SEALWIRE-1 part B1 — the algorithm the SENDER named on the frame, verbatim.
935
- *
936
- * `undefined` means the frame carried no name, which is a peer that predates the field and is
937
- * the one case we may safely assume `sha256` for. It is threaded through rather than read off
938
- * the session, because whether a hash is salted is a fact about the FRAME and its sender, never
939
- * about what this side happens to hold.
940
- */
941
- contentHashAlgIn?: string | null,
942
- /**
943
- * DOD-M15-SEALWIRE-1 bullet 5: the VERIFIED authorship proof for this message, when the caller
944
- * has one. The caller is the only place that has it — `#verifyAuthorshipClaim` verifies the
945
- * signature the frame carries beside the sender's own signed bytes, against the key inside those
946
- * bytes, and matches the signer to this session's counterparty. That result reaches here or
947
- * nowhere.
948
- *
949
- * ⚠️ IT USED TO NAME `#recordFrameOrdering`, and that was accurate until
950
- * `DOD-M15-AUTHORSHIP-ABSENT-1`: the signature arrived only inside the RELAY's Structure 2, so
951
- * checking authorship needed a relay record. It does not now, and the old name sends a reader to
952
- * a method that answers a different question. Rewritten, not deleted — that dependence is the
953
- * defect the unit removed.
954
- *
955
- * Optional, because the PARK route ingests without it: recovered mail proves its sender by the
956
- * mailbox envelope instead. The row records which it was, so absence is never silent.
957
- */
958
- verifiedAuthorship?: {
959
- senderPubkey: Uint8Array;
960
- senderSig: Uint8Array;
961
- },
962
- /**
963
- * 024-ORPHANTRIAGE — the key whose signature VERIFIED on a frame we could not tie to a session.
964
- *
965
- * Read by the orphan branch below and NOWHERE ELSE. It exists because the daemon establishes,
966
- * cryptographically, that the sender holds a private key — and then discarded that the instant
967
- * the session lookup came back empty, leaving the operator advised to go and make contact with
968
- * whoever sent a message for a conversation that does not exist.
969
- *
970
- * Absent on the park-recovery caller, which cannot reach the orphan branch at all:
971
- * `authenticateParkedEntry` refuses `counterparty_unknown` from the same missing record first.
972
- */
973
- verifiedSignerUnmatched?: Uint8Array): Promise<{
974
- ok: true;
975
- leafIndex: number;
976
- sequenceNumber: number;
977
- held?: boolean;
978
- appendedCount?: number;
979
- screenedOut?: boolean;
980
- } | {
981
- ok: false;
982
- reason: string;
983
- }>;
984
- /**
985
- * DOD-MSG-4: record the relay-witnessed canonical sequence for a content hash. The relay is the
986
- * ordering authority (Structure 2): it assigns each message a sequence from its hash and delivers
987
- * B the (content_hash -> sequence) binding via leaf_deliver. The strict-in-order gate orders the
988
- * transcript by THIS — never a sender-stamped field. Also advances the per-session high-water mark
989
- * (the largest witnessed sequence) reserved for the future catch-up-before-live increment. Idempotent.
990
- */
991
- recordWitnessedSequence(agentName: string, sessionId: string, contentHashHex: string, sequenceNumber: number): void;
992
598
  /** DOD-M12B-LEAF-TRIGGERS-FETCH-1 test seams. */
993
599
  setLeafFetchGraceMsForTest(ms: number): void;
994
600
  markContentPresentForTest(agentName: string, sessionId: string, contentHashHex: string): void;
995
- /**
996
- * DOD-MSG-4: the relay's high-water canonical sequence for this session (largest witnessed leaf),
997
- * or -1 if none. The relay is the ordering authority, so this is the outside view of how far the
998
- * session has actually progressed — which is why it is the right input to a catch-up-before-live
999
- * gate. Consumed by `sealReadiness` (M12-P14) for REPORTING only: the missing-leaf decision is made
1000
- * from `#witnessedSeq`, because this counts the relay's sequence space (which includes ctrl leaves)
1001
- * and the tree does not. Maintained by `recordWitnessedSequence`.
1002
- */
1003
- /**
1004
- * DOD-COATTEND-1 (review F2): leaf sequences whose plaintext failed to reach the transcript and
1005
- * are therefore undeliverable. Empty is the overwhelmingly normal case.
1006
- */
1007
- getUndeliverableSeqs(agentName: string, sessionId: string): readonly number[];
1008
- /**
1009
- * M12-P14: is this side's chain COMPLETE enough to be sealed?
1010
- *
1011
- * A seal is a bilateral signature over the same conversation, so a side that is missing a leaf
1012
- * cannot produce a signable one — the counterparty compares frontiers and refuses with
1013
- * `leaf_count_mismatch`. That refusal is correct and it is also terminal: there is no backfill
1014
- * request in the protocol, so the only exit is a force-abandon, which yields NO notarized receipt.
1015
- * Measured 2026-08-05 on two sessions that died exactly this way (initiator 2 leaves, responder 3).
1016
- *
1017
- * The cheap prevention is to notice BEFORE asking. Two local signals already exist and, until now,
1018
- * nothing read either of them at close time:
1019
- * - `#highWaterSeq` — the largest canonical sequence the RELAY has witnessed for this session.
1020
- * The relay is the ordering authority, so a high-water above our own frontier is proof that a
1021
- * leaf exists which we have not appended. (Its own doc comment called it "reserved … NOT yet
1022
- * consumed by the gate" — this is that consumer.)
1023
- * - `#heldContent` — content we HAVE received and verified but cannot append because it sits
1024
- * behind a gap. Holding content and sealing anyway would seal a chain we know is short.
1025
- *
1026
- * Deliberately NOT a network call: it must work when the counterparty is unreachable, which is
1027
- * the whole situation a seal-interrupted exists for.
1028
- *
1029
- * KNOWN LIMIT, stated rather than hidden: both maps are in-memory and cleared on teardown, so
1030
- * after a daemon restart this returns ready for a session whose gap predates the restart — which
1031
- * is the shape of the 2026-08-05 incident itself. Closing that needs the mailbox drained (or the
1032
- * high-water persisted) before the check; tracked with M12-P14, not claimed here.
1033
- */
1034
- sealReadiness(agentName: string, sessionId: string): {
1035
- ready: boolean;
1036
- treeSize: number;
1037
- highWaterSeq: number;
1038
- heldCount: number;
1039
- missingLeaves: number;
1040
- /** DOD-M12B-INDEX-1: of `heldCount`, how many are THIS side's own sends versus the
1041
- * counterparty's. They block a seal identically and mean completely different things. */
1042
- heldOwn: number;
1043
- heldReceived: number;
1044
- /**
1045
- * DOD-M15-DIVERGE-1 — this tree and the relay's counter have PROVABLY parted.
1046
- *
1047
- * The other two counters both measure the same direction: positions the relay committed that
1048
- * this tree has not appended. This is the OPPOSITE direction — this tree holds a leaf at a
1049
- * position the relay assigned to something else — and it is the direction an injected or forged
1050
- * leaf appears in. Without it `ready` was asymmetric, and a diverged session read as perfectly
1051
- * sealable right up until the counterparty answered `leaf_count_mismatch`, which is terminal.
1052
- */
1053
- diverged: boolean;
1054
- };
1055
- /**
1056
- * DOD-M12B-SEAL-STUCK-1 — the operator-facing answer to "can this session be closed?".
1057
- *
1058
- * THREE STATES, because there are three answers. `sealReadiness` above returns a boolean plus raw
1059
- * counters, and both of its counters are easy to misread on a surface:
1060
- *
1061
- * - `missingLeaves` is `#witnessedSeq.size`, which is every position the relay witnessed that
1062
- * this tree has not appended — and a HELD frame keeps its witness entry. So it INCLUDES the
1063
- * held ones. Reporting it beside `heldCount` counts the same message twice and labels one copy
1064
- * "never received" when it is sitting on our own disk. Split here into what each actually is.
1065
- * - Neither counter survives a restart on its own: `#witnessedSeq` is memory-only. Held content
1066
- * is durable since DOD-M12B-STRAND-1, but a position the relay witnessed for content that
1067
- * never arrived leaves no trace. So for a session carrying leaves this process did not watch
1068
- * arrive, "clean" is unknowable — and saying `ready` there invites a close that gets
1069
- * `leaf_count_mismatch` back, which is terminal and costs the receipt for good.
1070
- */
1071
- sealReadinessView(agentName: string, sessionId: string): SealReadinessView;
1072
- /**
1073
- * DOD-M12B-ABANDON-NOTIFY-1 — tell the counterparty we have hung up. Best effort, never blocking.
1074
- *
1075
- * A force-abandon marks the session terminal HERE and did nothing else, so the other side kept
1076
- * its half live, kept retrying delivery into it, and kept trying to re-establish — forever,
1077
- * because nothing would ever answer. That is what produced the 2026-08-17 notification storm:
1078
- * surviving halves calling continuously while the operator saw connection requests from agents
1079
- * nobody was driving.
1080
- *
1081
- * BEST EFFORT, and every caller must treat it that way. A peer that is offline cannot be told, so
1082
- * this is an improvement on silence rather than a guarantee — and it must never delay or fail the
1083
- * abandon, which is the operator's escape hatch out of a session that can never seal.
1084
- */
1085
- notifyCounterpartyAbandon(agentName: string, sessionId: string, correlationId?: string): Promise<AbandonNoticeResult>;
1086
- /**
1087
- * DOD-M12B-ABANDON-NOTIFY-1 — the receiving half: our counterparty has abandoned, so retire.
1088
- *
1089
- * RETIRING IS NOT DELETING. The counterparty walking away forfeits the notarized receipt; it must
1090
- * not also cost the operator the record of what was actually said. The transcript and the tree
1091
- * stay exactly as they are.
1092
- *
1093
- * Only an `active` or `interrupted` session moves. A SEALED session has a notarized receipt and
1094
- * must never be turned into an abandoned one by a late or duplicated notice — that would destroy
1095
- * the artifact this protocol exists to produce. An unknown session is refused rather than
1096
- * created: an authenticated stream proves who is speaking, not that a session exists.
1097
- */
1098
- retireOnCounterpartyAbandon(agentName: string, sessionId: string, correlationId?: string): Promise<boolean>;
1099
601
  /**
1100
602
  * DOD-CAP-SELF-HEAL-1 — the numbers behind a cap refusal, for the OPERATOR'S alarm only.
1101
603
  *
@@ -1111,79 +613,12 @@ export declare class SessionNodeManager {
1111
613
  mustClear: number;
1112
614
  blocked: boolean;
1113
615
  };
1114
- /**
1115
- * DOD-M12B-INDEX-1 — commit THIS agent's own leaf at the position the relay assigned it.
1116
- *
1117
- * The receiver has always enforced "leaf index === canonical position": content witnessed ahead
1118
- * of the next expected leaf is held, not appended out of order. The sender never did. It had the
1119
- * position in hand — the relay answers about 4 ms before the append — and called a push-only
1120
- * append that puts the leaf at the tail whatever the tail happens to be. While its own tree has
1121
- * no gap the two agree and nothing shows; the first gap puts its leaf at someone else's index,
1122
- * parts its root from the counterparty's, and the next seal gets `leaf_count_mismatch`, which is
1123
- * terminal.
1124
- *
1125
- * DELIVERY IS NOT DEFERRED BY THIS. The caller has already put the bytes on the wire; only the
1126
- * leaf waits for its slot, exactly as a received message does. Holding our own send is only
1127
- * affordable because holds are durable (DOD-M12B-STRAND-1) — before that it would have risked
1128
- * losing the message outright.
1129
- *
1130
- * `assignedSeq` absent means no ordering authority answered. That is the documented degradation
1131
- * and it appends in arrival order as before: with no position there is no discipline to enforce,
1132
- * and refusing would take messaging down whenever the relay is unreachable.
1133
- */
1134
- placeOwnLeaf(agentName: string, sessionId: string, contentHashHex: string, sentBytes: Uint8Array, assignedSeq: number | undefined, correlationId: string | undefined,
1135
- /**
1136
- * ⚠️ NO DEFAULT, for the same reason `authorship` has none.
1137
- *
1138
- * `kind = "msg"` meant a caller writing a `doc` or a `ctrl` leaf got a `msg` leaf by saying
1139
- * nothing, and the tree recorded a leaf kind the author never chose. TypeScript also forbids a
1140
- * required parameter after a defaulted one, so leaving the default here would have forced
1141
- * `authorship` back to optional — which is the defect above. Every one of the seven call sites
1142
- * already passed a kind or wanted "msg"; making it explicit cost nothing and removes a second
1143
- * silent answer from the same signature.
1144
- */
1145
- kind: WritableSessionTreeLeafKind,
1146
- /**
1147
- * DOD-M15-SEALWIRE-1 bullet 5 — the proof for THIS send, so a held row keeps it.
1148
- *
1149
- * ⚠️ REQUIRED, AND `undefined` IS A VALID ANSWER — the two are not the same thing.
1150
- *
1151
- * This was `authorship?:` for exactly one review cycle, and in that cycle THREE of the seven
1152
- * call sites omitted it: `daemon.ts` 1440, 1671, 1685 — the away-reply path, which is the
1153
- * highest-traffic sent-writer in the daemon and the one with no human watching it. All three
1154
- * had the proof **already in a local variable one line below**, handed to
1155
- * `recordTranscriptMessage` and not to this method. Nothing went red, because an optional
1156
- * parameter's whole behaviour on omission is to look deliberate.
1157
- *
1158
- * An unwitnessed send genuinely has no proof, so absence must stay expressible. Requiring the
1159
- * parameter keeps that while making the caller SAY it: omission is now a type error, and
1160
- * `undefined` is a claim the author made rather than one the signature made for them.
1161
- */
1162
- authorship: SentAuthorship | undefined): {
1163
- placed: true;
1164
- leafIndex: number;
1165
- diverged?: true;
1166
- unwitnessed?: true;
1167
- } | {
1168
- placed: false;
1169
- heldAt: number;
1170
- };
1171
- /** DAEMON-004: pop the oldest verified received content for cello_receive. */
1172
- takeReceivedContent(agentName: string, sessionId: string): ReceivedContentEntry | null;
1173
616
  /** DOD-AWAY-WRAP-1: peek at the hex of the most-recently buffered (last) received message without
1174
617
  * consuming it. Used by sendAwayResponse to detect [[WRAP]]-signalled messages and skip the away
1175
- * reply. Returning the last entry (not the first) is intentional — #appendVerifiedContent always
618
+ * reply. Returning the last entry (not the first) is intentional — the ingest file's
619
+ * appendVerifiedContent always
1176
620
  * pushes to the tail, so the tail is the message that just triggered onContentArrived. */
1177
621
  peekLatestReceivedContentHex(agentName: string, sessionId: string): string | null;
1178
- /**
1179
- * TEST-ONLY (M8C-INBOX-1 reviewer F1): buffer a received message + persist its transcript row,
1180
- * exactly as the real inbound path (#appendVerifiedContent) does, WITHOUT standing up a session
1181
- * tree — so a test can drive a live cello_receive that advances the read watermark (the N3
1182
- * "delivery marks read" coupling). Only reachable via the CELLO_ENV=test IPC hook.
1183
- */
1184
- /** CELLO_ENV=test only: patch a relay client and session-id bytes onto an existing active node entry
1185
- * so submitSealLeaf succeeds without a real relay handshake (used by the oneshot relay-path test). */
1186
- patchRelayClientForTest(agentName: string, sessionId: string, relayClient: AgentRelayClient, relaySessionIdBytes: Uint8Array): void;
1187
622
  pushReceivedContentForTest(agentName: string, sessionId: string, seq: number, content: string, senderPubkey: string): void;
1188
623
  /**
1189
624
  * F1-b: the terminal answer for a session that sealed while a blocking receive was (or could be)
@@ -1235,15 +670,6 @@ export declare class SessionNodeManager {
1235
670
  observeInboundContentFramesForTest(cb: (frame: Record<string, unknown>) => void): void;
1236
671
  /** Injected by the daemon once its per-agent key providers exist. See `#keyProviderResolver`. */
1237
672
  setKeyProviderResolver(resolver: (agentName: string) => KeyProvider | undefined): void;
1238
- /**
1239
- * Test seam: force this session's own salt half, so the LOCAL-defect path is reachable.
1240
- *
1241
- * `generateSaltContribution` cannot produce a degenerate half, which is the point of it — so the
1242
- * only way to exercise "our own random source is broken" end-to-end is to stand in for the broken
1243
- * source. Named `…ForTest` like every other seam in this file, and it writes the same map
1244
- * production writes rather than a parallel one, so a test cannot pass against state the daemon
1245
- * never reads.
1246
- */
1247
673
  /**
1248
674
  * Test seam: run the auto-acknowledge gate, exactly as the counterparty's SEAL ctrl leaf does.
1249
675
  *
@@ -1255,7 +681,7 @@ export declare class SessionNodeManager {
1255
681
  * `content_verification_unavailable` branch had no coverage anywhere in the repo and two mutants
1256
682
  * on it survived the full gate.
1257
683
  *
1258
- * It calls the REAL private method rather than reproducing its logic, so a test cannot pass
684
+ * It calls the REAL method (public on `SessionSeal` since the split, private before it) rather than reproducing its logic, so a test cannot pass
1259
685
  * against a decision production does not make.
1260
686
  */
1261
687
  runAutoAcknowledgeGateForTest(agentName: string, sessionId: string, correlationId?: string): void;
@@ -1314,22 +740,6 @@ export declare class SessionNodeManager {
1314
740
  salt: null;
1315
741
  reason: "none" | "unreadable";
1316
742
  };
1317
- /**
1318
- * M7-SESSION-001 AC-004/AC-005: Register a relay stream for an active session.
1319
- * Starts a background reader that watches for session_interrupted frames and
1320
- * stream close events. Both detection paths call markInterruptedWithDetails().
1321
- *
1322
- * The reader runs for the lifetime of the relay stream. If the stream closes
1323
- * without delivering a session_interrupted frame (AC-005 / 'stream_close' path),
1324
- * the session is still marked interrupted.
1325
- *
1326
- * @param sessionId The hex session ID
1327
- * @param stream The relay stream to monitor
1328
- * @param messageCount Number of message leaves at the time of registration
1329
- * (used as the count at interruption — best effort since exact count at frame
1330
- * receipt may differ, but this is the value available at stream setup time)
1331
- */
1332
- registerRelayStream(agentName: string, sessionId: string, stream: Stream, messageCount?: number): void;
1333
743
  /**
1334
744
  * DOD-M15-RELAYSLOTS-1: accept the directory's online token for an agent. Called on every
1335
745
  * signaling connect and reconnect, which is what keeps it fresh.
@@ -1343,109 +753,6 @@ export declare class SessionNodeManager {
1343
753
  * directory does not recognise.
1344
754
  */
1345
755
  getDirectoryOnlineToken(agentName: string): Uint8Array | undefined;
1346
- /**
1347
- * DOD-NAT-REACHABILITY-1 (Phase 2): accept the directory's relay-pool endpoints
1348
- * for an agent (arrives with signaling_auth_ok, i.e. on every connect AND every
1349
- * reconnect). If the agent's standing receiver is up but holds NO reservation —
1350
- * the agent-online ensure raced ahead of auth_ok, or every relay was down at
1351
- * create time — rebuild it now so the agent becomes dialable without waiting
1352
- * for a session handoff that (being unreachable) would never come.
1353
- */
1354
- setDirectoryRelayEndpoints(agentName: string, endpoints: Array<{
1355
- relayPeerId: string;
1356
- relayAddrs: string[];
1357
- }>): void;
1358
- /**
1359
- * Is this agent currently skipping this relay? The observable half of the failover decision — a
1360
- * test that asserts only on the classifier's boolean proves nothing about what the daemon does.
1361
- */
1362
- isRelayQuarantined(agentName: string, relayPeerId: string): boolean;
1363
- /**
1364
- * DOD-M12B-SESSION-SEED-1 — bring an interrupted session back on the peer id it already has.
1365
- *
1366
- * THE DEFECT THIS CLOSES. `markInterruptedWithDetails` and `destroySessionNode` stop the node and
1367
- * delete it from `#activeNodes`, and until now **nothing anywhere recreated one**. A laptop-close
1368
- * session stayed stuck even though both processes were alive and both keypairs were still in
1369
- * memory — the trace on 2026-08-17 found no missing transport capability, just a missing edge.
1370
- *
1371
- * TWO THINGS HAVE TO HAPPEN, and doing only one leaves the session exactly as stuck:
1372
- * 1. the NODE comes back, at the same peer id, or the counterparty can never dial us again;
1373
- * 2. the STATUS comes back to `active`, or every send still refuses with `session_not_active`.
1374
- *
1375
- * **DEMAND-DRIVEN ONLY.** Nothing calls this on a timer. That is the `REDIAL-1` discipline and it
1376
- * is also Andre's tenet — a background rebuilder would hold a dialable endpoint open for a session
1377
- * nobody is using, which is the "open connection a malicious agent can farm for" in as many words.
1378
- *
1379
- * **TERMINAL IS TERMINAL.** A sealed or abandoned session had its seed zeroed in the same step
1380
- * that wrote its status, so there is nothing to come back on. This refuses by name rather than
1381
- * minting a fresh identity — a revival that quietly mints would hand one session a second peer id
1382
- * and break the invariant while appearing to work.
1383
- *
1384
- * Idempotent: a session that already has a live node returns ok without building a second one.
1385
- */
1386
- reviveSessionNode(agentName: string, sessionId: string): Promise<{
1387
- ok: true;
1388
- peerId: string;
1389
- } | {
1390
- ok: false;
1391
- reason: string;
1392
- guidance?: string;
1393
- }>;
1394
- /**
1395
- * DOD-M12B-SESSION-SEED-1 — the DEMAND edge: a send on an interrupted session revives it.
1396
- *
1397
- * One of TWO production callers of `reviveSessionNode` — `reviveIfNeededForRead` is the other —
1398
- * and both are deliberately demand paths rather than timers. The `REDIAL-1` discipline and Andre's tenet say the same thing from two
1399
- * directions: nothing may re-open on its own, because a background rebuilder would hold a dialable
1400
- * endpoint open for a session nobody is using — the *"open connection a malicious agent can farm
1401
- * for"*. The operator sending is the demand; there is no other trigger.
1402
- *
1403
- * A no-op for the normal case. An `active` session with a live node returns immediately without
1404
- * touching it — this sits on the hot path of every send, and replacing a healthy node would be
1405
- * churn that changes the peer id for no reason.
1406
- */
1407
- reviveIfNeededForSend(agentName: string, sessionId: string): Promise<{
1408
- ok: true;
1409
- } | {
1410
- ok: false;
1411
- reason: string;
1412
- guidance?: string;
1413
- }>;
1414
- /**
1415
- * DOD-M12B-SESSION-SEED-1 (case B) — the INBOUND half of the demand edge.
1416
- *
1417
- * `reviveIfNeededForSend` covers the operator waking first. Case B's triggers are symmetric — a
1418
- * wifi hop, a relay restart, a directory node cycling — so half the time the COUNTERPARTY wakes
1419
- * first. They send; we have no node yet, because revival is demand-driven and we have demanded
1420
- * nothing. Their content parks at the relay, which is the backstop working as designed.
1421
- *
1422
- * Then the operator comes back and READS, and until now that told them nothing: the receive
1423
- * handler reads the transcript and never gates on status, so it happily reports what is already
1424
- * stored while messages sit parked, waiting for a node that will not exist until the operator
1425
- * happens to SEND. An operator who only reads was stuck forever with a surface that looked fine.
1426
- *
1427
- * **WHY A READ MAY TRIGGER THIS AND AN INBOUND DIAL MAY NOT.** Andre's tenet is about what a
1428
- * REMOTE party can cause: *"an open connection that a malicious agent can farm for."* Reviving
1429
- * because a peer dialled us would hand that lever straight to the peer — a stranger could keep our
1430
- * endpoints open indefinitely by poking dead sessions. A read is the OPERATOR asking, on their own
1431
- * machine, for their own session: the same class of demand as a send, and the class the tenet
1432
- * allows. That distinction is the whole reason this is a separate entry point rather than a
1433
- * revival triggered from the inbound handler.
1434
- */
1435
- reviveIfNeededForRead(agentName: string, sessionId: string): Promise<{
1436
- ok: true;
1437
- } | {
1438
- ok: false;
1439
- reason: string;
1440
- guidance?: string;
1441
- }>;
1442
- /** DOD-M12B-REVIVE-PARK-1 test seam: the relay the live entry will park to. Not otherwise
1443
- * observable — `#activeNodes` is private and the park's own refusal is silent about which of its
1444
- * four preconditions was missing. */
1445
- getSessionRelayForTest(agentName: string, sessionId: string): {
1446
- relayPeerId?: string;
1447
- relayAddrs?: string[];
1448
- } | null;
1449
756
  /** DOD-M12B-SESSION-SEED-1 test seams: the counterparty addresses a re-dial depends on. Not
1450
757
  * otherwise observable — they are set from a signed relay assignment that a fixture cannot mint. */
1451
758
  setCounterpartyAddrsForTest(agentName: string, sessionId: string, addrs: string[]): void;
@@ -1493,11 +800,5 @@ export declare class SessionNodeManager {
1493
800
  * locked out by the acceptance bound forever.
1494
801
  */
1495
802
  countEstablishedReceived(agentName: string, sessionId: string): number;
1496
- /** CC-5/F21: unilaterally mark a session locally-terminal ("abandoned") — retire its live node and
1497
- * set the DB status, with NO bilateral seal (a dead half-open handshake has nothing to notarize).
1498
- * Used by cello_close_session { force } and the dead-half-open reaper. Idempotent: a missing/already-
1499
- * abandoned session is a no-op. Resolves true iff the status flip was actually written (CC-10
1500
- * reviewer LOW: callers must not report a reap as successful when the write failed). */
1501
- abandonSession(agentName: string, sessionId: string): Promise<boolean>;
1502
803
  }
1503
804
  //# sourceMappingURL=session-node-manager.d.ts.map