@cello-protocol/daemon 0.0.227 → 0.0.229

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (895) hide show
  1. package/dist/agent-admin-handlers.d.ts +56 -0
  2. package/dist/agent-admin-handlers.d.ts.map +1 -0
  3. package/dist/agent-admin-handlers.js +73 -0
  4. package/dist/agent-admin-handlers.js.map +1 -0
  5. package/dist/agent-handlers.d.ts +72 -0
  6. package/dist/agent-handlers.d.ts.map +1 -0
  7. package/dist/agent-handlers.js +604 -0
  8. package/dist/agent-handlers.js.map +1 -0
  9. package/dist/agent-id-migration.d.ts +70 -0
  10. package/dist/agent-id-migration.d.ts.map +1 -0
  11. package/dist/agent-id-migration.js +468 -0
  12. package/dist/agent-id-migration.js.map +1 -0
  13. package/dist/agent-loader.d.ts +37 -0
  14. package/dist/agent-loader.d.ts.map +1 -0
  15. package/dist/agent-loader.js +37 -0
  16. package/dist/agent-loader.js.map +1 -0
  17. package/dist/agent-selection-root.d.ts +29 -0
  18. package/dist/agent-selection-root.d.ts.map +1 -0
  19. package/dist/agent-selection-root.js +126 -0
  20. package/dist/agent-selection-root.js.map +1 -0
  21. package/dist/agent-selection.d.ts +73 -0
  22. package/dist/agent-selection.d.ts.map +1 -0
  23. package/dist/agent-selection.js +80 -0
  24. package/dist/agent-selection.js.map +1 -0
  25. package/dist/agent-settings-keys.d.ts +52 -0
  26. package/dist/agent-settings-keys.d.ts.map +1 -0
  27. package/dist/agent-settings-keys.js +114 -0
  28. package/dist/agent-settings-keys.js.map +1 -0
  29. package/dist/agent-state.d.ts +67 -0
  30. package/dist/agent-state.d.ts.map +1 -0
  31. package/dist/agent-state.js +66 -0
  32. package/dist/agent-state.js.map +1 -0
  33. package/dist/assignment-verify.d.ts +90 -0
  34. package/dist/assignment-verify.d.ts.map +1 -0
  35. package/dist/assignment-verify.js +301 -0
  36. package/dist/assignment-verify.js.map +1 -0
  37. package/dist/attendance-wiring.d.ts +23 -0
  38. package/dist/attendance-wiring.d.ts.map +1 -0
  39. package/dist/attendance-wiring.js +248 -0
  40. package/dist/attendance-wiring.js.map +1 -0
  41. package/dist/attestation-body.d.ts +47 -0
  42. package/dist/attestation-body.d.ts.map +1 -0
  43. package/dist/attestation-body.js +71 -0
  44. package/dist/attestation-body.js.map +1 -0
  45. package/dist/authorship-verification.d.ts +91 -0
  46. package/dist/authorship-verification.d.ts.map +1 -0
  47. package/dist/authorship-verification.js +532 -0
  48. package/dist/authorship-verification.js.map +1 -0
  49. package/dist/away-detection.d.ts +139 -0
  50. package/dist/away-detection.d.ts.map +1 -0
  51. package/dist/away-detection.js +186 -0
  52. package/dist/away-detection.js.map +1 -0
  53. package/dist/away-inbox-oneshot.d.ts +40 -0
  54. package/dist/away-inbox-oneshot.d.ts.map +1 -0
  55. package/dist/away-inbox-oneshot.js +311 -0
  56. package/dist/away-inbox-oneshot.js.map +1 -0
  57. package/dist/backup-restore-handlers.d.ts +10 -0
  58. package/dist/backup-restore-handlers.d.ts.map +1 -0
  59. package/dist/backup-restore-handlers.js +82 -0
  60. package/dist/backup-restore-handlers.js.map +1 -0
  61. package/dist/backup-restore.d.ts +102 -0
  62. package/dist/backup-restore.d.ts.map +1 -0
  63. package/dist/backup-restore.js +341 -0
  64. package/dist/backup-restore.js.map +1 -0
  65. package/dist/bin/cello-daemon.d.ts +13 -0
  66. package/dist/bin/cello-daemon.d.ts.map +1 -0
  67. package/dist/bin/cello-daemon.js.map +1 -0
  68. package/dist/boot-agents.d.ts +64 -0
  69. package/dist/boot-agents.d.ts.map +1 -0
  70. package/dist/boot-agents.js +241 -0
  71. package/dist/boot-agents.js.map +1 -0
  72. package/dist/boot-connection-state.d.ts +38 -0
  73. package/dist/boot-connection-state.d.ts.map +1 -0
  74. package/dist/boot-connection-state.js +95 -0
  75. package/dist/boot-connection-state.js.map +1 -0
  76. package/dist/boot-core.d.ts +36 -0
  77. package/dist/boot-core.d.ts.map +1 -0
  78. package/dist/boot-core.js +293 -0
  79. package/dist/boot-core.js.map +1 -0
  80. package/dist/boot-parked-content.d.ts +34 -0
  81. package/dist/boot-parked-content.d.ts.map +1 -0
  82. package/dist/boot-parked-content.js +528 -0
  83. package/dist/boot-parked-content.js.map +1 -0
  84. package/dist/boot-sweeps.d.ts +22 -0
  85. package/dist/boot-sweeps.d.ts.map +1 -0
  86. package/dist/boot-sweeps.js +51 -0
  87. package/dist/boot-sweeps.js.map +1 -0
  88. package/dist/bundled-consortium-manifest.d.ts +47 -0
  89. package/dist/bundled-consortium-manifest.d.ts.map +1 -0
  90. package/dist/bundled-consortium-manifest.js +116 -0
  91. package/dist/bundled-consortium-manifest.js.map +1 -0
  92. package/dist/cbor-blob-migration.d.ts +8 -0
  93. package/dist/cbor-blob-migration.d.ts.map +1 -0
  94. package/dist/cbor-blob-migration.js +116 -0
  95. package/dist/cbor-blob-migration.js.map +1 -0
  96. package/dist/challenge-verifier.d.ts +12 -0
  97. package/dist/challenge-verifier.d.ts.map +1 -0
  98. package/dist/challenge-verifier.js +11 -0
  99. package/dist/challenge-verifier.js.map +1 -0
  100. package/dist/channel-log-store.d.ts +80 -0
  101. package/dist/channel-log-store.d.ts.map +1 -0
  102. package/dist/channel-log-store.js +290 -0
  103. package/dist/channel-log-store.js.map +1 -0
  104. package/dist/close-commitment.d.ts +67 -0
  105. package/dist/close-commitment.d.ts.map +1 -0
  106. package/dist/close-commitment.js +78 -0
  107. package/dist/close-commitment.js.map +1 -0
  108. package/dist/close-session-handler.d.ts +94 -0
  109. package/dist/close-session-handler.d.ts.map +1 -0
  110. package/dist/close-session-handler.js +1418 -0
  111. package/dist/close-session-handler.js.map +1 -0
  112. package/dist/co-attendance.d.ts +29 -0
  113. package/dist/co-attendance.d.ts.map +1 -0
  114. package/dist/co-attendance.js +32 -0
  115. package/dist/co-attendance.js.map +1 -0
  116. package/dist/column-birth.d.ts +40 -0
  117. package/dist/column-birth.d.ts.map +1 -0
  118. package/dist/column-birth.js +61 -0
  119. package/dist/column-birth.js.map +1 -0
  120. package/dist/connect-or-start.d.ts +25 -0
  121. package/dist/connect-or-start.d.ts.map +1 -0
  122. package/dist/connect-or-start.js +175 -0
  123. package/dist/connect-or-start.js.map +1 -0
  124. package/dist/connection-agents.d.ts +16 -0
  125. package/dist/connection-agents.d.ts.map +1 -0
  126. package/dist/connection-agents.js +68 -0
  127. package/dist/connection-agents.js.map +1 -0
  128. package/dist/consent-migration.d.ts +49 -0
  129. package/dist/consent-migration.d.ts.map +1 -0
  130. package/dist/consent-migration.js +128 -0
  131. package/dist/consent-migration.js.map +1 -0
  132. package/dist/consortium-bootstrap.d.ts +138 -0
  133. package/dist/consortium-bootstrap.d.ts.map +1 -0
  134. package/dist/consortium-bootstrap.js +339 -0
  135. package/dist/consortium-bootstrap.js.map +1 -0
  136. package/dist/consortium-fingerprint.d.ts +115 -0
  137. package/dist/consortium-fingerprint.d.ts.map +1 -0
  138. package/dist/consortium-fingerprint.js +175 -0
  139. package/dist/consortium-fingerprint.js.map +1 -0
  140. package/dist/contact-handlers.d.ts +59 -0
  141. package/dist/contact-handlers.d.ts.map +1 -0
  142. package/dist/contact-handlers.js +343 -0
  143. package/dist/contact-handlers.js.map +1 -0
  144. package/dist/contact-pubkey-case.d.ts +65 -0
  145. package/dist/contact-pubkey-case.d.ts.map +1 -0
  146. package/dist/contact-pubkey-case.js +136 -0
  147. package/dist/contact-pubkey-case.js.map +1 -0
  148. package/dist/contacts-tier-migration.d.ts +90 -0
  149. package/dist/contacts-tier-migration.d.ts.map +1 -0
  150. package/dist/contacts-tier-migration.js +150 -0
  151. package/dist/contacts-tier-migration.js.map +1 -0
  152. package/dist/content-encryption-status.d.ts +111 -0
  153. package/dist/content-encryption-status.d.ts.map +1 -0
  154. package/dist/content-encryption-status.js +158 -0
  155. package/dist/content-encryption-status.js.map +1 -0
  156. package/dist/content-park-client.d.ts +90 -0
  157. package/dist/content-park-client.d.ts.map +1 -0
  158. package/dist/content-park-client.js +362 -0
  159. package/dist/content-park-client.js.map +1 -0
  160. package/dist/content-park.d.ts +52 -0
  161. package/dist/content-park.d.ts.map +1 -0
  162. package/dist/content-park.js +1309 -0
  163. package/dist/content-park.js.map +1 -0
  164. package/dist/cross-node-negotiation.d.ts +44 -0
  165. package/dist/cross-node-negotiation.d.ts.map +1 -0
  166. package/dist/cross-node-negotiation.js +34 -0
  167. package/dist/cross-node-negotiation.js.map +1 -0
  168. package/dist/daemon-handle.d.ts +57 -0
  169. package/dist/daemon-handle.d.ts.map +1 -0
  170. package/dist/daemon-handle.js +2 -0
  171. package/dist/daemon-handle.js.map +1 -0
  172. package/dist/daemon-status-report.d.ts +50 -0
  173. package/dist/daemon-status-report.d.ts.map +1 -0
  174. package/dist/daemon-status-report.js +81 -0
  175. package/dist/daemon-status-report.js.map +1 -0
  176. package/dist/daemon.d.ts +45 -0
  177. package/dist/daemon.d.ts.map +1 -0
  178. package/dist/daemon.js +1177 -0
  179. package/dist/daemon.js.map +1 -0
  180. package/dist/db-identity-store.d.ts +159 -0
  181. package/dist/db-identity-store.d.ts.map +1 -0
  182. package/dist/db-identity-store.js +502 -0
  183. package/dist/db-identity-store.js.map +1 -0
  184. package/dist/delivery-open-registry.d.ts +92 -0
  185. package/dist/delivery-open-registry.d.ts.map +1 -0
  186. package/dist/delivery-open-registry.js +121 -0
  187. package/dist/delivery-open-registry.js.map +1 -0
  188. package/dist/delivery-session-suspects.d.ts +56 -0
  189. package/dist/delivery-session-suspects.d.ts.map +1 -0
  190. package/dist/delivery-session-suspects.js +94 -0
  191. package/dist/delivery-session-suspects.js.map +1 -0
  192. package/dist/directory-auth-posture.d.ts +87 -0
  193. package/dist/directory-auth-posture.d.ts.map +1 -0
  194. package/dist/directory-auth-posture.js +134 -0
  195. package/dist/directory-auth-posture.js.map +1 -0
  196. package/dist/directory-bootstrap.d.ts +310 -0
  197. package/dist/directory-bootstrap.d.ts.map +1 -0
  198. package/dist/directory-bootstrap.js +557 -0
  199. package/dist/directory-bootstrap.js.map +1 -0
  200. package/dist/directory-connect.d.ts +27 -0
  201. package/dist/directory-connect.d.ts.map +1 -0
  202. package/dist/directory-connect.js +106 -0
  203. package/dist/directory-connect.js.map +1 -0
  204. package/dist/disconnect-cleanup.d.ts +51 -0
  205. package/dist/disconnect-cleanup.d.ts.map +1 -0
  206. package/dist/disconnect-cleanup.js +72 -0
  207. package/dist/disconnect-cleanup.js.map +1 -0
  208. package/dist/document-amendment-store.d.ts +120 -0
  209. package/dist/document-amendment-store.d.ts.map +1 -0
  210. package/dist/document-amendment-store.js +266 -0
  211. package/dist/document-amendment-store.js.map +1 -0
  212. package/dist/document-delivery-transport.d.ts +168 -0
  213. package/dist/document-delivery-transport.d.ts.map +1 -0
  214. package/dist/document-delivery-transport.js +206 -0
  215. package/dist/document-delivery-transport.js.map +1 -0
  216. package/dist/document-engine.d.ts +134 -0
  217. package/dist/document-engine.d.ts.map +1 -0
  218. package/dist/document-engine.js +282 -0
  219. package/dist/document-engine.js.map +1 -0
  220. package/dist/document-flag.d.ts +58 -0
  221. package/dist/document-flag.d.ts.map +1 -0
  222. package/dist/document-flag.js +70 -0
  223. package/dist/document-flag.js.map +1 -0
  224. package/dist/document-frame-router.d.ts +245 -0
  225. package/dist/document-frame-router.d.ts.map +1 -0
  226. package/dist/document-frame-router.js +397 -0
  227. package/dist/document-frame-router.js.map +1 -0
  228. package/dist/document-gate-wiring.d.ts +61 -0
  229. package/dist/document-gate-wiring.d.ts.map +1 -0
  230. package/dist/document-gate-wiring.js +125 -0
  231. package/dist/document-gate-wiring.js.map +1 -0
  232. package/dist/document-gate.d.ts +149 -0
  233. package/dist/document-gate.d.ts.map +1 -0
  234. package/dist/document-gate.js +509 -0
  235. package/dist/document-gate.js.map +1 -0
  236. package/dist/document-handlers.d.ts +47 -0
  237. package/dist/document-handlers.d.ts.map +1 -0
  238. package/dist/document-handlers.js +2203 -0
  239. package/dist/document-handlers.js.map +1 -0
  240. package/dist/document-handshake.d.ts +176 -0
  241. package/dist/document-handshake.d.ts.map +1 -0
  242. package/dist/document-handshake.js +452 -0
  243. package/dist/document-handshake.js.map +1 -0
  244. package/dist/document-inbound.d.ts +162 -0
  245. package/dist/document-inbound.d.ts.map +1 -0
  246. package/dist/document-inbound.js +530 -0
  247. package/dist/document-inbound.js.map +1 -0
  248. package/dist/document-json.d.ts +120 -0
  249. package/dist/document-json.d.ts.map +1 -0
  250. package/dist/document-json.js +191 -0
  251. package/dist/document-json.js.map +1 -0
  252. package/dist/document-layer.d.ts +215 -0
  253. package/dist/document-layer.d.ts.map +1 -0
  254. package/dist/document-layer.js +1025 -0
  255. package/dist/document-layer.js.map +1 -0
  256. package/dist/document-lifecycle.d.ts +52 -0
  257. package/dist/document-lifecycle.d.ts.map +1 -0
  258. package/dist/document-lifecycle.js +134 -0
  259. package/dist/document-lifecycle.js.map +1 -0
  260. package/dist/document-live-docs.d.ts +58 -0
  261. package/dist/document-live-docs.d.ts.map +1 -0
  262. package/dist/document-live-docs.js +126 -0
  263. package/dist/document-live-docs.js.map +1 -0
  264. package/dist/document-notify.d.ts +228 -0
  265. package/dist/document-notify.d.ts.map +1 -0
  266. package/dist/document-notify.js +580 -0
  267. package/dist/document-notify.js.map +1 -0
  268. package/dist/document-profile.d.ts +61 -0
  269. package/dist/document-profile.d.ts.map +1 -0
  270. package/dist/document-profile.js +112 -0
  271. package/dist/document-profile.js.map +1 -0
  272. package/dist/document-publish.d.ts +87 -0
  273. package/dist/document-publish.d.ts.map +1 -0
  274. package/dist/document-publish.js +173 -0
  275. package/dist/document-publish.js.map +1 -0
  276. package/dist/document-reachability.d.ts +42 -0
  277. package/dist/document-reachability.d.ts.map +1 -0
  278. package/dist/document-reachability.js +80 -0
  279. package/dist/document-reachability.js.map +1 -0
  280. package/dist/document-reconcile-engine.d.ts +66 -0
  281. package/dist/document-reconcile-engine.d.ts.map +1 -0
  282. package/dist/document-reconcile-engine.js +225 -0
  283. package/dist/document-reconcile-engine.js.map +1 -0
  284. package/dist/document-reconcile-scheduler.d.ts +163 -0
  285. package/dist/document-reconcile-scheduler.d.ts.map +1 -0
  286. package/dist/document-reconcile-scheduler.js +303 -0
  287. package/dist/document-reconcile-scheduler.js.map +1 -0
  288. package/dist/document-rejection.d.ts +251 -0
  289. package/dist/document-rejection.d.ts.map +1 -0
  290. package/dist/document-rejection.js +435 -0
  291. package/dist/document-rejection.js.map +1 -0
  292. package/dist/document-screen.d.ts +114 -0
  293. package/dist/document-screen.d.ts.map +1 -0
  294. package/dist/document-screen.js +223 -0
  295. package/dist/document-screen.js.map +1 -0
  296. package/dist/document-store.d.ts +372 -0
  297. package/dist/document-store.d.ts.map +1 -0
  298. package/dist/document-store.js +931 -0
  299. package/dist/document-store.js.map +1 -0
  300. package/dist/document-surface.d.ts +32 -0
  301. package/dist/document-surface.d.ts.map +1 -0
  302. package/dist/document-surface.js +151 -0
  303. package/dist/document-surface.js.map +1 -0
  304. package/dist/document-types.d.ts +94 -0
  305. package/dist/document-types.d.ts.map +1 -0
  306. package/dist/document-types.js +90 -0
  307. package/dist/document-types.js.map +1 -0
  308. package/dist/document-watch.d.ts +69 -0
  309. package/dist/document-watch.d.ts.map +1 -0
  310. package/dist/document-watch.js +108 -0
  311. package/dist/document-watch.js.map +1 -0
  312. package/dist/document-wiring.d.ts +53 -0
  313. package/dist/document-wiring.d.ts.map +1 -0
  314. package/dist/document-wiring.js +323 -0
  315. package/dist/document-wiring.js.map +1 -0
  316. package/dist/document-write-guard.d.ts +66 -0
  317. package/dist/document-write-guard.d.ts.map +1 -0
  318. package/dist/document-write-guard.js +98 -0
  319. package/dist/document-write-guard.js.map +1 -0
  320. package/dist/document-write-path.d.ts +94 -0
  321. package/dist/document-write-path.d.ts.map +1 -0
  322. package/dist/document-write-path.js +530 -0
  323. package/dist/document-write-path.js.map +1 -0
  324. package/dist/error-message.d.ts +7 -0
  325. package/dist/error-message.d.ts.map +1 -0
  326. package/dist/error-message.js +19 -0
  327. package/dist/error-message.js.map +1 -0
  328. package/dist/file-manifest-provider.d.ts +37 -0
  329. package/dist/file-manifest-provider.d.ts.map +1 -0
  330. package/dist/file-manifest-provider.js +105 -0
  331. package/dist/file-manifest-provider.js.map +1 -0
  332. package/dist/frame-values.d.ts +3 -0
  333. package/dist/frame-values.d.ts.map +1 -0
  334. package/dist/frame-values.js +84 -0
  335. package/dist/frame-values.js.map +1 -0
  336. package/dist/frontier-mismatch.d.ts +73 -0
  337. package/dist/frontier-mismatch.d.ts.map +1 -0
  338. package/dist/frontier-mismatch.js +89 -0
  339. package/dist/frontier-mismatch.js.map +1 -0
  340. package/dist/gateway-config-handlers.d.ts +26 -0
  341. package/dist/gateway-config-handlers.d.ts.map +1 -0
  342. package/dist/gateway-config-handlers.js +454 -0
  343. package/dist/gateway-config-handlers.js.map +1 -0
  344. package/dist/held-content.d.ts +145 -0
  345. package/dist/held-content.d.ts.map +1 -0
  346. package/dist/held-content.js +389 -0
  347. package/dist/held-content.js.map +1 -0
  348. package/dist/http-manifest-poll.d.ts +67 -0
  349. package/dist/http-manifest-poll.d.ts.map +1 -0
  350. package/dist/http-manifest-poll.js +150 -0
  351. package/dist/http-manifest-poll.js.map +1 -0
  352. package/dist/identity-migration.d.ts +40 -0
  353. package/dist/identity-migration.d.ts.map +1 -0
  354. package/dist/identity-migration.js +461 -0
  355. package/dist/identity-migration.js.map +1 -0
  356. package/dist/inbound-refusals.d.ts +283 -0
  357. package/dist/inbound-refusals.d.ts.map +1 -0
  358. package/dist/inbound-refusals.js +919 -0
  359. package/dist/inbound-refusals.js.map +1 -0
  360. package/dist/inbound-seal-request.d.ts +32 -0
  361. package/dist/inbound-seal-request.d.ts.map +1 -0
  362. package/dist/inbound-seal-request.js +228 -0
  363. package/dist/inbound-seal-request.js.map +1 -0
  364. package/dist/inbound-sessions.d.ts +291 -0
  365. package/dist/inbound-sessions.d.ts.map +1 -0
  366. package/dist/inbound-sessions.js +1609 -0
  367. package/dist/inbound-sessions.js.map +1 -0
  368. package/dist/inclusion-proof-handlers.d.ts +43 -0
  369. package/dist/inclusion-proof-handlers.d.ts.map +1 -0
  370. package/dist/inclusion-proof-handlers.js +565 -0
  371. package/dist/inclusion-proof-handlers.js.map +1 -0
  372. package/dist/inclusion-proof.d.ts +151 -0
  373. package/dist/inclusion-proof.d.ts.map +1 -0
  374. package/dist/inclusion-proof.js +228 -0
  375. package/dist/inclusion-proof.js.map +1 -0
  376. package/dist/index.d.ts +32 -0
  377. package/dist/index.d.ts.map +1 -0
  378. package/dist/index.js.map +1 -0
  379. package/dist/initiate-session-handler.d.ts +55 -0
  380. package/dist/initiate-session-handler.d.ts.map +1 -0
  381. package/dist/initiate-session-handler.js +310 -0
  382. package/dist/initiate-session-handler.js.map +1 -0
  383. package/dist/ipc-client.d.ts +31 -0
  384. package/dist/ipc-client.d.ts.map +1 -0
  385. package/dist/ipc-client.js +113 -0
  386. package/dist/ipc-client.js.map +1 -0
  387. package/dist/ipc-server.d.ts +63 -0
  388. package/dist/ipc-server.d.ts.map +1 -0
  389. package/dist/ipc-server.js +429 -0
  390. package/dist/ipc-server.js.map +1 -0
  391. package/dist/ipc-surface.d.ts +44 -0
  392. package/dist/ipc-surface.d.ts.map +1 -0
  393. package/dist/ipc-surface.js +107 -0
  394. package/dist/ipc-surface.js.map +1 -0
  395. package/dist/line-lcs.d.ts +51 -0
  396. package/dist/line-lcs.d.ts.map +1 -0
  397. package/dist/line-lcs.js +71 -0
  398. package/dist/line-lcs.js.map +1 -0
  399. package/dist/lock-file.d.ts +39 -0
  400. package/dist/lock-file.d.ts.map +1 -0
  401. package/dist/lock-file.js +120 -0
  402. package/dist/lock-file.js.map +1 -0
  403. package/dist/log-collapse.d.ts +66 -0
  404. package/dist/log-collapse.d.ts.map +1 -0
  405. package/dist/log-collapse.js +244 -0
  406. package/dist/log-collapse.js.map +1 -0
  407. package/dist/log-rotate.d.ts +67 -0
  408. package/dist/log-rotate.d.ts.map +1 -0
  409. package/dist/log-rotate.js +134 -0
  410. package/dist/log-rotate.js.map +1 -0
  411. package/dist/manifest-deps.d.ts +25 -0
  412. package/dist/manifest-deps.d.ts.map +1 -0
  413. package/dist/manifest-deps.js +151 -0
  414. package/dist/manifest-deps.js.map +1 -0
  415. package/dist/manifest-poll-scheduler.d.ts +31 -0
  416. package/dist/manifest-poll-scheduler.d.ts.map +1 -0
  417. package/dist/manifest-poll-scheduler.js +59 -0
  418. package/dist/manifest-poll-scheduler.js.map +1 -0
  419. package/dist/manifest-validity.d.ts +153 -0
  420. package/dist/manifest-validity.d.ts.map +1 -0
  421. package/dist/manifest-validity.js +268 -0
  422. package/dist/manifest-validity.js.map +1 -0
  423. package/dist/manifest-version-store-db.d.ts +24 -0
  424. package/dist/manifest-version-store-db.d.ts.map +1 -0
  425. package/dist/manifest-version-store-db.js +58 -0
  426. package/dist/manifest-version-store-db.js.map +1 -0
  427. package/dist/manifest-version-store.d.ts +16 -0
  428. package/dist/manifest-version-store.d.ts.map +1 -0
  429. package/dist/manifest-version-store.js +15 -0
  430. package/dist/manifest-version-store.js.map +1 -0
  431. package/dist/network-directory-node.d.ts +136 -0
  432. package/dist/network-directory-node.d.ts.map +1 -0
  433. package/dist/network-directory-node.js +810 -0
  434. package/dist/network-directory-node.js.map +1 -0
  435. package/dist/nonce-dedup.d.ts +68 -0
  436. package/dist/nonce-dedup.d.ts.map +1 -0
  437. package/dist/nonce-dedup.js +205 -0
  438. package/dist/nonce-dedup.js.map +1 -0
  439. package/dist/notification-dispatcher.d.ts +92 -0
  440. package/dist/notification-dispatcher.d.ts.map +1 -0
  441. package/dist/notification-dispatcher.js +210 -0
  442. package/dist/notification-dispatcher.js.map +1 -0
  443. package/dist/notification-handlers.d.ts +53 -0
  444. package/dist/notification-handlers.d.ts.map +1 -0
  445. package/dist/notification-handlers.js +461 -0
  446. package/dist/notification-handlers.js.map +1 -0
  447. package/dist/onboarding-guidance.d.ts +79 -0
  448. package/dist/onboarding-guidance.d.ts.map +1 -0
  449. package/dist/onboarding-guidance.js +95 -0
  450. package/dist/onboarding-guidance.js.map +1 -0
  451. package/dist/operator-guidance.d.ts +25 -0
  452. package/dist/operator-guidance.d.ts.map +1 -0
  453. package/dist/operator-guidance.js +50 -0
  454. package/dist/operator-guidance.js.map +1 -0
  455. package/dist/orphan-triage.d.ts +130 -0
  456. package/dist/orphan-triage.d.ts.map +1 -0
  457. package/dist/orphan-triage.js +207 -0
  458. package/dist/orphan-triage.js.map +1 -0
  459. package/dist/outbound-sessions.d.ts +126 -0
  460. package/dist/outbound-sessions.d.ts.map +1 -0
  461. package/dist/outbound-sessions.js +1052 -0
  462. package/dist/outbound-sessions.js.map +1 -0
  463. package/dist/park-envelope.d.ts +329 -0
  464. package/dist/park-envelope.d.ts.map +1 -0
  465. package/dist/park-envelope.js +509 -0
  466. package/dist/park-envelope.js.map +1 -0
  467. package/dist/park-recovery.d.ts +273 -0
  468. package/dist/park-recovery.d.ts.map +1 -0
  469. package/dist/park-recovery.js +717 -0
  470. package/dist/park-recovery.js.map +1 -0
  471. package/dist/park-refusals.d.ts +147 -0
  472. package/dist/park-refusals.d.ts.map +1 -0
  473. package/dist/park-refusals.js +331 -0
  474. package/dist/park-refusals.js.map +1 -0
  475. package/dist/quarantine-framing.d.ts +92 -0
  476. package/dist/quarantine-framing.d.ts.map +1 -0
  477. package/dist/quarantine-framing.js +111 -0
  478. package/dist/quarantine-framing.js.map +1 -0
  479. package/dist/reconnect-drain.d.ts +22 -0
  480. package/dist/reconnect-drain.d.ts.map +1 -0
  481. package/dist/reconnect-drain.js +65 -0
  482. package/dist/reconnect-drain.js.map +1 -0
  483. package/dist/recovered-position.d.ts +14 -0
  484. package/dist/recovered-position.d.ts.map +1 -0
  485. package/dist/recovered-position.js +59 -0
  486. package/dist/recovered-position.js.map +1 -0
  487. package/dist/refusal-notices.d.ts +196 -0
  488. package/dist/refusal-notices.d.ts.map +1 -0
  489. package/dist/refusal-notices.js +516 -0
  490. package/dist/refusal-notices.js.map +1 -0
  491. package/dist/refusal-reasons.d.ts +245 -0
  492. package/dist/refusal-reasons.d.ts.map +1 -0
  493. package/dist/refusal-reasons.js +363 -0
  494. package/dist/refusal-reasons.js.map +1 -0
  495. package/dist/register-handler.d.ts +36 -0
  496. package/dist/register-handler.d.ts.map +1 -0
  497. package/dist/register-handler.js +285 -0
  498. package/dist/register-handler.js.map +1 -0
  499. package/dist/registration-context.d.ts +72 -0
  500. package/dist/registration-context.d.ts.map +1 -0
  501. package/dist/registration-context.js +126 -0
  502. package/dist/registration-context.js.map +1 -0
  503. package/dist/registration-manager.d.ts +94 -0
  504. package/dist/registration-manager.d.ts.map +1 -0
  505. package/dist/registration-manager.js +585 -0
  506. package/dist/registration-manager.js.map +1 -0
  507. package/dist/registration-persistence.d.ts +183 -0
  508. package/dist/registration-persistence.d.ts.map +1 -0
  509. package/dist/registration-persistence.js +263 -0
  510. package/dist/registration-persistence.js.map +1 -0
  511. package/dist/registry-poll.d.ts +52 -0
  512. package/dist/registry-poll.d.ts.map +1 -0
  513. package/dist/registry-poll.js +140 -0
  514. package/dist/registry-poll.js.map +1 -0
  515. package/dist/registry-version-store-db.d.ts +22 -0
  516. package/dist/registry-version-store-db.d.ts.map +1 -0
  517. package/dist/registry-version-store-db.js +50 -0
  518. package/dist/registry-version-store-db.js.map +1 -0
  519. package/dist/relay-endpoints.d.ts +18 -0
  520. package/dist/relay-endpoints.d.ts.map +1 -0
  521. package/dist/relay-endpoints.js +9 -0
  522. package/dist/relay-endpoints.js.map +1 -0
  523. package/dist/relay-only.d.ts +140 -0
  524. package/dist/relay-only.d.ts.map +1 -0
  525. package/dist/relay-only.js +193 -0
  526. package/dist/relay-only.js.map +1 -0
  527. package/dist/relay-receipt-store.d.ts +155 -0
  528. package/dist/relay-receipt-store.d.ts.map +1 -0
  529. package/dist/relay-receipt-store.js +284 -0
  530. package/dist/relay-receipt-store.js.map +1 -0
  531. package/dist/relay-reconnect.d.ts +32 -0
  532. package/dist/relay-reconnect.d.ts.map +1 -0
  533. package/dist/relay-reconnect.js +29 -0
  534. package/dist/relay-reconnect.js.map +1 -0
  535. package/dist/reply-lag.d.ts +11 -0
  536. package/dist/reply-lag.d.ts.map +1 -0
  537. package/dist/reply-lag.js +46 -0
  538. package/dist/reply-lag.js.map +1 -0
  539. package/dist/resolve-named-agent.d.ts +49 -0
  540. package/dist/resolve-named-agent.d.ts.map +1 -0
  541. package/dist/resolve-named-agent.js +77 -0
  542. package/dist/resolve-named-agent.js.map +1 -0
  543. package/dist/restart-seal-resolver.d.ts +110 -0
  544. package/dist/restart-seal-resolver.d.ts.map +1 -0
  545. package/dist/restart-seal-resolver.js +353 -0
  546. package/dist/restart-seal-resolver.js.map +1 -0
  547. package/dist/resume-last-seen.d.ts +15 -0
  548. package/dist/resume-last-seen.d.ts.map +1 -0
  549. package/dist/resume-last-seen.js +44 -0
  550. package/dist/resume-last-seen.js.map +1 -0
  551. package/dist/retry-queue.d.ts +203 -0
  552. package/dist/retry-queue.d.ts.map +1 -0
  553. package/dist/retry-queue.js +701 -0
  554. package/dist/retry-queue.js.map +1 -0
  555. package/dist/roster-freshness.d.ts +160 -0
  556. package/dist/roster-freshness.d.ts.map +1 -0
  557. package/dist/roster-freshness.js +250 -0
  558. package/dist/roster-freshness.js.map +1 -0
  559. package/dist/screening-status.d.ts +15 -0
  560. package/dist/screening-status.d.ts.map +1 -0
  561. package/dist/screening-status.js +65 -0
  562. package/dist/screening-status.js.map +1 -0
  563. package/dist/seal-carried-close.d.ts +37 -0
  564. package/dist/seal-carried-close.d.ts.map +1 -0
  565. package/dist/seal-carried-close.js +179 -0
  566. package/dist/seal-carried-close.js.map +1 -0
  567. package/dist/seal-certificate-pull.d.ts +79 -0
  568. package/dist/seal-certificate-pull.d.ts.map +1 -0
  569. package/dist/seal-certificate-pull.js +186 -0
  570. package/dist/seal-certificate-pull.js.map +1 -0
  571. package/dist/seal-certified-root-check.d.ts +37 -0
  572. package/dist/seal-certified-root-check.d.ts.map +1 -0
  573. package/dist/seal-certified-root-check.js +127 -0
  574. package/dist/seal-certified-root-check.js.map +1 -0
  575. package/dist/seal-coordinator.d.ts +118 -0
  576. package/dist/seal-coordinator.d.ts.map +1 -0
  577. package/dist/seal-coordinator.js +927 -0
  578. package/dist/seal-coordinator.js.map +1 -0
  579. package/dist/seal-escalation.d.ts +80 -0
  580. package/dist/seal-escalation.d.ts.map +1 -0
  581. package/dist/seal-escalation.js +283 -0
  582. package/dist/seal-escalation.js.map +1 -0
  583. package/dist/seal-evidence-root-check.d.ts +70 -0
  584. package/dist/seal-evidence-root-check.d.ts.map +1 -0
  585. package/dist/seal-evidence-root-check.js +199 -0
  586. package/dist/seal-evidence-root-check.js.map +1 -0
  587. package/dist/seal-failure-store.d.ts +139 -0
  588. package/dist/seal-failure-store.d.ts.map +1 -0
  589. package/dist/seal-failure-store.js +207 -0
  590. package/dist/seal-failure-store.js.map +1 -0
  591. package/dist/seal-flows.d.ts +107 -0
  592. package/dist/seal-flows.d.ts.map +1 -0
  593. package/dist/seal-flows.js +638 -0
  594. package/dist/seal-flows.js.map +1 -0
  595. package/dist/seal-frontier-verify.d.ts +103 -0
  596. package/dist/seal-frontier-verify.d.ts.map +1 -0
  597. package/dist/seal-frontier-verify.js +143 -0
  598. package/dist/seal-frontier-verify.js.map +1 -0
  599. package/dist/seal-leaf.d.ts +58 -0
  600. package/dist/seal-leaf.d.ts.map +1 -0
  601. package/dist/seal-leaf.js +112 -0
  602. package/dist/seal-leaf.js.map +1 -0
  603. package/dist/seal-legibility-tbs.d.ts +25 -0
  604. package/dist/seal-legibility-tbs.d.ts.map +1 -0
  605. package/dist/seal-legibility-tbs.js +77 -0
  606. package/dist/seal-legibility-tbs.js.map +1 -0
  607. package/dist/seal-local-terminus.d.ts +41 -0
  608. package/dist/seal-local-terminus.d.ts.map +1 -0
  609. package/dist/seal-local-terminus.js +170 -0
  610. package/dist/seal-local-terminus.js.map +1 -0
  611. package/dist/seal-receipt-upgrade.d.ts +30 -0
  612. package/dist/seal-receipt-upgrade.d.ts.map +1 -0
  613. package/dist/seal-receipt-upgrade.js +49 -0
  614. package/dist/seal-receipt-upgrade.js.map +1 -0
  615. package/dist/seal-relay-silence.d.ts +51 -0
  616. package/dist/seal-relay-silence.d.ts.map +1 -0
  617. package/dist/seal-relay-silence.js +76 -0
  618. package/dist/seal-relay-silence.js.map +1 -0
  619. package/dist/seal-settle.d.ts +38 -0
  620. package/dist/seal-settle.d.ts.map +1 -0
  621. package/dist/seal-settle.js +33 -0
  622. package/dist/seal-settle.js.map +1 -0
  623. package/dist/seal-upgrade.d.ts +107 -0
  624. package/dist/seal-upgrade.d.ts.map +1 -0
  625. package/dist/seal-upgrade.js +200 -0
  626. package/dist/seal-upgrade.js.map +1 -0
  627. package/dist/sealed-conversation.d.ts +36 -0
  628. package/dist/sealed-conversation.d.ts.map +1 -0
  629. package/dist/sealed-conversation.js +171 -0
  630. package/dist/sealed-conversation.js.map +1 -0
  631. package/dist/sealed-leaf-set.d.ts +92 -0
  632. package/dist/sealed-leaf-set.d.ts.map +1 -0
  633. package/dist/sealed-leaf-set.js +122 -0
  634. package/dist/sealed-leaf-set.js.map +1 -0
  635. package/dist/send-claims.d.ts +56 -0
  636. package/dist/send-claims.d.ts.map +1 -0
  637. package/dist/send-claims.js +50 -0
  638. package/dist/send-claims.js.map +1 -0
  639. package/dist/session-assignment-parser.d.ts +104 -0
  640. package/dist/session-assignment-parser.d.ts.map +1 -0
  641. package/dist/session-assignment-parser.js +345 -0
  642. package/dist/session-assignment-parser.js.map +1 -0
  643. package/dist/session-category.d.ts +19 -0
  644. package/dist/session-category.d.ts.map +1 -0
  645. package/dist/session-category.js +14 -0
  646. package/dist/session-category.js.map +1 -0
  647. package/dist/session-ceremony.d.ts +297 -0
  648. package/dist/session-ceremony.d.ts.map +1 -0
  649. package/dist/session-ceremony.js +964 -0
  650. package/dist/session-ceremony.js.map +1 -0
  651. package/dist/session-closed.d.ts +79 -0
  652. package/dist/session-closed.d.ts.map +1 -0
  653. package/dist/session-closed.js +189 -0
  654. package/dist/session-closed.js.map +1 -0
  655. package/dist/session-connection-gater.d.ts +148 -0
  656. package/dist/session-connection-gater.d.ts.map +1 -0
  657. package/dist/session-connection-gater.js +332 -0
  658. package/dist/session-connection-gater.js.map +1 -0
  659. package/dist/session-content-context.d.ts +162 -0
  660. package/dist/session-content-context.d.ts.map +1 -0
  661. package/dist/session-content-context.js +2 -0
  662. package/dist/session-content-context.js.map +1 -0
  663. package/dist/session-content-handlers.d.ts +82 -0
  664. package/dist/session-content-handlers.d.ts.map +1 -0
  665. package/dist/session-content-handlers.js +1382 -0
  666. package/dist/session-content-handlers.js.map +1 -0
  667. package/dist/session-content-ingest.d.ts +206 -0
  668. package/dist/session-content-ingest.d.ts.map +1 -0
  669. package/dist/session-content-ingest.js +2167 -0
  670. package/dist/session-content-ingest.js.map +1 -0
  671. package/dist/session-content-send.d.ts +191 -0
  672. package/dist/session-content-send.d.ts.map +1 -0
  673. package/dist/session-content-send.js +1360 -0
  674. package/dist/session-content-send.js.map +1 -0
  675. package/dist/session-delivery-acks.d.ts +169 -0
  676. package/dist/session-delivery-acks.d.ts.map +1 -0
  677. package/dist/session-delivery-acks.js +569 -0
  678. package/dist/session-delivery-acks.js.map +1 -0
  679. package/dist/session-ephemerals.d.ts +279 -0
  680. package/dist/session-ephemerals.d.ts.map +1 -0
  681. package/dist/session-ephemerals.js +591 -0
  682. package/dist/session-ephemerals.js.map +1 -0
  683. package/dist/session-leaf-records.d.ts +159 -0
  684. package/dist/session-leaf-records.d.ts.map +1 -0
  685. package/dist/session-leaf-records.js +408 -0
  686. package/dist/session-leaf-records.js.map +1 -0
  687. package/dist/session-lifecycle.d.ts +303 -0
  688. package/dist/session-lifecycle.d.ts.map +1 -0
  689. package/dist/session-lifecycle.js +1679 -0
  690. package/dist/session-lifecycle.js.map +1 -0
  691. package/dist/session-liveness.d.ts +135 -0
  692. package/dist/session-liveness.d.ts.map +1 -0
  693. package/dist/session-liveness.js +347 -0
  694. package/dist/session-liveness.js.map +1 -0
  695. package/dist/session-name.d.ts +35 -0
  696. package/dist/session-name.d.ts.map +1 -0
  697. package/dist/session-name.js +60 -0
  698. package/dist/session-name.js.map +1 -0
  699. package/dist/session-node-factory.d.ts +18 -0
  700. package/dist/session-node-factory.d.ts.map +1 -0
  701. package/dist/session-node-factory.js +182 -0
  702. package/dist/session-node-factory.js.map +1 -0
  703. package/dist/session-node-manager.d.ts +877 -0
  704. package/dist/session-node-manager.d.ts.map +1 -0
  705. package/dist/session-node-manager.js +2998 -0
  706. package/dist/session-node-manager.js.map +1 -0
  707. package/dist/session-node-types.d.ts +1057 -0
  708. package/dist/session-node-types.d.ts.map +1 -0
  709. package/dist/session-node-types.js +657 -0
  710. package/dist/session-node-types.js.map +1 -0
  711. package/dist/session-notify.d.ts +46 -0
  712. package/dist/session-notify.d.ts.map +1 -0
  713. package/dist/session-notify.js +116 -0
  714. package/dist/session-notify.js.map +1 -0
  715. package/dist/session-own-chain-store.d.ts +65 -0
  716. package/dist/session-own-chain-store.d.ts.map +1 -0
  717. package/dist/session-own-chain-store.js +75 -0
  718. package/dist/session-own-chain-store.js.map +1 -0
  719. package/dist/session-queries.d.ts +476 -0
  720. package/dist/session-queries.d.ts.map +1 -0
  721. package/dist/session-queries.js +1007 -0
  722. package/dist/session-queries.js.map +1 -0
  723. package/dist/session-read-handlers.d.ts +87 -0
  724. package/dist/session-read-handlers.d.ts.map +1 -0
  725. package/dist/session-read-handlers.js +689 -0
  726. package/dist/session-read-handlers.js.map +1 -0
  727. package/dist/session-records.d.ts +341 -0
  728. package/dist/session-records.d.ts.map +1 -0
  729. package/dist/session-records.js +858 -0
  730. package/dist/session-records.js.map +1 -0
  731. package/dist/session-relay-client.d.ts +659 -0
  732. package/dist/session-relay-client.d.ts.map +1 -0
  733. package/dist/session-relay-client.js +2876 -0
  734. package/dist/session-relay-client.js.map +1 -0
  735. package/dist/session-relay.d.ts +397 -0
  736. package/dist/session-relay.d.ts.map +1 -0
  737. package/dist/session-relay.js +1636 -0
  738. package/dist/session-relay.js.map +1 -0
  739. package/dist/session-salt-agreement.d.ts +331 -0
  740. package/dist/session-salt-agreement.d.ts.map +1 -0
  741. package/dist/session-salt-agreement.js +472 -0
  742. package/dist/session-salt-agreement.js.map +1 -0
  743. package/dist/session-salts.d.ts +432 -0
  744. package/dist/session-salts.d.ts.map +1 -0
  745. package/dist/session-salts.js +1540 -0
  746. package/dist/session-salts.js.map +1 -0
  747. package/dist/session-schema.d.ts +30 -0
  748. package/dist/session-schema.d.ts.map +1 -0
  749. package/dist/session-schema.js +877 -0
  750. package/dist/session-schema.js.map +1 -0
  751. package/dist/session-seal-leaf-store.d.ts +70 -0
  752. package/dist/session-seal-leaf-store.d.ts.map +1 -0
  753. package/dist/session-seal-leaf-store.js +105 -0
  754. package/dist/session-seal-leaf-store.js.map +1 -0
  755. package/dist/session-seal.d.ts +334 -0
  756. package/dist/session-seal.d.ts.map +1 -0
  757. package/dist/session-seal.js +1017 -0
  758. package/dist/session-seal.js.map +1 -0
  759. package/dist/session-terminal-refusal.d.ts +65 -0
  760. package/dist/session-terminal-refusal.d.ts.map +1 -0
  761. package/dist/session-terminal-refusal.js +87 -0
  762. package/dist/session-terminal-refusal.js.map +1 -0
  763. package/dist/session-tree.d.ts +110 -0
  764. package/dist/session-tree.d.ts.map +1 -0
  765. package/dist/session-tree.js +144 -0
  766. package/dist/session-tree.js.map +1 -0
  767. package/dist/session-views.d.ts +47 -0
  768. package/dist/session-views.d.ts.map +1 -0
  769. package/dist/session-views.js +278 -0
  770. package/dist/session-views.js.map +1 -0
  771. package/dist/signal-handlers.d.ts +63 -0
  772. package/dist/signal-handlers.d.ts.map +1 -0
  773. package/dist/signal-handlers.js +980 -0
  774. package/dist/signal-handlers.js.map +1 -0
  775. package/dist/signal-requirement-policy.d.ts +51 -0
  776. package/dist/signal-requirement-policy.d.ts.map +1 -0
  777. package/dist/signal-requirement-policy.js +89 -0
  778. package/dist/signal-requirement-policy.js.map +1 -0
  779. package/dist/signal-revocability.d.ts +49 -0
  780. package/dist/signal-revocability.d.ts.map +1 -0
  781. package/dist/signal-revocability.js +93 -0
  782. package/dist/signal-revocability.js.map +1 -0
  783. package/dist/signal-submission.d.ts +181 -0
  784. package/dist/signal-submission.d.ts.map +1 -0
  785. package/dist/signal-submission.js +368 -0
  786. package/dist/signal-submission.js.map +1 -0
  787. package/dist/signaling-connect.d.ts +118 -0
  788. package/dist/signaling-connect.d.ts.map +1 -0
  789. package/dist/signaling-connect.js +538 -0
  790. package/dist/signaling-connect.js.map +1 -0
  791. package/dist/signaling-wiring.d.ts +105 -0
  792. package/dist/signaling-wiring.d.ts.map +1 -0
  793. package/dist/signaling-wiring.js +393 -0
  794. package/dist/signaling-wiring.js.map +1 -0
  795. package/dist/singleton-lock.d.ts +85 -0
  796. package/dist/singleton-lock.d.ts.map +1 -0
  797. package/dist/singleton-lock.js +219 -0
  798. package/dist/singleton-lock.js.map +1 -0
  799. package/dist/sqlcipher-db.d.ts +139 -0
  800. package/dist/sqlcipher-db.d.ts.map +1 -0
  801. package/dist/sqlcipher-db.js +357 -0
  802. package/dist/sqlcipher-db.js.map +1 -0
  803. package/dist/standing-receivers.d.ts +319 -0
  804. package/dist/standing-receivers.d.ts.map +1 -0
  805. package/dist/standing-receivers.js +1295 -0
  806. package/dist/standing-receivers.js.map +1 -0
  807. package/dist/start-agent.d.ts +64 -0
  808. package/dist/start-agent.d.ts.map +1 -0
  809. package/dist/start-agent.js +136 -0
  810. package/dist/start-agent.js.map +1 -0
  811. package/dist/status-handler.d.ts +54 -0
  812. package/dist/status-handler.d.ts.map +1 -0
  813. package/dist/status-handler.js +61 -0
  814. package/dist/status-handler.js.map +1 -0
  815. package/dist/submission-retry.d.ts +208 -0
  816. package/dist/submission-retry.d.ts.map +1 -0
  817. package/dist/submission-retry.js +506 -0
  818. package/dist/submission-retry.js.map +1 -0
  819. package/dist/telegram-bot-client.d.ts +34 -0
  820. package/dist/telegram-bot-client.d.ts.map +1 -0
  821. package/dist/telegram-bot-client.js +36 -0
  822. package/dist/telegram-bot-client.js.map +1 -0
  823. package/dist/telegram-doorbell.d.ts +38 -0
  824. package/dist/telegram-doorbell.d.ts.map +1 -0
  825. package/dist/telegram-doorbell.js +140 -0
  826. package/dist/telegram-doorbell.js.map +1 -0
  827. package/dist/test-handlers.d.ts +62 -0
  828. package/dist/test-handlers.d.ts.map +1 -0
  829. package/dist/test-handlers.js +240 -0
  830. package/dist/test-handlers.js.map +1 -0
  831. package/dist/testing.d.ts +10 -0
  832. package/dist/testing.d.ts.map +1 -0
  833. package/dist/testing.js +10 -0
  834. package/dist/testing.js.map +1 -0
  835. package/dist/transport-composition.d.ts +31 -0
  836. package/dist/transport-composition.d.ts.map +1 -0
  837. package/dist/transport-composition.js +55 -0
  838. package/dist/transport-composition.js.map +1 -0
  839. package/dist/transport-selector.d.ts +202 -0
  840. package/dist/transport-selector.d.ts.map +1 -0
  841. package/dist/transport-selector.js +196 -0
  842. package/dist/transport-selector.js.map +1 -0
  843. package/dist/trust-signal-pickup-listener.d.ts +43 -0
  844. package/dist/trust-signal-pickup-listener.d.ts.map +1 -0
  845. package/dist/trust-signal-pickup-listener.js +48 -0
  846. package/dist/trust-signal-pickup-listener.js.map +1 -0
  847. package/dist/trust-signal-store.d.ts +406 -0
  848. package/dist/trust-signal-store.d.ts.map +1 -0
  849. package/dist/trust-signal-store.js +939 -0
  850. package/dist/trust-signal-store.js.map +1 -0
  851. package/dist/trust-signal-sweep-tick.d.ts +60 -0
  852. package/dist/trust-signal-sweep-tick.d.ts.map +1 -0
  853. package/dist/trust-signal-sweep-tick.js +101 -0
  854. package/dist/trust-signal-sweep-tick.js.map +1 -0
  855. package/dist/trust-signal-sweep.d.ts +94 -0
  856. package/dist/trust-signal-sweep.d.ts.map +1 -0
  857. package/dist/trust-signal-sweep.js +147 -0
  858. package/dist/trust-signal-sweep.js.map +1 -0
  859. package/dist/type-registry.d.ts +42 -0
  860. package/dist/type-registry.d.ts.map +1 -0
  861. package/dist/type-registry.js +37 -0
  862. package/dist/type-registry.js.map +1 -0
  863. package/dist/types.d.ts +746 -0
  864. package/dist/types.d.ts.map +1 -0
  865. package/dist/types.js +20 -0
  866. package/dist/types.js.map +1 -0
  867. package/dist/unresolved-nodes-report.d.ts +14 -0
  868. package/dist/unresolved-nodes-report.d.ts.map +1 -0
  869. package/dist/unresolved-nodes-report.js +96 -0
  870. package/dist/unresolved-nodes-report.js.map +1 -0
  871. package/dist/vocabulary.d.ts +150 -0
  872. package/dist/vocabulary.d.ts.map +1 -0
  873. package/dist/vocabulary.js +386 -0
  874. package/dist/vocabulary.js.map +1 -0
  875. package/dist/who-label.d.ts +28 -0
  876. package/dist/who-label.d.ts.map +1 -0
  877. package/dist/who-label.js +31 -0
  878. package/dist/who-label.js.map +1 -0
  879. package/dist/who-resolver.d.ts +15 -0
  880. package/dist/who-resolver.d.ts.map +1 -0
  881. package/dist/who-resolver.js +47 -0
  882. package/dist/who-resolver.js.map +1 -0
  883. package/dist/wire-content-hash.d.ts +93 -0
  884. package/dist/wire-content-hash.d.ts.map +1 -0
  885. package/dist/wire-content-hash.js +116 -0
  886. package/dist/wire-content-hash.js.map +1 -0
  887. package/dist/withheld-content.d.ts +13 -0
  888. package/dist/withheld-content.d.ts.map +1 -0
  889. package/dist/withheld-content.js +46 -0
  890. package/dist/withheld-content.js.map +1 -0
  891. package/dist/witness-alerts.d.ts +40 -0
  892. package/dist/witness-alerts.d.ts.map +1 -0
  893. package/dist/witness-alerts.js +102 -0
  894. package/dist/witness-alerts.js.map +1 -0
  895. package/package.json +5 -5
