@cello-protocol/daemon 0.0.225 → 0.0.227
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/dist/agent-admin-handlers.d.ts +0 -56
- package/dist/agent-admin-handlers.d.ts.map +0 -1
- package/dist/agent-admin-handlers.js +0 -73
- package/dist/agent-admin-handlers.js.map +0 -1
- package/dist/agent-handlers.d.ts +0 -72
- package/dist/agent-handlers.d.ts.map +0 -1
- package/dist/agent-handlers.js +0 -604
- package/dist/agent-handlers.js.map +0 -1
- package/dist/agent-id-migration.d.ts +0 -70
- package/dist/agent-id-migration.d.ts.map +0 -1
- package/dist/agent-id-migration.js +0 -468
- package/dist/agent-id-migration.js.map +0 -1
- package/dist/agent-loader.d.ts +0 -37
- package/dist/agent-loader.d.ts.map +0 -1
- package/dist/agent-loader.js +0 -37
- package/dist/agent-loader.js.map +0 -1
- package/dist/agent-selection-root.d.ts +0 -29
- package/dist/agent-selection-root.d.ts.map +0 -1
- package/dist/agent-selection-root.js +0 -126
- package/dist/agent-selection-root.js.map +0 -1
- package/dist/agent-selection.d.ts +0 -73
- package/dist/agent-selection.d.ts.map +0 -1
- package/dist/agent-selection.js +0 -80
- package/dist/agent-selection.js.map +0 -1
- package/dist/agent-settings-keys.d.ts +0 -52
- package/dist/agent-settings-keys.d.ts.map +0 -1
- package/dist/agent-settings-keys.js +0 -114
- package/dist/agent-settings-keys.js.map +0 -1
- package/dist/agent-state.d.ts +0 -67
- package/dist/agent-state.d.ts.map +0 -1
- package/dist/agent-state.js +0 -66
- package/dist/agent-state.js.map +0 -1
- package/dist/assignment-verify.d.ts +0 -90
- package/dist/assignment-verify.d.ts.map +0 -1
- package/dist/assignment-verify.js +0 -301
- package/dist/assignment-verify.js.map +0 -1
- package/dist/attendance-wiring.d.ts +0 -23
- package/dist/attendance-wiring.d.ts.map +0 -1
- package/dist/attendance-wiring.js +0 -248
- package/dist/attendance-wiring.js.map +0 -1
- package/dist/attestation-body.d.ts +0 -47
- package/dist/attestation-body.d.ts.map +0 -1
- package/dist/attestation-body.js +0 -71
- package/dist/attestation-body.js.map +0 -1
- package/dist/authorship-verification.d.ts +0 -91
- package/dist/authorship-verification.d.ts.map +0 -1
- package/dist/authorship-verification.js +0 -532
- package/dist/authorship-verification.js.map +0 -1
- package/dist/away-detection.d.ts +0 -139
- package/dist/away-detection.d.ts.map +0 -1
- package/dist/away-detection.js +0 -186
- package/dist/away-detection.js.map +0 -1
- package/dist/away-inbox-oneshot.d.ts +0 -40
- package/dist/away-inbox-oneshot.d.ts.map +0 -1
- package/dist/away-inbox-oneshot.js +0 -311
- package/dist/away-inbox-oneshot.js.map +0 -1
- package/dist/backup-restore-handlers.d.ts +0 -10
- package/dist/backup-restore-handlers.d.ts.map +0 -1
- package/dist/backup-restore-handlers.js +0 -82
- package/dist/backup-restore-handlers.js.map +0 -1
- package/dist/backup-restore.d.ts +0 -102
- package/dist/backup-restore.d.ts.map +0 -1
- package/dist/backup-restore.js +0 -341
- package/dist/backup-restore.js.map +0 -1
- package/dist/bin/cello-daemon.d.ts +0 -13
- package/dist/bin/cello-daemon.d.ts.map +0 -1
- package/dist/bin/cello-daemon.js.map +0 -1
- package/dist/boot-agents.d.ts +0 -64
- package/dist/boot-agents.d.ts.map +0 -1
- package/dist/boot-agents.js +0 -241
- package/dist/boot-agents.js.map +0 -1
- package/dist/boot-connection-state.d.ts +0 -38
- package/dist/boot-connection-state.d.ts.map +0 -1
- package/dist/boot-connection-state.js +0 -95
- package/dist/boot-connection-state.js.map +0 -1
- package/dist/boot-core.d.ts +0 -36
- package/dist/boot-core.d.ts.map +0 -1
- package/dist/boot-core.js +0 -293
- package/dist/boot-core.js.map +0 -1
- package/dist/boot-parked-content.d.ts +0 -34
- package/dist/boot-parked-content.d.ts.map +0 -1
- package/dist/boot-parked-content.js +0 -528
- package/dist/boot-parked-content.js.map +0 -1
- package/dist/boot-sweeps.d.ts +0 -22
- package/dist/boot-sweeps.d.ts.map +0 -1
- package/dist/boot-sweeps.js +0 -51
- package/dist/boot-sweeps.js.map +0 -1
- package/dist/bundled-consortium-manifest.d.ts +0 -47
- package/dist/bundled-consortium-manifest.d.ts.map +0 -1
- package/dist/bundled-consortium-manifest.js +0 -116
- package/dist/bundled-consortium-manifest.js.map +0 -1
- package/dist/cbor-blob-migration.d.ts +0 -8
- package/dist/cbor-blob-migration.d.ts.map +0 -1
- package/dist/cbor-blob-migration.js +0 -116
- package/dist/cbor-blob-migration.js.map +0 -1
- package/dist/challenge-verifier.d.ts +0 -12
- package/dist/challenge-verifier.d.ts.map +0 -1
- package/dist/challenge-verifier.js +0 -11
- package/dist/challenge-verifier.js.map +0 -1
- package/dist/close-commitment.d.ts +0 -67
- package/dist/close-commitment.d.ts.map +0 -1
- package/dist/close-commitment.js +0 -78
- package/dist/close-commitment.js.map +0 -1
- package/dist/close-session-handler.d.ts +0 -94
- package/dist/close-session-handler.d.ts.map +0 -1
- package/dist/close-session-handler.js +0 -1418
- package/dist/close-session-handler.js.map +0 -1
- package/dist/co-attendance.d.ts +0 -29
- package/dist/co-attendance.d.ts.map +0 -1
- package/dist/co-attendance.js +0 -32
- package/dist/co-attendance.js.map +0 -1
- package/dist/column-birth.d.ts +0 -40
- package/dist/column-birth.d.ts.map +0 -1
- package/dist/column-birth.js +0 -61
- package/dist/column-birth.js.map +0 -1
- package/dist/connect-or-start.d.ts +0 -25
- package/dist/connect-or-start.d.ts.map +0 -1
- package/dist/connect-or-start.js +0 -175
- package/dist/connect-or-start.js.map +0 -1
- package/dist/connection-agents.d.ts +0 -16
- package/dist/connection-agents.d.ts.map +0 -1
- package/dist/connection-agents.js +0 -68
- package/dist/connection-agents.js.map +0 -1
- package/dist/consent-migration.d.ts +0 -49
- package/dist/consent-migration.d.ts.map +0 -1
- package/dist/consent-migration.js +0 -128
- package/dist/consent-migration.js.map +0 -1
- package/dist/consortium-bootstrap.d.ts +0 -138
- package/dist/consortium-bootstrap.d.ts.map +0 -1
- package/dist/consortium-bootstrap.js +0 -339
- package/dist/consortium-bootstrap.js.map +0 -1
- package/dist/consortium-fingerprint.d.ts +0 -115
- package/dist/consortium-fingerprint.d.ts.map +0 -1
- package/dist/consortium-fingerprint.js +0 -175
- package/dist/consortium-fingerprint.js.map +0 -1
- package/dist/contact-handlers.d.ts +0 -59
- package/dist/contact-handlers.d.ts.map +0 -1
- package/dist/contact-handlers.js +0 -343
- package/dist/contact-handlers.js.map +0 -1
- package/dist/contact-pubkey-case.d.ts +0 -65
- package/dist/contact-pubkey-case.d.ts.map +0 -1
- package/dist/contact-pubkey-case.js +0 -136
- package/dist/contact-pubkey-case.js.map +0 -1
- package/dist/contacts-tier-migration.d.ts +0 -90
- package/dist/contacts-tier-migration.d.ts.map +0 -1
- package/dist/contacts-tier-migration.js +0 -150
- package/dist/contacts-tier-migration.js.map +0 -1
- package/dist/content-encryption-status.d.ts +0 -111
- package/dist/content-encryption-status.d.ts.map +0 -1
- package/dist/content-encryption-status.js +0 -158
- package/dist/content-encryption-status.js.map +0 -1
- package/dist/content-park-client.d.ts +0 -90
- package/dist/content-park-client.d.ts.map +0 -1
- package/dist/content-park-client.js +0 -362
- package/dist/content-park-client.js.map +0 -1
- package/dist/content-park.d.ts +0 -52
- package/dist/content-park.d.ts.map +0 -1
- package/dist/content-park.js +0 -1309
- package/dist/content-park.js.map +0 -1
- package/dist/cross-node-negotiation.d.ts +0 -44
- package/dist/cross-node-negotiation.d.ts.map +0 -1
- package/dist/cross-node-negotiation.js +0 -34
- package/dist/cross-node-negotiation.js.map +0 -1
- package/dist/daemon-handle.d.ts +0 -57
- package/dist/daemon-handle.d.ts.map +0 -1
- package/dist/daemon-handle.js +0 -2
- package/dist/daemon-handle.js.map +0 -1
- package/dist/daemon-status-report.d.ts +0 -50
- package/dist/daemon-status-report.d.ts.map +0 -1
- package/dist/daemon-status-report.js +0 -81
- package/dist/daemon-status-report.js.map +0 -1
- package/dist/daemon.d.ts +0 -45
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js +0 -1177
- package/dist/daemon.js.map +0 -1
- package/dist/db-identity-store.d.ts +0 -159
- package/dist/db-identity-store.d.ts.map +0 -1
- package/dist/db-identity-store.js +0 -502
- package/dist/db-identity-store.js.map +0 -1
- package/dist/delivery-open-registry.d.ts +0 -92
- package/dist/delivery-open-registry.d.ts.map +0 -1
- package/dist/delivery-open-registry.js +0 -121
- package/dist/delivery-open-registry.js.map +0 -1
- package/dist/delivery-session-suspects.d.ts +0 -56
- package/dist/delivery-session-suspects.d.ts.map +0 -1
- package/dist/delivery-session-suspects.js +0 -94
- package/dist/delivery-session-suspects.js.map +0 -1
- package/dist/directory-auth-posture.d.ts +0 -87
- package/dist/directory-auth-posture.d.ts.map +0 -1
- package/dist/directory-auth-posture.js +0 -134
- package/dist/directory-auth-posture.js.map +0 -1
- package/dist/directory-bootstrap.d.ts +0 -310
- package/dist/directory-bootstrap.d.ts.map +0 -1
- package/dist/directory-bootstrap.js +0 -557
- package/dist/directory-bootstrap.js.map +0 -1
- package/dist/directory-connect.d.ts +0 -27
- package/dist/directory-connect.d.ts.map +0 -1
- package/dist/directory-connect.js +0 -106
- package/dist/directory-connect.js.map +0 -1
- package/dist/disconnect-cleanup.d.ts +0 -51
- package/dist/disconnect-cleanup.d.ts.map +0 -1
- package/dist/disconnect-cleanup.js +0 -72
- package/dist/disconnect-cleanup.js.map +0 -1
- package/dist/document-amendment-store.d.ts +0 -120
- package/dist/document-amendment-store.d.ts.map +0 -1
- package/dist/document-amendment-store.js +0 -266
- package/dist/document-amendment-store.js.map +0 -1
- package/dist/document-delivery-transport.d.ts +0 -168
- package/dist/document-delivery-transport.d.ts.map +0 -1
- package/dist/document-delivery-transport.js +0 -206
- package/dist/document-delivery-transport.js.map +0 -1
- package/dist/document-engine.d.ts +0 -134
- package/dist/document-engine.d.ts.map +0 -1
- package/dist/document-engine.js +0 -282
- package/dist/document-engine.js.map +0 -1
- package/dist/document-flag.d.ts +0 -58
- package/dist/document-flag.d.ts.map +0 -1
- package/dist/document-flag.js +0 -70
- package/dist/document-flag.js.map +0 -1
- package/dist/document-frame-router.d.ts +0 -245
- package/dist/document-frame-router.d.ts.map +0 -1
- package/dist/document-frame-router.js +0 -397
- package/dist/document-frame-router.js.map +0 -1
- package/dist/document-gate-wiring.d.ts +0 -61
- package/dist/document-gate-wiring.d.ts.map +0 -1
- package/dist/document-gate-wiring.js +0 -125
- package/dist/document-gate-wiring.js.map +0 -1
- package/dist/document-gate.d.ts +0 -149
- package/dist/document-gate.d.ts.map +0 -1
- package/dist/document-gate.js +0 -509
- package/dist/document-gate.js.map +0 -1
- package/dist/document-handlers.d.ts +0 -47
- package/dist/document-handlers.d.ts.map +0 -1
- package/dist/document-handlers.js +0 -2203
- package/dist/document-handlers.js.map +0 -1
- package/dist/document-handshake.d.ts +0 -176
- package/dist/document-handshake.d.ts.map +0 -1
- package/dist/document-handshake.js +0 -452
- package/dist/document-handshake.js.map +0 -1
- package/dist/document-inbound.d.ts +0 -162
- package/dist/document-inbound.d.ts.map +0 -1
- package/dist/document-inbound.js +0 -530
- package/dist/document-inbound.js.map +0 -1
- package/dist/document-json.d.ts +0 -120
- package/dist/document-json.d.ts.map +0 -1
- package/dist/document-json.js +0 -191
- package/dist/document-json.js.map +0 -1
- package/dist/document-layer.d.ts +0 -215
- package/dist/document-layer.d.ts.map +0 -1
- package/dist/document-layer.js +0 -1025
- package/dist/document-layer.js.map +0 -1
- package/dist/document-lifecycle.d.ts +0 -52
- package/dist/document-lifecycle.d.ts.map +0 -1
- package/dist/document-lifecycle.js +0 -134
- package/dist/document-lifecycle.js.map +0 -1
- package/dist/document-live-docs.d.ts +0 -58
- package/dist/document-live-docs.d.ts.map +0 -1
- package/dist/document-live-docs.js +0 -126
- package/dist/document-live-docs.js.map +0 -1
- package/dist/document-notify.d.ts +0 -228
- package/dist/document-notify.d.ts.map +0 -1
- package/dist/document-notify.js +0 -580
- package/dist/document-notify.js.map +0 -1
- package/dist/document-profile.d.ts +0 -61
- package/dist/document-profile.d.ts.map +0 -1
- package/dist/document-profile.js +0 -112
- package/dist/document-profile.js.map +0 -1
- package/dist/document-publish.d.ts +0 -87
- package/dist/document-publish.d.ts.map +0 -1
- package/dist/document-publish.js +0 -173
- package/dist/document-publish.js.map +0 -1
- package/dist/document-reachability.d.ts +0 -42
- package/dist/document-reachability.d.ts.map +0 -1
- package/dist/document-reachability.js +0 -80
- package/dist/document-reachability.js.map +0 -1
- package/dist/document-reconcile-engine.d.ts +0 -66
- package/dist/document-reconcile-engine.d.ts.map +0 -1
- package/dist/document-reconcile-engine.js +0 -225
- package/dist/document-reconcile-engine.js.map +0 -1
- package/dist/document-reconcile-scheduler.d.ts +0 -163
- package/dist/document-reconcile-scheduler.d.ts.map +0 -1
- package/dist/document-reconcile-scheduler.js +0 -303
- package/dist/document-reconcile-scheduler.js.map +0 -1
- package/dist/document-rejection.d.ts +0 -251
- package/dist/document-rejection.d.ts.map +0 -1
- package/dist/document-rejection.js +0 -435
- package/dist/document-rejection.js.map +0 -1
- package/dist/document-screen.d.ts +0 -114
- package/dist/document-screen.d.ts.map +0 -1
- package/dist/document-screen.js +0 -223
- package/dist/document-screen.js.map +0 -1
- package/dist/document-store.d.ts +0 -372
- package/dist/document-store.d.ts.map +0 -1
- package/dist/document-store.js +0 -931
- package/dist/document-store.js.map +0 -1
- package/dist/document-surface.d.ts +0 -32
- package/dist/document-surface.d.ts.map +0 -1
- package/dist/document-surface.js +0 -151
- package/dist/document-surface.js.map +0 -1
- package/dist/document-types.d.ts +0 -94
- package/dist/document-types.d.ts.map +0 -1
- package/dist/document-types.js +0 -90
- package/dist/document-types.js.map +0 -1
- package/dist/document-watch.d.ts +0 -69
- package/dist/document-watch.d.ts.map +0 -1
- package/dist/document-watch.js +0 -108
- package/dist/document-watch.js.map +0 -1
- package/dist/document-wiring.d.ts +0 -53
- package/dist/document-wiring.d.ts.map +0 -1
- package/dist/document-wiring.js +0 -323
- package/dist/document-wiring.js.map +0 -1
- package/dist/document-write-guard.d.ts +0 -66
- package/dist/document-write-guard.d.ts.map +0 -1
- package/dist/document-write-guard.js +0 -98
- package/dist/document-write-guard.js.map +0 -1
- package/dist/document-write-path.d.ts +0 -94
- package/dist/document-write-path.d.ts.map +0 -1
- package/dist/document-write-path.js +0 -530
- package/dist/document-write-path.js.map +0 -1
- package/dist/error-message.d.ts +0 -7
- package/dist/error-message.d.ts.map +0 -1
- package/dist/error-message.js +0 -19
- package/dist/error-message.js.map +0 -1
- package/dist/file-manifest-provider.d.ts +0 -37
- package/dist/file-manifest-provider.d.ts.map +0 -1
- package/dist/file-manifest-provider.js +0 -105
- package/dist/file-manifest-provider.js.map +0 -1
- package/dist/frame-values.d.ts +0 -3
- package/dist/frame-values.d.ts.map +0 -1
- package/dist/frame-values.js +0 -84
- package/dist/frame-values.js.map +0 -1
- package/dist/frontier-mismatch.d.ts +0 -73
- package/dist/frontier-mismatch.d.ts.map +0 -1
- package/dist/frontier-mismatch.js +0 -89
- package/dist/frontier-mismatch.js.map +0 -1
- package/dist/gateway-config-handlers.d.ts +0 -26
- package/dist/gateway-config-handlers.d.ts.map +0 -1
- package/dist/gateway-config-handlers.js +0 -429
- package/dist/gateway-config-handlers.js.map +0 -1
- package/dist/held-content.d.ts +0 -145
- package/dist/held-content.d.ts.map +0 -1
- package/dist/held-content.js +0 -389
- package/dist/held-content.js.map +0 -1
- package/dist/http-manifest-poll.d.ts +0 -67
- package/dist/http-manifest-poll.d.ts.map +0 -1
- package/dist/http-manifest-poll.js +0 -150
- package/dist/http-manifest-poll.js.map +0 -1
- package/dist/identity-migration.d.ts +0 -40
- package/dist/identity-migration.d.ts.map +0 -1
- package/dist/identity-migration.js +0 -461
- package/dist/identity-migration.js.map +0 -1
- package/dist/inbound-refusals.d.ts +0 -283
- package/dist/inbound-refusals.d.ts.map +0 -1
- package/dist/inbound-refusals.js +0 -919
- package/dist/inbound-refusals.js.map +0 -1
- package/dist/inbound-seal-request.d.ts +0 -32
- package/dist/inbound-seal-request.d.ts.map +0 -1
- package/dist/inbound-seal-request.js +0 -228
- package/dist/inbound-seal-request.js.map +0 -1
- package/dist/inbound-sessions.d.ts +0 -291
- package/dist/inbound-sessions.d.ts.map +0 -1
- package/dist/inbound-sessions.js +0 -1609
- package/dist/inbound-sessions.js.map +0 -1
- package/dist/inclusion-proof-handlers.d.ts +0 -43
- package/dist/inclusion-proof-handlers.d.ts.map +0 -1
- package/dist/inclusion-proof-handlers.js +0 -565
- package/dist/inclusion-proof-handlers.js.map +0 -1
- package/dist/inclusion-proof.d.ts +0 -151
- package/dist/inclusion-proof.d.ts.map +0 -1
- package/dist/inclusion-proof.js +0 -228
- package/dist/inclusion-proof.js.map +0 -1
- package/dist/index.d.ts +0 -32
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/initiate-session-handler.d.ts +0 -55
- package/dist/initiate-session-handler.d.ts.map +0 -1
- package/dist/initiate-session-handler.js +0 -310
- package/dist/initiate-session-handler.js.map +0 -1
- package/dist/ipc-client.d.ts +0 -31
- package/dist/ipc-client.d.ts.map +0 -1
- package/dist/ipc-client.js +0 -113
- package/dist/ipc-client.js.map +0 -1
- package/dist/ipc-server.d.ts +0 -63
- package/dist/ipc-server.d.ts.map +0 -1
- package/dist/ipc-server.js +0 -429
- package/dist/ipc-server.js.map +0 -1
- package/dist/ipc-surface.d.ts +0 -44
- package/dist/ipc-surface.d.ts.map +0 -1
- package/dist/ipc-surface.js +0 -107
- package/dist/ipc-surface.js.map +0 -1
- package/dist/line-lcs.d.ts +0 -51
- package/dist/line-lcs.d.ts.map +0 -1
- package/dist/line-lcs.js +0 -71
- package/dist/line-lcs.js.map +0 -1
- package/dist/lock-file.d.ts +0 -39
- package/dist/lock-file.d.ts.map +0 -1
- package/dist/lock-file.js +0 -120
- package/dist/lock-file.js.map +0 -1
- package/dist/log-collapse.d.ts +0 -66
- package/dist/log-collapse.d.ts.map +0 -1
- package/dist/log-collapse.js +0 -244
- package/dist/log-collapse.js.map +0 -1
- package/dist/log-rotate.d.ts +0 -67
- package/dist/log-rotate.d.ts.map +0 -1
- package/dist/log-rotate.js +0 -134
- package/dist/log-rotate.js.map +0 -1
- package/dist/manifest-deps.d.ts +0 -25
- package/dist/manifest-deps.d.ts.map +0 -1
- package/dist/manifest-deps.js +0 -151
- package/dist/manifest-deps.js.map +0 -1
- package/dist/manifest-poll-scheduler.d.ts +0 -31
- package/dist/manifest-poll-scheduler.d.ts.map +0 -1
- package/dist/manifest-poll-scheduler.js +0 -59
- package/dist/manifest-poll-scheduler.js.map +0 -1
- package/dist/manifest-validity.d.ts +0 -153
- package/dist/manifest-validity.d.ts.map +0 -1
- package/dist/manifest-validity.js +0 -268
- package/dist/manifest-validity.js.map +0 -1
- package/dist/manifest-version-store-db.d.ts +0 -24
- package/dist/manifest-version-store-db.d.ts.map +0 -1
- package/dist/manifest-version-store-db.js +0 -58
- package/dist/manifest-version-store-db.js.map +0 -1
- package/dist/manifest-version-store.d.ts +0 -16
- package/dist/manifest-version-store.d.ts.map +0 -1
- package/dist/manifest-version-store.js +0 -15
- package/dist/manifest-version-store.js.map +0 -1
- package/dist/network-directory-node.d.ts +0 -136
- package/dist/network-directory-node.d.ts.map +0 -1
- package/dist/network-directory-node.js +0 -810
- package/dist/network-directory-node.js.map +0 -1
- package/dist/nonce-dedup.d.ts +0 -68
- package/dist/nonce-dedup.d.ts.map +0 -1
- package/dist/nonce-dedup.js +0 -205
- package/dist/nonce-dedup.js.map +0 -1
- package/dist/notification-dispatcher.d.ts +0 -92
- package/dist/notification-dispatcher.d.ts.map +0 -1
- package/dist/notification-dispatcher.js +0 -210
- package/dist/notification-dispatcher.js.map +0 -1
- package/dist/notification-handlers.d.ts +0 -53
- package/dist/notification-handlers.d.ts.map +0 -1
- package/dist/notification-handlers.js +0 -461
- package/dist/notification-handlers.js.map +0 -1
- package/dist/onboarding-guidance.d.ts +0 -79
- package/dist/onboarding-guidance.d.ts.map +0 -1
- package/dist/onboarding-guidance.js +0 -95
- package/dist/onboarding-guidance.js.map +0 -1
- package/dist/operator-guidance.d.ts +0 -25
- package/dist/operator-guidance.d.ts.map +0 -1
- package/dist/operator-guidance.js +0 -50
- package/dist/operator-guidance.js.map +0 -1
- package/dist/orphan-triage.d.ts +0 -130
- package/dist/orphan-triage.d.ts.map +0 -1
- package/dist/orphan-triage.js +0 -207
- package/dist/orphan-triage.js.map +0 -1
- package/dist/outbound-sessions.d.ts +0 -126
- package/dist/outbound-sessions.d.ts.map +0 -1
- package/dist/outbound-sessions.js +0 -1052
- package/dist/outbound-sessions.js.map +0 -1
- package/dist/park-envelope.d.ts +0 -329
- package/dist/park-envelope.d.ts.map +0 -1
- package/dist/park-envelope.js +0 -509
- package/dist/park-envelope.js.map +0 -1
- package/dist/park-recovery.d.ts +0 -273
- package/dist/park-recovery.d.ts.map +0 -1
- package/dist/park-recovery.js +0 -717
- package/dist/park-recovery.js.map +0 -1
- package/dist/park-refusals.d.ts +0 -147
- package/dist/park-refusals.d.ts.map +0 -1
- package/dist/park-refusals.js +0 -331
- package/dist/park-refusals.js.map +0 -1
- package/dist/quarantine-framing.d.ts +0 -92
- package/dist/quarantine-framing.d.ts.map +0 -1
- package/dist/quarantine-framing.js +0 -111
- package/dist/quarantine-framing.js.map +0 -1
- package/dist/reconnect-drain.d.ts +0 -22
- package/dist/reconnect-drain.d.ts.map +0 -1
- package/dist/reconnect-drain.js +0 -65
- package/dist/reconnect-drain.js.map +0 -1
- package/dist/recovered-position.d.ts +0 -14
- package/dist/recovered-position.d.ts.map +0 -1
- package/dist/recovered-position.js +0 -59
- package/dist/recovered-position.js.map +0 -1
- package/dist/refusal-notices.d.ts +0 -196
- package/dist/refusal-notices.d.ts.map +0 -1
- package/dist/refusal-notices.js +0 -516
- package/dist/refusal-notices.js.map +0 -1
- package/dist/refusal-reasons.d.ts +0 -245
- package/dist/refusal-reasons.d.ts.map +0 -1
- package/dist/refusal-reasons.js +0 -363
- package/dist/refusal-reasons.js.map +0 -1
- package/dist/register-handler.d.ts +0 -36
- package/dist/register-handler.d.ts.map +0 -1
- package/dist/register-handler.js +0 -285
- package/dist/register-handler.js.map +0 -1
- package/dist/registration-context.d.ts +0 -72
- package/dist/registration-context.d.ts.map +0 -1
- package/dist/registration-context.js +0 -126
- package/dist/registration-context.js.map +0 -1
- package/dist/registration-manager.d.ts +0 -94
- package/dist/registration-manager.d.ts.map +0 -1
- package/dist/registration-manager.js +0 -585
- package/dist/registration-manager.js.map +0 -1
- package/dist/registration-persistence.d.ts +0 -183
- package/dist/registration-persistence.d.ts.map +0 -1
- package/dist/registration-persistence.js +0 -263
- package/dist/registration-persistence.js.map +0 -1
- package/dist/registry-poll.d.ts +0 -52
- package/dist/registry-poll.d.ts.map +0 -1
- package/dist/registry-poll.js +0 -140
- package/dist/registry-poll.js.map +0 -1
- package/dist/registry-version-store-db.d.ts +0 -22
- package/dist/registry-version-store-db.d.ts.map +0 -1
- package/dist/registry-version-store-db.js +0 -50
- package/dist/registry-version-store-db.js.map +0 -1
- package/dist/relay-endpoints.d.ts +0 -18
- package/dist/relay-endpoints.d.ts.map +0 -1
- package/dist/relay-endpoints.js +0 -9
- package/dist/relay-endpoints.js.map +0 -1
- package/dist/relay-only.d.ts +0 -140
- package/dist/relay-only.d.ts.map +0 -1
- package/dist/relay-only.js +0 -193
- package/dist/relay-only.js.map +0 -1
- package/dist/relay-receipt-store.d.ts +0 -155
- package/dist/relay-receipt-store.d.ts.map +0 -1
- package/dist/relay-receipt-store.js +0 -284
- package/dist/relay-receipt-store.js.map +0 -1
- package/dist/relay-reconnect.d.ts +0 -32
- package/dist/relay-reconnect.d.ts.map +0 -1
- package/dist/relay-reconnect.js +0 -29
- package/dist/relay-reconnect.js.map +0 -1
- package/dist/reply-lag.d.ts +0 -11
- package/dist/reply-lag.d.ts.map +0 -1
- package/dist/reply-lag.js +0 -46
- package/dist/reply-lag.js.map +0 -1
- package/dist/resolve-named-agent.d.ts +0 -49
- package/dist/resolve-named-agent.d.ts.map +0 -1
- package/dist/resolve-named-agent.js +0 -77
- package/dist/resolve-named-agent.js.map +0 -1
- package/dist/restart-seal-resolver.d.ts +0 -110
- package/dist/restart-seal-resolver.d.ts.map +0 -1
- package/dist/restart-seal-resolver.js +0 -353
- package/dist/restart-seal-resolver.js.map +0 -1
- package/dist/resume-last-seen.d.ts +0 -15
- package/dist/resume-last-seen.d.ts.map +0 -1
- package/dist/resume-last-seen.js +0 -44
- package/dist/resume-last-seen.js.map +0 -1
- package/dist/retry-queue.d.ts +0 -203
- package/dist/retry-queue.d.ts.map +0 -1
- package/dist/retry-queue.js +0 -701
- package/dist/retry-queue.js.map +0 -1
- package/dist/roster-freshness.d.ts +0 -160
- package/dist/roster-freshness.d.ts.map +0 -1
- package/dist/roster-freshness.js +0 -250
- package/dist/roster-freshness.js.map +0 -1
- package/dist/screening-status.d.ts +0 -15
- package/dist/screening-status.d.ts.map +0 -1
- package/dist/screening-status.js +0 -65
- package/dist/screening-status.js.map +0 -1
- package/dist/seal-carried-close.d.ts +0 -37
- package/dist/seal-carried-close.d.ts.map +0 -1
- package/dist/seal-carried-close.js +0 -179
- package/dist/seal-carried-close.js.map +0 -1
- package/dist/seal-certificate-pull.d.ts +0 -79
- package/dist/seal-certificate-pull.d.ts.map +0 -1
- package/dist/seal-certificate-pull.js +0 -186
- package/dist/seal-certificate-pull.js.map +0 -1
- package/dist/seal-certified-root-check.d.ts +0 -37
- package/dist/seal-certified-root-check.d.ts.map +0 -1
- package/dist/seal-certified-root-check.js +0 -127
- package/dist/seal-certified-root-check.js.map +0 -1
- package/dist/seal-coordinator.d.ts +0 -118
- package/dist/seal-coordinator.d.ts.map +0 -1
- package/dist/seal-coordinator.js +0 -927
- package/dist/seal-coordinator.js.map +0 -1
- package/dist/seal-escalation.d.ts +0 -80
- package/dist/seal-escalation.d.ts.map +0 -1
- package/dist/seal-escalation.js +0 -283
- package/dist/seal-escalation.js.map +0 -1
- package/dist/seal-evidence-root-check.d.ts +0 -70
- package/dist/seal-evidence-root-check.d.ts.map +0 -1
- package/dist/seal-evidence-root-check.js +0 -199
- package/dist/seal-evidence-root-check.js.map +0 -1
- package/dist/seal-failure-store.d.ts +0 -129
- package/dist/seal-failure-store.d.ts.map +0 -1
- package/dist/seal-failure-store.js +0 -173
- package/dist/seal-failure-store.js.map +0 -1
- package/dist/seal-flows.d.ts +0 -107
- package/dist/seal-flows.d.ts.map +0 -1
- package/dist/seal-flows.js +0 -638
- package/dist/seal-flows.js.map +0 -1
- package/dist/seal-frontier-verify.d.ts +0 -103
- package/dist/seal-frontier-verify.d.ts.map +0 -1
- package/dist/seal-frontier-verify.js +0 -143
- package/dist/seal-frontier-verify.js.map +0 -1
- package/dist/seal-leaf.d.ts +0 -58
- package/dist/seal-leaf.d.ts.map +0 -1
- package/dist/seal-leaf.js +0 -112
- package/dist/seal-leaf.js.map +0 -1
- package/dist/seal-legibility-tbs.d.ts +0 -25
- package/dist/seal-legibility-tbs.d.ts.map +0 -1
- package/dist/seal-legibility-tbs.js +0 -77
- package/dist/seal-legibility-tbs.js.map +0 -1
- package/dist/seal-local-terminus.d.ts +0 -41
- package/dist/seal-local-terminus.d.ts.map +0 -1
- package/dist/seal-local-terminus.js +0 -170
- package/dist/seal-local-terminus.js.map +0 -1
- package/dist/seal-receipt-upgrade.d.ts +0 -30
- package/dist/seal-receipt-upgrade.d.ts.map +0 -1
- package/dist/seal-receipt-upgrade.js +0 -49
- package/dist/seal-receipt-upgrade.js.map +0 -1
- package/dist/seal-relay-silence.d.ts +0 -51
- package/dist/seal-relay-silence.d.ts.map +0 -1
- package/dist/seal-relay-silence.js +0 -76
- package/dist/seal-relay-silence.js.map +0 -1
- package/dist/seal-settle.d.ts +0 -38
- package/dist/seal-settle.d.ts.map +0 -1
- package/dist/seal-settle.js +0 -33
- package/dist/seal-settle.js.map +0 -1
- package/dist/seal-upgrade.d.ts +0 -107
- package/dist/seal-upgrade.d.ts.map +0 -1
- package/dist/seal-upgrade.js +0 -200
- package/dist/seal-upgrade.js.map +0 -1
- package/dist/sealed-conversation.d.ts +0 -36
- package/dist/sealed-conversation.d.ts.map +0 -1
- package/dist/sealed-conversation.js +0 -171
- package/dist/sealed-conversation.js.map +0 -1
- package/dist/sealed-leaf-set.d.ts +0 -92
- package/dist/sealed-leaf-set.d.ts.map +0 -1
- package/dist/sealed-leaf-set.js +0 -122
- package/dist/sealed-leaf-set.js.map +0 -1
- package/dist/send-claims.d.ts +0 -56
- package/dist/send-claims.d.ts.map +0 -1
- package/dist/send-claims.js +0 -50
- package/dist/send-claims.js.map +0 -1
- package/dist/session-assignment-parser.d.ts +0 -98
- package/dist/session-assignment-parser.d.ts.map +0 -1
- package/dist/session-assignment-parser.js +0 -325
- package/dist/session-assignment-parser.js.map +0 -1
- package/dist/session-category.d.ts +0 -19
- package/dist/session-category.d.ts.map +0 -1
- package/dist/session-category.js +0 -14
- package/dist/session-category.js.map +0 -1
- package/dist/session-ceremony.d.ts +0 -297
- package/dist/session-ceremony.d.ts.map +0 -1
- package/dist/session-ceremony.js +0 -964
- package/dist/session-ceremony.js.map +0 -1
- package/dist/session-closed.d.ts +0 -79
- package/dist/session-closed.d.ts.map +0 -1
- package/dist/session-closed.js +0 -189
- package/dist/session-closed.js.map +0 -1
- package/dist/session-connection-gater.d.ts +0 -148
- package/dist/session-connection-gater.d.ts.map +0 -1
- package/dist/session-connection-gater.js +0 -332
- package/dist/session-connection-gater.js.map +0 -1
- package/dist/session-content-context.d.ts +0 -162
- package/dist/session-content-context.d.ts.map +0 -1
- package/dist/session-content-context.js +0 -2
- package/dist/session-content-context.js.map +0 -1
- package/dist/session-content-handlers.d.ts +0 -82
- package/dist/session-content-handlers.d.ts.map +0 -1
- package/dist/session-content-handlers.js +0 -1382
- package/dist/session-content-handlers.js.map +0 -1
- package/dist/session-content-ingest.d.ts +0 -206
- package/dist/session-content-ingest.d.ts.map +0 -1
- package/dist/session-content-ingest.js +0 -2167
- package/dist/session-content-ingest.js.map +0 -1
- package/dist/session-content-send.d.ts +0 -191
- package/dist/session-content-send.d.ts.map +0 -1
- package/dist/session-content-send.js +0 -1360
- package/dist/session-content-send.js.map +0 -1
- package/dist/session-delivery-acks.d.ts +0 -169
- package/dist/session-delivery-acks.d.ts.map +0 -1
- package/dist/session-delivery-acks.js +0 -569
- package/dist/session-delivery-acks.js.map +0 -1
- package/dist/session-ephemerals.d.ts +0 -279
- package/dist/session-ephemerals.d.ts.map +0 -1
- package/dist/session-ephemerals.js +0 -591
- package/dist/session-ephemerals.js.map +0 -1
- package/dist/session-leaf-records.d.ts +0 -159
- package/dist/session-leaf-records.d.ts.map +0 -1
- package/dist/session-leaf-records.js +0 -408
- package/dist/session-leaf-records.js.map +0 -1
- package/dist/session-lifecycle.d.ts +0 -303
- package/dist/session-lifecycle.d.ts.map +0 -1
- package/dist/session-lifecycle.js +0 -1679
- package/dist/session-lifecycle.js.map +0 -1
- package/dist/session-liveness.d.ts +0 -135
- package/dist/session-liveness.d.ts.map +0 -1
- package/dist/session-liveness.js +0 -347
- package/dist/session-liveness.js.map +0 -1
- package/dist/session-name.d.ts +0 -35
- package/dist/session-name.d.ts.map +0 -1
- package/dist/session-name.js +0 -60
- package/dist/session-name.js.map +0 -1
- package/dist/session-node-factory.d.ts +0 -18
- package/dist/session-node-factory.d.ts.map +0 -1
- package/dist/session-node-factory.js +0 -182
- package/dist/session-node-factory.js.map +0 -1
- package/dist/session-node-manager.d.ts +0 -875
- package/dist/session-node-manager.d.ts.map +0 -1
- package/dist/session-node-manager.js +0 -2995
- package/dist/session-node-manager.js.map +0 -1
- package/dist/session-node-types.d.ts +0 -1057
- package/dist/session-node-types.d.ts.map +0 -1
- package/dist/session-node-types.js +0 -657
- package/dist/session-node-types.js.map +0 -1
- package/dist/session-notify.d.ts +0 -46
- package/dist/session-notify.d.ts.map +0 -1
- package/dist/session-notify.js +0 -116
- package/dist/session-notify.js.map +0 -1
- package/dist/session-own-chain-store.d.ts +0 -65
- package/dist/session-own-chain-store.d.ts.map +0 -1
- package/dist/session-own-chain-store.js +0 -75
- package/dist/session-own-chain-store.js.map +0 -1
- package/dist/session-queries.d.ts +0 -476
- package/dist/session-queries.d.ts.map +0 -1
- package/dist/session-queries.js +0 -1007
- package/dist/session-queries.js.map +0 -1
- package/dist/session-read-handlers.d.ts +0 -87
- package/dist/session-read-handlers.d.ts.map +0 -1
- package/dist/session-read-handlers.js +0 -682
- package/dist/session-read-handlers.js.map +0 -1
- package/dist/session-records.d.ts +0 -341
- package/dist/session-records.d.ts.map +0 -1
- package/dist/session-records.js +0 -858
- package/dist/session-records.js.map +0 -1
- package/dist/session-relay-client.d.ts +0 -659
- package/dist/session-relay-client.d.ts.map +0 -1
- package/dist/session-relay-client.js +0 -2876
- package/dist/session-relay-client.js.map +0 -1
- package/dist/session-relay.d.ts +0 -397
- package/dist/session-relay.d.ts.map +0 -1
- package/dist/session-relay.js +0 -1636
- package/dist/session-relay.js.map +0 -1
- package/dist/session-salt-agreement.d.ts +0 -331
- package/dist/session-salt-agreement.d.ts.map +0 -1
- package/dist/session-salt-agreement.js +0 -472
- package/dist/session-salt-agreement.js.map +0 -1
- package/dist/session-salts.d.ts +0 -432
- package/dist/session-salts.d.ts.map +0 -1
- package/dist/session-salts.js +0 -1540
- package/dist/session-salts.js.map +0 -1
- package/dist/session-schema.d.ts +0 -30
- package/dist/session-schema.d.ts.map +0 -1
- package/dist/session-schema.js +0 -877
- package/dist/session-schema.js.map +0 -1
- package/dist/session-seal-leaf-store.d.ts +0 -70
- package/dist/session-seal-leaf-store.d.ts.map +0 -1
- package/dist/session-seal-leaf-store.js +0 -105
- package/dist/session-seal-leaf-store.js.map +0 -1
- package/dist/session-seal.d.ts +0 -334
- package/dist/session-seal.d.ts.map +0 -1
- package/dist/session-seal.js +0 -1017
- package/dist/session-seal.js.map +0 -1
- package/dist/session-terminal-refusal.d.ts +0 -65
- package/dist/session-terminal-refusal.d.ts.map +0 -1
- package/dist/session-terminal-refusal.js +0 -87
- package/dist/session-terminal-refusal.js.map +0 -1
- package/dist/session-tree.d.ts +0 -110
- package/dist/session-tree.d.ts.map +0 -1
- package/dist/session-tree.js +0 -144
- package/dist/session-tree.js.map +0 -1
- package/dist/session-views.d.ts +0 -47
- package/dist/session-views.d.ts.map +0 -1
- package/dist/session-views.js +0 -278
- package/dist/session-views.js.map +0 -1
- package/dist/signal-handlers.d.ts +0 -63
- package/dist/signal-handlers.d.ts.map +0 -1
- package/dist/signal-handlers.js +0 -980
- package/dist/signal-handlers.js.map +0 -1
- package/dist/signal-requirement-policy.d.ts +0 -51
- package/dist/signal-requirement-policy.d.ts.map +0 -1
- package/dist/signal-requirement-policy.js +0 -89
- package/dist/signal-requirement-policy.js.map +0 -1
- package/dist/signal-revocability.d.ts +0 -49
- package/dist/signal-revocability.d.ts.map +0 -1
- package/dist/signal-revocability.js +0 -93
- package/dist/signal-revocability.js.map +0 -1
- package/dist/signal-submission.d.ts +0 -181
- package/dist/signal-submission.d.ts.map +0 -1
- package/dist/signal-submission.js +0 -368
- package/dist/signal-submission.js.map +0 -1
- package/dist/signaling-connect.d.ts +0 -118
- package/dist/signaling-connect.d.ts.map +0 -1
- package/dist/signaling-connect.js +0 -538
- package/dist/signaling-connect.js.map +0 -1
- package/dist/signaling-wiring.d.ts +0 -105
- package/dist/signaling-wiring.d.ts.map +0 -1
- package/dist/signaling-wiring.js +0 -393
- package/dist/signaling-wiring.js.map +0 -1
- package/dist/singleton-lock.d.ts +0 -85
- package/dist/singleton-lock.d.ts.map +0 -1
- package/dist/singleton-lock.js +0 -219
- package/dist/singleton-lock.js.map +0 -1
- package/dist/sqlcipher-db.d.ts +0 -139
- package/dist/sqlcipher-db.d.ts.map +0 -1
- package/dist/sqlcipher-db.js +0 -357
- package/dist/sqlcipher-db.js.map +0 -1
- package/dist/standing-receivers.d.ts +0 -319
- package/dist/standing-receivers.d.ts.map +0 -1
- package/dist/standing-receivers.js +0 -1295
- package/dist/standing-receivers.js.map +0 -1
- package/dist/start-agent.d.ts +0 -64
- package/dist/start-agent.d.ts.map +0 -1
- package/dist/start-agent.js +0 -136
- package/dist/start-agent.js.map +0 -1
- package/dist/status-handler.d.ts +0 -54
- package/dist/status-handler.d.ts.map +0 -1
- package/dist/status-handler.js +0 -61
- package/dist/status-handler.js.map +0 -1
- package/dist/submission-retry.d.ts +0 -208
- package/dist/submission-retry.d.ts.map +0 -1
- package/dist/submission-retry.js +0 -506
- package/dist/submission-retry.js.map +0 -1
- package/dist/telegram-bot-client.d.ts +0 -34
- package/dist/telegram-bot-client.d.ts.map +0 -1
- package/dist/telegram-bot-client.js +0 -36
- package/dist/telegram-bot-client.js.map +0 -1
- package/dist/telegram-doorbell.d.ts +0 -38
- package/dist/telegram-doorbell.d.ts.map +0 -1
- package/dist/telegram-doorbell.js +0 -140
- package/dist/telegram-doorbell.js.map +0 -1
- package/dist/test-handlers.d.ts +0 -62
- package/dist/test-handlers.d.ts.map +0 -1
- package/dist/test-handlers.js +0 -240
- package/dist/test-handlers.js.map +0 -1
- package/dist/testing.d.ts +0 -10
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -10
- package/dist/testing.js.map +0 -1
- package/dist/transport-composition.d.ts +0 -31
- package/dist/transport-composition.d.ts.map +0 -1
- package/dist/transport-composition.js +0 -55
- package/dist/transport-composition.js.map +0 -1
- package/dist/transport-selector.d.ts +0 -202
- package/dist/transport-selector.d.ts.map +0 -1
- package/dist/transport-selector.js +0 -196
- package/dist/transport-selector.js.map +0 -1
- package/dist/trust-signal-pickup-listener.d.ts +0 -43
- package/dist/trust-signal-pickup-listener.d.ts.map +0 -1
- package/dist/trust-signal-pickup-listener.js +0 -48
- package/dist/trust-signal-pickup-listener.js.map +0 -1
- package/dist/trust-signal-store.d.ts +0 -406
- package/dist/trust-signal-store.d.ts.map +0 -1
- package/dist/trust-signal-store.js +0 -939
- package/dist/trust-signal-store.js.map +0 -1
- package/dist/trust-signal-sweep-tick.d.ts +0 -60
- package/dist/trust-signal-sweep-tick.d.ts.map +0 -1
- package/dist/trust-signal-sweep-tick.js +0 -101
- package/dist/trust-signal-sweep-tick.js.map +0 -1
- package/dist/trust-signal-sweep.d.ts +0 -94
- package/dist/trust-signal-sweep.d.ts.map +0 -1
- package/dist/trust-signal-sweep.js +0 -147
- package/dist/trust-signal-sweep.js.map +0 -1
- package/dist/type-registry.d.ts +0 -42
- package/dist/type-registry.d.ts.map +0 -1
- package/dist/type-registry.js +0 -37
- package/dist/type-registry.js.map +0 -1
- package/dist/types.d.ts +0 -746
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -20
- package/dist/types.js.map +0 -1
- package/dist/unresolved-nodes-report.d.ts +0 -14
- package/dist/unresolved-nodes-report.d.ts.map +0 -1
- package/dist/unresolved-nodes-report.js +0 -96
- package/dist/unresolved-nodes-report.js.map +0 -1
- package/dist/vocabulary.d.ts +0 -150
- package/dist/vocabulary.d.ts.map +0 -1
- package/dist/vocabulary.js +0 -386
- package/dist/vocabulary.js.map +0 -1
- package/dist/who-label.d.ts +0 -28
- package/dist/who-label.d.ts.map +0 -1
- package/dist/who-label.js +0 -31
- package/dist/who-label.js.map +0 -1
- package/dist/who-resolver.d.ts +0 -15
- package/dist/who-resolver.d.ts.map +0 -1
- package/dist/who-resolver.js +0 -47
- package/dist/who-resolver.js.map +0 -1
- package/dist/wire-content-hash.d.ts +0 -93
- package/dist/wire-content-hash.d.ts.map +0 -1
- package/dist/wire-content-hash.js +0 -116
- package/dist/wire-content-hash.js.map +0 -1
- package/dist/withheld-content.d.ts +0 -13
- package/dist/withheld-content.d.ts.map +0 -1
- package/dist/withheld-content.js +0 -46
- package/dist/withheld-content.js.map +0 -1
- package/dist/witness-alerts.d.ts +0 -40
- package/dist/witness-alerts.d.ts.map +0 -1
- package/dist/witness-alerts.js +0 -102
- package/dist/witness-alerts.js.map +0 -1
|
@@ -1,1418 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cello_close_session — the bilateral close, and every way it can fail.
|
|
3
|
-
*
|
|
4
|
-
* M7 error discipline: each distinct failure cause produces a DISTINCT error code
|
|
5
|
-
* (session_already_sealed, seal_in_progress, seal_interrupted_counterparty_unavailable,
|
|
6
|
-
* seal_interrupted_rejected_by_counterparty, signaling_reconnecting). A close that fails must tell
|
|
7
|
-
* the operator WHY, not merely that it failed — this handler exists as much for its error paths as
|
|
8
|
-
* for its happy path.
|
|
9
|
-
*
|
|
10
|
-
* SI-001: there is NO auto-seal on a session_interrupted receipt. The operator must close
|
|
11
|
-
* explicitly. A daemon that sealed on its own would notarize a conversation nobody chose to end.
|
|
12
|
-
*
|
|
13
|
-
* SI-001 IS NARROWER THAN IT READS, and the boundary matters (DOD-M12B-RESTART-SEAL-1, 2026-08-17;
|
|
14
|
-
* RESTATED 2026-08-18 for DOD-M12B-PENDING-RESOLVE-1). SI-001 governs a LIVE interruption — the
|
|
15
|
-
* relay says the counterparty vanished while the operator is at the keyboard and may still want to
|
|
16
|
-
* wait. `restart-seal-resolver.ts` seals TWO different populations, and the test that licenses both
|
|
17
|
-
* is **somebody chose to end this**, not "we caused it":
|
|
18
|
-
*
|
|
19
|
-
* 1. `interrupted` with `interrupted_by = 'local'` — OUR OWN stop destroyed it, and it cannot be
|
|
20
|
-
* resumed because the transport keypairs died with the process. The only alternative is
|
|
21
|
-
* force-abandon, which forfeits the receipt: the choice is seal-or-abandon, not seal-or-resume.
|
|
22
|
-
* 2. `seal_interrupted_pending` — a seal commitment nobody asked the directory to notarize.
|
|
23
|
-
*
|
|
24
|
-
* **The old sentence here — "nothing the counterparty caused is auto-sealed" — is no longer true,
|
|
25
|
-
* and saying so plainly matters because this paragraph is what a future widening will cite.** A
|
|
26
|
-
* responder-side pending row is created by a request the COUNTERPARTY sent. What licenses it is not
|
|
27
|
-
* that we caused it but that they explicitly asked to seal; an initiator-side row is licensed by
|
|
28
|
-
* their signed leaf. Either way the conversation is one somebody chose to end, which is exactly what
|
|
29
|
-
* SI-001 protects against — and SI-001 itself holds unchanged for a live interruption.
|
|
30
|
-
*
|
|
31
|
-
* This was on my DO-NOT-CUT list. It has fifteen dependencies — a long list, but a KNOWN one, and
|
|
32
|
-
* that is the entire difference from a closure over 73 shared locals.
|
|
33
|
-
*/
|
|
34
|
-
import { randomUUID } from "node:crypto";
|
|
35
|
-
import { isLocalCredentialRefusal } from "./session-relay-client.js";
|
|
36
|
-
/**
|
|
37
|
-
* DOD-M12B-ABANDON-NOTIFY-1 — how long the courtesy notice may delay a force-abandon.
|
|
38
|
-
*
|
|
39
|
-
* Force-abandon is the operator's escape hatch out of a session that can never seal, and it must
|
|
40
|
-
* always return. Telling the counterparty is worth a moment; it is not worth the escape hatch.
|
|
41
|
-
*/
|
|
42
|
-
const ABANDON_NOTICE_DEADLINE_MS = 3_000;
|
|
43
|
-
/**
|
|
44
|
-
* What a failed SEAL-leaf submit means, by reason. `seal_stale` used to share the "local and
|
|
45
|
-
* temporary" text, which sent an operator to agent startup and relay reachability while every retry
|
|
46
|
-
* failed the same way: the relay was refusing because it holds a message this side never recorded.
|
|
47
|
-
*/
|
|
48
|
-
function sealSubmitCause(reason) {
|
|
49
|
-
if (reason === "counterparty_content_missing") {
|
|
50
|
-
return "The other side filed a message with the relay that this side does not hold, so a close signed now would seal a conversation missing it. " +
|
|
51
|
-
"It may still be arriving: wait a moment and close again. If it keeps failing, this side refused or lost that message (check cello_inbox for a refusal), " +
|
|
52
|
-
"and this conversation cannot be sealed as it stands — closing with { force: true } ends it without a seal.";
|
|
53
|
-
}
|
|
54
|
-
if (reason === "seal_stale") {
|
|
55
|
-
return "The relay holds a message from the other side that this side has not recorded, so a close signed now would seal a conversation missing it. " +
|
|
56
|
-
"If they are still sending, wait for the message to arrive (cello_receive) and close again. " +
|
|
57
|
-
"If nothing arrives, retrying will fail the same way: report the session id — do not force-abandon, which forfeits the seal.";
|
|
58
|
-
}
|
|
59
|
-
return "That is usually local and temporary — an agent that is not started yet (cello_start_agent), or a relay this daemon cannot currently reach (cello_status). " +
|
|
60
|
-
"The conversation is intact either way; retry cello_close_session once the daemon reports healthy.";
|
|
61
|
-
}
|
|
62
|
-
import { classifyOnlineResult } from "./cross-node-negotiation.js";
|
|
63
|
-
import { validateSessionName } from "./session-name.js";
|
|
64
|
-
import { escalateToUnilateralSeal as runUnilateralEscalation, UNILATERAL_SEAL_TIMEOUT_MS } from "./seal-escalation.js";
|
|
65
|
-
import { describeSealCommitted } from "./close-commitment.js";
|
|
66
|
-
import { extractErrorMessage } from "./error-message.js";
|
|
67
|
-
import { awaitOwnRecordSettled } from "./seal-settle.js";
|
|
68
|
-
export function registerCloseSessionHandler(deps) {
|
|
69
|
-
const { resolveConsortiumRoster, handlers, logger, sessionNodeManager, getConnState, resolveCurrentAgent, NO_CURRENT_AGENT_RESPONSE, getKeyProvider, signalingFor, sendOver, waitForSignalingConnected, openVisitingConnection, runDiscoveryLookup, crossNodeBrokerBySession, sealKey, sealInterruptedInProgress, registerBackgroundSeal, sealFailures, pendingSealWaiters, pendingUnilateralWaiters, handleSealInterruptedFlow, handleActiveSealFlow, recoverParkedContent, } = deps;
|
|
70
|
-
const UNILATERAL_TIMEOUT_MS = deps.unilateralTimeoutMs ?? UNILATERAL_SEAL_TIMEOUT_MS;
|
|
71
|
-
/**
|
|
72
|
-
* How long the seal path waits to learn where the counterparty is homed.
|
|
73
|
-
*
|
|
74
|
-
* DELIBERATELY SHORT, and not the 10s the session negotiator uses. This lookup is a point-read of
|
|
75
|
-
* replicated presence — milliseconds against a healthy directory — and it sits in front of EVERY
|
|
76
|
-
* interrupted close, including same-node ones that need no dial at all. Borrowing the negotiator's
|
|
77
|
-
* 10s added ten seconds to every such close and pushed the unilateral-escalation tests past their
|
|
78
|
-
* budget: the seal did not break, it just arrived too late to matter.
|
|
79
|
-
*
|
|
80
|
-
* A directory that cannot answer within this window is one we would not want to wait on anyway —
|
|
81
|
-
* the close proceeds on the home stream, exactly as it did before the lookup existed.
|
|
82
|
-
*/
|
|
83
|
-
const SEAL_DISCOVERY_TIMEOUT_MS = 2_500;
|
|
84
|
-
/**
|
|
85
|
-
* Open a transient visiting connection to the node that BROKERED this session, when that is not
|
|
86
|
-
* the node this agent is homed on. Returns null for a same-node session (the ordinary case) and
|
|
87
|
-
* whenever the connection cannot be established — in both the caller proceeds on the home stream.
|
|
88
|
-
*
|
|
89
|
-
* WHY THE CLIENT HAS TO DIAL. Directories never forward signaling to each other; the M12 journal
|
|
90
|
-
* struck that out as a channel that does not exist. A directory routes a frame by looking up a
|
|
91
|
-
* stream IT holds, and a daemon holds its stream to its own home node — so seal frames pushed by
|
|
92
|
-
* the broker reach a party only if that party holds a stream to the BROKER. Without this the
|
|
93
|
-
* close times out reporting the counterparty unavailable while they are online and waiting.
|
|
94
|
-
*
|
|
95
|
-
* SHARED BY BOTH SEAL PATHS ON PURPOSE. This lived inline in the active branch, gated on
|
|
96
|
-
* `status === "active"`, and the interrupted branch sent the same frames without it — which is
|
|
97
|
-
* precisely the defect (two agents on two nodes, symmetric timeout, 2026-08-07). One
|
|
98
|
-
* implementation is what stops that divergence coming back.
|
|
99
|
-
*/
|
|
100
|
-
async function openSealBrokerConnection(agentName, sessionId, correlationId,
|
|
101
|
-
/** Only set on the RETRY, after a home-stream attempt already failed — see the call site. */
|
|
102
|
-
discoverVia) {
|
|
103
|
-
let brokerNodeForSeal = crossNodeBrokerBySession.get(`${agentName}:${sessionId}`);
|
|
104
|
-
// THE MAP DOES NOT SURVIVE THE RESTART THAT CREATES THE CONDITION. It is in-memory, populated
|
|
105
|
-
// while a session is brokered, and emptied by a daemon restart — which is exactly what flips a
|
|
106
|
-
// live session to `interrupted`. Gating on it alone meant the dial never fired for the case it
|
|
107
|
-
// was added for (shipped as 0.0.140 and disproved against real stranded sessions the same hour).
|
|
108
|
-
//
|
|
109
|
-
// The answer is NOT to persist the broker. The node that brokered the session hours ago need
|
|
110
|
-
// not be where the counterparty lives now — agents re-home. What matters at seal time is where
|
|
111
|
-
// they are NOW, which any directory can answer from replicated presence. Classified by the same
|
|
112
|
-
// `classifyOnlineResult` the outbound path uses, so "same node" and "offline" mean here exactly
|
|
113
|
-
// what they mean there.
|
|
114
|
-
if (!brokerNodeForSeal && discoverVia && runDiscoveryLookup) {
|
|
115
|
-
const sig = signalingFor(agentName);
|
|
116
|
-
if (sig) {
|
|
117
|
-
const disc = await runDiscoveryLookup(sig, discoverVia.counterpartyPubkeyHex, SEAL_DISCOVERY_TIMEOUT_MS, correlationId);
|
|
118
|
-
if (disc.kind === "result") {
|
|
119
|
-
const action = classifyOnlineResult(disc.state, disc.owningNodeIds, sig.currentDirectoryNodeId ?? null);
|
|
120
|
-
// Only `cross_node` warrants a dial. `same_node` already has the right stream, and
|
|
121
|
-
// `offline` is a real answer — dialling their node cannot conjure a stream they do not have.
|
|
122
|
-
if (action.kind === "cross_node")
|
|
123
|
-
brokerNodeForSeal = action.owningNodeId;
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
// A lookup that did not answer is the pre-fix behaviour, not a new failure: proceed on the
|
|
127
|
-
// home stream. Logged so it stays distinguishable from "the counterparty is on our node".
|
|
128
|
-
logger.warn("session.seal.broker.discovery_failed", { agentName, sessionId, kind: disc.kind, correlationId });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Same-node session, or nowhere to dial: the home stream is already the right one.
|
|
133
|
-
if (!brokerNodeForSeal)
|
|
134
|
-
return null;
|
|
135
|
-
const sealKp = getKeyProvider(agentName);
|
|
136
|
-
if (!sealKp) {
|
|
137
|
-
// Never skip the cross-node reconnect silently — without a key provider the seal reverts to
|
|
138
|
-
// the pre-fix timeout, and this log is what keeps that distinguishable from an ordinary
|
|
139
|
-
// counterparty-didn't-close timeout.
|
|
140
|
-
logger.warn("session.seal.broker.no_keyprovider", { agentName, brokerNode: brokerNodeForSeal, correlationId });
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
const sealPubHex = Buffer.from(await sealKp.getPublicKey()).toString("hex");
|
|
144
|
-
const roster = await resolveConsortiumRoster();
|
|
145
|
-
const brokerTarget = roster?.find((e) => e.nodeId === brokerNodeForSeal) ?? null;
|
|
146
|
-
if (!brokerTarget) {
|
|
147
|
-
logger.warn("session.seal.broker.unresolved", { agentName, brokerNode: brokerNodeForSeal, correlationId });
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
const conn = openVisitingConnection(agentName, sealKp, sealPubHex, { peerId: brokerTarget.peerId, multiaddr: brokerTarget.multiaddr }, correlationId, brokerNodeForSeal);
|
|
151
|
-
if (await waitForSignalingConnected(conn.mgr, 10_000)) {
|
|
152
|
-
logger.info("session.seal.broker.reconnected", { agentName, brokerNode: brokerNodeForSeal, correlationId });
|
|
153
|
-
return conn;
|
|
154
|
-
}
|
|
155
|
-
// Tear the half-open connection down rather than leaking it, and proceed degraded.
|
|
156
|
-
await conn.stop("seal-broker-unreachable");
|
|
157
|
-
logger.warn("session.seal.broker.unreachable", { agentName, brokerNode: brokerNodeForSeal, correlationId });
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
// ─── M7-SESSION-001: cello_close_session ────────────────────────────────────
|
|
161
|
-
// M7 error discipline: each distinct failure cause produces a distinct error code.
|
|
162
|
-
// AC-010: session_already_sealed
|
|
163
|
-
// AC-011: seal_in_progress
|
|
164
|
-
// AC-012: seal_interrupted_counterparty_unavailable
|
|
165
|
-
// AC-013: seal_interrupted_rejected_by_counterparty
|
|
166
|
-
// DB-001: signaling_reconnecting
|
|
167
|
-
// SI-001: no auto-seal on session_interrupted receipt; operator must call explicitly
|
|
168
|
-
/**
|
|
169
|
-
* DOD-TERMINAL-STATE-DIVERGENCE-1 — close failures that might mean "it is ALREADY sealed, you were
|
|
170
|
-
* just never told".
|
|
171
|
-
*
|
|
172
|
-
* `session_sealed` is pushed once. Miss it and this side holds a non-terminal row while the
|
|
173
|
-
* counterparty holds a receipt, and every close from here fails in a way that reads exactly like
|
|
174
|
-
* "the seal has not happened yet". These are the reasons where that is a live possibility:
|
|
175
|
-
*
|
|
176
|
-
* - the counterparty REJECTED our seal request. It does that when it considers the session
|
|
177
|
-
* already finished, which is precisely the divergence.
|
|
178
|
-
* - the unilateral escalation TIMED OUT with no answer.
|
|
179
|
-
* - the relay says the session is sealed or gone — terminal, so nothing more can be added, which
|
|
180
|
-
* also means whatever exists is final.
|
|
181
|
-
*
|
|
182
|
-
* NOT included: `session_not_found`, `invalid_session_id`, an abandoned session, or a refusal we
|
|
183
|
-
* generated locally. Asking there would be noise on a question already answered.
|
|
184
|
-
*/
|
|
185
|
-
const MAY_ALREADY_BE_SEALED = new Set([
|
|
186
|
-
"seal_interrupted_rejected_by_counterparty",
|
|
187
|
-
"seal_unilateral_timeout",
|
|
188
|
-
"session_seal_already_pending",
|
|
189
|
-
"session_sealed",
|
|
190
|
-
/**
|
|
191
|
-
* DOD-M15-TERMINAL-REASON-1. Without this, a close on a session the relay REFUSED skips
|
|
192
|
-
* `pullSealCertificate` entirely — the one probe that distinguishes "never sealed" from "sealed
|
|
193
|
-
* and you were never told". Losing it here is how a close ends with no receipt and no
|
|
194
|
-
* explanation of why.
|
|
195
|
-
*/
|
|
196
|
-
"seal_refused",
|
|
197
|
-
"relay_session_gone",
|
|
198
|
-
]);
|
|
199
|
-
/**
|
|
200
|
-
* The close, wrapped so a failure gets ONE chance to discover the seal already happened.
|
|
201
|
-
*
|
|
202
|
-
* Wrapped rather than patched into each exit: this handler has six failure returns and a seventh
|
|
203
|
-
* would be added without the recovery. The operator's experience is what matters here — they ran a
|
|
204
|
-
* close, it failed, and the answer they get should be a receipt if one exists.
|
|
205
|
-
*/
|
|
206
|
-
const closeSession = async (params, connectionId) => {
|
|
207
|
-
const first = await runClose(params, connectionId);
|
|
208
|
-
const failed = first;
|
|
209
|
-
if (failed?.ok !== false || !deps.pullSealCertificate)
|
|
210
|
-
return first;
|
|
211
|
-
if (!MAY_ALREADY_BE_SEALED.has(String(failed.reason)))
|
|
212
|
-
return first;
|
|
213
|
-
const connState = getConnState(connectionId);
|
|
214
|
-
const agentName = resolveCurrentAgent(connState, params?.agent);
|
|
215
|
-
// `session_id` — the IPC parameter name. The MCP tool's `cello_session_id` is translated by the
|
|
216
|
-
// shim, and reading that name here would have found nothing and silently skipped the recovery.
|
|
217
|
-
const sessionId = typeof params?.session_id === "string" ? params.session_id : "";
|
|
218
|
-
if (!agentName || sessionId.length === 0)
|
|
219
|
-
return first;
|
|
220
|
-
const pulled = await deps.pullSealCertificate(agentName, sessionId);
|
|
221
|
-
if (!pulled.ok) {
|
|
222
|
-
// Said out loud. "We asked and there is none" is a different fact from "we never asked", and
|
|
223
|
-
// an operator deciding whether to force-abandon needs to know which one they have.
|
|
224
|
-
logger.info("seal.certificate.pull.none_on_close", {
|
|
225
|
-
agentName, sessionId, closeReason: failed.reason, pullReason: pulled.reason ?? "",
|
|
226
|
-
});
|
|
227
|
-
return {
|
|
228
|
-
...first,
|
|
229
|
-
seal_lookup: pulled.reason === "not_found" ? "asked_none_exists" : `asked_${pulled.reason ?? "failed"}`,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
const recovered = sessionNodeManager.getSealCertificate(agentName, sessionId);
|
|
233
|
-
if (!recovered)
|
|
234
|
-
return first;
|
|
235
|
-
logger.info("seal.certificate.recovered_on_close", {
|
|
236
|
-
agentName, sessionId, sealedRoot: recovered.sealed_root,
|
|
237
|
-
impact: "the seal existed and this side had never been told; the close now returns the receipt",
|
|
238
|
-
});
|
|
239
|
-
return {
|
|
240
|
-
ok: true,
|
|
241
|
-
sessionId,
|
|
242
|
-
sealed_root: recovered.sealed_root,
|
|
243
|
-
recovered: true,
|
|
244
|
-
guidance: "This session was ALREADY sealed — the notarization existed and this daemon had never been " +
|
|
245
|
-
"told, which is why the close appeared to fail. The certificate has been fetched and " +
|
|
246
|
-
"verified against its signature, and is now held locally. Read it with cello_sealed_receipt.",
|
|
247
|
-
};
|
|
248
|
-
};
|
|
249
|
-
handlers.set("cello_close_session", closeSession);
|
|
250
|
-
/**
|
|
251
|
-
* DOD-M12B-SEAL-ESCALATE-DUP-1: the body lives in `seal-escalation.ts` so the away/one-shot path
|
|
252
|
-
* in `daemon.ts` shares it. It used to be a second copy, and it missed every refusal this
|
|
253
|
-
* milestone added.
|
|
254
|
-
*/
|
|
255
|
-
const escalateToUnilateralSeal = (record, sessionId, escalation, correlationId, opts = { refuseOnUnusableCarry: false }) => runUnilateralEscalation({
|
|
256
|
-
logger, sessionNodeManager, sendOver, pendingUnilateralWaiters, sealKey, getKeyProvider,
|
|
257
|
-
timeoutMs: UNILATERAL_TIMEOUT_MS,
|
|
258
|
-
onSealed: (a, sid) => { crossNodeBrokerBySession.delete(`${a}:${sid}`); },
|
|
259
|
-
}, record.agent_name, sessionId, escalation, correlationId, opts);
|
|
260
|
-
/**
|
|
261
|
-
* Post this side's SEAL ctrl leaf (or recover the one a previous run already posted) and ask the
|
|
262
|
-
* directory to notarize. `null` means there was nothing to escalate with — the caller keeps its
|
|
263
|
-
* own result rather than inventing a failure.
|
|
264
|
-
*
|
|
265
|
-
* SHARED so a third copy cannot appear. It has two callers: the interrupted close, and the
|
|
266
|
-
* `seal_interrupted_pending` close that used to be refused outright.
|
|
267
|
-
*/
|
|
268
|
-
const submitAndEscalate = async (record, sessionId, correlationId) => {
|
|
269
|
-
const submitted = await sessionNodeManager.submitSealLeaf(record.agent_name, sessionId, correlationId);
|
|
270
|
-
if (submitted.ok || submitted.reason === "responder_seal_already_submitted") {
|
|
271
|
-
logger.info("session.interrupted.seal.leaf.submitted", {
|
|
272
|
-
agentName: record.agent_name, sessionId, correlationId,
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
logger.warn("session.interrupted.seal.leaf.submit_failed", {
|
|
277
|
-
agentName: record.agent_name, sessionId, reason: submitted.reason, correlationId,
|
|
278
|
-
impact: "both sides hold a signed commitment, but no notarization was requested — this session has no receipt and cannot get one once the relay drops it",
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
// The root and sequence the directory verifies against. `responder_seal_already_submitted`
|
|
282
|
-
// carries them from the FIRST submit — from THIS process's memory, or recovered from the
|
|
283
|
-
// durable carry after a restart — so a repeat close can still escalate (M8B FINDING-1).
|
|
284
|
-
const escalation = submitted.ok
|
|
285
|
-
? { reportedRootHex: submitted.reportedRootHex, sequenceNumber: submitted.sequenceNumber }
|
|
286
|
-
: typeof submitted.reportedRootHex === "string" && typeof submitted.sequenceNumber === "number"
|
|
287
|
-
? { reportedRootHex: submitted.reportedRootHex, sequenceNumber: submitted.sequenceNumber }
|
|
288
|
-
: null;
|
|
289
|
-
// `submitted.ok` always yields an escalation above, so this is only ever the failure reason.
|
|
290
|
-
// Kept as a total expression rather than a cast: if submitSealLeaf's contract is ever loosened,
|
|
291
|
-
// this names the new case instead of building `{reportedRootHex: undefined}` and throwing at
|
|
292
|
-
// `Buffer.from` several frames away.
|
|
293
|
-
if (!escalation)
|
|
294
|
-
return { escalated: false, reason: submitted.ok ? "submit_returned_no_root" : submitted.reason };
|
|
295
|
-
return await escalateToUnilateralSeal(record, sessionId, escalation, correlationId, {
|
|
296
|
-
refuseOnUnusableCarry: true,
|
|
297
|
-
});
|
|
298
|
-
};
|
|
299
|
-
const runClose = async (params, connectionId) => {
|
|
300
|
-
const connState = getConnState(connectionId);
|
|
301
|
-
// CC-3 / M8C-AUTOSTART-1 F18: explicit { agent } > this connection's current > sole online agent.
|
|
302
|
-
const agentName = resolveCurrentAgent(connState, params?.agent);
|
|
303
|
-
if (!agentName) {
|
|
304
|
-
return NO_CURRENT_AGENT_RESPONSE;
|
|
305
|
-
}
|
|
306
|
-
// round-2 BLOCKING: the public IPC contract field is snake_case `session_id`
|
|
307
|
-
// (this is what cello-mcp.ts forwards verbatim through IpcProxy, matching the
|
|
308
|
-
// rest of the public MCP tool surface — target_pubkey, content_hash, timeout_ms).
|
|
309
|
-
// Reading camelCase `sessionId` here meant every real proxy invocation produced
|
|
310
|
-
// undefined → missing_params. Consume the field the producer actually sends.
|
|
311
|
-
const sessionId = params?.session_id;
|
|
312
|
-
if (!sessionId) {
|
|
313
|
-
return {
|
|
314
|
-
ok: false,
|
|
315
|
-
reason: "missing_params",
|
|
316
|
-
guidance: "Provide 'session_id' parameter with the hex session ID to close.",
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
// DOD-SESSION-NAME-1 (AC-A6): THE NAME MUST NEVER BREAK THE CLOSE. A close is a seal ceremony
|
|
320
|
-
// and the seal is the valuable thing — a notarized receipt both parties keep. So a bad name is
|
|
321
|
-
// refused HERE, before any of it starts: not half-closed-then-failed, and not silently dropped
|
|
322
|
-
// and sealed anyway. Validate, then close. The name is a sticky note; it does not get a vote on
|
|
323
|
-
// whether the notarization happens.
|
|
324
|
-
const nameCheck = validateSessionName(params?.session_name);
|
|
325
|
-
if (!nameCheck.ok) {
|
|
326
|
-
logger.warn("session.name.rejected", {
|
|
327
|
-
// agentId, not agentName — session.name.set/.cleared key on agentId (AC-A16), and an
|
|
328
|
-
// operator correlating rejections against sets on two different keys gets nothing.
|
|
329
|
-
agentId: sessionNodeManager.resolveAgentId(agentName), sessionId, reason: nameCheck.reason, source: "close",
|
|
330
|
-
nameLength: typeof params?.session_name === "string" ? params.session_name.length : null,
|
|
331
|
-
});
|
|
332
|
-
return { ok: false, reason: nameCheck.reason, guidance: nameCheck.guidance };
|
|
333
|
-
}
|
|
334
|
-
// DOD-LOOP-1: scope the lookup to the current agent — the composite (agent, session_id) key IS
|
|
335
|
-
// the ownership scope. A session_id owned only by a DIFFERENT agent does not exist in this
|
|
336
|
-
// agent's namespace (returns null → session_not_found), which is correct for loopback (two
|
|
337
|
-
// agents can hold the same session_id on one daemon).
|
|
338
|
-
const record = sessionNodeManager.getSessionRecord(agentName, sessionId);
|
|
339
|
-
if (!record) {
|
|
340
|
-
return {
|
|
341
|
-
ok: false,
|
|
342
|
-
reason: "session_not_found",
|
|
343
|
-
guidance: "No session found with this ID. Check cello_sessions for active and interrupted sessions.",
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
// Ownership: redundant now that the lookup is agent-scoped (record.agent_name === currentAgent),
|
|
347
|
-
// kept as a defensive invariant.
|
|
348
|
-
if (record.agent_name !== agentName) {
|
|
349
|
-
return {
|
|
350
|
-
ok: false,
|
|
351
|
-
reason: "session_not_owned",
|
|
352
|
-
guidance: "This session belongs to a different agent. Call cello_use_agent to switch to the agent that owns it (see cello_sessions), then retry.",
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
// DOD-SESSION-NAME-1 (AC-A7): the name is written HERE — once, on the way in — rather than at
|
|
356
|
-
// each of the terminal exits (bilateral seal, unilateral seal, force-abandon). Threading it
|
|
357
|
-
// through all of them is how one silently ends up without it, and a name dropped on the very
|
|
358
|
-
// path where it is most useful (a force-abandoned ghost is the session you most want to identify
|
|
359
|
-
// later) is a defect nothing would fail on.
|
|
360
|
-
//
|
|
361
|
-
// It sits ABOVE the status gates deliberately. A name is not close-scoped: renaming is legal in
|
|
362
|
-
// ANY status (AC-A9), sealed included. Below the already-sealed gate, a RETRIED close carrying a
|
|
363
|
-
// name — the seal completed but the agent's call was interrupted, so it calls again — would be
|
|
364
|
-
// answered "already sealed, no further action is needed" and the name would be silently dropped,
|
|
365
|
-
// on exactly the path where the agent believes it just named the session.
|
|
366
|
-
//
|
|
367
|
-
// Writing it before the close COMPLETES is safe for the same reason: if the seal below fails, the
|
|
368
|
-
// operator has a named open session — a state they could produce with cello_name_session anyway.
|
|
369
|
-
// Nothing about the name gates, delays, or alters the ceremony.
|
|
370
|
-
if (nameCheck.value !== null) {
|
|
371
|
-
sessionNodeManager.setSessionName(agentName, sessionId, nameCheck.value);
|
|
372
|
-
logger.info("session.name.set", {
|
|
373
|
-
agentId: record.agent_id, sessionId, nameLength: nameCheck.value.length, source: "close",
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
// AC-010: already sealed
|
|
377
|
-
if (record.status === "sealed") {
|
|
378
|
-
return {
|
|
379
|
-
ok: false,
|
|
380
|
-
reason: "session_already_sealed",
|
|
381
|
-
// A name given here WAS applied (above) — say so, rather than let "no further action is
|
|
382
|
-
// needed" imply the name went nowhere.
|
|
383
|
-
...(nameCheck.value !== null ? { session_name: nameCheck.value } : {}),
|
|
384
|
-
guidance: nameCheck.value !== null
|
|
385
|
-
? "Already sealed by both sides. Nothing more to do; the name you gave was applied. View the receipt with cello_sealed_receipt."
|
|
386
|
-
: "Already sealed by both sides. Nothing more to do; view the receipt with cello_sealed_receipt.",
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
// An ABANDONED session is TERMINAL, and a close without --force must say so rather than try to
|
|
390
|
-
// seal it. The already-abandoned early-return below lives INSIDE the force branch, so a plain
|
|
391
|
-
// `cello close-session <abandoned-id>` fell through to the seal flow and fired a bilateral seal
|
|
392
|
-
// ceremony at a counterparty that, by definition, was never there — the exact unsealable hang
|
|
393
|
-
// that force exists to escape, re-entered from the other side. Found by the unit reviewer on this
|
|
394
|
-
// diff; pre-existing, fixed here rather than left for someone to hit in the dark.
|
|
395
|
-
// Scoped to the NON-force path: `force` on an already-abandoned session stays idempotent
|
|
396
|
-
// (ok:true / already_abandoned, below) — that contract is tested and must not change.
|
|
397
|
-
if (record.status === "abandoned" && params?.force !== true) {
|
|
398
|
-
return {
|
|
399
|
-
ok: false,
|
|
400
|
-
reason: "session_abandoned",
|
|
401
|
-
guidance: "This session was force-abandoned — it is terminal and has no counterparty to seal with, so it cannot be closed. It is already off the open list; check cello_sessions.",
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
// CC-5/F21 terminal-escape: force-abandon a session that can never be bilaterally sealed — a
|
|
405
|
-
// half-open handshake the counterparty never joined, whose normal close fires a seal the absent/
|
|
406
|
-
// rejecting counterparty can't complete (seal_interrupted_rejected_by_counterparty / timeout). Marks
|
|
407
|
-
// it locally-terminal ("abandoned") with NO seal, so it leaves the open list. Additive: placed BEFORE
|
|
408
|
-
// the seal branches, it never touches the seal flow. Owner-scoped (the lookup above is agent-scoped)
|
|
409
|
-
// and idempotent. NOT for a healthy session — a normal close (no force) still seals so both parties
|
|
410
|
-
// get a notarized receipt; force is the escape hatch for a provably unsealable ghost.
|
|
411
|
-
if (params?.force === true) {
|
|
412
|
-
if (record.status === "abandoned") {
|
|
413
|
-
return { ok: true, status: "abandoned", reason: "already_abandoned", guidance: "This session was already force-abandoned." };
|
|
414
|
-
}
|
|
415
|
-
// A session that reached a seal attempt (interrupted / seal_interrupted_pending) MIGHT have
|
|
416
|
-
// been notarized on the counterparty's side without this side ever learning — the
|
|
417
|
-
// no-pull-twin divergence. A half-open ghost never got that far and forfeits nothing. Warning
|
|
418
|
-
// on both would put the same paragraph on the ninety routine force-abandons that cost nothing
|
|
419
|
-
// and the one that cost a receipt, which buries the only occurrence that matters.
|
|
420
|
-
const mayHaveSealed = record.status === "interrupted" || record.status === "seal_interrupted_pending";
|
|
421
|
-
// ONE id for the close and every line the notice emits, so they can be joined. Minting a
|
|
422
|
-
// fresh one inside the notice call meant its sent/failed/skipped lines belonged to no flow.
|
|
423
|
-
const correlationId = randomUUID();
|
|
424
|
-
// DOD-M12B-ABANDON-NOTIFY-1: TELL THEM FIRST, while the session node still exists — the
|
|
425
|
-
// abandon tears it down, and after that there is no stream to tell them on. Awaited so the
|
|
426
|
-
// answer can be honest about whether they were reached, but it can only ever return: it never
|
|
427
|
-
// throws and never blocks the abandon, which is the operator's escape hatch and must not
|
|
428
|
-
// depend on the counterparty being reachable.
|
|
429
|
-
//
|
|
430
|
-
// Without this the other side keeps its half live, keeps retrying delivery into it, and keeps
|
|
431
|
-
// trying to re-establish — which is what produced the 2026-08-17 storm, where the operator saw
|
|
432
|
-
// connection requests from agents nobody was driving.
|
|
433
|
-
// DELIBERATELY FAIL-OPEN, and this is the one place in the milestone where that is right:
|
|
434
|
-
// force-abandon is the operator's escape hatch out of a session that can never seal, and it
|
|
435
|
-
// must not become conditional on a courtesy. The notice already handles its own failures; this
|
|
436
|
-
// catches the unexpected — an absent method, a throw from the transport layer — so that no
|
|
437
|
-
// fault in telling the counterparty can prevent the operator from ending their own session.
|
|
438
|
-
let notice = { told: false, reason: "not_attempted" };
|
|
439
|
-
try {
|
|
440
|
-
// A HARD DEADLINE, not just a catch. The catch covers a throw; it does not cover a hang,
|
|
441
|
-
// and `stream.close()` waits for the write buffer to drain with no timeout anywhere in the
|
|
442
|
-
// chain — so a half-dead connection could sit here indefinitely on the one call that must
|
|
443
|
-
// always return. Force-abandon is the escape hatch; it does not get to be slow either.
|
|
444
|
-
notice = await Promise.race([
|
|
445
|
-
sessionNodeManager.notifyCounterpartyAbandon(record.agent_name, sessionId, correlationId),
|
|
446
|
-
new Promise((resolve) => {
|
|
447
|
-
const t = setTimeout(() => resolve({ told: false, reason: "notice_timeout" }), ABANDON_NOTICE_DEADLINE_MS);
|
|
448
|
-
t.unref?.();
|
|
449
|
-
}),
|
|
450
|
-
]);
|
|
451
|
-
}
|
|
452
|
-
catch (err) {
|
|
453
|
-
logger.warn("session.abandon.notice.threw", {
|
|
454
|
-
agentName: record.agent_name, sessionId, correlationId,
|
|
455
|
-
error: extractErrorMessage(err),
|
|
456
|
-
impact: "the counterparty was not told and may keep calling; the abandon itself proceeds",
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
const told = notice.told;
|
|
460
|
-
await sessionNodeManager.abandonSession(record.agent_name, sessionId);
|
|
461
|
-
/**
|
|
462
|
-
* FORGET ANY SEAL FAILURE — review MEDIUM-5.
|
|
463
|
-
*
|
|
464
|
-
* An abandoned session is TERMINAL: there is no ceremony to retry and no receipt to obtain.
|
|
465
|
-
* Leaving the marker made two surfaces contradict each other — `cello_sealed_receipt` reported
|
|
466
|
-
* `seal_failed` and told the agent to *"call cello_close_session again"*, while
|
|
467
|
-
* `cello_close_session` refused with `session_abandoned` and *"it is terminal… it cannot be
|
|
468
|
-
* closed."* A refused loop, and exactly the contradicting-surfaces pair the previous unit's
|
|
469
|
-
* review flagged, reintroduced one unit later.
|
|
470
|
-
*/
|
|
471
|
-
sealFailures.clear(record.agent_name, sessionId);
|
|
472
|
-
logger.info("session.force_abandoned", {
|
|
473
|
-
agentName: record.agent_name,
|
|
474
|
-
sessionId,
|
|
475
|
-
priorStatus: record.status,
|
|
476
|
-
counterpartyNotified: told,
|
|
477
|
-
counterpartyNoticeReason: notice.reason,
|
|
478
|
-
correlationId,
|
|
479
|
-
// Makes the destructive case findable in a log instead of reconstructable by hand, which is
|
|
480
|
-
// how the 2026-08-06 incident had to be traced.
|
|
481
|
-
mayHaveForfeitedSeal: mayHaveSealed,
|
|
482
|
-
});
|
|
483
|
-
return {
|
|
484
|
-
ok: true,
|
|
485
|
-
status: "abandoned",
|
|
486
|
-
reason: "force_abandoned",
|
|
487
|
-
counterparty_notified: told,
|
|
488
|
-
counterparty_notice_reason: notice.reason,
|
|
489
|
-
guidance: `Session ${sessionId} was force-abandoned — marked terminal locally with no bilateral seal. Use force only for a half-open session that cannot be sealed; a normal close (no force) still attempts the seal so both parties get a notarized receipt.` +
|
|
490
|
-
(told
|
|
491
|
-
? ` The notice was sent — a counterparty running a current client will stop calling this session. There is no acknowledgement for it, so if connection attempts keep arriving they are on an older build that does not understand it.`
|
|
492
|
-
: notice.reason === "no_local_node"
|
|
493
|
-
? ` They were NOT told: this side had already torn the session down, so there was nothing to send the notice on. Their half stays open and they may go on retrying delivery and re-dialling until they give up. If connection attempts keep arriving from them, that is why — it is not a network fault.`
|
|
494
|
-
: ` They were NOT told (${notice.reason}), so their half stays open: they may go on retrying delivery and re-dialling this session until they give up. If connection attempts keep arriving from them, that is why.`) +
|
|
495
|
-
(mayHaveSealed
|
|
496
|
-
? ` This session had reached a seal attempt (prior status: ${record.status}), so if the counterparty did notarize it, this side's half is now PERMANENTLY forfeited and cannot be recovered from here. Their copy, if it exists, is the only remaining one — ask them for their sealed_root and record it with the operator.`
|
|
497
|
-
: ""),
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
// M12-P14: do not ask for a seal we already know cannot be granted.
|
|
501
|
-
//
|
|
502
|
-
// Placed AFTER the force branch on purpose — force is the operator's deliberate escape hatch and
|
|
503
|
-
// must never be gated — and before every seal path, because both the interrupted and the active
|
|
504
|
-
// flows sign this side's frontier.
|
|
505
|
-
//
|
|
506
|
-
// A short chain produces `leaf_count_mismatch` at the counterparty, which is correct and
|
|
507
|
-
// TERMINAL: nothing in the protocol backfills a missing leaf, so the session's only remaining
|
|
508
|
-
// exit is a force-abandon with no notarized receipt. Refusing here costs the operator a retry;
|
|
509
|
-
// not refusing costs them the receipt permanently.
|
|
510
|
-
// Review HIGH-3: scoped to the two statuses that can actually seal. Unconditional, it also fired
|
|
511
|
-
// for `seal_interrupted_pending` — displacing that status's accurate answer ("awaiting FROST
|
|
512
|
-
// notarization") with `session_incomplete`, and offering force:true against a seal that is
|
|
513
|
-
// mid-notarization. That is the same error substitution e3da3b4 exists to remove, reintroduced
|
|
514
|
-
// one branch earlier in the same handler. (markInterruptedWithDetails does not evict the caches,
|
|
515
|
-
// so the signals survive into that status and the branch was genuinely reachable.)
|
|
516
|
-
const sealable = record.status === "active" || record.status === "interrupted";
|
|
517
|
-
let readiness = sealable
|
|
518
|
-
? sessionNodeManager.sealReadiness(record.agent_name, sessionId)
|
|
519
|
-
// DOD-M15-DIVERGE-1: `diverged: false` for the same reason every other field here is a
|
|
520
|
-
// no-op value — this branch is the NOT-sealable statuses, where readiness is never consulted
|
|
521
|
-
// and both gates below are skipped. It asserts nothing about the record; it keeps the shape.
|
|
522
|
-
// DOD-M15-SEALPRECOND-1: `ownLeavesOrdered: 0` for the same reason `diverged` is false here —
|
|
523
|
-
// this is the NOT-sealable branch, where readiness is never consulted and every gate below is
|
|
524
|
-
// skipped. It keeps the shape and asserts nothing about the record.
|
|
525
|
-
: { ready: true, treeSize: 0, highWaterSeq: -1, heldCount: 0, missingLeaves: 0, heldOwn: 0, heldReceived: 0, diverged: false, ownLeavesOrdered: 0 };
|
|
526
|
-
// Review HIGH-1: the guidance used to promise "the daemon pulls missing content automatically"
|
|
527
|
-
// while nothing on this path pulled anything — autoRecoverForAgent fires on signaling reconnect,
|
|
528
|
-
// seal-upgrade and agent start, none of which a close triggers. So the operator waited for an
|
|
529
|
-
// event that would never happen, retried, got the same refusal, and reached for force:true — the
|
|
530
|
-
// terminal receipt-less outcome this gate exists to prevent. Drain FIRST, then judge, which is
|
|
531
|
-
// the sequence seal-upgrade.ts already documents: "Recover content -> consult the gate -> REFUSE".
|
|
532
|
-
// Review LOW-10: skip the drain for a diverged record. `diverged` is now a term in `ready`, so
|
|
533
|
-
// without this a diverged session pays a relay round trip that cannot change the answer — the
|
|
534
|
-
// gap the drain fills is not the condition that refused it.
|
|
535
|
-
if (sealable && !readiness.ready && !readiness.diverged && recoverParkedContent) {
|
|
536
|
-
try {
|
|
537
|
-
await recoverParkedContent(record.agent_name, "close_readiness_gate");
|
|
538
|
-
readiness = sessionNodeManager.sealReadiness(record.agent_name, sessionId);
|
|
539
|
-
}
|
|
540
|
-
catch (err) {
|
|
541
|
-
// A drain failure is not a reason to block the close: re-judging on the pre-drain readiness
|
|
542
|
-
// is the same answer we would have given anyway, and it is still the honest one.
|
|
543
|
-
logger.warn("session.seal.readiness.drain.failed", {
|
|
544
|
-
agentName: record.agent_name, sessionId,
|
|
545
|
-
error: extractErrorMessage(err),
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* DOD-M15-SEALPRECOND-1 — WAIT FOR THIS SIDE'S OWN RECORD TO STOP MOVING, then judge.
|
|
551
|
-
*
|
|
552
|
-
* After the drain, before the two refusals: it is the one condition here that clears on its own
|
|
553
|
-
* in milliseconds — an in-flight send the relay has ordered whose leaf this tree has not
|
|
554
|
-
* written. Signing inside that span produced the 2026-09-11 loss: a two-leaf root against a
|
|
555
|
-
* three-leaf relay set, refused by both directories, no second chance for either party.
|
|
556
|
-
*
|
|
557
|
-
* NOT for a diverged record — divergence never resolves, so waiting would delay a permanent
|
|
558
|
-
* answer to report a transient one, the substitution DOD-M15-DIVERGE-1 exists to prevent.
|
|
559
|
-
*/
|
|
560
|
-
if (sealable && !readiness.diverged && readiness.ownLeavesOrdered > 0) {
|
|
561
|
-
const settle = await awaitOwnRecordSettled(sessionNodeManager, record.agent_name, sessionId);
|
|
562
|
-
readiness = sessionNodeManager.sealReadiness(record.agent_name, sessionId);
|
|
563
|
-
if (!settle.settled) {
|
|
564
|
-
logger.warn("session.seal.blocked_settling", {
|
|
565
|
-
agentName: record.agent_name, sessionId,
|
|
566
|
-
treeSize: readiness.treeSize, highWaterSeq: readiness.highWaterSeq,
|
|
567
|
-
ownLeavesOrdered: settle.ownLeavesOrdered,
|
|
568
|
-
waitedMs: settle.waitedMs,
|
|
569
|
-
impact: "a message of this operator's own has a place in the relay's ordering that this side's record has not taken yet, so NOTHING was signed — a root signed short of the relay's leaf set is refused by the directory and the receipt cannot be recovered",
|
|
570
|
-
});
|
|
571
|
-
return {
|
|
572
|
-
ok: false,
|
|
573
|
-
reason: "session_record_settling",
|
|
574
|
-
own_leaves_settling: settle.ownLeavesOrdered,
|
|
575
|
-
waited_ms: settle.waitedMs,
|
|
576
|
-
tree_size: readiness.treeSize,
|
|
577
|
-
// NAMES THIS SIDE, and only this side (065-SEALREASON). The counterparty has done nothing
|
|
578
|
-
// and is owed no part of this sentence; `session_incomplete` next door says the opposite
|
|
579
|
-
// thing — that they have not sent something — and borrowing its words here would send two
|
|
580
|
-
// operators to argue about a condition neither of them caused.
|
|
581
|
-
guidance: `Nothing was signed. ${settle.ownLeavesOrdered} message(s) you sent have a place in the record that this side has not written yet, and a receipt signed without them is refused and cannot be recovered. ` +
|
|
582
|
-
`This is your own record catching up — the counterparty is not involved and there is nothing for them to do. ` +
|
|
583
|
-
`It normally settles in well under a second, so close again: cello_close_session ${sessionId}. ` +
|
|
584
|
-
`If it keeps returning this, cello_status shows the session as still settling and cello_transcript ${sessionId} shows what is already in the record.`,
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
logger.info("session.seal.settled", {
|
|
588
|
-
agentName: record.agent_name, sessionId, waitedMs: settle.waitedMs,
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* DOD-M15-DIVERGE-1 — a PERMANENT parting gets its own answer, before the transient one.
|
|
593
|
-
*
|
|
594
|
-
* `#diverged` is set when an ack came back behind this side's frontier, which proves the tree
|
|
595
|
-
* and the relay's counter can never agree on a root again. It was detected correctly, logged at
|
|
596
|
-
* ERROR, and reached exactly one consumer: the text `cello status` prints. This gate — the one
|
|
597
|
-
* place the fact changes an outcome — could not see it, so the operator learned only when the
|
|
598
|
-
* counterparty answered `leaf_count_mismatch`, by which point the receipt was gone.
|
|
599
|
-
*
|
|
600
|
-
* BRANCHED, NOT FOLDED INTO `session_incomplete`, and that is the whole point of the ordering.
|
|
601
|
-
* The refusal below tells the operator to "wait a moment and close again" and says the daemon
|
|
602
|
-
* just pulled from the relay — true and useful for a session waiting on arrival, and false for
|
|
603
|
-
* this one. Nothing backfills a position the relay already assigned to something else, so a
|
|
604
|
-
* retry loop on that guidance ends where every one of them ends: `force: true`, terminal, no
|
|
605
|
-
* receipt. Substituting a transient explanation for a permanent condition is the error class
|
|
606
|
-
* this whole milestone exists to remove; doing it inside its own fix would be the worst place.
|
|
607
|
-
*
|
|
608
|
-
* The ERROR log at the detection site STAYS. This is the second half of it, not a relocation:
|
|
609
|
-
* the log is the forensic record and this is the control. (M15-PROCEDURE §2b, Invariant 2.)
|
|
610
|
-
*/
|
|
611
|
-
if (sealable && readiness.diverged) {
|
|
612
|
-
logger.warn("session.seal.blocked_diverged", {
|
|
613
|
-
agentName: record.agent_name, sessionId,
|
|
614
|
-
treeSize: readiness.treeSize, highWaterSeq: readiness.highWaterSeq,
|
|
615
|
-
// Review MEDIUM-5: the same four counters the sibling refusal carries. Without them a
|
|
616
|
-
// session that is BOTH diverged and gapped reported its gap on no surface at all — this
|
|
617
|
-
// branch preempts the incomplete one, and `sealReadinessView` short-circuits to `unknown`.
|
|
618
|
-
heldCount: readiness.heldCount, missingLeaves: readiness.missingLeaves,
|
|
619
|
-
heldOwn: readiness.heldOwn, heldReceived: readiness.heldReceived,
|
|
620
|
-
impact: "this side's tree holds a leaf at a position the relay assigned to something else; the relay's ordering is what gets notarized, so a bilateral seal may be refused and this side cannot tell in advance",
|
|
621
|
-
});
|
|
622
|
-
return {
|
|
623
|
-
ok: false,
|
|
624
|
-
reason: "session_record_diverged",
|
|
625
|
-
tree_size: readiness.treeSize,
|
|
626
|
-
relay_high_water: readiness.highWaterSeq,
|
|
627
|
-
held_messages: readiness.heldCount,
|
|
628
|
-
missing_leaves: readiness.missingLeaves,
|
|
629
|
-
held_own: readiness.heldOwn,
|
|
630
|
-
held_received: readiness.heldReceived,
|
|
631
|
-
// Review HIGH-1: SAY WHAT WAS MEASURED, NOT WHAT IS PREDICTED. The earlier wording asserted
|
|
632
|
-
// the counterparty would refuse with `leaf_count_mismatch` and that force was the only exit.
|
|
633
|
-
// Neither is established here. What is measured is that THIS tree parted from THE RELAY's
|
|
634
|
-
// counter. The bilateral check compares LEAF COUNT, not the root (`seal-flows.ts`:
|
|
635
|
-
// "Merkle-root agreement is NOT verified at this leaf-exchange layer"), and both sides
|
|
636
|
-
// append a behind-frontier leaf at the tail — so if the counterparty skewed the same way
|
|
637
|
-
// the counts still agree and the seal can succeed. Predicting their refusal was an
|
|
638
|
-
// over-claim, and offering force-abandon as "the only exit" pointed at the one irreversible
|
|
639
|
-
// action while the codebase's own mismatch handling leaves the session retryable.
|
|
640
|
-
guidance: `This side's record no longer agrees with the relay's ordering — your tree holds ${readiness.treeSize} message(s) and the relay's counter is at ${readiness.highWaterSeq + 1}. ` +
|
|
641
|
-
`The relay's ordering is what gets notarized, so sealing now may be refused. It may also succeed: the bilateral check compares message COUNTS, not contents, so if the counterparty's record skewed the same way the counts still match. This side cannot tell which from here. ` +
|
|
642
|
-
`What is certain is that this will not resolve on its own — nothing backfills or re-numbers a leaf, so waiting does not change it. ` +
|
|
643
|
-
`Compare message counts with the counterparty before deciding; cello_transcript ${sessionId} shows your full record and it stays readable whatever you choose. ` +
|
|
644
|
-
`cello_close_session ${sessionId} { force: true } ends the session with NO notarized receipt and cannot be undone — the last resort, not the next step.`,
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
if (sealable && !readiness.ready) {
|
|
648
|
-
logger.warn("session.seal.blocked_incomplete", {
|
|
649
|
-
agentName: record.agent_name, sessionId,
|
|
650
|
-
treeSize: readiness.treeSize, highWaterSeq: readiness.highWaterSeq,
|
|
651
|
-
heldCount: readiness.heldCount, missingLeaves: readiness.missingLeaves,
|
|
652
|
-
heldOwn: readiness.heldOwn, heldReceived: readiness.heldReceived,
|
|
653
|
-
});
|
|
654
|
-
// DOD-M12B-INDEX-1: SAY WHOSE MESSAGES ARE WAITING. Our own held sends block the seal exactly
|
|
655
|
-
// as received ones do, but calling them "received message(s) waiting behind a gap" tells the
|
|
656
|
-
// operator to wait for content that is already in hand — and the relay pull this gate performs
|
|
657
|
-
// first can never resolve it. They retry, get the identical refusal, and reach for force:true:
|
|
658
|
-
// the receipt-less exit this gate exists to prevent.
|
|
659
|
-
const waiting = (readiness.heldReceived > 0 ? `, and ${readiness.heldReceived} received message(s) are waiting behind a gap` : "") +
|
|
660
|
-
(readiness.heldOwn > 0 ? `, and ${readiness.heldOwn} of YOUR OWN message(s) are waiting for their place in the record (they were delivered — the counterparty has them)` : "");
|
|
661
|
-
return {
|
|
662
|
-
ok: false,
|
|
663
|
-
reason: "session_incomplete",
|
|
664
|
-
missing_leaves: readiness.missingLeaves,
|
|
665
|
-
held_messages: readiness.heldCount,
|
|
666
|
-
held_own: readiness.heldOwn,
|
|
667
|
-
held_received: readiness.heldReceived,
|
|
668
|
-
guidance: `This side of the conversation is incomplete, so sealing now would produce a chain the counterparty cannot co-sign — and that refusal is terminal, leaving a force-abandon with no notarized receipt as the only way out. ` +
|
|
669
|
-
`You hold ${readiness.treeSize} message(s); the relay has witnessed ${readiness.highWaterSeq + 1}` +
|
|
670
|
-
waiting +
|
|
671
|
-
`. Everything here is waiting on an earlier message from the counterparty that has not arrived; the daemon just pulled from the relay and the gap is still there, so wait a moment and close again. If it does not resolve, cello_transcript ${sessionId} shows what did arrive, and cello_close_session ${sessionId} { force: true } abandons it terminally (no receipt).`,
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* AC-011: a seal attempt is already running for this session.
|
|
676
|
-
*
|
|
677
|
-
* DOD-M15-CLOSEWAIT-1 review HIGH-3 rewrote this, because the change made it COMMON and the old
|
|
678
|
-
* wording did not survive contact:
|
|
679
|
-
*
|
|
680
|
-
* - it said *"wait for `session.interrupted.sealed` to appear in the daemon logs"*. That event
|
|
681
|
-
* is emitted NOWHERE in the tree — grep finds it only inside this string. An operator would
|
|
682
|
-
* tail a log for a line that cannot arrive.
|
|
683
|
-
* - it named the seal-INTERRUPTED subsystem, but the common case now is an ACTIVE session
|
|
684
|
-
* whose background ceremony is mid-flight. Wrong subsystem, on the path an operator most
|
|
685
|
-
* often reaches.
|
|
686
|
-
* - it was hard to reach before, because the first close held the caller for the whole
|
|
687
|
-
* ceremony. Now the operator has their terminal back for up to eleven minutes, and
|
|
688
|
-
* re-closing is the obvious move.
|
|
689
|
-
*/
|
|
690
|
-
if (sealInterruptedInProgress.has(sealKey(record.agent_name, sessionId))) {
|
|
691
|
-
return {
|
|
692
|
-
ok: false,
|
|
693
|
-
reason: "seal_in_progress",
|
|
694
|
-
seal_status: "committed",
|
|
695
|
-
guidance: "A seal ceremony is ALREADY RUNNING for this session — your commitment is recorded and " +
|
|
696
|
-
"there is nothing to retry. This is the normal state after a close: the close answers as " +
|
|
697
|
-
"soon as the commitment is durable and notarizes in the background, which waits for the " +
|
|
698
|
-
"counterparty and can take several minutes. Fetch the result with cello_sealed_receipt " +
|
|
699
|
-
`(session_id ${sessionId}); a seal_in_progress answer there means the same thing — still ` +
|
|
700
|
-
"running, not failed. Do NOT re-close with { force: true } to hurry it: forcing ABANDONS " +
|
|
701
|
-
"the session and permanently forfeits the receipt this ceremony is about to produce.",
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
// DB-001: signaling stream reconnecting
|
|
705
|
-
if (record.status === "interrupted" && signalingFor(record.agent_name)?.status === "reconnecting") {
|
|
706
|
-
return {
|
|
707
|
-
ok: false,
|
|
708
|
-
reason: "signaling_reconnecting",
|
|
709
|
-
guidance: "The directory signaling stream is reconnecting. Wait for directory_signaling to show connected in cello status before initiating seal-interrupted. The daemon reconnects automatically — no manual intervention required.",
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
// AC-012 / AC-013: seal-interrupted bilateral flow for interrupted sessions.
|
|
713
|
-
// BLOCKING-1 fix: await the flow synchronously so the caller receives the real result
|
|
714
|
-
// (counterparty_unavailable, rejected_by_counterparty, or sealed).
|
|
715
|
-
// The sealInterruptedInProgress Set still guards concurrent calls (AC-011).
|
|
716
|
-
if (record.status === "interrupted") {
|
|
717
|
-
// H-1: the Merkle root at interruption is held by the client (the daemon
|
|
718
|
-
// does not maintain the session Merkle tree). The client supplies it here
|
|
719
|
-
// so both parties co-sign over the same root. Absent → empty string, in
|
|
720
|
-
// which case the bilateral commitment binds leafCount only.
|
|
721
|
-
const merkleRootAtInterruption = typeof params?.merkleRootAtInterruption === "string" ? params.merkleRootAtInterruption : "";
|
|
722
|
-
sealInterruptedInProgress.add(sealKey(record.agent_name, sessionId));
|
|
723
|
-
const correlationId = randomUUID();
|
|
724
|
-
// CROSS-NODE: dial the broker for the duration of the seal, exactly as the active path does.
|
|
725
|
-
// This branch sends the same seal frames and used to send them without the dial, so an
|
|
726
|
-
// interrupted close between two agents on different nodes timed out on BOTH sides while each
|
|
727
|
-
// counterparty was online — the frames were pushed to a stream the broker did not hold.
|
|
728
|
-
let sealBrokerConn = null;
|
|
729
|
-
// THE BILATERAL COMMITMENT IS NOT THE SEAL.
|
|
730
|
-
//
|
|
731
|
-
// Exactly one thing in the system causes a notarization: a SEAL ctrl leaf posted to the
|
|
732
|
-
// RELAY, which hands the chain to a directory for the FROST round once both sides have
|
|
733
|
-
// posted. The ACTIVE close does that below. This branch never did — it exchanged signed
|
|
734
|
-
// leaves with the counterparty over the directory's signaling pass-through, stored both
|
|
735
|
-
// halves, set the session to `seal_interrupted_pending` and returned. The directory only
|
|
736
|
-
// FORWARDS those frames; it reads nothing out of them and starts nothing on the back of them.
|
|
737
|
-
// So an interrupted session reached a mutually signed record that nobody was ever asked to
|
|
738
|
-
// notarize, and sat there until the relay swept it.
|
|
739
|
-
//
|
|
740
|
-
// BEST-EFFORT, deliberately. The commitment has already SUCCEEDED and is durable on both
|
|
741
|
-
// sides. The relay drops a session 24 hours after its last message, so this call will often
|
|
742
|
-
// arrive to find it gone — and turning that into a reported FAILURE is precisely what sends an
|
|
743
|
-
// operator to force:true, which permanently forfeits the half they still hold. A missing
|
|
744
|
-
// notarization is a lesser harm than a destroyed commitment, so the close keeps its result and
|
|
745
|
-
// the shortfall is logged rather than raised.
|
|
746
|
-
//
|
|
747
|
-
// No waiter is registered and nothing is awaited: the counterparty may not close for hours, and
|
|
748
|
-
// blocking a close on that would be a worse lie than returning the honest pending status. When
|
|
749
|
-
// their leaf does land, the already-wired session_sealed listener records the certificate.
|
|
750
|
-
//
|
|
751
|
-
// DOD-M12B-INTERRUPTED-ESCALATE-1: submitting the leaf was never enough, and this is where
|
|
752
|
-
// the receipt actually gets earned. The relay notarizes only once BOTH parties have posted a
|
|
753
|
-
// SEAL ctrl leaf, and the responder never posts one — `inbound-seal-request.ts` persists its
|
|
754
|
-
// commitment, acks, and stops. So waiting for the relay round here is waiting for something
|
|
755
|
-
// that cannot happen. The escalation asks the directory to notarize with the counterparty
|
|
756
|
-
// ABSENT, which is precisely what a unilateral seal is for.
|
|
757
|
-
const notarize = async (result) => {
|
|
758
|
-
// WHEN IT IS ALLOWED TO ESCALATE, and this is a trust decision, not a retry policy:
|
|
759
|
-
// result.ok — both sides signed the same root. Agreed.
|
|
760
|
-
// seal_interrupted_counterparty_unavailable — they never answered. The exact case the
|
|
761
|
-
// unilateral seal exists for.
|
|
762
|
-
// NEVER after `seal_interrupted_rejected_by_counterparty`. A rejection means the two trees
|
|
763
|
-
// DISAGREE (leaf_count_mismatch, a diverging index). Notarizing our own root over their
|
|
764
|
-
// stated objection is the one thing a trust layer must not do, however stuck the session is.
|
|
765
|
-
const mayEscalate = result.ok || result.reason === "seal_interrupted_counterparty_unavailable";
|
|
766
|
-
if (!mayEscalate)
|
|
767
|
-
return result;
|
|
768
|
-
// DOD-M12B-SEAL-BILATERAL-FIRST-1 — a BILATERAL seal is already running; do not take the
|
|
769
|
-
// worse artifact instead. The counterparty answered that it is on the relay's bilateral
|
|
770
|
-
// ceremony and was waiting for our half, which the flow just submitted. Escalating now asks
|
|
771
|
-
// the directory to notarize with that counterparty marked ABSENT — for a peer that is
|
|
772
|
-
// demonstrably present. The ACTIVE close gives a bilateral round eleven minutes before it
|
|
773
|
-
// escalates; this path was giving it none, and for any orphan older than the delivery grace
|
|
774
|
-
// (every one of the measured 26) the escalation is allowed, so the downgrade was certain.
|
|
775
|
-
if (result.ok && result.ceremony === "relay_bilateral") {
|
|
776
|
-
logger.info("session.seal.bilateral.in_flight", {
|
|
777
|
-
agentName: record.agent_name, sessionId, correlationId,
|
|
778
|
-
impact: "our half is submitted and the relay has both parties' leaves; the bilateral receipt is the better artifact and is already coming",
|
|
779
|
-
});
|
|
780
|
-
return {
|
|
781
|
-
...result,
|
|
782
|
-
seal_receipt: "bilateral_in_flight",
|
|
783
|
-
guidance: "The counterparty is running the relay's bilateral seal and was waiting for this side's half, which has now been submitted. That produces a BILATERAL receipt — a better artifact than the unilateral one a close would otherwise take, and it does not record the counterparty as absent. Watch for the seal to land (cello_sessions) and read it with cello_sealed_receipt.",
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
const uni = await submitAndEscalate(record, sessionId, correlationId);
|
|
787
|
-
if ("escalated" in uni) {
|
|
788
|
-
// THE REASON IS IN HAND — do not drop it. `result.ok` is true here (the bilateral
|
|
789
|
-
// commitment succeeded), so returning it bare makes the restart-seal resolver log
|
|
790
|
-
// `resolved` and dequeue a session that got NO notarization: the system reporting health
|
|
791
|
-
// it does not have. Same shape the pending branch was blocked for.
|
|
792
|
-
if (!result.ok)
|
|
793
|
-
return result;
|
|
794
|
-
return {
|
|
795
|
-
...result,
|
|
796
|
-
seal_receipt: "outstanding",
|
|
797
|
-
seal_pending_reason: uni.reason,
|
|
798
|
-
guidance: `The bilateral commitment is recorded, but this side could not post the SEAL leaf a notarization is requested with: ${uni.reason}. ` +
|
|
799
|
-
sealSubmitCause(uni.reason),
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
if (uni.ok)
|
|
803
|
-
return uni; // A REAL RECEIPT — the whole point of this line.
|
|
804
|
-
// BEST-EFFORT, and the commitment STANDS. Turning a successful commitment into a reported
|
|
805
|
-
// failure is exactly what sends an operator to `force: true`, which permanently forfeits the
|
|
806
|
-
// half they still hold — the comment above says so and it is still true. What changes is
|
|
807
|
-
// that the answer now says a receipt is OUTSTANDING rather than implying one exists, and
|
|
808
|
-
// carries the directory's own countdown so a caller that can wait does not have to guess.
|
|
809
|
-
logger.info("session.interrupted.seal.escalation.pending", {
|
|
810
|
-
agentName: record.agent_name, sessionId, reason: uni.reason, correlationId,
|
|
811
|
-
...(uni.retry_after_seconds !== undefined ? { retryAfterSeconds: uni.retry_after_seconds } : {}),
|
|
812
|
-
});
|
|
813
|
-
return {
|
|
814
|
-
...result,
|
|
815
|
-
seal_receipt: "outstanding",
|
|
816
|
-
seal_pending_reason: uni.reason,
|
|
817
|
-
...(uni.retry_after_seconds !== undefined ? { retry_after_seconds: uni.retry_after_seconds } : {}),
|
|
818
|
-
guidance: uni.guidance,
|
|
819
|
-
};
|
|
820
|
-
};
|
|
821
|
-
try {
|
|
822
|
-
// Pre-dial ONLY from memory. This costs nothing when the map is empty, so the seal waiter
|
|
823
|
-
// still registers immediately — a bilateral seal that lands promptly must not be missed
|
|
824
|
-
// because we were off looking something up.
|
|
825
|
-
sealBrokerConn = await openSealBrokerConnection(record.agent_name, sessionId, correlationId);
|
|
826
|
-
const first = await handleSealInterruptedFlow(sessionId, record, correlationId, merkleRootAtInterruption);
|
|
827
|
-
// DISCOVER-AND-RETRY, and only on the one reason that discovery can actually fix. After a
|
|
828
|
-
// restart the broker map is empty, so the attempt above went out on the home stream and the
|
|
829
|
-
// brokering node had no stream for the counterparty to push to. Asking where they are NOW
|
|
830
|
-
// and dialling there is the repair — paid for only on the path that just failed, never on
|
|
831
|
-
// the happy path, and never delaying the waiter.
|
|
832
|
-
const counterparty = record.counterparty_pubkey;
|
|
833
|
-
if (first.ok || first.reason !== "seal_interrupted_counterparty_unavailable" || !counterparty || sealBrokerConn) {
|
|
834
|
-
return await notarize(first);
|
|
835
|
-
}
|
|
836
|
-
sealBrokerConn = await openSealBrokerConnection(record.agent_name, sessionId, correlationId, { counterpartyPubkeyHex: counterparty });
|
|
837
|
-
// Escalate even here: discovery found nowhere to dial, which is the STRONGEST case for a
|
|
838
|
-
// unilateral seal, not a reason to give up on the receipt.
|
|
839
|
-
if (!sealBrokerConn)
|
|
840
|
-
return await notarize(first);
|
|
841
|
-
logger.info("session.seal.broker.retry_after_discovery", { agentName: record.agent_name, sessionId, correlationId });
|
|
842
|
-
// OVER THE VISITING CONNECTION. Dialling their node is only half of it — the request must
|
|
843
|
-
// be SENT there too. Sent on the home stream it reaches our own node, which holds no stream
|
|
844
|
-
// for a counterparty homed elsewhere, logs "target offline" and answers nothing.
|
|
845
|
-
return await notarize(await handleSealInterruptedFlow(sessionId, record, correlationId, merkleRootAtInterruption, sealBrokerConn.mgr));
|
|
846
|
-
}
|
|
847
|
-
finally {
|
|
848
|
-
// Release the transient connection; the seal result stands either way.
|
|
849
|
-
if (sealBrokerConn) {
|
|
850
|
-
try {
|
|
851
|
-
await sealBrokerConn.stop("seal-complete");
|
|
852
|
-
}
|
|
853
|
-
catch (err) {
|
|
854
|
-
logger.warn("session.seal.broker.release_failed", { sessionId, reason: extractErrorMessage(err) });
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
sealInterruptedInProgress.delete(sealKey(record.agent_name, sessionId));
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
// CELLO-M7-DAEMON-004 (AC-003): ACTIVE session — initiate the active-session
|
|
861
|
-
// seal over the daemon's OWN tree root. SI-001: any caller-supplied
|
|
862
|
-
// merkleRoot is IGNORED; the daemon signs only the root it built itself.
|
|
863
|
-
//
|
|
864
|
-
// round-2 finding #4: the active path must take the SAME concurrency guard as
|
|
865
|
-
// the interrupted path (the top-of-handler check at sealInterruptedInProgress.has
|
|
866
|
-
// rejects re-entry). Without adding to the set here, two concurrent active closes
|
|
867
|
-
// would both send a seal_interrupted_request and both await acks (double seal).
|
|
868
|
-
if (record.status === "active") {
|
|
869
|
-
sealInterruptedInProgress.add(sealKey(record.agent_name, sessionId));
|
|
870
|
-
const correlationId = randomUUID();
|
|
871
|
-
// Fix #1 (cross-node seal-liveness): if this session was brokered by another node, the seal_verified
|
|
872
|
-
// + session_sealed frames are pushed by that BROKER — but the initiator released its visiting
|
|
873
|
-
// connection after setup, so on the home stream they never arrive and close times out. Re-open a
|
|
874
|
-
// transient visiting connection to the broker (seal-capable — openVisitingConnection now wires the
|
|
875
|
-
// seal handlers) for the duration of the seal, then release it in finally. Same-node sessions have
|
|
876
|
-
// no entry here and use the home stream unchanged.
|
|
877
|
-
let sealBrokerConn = null;
|
|
878
|
-
// DOD-M15-CLOSEWAIT-1: set when the seal tail is handed to a background task, so the enclosing
|
|
879
|
-
// finally does not release a broker connection that ceremony still needs.
|
|
880
|
-
let handedOff = false;
|
|
881
|
-
/**
|
|
882
|
-
* The caller may still ASK to block. Default is the new contract (answer on commitment);
|
|
883
|
-
* `wait_for_seal: true` restores the inline behaviour for a script that genuinely wants the
|
|
884
|
-
* receipt in one call and will wait up to eleven minutes for it. Opt-IN rather than opt-out,
|
|
885
|
-
* because the default has to be the one that does not freeze an interactive operator.
|
|
886
|
-
*/
|
|
887
|
-
const waitForSeal = params?.["wait_for_seal"] === true;
|
|
888
|
-
try {
|
|
889
|
-
// Memory only, deliberately. An ACTIVE session has not been through the restart that
|
|
890
|
-
// empties the broker map, so the entry is there when it is needed — and a lookup here would
|
|
891
|
-
// sit in front of the waiter registration two lines below, which is what must not happen.
|
|
892
|
-
sealBrokerConn = await openSealBrokerConnection(record.agent_name, sessionId, correlationId);
|
|
893
|
-
// M7 DOD-SPINE-7: relay-mediated bilateral seal. Submit our SEAL ctrl leaf to the
|
|
894
|
-
// relay witness; when the counterparty ALSO closes, the relay's #maybeProcessSeal
|
|
895
|
-
// fires → directory processSeal rebuilds + verifies the signed chain → FROST
|
|
896
|
-
// notarization → session_sealed to BOTH parties. Register the waiter BEFORE
|
|
897
|
-
// submitting so the notification can never race ahead of us.
|
|
898
|
-
let resolveSeal;
|
|
899
|
-
const sealedP = new Promise((r) => { resolveSeal = r; });
|
|
900
|
-
pendingSealWaiters.set(sealKey(record.agent_name, sessionId), resolveSeal);
|
|
901
|
-
const submit = await sessionNodeManager.submitSealLeaf(record.agent_name, sessionId, correlationId);
|
|
902
|
-
// M7-UPGRADE-002: the auto-acknowledge path may have already submitted THIS party's
|
|
903
|
-
// responder SEAL leaf (it won the race against this explicit close). That is success, not
|
|
904
|
-
// failure — keep the waiter registered and fall through to await session_sealed (the
|
|
905
|
-
// auto-ack's submission drives the same bilateral seal).
|
|
906
|
-
if (!submit.ok && submit.reason !== "responder_seal_already_submitted") {
|
|
907
|
-
pendingSealWaiters.delete(sealKey(record.agent_name, sessionId));
|
|
908
|
-
// THE SEAL MAY HAVE ALREADY SUCCEEDED, so returning the raw reason is an exit-point label
|
|
909
|
-
// with the wrong diagnosis attached: the guidance below blames relay reachability when the
|
|
910
|
-
// relay was fine and the seal is notarized and DURABLE. The operator who asked to close is
|
|
911
|
-
// told it failed and gets no root — the receipt being the whole point of closing.
|
|
912
|
-
//
|
|
913
|
-
// HOW the window opens (corrected — my first version named a mechanism that does not
|
|
914
|
-
// exist): it is NOT that teardown precedes the status flip. In `destroySessionNode` the
|
|
915
|
-
// flip happens BEFORE `#activeNodes.delete`, so that ordering is safe. The real producers
|
|
916
|
-
// are (a) `record` is a snapshot taken at the top of this handler, before a broker dial that
|
|
917
|
-
// can take up to 10s, so it can be stale by now; and (b) `retireSessionNode` stops the node
|
|
918
|
-
// WITHOUT changing the DB status, and `destroySessionNode` then early-returns above the
|
|
919
|
-
// flip. Both parties closing at once is the ordinary case, so whichever call arrives second
|
|
920
|
-
// meets it.
|
|
921
|
-
//
|
|
922
|
-
// SCOPED to that reason. This previously fired on EVERY `!submit.ok`, while justifying
|
|
923
|
-
// itself for one — and a future failure (a tree mismatch, a signing failure) would have been
|
|
924
|
-
// absorbed into `ok:true` with an old root and never surfaced.
|
|
925
|
-
//
|
|
926
|
-
// Why this is not the fetched-receipt path wearing a different hat — the distinction is
|
|
927
|
-
// narrower than I first wrote, so stated exactly: the stored cert passed the
|
|
928
|
-
// field-completeness gate, passed the independent frontier re-derivation (the client never
|
|
929
|
-
// takes the directory's word for that value), and where this agent holds the signer key it
|
|
930
|
-
// was cryptographically verified. None of those were true of a fetched root. It is also not
|
|
931
|
-
// a new claim — `cello_get_sealed_receipt` already returns this exact cert with no status
|
|
932
|
-
// gate. Honest caveat: for a NON-initiator the stored cert is recorded `verified:false`,
|
|
933
|
-
// so it is ultimately directory-attested over an authenticated channel — the same trust the
|
|
934
|
-
// ordinary bilateral close already returns, not a new one introduced here.
|
|
935
|
-
// M12-P15 (review HIGH-3): this used to key on `session_node_unavailable` ALONE. That was
|
|
936
|
-
// the only reason submitSealLeaf could give when the node was gone — until M12-P15 taught
|
|
937
|
-
// it to fall back to a detached transport, after which it can no longer produce that
|
|
938
|
-
// string at all and this recovery went DEAD. The ordinary double-close then dialled the
|
|
939
|
-
// relay, submitted a second ctrl leaf to a session the relay had already destroyed on
|
|
940
|
-
// seal, and told the operator the close failed — the M12 defect back under a new label.
|
|
941
|
-
// The question this branch actually asks is "we could not submit; is the seal already
|
|
942
|
-
// durable?", so it keys on every reason that means exactly that. A stored certificate is
|
|
943
|
-
// the answer either way, and consulting it is cheap.
|
|
944
|
-
const SEAL_MAY_ALREADY_BE_DURABLE = new Set([
|
|
945
|
-
"session_node_unavailable",
|
|
946
|
-
"no_persisted_relay_endpoint",
|
|
947
|
-
"standing_receiver_unavailable",
|
|
948
|
-
"relay_client_unavailable",
|
|
949
|
-
"relay_unavailable",
|
|
950
|
-
"relay_session_gone",
|
|
951
|
-
"session_not_found",
|
|
952
|
-
/**
|
|
953
|
-
* `DOD-M15-TOKENSTALE-1` review F1 — these arrive here ONLY since that unit split the
|
|
954
|
-
* submit boundary's answer. They used to reach this line as `relay_unavailable` and were
|
|
955
|
-
* covered by the entry above; naming them keeps this set answering the question it says
|
|
956
|
-
* it answers ("could the seal already be durable?") rather than the narrower one the
|
|
957
|
-
* old label happened to encode.
|
|
958
|
-
*/
|
|
959
|
-
"online_token_expired",
|
|
960
|
-
"online_token_required",
|
|
961
|
-
"online_token_pubkey_mismatch",
|
|
962
|
-
]);
|
|
963
|
-
const localCert = SEAL_MAY_ALREADY_BE_DURABLE.has(submit.reason)
|
|
964
|
-
? sessionNodeManager.getSealCertificate(record.agent_name, sessionId)
|
|
965
|
-
: null;
|
|
966
|
-
if (localCert?.sealed_root) {
|
|
967
|
-
logger.info("session.seal.completed", {
|
|
968
|
-
sessionId, sealedRoot: localCert.sealed_root, role: "already_sealed_locally", correlationId,
|
|
969
|
-
});
|
|
970
|
-
crossNodeBrokerBySession.delete(`${record.agent_name}:${sessionId}`);
|
|
971
|
-
return { ok: true, sealed_root: localCert.sealed_root, legibility: localCert.legibility };
|
|
972
|
-
}
|
|
973
|
-
/**
|
|
974
|
-
* ⚠️ THE QUESTION IS "COULD WE NOT USE THE RELAY?", NOT "IS THE RELAY DOWN?" —
|
|
975
|
-
* `DOD-M15-TOKENSTALE-1` review F1.
|
|
976
|
-
*
|
|
977
|
-
* This read `submit.reason === "relay_unavailable"` while that string was the only way a
|
|
978
|
-
* relay-less submit could be reported. Splitting the boundary so a dead credential names
|
|
979
|
-
* itself broke that equivalence, and the consequence was the opposite of the unit's
|
|
980
|
-
* intent: an agent with an expired token has BY DEFINITION no relay witness, which is
|
|
981
|
-
* precisely the state this fallback exists for — and it would have been the first to stop
|
|
982
|
-
* taking it, told instead to "retry once the relay is reachable" about a relay that was
|
|
983
|
-
* never the problem.
|
|
984
|
-
*/
|
|
985
|
-
if (submit.reason === "relay_unavailable" || isLocalCredentialRefusal(submit.reason)) {
|
|
986
|
-
// No relay witness for this session (direct/interrupted, or our own credential is dead)
|
|
987
|
-
// — fall back to the directory-mediated bilateral-ack seal.
|
|
988
|
-
return await handleActiveSealFlow(sessionId, record, correlationId);
|
|
989
|
-
}
|
|
990
|
-
return {
|
|
991
|
-
ok: false,
|
|
992
|
-
reason: submit.reason,
|
|
993
|
-
guidance: "The SEAL leaf could not be submitted to the relay witness. Retry once the relay is reachable (cello status).",
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
// Both parties must close for the directory to notarize. Await session_sealed; a
|
|
997
|
-
// timeout means the counterparty has not closed yet (our leaf is recorded — the
|
|
998
|
-
// session seals when they call cello_close_session). CELLO_SEAL_BILATERAL_TIMEOUT_MS
|
|
999
|
-
// tunes how long to wait for the counterparty before escalating to a unilateral seal.
|
|
1000
|
-
// DOD-SEAL-BILATERAL-TIMEOUT-1: default is 660 s (11 min) — deliberately just over
|
|
1001
|
-
// the directory's deliveryGraceSeconds default (600 s / 10 min), so the bilateral
|
|
1002
|
-
// timeout always expires AFTER the grace window; override via the env var for tests or
|
|
1003
|
-
// operators who need a shorter window.
|
|
1004
|
-
//
|
|
1005
|
-
// ⚠️ THIS USED TO SAY seal_unilateral_too_early WAS "STRUCTURALLY UNREACHABLE", AND
|
|
1006
|
-
// 070-CARRIEDSEAL MADE THAT FALSE. Rewritten rather than deleted, because a reader who
|
|
1007
|
-
// believed it would be surprised by the refusal and go looking for a bug. The zero below
|
|
1008
|
-
// skips the window entirely, so a session closed with no relay AND less than the grace
|
|
1009
|
-
// period of age now reaches that refusal — which is correct (the directory's floor is a
|
|
1010
|
-
// real floor) and is loud, with its own guidance. It is no longer unreachable; it is
|
|
1011
|
-
// reachable exactly when there was never a ceremony to wait out.
|
|
1012
|
-
/**
|
|
1013
|
-
* ⚠️ 070-CARRIEDSEAL — THERE IS NOTHING TO WAIT FOR WHEN NO RELAY TOOK THE LEAF.
|
|
1014
|
-
*
|
|
1015
|
-
* A bilateral seal is a ceremony the RELAY runs: it watches its own leaf log for both
|
|
1016
|
-
* parties' control leaves and drives the directory itself. If no relay answered, our leaf
|
|
1017
|
-
* is not in any relay's log, the counterparty's closing leaf can never reach us, and the
|
|
1018
|
-
* ceremony cannot begin — so the eleven-minute window is eleven minutes of an operator
|
|
1019
|
-
* watching nothing happen before the solo seal that was always the only available outcome.
|
|
1020
|
-
*
|
|
1021
|
-
* It is also the last live-relay requirement on the close path. The evidence stopped needing
|
|
1022
|
-
* the relay when the leaf was signed here; the WAIT still assumed one, and a wait for
|
|
1023
|
-
* something that cannot happen is a dependency wearing a stopwatch.
|
|
1024
|
-
*
|
|
1025
|
-
* Zero, not "shorter": this is not a tuning judgement about how long a counterparty might
|
|
1026
|
-
* take. It is the absence of a counterparty channel.
|
|
1027
|
-
*/
|
|
1028
|
-
const bilateralTimeoutMs = submit.viaLocalTerminus === true
|
|
1029
|
-
? 0
|
|
1030
|
-
: Number(process.env["CELLO_SEAL_BILATERAL_TIMEOUT_MS"]) || 660_000;
|
|
1031
|
-
// DOD-M12B-CLOSE-SILENT-WAIT-1: SAY SO BEFORE THE SILENCE, not after it.
|
|
1032
|
-
//
|
|
1033
|
-
// This call is about to block for up to eleven minutes and return nothing. Measured
|
|
1034
|
-
// 2026-08-17: seal leaf submitted 16:48:55, ceremony completed 17:00:01 — and the operator
|
|
1035
|
-
// saw a frozen command for all of it. They concluded it was broken and force-abandoned
|
|
1036
|
-
// seventeen sessions, which forfeits the exact receipt this wait is earning. The log is what
|
|
1037
|
-
// a second window can read while the first is blocked, so it has to carry both facts: how
|
|
1038
|
-
// long this can legitimately take, and what forcing costs.
|
|
1039
|
-
logger.warn("session.seal.awaiting_counterparty", {
|
|
1040
|
-
sessionId, agentName: record.agent_name, deadlineMs: bilateralTimeoutMs, correlationId,
|
|
1041
|
-
impact: bilateralTimeoutMs === 0
|
|
1042
|
-
? "no relay took this side's closing leaf, so no bilateral ceremony can begin and there is nothing to wait for. The seal goes straight to the solo path over the evidence this side already holds, and produces a real receipt. Do NOT force-abandon it — that forfeits the receipt."
|
|
1043
|
-
: `the seal is waiting for the counterparty for up to ${Math.round(bilateralTimeoutMs / 60_000)} minutes, then escalates to a unilateral seal and produces a real receipt. It is working. Do NOT force-abandon it — that forfeits the receipt this wait is earning.`,
|
|
1044
|
-
});
|
|
1045
|
-
/**
|
|
1046
|
-
* DOD-M15-CLOSEWAIT-1 — THE TAIL, EXTRACTED SO IT CAN OUTLIVE THE RESPONSE.
|
|
1047
|
-
*
|
|
1048
|
-
* Everything from here to the escalation is "wait for the counterparty, then escalate if
|
|
1049
|
-
* they never came". It ran inline, which is why the caller sat on a frozen command for up to
|
|
1050
|
-
* eleven minutes while it worked correctly — and why one operator force-abandoned seventeen
|
|
1051
|
-
* sessions, forfeiting the exact receipts the wait was earning.
|
|
1052
|
-
*
|
|
1053
|
-
* Extracting it changes nothing about WHAT is signed or in what order: same race, same
|
|
1054
|
-
* escalation, same leaf. It changes only who waits for it. (Decisions Carried #4.)
|
|
1055
|
-
*
|
|
1056
|
-
* `rec`/`sid` are captured because TypeScript discards the narrowing of `record` and
|
|
1057
|
-
* `sessionId` inside a nested function.
|
|
1058
|
-
*/
|
|
1059
|
-
const rec = record;
|
|
1060
|
-
const sid = sessionId;
|
|
1061
|
-
const awaitSealAndEscalate = async () => {
|
|
1062
|
-
let timer;
|
|
1063
|
-
const timeoutP = new Promise((r) => { timer = setTimeout(() => r(null), bilateralTimeoutMs); });
|
|
1064
|
-
const sealedCompletion = await Promise.race([sealedP, timeoutP]);
|
|
1065
|
-
clearTimeout(timer);
|
|
1066
|
-
pendingSealWaiters.delete(sealKey(record.agent_name, sessionId));
|
|
1067
|
-
/**
|
|
1068
|
-
* THE CERTIFICATE WAS REFUSED — `DOD-M15-SEALWIRE-1` bullet 2 (review F4).
|
|
1069
|
-
*
|
|
1070
|
-
* The seal arrived, its signature verified, and its root did not describe this conversation.
|
|
1071
|
-
* Before this the coordinator logged and dropped the waiter, so control fell through to the
|
|
1072
|
-
* eleven-minute timeout and the operator was told the counterparty had not closed — about a
|
|
1073
|
-
* counterparty who had, and whose certificate had just been refused here.
|
|
1074
|
-
*
|
|
1075
|
-
* Answered immediately instead, with what was actually detected. NOT retryable, and it says
|
|
1076
|
-
* so: `session_sealed` is delivered once, so a retry re-enters the same wait and then finds
|
|
1077
|
-
* the relay session gone.
|
|
1078
|
-
*/
|
|
1079
|
-
const refused = sealedCompletion !== null && "refused" in sealedCompletion ? sealedCompletion : null;
|
|
1080
|
-
if (refused) {
|
|
1081
|
-
logger.error("session.seal.refused", {
|
|
1082
|
-
sessionId, agentName: record.agent_name, correlationId,
|
|
1083
|
-
reason: refused.reason, detail: refused.detail, source: refused.source ?? "local",
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
/**
|
|
1087
|
-
* 🚨 A DIRECTORY REFUSAL IS NOT THE END OF THE CLOSE — `DOD-M15-SEALPARTIES-1`, review F1.
|
|
1088
|
-
*
|
|
1089
|
-
* This branch was written for ONE producer: this daemon's own root check, where terminating
|
|
1090
|
-
* is right because there is nothing further to ask for. `DOD-M15-SEALPARTIES-1` made the
|
|
1091
|
-
* DIRECTORY a second producer of the same shape, and it silently inherited the terminal
|
|
1092
|
-
* semantics — so every directory refusal (`seal_approval_missing`, `seal_leaves_invalid`,
|
|
1093
|
-
* `merkle_root_mismatch`, …) stopped escalating.
|
|
1094
|
-
*
|
|
1095
|
-
* **What that cost a person:** before, a relay that dropped a leaf meant an eleven-minute
|
|
1096
|
-
* wait and then a SOLO receipt. After, the refusal came back in seconds and the close ended
|
|
1097
|
-
* — no receipt, in a case where one used to arrive. Faster and worse, which is the trap this
|
|
1098
|
-
* order names, reached through the refusal listener rather than through the approval rule.
|
|
1099
|
-
*
|
|
1100
|
-
* `seal_leaves_invalid` is the sharpest of them: the relay tampered, and a solo seal over
|
|
1101
|
-
* this side's OWN carried chain is precisely the remedy. Whether a PRESENT counterparty may
|
|
1102
|
-
* be sealed around is the directory's call on the unilateral gate, not this daemon's — so it
|
|
1103
|
-
* asks, rather than deciding not to.
|
|
1104
|
-
*/
|
|
1105
|
-
if (refused && (refused.source ?? "local") === "local") {
|
|
1106
|
-
return {
|
|
1107
|
-
ok: false,
|
|
1108
|
-
reason: refused.reason,
|
|
1109
|
-
seal_status: "refused",
|
|
1110
|
-
...(refused.ownRootHex ? { own_root: refused.ownRootHex } : {}),
|
|
1111
|
-
/**
|
|
1112
|
-
* ⚠️ THE REMEDY COMES FROM THE PARTY THAT KNOWS THE CAUSE — `DOD-M15-SEALPARTIES-1`.
|
|
1113
|
-
*
|
|
1114
|
-
* The text below was written when this branch had exactly one producer: this daemon's
|
|
1115
|
-
* own root check, where "the directory signed a root that is not your conversation" is
|
|
1116
|
-
* always the right sentence. The directory's own refusals now land here too, and for
|
|
1117
|
-
* those it would be a confident falsehood — it names a signature that was never
|
|
1118
|
-
* produced. So a refusal that carries its own guidance keeps it, and the local
|
|
1119
|
-
* root-mismatch text stays as the fallback for the case it was written for.
|
|
1120
|
-
*/
|
|
1121
|
-
guidance: refused.guidance ??
|
|
1122
|
-
"The directory returned a VALIDLY SIGNED seal whose root does not describe this " +
|
|
1123
|
-
`conversation (${refused.detail}). This daemon refused it: the session is NOT ` +
|
|
1124
|
-
"marked sealed, your transcript is untouched, and nothing was signed with your key. " +
|
|
1125
|
-
"Do NOT retry the close — the seal notification is delivered once, so a retry waits the " +
|
|
1126
|
-
"full window and then finds the relay session gone. Do NOT force-abandon either: that " +
|
|
1127
|
-
"permanently forfeits any receipt. Compare the leaf count and the message list with your " +
|
|
1128
|
-
"counterparty, and report the session id — this is a directory or relay fault, not " +
|
|
1129
|
-
"something you did.",
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
const succeeded = sealedCompletion !== null && !("refused" in sealedCompletion) ? sealedCompletion : null;
|
|
1133
|
-
if (succeeded) {
|
|
1134
|
-
logger.info("session.seal.completed", { sessionId, sealedRoot: succeeded.rootHex, role: "bilateral", correlationId });
|
|
1135
|
-
crossNodeBrokerBySession.delete(`${record.agent_name}:${sessionId}`); // Fix #1 review: evict on terminal seal success (a FAILED close keeps the entry so a retry can still reconnect).
|
|
1136
|
-
// M7-SESSION-004 (AC-006): return the legibility certificate on the seal completion so
|
|
1137
|
-
// a reader gets it on the same surface that proves the seal — per-party frontiers, attestation modes, and final_message.answered.
|
|
1138
|
-
return { ok: true, sealed_root: succeeded.rootHex, legibility: succeeded.legibility };
|
|
1139
|
-
}
|
|
1140
|
-
// M8B FINDING-1: `responder_seal_already_submitted` has two producers — (a) the auto-ack
|
|
1141
|
-
// path submitted our leaf because the COUNTERPARTY's SEAL arrived, or (b) our OWN earlier
|
|
1142
|
-
// close submitted it and the counterparty never co-closed. The result now carries the
|
|
1143
|
-
// first submit's reportedRootHex/sequenceNumber, so a retry close can still escalate to a
|
|
1144
|
-
// unilateral seal (case b — the live-deadlock path). We deliberately do NOT distinguish
|
|
1145
|
-
// (a) from (b) client-side: the bilateral wait above already gave case (a) its window, and
|
|
1146
|
-
// the directory's grace/already-sealed gates arbitrate a redundant seal_unilateral — that
|
|
1147
|
-
// is the sovereign-node-correct shape. Only when the not-ok result carries NO root (the
|
|
1148
|
-
// first submit is still in flight) is "pending" the honest terminal answer here.
|
|
1149
|
-
const escalation = submit.ok
|
|
1150
|
-
? { reportedRootHex: submit.reportedRootHex, sequenceNumber: submit.sequenceNumber }
|
|
1151
|
-
: submit.reason === "responder_seal_already_submitted" &&
|
|
1152
|
-
typeof submit.reportedRootHex === "string" &&
|
|
1153
|
-
typeof submit.sequenceNumber === "number"
|
|
1154
|
-
? { reportedRootHex: submit.reportedRootHex, sequenceNumber: submit.sequenceNumber }
|
|
1155
|
-
: null;
|
|
1156
|
-
if (!escalation) {
|
|
1157
|
-
/**
|
|
1158
|
-
* ⚠️ A REFUSAL WITH NOWHERE TO ESCALATE MUST STILL NAME THE REFUSAL — Invariant 3.
|
|
1159
|
-
*
|
|
1160
|
-
* `seal_pending_bilateral` says "it did not finalize within the wait window", which for a
|
|
1161
|
-
* refused seal is a downstream label pasted over an upstream cause the operator was told
|
|
1162
|
-
* one line earlier in the log.
|
|
1163
|
-
*/
|
|
1164
|
-
if (refused) {
|
|
1165
|
-
return {
|
|
1166
|
-
ok: false,
|
|
1167
|
-
reason: refused.reason,
|
|
1168
|
-
seal_status: "refused",
|
|
1169
|
-
guidance: `${refused.guidance ?? refused.detail} There was no submitted SEAL leaf to escalate from, so no solo seal was attempted either — close again once the cause above is addressed.`,
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
return {
|
|
1173
|
-
ok: false,
|
|
1174
|
-
reason: "seal_pending_bilateral",
|
|
1175
|
-
guidance: "Your SEAL leaf is recorded (auto-acknowledged) and the bilateral seal is completing, but it did not finalize within the wait window. Check cello status and the daemon logs; retry cello_close_session if the session remains unsealed.",
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
// SESSION-002 (DOD-SEAL): the counterparty did not co-close. Escalate to a UNILATERAL
|
|
1179
|
-
// seal. The body now lives in escalateToUnilateralSeal so the INTERRUPTED branch can reach
|
|
1180
|
-
// it too — it never could, and that is why an interrupted session could not get a receipt.
|
|
1181
|
-
if (refused) {
|
|
1182
|
-
logger.warn("session.seal.refused.escalating", {
|
|
1183
|
-
sessionId, agentName: record.agent_name, correlationId,
|
|
1184
|
-
reason: refused.reason,
|
|
1185
|
-
impact: "the bilateral seal was refused by the directory, so this close falls through to " +
|
|
1186
|
-
"a SOLO seal over this side's own carried chain rather than ending with no receipt.",
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
const escalated = await escalateToUnilateralSeal(rec, sid, escalation, correlationId);
|
|
1190
|
-
/**
|
|
1191
|
-
* ⚠️ THE UPSTREAM CAUSE SURVIVES THE DOWNSTREAM ANSWER — Invariant 3.
|
|
1192
|
-
*
|
|
1193
|
-
* If the solo seal ALSO fails, the operator would otherwise read only the unilateral gate's
|
|
1194
|
-
* reason (`counterparty_not_absent`, say) and never learn that a bilateral seal was refused
|
|
1195
|
-
* first — the two together are the story, and either alone points at the wrong subsystem.
|
|
1196
|
-
*/
|
|
1197
|
-
if (refused && escalated !== null && typeof escalated === "object" && escalated.ok === false) {
|
|
1198
|
-
const inner = escalated;
|
|
1199
|
-
return {
|
|
1200
|
-
...escalated,
|
|
1201
|
-
bilateral_refused_reason: refused.reason,
|
|
1202
|
-
bilateral_refused_detail: refused.detail,
|
|
1203
|
-
guidance: `${refused.guidance ?? refused.detail} The solo seal was then attempted and did not succeed either: ${typeof inner.guidance === "string" ? inner.guidance : "see the reason on this response."}`,
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
return escalated;
|
|
1207
|
-
};
|
|
1208
|
-
/**
|
|
1209
|
-
* OWNERSHIP OF THE BROKER CONNECTION MOVES WITH THE TAIL.
|
|
1210
|
-
*
|
|
1211
|
-
* This is the one genuinely dangerous part of the change. The enclosing `finally` releases
|
|
1212
|
-
* `sealBrokerConn`; if it ran while the tail was still going, the background seal would lose
|
|
1213
|
-
* the connection it is waiting on. So the tail releases it itself, and the enclosing
|
|
1214
|
-
* `finally` stands down via `handedOff`.
|
|
1215
|
-
*
|
|
1216
|
-
* PRECISELY WHAT IS LOST, corrected after review: not a corrupted seal. The escalation goes
|
|
1217
|
-
* over the HOME stream, so it still completes. What this connection carries is the
|
|
1218
|
-
* `seal_verified` / `session_sealed` push for a CROSS-NODE bilateral seal — so releasing it
|
|
1219
|
-
* early silently downgrades every cross-node close from a bilateral receipt to a unilateral
|
|
1220
|
-
* one, eleven minutes later. Quieter than corruption, and still worth the guard.
|
|
1221
|
-
*/
|
|
1222
|
-
const finishSeal = async () => {
|
|
1223
|
-
try {
|
|
1224
|
-
return await awaitSealAndEscalate();
|
|
1225
|
-
}
|
|
1226
|
-
finally {
|
|
1227
|
-
if (sealBrokerConn) {
|
|
1228
|
-
try {
|
|
1229
|
-
await sealBrokerConn.stop("seal-complete");
|
|
1230
|
-
}
|
|
1231
|
-
catch (err) {
|
|
1232
|
-
logger.warn("session.seal.broker.release_failed", { sessionId: sid, reason: extractErrorMessage(err) });
|
|
1233
|
-
}
|
|
1234
|
-
sealBrokerConn = null;
|
|
1235
|
-
}
|
|
1236
|
-
sealInterruptedInProgress.delete(sealKey(rec.agent_name, sid));
|
|
1237
|
-
}
|
|
1238
|
-
};
|
|
1239
|
-
/**
|
|
1240
|
-
* A NEW CEREMONY IS STARTING, so any previous failure verdict is now false.
|
|
1241
|
-
*
|
|
1242
|
-
* ABOVE the `waitForSeal` branch — review MEDIUM-4. It used to sit below, so the clear ran
|
|
1243
|
-
* only on the handed-off path: a blocking re-close (which the MCP tool exposes and the
|
|
1244
|
-
* document-delivery seal uses) left the old marker in place, and a stale `seal_failed` from
|
|
1245
|
-
* half an hour earlier was then reported as the current state. That is `STALEROSTER-1`'s
|
|
1246
|
-
* defect, in the store whose own docstring says it avoids it.
|
|
1247
|
-
*/
|
|
1248
|
-
sealFailures.clear(rec.agent_name, sid);
|
|
1249
|
-
if (waitForSeal)
|
|
1250
|
-
return await finishSeal();
|
|
1251
|
-
handedOff = true;
|
|
1252
|
-
/**
|
|
1253
|
-
* Detached deliberately, and never awaited. A rejection must not become an unhandled
|
|
1254
|
-
* rejection — and must not be silent either: the operator has already been told the seal is
|
|
1255
|
-
* running, so a failure they never hear about is the worst of both worlds.
|
|
1256
|
-
*/
|
|
1257
|
-
const tail = finishSeal();
|
|
1258
|
-
/**
|
|
1259
|
-
* TRACKED FOR SHUTDOWN — review MEDIUM-6.
|
|
1260
|
-
*
|
|
1261
|
-
* `stop()` cancels or awaits every other background worker: the reconcile scheduler, the
|
|
1262
|
-
* telegram poller, the manifest poll, the roster sweep. `RestartSealResolver.stop()` goes
|
|
1263
|
-
* further and AWAITS its in-flight seal, with a comment saying exactly why — *"severing
|
|
1264
|
-
* signaling under a half-finished seal-interrupted exchange leaves the counterparty holding
|
|
1265
|
-
* a commitment we never acknowledged… permanently divergent."*
|
|
1266
|
-
*
|
|
1267
|
-
* A detached, untracked ceremony was the one background task that could be cut at an
|
|
1268
|
-
* arbitrary point by `cello logout`. Recoverable on the next boot, but recoverable is not
|
|
1269
|
-
* the same as not breaking it.
|
|
1270
|
-
*/
|
|
1271
|
-
registerBackgroundSeal?.(tail);
|
|
1272
|
-
void tail.then((result) => {
|
|
1273
|
-
const r = result;
|
|
1274
|
-
if (r?.ok) {
|
|
1275
|
-
sealFailures.clear(rec.agent_name, sid);
|
|
1276
|
-
logger.info("session.seal.background.completed", { sessionId: sid, agentName: rec.agent_name, correlationId });
|
|
1277
|
-
}
|
|
1278
|
-
else {
|
|
1279
|
-
/**
|
|
1280
|
-
* THE BRANCH PRODUCTION ACTUALLY TAKES — review HIGH-1.
|
|
1281
|
-
*
|
|
1282
|
-
* `escalateToUnilateralSeal` contains zero `throw`s: all nine of its failure paths
|
|
1283
|
-
* RESOLVE with `{ ok: false, reason }`. So recording only in the `.catch` below meant
|
|
1284
|
-
* every ordinary dead ceremony went unrecorded and `cello_sealed_receipt` kept
|
|
1285
|
-
* answering `not_sealed_yet` — the very answer this unit exists to replace. The log
|
|
1286
|
-
* line three lines down said so out loud the whole time.
|
|
1287
|
-
*/
|
|
1288
|
-
sealFailures.record(rec.agent_name, sid, r?.reason ?? "seal_unresolved", new Date().toISOString(), "unresolved");
|
|
1289
|
-
logger.warn("session.seal.background.unresolved", {
|
|
1290
|
-
sessionId: sid, agentName: rec.agent_name, correlationId, reason: r?.reason,
|
|
1291
|
-
impact: "the close already answered; this session holds a durable commitment but has no receipt yet.",
|
|
1292
|
-
guidance: "cello_sealed_receipt now reports seal_failed with this reason; a daemon restart also retries it.",
|
|
1293
|
-
});
|
|
1294
|
-
}
|
|
1295
|
-
}, (err) => {
|
|
1296
|
-
// RECORDED FOR THE RESPONSE, not only the log. The caller already holds `ok: true`, so a
|
|
1297
|
-
// failure that lives only in daemon.log is one the agent has no way to discover.
|
|
1298
|
-
sealFailures.record(rec.agent_name, sid, extractErrorMessage(err), new Date().toISOString(), "threw");
|
|
1299
|
-
logger.error("session.seal.background.failed", {
|
|
1300
|
-
sessionId: sid, agentName: rec.agent_name, correlationId,
|
|
1301
|
-
error: extractErrorMessage(err),
|
|
1302
|
-
impact: "the close already answered ok; the notarization did NOT complete and no receipt exists.",
|
|
1303
|
-
guidance: "The commitment is durable — a daemon restart resolves it via the restart seal resolver.",
|
|
1304
|
-
});
|
|
1305
|
-
});
|
|
1306
|
-
// Live test 2026-09-13: closing with a message you never read gave no warning. The close
|
|
1307
|
-
// still goes ahead — the message is in the sealed record — but the operator is told.
|
|
1308
|
-
const unreadAtClose = sessionNodeManager.getUnreadReceivedCount(rec.agent_name, sid);
|
|
1309
|
-
return {
|
|
1310
|
-
...describeSealCommitted({ sessionId: sid, deadlineMs: bilateralTimeoutMs }),
|
|
1311
|
-
...(unreadAtClose > 0 ? {
|
|
1312
|
-
unread_count: unreadAtClose,
|
|
1313
|
-
unread_warning: `You closed this conversation with ${unreadAtClose} message(s) from the other side that you never read. They are part of the sealed record — read them with cello_transcript ${sid}.`,
|
|
1314
|
-
} : {}),
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
finally {
|
|
1318
|
-
// Fix #1: release the transient broker seal-connection (best-effort; the seal result stands).
|
|
1319
|
-
//
|
|
1320
|
-
// DOD-M15-CLOSEWAIT-1: skipped when the tail was handed to a background task, which owns
|
|
1321
|
-
// this cleanup itself. Releasing here would pull the transport out from under a ceremony
|
|
1322
|
-
// still in flight — the one way this change could corrupt a seal rather than merely report
|
|
1323
|
-
// it differently.
|
|
1324
|
-
if (!handedOff) {
|
|
1325
|
-
if (sealBrokerConn) {
|
|
1326
|
-
try {
|
|
1327
|
-
await sealBrokerConn.stop("seal-complete");
|
|
1328
|
-
}
|
|
1329
|
-
catch (err) {
|
|
1330
|
-
logger.warn("session.seal.broker.release_failed", { sessionId, reason: extractErrorMessage(err) });
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
sealInterruptedInProgress.delete(sealKey(record.agent_name, sessionId));
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
// DOD-M12B-PENDING-EXIT-1 — `seal_interrupted_pending` had NO exit, and the refusal below told
|
|
1338
|
-
// the operator it was "awaiting FROST notarization". It was not awaiting anything: nobody ever
|
|
1339
|
-
// requests that notarization. The relay stamps a chain only once BOTH parties have posted a
|
|
1340
|
-
// SEAL ctrl leaf, and the responder never posts one — `inbound-seal-request.ts` persists its
|
|
1341
|
-
// commitment, acks, and stops. Measured: 26 sessions idle in this status for 0.5 to 10.5 days.
|
|
1342
|
-
//
|
|
1343
|
-
// WHY THIS IS SAFE, stated accurately — the obvious answer is the wrong one. It is NOT that
|
|
1344
|
-
// "both sides signed the same root": the escalation reports the tree root WITH the SEAL ctrl
|
|
1345
|
-
// leaf appended, which is by construction not the committed root, and a responder-side row
|
|
1346
|
-
// never even receives the initiator's leaf. It is safe because **the directory rebuilds the
|
|
1347
|
-
// tree from relay-witnessed leaves and verifies their signatures** — it never consults the
|
|
1348
|
-
// commitment. The commitment is what makes it legitimate to ASK, not what makes it verifiable.
|
|
1349
|
-
// And re-entering cannot post a second ctrl leaf, because `submitSealLeaf` recovers the one a
|
|
1350
|
-
// previous run posted from the durable carry rather than posting again.
|
|
1351
|
-
if (record.status === "seal_interrupted_pending") {
|
|
1352
|
-
// The AC-011 check at the top of the handler already refuses a concurrent attempt on this
|
|
1353
|
-
// same key, for every status — so no second check is needed here, only the add/release pair
|
|
1354
|
-
// that makes THIS attempt visible to it.
|
|
1355
|
-
const key = sealKey(record.agent_name, sessionId);
|
|
1356
|
-
sealInterruptedInProgress.add(key);
|
|
1357
|
-
const correlationId = randomUUID();
|
|
1358
|
-
try {
|
|
1359
|
-
const uni = await submitAndEscalate(record, sessionId, correlationId);
|
|
1360
|
-
if ("escalated" in uni) {
|
|
1361
|
-
// NAME THE CAUSE `submitSealLeaf` GAVE, never a label invented here. Most of these are
|
|
1362
|
-
// transient and local — `standing_receiver_unavailable` is simply "the agent is not
|
|
1363
|
-
// started yet", which a freshly booted daemon reports for every session. The genuinely
|
|
1364
|
-
// permanent case (the relay released the session) has its own reason,
|
|
1365
|
-
// `seal_carry_empty`, raised inside the escalation where it is actually known.
|
|
1366
|
-
/**
|
|
1367
|
-
* ⚠️ "USUALLY LOCAL AND TEMPORARY" IS NOT TRUE OF EVERY REASON, AND THE ONES IT IS FALSE
|
|
1368
|
-
* FOR ARE THE ONES DESIGNED TO FAIL LOUD — review pass 2, MEDIUM-4.
|
|
1369
|
-
*
|
|
1370
|
-
* The sentence below was fixed text wrapped around whatever reason arrived. That is right
|
|
1371
|
-
* for `standing_receiver_unavailable` and a relay this daemon cannot reach. It is wrong
|
|
1372
|
-
* for the client-side seal-payload guards, which fire only on a deterministic defect that
|
|
1373
|
-
* will never clear — and the operator was being sent to `cello_start_agent`, `cello_status`
|
|
1374
|
-
* and an endless retry for a bug in this daemon.
|
|
1375
|
-
*
|
|
1376
|
-
* A guard built to fail loud, converted back into "try again later" by the sentence that
|
|
1377
|
-
* reports it. Enumerated, never pattern-matched: a substring rule would absorb the next
|
|
1378
|
-
* reason nobody has considered, which is the same collapse in a new coat.
|
|
1379
|
-
*/
|
|
1380
|
-
const NON_RETRYABLE_SEAL_SUBMIT_REASONS = new Set([
|
|
1381
|
-
"seal_payload_not_carried",
|
|
1382
|
-
"seal_payload_unbound",
|
|
1383
|
-
"seal_payload_invalid",
|
|
1384
|
-
"content_not_permitted_for_leaf_kind",
|
|
1385
|
-
]);
|
|
1386
|
-
return {
|
|
1387
|
-
ok: false,
|
|
1388
|
-
reason: uni.reason,
|
|
1389
|
-
guidance: NON_RETRYABLE_SEAL_SUBMIT_REASONS.has(uni.reason)
|
|
1390
|
-
? `This session holds a bilateral commitment, but no notarization could be requested for it: ${uni.reason}. ` +
|
|
1391
|
-
`This one is NOT transient and retrying will not clear it — it is a defect in this daemon's seal path, refused locally before anything was sent. ` +
|
|
1392
|
-
`The conversation is intact and nothing was disclosed. Report the reason code above; the daemon log carries the detail under session.relay.submit.*.`
|
|
1393
|
-
: `This session holds a bilateral commitment, but no notarization could be requested for it: ${uni.reason}. ` +
|
|
1394
|
-
sealSubmitCause(uni.reason),
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
if (uni.ok)
|
|
1398
|
-
return uni;
|
|
1399
|
-
return {
|
|
1400
|
-
ok: false,
|
|
1401
|
-
reason: uni.reason,
|
|
1402
|
-
...(uni.retry_after_seconds !== undefined ? { retry_after_seconds: uni.retry_after_seconds } : {}),
|
|
1403
|
-
guidance: uni.guidance,
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
finally {
|
|
1407
|
-
sealInterruptedInProgress.delete(key);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
// Any other status — nothing to do.
|
|
1411
|
-
return {
|
|
1412
|
-
ok: false,
|
|
1413
|
-
reason: "session_not_closeable",
|
|
1414
|
-
guidance: `Session is in status '${record.status}', which cannot be closed via cello_close_session. Check cello_sessions.`,
|
|
1415
|
-
};
|
|
1416
|
-
};
|
|
1417
|
-
}
|
|
1418
|
-
//# sourceMappingURL=close-session-handler.js.map
|