@cello-protocol/daemon 0.0.171 → 0.0.173
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/agent-id-migration.d.ts.map +1 -1
- package/dist/agent-id-migration.js +2 -0
- package/dist/agent-id-migration.js.map +1 -1
- package/dist/close-session-handler.d.ts +7 -0
- package/dist/close-session-handler.d.ts.map +1 -1
- package/dist/close-session-handler.js +343 -89
- package/dist/close-session-handler.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +62 -3
- package/dist/daemon.js.map +1 -1
- package/dist/restart-seal-resolver.d.ts +92 -0
- package/dist/restart-seal-resolver.d.ts.map +1 -0
- package/dist/restart-seal-resolver.js +280 -0
- package/dist/restart-seal-resolver.js.map +1 -0
- package/dist/seal-certificate-pull.d.ts.map +1 -1
- package/dist/seal-certificate-pull.js +17 -2
- package/dist/seal-certificate-pull.js.map +1 -1
- package/dist/seal-coordinator.d.ts.map +1 -1
- package/dist/seal-coordinator.js +59 -6
- package/dist/seal-coordinator.js.map +1 -1
- package/dist/session-node-manager.d.ts +88 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +253 -4
- package/dist/session-node-manager.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-id-migration.d.ts","sourceRoot":"","sources":["../src/agent-id-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-id-migration.d.ts","sourceRoot":"","sources":["../src/agent-id-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAwK5E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACtD;AAyDD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,sBAAsB,CAyIxG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-id-migration.js","sourceRoot":"","sources":["../src/agent-id-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAGH,OAAO,EAAE,kBAAkB,EAAuB,MAAM,mBAAmB,CAAC;AAkB5E;;;;;;;;;;;;;GAaG;AACH,MAAM,aAAa,GAA2B;IAC5C;QACE,KAAK,EAAE,UAAU;QACjB,QAAQ,EAAE,QAAQ;QAClB,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;qBACD,CAAC
|
|
1
|
+
{"version":3,"file":"agent-id-migration.js","sourceRoot":"","sources":["../src/agent-id-migration.ts"],"names":[],"mappings":"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"}
|
|
@@ -60,6 +60,13 @@ export interface CloseSessionDeps {
|
|
|
60
60
|
pendingUnilateralWaiters: Map<string, (r: UnilateralResult) => void>;
|
|
61
61
|
/** The verified consortium roster, re-resolved at ceremony time. */
|
|
62
62
|
resolveConsortiumRoster: () => Promise<ConsortiumEndpoint[] | null>;
|
|
63
|
+
/**
|
|
64
|
+
* DOD-M12B-INTERRUPTED-ESCALATE-1: how long to wait for the directory's answer to a
|
|
65
|
+
* `seal_unilateral` request. Default 30_000. Injectable because this wait now sits on the
|
|
66
|
+
* INTERRUPTED close path too, and a suite that exercises it should not spend half a minute per
|
|
67
|
+
* case discovering that a stub never answers.
|
|
68
|
+
*/
|
|
69
|
+
unilateralTimeoutMs?: number;
|
|
63
70
|
handleSealInterruptedFlow: (sessionId: string, record: SessionRecord, correlationId: string, merkleRootAtInterruption: string, via?: SignalingManager) => Promise<SealFlowResult>;
|
|
64
71
|
handleActiveSealFlow: (sessionId: string, record: SessionRecord, correlationId: string) => Promise<ActiveSealResult>;
|
|
65
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"close-session-handler.d.ts","sourceRoot":"","sources":["../src/close-session-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"close-session-handler.d.ts","sourceRoot":"","sources":["../src/close-session-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
* SI-001: there is NO auto-seal on a session_interrupted receipt. The operator must close
|
|
11
11
|
* explicitly. A daemon that sealed on its own would notarize a conversation nobody chose to end.
|
|
12
12
|
*
|
|
13
|
+
* SI-001 IS NARROWER THAN IT READS, and the boundary matters (DOD-M12B-RESTART-SEAL-1, 2026-08-17).
|
|
14
|
+
* It governs a LIVE interruption — the relay says the counterparty vanished while the operator is at
|
|
15
|
+
* the keyboard and may still want to wait. `restart-seal-resolver.ts` seals a different population:
|
|
16
|
+
* sessions OUR OWN stop destroyed (`interrupted_by = 'local'`), which cannot be resumed because the
|
|
17
|
+
* transport keypairs died with the process. There the only alternative is force-abandon, which
|
|
18
|
+
* forfeits the receipt — so the choice is seal-or-abandon, not seal-or-resume. Nothing the
|
|
19
|
+
* counterparty caused is auto-sealed, and SI-001 holds unchanged for it.
|
|
20
|
+
*
|
|
13
21
|
* This was on my DO-NOT-CUT list. It has fifteen dependencies — a long list, but a KNOWN one, and
|
|
14
22
|
* that is the entire difference from a closure over 73 shared locals.
|
|
15
23
|
*/
|
|
@@ -25,6 +33,7 @@ import { classifyOnlineResult } from "./cross-node-negotiation.js";
|
|
|
25
33
|
import { validateSessionName } from "./session-name.js";
|
|
26
34
|
export function registerCloseSessionHandler(deps) {
|
|
27
35
|
const { resolveConsortiumRoster, handlers, logger, sessionNodeManager, getConnState, resolveCurrentAgent, NO_CURRENT_AGENT_RESPONSE, getKeyProvider, signalingFor, sendOver, waitForSignalingConnected, openVisitingConnection, runDiscoveryLookup, crossNodeBrokerBySession, sealKey, sealInterruptedInProgress, pendingSealWaiters, pendingUnilateralWaiters, handleSealInterruptedFlow, handleActiveSealFlow, recoverParkedContent, } = deps;
|
|
36
|
+
const UNILATERAL_TIMEOUT_MS = deps.unilateralTimeoutMs ?? 30_000;
|
|
28
37
|
/**
|
|
29
38
|
* How long the seal path waits to learn where the counterparty is homed.
|
|
30
39
|
*
|
|
@@ -197,6 +206,232 @@ export function registerCloseSessionHandler(deps) {
|
|
|
197
206
|
};
|
|
198
207
|
};
|
|
199
208
|
handlers.set("cello_close_session", closeSession);
|
|
209
|
+
/**
|
|
210
|
+
* DOD-M12B-INTERRUPTED-ESCALATE-1 — ask the directory to notarize with the counterparty absent.
|
|
211
|
+
*
|
|
212
|
+
* Extracted from the ACTIVE branch so the INTERRUPTED branch can reach it too. It could not
|
|
213
|
+
* before: every exit from the interrupted branch is a `return`, so the active block below it —
|
|
214
|
+
* the only place this code lived — was structurally unreachable for an interrupted session. The
|
|
215
|
+
* result was that an interrupted session could never obtain a receipt, **even when a human closed
|
|
216
|
+
* it by hand**: it reached `seal_interrupted_pending`, a mutually signed record nobody was ever
|
|
217
|
+
* asked to notarize, and stopped there. 26 sessions sat in that state for up to 10.5 days.
|
|
218
|
+
*
|
|
219
|
+
* The directory enforces the delivery-grace gate and answers `seal_unilateral_too_early` with the
|
|
220
|
+
* time remaining, which is returned as `retry_after_seconds` so a caller that can wait does not
|
|
221
|
+
* have to be a human reading a sentence.
|
|
222
|
+
*/
|
|
223
|
+
const escalateToUnilateralSeal = async (record, sessionId, escalation, correlationId,
|
|
224
|
+
/**
|
|
225
|
+
* Refuse locally when the carry cannot possibly verify, instead of spending 30 s to be told
|
|
226
|
+
* nothing. ON for the two paths `submitAndEscalate` drives — the interrupted close and the
|
|
227
|
+
* `seal_interrupted_pending` close — and deliberately OFF for the ACTIVE one. Those two are new
|
|
228
|
+
* and are what the restart-seal resolver drives automatically for every orphan. The ACTIVE path's
|
|
229
|
+
* behaviour is left exactly as it was — it has shipped and been exercised for milestones, and
|
|
230
|
+
* a pre-flight refusal there could turn a seal that works today into one that does not.
|
|
231
|
+
*/
|
|
232
|
+
opts = { refuseOnUnusableCarry: false }) => {
|
|
233
|
+
let resolveUni;
|
|
234
|
+
const uniP = new Promise((r) => { resolveUni = r; });
|
|
235
|
+
pendingUnilateralWaiters.set(sealKey(record.agent_name, sessionId), resolveUni);
|
|
236
|
+
// FED-OPTIONB-SEAL-001 (Option B): carry the full leaf chain (both parties) + the relay receipts so
|
|
237
|
+
// the directory rebuilds + verifies the tree OFFLINE — no directory→relay getSealLeaves dial. The
|
|
238
|
+
// store is keyed by the agent's K_local pubkey (the same key the relay client recorded under).
|
|
239
|
+
const sealAgentKp = getKeyProvider(record.agent_name);
|
|
240
|
+
const sealAgentPubkeyHex = sealAgentKp ? Buffer.from(await sealAgentKp.getPublicKey()).toString("hex") : "";
|
|
241
|
+
// NAMED SEPARATELY, because without it this falls through to an empty carry and answers
|
|
242
|
+
// `seal_carry_empty` — "the relay released the session, the receipt is no longer obtainable",
|
|
243
|
+
// which is TERMINAL in the restart-seal resolver. A local agent that is not loaded would have
|
|
244
|
+
// been written off as permanent relay-side loss, with a durable give-up row to match.
|
|
245
|
+
if (!sealAgentPubkeyHex) {
|
|
246
|
+
pendingUnilateralWaiters.delete(sealKey(record.agent_name, sessionId));
|
|
247
|
+
return {
|
|
248
|
+
ok: false,
|
|
249
|
+
reason: "seal_agent_key_unavailable",
|
|
250
|
+
guidance: "This agent's identity key is not loaded in this daemon, so the leaf chain a notarization is verified from cannot be read. Start the agent (cello_start_agent) and retry cello_close_session. Nothing about the session is lost.",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const sealCarry = sessionNodeManager.getSealCarry(sealAgentPubkeyHex, sessionId);
|
|
254
|
+
const seal_leaves = sealCarry.map((l) => ({
|
|
255
|
+
sequence_number: l.sequenceNumber,
|
|
256
|
+
leaf_kind: l.leafKind,
|
|
257
|
+
structure2_cbor: l.structure2Cbor,
|
|
258
|
+
structure1_cbor: l.structure1Cbor,
|
|
259
|
+
// Relay receipt (present only for the present party's OWN leaves — the seq-pinning teeth).
|
|
260
|
+
relay_id: l.relayId,
|
|
261
|
+
relay_timestamp: l.relayTimestamp,
|
|
262
|
+
relay_signature: l.relaySignatureHex ? new Uint8Array(Buffer.from(l.relaySignatureHex, "hex")) : undefined,
|
|
263
|
+
}));
|
|
264
|
+
// REFUSE LOCALLY FOR A LOCALLY-KNOWABLE FAILURE, rather than spending 30 s to be told nothing.
|
|
265
|
+
//
|
|
266
|
+
// Every directory-side refusal — `unilateral_leaves_unavailable`, `unilateral_chain_noncontiguous`,
|
|
267
|
+
// `unilateral_own_leaf_unwitnessed`, `unilateral_receipt_invalid`, `unilateral_root_unverifiable`,
|
|
268
|
+
// `unilateral_seal_leaf_invalid`, and the already-sealed dedup — is a bare `return` that sends
|
|
269
|
+
// no frame. All seven reach the caller as `seal_unilateral_timeout`, which names our own wait:
|
|
270
|
+
// the one thing that was working. That is this milestone's founding error-fidelity defect,
|
|
271
|
+
// reproduced, and the restart-seal resolver now hits it automatically for every orphan.
|
|
272
|
+
//
|
|
273
|
+
// Two of the causes are checkable here, from the carry we are about to send, and they are the
|
|
274
|
+
// dominant ones. The directory still re-checks everything — this refuses earlier and by name,
|
|
275
|
+
// it does not become the authority.
|
|
276
|
+
if (opts.refuseOnUnusableCarry && seal_leaves.length === 0) {
|
|
277
|
+
pendingUnilateralWaiters.delete(sealKey(record.agent_name, sessionId));
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
reason: "seal_carry_empty",
|
|
281
|
+
guidance: "This side holds no relay-witnessed leaves for the session, so there is nothing the directory could rebuild the record from. That usually means the relay dropped the session (it releases one 24 hours after the last message) before it was closed. cello_transcript still shows what was said; a notarized receipt is no longer obtainable.",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const sequences = seal_leaves.map((l) => l.sequence_number).sort((a, b) => a - b);
|
|
285
|
+
const contiguousFromOne = sequences.every((n, i) => n === i + 1);
|
|
286
|
+
if (opts.refuseOnUnusableCarry && !contiguousFromOne) {
|
|
287
|
+
pendingUnilateralWaiters.delete(sealKey(record.agent_name, sessionId));
|
|
288
|
+
return {
|
|
289
|
+
ok: false,
|
|
290
|
+
reason: "seal_carry_noncontiguous",
|
|
291
|
+
guidance: `The record this side holds has a gap: the relay assigned sequences ${sequences.join(", ")}, and a unilateral seal requires an unbroken chain from 1. A message the relay witnessed never reached this daemon. Try again once it arrives (cello_receive drains what the relay still holds); if it never does, only a force-abandon can end the session, and that produces no receipt.`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
// A THIRD locally-knowable cause: a carry that already holds TWO of our SEAL ctrl leaves. Those
|
|
295
|
+
// sessions exist right now — the one-shot submit mark has always been in memory, so any close
|
|
296
|
+
// in flight across a restart could post a second before the durable recovery shipped. The
|
|
297
|
+
// directory refuses them with `unilateral_seal_leaf_invalid`, silently, so each one currently
|
|
298
|
+
// burns five resolver attempts and 30 s apiece before being reported as a directory timeout.
|
|
299
|
+
// JUDGED FIRST, because it is the PERMANENT one. Two of our own ctrl leaves can never be
|
|
300
|
+
// notarized by any directory, and a carry holding those plus the counterparty's would otherwise
|
|
301
|
+
// match the bilateral check below and be answered "wait, a better receipt is coming" — for a
|
|
302
|
+
// session where nothing is coming, and where the resolver would then spend five attempts
|
|
303
|
+
// instead of giving up once with the truthful reason.
|
|
304
|
+
const ownCtrl = sealCarry.filter((l) => l.leafKind === 0x02 && l.senderPubkeyHex === sealAgentPubkeyHex);
|
|
305
|
+
if (opts.refuseOnUnusableCarry && ownCtrl.length > 1) {
|
|
306
|
+
pendingUnilateralWaiters.delete(sealKey(record.agent_name, sessionId));
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
reason: "seal_carry_duplicate_own_ctrl_leaf",
|
|
310
|
+
guidance: `This session's record holds ${ownCtrl.length} SEAL control leaves from this side where it may hold exactly one, so no directory can notarize it. That happens when a close was interrupted mid-flight and a later close posted a second leaf. The conversation itself is intact — cello_transcript shows it — but a notarized receipt is no longer obtainable, and only a force-abandon can end the session.`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// WHAT THIS ACTUALLY CHECKS, and why it is deliberately NARROWER than the directory's rule.
|
|
314
|
+
// The directory refuses unless there is exactly ONE ctrl leaf (`ctrlLeaves.length !== 1`). This
|
|
315
|
+
// refuses only the case it can name usefully: more than one, from more than one SENDER — ours
|
|
316
|
+
// plus the counterparty's. Two cases the directory still refuses and this does NOT pre-empt:
|
|
317
|
+
// zero ctrl leaves — our own relay receipt may simply not have landed yet, so
|
|
318
|
+
// refusing here would be a FALSE refusal on a healthy session.
|
|
319
|
+
// >1, all authored by THEM — not ours to explain, and not actionable by this operator.
|
|
320
|
+
// The case it does catch is the responder-side normal: the relay reads two senders' leaves as
|
|
321
|
+
// both parties having posted and starts a full BILATERAL seal — a better receipt than a
|
|
322
|
+
// unilateral one — while this side would fire a unilateral request the directory refuses
|
|
323
|
+
// silently, costing a 30-second wait to be told nothing.
|
|
324
|
+
//
|
|
325
|
+
// ORDER MATTERS: the duplicate-own-leaf check above runs FIRST. A carry holding two of OUR
|
|
326
|
+
// leaves plus the counterparty's matches both predicates, and that session is permanently
|
|
327
|
+
// unsealable — telling the operator to wait for a bilateral seal that can never land would be
|
|
328
|
+
// worse than the silence this replaces.
|
|
329
|
+
const allCtrl = sealCarry.filter((l) => l.leafKind === 0x02);
|
|
330
|
+
if (opts.refuseOnUnusableCarry && allCtrl.length > 1 && new Set(allCtrl.map((l) => l.senderPubkeyHex)).size > 1) {
|
|
331
|
+
pendingUnilateralWaiters.delete(sealKey(record.agent_name, sessionId));
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
reason: "seal_carry_bilateral_in_progress",
|
|
335
|
+
guidance: "Both parties have posted their SEAL leaf, so the relay has what it needs to notarize this BILATERALLY — a better receipt than a unilateral one, and asking for a unilateral seal now would be refused. Wait for the seal to land (cello_sessions shows the status) and read the receipt with cello_sealed_receipt.",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const sent = await sendOver(record.agent_name, {
|
|
339
|
+
type: "seal_unilateral",
|
|
340
|
+
session_id: new Uint8Array(Buffer.from(sessionId, "hex")),
|
|
341
|
+
reported_root: new Uint8Array(Buffer.from(escalation.reportedRootHex, "hex")),
|
|
342
|
+
reported_seq: escalation.sequenceNumber,
|
|
343
|
+
seal_leaves,
|
|
344
|
+
});
|
|
345
|
+
if (!sent.ok) {
|
|
346
|
+
pendingUnilateralWaiters.delete(sealKey(record.agent_name, sessionId));
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
reason: "seal_unilateral_send_failed",
|
|
350
|
+
guidance: "The unilateral seal request could not be sent to the directory. Check the directory connection (cello status) and retry cello_close_session.",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
let uniTimer;
|
|
354
|
+
const uniTimeoutP = new Promise((r) => {
|
|
355
|
+
uniTimer = setTimeout(() => r({ ok: false, reason: "seal_unilateral_timeout" }), UNILATERAL_TIMEOUT_MS);
|
|
356
|
+
});
|
|
357
|
+
const uniResult = await Promise.race([uniP, uniTimeoutP]);
|
|
358
|
+
clearTimeout(uniTimer);
|
|
359
|
+
pendingUnilateralWaiters.delete(sealKey(record.agent_name, sessionId));
|
|
360
|
+
if (uniResult.ok) {
|
|
361
|
+
logger.info("session.seal.completed", { sessionId, sealedRoot: uniResult.sealedRootHex, role: "unilateral", correlationId });
|
|
362
|
+
crossNodeBrokerBySession.delete(`${record.agent_name}:${sessionId}`); // Fix #1 review: evict on terminal seal success (a FAILED close keeps the entry so a retry can still reconnect).
|
|
363
|
+
// M8B FINDING-3 (cascade-2): return the legibility certificate inline — same shape as the
|
|
364
|
+
// bilateral close (AC-006) — so the operator gets the receipt on the surface that proves
|
|
365
|
+
// the seal, not just a bare root. It is also persisted for cello_get_sealed_receipt.
|
|
366
|
+
return {
|
|
367
|
+
ok: true,
|
|
368
|
+
sealed_root: uniResult.sealedRootHex,
|
|
369
|
+
seal_type: "unilateral",
|
|
370
|
+
...(uniResult.legibility !== undefined ? { legibility: uniResult.legibility } : {}),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (uniResult.reason === "seal_unilateral_too_early") {
|
|
374
|
+
// F20: tell the operator WHEN the unilateral seal becomes available, not just "later".
|
|
375
|
+
const when = typeof uniResult.remainingSeconds === "number"
|
|
376
|
+
? `A unilateral seal becomes available in ~${uniResult.remainingSeconds}s. `
|
|
377
|
+
: "";
|
|
378
|
+
return {
|
|
379
|
+
ok: false,
|
|
380
|
+
reason: "seal_counterparty_pending",
|
|
381
|
+
// DOD-M12B-RESTART-SEAL-1: the deadline as a NUMBER, not only inside the sentence. The
|
|
382
|
+
// directory tells us exactly when this becomes allowed and, until now, the only consumer of
|
|
383
|
+
// that fact was English prose asking a human to come back in eleven minutes.
|
|
384
|
+
...(typeof uniResult.remainingSeconds === "number"
|
|
385
|
+
? { retry_after_seconds: uniResult.remainingSeconds }
|
|
386
|
+
: {}),
|
|
387
|
+
guidance: `Your SEAL leaf is recorded, but the counterparty has not closed and the directory's delivery-grace window has not yet elapsed, so a unilateral seal is not yet allowed. ${when}Retry cello_close_session after the grace period, or once the counterparty closes.`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
reason: uniResult.reason,
|
|
393
|
+
guidance: "The unilateral seal did not complete (the directory could not verify the reported root, or the certificate failed verification). Confirm your messages reached the relay (cello_sessions) before retrying cello_close_session.",
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Post this side's SEAL ctrl leaf (or recover the one a previous run already posted) and ask the
|
|
398
|
+
* directory to notarize. `null` means there was nothing to escalate with — the caller keeps its
|
|
399
|
+
* own result rather than inventing a failure.
|
|
400
|
+
*
|
|
401
|
+
* SHARED so a third copy cannot appear. It has two callers: the interrupted close, and the
|
|
402
|
+
* `seal_interrupted_pending` close that used to be refused outright.
|
|
403
|
+
*/
|
|
404
|
+
const submitAndEscalate = async (record, sessionId, correlationId) => {
|
|
405
|
+
const submitted = await sessionNodeManager.submitSealLeaf(record.agent_name, sessionId, correlationId);
|
|
406
|
+
if (submitted.ok || submitted.reason === "responder_seal_already_submitted") {
|
|
407
|
+
logger.info("session.interrupted.seal.leaf.submitted", {
|
|
408
|
+
agentName: record.agent_name, sessionId, correlationId,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
logger.warn("session.interrupted.seal.leaf.submit_failed", {
|
|
413
|
+
agentName: record.agent_name, sessionId, reason: submitted.reason, correlationId,
|
|
414
|
+
impact: "both sides hold a signed commitment, but no notarization was requested — this session has no receipt and cannot get one once the relay drops it",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
// The root and sequence the directory verifies against. `responder_seal_already_submitted`
|
|
418
|
+
// carries them from the FIRST submit — from THIS process's memory, or recovered from the
|
|
419
|
+
// durable carry after a restart — so a repeat close can still escalate (M8B FINDING-1).
|
|
420
|
+
const escalation = submitted.ok
|
|
421
|
+
? { reportedRootHex: submitted.reportedRootHex, sequenceNumber: submitted.sequenceNumber }
|
|
422
|
+
: typeof submitted.reportedRootHex === "string" && typeof submitted.sequenceNumber === "number"
|
|
423
|
+
? { reportedRootHex: submitted.reportedRootHex, sequenceNumber: submitted.sequenceNumber }
|
|
424
|
+
: null;
|
|
425
|
+
// `submitted.ok` always yields an escalation above, so this is only ever the failure reason.
|
|
426
|
+
// Kept as a total expression rather than a cast: if submitSealLeaf's contract is ever loosened,
|
|
427
|
+
// this names the new case instead of building `{reportedRootHex: undefined}` and throwing at
|
|
428
|
+
// `Buffer.from` several frames away.
|
|
429
|
+
if (!escalation)
|
|
430
|
+
return { escalated: false, reason: submitted.ok ? "submit_returned_no_root" : submitted.reason };
|
|
431
|
+
return await escalateToUnilateralSeal(record, sessionId, escalation, correlationId, {
|
|
432
|
+
refuseOnUnusableCarry: true,
|
|
433
|
+
});
|
|
434
|
+
};
|
|
200
435
|
const runClose = async (params, connectionId) => {
|
|
201
436
|
const connState = getConnState(connectionId);
|
|
202
437
|
// CC-3 / M8C-AUTOSTART-1 F18: explicit { agent } > this connection's current > sole online agent.
|
|
@@ -508,22 +743,59 @@ export function registerCloseSessionHandler(deps) {
|
|
|
508
743
|
// No waiter is registered and nothing is awaited: the counterparty may not close for hours, and
|
|
509
744
|
// blocking a close on that would be a worse lie than returning the honest pending status. When
|
|
510
745
|
// their leaf does land, the already-wired session_sealed listener records the certificate.
|
|
746
|
+
//
|
|
747
|
+
// DOD-M12B-INTERRUPTED-ESCALATE-1: submitting the leaf was never enough, and this is where
|
|
748
|
+
// the receipt actually gets earned. The relay notarizes only once BOTH parties have posted a
|
|
749
|
+
// SEAL ctrl leaf, and the responder never posts one — `inbound-seal-request.ts` persists its
|
|
750
|
+
// commitment, acks, and stops. So waiting for the relay round here is waiting for something
|
|
751
|
+
// that cannot happen. The escalation asks the directory to notarize with the counterparty
|
|
752
|
+
// ABSENT, which is precisely what a unilateral seal is for.
|
|
511
753
|
const notarize = async (result) => {
|
|
512
|
-
|
|
754
|
+
// WHEN IT IS ALLOWED TO ESCALATE, and this is a trust decision, not a retry policy:
|
|
755
|
+
// result.ok — both sides signed the same root. Agreed.
|
|
756
|
+
// seal_interrupted_counterparty_unavailable — they never answered. The exact case the
|
|
757
|
+
// unilateral seal exists for.
|
|
758
|
+
// NEVER after `seal_interrupted_rejected_by_counterparty`. A rejection means the two trees
|
|
759
|
+
// DISAGREE (leaf_count_mismatch, a diverging index). Notarizing our own root over their
|
|
760
|
+
// stated objection is the one thing a trust layer must not do, however stuck the session is.
|
|
761
|
+
const mayEscalate = result.ok || result.reason === "seal_interrupted_counterparty_unavailable";
|
|
762
|
+
if (!mayEscalate)
|
|
513
763
|
return result;
|
|
514
|
-
const
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
764
|
+
const uni = await submitAndEscalate(record, sessionId, correlationId);
|
|
765
|
+
if ("escalated" in uni) {
|
|
766
|
+
// THE REASON IS IN HAND — do not drop it. `result.ok` is true here (the bilateral
|
|
767
|
+
// commitment succeeded), so returning it bare makes the restart-seal resolver log
|
|
768
|
+
// `resolved` and dequeue a session that got NO notarization: the system reporting health
|
|
769
|
+
// it does not have. Same shape the pending branch was blocked for.
|
|
770
|
+
if (!result.ok)
|
|
771
|
+
return result;
|
|
772
|
+
return {
|
|
773
|
+
...result,
|
|
774
|
+
seal_receipt: "outstanding",
|
|
775
|
+
seal_pending_reason: uni.reason,
|
|
776
|
+
guidance: `The bilateral commitment is recorded, but this side could not post the SEAL leaf a notarization is requested with: ${uni.reason}. ` +
|
|
777
|
+
`That is usually local and temporary — an agent that is not started yet (cello_start_agent), or a relay this daemon cannot currently reach (cello_status). ` +
|
|
778
|
+
`Retry cello_close_session once the daemon reports healthy.`,
|
|
779
|
+
};
|
|
525
780
|
}
|
|
526
|
-
|
|
781
|
+
if (uni.ok)
|
|
782
|
+
return uni; // A REAL RECEIPT — the whole point of this line.
|
|
783
|
+
// BEST-EFFORT, and the commitment STANDS. Turning a successful commitment into a reported
|
|
784
|
+
// failure is exactly what sends an operator to `force: true`, which permanently forfeits the
|
|
785
|
+
// half they still hold — the comment above says so and it is still true. What changes is
|
|
786
|
+
// that the answer now says a receipt is OUTSTANDING rather than implying one exists, and
|
|
787
|
+
// carries the directory's own countdown so a caller that can wait does not have to guess.
|
|
788
|
+
logger.info("session.interrupted.seal.escalation.pending", {
|
|
789
|
+
agentName: record.agent_name, sessionId, reason: uni.reason, correlationId,
|
|
790
|
+
...(uni.retry_after_seconds !== undefined ? { retryAfterSeconds: uni.retry_after_seconds } : {}),
|
|
791
|
+
});
|
|
792
|
+
return {
|
|
793
|
+
...result,
|
|
794
|
+
seal_receipt: "outstanding",
|
|
795
|
+
seal_pending_reason: uni.reason,
|
|
796
|
+
...(uni.retry_after_seconds !== undefined ? { retry_after_seconds: uni.retry_after_seconds } : {}),
|
|
797
|
+
guidance: uni.guidance,
|
|
798
|
+
};
|
|
527
799
|
};
|
|
528
800
|
try {
|
|
529
801
|
// Pre-dial ONLY from memory. This costs nothing when the map is empty, so the seal waiter
|
|
@@ -541,8 +813,10 @@ export function registerCloseSessionHandler(deps) {
|
|
|
541
813
|
return await notarize(first);
|
|
542
814
|
}
|
|
543
815
|
sealBrokerConn = await openSealBrokerConnection(record.agent_name, sessionId, correlationId, { counterpartyPubkeyHex: counterparty });
|
|
816
|
+
// Escalate even here: discovery found nowhere to dial, which is the STRONGEST case for a
|
|
817
|
+
// unilateral seal, not a reason to give up on the receipt.
|
|
544
818
|
if (!sealBrokerConn)
|
|
545
|
-
return first;
|
|
819
|
+
return await notarize(first);
|
|
546
820
|
logger.info("session.seal.broker.retry_after_discovery", { agentName: record.agent_name, sessionId, correlationId });
|
|
547
821
|
// OVER THE VISITING CONNECTION. Dialling their node is only half of it — the request must
|
|
548
822
|
// be SENT there too. Sent on the home stream it reaches our own node, which holds no stream
|
|
@@ -725,79 +999,9 @@ export function registerCloseSessionHandler(deps) {
|
|
|
725
999
|
};
|
|
726
1000
|
}
|
|
727
1001
|
// SESSION-002 (DOD-SEAL): the counterparty did not co-close. Escalate to a UNILATERAL
|
|
728
|
-
// seal
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
let resolveUni;
|
|
732
|
-
const uniP = new Promise((r) => { resolveUni = r; });
|
|
733
|
-
pendingUnilateralWaiters.set(sessionId, resolveUni);
|
|
734
|
-
// FED-OPTIONB-SEAL-001 (Option B): carry the full leaf chain (both parties) + the relay receipts so
|
|
735
|
-
// the directory rebuilds + verifies the tree OFFLINE — no directory→relay getSealLeaves dial. The
|
|
736
|
-
// store is keyed by the agent's K_local pubkey (the same key the relay client recorded under).
|
|
737
|
-
const sealAgentKp = getKeyProvider(record.agent_name);
|
|
738
|
-
const sealAgentPubkeyHex = sealAgentKp ? Buffer.from(await sealAgentKp.getPublicKey()).toString("hex") : "";
|
|
739
|
-
const sealCarry = sealAgentPubkeyHex ? sessionNodeManager.getSealCarry(sealAgentPubkeyHex, sessionId) : [];
|
|
740
|
-
const seal_leaves = sealCarry.map((l) => ({
|
|
741
|
-
sequence_number: l.sequenceNumber,
|
|
742
|
-
leaf_kind: l.leafKind,
|
|
743
|
-
structure2_cbor: l.structure2Cbor,
|
|
744
|
-
structure1_cbor: l.structure1Cbor,
|
|
745
|
-
// Relay receipt (present only for the present party's OWN leaves — the seq-pinning teeth).
|
|
746
|
-
relay_id: l.relayId,
|
|
747
|
-
relay_timestamp: l.relayTimestamp,
|
|
748
|
-
relay_signature: l.relaySignatureHex ? new Uint8Array(Buffer.from(l.relaySignatureHex, "hex")) : undefined,
|
|
749
|
-
}));
|
|
750
|
-
const sent = await sendOver(record.agent_name, {
|
|
751
|
-
type: "seal_unilateral",
|
|
752
|
-
session_id: new Uint8Array(Buffer.from(sessionId, "hex")),
|
|
753
|
-
reported_root: new Uint8Array(Buffer.from(escalation.reportedRootHex, "hex")),
|
|
754
|
-
reported_seq: escalation.sequenceNumber,
|
|
755
|
-
seal_leaves,
|
|
756
|
-
});
|
|
757
|
-
if (!sent.ok) {
|
|
758
|
-
pendingUnilateralWaiters.delete(sessionId);
|
|
759
|
-
return {
|
|
760
|
-
ok: false,
|
|
761
|
-
reason: "seal_unilateral_send_failed",
|
|
762
|
-
guidance: "The unilateral seal request could not be sent to the directory. Check the directory connection (cello status) and retry cello_close_session.",
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
let uniTimer;
|
|
766
|
-
const uniTimeoutP = new Promise((r) => {
|
|
767
|
-
uniTimer = setTimeout(() => r({ ok: false, reason: "seal_unilateral_timeout" }), 30_000);
|
|
768
|
-
});
|
|
769
|
-
const uniResult = await Promise.race([uniP, uniTimeoutP]);
|
|
770
|
-
clearTimeout(uniTimer);
|
|
771
|
-
pendingUnilateralWaiters.delete(sessionId);
|
|
772
|
-
if (uniResult.ok) {
|
|
773
|
-
logger.info("session.seal.completed", { sessionId, sealedRoot: uniResult.sealedRootHex, role: "unilateral", correlationId });
|
|
774
|
-
crossNodeBrokerBySession.delete(`${record.agent_name}:${sessionId}`); // Fix #1 review: evict on terminal seal success (a FAILED close keeps the entry so a retry can still reconnect).
|
|
775
|
-
// M8B FINDING-3 (cascade-2): return the legibility certificate inline — same shape as the
|
|
776
|
-
// bilateral close (AC-006) — so the operator gets the receipt on the surface that proves
|
|
777
|
-
// the seal, not just a bare root. It is also persisted (above) for cello_get_sealed_receipt.
|
|
778
|
-
return {
|
|
779
|
-
ok: true,
|
|
780
|
-
sealed_root: uniResult.sealedRootHex,
|
|
781
|
-
seal_type: "unilateral",
|
|
782
|
-
...(uniResult.legibility !== undefined ? { legibility: uniResult.legibility } : {}),
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
if (uniResult.reason === "seal_unilateral_too_early") {
|
|
786
|
-
// F20: tell the operator WHEN the unilateral seal becomes available, not just "later".
|
|
787
|
-
const when = typeof uniResult.remainingSeconds === "number"
|
|
788
|
-
? `A unilateral seal becomes available in ~${uniResult.remainingSeconds}s. `
|
|
789
|
-
: "";
|
|
790
|
-
return {
|
|
791
|
-
ok: false,
|
|
792
|
-
reason: "seal_counterparty_pending",
|
|
793
|
-
guidance: `Your SEAL leaf is recorded, but the counterparty has not closed and the directory's delivery-grace window has not yet elapsed, so a unilateral seal is not yet allowed. ${when}Retry cello_close_session after the grace period, or once the counterparty closes.`,
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
return {
|
|
797
|
-
ok: false,
|
|
798
|
-
reason: uniResult.reason,
|
|
799
|
-
guidance: "The unilateral seal did not complete (the directory could not verify the reported root, or the certificate failed verification). Confirm your messages reached the relay (cello_sessions) before retrying cello_close_session.",
|
|
800
|
-
};
|
|
1002
|
+
// seal. The body now lives in escalateToUnilateralSeal so the INTERRUPTED branch can reach
|
|
1003
|
+
// it too — it never could, and that is why an interrupted session could not get a receipt.
|
|
1004
|
+
return await escalateToUnilateralSeal(record, sessionId, escalation, correlationId);
|
|
801
1005
|
}
|
|
802
1006
|
finally {
|
|
803
1007
|
// Fix #1: release the transient broker seal-connection (best-effort; the seal result stands).
|
|
@@ -812,11 +1016,61 @@ export function registerCloseSessionHandler(deps) {
|
|
|
812
1016
|
sealInterruptedInProgress.delete(sealKey(record.agent_name, sessionId));
|
|
813
1017
|
}
|
|
814
1018
|
}
|
|
815
|
-
//
|
|
1019
|
+
// DOD-M12B-PENDING-EXIT-1 — `seal_interrupted_pending` had NO exit, and the refusal below told
|
|
1020
|
+
// the operator it was "awaiting FROST notarization". It was not awaiting anything: nobody ever
|
|
1021
|
+
// requests that notarization. The relay stamps a chain only once BOTH parties have posted a
|
|
1022
|
+
// SEAL ctrl leaf, and the responder never posts one — `inbound-seal-request.ts` persists its
|
|
1023
|
+
// commitment, acks, and stops. Measured: 26 sessions idle in this status for 0.5 to 10.5 days.
|
|
1024
|
+
//
|
|
1025
|
+
// WHY THIS IS SAFE, stated accurately — the obvious answer is the wrong one. It is NOT that
|
|
1026
|
+
// "both sides signed the same root": the escalation reports the tree root WITH the SEAL ctrl
|
|
1027
|
+
// leaf appended, which is by construction not the committed root, and a responder-side row
|
|
1028
|
+
// never even receives the initiator's leaf. It is safe because **the directory rebuilds the
|
|
1029
|
+
// tree from relay-witnessed leaves and verifies their signatures** — it never consults the
|
|
1030
|
+
// commitment. The commitment is what makes it legitimate to ASK, not what makes it verifiable.
|
|
1031
|
+
// And re-entering cannot post a second ctrl leaf, because `submitSealLeaf` recovers the one a
|
|
1032
|
+
// previous run posted from the durable carry rather than posting again.
|
|
1033
|
+
if (record.status === "seal_interrupted_pending") {
|
|
1034
|
+
// The AC-011 check at the top of the handler already refuses a concurrent attempt on this
|
|
1035
|
+
// same key, for every status — so no second check is needed here, only the add/release pair
|
|
1036
|
+
// that makes THIS attempt visible to it.
|
|
1037
|
+
const key = sealKey(record.agent_name, sessionId);
|
|
1038
|
+
sealInterruptedInProgress.add(key);
|
|
1039
|
+
const correlationId = randomUUID();
|
|
1040
|
+
try {
|
|
1041
|
+
const uni = await submitAndEscalate(record, sessionId, correlationId);
|
|
1042
|
+
if ("escalated" in uni) {
|
|
1043
|
+
// NAME THE CAUSE `submitSealLeaf` GAVE, never a label invented here. Most of these are
|
|
1044
|
+
// transient and local — `standing_receiver_unavailable` is simply "the agent is not
|
|
1045
|
+
// started yet", which a freshly booted daemon reports for every session. The genuinely
|
|
1046
|
+
// permanent case (the relay released the session) has its own reason,
|
|
1047
|
+
// `seal_carry_empty`, raised inside the escalation where it is actually known.
|
|
1048
|
+
return {
|
|
1049
|
+
ok: false,
|
|
1050
|
+
reason: uni.reason,
|
|
1051
|
+
guidance: `This session holds a bilateral commitment, but no notarization could be requested for it: ${uni.reason}. ` +
|
|
1052
|
+
`That is usually local and temporary — an agent that is not started yet (cello_start_agent), or a relay this daemon cannot currently reach (cello_status). ` +
|
|
1053
|
+
`The conversation is intact either way; retry cello_close_session once the daemon reports healthy.`,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (uni.ok)
|
|
1057
|
+
return uni;
|
|
1058
|
+
return {
|
|
1059
|
+
ok: false,
|
|
1060
|
+
reason: uni.reason,
|
|
1061
|
+
...(uni.retry_after_seconds !== undefined ? { retry_after_seconds: uni.retry_after_seconds } : {}),
|
|
1062
|
+
guidance: uni.guidance,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
finally {
|
|
1066
|
+
sealInterruptedInProgress.delete(key);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
// Any other status — nothing to do.
|
|
816
1070
|
return {
|
|
817
1071
|
ok: false,
|
|
818
1072
|
reason: "session_not_closeable",
|
|
819
|
-
guidance: `Session is in status '${record.status}', which cannot be closed via cello_close_session. Check cello_sessions
|
|
1073
|
+
guidance: `Session is in status '${record.status}', which cannot be closed via cello_close_session. Check cello_sessions.`,
|
|
820
1074
|
};
|
|
821
1075
|
};
|
|
822
1076
|
}
|