@ours.network/fleet 1.2.0-nightly.6 → 1.2.0-nightly.60
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 +287 -65
- package/dist/account-origin.d.ts +2 -0
- package/dist/account-origin.js +6 -0
- package/dist/agent-ours/bridge.js +35 -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 +4 -0
- package/dist/agent-ours/mcp-endpoint.js +5 -0
- package/dist/agent-ours/runtime.d.ts +9 -1
- package/dist/agent-ours/runtime.js +50 -2
- package/dist/agent-ours/service.d.ts +21 -1
- package/dist/agent-ours/service.js +94 -14
- package/dist/application/codex-models.d.ts +15 -0
- package/dist/application/codex-models.js +56 -0
- package/dist/application/conversation-history.d.ts +27 -0
- package/dist/application/conversation-history.js +212 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- 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/role-creation-service.js +3 -2
- package/dist/application/role-removal-service.js +9 -1
- package/dist/application/role-repository.js +19 -2
- 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 +30 -0
- package/dist/application/session-control.js +72 -3
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +115 -0
- package/dist/application/task-layouts.d.ts +91 -0
- package/dist/application/task-layouts.js +259 -0
- package/dist/application/task-room-service.d.ts +99 -3
- package/dist/application/task-room-service.js +285 -32
- package/dist/application/types.d.ts +2 -0
- package/dist/atomic-file.d.ts +5 -0
- package/dist/atomic-file.js +62 -1
- package/dist/briefing.d.ts +3 -1
- package/dist/briefing.js +70 -30
- package/dist/build-info.json +7 -4
- package/dist/capabilities.d.ts +6 -1
- package/dist/capabilities.js +8 -0
- package/dist/cli.js +407 -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 +1 -8
- package/dist/daemon-recovery.js +8 -113
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +123 -19
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +55 -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 +26 -9
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/claude-code.js +20 -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 +23 -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 +8 -34
- package/dist/monitor.js +89 -152
- package/dist/notifications/fleet-producer.d.ts +19 -0
- package/dist/notifications/fleet-producer.js +208 -0
- package/dist/notifications/outbox.d.ts +46 -0
- package/dist/notifications/outbox.js +158 -0
- package/dist/notifications/target-cleanup.d.ts +8 -0
- package/dist/notifications/target-cleanup.js +41 -0
- package/dist/notifications/task-producer.d.ts +18 -0
- package/dist/notifications/task-producer.js +67 -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 +25 -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 +83 -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 +2 -4
- package/dist/rooms-tasks/cowork-adapter.js +7 -121
- package/dist/rooms-tasks/cowork-http.js +29 -1
- 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 +285 -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 +51 -8
- package/dist/rooms-tasks/task-state.js +245 -25
- 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 +25 -3
- package/dist/runner.js +291 -49
- package/dist/session/acp.d.ts +53 -1
- package/dist/session/acp.js +216 -40
- package/dist/session/arbiter.d.ts +11 -0
- package/dist/session/arbiter.js +43 -2
- package/dist/session/codex-app-server.d.ts +32 -0
- package/dist/session/codex-app-server.js +137 -23
- package/dist/session/control.d.ts +12 -1
- package/dist/session/control.js +68 -3
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-store.d.ts +1 -1
- package/dist/session/conversation-store.js +7 -11
- package/dist/session/conversation-types.d.ts +28 -3
- package/dist/session/prompt-batch.d.ts +63 -0
- package/dist/session/prompt-batch.js +86 -0
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +14 -0
- 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 +4 -0
- package/dist/startup-prompt.js +36 -0
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -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 +75 -0
- package/dist/subscriptions/login.js +182 -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 +140 -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/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 +113 -65
- 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/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +63 -2
- package/dist/web/auth.js +191 -10
- 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/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 +238 -0
- package/dist/web/runtime.d.ts +3 -1
- package/dist/web/runtime.js +116 -31
- package/dist/web/server.d.ts +16 -0
- package/dist/web/server.js +654 -55
- 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 +82 -0
- package/dist/workspace-enrollment.js +566 -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/fleet-migration.md +54 -0
- package/docs/live-voice.md +13 -0
- package/docs/room-layouts.md +227 -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 +20 -19
- 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 +1 -1
- 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 +34 -11
- package/presets/fleet/roles/Engineer.yaml +40 -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,208 @@
|
|
|
1
|
+
import { roomsDir } from '../rooms-tasks/room-state.js';
|
|
2
|
+
import { drainNotificationTargetCleanup } from './target-cleanup.js';
|
|
3
|
+
import { closeSync, existsSync, lstatSync, openSync, readSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { NotificationOutbox } from './outbox.js';
|
|
7
|
+
/** Unlike a UI inventory, unread classification must not treat unreadable records as absence. */
|
|
8
|
+
function taskRoleAssociations() {
|
|
9
|
+
const dir = roomsDir(), result = new Map();
|
|
10
|
+
let files;
|
|
11
|
+
try {
|
|
12
|
+
files = readdirSync(dir);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT')
|
|
16
|
+
return result;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
for (const name of files.filter(name => name.endsWith('.json'))) {
|
|
20
|
+
const file = join(dir, name), stat = lstatSync(file);
|
|
21
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024)
|
|
22
|
+
throw Error('Unreadable room association');
|
|
23
|
+
const room = JSON.parse(readFileSync(file, 'utf8'));
|
|
24
|
+
if (!room || typeof room.room_id !== 'string' || !Array.isArray(room.member_seats))
|
|
25
|
+
throw Error('Invalid room association');
|
|
26
|
+
if (room.task_id === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
if (typeof room.task_id !== 'string' || !/^[A-Za-z0-9_-]{1,120}$/.test(room.task_id))
|
|
29
|
+
throw Error('Invalid task association');
|
|
30
|
+
for (const seat of room.member_seats) {
|
|
31
|
+
if (typeof seat.role_name !== 'string')
|
|
32
|
+
throw Error('Invalid member association');
|
|
33
|
+
if (result.has(seat.role_name) && result.get(seat.role_name) !== room.task_id)
|
|
34
|
+
throw Error('Ambiguous task association');
|
|
35
|
+
result.set(seat.role_name, room.task_id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/** Read durable supervisor ledgers without restarting agents or attaching a
|
|
41
|
+
* conversation controller. Each tick reads at most 256 KiB per role, from its
|
|
42
|
+
* checkpoint; it never loads a complete transcript or requests model history. */
|
|
43
|
+
export class FleetNotificationProducer {
|
|
44
|
+
roots;
|
|
45
|
+
stateDir;
|
|
46
|
+
config;
|
|
47
|
+
warn;
|
|
48
|
+
queues = new Map();
|
|
49
|
+
timer;
|
|
50
|
+
existing = new Set();
|
|
51
|
+
cleanup;
|
|
52
|
+
constructor(roots, stateDir, config, warn = () => { }) {
|
|
53
|
+
this.roots = roots;
|
|
54
|
+
this.stateDir = stateDir;
|
|
55
|
+
this.config = config;
|
|
56
|
+
this.warn = warn;
|
|
57
|
+
for (const root of roots)
|
|
58
|
+
for (const name of this.names(root))
|
|
59
|
+
this.existing.add(join(root, name));
|
|
60
|
+
this.poll();
|
|
61
|
+
this.timer = setInterval(() => this.poll(), 1000);
|
|
62
|
+
this.timer.unref();
|
|
63
|
+
}
|
|
64
|
+
names(root) {
|
|
65
|
+
try {
|
|
66
|
+
return readdirSync(root, { withFileTypes: true }).filter(d => d.isDirectory() && /^[a-zA-Z0-9_-]+$/.test(d.name)).map(d => d.name);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
poll() {
|
|
73
|
+
this.cleanup ??= drainNotificationTargetCleanup(this.config, undefined, undefined, { beforeDelete: async (url) => {
|
|
74
|
+
const name = new URL(url, 'https://ours.invalid').searchParams.get('chat');
|
|
75
|
+
if (!name || !/^[A-Za-z0-9_-]+$/.test(name))
|
|
76
|
+
return false;
|
|
77
|
+
// A recreated or retained role still has a valid target. Filesystem errors cannot prove removal.
|
|
78
|
+
for (const root of this.roots) {
|
|
79
|
+
try {
|
|
80
|
+
lstatSync(join(root, name));
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code !== 'ENOENT')
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const root of this.roots) {
|
|
89
|
+
const roleDir = join(root, name), queue = this.queues.get(roleDir);
|
|
90
|
+
if (queue) {
|
|
91
|
+
await queue.retire();
|
|
92
|
+
this.queues.delete(roleDir);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const file = join(this.stateDir, createHash('sha256').update(roleDir).digest('hex') + '.json');
|
|
96
|
+
if (existsSync(file)) {
|
|
97
|
+
const queue = new NotificationOutbox(file, this.config, async () => { throw Error('retired target'); });
|
|
98
|
+
try {
|
|
99
|
+
await queue.retire();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await queue.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
} }).catch(() => { }).finally(() => { this.cleanup = undefined; });
|
|
109
|
+
let taskRoles;
|
|
110
|
+
try {
|
|
111
|
+
taskRoles = taskRoleAssociations();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
this.warn('Notification task association unavailable; delivering unclassified events');
|
|
115
|
+
}
|
|
116
|
+
for (const root of this.roots)
|
|
117
|
+
for (const roleId of this.names(root)) {
|
|
118
|
+
const roleDir = join(root, roleId), dir = join(roleDir, '.conversation');
|
|
119
|
+
try {
|
|
120
|
+
if (!existsSync(dir) || lstatSync(dir).isSymbolicLink())
|
|
121
|
+
continue;
|
|
122
|
+
const segments = readdirSync(dir).filter(n => /^events-\d{6}\.jsonl$/.test(n) && !lstatSync(join(dir, n)).isSymbolicLink()).sort();
|
|
123
|
+
if (!segments.length)
|
|
124
|
+
continue;
|
|
125
|
+
let outbox = this.queues.get(roleDir);
|
|
126
|
+
if (!outbox) {
|
|
127
|
+
const key = createHash('sha256').update(roleDir).digest('hex');
|
|
128
|
+
outbox = new NotificationOutbox(join(this.stateDir, key + '.json'), this.config, async (value, eventId) => ({ eventId, title: `${roleId}: agent completed`, body: 'Your agent finished responding. Open the chat to view the result.',
|
|
129
|
+
url: `/fleet/chats?chat=${encodeURIComponent(roleId)}&detail=1`, ...value }), this.warn);
|
|
130
|
+
if (!outbox.checkpoint) {
|
|
131
|
+
const segment = this.existing.has(roleDir) ? segments.at(-1) : segments[0];
|
|
132
|
+
outbox.saveCheckpoint({ segment, offset: this.existing.has(roleDir) ? statSync(join(dir, segment)).size : 0, prompts: [] });
|
|
133
|
+
}
|
|
134
|
+
this.queues.set(roleDir, outbox);
|
|
135
|
+
}
|
|
136
|
+
const saved = outbox.checkpoint;
|
|
137
|
+
const current = { ...saved, prompts: [...saved.prompts], messages: { ...saved.messages } };
|
|
138
|
+
let index = segments.indexOf(current.segment);
|
|
139
|
+
if (index < 0) {
|
|
140
|
+
this.warn('Notification ledger segment unavailable; checkpoint retained');
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const file = join(dir, current.segment), size = statSync(file).size;
|
|
144
|
+
if (size < current.offset) {
|
|
145
|
+
this.warn('Notification ledger truncated; checkpoint retained');
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (size === current.offset && index < segments.length - 1) {
|
|
149
|
+
current.segment = segments[++index];
|
|
150
|
+
current.offset = 0;
|
|
151
|
+
}
|
|
152
|
+
const fd = openSync(join(dir, current.segment), 'r'), buffer = Buffer.alloc(256 * 1024);
|
|
153
|
+
let count;
|
|
154
|
+
try {
|
|
155
|
+
count = readSync(fd, buffer, 0, buffer.length, current.offset);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
closeSync(fd);
|
|
159
|
+
}
|
|
160
|
+
let position = 0;
|
|
161
|
+
while (position < count) {
|
|
162
|
+
const end = buffer.indexOf(10, position);
|
|
163
|
+
if (end < 0 || end >= count) {
|
|
164
|
+
// Oversized model/media events are irrelevant to prompt/completion
|
|
165
|
+
// correlation. Skip in bounded chunks; retain partial small lines.
|
|
166
|
+
if (current.discarding || (position === 0 && count === buffer.length)) {
|
|
167
|
+
current.offset += count - position;
|
|
168
|
+
current.discarding = true;
|
|
169
|
+
this.warn('Notification observer skipping oversized ledger record');
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (current.discarding) {
|
|
174
|
+
current.offset += end - position + 1;
|
|
175
|
+
position = end + 1;
|
|
176
|
+
current.discarding = false;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const event = JSON.parse(buffer.subarray(position, end).toString('utf8'));
|
|
180
|
+
if (event.schemaVersion !== 1 || event.roleId !== roleId)
|
|
181
|
+
throw new Error('invalid ledger event');
|
|
182
|
+
const prompt = `${event.sessionGeneration}:${event.promptId}`;
|
|
183
|
+
if (event.kind === 'prompt.admitted' && event.promptId && ['browser', 'owner_admin_console', 'owner_channel'].includes(event.source ?? '')) {
|
|
184
|
+
if (!current.prompts.includes(prompt))
|
|
185
|
+
current.prompts.push(prompt);
|
|
186
|
+
}
|
|
187
|
+
if (['message.chunk', 'message.replace'].includes(event.kind) && event.promptId && event.messageId && current.prompts.includes(prompt))
|
|
188
|
+
current.messages[prompt] = event.messageId;
|
|
189
|
+
if (event.kind === 'turn.completed' && event.promptId && current.prompts.includes(prompt)) {
|
|
190
|
+
if (!outbox.enqueue(`${roleId}:${event.sessionGeneration}:${event.seq}`, { ...(taskRoles ? { taskId: taskRoles.get(roleId) ?? null } : {}), url: `/fleet/chats?chat=${encodeURIComponent(roleId)}&detail=1#fleet-message-${encodeURIComponent(current.messages[prompt] ?? event.promptId)}` }))
|
|
191
|
+
break;
|
|
192
|
+
current.prompts = current.prompts.filter(p => p !== prompt);
|
|
193
|
+
delete current.messages[prompt];
|
|
194
|
+
}
|
|
195
|
+
current.offset += end - position + 1;
|
|
196
|
+
position = end + 1;
|
|
197
|
+
}
|
|
198
|
+
if (current.segment !== saved.segment || current.offset !== saved.offset)
|
|
199
|
+
outbox.saveCheckpoint(current);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
this.warn('Notification ledger admission pending; checkpoint will replay');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async drain() { await Promise.all([...this.queues.values()].map(q => q.drain())); }
|
|
207
|
+
async close() { clearInterval(this.timer); await this.cleanup; await Promise.all([...this.queues.values()].map(q => q.close())); }
|
|
208
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface NotificationInput {
|
|
2
|
+
eventId: string;
|
|
3
|
+
title: string;
|
|
4
|
+
body: string;
|
|
5
|
+
url: string;
|
|
6
|
+
taskId?: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface ProducerConfig {
|
|
9
|
+
/** Service base URL. With a gateway credential it is the server gateway's notifications route. */
|
|
10
|
+
origin: string;
|
|
11
|
+
token: string;
|
|
12
|
+
/** Daemon API credential that authenticates this client to the server gateway. */
|
|
13
|
+
gatewayCredential?: string;
|
|
14
|
+
}
|
|
15
|
+
type ProfileLocation = {
|
|
16
|
+
serverUrl: string;
|
|
17
|
+
expectedInstanceId: string;
|
|
18
|
+
credentialPath: string;
|
|
19
|
+
};
|
|
20
|
+
/** The installer delivers the producer credential beside the profile credential, bound to that server installation. */
|
|
21
|
+
export declare const PRODUCER_CREDENTIAL_FILE = "notifications-producer.json";
|
|
22
|
+
export declare function producerConfig(env?: NodeJS.ProcessEnv, profile?: () => ProfileLocation | undefined, warn?: (message: string) => void): ProducerConfig | undefined;
|
|
23
|
+
/** Durable producer queue. Service dedupe makes crash-after-acceptance replay safe. */
|
|
24
|
+
export declare class NotificationOutbox {
|
|
25
|
+
private readonly file;
|
|
26
|
+
private readonly config;
|
|
27
|
+
private readonly project;
|
|
28
|
+
private readonly warn;
|
|
29
|
+
private entries;
|
|
30
|
+
cursor?: number;
|
|
31
|
+
checkpoint?: unknown;
|
|
32
|
+
private running?;
|
|
33
|
+
private retired;
|
|
34
|
+
private timer;
|
|
35
|
+
constructor(file: string, config: ProducerConfig, project: (value: unknown, id: string) => Promise<NotificationInput>, warn?: (message: string) => void);
|
|
36
|
+
private persist;
|
|
37
|
+
advance(cursor: number): void;
|
|
38
|
+
saveCheckpoint(checkpoint: unknown): void;
|
|
39
|
+
enqueue(id: string, value: unknown, cursor?: number | undefined): boolean;
|
|
40
|
+
drain(): Promise<void>;
|
|
41
|
+
private drainOnce;
|
|
42
|
+
/** Wait for the current send before clearing queued sends and retiring its target. */
|
|
43
|
+
retire(): Promise<void>;
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { readPrivateFile } from '@ours.network/sdk/connector';
|
|
4
|
+
import { readClientProfile } from '../client-profile.js';
|
|
5
|
+
function defaultProfile() {
|
|
6
|
+
try {
|
|
7
|
+
return readClientProfile();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** The installer delivers the producer credential beside the profile credential, bound to that server installation. */
|
|
14
|
+
export const PRODUCER_CREDENTIAL_FILE = 'notifications-producer.json';
|
|
15
|
+
const loopback = (url) => ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname);
|
|
16
|
+
export function producerConfig(env = process.env, profile = defaultProfile, warn = () => { }) {
|
|
17
|
+
const origin = env.OURS_NOTIFICATIONS_ORIGIN, token = env.OURS_NOTIFICATIONS_PRODUCER_TOKEN;
|
|
18
|
+
if (origin || token) {
|
|
19
|
+
if (!origin || !token || token.length < 32)
|
|
20
|
+
throw new Error('notifications producer requires origin and scoped token');
|
|
21
|
+
const url = new URL(origin);
|
|
22
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.origin !== origin || url.username || url.password)
|
|
23
|
+
throw new Error('invalid notification service origin');
|
|
24
|
+
if (url.protocol === 'http:' && !loopback(url))
|
|
25
|
+
throw new Error('notification credentials require HTTPS or loopback');
|
|
26
|
+
return { origin, token };
|
|
27
|
+
}
|
|
28
|
+
const selected = profile();
|
|
29
|
+
if (!selected)
|
|
30
|
+
return;
|
|
31
|
+
const file = join(dirname(selected.credentialPath), PRODUCER_CREDENTIAL_FILE);
|
|
32
|
+
if (!existsSync(file))
|
|
33
|
+
return;
|
|
34
|
+
// Reasons are fixed text: parser and file errors can quote the secret-bearing contents.
|
|
35
|
+
let reason = 'it is not a private owner-only file';
|
|
36
|
+
try {
|
|
37
|
+
const text = readPrivateFile(file, 4096).toString('utf8');
|
|
38
|
+
reason = 'it is not valid producer JSON';
|
|
39
|
+
const bound = JSON.parse(text);
|
|
40
|
+
reason = 'it belongs to another server installation';
|
|
41
|
+
if (!bound || bound.schema !== 1 || bound.serverUrl !== selected.serverUrl || bound.expectedInstanceId !== selected.expectedInstanceId)
|
|
42
|
+
throw new Error();
|
|
43
|
+
reason = 'its credential is invalid';
|
|
44
|
+
if (typeof bound.token !== 'string' || bound.token.length < 32)
|
|
45
|
+
throw new Error();
|
|
46
|
+
reason = 'the server requires HTTPS or loopback';
|
|
47
|
+
const server = new URL(selected.serverUrl);
|
|
48
|
+
if (server.protocol !== 'https:' && !(server.protocol === 'http:' && loopback(server)))
|
|
49
|
+
throw new Error();
|
|
50
|
+
reason = 'the client credential is unreadable';
|
|
51
|
+
const gatewayCredential = readPrivateFile(selected.credentialPath, 4096).toString('utf8').trim();
|
|
52
|
+
return { origin: selected.serverUrl.replace(/\/$/, '') + '/notifications', token: bound.token, gatewayCredential };
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
warn(`Notifications are not produced: ${file} is unusable (${reason}). Re-run the installer to repair it.`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Durable producer queue. Service dedupe makes crash-after-acceptance replay safe. */
|
|
60
|
+
export class NotificationOutbox {
|
|
61
|
+
file;
|
|
62
|
+
config;
|
|
63
|
+
project;
|
|
64
|
+
warn;
|
|
65
|
+
entries;
|
|
66
|
+
cursor;
|
|
67
|
+
checkpoint;
|
|
68
|
+
running;
|
|
69
|
+
retired = false;
|
|
70
|
+
timer;
|
|
71
|
+
constructor(file, config, project, warn = () => { }) {
|
|
72
|
+
this.file = file;
|
|
73
|
+
this.config = config;
|
|
74
|
+
this.project = project;
|
|
75
|
+
this.warn = warn;
|
|
76
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
77
|
+
const saved = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { entries: [] };
|
|
78
|
+
this.entries = Array.isArray(saved) ? saved : saved.entries;
|
|
79
|
+
this.cursor = saved.cursor;
|
|
80
|
+
this.checkpoint = saved.checkpoint;
|
|
81
|
+
if (!Array.isArray(this.entries))
|
|
82
|
+
throw new Error('invalid notification outbox');
|
|
83
|
+
this.timer = setInterval(() => { void this.drain(); }, 3000);
|
|
84
|
+
this.timer.unref();
|
|
85
|
+
}
|
|
86
|
+
persist(entries, cursor = this.cursor, checkpoint = this.checkpoint) {
|
|
87
|
+
const tmp = this.file + '.tmp', fd = openSync(tmp, 'w', 0o600);
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(fd, JSON.stringify({ entries, cursor, checkpoint }));
|
|
90
|
+
fsyncSync(fd);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
closeSync(fd);
|
|
94
|
+
}
|
|
95
|
+
renameSync(tmp, this.file);
|
|
96
|
+
const dir = openSync(dirname(this.file), 'r');
|
|
97
|
+
try {
|
|
98
|
+
fsyncSync(dir);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
closeSync(dir);
|
|
102
|
+
}
|
|
103
|
+
this.entries = entries;
|
|
104
|
+
this.cursor = cursor;
|
|
105
|
+
this.checkpoint = checkpoint;
|
|
106
|
+
}
|
|
107
|
+
advance(cursor) { this.persist(this.entries, cursor); }
|
|
108
|
+
saveCheckpoint(checkpoint) { this.persist(this.entries, this.cursor, checkpoint); }
|
|
109
|
+
enqueue(id, value, cursor = this.cursor) {
|
|
110
|
+
if (this.retired)
|
|
111
|
+
return false;
|
|
112
|
+
if (this.entries.some(e => e.id === id)) {
|
|
113
|
+
if (cursor !== this.cursor)
|
|
114
|
+
this.advance(cursor);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (this.entries.length >= 2000)
|
|
118
|
+
return false;
|
|
119
|
+
this.persist([...this.entries, { id, value }], cursor);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
drain() {
|
|
123
|
+
if (this.retired)
|
|
124
|
+
return Promise.resolve();
|
|
125
|
+
return this.running ??= this.drainOnce().finally(() => { this.running = undefined; });
|
|
126
|
+
}
|
|
127
|
+
async drainOnce() {
|
|
128
|
+
for (const entry of [...this.entries].slice(0, 32)) {
|
|
129
|
+
if (this.retired)
|
|
130
|
+
break;
|
|
131
|
+
try {
|
|
132
|
+
const value = entry.payload ?? await this.project(entry.value, entry.id);
|
|
133
|
+
// Freeze the canonical projection before any network attempt. A crash
|
|
134
|
+
// after service acceptance must replay the identical dedupe payload.
|
|
135
|
+
if (!entry.payload)
|
|
136
|
+
this.persist(this.entries.map(e => e.id === entry.id ? { ...e, payload: value } : e));
|
|
137
|
+
const response = await fetch(this.config.origin + '/api/v1/send', {
|
|
138
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000),
|
|
139
|
+
headers: { ...(this.config.gatewayCredential
|
|
140
|
+
? { 'X-Ours-Api-Token': this.config.gatewayCredential, 'X-Ours-Notifications-Producer': this.config.token }
|
|
141
|
+
: { Authorization: `Bearer ${this.config.token}` }), 'Content-Type': 'application/json' },
|
|
142
|
+
body: JSON.stringify(value),
|
|
143
|
+
});
|
|
144
|
+
await response.body?.cancel();
|
|
145
|
+
if (!response.ok)
|
|
146
|
+
throw new Error('notification service rejected event');
|
|
147
|
+
this.persist(this.entries.filter(e => e.id !== entry.id));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
this.warn('Notification producer delivery pending; retrying from durable outbox');
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Wait for the current send before clearing queued sends and retiring its target. */
|
|
156
|
+
async retire() { this.retired = true; clearInterval(this.timer); await this.running; this.persist([]); }
|
|
157
|
+
async close() { clearInterval(this.timer); await this.running; }
|
|
158
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ProducerConfig } from './outbox.js';
|
|
2
|
+
/** Queue only after the owning lifecycle has established deletion, never on a failed inventory read. */
|
|
3
|
+
export declare function retireNotificationTarget(url: string, config?: ProducerConfig | undefined, dir?: string, request?: typeof fetch): Promise<void>;
|
|
4
|
+
/** Persisted retries also run from the ordinary web notification producer after restart. */
|
|
5
|
+
export declare function drainNotificationTargetCleanup(config: ProducerConfig, dir?: string, request?: typeof fetch, options?: {
|
|
6
|
+
retain?: boolean;
|
|
7
|
+
beforeDelete?: (url: string) => Promise<boolean>;
|
|
8
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { replaceFileAtomically } from '../atomic-file.js';
|
|
5
|
+
import { stateRoot } from '../paths.js';
|
|
6
|
+
import { producerConfig } from './outbox.js';
|
|
7
|
+
const cleanupDir = () => join(stateRoot(), 'notifications', 'removed-targets');
|
|
8
|
+
/** Queue only after the owning lifecycle has established deletion, never on a failed inventory read. */
|
|
9
|
+
export async function retireNotificationTarget(url, config = producerConfig(), dir = cleanupDir(), request = fetch) {
|
|
10
|
+
if (!config)
|
|
11
|
+
return;
|
|
12
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
13
|
+
replaceFileAtomically(join(dir, createHash('sha256').update(url).digest('hex') + '.json'), JSON.stringify({ url }) + '\n', 0o600);
|
|
14
|
+
await drainNotificationTargetCleanup(config, dir, request, { retain: true });
|
|
15
|
+
}
|
|
16
|
+
/** Persisted retries also run from the ordinary web notification producer after restart. */
|
|
17
|
+
export async function drainNotificationTargetCleanup(config, dir = cleanupDir(), request = fetch, options = {}) {
|
|
18
|
+
if (!existsSync(dir))
|
|
19
|
+
return;
|
|
20
|
+
for (const file of readdirSync(dir).filter(file => /^[a-f0-9]{64}\.json$/.test(file)).slice(0, 32)) {
|
|
21
|
+
try {
|
|
22
|
+
const path = join(dir, file), value = JSON.parse(readFileSync(path, 'utf8'));
|
|
23
|
+
if (typeof value.url !== 'string' || !value.url.startsWith('/fleet/chats?'))
|
|
24
|
+
continue;
|
|
25
|
+
if (options.beforeDelete && !(await options.beforeDelete(value.url))) {
|
|
26
|
+
rmSync(path, { force: true });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const response = await request(config.origin + '/api/v1/delete-target', { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10000),
|
|
30
|
+
headers: { ...(config.gatewayCredential ? { 'X-Ours-Api-Token': config.gatewayCredential, 'X-Ours-Notifications-Producer': config.token } : { Authorization: `Bearer ${config.token}` }), 'Content-Type': 'application/json' }, body: JSON.stringify(value) });
|
|
31
|
+
await response.body?.cancel();
|
|
32
|
+
if (!response.ok)
|
|
33
|
+
break;
|
|
34
|
+
if (!options.retain)
|
|
35
|
+
rmSync(path, { force: true });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
break; /* No credentials or response bodies are included in logs. */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ProducerConfig } from './outbox.js';
|
|
2
|
+
/** The Fleet manager owns this sender even when the web console is stopped.
|
|
3
|
+
* Events are already durable in their owning task commit, so no second outbox
|
|
4
|
+
* admission/crash window is needed. A failed task does not block other tasks. */
|
|
5
|
+
export declare class TaskNotificationProducer {
|
|
6
|
+
private readonly config;
|
|
7
|
+
private readonly warn;
|
|
8
|
+
private readonly request;
|
|
9
|
+
private readonly timer;
|
|
10
|
+
private running?;
|
|
11
|
+
private offset;
|
|
12
|
+
private stopped;
|
|
13
|
+
private readonly shutdown;
|
|
14
|
+
constructor(config: ProducerConfig, warn?: (line: string) => void, request?: typeof fetch);
|
|
15
|
+
drain(): Promise<void>;
|
|
16
|
+
private drainOnce;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { acknowledgeTaskNotification, pendingTaskNotifications } from '../rooms-tasks/task-state.js';
|
|
2
|
+
/** The Fleet manager owns this sender even when the web console is stopped.
|
|
3
|
+
* Events are already durable in their owning task commit, so no second outbox
|
|
4
|
+
* admission/crash window is needed. A failed task does not block other tasks. */
|
|
5
|
+
export class TaskNotificationProducer {
|
|
6
|
+
config;
|
|
7
|
+
warn;
|
|
8
|
+
request;
|
|
9
|
+
timer;
|
|
10
|
+
running;
|
|
11
|
+
offset = 0;
|
|
12
|
+
stopped = false;
|
|
13
|
+
shutdown = new AbortController();
|
|
14
|
+
constructor(config, warn = () => { }, request = fetch) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.warn = warn;
|
|
17
|
+
this.request = request;
|
|
18
|
+
this.timer = setInterval(() => { void this.drain(); }, 3000);
|
|
19
|
+
this.timer.unref();
|
|
20
|
+
void this.drain();
|
|
21
|
+
}
|
|
22
|
+
drain() {
|
|
23
|
+
return this.running ??= this.drainOnce().catch(() => {
|
|
24
|
+
this.warn('Task notification snapshot unavailable; committed events retained');
|
|
25
|
+
}).finally(() => { this.running = undefined; });
|
|
26
|
+
}
|
|
27
|
+
async drainOnce() {
|
|
28
|
+
if (this.stopped)
|
|
29
|
+
return;
|
|
30
|
+
const pending = pendingTaskNotifications(this.warn);
|
|
31
|
+
if (!pending.length)
|
|
32
|
+
return;
|
|
33
|
+
const start = this.offset % pending.length;
|
|
34
|
+
const batch = [...pending.slice(start), ...pending.slice(0, start)].slice(0, 32);
|
|
35
|
+
this.offset = start + batch.length;
|
|
36
|
+
for (let index = 0; index < batch.length; index += 4) {
|
|
37
|
+
if (this.stopped)
|
|
38
|
+
break;
|
|
39
|
+
await Promise.all(batch.slice(index, index + 4).map(async (task) => {
|
|
40
|
+
for (const event of task.events.slice(0, 32)) {
|
|
41
|
+
if (this.stopped)
|
|
42
|
+
break;
|
|
43
|
+
try {
|
|
44
|
+
const response = await this.request(this.config.origin + '/api/v1/send', {
|
|
45
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.any([this.shutdown.signal, AbortSignal.timeout(10_000)]),
|
|
46
|
+
headers: { ...(this.config.gatewayCredential
|
|
47
|
+
? { 'X-Ours-Api-Token': this.config.gatewayCredential, 'X-Ours-Notifications-Producer': this.config.token }
|
|
48
|
+
: { Authorization: `Bearer ${this.config.token}` }), 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify(event),
|
|
50
|
+
});
|
|
51
|
+
await response.body?.cancel();
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
this.warn(`Task notification delivery pending (HTTP ${response.status}); committed event retained`);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
acknowledgeTaskNotification(task.taskId, event.eventId);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
this.warn('Task notification delivery pending; committed event retained');
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async close() { this.stopped = true; clearInterval(this.timer); this.shutdown.abort(); await this.running; }
|
|
67
|
+
}
|
package/dist/ops.d.ts
CHANGED
|
@@ -54,5 +54,7 @@ export declare function up(cfg: FleetConfig, names: string[], deps: OpsDeps, con
|
|
|
54
54
|
export declare function down(cfg: FleetConfig, names: string[], deps: OpsDeps): Promise<void>;
|
|
55
55
|
/** Re-sync from config + bounce. mode 'keep' resumes context; 'fresh' wipes it. */
|
|
56
56
|
export declare function restartRoles(cfg: FleetConfig, names: string[], deps: OpsDeps, mode: 'keep' | 'fresh', configPath?: string): Promise<void>;
|
|
57
|
+
/** Read-only admission check shared by CLI removal and web preview, before any archive. */
|
|
58
|
+
export declare function assertStandaloneRoleRemoval(cfg: FleetConfig, name: string): void;
|
|
57
59
|
/** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
|
|
58
60
|
export declare function rmRole(cfg: FleetConfig, name: string, deps: OpsDeps): Promise<void>;
|
package/dist/ops.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { retireNotificationTarget } from './notifications/target-cleanup.js';
|
|
1
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, unlinkSync, } from 'node:fs';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { randomUUID, createHash } from 'node:crypto';
|
|
@@ -5,8 +6,9 @@ import { agentDir, watchdogsRoot } from './paths.js';
|
|
|
5
6
|
import { findRole } from './config.js';
|
|
6
7
|
import { getAdapter } from './harness/registry.js';
|
|
7
8
|
import { generateBriefing } from './briefing.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { managedCliBriefing } from './managed-cli.js';
|
|
10
|
+
import { resetRestartLedger, loadTempRole } from './runner.js';
|
|
11
|
+
import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, readTempSupervisor, } from './temp-lifecycle.js';
|
|
10
12
|
import { realExec } from './exec.js';
|
|
11
13
|
import { daemonIdentityProvisioner, reconcilePermanentRoleIdentities, } from './creation.js';
|
|
12
14
|
import { provesGeneratedAgentSource } from './generated-agent-source.js';
|
|
@@ -28,7 +30,7 @@ export function applyRole(role, opts = {}) {
|
|
|
28
30
|
// brought up from so the supervised process can reload the SAME file instead of
|
|
29
31
|
// silently falling back to the default ~/fleet.yaml. Temp roles snapshot their
|
|
30
32
|
// whole resolved role into role.yaml instead and don't need this.
|
|
31
|
-
if (!opts.temp)
|
|
33
|
+
if (!opts.temp || opts.configPath)
|
|
32
34
|
writeFileSync(join(dir, '.config-path'), (opts.configPath ?? '') + '\n');
|
|
33
35
|
writeFileSync(join(dir, '.identity'), role.identity + '\n');
|
|
34
36
|
if (role.cwd)
|
|
@@ -43,6 +45,7 @@ export function applyRole(role, opts = {}) {
|
|
|
43
45
|
routinesPath: join(dir, 'ROUTINES.md'), briefingBody,
|
|
44
46
|
identityGuarantee: opts.identityGuarantee,
|
|
45
47
|
temporaryIdentity: opts.temp === true,
|
|
48
|
+
managedCli: managedCliBriefing(role, opts.configPath, opts.temp === true),
|
|
46
49
|
}));
|
|
47
50
|
if (opts.fresh)
|
|
48
51
|
for (const f of ['.booted', '.session-id', '.exit-status'])
|
|
@@ -220,8 +223,21 @@ export async function restartRoles(cfg, names, deps, mode, configPath) {
|
|
|
220
223
|
: `↻ ${role.name} — restarted (resumes; briefing re-synced)`);
|
|
221
224
|
}
|
|
222
225
|
}
|
|
226
|
+
/** Read-only admission check shared by CLI removal and web preview, before any archive. */
|
|
227
|
+
export function assertStandaloneRoleRemoval(cfg, name) {
|
|
228
|
+
if (cfg.roles.some(role => role.name === name) || !/^[A-Za-z0-9_-]+$/.test(name))
|
|
229
|
+
return;
|
|
230
|
+
const dir = agentDir(name, true);
|
|
231
|
+
if (!existsSync(dir))
|
|
232
|
+
return;
|
|
233
|
+
const taskId = readTempSupervisor(dir)?.taskOwner?.taskId
|
|
234
|
+
?? (existsSync(join(dir, 'role.yaml')) ? loadTempRole(name).roomMemberStartup?.task_id : undefined);
|
|
235
|
+
if (taskId)
|
|
236
|
+
throw Error(`TASK_MEMBER_IS_TASK_OWNED: ${name} belongs to task ${taskId}; use the owning task lifecycle; if its record is missing or corrupt, preserve evidence and restore the original records before retrying task delete`);
|
|
237
|
+
}
|
|
223
238
|
/** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
|
|
224
239
|
export async function rmRole(cfg, name, deps) {
|
|
240
|
+
assertStandaloneRoleRemoval(cfg, name);
|
|
225
241
|
const temporaryDir = /^[A-Za-z0-9_-]+$/.test(name) ? agentDir(name, true) : '';
|
|
226
242
|
const configured = cfg.roles.find(role => role.name === name);
|
|
227
243
|
if (!configured && temporaryDir && existsSync(temporaryDir)) {
|
|
@@ -255,6 +271,7 @@ export async function rmRole(cfg, name, deps) {
|
|
|
255
271
|
rmSync(agentDir(name), { recursive: true, force: true });
|
|
256
272
|
if (generatedSource && existsSync(role.sourceFile)) {
|
|
257
273
|
unlinkSync(role.sourceFile);
|
|
274
|
+
await retireNotificationTarget(`/fleet/chats?chat=${encodeURIComponent(name)}`).catch(() => { });
|
|
258
275
|
deps.log(`removed ${role.sourceFile}`);
|
|
259
276
|
}
|
|
260
277
|
deps.log(`removed '${name}' (its ours identity is left intact)`);
|