@cello-protocol/daemon 0.0.52 → 0.0.54

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 (173) hide show
  1. package/dist/agent-handlers.d.ts +57 -0
  2. package/dist/agent-handlers.d.ts.map +1 -0
  3. package/dist/agent-handlers.js +360 -0
  4. package/dist/agent-handlers.js.map +1 -0
  5. package/dist/agent-id-migration.d.ts.map +1 -1
  6. package/dist/agent-id-migration.js +1 -0
  7. package/dist/agent-id-migration.js.map +1 -1
  8. package/dist/agent-loader.d.ts +3 -4
  9. package/dist/agent-loader.d.ts.map +1 -1
  10. package/dist/agent-loader.js +3 -4
  11. package/dist/agent-loader.js.map +1 -1
  12. package/dist/cbor-blob-migration.d.ts +8 -0
  13. package/dist/cbor-blob-migration.d.ts.map +1 -0
  14. package/dist/cbor-blob-migration.js +115 -0
  15. package/dist/cbor-blob-migration.js.map +1 -0
  16. package/dist/close-session-handler.d.ts +41 -0
  17. package/dist/close-session-handler.d.ts.map +1 -0
  18. package/dist/close-session-handler.js +399 -0
  19. package/dist/close-session-handler.js.map +1 -0
  20. package/dist/consortium-bootstrap.d.ts +83 -0
  21. package/dist/consortium-bootstrap.d.ts.map +1 -0
  22. package/dist/consortium-bootstrap.js +120 -0
  23. package/dist/consortium-bootstrap.js.map +1 -0
  24. package/dist/contact-handlers.d.ts +56 -0
  25. package/dist/contact-handlers.d.ts.map +1 -0
  26. package/dist/contact-handlers.js +303 -0
  27. package/dist/contact-handlers.js.map +1 -0
  28. package/dist/content-park-client.d.ts.map +1 -1
  29. package/dist/content-park-client.js +7 -7
  30. package/dist/content-park-client.js.map +1 -1
  31. package/dist/content-park.d.ts +15 -0
  32. package/dist/content-park.d.ts.map +1 -0
  33. package/dist/content-park.js +227 -0
  34. package/dist/content-park.js.map +1 -0
  35. package/dist/daemon.d.ts +0 -11
  36. package/dist/daemon.d.ts.map +1 -1
  37. package/dist/daemon.js +407 -4552
  38. package/dist/daemon.js.map +1 -1
  39. package/dist/db-identity-store.d.ts +31 -35
  40. package/dist/db-identity-store.d.ts.map +1 -1
  41. package/dist/db-identity-store.js +62 -66
  42. package/dist/db-identity-store.js.map +1 -1
  43. package/dist/directory-bootstrap.d.ts +27 -31
  44. package/dist/directory-bootstrap.d.ts.map +1 -1
  45. package/dist/directory-bootstrap.js +18 -21
  46. package/dist/directory-bootstrap.js.map +1 -1
  47. package/dist/frame-values.d.ts +3 -0
  48. package/dist/frame-values.d.ts.map +1 -0
  49. package/dist/frame-values.js +73 -0
  50. package/dist/frame-values.js.map +1 -0
  51. package/dist/identity-migration.d.ts +8 -8
  52. package/dist/identity-migration.d.ts.map +1 -1
  53. package/dist/identity-migration.js +30 -31
  54. package/dist/identity-migration.js.map +1 -1
  55. package/dist/inbound-seal-request.d.ts +17 -0
  56. package/dist/inbound-seal-request.d.ts.map +1 -0
  57. package/dist/inbound-seal-request.js +155 -0
  58. package/dist/inbound-seal-request.js.map +1 -0
  59. package/dist/inbound-sessions.d.ts +54 -0
  60. package/dist/inbound-sessions.d.ts.map +1 -0
  61. package/dist/inbound-sessions.js +603 -0
  62. package/dist/inbound-sessions.js.map +1 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +8 -10
  65. package/dist/index.js.map +1 -1
  66. package/dist/initiate-session-handler.d.ts +22 -0
  67. package/dist/initiate-session-handler.d.ts.map +1 -0
  68. package/dist/initiate-session-handler.js +150 -0
  69. package/dist/initiate-session-handler.js.map +1 -0
  70. package/dist/network-directory-node.d.ts +6 -6
  71. package/dist/network-directory-node.d.ts.map +1 -1
  72. package/dist/network-directory-node.js +29 -28
  73. package/dist/network-directory-node.js.map +1 -1
  74. package/dist/notification-handlers.d.ts +32 -0
  75. package/dist/notification-handlers.d.ts.map +1 -0
  76. package/dist/notification-handlers.js +73 -0
  77. package/dist/notification-handlers.js.map +1 -0
  78. package/dist/outbound-sessions.d.ts +54 -0
  79. package/dist/outbound-sessions.d.ts.map +1 -0
  80. package/dist/outbound-sessions.js +429 -0
  81. package/dist/outbound-sessions.js.map +1 -0
  82. package/dist/park-envelope.d.ts.map +1 -1
  83. package/dist/park-envelope.js +3 -2
  84. package/dist/park-envelope.js.map +1 -1
  85. package/dist/register-handler.d.ts +35 -0
  86. package/dist/register-handler.d.ts.map +1 -0
  87. package/dist/register-handler.js +179 -0
  88. package/dist/register-handler.js.map +1 -0
  89. package/dist/registration-manager.d.ts.map +1 -1
  90. package/dist/registration-manager.js +3 -6
  91. package/dist/registration-manager.js.map +1 -1
  92. package/dist/relay-receipt-store.d.ts.map +1 -1
  93. package/dist/relay-receipt-store.js +1 -2
  94. package/dist/relay-receipt-store.js.map +1 -1
  95. package/dist/retry-queue.d.ts +18 -60
  96. package/dist/retry-queue.d.ts.map +1 -1
  97. package/dist/retry-queue.js +46 -97
  98. package/dist/retry-queue.js.map +1 -1
  99. package/dist/seal-coordinator.d.ts +47 -0
  100. package/dist/seal-coordinator.d.ts.map +1 -0
  101. package/dist/seal-coordinator.js +551 -0
  102. package/dist/seal-coordinator.js.map +1 -0
  103. package/dist/seal-flows.d.ts +50 -0
  104. package/dist/seal-flows.d.ts.map +1 -0
  105. package/dist/seal-flows.js +454 -0
  106. package/dist/seal-flows.js.map +1 -0
  107. package/dist/seal-frontier-verify.d.ts +9 -9
  108. package/dist/seal-frontier-verify.d.ts.map +1 -1
  109. package/dist/seal-frontier-verify.js +11 -12
  110. package/dist/seal-frontier-verify.js.map +1 -1
  111. package/dist/seal-leaf.d.ts +58 -0
  112. package/dist/seal-leaf.d.ts.map +1 -0
  113. package/dist/seal-leaf.js +111 -0
  114. package/dist/seal-leaf.js.map +1 -0
  115. package/dist/seal-receipt-upgrade.d.ts +1 -1
  116. package/dist/seal-receipt-upgrade.js +1 -1
  117. package/dist/seal-upgrade.d.ts +10 -10
  118. package/dist/seal-upgrade.d.ts.map +1 -1
  119. package/dist/seal-upgrade.js +23 -24
  120. package/dist/seal-upgrade.js.map +1 -1
  121. package/dist/session-assignment-parser.d.ts +16 -19
  122. package/dist/session-assignment-parser.d.ts.map +1 -1
  123. package/dist/session-assignment-parser.js +30 -29
  124. package/dist/session-assignment-parser.js.map +1 -1
  125. package/dist/session-ceremony.d.ts +3 -6
  126. package/dist/session-ceremony.d.ts.map +1 -1
  127. package/dist/session-ceremony.js +7 -9
  128. package/dist/session-ceremony.js.map +1 -1
  129. package/dist/session-content-handlers.d.ts +25 -0
  130. package/dist/session-content-handlers.d.ts.map +1 -0
  131. package/dist/session-content-handlers.js +436 -0
  132. package/dist/session-content-handlers.js.map +1 -0
  133. package/dist/session-name.d.ts +35 -0
  134. package/dist/session-name.d.ts.map +1 -0
  135. package/dist/session-name.js +60 -0
  136. package/dist/session-name.js.map +1 -0
  137. package/dist/session-node-manager.d.ts +26 -62
  138. package/dist/session-node-manager.d.ts.map +1 -1
  139. package/dist/session-node-manager.js +115 -92
  140. package/dist/session-node-manager.js.map +1 -1
  141. package/dist/session-read-handlers.d.ts +38 -0
  142. package/dist/session-read-handlers.d.ts.map +1 -0
  143. package/dist/session-read-handlers.js +233 -0
  144. package/dist/session-read-handlers.js.map +1 -0
  145. package/dist/session-relay-client.d.ts +8 -8
  146. package/dist/session-relay-client.d.ts.map +1 -1
  147. package/dist/session-relay-client.js +44 -46
  148. package/dist/session-relay-client.js.map +1 -1
  149. package/dist/session-tree.d.ts +11 -23
  150. package/dist/session-tree.d.ts.map +1 -1
  151. package/dist/session-tree.js +14 -26
  152. package/dist/session-tree.js.map +1 -1
  153. package/dist/signaling-connect.d.ts +2 -5
  154. package/dist/signaling-connect.d.ts.map +1 -1
  155. package/dist/signaling-connect.js +7 -12
  156. package/dist/signaling-connect.js.map +1 -1
  157. package/dist/sqlcipher-db.d.ts +18 -19
  158. package/dist/sqlcipher-db.d.ts.map +1 -1
  159. package/dist/sqlcipher-db.js +30 -30
  160. package/dist/sqlcipher-db.js.map +1 -1
  161. package/dist/telegram-doorbell.d.ts +38 -0
  162. package/dist/telegram-doorbell.d.ts.map +1 -0
  163. package/dist/telegram-doorbell.js +139 -0
  164. package/dist/telegram-doorbell.js.map +1 -0
  165. package/dist/types.d.ts +86 -79
  166. package/dist/types.d.ts.map +1 -1
  167. package/dist/types.js +0 -1
  168. package/dist/types.js.map +1 -1
  169. package/dist/vocabulary.d.ts +30 -30
  170. package/dist/vocabulary.d.ts.map +1 -1
  171. package/dist/vocabulary.js +41 -49
  172. package/dist/vocabulary.js.map +1 -1
  173. package/package.json +5 -5
