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