@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,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dispatcher never sleeps longer than this while reminders are open, so a
|
|
3
|
+
* wall-clock jump (host suspend, NTP step) is noticed within a minute. With
|
|
4
|
+
* nothing open it arms no timer at all: an idle agent costs nothing.
|
|
5
|
+
*/
|
|
6
|
+
export const REMINDER_POLL_CEILING_MS = 60_000;
|
|
7
|
+
/** Busy role: re-check at the next idle boundary within these bounds. */
|
|
8
|
+
const BUSY_RETRY_MIN_MS = 2_000;
|
|
9
|
+
const BUSY_RETRY_MAX_MS = 30_000;
|
|
10
|
+
/** Admission refused before reaching the model: bounded backoff, inside the late window. */
|
|
11
|
+
const UNAVAILABLE_RETRY_MS = [5_000, 15_000, 30_000, 60_000];
|
|
12
|
+
/**
|
|
13
|
+
* Turns due self-reminders into wakes for the agent that owns them.
|
|
14
|
+
*
|
|
15
|
+
* Runs inside that agent's runner, beside the role's turn arbiter. It starts
|
|
16
|
+
* only after the role's startup turn has succeeded and stops before the runner
|
|
17
|
+
* releases its runtime binding. Admission is idle-only by default: a wake
|
|
18
|
+
* never interrupts or queues behind work, and the session's release guard
|
|
19
|
+
* makes the durable `dispatched` claim at the moment the prompt reaches the
|
|
20
|
+
* model. A reminder created with `interrupt: true` is considered first and may
|
|
21
|
+
* ask an interruptible busy turn to stop once (`tryUrgentReminder`); the
|
|
22
|
+
* release guard is still the only path to the model.
|
|
23
|
+
*/
|
|
24
|
+
export class ReminderManager {
|
|
25
|
+
role;
|
|
26
|
+
store;
|
|
27
|
+
admitter;
|
|
28
|
+
deps;
|
|
29
|
+
timer;
|
|
30
|
+
started = false;
|
|
31
|
+
stopping = false;
|
|
32
|
+
polling = false;
|
|
33
|
+
busyStreak = 0;
|
|
34
|
+
/** Consecutive re-arms with work already overdue; drives the spin backoff. */
|
|
35
|
+
overdueStreak = 0;
|
|
36
|
+
inFlight = new Set();
|
|
37
|
+
constructor(role, store, admitter, deps) {
|
|
38
|
+
this.role = role;
|
|
39
|
+
this.store = store;
|
|
40
|
+
this.admitter = admitter;
|
|
41
|
+
this.deps = deps;
|
|
42
|
+
}
|
|
43
|
+
start() {
|
|
44
|
+
if (this.started || this.stopping)
|
|
45
|
+
return;
|
|
46
|
+
this.started = true;
|
|
47
|
+
this.schedule(0);
|
|
48
|
+
}
|
|
49
|
+
/** Synchronous: after this returns no timer is armed and every release guard refuses. */
|
|
50
|
+
stop() {
|
|
51
|
+
this.stopping = true;
|
|
52
|
+
if (this.timer !== undefined)
|
|
53
|
+
this.deps.clearTimer(this.timer);
|
|
54
|
+
this.timer = undefined;
|
|
55
|
+
}
|
|
56
|
+
get isStopping() { return this.stopping; }
|
|
57
|
+
changed() {
|
|
58
|
+
if (this.started && !this.stopping && !this.polling)
|
|
59
|
+
this.schedule();
|
|
60
|
+
}
|
|
61
|
+
/** Public fake-clock seam; the timer calls the same transition. */
|
|
62
|
+
async poll() {
|
|
63
|
+
if (!this.started || this.stopping || this.polling)
|
|
64
|
+
return;
|
|
65
|
+
this.polling = true;
|
|
66
|
+
let delay;
|
|
67
|
+
try {
|
|
68
|
+
// Urgent events that may still interrupt come first, so neither a busy
|
|
69
|
+
// ordinary event nor an urgent one whose single interruption is spent
|
|
70
|
+
// (now idle-only) can hide them. Earliest-due order is kept within each
|
|
71
|
+
// group; ordinary and spent events keep the idle-only default.
|
|
72
|
+
const ready = this.store.sweep(this.inFlight);
|
|
73
|
+
const rank = (id) => {
|
|
74
|
+
const summary = this.store.eventSummary(id);
|
|
75
|
+
return summary?.reminder.interrupt === true && !summary.event.interrupt ? 0 : 1;
|
|
76
|
+
};
|
|
77
|
+
const ranks = new Map(ready.map(event => [event.id, rank(event.id)]));
|
|
78
|
+
ready.sort((a, b) => ranks.get(a.id) - ranks.get(b.id));
|
|
79
|
+
for (const event of ready) {
|
|
80
|
+
if (this.stopping)
|
|
81
|
+
return;
|
|
82
|
+
const outcome = await this.admit(event);
|
|
83
|
+
if (outcome === 'busy') {
|
|
84
|
+
this.busyStreak++;
|
|
85
|
+
delay = Math.min(BUSY_RETRY_MAX_MS, BUSY_RETRY_MIN_MS * 2 ** Math.min(this.busyStreak - 1, 4));
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
this.busyStreak = 0;
|
|
89
|
+
// One wake per turn boundary: the next due reminder waits for the
|
|
90
|
+
// following poll, by which time this one's turn has claimed the role.
|
|
91
|
+
if (outcome === 'admitted') {
|
|
92
|
+
delay = 0;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
this.deps.log(`[${this.role}] self-reminder dispatch failed: ${error?.name ?? 'Error'}; retrying`);
|
|
99
|
+
delay = UNAVAILABLE_RETRY_MS[0];
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
this.polling = false;
|
|
103
|
+
}
|
|
104
|
+
this.schedule(delay);
|
|
105
|
+
}
|
|
106
|
+
async admit(event) {
|
|
107
|
+
const summary = this.store.eventSummary(event.id);
|
|
108
|
+
if (!summary)
|
|
109
|
+
return 'skipped';
|
|
110
|
+
const { reminder } = summary;
|
|
111
|
+
const origin = {
|
|
112
|
+
kind: 'self-reminder', eventId: event.id, reminderId: reminder.id, revision: event.revision,
|
|
113
|
+
};
|
|
114
|
+
const text = () => {
|
|
115
|
+
const current = this.store.eventSummary(event.id)?.event ?? event;
|
|
116
|
+
return wakePrompt(current, reminder, this.deps.now(), event.attempts > 0);
|
|
117
|
+
};
|
|
118
|
+
const beforeQueue = () => !this.stopping && this.store.markAdmitting(event.id);
|
|
119
|
+
const releaseGuard = () => this.guard(event.id);
|
|
120
|
+
this.inFlight.add(event.id);
|
|
121
|
+
let attempt;
|
|
122
|
+
try {
|
|
123
|
+
attempt = reminder.interrupt === true && this.admitter.tryUrgentReminder
|
|
124
|
+
? await this.admitter.tryUrgentReminder(origin, {
|
|
125
|
+
beforeQueue, releaseGuard, text,
|
|
126
|
+
mayInterrupt: () => !this.stopping && this.store.mayInterrupt(event.id),
|
|
127
|
+
markRequested: target => !this.stopping && this.store.markInterruptRequested(event.id, target),
|
|
128
|
+
recordOutcome: report => {
|
|
129
|
+
this.store.recordInterruptOutcome(event.id, report.outcome, report.reason);
|
|
130
|
+
this.deps.log(`[${this.role}] self-reminder ${event.id.slice(0, 12)} interrupt ${report.outcome}`
|
|
131
|
+
+ ` (${report.target.source} turn)`);
|
|
132
|
+
},
|
|
133
|
+
deferred: reason => this.store.recordInterruptDeferred(event.id, reason),
|
|
134
|
+
stillAdmitting: () => !this.stopping && this.store.stillAdmitting(event.id),
|
|
135
|
+
})
|
|
136
|
+
: await this.admitter.tryReminder(text(), origin, beforeQueue, releaseGuard);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
this.inFlight.delete(event.id);
|
|
140
|
+
this.store.releaseUndispatched(event.id, this.retryAt(event));
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (attempt.state === 'busy') {
|
|
144
|
+
this.inFlight.delete(event.id);
|
|
145
|
+
// An urgent attempt may have claimed the event before finding the role
|
|
146
|
+
// busy again (owner input won the boundary after an interruption).
|
|
147
|
+
this.store.releaseUndispatched(event.id, this.deps.now());
|
|
148
|
+
return 'busy';
|
|
149
|
+
}
|
|
150
|
+
if (attempt.state === 'unavailable') {
|
|
151
|
+
this.inFlight.delete(event.id);
|
|
152
|
+
this.store.releaseUndispatched(event.id, this.retryAt(event));
|
|
153
|
+
this.deps.log(`[${this.role}] self-reminder ${event.id.slice(0, 12)} not admitted; will retry within its window`);
|
|
154
|
+
return 'skipped';
|
|
155
|
+
}
|
|
156
|
+
// Own the completion before anything else can throw: the queued prompt
|
|
157
|
+
// ends this admission whatever happens to the receipt write below.
|
|
158
|
+
const settled = () => {
|
|
159
|
+
this.inFlight.delete(event.id);
|
|
160
|
+
// Settled without the guard dispatching it (offline session, refused at
|
|
161
|
+
// release while stopping): still undispatched, so it may be retried.
|
|
162
|
+
// A failed write leaves it admitting with no owner; sweep recovers that.
|
|
163
|
+
try {
|
|
164
|
+
this.store.releaseUndispatched(event.id, this.retryAt(event));
|
|
165
|
+
}
|
|
166
|
+
catch { /* recovered by sweep */ }
|
|
167
|
+
this.changed();
|
|
168
|
+
};
|
|
169
|
+
void attempt.queued.completion.then(settled, settled);
|
|
170
|
+
// The receipt is diagnostic only; dispatch state is the release guard's.
|
|
171
|
+
try {
|
|
172
|
+
this.store.recordReceipt(event.id, attempt.queued.promptId);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
this.deps.log(`[${this.role}] self-reminder ${event.id.slice(0, 12)} receipt not recorded`);
|
|
176
|
+
}
|
|
177
|
+
this.deps.log(`[${this.role}] self-reminder ${event.id.slice(0, 12)} admitted`);
|
|
178
|
+
return 'admitted';
|
|
179
|
+
}
|
|
180
|
+
guard(eventId) {
|
|
181
|
+
const result = this.store.claimDispatch(eventId, this.stopping);
|
|
182
|
+
if (!result.dispatch)
|
|
183
|
+
this.deps.log(`[${this.role}] self-reminder ${eventId.slice(0, 12)} not dispatched at release (${result.reason})`);
|
|
184
|
+
return result.dispatch;
|
|
185
|
+
}
|
|
186
|
+
retryAt(event) {
|
|
187
|
+
const step = UNAVAILABLE_RETRY_MS[Math.min(event.attempts, UNAVAILABLE_RETRY_MS.length - 1)];
|
|
188
|
+
return this.deps.now() + step;
|
|
189
|
+
}
|
|
190
|
+
schedule(delayMs) {
|
|
191
|
+
if (this.timer !== undefined)
|
|
192
|
+
this.deps.clearTimer(this.timer);
|
|
193
|
+
this.timer = undefined;
|
|
194
|
+
if (this.stopping)
|
|
195
|
+
return;
|
|
196
|
+
let delay = delayMs;
|
|
197
|
+
if (delay === undefined) {
|
|
198
|
+
const next = this.store.nextWorkAt(this.inFlight);
|
|
199
|
+
if (next === undefined) {
|
|
200
|
+
this.overdueStreak = 0;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
delay = Math.max(0, next - this.deps.now());
|
|
204
|
+
// Work that is already overdue after a full poll means the poll could
|
|
205
|
+
// not move it (storage refusing writes, say). Back off instead of
|
|
206
|
+
// re-arming at once; any real progress resets the streak.
|
|
207
|
+
if (delay === 0) {
|
|
208
|
+
delay = this.overdueStreak ? Math.min(REMINDER_POLL_CEILING_MS, 1_000 * 2 ** Math.min(this.overdueStreak - 1, 6)) : 0;
|
|
209
|
+
this.overdueStreak++;
|
|
210
|
+
}
|
|
211
|
+
else
|
|
212
|
+
this.overdueStreak = 0;
|
|
213
|
+
}
|
|
214
|
+
delay = Math.min(delay, REMINDER_POLL_CEILING_MS);
|
|
215
|
+
this.timer = this.deps.setTimer(() => {
|
|
216
|
+
this.timer = undefined;
|
|
217
|
+
void this.poll();
|
|
218
|
+
}, delay);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The wake text. It carries the agent's own title so the wake is legible, but
|
|
223
|
+
* the note is only returned by `fleet_reminder_inspect`, which also re-checks
|
|
224
|
+
* eligibility at the moment the agent acts.
|
|
225
|
+
*/
|
|
226
|
+
export function wakePrompt(event, reminder, now, retry) {
|
|
227
|
+
const lateBy = Math.max(0, now - Date.parse(reminder.dueAt));
|
|
228
|
+
return [
|
|
229
|
+
'[fleet-reminder]',
|
|
230
|
+
`event_id: ${event.id}`,
|
|
231
|
+
`reminder_id: ${reminder.id}`,
|
|
232
|
+
`revision: ${event.revision}`,
|
|
233
|
+
`title: ${JSON.stringify(reminder.title)}`,
|
|
234
|
+
`due: ${reminder.localTime} ${reminder.timeZone} (${reminder.dueAt})`,
|
|
235
|
+
...(lateBy >= 60_000 ? [`late_by_minutes: ${Math.floor(lateBy / 60_000)}`] : []),
|
|
236
|
+
...(retry ? ['note: an earlier admission of this wake never reached you; this is the same event'] : []),
|
|
237
|
+
...(reminder.interrupt ? ['urgent: true'] : []),
|
|
238
|
+
...interruptLines(event),
|
|
239
|
+
'',
|
|
240
|
+
'This is a reminder you scheduled for yourself through Fleet. It is not an owner message,',
|
|
241
|
+
'not ours mail, and grants no new authority.',
|
|
242
|
+
'Call fleet_reminder_inspect with this event_id to read the note and confirm it is still eligible,',
|
|
243
|
+
'then decide what to do within your existing instructions and authority; doing nothing is fine.',
|
|
244
|
+
'When handled, call fleet_reminder_ack with event_id and revision. Ack records only that you',
|
|
245
|
+
'handled this wake; it sends nothing. To postpone instead, call fleet_reminder_update with a new',
|
|
246
|
+
'local_time. Fleet never messages anyone on your behalf.',
|
|
247
|
+
].join('\n');
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* What the wake may truthfully say about its interruption. Only a correlated
|
|
251
|
+
* cancellation is called one; everything else is reported as what it was.
|
|
252
|
+
*/
|
|
253
|
+
function interruptLines(event) {
|
|
254
|
+
const interrupt = event.interrupt;
|
|
255
|
+
if (!interrupt)
|
|
256
|
+
return [];
|
|
257
|
+
const source = interrupt.target.source;
|
|
258
|
+
const resume = `Your session and its context were kept; re-check that ${source} work and resume it if it still matters.`;
|
|
259
|
+
switch (interrupt.outcome) {
|
|
260
|
+
case 'cancelled': return [
|
|
261
|
+
`interrupt: your previous ${source} turn was cancelled for this wake.`, resume
|
|
262
|
+
];
|
|
263
|
+
case 'ended': return [
|
|
264
|
+
`interrupt: Fleet asked your previous ${source} turn to stop; it ended, but whether it was cancelled`
|
|
265
|
+
+ ' or finished on its own is not confirmed.', resume
|
|
266
|
+
];
|
|
267
|
+
case 'forced': return [
|
|
268
|
+
`interrupt: your previous ${source} turn ignored the stop request and the adapter was restarted;`
|
|
269
|
+
+ ' this wake arrived later than its urgent time. Check what that turn left unfinished.'
|
|
270
|
+
];
|
|
271
|
+
case 'failed':
|
|
272
|
+
case 'unknown':
|
|
273
|
+
case 'requested': return [
|
|
274
|
+
`interrupt: Fleet asked your previous ${source} turn to stop but the outcome is unknown (${interrupt.outcome});`
|
|
275
|
+
+ ' this wake was delivered at the next idle boundary instead.'
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ReminderManager, type ReminderAdmitter, type ReminderManagerDeps } from './manager.js';
|
|
2
|
+
import type { ReminderToolBackend } from './tools.js';
|
|
3
|
+
export interface RoleReminderBinding {
|
|
4
|
+
role: string;
|
|
5
|
+
stateDir: string;
|
|
6
|
+
/** The managed runtime's own record; nothing here comes from the agent. */
|
|
7
|
+
runtime: {
|
|
8
|
+
instance?: string;
|
|
9
|
+
cid?: string;
|
|
10
|
+
action?: string;
|
|
11
|
+
lifetime?: 'permanent' | 'temporary';
|
|
12
|
+
};
|
|
13
|
+
/** Whether this session backend accepts supervisor-submitted prompts. */
|
|
14
|
+
promptInput: boolean;
|
|
15
|
+
admitter: ReminderAdmitter;
|
|
16
|
+
service: {
|
|
17
|
+
setReminders?(backend: ReminderToolBackend | undefined): void;
|
|
18
|
+
};
|
|
19
|
+
deps: ReminderManagerDeps;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Bind self-reminders for one runner attempt: load the store for exactly this
|
|
23
|
+
* runtime instance and identity, expose it behind the agent's own MCP tools,
|
|
24
|
+
* and return the (not yet started) dispatcher. Any doubt leaves reminders
|
|
25
|
+
* unavailable rather than guessing an owner.
|
|
26
|
+
*/
|
|
27
|
+
export declare function bindRoleReminders(binding: RoleReminderBinding): ReminderManager | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ReminderManager } from './manager.js';
|
|
2
|
+
import { ReminderStore } from './store.js';
|
|
3
|
+
/**
|
|
4
|
+
* Bind self-reminders for one runner attempt: load the store for exactly this
|
|
5
|
+
* runtime instance and identity, expose it behind the agent's own MCP tools,
|
|
6
|
+
* and return the (not yet started) dispatcher. Any doubt leaves reminders
|
|
7
|
+
* unavailable rather than guessing an owner.
|
|
8
|
+
*/
|
|
9
|
+
export function bindRoleReminders(binding) {
|
|
10
|
+
const { role, runtime, deps } = binding;
|
|
11
|
+
if (!binding.promptInput || !binding.service.setReminders)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (!runtime.instance || !runtime.action || !/^[A-Fa-f0-9]{64}$/.test(runtime.cid ?? '')
|
|
14
|
+
|| (runtime.lifetime !== 'permanent' && runtime.lifetime !== 'temporary')) {
|
|
15
|
+
deps.log(`[${role}] self-reminders unavailable: managed runtime identity is not established`);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const owner = {
|
|
19
|
+
role, lifetime: runtime.lifetime, instance: runtime.instance, cid: runtime.cid, action: runtime.action,
|
|
20
|
+
};
|
|
21
|
+
let store;
|
|
22
|
+
try {
|
|
23
|
+
store = new ReminderStore(binding.stateDir, owner, deps.now, deps.log);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
deps.log(`[${role}] self-reminders unavailable: state could not be opened (${error?.name ?? 'Error'})`);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const manager = new ReminderManager(role, store, binding.admitter, deps);
|
|
30
|
+
binding.service.setReminders({ store, changed: () => manager.changed() });
|
|
31
|
+
return manager;
|
|
32
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wall-clock schedule resolution for self-reminders.
|
|
3
|
+
*
|
|
4
|
+
* A reminder is stated the way a person states it — a calendar date, a time of
|
|
5
|
+
* day and the IANA zone it is meant in — and stored as the one UTC instant that
|
|
6
|
+
* statement denotes. Nothing is inferred from the host's own zone: a time with
|
|
7
|
+
* an offset, a missing date or an unknown zone is refused rather than guessed.
|
|
8
|
+
*
|
|
9
|
+
* Daylight-saving transitions are the two places a wall time is not one
|
|
10
|
+
* instant. A time skipped by a spring-forward gap does not exist and is
|
|
11
|
+
* refused. A time repeated by a fall-back overlap exists twice, and the caller
|
|
12
|
+
* must say which with `fold` (0 = the first occurrence, 1 = the second), the
|
|
13
|
+
* same convention as Python's `datetime.fold`.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ReminderScheduleError extends Error {
|
|
16
|
+
readonly code: string;
|
|
17
|
+
constructor(code: string, message: string);
|
|
18
|
+
}
|
|
19
|
+
/** Canonical IANA zone name, or a typed refusal. Host defaults never apply. */
|
|
20
|
+
export declare function canonicalTimeZone(raw: unknown): string;
|
|
21
|
+
export interface ResolvedSchedule {
|
|
22
|
+
/** Canonical `YYYY-MM-DDTHH:MM:SS`, as the caller meant it. */
|
|
23
|
+
localTime: string;
|
|
24
|
+
timeZone: string;
|
|
25
|
+
/** Present only when the wall time was ambiguous and the caller chose. */
|
|
26
|
+
fold?: 0 | 1;
|
|
27
|
+
dueAt: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a wall time in a zone to its instant. Every offset the zone uses in
|
|
31
|
+
* the surrounding two days is a candidate; a candidate survives only if it
|
|
32
|
+
* reads back as the requested wall time. Zero survivors is a gap, two is an
|
|
33
|
+
* overlap. Zones change offset at most once in any such window in practice,
|
|
34
|
+
* and a reading that does not round-trip is never accepted, so the answer is
|
|
35
|
+
* never a guess.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveSchedule(localTime: unknown, timeZone: unknown, fold?: unknown): ResolvedSchedule;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wall-clock schedule resolution for self-reminders.
|
|
3
|
+
*
|
|
4
|
+
* A reminder is stated the way a person states it — a calendar date, a time of
|
|
5
|
+
* day and the IANA zone it is meant in — and stored as the one UTC instant that
|
|
6
|
+
* statement denotes. Nothing is inferred from the host's own zone: a time with
|
|
7
|
+
* an offset, a missing date or an unknown zone is refused rather than guessed.
|
|
8
|
+
*
|
|
9
|
+
* Daylight-saving transitions are the two places a wall time is not one
|
|
10
|
+
* instant. A time skipped by a spring-forward gap does not exist and is
|
|
11
|
+
* refused. A time repeated by a fall-back overlap exists twice, and the caller
|
|
12
|
+
* must say which with `fold` (0 = the first occurrence, 1 = the second), the
|
|
13
|
+
* same convention as Python's `datetime.fold`.
|
|
14
|
+
*/
|
|
15
|
+
export class ReminderScheduleError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.name = 'ReminderScheduleError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const LOCAL_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
24
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
25
|
+
const formatters = new Map();
|
|
26
|
+
function formatter(timeZone) {
|
|
27
|
+
let value = formatters.get(timeZone);
|
|
28
|
+
if (!value) {
|
|
29
|
+
value = new Intl.DateTimeFormat('en-US', {
|
|
30
|
+
timeZone, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
31
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit', era: 'short',
|
|
32
|
+
});
|
|
33
|
+
formatters.set(timeZone, value);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
/** Canonical IANA zone name, or a typed refusal. Host defaults never apply. */
|
|
38
|
+
export function canonicalTimeZone(raw) {
|
|
39
|
+
if (typeof raw !== 'string' || !raw.trim() || raw.length > 64 || !/^[A-Za-z0-9_+\-/]+$/.test(raw))
|
|
40
|
+
throw new ReminderScheduleError('invalid_time_zone', 'time_zone must be an IANA zone name such as Europe/London or UTC');
|
|
41
|
+
try {
|
|
42
|
+
return new Intl.DateTimeFormat('en-US', { timeZone: raw }).resolvedOptions().timeZone;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new ReminderScheduleError('invalid_time_zone', `unknown IANA time zone '${raw}'`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function parseLocalTime(raw) {
|
|
49
|
+
const match = typeof raw === 'string' ? LOCAL_TIME_RE.exec(raw) : null;
|
|
50
|
+
if (!match)
|
|
51
|
+
throw new ReminderScheduleError('invalid_local_time', 'local_time must be a calendar date and time without an offset, e.g. 2027-01-20T16:00 or 2027-01-20T16:00:00');
|
|
52
|
+
const [year, month, day, hour, minute, second] = match.slice(1).map(part => Number(part ?? 0));
|
|
53
|
+
const wall = { year: year, month: month, day: day, hour: hour, minute: minute, second: second };
|
|
54
|
+
// Date.UTC normalizes overflow (Feb 30 -> Mar 2); a round trip rejects it.
|
|
55
|
+
const probe = new Date(wallMs(wall));
|
|
56
|
+
if (wall.year < 1970 || wall.year > 9999 || probe.getUTCFullYear() !== wall.year
|
|
57
|
+
|| probe.getUTCMonth() + 1 !== wall.month || probe.getUTCDate() !== wall.day
|
|
58
|
+
|| wall.hour > 23 || wall.minute > 59 || wall.second > 59)
|
|
59
|
+
throw new ReminderScheduleError('invalid_local_time', `local_time '${String(raw)}' is not a real calendar date and time`);
|
|
60
|
+
return wall;
|
|
61
|
+
}
|
|
62
|
+
function wallMs(wall) {
|
|
63
|
+
return Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second);
|
|
64
|
+
}
|
|
65
|
+
/** The wall-clock reading of `instant` in `timeZone`, expressed as naive UTC milliseconds. */
|
|
66
|
+
function wallAt(instant, timeZone) {
|
|
67
|
+
const parts = Object.fromEntries(formatter(timeZone).formatToParts(new Date(instant))
|
|
68
|
+
.map(part => [part.type, part.value]));
|
|
69
|
+
if (parts.era !== 'AD')
|
|
70
|
+
throw new ReminderScheduleError('invalid_local_time', 'dates before the common era are not supported');
|
|
71
|
+
return Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour), Number(parts.minute), Number(parts.second));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a wall time in a zone to its instant. Every offset the zone uses in
|
|
75
|
+
* the surrounding two days is a candidate; a candidate survives only if it
|
|
76
|
+
* reads back as the requested wall time. Zero survivors is a gap, two is an
|
|
77
|
+
* overlap. Zones change offset at most once in any such window in practice,
|
|
78
|
+
* and a reading that does not round-trip is never accepted, so the answer is
|
|
79
|
+
* never a guess.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveSchedule(localTime, timeZone, fold) {
|
|
82
|
+
const zone = canonicalTimeZone(timeZone);
|
|
83
|
+
const wall = parseLocalTime(localTime);
|
|
84
|
+
if (fold !== undefined && fold !== null && fold !== 0 && fold !== 1)
|
|
85
|
+
throw new ReminderScheduleError('invalid_fold', 'fold must be 0 (first occurrence) or 1 (second occurrence)');
|
|
86
|
+
const naive = wallMs(wall);
|
|
87
|
+
const offsets = new Set();
|
|
88
|
+
for (let probe = -48; probe <= 48; probe += 6) {
|
|
89
|
+
const instant = naive + probe * HOUR_MS;
|
|
90
|
+
offsets.add(wallAt(instant, zone) - instant);
|
|
91
|
+
}
|
|
92
|
+
const matches = [...new Set([...offsets].map(offset => naive - offset))]
|
|
93
|
+
.filter(instant => wallAt(instant, zone) === naive)
|
|
94
|
+
.sort((a, b) => a - b);
|
|
95
|
+
const canonical = new Date(naive).toISOString().slice(0, 19);
|
|
96
|
+
if (!matches.length)
|
|
97
|
+
throw new ReminderScheduleError('nonexistent_local_time', `${canonical} does not exist in ${zone} (skipped by a daylight-saving change); choose another time`);
|
|
98
|
+
if (matches.length > 1) {
|
|
99
|
+
if (fold !== 0 && fold !== 1)
|
|
100
|
+
throw new ReminderScheduleError('ambiguous_local_time', `${canonical} occurs twice in ${zone}; pass fold 0 for the first occurrence or 1 for the second`);
|
|
101
|
+
return { localTime: canonical, timeZone: zone, fold, dueAt: matches[fold] };
|
|
102
|
+
}
|
|
103
|
+
return { localTime: canonical, timeZone: zone, dueAt: matches[0] };
|
|
104
|
+
}
|