@coool/opencode-adaptive 1.0.0 → 1.2.0
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 +13 -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 +414 -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 +70 -24
- 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,42 @@ 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
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The manual-pick suppression actually applied at an admission. It yields
|
|
351
|
+
* when the picked model is health-blocked: keeping the session on a
|
|
352
|
+
* quarantined resource does not honor the pick — the configured fallback
|
|
353
|
+
* takes over instead (the pick is re-honored only if the user picks again).
|
|
354
|
+
* Uses the non-mutating evaluation, never a probe admission.
|
|
355
|
+
*/
|
|
356
|
+
#manualSuppressionHolds(sessionID, settings) {
|
|
357
|
+
if (!this.#manuallySelected(sessionID, settings))
|
|
358
|
+
return false;
|
|
359
|
+
const current = this.#tracker.lastModel(sessionID)?.model;
|
|
360
|
+
if (current === undefined)
|
|
361
|
+
return true;
|
|
362
|
+
return this.#health.evaluate(refKey(current)).kind !== "blocked";
|
|
363
|
+
}
|
|
273
364
|
/**
|
|
274
365
|
* Handle one `retry` hook invocation. On a veto this mutates
|
|
275
366
|
* `input.decision` to `{ retry: false }`; otherwise the decision is left
|
|
@@ -308,33 +399,38 @@ export class Supervisor {
|
|
|
308
399
|
* tool). STICKY: it steers the session's next and following admissions
|
|
309
400
|
* until cleared — an agent works across many units in one task. Returns
|
|
310
401
|
* false on an unknown tier (the tool re-validates before calling).
|
|
402
|
+
*
|
|
403
|
+
* Fire-and-forget persistence: the recorder signature is sync on both the
|
|
404
|
+
* tool and the RPC source interfaces, so the write is enqueued without
|
|
405
|
+
* awaiting it. Readers (routing, getTierRequest, the idle sweep) flush the
|
|
406
|
+
* store's queue before reading, which makes the record visible
|
|
407
|
+
* deterministically; a write failure drops the record fail-open (the tool
|
|
408
|
+
* told the agent "recorded", the persisted mirror simply never sees it —
|
|
409
|
+
* the same degradation the in-memory map had on restart).
|
|
311
410
|
*/
|
|
312
411
|
noteTierRequest(sessionID, tier) {
|
|
313
412
|
if (!isTierName(tier))
|
|
314
413
|
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" });
|
|
414
|
+
void this.#tierRequests.set(sessionID, tier).catch(() => undefined);
|
|
415
|
+
// The user/agent explicitly asked Adaptive to route again (ADP-028).
|
|
416
|
+
this.#manualSelections.delete(sessionID);
|
|
325
417
|
return true;
|
|
326
418
|
}
|
|
327
419
|
/** The session's tier request, or `null` when it follows the routing ladder. */
|
|
328
420
|
async getTierRequest(sessionID) {
|
|
329
421
|
await this.#ready;
|
|
330
|
-
|
|
422
|
+
// A fire-and-forget note may still be in the queue: flush before reading.
|
|
423
|
+
await this.#tierRequests.flush();
|
|
424
|
+
return this.#tierRequests.get(sessionID);
|
|
331
425
|
}
|
|
332
426
|
/** Clear one session's tier request (`null` is the only valid input). */
|
|
333
427
|
async setTierRequest(sessionID, tier) {
|
|
334
428
|
await this.#ready;
|
|
335
429
|
if (tier !== null)
|
|
336
430
|
throw new Error("only null clears a tier request");
|
|
337
|
-
this.#tierRequests.
|
|
431
|
+
await this.#tierRequests.set(sessionID, null);
|
|
432
|
+
// Clearing through the control surface is the user asking Adaptive back.
|
|
433
|
+
this.#manualSelections.delete(sessionID);
|
|
338
434
|
}
|
|
339
435
|
// ---------- state API for the control surfaces (ADP-033/034/036) ----------
|
|
340
436
|
/** Current settings (awaits the constructor load). */
|
|
@@ -344,7 +440,13 @@ export class Supervisor {
|
|
|
344
440
|
/** Validate and persist a settings patch; returns per-field errors. */
|
|
345
441
|
async updateSettings(patch) {
|
|
346
442
|
await this.#ready;
|
|
347
|
-
|
|
443
|
+
const result = await this.#settings.update(patch);
|
|
444
|
+
if (result.ok && patch.mode !== undefined) {
|
|
445
|
+
// A global mode change is the user steering Adaptive explicitly
|
|
446
|
+
// (ADP-028): manual-selection suppression lifts for every session.
|
|
447
|
+
this.#manualSelections.clear();
|
|
448
|
+
}
|
|
449
|
+
return result;
|
|
348
450
|
}
|
|
349
451
|
/** The session's mode override, or `null` when it follows the global mode. */
|
|
350
452
|
async getSessionMode(sessionID) {
|
|
@@ -359,6 +461,8 @@ export class Supervisor {
|
|
|
359
461
|
async setSessionMode(sessionID, mode) {
|
|
360
462
|
await this.#ready;
|
|
361
463
|
await this.#sessionModes.set(sessionID, mode);
|
|
464
|
+
// A mode change is the user steering Adaptive explicitly (ADP-028).
|
|
465
|
+
this.#manualSelections.delete(sessionID);
|
|
362
466
|
}
|
|
363
467
|
/**
|
|
364
468
|
* Bounded status snapshot for the status view (ADP-033). Health is read
|
|
@@ -416,14 +520,24 @@ export class Supervisor {
|
|
|
416
520
|
case "execution-interrupted":
|
|
417
521
|
await this.#onExecutionInterrupted(report);
|
|
418
522
|
return;
|
|
419
|
-
case "model-selected":
|
|
420
|
-
this.#tracker.apply({
|
|
523
|
+
case "model-selected": {
|
|
524
|
+
const result = this.#tracker.apply({
|
|
421
525
|
kind: "model-selected",
|
|
422
526
|
sessionID: report.sessionID,
|
|
423
527
|
at: report.at,
|
|
424
528
|
model: report.model,
|
|
425
529
|
});
|
|
530
|
+
// Whether the change was the user's pick or Adaptive's own switch
|
|
531
|
+
// decides who controls the session's model (ADP-028); the result
|
|
532
|
+
// carries the attribution the tracker resolved. Only the two
|
|
533
|
+
// selection sources reach this event kind.
|
|
534
|
+
const { source } = result.transition;
|
|
535
|
+
if (source === "selected-adaptive")
|
|
536
|
+
this.#manualSelections.delete(report.sessionID);
|
|
537
|
+
else
|
|
538
|
+
this.#noteManualSelection(report.sessionID);
|
|
426
539
|
return;
|
|
540
|
+
}
|
|
427
541
|
case "model-request":
|
|
428
542
|
if (report.agent !== undefined) {
|
|
429
543
|
if (!this.#agents.has(report.sessionID) &&
|
|
@@ -437,12 +551,20 @@ export class Supervisor {
|
|
|
437
551
|
}
|
|
438
552
|
this.#agents.set(report.sessionID, report.agent);
|
|
439
553
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
554
|
+
// Only the agent loop's own request reports the model actually
|
|
555
|
+
// executing the unit's work. An auxiliary request (title, compaction,
|
|
556
|
+
// generate — ADP-027) carries the host's own cheap pick for that side
|
|
557
|
+
// request; adopting it as the session's model would misattribute the
|
|
558
|
+
// primary unit's health evidence and leave the next admission
|
|
559
|
+
// believing the unit runs on the wrong resource.
|
|
560
|
+
if (report.requestKind === "primary") {
|
|
561
|
+
this.#tracker.apply({
|
|
562
|
+
kind: "model-request",
|
|
563
|
+
sessionID: report.sessionID,
|
|
564
|
+
at: this.#options.clock(),
|
|
565
|
+
model: report.model,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
446
568
|
return;
|
|
447
569
|
case "http-response":
|
|
448
570
|
await this.#onHttpResponse(report);
|
|
@@ -481,14 +603,26 @@ export class Supervisor {
|
|
|
481
603
|
// this admission re-routes from scratch.
|
|
482
604
|
this.#continuationTiers.delete(sessionID);
|
|
483
605
|
this.#promptAdmission.delete(sessionID);
|
|
606
|
+
this.#escalations.delete(sessionID);
|
|
484
607
|
// A pending probe ticket from an earlier prompt admission (or a failed
|
|
485
608
|
// 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.
|
|
609
|
+
// a fresh admission means that episode never ran, so the ticket is
|
|
610
|
+
// released here instead of being adopted by whichever unit opens next.
|
|
488
611
|
const stale = this.#sessionProbeTickets.get(sessionID);
|
|
489
612
|
if (stale !== undefined) {
|
|
490
613
|
this.#sessionProbeTickets.delete(sessionID);
|
|
491
|
-
this.#
|
|
614
|
+
this.#health.releaseProbe(stale);
|
|
615
|
+
}
|
|
616
|
+
if (this.#manualSuppressionHolds(sessionID, settings)) {
|
|
617
|
+
// ADP-028: the user's own model pick stands (yielded to health if the
|
|
618
|
+
// picked resource is quarantined). Recording the defer (no switch, no
|
|
619
|
+
// marker) leaves OpenCode's choice — the picked model — in place;
|
|
620
|
+
// execution-started will skip routing the same way.
|
|
621
|
+
await this.#recordReceipt({
|
|
622
|
+
sessionID,
|
|
623
|
+
decision: { kind: "defer", reason: "routing-defer", detail: "manual-selection" },
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
492
626
|
}
|
|
493
627
|
let probeTicket;
|
|
494
628
|
try {
|
|
@@ -513,9 +647,9 @@ export class Supervisor {
|
|
|
513
647
|
}
|
|
514
648
|
catch (error) {
|
|
515
649
|
// A probe admitted for this turn is lost when the admission fails (the
|
|
516
|
-
// switch threw): no unit will ever carry the ticket, so
|
|
650
|
+
// switch threw): no unit will ever carry the ticket, so release it.
|
|
517
651
|
if (probeTicket !== undefined) {
|
|
518
|
-
this.#
|
|
652
|
+
this.#health.releaseProbe(probeTicket);
|
|
519
653
|
}
|
|
520
654
|
// Fail-open: no marker → execution-started re-routes the unit itself.
|
|
521
655
|
logFailOpen("admission", error);
|
|
@@ -582,14 +716,22 @@ export class Supervisor {
|
|
|
582
716
|
}
|
|
583
717
|
}
|
|
584
718
|
/** Fresh admission: routing ladder → scheduler, raw (unwrapped). */
|
|
585
|
-
async #admit(settings, sessionID) {
|
|
719
|
+
async #admit(settings, sessionID, requiredTier) {
|
|
720
|
+
// The agent-declared tier request (adaptive_tier tool) outranks the
|
|
721
|
+
// role hints but stays below the pin (ADP-002 ladder). The note is
|
|
722
|
+
// recorded fire-and-forget, so flush the store's queue before reading.
|
|
723
|
+
await this.#tierRequests.flush();
|
|
724
|
+
const tierRequest = this.#tierRequests.get(sessionID);
|
|
725
|
+
let routedTier;
|
|
726
|
+
if (requiredTier !== undefined) {
|
|
727
|
+
routedTier = { tier: requiredTier, via: "manual-command" };
|
|
728
|
+
}
|
|
729
|
+
else if (tierRequest !== null) {
|
|
730
|
+
routedTier = { tier: tierRequest, via: "manual-command" };
|
|
731
|
+
}
|
|
586
732
|
const routing = resolveRouting({
|
|
587
733
|
...(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
|
-
: {}),
|
|
734
|
+
...(routedTier !== undefined ? { tierRequest: routedTier } : {}),
|
|
593
735
|
// The agent-role hint is the session's last observed agent — the host
|
|
594
736
|
// reports it on every model request (ADP-002 level 3).
|
|
595
737
|
...(this.#agents.has(sessionID) ? { agent: this.#agents.get(sessionID) } : {}),
|
|
@@ -603,10 +745,13 @@ export class Supervisor {
|
|
|
603
745
|
await this.#capacity.refresh();
|
|
604
746
|
const health = {};
|
|
605
747
|
const capacity = {};
|
|
606
|
-
// Tickets issued by this assessment sweep (see the
|
|
748
|
+
// Tickets issued by this assessment sweep (see the release below).
|
|
607
749
|
const issued = [];
|
|
608
|
-
|
|
609
|
-
|
|
750
|
+
// Only the routed candidates are assessed: assess flips a due breaker to
|
|
751
|
+
// half_open, and admitting a quick unit has no business touching an
|
|
752
|
+
// advanced-tier resource's breaker.
|
|
753
|
+
const routed = routing.kind === "pin" ? [routing.model] : tierCandidates;
|
|
754
|
+
for (const ref of routed) {
|
|
610
755
|
const mapKey = schedulerHealthKey(ref);
|
|
611
756
|
const assessment = this.#health.assess(refKey(ref));
|
|
612
757
|
health[mapKey] = assessment;
|
|
@@ -630,12 +775,12 @@ export class Supervisor {
|
|
|
630
775
|
};
|
|
631
776
|
const decision = admit(input);
|
|
632
777
|
// 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
|
-
//
|
|
778
|
+
// including candidates this decision passed over (an earlier ready
|
|
779
|
+
// candidate wins) or dispatched nothing for (defer/park). An
|
|
780
|
+
// issued-but-undispatched probe would wedge its resource in half_open:
|
|
781
|
+
// no unit ever carries the ticket. Release each unselected ticket — it
|
|
782
|
+
// stays due, because a probe that never ran is no health evidence and
|
|
783
|
+
// restarting its backoff would push it back on every admission.
|
|
639
784
|
const selectedId = decision.kind === "apply" ? decision.evidence.probeId : undefined;
|
|
640
785
|
let probeTicket;
|
|
641
786
|
for (const ticket of issued) {
|
|
@@ -643,12 +788,22 @@ export class Supervisor {
|
|
|
643
788
|
probeTicket = ticket;
|
|
644
789
|
}
|
|
645
790
|
else {
|
|
646
|
-
this.#health.
|
|
791
|
+
this.#health.releaseProbe(ticket);
|
|
647
792
|
}
|
|
648
793
|
}
|
|
649
794
|
return { routing, decision, ...(probeTicket !== undefined ? { probeTicket } : {}) };
|
|
650
795
|
}
|
|
651
796
|
async #routeAdmission(unit, settings, mode) {
|
|
797
|
+
if (this.#manualSuppressionHolds(unit.sessionID, settings)) {
|
|
798
|
+
// ADP-028: the session runs on the user's own pick (yielded to health
|
|
799
|
+
// if the picked resource is quarantined) — no re-route.
|
|
800
|
+
await this.#recordReceipt({
|
|
801
|
+
sessionID: unit.sessionID,
|
|
802
|
+
unitID: unit.id,
|
|
803
|
+
decision: { kind: "defer", reason: "routing-defer", detail: "manual-selection" },
|
|
804
|
+
});
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
652
807
|
const { routing, decision, probeTicket } = await this.#admit(settings, unit.sessionID);
|
|
653
808
|
// The tier the unit was admitted under is escalation context (ADP-015);
|
|
654
809
|
// recorded for every tiered routing outcome (including a defer, which
|
|
@@ -663,9 +818,9 @@ export class Supervisor {
|
|
|
663
818
|
}
|
|
664
819
|
catch (error) {
|
|
665
820
|
// The episode cannot carry the probe when the switch itself failed —
|
|
666
|
-
//
|
|
821
|
+
// release the ticket instead of wedging the resource half-open.
|
|
667
822
|
if (probeTicket !== undefined) {
|
|
668
|
-
this.#
|
|
823
|
+
this.#health.releaseProbe(probeTicket);
|
|
669
824
|
}
|
|
670
825
|
throw error;
|
|
671
826
|
}
|
|
@@ -675,8 +830,8 @@ export class Supervisor {
|
|
|
675
830
|
}
|
|
676
831
|
if (probeTicket !== undefined) {
|
|
677
832
|
// OBSERVE wraps an apply — including a probe admission — without
|
|
678
|
-
// running it: no episode will ever report the ticket, so
|
|
679
|
-
this.#
|
|
833
|
+
// running it: no episode will ever report the ticket, so release it.
|
|
834
|
+
this.#health.releaseProbe(probeTicket);
|
|
680
835
|
}
|
|
681
836
|
// Observe-only records the identical decision; defer and park leave the
|
|
682
837
|
// episode on OpenCode's own model (admission cannot hold an episode).
|
|
@@ -702,7 +857,7 @@ export class Supervisor {
|
|
|
702
857
|
async #onExecutionSucceeded(report) {
|
|
703
858
|
const unit = this.#tracker.active(report.sessionID);
|
|
704
859
|
const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
|
|
705
|
-
const probeTicket =
|
|
860
|
+
const probeTicket = this.#takeProbeTicket(unit?.id);
|
|
706
861
|
const closed = this.#tracker.apply({
|
|
707
862
|
kind: "execution-succeeded",
|
|
708
863
|
sessionID: report.sessionID,
|
|
@@ -714,7 +869,6 @@ export class Supervisor {
|
|
|
714
869
|
}
|
|
715
870
|
if (unit !== undefined) {
|
|
716
871
|
if (probeTicket !== undefined) {
|
|
717
|
-
this.#probeTickets.delete(unit.id);
|
|
718
872
|
this.#reportProbeOutcome(probeTicket, { success: true });
|
|
719
873
|
}
|
|
720
874
|
await this.#attachOutcome(unit, { status: "succeeded" });
|
|
@@ -724,19 +878,16 @@ export class Supervisor {
|
|
|
724
878
|
const unit = this.#tracker.active(report.sessionID);
|
|
725
879
|
const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
|
|
726
880
|
const unitID = unit?.id;
|
|
727
|
-
//
|
|
881
|
+
// The tier is read before the close clears the per-unit maps.
|
|
728
882
|
const admissionTier = unitID !== undefined ? this.#admissionTiers.get(unitID) : undefined;
|
|
729
|
-
const escalationUsed =
|
|
883
|
+
const escalationUsed = this.#escalations.get(report.sessionID) ?? 0;
|
|
730
884
|
// Tool evidence is captured pre-close like the other per-unit maps.
|
|
731
885
|
const toolEvidence = unitID !== undefined ? (this.#escalationEvidence.get(unitID) ?? []) : [];
|
|
732
|
-
const probeTicket =
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
error: report.error,
|
|
738
|
-
});
|
|
739
|
-
this.#afterUnitClose(closed);
|
|
886
|
+
const probeTicket = this.#takeProbeTicket(unitID);
|
|
887
|
+
// The terminal classifies and dedups BEFORE the close: the close clears
|
|
888
|
+
// the unit's attempt-failure entry, and the terminal must consult it —
|
|
889
|
+
// it repeats the episode's last attempt, which an attempt signal
|
|
890
|
+
// (http-response or retry hook) already counted.
|
|
740
891
|
const classification = classifyFailure({
|
|
741
892
|
structured: {
|
|
742
893
|
type: report.error.type,
|
|
@@ -750,13 +901,20 @@ export class Supervisor {
|
|
|
750
901
|
// The retry hook deliberately omits text: it must stay conservative.
|
|
751
902
|
text: report.error.message,
|
|
752
903
|
});
|
|
753
|
-
|
|
904
|
+
const countTerminal = this.#countTerminalFailure(unitID, classification.category);
|
|
905
|
+
const closed = this.#tracker.apply({
|
|
906
|
+
kind: "execution-failed",
|
|
907
|
+
sessionID: report.sessionID,
|
|
908
|
+
at: report.at,
|
|
909
|
+
error: report.error,
|
|
910
|
+
});
|
|
911
|
+
this.#afterUnitClose(closed);
|
|
912
|
+
if (model !== undefined && countTerminal) {
|
|
754
913
|
this.#observeHealth(failureEvent(refKey(model), classification));
|
|
755
914
|
}
|
|
756
915
|
if (unit !== undefined) {
|
|
757
916
|
await this.#attachOutcome(unit, { status: "failed", category: classification.category });
|
|
758
917
|
if (probeTicket !== undefined) {
|
|
759
|
-
this.#probeTickets.delete(unit.id);
|
|
760
918
|
this.#reportProbeOutcome(probeTicket, { success: false, category: classification.category });
|
|
761
919
|
}
|
|
762
920
|
}
|
|
@@ -783,8 +941,11 @@ export class Supervisor {
|
|
|
783
941
|
return [];
|
|
784
942
|
}
|
|
785
943
|
const eligible = [];
|
|
944
|
+
// Compared by resource key: the host reports base-model calls as variant
|
|
945
|
+
// "default", which is the same resource as the bare tier entry.
|
|
946
|
+
const failedID = resourceKeyID(refKey(failed));
|
|
786
947
|
for (const ref of refs) {
|
|
787
|
-
if (
|
|
948
|
+
if (resourceKeyID(refKey(ref)) === failedID)
|
|
788
949
|
continue;
|
|
789
950
|
const candidate = inventory.find((entry) => sameModelRef(candidateRef(entry), ref));
|
|
790
951
|
if (candidate === undefined)
|
|
@@ -822,11 +983,11 @@ export class Supervisor {
|
|
|
822
983
|
currentTier: admissionTier,
|
|
823
984
|
nextTierResources: eligible,
|
|
824
985
|
evidence,
|
|
825
|
-
budget: { used: escalationUsed, max:
|
|
986
|
+
budget: { used: escalationUsed, max: MAX_ESCALATIONS_PER_TASK },
|
|
826
987
|
});
|
|
827
988
|
const wrapped = schedulerForMode(plan, this.#modeFor(report.sessionID, settings));
|
|
828
989
|
if (wrapped.kind === "escalate") {
|
|
829
|
-
this.#escalations.set(
|
|
990
|
+
this.#escalations.set(report.sessionID, escalationUsed + 1);
|
|
830
991
|
// Recorded unit-less on purpose: the receipt describes the CONTINUATION
|
|
831
992
|
// decision, so the continuation unit adopts it and its outcome attaches
|
|
832
993
|
// here (the failed unit's own outcome attaches to its apply receipt).
|
|
@@ -838,10 +999,11 @@ export class Supervisor {
|
|
|
838
999
|
// Continue the interrupted work on the escalated resource; the unit is
|
|
839
1000
|
// terminally failed, so an ops failure is only logged (never parked).
|
|
840
1001
|
try {
|
|
841
|
-
await this.#ownRecovery(report.sessionID, wrapped.target, wrapped.tier);
|
|
1002
|
+
await this.#ownRecovery(report.sessionID, wrapped.target, wrapped.tier, () => this.#modeFor(report.sessionID, settings) !== "off");
|
|
842
1003
|
}
|
|
843
1004
|
catch (error) {
|
|
844
|
-
|
|
1005
|
+
if (!(error instanceof RecoveryAbortedError))
|
|
1006
|
+
logFailOpen("escalation-ops", error);
|
|
845
1007
|
}
|
|
846
1008
|
return;
|
|
847
1009
|
}
|
|
@@ -858,7 +1020,13 @@ export class Supervisor {
|
|
|
858
1020
|
return;
|
|
859
1021
|
}
|
|
860
1022
|
const unit = this.#tracker.active(report.sessionID);
|
|
861
|
-
const probeTicket =
|
|
1023
|
+
const probeTicket = this.#takeProbeTicket(unit?.id);
|
|
1024
|
+
// An interruption while a recovery is pending on this session aborts it
|
|
1025
|
+
// at its post-settle guard (the pending recovery's decision predates the
|
|
1026
|
+
// interruption and must not act after it).
|
|
1027
|
+
if (this.#recoveryPending.has(report.sessionID)) {
|
|
1028
|
+
this.#recoveryInterrupted.add(report.sessionID);
|
|
1029
|
+
}
|
|
862
1030
|
const closed = this.#tracker.apply({
|
|
863
1031
|
kind: "execution-interrupted",
|
|
864
1032
|
sessionID: report.sessionID,
|
|
@@ -872,7 +1040,6 @@ export class Supervisor {
|
|
|
872
1040
|
// An interrupted probe is no evidence about resource health: re-arm the
|
|
873
1041
|
// breaker's same window rather than doubling it.
|
|
874
1042
|
if (probeTicket !== undefined) {
|
|
875
|
-
this.#probeTickets.delete(unit.id);
|
|
876
1043
|
this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
|
|
877
1044
|
}
|
|
878
1045
|
await this.#attachOutcome(unit, { status: "interrupted", reason });
|
|
@@ -889,23 +1056,81 @@ export class Supervisor {
|
|
|
889
1056
|
// unknown ticket) are reported by its boolean return, not by throwing.
|
|
890
1057
|
this.#health.reportProbe(ticket, outcome);
|
|
891
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* One attempt's failure evidence (http-response event or retry hook):
|
|
1061
|
+
* returns whether THIS observation is the attempt's first count.
|
|
1062
|
+
*/
|
|
1063
|
+
#countAttemptFailure(unitID, source, category) {
|
|
1064
|
+
if (unitID === undefined)
|
|
1065
|
+
return true;
|
|
1066
|
+
const ev = this.#attemptFailures.get(unitID) ?? {};
|
|
1067
|
+
if (source === "http" ? ev.retry === category : ev.http === category) {
|
|
1068
|
+
// The other host signal already counted this attempt: consume it.
|
|
1069
|
+
const closed = { ...ev, http: undefined, retry: undefined, closed: category };
|
|
1070
|
+
this.#attemptFailures.set(unitID, closed);
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
this.#attemptFailures.set(unitID, source === "http" ? { ...ev, http: category } : { ...ev, retry: category });
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* The terminal `execution-failed` repeats the episode's last attempt: it
|
|
1078
|
+
* counts only when the last attempt left no live evidence (e.g. a
|
|
1079
|
+
* capability failure that never produced an HTTP response or retry hook).
|
|
1080
|
+
*/
|
|
1081
|
+
#countTerminalFailure(unitID, category) {
|
|
1082
|
+
if (unitID === undefined)
|
|
1083
|
+
return true;
|
|
1084
|
+
const ev = this.#attemptFailures.get(unitID) ?? {};
|
|
1085
|
+
if (ev.http === category) {
|
|
1086
|
+
this.#attemptFailures.set(unitID, { ...ev, http: undefined });
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
if (ev.retry === category) {
|
|
1090
|
+
this.#attemptFailures.set(unitID, { ...ev, retry: undefined });
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
if (ev.closed === category) {
|
|
1094
|
+
this.#attemptFailures.set(unitID, { ...ev, closed: undefined });
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
this.#attemptFailures.set(unitID, { ...ev, closed: category });
|
|
1098
|
+
return true;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Hands a closing unit's probe ticket to its explicit outcome reporter.
|
|
1102
|
+
* Taken BEFORE the close: #afterUnitClose treats any ticket still held as
|
|
1103
|
+
* lost and re-opens the breaker, which would turn a probe's real outcome
|
|
1104
|
+
* into a stale report.
|
|
1105
|
+
*/
|
|
1106
|
+
#takeProbeTicket(unitID) {
|
|
1107
|
+
if (unitID === undefined)
|
|
1108
|
+
return undefined;
|
|
1109
|
+
const ticket = this.#probeTickets.get(unitID);
|
|
1110
|
+
this.#probeTickets.delete(unitID);
|
|
1111
|
+
return ticket;
|
|
1112
|
+
}
|
|
892
1113
|
/** Bounded per-unit bookkeeping once the tracker closed the unit. */
|
|
893
1114
|
#afterUnitClose(closed) {
|
|
894
1115
|
const unitID = closedUnitID(closed);
|
|
895
1116
|
if (unitID !== undefined) {
|
|
896
1117
|
this.#vetoes.delete(unitID);
|
|
897
|
-
this.#escalations.delete(unitID);
|
|
898
1118
|
this.#admissionTiers.delete(unitID);
|
|
899
1119
|
this.#escalationEvidence.delete(unitID);
|
|
900
1120
|
const leftover = this.#probeTickets.get(unitID);
|
|
901
1121
|
if (leftover !== undefined) {
|
|
902
|
-
// The explicit outcome reporters
|
|
903
|
-
// here means the unit ended without one
|
|
1122
|
+
// The explicit outcome reporters take their ticket before this
|
|
1123
|
+
// cleanup; reaching here means the unit ended without one
|
|
1124
|
+
// (superseded or lost).
|
|
904
1125
|
this.#reportProbeOutcome(leftover, { success: false, category: "user_cancelled" });
|
|
905
1126
|
this.#probeTickets.delete(unitID);
|
|
906
1127
|
}
|
|
907
1128
|
}
|
|
908
1129
|
if (closed.kind === "opened" || closed.kind === "closed") {
|
|
1130
|
+
const unitID = closedUnitID(closed);
|
|
1131
|
+
if (unitID !== undefined) {
|
|
1132
|
+
this.#attemptFailures.delete(unitID);
|
|
1133
|
+
}
|
|
909
1134
|
// Per-session flags for a unit that will never open again must not
|
|
910
1135
|
// leak: a stale continuation mark would swallow a real user prompt's
|
|
911
1136
|
// admission; a stale admission marker would skip routing. The guard
|
|
@@ -939,7 +1164,11 @@ export class Supervisor {
|
|
|
939
1164
|
? { headers: { "retry-after": report.retryAfterHeader } }
|
|
940
1165
|
: {}),
|
|
941
1166
|
});
|
|
942
|
-
|
|
1167
|
+
// The retry hook classifies the same request for the host's retry
|
|
1168
|
+
// decision; counting both would double every attempt (ADP-019).
|
|
1169
|
+
if (this.#countAttemptFailure(unit?.id, "http", classification.category)) {
|
|
1170
|
+
this.#observeHealth(failureEvent(refKey(model), classification));
|
|
1171
|
+
}
|
|
943
1172
|
}
|
|
944
1173
|
// ---------- retry hook ----------
|
|
945
1174
|
async #handleRetry(input, settings) {
|
|
@@ -952,7 +1181,12 @@ export class Supervisor {
|
|
|
952
1181
|
},
|
|
953
1182
|
status: input.error.status,
|
|
954
1183
|
});
|
|
955
|
-
this.#
|
|
1184
|
+
const unitID = this.#tracker.active(input.sessionID)?.id;
|
|
1185
|
+
// The pumped http-response event describes the same failed request;
|
|
1186
|
+
// counting both would double every attempt (ADP-019).
|
|
1187
|
+
if (this.#countAttemptFailure(unitID, "retry", classification.category)) {
|
|
1188
|
+
this.#observeHealth(failureEvent(refKey(input.model), classification));
|
|
1189
|
+
}
|
|
956
1190
|
// Recovery = fresh admission; the failed resource is already health-blocked.
|
|
957
1191
|
let recovery = { kind: "none" };
|
|
958
1192
|
let routingTier;
|
|
@@ -976,7 +1210,6 @@ export class Supervisor {
|
|
|
976
1210
|
catch (error) {
|
|
977
1211
|
logFailOpen("recovery", error);
|
|
978
1212
|
}
|
|
979
|
-
const unitID = this.#tracker.active(input.sessionID)?.id;
|
|
980
1213
|
const vetoesUsed = unitID !== undefined ? (this.#vetoes.get(unitID) ?? 0) : 0;
|
|
981
1214
|
const core = decideRetry({
|
|
982
1215
|
classification,
|
|
@@ -988,10 +1221,10 @@ export class Supervisor {
|
|
|
988
1221
|
const wrapped = retryForMode(core, this.#modeFor(input.sessionID, settings));
|
|
989
1222
|
if (wrapped.kind !== "veto") {
|
|
990
1223
|
// OBSERVE (and a declined veto) never runs the recovery episode, so an
|
|
991
|
-
// admitted probe has no continuation to report it —
|
|
1224
|
+
// admitted probe has no continuation to report it — release it here or
|
|
992
1225
|
// the resource wedges half-open.
|
|
993
1226
|
if (probeTicket !== undefined) {
|
|
994
|
-
this.#
|
|
1227
|
+
this.#health.releaseProbe(probeTicket);
|
|
995
1228
|
}
|
|
996
1229
|
return;
|
|
997
1230
|
}
|
|
@@ -1004,10 +1237,10 @@ export class Supervisor {
|
|
|
1004
1237
|
if (probeTicket !== undefined) {
|
|
1005
1238
|
// The session holds one pending probe slot. A still-pending older
|
|
1006
1239
|
// ticket belongs to a recovery whose continuation has not started (its
|
|
1007
|
-
// episode can no longer claim this slot) —
|
|
1240
|
+
// episode can no longer claim this slot) — release it before replacing.
|
|
1008
1241
|
const stale = this.#sessionProbeTickets.get(input.sessionID);
|
|
1009
1242
|
if (stale !== undefined && stale !== probeTicket) {
|
|
1010
|
-
this.#
|
|
1243
|
+
this.#health.releaseProbe(stale);
|
|
1011
1244
|
}
|
|
1012
1245
|
this.#sessionProbeTickets.set(input.sessionID, probeTicket);
|
|
1013
1246
|
}
|
|
@@ -1024,17 +1257,28 @@ export class Supervisor {
|
|
|
1024
1257
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- a veto implies the admission was an apply (the recovery exists only then)
|
|
1025
1258
|
decision: admission,
|
|
1026
1259
|
});
|
|
1027
|
-
void this.#ownRecovery(input.sessionID, wrapped.model, routingTier)
|
|
1260
|
+
void this.#ownRecovery(input.sessionID, wrapped.model, routingTier, () => this.#modeFor(input.sessionID, settings) !== "off")
|
|
1028
1261
|
.then(() => undefined)
|
|
1029
1262
|
.catch(async (error) => {
|
|
1263
|
+
if (error instanceof RecoveryAbortedError) {
|
|
1264
|
+
// The user steered away (interruption / OFF) during the settle: the
|
|
1265
|
+
// veto stands, but no continuation episode will ever carry the
|
|
1266
|
+
// probe it admitted — release it instead of wedging half-open.
|
|
1267
|
+
if (probeTicket !== undefined &&
|
|
1268
|
+
this.#sessionProbeTickets.get(input.sessionID) === probeTicket) {
|
|
1269
|
+
this.#sessionProbeTickets.delete(input.sessionID);
|
|
1270
|
+
this.#health.releaseProbe(probeTicket);
|
|
1271
|
+
}
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1030
1274
|
logFailOpen("recovery-ops", error);
|
|
1031
1275
|
// The recovery rejected before (or while) delivering its continuation
|
|
1032
|
-
// prompt, so no unit will ever carry the probe it admitted —
|
|
1276
|
+
// prompt, so no unit will ever carry the probe it admitted — release
|
|
1033
1277
|
// it instead of wedging the resource half-open.
|
|
1034
1278
|
if (probeTicket !== undefined &&
|
|
1035
1279
|
this.#sessionProbeTickets.get(input.sessionID) === probeTicket) {
|
|
1036
1280
|
this.#sessionProbeTickets.delete(input.sessionID);
|
|
1037
|
-
this.#
|
|
1281
|
+
this.#health.releaseProbe(probeTicket);
|
|
1038
1282
|
}
|
|
1039
1283
|
if (unitID === undefined || routingTier === undefined)
|
|
1040
1284
|
return;
|
|
@@ -1059,17 +1303,33 @@ export class Supervisor {
|
|
|
1059
1303
|
* continuation's prompt hook re-arms the admission marker (the unit must
|
|
1060
1304
|
* not be re-routed: that would discard the recovery/escalation decision).
|
|
1061
1305
|
*/
|
|
1062
|
-
async #ownRecovery(sessionID, model, tier) {
|
|
1306
|
+
async #ownRecovery(sessionID, model, tier, stillValid) {
|
|
1063
1307
|
if (tier !== undefined)
|
|
1064
1308
|
this.#continuationTiers.set(sessionID, tier);
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
recovery,
|
|
1069
|
-
continuationPossible: alive,
|
|
1070
|
-
});
|
|
1309
|
+
// The guard window opens before the first await so an interrupt pumped
|
|
1310
|
+
// during the admission race is still seen.
|
|
1311
|
+
this.#recoveryPending.add(sessionID);
|
|
1071
1312
|
try {
|
|
1072
|
-
|
|
1313
|
+
const alive = await this.#isSessionAlive(sessionID);
|
|
1314
|
+
const recovery = { kind: "switch", model };
|
|
1315
|
+
const action = planContinuation({
|
|
1316
|
+
recovery,
|
|
1317
|
+
continuationPossible: alive,
|
|
1318
|
+
});
|
|
1319
|
+
// executeContinuation settles first; the supervisor settles here so the
|
|
1320
|
+
// post-settle guard (mode OFF, user interruption) can abort before any
|
|
1321
|
+
// switch or prompt — the settle is the only step that can take
|
|
1322
|
+
// unbounded time, and a decision made before it may be stale after it.
|
|
1323
|
+
await this.#ops.settle(sessionID);
|
|
1324
|
+
if (!stillValid() || this.#recoveryInterrupted.delete(sessionID)) {
|
|
1325
|
+
throw new RecoveryAbortedError(sessionID);
|
|
1326
|
+
}
|
|
1327
|
+
// Already settled above; the action's own settle is a no-op.
|
|
1328
|
+
await executeContinuation({
|
|
1329
|
+
sessionID,
|
|
1330
|
+
action,
|
|
1331
|
+
ops: { ...this.#ops, settle: () => Promise.resolve() },
|
|
1332
|
+
});
|
|
1073
1333
|
}
|
|
1074
1334
|
catch (error) {
|
|
1075
1335
|
// The continuation prompt never reached the host, so no unit will open
|
|
@@ -1079,6 +1339,9 @@ export class Supervisor {
|
|
|
1079
1339
|
this.#continuationTiers.delete(sessionID);
|
|
1080
1340
|
throw error;
|
|
1081
1341
|
}
|
|
1342
|
+
finally {
|
|
1343
|
+
this.#recoveryPending.delete(sessionID);
|
|
1344
|
+
}
|
|
1082
1345
|
}
|
|
1083
1346
|
// ---------- parked-work resume (safe boundary: idle) ----------
|
|
1084
1347
|
async #onIdle(settings) {
|
|
@@ -1087,7 +1350,8 @@ export class Supervisor {
|
|
|
1087
1350
|
// pruned here (its per-session steering is worthless once the session is
|
|
1088
1351
|
// gone), in the same sweep that clears the other per-session maps.
|
|
1089
1352
|
const overrideIDs = this.#sessionModes.snapshot().map((record) => record.sessionID);
|
|
1090
|
-
|
|
1353
|
+
const tierRequestIDs = this.#tierRequests.snapshot().map((record) => record.sessionID);
|
|
1354
|
+
if (records.length === 0 && overrideIDs.length === 0 && tierRequestIDs.length === 0)
|
|
1091
1355
|
return;
|
|
1092
1356
|
// OFF sessions are not supervised: their parked records are neither
|
|
1093
1357
|
// dropped nor resumed, and no alive probe runs for them.
|
|
@@ -1100,7 +1364,7 @@ export class Supervisor {
|
|
|
1100
1364
|
}
|
|
1101
1365
|
// Tier-request holders join the alive sweep too: a dead session's sticky
|
|
1102
1366
|
// tier request dies with its session, like the other per-session maps.
|
|
1103
|
-
for (const sessionID of
|
|
1367
|
+
for (const sessionID of tierRequestIDs) {
|
|
1104
1368
|
if (!alive.has(sessionID))
|
|
1105
1369
|
alive.set(sessionID, await this.#isSessionAlive(sessionID));
|
|
1106
1370
|
}
|
|
@@ -1114,9 +1378,18 @@ export class Supervisor {
|
|
|
1114
1378
|
await this.#dropParked(dropped);
|
|
1115
1379
|
}
|
|
1116
1380
|
// Tier requests die with their session, like the other per-session maps.
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1381
|
+
// The fire-and-forget note means the queue may still hold writes; the
|
|
1382
|
+
// serialized set applies after them. A write failure just leaves the
|
|
1383
|
+
// record (fail-open — it will die with the session on the next sweep).
|
|
1384
|
+
for (const sessionID of tierRequestIDs) {
|
|
1385
|
+
if (alive.get(sessionID) === false) {
|
|
1386
|
+
try {
|
|
1387
|
+
await this.#tierRequests.set(sessionID, null);
|
|
1388
|
+
}
|
|
1389
|
+
catch (error) {
|
|
1390
|
+
logFailOpen("tier-requests", error);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1120
1393
|
}
|
|
1121
1394
|
// At most ONE continuation per session per sweep: prompting a session
|
|
1122
1395
|
// with an active unit would supersede the user's in-flight episode (not
|
|
@@ -1138,6 +1411,7 @@ export class Supervisor {
|
|
|
1138
1411
|
this.#continuationTiers.delete(sessionID);
|
|
1139
1412
|
this.#promptAdmission.delete(sessionID);
|
|
1140
1413
|
this.#continuationPending.delete(sessionID);
|
|
1414
|
+
this.#escalations.delete(sessionID);
|
|
1141
1415
|
if (this.#sessionModes.get(sessionID) !== null) {
|
|
1142
1416
|
try {
|
|
1143
1417
|
await this.#sessionModes.set(sessionID, null);
|
|
@@ -1163,14 +1437,19 @@ export class Supervisor {
|
|
|
1163
1437
|
// report's session's mode.
|
|
1164
1438
|
const mode = this.#modeFor(record.sessionID, settings);
|
|
1165
1439
|
let decision;
|
|
1440
|
+
let probeTicket;
|
|
1166
1441
|
try {
|
|
1167
|
-
|
|
1442
|
+
const admission = await this.#admit(settings, record.sessionID, record.tier);
|
|
1443
|
+
decision = admission.decision;
|
|
1444
|
+
probeTicket = admission.probeTicket;
|
|
1168
1445
|
}
|
|
1169
1446
|
catch (error) {
|
|
1170
1447
|
logFailOpen("resume", error);
|
|
1171
1448
|
return;
|
|
1172
1449
|
}
|
|
1173
1450
|
if (mode === "observe") {
|
|
1451
|
+
if (probeTicket !== undefined)
|
|
1452
|
+
this.#health.releaseProbe(probeTicket);
|
|
1174
1453
|
await this.#recordReceipt({
|
|
1175
1454
|
sessionID: record.sessionID,
|
|
1176
1455
|
unitID: record.id,
|
|
@@ -1180,7 +1459,9 @@ export class Supervisor {
|
|
|
1180
1459
|
return;
|
|
1181
1460
|
}
|
|
1182
1461
|
if (decision.kind !== "apply") {
|
|
1183
|
-
// Still no usable resource (or the routing deferred) — stay parked.
|
|
1462
|
+
// Still no usable resource (or the routing deferred) — stay parked. The
|
|
1463
|
+
// admission held no probe ticket: #admit keeps a ticket only for apply
|
|
1464
|
+
// decisions and releases every other one itself.
|
|
1184
1465
|
await this.#recordReceipt({
|
|
1185
1466
|
sessionID: record.sessionID,
|
|
1186
1467
|
unitID: record.id,
|
|
@@ -1190,8 +1471,11 @@ export class Supervisor {
|
|
|
1190
1471
|
return;
|
|
1191
1472
|
}
|
|
1192
1473
|
const popped = await this.#popParked(record);
|
|
1193
|
-
if (popped === undefined)
|
|
1474
|
+
if (popped === undefined) {
|
|
1475
|
+
if (probeTicket !== undefined)
|
|
1476
|
+
this.#health.releaseProbe(probeTicket);
|
|
1194
1477
|
return;
|
|
1478
|
+
}
|
|
1195
1479
|
// The resume continuation is not re-admitted (owned continuation), so
|
|
1196
1480
|
// the freshly routed admission is recorded here, unit-less; the unit
|
|
1197
1481
|
// that opens next adopts it and carries the outcome.
|
|
@@ -1204,10 +1488,23 @@ export class Supervisor {
|
|
|
1204
1488
|
// freshly routed model, then prompt — the episode's first LLM call must
|
|
1205
1489
|
// not hit the model the work was parked away from.
|
|
1206
1490
|
try {
|
|
1207
|
-
|
|
1491
|
+
if (probeTicket !== undefined)
|
|
1492
|
+
this.#sessionProbeTickets.set(record.sessionID, probeTicket);
|
|
1493
|
+
await this.#ownRecovery(record.sessionID, decision.model, record.tier,
|
|
1494
|
+
// The parked session's own effective mode and its still-open unit
|
|
1495
|
+
// window are re-checked after the settle; a session that gained an
|
|
1496
|
+
// active unit must not be prompted (not a safe boundary).
|
|
1497
|
+
() => this.#modeFor(record.sessionID, settings) !== "off" &&
|
|
1498
|
+
this.#tracker.active(record.sessionID) === undefined);
|
|
1208
1499
|
}
|
|
1209
1500
|
catch (error) {
|
|
1210
|
-
|
|
1501
|
+
if (probeTicket !== undefined &&
|
|
1502
|
+
this.#sessionProbeTickets.get(record.sessionID) === probeTicket) {
|
|
1503
|
+
this.#sessionProbeTickets.delete(record.sessionID);
|
|
1504
|
+
this.#health.releaseProbe(probeTicket);
|
|
1505
|
+
}
|
|
1506
|
+
if (!(error instanceof RecoveryAbortedError))
|
|
1507
|
+
logFailOpen("resume", error);
|
|
1211
1508
|
await this.#rePark(record);
|
|
1212
1509
|
}
|
|
1213
1510
|
}
|
|
@@ -1238,8 +1535,14 @@ export class Supervisor {
|
|
|
1238
1535
|
await this.#options.session.get({ sessionID });
|
|
1239
1536
|
return true;
|
|
1240
1537
|
}
|
|
1241
|
-
catch {
|
|
1242
|
-
|
|
1538
|
+
catch (error) {
|
|
1539
|
+
// The host rejects `session.get` for a missing session with a plain
|
|
1540
|
+
// error and exposes no typed status; only a definitive not-found
|
|
1541
|
+
// counts as dead. Anything else is treated as alive (fail open toward
|
|
1542
|
+
// keeping durable user state, ADP-031): a transient storage or host
|
|
1543
|
+
// error must never prune parked work, mode overrides, or tier
|
|
1544
|
+
// requests — the next sweep re-probes instead.
|
|
1545
|
+
return !/not found/i.test(String(error));
|
|
1243
1546
|
}
|
|
1244
1547
|
}
|
|
1245
1548
|
async #recordReceipt(input) {
|