@@ -0,0 +1,1540 @@
1
+ import { extractErrorMessage } from "./error-message.js";
2
+ import { generateSaltContribution, SESSION_SALT_BYTES } from "@cello-protocol/crypto";
3
+ import { onPeerSaltFrame, SALT_ADOPTION_LABELS, SALT_FREEZE_GUIDANCE, } from "./session-salt-agreement.js";
4
+ import { UNSALTED_REASONS, UNSALTED_GUIDANCE, SALT_AGREEMENT_WAIT_MS, } from "./session-node-types.js";
5
+ import { contentHashFor, CONTENT_HASH_ALGS } from "./wire-content-hash.js";
6
+ import { CONTENT_ENCRYPTION_REASONS, } from "./content-encryption-status.js";
7
+ export class SessionSalts {
8
+ #ctx;
9
+ constructor(ctx) {
10
+ this.#ctx = ctx;
11
+ }
12
+ /** A getter so the moved queries still read `this.#db` and narrow exactly as they did. */
13
+ get #db() {
14
+ return this.#ctx.db();
15
+ }
16
+ /**
17
+ * DOD-M15-SEALWIRE-1 bullet 6 (part A) — the salt agreement's two pieces of per-session state.
18
+ *
19
+ * `#saltContributions` — OUR random half, MINTED ONCE PER SESSION. This being a map rather than a
20
+ * fresh call at each send is the whole correctness of the exchange: we re-announce on every
21
+ * counterparty connect, and a contribution regenerated per reconnect would have both sides
22
+ * deriving against a moving value with the fingerprints never settling — a session that
23
+ * reconnects and still disagrees, which reads as a network fault rather than a bug here.
24
+ *
25
+ * `#sessionSalts` — a CACHE over `sessions.content_salt`, which is the durable copy. Both are
26
+ * cleared by `#evictSessionCaches`: the contribution is worthless once a salt exists, and the
27
+ * salt is re-read from the row on revival, which is exactly what Decision #8 persists it for.
28
+ */
29
+ #saltContributions = new Map();
30
+ /**
31
+ * ─── B2b-2 state: what the SEND path needs that the row cannot answer ─────────────────────────
32
+ *
33
+ * `#saltPending` — an agreement that is outstanding. The first send waits on it (constraint 2).
34
+ *
35
+ * ⚠️ THIS SAID "an agreement that has ACTUALLY GONE OUT", and constraint 5 read that a park-only
36
+ * session never starts one at all. DOD-M15-AWAYSALT-1 amended both: an away ack hashes BEFORE the
37
+ * peer can attach, so it arms one SPECULATIVELY with no frame sent — otherwise it hashes unsalted,
38
+ * closes adoption, and the session can never seal. Corrected rather than deleted, because the
39
+ * constraint underneath still holds and is load-bearing: a park-only session must not be made to
40
+ * WAIT the full bound for a peer that will never answer. What carries it now is the `announced`
41
+ * flag — a speculative arm that times out reports `no_agreement_started` (nobody was connected),
42
+ * never `agreement_timed_out` (they ignored us), and stays silent.
43
+ *
44
+ * `#hashedWithoutSalt` — this session has already computed an unsalted content hash. Decision #8
45
+ * closes adoption at the moment content is HASHED, and for a session's first message that is a
46
+ * full network round trip before any leaf, held row or in-flight entry exists. Without this flag
47
+ * the frontier count reads empty for exactly the window in which adopting would split the
48
+ * transcript.
49
+ *
50
+ * `#unsaltedAnnounced` — the fallback has been stated for this session. Decision #15 says once per
51
+ * session; a per-message warning is a filter waiting to be written.
52
+ *
53
+ * All three are per-session and in-memory by design, and are dropped with the rest of a session's
54
+ * caches on eviction — a revived session re-reads its salt from the row, re-derives its frontier
55
+ * from durable state, and starts a fresh agreement if it reconnects.
56
+ */
57
+ #saltPending = new Map();
58
+ /**
59
+ * HOW THE LAST AGREEMENT ENDED, kept after `#saltPending` is cleared.
60
+ *
61
+ * ⚠️ FOUND BY FALSIFYING MY OWN FIX. `#settleSaltPending` deletes the pending entry, so a send that
62
+ * arrives AFTER an agreement has already failed finds nothing pending and is told
63
+ * `no_agreement_started` — *"your counterparty was not connected"* — when in fact they were
64
+ * connected and our own dial to them failed. The outcome was observable only to a send that
65
+ * happened to already be waiting, which is the minority case.
66
+ *
67
+ * So the verdict outlives the wait. An ABSENT entry still means what it always meant — no
68
+ * agreement was ever started, the park-only case — and that distinction is the whole reason this
69
+ * is a separate map rather than a default.
70
+ */
71
+ #saltLastOutcome = new Map();
72
+ #unsaltedAnnounced = new Set();
73
+ #sessionSalts = new Map();
74
+ /**
75
+ * The peer half we last answered with a repair, hex — review F14, and it is what makes the repair
76
+ * TERMINATE. Without it, two daemons that already hold the same salt trade contributions forever
77
+ * once a reconnect leaves a stale copy queued on each side. See `onPeerSaltFrame`'s
78
+ * `alreadyRepairedAgainstPeerHalf`.
79
+ */
80
+ #saltRepairedAgainst = new Map();
81
+ /**
82
+ * THE MIRROR OF THE ABOVE — the peer FINGERPRINT we last answered with our half, hex.
83
+ *
84
+ * 006-CRYPTO finding 1. `#saltRepairedAgainst` terminates the salt-HOLDER's direction only. A side
85
+ * holding no salt answered every fingerprint with its contribution, and a latched holder answers
86
+ * every contribution with its fingerprint — so after one failed persist plus a reconnect, two
87
+ * healthy daemons repair at each other for the life of the session, one new stream and one INFO
88
+ * line each per round trip. Keyed on the peer's fingerprint BYTES for the same reason the other
89
+ * map is keyed on its half: a genuinely NEW fingerprint is new information and must still be
90
+ * answered; only an identical re-offer is the loop.
91
+ */
92
+ #saltRepairedAgainstFingerprint = new Map();
93
+ /**
94
+ * `#saltSuspended` — the peer has told us it can never hold a salt, so ours must not be USED. The
95
+ * bytes stay on disk (`DOD-M15-SALTSPLIT-1`, the other lane's authorization argument).
96
+ *
97
+ * ⚠️ THIS REPLACED AN IMMEDIATE, IRREVERSIBLE ERASE, AND THE REFRAMING IS THE WHOLE POINT.
98
+ *
99
+ * I defended the erase as a compatibility question — a legacy peer might send the misleading frame,
100
+ * we are pre-launch, do not carry weight for a state nobody is in. All true, and it does not reach
101
+ * the question. **It is an AUTHORIZATION question:** the receiver performed an irreversible
102
+ * destruction of durable key material on a peer's bare assertion with nothing to check it against.
103
+ * Re-derived against an empty database — *would I let one side erase the other's key material on an
104
+ * unauthenticated claim carrying no evidence?* No. My own empty-database rule argued FOR a guard,
105
+ * not against one.
106
+ *
107
+ * And my own trigger was the proof I walked past: `frontier_unreadable` is not a legacy peer, it is
108
+ * a **healthy current peer having one bad second**. Fixing the producer made our side stop emitting
109
+ * it wrongly and left the receiver built to obey it — *one side of that exchange correct by
110
+ * construction, the other still correct by luck.*
111
+ *
112
+ * A salt that cannot be used is inert. The destruction is what turned a transient disagreement into
113
+ * a permanent one, so **nothing irreversible hangs on the claim any more** and proving the claim
114
+ * stops being load-bearing.
115
+ *
116
+ * ⚠️ IN MEMORY ON PURPOSE, AND THE ERASE IS DEFERRED RATHER THAN CANCELLED. A durable mark needs a
117
+ * column, and this milestone has lost data twice in the rebuild DDL. In-memory alone would split
118
+ * the transcript at the next restart — unsalted now, salted after a reboot — so the salt IS erased,
119
+ * at the first unsalted hash, which is the moment erasing becomes both harmless (nothing was hashed
120
+ * under it) and REQUIRED (keeping it would re-salt after a restart). Before that moment a corrected
121
+ * announce carrying a matching fingerprint un-suspends and the session recovers fully salted, which
122
+ * erasure makes impossible even in principle: the far side cannot re-derive without both halves.
123
+ *
124
+ * A restart before either outcome loses the mark, we are salted again, the peer refuses one message,
125
+ * and the announce re-runs and re-suspends. **One refused message, then convergence** — against a
126
+ * dead session.
127
+ */
128
+ #saltSuspended = new Set();
129
+ #hashedWithoutSalt = new Map();
130
+ /**
131
+ * `#hashedWithSalt` — how many content hashes this session has computed UNDER its salt and not yet
132
+ * landed anywhere a count can see (`DOD-M15-SALTSPLIT-1`, review HIGH-2).
133
+ *
134
+ * The mirror of `#hashedWithoutSalt`, and it exists for the same window: a hash is computed, then a
135
+ * relay round trip happens, and only afterwards does the message appear as a leaf, a hold or an
136
+ * awaiting-ack entry. In between, every count reads zero.
137
+ *
138
+ * It is read by `#discardUnspentSalt` alone. "Unspent" must mean *nothing has been hashed under
139
+ * it*, and without this the answer is *nothing has FINISHED being hashed under it* — which is the
140
+ * question nobody asked, answered destructively.
141
+ *
142
+ * Never decremented on success: a salted hash that reaches the wire is spent forever, and unlike
143
+ * the unsalted counter there is no `abandonUnsaltedHash` equivalent to undo. It is cleared only
144
+ * with the rest of the session's caches. **For a discard decision, erring toward "spent" is the
145
+ * safe direction** — a salt kept is recoverable, a salt erased is not.
146
+ */
147
+ #hashedWithSalt = new Map();
148
+ /**
149
+ * THE LABEL THE PEER GAVE when it closed adoption — 006-CRYPTO finding 2.
150
+ *
151
+ * The wire carries WHY, `session-salt-agreement.ts` makes it a union so a caller cannot close
152
+ * without saying why, and the agreement's `detail` puts it in the log. It was going no further:
153
+ * `#settleSaltPending(..., "closed")` recorded only that it was closed, so every one of the four
154
+ * reasons arrived at the operator as "they had already hashed messages".
155
+ *
156
+ * Stored raw and rendered through `#peerClosedReason`, which maps anything outside the known set
157
+ * to a non-asserting reason — the peer chooses these bytes.
158
+ */
159
+ #saltPeerClosedLabel = new Map();
160
+ /**
161
+ * cello_list_sessions: every persisted session for one agent, regardless of
162
+ * status (active, interrupted, sealed, seal_interrupted_pending). Ordered most
163
+ * recently updated first so the live session surfaces at the top. This is the
164
+ * discovery surface that the by-id reads (cello_get_transcript /
165
+ * cello_get_sealed_receipt) depend on — without it an agent has no way to learn
166
+ * its own session ids after a restart or from a fresh MCP connection.
167
+ */
168
+ /**
169
+ * DOD-M15-REFUSED-INBOUND-SILENT-1, the DECLINED PROTECTION half — a FIELD, not an alert.
170
+ *
171
+ * An unsalted session is exactly as verifiable as every session shipped before salting existed,
172
+ * so there is nothing to interrupt the operator with and no event to fire. What was missing is
173
+ * STATE: nothing let anyone tell *"unsalted because this build predates the feature"* from
174
+ * *"unsalted because adoption was refused"* — and only the second says something about their
175
+ * setup. The session's own status now answers it, which costs nothing per message and cannot
176
+ * become a flood.
177
+ *
178
+ * The raw salt is dropped on the way out rather than passed through. `SELECT *` was handing the
179
+ * BLOB to a listing surface that has no use for it; the boolean is the whole question a reader of
180
+ * this list is asking, and shipping key material to answer a yes/no is not a trade worth making.
181
+ */
182
+ /**
183
+ * ⚠️ THE STORED COLUMN IS NOT THE ANSWER ON ITS OWN — 006-CRYPTO finding 3.
184
+ *
185
+ * A SUSPENDED salt keeps its bytes on disk deliberately (`DOD-M15-SALTSPLIT-1`: a salt kept is
186
+ * recoverable, a salt erased is not), while `#saltForHashing` returns null for it and every
187
+ * message goes out `sha256`. Reading the column alone therefore reported `true` at the exact
188
+ * moment the session had STOPPED salting — and because the field is emitted only when `false`,
189
+ * the agent saw nothing at all, which reads as "not unsalted".
190
+ *
191
+ * That is precisely the case this field was added for. Its own note above says it exists to tell
192
+ * *"unsalted because this build predates the feature"* from *"unsalted because adoption was
193
+ * refused"*, and the refused case was the one it could not report.
194
+ */
195
+ saltStatusOf(row, agentName) {
196
+ const { content_salt, ...rest } = row;
197
+ const stored = content_salt != null && content_salt.length > 0;
198
+ const suspended = agentName !== null && this.#saltSuspended.has(this.#ctx.sessionKey(agentName, String(row.session_id)));
199
+ /**
200
+ * 007-CRYPTO: the REAL state, not a constant. In 006 this was hardcoded `false` with a single
201
+ * reason, because nothing exchanged keys and saying so was the honest answer. It exchanges now,
202
+ * so a hardcode would be the stale claim that unit existed to remove.
203
+ *
204
+ * A row with no live agent name cannot be looked up in memory — an orphaned session, whose key
205
+ * (if it ever had one) died with the process. Reported as not-agreed rather than guessed at.
206
+ */
207
+ const enc = agentName === null
208
+ ? { key: null, reason: CONTENT_ENCRYPTION_REASONS.NOT_YET_AGREED }
209
+ : this.#ctx.contentEncryptionState(agentName, String(row.session_id));
210
+ return {
211
+ ...rest,
212
+ content_hashes_salted: stored && !suspended,
213
+ content_encrypted: enc.key !== null,
214
+ ...(enc.key === null ? { content_encryption_reason: enc.reason } : {}),
215
+ };
216
+ }
217
+ saltContributionFor(agentName, sessionId) {
218
+ const key = this.#ctx.sessionKey(agentName, sessionId);
219
+ let contribution = this.#saltContributions.get(key);
220
+ if (!contribution) {
221
+ contribution = generateSaltContribution();
222
+ this.#saltContributions.set(key, contribution);
223
+ }
224
+ return contribution;
225
+ }
226
+ /**
227
+ * Our half for a session, **without minting one** — review F1, and the distinction is the whole
228
+ * safety of the repair.
229
+ *
230
+ * A session that already holds a salt must never mint a fresh half. If it did, the repair would
231
+ * offer the peer a half the stored salt was NOT derived from, they would compute a different salt,
232
+ * and both sides would believe they had agreed — silently, which is the one outcome worse than
233
+ * refusing. So `null` from here means exactly "we hold a salt and the half behind it is gone",
234
+ * and that is the only state the agreement is allowed to call unrepairable.
235
+ */
236
+ ownSaltHalf(agentName, sessionId) {
237
+ return this.#saltContributions.get(this.#ctx.sessionKey(agentName, sessionId)) ?? null;
238
+ }
239
+ /**
240
+ * Test seam: force this session's own salt half, so the LOCAL-defect path is reachable.
241
+ *
242
+ * `generateSaltContribution` cannot produce a degenerate half, which is the point of it — so the
243
+ * only way to exercise "our own random source is broken" end-to-end is to stand in for the broken
244
+ * source. Named `…ForTest` like every other seam, and it writes the same map production writes
245
+ * rather than a parallel one, so a test cannot pass against state the daemon never reads.
246
+ *
247
+ * ⚠️ This block stayed in `session-node-manager.ts` when the method moved here, and ended up
248
+ * stacked on an unrelated test seam — the fourth found doing that. `#saltContributions` above is
249
+ * "the same map production writes"; the sentence was true where it was written and unverifiable
250
+ * where it landed.
251
+ */
252
+ setSaltContributionForTest(agentName, sessionId, contribution) {
253
+ this.#saltContributions.set(this.#ctx.sessionKey(agentName, sessionId), contribution);
254
+ }
255
+ /**
256
+ * Test seam: drop this session's own half while leaving the stored salt in place — the state every
257
+ * teardown produces, because `#evictSessionCaches` clears the map and the row survives.
258
+ *
259
+ * It clears the SAME map the eviction clears rather than a stand-in, so a test cannot pass against
260
+ * a state the daemon never reaches. Reproducing it through a real teardown/revive would also drag
261
+ * in node rebuild and relay reconnection, none of which this is about.
262
+ */
263
+ forgetSaltContributionForTest(agentName, sessionId) {
264
+ this.#saltContributions.delete(this.#ctx.sessionKey(agentName, sessionId));
265
+ }
266
+ /**
267
+ * THE ONE PLACE THAT DECIDES HOW A SESSION'S OUTBOUND CONTENT IS HASHED —
268
+ * `DOD-M15-SEALWIRE-1` part B2b.
269
+ *
270
+ * Returns the hash AND the algorithm that produced it, together, because the two must not be
271
+ * decided separately. `wire-content-hash.ts` exists for exactly this reason and says so in its own
272
+ * header: the expression was written out at five call sites, the two added last got it wrong, and
273
+ * the failure was invisible — *"the send succeeds, `parked: false`, the sender's log says the frame
274
+ * left, and the receiver discards it at the authenticity check."* It took two real daemons.
275
+ *
276
+ * There are FOUR outbound sites (`session-content-handlers.ts`, two in `daemon.ts`,
277
+ * `document-delivery-transport.ts`). Once salting is switchable, each of them independently
278
+ * deciding whether to salt is that defect again with a worse failure mode — a message hashed one
279
+ * way and LABELLED another is refused by every peer, including a correct one.
280
+ *
281
+ * ⚠️ ASYNC, AND THAT IS THE POINT — B2b-2 constraint 2, not an implementation detail.
282
+ *
283
+ * The agreement is in flight while the operator composes their first message. Hash without waiting
284
+ * and it comes out unsalted, and that first unsalted hash closes adoption for the LIFE of the
285
+ * session (Decision #8, unit 1). Every session would fall back permanently while every log line
286
+ * about it stayed true — the feature present, wired, tested, and never once reached.
287
+ *
288
+ * The wait lives HERE rather than at the four call sites for the same reason `contentHashAlg` is a
289
+ * required parameter rather than a defaulted one: a site that forgets it must fail to compile. A
290
+ * caller that drops the `await` gets a `Promise` where bytes belong, which is a typecheck error;
291
+ * a caller that forgot to call a separate `awaitSaltSettled()` would silently send unsalted.
292
+ */
293
+ async contentHashForSession(agentName, sessionId, content) {
294
+ const { salt, reason } = await this.saltForHashing(agentName, sessionId);
295
+ if (salt !== null) {
296
+ /**
297
+ * ⚠️ THE SALTED HASH MARKS ITSELF SPENT — `DOD-M15-SALTSPLIT-1` review pass 1, HIGH-2.
298
+ *
299
+ * The unsalted branch below has counted itself since review pass 2 F1, for a reason stated
300
+ * there in full: between hashing and `#trackAwaitingAck` there is a relay round trip, and in
301
+ * that window leaves, held content and awaiting-ack ALL read zero. **The salted direction was
302
+ * left with no counterpart**, which was harmless while nothing acted on the answer — and
303
+ * `#discardUnspentSalt` is the first code that acts on it destructively.
304
+ *
305
+ * Without this, a peer's `adoption_closed` frame arriving inside that window finds adoption
306
+ * "open", discards the salt, and the message already on the wire carries
307
+ * `content_hash_alg: hmac-salt-v1` with a hash **nobody — including this daemon — can ever
308
+ * recompute**. The alg is copied verbatim into the parked envelope on TTF expiry, so it
309
+ * survives the round trip that would otherwise have hidden it.
310
+ *
311
+ * A COUNT, not a bit, for the same reason the unsalted side is a count: two connections can be
312
+ * mid-send at once, and one finishing must not clear the claim the other is still relying on.
313
+ */
314
+ const key = this.#ctx.sessionKey(agentName, sessionId);
315
+ this.#hashedWithSalt.set(key, (this.#hashedWithSalt.get(key) ?? 0) + 1);
316
+ const alg = CONTENT_HASH_ALGS.HMAC_SALT_V1;
317
+ return { hash: contentHashFor(content, { alg, salt }), alg };
318
+ }
319
+ /**
320
+ * ⚠️ MARKED BEFORE THE HASH IS RETURNED, and this closes a window the row cannot see.
321
+ *
322
+ * `#saltAdoptionClosed` counts leaves, held content and in-flight sends. For the FIRST message of
323
+ * a session none of the three exists at this moment — the leaf lands after `sendContent` returns,
324
+ * which is a network round trip later. A peer contribution arriving in that gap would be adopted,
325
+ * and the message already on the wire would become the single unsalted leaf in an otherwise
326
+ * salted transcript: the exact split Decision #8 forbids, reached by the one route every count
327
+ * reads as empty.
328
+ *
329
+ * In memory rather than in a column, and that is sufficient rather than convenient: if this
330
+ * process survives, the flag holds; if it does not, the message it protects either reached a
331
+ * durable form (leaf, held row, queued row — all of which the counts see) or never left, in which
332
+ * case there is nothing to split. The one remaining case — hashed, sent, and no local record —
333
+ * is covered from the other side, because the peer DID leaf it and closes its own adoption, and
334
+ * the wire state added in unit 1 tells us so.
335
+ */
336
+ /**
337
+ * ⚠️ NOT FOR A TORN-DOWN SESSION — review Finding 5. `#evictSessionCaches` settles the wait and
338
+ * clears both of these sets; a `.add()` afterwards re-populates a map whose eviction has already
339
+ * run, and the entries then outlive the session they describe. There is also nothing to protect:
340
+ * a session that no longer exists cannot adopt a salt or split a transcript.
341
+ */
342
+ /**
343
+ * ⚠️ THE DEFERRED ERASE — `DOD-M15-SALTSPLIT-1`. This is the moment a suspended salt becomes both
344
+ * harmless to erase and NECESSARY to erase, and it must run BEFORE the count below.
345
+ *
346
+ * Harmless: this session has hashed nothing under the salt, which is what let it be suspended.
347
+ * Necessary: we are about to hash unsalted, and a salt left on disk reads back fine after a
348
+ * restart — so the next process would hash salted and the transcript would be split down the
349
+ * middle by a reboot rather than by any frame.
350
+ *
351
+ * **Before the `#hashedWithoutSalt` increment on purpose.** `#discardUnspentSalt` refuses to erase
352
+ * once adoption is closed, and that counter is one of the things that closes it — increment first
353
+ * and the erase we just decided is correct gets refused by our own guard, leaving exactly the
354
+ * split this ordering exists to prevent.
355
+ */
356
+ if (reason !== UNSALTED_REASONS.SESSION_TORN_DOWN && this.#saltSuspended.has(this.#ctx.sessionKey(agentName, sessionId))) {
357
+ /**
358
+ * ⚠️ GOING UNSALTED AND ERASING THE SALT ARE ONE DECISION — pass 2, F2 (HIGH), and this is my
359
+ * regression, not a pre-existing one.
360
+ *
361
+ * The note above claimed the ordering was sufficient because `#hashedWithoutSalt` is what
362
+ * closes adoption. **It is one of FOUR contributors.** Leaves, held rows and awaiting-ack close
363
+ * it too — and the most ordinary event in the protocol closes it: *the peer sends us its next
364
+ * message.* Reproduced through the real inbound path: suspend, peer's message lands as leaf 0,
365
+ * we hash `sha256`, and the erase is REFUSED with `already_hashing` while the bytes stay on
366
+ * disk. One teardown-and-revive later — no process restart required — we hash `hmac` again.
367
+ * That is the split transcript, produced by the fix for the split transcript.
368
+ *
369
+ * Worth naming precisely: **the immediate-erase design this replaced could NOT produce it.**
370
+ * There, a refused discard simply kept the session salted — one rule throughout, and loud.
371
+ * Suspension is what made "unsalted now, salted later" reachable. Same shape as pass 1: the fix
372
+ * worse than the defect on one path.
373
+ *
374
+ * So the two are atomic. If the salt cannot be erased, we do **not** go unsalted — we keep
375
+ * hashing under the held salt, which is one rule for the whole session, and say so at ERROR.
376
+ * The counterparty may refuse those messages, and that is the honest failure: a dead session
377
+ * beats a transcript no single rule can verify. The durable column remains the real answer.
378
+ */
379
+ if (!this.discardUnspentSalt(agentName, sessionId)) {
380
+ const stillHeld = this.getSessionSalt(agentName, sessionId);
381
+ if (stillHeld !== null) {
382
+ const key = this.#ctx.sessionKey(agentName, sessionId);
383
+ this.#ctx.logger.error("session.salt.split", {
384
+ agentName, sessionId, reason: "suspended_but_unerasable",
385
+ impact: "this session stays SALTED even though the counterparty says it can never hold a salt, because the salt could not be erased and hashing unsalted now would leave half this transcript under each rule — verifiable by nobody. Expect the counterparty to refuse messages sent from here.",
386
+ guidance: "Start a new session with this counterparty: the salt agreement runs at open, before anything is hashed. This one cannot be repaired — look for session.salt.discard.refused immediately above for why the salt could not be released.",
387
+ });
388
+ this.#hashedWithSalt.set(key, (this.#hashedWithSalt.get(key) ?? 0) + 1);
389
+ const alg = CONTENT_HASH_ALGS.HMAC_SALT_V1;
390
+ return { hash: contentHashFor(content, { alg, salt: stillHeld }), alg };
391
+ }
392
+ }
393
+ }
394
+ if (reason !== UNSALTED_REASONS.SESSION_TORN_DOWN) {
395
+ /**
396
+ * ⚠️ A COUNT, NOT A BIT — review pass 2, F1 (HIGH). It was a `Set`, and that made it ONE FLAG
397
+ * PER SESSION for a fact that is per MESSAGE.
398
+ *
399
+ * The `sibling_send_in_flight` refusal path exists precisely when another connection is
400
+ * mid-send with an unsalted hash it computed itself — and `sendContent` awaits a full relay
401
+ * round trip before `#trackAwaitingAck` records anything. So: connection A hashes and sets the
402
+ * flag; A enters that round trip, visible in no count; connection B hashes, sees A's claim,
403
+ * refuses, and calls `abandonUnsaltedHash` — **deleting the flag A is still relying on.** The
404
+ * frontier then reads entirely empty, a salt frame arriving in that window is adopted, and A's
405
+ * message lands as leaf 0 hashed sha256 in a session that hashes everything after it under
406
+ * HMAC.
407
+ *
408
+ * That is the split transcript this unit exists to prevent, through a window a relay round
409
+ * trip wide. A count makes each in-flight hash hold its own claim.
410
+ */
411
+ const key = this.#ctx.sessionKey(agentName, sessionId);
412
+ this.#hashedWithoutSalt.set(key, (this.#hashedWithoutSalt.get(key) ?? 0) + 1);
413
+ }
414
+ /**
415
+ * NO `??` DEFAULT — review pass 2, F6. It read `reason ?? ADOPTION_CLOSED_LOCALLY`, which is the
416
+ * shape the closed set was built to eliminate: a seventh return path forgetting its reason would
417
+ * have been silently labelled *"you already hashed"* and inherited guidance about a frontier that
418
+ * never moved. `#saltForHashing` returns a discriminated union now, so a null salt without a
419
+ * reason does not compile.
420
+ */
421
+ this.announceUnsaltedOnce(agentName, sessionId, reason);
422
+ const alg = CONTENT_HASH_ALGS.SHA256;
423
+ return { hash: contentHashFor(content, { alg, salt: null }), alg };
424
+ }
425
+ /**
426
+ * The salt to hash this session's next message under, waiting for a pending agreement if one is
427
+ * genuinely in flight — B2b-2 constraints 2 and 5.
428
+ *
429
+ * Three exits, and the order matters:
430
+ *
431
+ * 1. We already hold one. No wait, ever.
432
+ * 2. Adoption is closed — this session has hashed or leafed something already, so a salt could
433
+ * never be adopted now even if one arrived. Waiting would be waiting for a value we would
434
+ * then have to refuse.
435
+ * 3. Nothing is pending. **The park-only case (constraint 5)**: the announcement hangs off
436
+ * `onPeerConnect`, an offline counterparty never connects, so no agreement was ever started.
437
+ * (An away ack now ARMS one speculatively — see `markSaltPending` — so it reaches exit 4 and
438
+ * settles `no_agreement_started` on timeout, which is this same answer by a slower road.) Waiting the full bound there pauses every message to an offline peer and falls
439
+ * back anyway — a stall bought for nothing.
440
+ *
441
+ * Only a session with an agreement actually in flight waits, and only until it settles or the
442
+ * bound expires.
443
+ */
444
+ async saltForHashing(agentName, sessionId) {
445
+ const key = this.#ctx.sessionKey(agentName, sessionId);
446
+ const held = this.getSessionSalt(agentName, sessionId);
447
+ if (held !== null) {
448
+ /**
449
+ * SUSPENDED BEATS HELD — `DOD-M15-SALTSPLIT-1`. The peer has said it can never hold a salt, so
450
+ * hashing under ours produces a message it must refuse. We hold one and deliberately do not
451
+ * use it.
452
+ *
453
+ * ⚠️ An earlier note here said `PEER_CLOSED_ADOPTION` "already carries exactly the right
454
+ * guidance, so no new reason is needed and none is invented." That was right about not
455
+ * inventing a reason and wrong about which one applies: the peer can suspend us for any of
456
+ * four reasons, and the one hardcoded here asserted the most flattering of them. It now asks
457
+ * the same mapping every other closed path asks (006-CRYPTO finding 2).
458
+ */
459
+ if (this.#saltSuspended.has(key)) {
460
+ return { salt: null, reason: this.peerClosedReason(key) };
461
+ }
462
+ return { salt: held };
463
+ }
464
+ if (this.saltAdoptionClosed(agentName, sessionId).closed) {
465
+ return { salt: null, reason: UNSALTED_REASONS.ADOPTION_CLOSED_LOCALLY };
466
+ }
467
+ const pending = this.#saltPending.get(key);
468
+ if (pending === undefined) {
469
+ // An agreement that already ENDED is not an agreement that never started. Only the second is
470
+ // "your counterparty was not connected", and only an absent entry means it.
471
+ const last = this.#saltLastOutcome.get(key);
472
+ if (last !== undefined)
473
+ return { salt: null, reason: this.reasonForOutcome(key, last) };
474
+ return { salt: null, reason: UNSALTED_REASONS.NO_AGREEMENT_STARTED };
475
+ }
476
+ const settled = await pending.settled;
477
+ if (settled === "agreed") {
478
+ const agreed = this.getSessionSalt(agentName, sessionId);
479
+ /**
480
+ * A settled-`agreed` that reads back NULL is a READ failure, not a persist failure — pass 2,
481
+ * F4. `persist_failed` has its own outcome now, so the only way to arrive here empty is
482
+ * `#getSessionSalt` returning null after the salt was stored: a throwing read, or a
483
+ * wrong-width row, with the cache evicted in the microtask between settle and resume. Rare —
484
+ * and labelling it `our_persist_failed` sent the operator to look for a
485
+ * `session.salt.persist.failed` line that will not be there.
486
+ */
487
+ return agreed !== null
488
+ ? { salt: agreed }
489
+ : { salt: null, reason: UNSALTED_REASONS.OUR_READ_FAILED };
490
+ }
491
+ if (settled === "announce_failed") {
492
+ return { salt: null, reason: UNSALTED_REASONS.ANNOUNCE_FAILED };
493
+ }
494
+ if (settled === "persist_failed") {
495
+ // Named separately from the timeout on purpose: the peer answered in time and OUR write
496
+ // failed, so nothing about their build is involved and sending the operator there wastes them.
497
+ return { salt: null, reason: UNSALTED_REASONS.OUR_PERSIST_FAILED };
498
+ }
499
+ if (settled === "closed") {
500
+ /**
501
+ * Two very different things reach `closed`, and only one of them is about the counterparty.
502
+ *
503
+ * `#handleSaltFrame`'s terminal branch — the peer told us it cannot adopt — is a settled
504
+ * bilateral outcome and the session is fine. `#evictSessionCaches` — this session is being
505
+ * torn down underneath us — is not: there is no session left to be unsalted, and a caller that
506
+ * marks `#hashedWithoutSalt` for it re-populates a map whose eviction has already run
507
+ * (review Finding 5). `#saltPending` is gone by the time we look, so the live node is what
508
+ * distinguishes them.
509
+ */
510
+ return {
511
+ salt: null,
512
+ reason: this.#ctx.activeEntry(key) !== undefined
513
+ ? UNSALTED_REASONS.PEER_CLOSED_ADOPTION
514
+ : UNSALTED_REASONS.SESSION_TORN_DOWN,
515
+ };
516
+ }
517
+ if (settled === "timeout" && !pending.announced) {
518
+ /**
519
+ * DOD-M15-AWAYSALT-1 review HIGH-2 — NOTHING WAS EVER ANNOUNCED, so this is not a counterparty
520
+ * who failed to answer. It is the park-only case: the operator is leaving a message for someone
521
+ * who is offline, which is the DESIGNED benign path. Reporting `agreement_timed_out` here
522
+ * substitutes a diagnosis — its guidance sends the operator to ask a perfectly up-to-date
523
+ * counterparty to upgrade — and, because it fires on the normal case, buries the one occurrence
524
+ * that means something. Silent for the same reason: `session.content.unsalted` already says it.
525
+ */
526
+ return { salt: null, reason: UNSALTED_REASONS.NO_AGREEMENT_STARTED };
527
+ }
528
+ if (settled === "timeout") {
529
+ /**
530
+ * A DECISION, NOT A RETRY. Logged once, here, because this is the moment the session became
531
+ * permanently unsalted — and an operator reading a later `session.content.unsalted` needs to
532
+ * be able to find out WHY this session has no salt when their others do.
533
+ */
534
+ this.#ctx.logger.warn("session.salt.agreement.timeout", {
535
+ agentName, sessionId, waitedMs: pending.boundMs,
536
+ impact: "the counterparty did not answer the salt agreement in time, so this session is unsalted FOR ITS LIFE — the message is being sent now rather than held any longer. Nothing is lost and nothing is degraded relative to any shipped release.",
537
+ // Review F4: `session.salt.persist.failed` reaches this same timeout by a completely
538
+ // different route — the peer answered promptly and OUR OWN write failed, so we returned
539
+ // before announcing and nothing came back. Omitting it sent that operator to ask their
540
+ // counterparty about a version mismatch that was never involved.
541
+ guidance: "Most often the counterparty is on a build that predates the salt agreement, in which case this is expected and permanent for this session — a newer one will agree normally. If you know they are on the same version, look for session.salt.persist.failed on THIS side first (our own write failing produces this same timeout), then session.salt.announce.failed on either side.",
542
+ });
543
+ return { salt: null, reason: UNSALTED_REASONS.AGREEMENT_TIMED_OUT };
544
+ }
545
+ return { salt: null, reason: UNSALTED_REASONS.AGREEMENT_TIMED_OUT };
546
+ }
547
+ /**
548
+ * Decision #15's fallback announcement — ONCE per session, never per message.
549
+ *
550
+ * A warning that fires on every message of every unsalted session is not a signal, it is a reason
551
+ * to build a filter; and the operator who filters it also filters the one session where it meant
552
+ * something. Stated once, with what the session actually loses.
553
+ */
554
+ announceUnsaltedOnce(agentName, sessionId, reason) {
555
+ const key = this.#ctx.sessionKey(agentName, sessionId);
556
+ if (this.#unsaltedAnnounced.has(key))
557
+ return;
558
+ this.#unsaltedAnnounced.add(key);
559
+ this.#ctx.logger.info("session.content.unsalted", {
560
+ agentName, sessionId,
561
+ // The REASON is the field that makes this line diagnosable, and it was the missing one. The
562
+ // impact is the same for all six; what to do about it is not.
563
+ reason,
564
+ impact: "this session hashes its messages the way every build before this feature did. Nothing is degraded relative to any shipped release and no message is affected — it only means a relay holding the hashes could confirm a guess at a short message in THIS conversation, which a salt would have prevented.",
565
+ guidance: UNSALTED_GUIDANCE[reason],
566
+ });
567
+ }
568
+ /**
569
+ * Register that a salt agreement is IN FLIGHT for this session, so the first send waits for it.
570
+ *
571
+ * Called where we announce our own state — not at session creation. That distinction is
572
+ * constraint 5: an agreement exists to be waited for only once a frame has actually gone out.
573
+ */
574
+ /**
575
+ * ─── DOD-M15-AWAYSALT-1: WHO IS ALLOWED TO SAY "AN AGREEMENT IS COMING" ────────────────────────
576
+ *
577
+ * `saltForHashing` waits for an agreement ONLY when one is marked pending here. For a long time the
578
+ * only production caller was `#sendSaltFrame`, which fires on peer ATTACH — and that is later than
579
+ * the inbound session REQUEST which triggers an away auto-reply. So the away ack hashed with
580
+ * nothing pending, took `NO_AGREEMENT_STARTED`, and hashed UNSALTED.
581
+ *
582
+ * One unsalted leaf closes salt adoption for the entire session (`already_hashing`), permanently.
583
+ * The initiator, meanwhile, derives a salt, cannot erase it (`session.salt.split`,
584
+ * `suspended_but_unerasable`) and keeps labelling content `hmac-sha256-salt-v1` — which this side
585
+ * can then only refuse. Every message from the initiator is quarantined
586
+ * (`content_hash_salt_unavailable`), and the session can never seal.
587
+ *
588
+ * Measured 2026-09-07 on session 436c92f4, on the DEFAULT path (initiate to an unattended agent):
589
+ * the ack hashed at 22:48:03.752, the peer announced at 22:48:04.561 — 0.8s late — and the
590
+ * operator's first real message was quarantined. Nothing recovers it; the guidance on
591
+ * `session.salt.split` says to start a new session, and a new session repeats the race.
592
+ *
593
+ * So `expectSaltAgreement` on the manager now calls this BEFORE any early hash. It is idempotent
594
+ * (a second call is a no-op while one is outstanding) and bounded by the same `boundMs` timer as
595
+ * every other pending agreement, so a counterparty that never announces settles `timeout` and the
596
+ * content hashes unsalted exactly as it does today — the wait is a delay, never a failure.
597
+ *
598
+ * ⚠️ STILL OPEN: this covers the away reply, which is the only content this daemon emits before a
599
+ * peer can attach. Any FUTURE caller that hashes that early needs the same line, and nothing here
600
+ * enforces that.
601
+ */
602
+ markSaltPending(agentName, sessionId, boundMs = SALT_AGREEMENT_WAIT_MS, announced = false) {
603
+ const key = this.#ctx.sessionKey(agentName, sessionId);
604
+ const outstanding = this.#saltPending.get(key);
605
+ if (outstanding !== undefined) {
606
+ /**
607
+ * ⚠️ AN OUTSTANDING ENTRY IS UPGRADED, NEVER IGNORED — DOD-M15-AWAYSALT-1 review HIGH-1.
608
+ *
609
+ * This used to early-return. Once a speculative arm existed, the REAL announce that followed
610
+ * was a no-op — so the agreement inherited the speculative bound instead of its own, and a
611
+ * healthy-but-slow peer could time out inside it. That reproduces the exact defect this unit
612
+ * removes: unsalted ack, adoption closed, every later message quarantined, session unsealable.
613
+ *
614
+ * A bound may only GROW, and `announced` may only go true. Both are the safe direction: the
615
+ * cost of waiting longer is a delayed machine acknowledgement, the cost of waiting less is a
616
+ * session that can never seal.
617
+ */
618
+ if (announced && !outstanding.announced)
619
+ outstanding.announced = true;
620
+ if (boundMs > outstanding.boundMs) {
621
+ clearTimeout(outstanding.timer);
622
+ outstanding.boundMs = boundMs;
623
+ outstanding.timer = setTimeout(() => this.settleSaltPending(agentName, sessionId, "timeout"), boundMs);
624
+ if (typeof outstanding.timer.unref === "function")
625
+ outstanding.timer.unref();
626
+ }
627
+ return;
628
+ }
629
+ let resolve = () => { };
630
+ const settled = new Promise((r) => { resolve = r; });
631
+ const timer = setTimeout(() => this.settleSaltPending(agentName, sessionId, "timeout"), boundMs);
632
+ // The daemon must be able to exit with this outstanding — a pending agreement is not a reason to
633
+ // hold the process open.
634
+ if (typeof timer.unref === "function")
635
+ timer.unref();
636
+ this.#saltPending.set(key, { settled, resolve, timer, boundMs, announced });
637
+ }
638
+ /**
639
+ * WHICH of the four terminal answers the peer actually gave — 006-CRYPTO finding 2.
640
+ *
641
+ * The default is the NON-ASSERTING reason, not the most common one. An unknown label means a build
642
+ * we do not understand, and rendering that as "they had already hashed messages" states something
643
+ * about a counterparty that may be untrue — which is what sends an operator to raise a
644
+ * non-problem with them. The label is peer-supplied, so nothing outside the known set is repeated
645
+ * back as our own diagnosis.
646
+ *
647
+ * A missing entry maps to the already-hashing case: `PEER_CLOSED_FIRST` and an absent label both
648
+ * mean the peer is answering a closure of OURS, and `#saltForHashing` answers that with
649
+ * `ADOPTION_CLOSED_LOCALLY` one branch earlier — this is only the fallback if it did not.
650
+ */
651
+ peerClosedReason(key) {
652
+ const label = this.#saltPeerClosedLabel.get(key);
653
+ if (label === undefined || label === SALT_ADOPTION_LABELS.PEER_CLOSED_FIRST) {
654
+ return UNSALTED_REASONS.PEER_CLOSED_ADOPTION;
655
+ }
656
+ if (label === SALT_ADOPTION_LABELS.ALREADY_HASHING)
657
+ return UNSALTED_REASONS.PEER_CLOSED_ADOPTION;
658
+ if (label === SALT_ADOPTION_LABELS.FRONTIER_UNREADABLE)
659
+ return UNSALTED_REASONS.PEER_FRONTIER_UNREADABLE;
660
+ if (label === SALT_ADOPTION_LABELS.EXCHANGE_STALLED)
661
+ return UNSALTED_REASONS.PEER_EXCHANGE_STALLED;
662
+ return UNSALTED_REASONS.PEER_CLOSED_UNSPECIFIED;
663
+ }
664
+ /** Resolve a pending agreement. Idempotent: the first outcome wins and the timer is cleared. */
665
+ settleSaltPending(agentName, sessionId, outcome) {
666
+ const key = this.#ctx.sessionKey(agentName, sessionId);
667
+ const pending = this.#saltPending.get(key);
668
+ if (pending === undefined)
669
+ return;
670
+ this.#saltPending.delete(key);
671
+ // `agreed` is not recorded: the salt itself is the record, and `#getSessionSalt` answers first.
672
+ if (outcome !== "agreed")
673
+ this.#saltLastOutcome.set(key, outcome);
674
+ clearTimeout(pending.timer);
675
+ pending.resolve(outcome);
676
+ }
677
+ /**
678
+ * THIS SESSION'S UNSALTED HASH NEVER BECAME A MESSAGE — release the permanent closure it caused.
679
+ *
680
+ * ⚠️ REVIEW FINDING 3, and it is the opposite of the direction the flag was written to defend.
681
+ * `#hashedWithoutSalt` closes adoption at hash time, because for a session's first message the
682
+ * leaf is a network round trip away and every frontier count reads zero in between. Correct — but
683
+ * `cello_send` has three paths that compute the hash and then produce NOTHING: a sibling send
684
+ * holding the in-flight claim, the frontier moving under the send, and a non-durable send failure
685
+ * whose bytes go to a queue with no production consumer.
686
+ *
687
+ * In all three the session was permanently unsalted for a message that exists nowhere: no leaf, no
688
+ * wire, no copy at the peer. And B2b-2 made two of them MORE likely on a first message, because
689
+ * the five-second wait widens the very window the frontier re-check is watching.
690
+ *
691
+ * Only safe because it is called on paths that provably sent nothing. It deliberately does NOT
692
+ * clear `#unsaltedAnnounced`: the announcement was true when it fired and re-announcing on the
693
+ * retry would be the per-message flood Decision #15 forbids.
694
+ *
695
+ * ─── THREE OTHER SITES HASH AND MAY SEND NOTHING, AND ARE EXEMPT ON PURPOSE (pass 2, F8) ──────
696
+ *
697
+ * `daemon.ts`'s one-shot rejection and away reply, and `document-delivery-transport.ts`'s frame
698
+ * send, can all fail after hashing. None of them needs to abandon, and the reason is the same in
699
+ * each: every one is a REPLY. The inbound message that triggered it has already been leafed on
700
+ * this side, so `#saltAdoptionClosed` is already closed by the leaf count and would stay closed
701
+ * whatever this flag said. Calling abandon there would be a no-op that looks like a guarantee.
702
+ *
703
+ * Written down rather than left to be re-derived: the next reader's first question is why the
704
+ * list is three and not six.
705
+ */
706
+ abandonUnsaltedHash(agentName, sessionId) {
707
+ const key = this.#ctx.sessionKey(agentName, sessionId);
708
+ const held = this.#hashedWithoutSalt.get(key) ?? 0;
709
+ if (held === 0)
710
+ return;
711
+ // DECREMENT, never delete — F1. Deleting released a sibling's claim along with this one.
712
+ if (held > 1) {
713
+ this.#hashedWithoutSalt.set(key, held - 1);
714
+ return;
715
+ }
716
+ this.#hashedWithoutSalt.delete(key);
717
+ /**
718
+ * INFO, not DEBUG — review pass 2, F3. `session.content.unsalted` has already told this operator
719
+ * at INFO that the session is unsalted *"permanently… start a new session if you want the
720
+ * protection."* That statement is now false, and a retraction logged below the level of the
721
+ * claim it retracts is not a retraction. The announcement itself is deliberately NOT re-armed —
722
+ * re-announcing on the retry is the per-message flood Decision #15 forbids.
723
+ */
724
+ this.#ctx.logger.info("session.content.unsalted.retracted", {
725
+ agentName, sessionId,
726
+ impact: "a hash computed unsalted never became a message — no leaf, nothing on the wire, no copy at the counterparty — so this session CAN still adopt a salt. An earlier session.content.unsalted line said the session was permanently unsalted; that no longer applies.",
727
+ });
728
+ }
729
+ /**
730
+ * PUBLIC read of a session's agreed salt — `DOD-M15-SEALWIRE-1` part B2a.
731
+ *
732
+ * `content-park.ts` runs a SECOND, independent content-hash verifier (the park signature does not
733
+ * cover the envelope content, so it checks before `ingestReceivedContent` is ever reached), and it
734
+ * hardcoded `sha256`. It needs the salt to verify a v3 envelope, and it is outside this class.
735
+ *
736
+ * Read-only and cache-backed, so exposing it adds no way to CHANGE the salt from outside — the
737
+ * only writer remains `#persistSessionSalt`, behind the one-salt-per-session predicate.
738
+ */
739
+ /**
740
+ * IS THIS SESSION ACTUALLY PROTECTED BY ITS SALT RIGHT NOW — pass 2, F3.
741
+ *
742
+ * Distinct from `getSessionContentSalt`, which is POSSESSION and is what the verifier needs: a
743
+ * message parked before suspension was hashed under this salt and must still be checkable against
744
+ * it, so that accessor must keep answering with the bytes.
745
+ *
746
+ * This one answers the OPERATOR's question, and it is a different question. A suspended session
747
+ * holds a salt it will not use, so every hash it produces is `sha256` — reporting `contentSalted:
748
+ * true` there is not a gap, it is an affirmatively false security claim on the surface whose own
749
+ * comment reads *"a security property must not be inferable from a gap."* Same predicate
750
+ * `#saltForHashing` uses, so the flag cannot drift from the behaviour it describes.
751
+ */
752
+ isContentSaltActive(agentName, sessionId) {
753
+ if (this.#saltSuspended.has(this.#ctx.sessionKey(agentName, sessionId)))
754
+ return false;
755
+ return this.getSessionSalt(agentName, sessionId) !== null;
756
+ }
757
+ /**
758
+ * This session's agreed salt, or null. Reads the durable row through a cache, because Decision #8
759
+ * persists it for exactly one reason: *"a restart silently splits the transcript"* if the lookup
760
+ * misses and a fresh salt is minted.
761
+ *
762
+ * A read failure returns null WITH a log rather than throwing — except the bare `!this.#db` guard,
763
+ * which is this file's convention at 60+ sites and only reachable during shutdown. Null means "we
764
+ * hold no salt", which drives the agreement to offer a contribution — and against a peer that does
765
+ * hold one that is a named, loud `salt_state_divergent` refusal. So the degraded path ends in a
766
+ * diagnosis, not in a session that quietly hashes under the wrong value.
767
+ *
768
+ * ⚠️ THIS PARAGRAPH SPENT A UNIT STRANDED 180 LINES AWAY, directly above `contentHashForSession`
769
+ * and followed by that method's own block — so a reader hovering the hash decision got prose about
770
+ * salt read failures. Harmless and exactly the kind of drift that makes a comment stop being read.
771
+ */
772
+ getSessionSalt(agentName, sessionId) {
773
+ const key = this.#ctx.sessionKey(agentName, sessionId);
774
+ const cached = this.#sessionSalts.get(key);
775
+ if (cached)
776
+ return cached;
777
+ if (!this.#db)
778
+ return null;
779
+ try {
780
+ const row = this.#db
781
+ .prepare("SELECT content_salt FROM sessions WHERE agent_id = ? AND session_id = ?")
782
+ .get(this.#ctx.requireAgentId(agentName), sessionId);
783
+ const stored = row?.content_salt;
784
+ if (!stored || stored.length === 0)
785
+ return null;
786
+ /**
787
+ * A WRONG-WIDTH ROW IS NOT A SALT — review F8.
788
+ *
789
+ * Any non-empty blob used to be accepted, so a truncated row became "our salt", the digests
790
+ * then differed, and the operator was told *"one of you is running an older build — compare
791
+ * versions with them"*: sent to their counterparty over corruption on their own disk. Refusing
792
+ * it here makes this side hold NO salt, which re-offers a contribution and repairs.
793
+ */
794
+ if (stored.length !== SESSION_SALT_BYTES) {
795
+ this.#ctx.logger.error("session.salt.read.failed", {
796
+ agentName, sessionId, storedBytes: stored.length, expected: SESSION_SALT_BYTES,
797
+ reason: "wrong_width",
798
+ impact: "the stored salt is the wrong size, so it is not used; this session is treated as holding no salt and will re-agree one with the counterparty rather than comparing a corrupt value and blaming their build",
799
+ });
800
+ return null;
801
+ }
802
+ const salt = new Uint8Array(stored);
803
+ this.#sessionSalts.set(key, salt);
804
+ return salt;
805
+ }
806
+ catch (err) {
807
+ this.#ctx.logger.error("session.salt.read.failed", {
808
+ agentName, sessionId, error: extractErrorMessage(err),
809
+ impact: "this session is treated as holding no salt, so it will offer a fresh contribution; against a counterparty that still holds theirs the agreement refuses by name rather than hashing under a value only one side has",
810
+ });
811
+ return null;
812
+ }
813
+ }
814
+ /**
815
+ * Is this session past the point where a salt can be adopted? — Decision #8, part B2b-2.
816
+ *
817
+ * ⚠️ THE PREDICATE IS "HAS ANYTHING BEEN HASHED", NOT "IS THERE A LEAF" — review F5. A leaf is
818
+ * APPENDED after `await sendContent(...)` returns, so a message can be hashed, put on the wire, and
819
+ * still be invisible to `tree.size()`. Adopting inside that window makes leaf 0 unsalted and the
820
+ * rest salted — the exact split this exists to prevent, with the guard green.
821
+ *
822
+ * ⚠️ HELD CONTENT COUNTS, AND MUST BE HYDRATED FIRST — review F6. `#ensureHeldRestored` is lazy and
823
+ * is not called at session-node creation, so a revived session whose first inbound frame is the
824
+ * salt frame reads a frontier that excludes durable `held_content` rows — rows already hashed
825
+ * unsalted, which `#releaseHeld` will append moments later. Every other frontier reader in this
826
+ * file hydrates first, for this reason. `release: false`, because a salt frame must never deliver
827
+ * messages as a side effect.
828
+ *
829
+ * ⚠️ "CANNOT TELL" IS CLOSED, NOT OPEN. `#requireAgentId` throws for a retired agent, and inferring
830
+ * "zero leaves" from a failure to count them is how a guard becomes a formality. The cost of
831
+ * refusing is an unsalted session; the cost of permitting is a transcript neither rule can verify.
832
+ */
833
+ saltAdoptionClosed(agentName, sessionId) {
834
+ try {
835
+ this.#ctx.ensureHeldRestored(agentName, sessionId, { release: false });
836
+ }
837
+ catch { /* hydration is best-effort; the counts below still refuse on their own failure */ }
838
+ try {
839
+ const key = this.#ctx.sessionKey(agentName, sessionId);
840
+ const leaves = this.#ctx.getSessionTree(agentName, sessionId).size();
841
+ const held = this.#ctx.heldContentFor(key)?.size ?? 0;
842
+ const inFlight = this.#ctx.awaitingAck.get(key)?.size ?? 0;
843
+ /**
844
+ * ⚠️ THE HASH ITSELF COUNTS — B2b-2, and none of the three counts above can see it.
845
+ *
846
+ * Decision #8 closes adoption when content is HASHED. For a session's first message the leaf
847
+ * lands after `sendContent` returns, a network round trip later; there is no held row and no
848
+ * in-flight entry yet either. So between the hash and the leaf every count reads zero, and a
849
+ * peer contribution arriving in that window would be adopted — leaving the message already on
850
+ * the wire as the one unsalted leaf in a salted transcript.
851
+ */
852
+ const hashed = this.#hashedWithoutSalt.get(key) ?? 0;
853
+ const total = leaves + held + inFlight + hashed;
854
+ return {
855
+ closed: total > 0,
856
+ // The label crosses the WIRE, so it carries no counts and no error text — only which of the
857
+ // two refusals this is. The counts stay in `why`, which stays local.
858
+ label: SALT_ADOPTION_LABELS.ALREADY_HASHING,
859
+ leafCount: total,
860
+ why: `leaves=${leaves} held=${held} awaiting_ack=${inFlight} hashed=${hashed}`,
861
+ };
862
+ }
863
+ catch (err) {
864
+ return {
865
+ closed: true,
866
+ label: SALT_ADOPTION_LABELS.FRONTIER_UNREADABLE,
867
+ leafCount: -1,
868
+ why: `frontier_unreadable: ${extractErrorMessage(err)}`,
869
+ };
870
+ }
871
+ }
872
+ /**
873
+ * Persist the agreed salt, and DO NOT ANNOUNCE ONE WE FAILED TO STORE.
874
+ *
875
+ * The caller sends its fingerprint only if this returns true. A salt held in memory and not on
876
+ * disk would confirm agreement to the counterparty and then be gone at the next restart — turning
877
+ * a loud `salt_state_divergent` refusal, which is the whole point of Decision #10, into the silent
878
+ * split it exists to prevent, one restart later.
879
+ *
880
+ * ⚠️ SECOND ORPHAN OF THE SAME KIND. This paragraph was stranded above `#saltAdoptionClosed` and
881
+ * followed by that method's own block, exactly like the `#getSessionSalt` one re-homed in the
882
+ * previous pass — which walked straight past this one sixty lines below it. Two in one file is not
883
+ * coincidence: inserting a method between a doc block and its subject leaves no error, no lint,
884
+ * and no test, so the drift is invisible until someone reads for it.
885
+ */
886
+ persistSessionSalt(agentName, sessionId, salt) {
887
+ if (!this.#db) {
888
+ // NOT a silent return — review F7. The other two persist failures each emit an event, so a
889
+ // derive that could not store because the handle is closed was the ONE salt path producing no
890
+ // record at all. Only reachable during shutdown, which is exactly when a lone unexplained
891
+ // gap in the log is hardest to account for later.
892
+ this.#ctx.logger.error("session.salt.persist.failed", {
893
+ agentName, sessionId, reason: "db_closed",
894
+ impact: "the salt was NOT stored and is not announced; the agreement stays open and re-runs on the next connect",
895
+ });
896
+ return false;
897
+ }
898
+ try {
899
+ /**
900
+ * ORDER MATTERS HERE, and getting it wrong cost three findings — review F3, F4, F7.
901
+ *
902
+ * The adoption guard used to run FIRST, above `!this.#db` and outside this `try`. That:
903
+ * - short-circuited the `salt_already_stored` discrimination below, so a session that DOES
904
+ * hold a valid salt was told it "stays unsalted FOR THE LIFE of the session" after a
905
+ * transient read failure — a refusal asserting something false about the row (F4);
906
+ * - put `getSessionTree`'s `#requireAgentId` throw outside the `try`, where it surfaced as
907
+ * *"the stream read failed"* instead of a named salt-persist failure (F7).
908
+ *
909
+ * So the row's own state is established first, and only a session with no salt at all reaches
910
+ * the adoption question.
911
+ */
912
+ const existingRow = this.#db
913
+ .prepare("SELECT length(content_salt) AS n FROM sessions WHERE agent_id = ? AND session_id = ?")
914
+ .get(this.#ctx.requireAgentId(agentName), sessionId);
915
+ if (existingRow?.n === SESSION_SALT_BYTES) {
916
+ this.#ctx.logger.error("session.salt.persist.failed", {
917
+ agentName, sessionId, reason: "salt_already_stored",
918
+ impact: "this session already has a salt and it was NOT replaced — Decision #8 is one salt per session. Reaching here means a read failure made this side believe it had none; the stored salt is intact, nothing was announced, and the agreement re-runs against it on the next connect.",
919
+ });
920
+ return false;
921
+ }
922
+ const adoption = this.saltAdoptionClosed(agentName, sessionId);
923
+ if (adoption.closed) {
924
+ this.#ctx.logger.warn("session.salt.adoption.refused", {
925
+ agentName, sessionId, reason: "already_hashing", leafCount: adoption.leafCount, frontier: adoption.why,
926
+ impact: "this session has already hashed content under the unsalted rule, so the salt was NOT adopted — it stays unsalted FOR THE LIFE of the session. Adopting now would hash the rest of the conversation differently and leave a transcript that neither rule can verify end to end.",
927
+ guidance: "Nothing is broken and no message was lost: an unsalted session is exactly as verifiable as every session before this feature existed. It only means a relay holding the hashes could confirm a guess at a short message in THIS conversation. If you want the protection, start a new session — the agreement runs at open, before anything is hashed.",
928
+ });
929
+ return false;
930
+ }
931
+ /**
932
+ * THE ROW COUNT IS THE CHECK, and without it this method reported success for a write that
933
+ * stored nothing.
934
+ *
935
+ * An `UPDATE` that matches no row does not throw — it returns `changes: 0`. So a session whose
936
+ * row is missing (retired agent, a row that failed to write at creation, an id that does not
937
+ * line up) took the success branch, cached the salt in memory, and announced our fingerprint
938
+ * to the counterparty. Agreement confirmed, nothing on disk, and the failure surfaces at the
939
+ * next restart as the divergence this whole design exists to make loud — except one restart
940
+ * late and with both sides believing they had agreed.
941
+ *
942
+ * Found by a mutant that removed the caller's `if (!persisted) return`: the suite stayed green,
943
+ * because nothing could produce a false from here.
944
+ */
945
+ /**
946
+ * ONE SALT PER SESSION, ENFORCED AT THE WRITE — review F18.
947
+ *
948
+ * This `UPDATE` was unconditional, so it could replace an already-stored VALID salt. The path
949
+ * is real: `#getSessionSalt` returns null on a transient read failure, which sends this side
950
+ * down the derive path, which then overwrote the perfectly good salt on disk. The read error
951
+ * was logged; the destruction of the durable value was not — and the read log actively said
952
+ * the wrong thing, promising only that we would "offer a fresh contribution".
953
+ *
954
+ * The predicate has to allow ONE overwrite: a wrong-width blob is refused by `#getSessionSalt`
955
+ * (F8) precisely so a corrupt row can be replaced rather than stranding the session forever.
956
+ * So: write when there is nothing there, or when what is there is not a salt.
957
+ */
958
+ const written = this.#db
959
+ .prepare("UPDATE sessions SET content_salt = ? WHERE agent_id = ? AND session_id = ? " +
960
+ "AND (content_salt IS NULL OR length(content_salt) <> ?)")
961
+ .run(Buffer.from(salt), this.#ctx.requireAgentId(agentName), sessionId, SESSION_SALT_BYTES);
962
+ if (Number(written.changes) !== 1) {
963
+ // WHICH of the two it was. "No row" is a broken session record; "a salt is already there" is
964
+ // this guard doing its job, and telling an operator the row is missing when it is not would
965
+ // send them to look at the wrong thing.
966
+ // The `salt_already_stored` case is decided above now, before the adoption question, so
967
+ // reaching here with a valid salt in the row is not possible. Re-read anyway rather than
968
+ // assume: a wrong-width blob also fails the predicate and must not be reported as a missing
969
+ // row, which would send the operator to look at session state for a corrupt value.
970
+ const existing = this.#db
971
+ .prepare("SELECT length(content_salt) AS n FROM sessions WHERE agent_id = ? AND session_id = ?")
972
+ .get(this.#ctx.requireAgentId(agentName), sessionId);
973
+ const alreadyStored = existing?.n === SESSION_SALT_BYTES;
974
+ this.#ctx.logger.error("session.salt.persist.failed", {
975
+ agentName, sessionId, changes: Number(written.changes),
976
+ reason: alreadyStored ? "salt_already_stored" : "no_session_row",
977
+ impact: alreadyStored
978
+ ? "this session already has a salt and it was NOT replaced — Decision #8 is one salt per session. Reaching here means a read failure made this side believe it had none; the stored salt is intact, nothing was announced, and the agreement re-runs against it on the next connect."
979
+ : "the salt was NOT stored — no session row matched — so it is not announced either; the agreement stays open rather than being confirmed against a value that exists only in memory",
980
+ });
981
+ return false;
982
+ }
983
+ this.#sessionSalts.set(this.#ctx.sessionKey(agentName, sessionId), salt);
984
+ return true;
985
+ }
986
+ catch (err) {
987
+ this.#ctx.logger.error("session.salt.persist.failed", {
988
+ agentName, sessionId, error: extractErrorMessage(err),
989
+ impact: "the salt was NOT stored, so it is not announced to the counterparty either; the agreement stays open rather than being confirmed against a value that would vanish at the next restart",
990
+ });
991
+ return false;
992
+ }
993
+ }
994
+ /**
995
+ * DROP AN UNSPENT SALT — `DOD-M15-SALTSPLIT-1`. The second writer of `content_salt`, and the only
996
+ * one that clears it.
997
+ *
998
+ * Reached when the counterparty tells us it can never adopt a salt for this session. Keeping ours
999
+ * would mean every message we send from here is refused by them with
1000
+ * `content_hash_salt_unavailable` — a conversation that dies while looking merely quiet, which is
1001
+ * the failure this exists to prevent.
1002
+ *
1003
+ * ⚠️ THE ADOPTION CHECK IS REPEATED HERE ON PURPOSE, not because the caller is untrusted.
1004
+ *
1005
+ * The caller has already computed `adoption`, so this looks redundant — and it is, for today's one
1006
+ * call site. It stays because the cost of a future caller getting it wrong is a transcript that no
1007
+ * single rule can verify: leaves hashed under a salt that has just been erased, with nothing
1008
+ * recording that they were. A guard whose failure mode is silent and permanent belongs next to the
1009
+ * destructive act, not only at the place that currently decides to perform it. Same reasoning that
1010
+ * made `placeOwnLeaf`'s authorship parameter required rather than optional.
1011
+ *
1012
+ * Returns true only if a salt was actually cleared.
1013
+ */
1014
+ /**
1015
+ * SUSPEND, don't destroy — `DOD-M15-SALTSPLIT-1`, the authorization argument. Returns true if a
1016
+ * salt is now suspended (or already was).
1017
+ *
1018
+ * This is the frame handler's entry point. It runs the same two refusals as the erase below —
1019
+ * a spent salt and one mid-flight are not ours to set aside either, because the messages already
1020
+ * hashed under them would become unverifiable the moment we stop using it — and where they do not
1021
+ * fire it records the suspension instead of doing anything irreversible.
1022
+ */
1023
+ suspendSalt(agentName, sessionId, correlationId) {
1024
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1025
+ if (this.getSessionSalt(agentName, sessionId) === null)
1026
+ return false;
1027
+ if (this.#saltSuspended.has(key))
1028
+ return true;
1029
+ const inFlight = this.#hashedWithSalt.get(key) ?? 0;
1030
+ const adoption = this.saltAdoptionClosed(agentName, sessionId);
1031
+ if (inFlight > 0 || adoption.closed) {
1032
+ /**
1033
+ * SPENT, or mid-send. Suspending is not destructive, but it IS a split: content already hashed
1034
+ * under this salt stays hashed under it while everything after would be hashed the other way,
1035
+ * in one session, with nothing recording where the change happened. That is the one thing
1036
+ * Decision #8 forbids outright, so the salt keeps being used and the session stays honestly
1037
+ * broken rather than becoming dishonestly half-verifiable.
1038
+ */
1039
+ this.#ctx.logger.info("session.salt.suspend.refused", {
1040
+ agentName, sessionId, correlationId,
1041
+ reason: inFlight > 0 ? "salted_hash_in_flight" : adoption.label,
1042
+ ...(inFlight > 0 ? { inFlight } : { frontier: adoption.why }),
1043
+ impact: "the salt stays IN USE, because content in this session is already hashed under it and switching now would split the transcript — half verifiable by one rule, half by another. The counterparty cannot hold this salt, so it will keep refusing messages sent from here. See session.salt.split.",
1044
+ });
1045
+ return false;
1046
+ }
1047
+ this.#saltSuspended.add(key);
1048
+ this.#ctx.logger.info("session.salt.suspended", {
1049
+ agentName, sessionId, correlationId,
1050
+ impact: "the counterparty can never adopt a salt for this session, so this side has STOPPED USING its own — messages are hashed the way every build before content salting hashed them, and every message continues to be accepted. Nothing was hashed under it, so nothing is split.",
1051
+ guidance: "No action. The salt bytes are kept, not erased: if the counterparty was merely unable to read its own state for a moment, its next announcement carrying a matching fingerprint restores this session to salted automatically. The bytes are erased only when this session actually hashes a message unsalted, which is the point after which keeping them would re-salt the session at the next restart.",
1052
+ });
1053
+ return true;
1054
+ }
1055
+ /** Un-suspend: the peer answered with a fingerprint matching the salt we kept. */
1056
+ resumeSalt(agentName, sessionId, correlationId) {
1057
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1058
+ if (!this.#saltSuspended.has(key))
1059
+ return;
1060
+ /**
1061
+ * ⚠️ REFUSE THE RESUME IF THIS SESSION HAS ALREADY HASHED UNSALTED — pass 2, F1 (HIGH).
1062
+ *
1063
+ * `#resumeSalt` deleted the mark unconditionally, and the reviewer produced the counter-example
1064
+ * in ONE process with no restart: suspend, the peer keeps talking so a leaf lands, we send `m1`
1065
+ * under `sha256`, the peer's frontier recovers and announces `fingerprint(S)`, we resume, and
1066
+ * `m2` goes out under `hmac`. Two rules, one session — and `session.salt.resumed` asserted
1067
+ * *"No message was hashed while suspended, so the transcript is uniform"* while it was happening.
1068
+ * **The code never checked the thing its own log line claimed**, which is this milestone's
1069
+ * signature defect committed inside the fix for it.
1070
+ *
1071
+ * `#unsaltedAnnounced` is exactly that fact and is already maintained, so the check costs a
1072
+ * lookup. Once it is set the salt can never be used again, so it is erased here rather than left
1073
+ * to be found by a later restart.
1074
+ */
1075
+ if (this.#unsaltedAnnounced.has(key)) {
1076
+ this.#ctx.logger.warn("session.salt.resume.refused", {
1077
+ agentName, sessionId, correlationId,
1078
+ impact: "the counterparty now confirms a salt this side is holding, but this session has ALREADY hashed at least one message unsalted. Resuming would put half the transcript under each rule, which no single rule can verify — so the session stays unsalted for its whole life and the salt is released.",
1079
+ guidance: "Nothing to do here, and nothing is lost: the transcript stays uniform and every message is intact. If you want the salt protection with this counterparty, start a new session — the agreement runs at open, before anything is hashed.",
1080
+ });
1081
+ this.#saltSuspended.delete(key);
1082
+ this.discardUnspentSalt(agentName, sessionId, correlationId);
1083
+ return;
1084
+ }
1085
+ this.#saltSuspended.delete(key);
1086
+ /**
1087
+ * THE RECOVERY THE ERASE MADE IMPOSSIBLE. Keeping the bytes is what allows this line to exist:
1088
+ * the peer's earlier terminal frame was wrong (a frontier it could not read for a moment), it can
1089
+ * read again, and the fingerprints match — so the session resumes salted with nothing lost. An
1090
+ * erased salt cannot be re-derived from one side.
1091
+ */
1092
+ this.#ctx.logger.info("session.salt.resumed", {
1093
+ agentName, sessionId, correlationId,
1094
+ impact: "the counterparty now confirms the same salt this side kept, so this session is salted again. It was suspended earlier because the counterparty reported it could never hold one; that has resolved. No message was hashed while suspended, so the transcript is uniform.",
1095
+ });
1096
+ }
1097
+ discardUnspentSalt(agentName, sessionId, correlationId) {
1098
+ const held = this.getSessionSalt(agentName, sessionId);
1099
+ if (held === null)
1100
+ return false;
1101
+ /**
1102
+ * ⚠️ IN-FLIGHT FIRST — `DOD-M15-SALTSPLIT-1` review HIGH-2. `#saltAdoptionClosed` cannot see a
1103
+ * hash that has been computed under the salt but has not yet become a leaf, a hold or an
1104
+ * awaiting-ack entry, and that gap is a full relay round trip wide.
1105
+ */
1106
+ /**
1107
+ * ⚠️ MEASURED UNREACHABLE FROM TODAY'S CALLERS, AND KEPT ANYWAY — pass 2 test-teeth, survivor 2.
1108
+ *
1109
+ * Deleting this block leaves the whole salt suite GREEN. That is the definition this unit has
1110
+ * used all along for *"not a guard, a comment that happens to execute"*, so it is labelled rather
1111
+ * than quietly left to look load-bearing. `#suspendSalt` refuses on `inFlight > 0` before a
1112
+ * session can ever be marked, and both callers of this method require the mark — so the deferred
1113
+ * erase cannot observe a non-zero count.
1114
+ *
1115
+ * It stays for one reason: **it sits at an irreversible write.** The earlier instance of this
1116
+ * question in this same unit was resolved by making the guard the actual decision-maker, and that
1117
+ * option does not exist here — `#suspendSalt` genuinely must refuse early, so the duplication is
1118
+ * structural rather than a mistake about where responsibility lives. For a destructive act, the
1119
+ * safe direction is to keep a check that cannot fire over removing one that turns out it could.
1120
+ *
1121
+ * What must NOT happen is claiming it as coverage. It is not tested and it is not testable from
1122
+ * outside; if a third caller ever reaches this method without the suspension mark, this becomes
1123
+ * reachable and needs a test in the same commit.
1124
+ */
1125
+ const inFlight = this.#hashedWithSalt.get(this.#ctx.sessionKey(agentName, sessionId)) ?? 0;
1126
+ if (inFlight > 0) {
1127
+ this.#ctx.logger.info("session.salt.discard.refused", {
1128
+ agentName, sessionId, correlationId, reason: "salted_hash_in_flight", inFlight,
1129
+ impact: "the salt was NOT dropped: a message has already been hashed under it and is mid-send, so erasing it now would put a hash on the wire that nothing — including this daemon — could ever recompute. The session stays salted and the counterparty, which cannot adopt, will refuse what is in flight.",
1130
+ });
1131
+ return false;
1132
+ }
1133
+ const adoption = this.saltAdoptionClosed(agentName, sessionId);
1134
+ if (adoption.closed) {
1135
+ /**
1136
+ * SPENT. Something is already hashed under this salt, so it is not ours to drop.
1137
+ *
1138
+ * INFO, not ERROR, and the level is a judgement rather than a downgrade: this is the guard
1139
+ * doing its job correctly, and the FAILURE it accompanies — the session is split and unusable
1140
+ * — is reported by `session.salt.split` at ERROR from the caller that has the operator-facing
1141
+ * detail. Two ERRORs for one condition trains people to read neither. This line stays so the
1142
+ * refusal itself is correlatable when someone asks why the salt is still on disk.
1143
+ */
1144
+ this.#ctx.logger.info("session.salt.discard.refused", {
1145
+ agentName, sessionId, correlationId, reason: adoption.label, frontier: adoption.why,
1146
+ impact: "the salt was NOT dropped, because content in this session is already hashed under it and erasing it would leave a transcript no single rule can verify. The session stays split: the counterparty holds no salt and refuses everything sent from here.",
1147
+ });
1148
+ return false;
1149
+ }
1150
+ if (!this.#db) {
1151
+ this.#ctx.logger.error("session.salt.discard.failed", {
1152
+ agentName, sessionId, correlationId, reason: "db_closed",
1153
+ impact: "the salt is still stored, so after the next restart this side hashes salted while the counterparty refuses every message. Only reachable during shutdown; the agreement re-runs on the next connect, which discards it then.",
1154
+ });
1155
+ return false;
1156
+ }
1157
+ try {
1158
+ const cleared = this.#db
1159
+ .prepare("UPDATE sessions SET content_salt = NULL WHERE agent_id = ? AND session_id = ?")
1160
+ .run(this.#ctx.requireAgentId(agentName), sessionId);
1161
+ if (Number(cleared.changes) !== 1) {
1162
+ // The row-count check that `#persistSessionSalt` learned the hard way: an UPDATE matching no
1163
+ // row does not throw, and reporting success here would leave the durable salt in place while
1164
+ // the cache said otherwise — salted after a restart, unsalted before one.
1165
+ this.#ctx.logger.error("session.salt.discard.failed", {
1166
+ agentName, sessionId, correlationId, changes: Number(cleared.changes), reason: "no_session_row",
1167
+ impact: "the stored salt was NOT cleared, so this side hashes unsalted now and salted again after a restart — the transcript splits at the restart rather than here",
1168
+ });
1169
+ return false;
1170
+ }
1171
+ }
1172
+ catch (err) {
1173
+ this.#ctx.logger.error("session.salt.discard.failed", {
1174
+ agentName, sessionId, correlationId, error: extractErrorMessage(err),
1175
+ impact: "the stored salt was NOT cleared, so this side hashes unsalted now and salted again after a restart — the transcript splits at the restart rather than here",
1176
+ });
1177
+ return false;
1178
+ }
1179
+ /**
1180
+ * CACHE AFTER ROW, and both or the session is worse off than before.
1181
+ *
1182
+ * `#saltForHashing` reads the cache on its first line and never consults the row, so clearing
1183
+ * one without the other produces a session that hashes one way in this process and the other way
1184
+ * in the next — the split transcript, arriving at a daemon restart instead of at a frame.
1185
+ */
1186
+ this.#sessionSalts.delete(this.#ctx.sessionKey(agentName, sessionId));
1187
+ /**
1188
+ * ⚠️ THE MARK GOES WITH THE BYTES — pass 2, F5. Leaving the key in `#saltSuspended` after a
1189
+ * successful erase means a LATER agreed salt is silently never used: `#persistSessionSalt`'s
1190
+ * predicate explicitly allows a write when the column is NULL, and `abandonUnsaltedHash` can
1191
+ * re-open adoption — so the session would log `session.salt.agreed`, surface as protected, and
1192
+ * hash `sha256` for the rest of its life. A stale suppression is indistinguishable from a
1193
+ * feature that does not work.
1194
+ */
1195
+ this.#saltSuspended.delete(this.#ctx.sessionKey(agentName, sessionId));
1196
+ this.#ctx.logger.info("session.salt.discarded", {
1197
+ agentName, sessionId, correlationId,
1198
+ impact: "the counterparty can never adopt a salt for this session, so this side dropped its own before spending it. Both sides now hash unsalted — exactly as verifiable as every session shipped before content salting existed, and every message continues to be accepted. Nothing was hashed under the discarded salt.",
1199
+ });
1200
+ return true;
1201
+ }
1202
+ /** Apply one inbound salt-agreement frame. The verdict is the pure function's; this executes it. */
1203
+ async handleSaltFrame(agentName, sessionId, frame, correlationId) {
1204
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1205
+ const peerHalfHex = frame.contribution ? Buffer.from(frame.contribution).toString("hex") : null;
1206
+ const peerFingerprintHex = frame.fingerprint ? Buffer.from(frame.fingerprint).toString("hex") : null;
1207
+ // WHY the peer closed, kept for the operator-facing reason — 006-CRYPTO finding 2. Recorded here
1208
+ // rather than in the `adoption_closed` handler because that action fires for OUR closure too,
1209
+ // and only the frame says what the PEER said.
1210
+ if (typeof frame.adoptionClosed === "string") {
1211
+ this.#saltPeerClosedLabel.set(key, frame.adoptionClosed);
1212
+ }
1213
+ const adoption = this.saltAdoptionClosed(agentName, sessionId);
1214
+ const action = onPeerSaltFrame({
1215
+ ...this.saltState(agentName, sessionId),
1216
+ // Review F2: the frontier is what decides whether THIS side can still adopt, and only the
1217
+ // caller can count it. Without this the state machine derives, the persist refuses, and the
1218
+ // peer never learns — which is how the two sides end up on opposite verdicts.
1219
+ ownAdoption: adoption.closed
1220
+ ? { closed: true, label: adoption.label, why: adoption.why }
1221
+ : { closed: false },
1222
+ // Keyed on the peer's BYTES, not on a repair counter: a genuinely NEW half from the peer must
1223
+ // still get our contribution back, and only an identical re-offer is the loop (review F14).
1224
+ alreadyRepairedAgainstPeerHalf: peerHalfHex !== null && this.#saltRepairedAgainst.get(key) === peerHalfHex,
1225
+ // The mirror, 006-CRYPTO finding 1: without it a saltless side answers a latched holder's
1226
+ // fingerprint forever. Same keying rule — an identical re-offer is the loop, a new one is not.
1227
+ alreadyRepairedAgainstPeerFingerprint: peerFingerprintHex !== null && this.#saltRepairedAgainstFingerprint.get(key) === peerFingerprintHex,
1228
+ frame,
1229
+ });
1230
+ if (action.action === "confirmed") {
1231
+ // DOD-M15-SALTSPLIT-1: the peer confirms the salt we KEPT while suspended — resume before logging
1232
+ // agreement, so a resumed session is never reported as agreed while still suspended.
1233
+ this.resumeSalt(agentName, sessionId, correlationId);
1234
+ this.#ctx.logger.info("session.salt.agreed", {
1235
+ agentName, sessionId, correlationId, via: "fingerprint_match",
1236
+ });
1237
+ // B2b-2: release a first send that is waiting on this agreement. Both `confirmed` and
1238
+ // `derive_and_announce` end with a salt this side can hash under, so both settle the wait.
1239
+ this.settleSaltPending(agentName, sessionId, "agreed");
1240
+ return;
1241
+ }
1242
+ if (action.action === "derive_and_announce") {
1243
+ /**
1244
+ * ⚠️ I DEFENDED THE OPPOSITE OF THIS TWICE, AND BOTH DEFENCES WERE WRONG. The code now does
1245
+ * what the "surviving mutant" did; recording that rather than quietly switching, because the
1246
+ * reasoning is the useful part.
1247
+ *
1248
+ * A failed persist used to fall through with no settle, so a waiting first send sat out the
1249
+ * FULL FIVE SECONDS and was then told, by the timeout path, to go and check its counterparty's
1250
+ * build version — for a fault that was this machine's own disk.
1251
+ *
1252
+ * Defence #1 said releasing the waiter "would hand it a null it would hash unsalted under."
1253
+ * True, and not a consequence: that is exactly what the timeout does. Defence #2 said the
1254
+ * remaining bound gave a repair a chance to land — and the review showed that essentially
1255
+ * cannot fire. This branch returns BEFORE the announce, so nothing goes out and nothing comes
1256
+ * back; all five of `#sendSaltFrame`'s callers are triggered by a peer connect or an inbound
1257
+ * frame. Only a counterparty reconnect inside those seconds could do it.
1258
+ *
1259
+ * So the real trade was a rare reconnect-within-five-seconds repair against five seconds of
1260
+ * visible latency on the operator's first message AND a diagnosis pointing at the wrong
1261
+ * machine. The repair loses. Settle immediately under its own name, so `#saltForHashing` can
1262
+ * say *our own write failed* instead of *they did not answer*.
1263
+ */
1264
+ if (!this.persistSessionSalt(agentName, sessionId, action.salt)) {
1265
+ this.settleSaltPending(agentName, sessionId, "persist_failed");
1266
+ return;
1267
+ }
1268
+ this.#ctx.logger.info("session.salt.agreed", {
1269
+ agentName, sessionId, correlationId, via: "derived",
1270
+ });
1271
+ this.settleSaltPending(agentName, sessionId, "agreed");
1272
+ // `void`, not `await` — review F10. This runs inside the INBOUND content-stream handler, so
1273
+ // awaiting an outbound `newStream` here lets a stalled dial hold up the stream we are reading.
1274
+ // The connect-side call is `void`-ed for the same reason and this is now consistent with it.
1275
+ void this.#ctx.sendSaltFrame(agentName, sessionId, correlationId);
1276
+ return;
1277
+ }
1278
+ if (action.action === "adoption_closed") {
1279
+ // B2b-2: terminal means there is nothing left to wait for. A send still holding on the bound
1280
+ // would otherwise sit out the full five seconds for an answer that has already arrived and
1281
+ // said no — the slowest possible way to reach a decision both sides already agree on.
1282
+ this.settleSaltPending(agentName, sessionId, "closed");
1283
+ /**
1284
+ * Terminal, and NOT a freeze — review F1/F2. Both sides stay unsalted, which is exactly as
1285
+ * verifiable as every session shipped before the salt existed; the thing that was broken was
1286
+ * them disagreeing about it silently.
1287
+ *
1288
+ * WHICH SIDE DECLINED decides the level, and it is not decoration.
1289
+ *
1290
+ * If WE closed, an operator has lost a protection they could otherwise have had, and there is
1291
+ * something they can do about it — that is a WARN under `session.salt.adoption.refused`, which
1292
+ * keeps meaning what it has always meant.
1293
+ *
1294
+ * If we are merely LEARNING the peer closed, nothing about this machine is at fault and there
1295
+ * is nothing for its operator to do. Logging that at WARN would fire on the innocent side of
1296
+ * every such session and train them to ignore the name.
1297
+ */
1298
+ /**
1299
+ * DOD-M15-SALTSPLIT-1 — ONE PLACE DECIDES WHETHER THE SALT GOES, and it is not here.
1300
+ *
1301
+ * ⚠️ THIS CALL WAS INSIDE THE `else` BELOW, AND THE REVERT TEST CAUGHT IT.
1302
+ *
1303
+ * Guarding it by `adoption.closed` here meant `#discardUnspentSalt`'s own adoption check could
1304
+ * never be reached, so deleting that check left all three tests GREEN — the survivor. A guard
1305
+ * nothing can redden is not a guard; it is a comment that happens to execute, which is the
1306
+ * shape this milestone keeps finding.
1307
+ *
1308
+ * Called unconditionally now. The method owns the spent/unspent decision, both outcomes run
1309
+ * through it, and deleting its check reddens the spent test immediately. That also removes the
1310
+ * duplicated condition: two places deciding the same thing is one place being wrong later.
1311
+ */
1312
+ /**
1313
+ * The return is CONSUMED, not decorative — review LOW-5. `true` means a salt was actually
1314
+ * cleared, which settles the question below without a second read; `false` is ambiguous (we
1315
+ * held none, or we refused to drop one), so that case still asks.
1316
+ */
1317
+ const suspended = this.suspendSalt(agentName, sessionId, correlationId);
1318
+ /**
1319
+ * "Still holds a salt it is USING" — suspension is what settles it, not possession. A suspended
1320
+ * session keeps the bytes on disk deliberately, and reporting that as an unrecoverable split
1321
+ * would fire the ERROR below on the one case that recovers by itself.
1322
+ */
1323
+ const stillHoldsSalt = !suspended && this.getSessionSalt(agentName, sessionId) !== null;
1324
+ const shared = {
1325
+ agentName, sessionId, correlationId, detail: action.detail,
1326
+ /**
1327
+ * ⚠️ *"no message is affected"* IS FALSE WHEN WE ARE STILL HOLDING A SALT — review MEDIUM-4,
1328
+ * second instance. The sentence was written for a session where neither side ever adopted
1329
+ * one, and it stayed attached to a branch that now also covers the case where this side
1330
+ * kept a spent salt and every message it sends is about to be refused. Two log lines from
1331
+ * one event contradicting each other is worse than either alone.
1332
+ */
1333
+ impact: stillHoldsSalt
1334
+ ? "the counterparty will not use a content salt, and this side is still holding one it cannot drop — see session.salt.split on the next line for what that costs and what to do about it."
1335
+ : "neither side will use a content salt for this session, and both now know it. Messages are hashed the way every build before this feature hashed them — nothing is degraded relative to any shipped release, and no message is affected.",
1336
+ };
1337
+ if (adoption.closed) {
1338
+ /**
1339
+ * ⚠️ TWO REFUSALS, TWO DIFFERENT THINGS TO DO — and this used to report both as
1340
+ * `already_hashing`.
1341
+ *
1342
+ * A session that has already sent messages is the feature working: the fix is a new session,
1343
+ * and it will work. A frontier this side could not READ is local storage trouble: a new
1344
+ * session will refuse in exactly the same way, so sending the operator to open one is
1345
+ * sending them somewhere that cannot help. `frontier` carries the counts (or the error) so
1346
+ * the two are separable from the log alone.
1347
+ */
1348
+ const unreadable = adoption.label === SALT_ADOPTION_LABELS.FRONTIER_UNREADABLE;
1349
+ this.#ctx.logger.warn("session.salt.adoption.refused", {
1350
+ ...shared,
1351
+ reason: adoption.label,
1352
+ leafCount: adoption.leafCount,
1353
+ frontier: adoption.why,
1354
+ guidance: unreadable
1355
+ ? "This side could not read its own message frontier, so it refused the salt rather than risk hashing half the session one way and half the other. Starting a new session will NOT help — it will refuse the same way. Look for session.content.held.restore.failed or other storage errors around this line; the conversation still works and every message is intact, it is just unsalted."
1356
+ : "Nothing is broken and no message was lost: an unsalted session is exactly as verifiable as every session before this feature existed. It only means a relay holding the hashes could confirm a guess at a short message in THIS conversation. If you want the protection, start a new session — the agreement runs at open, before anything is hashed.",
1357
+ });
1358
+ }
1359
+ else {
1360
+ /**
1361
+ * DOD-M15-SALTSPLIT-1 — CARRY OUT THE CLAIM ABOVE INSTEAD OF ONLY STATING IT.
1362
+ *
1363
+ * `shared.impact` says *"neither side will use a content salt for this session, and both now
1364
+ * know it."* Nothing made that true: a salt already agreed on this side stayed on disk and in
1365
+ * the cache, and `#saltForHashing` returns it before it ever looks at adoption. Our adoption
1366
+ * is still open here, so nothing has been hashed under it and dropping it is free.
1367
+ *
1368
+ * Ordering matters — discard BEFORE the log, so the line cannot claim an outcome that the
1369
+ * write then failed to produce.
1370
+ */
1371
+ this.#ctx.logger.info("session.salt.adoption.closed", shared);
1372
+ }
1373
+ /**
1374
+ * ⚠️ OUTSIDE THE ADOPTION BRANCH — pass 2, F4. This used to live inside `if (adoption.closed)`,
1375
+ * so the one case that needed it most never got it: suspension refused for
1376
+ * `salted_hash_in_flight` while adoption is still OPEN leaves us holding a salt the peer can
1377
+ * never accept, and it took the `else` path. Measured on that exact scenario:
1378
+ * `suspend.refused = 1`, `adoption.closed = 1`, **`split = 0`** — while two other log lines
1379
+ * told the operator to *"see session.salt.split on the next line"*, a line that was never
1380
+ * written. Guidance pointing at an event that does not fire is worse than no guidance: it
1381
+ * spends the reader's trust and their time.
1382
+ *
1383
+ * The condition was always `stillHoldsSalt`; only its placement disagreed.
1384
+ *
1385
+ * ─── What this event means, moved here with the code it describes ─────────────────────────
1386
+ *
1387
+ * We hold a salt AND the peer has told us it can never hold one. Either our frontier closed
1388
+ * with the salt already spent, or a salted hash is mid-flight — both mean the salt cannot be
1389
+ * released, so the peer will refuse every message we send with `content_hash_salt_unavailable`.
1390
+ *
1391
+ * `session.salt.adoption.refused` may fire alongside, saying *"nothing is degraded relative to
1392
+ * any shipped release, and no message is affected"* — true for the ordinary refusal and FALSE
1393
+ * here, at the exact moment every message stops being accepted. Hence its own event at ERROR
1394
+ * rather than a tightened sentence on that one: an operator filtering for the refusal is
1395
+ * looking at a benign condition, and this is not it.
1396
+ */
1397
+ if (stillHoldsSalt) {
1398
+ /**
1399
+ * ⚠️ TWO REASONS REACH `adoption.closed`, AND ONLY ONE IS ABOUT CONTENT — review MEDIUM-4.
1400
+ *
1401
+ * This fired for both with a single impact asserting *"content here is already hashed
1402
+ * under a salt"*. For `frontier_unreadable` that is a claim about content made from a
1403
+ * database read that FAILED — we do not know what was hashed; that is the whole condition.
1404
+ *
1405
+ * The WARN twenty lines above was explicitly corrected for this exact collapse — its
1406
+ * comment reads *"TWO REFUSALS, TWO DIFFERENT THINGS TO DO — and this used to report both
1407
+ * as `already_hashing`"* — and I reintroduced it one severity level up, with the guidance
1408
+ * that WARN was fixed to stop giving. Branching on the label the way it already does.
1409
+ */
1410
+ const unreadable = adoption.label === SALT_ADOPTION_LABELS.FRONTIER_UNREADABLE;
1411
+ this.#ctx.logger.error("session.salt.split", {
1412
+ agentName, sessionId, correlationId, reason: adoption.label, frontier: adoption.why,
1413
+ impact: unreadable
1414
+ ? "this side holds a salt, the counterparty can never hold one, and this side could NOT read its own message frontier — so whether anything has been hashed under that salt is unknown. The salt is kept rather than dropped, because dropping one that HAS been spent leaves a transcript no single rule can verify. Until the read succeeds, expect the counterparty to refuse messages sent from here."
1415
+ : "this session cannot continue. Content here is already hashed under a salt the counterparty can never hold, so they refuse every message sent from this side — the conversation looks quiet rather than broken, and the session can never be sealed because the two transcripts no longer agree on a leaf.",
1416
+ guidance: unreadable
1417
+ ? "Do NOT start a new session yet — it would refuse in exactly the same way, because the fault is this side's storage rather than this conversation. Look for session.content.held.restore.failed or other storage errors around this line. Once the frontier reads again, this resolves to either an ordinary salted session or the split case, and the log will say which."
1418
+ : "Start a new session with this counterparty: the salt agreement runs at open, before anything is hashed, so a fresh session agrees or declines cleanly on both sides. This one cannot be repaired — the salt cannot be dropped without leaving a transcript no single rule can verify, and it cannot be shared with a peer that has already closed adoption.",
1419
+ });
1420
+ }
1421
+ if (action.announce) {
1422
+ void this.#ctx.sendSaltFrame(agentName, sessionId, correlationId, action.announce);
1423
+ }
1424
+ return;
1425
+ }
1426
+ if (action.action === "repair") {
1427
+ /**
1428
+ * THE REPAIR — review F1. The two sides are out of step and CAN converge, so re-send our half
1429
+ * rather than destroying the session.
1430
+ *
1431
+ * At INFO because it is a real event an operator may need to correlate with a
1432
+ * `session.salt.announce.failed` or `session.salt.persist.failed` on either machine, and
1433
+ * because a session that repairs REPEATEDLY is a signal even though each repair is benign.
1434
+ */
1435
+ this.#ctx.logger.info("session.salt.repair", {
1436
+ agentName, sessionId, correlationId, detail: action.detail,
1437
+ answeredWith: action.frame.contribution ? "contribution" : "fingerprint",
1438
+ });
1439
+ // Recorded ONLY for a repair that sent our half, because that is the one a second identical
1440
+ // offer must not repeat (review F14).
1441
+ if (peerHalfHex && action.frame.contribution)
1442
+ this.#saltRepairedAgainst.set(key, peerHalfHex);
1443
+ // AND THE MIRROR (006-CRYPTO finding 1): we answered the peer's FINGERPRINT with our half. An
1444
+ // earlier note here said recording this "says nothing, that branch is already terminal for the
1445
+ // peer" — it is terminal only for a peer that HOLDS a salt, and the loop is the case where we
1446
+ // do not. A second identical fingerprint now closes adoption instead of repairing again.
1447
+ if (peerFingerprintHex && action.frame.contribution) {
1448
+ this.#saltRepairedAgainstFingerprint.set(key, peerFingerprintHex);
1449
+ }
1450
+ void this.#ctx.sendSaltFrame(agentName, sessionId, correlationId, action.frame);
1451
+ return;
1452
+ }
1453
+ // `detail` is the primitive's own sentence wherever the primitive produced it — never a code of
1454
+ // ours substituted for it (Invariant 2). `guidance` is what the operator can DO, and it comes
1455
+ // from the total map so a reason can never reach a log without one.
1456
+ this.#ctx.logger.error("session.salt.disagreement", {
1457
+ agentName, sessionId, correlationId,
1458
+ reason: action.reason,
1459
+ detail: action.detail,
1460
+ guidance: SALT_FREEZE_GUIDANCE[action.reason],
1461
+ });
1462
+ /**
1463
+ * TELL THE PEER BEFORE TEARING DOWN — review F1's mirror.
1464
+ *
1465
+ * Only the fingerprint mismatch carries a notice, and only it can: the peer holds everything
1466
+ * needed to run the identical comparison and has simply not been given our side of it. Without
1467
+ * this the session stops answering and the far operator gets no reason at all, while ours gets a
1468
+ * full explanation — Decision #10 asks for BOTH sides to refuse by name.
1469
+ *
1470
+ * Awaited, unlike the other sends, because `destroySessionNode` on the next line takes the node
1471
+ * away and an un-awaited write would race its own transport. A failure is already handled
1472
+ * inside — the refusal here has happened either way.
1473
+ */
1474
+ if (action.notifyPeer) {
1475
+ await this.#ctx.sendSaltFrame(agentName, sessionId, correlationId, action.notifyPeer);
1476
+ }
1477
+ await this.#ctx.freezeSession(agentName, sessionId, action.reason, {
1478
+ event: "session.salt.frozen",
1479
+ observation: `the salt agreement could not be completed with this counterparty: ${action.detail}`,
1480
+ impact: "the session was stopped rather than left to hash under a value the two sides do not share; no message was lost and the transcript is unaffected — only a NEW session moves this forward",
1481
+ reviveReason: `session_frozen_${action.reason}`,
1482
+ // The operator-facing sentence comes from the TOTAL guidance map, so a reason can never reach
1483
+ // this refusal without one — and it is what stops a salt disagreement being reported to them
1484
+ // as their counterparty failing a key check.
1485
+ reviveGuidance: SALT_FREEZE_GUIDANCE[action.reason],
1486
+ }, correlationId);
1487
+ }
1488
+ /**
1489
+ * Forget every salt fact this module holds for one session.
1490
+ *
1491
+ * ⚠️ ELEVEN MAPS BEHIND ONE CALL, and that is the point. These were eleven of the twenty-four
1492
+ * per-session containers the manager's cache eviction cleared by hand — so forgetting a session
1493
+ * meant knowing every map that might hold a piece of it, and adding a twelfth meant remembering to
1494
+ * add a twelfth delete. Missing one leaves a per-session entry alive for the life of the process.
1495
+ * The list of what to forget now lives beside the code that fills them.
1496
+ */
1497
+ evictSession(agentName, sessionId) {
1498
+ const key = this.#ctx.sessionKey(agentName, sessionId);
1499
+ this.#saltContributions.delete(key);
1500
+ this.#sessionSalts.delete(key);
1501
+ this.#saltRepairedAgainst.delete(key);
1502
+ this.#saltRepairedAgainstFingerprint.delete(key);
1503
+ this.#saltPeerClosedLabel.delete(key);
1504
+ this.#hashedWithoutSalt.delete(key);
1505
+ this.#hashedWithSalt.delete(key);
1506
+ this.#saltSuspended.delete(key);
1507
+ this.#unsaltedAnnounced.delete(key);
1508
+ this.#saltLastOutcome.delete(key);
1509
+ }
1510
+ /**
1511
+ * The pair the agreement reasons over: our salt, and the half that goes with it.
1512
+ *
1513
+ * Minting is deliberate and conditional. With NO salt we are certain to need a half — to offer, or
1514
+ * to derive with — so minting here is what makes the exchange work at all. WITH a salt we must
1515
+ * never mint; see `#ownSaltHalf`.
1516
+ */
1517
+ saltState(agentName, sessionId) {
1518
+ const ownSalt = this.getSessionSalt(agentName, sessionId);
1519
+ return {
1520
+ ownSalt,
1521
+ ownContribution: ownSalt
1522
+ ? this.ownSaltHalf(agentName, sessionId)
1523
+ : this.saltContributionFor(agentName, sessionId),
1524
+ };
1525
+ }
1526
+ /**
1527
+ * ONE mapping from a settled outcome to the operator-facing reason, so the send that WAITED and the
1528
+ * send that arrived afterwards cannot disagree about what happened.
1529
+ */
1530
+ reasonForOutcome(key, outcome) {
1531
+ if (outcome === "announce_failed")
1532
+ return UNSALTED_REASONS.ANNOUNCE_FAILED;
1533
+ if (outcome === "persist_failed")
1534
+ return UNSALTED_REASONS.OUR_PERSIST_FAILED;
1535
+ if (outcome === "closed")
1536
+ return this.peerClosedReason(key);
1537
+ return UNSALTED_REASONS.AGREEMENT_TIMED_OUT;
1538
+ }
1539
+ }
1540
+ //# sourceMappingURL=session-salts.js.map