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