@cello-protocol/daemon 0.0.20 → 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 +112 -15
- package/dist/daemon.js.map +1 -1
- package/dist/session-node-manager.d.ts +11 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +189 -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 +2 -2
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
|
|
@@ -1697,7 +1724,14 @@ export async function startDaemon(config) {
|
|
|
1697
1724
|
return;
|
|
1698
1725
|
if (ftype === "seal_unilateral_too_early") {
|
|
1699
1726
|
pendingUnilateralWaiters.delete(sidHex);
|
|
1700
|
-
|
|
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
|
+
});
|
|
1701
1735
|
return;
|
|
1702
1736
|
}
|
|
1703
1737
|
void (async () => {
|
|
@@ -1877,6 +1911,8 @@ export async function startDaemon(config) {
|
|
|
1877
1911
|
// M-1 PULL: live MCP clients must see interrupted sessions too, exactly as
|
|
1878
1912
|
// the daemon-wide getStatus() surfaces them.
|
|
1879
1913
|
interrupted_sessions: buildInterruptedSessions(),
|
|
1914
|
+
// M8B F16: per-session liveness on the MCP surface too.
|
|
1915
|
+
active_sessions: buildActiveSessions(),
|
|
1880
1916
|
};
|
|
1881
1917
|
});
|
|
1882
1918
|
// ─── MCP-001: no_current_agent guard for session tools ───
|
|
@@ -1945,6 +1981,25 @@ export async function startDaemon(config) {
|
|
|
1945
1981
|
// SI-001: the selector consumes the assignment's signed transport_mode as the
|
|
1946
1982
|
// sole dial authority — never inferred from address format.
|
|
1947
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
|
+
}
|
|
1948
2003
|
const result = await transportSelector.dial(assignment, { correlationId });
|
|
1949
2004
|
const sessionId = Buffer.from(assignment.session_id).toString("hex");
|
|
1950
2005
|
if (!result.ok) {
|
|
@@ -2155,12 +2210,23 @@ export async function startDaemon(config) {
|
|
|
2155
2210
|
// per-party frontiers, attestation modes, and final_message.answered.
|
|
2156
2211
|
return { ok: true, sealed_root: sealedCompletion.rootHex, legibility: sealedCompletion.legibility };
|
|
2157
2212
|
}
|
|
2158
|
-
//
|
|
2159
|
-
//
|
|
2160
|
-
//
|
|
2161
|
-
//
|
|
2162
|
-
//
|
|
2163
|
-
|
|
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) {
|
|
2164
2230
|
return {
|
|
2165
2231
|
ok: false,
|
|
2166
2232
|
reason: "seal_pending_bilateral",
|
|
@@ -2193,8 +2259,8 @@ export async function startDaemon(config) {
|
|
|
2193
2259
|
const sent = await sendOver(record.agent_name, {
|
|
2194
2260
|
type: "seal_unilateral",
|
|
2195
2261
|
session_id: new Uint8Array(Buffer.from(sessionId, "hex")),
|
|
2196
|
-
reported_root: new Uint8Array(Buffer.from(
|
|
2197
|
-
reported_seq:
|
|
2262
|
+
reported_root: new Uint8Array(Buffer.from(escalation.reportedRootHex, "hex")),
|
|
2263
|
+
reported_seq: escalation.sequenceNumber,
|
|
2198
2264
|
seal_leaves,
|
|
2199
2265
|
});
|
|
2200
2266
|
if (!sent.ok) {
|
|
@@ -2217,10 +2283,14 @@ export async function startDaemon(config) {
|
|
|
2217
2283
|
return { ok: true, sealed_root: uniResult.sealedRootHex, seal_type: "unilateral" };
|
|
2218
2284
|
}
|
|
2219
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
|
+
: "";
|
|
2220
2290
|
return {
|
|
2221
2291
|
ok: false,
|
|
2222
2292
|
reason: "seal_counterparty_pending",
|
|
2223
|
-
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.`,
|
|
2224
2294
|
};
|
|
2225
2295
|
}
|
|
2226
2296
|
return {
|
|
@@ -2915,6 +2985,17 @@ export async function startDaemon(config) {
|
|
|
2915
2985
|
}
|
|
2916
2986
|
async function acceptInboundAssignment(parsed, agentName, correlationId) {
|
|
2917
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
|
+
});
|
|
2918
2999
|
// M2: do not drop the session if this agent's standing receiver is mid-rebuild.
|
|
2919
3000
|
const ready = await waitForStandingReceiver(agentName);
|
|
2920
3001
|
if (!ready) {
|
|
@@ -3038,11 +3119,15 @@ export async function startDaemon(config) {
|
|
|
3038
3119
|
// is NOT yet verified here. The old client's receiveSessionAssignment performed that
|
|
3039
3120
|
// check; it must be re-homed natively before this path faces a real (untrusted)
|
|
3040
3121
|
// directory. Until then we accept directory-pushed assignments on trust — the in-process
|
|
3041
|
-
// seam tests inject trusted frames.
|
|
3042
|
-
|
|
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", {
|
|
3043
3128
|
sessionId: parsed.sessionIdHex,
|
|
3044
3129
|
agentName: localAgent.name,
|
|
3045
|
-
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)",
|
|
3046
3131
|
correlationId,
|
|
3047
3132
|
});
|
|
3048
3133
|
inboundInFlight.add(parsed.sessionIdHex);
|
|
@@ -3741,6 +3826,18 @@ export async function startDaemon(config) {
|
|
|
3741
3826
|
// 3) Out of time — non-blocking-equivalent empty answer.
|
|
3742
3827
|
const remaining = deadline - Date.now();
|
|
3743
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
|
+
}
|
|
3744
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." };
|
|
3745
3842
|
}
|
|
3746
3843
|
await new Promise((r) => setTimeout(r, Math.min(20, remaining)));
|