@ours.network/fleet 1.2.0-nightly.7 → 1.2.0-nightly.71
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/README.md +279 -68
- package/dist/account-origin.d.ts +2 -0
- package/dist/account-origin.js +6 -0
- package/dist/agent-ours/bridge.js +26 -9
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/file-rpc.d.ts +12 -0
- package/dist/agent-ours/file-rpc.js +18 -4
- package/dist/agent-ours/harness.js +9 -1
- package/dist/agent-ours/mcp-endpoint.d.ts +19 -0
- package/dist/agent-ours/mcp-endpoint.js +98 -25
- package/dist/agent-ours/runtime.d.ts +9 -1
- package/dist/agent-ours/runtime.js +50 -2
- package/dist/agent-ours/service.d.ts +24 -1
- package/dist/agent-ours/service.js +97 -13
- package/dist/application/codex-models.d.ts +15 -0
- package/dist/application/codex-models.js +56 -0
- package/dist/application/conversation-history.d.ts +34 -0
- package/dist/application/conversation-history.js +280 -0
- package/dist/application/conversation-role-observer.d.ts +62 -0
- package/dist/application/conversation-role-observer.js +206 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +3 -3
- package/dist/application/log-service.js +8 -8
- package/dist/application/model-catalog.d.ts +1 -1
- package/dist/application/model-catalog.js +5 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/resource-observer.d.ts +40 -0
- package/dist/application/resource-observer.js +278 -0
- package/dist/application/role-creation-service.js +3 -2
- package/dist/application/role-removal-service.js +17 -3
- package/dist/application/role-repository.d.ts +1 -1
- package/dist/application/role-repository.js +30 -7
- package/dist/application/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +61 -17
- package/dist/application/session-control.js +207 -23
- package/dist/application/supervisor-ours-tools.js +5 -11
- package/dist/application/task-layouts.d.ts +91 -0
- package/dist/application/task-layouts.js +261 -0
- package/dist/application/task-room-service.d.ts +101 -3
- package/dist/application/task-room-service.js +286 -33
- package/dist/application/types.d.ts +2 -0
- package/dist/atomic-file.d.ts +34 -1
- package/dist/atomic-file.js +161 -18
- package/dist/briefing.d.ts +3 -1
- package/dist/briefing.js +97 -31
- package/dist/build-info.json +9 -4
- package/dist/capabilities.d.ts +9 -1
- package/dist/capabilities.js +13 -0
- package/dist/cli.js +400 -37
- package/dist/client-profile.d.ts +5 -17
- package/dist/client-profile.js +4 -127
- package/dist/config.d.ts +45 -0
- package/dist/config.js +57 -6
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +4 -4
- package/dist/daemon-recovery.d.ts +14 -9
- package/dist/daemon-recovery.js +124 -115
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +207 -19
- package/dist/doctor.d.ts +4 -1
- package/dist/doctor.js +75 -60
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/file-delivery/reader.d.ts +15 -0
- package/dist/file-delivery/reader.js +63 -0
- package/dist/file-delivery/store.d.ts +15 -0
- package/dist/file-delivery/store.js +124 -0
- package/dist/file-delivery/types.d.ts +19 -0
- package/dist/file-delivery/types.js +1 -0
- package/dist/fleet-command-audit.d.ts +7 -0
- package/dist/fleet-command-audit.js +24 -8
- package/dist/harness/agent-session.d.ts +4 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/claude-code.js +21 -11
- package/dist/harness/claude-runtime.d.ts +12 -0
- package/dist/harness/claude-runtime.js +43 -0
- package/dist/harness/codex-app-server-proxy.d.ts +4 -1
- package/dist/harness/codex-app-server-proxy.js +41 -5
- package/dist/harness/codex-reasoning.d.ts +5 -0
- package/dist/harness/codex-reasoning.js +12 -0
- package/dist/harness/codex-runtime.d.ts +11 -1
- package/dist/harness/codex-runtime.js +31 -4
- package/dist/harness/codex-session.js +2 -2
- package/dist/harness/codex.js +24 -12
- package/dist/init-wizard.d.ts +2 -0
- package/dist/init-wizard.js +38 -19
- package/dist/isolation/policy.js +6 -1
- package/dist/isolation/types.d.ts +5 -0
- package/dist/managed-cli.d.ts +253 -0
- package/dist/managed-cli.js +923 -0
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/minimal-setup.d.ts +2 -0
- package/dist/minimal-setup.js +44 -0
- package/dist/monitor.d.ts +9 -34
- package/dist/monitor.js +90 -153
- package/dist/notifications/fleet-producer.d.ts +22 -0
- package/dist/notifications/fleet-producer.js +237 -0
- package/dist/notifications/outbox.d.ts +46 -0
- package/dist/notifications/outbox.js +158 -0
- package/dist/notifications/target-cleanup.d.ts +11 -0
- package/dist/notifications/target-cleanup.js +63 -0
- package/dist/notifications/task-producer.d.ts +20 -0
- package/dist/notifications/task-producer.js +100 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +20 -3
- package/dist/owner-channel/channel.js +17 -6
- package/dist/owner-channel/commands.d.ts +4 -0
- package/dist/owner-channel/commands.js +8 -0
- package/dist/owner-channel/lifecycle-outbox.js +4 -3
- package/dist/owner-channel/ours-client.js +2 -17
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +2 -0
- package/dist/preset-migration.js +43 -0
- package/dist/process-incarnation.d.ts +23 -0
- package/dist/process-incarnation.js +42 -0
- package/dist/reminders/manager.d.ts +75 -0
- package/dist/reminders/manager.js +278 -0
- package/dist/reminders/runtime.d.ts +27 -0
- package/dist/reminders/runtime.js +32 -0
- package/dist/reminders/schedule.d.ts +37 -0
- package/dist/reminders/schedule.js +104 -0
- package/dist/reminders/store.d.ts +341 -0
- package/dist/reminders/store.js +762 -0
- package/dist/reminders/tools.d.ts +20 -0
- package/dist/reminders/tools.js +114 -0
- package/dist/resolved-plan.js +1 -0
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +84 -0
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +65 -5
- package/dist/rooms-tasks/close.d.ts +6 -2
- package/dist/rooms-tasks/close.js +246 -37
- package/dist/rooms-tasks/config.d.ts +1 -1
- package/dist/rooms-tasks/config.js +11 -4
- package/dist/rooms-tasks/cowork-adapter.d.ts +12 -5
- package/dist/rooms-tasks/cowork-adapter.js +15 -121
- package/dist/rooms-tasks/cowork-http.js +56 -6
- package/dist/rooms-tasks/deletion.d.ts +1 -1
- package/dist/rooms-tasks/deletion.js +84 -5
- package/dist/rooms-tasks/erasure.d.ts +6 -0
- package/dist/rooms-tasks/erasure.js +173 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -4
- package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
- package/dist/rooms-tasks/layout-binding-client.js +56 -0
- package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
- package/dist/rooms-tasks/layout-binding-grants.js +128 -0
- package/dist/rooms-tasks/layout-cli.d.ts +4 -0
- package/dist/rooms-tasks/layout-cli.js +107 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +43 -0
- package/dist/rooms-tasks/layout-service.js +203 -0
- package/dist/rooms-tasks/layout.d.ts +121 -0
- package/dist/rooms-tasks/layout.js +286 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -0
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +85 -0
- package/dist/rooms-tasks/member-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +3 -0
- package/dist/rooms-tasks/provision.js +101 -49
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/room-state.d.ts +4 -2
- package/dist/rooms-tasks/room-state.js +47 -8
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +52 -8
- package/dist/rooms-tasks/task-state.js +251 -26
- package/dist/rooms-tasks/types.d.ts +40 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +35 -0
- package/dist/rooms-tasks/workspace.js +325 -0
- package/dist/runner.d.ts +33 -4
- package/dist/runner.js +410 -62
- package/dist/session/acp.d.ts +75 -2
- package/dist/session/acp.js +345 -82
- package/dist/session/arbiter.d.ts +98 -0
- package/dist/session/arbiter.js +272 -3
- package/dist/session/codex-app-server.d.ts +45 -1
- package/dist/session/codex-app-server.js +225 -32
- package/dist/session/control.d.ts +17 -3
- package/dist/session/control.js +213 -23
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-observation.d.ts +25 -0
- package/dist/session/conversation-observation.js +100 -0
- package/dist/session/conversation-replay.d.ts +50 -0
- package/dist/session/conversation-replay.js +203 -0
- package/dist/session/conversation-store.d.ts +15 -1
- package/dist/session/conversation-store.js +206 -26
- package/dist/session/conversation-types.d.ts +57 -4
- package/dist/session/prompt-batch.d.ts +79 -0
- package/dist/session/prompt-batch.js +120 -0
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +42 -1
- package/dist/setup-tunnel-preflight.d.ts +34 -0
- package/dist/setup-tunnel-preflight.js +62 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +40 -4
- package/dist/startup-prompt.d.ts +15 -0
- package/dist/startup-prompt.js +71 -0
- package/dist/startup-readiness.d.ts +32 -0
- package/dist/startup-readiness.js +101 -0
- package/dist/startup-telemetry.d.ts +16 -0
- package/dist/startup-telemetry.js +46 -0
- package/dist/subscriptions/cli.d.ts +91 -0
- package/dist/subscriptions/cli.js +223 -0
- package/dist/subscriptions/launch.d.ts +37 -0
- package/dist/subscriptions/launch.js +93 -0
- package/dist/subscriptions/login.d.ts +77 -0
- package/dist/subscriptions/login.js +215 -0
- package/dist/subscriptions/observed.d.ts +7 -0
- package/dist/subscriptions/observed.js +44 -0
- package/dist/subscriptions/probe.d.ts +10 -0
- package/dist/subscriptions/probe.js +73 -0
- package/dist/subscriptions/service.d.ts +102 -0
- package/dist/subscriptions/service.js +364 -0
- package/dist/subscriptions/store.d.ts +71 -0
- package/dist/subscriptions/store.js +143 -0
- package/dist/supervisor/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +26 -13
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +270 -0
- package/dist/supervisor/registration-refresh.d.ts +45 -0
- package/dist/supervisor/registration-refresh.js +266 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +19 -33
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +23 -8
- package/dist/temp-lifecycle.js +123 -66
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/dist/tunnel-setup-completion.d.ts +7 -0
- package/dist/tunnel-setup-completion.js +13 -0
- package/dist/tunnel-setup-input.d.ts +19 -0
- package/dist/tunnel-setup-input.js +124 -0
- package/dist/watchdog/query.d.ts +3 -1
- package/dist/watchdog/query.js +2 -1
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +65 -2
- package/dist/web/auth.js +201 -11
- package/dist/web/available-port.d.ts +2 -0
- package/dist/web/available-port.js +23 -0
- package/dist/web/control.d.ts +3 -3
- package/dist/web/control.js +10 -8
- package/dist/web/conversation-stream-codec.d.ts +23 -0
- package/dist/web/conversation-stream-codec.js +110 -0
- package/dist/web/conversation-stream.d.ts +25 -0
- package/dist/web/conversation-stream.js +231 -0
- package/dist/web/events.d.ts +2 -0
- package/dist/web/events.js +9 -0
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +32 -0
- package/dist/web/prefix-gateway.js +248 -0
- package/dist/web/resource-views.d.ts +124 -0
- package/dist/web/resource-views.js +424 -0
- package/dist/web/runtime.d.ts +3 -1
- package/dist/web/runtime.js +132 -34
- package/dist/web/server.d.ts +19 -0
- package/dist/web/server.js +802 -67
- package/dist/web/service.d.ts +11 -2
- package/dist/web/service.js +30 -4
- package/dist/web/workspace-devices.d.ts +107 -0
- package/dist/web/workspace-devices.js +277 -0
- package/dist/web/workspace-tunnel.d.ts +1 -0
- package/dist/web/workspace-tunnel.js +34 -0
- package/dist/workspace-enrollment.d.ts +83 -0
- package/dist/workspace-enrollment.js +580 -0
- package/dist/workspace-replacement.d.ts +66 -0
- package/dist/workspace-replacement.js +220 -0
- package/docs/acp-files.md +146 -0
- package/docs/conversation-stream.md +196 -0
- package/docs/fleet-migration.md +54 -0
- package/docs/live-voice.md +13 -0
- package/docs/room-layouts.md +227 -0
- package/docs/self-reminders.md +374 -0
- package/docs/task-workspaces.md +225 -0
- package/docs/validation/managed-cli-permissions.md +421 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/scoped.yaml +21 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +24 -20
- package/presets/README.md +78 -0
- package/presets/brain-catalog.json +8 -2
- package/presets/fleet/agent_templates/Critic.yaml +1 -1
- package/presets/fleet/agent_templates/Developer.yaml +1 -1
- package/presets/fleet/agent_templates/Engineer.yaml +5 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -2
- package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
- package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +12 -11
- package/presets/fleet/roles/Critic.yaml +13 -0
- package/presets/fleet/roles/Developer.yaml +13 -0
- package/presets/fleet/roles/Engineer.yaml +53 -0
- package/presets/fleet/room_layouts/engineering.yaml +25 -0
- package/presets/fleet/room_layouts/pair.yaml +24 -0
- package/presets/fleet/room_layouts/single.yaml +17 -0
- package/presets/fleet/room_layouts/team.yaml +30 -0
- package/presets/fleet/room_templates/engineering.yaml +19 -0
- package/presets/manifest.json +2 -2
- package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
- package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
- package/dist/web-app/icons/ours-fleet.svg +0 -4
- package/dist/web-app/index.html +0 -17
- package/dist/web-app/manifest.webmanifest +0 -15
- package/dist/web-app/offline.html +0 -18
- package/dist/web-app/sw.js +0 -51
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, readFileSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { replaceFileAtomically } from '../atomic-file.js';
|
|
5
|
+
import { ReminderScheduleError, resolveSchedule } from './schedule.js';
|
|
6
|
+
/**
|
|
7
|
+
* Durable, private, single-writer state for one agent's self-reminders.
|
|
8
|
+
*
|
|
9
|
+
* The runner that supervises the agent is the only writer: the agent's MCP
|
|
10
|
+
* tools, the dispatch timer and the session's release guard all run in that
|
|
11
|
+
* one process, and every mutation here is synchronous, so each method call is
|
|
12
|
+
* its own linearization point. Asynchronous callers (a queue receipt, a turn
|
|
13
|
+
* completion) never carry state across an `await`; they re-read the current
|
|
14
|
+
* record and apply only forward transitions, so a late receipt can never undo
|
|
15
|
+
* an acknowledgement, update or cancellation that happened meanwhile.
|
|
16
|
+
*
|
|
17
|
+
* Event lifecycle — the boundary that matters is `dispatched`:
|
|
18
|
+
*
|
|
19
|
+
* pending ──admit──▶ admitting ──release guard──▶ dispatched ──ack──▶ acknowledged
|
|
20
|
+
* ▲ │ │
|
|
21
|
+
* └── not released ───┘ └─ restart ─▶ uncertain (held)
|
|
22
|
+
*
|
|
23
|
+
* `dispatched` is written durably by the session's release guard immediately
|
|
24
|
+
* before the prompt is handed to the model. An event found `admitting` after a
|
|
25
|
+
* restart therefore provably never reached the model (reminder prompt bodies
|
|
26
|
+
* are not retained, so session recovery cannot replay them) and is retried
|
|
27
|
+
* under the same event id. An event found `dispatched` may or may not have been
|
|
28
|
+
* seen, so it is held as `uncertain` and never automatically re-admitted.
|
|
29
|
+
*
|
|
30
|
+
* Urgency is an explicit opt-in per reminder (`interrupt`). Absent means false,
|
|
31
|
+
* so stores written before it existed keep their exact idle-only meaning, and
|
|
32
|
+
* an older runtime that ignores the field stays idle-only. An urgent event may
|
|
33
|
+
* request at most ONE interruption of the agent's current turn per revision:
|
|
34
|
+
* the request is durably recorded before the session is asked to cancel, so a
|
|
35
|
+
* crash, a forced adapter restart or a second urgent reminder can never turn
|
|
36
|
+
* it into a cancellation loop. Every later admission of that event is
|
|
37
|
+
* idle-only, and only the release guard ever records `dispatched`.
|
|
38
|
+
*/
|
|
39
|
+
export const SELF_REMINDERS_FILE = '.self-reminders.json';
|
|
40
|
+
export const MAX_OPEN_REMINDERS = 100;
|
|
41
|
+
export const MAX_RETAINED_CLOSED = 200;
|
|
42
|
+
export const DEFAULT_LATE_WINDOW_SECONDS = 3600;
|
|
43
|
+
export const MAX_LATE_WINDOW_SECONDS = 7 * 24 * 3600;
|
|
44
|
+
export const MIN_LATE_WINDOW_SECONDS = 60;
|
|
45
|
+
const MAX_HORIZON_MS = 5 * 366 * 24 * 3600 * 1000;
|
|
46
|
+
const MAX_TITLE_CHARS = 200;
|
|
47
|
+
const MAX_NOTE_CHARS = 4000;
|
|
48
|
+
const MAX_REQUEST_KEY_CHARS = 200;
|
|
49
|
+
const INTERRUPT_OUTCOMES = new Set(['requested', 'cancelled', 'ended', 'forced', 'failed', 'unknown']);
|
|
50
|
+
export class ReminderError extends Error {
|
|
51
|
+
code;
|
|
52
|
+
constructor(code, message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.code = code;
|
|
55
|
+
this.name = 'ReminderError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const durableWrite = (path, contents) => replaceFileAtomically(path, contents, 0o600, {}, { durable: true });
|
|
59
|
+
const OPEN_EVENT_STATES = new Set(['pending', 'admitting', 'dispatched', 'uncertain']);
|
|
60
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
61
|
+
function digest(value) {
|
|
62
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
63
|
+
}
|
|
64
|
+
function text(raw, field, max, required) {
|
|
65
|
+
if (raw === undefined || raw === null) {
|
|
66
|
+
if (required)
|
|
67
|
+
throw new ReminderError('invalid_request', `${field} is required`);
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
if (typeof raw !== 'string')
|
|
71
|
+
throw new ReminderError('invalid_request', `${field} must be text`);
|
|
72
|
+
const value = raw.replace(/\r\n?/g, '\n').normalize('NFC');
|
|
73
|
+
if (required && !value.trim())
|
|
74
|
+
throw new ReminderError('invalid_request', `${field} must not be blank`);
|
|
75
|
+
if (value.includes('\0'))
|
|
76
|
+
throw new ReminderError('invalid_request', `${field} must not contain NUL`);
|
|
77
|
+
if (Array.from(value).length > max)
|
|
78
|
+
throw new ReminderError('invalid_request', `${field} exceeds ${max} characters`);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function lateWindow(raw) {
|
|
82
|
+
if (raw === undefined || raw === null)
|
|
83
|
+
return DEFAULT_LATE_WINDOW_SECONDS;
|
|
84
|
+
if (typeof raw !== 'number' || !Number.isSafeInteger(raw)
|
|
85
|
+
|| raw < MIN_LATE_WINDOW_SECONDS || raw > MAX_LATE_WINDOW_SECONDS)
|
|
86
|
+
throw new ReminderError('invalid_request', `late_window_seconds must be a whole number from ${MIN_LATE_WINDOW_SECONDS} to ${MAX_LATE_WINDOW_SECONDS}`);
|
|
87
|
+
return raw;
|
|
88
|
+
}
|
|
89
|
+
function revisionOf(raw) {
|
|
90
|
+
if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 1)
|
|
91
|
+
throw new ReminderError('invalid_request', 'expected_revision must be the current positive revision');
|
|
92
|
+
return raw;
|
|
93
|
+
}
|
|
94
|
+
function urgency(raw) {
|
|
95
|
+
if (raw === undefined || raw === null)
|
|
96
|
+
return false;
|
|
97
|
+
if (typeof raw !== 'boolean')
|
|
98
|
+
throw new ReminderError('invalid_request', 'interrupt must be true or false');
|
|
99
|
+
return raw;
|
|
100
|
+
}
|
|
101
|
+
export class ReminderStore {
|
|
102
|
+
owner;
|
|
103
|
+
now;
|
|
104
|
+
log;
|
|
105
|
+
write;
|
|
106
|
+
path;
|
|
107
|
+
state;
|
|
108
|
+
/**
|
|
109
|
+
* Load or create the store for exactly this owner (see `ReminderOwner`). A
|
|
110
|
+
* store written for anyone else is never adopted: it is moved aside and
|
|
111
|
+
* nothing in it is delivered, so a same-named successor or a replaced
|
|
112
|
+
* identity cannot inherit a predecessor's wakes.
|
|
113
|
+
*/
|
|
114
|
+
constructor(stateDir, owner, now, log = () => { }, write = durableWrite) {
|
|
115
|
+
this.owner = owner;
|
|
116
|
+
this.now = now;
|
|
117
|
+
this.log = log;
|
|
118
|
+
this.write = write;
|
|
119
|
+
this.path = join(stateDir, SELF_REMINDERS_FILE);
|
|
120
|
+
let restored;
|
|
121
|
+
if (existsSync(this.path)) {
|
|
122
|
+
let reason;
|
|
123
|
+
try {
|
|
124
|
+
if (!privateFile(this.path))
|
|
125
|
+
throw new Error('insecure');
|
|
126
|
+
const parsed = JSON.parse(readFileSync(this.path, 'utf8'));
|
|
127
|
+
if (!validFile(parsed))
|
|
128
|
+
reason = 'corrupt';
|
|
129
|
+
else if (!sameOwner(parsed.owner, owner))
|
|
130
|
+
reason = 'foreign-owner';
|
|
131
|
+
else {
|
|
132
|
+
// A permanent agent's runtime is re-provisioned on every orderly
|
|
133
|
+
// start; record the current one, keep the reminders.
|
|
134
|
+
parsed.owner = { ...owner };
|
|
135
|
+
restored = parsed;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
reason = 'corrupt';
|
|
140
|
+
}
|
|
141
|
+
if (!restored) {
|
|
142
|
+
const aside = `${this.path}.${reason}-${now()}`;
|
|
143
|
+
renameSync(this.path, aside);
|
|
144
|
+
chmodSync(aside, 0o600);
|
|
145
|
+
this.log(`[${owner.role}] self-reminder store ${reason}; moved aside, nothing in it will be delivered`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.state = restored ?? { version: 1, owner: { ...owner }, reminders: {}, events: {} };
|
|
149
|
+
if (restored)
|
|
150
|
+
this.recover();
|
|
151
|
+
this.persist();
|
|
152
|
+
}
|
|
153
|
+
/** Restart recovery. Never re-admits anything that may have reached the model. */
|
|
154
|
+
recover() {
|
|
155
|
+
const at = iso(this.now());
|
|
156
|
+
for (const event of Object.values(this.state.events)) {
|
|
157
|
+
// An interruption requested by the previous process has an unknown end.
|
|
158
|
+
// Keep the mark (so it is never requested again) and say so.
|
|
159
|
+
if (event.interrupt?.outcome === 'requested')
|
|
160
|
+
event.interrupt = { ...event.interrupt, outcome: 'unknown', settledAt: at, reason: 'restarted_before_outcome' };
|
|
161
|
+
if (event.state === 'admitting') {
|
|
162
|
+
// The release guard never ran, so the model never saw it.
|
|
163
|
+
event.state = 'pending';
|
|
164
|
+
delete event.promptId;
|
|
165
|
+
delete event.retryAt;
|
|
166
|
+
}
|
|
167
|
+
else if (event.state === 'dispatched') {
|
|
168
|
+
event.state = 'uncertain';
|
|
169
|
+
event.reason = 'restarted_after_dispatch';
|
|
170
|
+
event.closedAt = at;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
persist() {
|
|
175
|
+
this.write(this.path, JSON.stringify(this.state, null, 2) + '\n');
|
|
176
|
+
chmodSync(this.path, 0o600);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Apply a mutation and persist it as one step. If the write fails the
|
|
180
|
+
* in-memory state is restored, so memory never claims what disk does not.
|
|
181
|
+
*/
|
|
182
|
+
commit(mutate) {
|
|
183
|
+
const before = structuredClone(this.state);
|
|
184
|
+
try {
|
|
185
|
+
const result = mutate(this.state);
|
|
186
|
+
this.persist();
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
this.state = before;
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
reminder(id) {
|
|
195
|
+
if (typeof id !== 'string' || !Object.hasOwn(this.state.reminders, id))
|
|
196
|
+
throw new ReminderError('not_found', 'no such reminder for this agent');
|
|
197
|
+
return this.state.reminders[id];
|
|
198
|
+
}
|
|
199
|
+
event(id) {
|
|
200
|
+
if (typeof id !== 'string' || !Object.hasOwn(this.state.events, id))
|
|
201
|
+
throw new ReminderError('not_found', 'no such reminder event for this agent');
|
|
202
|
+
return this.state.events[id];
|
|
203
|
+
}
|
|
204
|
+
schedule(input, previous) {
|
|
205
|
+
const title = input.title === undefined && previous ? previous.title : text(input.title, 'title', MAX_TITLE_CHARS, true);
|
|
206
|
+
const note = input.note === undefined && previous ? previous.note : text(input.note, 'note', MAX_NOTE_CHARS, false);
|
|
207
|
+
const retime = !previous || input.local_time !== undefined || input.time_zone !== undefined || input.fold !== undefined;
|
|
208
|
+
const late = input.late_window_seconds === undefined && previous
|
|
209
|
+
? previous.lateWindowSeconds : lateWindow(input.late_window_seconds);
|
|
210
|
+
const interrupt = input.interrupt === undefined && previous
|
|
211
|
+
? previous.interrupt === true : urgency(input.interrupt);
|
|
212
|
+
let when;
|
|
213
|
+
if (retime) {
|
|
214
|
+
try {
|
|
215
|
+
when = resolveSchedule(input.local_time ?? previous?.localTime, input.time_zone ?? previous?.timeZone, input.fold === undefined ? (input.local_time === undefined && input.time_zone === undefined ? previous?.fold : undefined) : input.fold);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error instanceof ReminderScheduleError)
|
|
219
|
+
throw new ReminderError(error.code, error.message);
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
const now = this.now();
|
|
223
|
+
if (when.dueAt <= now)
|
|
224
|
+
throw new ReminderError('not_in_future', 'the reminder time must be in the future');
|
|
225
|
+
if (when.dueAt - now > MAX_HORIZON_MS)
|
|
226
|
+
throw new ReminderError('too_far', 'the reminder time must be within five years');
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
when = { localTime: previous.localTime, timeZone: previous.timeZone, fold: previous.fold, dueAt: Date.parse(previous.dueAt) };
|
|
230
|
+
}
|
|
231
|
+
return { title, note, ...when, late, interrupt };
|
|
232
|
+
}
|
|
233
|
+
openCount() {
|
|
234
|
+
return Object.values(this.state.reminders).filter(item => item.state === 'active' || item.state === 'delivered').length;
|
|
235
|
+
}
|
|
236
|
+
create(requestKey, input) {
|
|
237
|
+
const key = text(requestKey, 'request_key', MAX_REQUEST_KEY_CHARS, true);
|
|
238
|
+
const requestDigest = digest({
|
|
239
|
+
title: input.title ?? null, note: input.note ?? null, local_time: input.local_time ?? null,
|
|
240
|
+
time_zone: input.time_zone ?? null, fold: input.fold ?? null,
|
|
241
|
+
late_window_seconds: input.late_window_seconds ?? null,
|
|
242
|
+
// Only an explicit urgent request changes the digest, so a retry of a
|
|
243
|
+
// call made before `interrupt` existed (or with it false) still matches.
|
|
244
|
+
...(input.interrupt === true ? { interrupt: true } : {}),
|
|
245
|
+
});
|
|
246
|
+
const existing = Object.values(this.state.reminders).find(item => item.requestKey === key);
|
|
247
|
+
if (existing) {
|
|
248
|
+
if (existing.requestDigest !== requestDigest)
|
|
249
|
+
throw new ReminderError('request_key_conflict', 'request_key was already used with different input');
|
|
250
|
+
return { reminder: this.view(existing), created: false };
|
|
251
|
+
}
|
|
252
|
+
if (this.openCount() >= MAX_OPEN_REMINDERS)
|
|
253
|
+
throw new ReminderError('limit_reached', `at most ${MAX_OPEN_REMINDERS} open reminders per agent`);
|
|
254
|
+
const values = this.schedule(input);
|
|
255
|
+
return this.commit(state => {
|
|
256
|
+
const at = iso(this.now());
|
|
257
|
+
const record = {
|
|
258
|
+
id: `rem_${randomUUID()}`, requestKey: key, requestDigest, revision: 1, state: 'active',
|
|
259
|
+
title: values.title, note: values.note, localTime: values.localTime, timeZone: values.timeZone,
|
|
260
|
+
...(values.fold === undefined ? {} : { fold: values.fold }),
|
|
261
|
+
dueAt: iso(values.dueAt), lateWindowSeconds: values.late,
|
|
262
|
+
expiresAt: iso(values.dueAt + values.late * 1000), createdAt: at, updatedAt: at,
|
|
263
|
+
...(values.interrupt ? { interrupt: true } : {}),
|
|
264
|
+
};
|
|
265
|
+
state.reminders[record.id] = record;
|
|
266
|
+
this.prune(state);
|
|
267
|
+
return { reminder: this.view(record), created: true };
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
update(id, expectedRevision, input) {
|
|
271
|
+
const record = this.reminder(id);
|
|
272
|
+
const revision = revisionOf(expectedRevision);
|
|
273
|
+
if (record.revision !== revision)
|
|
274
|
+
throw new ReminderError('stale_revision', `reminder is at revision ${record.revision}`);
|
|
275
|
+
if (record.state !== 'active' && record.state !== 'delivered')
|
|
276
|
+
throw new ReminderError('closed', `reminder is ${record.state}`);
|
|
277
|
+
if (Object.values(input).every(value => value === undefined))
|
|
278
|
+
throw new ReminderError('invalid_request', 'nothing to update');
|
|
279
|
+
const values = this.schedule(input, record);
|
|
280
|
+
return this.commit(state => {
|
|
281
|
+
const target = state.reminders[record.id];
|
|
282
|
+
const at = iso(this.now());
|
|
283
|
+
this.supersedeEvents(state, target.id, 'superseded_by_update', at);
|
|
284
|
+
Object.assign(target, {
|
|
285
|
+
revision: target.revision + 1, state: 'active',
|
|
286
|
+
title: values.title, note: values.note, localTime: values.localTime, timeZone: values.timeZone,
|
|
287
|
+
dueAt: iso(values.dueAt), lateWindowSeconds: values.late,
|
|
288
|
+
expiresAt: iso(values.dueAt + values.late * 1000), updatedAt: at,
|
|
289
|
+
});
|
|
290
|
+
if (values.fold === undefined)
|
|
291
|
+
delete target.fold;
|
|
292
|
+
else
|
|
293
|
+
target.fold = values.fold;
|
|
294
|
+
if (values.interrupt)
|
|
295
|
+
target.interrupt = true;
|
|
296
|
+
else
|
|
297
|
+
delete target.interrupt;
|
|
298
|
+
delete target.closedReason;
|
|
299
|
+
return this.view(target);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
cancel(id, expectedRevision) {
|
|
303
|
+
const record = this.reminder(id);
|
|
304
|
+
const revision = revisionOf(expectedRevision);
|
|
305
|
+
if (record.revision !== revision)
|
|
306
|
+
throw new ReminderError('stale_revision', `reminder is at revision ${record.revision}`);
|
|
307
|
+
if (record.state !== 'active' && record.state !== 'delivered')
|
|
308
|
+
throw new ReminderError('closed', `reminder is ${record.state}`);
|
|
309
|
+
return this.commit(state => {
|
|
310
|
+
const target = state.reminders[record.id];
|
|
311
|
+
const at = iso(this.now());
|
|
312
|
+
this.supersedeEvents(state, target.id, 'cancelled', at);
|
|
313
|
+
target.revision++;
|
|
314
|
+
target.state = 'cancelled';
|
|
315
|
+
target.closedReason = 'cancelled';
|
|
316
|
+
target.updatedAt = at;
|
|
317
|
+
this.prune(state);
|
|
318
|
+
return this.view(target);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
list(filter = {}) {
|
|
322
|
+
const scope = filter.state ?? 'open';
|
|
323
|
+
if (!['open', 'closed', 'all'].includes(scope))
|
|
324
|
+
throw new ReminderError('invalid_request', "state must be 'open', 'closed' or 'all'");
|
|
325
|
+
const limit = filter.limit ?? 50;
|
|
326
|
+
if (typeof limit !== 'number' || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
327
|
+
throw new ReminderError('invalid_request', 'limit must be 1..100');
|
|
328
|
+
if (filter.after !== undefined && typeof filter.after !== 'string')
|
|
329
|
+
throw new ReminderError('invalid_request', 'after must be a reminder id from a previous page');
|
|
330
|
+
const open = (item) => item.state === 'active' || item.state === 'delivered';
|
|
331
|
+
const rows = Object.values(this.state.reminders)
|
|
332
|
+
.filter(item => scope === 'all' || (scope === 'open') === open(item))
|
|
333
|
+
.sort((a, b) => a.dueAt.localeCompare(b.dueAt) || a.id.localeCompare(b.id));
|
|
334
|
+
const start = filter.after ? rows.findIndex(item => item.id === filter.after) + 1 : 0;
|
|
335
|
+
if (filter.after && start === 0)
|
|
336
|
+
throw new ReminderError('invalid_request', 'after does not name a reminder in this listing');
|
|
337
|
+
const page = rows.slice(start, start + limit);
|
|
338
|
+
const more = start + limit < rows.length;
|
|
339
|
+
return {
|
|
340
|
+
reminders: page.map(item => this.view(item)),
|
|
341
|
+
more, ...(more ? { after: page.at(-1).id } : {}),
|
|
342
|
+
uncertain_events: Object.values(this.state.events).filter(event => event.state === 'uncertain').length,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
inspect(eventId) {
|
|
346
|
+
const event = this.event(eventId);
|
|
347
|
+
const record = this.state.reminders[event.reminderId];
|
|
348
|
+
const now = this.now();
|
|
349
|
+
const current = Boolean(record && record.revision === event.revision);
|
|
350
|
+
const eligible = Boolean(record && current && (event.state === 'dispatched' || event.state === 'uncertain')
|
|
351
|
+
&& (record.state === 'delivered' || record.state === 'active'));
|
|
352
|
+
return {
|
|
353
|
+
event: this.eventView(event),
|
|
354
|
+
reminder: record ? this.view(record) : undefined,
|
|
355
|
+
current, eligible,
|
|
356
|
+
late_by_ms: record ? Math.max(0, now - Date.parse(record.dueAt)) : undefined,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
ack(eventId, expectedRevision) {
|
|
360
|
+
const event = this.event(eventId);
|
|
361
|
+
const revision = revisionOf(expectedRevision);
|
|
362
|
+
if (event.revision !== revision)
|
|
363
|
+
throw new ReminderError('stale_revision', `event belongs to revision ${event.revision}`);
|
|
364
|
+
if (event.state === 'acknowledged')
|
|
365
|
+
return { event: this.eventView(event), acknowledged: true, duplicate: true };
|
|
366
|
+
if (event.state !== 'dispatched' && event.state !== 'uncertain')
|
|
367
|
+
throw new ReminderError('not_acknowledgeable', `event is ${event.state}`);
|
|
368
|
+
const record = this.state.reminders[event.reminderId];
|
|
369
|
+
if (!record || record.revision !== event.revision)
|
|
370
|
+
throw new ReminderError('stale_revision', 'the reminder changed after this wake; this event is no longer current');
|
|
371
|
+
return this.commit(state => {
|
|
372
|
+
const target = state.events[event.id];
|
|
373
|
+
const at = iso(this.now());
|
|
374
|
+
target.state = 'acknowledged';
|
|
375
|
+
target.closedAt = at;
|
|
376
|
+
const owner = state.reminders[target.reminderId];
|
|
377
|
+
owner.state = 'acknowledged';
|
|
378
|
+
owner.closedReason = 'acknowledged';
|
|
379
|
+
owner.updatedAt = at;
|
|
380
|
+
this.prune(state);
|
|
381
|
+
return { event: this.eventView(target), acknowledged: true, duplicate: false };
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
// ── dispatch side (runner only) ────────────────────────────────────────────
|
|
385
|
+
/**
|
|
386
|
+
* Earliest instant the dispatcher has work, or undefined when idle.
|
|
387
|
+
* `live` names admissions still owned by a queued prompt in this process:
|
|
388
|
+
* their own completion re-arms the dispatcher, so they are not work here.
|
|
389
|
+
*/
|
|
390
|
+
nextWorkAt(live = new Set()) {
|
|
391
|
+
let next;
|
|
392
|
+
const consider = (value) => { next = next === undefined ? value : Math.min(next, value); };
|
|
393
|
+
for (const record of Object.values(this.state.reminders)) {
|
|
394
|
+
if (record.state !== 'active')
|
|
395
|
+
continue;
|
|
396
|
+
const open = this.openEvent(record);
|
|
397
|
+
if (open && live.has(open.id))
|
|
398
|
+
continue;
|
|
399
|
+
if (!open)
|
|
400
|
+
consider(Date.parse(record.dueAt));
|
|
401
|
+
else if (open.state === 'pending')
|
|
402
|
+
consider(open.retryAt ? Date.parse(open.retryAt) : Date.parse(record.dueAt));
|
|
403
|
+
else if (open.state === 'admitting')
|
|
404
|
+
consider(Date.parse(record.dueAt));
|
|
405
|
+
if (!open || open.state === 'pending' || open.state === 'admitting')
|
|
406
|
+
consider(Date.parse(record.expiresAt));
|
|
407
|
+
}
|
|
408
|
+
return next;
|
|
409
|
+
}
|
|
410
|
+
openEvent(record) {
|
|
411
|
+
return Object.values(this.state.events).find(event => event.reminderId === record.id
|
|
412
|
+
&& event.revision === record.revision && OPEN_EVENT_STATES.has(event.state));
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Expire what is past its window and create a pending event for each due
|
|
416
|
+
* reminder. Returns the events ready to admit now, earliest due first.
|
|
417
|
+
*/
|
|
418
|
+
sweep(live = new Set()) {
|
|
419
|
+
const now = this.now();
|
|
420
|
+
let changed = false;
|
|
421
|
+
const ready = [];
|
|
422
|
+
const next = structuredClone(this.state);
|
|
423
|
+
for (const record of Object.values(next.reminders).sort((a, b) => a.dueAt.localeCompare(b.dueAt))) {
|
|
424
|
+
if (record.state !== 'active')
|
|
425
|
+
continue;
|
|
426
|
+
const open = Object.values(next.events).find(event => event.reminderId === record.id
|
|
427
|
+
&& event.revision === record.revision && OPEN_EVENT_STATES.has(event.state));
|
|
428
|
+
if (open && live.has(open.id))
|
|
429
|
+
continue;
|
|
430
|
+
if (open?.state === 'admitting') {
|
|
431
|
+
// No queued prompt in this process owns it (a write that would have
|
|
432
|
+
// returned it to pending failed). The release guard never committed
|
|
433
|
+
// a dispatch, so it is still undispatched: the same move as restart
|
|
434
|
+
// recovery, made here so it can neither strand nor spin.
|
|
435
|
+
open.state = 'pending';
|
|
436
|
+
delete open.promptId;
|
|
437
|
+
delete open.retryAt;
|
|
438
|
+
changed = true;
|
|
439
|
+
}
|
|
440
|
+
if (now >= Date.parse(record.expiresAt) && (!open || open.state === 'pending')) {
|
|
441
|
+
if (open) {
|
|
442
|
+
open.state = 'expired';
|
|
443
|
+
open.closedAt = iso(now);
|
|
444
|
+
}
|
|
445
|
+
record.state = 'expired';
|
|
446
|
+
record.closedReason = 'late_window_elapsed';
|
|
447
|
+
record.updatedAt = iso(now);
|
|
448
|
+
changed = true;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (now < Date.parse(record.dueAt))
|
|
452
|
+
continue;
|
|
453
|
+
if (!open) {
|
|
454
|
+
const event = {
|
|
455
|
+
id: `rev_${randomUUID()}`, reminderId: record.id, revision: record.revision,
|
|
456
|
+
state: 'pending', createdAt: iso(now), attempts: 0,
|
|
457
|
+
};
|
|
458
|
+
next.events[event.id] = event;
|
|
459
|
+
changed = true;
|
|
460
|
+
ready.push(event);
|
|
461
|
+
}
|
|
462
|
+
else if (open.state === 'pending' && (!open.retryAt || Date.parse(open.retryAt) <= now)) {
|
|
463
|
+
ready.push(open);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (changed) {
|
|
467
|
+
this.prune(next);
|
|
468
|
+
const before = this.state;
|
|
469
|
+
this.state = next;
|
|
470
|
+
try {
|
|
471
|
+
this.persist();
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
this.state = before;
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return ready.map(event => structuredClone(this.state.events[event.id]));
|
|
479
|
+
}
|
|
480
|
+
/** Durable claim made before the prompt is queued. False: do not queue. */
|
|
481
|
+
markAdmitting(eventId) {
|
|
482
|
+
const event = this.state.events[eventId];
|
|
483
|
+
const record = event && this.state.reminders[event.reminderId];
|
|
484
|
+
const now = this.now();
|
|
485
|
+
if (!event || event.state !== 'pending' || !record || record.state !== 'active'
|
|
486
|
+
|| record.revision !== event.revision || now < Date.parse(record.dueAt)
|
|
487
|
+
|| now >= Date.parse(record.expiresAt))
|
|
488
|
+
return false;
|
|
489
|
+
return this.commit(() => {
|
|
490
|
+
event.state = 'admitting';
|
|
491
|
+
event.attempts++;
|
|
492
|
+
event.admittedAt = iso(this.now());
|
|
493
|
+
delete event.retryAt;
|
|
494
|
+
return true;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/** Record the session's queue receipt; never moves an event backwards. */
|
|
498
|
+
recordReceipt(eventId, promptId) {
|
|
499
|
+
const event = this.state.events[eventId];
|
|
500
|
+
if (!event || (event.state !== 'admitting' && event.state !== 'dispatched') || event.promptId)
|
|
501
|
+
return;
|
|
502
|
+
this.commit(() => { event.promptId = promptId; });
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The release guard: called synchronously by the session at the moment the
|
|
506
|
+
* prompt would be handed to the model. It returns true only after
|
|
507
|
+
* `dispatched` is durably on disk; any doubt refuses the dispatch.
|
|
508
|
+
*/
|
|
509
|
+
claimDispatch(eventId, stopping) {
|
|
510
|
+
const event = this.state.events[eventId];
|
|
511
|
+
const record = event && this.state.reminders[event.reminderId];
|
|
512
|
+
if (!event || event.state !== 'admitting')
|
|
513
|
+
return { dispatch: false, reason: 'event_not_admitting' };
|
|
514
|
+
if (stopping)
|
|
515
|
+
return { dispatch: false, reason: 'stopping' };
|
|
516
|
+
const at = this.now();
|
|
517
|
+
const close = (state, reason) => {
|
|
518
|
+
try {
|
|
519
|
+
this.commit(() => { event.state = state; event.reason = reason; event.closedAt = iso(at); });
|
|
520
|
+
}
|
|
521
|
+
catch { /* left admitting: recovery returns it to pending, still undispatched */ }
|
|
522
|
+
return { dispatch: false, reason };
|
|
523
|
+
};
|
|
524
|
+
if (!record || record.revision !== event.revision || record.state !== 'active')
|
|
525
|
+
return close('invalidated', 'stale_revision');
|
|
526
|
+
// The wall clock can step back after admission. Not due yet means not yet:
|
|
527
|
+
// the event stays undispatched and is admitted again once it is due.
|
|
528
|
+
if (at < Date.parse(record.dueAt))
|
|
529
|
+
return { dispatch: false, reason: 'not_due' };
|
|
530
|
+
if (at >= Date.parse(record.expiresAt)) {
|
|
531
|
+
try {
|
|
532
|
+
this.commit(() => {
|
|
533
|
+
event.state = 'expired';
|
|
534
|
+
event.reason = 'late_window_elapsed';
|
|
535
|
+
event.closedAt = iso(at);
|
|
536
|
+
record.state = 'expired';
|
|
537
|
+
record.closedReason = 'late_window_elapsed';
|
|
538
|
+
record.updatedAt = iso(at);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
catch { /* undispatched either way */ }
|
|
542
|
+
return { dispatch: false, reason: 'expired' };
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
this.commit(() => {
|
|
546
|
+
event.state = 'dispatched';
|
|
547
|
+
event.dispatchedAt = iso(at);
|
|
548
|
+
record.state = 'delivered';
|
|
549
|
+
record.updatedAt = iso(at);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
return { dispatch: false, reason: 'dispatch_commit_failed' };
|
|
554
|
+
}
|
|
555
|
+
return { dispatch: true };
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Whether this undispatched event may still ask to interrupt a busy turn: its
|
|
559
|
+
* reminder is urgent and current, and no interruption was ever requested
|
|
560
|
+
* for it (in this process or before a restart).
|
|
561
|
+
*/
|
|
562
|
+
mayInterrupt(eventId) {
|
|
563
|
+
const event = this.state.events[eventId];
|
|
564
|
+
const record = event && this.state.reminders[event.reminderId];
|
|
565
|
+
return Boolean(event && record && record.interrupt === true && !event.interrupt
|
|
566
|
+
&& (event.state === 'pending' || event.state === 'admitting')
|
|
567
|
+
&& record.state === 'active' && record.revision === event.revision);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Durable mark made BEFORE the session is asked to cancel. False: do not
|
|
571
|
+
* interrupt. Once written it is never cleared, which is what bounds every
|
|
572
|
+
* urgent event to one interruption.
|
|
573
|
+
*/
|
|
574
|
+
markInterruptRequested(eventId, target) {
|
|
575
|
+
if (!this.mayInterrupt(eventId) || this.state.events[eventId].state !== 'admitting')
|
|
576
|
+
return false;
|
|
577
|
+
if (!target.source || !target.promptId || !target.generation)
|
|
578
|
+
return false;
|
|
579
|
+
const event = this.state.events[eventId];
|
|
580
|
+
try {
|
|
581
|
+
this.commit(() => {
|
|
582
|
+
event.interrupt = {
|
|
583
|
+
requestedAt: iso(this.now()), outcome: 'requested',
|
|
584
|
+
target: {
|
|
585
|
+
source: target.source, promptId: target.promptId, generation: target.generation,
|
|
586
|
+
...(target.sessionId ? { sessionId: target.sessionId } : {}),
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
delete event.interruptDeferred;
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Record why an urgent event did not interrupt this time. Written only when
|
|
599
|
+
* the reason changes, so a long busy stretch costs one write, not one per poll.
|
|
600
|
+
*/
|
|
601
|
+
recordInterruptDeferred(eventId, reason) {
|
|
602
|
+
const event = this.state.events[eventId];
|
|
603
|
+
if (!event || event.interrupt || event.interruptDeferred === reason)
|
|
604
|
+
return;
|
|
605
|
+
try {
|
|
606
|
+
this.commit(() => { event.interruptDeferred = reason; });
|
|
607
|
+
}
|
|
608
|
+
catch { /* diagnostic only */ }
|
|
609
|
+
}
|
|
610
|
+
/** How the requested interruption ended. Diagnostic; never moves the event. */
|
|
611
|
+
recordInterruptOutcome(eventId, outcome, reason) {
|
|
612
|
+
const event = this.state.events[eventId];
|
|
613
|
+
if (!event?.interrupt || event.interrupt.outcome !== 'requested')
|
|
614
|
+
return;
|
|
615
|
+
try {
|
|
616
|
+
this.commit(() => {
|
|
617
|
+
event.interrupt = {
|
|
618
|
+
...event.interrupt, outcome, settledAt: iso(this.now()), ...(reason ? { reason } : {}),
|
|
619
|
+
};
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
catch { /* stays `requested`: still never re-requested, still not delivered */ }
|
|
623
|
+
}
|
|
624
|
+
/** Still the current, undispatched claim this process made? Re-checked after every await. */
|
|
625
|
+
stillAdmitting(eventId) {
|
|
626
|
+
const event = this.state.events[eventId];
|
|
627
|
+
const record = event && this.state.reminders[event.reminderId];
|
|
628
|
+
return Boolean(event && record && event.state === 'admitting' && record.state === 'active'
|
|
629
|
+
&& record.revision === event.revision);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* An admission that ended without the release guard dispatching it (queue
|
|
633
|
+
* refused, session died, release refused while stopping) goes back to
|
|
634
|
+
* pending. Anything already past `admitting` is left exactly as it is.
|
|
635
|
+
*/
|
|
636
|
+
releaseUndispatched(eventId, retryAt) {
|
|
637
|
+
const event = this.state.events[eventId];
|
|
638
|
+
if (!event || event.state !== 'admitting')
|
|
639
|
+
return;
|
|
640
|
+
try {
|
|
641
|
+
this.commit(() => {
|
|
642
|
+
event.state = 'pending';
|
|
643
|
+
event.retryAt = iso(retryAt);
|
|
644
|
+
delete event.promptId;
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
catch { /* stays admitting; restart recovery makes the same move */ }
|
|
648
|
+
}
|
|
649
|
+
eventSummary(eventId) {
|
|
650
|
+
const event = this.state.events[eventId];
|
|
651
|
+
const reminder = event && this.state.reminders[event.reminderId];
|
|
652
|
+
return event && reminder ? structuredClone({ event, reminder }) : undefined;
|
|
653
|
+
}
|
|
654
|
+
supersedeEvents(state, reminderId, reason, at) {
|
|
655
|
+
for (const event of Object.values(state.events))
|
|
656
|
+
if (event.reminderId === reminderId && OPEN_EVENT_STATES.has(event.state)) {
|
|
657
|
+
event.state = 'invalidated';
|
|
658
|
+
event.reason = reason;
|
|
659
|
+
event.closedAt = at;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/** Bound retained history; open reminders and their events are never dropped. */
|
|
663
|
+
prune(state) {
|
|
664
|
+
const closed = Object.values(state.reminders)
|
|
665
|
+
.filter(item => item.state !== 'active' && item.state !== 'delivered'
|
|
666
|
+
&& !Object.values(state.events).some(event => event.reminderId === item.id && event.state === 'uncertain'))
|
|
667
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
668
|
+
for (const item of closed.slice(MAX_RETAINED_CLOSED)) {
|
|
669
|
+
delete state.reminders[item.id];
|
|
670
|
+
for (const [id, event] of Object.entries(state.events))
|
|
671
|
+
if (event.reminderId === item.id)
|
|
672
|
+
delete state.events[id];
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
view(record) {
|
|
676
|
+
const event = Object.values(this.state.events)
|
|
677
|
+
.filter(item => item.reminderId === record.id && item.revision === record.revision)
|
|
678
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
|
|
679
|
+
return {
|
|
680
|
+
id: record.id, request_key: record.requestKey, revision: record.revision, state: record.state,
|
|
681
|
+
title: record.title, note: record.note,
|
|
682
|
+
local_time: record.localTime, time_zone: record.timeZone,
|
|
683
|
+
...(record.fold === undefined ? {} : { fold: record.fold }),
|
|
684
|
+
due_at: record.dueAt, late_window_seconds: record.lateWindowSeconds, interrupt: record.interrupt === true,
|
|
685
|
+
expires_at: record.expiresAt,
|
|
686
|
+
created_at: record.createdAt, updated_at: record.updatedAt,
|
|
687
|
+
...(record.closedReason ? { closed_reason: record.closedReason } : {}),
|
|
688
|
+
...(event ? { event: {
|
|
689
|
+
id: event.id, state: event.state, revision: event.revision,
|
|
690
|
+
...(event.interrupt ? { interrupt_state: event.interrupt.outcome } : {}),
|
|
691
|
+
} } : {}),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
eventView(event) {
|
|
695
|
+
return {
|
|
696
|
+
id: event.id, reminder_id: event.reminderId, revision: event.revision, state: event.state,
|
|
697
|
+
created_at: event.createdAt,
|
|
698
|
+
...(event.dispatchedAt ? { dispatched_at: event.dispatchedAt } : {}),
|
|
699
|
+
...(event.closedAt ? { closed_at: event.closedAt } : {}),
|
|
700
|
+
...(event.reason ? { reason: event.reason } : {}),
|
|
701
|
+
...(event.interruptDeferred ? { interrupt_deferred: event.interruptDeferred } : {}),
|
|
702
|
+
...(event.interrupt ? { interrupt: {
|
|
703
|
+
state: event.interrupt.outcome, requested_at: event.interrupt.requestedAt,
|
|
704
|
+
target_source: event.interrupt.target.source,
|
|
705
|
+
target: {
|
|
706
|
+
source: event.interrupt.target.source, prompt_id: event.interrupt.target.promptId,
|
|
707
|
+
generation: event.interrupt.target.generation,
|
|
708
|
+
...(event.interrupt.target.sessionId ? { session_id: event.interrupt.target.sessionId } : {}),
|
|
709
|
+
},
|
|
710
|
+
...(event.interrupt.settledAt ? { settled_at: event.interrupt.settledAt } : {}),
|
|
711
|
+
...(event.interrupt.reason ? { reason: event.interrupt.reason } : {}),
|
|
712
|
+
} } : {}),
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function sameOwner(a, b) {
|
|
717
|
+
if (a.role !== b.role || a.lifetime !== b.lifetime || a.cid.toUpperCase() !== b.cid.toUpperCase())
|
|
718
|
+
return false;
|
|
719
|
+
return a.lifetime === 'permanent' || (a.instance === b.instance && a.action === b.action);
|
|
720
|
+
}
|
|
721
|
+
function privateFile(path) {
|
|
722
|
+
const stat = lstatSync(path);
|
|
723
|
+
const uid = process.getuid?.();
|
|
724
|
+
return stat.isFile() && !stat.isSymbolicLink() && (stat.mode & 0o077) === 0
|
|
725
|
+
&& (uid === undefined || stat.uid === uid);
|
|
726
|
+
}
|
|
727
|
+
function validFile(value) {
|
|
728
|
+
if (!value || typeof value !== 'object')
|
|
729
|
+
return false;
|
|
730
|
+
const file = value;
|
|
731
|
+
const owner = file.owner;
|
|
732
|
+
if (file.version !== 1 || !owner || typeof owner !== 'object'
|
|
733
|
+
|| !['role', 'instance', 'cid', 'action'].every(key => typeof owner[key] === 'string')
|
|
734
|
+
|| (owner.lifetime !== 'permanent' && owner.lifetime !== 'temporary')
|
|
735
|
+
|| !file.reminders || typeof file.reminders !== 'object' || !file.events || typeof file.events !== 'object')
|
|
736
|
+
return false;
|
|
737
|
+
const reminderStates = new Set(['active', 'delivered', 'acknowledged', 'cancelled', 'expired']);
|
|
738
|
+
const eventStates = new Set(['pending', 'admitting', 'dispatched', 'uncertain', 'acknowledged', 'invalidated', 'expired']);
|
|
739
|
+
return Object.entries(file.reminders).every(([id, item]) => item && item.id === id
|
|
740
|
+
&& Number.isSafeInteger(item.revision) && item.revision >= 1 && reminderStates.has(item.state)
|
|
741
|
+
&& typeof item.requestKey === 'string' && typeof item.title === 'string' && typeof item.note === 'string'
|
|
742
|
+
&& Number.isFinite(Date.parse(item.dueAt)) && Number.isFinite(Date.parse(item.expiresAt))
|
|
743
|
+
&& (item.interrupt === undefined || item.interrupt === true))
|
|
744
|
+
&& Object.entries(file.events).every(([id, item]) => item && item.id === id
|
|
745
|
+
&& typeof item.reminderId === 'string' && Number.isSafeInteger(item.revision) && eventStates.has(item.state)
|
|
746
|
+
&& Number.isSafeInteger(item.attempts)
|
|
747
|
+
&& (item.interrupt === undefined || (typeof item.interrupt === 'object' && item.interrupt !== null
|
|
748
|
+
&& typeof item.interrupt.requestedAt === 'string' && typeof item.interrupt.target?.source === 'string'
|
|
749
|
+
&& typeof item.interrupt.target.promptId === 'string' && typeof item.interrupt.target.generation === 'string'
|
|
750
|
+
&& (item.interrupt.target.sessionId === undefined || typeof item.interrupt.target.sessionId === 'string')
|
|
751
|
+
&& INTERRUPT_OUTCOMES.has(item.interrupt.outcome))));
|
|
752
|
+
}
|
|
753
|
+
/** Remove a retired agent's reminder store and any quarantined copies. */
|
|
754
|
+
export function removeReminderState(stateDir) {
|
|
755
|
+
rmSync(join(stateDir, SELF_REMINDERS_FILE), { force: true });
|
|
756
|
+
try {
|
|
757
|
+
for (const name of readdirSync(stateDir))
|
|
758
|
+
if (name.startsWith(`${SELF_REMINDERS_FILE}.`) || name.startsWith(`.${SELF_REMINDERS_FILE}.`))
|
|
759
|
+
rmSync(join(stateDir, name), { force: true });
|
|
760
|
+
}
|
|
761
|
+
catch { /* directory already gone */ }
|
|
762
|
+
}
|