@cello-protocol/daemon 0.0.213 → 0.0.215

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.
Files changed (51) hide show
  1. package/dist/attendance-wiring.d.ts +0 -2
  2. package/dist/attendance-wiring.d.ts.map +1 -1
  3. package/dist/attendance-wiring.js +2 -6
  4. package/dist/attendance-wiring.js.map +1 -1
  5. package/dist/boot-connection-state.d.ts +0 -2
  6. package/dist/boot-connection-state.d.ts.map +1 -1
  7. package/dist/boot-connection-state.js +5 -46
  8. package/dist/boot-connection-state.js.map +1 -1
  9. package/dist/close-session-handler.d.ts.map +1 -1
  10. package/dist/close-session-handler.js +12 -3
  11. package/dist/close-session-handler.js.map +1 -1
  12. package/dist/co-attendance.d.ts +8 -72
  13. package/dist/co-attendance.d.ts.map +1 -1
  14. package/dist/co-attendance.js +8 -116
  15. package/dist/co-attendance.js.map +1 -1
  16. package/dist/daemon.js +2 -7
  17. package/dist/daemon.js.map +1 -1
  18. package/dist/disconnect-cleanup.d.ts +0 -1
  19. package/dist/disconnect-cleanup.d.ts.map +1 -1
  20. package/dist/disconnect-cleanup.js +1 -9
  21. package/dist/disconnect-cleanup.js.map +1 -1
  22. package/dist/document-gate-wiring.d.ts.map +1 -1
  23. package/dist/document-gate-wiring.js +7 -1
  24. package/dist/document-gate-wiring.js.map +1 -1
  25. package/dist/session-content-handlers.d.ts +0 -17
  26. package/dist/session-content-handlers.d.ts.map +1 -1
  27. package/dist/session-content-handlers.js +94 -284
  28. package/dist/session-content-handlers.js.map +1 -1
  29. package/dist/session-content-ingest.d.ts.map +1 -1
  30. package/dist/session-content-ingest.js +8 -3
  31. package/dist/session-content-ingest.js.map +1 -1
  32. package/dist/session-content-send.js +1 -1
  33. package/dist/session-content-send.js.map +1 -1
  34. package/dist/session-delivery-acks.d.ts.map +1 -1
  35. package/dist/session-delivery-acks.js +18 -3
  36. package/dist/session-delivery-acks.js.map +1 -1
  37. package/dist/session-notify.js +1 -1
  38. package/dist/session-notify.js.map +1 -1
  39. package/dist/session-relay-client.d.ts.map +1 -1
  40. package/dist/session-relay-client.js +2 -0
  41. package/dist/session-relay-client.js.map +1 -1
  42. package/dist/session-seal.d.ts.map +1 -1
  43. package/dist/session-seal.js +36 -11
  44. package/dist/session-seal.js.map +1 -1
  45. package/dist/session-terminal-refusal.d.ts +3 -0
  46. package/dist/session-terminal-refusal.d.ts.map +1 -1
  47. package/dist/session-terminal-refusal.js +21 -0
  48. package/dist/session-terminal-refusal.js.map +1 -1
  49. package/dist/telegram-doorbell.js +1 -1
  50. package/dist/telegram-doorbell.js.map +1 -1
  51. package/package.json +5 -5
@@ -178,7 +178,7 @@ export function sentAuthorship(r) {
178
178
  return r.authorship;
179
179
  }
