@cello-protocol/daemon 0.0.225 → 0.0.227

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