@cello-protocol/daemon 0.0.193 → 0.0.195

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 (114) hide show
  1. package/dist/assignment-verify.d.ts +39 -11
  2. package/dist/assignment-verify.d.ts.map +1 -1
  3. package/dist/assignment-verify.js +133 -17
  4. package/dist/assignment-verify.js.map +1 -1
  5. package/dist/authorship-verification.d.ts +70 -0
  6. package/dist/authorship-verification.d.ts.map +1 -0
  7. package/dist/authorship-verification.js +441 -0
  8. package/dist/authorship-verification.js.map +1 -0
  9. package/dist/daemon.js +4 -1
  10. package/dist/daemon.js.map +1 -1
  11. package/dist/db-identity-store.d.ts +1 -0
  12. package/dist/db-identity-store.d.ts.map +1 -1
  13. package/dist/db-identity-store.js +19 -1
  14. package/dist/db-identity-store.js.map +1 -1
  15. package/dist/held-content.d.ts +141 -0
  16. package/dist/held-content.d.ts.map +1 -0
  17. package/dist/held-content.js +388 -0
  18. package/dist/held-content.js.map +1 -0
  19. package/dist/inbound-refusals.d.ts +283 -0
  20. package/dist/inbound-refusals.d.ts.map +1 -0
  21. package/dist/inbound-refusals.js +919 -0
  22. package/dist/inbound-refusals.js.map +1 -0
  23. package/dist/inbound-sessions.d.ts +1 -1
  24. package/dist/inbound-sessions.d.ts.map +1 -1
  25. package/dist/inbound-sessions.js +189 -32
  26. package/dist/inbound-sessions.js.map +1 -1
  27. package/dist/initiate-session-handler.d.ts.map +1 -1
  28. package/dist/initiate-session-handler.js +82 -0
  29. package/dist/initiate-session-handler.js.map +1 -1
  30. package/dist/outbound-sessions.d.ts.map +1 -1
  31. package/dist/outbound-sessions.js +108 -8
  32. package/dist/outbound-sessions.js.map +1 -1
  33. package/dist/park-recovery.d.ts +205 -0
  34. package/dist/park-recovery.d.ts.map +1 -0
  35. package/dist/park-recovery.js +600 -0
  36. package/dist/park-recovery.js.map +1 -0
  37. package/dist/refusal-notices.d.ts +196 -0
  38. package/dist/refusal-notices.d.ts.map +1 -0
  39. package/dist/refusal-notices.js +496 -0
  40. package/dist/refusal-notices.js.map +1 -0
  41. package/dist/refusal-reasons.d.ts +34 -0
  42. package/dist/refusal-reasons.d.ts.map +1 -1
  43. package/dist/refusal-reasons.js +84 -0
  44. package/dist/refusal-reasons.js.map +1 -1
  45. package/dist/registration-manager.d.ts.map +1 -1
  46. package/dist/registration-manager.js +126 -5
  47. package/dist/registration-manager.js.map +1 -1
  48. package/dist/registration-persistence.d.ts +18 -0
  49. package/dist/registration-persistence.d.ts.map +1 -1
  50. package/dist/registration-persistence.js +6 -0
  51. package/dist/registration-persistence.js.map +1 -1
  52. package/dist/session-assignment-parser.d.ts.map +1 -1
  53. package/dist/session-assignment-parser.js +23 -1
  54. package/dist/session-assignment-parser.js.map +1 -1
  55. package/dist/session-ceremony.d.ts +12 -7
  56. package/dist/session-ceremony.d.ts.map +1 -1
  57. package/dist/session-ceremony.js +12 -7
  58. package/dist/session-ceremony.js.map +1 -1
  59. package/dist/session-ephemerals.d.ts +271 -0
  60. package/dist/session-ephemerals.d.ts.map +1 -0
  61. package/dist/session-ephemerals.js +546 -0
  62. package/dist/session-ephemerals.js.map +1 -0
  63. package/dist/session-leaf-records.d.ts +132 -0
  64. package/dist/session-leaf-records.d.ts.map +1 -0
  65. package/dist/session-leaf-records.js +320 -0
  66. package/dist/session-leaf-records.js.map +1 -0
  67. package/dist/session-liveness.d.ts +135 -0
  68. package/dist/session-liveness.d.ts.map +1 -0
  69. package/dist/session-liveness.js +347 -0
  70. package/dist/session-liveness.js.map +1 -0
  71. package/dist/session-node-manager.d.ts +293 -1652
  72. package/dist/session-node-manager.d.ts.map +1 -1
  73. package/dist/session-node-manager.js +1039 -9659
  74. package/dist/session-node-manager.js.map +1 -1
  75. package/dist/session-node-types.d.ts +933 -0
  76. package/dist/session-node-types.d.ts.map +1 -0
  77. package/dist/session-node-types.js +576 -0
  78. package/dist/session-node-types.js.map +1 -0
  79. package/dist/session-own-chain-store.d.ts +65 -0
  80. package/dist/session-own-chain-store.d.ts.map +1 -0
  81. package/dist/session-own-chain-store.js +75 -0
  82. package/dist/session-own-chain-store.js.map +1 -0
  83. package/dist/session-queries.d.ts +476 -0
  84. package/dist/session-queries.d.ts.map +1 -0
  85. package/dist/session-queries.js +1006 -0
  86. package/dist/session-queries.js.map +1 -0
  87. package/dist/session-records.d.ts +320 -0
  88. package/dist/session-records.d.ts.map +1 -0
  89. package/dist/session-records.js +792 -0
  90. package/dist/session-records.js.map +1 -0
  91. package/dist/session-relay-client.d.ts +35 -3
  92. package/dist/session-relay-client.d.ts.map +1 -1
  93. package/dist/session-relay-client.js +211 -30
  94. package/dist/session-relay-client.js.map +1 -1
  95. package/dist/session-salts.d.ts +390 -0
  96. package/dist/session-salts.d.ts.map +1 -0
  97. package/dist/session-salts.js +1457 -0
  98. package/dist/session-salts.js.map +1 -0
  99. package/dist/session-schema.d.ts +30 -0
  100. package/dist/session-schema.d.ts.map +1 -0
  101. package/dist/session-schema.js +786 -0
  102. package/dist/session-schema.js.map +1 -0
  103. package/dist/standing-receivers.d.ts +280 -0
  104. package/dist/standing-receivers.d.ts.map +1 -0
  105. package/dist/standing-receivers.js +1100 -0
  106. package/dist/standing-receivers.js.map +1 -0
  107. package/dist/transport-selector.d.ts +13 -0
  108. package/dist/transport-selector.d.ts.map +1 -1
  109. package/dist/transport-selector.js.map +1 -1
  110. package/dist/witness-alerts.d.ts +40 -0
  111. package/dist/witness-alerts.d.ts.map +1 -0
  112. package/dist/witness-alerts.js +102 -0
  113. package/dist/witness-alerts.js.map +1 -0
  114. package/package.json +5 -5
@@ -5,8 +5,10 @@
5
5
  * 1. Per-session nodes: fresh transport key + Peer ID, connectionGater allows
6
6
  * only the designated counterparty. Created during cello_initiate_session
7
7
  * (outbound) or cello_await_session (inbound, via standing receiver handoff).
8
- * 2. Standing receiver node: pre-created, open gater, kept alive at all times.
9
- * Handed to the first inbound session; immediately replaced.
8
+ * 2. Standing receiver node: pre-created, kept alive at all times, handed to the first inbound
9
+ * session and immediately replaced. Its gater is NOT open — `DOD-M15-ASSIGN-1` made it admit
10
+ * nobody inbound until a session offer names the dialer (see `standing-receivers.ts`). This
11
+ * line said "open gater" for as long as that was true and for a while after it was not.
10
12
  * 3. 32-node cap: enforced before any new node is created.
11
13
  * 4. Session status in the DB: active → sealed (on close) or interrupted
12
14
  * (on graceful shutdown or SIGKILL-restart detection).
@@ -21,509 +23,326 @@ import { type DaemonDatabase } from "./sqlcipher-db.js";
21
23
  import { type RefusalKind } from "./refusal-reasons.js";
22
24
  import type { Stream } from "@libp2p/interface";
23
25
  import type { Logger, SessionRecord, SealReadinessView } from "./types.js";
24
- import { SessionConnectionGater } from "./session-connection-gater.js";
25
26
  import { SessionTree, type WritableSessionTreeLeafKind } from "./session-tree.js";
26
27
  import { type CelloNode, type IAutoNatService } from "@cello-protocol/transport";
27
28
  import type { KeyProvider } from "@cello-protocol/crypto";
28
29
  import { type SessionEphemeral } from "@cello-protocol/crypto";
29
30
  import { type ParkEnvelope } from "./park-envelope.js";
30
- import { AgentRelayClient, type RelayAssignmentCarry, type RelayAuthRefusal, type RelayWitnessAlert } from "./session-relay-client.js";
31
- /**
32
- * One row in an agent's witness-alert list — DOD-M15-CORROBORATE-1 review F1. Deduped on
33
- * `(witness relay, session)`, so a repeated observation raises `occurrences` rather than taking
34
- * another slot in a bounded list.
35
- */
36
- export interface WitnessAlertNotice {
37
- /** `${relayId}::${sessionIdHex}` — the dedupe key, not shown to anyone. */
38
- key: string;
39
- alert: RelayWitnessAlert;
40
- occurrences: number;
41
- /**
42
- * When this witness first said it. Held SEPARATELY from `alert.observedAt`, because a later
43
- * repeat can replace `alert` (a provable one supersedes an unprovable one) and that must not
44
- * silently move the first sighting forward — an operator reading "first observed" wants to know
45
- * when this started, not when the strongest version of it arrived.
46
- */
47
- firstObservedAt: number;
48
- lastObservedAt: number;
49
- }
31
+ import { AgentRelayClient, type RelayAuthRefusal, type RelayWitnessAlert } from "./session-relay-client.js";
50
32
  import { RelayReceiptStore, type RelayReceipt } from "./relay-receipt-store.js";
51
33
  import { SessionSealLeafStore, type SealCarryLeaf } from "./session-seal-leaf-store.js";
34
+ import { SessionOwnChainStore } from "./session-own-chain-store.js";
52
35
  import type { SealUpgradeReadiness } from "./seal-upgrade.js";
53
36
  import type { SealFrontierLeaf } from "./seal-frontier-verify.js";
54
37
  import { type QuarantineFrameMeta } from "./quarantine-framing.js";
55
38
  import { type SecurityGatewayClient } from "@cello-protocol/gateway";
