@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,1679 @@
1
+ /**
2
+ * CELLO Daemon — A SESSION'S LIFE, FROM OPENED TO GONE
3
+ *
4
+ * Split out of `session-node-manager.ts`, and the last of the four paths to leave it. Everything
5
+ * that changes what a session IS rather than what it carries: opening one as the initiator,
6
+ * accepting one as the responder, connecting to the counterparty, rebuilding a torn-down session on
7
+ * the peer id the other side still holds, moving the row between `active`, `interrupted`, `sealed`
8
+ * and `abandoned`, and tearing the node down when it ends.
9
+ *
10
+ * **Moved verbatim, comments included.**
11
+ *
12
+ * ⚠️ **REVIVAL IS THE HARD PART, FOR A SPECIFIC REASON.** A rebuilt session must come back on the
13
+ * SAME transport peer id the counterparty was handed at establishment, or they can never dial back
14
+ * and the conversation is one-way without saying so. Hence the durable session seed, the rule that
15
+ * a terminal session never revives (its seed is destroyed with it), and the requirement that a
16
+ * revival take every step establishment takes — `msg-022-session-rebuild.test.ts` derives those
17
+ * steps from establishment and requires revival to match, because a revived session that behaves
18
+ * differently from a fresh one is the defect.
19
+ *
20
+ * ⚠️ **WHAT DELIBERATELY STAYED ON THE MANAGER.** `gracefulShutdown` is PROCESS teardown and
21
+ * `#evictSessionCaches` clears the eleven containers every collaborator shares; both would have had
22
+ * to mutate manager state through this context, and neither is about one session. The seam: this
23
+ * file owns what happens to A SESSION, the manager owns the process and the shared state. **The
24
+ * freeze path is the one exception** — `#freezeSession` stayed there and the reader that refuses to
25
+ * revive a frozen session is here, so this file holds the consumer and not the producer.
26
+ */
27
+ import { randomUUID, randomBytes } from "node:crypto";
28
+ import * as lp from "it-length-prefixed";
29
+ import { encodeCbor } from "@cello-protocol/protocol-types";
30
+ import { extractErrorMessage } from "./error-message.js";
31
+ import { MAX_SESSION_NODES } from "./types.js";
32
+ import { SessionConnectionGater } from "./session-connection-gater.js";
33
+ import { NodeAutoNatService } from "@cello-protocol/transport";
34
+ import { SHUTDOWN_STEP_DEADLINE_MS, } from "./session-node-types.js";
35
+ export class SessionLifecycle {
36
+ #ctx;
37
+ constructor(ctx) {
38
+ this.#ctx = ctx;
39
+ }
40
+ /** A getter so the moved queries still read `this.#db` and narrow exactly as they did. */
41
+ get #db() {
42
+ return this.#ctx.db();
43
+ }
44
+ /**
45
+ * The libp2p Peer ID of an active session's node (N_A for an initiated session), or
46
+ * null if no active node exists for it. This is the initiator's session peer id that an
47
+ * inbound session_assignment must carry to the counterparty (so the counterparty gates
48
+ * its handed-off receiver to it). Read-only.
49
+ */
50
+ getSessionNodePeerId(agentName, sessionId) {
51
+ return this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId))?.node.getPeerId() ?? null;
52
+ }
53
+ /**
54
+ * Create a new outbound session node.
55
+ * Called during cello_initiate_session.
56
+ *
57
+ * @param sessionId Unique session ID (hex string)
58
+ * @param agentName Name of the initiating agent
59
+ * @param counterpartyPubkey Counterparty's K_local public key (hex)
60
+ * @param counterpartyPeerId Counterparty's session-layer Peer ID (for gater)
61
+ * @param correlationId Correlation ID minted at session initiation
62
+ */
63
+ async createSessionNode(sessionId, agentName, counterpartyPubkey, counterpartyPeerId, correlationId, reuseStandingReceiver = false, relay) {
64
+ // Cap enforcement (AC-006)
65
+ if (this.#ctx.activeNodes.size >= MAX_SESSION_NODES) {
66
+ this.#ctx.logger.warn("session.node.cap.reached", {
67
+ agentName,
68
+ currentCount: this.#ctx.activeNodes.size,
69
+ maxCount: MAX_SESSION_NODES,
70
+ });
71
+ return {
72
+ ok: false,
73
+ reason: "max_sessions_reached",
74
+ guidance: "The daemon has reached its maximum of 32 concurrent session nodes. " +
75
+ "Close an existing session before starting a new one.",
76
+ };
77
+ }
78
+ /**
79
+ * ⚠️ THE "NO ASSIGNMENT" REFUSAL IS NOT HERE, AND THE PLACE IT MOVED TO IS THE POINT.
80
+ *
81
+ * `DOD-M15-SELFCHAIN-1`, ruled 2026-09-06: a session offered with no directory assignment is
82
+ * suspicious and must be refused and surfaced. It was briefly enforced HERE, and that was the
83
+ * wrong door: `createSessionNode` also runs on this agent's OWN outbound path, where the
84
+ * counterparty has no say in whether an assignment exists. A refusal there fires on our own
85
+ * initiations and says nothing about anyone's conduct.
86
+ *
87
+ * A counterparty can only attempt it INBOUND, so that is where it is refused and recorded —
88
+ * see `inbound-sessions.ts`. What remains true here is the correctness backstop: a session with
89
+ * no anchor cannot sign a chained message, so the SEND path refuses (`session_unchainable`)
90
+ * rather than emitting a message whose place could never be proven.
91
+ */
92
+ // The session node N_A: either a FRESH ephemeral node (default), or — for the initiator
93
+ // path (reuseStandingReceiver) — the standing receiver handed off as the session node. The
94
+ // latter makes N_A's peer id equal the SESSION endpoint the initiator ADVERTISED to the
95
+ // directory (its standing receiver), so the counterparty's connection gater (set to that
96
+ // advertised peer id) admits N_A's dial. Mirrors acceptSession, which already hands off the
97
+ // standing receiver on the receiver side. WIRE-001/INV-5: a fully-fresh ephemeral initiator
98
+ // node would require advertising N_A's peer id pre-negotiation (a session-node lifecycle
99
+ // split); the symmetric standing-receiver handoff is the consistent interim model.
100
+ let node;
101
+ let gater;
102
+ let autoNat;
103
+ // DOD-M12B-SESSION-SEED-1: whichever branch below runs, the session ends up owning a seed.
104
+ // Promotion inherits the receiver's; a freshly-built node mints its own.
105
+ let seed;
106
+ if (reuseStandingReceiver) {
107
+ const sr = this.#ctx.standingReceivers.get(agentName);
108
+ if (!sr) {
109
+ // DOD-LOOP-1: this agent has no standing receiver ready — kick off (idempotent) creation
110
+ // so a retry finds it, and report unavailable. Per-agent, so the initiator consuming its
111
+ // OWN agent's receiver never contends with a co-resident responder agent (the loopback case).
112
+ void this.#ctx.receivers.ensureStandingReceiver(agentName, correlationId);
113
+ return {
114
+ ok: false,
115
+ reason: "standing_receiver_unavailable",
116
+ guidance: "The standing receiver node is initializing (completes within 200ms). Retry the session in a moment.",
117
+ };
118
+ }
119
+ ({ node, gater, autoNat, seed } = sr);
120
+ gater.setAllowedPeer(counterpartyPeerId);
121
+ await this.#evictPeersOutsideGate(node, gater, sessionId, counterpartyPeerId, "outbound_promotion");
122
+ // Hand this agent's standing receiver off to this session; a replacement is spun up below.
123
+ this.#ctx.standingReceivers.delete(agentName);
124
+ }
125
+ else {
126
+ gater = new SessionConnectionGater({
127
+ sessionId,
128
+ allowedPeerId: counterpartyPeerId,
129
+ logger: this.#ctx.logger,
130
+ });
131
+ try {
132
+ seed = randomBytes(32);
133
+ node = await this.#ctx.receivers.createAgentNode(agentName, { sessionId, connectionGater: gater, nodeType: "session", transportPrivateKey: seed });
134
+ await node.start();
135
+ }
136
+ catch (err) {
137
+ const errorMessage = extractErrorMessage(err);
138
+ this.#ctx.logger.error("session.node.create.failed", {
139
+ sessionId,
140
+ agentName,
141
+ error: errorMessage,
142
+ correlationId,
143
+ });
144
+ return {
145
+ ok: false,
146
+ reason: "session_node_creation_failed",
147
+ guidance: "Failed to create session transport node. The daemon logged the cause in " +
148
+ "session.node.create.failed. Check that the system has available ports and sufficient memory.",
149
+ };
150
+ }
151
+ // CELLO-M7-TRANSPORT-001: session nodes also need dialability awareness for the
152
+ // dcutr decision path (AC-002). Wrap the node in a NodeAutoNatService and emit
153
+ // its initial result (nodeType: 'session').
154
+ autoNat = new NodeAutoNatService({
155
+ node,
156
+ logger: this.#ctx.logger,
157
+ nodeType: "session",
158
+ probers: this.#ctx.autoNatProbers(),
159
+ });
160
+ autoNat.emitInitialResult();
161
+ }
162
+ const peerId = node.getPeerId();
163
+ const addrs = node.listenAddresses();
164
+ // Persist to SQLite. D4 review F1: #insertSessionRow swallows the write failure (returns
165
+ // false) — ignoring it let a session go fully live with NO sessions row, which after D4a means
166
+ // every inbound message is refused session_orphaned while the session looks healthy to both
167
+ // operators. A rowless session is a dead session by definition — fail ONCE, here, at creation.
168
+ if (!this.#insertSessionRow(sessionId, agentName, counterpartyPubkey, "active")) {
169
+ try {
170
+ await node.stop();
171
+ }
172
+ catch (err) {
173
+ this.#ctx.logger.warn("session.node.stop.failed", {
174
+ sessionId,
175
+ agentName,
176
+ error: extractErrorMessage(err),
177
+ correlationId,
178
+ });
179
+ }
180
+ // The handed-off standing receiver was consumed above — rebuild it (idempotent).
181
+ if (reuseStandingReceiver)
182
+ void this.#ctx.receivers.ensureStandingReceiver(agentName, correlationId);
183
+ return {
184
+ ok: false,
185
+ reason: "session_persist_failed",
186
+ guidance: "The daemon could not persist the session row (see session.row.write.failed in the log). " +
187
+ "The session was not created — a session without a durable row cannot receive content. " +
188
+ "Check the daemon's database (disk space, permissions) and retry.",
189
+ };
190
+ }
191
+ // Log observability event (session.node.created)
192
+ //
193
+ // `counterpartySessionPeerId` IS LOGGED because it is recorded here ONCE and never refreshed,
194
+ // while a standing receiver is rebuilt with a fresh libp2p keypair on a lost relay reservation
195
+ // and every lost reservation. If the peer rebuilds between advertising its endpoint and this
196
+ // handoff, we record an identity that no longer exists — and since `newStream` never dials, it
197
+ // only ever looks for an ALREADY-OPEN connection filed under exactly this string, so every send
198
+ // in this direction parks forever while the reverse direction works fine.
199
+ //
200
+ // Both sides of a local session log this event, so recording the id we will dial makes that
201
+ // mismatch a direct comparison in the log instead of an unfalsifiable hypothesis.
202
+ this.#ctx.logger.info("session.node.created", {
203
+ sessionId,
204
+ agentName,
205
+ sessionPeerId: peerId,
206
+ counterpartySessionPeerId: counterpartyPeerId,
207
+ correlationId,
208
+ });
209
+ // Add to active map (keyed by (agentName, sessionId) — DOD-LOOP-1)
210
+ // 006-CRYPTO: the session's throwaway keypair is minted here, with the node, so "a session is
211
+ // active" and "a session has a key" are the same moment. All THREE activation paths mint.
212
+ this.#ctx.ephemerals.mintSessionEphemeral(agentName, sessionId);
213
+ this.#ctx.activeNodes.set(this.#ctx.sessionKey(agentName, sessionId), {
214
+ node,
215
+ agentName,
216
+ sessionId,
217
+ counterpartyPubkey,
218
+ gater,
219
+ correlationId,
220
+ counterpartySessionPeerId: counterpartyPeerId,
221
+ autoNat,
222
+ });
223
+ this.#rememberSessionSeed(agentName, sessionId, seed, counterpartyPeerId, counterpartyPubkey);
224
+ // DAEMON-004: register the content stream handler so inbound content_frames
225
+ // are cross-checked, appended to the daemon-owned tree, and buffered.
226
+ await this.#ctx.contentIn.registerContentHandler(agentName, sessionId, node, counterpartyPubkey);
227
+ // M7-SESSION-003 AC-004: act on the session node's peer events for direct-path
228
+ // liveness. The session connection IS the authority for a direct session.
229
+ this.#ctx.liveness.wireSessionLiveness(agentName, sessionId, node, counterpartyPubkey, correlationId, counterpartyPeerId);
230
+ // M7 DOD-SPINE-6 / MSG-001-3b: connect this session node to the relay as the
231
+ // Structure-2 witness (non-fatal — direct content still works without it).
232
+ if (relay) {
233
+ await this.#ctx.relay.connectSessionRelay(sessionId, node, agentName, relay, correlationId);
234
+ }
235
+ // If we consumed this agent's standing receiver, spin up a replacement (async — do NOT await).
236
+ if (reuseStandingReceiver) {
237
+ void this.#ctx.receivers.ensureStandingReceiver(agentName, correlationId);
238
+ }
239
+ return { ok: true, peerId, addrs };
240
+ }
241
+ /**
242
+ * Hand the standing receiver to an inbound session.
243
+ * Called during cello_await_session.
244
+ *
245
+ * CRITICAL (AC-015): gater.setAllowedPeer() is called BEFORE returning
246
+ * the node's multiaddr to the caller. This closes the window where an
247
+ * unexpected peer could connect during the hand-off.
248
+ */
249
+ async acceptSession(sessionId, agentName, counterpartyPubkey, initiatorPeerId, correlationId, relay) {
250
+ // DOD-M15-OFFER-SIGNED-1 review N5: the offer record has done its job the moment this session is
251
+ // claimed. Keying it by session (the F1 fix) removed the accidental bound that agent-keying gave
252
+ // it — each new offer used to overwrite the last — so without a clear on the SUCCESS path the
253
+ // map gained one permanent entry per offer ever received, on directory-supplied keys. Cleared
254
+ // here rather than only on refusal, which is what the doc comment always claimed.
255
+ this.#ctx.receivers.clearOfferedDialer(agentName, sessionId);
256
+ const inboundSr = this.#ctx.standingReceivers.get(agentName);
257
+ if (!inboundSr) {
258
+ // DOD-LOOP-1: per-agent — kick off (idempotent) creation so a retry finds it.
259
+ void this.#ctx.receivers.ensureStandingReceiver(agentName, correlationId);
260
+ return {
261
+ ok: false,
262
+ reason: "standing_receiver_unavailable",
263
+ guidance: "The standing receiver node is initializing (completes within 200ms). " +
264
+ "Retry cello_await_session in a moment.",
265
+ };
266
+ }
267
+ // Cap enforcement — inbound sessions count against the same limit (AC-006)
268
+ if (this.#ctx.activeNodes.size >= MAX_SESSION_NODES) {
269
+ this.#ctx.logger.warn("session.node.cap.reached", {
270
+ agentName,
271
+ currentCount: this.#ctx.activeNodes.size,
272
+ maxCount: MAX_SESSION_NODES,
273
+ });
274
+ return {
275
+ ok: false,
276
+ reason: "max_sessions_reached",
277
+ guidance: "The daemon has reached its maximum of 32 concurrent session nodes. " +
278
+ "Close an existing session before starting a new one.",
279
+ };
280
+ }
281
+ const { node, gater, autoNat, seed } = inboundSr;
282
+ // AC-015: update gater BEFORE retrieving multiaddr / returning to caller
283
+ gater.setAllowedPeer(initiatorPeerId);
284
+ await this.#evictPeersOutsideGate(node, gater, sessionId, initiatorPeerId, "inbound_promotion");
285
+ const peerId = node.getPeerId();
286
+ const addrs = node.listenAddresses();
287
+ // Persist to SQLite. D4 review F1 (same as createSessionNode): a swallowed row-write failure
288
+ // must fail the accept ONCE here — after D4a a rowless session refuses every ingest. The
289
+ // standing receiver (this node) is consumed and rebuilt rather than left with its gater
290
+ // pointed at this initiator.
291
+ if (!this.#insertSessionRow(sessionId, agentName, counterpartyPubkey, "active")) {
292
+ this.#ctx.standingReceivers.delete(agentName);
293
+ // DOD-M12B-SESSION-SEED-1 (review F8): this abort happens BEFORE `#rememberSessionSeed`, so
294
+ // the identity is not being handed to a session — it is being discarded, and is zeroed like
295
+ // any other discard. (The two PROMOTION sites deliberately do not zero: there the same bytes
296
+ // become the session's.)
297
+ inboundSr.seed.fill(0);
298
+ try {
299
+ await node.stop();
300
+ }
301
+ catch (err) {
302
+ this.#ctx.logger.warn("session.node.stop.failed", {
303
+ sessionId,
304
+ agentName,
305
+ error: extractErrorMessage(err),
306
+ correlationId,
307
+ });
308
+ }
309
+ void this.#ctx.receivers.ensureStandingReceiver(agentName, correlationId);
310
+ return {
311
+ ok: false,
312
+ reason: "session_persist_failed",
313
+ guidance: "The daemon could not persist the session row (see session.row.write.failed in the log). " +
314
+ "The inbound session was not accepted — a session without a durable row cannot receive content. " +
315
+ "Check the daemon's database (disk space, permissions).",
316
+ };
317
+ }
318
+ // Log observability event. `counterpartySessionPeerId` for the same reason as the initiator
319
+ // side: this is the identity every later send will look for an open connection under, it is
320
+ // never refreshed, and the peer's standing receiver may already have been rebuilt under a new
321
+ // one. The RESPONDER is the side that can go stale — only the initiator dials, so this is the
322
+ // half that inherits an id it never verified.
323
+ this.#ctx.logger.info("session.node.created", {
324
+ sessionId,
325
+ agentName,
326
+ sessionPeerId: peerId,
327
+ counterpartySessionPeerId: initiatorPeerId,
328
+ correlationId,
329
+ });
330
+ // Remove this agent's standing receiver from the slot and add to active map. The handed-off
331
+ // node keeps its AutoNAT service (it continues to surface dialability).
332
+ this.#ctx.standingReceivers.delete(agentName);
333
+ // 006-CRYPTO: the hand-off path. A session promoted out of the standing receiver is as new as
334
+ // one opened outbound, so it mints here too.
335
+ this.#ctx.ephemerals.mintSessionEphemeral(agentName, sessionId);
336
+ this.#ctx.activeNodes.set(this.#ctx.sessionKey(agentName, sessionId), {
337
+ node,
338
+ agentName,
339
+ sessionId,
340
+ counterpartyPubkey,
341
+ gater,
342
+ correlationId,
343
+ counterpartySessionPeerId: initiatorPeerId,
344
+ autoNat,
345
+ });
346
+ this.#rememberSessionSeed(agentName, sessionId, seed, initiatorPeerId, counterpartyPubkey);
347
+ // DAEMON-004: register the content stream handler for the inbound session.
348
+ await this.#ctx.contentIn.registerContentHandler(agentName, sessionId, node, counterpartyPubkey);
349
+ // M7-SESSION-003 AC-004: act on the inbound session node's peer events too.
350
+ /**
351
+ * DOD-M12B-RESPONDER-ADDR-1 — LEARN THE INITIATOR'S ADDRESS, because we will need it and this is
352
+ * the only moment we have it.
353
+ *
354
+ * MEASURED LIVE 2026-08-18. After an interruption the responder's re-dial reported
355
+ * `session.transport.redial.unavailable` — *"this side holds no address for the counterparty, so
356
+ * every send parks until they re-establish"* — and every reply it tried to send failed. The
357
+ * initiator can always come back because it kept the addresses it dialled; the responder dialled
358
+ * nothing, so it kept nothing.
359
+ *
360
+ * In plain terms that meant: whoever ANSWERED a conversation could not restart it. Their replies
361
+ * went nowhere until the other side spoke first.
362
+ *
363
+ * The live connection has known the address all along — the responder is holding it right now,
364
+ * because the initiator just dialled in on it. `#counterpartyAddrs` is the same store the
365
+ * initiator fills from its signed relay assignment, and `#evictSessionCaches` hands both to the
366
+ * revival record on the way down, so this needs no separate lifetime.
367
+ */
368
+ const inboundAddrs = node
369
+ .getConnections()
370
+ .filter((c) => c.peerId === initiatorPeerId && typeof c.remoteAddr === "string")
371
+ .map((c) => c.remoteAddr);
372
+ if (inboundAddrs.length > 0) {
373
+ this.#ctx.counterpartyAddrs.set(this.#ctx.sessionKey(agentName, sessionId), [...new Set(inboundAddrs)]);
374
+ this.#ctx.logger.info("session.counterparty.addr.learned", {
375
+ agentName,
376
+ sessionId,
377
+ addrs: inboundAddrs.length,
378
+ source: "inbound_connection",
379
+ impact: "this side can now re-dial after an interruption instead of parking every reply",
380
+ });
381
+ }
382
+ else {
383
+ // NOT A WARNING. Review MEDIUM-4: accept runs off a signaling frame and the initiator dials
384
+ // separately, so "no connection yet" is the ordinary in-flight case — warning on it puts a
385
+ // signal on the normal path, which is how the one occurrence that matters gets buried. The
386
+ // race-free capture is in `#wireSessionLiveness`'s onPeerConnect, which fires when the dial
387
+ // actually lands; this read is only a fast path for when it already has.
388
+ this.#ctx.logger.debug("session.counterparty.addr.deferred", {
389
+ agentName,
390
+ sessionId,
391
+ initiatorPeerId,
392
+ impact: "no connection observed yet; the address is captured when the counterparty connects",
393
+ });
394
+ }
395
+ this.#ctx.liveness.wireSessionLiveness(agentName, sessionId, node, counterpartyPubkey, correlationId, initiatorPeerId);
396
+ // M7 DOD-SPINE-6 / MSG-001-3b: the receiver also connects to the relay witness so
397
+ // the relay can deliver the initiator's witnessed leaves (leaf_deliver) to it.
398
+ if (relay) {
399
+ await this.#ctx.relay.connectSessionRelay(sessionId, node, agentName, relay, correlationId);
400
+ }
401
+ // Immediately spin up a replacement for THIS agent (async — do NOT await, AC-003)
402
+ void this.#ctx.receivers.ensureStandingReceiver(agentName, correlationId);
403
+ return { ok: true, peerId, addrs };
404
+ }
405
+ /**
406
+ * Destroy a session node after seal or on error teardown.
407
+ * Status written to SQLite.
408
+ */
409
+ async destroySessionNode(agentName, sessionId, reason) {
410
+ // F1-b: record the terminal answer BEFORE the caches are evicted (and before the
411
+ // early-return below), so a blocking cello_receive that was waiting when the seal fired
412
+ // returns "session_sealed" (with how many buffered messages it never read) instead of
413
+ // hanging to timeout or 404ing. Set even if the node was already retired — a late receive
414
+ // on a sealed session should always learn it is sealed. The receiver (the party that races
415
+ // the seal on cello_receive) is torn down through THIS path; the closer goes through
416
+ // retireSessionNode and is not blocking on receive.
417
+ if (reason === "sealed") {
418
+ const tkey = this.#ctx.sessionKey(agentName, sessionId);
419
+ // DOD-COATTEND-1: counted from the DURABLE read watermark, not the buffer's length.
420
+ // Delivery no longer drains that buffer (it reads the transcript against a per-connection
421
+ // bookmark), so its length is now "everything that ever arrived", not "what nobody read" —
422
+ // reporting it would tell the operator every message of a healthy conversation went unread.
423
+ const unreadCount = this.#ctx.records.getUnreadReceivedCount(agentName, sessionId);
424
+ this.#ctx.sessionTerminal.set(tkey, { type: "sealed", unreadCount });
425
+ }
426
+ const entry = this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId));
427
+ if (!entry)
428
+ return;
429
+ entry.autoNat.stop();
430
+ // M7 DOD-SPINE-6 / MSG-001-3b: close the relay witness stream so we don't leak it.
431
+ this.#ctx.relay.detachSessionRelay(entry);
432
+ try {
433
+ await entry.node.stop();
434
+ }
435
+ catch (err) {
436
+ this.#ctx.logger.error("session.node.stop.failed", {
437
+ sessionId,
438
+ agentName: entry.agentName,
439
+ error: extractErrorMessage(err),
440
+ correlationId: entry.correlationId,
441
+ });
442
+ // Fall through — still remove from active map and update DB
443
+ }
444
+ // Update SQLite — 'sealed' → 'sealed', 'interrupted'/'error' → 'interrupted'.
445
+ // 'error' is not a valid SessionStatus in SQLite; error-torn-down sessions
446
+ // surface as interrupted so AC-010 recovery handles them at next login.
447
+ // The session.node.destroyed log preserves the original reason for observability.
448
+ const dbStatus = reason === "sealed" ? "sealed" : "interrupted";
449
+ // DOD-CAP-SELF-HEAL-1: OURS. Every caller of this with a non-sealed reason is a local teardown
450
+ // — the operator's kill switch (`cello_set_agent_offline`), an internal error, a node replaced.
451
+ // The counterparty did nothing, so they must not be charged a cap slot for it.
452
+ this.updateSessionStatus(agentName, sessionId, dbStatus, dbStatus === "interrupted" ? "local" : undefined);
453
+ /**
454
+ * 055-ONDEMAND — **THE SLOT GOES BACK HERE TOO, AND THIS IS THE PATH THAT MATTERS MOST.**
455
+ *
456
+ * The first version put the release only in `retireSessionNode`. The comment at the top of THIS
457
+ * function says which side goes where: *"The receiver (the party that races the seal on
458
+ * `cello_receive`) is torn down through THIS path; the closer goes through
459
+ * retireSessionNode."* The receiver is exactly the party that took the on-demand reservation —
460
+ * it reserved to answer the offer. So whenever the INITIATOR closes, which is the ordinary
461
+ * case, the responder's slot was never released at all.
462
+ *
463
+ * Both paths now release, from the session's own node. Awaited, never allowed to throw.
464
+ */
465
+ await this.#ctx.receivers.releaseSessionReservation(agentName, entry.node, sessionId, entry.correlationId)
466
+ .catch((err) => {
467
+ this.#ctx.logger.warn("session.reservation.release.failed", {
468
+ agentName, sessionId, error: extractErrorMessage(err),
469
+ impact: "the relay keeps this slot until its TTL expires. The teardown itself is unaffected.",
470
+ });
471
+ });
472
+ this.#ctx.activeNodes.delete(this.#ctx.sessionKey(agentName, sessionId));
473
+ // Evict the in-memory per-session caches on teardown. The tree is durable in
474
+ // SQLite (getSessionTree reloads it on demand), and the received-content buffer
475
+ // holds plaintext that must not linger after a session ends. Without this, both
476
+ // maps grow unbounded by total sessions seen over a long-lived daemon.
477
+ // (#evictSessionCaches also drops the M7-SESSION-003 liveness flag, so both the
478
+ // destroy and retire teardown paths clear it — no stale verdict survives.)
479
+ this.#ctx.evictSessionCaches(agentName, sessionId);
480
+ this.#ctx.logger.info("session.node.destroyed", {
481
+ sessionId,
482
+ agentName: entry.agentName,
483
+ reason,
484
+ });
485
+ // M8B F14 (fix 1): the torn-down node has just released its port — on a fixed-port
486
+ // deployment this is the FIRST moment a previously-failed re-arm can succeed. Re-arm
487
+ // the standing receiver for an online agent that has none (async, never awaited).
488
+ this.#rearmAfterTeardown(agentName);
489
+ }
490
+ /**
491
+ * M8B F14: re-arm an online agent's standing receiver after a session-node teardown
492
+ * freed resources (notably the fixed port). No-op when the agent is offline, already
493
+ * has a receiver, or one is being created. The re-arm is a NEW async flow — it mints
494
+ * its own correlationId (via the ensure default) rather than inheriting the torn-down
495
+ * session's.
496
+ */
497
+ #rearmAfterTeardown(agentName) {
498
+ if (this.#ctx.shuttingDown)
499
+ return;
500
+ if (!this.#ctx.agentsWantingReceiver.has(agentName))
501
+ return;
502
+ if (this.#ctx.standingReceivers.has(agentName) || this.#ctx.standingReceiverCreating.has(agentName))
503
+ return;
504
+ void this.#ctx.receivers.ensureStandingReceiver(agentName);
505
+ }
506
+ /**
507
+ * round-2 finding #5: retire a session's live libp2p node WITHOUT changing its
508
+ * DB status. Used after the active-session bilateral seal commitment has already
509
+ * advanced the row to 'seal_interrupted_pending': the session is frozen, so we
510
+ * stop the node and unregister its /cello/content handler (no more inbound leaves,
511
+ * no leaked node per active close) but must NOT overwrite the pending/sealed status
512
+ * the way destroySessionNode would. The durable tree stays in SQLite (getSessionTree
513
+ * reloads it); the in-memory plaintext buffer is evicted.
514
+ */
515
+ async retireSessionNode(agentName, sessionId) {
516
+ const entry = this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId));
517
+ if (!entry)
518
+ return;
519
+ this.#ctx.relay.detachSessionRelay(entry);
520
+ try {
521
+ await entry.node.stop();
522
+ }
523
+ catch (err) {
524
+ this.#ctx.logger.error("session.node.stop.failed", {
525
+ sessionId,
526
+ agentName: entry.agentName,
527
+ error: extractErrorMessage(err),
528
+ correlationId: entry.correlationId,
529
+ });
530
+ // Fall through — still remove from active map.
531
+ }
532
+ this.#ctx.activeNodes.delete(this.#ctx.sessionKey(agentName, sessionId));
533
+ this.#ctx.evictSessionCaches(agentName, sessionId);
534
+ this.#ctx.logger.info("session.node.destroyed", {
535
+ sessionId,
536
+ agentName: entry.agentName,
537
+ reason: "sealing",
538
+ });
539
+ /**
540
+ * 055-ONDEMAND — **THE SLOT GOES BACK, FROM THIS SESSION'S OWN NODE.**
541
+ *
542
+ * `entry.node` is the node that actually holds the circuit — the standing receiver was PROMOTED
543
+ * into it when the session opened, and a fresh empty receiver was built behind it. Looking the
544
+ * node up by agent name (the first version) always found that empty replacement, read zero
545
+ * circuits, and returned having told the relay nothing.
546
+ *
547
+ * Awaited but never allowed to throw: giving a slot back must not be able to fail a seal.
548
+ */
549
+ await this.#ctx.receivers.releaseSessionReservation(agentName, entry.node, sessionId, entry.correlationId)
550
+ .catch((err) => {
551
+ this.#ctx.logger.warn("session.reservation.release.failed", {
552
+ agentName, sessionId, error: extractErrorMessage(err),
553
+ impact: "the relay keeps this slot until its TTL expires. The seal itself is unaffected.",
554
+ });
555
+ });
556
+ // M8B F14 (fix 1): same re-arm point as destroySessionNode — the retired node freed its port.
557
+ this.#rearmAfterTeardown(agentName);
558
+ }
559
+ /**
560
+ * Graceful shutdown: mark all active sessions as interrupted, stop all nodes.
561
+ * Called from the SIGTERM / cello logout path (AC-009).
562
+ * SQLite writes complete before this method returns.
563
+ */
564
+ /**
565
+ * DOD-M12B-SHUTDOWN-1 — wait for a teardown step, but never forever.
566
+ *
567
+ * Every step of shutdown used to be an unbounded `await` on libp2p. That is what makes "the
568
+ * daemon acknowledged the request but is still running" possible: nothing on the daemon side
569
+ * emits a word while it hangs, so the operator's own message ("it may be stuck closing sessions
570
+ * or its database") was a guess. Past the deadline the step is ABANDONED and SAID — the resources
571
+ * it was closing are reclaimed by the OS on exit, and an exit is worth more than a tidy one.
572
+ */
573
+ async boundedTeardown(work, step, count) {
574
+ if (count === 0)
575
+ return;
576
+ const started = Date.now();
577
+ let timer;
578
+ const deadline = new Promise((resolve) => {
579
+ timer = setTimeout(() => resolve("timeout"), SHUTDOWN_STEP_DEADLINE_MS);
580
+ timer.unref?.();
581
+ });
582
+ const outcome = await Promise.race([work.then(() => "done"), deadline]);
583
+ if (timer)
584
+ clearTimeout(timer);
585
+ if (outcome === "timeout") {
586
+ this.#ctx.logger.error("session.shutdown.step.timeout", {
587
+ step, count, waitedMs: Date.now() - started,
588
+ impact: "this teardown step did not finish and was abandoned so the daemon can exit; the OS reclaims what it held",
589
+ });
590
+ }
591
+ else {
592
+ this.#ctx.logger.debug("session.shutdown.step.done", { step, count, tookMs: Date.now() - started });
593
+ }
594
+ }
595
+ /**
596
+ * M7-SESSION-001: Mark a session as interrupted with message count and timestamp.
597
+ * Called when a relay session_interrupted frame arrives or a relay stream closes.
598
+ * Also tears down the in-memory session node if one exists for this sessionId.
599
+ *
600
+ * @param sessionId The hex session ID from the relay frame
601
+ * @param messageCount Number of message leaves at interruption
602
+ * @param source 'relay_frame' | 'stream_close'
603
+ */
604
+ async markInterruptedWithDetails(agentName, sessionId, messageCount,
605
+ /**
606
+ * WHAT ACTUALLY HAPPENED, and it is written to the row — review F3.
607
+ *
608
+ * `key_refused` is its own source rather than a borrowed `stream_close`, because the row's
609
+ * `interrupted_by` is what an operator reads days later: labelling a key-authentication refusal
610
+ * `relay_stream_close` sends them to the relay fleet for a fault in the payload.
611
+ */
612
+ source) {
613
+ if (!this.#db)
614
+ return false;
615
+ // H-3 SECURITY: only an 'active' session may transition to 'interrupted'.
616
+ // A late or forged relay frame must NOT revert a 'sealed', 'seal_interrupted_pending',
617
+ // or already-'interrupted' session back to 'interrupted'. This mirrors the
618
+ // stream-close guard in `#watchRelayStream` (`session-relay.ts`) — the two paths must agree.
619
+ // Not "below" any more: that invariant is now a claim about two FILES, so it is named.
620
+ const existing = this.#ctx.queries.getSessionRecord(agentName, sessionId);
621
+ if (!existing || existing.status !== "active") {
622
+ this.#ctx.logger.warn("session.interrupt.ignored", {
623
+ sessionId,
624
+ source,
625
+ currentStatus: existing?.status ?? "absent",
626
+ reason: "session_not_active",
627
+ });
628
+ // FALSE, not void — the caller needs to know nothing was torn down (review F11).
629
+ return false;
630
+ }
631
+ const now = Date.now();
632
+ const interruptedAt = new Date(now).toISOString();
633
+ // round-2 finding #7: the daemon-owned tree is the authoritative transcript
634
+ // length. The `messageCount` arg comes from registerRelayStream time and defaults
635
+ // to 0, so writing it blindly would clobber the column out of sync with the tree
636
+ // (both seal flows prefer tree.size(), but the column must not lie). When a tree
637
+ // exists for this session, persist its size; otherwise fall back to the arg.
638
+ const treeSize = this.#ctx.getSessionTree(agentName, sessionId).size();
639
+ const authoritativeCount = treeSize > 0 ? treeSize : messageCount;
640
+ try {
641
+ // The `AND status = 'active'` predicate is the authoritative guard: even if
642
+ // the pre-check above raced (it cannot — DatabaseSync is synchronous), the
643
+ // UPDATE only mutates a row that is still active.
644
+ this.#db
645
+ .prepare(
646
+ // DOD-CAP-SELF-HEAL-1: labelled by SOURCE, because the two are not the same event.
647
+ //
648
+ // relay_frame — the relay telling us the counterparty went. THEIRS. The D18
649
+ // disconnect-evasion move, and it must keep counting.
650
+ // stream_close — OUR witness stream to the relay ended. That fires on a relay restart,
651
+ // a relay fleet roll, or a local network blip. Claiming the counterparty
652
+ // did it means three relay deploys permanently refuse a peer who was
653
+ // never involved — and relay deploys are routine, so it ratchets faster
654
+ // than daemon restarts do.
655
+ //
656
+ // `relay_stream_close` is its own label and STILL COUNTS (the bound excuses only 'local'),
657
+ // because an attacker who can disturb our relay link must not get a free cap reset. It is
658
+ // recorded honestly rather than blamed on the wrong party.
659
+ `UPDATE sessions SET status = 'interrupted', updated_at = ?, message_count = ?, interrupted_at = ?, interrupted_by = '${source === "relay_frame" ? "counterparty" : source === "key_refused" ? "key_refused" : "relay_stream_close"}' WHERE agent_id = ? AND session_id = ? AND status = 'active'`)
660
+ .run(now, authoritativeCount, interruptedAt, this.#ctx.requireAgentId(agentName), sessionId);
661
+ }
662
+ catch (err) {
663
+ this.#ctx.logger.error("session.interrupt.db.write.failed", {
664
+ sessionId,
665
+ error: extractErrorMessage(err),
666
+ });
667
+ }
668
+ // Look up the in-memory entry (keyed by (agent, session)) for teardown.
669
+ const entry = this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId));
670
+ // Tear down the in-memory session node if it exists
671
+ if (entry) {
672
+ entry.autoNat.stop();
673
+ this.#ctx.relay.detachSessionRelay(entry);
674
+ try {
675
+ await entry.node.stop();
676
+ }
677
+ catch (err) {
678
+ this.#ctx.logger.error("session.node.stop.failed", {
679
+ sessionId,
680
+ agentName,
681
+ error: extractErrorMessage(err),
682
+ correlationId: entry.correlationId,
683
+ });
684
+ // Fall through — still remove from active map
685
+ }
686
+ this.#ctx.activeNodes.delete(this.#ctx.sessionKey(agentName, sessionId));
687
+ /**
688
+ * THE SECRET GOES WITH THE ENTRY — 006-CRYPTO, review pass 2 finding 2.
689
+ *
690
+ * This is the path an interrupted session actually takes, and it is the ORDINARY way a
691
+ * session ends badly: a relay blip, a closed stream, a sleeping laptop. Because it does not
692
+ * evict (see below) the secret used to survive here, and when the session later sealed
693
+ * `destroySessionNode` returned at its `if (!entry) return` without evicting either — so the
694
+ * receipt landed, the session was over, and the key stayed resident until the process exited.
695
+ *
696
+ * The reasons below for KEEPING the other caches do not transfer to key material: buffered
697
+ * plaintext must stay drainable and TTF timers must stay armed, whereas a secret nothing
698
+ * reads must not stay alive. A revived session mints a fresh one and re-keys, which is
699
+ * Decisions Carried #5 and is only true because of this line.
700
+ */
701
+ this.#ctx.ephemerals.destroySessionEphemeralFor(agentName, sessionId, entry.correlationId);
702
+ this.#ctx.logger.info("session.node.destroyed", {
703
+ sessionId,
704
+ agentName,
705
+ reason: "interrupted",
706
+ });
707
+ // DELIBERATELY NOT #evictSessionCaches here (unlike destroySessionNode/retireSessionNode):
708
+ // an interrupted session is not terminal. (1) Reason (1) used to be the in-memory arrival
709
+ // buffer, which had to stay drainable across a blip; that buffer is gone and cello_receive
710
+ // reads the durable transcript, which eviction never touched anyway. (2) Evict
711
+ // also cancels armed TTF timers (`clearAwaitingForSession`, in `session-content-send.ts`) — on
712
+ // a dying session the TTF
713
+ // park backstop is exactly what must fire for un-acked content (MSG-001). The caches are
714
+ // reclaimed when the session later seals (destroy/retire paths) or at daemon restart.
715
+ // M8B F14 (fix 1): the relay-detected interruption is the THIRD teardown path that
716
+ // frees the fixed port — it must re-arm too, or a session ending on a network blip
717
+ // leaves the agent deaf again (review finding on the F14 fix).
718
+ this.#rearmAfterTeardown(agentName);
719
+ }
720
+ this.#ctx.logger.warn("session.interrupted.detected", {
721
+ sessionId,
722
+ agentName,
723
+ source,
724
+ });
725
+ // M7-SESSION-001 (M-1 PUSH): notify live MCP clients that this session is now
726
+ // interrupted. Only fires on a real active→interrupted transition (the guard
727
+ // above already returned for any non-active session).
728
+ try {
729
+ this.#ctx.onSessionStateChanged?.(agentName, sessionId, "interrupted", existing.counterparty_pubkey);
730
+ }
731
+ catch (err) {
732
+ this.#ctx.logger.debug("session.state.notify.failed", {
733
+ sessionId,
734
+ error: extractErrorMessage(err),
735
+ });
736
+ }
737
+ return true;
738
+ }
739
+ /**
740
+ * SEAM 1b (dialer ⇄ session-node reconciliation): dial the counterparty THROUGH
741
+ * this session's OWN node, so the session node N_A holds the connection its content
742
+ * newStream actually rides. TRANSPORT-001's transport selector dialed on a separate
743
+ * (composition-root) node whose connection N_A could not use — the per-session node
744
+ * must be the dialer. Direct mode only here (the default content path, Part 4 D-a);
745
+ * relay-circuit + dcutr strategy via N_A is a later seam. Tries each addr in turn;
746
+ * succeeds on the first connection, returns a named failure if none connect.
747
+ */
748
+ async connectToCounterparty(agentName, sessionId, addrs) {
749
+ const entry = this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId));
750
+ if (!entry) {
751
+ return { ok: false, reason: "session_node_unavailable", error: "no active session node for this session" };
752
+ }
753
+ if (addrs.length === 0) {
754
+ return { ok: false, reason: "no_counterparty_addrs", error: "the assignment carried no counterparty session addrs to dial" };
755
+ }
756
+ // DOD-NAT-REACHABILITY-1: a /p2p-circuit counterparty address is dialed
757
+ // THROUGH its relay, so the gater must admit that relay peer OUTBOUND. The
758
+ // relay id is embedded in the address, which arrived inside the FROST-signed
759
+ // assignment — the same authorization rail as the assigned witness relay.
760
+ for (const addr of addrs) {
761
+ const viaRelay = addr.match(/\/p2p\/([^/]+)\/p2p-circuit/);
762
+ if (viaRelay)
763
+ entry.gater.setAllowedOutboundPeer(viaRelay[1]);
764
+ }
765
+ // DOD-M15-RELAYAUTH-1 review H1: the RELAY's gater must also admit this dial, and it only does
766
+ // so once it holds the assignment. Await that here — see the method's own comment for why the
767
+ // counterparty presenting it cannot be relied on.
768
+ await this.#ctx.relay.authorizeCircuitDialsToCounterparty(agentName, sessionId, entry, addrs);
769
+ let lastError = "";
770
+ for (const addr of addrs) {
771
+ try {
772
+ await entry.node.dial(addr);
773
+ // DOD-M12B-REDIAL-1: keep them. They arrived in the signed assignment and were used once
774
+ // and dropped, which is the reason nothing could ever dial this counterparty again.
775
+ this.#ctx.counterpartyAddrs.set(this.#ctx.sessionKey(agentName, sessionId), [...addrs]);
776
+ this.#ctx.logger.info("session.transport.connected", {
777
+ sessionId,
778
+ addr,
779
+ correlationId: entry.correlationId,
780
+ });
781
+ return { ok: true };
782
+ }
783
+ catch (err) {
784
+ // extractErrorMessage handles the transport's structured plain-object
785
+ // throws (dial() never throws Error instances) — the old
786
+ // `instanceof Error` idiom logged "[object Object]" on every dial
787
+ // failure; try the next addr.
788
+ lastError = extractErrorMessage(err);
789
+ }
790
+ }
791
+ this.#ctx.logger.warn("session.transport.connect.failed", {
792
+ sessionId,
793
+ reason: "counterparty_dial_failed",
794
+ error: lastError,
795
+ correlationId: entry.correlationId,
796
+ });
797
+ return { ok: false, reason: "counterparty_dial_failed", error: lastError };
798
+ }
799
+ /**
800
+ * DOD-M12B-ABANDON-NOTIFY-1 — tell the counterparty we have hung up. Best effort, never blocking.
801
+ *
802
+ * A force-abandon marks the session terminal HERE and did nothing else, so the other side kept
803
+ * its half live, kept retrying delivery into it, and kept trying to re-establish — forever,
804
+ * because nothing would ever answer. That is what produced the 2026-08-17 notification storm:
805
+ * surviving halves calling continuously while the operator saw connection requests from agents
806
+ * nobody was driving.
807
+ *
808
+ * BEST EFFORT, and every caller must treat it that way. A peer that is offline cannot be told, so
809
+ * this is an improvement on silence rather than a guarantee — and it must never delay or fail the
810
+ * abandon, which is the operator's escape hatch out of a session that can never seal.
811
+ */
812
+ async notifyCounterpartyAbandon(agentName, sessionId, correlationId) {
813
+ const entry = this.#ctx.activeNodes.get(this.#ctx.sessionKey(agentName, sessionId));
814
+ if (!entry) {
815
+ // NAMES ITS CAUSE, and it is not the network. An `interrupted` session has no node — the
816
+ // restart sweep and markInterrupted both tear it down — and `interrupted` is exactly the
817
+ // status force-abandon exists for. Reporting this as "could not be reached" sends the
818
+ // operator to debug a connection when the answer is in our own process. At INFO, not debug,
819
+ // because it is the common case and it changes what the operator is told.
820
+ this.#ctx.logger.info("session.abandon.notice.skipped", {
821
+ agentName, sessionId, reason: "no_local_node", correlationId,
822
+ impact: "this side had already torn the session down, so there was nothing to send on — the counterparty was not told",
823
+ });
824
+ return { told: false, reason: "no_local_node" };
825
+ }
826
+ let stream;
827
+ try {
828
+ // Through the RE-DIAL path, not a bare newStream. A session worth force-abandoning is very
829
+ // often one whose connection blipped — the peer is online and calling us, which is the whole
830
+ // complaint — so one demand-driven dial is the difference between telling them and not.
831
+ stream = await this.#ctx.contentOut.openContentStream(agentName, sessionId, entry, correlationId);
832
+ // Typed against protocol-types so the shape cannot drift from the declaration the receiving
833
+ // side (and any second client implementation) reads.
834
+ const notice = {
835
+ type: "session_abandoned_notice",
836
+ session_id: sessionId,
837
+ ...(correlationId === undefined ? {} : { correlation_id: correlationId }),
838
+ };
839
+ const frame = encodeCbor(notice);
840
+ stream.send(lp.encode.single(frame));
841
+ await stream.close();
842
+ this.#ctx.logger.info("session.abandon.notice.sent", { agentName, sessionId, correlationId });
843
+ return { told: true, reason: "sent" };
844
+ }
845
+ catch (err) {
846
+ if (stream !== undefined) {
847
+ try {
848
+ stream.abort(err instanceof Error ? err : new Error(String(err)));
849
+ }
850
+ catch { /* already gone */ }
851
+ }
852
+ this.#ctx.logger.warn("session.abandon.notice.failed", {
853
+ agentName, sessionId, correlationId,
854
+ error: extractErrorMessage(err),
855
+ impact: "the counterparty was not told and may keep calling until it gives up",
856
+ });
857
+ return { told: false, reason: "send_failed" };
858
+ }
859
+ }
860
+ /**
861
+ * DOD-M12B-ABANDON-NOTIFY-1 — the receiving half: our counterparty has abandoned, so retire.
862
+ *
863
+ * RETIRING IS NOT DELETING. The counterparty walking away forfeits the notarized receipt; it must
864
+ * not also cost the operator the record of what was actually said. The transcript and the tree
865
+ * stay exactly as they are.
866
+ *
867
+ * Only an `active` or `interrupted` session moves. A SEALED session has a notarized receipt and
868
+ * must never be turned into an abandoned one by a late or duplicated notice — that would destroy
869
+ * the artifact this protocol exists to produce. An unknown session is refused rather than
870
+ * created: an authenticated stream proves who is speaking, not that a session exists.
871
+ */
872
+ async retireOnCounterpartyAbandon(agentName, sessionId, correlationId) {
873
+ const record = this.#ctx.queries.getSessionRecord(agentName, sessionId);
874
+ if (!record) {
875
+ this.#ctx.logger.warn("session.abandon.notice.unknown_session", { agentName, sessionId, correlationId });
876
+ return false;
877
+ }
878
+ if (record.status !== "active" && record.status !== "interrupted") {
879
+ this.#ctx.logger.debug("session.abandon.notice.ignored", {
880
+ agentName, sessionId, status: record.status, correlationId,
881
+ reason: "session already terminal",
882
+ });
883
+ return false;
884
+ }
885
+ // THE TRANSPORT IS RETIRED. THE SESSION IS NOT.
886
+ //
887
+ // The first build flipped the status to `abandoned`, and that was wrong twice over. It handed
888
+ // the abandoning party a button that DENIES US OUR RECEIPT: the unilateral seal exists for
889
+ // exactly this case — "the counterparty never co-closes" — and produces a notarized certificate
890
+ // after a grace period, but `cello_close_session` refuses an `abandoned` session outright. So
891
+ // one frame from them destroyed a recovery path that already existed, remotely and for free.
892
+ // Today the abandoner can only go silent, and going silent is what the unilateral seal was
893
+ // built to survive.
894
+ //
895
+ // What the DoD actually asks for is that we stop calling them. That is a transport concern:
896
+ // mark it, stop re-dialling, stop retrying delivery — and leave the session sealable.
897
+ const marked = this.#ctx.queries.markCounterpartyAbandoned(agentName, sessionId);
898
+ if (!marked)
899
+ return false;
900
+ // The addresses go, so the demand-driven re-dial has nothing to dial. This is the storm.
901
+ const key = this.#ctx.sessionKey(agentName, sessionId);
902
+ this.#ctx.counterpartyAddrs.delete(key);
903
+ this.#ctx.logger.warn("session.counterparty.abandoned", {
904
+ agentName, sessionId, priorStatus: record.status, correlationId,
905
+ impact: "the counterparty ended this session on their side, so nothing more will arrive and replies cannot reach them — this side stops calling. The session is NOT terminal: a unilateral seal is still available, and the transcript is intact",
906
+ });
907
+ // AWAITED, and `retireSessionNode` NOT `destroySessionNode`. The latter writes the status back
908
+ // — `error` maps to `interrupted` — a few hundred milliseconds later, which silently undid the
909
+ // whole unit; the former is the method that tears a node down without touching the status, and
910
+ // it is what the local force-abandon path already uses.
911
+ await this.retireSessionNode(agentName, sessionId);
912
+ return true;
913
+ }
914
+ /**
915
+ * DOD-M15-FRAME-1 — NARROWING THE GATE DOES NOT EVICT ANYONE ALREADY INSIDE. This does.
916
+ *
917
+ * libp2p consults the gater only when a connection is ESTABLISHED, so narrowing it never evicts a
918
+ * peer already attached. That is why this sweep exists and why it cannot be replaced by the gate.
919
+ *
920
+ * A peer that attached early can therefore hold its connection open, still be attached when the
921
+ * receiver is promoted, and be sitting there when the content protocol activates. DOD-M15-ASSIGN-1
922
+ * shrank who can get that foothold — an unclaimed standing receiver now admits nobody inbound,
923
+ * where it used to admit everyone — but it did not, and could not, change the constraint above.
924
+ *
925
+ * That is the foothold the whole injection path depends on — placed before the door narrows. The
926
+ * frame-level gate above refuses what they send; this closes the connection they send it on, so
927
+ * the stranger is not merely ineffective but gone, and is not sitting there for the next protocol
928
+ * to activate.
929
+ *
930
+ * BEST-EFFORT BY CONSTRUCTION, and it must stay that way. A failure to hang up one peer must not
931
+ * fail the session setup that is mid-flight — the frame gate is the load-bearing control and it
932
+ * does not depend on this succeeding. Relay peers are exempt: they are on the OUTBOUND allowlist
933
+ * because reservation refreshes ride them, and hanging one up would cost the agent its inbound
934
+ * reachability to remove a peer that cannot speak the content protocol anyway.
935
+ */
936
+ async #evictPeersOutsideGate(node, gater, sessionId, allowedPeerId, trigger) {
937
+ let connections;
938
+ try {
939
+ connections = node.getConnections();
940
+ }
941
+ catch (err) {
942
+ this.#ctx.logger.debug("session.gate.evict.unavailable", {
943
+ sessionId, trigger, error: extractErrorMessage(err),
944
+ });
945
+ return;
946
+ }
947
+ const toEvict = connections
948
+ .map((c) => c.peerId)
949
+ .filter((peerId) => peerId !== allowedPeerId && !gater.isAllowedOutboundPeer(peerId));
950
+ /**
951
+ * CONCURRENT AND CAPPED, because the count is ATTACKER-CONTROLLED (review F4).
952
+ *
953
+ * This runs inside `acceptSession`, before the session row is written, and the standing receiver
954
+ * used to accept everyone (closed by DOD-M15-ASSIGN-1) — so opening N connections to an agent's advertised receiver used
955
+ * to make every later session setup on that agent wait for N sequential graceful closes.
956
+ * `hangUp` is libp2p's graceful close and takes no timeout, so the wait was unbounded in both
957
+ * directions. Evicting an injection foothold must not itself become the way to stall an agent.
958
+ *
959
+ * The cap is a LOGGED truncation, never a silent one: what is left behind still cannot inject
960
+ * (the frame gate refuses it and now hangs it up on first contact), and the next promotion
961
+ * sweeps again — but an operator reading this needs to know the sweep did not finish.
962
+ */
963
+ const EVICT_CAP = 32;
964
+ const batch = toEvict.slice(0, EVICT_CAP);
965
+ if (toEvict.length > batch.length) {
966
+ this.#ctx.logger.warn("session.gate.evict.capped", {
967
+ sessionId, trigger, attached: toEvict.length, evicting: batch.length,
968
+ impact: "more peers were attached outside the gate than one promotion evicts; the rest keep their connections until a later sweep, and are refused and hung up by the frame gate if they speak",
969
+ });
970
+ }
971
+ await Promise.allSettled(batch.map(async (peerId) => {
972
+ try {
973
+ await node.hangUp(peerId);
974
+ this.#ctx.logger.warn("session.gate.evicted", {
975
+ sessionId, trigger, evictedPeerId: peerId, allowedPeerId,
976
+ impact: "a peer attached to this node before the session narrowed its gate was disconnected; libp2p does not re-run the gater against live connections, so it would otherwise have stayed attached when the content protocol activated",
977
+ });
978
+ }
979
+ catch (err) {
980
+ // Best-effort: the frame gate still refuses anything this peer sends, and now hangs it up.
981
+ this.#ctx.logger.debug("session.gate.evict.failed", {
982
+ sessionId, trigger, peerId, error: extractErrorMessage(err),
983
+ });
984
+ }
985
+ }));
986
+ }
987
+ /**
988
+ * DOD-LOOP-1: public hook for the composition root to create an agent's standing receiver when
989
+ * the agent comes online (cello_start_agent), and to tear it down when it goes offline.
990
+ * M8B F14: also called from the inbound accept path (ensure on demand). Marks the agent as
991
+ * WANTING a receiver, which arms the teardown re-arm in destroySessionNode/retireSessionNode.
992
+ */
993
+ /**
994
+ * DOD-M12B-SESSION-SEED-1 test seam: the seed the agent's current standing receiver holds.
995
+ *
996
+ * The property under test — "the receiver built behind a promoted one never reuses its seed" — is
997
+ * about an identity that by design never leaves the process, so there is no observable surface for
998
+ * it short of a live two-node dial. Reading it here is the narrowest way to pin it.
999
+ */
1000
+ /** DOD-M12B-SESSION-SEED-1: record the identity this session must be able to return at. */
1001
+ #rememberSessionSeed(agentName, sessionId, seed, counterpartyPeerId, counterpartyPubkey) {
1002
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1003
+ // Defensive: unreachable today because `insertSessionRow` PK-conflicts on a repeat, but an
1004
+ // overwrite that dropped a live seed un-zeroed would leave the one copy we are responsible for
1005
+ // in the heap with nothing tracking it.
1006
+ this.#ctx.sessionSeeds.get(key)?.seed.fill(0);
1007
+ // `counterpartyAddrs` starts empty: at creation the signed assignment has not necessarily
1008
+ // arrived yet. It is filled by `#evictSessionCaches` on the way down, which is the last moment
1009
+ // the live addresses exist.
1010
+ this.#ctx.sessionSeeds.set(key, { seed, counterpartyPeerId, counterpartyPubkey, counterpartyAddrs: [] });
1011
+ }
1012
+ /**
1013
+ * DOD-M12B-SESSION-SEED-1 — destroy a session's transport identity.
1014
+ *
1015
+ * Called from `#updateSessionStatus` on a terminal status, in the SAME step that writes it, so
1016
+ * there is no window in which a session is closed on paper and still revivable in memory.
1017
+ *
1018
+ * **WHAT THE ZERO-FILL DOES AND DOES NOT DO** — checked against the derivation, not assumed.
1019
+ * `createNode` hands the buffer to `generateKeyPairFromSeed`, and `@libp2p/crypto` COPIES it
1020
+ * (`uint8arrayConcat([seed, publicKeyRaw])`, then `Uint8Array.from`). Two consequences:
1021
+ * - zeroing after the node has started is SAFE — the running node holds its own copy;
1022
+ * - it does NOT erase the key from the heap. An identical usable copy is the first 32 bytes of
1023
+ * `privateKey.raw` on the node object until that node is dropped.
1024
+ * So this removes OUR long-lived copy — the one that would otherwise sit in a map for the life of
1025
+ * the process, decoupled from any node — and that is worth doing. It is not a heap scrub, and
1026
+ * the DoD already says the bound rather than secrecy is the control.
1027
+ */
1028
+ destroySessionSeed(agentName, sessionId) {
1029
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1030
+ const identity = this.#ctx.sessionSeeds.get(key);
1031
+ if (identity === undefined)
1032
+ return;
1033
+ identity.seed.fill(0);
1034
+ this.#ctx.sessionSeeds.delete(key);
1035
+ this.#ctx.logger.debug("session.seed.destroyed", { agentName, sessionId });
1036
+ }
1037
+ /**
1038
+ * DOD-M12B-SESSION-SEED-1 — bring an interrupted session back on the peer id it already has.
1039
+ *
1040
+ * THE DEFECT THIS CLOSES. `markInterruptedWithDetails` and `destroySessionNode` stop the node and
1041
+ * delete it from `#activeNodes`, and until now **nothing anywhere recreated one**. A laptop-close
1042
+ * session stayed stuck even though both processes were alive and both keypairs were still in
1043
+ * memory — the trace on 2026-08-17 found no missing transport capability, just a missing edge.
1044
+ *
1045
+ * TWO THINGS HAVE TO HAPPEN, and doing only one leaves the session exactly as stuck:
1046
+ * 1. the NODE comes back, at the same peer id, or the counterparty can never dial us again;
1047
+ * 2. the STATUS comes back to `active`, or every send still refuses with `session_not_active`.
1048
+ *
1049
+ * **DEMAND-DRIVEN ONLY.** Nothing calls this on a timer. That is the `REDIAL-1` discipline and it
1050
+ * is also Andre's tenet — a background rebuilder would hold a dialable endpoint open for a session
1051
+ * nobody is using, which is the "open connection a malicious agent can farm for" in as many words.
1052
+ *
1053
+ * **TERMINAL IS TERMINAL.** A sealed or abandoned session had its seed zeroed in the same step
1054
+ * that wrote its status, so there is nothing to come back on. This refuses by name rather than
1055
+ * minting a fresh identity — a revival that quietly mints would hand one session a second peer id
1056
+ * and break the invariant while appearing to work.
1057
+ *
1058
+ * Idempotent: a session that already has a live node returns ok without building a second one.
1059
+ */
1060
+ async reviveSessionNode(agentName, sessionId) {
1061
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1062
+ const live = this.#ctx.activeNodes.get(key);
1063
+ if (live)
1064
+ return { ok: true, peerId: live.node.getPeerId() };
1065
+ // PARITY with `acceptSession` — and the parity guard in msg-022 is what caught its absence.
1066
+ // A revived session's offer record has almost always been cleared already (it was cleared when
1067
+ // the session was first accepted), so this is usually a no-op. It is here because "usually a
1068
+ // no-op" is not a reason for establishment and revival to do different things: every divergence
1069
+ // between those two paths in this file has been a defect, and the guard exists because one of
1070
+ // them shipped past a green suite for two days.
1071
+ this.#ctx.receivers.clearOfferedDialer(agentName, sessionId);
1072
+ const record = this.#ctx.queries.getSessionRecord(agentName, sessionId);
1073
+ if (!record)
1074
+ return { ok: false, reason: "session_not_found" };
1075
+ /**
1076
+ * A REVIVED SESSION GETS ITS SEAL CHANCES BACK — `DOD-M15-SEAL-FAILED-TERMINAL-1` review
1077
+ * MEDIUM-6, and without this a receipt can be lost permanently and silently.
1078
+ *
1079
+ * `restart_seal_gave_up_at` is written when the restart resolver exhausts its attempts, and
1080
+ * NOTHING ever cleared it. Its stated purpose is narrow — *"a machine restarting ~6 times a day
1081
+ * must not re-run five ceremonies against a hopeless session on every boot"* — and a session
1082
+ * being revived is the opposite of hopeless: something is talking to it again.
1083
+ *
1084
+ * The path it closes: resolver gives up → the column is stamped → the session is REVIVED and
1085
+ * carries live traffic → it is closed → the background ceremony dies → the in-memory failure
1086
+ * marker is lost at the next restart → `listRestartOrphanedSessions` excludes the row forever on
1087
+ * this column → and `listExpiredUnrevivableSessions` explicitly INCLUDES
1088
+ * `restart_seal_gave_up_at IS NOT NULL`, so the revival sweep force-abandons it. Receipt gone,
1089
+ * with no surface having ever said so.
1090
+ *
1091
+ * Bounded, because revival is not a boot-loop: it takes a live counterparty or an operator read.
1092
+ */
1093
+ // One statement, gated in SQL rather than on a field: `SessionRecord` does not carry this column
1094
+ // and widening the type to read it once would spread it through every consumer. `changes` tells
1095
+ // us whether it actually cleared, so the log stays a signal instead of firing on every revival.
1096
+ const clearedGaveUp = this.#db
1097
+ ?.prepare("UPDATE sessions SET restart_seal_gave_up_at = NULL, restart_seal_gave_up_reason = NULL " +
1098
+ "WHERE agent_id = ? AND session_id = ? AND restart_seal_gave_up_at IS NOT NULL")
1099
+ .run(this.#ctx.resolveAgentId(agentName), sessionId);
1100
+ if ((clearedGaveUp?.changes ?? 0) > 0) {
1101
+ this.#ctx.logger.info("session.restart_seal.gave_up.cleared", {
1102
+ agentName, sessionId,
1103
+ impact: "this session is eligible for restart-seal recovery again — it is being revived, so it is not hopeless.",
1104
+ });
1105
+ }
1106
+ if (record.status === "sealed" || record.status === "abandoned" || record.status === "seal_interrupted_pending") {
1107
+ return {
1108
+ ok: false,
1109
+ reason: "session_terminal",
1110
+ guidance: `Session is '${record.status}'. A session that has ended cannot be revived; start a new one.`,
1111
+ };
1112
+ }
1113
+ /**
1114
+ * DOD-M15-FRAME-1 (review F1) — A DEFENSIVE FREEZE MUST NOT UNDO ITSELF ON THE NEXT READ.
1115
+ *
1116
+ * `#freezeOnIdentityFailure` tears the node down, and a teardown writes status `interrupted`.
1117
+ * `interrupted` is not terminal — it is the *revivable* status — so `reviveIfNeededForRead`
1118
+ * fired on the operator's very next `cello_receive`, rebuilt a node behind a gater allowing the
1119
+ * SAME counterparty peer, flipped the row back to `active`, and logged it as a success.
1120
+ *
1121
+ * The freeze therefore lasted until the next keystroke, while the log line said *"no further
1122
+ * content will be accepted on this session"*. A security decision that silently reverses itself,
1123
+ * with a message asserting the opposite, is a worse defect than the one the freeze was added to
1124
+ * fix — and it is the class this milestone exists to remove, reintroduced by its own fix.
1125
+ *
1126
+ * Checked BEFORE the cap and after the terminal statuses, so the answer names the freeze rather
1127
+ * than whatever else the session would have been refused for.
1128
+ *
1129
+ * In memory, and so lost on a daemon restart — the same bound as `DOD-M15-DIVERGE-DURABLE-1`
1130
+ * and for the same reason. The durable column is `DOD-M15-FREEZE-STATUS-1`; the reversibility
1131
+ * could not wait for it.
1132
+ */
1133
+ const frozen = this.#ctx.frozenSessions.get(key);
1134
+ if (frozen) {
1135
+ // The REASON and the GUIDANCE both come from the site that froze it. Hardcoding them here was
1136
+ // correct while an identity failure was the only way in, and became a false accusation the
1137
+ // moment a second one existed — see the note on `#frozenSessions` in `session-node-manager.ts`.
1138
+ return {
1139
+ ok: false,
1140
+ reason: frozen.reason,
1141
+ guidance: `${frozen.guidance} It is not revived automatically, and reading or sending will not clear it. ` +
1142
+ `Your transcript up to the freeze is intact: cello_transcript ${sessionId} reads it. ` +
1143
+ `To end the session and keep what it earned, close it — cello_close_session ${sessionId}. To talk to them again, start a fresh session rather than reviving this one.`,
1144
+ };
1145
+ }
1146
+ /**
1147
+ * THE CAP APPLIES TO A REVIVAL TOO (review: parity gap). Establishment refuses at
1148
+ * `MAX_SESSION_NODES` because each node is a real libp2p instance with listeners, connections
1149
+ * and a relay reservation. A revival builds exactly the same thing, so letting it past the cap
1150
+ * would let a daemon walk over the limit one reconnect at a time — and the limit exists to stop
1151
+ * a machine being taken down by its own session count.
1152
+ *
1153
+ * Refused by name, so the caller can say something true: this is a local resource limit, not a
1154
+ * problem with the session or the counterparty.
1155
+ */
1156
+ if (this.#ctx.activeNodes.size >= MAX_SESSION_NODES) {
1157
+ this.#ctx.logger.warn("session.revive.cap.reached", {
1158
+ agentName,
1159
+ sessionId,
1160
+ activeCount: this.#ctx.activeNodes.size,
1161
+ maxCount: MAX_SESSION_NODES,
1162
+ impact: "this session stays interrupted until another session ends and frees a node slot",
1163
+ });
1164
+ return {
1165
+ ok: false,
1166
+ reason: "session_node_cap_reached",
1167
+ guidance: `This daemon already holds ${MAX_SESSION_NODES} active session nodes, so this session ` +
1168
+ "cannot be brought back yet. Close a session you have finished with and try again.",
1169
+ };
1170
+ }
1171
+ const identity = this.#ctx.sessionSeeds.get(key);
1172
+ if (identity === undefined) {
1173
+ // The honest case: the daemon restarted, so the keypair is genuinely gone. That is
1174
+ // RESTART-SEAL-1's territory (resolve with a receipt), not a revival — and saying so is the
1175
+ // difference between an operator waiting for a reconnect that cannot happen and one closing
1176
+ // the session.
1177
+ return {
1178
+ ok: false,
1179
+ reason: "session_identity_lost",
1180
+ guidance: "This session's transport identity did not survive a daemon restart, so it cannot be " +
1181
+ "revived. It will be sealed automatically, or you can close it now to get its receipt.",
1182
+ };
1183
+ }
1184
+ const gater = new SessionConnectionGater({
1185
+ sessionId,
1186
+ allowedPeerId: identity.counterpartyPeerId,
1187
+ logger: this.#ctx.logger,
1188
+ });
1189
+ // The relay peers must be allowed OUTBOUND before the node starts, or the reservation the line
1190
+ // below depends on is refused by our own gater — the same ordering the receiver builder uses.
1191
+ const reservations = this.#ctx.relay.reservationCircuitAddrs(agentName);
1192
+ for (const relayPeerId of reservations.relayPeerIds)
1193
+ gater.setAllowedOutboundPeer(relayPeerId);
1194
+ let node;
1195
+ const t0 = Date.now();
1196
+ this.#ctx.logger.info("session.revive.node.building", {
1197
+ agentName,
1198
+ sessionId,
1199
+ circuitAddrs: reservations.addrs.length,
1200
+ relayPeerIds: reservations.relayPeerIds.length,
1201
+ });
1202
+ try {
1203
+ node = await this.#ctx.receivers.buildRevivedNode(sessionId, gater, identity.seed, reservations.addrs, agentName);
1204
+ this.#ctx.logger.info("session.revive.node.started", {
1205
+ agentName,
1206
+ sessionId,
1207
+ startMs: Date.now() - t0,
1208
+ listenAddrs: node.listenAddresses().length,
1209
+ circuitListen: node.listenAddresses().filter((a) => a.includes("/p2p-circuit")).length,
1210
+ });
1211
+ }
1212
+ catch (err) {
1213
+ this.#ctx.logger.error("session.revive.node.failed", {
1214
+ agentName,
1215
+ sessionId,
1216
+ error: extractErrorMessage(err),
1217
+ impact: "the session stays interrupted; the next send will attempt this again",
1218
+ });
1219
+ return { ok: false, reason: "session_node_creation_failed" };
1220
+ }
1221
+ const autoNat = new NodeAutoNatService({
1222
+ node,
1223
+ logger: this.#ctx.logger,
1224
+ nodeType: "session",
1225
+ probers: this.#ctx.autoNatProbers(),
1226
+ });
1227
+ autoNat.emitInitialResult();
1228
+ // DOD-M12B-SESSION-SEED-1: give the re-dial its addresses back BEFORE the session goes active,
1229
+ // so the first send after a revival has somewhere to go. Without this the send fails instantly
1230
+ // on a connection that was never made, and — measured live — is lost rather than parked.
1231
+ if (identity.counterpartyAddrs.length > 0) {
1232
+ this.#ctx.counterpartyAddrs.set(key, [...identity.counterpartyAddrs]);
1233
+ }
1234
+ const correlationId = randomUUID();
1235
+ /**
1236
+ * DOD-M12B-REVIVE-PARK-1 — RESTORE THE RELAY, or a revived session cannot park and every failed
1237
+ * send is declared lost.
1238
+ *
1239
+ * This is the defect behind five identical live failures on 2026-08-18. `#parkContent` opens
1240
+ * with `if (!hook || !entry || !entry.relayPeerId || !entry.relayAddrs) return "unconfigured"`,
1241
+ * and a revived entry carried none of it — so the park was skipped and the send fell through to
1242
+ * *"could NOT be queued for retry — it is lost. Send it again."* The relay was recorded on the
1243
+ * session row the whole time, and store-and-forward would have delivered the message: the
1244
+ * counterparty's own sends park through it successfully in the same minute.
1245
+ *
1246
+ * What it cost the operator: their reply was accepted, discarded, and they were told to retype
1247
+ * it — which is how a transcript gets duplicates of a message that was never lost in the first
1248
+ * place.
1249
+ *
1250
+ * Read from the row rather than carried in the revival record on purpose: the row is where the
1251
+ * relay assignment is durable, and it is the same source `getPersistedRelayEndpoint` already
1252
+ * serves the startup flush from — a path that exists precisely because in-memory entries are
1253
+ * gone by then, which is exactly the situation a revival is in.
1254
+ */
1255
+ // ONE lookup, and ONE event for the absent case (review LOW-8). This used to read the endpoint
1256
+ // here and again inside the relay reconnect, and both logged `session.revive.relay.absent` with
1257
+ // different `impact` text — one event name standing for two meanings, fired twice for a single
1258
+ // condition. `reconnectRevivedSessionRelay` (`session-relay.ts`) takes it as a parameter now.
1259
+ const persistedRelay = this.#ctx.queries.getPersistedRelayEndpoint(agentName, sessionId);
1260
+ // 006-CRYPTO: a REVIVED session mints a FRESH keypair and re-keys — Decisions Carried #5. That
1261
+ // holds because the interrupt path destroys the old secret when it drops the entry; until it
1262
+ // did, this call found the stale key still in the map and quietly kept it. The salt, which IS
1263
+ // persisted, is re-read from the row instead — opposite lifetimes, deliberately.
1264
+ this.#ctx.ephemerals.mintSessionEphemeral(agentName, sessionId);
1265
+ /**
1266
+ * AND ANNOUNCE IT — review F1, second half. Minting a fresh key achieves nothing on its own: the
1267
+ * COUNTERPARTY has to hear about it, and it is the side that did NOT restart, so it is not
1268
+ * tearing anything down or reconnecting. `#sendEphemeralFrame` otherwise rides `onPeerConnect`,
1269
+ * which does not fire again for a connection that never dropped — the ordinary shape when only
1270
+ * one end's witness stream closed, which is what a relay roll produces.
1271
+ *
1272
+ * Without this the two ends sit on different keys for the life of the session, every message
1273
+ * fails GCM, and the receiving operator is told the content may have been MODIFIED IN FLIGHT for
1274
+ * what is a local key skew. Deferred a tick so the revived node's handlers are registered before
1275
+ * the frame goes out.
1276
+ */
1277
+ setTimeout(() => { void this.#ctx.ephemerals.sendEphemeralFrame(agentName, sessionId, "revive-rekey"); }, 0);
1278
+ this.#ctx.activeNodes.set(key, {
1279
+ node,
1280
+ agentName,
1281
+ sessionId,
1282
+ counterpartyPubkey: identity.counterpartyPubkey,
1283
+ gater,
1284
+ correlationId,
1285
+ counterpartySessionPeerId: identity.counterpartyPeerId,
1286
+ autoNat,
1287
+ ...(persistedRelay
1288
+ ? { relayPeerId: persistedRelay.relayPeerId, relayAddrs: persistedRelay.relayAddrs }
1289
+ : {}),
1290
+ });
1291
+ await this.#ctx.contentIn.registerContentHandler(agentName, sessionId, node, identity.counterpartyPubkey);
1292
+ /**
1293
+ * review HIGH-2 — REWIRE LIVENESS, or this session can never be interrupted again.
1294
+ *
1295
+ * Both creation paths call this; the first build of the revival did not. Without it the revived
1296
+ * session is pinned `active`: a later disconnect fires no transition, no `session_state_changed`
1297
+ * reaches the MCP client, and the receive surface renders unknown liveness as healthy-and-quiet.
1298
+ * So the SECOND laptop close would leave the operator staring at a session that reports fine and
1299
+ * is dead — this milestone's founding defect, one revival later, and with no status change left
1300
+ * to trigger the next revival either.
1301
+ */
1302
+ this.#ctx.liveness.wireSessionLiveness(agentName, sessionId, node, identity.counterpartyPubkey, correlationId, identity.counterpartyPeerId);
1303
+ // DOD-M12B-REVIVE-RELAY-1: the step revival skipped. Establishment connects the relay witness
1304
+ // here; without it the session comes back with no live inbound path at all.
1305
+ await this.#ctx.relay.reconnectRevivedSessionRelay(agentName, sessionId, node, gater, correlationId, persistedRelay);
1306
+ // THE REVERSE EDGE. A transport event took this session out of `active` and nothing has ever
1307
+ // put one back. Written after the node is live and its handler registered, so the row never
1308
+ // claims `active` for a session that cannot yet receive.
1309
+ //
1310
+ // review MEDIUM-3: the result is CHECKED. `#updateSessionStatus` returns false when the write
1311
+ // matched no row or the DB errored — and reporting revival ok on a row that still says
1312
+ // `interrupted` leaves a live, talking session where REVIVAL-BOUND-1's sweep can seal or abandon
1313
+ // it. Failing here means tearing the node back down rather than running in that split state.
1314
+ if (!this.updateSessionStatus(agentName, sessionId, "active")) {
1315
+ /**
1316
+ * DOD-M15-RELAYLEAK-1 (review MEDIUM-4) — **THIS TEARDOWN LEAKED THE EXACT THING THE LINE IS
1317
+ * ABOUT, THROUGH A DIFFERENT DOOR.**
1318
+ *
1319
+ * `reconnectRevivedSessionRelay` (`session-relay.ts`, not "above") has already called `registerSession` on the cached
1320
+ * relay client and hung it on this entry. Deleting the map key and stopping the node released
1321
+ * the daemon's own objects and left that registration standing with **no owner** — and
1322
+ * `detachSessionRelay` (`session-relay.ts`) closes a client only when `!hasSessions()`, so the orphaned
1323
+ * registration held that predicate false for the life of the process. The client, its
1324
+ * authenticated stream and its relay-side reservation were unreachable and immortal.
1325
+ *
1326
+ * The shutdown loop this line added does sweep it at exit, which is precisely why it had to be
1327
+ * fixed here too: a leak that is only cleaned up by process death is still a leak for every
1328
+ * hour the daemon is up.
1329
+ */
1330
+ /**
1331
+ * ⚠️ Review MEDIUM-2 — **THE ENTRY IS MATCHED BY IDENTITY, NOT BY KEY.** `reviveSessionNode`
1332
+ * has no in-flight guard: its `if (live) return` is separated from `#activeNodes.set` by the
1333
+ * whole node build, so two revivals for one key can both reach the `set` and the second
1334
+ * overwrites the first. Looking up by key alone would then hand THIS failing revival the
1335
+ * OTHER one's live entry, and detaching it would unregister a running session's leaf handler
1336
+ * — closing the client that session is using if it was the last one on it. Comparing `node`
1337
+ * costs one token and makes "the entry I created" provable rather than assumed.
1338
+ */
1339
+ const revivedEntry = this.#ctx.activeNodes.get(key);
1340
+ if (revivedEntry?.node === node)
1341
+ this.#ctx.relay.detachSessionRelay(revivedEntry);
1342
+ this.#ctx.activeNodes.delete(key);
1343
+ // 006-CRYPTO: the revival FAILED, so the key it just minted belongs to a session that never
1344
+ // came back. Dropping the entry without this would strand it for the daemon's lifetime.
1345
+ this.#ctx.ephemerals.destroySessionEphemeralFor(agentName, sessionId);
1346
+ try {
1347
+ await node.stop();
1348
+ }
1349
+ catch { /* best-effort: the status write already failed and is logged with its cause */ }
1350
+ return {
1351
+ ok: false,
1352
+ reason: "session_status_write_failed",
1353
+ guidance: "The session node was rebuilt but its status could not be written, so it was torn back " +
1354
+ "down rather than left live under an interrupted row. The daemon logged the cause.",
1355
+ };
1356
+ }
1357
+ // The messages that failed while this session was down were queued on a promise of "retried on
1358
+ // reconnect". This is that reconnect — fire it before anyone is told the session is back.
1359
+ if (this.#ctx.retryDrainHook !== null) {
1360
+ try {
1361
+ this.#ctx.retryDrainHook(agentName, sessionId);
1362
+ }
1363
+ catch (err) {
1364
+ this.#ctx.logger.warn("session.revive.retry_drain.failed", {
1365
+ agentName,
1366
+ sessionId,
1367
+ error: extractErrorMessage(err),
1368
+ impact: "messages queued while this session was down are still queued",
1369
+ });
1370
+ }
1371
+ }
1372
+ const peerId = node.getPeerId();
1373
+ this.#ctx.logger.info("session.revived", {
1374
+ agentName,
1375
+ sessionId,
1376
+ peerId,
1377
+ // The whole claim of this line, in the log: the id did not change, so the counterparty's
1378
+ // stored dial target is still correct and they do not need to be told anything.
1379
+ identityPreserved: true,
1380
+ });
1381
+ return { ok: true, peerId };
1382
+ }
1383
+ /**
1384
+ * DOD-M12B-SESSION-SEED-1 — the DEMAND edge: a send on an interrupted session revives it.
1385
+ *
1386
+ * One of TWO production callers of `reviveSessionNode` — `reviveIfNeededForRead` is the other —
1387
+ * and both are deliberately demand paths rather than timers. The `REDIAL-1` discipline and Andre's tenet say the same thing from two
1388
+ * directions: nothing may re-open on its own, because a background rebuilder would hold a dialable
1389
+ * endpoint open for a session nobody is using — the *"open connection a malicious agent can farm
1390
+ * for"*. The operator sending is the demand; there is no other trigger.
1391
+ *
1392
+ * A no-op for the normal case. An `active` session with a live node returns immediately without
1393
+ * touching it — this sits on the hot path of every send, and replacing a healthy node would be
1394
+ * churn that changes the peer id for no reason.
1395
+ */
1396
+ async reviveIfNeededForSend(agentName, sessionId) {
1397
+ const record = this.#ctx.queries.getSessionRecord(agentName, sessionId);
1398
+ if (!record)
1399
+ return { ok: false, reason: "session_not_found" };
1400
+ // The overwhelmingly common case: nothing to do, and no node was disturbed to find that out.
1401
+ if (record.status === "active" && this.#ctx.activeNodes.has(this.#ctx.sessionKey(agentName, sessionId)))
1402
+ return { ok: true };
1403
+ const revived = await this.reviveSessionNode(agentName, sessionId);
1404
+ if (!revived.ok) {
1405
+ this.#ctx.logger.info("session.revive.declined", {
1406
+ agentName,
1407
+ sessionId,
1408
+ previousStatus: record.status,
1409
+ trigger: "send",
1410
+ reason: revived.reason,
1411
+ });
1412
+ return revived;
1413
+ }
1414
+ this.#ctx.logger.info("session.revived.on_demand", {
1415
+ agentName,
1416
+ sessionId,
1417
+ previousStatus: record.status,
1418
+ trigger: "send",
1419
+ });
1420
+ return { ok: true };
1421
+ }
1422
+ /**
1423
+ * DOD-M12B-SESSION-SEED-1 (case B) — the INBOUND half of the demand edge.
1424
+ *
1425
+ * `reviveIfNeededForSend` covers the operator waking first. Case B's triggers are symmetric — a
1426
+ * wifi hop, a relay restart, a directory node cycling — so half the time the COUNTERPARTY wakes
1427
+ * first. They send; we have no node yet, because revival is demand-driven and we have demanded
1428
+ * nothing. Their content parks at the relay, which is the backstop working as designed.
1429
+ *
1430
+ * Then the operator comes back and READS, and until now that told them nothing: the receive
1431
+ * handler reads the transcript and never gates on status, so it happily reports what is already
1432
+ * stored while messages sit parked, waiting for a node that will not exist until the operator
1433
+ * happens to SEND. An operator who only reads was stuck forever with a surface that looked fine.
1434
+ *
1435
+ * **WHY A READ MAY TRIGGER THIS AND AN INBOUND DIAL MAY NOT.** Andre's tenet is about what a
1436
+ * REMOTE party can cause: *"an open connection that a malicious agent can farm for."* Reviving
1437
+ * because a peer dialled us would hand that lever straight to the peer — a stranger could keep our
1438
+ * endpoints open indefinitely by poking dead sessions. A read is the OPERATOR asking, on their own
1439
+ * machine, for their own session: the same class of demand as a send, and the class the tenet
1440
+ * allows. That distinction is the whole reason this is a separate entry point rather than a
1441
+ * revival triggered from the inbound handler.
1442
+ */
1443
+ async reviveIfNeededForRead(agentName, sessionId) {
1444
+ const record = this.#ctx.queries.getSessionRecord(agentName, sessionId);
1445
+ if (!record)
1446
+ return { ok: false, reason: "session_not_found" };
1447
+ if (record.status === "active" && this.#ctx.activeNodes.has(this.#ctx.sessionKey(agentName, sessionId)))
1448
+ return { ok: true };
1449
+ // Reading the transcript of an ended session is normal and must keep working — the CALLER does
1450
+ // not treat this refusal as an error, it just reads what is stored. What must not happen is the
1451
+ // read bringing the session back: the receipt is issued and the identity is gone.
1452
+ const revived = await this.reviveSessionNode(agentName, sessionId);
1453
+ if (!revived.ok) {
1454
+ // review MEDIUM-4: the absence of a success line was the only signal that a session could not
1455
+ // come back. `session_identity_lost` is the one an operator most needs, and it was generated
1456
+ // and destroyed one stack frame later with nothing written down.
1457
+ this.#ctx.logger.info("session.revive.declined", {
1458
+ agentName,
1459
+ sessionId,
1460
+ previousStatus: record.status,
1461
+ trigger: "read",
1462
+ reason: revived.reason,
1463
+ });
1464
+ return revived;
1465
+ }
1466
+ this.#ctx.logger.info("session.revived.on_demand", {
1467
+ agentName,
1468
+ sessionId,
1469
+ previousStatus: record.status,
1470
+ trigger: "read",
1471
+ });
1472
+ // Fetch what is waiting NOW. Review MEDIUM-5 corrected the claim this used to make: the drain
1473
+ // runs off the AGENT's standing receiver, not the session node, and the 5-minute backstop would
1474
+ // have delivered this content anyway. So this is an accelerator, not a rescue — worth having,
1475
+ // and worth describing accurately. (The send path deliberately does not fire one: the same
1476
+ // backstop covers it, at a cost of at most one interval.)
1477
+ this.#ctx.park.fireParkedDrain(agentName, "session_revived");
1478
+ return { ok: true };
1479
+ }
1480
+ #insertSessionRow(sessionId, agentName, counterpartyPubkey, status) {
1481
+ if (!this.#db)
1482
+ return false;
1483
+ const now = Date.now();
1484
+ try {
1485
+ /**
1486
+ * ⚠️ THE STARTING POINT AND THE RELAY ANCHOR BOTH GO IN AT INSERT — `DOD-M15-SELFCHAIN-1` and
1487
+ * `DOD-M15-ORDERPROOF-1`, one rule, one reason. Both are recorded before this row exists, so
1488
+ * an UPDATE then matches nothing — as the anchor, written that way first, found. On disk is
1489
+ * what survives a restart: without the genesis the chain cannot resume, without the anchor
1490
+ * there is no key to check the relay against. `null` when none — refused by name, not unlinked.
1491
+ */
1492
+ const genesis = this.#ctx.leafRecords.genesisFor(agentName, sessionId);
1493
+ const relayAnchor = this.#ctx.leafRecords.relayAnchorFor(agentName, sessionId);
1494
+ this.#db
1495
+ .prepare(`INSERT INTO sessions
1496
+ (session_id, agent_id, counterparty_pubkey, status, created_at, updated_at, genesis_prev_root, relay_anchor_hex)
1497
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1498
+ .run(sessionId, this.#ctx.requireAgentId(agentName), counterpartyPubkey, status, now, now, genesis ? Buffer.from(genesis) : null, relayAnchor ?? null);
1499
+ return true;
1500
+ }
1501
+ catch (err) {
1502
+ // D4 review F2: this helper serves the CREATE/ACCEPT paths (and interrupt-restore) — the old
1503
+ // event name `session.interrupt.db.write.failed` steered diagnosis to the interrupt path only.
1504
+ this.#ctx.logger.error("session.row.write.failed", {
1505
+ sessionId,
1506
+ agentName,
1507
+ status,
1508
+ error: extractErrorMessage(err),
1509
+ });
1510
+ return false;
1511
+ }
1512
+ }
1513
+ /** CC-5/F21: unilaterally mark a session locally-terminal ("abandoned") — retire its live node and
1514
+ * set the DB status, with NO bilateral seal (a dead half-open handshake has nothing to notarize).
1515
+ * Used by cello_close_session { force } and the dead-half-open reaper. Idempotent: a missing/already-
1516
+ * abandoned session is a no-op. Resolves true iff the status flip was actually written (CC-10
1517
+ * reviewer LOW: callers must not report a reap as successful when the write failed). */
1518
+ async abandonSession(agentName, sessionId) {
1519
+ // Status flip FIRST and synchronous (before the async node teardown yields), so a non-awaited
1520
+ // reaper call from a read path takes effect for the SAME read (the DB is updated before the await).
1521
+ const flipped = this.updateSessionStatus(agentName, sessionId, "abandoned");
1522
+ await this.retireSessionNode(agentName, sessionId);
1523
+ return flipped;
1524
+ }
1525
+ updateSessionStatus(agentName, sessionId, status,
1526
+ // DOD-CAP-SELF-HEAL-1: who caused an interruption, when this call is the one causing it.
1527
+ // Omitting it leaves the column NULL, which the acceptance bound reads as the counterparty's —
1528
+ // so a LOCAL teardown that forgets to say so is charged to the peer. That is exactly how the
1529
+ // operator's own kill switch (`cello_set_agent_offline` → destroySessionNode) was locking out
1530
+ // a counterparty who had done nothing.
1531
+ interruptedBy) {
1532
+ if (!this.#db)
1533
+ return false;
1534
+ /**
1535
+ * DOD-M12B-SESSION-SEED-1 (review F2) — the identity dies on terminal INTENT, not on a
1536
+ * successful UPDATE.
1537
+ *
1538
+ * The first build destroyed the seed only after the write landed. `#requireAgentId` THROWS for
1539
+ * a retired agent, so every terminal write for a revoked agent's sessions fell into the catch
1540
+ * and kept its transport identity for the life of the process — an identity whose agent has
1541
+ * just been revoked in the directory, held with nothing reporting it, and REVIVAL-BOUND-1's
1542
+ * sweep excludes retired agents so nothing else closed it either. The same held for a
1543
+ * `session.status.write.missed` and for any DB error.
1544
+ *
1545
+ * Coupling a security teardown to a database write is backwards: the write can fail, and the
1546
+ * failure is exactly when we least want a live key lying around. So it runs FIRST and
1547
+ * unconditionally, and if the write then fails the session is one we can no longer revive —
1548
+ * which is the safe direction, and is reported loudly below rather than inferred from the
1549
+ * absence of a debug line.
1550
+ */
1551
+ if (status === "sealed" || status === "abandoned") {
1552
+ this.destroySessionSeed(agentName, sessionId);
1553
+ // DOD-M15-DIVERGE-1: divergence stops being true HERE and only here. It used to be dropped by
1554
+ // `#evictSessionCaches` on every node teardown — including the one that writes `interrupted`,
1555
+ // which is a status the seal gate still acts on, so the fact was forgotten while it was still
1556
+ // load-bearing. A terminal status is the one point at which no future close can be refused,
1557
+ // so the flag has nothing left to protect.
1558
+ this.#ctx.records.clearDivergedMemo(agentName, sessionId);
1559
+ // DURABLE too (DOD-M15-DIVERGE-DURABLE-1) — otherwise a sealed session comes back after a
1560
+ // restart still carrying a refusal for a close that can no longer happen.
1561
+ // (agent_id, session_id) — see markSessionDiverged. Unkeyed, one side sealing cleared the
1562
+ // OTHER side's divergence on a loopback session.
1563
+ this.#db
1564
+ ?.prepare("UPDATE sessions SET diverged_at = NULL WHERE agent_id = ? AND session_id = ?")
1565
+ .run(this.#ctx.requireAgentId(agentName), sessionId);
1566
+ }
1567
+ // THE TERMINAL GUARD LIVES HERE, not in one wrapper, because there are three writers of
1568
+ // "sealed": markSealed, destroySessionNode, and retireSession on the witnessed-submit path.
1569
+ // Guarding only the wrapper asserts the invariant in a test while two other paths still break
1570
+ // it.
1571
+ //
1572
+ // abandoned → sealed REFUSED. A force-abandon is the documented way to give up a receipt.
1573
+ // A certificate arriving afterwards must not silently overturn the operator's decision. The
1574
+ // certificate is still stored by recordSealCertificate and stays retrievable.
1575
+ // sealed → sealed REFUSED. Nothing to write, and re-running the terminal disposition
1576
+ // hooks for a no-op should not be reported as a status that landed.
1577
+ if (status === "sealed") {
1578
+ const current = this.#ctx.queries.getSessionRecord(agentName, sessionId)?.status;
1579
+ if (current === "abandoned" || current === "sealed") {
1580
+ this.#ctx.logger.info("session.seal.status.not_written", {
1581
+ agentName, sessionId, currentStatus: current,
1582
+ impact: current === "abandoned"
1583
+ ? "a certificate arrived for a session the operator force-abandoned; it is stored and retrievable, but the row keeps saying abandoned"
1584
+ : "already sealed — nothing to write",
1585
+ });
1586
+ return false;
1587
+ }
1588
+ if (current === undefined) {
1589
+ // ORDINARY, not an error. recordSealCertificate documents this case: the seal can arrive
1590
+ // before the row is persisted. Falling through would emit `session.status.write.missed` at
1591
+ // ERROR level for a shape the system expects.
1592
+ this.#ctx.logger.info("session.seal.status.no_row", {
1593
+ agentName, sessionId,
1594
+ impact: "no session row yet — the certificate is still recorded and retrievable",
1595
+ });
1596
+ return false;
1597
+ }
1598
+ }
1599
+ const now = Date.now();
1600
+ try {
1601
+ const res = this.#db
1602
+ .prepare(
1603
+ // DOD-M12B-REVIVAL-BOUND-1: this is the FOURTH writer of `status = 'interrupted'`, and
1604
+ // until now the only one that wrote no `interrupted_at`. That is where Entry 41's two
1605
+ // timestamp-less rows came from, and a row with no timestamp has no revival bound that
1606
+ // can be evaluated. `COALESCE` matches the three sibling producers: the FIRST
1607
+ // interruption is the clock, so re-entering the status cannot push the deadline out.
1608
+ status === "interrupted"
1609
+ ? (interruptedBy === undefined
1610
+ ? "UPDATE sessions SET status = ?, updated_at = ?, interrupted_at = COALESCE(interrupted_at, ?) WHERE agent_id = ? AND session_id = ?"
1611
+ : "UPDATE sessions SET status = ?, updated_at = ?, interrupted_at = COALESCE(interrupted_at, ?), interrupted_by = 'local' WHERE agent_id = ? AND session_id = ?")
1612
+ : (interruptedBy === undefined
1613
+ ? "UPDATE sessions SET status = ?, updated_at = ? WHERE agent_id = ? AND session_id = ?"
1614
+ : "UPDATE sessions SET status = ?, updated_at = ?, interrupted_by = 'local' WHERE agent_id = ? AND session_id = ?"))
1615
+ .run(...(status === "interrupted"
1616
+ ? [status, now, new Date(now).toISOString(), this.#ctx.requireAgentId(agentName), sessionId]
1617
+ : [status, now, this.#ctx.requireAgentId(agentName), sessionId]));
1618
+ // "Did not throw" is NOT "landed". An UPDATE whose WHERE matches no row — a wrong agent_id, a
1619
+ // session_id with no row — succeeds silently and changes nothing. Reporting that as a written
1620
+ // status flip is what let a disposition hook delete a live session's content, so the row count
1621
+ // is the answer to both questions.
1622
+ const landed = Number(res?.changes ?? 0) > 0;
1623
+ if (!landed) {
1624
+ this.#ctx.logger.error("session.status.write.missed", {
1625
+ sessionId,
1626
+ status,
1627
+ agentName,
1628
+ impact: (status === "sealed" || status === "abandoned")
1629
+ ? "no session row matched — the status was NOT changed and no disposition was run, AND "
1630
+ + "this session's transport identity has already been destroyed, so it can no longer "
1631
+ + "be revived even though its row still says it is open"
1632
+ : "no session row matched — the status was NOT changed and no disposition was run",
1633
+ });
1634
+ return false;
1635
+ }
1636
+ // DOD-RETRYQ-STRAND-1: only AFTER the status write actually landed. Disposing of durable
1637
+ // state on the strength of a write that did not land would discard content while the session
1638
+ // is still, on disk, drainable. 'interrupted' and 'seal_interrupted_pending' are deliberately
1639
+ // NOT terminal — both can still complete, and reaping them would destroy live content.
1640
+ if (status === "sealed" || status === "abandoned") {
1641
+ // DOD-M12B-STRAND-1: held frames outlive the chain that could have carried them.
1642
+ //
1643
+ // Once a session is terminal, `ingestReceivedContent` (in `session-content-ingest.ts`)
1644
+ // refuses it — and #releaseHeld is only
1645
+ // reachable from ingest — so no code path that exists can ever release a held frame again.
1646
+ // Left alone the rows sit on disk, unreachable by any surface, while the teardown alarm
1647
+ // reports `lost: 0`: a success message for content that has just become permanently
1648
+ // unreadable. The annex is the store built for exactly this shape.
1649
+ this.#ctx.held.annexHeldContentOnTerminal(agentName, sessionId, status);
1650
+ try {
1651
+ this.#ctx.onSessionTerminal?.(sessionId, status);
1652
+ }
1653
+ catch (hookErr) {
1654
+ // The status flip is the caller's contract and has already succeeded; a failing
1655
+ // disposition must not turn it into a reported failure. Named so the strand it leaves
1656
+ // behind is attributable rather than mysterious.
1657
+ this.#ctx.logger.error("session.terminal.disposition.failed", {
1658
+ sessionId,
1659
+ status,
1660
+ error: extractErrorMessage(hookErr),
1661
+ impact: "durable state keyed to this session was not disposed of and may strand",
1662
+ });
1663
+ }
1664
+ }
1665
+ return true;
1666
+ }
1667
+ catch (err) {
1668
+ // CC-5 (reviewer F-2): status-agnostic event + the actual target status in context — this method
1669
+ // now writes "abandoned" too, so labeling every failure "interrupt" was misleading.
1670
+ this.#ctx.logger.error("session.status.write.failed", {
1671
+ sessionId,
1672
+ status,
1673
+ error: extractErrorMessage(err),
1674
+ });
1675
+ return false;
1676
+ }
1677
+ }
1678
+ }
1679
+ //# sourceMappingURL=session-lifecycle.js.map