@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,2998 @@
1
+ /**
2
+ * CELLO Daemon — SessionNodeManager
3
+ *
4
+ * Manages the lifecycle of all ephemeral session nodes:
5
+ * 1. Per-session nodes: fresh transport key + Peer ID, connectionGater allows
6
+ * only the designated counterparty. Created during cello_initiate_session
7
+ * (outbound) or cello_await_session (inbound, via standing receiver handoff).
8
+ * 2. Standing receiver node: pre-created, kept alive at all times, handed to the first inbound
9
+ * session and immediately replaced. Its gater is NOT open — `DOD-M15-ASSIGN-1` made it admit
10
+ * nobody inbound until a session offer names the dialer (see `standing-receivers.ts`). This
11
+ * line said "open gater" for as long as that was true and for a while after it was not.
12
+ * 3. 32-node cap: enforced before any new node is created.
13
+ * 4. Session status in the DB: active → sealed (on close) or interrupted
14
+ * (on graceful shutdown or SIGKILL-restart detection).
15
+ *
16
+ * Interrupted-session detection runs BEFORE the IPC socket opens, so a client cannot observe a
17
+ * stale 'active' row from a previous process.
18
+ */
19
+ import { CAPACITY_REASONS } from "./refusal-reasons.js";
20
+ import { ownSaltFrame } from "./session-salt-agreement.js";
21
+ import { CONTENT_ENCRYPTION_REASONS, CONTENT_ENCRYPTION_GUIDANCE } from "./content-encryption-status.js";
22
+ import { openEncryptedDatabase, resolveDbKey, dbKeyPathFor, } from "./sqlcipher-db.js";
23
+ import { migrateToEncryptedIfNeeded } from "./identity-migration.js";
24
+ import { ensureIdentitySchema } from "./db-identity-store.js";
25
+ import { TIER } from "./contacts-tier-migration.js";
26
+ import { normalizeContactPubkey } from "./contact-pubkey-case.js";
27
+ import { settableTierName, awayTierSettingKey, AWAY_DEFAULT_KEY } from "./agent-settings-keys.js";
28
+ import * as lp from "it-length-prefixed";
29
+ import { encodeCbor } from "@cello-protocol/protocol-types";
30
+ import { STANDING_RECEIVER_AGENT_NAME } from "./types.js";
31
+ import { CELLO_CONTENT_PROTOCOL_ID } from "@cello-protocol/transport";
32
+ // `PARK_ENVELOPE_REASONS` is deliberately NOT imported here. The reason codes are compared inside
33
+ // `park-envelope.ts` itself (`parkRefusalGuidance`) and asserted in its own test; this file only ever
34
+ // receives the already-classified `ParkAuthFailure`, so importing the code table here would invite a
35
+ // second, drifting copy of the classification logic.
36
+ import { decodeParkEnvelope } from "./park-envelope.js";
37
+ import { extractErrorMessage } from "./error-message.js";
38
+ import { AuthorshipVerifier } from "./authorship-verification.js";
39
+ import { InboundRefusals } from "./inbound-refusals.js";
40
+ import { SessionRecords } from "./session-records.js";
41
+ import { ParkRecovery } from "./park-recovery.js";
42
+ import { SessionSalts } from "./session-salts.js";
43
+ import { ensureSessionSchema } from "./session-schema.js";
44
+ import { SessionQueries } from "./session-queries.js";
45
+ import { RefusalNotices } from "./refusal-notices.js";
46
+ import { SessionEphemerals } from "./session-ephemerals.js";
47
+ import { SessionLiveness } from "./session-liveness.js";
48
+ import { WitnessAlerts } from "./witness-alerts.js";
49
+ import { HeldContent } from "./held-content.js";
50
+ import { SessionLeafRecords } from "./session-leaf-records.js";
51
+ import { SessionContentSender } from "./session-content-send.js";
52
+ import { SessionContentIngest } from "./session-content-ingest.js";
53
+ import { SessionSeal } from "./session-seal.js";
54
+ import { SessionRelay } from "./session-relay.js";
55
+ import { SessionLifecycle } from "./session-lifecycle.js";
56
+ import { sendDeliveryAck } from "./session-delivery-acks.js";
57
+ import { SESSION_CLOSED_REASON, sessionClosedState } from "./session-closed.js";
58
+ import { StandingReceivers } from "./standing-receivers.js";
59
+ import { storeRecoveredPosition } from "./recovered-position.js";
60
+ import { ChannelLogStore } from "./channel-log-store.js";
61
+ import { SessionOwnChainStore } from "./session-own-chain-store.js";
62
+ /**
63
+ * One row in an agent's witness-alert list — DOD-M15-CORROBORATE-1 review F1. Deduped on
64
+ * `(witness relay, session)`, so a repeated observation raises `occurrences` rather than taking
65
+ * another slot in a bounded list.
66
+ */
67
+ import { mergeRelayEndpoints } from "./relay-endpoints.js"; // directory pool beats saved rows
68
+ import { ABUSE_MAX_UNKNOWN_SESSIONS_GLOBAL, heldRelayIdsOf, PARKED_DRAIN_BACKSTOP_DEFAULT_MS, SALT_AGREEMENT_WAIT_MS, CONTENT_MAX_INBOUND_STREAMS, LEAF_FETCH_GRACE_MS } from "./session-node-types.js";
69
+ // Re-exported so this module's public surface is unchanged by the split: every existing
70
+ // importer of session-node-manager.js keeps working, and no test moves an import path.
71
+ export { ABUSE_MAX_SESSIONS_PER_UNKNOWN_SENDER, ABUSE_MAX_SESSION_RECEIVED_BYTES, ABUSE_MAX_UNKNOWN_SESSIONS_GLOBAL, CAP_INTERRUPTED_TTL_MS, LEAF_FETCH_GRACE_MS, MAX_TERMINAL_REFUSALS_PER_SESSION, RELAY_QUARANTINE_MS, REVIVAL_BOUND_SWEEP_MS, REVIVAL_WINDOW_MS, REVIVE_RESERVATION_CANDIDATES, REVIVE_RESERVATION_TIMEOUT_MS, SR_RESERVATION_MAX_RETRIES, TERMINAL_REFUSAL_READ_RETRY_MS, TERMINAL_REFUSAL_REASONS, } from "./session-node-types.js";
72
+ export class SessionNodeManager {
73
+ #factory;
74
+ #logger;
75
+ #dbPath;
76
+ #db = null;
77
+ /**
78
+ * DOD-COATTEND-1 (review F2): sessions whose RECEIVED transcript row failed to write, and are
79
+ * therefore holding content that can never be delivered. Read by `cello_receive` so the timeout
80
+ * answer names the local failure instead of telling the operator to keep waiting on a
81
+ * counterparty who already sent. Keyed (agent, session) → the leaf sequences that were lost.
82
+ */
83
+ #undeliverableSeqs = new Map();
84
+ /** RELAYSIG-1: shared immutable store of the relay's signed ordering-record receipts (keyed by agent). */
85
+ #relayReceiptStore = null;
86
+ /** FED-OPTIONB-SEAL-001: the per-session leaf log (both parties) carried at a unilateral seal. */
87
+ #sealLeafStore = null;
88
+ #channelLogStore = null; /** M16 007-PUBLOG: every artifact a local channel published, beside the seal-leaf log. */
89
+ getChannelLogStore() { return (this.#channelLogStore ??= new ChannelLogStore(this.getDb(), this.#logger)); }
90
+ /** `DOD-M15-SELFCHAIN-1` — this agent's own last message per session, so the next one links to it. */
91
+ #ownChainStore = null;
92
+ // M9-CORE-001: the inbound screening seam. Every byte that reaches the agent passes
93
+ // through `session-content-ingest.ts`'s appendVerifiedContent buffer write; screenInbound gates
94
+ // it there, on every
95
+ // arrival path (direct, held-release, recovered-park). Defaults to always-allow when no
96
+ // gateway is configured (SI-001: still a verdict, not an ungated pass).
97
+ #securityGateway;
98
+ #activeNodes = new Map();
99
+ // M7 DOD-SPINE-6 / MSG-001-3b: ONE relay witness client per AGENT (keyed by agent name).
100
+ // The relay authenticates and keys delivery by the agent's K_local pubkey, so all of an
101
+ // agent's sessions share one authenticated relay stream (each frame carries session_id).
102
+ #relayClients = new Map();
103
+ /**
104
+ * M12-P15: build a relay client for a session that has NO in-memory node.
105
+ *
106
+ * Injected by the composition root because it needs the agent's K_local and pubkey, which this
107
+ * manager deliberately does not hold. Only consulted on the detached seal path — an ACTIVE session
108
+ * always uses its own registered client.
109
+ */
110
+ /**
111
+ * DOD-M15-RELAYSLOTS-1: `onlineToken` is REQUIRED on the dependency bag, not optional. Every
112
+ * relay client needs the directory's token or the relay refuses it a reservation slot, and the
113
+ * failure is invisible from the client side — the agent comes up, reports itself online, and is
114
+ * reachable by nobody. Making it required means a call site that forgets to pass it is a type
115
+ * error rather than an agent that quietly stops being dialable.
116
+ */
117
+ #detachedRelayClientBuilder = null;
118
+ setDetachedRelayClientBuilder(fn) {
119
+ this.#detachedRelayClientBuilder = fn;
120
+ }
121
+ /**
122
+ * Review F7: relays that sent a witness alert this build could not read or verify, per agent.
123
+ * Peer id → cause + count. NO session and NO party: it reports that our witness layer is not
124
+ * working, never anything about a participant.
125
+ */
126
+ #witnessUnreadable = new Map();
127
+ /**
128
+ * DOD-M15-RELAYSLOTS-1: the last relay refusal per agent, with the advice that goes with it.
129
+ * Written where the refusal is actually known; read by whatever tells the operator.
130
+ */
131
+ #srRelayRefusal = new Map();
132
+ /**
133
+ * Why this agent's standing receiver could not hold a reservation, in words the person running it
134
+ * can act on — or null when the last attempt succeeded or none has been made.
135
+ *
136
+ * This is the surface DoD clause 7 is about: the assertion that matters is what the CLIENT can
137
+ * show someone, not what the relay wrote in its own log.
138
+ */
139
+ getStandingReceiverRefusal(agentName) {
140
+ return this.#srRelayRefusal.get(agentName) ?? null;
141
+ }
142
+ /**
143
+ * DOD-M15-RELAYSLOTS-1 review M1 — **replace the relay's guess with the directory's fact.**
144
+ *
145
+ * The relay can only say "no token was presented"; the DIRECTORY knows why there was none, and
146
+ * the two most useful answers point somewhere the relay's own advice does not. Left alone,
147
+ * `online_token_required` tells the operator to check that their agent is reaching a directory —
148
+ * which, in the `not_registered_here` case, it plainly is.
149
+ */
150
+ #withDirectoryCause(agentName, refusal) {
151
+ if (refusal.reason !== "online_token_required")
152
+ return refusal;
153
+ const absent = this.#directoryOnlineTokenAbsent.get(agentName);
154
+ if (absent === "not_registered_here") {
155
+ return {
156
+ ...refusal,
157
+ advice: "The directory this agent is connected to holds no profile for its key, so it " +
158
+ "issued no token and no relay will grant a reservation. The directory connection itself " +
159
+ "is fine — either this agent registered against a different sovereign node and its " +
160
+ "profile has not replicated here yet, or it is not registered at all.",
161
+ };
162
+ }
163
+ if (absent === "issue_failed") {
164
+ return {
165
+ ...refusal,
166
+ advice: "The directory could not issue this agent a token — its own lookup or signing " +
167
+ "failed. That is a fault on the directory, not on this agent or on any relay; it usually " +
168
+ "clears on the next connection.",
169
+ };
170
+ }
171
+ return refusal;
172
+ }
173
+ // DOD-LOOP-1: the standing receiver is PER-AGENT, not per-daemon. A daemon hosting two agents
174
+ // (the loopback case) needs each agent to have its OWN inbound receiver node — otherwise the
175
+ // initiator (consuming its agent's standing receiver) and the responder (consuming its agent's)
176
+ // would contend for a single node and thrash. Keyed by agentName. A creation-in-flight guard set
177
+ // prevents two concurrent ensure() calls from building two nodes for the same agent.
178
+ // `relayPeerIds`: the relays this receiver holds an announced circuit through. The watchdog reads
179
+ // it as a COUNT — zero tells "never had one" (already degraded, and already loud) apart from a
180
+ // loss, and a drop that leaves it non-empty is a lost relay the agent can absorb without being
181
+ // rebuilt. See `#reservationWatchdogTick` in `session-relay.ts`.
182
+ /**
183
+ * DOD-M12B-SESSION-SEED-1 — session id → the transport seed its node identity derives from.
184
+ *
185
+ * **DELIBERATELY NOT ON `ActiveSessionEntry`.** That entry is deleted the instant a session is
186
+ * interrupted (`markInterruptedWithDetails` and `destroySessionNode` both do it), which is exactly
187
+ * the moment the seed becomes necessary — storing it there would destroy it precisely when the
188
+ * session needs to come back. It has to outlive the NODE without outliving the SESSION.
189
+ *
190
+ * Andre's 2026-08-18 tenet is the lifetime rule: *"it should be possible to revive that session on
191
+ * those peer IDs. But after that, those peer IDs and that peer connection needs to be shut down."*
192
+ * So this map is cleared in the same step that writes a terminal status (`SessionLifecycle.updateSessionStatus`),
193
+ * not on a later sweep — and the bytes are zeroed before the reference is dropped, because a seed
194
+ * that stays readable in the heap is exactly the "left open" the tenet forbids.
195
+ *
196
+ * It holds the counterparty's session peer id alongside the seed, and not for convenience: that
197
+ * value lives ONLY on `ActiveSessionEntry` and is destroyed with it on interruption, so without a
198
+ * copy here we could rebuild our own identity and still not know who to let back in. Both halves
199
+ * of the revival have the same lifetime, so they are destroyed in one step.
200
+ *
201
+ * In memory only, never persisted. A daemon restart genuinely destroys these identities, which is
202
+ * why restart is `RESTART-SEAL-1`'s case (resolve with a receipt) and not a revival case.
203
+ */
204
+ #sessionSeeds = new Map();
205
+ /** DOD-M12B-SESSION-SEED-1: see setRetryDrainHook — fired when a session is revived. */
206
+ #retryDrainHook = null;
207
+ /**
208
+ * 036-GODFILE Part 1 — WHO SENT THIS, AND DID THEY SEE WHAT THEY CLAIM.
209
+ *
210
+ * It owns the two maps it maintains (`#receivedFromCounterparty`, `#lastFromCounterparty`), which
211
+ * nothing else in the daemon reads. Everything else it needs is listed explicitly in the context
212
+ * object rather than handed over as `this` — the manager's private state stays private, and a
213
+ * reader can see the whole of what authorship verification is allowed to touch in one place.
214
+ *
215
+ * ⚠️ BUILT IN THE CONSTRUCTOR, not as a field initializer. Field initializers run in declaration
216
+ * order, and this one closes over `#logger`, so declaring it above `#logger` made it read an
217
+ * uninitialized field — caught by tsc, and silent at runtime if it had been assigned later.
218
+ */
219
+ #authorship;
220
+ /**
221
+ * 036-GODFILE Parts 3+4 — everything that happens to an inbound message we will NOT deliver.
222
+ *
223
+ * It owns the five remote-fed maps it bounds. The manager used to clear them by hand in cache
224
+ * eviction, which is why they are gone from here: a caller that had to know five field names
225
+ * in order to forget a session knew too much, and a sixth would have been missed in silence.
226
+ */
227
+ #refusals;
228
+ /**
229
+ * 036-GODFILE — contacts and tiers, agent settings, the transcript, and session divergence.
230
+ *
231
+ * Everything the daemon REMEMBERS about a conversation, as opposed to what it does with a
232
+ * message in flight. Its context is five items, which is why it was a seam.
233
+ */
234
+ #records;
235
+ /**
236
+ * 036-GODFILE Part 6 — the relay store-and-forward mailbox: depositing content the counterparty
237
+ * could not take live, recovering it when they return, and the backstop sweep that drains it
238
+ * even when a trigger is missing.
239
+ */
240
+ #park;
241
+ /**
242
+ * 037-SESSIONCORE Unit 1 — the per-session content salt, and the eleven maps it maintains.
243
+ *
244
+ * Those eleven were the largest single block of the twenty-four per-session containers that
245
+ * `#evictSessionCaches` cleared by hand. They leave with the code that fills them.
246
+ */
247
+ #salts;
248
+ /**
249
+ * 037-SESSIONCORE — the methods that are SQL and nothing else.
250
+ *
251
+ * Grouped by SHAPE rather than subject: "holds no state, calls nothing but the agent-id lookup,
252
+ * and actually touches the database" is a property a reader can check on any one of them
253
+ * without knowing which subsystem it serves.
254
+ *
255
+ * ⚠️ THE `touches the database` CLAUSE IS LOAD-BEARING. A first pass grouped on "holds no state"
256
+ * alone and swept in `#evictPeersOutsideGate` (`session-lifecycle.ts`), which operates on libp2p connections and is not
257
+ * SQL at all. A source-scanning parity test caught the mis-grouping; the filter now requires the
258
+ * method to name `#db`.
259
+ */
260
+ #queries;
261
+ /**
262
+ * 037-SESSIONCORE — the refusals an operator can actually see.
263
+ *
264
+ * A refusal whose only consumer is the log is not a control: from the receiving operator's
265
+ * chair the conversation just goes quiet, and they conclude the other person stopped replying.
266
+ */
267
+ #notices;
268
+ /**
269
+ * 037-SESSIONCORE — the throwaway per-session keypair and the content key its halves agree.
270
+ *
271
+ * It owns the four maps that hold the secrets, which is the right place for them: the teardown
272
+ * that ZEROES them lives beside the code that mints them, rather than being one line in a
273
+ * cache-eviction method that has to remember every map in the class.
274
+ */
275
+ #ephemerals;
276
+ /**
277
+ * 037-SESSIONCORE — connection liveness, and the impairment that explains a degraded session.
278
+ */
279
+ #liveness;
280
+ /**
281
+ * 037-SESSIONCORE — what a relay reported seeing, when it does not reconcile with our record.
282
+ */
283
+ #witness;
284
+ /**
285
+ * 037-SESSIONCORE — content we have verified but cannot show yet, because it arrived ahead of
286
+ * a gap. The MAP stays shared: ingest, placeOwnLeaf and sealReadiness all read it, and two
287
+ * sources of truth for "what are we holding" would be worse than one shared one.
288
+ */
289
+ #held;
290
+ /**
291
+ * 037-SESSIONCORE — the two durable facts about a session's leaves: where the chain starts
292
+ * (the genesis prev-root) and which leaves the directory attested.
293
+ */
294
+ #leafRecords;
295
+ /**
296
+ * The hot path, in two halves — the message going out, and the message coming in. Together they
297
+ * are the single largest piece that left this class.
298
+ *
299
+ * They are separate because they share almost nothing: outbound reaches nothing inbound at all,
300
+ * and inbound reaches exactly one thing outbound (settling the acknowledgement for a message we
301
+ * sent, which arrives on the stream the receiver is already reading). The seven methods further
302
+ * down that delegate to them stay on this class because 451 call sites across the daemon and its
303
+ * tests name them here; the rest of their surface is reached as `#contentIn.…` / `#contentOut.…`.
304
+ */
305
+ #contentOut;
306
+ #contentIn;
307
+ /**
308
+ * Closing a conversation and proving it closed — readiness, the SEAL leaf, the carry, the
309
+ * certificate, and the auto-acknowledgement gate. Eleven of its methods keep delegators below
310
+ * because 86 call sites outside this class name them on the manager.
311
+ */
312
+ #seal;
313
+ /**
314
+ * Talking to the blind witness: connecting a session to its relay, proving key possession, holding
315
+ * and renewing the circuit reservation that keeps this agent reachable behind a NAT, quarantining
316
+ * a relay that misbehaves, and detaching cleanly.
317
+ */
318
+ #relay;
319
+ /**
320
+ * A session's life: opened, accepted, connected, revived on the peer id the counterparty still
321
+ * holds, moved between statuses, and torn down. The last of the four paths to leave this class.
322
+ */
323
+ #life;
324
+ // CELLO-M7-TRANSPORT-001: the directory-node multiaddrs serving as AutoNAT probers (SI-002).
325
+ // Empty () => [] when the directory is in 'reconnecting' state — AutoNAT cannot run and
326
+ // dialability stays the conservative default.
327
+ #autoNatProbers;
328
+ /**
329
+ * 037-SESSIONCORE — the always-listening node, one per agent.
330
+ *
331
+ * Its maps are passed BY REFERENCE rather than moved: the reservation watchdog and the relay
332
+ * paths still read them and stayed behind, and two sources of truth for "which agents have a
333
+ * receiver" would be worse than one shared one.
334
+ */
335
+ #receivers;
336
+ /** ─── DELEGATORS — the standing-receiver API other files call ───────────────────────── */
337
+ standingReceiverAbsenceReason(agentName) { return this.#receivers.standingReceiverAbsenceReason(agentName); }
338
+ getStandingReceiverInfo(agentName) { return this.#receivers.getStandingReceiverInfo(agentName); }
339
+ getStandingReceiverReady(agentName) { return this.#receivers.getStandingReceiverReady(agentName); }
340
+ getStandingReceiverNode(agentName) { return this.#receivers.getStandingReceiverNode(agentName); }
341
+ getStandingReceiverReachability(agentName) { return this.#receivers.getStandingReceiverReachability(agentName); }
342
+ getStandingReceiverAutoNat() { return this.#receivers.getStandingReceiverAutoNat(); }
343
+ getStandingReceiverAllowedPeer(agentName) { return this.#receivers.getStandingReceiverAllowedPeer(agentName); }
344
+ admitOfferedDialer(a, p, sid) { return this.#receivers.admitOfferedDialer(a, p, sid); }
345
+ takeReservationForSession(a, c, cid, offerSid) { return this.#receivers.takeReservationForSession(a, c, cid, offerSid); } // 055-ONDEMAND
346
+ // 056-SLOTDEAD — the RE-TAKE, for a live session whose circuit died. Exposed so the outage-and-
347
+ // recovery path can be driven end to end; the offer path alone is the wrong half of the pair.
348
+ retakeReservationOn(a, node, c, cid) { return this.#receivers.retakeReservationOn(a, node, c, cid); }
349
+ getStandingReceiverRelayIds(a) { return [...(this.#standingReceivers.get(a)?.relayPeerIds ?? [])]; } // 055-ONDEMAND
350
+ /** 055-ONDEMAND — a live session's own node. Its circuit is not the receiver's; see the release path. */
351
+ getSessionNodeForTest(a, sid) { return this.#activeNodes.get(this.#k(a, sid))?.node ?? null; }
352
+ getOfferedDialer(agentName, sessionIdHex) { return this.#receivers.getOfferedDialer(agentName, sessionIdHex); }
353
+ clearOfferedDialer(agentName, sessionIdHex) { return this.#receivers.clearOfferedDialer(agentName, sessionIdHex); }
354
+ revokeOfferedDialer(agentName, sessionIdHex, offeredPeerId) { return this.#receivers.revokeOfferedDialer(agentName, sessionIdHex, offeredPeerId); }
355
+ /** ─── DELEGATORS — the leaf-record API other files call ─────────────────────────────── */
356
+ recordSessionGenesis(agentName, sessionId, participantA, participantB, sessionTimestamp, sessionSignature, relayAnchorHex) { return this.#leafRecords.recordSessionGenesis(agentName, sessionId, participantA, participantB, sessionTimestamp, sessionSignature, relayAnchorHex); }
357
+ /** 069-ORDERPROOF: the relay key this session's ordering attestations are verified against. */
358
+ sessionRelayAnchor(agentName, sessionId) { return this.#leafRecords.sessionRelayAnchor(agentName, sessionId); }
359
+ persistRelayAnchor(agentName, sessionId, relayAnchorHex) { return this.#leafRecords.persistRelayAnchor(agentName, sessionId, relayAnchorHex); }
360
+ setSessionGenesisForTest(agentName, sessionId, genesis) { return this.#leafRecords.setSessionGenesisForTest(agentName, sessionId, genesis); }
361
+ recordCertifiedLeafSet(agentName, sessionId, signedLeaves, sealedRootHex, correlationId) { return this.#leafRecords.recordCertifiedLeafSet(agentName, sessionId, signedLeaves, sealedRootHex, correlationId); }
362
+ noteCertifiedLeafSetUnavailable(agentName, sessionId, state, detail) { return this.#leafRecords.noteCertifiedLeafSetUnavailable(agentName, sessionId, state, detail); }
363
+ getCertifiedLeafSet(agentName, sessionId) { return this.#leafRecords.getCertifiedLeafSet(agentName, sessionId); }
364
+ getCertifiedLeafSetState(agentName, sessionId) { return this.#leafRecords.getCertifiedLeafSetState(agentName, sessionId); }
365
+ /** ─── DELEGATORS — the held-content seams other files call ──────────────────────────── */
366
+ holdOwnLeafForTest(agentName, sessionId, canonicalSeq, contentHashHex) { return this.#held.holdOwnLeafForTest(agentName, sessionId, canonicalSeq, contentHashHex); }
367
+ /** ─── DELEGATORS — the witness-alert API other files call ────────────────────────────── */
368
+ recordRelayWitnessAlert(agentName, alert) { return this.#witness.recordRelayWitnessAlert(agentName, alert); }
369
+ getWitnessAlerts(agentName) { return this.#witness.getWitnessAlerts(agentName); }
370
+ witnessAlertsTruncated(agentName) { return this.#witness.witnessAlertsTruncated(agentName); }
371
+ /** ─── DELEGATORS — the liveness API other files call, unchanged by the split ───────────── */
372
+ getSessionLiveness(agentName, sessionId) { return this.#liveness.getSessionLiveness(agentName, sessionId); }
373
+ getSessionImpairment(agentName, sessionId) { return this.#liveness.getSessionImpairment(agentName, sessionId); }
374
+ markSessionLivenessForTest(agentName, sessionId, state) { return this.#liveness.markSessionLivenessForTest(agentName, sessionId, state); }
375
+ /** ─── DELEGATORS — the test seams other files call, unchanged by the split ─────────────── */
376
+ mintSessionEphemeralForTest(agentName, sessionId) { return this.#ephemerals.mintSessionEphemeralForTest(agentName, sessionId); }
377
+ sessionEphemeralPublicForTest(agentName, sessionId) { return this.#ephemerals.sessionEphemeralPublicForTest(agentName, sessionId); }
378
+ setSessionEphemeralForTest(agentName, sessionId, ephemeral) { return this.#ephemerals.setSessionEphemeralForTest(agentName, sessionId, ephemeral); }
379
+ setSessionContentKeyForTest(agentName, sessionId, key) { return this.#ephemerals.setSessionContentKeyForTest(agentName, sessionId, key); }
380
+ forgetSessionContentKeyForTest(agentName, sessionId) { return this.#ephemerals.forgetSessionContentKeyForTest(agentName, sessionId); }
381
+ async signOwnEphemeralForTest(agentName, sessionId) { return this.#ephemerals.signOwnEphemeralForTest(agentName, sessionId); }
382
+ async handleEphemeralFrameForTest(agentName, sessionId, frame, correlationId = "test") { return this.#ephemerals.handleEphemeralFrameForTest(agentName, sessionId, frame, correlationId); }
383
+ /**
384
+ * ─── DELEGATORS — the refusal-notice API other files call, unchanged by the split ──────────
385
+ *
386
+ * ⚠️ **REFUSALS LOGGED AND FILED NOTHING, AND THAT IS WHY THIS API EXISTS.** The encryption gate's
387
+ * three causes each carried a good `impact` and `guidance` at ERROR and none of them called
388
+ * `noteContentRefusal`, so the sentences an operator needed were in a file they have no reason to
389
+ * open. From their chair a message never arrived and the conversation went quiet — the exact defect
390
+ * `DOD-M15-NO-SILENT-REFUSAL-1` was built to end, three checks above the one that respected it.
391
+ *
392
+ * Both surfaces, always: the ERROR is the durable forensic record an investigation reads days
393
+ * later, and the notice is the control — the thing that actually reaches the person.
394
+ */
395
+ noteContentRefusal(agentName, sessionId, reason, detail) { return this.#notices.noteContentRefusal(agentName, sessionId, reason, detail); }
396
+ takeContentRefusals(agentName, sessionId, consumerId) { return this.#notices.takeContentRefusals(agentName, sessionId, consumerId); }
397
+ dismissContentRefusals(agentName, sessionId) { return this.#notices.dismissContentRefusals(agentName, sessionId); }
398
+ takeAgentContentRefusals(agentName, consumerId) { return this.#notices.takeAgentContentRefusals(agentName, consumerId); }
399
+ /**
400
+ * ─── DELEGATORS — the query API other files call, unchanged by the split ────────────────────
401
+ */
402
+ listExpiredUnrevivableSessions(nowMs, windowMs) { return this.#queries.listExpiredUnrevivableSessions(nowMs, windowMs); }
403
+ listRestartOrphanedSessions() { return this.#queries.listRestartOrphanedSessions(); }
404
+ readQuarantined(agentName, sessionId, sequence) { return this.#queries.readQuarantined(agentName, sessionId, sequence); }
405
+ getPinnedCounterpartyPrimary(agentName, counterpartyPubkeyHex) { return this.#queries.getPinnedCounterpartyPrimary(agentName, counterpartyPubkeyHex); }
406
+ recordSealedAnnex(agentName, sessionId, contentHashHex, content, senderPubkeyHex) { return this.#queries.recordSealedAnnex(agentName, sessionId, contentHashHex, content, senderPubkeyHex); }
407
+ findNextReceivedAfter(agentName, sessionId, afterSeq) { return this.#queries.findNextReceivedAfter(agentName, sessionId, afterSeq); }
408
+ getSealInterruptedArtifacts(agentName, sessionId) { return this.#queries.getSealInterruptedArtifacts(agentName, sessionId); }
409
+ getSessionsByStatus(status) { return this.#queries.getSessionsByStatus(status); }
410
+ recordRefusedSession(agentName, sessionId, reason) { return this.#queries.recordRefusedSession(agentName, sessionId, reason); }
411
+ getAgentRelayEndpoints(agentName) { return mergeRelayEndpoints(this.#directoryRelayEndpoints.get(agentName), this.#queries.getAgentRelayEndpoints(agentName)); }
412
+ countActiveSessionsFromUnknownSenders(agentName) { return this.#queries.countActiveSessionsFromUnknownSenders(agentName); }
413
+ getSealCertificate(agentName, sessionId) { return this.#queries.getSealCertificate(agentName, sessionId); }
414
+ setSessionName(agentName, sessionId, sessionName) { return this.#queries.setSessionName(agentName, sessionId, sessionName); }
415
+ getPersistedRelayEndpoint(agentName, sessionId) { return this.#queries.getPersistedRelayEndpoint(agentName, sessionId); }
416
+ markRestartSealGaveUp(agentName, sessionId, reason) { return this.#queries.markRestartSealGaveUp(agentName, sessionId, reason); }
417
+ dismissSession(agentName, sessionId) { return this.#queries.dismissSession(agentName, sessionId); }
418
+ agentNameForId(agentId) { return this.#queries.agentNameForId(agentId); }
419
+ getRenameNotices(agentName) { return this.#queries.getRenameNotices(agentName); }
420
+ sessionsConsumingCap(agentName, counterpartyPubkey, limit = 10) { return this.#queries.sessionsConsumingCap(agentName, counterpartyPubkey, limit); }
421
+ advanceLastDeliveredSeq(agentName, sessionId, seq) { return this.#queries.advanceLastDeliveredSeq(agentName, sessionId, seq); }
422
+ countActiveSessionsForCounterparty(agentName, counterpartyPubkey) { return this.#queries.countActiveSessionsForCounterparty(agentName, counterpartyPubkey); }
423
+ getSessionRecord(agentName, sessionId) { return this.#queries.getSessionRecord(agentName, sessionId); }
424
+ hasDatabase() { return this.#queries.hasDatabase(); }
425
+ readSealedAnnex(agentName, sessionId) { return this.#queries.readSealedAnnex(agentName, sessionId); }
426
+ counterpartyAbandonedAt(agentName, sessionId) { return this.#queries.counterpartyAbandonedAt(agentName, sessionId); }
427
+ getSealedRootHex(agentName, sessionId) { return this.#queries.getSealedRootHex(agentName, sessionId); }
428
+ getLastDeliveredSeq(agentName, sessionId) { return this.#queries.getLastDeliveredSeq(agentName, sessionId); }
429
+ markSessionsInterruptedByLocalShutdownForTest() { return this.#queries.markSessionsInterruptedByLocalShutdownForTest(); }
430
+ wasSessionRefused(agentName, sessionId) { return this.#queries.wasSessionRefused(agentName, sessionId); }
431
+ countReceivedMessages(agentName, sessionId) { return this.#queries.countReceivedMessages(agentName, sessionId); }
432
+ markInterruptedByCounterpartyForTest(agentName, sessionId) { return this.#queries.markInterruptedByCounterpartyForTest(agentName, sessionId); }
433
+ /**
434
+ * M7-SESSION-004 (AC-005): persist the seal certificate's legibility object with the
435
+ * sealed record. Stored as a JSON string (hex-encoded pubkeys) so it round-trips a
436
+ * daemon restart and is returned intact on the cert-read surface. The caller normalises
437
+ * the raw wire legibility (Uint8Array pubkeys) into a JSON-safe shape before storing.
438
+ * Best-effort: a session row may not yet exist (the seal arrived before the row was
439
+ * persisted); in that case we no-op rather than throw — the cert still flows through the
440
+ * live return path. The legibility content is identical regardless of delivery timing.
441
+ *
442
+ * ⚠️ THIS BLOCK WAS STRANDED WHEN THE METHOD BECAME A DELEGATOR. It stayed behind and ended up
443
+ * stacked on top of `markSealed`'s own doc block, so that method showed two descriptions and the
444
+ * first one described a different method entirely — and the seal split then carried it into
445
+ * `session-seal.ts`, where `recordSealCertificate` does not appear at all. Returned to the call
446
+ * it describes. The row-writing itself lives in `session-queries.ts`.
447
+ */
448
+ recordSealCertificate(agentName, sessionId, sealedRootHex, legibilityJson) { return this.#queries.recordSealCertificate(agentName, sessionId, sealedRootHex, legibilityJson); }
449
+ recordCounterpartyPrimary(agentName, sessionId, primaryPubkeyHex) { return this.#queries.recordCounterpartyPrimary(agentName, sessionId, primaryPubkeyHex); }
450
+ /**
451
+ * ─── DELEGATORS — the salt API other files call, unchanged by the split ─────────────────────
452
+ *
453
+ * `daemon.ts` and `session-content-handlers.ts` still say `manager.contentHashForSession(...)`.
454
+ * Keeping the surface identical is what lets the suite stand as evidence that this was a
455
+ * restructure and not a rewrite.
456
+ */
457
+ setSaltContributionForTest(agentName, sessionId, contribution) { return this.#salts.setSaltContributionForTest(agentName, sessionId, contribution); }
458
+ forgetSaltContributionForTest(agentName, sessionId) { return this.#salts.forgetSaltContributionForTest(agentName, sessionId); }
459
+ async contentHashForSession(agentName, sessionId, content) { return this.#salts.contentHashForSession(agentName, sessionId, content); }
460
+ abandonUnsaltedHash(agentName, sessionId) { return this.#salts.abandonUnsaltedHash(agentName, sessionId); }
461
+ isContentSaltActive(agentName, sessionId) { return this.#salts.isContentSaltActive(agentName, sessionId); } // `expectSaltAgreement` is PRODUCTION (DOD-M15-AWAYSALT-1 — the away ack calls it before hashing);
462
+ // it also arms the teardown-must-settle regression seam (037-SESSIONCORE) below.
463
+ expectSaltAgreement(agentName, sessionId) { return this.#salts.markSaltPending(agentName, sessionId); }
464
+ saltForHashingForTest(agentName, sessionId) { return this.#salts.saltForHashing(agentName, sessionId); }
465
+ /**
466
+ * ─── DELEGATORS — the mailbox API other files call, unchanged by the split ───────────────────
467
+ *
468
+ * `content-park.ts` and `daemon.ts` still say `manager.recoverParkedEntry(...)` and
469
+ * `manager.setContentParkHook(...)`. Keeping the surface identical is what lets the full suite
470
+ * stand as evidence that this was a move: a caller that had to change would mean the contract
471
+ * moved with it.
472
+ */
473
+ injectParkFault(count, cause) { return this.#park.injectParkFault(count, cause); }
474
+ getParkFaultRemaining() { return this.#park.getParkFaultRemaining(); }
475
+ setContentParkHook(fn) { return this.#park.setContentParkHook(fn); }
476
+ setParkedDrainHook(fn) { return this.#park.setParkedDrainHook(fn); }
477
+ recoverOwnSealCtrlLeafForTest(agentName, sessionId) { return this.#park.recoverOwnSealCtrlLeafForTest(agentName, sessionId); }
478
+ async recoverParkedEntry(agentName, sessionId, recipientPubkey, unsealed, contentHash, correlationId) { return this.#park.recoverParkedEntry(agentName, sessionId, recipientPubkey, unsealed, contentHash, correlationId); }
479
+ /**
480
+ * ─── DELEGATORS — the class API is unchanged by the split, deliberately ──────────────────────
481
+ *
482
+ * Every method below moved into `session-records.ts` with its implementation and its comments.
483
+ * These one-liners exist so that no CALLER had to change: `contact-handlers.ts`, the IPC surface
484
+ * and the tests all still say `manager.getTier(...)`. That is what lets the full suite stand as
485
+ * the evidence that this was a move and not a rewrite — a test that had to change would have
486
+ * meant behaviour moved (Rule B).
487
+ *
488
+ * They are the price of the split and they are the cheap half: one line each, no logic, and the
489
+ * prose that explains each rule lives beside the code that enforces it.
490
+ */
491
+ isContact(agentName, pubkey) { return this.#records.isContact(agentName, pubkey); }
492
+ getTier(agentName, pubkey) { return this.#records.getTier(agentName, pubkey); }
493
+ resolveTierBound(agentName, tier, field) { return this.#records.resolveTierBound(agentName, tier, field); }
494
+ addContact(agentName, pubkey, moniker, provenance, tier = TIER.UNKNOWN) { return this.#records.addContact(agentName, pubkey, moniker, provenance, tier); }
495
+ setContactMoniker(agentName, pubkey, moniker) { return this.#records.setContactMoniker(agentName, pubkey, moniker); }
496
+ setContactSignalPref(agentName, pubkey, signalHash, present) { return this.#records.setContactSignalPref(agentName, pubkey, signalHash, present); }
497
+ getContactSignalPrefs(agentName, pubkey) { return this.#records.getContactSignalPrefs(agentName, pubkey); }
498
+ setContactAwayMessage(agentName, pubkey, message) { return this.#records.setContactAwayMessage(agentName, pubkey, message); }
499
+ setContactTier(agentName, pubkey, tier) { return this.#records.setContactTier(agentName, pubkey, tier); }
500
+ recordOfferedMoniker(agentName, pubkey, offered) { return this.#records.recordOfferedMoniker(agentName, pubkey, offered); }
501
+ removeContact(agentName, pubkey) { return this.#records.removeContact(agentName, pubkey); }
502
+ getContactMoniker(agentName, pubkey) { return this.#records.getContactMoniker(agentName, pubkey); }
503
+ listContacts(agentName) { return this.#records.listContacts(agentName); }
504
+ clearRenameNotice(agentName, pubkey) { return this.#records.clearRenameNotice(agentName, pubkey); }
505
+ clearPinnedCounterpartyPrimary(agentName, counterpartyPubkeyHex) { return this.#records.clearPinnedCounterpartyPrimary(agentName, counterpartyPubkeyHex); }
506
+ getTelegramSettings() { return this.#records.getTelegramSettings(); }
507
+ setTelegramSettings(botToken, allowlistedChatId) { return this.#records.setTelegramSettings(botToken, allowlistedChatId); }
508
+ getSetting(agentName, key) { return this.#records.getSetting(agentName, key); }
509
+ deleteSetting(agentName, key) { return this.#records.deleteSetting(agentName, key); }
510
+ setSetting(agentName, key, value) { return this.#records.setSetting(agentName, key, value); }
511
+ getAllSettings(agentName) { return this.#records.getAllSettings(agentName); }
512
+ recordRelayWitnessUnreadable(agentName, relayPeerId, why) { return this.#records.recordRelayWitnessUnreadable(agentName, relayPeerId, why); }
513
+ getWitnessUnreadable(agentName) { return this.#records.getWitnessUnreadable(agentName); }
514
+ recordTranscriptMessage(agentName, sessionId, sequence, direction, plaintext, correlationId, authorship, quarantineReason, senderPubkeyHexOverride) { return this.#records.recordTranscriptMessage(agentName, sessionId, sequence, direction, plaintext, correlationId, authorship, quarantineReason, senderPubkeyHexOverride); }
515
+ readTranscript(agentName, sessionId) { return this.#records.readTranscript(agentName, sessionId); }
516
+ getUnreadSummary(agentName) { return this.#records.getUnreadSummary(agentName); }
517
+ getEndedUnread(agentName) { return this.#records.getEndedUnread(agentName); }
518
+ getUnreadReceivedCount(agentName, sessionId) { return this.#records.getUnreadReceivedCount(agentName, sessionId); }
519
+ markSessionDiverged(agentName, sessionId) { return this.#records.markSessionDiverged(agentName, sessionId); }
520
+ /**
521
+ * DOD-M15-CLOSEDSESSION-1 — has THIS side committed its half of the seal? The state a status row
522
+ * cannot show; the reasoning is in `session-closed.ts`.
523
+ *
524
+ * ⚠️ THE MARK IS MEMORY-ONLY, so the durable evidence is the second half of the answer (review
525
+ * F7). `#responderSealSubmitted` is cleared on teardown, so a daemon that stopped between the
526
+ * ctrl-leaf submit and the ceremony's status write comes back with an `active` row and no mark —
527
+ * and the original defect reproduces on the first send. Our own SEAL ctrl leaf is in
528
+ * `session_seal_leaves`, which is what the seal path itself consults before submitting a second.
529
+ *
530
+ * `"unknown"` — the read FAILED — is deliberately not treated as closed. It is the answer a
531
+ * broken database gives, and wedging every live session shut on it would be a far worse failure
532
+ * than the window it would close.
533
+ */
534
+ hasCommittedSealLeaf(agentName, sessionId) {
535
+ if (this.#responderSealSubmitted.has(this.#k(agentName, sessionId)))
536
+ return true;
537
+ const durable = this.#park.recoverOwnSealCtrlLeaf(agentName, sessionId);
538
+ return durable !== "none" && durable !== "unknown";
539
+ }
540
+ /** DOD-M15-CLOSEDSESSION-1 test seam: commit this side's half with no relay, by marking the REAL map production reads. */
541
+ markSealLeafCommittedForTest(agentName, sessionId) { this.#responderSealSubmitted.set(this.#k(agentName, sessionId), null); }
542
+ isSessionDiverged(agentName, sessionId) { return this.#records.isSessionDiverged(agentName, sessionId); }
543
+ /** DOD-M12B-LEAF-TRIGGERS-FETCH-1: content hashes this session has actually resolved — ingested,
544
+ * held, or authored by us. A witnessed leaf whose hash is in here needs no fetch. */
545
+ #resolvedContent = new Map();
546
+ /** In-flight grace timers, keyed session+hash, so a redelivered leaf does not schedule a second
547
+ * fetch for the same content — a slow relay must not be turned into a storm against itself. */
548
+ #leafFetchTimers = new Map();
549
+ /** Test seam: collapse the grace window so a test does not have to wait two real seconds. The
550
+ * window itself is covered by its own case. */
551
+ #leafFetchGraceMs = LEAF_FETCH_GRACE_MS;
552
+ #standingReceivers = new Map();
553
+ #standingReceiverCreating = new Set();
554
+ // M8B F14: agents that SHOULD have a standing receiver — marked by
555
+ // ensureStandingReceiverForAgent (cello_start_agent / the inbound accept path) and
556
+ // unmarked by removeStandingReceiverForAgent (cello_set_agent_offline). Consulted by the
557
+ // teardown re-arm so a session-node teardown never re-arms an offline agent.
558
+ #agentsWantingReceiver = new Set();
559
+ // M8B F14: standing-receiver create retry schedule (see constructor opts).
560
+ #srRetryDelaysMs;
561
+ /** DOD-NAT-REACHABILITY-1: reservation deadline — see #startReceiverNode. */
562
+ #srReservationTimeoutMs;
563
+ /** DOD-NAT-REACHABILITY-1: watchdog for a SILENTLY lost reservation. */
564
+ #srWatchdogIntervalMs;
565
+ #srReservationRetryMs;
566
+ /**
567
+ * DOD-M12B-RESERVATION-RETRY-1: per-agent re-attempt state for a receiver the relay refused a
568
+ * reservation to. `attempts` counts RETRIES, not the creation attempt.
569
+ */
570
+ /** The reason the last reservation attempt was refused, per agent — captured at the rejection so
571
+ * the retry and give-up can name a CAUSE instead of only their own exit point. */
572
+ #srLastRejectionReason = new Map();
573
+ /** 032-RELAYSPREAD: when this agent's receiver was last re-spread, so it never rides the 30s grid. */
574
+ #srReservationRetry = new Map();
575
+ #reservationWatchdog = null;
576
+ // Agents whose removeStandingReceiverForAgent ran while an #ensureStandingReceiver for them was
577
+ // in flight (parked on createNode/start, so the map had no entry to delete yet). The in-flight
578
+ // ensure checks this after start() and tears the fresh node down instead of installing an SR for
579
+ // an agent that has since gone offline (cello_set_agent_offline race). A fresh ensure clears it.
580
+ #standingReceiverRemoving = new Set();
581
+ // Set once gracefulShutdown begins. The standing-receiver replacement that
582
+ // acceptSession kicks off runs un-awaited (AC-003), so it can be in flight when
583
+ // shutdown starts; #createStandingReceiver checks this flag and stops a freshly
584
+ // built node instead of leaving an orphan bound to a TCP port (review M2).
585
+ #shuttingDown = false;
586
+ // DAEMON-004: lazily-loaded in-memory cache of each session's daemon-owned
587
+ // Merkle tree. The authoritative store is the session_tree_leaves table —
588
+ // the cache is rebuilt from it on first access (so it survives a restart).
589
+ #trees = new Map();
590
+ // DAEMON-004: per-session FIFO buffer of verified received content awaiting
591
+ // cello_receive. Populated by `session-content-ingest.ts` — ingestReceivedContent and the
592
+ // content stream handler.
593
+ // F1-b: a terminal answer for a sealed session, set at seal teardown BEFORE the
594
+ // received-content buffer is evicted. A blocking cello_receive waiting when the seal
595
+ // fires returns this instead of hanging or 404ing; `unreadCount` tells the caller how
596
+ // many buffered messages were dropped (still durable — read via cello_get_transcript).
597
+ // This map is deliberately NOT cleared by #evictSessionCaches (it must outlive teardown);
598
+ // it holds one tiny entry per sealed session for the daemon's lifetime and is cleared on
599
+ // restart. Idempotent: a sealed session always answers "sealed" to a receive.
600
+ #sessionTerminal = new Map();
601
+ // M7-SESSION-003: per-session direct-path counterparty liveness, observed on the
602
+ // session node's onPeerConnect ('alive') / onPeerDisconnect ('gone'). This is
603
+ // the liveness authority for direct sessions (relay sessions query the relay
604
+ // instead). NEVER the directory (SI-002). Read by exactly three consumers: the
605
+ // half-open reaper, both status surfaces, and cello_receive. No seal path reads
606
+ // it — the coupling to sealing runs through the receive guidance, which turns
607
+ // 'gone' into "call cello_close_session".
608
+ // DOD-M12B-ACK-1 adds 'impaired': connection up, our writes on it failing. It sits BELOW
609
+ // 'alive' and never above 'gone' — see #markSessionImpaired.
610
+ // DOD-M12B-ACK-1: WHY a session is impaired and what became of the content. Separate from the
611
+ // state above because the state is what surfaces print and this is what they must explain.
612
+ // DOD-M12B-STRAND-1: sessions whose durable holds have been read back. One read per session per
613
+ // process; the Map is the working copy from then on.
614
+ // DOD-M12B-SEAL-STUCK-1: sessions whose post-restore release has been attempted. Separate from
615
+ // #heldRestored so a READ-ONLY probe can hydrate without performing (or consuming) the release.
616
+ // DOD-M12B-SEAL-STUCK-1: sessions whose ordering state THIS PROCESS has observed — a relay
617
+ // witness recorded for it. `#witnessedSeq` is memory-only, so for a session that predates this
618
+ // daemon "no gap recorded" means "not recorded", not "no gap", and that difference decides
619
+ // whether we may tell an operator the session is safe to close.
620
+ #orderingObserved = new Set();
621
+ // DOD-M12B-INDEX-1: sessions whose tree and whose relay counter have provably parted. A diverged
622
+ // session can never produce a root the counterparty agrees with, so it must never be reported as
623
+ // safe to close — the close would be signed, refused as `leaf_count_mismatch`, and the receipt
624
+ // lost for good.
625
+ // DOD-M15-FRAME-1 (review F1): sessions frozen because a frame failed to verify against the
626
+ // expected counterparty. Consulted by `reviveSessionNode` — a teardown writes `interrupted`,
627
+ // which is the REVIVABLE status, so without this the next `cello_receive` silently rebuilt the
628
+ // session and re-admitted the same peer. NOT cleared by `#evictSessionCaches`: the freeze is a
629
+ // fact about the session, not about the node that was torn down to enforce it.
630
+ //
631
+ // DOD-M15-SEALWIRE-1 (part A): a MAP, not a Set, and the value is load-bearing. It was a Set when
632
+ // the identity freeze was the only freeze, so `reviveSessionNode` could hardcode its refusal —
633
+ // *"a message failed to verify against the expected counterparty's key"*. A salt disagreement now
634
+ // freezes through the same path, and that sentence would accuse a counterparty who did nothing:
635
+ // the ordinary cause is two builds that do not match. The refusal carries the freezing site's own
636
+ // words instead.
637
+ #frozenSessions = new Map();
638
+ /**
639
+ * Resolve an agent's long-term identity signer.
640
+ *
641
+ * Injected after construction, like `setParkedDrainHook`, because the daemon builds its agent key
642
+ * providers after this manager exists. Absent resolver, or an agent it does not know, means this
643
+ * side cannot sign its ephemeral — reported as `NO_LOCAL_IDENTITY` rather than quietly skipping
644
+ * the exchange, because "we could not" and "they would not" send the operator to opposite
645
+ * machines.
646
+ */
647
+ #keyProviderResolver = null;
648
+ /** Test-only observer of decoded inbound content frames — see `observeInboundContentFramesForTest`. */
649
+ #inboundFrameObserver = null;
650
+ // DOD-M12B-ACK-1: pending linger-resets for inbound content streams the peer has not closed.
651
+ // Held so shutdown can drop them rather than leave timers pointing at a torn-down node.
652
+ #lingeringStreams = new Set();
653
+ // M7-UPGRADE-002: sessions whose content integrity could NOT be verified (a content_hash
654
+ // mismatch = tamper was observed). The auto-acknowledge gate (SI-002) refuses to auto-co-sign
655
+ // for a desynced session — B must never blind-sign a tail it cannot verify. Keyed by sessionId hex.
656
+ //
657
+ // DOD-M15-SEALWIRE-1 part B1 (review F1): a MAP, not a Set, and the value is the whole fix.
658
+ //
659
+ // It was a Set because a content_hash mismatch was the only way in, so "present" could mean
660
+ // "tampered". B1 added two more ways for a frame to fail verification — an algorithm we cannot
661
+ // read, and a salted frame we hold no salt for — and BOTH ARE ORDINARY. An honest peer on a newer
662
+ // build produces the first. So the gate must still fire (never sign content you could not verify)
663
+ // while the LABEL must not accuse anyone: `unverifiable` is not `tampered`.
664
+ //
665
+ // One structure with two labels rather than two sets, deliberately: a second set is a second thing
666
+ // for every gate to remember to consult, and the one that gets forgotten is the one that matters.
667
+ #contentDesynced = new Map();
668
+ // DOD-MSG-4 (strict in-order): the RELAY is the ordering authority (Structure 2). For each
669
+ // message the relay witnesses, it delivers B a (content_hash -> canonical sequence) binding via
670
+ // the leaf_deliver stream. B records it here — keyed #k(agent,session) -> (contentHashHex -> seq)
671
+ // — and orders its transcript by THIS, never by a sender-stamped field (sovereign-node: B does
672
+ // not trust the counterparty for ordering). When B has no witness for an arriving hash
673
+ // (relay-degraded), it falls back to arrival-order append.
674
+ #witnessedSeq = new Map();
675
+ /**
676
+ * DOD-M15-SEALPRECOND-1 — our OWN leaves the relay has ordered and this tree has not yet placed,
677
+ * keyed #k(agent,session) -> (contentHashHex -> assigned leaf index). `#witnessedSeq` above holds
678
+ * only the COUNTERPARTY's, so nothing in the seal gate could see a send of ours between the relay
679
+ * assigning its sequence and `placeOwnLeaf` writing it — the window the 2026-09-11 close signed
680
+ * a two-leaf root inside, losing the receipt for both sides.
681
+ */
682
+ #ownLeavesOrdered = new Map();
683
+ // DOD-MSG-4: out-of-order direct arrivals. A content frame whose canonical sequence is AHEAD of
684
+ // the next expected leaf is HELD here (keyed #k(agent,session) -> (canonicalSeq -> entry)) instead
685
+ // of being appended out of order. Once the missing in-between sequence(s) land (recovered from the
686
+ // relay mailbox), #releaseHeld drains the held entries in canonical order. content is plaintext in
687
+ // memory only — evicted on teardown, same as the other per-session caches.
688
+ /**
689
+ * DOD-M15-SEALWIRE-1 bullet 5 — `authorship` rides the held entry.
690
+ *
691
+ * A SENT message that lands ahead of our tree tail is held here and its transcript row is written
692
+ * later, on release. The hold happens AFTER the submit, so it was signed exactly like an unheld
693
+ * one — without carrying the proof through, a message that happened to queue behind a gap became
694
+ * permanently less provable than the identical message that did not, for a reason with nothing to
695
+ * do with authorship.
696
+ */
697
+ /**
698
+ * 033-ACKEMIT review F1 — what this side has ACTUALLY RECEIVED, per session: the canonical
699
+ * position and the content hash at it.
700
+ *
701
+ * ⚠️ **IT MIRRORS THE RELAY CLIENT'S `#lastSeen` RATHER THAN REPLACING IT, and the duplication is
702
+ * deliberate.** The submit path needs the value on the client, because that is where the claim is
703
+ * built; the unwitnessed content path needs it here, because a session with no relay client has no
704
+ * client to read it from. Both are written from ONE place — `noteAcknowledgeable`, in
705
+ * `session-content-ingest.ts` — so they
706
+ * cannot come to disagree, and the client is preferred on read because it also sees leaves the
707
+ * relay delivered that never came through this path.
708
+ */
709
+ #lastAck = new Map();
710
+ #heldContent = new Map();
711
+ // DOD-MSG-4: the relay's high-water canonical sequence for this session — the largest sequence the
712
+ // relay has witnessed (max over leaf_deliver). Keyed #k(agent,session). EXPOSED for the next
713
+ // sub-increment (catch-up-before-live: on reconnect, hold live arrivals until the tree reaches this
714
+ // so a fresh message can't append ahead of earlier ones still parked) — it is NOT yet consumed by
715
+ // the gate, which today holds purely on the per-message `canonicalSeq > nextExpected` test.
716
+ #highWaterSeq = new Map();
717
+ // M7-UPGRADE-002: sessions for which B has already submitted its responder SEAL leaf (via
718
+ // auto-ack OR cello_close_session). Idempotency guard — A's SEAL ctrl leaf may be delivered
719
+ // more than once (and the relay echoes leaves), so auto-ack fires AT MOST ONCE per session.
720
+ // M8B FINDING-1: the value carries the first successful submit's reportedRootHex/sequenceNumber
721
+ // (null while the submit is still in flight), so a RETRY close can escalate to a unilateral
722
+ // seal with the original reported root instead of deadlocking on seal_pending_bilateral.
723
+ #responderSealSubmitted = new Map();
724
+ // M7-SESSION-001 (M-1 PUSH): optional callback fired when a session changes
725
+ // state, so the composition root can dispatch a session_state_changed
726
+ // notification to live MCP clients. Injected via a setter AFTER construction
727
+ // because the NotificationDispatcher is built later than this manager in
728
+ // daemon.ts (it depends on the IPC server). Never required — when unset,
729
+ // state changes are persisted and logged but no push notification is emitted.
730
+ /**
731
+ * Fix #1 EXTENSION (cross-node seal-liveness), injected by daemon.ts because the broker-dial
732
+ * machinery (consortium roster + visiting connections) lives above this class.
733
+ *
734
+ * The AUTO-ACKNOWLEDGE path below submits a seal leaf, and the directory answers it within ~60ms
735
+ * by pushing `seal_verified` to the INITIATOR. On a cross-node session the initiator released its
736
+ * visiting connection to the broker after setup, so that push finds no stream, the directory
737
+ * ENQUEUES the frame instead, and the seal blocks forever waiting for a co-signature it never
738
+ * asked for. close-session-handler already guards its own path this way; the auto-ack path did
739
+ * not, and it is the path that fires FIRST whenever the counterparty closes first.
740
+ *
741
+ * Unset (single-node / M6 back-compat) is fine: the initiator is reachable on its home stream.
742
+ */
743
+ #ensureSealBroker;
744
+ #onSessionStateChanged = null;
745
+ // M8C-MSGWAKE-1 (channel stage 2): fired when a verified inbound message is buffered for
746
+ // cello_receive, so the daemon can push a content-free `cello_message` doorbell. Wired in
747
+ // daemon.ts (depends on the notification dispatcher). Content-free by signature — carries only
748
+ // agent / session / senderPubkey, NEVER the plaintext (INV-CONTENTFREE).
749
+ #onContentArrived = null;
750
+ /**
751
+ * M14 / DOD-DOC-INBOUND-2: the document-layer interception, injected by the composition root.
752
+ *
753
+ * Returns whether the document layer CONSUMED the frame. Absent (the default) means every frame
754
+ * is conversation, exactly as before — the document layer cannot change message handling by being
755
+ * unwired, which is the property that lets it be wired incrementally.
756
+ *
757
+ * It is handed the decrypted CONTENT, unlike `#onContentArrived`, which is content-free by
758
+ * signature. That is unavoidable: deciding whether bytes are a document frame requires the bytes.
759
+ * The router it calls never logs them.
760
+ */
761
+ /**
762
+ * ⚠️ THE RETURN TYPE USED TO DECLARE `ok?: boolean; reason?: string`, AND THE PRODUCER CANNOT
763
+ * SUPPLY EITHER. Narrowed so reading them is a compile error rather than a silent `undefined`.
764
+ *
765
+ * The implementation is `DocumentFrameRouter.routeSync`, whose own return type
766
+ * (`FrameClassification`) is exactly `{consumed:false} | {consumed:true; kind}` — it has no such
767
+ * fields at all. The wider shape here was a promise only this declaration made, and it was
768
+ * assignable precisely because the extra members were optional.
769
+ *
770
+ * It is not an oversight in the router: `routeSync` dispatches with `void this.#enqueue(...)`, so
771
+ * when it returns, the frame has been classified and queued and **no verdict exists yet**. A
772
+ * synchronous caller cannot be told an asynchronous outcome.
773
+ *
774
+ * **The cost of the lie was a wrong lead.** `j-stale-session`'s investigation read those fields'
775
+ * absence from every log line as "the router returned neither" and filed it as the next thread to
776
+ * pull. There was no thread: a JSON logger omits `undefined`, so a field that can never be set is
777
+ * indistinguishable from one that was set to nothing. The verdict lives on
778
+ * `document.frame.refused`, joined by `correlationId`.
779
+ */
780
+ #onDocumentFrame = null;
781
+ /**
782
+ * DOD-DOC-SCREEN-CLASSIFY-1: the classify-only half of the hook above — is this a document
783
+ * frame, deciding nothing else. Injected together with it so the two cannot disagree about what
784
+ * a document frame is. Null means every frame takes the full inbound screen, exactly as before
785
+ * the document layer existed.
786
+ */
787
+ #isDocumentFrame = null;
788
+ // A send is NOT fire-and-forget. After a content_frame is delivered over the direct session
789
+ // channel, the sender arms a TTF timer and waits for an unsigned, transport-authenticated
790
+ // `persisted` delivery ACK on the same /cello/content/1.0.0 protocol. A persisted ACK cancels the
791
+ // timer (content.delivery.acked); TTF expiry hands the content to the park backstop.
792
+ // Keyed sessionId → contentHashHex → entry.
793
+ #awaitingAck = new Map();
794
+ // TTF (time-to-flush) for an un-acked content entry. Injectable so tests can drive
795
+ // expiry deterministically; production default sits in the Part-4 proposed 10–30s band.
796
+ #contentTtfMs = 20_000;
797
+ // CELLO-M7-MSG-001: side-effect hooks the composition root wires to the durable
798
+ // retry_queue (and, in 3b, the relay park deposit). Injected after construction
799
+ // because RetryQueue is built later in daemon.ts. When unset, the awaiting-ACK timer
800
+ // still fires and the ACK still resolves — only the durable crash-backstop is skipped.
801
+ // DOD-RETRYQ-STRAND-1: fired on the transition INTO a status from which no resend can ever
802
+ // succeed, so durable state keyed to that session gets a disposition instead of stranding.
803
+ // Injected after construction because RetryQueue is built later in daemon.ts.
804
+ #onSessionTerminal = null;
805
+ #onAwaitingPersisted = null;
806
+ #onAwaitingTtf = null;
807
+ // M12-P12 verification: force the next N park deposits to be REFUSED, so the failure this unit
808
+ // fixes can be produced on demand instead of waited for. The real failure is a race — the deposit
809
+ // is refused only in the seconds-long window while the sender's standing receiver rebuilds — and
810
+ // no CLI lever reaches that window: set-agent-offline leaves an open session's node serving, and
811
+ // the CLI refuses a send from an offline agent. Without this the fix ships unwatched.
812
+ // INERT unless the daemon is started with CELLO_FAULT_INJECTION=1; the IPC handler that sets it
813
+ // refuses outright otherwise, so a normal daemon cannot be talked into dropping messages.
814
+ // The incident needs BOTH halves: the direct dial has to fail (or the park path is never entered
815
+ // — measured, the counterparty's session node accepts the frame and reports delivered:true even
816
+ // with its agent away), and the park deposit that follows has to be refused. One without the
817
+ // other reproduces nothing.
818
+ #sendFaultRemaining = 0;
819
+ // DOD-M12B-ACK-1 — the same seam for the delivery-ACK write. See injectAckFault.
820
+ #ackFaultRemaining = 0;
821
+ // DOD-M12B-REDIAL-1 — makes the next N `newStream` calls report the connection as gone, BEFORE
822
+ // the node is touched. The sibling of injectSendFault for the one condition that used to end a
823
+ // conversation permanently; a real connection drop is not reproducible in-process.
824
+ #connectionLossRemaining = 0;
825
+ // DOD-M12B-REDIAL-1: the counterparty addresses this session dialled, kept so it can dial them
826
+ // again. They arrived in the FROST-signed assignment and were used once and dropped, which is
827
+ // why nothing could ever re-dial.
828
+ #counterpartyAddrs = new Map();
829
+ // DOD-M12B-REDIAL-1: when this session may next attempt a re-dial. Continuous re-dialling is what
830
+ // produced the 2026-08-17 notification storm, so a burst of sends against a peer that is gone
831
+ // costs one attempt, not one per message.
832
+ #redialNotBefore = new Map();
833
+ /** Arm the direct-send fault — makes the next N sends take the dial-failure path. */
834
+ injectSendFault(count) {
835
+ this.#sendFaultRemaining = Math.max(0, count);
836
+ return this.#sendFaultRemaining;
837
+ }
838
+ /** DOD-M12B-ACK-1: arm the delivery-ACK write fault — the sibling of injectSendFault for the
839
+ * path that fails on a LISTENING agent. Without it the ACK failure branch (which impairs the
840
+ * session and, until this milestone, could never clear it again) is unreachable from a test:
841
+ * a listener sends no content, so the direct-send fault never fires for it. */
842
+ injectAckFault(count) {
843
+ this.#ackFaultRemaining = Math.max(0, count);
844
+ return this.#ackFaultRemaining;
845
+ }
846
+ /** DOD-M12B-REDIAL-1: arm the connection-loss fault — the next N direct sends find no open
847
+ * connection, exactly as they do after any blip. See #connectionLossRemaining. */
848
+ injectConnectionLoss(count) {
849
+ this.#connectionLossRemaining = Math.max(0, count);
850
+ return this.#connectionLossRemaining;
851
+ }
852
+ // M12-P12: the durable enqueue for a park deposit that FAILED. Distinct from onTtf because the
853
+ // cause is distinct — nothing timed out here, the deposit was refused — and an event named for
854
+ // the wrong cause is how this path stayed invisible.
855
+ // M12-P13 (review HIGH-1): returns whether the content is ACTUALLY queued. `false` means the
856
+ // queue dropped it (today: the content-derived dedupe key collided), and the caller must then not
857
+ // claim durability — nor commit the leaf that claim now authorises.
858
+ #onParkFailed = null;
859
+ constructor(opts) {
860
+ this.#factory = opts.factory;
861
+ this.#logger = opts.logger;
862
+ // REQUIRED, no fallback (INV-9, audit finding). This check used to read
863
+ // `opts.securityGateway ?? new PassthroughGatewayClient()` — the identical shape as the defect
864
+ // that reopened this milestone, one layer down and still shipping in the binary. `daemon.ts`
865
+ // was hardened to throw while this constructor was not, so the inbound screen had a silent
866
+ // always-allow path that nothing in the product reached TODAY and any future refactor could.
867
+ // "Currently unreachable" is a property of today's call sites, not of the code.
868
+ //
869
+ // ⚠️ CHECKED AND ASSIGNED FIRST, ahead of every collaborator: the content pipeline below is
870
+ // handed this client by value, and a constructor cannot hand out a field it has not set. It also
871
+ // means a caller that forgot to screen is told before the manager builds anything.
872
+ if (!opts.securityGateway) {
873
+ throw new Error("SessionNodeManager: securityGateway is required (INV-9). The inbound screen has no " +
874
+ "always-allow fallback, because that fallback is how the entire security layer shipped " +
875
+ "inert. Pass a real client, or new PassthroughGatewayClient() from a test that " +
876
+ "deliberately does not screen.");
877
+ }
878
+ this.#securityGateway = opts.securityGateway;
879
+ // ⚠️ BUILT FIRST, AND THE ORDER IS LOAD-BEARING: `#authorship` and `#refusals` close over
880
+ // `this.#records`. Nothing invokes them during construction today (every context member is a
881
+ // lazy arrow), but tsc caught this exact class once on `#logger`, and the next line added to
882
+ // this constructor is the one that would pay for it.
883
+ this.#records = new SessionRecords({
884
+ logger: this.#logger,
885
+ db: () => this.#db,
886
+ sessionKey: (a, sid) => this.#k(a, sid),
887
+ requireAgentId: (a) => this.#requireAgentId(a),
888
+ witnessUnreadable: this.#witnessUnreadable,
889
+ });
890
+ this.#authorship = new AuthorshipVerifier({
891
+ logger: this.#logger,
892
+ sessionKey: (a, sid) => this.#k(a, sid),
893
+ getSessionRecord: (a, sid) => this.#queries.getSessionRecord(a, sid),
894
+ getSessionTree: (a, sid) => this.getSessionTree(a, sid),
895
+ isSessionDiverged: (a, sid) => this.#records.isSessionDiverged(a, sid),
896
+ sessionGenesisPrevRoot: (a, sid) => this.#leafRecords.sessionGenesisPrevRoot(a, sid),
897
+ relaySessionIdBytes: (a, sid) => this.#activeNodes.get(this.#k(a, sid))?.relaySessionIdBytes,
898
+ heldContentFor: (a, sid) => this.#heldContent.get(this.#k(a, sid)),
899
+ });
900
+ this.#refusals = new InboundRefusals({
901
+ logger: this.#logger,
902
+ // A function, not a value: the database is opened after construction, so a snapshot taken
903
+ // here would be null for the life of the process.
904
+ db: () => this.#db,
905
+ sessionKey: (a, sid) => this.#k(a, sid),
906
+ requireAgentId: (a) => this.#requireAgentId(a),
907
+ cancelLeafFetch: (key, hashHex) => this.#contentIn.cancelLeafFetch(key, hashHex),
908
+ noteContentRefusal: (a, sid, reason, detail) => this.#notices.noteContentRefusal(a, sid, reason, detail),
909
+ recordTranscriptMessage: (...args) => this.#records.recordTranscriptMessage(...args),
910
+ recordWitnessedSequence: (a, sid, h, n) => this.recordWitnessedSequence(a, sid, h, n),
911
+ getTier: (a, pk) => this.#records.getTier(a, pk),
912
+ resolveTierBound: (a, t, f) => this.#records.resolveTierBound(a, t, f),
913
+ mailboxRouteAvailable: (a) => this.#mailboxRouteAvailable(a),
914
+ receivedBytesTotal: (a, sid) => this.#queries.getReceivedBytesTotal(a, sid),
915
+ verifyAuthorshipClaim: (a, sid, s1, sig, h) => this.#authorship.verifyAuthorshipClaim(a, sid, s1, sig, h),
916
+ });
917
+ this.#park = new ParkRecovery({
918
+ logger: this.#logger,
919
+ refusals: this.#refusals,
920
+ shuttingDown: () => this.#shuttingDown,
921
+ sessionKey: (a, sid) => this.#k(a, sid),
922
+ requireAgentId: (a) => this.#requireAgentId(a),
923
+ ownPubkeyHex: (a) => this.#queries.ownPubkeyHex(a),
924
+ activeEntry: (key) => this.#activeNodes.get(key),
925
+ agentsWithLiveReceiver: () => this.#standingReceivers.keys(),
926
+ agentWantsReceiver: (a) => this.#agentsWantingReceiver.has(a),
927
+ getSessionRecord: (a, sid) => this.#queries.getSessionRecord(a, sid),
928
+ getSealCarry: (pk, sid) => this.#seal.getSealCarry(pk, sid),
929
+ getSessionTree: (a, sid) => this.getSessionTree(a, sid),
930
+ recordOrderingRecord: (a, sid, s1, s2, h, cid) => this.recordOrderingRecord(a, sid, s1, s2, h, cid),
931
+ ingestReceivedContent: (a, sid, c, h, cid, seq, alg) => this.ingestReceivedContent(a, sid, c, h, cid, seq, alg),
932
+ witnessReceivedLeaf: (a, sid, h, s1, sig, kind, cid) => this.#contentIn.witnessReceivedLeaf(a, sid, h, s1, sig, kind, cid),
933
+ noteAcknowledgeable: (a, sid, seq, h) => this.#contentIn.noteAcknowledgeable(a, sid, seq, h),
934
+ sendDeliveryAck: (a, sid, h, cid) => { void sendDeliveryAck(contentCtx, a, sid, h, cid); },
935
+ persistedRelayEndpoint: (a, sid) => this.#queries.getPersistedRelayEndpoint(a, sid),
936
+ }, opts.parkedDrainBackstopMs ?? PARKED_DRAIN_BACKSTOP_DEFAULT_MS);
937
+ this.#salts = new SessionSalts({
938
+ logger: this.#logger,
939
+ db: () => this.#db,
940
+ sessionKey: (a, sid) => this.#k(a, sid),
941
+ requireAgentId: (a) => this.#requireAgentId(a),
942
+ activeEntry: (key) => this.#activeNodes.get(key),
943
+ getSessionTree: (a, sid) => this.getSessionTree(a, sid),
944
+ getSessionRecord: (a, sid) => this.#queries.getSessionRecord(a, sid),
945
+ heldContentFor: (key) => this.#heldContent.get(key),
946
+ ensureHeldRestored: (a, sid, opts) => this.#held.ensureHeldRestored(a, sid, opts),
947
+ awaitingAck: this.#awaitingAck,
948
+ contentEncryptionState: (a, sid) => this.#ephemerals.contentEncryptionState(a, sid),
949
+ freezeSession: (a, sid, reason, narrative, cid) => this.#freezeSession(a, sid, reason, narrative, cid),
950
+ sendSaltFrame: (a, sid, cid, override) => this.#sendSaltFrame(a, sid, cid, override),
951
+ });
952
+ this.#queries = new SessionQueries({
953
+ logger: this.#logger,
954
+ db: () => this.#db,
955
+ requireAgentId: (a) => this.#requireAgentId(a),
956
+ sessionKey: (a, sid) => this.#k(a, sid),
957
+ unkey: (key, agentName) => this.#unk(key, agentName),
958
+ });
959
+ this.#notices = new RefusalNotices({
960
+ logger: this.#logger,
961
+ queries: this.#queries,
962
+ db: () => this.#db,
963
+ requireAgentId: (a) => this.#requireAgentId(a),
964
+ sessionKey: (a, sid) => this.#k(a, sid),
965
+ unkey: (key, agentName) => this.#unk(key, agentName),
966
+ });
967
+ this.#ephemerals = new SessionEphemerals({
968
+ logger: this.#logger,
969
+ sessionKey: (a, sid) => this.#k(a, sid),
970
+ activeEntry: (key) => this.#activeNodes.get(key),
971
+ keyProvider: (a) => this.#keyProviderResolver?.(a),
972
+ freezeSessionForKeyRefusal: (a, sid, reason, cid) => this.#freezeSessionForKeyRefusal(a, sid, reason, cid),
973
+ });
974
+ this.#liveness = new SessionLiveness({
975
+ logger: this.#logger,
976
+ queries: this.#queries,
977
+ notices: this.#notices,
978
+ ephemerals: this.#ephemerals,
979
+ counterpartyAddrs: this.#counterpartyAddrs,
980
+ sessionKey: (a, sid) => this.#k(a, sid),
981
+ activeEntry: (key) => this.#activeNodes.get(key),
982
+ sendSaltFrame: (a, sid, cid, override) => this.#sendSaltFrame(a, sid, cid, override),
983
+ });
984
+ this.#witness = new WitnessAlerts({ logger: this.#logger });
985
+ this.#held = new HeldContent({
986
+ logger: this.#logger,
987
+ queries: this.#queries,
988
+ records: this.#records,
989
+ db: () => this.#db,
990
+ sessionKey: (a, sid) => this.#k(a, sid),
991
+ requireAgentId: (a) => this.#requireAgentId(a),
992
+ heldContent: this.#heldContent,
993
+ witnessedSeq: this.#witnessedSeq,
994
+ getSessionTree: (a, sid) => this.getSessionTree(a, sid),
995
+ appendSessionLeaf: (a, sid, kind, h, cid) => this.appendSessionLeaf(a, sid, kind, h, cid),
996
+ appendVerifiedContent: (a, sid, c, h, pk, cid, orig, auth) => this.#contentIn.appendVerifiedContent(a, sid, c, h, pk, cid, orig, auth),
997
+ });
998
+ this.#leafRecords = new SessionLeafRecords({
999
+ logger: this.#logger,
1000
+ queries: this.#queries,
1001
+ db: () => this.#db,
1002
+ sessionKey: (a, sid) => this.#k(a, sid),
1003
+ requireAgentId: (a) => this.#requireAgentId(a),
1004
+ activeEntry: (key) => this.#activeNodes.get(key),
1005
+ });
1006
+ /**
1007
+ * ⚠️ **CONSTRUCTED LAST OF THE COLLABORATORS, and the reason is the same one written above
1008
+ * `#records`:** every collaborator below is handed to it BY VALUE, so each has to already
1009
+ * exist. `#receivers` is built after this and is not reachable from the content path.
1010
+ *
1011
+ * `mgr` exists because the getters below need the MANAGER's fields, and a getter written in an
1012
+ * object literal binds `this` to the literal. The alternative — capturing the values — is the
1013
+ * defect that once left `keyProvider` frozen at `null`, so signing stopped and sessions fell
1014
+ * back to unencrypted content with the whole suite green. Anything the manager can REPLACE
1015
+ * after this line is reached through a getter; anything it cannot is passed straight through.
1016
+ */
1017
+ // The rule is right about the pattern it was written for — `var self = this` standing in for a
1018
+ // closure. This is the one case the language forces: a `get` in an object literal binds `this`
1019
+ // to the LITERAL, so a getter cannot reach the manager's fields without a name for it. The
1020
+ // alternative is passing the values, which is the defect this whole block exists to avoid.
1021
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
1022
+ const mgr = this;
1023
+ const contentCtx = {
1024
+ logger: this.#logger,
1025
+ securityGateway: this.#securityGateway,
1026
+ hasCommittedSealLeaf: (a, sid) => mgr.hasCommittedSealLeaf(a, sid),
1027
+ records: this.#records,
1028
+ authorship: this.#authorship,
1029
+ refusals: this.#refusals,
1030
+ park: this.#park,
1031
+ salts: this.#salts,
1032
+ queries: this.#queries,
1033
+ notices: this.#notices,
1034
+ ephemerals: this.#ephemerals,
1035
+ liveness: this.#liveness,
1036
+ held: this.#held,
1037
+ leafRecords: this.#leafRecords,
1038
+ // One Map object each, shared — never a copy. See the context's own note.
1039
+ activeNodes: this.#activeNodes,
1040
+ awaitingAck: this.#awaitingAck,
1041
+ heldContent: this.#heldContent,
1042
+ witnessedSeq: this.#witnessedSeq,
1043
+ ownLeavesOrdered: this.#ownLeavesOrdered,
1044
+ leafFetchTimers: this.#leafFetchTimers,
1045
+ lastAck: this.#lastAck,
1046
+ resolvedContent: this.#resolvedContent,
1047
+ undeliverableSeqs: this.#undeliverableSeqs,
1048
+ highWaterSeq: this.#highWaterSeq,
1049
+ counterpartyAddrs: this.#counterpartyAddrs,
1050
+ lingeringStreams: this.#lingeringStreams,
1051
+ redialNotBefore: this.#redialNotBefore,
1052
+ orderingObserved: this.#orderingObserved,
1053
+ // Settable after this point — `#contentTtfMs` is overridden from `opts` three lines below.
1054
+ get shuttingDown() { return mgr.#shuttingDown; },
1055
+ get contentTtfMs() { return mgr.#contentTtfMs; },
1056
+ get leafFetchGraceMs() { return mgr.#leafFetchGraceMs; },
1057
+ get ownChainStore() { return mgr.#ownChainStore; },
1058
+ // Wired by the composition root long after construction; `null` until then, and `null` is an
1059
+ // answer the pipeline reports rather than papers over.
1060
+ get onParkFailed() { return mgr.#onParkFailed; },
1061
+ get onContentArrived() { return mgr.#onContentArrived; },
1062
+ get onDocumentFrame() { return mgr.#onDocumentFrame; },
1063
+ get isDocumentFrame() { return mgr.#isDocumentFrame; },
1064
+ get onAwaitingPersisted() { return mgr.#onAwaitingPersisted; },
1065
+ get inboundFrameObserver() { return mgr.#inboundFrameObserver; },
1066
+ // Read AND decremented by the pipeline, so these are the one place a pair is needed.
1067
+ get sendFaultRemaining() { return mgr.#sendFaultRemaining; },
1068
+ set sendFaultRemaining(v) { mgr.#sendFaultRemaining = v; },
1069
+ get ackFaultRemaining() { return mgr.#ackFaultRemaining; },
1070
+ set ackFaultRemaining(v) { mgr.#ackFaultRemaining = v; },
1071
+ get connectionLossRemaining() { return mgr.#connectionLossRemaining; },
1072
+ set connectionLossRemaining(v) { mgr.#connectionLossRemaining = v; },
1073
+ sessionKey: (a, sid) => this.#k(a, sid),
1074
+ keyProvider: (a) => this.#keyProviderResolver?.(a),
1075
+ getSessionTree: (a, sid) => this.getSessionTree(a, sid),
1076
+ appendSessionLeaf: (a, sid, kind, h, cid) => this.appendSessionLeaf(a, sid, kind, h, cid),
1077
+ mailboxRouteAvailable: (a) => this.#mailboxRouteAvailable(a),
1078
+ streamCensus: (node, peerId) => this.#streamCensus(node, peerId),
1079
+ freezeOnIdentityFailure: (a, sid, reason, cid) => this.#freezeOnIdentityFailure(a, sid, reason, cid),
1080
+ handleTtfExpiry: (a, sid, hashHex) => this.#handleTtfExpiry(a, sid, hashHex),
1081
+ markContentUnverifiable: (a, sid, why) => this.#markContentUnverifiable(a, sid, why),
1082
+ maybeAutoAcknowledgeSeal: (a, sid, cid) => this.#seal.maybeAutoAcknowledgeSeal(a, sid, cid),
1083
+ ownChainOf: (a, sid, entry, ownPubkey) => this.#ownChainOf(a, sid, entry, ownPubkey),
1084
+ updateSessionStatus: (a, sid, status, by) => this.#life.updateSessionStatus(a, sid, status, by),
1085
+ abandonSession: (a, sid) => this.#life.abandonSession(a, sid),
1086
+ connectToCounterparty: (a, sid, addrs) => this.#life.connectToCounterparty(a, sid, addrs),
1087
+ destroySessionNode: (a, sid, reason) => this.#life.destroySessionNode(a, sid, reason),
1088
+ retireOnCounterpartyAbandon: (a, sid, cid) => this.#life.retireOnCounterpartyAbandon(a, sid, cid),
1089
+ };
1090
+ // The sender first: the receiver holds it, and nothing holds the receiver.
1091
+ this.#contentOut = new SessionContentSender(contentCtx);
1092
+ this.#contentIn = new SessionContentIngest(contentCtx, this.#contentOut);
1093
+ this.#dbPath = opts.dbPath;
1094
+ if (typeof opts.contentTtfMs === "number" && opts.contentTtfMs > 0) {
1095
+ this.#contentTtfMs = opts.contentTtfMs;
1096
+ }
1097
+ this.#autoNatProbers = opts.autoNatProbers ?? (() => []);
1098
+ this.#srRetryDelaysMs = opts.standingReceiverRetryDelaysMs ?? [1_000, 5_000, 15_000];
1099
+ this.#srReservationTimeoutMs = opts.standingReceiverReservationTimeoutMs ?? 15_000;
1100
+ // ⚠️ BUILT AFTER the retry/timeout settings it captures. Those come from `opts` with defaults
1101
+ // applied HERE, and a collaborator constructed above them would capture `undefined` — the
1102
+ // same class tsc caught on `#logger`. Duplicating the defaults at the call site would be
1103
+ // worse: two places deciding one number is how they stop agreeing.
1104
+ this.#receivers = new StandingReceivers({
1105
+ logger: this.#logger,
1106
+ records: this.#records,
1107
+ park: this.#park,
1108
+ factory: this.#factory,
1109
+ db: () => this.#db,
1110
+ shuttingDown: () => this.#shuttingDown,
1111
+ sessionKey: (a, sid) => this.#k(a, sid),
1112
+ standingReceivers: this.#standingReceivers,
1113
+ standingReceiverCreating: this.#standingReceiverCreating,
1114
+ agentsWantingReceiver: this.#agentsWantingReceiver,
1115
+ srReservationRetry: this.#srReservationRetry,
1116
+ srLastRejectionReason: this.#srLastRejectionReason,
1117
+ directoryRelayEndpoints: this.#directoryRelayEndpoints,
1118
+ standingReceiverRemoving: this.#standingReceiverRemoving,
1119
+ srRetryDelaysMs: this.#srRetryDelaysMs,
1120
+ srReservationTimeoutMs: this.#srReservationTimeoutMs,
1121
+ autoNatProbers: () => this.#autoNatProbers(),
1122
+ proveToRelay: (a, circuitAddr, node, cid, surface) => this.#relay.proveToRelay(a, circuitAddr, node, cid, surface),
1123
+ tellRelayReleased: (a, relayPeerId, node, cid) => this.#relay.tellRelayReleased(a, relayPeerId, node, cid),
1124
+ reservationCircuitAddrs: (a) => this.#relay.reservationCircuitAddrs(a),
1125
+ // 055-ONDEMAND — the abandoned-offer release asks these two: did the session this offer was
1126
+ // for actually start, and what do the agent's remaining live sessions still need?
1127
+ sessionIsLive: (a, sid) => this.#activeNodes.has(this.#k(a, sid)),
1128
+ // ⚠️ `heldRelayIdsOf`, NOT a substring test — 056-SLOTDEAD. This shipped asking only whether an
1129
+ // address contains `p2p-circuit`, a looser definition than the rest of the daemon's. One that
1130
+ // does not NAME its relay cannot be watched, proved to, or admitted inbound (`msg-018` says so),
1131
+ // yet this reported `reserved` — an undialable agent looking healthy where an operator looks first.
1132
+ anyLiveSessionHoldsCircuit: (a) => [...this.#activeNodes.values()]
1133
+ .some((e) => e.agentName === a && heldRelayIdsOf(e.node).length > 0),
1134
+ authenticateStandingReceiver: (a, node, relayPeerId, heldCircuitAddr, cid) => this.#relay.authenticateStandingReceiver(a, node, relayPeerId, heldCircuitAddr, cid),
1135
+ });
1136
+ /**
1137
+ * ⚠️ BUILT AFTER `#receivers`, which it holds BY VALUE — the same ordering rule as every
1138
+ * collaborator above, and the one tsc caught on `#logger`. The seal path falls back to the
1139
+ * agent's standing receiver when an interrupted session has no node of its own, so a seal
1140
+ * constructed before the receivers would have captured `undefined` for exactly the case it
1141
+ * exists to serve.
1142
+ *
1143
+ * `mgr` again, and for the same reason as the content context above: the two seal stores are
1144
+ * opened LAZILY — on first use, because they need a database that does not exist yet — and both
1145
+ * sides must end up holding the SAME instance. Passing them by value would have handed this file
1146
+ * a permanent `null` and let it open a second store over the same rows.
1147
+ */
1148
+ this.#seal = new SessionSeal({
1149
+ logger: this.#logger,
1150
+ records: this.#records,
1151
+ queries: this.#queries,
1152
+ park: this.#park,
1153
+ held: this.#held,
1154
+ leafRecords: this.#leafRecords,
1155
+ receivers: this.#receivers,
1156
+ db: () => this.#db,
1157
+ activeNodes: this.#activeNodes,
1158
+ heldContent: this.#heldContent,
1159
+ witnessedSeq: this.#witnessedSeq,
1160
+ ownLeavesOrdered: this.#ownLeavesOrdered,
1161
+ highWaterSeq: this.#highWaterSeq,
1162
+ orderingObserved: this.#orderingObserved,
1163
+ contentDesynced: this.#contentDesynced,
1164
+ responderSealSubmitted: this.#responderSealSubmitted,
1165
+ relayClients: this.#relayClients,
1166
+ get relayReceiptStore() { return mgr.#relayReceiptStore; },
1167
+ set relayReceiptStore(v) { mgr.#relayReceiptStore = v; },
1168
+ get sealLeafStore() { return mgr.#sealLeafStore; },
1169
+ set sealLeafStore(v) { mgr.#sealLeafStore = v; },
1170
+ get ensureSealBroker() { return mgr.#ensureSealBroker; },
1171
+ set ensureSealBroker(v) { mgr.#ensureSealBroker = v; },
1172
+ get onSessionStateChanged() { return mgr.#onSessionStateChanged; },
1173
+ get ownChainStore() { return mgr.#ownChainStore; },
1174
+ get detachedRelayClientBuilder() { return mgr.#detachedRelayClientBuilder; },
1175
+ sessionKey: (a, sid) => this.#k(a, sid),
1176
+ requireAgentId: (a) => this.#requireAgentId(a),
1177
+ getSessionTree: (a, sid) => this.getSessionTree(a, sid),
1178
+ getSessionTreeRootHex: (a, sid) => this.getSessionTreeRootHex(a, sid),
1179
+ getDirectoryOnlineToken: (a) => this.getDirectoryOnlineToken(a),
1180
+ getKeyProvider: (a) => this.#keyProviderResolver?.(a),
1181
+ destroySessionSeed: (a, sid) => this.#life.destroySessionSeed(a, sid),
1182
+ updateSessionStatus: (a, sid, status, by) => this.#life.updateSessionStatus(a, sid, status, by),
1183
+ });
1184
+ /**
1185
+ * ⚠️ ALSO AFTER `#receivers`, and after `#contentIn` — both held by value. The reservation
1186
+ * watchdog reads the standing receivers directly, and a witnessed leaf arriving on a relay
1187
+ * stream is handed straight to the ingest side. Built before either, this would have captured
1188
+ * `undefined` for the two things it exists to connect.
1189
+ *
1190
+ * The two stores and the watchdog handle are accessors for the same reason as the seal's: they
1191
+ * are written after construction, and both sides must see one instance rather than each opening
1192
+ * its own over the same rows.
1193
+ */
1194
+ this.#relay = new SessionRelay({
1195
+ logger: this.#logger,
1196
+ records: this.#records,
1197
+ queries: this.#queries,
1198
+ // 055-ONDEMAND: the watchdog re-takes a live session's circuit rather than rebuilding a
1199
+ // receiver that would reserve nothing.
1200
+ retakeReservationOn: (a, node, c, cid) => this.#receivers.retakeReservationOn(a, node, c, cid),
1201
+ park: this.#park,
1202
+ refusals: this.#refusals,
1203
+ leafRecords: this.#leafRecords,
1204
+ receivers: this.#receivers,
1205
+ witness: this.#witness,
1206
+ contentIn: this.#contentIn,
1207
+ // DOD-M15-AWAYSCOPE-1: injected, and the daemon sets it in the composition root. The manager
1208
+ // cannot answer it — attendance is an IPC-layer fact (who has claimed this agent), and this
1209
+ // class is deliberately connection-agnostic.
1210
+ currentAttendance: (agentName) => this.#currentAttendance(agentName),
1211
+ db: () => this.#db,
1212
+ activeNodes: this.#activeNodes,
1213
+ relayClients: this.#relayClients,
1214
+ standingReceivers: this.#standingReceivers,
1215
+ agentsWantingReceiver: this.#agentsWantingReceiver,
1216
+ directoryRelayEndpoints: this.#directoryRelayEndpoints,
1217
+ relayQuarantine: this.#relayQuarantine,
1218
+ srReservationRetry: this.#srReservationRetry,
1219
+ srLastRejectionReason: this.#srLastRejectionReason,
1220
+ srRelayRefusal: this.#srRelayRefusal,
1221
+ get shuttingDown() { return mgr.#shuttingDown; },
1222
+ get srReservationRetryMs() { return mgr.#srReservationRetryMs; },
1223
+ get srWatchdogIntervalMs() { return mgr.#srWatchdogIntervalMs; },
1224
+ get ownChainStore() { return mgr.#ownChainStore; },
1225
+ get relayReceiptStore() { return mgr.#relayReceiptStore; },
1226
+ set relayReceiptStore(v) { mgr.#relayReceiptStore = v; },
1227
+ get sealLeafStore() { return mgr.#sealLeafStore; },
1228
+ set sealLeafStore(v) { mgr.#sealLeafStore = v; },
1229
+ get reservationWatchdog() { return mgr.#reservationWatchdog; },
1230
+ set reservationWatchdog(v) { mgr.#reservationWatchdog = v; },
1231
+ get detachedRelayClientBuilder() { return mgr.#detachedRelayClientBuilder; },
1232
+ sessionKey: (a, sid) => this.#k(a, sid),
1233
+ requireAgentId: (a) => this.#requireAgentId(a),
1234
+ getDirectoryOnlineToken: (a) => this.getDirectoryOnlineToken(a),
1235
+ withDirectoryCause: (a, refusal) => this.#withDirectoryCause(a, refusal),
1236
+ markInterruptedWithDetails: (a, sid, n, source) => this.#life.markInterruptedWithDetails(a, sid, n, source),
1237
+ });
1238
+ /**
1239
+ * ⚠️ LAST, because it holds every other collaborator by value — it is the path that stands a
1240
+ * session up and tears it down, so it touches all of them. Anything added above this line is
1241
+ * fine; anything added below and handed to it by value is the `undefined` capture this
1242
+ * constructor has now been bitten by three times.
1243
+ */
1244
+ this.#life = new SessionLifecycle({
1245
+ logger: this.#logger,
1246
+ records: this.#records,
1247
+ queries: this.#queries,
1248
+ park: this.#park,
1249
+ held: this.#held,
1250
+ leafRecords: this.#leafRecords,
1251
+ receivers: this.#receivers,
1252
+ ephemerals: this.#ephemerals,
1253
+ liveness: this.#liveness,
1254
+ contentIn: this.#contentIn,
1255
+ contentOut: this.#contentOut,
1256
+ relay: this.#relay,
1257
+ db: () => this.#db,
1258
+ activeNodes: this.#activeNodes,
1259
+ standingReceivers: this.#standingReceivers,
1260
+ standingReceiverCreating: this.#standingReceiverCreating,
1261
+ agentsWantingReceiver: this.#agentsWantingReceiver,
1262
+ counterpartyAddrs: this.#counterpartyAddrs,
1263
+ sessionSeeds: this.#sessionSeeds,
1264
+ frozenSessions: this.#frozenSessions,
1265
+ sessionTerminal: this.#sessionTerminal,
1266
+ get shuttingDown() { return mgr.#shuttingDown; },
1267
+ get autoNatProbers() { return mgr.#autoNatProbers; },
1268
+ get onSessionStateChanged() { return mgr.#onSessionStateChanged; },
1269
+ get onSessionTerminal() { return mgr.#onSessionTerminal; },
1270
+ get retryDrainHook() { return mgr.#retryDrainHook; },
1271
+ sessionKey: (a, sid) => this.#k(a, sid),
1272
+ requireAgentId: (a) => this.#requireAgentId(a),
1273
+ resolveAgentId: (a) => this.resolveAgentId(a),
1274
+ getSessionTree: (a, sid) => this.getSessionTree(a, sid),
1275
+ evictSessionCaches: (a, sid) => this.#evictSessionCaches(a, sid),
1276
+ });
1277
+ this.#srWatchdogIntervalMs = opts.standingReceiverWatchdogIntervalMs ?? 30_000;
1278
+ this.#srReservationRetryMs = opts.standingReceiverReservationRetryMs ?? 5 * 60_000;
1279
+ }
1280
+ /**
1281
+ * CELLO-M7-MSG-001: wire the durable-backstop side effects of the awaiting-ACK
1282
+ * lifecycle. `onPersisted` clears the durable retry_queue entry when a persisted ACK
1283
+ * arrives; `onTtf` records/parks the un-acked content when the TTF timer fires.
1284
+ * Injected by the composition root (daemon.ts) after the RetryQueue exists.
1285
+ */
1286
+ setAwaitingAckHooks(hooks) {
1287
+ this.#onAwaitingPersisted = hooks.onPersisted ?? null;
1288
+ this.#onAwaitingTtf = hooks.onTtf ?? null;
1289
+ this.#onParkFailed = hooks.onParkFailed ?? null;
1290
+ }
1291
+ /**
1292
+ * DOD-RETRYQ-STRAND-1: wire the disposition of durable state a session can no longer drain.
1293
+ * Fires once per transition INTO a status from which no resend can succeed. Injected by the
1294
+ * composition root (daemon.ts) after the RetryQueue exists.
1295
+ */
1296
+ setSessionTerminalHook(hook) {
1297
+ this.#onSessionTerminal = hook;
1298
+ }
1299
+ // ─── Initialization ──────────────────────────────────────────────────────
1300
+ async initialize() {
1301
+ // Step 1: Open the SQLCipher database (DEC-1). The key is the single plaintext key file beside
1302
+ // the DB (DEC-2). Fail-closed (SI-002/AC-011): resolveDbKey refuses to mint a fresh key over an
1303
+ // existing DB, and openEncryptedDatabase throws db_encryption_key_mismatch on a wrong key — there
1304
+ // is no plaintext fallback. Whole-DB encryption supersedes the old per-column cipher (AC-010).
1305
+ // PERSIST-002 (AC-006): one-time migration of pre-story flat-file identity / a plaintext DB into
1306
+ // the encrypted store, BEFORE the key is resolved and the DB opened. A no-op on a fresh install
1307
+ // or an already-encrypted DB. Throws identity_migration_failed on a failed migration (DB-002).
1308
+ const migration = migrateToEncryptedIfNeeded(this.#dbPath, this.#logger);
1309
+ const dbKey = resolveDbKey(this.#dbPath, dbKeyPathFor(this.#dbPath));
1310
+ this.#db = openEncryptedDatabase(this.#dbPath, dbKey, this.#logger);
1311
+ this.#logger.info("persist.db.opened", { encrypted: true, migrated: migration.migrated });
1312
+ // PERSIST-002: the identity store (agents + manifest_state) lives in the same encrypted DB.
1313
+ ensureIdentitySchema(this.#db);
1314
+ /**
1315
+ * ⚠️ THE OWN-CHAIN STORE IS BUILT HERE, NOT LAZILY INSIDE THE RELAY-CLIENT BUILDERS —
1316
+ * `DOD-M15-SELFCHAIN-1`, review F2.
1317
+ *
1318
+ * It was constructed only when a relay client was attached, so a session that never attached one
1319
+ * left it null: nothing was ever recorded, and every message that side sent carried the same
1320
+ * self link. That is the exact defect this unit exists to close, on the path its own comments
1321
+ * call the one that matters most — a conversation that ran while the relay was down is precisely
1322
+ * the one whose order gets disputed later.
1323
+ *
1324
+ * The chain belongs to the AGENT and the SESSION. The relay is how the conversation travels; it
1325
+ * is not what makes the conversation provable.
1326
+ */
1327
+ this.#ownChainStore = new SessionOwnChainStore(this.#db, this.#logger);
1328
+ ensureSessionSchema(this.#db, this.#logger, () => this.#records.loadDivergedFromDb());
1329
+ // Step 2: Detect interrupted sessions (SIGKILL detection — AC-010).
1330
+ // Any 'active' row in a freshly-started daemon is a remnant of a prior
1331
+ // killed process. Batch-update to 'interrupted' before IPC opens.
1332
+ // DOD-AGENT-ID-JOINKEY-1: this sweep spans EVERY agent, so it cannot resolve one name up front.
1333
+ // It scopes its UPDATE by the row's own agent_id and LEFT JOINs `agents` only to LOG a human
1334
+ // name. LEFT, not INNER: an inner join would silently skip a session whose agent row is missing,
1335
+ // leaving it 'active' forever — a stuck row hidden by the query that was meant to find it. An
1336
+ // orphan is instead marked interrupted like any other AND reported loudly.
1337
+ const activeRows = this.#db
1338
+ .prepare(`SELECT s.session_id, s.agent_id, a.agent_name
1339
+ FROM sessions s LEFT JOIN agents a ON a.agent_id = s.agent_id
1340
+ WHERE s.status = 'active'`)
1341
+ .all();
1342
+ if (activeRows.length > 0) {
1343
+ const now = Date.now();
1344
+ const interruptedAt = new Date(now).toISOString();
1345
+ for (const row of activeRows) {
1346
+ try {
1347
+ this.#db
1348
+ .prepare(
1349
+ // DOD-CAP-SELF-HEAL-1: OURS. This is the boot sweep finding sessions a previous
1350
+ // process left `active`; the counterparty did nothing, so they are not charged for it.
1351
+ "UPDATE sessions SET status = 'interrupted', updated_at = ?, interrupted_at = COALESCE(interrupted_at, ?), interrupted_by = 'local' WHERE agent_id = ? AND session_id = ?")
1352
+ .run(now, interruptedAt, row.agent_id, row.session_id);
1353
+ if (row.agent_name === null) {
1354
+ this.#logger.error("session.agent.orphaned", {
1355
+ sessionId: row.session_id,
1356
+ agentId: row.agent_id,
1357
+ impact: "session row references an agent_id with no agents row",
1358
+ });
1359
+ }
1360
+ this.#logger.warn("session.interrupted.detected", {
1361
+ sessionId: row.session_id,
1362
+ agentName: row.agent_name,
1363
+ source: "daemon_restart",
1364
+ });
1365
+ }
1366
+ catch (err) {
1367
+ this.#logger.error("session.interrupt.db.write.failed", {
1368
+ sessionId: row.session_id,
1369
+ error: extractErrorMessage(err),
1370
+ });
1371
+ }
1372
+ }
1373
+ }
1374
+ // DOD-LOOP-1: standing receivers are now PER-AGENT, created when each agent comes online
1375
+ // (cello_start_agent → ensureStandingReceiverForAgent). No daemon-global receiver is created at
1376
+ // init (no agent is online yet). The initiate/accept paths kick off creation on demand if missing.
1377
+ }
1378
+ // ─── Public API ──────────────────────────────────────────────────────────
1379
+ /**
1380
+ * Get the underlying DatabaseSync handle.
1381
+ * Used by the composition root (daemon.ts) to pass to RetryQueue and
1382
+ * NonceDedupStore — they share the same SQLCipher DB file (DAEMON-003 AC-008).
1383
+ */
1384
+ /**
1385
+ * Get the underlying DaemonDatabase handle (the SQLCipher-backed adapter). Used by the
1386
+ * composition root (daemon.ts) to pass to RetryQueue and NonceDedupStore — they share the same
1387
+ * encrypted DB file.
1388
+ */
1389
+ getDb() {
1390
+ if (!this.#db) {
1391
+ throw new Error("SessionNodeManager not initialized — call initialize() first");
1392
+ }
1393
+ return this.#db;
1394
+ }
1395
+ // ─── M8C-INBOX-1 (N2/N3): read-watermark accessors ───────────────────────────
1396
+ /** DOD-TIER-4: the DISPLAY/relationship check — is this counterparty a genuine contact (KNOWN or
1397
+ * above)? Replaces the old binary `isContact` for behaviour that keyed on "we have a relationship"
1398
+ * (e.g. the away-response wording). An UNKNOWN-tier contact (a mere row) is NOT known. */
1399
+ isKnown(agentName, pubkey) {
1400
+ return this.#records.getTier(agentName, pubkey) >= TIER.KNOWN;
1401
+ }
1402
+ /** DOD-TIER-4: the POLICY gate — may an inbound session from this counterparty be auto-accepted
1403
+ * when the operator is unattended (WHITELISTED or VIP)? The behavioural consumer is the offline
1404
+ * relay mailbox (LEAVEMSG-1), out of scope for this unit; defined here as the seam. Being merely
1405
+ * KNOWN is NOT enough to auto-accept — whitelisting is the deliberate `cello_contact_set_tier` act. */
1406
+ isAutoAccept(agentName, pubkey) {
1407
+ return this.#records.getTier(agentName, pubkey) >= TIER.WHITELISTED;
1408
+ }
1409
+ /** DOD-AWAY-TIER-1: resolve the most-specific CUSTOM away text for a counterparty, most-specific
1410
+ * first: per-contact `away_message` → per-tier away setting → agent default away setting. Returns
1411
+ * null when none is configured, so the CALLER applies the system default (code) — making the full
1412
+ * four-level resolution TOTAL. A pure read; the resolved text is screened on the outbound path by
1413
+ * the caller like any content (SI — it does not bypass the gateway). */
1414
+ resolveAwayMessage(agentName, pubkey) {
1415
+ // A public key is bytes; its hex case is not part of its identity. Normalized HERE so the
1416
+ // query below cannot see two spellings of one contact — see `contact-pubkey-case.ts`.
1417
+ pubkey = normalizeContactPubkey(pubkey);
1418
+ if (!this.#db)
1419
+ return null;
1420
+ const agentId = this.#requireAgentId(agentName);
1421
+ const row = this.#db
1422
+ .prepare("SELECT away_message FROM contacts WHERE agent_id = ? AND pubkey = ?")
1423
+ .get(agentId, pubkey);
1424
+ if (row?.away_message != null) {
1425
+ this.#logger.debug("contact.away.resolved", { agentName, pubkey, level: "contact" }); // obs AC
1426
+ return row.away_message; // 1. per-contact
1427
+ }
1428
+ const tierName = settableTierName(this.#records.getTier(agentName, pubkey));
1429
+ if (tierName !== null) {
1430
+ const tierAway = this.#records.getSetting(agentName, awayTierSettingKey(tierName));
1431
+ if (tierAway !== null) {
1432
+ this.#logger.debug("contact.away.resolved", { agentName, pubkey, level: "tier" });
1433
+ return tierAway; // 2. per-tier
1434
+ }
1435
+ }
1436
+ const agentDefault = this.#records.getSetting(agentName, AWAY_DEFAULT_KEY);
1437
+ // 3. agent default, else null → caller applies the system default (code). Level logged HERE.
1438
+ this.#logger.debug("contact.away.resolved", { agentName, pubkey, level: agentDefault !== null ? "agent_default" : "system" });
1439
+ return agentDefault;
1440
+ }
1441
+ /** M8C-ABUSE-1 + DOD-TIER-2/3: is a NEW inbound session from this counterparty within the
1442
+ * acceptance bounds? The per-sender cap is now the sender's TIER cap (DEFAULT_TIER_BOUNDS), not a
1443
+ * flat "3 for strangers, unbounded for contacts". This is where DOD-TIER-3 falls out for free: a
1444
+ * BLOCKED sender's cap is 0, so `perSender (>= 0) >= 0` refuses it through the SAME reason and the
1445
+ * SAME path an over-cap UNKNOWN takes — no separate blocked branch, no distinguishing oracle. The
1446
+ * global anti-swarm cap then applies ONLY to UNKNOWN-tier senders (KNOWN+ are trusted, not part of
1447
+ * the stranger pool; BLOCKED never reaches it). Checked BEFORE accepting a fresh inbound session
1448
+ * (counts reflect sessions already active, not yet counting this one). */
1449
+ checkUnknownSenderAcceptanceBound(agentName, counterpartyPubkey) {
1450
+ const tier = this.#records.getTier(agentName, counterpartyPubkey);
1451
+ const perSenderCap = this.#records.resolveTierBound(agentName, tier, "max_sessions");
1452
+ const perSender = this.#queries.countActiveSessionsForCounterparty(agentName, counterpartyPubkey);
1453
+ if (perSender >= perSenderCap) {
1454
+ // BYTE-IDENTICAL to every other refusal, deliberately — DOD-TIER-3. A BLOCKED sender and an
1455
+ // over-cap UNKNOWN must be indistinguishable, or the refusal tells someone they are blocked.
1456
+ // The operator's alarm needs numbers, so it asks for them SEPARATELY via capDiagnostics;
1457
+ // hanging them off this object would put a distinguishing oracle in the return value.
1458
+ return { ok: false, reason: CAPACITY_REASONS.ABUSE_BOUND_SESSIONS_PER_SENDER };
1459
+ }
1460
+ // The global stranger cap is only for the UNKNOWN pool. A KNOWN+ sender is past it by trust;
1461
+ // a BLOCKED sender was already refused above (cap 0).
1462
+ if (tier === TIER.UNKNOWN) {
1463
+ const globalUnknown = this.#queries.countActiveSessionsFromUnknownSenders(agentName);
1464
+ if (globalUnknown >= ABUSE_MAX_UNKNOWN_SESSIONS_GLOBAL) {
1465
+ return { ok: false, reason: CAPACITY_REASONS.ABUSE_BOUND_UNKNOWN_SESSIONS_GLOBAL };
1466
+ }
1467
+ }
1468
+ return { ok: true };
1469
+ }
1470
+ /**
1471
+ * M7-SESSION-001 (M-1 PUSH): register the session-state-change callback.
1472
+ * Called by the composition root (daemon.ts) after the NotificationDispatcher
1473
+ * exists. Setter injection avoids a construction-order/circular dependency.
1474
+ */
1475
+ setOnSessionStateChanged(cb) {
1476
+ this.#onSessionStateChanged = cb;
1477
+ }
1478
+ /**
1479
+ * M8C-MSGWAKE-1: inject the content-arrival callback (daemon.ts → NotificationDispatcher.
1480
+ * dispatchCelloMessage). Setter injection, same construction-order reason as above.
1481
+ */
1482
+ setOnContentArrived(cb) {
1483
+ this.#onContentArrived = cb;
1484
+ }
1485
+ /** M14 / DOD-DOC-INBOUND-2: inject the document-frame interception. See the field's note. */
1486
+ setOnDocumentFrame(cb, classifyOnly) {
1487
+ this.#onDocumentFrame = cb;
1488
+ this.#isDocumentFrame = classifyOnly ?? null;
1489
+ }
1490
+ /**
1491
+ * DOD-LOOP-1: the session core is keyed by (agentName, sessionId), NOT sessionId alone. Two of
1492
+ * the operator's own agents (the loopback case) can hold the two ends of the SAME session_id on
1493
+ * ONE daemon, so a bare session_id is ambiguous between them. This composite string key — the
1494
+ * agent name and the hex session id joined by a 0x1f unit separator (which appears in neither) —
1495
+ * is the key for every in-memory session-core map (#activeNodes, #trees,
1496
+ * #contentDesynced, #responderSealSubmitted, #awaitingAck) and for the per-session maps the
1497
+ * collaborators own, which build the same key the same way. #relayClients is already per-agent
1498
+ * (its own key), and the standing receivers are keyed by agent name directly.
1499
+ */
1500
+ #k(agentName, sessionId) {
1501
+ return `${agentName}\x1f${sessionId}`;
1502
+ }
1503
+ /**
1504
+ * The inverse of `#k`, for the ONE reader that has a key and needs the session id back: the
1505
+ * unpersisted-refusal fallback, which is keyed like every other per-session map but is drained
1506
+ * per AGENT rather than per session.
1507
+ *
1508
+ * Returns null when the key belongs to a different agent. Split on the FIRST separator only —
1509
+ * `agentName` cannot contain 0x1f, so anything after the first one is the session id, and a
1510
+ * greedy split would silently mis-attribute a key rather than reject it.
1511
+ */
1512
+ #unk(key, agentName) {
1513
+ const sep = key.indexOf("\x1f");
1514
+ if (sep < 0)
1515
+ return null;
1516
+ return key.slice(0, sep) === agentName ? key.slice(sep + 1) : null;
1517
+ }
1518
+ /**
1519
+ * DOD-AGENT-ID-JOINKEY-1: resolve an agent's NAME to its STABLE agent_id. This is the ONE place a
1520
+ * name becomes a key, and it is the boundary between the two worlds:
1521
+ *
1522
+ * - ABOVE it, addressing by name is correct. The operator says `cello_use_agent { name }`, and
1523
+ * the in-memory maps (#k, standing receivers, keyProviders) key by name safely, because
1524
+ * name-addressing only ever resolves ACTIVE agents and the `agents_active_name` partial unique
1525
+ * index makes active names unique.
1526
+ * - BELOW it, only `agent_id` may touch SQL. `agent_name` is a mutable display attribute that a
1527
+ * retire frees for reuse; a table joined on it hands a new keypair the dead identity's rows.
1528
+ *
1529
+ * It resolves ONLY non-retired agents — a retired identity is gone from the runtime (`list` omits
1530
+ * it, `start` returns agent_not_found), so no live surface may act as one.
1531
+ *
1532
+ * It THROWS on an unresolvable name rather than returning null. A null would flow into a
1533
+ * `WHERE agent_id IS NULL` that quietly matches nothing: reads would return empty and writes would
1534
+ * vanish, and the daemon would look healthy while losing an agent's data. Every caller has already
1535
+ * resolved the agent before it gets here, so an unresolvable name is a bug in the caller, not a
1536
+ * condition to absorb.
1537
+ */
1538
+ #requireAgentId(agentName) {
1539
+ if (!this.#db)
1540
+ throw new Error("agent_id_unresolved: database is not open");
1541
+ const row = this.#db
1542
+ .prepare("SELECT agent_id FROM agents WHERE agent_name = ? AND state != 'retired'")
1543
+ .get(agentName);
1544
+ if (!row) {
1545
+ this.#logger.error("session.agent_id.unresolved", { agentName });
1546
+ throw new Error(`agent_id_unresolved: no active agent named '${agentName}'. The session tables are keyed by the ` +
1547
+ `stable agent_id (DOD-AGENT-ID-JOINKEY-1); scoping a query by an unresolvable name would ` +
1548
+ `silently match nothing.`);
1549
+ }
1550
+ return row.agent_id;
1551
+ }
1552
+ /**
1553
+ * DOD-AGENT-ID-JOINKEY-1: the public form of the name→stable-id resolver, for components that own
1554
+ * agent-scoped tables of their own (RetryQueue) and must be handed the STABLE key, never a name.
1555
+ * The daemon resolves ONCE, at its own boundary, exactly as this class does internally. Throws on
1556
+ * an unresolvable name — see #requireAgentId for why null is not an option.
1557
+ */
1558
+ resolveAgentId(agentName) {
1559
+ return this.#requireAgentId(agentName);
1560
+ }
1561
+ /**
1562
+ * DOD-M12B-ACK-1 — live `/cello/content/1.0.0` stream counts on a session's direct path, or null
1563
+ * when the session has no active node.
1564
+ *
1565
+ * Answerable at runtime on purpose, in the same spirit as getConnectionMonitorPolicy: the count
1566
+ * is what decides whether the next send survives, and until this existed it could only be
1567
+ * recovered by measuring a log after the fact. It is also what lets the regression assert that a
1568
+ * slot was RELEASED, rather than that some particular number of messages happened to fit.
1569
+ */
1570
+ countSessionContentStreams(agentName, sessionId) {
1571
+ const entry = this.#activeNodes.get(this.#k(agentName, sessionId));
1572
+ if (!entry || typeof entry.node.countProtocolStreams !== "function")
1573
+ return null;
1574
+ return entry.node.countProtocolStreams(entry.counterpartySessionPeerId, CELLO_CONTENT_PROTOCOL_ID);
1575
+ }
1576
+ /**
1577
+ * DOD-M12B-ACK-1 — the live content-stream counts for a peer, as log context.
1578
+ *
1579
+ * A diagnostic must not be able to break the failure path it describes, so a node that predates
1580
+ * `countProtocolStreams` (test fakes do) yields no fields rather than throwing.
1581
+ */
1582
+ #streamCensus(node, peerId) {
1583
+ if (typeof node.countProtocolStreams !== "function")
1584
+ return {};
1585
+ try {
1586
+ const { inbound, outbound } = node.countProtocolStreams(peerId, CELLO_CONTENT_PROTOCOL_ID);
1587
+ return { contentStreamsInbound: inbound, contentStreamsOutbound: outbound, contentStreamsInboundCap: CONTENT_MAX_INBOUND_STREAMS };
1588
+ }
1589
+ catch {
1590
+ return {};
1591
+ }
1592
+ }
1593
+ /** Drop the in-memory tree + received-content caches for a torn-down session (DOD-LOOP-1: per (agent, session)). */
1594
+ #evictSessionCaches(agentName, sessionId) {
1595
+ const key = this.#k(agentName, sessionId);
1596
+ // F1-c: dropping a NON-empty received-content buffer means deliverable plaintext the app
1597
+ // never read live is being discarded (still durable in the transcript). Make that silent
1598
+ // drop diagnosable — it fires on both the destroy (sealed) and retire (sealing) paths.
1599
+ // DOD-COATTEND-1: same correction as the terminal marker above — the buffer is no longer
1600
+ // drained by delivery, so its length no longer means "unread". The watermark does.
1601
+ const unreadCount = this.#records.getUnreadReceivedCount(agentName, sessionId);
1602
+ if (unreadCount > 0) {
1603
+ this.#logger.info("session.receive.buffer.evicted", { sessionId, agentName, unreadCount });
1604
+ }
1605
+ // READ BEFORE THE EVICTION. The held-content loss report below wants the tree size, and asking
1606
+ // for it AFTER this line is not a read — `getSessionTree` misses the cache, reloads the whole
1607
+ // leaf table from disk, and puts the tree straight back into `#trees`, so the diagnostic
1608
+ // resurrects the state its own teardown exists to drop. Worse, that reload goes through
1609
+ // `#requireAgentId`, which THROWS for a retired agent — and it would throw here, before the
1610
+ // held content and high-water map below are cleared, on the abnormal path only.
1611
+ const treeSizeBeforeEviction = this.#trees.get(key)?.size() ?? null;
1612
+ this.#trees.delete(key);
1613
+ // CELLO-M7-MSG-001: cancel any armed TTF timers so a torn-down session never
1614
+ // fires a park backstop (or keeps a timer) after it is gone.
1615
+ this.#contentOut.clearAwaitingForSession(agentName, sessionId);
1616
+ // M7-SESSION-003: drop the direct-path liveness flag (the seal gate already read
1617
+ // its verdict) so a destroyed/retired session retains no stale alive/gone state.
1618
+ this.#liveness.evictSession(agentName, sessionId);
1619
+ // M7-UPGRADE-002: drop the auto-acknowledge bookkeeping for a torn-down session.
1620
+ this.#contentDesynced.delete(key);
1621
+ /**
1622
+ * DOD-M15-NO-SILENT-REFUSAL-1 review N2: the UNPERSISTED half IS torn down, and only that half.
1623
+ * The in-memory fallback (now `RefusalNotices`' own, dropped by its `evictSession`) restores
1624
+ * what the deleted Map did, so it belongs in the teardown set exactly as that Map did. Leaving it out meant a daemon that could not
1625
+ * write to disk — already in trouble — grew without bound in memory as well. The durable rows
1626
+ * stay, for the reason below.
1627
+ */
1628
+ this.#notices.evictSession(agentName, sessionId);
1629
+ // DOD-M15-NO-SILENT-REFUSAL-1: the DURABLE notices are NOT torn down here, and the omission is
1630
+ // deliberate — this list is the documented teardown set, so anything absent from it needs a
1631
+ // reason. They live in `content_refusal_notices`, keyed on agent_id + session_id, and the
1632
+ // question they answer ("why did that person stop replying?") is one an operator asks AFTER a
1633
+ // session ends, most sharply for `session_committed` — a refusal that exists only because the
1634
+ // session was already sealed. Dropping them at seal would delete exactly the ones a sealed
1635
+ // session produces. Growth is one row per (session, reason), i.e. proportional to `sessions`.
1636
+ /**
1637
+ * DOD-M15-REFUSALTERMINAL-1 review F7 — named because this list is the documented teardown set.
1638
+ *
1639
+ * `#terminallyRefused` and `#terminalRefusalsLoaded` are a READ CACHE over
1640
+ * `terminal_content_refusals` and are dropped here with everything else in-memory; the durable
1641
+ * rows stay, for the same reason the notices do — the question they answer outlives the
1642
+ * session, and a fresh check reloads them on demand. `#terminalRefusalsReadFailedAt` goes too,
1643
+ * so a torn-down session's back-off does not delay the first read after it is revived.
1644
+ */
1645
+ this.#refusals.evictSession(agentName, sessionId);
1646
+ this.#responderSealSubmitted.delete(key);
1647
+ // DOD-MSG-4: drop the strict-in-order bookkeeping (witness map, held plaintext, high-water)
1648
+ // so a torn-down session retains no stale ordering state or buffered plaintext.
1649
+ this.#witnessedSeq.delete(key);
1650
+ // DOD-M15-SEALPRECOND-1: a marker cannot outlive the node that would have placed it, or a
1651
+ // revived session's every close is refused for a send that can no longer land.
1652
+ this.#ownLeavesOrdered.delete(key);
1653
+ /**
1654
+ * DOD-M15-SEALWIRE-1 bullet 6 (part A) — both salt maps are CACHES and both go.
1655
+ *
1656
+ * The salt is re-read from `sessions.content_salt` on revival, which is the reason Decision #8
1657
+ * persists it. The contribution is worthless once a salt exists, and if none was agreed yet, a
1658
+ * revival minting a fresh one is correct: the peer either also holds nothing (both re-derive
1659
+ * from the two current halves and reach the same bytes), or it derived against our old half
1660
+ * while we were down — in which case the agreement refuses by name, which is the outcome
1661
+ * Decision #10 asks for. What must never happen is a NEW contribution mid-session without a
1662
+ * teardown, and that is why `#saltContributionFor` mints once rather than per send.
1663
+ */
1664
+ this.#salts.evictSession(agentName, sessionId);
1665
+ // The throwaway secret is destroyed where the `#activeNodes` entry is DROPPED, not here — see
1666
+ // `#destroySessionEphemeralFor`. This call is the belt to that braces: both teardown paths that
1667
+ // evict have already dropped the entry, so it is a no-op for them, and it is what catches any
1668
+ // future path that evicts without going through one of those.
1669
+ this.#ephemerals.destroySessionEphemeralFor(agentName, sessionId);
1670
+ /**
1671
+ * B2b-2's three, and the pending one is SETTLED rather than dropped.
1672
+ *
1673
+ * A `delete` alone leaves any send waiting on that promise waiting until its own timer fires —
1674
+ * five seconds of a torn-down session holding a message that has nowhere to go. `closed` is the
1675
+ * truthful outcome: the agreement is over, because the session is.
1676
+ *
1677
+ * `#hashedWithoutSalt` and `#unsaltedAnnounced` go with the caches, and that is correct rather
1678
+ * than merely tidy. A revived session re-derives its frontier from durable state — leaves, held
1679
+ * rows, queued rows — so the adoption question is answered from disk, not from a flag that would
1680
+ * be a stale in-memory claim about a process that no longer exists. Re-announcing the fallback
1681
+ * once per revival is the right frequency too: it is what an operator reading a fresh log needs
1682
+ * in order to know why this session has no salt.
1683
+ */
1684
+ // SALTSPLIT-1 HIGH-2 goes with its mirror. SETTLED, never deleted: a send awaiting the
1685
+ // agreement must be TOLD the session is gone, not left waiting on a promise nobody resolves.
1686
+ this.#salts.settleSaltPending(agentName, sessionId, "closed");
1687
+ // HELD CONTENT IS LOST HERE, AND IT MUST SAY SO.
1688
+ //
1689
+ // These are frames we RECEIVED and VERIFIED and could not yet append, because the relay's
1690
+ // canonical sequence put them ahead of our tree. Deleting the map drops them — the sender
1691
+ // believes they were delivered, we never applied them, and until now nothing anywhere recorded
1692
+ // that it happened.
1693
+ //
1694
+ // Found live: a document ack arrived, logged `session.content.held` for a one-slot gap, and was
1695
+ // destroyed with the session three seconds later. The sender then re-sent that envelope 90
1696
+ // times against a ceiling of 5, and every surface reported the delivery as merely pending.
1697
+ //
1698
+ // DOD-M12B-STRAND-1 — THIS IS NO LONGER AN EPITAPH. Dropping the Map now drops a CACHE: the
1699
+ // frames are rows in `held_content`, and #restoreHeldContent brings them back the next time
1700
+ // this session gets a node. `session.content.held.discarded` is kept, at WARN rather than
1701
+ // ERROR, and only for what it now means — a gap is still open on a session going away, which
1702
+ // is worth an alarm even though nothing is lost.
1703
+ //
1704
+ // It fires ONLY when the durable rows are confirmed present. A hold whose persist failed is a
1705
+ // genuine loss and must not be reported with the same event as a survivor, so it gets its own
1706
+ // error naming the count that is actually gone. The check is a COUNT against the store rather
1707
+ // than a belief about the write that ran earlier.
1708
+ const strandedHolds = this.#heldContent.get(key);
1709
+ if (strandedHolds && strandedHolds.size > 0) {
1710
+ const canonicalSeqs = [...strandedHolds.keys()].sort((a, b) => a - b);
1711
+ // NULL means "we could not find out", and it must never render as "destroyed". The bare
1712
+ // catch this replaces coerced a failed COUNT to 0, which then claimed every held frame had
1713
+ // been lost — asserting a cause it had not established. It is not hypothetical:
1714
+ // #requireAgentId THROWS for a retired agent, on this exact path, so retiring an agent with
1715
+ // an open hold fabricated a data-loss alarm pointing at the persistence layer while the real
1716
+ // fault was name resolution.
1717
+ let durable = null;
1718
+ try {
1719
+ const row = this.#db?.prepare("SELECT COUNT(*) AS n FROM held_content WHERE agent_id = ? AND session_id = ?").get(this.#requireAgentId(agentName), sessionId);
1720
+ durable = row?.n ?? 0;
1721
+ }
1722
+ catch (err) {
1723
+ this.#logger.warn("session.content.held.durable_count.failed", {
1724
+ agentName, sessionId,
1725
+ impact: "cannot say whether the held frames are durable — reported as unknown, NOT as lost",
1726
+ error: extractErrorMessage(err),
1727
+ });
1728
+ }
1729
+ const lost = durable === null ? null : strandedHolds.size - durable;
1730
+ this.#logger.warn("session.content.held.discarded", {
1731
+ agentName,
1732
+ sessionId,
1733
+ count: strandedHolds.size,
1734
+ durable,
1735
+ canonicalSeqs,
1736
+ // NULL when the tree was not cached at teardown. Honest, and cheap — reloading the leaf
1737
+ // table to fill in a diagnostic field is not worth a disk read on every teardown, let
1738
+ // alone the cache resurrection it caused.
1739
+ treeSize: treeSizeBeforeEviction,
1740
+ });
1741
+ if (lost !== null && lost > 0) {
1742
+ this.#logger.error("session.content.held.lost", {
1743
+ agentName,
1744
+ sessionId,
1745
+ lost,
1746
+ held: strandedHolds.size,
1747
+ impact: "verified content was NOT written to held_content and is destroyed by this teardown — the sender was never acknowledged and believes it is still pending",
1748
+ });
1749
+ }
1750
+ }
1751
+ this.#heldContent.delete(key);
1752
+ // DOD-M12B-STRAND-1: the hydration guard goes with the cache it guards. Without this, a session
1753
+ // torn down and given a node again inside ONE process would never re-read its durable holds —
1754
+ // the frames would sit in the table, unreleasable, which is indistinguishable from the loss
1755
+ // this unit exists to stop.
1756
+ this.#held.evictSession(agentName, sessionId);
1757
+ // DOD-M15-DIVERGE-1: `#diverged` is NOT evicted here, and the omission is the point.
1758
+ //
1759
+ // It was, and that made the gate that reads it best-effort in exactly the population it targets.
1760
+ // This eviction runs on EVERY teardown including `destroySessionNode` with a non-sealed reason,
1761
+ // which writes status `interrupted` — one of the two statuses the seal gate is scoped to. So a
1762
+ // session that diverged and was then torn down arrived at the gate with the fact already
1763
+ // forgotten, and the read site cannot tell "not diverged" from "we forgot": both are
1764
+ // `has() === false`, both read ready, and the close proceeds.
1765
+ //
1766
+ // Divergence is a fact about the DURABLE TREE, not about the live node — the tree keeps the
1767
+ // misplaced leaf whether or not a node exists — so it does not belong to a cache keyed on node
1768
+ // lifetime. It is cleared where it actually stops being true: `clearDivergedOnTerminal`, below.
1769
+ //
1770
+ // NOT YET DURABLE ACROSS A RESTART, stated here rather than left to be rediscovered — the same
1771
+ // way `frontier-mismatch.ts` states its own trade. A daemon restart still empties this set, and
1772
+ // unlike a frontier mismatch (re-detected by the very next close) divergence is only
1773
+ // re-detected by the next send that gets an ack behind the frontier. Until it has a column, a
1774
+ // restarted daemon can read a diverged session as ready. Tracked as `DOD-M15-DIVERGE-DURABLE-1`.
1775
+ // DOD-M12B-SESSION-SEED-1: HAND THEM TO THE REVIVAL RECORD BEFORE DROPPING THEM. This eviction
1776
+ // runs on every teardown, including the interruption a revival is meant to undo — so clearing
1777
+ // the addresses here is what left a revived session unable to dial anyone. The revival record
1778
+ // has exactly the right lifetime for them: it dies when the session reaches a terminal status.
1779
+ const survivingAddrs = this.#counterpartyAddrs.get(key);
1780
+ if (survivingAddrs && survivingAddrs.length > 0) {
1781
+ const identity = this.#sessionSeeds.get(key);
1782
+ if (identity)
1783
+ identity.counterpartyAddrs = [...survivingAddrs];
1784
+ }
1785
+ this.#counterpartyAddrs.delete(key);
1786
+ this.#redialNotBefore.delete(key);
1787
+ this.#highWaterSeq.delete(key);
1788
+ }
1789
+ async gracefulShutdown() {
1790
+ // DOD-NAT-REACHABILITY-1: stop the reservation watchdog before anything is torn
1791
+ // down — a tick landing mid-shutdown would try to rebuild a receiver we are in
1792
+ // the middle of stopping.
1793
+ if (this.#reservationWatchdog !== null) {
1794
+ clearInterval(this.#reservationWatchdog);
1795
+ this.#reservationWatchdog = null;
1796
+ }
1797
+ // Signal any in-flight standing-receiver replacement to self-stop (review M2).
1798
+ this.#shuttingDown = true;
1799
+ // DOD-M12B-ACK-1: drop the inbound-content linger resets. The nodes they would reset are being
1800
+ // torn down here anyway, so firing after this point is pure noise on the way out.
1801
+ for (const timer of this.#lingeringStreams)
1802
+ clearTimeout(timer);
1803
+ this.#lingeringStreams.clear();
1804
+ /**
1805
+ * DOD-M15-RELAYLEAK-1 — **CLOSE THE RELAY CLIENTS. Shutdown never did.**
1806
+ *
1807
+ * This method stops every session NODE and left `#relayClients` untouched — verified by reading:
1808
+ * the whole of `gracefulShutdown` referenced `relayClient` zero times. Each cached client holds
1809
+ * an authenticated libp2p stream to a relay and a reader loop, so a `cello logout` left them
1810
+ * open until the process itself exited.
1811
+ *
1812
+ * That is a real cost rather than untidiness: the relay counts a reservation per client and its
1813
+ * slots are finite, so a daemon that restarts repeatedly consumes them faster than they are
1814
+ * released, which is the "agents cannot get a reservation" failure the relay's own limits note
1815
+ * describes from the other side.
1816
+ *
1817
+ * Best-effort and individually caught: teardown must not be the thing that throws. One client
1818
+ * that refuses to close must not prevent the next from being released.
1819
+ */
1820
+ for (const [key, client] of this.#relayClients) {
1821
+ try {
1822
+ client.close();
1823
+ }
1824
+ catch (err) {
1825
+ this.#logger.warn("session.relay_client.close_failed", {
1826
+ relayClientKey: key,
1827
+ reason: extractErrorMessage(err),
1828
+ impact: "one cached relay client did not close cleanly on shutdown; the rest are still released",
1829
+ });
1830
+ }
1831
+ }
1832
+ this.#relayClients.clear();
1833
+ // Cancel every armed awaiting-ACK timer so an un-acked send (e.g. a rejected /
1834
+ // tampered frame that never produced a `persisted` ACK) does not leave a 20s
1835
+ // timer pinning the content + this manager in memory past teardown (review M1).
1836
+ for (const bySession of this.#awaitingAck.values()) {
1837
+ for (const entry of bySession.values())
1838
+ clearTimeout(entry.timer);
1839
+ }
1840
+ this.#awaitingAck.clear();
1841
+ // Mark ALL 'active' rows interrupted in SQLite — single batch UPDATE covers
1842
+ // both in-memory managed nodes AND any rows that were inserted directly
1843
+ // (e.g. by the binary AC-009 SIGTERM test inserting synthetic rows).
1844
+ // This is the authoritative persistence step; in-memory map is secondary.
1845
+ const now = Date.now();
1846
+ if (!this.#db) {
1847
+ this.#logger.error("session.interrupt.db.write.failed", {
1848
+ sessionId: "__all__",
1849
+ error: "db not initialized",
1850
+ });
1851
+ }
1852
+ else {
1853
+ const interruptedAt = new Date(now).toISOString();
1854
+ try {
1855
+ const res = this.#db.prepare(
1856
+ // DOD-CAP-SELF-HEAL-1: OURS. Our own shutdown ended these, not the counterparty.
1857
+ "UPDATE sessions SET status = 'interrupted', updated_at = ?, interrupted_at = COALESCE(interrupted_at, ?), interrupted_by = 'local' WHERE status = 'active'").run(now, interruptedAt);
1858
+ /**
1859
+ * ⚠️ THE AUTHORITATIVE PERSISTENCE STEP WAS SILENT ON SUCCESS, AND THAT IS WHY ITS OWN TEST
1860
+ * CANNOT BE DIAGNOSED.
1861
+ *
1862
+ * `AC-009 (binary): SIGTERM marks active sessions interrupted` failed twice in a row on CI
1863
+ * and blocked a publish. The captured daemon log ends at `daemon.started` with nothing after
1864
+ * it — because the ONLY thing this block could ever log was a thrown error. So the evidence
1865
+ * is equally consistent with two very different failures:
1866
+ *
1867
+ * - the shutdown never ran (signal handler, early exit, teardown ordering), or
1868
+ * - it ran and the UPDATE matched ZERO rows (visibility, or the rows genuinely were not
1869
+ * `active` at that moment).
1870
+ *
1871
+ * Nothing in the log separates them, so the test's own comment picked one — *"the daemon's
1872
+ * connection can begin its shutdown UPDATE against a snapshot that predates this commit"* —
1873
+ * added a `wal_checkpoint(TRUNCATE)` for it, and the test failed again. One diagnosis, one
1874
+ * fix, one recurrence: the point at which the diagnosis is the thing to doubt.
1875
+ *
1876
+ * `changes` was on the result object the whole time and nobody read it. It discriminates the
1877
+ * two outright, and this is the last line the daemon writes before it dies, so it is the last
1878
+ * thing anyone investigating a bad shutdown will see.
1879
+ *
1880
+ * ⚠️ INFO AND NOT AN ERROR EVEN AT ZERO. Zero is the ordinary case for a daemon with no
1881
+ * active sessions — most shutdowns. Making it a warning would fire on nearly every clean exit
1882
+ * and train operators to filter the one signal that matters.
1883
+ */
1884
+ this.#logger.info("session.interrupt.db.write.complete", {
1885
+ sessionId: "__all__",
1886
+ rowsMarkedInterrupted: Number(res.changes),
1887
+ impact: Number(res.changes) === 0
1888
+ ? "no session rows were 'active' at shutdown, so none was marked interrupted. Ordinary for a daemon with no live sessions — but if a session WAS expected to be interrupted, the UPDATE ran and matched nothing, which is a visibility or state question and NOT a shutdown that failed to run."
1889
+ : "these sessions are recorded as interrupted by OUR OWN shutdown, so they can be resumed or sealed rather than read as abandoned.",
1890
+ });
1891
+ }
1892
+ catch (err) {
1893
+ this.#logger.error("session.interrupt.db.write.failed", {
1894
+ sessionId: "__all__",
1895
+ error: extractErrorMessage(err),
1896
+ });
1897
+ }
1898
+ }
1899
+ // Stop all session nodes, then emit session.node.destroyed only on success
1900
+ // (mirrors destroySessionNode ordering: stop first, log destroyed after)
1901
+ const stopPromises = [];
1902
+ for (const entry of this.#activeNodes.values()) {
1903
+ entry.autoNat.stop();
1904
+ // M7 DOD-SPINE-6: detach from the agent relay client (closes it when its last
1905
+ // session goes) — consistent with the other teardown paths.
1906
+ this.#relay.detachSessionRelay(entry);
1907
+ stopPromises.push(entry.node.stop().then(() => {
1908
+ this.#logger.info("session.node.destroyed", {
1909
+ sessionId: entry.sessionId,
1910
+ agentName: entry.agentName,
1911
+ reason: "interrupted",
1912
+ });
1913
+ }).catch((err) => {
1914
+ this.#logger.error("session.node.stop.failed", {
1915
+ sessionId: entry.sessionId,
1916
+ agentName: entry.agentName,
1917
+ error: extractErrorMessage(err),
1918
+ correlationId: entry.correlationId,
1919
+ });
1920
+ }));
1921
+ }
1922
+ // DOD-M12B-SHUTDOWN-1: BOUNDED. `node.stop()` awaits libp2p's own teardown, which has no
1923
+ // deadline of its own — one connection that will not close holds this, and this holds the whole
1924
+ // daemon. Measured 2026-08-17: `cello logout` acknowledged, then the process was still alive
1925
+ // 30+ seconds later and needed a signal. An abandoned stop costs a socket the OS reclaims when
1926
+ // we exit; an unbounded wait costs the exit itself.
1927
+ await this.#life.boundedTeardown(Promise.all(stopPromises), "session_nodes", stopPromises.length);
1928
+ this.#activeNodes.clear();
1929
+ // Evict in-memory per-session caches (trees reload from SQLite; received-content
1930
+ // plaintext must not survive shutdown in memory).
1931
+ this.#trees.clear();
1932
+ // DOD-M12B-SESSION-SEED-1 (review F5): transport identities are key material and belong in the
1933
+ // same sentence as the plaintext above. Shutdown marks every active row `interrupted` by direct
1934
+ // SQL, so no `SessionLifecycle.updateSessionStatus` destroy fires for them — without this, every live session's
1935
+ // seed survives the shutdown in memory for as long as the process lingers.
1936
+ for (const identity of this.#sessionSeeds.values())
1937
+ identity.seed.fill(0);
1938
+ this.#sessionSeeds.clear();
1939
+ // 006-CRYPTO: the per-session throwaway secrets belong in the same sentence, for the same
1940
+ // reason and with the same measured cause — shutdown marks rows `interrupted` by direct SQL, so
1941
+ // no per-session teardown fires for them, and this process is known to linger (a `cello logout`
1942
+ // was still alive 30+ seconds later). Without this, every live session's key survives the
1943
+ // shutdown in memory for as long as it lingers.
1944
+ this.#ephemerals.destroyAll();
1945
+ // Stop ALL per-agent standing receivers (DOD-LOOP-1). In PARALLEL and BOUNDED: this was a
1946
+ // sequential await per agent with no deadline, so five agents meant five chances for one stuck
1947
+ // libp2p teardown to hold the exit — and it sits between the operator being told the daemon is
1948
+ // stopping and the process actually going.
1949
+ await this.#life.boundedTeardown(Promise.all([...this.#standingReceivers].map(async ([agentName, sr]) => {
1950
+ sr.autoNat.stop();
1951
+ try {
1952
+ await sr.node.stop();
1953
+ }
1954
+ catch (err) {
1955
+ this.#logger.error("session.node.stop.failed", {
1956
+ sessionId: "standing_receiver_shutdown",
1957
+ agentName: `${STANDING_RECEIVER_AGENT_NAME}:${agentName}`,
1958
+ error: extractErrorMessage(err),
1959
+ correlationId: "n/a",
1960
+ });
1961
+ }
1962
+ })), "standing_receivers", this.#standingReceivers.size);
1963
+ // Same reason: a receiver's seed is the identity it has already advertised in
1964
+ // `session_offer_accept` for any session it is mid-handshake on.
1965
+ for (const sr of this.#standingReceivers.values())
1966
+ sr.seed.fill(0);
1967
+ this.#standingReceivers.clear();
1968
+ this.#srReservationRetry.clear();
1969
+ this.#srLastRejectionReason.clear();
1970
+ // Release the SQLite handle so the DB file is no longer held open after shutdown
1971
+ // (review L5). Queries guard on `#db === null` and degrade to empty/null.
1972
+ if (this.#db) {
1973
+ try {
1974
+ this.#db.close();
1975
+ }
1976
+ catch { /* already closed */ }
1977
+ this.#db = null;
1978
+ }
1979
+ }
1980
+ /**
1981
+ * DOD-M12B-REVIVAL-BOUND-1 — close every session the revival window has expired.
1982
+ *
1983
+ * `abandonSession` is the right instrument and already exists: it flips the status FIRST and
1984
+ * synchronously, annexes held content so the operator does not lose mail that has nowhere to go,
1985
+ * and retires the node. It notarizes nothing, which is the point — we are closing a door, not
1986
+ * asserting how it came to be open.
1987
+ *
1988
+ * One session's failure must not strand the rest, so each is caught and logged; the sweep runs at
1989
+ * boot beside the restart-seal resolver and a throw there would take the daemon with it.
1990
+ *
1991
+ * @returns how many sessions actually flipped — not how many were attempted.
1992
+ */
1993
+ async closeExpiredUnrevivableSessions(nowMs, windowMs) {
1994
+ // Stamp FIRST. A row with no clock cannot be evaluated by the query below, and this is the only
1995
+ // thing that gives it one. Running it before every sweep is safe because the write is scoped to
1996
+ // rows that have no timestamp yet.
1997
+ this.#queries.stampMissingInterruptedAt(nowMs);
1998
+ const expired = this.#queries.listExpiredUnrevivableSessions(nowMs, windowMs);
1999
+ let closed = 0;
2000
+ for (const s of expired) {
2001
+ try {
2002
+ if (await this.#life.abandonSession(s.agentName, s.sessionId)) {
2003
+ closed += 1;
2004
+ this.#logger.info("session.revival_bound.closed", {
2005
+ agentName: s.agentName,
2006
+ sessionId: s.sessionId,
2007
+ // The cause we could NOT establish is the reason this ends without a receipt — log it
2008
+ // so an operator asking "why no certificate?" gets the answer here.
2009
+ interruptedBy: s.cause ?? "unknown",
2010
+ windowMs,
2011
+ // NAME THE FORFEIT. Until this sweep ran, `cello_close_session` (without `force`) still
2012
+ // accepted this session — it takes `status IN ('active','interrupted')` — so the
2013
+ // operator could have come back days later and obtained a real seal. `abandoned` is in
2014
+ // TERMINAL_SEAL_REFUSALS, so after this they cannot. That is a deliberate trade (SI-001
2015
+ // forbids auto-sealing a session nobody chose to end) and it costs something real, so
2016
+ // it is stated rather than left implicit in a WHERE clause.
2017
+ forfeited: "a seal was still obtainable by hand until now; it is not after this",
2018
+ });
2019
+ }
2020
+ }
2021
+ catch (err) {
2022
+ this.#logger.warn("session.revival_bound.close.failed", {
2023
+ agentName: s.agentName,
2024
+ sessionId: s.sessionId,
2025
+ error: extractErrorMessage(err),
2026
+ });
2027
+ }
2028
+ }
2029
+ // UNCONDITIONAL. A sweep that found nothing and a sweep that never really ran must not produce
2030
+ // the same silence — this line is the only proof the control executed at all.
2031
+ this.#logger.info("session.revival_bound.sweep", { expired: expired.length, closed, windowMs });
2032
+ if (closed !== expired.length) {
2033
+ // Not arithmetic for the reader to do: a session the security control failed to close is one
2034
+ // that is still interrupted and still accepting content.
2035
+ this.#logger.warn("session.revival_bound.sweep.incomplete", {
2036
+ expired: expired.length,
2037
+ closed,
2038
+ failed: expired.length - closed,
2039
+ impact: "these sessions are still interrupted and still accept content from any peer that dials them",
2040
+ });
2041
+ }
2042
+ return closed;
2043
+ }
2044
+ getSessionsForAgent(agentName) {
2045
+ if (!this.#db)
2046
+ return [];
2047
+ // Scoped by the STABLE id. `agent_name` is not a column of `sessions` any more, so it is stamped
2048
+ // back on for display — and it is exactly the name we just resolved the id FROM, so no join is
2049
+ // needed and no stale copy can exist.
2050
+ const rows = this.#db
2051
+ .prepare("SELECT * FROM sessions WHERE agent_id = ? ORDER BY updated_at DESC")
2052
+ .all(this.#requireAgentId(agentName));
2053
+ return rows.map((r) => ({ ...this.#salts.saltStatusOf(r, agentName), agent_name: agentName }));
2054
+ }
2055
+ /**
2056
+ * Every persisted session across ALL agents, most-recently-updated first. Backs the daemon-wide
2057
+ * `cello sessions` CLI surface (which has no per-connection current agent, unlike the MCP
2058
+ * cello_list_sessions). Classification + filtering + the count limit are applied by the caller.
2059
+ */
2060
+ getAllSessions() {
2061
+ if (!this.#db)
2062
+ return [];
2063
+ // Spans EVERY agent, so no single name can be resolved up front: the display name is joined in
2064
+ // from `agents`, its one source of truth. LEFT JOIN, not INNER — a session whose agent row is
2065
+ // missing must still be listed (an invisible session is worse than an unnamed one).
2066
+ return this.#db
2067
+ .prepare(`SELECT s.*, a.agent_name AS agent_name
2068
+ FROM sessions s LEFT JOIN agents a ON a.agent_id = s.agent_id
2069
+ ORDER BY s.updated_at DESC`)
2070
+ .all()
2071
+ // The joined display name is what `#saltSuspended` is keyed on. NULL only where the agent row
2072
+ // is missing — an orphaned session, which has no live in-memory state to be suspended in.
2073
+ .map((r) => {
2074
+ const row = r;
2075
+ return this.#salts.saltStatusOf(row, row.agent_name ?? null);
2076
+ });
2077
+ }
2078
+ // ─── The session-lifecycle path's public surface, kept on the manager ──────────────────────
2079
+ //
2080
+ // These thirteen live in `session-lifecycle.ts`; their documentation is there, next to the code
2081
+ // it describes. They stay reachable here because 429 call sites outside this class name them on
2082
+ // the manager — `createSessionNode` alone has 177. Signatures are DERIVED, not copied.
2083
+ createSessionNode(...args) { return this.#life.createSessionNode(...args); }
2084
+ acceptSession(...args) { return this.#life.acceptSession(...args); }
2085
+ destroySessionNode(...args) { return this.#life.destroySessionNode(...args); }
2086
+ retireSessionNode(...args) { return this.#life.retireSessionNode(...args); }
2087
+ markInterruptedWithDetails(...args) { return this.#life.markInterruptedWithDetails(...args); }
2088
+ connectToCounterparty(...args) { return this.#life.connectToCounterparty(...args); }
2089
+ notifyCounterpartyAbandon(...args) { return this.#life.notifyCounterpartyAbandon(...args); }
2090
+ retireOnCounterpartyAbandon(...args) { return this.#life.retireOnCounterpartyAbandon(...args); }
2091
+ reviveSessionNode(...args) { return this.#life.reviveSessionNode(...args); }
2092
+ reviveIfNeededForSend(...args) { return this.#life.reviveIfNeededForSend(...args); }
2093
+ reviveIfNeededForRead(...args) { return this.#life.reviveIfNeededForRead(...args); }
2094
+ abandonSession(...args) { return this.#life.abandonSession(...args); }
2095
+ getSessionNodePeerId(...args) { return this.#life.getSessionNodePeerId(...args); }
2096
+ /**
2097
+ * ⚠️ EVERY TRIVIAL DELEGATOR IN THIS FILE IS ONE LINE, and that is a ratchet decision rather than
2098
+ * a formatting preference. Thirty-seven of them were five lines each: a signature line, a
2099
+ * `Parameters<>` line, a `ReturnType<>` line, a forward and a brace. That is ~150 lines of the
2100
+ * budget spent on punctuation, in the file the `max-lines` pin exists to shrink — and it is
2101
+ * budget that cannot then be spent on the comments this file's prose is actually for.
2102
+ *
2103
+ * The signatures stay DERIVED, which is the property that matters: a change to the moved method
2104
+ * changes this one, so a copy can never drift from its original.
2105
+ */
2106
+ // ─── The relay path's public surface, kept on the manager ──────────────────────────────────
2107
+ //
2108
+ // These nine live in `session-relay.ts`; their documentation is there, next to the code it
2109
+ // describes. They stay reachable here because 43 call sites outside this class name them on the
2110
+ // manager. Signatures are DERIVED, not copied, for the same reason as the seal and content
2111
+ // delegators: a copy is a second declaration free to drift from the first.
2112
+ setDirectoryRelayEndpoints(...args) { return this.#relay.setDirectoryRelayEndpoints(...args); }
2113
+ registerRelayStream(...args) { return this.#relay.registerRelayStream(...args); }
2114
+ isRelayCarvedOutInbound(...args) { return this.#relay.isRelayCarvedOutInbound(...args); }
2115
+ isRelayQuarantined(...args) { return this.#relay.isRelayQuarantined(...args); }
2116
+ quarantineRefusedInbound(...args) { return this.#relay.quarantineRefusedInbound(...args); }
2117
+ quarantineFrameMeta(...args) { return this.#relay.quarantineFrameMeta(...args); }
2118
+ getRelayReceipts(...args) { return this.#relay.getRelayReceipts(...args); }
2119
+ patchRelayClientForTest(...args) { return this.#relay.patchRelayClientForTest(...args); }
2120
+ getSessionRelayForTest(...args) { return this.#relay.getSessionRelayForTest(...args); }
2121
+ // ─── The seal path's public surface, kept on the manager ───────────────────────────────────
2122
+ //
2123
+ // These eleven live in `session-seal.ts`; their documentation is there, next to the code it
2124
+ // describes. They stay reachable here because 86 call sites outside this class — the close
2125
+ // handler, the seal coordinator, the escalation and certificate-pull paths, and `daemon.ts`
2126
+ // itself — name them on the manager. Signatures are DERIVED for the same reason as the content
2127
+ // delegators below: a copy is a second declaration free to drift from the first.
2128
+ submitSealLeaf(...args) { return this.#seal.submitSealLeaf(...args); }
2129
+ /**
2130
+ * DOD-M15-AWAYSCOPE-1 — tell every relay this agent is talking through whether anyone is watching.
2131
+ *
2132
+ * ⚠️ THIS IS WHAT REPLACED THE AWAY REPLY. An unattended agent used to answer inbound messages
2133
+ * with its greeting, which took a hash-chain leaf inside a live conversation and cost session
2134
+ * `e7dd3f43…` its receipt on both machines. The fact is worth telling; it is a fact ABOUT the
2135
+ * session, so it rides the out-of-band liveness frame and takes no leaf, enters no transcript, and
2136
+ * is never seen by the counterparty's agent as a message.
2137
+ *
2138
+ * BEST EFFORT AND SILENT ON FAILURE, by design. A session with no relay (direct-only) is skipped;
2139
+ * a send that fails is logged at debug inside the client. The cost of every failure is the same
2140
+ * and it is small: the counterparty reads "unknown" until the next notice.
2141
+ */
2142
+ /**
2143
+ * DOD-M15-AWAYSCOPE-1 — who answers "is anyone attending this agent". Set by the daemon.
2144
+ *
2145
+ * Defaults to `unattended` rather than throwing or guessing `attended`: before the composition
2146
+ * root wires it, the truthful answer is that nobody has claimed anything. Defaulting the other
2147
+ * way would tell a counterparty a person is watching during exactly the window in which the
2148
+ * daemon has not finished starting.
2149
+ */
2150
+ #currentAttendance = () => "unattended";
2151
+ setCurrentAttendanceSource(fn) {
2152
+ this.#currentAttendance = fn;
2153
+ }
2154
+ announceAttendance(agentName, attendance) {
2155
+ // The separator is `#k`'s own `\x1f`, not a colon — a colon is legal inside an agent name and
2156
+ // the map has never used one. Getting this wrong is silent: the loop matches nothing and every
2157
+ // announcement is skipped, with no error anywhere.
2158
+ const prefix = this.#k(agentName, "");
2159
+ for (const [key, entry] of this.#activeNodes) {
2160
+ if (!key.startsWith(prefix))
2161
+ continue;
2162
+ if (!entry.relayClient || !entry.relaySessionIdBytes)
2163
+ continue;
2164
+ entry.relayClient.announceAttendance(entry.node, entry.relaySessionIdBytes, attendance);
2165
+ }
2166
+ }
2167
+ /**
2168
+ * DOD-M15-AWAYSCOPE-1 — ask the relay what it knows about the counterparty of ONE session.
2169
+ *
2170
+ * ⚠️ NOT `getSessionLiveness`, which is a different question with a different answer. That one is
2171
+ * daemon-local: does THIS process hold a libp2p connection for the session, and it has a fourth
2172
+ * value (`impaired`) the wire type does not. This one asks the RELAY, which sees the counterparty's
2173
+ * standing connection even when this daemon has no direct link to them, and it is the only path
2174
+ * that can carry attendance — because attendance is the far daemon's own assertion, relayed.
2175
+ *
2176
+ * Returns null when there is nothing to ask: no session, or a session with no relay behind it.
2177
+ * That is distinct from an answer of 'unknown', which means the relay was asked and did not know.
2178
+ */
2179
+ async queryRelayLiveness(agentName, sessionId) {
2180
+ const entry = this.#activeNodes.get(this.#k(agentName, sessionId));
2181
+ if (!entry?.relayClient || !entry.relaySessionIdBytes)
2182
+ return null;
2183
+ const record = this.getSessionRecord(agentName, sessionId);
2184
+ if (!record)
2185
+ return null;
2186
+ return entry.relayClient.queryLiveness(entry.relaySessionIdBytes, Uint8Array.from(Buffer.from(record.counterparty_pubkey, "hex")));
2187
+ }
2188
+ sealReadiness(...args) { return this.#seal.sealReadiness(...args); }
2189
+ sealReadinessView(...args) { return this.#seal.sealReadinessView(...args); }
2190
+ verifyCertifiedRoot(...args) { return this.#seal.verifyCertifiedRoot(...args); }
2191
+ getSealCarry(...args) { return this.#seal.getSealCarry(...args); }
2192
+ markSealed(...args) { return this.#seal.markSealed(...args); }
2193
+ recordSealCertificateEnsuringRow(...args) { return this.#seal.recordSealCertificateEnsuringRow(...args); }
2194
+ persistSealInterruptedCommitment(...args) { return this.#seal.persistSealInterruptedCommitment(...args); }
2195
+ getSealUpgradeReadiness(...args) { return this.#seal.getSealUpgradeReadiness(...args); }
2196
+ countersignedThroughSeqFromCarry(...args) { return this.#seal.countersignedThroughSeqFromCarry(...args); }
2197
+ setEnsureSealBroker(...args) { return this.#seal.setEnsureSealBroker(...args); }
2198
+ // ─── The content pipeline's public surface, kept on the manager ────────────────────────────
2199
+ //
2200
+ // Two of these live in `session-content-send.ts` and five in `session-content-ingest.ts`; the
2201
+ // return types below say which. Their documentation is there, next to the code it describes.
2202
+ // They stay reachable here because 451 call sites across the daemon and its tests name them on
2203
+ // the manager, and moving those is a change to every caller for no gain.
2204
+ //
2205
+ // ⚠️ The signatures are DERIVED, not copied. `sendContent` alone carries ~60 lines of parameter
2206
+ // documentation and a five-branch return type; a hand-copied duplicate of that is a second
2207
+ // declaration that drifts from the first, silently, the moment either is edited. Deriving them
2208
+ // makes drift impossible — a change to the moved signature is a change to this one.
2209
+ ingestReceivedContent(...args) { return this.#contentIn.ingestReceivedContent(...args); }
2210
+ /**
2211
+ * ⚠️ **THE CHOKE POINT, AND THE GATE BELONGS HERE RATHER THAN ON `cello_send` ALONE — review F2.**
2212
+ *
2213
+ * `DOD-M15-CLOSEDSESSION-1` put a closed-session refusal on the `cello_send` handler, which is
2214
+ * where the operator's sentence is shaped. Three other production callers place content leaves
2215
+ * without passing through it — the document delivery transport, and both away-reply sends in the
2216
+ * attendance wiring — and each reuses a session whose row says `active`, which is exactly the
2217
+ * row the seal ceremony leaves standing while it finishes. So a document push could still land a
2218
+ * leaf in a conversation this side had already sealed.
2219
+ *
2220
+ * This is the argument the inbound path already makes for its own shared gate: placing the check
2221
+ * above the dispatch makes it the DEFAULT, so a fourth caller added later is covered by
2222
+ * construction rather than by whoever adds it reading this comment.
2223
+ *
2224
+ * `durable: false` is the load-bearing field: it tells the caller nothing was witnessed and
2225
+ * nothing was queued, so no leaf may be committed for this.
2226
+ */
2227
+ async sendContent(...args) {
2228
+ const [agentName, sessionId] = args;
2229
+ const closed = sessionClosedState(this.#queries.getSessionRecord(agentName, sessionId)?.status ?? null, this.hasCommittedSealLeaf(agentName, sessionId));
2230
+ if (closed.closed) {
2231
+ this.#logger.info("session.send.refused_closed", { agentName, sessionId, at: "sendContent", impact: closed.impact });
2232
+ return { ok: false, reason: SESSION_CLOSED_REASON, error: closed.impact, durable: false };
2233
+ }
2234
+ return this.#contentOut.sendContent(...args);
2235
+ }
2236
+ /** DOD-M15-SEALPRECOND-1 — see SessionContentSender.noteOwnLeafOrdered. */
2237
+ noteOwnLeafOrdered(...args) { return this.#contentOut.noteOwnLeafOrdered(...args); }
2238
+ placeOwnLeaf(...args) { return this.#contentOut.placeOwnLeaf(...args); }
2239
+ recordWitnessedSequence(...args) { return this.#contentIn.recordWitnessedSequence(...args); }
2240
+ getUndeliverableSeqs(...args) { return this.#contentIn.getUndeliverableSeqs(...args); }
2241
+ handleContentFrameForTest(...args) { return this.#contentIn.handleContentFrameForTest(...args); }
2242
+ // ─── DAEMON-004: daemon-owned Merkle tree ──────────────────────────────────
2243
+ /** Loaded from SQLite on first access so it survives a restart (AC-007). NEVER null: an unknown session yields an EMPTY tree. */
2244
+ getSessionTree(agentName, sessionId) {
2245
+ const key = this.#k(agentName, sessionId);
2246
+ const cached = this.#trees.get(key);
2247
+ if (cached)
2248
+ return cached;
2249
+ const tree = this.#queries.loadTreeFromDb(agentName, sessionId);
2250
+ this.#trees.set(key, tree);
2251
+ return tree;
2252
+ }
2253
+ /** Current daemon-owned tree root for a session, as hex. */
2254
+ getSessionTreeRootHex(agentName, sessionId) {
2255
+ return this.getSessionTree(agentName, sessionId).rootHex();
2256
+ }
2257
+ /**
2258
+ * Append a leaf (by its 32-byte leaf-hash hex) to the daemon-owned tree,
2259
+ * persist it, advance the root, and fire session.tree.appended.
2260
+ *
2261
+ * @returns the new leaf index and the recomputed root hex.
2262
+ */
2263
+ appendSessionLeaf(agentName, sessionId, kind, leafHashHex, correlationId) {
2264
+ const tree = this.getSessionTree(agentName, sessionId);
2265
+ const { leafIndex, newRootHex } = tree.appendLeafHash(kind, leafHashHex);
2266
+ if (this.#db) {
2267
+ try {
2268
+ this.#db
2269
+ .prepare(`INSERT INTO session_tree_leaves
2270
+ (agent_id, session_id, leaf_index, leaf_kind, leaf_hash_hex, created_at)
2271
+ VALUES (?, ?, ?, ?, ?, ?)`)
2272
+ .run(this.#requireAgentId(agentName), sessionId, leafIndex, kind, leafHashHex, Date.now());
2273
+ // DAEMON-004 (finding #2): keep sessions.message_count synced to the tree
2274
+ // size. message_count is the bilateral leafCount the seal flow signs over
2275
+ // (handleSealInterruptedFlow / the responder). If it diverged from the
2276
+ // daemon-owned tree, a post-active-messaging seal would attest to a
2277
+ // truncated transcript and the bilateral leafCount check would mismatch.
2278
+ // The tree (leafIndex + 1 leaves) is authoritative; the column tracks it.
2279
+ this.#db
2280
+ .prepare("UPDATE sessions SET message_count = ?, updated_at = ? WHERE agent_id = ? AND session_id = ?")
2281
+ .run(leafIndex + 1, Date.now(), this.#requireAgentId(agentName), sessionId);
2282
+ }
2283
+ catch (err) {
2284
+ // A persist failure must be visible, not swallowed: the in-memory tree
2285
+ // has advanced but the durable transcript has not, which would diverge
2286
+ // on restart. Surface it loudly.
2287
+ this.#logger.error("session.tree.persist.failed", {
2288
+ sessionId,
2289
+ leafIndex,
2290
+ error: extractErrorMessage(err),
2291
+ correlationId,
2292
+ });
2293
+ }
2294
+ }
2295
+ this.#logger.info("session.tree.appended", {
2296
+ sessionId,
2297
+ leafIndex,
2298
+ newRootHex,
2299
+ correlationId,
2300
+ });
2301
+ return { leafIndex, newRootHex };
2302
+ }
2303
+ #markContentUnverifiable(agentName, sessionId, why) {
2304
+ const key = this.#k(agentName, sessionId);
2305
+ if (why === "unverifiable" && this.#contentDesynced.get(key) === "tampered")
2306
+ return;
2307
+ this.#contentDesynced.set(key, why);
2308
+ }
2309
+ /** DOD-M12B-LEAF-TRIGGERS-FETCH-1 test seams. */
2310
+ setLeafFetchGraceMsForTest(ms) { this.#leafFetchGraceMs = ms; }
2311
+ markContentPresentForTest(agentName, sessionId, contentHashHex) {
2312
+ this.#contentIn.markContentResolved(agentName, sessionId, contentHashHex);
2313
+ }
2314
+ /**
2315
+ * DOD-CAP-SELF-HEAL-1 — the numbers behind a cap refusal, for the OPERATOR'S alarm only.
2316
+ *
2317
+ * Kept off `checkUnknownSenderAcceptanceBound`'s return on purpose. That refusal is byte-identical
2318
+ * across tiers by design (DOD-TIER-3) so a blocked party cannot tell blocking from throttling;
2319
+ * attaching the counts to it would put the oracle straight into the value the refusal path
2320
+ * carries. This is a separate, purely local read, and nothing it returns crosses the wire.
2321
+ */
2322
+ capDiagnostics(agentName, counterpartyPubkey) {
2323
+ const tier = this.#records.getTier(agentName, counterpartyPubkey);
2324
+ const cap = this.#records.resolveTierBound(agentName, tier, "max_sessions");
2325
+ const counted = this.#queries.countActiveSessionsForCounterparty(agentName, counterpartyPubkey);
2326
+ return {
2327
+ tier, cap, counted,
2328
+ // How many to close to get UNDER the cap — not how many exist. At 5 against a cap of 3 the
2329
+ // answer is 3, and "close 5" tells the operator to do more than the job needs.
2330
+ mustClear: Math.max(0, counted - cap + 1),
2331
+ blocked: tier === TIER.BLOCKED,
2332
+ };
2333
+ }
2334
+ /**
2335
+ * THE IN-MEMORY ARRIVAL BUFFER WAS HERE, with `takeReceivedContent`, `pushReceivedContentForTest`
2336
+ * and `peekLatestReceivedContentHex`. All three are gone, and so is the buffer.
2337
+ *
2338
+ * It kept the plaintext of the last 32 messages of every live session in daemon memory for the
2339
+ * life of the process. `cello_receive` stopped draining it at DOD-COATTEND-1, when delivery moved
2340
+ * onto the durable transcript; the away responder's peek at its tail was the last reader, and
2341
+ * DOD-M15-AWAYSCOPE-1 deleted that with the branch that answered an accepted session. What
2342
+ * remained was every conversation this agent had, held in memory, for nobody.
2343
+ *
2344
+ * Tests that asked it "did this message reach the agent" now ask the transcript, which is what
2345
+ * production serves — see `__tests__/helpers/received-rows.ts` for why the answer is the same.
2346
+ */
2347
+ /**
2348
+ * F1-b: the terminal answer for a session that sealed while a blocking receive was (or could be)
2349
+ * waiting. Idempotent — a sealed session always answers "sealed" to a receive. Null while active.
2350
+ *
2351
+ * DOD-TERMINAL-WAKE-1: the in-memory marker is written only by `destroySessionNode`, so it does
2352
+ * NOT survive a restart — while the `sealed` row on disk does. Reading the absent marker as "not
2353
+ * terminal" is what let a sealed session's unread message come back as live work hours later, and
2354
+ * an agent obey a `[[STANDBY]]` directive out of a conversation that had already ended. Absent is
2355
+ * not fine: fall through to the durable record, which is the authority the marker only caches.
2356
+ */
2357
+ peekTerminalMarker(agentName, sessionId) {
2358
+ const cached = this.#sessionTerminal.get(this.#k(agentName, sessionId));
2359
+ if (cached)
2360
+ return cached;
2361
+ // Only 'sealed' answers here. 'abandoned' forfeited its receipt and 'interrupted' /
2362
+ // 'seal_interrupted_pending' can still complete, so none of them may claim a seal — the
2363
+ // DOD-SEALED-INBOX-2 lesson, which is what makes this a status read and not a "is it over" read.
2364
+ const record = this.#queries.getSessionRecord(agentName, sessionId);
2365
+ if (record?.status !== "sealed")
2366
+ return null;
2367
+ return { type: "sealed", unreadCount: this.#records.getUnreadReceivedCount(agentName, sessionId) };
2368
+ }
2369
+ // ─── CELLO-M7-MSG-001: delivery ACK / TTF tracking (send side) ──────────────
2370
+ /**
2371
+ * TTF timer fired with no `persisted` ACK (AC-003/AC-019): hand the un-acked content
2372
+ * to the park backstop (the durable retry_queue today; the relay store-and-forward
2373
+ * deposit in 3b). The session is never killed and the operator is never interrupted —
2374
+ * parking is best-effort durability.
2375
+ */
2376
+ #handleTtfExpiry(agentName, sessionId, hashHex) {
2377
+ const ackKey = this.#k(agentName, sessionId);
2378
+ const bySession = this.#awaitingAck.get(ackKey);
2379
+ const entry = bySession?.get(hashHex);
2380
+ if (!entry || !bySession)
2381
+ return;
2382
+ bySession.delete(hashHex);
2383
+ if (bySession.size === 0)
2384
+ this.#awaitingAck.delete(ackKey);
2385
+ this.#logger.debug("content.delivery.ttf_expired", { sessionId, contentHash: hashHex });
2386
+ try {
2387
+ // M12-P12 (review pass 2): the ordering record travels on THIS path too. It is in hand — the
2388
+ // very next statement hands it to #parkContent — and a TTF row written without it re-parks in
2389
+ // arrival order, which is the divergent-leaf-index failure the durable columns exist to stop.
2390
+ this.#onAwaitingTtf?.(agentName, sessionId, hashHex, entry.content, entry.structure1Cbor, entry.structure2Cbor, entry.contentHashAlg, entry.structure1Signature, entry.leafKind);
2391
+ }
2392
+ catch (err) {
2393
+ this.#logger.error("content.park.backstop.failed", {
2394
+ sessionId, contentHash: hashHex, error: extractErrorMessage(err),
2395
+ });
2396
+ }
2397
+ // 2b: delivered to the wire but never confirmed `persisted` — deposit it to the relay
2398
+ // store-and-forward so the recipient recovers it (at the witnessed sequence). The durable
2399
+ // awaiting entry above remains the crash backstop. Carry the retained ordering record (review #1)
2400
+ // so a TTF-parked entry self-orders on recover, exactly like the direct-dial-fail park.
2401
+ // Fire-and-forget: unlike sendContent's live caller, nothing here is awaiting an IPC response
2402
+ // to shape (the TTF timer fires long after cello_send already returned) — the deposit's own
2403
+ // success/failure logging inside #parkContent is the only observability this path needs.
2404
+ // B2b-1 review F2 — the THIRD `#parkContent` caller, and the one that was left unthreaded.
2405
+ void this.#park.parkContent(agentName, sessionId, hashHex, entry.content, entry.structure1Cbor, entry.structure2Cbor, entry.contentHashAlg);
2406
+ }
2407
+ /**
2408
+ * DOD-MSG-4 (self-ordering content frame): verify the relay's signed ordering record carried IN the
2409
+ * content frame and record the canonical sequence for the strict-in-order gate — so ordering does
2410
+ * not depend on the separate leaf_deliver witness arriving first. Best-effort: any failure (malformed,
2411
+ * hash mismatch, bad signature, wrong signer) is logged and ignored — the content still ingests and
2412
+ * orders via the witness stream / arrival, so a bad record cannot block delivery.
2413
+ *
2414
+ * structure1_cbor = [1, content_hash(32), sender_pubkey(32), session_id(16), last_seen_seq, ts] —
2415
+ * the EXACT bytes the sender signed (needed to verify; Structure2 omits session_id/last_seen/ts).
2416
+ * structure2_cbor = [seq, sender_pubkey, content_hash, sender_signature, scan_result, prev_root].
2417
+ */
2418
+ /**
2419
+ * DOD-MSG-4 (2b) / SEC-1: decode a park envelope. Legacy/unsigned shapes still DECODE so that
2420
+ * `recoverParkedEntry` can refuse them BY NAME (`unsigned_envelope`) — decoding is not accepting.
2421
+ * Encoding lives in park-envelope.ts and REQUIRES a sender signature (see SEC-1); it is not
2422
+ * exposed here, so no caller can seal an unsigned envelope through this class.
2423
+ */
2424
+ decodeParkEnvelope(plaintext) {
2425
+ return decodeParkEnvelope(plaintext);
2426
+ }
2427
+ /**
2428
+ * DOD-MSG-4 (2b): public entry for the recover path to verify + record a parked entry's ordering
2429
+ * record (the recover handler lives in daemon.ts, which has no access to the private method).
2430
+ */
2431
+ recordOrderingRecord(agentName, sessionId, structure1Cbor, structure2Cbor, contentHash, correlationId) {
2432
+ // DOD-M15-FRAME-1: the POSITION only — this path does not act on `fatal`, because its identity proof is
2433
+ // elsewhere and already fail-closed: the sealed envelope carries the sender's signature over
2434
+ // (session_id, recipient_pubkey, content_hash), and a missing, bad, or wrong-signer envelope is refused.
2435
+ // A second refusal on the ordering record would gate mail on a record the relay-degraded path may omit.
2436
+ // The live direct path is where the ordering record IS the proof, and that is where `fatal` is consumed.
2437
+ const seq = this.#refusals.recordFrameOrdering(agentName, sessionId, structure1Cbor, structure2Cbor, contentHash, correlationId, "park").seq;
2438
+ let agentId = null;
2439
+ try {
2440
+ agentId = this.#requireAgentId(agentName);
2441
+ }
2442
+ catch { /* reported by storeRecoveredPosition */ }
2443
+ if (seq !== null)
2444
+ storeRecoveredPosition({ db: this.#db, logger: this.#logger, agentId, sessionId, canonicalSeq: seq, contentHash, structure1Cbor }); // see recovered-position.ts
2445
+ return seq;
2446
+ }
2447
+ /**
2448
+ * Stop a session whose counterparty's key could not be tied to them.
2449
+ *
2450
+ * Reuses the identity-freeze machinery rather than inventing a second way for a session to stop:
2451
+ * the operator-facing shape, the refusal-to-revive, and the status write are already right there,
2452
+ * and a second mechanism is a second thing to keep correct.
2453
+ */
2454
+ async #freezeSessionForKeyRefusal(agentName, sessionId, reason, correlationId) {
2455
+ /**
2456
+ * THE REASON IS RECORDED BEFORE THE TEARDOWN, AND SURVIVES IT — review F2.
2457
+ *
2458
+ * The teardown destroys this session's key material and used to clear the reason with it, so the
2459
+ * listing recomputed `NOT_YET_AGREED` and the agent was told *"still agreeing its key, sending is
2460
+ * held"* — a reassurance, for the one detection in this unit that means someone may be
2461
+ * substituting keys on the connection. Its only real consumer was a log line.
2462
+ */
2463
+ this.#ephemerals.noteContentEncryptionReason(agentName, sessionId, CONTENT_ENCRYPTION_REASONS.KEY_REFUSED);
2464
+ /**
2465
+ * ⚠️ `"key_refused"`, NOT `"stream_close"` — review F3, and this is error substitution of the
2466
+ * exact kind Invariant 3 names. `stream_close` is written to the row as
2467
+ * `interrupted_by = 'relay_stream_close'`, so a key-authentication refusal was durably recorded
2468
+ * as a relay problem and an operator debugging it would go and look at the relay fleet.
2469
+ */
2470
+ const stopped = await this.#life.markInterruptedWithDetails(agentName, sessionId, 0, "key_refused");
2471
+ /**
2472
+ * OBSERVE THE OUTCOME rather than asserting it — review F11. `markInterruptedWithDetails`
2473
+ * returns early when the row is not `active`, so claiming "the session was stopped" here
2474
+ * unconditionally would state something that did not happen.
2475
+ */
2476
+ this.#logger.error("session.key.session_stopped", {
2477
+ agentName, sessionId, correlationId, reason,
2478
+ stopped,
2479
+ impact: stopped
2480
+ ? "the session was stopped rather than continued unencrypted; a substituted key would otherwise have been handed exactly the plaintext it was reaching for"
2481
+ : "the session was already not active, so nothing was torn down here — the refusal stands and no content was accepted",
2482
+ guidance: CONTENT_ENCRYPTION_GUIDANCE[CONTENT_ENCRYPTION_REASONS.KEY_REFUSED],
2483
+ });
2484
+ }
2485
+ /**
2486
+ * Test seam: see every decoded inbound content frame, as it arrived.
2487
+ *
2488
+ * Review F4. The "bytes on the wire are ciphertext" claim needs the ACTUAL frame; asserting on a
2489
+ * freshly sealed stand-in tests the crypto primitive and stays green when the send path is
2490
+ * reverted to putting plaintext on the wire. There is no other way to reach the decoded frame from
2491
+ * outside — the handler consumes it and hands ingest the plaintext.
2492
+ *
2493
+ * Read-only by construction: the callback receives the frame and cannot influence routing.
2494
+ */
2495
+ observeInboundContentFramesForTest(cb) {
2496
+ this.#inboundFrameObserver = cb;
2497
+ }
2498
+ /** Injected by the daemon once its per-agent key providers exist. See `#keyProviderResolver`. */
2499
+ setKeyProviderResolver(resolver) {
2500
+ this.#keyProviderResolver = resolver;
2501
+ }
2502
+ /**
2503
+ * Test seam: run the auto-acknowledge gate, exactly as the counterparty's SEAL ctrl leaf does.
2504
+ *
2505
+ * `DOD-M15-SEALWIRE-1` part B1, review F-B. The gate has ONE production call site — inside the
2506
+ * relay leaf handler, behind `leaf_kind === CTRL && !authored_by_us` — so reaching it from a test
2507
+ * needs a live relay client delivering a real ctrl leaf. The consequence was measured: my
2508
+ * "tampered never downgrades" test wrapped its decisive assertion in
2509
+ * `if (skipped.length > 0)`, which was ALWAYS FALSE, so the whole `content_tamper` vs
2510
+ * `content_verification_unavailable` branch had no coverage anywhere in the repo and two mutants
2511
+ * on it survived the full gate.
2512
+ *
2513
+ * It calls the REAL method (public on `SessionSeal` since the split, private before it) rather than reproducing its logic, so a test cannot pass
2514
+ * against a decision production does not make.
2515
+ */
2516
+ runAutoAcknowledgeGateForTest(agentName, sessionId, correlationId = "test") {
2517
+ this.#seal.maybeAutoAcknowledgeSeal(agentName, sessionId, correlationId);
2518
+ }
2519
+ /**
2520
+ * Test seam: deliver an inbound salt frame, exactly as the content-stream decoder does.
2521
+ *
2522
+ * 006-CRYPTO finding 2. WHICH of the four reasons the peer gave decides what the operator is told,
2523
+ * and reaching that decision from a test otherwise needs a second live daemon that has closed
2524
+ * adoption for a specific reason — which is not something a counterparty can be asked to do on
2525
+ * demand. The four labels are the whole point of the finding, so they need to be reachable.
2526
+ *
2527
+ * It calls the REAL private handler rather than reproducing its routing, so a test cannot pass
2528
+ * against a decision production does not make. It takes the DECODED frame, so it deliberately
2529
+ * does NOT stand in for the decoder above it — the length and vocabulary checks there have their
2530
+ * own tests driving `handleContentFrameForTest`.
2531
+ */
2532
+ async handleSaltFrameForTest(agentName, sessionId, frame, correlationId = "test") {
2533
+ await this.#salts.handleSaltFrame(agentName, sessionId, frame, correlationId);
2534
+ }
2535
+ /**
2536
+
2537
+
2538
+ /**
2539
+ /**
2540
+ * TEST SEAM — put a session into the state a real one is in between announcing and being answered.
2541
+ *
2542
+ * Reaching that state for real needs a live counterparty connection, which the daemon-level fixtures
2543
+ * do not have. It calls the same private registration the announce path calls, so it cannot drift.
2544
+ */
2545
+ markSaltAgreementPendingForTest(agentName, sessionId, boundMs) {
2546
+ // ANNOUNCED, per this seam's contract above: "between ANNOUNCING and being answered" — AWAYSALT-1's speculative arm is the other state and settles differently.
2547
+ this.#salts.markSaltPending(agentName, sessionId, boundMs, true);
2548
+ }
2549
+ getSessionContentSalt(agentName, sessionId) {
2550
+ return this.#salts.getSessionSalt(agentName, sessionId);
2551
+ }
2552
+ /**
2553
+ * THE SALT, OR WHY THERE ISN'T ONE — `DOD-M15-INCLUSION-1`, fallback-finder finding 2.
2554
+ *
2555
+ * `getSessionContentSalt` above answers `null` for THREE different situations, and a caller that
2556
+ * turns that null into a sentence for an operator gets two of them wrong:
2557
+ *
2558
+ * `none` — no salt was ever agreed. The session really is unsalted.
2559
+ * `unreadable` — a salt row EXISTS and could not be used: the wrong width (corruption on this
2560
+ * operator's own disk), or the read threw.
2561
+ *
2562
+ * The distinction is not cosmetic. `unreadable` means the session's leaves WERE hashed under a
2563
+ * salt, so telling its operator *"this session's content hashes are UNSALTED … start a session
2564
+ * while you are both connected"* is an affirmatively false statement about a security property
2565
+ * their session has, and it points them at their counterparty over damage to their own database.
2566
+ * That is the same defect `#getSessionSalt`'s own F8 note was written to end, re-committed one
2567
+ * surface out.
2568
+ *
2569
+ * ⚠️ IT DELEGATES — there is no second read here. Calling `#getSessionSalt` first means the salt
2570
+ * this reports is the salt the hashing path uses, including its cache and its wrong-width refusal.
2571
+ * A parallel query would be free to disagree with it, which is the whole failure this returns a
2572
+ * reason to prevent.
2573
+ */
2574
+ getSessionContentSaltState(agentName, sessionId) {
2575
+ const salt = this.#salts.getSessionSalt(agentName, sessionId);
2576
+ if (salt)
2577
+ return { salt };
2578
+ if (!this.#db)
2579
+ return { salt: null, reason: "unreadable" };
2580
+ try {
2581
+ const row = this.#db
2582
+ .prepare("SELECT length(content_salt) AS n FROM sessions WHERE agent_id = ? AND session_id = ?")
2583
+ .get(this.#requireAgentId(agentName), sessionId);
2584
+ // A row with a non-empty blob that `#getSessionSalt` still refused is the corruption case: the
2585
+ // bytes are there and they are not a salt. NULL or zero-length is a genuine absence.
2586
+ const stored = row?.n ?? 0;
2587
+ return { salt: null, reason: stored > 0 ? "unreadable" : "none" };
2588
+ }
2589
+ catch {
2590
+ // The read that would tell us which case it is has itself failed, so "no salt was agreed" is
2591
+ // exactly the thing we cannot assert.
2592
+ return { salt: null, reason: "unreadable" };
2593
+ }
2594
+ }
2595
+ /**
2596
+ * Announce our state to the counterparty: a contribution if we hold no salt, a fingerprint if we
2597
+ * do. Called on every counterparty connect — first connection, reconnect and revival alike — and
2598
+ * on receiving a peer contribution.
2599
+ *
2600
+ * ⚠️ THIS PARAGRAPH USED TO SAY *"nothing hashes with the salt yet, so a frame that never lands
2601
+ * costs nothing today"*, and ended by instructing the next unit not to carry it forward unchanged.
2602
+ * **This is that unit, and pass 2 caught me leaving it** — I corrected the same claim inside
2603
+ * `session.salt.announce.failed`'s `impact` in the previous pass and walked past the method header
2604
+ * saying the opposite twenty lines above it.
2605
+ *
2606
+ * As it stands now: the SEND is still best-effort, but a frame that never lands is no longer free.
2607
+ * A first send waiting on this agreement falls back to an unsalted hash, and that is permanent for
2608
+ * the session. The catch therefore settles the pending under `announce_failed` rather than leaving
2609
+ * the send to time out — so the cost is one dial attempt and an accurate reason, instead of five
2610
+ * seconds and a diagnosis blaming the counterparty for a frame we never sent.
2611
+ */
2612
+ async #sendSaltFrame(agentName, sessionId, correlationId, override) {
2613
+ const entry = this.#activeNodes.get(this.#k(agentName, sessionId));
2614
+ if (!entry)
2615
+ return;
2616
+ // `override` is the repair and the mismatch notice: a frame the AGREEMENT chose, which is not
2617
+ // the one our current state would produce. A side that holds a salt normally announces a
2618
+ // fingerprint — the repair has it send its CONTRIBUTION instead, which is the whole mechanism.
2619
+ /**
2620
+ * `#saltState`, NOT a direct call to the minting accessor — review F15, and this one line was
2621
+ * the difference between the safety argument written for the repair and the code that ran.
2622
+ *
2623
+ * It read `ownContribution: this.#salts.saltContributionFor(...)`, which mints unconditionally. Both
2624
+ * arguments are evaluated, so a session that already held a salt got a fresh half minted into
2625
+ * `#saltContributions` even though `ownSaltFrame` discards it on that branch — and
2626
+ * `#evictSessionCaches` drops the half on every teardown while the salt stays on disk, so ANY
2627
+ * revived session hit it, before any inbound frame could be handled.
2628
+ *
2629
+ * The consequence was not cosmetic. `#ownSaltHalf` never returned null, so `STATE_DIVERGENT`
2630
+ * was dead code, and a revived session would REPAIR its counterparty onto a half its salt was
2631
+ * never built from — then both sides froze on `salt_fingerprint_mismatch`, whose guidance sends
2632
+ * the operator to compare build versions with a counterparty that did nothing wrong.
2633
+ */
2634
+ const state = this.#salts.saltState(agentName, sessionId);
2635
+ const frame = override ?? ownSaltFrame(state);
2636
+ if (!frame) {
2637
+ // Neither a salt nor a half. `#saltState` does not produce this, so it is a defect rather
2638
+ // than a state to paper over — and inventing a contribution here is exactly what F15 was.
2639
+ this.#logger.error("session.salt.announce.failed", {
2640
+ agentName, sessionId, correlationId, reason: "no_salt_and_no_contribution",
2641
+ impact: "this side has neither an agreed salt nor a half to offer, so it announced nothing; no half was invented, because one minted now would not be the half any stored salt was built from",
2642
+ });
2643
+ return;
2644
+ }
2645
+ /**
2646
+ * ⚠️ REGISTERED BEFORE THE DIAL, NOT AFTER IT — review Finding 2, and this ordering is the whole
2647
+ * of constraint 2 for the case that actually happens.
2648
+ *
2649
+ * This sat after `await newStream(...)`, which negotiates the protocol with the peer and takes
2650
+ * tens to hundreds of milliseconds. A `cello_send` landing inside that interval found nothing
2651
+ * pending, took the park-only exit, hashed unsalted, and **closed adoption for the life of the
2652
+ * session** — a session with a live counterparty and an agreement about to complete. The feature
2653
+ * off forever, and the log telling the operator their counterparty was on an old build.
2654
+ *
2655
+ * That is precisely the failure constraint 2 exists to prevent, arriving by the one route the
2656
+ * implementation did not cover: the gap between deciding to announce and the frame leaving.
2657
+ *
2658
+ * Park-only is untouched — it never reaches this line, because `#sendSaltFrame` returns at the
2659
+ * `!entry` guard above when there is no active node. And a failed announce settles the waiter in
2660
+ * the catch below rather than leaving a send to sit out the full bound for a frame that never
2661
+ * left.
2662
+ */
2663
+ if (!frame.adoptionClosed)
2664
+ this.#salts.markSaltPending(agentName, sessionId, SALT_AGREEMENT_WAIT_MS, true);
2665
+ // Held outside the try so the catch can retire a stream that was opened and then failed to
2666
+ // write — the same leak, and the same fix, as `#sendDeliveryAck` in `session-content-ingest.ts`.
2667
+ let saltStream;
2668
+ try {
2669
+ const stream = await entry.node.newStream(entry.counterpartySessionPeerId, CELLO_CONTENT_PROTOCOL_ID);
2670
+ saltStream = stream;
2671
+ stream.send(lp.encode.single(encodeCbor({
2672
+ type: "session_salt_agreement",
2673
+ session_id: sessionId,
2674
+ ...(frame.contribution ? { contribution: frame.contribution } : {}),
2675
+ ...(frame.fingerprint ? { fingerprint: frame.fingerprint } : {}),
2676
+ ...(frame.adoptionClosed ? { adoption_closed: frame.adoptionClosed } : {}),
2677
+ })));
2678
+ await stream.close();
2679
+ this.#logger.debug("session.salt.announced", {
2680
+ agentName, sessionId, correlationId,
2681
+ /**
2682
+ * THREE STATES, not two. This read `fingerprint ? holds_salt : offering_contribution`, which
2683
+ * was exhaustive until the adoption refusal put a third frame on the wire — and a refusal
2684
+ * carries no fingerprint, so it logged as `offering_contribution`: the log claiming we asked
2685
+ * for a salt at the exact moment we told the peer we were declining one. An operator reading
2686
+ * the pair would see an offer that was never answered and go looking for a dropped frame.
2687
+ */
2688
+ state: frame.adoptionClosed ? "adoption_closed" : frame.fingerprint ? "holds_salt" : "offering_contribution",
2689
+ });
2690
+ }
2691
+ catch (err) {
2692
+ /**
2693
+ * ⚠️ RELEASE THE WAITER — the frame never left, so there is nothing to wait for.
2694
+ *
2695
+ * Registering before the dial (above) means a failed announce would otherwise leave a first
2696
+ * send holding for the full five seconds against a frame that was never sent. Settling here
2697
+ * makes the failure cost one dial attempt instead of the whole bound, and B2b-2 turned this
2698
+ * from theory into something a user feels: it is the pause before their first message.
2699
+ */
2700
+ this.#salts.settleSaltPending(agentName, sessionId, "announce_failed");
2701
+ this.#logger.warn("session.salt.announce.failed", {
2702
+ agentName, sessionId, correlationId, error: extractErrorMessage(err),
2703
+ // Review F1: this used to end "so no message is affected", which was FALSE in the situation
2704
+ // it fires in — a one-sided announce is exactly what put the two sides out of step, and the
2705
+ // peer used to freeze the session over it. It converges now, so the claim is true again;
2706
+ // it is stated with its reason rather than as a bare reassurance.
2707
+ //
2708
+ // ⚠️ B2b-2 CHANGED THE LAST CLAUSE'S TRUTH. "Nothing hashes with the salt yet" was true for
2709
+ // part A and is now false: a first send waiting on this agreement falls back to sha256 when
2710
+ // it fails, which permanently unsalts the session. Not a message LOST — a protection not
2711
+ // taken, and the sentence has to stop promising otherwise.
2712
+ impact: "the counterparty was not told our salt state on this attempt. We re-announce on every reconnect, and a side that is out of step re-offers its half rather than refusing, so the agreement converges from here. No message is affected, but a first send waiting on this agreement now falls back to an unsalted hash, which is permanent for this session.",
2713
+ });
2714
+ if (saltStream !== undefined) {
2715
+ try {
2716
+ saltStream.abort(err instanceof Error ? err : new Error(String(err)));
2717
+ }
2718
+ catch { /* already gone */ }
2719
+ }
2720
+ }
2721
+ }
2722
+ async #freezeOnIdentityFailure(agentName, sessionId, reason, correlationId) {
2723
+ await this.#freezeSession(agentName, sessionId, reason, {
2724
+ event: "session.content.identity.frozen",
2725
+ observation: "a frame failed to verify against the expected counterparty's key; the session was frozen defensively; cause undetermined",
2726
+ reviveReason: "session_frozen_identity_failure",
2727
+ reviveGuidance: "This session was frozen because a message failed to verify against the expected counterparty's key. " +
2728
+ "Cause undetermined — that signal looks the same whether someone was impersonating your counterparty or CELLO's own delivery mishandled a fallback, so it is recorded as an observation and nothing has been concluded about them.",
2729
+ // Review F1: this said "no further content will be accepted on this session", which was FALSE
2730
+ // as shipped — the next read revived it. It is true now, and it says how it is true, because
2731
+ // an operator who reads "frozen" and then watches the session work again learns to distrust
2732
+ // the log rather than the session.
2733
+ impact: "the content was NOT ingested, NOT displayed and NOT attributed to anyone; the session will not be revived by a read or a send, and only a close or a fresh session moves it now",
2734
+ }, correlationId);
2735
+ }
2736
+ /**
2737
+ * Stop a session and refuse to revive it. Shared by the identity freeze and the salt
2738
+ * disagreement, because the MECHANISM is identical — mark, then tear down — while the two have
2739
+ * nothing else in common and must not describe each other. An identity failure is evidence about
2740
+ * the counterparty; a salt disagreement is usually two builds that do not match, and telling an
2741
+ * operator their counterparty failed a key check when they did not is worse than saying nothing.
2742
+ * Hence the caller supplies both sentences.
2743
+ */
2744
+ async #freezeSession(agentName, sessionId, reason, narrative, correlationId) {
2745
+ // Review F1: MARK BEFORE TEARING DOWN. `destroySessionNode` writes `interrupted`, which is the
2746
+ // revivable status — if the mark landed after, a read racing the teardown could revive the
2747
+ // session out from under the freeze.
2748
+ //
2749
+ // `reviveReason` is SPELLED OUT by the caller rather than built from `reason`. Deriving it looks
2750
+ // tidier and silently changed a shipped contract: the identity freeze's `reason` is the specific
2751
+ // ordering failure (`bad_signature`, `signer_not_counterparty`), so a derived code would have
2752
+ // turned the stable `session_frozen_identity_failure` into a family of varying strings that no
2753
+ // caller matches. Caught by that path's own test.
2754
+ this.#frozenSessions.set(this.#k(agentName, sessionId), {
2755
+ reason: narrative.reviveReason,
2756
+ guidance: narrative.reviveGuidance,
2757
+ });
2758
+ // The EVENT NAME is the caller's, not this method's. Both freezes share a mechanism and nothing
2759
+ // else, and a salt disagreement logged under `…identity.frozen` would tell an operator their
2760
+ // counterparty failed a key check when they did not — a worse outcome than saying nothing.
2761
+ this.#logger.error(narrative.event, {
2762
+ agentName, sessionId, reason, correlationId,
2763
+ observation: narrative.observation,
2764
+ // Review F1: the identity path's impact line said "no further content will be accepted on this
2765
+ // session", which was FALSE as shipped — the next read revived it. It is true now, and it says
2766
+ // how it is true, because an operator who reads "frozen" and then watches the session work
2767
+ // again learns to distrust the log rather than the session.
2768
+ impact: narrative.impact,
2769
+ });
2770
+ try {
2771
+ /**
2772
+ * `"error"` maps to DB status `interrupted`, which is the SAME row an ordinary
2773
+ * counterparty-gone teardown writes — so a freeze is distinguishable in the log and in
2774
+ * behaviour, but not in the session record.
2775
+ *
2776
+ * ⚠️ THIS COMMENT USED TO SAY THE COLUMNS DID NOT EXIST. They do now: `frozen_at` and
2777
+ * `frozen_reason` were added to `sessions` by the salt migration, which carried them for
2778
+ * `DOD-M15-FREEZE-STATUS-1` so that two lanes would not both edit this file's migration list.
2779
+ * What is still missing is a WRITER — that is `FREEZE-STATUS-1`'s work and it belongs to the
2780
+ * other lane, so nothing here fills them in. The gap is unchanged; only the reason for it is.
2781
+ */
2782
+ await this.#life.destroySessionNode(agentName, sessionId, "error");
2783
+ }
2784
+ catch (err) {
2785
+ // The refusal already happened — the content did not ingest. A teardown failure must not
2786
+ // turn a successful refusal into a thrown handler, which would close the stream on a path
2787
+ // that reads as "nothing arrived".
2788
+ this.#logger.warn("session.content.identity.freeze_teardown_failed", {
2789
+ agentName, sessionId, error: extractErrorMessage(err), correlationId,
2790
+ });
2791
+ }
2792
+ }
2793
+ /**
2794
+ * This agent's own last message on a session, from the ONE chain both send paths share.
2795
+ *
2796
+ * `undefined` means it has not spoken here yet, which is the session genesis and not an absence.
2797
+ * The caller supplies that; this does not guess.
2798
+ */
2799
+ #ownChainOf(agentName, sessionId, entry, ownPubkey) {
2800
+ const relayHex = entry?.relaySessionIdBytes
2801
+ ? Buffer.from(entry.relaySessionIdBytes).toString("hex")
2802
+ : sessionId;
2803
+ return entry?.relayClient?.lastOwnHash(relayHex)
2804
+ ?? this.#ownChainStore?.lastOwnHash(Buffer.from(ownPubkey).toString("hex"), sessionId)
2805
+ ?? undefined;
2806
+ }
2807
+ /**
2808
+ * Can a refused message still reach this operator through the relay mailbox? — review F2.
2809
+ *
2810
+ * Feature-detected, not assumed: `openContentSeal` is documented OPTIONAL on `KeyProvider`, and
2811
+ * `content-park.ts` refuses recovery without it. Asking the same resolver `content-park.ts` asks
2812
+ * is what keeps the sentence on the operator's screen tied to what their machine can actually do.
2813
+ */
2814
+ #mailboxRouteAvailable(agentName) {
2815
+ const kp = this.#keyProviderResolver?.(agentName);
2816
+ return kp !== undefined && typeof kp.openContentSeal === "function";
2817
+ }
2818
+ // ─── Private helpers ──────────────────────────────────────────────────────
2819
+ /**
2820
+ * DOD-NAT-REACHABILITY-1 (Phase 2): relay endpoints the DIRECTORY handed this
2821
+ * agent at signaling-auth time — the freshest, health-filtered view of the
2822
+ * relay pool, and the only source a FRESH agent (no session history) has.
2823
+ */
2824
+ #directoryRelayEndpoints = new Map();
2825
+ /**
2826
+ * DOD-M15-RELAYSLOTS-1: the directory-issued online token per agent — the credential the relays
2827
+ * above now require before they will let this agent hold a circuit reservation slot.
2828
+ *
2829
+ * Arrives with `signaling_auth_ok`, on the same frame as the relay endpoints and on the same
2830
+ * cadence: every connect AND every reconnect. Held here rather than passed to a relay client at
2831
+ * construction because it expires within the hour and the client outlives it — the client reads
2832
+ * it through `getDirectoryOnlineToken` at each authentication.
2833
+ */
2834
+ #directoryOnlineTokens = new Map();
2835
+ /**
2836
+ * DOD-M15-RELAYSLOTS-1: accept the directory's online token for an agent. Called on every
2837
+ * signaling connect and reconnect, which is what keeps it fresh.
2838
+ */
2839
+ setDirectoryOnlineToken(agentName, token) {
2840
+ this.#directoryOnlineTokens.set(agentName, token);
2841
+ this.#directoryOnlineTokenAbsent.delete(agentName);
2842
+ }
2843
+ /**
2844
+ * DOD-M15-RELAYSLOTS-1 review M1: the directory issued no token, and this is which absence it was.
2845
+ *
2846
+ * Kept so the relay's eventual `online_token_required` refusal can be reported with the cause the
2847
+ * DIRECTORY knew and the relay never learns — most importantly `not_registered_here`, where the
2848
+ * generic advice ("check that you are reaching a directory") points at a connection that is
2849
+ * working and away from the actual problem.
2850
+ */
2851
+ #directoryOnlineTokenAbsent = new Map();
2852
+ setDirectoryOnlineTokenAbsent(agentName, reason) {
2853
+ this.#directoryOnlineTokens.delete(agentName);
2854
+ this.#directoryOnlineTokenAbsent.set(agentName, reason ?? "unstated");
2855
+ }
2856
+ /**
2857
+ * The current token, or `undefined` when the directory has not issued one — either no directory
2858
+ * connection yet, or this key has no agent profile there. Undefined is a real answer, not a
2859
+ * missing one: the relay refuses without a token, which is the intended outcome for a key the
2860
+ * directory does not recognise.
2861
+ */
2862
+ getDirectoryOnlineToken(agentName) {
2863
+ return this.#directoryOnlineTokens.get(agentName);
2864
+ }
2865
+ /**
2866
+ * DOD-NAT-REACHABILITY-1: circuit-relay listen addresses for an agent's known
2867
+ * relays, so its standing receiver takes reservations and becomes dialable
2868
+ * behind NAT. Sources, merged and deduped by relay peer id: the directory's
2869
+ * auth-time relay pool (freshest — first), then the persisted relay endpoints
2870
+ * of past sessions (getAgentRelayEndpoints — covers a directory that predates
2871
+ * the auth_ok extension).
2872
+ */
2873
+ /**
2874
+ * DOD-M15-RELAYSLOTS-1: relays this agent should skip, and until when — see the failover note in
2875
+ * `reservationCircuitAddrs` (`session-relay.ts`). Keyed agent → relay peer id → expiry.
2876
+ *
2877
+ * Time-boxed rather than permanent because the fault is somebody else's to fix and we will not
2878
+ * hear when they have: an operator sets the missing directory key and restarts, and this agent
2879
+ * should find that relay again without needing its own restart.
2880
+ */
2881
+ #relayQuarantine = new Map();
2882
+ /** DOD-M12B-SESSION-SEED-1 test seams: the counterparty addresses a re-dial depends on. Not
2883
+ * otherwise observable — they are set from a signed relay assignment that a fixture cannot mint. */
2884
+ setCounterpartyAddrsForTest(agentName, sessionId, addrs) {
2885
+ this.#counterpartyAddrs.set(this.#k(agentName, sessionId), [...addrs]);
2886
+ }
2887
+ getCounterpartyAddrsForTest(agentName, sessionId) {
2888
+ return this.#counterpartyAddrs.get(this.#k(agentName, sessionId)) ?? [];
2889
+ }
2890
+ /** DOD-M12B-SESSION-SEED-1 test seam: drop a seed WITHOUT zeroing or a status change — what a
2891
+ * process restart does to it. The refusal that follows is the one an operator most needs named. */
2892
+ forgetSessionSeedForTest(agentName, sessionId) {
2893
+ this.#sessionSeeds.delete(this.#k(agentName, sessionId));
2894
+ }
2895
+ /**
2896
+ * DOD-M12B-SESSION-SEED-1 — drain the direct-resend queue when a session comes back.
2897
+ *
2898
+ * `retryQueue.drainSession` had NO production caller. The send path enqueues into it on a failed
2899
+ * delivery and tells the operator the message will be "retried on reconnect", and nothing ever
2900
+ * reconnected it — the row sat there until the session went terminal and was reaped. Measured
2901
+ * live 2026-08-18: two of the operator's messages went in, and the response told them both were
2902
+ * lost and to send again, which is how a transcript gets duplicates.
2903
+ *
2904
+ * A revival IS the reconnect that sentence promised. This is the hook that makes it true.
2905
+ */
2906
+ setRetryDrainHook(fn) {
2907
+ this.#retryDrainHook = fn;
2908
+ }
2909
+ /** DOD-M12B-SESSION-SEED-1 test seam: does this session still hold a revivable identity? */
2910
+ hasSessionSeedForTest(agentName, sessionId) {
2911
+ return this.#sessionSeeds.has(this.#k(agentName, sessionId));
2912
+ }
2913
+ getStandingReceiverSeedForTest(agentName) {
2914
+ return this.#standingReceivers.get(agentName)?.seed;
2915
+ }
2916
+ async ensureStandingReceiverForAgent(agentName) {
2917
+ this.#agentsWantingReceiver.add(agentName);
2918
+ this.#relay.startReservationWatchdog();
2919
+ await this.#receivers.ensureStandingReceiver(agentName);
2920
+ }
2921
+ async removeStandingReceiverForAgent(agentName) {
2922
+ // M8B F14: the agent no longer wants a receiver — disarm the teardown re-arm.
2923
+ this.#agentsWantingReceiver.delete(agentName);
2924
+ // The directory hands these out at signaling-auth time, so a re-started agent
2925
+ // gets a fresh set on its next connect — holding the old ones would keep a
2926
+ // retired agent's relay list alive for the daemon's lifetime.
2927
+ this.#directoryRelayEndpoints.delete(agentName);
2928
+ // DOD-M12B-RESERVATION-RETRY-1: and the retry budget with them. A spent budget that survives an
2929
+ // offline→online cycle is a LATCH: the new receiver gets no reservation, the watchdog finds
2930
+ // `attempts` already past the cap, and returns having done nothing — no retry and not even a
2931
+ // second give-up. The agent is undialable and the machinery is inert and mute until a daemon
2932
+ // restart. Same reason the line above exists ("holding the old ones would keep a retired agent's
2933
+ // relay list alive for the daemon's lifetime").
2934
+ this.#srReservationRetry.delete(agentName);
2935
+ this.#srLastRejectionReason.delete(agentName);
2936
+ const sr = this.#standingReceivers.get(agentName);
2937
+ if (!sr) {
2938
+ // L1: an #ensureStandingReceiver for this agent may be in flight (parked on start(), so no
2939
+ // map entry yet). Leave a tombstone — that ensure tears its fresh node down on completion
2940
+ // instead of installing an SR for an agent that is now offline. Also drop any stale creating
2941
+ // marker so a later start can re-ensure.
2942
+ if (this.#standingReceiverCreating.has(agentName))
2943
+ this.#standingReceiverRemoving.add(agentName);
2944
+ return;
2945
+ }
2946
+ const removed = this.#standingReceivers.get(agentName);
2947
+ this.#standingReceivers.delete(agentName);
2948
+ // DOD-M12B-SESSION-SEED-1 (review F8): the operator took this agent offline — its advertised
2949
+ // identity is not needed any more, and the tenet's rule is that nothing unneeded stays live.
2950
+ removed?.seed.fill(0);
2951
+ // Best-effort teardown, but NOT silent: a standing receiver that failed to stop keeps a libp2p
2952
+ // node live on the network. For a removal/retire (a revocation-class action) that must be visible,
2953
+ // so the caller and the operator can see the leak rather than trust a false "torn down". autoNat is
2954
+ // inside the try too — its stop() throwing must not skip node.stop() or escape unlogged.
2955
+ try {
2956
+ sr.autoNat.stop();
2957
+ await sr.node.stop();
2958
+ }
2959
+ catch (err) {
2960
+ this.#logger.warn("session.standing_receiver.teardown.failed", {
2961
+ agentName,
2962
+ error: extractErrorMessage(err),
2963
+ });
2964
+ }
2965
+ }
2966
+ /** CC-5/F21: count of RECEIVED messages on a session — the "did the counterparty ever speak"
2967
+ * signal the dead-half-open reaper uses (message_count also counts our own auto-"Dispatched." ack,
2968
+ * so it is NOT a reliable half-open discriminator). Mirrors #getReceivedBytesTotal. */
2969
+ /**
2970
+ * DOD-M12B-REAP-HELD-1 — did the counterparty EVER establish? Counted from every place their
2971
+ * messages can be, not just the one.
2972
+ *
2973
+ * OBSERVED LIVE 2026-08-18: the half-open reaper abandoned session `d28db475…` — twenty leaves in
2974
+ * the chain and sixteen more frames verified and held, ten of them from the counterparty — while
2975
+ * the restart-seal resolver was actively trying to notarize it. The receipt was forfeited.
2976
+ *
2977
+ * `countReceivedMessages` asks the TRANSCRIPT, and **held content never reaches the transcript**;
2978
+ * it sits in `held_content` until it can join the chain. So the very condition that holds content
2979
+ * — an interrupted session — is the condition that makes the counterparty's messages invisible,
2980
+ * and a fully-established conversation reads identically to an offer nobody ever answered.
2981
+ *
2982
+ * `origin = 'received'` is load-bearing. Our OWN held frames prove nothing about them, and
2983
+ * counting those would make every session we ever spoke into un-reapable — which is exactly the
2984
+ * clutter the reaper exists to clear. D18 also depends on the zero case staying zero: reaping only
2985
+ * genuinely 0-received ghosts is what stops a stranger whose first handshakes died from being
2986
+ * locked out by the acceptance bound forever.
2987
+ */
2988
+ countEstablishedReceived(agentName, sessionId) {
2989
+ if (!this.#db)
2990
+ return 0;
2991
+ const agentId = this.#requireAgentId(agentName);
2992
+ const held = this.#db
2993
+ .prepare("SELECT COUNT(*) AS n FROM held_content WHERE agent_id = ? AND session_id = ? AND origin = 'received'")
2994
+ .get(agentId, sessionId);
2995
+ return this.#queries.countReceivedMessages(agentName, sessionId) + (held?.n ?? 0);
2996
+ }
2997
+ }
2998
+ //# sourceMappingURL=session-node-manager.js.map