@raidiant/notifai 11.7.1-beta.2 → 12.0.0-beta.5
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/CHANGELOG.md +56 -4
- package/NOTICE +16 -0
- package/THIRD_PARTY_NOTICES +725 -0
- package/bin/notifai.mjs +11387 -0
- package/data/install.ps1 +318 -0
- package/data/release.json +1 -0
- package/inventory.json +1 -0
- package/npm-adapter-files.json +52 -0
- package/package.json +14 -44
- package/README.md +0 -168
- package/dist/agent-update-notice.d.ts +0 -7
- package/dist/agent-update-notice.js +0 -77
- package/dist/agent-update-notice.js.map +0 -1
- package/dist/atomic-file.d.ts +0 -30
- package/dist/atomic-file.js +0 -148
- package/dist/atomic-file.js.map +0 -1
- package/dist/attendant-update.d.ts +0 -15
- package/dist/attendant-update.js +0 -96
- package/dist/attendant-update.js.map +0 -1
- package/dist/claude-wake.d.ts +0 -108
- package/dist/claude-wake.js +0 -369
- package/dist/claude-wake.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -26
- package/dist/cli-bin.js +0 -220
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli-contract.d.ts +0 -16
- package/dist/cli-contract.js +0 -27
- package/dist/cli-contract.js.map +0 -1
- package/dist/cli-release.d.ts +0 -36
- package/dist/cli-release.js +0 -85
- package/dist/cli-release.js.map +0 -1
- package/dist/client.d.ts +0 -109
- package/dist/client.js +0 -217
- package/dist/client.js.map +0 -1
- package/dist/codex-answer-control.d.ts +0 -14
- package/dist/codex-answer-control.js +0 -41
- package/dist/codex-answer-control.js.map +0 -1
- package/dist/codex-answer-presentation.d.ts +0 -18
- package/dist/codex-answer-presentation.js +0 -113
- package/dist/codex-answer-presentation.js.map +0 -1
- package/dist/codex-input-lifecycle.d.ts +0 -10
- package/dist/codex-input-lifecycle.js +0 -63
- package/dist/codex-input-lifecycle.js.map +0 -1
- package/dist/codex-native-control.d.ts +0 -13
- package/dist/codex-native-control.js +0 -120
- package/dist/codex-native-control.js.map +0 -1
- package/dist/codex-native-turn.d.ts +0 -37
- package/dist/codex-native-turn.js +0 -155
- package/dist/codex-native-turn.js.map +0 -1
- package/dist/codex-question-bindings.d.ts +0 -76
- package/dist/codex-question-bindings.js +0 -151
- package/dist/codex-question-bindings.js.map +0 -1
- package/dist/codex-queue-control.d.ts +0 -21
- package/dist/codex-queue-control.js +0 -59
- package/dist/codex-queue-control.js.map +0 -1
- package/dist/codex-tool-messages.d.ts +0 -7
- package/dist/codex-tool-messages.js +0 -126
- package/dist/codex-tool-messages.js.map +0 -1
- package/dist/codex-wake.d.ts +0 -58
- package/dist/codex-wake.js +0 -135
- package/dist/codex-wake.js.map +0 -1
- package/dist/command-session.d.ts +0 -14
- package/dist/command-session.js +0 -31
- package/dist/command-session.js.map +0 -1
- package/dist/commands-acknowledge.d.ts +0 -13
- package/dist/commands-acknowledge.js +0 -116
- package/dist/commands-acknowledge.js.map +0 -1
- package/dist/commands-agent-sessions.d.ts +0 -6
- package/dist/commands-agent-sessions.js +0 -51
- package/dist/commands-agent-sessions.js.map +0 -1
- package/dist/commands-ask.d.ts +0 -62
- package/dist/commands-ask.js +0 -503
- package/dist/commands-ask.js.map +0 -1
- package/dist/commands-auth.d.ts +0 -76
- package/dist/commands-auth.js +0 -421
- package/dist/commands-auth.js.map +0 -1
- package/dist/commands-close.d.ts +0 -6
- package/dist/commands-close.js +0 -273
- package/dist/commands-close.js.map +0 -1
- package/dist/commands-config.d.ts +0 -31
- package/dist/commands-config.js +0 -272
- package/dist/commands-config.js.map +0 -1
- package/dist/commands-core.d.ts +0 -145
- package/dist/commands-core.js +0 -190
- package/dist/commands-core.js.map +0 -1
- package/dist/commands-devices.d.ts +0 -41
- package/dist/commands-devices.js +0 -144
- package/dist/commands-devices.js.map +0 -1
- package/dist/commands-doctor.d.ts +0 -25
- package/dist/commands-doctor.js +0 -1553
- package/dist/commands-doctor.js.map +0 -1
- package/dist/commands-guidance.d.ts +0 -27
- package/dist/commands-guidance.js +0 -150
- package/dist/commands-guidance.js.map +0 -1
- package/dist/commands-harness-context.d.ts +0 -57
- package/dist/commands-harness-context.js +0 -102
- package/dist/commands-harness-context.js.map +0 -1
- package/dist/commands-hook-attend.d.ts +0 -70
- package/dist/commands-hook-attend.js +0 -655
- package/dist/commands-hook-attend.js.map +0 -1
- package/dist/commands-hook-diagnostics.d.ts +0 -37
- package/dist/commands-hook-diagnostics.js +0 -136
- package/dist/commands-hook-diagnostics.js.map +0 -1
- package/dist/commands-hook-install.d.ts +0 -26
- package/dist/commands-hook-install.js +0 -784
- package/dist/commands-hook-install.js.map +0 -1
- package/dist/commands-hook-run.d.ts +0 -29
- package/dist/commands-hook-run.js +0 -933
- package/dist/commands-hook-run.js.map +0 -1
- package/dist/commands-hook-shape.d.ts +0 -19
- package/dist/commands-hook-shape.js +0 -48
- package/dist/commands-hook-shape.js.map +0 -1
- package/dist/commands-init.d.ts +0 -42
- package/dist/commands-init.js +0 -901
- package/dist/commands-init.js.map +0 -1
- package/dist/commands-io.d.ts +0 -2
- package/dist/commands-io.js +0 -117
- package/dist/commands-io.js.map +0 -1
- package/dist/commands-logs.d.ts +0 -30
- package/dist/commands-logs.js +0 -201
- package/dist/commands-logs.js.map +0 -1
- package/dist/commands-native-acknowledge.d.ts +0 -8
- package/dist/commands-native-acknowledge.js +0 -114
- package/dist/commands-native-acknowledge.js.map +0 -1
- package/dist/commands-project.d.ts +0 -4
- package/dist/commands-project.js +0 -41
- package/dist/commands-project.js.map +0 -1
- package/dist/commands-receive.d.ts +0 -3
- package/dist/commands-receive.js +0 -59
- package/dist/commands-receive.js.map +0 -1
- package/dist/commands-send-support.d.ts +0 -59
- package/dist/commands-send-support.js +0 -258
- package/dist/commands-send-support.js.map +0 -1
- package/dist/commands-send.d.ts +0 -34
- package/dist/commands-send.js +0 -900
- package/dist/commands-send.js.map +0 -1
- package/dist/commands-setup-proof.d.ts +0 -43
- package/dist/commands-setup-proof.js +0 -134
- package/dist/commands-setup-proof.js.map +0 -1
- package/dist/commands-skill.d.ts +0 -21
- package/dist/commands-skill.js +0 -199
- package/dist/commands-skill.js.map +0 -1
- package/dist/commands-sounds.d.ts +0 -12
- package/dist/commands-sounds.js +0 -35
- package/dist/commands-sounds.js.map +0 -1
- package/dist/commands-update-check.d.ts +0 -6
- package/dist/commands-update-check.js +0 -102
- package/dist/commands-update-check.js.map +0 -1
- package/dist/commands-update-resume.d.ts +0 -7
- package/dist/commands-update-resume.js +0 -143
- package/dist/commands-update-resume.js.map +0 -1
- package/dist/commands-update-skill.d.ts +0 -5
- package/dist/commands-update-skill.js +0 -36
- package/dist/commands-update-skill.js.map +0 -1
- package/dist/commands-update.d.ts +0 -11
- package/dist/commands-update.js +0 -286
- package/dist/commands-update.js.map +0 -1
- package/dist/commands.d.ts +0 -26
- package/dist/commands.js +0 -27
- package/dist/commands.js.map +0 -1
- package/dist/config-schema.d.ts +0 -62
- package/dist/config-schema.js +0 -302
- package/dist/config-schema.js.map +0 -1
- package/dist/config.d.ts +0 -181
- package/dist/config.js +0 -357
- package/dist/config.js.map +0 -1
- package/dist/credentials.d.ts +0 -96
- package/dist/credentials.js +0 -342
- package/dist/credentials.js.map +0 -1
- package/dist/file-lock.d.ts +0 -34
- package/dist/file-lock.js +0 -367
- package/dist/file-lock.js.map +0 -1
- package/dist/generated-session-label.d.ts +0 -2
- package/dist/generated-session-label.js +0 -152
- package/dist/generated-session-label.js.map +0 -1
- package/dist/guidance-content.d.ts +0 -53
- package/dist/guidance-content.js +0 -248
- package/dist/guidance-content.js.map +0 -1
- package/dist/guidance-render.d.ts +0 -25
- package/dist/guidance-render.js +0 -47
- package/dist/guidance-render.js.map +0 -1
- package/dist/guidance.d.ts +0 -69
- package/dist/guidance.js +0 -173
- package/dist/guidance.js.map +0 -1
- package/dist/harness-session-title.d.ts +0 -22
- package/dist/harness-session-title.js +0 -74
- package/dist/harness-session-title.js.map +0 -1
- package/dist/harnesses.d.ts +0 -65
- package/dist/harnesses.js +0 -159
- package/dist/harnesses.js.map +0 -1
- package/dist/hermes-attendant.d.ts +0 -38
- package/dist/hermes-attendant.js +0 -337
- package/dist/hermes-attendant.js.map +0 -1
- package/dist/hermes-plugin.d.ts +0 -11
- package/dist/hermes-plugin.js +0 -299
- package/dist/hermes-plugin.js.map +0 -1
- package/dist/hook-acknowledgements.d.ts +0 -70
- package/dist/hook-acknowledgements.js +0 -490
- package/dist/hook-acknowledgements.js.map +0 -1
- package/dist/hook-adapter.d.ts +0 -48
- package/dist/hook-adapter.js +0 -419
- package/dist/hook-adapter.js.map +0 -1
- package/dist/hook-events.d.ts +0 -105
- package/dist/hook-events.js +0 -130
- package/dist/hook-events.js.map +0 -1
- package/dist/hook-gates.d.ts +0 -7
- package/dist/hook-gates.js +0 -20
- package/dist/hook-gates.js.map +0 -1
- package/dist/hook-input.d.ts +0 -10
- package/dist/hook-input.js +0 -64
- package/dist/hook-input.js.map +0 -1
- package/dist/hook-lifecycle.d.ts +0 -112
- package/dist/hook-lifecycle.js +0 -2031
- package/dist/hook-lifecycle.js.map +0 -1
- package/dist/hook-project-sessions.d.ts +0 -28
- package/dist/hook-project-sessions.js +0 -141
- package/dist/hook-project-sessions.js.map +0 -1
- package/dist/hook-question-lock.d.ts +0 -29
- package/dist/hook-question-lock.js +0 -217
- package/dist/hook-question-lock.js.map +0 -1
- package/dist/hook-question-retirement.d.ts +0 -74
- package/dist/hook-question-retirement.js +0 -314
- package/dist/hook-question-retirement.js.map +0 -1
- package/dist/hook-question-state.d.ts +0 -58
- package/dist/hook-question-state.js +0 -239
- package/dist/hook-question-state.js.map +0 -1
- package/dist/hook-session-state.d.ts +0 -127
- package/dist/hook-session-state.js +0 -488
- package/dist/hook-session-state.js.map +0 -1
- package/dist/hook-types.d.ts +0 -476
- package/dist/hook-types.js +0 -4
- package/dist/hook-types.js.map +0 -1
- package/dist/injection-render.d.ts +0 -36
- package/dist/injection-render.js +0 -61
- package/dist/injection-render.js.map +0 -1
- package/dist/install-hooks.d.ts +0 -470
- package/dist/install-hooks.js +0 -1622
- package/dist/install-hooks.js.map +0 -1
- package/dist/integration-health.d.ts +0 -26
- package/dist/integration-health.js +0 -133
- package/dist/integration-health.js.map +0 -1
- package/dist/interactive.d.ts +0 -22
- package/dist/interactive.js +0 -644
- package/dist/interactive.js.map +0 -1
- package/dist/invocation-context.d.ts +0 -47
- package/dist/invocation-context.js +0 -164
- package/dist/invocation-context.js.map +0 -1
- package/dist/local-path.d.ts +0 -7
- package/dist/local-path.js +0 -27
- package/dist/local-path.js.map +0 -1
- package/dist/logging.d.ts +0 -157
- package/dist/logging.js +0 -599
- package/dist/logging.js.map +0 -1
- package/dist/main-run.d.ts +0 -1
- package/dist/main-run.js +0 -42
- package/dist/main-run.js.map +0 -1
- package/dist/main.d.ts +0 -2
- package/dist/main.js +0 -12
- package/dist/main.js.map +0 -1
- package/dist/native-answer-operation.d.ts +0 -70
- package/dist/native-answer-operation.js +0 -247
- package/dist/native-answer-operation.js.map +0 -1
- package/dist/native-skills.d.ts +0 -64
- package/dist/native-skills.js +0 -167
- package/dist/native-skills.js.map +0 -1
- package/dist/node-floor.d.ts +0 -19
- package/dist/node-floor.js +0 -32
- package/dist/node-floor.js.map +0 -1
- package/dist/npm-invocation.d.ts +0 -34
- package/dist/npm-invocation.js +0 -53
- package/dist/npm-invocation.js.map +0 -1
- package/dist/openclaw-continuation-bridge.d.ts +0 -7
- package/dist/openclaw-continuation-bridge.js +0 -64
- package/dist/openclaw-continuation-bridge.js.map +0 -1
- package/dist/openclaw-gateway-readiness.d.ts +0 -1
- package/dist/openclaw-gateway-readiness.js +0 -24
- package/dist/openclaw-gateway-readiness.js.map +0 -1
- package/dist/openclaw-generation.d.ts +0 -21
- package/dist/openclaw-generation.js +0 -115
- package/dist/openclaw-generation.js.map +0 -1
- package/dist/openclaw-message-bridge.d.ts +0 -8
- package/dist/openclaw-message-bridge.js +0 -93
- package/dist/openclaw-message-bridge.js.map +0 -1
- package/dist/openclaw-pending.d.ts +0 -8
- package/dist/openclaw-pending.js +0 -47
- package/dist/openclaw-pending.js.map +0 -1
- package/dist/openclaw-plugin.d.ts +0 -75
- package/dist/openclaw-plugin.js +0 -1353
- package/dist/openclaw-plugin.js.map +0 -1
- package/dist/openclaw-session-access.d.ts +0 -12
- package/dist/openclaw-session-access.js +0 -79
- package/dist/openclaw-session-access.js.map +0 -1
- package/dist/opencode-plugin.d.ts +0 -88
- package/dist/opencode-plugin.js +0 -286
- package/dist/opencode-plugin.js.map +0 -1
- package/dist/orca-session-title.d.ts +0 -11
- package/dist/orca-session-title.js +0 -111
- package/dist/orca-session-title.js.map +0 -1
- package/dist/pack-feedback-slice.d.ts +0 -35
- package/dist/pack-feedback-slice.js +0 -126
- package/dist/pack-feedback-slice.js.map +0 -1
- package/dist/pairing-qr.d.ts +0 -5
- package/dist/pairing-qr.js +0 -24
- package/dist/pairing-qr.js.map +0 -1
- package/dist/pending-pairing.d.ts +0 -38
- package/dist/pending-pairing.js +0 -70
- package/dist/pending-pairing.js.map +0 -1
- package/dist/platform.d.ts +0 -67
- package/dist/platform.js +0 -140
- package/dist/platform.js.map +0 -1
- package/dist/process-identity.d.ts +0 -26
- package/dist/process-identity.js +0 -82
- package/dist/process-identity.js.map +0 -1
- package/dist/program.d.ts +0 -59
- package/dist/program.js +0 -691
- package/dist/program.js.map +0 -1
- package/dist/project-enablement.d.ts +0 -21
- package/dist/project-enablement.js +0 -71
- package/dist/project-enablement.js.map +0 -1
- package/dist/question-settlement-process.d.ts +0 -17
- package/dist/question-settlement-process.js +0 -35
- package/dist/question-settlement-process.js.map +0 -1
- package/dist/question-timing.d.ts +0 -46
- package/dist/question-timing.js +0 -48
- package/dist/question-timing.js.map +0 -1
- package/dist/readiness.d.ts +0 -153
- package/dist/readiness.js +0 -149
- package/dist/readiness.js.map +0 -1
- package/dist/release.d.ts +0 -31
- package/dist/release.js +0 -52
- package/dist/release.js.map +0 -1
- package/dist/send-attempts.d.ts +0 -26
- package/dist/send-attempts.js +0 -139
- package/dist/send-attempts.js.map +0 -1
- package/dist/send.d.ts +0 -115
- package/dist/send.js +0 -421
- package/dist/send.js.map +0 -1
- package/dist/session-activation.d.ts +0 -13
- package/dist/session-activation.js +0 -64
- package/dist/session-activation.js.map +0 -1
- package/dist/session-attendant-probe.d.ts +0 -85
- package/dist/session-attendant-probe.js +0 -173
- package/dist/session-attendant-probe.js.map +0 -1
- package/dist/session-attendant-state.d.ts +0 -70
- package/dist/session-attendant-state.js +0 -256
- package/dist/session-attendant-state.js.map +0 -1
- package/dist/session-attendant.d.ts +0 -143
- package/dist/session-attendant.js +0 -520
- package/dist/session-attendant.js.map +0 -1
- package/dist/session-delivery.d.ts +0 -249
- package/dist/session-delivery.js +0 -576
- package/dist/session-delivery.js.map +0 -1
- package/dist/session-handoff.d.ts +0 -98
- package/dist/session-handoff.js +0 -118
- package/dist/session-handoff.js.map +0 -1
- package/dist/session-input-wakes.d.ts +0 -78
- package/dist/session-input-wakes.js +0 -239
- package/dist/session-input-wakes.js.map +0 -1
- package/dist/session-inputs.d.ts +0 -57
- package/dist/session-inputs.js +0 -434
- package/dist/session-inputs.js.map +0 -1
- package/dist/session-labels.d.ts +0 -56
- package/dist/session-labels.js +0 -396
- package/dist/session-labels.js.map +0 -1
- package/dist/session-message-handoff.d.ts +0 -39
- package/dist/session-message-handoff.js +0 -107
- package/dist/session-message-handoff.js.map +0 -1
- package/dist/setup-destinations.d.ts +0 -23
- package/dist/setup-destinations.js +0 -44
- package/dist/setup-destinations.js.map +0 -1
- package/dist/skill-integrity.d.ts +0 -54
- package/dist/skill-integrity.js +0 -164
- package/dist/skill-integrity.js.map +0 -1
- package/dist/skill-source/manifest.json +0 -40
- package/dist/skill-source/notifai/SKILL.md +0 -350
- package/dist/skill-source/notifai/references/diagnostics.md +0 -36
- package/dist/skill-source/notifai/references/harness-setup.md +0 -384
- package/dist/skill-source/notifai/references/native-questions.md +0 -51
- package/dist/skill-source/notifai/references/notes-and-edits.md +0 -54
- package/dist/skill-source/notifai/references/send-details.md +0 -14
- package/dist/skill-source/notifai/references/updates.md +0 -116
- package/dist/skill-source/notifai/references/writing-examples.md +0 -56
- package/dist/sound-ref.d.ts +0 -23
- package/dist/sound-ref.js +0 -28
- package/dist/sound-ref.js.map +0 -1
- package/dist/ui/banner.d.ts +0 -22
- package/dist/ui/banner.js +0 -87
- package/dist/ui/banner.js.map +0 -1
- package/dist/ui/config-view.d.ts +0 -30
- package/dist/ui/config-view.js +0 -108
- package/dist/ui/config-view.js.map +0 -1
- package/dist/ui/help.d.ts +0 -44
- package/dist/ui/help.js +0 -107
- package/dist/ui/help.js.map +0 -1
- package/dist/ui/theme.d.ts +0 -90
- package/dist/ui/theme.js +0 -244
- package/dist/ui/theme.js.map +0 -1
- package/dist/update-handoff.d.ts +0 -29
- package/dist/update-handoff.js +0 -69
- package/dist/update-handoff.js.map +0 -1
- package/dist/url-policy.d.ts +0 -85
- package/dist/url-policy.js +0 -369
- package/dist/url-policy.js.map +0 -1
- package/dist/version.d.ts +0 -40
- package/dist/version.js +0 -132
- package/dist/version.js.map +0 -1
- package/dist/wake-support.d.ts +0 -40
- package/dist/wake-support.js +0 -95
- package/dist/wake-support.js.map +0 -1
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Session Attendant: one long-lived CLI process per Agent Session.
|
|
3
|
-
*
|
|
4
|
-
* It starts as that session's own asynchronous hook child, proves the harness
|
|
5
|
-
* process alive every two seconds by local evidence only, and holds the
|
|
6
|
-
* session's Session Presence lease with the service. It is resident from its
|
|
7
|
-
* start but makes no network call until the session had a Notification
|
|
8
|
-
* Request accepted, so sessions that never notified are never reported.
|
|
9
|
-
*
|
|
10
|
-
* Nothing ends it from outside. Claude Code neither signals nor waits for a
|
|
11
|
-
* resident async hook on `/exit` or when the harness is killed, so the local
|
|
12
|
-
* probe is the one universal end signal; SessionEnd's marker is a fast path,
|
|
13
|
-
* and a SIGTERM (terminal close) is treated as "ending, report once".
|
|
14
|
-
*
|
|
15
|
-
* Local only: PIDs, process start times, descriptor paths, and harness homes
|
|
16
|
-
* never leave this machine. The service sees an opaque incarnation id.
|
|
17
|
-
*/
|
|
18
|
-
import type { AttendanceMessage, SessionActivity } from '@raidiant/notifai-protocol';
|
|
19
|
-
import { type ApiClient } from './client.js';
|
|
20
|
-
import type { Logger } from './logging.js';
|
|
21
|
-
/** Local probe cadence. */
|
|
22
|
-
export declare const ATTENDANT_PROBE_INTERVAL_MS = 2000;
|
|
23
|
-
/** Harness writes stop this long before the lease ends by the attendant's own clock. */
|
|
24
|
-
export declare const ATTENDANT_WRITE_MARGIN_MS = 15000;
|
|
25
|
-
/** A wall-clock move this far from the monotonic clock means sleep or a clock jump. */
|
|
26
|
-
export declare const ATTENDANT_CLOCK_JUMP_MS = 5000;
|
|
27
|
-
/** Best-effort bound on the final `ended` or `withdrawn` report. */
|
|
28
|
-
export declare const ATTENDANT_FINAL_REPORT_MS = 3000;
|
|
29
|
-
/** How often an unsupported service is asked again. */
|
|
30
|
-
export declare const ATTENDANT_UNSUPPORTED_RECHECK_MS: number;
|
|
31
|
-
/** How long to wait before asking again about a session the service does not know yet. */
|
|
32
|
-
export declare const ATTENDANT_UNKNOWN_SESSION_RETRY_MS = 60000;
|
|
33
|
-
/** First pause before re-asking for a message that waits on something brief; doubles to the hold. */
|
|
34
|
-
export declare const ATTENDANT_MESSAGE_RETRY_MS = 2000;
|
|
35
|
-
export type HarnessProbe = {
|
|
36
|
-
state: 'running';
|
|
37
|
-
activity: SessionActivity;
|
|
38
|
-
}
|
|
39
|
-
/** Evidence is missing or ambiguous: no renewal, no deliveries, never `running`. */
|
|
40
|
-
| {
|
|
41
|
-
state: 'uncertain';
|
|
42
|
-
reason: string;
|
|
43
|
-
} | {
|
|
44
|
-
state: 'ended';
|
|
45
|
-
reason: 'harness-gone' | 'session-replaced' | 'session-end-hook';
|
|
46
|
-
};
|
|
47
|
-
export type GateResult = {
|
|
48
|
-
ok: true;
|
|
49
|
-
} | {
|
|
50
|
-
ok: false;
|
|
51
|
-
reason: string;
|
|
52
|
-
};
|
|
53
|
-
export type AttendantPhase =
|
|
54
|
-
/** Resident, no network: the session has not notified yet. */
|
|
55
|
-
'dormant'
|
|
56
|
-
/** The service does not offer Session Attendance; resident without reporting. */
|
|
57
|
-
| 'unsupported' | 'acquiring' | 'attending'
|
|
58
|
-
/** Another incarnation holds the lease. */
|
|
59
|
-
| 'waiting-for-lease' | 'uncertain' | 'exited';
|
|
60
|
-
export type AttendantExitReason = 'harness-gone' | 'session-replaced' | 'session-end-hook' | 'signal' | 'superseded' | 'claim-lost' | 'withdrawn-by-service' | 'unauthorized' | `gate:${string}`;
|
|
61
|
-
export interface AttendantStatus {
|
|
62
|
-
phase: AttendantPhase;
|
|
63
|
-
incarnation: string;
|
|
64
|
-
generation: number | null;
|
|
65
|
-
activity: SessionActivity | null;
|
|
66
|
-
reason: string | null;
|
|
67
|
-
/** Whether this attendant hands Session Messages in, so the service accepts notes. */
|
|
68
|
-
accepts_messages: boolean;
|
|
69
|
-
/** Fresh in-process proof that a harness writer still targets this session. */
|
|
70
|
-
writer_ready?: boolean;
|
|
71
|
-
updated_at: number;
|
|
72
|
-
}
|
|
73
|
-
export interface AttendantClock {
|
|
74
|
-
/** Monotonic milliseconds; never moves with the wall clock. */
|
|
75
|
-
monotonic(): number;
|
|
76
|
-
wall(): number;
|
|
77
|
-
sleep(milliseconds: number, signal: AbortSignal): Promise<void>;
|
|
78
|
-
}
|
|
79
|
-
export interface SessionAttendantOptions {
|
|
80
|
-
sessionId: string;
|
|
81
|
-
/** The opaque incarnation this attendant serves; see `incarnationNow`. */
|
|
82
|
-
incarnation: string;
|
|
83
|
-
/** The incarnation currently recorded for the session, read fresh each probe. */
|
|
84
|
-
incarnationNow(): string | null;
|
|
85
|
-
/** Replace a refused incarnation id; null when the record moved on. */
|
|
86
|
-
rotateIncarnation(expected: string): string | null;
|
|
87
|
-
probe(): HarnessProbe;
|
|
88
|
-
/** Whether this attendant still holds its exclusive local claim. */
|
|
89
|
-
claimHeld(): boolean;
|
|
90
|
-
/** Whether this session had a Notification Request accepted on this machine. */
|
|
91
|
-
notified(): boolean;
|
|
92
|
-
/** Project Enablement and the installed CLI/hook contract. */
|
|
93
|
-
gates(): GateResult;
|
|
94
|
-
/** Authenticated client, or null when this machine is not paired. */
|
|
95
|
-
client(): ApiClient | null;
|
|
96
|
-
/** Whether the service advertises Session Attendance. */
|
|
97
|
-
serverSupportsAttendance(client: ApiClient): Promise<boolean>;
|
|
98
|
-
/** Whether this attendant can hand Session Messages into the session. */
|
|
99
|
-
acceptsMessages: boolean;
|
|
100
|
-
/**
|
|
101
|
-
* Hands claimable Session Messages into the session. `retry-soon` asks for
|
|
102
|
-
* the next exchange without holding it open: a message is waiting on
|
|
103
|
-
* something that resolves in seconds.
|
|
104
|
-
*/
|
|
105
|
-
onMessages?: (messages: AttendanceMessage[], attendant: AttendantHandle) => Promise<'done' | 'retry-soon'>;
|
|
106
|
-
clock: AttendantClock;
|
|
107
|
-
logger: Logger;
|
|
108
|
-
/** Staged local input needs a fresh exchange rather than a quiet long-poll. */
|
|
109
|
-
localInputPending?: () => boolean;
|
|
110
|
-
/** Persist the local status `notifai doctor` reads. */
|
|
111
|
-
writeStatus(status: AttendantStatus): void;
|
|
112
|
-
/** Periodic housekeeping (e.g. keep markers younger than pruning). */
|
|
113
|
-
heartbeat?: () => void;
|
|
114
|
-
/** Harness-owned work that needs the fresh probe while attendance continues. */
|
|
115
|
-
onProbe?: (probe: HarnessProbe) => void;
|
|
116
|
-
/** Publish fresh writer evidence for ask admission. */
|
|
117
|
-
publishProbeStatus?: boolean;
|
|
118
|
-
/** Resolves when a termination signal arrived. */
|
|
119
|
-
signalled?: Promise<void>;
|
|
120
|
-
probeIntervalMs?: number;
|
|
121
|
-
waitSeconds?: number;
|
|
122
|
-
}
|
|
123
|
-
/** What a harness writer asks before each write. */
|
|
124
|
-
export interface AttendantHandle {
|
|
125
|
-
/**
|
|
126
|
-
* True only while, right now: this attendant still owns its claim and
|
|
127
|
-
* incarnation, fresh local evidence shows the session running, every gate
|
|
128
|
-
* passes, no clock discontinuity happened since the lease was granted, and
|
|
129
|
-
* the lease cannot lapse before the write ends by the monotonic clock.
|
|
130
|
-
*/
|
|
131
|
-
mayWrite(): boolean;
|
|
132
|
-
generation(): number | null;
|
|
133
|
-
incarnation(): string;
|
|
134
|
-
}
|
|
135
|
-
export interface AttendantResult {
|
|
136
|
-
reason: AttendantExitReason;
|
|
137
|
-
/** Whether a final report was attempted. */
|
|
138
|
-
reported: 'ended' | 'withdrawn' | null;
|
|
139
|
-
}
|
|
140
|
-
/** Runs one Session Attendant until its session ends or a gate fails. */
|
|
141
|
-
export declare function runSessionAttendant(options: SessionAttendantOptions): Promise<AttendantResult>;
|
|
142
|
-
/** Production clocks: `performance.now()` pauses with the machine; `Date.now()` does not. */
|
|
143
|
-
export declare const systemAttendantClock: AttendantClock;
|
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
import { ATTENDANCE_MAX_WAIT_SECONDS } from '@raidiant/notifai-protocol';
|
|
2
|
-
import { ApiCallError } from './client.js';
|
|
3
|
-
/** Local probe cadence. */
|
|
4
|
-
export const ATTENDANT_PROBE_INTERVAL_MS = 2_000;
|
|
5
|
-
/** Harness writes stop this long before the lease ends by the attendant's own clock. */
|
|
6
|
-
export const ATTENDANT_WRITE_MARGIN_MS = 15_000;
|
|
7
|
-
/** A wall-clock move this far from the monotonic clock means sleep or a clock jump. */
|
|
8
|
-
export const ATTENDANT_CLOCK_JUMP_MS = 5_000;
|
|
9
|
-
/** Best-effort bound on the final `ended` or `withdrawn` report. */
|
|
10
|
-
export const ATTENDANT_FINAL_REPORT_MS = 3_000;
|
|
11
|
-
/** How often an unsupported service is asked again. */
|
|
12
|
-
export const ATTENDANT_UNSUPPORTED_RECHECK_MS = 30 * 60_000;
|
|
13
|
-
/** How long to wait before asking again about a session the service does not know yet. */
|
|
14
|
-
export const ATTENDANT_UNKNOWN_SESSION_RETRY_MS = 60_000;
|
|
15
|
-
/** First pause before re-asking for a message that waits on something brief; doubles to the hold. */
|
|
16
|
-
export const ATTENDANT_MESSAGE_RETRY_MS = 2_000;
|
|
17
|
-
const MAX_NETWORK_BACKOFF_MS = 30_000;
|
|
18
|
-
/** Runs one Session Attendant until its session ends or a gate fails. */
|
|
19
|
-
export async function runSessionAttendant(options) {
|
|
20
|
-
const { clock, logger } = options;
|
|
21
|
-
const probeInterval = options.probeIntervalMs ?? ATTENDANT_PROBE_INTERVAL_MS;
|
|
22
|
-
const waitSeconds = Math.min(options.waitSeconds ?? ATTENDANCE_MAX_WAIT_SECONDS, ATTENDANCE_MAX_WAIT_SECONDS);
|
|
23
|
-
const stopping = new AbortController();
|
|
24
|
-
let incarnation = options.incarnation;
|
|
25
|
-
let generation = null;
|
|
26
|
-
let cursor;
|
|
27
|
-
/** Monotonic time after which the lease may have lapsed at the service. */
|
|
28
|
-
let leaseEndsAt = null;
|
|
29
|
-
let probe = options.probe();
|
|
30
|
-
let activity = probe.state === 'running' ? probe.activity : null;
|
|
31
|
-
let phase = 'dormant';
|
|
32
|
-
let phaseReason = null;
|
|
33
|
-
let networkStarted = false;
|
|
34
|
-
let exit = probe.state === 'ended' ? probe.reason : null;
|
|
35
|
-
let exchange = null;
|
|
36
|
-
let exchangeHeld = false;
|
|
37
|
-
let wake = null;
|
|
38
|
-
const setPhase = (next, reason = null) => {
|
|
39
|
-
if (next === phase && reason === phaseReason)
|
|
40
|
-
return;
|
|
41
|
-
phase = next;
|
|
42
|
-
phaseReason = reason;
|
|
43
|
-
logger.info('attendant.state', {
|
|
44
|
-
phase,
|
|
45
|
-
...(reason === null ? {} : { reason }),
|
|
46
|
-
...(generation === null ? {} : { generation }),
|
|
47
|
-
});
|
|
48
|
-
writeStatus();
|
|
49
|
-
};
|
|
50
|
-
const writeStatus = () => {
|
|
51
|
-
try {
|
|
52
|
-
options.writeStatus({
|
|
53
|
-
phase,
|
|
54
|
-
incarnation,
|
|
55
|
-
generation,
|
|
56
|
-
activity,
|
|
57
|
-
reason: phaseReason,
|
|
58
|
-
accepts_messages: options.acceptsMessages,
|
|
59
|
-
...(options.publishProbeStatus ? { writer_ready: probe.state === 'running' } : {}),
|
|
60
|
-
updated_at: clock.wall(),
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// Status is diagnostic; the attendant keeps attending without it.
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
const nudge = () => {
|
|
68
|
-
const resolve = wake;
|
|
69
|
-
wake = null;
|
|
70
|
-
resolve?.();
|
|
71
|
-
};
|
|
72
|
-
const abortExchange = () => {
|
|
73
|
-
exchange?.abort();
|
|
74
|
-
};
|
|
75
|
-
const stop = (reason) => {
|
|
76
|
-
if (exit === null)
|
|
77
|
-
exit = reason;
|
|
78
|
-
exchange?.abort();
|
|
79
|
-
stopping.abort();
|
|
80
|
-
nudge();
|
|
81
|
-
};
|
|
82
|
-
/** Bumped by every clock discontinuity; a lease granted in an older epoch is void. */
|
|
83
|
-
let epoch = 0;
|
|
84
|
-
let reacquireNow = false;
|
|
85
|
-
const loseLease = (reason) => {
|
|
86
|
-
if (leaseEndsAt !== null || generation !== null) {
|
|
87
|
-
logger.info('attendant.lease', { event: 'lost', reason, ...(generation === null ? {} : { generation }) });
|
|
88
|
-
}
|
|
89
|
-
leaseEndsAt = null;
|
|
90
|
-
};
|
|
91
|
-
let lastWall = clock.wall();
|
|
92
|
-
let lastMono = clock.monotonic();
|
|
93
|
-
/**
|
|
94
|
-
* Sleep pauses the monotonic clock while the service's lease keeps running;
|
|
95
|
-
* a wall-clock step says the same. Either way nothing may be written until
|
|
96
|
-
* a fresh exchange, sent after the jump, re-acquires.
|
|
97
|
-
*/
|
|
98
|
-
const checkClock = () => {
|
|
99
|
-
const wall = clock.wall();
|
|
100
|
-
const mono = clock.monotonic();
|
|
101
|
-
const jump = Math.abs(wall - lastWall - (mono - lastMono));
|
|
102
|
-
lastWall = wall;
|
|
103
|
-
lastMono = mono;
|
|
104
|
-
if (jump > ATTENDANT_CLOCK_JUMP_MS) {
|
|
105
|
-
epoch += 1;
|
|
106
|
-
reacquireNow = true;
|
|
107
|
-
loseLease('clock-jump');
|
|
108
|
-
exchange?.abort();
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
/**
|
|
112
|
-
* Re-establish everything this attendant relies on, now: clock continuity,
|
|
113
|
-
* its claim, its incarnation, and fresh session evidence. Returns whether
|
|
114
|
-
* the session is running and this attendant may still act for it.
|
|
115
|
-
*/
|
|
116
|
-
const refresh = () => {
|
|
117
|
-
if (exit !== null)
|
|
118
|
-
return false;
|
|
119
|
-
checkClock();
|
|
120
|
-
if (!options.claimHeld()) {
|
|
121
|
-
stop('claim-lost');
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
const recorded = options.incarnationNow();
|
|
125
|
-
if (recorded !== null && recorded !== incarnation) {
|
|
126
|
-
stop('superseded');
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
const previous = probe;
|
|
130
|
-
probe = options.probe();
|
|
131
|
-
if (probe.state === 'ended') {
|
|
132
|
-
stop(probe.reason);
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
if (probe.state === 'uncertain') {
|
|
136
|
-
if (previous.state !== 'uncertain') {
|
|
137
|
-
exchange?.abort();
|
|
138
|
-
if (networkStarted)
|
|
139
|
-
setPhase('uncertain', probe.reason);
|
|
140
|
-
}
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
const changed = activity !== probe.activity;
|
|
144
|
-
activity = probe.activity;
|
|
145
|
-
if (changed) {
|
|
146
|
-
logger.info('attendant.state', { phase, activity, reason: 'activity-changed', generation });
|
|
147
|
-
writeStatus();
|
|
148
|
-
}
|
|
149
|
-
if (previous.state !== 'running' || changed) {
|
|
150
|
-
// A held exchange carries the old activity; cut it so the next one reports this.
|
|
151
|
-
exchange?.abort();
|
|
152
|
-
nudge();
|
|
153
|
-
}
|
|
154
|
-
return true;
|
|
155
|
-
};
|
|
156
|
-
const handle = {
|
|
157
|
-
mayWrite: () => {
|
|
158
|
-
if (!refresh())
|
|
159
|
-
return false;
|
|
160
|
-
const gate = options.gates();
|
|
161
|
-
if (!gate.ok) {
|
|
162
|
-
stop(`gate:${gate.reason}`);
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
return (generation !== null &&
|
|
166
|
-
leaseEndsAt !== null &&
|
|
167
|
-
clock.monotonic() < leaseEndsAt - ATTENDANT_WRITE_MARGIN_MS);
|
|
168
|
-
},
|
|
169
|
-
generation: () => generation,
|
|
170
|
-
incarnation: () => incarnation,
|
|
171
|
-
};
|
|
172
|
-
// -- local probe -------------------------------------------------------
|
|
173
|
-
let lastHeartbeat = lastMono;
|
|
174
|
-
const tick = () => {
|
|
175
|
-
refresh();
|
|
176
|
-
if (exit !== null)
|
|
177
|
-
return;
|
|
178
|
-
if (options.publishProbeStatus)
|
|
179
|
-
writeStatus();
|
|
180
|
-
try {
|
|
181
|
-
options.onProbe?.(probe);
|
|
182
|
-
}
|
|
183
|
-
catch (err) {
|
|
184
|
-
logger.error('attendant.state', {
|
|
185
|
-
phase, reason: 'probe-work-failed',
|
|
186
|
-
message: err instanceof Error ? err.message : String(err),
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
if (!networkStarted && options.notified())
|
|
190
|
-
nudge();
|
|
191
|
-
if (probe.state === 'running' && exchangeHeld && exchange !== null && options.localInputPending?.()) {
|
|
192
|
-
// A reply can arrive locally while attendance is holding for server notes.
|
|
193
|
-
// Refresh the lease and invoke the genuine resident writer on the next
|
|
194
|
-
// exchange, without waiting the server's entire quiet-poll interval.
|
|
195
|
-
reacquireNow = true;
|
|
196
|
-
exchange.abort();
|
|
197
|
-
nudge();
|
|
198
|
-
}
|
|
199
|
-
const mono = clock.monotonic();
|
|
200
|
-
if (mono - lastHeartbeat >= 60 * 60_000) {
|
|
201
|
-
lastHeartbeat = mono;
|
|
202
|
-
try {
|
|
203
|
-
options.heartbeat?.();
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
// Housekeeping only.
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
const probeLoop = (async () => {
|
|
211
|
-
while (!stopping.signal.aborted) {
|
|
212
|
-
await clock.sleep(probeInterval, stopping.signal).catch(() => undefined);
|
|
213
|
-
if (stopping.signal.aborted)
|
|
214
|
-
break;
|
|
215
|
-
tick();
|
|
216
|
-
}
|
|
217
|
-
})();
|
|
218
|
-
void options.signalled?.then(() => stop('signal'));
|
|
219
|
-
/** Wait until a probe tick changes something this loop waits on, or a stop. */
|
|
220
|
-
const nextChange = async () => {
|
|
221
|
-
if (stopping.signal.aborted)
|
|
222
|
-
return;
|
|
223
|
-
await new Promise((resolve) => {
|
|
224
|
-
wake = resolve;
|
|
225
|
-
});
|
|
226
|
-
};
|
|
227
|
-
const pause = async (ms) => {
|
|
228
|
-
await clock.sleep(ms, stopping.signal).catch(() => undefined);
|
|
229
|
-
};
|
|
230
|
-
writeStatus();
|
|
231
|
-
logger.info('attendant.start', { notified: options.notified(), probe: probe.state });
|
|
232
|
-
// -- exchange loop -----------------------------------------------------
|
|
233
|
-
let backoff = 1_000;
|
|
234
|
-
let rotated = false;
|
|
235
|
-
let serverChecked = false;
|
|
236
|
-
/** Consecutive `retry-soon` answers from message hand-off. */
|
|
237
|
-
let messageRetries = 0;
|
|
238
|
-
while (exit === null) {
|
|
239
|
-
if (!networkStarted) {
|
|
240
|
-
if (!options.notified()) {
|
|
241
|
-
await nextChange();
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
networkStarted = true;
|
|
245
|
-
}
|
|
246
|
-
// Fresh evidence before every exchange, not the last tick's.
|
|
247
|
-
if (!refresh()) {
|
|
248
|
-
if (exit !== null)
|
|
249
|
-
break;
|
|
250
|
-
if (probe.state === 'uncertain')
|
|
251
|
-
setPhase('uncertain', probe.reason);
|
|
252
|
-
await nextChange();
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
const gate = options.gates();
|
|
256
|
-
if (!gate.ok) {
|
|
257
|
-
stop(`gate:${gate.reason}`);
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
const client = options.client();
|
|
261
|
-
if (client === null) {
|
|
262
|
-
stop('gate:not-paired');
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
if (!serverChecked) {
|
|
266
|
-
// Discovery is an exchange like any other: a stop, an uncertain probe,
|
|
267
|
-
// or an activity change cuts it, and whatever it answers, every check
|
|
268
|
-
// above runs again before anything is sent.
|
|
269
|
-
const discovery = new AbortController();
|
|
270
|
-
exchange = discovery;
|
|
271
|
-
exchangeHeld = false;
|
|
272
|
-
let supported;
|
|
273
|
-
try {
|
|
274
|
-
supported = await abortable(options.serverSupportsAttendance(client), discovery.signal);
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
exchange = null;
|
|
278
|
-
if (exit !== null || discovery.signal.aborted)
|
|
279
|
-
continue;
|
|
280
|
-
await pause(backoff);
|
|
281
|
-
backoff = Math.min(backoff * 2, MAX_NETWORK_BACKOFF_MS);
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
exchange = null;
|
|
285
|
-
if (!supported) {
|
|
286
|
-
setPhase('unsupported', 'service-has-no-session-attendance');
|
|
287
|
-
await pause(ATTENDANT_UNSUPPORTED_RECHECK_MS);
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
serverChecked = true;
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
if (generation === null)
|
|
294
|
-
setPhase('acquiring');
|
|
295
|
-
const body = {
|
|
296
|
-
incarnation,
|
|
297
|
-
...(generation === null ? {} : { generation }),
|
|
298
|
-
state: 'running',
|
|
299
|
-
// Set by the successful refresh above.
|
|
300
|
-
activity: activity ?? 'idle',
|
|
301
|
-
accepts_messages: options.acceptsMessages,
|
|
302
|
-
...(cursor === undefined ? {} : { message_cursor: cursor }),
|
|
303
|
-
};
|
|
304
|
-
const current = new AbortController();
|
|
305
|
-
exchange = current;
|
|
306
|
-
const sentAt = clock.monotonic();
|
|
307
|
-
const sentEpoch = epoch;
|
|
308
|
-
const hold = cursor !== undefined && !reacquireNow && messageRetries === 0;
|
|
309
|
-
exchangeHeld = hold;
|
|
310
|
-
reacquireNow = false;
|
|
311
|
-
let response;
|
|
312
|
-
try {
|
|
313
|
-
response = await client.attend(options.sessionId, body, {
|
|
314
|
-
waitSeconds: hold ? waitSeconds : 0,
|
|
315
|
-
signal: current.signal,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
catch (err) {
|
|
319
|
-
exchange = null;
|
|
320
|
-
if (exit !== null)
|
|
321
|
-
break;
|
|
322
|
-
if (current.signal.aborted)
|
|
323
|
-
continue;
|
|
324
|
-
if (err instanceof ApiCallError) {
|
|
325
|
-
if (err.status === 401 || err.status === 403) {
|
|
326
|
-
stop('unauthorized');
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
if (err.status === 404) {
|
|
330
|
-
setPhase('acquiring', 'session-unknown-to-service');
|
|
331
|
-
await pause(ATTENDANT_UNKNOWN_SESSION_RETRY_MS);
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
if (err.status === 409 && err.code === 'feature_unavailable') {
|
|
335
|
-
serverChecked = false;
|
|
336
|
-
setPhase('unsupported', 'service-refused-session-attendance');
|
|
337
|
-
await pause(ATTENDANT_UNSUPPORTED_RECHECK_MS);
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
await pause(backoff);
|
|
342
|
-
backoff = Math.min(backoff * 2, MAX_NETWORK_BACKOFF_MS);
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
exchange = null;
|
|
346
|
-
backoff = 1_000;
|
|
347
|
-
if (exit !== null)
|
|
348
|
-
break;
|
|
349
|
-
if (response.status === 'attending') {
|
|
350
|
-
const acquired = generation !== response.generation;
|
|
351
|
-
if (acquired) {
|
|
352
|
-
logger.info('attendant.lease', { event: 'acquired', generation: response.generation });
|
|
353
|
-
}
|
|
354
|
-
generation = response.generation;
|
|
355
|
-
// A grant sent before a clock discontinuity says nothing about now.
|
|
356
|
-
leaseEndsAt = sentEpoch === epoch ? sentAt + response.lease_remaining_ms : null;
|
|
357
|
-
cursor = response.message_cursor;
|
|
358
|
-
rotated = false;
|
|
359
|
-
// The answer waiter claims under the generation this status names.
|
|
360
|
-
if (acquired)
|
|
361
|
-
writeStatus();
|
|
362
|
-
setPhase('attending');
|
|
363
|
-
let handedOff = 'done';
|
|
364
|
-
if (options.acceptsMessages && options.onMessages !== undefined) {
|
|
365
|
-
try {
|
|
366
|
-
handedOff = await options.onMessages(response.messages, handle);
|
|
367
|
-
}
|
|
368
|
-
catch (err) {
|
|
369
|
-
logger.error('attendant.state', {
|
|
370
|
-
phase,
|
|
371
|
-
reason: 'message-handoff-failed',
|
|
372
|
-
message: err instanceof Error ? err.message : String(err),
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
if (handedOff === 'retry-soon' && exit === null) {
|
|
377
|
-
// Ask again without holding the exchange, backing off to the ordinary
|
|
378
|
-
// hold so a message that keeps waiting cannot spin the attendant.
|
|
379
|
-
const pauseMs = ATTENDANT_MESSAGE_RETRY_MS * 2 ** Math.min(messageRetries, 4);
|
|
380
|
-
messageRetries += 1;
|
|
381
|
-
await pause(Math.min(pauseMs, waitSeconds * 1_000));
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
messageRetries = 0;
|
|
385
|
-
}
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
if (response.status === 'owned') {
|
|
389
|
-
if (generation !== null)
|
|
390
|
-
loseLease('owned-by-another-incarnation');
|
|
391
|
-
generation = null;
|
|
392
|
-
cursor = undefined;
|
|
393
|
-
setPhase('waiting-for-lease');
|
|
394
|
-
await pause(Math.min(Math.max(response.retry_after_ms, 1_000), 120_000));
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
// withdrawn: the service holds no lease for this incarnation.
|
|
398
|
-
if (generation !== null) {
|
|
399
|
-
loseLease('withdrawn-by-service');
|
|
400
|
-
generation = null;
|
|
401
|
-
stop('withdrawn-by-service');
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
// Refused while acquiring: this incarnation id already released its lease
|
|
405
|
-
// (a report made before a restart). A fresh id may acquire once.
|
|
406
|
-
const next = rotated ? null : options.rotateIncarnation(incarnation);
|
|
407
|
-
if (next === null) {
|
|
408
|
-
stop('withdrawn-by-service');
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
logger.info('attendant.lease', { event: 'incarnation-rotated' });
|
|
412
|
-
incarnation = next;
|
|
413
|
-
rotated = true;
|
|
414
|
-
cursor = undefined;
|
|
415
|
-
}
|
|
416
|
-
// -- end -----------------------------------------------------------------
|
|
417
|
-
stopping.abort();
|
|
418
|
-
abortExchange();
|
|
419
|
-
await probeLoop;
|
|
420
|
-
const reason = exit ?? 'signal';
|
|
421
|
-
let reported = null;
|
|
422
|
-
const final = finalReport(reason);
|
|
423
|
-
if (networkStarted && final !== null && (final === 'ended' || generation !== null)) {
|
|
424
|
-
const client = safeClient(options);
|
|
425
|
-
if (client !== null) {
|
|
426
|
-
reported = final;
|
|
427
|
-
const bound = new AbortController();
|
|
428
|
-
const timer = setTimeout(() => bound.abort(), ATTENDANT_FINAL_REPORT_MS);
|
|
429
|
-
try {
|
|
430
|
-
await client.attend(options.sessionId, {
|
|
431
|
-
incarnation,
|
|
432
|
-
...(generation === null ? {} : { generation }),
|
|
433
|
-
state: final,
|
|
434
|
-
}, { waitSeconds: 0, signal: bound.signal });
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
// Without the report the lease lapses and presence reads out of reach.
|
|
438
|
-
}
|
|
439
|
-
finally {
|
|
440
|
-
clearTimeout(timer);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
leaseEndsAt = null;
|
|
445
|
-
phase = 'exited';
|
|
446
|
-
phaseReason = reason;
|
|
447
|
-
writeStatus();
|
|
448
|
-
logger.info('attendant.exit', {
|
|
449
|
-
reason,
|
|
450
|
-
...(reported === null ? {} : { reported }),
|
|
451
|
-
...(generation === null ? {} : { generation }),
|
|
452
|
-
});
|
|
453
|
-
return { reason, reported };
|
|
454
|
-
}
|
|
455
|
-
/** Which final state a way of ending reports, if any. */
|
|
456
|
-
function finalReport(reason) {
|
|
457
|
-
switch (reason) {
|
|
458
|
-
case 'harness-gone':
|
|
459
|
-
case 'session-replaced':
|
|
460
|
-
case 'session-end-hook':
|
|
461
|
-
case 'signal':
|
|
462
|
-
return 'ended';
|
|
463
|
-
case 'superseded':
|
|
464
|
-
// A newer start of the same session attends now; ending would end it.
|
|
465
|
-
return 'withdrawn';
|
|
466
|
-
case 'withdrawn-by-service':
|
|
467
|
-
case 'unauthorized':
|
|
468
|
-
case 'claim-lost':
|
|
469
|
-
// Another attendant may hold this incarnation's lease now; any report
|
|
470
|
-
// from here could release it.
|
|
471
|
-
return null;
|
|
472
|
-
default:
|
|
473
|
-
return reason.startsWith('gate:') && reason !== 'gate:not-paired' ? 'withdrawn' : null;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
function safeClient(options) {
|
|
477
|
-
try {
|
|
478
|
-
return options.client();
|
|
479
|
-
}
|
|
480
|
-
catch {
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
/** Rejects when `signal` aborts, so an awaited call cannot outlive a stop. */
|
|
485
|
-
function abortable(work, signal) {
|
|
486
|
-
if (signal.aborted)
|
|
487
|
-
return Promise.reject(new Error('aborted'));
|
|
488
|
-
return new Promise((resolve, reject) => {
|
|
489
|
-
const onAbort = () => reject(new Error('aborted'));
|
|
490
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
491
|
-
work.then((value) => {
|
|
492
|
-
signal.removeEventListener('abort', onAbort);
|
|
493
|
-
resolve(value);
|
|
494
|
-
}, (err) => {
|
|
495
|
-
signal.removeEventListener('abort', onAbort);
|
|
496
|
-
reject(err);
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
/** Production clocks: `performance.now()` pauses with the machine; `Date.now()` does not. */
|
|
501
|
-
export const systemAttendantClock = {
|
|
502
|
-
monotonic: () => performance.now(),
|
|
503
|
-
wall: () => Date.now(),
|
|
504
|
-
sleep: (milliseconds, signal) => new Promise((resolve, reject) => {
|
|
505
|
-
if (signal.aborted) {
|
|
506
|
-
reject(signal.reason);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
const timer = setTimeout(() => {
|
|
510
|
-
signal.removeEventListener('abort', onAbort);
|
|
511
|
-
resolve();
|
|
512
|
-
}, milliseconds);
|
|
513
|
-
const onAbort = () => {
|
|
514
|
-
clearTimeout(timer);
|
|
515
|
-
reject(signal.reason);
|
|
516
|
-
};
|
|
517
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
518
|
-
}),
|
|
519
|
-
};
|
|
520
|
-
//# sourceMappingURL=session-attendant.js.map
|