56
39
  /**
57
- * DOD-CAP-SELF-HEAL-1 how long an interrupted session keeps consuming a cap slot.
58
- *
59
- * ATTRIBUTION ALONE DID NOT FIX THIS, and the reason is worth keeping. Recording who ended a
60
- * session only works for sessions ended after the recording started: every row written before the
61
- * column existed is unlabelled, and an unlabelled row counts. So the operator's actual backlog —
62
- * five finished conversations that were blocking two of their own agents — was untouched by it.
63
- * Attribution can never clear history, and history is what fills a cap.
64
- *
65
- * Age can. An interrupted session nobody has touched for hours is debris, not a live obligation,
66
- * and that is true whether our restart or their disconnect produced it.
67
- *
68
- * D18 SURVIVES BECAUSE THE ATTACK IS A RATE. The disconnect-evasion peer has to drop and reopen
69
- * faster than this window to gain anything, so everything it churns is recent and everything it
70
- * churns still counts. What ages out is the thing that was never an attack: a conversation that
71
- * finished. An attacker who waits out the window to gain one slot per window is not evading the
72
- * bound, they are obeying a slower one — and the global anti-swarm cap still applies on top.
73
- *
74
- * Two hours: comfortably longer than any churn worth attacking with, comfortably shorter than
75
- * "yesterday's conversation still blocks me".
76
- */
77
- /**
78
- * DOD-M12B-RESERVATION-RETRY-1 — how many times to re-ask for a refused reservation before saying
79
- * the agent is undialable and stopping. Bounded because a reservation is scarce and a fleet that
80
- * retries forever is how a relay is exhausted. With the 5-minute base and doubling, five retries
81
- * span about 2.5 hours.
82
- */
83
- export declare const SR_RESERVATION_MAX_RETRIES = 5;
84
- /**
85
- * DOD-M15-RELAYSLOTS-1 — how long an agent skips a relay that refused it for a relay-side fault.
86
- *
87
- * Ten minutes: long enough that the agent is not re-asking a relay that cannot serve it every time
88
- * its receiver rebuilds, short enough that when someone fixes that relay the agent finds it again
89
- * without needing its own restart. The fault is on somebody else's machine and nobody tells us when
90
- * it is fixed, so this has to expire on its own.
91
- */
92
- export declare const RELAY_QUARANTINE_MS: number;
93
- export declare const CAP_INTERRUPTED_TTL_MS: number;
94
- /**
95
- * DOD-M12B-ABANDON-NOTIFY-1 — what happened when we tried to tell the counterparty we hung up.
96
- *
97
- * A REASON, not a boolean. The three causes are not interchangeable, and collapsing them made the
98
- * operator's guidance blame the network for a session this side had already torn down — which is
99
- * the most common case, since force-abandon is largely used on `interrupted` sessions and those
100
- * have no node left to send on.
101
- *
102
- * `told: true` means the bytes left this node. There is no acknowledgement, so it is not proof the
103
- * far side acted — a counterparty on an older client does not understand the frame and keeps
104
- * calling. The guidance says so rather than promising they will stop.
105
- */
106
- export interface AbandonNoticeResult {
107
- told: boolean;
108
- reason: "sent" | "no_local_node" | "send_failed";
109
- }
110
- /**
111
- * DOD-M12B-ACK-1 — why a session is impaired, and what became of the content that revealed it.
112
- *
113
- * `cause` separates "your own message did not reach them" (`direct_send`) from "our acknowledgement
114
- * to them did not go out" (`delivery_ack`) — on the second the operator sent nothing at all, so a
115
- * surface that talks about their last send is describing a message they never wrote.
116
- *
117
- * `retained` is what makes it safe to advise. A parked or durably-queued message must NOT be
118
- * resent — a resend takes a second canonical position, which is this milestone's founding defect.
119
- * A LOST one must be, and `cello_send` has already said so. `unknown` means do not claim either.
120
- */
121
- export interface SessionImpairment {
122
- /**
123
- * `content_key` added by `029c` review F7: a send that failed because this machine had no content
124
- * encryption key — never agreed, or gone between the preflight and the seal — is a LOCAL key
125
- * fault, not a transport one. Reported as `direct_send` it sent the operator to inspect a
126
- * connection that was working.
127
- */
128
- cause: "direct_send" | "delivery_ack" | "content_key";
129
- retained: "parked" | "durable" | "lost" | "unknown";
130
- }
131
- /** Anti-drip-feed: cumulative RECEIVED bytes per session at the UNKNOWN tier (= the grid's UNKNOWN
132
- * byte cap). Higher tiers get more (DEFAULT_TIER_BOUNDS); no tier is unbounded (INV-TIER-BOUND). */
133
- export declare const ABUSE_MAX_SESSION_RECEIVED_BYTES: number;
134
- /** Anti-drip-feed via many sessions: active sessions an UNKNOWN counterparty may hold open at once
135
- * (= the grid's UNKNOWN per-sender cap). */
136
- export declare const ABUSE_MAX_SESSIONS_PER_UNKNOWN_SENDER: number;
137
- /** Anti-swarm: total active sessions from ALL UNKNOWN-tier counterparties combined, per agent. A
138
- * scalar across the whole unknown pool — not per-tier — so it stays a standalone const. */
139
- export declare const ABUSE_MAX_UNKNOWN_SESSIONS_GLOBAL = 50;
140
- /**
141
- * M7 DOD-SPINE-6 / MSG-001-3b: the inputs a session node needs to connect to the relay
142
- * as the Structure-2 witness (relay endpoint from the FROST-signed assignment + the
143
- * agent's K_local identity + the 16-byte session id). Optional on node creation: when
144
- * absent (or connect fails), the session still works over the direct content path — the
145
- * relay just doesn't witness the leaf yet.
146
- */
147
- export interface RelayConnectParams {
148
- relayPeerId: string;
149
- relayAddrs: string[];
150
- keyProvider: KeyProvider;
151
- senderPubkey: Uint8Array;
152
- sessionIdBytes: Uint8Array;
153
- /**
154
- * FED-OPTIONB-SETUP-001 (Option B): the directory-signed relay assignment the client presents to its
155
- * chosen relay (replaces the directory→relay dial). Absent for direct-mode and on the restart/persisted
156
- * reconnect path (the relay already recorded the session at first establishment) — the client then just
157
- * reconnects without re-recording.
158
- */
159
- assignment?: RelayAssignmentCarry;
160
- }
161
- /**
162
- * DOD-PARK-DRAIN-1: why the parked-mailbox drain is being asked to run.
163
- *
164
- * `standing_receiver_ready` — a receiver was just installed, first time or rebuilt. The rebuild is
165
- * the case that matters: content parks precisely because the relay link died, and the watchdog
166
- * rebuild is that same event seen from the client side.
167
- * `periodic_backstop` — nothing happened; this is the slow sweep that keeps a missed trigger from
168
- * stranding content until someone restarts the daemon. Drains are deduped and delete-on-confirm,
169
- * so an extra one costs a pull.
170
- */
171
- export type ParkedDrainReason = "standing_receiver_ready" | "periodic_backstop"
172
- /**
173
- * DOD-M12B-SESSION-SEED-1 (case B): a session that was interrupted has just been revived, so the
174
- * content the counterparty parked while we had no node can finally be fetched. Reviving the node
175
- * without draining leaves the operator looking at a healthy session with no new mail.
176
- */
177
- | "session_revived"
178
- /**
179
- * DOD-M12B-LEAF-TRIGGERS-FETCH-1: the relay told us a specific message exists — we hold its hash
180
- * and its canonical sequence — and its plaintext never arrived on the direct path. Measured live
181
- * 2026-08-18: the leaf landed in ONE second and the bytes took 102, because nothing connected the
182
- * two facts.
183
- */
184
- | "witnessed_leaf_unresolved";
185
- /**
186
- * Adapter interface for session node creation. Allows test injection of a
187
- * failing factory (AC-007) without touching the real libp2p stack.
188
- * The adapter pattern is mandatory per outline.md constraints.
189
- */
190
- export interface ISessionNodeFactory {
191
- createNode(config: SessionNodeConfig): Promise<CelloNode>;
192
- }
193
- export interface SessionNodeConfig {
194
- sessionId: string;
195
- connectionGater?: SessionConnectionGater;
196
- /**
197
- * DOD-M15-RELAYONLY-1: this agent has asked never to be directly reachable, so the factory must
198
- * omit dcutr from the node's service set.
199
- *
200
- * ⚠️ NOT a duplicate of filtering the published addresses. Those two things stop DIFFERENT
201
- * disclosures: the filter controls what the DIRECTORY is told, and dcutr talks to the peer
202
- * directly. Its whole job is to upgrade a relayed connection into a direct one, and the inbound
203
- * side starts the upgrade — which is exactly a standing receiver. Leaving it on means the agent
204
- * routes over the relay precisely as asked and then hole-punches to a direct connection anyway,
205
- * with every test still green because the leak happens inside libp2p after the assertions.
206
- */
207
- relayOnly?: boolean;
208
- /**
209
- * CELLO-M7-TRANSPORT-001: role of the node, forwarded to createNode to tune the
210
- * libp2p service set (dcutr is included for 'session' dialers, omitted for the
211
- * 'standing_receiver'). AutoNAT is present for both.
212
- */
213
- nodeType?: "session" | "standing_receiver";
214
- /**
215
- * M7-SESSION-003 (AC-005): keepalive ping interval for the session node so a
216
- * counterparty that vanishes without a clean close is detected within a bounded
217
- * window. Factories should forward this to createNode({ keepAliveIntervalMs }).
218
- */
219
- keepAliveIntervalMs?: number;
220
- /**
221
- * DOD-NAT-REACHABILITY-1: circuit-relay listen addresses
222
- * (`<relay-multiaddr>/p2p/<relay-peer-id>/p2p-circuit`) the node should take
223
- * reservations on. Each entry makes libp2p reserve a slot with that relay and
224
- * advertise the relayed address via getMultiaddrs() — which is what makes a
225
- * NAT'd standing receiver dialable at all. A dead relay in this list degrades
226
- * (no reservation, WARN) — it never fails node creation.
227
- */
228
- circuitRelayListenAddrs?: string[];
229
- /**
230
- * DOD-M12B-SESSION-SEED-1: the 32-byte Ed25519 seed this node's transport identity is derived
231
- * from, so a node that is torn down can be rebuilt at the SAME peer id.
232
- *
233
- * Without it libp2p generates the key internally and the id is unrecoverable — which is why a
234
- * laptop-close session cannot come back today: a rebuilt node could dial the counterparty, but
235
- * the counterparty holds an id that no longer exists.
236
- *
237
- * PER SESSION, NEVER PER AGENT. Each standing receiver mints its own; at handoff the seed becomes
238
- * that session's and the replacement receiver mints a fresh one. That preserves the recorded
239
- * 2026-04-11 rationale for ephemeral ids (a passive observer must not be able to correlate one
240
- * agent's sessions across days) while making the id stable WITHIN the one session an observer can
241
- * already correlate by watching the connection.
242
- */
243
- transportPrivateKey?: Uint8Array;
244
- /**
245
- * DOD-M12B-SESSION-SEED-1 (case B review HIGH-1): bind a ROUTABLE interface, not loopback.
246
- *
247
- * Ordinary session nodes dial OUT and need no inbound reachability, so the factory gives them
248
- * `127.0.0.1`. But a session node that reached its role by PROMOTION inherited the standing
249
- * receiver's `0.0.0.0` bind and its circuit-relay reservation — which is every session node in
250
- * production. A REBUILT one does not inherit anything, so without this it comes back on loopback
251
- * with no relay address: the peer id is preserved and the counterparty still cannot reach us,
252
- * which is precisely the half of the promise revival exists to keep. Preserving the id only
253
- * matters because the circuit address `/p2p/<relay>/p2p-circuit/p2p/<sessionPeerId>` embeds it —
254
- * and that is the address that was not being taken.
255
- */
256
- inboundReachable?: boolean;
257
- }
258
- /** DAEMON-004: a piece of content received and verified, awaiting cello_receive. */
259
- interface ReceivedContentEntry {
260
- contentHex: string;
261
- senderPubkey: string;
262
- sequenceNumber: number;
263
- }
264
- type CreateSessionResult = {
265
- ok: true;
266
- peerId: string;
267
- addrs: string[];
268
- } | {
269
- ok: false;
270
- reason: string;
271
- guidance: string;
272
- };
273
- /**
274
- * DOD-M12B-REVIVAL-BOUND-1 — how long an interrupted session stays revivable before it is closed.
275
- *
276
- * 24 hours. The bound exists because of Andre's 2026-08-18 tenet — *"leave nothing open that is no
277
- * longer needed"* — and its value is set by the case it must not break: a laptop closed for the
278
- * night. A window shorter than a night's sleep would abandon exactly the sessions case A/B exist to
279
- * rescue, which is why this is not zero and not an hour.
280
- *
281
- * It is deliberately a plain constant and not a setting. A per-operator knob here is a knob that
282
- * turns the guarantee off, and the guarantee is the security property, not a preference.
283
- */
284
- export declare const REVIVAL_WINDOW_MS: number;
285
- /**
286
- * DOD-M12B-REVIVAL-BOUND-1 — how often the revival bound is applied.
287
- *
288
- * Hourly. The window is 24 hours, so the worst-case overshoot is ~4% of the bound, and the pass is
289
- * one DB walk with no network in it. Boot-only was the first build and it is not a bound at all: a
290
- * daemon left up for a week never applies it, and a long-lived daemon is the normal case.
291
- */
292
- export declare const REVIVAL_BOUND_SWEEP_MS: number;
293
- /** DOD-M12B-SESSION-SEED-1: per-relay deadline when a revived node asks for a circuit reservation.
294
- * Three seconds — a relay that has a slot answers well inside it, and one that does not never
295
- * answers at all (measured: 10,002ms and still waiting). */
296
- export declare const REVIVE_RESERVATION_TIMEOUT_MS = 3000;
297
- /** How many relays a revival will ask before settling for a plain node. Two: the worst case is then
298
- * ~6s to a usable session, against a measured "never" for the unbounded form. */
299
- export declare const REVIVE_RESERVATION_CANDIDATES = 2;
300
- /**
301
- * DOD-M12B-LEAF-TRIGGERS-FETCH-1 — how long the DIRECT path gets before we go and fetch content the
302
- * relay has already told us about.
303
- *
304
- * Two seconds. The witness leaf and the plaintext are separate deliveries, and on a healthy session
305
- * the direct content normally lands within milliseconds of the leaf — so fetching the instant a leaf
306
- * arrives would put a relay round trip on the hot path of every message in every session, which is a
307
- * self-inflicted load problem. Waiting forever is what cost 102 seconds. Two seconds is far above
308
- * the healthy direct latency and far below anything a person would notice.
309
- */
310
- export declare const LEAF_FETCH_GRACE_MS = 2000;
311
- /**
312
- * DOD-M15-REFUSALTERMINAL-1 — the refusal reasons no retry can ever get past.
313
- *
314
- * MEASURED LIVE 2026-09-04: one message aimed at a conversation the counterparty had already
315
- * closed, refused `session_committed` and re-fetched roughly twice a second for 62 hours across
316
- * several daemon restarts — 232,056 refusal events on that one session and a 484 MB `daemon.log`.
317
- *
318
- * **WHY `session_committed` QUALIFIES.** A committed session carries a signature over its contents.
319
- * Nothing can be appended to it by anyone — not the counterparty, not us — so there is no future
320
- * state in which this content is accepted. That is the bar, and it is the whole bar.
321
- *
322
- * **⚠️ DO NOT ADD A REASON WITHOUT MEETING IT.** A reason wrongly called terminal silently drops a
323
- * message that would have arrived on the next try, which is worse than the loop this set exists to
324
- * end. The tempting ones and why each fails:
325
- *
326
- * - `content_hash_mismatch` — the fetch is BY CONTENT HASH, and a later fetch may retrieve a
327
- * correct copy from a different relay. Retrying can succeed.
328
- * - `sender_unresolved` — the sender may become resolvable when a profile arrives or a directory
329
- * syncs. Retrying can succeed.
330
- * - `session_orphaned` — `024-ORPHANTRIAGE` owns that path and decides its disposition.
331
- * - `session_size_limit_exceeded` — the cap IS monotonic, but its bound is a setting, and an
332
- * operator who raises it must be able to un-stick the conversation.
333
- * - a transient screener block — transient is in its name.
334
- */
335
- /**
336
- * ⚠️ NOT the same concept as `session-terminal-refusal.ts` (`DOD-MP-SESSION-RETIRE-1`), which is the
337
- * RELAY terminally refusing one of OUR SENDS. This set is about INBOUND content this side will
338
- * never accept. Two "terminal refusal" ideas live in this daemon and they point in opposite
339
- * directions — review F9.
340
- */
341
- export declare const TERMINAL_REFUSAL_REASONS: ReadonlySet<string>;
342
- /**
343
- * DOD-M15-REFUSALTERMINAL-1 review F3 — how long a FAILED read of the terminal-refusal rows is
344
- * backed off for, per session.
345
- *
346
- * A minute: long enough that a database throwing on every witnessed leaf produces one ERROR rather
347
- * than one per message (the exact log growth this unit exists to end), short enough that a disk
348
- * which recovers is noticed within a message or two rather than at the next restart.
349
- */
350
- export declare const TERMINAL_REFUSAL_READ_RETRY_MS = 60000;
351
- /**
352
- * DOD-M15-REFUSALTERMINAL-1 review F7 — how many terminally-refused content hashes are remembered
353
- * per session.
354
- *
355
- * The counterparty chooses how many rows this table gets: one per distinct message aimed at a
356
- * closed conversation, written even after the byte cap has stopped retaining evidence. 512 matches
357
- * `MAX_REFUSED_PARKED_ENTRIES`, is far above any honest volume for a conversation that has ENDED,
358
- * and bounds the table at (sessions × 512) small rows.
359
- */
360
- export declare const MAX_TERMINAL_REFUSALS_PER_SESSION = 512;
361
- /**
362
- * DOD-M15-REFUSALTERMINAL-1 — ONE refusal, TWO counts, and the NAMES are the fix.
363
- *
364
- * The inbox reported `times: 58` for a refusal that had fired tens of thousands of times, and the
365
- * code was not wrong — the sentence describing it was. One number is "since you last dismissed this
366
- * conversation" and the other is "ever". Neither may be called `times`, because that is the word an
367
- * operator, and an agent deciding whether to escalate, reads as a lifetime figure.
368
- */
369
- export interface RefusalNotice {
370
- sessionId: string;
371
- reason: string;
372
- kind: RefusalKind;
373
- impact: string;
374
- guidance: string;
375
- /** Refusals of this reason on this session since the last `cello_dismiss` — which DELETES the
376
- * notice row, restarting this counter at 1. Zero dismissals and it equals `timesTotal`. */
377
- timesSinceDismissed: number;
378
- /**
379
- * Every refusal of this reason on this session, from the first one, untouched by dismissal.
380
- *
381
- * OMITTED, never guessed, when the notice is served from the in-memory fallback — that path
382
- * exists precisely because the database write failed, so no durable total was ever written, and
383
- * reporting the smaller number twice would put the original lie back with two names on it.
384
- *
385
- * MUTUALLY EXCLUSIVE with `timesTotalAtLeast`: a figure and a floor are different claims and
386
- * must not share a name.
387
- */
388
- timesTotal?: number;
389
- /**
390
- * A LOWER BOUND on the lifetime count, for a row SEEDED at upgrade from a notice that already
391
- * existed — review F1c.
392
- *
393
- * The seed is that notice's `count`, which is refusals since the last dismissal, so the true
394
- * figure is at least this and may be far more: on the machine this unit was written for, the
395
- * notice read 58 and the log held 232,056 refusal events. Reporting 58 as `timesTotal` would be
396
- * the original defect with the new name on it. "At least 58" is true; "58" is not.
397
- */
398
- timesTotalAtLeast?: number;
399
- repeat?: boolean;
400
- }
401
- /**
402
- * DOD-M15-SEALWIRE-1 bullet 5, SENT half — our own authorship proof for a message we sent.
403
- *
404
- * Deliberately the SAME shape as the received half's `verifiedAuthorship`, because the transcript
405
- * column pair is the same and a second shape would invite a second meaning. What differs is the
406
- * ATTRIBUTION the row records: a sent row is `self_authored` (we PRODUCED this signature), never
407
- * `verified_signature` (we CHECKED someone else's). Same bytes, different claim.
408
- */
409
- export interface SentAuthorship {
410
- senderPubkey: Uint8Array;
411
- senderSig: Uint8Array;
412
- }
413
- /**
414
- * ─── 033-ACKEMIT: the three things that can be wrong with an ACKNOWLEDGEMENT ─────────────────────
415
- *
416
- * All three are `unusable` — the message is refused and the session lives. None of them is an
417
- * identity fault: by the time any is returned the signature has verified, the signer IS this
418
- * session's counterparty, and the claim is about this content in this conversation. What is wrong is
419
- * what the claim says the sender had SEEN.
420
- *
421
- * They are three names and not one because the operator's next move differs for each, and because an
422
- * investigator who cannot tell "your counterparty is on an older build" from "your counterparty
423
- * acknowledged something you never sent" is looking at the wrong half of the problem.
424
- *
425
- * ⚠️ **NAME WHAT WAS OBSERVED, NEVER AN INFERRED CONCLUSION** (`DOD-M15-ERRSTRING-1`). Not one of
426
- * these says "peer is malicious" — a mismatch is equally what a genuine software fault on the other
427
- * side looks like, and an error that names a party the code did not check is this milestone's
428
- * founding defect.
429
- */
430
- /** A v1 claim: it carries no `last_seen_hash`, so it asserts a POSITION and no content at all. */
431
- declare const AUTHORSHIP_ACK_HASH_ABSENT = "ack_hash_absent";
432
- /** The hash names content this side does not hold at the position the claim names. */
433
- declare const AUTHORSHIP_ACK_HASH_MISMATCH = "ack_hash_mismatch";
434
- /** The hash names content this side has never held — not in the tree, and not held pending a gap. */
435
- declare const AUTHORSHIP_ACK_HASH_UNKNOWN = "ack_hash_unknown_content";
436
- /**
437
- * The set that routes an `unusable` reason to the acknowledgement wording rather than the generic
438
- * one. A SET, not a string prefix test: a name-shaped check would silently adopt any future reason
439
- * someone happens to call `ack_*`, and give it a sentence written for these four.
440
- */
441
- export type AckHashReason = typeof AUTHORSHIP_ACK_HASH_ABSENT | typeof AUTHORSHIP_ACK_HASH_MISMATCH | typeof AUTHORSHIP_ACK_HASH_UNKNOWN;
442
- /**
443
- * One row of a session's durable transcript as a reader sees it.
444
- *
445
- * DOD-M15-REFUSEDEVIDENCE-1: `'quarantined'` is a message that was received and REFUSED. Its `text`
446
- * is the withholding statement, never the payload — the storage is complete and only the READ is
447
- * redacted. `withheld` is present exactly on those rows so a caller can style or skip them without
448
- * string-matching the statement.
40
+ * One row in an agent's witness-alert list DOD-M15-CORROBORATE-1 review F1. Deduped on
41
+ * `(witness relay, session)`, so a repeated observation raises `occurrences` rather than taking
42
+ * another slot in a bounded list.
449
43
  */