180
180
  export function registerSessionContentHandlers(deps) {
181
- const { handlers, logger, sessionNodeManager, securityGateway, retryQueue, getConnState, resolveCurrentAgent, NO_CURRENT_AGENT_RESPONSE, getConnectionCursor, advanceConnectionCursor, safeCursorAdvance, getDeliveryBookmark, advanceDeliveryBookmark, clearTelegramRung, attendanceCount, contentTakes, } = deps;
181
+ const { handlers, logger, sessionNodeManager, securityGateway, retryQueue, getConnState, resolveCurrentAgent, NO_CURRENT_AGENT_RESPONSE, getConnectionCursor, advanceConnectionCursor, clearTelegramRung, attendanceCount, } = deps;
182
182
  /**
183
183
  * DOD-COATTEND-SENDWINDOW-1 (review F1) — sessions with a send ON THE WIRE right now.
184
184
  *
@@ -622,7 +622,7 @@ export function registerSessionContentHandlers(deps) {
622
622
  const placed = sessionNodeManager.placeOwnLeaf(record.agent_name, sessionId, contentHashHex, sendBytes, sendResult.sequenceNumber, correlationId, "msg", sentAuthorship(sendResult));
623
623
  // `sequence_number` is a LEAF INDEX or nothing. Reporting the relay's number when no leaf
624
624
  // exists hands the caller a value that matches no leaf on this side, and anything that
625
- // feeds it to a cursor or a `--since-seq` read is then wrong.
625
+ // feeds it to a cursor is then wrong.
626
626
  const queuedLeaf = placed.placed ? placed.leafIndex : -1;
627
627
  if (placed.placed) {
628
628
  sessionNodeManager.recordTranscriptMessage(record.agent_name, sessionId, queuedLeaf, "sent", sendBytes, correlationId, sentAuthorship(sendResult));
@@ -960,8 +960,8 @@ export function registerSessionContentHandlers(deps) {
960
960
  // DOD-UNREAD-1 D4b: a TRANSCRIPT-ONLY session — received rows exist but no sessions row (the
961
961
  // pre-D3/D4a phantom residue: the counterparty's reply landed while this side refused the
962
962
  // session). Those rows are counted unread by getUnreadSummary, so cello_receive MUST be able
963
- // to read them or the badge can never clear. Reading is a transcript operation: the since_seq
964
- // catch-up below works from the durable transcript alone. Only a session with NEITHER a row
963
+ // to read them or the badge can never clear. Reading is a transcript operation: the read below
964
+ // works from the durable transcript alone. Only a session with NEITHER a row
965
965
  // NOR transcript rows is truly not found.
966
966
  let transcriptOnly = false;
967
967
  if (!record) {
@@ -1041,139 +1041,103 @@ export function registerSessionContentHandlers(deps) {
1041
1041
  if (record && record.status === "interrupted") {
1042
1042
  void sessionNodeManager.reviveIfNeededForRead(agentName, sessionId).catch(() => { });
1043
1043
  }
1044
- // M8C-SINCESEQ-1: stateless catch-up. When since_seq is provided, return a BATCH of received
1045
- // transcript messages with sequence > since_seq (durable transcript, not the ephemeral buffer —
1046
- // so concurrent arrivals don't shift what a given since_seq returns; no replay race). Replaces
1047
- // the cello_get_transcript workaround for away-then-return. Received-direction only (the messages
1048
- // you'd have gotten live). Advances the read watermark (delivery marks read — clears INBOX
1049
- // unread). A distinct early branch: the plain (no since_seq) receive is entirely unchanged.
1050
- const rawSince = params?.since_seq;
1051
- if (typeof rawSince === "number" && Number.isFinite(rawSince)) {
1052
- const sinceSeq = rawSince;
1053
- // D4b AC2: a transcript-only session has no record to attribute from — `from` is null,
1054
- // NEVER the string "unknown" (the transcript stores no counterparty; don't invent one).
1055
- const from = record ? record.counterparty_pubkey : null;
1056
- const { messages } = sessionNodeManager.readTranscript(agentName, sessionId);
1057
- const received = messages.filter((m) => m.direction === "received" && m.sequence > sinceSeq);
1058
- // Read ONCE, above both authorities below — the watermark walk and the connection cursor
1059
- // both need it, and reading the tree twice invites the two to disagree about the same leaf.
1060
- const leafKinds = sessionNodeManager.getSessionTree(agentName, sessionId).leaves();
1061
- /** A `doc`/`reject` leaf occupies a sequence but is not a message: present, never unread. */
1062
- const notAMessage = (seq) => {
1063
- const kind = leafKinds[seq]?.kind;
1064
- return kind === "doc" || kind === "reject";
1044
+ // Catch-up mode is GONE (Andre, live test 2026-09-13): the ordinary read already returns
1045
+ // everything unread. Refused by name so an old caller learns that, rather than being ignored.
1046
+ if (params?.since_seq !== undefined) {
1047
+ return {
1048
+ ok: false,
1049
+ reason: "since_seq_removed",
1050
+ guidance: "since_seq no longer exists. Call cello_receive without it — it returns every unread message at once. For the whole conversation, use cello_transcript.",
1065
1051
  };
1066
- if (received.length > 0) {
1067
- // ─── review F8 (pre-existing, fixed here): this was a RAW VAULT to the highest received
1068
- // sequence in the batch `advanceLastDeliveredSeq(…, maxSeq)` which jumps any leaf
1069
- // in between. The leaf it jumps can be a transcript row that failed to decrypt:
1070
- // `readTranscript` drops such a row from `messages`, but `getUnreadReceivedCount`
1071
- // still counts it. So an undecryptable message was silently marked read, stopped
1072
- // counting as unread, and cleared the send gate's second authority — a message nobody
1073
- // could read, reported as read. That is precisely what CATCHUP AC3 forbids of a
1074
- // catch-up path, on the door that was NOT chosen, which is why it survived this long.
1075
- //
1076
- // TWO THINGS THE WALK MUST GET RIGHT, and I got the second one wrong first (review H1).
1077
- //
1078
- // (a) SEEDED AT `sinceSeq`, not at the stored watermark. `since_seq: N` is the CALLER
1079
- // ASSERTING it already holds through N, so rows at or below N are its claim and must
1080
- // not block the advance. Seeding at the watermark makes ordinary catch-up stop dead
1081
- // at the first row the caller skipped M8C-SINCESEQ-1 S1/S2/S3 caught that.
1082
- //
1083
- // (b) CONTIGUOUS OVER **BOTH DIRECTIONS**, not over the received-only batch. Leaf
1084
- // indices are contiguous across both directions, so a SENT leaf — this agent's own
1085
- // reply, or a sibling connection's — is a hole in any received-only set. Walking
1086
- // `received` therefore stopped on the most ordinary event in the protocol, and
1087
- // reading everything no longer cleared unread: the badge could not be cleared, and
1088
- // a stateless CLI caller was refused forever through the very door the guidance
1089
- // points at. That is CATCHUP §3b's own defect — a rule satisfiable only through a
1090
- // door the caller is not pointed at — reintroduced on the watermark.
1091
- //
1092
- // The same received-only-view mistake as DOD-COATTEND-1 review F1, made a second time
1093
- // one file over: `daemon.ts` already says "every sibling send is a hole" in as many
1094
- // words. `cello_get_transcript` gets this right by walking both directions; so does this
1095
- // now. What still stops the walk is a genuinely absent index — an undecryptable row,
1096
- // which has no transcript row at all and IS unread.
1097
- //
1098
- // ⚠️ **A SCREENED-OUT LEAF USED TO BE IN THAT LIST AND NO LONGER BELONGS — review F5.**
1099
- // Corrected rather than deleted, because the sentence was true when written and its change
1100
- // is a behaviour change worth seeing. `DOD-M15-REFUSEDEVIDENCE-1` gives a blocked message a
1101
- // transcript row (flagged `quarantined`), so the walk now CROSSES it — which is the right
1102
- // answer and the same one already reached for `doc` leaves below: a blocked message is not
1103
- // unread, never will be, and can never be delivered, so wedging the watermark behind it
1104
- // strands every later message forever. It is included deliberately, not incidentally.
1105
- let frontier = sinceSeq;
1106
- const presentSeqs = new Set(messages.map((m) => m.sequence));
1107
- // A DOCUMENT LEAF IS NOT A HOLE it is a frame that was never a message.
1108
- //
1109
- // `doc` and `reject` leaves take a sequence number and deliberately write no transcript row
1110
- // ("A document frame is NOT a transcript message" — document-frame-router.ts), so from the
1111
- // transcript side they are indistinguishable from a row that is missing because it could
1112
- // never be read. The walk treated both as unread and stopped, permanently: a pair that had
1113
- // ever exchanged one document frame could never catch up again, and the send gate refused
1114
- // them while the guidance pointed at the very tool they had just used. Observed live, four
1115
- // holes in twenty minutes between two agents who never mentioned a document to each other.
1116
- //
1117
- // KEYED ON THE LEAF KIND, never on row-absence. That distinction is the entire fix: absent
1118
- // -and-unreadable and absent-because-not-a-message look identical from the transcript, and
1119
- // a test for a missing row would relocate the bug rather than close it.
1120
- while (presentSeqs.has(frontier + 1) || notAMessage(frontier + 1))
1121
- frontier += 1;
1122
- sessionNodeManager.advanceLastDeliveredSeq(agentName, sessionId, frontier); // MONOTONIC — takes MAX
1123
- clearTelegramRung(agentName, sessionId); // M8C-TGDOOR-1: read clears the ring
1124
- }
1125
- // M8C-CURSOR-1 (reviewer HIGH fix): only advance through the CONTIGUOUS run this batch
1126
- // actually delivered — if a sent leaf from another local connection sits in a gap, this
1127
- // correctly refuses to advance past it (cello_get_transcript is still required to catch up).
1128
- // The CONNECTION CURSOR needs the same treatment, and for the same reason. It is the second
1129
- // authority the send gate consults — the gate passes if EITHER is satisfied — so leaving this
1130
- // one keyed on received-rows-only means a document leaf still wedges it, and the caller is
1131
- // still refused after reading everything there is to read.
1132
- safeCursorAdvance(connectionId, sessionId, new Set([
1133
- ...received.map((m) => m.sequence),
1134
- ...leafKinds.flatMap((leaf, index) => (leaf.kind === "doc" || leaf.kind === "reject" ? [index] : [])),
1135
- ]));
1136
- logger.info("session.receive.since_seq", { sessionId, agentName, since_seq: sinceSeq, count: received.length });
1137
- // DOD-M12B-AWAY-MARK-1: an away auto-reply arrives as an ordinary msg leaf at a real sequence,
1138
- // so without this the batch read is positive evidence a person answered. Two agents spent the
1139
- // morning of 2026-08-17 exchanging each other's away responders while both operators believed
1140
- // a conversation was happening. Per-message boolean first (a caller can branch on it), one
1141
- // guidance line second, and only when there is something to explain.
1142
- const autoReplyCount = received.filter((m) => isAutoReplyMarked(m.text)).length;
1052
+ }
1053
+ /**
1054
+ * ─── ONE READ, EVERY UNREAD MESSAGE, ONE BOOKMARK ───────────────────────────────────────────
1055
+ *
1056
+ * The bookmark is the agent's persisted watermark for this session. Everything received after it
1057
+ * is handed over together and the bookmark moves to the last of them.
1058
+ *
1059
+ * It replaced three bookmarks, and the live test on 2026-09-13 shows why: catch-up moved two of
1060
+ * them, the plain read consulted a third that belonged to the CONNECTION and started empty on
1061
+ * every new one — so after catching up the next read re-served the old message, and every `/mcp`
1062
+ * reconnect or `cello` CLI command started over at the oldest message in the conversation.
1063
+ *
1064
+ * The bookmark moves to the LAST message handed over, not through a gap-stopping walk. A hole
1065
+ * a blocked message, a document frame, a row that could not be decrypted would otherwise strand
1066
+ * every later message, re-served on every call. A row that could not be written is reported on
1067
+ * the empty answer (`content_undeliverable`) rather than by holding the bookmark behind it.
1068
+ */
1069
+ const from = record ? record.counterparty_pubkey : null;
1070
+ const takeUnread = () => {
1071
+ const watermark = sessionNodeManager.getLastDeliveredSeq(agentName, sessionId);
1072
+ // Asked ~47x/second while blocked, so the cheap SQL probe runs first and the transcript is
1073
+ // decoded only when something is actually there.
1074
+ if (!sessionNodeManager.findNextReceivedAfter(agentName, sessionId, watermark))
1075
+ return null;
1076
+ const unread = sessionNodeManager.readTranscript(agentName, sessionId).messages
1077
+ .filter((m) => m.direction === "received" && m.sequence > watermark);
1078
+ if (unread.length === 0)
1079
+ return null;
1080
+ sessionNodeManager.advanceLastDeliveredSeq(agentName, sessionId, unread[unread.length - 1].sequence);
1081
+ clearTelegramRung(agentName, sessionId); // M8C-TGDOOR-1: read clears the ring
1082
+ return Object.assign(unread, { previousWatermark: watermark });
1083
+ };
1084
+ const deliver = (unread, correlationId) => {
1085
+ logger.info("session.receive.delivered", {
1086
+ sessionId, agentName, connectionId, count: unread.length,
1087
+ firstSequence: unread[0].sequence, lastSequence: unread[unread.length - 1].sequence,
1088
+ attendance: attendanceCount(agentName), correlationId,
1089
+ });
1090
+ const lastText = unread[unread.length - 1].text.trimEnd();
1091
+ // The turn signal belongs to the LAST message: it is what says whose move it is now.
1092
+ const signalGuidance = lastText.endsWith("[[WRAP]]")
1093
+ ? "Counterparty wrapped. Call cello_close_session nowdo not reply."
1094
+ : lastText.endsWith("[[OVER]]")
1095
+ ? "Counterparty's turn is done. Counterparty has indicated they are expecting a reply use cello_send to reply."
1096
+ : /\[\[STANDBY EST:\d+m\]\]$/.test(lastText)
1097
+ ? "Counterparty is working and will follow up when done — no response expected. To block: call cello_receive with a longer timeout_ms. To check back later: schedule a cron and call cello_receive then."
1098
+ : undefined;
1099
+ // DOD-M12B-AWAY-MARK-1: an away auto-reply is an ordinary message leaf, so without the mark a
1100
+ // read is positive evidence a person answered. The content is passed through WHOLE.
1101
+ const autoReplyCount = unread.filter((m) => isAutoReplyMarked(m.text)).length;
1102
+ // A message that arrived but could not be written to the transcript has no row, so the
1103
+ // bookmark just moved past it. Say so HERE, on the answer that crossed it otherwise the
1104
+ // operator sees only a gap in the sequence numbers (review of 86ec7524).
1105
+ const lastSeq = unread[unread.length - 1].sequence;
1106
+ // Only the lost messages THIS read moved past — the in-memory set is never pruned, so without
1107
+ // the lower bound every later read would repeat the warning until a restart.
1108
+ const readFrom = unread.previousWatermark ?? -1;
1109
+ const crossed = sessionNodeManager.getUndeliverableSeqs(agentName, sessionId).filter((s) => s > readFrom && s <= lastSeq);
1143
1110
  return {
1144
1111
  ok: true,
1145
- since_seq: sinceSeq,
1146
- count: received.length,
1147
- // DOD-M15-REFUSED-INBOUND-SILENT-1: refusals the operator has not been shown yet. Omitted
1148
- // entirely when there are none — an always-present empty array trains readers to skip the
1149
- // field, and this is the field that explains why a conversation went quiet.
1150
- ...refusalsField(sessionNodeManager, agentName, sessionId, connectionId),
1151
- ...(autoReplyCount > 0 ? { auto_reply_guidance: AUTO_REPLY_GUIDANCE } : {}),
1152
- // AC6 (review F1): the catch-up batch is THE stateless-client door — `cello receive <id>
1153
- // --since-seq -1` is a fresh connection every time, so it never saw a doorbell, and the
1154
- // `session_not_live` refusal below points callers here BY NAME. Shipping attendance on the
1155
- // live exits and not this one left the defect alive in the exact shape the AC exists for.
1156
- attendance: attendingNow(agentName),
1157
- // The content is passed through WHOLE — marker included, nothing stripped. The marker
1158
- // labels; it never suppresses, and a reader that never sees it cannot judge it.
1159
- messages: received.map((m) => ({
1112
+ session_id: sessionId,
1113
+ count: unread.length,
1114
+ ...(crossed.length > 0 ? {
1115
+ undeliverable_sequences: [...crossed],
1116
+ undeliverable_guidance: `${crossed.length} message(s) arrived but could not be written to the local transcript, so they cannot be shown. This is a fault on THIS machine (check disk space and ~/.cello permissions), not the counterparty. Ask them to resend once it is fixed.`,
1117
+ } : {}),
1118
+ messages: unread.map((m) => ({
1160
1119
  sequence: m.sequence,
1161
1120
  content: m.text,
1162
1121
  from,
1163
1122
  ...(isAutoReplyMarked(m.text) ? { auto_reply: true } : {}),
1164
1123
  })),
1124
+ ...(autoReplyCount > 0 ? { auto_reply_guidance: AUTO_REPLY_GUIDANCE } : {}),
1125
+ ...refusalsField(sessionNodeManager, agentName, sessionId, connectionId),
1126
+ attendance: attendingNow(agentName),
1127
+ ...(signalGuidance !== undefined ? { guidance: signalGuidance } : {}),
1165
1128
  };
1166
- }
1167
- // D4b AC3: the plain (blocking) receive waits on a LIVE session's buffer a transcript-only
1168
- // session has no live node and nothing will ever arrive. Waiting to a null timeout would be
1169
- // misleading and session_not_found would be a lie (the transcript exists). A distinct reason
1170
- // points the caller at the read that works.
1129
+ };
1130
+ // D4b: a transcript-only session has no live node, so nothing new will ever arrive. Hand over
1131
+ // what is unread without waiting; if nothing is, say where the history is.
1171
1132
  if (transcriptOnly) {
1133
+ const unread = takeUnread();
1134
+ if (unread)
1135
+ return deliver(unread, randomUUID());
1172
1136
  return {
1173
1137
  ok: false,
1174
1138
  reason: "session_not_live",
1175
1139
  attendance: attendingNow(agentName),
1176
- guidance: "This session exists only as a durable transcript (no live session it was never established or predates this daemon). Read it with cello_receive { since_seq } (e.g. since_seq: -1 for everything) or cello_transcript.",
1140
+ guidance: "This session exists only as a stored transcript and nothing in it is unread. Read the whole conversation with cello_transcript.",
1177
1141
  };
1178
1142
  }
1179
1143
  const rawTimeout = params?.timeout_ms;
@@ -1205,6 +1169,10 @@ export function registerSessionContentHandlers(deps) {
1205
1169
  const terminal = sessionNodeManager.peekTerminalMarker(agentName, sessionId);
1206
1170
  if (terminal) {
1207
1171
  const sealedRoot = sessionNodeManager.getSealedRootHex(agentName, sessionId);
1172
+ // ⚠️ Unread messages are deliberately NOT handed over here, although that is the one place
1173
+ // "every unread message" does not hold. A sealed conversation's leftover message is not live
1174
+ // work: on 2026-08-05 an agent obeyed a [[STANDBY]] from a session that had ended. The
1175
+ // operator is pointed at cello_transcript instead (DOD-TERMINAL-WAKE-1).
1208
1176
  // AC3 covers "both outcomes" — got something / got nothing. This is the third exit from the
1209
1177
  // same silent handler, and leaving it silent would reproduce the defect one branch over.
1210
1178
  logger.info("session.receive.sealed", {
@@ -1230,102 +1198,9 @@ export function registerSessionContentHandlers(deps) {
1230
1198
  : "The session has been sealed by both parties. The full history is available via cello_transcript. No further actions are required on this session.",
1231
1199
  };
1232
1200
  }
1233
- // ─── DOD-COATTEND-1: read the DURABLE RECORD against THIS connection's bookmark ───
1234
- //
1235
- // This used to be `takeReceivedContent`, a `buf.shift()` on a buffer keyed
1236
- // (agentName, sessionId) and NOT by connection. The doorbell is multicast, so two attached
1237
- // sessions were both woken, both entered this loop, and whichever hit the next 20 ms tick
1238
- // first REMOVED the message from the other's view — which was then told, word for word, what
1239
- // a quiet counterparty produces.
1240
- //
1241
- // Now: the transcript is the source of truth and each connection has its own cursor, so
1242
- // reading is non-destructive by construction. Nothing one consumer does mutates state another
1243
- // consumer reads — which is exactly what `shift()` violated. The doorbell STAYS multicast
1244
- // (AC 2); the queue was the defect, not the wake-up.
1245
- //
1246
- // Ordering is safe: `#appendVerifiedContent` writes the transcript row (`:3996`) BEFORE it
1247
- // pushes to the buffer (`:4003`), so the record can never lag the queue this replaces.
1248
- //
1249
- // The bar is the DELIVERY BOOKMARK, not the gate's read cursor (review F1, BLOCKING). Using
1250
- // the cursor here pinned this connection below the first gap in its received-only view — and
1251
- // a sibling connection's SENT leaf is such a gap, as is a security-gateway block that leaves
1252
- // a permanent hole. The same message was then re-served on every call, forever. The gate's
1253
- // cursor stays exactly as it was; it simply is not the thing that answers this question.
1254
- const deliveredThrough = getDeliveryBookmark(connectionId, sessionId);
1255
- // Asked ~47x/second per blocked connection, so the predicate is in SQL and exactly one blob is
1256
- // decoded (review F5). The obvious `readTranscript().messages.find(...)` decoded the entire
1257
- // session on every tick and threw all of it away.
1258
- //
1259
- // Guarded on `record` (review F7 claimed this guard was dead — it is NOT, and the typecheck
1260
- // proved it): a TRANSCRIPT-ONLY session has received rows and no `sessions` row, and it does
1261
- // not return above, it falls through with `record === null`. Live delivery needs the
1262
- // counterparty pubkey, which only the record carries, so those sessions are read through the
1263
- // since_seq catch-up from the transcript alone — exactly as before this change. Dropping the
1264
- // guard is a null dereference on the one session shape that reaches here without a record.
1265
- const nextRow = record
1266
- ? sessionNodeManager.findNextReceivedAfter(agentName, sessionId, deliveredThrough)
1267
- : null;
1268
- const entry = nextRow
1269
- ? {
1270
- contentHex: Buffer.from(nextRow.text, "utf8").toString("hex"),
1271
- senderPubkey: record.counterparty_pubkey,
1272
- sequenceNumber: nextRow.sequence,
1273
- }
1274
- : null;
1275
- if (entry) {
1276
- // M8C-INBOX-1 (N3): delivery marks read — advance the persisted read watermark so this
1277
- // message no longer counts as unread in cello_check_notifications. Monotonic (never lowers).
1278
- sessionNodeManager.advanceLastDeliveredSeq(agentName, sessionId, entry.sequenceNumber);
1279
- clearTelegramRung(agentName, sessionId); // M8C-TGDOOR-1: read clears the ring
1280
- // M8C-CURSOR-1 (reviewer HIGH fix): a single delivered message only proves THIS sequence
1281
- // was read — safeCursorAdvance refuses to vault past a gap (e.g. an unread sent leaf from
1282
- // another local connection) even though this specific sequence number is now known.
1283
- safeCursorAdvance(connectionId, sessionId, new Set([entry.sequenceNumber]));
1284
- // ...and separately, this connection has now BEEN HANDED this leaf. Monotonic, no gap walk:
1285
- // that is the whole distinction (review F1). Without this the read above re-finds the same
1286
- // row on the next call.
1287
- advanceDeliveryBookmark(connectionId, sessionId, entry.sequenceNumber);
1288
- // DOD-COATTEND-VISIBLE-1: the ledger of which connection was handed which leaf.
1289
- //
1290
- // It was written when this WAS a destructive drain, and the comment here used to say so —
1291
- // "the message has just been REMOVED from every co-attending session's view". That is no
1292
- // longer true and had become the most misleading sentence in the file (review F6): it
1293
- // asserted the very property this unit removed, at the one site a reader would come to
1294
- // check it. Delivery is non-destructive now; the ledger survives because the `taken_by_
1295
- // sibling_session` discriminator still reads it, and its deletion is its own unit.
1296
- contentTakes.record(agentName, sessionId, connectionId, entry.sequenceNumber);
1297
- logger.info("session.receive.delivered", {
1298
- sessionId, agentName, connectionId, sequenceNumber: entry.sequenceNumber,
1299
- attendance: attendanceCount(agentName), correlationId: receiveCorrelationId,
1300
- });
1301
- const contentText = Buffer.from(entry.contentHex, "hex").toString("utf8");
1302
- const trimmed = contentText.trimEnd();
1303
- const signalGuidance = trimmed.endsWith("[[WRAP]]")
1304
- ? "Counterparty wrapped. Call cello_close_session now — do not reply."
1305
- : trimmed.endsWith("[[OVER]]")
1306
- ? "Counterparty's turn is done. Counterparty has indicated they are expecting a reply — use cello_send to reply."
1307
- : /\[\[STANDBY EST:\d+m\]\]$/.test(trimmed)
1308
- ? "Counterparty is working and will follow up when done — no response expected. To block: call cello_receive with a longer timeout_ms. To check back later: schedule a cron and call cello_receive then."
1309
- : undefined;
1310
- // DOD-M12B-AWAY-MARK-1: same fact on the live exit as on the batch exit above. This is the
1311
- // one an attended agent actually hits, so leaving it off here would have left the defect
1312
- // alive in the shape it was measured in.
1313
- const isAutoReply = isAutoReplyMarked(contentText);
1314
- return {
1315
- ok: true,
1316
- content: contentText,
1317
- ...(isAutoReply ? { auto_reply: true, auto_reply_guidance: AUTO_REPLY_GUIDANCE } : {}),
1318
- // AC6: every READ answer says whether this session is alone. The push already carried
1319
- // this; the read surfaces did not, so a session that never saw a doorbell — a fresh MCP
1320
- // connection, EVERY `cello` CLI invocation, anything that attached after the last
1321
- // arrival — had no way to learn it was co-attended. Live finding, journal Entry 33.
1322
- attendance: attendingNow(agentName),
1323
- sessionId,
1324
- sequence_number: entry.sequenceNumber,
1325
- senderPubkey: entry.senderPubkey,
1326
- ...(signalGuidance !== undefined ? { guidance: signalGuidance } : {}),
1327
- };
1328
- }
1201
+ const unread = takeUnread();
1202
+ if (unread)
1203
+ return deliver(unread, receiveCorrelationId);
1329
1204
  // 3) Out of time — non-blocking-equivalent empty answer.
1330
1205
  const remaining = deadline - Date.now();
1331
1206
  if (remaining <= 0) {
@@ -1357,71 +1232,6 @@ export function registerSessionContentHandlers(deps) {
1357
1232
  }
1358
1233
  const liveness = sessionNodeManager.getSessionLiveness(agentName, sessionId);
1359
1234
  const attendance = attendanceCount(agentName);
1360
- // ─── DOD-COATTEND-VISIBLE-1 AC1: the loser of the race gets a DIFFERENT answer ───
1361
- //
1362
- // The buffer is keyed (agentName, sessionId) and drained with `buf.shift()`, so when the
1363
- // multicast doorbell wakes two attending sessions the faster one REMOVES the message. Until
1364
- // this branch existed the slower one was told, word for word, what a quiet counterparty
1365
- // produces — and nothing was logged either, so the theft left no trace anywhere.
1366
- //
1367
- // The bar is this connection's own read cursor, not a time window: it reports content this
1368
- // connection genuinely has not seen, and it CLEARS itself once the caller catches up. The
1369
- // discriminator is `reason` — the field this very return already uses for its other branch —
1370
- // so a caller switching on it needs no new shape. `taken_by_sibling` carries the machine-
1371
- // readable detail; the prose below is the presentation of that, never a substitute for it.
1372
- const missed = contentTakes.missedBy(agentName, sessionId, connectionId, getConnectionCursor(connectionId, sessionId));
1373
- if (missed) {
1374
- // The message a sibling took is described WITHOUT claiming that sibling still attends
1375
- // this agent (review MEDIUM). A connection may operate on the sole online agent through
1376
- // `resolveCurrentAgent` WITHOUT attending it, and it drains the same buffer — so a real
1377
- // thief need not be counted in `attendance`. The old wording produced sentences that
1378
- // contradicted themselves ("another session attending alice — 1 sessions are attending").
1379
- // `attendance` stays as its own field, meaning what it says: how many sessions SELECTED
1380
- // this agent.
1381
- const takenDetail = {
1382
- count: missed.count,
1383
- last_sequence: missed.lastSequence,
1384
- connections: missed.connections,
1385
- // `count` is a floor once the per-session cap has discarded older takes.
1386
- ...(missed.truncated ? { truncated: true } : {}),
1387
- };
1388
- logger.warn("session.receive.taken_by_sibling", {
1389
- sessionId, agentName, connectionId, timeoutMs, attendance, liveness,
1390
- takenCount: missed.count, lastTakenSeq: missed.lastSequence, takenBy: missed.connections,
1391
- truncated: missed.truncated, correlationId: receiveCorrelationId,
1392
- });
1393
- const missedText = `Another session on this daemon already received ${missed.count} message(s) on this session that you have not read (up to sequence ${missed.lastSequence}). Nothing was lost: read what you missed with cello_transcript ${sessionId}.`;
1394
- // BOTH conditions can be true, and `counterparty_gone` WINS the `reason` field when they
1395
- // are (review MEDIUM). `reason` is the machine-readable discriminator, and a caller
1396
- // switching on it must still see the TERMINAL, actionable condition: telling an operator
1397
- // to "read what you missed, then reply" to a counterparty whose connection is dead sends
1398
- // them to the wrong subsystem and the reply goes nowhere. The theft is additive
1399
- // information, so it rides as a field and as the first half of the guidance — nothing is
1400
- // hidden, but the answer names the condition that changes what the operator should DO.
1401
- if (liveness === "gone") {
1402
- return {
1403
- ok: true,
1404
- content: null,
1405
- reason: "counterparty_gone",
1406
- liveness: "gone",
1407
- // Same reasoning as the standalone `counterparty_gone` exit below.
1408
- ...refusalsField(sessionNodeManager, agentName, sessionId, connectionId),
1409
- taken_by_sibling: takenDetail,
1410
- attendance,
1411
- // Same wording rule as the standalone `counterparty_gone` exit below: name what was
1412
- // observed (a dropped connection), never a crash, and do not lead with the seal.
1413
- guidance: `${missedText} Note the direct connection to the counterparty's session peer has ALSO dropped (liveness: gone) — that is all that was observed, not that they crashed. No more content will arrive on the direct path and a reply cannot reach them there. Read the history first, and check \`refusals\` here and in cello_inbox before blaming the network — this side refusing their messages produces exactly this state. Sealing with cello_close_session ENDS the conversation permanently, so do it only once you are satisfied nothing is outstanding; if they never co-close, a unilateral seal becomes available after the directory's delivery-grace window.`,
1414
- };
1415
- }
1416
- return {
1417
- ok: true,
1418
- content: null,
1419
- reason: "taken_by_sibling_session",
1420
- taken_by_sibling: takenDetail,
1421
- attendance,
1422
- guidance: `${missedText} Then reply — do not resend your last message.`,
1423
- };
1424
- }
1425
1235
  // M8B F16: a dead session must not return the SAME null timeout as a
1426
1236
  // quiet-but-healthy one. The liveness signal (session.liveness.changed → gone,
1427
1237
  // tracked per session by the node manager) finally reaches the MCP surface here.