@cello-protocol/daemon 0.0.194 → 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 (103) 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/db-identity-store.d.ts +1 -0
  10. package/dist/db-identity-store.d.ts.map +1 -1
  11. package/dist/db-identity-store.js +19 -1
  12. package/dist/db-identity-store.js.map +1 -1
  13. package/dist/held-content.d.ts +141 -0
  14. package/dist/held-content.d.ts.map +1 -0
  15. package/dist/held-content.js +388 -0
  16. package/dist/held-content.js.map +1 -0
  17. package/dist/inbound-refusals.d.ts +283 -0
  18. package/dist/inbound-refusals.d.ts.map +1 -0
  19. package/dist/inbound-refusals.js +919 -0
  20. package/dist/inbound-refusals.js.map +1 -0
  21. package/dist/inbound-sessions.d.ts.map +1 -1
  22. package/dist/inbound-sessions.js +65 -27
  23. package/dist/inbound-sessions.js.map +1 -1
  24. package/dist/initiate-session-handler.d.ts.map +1 -1
  25. package/dist/initiate-session-handler.js +63 -0
  26. package/dist/initiate-session-handler.js.map +1 -1
  27. package/dist/outbound-sessions.d.ts.map +1 -1
  28. package/dist/outbound-sessions.js +108 -8
  29. package/dist/outbound-sessions.js.map +1 -1
  30. package/dist/park-recovery.d.ts +205 -0
  31. package/dist/park-recovery.d.ts.map +1 -0
  32. package/dist/park-recovery.js +600 -0
  33. package/dist/park-recovery.js.map +1 -0
  34. package/dist/refusal-notices.d.ts +196 -0
  35. package/dist/refusal-notices.d.ts.map +1 -0
  36. package/dist/refusal-notices.js +496 -0
  37. package/dist/refusal-notices.js.map +1 -0
  38. package/dist/refusal-reasons.d.ts +11 -0
  39. package/dist/refusal-reasons.d.ts.map +1 -1
  40. package/dist/refusal-reasons.js +18 -0
  41. package/dist/refusal-reasons.js.map +1 -1
  42. package/dist/registration-manager.d.ts.map +1 -1
  43. package/dist/registration-manager.js +126 -5
  44. package/dist/registration-manager.js.map +1 -1
  45. package/dist/registration-persistence.d.ts +18 -0
  46. package/dist/registration-persistence.d.ts.map +1 -1
  47. package/dist/registration-persistence.js +6 -0
  48. package/dist/registration-persistence.js.map +1 -1
  49. package/dist/session-assignment-parser.d.ts.map +1 -1
  50. package/dist/session-assignment-parser.js +23 -1
  51. package/dist/session-assignment-parser.js.map +1 -1
  52. package/dist/session-ceremony.d.ts +12 -7
  53. package/dist/session-ceremony.d.ts.map +1 -1
  54. package/dist/session-ceremony.js +12 -7
  55. package/dist/session-ceremony.js.map +1 -1
  56. package/dist/session-ephemerals.d.ts +271 -0
  57. package/dist/session-ephemerals.d.ts.map +1 -0
  58. package/dist/session-ephemerals.js +546 -0
  59. package/dist/session-ephemerals.js.map +1 -0
  60. package/dist/session-leaf-records.d.ts +132 -0
  61. package/dist/session-leaf-records.d.ts.map +1 -0
  62. package/dist/session-leaf-records.js +320 -0
  63. package/dist/session-leaf-records.js.map +1 -0
  64. package/dist/session-liveness.d.ts +135 -0
  65. package/dist/session-liveness.d.ts.map +1 -0
  66. package/dist/session-liveness.js +347 -0
  67. package/dist/session-liveness.js.map +1 -0
  68. package/dist/session-node-manager.d.ts +300 -1670
  69. package/dist/session-node-manager.d.ts.map +1 -1
  70. package/dist/session-node-manager.js +871 -9951
  71. package/dist/session-node-manager.js.map +1 -1
  72. package/dist/session-node-types.d.ts +933 -0
  73. package/dist/session-node-types.d.ts.map +1 -0
  74. package/dist/session-node-types.js +576 -0
  75. package/dist/session-node-types.js.map +1 -0
  76. package/dist/session-queries.d.ts +476 -0
  77. package/dist/session-queries.d.ts.map +1 -0
  78. package/dist/session-queries.js +1006 -0
  79. package/dist/session-queries.js.map +1 -0
  80. package/dist/session-records.d.ts +320 -0
  81. package/dist/session-records.d.ts.map +1 -0
  82. package/dist/session-records.js +792 -0
  83. package/dist/session-records.js.map +1 -0
  84. package/dist/session-salts.d.ts +390 -0
  85. package/dist/session-salts.d.ts.map +1 -0
  86. package/dist/session-salts.js +1457 -0
  87. package/dist/session-salts.js.map +1 -0
  88. package/dist/session-schema.d.ts +30 -0
  89. package/dist/session-schema.d.ts.map +1 -0
  90. package/dist/session-schema.js +786 -0
  91. package/dist/session-schema.js.map +1 -0
  92. package/dist/standing-receivers.d.ts +280 -0
  93. package/dist/standing-receivers.d.ts.map +1 -0
  94. package/dist/standing-receivers.js +1100 -0
  95. package/dist/standing-receivers.js.map +1 -0
  96. package/dist/transport-selector.d.ts +13 -0
  97. package/dist/transport-selector.d.ts.map +1 -1
  98. package/dist/transport-selector.js.map +1 -1
  99. package/dist/witness-alerts.d.ts +40 -0
  100. package/dist/witness-alerts.d.ts.map +1 -0
  101. package/dist/witness-alerts.js +102 -0
  102. package/dist/witness-alerts.js.map +1 -0
  103. package/package.json +5 -5
