@cello-protocol/daemon 0.0.19 → 0.0.21
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/daemon.d.ts.map +1 -1
- package/dist/daemon.js +180 -35
- package/dist/daemon.js.map +1 -1
- package/dist/session-ceremony.d.ts +26 -13
- package/dist/session-ceremony.d.ts.map +1 -1
- package/dist/session-ceremony.js +36 -18
- package/dist/session-ceremony.js.map +1 -1
- package/dist/session-node-manager.d.ts +25 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +236 -87
- package/dist/session-node-manager.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
package/dist/daemon.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EASrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAM/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAqB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAM/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAqInF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AAyCD,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAwjI7E"}
|
package/dist/daemon.js
CHANGED
|
@@ -983,7 +983,14 @@ export async function startDaemon(config) {
|
|
|
983
983
|
else {
|
|
984
984
|
state = "registered";
|
|
985
985
|
}
|
|
986
|
-
return {
|
|
986
|
+
return {
|
|
987
|
+
name: a.name,
|
|
988
|
+
state,
|
|
989
|
+
pubkey: a.pubkey,
|
|
990
|
+
// M8B F14 (fix 5): per-agent standing-receiver readiness on the MCP surface
|
|
991
|
+
// (cello_status / cello_list_agents), so a deaf agent is visible to the operator.
|
|
992
|
+
standing_receiver_ready: sessionNodeManager.getStandingReceiverReady(a.name),
|
|
993
|
+
};
|
|
987
994
|
});
|
|
988
995
|
}
|
|
989
996
|
// M7-SESSION-001 AC-006/AC-007 (and M-1 PULL): build the interrupted_sessions
|
|
@@ -1009,6 +1016,18 @@ export async function startDaemon(config) {
|
|
|
1009
1016
|
interruptedAt: row.interrupted_at ?? new Date(row.updated_at).toISOString(),
|
|
1010
1017
|
}));
|
|
1011
1018
|
}
|
|
1019
|
+
// M8B F16: per-session liveness for ACTIVE sessions, shared by both status surfaces
|
|
1020
|
+
// ("status" for the CLI, "cello_status" for MCP). The signal (session.liveness.changed,
|
|
1021
|
+
// tracked in the node manager) existed but nothing consumed it — a dead counterparty
|
|
1022
|
+
// was invisible to the operator.
|
|
1023
|
+
function buildActiveSessions() {
|
|
1024
|
+
return sessionNodeManager.getSessionsByStatus("active").map((row) => ({
|
|
1025
|
+
sessionId: row.session_id,
|
|
1026
|
+
agentName: row.agent_name,
|
|
1027
|
+
counterpartyPubkey: row.counterparty_pubkey,
|
|
1028
|
+
liveness: sessionNodeManager.getSessionLiveness(row.agent_name, row.session_id),
|
|
1029
|
+
}));
|
|
1030
|
+
}
|
|
1012
1031
|
// Build status response factory
|
|
1013
1032
|
function getStatus() {
|
|
1014
1033
|
// M7-SESSION-001 AC-006/AC-007: surface interrupted sessions
|
|
@@ -1016,11 +1035,19 @@ export async function startDaemon(config) {
|
|
|
1016
1035
|
return {
|
|
1017
1036
|
daemon: "running",
|
|
1018
1037
|
directory_signaling: directorySignalingStatus(),
|
|
1019
|
-
|
|
1038
|
+
// M8B F14 (fix 5): per-agent standing-receiver readiness, so a deaf agent (online but
|
|
1039
|
+
// no armed receiver) is visible in cello_status instead of hiding behind the ANY-agent
|
|
1040
|
+
// aggregate below (kept for backward compatibility).
|
|
1041
|
+
agents: agents.map((a) => ({
|
|
1042
|
+
...a,
|
|
1043
|
+
standing_receiver_ready: sessionNodeManager.getStandingReceiverReady(a.name),
|
|
1044
|
+
})),
|
|
1020
1045
|
connections,
|
|
1021
1046
|
standing_receiver_ready: sessionNodeManager.getStandingReceiverReady(),
|
|
1022
1047
|
retryQueueDepth: retryQueue.getTotalDepth(),
|
|
1023
1048
|
interrupted_sessions,
|
|
1049
|
+
// M8B F16: per-session liveness so a counterparty-gone session is visible.
|
|
1050
|
+
active_sessions: buildActiveSessions(),
|
|
1024
1051
|
};
|
|
1025
1052
|
}
|
|
1026
1053
|
// Register IPC handlers
|
|
@@ -1576,7 +1603,14 @@ export async function startDaemon(config) {
|
|
|
1576
1603
|
logger.error("session.sealed.signature.invalid", { sessionId: sidHex, reason: verdict.reason });
|
|
1577
1604
|
return;
|
|
1578
1605
|
}
|
|
1579
|
-
|
|
1606
|
+
// F2-a: on verified:false, surface WHY (signer_key_not_held / no_frost_share / …) so this
|
|
1607
|
+
// event can never be mistaken for a tolerated failed check. A real failure took the early
|
|
1608
|
+
// return above (session.sealed.signature.invalid) and never reaches here.
|
|
1609
|
+
logger.info("session.sealed.signature.checked", {
|
|
1610
|
+
sessionId: sidHex,
|
|
1611
|
+
verified: verdict.verified,
|
|
1612
|
+
...(verdict.verified ? {} : { reason: verdict.reason }),
|
|
1613
|
+
});
|
|
1580
1614
|
}
|
|
1581
1615
|
// M7-SESSION-004 (AC-005): normalise the wire legibility (Uint8Array pubkeys → hex) into a
|
|
1582
1616
|
// JSON-safe certificate and persist it with the sealed record so it survives a restart and
|
|
@@ -1690,7 +1724,14 @@ export async function startDaemon(config) {
|
|
|
1690
1724
|
return;
|
|
1691
1725
|
if (ftype === "seal_unilateral_too_early") {
|
|
1692
1726
|
pendingUnilateralWaiters.delete(sidHex);
|
|
1693
|
-
|
|
1727
|
+
// F20: thread the directory's remaining_seconds through so the close guidance can
|
|
1728
|
+
// say when the unilateral seal becomes available.
|
|
1729
|
+
const rs = frame["remaining_seconds"];
|
|
1730
|
+
waiter({
|
|
1731
|
+
ok: false,
|
|
1732
|
+
reason: "seal_unilateral_too_early",
|
|
1733
|
+
...(typeof rs === "number" ? { remainingSeconds: rs } : {}),
|
|
1734
|
+
});
|
|
1694
1735
|
return;
|
|
1695
1736
|
}
|
|
1696
1737
|
void (async () => {
|
|
@@ -1870,6 +1911,8 @@ export async function startDaemon(config) {
|
|
|
1870
1911
|
// M-1 PULL: live MCP clients must see interrupted sessions too, exactly as
|
|
1871
1912
|
// the daemon-wide getStatus() surfaces them.
|
|
1872
1913
|
interrupted_sessions: buildInterruptedSessions(),
|
|
1914
|
+
// M8B F16: per-session liveness on the MCP surface too.
|
|
1915
|
+
active_sessions: buildActiveSessions(),
|
|
1873
1916
|
};
|
|
1874
1917
|
});
|
|
1875
1918
|
// ─── MCP-001: no_current_agent guard for session tools ───
|
|
@@ -1878,9 +1921,10 @@ export async function startDaemon(config) {
|
|
|
1878
1921
|
// NOTE: cello_await_session is NOT in this stub list — Seam 2 registers a real
|
|
1879
1922
|
// handler for it below (inbound session establishment), with its own inline
|
|
1880
1923
|
// no_current_agent guard.
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1924
|
+
// F1-a2: cello_receive_session is NO LONGER stubbed here — it is registered below as a true
|
|
1925
|
+
// alias of the real (blocking) cello_receive handler. Kept as an (empty) extension point for
|
|
1926
|
+
// future tools that need the plain no_current_agent guard without their own handler.
|
|
1927
|
+
const SESSION_TOOLS_REQUIRING_AGENT = [];
|
|
1884
1928
|
const NO_CURRENT_AGENT_RESPONSE = {
|
|
1885
1929
|
ok: false,
|
|
1886
1930
|
reason: "no_current_agent",
|
|
@@ -1937,6 +1981,25 @@ export async function startDaemon(config) {
|
|
|
1937
1981
|
// SI-001: the selector consumes the assignment's signed transport_mode as the
|
|
1938
1982
|
// sole dial authority — never inferred from address format.
|
|
1939
1983
|
const assignment = negotiation.assignment;
|
|
1984
|
+
// M8B F13: validate-what-you-receive at the trust boundary. When the responder cannot
|
|
1985
|
+
// proceed it aborts SILENTLY (session-ceremony.ts wireSessionOfferHandler sends nothing)
|
|
1986
|
+
// and the directory folds an EMPTY counterparty endpoint into the FROST-signed
|
|
1987
|
+
// assignment. Accepting that assignment produced a false ok:true + a phantom session
|
|
1988
|
+
// whose failure only surfaced on the first cello_send. Reject it HERE — before any
|
|
1989
|
+
// local session state exists — covering every cause of a missing accept (abort,
|
|
1990
|
+
// offline, crash, timeout) without needing a directory change.
|
|
1991
|
+
if (!assignment.counterparty_session_peer_id) {
|
|
1992
|
+
logger.warn("session.initiate.counterparty_unavailable", {
|
|
1993
|
+
sessionId: Buffer.from(assignment.session_id).toString("hex"),
|
|
1994
|
+
agentName,
|
|
1995
|
+
correlationId,
|
|
1996
|
+
});
|
|
1997
|
+
return {
|
|
1998
|
+
ok: false,
|
|
1999
|
+
reason: "counterparty_unavailable",
|
|
2000
|
+
guidance: "The counterparty did not accept the session offer (it may be offline or unable to receive). No session was established. Verify the counterparty agent is online (its operator can check cello_status), then retry cello_initiate_session.",
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
1940
2003
|
const result = await transportSelector.dial(assignment, { correlationId });
|
|
1941
2004
|
const sessionId = Buffer.from(assignment.session_id).toString("hex");
|
|
1942
2005
|
if (!result.ok) {
|
|
@@ -2147,12 +2210,23 @@ export async function startDaemon(config) {
|
|
|
2147
2210
|
// per-party frontiers, attestation modes, and final_message.answered.
|
|
2148
2211
|
return { ok: true, sealed_root: sealedCompletion.rootHex, legibility: sealedCompletion.legibility };
|
|
2149
2212
|
}
|
|
2150
|
-
//
|
|
2151
|
-
//
|
|
2152
|
-
//
|
|
2153
|
-
//
|
|
2154
|
-
//
|
|
2155
|
-
|
|
2213
|
+
// M8B FINDING-1: `responder_seal_already_submitted` has two producers — (a) the auto-ack
|
|
2214
|
+
// path submitted our leaf because the COUNTERPARTY's SEAL arrived, or (b) our OWN earlier
|
|
2215
|
+
// close submitted it and the counterparty never co-closed. The result now carries the
|
|
2216
|
+
// first submit's reportedRootHex/sequenceNumber, so a retry close can still escalate to a
|
|
2217
|
+
// unilateral seal (case b — the live-deadlock path). We deliberately do NOT distinguish
|
|
2218
|
+
// (a) from (b) client-side: the bilateral wait above already gave case (a) its window, and
|
|
2219
|
+
// the directory's grace/already-sealed gates arbitrate a redundant seal_unilateral — that
|
|
2220
|
+
// is the sovereign-node-correct shape. Only when the not-ok result carries NO root (the
|
|
2221
|
+
// first submit is still in flight) is "pending" the honest terminal answer here.
|
|
2222
|
+
const escalation = submit.ok
|
|
2223
|
+
? { reportedRootHex: submit.reportedRootHex, sequenceNumber: submit.sequenceNumber }
|
|
2224
|
+
: submit.reason === "responder_seal_already_submitted" &&
|
|
2225
|
+
typeof submit.reportedRootHex === "string" &&
|
|
2226
|
+
typeof submit.sequenceNumber === "number"
|
|
2227
|
+
? { reportedRootHex: submit.reportedRootHex, sequenceNumber: submit.sequenceNumber }
|
|
2228
|
+
: null;
|
|
2229
|
+
if (!escalation) {
|
|
2156
2230
|
return {
|
|
2157
2231
|
ok: false,
|
|
2158
2232
|
reason: "seal_pending_bilateral",
|
|
@@ -2185,8 +2259,8 @@ export async function startDaemon(config) {
|
|
|
2185
2259
|
const sent = await sendOver(record.agent_name, {
|
|
2186
2260
|
type: "seal_unilateral",
|
|
2187
2261
|
session_id: new Uint8Array(Buffer.from(sessionId, "hex")),
|
|
2188
|
-
reported_root: new Uint8Array(Buffer.from(
|
|
2189
|
-
reported_seq:
|
|
2262
|
+
reported_root: new Uint8Array(Buffer.from(escalation.reportedRootHex, "hex")),
|
|
2263
|
+
reported_seq: escalation.sequenceNumber,
|
|
2190
2264
|
seal_leaves,
|
|
2191
2265
|
});
|
|
2192
2266
|
if (!sent.ok) {
|
|
@@ -2209,10 +2283,14 @@ export async function startDaemon(config) {
|
|
|
2209
2283
|
return { ok: true, sealed_root: uniResult.sealedRootHex, seal_type: "unilateral" };
|
|
2210
2284
|
}
|
|
2211
2285
|
if (uniResult.reason === "seal_unilateral_too_early") {
|
|
2286
|
+
// F20: tell the operator WHEN the unilateral seal becomes available, not just "later".
|
|
2287
|
+
const when = typeof uniResult.remainingSeconds === "number"
|
|
2288
|
+
? `A unilateral seal becomes available in ~${uniResult.remainingSeconds}s. `
|
|
2289
|
+
: "";
|
|
2212
2290
|
return {
|
|
2213
2291
|
ok: false,
|
|
2214
2292
|
reason: "seal_counterparty_pending",
|
|
2215
|
-
guidance:
|
|
2293
|
+
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.`,
|
|
2216
2294
|
};
|
|
2217
2295
|
}
|
|
2218
2296
|
return {
|
|
@@ -2907,6 +2985,17 @@ export async function startDaemon(config) {
|
|
|
2907
2985
|
}
|
|
2908
2986
|
async function acceptInboundAssignment(parsed, agentName, correlationId) {
|
|
2909
2987
|
try {
|
|
2988
|
+
// M8B F14 (fix 2): KICK creation before polling — an inbound offer arriving while no
|
|
2989
|
+
// receiver exists and none is being created must trigger the ensure itself (the doc
|
|
2990
|
+
// comment's "retries on demand" made true), instead of polling a creation nobody
|
|
2991
|
+
// started and dropping the offer. Fire-and-forget: the poll below observes readiness.
|
|
2992
|
+
void sessionNodeManager.ensureStandingReceiverForAgent(agentName).catch((err) => {
|
|
2993
|
+
logger.warn("session.standing_receiver.ensure.failed", {
|
|
2994
|
+
agentName,
|
|
2995
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
2996
|
+
correlationId,
|
|
2997
|
+
});
|
|
2998
|
+
});
|
|
2910
2999
|
// M2: do not drop the session if this agent's standing receiver is mid-rebuild.
|
|
2911
3000
|
const ready = await waitForStandingReceiver(agentName);
|
|
2912
3001
|
if (!ready) {
|
|
@@ -3030,11 +3119,15 @@ export async function startDaemon(config) {
|
|
|
3030
3119
|
// is NOT yet verified here. The old client's receiveSessionAssignment performed that
|
|
3031
3120
|
// check; it must be re-homed natively before this path faces a real (untrusted)
|
|
3032
3121
|
// directory. Until then we accept directory-pushed assignments on trust — the in-process
|
|
3033
|
-
// seam tests inject trusted frames.
|
|
3034
|
-
|
|
3122
|
+
// seam tests inject trusted frames.
|
|
3123
|
+
// M8B F15: logged at DEBUG with an "expected" note — it fires on EVERY healthy inbound
|
|
3124
|
+
// session, and at WARN it read as a failure and actively misled live diagnosis. The
|
|
3125
|
+
// deferred-verification state is a known, tracked gap (SESSION-004), not a per-session
|
|
3126
|
+
// anomaly.
|
|
3127
|
+
logger.debug("session.inbound.assignment.unverified", {
|
|
3035
3128
|
sessionId: parsed.sessionIdHex,
|
|
3036
3129
|
agentName: localAgent.name,
|
|
3037
|
-
note: "FROST assignment signature verification deferred to SESSION-004 re-home",
|
|
3130
|
+
note: "FROST assignment signature verification deferred to SESSION-004 re-home (expected on every inbound session until SESSION-004)",
|
|
3038
3131
|
correlationId,
|
|
3039
3132
|
});
|
|
3040
3133
|
inboundInFlight.add(parsed.sessionIdHex);
|
|
@@ -3671,35 +3764,87 @@ export async function startDaemon(config) {
|
|
|
3671
3764
|
void newRootHex;
|
|
3672
3765
|
return { ok: true, sequence_number: leafIndex };
|
|
3673
3766
|
});
|
|
3674
|
-
// ─── CELLO-M7-DAEMON-004: cello_receive (
|
|
3675
|
-
|
|
3767
|
+
// ─── CELLO-M7-DAEMON-004 / F1-a: cello_receive (BLOCKING, session-scoped) ────────
|
|
3768
|
+
// F1-a fix: the daemon port had dropped the blocking receive (the handler was a
|
|
3769
|
+
// non-blocking buf.shift and cello_receive_session was a not_implemented stub). It now
|
|
3770
|
+
// BLOCKS up to timeout_ms, polling the received-content buffer — resolved by the next
|
|
3771
|
+
// arrival, a terminal seal answer (F1-b), or timeout. This is the "blocking receive
|
|
3772
|
+
// variant" the guidance names. Registered under both cello_receive and cello_receive_session
|
|
3773
|
+
// (F1-a2: one implementation, both names — the redundant tools are collapsed).
|
|
3774
|
+
const RECEIVE_DEFAULT_TIMEOUT_MS = 30000; // matches the cello-mcp shim's documented default
|
|
3775
|
+
const handleReceive = async (params, connectionId) => {
|
|
3676
3776
|
const connState = perConnectionState.get(connectionId);
|
|
3677
3777
|
if (!connState || !connState.currentAgent)
|
|
3678
3778
|
return NO_CURRENT_AGENT_RESPONSE;
|
|
3679
|
-
|
|
3779
|
+
const agentName = connState.currentAgent;
|
|
3780
|
+
// Read the snake_case public field cello-mcp.ts actually sends.
|
|
3680
3781
|
const sessionId = params?.session_id;
|
|
3681
3782
|
if (!sessionId) {
|
|
3682
3783
|
return { ok: false, reason: "missing_params", guidance: "Provide 'session_id' (hex) to receive content for a specific session." };
|
|
3683
3784
|
}
|
|
3684
|
-
const record = sessionNodeManager.getSessionRecord(
|
|
3785
|
+
const record = sessionNodeManager.getSessionRecord(agentName, sessionId);
|
|
3685
3786
|
if (!record) {
|
|
3686
3787
|
return { ok: false, reason: "session_not_found", guidance: "No session found with this ID. Check cello_list_sessions." };
|
|
3687
3788
|
}
|
|
3688
|
-
if (record.agent_name !==
|
|
3789
|
+
if (record.agent_name !== agentName) {
|
|
3689
3790
|
return { ok: false, reason: "session_not_owned", guidance: "This session belongs to a different agent. Call cello_use_agent to switch to the agent that owns it, then retry." };
|
|
3690
3791
|
}
|
|
3691
|
-
const
|
|
3692
|
-
|
|
3693
|
-
|
|
3792
|
+
const rawTimeout = params?.timeout_ms;
|
|
3793
|
+
const timeoutMs = typeof rawTimeout === "number" && Number.isFinite(rawTimeout) && rawTimeout >= 0
|
|
3794
|
+
? rawTimeout
|
|
3795
|
+
: RECEIVE_DEFAULT_TIMEOUT_MS;
|
|
3796
|
+
const deadline = Date.now() + timeoutMs;
|
|
3797
|
+
for (;;) {
|
|
3798
|
+
// 1) Deliverable content wins — drain one buffered message per call (FIFO).
|
|
3799
|
+
const entry = sessionNodeManager.takeReceivedContent(agentName, sessionId);
|
|
3800
|
+
if (entry) {
|
|
3801
|
+
return {
|
|
3802
|
+
ok: true,
|
|
3803
|
+
content: Buffer.from(entry.contentHex, "hex").toString("utf8"),
|
|
3804
|
+
sessionId,
|
|
3805
|
+
sequence_number: entry.sequenceNumber,
|
|
3806
|
+
senderPubkey: entry.senderPubkey,
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
// 2) F1-b: the session sealed while we were (or before we started) waiting — return the
|
|
3810
|
+
// terminal answer instead of hanging to timeout. unread_count reports messages that
|
|
3811
|
+
// were evicted unread (still durable — recoverable via cello_get_transcript).
|
|
3812
|
+
const terminal = sessionNodeManager.peekTerminalMarker(agentName, sessionId);
|
|
3813
|
+
if (terminal) {
|
|
3814
|
+
const sealedRoot = sessionNodeManager.getSealedRootHex(agentName, sessionId);
|
|
3815
|
+
return {
|
|
3816
|
+
ok: true,
|
|
3817
|
+
type: "session_sealed",
|
|
3818
|
+
session_id: sessionId,
|
|
3819
|
+
...(sealedRoot ? { sealed_root: sealedRoot } : {}),
|
|
3820
|
+
unread_count: terminal.unreadCount,
|
|
3821
|
+
guidance: terminal.unreadCount > 0
|
|
3822
|
+
? `The session has been sealed by both parties. ${terminal.unreadCount} message(s) arrived that were not read live — call cello_get_transcript to retrieve the full sealed history. No further actions are required on this session.`
|
|
3823
|
+
: "The session has been sealed by both parties. The full history is available via cello_get_transcript. No further actions are required on this session.",
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
// 3) Out of time — non-blocking-equivalent empty answer.
|
|
3827
|
+
const remaining = deadline - Date.now();
|
|
3828
|
+
if (remaining <= 0) {
|
|
3829
|
+
// M8B F16: a dead session must not return the SAME null timeout as a
|
|
3830
|
+
// quiet-but-healthy one. The liveness signal (session.liveness.changed → gone,
|
|
3831
|
+
// tracked per session by the node manager) finally reaches the MCP surface here.
|
|
3832
|
+
if (sessionNodeManager.getSessionLiveness(agentName, sessionId) === "gone") {
|
|
3833
|
+
return {
|
|
3834
|
+
ok: true,
|
|
3835
|
+
content: null,
|
|
3836
|
+
reason: "counterparty_gone",
|
|
3837
|
+
liveness: "gone",
|
|
3838
|
+
guidance: "The counterparty's session connection has dropped (liveness: gone) — it may have crashed or gone offline. No more content will arrive on the direct path. Call cello_close_session to seal the session; if the counterparty never co-closes, a unilateral seal becomes available after the directory's delivery-grace window.",
|
|
3839
|
+
};
|
|
3840
|
+
}
|
|
3841
|
+
return { ok: true, content: null, guidance: "No content arrived within timeout_ms. Call cello_receive again to keep waiting, or read cello_get_transcript for the full session history." };
|
|
3842
|
+
}
|
|
3843
|
+
await new Promise((r) => setTimeout(r, Math.min(20, remaining)));
|
|
3694
3844
|
}
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
sessionId,
|
|
3699
|
-
sequence_number: entry.sequenceNumber,
|
|
3700
|
-
senderPubkey: entry.senderPubkey,
|
|
3701
|
-
};
|
|
3702
|
-
});
|
|
3845
|
+
};
|
|
3846
|
+
handlers.set("cello_receive", handleReceive);
|
|
3847
|
+
handlers.set("cello_receive_session", handleReceive);
|
|
3703
3848
|
let shutdownPromise = null;
|
|
3704
3849
|
handlers.set("shutdown", async (_params, _connectionId) => {
|
|
3705
3850
|
if (!shutdownPromise) {
|