@cello-protocol/daemon 0.0.227 → 0.0.228

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 (895) hide show
  1. package/dist/agent-admin-handlers.d.ts +56 -0
  2. package/dist/agent-admin-handlers.d.ts.map +1 -0
  3. package/dist/agent-admin-handlers.js +73 -0
  4. package/dist/agent-admin-handlers.js.map +1 -0
  5. package/dist/agent-handlers.d.ts +72 -0
  6. package/dist/agent-handlers.d.ts.map +1 -0
  7. package/dist/agent-handlers.js +604 -0
  8. package/dist/agent-handlers.js.map +1 -0
  9. package/dist/agent-id-migration.d.ts +70 -0
  10. package/dist/agent-id-migration.d.ts.map +1 -0
  11. package/dist/agent-id-migration.js +468 -0
  12. package/dist/agent-id-migration.js.map +1 -0
  13. package/dist/agent-loader.d.ts +37 -0
  14. package/dist/agent-loader.d.ts.map +1 -0
  15. package/dist/agent-loader.js +37 -0
  16. package/dist/agent-loader.js.map +1 -0
  17. package/dist/agent-selection-root.d.ts +29 -0
  18. package/dist/agent-selection-root.d.ts.map +1 -0
  19. package/dist/agent-selection-root.js +126 -0
  20. package/dist/agent-selection-root.js.map +1 -0
  21. package/dist/agent-selection.d.ts +73 -0
  22. package/dist/agent-selection.d.ts.map +1 -0
  23. package/dist/agent-selection.js +80 -0
  24. package/dist/agent-selection.js.map +1 -0
  25. package/dist/agent-settings-keys.d.ts +52 -0
  26. package/dist/agent-settings-keys.d.ts.map +1 -0
  27. package/dist/agent-settings-keys.js +114 -0
  28. package/dist/agent-settings-keys.js.map +1 -0
  29. package/dist/agent-state.d.ts +67 -0
  30. package/dist/agent-state.d.ts.map +1 -0
  31. package/dist/agent-state.js +66 -0
  32. package/dist/agent-state.js.map +1 -0
  33. package/dist/assignment-verify.d.ts +90 -0
  34. package/dist/assignment-verify.d.ts.map +1 -0
  35. package/dist/assignment-verify.js +301 -0
  36. package/dist/assignment-verify.js.map +1 -0
  37. package/dist/attendance-wiring.d.ts +23 -0
  38. package/dist/attendance-wiring.d.ts.map +1 -0
  39. package/dist/attendance-wiring.js +248 -0
  40. package/dist/attendance-wiring.js.map +1 -0
  41. package/dist/attestation-body.d.ts +47 -0
  42. package/dist/attestation-body.d.ts.map +1 -0
  43. package/dist/attestation-body.js +71 -0
  44. package/dist/attestation-body.js.map +1 -0
  45. package/dist/authorship-verification.d.ts +91 -0
  46. package/dist/authorship-verification.d.ts.map +1 -0
  47. package/dist/authorship-verification.js +532 -0
  48. package/dist/authorship-verification.js.map +1 -0
  49. package/dist/away-detection.d.ts +139 -0
  50. package/dist/away-detection.d.ts.map +1 -0
  51. package/dist/away-detection.js +186 -0
  52. package/dist/away-detection.js.map +1 -0
  53. package/dist/away-inbox-oneshot.d.ts +40 -0
  54. package/dist/away-inbox-oneshot.d.ts.map +1 -0
  55. package/dist/away-inbox-oneshot.js +311 -0
  56. package/dist/away-inbox-oneshot.js.map +1 -0
  57. package/dist/backup-restore-handlers.d.ts +10 -0
  58. package/dist/backup-restore-handlers.d.ts.map +1 -0
  59. package/dist/backup-restore-handlers.js +82 -0
  60. package/dist/backup-restore-handlers.js.map +1 -0
  61. package/dist/backup-restore.d.ts +102 -0
  62. package/dist/backup-restore.d.ts.map +1 -0
  63. package/dist/backup-restore.js +341 -0
  64. package/dist/backup-restore.js.map +1 -0
  65. package/dist/bin/cello-daemon.d.ts +13 -0
  66. package/dist/bin/cello-daemon.d.ts.map +1 -0
  67. package/dist/bin/cello-daemon.js.map +1 -0
  68. package/dist/boot-agents.d.ts +64 -0
  69. package/dist/boot-agents.d.ts.map +1 -0
  70. package/dist/boot-agents.js +241 -0
  71. package/dist/boot-agents.js.map +1 -0
  72. package/dist/boot-connection-state.d.ts +38 -0
  73. package/dist/boot-connection-state.d.ts.map +1 -0
  74. package/dist/boot-connection-state.js +95 -0
  75. package/dist/boot-connection-state.js.map +1 -0
  76. package/dist/boot-core.d.ts +36 -0
  77. package/dist/boot-core.d.ts.map +1 -0
  78. package/dist/boot-core.js +293 -0
  79. package/dist/boot-core.js.map +1 -0
  80. package/dist/boot-parked-content.d.ts +34 -0
  81. package/dist/boot-parked-content.d.ts.map +1 -0
  82. package/dist/boot-parked-content.js +528 -0
  83. package/dist/boot-parked-content.js.map +1 -0
  84. package/dist/boot-sweeps.d.ts +22 -0
  85. package/dist/boot-sweeps.d.ts.map +1 -0
  86. package/dist/boot-sweeps.js +51 -0
  87. package/dist/boot-sweeps.js.map +1 -0
  88. package/dist/bundled-consortium-manifest.d.ts +47 -0
  89. package/dist/bundled-consortium-manifest.d.ts.map +1 -0
  90. package/dist/bundled-consortium-manifest.js +116 -0
  91. package/dist/bundled-consortium-manifest.js.map +1 -0
  92. package/dist/cbor-blob-migration.d.ts +8 -0
  93. package/dist/cbor-blob-migration.d.ts.map +1 -0
  94. package/dist/cbor-blob-migration.js +116 -0
  95. package/dist/cbor-blob-migration.js.map +1 -0
  96. package/dist/challenge-verifier.d.ts +12 -0
  97. package/dist/challenge-verifier.d.ts.map +1 -0
  98. package/dist/challenge-verifier.js +11 -0
  99. package/dist/challenge-verifier.js.map +1 -0
  100. package/dist/channel-log-store.d.ts +80 -0
  101. package/dist/channel-log-store.d.ts.map +1 -0
  102. package/dist/channel-log-store.js +290 -0
  103. package/dist/channel-log-store.js.map +1 -0
  104. package/dist/close-commitment.d.ts +67 -0
  105. package/dist/close-commitment.d.ts.map +1 -0
  106. package/dist/close-commitment.js +78 -0
  107. package/dist/close-commitment.js.map +1 -0
  108. package/dist/close-session-handler.d.ts +94 -0
  109. package/dist/close-session-handler.d.ts.map +1 -0
  110. package/dist/close-session-handler.js +1418 -0
  111. package/dist/close-session-handler.js.map +1 -0
  112. package/dist/co-attendance.d.ts +29 -0
  113. package/dist/co-attendance.d.ts.map +1 -0
  114. package/dist/co-attendance.js +32 -0
  115. package/dist/co-attendance.js.map +1 -0
  116. package/dist/column-birth.d.ts +40 -0
  117. package/dist/column-birth.d.ts.map +1 -0
  118. package/dist/column-birth.js +61 -0
  119. package/dist/column-birth.js.map +1 -0
  120. package/dist/connect-or-start.d.ts +25 -0
  121. package/dist/connect-or-start.d.ts.map +1 -0
  122. package/dist/connect-or-start.js +175 -0
  123. package/dist/connect-or-start.js.map +1 -0
  124. package/dist/connection-agents.d.ts +16 -0
  125. package/dist/connection-agents.d.ts.map +1 -0
  126. package/dist/connection-agents.js +68 -0
  127. package/dist/connection-agents.js.map +1 -0
  128. package/dist/consent-migration.d.ts +49 -0
  129. package/dist/consent-migration.d.ts.map +1 -0
  130. package/dist/consent-migration.js +128 -0
  131. package/dist/consent-migration.js.map +1 -0
  132. package/dist/consortium-bootstrap.d.ts +138 -0
  133. package/dist/consortium-bootstrap.d.ts.map +1 -0
  134. package/dist/consortium-bootstrap.js +339 -0
  135. package/dist/consortium-bootstrap.js.map +1 -0
  136. package/dist/consortium-fingerprint.d.ts +115 -0
  137. package/dist/consortium-fingerprint.d.ts.map +1 -0
  138. package/dist/consortium-fingerprint.js +175 -0
  139. package/dist/consortium-fingerprint.js.map +1 -0
  140. package/dist/contact-handlers.d.ts +59 -0
  141. package/dist/contact-handlers.d.ts.map +1 -0
  142. package/dist/contact-handlers.js +343 -0
  143. package/dist/contact-handlers.js.map +1 -0
  144. package/dist/contact-pubkey-case.d.ts +65 -0
  145. package/dist/contact-pubkey-case.d.ts.map +1 -0
  146. package/dist/contact-pubkey-case.js +136 -0
  147. package/dist/contact-pubkey-case.js.map +1 -0
  148. package/dist/contacts-tier-migration.d.ts +90 -0
  149. package/dist/contacts-tier-migration.d.ts.map +1 -0
  150. package/dist/contacts-tier-migration.js +150 -0
  151. package/dist/contacts-tier-migration.js.map +1 -0
  152. package/dist/content-encryption-status.d.ts +111 -0
  153. package/dist/content-encryption-status.d.ts.map +1 -0
  154. package/dist/content-encryption-status.js +158 -0
  155. package/dist/content-encryption-status.js.map +1 -0
  156. package/dist/content-park-client.d.ts +90 -0
  157. package/dist/content-park-client.d.ts.map +1 -0
  158. package/dist/content-park-client.js +362 -0
  159. package/dist/content-park-client.js.map +1 -0
  160. package/dist/content-park.d.ts +52 -0
  161. package/dist/content-park.d.ts.map +1 -0
  162. package/dist/content-park.js +1309 -0
  163. package/dist/content-park.js.map +1 -0
  164. package/dist/cross-node-negotiation.d.ts +44 -0
  165. package/dist/cross-node-negotiation.d.ts.map +1 -0
  166. package/dist/cross-node-negotiation.js +34 -0
  167. package/dist/cross-node-negotiation.js.map +1 -0
  168. package/dist/daemon-handle.d.ts +57 -0
  169. package/dist/daemon-handle.d.ts.map +1 -0
  170. package/dist/daemon-handle.js +2 -0
  171. package/dist/daemon-handle.js.map +1 -0
  172. package/dist/daemon-status-report.d.ts +50 -0
  173. package/dist/daemon-status-report.d.ts.map +1 -0
  174. package/dist/daemon-status-report.js +81 -0
  175. package/dist/daemon-status-report.js.map +1 -0
  176. package/dist/daemon.d.ts +45 -0
  177. package/dist/daemon.d.ts.map +1 -0
  178. package/dist/daemon.js +1177 -0
  179. package/dist/daemon.js.map +1 -0
  180. package/dist/db-identity-store.d.ts +159 -0
  181. package/dist/db-identity-store.d.ts.map +1 -0
  182. package/dist/db-identity-store.js +502 -0
  183. package/dist/db-identity-store.js.map +1 -0
  184. package/dist/delivery-open-registry.d.ts +92 -0
  185. package/dist/delivery-open-registry.d.ts.map +1 -0
  186. package/dist/delivery-open-registry.js +121 -0
  187. package/dist/delivery-open-registry.js.map +1 -0
  188. package/dist/delivery-session-suspects.d.ts +56 -0
  189. package/dist/delivery-session-suspects.d.ts.map +1 -0
  190. package/dist/delivery-session-suspects.js +94 -0
  191. package/dist/delivery-session-suspects.js.map +1 -0
  192. package/dist/directory-auth-posture.d.ts +87 -0
  193. package/dist/directory-auth-posture.d.ts.map +1 -0
  194. package/dist/directory-auth-posture.js +134 -0
  195. package/dist/directory-auth-posture.js.map +1 -0
  196. package/dist/directory-bootstrap.d.ts +310 -0
  197. package/dist/directory-bootstrap.d.ts.map +1 -0
  198. package/dist/directory-bootstrap.js +557 -0
  199. package/dist/directory-bootstrap.js.map +1 -0
  200. package/dist/directory-connect.d.ts +27 -0
  201. package/dist/directory-connect.d.ts.map +1 -0
  202. package/dist/directory-connect.js +106 -0
  203. package/dist/directory-connect.js.map +1 -0
  204. package/dist/disconnect-cleanup.d.ts +51 -0
  205. package/dist/disconnect-cleanup.d.ts.map +1 -0
  206. package/dist/disconnect-cleanup.js +72 -0
  207. package/dist/disconnect-cleanup.js.map +1 -0
  208. package/dist/document-amendment-store.d.ts +120 -0
  209. package/dist/document-amendment-store.d.ts.map +1 -0
  210. package/dist/document-amendment-store.js +266 -0
  211. package/dist/document-amendment-store.js.map +1 -0
  212. package/dist/document-delivery-transport.d.ts +168 -0
  213. package/dist/document-delivery-transport.d.ts.map +1 -0
  214. package/dist/document-delivery-transport.js +206 -0
  215. package/dist/document-delivery-transport.js.map +1 -0
  216. package/dist/document-engine.d.ts +134 -0
  217. package/dist/document-engine.d.ts.map +1 -0
  218. package/dist/document-engine.js +282 -0
  219. package/dist/document-engine.js.map +1 -0
  220. package/dist/document-flag.d.ts +58 -0
  221. package/dist/document-flag.d.ts.map +1 -0
  222. package/dist/document-flag.js +70 -0
  223. package/dist/document-flag.js.map +1 -0
  224. package/dist/document-frame-router.d.ts +245 -0
  225. package/dist/document-frame-router.d.ts.map +1 -0
  226. package/dist/document-frame-router.js +397 -0
  227. package/dist/document-frame-router.js.map +1 -0
  228. package/dist/document-gate-wiring.d.ts +61 -0
  229. package/dist/document-gate-wiring.d.ts.map +1 -0
  230. package/dist/document-gate-wiring.js +125 -0
  231. package/dist/document-gate-wiring.js.map +1 -0
  232. package/dist/document-gate.d.ts +149 -0
  233. package/dist/document-gate.d.ts.map +1 -0
  234. package/dist/document-gate.js +509 -0
  235. package/dist/document-gate.js.map +1 -0
  236. package/dist/document-handlers.d.ts +47 -0
  237. package/dist/document-handlers.d.ts.map +1 -0
  238. package/dist/document-handlers.js +2203 -0
  239. package/dist/document-handlers.js.map +1 -0
  240. package/dist/document-handshake.d.ts +176 -0
  241. package/dist/document-handshake.d.ts.map +1 -0
  242. package/dist/document-handshake.js +452 -0
  243. package/dist/document-handshake.js.map +1 -0
  244. package/dist/document-inbound.d.ts +162 -0
  245. package/dist/document-inbound.d.ts.map +1 -0
  246. package/dist/document-inbound.js +530 -0
  247. package/dist/document-inbound.js.map +1 -0
  248. package/dist/document-json.d.ts +120 -0
  249. package/dist/document-json.d.ts.map +1 -0
  250. package/dist/document-json.js +191 -0
  251. package/dist/document-json.js.map +1 -0
  252. package/dist/document-layer.d.ts +215 -0
  253. package/dist/document-layer.d.ts.map +1 -0
  254. package/dist/document-layer.js +1025 -0
  255. package/dist/document-layer.js.map +1 -0
  256. package/dist/document-lifecycle.d.ts +52 -0
  257. package/dist/document-lifecycle.d.ts.map +1 -0
  258. package/dist/document-lifecycle.js +134 -0
  259. package/dist/document-lifecycle.js.map +1 -0
  260. package/dist/document-live-docs.d.ts +58 -0
  261. package/dist/document-live-docs.d.ts.map +1 -0
  262. package/dist/document-live-docs.js +126 -0
  263. package/dist/document-live-docs.js.map +1 -0
  264. package/dist/document-notify.d.ts +228 -0
  265. package/dist/document-notify.d.ts.map +1 -0
  266. package/dist/document-notify.js +580 -0
  267. package/dist/document-notify.js.map +1 -0
  268. package/dist/document-profile.d.ts +61 -0
  269. package/dist/document-profile.d.ts.map +1 -0
  270. package/dist/document-profile.js +112 -0
  271. package/dist/document-profile.js.map +1 -0
  272. package/dist/document-publish.d.ts +87 -0
  273. package/dist/document-publish.d.ts.map +1 -0
  274. package/dist/document-publish.js +173 -0
  275. package/dist/document-publish.js.map +1 -0
  276. package/dist/document-reachability.d.ts +42 -0
  277. package/dist/document-reachability.d.ts.map +1 -0
  278. package/dist/document-reachability.js +80 -0
  279. package/dist/document-reachability.js.map +1 -0
  280. package/dist/document-reconcile-engine.d.ts +66 -0
  281. package/dist/document-reconcile-engine.d.ts.map +1 -0
  282. package/dist/document-reconcile-engine.js +225 -0
  283. package/dist/document-reconcile-engine.js.map +1 -0
  284. package/dist/document-reconcile-scheduler.d.ts +163 -0
  285. package/dist/document-reconcile-scheduler.d.ts.map +1 -0
  286. package/dist/document-reconcile-scheduler.js +303 -0
  287. package/dist/document-reconcile-scheduler.js.map +1 -0
  288. package/dist/document-rejection.d.ts +251 -0
  289. package/dist/document-rejection.d.ts.map +1 -0
  290. package/dist/document-rejection.js +435 -0
  291. package/dist/document-rejection.js.map +1 -0
  292. package/dist/document-screen.d.ts +114 -0
  293. package/dist/document-screen.d.ts.map +1 -0
  294. package/dist/document-screen.js +223 -0
  295. package/dist/document-screen.js.map +1 -0
  296. package/dist/document-store.d.ts +372 -0
  297. package/dist/document-store.d.ts.map +1 -0
  298. package/dist/document-store.js +931 -0
  299. package/dist/document-store.js.map +1 -0
  300. package/dist/document-surface.d.ts +32 -0
  301. package/dist/document-surface.d.ts.map +1 -0
  302. package/dist/document-surface.js +151 -0
  303. package/dist/document-surface.js.map +1 -0
  304. package/dist/document-types.d.ts +94 -0
  305. package/dist/document-types.d.ts.map +1 -0
  306. package/dist/document-types.js +90 -0
  307. package/dist/document-types.js.map +1 -0
  308. package/dist/document-watch.d.ts +69 -0
  309. package/dist/document-watch.d.ts.map +1 -0
  310. package/dist/document-watch.js +108 -0
  311. package/dist/document-watch.js.map +1 -0
  312. package/dist/document-wiring.d.ts +53 -0
  313. package/dist/document-wiring.d.ts.map +1 -0
  314. package/dist/document-wiring.js +323 -0
  315. package/dist/document-wiring.js.map +1 -0
  316. package/dist/document-write-guard.d.ts +66 -0
  317. package/dist/document-write-guard.d.ts.map +1 -0
  318. package/dist/document-write-guard.js +98 -0
  319. package/dist/document-write-guard.js.map +1 -0
  320. package/dist/document-write-path.d.ts +94 -0
  321. package/dist/document-write-path.d.ts.map +1 -0
  322. package/dist/document-write-path.js +530 -0
  323. package/dist/document-write-path.js.map +1 -0
  324. package/dist/error-message.d.ts +7 -0
  325. package/dist/error-message.d.ts.map +1 -0
  326. package/dist/error-message.js +19 -0
  327. package/dist/error-message.js.map +1 -0
  328. package/dist/file-manifest-provider.d.ts +37 -0
  329. package/dist/file-manifest-provider.d.ts.map +1 -0
  330. package/dist/file-manifest-provider.js +105 -0
  331. package/dist/file-manifest-provider.js.map +1 -0
  332. package/dist/frame-values.d.ts +3 -0
  333. package/dist/frame-values.d.ts.map +1 -0
  334. package/dist/frame-values.js +84 -0
  335. package/dist/frame-values.js.map +1 -0
  336. package/dist/frontier-mismatch.d.ts +73 -0
  337. package/dist/frontier-mismatch.d.ts.map +1 -0
  338. package/dist/frontier-mismatch.js +89 -0
  339. package/dist/frontier-mismatch.js.map +1 -0
  340. package/dist/gateway-config-handlers.d.ts +26 -0
  341. package/dist/gateway-config-handlers.d.ts.map +1 -0
  342. package/dist/gateway-config-handlers.js +429 -0
  343. package/dist/gateway-config-handlers.js.map +1 -0
  344. package/dist/held-content.d.ts +145 -0
  345. package/dist/held-content.d.ts.map +1 -0
  346. package/dist/held-content.js +389 -0
  347. package/dist/held-content.js.map +1 -0
  348. package/dist/http-manifest-poll.d.ts +67 -0
  349. package/dist/http-manifest-poll.d.ts.map +1 -0
  350. package/dist/http-manifest-poll.js +150 -0
  351. package/dist/http-manifest-poll.js.map +1 -0
  352. package/dist/identity-migration.d.ts +40 -0
  353. package/dist/identity-migration.d.ts.map +1 -0
  354. package/dist/identity-migration.js +461 -0
  355. package/dist/identity-migration.js.map +1 -0
  356. package/dist/inbound-refusals.d.ts +283 -0
  357. package/dist/inbound-refusals.d.ts.map +1 -0
  358. package/dist/inbound-refusals.js +919 -0
  359. package/dist/inbound-refusals.js.map +1 -0
  360. package/dist/inbound-seal-request.d.ts +32 -0
  361. package/dist/inbound-seal-request.d.ts.map +1 -0
  362. package/dist/inbound-seal-request.js +228 -0
  363. package/dist/inbound-seal-request.js.map +1 -0
  364. package/dist/inbound-sessions.d.ts +291 -0
  365. package/dist/inbound-sessions.d.ts.map +1 -0
  366. package/dist/inbound-sessions.js +1609 -0
  367. package/dist/inbound-sessions.js.map +1 -0
  368. package/dist/inclusion-proof-handlers.d.ts +43 -0
  369. package/dist/inclusion-proof-handlers.d.ts.map +1 -0
  370. package/dist/inclusion-proof-handlers.js +565 -0
  371. package/dist/inclusion-proof-handlers.js.map +1 -0
  372. package/dist/inclusion-proof.d.ts +151 -0
  373. package/dist/inclusion-proof.d.ts.map +1 -0
  374. package/dist/inclusion-proof.js +228 -0
  375. package/dist/inclusion-proof.js.map +1 -0
  376. package/dist/index.d.ts +32 -0
  377. package/dist/index.d.ts.map +1 -0
  378. package/dist/index.js.map +1 -0
  379. package/dist/initiate-session-handler.d.ts +55 -0
  380. package/dist/initiate-session-handler.d.ts.map +1 -0
  381. package/dist/initiate-session-handler.js +310 -0
  382. package/dist/initiate-session-handler.js.map +1 -0
  383. package/dist/ipc-client.d.ts +31 -0
  384. package/dist/ipc-client.d.ts.map +1 -0
  385. package/dist/ipc-client.js +113 -0
  386. package/dist/ipc-client.js.map +1 -0
  387. package/dist/ipc-server.d.ts +63 -0
  388. package/dist/ipc-server.d.ts.map +1 -0
  389. package/dist/ipc-server.js +429 -0
  390. package/dist/ipc-server.js.map +1 -0
  391. package/dist/ipc-surface.d.ts +44 -0
  392. package/dist/ipc-surface.d.ts.map +1 -0
  393. package/dist/ipc-surface.js +107 -0
  394. package/dist/ipc-surface.js.map +1 -0
  395. package/dist/line-lcs.d.ts +51 -0
  396. package/dist/line-lcs.d.ts.map +1 -0
  397. package/dist/line-lcs.js +71 -0
  398. package/dist/line-lcs.js.map +1 -0
  399. package/dist/lock-file.d.ts +39 -0
  400. package/dist/lock-file.d.ts.map +1 -0
  401. package/dist/lock-file.js +120 -0
  402. package/dist/lock-file.js.map +1 -0
  403. package/dist/log-collapse.d.ts +66 -0
  404. package/dist/log-collapse.d.ts.map +1 -0
  405. package/dist/log-collapse.js +244 -0
  406. package/dist/log-collapse.js.map +1 -0
  407. package/dist/log-rotate.d.ts +67 -0
  408. package/dist/log-rotate.d.ts.map +1 -0
  409. package/dist/log-rotate.js +134 -0
  410. package/dist/log-rotate.js.map +1 -0
  411. package/dist/manifest-deps.d.ts +25 -0
  412. package/dist/manifest-deps.d.ts.map +1 -0
  413. package/dist/manifest-deps.js +151 -0
  414. package/dist/manifest-deps.js.map +1 -0
  415. package/dist/manifest-poll-scheduler.d.ts +31 -0
  416. package/dist/manifest-poll-scheduler.d.ts.map +1 -0
  417. package/dist/manifest-poll-scheduler.js +59 -0
  418. package/dist/manifest-poll-scheduler.js.map +1 -0
  419. package/dist/manifest-validity.d.ts +153 -0
  420. package/dist/manifest-validity.d.ts.map +1 -0
  421. package/dist/manifest-validity.js +268 -0
  422. package/dist/manifest-validity.js.map +1 -0
  423. package/dist/manifest-version-store-db.d.ts +24 -0
  424. package/dist/manifest-version-store-db.d.ts.map +1 -0
  425. package/dist/manifest-version-store-db.js +58 -0
  426. package/dist/manifest-version-store-db.js.map +1 -0
  427. package/dist/manifest-version-store.d.ts +16 -0
  428. package/dist/manifest-version-store.d.ts.map +1 -0
  429. package/dist/manifest-version-store.js +15 -0
  430. package/dist/manifest-version-store.js.map +1 -0
  431. package/dist/network-directory-node.d.ts +136 -0
  432. package/dist/network-directory-node.d.ts.map +1 -0
  433. package/dist/network-directory-node.js +810 -0
  434. package/dist/network-directory-node.js.map +1 -0
  435. package/dist/nonce-dedup.d.ts +68 -0
  436. package/dist/nonce-dedup.d.ts.map +1 -0
  437. package/dist/nonce-dedup.js +205 -0
  438. package/dist/nonce-dedup.js.map +1 -0
  439. package/dist/notification-dispatcher.d.ts +92 -0
  440. package/dist/notification-dispatcher.d.ts.map +1 -0
  441. package/dist/notification-dispatcher.js +210 -0
  442. package/dist/notification-dispatcher.js.map +1 -0
  443. package/dist/notification-handlers.d.ts +53 -0
  444. package/dist/notification-handlers.d.ts.map +1 -0
  445. package/dist/notification-handlers.js +461 -0
  446. package/dist/notification-handlers.js.map +1 -0
  447. package/dist/onboarding-guidance.d.ts +79 -0
  448. package/dist/onboarding-guidance.d.ts.map +1 -0
  449. package/dist/onboarding-guidance.js +95 -0
  450. package/dist/onboarding-guidance.js.map +1 -0
  451. package/dist/operator-guidance.d.ts +25 -0
  452. package/dist/operator-guidance.d.ts.map +1 -0
  453. package/dist/operator-guidance.js +50 -0
  454. package/dist/operator-guidance.js.map +1 -0
  455. package/dist/orphan-triage.d.ts +130 -0
  456. package/dist/orphan-triage.d.ts.map +1 -0
  457. package/dist/orphan-triage.js +207 -0
  458. package/dist/orphan-triage.js.map +1 -0
  459. package/dist/outbound-sessions.d.ts +126 -0
  460. package/dist/outbound-sessions.d.ts.map +1 -0
  461. package/dist/outbound-sessions.js +1052 -0
  462. package/dist/outbound-sessions.js.map +1 -0
  463. package/dist/park-envelope.d.ts +329 -0
  464. package/dist/park-envelope.d.ts.map +1 -0
  465. package/dist/park-envelope.js +509 -0
  466. package/dist/park-envelope.js.map +1 -0
  467. package/dist/park-recovery.d.ts +273 -0
  468. package/dist/park-recovery.d.ts.map +1 -0
  469. package/dist/park-recovery.js +717 -0
  470. package/dist/park-recovery.js.map +1 -0
  471. package/dist/park-refusals.d.ts +147 -0
  472. package/dist/park-refusals.d.ts.map +1 -0
  473. package/dist/park-refusals.js +331 -0
  474. package/dist/park-refusals.js.map +1 -0
  475. package/dist/quarantine-framing.d.ts +92 -0
  476. package/dist/quarantine-framing.d.ts.map +1 -0
  477. package/dist/quarantine-framing.js +111 -0
  478. package/dist/quarantine-framing.js.map +1 -0
  479. package/dist/reconnect-drain.d.ts +22 -0
  480. package/dist/reconnect-drain.d.ts.map +1 -0
  481. package/dist/reconnect-drain.js +65 -0
  482. package/dist/reconnect-drain.js.map +1 -0
  483. package/dist/recovered-position.d.ts +14 -0
  484. package/dist/recovered-position.d.ts.map +1 -0
  485. package/dist/recovered-position.js +59 -0
  486. package/dist/recovered-position.js.map +1 -0
  487. package/dist/refusal-notices.d.ts +196 -0
  488. package/dist/refusal-notices.d.ts.map +1 -0
  489. package/dist/refusal-notices.js +516 -0
  490. package/dist/refusal-notices.js.map +1 -0
  491. package/dist/refusal-reasons.d.ts +245 -0
  492. package/dist/refusal-reasons.d.ts.map +1 -0
  493. package/dist/refusal-reasons.js +363 -0
  494. package/dist/refusal-reasons.js.map +1 -0
  495. package/dist/register-handler.d.ts +36 -0
  496. package/dist/register-handler.d.ts.map +1 -0
  497. package/dist/register-handler.js +285 -0
  498. package/dist/register-handler.js.map +1 -0
  499. package/dist/registration-context.d.ts +72 -0
  500. package/dist/registration-context.d.ts.map +1 -0
  501. package/dist/registration-context.js +126 -0
  502. package/dist/registration-context.js.map +1 -0
  503. package/dist/registration-manager.d.ts +94 -0
  504. package/dist/registration-manager.d.ts.map +1 -0
  505. package/dist/registration-manager.js +585 -0
  506. package/dist/registration-manager.js.map +1 -0
  507. package/dist/registration-persistence.d.ts +183 -0
  508. package/dist/registration-persistence.d.ts.map +1 -0
  509. package/dist/registration-persistence.js +263 -0
  510. package/dist/registration-persistence.js.map +1 -0
  511. package/dist/registry-poll.d.ts +52 -0
  512. package/dist/registry-poll.d.ts.map +1 -0
  513. package/dist/registry-poll.js +140 -0
  514. package/dist/registry-poll.js.map +1 -0
  515. package/dist/registry-version-store-db.d.ts +22 -0
  516. package/dist/registry-version-store-db.d.ts.map +1 -0
  517. package/dist/registry-version-store-db.js +50 -0
  518. package/dist/registry-version-store-db.js.map +1 -0
  519. package/dist/relay-endpoints.d.ts +18 -0
  520. package/dist/relay-endpoints.d.ts.map +1 -0
  521. package/dist/relay-endpoints.js +9 -0
  522. package/dist/relay-endpoints.js.map +1 -0
  523. package/dist/relay-only.d.ts +140 -0
  524. package/dist/relay-only.d.ts.map +1 -0
  525. package/dist/relay-only.js +193 -0
  526. package/dist/relay-only.js.map +1 -0
  527. package/dist/relay-receipt-store.d.ts +155 -0
  528. package/dist/relay-receipt-store.d.ts.map +1 -0
  529. package/dist/relay-receipt-store.js +284 -0
  530. package/dist/relay-receipt-store.js.map +1 -0
  531. package/dist/relay-reconnect.d.ts +32 -0
  532. package/dist/relay-reconnect.d.ts.map +1 -0
  533. package/dist/relay-reconnect.js +29 -0
  534. package/dist/relay-reconnect.js.map +1 -0
  535. package/dist/reply-lag.d.ts +11 -0
  536. package/dist/reply-lag.d.ts.map +1 -0
  537. package/dist/reply-lag.js +46 -0
  538. package/dist/reply-lag.js.map +1 -0
  539. package/dist/resolve-named-agent.d.ts +49 -0
  540. package/dist/resolve-named-agent.d.ts.map +1 -0
  541. package/dist/resolve-named-agent.js +77 -0
  542. package/dist/resolve-named-agent.js.map +1 -0
  543. package/dist/restart-seal-resolver.d.ts +110 -0
  544. package/dist/restart-seal-resolver.d.ts.map +1 -0
  545. package/dist/restart-seal-resolver.js +353 -0
  546. package/dist/restart-seal-resolver.js.map +1 -0
  547. package/dist/resume-last-seen.d.ts +15 -0
  548. package/dist/resume-last-seen.d.ts.map +1 -0
  549. package/dist/resume-last-seen.js +44 -0
  550. package/dist/resume-last-seen.js.map +1 -0
  551. package/dist/retry-queue.d.ts +203 -0
  552. package/dist/retry-queue.d.ts.map +1 -0
  553. package/dist/retry-queue.js +701 -0
  554. package/dist/retry-queue.js.map +1 -0
  555. package/dist/roster-freshness.d.ts +160 -0
  556. package/dist/roster-freshness.d.ts.map +1 -0
  557. package/dist/roster-freshness.js +250 -0
  558. package/dist/roster-freshness.js.map +1 -0
  559. package/dist/screening-status.d.ts +15 -0
  560. package/dist/screening-status.d.ts.map +1 -0
  561. package/dist/screening-status.js +65 -0
  562. package/dist/screening-status.js.map +1 -0
  563. package/dist/seal-carried-close.d.ts +37 -0
  564. package/dist/seal-carried-close.d.ts.map +1 -0
  565. package/dist/seal-carried-close.js +179 -0
  566. package/dist/seal-carried-close.js.map +1 -0
  567. package/dist/seal-certificate-pull.d.ts +79 -0
  568. package/dist/seal-certificate-pull.d.ts.map +1 -0
  569. package/dist/seal-certificate-pull.js +186 -0
  570. package/dist/seal-certificate-pull.js.map +1 -0
  571. package/dist/seal-certified-root-check.d.ts +37 -0
  572. package/dist/seal-certified-root-check.d.ts.map +1 -0
  573. package/dist/seal-certified-root-check.js +127 -0
  574. package/dist/seal-certified-root-check.js.map +1 -0
  575. package/dist/seal-coordinator.d.ts +118 -0
  576. package/dist/seal-coordinator.d.ts.map +1 -0
  577. package/dist/seal-coordinator.js +927 -0
  578. package/dist/seal-coordinator.js.map +1 -0
  579. package/dist/seal-escalation.d.ts +80 -0
  580. package/dist/seal-escalation.d.ts.map +1 -0
  581. package/dist/seal-escalation.js +283 -0
  582. package/dist/seal-escalation.js.map +1 -0
  583. package/dist/seal-evidence-root-check.d.ts +70 -0
  584. package/dist/seal-evidence-root-check.d.ts.map +1 -0
  585. package/dist/seal-evidence-root-check.js +199 -0
  586. package/dist/seal-evidence-root-check.js.map +1 -0
  587. package/dist/seal-failure-store.d.ts +139 -0
  588. package/dist/seal-failure-store.d.ts.map +1 -0
  589. package/dist/seal-failure-store.js +207 -0
  590. package/dist/seal-failure-store.js.map +1 -0
  591. package/dist/seal-flows.d.ts +107 -0
  592. package/dist/seal-flows.d.ts.map +1 -0
  593. package/dist/seal-flows.js +638 -0
  594. package/dist/seal-flows.js.map +1 -0
  595. package/dist/seal-frontier-verify.d.ts +103 -0
  596. package/dist/seal-frontier-verify.d.ts.map +1 -0
  597. package/dist/seal-frontier-verify.js +143 -0
  598. package/dist/seal-frontier-verify.js.map +1 -0
  599. package/dist/seal-leaf.d.ts +58 -0
  600. package/dist/seal-leaf.d.ts.map +1 -0
  601. package/dist/seal-leaf.js +112 -0
  602. package/dist/seal-leaf.js.map +1 -0
  603. package/dist/seal-legibility-tbs.d.ts +25 -0
  604. package/dist/seal-legibility-tbs.d.ts.map +1 -0
  605. package/dist/seal-legibility-tbs.js +77 -0
  606. package/dist/seal-legibility-tbs.js.map +1 -0
  607. package/dist/seal-local-terminus.d.ts +41 -0
  608. package/dist/seal-local-terminus.d.ts.map +1 -0
  609. package/dist/seal-local-terminus.js +170 -0
  610. package/dist/seal-local-terminus.js.map +1 -0
  611. package/dist/seal-receipt-upgrade.d.ts +30 -0
  612. package/dist/seal-receipt-upgrade.d.ts.map +1 -0
  613. package/dist/seal-receipt-upgrade.js +49 -0
  614. package/dist/seal-receipt-upgrade.js.map +1 -0
  615. package/dist/seal-relay-silence.d.ts +51 -0
  616. package/dist/seal-relay-silence.d.ts.map +1 -0
  617. package/dist/seal-relay-silence.js +76 -0
  618. package/dist/seal-relay-silence.js.map +1 -0
  619. package/dist/seal-settle.d.ts +38 -0
  620. package/dist/seal-settle.d.ts.map +1 -0
  621. package/dist/seal-settle.js +33 -0
  622. package/dist/seal-settle.js.map +1 -0
  623. package/dist/seal-upgrade.d.ts +107 -0
  624. package/dist/seal-upgrade.d.ts.map +1 -0
  625. package/dist/seal-upgrade.js +200 -0
  626. package/dist/seal-upgrade.js.map +1 -0
  627. package/dist/sealed-conversation.d.ts +36 -0
  628. package/dist/sealed-conversation.d.ts.map +1 -0
  629. package/dist/sealed-conversation.js +171 -0
  630. package/dist/sealed-conversation.js.map +1 -0
  631. package/dist/sealed-leaf-set.d.ts +92 -0
  632. package/dist/sealed-leaf-set.d.ts.map +1 -0
  633. package/dist/sealed-leaf-set.js +122 -0
  634. package/dist/sealed-leaf-set.js.map +1 -0
  635. package/dist/send-claims.d.ts +56 -0
  636. package/dist/send-claims.d.ts.map +1 -0
  637. package/dist/send-claims.js +50 -0
  638. package/dist/send-claims.js.map +1 -0
  639. package/dist/session-assignment-parser.d.ts +104 -0
  640. package/dist/session-assignment-parser.d.ts.map +1 -0
  641. package/dist/session-assignment-parser.js +345 -0
  642. package/dist/session-assignment-parser.js.map +1 -0
  643. package/dist/session-category.d.ts +19 -0
  644. package/dist/session-category.d.ts.map +1 -0
  645. package/dist/session-category.js +14 -0
  646. package/dist/session-category.js.map +1 -0
  647. package/dist/session-ceremony.d.ts +297 -0
  648. package/dist/session-ceremony.d.ts.map +1 -0
  649. package/dist/session-ceremony.js +964 -0
  650. package/dist/session-ceremony.js.map +1 -0
  651. package/dist/session-closed.d.ts +79 -0
  652. package/dist/session-closed.d.ts.map +1 -0
  653. package/dist/session-closed.js +189 -0
  654. package/dist/session-closed.js.map +1 -0
  655. package/dist/session-connection-gater.d.ts +148 -0
  656. package/dist/session-connection-gater.d.ts.map +1 -0
  657. package/dist/session-connection-gater.js +332 -0
  658. package/dist/session-connection-gater.js.map +1 -0
  659. package/dist/session-content-context.d.ts +162 -0
  660. package/dist/session-content-context.d.ts.map +1 -0
  661. package/dist/session-content-context.js +2 -0
  662. package/dist/session-content-context.js.map +1 -0
  663. package/dist/session-content-handlers.d.ts +82 -0
  664. package/dist/session-content-handlers.d.ts.map +1 -0
  665. package/dist/session-content-handlers.js +1382 -0
  666. package/dist/session-content-handlers.js.map +1 -0
  667. package/dist/session-content-ingest.d.ts +206 -0
  668. package/dist/session-content-ingest.d.ts.map +1 -0
  669. package/dist/session-content-ingest.js +2167 -0
  670. package/dist/session-content-ingest.js.map +1 -0
  671. package/dist/session-content-send.d.ts +191 -0
  672. package/dist/session-content-send.d.ts.map +1 -0
  673. package/dist/session-content-send.js +1360 -0
  674. package/dist/session-content-send.js.map +1 -0
  675. package/dist/session-delivery-acks.d.ts +169 -0
  676. package/dist/session-delivery-acks.d.ts.map +1 -0
  677. package/dist/session-delivery-acks.js +569 -0
  678. package/dist/session-delivery-acks.js.map +1 -0
  679. package/dist/session-ephemerals.d.ts +279 -0
  680. package/dist/session-ephemerals.d.ts.map +1 -0
  681. package/dist/session-ephemerals.js +591 -0
  682. package/dist/session-ephemerals.js.map +1 -0
  683. package/dist/session-leaf-records.d.ts +159 -0
  684. package/dist/session-leaf-records.d.ts.map +1 -0
  685. package/dist/session-leaf-records.js +408 -0
  686. package/dist/session-leaf-records.js.map +1 -0
  687. package/dist/session-lifecycle.d.ts +303 -0
  688. package/dist/session-lifecycle.d.ts.map +1 -0
  689. package/dist/session-lifecycle.js +1679 -0
  690. package/dist/session-lifecycle.js.map +1 -0
  691. package/dist/session-liveness.d.ts +135 -0
  692. package/dist/session-liveness.d.ts.map +1 -0
  693. package/dist/session-liveness.js +347 -0
  694. package/dist/session-liveness.js.map +1 -0
  695. package/dist/session-name.d.ts +35 -0
  696. package/dist/session-name.d.ts.map +1 -0
  697. package/dist/session-name.js +60 -0
  698. package/dist/session-name.js.map +1 -0
  699. package/dist/session-node-factory.d.ts +18 -0
  700. package/dist/session-node-factory.d.ts.map +1 -0
  701. package/dist/session-node-factory.js +182 -0
  702. package/dist/session-node-factory.js.map +1 -0
  703. package/dist/session-node-manager.d.ts +877 -0
  704. package/dist/session-node-manager.d.ts.map +1 -0
  705. package/dist/session-node-manager.js +2998 -0
  706. package/dist/session-node-manager.js.map +1 -0
  707. package/dist/session-node-types.d.ts +1057 -0
  708. package/dist/session-node-types.d.ts.map +1 -0
  709. package/dist/session-node-types.js +657 -0
  710. package/dist/session-node-types.js.map +1 -0
  711. package/dist/session-notify.d.ts +46 -0
  712. package/dist/session-notify.d.ts.map +1 -0
  713. package/dist/session-notify.js +116 -0
  714. package/dist/session-notify.js.map +1 -0
  715. package/dist/session-own-chain-store.d.ts +65 -0
  716. package/dist/session-own-chain-store.d.ts.map +1 -0
  717. package/dist/session-own-chain-store.js +75 -0
  718. package/dist/session-own-chain-store.js.map +1 -0
  719. package/dist/session-queries.d.ts +476 -0
  720. package/dist/session-queries.d.ts.map +1 -0
  721. package/dist/session-queries.js +1007 -0
  722. package/dist/session-queries.js.map +1 -0
  723. package/dist/session-read-handlers.d.ts +87 -0
  724. package/dist/session-read-handlers.d.ts.map +1 -0
  725. package/dist/session-read-handlers.js +689 -0
  726. package/dist/session-read-handlers.js.map +1 -0
  727. package/dist/session-records.d.ts +341 -0
  728. package/dist/session-records.d.ts.map +1 -0
  729. package/dist/session-records.js +858 -0
  730. package/dist/session-records.js.map +1 -0
  731. package/dist/session-relay-client.d.ts +659 -0
  732. package/dist/session-relay-client.d.ts.map +1 -0
  733. package/dist/session-relay-client.js +2876 -0
  734. package/dist/session-relay-client.js.map +1 -0
  735. package/dist/session-relay.d.ts +397 -0
  736. package/dist/session-relay.d.ts.map +1 -0
  737. package/dist/session-relay.js +1636 -0
  738. package/dist/session-relay.js.map +1 -0
  739. package/dist/session-salt-agreement.d.ts +331 -0
  740. package/dist/session-salt-agreement.d.ts.map +1 -0
  741. package/dist/session-salt-agreement.js +472 -0
  742. package/dist/session-salt-agreement.js.map +1 -0
  743. package/dist/session-salts.d.ts +432 -0
  744. package/dist/session-salts.d.ts.map +1 -0
  745. package/dist/session-salts.js +1540 -0
  746. package/dist/session-salts.js.map +1 -0
  747. package/dist/session-schema.d.ts +30 -0
  748. package/dist/session-schema.d.ts.map +1 -0
  749. package/dist/session-schema.js +877 -0
  750. package/dist/session-schema.js.map +1 -0
  751. package/dist/session-seal-leaf-store.d.ts +70 -0
  752. package/dist/session-seal-leaf-store.d.ts.map +1 -0
  753. package/dist/session-seal-leaf-store.js +105 -0
  754. package/dist/session-seal-leaf-store.js.map +1 -0
  755. package/dist/session-seal.d.ts +334 -0
  756. package/dist/session-seal.d.ts.map +1 -0
  757. package/dist/session-seal.js +1017 -0
  758. package/dist/session-seal.js.map +1 -0
  759. package/dist/session-terminal-refusal.d.ts +65 -0
  760. package/dist/session-terminal-refusal.d.ts.map +1 -0
  761. package/dist/session-terminal-refusal.js +87 -0
  762. package/dist/session-terminal-refusal.js.map +1 -0
  763. package/dist/session-tree.d.ts +110 -0
  764. package/dist/session-tree.d.ts.map +1 -0
  765. package/dist/session-tree.js +144 -0
  766. package/dist/session-tree.js.map +1 -0
  767. package/dist/session-views.d.ts +47 -0
  768. package/dist/session-views.d.ts.map +1 -0
  769. package/dist/session-views.js +278 -0
  770. package/dist/session-views.js.map +1 -0
  771. package/dist/signal-handlers.d.ts +63 -0
  772. package/dist/signal-handlers.d.ts.map +1 -0
  773. package/dist/signal-handlers.js +980 -0
  774. package/dist/signal-handlers.js.map +1 -0
  775. package/dist/signal-requirement-policy.d.ts +51 -0
  776. package/dist/signal-requirement-policy.d.ts.map +1 -0
  777. package/dist/signal-requirement-policy.js +89 -0
  778. package/dist/signal-requirement-policy.js.map +1 -0
  779. package/dist/signal-revocability.d.ts +49 -0
  780. package/dist/signal-revocability.d.ts.map +1 -0
  781. package/dist/signal-revocability.js +93 -0
  782. package/dist/signal-revocability.js.map +1 -0
  783. package/dist/signal-submission.d.ts +181 -0
  784. package/dist/signal-submission.d.ts.map +1 -0
  785. package/dist/signal-submission.js +368 -0
  786. package/dist/signal-submission.js.map +1 -0
  787. package/dist/signaling-connect.d.ts +118 -0
  788. package/dist/signaling-connect.d.ts.map +1 -0
  789. package/dist/signaling-connect.js +538 -0
  790. package/dist/signaling-connect.js.map +1 -0
  791. package/dist/signaling-wiring.d.ts +105 -0
  792. package/dist/signaling-wiring.d.ts.map +1 -0
  793. package/dist/signaling-wiring.js +393 -0
  794. package/dist/signaling-wiring.js.map +1 -0
  795. package/dist/singleton-lock.d.ts +85 -0
  796. package/dist/singleton-lock.d.ts.map +1 -0
  797. package/dist/singleton-lock.js +219 -0
  798. package/dist/singleton-lock.js.map +1 -0
  799. package/dist/sqlcipher-db.d.ts +139 -0
  800. package/dist/sqlcipher-db.d.ts.map +1 -0
  801. package/dist/sqlcipher-db.js +357 -0
  802. package/dist/sqlcipher-db.js.map +1 -0
  803. package/dist/standing-receivers.d.ts +319 -0
  804. package/dist/standing-receivers.d.ts.map +1 -0
  805. package/dist/standing-receivers.js +1295 -0
  806. package/dist/standing-receivers.js.map +1 -0
  807. package/dist/start-agent.d.ts +64 -0
  808. package/dist/start-agent.d.ts.map +1 -0
  809. package/dist/start-agent.js +136 -0
  810. package/dist/start-agent.js.map +1 -0
  811. package/dist/status-handler.d.ts +54 -0
  812. package/dist/status-handler.d.ts.map +1 -0
  813. package/dist/status-handler.js +61 -0
  814. package/dist/status-handler.js.map +1 -0
  815. package/dist/submission-retry.d.ts +208 -0
  816. package/dist/submission-retry.d.ts.map +1 -0
  817. package/dist/submission-retry.js +506 -0
  818. package/dist/submission-retry.js.map +1 -0
  819. package/dist/telegram-bot-client.d.ts +34 -0
  820. package/dist/telegram-bot-client.d.ts.map +1 -0
  821. package/dist/telegram-bot-client.js +36 -0
  822. package/dist/telegram-bot-client.js.map +1 -0
  823. package/dist/telegram-doorbell.d.ts +38 -0
  824. package/dist/telegram-doorbell.d.ts.map +1 -0
  825. package/dist/telegram-doorbell.js +140 -0
  826. package/dist/telegram-doorbell.js.map +1 -0
  827. package/dist/test-handlers.d.ts +62 -0
  828. package/dist/test-handlers.d.ts.map +1 -0
  829. package/dist/test-handlers.js +240 -0
  830. package/dist/test-handlers.js.map +1 -0
  831. package/dist/testing.d.ts +10 -0
  832. package/dist/testing.d.ts.map +1 -0
  833. package/dist/testing.js +10 -0
  834. package/dist/testing.js.map +1 -0
  835. package/dist/transport-composition.d.ts +31 -0
  836. package/dist/transport-composition.d.ts.map +1 -0
  837. package/dist/transport-composition.js +55 -0
  838. package/dist/transport-composition.js.map +1 -0
  839. package/dist/transport-selector.d.ts +202 -0
  840. package/dist/transport-selector.d.ts.map +1 -0
  841. package/dist/transport-selector.js +196 -0
  842. package/dist/transport-selector.js.map +1 -0
  843. package/dist/trust-signal-pickup-listener.d.ts +43 -0
  844. package/dist/trust-signal-pickup-listener.d.ts.map +1 -0
  845. package/dist/trust-signal-pickup-listener.js +48 -0
  846. package/dist/trust-signal-pickup-listener.js.map +1 -0
  847. package/dist/trust-signal-store.d.ts +406 -0
  848. package/dist/trust-signal-store.d.ts.map +1 -0
  849. package/dist/trust-signal-store.js +939 -0
  850. package/dist/trust-signal-store.js.map +1 -0
  851. package/dist/trust-signal-sweep-tick.d.ts +60 -0
  852. package/dist/trust-signal-sweep-tick.d.ts.map +1 -0
  853. package/dist/trust-signal-sweep-tick.js +101 -0
  854. package/dist/trust-signal-sweep-tick.js.map +1 -0
  855. package/dist/trust-signal-sweep.d.ts +94 -0
  856. package/dist/trust-signal-sweep.d.ts.map +1 -0
  857. package/dist/trust-signal-sweep.js +147 -0
  858. package/dist/trust-signal-sweep.js.map +1 -0
  859. package/dist/type-registry.d.ts +42 -0
  860. package/dist/type-registry.d.ts.map +1 -0
  861. package/dist/type-registry.js +37 -0
  862. package/dist/type-registry.js.map +1 -0
  863. package/dist/types.d.ts +746 -0
  864. package/dist/types.d.ts.map +1 -0
  865. package/dist/types.js +20 -0
  866. package/dist/types.js.map +1 -0
  867. package/dist/unresolved-nodes-report.d.ts +14 -0
  868. package/dist/unresolved-nodes-report.d.ts.map +1 -0
  869. package/dist/unresolved-nodes-report.js +96 -0
  870. package/dist/unresolved-nodes-report.js.map +1 -0
  871. package/dist/vocabulary.d.ts +150 -0
  872. package/dist/vocabulary.d.ts.map +1 -0
  873. package/dist/vocabulary.js +386 -0
  874. package/dist/vocabulary.js.map +1 -0
  875. package/dist/who-label.d.ts +28 -0
  876. package/dist/who-label.d.ts.map +1 -0
  877. package/dist/who-label.js +31 -0
  878. package/dist/who-label.js.map +1 -0
  879. package/dist/who-resolver.d.ts +15 -0
  880. package/dist/who-resolver.d.ts.map +1 -0
  881. package/dist/who-resolver.js +47 -0
  882. package/dist/who-resolver.js.map +1 -0
  883. package/dist/wire-content-hash.d.ts +93 -0
  884. package/dist/wire-content-hash.d.ts.map +1 -0
  885. package/dist/wire-content-hash.js +116 -0
  886. package/dist/wire-content-hash.js.map +1 -0
  887. package/dist/withheld-content.d.ts +13 -0
  888. package/dist/withheld-content.d.ts.map +1 -0
  889. package/dist/withheld-content.js +46 -0
  890. package/dist/withheld-content.js.map +1 -0
  891. package/dist/witness-alerts.d.ts +40 -0
  892. package/dist/witness-alerts.d.ts.map +1 -0
  893. package/dist/witness-alerts.js +102 -0
  894. package/dist/witness-alerts.js.map +1 -0
  895. package/package.json +5 -5