@@ -0,0 +1,933 @@
1
+ import type { RefusalKind } from "./refusal-reasons.js";
2
+ import { SessionConnectionGater } from "./session-connection-gater.js";
3
+ import { NodeAutoNatService, type CelloNode } from "@cello-protocol/transport";
4
+ import type { KeyProvider, LeafInput } from "@cello-protocol/crypto";
5
+ import { AgentRelayClient, type RelayAssignmentCarry, type RelayWitnessAlert } from "./session-relay-client.js";
6
+ import type { SealCarryLeaf } from "./session-seal-leaf-store.js";
7
+ export interface WitnessAlertNotice {
8
+ /** `${relayId}::${sessionIdHex}` — the dedupe key, not shown to anyone. */
9
+ key: string;
10
+ alert: RelayWitnessAlert;
11
+ occurrences: number;
12
+ /**
13
+ * When this witness first said it. Held SEPARATELY from `alert.observedAt`, because a later
14
+ * repeat can replace `alert` (a provable one supersedes an unprovable one) and that must not
15
+ * silently move the first sighting forward — an operator reading "first observed" wants to know
16
+ * when this started, not when the strongest version of it arrived.
17
+ */
18
+ firstObservedAt: number;
19
+ lastObservedAt: number;
20
+ }
21
+ /** SEC-1 / review M4: cap on the refused-parked-entry memo (remote-fed → must be bounded). */
22
+ /**
23
+ * How long the auto-acknowledge path holds its broker visiting connection AFTER submitting the seal
24
+ * leaf. The directory pushes `seal_verified` back ~60ms later (measured on GCP), so releasing on
25
+ * submit closed the stream before the frame it was opened for. Generous against 60ms, and bounded so
26
+ * a stalled seal cannot leak the connection.
27
+ */
28
+ export declare const AUTOACK_BROKER_GRACE_MS = 30000;
29
+ export declare const MAX_REFUSED_PARKED_ENTRIES = 512;
30
+ /**
31
+ * Per-session cap on remembered unreadable-algorithm frames (`DOD-M15-SEALWIRE-1` part B1).
32
+ *
33
+ * Fed by a REMOTE party — a peer on a newer build refuses every frame it sends — so it needs a
34
+ * bound for the same reason `MAX_REFUSED_PARKED_ENTRIES` does. Small on purpose: the entries exist
35
+ * only to reconcile a refusal with its park-route redelivery, which happens within seconds, and
36
+ * losing an old one costs a log line rather than correctness.
37
+ */
38
+ export declare const MAX_UNREADABLE_ALG_FRAMES = 64;
39
+ /**
40
+ * How many consumers' read positions a single refusal notice remembers.
41
+ *
42
+ * A consumer id is an IPC connection id, so every reconnect mints a new one and the read state would
43
+ * otherwise grow without bound in a durable table. Sixteen is far above the real number of windows
44
+ * attending one agent; past that the OLDEST reader is evicted, which costs at worst one repeated
45
+ * announcement to a window that has already gone.
46
+ */
47
+ export declare const MAX_REFUSAL_READERS = 16;
48
+ /**
49
+ * How many refusal notices one read returns, newest first.
50
+ *
51
+ * Review F3. The store is never emptied for an agent — a refusal records something that happened —
52
+ * and read state is per IPC connection, so a fresh window after a restart is entitled to every
53
+ * notice ever recorded. Uncapped, the answer to "why did this conversation go quiet?" was at the
54
+ * bottom of an archive. Capped and newest-first, the recent cause leads and the caller is TOLD the
55
+ * list was cut (`refusals_incomplete`), rather than the tail vanishing silently.
56
+ */
57
+ export declare const MAX_REFUSALS_PER_READ = 25;
58
+ /**
59
+ * How long the first send waits for an in-flight salt agreement before giving up on it —
60
+ * `DOD-M15-SEALWIRE-1` B2b-2 constraint 2.
61
+ *
62
+ * The agreement is ONE round trip on a stream that is already open, so a healthy exchange finishes
63
+ * in milliseconds; this bound is not sized for the normal case, it is sized for how long an operator
64
+ * should wait before their message goes out unsalted instead of not going out.
65
+ *
66
+ * Five seconds because both errors cost real things. Too short and a merely slow counterparty makes
67
+ * the session permanently unsalted for no reason — the decision is irreversible, so the bound should
68
+ * be generous relative to the round trip. Too long and the first message of every conversation with
69
+ * a peer on an older build visibly hangs, which is the failure a user actually notices and blames
70
+ * the product for. This is only ever paid by a session that HAS an agreement outstanding: a
71
+ * park-only session never starts one and never waits (constraint 5).
72
+ */
73
+ export declare const SALT_AGREEMENT_WAIT_MS = 5000;
74
+ /**
75
+ * How many times this side re-attempts its session-key announce, and the base delay between them.
76
+ *
77
+ * Bounded on purpose (review F5): the announce rides `onPeerConnect`, so a connection that stays up
78
+ * after one failed attempt would never produce another — encryption off for the life of the session,
79
+ * with guidance pointing at a reconnect that never comes.
80
+ */
81
+ export declare const SESSION_KEY_ANNOUNCE_RETRIES = 4;
82
+ export declare const SESSION_KEY_ANNOUNCE_RETRY_MS = 250;
83
+ /**
84
+ * WHY a session is hashing unsalted — review Finding 1, and this exists because one sentence was
85
+ * carrying five different situations.
86
+ *
87
+ * `#saltForHashing` returns null for five distinct upstream conditions, and the single guidance
88
+ * string asserted one of them: *"expected when your counterparty runs a build that predates the salt
89
+ * agreement… start a new session once they upgrade."* An operator whose counterparty was merely
90
+ * OFFLINE when they sent their first message — the most common case by far, since a parked first
91
+ * message unsalts the session by design — read that and went and told a fully up-to-date
92
+ * counterparty to upgrade.
93
+ *
94
+ * That is the `directory_unreachable` shape this project keeps re-learning: the message names the
95
+ * exit point and points at the wrong machine. A closed set of reasons with its own guidance per
96
+ * reason is the fix, and a closed set is what stops a sixth condition quietly inheriting a fifth's
97
+ * explanation.
98
+ */
99
+ export declare const UNSALTED_REASONS: {
100
+ /** No agreement was ever started — the counterparty has not connected. The park-only case. */
101
+ readonly NO_AGREEMENT_STARTED: "no_agreement_started";
102
+ /** We announced and they did not answer inside the bound. */
103
+ readonly AGREEMENT_TIMED_OUT: "agreement_timed_out";
104
+ /**
105
+ * They answered, terminally: they have already hashed content and can never adopt.
106
+ *
107
+ * ⚠️ THIS IS ONE OF FOUR THINGS THE PEER CAN SAY, AND IT USED TO BE ALL OF THEM — 006-CRYPTO
108
+ * finding 2. The wire frame carries WHICH reason, and `SaltAgreementFrame.adoptionClosed` is a
109
+ * label rather than a boolean precisely so a caller cannot say `closed` without saying why. That
110
+ * distinction reached the log and was then dropped one call before the operator, who was told
111
+ * "they had already hashed messages" no matter which of the four it was.
112
+ */
113
+ readonly PEER_CLOSED_ADOPTION: "peer_closed_adoption";
114
+ /**
115
+ * They answered terminally because their side could NOT READ its own frontier — local storage
116
+ * trouble on their machine, not a conversation that started early.
117
+ *
118
+ * Kept apart from `PEER_CLOSED_ADOPTION` because the remedies are opposites: a new session fixes
119
+ * the already-hashing case and does nothing at all for this one.
120
+ */
121
+ readonly PEER_FRONTIER_UNREADABLE: "peer_frontier_unreadable";
122
+ /** They answered terminally because the two sides could not converge — 006-CRYPTO finding 1. */
123
+ readonly PEER_EXCHANGE_STALLED: "peer_exchange_stalled";
124
+ /**
125
+ * They closed adoption naming a reason THIS build does not recognise.
126
+ *
127
+ * Deliberately non-asserting. The peer chooses this string, so the safe rendering states what we
128
+ * know — they declined, and the label is in the log line above — and asserts nothing about why.
129
+ * Guessing here is how an operator ends up asking a counterparty to change something that was
130
+ * never the problem.
131
+ */
132
+ readonly PEER_CLOSED_UNSPECIFIED: "peer_closed_unspecified";
133
+ /** The session was torn down while the first send was still waiting. */
134
+ readonly SESSION_TORN_DOWN: "session_torn_down";
135
+ /** This side already hashed, leafed, held or has in flight — adoption closed here. */
136
+ readonly ADOPTION_CLOSED_LOCALLY: "adoption_closed_locally";
137
+ /** They answered in time and OUR OWN write failed. Nothing about their build is involved. */
138
+ readonly OUR_PERSIST_FAILED: "our_persist_failed";
139
+ /**
140
+ * OUR announce never left this machine — review pass 2, F2. Reusing `AGREEMENT_TIMED_OUT` here
141
+ * told the operator *"your counterparty did not answer"* about a frame we never sent, which is the
142
+ * exact substitution this closed set was created to end, re-entering through the settle site the
143
+ * previous pass asked for.
144
+ */
145
+ readonly ANNOUNCE_FAILED: "our_announce_failed";
146
+ /**
147
+ * They answered, we stored it, and reading it back FAILED — review pass 2, F4. Distinct from
148
+ * `OUR_PERSIST_FAILED` because the two point at different log lines, and the guidance names one.
149
+ */
150
+ readonly OUR_READ_FAILED: "our_read_failed";
151
+ };
152
+ export type UnsaltedReason = (typeof UNSALTED_REASONS)[keyof typeof UNSALTED_REASONS];
153
+ /**
154
+ * What the operator should DO about each. TOTAL by construction — a `Record` over the union, so a
155
+ * new reason cannot be added without something for the reader to act on. Same shape, and the same
156
+ * reason, as `refusal-reasons.ts`: that file exists because a free-form `reason: string` let a new
157
+ * code slip past every test in its own guard file.
158
+ */
159
+ export declare const UNSALTED_GUIDANCE: Record<UnsaltedReason, string>;
160
+ /**
161
+ * DOD-M12B-ACK-1 — inbound `/cello/content/1.0.0` streams allowed per connection.
162
+ *
163
+ * libp2p's registrar default is 32, and it enforces the cap AFTER protocol negotiation has already
164
+ * answered, so exceeding it resets a stream the sender believes it just opened and the sender's
165
+ * next write fails with a message that names nothing. Content delivery is bursty by design (a
166
+ * document sweep opened 99 events in one second on a live daemon), and a slot stays occupied for
167
+ * the whole of ingest — which awaits SQLCipher and the security gateway. 32 is simply too close to
168
+ * normal traffic to be a safety limit.
169
+ *
170
+ * This is headroom, NOT the fix. The fix is that #handleContentStream now closes what it opens; a
171
+ * raised cap without that would only move the cliff. Kept finite on purpose: an unbounded cap would
172
+ * let a peer pin memory by opening streams it never uses, and the ceiling is what makes a future
173
+ * leak of this shape show up as a bounded failure instead of a heap.
174
+ */
175
+ export declare const CONTENT_MAX_INBOUND_STREAMS = 512;
176
+ /**
177
+ * DOD-M12B-ACK-1 — how long an inbound content stream may stay open after we have closed our end.
178
+ *
179
+ * Closing our write end retires the stream only once the PEER has closed its end too, so a peer
180
+ * that opens streams and never closes them still fills our inbound slots — a guard that runs only
181
+ * on the party it constrains is not a guard. After this window we reset it ourselves, which the
182
+ * muxer honours unilaterally.
183
+ *
184
+ * It cannot be zero: an immediate reset would land while a well-behaved sender is still inside its
185
+ * own `await stream.close()`, rejecting that close and turning every ordinary send into a park.
186
+ * The frame is already ingested long before this fires, so nothing waits on it.
187
+ */
188
+ export declare const CONTENT_STREAM_LINGER_MS = 30000;
189
+ /**
190
+ * DOD-M12B-SHUTDOWN-1 — how long one teardown step may block the daemon's exit.
191
+ *
192
+ * Chosen against the surface that complains: `cello logout` gives up and reports the daemon still
193
+ * running after 5 s, so a step that can burn longer than that guarantees the message the operator
194
+ * saw. Two steps at 2 s each stay inside it.
195
+ */
196
+ export declare const SHUTDOWN_STEP_DEADLINE_MS = 2000;
197
+ /**
198
+ * DOD-M12B-REDIAL-1 — the shortest gap between two re-dials of one session.
199
+ *
200
+ * Long enough that a burst of sends against a peer that is genuinely gone costs one dial rather
201
+ * than one per message; short enough that a peer coming back is picked up on the next thing the
202
+ * operator says. It is cleared on a successful dial, so it never delays a live counterparty.
203
+ */
204
+ export declare const REDIAL_COOLDOWN_MS = 15000;
205
+ /**
206
+ * DOD-CAP-SELF-HEAL-1 — how long an interrupted session keeps consuming a cap slot.
207
+ *
208
+ * ATTRIBUTION ALONE DID NOT FIX THIS, and the reason is worth keeping. Recording who ended a
209
+ * session only works for sessions ended after the recording started: every row written before the
210
+ * column existed is unlabelled, and an unlabelled row counts. So the operator's actual backlog —
211
+ * five finished conversations that were blocking two of their own agents — was untouched by it.
212
+ * Attribution can never clear history, and history is what fills a cap.
213
+ *
214
+ * Age can. An interrupted session nobody has touched for hours is debris, not a live obligation,
215
+ * and that is true whether our restart or their disconnect produced it.
216
+ *
217
+ * D18 SURVIVES BECAUSE THE ATTACK IS A RATE. The disconnect-evasion peer has to drop and reopen
218
+ * faster than this window to gain anything, so everything it churns is recent and everything it
219
+ * churns still counts. What ages out is the thing that was never an attack: a conversation that
220
+ * finished. An attacker who waits out the window to gain one slot per window is not evading the
221
+ * bound, they are obeying a slower one — and the global anti-swarm cap still applies on top.
222
+ *
223
+ * Two hours: comfortably longer than any churn worth attacking with, comfortably shorter than
224
+ * "yesterday's conversation still blocks me".
225
+ */
226
+ /**
227
+ * DOD-M12B-RESERVATION-RETRY-1 — how many times to re-ask for a refused reservation before saying
228
+ * the agent is undialable and stopping. Bounded because a reservation is scarce and a fleet that
229
+ * retries forever is how a relay is exhausted. With the 5-minute base and doubling, five retries
230
+ * span about 2.5 hours.
231
+ */
232
+ export declare const SR_RESERVATION_MAX_RETRIES = 5;
233
+ /** The relay peer id inside a `/…/p2p/<relay>/p2p-circuit/…` address. */
234
+ export declare const CIRCUIT_RELAY_ID: RegExp;
235
+ /**
236
+ * 032-RELAYSPREAD — the relays a node ACTUALLY HOLDS a circuit with, read off the addresses it is
237
+ * announcing. One entry per relay, deduped.
238
+ *
239
+ * This is the single definition of "a reservation is held", and it is deliberately the strictest
240
+ * one available: an ANNOUNCED circuit address. `start()` resolving is not enough — a relay out of
241
+ * reservation slots completes the handshake, grants nothing, and leaves a node that looks started
242
+ * and is dialable by nobody. Nor is a candidate address enough: a candidate is a relay we asked.
243
+ */
244
+ export declare function heldRelayIdsOf(node: CelloNode): string[];
245
+ /**
246
+ * DOD-M15-RELAYSLOTS-1 — how long an agent skips a relay that refused it for a relay-side fault.
247
+ *
248
+ * Ten minutes: long enough that the agent is not re-asking a relay that cannot serve it every time
249
+ * its receiver rebuilds, short enough that when someone fixes that relay the agent finds it again
250
+ * without needing its own restart. The fault is on somebody else's machine and nobody tells us when
251
+ * it is fixed, so this has to expire on its own.
252
+ */
253
+ export declare const RELAY_QUARANTINE_MS: number;
254
+ export declare const CAP_INTERRUPTED_TTL_MS: number;
255
+ /**
256
+ * DOD-CAP-SELF-HEAL-1 — what counts against a per-sender acceptance bound.
257
+ *
258
+ * `active` always. `interrupted` ONLY when the counterparty caused it.
259
+ *
260
+ * D18 is why `interrupted` has to count at all: a peer can flip a session to `interrupted` for free
261
+ * by dropping its stream, then open a fresh one, indefinitely. Those are theirs and still count.
262
+ *
263
+ * What broke was charging them for OURS. A daemon restart flips every live session to
264
+ * `interrupted`, nothing resolves them, and the reaper correctly refuses to take any with received
265
+ * content — so the bound became all-time instead of concurrent. Measured 2026-08-17: two of one
266
+ * operator's own agents could not open a session, because one held five finished conversations with
267
+ * the other against a stranger cap of three.
268
+ *
269
+ * NULL counts as the counterparty's. The column is new, so every pre-existing row is unlabelled,
270
+ * and the safe default for an anti-abuse bound is to count rather than to excuse.
271
+ */
272
+ export declare const CAP_COUNTS: (alias?: string) => string;
273
+ export declare const CAP_COUNT_SQL: (where: string) => string;
274
+ /** The cutoff an interrupted session must be newer than to still count. */
275
+ export declare const capStaleBefore: () => number;
276
+ /**
277
+ * DOD-M12B-ABANDON-NOTIFY-1 — what happened when we tried to tell the counterparty we hung up.
278
+ *
279
+ * A REASON, not a boolean. The three causes are not interchangeable, and collapsing them made the
280
+ * operator's guidance blame the network for a session this side had already torn down — which is
281
+ * the most common case, since force-abandon is largely used on `interrupted` sessions and those
282
+ * have no node left to send on.
283
+ *
284
+ * `told: true` means the bytes left this node. There is no acknowledgement, so it is not proof the
285
+ * far side acted — a counterparty on an older client does not understand the frame and keeps
286
+ * calling. The guidance says so rather than promising they will stop.
287
+ */
288
+ export interface AbandonNoticeResult {
289
+ told: boolean;
290
+ reason: "sent" | "no_local_node" | "send_failed";
291
+ }
292
+ /**
293
+ * DOD-M12B-ACK-1 — why a session is impaired, and what became of the content that revealed it.
294
+ *
295
+ * `cause` separates "your own message did not reach them" (`direct_send`) from "our acknowledgement
296
+ * to them did not go out" (`delivery_ack`) — on the second the operator sent nothing at all, so a
297
+ * surface that talks about their last send is describing a message they never wrote.
298
+ *
299
+ * `retained` is what makes it safe to advise. A parked or durably-queued message must NOT be
300
+ * resent — a resend takes a second canonical position, which is this milestone's founding defect.
301
+ * A LOST one must be, and `cello_send` has already said so. `unknown` means do not claim either.
302
+ */
303
+ export interface SessionImpairment {
304
+ /**
305
+ * `content_key` added by `029c` review F7: a send that failed because this machine had no content
306
+ * encryption key — never agreed, or gone between the preflight and the seal — is a LOCAL key
307
+ * fault, not a transport one. Reported as `direct_send` it sent the operator to inspect a
308
+ * connection that was working.
309
+ */
310
+ cause: "direct_send" | "delivery_ack" | "content_key";
311
+ retained: "parked" | "durable" | "lost" | "unknown";
312
+ }
313
+ /** Anti-drip-feed: cumulative RECEIVED bytes per session at the UNKNOWN tier (= the grid's UNKNOWN
314
+ * byte cap). Higher tiers get more (DEFAULT_TIER_BOUNDS); no tier is unbounded (INV-TIER-BOUND). */
315
+ export declare const ABUSE_MAX_SESSION_RECEIVED_BYTES: number;
316
+ /** Anti-drip-feed via many sessions: active sessions an UNKNOWN counterparty may hold open at once
317
+ * (= the grid's UNKNOWN per-sender cap). */
318
+ export declare const ABUSE_MAX_SESSIONS_PER_UNKNOWN_SENDER: number;
319
+ /** Anti-swarm: total active sessions from ALL UNKNOWN-tier counterparties combined, per agent. A
320
+ * scalar across the whole unknown pool — not per-tier — so it stays a standalone const. */
321
+ export declare const ABUSE_MAX_UNKNOWN_SESSIONS_GLOBAL = 50;
322
+ /**
323
+ * M7 DOD-SPINE-6 / MSG-001-3b: the inputs a session node needs to connect to the relay
324
+ * as the Structure-2 witness (relay endpoint from the FROST-signed assignment + the
325
+ * agent's K_local identity + the 16-byte session id). Optional on node creation: when
326
+ * absent (or connect fails), the session still works over the direct content path — the
327
+ * relay just doesn't witness the leaf yet.
328
+ */
329
+ export interface RelayConnectParams {
330
+ relayPeerId: string;
331
+ relayAddrs: string[];
332
+ keyProvider: KeyProvider;
333
+ senderPubkey: Uint8Array;
334
+ sessionIdBytes: Uint8Array;
335
+ /**
336
+ * FED-OPTIONB-SETUP-001 (Option B): the directory-signed relay assignment the client presents to its
337
+ * chosen relay (replaces the directory→relay dial). Absent for direct-mode and on the restart/persisted
338
+ * reconnect path (the relay already recorded the session at first establishment) — the client then just
339
+ * reconnects without re-recording.
340
+ */
341
+ assignment?: RelayAssignmentCarry;
342
+ }
343
+ /**
344
+ * DOD-PARK-DRAIN-1: why the parked-mailbox drain is being asked to run.
345
+ *
346
+ * `standing_receiver_ready` — a receiver was just installed, first time or rebuilt. The rebuild is
347
+ * the case that matters: content parks precisely because the relay link died, and the watchdog
348
+ * rebuild is that same event seen from the client side.
349
+ * `periodic_backstop` — nothing happened; this is the slow sweep that keeps a missed trigger from
350
+ * stranding content until someone restarts the daemon. Drains are deduped and delete-on-confirm,
351
+ * so an extra one costs a pull.
352
+ */
353
+ export type ParkedDrainReason = "standing_receiver_ready" | "periodic_backstop"
354
+ /**
355
+ * DOD-M12B-SESSION-SEED-1 (case B): a session that was interrupted has just been revived, so the
356
+ * content the counterparty parked while we had no node can finally be fetched. Reviving the node
357
+ * without draining leaves the operator looking at a healthy session with no new mail.
358
+ */
359
+ | "session_revived"
360
+ /**
361
+ * DOD-M12B-LEAF-TRIGGERS-FETCH-1: the relay told us a specific message exists — we hold its hash
362
+ * and its canonical sequence — and its plaintext never arrived on the direct path. Measured live
363
+ * 2026-08-18: the leaf landed in ONE second and the bytes took 102, because nothing connected the
364
+ * two facts.
365
+ */
366
+ | "witnessed_leaf_unresolved";
367
+ /**
368
+ * Adapter interface for session node creation. Allows test injection of a
369
+ * failing factory (AC-007) without touching the real libp2p stack.
370
+ * The adapter pattern is mandatory per outline.md constraints.
371
+ */
372
+ export interface ISessionNodeFactory {
373
+ createNode(config: SessionNodeConfig): Promise<CelloNode>;
374
+ }
375
+ export interface SessionNodeConfig {
376
+ sessionId: string;
377
+ connectionGater?: SessionConnectionGater;
378
+ /**
379
+ * DOD-M15-RELAYONLY-1: this agent has asked never to be directly reachable, so the factory must
380
+ * omit dcutr from the node's service set.
381
+ *
382
+ * ⚠️ NOT a duplicate of filtering the published addresses. Those two things stop DIFFERENT
383
+ * disclosures: the filter controls what the DIRECTORY is told, and dcutr talks to the peer
384
+ * directly. Its whole job is to upgrade a relayed connection into a direct one, and the inbound
385
+ * side starts the upgrade — which is exactly a standing receiver. Leaving it on means the agent
386
+ * routes over the relay precisely as asked and then hole-punches to a direct connection anyway,
387
+ * with every test still green because the leak happens inside libp2p after the assertions.
388
+ */
389
+ relayOnly?: boolean;
390
+ /**
391
+ * CELLO-M7-TRANSPORT-001: role of the node, forwarded to createNode to tune the
392
+ * libp2p service set (dcutr is included for 'session' dialers, omitted for the
393
+ * 'standing_receiver'). AutoNAT is present for both.
394
+ */
395
+ nodeType?: "session" | "standing_receiver";
396
+ /**
397
+ * M7-SESSION-003 (AC-005): keepalive ping interval for the session node so a
398
+ * counterparty that vanishes without a clean close is detected within a bounded
399
+ * window. Factories should forward this to createNode({ keepAliveIntervalMs }).
400
+ */
401
+ keepAliveIntervalMs?: number;
402
+ /**
403
+ * DOD-NAT-REACHABILITY-1: circuit-relay listen addresses
404
+ * (`<relay-multiaddr>/p2p/<relay-peer-id>/p2p-circuit`) the node should take
405
+ * reservations on. Each entry makes libp2p reserve a slot with that relay and
406
+ * advertise the relayed address via getMultiaddrs() — which is what makes a
407
+ * NAT'd standing receiver dialable at all. A dead relay in this list degrades
408
+ * (no reservation, WARN) — it never fails node creation.
409
+ */
410
+ circuitRelayListenAddrs?: string[];
411
+ /**
412
+ * DOD-M12B-SESSION-SEED-1: the 32-byte Ed25519 seed this node's transport identity is derived
413
+ * from, so a node that is torn down can be rebuilt at the SAME peer id.
414
+ *
415
+ * Without it libp2p generates the key internally and the id is unrecoverable — which is why a
416
+ * laptop-close session cannot come back today: a rebuilt node could dial the counterparty, but
417
+ * the counterparty holds an id that no longer exists.
418
+ *
419
+ * PER SESSION, NEVER PER AGENT. Each standing receiver mints its own; at handoff the seed becomes
420
+ * that session's and the replacement receiver mints a fresh one. That preserves the recorded
421
+ * 2026-04-11 rationale for ephemeral ids (a passive observer must not be able to correlate one
422
+ * agent's sessions across days) while making the id stable WITHIN the one session an observer can
423
+ * already correlate by watching the connection.
424
+ */
425
+ transportPrivateKey?: Uint8Array;
426
+ /**
427
+ * DOD-M12B-SESSION-SEED-1 (case B review HIGH-1): bind a ROUTABLE interface, not loopback.
428
+ *
429
+ * Ordinary session nodes dial OUT and need no inbound reachability, so the factory gives them
430
+ * `127.0.0.1`. But a session node that reached its role by PROMOTION inherited the standing
431
+ * receiver's `0.0.0.0` bind and its circuit-relay reservation — which is every session node in
432
+ * production. A REBUILT one does not inherit anything, so without this it comes back on loopback
433
+ * with no relay address: the peer id is preserved and the counterparty still cannot reach us,
434
+ * which is precisely the half of the promise revival exists to keep. Preserving the id only
435
+ * matters because the circuit address `/p2p/<relay>/p2p-circuit/p2p/<sessionPeerId>` embeds it —
436
+ * and that is the address that was not being taken.
437
+ */
438
+ inboundReachable?: boolean;
439
+ }
440
+ /**
441
+ * DOD-M12B-SESSION-SEED-1 — everything needed to bring one session back, and nothing else.
442
+ *
443
+ * Deliberately minimal: a revival re-establishes the SAME session with the SAME two parties, so it
444
+ * needs our identity and theirs and no more. Anything else added here would be state that has to be
445
+ * destroyed on the same edge, and each addition is another way to leave something open.
446
+ */
447
+ export interface SessionRevivalIdentity {
448
+ /** The 32-byte Ed25519 seed our session node's peer id derives from. Zeroed on destruction. */
449
+ seed: Uint8Array;
450
+ /** The counterparty's SESSION-layer peer id — the gater's allowed peer, and our dial target. */
451
+ counterpartyPeerId: string;
452
+ /** Their agent pubkey, for the rebuilt content handler's authentication check. */
453
+ counterpartyPubkey: string;
454
+ /**
455
+ * DOD-M12B-SESSION-SEED-1 — the counterparty's transport addresses, so a revived session can DIAL
456
+ * them. Measured live 2026-08-18: without this the rebuild succeeded in 1ms and the very next send
457
+ * failed and was LOST, because `#evictSessionCaches` clears `#counterpartyAddrs` on teardown and
458
+ * the re-dial then has nothing to dial. A revived node with no way to reach the other side is a
459
+ * session that is "active" and cannot speak.
460
+ */
461
+ counterpartyAddrs: string[];
462
+ }
463
+ export interface ActiveSessionEntry {
464
+ node: CelloNode;
465
+ agentName: string;
466
+ /** DOD-LOOP-1: the bare session id (hex). The map key is composite (agentName, sessionId), so
467
+ * iteration/logging reads the real session id from here, not from the map key. */
468
+ sessionId: string;
469
+ counterpartyPubkey: string;
470
+ gater: SessionConnectionGater;
471
+ correlationId: string;
472
+ /**
473
+ * DAEMON-004: the counterparty's SESSION-layer Peer ID — the dial target for
474
+ * the direct content stream (/cello/content/1.0.0). Set when the node is
475
+ * created (outbound: the gater-allowed peer) or accepted (inbound: initiator).
476
+ */
477
+ counterpartySessionPeerId: string;
478
+ /**
479
+ * CELLO-M7-TRANSPORT-001: the AutoNAT service wrapping this session node. Emits
480
+ * transport.autonat.result on each probe cycle; stopped when the node is torn
481
+ * down so its node subscription is released.
482
+ */
483
+ autoNat: NodeAutoNatService;
484
+ /**
485
+ * M7 DOD-SPINE-6 / MSG-001-3b: the agent's shared relay witness client (one stream per
486
+ * agent, multiplexing all that agent's sessions — the relay keys delivery by agent
487
+ * pubkey). The leaf submit path uses it on cello_send. Absent when the relay is
488
+ * unreachable — the direct content path still delivers.
489
+ */
490
+ relayClient?: AgentRelayClient;
491
+ /** The 16-byte session id, for relay leaf submission (the relay frame carries it). */
492
+ relaySessionIdBytes?: Uint8Array;
493
+ /** The `#relayClients` map key (agentName + relay peer id) — federation-safe teardown. */
494
+ relayClientKey?: string;
495
+ /**
496
+ * MSG-001-3b (2b): the session's relay endpoint (peer id + addrs) from the FROST assignment.
497
+ * Held so the content-park backstop can deposit to the SAME relay this session is witnessed by
498
+ * when direct delivery fails. In-memory only (not persisted — the startup-flush park is the
499
+ * separate schema concern; this live park has the endpoint in hand).
500
+ */
501
+ relayPeerId?: string;
502
+ relayAddrs?: string[];
503
+ /**
504
+ * DOD-M15-RELAYAUTH-1 review H1: the session's directory-signed assignment, held so the DIAL path
505
+ * can present it to whichever relay is about to be asked to allow the dial. Without it here, the
506
+ * dialer has no credential in hand at the moment it needs one and the gate refuses a legitimate
507
+ * dial. Self-authenticating, so holding it grants nothing that forging it would not already require.
508
+ */
509
+ relayAssignment?: RelayAssignmentCarry;
510
+ /**
511
+ * DOD-M15-RELAYAUTH-1 review M4: `#relayClients` keys for the ADDITIONAL relay clients this
512
+ * session opened beyond its witness relay — the relays that gate circuit dials. `relayClientKey`
513
+ * only ever names the witness one, so without this list these clients and their `#sessions`
514
+ * entries are never released and accumulate for the life of the daemon.
515
+ */
516
+ extraRelayClientKeys?: string[];
517
+ }
518
+ /** DAEMON-004: a piece of content received and verified, awaiting cello_receive. */
519
+ export interface ReceivedContentEntry {
520
+ contentHex: string;
521
+ senderPubkey: string;
522
+ sequenceNumber: number;
523
+ }
524
+ export type CreateSessionResult = {
525
+ ok: true;
526
+ peerId: string;
527
+ addrs: string[];
528
+ } | {
529
+ ok: false;
530
+ reason: string;
531
+ guidance: string;
532
+ };
533
+ /**
534
+ * DOD-COATTEND-1: how much of the arrival buffer is kept. Delivery reads the durable transcript
535
+ * now, so this buffer is only a recency hint (`peekLatestReceivedContentHex` for M8C-AWAY-1's
536
+ * [[WRAP]] check). Small, and stated: an unstated cap is a silent truncation, and no cap at all is
537
+ * the leak the old destructive read was accidentally preventing.
538
+ */
539
+ export declare const RECEIVED_BUFFER_CAP = 32;
540
+ /**
541
+ * M12-P12 (review F6): the outcome of one park-deposit attempt. A bare boolean conflated
542
+ * "this session has no relay to park to" with "the relay refused the deposit" — only the latter is
543
+ * worth queuing for a later retry, and queuing the former grows the durable queue with rows that
544
+ * can never drain.
545
+ */
546
+ /**
547
+ * M12-P13 (review MEDIUM-5): the outcome AND the cause. `standing_receiver_unavailable` is an
548
+ * exit-point label; `cause` is the four-way answer from `standingReceiverAbsenceReason()` that says
549
+ * WHICH state the receiver was in — the distinction M12-P12 added precisely because the label had
550
+ * misnamed this incident. It used to be logged here and then discarded at the mapping site, so the
551
+ * caller (and the operator reading `reason`) was sent to the transport when the blocker was the
552
+ * standing receiver.
553
+ */
554
+ /**
555
+ * ⚠️ `retryAfterMs` — `DOD-M15-RELAYABUSE-1` review MEDIUM-6. The guidance told operators the
556
+ * throttle clears *"in about a minute"*, which is a hardcoded guess about the RELAY's configurable
557
+ * window: a relay run at ten minutes makes that sentence a wrong promise. The real number was in
558
+ * hand two frames away and died here, which is the value-with-no-reader defect one layer further
559
+ * out from where it was just fixed twice.
560
+ */
561
+ export type ParkAttempt = {
562
+ outcome: "parked" | "refused" | "unconfigured";
563
+ cause?: string;
564
+ retryAfterMs?: number;
565
+ };
566
+ /**
567
+ * DOD-M12B-REVIVAL-BOUND-1 — how long an interrupted session stays revivable before it is closed.
568
+ *
569
+ * 24 hours. The bound exists because of Andre's 2026-08-18 tenet — *"leave nothing open that is no
570
+ * longer needed"* — and its value is set by the case it must not break: a laptop closed for the
571
+ * night. A window shorter than a night's sleep would abandon exactly the sessions case A/B exist to
572
+ * rescue, which is why this is not zero and not an hour.
573
+ *
574
+ * It is deliberately a plain constant and not a setting. A per-operator knob here is a knob that
575
+ * turns the guarantee off, and the guarantee is the security property, not a preference.
576
+ */
577
+ export declare const REVIVAL_WINDOW_MS: number;
578
+ /**
579
+ * DOD-M12B-REVIVAL-BOUND-1 — how often the revival bound is applied.
580
+ *
581
+ * Hourly. The window is 24 hours, so the worst-case overshoot is ~4% of the bound, and the pass is
582
+ * one DB walk with no network in it. Boot-only was the first build and it is not a bound at all: a
583
+ * daemon left up for a week never applies it, and a long-lived daemon is the normal case.
584
+ */
585
+ export declare const REVIVAL_BOUND_SWEEP_MS: number;
586
+ /** DOD-M12B-SESSION-SEED-1: per-relay deadline when a revived node asks for a circuit reservation.
587
+ * Three seconds — a relay that has a slot answers well inside it, and one that does not never
588
+ * answers at all (measured: 10,002ms and still waiting). */
589
+ export declare const REVIVE_RESERVATION_TIMEOUT_MS = 3000;
590
+ /** How many relays a revival will ask before settling for a plain node. Two: the worst case is then
591
+ * ~6s to a usable session, against a measured "never" for the unbounded form. */
592
+ export declare const REVIVE_RESERVATION_CANDIDATES = 2;
593
+ /**
594
+ * DOD-M12B-LEAF-TRIGGERS-FETCH-1 — how long the DIRECT path gets before we go and fetch content the
595
+ * relay has already told us about.
596
+ *
597
+ * Two seconds. The witness leaf and the plaintext are separate deliveries, and on a healthy session
598
+ * the direct content normally lands within milliseconds of the leaf — so fetching the instant a leaf
599
+ * arrives would put a relay round trip on the hot path of every message in every session, which is a
600
+ * self-inflicted load problem. Waiting forever is what cost 102 seconds. Two seconds is far above
601
+ * the healthy direct latency and far below anything a person would notice.
602
+ */
603
+ export declare const LEAF_FETCH_GRACE_MS = 2000;
604
+ /**
605
+ * DOD-M15-REFUSALTERMINAL-1 — the refusal reasons no retry can ever get past.
606
+ *
607
+ * MEASURED LIVE 2026-09-04: one message aimed at a conversation the counterparty had already
608
+ * closed, refused `session_committed` and re-fetched roughly twice a second for 62 hours across
609
+ * several daemon restarts — 232,056 refusal events on that one session and a 484 MB `daemon.log`.
610
+ *
611
+ * **WHY `session_committed` QUALIFIES.** A committed session carries a signature over its contents.
612
+ * Nothing can be appended to it by anyone — not the counterparty, not us — so there is no future
613
+ * state in which this content is accepted. That is the bar, and it is the whole bar.
614
+ *
615
+ * **⚠️ DO NOT ADD A REASON WITHOUT MEETING IT.** A reason wrongly called terminal silently drops a
616
+ * message that would have arrived on the next try, which is worse than the loop this set exists to
617
+ * end. The tempting ones and why each fails:
618
+ *
619
+ * - `content_hash_mismatch` — the fetch is BY CONTENT HASH, and a later fetch may retrieve a
620
+ * correct copy from a different relay. Retrying can succeed.
621
+ * - `sender_unresolved` — the sender may become resolvable when a profile arrives or a directory
622
+ * syncs. Retrying can succeed.
623
+ * - `session_orphaned` — `024-ORPHANTRIAGE` owns that path and decides its disposition.
624
+ * - `session_size_limit_exceeded` — the cap IS monotonic, but its bound is a setting, and an
625
+ * operator who raises it must be able to un-stick the conversation.
626
+ * - a transient screener block — transient is in its name.
627
+ */
628
+ /**
629
+ * ⚠️ NOT the same concept as `session-terminal-refusal.ts` (`DOD-MP-SESSION-RETIRE-1`), which is the
630
+ * RELAY terminally refusing one of OUR SENDS. This set is about INBOUND content this side will
631
+ * never accept. Two "terminal refusal" ideas live in this daemon and they point in opposite
632
+ * directions — review F9.
633
+ */
634
+ export declare const TERMINAL_REFUSAL_REASONS: ReadonlySet<string>;
635
+ /**
636
+ * DOD-M15-REFUSALTERMINAL-1 review F3 — how long a FAILED read of the terminal-refusal rows is
637
+ * backed off for, per session.
638
+ *
639
+ * A minute: long enough that a database throwing on every witnessed leaf produces one ERROR rather
640
+ * than one per message (the exact log growth this unit exists to end), short enough that a disk
641
+ * which recovers is noticed within a message or two rather than at the next restart.
642
+ */
643
+ export declare const TERMINAL_REFUSAL_READ_RETRY_MS = 60000;
644
+ /**
645
+ * DOD-M15-REFUSALTERMINAL-1 review F7 — how many terminally-refused content hashes are remembered
646
+ * per session.
647
+ *
648
+ * The counterparty chooses how many rows this table gets: one per distinct message aimed at a
649
+ * closed conversation, written even after the byte cap has stopped retaining evidence. 512 matches
650
+ * `MAX_REFUSED_PARKED_ENTRIES`, is far above any honest volume for a conversation that has ENDED,
651
+ * and bounds the table at (sessions × 512) small rows.
652
+ */
653
+ export declare const MAX_TERMINAL_REFUSALS_PER_SESSION = 512;
654
+ /**
655
+ * DOD-M15-REFUSALTERMINAL-1 — ONE refusal, TWO counts, and the NAMES are the fix.
656
+ *
657
+ * The inbox reported `times: 58` for a refusal that had fired tens of thousands of times, and the
658
+ * code was not wrong — the sentence describing it was. One number is "since you last dismissed this
659
+ * conversation" and the other is "ever". Neither may be called `times`, because that is the word an
660
+ * operator, and an agent deciding whether to escalate, reads as a lifetime figure.
661
+ */
662
+ export interface RefusalNotice {
663
+ sessionId: string;
664
+ reason: string;
665
+ kind: RefusalKind;
666
+ impact: string;
667
+ guidance: string;
668
+ /** Refusals of this reason on this session since the last `cello_dismiss` — which DELETES the
669
+ * notice row, restarting this counter at 1. Zero dismissals and it equals `timesTotal`. */
670
+ timesSinceDismissed: number;
671
+ /**
672
+ * Every refusal of this reason on this session, from the first one, untouched by dismissal.
673
+ *
674
+ * OMITTED, never guessed, when the notice is served from the in-memory fallback — that path
675
+ * exists precisely because the database write failed, so no durable total was ever written, and
676
+ * reporting the smaller number twice would put the original lie back with two names on it.
677
+ *
678
+ * MUTUALLY EXCLUSIVE with `timesTotalAtLeast`: a figure and a floor are different claims and
679
+ * must not share a name.
680
+ */
681
+ timesTotal?: number;
682
+ /**
683
+ * A LOWER BOUND on the lifetime count, for a row SEEDED at upgrade from a notice that already
684
+ * existed — review F1c.
685
+ *
686
+ * The seed is that notice's `count`, which is refusals since the last dismissal, so the true
687
+ * figure is at least this and may be far more: on the machine this unit was written for, the
688
+ * notice read 58 and the log held 232,056 refusal events. Reporting 58 as `timesTotal` would be
689
+ * the original defect with the new name on it. "At least 58" is true; "58" is not.
690
+ */
691
+ timesTotalAtLeast?: number;
692
+ repeat?: boolean;
693
+ }
694
+ /**
695
+ * DOD-M15-SEALWIRE-1 bullet 5, SENT half — our own authorship proof for a message we sent.
696
+ *
697
+ * Deliberately the SAME shape as the received half's `verifiedAuthorship`, because the transcript
698
+ * column pair is the same and a second shape would invite a second meaning. What differs is the
699
+ * ATTRIBUTION the row records: a sent row is `self_authored` (we PRODUCED this signature), never
700
+ * `verified_signature` (we CHECKED someone else's). Same bytes, different claim.
701
+ */
702
+ export interface SentAuthorship {
703
+ senderPubkey: Uint8Array;
704
+ senderSig: Uint8Array;
705
+ }
706
+ /**
707
+ * `DOD-M15-AUTHORSHIP-ABSENT-1` — the answer to "did the sender prove they wrote this message?",
708
+ * with the three NOT-YES cases kept apart because they are three different facts about the peer.
709
+ *
710
+ * The distinction that matters is between `refuted` and `unusable`, and getting it backwards is the
711
+ * trap this unit was written against:
712
+ *
713
+ * - `refuted` — a proof was supplied and it FAILED. That is evidence about the counterparty's
714
+ * key, and it freezes the session (`#freezeOnIdentityFailure`).
715
+ * - `unusable` — there is nothing here that could be checked against this message. Almost always
716
+ * a peer on an older build; possibly someone stripping the field. It refuses THE
717
+ * MESSAGE and leaves the session alone, because freezing on it would turn every
718
+ * version skew into an incident only a new session can clear.
719
+ *
720
+ * Absent is not a fourth case: a frame with no `sender_signature` never reaches the verifier, and
721
+ * its caller refuses it on the same path an `unusable` verdict takes. Missing, malformed and
722
+ * mismatched arrive at one outcome, which is the rule this whole class of defect comes from.
723
+ */
724
+ export type AuthorshipVerdict =
725
+ /** Signature verified against the key inside the signed bytes, and that key IS this session's counterparty. */
726
+ {
727
+ verdict: "verified";
728
+ senderPubkey: Uint8Array;
729
+ senderSig: Uint8Array;
730
+ }
731
+ /**
732
+ * 024-ORPHANTRIAGE: the signature VERIFIED and there was no session record to match it against.
733
+ *
734
+ * ⚠️ **DELIBERATELY A SEPARATE VERDICT, NOT A `verified` WITH A FLAG.** `verified` means *verified
735
+ * AND matched to this session's counterparty*; the transcript column it feeds is documented as
736
+ * exactly that ("verified, never claimed") and seal-time attribution rests on it. Returning an
737
+ * unmatched signer under that name would silently widen a proof the rest of the system reads as
738
+ * stronger. This one carries the weaker fact under its own name, and has exactly one consumer:
739
+ * the orphan branch, which needs to tell an operator whether anything at all is known about who
740
+ * sent a message for a conversation that does not exist here.
741
+ */
742
+ | {
743
+ verdict: "verified_unmatched";
744
+ senderPubkey: Uint8Array;
745
+ }
746
+ /** A proof was supplied and it is WRONG. Identity failure — fatal for the session. */
747
+ | {
748
+ verdict: "refuted";
749
+ reason: "bad_signature" | "signer_not_counterparty";
750
+ }
751
+ /** Nothing checkable arrived. Refuses the message; says nothing about the counterparty's key. */
752
+ | {
753
+ verdict: "unusable";
754
+ reason: string;
755
+ };
756
+ /**
757
+ * The `unusable` reason for a proof that is real and describes some OTHER message.
758
+ *
759
+ * ⚠️ NOT `"content_hash_mismatch"` — review §6. That string is already the refusal reason for the
760
+ * RECEIVER's own recompute failing (`ingestReceivedContent`), which is a tamper signal about the
761
+ * BODY. This one says the sender's signed claim is about different content. Two different failures
762
+ * sharing one name is a collision an operator grepping the log walks straight into.
763
+ */
764
+ export declare const AUTHORSHIP_CONTENT_HASH_MISMATCH = "authorship_hash_mismatch";
765
+ /**
766
+ * The `unusable` reason for a proof that is real, is by the right signer, describes this content —
767
+ * and was signed for a DIFFERENT conversation. A replay, not a forgery.
768
+ *
769
+ * Every one of those properties has been ESTABLISHED by the time this is returned; see the ordering
770
+ * note in `#verifyAuthorshipClaim`. An earlier version of this sentence was true of the intent and
771
+ * not of the code, because the check ran before the signature was verified.
772
+ */
773
+ export declare const AUTHORSHIP_SESSION_MISMATCH = "session_mismatch";
774
+ /**
775
+ * ─── 033-ACKEMIT: the three things that can be wrong with an ACKNOWLEDGEMENT ─────────────────────
776
+ *
777
+ * All three are `unusable` — the message is refused and the session lives. None of them is an
778
+ * identity fault: by the time any is returned the signature has verified, the signer IS this
779
+ * session's counterparty, and the claim is about this content in this conversation. What is wrong is
780
+ * what the claim says the sender had SEEN.
781
+ *
782
+ * They are three names and not one because the operator's next move differs for each, and because an
783
+ * investigator who cannot tell "your counterparty is on an older build" from "your counterparty
784
+ * acknowledged something you never sent" is looking at the wrong half of the problem.
785
+ *
786
+ * ⚠️ **NAME WHAT WAS OBSERVED, NEVER AN INFERRED CONCLUSION** (`DOD-M15-ERRSTRING-1`). Not one of
787
+ * these says "peer is malicious" — a mismatch is equally what a genuine software fault on the other
788
+ * side looks like, and an error that names a party the code did not check is this milestone's
789
+ * founding defect.
790
+ */
791
+ /**
792
+ * `DOD-M15-SELFCHAIN-1` — the sender's link to their OWN previous message names content this side
793
+ * did not receive from them as their last one.
794
+ *
795
+ * ⚠️ THIS IS THE ONE THAT MEANS THE ORDER OF THE CONVERSATION IS IN DISPUTE, and it is why it is
796
+ * named apart from the acknowledgement reasons above rather than folded in with them. Those say the
797
+ * sender is wrong about what WE said; this says they are wrong about what THEY said, which is the
798
+ * only thing they cannot be honestly mistaken about for long.
799
+ *
800
+ * ⚠️ AND IT STILL NAMES WHAT WAS OBSERVED, NEVER A CONCLUSION. The same signal is produced by a
801
+ * peer reordering a conversation and by a peer whose own chain record went out of step after a
802
+ * restart, and this side cannot tell them apart.
803
+ */
804
+ export declare const AUTHORSHIP_SELF_CHAIN_MISMATCH = "self_chain_mismatch";
805
+ /** The hash names content this side does not hold at the position the claim names. */
806
+ export declare const AUTHORSHIP_ACK_HASH_MISMATCH = "ack_hash_mismatch";
807
+ /** The hash names content this side has never held — not in the tree, and not held pending a gap. */
808
+ export declare const AUTHORSHIP_ACK_HASH_UNKNOWN = "ack_hash_unknown_content";
809
+ /**
810
+ * The set that routes an `unusable` reason to the acknowledgement wording rather than the generic
811
+ * one. A SET, not a string prefix test: a name-shaped check would silently adopt any future reason
812
+ * someone happens to call `ack_*`, and give it a sentence written for the two that are in it.
813
+ *
814
+ * (The count in this sentence has been wrong twice — it said "four" when the set held three, and
815
+ * the deletion of `ack_hash_absent` decremented the wrong number rather than recounting. It is a
816
+ * count of the SET below, so read that rather than trusting this.)
817
+ */
818
+ export type AckHashReason = typeof AUTHORSHIP_ACK_HASH_MISMATCH | typeof AUTHORSHIP_ACK_HASH_UNKNOWN;
819
+ export declare const ACK_HASH_REASONS: ReadonlySet<string>;
820
+ /**
821
+ * ⚠️ **THE REFUSALS THAT SAY THIS ARE THE ONES WHERE THE REFUSAL DOES NOT HOLD — NOT ALL OF THEM.**
822
+ *
823
+ * It said "EVERY INBOUND REFUSAL SAYS THIS", and review F5 measured that: fifteen call sites file a
824
+ * refusal notice in this file and four carry this sentence — the three encryption causes and the
825
+ * authorship one. The rest MUST NOT. A screened-out message is deliberately never delivered by any
826
+ * route, and a transcript write failure lost content that was already accepted; promising either
827
+ * operator a second chance would be a lie in the opposite direction. Rewritten rather than deleted,
828
+ * because "EVERY" read as a rule and the next person to add a refusal would have applied it blindly.
829
+ *
830
+ * Where it DOES apply: refusing an inbound frame sends back no delivery acknowledgement, so a CELLO
831
+ * sender's TTF backstop parks a copy in the relay mailbox — sealed to this agent's LONG-TERM
832
+ * IDENTITY key, not the session key — and recovery opens that one whatever went wrong with the
833
+ * direct copy.
834
+ *
835
+ * ⚠️ AND ONLY WHEN THIS MACHINE CAN OPEN ONE. See `REFUSAL_NO_OTHER_ROUTE`; the choice is made by
836
+ * `#mailboxRouteAvailable`, never by a caller writing the sentence into a literal.
837
+ */
838
+ export declare const REFUSAL_MAY_STILL_ARRIVE: string;
839
+ /**
840
+ * ⚠️ **THE OTHER ROUTE DOES NOT EXIST ON THIS MACHINE, AND SAYING SO IS THE POINT** — review F2.
841
+ *
842
+ * Opening a mailbox copy needs `KeyProvider.openContentSeal`, which is OPTIONAL: a threshold or
843
+ * signing-only provider does not implement it, and an agent loaded without a provider has none at
844
+ * all. `content-park.ts` refuses both — `signing_key_unavailable`, `cannot_unseal`.
845
+ *
846
+ * That is the SAME condition `CONTENT_ENCRYPTION_REASONS.NO_LOCAL_IDENTITY` reports. So on the one
847
+ * refusal that names a missing local identity, the reassurance above was false: both routes are shut
848
+ * by one cause, permanently, for every message on every session of that agent — and the operator was
849
+ * told to wait for a delivery that cannot happen. That is the H1 defect exactly: a refusal
850
+ * announcing a better outcome than it delivers.
851
+ */
852
+ export declare const REFUSAL_NO_OTHER_ROUTE: string;
853
+ /**
854
+ * Constant-shape byte equality for the two binding checks. Lifted rather than hand-rolled a second
855
+ * time — `seal-frontier-verify` has the same helper for the same comparison, and two copies of
856
+ * "are these the same bytes" is two things to keep true.
857
+ */
858
+ export declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
859
+ /**
860
+ * One row of a session's durable transcript as a reader sees it.
861
+ *
862
+ * DOD-M15-REFUSEDEVIDENCE-1: `'quarantined'` is a message that was received and REFUSED. Its `text`
863
+ * is the withholding statement, never the payload — the storage is complete and only the READ is
864
+ * redacted. `withheld` is present exactly on those rows so a caller can style or skip them without
865
+ * string-matching the statement.
866
+ */
867
+ export interface TranscriptEntry {
868
+ sequence: number;
869
+ direction: "sent" | "received" | "quarantined";
870
+ text: string;
871
+ createdAt: number;
872
+ refusalReason?: string;
873
+ withheld?: true;
874
+ /** Named to end in `guidance` on purpose: that suffix is what the vocabulary layer rewrites, so a
875
+ * CLI reader is told `cello quarantined` and an MCP reader `cello_quarantined`. */
876
+ withheld_guidance?: string;
877
+ }
878
+ /** A retained refused message, read back whole. The payload is handed out FRAMED, never raw. */
879
+ export interface QuarantinedRecord {
880
+ sequence: number;
881
+ reason: string;
882
+ content: Uint8Array;
883
+ senderPubkeyHex: string | null;
884
+ senderSig: Uint8Array | null;
885
+ attribution: string;
886
+ createdAt: number;
887
+ }
888
+ /**
889
+ * The relay's peer id out of a circuit listen address, or `null` if the address does not name one.
890
+ *
891
+ * Returns null rather than throwing or guessing: an unreadable address means we cannot tell which
892
+ * relay this candidate was for, and every caller has a real thing to do with that answer.
893
+ */
894
+ export declare function relayPeerIdOf(circuitAddr: string): string | null;
895
+ /**
896
+ * The Merkle leaf inputs for a seal carry: each leaf's `content_hash`, read out of the bytes its
897
+ * SENDER SIGNED (`structure1_cbor`), never out of an envelope field somebody else filled in.
898
+ *
899
+ * `null` when any leaf is unreadable — the caller must then answer "I cannot judge", never "we
900
+ * disagree". A decode failure is this daemon's limitation, not evidence against anyone.
901
+ *
902
+ * Canonical Structure 1 is `[version, content_hash, sender_pubkey, session_id, last_seen_seq,
903
+ * timestamp]`, plus `last_seen_hash` at index 6 on a v2 claim (020-ACKHASH). The content hash is at
904
+ * index 1 in both and is used AS the leaf hash (RFC 6962 §2.1 "hash" leaves are taken as-is), which
905
+ * is the domain the certified root lives in.
906
+ */
907
+ export declare function carryContentHashInputs(carry: readonly SealCarryLeaf[]): LeafInput[] | null;
908
+ /**
909
+ * How many of the counterparty's content hashes a session remembers for the self-link check.
910
+ *
911
+ * Bounded because a peer feeds it. 256 covers any realistic gap in our own copy of a conversation —
912
+ * a held message, one the inbound screen refused, one lost in flight — while keeping the memory a
913
+ * single session can cost fixed. Past the cap the check gets STRICTER, never looser.
914
+ */
915
+ export declare const SELF_CHAIN_MEMORY = 256;
916
+ /**
917
+ * The ONE definition of "unread" in this daemon: a RECEIVED transcript row whose sequence is
918
+ * beyond the agent's persisted read watermark. A constant, not a copy-pasted string, so the
919
+ * INBOX unread count and the DOD-CURSOR-DURABLE-1 read-before-write gate can never drift into
920
+ * disagreeing about what "unread" means — the gate deciding one thing while the inbox shows
921
+ * another is precisely the bug this shape prevents. Interpolated SQL only (no user input).
922
+ *
923
+ * ⚠️ IT LIVES HERE, NOT ON A CLASS, BECAUSE 036-GODFILE MOVED ITS READERS OUT. All three unread
924
+ * queries went to `session-records.ts`, so today there is exactly ONE importer. It is exported
925
+ * rather than left a `static #private` because a `#private` cannot cross a file boundary at all —
926
+ * so the moment a second reader appears in another module, the only way to give it the value would
927
+ * be to copy the string, which is precisely the drift the paragraph above forbids. Exported now so
928
+ * that nobody is ever tempted.
929
+ */
930
+ export declare const UNREAD_RECEIVED_WHERE = "\n t.direction = 'received'\n AND t.sequence > COALESCE(w.last_delivered_seq, -1)";
931
+ export declare const REFUSED_SESSIONS_CAP = 200;
932
+ export declare const TERMINAL_STATUSES = "('sealed','abandoned','seal_interrupted_pending','interrupted')";
933
+ //# sourceMappingURL=session-node-types.d.ts.map