@@ -0,0 +1,436 @@
1
+ /**
2
+ * cello_send and cello_receive — the content path.
3
+ *
4
+ * The two biggest handlers in the daemon, and the two I had put on a DO-NOT-CUT list. That list was
5
+ * written before anything had been measured, and it was wrong: these close over eleven things, not
6
+ * the whole daemon. They are listed below, which is the point of the move.
7
+ *
8
+ * They stay TOGETHER because they are two halves of one state machine — the read cursor. cello_send
9
+ * appends to the daemon-owned tree; cello_receive advances the per-connection cursor and the durable
10
+ * watermark. Splitting them would put the cursor's writer and its reader in different modules.
11
+ */
12
+ import { randomUUID, createHash } from "node:crypto";
13
+ import { MAX_CONTENT_BYTES } from "@cello-protocol/protocol-types";
14
+ import { TIER } from "./contacts-tier-migration.js";
15
+ import { GATEWAY_UNAVAILABLE, GOVERNANCE_TIMEOUT } from "@cello-protocol/gateway";
16
+ export function registerSessionContentHandlers(deps) {
17
+ const { handlers, logger, sessionNodeManager, securityGateway, retryQueue, getConnState, resolveCurrentAgent, NO_CURRENT_AGENT_RESPONSE, getConnectionCursor, advanceConnectionCursor, safeCursorAdvance, clearTelegramRung, } = deps;
18
+ // ─── CELLO-M7-DAEMON-004: cello_send (live send + daemon-owned tree append) ──
19
+ handlers.set("cello_send", async (params, connectionId) => {
20
+ const connState = getConnState(connectionId);
21
+ // CC-3 / M8C-AUTOSTART-1 F18: explicit { agent } > current > sole online agent.
22
+ const agentName = resolveCurrentAgent(connState, params?.agent);
23
+ if (!agentName)
24
+ return NO_CURRENT_AGENT_RESPONSE;
25
+ // round-2 BLOCKING: read the snake_case public field cello-mcp.ts actually sends.
26
+ const sessionId = params?.session_id;
27
+ const contentStr = typeof params?.content === "string" ? params.content : undefined;
28
+ if (!sessionId || contentStr === undefined) {
29
+ return { ok: false, reason: "missing_params", guidance: "Provide 'session_id' (hex) and 'content' (string) parameters." };
30
+ }
31
+ // DOD-LOOP-1: the (agent, session_id) lookup is itself the ownership scope.
32
+ const record = sessionNodeManager.getSessionRecord(agentName, sessionId);
33
+ if (!record) {
34
+ return { ok: false, reason: "session_not_found", guidance: "No session found with this ID. Check cello_sessions for active sessions." };
35
+ }
36
+ if (record.agent_name !== agentName) {
37
+ return { ok: false, reason: "session_not_owned", guidance: "This session belongs to a different agent. Call cello_use_agent to switch to the agent that owns it, then retry." };
38
+ }
39
+ if (record.status !== "active") {
40
+ return { ok: false, reason: "session_not_active", guidance: `Session is '${record.status}', not active. Content can only be sent on an active session. If it is interrupted, call cello_close_session to seal it.` };
41
+ }
42
+ // M8C-CURSOR-1: read-before-write gate. current_seq is the tree's highest leaf index
43
+ // (message_count is kept in sync with leafCount on every append, both directions — DAEMON-004
44
+ // finding #2), so it reflects EVERY message in the session regardless of which connection sent
45
+ // or received it. If this connection hasn't read up to current_seq (e.g. a second attended
46
+ // session on the same agent that hasn't polled since the other connection's last send), refuse
47
+ // rather than let it send blind — the WhatsApp-group-chat model. Runs BEFORE the
48
+ // governance-decisions parsing below: an access-control gate must short-circuit before any
49
+ // unrelated prep work for a send that may not be allowed to proceed at all.
50
+ //
51
+ // The gate consults TWO authorities and passes if EITHER says the caller is caught up:
52
+ //
53
+ // 1. the connection cursor (in-memory, per-connection) — satisfied by a long-lived client
54
+ // like the MCP shim, which holds one socket for the whole session.
55
+ //
56
+ // 2. the persisted per-(agent, session) read watermark. A STATELESS client CANNOT satisfy
57
+ // authority 1: the `cello` CLI runs a fresh process, and therefore a fresh connection, per
58
+ // command, so it always presents cursor -1. Without the watermark, every CLI send is
59
+ // refused forever once the counterparty has spoken — as is every send from a RECONNECTING
60
+ // MCP client, whose new connectionId also resets the cursor to -1. Do not remove it.
61
+ //
62
+ // What the gate protects, precisely: an agent must never reply to COUNTERPARTY content nobody
63
+ // on its side has seen. That guarantee is durable — it survives the socket.
64
+ //
65
+ // What it deliberately does NOT protect: a message this agent SENT from a different local
66
+ // connection does not block. Two attended windows on one agent do not gate each other, because
67
+ // locally-authored content is not "unread counterparty content" — the AGENT wrote it. The
68
+ // principal is the agent, not the socket, and the daemon cannot referee which of an operator's
69
+ // own windows a human is looking at. This is deliberate; do not "fix" it.
70
+ const currentSeq = record.message_count - 1;
71
+ const connectionCursor = getConnectionCursor(connectionId, sessionId);
72
+ const unreadReceived = sessionNodeManager.getUnreadReceivedCount(agentName, sessionId);
73
+ const caughtUp = connectionCursor >= currentSeq || unreadReceived === 0;
74
+ if (!caughtUp) {
75
+ // M8C-CURSOR-1 (reviewer MEDIUM fix): every sibling rejection in this handler logs; this
76
+ // gate must too — a security-relevant control-flow path with no observability is a gap.
77
+ // Both authorities are logged, so the next reader can tell WHICH one refused.
78
+ logger.warn("session.send.blocked", {
79
+ sessionId,
80
+ currentSeq,
81
+ lastReadSeq: connectionCursor,
82
+ unreadReceived,
83
+ connectionId,
84
+ agentName,
85
+ });
86
+ return {
87
+ ok: false,
88
+ reason: "session_not_current",
89
+ current_seq: currentSeq,
90
+ last_read_seq: connectionCursor,
91
+ unread_received: unreadReceived,
92
+ // DOD-ONBOARD-HELP-1 §5: say what is wrong and what to do, in that order, in plain words.
93
+ // The old text opened with the internal noun ("this agent hasn't read…"); a refused send is
94
+ // the most common wall a new operator hits, so it leads with the COUNT and the FIX. The
95
+ // session id is interpolated so the remedy is copy-pasteable, not a template to fill in.
96
+ // Rendered per surface at the IPC boundary — a CLI caller sees `cello receive <id>`.
97
+ guidance: `${unreadReceived} unread message(s) — run cello_receive ${sessionId} first to read them, then send again. (Or cello_transcript ${sessionId} for the whole conversation.) You are blocked from replying to something you haven't read.`,
98
+ };
99
+ }
100
+ // M9-FEED-001 §6: the agent's governance re-send decisions, keyed by the flagId a prior `warn`
101
+ // returned. Optional; validated shape only (the gateway re-scans + applies them, INV-4). A
102
+ // malformed map is ignored rather than failing the send (the gateway will just re-warn).
103
+ const rawDecisions = params?.governance_decisions;
104
+ let governanceDecisions;
105
+ if (rawDecisions && typeof rawDecisions === "object" && !Array.isArray(rawDecisions)) {
106
+ const valid = {};
107
+ for (const [k, v] of Object.entries(rawDecisions)) {
108
+ if (v === "redact" || v === "allow_once" || v === "allow_always")
109
+ valid[k] = v;
110
+ }
111
+ if (Object.keys(valid).length > 0)
112
+ governanceDecisions = valid;
113
+ }
114
+ const correlationId = randomUUID();
115
+ const contentBytes = new TextEncoder().encode(contentStr);
116
+ // CELLO-M7-MSG-001 (AC-013/AC-018/AC-021): enforce the 1 MB application content cap
117
+ // BEFORE any transmission or hash/leaf production. This replaces the silent oversize
118
+ // decode-failure → desync: the send is rejected with a distinct, diagnosable reason
119
+ // and actionable guidance; no content frame is transmitted, no leaf is appended, and
120
+ // the session stays usable.
121
+ if (contentBytes.length > MAX_CONTENT_BYTES) {
122
+ logger.warn("content.rejected.too_large", {
123
+ sessionId,
124
+ contentSize: contentBytes.length,
125
+ cap: MAX_CONTENT_BYTES,
126
+ correlationId,
127
+ });
128
+ return {
129
+ ok: false,
130
+ reason: "content_too_large",
131
+ guidance: `This message is ${contentBytes.length} bytes, over the ${MAX_CONTENT_BYTES}-byte (1 MB) per-message content cap. Split it into multiple messages each under the cap, or use the large-object/file transfer path for large payloads (not cello_send). Nothing was sent and the session is still active — retry with smaller content.`,
132
+ };
133
+ }
134
+ const recipientPubkey = record.counterparty_pubkey;
135
+ // M9 outbound screening seam (INV-5/SI-001). Screen BEFORE anything reaches the wire. The
136
+ // gateway verdict drives the four cello_send outcomes (M9-FEED-001): block / warn → NOT sent;
137
+ // allow → sent as-is; redact → sent in ALTERED form. A configured-but-unreachable gateway fails
138
+ // closed (block, gateway_unavailable), so a screening outage can never let content out ungated.
139
+ const outboundVerdict = await securityGateway.screenOutbound(contentBytes, {
140
+ direction: "outbound",
141
+ agentName: record.agent_name,
142
+ sessionId,
143
+ correlationId,
144
+ ...(governanceDecisions !== undefined ? { governanceDecisions } : {}),
145
+ });
146
+ if (outboundVerdict.disposition === "block") {
147
+ if (outboundVerdict.reason === GOVERNANCE_TIMEOUT) {
148
+ logger.error("security.gateway.timeout", { sessionId, reason: outboundVerdict.reason, correlationId });
149
+ }
150
+ else if (outboundVerdict.reason === GATEWAY_UNAVAILABLE) {
151
+ logger.error("security.gateway.unavailable", { direction: "outbound", reason: outboundVerdict.reason, correlationId });
152
+ }
153
+ else {
154
+ logger.info("security.verdict.returned", { disposition: "block", sessionId, reason: outboundVerdict.reason, correlationId });
155
+ }
156
+ return {
157
+ ok: false,
158
+ reason: outboundVerdict.reason ?? "blocked_by_governance",
159
+ guidance: outboundVerdict.guidance ??
160
+ "This message was blocked by the security gateway and was NOT sent. The session is still active.",
161
+ blocks: (outboundVerdict.events ?? []).filter((e) => e.disposition === "block"),
162
+ };
163
+ }
164
+ if (outboundVerdict.disposition === "warn") {
165
+ logger.info("security.verdict.returned", { disposition: "warn", sessionId, correlationId });
166
+ return {
167
+ ok: false,
168
+ reason: "governance_warn",
169
+ guidance: outboundVerdict.guidance ??
170
+ "This message was held for a governance decision and was NOT sent. Re-send the same content with a " +
171
+ "governance_decisions map ({flagId: redact | allow_once | allow_always}) to resolve each flagged item.",
172
+ flags: (outboundVerdict.events ?? []).filter((e) => e.disposition === "warn"),
173
+ };
174
+ }
175
+ // FAIL-CLOSED (code-review MED): a `redact` verdict MUST carry the redacted content. If it ever
176
+ // arrives without it, sending the original `contentBytes` would leak the pre-redaction draft — the
177
+ // one place M9 could fail OPEN. Treat it as a block, never an allow-original. (Unreachable today:
178
+ // the gateway always includes content on redact; this is the defensive floor.)
179
+ if (outboundVerdict.disposition === "redact" && outboundVerdict.content === undefined) {
180
+ logger.error("security.verdict.redact_without_content", { sessionId, correlationId });
181
+ return {
182
+ ok: false,
183
+ reason: "redact_without_content",
184
+ guidance: "The security gateway returned a redact verdict without the redacted content. To avoid " +
185
+ "leaking the original, nothing was sent. This is a gateway fault — check the gateway logs and retry.",
186
+ };
187
+ }
188
+ // allow or redact → send. On redact the ALTERED bytes are what go on the wire AND what the leaf
189
+ // hash binds — the transcript records what was actually sent, not the pre-redaction draft.
190
+ const modified = outboundVerdict.disposition === "redact" && outboundVerdict.content !== undefined;
191
+ const sendBytes = modified ? new Uint8Array(outboundVerdict.content) : contentBytes;
192
+ const contentHash = createHash("sha256").update(new Uint8Array([0x00])).update(sendBytes).digest();
193
+ const contentHashHex = Buffer.from(contentHash).toString("hex");
194
+ const sendResult = await sessionNodeManager.sendContent(record.agent_name, sessionId, sendBytes, new Uint8Array(contentHash), correlationId);
195
+ if (!sendResult.ok) {
196
+ // DB-001 / dead-channel contract: never silently drop, never desync. Preserve
197
+ // the content in the durable retry_queue so it is retried on reconnect, and
198
+ // surface a named, diagnosable failure.
199
+ const nonce = randomUUID();
200
+ try {
201
+ retryQueue.enqueue(sessionId, new TextEncoder().encode(nonce), sendBytes);
202
+ }
203
+ catch (err) {
204
+ logger.error("session.content.queue.failed", {
205
+ sessionId,
206
+ error: err instanceof Error ? err.message : String(err),
207
+ correlationId,
208
+ });
209
+ }
210
+ logger.warn("session.content.send.failed", {
211
+ sessionId,
212
+ recipientPubkey,
213
+ reason: sendResult.reason,
214
+ errorMessage: sendResult.error,
215
+ correlationId,
216
+ });
217
+ return {
218
+ ok: false,
219
+ reason: sendResult.reason,
220
+ guidance: "The content could not be delivered over the session stream right now. It has been queued in the durable retry queue and will be retried when the counterparty reconnects. The session remains usable — check cello_status for the counterparty's status.",
221
+ };
222
+ }
223
+ // Delivered directly OR dispatched to relay (DOD-LEAVEMSG-1) — either way the content is now
224
+ // part of the daemon-owned tree: the relay witness (R1) already assigned it a sequence before
225
+ // direct delivery was even attempted, so a parked message occupies the SAME leaf position it
226
+ // would have taken if delivered live. Append once, for both outcomes.
227
+ const { leafIndex, newRootHex } = sessionNodeManager.appendSessionLeaf(record.agent_name, sessionId, "msg", contentHashHex, correlationId);
228
+ // DOD-LOG-1: persist the readable SENT plaintext to the durable transcript, keyed by the
229
+ // canonical leaf sequence so it joins the committed hash chain (survives restart).
230
+ // M9 merge fix: use sendBytes (the ALTERED bytes on a redact verdict), never the pre-redaction
231
+ // contentBytes — the leaf hash above already binds sendBytes; the transcript must match what
232
+ // actually went on the wire, not the pre-redaction draft (M9's own stated seam invariant).
233
+ sessionNodeManager.recordTranscriptMessage(record.agent_name, sessionId, leafIndex, "sent", sendBytes, correlationId);
234
+ // M8C-CURSOR-1: the sender authored this leaf — advance ITS OWN cursor so it doesn't get
235
+ // blocked by session_not_current on its own just-sent message.
236
+ advanceConnectionCursor(connectionId, sessionId, leafIndex);
237
+ void newRootHex;
238
+ // CC-1 (2026-07-07): operator engagement promotes the counterparty to a known contact. A
239
+ // committed reply — past the read-before-write gate, content now on the wire AND in the tree —
240
+ // IS the operator choosing to trust this sender; the inbound-accept path deliberately no longer
241
+ // auto-adds (that defeated screening + anti-spam). For an OUTBOUND session the counterparty is
242
+ // already a contact (cello_initiate_session added it), so this is an idempotent no-op there; it
243
+ // matters for inbound-originated sessions, where the reply is the trust signal. addContact is
244
+ // INSERT OR IGNORE — it never refreshes added_at. DOD-TIER-4 AC3: engaging (a committed reply into
245
+ // an inbound session I accepted) makes the counterparty KNOWN, provenance 'accepted'. For an
246
+ // OUTBOUND session the row already exists ('initiated', KNOWN) and INSERT OR IGNORE leaves it
247
+ // untouched — 'initiated' correctly wins there.
248
+ sessionNodeManager.addContact(record.agent_name, recipientPubkey, undefined, "accepted", TIER.KNOWN);
249
+ if (modified) {
250
+ logger.info("security.verdict.returned", { disposition: "redact", sessionId, sequenceNumber: leafIndex, correlationId });
251
+ }
252
+ if (!sendResult.delivered) {
253
+ // DOD-LEAVEMSG-1 (sender half): direct delivery failed but the sealed, hashed content was
254
+ // successfully deposited at the relay (pickup_queue) — this is a SUCCESS outcome, not a
255
+ // failure. The recipient's daemon pulls it via RELAYWAKE on next reconnect. Reporting this
256
+ // as ok:false (the pre-LEAVEMSG-1 behavior) misrepresented an in-flight message as lost.
257
+ logger.info("session.content.dispatched_to_relay", {
258
+ sessionId,
259
+ recipientPubkey,
260
+ contentHashHex,
261
+ sequenceNumber: leafIndex,
262
+ correlationId,
263
+ });
264
+ return {
265
+ ok: true,
266
+ sequence_number: leafIndex,
267
+ delivered: false,
268
+ reason: "dispatched_to_relay",
269
+ modified,
270
+ guidance: "The counterparty is not directly reachable right now, so this message was sealed and dispatched to relay store-and-forward. It will be delivered the next time the counterparty's daemon reconnects — no further action is needed.",
271
+ ...(modified ? { transformations: (outboundVerdict.events ?? []).filter((e) => e.disposition === "redact") } : {}),
272
+ };
273
+ }
274
+ logger.info("session.content.sent", {
275
+ sessionId,
276
+ recipientPubkey,
277
+ contentHashHex,
278
+ sequenceNumber: leafIndex,
279
+ correlationId,
280
+ });
281
+ return {
282
+ ok: true,
283
+ sequence_number: leafIndex,
284
+ delivered: true,
285
+ modified,
286
+ // On a redact, tell the agent exactly what was transformed (the §6 sender-side surface).
287
+ ...(modified ? { transformations: (outboundVerdict.events ?? []).filter((e) => e.disposition === "redact") } : {}),
288
+ };
289
+ });
290
+ // ─── CELLO-M7-DAEMON-004 / F1-a: cello_receive (BLOCKING, session-scoped) ────────
291
+ // F1-a fix: the daemon port had dropped the blocking receive (the handler was a
292
+ // non-blocking buf.shift). It now BLOCKS up to timeout_ms, polling the received-content
293
+ // buffer — resolved by the next arrival, a terminal seal answer (F1-b), or timeout. This is
294
+ // the "blocking receive variant" the guidance names.
295
+ //
296
+ // ONE name (DOD-ONBOARD-HELP-1, Andre 2026-07-11). This was briefly registered under
297
+ // `cello_receive_session` as well — one implementation behind two names. That alias is DELETED:
298
+ // it accepted or joined nothing (inbound sessions are auto-accepted by the standing receiver),
299
+ // so it was a second name for a step that does not exist, and its help said otherwise.
300
+ const RECEIVE_DEFAULT_TIMEOUT_MS = 30000; // matches the cello-mcp shim's documented default
301
+ const handleReceive = async (params, connectionId) => {
302
+ const connState = getConnState(connectionId);
303
+ // CC-3 / M8C-AUTOSTART-1 F18: explicit { agent } > current > sole online agent.
304
+ const agentName = resolveCurrentAgent(connState, params?.agent);
305
+ if (!agentName)
306
+ return NO_CURRENT_AGENT_RESPONSE;
307
+ // Read the snake_case public field cello-mcp.ts actually sends.
308
+ const sessionId = params?.session_id;
309
+ if (!sessionId) {
310
+ return { ok: false, reason: "missing_params", guidance: "Provide 'session_id' (hex) to receive content for a specific session." };
311
+ }
312
+ const record = sessionNodeManager.getSessionRecord(agentName, sessionId);
313
+ // DOD-UNREAD-1 D4b: a TRANSCRIPT-ONLY session — received rows exist but no sessions row (the
314
+ // pre-D3/D4a phantom residue: the counterparty's reply landed while this side refused the
315
+ // session). Those rows are counted unread by getUnreadSummary, so cello_receive MUST be able
316
+ // to read them or the badge can never clear. Reading is a transcript operation: the since_seq
317
+ // catch-up below works from the durable transcript alone. Only a session with NEITHER a row
318
+ // NOR transcript rows is truly not found.
319
+ let transcriptOnly = false;
320
+ if (!record) {
321
+ if (sessionNodeManager.readTranscript(agentName, sessionId).messages.length === 0) {
322
+ return { ok: false, reason: "session_not_found", guidance: "No session found with this ID. Check cello_sessions." };
323
+ }
324
+ transcriptOnly = true;
325
+ }
326
+ // (D4 review F3: the old `record.agent_name !== agentName` → session_not_owned branch was dead
327
+ // code — getSessionRecord is keyed by (agent_name, session_id), so a record can never belong to
328
+ // a different agent. A cross-agent session id simply has no record under this agent.)
329
+ // M8C-SINCESEQ-1: stateless catch-up. When since_seq is provided, return a BATCH of received
330
+ // transcript messages with sequence > since_seq (durable transcript, not the ephemeral buffer —
331
+ // so concurrent arrivals don't shift what a given since_seq returns; no replay race). Replaces
332
+ // the cello_get_transcript workaround for away-then-return. Received-direction only (the messages
333
+ // you'd have gotten live). Advances the read watermark (delivery marks read — clears INBOX
334
+ // unread). A distinct early branch: the plain (no since_seq) receive is entirely unchanged.
335
+ const rawSince = params?.since_seq;
336
+ if (typeof rawSince === "number" && Number.isFinite(rawSince)) {
337
+ const sinceSeq = rawSince;
338
+ // D4b AC2: a transcript-only session has no record to attribute from — `from` is null,
339
+ // NEVER the string "unknown" (the transcript stores no counterparty; don't invent one).
340
+ const from = record ? record.counterparty_pubkey : null;
341
+ const { messages } = sessionNodeManager.readTranscript(agentName, sessionId);
342
+ const received = messages.filter((m) => m.direction === "received" && m.sequence > sinceSeq);
343
+ if (received.length > 0) {
344
+ // readTranscript is ordered by sequence ASC → the last is the max.
345
+ const maxSeq = received[received.length - 1].sequence;
346
+ sessionNodeManager.advanceLastDeliveredSeq(agentName, sessionId, maxSeq);
347
+ clearTelegramRung(agentName, sessionId); // M8C-TGDOOR-1: read clears the ring
348
+ }
349
+ // M8C-CURSOR-1 (reviewer HIGH fix): only advance through the CONTIGUOUS run this batch
350
+ // actually delivered — if a sent leaf from another local connection sits in a gap, this
351
+ // correctly refuses to advance past it (cello_get_transcript is still required to catch up).
352
+ safeCursorAdvance(connectionId, sessionId, new Set(received.map((m) => m.sequence)));
353
+ logger.info("session.receive.since_seq", { sessionId, agentName, since_seq: sinceSeq, count: received.length });
354
+ return {
355
+ ok: true,
356
+ since_seq: sinceSeq,
357
+ count: received.length,
358
+ messages: received.map((m) => ({ sequence: m.sequence, content: m.text, from })),
359
+ };
360
+ }
361
+ // D4b AC3: the plain (blocking) receive waits on a LIVE session's buffer — a transcript-only
362
+ // session has no live node and nothing will ever arrive. Waiting to a null timeout would be
363
+ // misleading and session_not_found would be a lie (the transcript exists). A distinct reason
364
+ // points the caller at the read that works.
365
+ if (transcriptOnly) {
366
+ return {
367
+ ok: false,
368
+ reason: "session_not_live",
369
+ 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.",
370
+ };
371
+ }
372
+ const rawTimeout = params?.timeout_ms;
373
+ const timeoutMs = typeof rawTimeout === "number" && Number.isFinite(rawTimeout) && rawTimeout >= 0
374
+ ? rawTimeout
375
+ : RECEIVE_DEFAULT_TIMEOUT_MS;
376
+ const deadline = Date.now() + timeoutMs;
377
+ for (;;) {
378
+ // 1) Deliverable content wins — drain one buffered message per call (FIFO).
379
+ const entry = sessionNodeManager.takeReceivedContent(agentName, sessionId);
380
+ if (entry) {
381
+ // M8C-INBOX-1 (N3): delivery marks read — advance the persisted read watermark so this
382
+ // message no longer counts as unread in cello_check_notifications. Monotonic (never lowers).
383
+ sessionNodeManager.advanceLastDeliveredSeq(agentName, sessionId, entry.sequenceNumber);
384
+ clearTelegramRung(agentName, sessionId); // M8C-TGDOOR-1: read clears the ring
385
+ // M8C-CURSOR-1 (reviewer HIGH fix): a single delivered message only proves THIS sequence
386
+ // was read — safeCursorAdvance refuses to vault past a gap (e.g. an unread sent leaf from
387
+ // another local connection) even though this specific sequence number is now known.
388
+ safeCursorAdvance(connectionId, sessionId, new Set([entry.sequenceNumber]));
389
+ return {
390
+ ok: true,
391
+ content: Buffer.from(entry.contentHex, "hex").toString("utf8"),
392
+ sessionId,
393
+ sequence_number: entry.sequenceNumber,
394
+ senderPubkey: entry.senderPubkey,
395
+ };
396
+ }
397
+ // 2) F1-b: the session sealed while we were (or before we started) waiting — return the
398
+ // terminal answer instead of hanging to timeout. unread_count reports messages that
399
+ // were evicted unread (still durable — recoverable via cello_get_transcript).
400
+ const terminal = sessionNodeManager.peekTerminalMarker(agentName, sessionId);
401
+ if (terminal) {
402
+ const sealedRoot = sessionNodeManager.getSealedRootHex(agentName, sessionId);
403
+ return {
404
+ ok: true,
405
+ type: "session_sealed",
406
+ session_id: sessionId,
407
+ ...(sealedRoot ? { sealed_root: sealedRoot } : {}),
408
+ unread_count: terminal.unreadCount,
409
+ guidance: terminal.unreadCount > 0
410
+ ? `The session has been sealed by both parties. ${terminal.unreadCount} message(s) arrived that were not read live — call cello_transcript to retrieve the full sealed history. No further actions are required on this session.`
411
+ : "The session has been sealed by both parties. The full history is available via cello_transcript. No further actions are required on this session.",
412
+ };
413
+ }
414
+ // 3) Out of time — non-blocking-equivalent empty answer.
415
+ const remaining = deadline - Date.now();
416
+ if (remaining <= 0) {
417
+ // M8B F16: a dead session must not return the SAME null timeout as a
418
+ // quiet-but-healthy one. The liveness signal (session.liveness.changed → gone,
419
+ // tracked per session by the node manager) finally reaches the MCP surface here.
420
+ if (sessionNodeManager.getSessionLiveness(agentName, sessionId) === "gone") {
421
+ return {
422
+ ok: true,
423
+ content: null,
424
+ reason: "counterparty_gone",
425
+ liveness: "gone",
426
+ 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.",
427
+ };
428
+ }
429
+ return { ok: true, content: null, guidance: "No content arrived within timeout_ms. Call cello_receive again to keep waiting, or read cello_transcript for the full session history." };
430
+ }
431
+ await new Promise((r) => setTimeout(r, Math.min(20, remaining)));
432
+ }
433
+ };
434
+ handlers.set("cello_receive", handleReceive);
435
+ }
436
+ //# sourceMappingURL=session-content-handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-content-handlers.js","sourceRoot":"","sources":["../src/session-content-handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * DOD-SESSION-NAME-1 — validation for a session's human-readable name.
3
+ *
4
+ * This lives in the DAEMON, not in protocol-types, and that is deliberate: a session name is local
5
+ * and cosmetic. protocol-types holds the wire encoders and the TBS builders — putting a validator
6
+ * there would say the name is protocol, and the next reader would reasonably wire it into a frame.
7
+ * It is a sticky note on your own copy of the folder.
8
+ *
9
+ * It is NOT handle-shaped. Do not reach for MONIKER_RE: a moniker is a handle a counterparty sees,
10
+ * so it is deliberately narrow; this is a DESCRIPTION ("Q3 budget review with Bob") that only its
11
+ * author reads. Letters, digits, spaces, punctuation, accents, CJK and emoji are all legal.
12
+ */
13
+ /** Free text, but bounded: long enough for a sentence, short enough to list in a terminal. */
14
+ export declare const SESSION_NAME_MAX_LENGTH = 200;
15
+ export type SessionNameResult = {
16
+ ok: true;
17
+ value: string | null;
18
+ } | {
19
+ ok: false;
20
+ reason: "session_name_not_a_string" | "session_name_control_chars" | "session_name_too_long";
21
+ guidance: string;
22
+ };
23
+ /**
24
+ * Validate a session name.
25
+ *
26
+ * Returns the trimmed name, or `null` for "no name" — which is a legal, meaningful value, not a
27
+ * failure. `null`, `undefined` and an all-whitespace string all mean CLEAR.
28
+ *
29
+ * Rejects rather than repairs. A 201-character name is refused, not truncated, and a name carrying
30
+ * a newline is refused, not stripped: silently altering what the operator typed and then storing it
31
+ * under their name is worse than telling them it was wrong. Each refusal names its own cause, so
32
+ * "why was my name rejected" never requires reading the daemon log.
33
+ */
34
+ export declare function validateSessionName(raw: unknown): SessionNameResult;
35
+ //# sourceMappingURL=session-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-name.d.ts","sourceRoot":"","sources":["../src/session-name.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,8FAA8F;AAC9F,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAK3C,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,GAAG,4BAA4B,GAAG,uBAAuB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAElI;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,CAkCnE"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * DOD-SESSION-NAME-1 — validation for a session's human-readable name.
3
+ *
4
+ * This lives in the DAEMON, not in protocol-types, and that is deliberate: a session name is local
5
+ * and cosmetic. protocol-types holds the wire encoders and the TBS builders — putting a validator
6
+ * there would say the name is protocol, and the next reader would reasonably wire it into a frame.
7
+ * It is a sticky note on your own copy of the folder.
8
+ *
9
+ * It is NOT handle-shaped. Do not reach for MONIKER_RE: a moniker is a handle a counterparty sees,
10
+ * so it is deliberately narrow; this is a DESCRIPTION ("Q3 budget review with Bob") that only its
11
+ * author reads. Letters, digits, spaces, punctuation, accents, CJK and emoji are all legal.
12
+ */
13
+ /** Free text, but bounded: long enough for a sentence, short enough to list in a terminal. */
14
+ export const SESSION_NAME_MAX_LENGTH = 200;
15
+ /** C0 (including \n, \r, \t, \0), DEL, and C1. Rejected, never stripped. */
16
+ const CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/;
17
+ /**
18
+ * Validate a session name.
19
+ *
20
+ * Returns the trimmed name, or `null` for "no name" — which is a legal, meaningful value, not a
21
+ * failure. `null`, `undefined` and an all-whitespace string all mean CLEAR.
22
+ *
23
+ * Rejects rather than repairs. A 201-character name is refused, not truncated, and a name carrying
24
+ * a newline is refused, not stripped: silently altering what the operator typed and then storing it
25
+ * under their name is worse than telling them it was wrong. Each refusal names its own cause, so
26
+ * "why was my name rejected" never requires reading the daemon log.
27
+ */
28
+ export function validateSessionName(raw) {
29
+ if (raw === null || raw === undefined)
30
+ return { ok: true, value: null };
31
+ if (typeof raw !== "string") {
32
+ return {
33
+ ok: false,
34
+ reason: "session_name_not_a_string",
35
+ guidance: "A session name must be a string (or null to clear it).",
36
+ };
37
+ }
38
+ // Control characters are checked BEFORE trimming: trim() would silently eat a leading \n or \t
39
+ // and let a name through that the operator never meant to type — and a name that renders across
40
+ // two lines corrupts every listing it appears in.
41
+ if (CONTROL_CHARS.test(raw)) {
42
+ return {
43
+ ok: false,
44
+ reason: "session_name_control_chars",
45
+ guidance: "A session name cannot contain control characters (newlines, tabs, NUL). It is a single-line label — spaces, punctuation, accents and emoji are all fine.",
46
+ };
47
+ }
48
+ const trimmed = raw.trim();
49
+ if (trimmed.length === 0)
50
+ return { ok: true, value: null };
51
+ if (trimmed.length > SESSION_NAME_MAX_LENGTH) {
52
+ return {
53
+ ok: false,
54
+ reason: "session_name_too_long",
55
+ guidance: `A session name is at most ${SESSION_NAME_MAX_LENGTH} characters (this one is ${trimmed.length}). It is refused rather than truncated — a half-name is a name you did not choose.`,
56
+ };
57
+ }
58
+ return { ok: true, value: trimmed };
59
+ }
60
+ //# sourceMappingURL=session-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-name.js","sourceRoot":"","sources":["../src/session-name.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,8FAA8F;AAC9F,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAE3C,4EAA4E;AAC5E,MAAM,aAAa,GAAG,8BAA8B,CAAC;AAMrD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAExE,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,2BAA2B;YACnC,QAAQ,EAAE,wDAAwD;SACnE,CAAC;IACJ,CAAC;IAED,+FAA+F;IAC/F,gGAAgG;IAChG,kDAAkD;IAClD,IAAI,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,4BAA4B;YACpC,QAAQ,EAAE,0JAA0J;SACrK,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE3D,IAAI,OAAO,CAAC,MAAM,GAAG,uBAAuB,EAAE,CAAC;QAC7C,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,uBAAuB;YAC/B,QAAQ,EAAE,6BAA6B,uBAAuB,4BAA4B,OAAO,CAAC,MAAM,oFAAoF;SAC7L,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AACtC,CAAC"}