@@ -0,0 +1,2167 @@
1
+ /**
2
+ * CELLO Daemon — TAKING A MESSAGE IN
3
+ *
4
+ * The inbound half of the content path, split out of `session-node-manager.ts` with its outbound
5
+ * counterpart in `session-content-send.ts`. Everything between a frame arriving on the wire and a
6
+ * row appearing in the transcript: the stream that carries it, the ordering record that places it,
7
+ * the hash, the salt, the authorship proof, the screening gateway, the dedupe, the size bound, and
8
+ * the ten distinct reasons a message can be refused.
9
+ *
10
+ * **Moved verbatim, comments included.** The comments here are the record of why each guard
11
+ * exists, several of them recording a defect that was reintroduced once already. They are the
12
+ * asset; they moved with the code they describe and none was summarised.
13
+ *
14
+ * `ingestReceivedContent` alone is ~1,000 lines and forty top-level statements, and it is not
15
+ * decomposed here: every guard reads locals the guards above it declared, so lifting a phase out
16
+ * needs either a state object or a signature nobody can read. Moving it intact preserves behaviour
17
+ * exactly, which is the point of this pass; taking it apart is a separate piece of work.
18
+ */
19
+ import * as lp from "it-length-prefixed";
20
+ import { decode } from "cbor-x";
21
+ import { decodeStructure1 } from "@cello-protocol/protocol-types";
22
+ import { openSessionContent } from "@cello-protocol/crypto";
23
+ import { CELLO_CONTENT_PROTOCOL_ID } from "@cello-protocol/transport";
24
+ import { GATEWAY_UNAVAILABLE, GOVERNANCE_TIMEOUT } from "@cello-protocol/gateway";
25
+ import { contentHashFor, resolveContentHashAlg } from "./wire-content-hash.js";
26
+ import { SALT_ADOPTION_LABEL_MAX } from "./session-salt-agreement.js";
27
+ import { CONTENT_ENCRYPTION_INBOUND_GUIDANCE, SESSION_CONTENT_ENCRYPTION_V1 } from "./content-encryption-status.js";
28
+ import { REFUSAL_KINDS } from "./refusal-reasons.js";
29
+ import { refuseIfSessionClosed } from "./session-closed.js";
30
+ import { triageOrphanedContent } from "./orphan-triage.js";
31
+ import { extractErrorMessage } from "./error-message.js";
32
+ import { retentionSentence } from "./quarantine-framing.js";
33
+ import { LEAF_KIND_CTRL } from "./session-relay-client.js";
34
+ import { ACK_HASH_REASONS, AUTHORSHIP_SELF_CHAIN_MISMATCH, AUTHORSHIP_SESSION_MISMATCH, CONTENT_MAX_INBOUND_STREAMS, CONTENT_STREAM_LINGER_MS, REFUSAL_MAY_STILL_ARRIVE, REFUSAL_NO_OTHER_ROUTE } from "./session-node-types.js";
35
+ // DOD-M15-DELIVERYACK-1: the acknowledgement is its own subject — signed, verified against the
36
+ // session's recorded counterparty key, and kept as evidence. It lives in its own module.
37
+ import { sendDeliveryAck, onDeliveryAck } from "./session-delivery-acks.js";
38
+ export class SessionContentIngest {
39
+ #ctx;
40
+ /**
41
+ * The outbound half — reached for exactly ONE thing, and the reason is worth stating.
42
+ *
43
+ * The counterparty's delivery acknowledgement arrives on the SAME stream this class is already
44
+ * reading, so the receiver is the only code positioned to notice it; what it settles, though, is
45
+ * a message WE sent, whose timer and parked copy belong to the sender. Rather than give both
46
+ * halves a claim on that state, the frame is handed across this one edge.
47
+ */
48
+ #send;
49
+ constructor(ctx, send) {
50
+ this.#ctx = ctx;
51
+ this.#send = send;
52
+ }
53
+ /**
54
+ * DOD-M12B-REVIVE-RELAY-1 — the relay witness leaf handler, shared by establishment and revival.
55
+ *
56
+ * Extracted because a REVIVED session must register the same handler. It was inline in
57
+ * `#connectSessionRelay`, so revival — which never called that at all — had no live inbound path:
58
+ * every message fell back to the five-minute mailbox poll, which is why a reconnected session took
59
+ * three minutes to deliver what a fresh one delivers in seconds, and why doorbells stopped firing.
60
+ *
61
+ * A revived session that behaves differently from a fresh one is the defect. This is one of the
62
+ * two halves of making them the same.
63
+ */
64
+ relayLeafHandler(agentName, sessionId, correlationId) {
65
+ return (frame) => {
66
+ // The counterparty's witnessed leaf arrived with its canonical sequence. The
67
+ // plaintext is delivered separately over the direct content stream; this is the
68
+ // ordering/witness signal. Full canonical-sequence reconciliation against the
69
+ // local tree is MSG-001-3b (J-CONTENT).
70
+ this.#ctx.logger.info("session.relay.leaf.delivered", {
71
+ sessionId,
72
+ sequenceNumber: frame.sequence_number,
73
+ leafKind: frame.leaf_kind,
74
+ correlationId,
75
+ });
76
+ // DOD-MSG-4 (strict in-order): record the relay-witnessed canonical sequence for the
77
+ // counterparty's MSG leaves. The relay is the ordering authority; structure1_cbor =
78
+ // [version, content_hash(32), sender_pubkey, session_id, last_seen_seq, ts] (+ last_seen_hash
79
+ // at index 6 on a v2 claim — 020-ACKHASH; content_hash stays at 1). The relay sequence
80
+ // is 1-based and global per session; the daemon tree is 0-based — normalize with -1. Only
81
+ // COUNTERPARTY leaves (the ones B will ingest); our own echoed leaf already lands via the
82
+ // send path. The gate (ingestReceivedContent) reads this map to hold out-of-order arrivals.
83
+ if (!frame.authored_by_us && frame.leaf_kind !== LEAF_KIND_CTRL) {
84
+ const s1 = decodeStructure1(frame.structure1_cbor);
85
+ if (s1.ok) {
86
+ if (frame.sequence_number > 0) {
87
+ this.recordWitnessedSequence(agentName, sessionId, Buffer.from(s1.fields.contentHash).toString("hex"), frame.sequence_number - 1);
88
+ }
89
+ }
90
+ else {
91
+ // `structure1Reason`, not `error` — review F6. This is a named refusal code, and putting
92
+ // it in a field called `error` reads as an exception message to anyone scanning logs.
93
+ // The old `try` here also wrapped `recordWitnessedSequence`, so a throw from THAT was
94
+ // reported as a decode failure; the decode no longer throws, and the split is deliberate.
95
+ this.#ctx.logger.warn("session.relay.leaf.witness.decode.failed", {
96
+ sessionId,
97
+ structure1Reason: s1.reason,
98
+ correlationId,
99
+ });
100
+ }
101
+ }
102
+ // M7-UPGRADE-002: auto-acknowledge close. When the COUNTERPARTY's SEAL ctrl leaf (0x02)
103
+ // arrives and B has verified the content, B's OWN node auto-co-signs the responder SEAL
104
+ // leaf — no agent prompt — so the bilateral seal completes promptly instead of degrading
105
+ // to unilateral on a slow/busy/crashed agent. Never auto-ack our OWN echoed ctrl leaf.
106
+ if (frame.leaf_kind === LEAF_KIND_CTRL && !frame.authored_by_us) {
107
+ this.#ctx.maybeAutoAcknowledgeSeal(agentName, sessionId, correlationId);
108
+ }
109
+ };
110
+ }
111
+ /**
112
+ * DOD-M12B-ABANDON-NOTIFY-1 — drive the REAL inbound content handler with one framed message and
113
+ * a claimed peer identity.
114
+ *
115
+ * The handler is registered on a live libp2p node, so without this the only way to reach its
116
+ * branches is a full two-node transport fixture — which is why the session-abandoned branch and
117
+ * its peer pinning had no coverage at all. This feeds the same function the protocol handler
118
+ * calls, including the authentication check, rather than a copy of its logic.
119
+ */
120
+ async handleContentFrameForTest(agentName, sessionId, framedBytes, remotePeerId) {
121
+ const source = {
122
+ async *[Symbol.asyncIterator]() { yield framedBytes; },
123
+ close: async () => { },
124
+ abort: () => { },
125
+ status: "closed",
126
+ };
127
+ await this.#handleContentStream(agentName, sessionId, source, remotePeerId);
128
+ }
129
+ async ingestReceivedContent(agentName, sessionId, content, contentHash, correlationId,
130
+ /**
131
+ * DOD-FRONTIER-STRAND-1 AC1: the relay-assigned canonical position for THIS message, taken from
132
+ * the verified ordering record by the caller. Passed EXPLICITLY rather than recovered from
133
+ * `#witnessedSeq`, because that map is keyed by content hash — so two byte-identical messages
134
+ * collapse in it before dedup is ever consulted, which is the whole defect. Absent when the
135
+ * session has no relay witness (relay-degraded): see the announced fallback below.
136
+ */
137
+ canonicalSeqIn,
138
+ /**
139
+ * DOD-M15-SEALWIRE-1 part B1 — the algorithm the SENDER named on the frame, verbatim.
140
+ *
141
+ * `undefined` means the frame carried no name, which is a peer that predates the field and is
142
+ * the one case we may safely assume `sha256` for. It is threaded through rather than read off
143
+ * the session, because whether a hash is salted is a fact about the FRAME and its sender, never
144
+ * about what this side happens to hold.
145
+ */
146
+ contentHashAlgIn,
147
+ /**
148
+ * DOD-M15-SEALWIRE-1 bullet 5: the VERIFIED authorship proof for this message, when the caller
149
+ * has one. The caller is the only place that has it — `#verifyAuthorshipClaim` verifies the
150
+ * signature the frame carries beside the sender's own signed bytes, against the key inside those
151
+ * bytes, and matches the signer to this session's counterparty. That result reaches here or
152
+ * nowhere.
153
+ *
154
+ * ⚠️ IT USED TO NAME `#recordFrameOrdering`, and that was accurate until
155
+ * `DOD-M15-AUTHORSHIP-ABSENT-1`: the signature arrived only inside the RELAY's Structure 2, so
156
+ * checking authorship needed a relay record. It does not now, and the old name sends a reader to
157
+ * a method that answers a different question. Rewritten, not deleted — that dependence is the
158
+ * defect the unit removed.
159
+ *
160
+ * Optional, because the PARK route ingests without it: recovered mail proves its sender by the
161
+ * mailbox envelope instead. The row records which it was, so absence is never silent.
162
+ */
163
+ verifiedAuthorship,
164
+ /**
165
+ * 024-ORPHANTRIAGE — the key whose signature VERIFIED on a frame we could not tie to a session.
166
+ *
167
+ * Read by the orphan branch below and NOWHERE ELSE. It exists because the daemon establishes,
168
+ * cryptographically, that the sender holds a private key — and then discarded that the instant
169
+ * the session lookup came back empty, leaving the operator advised to go and make contact with
170
+ * whoever sent a message for a conversation that does not exist.
171
+ *
172
+ * Absent on the park-recovery caller, which cannot reach the orphan branch at all:
173
+ * `authenticateParkedEntry` refuses `counterparty_unknown` from the same missing record first.
174
+ */
175
+ verifiedSignerUnmatched) {
176
+ // The transcript is frozen ONLY once it is COMMITTED + signed — 'sealed' or
177
+ // 'seal_interrupted_pending' (the bilateral seal commitment) — because a later FROST
178
+ // notarization attests that exact root; a late leaf would diverge from it.
179
+ //
180
+ // MSG-001-3b recovery: a merely 'interrupted' session is NOT yet committed. The
181
+ // counterparty's last message(s) may have been parked while this party was offline, so its
182
+ // local transcript is INCOMPLETE (not frozen-final). Recovering that parked content COMPLETES
183
+ // the local view to match the counterparty BEFORE the bilateral seal — it is not a resumption
184
+ // (no new activity, no re-accept) and its root was never committed. So allow 'active' AND
185
+ // 'interrupted'; reject only the two committed states.
186
+ const record = this.#ctx.queries.getSessionRecord(agentName, sessionId);
187
+ // DOD-UNREAD-1 D4a: NEVER record content you cannot attribute. With no sessions row there is
188
+ // no counterparty — the transcript has no counterparty column, so a row written here is
189
+ // unattributable forever, counted unread by getUnreadSummary, and unreadable by cello_receive
190
+ // (the phantom-session residue). The old "(No DB row = test-only path, allowed.)" fallback
191
+ // papered that in with senderPubkey="unknown". Refuse loudly instead; the content stays
192
+ // un-acked, so a live sender redelivers once the session actually exists. After D3
193
+ // (DOD-INBOUND-GUARD-1) this path is unreachable from the wire — a fail-loud assertion.
194
+ /**
195
+ * DOD-M15-REFUSEDEVIDENCE-1 — HOISTED from below the hash cross-check, so that every refusal
196
+ * above that point can retain the bytes under it. Same expression, earlier.
197
+ *
198
+ * It is the SENDER'S CLAIM at this point — nothing has checked it yet, and on a
199
+ * `content_hash_mismatch` it provably does not describe these bytes. The quarantine read
200
+ * recomputes its own hash over what was retained rather than reprinting this one.
201
+ */
202
+ const contentHashHex = Buffer.from(contentHash).toString("hex");
203
+ if (!record) {
204
+ /**
205
+ * RETAINED FIRST, because the triage below now tells the operator whether there is an artifact
206
+ * to report — and that claim has to be made after the write, never before it (023 review F3).
207
+ *
208
+ * This is the case retention matters most for. A message for a session this daemon has no
209
+ * record of is the least explicable thing that can arrive, so it is the thing an operator has
210
+ * the least other way to show anyone. There is no `sessions` row and no counterparty, so no
211
+ * tier — `#quarantineRefusedContent` bounds it at UNKNOWN and files it at a negative position,
212
+ * outside the chain it never joined.
213
+ */
214
+ const keptOrphan = this.#ctx.refusals.quarantineRefusedContent(agentName, sessionId, "session_orphaned", content, contentHashHex, { correlationId });
215
+ /**
216
+ * 024-ORPHANTRIAGE — TWO ACTIONS EXIST AND THE EVIDENCE DECIDES WHICH.
217
+ *
218
+ * The advice here used to be *"ask the counterparty to start a NEW session."* When the message
219
+ * is a stranger probing a peer id, obeying that advice is the probe succeeding: it confirms
220
+ * somebody is home and that this agent answers, from a message that was refused.
221
+ *
222
+ * All three signals are read from things the sender does not control — their signature is
223
+ * checked against the key inside their own signed bytes, "known" comes from OUR address book,
224
+ * and "ongoing" comes from OUR transcript rows rather than the sequence number they chose.
225
+ */
226
+ const evidence = this.#ctx.refusals.orphanEvidence(agentName, sessionId, verifiedSignerUnmatched);
227
+ const triage = triageOrphanedContent(evidence, retentionSentence(sessionId, keptOrphan));
228
+ /**
229
+ * BOTH SURFACES, per Invariant 2. The log is the durable forensic record and carries the
230
+ * signals structurally — this is where an investigation days later reads what was known and
231
+ * when. The notice below is the control: it is what the agent actually reads and acts on.
232
+ */
233
+ this.#ctx.logger.warn("session.content.orphaned", {
234
+ agentName, sessionId, correlationId,
235
+ signerPubkey: evidence.signerPubkeyHex ?? "(no verifiable signature)",
236
+ signatureVerified: evidence.signerPubkeyHex !== null,
237
+ // Review F6: `"not_checked"` where nothing was measured, never a `false` that reads as a
238
+ // reading. An investigator filtering this event is the only person who will ever ask.
239
+ knownContact: evidence.knownContact,
240
+ ongoingConversation: evidence.ongoingConversation,
241
+ action: triage.action,
242
+ // 023: whether the evidence the triage points at actually exists.
243
+ retained: keptOrphan !== null,
244
+ impact: triage.impact,
245
+ });
246
+ // DOD-M15-NO-SILENT-REFUSAL-1. The notice is written even though there is no session row —
247
+ // the store is keyed (agent_id, session_id) and holds no foreign key to `sessions` precisely
248
+ // so this case can be recorded. A refusal for a session that does not exist here is the one
249
+ // the operator has the least other way to learn about.
250
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, "session_orphaned", {
251
+ kind: REFUSAL_KINDS.REFUSED,
252
+ impact: triage.impact,
253
+ guidance: triage.guidance,
254
+ });
255
+ return { ok: false, reason: "session_orphaned" };
256
+ }
257
+ // The PARK-recovery route's door into the closed check. `session-closed.ts` holds the whole
258
+ // reasoning — the three statuses, why `abandoned` is one of them, and what the refusal retains.
259
+ const closed = refuseIfSessionClosed(this.#ctx, agentName, sessionId, content, contentHashHex, correlationId);
260
+ if (closed.refused)
261
+ return { ok: false, reason: "session_committed", retained: closed.retained };
262
+ /**
263
+ * DOD-M15-SEALWIRE-1 part B1 — VERIFY UNDER THE ALGORITHM THE SENDER NAMED.
264
+ *
265
+ * Three outcomes and they must stay apart, because two of them are version differences and only
266
+ * the third is evidence of tampering. Collapsing them is how a routine skew becomes a security
267
+ * incident in the operator's log, and how a real tamper gets dismissed as a skew.
268
+ */
269
+ /**
270
+ * ⚠️ `content_hash_alg` IS NOT COVERED BY ANY SIGNATURE — review F1, and it shapes both branches
271
+ * below.
272
+ *
273
+ * The sender's signature is over `structure1_cbor`, which binds `content_hash`. It does NOT bind
274
+ * the frame envelope, so this field is an unauthenticated CLAIM by whoever sent the frame. That
275
+ * is fine for choosing how to verify — a wrong choice simply fails — but it means neither branch
276
+ * may state, as fact, anything it learned only from this field.
277
+ *
278
+ * It also means both branches MUST mark the session unverifiable. Before B1 every frame that
279
+ * failed the cross-check reached `#contentDesynced`, which gates auto-co-signing and unilateral
280
+ * ratification. Returning early here would have let a sender bypass the tamper detector by
281
+ * appending one unsigned string: sign hash H, send different bytes, add an unreadable algorithm
282
+ * name, and the receiver refuses politely, records nothing, and auto-co-signs at seal time.
283
+ */
284
+ const algResolved = resolveContentHashAlg(contentHashAlgIn);
285
+ if (!algResolved.ok) {
286
+ // A NAME WE CANNOT READ. Not a legacy peer — an unreadable one. There is no value to compare
287
+ // against, so `content_hash_mismatch` here would be an exit-point label standing in for
288
+ // "their build is newer than ours" (Invariant 2). Refused by its own name instead.
289
+ this.#ctx.markContentUnverifiable(agentName, sessionId, "unverifiable");
290
+ this.#ctx.refusals.noteUnreadableAlgFrame(agentName, sessionId, contentHash, algResolved.value);
291
+ this.#ctx.logger.error("session.content.cross_check.failed", {
292
+ sessionId, correlationId,
293
+ reason: "content_hash_alg_unknown",
294
+ declaredAlg: algResolved.value,
295
+ // States only what is KNOWN. The old wording said "nothing was altered and nobody did
296
+ // anything wrong" and "Do not treat this as a security event" — both inferred from the
297
+ // unsigned field, i.e. from the attacker in the case that matters.
298
+ impact: "this message could not be verified, so it was NOT ingested and NOT shown. The algorithm name is a claim by the sender and is not covered by any signature, so it does not establish what they actually did. This session will not auto-co-sign at close.",
299
+ guidance: "Almost always their CELLO build is newer than this one: ask which version they are running, and upgrade. If they are on the SAME version as you, that explanation does not hold and the frame was malformed or crafted — do not close the session by auto-acknowledgement.",
300
+ });
301
+ // DOD-M15-REFUSEDEVIDENCE-1 — RETAINED. The algorithm name is an unsigned claim by whoever
302
+ // sent the frame, so this branch is reachable by crafting as well as by version skew, and the
303
+ // crafted case is one to be able to show someone.
304
+ this.#ctx.refusals.quarantineRefusedContent(agentName, sessionId, "content_hash_alg_unknown", content, contentHashHex, {
305
+ senderPubkeyHex: record.counterparty_pubkey ?? null, correlationId,
306
+ });
307
+ // DOD-M15-REFUSED-INBOUND-SILENT-1: the SAME strings the log just carried, to the operator.
308
+ // This reason is a version skew, so it affects every message from that counterparty — without
309
+ // this the conversation goes permanently quiet and they conclude the peer stopped replying.
310
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, "content_hash_alg_unknown", {
311
+ kind: REFUSAL_KINDS.REFUSED,
312
+ impact: "this message could not be verified, so it was NOT ingested and NOT shown. The algorithm name is a claim by the sender and is not covered by any signature, so it does not establish what they actually did. This session will not auto-co-sign at close.",
313
+ guidance: "Almost always their CELLO build is newer than this one: ask which version they are running, and upgrade. If they are on the SAME version as you, that explanation does not hold and the frame was malformed or crafted — do not close the session by auto-acknowledgement.",
314
+ });
315
+ return { ok: false, reason: "content_hash_alg_unknown" };
316
+ }
317
+ let computed;
318
+ try {
319
+ computed = contentHashFor(content, {
320
+ alg: algResolved.alg,
321
+ // The salt is OURS — the sender's frame never carries one, and could not be trusted if it
322
+ // did. A salted frame we hold no salt for throws below and is refused by name.
323
+ salt: this.#ctx.salts.getSessionSalt(agentName, sessionId),
324
+ });
325
+ }
326
+ catch (err) {
327
+ // Reached when the peer named the salted algorithm and this side holds no salt for the
328
+ // session — the agreement never completed, or its record is gone. Distinct from a mismatch
329
+ // for the same reason as above: nothing was tampered with, we simply cannot check it.
330
+ this.#ctx.markContentUnverifiable(agentName, sessionId, "unverifiable");
331
+ this.#ctx.logger.error("session.content.cross_check.failed", {
332
+ sessionId, correlationId,
333
+ reason: "content_hash_salt_unavailable",
334
+ declaredAlg: algResolved.alg,
335
+ detail: extractErrorMessage(err),
336
+ // "Nothing was altered" was the same mistake as the branch above: it is not knowable from
337
+ // here. What IS knowable is that we could not check.
338
+ impact: "this message could not be verified — the sender says it is salted and this side holds no salt for the session — so it was NOT ingested and NOT shown. This session will not auto-co-sign at close.",
339
+ // Review F6: `#getSessionSalt` returns null for THREE conditions and only one of them wants
340
+ // a close. A read failure and a corrupt row both leave us holding no salt, which is exactly
341
+ // what makes the agreement re-offer a contribution and repair itself on the next connect.
342
+ //
343
+ // The adoption refusal is the FOURTH, added with the Decision #8 guard, and it is the only
344
+ // one that does not repair: this side declined the salt permanently for this session, so
345
+ // waiting for a reconnect is exactly the wrong advice. Leaving it out of this list would
346
+ // have sent an operator to look for a read failure that is not there and never will be.
347
+ // DOD-M15-SALTSPLIT-1 review MEDIUM-3: `session.salt.discarded` is the FIFTH cause, and it
348
+ // was added by the discard without appearing in this tree. Without it an operator whose salt
349
+ // was deliberately dropped is sent to look for three events that will not be there and then
350
+ // told a fifth thing that is false — the agreement DID complete here, and was then undone on
351
+ // purpose.
352
+ guidance: "Look for session.salt.discarded first: if it is there, this side dropped its salt because the counterparty said it could never hold one, the agreement did complete and was deliberately undone, and a new session is the repair. Otherwise look for session.salt.adoption.refused: if it is there, this side declined the salt because the session had already hashed messages, that is permanent for this session, and reconnecting will NOT fix it — close the session and start a new one. Otherwise look for session.salt.read.failed or session.salt.persist.failed. If either is present the agreement re-runs on the next reconnect and this repairs itself — wait for that before doing anything. If none of the four is present, the agreement never completed with this counterparty: close the session and start a new one. In every case the transcript up to here is intact.",
353
+ });
354
+ // DOD-M15-REFUSEDEVIDENCE-1 — RETAINED. We could not check it, which is precisely why the
355
+ // bytes have to survive: the question of what they actually were stays open, and a hash we
356
+ // could not verify answers none of it.
357
+ this.#ctx.refusals.quarantineRefusedContent(agentName, sessionId, "content_hash_salt_unavailable", content, contentHashHex, {
358
+ senderPubkeyHex: record.counterparty_pubkey ?? null, correlationId,
359
+ });
360
+ // DOD-M15-REFUSED-INBOUND-SILENT-1 — and this branch needed it MORE than the two that had it.
361
+ //
362
+ // It was refused, logged with a full impact and guidance, not ingested, not shown — and the
363
+ // operator was told nothing. Twenty lines below the branches that were wired, in the same
364
+ // function, with the same shape.
365
+ //
366
+ // One of its four causes is permanent, and the guidance above says so in its own words: an
367
+ // adoption refusal means this side declined the salt for the life of the session and
368
+ // reconnecting will NOT fix it. So the failure this line exists to close — the conversation
369
+ // goes quiet, the explanation sits in a log nobody opens — was still live on the one branch
370
+ // that never repairs itself.
371
+ //
372
+ // The guidance is passed by reference to the log's own text rather than duplicated: a second
373
+ // copy is a second thing to keep true, and the log's version is the one that gets maintained.
374
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, "content_hash_salt_unavailable", {
375
+ kind: REFUSAL_KINDS.REFUSED,
376
+ impact: "this message could not be verified — the sender says it is salted and this side holds no salt for the session — so it was NOT ingested and NOT shown. This session will not auto-co-sign at close.",
377
+ guidance: "If session.salt.discarded is present, this side dropped its salt on purpose because the counterparty said it could never hold one — a new session is the repair. If this side refused the salt because the session had already hashed messages, that is PERMANENT for this session and reconnecting will not fix it — close the session and start a new one. Otherwise the salt agreement re-runs on the next reconnect and this repairs itself. Check session.salt.discarded and session.salt.adoption.refused in the log to tell which. The transcript up to here is intact either way.",
378
+ });
379
+ return { ok: false, reason: "content_hash_salt_unavailable" };
380
+ }
381
+ if (Buffer.from(computed).toString("hex") !== contentHashHex) {
382
+ this.#ctx.logger.warn("session.content.cross_check.failed", {
383
+ sessionId,
384
+ reason: "content_hash_mismatch",
385
+ // WHICH algorithm the comparison ran under. Without it, a mismatch is unfalsifiable from the
386
+ // log: an operator cannot tell "the bytes were altered" from "we checked it the wrong way".
387
+ declaredAlg: algResolved.alg,
388
+ correlationId,
389
+ });
390
+ // M7-UPGRADE-002 (SI-002): a tamper makes this session's content unverifiable — the
391
+ // auto-acknowledge gate must never auto-co-sign it. The session stays alive (DOD-MSG-7),
392
+ // but the responder seal now requires the agent's explicit decision, not an auto-ack.
393
+ this.#ctx.markContentUnverifiable(agentName, sessionId, "tampered");
394
+ /**
395
+ * DOD-M15-REFUSEDEVIDENCE-1 — RETAINED, and this is the highest-value row in the table.
396
+ *
397
+ * A tampered frame is the one case where the message and the sender's commitment PROVABLY
398
+ * disagree, and the proof only exists while both halves do. Before this, the bytes went on the
399
+ * floor and all that survived was a hash of something nobody still had.
400
+ *
401
+ * `verifiedAuthorship` is stored when the caller verified a signature over the sender's own
402
+ * bytes. That is what makes the row evidence rather than a note: the signature is checked
403
+ * against the key inside the sender's signed bytes, not against anything this side chose.
404
+ */
405
+ this.#ctx.refusals.quarantineRefusedContent(agentName, sessionId, "content_hash_mismatch", content, contentHashHex, {
406
+ senderPubkeyHex: this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId))?.counterpartyPubkey ?? record.counterparty_pubkey ?? null,
407
+ ...(verifiedAuthorship ? { authorship: verifiedAuthorship } : {}),
408
+ correlationId,
409
+ });
410
+ // DOD-M15-REFUSED-INBOUND-SILENT-1. Deliberately does NOT include the content or the hashes:
411
+ // it failed verification, and showing it is the injection path this cross-check closes.
412
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, "content_hash_mismatch", {
413
+ kind: REFUSAL_KINDS.REFUSED,
414
+ impact: "a message arrived whose bytes do not match the hash the sender committed to, so it was NOT ingested and NOT shown. This session will not auto-co-sign at close.",
415
+ guidance: "Either the message was altered in transit or the sender's record is wrong. Ask the counterparty to resend. Do not close this session by auto-acknowledgement — seal it only by an explicit decision.",
416
+ });
417
+ return { ok: false, reason: "content_hash_mismatch" };
418
+ }
419
+ const entry = this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId));
420
+ const senderPubkey = entry?.counterpartyPubkey ?? record.counterparty_pubkey;
421
+ if (!senderPubkey) {
422
+ // DOD-UNREAD-1 D4a (AC4, supersedes the MSGWAKE-1 F1 paper-in): the schema requires
423
+ // counterparty_pubkey NOT NULL, so this is unreachable unless a row was hand-crafted empty.
424
+ // Either way, "unknown" is never written to a transcript row — refuse instead.
425
+ this.#ctx.logger.warn("session.content.sender_unresolved", { sessionId, agentName, correlationId });
426
+ // DOD-M15-REFUSEDEVIDENCE-1 — RETAINED, with NO sender key, because there is none and that
427
+ // absence is the evidence. The guidance below says to report this; this is the artifact there
428
+ // is to report. Bounded at the UNKNOWN tier — there is no contact to look a tier up on, which
429
+ // is the same fact that made it unattributable.
430
+ const keptUnresolved = this.#ctx.refusals.quarantineRefusedContent(agentName, sessionId, "sender_unresolved", content, contentHashHex, { correlationId });
431
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, "sender_unresolved", {
432
+ kind: REFUSAL_KINDS.REFUSED,
433
+ impact: "A message arrived that this daemon could not attribute to anyone, so it was not delivered. This conversation's record does not say who the other party is, which a conversation opened normally always does. TREAT THIS AS HOSTILE: a message that cannot be tied to a sender is far more likely to be a probe or an attack than a fault.",
434
+ /**
435
+ * ⚠️ NO "WHEN IN DOUBT" HERE — Andre, 2026-09-03: *"This message has no sender, the chances
436
+ * that it is hostile are very high. When in doubt? No. Just report it."*
437
+ *
438
+ * That hedge belongs on the ambiguous branch in `024-ORPHANTRIAGE`, where a verified
439
+ * signature from a known contact leaves a real judgement to make. There is no judgement
440
+ * here. Softening it would teach the operator to weigh a case that does not need weighing.
441
+ *
442
+ * ⚠️ IT NAMES NO REPORTING DESTINATION, and that is still true — but HALF of the reason has
443
+ * gone, so the sentence is rewritten rather than left to read as though nothing changed.
444
+ *
445
+ * It used to rest on two facts: `CELLO_Reporting` does not exist (`DOD-M15-ORPHANTRIAGE-1`,
446
+ * still open) and **the message itself is not retained**. The second is no longer true —
447
+ * `DOD-M15-REFUSEDEVIDENCE-1` retains it, and the guidance below now says so and names where
448
+ * it is. Telling an operator to report something while keeping nothing to report was the
449
+ * gap; naming a destination nobody can reach would be Invariant 4's failure. So: the
450
+ * artifact is named now, the destination when 024 lands.
451
+ *
452
+ * ⚠️ THE ROTATION ADVICE IS MEASURED, NOT ASSUMED. `#startReceiverNode` mints the standing
453
+ * receiver's transport key with `randomBytes(32)` and never persists it, so a logout/login
454
+ * genuinely yields a NEW peer id and fresh directory connections. **And the bound is stated
455
+ * in the same breath:** session nodes DO persist their seed (`DOD-M12B-SESSION-SEED-1`, so a
456
+ * revived conversation keeps its address), so this rotates the front door and not the doors
457
+ * already open. Telling an operator to rotate without that bound would have them believe
458
+ * they had closed something they had not.
459
+ */
460
+ guidance:
461
+ // "That is the artifact to show someone" is NOT appended: it would be false on the branch
462
+ // where nothing was retained, which is the branch this sentence exists to be honest about.
463
+ "Report this. " + retentionSentence(sessionId, keptUnresolved) +
464
+ "Do not try to reply — there is no one to reply to, and answering an unattributable message is what a probe is looking for. " +
465
+ "Then rotate your address: run cello logout followed by cello login. Your standing receiver's network identity is generated fresh each time it starts and is never stored, so this gives you a new one and rebuilds your connections to the directory — anyone holding the old address is left talking to something that no longer answers. " +
466
+ "It does NOT change the addresses of conversations you already have open: those identities are kept on purpose so an interrupted conversation can resume. " +
467
+ "This conversation cannot be repaired: close it with cello_close_session, and open a new one yourself if you were expecting someone. See session.content.sender_unresolved in the daemon log.",
468
+ });
469
+ return { ok: false, reason: "sender_unresolved" };
470
+ }
471
+ // DOD-MSG-5: a content_hash satisfies AT MOST ONE Merkle leaf, exactly once. If this hash is
472
+ // already a leaf in the tree — it arrived BOTH directly and via the relay-park backstop, or it
473
+ // is a replay — do NOT append a second leaf and do NOT double-count it. The recipient already
474
+ // holds this message at its assigned sequence. (In the normal single-delivery case this find is
475
+ // -1, so the live/recover append paths are unchanged.)
476
+ // ─── DOD-FRONTIER-STRAND-1 AC1: the discriminator is the POSITION, not the content ───
477
+ //
478
+ // The old rule ("a content_hash satisfies AT MOST ONE Merkle leaf") is false whenever two
479
+ // genuinely distinct messages match byte-for-byte — and two instances of the same model,
480
+ // answering the same message with similar context, collide far more readily than humans do.
481
+ // That is what stranded session dbb93dfc... for a week: an away responder fired twice with
482
+ // identical text, the sender appended both, the receiver dropped the second as a "redelivery",
483
+ // and the two frontiers disagreed forever. No receipt was ever possible.
484
+ //
485
+ // The relay already assigns every submission a unique position: a REDELIVERY carries the same
486
+ // position, a genuinely new identical message carries a NEW one. So a duplicate is the same
487
+ // hash AT THE SAME POSITION -- never the same hash anywhere.
488
+ const tree = this.#ctx.getSessionTree(agentName, sessionId);
489
+ let existingIdx;
490
+ if (canonicalSeqIn !== undefined && canonicalSeqIn >= 0 && tree.hashAt(canonicalSeqIn) === contentHashHex) {
491
+ // The relay position holds exactly this content: a redelivery.
492
+ existingIdx = canonicalSeqIn;
493
+ }
494
+ else if (canonicalSeqIn !== undefined && canonicalSeqIn >= 0 && canonicalSeqIn >= tree.size()) {
495
+ // The position is at or beyond the frontier, so it cannot be a leaf we already hold. A
496
+ // genuinely new message — including one byte-identical to an earlier leaf, which is the whole
497
+ // point of AC1.
498
+ existingIdx = -1;
499
+ }
500
+ else if (canonicalSeqIn !== undefined && canonicalSeqIn >= 0) {
501
+ // ─── POSITION DRIFT (review F2, a regression this fix introduced and this branch repairs) ───
502
+ //
503
+ // `canonicalSeqIn < tree.size()` yet that slot holds different content, so **leaf index is no
504
+ // longer the relay position** and the position cannot be used as an index into the tree. That
505
+ // is §7a's drift: a first message whose relay submit failed is appended locally and never
506
+ // counted by the relay, leaving the local record permanently one ahead.
507
+ //
508
+ // Using the position as an index here made a TRUE REDELIVERY append a second leaf — measured:
509
+ // tree size 3 where the pre-fix code correctly gave 2. That is the "too permissive" direction,
510
+ // and it inflates this side's tree against the counterparty's: the strand, from the other end.
511
+ //
512
+ // So under drift, fall back to the content-hash rule. It is weaker — it still cannot tell two
513
+ // identical messages apart — but it is CORRECT about redelivery, which is the failure actually
514
+ // reachable here, and it is exactly the pre-existing behavior, so this is not a regression in
515
+ // either direction. Loudly announced, because the ambiguity is real and the drift is the thing
516
+ // that should be fixed (DOD-FIRSTMSG-WITNESS-1 closes the producer).
517
+ existingIdx = tree.indexOfHash(contentHashHex);
518
+ // Announce only when the fallback actually DECIDED something (it found a duplicate). When it
519
+ // finds nothing the message simply appends, `session.content.sequence_behind_tree` already
520
+ // reports the drift itself, and a second warn on every message of a drifted session would
521
+ // bury the case that matters. A signal that fires on the normal case is not a signal.
522
+ if (existingIdx >= 0)
523
+ this.#ctx.logger.warn("session.content.dedup.position_drifted", {
524
+ sessionId,
525
+ agentName,
526
+ contentHashHex,
527
+ canonicalSeq: canonicalSeqIn,
528
+ treeSize: tree.size(),
529
+ dedupedAt: existingIdx,
530
+ reason: "leaf_index_is_not_relay_position_fell_back_to_content_hash",
531
+ correlationId,
532
+ });
533
+ }
534
+ else {
535
+ // RELAY-DEGRADED: no witness, so no discriminator exists and the content-hash rule is all
536
+ // there is. Keeping it preserves today's protection against real redelivery and today's blind
537
+ // spot for identical messages -- the strand can still form on this path. Section 5a permits
538
+ // proceeding rather than refusing (losing content is worse than mis-ordering it), but only
539
+ // ANNOUNCED: a silent fallback is exactly how this went a week unnoticed. Fires only when the
540
+ // hash actually matches, so it marks a real decision rather than every unwitnessed message.
541
+ existingIdx = tree.indexOfHash(contentHashHex);
542
+ // Gated exactly as its sibling `session.content.unwitnessed` is (see :3933): a session with NO
543
+ // RELAY ATTACHED has no witness BY DESIGN, so warning there would fire on every message of a
544
+ // normal no-relay session and bury the case that means something. A signal that fires on the
545
+ // normal case is not a signal. The reason distinguishes the two shapes rather than asserting
546
+ // the relay is absent — the position can also be missing because this particular frame carried
547
+ // no ordering record while the relay is perfectly healthy.
548
+ if (existingIdx >= 0 && this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId))?.relayClient) {
549
+ this.#ctx.logger.warn("session.content.dedup.unwitnessed", {
550
+ sessionId,
551
+ agentName,
552
+ contentHashHex,
553
+ sequenceNumber: existingIdx,
554
+ reason: "no_ordering_record_deduped_on_content_hash",
555
+ correlationId,
556
+ });
557
+ }
558
+ }
559
+ if (existingIdx >= 0) {
560
+ this.#ctx.logger.info("session.content.deduplicated", {
561
+ sessionId,
562
+ contentHashHex,
563
+ sequenceNumber: existingIdx,
564
+ witnessed: canonicalSeqIn !== undefined,
565
+ correlationId,
566
+ });
567
+ // appendedCount 0 — a dedup appends NO new leaf, so a recover that re-pulls an already-ingested
568
+ // entry (e.g. after auto-recover already drained it) must not count it as a fresh recovery.
569
+ return { ok: true, leafIndex: existingIdx, sequenceNumber: existingIdx, appendedCount: 0 };
570
+ }
571
+ // M8C-ABUSE-1 (reviewer HIGH fix, D18): per-session total-size cap (anti-drip-feed) —
572
+ // "whitelisted senders bounded only by disk" (DoD), so a known contact is exempt entirely.
573
+ // MUST run BEFORE the hold-branch below — the original placement (after it) let a
574
+ // non-contact sender drip-feed unbounded bytes by making every message arrive "out of order"
575
+ // relative to the relay witness (held content skipped the cap entirely, then #releaseHeld
576
+ // appended it later with no re-check). Accounts for bytes already committed AND bytes
577
+ // currently sitting in the hold buffer (multiple held chunks could otherwise each individually
578
+ // pass the check while cumulatively exceeding it once released). Runs BEFORE the M9 screening
579
+ // seam below (cheap + synchronous — fail fast on volume before spending gateway compute on
580
+ // content headed for rejection anyway); both gates are independent and either rejects on its
581
+ // own criteria, so ordering between them does not change correctness.
582
+ {
583
+ // DOD-TIER-2 AC2: the per-session byte cap is the sender's TIER cap (DEFAULT_TIER_BOUNDS),
584
+ // applied to EVERY sender — no tier is unbounded (INV-TIER-BOUND), so a contact is no longer
585
+ // "exempt entirely". A stranger (no row → UNKNOWN) keeps the 25 MB cap; KNOWN+ get more.
586
+ const senderTier = this.#ctx.records.getTier(agentName, senderPubkey);
587
+ const cap = this.#ctx.records.resolveTierBound(agentName, senderTier, "max_bytes");
588
+ const priorTotal = this.#ctx.queries.getReceivedBytesTotal(agentName, sessionId);
589
+ const heldTotal = this.#ctx.held.getHeldBytesTotal(agentName, sessionId);
590
+ if (priorTotal + heldTotal + content.length > cap) {
591
+ this.#ctx.logger.warn("session.content.abuse_bound.session_size_exceeded", {
592
+ sessionId,
593
+ agentName,
594
+ senderPubkey,
595
+ priorTotal,
596
+ heldTotal,
597
+ incoming: content.length,
598
+ cap,
599
+ tier: senderTier,
600
+ correlationId,
601
+ });
602
+ this.#ctx.notices.noteSizeCapRefusal(agentName, sessionId, cap, senderTier);
603
+ return { ok: false, reason: "session_size_limit_exceeded" };
604
+ }
605
+ }
606
+ // M9-CORE-001: the inbound screening seam (INV-5). Screen here — after the content is proven
607
+ // authentic (hash cross-check) and confirmed not a duplicate, before it is either held for
608
+ // ordering or appended to the agent-facing buffer. This is the SINGLE inbound funnel: direct
609
+ // arrivals, recovered/parked content (daemon recover → here), and held-then-released content
610
+ // (held below, screened now, released already-screened) all pass this point. A non-allow
611
+ // verdict means the content is NOT delivered to the agent: it is not held, not buffered, and
612
+ // no leaf is appended — the message stays un-acked so the sender's TTF/park/retry redelivers
613
+ // it once the gateway is reachable again (DB-001 fail-closed: hold, never expose ungated).
614
+ // DOD-DOC-SCREEN-CLASSIFY-1: a DOCUMENT frame skips the gateway's content screen HERE, and is
615
+ // screened later on text instead of bytes. Every content step is inert or worse for one at this
616
+ // point — the sanitizer's rewrites are deliberately discarded by the funnel below (rewriting a
617
+ // signed envelope destroys it), and language/injection judge a UTF-8 decode of binary. Size stays
618
+ // bounded twice (MAX_DOCUMENT_FRAME_BYTES at classify, the gate's own cap).
619
+ //
620
+ // WHAT IS TRADED, stated plainly: the screen skipped here is fail-CLOSED (a gateway that is down
621
+ // returns a transient block, and the frame is held un-acked for redelivery). Its replacement —
622
+ // the gate's in-process rules, then the semantic screen at `document-inbound.ts` step 7a-bis —
623
+ // is fail-OPEN on that same condition, because holding document convergence hostage to an
624
+ // optional layer breaks a layer that degrades by design. That degradation is LOGGED BY NAME
625
+ // there (`document.inbound.screen.unavailable`); it is not silent, and it is not free.
626
+ //
627
+ // Logged by name so the skip is visible rather than assumed.
628
+ const isDocFrame = this.#ctx.isDocumentFrame?.(content) === true;
629
+ if (isDocFrame) {
630
+ this.#ctx.logger.info("session.content.screen.skipped_document_frame", {
631
+ sessionId,
632
+ agentName,
633
+ correlationId,
634
+ });
635
+ }
636
+ const inboundVerdict = isDocFrame
637
+ ? { disposition: "allow", content }
638
+ : await this.#ctx.securityGateway.screenInbound(content, {
639
+ direction: "inbound",
640
+ agentName,
641
+ sessionId,
642
+ correlationId,
643
+ });
644
+ // M9 terminal-vs-transient split. A TERMINAL block (inboundVerdict.terminal) is a detector
645
+ // rejecting the CONTENT itself — a confident non-allowlisted language (IN-003), a high-score
646
+ // injection (IN-002), or an oversized payload (IN-001). The identical bytes would be rejected
647
+ // identically on redelivery, so holding them un-acked would loop the sender forever. Instead a
648
+ // terminal block is `screenedOut`: it records a leaf binding the ORIGINAL content hash and is
649
+ // acknowledged (the sender stops), but is NEVER buffered for the agent (cello_receive never sees
650
+ // it). The leaf is REQUIRED, not cosmetic: the sender appended this leaf at its CANONICAL position
651
+ // on send, so a terminal block must take the SAME strict-in-order path as a delivered message —
652
+ // record the leaf at its canonical index, not in arrival order — or the two parties' hash chains
653
+ // diverge by POSITION and the bilateral seal cross-check mismatches (code-review HIGH-1). The only
654
+ // difference from a normal message is that it leafs WITHOUT buffering. A TRANSIENT block (a
655
+ // fail-closed gateway_unavailable / governance_timeout) records nothing and is not acked.
656
+ const terminalBlock = inboundVerdict.disposition === "block" && inboundVerdict.terminal === true;
657
+ if (inboundVerdict.disposition !== "allow" && inboundVerdict.disposition !== "redact" && !terminalBlock) {
658
+ // TRANSIENT block / warn HOLD (do not deliver, do not leaf, do not ack). The message stays
659
+ // un-acked so the sender's TTF/park/retry redelivers and re-screens it once the gateway recovers.
660
+ // (If we committed a leaf, dedup would later swallow the redelivery and the agent would never
661
+ // receive it.)
662
+ if (inboundVerdict.reason === GOVERNANCE_TIMEOUT) {
663
+ this.#ctx.logger.error("security.gateway.timeout", {
664
+ sessionId,
665
+ reason: inboundVerdict.reason,
666
+ correlationId,
667
+ });
668
+ }
669
+ else if (inboundVerdict.reason === GATEWAY_UNAVAILABLE) {
670
+ this.#ctx.logger.error("security.gateway.unavailable", {
671
+ direction: "inbound",
672
+ reason: inboundVerdict.reason,
673
+ correlationId,
674
+ });
675
+ }
676
+ else {
677
+ this.#ctx.logger.warn("security.gateway.inbound.blocked", {
678
+ sessionId,
679
+ disposition: inboundVerdict.disposition,
680
+ reason: inboundVerdict.reason,
681
+ correlationId,
682
+ });
683
+ }
684
+ /**
685
+ * DOD-M15-REFUSEDEVIDENCE-1 — **A TRANSIENT BLOCK RETAINS NOTHING, and nothing is lost by
686
+ * that.** Nothing was recorded and, decisively, nothing was ACKNOWLEDGED: the message is still
687
+ * with the sender, whose daemon redelivers it. When the gateway recovers the same bytes are
688
+ * screened, and if they are blocked they are retained then, under the detector's own reason.
689
+ *
690
+ * Retaining here would file a copy of a message that is coming back — a duplicate, not
691
+ * evidence — and it would do so for content nothing has yet judged, once per redelivery
692
+ * attempt, for as long as the gateway stays down.
693
+ */
694
+ // DOD-M15-NO-SILENT-REFUSAL-1 — a TRANSIENT block, and saying which it is, is the whole
695
+ // value of the notice. Nothing was recorded and nothing was acked, so the sender's daemon
696
+ // redelivers on its own. An operator who reads the silence as delivery, or who asks the
697
+ // counterparty to resend, is acting on the opposite of what happened.
698
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, inboundVerdict.reason ?? "inbound_screen_blocked", {
699
+ kind: REFUSAL_KINDS.DEFERRED,
700
+ impact: "the screener could not reach a verdict on an inbound message, so it was NOT ingested and NOT shown. Nothing was recorded and nothing was acknowledged — the message is still with the sender and their daemon will redeliver it once screening works again. Do not read this silence as delivery.",
701
+ guidance: "TRANSIENT — do not ask the counterparty to resend, and do not close the session. Get the local screening gateway healthy and the backlog comes through on its own: look for security.gateway.timeout, security.gateway.unavailable and security.gateway.inbound.blocked in the daemon log — the third is what an internal screen_error logs, and naming only the first two sends you looking for lines that will not be there. While it stays down, every message from every counterparty takes this path.",
702
+ });
703
+ return { ok: false, reason: inboundVerdict.reason ?? "inbound_screen_blocked" };
704
+ }
705
+ // Assigned only on the terminal-block branch and invoked beside each retention attempt below.
706
+ let noteTerminalBlock;
707
+ if (terminalBlock) {
708
+ this.#ctx.logger.warn("security.gateway.inbound.terminal_block", {
709
+ sessionId,
710
+ disposition: inboundVerdict.disposition,
711
+ reason: inboundVerdict.reason,
712
+ correlationId,
713
+ });
714
+ /**
715
+ * DOD-M15-REFUSEDEVIDENCE-1 — retention for a terminal block happens where its LEAF happens,
716
+ * not here. Two sites below (the hold branch and the in-order append), each writing the
717
+ * quarantine row at the same index as the leaf it accompanies.
718
+ *
719
+ * Not here, deliberately: this point is upstream of the post-screen dedup re-check and the
720
+ * size-cap re-check, either of which can still refuse. Retaining above them would file
721
+ * evidence for a message this call then reports as capped — and the cap path is the one that
722
+ * is ruled NOT to retain.
723
+ */
724
+ /**
725
+ * DOD-M15-NO-SILENT-REFUSAL-1 — **the moment the product catches the attack it exists to
726
+ * catch, and until now the operator was told nothing about it.**
727
+ *
728
+ * This path is not an error path, which is exactly why it had no notice: the block leafs the
729
+ * original content hash at its canonical position and acknowledges the sender, so nothing
730
+ * fails and nothing loops. The message is simply never handed to the agent. From the
731
+ * operator's chair a message they were expecting never arrives and the record shows a leaf
732
+ * with nothing in it.
733
+ *
734
+ * The notice NEVER carries the blocked content — a screener that can be talked into surfacing
735
+ * what it blocked is not a screener.
736
+ *
737
+ * ⚠️ **THE GUIDANCE USED TO SAY "DO NOT ASK FOR THE ORIGINAL TEXT", AND THAT IS NOW WRONG.**
738
+ * Rewritten rather than deleted, per the claim-comment rule, because the reasoning is what
739
+ * changed and not just the sentence. It rested on the content being unavailable; under
740
+ * `DOD-M15-REFUSEDEVIDENCE-1` it is retained and there is a route that returns it FRAMED. And
741
+ * the friction was never protection: Andre, 2026-09-03 — *"eventually the LLM is going to go
742
+ * searching for it, because human beings are going to direct their LLMs to find it, and it's
743
+ * going to come back and say 'Hey, I found it here, the message says…' — which is far
744
+ * worse."* Withholding the route removes the WARNING from the read, not the read.
745
+ *
746
+ * What survives unchanged: do not turn screening off. That is still the one action that makes
747
+ * things worse, and it is the one the guidance still refuses.
748
+ */
749
+ /**
750
+ * ⚠️ THE DETECTOR'S OWN REASON SURVIVES — `inbound_screen_blocked` is only the fallback.
751
+ *
752
+ * Invariant 3: a downstream handler must not replace an upstream descriptive error with a
753
+ * generic one. The verdict already says WHICH detector fired — `inbound_language_blocked` and
754
+ * an injection block are different problems with different remedies, and one of them has an
755
+ * operator command that fixes it. Flattening both to `inbound_screen_blocked` would also
756
+ * deduplicate them together, so the second kind would be silent for the life of the session.
757
+ *
758
+ * The gateway's own `guidance` is appended when it has one, for the same reason: it is the
759
+ * half that names the actual command.
760
+ */
761
+ // `?? "inbound_screen_blocked"` is a floor, not a live branch: every verdict producer in the
762
+ // tree sets `reason`, so today it never fires. It stays because `reason` is optional on the
763
+ // type, and a notice keyed on `undefined` would collapse every future detector into one row.
764
+ /**
765
+ * ⚠️ **DEFERRED UNTIL THE RETENTION HAS ACTUALLY RUN — review F3.** The notice used to be
766
+ * written here, above both append sites, and claimed the message was kept before anything had
767
+ * tried to keep it. It is now a closure invoked beside each `#quarantineRefusedContent` call,
768
+ * carrying that call's own answer.
769
+ *
770
+ * Two paths between here and there deliberately write NO notice now, and both are the better
771
+ * answer: a post-screen dedup means this exact message was already noticed the first time, and
772
+ * a size-cap refusal writes `#noteSizeCapRefusal` instead — which is what actually happened,
773
+ * where before the operator got both stories at once.
774
+ */
775
+ noteTerminalBlock = (stored) => {
776
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, inboundVerdict.reason ?? "inbound_screen_blocked", {
777
+ kind: REFUSAL_KINDS.BLOCKED,
778
+ impact: "the screener blocked an inbound message: its content matched a detector this agent runs on everything that arrives. It was NOT shown to the agent. It IS recorded in the hash chain at its position and the sender was acknowledged, so they will not resend it and they were not told it was blocked.",
779
+ guidance: "This is the protection doing its job, and nothing is required of you. If you were expecting something from this counterparty around now, tell them it was blocked and ask them to say it differently. " +
780
+ retentionSentence(sessionId, stored) +
781
+ (stored === null ? "" : "There is no reason to read it unless you need to show someone, or judge whether this was an attack. ") +
782
+ "Do NOT turn screening off to read it: that is the one action here that makes things worse. security.gateway.inbound.terminal_block in the daemon log names which detector fired." +
783
+ (inboundVerdict.guidance !== undefined ? ` The detector says: ${inboundVerdict.guidance}` : ""),
784
+ });
785
+ };
786
+ }
787
+ // M9-IN-001: a `redact` verdict (inbound sanitization) DELIVERS the sanitized text to the agent,
788
+ // while the Merkle leaf still binds the ORIGINAL content hash below — the transcript records what
789
+ // the peer actually sent; the agent sees the sanitized form. `allow` leaves the content unchanged.
790
+ // A terminal block carries the original bytes here only so its leaf binds the right hash; it is
791
+ // never delivered (the screenedOut flag below routes it to a leaf-without-buffer).
792
+ const deliverContent = inboundVerdict.disposition === "redact" && inboundVerdict.content !== undefined
793
+ ? inboundVerdict.content
794
+ : content;
795
+ // DOD-M9C-SCREENPASSIVE-1: the log says what the AGENT was told. A wrap and a removal change
796
+ // what the recipient reads, and until now neither left a trace anywhere — an operator asking
797
+ // "why did my agent see a warning on that message?" had nothing to read. `correlationId` is the
798
+ // ingest's own, so the finding, the leaf and the delivery are one story.
799
+ if (inboundVerdict.disposition === "redact") {
800
+ const findings = (inboundVerdict.events ?? [])
801
+ .filter((e) => String(e.category).startsWith("injection:"))
802
+ .map((e) => String(e.category));
803
+ const removals = (inboundVerdict.events ?? [])
804
+ .filter((e) => e.disposition === "redact")
805
+ .map((e) => String(e.category));
806
+ this.#ctx.logger.info("security.screen.inbound.annotated", {
807
+ sessionId, contentHashHex, correlationId,
808
+ findings, removals,
809
+ deliveredBytes: deliverContent.length,
810
+ originalBytes: content.length,
811
+ });
812
+ }
813
+ // screenInbound above is the ONLY suspension point in this method, and it splits the dedup check
814
+ // (indexOfHash, above) from the leaf append (below). Across that await, two concurrent ingests of
815
+ // the SAME content hash — e.g. a direct retry and a park-recovery racing on reconnect — can BOTH
816
+ // pass the first dedup check before either appends, producing two leaves for one hash
817
+ // (DOD-MSG-5 break → leafIndex≠canonicalSeq → root divergence). So re-check dedup on resume.
818
+ // Everything from here to the append is synchronous (atomic under Node's single thread): the
819
+ // first to resume appends, and the second sees its leaf and dedups.
820
+ //
821
+ // Adding any further await between here and the append reopens the window.
822
+ // DOD-FRONTIER-STRAND-1 AC1: this re-check must use the SAME discriminator as the first one.
823
+ // Left keyed on the content hash it silently re-created the whole defect one branch later --
824
+ // the pre-screen check would correctly let a second identical-but-distinct message through, and
825
+ // then this one would drop it anyway. The race it exists to close is unaffected: two concurrent
826
+ // ingests of a true redelivery share a position, so the second still sees the first's leaf.
827
+ const treeAfterScreen = this.#ctx.getSessionTree(agentName, sessionId);
828
+ const dedupAfterScreen = canonicalSeqIn !== undefined && canonicalSeqIn >= 0
829
+ ? (treeAfterScreen.hashAt(canonicalSeqIn) === contentHashHex ? canonicalSeqIn : -1)
830
+ : treeAfterScreen.indexOfHash(contentHashHex);
831
+ if (dedupAfterScreen >= 0) {
832
+ this.#ctx.logger.info("session.content.deduplicated", {
833
+ sessionId,
834
+ contentHashHex,
835
+ sequenceNumber: dedupAfterScreen,
836
+ witnessed: canonicalSeqIn !== undefined,
837
+ phase: "post_screen",
838
+ correlationId,
839
+ });
840
+ return { ok: true, leafIndex: dedupAfterScreen, sequenceNumber: dedupAfterScreen, appendedCount: 0, ...(terminalBlock ? { screenedOut: true } : {}) };
841
+ }
842
+ // M8C-ABUSE-1 (cello-unit-reviewer HIGH fix, post-M9INT-1 merge): re-check the size cap here,
843
+ // in the SAME synchronous window as the dedup re-check above. The original check (before the
844
+ // screenInbound await) used totals that can go stale: two concurrent ingests for the same
845
+ // non-contact session — e.g. a live direct arrival racing a recoverParkedFromRelay pull —
846
+ // could each independently pass the pre-await check using the SAME stale totals, then both
847
+ // append/hold, jointly exceeding the cap. Symmetric to the dedup fix: everything from here to
848
+ // the append/hold branch is synchronous, so whichever call resumes first appends/holds before
849
+ // the second's re-check runs, and the second's freshly-recomputed totals correctly include the
850
+ // first's contribution.
851
+ {
852
+ // DOD-TIER-2 AC2 (re-check): the SAME tier cap as the primary gate above, recomputed in this
853
+ // synchronous window (the totals can go stale across the screenInbound await). Applied to EVERY
854
+ // sender — a contact is no longer exempt (INV-TIER-BOUND). Must mirror the primary gate exactly
855
+ // so a sender can never pass one and fail the other.
856
+ const senderTier = this.#ctx.records.getTier(agentName, senderPubkey);
857
+ const cap = this.#ctx.records.resolveTierBound(agentName, senderTier, "max_bytes");
858
+ const priorTotal = this.#ctx.queries.getReceivedBytesTotal(agentName, sessionId);
859
+ const heldTotal = this.#ctx.held.getHeldBytesTotal(agentName, sessionId);
860
+ if (priorTotal + heldTotal + content.length > cap) {
861
+ this.#ctx.logger.warn("session.content.abuse_bound.session_size_exceeded", {
862
+ sessionId,
863
+ agentName,
864
+ senderPubkey,
865
+ priorTotal,
866
+ heldTotal,
867
+ incoming: content.length,
868
+ cap,
869
+ tier: senderTier,
870
+ correlationId,
871
+ recheck: true,
872
+ });
873
+ this.#ctx.notices.noteSizeCapRefusal(agentName, sessionId, cap, senderTier);
874
+ return { ok: false, reason: "session_size_limit_exceeded" };
875
+ }
876
+ }
877
+ // DOD-MSG-4 (strict in-order gate): the RELAY is the ordering authority. If B holds the
878
+ // canonical sequence for this hash (witnessed via leaf_deliver) and it is AHEAD of the next
879
+ // expected leaf, HOLD the content rather than append it out of order. The missing in-between
880
+ // sequence(s) are recovered from the relay mailbox; #releaseHeld then drains the held entries
881
+ // in canonical order. This keeps the daemon-owned leaf index === the canonical sequence by
882
+ // construction, so two parties' roots match even when direct delivery and park-recovery
883
+ // interleave. With NO witness for this hash (relay-degraded) B falls back to arrival-order
884
+ // append — the pre-MSG-4 behavior (no ordering signal available).
885
+ const key = this.#ctx.sessionKey(agentName, sessionId);
886
+ // Prefer the position the CALLER verified for this specific message over the hash-keyed map.
887
+ // The map cannot distinguish two identical messages (AC1) -- it holds one entry per hash, so the
888
+ // second firing overwrites the first's position. The explicit value is per-message and correct;
889
+ // the map remains the fallback for paths that have no ordering record.
890
+ const canonicalSeq = canonicalSeqIn !== undefined && canonicalSeqIn >= 0
891
+ ? canonicalSeqIn
892
+ : this.#ctx.witnessedSeq.get(key)?.get(contentHashHex);
893
+ const nextExpected = this.#ctx.getSessionTree(agentName, sessionId).size();
894
+ if (canonicalSeq !== undefined && canonicalSeq > nextExpected) {
895
+ this.#ctx.held.ensureHeldRestored(agentName, sessionId);
896
+ let held = this.#ctx.heldContent.get(key);
897
+ if (!held) {
898
+ held = new Map();
899
+ this.#ctx.heldContent.set(key, held);
900
+ }
901
+ // A terminal block out of canonical order is held WITHOUT delivery (screenedOut): #releaseHeld
902
+ // leafs it at its canonical index when the gap fills, but never buffers it for the agent. This
903
+ // keeps leafIndex === canonicalSeq for screened-out content too (code-review HIGH-1).
904
+ // THE PEER'S RAW BYTES RIDE ALONG. Classification (document frame vs conversation) reads
905
+ // byte 0, and `deliverContent` is the SCREENED copy — for a CBOR frame that is no longer a
906
+ // map header, so a held document frame was released into the CONVERSATION path: transcript,
907
+ // doorbell, and `cello_receive` handing an agent raw CBOR as though a person typed it.
908
+ // The in-order path has always passed these bytes; only the held path dropped them.
909
+ held.set(canonicalSeq, { content: deliverContent, originalContent: content, contentHashHex, correlationId, ...(terminalBlock ? { screenedOut: true } : {}) });
910
+ // DOD-M12B-STRAND-1: and to disk, before we answer. The in-memory Map is the working copy;
911
+ // this row is the one that survives the teardown that used to destroy it.
912
+ this.#ctx.queries.persistHeldContent(agentName, sessionId, canonicalSeq, deliverContent, content, contentHashHex, terminalBlock === true, correlationId);
913
+ // DOD-M15-REFUSEDEVIDENCE-1 (site 1 of 2 for a terminal block): a block held behind an
914
+ // ordering gap. `#releaseHeld` appends its leaf later WITHOUT re-entering this method, so
915
+ // retaining at release is not available — it is retained here, at the position the leaf will
916
+ // take. `held_content` is not a substitute: that row is deleted the moment the gap fills.
917
+ if (terminalBlock) {
918
+ const keptHeld = this.#ctx.refusals.quarantineRefusedContent(agentName, sessionId, inboundVerdict.reason ?? "inbound_screen_blocked", content, contentHashHex, {
919
+ senderPubkeyHex: senderPubkey, canonicalSeq,
920
+ ...(verifiedAuthorship ? { authorship: verifiedAuthorship } : {}),
921
+ correlationId,
922
+ });
923
+ noteTerminalBlock?.(keptHeld);
924
+ }
925
+ this.#ctx.logger.info("session.content.held", {
926
+ sessionId,
927
+ canonicalSeq,
928
+ nextExpected,
929
+ gap: canonicalSeq - nextExpected,
930
+ screenedOut: terminalBlock,
931
+ correlationId,
932
+ });
933
+ // Held content is NOT yet a durable leaf, so it is deliberately NOT acknowledged `persisted`
934
+ // (the caller checks `held`). The sender's TTF→park backstop and the recover/dedup path
935
+ // guarantee eventual delivery; B never claims persisted for content it only holds in memory.
936
+ return { ok: true, leafIndex: canonicalSeq, sequenceNumber: canonicalSeq, held: true, ...(terminalBlock ? { screenedOut: true } : {}) };
937
+ }
938
+ if (canonicalSeq !== undefined && canonicalSeq < nextExpected) {
939
+ // Contradiction (review finding #2): the witness says this hash belongs BEHIND the current
940
+ // tree, yet the dedup scan above found no existing leaf for it — so it is neither a duplicate
941
+ // nor in canonical order. This is only reachable via the accepted content-before-witness /
942
+ // relay-degraded interleaving (the next sub-increment's pending-witness buffer closes it). Log
943
+ // it loudly (the leaf-index===sequence invariant is at risk) and append rather than DROP the
944
+ // message — losing content is worse than a transient mis-order the seal cross-check will catch.
945
+ this.#ctx.logger.warn("session.content.sequence_behind_tree", {
946
+ sessionId,
947
+ canonicalSeq,
948
+ nextExpected,
949
+ correlationId,
950
+ });
951
+ }
952
+ // In-order append. A terminal block leafs the ORIGINAL content hash WITHOUT buffering it for the
953
+ // agent (screenedOut); a delivered message buffers + leafs via #appendVerifiedContent.
954
+ const leafIndex = terminalBlock
955
+ ? this.#ctx.appendSessionLeaf(agentName, sessionId, "msg", contentHashHex, correlationId).leafIndex
956
+ : this.appendVerifiedContent(agentName, sessionId, deliverContent, contentHashHex, senderPubkey, correlationId, content, verifiedAuthorship).leafIndex;
957
+ /**
958
+ * DOD-M15-REFUSEDEVIDENCE-1 (site 2 of 2) — **the moment the product catches the attack it
959
+ * exists to catch, and until now it kept only the hash.**
960
+ *
961
+ * The terminal-block branch above takes `appendSessionLeaf`, not `#appendVerifiedContent`, so
962
+ * the row carrying the plaintext, the sender's key and the sender's signature was never written.
963
+ * A hash proves a message you still hold has not changed; it proves nothing about one you threw
964
+ * away — and this is precisely the message an operator would most want to produce.
965
+ *
966
+ * At `leafIndex`, so the leaf and the evidence describe one event and DoD 7's leaf placement is
967
+ * untouched. The ORIGINAL bytes, never the sanitized `deliverContent`: evidence is what they
968
+ * sent, not what a filter made of it.
969
+ */
970
+ if (terminalBlock) {
971
+ const keptBlocked = this.#ctx.refusals.quarantineRefusedContent(agentName, sessionId, inboundVerdict.reason ?? "inbound_screen_blocked", content, contentHashHex, {
972
+ senderPubkeyHex: senderPubkey, canonicalSeq: leafIndex,
973
+ ...(verifiedAuthorship ? { authorship: verifiedAuthorship } : {}),
974
+ correlationId,
975
+ });
976
+ noteTerminalBlock?.(keptBlocked);
977
+ /**
978
+ * ⚠️ **DROP THE WITNESS — A BLOCKED MESSAGE MADE THE SESSION PERMANENTLY UNSEALABLE.**
979
+ *
980
+ * THE THIRD INSTANCE of the shape already fixed for document frames at `:10593`, found by the
981
+ * first journey that ever sealed a session after a screener block.
982
+ *
983
+ * `sealReadiness` derives `missingLeaves` from `#witnessedSeq.size` — every position the
984
+ * ordering authority committed that this tree has not appended. The entry is dropped where the
985
+ * leaf is credited, and that drop lives inside `#appendVerifiedContent`. A terminal block does
986
+ * not go through it: the branch above takes `appendSessionLeaf` directly, so the leaf WAS
987
+ * committed and the witness was never retired.
988
+ *
989
+ * **From the operator's chair:** their screener catches one hostile message, and from that
990
+ * moment `cello_close_session` answers `session_incomplete` forever — *"waiting on an earlier
991
+ * message from the counterparty that has not arrived"* — about a message that arrived, was
992
+ * judged, and is sitting in the chain. The only exit is a force-abandon, which forfeits the
993
+ * notarized receipt. Measured live: `treeSize 3, highWaterSeq 2, missingLeaves 1`.
994
+ *
995
+ * Not introduced by `DOD-M15-REFUSEDEVIDENCE-1` — it is older than this unit and simply had no
996
+ * test that both blocked a message and then sealed. It is fixed here because this unit's own
997
+ * DoD requires that session to seal.
998
+ */
999
+ this.#ctx.witnessedSeq.get(key)?.delete(contentHashHex);
1000
+ }
1001
+ // DOD-COATTEND-1 (review F2): the plaintext failed to reach the transcript, and since Tier 1 the
1002
+ // transcript IS the delivery path — so this message can never be handed to any session. Report
1003
+ // the ingest as failed. Reporting `ok: true` here is what let a local SQLCipher failure surface,
1004
+ // 30 seconds later and one subsystem away, as "no content arrived — keep waiting": the operator
1005
+ // is sent to debug a counterparty who did nothing wrong.
1006
+ //
1007
+ // The leaf STAYS. It is genuinely committed to the hash chain, and unwinding a committed leaf to
1008
+ // tidy up a reporting problem would corrupt the frontier the counterparty already co-signs
1009
+ // against. The hole is now crossable by delivery (F1), so it costs a gap, not a stall.
1010
+ if (!terminalBlock && this.getUndeliverableSeqs(agentName, sessionId).includes(leafIndex)) {
1011
+ /**
1012
+ * DOD-M15-REFUSEDEVIDENCE-1 — **THIS PATH CANNOT RETAIN, because the storage layer is what
1013
+ * just failed.** The write that would keep the evidence is the same `INSERT` into the same
1014
+ * table that has already thrown for this message. Attempting it produces a second error line
1015
+ * and no evidence. Named here rather than left to be rediscovered as a missing case.
1016
+ */
1017
+ // DOD-M15-NO-SILENT-REFUSAL-1. `#appendVerifiedContent` already noted `content_undeliverable`
1018
+ // at the point the write failed; this is the INGEST's own refusal, and it is a different fact
1019
+ // — the sender is told the ingest failed, so it will redeliver, and every redelivery of the
1020
+ // same hash now dedups against a leaf whose plaintext is not there. Two reasons, because a
1021
+ // reader fixing the disk fault needs to know both that the text is gone and that the sender
1022
+ // is retrying into a hole.
1023
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, "transcript_write_failed", {
1024
+ kind: REFUSAL_KINDS.LOST,
1025
+ impact: "A message reached this agent, was verified, and was committed to the conversation's record — and then its text could not be written to local storage, so it can never be delivered. There is a permanent gap in your copy of this conversation. This is a fault on THIS machine; the counterparty did nothing wrong and cannot fix it.",
1026
+ /**
1027
+ * ⚠️ THE READER IS USUALLY ALREADY IN A CODING AGENT, so the guidance says GO AND LOOK
1028
+ * rather than listing symptoms. Andre, 2026-09-03: *"The message should mention to try and
1029
+ * figure out why you cannot store it — it is likely a local machine problem. But if you
1030
+ * truly cannot figure this out using a coding agent, then we advise reaching out to
1031
+ * CELLO_Support."*
1032
+ *
1033
+ * That ordering matters: this is a machine fault with an ordinary cause, and an operator
1034
+ * sent straight to support for a full disk has been wasted. Support is the exit, not the
1035
+ * first step.
1036
+ */
1037
+ guidance: "Find out why the write failed — this is almost always something ordinary on this machine. " +
1038
+ "If you are reading this through a coding agent, have it check: free disk space, the permissions on ~/.cello, whether the database file is readable and writable, and transcript.message.record.failed in the daemon log, which carries the underlying error. " +
1039
+ "Waiting cannot recover the message. Once the fault is fixed, ask them to resend — the text is gone and only its hash remains. " +
1040
+ "If you genuinely cannot work out the cause, reach out to CELLO_Support.",
1041
+ });
1042
+ return { ok: false, reason: "transcript_write_failed" };
1043
+ }
1044
+ // NO relay witness for this hash. We appended it anyway — refusing would make the relay a hard
1045
+ // precondition for reading mail, so a relay outage would render the inbox unreadable, and the
1046
+ // direct path and park backstop exist precisely to survive that. But this append is a WEAKER
1047
+ // guarantee and must not masquerade as the stronger one: with a witness, the received content is
1048
+ // checked against a hash the sender committed to a third party; without one, the only available
1049
+ // hash rode in the same frame as the content, so the check is the sender's claim against the
1050
+ // sender's own claim. Say so. A sender who simply never submits to the relay is otherwise
1051
+ // indistinguishable from one the relay merely has not witnessed YET.
1052
+ // The relay witness is an INDEPENDENT attestation: a (content_hash → sequence) binding derived
1053
+ // from the sender's own signed leaf. Holding one, we check received content against a hash the
1054
+ // sender committed to a THIRD PARTY. Holding none, the only hash available rode in the same frame
1055
+ // as the content — the sender's claim checked against the sender's claim.
1056
+ //
1057
+ // Unwitnessed content is still ingested. Refusing it would make the relay a precondition for
1058
+ // READING mail, so a relay outage would render the inbox unreadable — the redundancy the direct
1059
+ // path and the park backstop exist to provide.
1060
+ //
1061
+ // Warn ONLY when a witness was EXPECTED. A session with no relay attached has no witness BY
1062
+ // DESIGN, and warning on every message there would bury the one case that means something —
1063
+ // a relay IS attached, so the sender's leaf should have been submitted and witnessed, and it
1064
+ // was not. A signal that fires on the normal case is not a signal.
1065
+ if (canonicalSeq === undefined && this.#ctx.activeNodes.get(key)?.relayClient) {
1066
+ this.#ctx.logger.warn("session.content.unwitnessed", {
1067
+ agentName,
1068
+ sessionId,
1069
+ leafIndex,
1070
+ contentHash: contentHashHex,
1071
+ correlationId,
1072
+ guidance: "a relay is attached to this session but no witness bound this content hash — it was ingested with no independent commitment from the sender",
1073
+ });
1074
+ }
1075
+ // A just-appended leaf may unblock held out-of-order arrivals whose turn is now next.
1076
+ // appendedCount = this leaf + any held leaves released by it, so a caller (recover) can tally the
1077
+ // leaves ACTUALLY written, not just the directly-ingested one (review #3).
1078
+ /**
1079
+ * ─── THE SENDER'S SELF CHAIN ADVANCES HERE, WHERE EVERY ROUTE PASSES ─────────────────────────
1080
+ *
1081
+ * `noteReceivedFromCounterparty` records "the last message we accepted from them", which is what
1082
+ * `#verifySenderSelfChain` compares their NEXT message's `prevOwnHash` against.
1083
+ *
1084
+ * It used to be called by the live content-frame caller only, so a message recovered from the
1085
+ * relay mailbox was ingested without advancing it. Measured live 2026-09-16: a machine came back
1086
+ * from a blackout, took both parked messages, and the SECOND one named the first as its
1087
+ * predecessor — which this side had just accepted but not recorded. The chain check called it a
1088
+ * predecessor we do not hold, the ordering record was thrown away, and the seal answer told the
1089
+ * operator their copy did not match a seal that was correct.
1090
+ *
1091
+ * Here it is on the one path every route ends at, and only for a leaf actually appended: a HELD
1092
+ * frame returns earlier (it is not accepted yet), and so does a DEDUP hit — which is also a fix,
1093
+ * because advancing on a duplicate would rewind the head to an older message when a late copy
1094
+ * arrives. Both callers verify the chain BEFORE they call this, so the comparison still happens
1095
+ * against the previous message and never against this one.
1096
+ */
1097
+ this.#ctx.authorship.noteReceivedFromCounterparty(agentName, sessionId, contentHash);
1098
+ const released = this.#ctx.held.releaseHeld(agentName, sessionId, senderPubkey);
1099
+ return { ok: true, leafIndex, sequenceNumber: leafIndex, appendedCount: 1 + released, ...(terminalBlock ? { screenedOut: true } : {}) };
1100
+ }
1101
+ /**
1102
+ * DOD-MSG-4: record the relay-witnessed canonical sequence for a content hash. The relay is the
1103
+ * ordering authority (Structure 2): it assigns each message a sequence from its hash and delivers
1104
+ * B the (content_hash -> sequence) binding via leaf_deliver. The strict-in-order gate orders the
1105
+ * transcript by THIS — never a sender-stamped field. Also advances the per-session high-water mark
1106
+ * (the largest witnessed sequence) reserved for the future catch-up-before-live increment. Idempotent.
1107
+ */
1108
+ recordWitnessedSequence(agentName, sessionId, contentHashHex, sequenceNumber) {
1109
+ if (sequenceNumber < 0)
1110
+ return;
1111
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1112
+ // DOD-M12B-SEAL-STUCK-1: this process has now seen this session's ordering state, so an empty
1113
+ // witness map for it means "no gap" rather than "never looked".
1114
+ this.#ctx.orderingObserved.add(key);
1115
+ const hw = this.#ctx.highWaterSeq.get(key) ?? -1;
1116
+ if (sequenceNumber > hw)
1117
+ this.#ctx.highWaterSeq.set(key, sequenceNumber);
1118
+ // ALREADY PLACED: the message beat the relay's copy on the direct path. The entry is retired
1119
+ // when a leaf is appended, so recording it now would never be retired, and every close would
1120
+ // answer `session_incomplete` until force-abandon — live on `0a534851…`, 2026-09-13.
1121
+ if (this.#ctx.getSessionTree(agentName, sessionId).hashAt(sequenceNumber) === contentHashHex)
1122
+ return;
1123
+ let map = this.#ctx.witnessedSeq.get(key);
1124
+ if (!map) {
1125
+ map = new Map();
1126
+ this.#ctx.witnessedSeq.set(key, map);
1127
+ }
1128
+ map.set(contentHashHex, sequenceNumber);
1129
+ /**
1130
+ * DOD-M12B-LEAF-TRIGGERS-FETCH-1 — A LEAF WE CANNOT READ IS A FETCH ORDER.
1131
+ *
1132
+ * MEASURED LIVE 2026-08-18: the relay delivered this leaf one second after the counterparty
1133
+ * sent. We had the hash and the sequence, the bytes were parked at that same relay, and the
1134
+ * plaintext arrived 102 seconds later on a background sweep. Nothing connected the two facts —
1135
+ * this method recorded the sequence and stopped.
1136
+ *
1137
+ * The witness leaf and the plaintext are separate deliveries: the leaf comes over the relay, the
1138
+ * bytes over the direct content stream. After an interruption the two session nodes have no
1139
+ * direct connection, so the bytes go to the park instead and only a timer ever finds them.
1140
+ *
1141
+ * The grace window is what keeps this off the hot path. On a healthy session the direct content
1142
+ * lands within milliseconds of its leaf, so fetching immediately would mean a relay round trip
1143
+ * for every message in every session. We give the direct path its two seconds first.
1144
+ */
1145
+ this.#scheduleLeafFetchIfUnresolved(agentName, sessionId, contentHashHex);
1146
+ }
1147
+ /**
1148
+ * Cancel a pending leaf fetch for one piece of content. The TIMERS stay the manager's — it has
1149
+ * three users and only one of them moved — so `#markContentTerminallyRefused`, now in
1150
+ * `inbound-refusals.ts`, asks for the cancellation instead of reaching into the map.
1151
+ */
1152
+ cancelLeafFetch(key, contentHashHex) {
1153
+ const timerKey = `${key}::${contentHashHex}`;
1154
+ const t = this.#ctx.leafFetchTimers.get(timerKey);
1155
+ if (t !== undefined) {
1156
+ clearTimeout(t);
1157
+ this.#ctx.leafFetchTimers.delete(timerKey);
1158
+ }
1159
+ }
1160
+ /** DOD-M12B-LEAF-TRIGGERS-FETCH-1: this content is here — no fetch is owed for it, and any
1161
+ * pending one is cancelled. Called wherever content actually lands. */
1162
+ markContentResolved(agentName, sessionId, contentHashHex) {
1163
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1164
+ let set = this.#ctx.resolvedContent.get(key);
1165
+ if (!set) {
1166
+ set = new Set();
1167
+ this.#ctx.resolvedContent.set(key, set);
1168
+ }
1169
+ set.add(contentHashHex);
1170
+ const timerKey = `${key}::${contentHashHex}`;
1171
+ const t = this.#ctx.leafFetchTimers.get(timerKey);
1172
+ if (t !== undefined) {
1173
+ clearTimeout(t);
1174
+ this.#ctx.leafFetchTimers.delete(timerKey);
1175
+ }
1176
+ }
1177
+ #scheduleLeafFetchIfUnresolved(agentName, sessionId, contentHashHex) {
1178
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1179
+ if (this.#ctx.resolvedContent.get(key)?.has(contentHashHex))
1180
+ return;
1181
+ // DOD-M15-REFUSALTERMINAL-1: a refusal nothing can get past is the end of the work, not a
1182
+ // reason to come back in two seconds.
1183
+ if (this.#ctx.refusals.isTerminallyRefused(agentName, sessionId, contentHashHex))
1184
+ return;
1185
+ const timerKey = `${key}::${contentHashHex}`;
1186
+ // ONE fetch per content hash. The relay redelivers, and a redelivery carries the same sequence —
1187
+ // scheduling per redelivery turns a slow relay into a storm against itself.
1188
+ if (this.#ctx.leafFetchTimers.has(timerKey))
1189
+ return;
1190
+ const timer = setTimeout(() => {
1191
+ this.#ctx.leafFetchTimers.delete(timerKey);
1192
+ if (this.#ctx.resolvedContent.get(key)?.has(contentHashHex))
1193
+ return; // the direct path won
1194
+ if (this.#ctx.shuttingDown)
1195
+ return;
1196
+ this.#ctx.logger.info("session.content.leaf_unresolved.fetch", {
1197
+ agentName,
1198
+ sessionId,
1199
+ contentHash: contentHashHex,
1200
+ graceMs: this.#ctx.leafFetchGraceMs,
1201
+ impact: "the relay told us this message exists and its plaintext never arrived directly — "
1202
+ + "fetching it now instead of waiting for the periodic sweep",
1203
+ });
1204
+ this.#ctx.park.fireParkedDrain(agentName, "witnessed_leaf_unresolved");
1205
+ }, this.#ctx.leafFetchGraceMs);
1206
+ timer.unref?.();
1207
+ this.#ctx.leafFetchTimers.set(timerKey, timer);
1208
+ }
1209
+ /**
1210
+ * DOD-MSG-4: the relay's high-water canonical sequence for this session (largest witnessed leaf),
1211
+ * or -1 if none. The relay is the ordering authority, so this is the outside view of how far the
1212
+ * session has actually progressed — which is why it is the right input to a catch-up-before-live
1213
+ * gate. Consumed by `sealReadiness` (M12-P14) for REPORTING only: the missing-leaf decision is made
1214
+ * from `#witnessedSeq`, because this counts the relay's sequence space (which includes ctrl leaves)
1215
+ * and the tree does not. Maintained by `recordWitnessedSequence`.
1216
+ */
1217
+ /**
1218
+ * DOD-COATTEND-1 (review F2): leaf sequences whose plaintext failed to reach the transcript and
1219
+ * are therefore undeliverable. Empty is the overwhelmingly normal case.
1220
+ */
1221
+ getUndeliverableSeqs(agentName, sessionId) {
1222
+ return [...(this.#ctx.undeliverableSeqs.get(this.#ctx.sessionKey(agentName, sessionId)) ?? [])];
1223
+ }
1224
+ /** DOD-MSG-4 / DAEMON-004: append a verified message leaf and buffer it for cello_receive. */
1225
+ appendVerifiedContent(agentName, sessionId, content, contentHashHex, senderPubkey, correlationId,
1226
+ /**
1227
+ * The bytes as the PEER SENT THEM, before inbound sanitization — for the document classifier
1228
+ * only. Defaults to `content` for callers that never screened (the held-release path).
1229
+ *
1230
+ * A `redact` verdict rewrites `content` for the agent's benefit, and that is right for
1231
+ * conversation: the operator sees the sanitized form while the leaf still binds the original.
1232
+ * It is WRONG for a document frame, and not marginally. Rewriting bytes inside a signed CBOR
1233
+ * envelope does not sanitize it — it destroys it. The frame stops decoding, stops being
1234
+ * recognised as document traffic at all, and falls through to the conversation path, where it
1235
+ * is recorded as something a person said and handed to the agent by `cello_receive`.
1236
+ *
1237
+ * Measured live: roughly half of proposals vanished this way. Intermittent because a proposal
1238
+ * carries a random 16-byte nonce, so whether its bytes trip a sanitizer rule varies per run —
1239
+ * which is why it read as flakiness rather than as a rule firing.
1240
+ *
1241
+ * Documents are NOT unscreened as a result. They are screened by `DocumentGate`, which is built
1242
+ * for them and REFUSES rather than mutates (§16.7) — because mutating one party's replica of a
1243
+ * CRDT is not a false positive, it is permanent divergence that both sides converge on and
1244
+ * neither can see.
1245
+ */
1246
+ originalContent,
1247
+ /**
1248
+ * DOD-M15-SEALWIRE-1 bullet 5: threaded from `ingestReceivedContent`, which is the only place
1249
+ * that has it — `#verifyAuthorshipClaim` verified this signature (carried on the frame beside
1250
+ * the bytes it signs) against the pubkey inside those bytes, and matched the signer to this
1251
+ * session's counterparty. It reaches the transcript row from here or not at all.
1252
+ *
1253
+ * ⚠️ IT USED TO NAME `#recordFrameOrdering`, true until `DOD-M15-AUTHORSHIP-ABSENT-1` moved the
1254
+ * check off the relay's record and onto the frame's own signature. Rewritten rather than
1255
+ * deleted: the old name is the evidence of what authorship used to depend on.
1256
+ *
1257
+ * Undefined on the held-release and soft-fallback paths; the row records that as
1258
+ * `local_session_state` rather than leaving it indistinguishable from a proven one.
1259
+ */
1260
+ verifiedAuthorship) {
1261
+ // M14 / DOD-DOC-INBOUND-2 — DOCUMENT FRAMES DIVERGE HERE, and the three-way split is the whole
1262
+ // contract:
1263
+ //
1264
+ // LEAF yes, and as `doc` (0x04) rather than `msg` (0x00). The seal covers document traffic
1265
+ // — that is what makes the exchange provable — but it is not conversation, and the
1266
+ // leaf kind is what a verifier renders it by.
1267
+ // TRANSCRIPT no. Recording CRDT bytes as a received message puts them in the operator's
1268
+ // conversation history, where `cello_receive` hands them to an agent as something a
1269
+ // person said.
1270
+ // DOORBELL no (§11.3). A collaborator typing produces a stream of updates; a doorbell each
1271
+ // time would interrupt the operator's agent continuously for something with no
1272
+ // deadline.
1273
+ //
1274
+ // The hook is injected and absent by default, so a daemon without the document layer behaves
1275
+ // exactly as before — this cannot change the conversation path by being unwired.
1276
+ const routed = this.#ctx.onDocumentFrame?.(agentName, sessionId,
1277
+ // THE PEER'S BYTES, not the sanitized ones. See `originalContent` above.
1278
+ originalContent ?? content, senderPubkey, correlationId);
1279
+ if (routed?.consumed === true) {
1280
+ const { leafIndex } = this.#ctx.appendSessionLeaf(agentName, sessionId, "doc", contentHashHex, correlationId);
1281
+ // DROP THE WITNESS, exactly as the conversation branch does once its leaf is appended. The
1282
+ // witness has done its ordering job either way — the leaf IS committed here.
1283
+ //
1284
+ // This branch returns early and so never reached that cleanup, and every inbound document
1285
+ // frame left a permanent entry behind. Harmless until `sealReadiness` started deriving
1286
+ // `missingLeaves` from the size of that map (M12-P14): from then on a session that carried
1287
+ // ANY document traffic could never seal, because the ordering authority was recorded as
1288
+ // having committed leaves this tree had — but had not been credited with. The refusal is
1289
+ // `session_incomplete`, whose only escape is a force-abandon with no notarized receipt.
1290
+ //
1291
+ // Two correct changes, each fine alone, that break where they meet. Caught by running the
1292
+ // live enforcers straight after merging main rather than trusting a green unit suite.
1293
+ this.#ctx.witnessedSeq.get(this.#ctx.sessionKey(agentName, sessionId))?.delete(contentHashHex);
1294
+ /**
1295
+ * ⚠️ THIS LINE USED TO LOG `ok: routed.ok` AND `reason: routed.reason`, AND NEITHER CAN EVER
1296
+ * BE PRESENT HERE. Removed rather than left, because their absence was read as evidence.
1297
+ *
1298
+ * The producer is `DocumentFrameRouter.routeSync`, and it has four returns — `unshaped`,
1299
+ * `undecodable`, `owner_unresolved`, and the normal path — **none of which sets either
1300
+ * field.** It cannot: the normal path is `void this.#enqueue(...)`, fire-and-forget, so at the
1301
+ * instant this line is written the frame has been CLASSIFIED and QUEUED and nothing has yet
1302
+ * decided whether it will be accepted. The verdict is genuinely not knowable here.
1303
+ *
1304
+ * **What that cost:** `j-stale-session` reported `framesReceived=3 inbound=0`, and the
1305
+ * investigation recorded that `ok` and `reason` were "ABSENT from every line in the run — so
1306
+ * the router returned neither, which is itself the next thread to pull: a routing result that
1307
+ * reports no outcome cannot say whether it accepted or dropped the frame." That thread leads
1308
+ * nowhere. The router did not fail to report an outcome; **it has no outcome to report at this
1309
+ * point in the flow**, and a JSON logger omits an `undefined` field, so a structural absence
1310
+ * looked exactly like a fault. A field that can never be populated is worse than no field.
1311
+ *
1312
+ * **Where the verdict actually lands**, named here so the next reader does not have to find it
1313
+ * the hard way: a refusal is `document.frame.refused` (warn, carrying `kind` + `reason`,
1314
+ * emitted from `#enqueue`'s continuation under the same `correlationId`). Acceptance is
1315
+ * silent on this event. So "was this frame ingested?" is answered by joining on
1316
+ * `correlationId`, never by reading this line alone.
1317
+ */
1318
+ this.#ctx.logger.info("session.document.received", {
1319
+ sessionId,
1320
+ senderPubkey,
1321
+ contentHashHex,
1322
+ sequenceNumber: leafIndex,
1323
+ kind: routed.kind,
1324
+ // The verdict is asynchronous. Stated positively so absence is not mistaken for silence.
1325
+ dispatch: "queued",
1326
+ verdictEvent: "document.frame.refused",
1327
+ correlationId,
1328
+ });
1329
+ return { leafIndex };
1330
+ }
1331
+ const { leafIndex } = this.#ctx.appendSessionLeaf(agentName, sessionId, "msg", contentHashHex, correlationId);
1332
+ // DOD-LOG-1: persist the readable RECEIVED plaintext to the durable transcript, keyed by the
1333
+ // canonical leaf sequence so it joins the committed hash chain (survives restart; INV-3 — the
1334
+ // relay/directory never see this plaintext, only the hash).
1335
+ const durable = this.#ctx.records.recordTranscriptMessage(agentName, sessionId, leafIndex, "received", content, correlationId,
1336
+ // DOD-M15-SEALWIRE-1 bullet 5: present only when the ordering record verified AND the signer
1337
+ // matched this session's counterparty. Undefined on the soft fallback, which the row records
1338
+ // as `local_session_state` rather than leaving indistinguishable.
1339
+ verifiedAuthorship);
1340
+ const recvKey = this.#ctx.sessionKey(agentName, sessionId);
1341
+ if (!durable) {
1342
+ // The leaf is committed and the plaintext is not. Delivery reads the transcript, so this
1343
+ // message is now unreachable by every session — record it so the receive path can SAY that
1344
+ // rather than time out wearing the quiet-counterparty answer (review F2).
1345
+ let lost = this.#ctx.undeliverableSeqs.get(recvKey);
1346
+ if (!lost) {
1347
+ lost = new Set();
1348
+ this.#ctx.undeliverableSeqs.set(recvKey, lost);
1349
+ }
1350
+ lost.add(leafIndex);
1351
+ // DOD-M15-NO-SILENT-REFUSAL-1: noted HERE, where the write actually fails, and not on the
1352
+ // cello_receive exit that reports it. `#undeliverableSeqs` is in memory, so the receive exit
1353
+ // stops being able to say this after a restart while the transcript hole stays permanent —
1354
+ // and the exit only runs if somebody is attending, which is the case this whole line is for.
1355
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, "content_undeliverable", {
1356
+ kind: REFUSAL_KINDS.LOST,
1357
+ impact: `a message arrived and was committed to the hash chain at sequence ${leafIndex}, and then its text could not be written to the local transcript. Delivery reads the transcript, so that message can never be handed to any session — it is a permanent hole in this side's copy of the conversation.`,
1358
+ guidance: "This is a fault on THIS machine; the counterparty did nothing wrong. Find out why the write failed — it is almost always something ordinary. " +
1359
+ "If you are reading this through a coding agent, have it check free disk space, the permissions on ~/.cello, and transcript.message.record.failed in the daemon log, which carries the underlying error. " +
1360
+ "Waiting cannot recover it. Once the fault is fixed, ask them to resend. If you genuinely cannot work out the cause, reach out to CELLO_Support.",
1361
+ });
1362
+ }
1363
+ // Review finding #6: the witness for this hash has done its ordering job once the leaf is
1364
+ // appended — drop it so #witnessedSeq stays proportional to held/pending content, not the whole
1365
+ // transcript. A later replay of the same hash is still caught by the dedup leaf-scan, which is
1366
+ // independent of the witness map.
1367
+ this.#ctx.witnessedSeq.get(recvKey)?.delete(contentHashHex);
1368
+ // DOD-M12B-LEAF-TRIGGERS-FETCH-1: the bytes are here, so cancel any fetch the witness leaf
1369
+ // scheduled. On a healthy session this is the branch that runs — the direct path beats the
1370
+ // grace window and the relay is never asked, which is what keeps a fetch off the hot path of
1371
+ // every message.
1372
+ this.markContentResolved(agentName, sessionId, contentHashHex);
1373
+ /**
1374
+ * THE ARRIVAL BUFFER USED TO BE FILLED HERE, and nothing read it.
1375
+ *
1376
+ * Every verified message was copied into an in-memory list as plaintext, capped at the last 32
1377
+ * per session and held for the life of the daemon process. `cello_receive` stopped draining it
1378
+ * at DOD-COATTEND-1, when delivery moved onto the durable transcript written a few lines above;
1379
+ * the away responder's peek at its tail was the last reader, and DOD-M15-AWAYSCOPE-1 deleted
1380
+ * that. What was left was the plaintext of every conversation, kept in memory, for nobody.
1381
+ *
1382
+ * The transcript row IS the delivery record. There is nothing to keep alongside it.
1383
+ */
1384
+ this.#ctx.logger.info("session.content.received", {
1385
+ sessionId,
1386
+ senderPubkey,
1387
+ contentHashHex,
1388
+ sequenceNumber: leafIndex,
1389
+ correlationId,
1390
+ });
1391
+ // M8C-MSGWAKE-1: content is now buffered and drainable — fire the doorbell AFTER the push so a
1392
+ // woken cello_receive finds the message. Content-free (agent/session/senderPubkey only). Never
1393
+ // let a listener error escape the content path.
1394
+ try {
1395
+ this.#ctx.onContentArrived?.(agentName, sessionId, senderPubkey);
1396
+ }
1397
+ catch (err) {
1398
+ this.#ctx.logger.warn("notification.cello_message.dispatch.failed", {
1399
+ sessionId, agentName, reason: extractErrorMessage(err),
1400
+ });
1401
+ }
1402
+ return { leafIndex };
1403
+ }
1404
+ /**
1405
+ * DAEMON-004: register the /cello/content/1.0.0 handler on a session node so
1406
+ * inbound content_frames are decoded, cross-checked, and ingested.
1407
+ */
1408
+ // Awaited by createSessionNode / acceptSession so the /cello/content/1.0.0 handler
1409
+ // is provably registered before the caller returns (and thus before any peer sends
1410
+ // content). libp2p registers the protocol synchronously today, but awaiting removes
1411
+ // the fragile dependency on that internal timing (review L4).
1412
+ async registerContentHandler(agentName, sessionId, node, _counterpartyPubkey) {
1413
+ try {
1414
+ await node.handle(CELLO_CONTENT_PROTOCOL_ID, (stream, remotePeerId) => {
1415
+ // `.catch` is not decoration: the handler builds its length-prefixed decoder before its own
1416
+ // try, and a throw there would otherwise become an unhandled rejection that takes the
1417
+ // daemon down for one malformed inbound stream.
1418
+ void this.#handleContentStream(agentName, sessionId, stream, remotePeerId).catch((err) => {
1419
+ this.#ctx.logger.warn("session.content.stream.handler.failed", {
1420
+ sessionId,
1421
+ error: extractErrorMessage(err),
1422
+ });
1423
+ });
1424
+ }, { maxInboundStreams: CONTENT_MAX_INBOUND_STREAMS });
1425
+ }
1426
+ catch (err) {
1427
+ this.#ctx.logger.error("session.content.handler.register.failed", {
1428
+ sessionId,
1429
+ error: extractErrorMessage(err),
1430
+ });
1431
+ }
1432
+ }
1433
+ /**
1434
+ * Hand the relay a leaf this agent RECEIVED whose author never submitted it — 034-CARRYLEAF.
1435
+ *
1436
+ * **The attack this closes:** somebody sends you something, declines to have it witnessed, and
1437
+ * seals one message short. The relay's account really does end before their last message, so your
1438
+ * receipt does too — every leaf validly signed, nothing false, the last thing said simply absent.
1439
+ *
1440
+ * **Why this is admissible and not a forgery:** the bytes are theirs, the signature over them is
1441
+ * theirs, and `#verifyAuthorshipClaim` verified it against this session's counterparty before a
1442
+ * word of it was ingested. The relay verifies it again against the directory-signed assignment.
1443
+ * Nothing here is asserted by us except that we received it.
1444
+ *
1445
+ * ⚠️ BEST-EFFORT, AND ITS FAILURE IS NOT SILENT. If the relay cannot be reached, the message is
1446
+ * still delivered and read — refusing it would make the relay a precondition for reading mail,
1447
+ * which is the thing every unit on this path has been careful not to do. What is lost is only the
1448
+ * guarantee that it can enter a receipt, and that surfaces where the operator can act on it: the
1449
+ * seal's own pre-flight refuses a gapped chain by name (`seal_carry_noncontiguous`) with guidance,
1450
+ * so the consequence reaches them at the moment it matters rather than as a log line here.
1451
+ */
1452
+ witnessReceivedLeaf(agentName, sessionId, contentHash, structure1Cbor, senderSignature,
1453
+ /** The domain the AUTHOR assigned this leaf, read off their frame — never guessed (review F5). */
1454
+ leafKind, correlationId) {
1455
+ const entry = this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId));
1456
+ if (!entry?.relayClient || !entry.relaySessionIdBytes) {
1457
+ this.#ctx.logger.warn("session.content.witness_received.unavailable", {
1458
+ agentName, sessionId, correlationId,
1459
+ impact: "a message arrived that its sender never had witnessed, and this side has no relay client " +
1460
+ "for the session, so it could not be witnessed here either. It is delivered and readable; " +
1461
+ "it cannot enter a notarized receipt until some party witnesses it.",
1462
+ });
1463
+ return;
1464
+ }
1465
+ void entry.relayClient
1466
+ .witnessReceivedLeaf(entry.node, entry.relaySessionIdBytes, contentHash, leafKind, {
1467
+ structure1Cbor,
1468
+ senderSignature,
1469
+ })
1470
+ .then((res) => {
1471
+ if (res.ok) {
1472
+ this.#ctx.logger.info("session.content.witness_received", {
1473
+ agentName, sessionId, correlationId, relaySequence: res.sequence_number,
1474
+ impact: "this side witnessed a message its SENDER did not. The leaf now holds a canonical " +
1475
+ "position, so it can appear in a receipt whatever the sender does next.",
1476
+ });
1477
+ // It has a position now, so it can be acknowledged like any other received message.
1478
+ this.noteAcknowledgeable(agentName, sessionId, res.sequence_number - 1, contentHash);
1479
+ return;
1480
+ }
1481
+ /**
1482
+ * `counter_submit_duplicate` is NOT a failure and must not be logged as one: it means this
1483
+ * relay already holds the leaf, which is the outcome we wanted. It fires on the ordinary
1484
+ * race where the sender's own submit lands while ours is in flight.
1485
+ */
1486
+ if (res.reason === "counter_submit_duplicate") {
1487
+ this.#ctx.logger.info("session.content.witness_received.already_held", {
1488
+ agentName, sessionId, correlationId,
1489
+ impact: "the relay already held this leaf — its sender witnessed it after all, or in parallel with us.",
1490
+ });
1491
+ return;
1492
+ }
1493
+ this.#ctx.logger.error("session.content.witness_received.failed", {
1494
+ agentName, sessionId, correlationId, reason: res.reason,
1495
+ ...(res.detail === undefined ? {} : { detail: res.detail }),
1496
+ impact: "a message arrived that its sender never had witnessed, and this side could not witness " +
1497
+ "it either. It is delivered and readable. What is at risk is the RECEIPT: if this " +
1498
+ "message is still unwitnessed when the conversation is sealed, the seal will refuse a " +
1499
+ "gapped chain by name rather than quietly leaving it out.",
1500
+ });
1501
+ })
1502
+ .catch((err) => {
1503
+ this.#ctx.logger.error("session.content.witness_received.threw", {
1504
+ agentName, sessionId, correlationId, error: extractErrorMessage(err),
1505
+ });
1506
+ });
1507
+ }
1508
+ /**
1509
+ * Record that a message ARRIVED and was accepted at a known canonical position — 033-ACKEMIT
1510
+ * review F1.
1511
+ *
1512
+ * The one writer for both copies of the acknowledgement, so the claim this daemon signs says what
1513
+ * it actually received rather than what the relay got round to delivering back to it.
1514
+ *
1515
+ * Monotonic, and it must be: a re-delivery or a recovered park of an EARLIER message must not walk
1516
+ * the acknowledgement backwards, and must not swap the hash under an unchanged position.
1517
+ */
1518
+ noteAcknowledgeable(agentName, sessionId, canonicalSeq, contentHash) {
1519
+ // Relay sequences are 1-based; a canonical leaf index is 0-based. The claim carries the relay's
1520
+ // number, because the relay is what checks it.
1521
+ const relaySeq = canonicalSeq + 1;
1522
+ if (relaySeq < 1)
1523
+ return;
1524
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1525
+ const prev = this.#ctx.lastAck.get(key);
1526
+ if (prev && relaySeq <= prev.seq)
1527
+ return;
1528
+ this.#ctx.lastAck.set(key, { seq: relaySeq, hash: Uint8Array.from(contentHash) });
1529
+ this.#ctx.records.recordLastAck(agentName, sessionId, relaySeq, contentHash);
1530
+ const entry = this.#ctx.activeNodes.get(key);
1531
+ const sessionIdHex = entry?.relaySessionIdBytes
1532
+ ? Buffer.from(entry.relaySessionIdBytes).toString("hex")
1533
+ : sessionId;
1534
+ entry?.relayClient?.noteReceivedLeaf(sessionIdHex, relaySeq, contentHash);
1535
+ }
1536
+ #refuseInboundContent(agentName, sessionId, reason, contentHash, detail, correlationId) {
1537
+ // The sentence about the other route is chosen HERE, from what this machine can actually do —
1538
+ // never written into a caller's literal, where it would be a promise nobody re-checked.
1539
+ const guidance = `${detail.guidance} ${this.#ctx.mailboxRouteAvailable(agentName) ? REFUSAL_MAY_STILL_ARRIVE : REFUSAL_NO_OTHER_ROUTE}`;
1540
+ this.#ctx.logger.error("session.content.refused", { agentName, sessionId, correlationId, reason, ...detail, guidance });
1541
+ this.#ctx.notices.noteContentRefusal(agentName, sessionId, reason, {
1542
+ kind: REFUSAL_KINDS.REFUSED, impact: detail.impact, guidance,
1543
+ });
1544
+ /**
1545
+ * Review F4 — A PROMISE MADE HERE IS CLOSED IN `recoverParkedEntry`, not left standing.
1546
+ *
1547
+ * The guidance above tells the operator the message may arrive by the mailbox. Both sibling
1548
+ * refusals on this path already arm a memo so the recovery can say the refusal did not hold;
1549
+ * this one armed nothing, so a delivered message would have left a permanent alarm sitting in
1550
+ * `cello_check_notifications` saying it had been turned away.
1551
+ *
1552
+ * Armed AFTER the notice is filed, so the memo can never claim a refusal that did not happen.
1553
+ */
1554
+ this.#ctx.refusals.noteRefusedOnDirectPath(agentName, sessionId, contentHash);
1555
+ }
1556
+ async #handleContentStream(agentName, sessionId, stream, remotePeerId) {
1557
+ // CLOSING THIS STREAM IS WHAT KEEPS THE SESSION ALIVE PAST ITS 33RD MESSAGE.
1558
+ //
1559
+ // Every content frame and every delivery ACK opens a fresh /cello/content/1.0.0 stream on the
1560
+ // one muxed connection the session holds, and libp2p caps INBOUND streams per protocol per
1561
+ // connection. It enforces that cap AFTER multistream-select has answered, so an over-cap stream
1562
+ // negotiates fine and is reset an instant later, and the SENDER's next `stream.send(...)`
1563
+ // throws "Cannot write to a stream that is closed" — an error that names the exit point and not
1564
+ // one thing about the cause.
1565
+ //
1566
+ // A stream leaves the muxer's set only on its `close` event, and closing our write end triggers
1567
+ // that only once the peer has closed its end too. So a handler that reads its frame and returns
1568
+ // leaves the stream half-open for the life of the connection and the count only ever rises.
1569
+ // Measured on a live daemon: 115 failures over 3.5 hours, with EXACTLY 32 successful streams
1570
+ // before the first one on both affected sessions (M12B Entry 10).
1571
+ //
1572
+ // The decoder is built INSIDE the try so a malformed stream cannot throw past the close below.
1573
+ let iter;
1574
+ try {
1575
+ iter = lp.decode(stream)[Symbol.asyncIterator]();
1576
+ const result = await iter.next();
1577
+ if (result.done || result.value === undefined)
1578
+ return;
1579
+ const bytes = result.value instanceof Uint8Array ? result.value
1580
+ : Buffer.isBuffer(result.value) ? new Uint8Array(result.value)
1581
+ : result.value.slice();
1582
+ const frame = decode(bytes);
1583
+ const correlationId = typeof frame["correlation_id"] === "string" ? frame["correlation_id"] : undefined;
1584
+ const frameType = typeof frame["type"] === "string" ? frame["type"] : "(absent)";
1585
+ /**
1586
+ * DOD-M15-FRAME-1 — ONE GATE, BEFORE THE DISPATCH, FOR EVERY FRAME ON THIS PROTOCOL.
1587
+ *
1588
+ * A stranger could dial an agent's standing receiver (it admitted everyone until DOD-M15-ASSIGN-1), hold
1589
+ * the connection open through promotion — libp2p's gater runs only at connection
1590
+ * establishment, so narrowing it does not evict anyone already attached — and then speak the
1591
+ * content protocol the moment it activated. The frame was ingested, leafed, transcribed, and
1592
+ * attributed to the legitimate counterparty, because attribution is read from local session
1593
+ * state rather than from anything the frame proved.
1594
+ *
1595
+ * DELIBERATELY SHARED RATHER THAN COPIED INTO EACH BRANCH. `session_abandoned_notice` already
1596
+ * had both checks, correct and complete, twenty lines below — and the other two frame types
1597
+ * did not. Copying the pattern a third and fourth time would fix today's three and leave the
1598
+ * fifth frame type, added later by someone who did not read this comment, unguarded again.
1599
+ * Placing it above the dispatch makes the guard the DEFAULT: a new frame type is protected by
1600
+ * construction and has to opt OUT visibly rather than opt in silently.
1601
+ *
1602
+ * Verified safe for all three current types by enumeration, not assumption — `content_frame`
1603
+ * (:5169), `session_abandoned_notice` (:6663) and `content_delivery_ack` (:7439) are the only
1604
+ * senders on `CELLO_CONTENT_PROTOCOL_ID`, and all three put `session_id` in the frame.
1605
+ *
1606
+ * MISSING, MALFORMED AND MISMATCHED TAKE ONE PATH. An attacker evading a mismatch check does
1607
+ * not send a wrong value — it sends no value, and a guard that only fires on a present-and-
1608
+ * wrong field is a guard that is trivially skipped. That is exactly what the old
1609
+ * `content_frame` check did: `typeof x === "string" && x !== sessionId`.
1610
+ */
1611
+ const expectedPeer = this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId))?.counterpartySessionPeerId;
1612
+ if (!remotePeerId || !expectedPeer || remotePeerId !== expectedPeer) {
1613
+ // Loud in the LOG — there is no caller to answer on an inbound stream, so this is the whole
1614
+ // surface. Neutral wording: this is an observation, not a verdict about intent. The same
1615
+ // signal comes from a real impersonation attempt and from our own fallback paths
1616
+ // mishandling a reconnect, and nothing here can tell them apart.
1617
+ this.#ctx.logger.warn("session.content.peer_mismatch", {
1618
+ agentName, sessionId, frameType,
1619
+ remotePeerId: remotePeerId ?? "(absent)", expected: expectedPeer ?? "(unknown)",
1620
+ impact: "a frame arrived on this session's content protocol from a peer that is not its counterparty; it was refused — not ingested, not attributed, not recorded — and the peer was disconnected",
1621
+ });
1622
+ /**
1623
+ * PEER-ENDING, NOT SESSION-ENDING — and the difference is a deliberate deviation from the
1624
+ * DoD clause (review F2).
1625
+ *
1626
+ * The clause says the refusal is session-ending. Applied HERE that would be a worse hole
1627
+ * than the one it closes: a pre-positioned stranger could kill any session on the machine
1628
+ * with a single frame, trading an injection hole for a denial-of-service hole. The
1629
+ * session-ending response belongs where the evidence is about the SESSION's counterparty —
1630
+ * `#freezeOnIdentityFailure`, reached when a party that IS the peer we dialled signs with a
1631
+ * key that is not theirs.
1632
+ *
1633
+ * Here the evidence is about the PEER: they are not party to this session at all. So the
1634
+ * connection goes and the session is untouched. Without this the stranger stayed attached
1635
+ * for the life of the session and the gate re-refused each frame forever — and the eviction
1636
+ * sweep's own fallback ("the frame gate still refuses anything this peer sends") only closes
1637
+ * the loop if the frame gate does something about the connection.
1638
+ *
1639
+ * Fire-and-forget: a hang-up that fails must not turn a successful refusal into a thrown
1640
+ * handler, and the refusal above has already done the load-bearing work.
1641
+ */
1642
+ if (remotePeerId) {
1643
+ const entry = this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId));
1644
+ void entry?.node.hangUp(remotePeerId).catch((err) => {
1645
+ this.#ctx.logger.debug("session.content.peer_mismatch.hangup_failed", {
1646
+ sessionId, peerId: remotePeerId, error: extractErrorMessage(err),
1647
+ });
1648
+ });
1649
+ }
1650
+ return;
1651
+ }
1652
+ const claimedSessionId = frame["session_id"];
1653
+ if (typeof claimedSessionId !== "string" || claimedSessionId !== sessionId) {
1654
+ this.#ctx.logger.warn("session.content.session_mismatch", {
1655
+ agentName, sessionId, frameType,
1656
+ claimedSessionId: typeof claimedSessionId === "string" ? claimedSessionId : "(absent)",
1657
+ impact: "the frame does not name the session whose stream it arrived on; it was refused rather than routed, because the authenticated stream is the better authority for where content belongs",
1658
+ });
1659
+ return;
1660
+ }
1661
+ // CELLO-M7-MSG-001 (AC-001/AC-002): a `persisted` delivery ACK arriving on the
1662
+ // same /cello/content/1.0.0 protocol resolves the sender's awaiting-ACK timer.
1663
+ // The protocol acts on `persisted` ONLY — any other level leaves the timer armed.
1664
+ if (frame["type"] === "content_delivery_ack") {
1665
+ const ackHash = frame["content_hash"];
1666
+ const level = frame["level"];
1667
+ if (ackHash instanceof Uint8Array && level === "persisted") {
1668
+ onDeliveryAck(this.#ctx, (a, sid, h) => this.#send.resolveAwaitingAck(a, sid, h), agentName, sessionId, ackHash, frame["ack_sig"], correlationId);
1669
+ }
1670
+ return;
1671
+ }
1672
+ // DOD-M12B-ABANDON-NOTIFY-1: the counterparty force-abandoned. Handled here, on the same
1673
+ // authenticated stream the delivery acknowledgement rides, and AFTER the session-id check
1674
+ // below cannot be skipped — the frame names its session and the handler is bound to one.
1675
+ if (frame["type"] === "session_abandoned_notice") {
1676
+ // DOD-M15-FRAME-1: the peer and session checks that used to live here now run above, for
1677
+ // EVERY frame type, unchanged in substance — this branch was where they were written first
1678
+ // and correctly, and it is the reference the shared gate was lifted from. Its comment is
1679
+ // preserved there, including the reason the transport being authenticated is not enough.
1680
+ // Left as a bare dispatch on purpose: a second copy of a guard is a second thing to keep in
1681
+ // step, and the one that drifts is the one nobody is reading.
1682
+ void this.#ctx.retireOnCounterpartyAbandon(agentName, sessionId, correlationId);
1683
+ return;
1684
+ }
1685
+ /**
1686
+ * DOD-M15-SEALWIRE-1 bullet 6 (part A) — the salt agreement.
1687
+ *
1688
+ * Placed BELOW the shared peer/session gate deliberately, which is the whole reason that gate
1689
+ * was lifted above the dispatch: a new frame type is protected by construction rather than
1690
+ * having to remember to opt in. A stranger's salt frame is refused before it reaches here, so
1691
+ * nothing about this session's salt can be steered by a peer that is not its counterparty.
1692
+ *
1693
+ * The fields are read defensively into the frame shape rather than cast: an inbound value is
1694
+ * whatever a peer chose to encode, and `onPeerSaltFrame` refuses both-fields and neither-field
1695
+ * by name — so a non-Uint8Array in either slot must arrive at that function as ABSENT, not as
1696
+ * a present-but-wrong value it would then try to use.
1697
+ */
1698
+ /**
1699
+ * 007-CRYPTO — the peer's SIGNED ephemeral.
1700
+ *
1701
+ * Fields are read defensively rather than cast, exactly like the salt frame below: an inbound
1702
+ * value is whatever a peer chose to encode, and `verifySessionEphemeral` refuses a missing or
1703
+ * wrong-width one BY NAME — so a non-`Uint8Array` must arrive there as ABSENT rather than as a
1704
+ * present-but-wrong value it would try to use.
1705
+ */
1706
+ if (frame["type"] === "session_key_agreement") {
1707
+ const ephemeralPublic = frame["ephemeral_public"];
1708
+ const signature = frame["ephemeral_sig"];
1709
+ await this.#ctx.ephemerals.handleEphemeralFrame(agentName, sessionId, {
1710
+ ...(ephemeralPublic instanceof Uint8Array ? { ephemeralPublic } : {}),
1711
+ ...(signature instanceof Uint8Array ? { signature } : {}),
1712
+ }, correlationId);
1713
+ return;
1714
+ }
1715
+ if (frame["type"] === "session_salt_agreement") {
1716
+ const contribution = frame["contribution"];
1717
+ const fingerprint = frame["fingerprint"];
1718
+ const adoptionClosed = frame["adoption_closed"];
1719
+ await this.#ctx.salts.handleSaltFrame(agentName, sessionId, {
1720
+ ...(contribution instanceof Uint8Array ? { contribution } : {}),
1721
+ ...(fingerprint instanceof Uint8Array ? { fingerprint } : {}),
1722
+ // A non-string stays ABSENT rather than being coerced, exactly like the other two: the
1723
+ // decision function refuses a shape it cannot read, and must never be handed a `"42"`.
1724
+ //
1725
+ // TRUNCATED AT THE BOUNDARY — 006-CRYPTO finding 6. Every label CELLO sends is under
1726
+ // twenty characters, and this one is chosen entirely by the peer. Cutting it here means
1727
+ // no unbounded peer string is stored, logged or rendered anywhere downstream; the
1728
+ // rendering that keeps it out of our own sentences is `renderPeerAdoptionLabel`.
1729
+ ...(typeof adoptionClosed === "string" && adoptionClosed.length > 0
1730
+ ? { adoptionClosed: adoptionClosed.slice(0, SALT_ADOPTION_LABEL_MAX) }
1731
+ : {}),
1732
+ }, correlationId);
1733
+ return;
1734
+ }
1735
+ if (frame["type"] !== "content_frame") {
1736
+ // LOGGED, not silently dropped. This handler is bound to one session, and a frame it does
1737
+ // not understand arriving on that stream is either a peer speaking a newer protocol or a
1738
+ // bug on our side — both worth a line, and neither distinguishable from "nothing arrived"
1739
+ // when the return is silent.
1740
+ this.#ctx.logger.warn("session.content.frame_unknown_type", {
1741
+ sessionId,
1742
+ type: typeof frame["type"] === "string" ? String(frame["type"]) : "(absent)",
1743
+ });
1744
+ return;
1745
+ }
1746
+ // DOD-M15-FRAME-1: the session-id check moved to the shared gate above, and its `&&` became
1747
+ // `||` on the way. It read `typeof x === "string" && x !== sessionId` — firing only when the
1748
+ // field was PRESENT and wrong, so omitting it passed. Its own sibling twenty lines up already
1749
+ // refused absence, with a comment saying treating a missing field as agreement is how a guard
1750
+ // stops guarding. Same file, same switch, opposite conclusion.
1751
+ // Review F4: hand the DECODED frame to a test observer before anything consumes it. Absent in
1752
+ // production — the field is null unless a test installs one.
1753
+ this.#ctx.inboundFrameObserver?.(frame);
1754
+ const contentBytes = frame["content_bytes"];
1755
+ const contentHash = frame["content_hash"];
1756
+ if (!(contentBytes instanceof Uint8Array) || !(contentHash instanceof Uint8Array)) {
1757
+ // Same reasoning as the unknown type above: a malformed frame that vanishes without a trace
1758
+ // is indistinguishable, from the operator's side, from a counterparty who never sent
1759
+ // anything.
1760
+ this.#ctx.logger.warn("session.content.frame_malformed", {
1761
+ sessionId,
1762
+ hasContent: contentBytes instanceof Uint8Array,
1763
+ hasHash: contentHash instanceof Uint8Array,
1764
+ });
1765
+ return;
1766
+ }
1767
+ /**
1768
+ * 🚨 DECRYPT BEFORE ANYTHING ELSE READS THE BODY — `DOD-M15-EPHEMERAL-AUTH-1`.
1769
+ *
1770
+ * `content_hash` is over the PLAINTEXT, so the hash check, the transcript, the seal and the
1771
+ * salted hash all keep meaning exactly what they mean today — but only if the body is put back
1772
+ * before any of them run.
1773
+ *
1774
+ * ⚠️ ABSENT IS NOT A PASS. A frame with no `content_encryption` is refused rather than read as
1775
+ * plaintext. There is no unencrypted sender to be compatible with, and treating a missing
1776
+ * marker as "this one is in the clear" is precisely the downgrade an attacker asks for: strip
1777
+ * one field and the receiver reads the body raw. Missing and unknown take the same path as a
1778
+ * failed decrypt, for the reason that runs through this whole unit — a check lenient about an
1779
+ * absent proof is a check that gets skipped.
1780
+ */
1781
+ const declaredEncryption = frame["content_encryption"];
1782
+ const encState = this.#ctx.ephemerals.contentEncryptionState(agentName, sessionId);
1783
+ let plaintextBody;
1784
+ if (declaredEncryption !== SESSION_CONTENT_ENCRYPTION_V1) {
1785
+ this.#refuseInboundContent(agentName, sessionId, "content_encryption_absent_or_unknown", contentHash, {
1786
+ declared: typeof declaredEncryption === "string" ? declaredEncryption : "(absent)",
1787
+ impact: "the frame did not say it was encrypted under this session's key, so it was refused unread — nothing was shown and this copy was not kept.",
1788
+ guidance: "STOPPED ON PURPOSE. A message arrived that was not encrypted under this session's key. " +
1789
+ "This build never sends one, so either something between you rewrote the frame, or your " +
1790
+ "counterparty is running something that is not CELLO. Confirm with them OUT OF BAND " +
1791
+ "before opening another session.",
1792
+ }, correlationId);
1793
+ return;
1794
+ }
1795
+ if (encState.key === null) {
1796
+ this.#refuseInboundContent(agentName, sessionId, "no_session_key", contentHash, {
1797
+ detail: encState.reason,
1798
+ impact: "an encrypted message arrived and this side has no agreed key to open it, so it was refused unread rather than shown as garbage.",
1799
+ // Review F6: the RECEIVE-side wording. The send-side table explains what became of a
1800
+ // message this operator sent, which is the wrong direction entirely for a message they
1801
+ // cannot open.
1802
+ guidance: CONTENT_ENCRYPTION_INBOUND_GUIDANCE[encState.reason],
1803
+ }, correlationId);
1804
+ return;
1805
+ }
1806
+ const opened = openSessionContent(encState.key, contentBytes);
1807
+ if (opened === null) {
1808
+ // GCM's tag is the only thing separating "not for us" from "modified in flight", and this
1809
+ // side must not branch on which — that would be branching on attacker-controlled input.
1810
+ this.#refuseInboundContent(agentName, sessionId, "decrypt_failed", contentHash, {
1811
+ impact: "the message did not decrypt under this session's agreed key — it was modified in flight, or it was encrypted under a different key. Refused unread.",
1812
+ guidance: "STOPPED ON PURPOSE. Nothing was shown and this copy was not kept. A message that fails " +
1813
+ "this check has either been altered on its way to you or was not encrypted for this " +
1814
+ "session. Confirm with your counterparty OUT OF BAND, then start a new session.",
1815
+ }, correlationId);
1816
+ return;
1817
+ }
1818
+ plaintextBody = opened;
1819
+ /**
1820
+ * ─── THE CONVERSATION IS OVER, AND THAT OUTRANKS EVERY QUESTION BELOW IT ──────────────────
1821
+ * `DOD-M15-CLOSEDSESSION-1`.
1822
+ *
1823
+ * Placed HERE — after the decrypt, before the authorship claim — for two reasons, and both
1824
+ * are load-bearing:
1825
+ *
1826
+ * - AFTER the decrypt, because refusing retains the bytes, and the bytes worth retaining are
1827
+ * the message. Refusing a line earlier would quarantine ciphertext nobody can read.
1828
+ * - BEFORE the authorship claim, because that is what was answering. A message composed
1829
+ * after the seal acknowledges content our frozen record does not hold, so
1830
+ * `verifyAuthorshipClaim` returned `ack_hash_unknown_content` and the status was never
1831
+ * consulted at all. Measured live on session `9d253bce…`.
1832
+ *
1833
+ * ⚠️ THIS IS A REORDER, NOT A REMOVAL. The acknowledgement check still runs, and still
1834
+ * refuses, for every session that is not closed — which is the state it exists for. What it
1835
+ * no longer does is describe a hash where the situation is that there is nothing left to
1836
+ * acknowledge.
1837
+ *
1838
+ * The peer gate above has already established this frame came from THIS session's
1839
+ * counterparty peer, and the session key opened it. So nothing is attributed on the strength
1840
+ * of an unverified signature: the quarantine row records the counterparty from local session
1841
+ * state, exactly as the park route's refusal does.
1842
+ */
1843
+ if (refuseIfSessionClosed(this.#ctx, agentName, sessionId, plaintextBody, Buffer.from(contentHash).toString("hex"), correlationId).refused) {
1844
+ /**
1845
+ * ⚠️ ONE SIGNAL IS DELIBERATELY GIVEN UP, AND IT IS NAMED RATHER THAN LOST — review F4. A
1846
+ * frame signed by a key that is NOT the counterparty's used to reach
1847
+ * `session.content.authorship.refuted` here. The FREEZE it triggers is moot on a session
1848
+ * that is already terminal, and verifying a signature purely to log it is work an attacker
1849
+ * can ask for by volume — so what survives is whether a proof was carried at all. The bytes
1850
+ * are retained either way, so the signature is still there to be examined.
1851
+ */
1852
+ this.#ctx.logger.info("session.content.closed.frame_proof", {
1853
+ agentName, sessionId, correlationId,
1854
+ hasStructure1: frame["structure1_cbor"] instanceof Uint8Array,
1855
+ hasSenderSignature: frame["sender_signature"] instanceof Uint8Array,
1856
+ impact: "the message was refused because this conversation is closed, so its authorship proof was NOT verified and no identity verdict was reached about it. The bytes are retained and readable.",
1857
+ });
1858
+ return;
1859
+ }
1860
+ // DOD-MSG-4 (self-ordering content frame): if the frame carries the relay's signed ordering
1861
+ // record, verify the sender signature and record the canonical sequence FROM THE FRAME, BEFORE
1862
+ // ingest — so the strict-in-order gate has the position without waiting on the separate
1863
+ // leaf_deliver witness (removes the content-before-witness race).
1864
+ //
1865
+ // DOD-M15-FRAME-1 — POSITION MAY BE SOFT; IDENTITY MAY NOT. The old comment here read "A
1866
+ // bad/absent record is non-fatal: the content still ingests", and it was accurate: a
1867
+ // signature that failed to verify, and a signature by a key that is NOT this session's
1868
+ // counterparty, both returned null and the content was ingested and attributed anyway. An
1869
+ // ABSENT record stays soft — that is the documented relay-degraded path and refusing it would
1870
+ // make the relay a precondition for reading mail. A record that is PRESENT and REFUTED is a
1871
+ // different fact, and it is now refused.
1872
+ const s1Cbor = frame["structure1_cbor"];
1873
+ const s2Cbor = frame["structure2_cbor"];
1874
+ /**
1875
+ * `DOD-M15-AUTHORSHIP-ABSENT-1` — the sender's own signature, carried BESIDE the bytes it
1876
+ * signs, exactly as `hash_submit` has always carried it. This field is why identity no longer
1877
+ * depends on the relay: it arrives whether or not a relay witnessed the message.
1878
+ */
1879
+ const senderSig = frame["sender_signature"];
1880
+ let framedSeq = null;
1881
+ /**
1882
+ * DOD-M15-SEALWIRE-1 bullet 5. Set ONLY when the ordering record verified — the signature
1883
+ * checked against the pubkey inside the sender's own signed bytes AND the signer matched this
1884
+ * session's counterparty. It is deliberately NOT set on the two soft paths below (no record
1885
+ * supplied; decode failed), because on those the author is attested by local session state
1886
+ * and the transcript row must say so rather than imply a proof it does not have.
1887
+ */
1888
+ let verifiedAuthorship;
1889
+ /**
1890
+ * 024-ORPHANTRIAGE: the signer when the signature verified but there was no counterparty to
1891
+ * match it against. Its ONLY consumer is the orphan branch inside ingest — everywhere else a
1892
+ * session record exists, so this stays `undefined` and nothing reads it.
1893
+ */
1894
+ let verifiedSignerUnmatched;
1895
+ /**
1896
+ * ─── NO PASSPORT, NO ENTRY — `DOD-M15-AUTHORSHIP-ABSENT-1` ───────────────────────────────
1897
+ *
1898
+ * ⚠️ **THIS COMMENT USED TO SAY THE OPPOSITE, AND THE SENTENCE IT REPLACES IS THE DEFECT.**
1899
+ * It read: *"it means the per-message signer check is **opt-in for the sender** — a party that
1900
+ * passed the peer gate and wants to avoid the comparison simply omits the proof."* That was an
1901
+ * accurate description of the code, which is why it is rewritten here rather than deleted: it
1902
+ * is the sentence a reader with a coding agent finds, and it must now describe what the code
1903
+ * does. A frame that supplies nothing checkable is REFUSED. Omitting the proof buys the sender
1904
+ * nothing except a message that does not arrive.
1905
+ *
1906
+ * The old reasoning was sound as far as it went — the signature was only ever DELIVERED inside
1907
+ * the relay's Structure 2, so refusing on its absence would have made the relay a precondition
1908
+ * for reading mail. It stopped one field short: the signature travels beside the bytes it
1909
+ * signs now, on every content frame, so identity no longer needs the relay and position still
1910
+ * does not require identity.
1911
+ *
1912
+ * ⚠️ REFUSED, NOT FROZEN. A frozen session is only cleared by opening a new one, and the
1913
+ * overwhelmingly likely cause of an absent proof is a counterparty on an older build. The
1914
+ * freeze is for a proof that FAILED (below, and in `#recordFrameOrdering`) — a positive fact
1915
+ * about their key.
1916
+ */
1917
+ if (!(s1Cbor instanceof Uint8Array) || !(senderSig instanceof Uint8Array)) {
1918
+ this.#ctx.refusals.refuseUnprovenAuthorship(agentName, sessionId, "authorship_proof_absent", contentHash, {
1919
+ // WHICH half is missing. A sender on an older build supplies neither; a stripped frame is
1920
+ // likelier to be missing one, and an investigator should not have to guess which.
1921
+ hasStructure1: s1Cbor instanceof Uint8Array,
1922
+ hasSenderSignature: senderSig instanceof Uint8Array,
1923
+ }, correlationId);
1924
+ return;
1925
+ }
1926
+ const authorship = this.#ctx.authorship.verifyAuthorshipClaim(agentName, sessionId, s1Cbor, senderSig, contentHash);
1927
+ if (authorship.verdict === "refuted") {
1928
+ /**
1929
+ * THE FORENSIC LINE, BEFORE THE FREEZE. `session.content.identity.frozen` records that a
1930
+ * session was stopped; this records WHICH check stopped it and on WHICH proof — the frame's
1931
+ * own signature, not the relay's copy of it. The two used to be the same event because there
1932
+ * was only one place a signer was checked; there are two now, and an investigation that
1933
+ * cannot tell them apart is looking at the wrong half of the wire.
1934
+ */
1935
+ this.#ctx.logger.warn("session.content.authorship.refuted", {
1936
+ agentName, sessionId, correlationId, reason: authorship.reason,
1937
+ impact: "a message arrived with a proof of authorship that FAILED — it does not verify, or it is signed by a key that is not this session's counterparty. Nothing was ingested and the session is being frozen.",
1938
+ });
1939
+ await this.#ctx.freezeOnIdentityFailure(agentName, sessionId, authorship.reason, correlationId);
1940
+ return;
1941
+ }
1942
+ if (authorship.verdict === "unusable") {
1943
+ // A replayed claim gets its own name on BOTH surfaces, not just in the log context: it is
1944
+ // the one `unusable` cause that may be adversarial, and it is the one the operator can act
1945
+ // on. The others are a peer whose build or bytes we could not read.
1946
+ /**
1947
+ * 033-ACKEMIT — AND THE THREE ACKNOWLEDGEMENT CAUSES GET THEIR OWN SURFACE REASON, for the
1948
+ * same argument that gave the replay one: `authorship_proof_unusable` tells the operator the
1949
+ * proof was "unreadable, or signed over different content", and for these it is neither.
1950
+ * The proof is perfect; what it CLAIMS TO HAVE SEEN is wrong. Filing them under the generic
1951
+ * name would send someone to audit a decoder, and would spend the operator's attention
1952
+ * asking their counterparty about a version number that is not the question.
1953
+ */
1954
+ this.#ctx.refusals.refuseUnprovenAuthorship(agentName, sessionId, authorship.reason === AUTHORSHIP_SESSION_MISMATCH
1955
+ ? "authorship_wrong_conversation"
1956
+ /**
1957
+ * `DOD-M15-SELFCHAIN-1` — its own name on the surface the operator reads, not only in a
1958
+ * log field. See the sentences in `#refuseUnprovenAuthorship`.
1959
+ */
1960
+ : authorship.reason === AUTHORSHIP_SELF_CHAIN_MISMATCH
1961
+ ? AUTHORSHIP_SELF_CHAIN_MISMATCH
1962
+ : ACK_HASH_REASONS.has(authorship.reason)
1963
+ /**
1964
+ * ⚠️ THE SPECIFIC CAUSE, NOT THE CLASS — review F5, and the diff's own comment on
1965
+ * `ACK_HASH_REASONS` had already said why: "the operator's next move differs for
1966
+ * each." It then collapsed all three into ONE surface reason carrying ONE sentence,
1967
+ * so the three names survived only in a log field nobody reads. For an absent
1968
+ * acknowledgement the shared impact was flatly false — there is no part that "does
1969
+ * not match", because there is no part — and for the other two the shared guidance
1970
+ * sent the reader to ask about a build version that cannot be the cause.
1971
+ */
1972
+ ? authorship.reason
1973
+ : "authorship_proof_unusable", contentHash, { detail: authorship.reason }, correlationId);
1974
+ /**
1975
+ * ─── AND THE SESSION FREEZES, EVENTUALLY — `DOD-M15-SELFCHAIN-1` + `-GAP-1` ──────────────
1976
+ *
1977
+ * ⚠️ ONLY THIS ONE OF THE `unusable` CAUSES FREEZES. The acknowledgement causes say the
1978
+ * sender is wrong about what WE said, which a drifted record produces honestly. This one
1979
+ * says they are wrong about what THEY said, so continuing writes a disputed order into the
1980
+ * receipt. The freeze is what makes the refusal an ESCALATION rather than a dropped frame.
1981
+ *
1982
+ * ⚠️ **BUT IT IS NO LONGER IMMEDIATE.** "A party cannot be honestly mistaken about what
1983
+ * they said" is false on the relay path — a message routinely arrives before its own
1984
+ * predecessor, which is still in the mailbox. The decision, the grace and its log line live
1985
+ * with the gap state, in `AuthorshipVerifier.noteSelfChainGapAndShouldFreeze`.
1986
+ */
1987
+ if (authorship.reason === AUTHORSHIP_SELF_CHAIN_MISMATCH
1988
+ && this.#ctx.authorship.noteSelfChainGapAndShouldFreeze(agentName, sessionId, correlationId)) {
1989
+ await this.#ctx.freezeOnIdentityFailure(agentName, sessionId, authorship.reason, correlationId);
1990
+ }
1991
+ return;
1992
+ }
1993
+ if (authorship.verdict === "verified") {
1994
+ verifiedAuthorship = { senderPubkey: authorship.senderPubkey, senderSig: authorship.senderSig };
1995
+ }
1996
+ else {
1997
+ verifiedSignerUnmatched = authorship.senderPubkey;
1998
+ }
1999
+ if (s2Cbor instanceof Uint8Array) {
2000
+ const ordering = this.#ctx.refusals.recordFrameOrdering(agentName, sessionId, s1Cbor, s2Cbor, contentHash, correlationId);
2001
+ if (ordering.fatal) {
2002
+ await this.#ctx.freezeOnIdentityFailure(agentName, sessionId, ordering.fatal.reason, correlationId);
2003
+ return;
2004
+ }
2005
+ framedSeq = ordering.seq;
2006
+ }
2007
+ else {
2008
+ /**
2009
+ * POSITION IS THE ONLY THING THAT CAN BE ABSENT NOW, and this event is about position.
2010
+ *
2011
+ * It fires on the relay-degraded path, where the sender had no witnessed record to stamp
2012
+ * on. The message is ingested — its author is proven, above, by the frame's own signature —
2013
+ * and only its place in the canonical sequence falls back to the witness stream. Refusing
2014
+ * here would make the relay a precondition for reading mail, which is the thing this unit
2015
+ * was careful NOT to do.
2016
+ */
2017
+ this.#ctx.logger.info("session.content.ordering.absent", {
2018
+ agentName, sessionId, correlationId,
2019
+ impact: "this frame carried no relay ordering record, so its POSITION in the canonical sequence is not known from the frame and falls back to the witness stream. Its AUTHOR was verified from the frame's own signature.",
2020
+ });
2021
+ }
2022
+ // AC-001: carry the sender's correlationId from the frame into the receive
2023
+ // path so both sides log the same flow id (never re-minted on receipt).
2024
+ /**
2025
+ * DOD-M15-SEALWIRE-1 part B1 — the algorithm the sender named, taken from the FRAME.
2026
+ *
2027
+ * Read as `unknown` and passed through verbatim, deliberately: `resolveContentHashAlg` is the
2028
+ * one place that decides what a value means, and it distinguishes ABSENT (a peer predating
2029
+ * the field — verify as `sha256`) from a non-string or an unreadable name (refuse by name).
2030
+ * Coercing here would collapse that distinction and turn a version skew into a tamper report.
2031
+ */
2032
+ const declaredAlg = frame["content_hash_alg"];
2033
+ const ingest = await this.ingestReceivedContent(
2034
+ // THE DECRYPTED body — everything downstream (the hash cross-check, the leaf, the transcript,
2035
+ // the delivery buffer) works on plaintext, exactly as it did before this layer existed.
2036
+ agentName, sessionId, plaintextBody, contentHash, correlationId, framedSeq ?? undefined, declaredAlg === undefined ? undefined : declaredAlg, verifiedAuthorship, verifiedSignerUnmatched);
2037
+ // AC-001: after the content is durably ingested AND its hash cross-check
2038
+ // succeeds, emit an unsigned `persisted` delivery ACK back to the sender. A
2039
+ // rejected ingest (tamper / not-active) produces NO ACK, so the sender's TTF
2040
+ // path can park / recover.
2041
+ // DOD-MSG-4: a HELD (out-of-order) frame is NOT yet a durable leaf, so it is NOT
2042
+ // acknowledged `persisted` — the sender's TTF→park backstop then guarantees the
2043
+ // missing-earlier message is fetchable, and dedup absorbs the redundant copy.
2044
+ if (ingest.ok && !ingest.held) {
2045
+ /**
2046
+ * ─── THE SELF CHAIN ADVANCES INSIDE `ingestReceivedContent`, NOT HERE — `DOD-M15-SELFCHAIN-1`
2047
+ *
2048
+ * It was called here, and this is the live content-frame path only: a message recovered from
2049
+ * the relay mailbox never reached it. It moved down into the ingest's durable-append point,
2050
+ * which every route ends at — see the comment there for what that cost live.
2051
+ *
2052
+ * The reason it is not inside `#noteAcknowledgeable` still stands and is why it did not move
2053
+ * there: the acknowledgement is a (POSITION, content) pair needing the relay's number, so on
2054
+ * a session the relay never witnessed it is never written, while the self link needs no
2055
+ * position at all — it is one party's hash chain over their own messages.
2056
+ */
2057
+ /**
2058
+ * 033-ACKEMIT review F1 — ACKNOWLEDGE WHAT ARRIVED, HERE, not when the relay gets round to
2059
+ * delivering its copy back to us.
2060
+ *
2061
+ * Placed after a successful, non-held ingest deliberately: a HELD frame is not yet a durable
2062
+ * leaf and is not acknowledged `persisted` either, so claiming to have seen it would put a
2063
+ * position in our signed claim that our own record does not yet hold.
2064
+ *
2065
+ * `framedSeq` is the relay's canonical position taken from the sender's own signed ordering
2066
+ * record and verified before it got here. When it is absent the message arrived with no
2067
+ * ordering record — the withheld-submit case — and there is no position to acknowledge,
2068
+ * whatever we hold of the content. That limit is structural to a (position, content) pair
2069
+ * and it is what the carried-leaf follow-on closes.
2070
+ */
2071
+ if (framedSeq !== null) {
2072
+ this.noteAcknowledgeable(agentName, sessionId, framedSeq, contentHash);
2073
+ }
2074
+ else {
2075
+ /**
2076
+ * ─── WITNESS WHAT THEY DID NOT — 034-CARRYLEAF, and this is the line that closes
2077
+ * `DOD-M15-WITHHOLD-SEAL-1` ────────────────────────────────────────────────────────────
2078
+ *
2079
+ * No ordering record means the sender never asked the relay to witness this message. Two
2080
+ * things look identical from here: their relay was briefly unreachable, or they are
2081
+ * withholding it on purpose so it cannot appear in the receipt. **We do not need to tell
2082
+ * those apart, and that is the point** — the same action repairs both, and it costs the
2083
+ * honest case nothing.
2084
+ *
2085
+ * We hold their signature over their own bytes. So we hand it to the relay ourselves.
2086
+ */
2087
+ /**
2088
+ * ─── THE KIND COMES OFF THE FRAME, AND A FRAME WITHOUT ONE IS REFUSED ─────────────────
2089
+ *
2090
+ * A leaf kind selects a HASH DOMAIN — documents and rejection envelopes ride this same
2091
+ * frame — so witnessing under a guessed domain would put a wrong statement in the
2092
+ * canonical record.
2093
+ *
2094
+ * ⚠️ **THIS USED TO DECLINE TO WITNESS AND DELIVER THE MESSAGE ANYWAY, "because a peer
2095
+ * too old to send the field should be left alone". THAT SENTENCE WAS INHERITED, NOT
2096
+ * DERIVED, AND IT LEFT THE WHOLE ATTACK OPEN.** CELLO is alpha with no users; there is no
2097
+ * older peer to protect. What the leniency actually bought was an opt-out: emit the shape
2098
+ * a 2026-09-04 build emitted, and your message is delivered AND cannot be witnessed —
2099
+ * which is precisely the withholding this line exists to stop, reachable by anyone
2100
+ * willing to modify their client.
2101
+ *
2102
+ * So it is refused. Missing, malformed and mismatched take one path (§5), and a peer that
2103
+ * cannot say which domain its own leaf belongs to has supplied an unusable proof.
2104
+ */
2105
+ const framedKind = frame["leaf_kind"];
2106
+ if (typeof framedKind !== "number") {
2107
+ this.#ctx.refusals.refuseUnprovenAuthorship(agentName, sessionId, "authorship_proof_unusable", contentHash, {
2108
+ detail: "leaf_kind_absent",
2109
+ }, correlationId);
2110
+ return;
2111
+ }
2112
+ void this.witnessReceivedLeaf(agentName, sessionId, contentHash, s1Cbor, senderSig, framedKind, correlationId);
2113
+ }
2114
+ void sendDeliveryAck(this.#ctx, agentName, sessionId, contentHash, correlationId);
2115
+ }
2116
+ }
2117
+ catch (err) {
2118
+ this.#ctx.logger.warn("session.content.stream.read.failed", {
2119
+ sessionId,
2120
+ error: extractErrorMessage(err),
2121
+ });
2122
+ }
2123
+ finally {
2124
+ // `close()` waits only for OUR write buffer, which is empty here, so this cannot stall the
2125
+ // handler; it runs on every exit above, and there are several early returns.
2126
+ try {
2127
+ await stream.close();
2128
+ }
2129
+ catch (err) {
2130
+ // NOT SILENT. A close that fails here is the signature of the cap biting from the other
2131
+ // side, and it was the absence of exactly this line that turned the original diagnosis
2132
+ // into a 6,451-record log measurement.
2133
+ this.#ctx.logger.warn("session.content.stream.close.failed", {
2134
+ sessionId,
2135
+ error: extractErrorMessage(err),
2136
+ });
2137
+ try {
2138
+ stream.abort(err instanceof Error ? err : new Error(String(err)));
2139
+ }
2140
+ catch { /* already gone */ }
2141
+ return;
2142
+ }
2143
+ // OUR CLOSE ALONE DOES NOT FREE THE SLOT — the peer has to close its end too, and a peer
2144
+ // owns its own daemon. Without this, someone who opens content streams and never closes them
2145
+ // pins every inbound slot we have and puts us straight back into the defect above, with the
2146
+ // same unreadable error. `abort` resets unilaterally, so it works regardless of the peer;
2147
+ // the delay is what keeps it from landing while a well-behaved sender is still inside its
2148
+ // own `close()`. Unref'd so it can never hold the process open at shutdown, and tracked so
2149
+ // teardown can drop it.
2150
+ if (stream.status === "open" || stream.status === "closing") {
2151
+ const linger = setTimeout(() => {
2152
+ this.#ctx.lingeringStreams.delete(linger);
2153
+ if (stream.status !== "open" && stream.status !== "closing")
2154
+ return;
2155
+ this.#ctx.logger.debug("session.content.stream.linger.reset", { sessionId });
2156
+ try {
2157
+ stream.abort(new Error("inbound content stream not closed by peer"));
2158
+ }
2159
+ catch { /* already gone */ }
2160
+ }, CONTENT_STREAM_LINGER_MS);
2161
+ linger.unref?.();
2162
+ this.#ctx.lingeringStreams.add(linger);
2163
+ }
2164
+ }
2165
+ }
2166
+ }
2167
+ //# sourceMappingURL=session-content-ingest.js.map