@cello-protocol/daemon 0.0.30 → 0.0.31
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 +189 -27
- package/dist/daemon.js.map +1 -1
- package/dist/session-node-manager.d.ts +21 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +84 -1
- package/dist/session-node-manager.js.map +1 -1
- package/dist/signaling-connect.js +7 -2
- package/dist/signaling-connect.js.map +1 -1
- package/dist/types.d.ts +7 -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,EASrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAM/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAO/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,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,
|
|
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;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAO/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,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,CA0uJ7E"}
|
package/dist/daemon.js
CHANGED
|
@@ -1254,19 +1254,16 @@ export async function startDaemon(config) {
|
|
|
1254
1254
|
return agents
|
|
1255
1255
|
.filter((a) => a.state !== "load_failed")
|
|
1256
1256
|
.map((a) => {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
}
|
|
1264
|
-
else {
|
|
1265
|
-
state = "registered";
|
|
1266
|
-
}
|
|
1257
|
+
// M8C-AUTOSTART-1 (F5): `state` reports readiness only (online vs registered); selection
|
|
1258
|
+
// is a SEPARATE `selected` flag. Previously `state = "current"` overloaded the current
|
|
1259
|
+
// agent, making it read as a different readiness level than a second healthy online agent.
|
|
1260
|
+
const online = onlineAgents.has(a.name);
|
|
1261
|
+
const state = online ? "online" : "registered";
|
|
1262
|
+
const selected = online && a.name === currentAgent;
|
|
1267
1263
|
return {
|
|
1268
1264
|
name: a.name,
|
|
1269
1265
|
state,
|
|
1266
|
+
selected,
|
|
1270
1267
|
pubkey: a.pubkey,
|
|
1271
1268
|
// M8B F14 (fix 5): per-agent standing-receiver readiness on the MCP surface
|
|
1272
1269
|
// (cello_status / cello_list_agents), so a deaf agent is visible to the operator.
|
|
@@ -1348,11 +1345,13 @@ export async function startDaemon(config) {
|
|
|
1348
1345
|
return { connectionId };
|
|
1349
1346
|
});
|
|
1350
1347
|
// ─── MCP-001: cello_start_agent handler ───
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1348
|
+
// M8C-AUTOSTART-1 (A2): the shared start path. Extracted from cello_start_agent so cello_use_agent
|
|
1349
|
+
// can AUTO-START an offline agent through the exact same code (idempotent, same signaling +
|
|
1350
|
+
// standing-receiver setup, same agent_state_changed event) — never a divergent shim-side retry.
|
|
1351
|
+
// Permissive by design (D12): an agent that exists goes online regardless of directory
|
|
1352
|
+
// registration state (online-without-registration is an established contract). Returns a
|
|
1353
|
+
// structured failure so callers can surface agent_start_failed with a real reason + guidance.
|
|
1354
|
+
function startAgentInternal(name) {
|
|
1356
1355
|
const agent = agents.find((a) => a.name === name);
|
|
1357
1356
|
if (!agent || agent.state === "load_failed") {
|
|
1358
1357
|
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Run 'cello login' to register agents, or check agent names with cello_list_agents.` };
|
|
@@ -1397,6 +1396,29 @@ export async function startDaemon(config) {
|
|
|
1397
1396
|
// MCP-002: Broadcast agent_state_changed to ALL connections
|
|
1398
1397
|
notificationDispatcher.dispatchAgentStateChanged(name, "online", "started");
|
|
1399
1398
|
return { ok: true };
|
|
1399
|
+
}
|
|
1400
|
+
// M8C-AUTOSTART-1 (F18): resolve which agent a name-defaulting tool should act on for this
|
|
1401
|
+
// connection: an explicit name wins; else the connection's current agent; else — when EXACTLY one
|
|
1402
|
+
// agent is online daemon-wide — that sole agent (removes the "why did it forget my agent" moment
|
|
1403
|
+
// after a /mcp reconnect). Two-or-more online with none selected stays ambiguous → null (the
|
|
1404
|
+
// caller returns no_current_agent), because guessing between peers would misroute.
|
|
1405
|
+
function resolveCurrentAgent(connState, explicitName) {
|
|
1406
|
+
if (explicitName)
|
|
1407
|
+
return explicitName;
|
|
1408
|
+
if (connState?.currentAgent)
|
|
1409
|
+
return connState.currentAgent;
|
|
1410
|
+
if (onlineAgents.size === 1)
|
|
1411
|
+
return [...onlineAgents][0];
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
// ─── MCP-001: cello_start_agent handler ───
|
|
1415
|
+
// Bring a registered agent online WITHOUT claiming it as this connection's current agent.
|
|
1416
|
+
handlers.set("cello_start_agent", async (params, _connectionId) => {
|
|
1417
|
+
const name = params?.name;
|
|
1418
|
+
if (!name) {
|
|
1419
|
+
return { ok: false, reason: "missing_params", guidance: "Provide 'name' parameter with the agent name to start." };
|
|
1420
|
+
}
|
|
1421
|
+
return startAgentInternal(name);
|
|
1400
1422
|
});
|
|
1401
1423
|
// ─── PERSIST-002 (AC-004): cello_create_agent handler ───
|
|
1402
1424
|
// The explicit agent-creation path: generate a fresh K_local seed, write it as an `agents` row in
|
|
@@ -1648,15 +1670,36 @@ export async function startDaemon(config) {
|
|
|
1648
1670
|
}
|
|
1649
1671
|
const agent = agents.find((a) => a.name === name);
|
|
1650
1672
|
if (!agent || agent.state === "load_failed") {
|
|
1651
|
-
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist.
|
|
1652
|
-
}
|
|
1653
|
-
if (!onlineAgents.has(name)) {
|
|
1654
|
-
return { ok: false, reason: "agent_not_online", guidance: `Agent '${name}' exists but is not online. Call cello_start_agent('${name}') first to bring it online, then retry cello_use_agent.` };
|
|
1673
|
+
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Create it with 'cello create-agent ${name}', register it, then retry — or check names with cello_list_agents.` };
|
|
1655
1674
|
}
|
|
1656
1675
|
const connState = perConnectionState.get(connectionId);
|
|
1657
1676
|
if (!connState) {
|
|
1658
1677
|
return { ok: false, reason: "connection_not_registered", guidance: "Send ipc.connect frame before calling agent tools." };
|
|
1659
1678
|
}
|
|
1679
|
+
// M8C-AUTOSTART-1 (A1): auto-start the agent if it is not online, so the incantation collapses
|
|
1680
|
+
// to `login → use_agent` (no separate cello_start_agent). On a start failure, return a
|
|
1681
|
+
// structured agent_start_failed and leave the current selection UNCHANGED — no half-selected
|
|
1682
|
+
// state (A3). cello_start_agent stays available for bring-online-without-claiming.
|
|
1683
|
+
if (!onlineAgents.has(name)) {
|
|
1684
|
+
logger.info("agent.autostart.attempted", { connectionId, agentName: name });
|
|
1685
|
+
const startRes = startAgentInternal(name);
|
|
1686
|
+
if (!startRes.ok) {
|
|
1687
|
+
// Structured failure envelope (D6). Today `startAgentInternal` is permissive (D12) and its
|
|
1688
|
+
// only failure — agent_not_found — is already caught by the existence pre-check above, so
|
|
1689
|
+
// this branch is currently unreachable. It is the RESERVED structured-failure surface for
|
|
1690
|
+
// when auto-start gains a synchronous failure mode (e.g. D12's reverse: a bounded
|
|
1691
|
+
// waitForSignalingConnected making `directory_unreachable` a real start failure). Kept so
|
|
1692
|
+
// that extension surfaces the reason + guidance here with the selection left unchanged.
|
|
1693
|
+
// See M8C-DECISIONS D12 + BUILD-JOURNAL Entry 8.
|
|
1694
|
+
logger.warn("agent.autostart.failed", { connectionId, agentName: name, reason: startRes.reason });
|
|
1695
|
+
return {
|
|
1696
|
+
ok: false,
|
|
1697
|
+
reason: "agent_start_failed",
|
|
1698
|
+
start_reason: startRes.reason,
|
|
1699
|
+
guidance: `Could not start agent '${name}' to select it: ${startRes.guidance} Your current agent is unchanged.`,
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1660
1703
|
if (connState.currentAgent === name) {
|
|
1661
1704
|
return { ok: false, reason: "agent_already_current", guidance: `Agent '${name}' is already the current agent for this connection. No action needed — you can proceed with session operations.` };
|
|
1662
1705
|
}
|
|
@@ -1666,7 +1709,26 @@ export async function startDaemon(config) {
|
|
|
1666
1709
|
notificationDispatcher.setCurrentAgent(connectionId, name);
|
|
1667
1710
|
notificationDispatcher.dispatchAgentCurrentChanged(connectionId, fromAgent, name);
|
|
1668
1711
|
logger.info("agent.current.switched", { connectionId, fromAgent, toAgent: name });
|
|
1669
|
-
|
|
1712
|
+
// M8C-AUTOSTART-1 (A3, D12): not_registered is a NON-BLOCKING warning — the agent is selected
|
|
1713
|
+
// and usable locally, but it cannot establish directory sessions until registered. Surface the
|
|
1714
|
+
// next step (ONBOARD-NEXTSTEP style) without stranding the selection. One-row read.
|
|
1715
|
+
const result = { ok: true };
|
|
1716
|
+
try {
|
|
1717
|
+
const reg = await new DbRegistrationPersistence({ db: sessionNodeManager.getDb(), agentName: name, logger }).loadRegistrationState();
|
|
1718
|
+
if (!reg || reg.status !== "active") {
|
|
1719
|
+
result["warning"] = "not_registered";
|
|
1720
|
+
result["warning_guidance"] = `Agent '${name}' is now selected but is not registered with the directory — run 'cello register ${name}' to enable sessions with peers. Run 'cello status' to watch registration complete.`;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
catch (err) {
|
|
1724
|
+
// A failed registration read must not break selection (the agent IS selected). Log the real
|
|
1725
|
+
// reason, and surface a softer `registration_unknown` warning so the operator's surface is NOT
|
|
1726
|
+
// falsely clean — we could not confirm registration, so don't imply it is fine (Finding 3).
|
|
1727
|
+
logger.warn("agent.registration.read.failed", { agentName: name, reason: err instanceof Error ? err.message : String(err) });
|
|
1728
|
+
result["warning"] = "registration_unknown";
|
|
1729
|
+
result["warning_guidance"] = `Agent '${name}' is selected, but its registration status could not be read — run 'cello status' to check whether it is registered with the directory.`;
|
|
1730
|
+
}
|
|
1731
|
+
return result;
|
|
1670
1732
|
});
|
|
1671
1733
|
// ─── MCP-001: cello_list_agents handler ───
|
|
1672
1734
|
handlers.set("cello_list_agents", async (_params, connectionId) => {
|
|
@@ -2304,7 +2366,7 @@ export async function startDaemon(config) {
|
|
|
2304
2366
|
// ─── M8B DOD-REFRESH-1: cello_refresh_shares — proactive share refresh / epoch rollover ───
|
|
2305
2367
|
handlers.set("cello_refresh_shares", async (params, connectionId) => {
|
|
2306
2368
|
const connState = perConnectionState.get(connectionId);
|
|
2307
|
-
const agentName = params?.name
|
|
2369
|
+
const agentName = resolveCurrentAgent(connState, params?.name); // M8C-AUTOSTART-1 F18: sole-online fallback
|
|
2308
2370
|
if (!agentName) {
|
|
2309
2371
|
return { ok: false, reason: "no_current_agent", guidance: "Select an agent with cello_use_agent, or pass { name }." };
|
|
2310
2372
|
}
|
|
@@ -2336,7 +2398,7 @@ export async function startDaemon(config) {
|
|
|
2336
2398
|
// ─── M8B DOD-RELAYSIG-1: cello_get_relay_receipts — the agent's stored relay ordering receipts ───
|
|
2337
2399
|
handlers.set("cello_get_relay_receipts", async (params, connectionId) => {
|
|
2338
2400
|
const connState = perConnectionState.get(connectionId);
|
|
2339
|
-
const agentName = params?.name
|
|
2401
|
+
const agentName = resolveCurrentAgent(connState, params?.name); // M8C-AUTOSTART-1 F18: sole-online fallback
|
|
2340
2402
|
if (!agentName) {
|
|
2341
2403
|
return { ok: false, reason: "no_current_agent", guidance: "Select an agent with cello_use_agent, or pass { name }." };
|
|
2342
2404
|
}
|
|
@@ -2845,15 +2907,47 @@ export async function startDaemon(config) {
|
|
|
2845
2907
|
if (!connState || !connState.currentAgent) {
|
|
2846
2908
|
return NO_CURRENT_AGENT_RESPONSE;
|
|
2847
2909
|
}
|
|
2848
|
-
const
|
|
2849
|
-
|
|
2910
|
+
const agentName = connState.currentAgent;
|
|
2911
|
+
const cert = sessionNodeManager.getSealCertificate(agentName, sessionId);
|
|
2912
|
+
if (cert) {
|
|
2913
|
+
return { ok: true, session_id: sessionId, sealed_root: cert.sealed_root, legibility: cert.legibility };
|
|
2914
|
+
}
|
|
2915
|
+
// M8C-INBOX-1 (F4): the single `sealed_receipt_not_found` conflated four distinct causes, so a
|
|
2916
|
+
// caller could not tell a typo from a not-sealed-yet session from a wrong-agent selection.
|
|
2917
|
+
// Split them (full session ids on cello_list_sessions / cello_status already let a pasted id match).
|
|
2918
|
+
if (sessionNodeManager.getSessionRecord(agentName, sessionId)) {
|
|
2919
|
+
// The session is THIS agent's — it simply has no seal certificate yet.
|
|
2920
|
+
return {
|
|
2921
|
+
ok: false,
|
|
2922
|
+
reason: "not_sealed_yet",
|
|
2923
|
+
guidance: "This session exists but is not sealed yet. Close it with cello_close_session and confirm it reports sealed (or wait for the counterparty to co-seal), then retry.",
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
// Owned by a DIFFERENT loaded agent → the caller has the wrong current agent selected.
|
|
2927
|
+
const owner = loadedAgents.find((a) => a.name !== agentName && sessionNodeManager.getSessionRecord(a.name, sessionId));
|
|
2928
|
+
if (owner) {
|
|
2929
|
+
return {
|
|
2930
|
+
ok: false,
|
|
2931
|
+
reason: "wrong_agent",
|
|
2932
|
+
guidance: `This session belongs to agent '${owner.name}', not the current agent '${agentName}'. Call cello_use_agent('${owner.name}') then retry cello_get_sealed_receipt.`,
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
// A truncated paste: the id is a strict prefix of one of this agent's real session ids.
|
|
2936
|
+
const truncated = sessionNodeManager
|
|
2937
|
+
.getSessionsForAgent(agentName)
|
|
2938
|
+
.some((s) => s.session_id.startsWith(sessionId) && s.session_id.length > sessionId.length);
|
|
2939
|
+
if (truncated) {
|
|
2850
2940
|
return {
|
|
2851
2941
|
ok: false,
|
|
2852
|
-
reason: "
|
|
2853
|
-
guidance: "
|
|
2942
|
+
reason: "session_id_too_short",
|
|
2943
|
+
guidance: "That looks like a truncated session id. cello_list_sessions and cello status show the FULL id — copy the complete id and retry.",
|
|
2854
2944
|
};
|
|
2855
2945
|
}
|
|
2856
|
-
return {
|
|
2946
|
+
return {
|
|
2947
|
+
ok: false,
|
|
2948
|
+
reason: "unknown_session",
|
|
2949
|
+
guidance: "No session with this id belongs to the current agent. Run cello_list_sessions to see the full ids of this agent's sessions.",
|
|
2950
|
+
};
|
|
2857
2951
|
});
|
|
2858
2952
|
// DOD-LOG-1 (PERSIST-LOG-001): read the durable, decrypted conversation transcript for a session —
|
|
2859
2953
|
// the readable sent+received messages in canonical-sequence order, recovered AFTER a daemon restart
|
|
@@ -3200,6 +3294,32 @@ export async function startDaemon(config) {
|
|
|
3200
3294
|
}
|
|
3201
3295
|
return { ok: true };
|
|
3202
3296
|
});
|
|
3297
|
+
// M8C-INBOX-1: test hook to enqueue a pending inbound session request (mirrors the real inbound
|
|
3298
|
+
// flow's enqueueInboundSession) so cello_check_notifications' pending_session_requests is testable
|
|
3299
|
+
// without standing up the full libp2p inbound path.
|
|
3300
|
+
handlers.set("__test_enqueue_inbound_session", async (params, _connectionId) => {
|
|
3301
|
+
const agentName = params?.agentName;
|
|
3302
|
+
const sessionIdHex = params?.sessionId;
|
|
3303
|
+
const counterpartyPubkeyHex = params?.counterpartyPubkey ?? "";
|
|
3304
|
+
if (!agentName || !sessionIdHex) {
|
|
3305
|
+
return { error: "missing_params", guidance: "Provide agentName and sessionId." };
|
|
3306
|
+
}
|
|
3307
|
+
enqueueInboundSession(agentName, { sessionIdHex, counterpartyPubkeyHex, genesisPrevRootHex: "" });
|
|
3308
|
+
return { ok: true };
|
|
3309
|
+
});
|
|
3310
|
+
// M8C-INBOX-1 (reviewer F1): buffer a received message so a test can drive a live cello_receive
|
|
3311
|
+
// and assert the watermark advances (the N3 delivery-marks-read coupling), without a session tree.
|
|
3312
|
+
handlers.set("__test_buffer_received", async (params, _connectionId) => {
|
|
3313
|
+
const agentName = params?.agentName;
|
|
3314
|
+
const sessionId = params?.sessionId;
|
|
3315
|
+
const seq = params?.seq;
|
|
3316
|
+
const content = params?.content ?? "hello";
|
|
3317
|
+
if (!agentName || !sessionId || typeof seq !== "number") {
|
|
3318
|
+
return { error: "missing_params", guidance: "Provide agentName, sessionId, seq." };
|
|
3319
|
+
}
|
|
3320
|
+
sessionNodeManager.pushReceivedContentForTest(agentName, sessionId, seq, content, params?.senderPubkey ?? "cp");
|
|
3321
|
+
return { ok: true };
|
|
3322
|
+
});
|
|
3203
3323
|
} // end CELLO_ENV=test guard
|
|
3204
3324
|
// ─── M7-SESSION-001 (H-1): seal-interrupted bilateral RESPONDER ────────────
|
|
3205
3325
|
//
|
|
@@ -4307,6 +4427,9 @@ export async function startDaemon(config) {
|
|
|
4307
4427
|
// 1) Deliverable content wins — drain one buffered message per call (FIFO).
|
|
4308
4428
|
const entry = sessionNodeManager.takeReceivedContent(agentName, sessionId);
|
|
4309
4429
|
if (entry) {
|
|
4430
|
+
// M8C-INBOX-1 (N3): delivery marks read — advance the persisted read watermark so this
|
|
4431
|
+
// message no longer counts as unread in cello_check_notifications. Monotonic (never lowers).
|
|
4432
|
+
sessionNodeManager.advanceLastDeliveredSeq(agentName, sessionId, entry.sequenceNumber);
|
|
4310
4433
|
return {
|
|
4311
4434
|
ok: true,
|
|
4312
4435
|
content: Buffer.from(entry.contentHex, "hex").toString("utf8"),
|
|
@@ -4354,6 +4477,45 @@ export async function startDaemon(config) {
|
|
|
4354
4477
|
};
|
|
4355
4478
|
handlers.set("cello_receive", handleReceive);
|
|
4356
4479
|
handlers.set("cello_receive_session", handleReceive);
|
|
4480
|
+
// ─── M8C-INBOX-1 (N1/N4): cello_check_notifications — push-loss reconciler + poll-only inbox ───
|
|
4481
|
+
// Notifications are fire-and-forget (no ack, no redelivery); this is how a client discovers what
|
|
4482
|
+
// it missed while its shim was down/busy, and the primary inbox for poll-only clients (Bedrock,
|
|
4483
|
+
// cron). Content-free: pending session requests (from the in-memory queue, READ non-destructively —
|
|
4484
|
+
// cello_await_session owns draining) + unread message counts (derived from the persisted read
|
|
4485
|
+
// watermark, N2). No separate notification store; no ack verb (N4).
|
|
4486
|
+
handlers.set("cello_check_notifications", async (params, connectionId) => {
|
|
4487
|
+
const connState = perConnectionState.get(connectionId);
|
|
4488
|
+
const scope = params?.scope === "all" ? "all" : "current";
|
|
4489
|
+
let agentNames;
|
|
4490
|
+
if (scope === "all") {
|
|
4491
|
+
agentNames = loadedAgents.map((a) => a.name);
|
|
4492
|
+
}
|
|
4493
|
+
else {
|
|
4494
|
+
// F18: an explicit current agent, else the sole-online agent; ambiguous → no_current_agent.
|
|
4495
|
+
const current = resolveCurrentAgent(connState);
|
|
4496
|
+
if (!current) {
|
|
4497
|
+
return {
|
|
4498
|
+
ok: false,
|
|
4499
|
+
reason: "no_current_agent",
|
|
4500
|
+
guidance: "No current agent for this connection. Call cello_use_agent to select one, or use scope:\"all\" to check every loaded agent.",
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
agentNames = [current];
|
|
4504
|
+
}
|
|
4505
|
+
const agents = agentNames.map((agent) => {
|
|
4506
|
+
const pending = (inboundSessionQueues.get(agent) ?? []).map((e) => ({
|
|
4507
|
+
session_id: e.sessionIdHex,
|
|
4508
|
+
from: e.counterpartyPubkeyHex,
|
|
4509
|
+
}));
|
|
4510
|
+
const unread = sessionNodeManager.getUnreadSummary(agent);
|
|
4511
|
+
const total_unread = unread.reduce((sum, u) => sum + u.unread_count, 0);
|
|
4512
|
+
return { agent, pending_session_requests: pending, unread, total_unread };
|
|
4513
|
+
});
|
|
4514
|
+
const totalUnread = agents.reduce((sum, a) => sum + a.total_unread, 0);
|
|
4515
|
+
const totalPending = agents.reduce((sum, a) => sum + a.pending_session_requests.length, 0);
|
|
4516
|
+
logger.info("inbox.checked", { connectionId, scope, agentCount: agents.length, totalUnread, totalPending });
|
|
4517
|
+
return { ok: true, scope, agents };
|
|
4518
|
+
});
|
|
4357
4519
|
let shutdownPromise = null;
|
|
4358
4520
|
handlers.set("shutdown", async (_params, _connectionId) => {
|
|
4359
4521
|
if (!shutdownPromise) {
|