450
- export interface TranscriptEntry {
451
- sequence: number;
452
- direction: "sent" | "received" | "quarantined";
453
- text: string;
454
- createdAt: number;
455
- refusalReason?: string;
456
- withheld?: true;
457
- /** Named to end in `guidance` on purpose: that suffix is what the vocabulary layer rewrites, so a
458
- * CLI reader is told `cello quarantined` and an MCP reader `cello_quarantined`. */
459
- withheld_guidance?: string;
460
- }
461
- /** A retained refused message, read back whole. The payload is handed out FRAMED, never raw. */
462
- export interface QuarantinedRecord {
463
- sequence: number;
464
- reason: string;
465
- content: Uint8Array;
466
- senderPubkeyHex: string | null;
467
- senderSig: Uint8Array | null;
468
- attribution: string;
469
- createdAt: number;
470
- }
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";
45
+ 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";
471
46
  export declare class SessionNodeManager {
472
47
  #private;
473
48
  setDetachedRelayClientBuilder(fn: (agentName: string, relayPeerId: string, relayAddrs: string[], stores: {
474
49
  receiptStore?: RelayReceiptStore;
475
50
  sealLeafStore?: SessionSealLeafStore;
51
+ ownChainStore?: SessionOwnChainStore;
476
52
  onlineToken: () => Uint8Array | undefined;
477
53
  }) => AgentRelayClient | undefined): void;
478
54
  /**
479
- * Record what one relay says it saw on one of this agent's sessions, for the operator to read.
55
+ * Why this agent's standing receiver could not hold a reservation, in words the person running it
56
+ * can act on — or null when the last attempt succeeded or none has been made.
480
57
  *
481
- * ⚠️ **IT DOES NOT FREEZE THE SESSION, AND THAT IS THE DESIGN.** A client freezing on its OWN
482
- * verification is safe: it limits only what that client trusts. Freezing on a REMOTE party's
483
- * say-so hands any single relay the power to end any conversation it carries, and to write an
484
- * accusatory record about a counterparty who did nothing. The identity freeze stays where it is —
485
- * on this daemon's own check of an inbound frame — and this surfaces a second, independent
486
- * observation next to it. One witness reports; it does not rule.
58
+ * This is the surface DoD clause 7 is about: the assertion that matters is what the CLIENT can
59
+ * show someone, not what the relay wrote in its own log.
487
60
  */
61
+ getStandingReceiverRefusal(agentName: string): (RelayAuthRefusal & {
62
+ relayPeerId: string;
63
+ }) | null;
64
+ /** ─── DELEGATORS — the standing-receiver API other files call ───────────────────────── */
65
+ standingReceiverAbsenceReason(agentName: string): "daemon_shutting_down" | "standing_receiver_creating" | "agent_offline" | "no_standing_receiver";
66
+ getStandingReceiverInfo(agentName: string): {
67
+ peerId: string;
68
+ addrs: string[];
69
+ } | null;
70
+ getStandingReceiverReady(agentName?: string): boolean;
71
+ getStandingReceiverNode(agentName?: string): CelloNode | null;
72
+ getStandingReceiverReachability(agentName: string): "reserved" | "retrying" | "unreachable" | "absent";
73
+ getStandingReceiverAutoNat(): IAutoNatService | null;
74
+ getStandingReceiverAllowedPeer(agentName: string): string | null;
75
+ admitOfferedDialer(agentName: string, initiatorSessionPeerId: string, sessionIdHex: string): "narrowed" | "no_receiver" | "no_peer_named";
76
+ getOfferedDialer(agentName: string, sessionIdHex: string): string | null;
77
+ clearOfferedDialer(agentName: string, sessionIdHex: string): void;
78
+ revokeOfferedDialer(agentName: string, sessionIdHex: string, offeredPeerId: string | null): void;
79
+ /** ─── DELEGATORS — the leaf-record API other files call ─────────────────────────────── */
80
+ recordSessionGenesis(agentName: string, sessionId: string, participantA: Uint8Array, participantB: Uint8Array, sessionTimestamp: number): void;
81
+ setSessionGenesisForTest(agentName: string, sessionId: string, genesis: Uint8Array): void;
82
+ recordCertifiedLeafSet(agentName: string, sessionId: string, signedLeaves: readonly SealFrontierLeaf[], sealedRootHex: string, correlationId?: string): boolean;
83
+ noteCertifiedLeafSetUnavailable(agentName: string, sessionId: string, state: "not_carried_absent_party" | "not_carried_present_party", detail: string): void;
84
+ getCertifiedLeafSet(agentName: string, sessionId: string): string[] | null;
85
+ getCertifiedLeafSetState(agentName: string, sessionId: string): {
86
+ state: string;
87
+ detail: string | null;
88
+ } | null;
89
+ /** ─── DELEGATORS — the held-content seams other files call ──────────────────────────── */
90
+ holdOwnLeafForTest(agentName: string, sessionId: string, canonicalSeq: number, contentHashHex: string): void;
91
+ /** ─── DELEGATORS — the witness-alert API other files call ────────────────────────────── */
488
92
  recordRelayWitnessAlert(agentName: string, alert: RelayWitnessAlert): void;
489
- /** The witness alerts an agent has been told about, oldest first, one row per witness+session. */
490
93
  getWitnessAlerts(agentName: string): ReadonlyArray<WitnessAlertNotice>;
491
- /** Whether this agent's alert list hit its cap, so the inbox can say the list is incomplete. */
492
94
  witnessAlertsTruncated(agentName: string): boolean;
95
+ /** ─── DELEGATORS — the liveness API other files call, unchanged by the split ───────────── */
96
+ getSessionLiveness(agentName: string, sessionId: string): "alive" | "impaired" | "gone" | "unknown";
97
+ getSessionImpairment(agentName: string, sessionId: string): SessionImpairment | null;
98
+ markSessionLivenessForTest(agentName: string, sessionId: string, state: "alive" | "impaired" | "gone"): void;
99
+ /** ─── DELEGATORS — the test seams other files call, unchanged by the split ─────────────── */
100
+ mintSessionEphemeralForTest(agentName: string, sessionId: string): void;
101
+ sessionEphemeralPublicForTest(agentName: string, sessionId: string): Uint8Array | null;
102
+ setSessionEphemeralForTest(agentName: string, sessionId: string, ephemeral: SessionEphemeral): void;
103
+ setSessionContentKeyForTest(agentName: string, sessionId: string, key: Uint8Array): void;
104
+ forgetSessionContentKeyForTest(agentName: string, sessionId: string): void;
105
+ signOwnEphemeralForTest(agentName: string, sessionId: string): Promise<{
106
+ ephemeralPublic: Uint8Array;
107
+ signature: Uint8Array;
108
+ } | null>;
109
+ handleEphemeralFrameForTest(agentName: string, sessionId: string, frame: {
110
+ ephemeralPublic?: Uint8Array;
111
+ signature?: Uint8Array;
112
+ }, correlationId?: string): Promise<void>;
113
+ /** ─── DELEGATORS — the refusal-notice API other files call, unchanged by the split ────────── */
114
+ noteContentRefusal(agentName: string, sessionId: string, reason: string, detail: {
115
+ kind: RefusalKind;
116
+ impact: string;
117
+ guidance: string;
118
+ }): void;
119
+ takeContentRefusals(agentName: string, sessionId: string, consumerId: string): Array<Omit<RefusalNotice, "sessionId">>;
120
+ dismissContentRefusals(agentName: string, sessionId: string): number;
121
+ takeAgentContentRefusals(agentName: string, consumerId: string): {
122
+ notices: RefusalNotice[];
123
+ truncated: boolean;
124
+ };
493
125
  /**
494
- * Review F7: a relay sent a witness alert this build could not read or could not verify.
126
+ * ─── DELEGATORS the query API other files call, unchanged by the split ────────────────────
127
+ */
128
+ listExpiredUnrevivableSessions(nowMs: number, windowMs: number): Array<{
129
+ agentName: string;
130
+ sessionId: string;
131
+ cause: string | null;
132
+ }>;
133
+ listRestartOrphanedSessions(): Array<{
134
+ agentName: string;
135
+ sessionId: string;
136
+ messageCount: number;
137
+ status: "interrupted" | "seal_interrupted_pending";
138
+ }>;
139
+ readQuarantined(agentName: string, sessionId: string, sequence?: number): QuarantinedRecord[];
140
+ getPinnedCounterpartyPrimary(agentName: string, counterpartyPubkeyHex: string): string | null;
141
+ recordSealedAnnex(agentName: string, sessionId: string, contentHashHex: string, content: Uint8Array, senderPubkeyHex: string | null): boolean;
142
+ findNextReceivedAfter(agentName: string, sessionId: string, afterSeq: number): {
143
+ sequence: number;
144
+ text: string;
145
+ } | null;
146
+ getSealInterruptedArtifacts(agentName: string, sessionId: string): {
147
+ role: string;
148
+ ownLeaf: unknown;
149
+ counterpartyLeaf: unknown;
150
+ merkleRoot: string;
151
+ nonce: string;
152
+ } | null;
153
+ getSessionsByStatus(status: "active" | "sealed" | "interrupted"): SessionRecord[];
154
+ recordRefusedSession(agentName: string, sessionId: string, reason: string): void;
155
+ getAgentRelayEndpoints(agentName: string): Array<{
156
+ relayPeerId: string;
157
+ relayAddrs: string[];
158
+ }>;
159
+ countActiveSessionsFromUnknownSenders(agentName: string): number;
160
+ getSealCertificate(agentName: string, sessionId: string): {
161
+ sealed_root: string;
162
+ legibility: unknown;
163
+ } | null;
164
+ setSessionName(agentName: string, sessionId: string, sessionName: string | null): boolean;
165
+ getPersistedRelayEndpoint(agentName: string, sessionId: string): {
166
+ relayPeerId: string;
167
+ relayAddrs: string[];
168
+ } | null;
169
+ markRestartSealGaveUp(agentName: string, sessionId: string, reason: string): void;
170
+ dismissSession(agentName: string, sessionId: string): {
171
+ ok: true;
172
+ } | {
173
+ ok: false;
174
+ reason: string;
175
+ };
176
+ agentNameForId(agentId: string): string | null;
177
+ getRenameNotices(agentName: string): Array<{
178
+ pubkey: string;
179
+ offered_name: string;
180
+ noticed_at: number;
181
+ moniker: string | null;
182
+ }>;
183
+ sessionsConsumingCap(agentName: string, counterpartyPubkey: string, limit?: number): string[];
184
+ advanceLastDeliveredSeq(agentName: string, sessionId: string, seq: number): void;
185
+ countActiveSessionsForCounterparty(agentName: string, counterpartyPubkey: string): number;
186
+ getSessionRecord(agentName: string, sessionId: string): SessionRecord | null;
187
+ hasDatabase(): boolean;
188
+ readSealedAnnex(agentName: string, sessionId?: string): Array<{
189
+ session_id: string;
190
+ content_hash: string;
191
+ sender_pubkey: string | null;
192
+ text: string;
193
+ arrived_at: number;
194
+ }>;
195
+ counterpartyAbandonedAt(agentName: string, sessionId: string): number | null;
196
+ getSealedRootHex(agentName: string, sessionId: string): string | null;
197
+ getLastDeliveredSeq(agentName: string, sessionId: string): number;
198
+ markSessionsInterruptedByLocalShutdownForTest(): void;
199
+ wasSessionRefused(agentName: string, sessionId: string): boolean;
200
+ countReceivedMessages(agentName: string, sessionId: string): number;
201
+ markInterruptedByCounterpartyForTest(agentName: string, sessionId: string): void;
202
+ recordSealCertificate(agentName: string, sessionId: string, sealedRootHex: string, legibilityJson: string): void;
203
+ recordCounterpartyPrimary(agentName: string, sessionId: string, primaryPubkeyHex: string): void;
204
+ /**
205
+ * ─── DELEGATORS — the salt API other files call, unchanged by the split ─────────────────────
495
206
  *
496
- * Recorded so a version skew that silently kills the witness layer is visible to the operator
497
- * instead of living only in a log file. Carries no session and no party by construction.
207
+ * `daemon.ts` and `session-content-handlers.ts` still say `manager.contentHashForSession(...)`.
208
+ * Keeping the surface identical is what lets the suite stand as evidence that this was a
209
+ * restructure and not a rewrite.
498
210
  */
211
+ setSaltContributionForTest(agentName: string, sessionId: string, contribution: Uint8Array): void;
212
+ forgetSaltContributionForTest(agentName: string, sessionId: string): void;
213
+ contentHashForSession(agentName: string, sessionId: string, content: Uint8Array): Promise<{
214
+ hash: Uint8Array;
215
+ alg: ContentHashAlg;
216
+ }>;
217
+ abandonUnsaltedHash(agentName: string, sessionId: string): void;
218
+ isContentSaltActive(agentName: string, sessionId: string): boolean;
219
+ markSaltPendingForTest(agentName: string, sessionId: string): void;
220
+ saltForHashingForTest(agentName: string, sessionId: string): Promise<{
221
+ salt: Uint8Array | null;
222
+ reason?: string;
223
+ }>;
224
+ /**
225
+ * ─── DELEGATORS — the mailbox API other files call, unchanged by the split ───────────────────
226
+ *
227
+ * `content-park.ts` and `daemon.ts` still say `manager.recoverParkedEntry(...)` and
228
+ * `manager.setContentParkHook(...)`. Keeping the surface identical is what lets the full suite
229
+ * stand as evidence that this was a move: a caller that had to change would mean the contract
230
+ * moved with it.
231
+ */
232
+ injectParkFault(count: number, cause?: string): number;
233
+ getParkFaultRemaining(): number;
234
+ setContentParkHook(fn: (args: {
235
+ agentName: string;
236
+ sessionId: string;
237
+ recipientPubkeyHex: string;
238
+ relayPeerId: string;
239
+ relayAddrs: readonly string[];
240
+ contentHashHex: string;
241
+ content: Uint8Array;
242
+ structure1Cbor?: Uint8Array;
243
+ structure2Cbor?: Uint8Array;
244
+ structure1Signature?: Uint8Array;
245
+ leafKind?: number;
246
+ contentHashAlg: string | undefined;
247
+ }) => Promise<{
248
+ ok: true;
249
+ } | {
250
+ ok: false;
251
+ reason: string;
252
+ cause?: string;
253
+ retryAfterMs?: number;
254
+ }>): void;
255
+ setParkedDrainHook(fn: (agentName: string, reason: ParkedDrainReason) => void): void;
256
+ recoverOwnSealCtrlLeafForTest(agentName: string, sessionId: string): {
257
+ reportedRootHex: string;
258
+ sequenceNumber: number;
259
+ } | "none" | "unknown";
260
+ recoverParkedEntry(agentName: string, sessionId: string, recipientPubkey: Uint8Array, unsealed: Uint8Array, contentHash: Uint8Array, correlationId?: string): Promise<{
261
+ ok: true;
262
+ leafIndex: number;
263
+ sequenceNumber: number;
264
+ held?: boolean;
265
+ appendedCount?: number;
266
+ screenedOut?: boolean;
267
+ } | {
268
+ ok: false;
269
+ reason: string;
270
+ }>;
271
+ /**
272
+ * ─── DELEGATORS — the class API is unchanged by the split, deliberately ──────────────────────
273
+ *
274
+ * Every method below moved into `session-records.ts` with its implementation and its comments.
275
+ * These one-liners exist so that no CALLER had to change: `contact-handlers.ts`, the IPC surface
276
+ * and the tests all still say `manager.getTier(...)`. That is what lets the full suite stand as
277
+ * the evidence that this was a move and not a rewrite — a test that had to change would have
278
+ * meant behaviour moved (Rule B).
279
+ *
280
+ * They are the price of the split and they are the cheap half: one line each, no logic, and the
281
+ * prose that explains each rule lives beside the code that enforces it.
282
+ */
283
+ isContact(agentName: string, pubkey: string): boolean;
284
+ getTier(agentName: string, pubkey: string): number;
285
+ resolveTierBound(agentName: string, tier: number, field: "max_sessions" | "max_bytes"): number;
286
+ addContact(agentName: string, pubkey: string, moniker?: string | null, provenance?: string | null, tier?: number): void;
287
+ setContactMoniker(agentName: string, pubkey: string, moniker: string | null): boolean;
288
+ setContactSignalPref(agentName: string, pubkey: string, signalHash: string, present: boolean | null): void;
289
+ getContactSignalPrefs(agentName: string, pubkey: string): Map<string, boolean>;
290
+ setContactAwayMessage(agentName: string, pubkey: string, message: string | null): boolean;
291
+ setContactTier(agentName: string, pubkey: string, tier: number): boolean;
292
+ recordOfferedMoniker(agentName: string, pubkey: string, offered: string): void;
293
+ removeContact(agentName: string, pubkey: string): boolean;
294
+ getContactMoniker(agentName: string, pubkey: string): string | null;
295
+ listContacts(agentName: string): Array<{
296
+ pubkey: string;
297
+ added_at: number;
298
+ moniker: string | null;
299
+ tier: number | null;
300
+ provenance: string | null;
301
+ sealed_count: number;
302
+ last_spoke: number | null;
303
+ }>;
304
+ clearRenameNotice(agentName: string, pubkey: string): void;
305
+ clearPinnedCounterpartyPrimary(agentName: string, counterpartyPubkeyHex: string): number;
306
+ getTelegramSettings(): {
307
+ botToken: string;
308
+ allowlistedChatId: string;
309
+ } | null;
310
+ setTelegramSettings(botToken: string, allowlistedChatId: string): void;
311
+ getSetting(agentName: string, key: string): string | null;
312
+ deleteSetting(agentName: string, key: string): boolean;
313
+ setSetting(agentName: string, key: string, value: string): void;
314
+ getAllSettings(agentName: string): Array<{
315
+ key: string;
316
+ value: string;
317
+ }>;
499
318
  recordRelayWitnessUnreadable(agentName: string, relayPeerId: string, why: string): void;
500
- /** Relays whose witness alerts this build could not read, for the agent's inbox. */
501
319
  getWitnessUnreadable(agentName: string): ReadonlyArray<{
502
320
  relayPeerId: string;
503
321
  why: string;
504
322
  count: number;
505
323
  }>;
506
- /**
507
- * Why this agent's standing receiver could not hold a reservation, in words the person running it
508
- * can act on — or null when the last attempt succeeded or none has been made.
509
- *
510
- * This is the surface DoD clause 7 is about: the assertion that matters is what the CLIENT can
511
- * show someone, not what the relay wrote in its own log.
512
- */
513
- getStandingReceiverRefusal(agentName: string): (RelayAuthRefusal & {
514
- relayPeerId: string;
515
- }) | null;
516
- /**
517
- * Record that this session's tree and the relay's counter have provably parted.
518
- *
519
- * Idempotent, and deliberately does NOT touch `updated_at`: that column drives the inbox's
520
- * last-spoke ordering, and divergence is not activity.
521
- */
324
+ recordTranscriptMessage(agentName: string, sessionId: string, sequence: number, direction: "sent" | "received" | "quarantined", plaintext: Uint8Array, correlationId?: string, authorship?: {
325
+ senderPubkey: Uint8Array;
326
+ senderSig: Uint8Array;
327
+ }, quarantineReason?: string, senderPubkeyHexOverride?: string | null): boolean;
328
+ readTranscript(agentName: string, sessionId: string): {
329
+ messages: TranscriptEntry[];
330
+ undecryptable: number;
331
+ };
332
+ getUnreadSummary(agentName: string): Array<{
333
+ session_id: string;
334
+ unread_count: number;
335
+ last_seq: number;
336
+ }>;
337
+ getEndedUnread(agentName: string): Array<{
338
+ session_id: string;
339
+ unread_count: number;
340
+ last_seq: number;
341
+ status: string;
342
+ }>;
343
+ getUnreadReceivedCount(agentName: string, sessionId: string): number;
522
344
  markSessionDiverged(agentName: string, sessionId: string): void;
523
- /** Whether this session has provably parted from the relay's ordering. */
524
345
  isSessionDiverged(agentName: string, sessionId: string): boolean;
525
- /** Arm the park-deposit fault. Returns the count now armed. */
526
- injectParkFault(count: number, cause?: string): number;
527
346
  /** Arm the direct-send fault — makes the next N sends take the dial-failure path. */
528
347
  injectSendFault(count: number): number;
529
348
  /** DOD-M12B-ACK-1: arm the delivery-ACK write fault — the sibling of injectSendFault for the
@@ -534,9 +353,6 @@ export declare class SessionNodeManager {
534
353
  /** DOD-M12B-REDIAL-1: arm the connection-loss fault — the next N direct sends find no open
535
354
  * connection, exactly as they do after any blip. See #connectionLossRemaining. */
536
355
  injectConnectionLoss(count: number): number;
537
- getSendFaultRemaining(): number;
538
- /** Remaining armed park faults — so a test can assert the fault was actually consumed. */
539
- getParkFaultRemaining(): number;
540
356
  constructor(opts: {
541
357
  factory: ISessionNodeFactory;
542
358
  logger: Logger;
@@ -607,60 +423,6 @@ export declare class SessionNodeManager {
607
423
  * composition root (daemon.ts) after the RetryQueue exists.
608
424
  */
609
425
  setSessionTerminalHook(hook: (sessionId: string, terminalStatus: "sealed" | "abandoned") => void): void;
610
- /**
611
- * MSG-001-3b (2b): inject the live content-park deposit (seal + ContentParkClient.deposit).
612
- * Injected by the composition root (daemon.ts). When absent, a not-confirmed send still records
613
- * the durable awaiting entry (crash backstop) but does not deposit live.
614
- * DOD-LEAVEMSG-1 (cello-unit-reviewer HIGH fix): the hook returns a TYPED result — `{ok:true}` or
615
- * `{ok:false, reason}` — mirroring RetryQueue's ParkFn contract. It must NEVER resolve `{ok:true}`
616
- * merely because it didn't throw: the production hook's own failure branches (standing receiver
617
- * unavailable, relay explicitly rejects the deposit) log-and-return without throwing, and a
618
- * throw-only contract would silently report those as success — the exact "system lies about its
619
- * own health" bug the reviewer caught (a park that never happened reported to the operator as
620
- * "dispatched to relay," with the durable retry_queue backstop skipped because sendContent's own
621
- * caller only enqueues on an honest {ok:false}).
622
- */
623
- setContentParkHook(fn: (args: {
624
- agentName: string;
625
- sessionId: string;
626
- recipientPubkeyHex: string;
627
- relayPeerId: string;
628
- relayAddrs: readonly string[];
629
- contentHashHex: string;
630
- content: Uint8Array;
631
- structure1Cbor?: Uint8Array;
632
- structure2Cbor?: Uint8Array;
633
- structure1Signature?: Uint8Array;
634
- leafKind?: number;
635
- contentHashAlg: string | undefined;
636
- }) => Promise<{
637
- ok: true;
638
- } | {
639
- ok: false;
640
- reason: string;
641
- cause?: string;
642
- retryAfterMs?: number;
643
- }>): void;
644
- /**
645
- * DOD-PARK-DRAIN-1: inject the parked-mailbox drain (daemon.ts → contentPark.autoRecoverForAgent).
646
- *
647
- * The manager owns the two events that mean "content may be waiting for this agent on a relay":
648
- * a standing receiver was (re)built, and the slow backstop sweep. It does not own the drain
649
- * itself — that needs the agent's key provider and the inbound ingest funnel. So it calls out.
650
- *
651
- * Injected by the composition root, not passed to the constructor: the manager is built long
652
- * before the content park exists (content-park.ts documents why that ordering is load-bearing).
653
- */
654
- setParkedDrainHook(fn: (agentName: string, reason: ParkedDrainReason) => void): void;
655
- /**
656
- * DOD-PARK-DRAIN-1 (review F6): why there is no standing-receiver node to dial from — named
657
- * precisely, because `standing_receiver_unavailable` is the exit-point label that stood in for
658
- * four different causes and misnamed this very incident 102 times.
659
- *
660
- * Only meaningful once `getStandingReceiverNode()` has returned null, which means NO agent on
661
- * this daemon has a ready receiver — the dial node is not agent-scoped.
662
- */
663
- standingReceiverAbsenceReason(agentName: string): "daemon_shutting_down" | "standing_receiver_creating" | "agent_offline" | "no_standing_receiver";
664
426
  initialize(): Promise<void>;
665
427
  /**
666
428
  * Get the underlying DatabaseSync handle.
@@ -673,16 +435,6 @@ export declare class SessionNodeManager {
673
435
  * encrypted DB file.
674
436
  */
675
437
  getDb(): DaemonDatabase;
676
- /**
677
- * DOD-M15-RELAYONLY-1: is the settings store readable RIGHT NOW?
678
- *
679
- * ⚠️ Exists because `getSetting` cannot answer it. That method returns `null` for BOTH "the key is
680
- * unset" and "there is no database", and a security setting must tell those apart: unset-means-off
681
- * is correct, db-gone-means-off publishes the operator's real address during the shutdown window.
682
- * `getDb()` cannot stand in either — it THROWS when there is no database, which on a catch-less
683
- * ceremony path is worse than the wrong answer.
684
- */
685
- hasDatabase(): boolean;
686
438
  /**
687
439
  * RELAYSIG-1: the durably-stored, signature-verified relay ordering-record receipts for an agent
688
440
  * (optionally a single session). Empty when no receipts have been recorded yet. Read-only.
@@ -753,137 +505,6 @@ export declare class SessionNodeManager {
753
505
  */
754
506
  countersignedThroughSeqFromCarry(agentPubkeyHex: string, sessionIdHex: string): number | null;
755
507
  getSealCarry(agentPubkeyHex: string, sessionIdHex: string): SealCarryLeaf[];
756
- /**
757
- * DOD-LOG-1 / PERSIST-002 (AC-010): append one readable message to the durable transcript, keyed
758
- * by the canonical leaf `sequence` so it joins to the committed hash chain. The blob is stored as
759
- * plaintext bytes: the whole DB is SQLCipher-encrypted at rest, so there is no per-column cipher.
760
- * Idempotent on replay (INSERT OR IGNORE). Never throws into the caller's content path — but it
761
- * REPORTS: returns false when the row did not land, so a caller for whom the row is a delivery
762
- * precondition can fail instead of proceeding (review F2). Before Tier 1 the return value would
763
- * have been pointless, because `cello_receive` served content from the in-memory buffer and the
764
- * lost row only cost the unread count. Delivery reads the transcript now, so a swallowed received
765
- * row is TOTAL content loss and the caller has to know.
766
- */
767
- recordTranscriptMessage(agentName: string, sessionId: string, sequence: number,
768
- /**
769
- * DOD-M15-REFUSEDEVIDENCE-1 adds `'quarantined'` — received and REFUSED, kept as evidence and
770
- * never delivered. It goes through THIS writer rather than a second one so that the attribution
771
- * rule, the blob handling and the write-failure logging cannot drift between a delivered message
772
- * and a refused one. One store, one writer.
773
- */
774
- direction: "sent" | "received" | "quarantined", plaintext: Uint8Array, correlationId?: string,
775
- /**
776
- * DOD-M15-SEALWIRE-1 bullet 5: the VERIFIED authorship proof, when there is one.
777
- *
778
- * Optional because there legitimately is not always one — the ordering decode can fail SOFT and
779
- * the message is still ingested via hash-dedup. Optional is NOT the same as unremarked: absence
780
- * is written into the row as `attribution = 'local_session_state'`, so a reader can tell a row
781
- * whose author was proven from one whose author was assumed. That distinction is the bullet.
782
- */
783
- authorship?: {
784
- senderPubkey: Uint8Array;
785
- senderSig: Uint8Array;
786
- },
787
- /** Required on a `'quarantined'` row and meaningless on any other: WHY it was refused. */
788
- quarantineReason?: string,
789
- /**
790
- * DOD-M15-REFUSEDEVIDENCE-1: the sender's key when there is one but no verified signature to go
791
- * with it. A refused frame often has an identified sender and an unusable proof — a tampered
792
- * message is still FROM someone — and dropping the key because the signature failed would throw
793
- * away the half of the attribution that survived.
794
- */
795
- senderPubkeyHexOverride?: string | null): boolean;
796
- /**
797
- * DOD-LOG-1: read a session's durable transcript back (after a restart), decrypted and ordered by
798
- * canonical sequence then direction. A blob that fails to decrypt (tamper/wrong key) is skipped
799
- * with a loud log rather than crashing the read.
800
- */
801
- readTranscript(agentName: string, sessionId: string): {
802
- messages: TranscriptEntry[];
803
- undecryptable: number;
804
- };
805
- /**
806
- * DOD-COATTEND-1 (review F5) — the single next RECEIVED message after `afterSeq`, or null.
807
- *
808
- * The delivery path asks this question inside a 20 ms poll, so it is asked ~47 times a second per
809
- * blocked connection — ~1,400 times over a default 30 s receive. Answering it with
810
- * `readTranscript()` meant, every single time: SELECT every row of the session with no predicate
811
- * and no limit, `TextDecoder().decode()` every blob in it, build the array, then `.find()` one
812
- * row and discard the rest. On a 200-message session with three co-attending connections blocking
813
- * — which is the M8D use case, not a worst case — that is tens of thousands of blob decodes per
814
- * second on the daemon's single synchronous SQLCipher handle, contending with the write path.
815
- *
816
- * The predicate belongs in SQL. This is O(1) on the existing (agent_id, session_id, sequence)
817
- * key and decodes exactly the one blob it returns.
818
- */
819
- findNextReceivedAfter(agentName: string, sessionId: string, afterSeq: number): {
820
- sequence: number;
821
- text: string;
822
- } | null;
823
- /** The highest RECEIVED transcript sequence delivered to the operator for (agent, session).
824
- * -1 when nothing has been delivered yet (so a seq-0 message reads as unread). */
825
- getLastDeliveredSeq(agentName: string, sessionId: string): number;
826
- /** Advance the read watermark (delivery marks read). MONOTONIC — never lowers, so a replayed or
827
- * out-of-order cello_receive cannot un-read already-read messages. */
828
- advanceLastDeliveredSeq(agentName: string, sessionId: string, seq: number): void;
829
- /** INBOX-1 (N2): per-session unread summary for an agent — sessions that have RECEIVED transcript
830
- * messages beyond the read watermark, excluding terminal sessions (sealed, abandoned,
831
- * seal_interrupted_pending) which belong in getEndedUnread instead.
832
- * Sessions with no sessions row are treated as non-terminal (LEFT JOIN).
833
- * Content-free (counts + ids + last seq, never message text); a COUNT/MAX query, no decrypt. */
834
- getUnreadSummary(agentName: string): Array<{
835
- session_id: string;
836
- unread_count: number;
837
- last_seq: number;
838
- }>;
839
- /** DOD-SEALED-INBOX-1: terminal sessions with unread received messages that have not been
840
- * dismissed. These are answering-machine style messages left in an ENDED session — the operator
841
- * can read them via cello_transcript but cannot advance the watermark via cello_receive.
842
- * Only returned when read_at IS NULL (not yet dismissed).
843
- *
844
- * DOD-SEALED-INBOX-2: named `getEndedUnread`, not `getSealedUnread`, and it SELECTS `s.status`.
845
- * All four #TERMINAL_STATUSES belong here — that part was always right — but only `sealed` is
846
- * NOTARIZED. The old name and the caller's hardcoded `session_state: "sealed"` asserted a
847
- * cryptographic receipt for `abandoned`, `interrupted` and `seal_interrupted_pending` sessions,
848
- * which have none. Callers must render the row's own status; there is nothing to infer from
849
- * membership in this list beyond "it ended". */
850
- getEndedUnread(agentName: string): Array<{
851
- session_id: string;
852
- unread_count: number;
853
- last_seq: number;
854
- status: string;
855
- }>;
856
- /** DOD-SEALED-INBOX-1: mark a terminal session as dismissed — sets read_at to now.
857
- * Only valid for terminal sessions; active/interrupted sessions return session_not_terminal. */
858
- dismissSession(agentName: string, sessionId: string): {
859
- ok: true;
860
- } | {
861
- ok: false;
862
- reason: string;
863
- };
864
- /**
865
- * DOD-CURSOR-DURABLE-1: how many RECEIVED messages in THIS session the agent has not read —
866
- * the durable half of the read-before-write gate. Same predicate as getUnreadSummary (shared
867
- * constant above), scoped to one session.
868
- *
869
- * This is DURABLE and PER-AGENT, where the send gate's other authority (the connection cursor) is
870
- * in-memory and per-connection. It is what lets a stateless client — the `cello` CLI, one process
871
- * per command — prove it has read the counterparty, which a dead socket's cursor never can.
872
- *
873
- * FAILS CLOSED: an uninitialized DB returns a positive count (treated as "unread"), never 0. A 0
874
- * here unblocks a send; guessing 0 from a broken DB would silently defeat the gate.
875
- */
876
- getUnreadReceivedCount(agentName: string, sessionId: string): number;
877
- /** M8C-CONTACT-1: is this pubkey a known contact of this agent? */
878
- isContact(agentName: string, pubkey: string): boolean;
879
- /** DOD-TIER-1: the reachability tier for a counterparty of this agent. The RESULT is total — an
880
- * absent contact row (undefined), a NULL `tier`, or a corrupt out-of-range value all resolve to
881
- * UNKNOWN via `normalizeTier`, so the return is always in 0..4 and guards the JS `null >= 0`/`0 ||
882
- * 1`/`grid[99]` traps. It is a SECURITY read (Step 2 gates inbound bounds on it), so it FAILS
883
- * CLOSED, never open: an uninitialized DB throws (same contract as addContact) rather than
884
- * silently returning UNKNOWN and admitting a BLOCKED sender; an unresolvable/retired agent name
885
- * throws via #requireAgentId. Both are invariant violations a caller must surface, not swallow. */
886
- getTier(agentName: string, pubkey: string): number;
887
508
  /** DOD-TIER-4: the DISPLAY/relationship check — is this counterparty a genuine contact (KNOWN or
888
509
  * above)? Replaces the old binary `isContact` for behaviour that keyed on "we have a relationship"
889
510
  * (e.g. the away-response wording). An UNKNOWN-tier contact (a mere row) is NOT known. */
@@ -893,127 +514,12 @@ export declare class SessionNodeManager {
893
514
  * relay mailbox (LEAVEMSG-1), out of scope for this unit; defined here as the seam. Being merely
894
515
  * KNOWN is NOT enough to auto-accept — whitelisting is the deliberate `cello_contact_set_tier` act. */
895
516
  isAutoAccept(agentName: string, pubkey: string): boolean;
896
- /** DOD-TIER-BOUNDS-SETTINGS: the effective bound for (agent, tier, field) — a per-agent SETTINGS
897
- * override if one is set and valid, else the hardcoded grid default (DEFAULT_TIER_BOUNDS). With no
898
- * settings this is byte-identical to Step 2 (the daemon runs on defaults alone). A stored value
899
- * that is somehow non-positive/non-finite (should be impossible — validated at SET time) falls back
900
- * to the grid default rather than removing the bound (INV-TIER-BOUND, defensive). BLOCKED is never
901
- * settable — it always returns the fixed grid value (0). */
902
- resolveTierBound(agentName: string, tier: number, field: "max_sessions" | "max_bytes"): number;
903
- /** M8C-CONTACT-1: pin a contact at add time — idempotent (re-adding an existing contact is a
904
- * no-op, never refreshes added_at; identity does not get re-resolved). MONIKER-3 AC2: an
905
- * optional pet name; a NEW non-null moniker on re-add updates it, absence leaves it untouched.
906
- * THROWS on an invalid moniker — callers validate first; this is the can-never-be-stored
907
- * backstop (same contract as DbIdentityStore.setMoniker).
908
- *
909
- * DOD-TIER-1/4: a NEW row is stamped `tier` (never NULL) and an optional `provenance`
910
- * ('accepted' | 'initiated' | null). The `tier` defaults to the least-privilege UNKNOWN floor —
911
- * a caller GRANTS trust by passing a higher tier explicitly. Every production creation path is a
912
- * deliberate operator action and passes KNOWN (initiate, engage/reply, explicit cello_contact_add
913
- * — DEC-AB-1). INSERT OR IGNORE means an EXISTING contact is untouched — tier and provenance pin
914
- * at first add, exactly as `added_at`/`moniker` already do; re-adding never downgrades a contact
915
- * the operator has since promoted. Raising the tier later is `cello_contact_set_tier`'s job. */
916
- addContact(agentName: string, pubkey: string, moniker?: string | null, provenance?: string | null, tier?: number): void;
917
- /** MONIKER-3 AC3: rename (string) or clear (null) an EXISTING contact's pet name. Returns false
918
- * when no such contact — fail-loud at the caller, never a silent no-op success. Same
919
- * validate-throw backstop as addContact. */
920
- setContactMoniker(agentName: string, pubkey: string, moniker: string | null): boolean;
921
- /**
922
- * M10B / DOD-END-SURFACE-1 — decide whether ONE signal is presented to ONE counterparty.
923
- *
924
- * `present: null` CLEARS the choice, which is not the same as `false`: cleared means "no opinion,
925
- * use the signal's own default", while false means "specifically not this person". Collapsing
926
- * them would make an operator unable to undo an omission without knowing what the default was.
927
- *
928
- * Deliberately does NOT require an existing contact row, unlike the tier/moniker/away setters. A
929
- * decision about what to disclose is meaningful before a relationship is established — indeed
930
- * that is when it matters most — and refusing here would force the operator to add someone as a
931
- * contact in order to withhold something from them.
932
- */
933
- setContactSignalPref(agentName: string, pubkey: string, signalHash: string, present: boolean | null): void;
934
- /**
935
- * The explicit per-counterparty choices for this contact: signal hash → present.
936
- *
937
- * A signal ABSENT from this map has no choice recorded and falls back to its own
938
- * `default_present`. Returns an EMPTY map on an uninitialised DB rather than throwing, because
939
- * this is a preference read on the presentation path and losing preferences must not break a
940
- * session — but note the direction that failure takes: with no preferences, `default_present`
941
- * decides, and consent still gates everything upstream in SQL. It can therefore only fall back to
942
- * the operator's standing default, never to disclosing something consent has not cleared.
943
- */
944
- getContactSignalPrefs(agentName: string, pubkey: string): Map<string, boolean>;
945
- /** DOD-AWAY-TIER-1: set (or clear, with null) a contact's per-contact away message. Returns false
946
- * when no such contact — fail-loud at the caller (same contract as setContactMoniker/setContactTier). */
947
- setContactAwayMessage(agentName: string, pubkey: string, message: string | null): boolean;
948
517
  /** DOD-AWAY-TIER-1: resolve the most-specific CUSTOM away text for a counterparty, most-specific
949
518
  * first: per-contact `away_message` → per-tier away setting → agent default away setting. Returns
950
519
  * null when none is configured, so the CALLER applies the system default (code) — making the full
951
520
  * four-level resolution TOTAL. A pure read; the resolved text is screened on the outbound path by
952
521
  * the caller like any content (SI — it does not bypass the gateway). */
953
522
  resolveAwayMessage(agentName: string, pubkey: string): string | null;
954
- /** DOD-CONTACT-VIEW-1: set an EXISTING contact's reachability tier. Returns false when no such
955
- * contact — fail-loud at the caller, never a silent no-op success (same contract as
956
- * setContactMoniker). The caller validates the tier is a known constant BEFORE calling; this
957
- * stores whatever it is handed (the handler is the validation boundary). */
958
- setContactTier(agentName: string, pubkey: string, tier: number): boolean;
959
- /** DOD-RENAME-1 (Option C): record a self-declared name a peer offered, at the moment the offer is
960
- * SEEN. The stored local pet name (contacts.moniker) is SACROSANCT — this only ever touches
961
- * last_offered_moniker and the notice queue, never the moniker (AC2). A rename NOTICE is queued
962
- * only when the peer is a contact the operator has PERSONALLY NAMED (moniker non-null), a name was
963
- * seen BEFORE (last_offered_moniker non-null), and the new offer DIFFERS (AC3). The first-ever
964
- * offer just records the baseline (no notice); a repeat of the same name is idempotent (AC4).
965
- * Called only when a moniker WAS offered (caller-guarded), so silence never clears the baseline
966
- * (AC5). Limitation: last_offered_moniker updates only on the RECEIVING side of an offer, so rename
967
- * detection works only for peers who INITIATE to you — a property, not a bug. */
968
- recordOfferedMoniker(agentName: string, pubkey: string, offered: string): void;
969
- /** DOD-RENAME-1: pending rename notices for an agent, oldest first (surfaced in
970
- * cello_check_notifications — an INBOX pull, never a real-time push). */
971
- getRenameNotices(agentName: string): Array<{
972
- pubkey: string;
973
- offered_name: string;
974
- noticed_at: number;
975
- moniker: string | null;
976
- }>;
977
- /** DOD-RENAME-1: clear a pending rename notice — the operator acted (adopted a name or removed the
978
- * contact). Idempotent (no notice → no-op). Fail-closed on a missing DB, like the writes above. */
979
- clearRenameNotice(agentName: string, pubkey: string): void;
980
- /** M8C-CONTACT-1: known stays known until explicitly removed. */
981
- removeContact(agentName: string, pubkey: string): boolean;
982
- /**
983
- * Forget the pinned threshold group key for a counterparty, so the next session re-pins.
984
- *
985
- * DOD-M15-OFFER-SIGNED-1 review F2 — WITHOUT THIS THE REFUSAL WAS PERMANENT. The identity-change
986
- * check refuses a counterparty whose group key differs from the one recorded in an earlier
987
- * session, and its guidance told the operator to confirm out of band and then remove the contact
988
- * so the new identity is pinned afresh. `removeContact` deleted a row in `contacts`; the pin lives
989
- * in `sessions.counterparty_primary_pubkey`, and nothing in the daemon ever cleared it.
990
- *
991
- * So an operator who did exactly as instructed — called their counterparty, confirmed the
992
- * re-registration was genuine, removed the contact, retried — got the identical refusal, with no
993
- * way out short of editing the database. A security control that cannot be reset by the person it
994
- * protects is a lockout, and the printed remedy made it worse by reading as though it worked.
995
- *
996
- * Nulls the column rather than deleting the session rows: those rows are the transcript record,
997
- * and a re-pin is not a reason to lose them.
998
- */
999
- clearPinnedCounterpartyPrimary(agentName: string, counterpartyPubkeyHex: string): number;
1000
- /** MONIKER-4: the operator's pet name for a pubkey (whoLabel's top tier), or null. Read-only
1001
- * and tolerant of a not-yet-open DB (a missing label degrades the doorbell, never blocks it). */
1002
- getContactMoniker(agentName: string, pubkey: string): string | null;
1003
- /** M8C-CONTACT-1 + DOD-CONTACT-VIEW-1: list an agent's contacts, oldest-added first, each with its
1004
- * pet name (MONIKER-3), tier + provenance (the address-book metadata), and a READ-side LEFT JOIN
1005
- * against `sessions` for how many SEALED sessions were shared and when they last spoke (MAX
1006
- * updated_at). No new stored data — a pure read. A contact with no sessions shows 0 / null (never),
1007
- * not an error. The JOIN is scoped by agent_id so one agent's sessions never bleed into another's. */
1008
- listContacts(agentName: string): Array<{
1009
- pubkey: string;
1010
- added_at: number;
1011
- moniker: string | null;
1012
- tier: number | null;
1013
- provenance: string | null;
1014
- sealed_count: number;
1015
- last_spoke: number | null;
1016
- }>;
1017
523
  /**
1018
524
  * DOD-M15-REFUSEDEVIDENCE-1 — retain a message refused OUTSIDE `ingestReceivedContent`.
1019
525
  *
@@ -1027,33 +533,9 @@ export declare class SessionNodeManager {
1027
533
  * the logging are the ones every other refusal uses.
1028
534
  */
1029
535
  quarantineRefusedInbound(agentName: string, sessionId: string, reason: string, content: Uint8Array, contentHashHex: string, senderPubkeyHex: string | null, correlationId?: string): number | null;
1030
- /**
1031
- * DOD-M15-REFUSEDEVIDENCE-1: read retained refused messages back — all of a session's, or the one
1032
- * at `sequence`.
1033
- *
1034
- * Returns the RAW payload. Every caller that hands it to a reader must frame it first
1035
- * (`frameQuarantinedPayload`); nothing else in the tree may read this without doing so.
1036
- */
1037
- readQuarantined(agentName: string, sessionId: string, sequence?: number): QuarantinedRecord[];
1038
536
  /** The metadata half of a framed quarantine read — everything known ABOUT the message, none of it
1039
537
  * taken from the message. Split out so the framing module never touches the database. */
1040
538
  quarantineFrameMeta(agentName: string, sessionId: string, rec: QuarantinedRecord): QuarantineFrameMeta;
1041
- /** M8C-ABUSE-1: non-terminal sessions this agent currently holds with the given counterparty.
1042
- * Reviewer HIGH fix (aeffb82f, D18): counting `status = 'active'` ONLY let a counterparty
1043
- * evade the bound for free by disconnecting (a trivial, attacker-controlled action that flips
1044
- * a session to 'interrupted' — markInterruptedWithDetails) and opening a fresh session,
1045
- * repeated indefinitely. 'interrupted' sessions still accept content (ingestReceivedContent
1046
- * explicitly allows both statuses) and are NOT terminal (sealed/seal_interrupted_pending are),
1047
- * so they must still count against the bound. */
1048
- countActiveSessionsForCounterparty(agentName: string, counterpartyPubkey: string): number;
1049
- /** M8C-ABUSE-1 (anti-swarm) + DOD-TIER-2: non-terminal sessions this agent holds with UNKNOWN-tier
1050
- * counterparties — the global cap counts across the whole stranger pool. A sender is exempt from
1051
- * THIS pool iff it is a KNOWN+ contact (tier >= KNOWN); a bare stranger (no row → UNKNOWN) or an
1052
- * explicitly UNKNOWN-tier contact both count. Keying on `tier >= KNOWN` (bounded to <= VIP so a
1053
- * corrupt high value cannot grant pool-exemption) replaces the old row-existence proxy, which
1054
- * would have let a merely-recorded UNKNOWN contact escape the anti-swarm cap. Same
1055
- * 'interrupted'-status fix as countActiveSessionsForCounterparty above. */
1056
- countActiveSessionsFromUnknownSenders(agentName: string): number;
1057
539
  /** M8C-ABUSE-1 + DOD-TIER-2/3: is a NEW inbound session from this counterparty within the
1058
540
  * acceptance bounds? The per-sender cap is now the sender's TIER cap (DEFAULT_TIER_BOUNDS), not a
1059
541
  * flat "3 for strangers, unbounded for contacts". This is where DOD-TIER-3 falls out for free: a
@@ -1068,62 +550,6 @@ export declare class SessionNodeManager {
1068
550
  ok: false;
1069
551
  reason: CapacityReason;
1070
552
  };
1071
- /** M8C-TGDOOR-1: the daemon-wide Telegram bot settings, or null if never configured. */
1072
- getTelegramSettings(): {
1073
- botToken: string;
1074
- allowlistedChatId: string;
1075
- } | null;
1076
- /** M8C-TGDOOR-1: persist (or replace) the singleton Telegram settings row. */
1077
- setTelegramSettings(botToken: string, allowlistedChatId: string): void;
1078
- /** DOD-SETTINGS-1: read a per-agent setting, or null if unset. The get-with-default is the CALLER's
1079
- * job (an unset key falls back to the hardcoded grid/system default — the daemon runs correctly on
1080
- * defaults alone, AC3). Returns null on a missing DB (settings are always optional). */
1081
- getSetting(agentName: string, key: string): string | null;
1082
- /**
1083
- * DOD-SETTINGS-1: DELETE a per-agent setting so the built-in default applies again.
1084
- *
1085
- * Deleting is NOT storing "". `getSetting` returns null for both, but the away-text resolver walks
1086
- * per-contact → per-tier → agent-default → system default, and an empty string is a VALUE that
1087
- * wins that walk and blanks the reply. Unsetting is the only way back to the default, and until
1088
- * this existed there was no way back at all: `cello_settings_set` accepted a string, refused an
1089
- * empty one, and told the caller to "pass null to clear" — a null it coerced to undefined and
1090
- * rejected as missing_params. Following that guidance from the CLI set the literal text "null",
1091
- * so an operator trying to remove their away message ended up broadcasting the word "null" to
1092
- * every caller.
1093
- *
1094
- * Returns whether a row was actually removed, so the handler can report what it did rather than
1095
- * claiming a clear it never performed.
1096
- */
1097
- deleteSetting(agentName: string, key: string): boolean;
1098
- /** DOD-SETTINGS-1: write a per-agent setting (upsert). Key VALIDATION is the handler's boundary
1099
- * (isValidSettingKey); value validation for typed settings (finite bounds, etc.) belongs to the
1100
- * specific consumer. Throws on a missing DB — a write that silently no-ops would be a lie. */
1101
- setSetting(agentName: string, key: string, value: string): void;
1102
- /** DOD-SETTINGS-1: all explicitly-set settings for an agent (the ones that OVERRIDE a default),
1103
- * key-sorted. Unset keys are absent — the operator sees only what they changed. */
1104
- getAllSettings(agentName: string): Array<{
1105
- key: string;
1106
- value: string;
1107
- }>;
1108
- /** DOD-LOOP-1: whether the given agent has a standing receiver ready (any agent if omitted). */
1109
- getStandingReceiverReady(agentName?: string): boolean;
1110
- /**
1111
- * DOD-M12B-RESERVATION-RETRY-1 — whether a NAT'd peer can actually DIAL this agent.
1112
- *
1113
- * `standing_receiver_ready` answers "is there a receiver?", which is true for a plain TCP node
1114
- * that no relay would give a circuit reservation to. Behind NAT that node is reachable by nobody,
1115
- * and the difference was visible only in the log — where it was visible 481 times and nobody
1116
- * acted. `"retrying"` and `"unreachable"` are the states an operator can do something about.
1117
- *
1118
- * reserved — holds a circuit reservation; a NAT'd peer can dial it.
1119
- * retrying — no reservation yet, still re-asking on a backoff.
1120
- * unreachable — no circuit reservation and the automatic re-attempts are spent, so only peers
1121
- * that can connect DIRECTLY will get in. It is not permanent: a directory
1122
- * reconnect carrying a DIFFERENT relay pool re-arms the budget, because a relay we
1123
- * have never tried is new information.
1124
- * absent — no receiver at all (the agent is not online).
1125
- */
1126
- getStandingReceiverReachability(agentName: string): "reserved" | "retrying" | "unreachable" | "absent";
1127
553
  /**
1128
554
  * The current standing receiver node's session-transport coordinates (peer id +
1129
555
  * listen multiaddrs), or null if it is not ready. These are the addresses a local
@@ -1145,92 +571,6 @@ export declare class SessionNodeManager {
1145
571
  * Reads the live gater rather than a copy, so it cannot drift from what the gate actually does.
1146
572
  */
1147
573
  isRelayCarvedOutInbound(agentName: string, relayPeerId: string): boolean;
1148
- getStandingReceiverInfo(agentName: string): {
1149
- peerId: string;
1150
- addrs: string[];
1151
- } | null;
1152
- /**
1153
- * DOD-M15-ASSIGN-1 — name the one peer allowed to dial this agent's standing receiver, at the
1154
- * moment the directory's `session_offer` says who is coming.
1155
- *
1156
- * This is what makes the receiver's deny-by-default safe. The offer names
1157
- * `initiator_session_peer_id`, and the responder answers it by advertising its OWN address in
1158
- * `session_offer_accept`. Narrowing here — BEFORE that answer goes out — means the door opens to
1159
- * exactly one peer at the same instant the address that reaches them is published, and never
1160
- * before. The initiator cannot know where to dial until the accept it triggers has been sent.
1161
- *
1162
- * Returns WHICH failure it was, never a bare false (review F6). The caller reports a distinct
1163
- * reason per cause: "no receiver" and "the directory named nobody" are different subsystems, and
1164
- * collapsing them sent the operator to the directory for a local problem. This method never
1165
- * widens the gate to compensate.
1166
- *
1167
- * Narrows INBOUND ONLY. The receiver is still the daemon's general-purpose dialer at this point
1168
- * — no assignment exists yet — so revoking its outbound latitude here would break content
1169
- * parking and restart-seal submission (review F2).
1170
- */
1171
- admitOfferedDialer(agentName: string, initiatorSessionPeerId: string, sessionIdHex: string): "narrowed" | "no_receiver" | "no_peer_named";
1172
- /**
1173
- * What the UNSIGNED offer claimed, so the SIGNED assignment can be checked against it.
1174
- *
1175
- * DOD-M15-OFFER-SIGNED-1. Decision 2 rules that the listening socket is "gated on the
1176
- * assignment", and the gate is narrowed from `session_offer` — a frame carrying no signature —
1177
- * because that is the only thing that arrives early enough. Timing forced the offer; it does not
1178
- * excuse trusting it.
1179
- *
1180
- * Keeping what the offer said turns the two frames into a CHECK ON EACH OTHER. The assignment is
1181
- * FROST-signed by the initiator's own threshold group, which no single directory can produce, and
1182
- * it names the same peer id. A directory that says one peer in the offer and another in the
1183
- * assignment is naming two different dialers for one session — which a truthful directory never
1184
- * does, and which is exactly the move a compromised one would make to slip a peer past the gate
1185
- * before the signed document arrives.
1186
- */
1187
- getOfferedDialer(agentName: string, sessionIdHex: string): string | null;
1188
- /**
1189
- * Which peer this agent's standing receiver is currently admitting INBOUND — `null` for nobody.
1190
- *
1191
- * Read-only, and it answers a question the daemon otherwise cannot: *"whose dial would this
1192
- * receiver accept right now?"* The gate is narrowed and re-closed from several paths (an offer
1193
- * arrives, an assignment is refused, a session is promoted), and until now the only way to know
1194
- * where it had ended up was to reproduce the sequence in your head.
1195
- *
1196
- * Added for `DOD-M15-RESPONDER-VERIFY-1`, where a refusal for one session was closing the gate a
1197
- * DIFFERENT session had narrowed — a defect with no observable symptom short of the second
1198
- * session's initiator being refused with "nothing invited it".
1199
- */
1200
- getStandingReceiverAllowedPeer(agentName: string): string | null;
1201
- /** Forget the offered dialer for ONE session — called on BOTH the claim and the refusal paths. */
1202
- clearOfferedDialer(agentName: string, sessionIdHex: string): void;
1203
- /**
1204
- * RE-CLOSE the standing receiver — but ONLY if this session is still the one holding it.
1205
- *
1206
- * DOD-M15-OFFER-SIGNED-1 review F4, then N1. The first version closed the gate unconditionally,
1207
- * and that was worse than the defect it fixed: an agent has ONE standing receiver with ONE allowed
1208
- * peer, so a refusal for session P closed the gate that offer Q had narrowed. Q's initiator —
1209
- * invited, legitimate — was then refused with *"nothing invited it"*, which this daemon had.
1210
- *
1211
- * That is the same cross-session interference F1 was written to remove, moved one method along,
1212
- * and triggerable the same way: one bogus offer/assignment pair collapses a concurrent real
1213
- * session.
1214
- *
1215
- * So the gate is closed only when it still names the peer THIS session opened it to. If a later
1216
- * offer has already re-narrowed it, that offer owns the receiver and its narrowing stands.
1217
- *
1218
- * NO EVICTION SWEEP, deliberately (N4). The sweep evicts by "not the allowed peer", and
1219
- * `getConnections()` returns OUTBOUND connections too — including the content-park and
1220
- * restart-seal dials this node makes as the daemon's general-purpose dialer, whose targets are on
1221
- * no allowlist by construction. Sweeping here hung those up, and the failure surfaced as
1222
- * `relay_unavailable`: a transport label for a local decision, which is the exact substitution
1223
- * that comment was written to prevent. The load-bearing control is `DOD-M15-FRAME-1`'s frame gate,
1224
- * which refuses what an unauthorised peer sends; closing the door is enough here.
1225
- */
1226
- revokeOfferedDialer(agentName: string, sessionIdHex: string, offeredPeerId: string | null): void;
1227
- /**
1228
- * The standing receiver's libp2p node — a general-purpose node usable for OUTBOUND dials that
1229
- * are not session-scoped (e.g. the content-park deposit/pull to the relay, MSG-001-3b). Its
1230
- * gater admits nobody INBOUND until a session names them (DOD-M15-ASSIGN-1), but leaves these
1231
- * outbound errands open. Returns null until the receiver is ready.
1232
- */
1233
- getStandingReceiverNode(agentName?: string): CelloNode | null;
1234
574
  /**
1235
575
  * The libp2p Peer ID of an active session's node (N_A for an initiated session), or
1236
576
  * null if no active node exists for it. This is the initiator's session peer id that an
@@ -1238,14 +578,6 @@ export declare class SessionNodeManager {
1238
578
  * its handed-off receiver to it). Read-only.
1239
579
  */
1240
580
  getSessionNodePeerId(agentName: string, sessionId: string): string | null;
1241
- /**
1242
- * CELLO-M7-TRANSPORT-001: the AutoNAT service wrapping the current standing
1243
- * receiver node, or null if the standing receiver is not ready. The composition
1244
- * root uses this as the daemon's runtime IAutoNatService — its getDialability()
1245
- * drives the SessionAssignment advertised address (AC-004/AC-019), and it is the
1246
- * source of the transport.autonat.result / transport.autonat.unavailable events.
1247
- */
1248
- getStandingReceiverAutoNat(): IAutoNatService | null;
1249
581
  /**
1250
582
  * M7-SESSION-001 (M-1 PUSH): register the session-state-change callback.
1251
583
  * Called by the composition root (daemon.ts) after the NotificationDispatcher
@@ -1275,16 +607,6 @@ export declare class SessionNodeManager {
1275
607
  * an unresolvable name — see #requireAgentId for why null is not an option.
1276
608
  */
1277
609
  resolveAgentId(agentName: string): string;
1278
- /**
1279
- * Reverse lookup: the display name of a stable agent_id, or null if no such agent.
1280
- *
1281
- * Deliberately INCLUDES retired agents. Its caller (the startup awaiting-content re-park) holds an
1282
- * agent_id read off a durable row and needs a name to find that agent's standing receiver. A
1283
- * retired agent resolves to its name and then has no standing receiver, so the park fails cleanly
1284
- * and loudly — which is correct. Filtering retired agents out here would instead make the row
1285
- * unattributable and the failure mute.
1286
- */
1287
- agentNameForId(agentId: string): string | null;
1288
610
  /**
1289
611
  * Create a new outbound session node.
1290
612
  * Called during cello_initiate_session.
@@ -1293,18 +615,9 @@ export declare class SessionNodeManager {
1293
615
  * @param agentName Name of the initiating agent
1294
616
  * @param counterpartyPubkey Counterparty's K_local public key (hex)
1295
617
  * @param counterpartyPeerId Counterparty's session-layer Peer ID (for gater)
1296
- * @param correlationId Correlation ID minted at session initiation
1297
- */
1298
- createSessionNode(sessionId: string, agentName: string, counterpartyPubkey: string, counterpartyPeerId: string, correlationId: string, reuseStandingReceiver?: boolean, relay?: RelayConnectParams): Promise<CreateSessionResult>;
1299
- /**
1300
- * M7-SESSION-003: read the direct-path counterparty liveness for a session.
1301
- * 'unknown' when no session node observation has occurred yet.
1302
- *
1303
- * DOD-M12B-ACK-1: 'impaired' is DAEMON-LOCAL and deliberately not on the relay's
1304
- * SessionLiveness wire type — the relay answers a different question (does it hold the
1305
- * recipient's standing connection) and its three states are a deployed bilateral contract.
618
+ * @param correlationId Correlation ID minted at session initiation
1306
619
  */
1307
- getSessionLiveness(agentName: string, sessionId: string): "alive" | "impaired" | "gone" | "unknown";
620
+ createSessionNode(sessionId: string, agentName: string, counterpartyPubkey: string, counterpartyPeerId: string, correlationId: string, reuseStandingReceiver?: boolean, relay?: RelayConnectParams): Promise<CreateSessionResult>;
1308
621
  /**
1309
622
  * DOD-M12B-ACK-1 — live `/cello/content/1.0.0` stream counts on a session's direct path, or null
1310
623
  * when the session has no active node.
@@ -1318,9 +631,6 @@ export declare class SessionNodeManager {
1318
631
  inbound: number;
1319
632
  outbound: number;
1320
633
  } | null;
1321
- /** DOD-M12B-ACK-1: why this session is impaired, for the surface that has to explain it. Null
1322
- * when it is not impaired — a caller must not narrate a failure that is not current. */
1323
- getSessionImpairment(agentName: string, sessionId: string): SessionImpairment | null;
1324
634
  /**
1325
635
  * DOD-M12B-ABANDON-NOTIFY-1 — drive the REAL inbound content handler with one framed message and
1326
636
  * a claimed peer identity.
@@ -1331,16 +641,6 @@ export declare class SessionNodeManager {
1331
641
  * calls, including the authentication check, rather than a copy of its logic.
1332
642
  */
1333
643
  handleContentFrameForTest(agentName: string, sessionId: string, framedBytes: Uint8Array, remotePeerId?: string): Promise<void>;
1334
- /** DOD-CAP-SELF-HEAL-1 test seam: the shutdown sweep's effect, without a shutdown. Mirrors the
1335
- * real UPDATE at gracefulShutdown so a test cannot pass against a label production never sets. */
1336
- markSessionsInterruptedByLocalShutdownForTest(): void;
1337
- /** DOD-CAP-SELF-HEAL-1 test seam: the counterparty's stream closing, without a real peer. */
1338
- markInterruptedByCounterpartyForTest(agentName: string, sessionId: string): void;
1339
- /** Test seam (same spirit as getDb()): seed per-session direct-path liveness, which is otherwise
1340
- * only set by the live node's onPeerConnect/onPeerDisconnect (#wireSessionLiveness). Lets a
1341
- * DB-seeded test exercise the CC-5 reaper's "alive counterparty must survive" gate without standing
1342
- * up a real libp2p peer connection. */
1343
- markSessionLivenessForTest(agentName: string, sessionId: string, state: "alive" | "impaired" | "gone"): void;
1344
644
  /**
1345
645
  * Hand the standing receiver to an inbound session.
1346
646
  * Called during cello_await_session.
@@ -1366,142 +666,6 @@ export declare class SessionNodeManager {
1366
666
  */
1367
667
  retireSessionNode(agentName: string, sessionId: string): Promise<void>;
1368
668
  gracefulShutdown(): Promise<void>;
1369
- /**
1370
- * Return all sessions with a given status from SQLite.
1371
- * Used by cello status to surface interrupted sessions.
1372
- */
1373
- getSessionsByStatus(status: "active" | "sealed" | "interrupted"): SessionRecord[];
1374
- /**
1375
- * DOD-M12B-RESTART-SEAL-1 / DOD-M12B-PENDING-RESOLVE-1 — sessions that need a receipt and have
1376
- * nobody asking for one. TWO populations, one queue, each with its own safety argument.
1377
- *
1378
- * **(2) `seal_interrupted_pending` — a seal commitment nobody notarized.** Measured 2026-08-18 on
1379
- * the live store: 28 sessions, aged 0.3 to 12.8 days, one of them 14 messages long, 26 holding
1380
- * relay-witnessed seal leaves, and **not one with a sealed root**.
1381
- *
1382
- * **HALF OF THEM ARE NOT BILATERAL, and the first version of this comment claimed they were.**
1383
- * Measured split: 14 initiator rows, each carrying the counterparty's signed leaf — and 14
1384
- * responder rows with `counterparty_leaf = NULL`. A responder row is written by
1385
- * `inbound-seal-request.ts` from an UNSIGNED `seal_interrupted_request` frame, before its ack is
1386
- * even sent, so an ordinary send failure produces a one-sided pending row.
1387
- *
1388
- * So the licence is NOT "both parties signed". It is **somebody chose to end this**, on two
1389
- * branches: an initiator row carries the counterparty's signature, and a responder row exists
1390
- * because the counterparty sent a request to seal. And what makes the result VERIFIABLE is
1391
- * neither — it is that the directory rebuilds the tree from relay-witnessed leaves and checks
1392
- * their signatures, never consulting the commitment at all. The commitment is what makes it
1393
- * legitimate to ASK. (`close-session-handler.ts` states this in full; the first draft of this
1394
- * header contradicted it 200 lines away.) `PENDING-EXIT-1` built their exit and it works — but only when an operator runs
1395
- * `cello_close_session` on that session by hand, having somehow deduced they should. Nothing
1396
- * enumerated them, because both sweeps filtered `status = 'interrupted'`. An exit nobody is told
1397
- * about is not an exit.
1398
- *
1399
- * `interrupted_by` is deliberately NOT consulted for that population. It answers "did WE cause
1400
- * this, and may we therefore describe it" — and that is the wrong question once a seal was
1401
- * requested or signed. **SI-001 is not weakened:** it forbids notarizing *"a conversation nobody
1402
- * chose to end"*, and every row here was chosen to be ended by one side or the other. The only
1403
- * thing missing is the request to notarize it.
1404
- *
1405
- * **(1) `interrupted` — sessions our own stop orphaned, and only those.** Here `interrupted_by` is
1406
- * the whole safety argument. `'local'` means the boot sweep, the shutdown
1407
- * sweep, or the operator's own kill switch ended this session — nobody else did, and it cannot be
1408
- * resumed because the transport keypairs died with the process. Those are the ones the resolver
1409
- * may seal on its own.
1410
- *
1411
- * Everything else is excluded and must stay excluded:
1412
- * 'counterparty' — they hung up. SI-001: the operator may still want to wait.
1413
- * 'relay_stream_close' — our relay witness link ended; the session itself may be fine.
1414
- * NULL — written before the column existed, so the cause is UNKNOWN. An
1415
- * unknown cause is not a licence to notarize; it is the reason not to.
1416
- *
1417
- * Same INNER JOIN discipline as getSessionsByStatus: a retired agent's rows are kept for
1418
- * accountability and are not resumable, so they are not offered for sealing either.
1419
- */
1420
- listRestartOrphanedSessions(): Array<{
1421
- agentName: string;
1422
- sessionId: string;
1423
- messageCount: number;
1424
- status: "interrupted" | "seal_interrupted_pending";
1425
- }>;
1426
- /**
1427
- * DOD-M12B-REVIVAL-BOUND-1 — interrupted sessions that can no longer be revived, and must close.
1428
- *
1429
- * Andre, 2026-08-18: *"after that, those peer IDs and that peer connection needs to be shut down.
1430
- * It is an open connection that a malicious agent can farm for."* The tenet is **leave nothing
1431
- * open that is no longer needed**, and the threat model is a daemon that has been reprogrammed —
1432
- * so the guarantee has to hold on the side that is not the attacker.
1433
- *
1434
- * WHAT IS OPEN. `ingestReceivedContent` refuses `sealed`, `seal_interrupted_pending` and
1435
- * `abandoned`, but deliberately ACCEPTS `interrupted` — that acceptance is the only reason
1436
- * recovery can work. Nothing else ever leaves `interrupted`, so it means accepts FOREVER.
1437
- *
1438
- * **THIS IS THE BACKSTOP, NOT THE COMPLEMENT.** The seal path gets first refusal on every
1439
- * local-cause session, because a session whose ending we can describe truthfully earns a
1440
- * notarized receipt. But "the seal path owns it" is not the same as "the seal path will finish
1441
- * it", and two populations fall through the gap between those:
1442
- *
1443
- * - **The resolver gave up.** `markRestartSealGaveUp` writes only `restart_seal_gave_up_at`;
1444
- * the status stays `interrupted`, and `listRestartOrphanedSessions` then excludes the row by
1445
- * `restart_seal_gave_up_at IS NULL` so it is never retried. `TERMINAL_SEAL_REFUSALS` has ten
1446
- * entries and the measured figure is that 59% of seals that start never finish, so this is
1447
- * the common case, not a corner.
1448
- * - **Zero-message local sessions.** The resolver requires `message_count > 0` — a dead
1449
- * handshake is not worth a ceremony. It is still an open write surface.
1450
- *
1451
- * Excluding those left them permanently interrupted and permanently writable, which is the exact
1452
- * condition this line exists to end. And the population is about to become the majority: no row
1453
- * has ever carried `interrupted_by = 'local'` yet, and from the next shutdown onward every
1454
- * shutdown-orphaned session will. So the sweep takes a local-cause session once the seal path
1455
- * has either declined it or exhausted it — never before.
1456
- *
1457
- * **THE CLOCK MUST BE ONE THE COUNTERPARTY CANNOT MOVE.** The obvious fallback for a row with no
1458
- * `interrupted_at` is `updated_at` — and it is exactly wrong. `ingestReceivedContent` accepts
1459
- * content into an `interrupted` session (that acceptance is this whole line's premise), and a
1460
- * successful ingest runs `UPDATE sessions SET message_count = ?, updated_at = <now>`. So
1461
- * `updated_at` is a clock the reprogrammed peer holds: one message every 24 hours and the session
1462
- * never expires, forever. The fallback would have handed the attacker the off switch for the
1463
- * control built to stop them.
1464
- *
1465
- * Instead the missing timestamps are STAMPED ONCE, by `#stampMissingInterruptedAt` immediately
1466
- * before this query runs, and this query reads `interrupted_at` and nothing else. The stamp is
1467
- * written under `WHERE interrupted_at IS NULL`, so it is monotone — set once, never moved, by us
1468
- * and not by a peer. A legacy row therefore gets its full window starting from the first sweep
1469
- * that sees it, which is later than the true interruption but is the only bound that is sound.
1470
- *
1471
- * Same retired-agent INNER JOIN as the sibling query: a retired agent's rows are kept for
1472
- * accountability, are not resumable, and are not writable either.
1473
- *
1474
- * **THE TIME ARITHMETIC IS LOAD-BEARING, AND BOTH OBVIOUS FORMS OF IT ARE WRONG.** These two
1475
- * columns do not hold the same kind of value:
1476
- *
1477
- * `interrupted_at` TEXT, an ISO-8601 string — `new Date(now).toISOString()`.
1478
- * `updated_at` INTEGER, epoch milliseconds.
1479
- *
1480
- * There are FOUR writers of `status = 'interrupted'`, not three. The fourth is
1481
- * `destroySessionNode` → `#updateSessionStatus(…, "interrupted", "local")`, which historically
1482
- * wrote `interrupted_by` and **no timestamp at all** — and it is the path that produced the two
1483
- * rows in Entry 41. It now stamps `interrupted_at` like the others, so NULL is a legacy state
1484
- * rather than one production keeps creating.
1485
- *
1486
- * So a bare `interrupted_at <= ?` against a numeric bound is **always false** — the column has
1487
- * TEXT affinity and the bound parameter has none, so SQLite applies TEXT affinity to the
1488
- * parameter and compares them as STRINGS (`'2026-08-18T05:32:04.183Z' <= 1755000000000` → 0).
1489
- * The query silently returns nothing forever and reads as "nothing has expired yet". And
1490
- * `CAST(interrupted_at AS
1491
- * INTEGER)` is worse than useless: SQLite casts by taking the leading digits, so
1492
- * `'2026-08-18T05:32:04Z'` becomes **2026**, which is older than any epoch bound. That form
1493
- * abandons every interrupted session on the next boot, immediately, whatever its age. It was
1494
- * written, and `session-001`/`cello-list-sessions` failed on it in the gate.
1495
- *
1496
- * `strftime('%s', …) * 1000` parses the ISO string properly and returns NULL for anything it
1497
- * cannot parse — so a malformed or differently-formatted value falls through the COALESCE to
1498
- * `updated_at` rather than being read as the year 2026.
1499
- */
1500
- listExpiredUnrevivableSessions(nowMs: number, windowMs: number): Array<{
1501
- agentName: string;
1502
- sessionId: string;
1503
- cause: string | null;
1504
- }>;
1505
669
  /**
1506
670
  * DOD-M12B-REVIVAL-BOUND-1 — close every session the revival window has expired.
1507
671
  *
@@ -1516,18 +680,6 @@ export declare class SessionNodeManager {
1516
680
  * @returns how many sessions actually flipped — not how many were attempted.
1517
681
  */
1518
682
  closeExpiredUnrevivableSessions(nowMs: number, windowMs: number): Promise<number>;
1519
- /**
1520
- * DOD-M12B-RESTART-SEAL-1 — record that automatic sealing has exhausted this session.
1521
- *
1522
- * Durable on purpose. The resolver's attempt budget is in memory, so without this the budget
1523
- * resets on every boot and a session that can never seal costs five directory ceremonies a day
1524
- * for the life of the machine.
1525
- *
1526
- * IT IS NOT A DEAD END FOR THE OPERATOR. The row keeps status `interrupted`, so a manual
1527
- * `cello_close_session` still works on it and — since DOD-M12B-INTERRUPTED-ESCALATE-1 — still
1528
- * escalates to a unilateral seal. This column only withdraws the session from AUTOMATIC retries.
1529
- */
1530
- markRestartSealGaveUp(agentName: string, sessionId: string, reason: string): void;
1531
683
  getSessionsForAgent(agentName: string): SessionRecord[];
1532
684
  /**
1533
685
  * Every persisted session across ALL agents, most-recently-updated first. Backs the daemon-wide
@@ -1564,7 +716,6 @@ export declare class SessionNodeManager {
1564
716
  * annexed, not stranded), which the early return skipped entirely.
1565
717
  */
1566
718
  markSealed(agentName: string, sessionId: string): boolean;
1567
- recordSealCertificate(agentName: string, sessionId: string, sealedRootHex: string, legibilityJson: string): void;
1568
719
  /**
1569
720
  * M8B FINDING-6 (cascade-2): persist a seal certificate for a session that may have NO local
1570
721
  * `sessions` row. recordSealCertificate above is an `UPDATE ... WHERE` — a SILENT no-op when the
@@ -1576,85 +727,6 @@ export declare class SessionNodeManager {
1576
727
  * derives it from the notification's present_pubkey.
1577
728
  */
1578
729
  recordSealCertificateEnsuringRow(agentName: string, sessionId: string, counterpartyPubkeyHex: string, sealedRootHex: string, legibilityJson: string): void;
1579
- /**
1580
- * M7 legibility-TBS-binding (responder verify): record the counterparty's FROST primary (group)
1581
- * pubkey from the FROST-signed SessionAssignment, so the responder can VERIFY the bilateral seal
1582
- * signature locally. Best-effort — a missing row (race) is a no-op; the seal then falls back to
1583
- * accept-without-verify (still sound: the live frame arrives over the authenticated Noise channel).
1584
- */
1585
- /**
1586
- * The counterparty's threshold group key as this agent has seen it BEFORE — trust on first use.
1587
- *
1588
- * DOD-M15-OFFER-SIGNED-1 / RESPONDER-VERIFY-1. The responder does not verify the assignment's
1589
- * signature (deferred to SESSION-004), so every field in it is whatever the directory said. That
1590
- * makes a same-frame check circular: a compromised directory just says the same thing twice.
1591
- *
1592
- * This is the one anchor the responder holds that a directory CANNOT retroactively change — its
1593
- * own memory of previous sessions with this counterparty. A directory that names a different
1594
- * threshold group key for someone you have already talked to is either substituting an identity
1595
- * or has been compromised since; neither is a session to accept quietly.
1596
- *
1597
- * THE BOUND, stated rather than glossed: this is worth nothing on FIRST contact, which is the
1598
- * definition of trust-on-first-use. It hardens every session after it, which is where a long-lived
1599
- * counterparty relationship actually lives.
1600
- *
1601
- * Keyed on `counterparty_pubkey` — the K_local IDENTITY, which is the stable thing — not on a
1602
- * session id or a display name.
1603
- */
1604
- getPinnedCounterpartyPrimary(agentName: string, counterpartyPubkeyHex: string): string | null;
1605
- recordCounterpartyPrimary(agentName: string, sessionId: string, primaryPubkeyHex: string): void;
1606
- /**
1607
- * M7-SESSION-004 (AC-005/AC-006): read the persisted seal certificate for a session.
1608
- * Returns the sealed root and the parsed legibility object (JSON-safe, hex pubkeys), or
1609
- * null if the session is unknown or not yet sealed. This is the cert-read surface a
1610
- * reader (operator, agent, arbitrator) — possibly in a DIFFERENT process than the one
1611
- * that built the certificate — uses to determine receipt-not-assent, per-party frontiers,
1612
- * attestation modes, and whether the final message was answered.
1613
- */
1614
- getSealCertificate(agentName: string, sessionId: string): {
1615
- sealed_root: string;
1616
- legibility: unknown;
1617
- } | null;
1618
- /**
1619
- * DOD-M15-INCLUSION-1: keep the leaf set the certificate is signed over, so one message can later
1620
- * be proved to sit under it.
1621
- *
1622
- * REFUSES unless the hashes reproduce `sealedRootHex` — `certifiedLeafSetFrom` does that check and
1623
- * this method never bypasses it. That is what separates "the leaves the directory sent" from "the
1624
- * leaves the consortium signed", and only the second is worth storing: a proof built on the first
1625
- * would inherit whatever the directory chose to say.
1626
- *
1627
- * Idempotent (INSERT OR REPLACE keyed on leaf_index) so a re-delivered seal frame, or a unilateral
1628
- * seal later upgraded to bilateral, rewrites the same rows instead of failing or doubling them.
1629
- *
1630
- * @returns whether the set was accepted and stored.
1631
- */
1632
- recordCertifiedLeafSet(agentName: string, sessionId: string, signedLeaves: readonly SealFrontierLeaf[], sealedRootHex: string, correlationId?: string): boolean;
1633
- /**
1634
- * Record WHY this session does or does not have a certified leaf set.
1635
- *
1636
- * Public for the one case the manager cannot see: a seal frame that carried no signed leaves at
1637
- * all never reaches `recordCertifiedLeafSet`, and that absence is a permanent fact about the
1638
- * session for the party that observed it.
1639
- */
1640
- noteCertifiedLeafSetUnavailable(agentName: string, sessionId: string, state: "not_carried_absent_party" | "not_carried_present_party", detail: string): void;
1641
- /**
1642
- * The last thing that happened to this session's certified leaf set, or null if nothing has.
1643
- *
1644
- * Null here and a null from `getCertifiedLeafSet` together mean "no seal has been processed on
1645
- * this side yet" — which is a different sentence again from any of the recorded states.
1646
- */
1647
- getCertifiedLeafSetState(agentName: string, sessionId: string): {
1648
- state: string;
1649
- detail: string | null;
1650
- } | null;
1651
- /**
1652
- * The certified leaf set, in order, or null when none was stored for this session.
1653
- *
1654
- * Null is a REFUSAL upstream, never a fallback to the local tree: the two cover different leaves
1655
- * and substituting one for the other is how a proof comes to land on a root nobody signed.
1656
- */
1657
- getCertifiedLeafSet(agentName: string, sessionId: string): string[] | null;
1658
730
  /**
1659
731
  * M7-SESSION-001: Mark a session as interrupted with message count and timestamp.
1660
732
  * Called when a relay session_interrupted frame arrives or a relay stream closes.
@@ -1697,81 +769,7 @@ export declare class SessionNodeManager {
1697
769
  merkleRoot: string;
1698
770
  nonce: string;
1699
771
  }): boolean;
1700
- /**
1701
- * M7-SESSION-001 (H-1): read back the persisted bilateral commitment artifacts
1702
- * for a session. Returns null when none exist.
1703
- */
1704
- /**
1705
- * M12-P17: durably record verified content that arrived for an ALREADY-ENDED session.
1706
- *
1707
- * Returns true only when the row is committed — the caller confirm-deletes the relay copy on the
1708
- * strength of this answer, and the ORDER is load-bearing: annex first, delete second. A crash
1709
- * between them must lose nothing, so a failure here MUST report false and leave the relay copy
1710
- * alone. Getting that backwards converts a noisy re-pull loop into permanent silent loss, which is
1711
- * the outcome this whole unit exists to prevent.
1712
- */
1713
- recordSealedAnnex(agentName: string, sessionId: string, contentHashHex: string, content: Uint8Array, senderPubkeyHex: string | null): boolean;
1714
- /**
1715
- * M12-P18: record that this agent refused a session, so parked content that later arrives for it
1716
- * (and fails `counterparty_unknown`, because no session row exists) can be swept instead of
1717
- * re-pulled forever. Keeps the most recent REFUSED_SESSIONS_CAP per agent.
1718
- */
1719
- recordRefusedSession(agentName: string, sessionId: string, reason: string): void;
1720
- /** M12-P18: did this agent refuse this session? Consulted at drain to sweep orphaned parked content. */
1721
- wasSessionRefused(agentName: string, sessionId: string): boolean;
1722
- /** M12-P17: read the annex. Operator-initiated ONLY — never wired to a wake path or inbox count. */
1723
- readSealedAnnex(agentName: string, sessionId?: string): Array<{
1724
- session_id: string;
1725
- content_hash: string;
1726
- sender_pubkey: string | null;
1727
- text: string;
1728
- arrived_at: number;
1729
- }>;
1730
- getSealInterruptedArtifacts(agentName: string, sessionId: string): {
1731
- role: string;
1732
- ownLeaf: unknown;
1733
- counterpartyLeaf: unknown;
1734
- merkleRoot: string;
1735
- nonce: string;
1736
- } | null;
1737
- /**
1738
- * Return the session record for a specific sessionId, regardless of status.
1739
- * Used by cello_close_session to inspect session state.
1740
- */
1741
- getSessionRecord(agentName: string, sessionId: string): SessionRecord | null;
1742
- /**
1743
- * DOD-SESSION-NAME-1: set (string) or clear (null) THIS agent's name for a session.
1744
- *
1745
- * Returns false when the (agent_id, session_id) row does not exist — i.e. the session is not this
1746
- * agent's — so the caller refuses with session_not_found rather than reporting a silent success on
1747
- * a write that landed nowhere. Same contract as setContactMoniker.
1748
- *
1749
- * Ownership is the ONLY scope: the composite key IS the ownership check, and status is deliberately
1750
- * not consulted. A sealed session can be named — naming one long after the fact is the point — and
1751
- * a name is a local column, so writing it cannot touch the seal, a Merkle leaf, or the wire.
1752
- *
1753
- * The caller validates (validateSessionName) before calling; this stores what it is given.
1754
- */
1755
- setSessionName(agentName: string, sessionId: string, sessionName: string | null): boolean;
1756
- /**
1757
- * MSG-2 startup-flush: the persisted relay endpoint for a session, or null if none was
1758
- * recorded. Used by the crash-backstop flush, which runs at startup BEFORE the in-memory
1759
- * session entries exist, so it cannot use `entry.relayPeerId`.
1760
- */
1761
- getPersistedRelayEndpoint(agentName: string, sessionId: string): {
1762
- relayPeerId: string;
1763
- relayAddrs: string[];
1764
- } | null;
1765
- /**
1766
- * DOD-MSG-4 (auto-recover): the DISTINCT relay endpoints this agent has sessions on, so the daemon
1767
- * can pull the agent's parked mailbox from each on reconnect (the relay mailbox is keyed by recipient
1768
- * pubkey, so one pull per relay drains all of the agent's parked content there). Distinct by relay
1769
- * peer id.
1770
- */
1771
- getAgentRelayEndpoints(agentName: string): Array<{
1772
- relayPeerId: string;
1773
- relayAddrs: string[];
1774
- }>;
772
+ /** Loaded from SQLite on first access so it survives a restart (AC-007). NEVER null: an unknown session yields an EMPTY tree. */
1775
773
  getSessionTree(agentName: string, sessionId: string): SessionTree;
1776
774
  /** Current daemon-owned tree root for a session, as hex. */
1777
775
  getSessionTreeRootHex(agentName: string, sessionId: string): string;
@@ -1923,118 +921,6 @@ export declare class SessionNodeManager {
1923
921
  * reconciliation — same documented limitation as the UP-2 gate above.
1924
922
  */
1925
923
  getSealUpgradeReadiness(agentName: string, sessionId: string): SealUpgradeReadiness;
1926
- /**
1927
- * ─── DOD-M15-NO-SILENT-REFUSAL-1: refusals the RECEIVING operator can actually see ────────────
1928
- *
1929
- * Every inbound refusal already logs a `reason`, an `impact` and a `guidance` — and they are
1930
- * good. They had no reader. From the receiving operator's chair a refused message simply never
1931
- * arrives: the conversation goes quiet with a full explanation sitting in a file they have no
1932
- * reason to open, and they conclude the other person stopped replying.
1933
- *
1934
- * **DURABLE, and that is the half that makes this useful.** The predecessor kept notices in a
1935
- * `Map` on this instance and drained them on the receive path for one session. So a restart lost
1936
- * them, and an agent NOBODY IS ATTENDING lost them too — the connection is live, the daemon is
1937
- * up, and the notice only ever reaches whoever happens to call `cello_receive` on that exact
1938
- * session. `cello_check_notifications` now reads them as its own inbox category.
1939
- *
1940
- * **DEDUPLICATED PER SESSION PER REASON, and that is the design, not an optimisation.** A skewed
1941
- * peer turns one problem into a flood: the first refusal of a kind is the signal, the ninetieth is
1942
- * noise that trains the operator to ignore the surface. `count` keeps the scale visible without
1943
- * repeating the alert.
1944
- *
1945
- * **NEVER carries the content.** It failed verification; surfacing it is the injection path the
1946
- * cross-check exists to close. The operator learns that a message was refused and why — never
1947
- * what it said.
1948
- */
1949
- /**
1950
- * Record an inbound refusal for the operator. First of its kind per session is the signal.
1951
- *
1952
- * ⚠️ **DOES NOT THROW, and that is a decision with a cost — stated so it is not mistaken for an
1953
- * oversight.** Every call site here has already decided to refuse and is about to return a reason
1954
- * to its caller; a throw would replace that clean refusal with an exception on the ingest path,
1955
- * changing what the SENDER observes because this daemon could not file a note. So a persistence
1956
- * failure is logged at ERROR under `session.refusal.persist.failed`, carrying the reason, the
1957
- * impact and the guidance verbatim — the forensic record survives even when the operator-facing
1958
- * one does not. It is not silent; it is one surface short, and the log says which notice was lost.
1959
- */
1960
- noteContentRefusal(agentName: string, sessionId: string, reason: string,
1961
- /**
1962
- * ALL THREE REQUIRED, and that is the enforcement rather than the convention.
1963
- *
1964
- * The DoD clause is "every reason calls this with an impact and a guidance", and an optional
1965
- * field makes that a thing a reviewer checks by reading thirteen call sites. `kind` is required
1966
- * for the same reason one level up: the header over a list of refusals is composed from it, and
1967
- * a notice that could omit it would silently inherit whichever header happened to be first.
1968
- */
1969
- detail: {
1970
- kind: RefusalKind;
1971
- impact: string;
1972
- guidance: string;
1973
- }): void;
1974
- /**
1975
- * DOD-M15-NO-SILENT-REFUSAL-1: the operator has seen these and does not want to see them again.
1976
- *
1977
- * ⚠️ **WITHOUT THIS THE NOTICES ARE PERMANENT, and that is what makes people stop reading the
1978
- * inbox.** "Already shown you" is tracked per WINDOW — a new MCP connection has been told nothing,
1979
- * so it is told everything. Someone on an older build messages you, you sort it out with them,
1980
- * they upgrade, and every new session you ever open still opens with that refusal.
1981
- *
1982
- * Dismissing does NOT turn anything off. If the cause fires again the notice comes back, because
1983
- * a fresh refusal writes a fresh row. The operator is saying "I know", not "stop telling me".
1984
- *
1985
- * Returns how many were cleared, so the caller can say so rather than claiming a silent success.
1986
- */
1987
- dismissContentRefusals(agentName: string, sessionId: string): number;
1988
- /**
1989
- * Drain the refusals a GIVEN CONSUMER has not been shown yet, and remember what it was shown.
1990
- *
1991
- * ─── Why this is keyed by consumer, and not by a single flag ──────────────────────────────────
1992
- *
1993
- * It used to set one `surfaced: boolean` on the notice. Two MCP windows attending the same agent
1994
- * is the ordinary case, and under that flag whoever read FIRST consumed the notice — the second
1995
- * window was told nothing, permanently. **That is the same defect `takeReceivedContent` had**, and
1996
- * the comment above the delivery loop in `session-content-handlers.ts` spells out why it was
1997
- * removed: *"reading is non-destructive by construction. Nothing one consumer does mutates state
1998
- * another consumer reads."*
1999
- *
2000
- * ─── Why the count has a reader ───────────────────────────────────────────────────────────────
2001
- *
2002
- * A reason RE-ANNOUNCES to a consumer when its count has grown by an order of magnitude since that
2003
- * consumer last saw it (1 → 10 → 100 → …), marked `repeat: true`. That keeps the first refusal the
2004
- * signal and the ninetieth silent, which is the dedup's point, while still making a skew that has
2005
- * swallowed hundreds of messages visible — at a handful of announcements per session, not one per
2006
- * message.
2007
- *
2008
- * ─── What a restart does, deliberately ────────────────────────────────────────────────────────
2009
- *
2010
- * The notices survive; the read state is keyed by IPC connection id, which does not. So after a
2011
- * restart every notice is unseen again and the next reader is told. That is the correct direction:
2012
- * a fresh window has not been told anything, and re-announcing costs one line where staying silent
2013
- * costs the whole point of storing it.
2014
- */
2015
- takeContentRefusals(agentName: string, sessionId: string,
2016
- /**
2017
- * REQUIRED, deliberately — no default.
2018
- *
2019
- * It had one (`"default"`), and a default is the defect this method was rewritten to remove,
2020
- * lying in wait: any future call site that omits the argument silently shares ONE bucket across
2021
- * every window, the first reader consumes the notice for all the others, and nothing fails to
2022
- * compile and no test goes red. The parameter existing is not the protection; being unable to
2023
- * forget it is.
2024
- */
2025
- consumerId: string): Array<Omit<RefusalNotice, "sessionId">>;
2026
- /**
2027
- * DOD-M15-NO-SILENT-REFUSAL-1: every unshown refusal for an agent, ACROSS its sessions.
2028
- *
2029
- * The inbox's door. `takeContentRefusals` answers for one session because its caller already holds
2030
- * one; `cello_check_notifications` holds an agent and nothing else, and the case this whole line
2031
- * exists for is that nobody is attending any of that agent's sessions — so a per-session read
2032
- * cannot reach it. Same store, same per-consumer rule, same re-announce.
2033
- */
2034
- takeAgentContentRefusals(agentName: string, consumerId: string): {
2035
- notices: RefusalNotice[];
2036
- truncated: boolean;
2037
- };
2038
924
  ingestReceivedContent(agentName: string, sessionId: string, content: Uint8Array, contentHash: Uint8Array, correlationId?: string,
2039
925
  /**
2040
926
  * DOD-FRONTIER-STRAND-1 AC1: the relay-assigned canonical position for THIS message, taken from
@@ -2119,7 +1005,6 @@ export declare class SessionNodeManager {
2119
1005
  * are therefore undeliverable. Empty is the overwhelmingly normal case.
2120
1006
  */
2121
1007
  getUndeliverableSeqs(agentName: string, sessionId: string): readonly number[];
2122
- getHighWaterSeq(agentName: string, sessionId: string): number;
2123
1008
  /**
2124
1009
  * M12-P14: is this side's chain COMPLETE enough to be sealed?
2125
1010
  *
@@ -2184,18 +1069,6 @@ export declare class SessionNodeManager {
2184
1069
  * `leaf_count_mismatch` back, which is terminal and costs the receipt for good.
2185
1070
  */
2186
1071
  sealReadinessView(agentName: string, sessionId: string): SealReadinessView;
2187
- /** DOD-M12B-INDEX-1 — this agent's own K_local pubkey, for attributing its own held content.
2188
- * Null when it cannot be resolved: an UNATTRIBUTED annex row is true, a falsely attributed one
2189
- * is not, and this is the record that outlives the session. */
2190
- /**
2191
- * Test seam for `#recoverOwnSealCtrlLeaf` (documented on the method itself, below). The
2192
- * distinction it draws — "there is none" versus "I could not tell" — is the whole safety
2193
- * property, and it had no coverage at any level.
2194
- */
2195
- recoverOwnSealCtrlLeafForTest(agentName: string, sessionId: string): {
2196
- reportedRootHex: string;
2197
- sequenceNumber: number;
2198
- } | "none" | "unknown";
2199
1072
  /**
2200
1073
  * DOD-M12B-ABANDON-NOTIFY-1 — tell the counterparty we have hung up. Best effort, never blocking.
2201
1074
  *
@@ -2238,12 +1111,6 @@ export declare class SessionNodeManager {
2238
1111
  mustClear: number;
2239
1112
  blocked: boolean;
2240
1113
  };
2241
- /** DOD-CAP-SELF-HEAL-1: the sessions with this counterparty that are consuming cap slots, oldest
2242
- * first. The operator is told to close some — this is WHICH, because "close three of them" with
2243
- * no list is not an instruction they can follow. */
2244
- sessionsConsumingCap(agentName: string, counterpartyPubkey: string, limit?: number): string[];
2245
- /** DOD-M12B-ABANDON-NOTIFY-1: has the counterparty told us they hung up? */
2246
- counterpartyAbandonedAt(agentName: string, sessionId: string): number | null;
2247
1114
  /**
2248
1115
  * DOD-M12B-INDEX-1 — commit THIS agent's own leaf at the position the relay assigned it.
2249
1116
  *
@@ -2332,12 +1199,6 @@ export declare class SessionNodeManager {
2332
1199
  type: "sealed";
2333
1200
  unreadCount: number;
2334
1201
  } | null;
2335
- /**
2336
- * F1-b: the durable sealed root hex for a session (written by recordSealCertificate on the
2337
- * bilateral path), or null if not recorded. Lets cello_receive echo the sealed root in its
2338
- * terminal answer without threading it through destroySessionNode.
2339
- */
2340
- getSealedRootHex(agentName: string, sessionId: string): string | null;
2341
1202
  /**
2342
1203
  * DOD-MSG-4 (self-ordering content frame): verify the relay's signed ordering record carried IN the
2343
1204
  * content frame and record the canonical sequence for the strict-in-order gate — so ordering does
@@ -2356,31 +1217,6 @@ export declare class SessionNodeManager {
2356
1217
  * exposed here, so no caller can seal an unsigned envelope through this class.
2357
1218
  */
2358
1219
  decodeParkEnvelope(plaintext: Uint8Array): ParkEnvelope;
2359
- /**
2360
- * SEC-1 — THE ONLY WAY PARKED CONTENT ENTERS THE TRANSCRIPT.
2361
- *
2362
- * Authentication and ingest are FUSED here on purpose, and must stay fused. A caller cannot ingest
2363
- * parked content without passing the signature gate, because this is the only entry point. Exposing
2364
- * a separate `decode → ingest` path — with the signature check as its own optional step — is a
2365
- * downgrade attack: an attacker omits the thing that triggers the check, and the check never runs.
2366
- * A gate the caller can skip by leaving a field out is not a gate.
2367
- *
2368
- * FAILS CLOSED. No signature / bad signature / signer is not this session's counterparty / no
2369
- * session at all → REFUSED, nothing is appended, nothing is written, and the caller MUST NOT
2370
- * confirm-delete the entry from the relay (a forgery must not be able to evict itself, and a
2371
- * genuine bug must not silently eat mail).
2372
- */
2373
- recoverParkedEntry(agentName: string, sessionId: string, recipientPubkey: Uint8Array, unsealed: Uint8Array, contentHash: Uint8Array, correlationId?: string): Promise<{
2374
- ok: true;
2375
- leafIndex: number;
2376
- sequenceNumber: number;
2377
- held?: boolean;
2378
- appendedCount?: number;
2379
- screenedOut?: boolean;
2380
- } | {
2381
- ok: false;
2382
- reason: string;
2383
- }>;
2384
1220
  /**
2385
1221
  * DOD-MSG-4 (2b): public entry for the recover path to verify + record a parked entry's ordering
2386
1222
  * record (the recover handler lives in daemon.ts, which has no access to the private method).
@@ -2423,109 +1259,6 @@ export declare class SessionNodeManager {
2423
1259
  * against a decision production does not make.
2424
1260
  */
2425
1261
  runAutoAcknowledgeGateForTest(agentName: string, sessionId: string, correlationId?: string): void;
2426
- setSaltContributionForTest(agentName: string, sessionId: string, contribution: Uint8Array): void;
2427
- /**
2428
- * Test seams: re-enter the mint path, and read back the PUBLIC half — 006-CRYPTO.
2429
- *
2430
- * `#mintSessionEphemeral` is idempotent because a reconnect can re-enter an activation path, and a
2431
- * second keypair mid-session would leave the two sides deriving against a moving value. Proving
2432
- * that needs the path called TWICE, and the alternative — driving a real reconnect — drags in node
2433
- * rebuild and relay reconnection, none of which the property is about. Same justification as
2434
- * `forgetSaltContributionForTest` above.
2435
- *
2436
- * It calls the REAL private method, so a test cannot pass against a decision production does not
2437
- * make. The reader returns the public half ONLY: a seam that could hand out the secret is a way
2438
- * for the secret to leave this object, which is the one thing the whole unit is about.
2439
- */
2440
- mintSessionEphemeralForTest(agentName: string, sessionId: string): void;
2441
- sessionEphemeralPublicForTest(agentName: string, sessionId: string): Uint8Array | null;
2442
- /**
2443
- * Test seam: INSTALL a keypair the caller already holds — the only way to prove ZEROING.
2444
- *
2445
- * Presence is easy to assert and is not the property. `destroySessionEphemeral` overwrites the
2446
- * buffer before the entry is dropped, and a mutant that drops without overwriting leaves the
2447
- * secret wherever the collector last moved it while passing every presence check — which is
2448
- * exactly what happened: the shutdown zeroing shipped with a surviving mutant, and the transport
2449
- * seeds four lines above it have the same untested gap today.
2450
- *
2451
- * The direction matters. Nothing here HANDS OUT a secret — the test supplies an object it already
2452
- * owns and then inspects its own reference. A reader that returned the live keypair would be a
2453
- * path for the secret to leave this object, which is the one thing this unit exists to prevent.
2454
- */
2455
- setSessionEphemeralForTest(agentName: string, sessionId: string, ephemeral: SessionEphemeral): void;
2456
- /**
2457
- * Test seam: put a session into the state a COMPLETED exchange leaves it in — 007-CRYPTO.
2458
- *
2459
- * A live send now requires an agreed key, because there is no plaintext path to fall back to. In
2460
- * production the exchange completes on connect, before any send. A fixture with no real peer never
2461
- * completes it, so without this every content test in the repo would be exercising the refusal
2462
- * path instead of the thing it was written for.
2463
- *
2464
- * ⚠️ IT SHORT-CIRCUITS HOW THE KEY GOT THERE, NEVER WHAT THE KEY IS FOR. The state it produces —
2465
- * a session holding an agreed content key — is exactly the production state, which is what makes
2466
- * it legitimate; `setSaltContributionForTest` exists for the same reason. Tests of the EXCHANGE
2467
- * itself drive the real signed frames and must not use this.
2468
- */
2469
- setSessionContentKeyForTest(agentName: string, sessionId: string, key: Uint8Array): void;
2470
- /**
2471
- * Test seam: put an own leaf in the HELD state instead of the tree — 033-ACKEMIT.
2472
- *
2473
- * The state `placeOwnLeaf` produces when the relay assigns a position ahead of our tail: the leaf
2474
- * exists on this side and is not in the tree, while the counterparty already has it from the relay
2475
- * and can acknowledge it. Reproducing it through the real hold map rather than by asserting the
2476
- * tree is short is what makes the acknowledgement test measure the case instead of a neighbour of
2477
- * it.
2478
- */
2479
- holdOwnLeafForTest(agentName: string, sessionId: string, canonicalSeq: number, contentHashHex: string): void;
2480
- /**
2481
- * Test seam: put the session's genesis prev_root where a completed session open leaves it —
2482
- * 033-ACKEMIT.
2483
- *
2484
- * ⚠️ THE STATE IS THE PRODUCTION ONE; ONLY HOW IT GOT THERE IS SHORT-CIRCUITED, exactly as
2485
- * `setSessionContentKeyForTest` short-circuits the key exchange next door.
2486
- *
2487
- * In production this value is derived from the directory-signed relay assignment and written to
2488
- * the session row the moment the session learns it, so every real session has one. A fixture that
2489
- * builds a session node directly never sees an assignment — so without this seam every content
2490
- * test built on the fixture would be exercising the "no starting point" REFUSAL path instead of
2491
- * the thing it was written for, and would report that as a pass or a mysterious failure depending
2492
- * on which side of the send it sat on.
2493
- */
2494
- setSessionGenesisForTest(agentName: string, sessionId: string, genesis: Uint8Array): void;
2495
- /**
2496
- * Test seam: drop the agreed key while leaving the session up — the state before an exchange
2497
- * completes, and after a teardown evicts one. Its mirror above is what a completed exchange
2498
- * leaves; both are needed, or a status field stuck in one position passes either test alone.
2499
- */
2500
- forgetSessionContentKeyForTest(agentName: string, sessionId: string): void;
2501
- /**
2502
- * Test seam: produce THIS side's signed ephemeral, using the manager's own identity resolver.
2503
- *
2504
- * For harnesses whose connectivity is one-directional — one side dials, so only one announce ever
2505
- * lands. Carrying the other side's half across with a REAL signature is what completes the
2506
- * exchange, and it beats seeding a key: a seeded key has no peer half recorded against it, so the
2507
- * first genuine announce replaces it and the two ends drift apart (which is correct behaviour —
2508
- * see the re-key guard — and exactly what made seeding fragile here).
2509
- *
2510
- * It signs with the same provider production signs with, so a test cannot pass against a signature
2511
- * production would have refused.
2512
- */
2513
- signOwnEphemeralForTest(agentName: string, sessionId: string): Promise<{
2514
- ephemeralPublic: Uint8Array;
2515
- signature: Uint8Array;
2516
- } | null>;
2517
- /**
2518
- * Test seam: deliver a peer's signed ephemeral, exactly as the content-stream decoder does.
2519
- *
2520
- * For harnesses whose connectivity is one-directional — one side dials, so only one announce ever
2521
- * lands — this is what completes the exchange instead of stuffing a key in. It runs the REAL
2522
- * verification and the REAL derivation, so a test cannot pass against a signature production would
2523
- * have refused.
2524
- */
2525
- handleEphemeralFrameForTest(agentName: string, sessionId: string, frame: {
2526
- ephemeralPublic?: Uint8Array;
2527
- signature?: Uint8Array;
2528
- }, correlationId?: string): Promise<void>;
2529
1262
  /**
2530
1263
  * Test seam: deliver an inbound salt frame, exactly as the content-stream decoder does.
2531
1264
  *
@@ -2541,76 +1274,10 @@ export declare class SessionNodeManager {
2541
1274
  */
2542
1275
  handleSaltFrameForTest(agentName: string, sessionId: string, frame: SaltAgreementFrame, correlationId?: string): Promise<void>;
2543
1276
  /**
2544
- * Test seam: drop this session's own half while leaving the stored salt in place — the state every
2545
- * teardown produces, because `#evictSessionCaches` clears the map and the row survives.
2546
- *
2547
- * It clears the SAME map the eviction clears rather than a stand-in, so a test cannot pass against
2548
- * a state the daemon never reaches. Reproducing it through a real teardown/revive would also drag
2549
- * in node rebuild and relay reconnection, none of which this is about.
2550
- */
2551
- forgetSaltContributionForTest(agentName: string, sessionId: string): void;
2552
- /**
2553
- * THE ONE PLACE THAT DECIDES HOW A SESSION'S OUTBOUND CONTENT IS HASHED —
2554
- * `DOD-M15-SEALWIRE-1` part B2b.
2555
- *
2556
- * Returns the hash AND the algorithm that produced it, together, because the two must not be
2557
- * decided separately. `wire-content-hash.ts` exists for exactly this reason and says so in its own
2558
- * header: the expression was written out at five call sites, the two added last got it wrong, and
2559
- * the failure was invisible — *"the send succeeds, `parked: false`, the sender's log says the frame
2560
- * left, and the receiver discards it at the authenticity check."* It took two real daemons.
2561
- *
2562
- * There are FOUR outbound sites (`session-content-handlers.ts`, two in `daemon.ts`,
2563
- * `document-delivery-transport.ts`). Once salting is switchable, each of them independently
2564
- * deciding whether to salt is that defect again with a worse failure mode — a message hashed one
2565
- * way and LABELLED another is refused by every peer, including a correct one.
2566
- *
2567
- * ⚠️ ASYNC, AND THAT IS THE POINT — B2b-2 constraint 2, not an implementation detail.
2568
- *
2569
- * The agreement is in flight while the operator composes their first message. Hash without waiting
2570
- * and it comes out unsalted, and that first unsalted hash closes adoption for the LIFE of the
2571
- * session (Decision #8, unit 1). Every session would fall back permanently while every log line
2572
- * about it stayed true — the feature present, wired, tested, and never once reached.
2573
- *
2574
- * The wait lives HERE rather than at the four call sites for the same reason `contentHashAlg` is a
2575
- * required parameter rather than a defaulted one: a site that forgets it must fail to compile. A
2576
- * caller that drops the `await` gets a `Promise` where bytes belong, which is a typecheck error;
2577
- * a caller that forgot to call a separate `awaitSaltSettled()` would silently send unsalted.
2578
- */
2579
- contentHashForSession(agentName: string, sessionId: string, content: Uint8Array): Promise<{
2580
- hash: Uint8Array;
2581
- alg: ContentHashAlg;
2582
- }>;
2583
- /**
2584
- * THIS SESSION'S UNSALTED HASH NEVER BECAME A MESSAGE — release the permanent closure it caused.
2585
- *
2586
- * ⚠️ REVIEW FINDING 3, and it is the opposite of the direction the flag was written to defend.
2587
- * `#hashedWithoutSalt` closes adoption at hash time, because for a session's first message the
2588
- * leaf is a network round trip away and every frontier count reads zero in between. Correct — but
2589
- * `cello_send` has three paths that compute the hash and then produce NOTHING: a sibling send
2590
- * holding the in-flight claim, the frontier moving under the send, and a non-durable send failure
2591
- * whose bytes go to a queue with no production consumer.
2592
- *
2593
- * In all three the session was permanently unsalted for a message that exists nowhere: no leaf, no
2594
- * wire, no copy at the peer. And B2b-2 made two of them MORE likely on a first message, because
2595
- * the five-second wait widens the very window the frontier re-check is watching.
2596
- *
2597
- * Only safe because it is called on paths that provably sent nothing. It deliberately does NOT
2598
- * clear `#unsaltedAnnounced`: the announcement was true when it fired and re-announcing on the
2599
- * retry would be the per-message flood Decision #15 forbids.
2600
- *
2601
- * ─── THREE OTHER SITES HASH AND MAY SEND NOTHING, AND ARE EXEMPT ON PURPOSE (pass 2, F8) ──────
2602
- *
2603
- * `daemon.ts`'s one-shot rejection and away reply, and `document-delivery-transport.ts`'s frame
2604
- * send, can all fail after hashing. None of them needs to abandon, and the reason is the same in
2605
- * each: every one is a REPLY. The inbound message that triggered it has already been leafed on
2606
- * this side, so `#saltAdoptionClosed` is already closed by the leaf count and would stay closed
2607
- * whatever this flag said. Calling abandon there would be a no-op that looks like a guarantee.
2608
- *
2609
- * Written down rather than left to be re-derived: the next reader's first question is why the
2610
- * list is three and not six.
2611
- */
2612
- abandonUnsaltedHash(agentName: string, sessionId: string): void;
1277
+
1278
+
2613
1279
  /**
1280
+ /**
2614
1281
  * TEST SEAM — put a session into the state a real one is in between announcing and being answered.
2615
1282
  *
2616
1283
  * Reaching that state for real needs a live counterparty connection, which the daemon-level
@@ -2618,30 +1285,6 @@ export declare class SessionNodeManager {
2618
1285
  * the same private registration the announce path calls, so it cannot drift from it.
2619
1286
  */
2620
1287
  markSaltAgreementPendingForTest(agentName: string, sessionId: string, boundMs?: number): void;
2621
- /**
2622
- * PUBLIC read of a session's agreed salt — `DOD-M15-SEALWIRE-1` part B2a.
2623
- *
2624
- * `content-park.ts` runs a SECOND, independent content-hash verifier (the park signature does not
2625
- * cover the envelope content, so it checks before `ingestReceivedContent` is ever reached), and it
2626
- * hardcoded `sha256`. It needs the salt to verify a v3 envelope, and it is outside this class.
2627
- *
2628
- * Read-only and cache-backed, so exposing it adds no way to CHANGE the salt from outside — the
2629
- * only writer remains `#persistSessionSalt`, behind the one-salt-per-session predicate.
2630
- */
2631
- /**
2632
- * IS THIS SESSION ACTUALLY PROTECTED BY ITS SALT RIGHT NOW — pass 2, F3.
2633
- *
2634
- * Distinct from `getSessionContentSalt`, which is POSSESSION and is what the verifier needs: a
2635
- * message parked before suspension was hashed under this salt and must still be checkable against
2636
- * it, so that accessor must keep answering with the bytes.
2637
- *
2638
- * This one answers the OPERATOR's question, and it is a different question. A suspended session
2639
- * holds a salt it will not use, so every hash it produces is `sha256` — reporting `contentSalted:
2640
- * true` there is not a gap, it is an affirmatively false security claim on the surface whose own
2641
- * comment reads *"a security property must not be inferable from a gap."* Same predicate
2642
- * `#saltForHashing` uses, so the flag cannot drift from the behaviour it describes.
2643
- */
2644
- isContentSaltActive(agentName: string, sessionId: string): boolean;
2645
1288
  getSessionContentSalt(agentName: string, sessionId: string): Uint8Array | null;
2646
1289
  /**
2647
1290
  * THE SALT, OR WHY THERE ISN'T ONE — `DOD-M15-INCLUSION-1`, fallback-finder finding 2.
@@ -2850,7 +1493,6 @@ export declare class SessionNodeManager {
2850
1493
  * locked out by the acceptance bound forever.
2851
1494
  */
2852
1495
  countEstablishedReceived(agentName: string, sessionId: string): number;
2853
- countReceivedMessages(agentName: string, sessionId: string): number;
2854
1496
  /** CC-5/F21: unilaterally mark a session locally-terminal ("abandoned") — retire its live node and
2855
1497
  * set the DB status, with NO bilateral seal (a dead half-open handshake has nothing to notarize).
2856
1498
  * Used by cello_close_session { force } and the dead-half-open reaper. Idempotent: a missing/already-
@@ -2858,5 +1500,4 @@ export declare class SessionNodeManager {
2858
1500
  * reviewer LOW: callers must not report a reap as successful when the write failed). */
2859
1501
  abandonSession(agentName: string, sessionId: string): Promise<boolean>;
2860
1502
  }
2861
- export {};
2862
1503
  //# sourceMappingURL=session-node-manager.d.ts.map