@coool/opencode-adaptive 1.0.0 → 1.0.1
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 +8 -7
- package/dist/adapters/codex-capacity/index.d.ts +0 -1
- package/dist/adapters/codex-capacity/index.js.map +1 -1
- package/dist/adapters/codex-capacity/parse.d.ts +2 -2
- package/dist/adapters/codex-capacity/parse.js +19 -3
- package/dist/adapters/codex-capacity/parse.js.map +1 -1
- package/dist/adapters/codex-capacity/reading.d.ts +0 -1
- package/dist/adapters/codex-capacity/reading.js.map +1 -1
- package/dist/adapters/codex-capacity/sensor.d.ts +0 -1
- package/dist/adapters/codex-capacity/sensor.js.map +1 -1
- package/dist/adapters/codex-capacity/types.d.ts +0 -4
- package/dist/adapters/codex-capacity/types.js +14 -0
- package/dist/adapters/codex-capacity/types.js.map +1 -1
- package/dist/adapters/opencode/escalation-tool.d.ts +0 -1
- package/dist/adapters/opencode/escalation-tool.js +5 -3
- package/dist/adapters/opencode/escalation-tool.js.map +1 -1
- package/dist/adapters/opencode/events.d.ts +0 -1
- package/dist/adapters/opencode/events.js.map +1 -1
- package/dist/adapters/opencode/hooks.d.ts +0 -1
- package/dist/adapters/opencode/hooks.js +24 -7
- package/dist/adapters/opencode/hooks.js.map +1 -1
- package/dist/adapters/opencode/index.d.ts +0 -1
- package/dist/adapters/opencode/index.js.map +1 -1
- package/dist/adapters/opencode/inventory.d.ts +0 -1
- package/dist/adapters/opencode/inventory.js.map +1 -1
- package/dist/adapters/opencode/report.d.ts +0 -1
- package/dist/adapters/opencode/report.js.map +1 -1
- package/dist/adapters/opencode/session-ops.d.ts +0 -1
- package/dist/adapters/opencode/session-ops.js.map +1 -1
- package/dist/adapters/opencode/storage.d.ts +0 -1
- package/dist/adapters/opencode/storage.js.map +1 -1
- package/dist/adapters/opencode/tier-tool.d.ts +0 -1
- package/dist/adapters/opencode/tier-tool.js.map +1 -1
- package/dist/adapters/opencode/types.d.ts +0 -1
- package/dist/adapters/opencode/types.js.map +1 -1
- package/dist/core/compatibility/gate.d.ts +0 -3
- package/dist/core/compatibility/gate.js +0 -4
- package/dist/core/compatibility/gate.js.map +1 -1
- package/dist/core/compatibility/index.d.ts +0 -1
- package/dist/core/compatibility/index.js.map +1 -1
- package/dist/core/compatibility/types.d.ts +0 -1
- package/dist/core/compatibility/types.js.map +1 -1
- package/dist/core/scheduler/admit.d.ts +0 -1
- package/dist/core/scheduler/admit.js +22 -21
- package/dist/core/scheduler/admit.js.map +1 -1
- package/dist/core/scheduler/escalate.d.ts +0 -1
- package/dist/core/scheduler/escalate.js +6 -17
- package/dist/core/scheduler/escalate.js.map +1 -1
- package/dist/core/scheduler/index.d.ts +0 -1
- package/dist/core/scheduler/index.js.map +1 -1
- package/dist/core/scheduler/mode.d.ts +0 -1
- package/dist/core/scheduler/mode.js.map +1 -1
- package/dist/core/scheduler/types.d.ts +2 -9
- package/dist/core/scheduler/types.js +2 -1
- package/dist/core/scheduler/types.js.map +1 -1
- package/dist/core/tier-policy/index.d.ts +0 -1
- package/dist/core/tier-policy/index.js.map +1 -1
- package/dist/core/tier-policy/resolve.d.ts +0 -1
- package/dist/core/tier-policy/resolve.js.map +1 -1
- package/dist/core/tier-policy/tier-list.d.ts +0 -1
- package/dist/core/tier-policy/tier-list.js.map +1 -1
- package/dist/core/tier-policy/types.d.ts +2 -1
- package/dist/core/tier-policy/types.js +4 -0
- package/dist/core/tier-policy/types.js.map +1 -1
- package/dist/core/work-unit/index.d.ts +0 -1
- package/dist/core/work-unit/index.js.map +1 -1
- package/dist/core/work-unit/interruption.d.ts +0 -1
- package/dist/core/work-unit/interruption.js.map +1 -1
- package/dist/core/work-unit/tracker.d.ts +11 -4
- package/dist/core/work-unit/tracker.js +26 -6
- package/dist/core/work-unit/tracker.js.map +1 -1
- package/dist/core/work-unit/types.d.ts +6 -1
- package/dist/core/work-unit/types.js.map +1 -1
- package/dist/execution/continuation/execute.d.ts +0 -1
- package/dist/execution/continuation/execute.js.map +1 -1
- package/dist/execution/continuation/index.d.ts +0 -2
- package/dist/execution/continuation/index.js +0 -1
- package/dist/execution/continuation/index.js.map +1 -1
- package/dist/execution/continuation/plan.d.ts +0 -1
- package/dist/execution/continuation/plan.js +1 -3
- package/dist/execution/continuation/plan.js.map +1 -1
- package/dist/execution/continuation/prompts.d.ts +0 -1
- package/dist/execution/continuation/prompts.js.map +1 -1
- package/dist/execution/continuation/types.d.ts +0 -2
- package/dist/execution/continuation/types.js.map +1 -1
- package/dist/execution/escalation/index.d.ts +0 -1
- package/dist/execution/escalation/index.js.map +1 -1
- package/dist/execution/escalation/types.d.ts +0 -1
- package/dist/execution/escalation/types.js.map +1 -1
- package/dist/execution/index.d.ts +0 -1
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/parking/index.d.ts +0 -1
- package/dist/execution/parking/index.js.map +1 -1
- package/dist/execution/parking/plan.d.ts +0 -1
- package/dist/execution/parking/plan.js.map +1 -1
- package/dist/execution/retry/decide.d.ts +0 -1
- package/dist/execution/retry/decide.js +1 -2
- package/dist/execution/retry/decide.js.map +1 -1
- package/dist/execution/retry/index.d.ts +0 -1
- package/dist/execution/retry/index.js.map +1 -1
- package/dist/execution/retry/types.d.ts +0 -2
- package/dist/execution/retry/types.js.map +1 -1
- package/dist/gui-experimental.cjs +1 -1
- package/dist/gui.d.ts +0 -1
- package/dist/gui.js +6 -3
- package/dist/gui.js.map +1 -1
- package/dist/health/capacity/index.d.ts +0 -1
- package/dist/health/capacity/index.js.map +1 -1
- package/dist/health/capacity/merge.d.ts +0 -1
- package/dist/health/capacity/merge.js.map +1 -1
- package/dist/health/capacity/registry.d.ts +0 -1
- package/dist/health/capacity/registry.js.map +1 -1
- package/dist/health/capacity/types.d.ts +0 -1
- package/dist/health/capacity/types.js.map +1 -1
- package/dist/health/circuit-breaker/breaker.d.ts +8 -1
- package/dist/health/circuit-breaker/breaker.js +18 -0
- package/dist/health/circuit-breaker/breaker.js.map +1 -1
- package/dist/health/circuit-breaker/index.d.ts +0 -1
- package/dist/health/circuit-breaker/index.js.map +1 -1
- package/dist/health/circuit-breaker/types.d.ts +0 -1
- package/dist/health/circuit-breaker/types.js.map +1 -1
- package/dist/health/classifier/classify.d.ts +0 -1
- package/dist/health/classifier/classify.js.map +1 -1
- package/dist/health/classifier/index.d.ts +0 -1
- package/dist/health/classifier/index.js.map +1 -1
- package/dist/health/classifier/types.d.ts +0 -1
- package/dist/health/classifier/types.js.map +1 -1
- package/dist/health/index.d.ts +0 -1
- package/dist/health/index.js.map +1 -1
- package/dist/health/resource-health/index.d.ts +0 -1
- package/dist/health/resource-health/index.js.map +1 -1
- package/dist/health/resource-health/key.d.ts +0 -1
- package/dist/health/resource-health/key.js.map +1 -1
- package/dist/health/resource-health/registry.d.ts +6 -1
- package/dist/health/resource-health/registry.js +19 -1
- package/dist/health/resource-health/registry.js.map +1 -1
- package/dist/health/resource-health/transitions.d.ts +2 -4
- package/dist/health/resource-health/transitions.js +10 -18
- package/dist/health/resource-health/transitions.js.map +1 -1
- package/dist/health/resource-health/types.d.ts +7 -1
- package/dist/health/resource-health/types.js +1 -0
- package/dist/health/resource-health/types.js.map +1 -1
- package/dist/index.d.ts +0 -6
- package/dist/index.js +44 -19
- package/dist/index.js.map +1 -1
- package/dist/plugin-id.d.ts +0 -1
- package/dist/plugin-id.js.map +1 -1
- package/dist/rpc/definition.d.ts +24 -1
- package/dist/rpc/definition.js +28 -2
- package/dist/rpc/definition.js.map +1 -1
- package/dist/rpc/handlers.d.ts +12 -24
- package/dist/rpc/handlers.js +22 -8
- package/dist/rpc/handlers.js.map +1 -1
- package/dist/rpc/status.d.ts +0 -1
- package/dist/rpc/status.js.map +1 -1
- package/dist/rpc.d.ts +0 -1
- package/dist/rpc.js.map +1 -1
- package/dist/state/capacity/index.d.ts +0 -1
- package/dist/state/capacity/index.js.map +1 -1
- package/dist/state/capacity/parse.d.ts +0 -1
- package/dist/state/capacity/parse.js +14 -2
- package/dist/state/capacity/parse.js.map +1 -1
- package/dist/state/capacity/store.d.ts +0 -1
- package/dist/state/capacity/store.js +16 -2
- package/dist/state/capacity/store.js.map +1 -1
- package/dist/state/capacity/types.d.ts +0 -1
- package/dist/state/capacity/types.js.map +1 -1
- package/dist/state/health/index.d.ts +0 -1
- package/dist/state/health/index.js.map +1 -1
- package/dist/state/health/parse.d.ts +0 -1
- package/dist/state/health/parse.js +20 -4
- package/dist/state/health/parse.js.map +1 -1
- package/dist/state/health/store.d.ts +0 -1
- package/dist/state/health/store.js +16 -2
- package/dist/state/health/store.js.map +1 -1
- package/dist/state/health/types.d.ts +0 -1
- package/dist/state/health/types.js.map +1 -1
- package/dist/state/parked-work/index.d.ts +0 -1
- package/dist/state/parked-work/index.js.map +1 -1
- package/dist/state/parked-work/parse.d.ts +0 -1
- package/dist/state/parked-work/parse.js.map +1 -1
- package/dist/state/parked-work/store.d.ts +0 -1
- package/dist/state/parked-work/store.js +20 -3
- package/dist/state/parked-work/store.js.map +1 -1
- package/dist/state/parked-work/types.d.ts +0 -1
- package/dist/state/parked-work/types.js.map +1 -1
- package/dist/state/receipts/bound.d.ts +0 -1
- package/dist/state/receipts/bound.js.map +1 -1
- package/dist/state/receipts/index.d.ts +0 -1
- package/dist/state/receipts/index.js.map +1 -1
- package/dist/state/receipts/parse.d.ts +0 -1
- package/dist/state/receipts/parse.js.map +1 -1
- package/dist/state/receipts/store.d.ts +0 -1
- package/dist/state/receipts/store.js +24 -4
- package/dist/state/receipts/store.js.map +1 -1
- package/dist/state/receipts/types.d.ts +0 -1
- package/dist/state/receipts/types.js.map +1 -1
- package/dist/state/session-modes/index.d.ts +0 -1
- package/dist/state/session-modes/index.js.map +1 -1
- package/dist/state/session-modes/parse.d.ts +0 -1
- package/dist/state/session-modes/parse.js.map +1 -1
- package/dist/state/session-modes/store.d.ts +0 -1
- package/dist/state/session-modes/store.js +15 -1
- package/dist/state/session-modes/store.js.map +1 -1
- package/dist/state/session-modes/types.d.ts +0 -1
- package/dist/state/session-modes/types.js.map +1 -1
- package/dist/state/settings/index.d.ts +0 -1
- package/dist/state/settings/index.js.map +1 -1
- package/dist/state/settings/parse.d.ts +0 -1
- package/dist/state/settings/parse.js.map +1 -1
- package/dist/state/settings/store.d.ts +0 -1
- package/dist/state/settings/store.js +9 -1
- package/dist/state/settings/store.js.map +1 -1
- package/dist/state/settings/types.d.ts +0 -1
- package/dist/state/settings/types.js.map +1 -1
- package/dist/state/tier-requests/index.d.ts +4 -0
- package/dist/state/tier-requests/index.js +4 -0
- package/dist/state/tier-requests/index.js.map +1 -0
- package/dist/state/tier-requests/parse.d.ts +3 -0
- package/dist/state/tier-requests/parse.js +53 -0
- package/dist/state/tier-requests/parse.js.map +1 -0
- package/dist/state/tier-requests/store.d.ts +27 -0
- package/dist/state/tier-requests/store.js +96 -0
- package/dist/state/tier-requests/store.js.map +1 -0
- package/dist/state/tier-requests/types.d.ts +30 -0
- package/dist/state/tier-requests/types.js +7 -0
- package/dist/state/tier-requests/types.js.map +1 -0
- package/dist/supervisor/index.d.ts +0 -1
- package/dist/supervisor/index.js.map +1 -1
- package/dist/supervisor/log.d.ts +0 -1
- package/dist/supervisor/log.js.map +1 -1
- package/dist/supervisor/supervisor.d.ts +23 -6
- package/dist/supervisor/supervisor.js +397 -111
- package/dist/supervisor/supervisor.js.map +1 -1
- package/dist/supervisor/types.d.ts +0 -1
- package/dist/supervisor/types.js.map +1 -1
- package/dist/supervisor/view.d.ts +0 -1
- package/dist/supervisor/view.js.map +1 -1
- package/dist/tui.d.ts +0 -1
- package/dist/tui.js +14 -10
- package/dist/tui.js.map +1 -1
- package/dist/ui/gui/api.d.ts +0 -1
- package/dist/ui/gui/api.js.map +1 -1
- package/dist/ui/gui/view.d.ts +0 -1
- package/dist/ui/gui/view.js.map +1 -1
- package/dist/ui/tui/controller.d.ts +0 -1
- package/dist/ui/tui/controller.js +28 -12
- package/dist/ui/tui/controller.js.map +1 -1
- package/dist/ui/tui/menu.d.ts +1 -2
- package/dist/ui/tui/menu.js +2 -2
- package/dist/ui/tui/menu.js.map +1 -1
- package/dist/ui/tui/options.d.ts +0 -1
- package/dist/ui/tui/options.js.map +1 -1
- package/dist/ui/tui/receipts.d.ts +0 -1
- package/dist/ui/tui/receipts.js +4 -1
- package/dist/ui/tui/receipts.js.map +1 -1
- package/dist/ui/tui/status.d.ts +1 -2
- package/dist/ui/tui/status.js +4 -2
- package/dist/ui/tui/status.js.map +1 -1
- package/dist/ui/tui/tiers.d.ts +0 -1
- package/dist/ui/tui/tiers.js.map +1 -1
- package/package.json +2 -2
- package/dist/adapters/codex-capacity/index.d.ts.map +0 -1
- package/dist/adapters/codex-capacity/parse.d.ts.map +0 -1
- package/dist/adapters/codex-capacity/reading.d.ts.map +0 -1
- package/dist/adapters/codex-capacity/sensor.d.ts.map +0 -1
- package/dist/adapters/codex-capacity/types.d.ts.map +0 -1
- package/dist/adapters/opencode/escalation-tool.d.ts.map +0 -1
- package/dist/adapters/opencode/events.d.ts.map +0 -1
- package/dist/adapters/opencode/hooks.d.ts.map +0 -1
- package/dist/adapters/opencode/index.d.ts.map +0 -1
- package/dist/adapters/opencode/inventory.d.ts.map +0 -1
- package/dist/adapters/opencode/report.d.ts.map +0 -1
- package/dist/adapters/opencode/session-ops.d.ts.map +0 -1
- package/dist/adapters/opencode/storage.d.ts.map +0 -1
- package/dist/adapters/opencode/tier-tool.d.ts.map +0 -1
- package/dist/adapters/opencode/types.d.ts.map +0 -1
- package/dist/core/compatibility/gate.d.ts.map +0 -1
- package/dist/core/compatibility/index.d.ts.map +0 -1
- package/dist/core/compatibility/types.d.ts.map +0 -1
- package/dist/core/scheduler/admit.d.ts.map +0 -1
- package/dist/core/scheduler/escalate.d.ts.map +0 -1
- package/dist/core/scheduler/index.d.ts.map +0 -1
- package/dist/core/scheduler/mode.d.ts.map +0 -1
- package/dist/core/scheduler/types.d.ts.map +0 -1
- package/dist/core/tier-policy/index.d.ts.map +0 -1
- package/dist/core/tier-policy/resolve.d.ts.map +0 -1
- package/dist/core/tier-policy/tier-list.d.ts.map +0 -1
- package/dist/core/tier-policy/types.d.ts.map +0 -1
- package/dist/core/work-unit/index.d.ts.map +0 -1
- package/dist/core/work-unit/interruption.d.ts.map +0 -1
- package/dist/core/work-unit/tracker.d.ts.map +0 -1
- package/dist/core/work-unit/types.d.ts.map +0 -1
- package/dist/execution/continuation/execute.d.ts.map +0 -1
- package/dist/execution/continuation/index.d.ts.map +0 -1
- package/dist/execution/continuation/ledger.d.ts +0 -20
- package/dist/execution/continuation/ledger.d.ts.map +0 -1
- package/dist/execution/continuation/ledger.js +0 -31
- package/dist/execution/continuation/ledger.js.map +0 -1
- package/dist/execution/continuation/plan.d.ts.map +0 -1
- package/dist/execution/continuation/prompts.d.ts.map +0 -1
- package/dist/execution/continuation/types.d.ts.map +0 -1
- package/dist/execution/escalation/index.d.ts.map +0 -1
- package/dist/execution/escalation/types.d.ts.map +0 -1
- package/dist/execution/index.d.ts.map +0 -1
- package/dist/execution/parking/index.d.ts.map +0 -1
- package/dist/execution/parking/plan.d.ts.map +0 -1
- package/dist/execution/recovery/gate.d.ts +0 -20
- package/dist/execution/recovery/gate.d.ts.map +0 -1
- package/dist/execution/recovery/gate.js +0 -23
- package/dist/execution/recovery/gate.js.map +0 -1
- package/dist/execution/recovery/index.d.ts +0 -3
- package/dist/execution/recovery/index.d.ts.map +0 -1
- package/dist/execution/recovery/index.js +0 -2
- package/dist/execution/recovery/index.js.map +0 -1
- package/dist/execution/retry/decide.d.ts.map +0 -1
- package/dist/execution/retry/index.d.ts.map +0 -1
- package/dist/execution/retry/types.d.ts.map +0 -1
- package/dist/gui.d.ts.map +0 -1
- package/dist/health/capacity/index.d.ts.map +0 -1
- package/dist/health/capacity/merge.d.ts.map +0 -1
- package/dist/health/capacity/registry.d.ts.map +0 -1
- package/dist/health/capacity/types.d.ts.map +0 -1
- package/dist/health/circuit-breaker/breaker.d.ts.map +0 -1
- package/dist/health/circuit-breaker/index.d.ts.map +0 -1
- package/dist/health/circuit-breaker/types.d.ts.map +0 -1
- package/dist/health/classifier/classify.d.ts.map +0 -1
- package/dist/health/classifier/index.d.ts.map +0 -1
- package/dist/health/classifier/types.d.ts.map +0 -1
- package/dist/health/index.d.ts.map +0 -1
- package/dist/health/resource-health/index.d.ts.map +0 -1
- package/dist/health/resource-health/key.d.ts.map +0 -1
- package/dist/health/resource-health/registry.d.ts.map +0 -1
- package/dist/health/resource-health/transitions.d.ts.map +0 -1
- package/dist/health/resource-health/types.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/plugin-id.d.ts.map +0 -1
- package/dist/rpc/definition.d.ts.map +0 -1
- package/dist/rpc/handlers.d.ts.map +0 -1
- package/dist/rpc/status.d.ts.map +0 -1
- package/dist/rpc.d.ts.map +0 -1
- package/dist/state/capacity/index.d.ts.map +0 -1
- package/dist/state/capacity/parse.d.ts.map +0 -1
- package/dist/state/capacity/store.d.ts.map +0 -1
- package/dist/state/capacity/types.d.ts.map +0 -1
- package/dist/state/health/index.d.ts.map +0 -1
- package/dist/state/health/parse.d.ts.map +0 -1
- package/dist/state/health/store.d.ts.map +0 -1
- package/dist/state/health/types.d.ts.map +0 -1
- package/dist/state/parked-work/index.d.ts.map +0 -1
- package/dist/state/parked-work/parse.d.ts.map +0 -1
- package/dist/state/parked-work/store.d.ts.map +0 -1
- package/dist/state/parked-work/types.d.ts.map +0 -1
- package/dist/state/receipts/bound.d.ts.map +0 -1
- package/dist/state/receipts/index.d.ts.map +0 -1
- package/dist/state/receipts/parse.d.ts.map +0 -1
- package/dist/state/receipts/store.d.ts.map +0 -1
- package/dist/state/receipts/types.d.ts.map +0 -1
- package/dist/state/session-modes/index.d.ts.map +0 -1
- package/dist/state/session-modes/parse.d.ts.map +0 -1
- package/dist/state/session-modes/store.d.ts.map +0 -1
- package/dist/state/session-modes/types.d.ts.map +0 -1
- package/dist/state/settings/index.d.ts.map +0 -1
- package/dist/state/settings/parse.d.ts.map +0 -1
- package/dist/state/settings/store.d.ts.map +0 -1
- package/dist/state/settings/types.d.ts.map +0 -1
- package/dist/supervisor/index.d.ts.map +0 -1
- package/dist/supervisor/log.d.ts.map +0 -1
- package/dist/supervisor/supervisor.d.ts.map +0 -1
- package/dist/supervisor/types.d.ts.map +0 -1
- package/dist/supervisor/view.d.ts.map +0 -1
- package/dist/tui.d.ts.map +0 -1
- package/dist/ui/gui/api.d.ts.map +0 -1
- package/dist/ui/gui/view.d.ts.map +0 -1
- package/dist/ui/tui/controller.d.ts.map +0 -1
- package/dist/ui/tui/menu.d.ts.map +0 -1
- package/dist/ui/tui/options.d.ts.map +0 -1
- package/dist/ui/tui/receipts.d.ts.map +0 -1
- package/dist/ui/tui/status.d.ts.map +0 -1
- package/dist/ui/tui/tiers.d.ts.map +0 -1
|
@@ -20,13 +20,13 @@ var _a;
|
|
|
20
20
|
* (a later episode supersedes it); no outcome is ever fabricated.
|
|
21
21
|
* - escalation (ADP-015..017) triggers only on capability-classified unit
|
|
22
22
|
* failures (context overflow, content policy); infrastructure failures
|
|
23
|
-
* never escalate, and the per-
|
|
23
|
+
* never escalate, and the per-task budget is finite.
|
|
24
24
|
*/
|
|
25
25
|
import { WorkUnitTracker } from "../core/work-unit/tracker.js";
|
|
26
26
|
import { sameModelRef, } from "../core/work-unit/types.js";
|
|
27
27
|
import { INTERRUPT_REASONS } from "../core/work-unit/interruption.js";
|
|
28
28
|
import { resolveRouting } from "../core/tier-policy/resolve.js";
|
|
29
|
-
import {
|
|
29
|
+
import { isTierName, nextTier } from "../core/tier-policy/types.js";
|
|
30
30
|
import { admit, forMode as schedulerForMode, planEscalation, schedulerHealthKey, } from "../core/scheduler/index.js";
|
|
31
31
|
import { MAX_ESCALATION_EVIDENCE } from "../core/scheduler/types.js";
|
|
32
32
|
import { checkCompatibility } from "../core/compatibility/gate.js";
|
|
@@ -38,6 +38,7 @@ import { mergeReadings } from "../health/capacity/merge.js";
|
|
|
38
38
|
import { resourceChain, resourceKeyID } from "../health/resource-health/key.js";
|
|
39
39
|
import { SettingsStore, SETTINGS_KEY } from "../state/settings/index.js";
|
|
40
40
|
import { SessionModeStore, SESSION_MODES_KEY } from "../state/session-modes/index.js";
|
|
41
|
+
import { TierRequestStore, TIER_REQUESTS_KEY } from "../state/tier-requests/index.js";
|
|
41
42
|
import { ReceiptStore, RECEIPTS_KEY } from "../state/receipts/index.js";
|
|
42
43
|
import { buildStatus } from "../rpc/status.js";
|
|
43
44
|
import { RECEIPTS_VIEW_LIMIT } from "./view.js";
|
|
@@ -48,11 +49,8 @@ import { listCandidates, sessionOps, storageSeam } from "../adapters/opencode/in
|
|
|
48
49
|
import { CodexCapacitySensor } from "../adapters/codex-capacity/index.js";
|
|
49
50
|
import { logFailOpen } from "./log.js";
|
|
50
51
|
import { CODEX_PROVIDER_ID } from "./types.js";
|
|
52
|
+
import { TIERS } from "../core/tier-policy/types.js";
|
|
51
53
|
/** Which tiers currently have configured resources (ADP-005). */
|
|
52
|
-
/** Vocabulary guard without a cast: strict member equality. */
|
|
53
|
-
function isTierName(tier) {
|
|
54
|
-
return TIERS.some((name) => name === tier);
|
|
55
|
-
}
|
|
56
54
|
function configuredTiers(settings) {
|
|
57
55
|
return TIERS.filter((tier) => settings.tiers[tier].length > 0);
|
|
58
56
|
}
|
|
@@ -71,8 +69,20 @@ const CAPABILITY_EVIDENCE_CATEGORIES = [
|
|
|
71
69
|
"context_overflow",
|
|
72
70
|
"content_policy",
|
|
73
71
|
];
|
|
74
|
-
/** Finite per-
|
|
75
|
-
const
|
|
72
|
+
/** Finite per-task escalation budget (ADP-017). */
|
|
73
|
+
const MAX_ESCALATIONS_PER_TASK = 2;
|
|
74
|
+
/**
|
|
75
|
+
* The recovery's post-settle guard aborted it (the session went OFF or the
|
|
76
|
+
* user interrupted while the host settle was in flight). Callers use it to
|
|
77
|
+
* skip the ordinary ops-failure degradation (parking) — an aborted recovery
|
|
78
|
+
* is a steering change, not an infrastructure failure.
|
|
79
|
+
*/
|
|
80
|
+
export class RecoveryAbortedError extends Error {
|
|
81
|
+
constructor(sessionID) {
|
|
82
|
+
super(`recovery aborted for session ${sessionID}`);
|
|
83
|
+
this.name = "RecoveryAbortedError";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
76
86
|
export class Supervisor {
|
|
77
87
|
#options;
|
|
78
88
|
#settings;
|
|
@@ -86,6 +96,12 @@ export class Supervisor {
|
|
|
86
96
|
#capacity;
|
|
87
97
|
#codex;
|
|
88
98
|
#vetoes = new Map();
|
|
99
|
+
/**
|
|
100
|
+
* Escalations spent per session on the current task (one user prompt and
|
|
101
|
+
* the owned continuations it spawns). Keyed by session, not unit: every
|
|
102
|
+
* escalation continues on a NEW unit, so a per-unit count would restart
|
|
103
|
+
* the budget at each step. A fresh user prompt starts a new task.
|
|
104
|
+
*/
|
|
89
105
|
#escalations = new Map();
|
|
90
106
|
#admissionTiers = new Map();
|
|
91
107
|
/** Recorded escalation-tool requests per unit (ADP-015 agent self-reports). */
|
|
@@ -100,10 +116,20 @@ export class Supervisor {
|
|
|
100
116
|
* Per-session tier requests (agent-declared via the adaptive_tier tool):
|
|
101
117
|
* the request is STICKY — it steers the session's next and following
|
|
102
118
|
* admissions until cleared or the session dies — because an agent works
|
|
103
|
-
* across many units in one task.
|
|
119
|
+
* across many units in one task. Persisted like the session-mode
|
|
120
|
+
* overrides: the host's double plugin instance (hooks vs RPC) must not
|
|
121
|
+
* strand a request on the instance that never routes.
|
|
104
122
|
*/
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
#tierRequests;
|
|
124
|
+
/**
|
|
125
|
+
* Sessions whose last model change was the user's own pick
|
|
126
|
+
* (`selected-external`): prompt admission does not re-route them —
|
|
127
|
+
* switching would undo the user's `/model` choice (ADP-028). Cleared by an
|
|
128
|
+
* adaptive switch, a tier request, or a mode change; a standing settings
|
|
129
|
+
* pin overrides the suppression (the pin is manual intent too).
|
|
130
|
+
*/
|
|
131
|
+
static MAX_MANUAL_SELECTIONS = 1024;
|
|
132
|
+
#manualSelections = new Map();
|
|
107
133
|
/**
|
|
108
134
|
* Sessions with an owned continuation prompt in flight: the prompt hook
|
|
109
135
|
* will fire for it, but that prompt belongs to the active unit — it must
|
|
@@ -119,6 +145,15 @@ export class Supervisor {
|
|
|
119
145
|
* make the continuation re-admit (admission ping-pong).
|
|
120
146
|
*/
|
|
121
147
|
#continuationsInFlight = new Map();
|
|
148
|
+
/**
|
|
149
|
+
* Sessions with an owned recovery in flight (between veto/resume and the
|
|
150
|
+
* delivered continuation prompt). The host settle inside it can take
|
|
151
|
+
* unbounded time; an interruption arriving during that window must abort
|
|
152
|
+
* the stale decision instead of acting after it.
|
|
153
|
+
*/
|
|
154
|
+
#recoveryPending = new Set();
|
|
155
|
+
/** Interruptions observed while a recovery was pending on the session. */
|
|
156
|
+
#recoveryInterrupted = new Set();
|
|
122
157
|
/**
|
|
123
158
|
* Half-open probe tickets by session: a probe admitted while computing the
|
|
124
159
|
* recovery for a veto belongs to the continuation episode that follows, so
|
|
@@ -132,6 +167,21 @@ export class Supervisor {
|
|
|
132
167
|
* it — otherwise a lost probe would wedge the resource in half_open.
|
|
133
168
|
*/
|
|
134
169
|
#probeTickets = new Map();
|
|
170
|
+
/**
|
|
171
|
+
* Per-unit dedup for breaker failure evidence (ADP-019). One failed attempt
|
|
172
|
+
* surfaces through up to three host signals — the pumped `http.response`
|
|
173
|
+
* event, the `retry` hook (the same request), and the terminal
|
|
174
|
+
* `execution-failed` (the last attempt) — and counting each would trip the
|
|
175
|
+
* breaker after ~2 real failures instead of `failureThreshold`. The exact
|
|
176
|
+
* arrival order of a pair is not guaranteed (the retry hook runs in the
|
|
177
|
+
* request lifecycle, the event pump lags), so the dedup is order-symmetric:
|
|
178
|
+
* the first signal of a pair counts and arms, the matching counterpart
|
|
179
|
+
* consumes it, a completed pair closes the attempt, and the terminal
|
|
180
|
+
* repeats only when the last attempt left no live evidence. A same-category
|
|
181
|
+
* observation with nothing armed is a NEW attempt (consecutive http-only
|
|
182
|
+
* failures each count). Cleared at unit close.
|
|
183
|
+
*/
|
|
184
|
+
#attemptFailures = new Map();
|
|
135
185
|
/**
|
|
136
186
|
* Session → tier admitted by the prompt hook for the unit that is about to
|
|
137
187
|
* open. Consumed by execution-started, which then skips re-routing (the
|
|
@@ -162,6 +212,7 @@ export class Supervisor {
|
|
|
162
212
|
this.#healthRecords = new HealthRecordStore(storageSeam(HEALTH_KEY, options.storage));
|
|
163
213
|
this.#capacityStore = new CapacityReadingStore(storageSeam(CAPACITY_KEY, options.storage));
|
|
164
214
|
this.#sessionModes = new SessionModeStore(storageSeam(SESSION_MODES_KEY, options.storage), options.clock);
|
|
215
|
+
this.#tierRequests = new TierRequestStore(storageSeam(TIER_REQUESTS_KEY, options.storage), options.clock);
|
|
165
216
|
this.#ops = sessionOps(options.session, {
|
|
166
217
|
beforeSwitch: (sessionID, model) => {
|
|
167
218
|
this.#tracker.noteOwnSwitch(sessionID, model);
|
|
@@ -234,6 +285,10 @@ export class Supervisor {
|
|
|
234
285
|
// host's double-instance reconciliation) must keep steering sessions
|
|
235
286
|
// under the modes the user set for them.
|
|
236
287
|
await this.#sessionModes.load();
|
|
288
|
+
// Tier requests hydrate the same way: a tier request recorded through
|
|
289
|
+
// the host's other plugin instance (or before a restart) must still
|
|
290
|
+
// steer this instance's admissions.
|
|
291
|
+
await this.#tierRequests.load();
|
|
237
292
|
await this.#capacityStore.load();
|
|
238
293
|
return this.#settings.load();
|
|
239
294
|
};
|
|
@@ -270,6 +325,27 @@ export class Supervisor {
|
|
|
270
325
|
logFailOpen("report", error);
|
|
271
326
|
}
|
|
272
327
|
}
|
|
328
|
+
/** Marks the session's manual-selection state from an external selection event. */
|
|
329
|
+
#noteManualSelection(sessionID) {
|
|
330
|
+
if (!this.#manualSelections.has(sessionID) &&
|
|
331
|
+
this.#manualSelections.size >= _a.MAX_MANUAL_SELECTIONS) {
|
|
332
|
+
// The guard above ensures the map is at its cap (>= 1 entries), so the
|
|
333
|
+
// first key always exists; the assertion avoids a branch the coverage
|
|
334
|
+
// gate would reject as unreachable.
|
|
335
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
336
|
+
const oldest = this.#manualSelections.keys().next().value;
|
|
337
|
+
this.#manualSelections.delete(oldest);
|
|
338
|
+
}
|
|
339
|
+
this.#manualSelections.set(sessionID, true);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* True while the session's last model change was the user's own pick.
|
|
343
|
+
* A standing settings pin is explicit manual intent as well (ADP-002):
|
|
344
|
+
* honoring it does not fight the pick, so suppression is skipped there.
|
|
345
|
+
*/
|
|
346
|
+
#manuallySelected(sessionID, settings) {
|
|
347
|
+
return settings.pin === null && this.#manualSelections.has(sessionID);
|
|
348
|
+
}
|
|
273
349
|
/**
|
|
274
350
|
* Handle one `retry` hook invocation. On a veto this mutates
|
|
275
351
|
* `input.decision` to `{ retry: false }`; otherwise the decision is left
|
|
@@ -308,33 +384,38 @@ export class Supervisor {
|
|
|
308
384
|
* tool). STICKY: it steers the session's next and following admissions
|
|
309
385
|
* until cleared — an agent works across many units in one task. Returns
|
|
310
386
|
* false on an unknown tier (the tool re-validates before calling).
|
|
387
|
+
*
|
|
388
|
+
* Fire-and-forget persistence: the recorder signature is sync on both the
|
|
389
|
+
* tool and the RPC source interfaces, so the write is enqueued without
|
|
390
|
+
* awaiting it. Readers (routing, getTierRequest, the idle sweep) flush the
|
|
391
|
+
* store's queue before reading, which makes the record visible
|
|
392
|
+
* deterministically; a write failure drops the record fail-open (the tool
|
|
393
|
+
* told the agent "recorded", the persisted mirror simply never sees it —
|
|
394
|
+
* the same degradation the in-memory map had on restart).
|
|
311
395
|
*/
|
|
312
396
|
noteTierRequest(sessionID, tier) {
|
|
313
397
|
if (!isTierName(tier))
|
|
314
398
|
return false;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// preserves insertion order, so the first key is the oldest request.
|
|
319
|
-
for (const oldest of this.#tierRequests.keys()) {
|
|
320
|
-
this.#tierRequests.delete(oldest);
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
this.#tierRequests.set(sessionID, { tier, via: "manual-command" });
|
|
399
|
+
void this.#tierRequests.set(sessionID, tier).catch(() => undefined);
|
|
400
|
+
// The user/agent explicitly asked Adaptive to route again (ADP-028).
|
|
401
|
+
this.#manualSelections.delete(sessionID);
|
|
325
402
|
return true;
|
|
326
403
|
}
|
|
327
404
|
/** The session's tier request, or `null` when it follows the routing ladder. */
|
|
328
405
|
async getTierRequest(sessionID) {
|
|
329
406
|
await this.#ready;
|
|
330
|
-
|
|
407
|
+
// A fire-and-forget note may still be in the queue: flush before reading.
|
|
408
|
+
await this.#tierRequests.flush();
|
|
409
|
+
return this.#tierRequests.get(sessionID);
|
|
331
410
|
}
|
|
332
411
|
/** Clear one session's tier request (`null` is the only valid input). */
|
|
333
412
|
async setTierRequest(sessionID, tier) {
|
|
334
413
|
await this.#ready;
|
|
335
414
|
if (tier !== null)
|
|
336
415
|
throw new Error("only null clears a tier request");
|
|
337
|
-
this.#tierRequests.
|
|
416
|
+
await this.#tierRequests.set(sessionID, null);
|
|
417
|
+
// Clearing through the control surface is the user asking Adaptive back.
|
|
418
|
+
this.#manualSelections.delete(sessionID);
|
|
338
419
|
}
|
|
339
420
|
// ---------- state API for the control surfaces (ADP-033/034/036) ----------
|
|
340
421
|
/** Current settings (awaits the constructor load). */
|
|
@@ -344,7 +425,13 @@ export class Supervisor {
|
|
|
344
425
|
/** Validate and persist a settings patch; returns per-field errors. */
|
|
345
426
|
async updateSettings(patch) {
|
|
346
427
|
await this.#ready;
|
|
347
|
-
|
|
428
|
+
const result = await this.#settings.update(patch);
|
|
429
|
+
if (result.ok && patch.mode !== undefined) {
|
|
430
|
+
// A global mode change is the user steering Adaptive explicitly
|
|
431
|
+
// (ADP-028): manual-selection suppression lifts for every session.
|
|
432
|
+
this.#manualSelections.clear();
|
|
433
|
+
}
|
|
434
|
+
return result;
|
|
348
435
|
}
|
|
349
436
|
/** The session's mode override, or `null` when it follows the global mode. */
|
|
350
437
|
async getSessionMode(sessionID) {
|
|
@@ -359,6 +446,8 @@ export class Supervisor {
|
|
|
359
446
|
async setSessionMode(sessionID, mode) {
|
|
360
447
|
await this.#ready;
|
|
361
448
|
await this.#sessionModes.set(sessionID, mode);
|
|
449
|
+
// A mode change is the user steering Adaptive explicitly (ADP-028).
|
|
450
|
+
this.#manualSelections.delete(sessionID);
|
|
362
451
|
}
|
|
363
452
|
/**
|
|
364
453
|
* Bounded status snapshot for the status view (ADP-033). Health is read
|
|
@@ -416,14 +505,24 @@ export class Supervisor {
|
|
|
416
505
|
case "execution-interrupted":
|
|
417
506
|
await this.#onExecutionInterrupted(report);
|
|
418
507
|
return;
|
|
419
|
-
case "model-selected":
|
|
420
|
-
this.#tracker.apply({
|
|
508
|
+
case "model-selected": {
|
|
509
|
+
const result = this.#tracker.apply({
|
|
421
510
|
kind: "model-selected",
|
|
422
511
|
sessionID: report.sessionID,
|
|
423
512
|
at: report.at,
|
|
424
513
|
model: report.model,
|
|
425
514
|
});
|
|
515
|
+
// Whether the change was the user's pick or Adaptive's own switch
|
|
516
|
+
// decides who controls the session's model (ADP-028); the result
|
|
517
|
+
// carries the attribution the tracker resolved. Only the two
|
|
518
|
+
// selection sources reach this event kind.
|
|
519
|
+
const { source } = result.transition;
|
|
520
|
+
if (source === "selected-adaptive")
|
|
521
|
+
this.#manualSelections.delete(report.sessionID);
|
|
522
|
+
else
|
|
523
|
+
this.#noteManualSelection(report.sessionID);
|
|
426
524
|
return;
|
|
525
|
+
}
|
|
427
526
|
case "model-request":
|
|
428
527
|
if (report.agent !== undefined) {
|
|
429
528
|
if (!this.#agents.has(report.sessionID) &&
|
|
@@ -437,12 +536,20 @@ export class Supervisor {
|
|
|
437
536
|
}
|
|
438
537
|
this.#agents.set(report.sessionID, report.agent);
|
|
439
538
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
539
|
+
// Only the agent loop's own request reports the model actually
|
|
540
|
+
// executing the unit's work. An auxiliary request (title, compaction,
|
|
541
|
+
// generate — ADP-027) carries the host's own cheap pick for that side
|
|
542
|
+
// request; adopting it as the session's model would misattribute the
|
|
543
|
+
// primary unit's health evidence and leave the next admission
|
|
544
|
+
// believing the unit runs on the wrong resource.
|
|
545
|
+
if (report.requestKind === "primary") {
|
|
546
|
+
this.#tracker.apply({
|
|
547
|
+
kind: "model-request",
|
|
548
|
+
sessionID: report.sessionID,
|
|
549
|
+
at: this.#options.clock(),
|
|
550
|
+
model: report.model,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
446
553
|
return;
|
|
447
554
|
case "http-response":
|
|
448
555
|
await this.#onHttpResponse(report);
|
|
@@ -481,14 +588,25 @@ export class Supervisor {
|
|
|
481
588
|
// this admission re-routes from scratch.
|
|
482
589
|
this.#continuationTiers.delete(sessionID);
|
|
483
590
|
this.#promptAdmission.delete(sessionID);
|
|
591
|
+
this.#escalations.delete(sessionID);
|
|
484
592
|
// A pending probe ticket from an earlier prompt admission (or a failed
|
|
485
593
|
// recovery) can only be carried by the unit the ORIGINAL prompt opens;
|
|
486
|
-
// a fresh admission means that episode never ran, so the ticket
|
|
487
|
-
// here instead of being adopted by whichever unit opens next.
|
|
594
|
+
// a fresh admission means that episode never ran, so the ticket is
|
|
595
|
+
// released here instead of being adopted by whichever unit opens next.
|
|
488
596
|
const stale = this.#sessionProbeTickets.get(sessionID);
|
|
489
597
|
if (stale !== undefined) {
|
|
490
598
|
this.#sessionProbeTickets.delete(sessionID);
|
|
491
|
-
this.#
|
|
599
|
+
this.#health.releaseProbe(stale);
|
|
600
|
+
}
|
|
601
|
+
if (this.#manuallySelected(sessionID, settings)) {
|
|
602
|
+
// ADP-028: the user's own model pick stands. Recording the defer (no
|
|
603
|
+
// switch, no marker) leaves OpenCode's choice — the picked model — in
|
|
604
|
+
// place; execution-started will skip routing the same way.
|
|
605
|
+
await this.#recordReceipt({
|
|
606
|
+
sessionID,
|
|
607
|
+
decision: { kind: "defer", reason: "routing-defer", detail: "manual-selection" },
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
492
610
|
}
|
|
493
611
|
let probeTicket;
|
|
494
612
|
try {
|
|
@@ -513,9 +631,9 @@ export class Supervisor {
|
|
|
513
631
|
}
|
|
514
632
|
catch (error) {
|
|
515
633
|
// A probe admitted for this turn is lost when the admission fails (the
|
|
516
|
-
// switch threw): no unit will ever carry the ticket, so
|
|
634
|
+
// switch threw): no unit will ever carry the ticket, so release it.
|
|
517
635
|
if (probeTicket !== undefined) {
|
|
518
|
-
this.#
|
|
636
|
+
this.#health.releaseProbe(probeTicket);
|
|
519
637
|
}
|
|
520
638
|
// Fail-open: no marker → execution-started re-routes the unit itself.
|
|
521
639
|
logFailOpen("admission", error);
|
|
@@ -582,14 +700,22 @@ export class Supervisor {
|
|
|
582
700
|
}
|
|
583
701
|
}
|
|
584
702
|
/** Fresh admission: routing ladder → scheduler, raw (unwrapped). */
|
|
585
|
-
async #admit(settings, sessionID) {
|
|
703
|
+
async #admit(settings, sessionID, requiredTier) {
|
|
704
|
+
// The agent-declared tier request (adaptive_tier tool) outranks the
|
|
705
|
+
// role hints but stays below the pin (ADP-002 ladder). The note is
|
|
706
|
+
// recorded fire-and-forget, so flush the store's queue before reading.
|
|
707
|
+
await this.#tierRequests.flush();
|
|
708
|
+
const tierRequest = this.#tierRequests.get(sessionID);
|
|
709
|
+
let routedTier;
|
|
710
|
+
if (requiredTier !== undefined) {
|
|
711
|
+
routedTier = { tier: requiredTier, via: "manual-command" };
|
|
712
|
+
}
|
|
713
|
+
else if (tierRequest !== null) {
|
|
714
|
+
routedTier = { tier: tierRequest, via: "manual-command" };
|
|
715
|
+
}
|
|
586
716
|
const routing = resolveRouting({
|
|
587
717
|
...(settings.pin !== null ? { pin: settings.pin } : {}),
|
|
588
|
-
|
|
589
|
-
// role hints but stays below the pin (ADP-002 ladder).
|
|
590
|
-
...(this.#tierRequests.has(sessionID)
|
|
591
|
-
? { tierRequest: this.#tierRequests.get(sessionID) }
|
|
592
|
-
: {}),
|
|
718
|
+
...(routedTier !== undefined ? { tierRequest: routedTier } : {}),
|
|
593
719
|
// The agent-role hint is the session's last observed agent — the host
|
|
594
720
|
// reports it on every model request (ADP-002 level 3).
|
|
595
721
|
...(this.#agents.has(sessionID) ? { agent: this.#agents.get(sessionID) } : {}),
|
|
@@ -603,10 +729,13 @@ export class Supervisor {
|
|
|
603
729
|
await this.#capacity.refresh();
|
|
604
730
|
const health = {};
|
|
605
731
|
const capacity = {};
|
|
606
|
-
// Tickets issued by this assessment sweep (see the
|
|
732
|
+
// Tickets issued by this assessment sweep (see the release below).
|
|
607
733
|
const issued = [];
|
|
608
|
-
|
|
609
|
-
|
|
734
|
+
// Only the routed candidates are assessed: assess flips a due breaker to
|
|
735
|
+
// half_open, and admitting a quick unit has no business touching an
|
|
736
|
+
// advanced-tier resource's breaker.
|
|
737
|
+
const routed = routing.kind === "pin" ? [routing.model] : tierCandidates;
|
|
738
|
+
for (const ref of routed) {
|
|
610
739
|
const mapKey = schedulerHealthKey(ref);
|
|
611
740
|
const assessment = this.#health.assess(refKey(ref));
|
|
612
741
|
health[mapKey] = assessment;
|
|
@@ -630,12 +759,12 @@ export class Supervisor {
|
|
|
630
759
|
};
|
|
631
760
|
const decision = admit(input);
|
|
632
761
|
// Assess admits a half-open probe for every due breaker it touches —
|
|
633
|
-
// including candidates this decision passed over (
|
|
634
|
-
// or dispatched nothing for (defer/park). An
|
|
635
|
-
// probe would wedge its resource in half_open:
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
//
|
|
762
|
+
// including candidates this decision passed over (an earlier ready
|
|
763
|
+
// candidate wins) or dispatched nothing for (defer/park). An
|
|
764
|
+
// issued-but-undispatched probe would wedge its resource in half_open:
|
|
765
|
+
// no unit ever carries the ticket. Release each unselected ticket — it
|
|
766
|
+
// stays due, because a probe that never ran is no health evidence and
|
|
767
|
+
// restarting its backoff would push it back on every admission.
|
|
639
768
|
const selectedId = decision.kind === "apply" ? decision.evidence.probeId : undefined;
|
|
640
769
|
let probeTicket;
|
|
641
770
|
for (const ticket of issued) {
|
|
@@ -643,12 +772,21 @@ export class Supervisor {
|
|
|
643
772
|
probeTicket = ticket;
|
|
644
773
|
}
|
|
645
774
|
else {
|
|
646
|
-
this.#health.
|
|
775
|
+
this.#health.releaseProbe(ticket);
|
|
647
776
|
}
|
|
648
777
|
}
|
|
649
778
|
return { routing, decision, ...(probeTicket !== undefined ? { probeTicket } : {}) };
|
|
650
779
|
}
|
|
651
780
|
async #routeAdmission(unit, settings, mode) {
|
|
781
|
+
if (this.#manuallySelected(unit.sessionID, settings)) {
|
|
782
|
+
// ADP-028: the session runs on the user's own pick — no re-route.
|
|
783
|
+
await this.#recordReceipt({
|
|
784
|
+
sessionID: unit.sessionID,
|
|
785
|
+
unitID: unit.id,
|
|
786
|
+
decision: { kind: "defer", reason: "routing-defer", detail: "manual-selection" },
|
|
787
|
+
});
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
652
790
|
const { routing, decision, probeTicket } = await this.#admit(settings, unit.sessionID);
|
|
653
791
|
// The tier the unit was admitted under is escalation context (ADP-015);
|
|
654
792
|
// recorded for every tiered routing outcome (including a defer, which
|
|
@@ -663,9 +801,9 @@ export class Supervisor {
|
|
|
663
801
|
}
|
|
664
802
|
catch (error) {
|
|
665
803
|
// The episode cannot carry the probe when the switch itself failed —
|
|
666
|
-
//
|
|
804
|
+
// release the ticket instead of wedging the resource half-open.
|
|
667
805
|
if (probeTicket !== undefined) {
|
|
668
|
-
this.#
|
|
806
|
+
this.#health.releaseProbe(probeTicket);
|
|
669
807
|
}
|
|
670
808
|
throw error;
|
|
671
809
|
}
|
|
@@ -675,8 +813,8 @@ export class Supervisor {
|
|
|
675
813
|
}
|
|
676
814
|
if (probeTicket !== undefined) {
|
|
677
815
|
// OBSERVE wraps an apply — including a probe admission — without
|
|
678
|
-
// running it: no episode will ever report the ticket, so
|
|
679
|
-
this.#
|
|
816
|
+
// running it: no episode will ever report the ticket, so release it.
|
|
817
|
+
this.#health.releaseProbe(probeTicket);
|
|
680
818
|
}
|
|
681
819
|
// Observe-only records the identical decision; defer and park leave the
|
|
682
820
|
// episode on OpenCode's own model (admission cannot hold an episode).
|
|
@@ -702,7 +840,7 @@ export class Supervisor {
|
|
|
702
840
|
async #onExecutionSucceeded(report) {
|
|
703
841
|
const unit = this.#tracker.active(report.sessionID);
|
|
704
842
|
const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
|
|
705
|
-
const probeTicket =
|
|
843
|
+
const probeTicket = this.#takeProbeTicket(unit?.id);
|
|
706
844
|
const closed = this.#tracker.apply({
|
|
707
845
|
kind: "execution-succeeded",
|
|
708
846
|
sessionID: report.sessionID,
|
|
@@ -714,7 +852,6 @@ export class Supervisor {
|
|
|
714
852
|
}
|
|
715
853
|
if (unit !== undefined) {
|
|
716
854
|
if (probeTicket !== undefined) {
|
|
717
|
-
this.#probeTickets.delete(unit.id);
|
|
718
855
|
this.#reportProbeOutcome(probeTicket, { success: true });
|
|
719
856
|
}
|
|
720
857
|
await this.#attachOutcome(unit, { status: "succeeded" });
|
|
@@ -724,19 +861,16 @@ export class Supervisor {
|
|
|
724
861
|
const unit = this.#tracker.active(report.sessionID);
|
|
725
862
|
const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
|
|
726
863
|
const unitID = unit?.id;
|
|
727
|
-
//
|
|
864
|
+
// The tier is read before the close clears the per-unit maps.
|
|
728
865
|
const admissionTier = unitID !== undefined ? this.#admissionTiers.get(unitID) : undefined;
|
|
729
|
-
const escalationUsed =
|
|
866
|
+
const escalationUsed = this.#escalations.get(report.sessionID) ?? 0;
|
|
730
867
|
// Tool evidence is captured pre-close like the other per-unit maps.
|
|
731
868
|
const toolEvidence = unitID !== undefined ? (this.#escalationEvidence.get(unitID) ?? []) : [];
|
|
732
|
-
const probeTicket =
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
error: report.error,
|
|
738
|
-
});
|
|
739
|
-
this.#afterUnitClose(closed);
|
|
869
|
+
const probeTicket = this.#takeProbeTicket(unitID);
|
|
870
|
+
// The terminal classifies and dedups BEFORE the close: the close clears
|
|
871
|
+
// the unit's attempt-failure entry, and the terminal must consult it —
|
|
872
|
+
// it repeats the episode's last attempt, which an attempt signal
|
|
873
|
+
// (http-response or retry hook) already counted.
|
|
740
874
|
const classification = classifyFailure({
|
|
741
875
|
structured: {
|
|
742
876
|
type: report.error.type,
|
|
@@ -750,13 +884,20 @@ export class Supervisor {
|
|
|
750
884
|
// The retry hook deliberately omits text: it must stay conservative.
|
|
751
885
|
text: report.error.message,
|
|
752
886
|
});
|
|
753
|
-
|
|
887
|
+
const countTerminal = this.#countTerminalFailure(unitID, classification.category);
|
|
888
|
+
const closed = this.#tracker.apply({
|
|
889
|
+
kind: "execution-failed",
|
|
890
|
+
sessionID: report.sessionID,
|
|
891
|
+
at: report.at,
|
|
892
|
+
error: report.error,
|
|
893
|
+
});
|
|
894
|
+
this.#afterUnitClose(closed);
|
|
895
|
+
if (model !== undefined && countTerminal) {
|
|
754
896
|
this.#observeHealth(failureEvent(refKey(model), classification));
|
|
755
897
|
}
|
|
756
898
|
if (unit !== undefined) {
|
|
757
899
|
await this.#attachOutcome(unit, { status: "failed", category: classification.category });
|
|
758
900
|
if (probeTicket !== undefined) {
|
|
759
|
-
this.#probeTickets.delete(unit.id);
|
|
760
901
|
this.#reportProbeOutcome(probeTicket, { success: false, category: classification.category });
|
|
761
902
|
}
|
|
762
903
|
}
|
|
@@ -783,8 +924,11 @@ export class Supervisor {
|
|
|
783
924
|
return [];
|
|
784
925
|
}
|
|
785
926
|
const eligible = [];
|
|
927
|
+
// Compared by resource key: the host reports base-model calls as variant
|
|
928
|
+
// "default", which is the same resource as the bare tier entry.
|
|
929
|
+
const failedID = resourceKeyID(refKey(failed));
|
|
786
930
|
for (const ref of refs) {
|
|
787
|
-
if (
|
|
931
|
+
if (resourceKeyID(refKey(ref)) === failedID)
|
|
788
932
|
continue;
|
|
789
933
|
const candidate = inventory.find((entry) => sameModelRef(candidateRef(entry), ref));
|
|
790
934
|
if (candidate === undefined)
|
|
@@ -822,11 +966,11 @@ export class Supervisor {
|
|
|
822
966
|
currentTier: admissionTier,
|
|
823
967
|
nextTierResources: eligible,
|
|
824
968
|
evidence,
|
|
825
|
-
budget: { used: escalationUsed, max:
|
|
969
|
+
budget: { used: escalationUsed, max: MAX_ESCALATIONS_PER_TASK },
|
|
826
970
|
});
|
|
827
971
|
const wrapped = schedulerForMode(plan, this.#modeFor(report.sessionID, settings));
|
|
828
972
|
if (wrapped.kind === "escalate") {
|
|
829
|
-
this.#escalations.set(
|
|
973
|
+
this.#escalations.set(report.sessionID, escalationUsed + 1);
|
|
830
974
|
// Recorded unit-less on purpose: the receipt describes the CONTINUATION
|
|
831
975
|
// decision, so the continuation unit adopts it and its outcome attaches
|
|
832
976
|
// here (the failed unit's own outcome attaches to its apply receipt).
|
|
@@ -838,10 +982,11 @@ export class Supervisor {
|
|
|
838
982
|
// Continue the interrupted work on the escalated resource; the unit is
|
|
839
983
|
// terminally failed, so an ops failure is only logged (never parked).
|
|
840
984
|
try {
|
|
841
|
-
await this.#ownRecovery(report.sessionID, wrapped.target, wrapped.tier);
|
|
985
|
+
await this.#ownRecovery(report.sessionID, wrapped.target, wrapped.tier, () => this.#modeFor(report.sessionID, settings) !== "off");
|
|
842
986
|
}
|
|
843
987
|
catch (error) {
|
|
844
|
-
|
|
988
|
+
if (!(error instanceof RecoveryAbortedError))
|
|
989
|
+
logFailOpen("escalation-ops", error);
|
|
845
990
|
}
|
|
846
991
|
return;
|
|
847
992
|
}
|
|
@@ -858,7 +1003,13 @@ export class Supervisor {
|
|
|
858
1003
|
return;
|
|
859
1004
|
}
|
|
860
1005
|
const unit = this.#tracker.active(report.sessionID);
|
|
861
|
-
const probeTicket =
|
|
1006
|
+
const probeTicket = this.#takeProbeTicket(unit?.id);
|
|
1007
|
+
// An interruption while a recovery is pending on this session aborts it
|
|
1008
|
+
// at its post-settle guard (the pending recovery's decision predates the
|
|
1009
|
+
// interruption and must not act after it).
|
|
1010
|
+
if (this.#recoveryPending.has(report.sessionID)) {
|
|
1011
|
+
this.#recoveryInterrupted.add(report.sessionID);
|
|
1012
|
+
}
|
|
862
1013
|
const closed = this.#tracker.apply({
|
|
863
1014
|
kind: "execution-interrupted",
|
|
864
1015
|
sessionID: report.sessionID,
|
|
@@ -872,7 +1023,6 @@ export class Supervisor {
|
|
|
872
1023
|
// An interrupted probe is no evidence about resource health: re-arm the
|
|
873
1024
|
// breaker's same window rather than doubling it.
|
|
874
1025
|
if (probeTicket !== undefined) {
|
|
875
|
-
this.#probeTickets.delete(unit.id);
|
|
876
1026
|
this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
|
|
877
1027
|
}
|
|
878
1028
|
await this.#attachOutcome(unit, { status: "interrupted", reason });
|
|
@@ -889,23 +1039,81 @@ export class Supervisor {
|
|
|
889
1039
|
// unknown ticket) are reported by its boolean return, not by throwing.
|
|
890
1040
|
this.#health.reportProbe(ticket, outcome);
|
|
891
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* One attempt's failure evidence (http-response event or retry hook):
|
|
1044
|
+
* returns whether THIS observation is the attempt's first count.
|
|
1045
|
+
*/
|
|
1046
|
+
#countAttemptFailure(unitID, source, category) {
|
|
1047
|
+
if (unitID === undefined)
|
|
1048
|
+
return true;
|
|
1049
|
+
const ev = this.#attemptFailures.get(unitID) ?? {};
|
|
1050
|
+
if (source === "http" ? ev.retry === category : ev.http === category) {
|
|
1051
|
+
// The other host signal already counted this attempt: consume it.
|
|
1052
|
+
const closed = { ...ev, http: undefined, retry: undefined, closed: category };
|
|
1053
|
+
this.#attemptFailures.set(unitID, closed);
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
this.#attemptFailures.set(unitID, source === "http" ? { ...ev, http: category } : { ...ev, retry: category });
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* The terminal `execution-failed` repeats the episode's last attempt: it
|
|
1061
|
+
* counts only when the last attempt left no live evidence (e.g. a
|
|
1062
|
+
* capability failure that never produced an HTTP response or retry hook).
|
|
1063
|
+
*/
|
|
1064
|
+
#countTerminalFailure(unitID, category) {
|
|
1065
|
+
if (unitID === undefined)
|
|
1066
|
+
return true;
|
|
1067
|
+
const ev = this.#attemptFailures.get(unitID) ?? {};
|
|
1068
|
+
if (ev.http === category) {
|
|
1069
|
+
this.#attemptFailures.set(unitID, { ...ev, http: undefined });
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
if (ev.retry === category) {
|
|
1073
|
+
this.#attemptFailures.set(unitID, { ...ev, retry: undefined });
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
if (ev.closed === category) {
|
|
1077
|
+
this.#attemptFailures.set(unitID, { ...ev, closed: undefined });
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
this.#attemptFailures.set(unitID, { ...ev, closed: category });
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Hands a closing unit's probe ticket to its explicit outcome reporter.
|
|
1085
|
+
* Taken BEFORE the close: #afterUnitClose treats any ticket still held as
|
|
1086
|
+
* lost and re-opens the breaker, which would turn a probe's real outcome
|
|
1087
|
+
* into a stale report.
|
|
1088
|
+
*/
|
|
1089
|
+
#takeProbeTicket(unitID) {
|
|
1090
|
+
if (unitID === undefined)
|
|
1091
|
+
return undefined;
|
|
1092
|
+
const ticket = this.#probeTickets.get(unitID);
|
|
1093
|
+
this.#probeTickets.delete(unitID);
|
|
1094
|
+
return ticket;
|
|
1095
|
+
}
|
|
892
1096
|
/** Bounded per-unit bookkeeping once the tracker closed the unit. */
|
|
893
1097
|
#afterUnitClose(closed) {
|
|
894
1098
|
const unitID = closedUnitID(closed);
|
|
895
1099
|
if (unitID !== undefined) {
|
|
896
1100
|
this.#vetoes.delete(unitID);
|
|
897
|
-
this.#escalations.delete(unitID);
|
|
898
1101
|
this.#admissionTiers.delete(unitID);
|
|
899
1102
|
this.#escalationEvidence.delete(unitID);
|
|
900
1103
|
const leftover = this.#probeTickets.get(unitID);
|
|
901
1104
|
if (leftover !== undefined) {
|
|
902
|
-
// The explicit outcome reporters
|
|
903
|
-
// here means the unit ended without one
|
|
1105
|
+
// The explicit outcome reporters take their ticket before this
|
|
1106
|
+
// cleanup; reaching here means the unit ended without one
|
|
1107
|
+
// (superseded or lost).
|
|
904
1108
|
this.#reportProbeOutcome(leftover, { success: false, category: "user_cancelled" });
|
|
905
1109
|
this.#probeTickets.delete(unitID);
|
|
906
1110
|
}
|
|
907
1111
|
}
|
|
908
1112
|
if (closed.kind === "opened" || closed.kind === "closed") {
|
|
1113
|
+
const unitID = closedUnitID(closed);
|
|
1114
|
+
if (unitID !== undefined) {
|
|
1115
|
+
this.#attemptFailures.delete(unitID);
|
|
1116
|
+
}
|
|
909
1117
|
// Per-session flags for a unit that will never open again must not
|
|
910
1118
|
// leak: a stale continuation mark would swallow a real user prompt's
|
|
911
1119
|
// admission; a stale admission marker would skip routing. The guard
|
|
@@ -939,7 +1147,11 @@ export class Supervisor {
|
|
|
939
1147
|
? { headers: { "retry-after": report.retryAfterHeader } }
|
|
940
1148
|
: {}),
|
|
941
1149
|
});
|
|
942
|
-
|
|
1150
|
+
// The retry hook classifies the same request for the host's retry
|
|
1151
|
+
// decision; counting both would double every attempt (ADP-019).
|
|
1152
|
+
if (this.#countAttemptFailure(unit?.id, "http", classification.category)) {
|
|
1153
|
+
this.#observeHealth(failureEvent(refKey(model), classification));
|
|
1154
|
+
}
|
|
943
1155
|
}
|
|
944
1156
|
// ---------- retry hook ----------
|
|
945
1157
|
async #handleRetry(input, settings) {
|
|
@@ -952,7 +1164,12 @@ export class Supervisor {
|
|
|
952
1164
|
},
|
|
953
1165
|
status: input.error.status,
|
|
954
1166
|
});
|
|
955
|
-
this.#
|
|
1167
|
+
const unitID = this.#tracker.active(input.sessionID)?.id;
|
|
1168
|
+
// The pumped http-response event describes the same failed request;
|
|
1169
|
+
// counting both would double every attempt (ADP-019).
|
|
1170
|
+
if (this.#countAttemptFailure(unitID, "retry", classification.category)) {
|
|
1171
|
+
this.#observeHealth(failureEvent(refKey(input.model), classification));
|
|
1172
|
+
}
|
|
956
1173
|
// Recovery = fresh admission; the failed resource is already health-blocked.
|
|
957
1174
|
let recovery = { kind: "none" };
|
|
958
1175
|
let routingTier;
|
|
@@ -976,7 +1193,6 @@ export class Supervisor {
|
|
|
976
1193
|
catch (error) {
|
|
977
1194
|
logFailOpen("recovery", error);
|
|
978
1195
|
}
|
|
979
|
-
const unitID = this.#tracker.active(input.sessionID)?.id;
|
|
980
1196
|
const vetoesUsed = unitID !== undefined ? (this.#vetoes.get(unitID) ?? 0) : 0;
|
|
981
1197
|
const core = decideRetry({
|
|
982
1198
|
classification,
|
|
@@ -988,10 +1204,10 @@ export class Supervisor {
|
|
|
988
1204
|
const wrapped = retryForMode(core, this.#modeFor(input.sessionID, settings));
|
|
989
1205
|
if (wrapped.kind !== "veto") {
|
|
990
1206
|
// OBSERVE (and a declined veto) never runs the recovery episode, so an
|
|
991
|
-
// admitted probe has no continuation to report it —
|
|
1207
|
+
// admitted probe has no continuation to report it — release it here or
|
|
992
1208
|
// the resource wedges half-open.
|
|
993
1209
|
if (probeTicket !== undefined) {
|
|
994
|
-
this.#
|
|
1210
|
+
this.#health.releaseProbe(probeTicket);
|
|
995
1211
|
}
|
|
996
1212
|
return;
|
|
997
1213
|
}
|
|
@@ -1004,10 +1220,10 @@ export class Supervisor {
|
|
|
1004
1220
|
if (probeTicket !== undefined) {
|
|
1005
1221
|
// The session holds one pending probe slot. A still-pending older
|
|
1006
1222
|
// ticket belongs to a recovery whose continuation has not started (its
|
|
1007
|
-
// episode can no longer claim this slot) —
|
|
1223
|
+
// episode can no longer claim this slot) — release it before replacing.
|
|
1008
1224
|
const stale = this.#sessionProbeTickets.get(input.sessionID);
|
|
1009
1225
|
if (stale !== undefined && stale !== probeTicket) {
|
|
1010
|
-
this.#
|
|
1226
|
+
this.#health.releaseProbe(stale);
|
|
1011
1227
|
}
|
|
1012
1228
|
this.#sessionProbeTickets.set(input.sessionID, probeTicket);
|
|
1013
1229
|
}
|
|
@@ -1024,17 +1240,28 @@ export class Supervisor {
|
|
|
1024
1240
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- a veto implies the admission was an apply (the recovery exists only then)
|
|
1025
1241
|
decision: admission,
|
|
1026
1242
|
});
|
|
1027
|
-
void this.#ownRecovery(input.sessionID, wrapped.model, routingTier)
|
|
1243
|
+
void this.#ownRecovery(input.sessionID, wrapped.model, routingTier, () => this.#modeFor(input.sessionID, settings) !== "off")
|
|
1028
1244
|
.then(() => undefined)
|
|
1029
1245
|
.catch(async (error) => {
|
|
1246
|
+
if (error instanceof RecoveryAbortedError) {
|
|
1247
|
+
// The user steered away (interruption / OFF) during the settle: the
|
|
1248
|
+
// veto stands, but no continuation episode will ever carry the
|
|
1249
|
+
// probe it admitted — release it instead of wedging half-open.
|
|
1250
|
+
if (probeTicket !== undefined &&
|
|
1251
|
+
this.#sessionProbeTickets.get(input.sessionID) === probeTicket) {
|
|
1252
|
+
this.#sessionProbeTickets.delete(input.sessionID);
|
|
1253
|
+
this.#health.releaseProbe(probeTicket);
|
|
1254
|
+
}
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1030
1257
|
logFailOpen("recovery-ops", error);
|
|
1031
1258
|
// The recovery rejected before (or while) delivering its continuation
|
|
1032
|
-
// prompt, so no unit will ever carry the probe it admitted —
|
|
1259
|
+
// prompt, so no unit will ever carry the probe it admitted — release
|
|
1033
1260
|
// it instead of wedging the resource half-open.
|
|
1034
1261
|
if (probeTicket !== undefined &&
|
|
1035
1262
|
this.#sessionProbeTickets.get(input.sessionID) === probeTicket) {
|
|
1036
1263
|
this.#sessionProbeTickets.delete(input.sessionID);
|
|
1037
|
-
this.#
|
|
1264
|
+
this.#health.releaseProbe(probeTicket);
|
|
1038
1265
|
}
|
|
1039
1266
|
if (unitID === undefined || routingTier === undefined)
|
|
1040
1267
|
return;
|
|
@@ -1059,17 +1286,33 @@ export class Supervisor {
|
|
|
1059
1286
|
* continuation's prompt hook re-arms the admission marker (the unit must
|
|
1060
1287
|
* not be re-routed: that would discard the recovery/escalation decision).
|
|
1061
1288
|
*/
|
|
1062
|
-
async #ownRecovery(sessionID, model, tier) {
|
|
1289
|
+
async #ownRecovery(sessionID, model, tier, stillValid) {
|
|
1063
1290
|
if (tier !== undefined)
|
|
1064
1291
|
this.#continuationTiers.set(sessionID, tier);
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
recovery,
|
|
1069
|
-
continuationPossible: alive,
|
|
1070
|
-
});
|
|
1292
|
+
// The guard window opens before the first await so an interrupt pumped
|
|
1293
|
+
// during the admission race is still seen.
|
|
1294
|
+
this.#recoveryPending.add(sessionID);
|
|
1071
1295
|
try {
|
|
1072
|
-
|
|
1296
|
+
const alive = await this.#isSessionAlive(sessionID);
|
|
1297
|
+
const recovery = { kind: "switch", model };
|
|
1298
|
+
const action = planContinuation({
|
|
1299
|
+
recovery,
|
|
1300
|
+
continuationPossible: alive,
|
|
1301
|
+
});
|
|
1302
|
+
// executeContinuation settles first; the supervisor settles here so the
|
|
1303
|
+
// post-settle guard (mode OFF, user interruption) can abort before any
|
|
1304
|
+
// switch or prompt — the settle is the only step that can take
|
|
1305
|
+
// unbounded time, and a decision made before it may be stale after it.
|
|
1306
|
+
await this.#ops.settle(sessionID);
|
|
1307
|
+
if (!stillValid() || this.#recoveryInterrupted.delete(sessionID)) {
|
|
1308
|
+
throw new RecoveryAbortedError(sessionID);
|
|
1309
|
+
}
|
|
1310
|
+
// Already settled above; the action's own settle is a no-op.
|
|
1311
|
+
await executeContinuation({
|
|
1312
|
+
sessionID,
|
|
1313
|
+
action,
|
|
1314
|
+
ops: { ...this.#ops, settle: () => Promise.resolve() },
|
|
1315
|
+
});
|
|
1073
1316
|
}
|
|
1074
1317
|
catch (error) {
|
|
1075
1318
|
// The continuation prompt never reached the host, so no unit will open
|
|
@@ -1079,6 +1322,9 @@ export class Supervisor {
|
|
|
1079
1322
|
this.#continuationTiers.delete(sessionID);
|
|
1080
1323
|
throw error;
|
|
1081
1324
|
}
|
|
1325
|
+
finally {
|
|
1326
|
+
this.#recoveryPending.delete(sessionID);
|
|
1327
|
+
}
|
|
1082
1328
|
}
|
|
1083
1329
|
// ---------- parked-work resume (safe boundary: idle) ----------
|
|
1084
1330
|
async #onIdle(settings) {
|
|
@@ -1087,7 +1333,8 @@ export class Supervisor {
|
|
|
1087
1333
|
// pruned here (its per-session steering is worthless once the session is
|
|
1088
1334
|
// gone), in the same sweep that clears the other per-session maps.
|
|
1089
1335
|
const overrideIDs = this.#sessionModes.snapshot().map((record) => record.sessionID);
|
|
1090
|
-
|
|
1336
|
+
const tierRequestIDs = this.#tierRequests.snapshot().map((record) => record.sessionID);
|
|
1337
|
+
if (records.length === 0 && overrideIDs.length === 0 && tierRequestIDs.length === 0)
|
|
1091
1338
|
return;
|
|
1092
1339
|
// OFF sessions are not supervised: their parked records are neither
|
|
1093
1340
|
// dropped nor resumed, and no alive probe runs for them.
|
|
@@ -1100,7 +1347,7 @@ export class Supervisor {
|
|
|
1100
1347
|
}
|
|
1101
1348
|
// Tier-request holders join the alive sweep too: a dead session's sticky
|
|
1102
1349
|
// tier request dies with its session, like the other per-session maps.
|
|
1103
|
-
for (const sessionID of
|
|
1350
|
+
for (const sessionID of tierRequestIDs) {
|
|
1104
1351
|
if (!alive.has(sessionID))
|
|
1105
1352
|
alive.set(sessionID, await this.#isSessionAlive(sessionID));
|
|
1106
1353
|
}
|
|
@@ -1114,9 +1361,18 @@ export class Supervisor {
|
|
|
1114
1361
|
await this.#dropParked(dropped);
|
|
1115
1362
|
}
|
|
1116
1363
|
// Tier requests die with their session, like the other per-session maps.
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1364
|
+
// The fire-and-forget note means the queue may still hold writes; the
|
|
1365
|
+
// serialized set applies after them. A write failure just leaves the
|
|
1366
|
+
// record (fail-open — it will die with the session on the next sweep).
|
|
1367
|
+
for (const sessionID of tierRequestIDs) {
|
|
1368
|
+
if (alive.get(sessionID) === false) {
|
|
1369
|
+
try {
|
|
1370
|
+
await this.#tierRequests.set(sessionID, null);
|
|
1371
|
+
}
|
|
1372
|
+
catch (error) {
|
|
1373
|
+
logFailOpen("tier-requests", error);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1120
1376
|
}
|
|
1121
1377
|
// At most ONE continuation per session per sweep: prompting a session
|
|
1122
1378
|
// with an active unit would supersede the user's in-flight episode (not
|
|
@@ -1138,6 +1394,7 @@ export class Supervisor {
|
|
|
1138
1394
|
this.#continuationTiers.delete(sessionID);
|
|
1139
1395
|
this.#promptAdmission.delete(sessionID);
|
|
1140
1396
|
this.#continuationPending.delete(sessionID);
|
|
1397
|
+
this.#escalations.delete(sessionID);
|
|
1141
1398
|
if (this.#sessionModes.get(sessionID) !== null) {
|
|
1142
1399
|
try {
|
|
1143
1400
|
await this.#sessionModes.set(sessionID, null);
|
|
@@ -1163,14 +1420,19 @@ export class Supervisor {
|
|
|
1163
1420
|
// report's session's mode.
|
|
1164
1421
|
const mode = this.#modeFor(record.sessionID, settings);
|
|
1165
1422
|
let decision;
|
|
1423
|
+
let probeTicket;
|
|
1166
1424
|
try {
|
|
1167
|
-
|
|
1425
|
+
const admission = await this.#admit(settings, record.sessionID, record.tier);
|
|
1426
|
+
decision = admission.decision;
|
|
1427
|
+
probeTicket = admission.probeTicket;
|
|
1168
1428
|
}
|
|
1169
1429
|
catch (error) {
|
|
1170
1430
|
logFailOpen("resume", error);
|
|
1171
1431
|
return;
|
|
1172
1432
|
}
|
|
1173
1433
|
if (mode === "observe") {
|
|
1434
|
+
if (probeTicket !== undefined)
|
|
1435
|
+
this.#health.releaseProbe(probeTicket);
|
|
1174
1436
|
await this.#recordReceipt({
|
|
1175
1437
|
sessionID: record.sessionID,
|
|
1176
1438
|
unitID: record.id,
|
|
@@ -1180,7 +1442,9 @@ export class Supervisor {
|
|
|
1180
1442
|
return;
|
|
1181
1443
|
}
|
|
1182
1444
|
if (decision.kind !== "apply") {
|
|
1183
|
-
// Still no usable resource (or the routing deferred) — stay parked.
|
|
1445
|
+
// Still no usable resource (or the routing deferred) — stay parked. The
|
|
1446
|
+
// admission held no probe ticket: #admit keeps a ticket only for apply
|
|
1447
|
+
// decisions and releases every other one itself.
|
|
1184
1448
|
await this.#recordReceipt({
|
|
1185
1449
|
sessionID: record.sessionID,
|
|
1186
1450
|
unitID: record.id,
|
|
@@ -1190,8 +1454,11 @@ export class Supervisor {
|
|
|
1190
1454
|
return;
|
|
1191
1455
|
}
|
|
1192
1456
|
const popped = await this.#popParked(record);
|
|
1193
|
-
if (popped === undefined)
|
|
1457
|
+
if (popped === undefined) {
|
|
1458
|
+
if (probeTicket !== undefined)
|
|
1459
|
+
this.#health.releaseProbe(probeTicket);
|
|
1194
1460
|
return;
|
|
1461
|
+
}
|
|
1195
1462
|
// The resume continuation is not re-admitted (owned continuation), so
|
|
1196
1463
|
// the freshly routed admission is recorded here, unit-less; the unit
|
|
1197
1464
|
// that opens next adopts it and carries the outcome.
|
|
@@ -1204,10 +1471,23 @@ export class Supervisor {
|
|
|
1204
1471
|
// freshly routed model, then prompt — the episode's first LLM call must
|
|
1205
1472
|
// not hit the model the work was parked away from.
|
|
1206
1473
|
try {
|
|
1207
|
-
|
|
1474
|
+
if (probeTicket !== undefined)
|
|
1475
|
+
this.#sessionProbeTickets.set(record.sessionID, probeTicket);
|
|
1476
|
+
await this.#ownRecovery(record.sessionID, decision.model, record.tier,
|
|
1477
|
+
// The parked session's own effective mode and its still-open unit
|
|
1478
|
+
// window are re-checked after the settle; a session that gained an
|
|
1479
|
+
// active unit must not be prompted (not a safe boundary).
|
|
1480
|
+
() => this.#modeFor(record.sessionID, settings) !== "off" &&
|
|
1481
|
+
this.#tracker.active(record.sessionID) === undefined);
|
|
1208
1482
|
}
|
|
1209
1483
|
catch (error) {
|
|
1210
|
-
|
|
1484
|
+
if (probeTicket !== undefined &&
|
|
1485
|
+
this.#sessionProbeTickets.get(record.sessionID) === probeTicket) {
|
|
1486
|
+
this.#sessionProbeTickets.delete(record.sessionID);
|
|
1487
|
+
this.#health.releaseProbe(probeTicket);
|
|
1488
|
+
}
|
|
1489
|
+
if (!(error instanceof RecoveryAbortedError))
|
|
1490
|
+
logFailOpen("resume", error);
|
|
1211
1491
|
await this.#rePark(record);
|
|
1212
1492
|
}
|
|
1213
1493
|
}
|
|
@@ -1238,8 +1518,14 @@ export class Supervisor {
|
|
|
1238
1518
|
await this.#options.session.get({ sessionID });
|
|
1239
1519
|
return true;
|
|
1240
1520
|
}
|
|
1241
|
-
catch {
|
|
1242
|
-
|
|
1521
|
+
catch (error) {
|
|
1522
|
+
// The host rejects `session.get` for a missing session with a plain
|
|
1523
|
+
// error and exposes no typed status; only a definitive not-found
|
|
1524
|
+
// counts as dead. Anything else is treated as alive (fail open toward
|
|
1525
|
+
// keeping durable user state, ADP-031): a transient storage or host
|
|
1526
|
+
// error must never prune parked work, mode overrides, or tier
|
|
1527
|
+
// requests — the next sweep re-probes instead.
|
|
1528
|
+
return !/not found/i.test(String(error));
|
|
1243
1529
|
}
|
|
1244
1530
|
}
|
|
1245
1531
|
async #recordReceipt(input) {
|