@cello-protocol/daemon 0.0.46 → 0.0.47

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,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/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;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA64L7E"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/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;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAy8L7E"}
package/dist/daemon.js CHANGED
@@ -786,6 +786,24 @@ export async function startDaemon(config) {
786
786
  cursor += 1;
787
787
  advanceConnectionCursor(connectionId, sessionId, cursor);
788
788
  }
789
+ /**
790
+ * DOD-CURSOR-DURABLE-1: the same hole-safe walk, applied to the PERSISTED per-(agent, session)
791
+ * read watermark. Used by cello_get_transcript, whose delivery covers BOTH directions.
792
+ *
793
+ * Walks a CONTIGUOUS run from the agent's current watermark, stopping at the first gap — for the
794
+ * identical reason safeCursorAdvance does: leaf indices are contiguous across both directions, so
795
+ * a gap can hide an unread RECEIVED message (e.g. a row that failed to decrypt is absent from
796
+ * `messages`). Advancing past it would mark unseen counterparty content as read and unblock a
797
+ * send that never saw it — defeating the very guarantee this gate exists to enforce. Monotonic:
798
+ * advanceLastDeliveredSeq takes MAX, so this can never lower a watermark.
799
+ */
800
+ function safeWatermarkAdvance(agentName, sessionId, deliveredSeqs) {
801
+ let frontier = sessionNodeManager.getLastDeliveredSeq(agentName, sessionId);
802
+ while (deliveredSeqs.has(frontier + 1))
803
+ frontier += 1;
804
+ if (frontier >= 0)
805
+ sessionNodeManager.advanceLastDeliveredSeq(agentName, sessionId, frontier);
806
+ }
789
807
  // M8C-AWAY-1: away response — an unattended Primary auto-answers session requests + messages
790
808
  // with the default transparent away text and queues them (the DoD's own mandated default).
791
809
  // CORE ships now; the operator-configurable custom-text / opaque-privacy-mode SWITCH is PARKED
@@ -3455,7 +3473,16 @@ export async function startDaemon(config) {
3455
3473
  // reviewer HIGH finding (aa5928e2/a9099571): recordTranscriptMessage swallows a DB write
3456
3474
  // failure without rolling back the tree's leaf count, so a hole in `messages` is possible in
3457
3475
  // principle; the contiguous-run walk refuses to vault the cursor past such a hole even here.
3458
- safeCursorAdvance(connectionId, sessionId, new Set(messages.map((m) => m.sequence)));
3476
+ const deliveredSeqs = new Set(messages.map((m) => m.sequence));
3477
+ safeCursorAdvance(connectionId, sessionId, deliveredSeqs);
3478
+ // DOD-CURSOR-DURABLE-1 (AC3): reading the full history IS reading — so this must advance the
3479
+ // PERSISTED watermark too, not just this connection's in-memory cursor. Previously it advanced
3480
+ // only the cursor, which made the gate's own guidance ("call cello_get_transcript, then retry")
3481
+ // a dead end for any stateless client: a fresh process reads the transcript, exits, and the next
3482
+ // process still presents an unread count. Same contiguous-run safety as the cursor — never vault
3483
+ // past a gap (an undecryptable row is absent from `messages`, so the walk stops there and those
3484
+ // messages correctly stay unread).
3485
+ safeWatermarkAdvance(agentName, sessionId, deliveredSeqs);
3459
3486
  return { ok: true, session_id: sessionId, messages, undecryptable };
3460
3487
  });
3461
3488
  // cello_list_sessions: the discovery surface — every persisted session for the
