@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.
@@ -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,CAmkJ7E"}
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
- let state;
1258
- if (a.name === currentAgent && onlineAgents.has(a.name)) {
1259
- state = "current";
1260
- }
1261
- else if (onlineAgents.has(a.name)) {
1262
- state = "online";
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
- handlers.set("cello_start_agent", async (params, _connectionId) => {
1352
- const name = params?.name;
1353
- if (!name) {
1354
- return { ok: false, reason: "missing_params", guidance: "Provide 'name' parameter with the agent name to start." };
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. Check agent names with cello_list_agents.` };
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
- return { ok: true };
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 ?? connState?.currentAgent;
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 ?? connState?.currentAgent;
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 cert = sessionNodeManager.getSealCertificate(connState.currentAgent, sessionId);
2849
- if (!cert) {
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: "sealed_receipt_not_found",
2853
- guidance: "No sealed certificate is recorded for this session. It may not be sealed yet, or the session_id is wrong close it with cello_close_session and confirm it reports sealed, then retry.",
2942
+ reason: "session_id_too_short",
2943
+ guidance: "That looks like a truncated session id. cello_list_sessions and cello status show the FULL idcopy the complete id and retry.",
2854
2944
  };
2855
2945
  }
2856
- return { ok: true, session_id: sessionId, sealed_root: cert.sealed_root, legibility: cert.legibility };
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) {