@@ -5019,18 +5046,52 @@ export async function startDaemon(config) {
5019
5046
  // rather than let it send blind — the WhatsApp-group-chat model. Runs BEFORE M9's
5020
5047
  // governance-decisions parsing below — an access-control gate should short-circuit before any
5021
5048
  // unrelated prep work for a send that may not even be allowed to proceed.
5049
+ // DOD-CURSOR-DURABLE-1: the gate now consults TWO authorities and passes if EITHER says the
5050
+ // caller is caught up.
5051
+ //
5052
+ // 1. the connection cursor (in-memory, per-connection) — today's rule, UNCHANGED. A
5053
+ // long-lived client (the MCP shim, one socket for the whole session) satisfies this
5054
+ // exactly as before, so the shipped single-connection path is byte-for-byte identical.
5055
+ //
5056
+ // 2. the persisted per-(agent, session) read watermark — NEW. A STATELESS client (the `cello`
5057
+ // CLI runs a fresh process, and therefore a fresh connection, per command) always presents
5058
+ // cursor -1, so authority 1 can never be satisfied by it: after the counterparty spoke
5059
+ // once, every CLI send was refused forever, even though the agent had demonstrably read
5060
+ // the message. That made a two-way conversation impossible from bash — and it silently hit
5061
+ // any RECONNECTING MCP client too, whose fresh connectionId also resets the cursor to -1.
5062
+ //
5063
+ // What the gate protects, precisely: an agent must never reply to COUNTERPARTY content nobody
5064
+ // on its side has seen. That guarantee is preserved and is now durable — it survives the socket.
5065
+ //
5066
+ // What it deliberately no longer protects (the approved trade — see
5067
+ // 2026-07-11_cursor-durable-read-before-write-design.md §6, Andre's go): a message this agent
5068
+ // SENT from a DIFFERENT local connection no longer blocks. Two attended windows on one agent
5069
+ // used to gate each other; now they do not, because a locally-authored message is not "unread
5070
+ // counterparty content" — the AGENT wrote it. The principal here is the agent, not the socket,
5071
+ // and the daemon cannot referee which of an operator's own windows a human is looking at.
5022
5072
  const currentSeq = record.message_count - 1;
5023
- const lastReadSeq = getConnectionCursor(connectionId, sessionId);
5024
- if (lastReadSeq < currentSeq) {
5073
+ const connectionCursor = getConnectionCursor(connectionId, sessionId);
5074
+ const unreadReceived = sessionNodeManager.getUnreadReceivedCount(agentName, sessionId);
5075
+ const caughtUp = connectionCursor >= currentSeq || unreadReceived === 0;
5076
+ if (!caughtUp) {
5025
5077
  // M8C-CURSOR-1 (reviewer MEDIUM fix): every sibling rejection in this handler logs; this
5026
5078
  // gate must too — a security-relevant control-flow path with no observability is a gap.
5027
- logger.warn("session.send.blocked", { sessionId, currentSeq, lastReadSeq, connectionId });
5079
+ // Both authorities are logged, so the next reader can tell WHICH one refused.
5080
+ logger.warn("session.send.blocked", {
5081
+ sessionId,
5082
+ currentSeq,
5083
+ lastReadSeq: connectionCursor,
5084
+ unreadReceived,
5085
+ connectionId,
5086
+ agentName,
5087
+ });
5028
5088
  return {
5029
5089
  ok: false,
5030
5090
  reason: "session_not_current",
5031
5091
  current_seq: currentSeq,
5032
- last_read_seq: lastReadSeq,
5033
- guidance: `This connection hasn't caught up on session ${sessionId} — ${currentSeq - lastReadSeq} message(s) unread (this may include messages authored by another connection on this same agent). Call cello_get_transcript to read the full history (covers both sent and received), then retry the send.`,
5092
+ last_read_seq: connectionCursor,
5093
+ unread_received: unreadReceived,
5094
+ guidance: `This agent hasn't read ${unreadReceived} received message(s) on session ${sessionId}. Call cello_receive (or cello_get_transcript for the full history) to read them, then retry the send.`,
5034
5095
  };
5035
5096
  }
5036
5097
  // M9-FEED-001 §6: the agent's governance re-send decisions, keyed by the flagId a prior `warn`