@coool/opencode-adaptive 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +98 -2
- package/dist/adapters/codex-capacity/index.d.ts +5 -0
- package/dist/adapters/codex-capacity/index.d.ts.map +1 -0
- package/dist/adapters/codex-capacity/index.js +5 -0
- package/dist/adapters/codex-capacity/index.js.map +1 -0
- package/dist/adapters/codex-capacity/parse.d.ts +28 -0
- package/dist/adapters/codex-capacity/parse.d.ts.map +1 -0
- package/dist/adapters/codex-capacity/parse.js +130 -0
- package/dist/adapters/codex-capacity/parse.js.map +1 -0
- package/dist/adapters/codex-capacity/reading.d.ts +10 -0
- package/dist/adapters/codex-capacity/reading.d.ts.map +1 -0
- package/dist/adapters/codex-capacity/reading.js +45 -0
- package/dist/adapters/codex-capacity/reading.js.map +1 -0
- package/dist/adapters/codex-capacity/sensor.d.ts +24 -0
- package/dist/adapters/codex-capacity/sensor.d.ts.map +1 -0
- package/dist/adapters/codex-capacity/sensor.js +30 -0
- package/dist/adapters/codex-capacity/sensor.js.map +1 -0
- package/dist/adapters/codex-capacity/types.d.ts +35 -0
- package/dist/adapters/codex-capacity/types.d.ts.map +1 -0
- package/dist/adapters/codex-capacity/types.js +4 -0
- package/dist/adapters/codex-capacity/types.js.map +1 -0
- package/dist/adapters/opencode/escalation-tool.d.ts +52 -0
- package/dist/adapters/opencode/escalation-tool.d.ts.map +1 -0
- package/dist/adapters/opencode/escalation-tool.js +59 -0
- package/dist/adapters/opencode/escalation-tool.js.map +1 -0
- package/dist/adapters/opencode/events.d.ts +24 -0
- package/dist/adapters/opencode/events.d.ts.map +1 -0
- package/dist/adapters/opencode/events.js +68 -0
- package/dist/adapters/opencode/events.js.map +1 -0
- package/dist/adapters/opencode/hooks.d.ts +35 -0
- package/dist/adapters/opencode/hooks.d.ts.map +1 -0
- package/dist/adapters/opencode/hooks.js +12 -0
- package/dist/adapters/opencode/hooks.js.map +1 -0
- package/dist/adapters/opencode/index.d.ts +17 -0
- package/dist/adapters/opencode/index.d.ts.map +1 -0
- package/dist/adapters/opencode/index.js +9 -0
- package/dist/adapters/opencode/index.js.map +1 -0
- package/dist/adapters/opencode/inventory.d.ts +31 -0
- package/dist/adapters/opencode/inventory.d.ts.map +1 -0
- package/dist/adapters/opencode/inventory.js +53 -0
- package/dist/adapters/opencode/inventory.js.map +1 -0
- package/dist/adapters/opencode/report.d.ts +92 -0
- package/dist/adapters/opencode/report.d.ts.map +1 -0
- package/dist/adapters/opencode/report.js +145 -0
- package/dist/adapters/opencode/report.js.map +1 -0
- package/dist/adapters/opencode/session-ops.d.ts +40 -0
- package/dist/adapters/opencode/session-ops.d.ts.map +1 -0
- package/dist/adapters/opencode/session-ops.js +32 -0
- package/dist/adapters/opencode/session-ops.js.map +1 -0
- package/dist/adapters/opencode/storage.d.ts +16 -0
- package/dist/adapters/opencode/storage.d.ts.map +1 -0
- package/dist/adapters/opencode/storage.js +8 -0
- package/dist/adapters/opencode/storage.js.map +1 -0
- package/dist/adapters/opencode/tier-tool.d.ts +38 -0
- package/dist/adapters/opencode/tier-tool.d.ts.map +1 -0
- package/dist/adapters/opencode/tier-tool.js +72 -0
- package/dist/adapters/opencode/tier-tool.js.map +1 -0
- package/dist/adapters/opencode/types.d.ts +102 -0
- package/dist/adapters/opencode/types.d.ts.map +1 -0
- package/dist/adapters/opencode/types.js +2 -0
- package/dist/adapters/opencode/types.js.map +1 -0
- package/dist/core/compatibility/gate.d.ts +10 -0
- package/dist/core/compatibility/gate.d.ts.map +1 -0
- package/dist/core/compatibility/gate.js +55 -0
- package/dist/core/compatibility/gate.js.map +1 -0
- package/dist/core/compatibility/index.d.ts +3 -0
- package/dist/core/compatibility/index.d.ts.map +1 -0
- package/dist/core/compatibility/index.js +3 -0
- package/dist/core/compatibility/index.js.map +1 -0
- package/dist/core/compatibility/types.d.ts +54 -0
- package/dist/core/compatibility/types.d.ts.map +1 -0
- package/dist/core/compatibility/types.js +27 -0
- package/dist/core/compatibility/types.js.map +1 -0
- package/dist/core/scheduler/admit.d.ts +7 -0
- package/dist/core/scheduler/admit.d.ts.map +1 -0
- package/dist/core/scheduler/admit.js +153 -0
- package/dist/core/scheduler/admit.js.map +1 -0
- package/dist/core/scheduler/escalate.d.ts +7 -0
- package/dist/core/scheduler/escalate.d.ts.map +1 -0
- package/dist/core/scheduler/escalate.js +56 -0
- package/dist/core/scheduler/escalate.js.map +1 -0
- package/dist/core/scheduler/index.d.ts +5 -0
- package/dist/core/scheduler/index.d.ts.map +1 -0
- package/dist/core/scheduler/index.js +5 -0
- package/dist/core/scheduler/index.js.map +1 -0
- package/dist/core/scheduler/mode.d.ts +9 -0
- package/dist/core/scheduler/mode.d.ts.map +1 -0
- package/dist/core/scheduler/mode.js +10 -0
- package/dist/core/scheduler/mode.js.map +1 -0
- package/dist/core/scheduler/types.d.ts +143 -0
- package/dist/core/scheduler/types.d.ts.map +1 -0
- package/dist/core/scheduler/types.js +74 -0
- package/dist/core/scheduler/types.js.map +1 -0
- package/dist/core/tier-policy/index.d.ts +4 -0
- package/dist/core/tier-policy/index.d.ts.map +1 -0
- package/dist/core/tier-policy/index.js +4 -0
- package/dist/core/tier-policy/index.js.map +1 -0
- package/dist/core/tier-policy/resolve.d.ts +19 -0
- package/dist/core/tier-policy/resolve.d.ts.map +1 -0
- package/dist/core/tier-policy/resolve.js +52 -0
- package/dist/core/tier-policy/resolve.js.map +1 -0
- package/dist/core/tier-policy/tier-list.d.ts +11 -0
- package/dist/core/tier-policy/tier-list.d.ts.map +1 -0
- package/dist/core/tier-policy/tier-list.js +23 -0
- package/dist/core/tier-policy/tier-list.js.map +1 -0
- package/dist/core/tier-policy/types.d.ts +85 -0
- package/dist/core/tier-policy/types.d.ts.map +1 -0
- package/dist/core/tier-policy/types.js +45 -0
- package/dist/core/tier-policy/types.js.map +1 -0
- package/dist/core/work-unit/index.d.ts +4 -0
- package/dist/core/work-unit/index.d.ts.map +1 -0
- package/dist/core/work-unit/index.js +4 -0
- package/dist/core/work-unit/index.js.map +1 -0
- package/dist/core/work-unit/interruption.d.ts +8 -0
- package/dist/core/work-unit/interruption.d.ts.map +1 -0
- package/dist/core/work-unit/interruption.js +12 -0
- package/dist/core/work-unit/interruption.js.map +1 -0
- package/dist/core/work-unit/tracker.d.ts +46 -0
- package/dist/core/work-unit/tracker.d.ts.map +1 -0
- package/dist/core/work-unit/tracker.js +136 -0
- package/dist/core/work-unit/tracker.js.map +1 -0
- package/dist/core/work-unit/types.d.ts +146 -0
- package/dist/core/work-unit/types.d.ts.map +1 -0
- package/dist/core/work-unit/types.js +23 -0
- package/dist/core/work-unit/types.js.map +1 -0
- package/dist/execution/continuation/execute.d.ts +19 -0
- package/dist/execution/continuation/execute.d.ts.map +1 -0
- package/dist/execution/continuation/execute.js +31 -0
- package/dist/execution/continuation/execute.js.map +1 -0
- package/dist/execution/continuation/index.d.ts +6 -0
- package/dist/execution/continuation/index.d.ts.map +1 -0
- package/dist/execution/continuation/index.js +5 -0
- package/dist/execution/continuation/index.js.map +1 -0
- package/dist/execution/continuation/ledger.d.ts +20 -0
- package/dist/execution/continuation/ledger.d.ts.map +1 -0
- package/dist/execution/continuation/ledger.js +31 -0
- package/dist/execution/continuation/ledger.js.map +1 -0
- package/dist/execution/continuation/plan.d.ts +13 -0
- package/dist/execution/continuation/plan.d.ts.map +1 -0
- package/dist/execution/continuation/plan.js +20 -0
- package/dist/execution/continuation/plan.js.map +1 -0
- package/dist/execution/continuation/prompts.d.ts +8 -0
- package/dist/execution/continuation/prompts.d.ts.map +1 -0
- package/dist/execution/continuation/prompts.js +8 -0
- package/dist/execution/continuation/prompts.js.map +1 -0
- package/dist/execution/continuation/types.d.ts +70 -0
- package/dist/execution/continuation/types.d.ts.map +1 -0
- package/dist/execution/continuation/types.js +15 -0
- package/dist/execution/continuation/types.js.map +1 -0
- package/dist/execution/escalation/index.d.ts +2 -0
- package/dist/execution/escalation/index.d.ts.map +1 -0
- package/dist/execution/escalation/index.js +2 -0
- package/dist/execution/escalation/index.js.map +1 -0
- package/dist/execution/escalation/types.d.ts +31 -0
- package/dist/execution/escalation/types.d.ts.map +1 -0
- package/dist/execution/escalation/types.js +49 -0
- package/dist/execution/escalation/types.js.map +1 -0
- package/dist/execution/index.d.ts +5 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +5 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/parking/index.d.ts +3 -0
- package/dist/execution/parking/index.d.ts.map +1 -0
- package/dist/execution/parking/index.js +2 -0
- package/dist/execution/parking/index.js.map +1 -0
- package/dist/execution/parking/plan.d.ts +40 -0
- package/dist/execution/parking/plan.d.ts.map +1 -0
- package/dist/execution/parking/plan.js +34 -0
- package/dist/execution/parking/plan.js.map +1 -0
- package/dist/execution/recovery/gate.d.ts +20 -0
- package/dist/execution/recovery/gate.d.ts.map +1 -0
- package/dist/execution/recovery/gate.js +23 -0
- package/dist/execution/recovery/gate.js.map +1 -0
- package/dist/execution/recovery/index.d.ts +3 -0
- package/dist/execution/recovery/index.d.ts.map +1 -0
- package/dist/execution/recovery/index.js +2 -0
- package/dist/execution/recovery/index.js.map +1 -0
- package/dist/execution/retry/decide.d.ts +30 -0
- package/dist/execution/retry/decide.d.ts.map +1 -0
- package/dist/execution/retry/decide.js +76 -0
- package/dist/execution/retry/decide.js.map +1 -0
- package/dist/execution/retry/index.d.ts +3 -0
- package/dist/execution/retry/index.d.ts.map +1 -0
- package/dist/execution/retry/index.js +3 -0
- package/dist/execution/retry/index.js.map +1 -0
- package/dist/execution/retry/types.d.ts +66 -0
- package/dist/execution/retry/types.d.ts.map +1 -0
- package/dist/execution/retry/types.js +9 -0
- package/dist/execution/retry/types.js.map +1 -0
- package/dist/gui-experimental.cjs +1 -0
- package/dist/gui.d.ts +17 -0
- package/dist/gui.d.ts.map +1 -0
- package/dist/gui.js +70 -0
- package/dist/gui.js.map +1 -0
- package/dist/health/capacity/index.d.ts +4 -0
- package/dist/health/capacity/index.d.ts.map +1 -0
- package/dist/health/capacity/index.js +4 -0
- package/dist/health/capacity/index.js.map +1 -0
- package/dist/health/capacity/merge.d.ts +58 -0
- package/dist/health/capacity/merge.d.ts.map +1 -0
- package/dist/health/capacity/merge.js +154 -0
- package/dist/health/capacity/merge.js.map +1 -0
- package/dist/health/capacity/registry.d.ts +41 -0
- package/dist/health/capacity/registry.d.ts.map +1 -0
- package/dist/health/capacity/registry.js +106 -0
- package/dist/health/capacity/registry.js.map +1 -0
- package/dist/health/capacity/types.d.ts +74 -0
- package/dist/health/capacity/types.d.ts.map +1 -0
- package/dist/health/capacity/types.js +6 -0
- package/dist/health/capacity/types.js.map +1 -0
- package/dist/health/circuit-breaker/breaker.d.ts +84 -0
- package/dist/health/circuit-breaker/breaker.d.ts.map +1 -0
- package/dist/health/circuit-breaker/breaker.js +132 -0
- package/dist/health/circuit-breaker/breaker.js.map +1 -0
- package/dist/health/circuit-breaker/index.d.ts +3 -0
- package/dist/health/circuit-breaker/index.d.ts.map +1 -0
- package/dist/health/circuit-breaker/index.js +3 -0
- package/dist/health/circuit-breaker/index.js.map +1 -0
- package/dist/health/circuit-breaker/types.d.ts +53 -0
- package/dist/health/circuit-breaker/types.d.ts.map +1 -0
- package/dist/health/circuit-breaker/types.js +13 -0
- package/dist/health/circuit-breaker/types.js.map +1 -0
- package/dist/health/classifier/classify.d.ts +9 -0
- package/dist/health/classifier/classify.d.ts.map +1 -0
- package/dist/health/classifier/classify.js +245 -0
- package/dist/health/classifier/classify.js.map +1 -0
- package/dist/health/classifier/index.d.ts +3 -0
- package/dist/health/classifier/index.d.ts.map +1 -0
- package/dist/health/classifier/index.js +3 -0
- package/dist/health/classifier/index.js.map +1 -0
- package/dist/health/classifier/types.d.ts +72 -0
- package/dist/health/classifier/types.d.ts.map +1 -0
- package/dist/health/classifier/types.js +27 -0
- package/dist/health/classifier/types.js.map +1 -0
- package/dist/health/index.d.ts +5 -0
- package/dist/health/index.d.ts.map +1 -0
- package/dist/health/index.js +5 -0
- package/dist/health/index.js.map +1 -0
- package/dist/health/resource-health/index.d.ts +5 -0
- package/dist/health/resource-health/index.d.ts.map +1 -0
- package/dist/health/resource-health/index.js +5 -0
- package/dist/health/resource-health/index.js.map +1 -0
- package/dist/health/resource-health/key.d.ts +19 -0
- package/dist/health/resource-health/key.d.ts.map +1 -0
- package/dist/health/resource-health/key.js +54 -0
- package/dist/health/resource-health/key.js.map +1 -0
- package/dist/health/resource-health/registry.d.ts +41 -0
- package/dist/health/resource-health/registry.d.ts.map +1 -0
- package/dist/health/resource-health/registry.js +258 -0
- package/dist/health/resource-health/registry.js.map +1 -0
- package/dist/health/resource-health/transitions.d.ts +11 -0
- package/dist/health/resource-health/transitions.d.ts.map +1 -0
- package/dist/health/resource-health/transitions.js +165 -0
- package/dist/health/resource-health/transitions.js.map +1 -0
- package/dist/health/resource-health/types.d.ts +116 -0
- package/dist/health/resource-health/types.d.ts.map +1 -0
- package/dist/health/resource-health/types.js +43 -0
- package/dist/health/resource-health/types.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-id.d.ts +3 -0
- package/dist/plugin-id.d.ts.map +1 -0
- package/dist/plugin-id.js +3 -0
- package/dist/plugin-id.js.map +1 -0
- package/dist/rpc/definition.d.ts +100 -0
- package/dist/rpc/definition.d.ts.map +1 -0
- package/dist/rpc/definition.js +104 -0
- package/dist/rpc/definition.js.map +1 -0
- package/dist/rpc/handlers.d.ts +65 -0
- package/dist/rpc/handlers.d.ts.map +1 -0
- package/dist/rpc/handlers.js +111 -0
- package/dist/rpc/handlers.js.map +1 -0
- package/dist/rpc/status.d.ts +43 -0
- package/dist/rpc/status.d.ts.map +1 -0
- package/dist/rpc/status.js +45 -0
- package/dist/rpc/status.js.map +1 -0
- package/dist/rpc.d.ts +11 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +8 -0
- package/dist/rpc.js.map +1 -0
- package/dist/state/capacity/index.d.ts +4 -0
- package/dist/state/capacity/index.d.ts.map +1 -0
- package/dist/state/capacity/index.js +4 -0
- package/dist/state/capacity/index.js.map +1 -0
- package/dist/state/capacity/parse.d.ts +3 -0
- package/dist/state/capacity/parse.d.ts.map +1 -0
- package/dist/state/capacity/parse.js +110 -0
- package/dist/state/capacity/parse.js.map +1 -0
- package/dist/state/capacity/store.d.ts +19 -0
- package/dist/state/capacity/store.d.ts.map +1 -0
- package/dist/state/capacity/store.js +64 -0
- package/dist/state/capacity/store.js.map +1 -0
- package/dist/state/capacity/types.d.ts +38 -0
- package/dist/state/capacity/types.d.ts.map +1 -0
- package/dist/state/capacity/types.js +11 -0
- package/dist/state/capacity/types.js.map +1 -0
- package/dist/state/health/index.d.ts +5 -0
- package/dist/state/health/index.d.ts.map +1 -0
- package/dist/state/health/index.js +4 -0
- package/dist/state/health/index.js.map +1 -0
- package/dist/state/health/parse.d.ts +3 -0
- package/dist/state/health/parse.d.ts.map +1 -0
- package/dist/state/health/parse.js +121 -0
- package/dist/state/health/parse.js.map +1 -0
- package/dist/state/health/store.d.ts +18 -0
- package/dist/state/health/store.d.ts.map +1 -0
- package/dist/state/health/store.js +55 -0
- package/dist/state/health/store.js.map +1 -0
- package/dist/state/health/types.d.ts +24 -0
- package/dist/state/health/types.d.ts.map +1 -0
- package/dist/state/health/types.js +5 -0
- package/dist/state/health/types.js.map +1 -0
- package/dist/state/parked-work/index.d.ts +5 -0
- package/dist/state/parked-work/index.d.ts.map +1 -0
- package/dist/state/parked-work/index.js +4 -0
- package/dist/state/parked-work/index.js.map +1 -0
- package/dist/state/parked-work/parse.d.ts +7 -0
- package/dist/state/parked-work/parse.d.ts.map +1 -0
- package/dist/state/parked-work/parse.js +83 -0
- package/dist/state/parked-work/parse.js.map +1 -0
- package/dist/state/parked-work/store.d.ts +18 -0
- package/dist/state/parked-work/store.d.ts.map +1 -0
- package/dist/state/parked-work/store.js +81 -0
- package/dist/state/parked-work/store.js.map +1 -0
- package/dist/state/parked-work/types.d.ts +42 -0
- package/dist/state/parked-work/types.d.ts.map +1 -0
- package/dist/state/parked-work/types.js +7 -0
- package/dist/state/parked-work/types.js.map +1 -0
- package/dist/state/receipts/bound.d.ts +15 -0
- package/dist/state/receipts/bound.d.ts.map +1 -0
- package/dist/state/receipts/bound.js +77 -0
- package/dist/state/receipts/bound.js.map +1 -0
- package/dist/state/receipts/index.d.ts +7 -0
- package/dist/state/receipts/index.d.ts.map +1 -0
- package/dist/state/receipts/index.js +5 -0
- package/dist/state/receipts/index.js.map +1 -0
- package/dist/state/receipts/parse.d.ts +7 -0
- package/dist/state/receipts/parse.d.ts.map +1 -0
- package/dist/state/receipts/parse.js +226 -0
- package/dist/state/receipts/parse.js.map +1 -0
- package/dist/state/receipts/store.d.ts +35 -0
- package/dist/state/receipts/store.d.ts.map +1 -0
- package/dist/state/receipts/store.js +128 -0
- package/dist/state/receipts/store.js.map +1 -0
- package/dist/state/receipts/types.d.ts +62 -0
- package/dist/state/receipts/types.d.ts.map +1 -0
- package/dist/state/receipts/types.js +12 -0
- package/dist/state/receipts/types.js.map +1 -0
- package/dist/state/session-modes/index.d.ts +5 -0
- package/dist/state/session-modes/index.d.ts.map +1 -0
- package/dist/state/session-modes/index.js +4 -0
- package/dist/state/session-modes/index.js.map +1 -0
- package/dist/state/session-modes/parse.d.ts +9 -0
- package/dist/state/session-modes/parse.d.ts.map +1 -0
- package/dist/state/session-modes/parse.js +62 -0
- package/dist/state/session-modes/parse.js.map +1 -0
- package/dist/state/session-modes/store.d.ts +21 -0
- package/dist/state/session-modes/store.d.ts.map +1 -0
- package/dist/state/session-modes/store.js +73 -0
- package/dist/state/session-modes/store.js.map +1 -0
- package/dist/state/session-modes/types.d.ts +42 -0
- package/dist/state/session-modes/types.d.ts.map +1 -0
- package/dist/state/session-modes/types.js +7 -0
- package/dist/state/session-modes/types.js.map +1 -0
- package/dist/state/settings/index.d.ts +5 -0
- package/dist/state/settings/index.d.ts.map +1 -0
- package/dist/state/settings/index.js +4 -0
- package/dist/state/settings/index.js.map +1 -0
- package/dist/state/settings/parse.d.ts +34 -0
- package/dist/state/settings/parse.d.ts.map +1 -0
- package/dist/state/settings/parse.js +261 -0
- package/dist/state/settings/parse.js.map +1 -0
- package/dist/state/settings/store.d.ts +22 -0
- package/dist/state/settings/store.d.ts.map +1 -0
- package/dist/state/settings/store.js +57 -0
- package/dist/state/settings/store.js.map +1 -0
- package/dist/state/settings/types.d.ts +63 -0
- package/dist/state/settings/types.d.ts.map +1 -0
- package/dist/state/settings/types.js +12 -0
- package/dist/state/settings/types.js.map +1 -0
- package/dist/supervisor/index.d.ts +5 -0
- package/dist/supervisor/index.d.ts.map +1 -0
- package/dist/supervisor/index.js +4 -0
- package/dist/supervisor/index.js.map +1 -0
- package/dist/supervisor/log.d.ts +11 -0
- package/dist/supervisor/log.d.ts.map +1 -0
- package/dist/supervisor/log.js +14 -0
- package/dist/supervisor/log.js.map +1 -0
- package/dist/supervisor/supervisor.d.ts +69 -0
- package/dist/supervisor/supervisor.d.ts.map +1 -0
- package/dist/supervisor/supervisor.js +1343 -0
- package/dist/supervisor/supervisor.js.map +1 -0
- package/dist/supervisor/types.d.ts +50 -0
- package/dist/supervisor/types.d.ts.map +1 -0
- package/dist/supervisor/types.js +3 -0
- package/dist/supervisor/types.js.map +1 -0
- package/dist/supervisor/view.d.ts +4 -0
- package/dist/supervisor/view.d.ts.map +1 -0
- package/dist/supervisor/view.js +4 -0
- package/dist/supervisor/view.js.map +1 -0
- package/dist/tui.d.ts +16 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +252 -0
- package/dist/tui.js.map +1 -0
- package/dist/ui/gui/api.d.ts +18 -0
- package/dist/ui/gui/api.d.ts.map +1 -0
- package/dist/ui/gui/api.js +21 -0
- package/dist/ui/gui/api.js.map +1 -0
- package/dist/ui/gui/view.d.ts +27 -0
- package/dist/ui/gui/view.d.ts.map +1 -0
- package/dist/ui/gui/view.js +49 -0
- package/dist/ui/gui/view.js.map +1 -0
- package/dist/ui/tui/controller.d.ts +98 -0
- package/dist/ui/tui/controller.d.ts.map +1 -0
- package/dist/ui/tui/controller.js +399 -0
- package/dist/ui/tui/controller.js.map +1 -0
- package/dist/ui/tui/menu.d.ts +56 -0
- package/dist/ui/tui/menu.d.ts.map +1 -0
- package/dist/ui/tui/menu.js +176 -0
- package/dist/ui/tui/menu.js.map +1 -0
- package/dist/ui/tui/options.d.ts +20 -0
- package/dist/ui/tui/options.d.ts.map +1 -0
- package/dist/ui/tui/options.js +17 -0
- package/dist/ui/tui/options.js.map +1 -0
- package/dist/ui/tui/receipts.d.ts +22 -0
- package/dist/ui/tui/receipts.d.ts.map +1 -0
- package/dist/ui/tui/receipts.js +109 -0
- package/dist/ui/tui/receipts.js.map +1 -0
- package/dist/ui/tui/status.d.ts +23 -0
- package/dist/ui/tui/status.d.ts.map +1 -0
- package/dist/ui/tui/status.js +90 -0
- package/dist/ui/tui/status.js.map +1 -0
- package/dist/ui/tui/tiers.d.ts +52 -0
- package/dist/ui/tui/tiers.d.ts.map +1 -0
- package/dist/ui/tui/tiers.js +108 -0
- package/dist/ui/tui/tiers.js.map +1 -0
- package/package.json +85 -4
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
/**
|
|
3
|
+
* The supervisor (ADP-025..ADP-032): owns all state stores and glues the pure
|
|
4
|
+
* core to the host reports. It contains no policy of its own — every decision
|
|
5
|
+
* comes from the pure scheduler/retry/tier modules — and it applies the two
|
|
6
|
+
* wiring contracts: fail-open (ADP-031, internal failures log and defer) and
|
|
7
|
+
* Observe ≡ Control (the same admission path, only the mode wrapper differs).
|
|
8
|
+
*
|
|
9
|
+
* Wiring decisions recorded here (see docs/ARCHITECTURE.md):
|
|
10
|
+
* - admission runs only on `execution-started` (a safe boundary); auxiliary
|
|
11
|
+
* per-request routing is not applied in v1 because switching the model for
|
|
12
|
+
* an auxiliary request would also move the primary model.
|
|
13
|
+
* - park records arise only from owned-recovery failures and resume
|
|
14
|
+
* boundaries; admission cannot hold an already-started episode, so an
|
|
15
|
+
* admission `park` decision is recorded as a receipt without a parked
|
|
16
|
+
* record (the episode proceeds on OpenCode's own model).
|
|
17
|
+
* - retry-hook veto decisions are not receipts (they are not scheduler
|
|
18
|
+
* decisions); admission receipts plus outcome attachment cover ADP-030.
|
|
19
|
+
* - interruption reasons outside the known vocabulary leave the unit open
|
|
20
|
+
* (a later episode supersedes it); no outcome is ever fabricated.
|
|
21
|
+
* - escalation (ADP-015..017) triggers only on capability-classified unit
|
|
22
|
+
* failures (context overflow, content policy); infrastructure failures
|
|
23
|
+
* never escalate, and the per-unit budget is finite.
|
|
24
|
+
*/
|
|
25
|
+
import { WorkUnitTracker } from "../core/work-unit/tracker.js";
|
|
26
|
+
import { sameModelRef, } from "../core/work-unit/types.js";
|
|
27
|
+
import { INTERRUPT_REASONS } from "../core/work-unit/interruption.js";
|
|
28
|
+
import { resolveRouting } from "../core/tier-policy/resolve.js";
|
|
29
|
+
import { TIERS, nextTier } from "../core/tier-policy/types.js";
|
|
30
|
+
import { admit, forMode as schedulerForMode, planEscalation, schedulerHealthKey, } from "../core/scheduler/index.js";
|
|
31
|
+
import { MAX_ESCALATION_EVIDENCE } from "../core/scheduler/types.js";
|
|
32
|
+
import { checkCompatibility } from "../core/compatibility/gate.js";
|
|
33
|
+
import { classifyFailure, CapacityRegistry, ResourceHealthRegistry } from "../health/index.js";
|
|
34
|
+
import { sweepRecord } from "../health/resource-health/transitions.js";
|
|
35
|
+
import { HealthRecordStore, HEALTH_KEY } from "../state/health/index.js";
|
|
36
|
+
import { CapacityReadingStore, CAPACITY_KEY } from "../state/capacity/index.js";
|
|
37
|
+
import { mergeReadings } from "../health/capacity/merge.js";
|
|
38
|
+
import { resourceChain, resourceKeyID } from "../health/resource-health/key.js";
|
|
39
|
+
import { SettingsStore, SETTINGS_KEY } from "../state/settings/index.js";
|
|
40
|
+
import { SessionModeStore, SESSION_MODES_KEY } from "../state/session-modes/index.js";
|
|
41
|
+
import { ReceiptStore, RECEIPTS_KEY } from "../state/receipts/index.js";
|
|
42
|
+
import { buildStatus } from "../rpc/status.js";
|
|
43
|
+
import { RECEIPTS_VIEW_LIMIT } from "./view.js";
|
|
44
|
+
import { ParkedWorkStore, PARKED_KEY } from "../state/parked-work/index.js";
|
|
45
|
+
import { decideRetry, executeContinuation, forMode as retryForMode, planContinuation, selectResumable, } from "../execution/index.js";
|
|
46
|
+
import { escalationDetail } from "../execution/escalation/index.js";
|
|
47
|
+
import { listCandidates, sessionOps, storageSeam } from "../adapters/opencode/index.js";
|
|
48
|
+
import { CodexCapacitySensor } from "../adapters/codex-capacity/index.js";
|
|
49
|
+
import { logFailOpen } from "./log.js";
|
|
50
|
+
import { CODEX_PROVIDER_ID } from "./types.js";
|
|
51
|
+
/** 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
|
+
function configuredTiers(settings) {
|
|
57
|
+
return TIERS.filter((tier) => settings.tiers[tier].length > 0);
|
|
58
|
+
}
|
|
59
|
+
/** Narrow a wire interrupt reason to the known vocabulary. */
|
|
60
|
+
function interruptReason(reason) {
|
|
61
|
+
return INTERRUPT_REASONS.includes(reason)
|
|
62
|
+
? reason
|
|
63
|
+
: undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Failure categories that count as structured capability evidence (ADP-015).
|
|
67
|
+
* The remaining evidence kinds (agent request, reviewer rejection) have no
|
|
68
|
+
* structured host source in v2 and are never inferred from free text.
|
|
69
|
+
*/
|
|
70
|
+
const CAPABILITY_EVIDENCE_CATEGORIES = [
|
|
71
|
+
"context_overflow",
|
|
72
|
+
"content_policy",
|
|
73
|
+
];
|
|
74
|
+
/** Finite per-unit escalation budget (ADP-017). */
|
|
75
|
+
const MAX_ESCALATIONS_PER_UNIT = 2;
|
|
76
|
+
export class Supervisor {
|
|
77
|
+
#options;
|
|
78
|
+
#settings;
|
|
79
|
+
#receipts;
|
|
80
|
+
#parked;
|
|
81
|
+
#tracker = new WorkUnitTracker();
|
|
82
|
+
#health;
|
|
83
|
+
#healthRecords;
|
|
84
|
+
#capacityStore;
|
|
85
|
+
#sessionModes;
|
|
86
|
+
#capacity;
|
|
87
|
+
#codex;
|
|
88
|
+
#vetoes = new Map();
|
|
89
|
+
#escalations = new Map();
|
|
90
|
+
#admissionTiers = new Map();
|
|
91
|
+
/** Recorded escalation-tool requests per unit (ADP-015 agent self-reports). */
|
|
92
|
+
#escalationEvidence = new Map();
|
|
93
|
+
/** Last agent observed per session (role-hint routing evidence, ADP-002). */
|
|
94
|
+
// Bounded like every other per-session map: long-lived hosts meet many
|
|
95
|
+
// short-lived subagent sessions, and agent hints for long-gone sessions
|
|
96
|
+
// are worthless. Insertion order gives the eviction (oldest hint first).
|
|
97
|
+
static MAX_AGENT_HINTS = 1024;
|
|
98
|
+
#agents = new Map();
|
|
99
|
+
/**
|
|
100
|
+
* Per-session tier requests (agent-declared via the adaptive_tier tool):
|
|
101
|
+
* the request is STICKY — it steers the session's next and following
|
|
102
|
+
* admissions until cleared or the session dies — because an agent works
|
|
103
|
+
* across many units in one task. Insertion order gives the eviction.
|
|
104
|
+
*/
|
|
105
|
+
static MAX_TIER_REQUESTS = 1024;
|
|
106
|
+
#tierRequests = new Map();
|
|
107
|
+
/**
|
|
108
|
+
* Sessions with an owned continuation prompt in flight: the prompt hook
|
|
109
|
+
* will fire for it, but that prompt belongs to the active unit — it must
|
|
110
|
+
* not re-admit (prevents an admission ping-pong).
|
|
111
|
+
*/
|
|
112
|
+
#continuationPending = new Set();
|
|
113
|
+
/**
|
|
114
|
+
* Depth of continuation prompts whose prompt hook has not run yet (or whose
|
|
115
|
+
* host call is still in flight). While a session is inside this window, a
|
|
116
|
+
* unit close must not clear the ownership mark: the close event of the
|
|
117
|
+
* failed unit is pumped concurrently with the continuation's own
|
|
118
|
+
* beforePrompt → prompt-hook sequence, and clearing the mark there would
|
|
119
|
+
* make the continuation re-admit (admission ping-pong).
|
|
120
|
+
*/
|
|
121
|
+
#continuationsInFlight = new Map();
|
|
122
|
+
/**
|
|
123
|
+
* Half-open probe tickets by session: a probe admitted while computing the
|
|
124
|
+
* recovery for a veto belongs to the continuation episode that follows, so
|
|
125
|
+
* the ticket is parked here until that unit opens and takes it.
|
|
126
|
+
*/
|
|
127
|
+
#sessionProbeTickets = new Map();
|
|
128
|
+
/**
|
|
129
|
+
* Half-open probe tickets by unit: the unit admitted as (or carrying) the
|
|
130
|
+
* breaker's single probe. Its termination reports the probe outcome — a
|
|
131
|
+
* success closes the breaker, a failure reopens it, an interruption re-arms
|
|
132
|
+
* it — otherwise a lost probe would wedge the resource in half_open.
|
|
133
|
+
*/
|
|
134
|
+
#probeTickets = new Map();
|
|
135
|
+
/**
|
|
136
|
+
* Session → tier admitted by the prompt hook for the unit that is about to
|
|
137
|
+
* open. Consumed by execution-started, which then skips re-routing (the
|
|
138
|
+
* model is already applied to the very turn that triggered admission).
|
|
139
|
+
*/
|
|
140
|
+
#promptAdmission = new Map();
|
|
141
|
+
/**
|
|
142
|
+
* Tier intent for an owned continuation (retry recovery, escalation,
|
|
143
|
+
* parked resume) set before its prompt is delivered. The continuation's
|
|
144
|
+
* prompt hook re-arms the prompt-admission marker from this so the
|
|
145
|
+
* continuation's execution-started skips re-routing — a fresh admission
|
|
146
|
+
* would flip an escalated episode back to the preferred model mid-unit
|
|
147
|
+
* (a switch only lands on the next turn). Cleared when the continuation
|
|
148
|
+
* unit opens, its prompt is rejected, or the session dies.
|
|
149
|
+
*/
|
|
150
|
+
#continuationTiers = new Map();
|
|
151
|
+
#ready;
|
|
152
|
+
#ops;
|
|
153
|
+
constructor(options) {
|
|
154
|
+
this.#options = options;
|
|
155
|
+
this.#health = new ResourceHealthRegistry(options.clock);
|
|
156
|
+
this.#capacity = new CapacityRegistry(options.clock);
|
|
157
|
+
this.#codex = new CodexCapacitySensor({ providerID: CODEX_PROVIDER_ID });
|
|
158
|
+
this.#capacity.register(this.#codex);
|
|
159
|
+
this.#settings = new SettingsStore(storageSeam(SETTINGS_KEY, options.storage));
|
|
160
|
+
this.#receipts = new ReceiptStore(storageSeam(RECEIPTS_KEY, options.storage));
|
|
161
|
+
this.#parked = new ParkedWorkStore(storageSeam(PARKED_KEY, options.storage));
|
|
162
|
+
this.#healthRecords = new HealthRecordStore(storageSeam(HEALTH_KEY, options.storage));
|
|
163
|
+
this.#capacityStore = new CapacityReadingStore(storageSeam(CAPACITY_KEY, options.storage));
|
|
164
|
+
this.#sessionModes = new SessionModeStore(storageSeam(SESSION_MODES_KEY, options.storage), options.clock);
|
|
165
|
+
this.#ops = sessionOps(options.session, {
|
|
166
|
+
beforeSwitch: (sessionID, model) => {
|
|
167
|
+
this.#tracker.noteOwnSwitch(sessionID, model);
|
|
168
|
+
},
|
|
169
|
+
beforePrompt: (sessionID) => {
|
|
170
|
+
this.#continuationPending.add(sessionID);
|
|
171
|
+
this.#continuationsInFlight.set(sessionID, (this.#continuationsInFlight.get(sessionID) ?? 0) + 1);
|
|
172
|
+
},
|
|
173
|
+
afterPrompt: (sessionID, delivered) => {
|
|
174
|
+
// sessionOps.prompt calls this only after its own beforePrompt, so
|
|
175
|
+
// the entry is always present; the assertion avoids a default branch
|
|
176
|
+
// no caller can reach.
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
178
|
+
const depth = this.#continuationsInFlight.get(sessionID);
|
|
179
|
+
if (depth <= 1) {
|
|
180
|
+
this.#continuationsInFlight.delete(sessionID);
|
|
181
|
+
if (!delivered) {
|
|
182
|
+
// The last in-flight owned prompt never reached the host's hook:
|
|
183
|
+
// its ownership mark would swallow the next user prompt's
|
|
184
|
+
// admission, so it is dropped here (the hook can no longer fire).
|
|
185
|
+
// The continuation's tier intent dies with it — no episode will
|
|
186
|
+
// open to carry it.
|
|
187
|
+
this.#continuationPending.delete(sessionID);
|
|
188
|
+
this.#continuationTiers.delete(sessionID);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
this.#continuationsInFlight.set(sessionID, depth - 1);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
// Durable state loads before the first report is handled: a later load
|
|
197
|
+
// must never clobber decisions recorded in the meantime.
|
|
198
|
+
const loadState = async () => {
|
|
199
|
+
await this.#receipts.load();
|
|
200
|
+
await this.#parked.load();
|
|
201
|
+
// Unit ids are durable keys (`${sessionID}#${episode}`) persisted in
|
|
202
|
+
// receipts and parked work. Seed the tracker's per-session episode
|
|
203
|
+
// counters above every persisted id: after a restart the counters
|
|
204
|
+
// must never re-mint a colliding id, which would replace a parked
|
|
205
|
+
// record (the store parks one record per id) and misattribute
|
|
206
|
+
// receipt outcomes.
|
|
207
|
+
const floors = new Map();
|
|
208
|
+
const bump = (unitID) => {
|
|
209
|
+
if (unitID === undefined)
|
|
210
|
+
return;
|
|
211
|
+
const cut = unitID.lastIndexOf("#");
|
|
212
|
+
if (cut <= 0)
|
|
213
|
+
return;
|
|
214
|
+
const sessionID = unitID.slice(0, cut);
|
|
215
|
+
const episode = Number(unitID.slice(cut + 1));
|
|
216
|
+
if (!Number.isInteger(episode) || episode <= 0)
|
|
217
|
+
return;
|
|
218
|
+
if (episode > (floors.get(sessionID) ?? 0))
|
|
219
|
+
floors.set(sessionID, episode);
|
|
220
|
+
};
|
|
221
|
+
for (const receipt of this.#receipts.snapshot())
|
|
222
|
+
bump(receipt.unitID);
|
|
223
|
+
for (const record of this.#parked.list())
|
|
224
|
+
bump(record.id);
|
|
225
|
+
this.#tracker.seedEpisodes(floors);
|
|
226
|
+
// Health records hydrate the registry: the host's TUI-mode plugin
|
|
227
|
+
// reconciliation can serve RPC from an instance whose hooks (event
|
|
228
|
+
// pump) live on another instance — persisted failures are the only
|
|
229
|
+
// health that instance can see, and a fresh instance must also assess
|
|
230
|
+
// persisted failures before routing instead of starting from empty.
|
|
231
|
+
await this.#healthRecords.load();
|
|
232
|
+
this.#health.hydrate(this.#healthRecords.snapshot());
|
|
233
|
+
// Session-mode overrides hydrate the same way: a restart (or the
|
|
234
|
+
// host's double-instance reconciliation) must keep steering sessions
|
|
235
|
+
// under the modes the user set for them.
|
|
236
|
+
await this.#sessionModes.load();
|
|
237
|
+
await this.#capacityStore.load();
|
|
238
|
+
return this.#settings.load();
|
|
239
|
+
};
|
|
240
|
+
this.#ready = loadState();
|
|
241
|
+
}
|
|
242
|
+
/** Settings as of the latest load/patch (awaits the constructor load). */
|
|
243
|
+
async #ensureSettings() {
|
|
244
|
+
await this.#ready;
|
|
245
|
+
return this.#settings.get();
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The session's effective operating mode (ADP-054): its per-session
|
|
249
|
+
* override when one is set, else the global mode. Every session-scoped
|
|
250
|
+
* supervision decision consults this instead of `settings.mode` directly.
|
|
251
|
+
*/
|
|
252
|
+
#modeFor(sessionID, settings) {
|
|
253
|
+
return this.#sessionModes.get(sessionID) ?? settings.mode;
|
|
254
|
+
}
|
|
255
|
+
// ---------- public entry points (never throw — fail-open, ADP-031) ----------
|
|
256
|
+
/** Handle one host report (event pump or session hook). */
|
|
257
|
+
async handleReport(report) {
|
|
258
|
+
try {
|
|
259
|
+
const settings = await this.#ensureSettings();
|
|
260
|
+
// An OFF session is fully inert — no supervision, no receipts, no
|
|
261
|
+
// health evidence. Idle is the exception: it is the bookkeeping
|
|
262
|
+
// boundary (parked-work sweep, dead-session override pruning) and runs
|
|
263
|
+
// for every report so other sessions' overrides are maintained; the
|
|
264
|
+
// sweep itself gates per parked session below.
|
|
265
|
+
if (report.kind !== "idle" && this.#modeFor(report.sessionID, settings) === "off")
|
|
266
|
+
return;
|
|
267
|
+
await this.#dispatch(report, settings);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
logFailOpen("report", error);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Handle one `retry` hook invocation. On a veto this mutates
|
|
275
|
+
* `input.decision` to `{ retry: false }`; otherwise the decision is left
|
|
276
|
+
* untouched and OpenCode's native retry policy stands.
|
|
277
|
+
*/
|
|
278
|
+
async handleRetry(input) {
|
|
279
|
+
try {
|
|
280
|
+
const settings = await this.#ensureSettings();
|
|
281
|
+
if (this.#modeFor(input.sessionID, settings) === "off")
|
|
282
|
+
return;
|
|
283
|
+
await this.#handleRetry(input, settings);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
logFailOpen("retry", error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Record an escalation-tool request (ADP-015 agent self-report) against the
|
|
291
|
+
* session's current work unit. Returns false when no unit is active — the
|
|
292
|
+
* request is then discarded (escalation is a per-unit signal).
|
|
293
|
+
*/
|
|
294
|
+
noteEscalationRequest(sessionID, reason, evidence) {
|
|
295
|
+
const unit = this.#tracker.active(sessionID);
|
|
296
|
+
if (unit === undefined)
|
|
297
|
+
return false;
|
|
298
|
+
const recorded = this.#escalationEvidence.get(unit.id) ?? [];
|
|
299
|
+
recorded.push({
|
|
300
|
+
kind: "agent-request",
|
|
301
|
+
detail: escalationDetail({ reason, evidence }),
|
|
302
|
+
});
|
|
303
|
+
this.#escalationEvidence.set(unit.id, recorded.slice(-MAX_ESCALATION_EVIDENCE));
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Record an agent-declared tier request for the session (the adaptive_tier
|
|
308
|
+
* tool). STICKY: it steers the session's next and following admissions
|
|
309
|
+
* until cleared — an agent works across many units in one task. Returns
|
|
310
|
+
* false on an unknown tier (the tool re-validates before calling).
|
|
311
|
+
*/
|
|
312
|
+
noteTierRequest(sessionID, tier) {
|
|
313
|
+
if (!isTierName(tier))
|
|
314
|
+
return false;
|
|
315
|
+
if (!this.#tierRequests.has(sessionID) &&
|
|
316
|
+
this.#tierRequests.size >= _a.MAX_TIER_REQUESTS) {
|
|
317
|
+
// The map is non-empty here (size >= MAX_TIER_REQUESTS > 0): Map
|
|
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" });
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
/** The session's tier request, or `null` when it follows the routing ladder. */
|
|
328
|
+
async getTierRequest(sessionID) {
|
|
329
|
+
await this.#ready;
|
|
330
|
+
return this.#tierRequests.get(sessionID)?.tier ?? null;
|
|
331
|
+
}
|
|
332
|
+
/** Clear one session's tier request (`null` is the only valid input). */
|
|
333
|
+
async setTierRequest(sessionID, tier) {
|
|
334
|
+
await this.#ready;
|
|
335
|
+
if (tier !== null)
|
|
336
|
+
throw new Error("only null clears a tier request");
|
|
337
|
+
this.#tierRequests.delete(sessionID);
|
|
338
|
+
}
|
|
339
|
+
// ---------- state API for the control surfaces (ADP-033/034/036) ----------
|
|
340
|
+
/** Current settings (awaits the constructor load). */
|
|
341
|
+
async settings() {
|
|
342
|
+
return this.#ensureSettings();
|
|
343
|
+
}
|
|
344
|
+
/** Validate and persist a settings patch; returns per-field errors. */
|
|
345
|
+
async updateSettings(patch) {
|
|
346
|
+
await this.#ready;
|
|
347
|
+
return this.#settings.update(patch);
|
|
348
|
+
}
|
|
349
|
+
/** The session's mode override, or `null` when it follows the global mode. */
|
|
350
|
+
async getSessionMode(sessionID) {
|
|
351
|
+
await this.#ready;
|
|
352
|
+
return this.#sessionModes.get(sessionID);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Persist one session's mode override (`null` clears it). Throws on write
|
|
356
|
+
* failure — the RPC handler degrades that to an error patch result
|
|
357
|
+
* (fail-open, ADP-031), while the routing path never calls this.
|
|
358
|
+
*/
|
|
359
|
+
async setSessionMode(sessionID, mode) {
|
|
360
|
+
await this.#ready;
|
|
361
|
+
await this.#sessionModes.set(sessionID, mode);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Bounded status snapshot for the status view (ADP-033). Health is read
|
|
365
|
+
* from the persisted record set (not the in-memory registry): the serving
|
|
366
|
+
* instance's registry can be empty when the host's plugin reconciliation
|
|
367
|
+
* placed the event-pump hooks on another instance, while the persisted
|
|
368
|
+
* mirror is shared by every instance.
|
|
369
|
+
*/
|
|
370
|
+
async status() {
|
|
371
|
+
const settings = await this.#ensureSettings();
|
|
372
|
+
await this.#healthRecords.load();
|
|
373
|
+
// The persisted mirror serves the status surface: a serving instance
|
|
374
|
+
// without the event-pump hooks must still report sensed capacity.
|
|
375
|
+
await this.#capacityStore.load();
|
|
376
|
+
const now = this.#options.clock();
|
|
377
|
+
const records = this.#healthRecords.snapshot().map((record) => sweepRecord(record, now));
|
|
378
|
+
// Capacity readings are gathered per configured ref (mirroring the health
|
|
379
|
+
// input shape) so buildStatus stays a pure function of its inputs.
|
|
380
|
+
const capacity = {};
|
|
381
|
+
for (const tier of TIERS) {
|
|
382
|
+
for (const ref of settings.tiers[tier]) {
|
|
383
|
+
const chainIDs = new Set(resourceChain(refKey(ref)).map(resourceKeyID));
|
|
384
|
+
const candidates = this.#capacityStore
|
|
385
|
+
.snapshot()
|
|
386
|
+
.filter((entry) => chainIDs.has(resourceKeyID(entry.key)))
|
|
387
|
+
.map((entry) => entry.reading);
|
|
388
|
+
capacity[schedulerHealthKey(ref)] = mergeReadings(candidates, now);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return buildStatus({
|
|
392
|
+
settings,
|
|
393
|
+
health: records,
|
|
394
|
+
capacity,
|
|
395
|
+
parked: this.#parked.count(),
|
|
396
|
+
receipts: this.#receipts.snapshot().length,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/** Newest-first bounded receipt view (ADP-030). */
|
|
400
|
+
async receipts() {
|
|
401
|
+
await this.#ready;
|
|
402
|
+
return this.#receipts.snapshot().slice(-RECEIPTS_VIEW_LIMIT).reverse();
|
|
403
|
+
}
|
|
404
|
+
// ---------- dispatch ----------
|
|
405
|
+
async #dispatch(report, settings) {
|
|
406
|
+
switch (report.kind) {
|
|
407
|
+
case "execution-started":
|
|
408
|
+
await this.#onExecutionStarted(report, settings);
|
|
409
|
+
return;
|
|
410
|
+
case "execution-succeeded":
|
|
411
|
+
await this.#onExecutionSucceeded(report);
|
|
412
|
+
return;
|
|
413
|
+
case "execution-failed":
|
|
414
|
+
await this.#onExecutionFailed(report, settings);
|
|
415
|
+
return;
|
|
416
|
+
case "execution-interrupted":
|
|
417
|
+
await this.#onExecutionInterrupted(report);
|
|
418
|
+
return;
|
|
419
|
+
case "model-selected":
|
|
420
|
+
this.#tracker.apply({
|
|
421
|
+
kind: "model-selected",
|
|
422
|
+
sessionID: report.sessionID,
|
|
423
|
+
at: report.at,
|
|
424
|
+
model: report.model,
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
case "model-request":
|
|
428
|
+
if (report.agent !== undefined) {
|
|
429
|
+
if (!this.#agents.has(report.sessionID) &&
|
|
430
|
+
this.#agents.size >= _a.MAX_AGENT_HINTS) {
|
|
431
|
+
// The guard above ensures the map is at its cap (>= 1 entries),
|
|
432
|
+
// so the first key always exists; the assertion avoids a branch
|
|
433
|
+
// the coverage gate would reject as unreachable.
|
|
434
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
435
|
+
const oldest = this.#agents.keys().next().value;
|
|
436
|
+
this.#agents.delete(oldest);
|
|
437
|
+
}
|
|
438
|
+
this.#agents.set(report.sessionID, report.agent);
|
|
439
|
+
}
|
|
440
|
+
this.#tracker.apply({
|
|
441
|
+
kind: "model-request",
|
|
442
|
+
sessionID: report.sessionID,
|
|
443
|
+
at: this.#options.clock(),
|
|
444
|
+
model: report.model,
|
|
445
|
+
});
|
|
446
|
+
return;
|
|
447
|
+
case "http-response":
|
|
448
|
+
await this.#onHttpResponse(report);
|
|
449
|
+
return;
|
|
450
|
+
case "idle":
|
|
451
|
+
await this.#onIdle(settings);
|
|
452
|
+
return;
|
|
453
|
+
case "prompt":
|
|
454
|
+
await this.#onPrompt(report.sessionID, settings);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// ---------- admission ----------
|
|
459
|
+
/**
|
|
460
|
+
* CONTROL-mode admission in the awaited prompt hook. The hook runs before
|
|
461
|
+
* the turn's model resolution, so the applied model governs the very turn
|
|
462
|
+
* that triggered admission (the event-pump execution-started report is
|
|
463
|
+
* asynchronous and would only affect the next turn — verified on host
|
|
464
|
+
* 2.2.5). OBSERVE never switches, so it needs no prompt-time decision and
|
|
465
|
+
* stays on the pump entirely.
|
|
466
|
+
*/
|
|
467
|
+
async #onPrompt(sessionID, settings) {
|
|
468
|
+
const mode = this.#modeFor(sessionID, settings);
|
|
469
|
+
if (mode !== "active")
|
|
470
|
+
return;
|
|
471
|
+
if (this.#continuationPending.delete(sessionID)) {
|
|
472
|
+
// An owned continuation (retry recovery, escalation, parked resume)
|
|
473
|
+
// already owns this unit's routing: re-arm the admission marker from
|
|
474
|
+
// the continuation's tier intent so execution-started skips a fresh
|
|
475
|
+
// (continuation-unaware) admission.
|
|
476
|
+
const tier = this.#continuationTiers.get(sessionID);
|
|
477
|
+
this.#promptAdmission.set(sessionID, tier !== undefined ? { tier } : {});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// A fresh user prompt cancels any stale continuation intent and marker:
|
|
481
|
+
// this admission re-routes from scratch.
|
|
482
|
+
this.#continuationTiers.delete(sessionID);
|
|
483
|
+
this.#promptAdmission.delete(sessionID);
|
|
484
|
+
// A pending probe ticket from an earlier prompt admission (or a failed
|
|
485
|
+
// recovery) can only be carried by the unit the ORIGINAL prompt opens;
|
|
486
|
+
// a fresh admission means that episode never ran, so the ticket re-arms
|
|
487
|
+
// here instead of being adopted by whichever unit opens next.
|
|
488
|
+
const stale = this.#sessionProbeTickets.get(sessionID);
|
|
489
|
+
if (stale !== undefined) {
|
|
490
|
+
this.#sessionProbeTickets.delete(sessionID);
|
|
491
|
+
this.#reportProbeOutcome(stale, { success: false, category: "user_cancelled" });
|
|
492
|
+
}
|
|
493
|
+
let probeTicket;
|
|
494
|
+
try {
|
|
495
|
+
const admission = await this.#admit(settings, sessionID);
|
|
496
|
+
probeTicket = admission.probeTicket;
|
|
497
|
+
const wrapped = schedulerForMode(admission.decision, mode);
|
|
498
|
+
if (wrapped.kind === "apply") {
|
|
499
|
+
await this.#ops.switchModel(sessionID, wrapped.model);
|
|
500
|
+
}
|
|
501
|
+
// A pin is not a tier — receipts only carry a tier for tiered routing.
|
|
502
|
+
const tier = routingTier(admission.routing);
|
|
503
|
+
await this.#recordReceipt({
|
|
504
|
+
sessionID,
|
|
505
|
+
...(tier !== undefined ? { tier } : {}),
|
|
506
|
+
decision: wrapped,
|
|
507
|
+
});
|
|
508
|
+
this.#promptAdmission.set(sessionID, { ...(tier !== undefined ? { tier } : {}) });
|
|
509
|
+
// The prompt-admitted probe episode is the turn that follows.
|
|
510
|
+
if (wrapped.kind === "apply" && probeTicket !== undefined) {
|
|
511
|
+
this.#sessionProbeTickets.set(sessionID, probeTicket);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
// A probe admitted for this turn is lost when the admission fails (the
|
|
516
|
+
// switch threw): no unit will ever carry the ticket, so re-arm it.
|
|
517
|
+
if (probeTicket !== undefined) {
|
|
518
|
+
this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
|
|
519
|
+
}
|
|
520
|
+
// Fail-open: no marker → execution-started re-routes the unit itself.
|
|
521
|
+
logFailOpen("admission", error);
|
|
522
|
+
await this.#recordReceipt({
|
|
523
|
+
sessionID,
|
|
524
|
+
decision: { kind: "defer", reason: "routing-defer", detail: "internal error" },
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async #onExecutionStarted(report, settings) {
|
|
529
|
+
const opened = this.#tracker.apply({
|
|
530
|
+
kind: "execution-started",
|
|
531
|
+
sessionID: report.sessionID,
|
|
532
|
+
at: report.at,
|
|
533
|
+
});
|
|
534
|
+
const unit = opened.unit;
|
|
535
|
+
// The prompt-hook marker belongs to the unit this report OPENS, so read
|
|
536
|
+
// it before the close cleanup: a superseding start would otherwise
|
|
537
|
+
// discard it and re-route an already-admitted turn.
|
|
538
|
+
const admitted = this.#promptAdmission.get(report.sessionID);
|
|
539
|
+
// A superseding start closes the previous episode — release its
|
|
540
|
+
// per-unit bookkeeping (veto budget) too.
|
|
541
|
+
this.#afterUnitClose(opened);
|
|
542
|
+
// The prompt hook already admitted and applied for this unit — carry the
|
|
543
|
+
// tier into escalation context and skip re-routing.
|
|
544
|
+
if (admitted !== undefined) {
|
|
545
|
+
this.#promptAdmission.delete(report.sessionID);
|
|
546
|
+
this.#continuationTiers.delete(report.sessionID);
|
|
547
|
+
if (admitted.tier !== undefined)
|
|
548
|
+
this.#admissionTiers.set(unit.id, admitted.tier);
|
|
549
|
+
// The prompt-time admission receipt was recorded before this unit
|
|
550
|
+
// existed; adopt it so outcomes attach to it like any unit receipt.
|
|
551
|
+
try {
|
|
552
|
+
await this.#receipts.adoptUnit(report.sessionID, unit.id);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
logFailOpen("receipts", error);
|
|
556
|
+
}
|
|
557
|
+
const sessionTicket = this.#sessionProbeTickets.get(report.sessionID);
|
|
558
|
+
if (sessionTicket !== undefined) {
|
|
559
|
+
this.#sessionProbeTickets.delete(report.sessionID);
|
|
560
|
+
this.#probeTickets.set(unit.id, sessionTicket);
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// A probe admitted while computing a veto's recovery belongs to the
|
|
565
|
+
// continuation episode that follows — the first start on the session
|
|
566
|
+
// after the veto.
|
|
567
|
+
const carriedTicket = this.#sessionProbeTickets.get(report.sessionID);
|
|
568
|
+
if (carriedTicket !== undefined) {
|
|
569
|
+
this.#sessionProbeTickets.delete(report.sessionID);
|
|
570
|
+
this.#probeTickets.set(unit.id, carriedTicket);
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
await this.#routeAdmission(unit, settings, this.#modeFor(unit.sessionID, settings));
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
logFailOpen("admission", error);
|
|
577
|
+
await this.#recordReceipt({
|
|
578
|
+
sessionID: unit.sessionID,
|
|
579
|
+
unitID: unit.id,
|
|
580
|
+
decision: { kind: "defer", reason: "routing-defer", detail: "internal error" },
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/** Fresh admission: routing ladder → scheduler, raw (unwrapped). */
|
|
585
|
+
async #admit(settings, sessionID) {
|
|
586
|
+
const routing = resolveRouting({
|
|
587
|
+
...(settings.pin !== null ? { pin: settings.pin } : {}),
|
|
588
|
+
// The agent-declared tier request (adaptive_tier tool) outranks the
|
|
589
|
+
// role hints but stays below the pin (ADP-002 ladder).
|
|
590
|
+
...(this.#tierRequests.has(sessionID)
|
|
591
|
+
? { tierRequest: this.#tierRequests.get(sessionID) }
|
|
592
|
+
: {}),
|
|
593
|
+
// The agent-role hint is the session's last observed agent — the host
|
|
594
|
+
// reports it on every model request (ADP-002 level 3).
|
|
595
|
+
...(this.#agents.has(sessionID) ? { agent: this.#agents.get(sessionID) } : {}),
|
|
596
|
+
agentTiers: settings.agentTiers,
|
|
597
|
+
configuredTiers: configuredTiers(settings),
|
|
598
|
+
});
|
|
599
|
+
const tierCandidates = routing.kind === "tier" ? settings.tiers[routing.tier] : [];
|
|
600
|
+
const inventory = await listCandidates(this.#options.model);
|
|
601
|
+
// Capacity sensors are pull-based: refresh before assessing so the
|
|
602
|
+
// admission decision sees current readings.
|
|
603
|
+
await this.#capacity.refresh();
|
|
604
|
+
const health = {};
|
|
605
|
+
const capacity = {};
|
|
606
|
+
// Tickets issued by this assessment sweep (see the re-arm below).
|
|
607
|
+
const issued = [];
|
|
608
|
+
for (const candidate of inventory) {
|
|
609
|
+
const ref = candidateRef(candidate);
|
|
610
|
+
const mapKey = schedulerHealthKey(ref);
|
|
611
|
+
const assessment = this.#health.assess(refKey(ref));
|
|
612
|
+
health[mapKey] = assessment;
|
|
613
|
+
if (assessment.kind === "probe")
|
|
614
|
+
issued.push(assessment.ticket);
|
|
615
|
+
const capacityAssessment = this.#capacity.assessment(refKey(ref));
|
|
616
|
+
if (capacityAssessment.kind === "estimate")
|
|
617
|
+
capacity[mapKey] = capacityAssessment.signals;
|
|
618
|
+
}
|
|
619
|
+
const input = {
|
|
620
|
+
routing,
|
|
621
|
+
// The host exposes no per-episode demand source in the v2 wiring
|
|
622
|
+
// (no tool/modality/context requirement reaches the plugin), so the
|
|
623
|
+
// compatibility gate runs on availability metadata only. Documented
|
|
624
|
+
// limitation — see TEST_STRATEGY "Compatibility gate demand".
|
|
625
|
+
demand: {},
|
|
626
|
+
inventory,
|
|
627
|
+
tierCandidates,
|
|
628
|
+
health,
|
|
629
|
+
capacity,
|
|
630
|
+
};
|
|
631
|
+
const decision = admit(input);
|
|
632
|
+
// Assess admits a half-open probe for every due breaker it touches —
|
|
633
|
+
// including candidates this decision passed over (a ready sibling wins)
|
|
634
|
+
// or dispatched nothing for (defer/park). An issued-but-undispatched
|
|
635
|
+
// probe would wedge its resource in half_open: no unit ever carries the
|
|
636
|
+
// ticket, so nothing reports it and the resource never recovers. Re-arm
|
|
637
|
+
// each unselected ticket with the breaker's same backoff window — a
|
|
638
|
+
// probe that never ran is no health evidence.
|
|
639
|
+
const selectedId = decision.kind === "apply" ? decision.evidence.probeId : undefined;
|
|
640
|
+
let probeTicket;
|
|
641
|
+
for (const ticket of issued) {
|
|
642
|
+
if (selectedId !== undefined && ticket.id === selectedId) {
|
|
643
|
+
probeTicket = ticket;
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
this.#health.reportProbe(ticket, { success: false, category: "user_cancelled" });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return { routing, decision, ...(probeTicket !== undefined ? { probeTicket } : {}) };
|
|
650
|
+
}
|
|
651
|
+
async #routeAdmission(unit, settings, mode) {
|
|
652
|
+
const { routing, decision, probeTicket } = await this.#admit(settings, unit.sessionID);
|
|
653
|
+
// The tier the unit was admitted under is escalation context (ADP-015);
|
|
654
|
+
// recorded for every tiered routing outcome (including a defer, which
|
|
655
|
+
// still names the tier it could not honor).
|
|
656
|
+
if (routing.kind !== "pin") {
|
|
657
|
+
this.#admissionTiers.set(unit.id, routing.tier);
|
|
658
|
+
}
|
|
659
|
+
const wrapped = schedulerForMode(decision, mode);
|
|
660
|
+
if (wrapped.kind === "apply") {
|
|
661
|
+
try {
|
|
662
|
+
await this.#applyModel(unit, wrapped);
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
// The episode cannot carry the probe when the switch itself failed —
|
|
666
|
+
// re-arm the ticket instead of wedging the resource half-open.
|
|
667
|
+
if (probeTicket !== undefined) {
|
|
668
|
+
this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
|
|
669
|
+
}
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
672
|
+
if (probeTicket !== undefined)
|
|
673
|
+
this.#probeTickets.set(unit.id, probeTicket);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (probeTicket !== undefined) {
|
|
677
|
+
// OBSERVE wraps an apply — including a probe admission — without
|
|
678
|
+
// running it: no episode will ever report the ticket, so re-arm it.
|
|
679
|
+
this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
|
|
680
|
+
}
|
|
681
|
+
// Observe-only records the identical decision; defer and park leave the
|
|
682
|
+
// episode on OpenCode's own model (admission cannot hold an episode).
|
|
683
|
+
const tier = routingTier(routing);
|
|
684
|
+
await this.#recordReceipt({
|
|
685
|
+
sessionID: unit.sessionID,
|
|
686
|
+
unitID: unit.id,
|
|
687
|
+
...(tier !== undefined ? { tier } : {}),
|
|
688
|
+
decision: wrapped,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/** CONTROL-mode application at the admission boundary; OBSERVE never reaches here. */
|
|
692
|
+
async #applyModel(unit, decision) {
|
|
693
|
+
await this.#ops.switchModel(unit.sessionID, decision.model);
|
|
694
|
+
await this.#recordReceipt({
|
|
695
|
+
sessionID: unit.sessionID,
|
|
696
|
+
unitID: unit.id,
|
|
697
|
+
...(decision.tier !== undefined ? { tier: decision.tier } : {}),
|
|
698
|
+
decision,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
// ---------- unit termination ----------
|
|
702
|
+
async #onExecutionSucceeded(report) {
|
|
703
|
+
const unit = this.#tracker.active(report.sessionID);
|
|
704
|
+
const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
|
|
705
|
+
const probeTicket = unit !== undefined ? this.#probeTickets.get(unit.id) : undefined;
|
|
706
|
+
const closed = this.#tracker.apply({
|
|
707
|
+
kind: "execution-succeeded",
|
|
708
|
+
sessionID: report.sessionID,
|
|
709
|
+
at: report.at,
|
|
710
|
+
});
|
|
711
|
+
this.#afterUnitClose(closed);
|
|
712
|
+
if (model !== undefined) {
|
|
713
|
+
this.#observeHealth({ kind: "success", key: refKey(model) });
|
|
714
|
+
}
|
|
715
|
+
if (unit !== undefined) {
|
|
716
|
+
if (probeTicket !== undefined) {
|
|
717
|
+
this.#probeTickets.delete(unit.id);
|
|
718
|
+
this.#reportProbeOutcome(probeTicket, { success: true });
|
|
719
|
+
}
|
|
720
|
+
await this.#attachOutcome(unit, { status: "succeeded" });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async #onExecutionFailed(report, settings) {
|
|
724
|
+
const unit = this.#tracker.active(report.sessionID);
|
|
725
|
+
const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
|
|
726
|
+
const unitID = unit?.id;
|
|
727
|
+
// Budget and tier are read before the close clears the per-unit maps.
|
|
728
|
+
const admissionTier = unitID !== undefined ? this.#admissionTiers.get(unitID) : undefined;
|
|
729
|
+
const escalationUsed = unitID !== undefined ? (this.#escalations.get(unitID) ?? 0) : 0;
|
|
730
|
+
// Tool evidence is captured pre-close like the other per-unit maps.
|
|
731
|
+
const toolEvidence = unitID !== undefined ? (this.#escalationEvidence.get(unitID) ?? []) : [];
|
|
732
|
+
const probeTicket = unitID !== undefined ? this.#probeTickets.get(unitID) : undefined;
|
|
733
|
+
const closed = this.#tracker.apply({
|
|
734
|
+
kind: "execution-failed",
|
|
735
|
+
sessionID: report.sessionID,
|
|
736
|
+
at: report.at,
|
|
737
|
+
error: report.error,
|
|
738
|
+
});
|
|
739
|
+
this.#afterUnitClose(closed);
|
|
740
|
+
const classification = classifyFailure({
|
|
741
|
+
structured: {
|
|
742
|
+
type: report.error.type,
|
|
743
|
+
...(report.error.retryAfterMs !== undefined
|
|
744
|
+
? { retryAfterMs: report.error.retryAfterMs }
|
|
745
|
+
: {}),
|
|
746
|
+
},
|
|
747
|
+
status: report.error.status,
|
|
748
|
+
// Terminal classification may use the bounded textual last resort
|
|
749
|
+
// (content-policy rejections arrive as free text, not error types).
|
|
750
|
+
// The retry hook deliberately omits text: it must stay conservative.
|
|
751
|
+
text: report.error.message,
|
|
752
|
+
});
|
|
753
|
+
if (model !== undefined) {
|
|
754
|
+
this.#observeHealth(failureEvent(refKey(model), classification));
|
|
755
|
+
}
|
|
756
|
+
if (unit !== undefined) {
|
|
757
|
+
await this.#attachOutcome(unit, { status: "failed", category: classification.category });
|
|
758
|
+
if (probeTicket !== undefined) {
|
|
759
|
+
this.#probeTickets.delete(unit.id);
|
|
760
|
+
this.#reportProbeOutcome(probeTicket, { success: false, category: classification.category });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
await this.#escalateOnFailure(report, unitID, model, classification, admissionTier, escalationUsed, toolEvidence, settings);
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Escalation targets must be usable: the scheduler contract hands
|
|
767
|
+
* planEscalation "already gated" resources, so the wiring filters the next
|
|
768
|
+
* tier's configured refs to candidates that exist in the inventory, pass
|
|
769
|
+
* the compatibility gate, are not health-blocked, and differ from the
|
|
770
|
+
* model that just failed. Evaluated WITHOUT probe admission (evaluate,
|
|
771
|
+
* not assess) — gating an escalation must never flip a breaker or strand
|
|
772
|
+
* an unowned ticket.
|
|
773
|
+
*/
|
|
774
|
+
async #eligibleEscalationTargets(refs, failed) {
|
|
775
|
+
let inventory;
|
|
776
|
+
try {
|
|
777
|
+
inventory = await listCandidates(this.#options.model);
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
// No inventory means no verifiable target: escalating would switch
|
|
781
|
+
// blind, so the budget is kept instead of burned.
|
|
782
|
+
logFailOpen("escalation", error);
|
|
783
|
+
return [];
|
|
784
|
+
}
|
|
785
|
+
const eligible = [];
|
|
786
|
+
for (const ref of refs) {
|
|
787
|
+
if (sameModelRef(ref, failed))
|
|
788
|
+
continue;
|
|
789
|
+
const candidate = inventory.find((entry) => sameModelRef(candidateRef(entry), ref));
|
|
790
|
+
if (candidate === undefined)
|
|
791
|
+
continue;
|
|
792
|
+
if (!checkCompatibility(candidate, {}).compatible)
|
|
793
|
+
continue;
|
|
794
|
+
if (this.#health.evaluate(refKey(ref)).kind !== "available")
|
|
795
|
+
continue;
|
|
796
|
+
eligible.push(ref);
|
|
797
|
+
}
|
|
798
|
+
return eligible;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Escalation on a terminally failed unit (ADP-015..017): structured
|
|
802
|
+
* capability evidence only, same-model variant first then next tier,
|
|
803
|
+
* finite budget. Control continues the interrupted work on the escalated
|
|
804
|
+
* resource; Observe records the identical decision; defer receipts stand.
|
|
805
|
+
*/
|
|
806
|
+
async #escalateOnFailure(report, unitID, model, classification, admissionTier, escalationUsed, toolEvidence, settings) {
|
|
807
|
+
if (unitID === undefined ||
|
|
808
|
+
model === undefined ||
|
|
809
|
+
admissionTier === undefined ||
|
|
810
|
+
!CAPABILITY_EVIDENCE_CATEGORIES.includes(classification.category)) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const next = nextTier(admissionTier);
|
|
814
|
+
const evidence = [
|
|
815
|
+
...toolEvidence,
|
|
816
|
+
{ kind: "repeated-failure", detail: classification.category },
|
|
817
|
+
];
|
|
818
|
+
const nextResources = next !== undefined ? settings.tiers[next] : [];
|
|
819
|
+
const eligible = await this.#eligibleEscalationTargets(nextResources, model);
|
|
820
|
+
const plan = planEscalation({
|
|
821
|
+
currentModel: model,
|
|
822
|
+
currentTier: admissionTier,
|
|
823
|
+
nextTierResources: eligible,
|
|
824
|
+
evidence,
|
|
825
|
+
budget: { used: escalationUsed, max: MAX_ESCALATIONS_PER_UNIT },
|
|
826
|
+
});
|
|
827
|
+
const wrapped = schedulerForMode(plan, this.#modeFor(report.sessionID, settings));
|
|
828
|
+
if (wrapped.kind === "escalate") {
|
|
829
|
+
this.#escalations.set(unitID, escalationUsed + 1);
|
|
830
|
+
// Recorded unit-less on purpose: the receipt describes the CONTINUATION
|
|
831
|
+
// decision, so the continuation unit adopts it and its outcome attaches
|
|
832
|
+
// here (the failed unit's own outcome attaches to its apply receipt).
|
|
833
|
+
await this.#recordReceipt({
|
|
834
|
+
sessionID: report.sessionID,
|
|
835
|
+
tier: wrapped.tier,
|
|
836
|
+
decision: wrapped,
|
|
837
|
+
});
|
|
838
|
+
// Continue the interrupted work on the escalated resource; the unit is
|
|
839
|
+
// terminally failed, so an ops failure is only logged (never parked).
|
|
840
|
+
try {
|
|
841
|
+
await this.#ownRecovery(report.sessionID, wrapped.target, wrapped.tier);
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
logFailOpen("escalation-ops", error);
|
|
845
|
+
}
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
await this.#recordReceipt({
|
|
849
|
+
sessionID: report.sessionID,
|
|
850
|
+
tier: plan.kind === "escalate" ? plan.tier : admissionTier,
|
|
851
|
+
decision: wrapped,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
async #onExecutionInterrupted(report) {
|
|
855
|
+
const reason = interruptReason(report.reason);
|
|
856
|
+
if (reason === undefined) {
|
|
857
|
+
logFailOpen("interrupt", new Error(`unknown interruption reason: ${report.reason.slice(0, 32)}`));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const unit = this.#tracker.active(report.sessionID);
|
|
861
|
+
const probeTicket = unit !== undefined ? this.#probeTickets.get(unit.id) : undefined;
|
|
862
|
+
const closed = this.#tracker.apply({
|
|
863
|
+
kind: "execution-interrupted",
|
|
864
|
+
sessionID: report.sessionID,
|
|
865
|
+
at: report.at,
|
|
866
|
+
reason,
|
|
867
|
+
});
|
|
868
|
+
this.#afterUnitClose(closed);
|
|
869
|
+
// User and host interruptions never produce health evidence (the
|
|
870
|
+
// classifier maps them outside the infrastructure vocabulary).
|
|
871
|
+
if (unit !== undefined) {
|
|
872
|
+
// An interrupted probe is no evidence about resource health: re-arm the
|
|
873
|
+
// breaker's same window rather than doubling it.
|
|
874
|
+
if (probeTicket !== undefined) {
|
|
875
|
+
this.#probeTickets.delete(unit.id);
|
|
876
|
+
this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
|
|
877
|
+
}
|
|
878
|
+
await this.#attachOutcome(unit, { status: "interrupted", reason });
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* One probe ticket's resolution at its episode's termination. A probe that
|
|
883
|
+
* ends without a reported outcome (superseded, lost) would wedge the
|
|
884
|
+
* resource in half_open, so the close cleanup reports those as
|
|
885
|
+
* interruptions — re-arming the same backoff window, never doubling it.
|
|
886
|
+
*/
|
|
887
|
+
#reportProbeOutcome(ticket, outcome) {
|
|
888
|
+
// reportProbe is total and synchronous; validation failures (stale or
|
|
889
|
+
// unknown ticket) are reported by its boolean return, not by throwing.
|
|
890
|
+
this.#health.reportProbe(ticket, outcome);
|
|
891
|
+
}
|
|
892
|
+
/** Bounded per-unit bookkeeping once the tracker closed the unit. */
|
|
893
|
+
#afterUnitClose(closed) {
|
|
894
|
+
const unitID = closedUnitID(closed);
|
|
895
|
+
if (unitID !== undefined) {
|
|
896
|
+
this.#vetoes.delete(unitID);
|
|
897
|
+
this.#escalations.delete(unitID);
|
|
898
|
+
this.#admissionTiers.delete(unitID);
|
|
899
|
+
this.#escalationEvidence.delete(unitID);
|
|
900
|
+
const leftover = this.#probeTickets.get(unitID);
|
|
901
|
+
if (leftover !== undefined) {
|
|
902
|
+
// The explicit outcome reporters run before this cleanup; reaching
|
|
903
|
+
// here means the unit ended without one (superseded or lost).
|
|
904
|
+
this.#reportProbeOutcome(leftover, { success: false, category: "user_cancelled" });
|
|
905
|
+
this.#probeTickets.delete(unitID);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (closed.kind === "opened" || closed.kind === "closed") {
|
|
909
|
+
// Per-session flags for a unit that will never open again must not
|
|
910
|
+
// leak: a stale continuation mark would swallow a real user prompt's
|
|
911
|
+
// admission; a stale admission marker would skip routing. The guard
|
|
912
|
+
// keeps the wipe out of an in-flight continuation's mark window (its
|
|
913
|
+
// own close events are pumped concurrently with the mark).
|
|
914
|
+
const sid = closed.unit.sessionID;
|
|
915
|
+
// A continuation tier intent pins the marker: the failed unit's close
|
|
916
|
+
// event can arrive after the continuation's prompt hook set it, and
|
|
917
|
+
// wiping then would re-route the continuation unit.
|
|
918
|
+
if ((this.#continuationsInFlight.get(sid) ?? 0) === 0 && !this.#continuationTiers.has(sid)) {
|
|
919
|
+
this.#continuationPending.delete(sid);
|
|
920
|
+
this.#promptAdmission.delete(sid);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
// ---------- http evidence ----------
|
|
925
|
+
async #onHttpResponse(report) {
|
|
926
|
+
if (report.capturedHeaders !== undefined) {
|
|
927
|
+
this.#codex.observe(report.capturedHeaders, this.#options.clock());
|
|
928
|
+
await this.#syncCapacityMirror();
|
|
929
|
+
}
|
|
930
|
+
if (report.status < 400)
|
|
931
|
+
return;
|
|
932
|
+
const unit = this.#tracker.active(report.sessionID);
|
|
933
|
+
const model = unit?.model ?? this.#tracker.lastModel(report.sessionID)?.model;
|
|
934
|
+
if (model === undefined)
|
|
935
|
+
return;
|
|
936
|
+
const classification = classifyFailure({
|
|
937
|
+
status: report.status,
|
|
938
|
+
...(report.retryAfterHeader !== undefined
|
|
939
|
+
? { headers: { "retry-after": report.retryAfterHeader } }
|
|
940
|
+
: {}),
|
|
941
|
+
});
|
|
942
|
+
this.#observeHealth(failureEvent(refKey(model), classification));
|
|
943
|
+
}
|
|
944
|
+
// ---------- retry hook ----------
|
|
945
|
+
async #handleRetry(input, settings) {
|
|
946
|
+
const classification = classifyFailure({
|
|
947
|
+
structured: {
|
|
948
|
+
type: input.error.type,
|
|
949
|
+
...(input.error.retryAfterMs !== undefined
|
|
950
|
+
? { retryAfterMs: input.error.retryAfterMs }
|
|
951
|
+
: {}),
|
|
952
|
+
},
|
|
953
|
+
status: input.error.status,
|
|
954
|
+
});
|
|
955
|
+
this.#observeHealth(failureEvent(refKey(input.model), classification));
|
|
956
|
+
// Recovery = fresh admission; the failed resource is already health-blocked.
|
|
957
|
+
let recovery = { kind: "none" };
|
|
958
|
+
let routingTier;
|
|
959
|
+
let probeTicket;
|
|
960
|
+
let admission;
|
|
961
|
+
try {
|
|
962
|
+
const { routing, decision, probeTicket: ticket, } = await this.#admit(settings, input.sessionID);
|
|
963
|
+
routingTier = routing.kind === "pin" ? undefined : routing.tier;
|
|
964
|
+
if (decision.kind === "apply") {
|
|
965
|
+
admission = decision;
|
|
966
|
+
recovery = {
|
|
967
|
+
kind: "switch",
|
|
968
|
+
model: decision.model,
|
|
969
|
+
...(decision.evidence.probeId !== undefined
|
|
970
|
+
? { probeId: decision.evidence.probeId }
|
|
971
|
+
: {}),
|
|
972
|
+
};
|
|
973
|
+
probeTicket = ticket;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
catch (error) {
|
|
977
|
+
logFailOpen("recovery", error);
|
|
978
|
+
}
|
|
979
|
+
const unitID = this.#tracker.active(input.sessionID)?.id;
|
|
980
|
+
const vetoesUsed = unitID !== undefined ? (this.#vetoes.get(unitID) ?? 0) : 0;
|
|
981
|
+
const core = decideRetry({
|
|
982
|
+
classification,
|
|
983
|
+
failingModel: input.model,
|
|
984
|
+
native: input.decision,
|
|
985
|
+
recovery,
|
|
986
|
+
vetoesUsed,
|
|
987
|
+
});
|
|
988
|
+
const wrapped = retryForMode(core, this.#modeFor(input.sessionID, settings));
|
|
989
|
+
if (wrapped.kind !== "veto") {
|
|
990
|
+
// OBSERVE (and a declined veto) never runs the recovery episode, so an
|
|
991
|
+
// admitted probe has no continuation to report it — re-arm it here or
|
|
992
|
+
// the resource wedges half-open.
|
|
993
|
+
if (probeTicket !== undefined) {
|
|
994
|
+
this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
|
|
995
|
+
}
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
input.decision = { retry: false };
|
|
999
|
+
if (unitID !== undefined) {
|
|
1000
|
+
this.#vetoes.set(unitID, vetoesUsed + 1);
|
|
1001
|
+
}
|
|
1002
|
+
// A probe admitted by this recovery belongs to the continuation episode
|
|
1003
|
+
// that ownRecovery is about to start; its termination reports the probe.
|
|
1004
|
+
if (probeTicket !== undefined) {
|
|
1005
|
+
// The session holds one pending probe slot. A still-pending older
|
|
1006
|
+
// ticket belongs to a recovery whose continuation has not started (its
|
|
1007
|
+
// episode can no longer claim this slot) — re-arm it before replacing.
|
|
1008
|
+
const stale = this.#sessionProbeTickets.get(input.sessionID);
|
|
1009
|
+
if (stale !== undefined && stale !== probeTicket) {
|
|
1010
|
+
this.#reportProbeOutcome(stale, { success: false, category: "user_cancelled" });
|
|
1011
|
+
}
|
|
1012
|
+
this.#sessionProbeTickets.set(input.sessionID, probeTicket);
|
|
1013
|
+
}
|
|
1014
|
+
// The continuation episode is not re-admitted — its prompt hook is
|
|
1015
|
+
// consumed as an owned continuation — so the recovery admission is
|
|
1016
|
+
// recorded here, unit-less; the unit that opens next adopts this receipt
|
|
1017
|
+
// and carries the outcome.
|
|
1018
|
+
// A veto implies the admission was an apply: the recovery option exists
|
|
1019
|
+
// only when the fresh admission selected a resource, so the assertion
|
|
1020
|
+
// cannot be undefined on this path.
|
|
1021
|
+
await this.#recordReceipt({
|
|
1022
|
+
sessionID: input.sessionID,
|
|
1023
|
+
tier: routingTier,
|
|
1024
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- a veto implies the admission was an apply (the recovery exists only then)
|
|
1025
|
+
decision: admission,
|
|
1026
|
+
});
|
|
1027
|
+
void this.#ownRecovery(input.sessionID, wrapped.model, routingTier)
|
|
1028
|
+
.then(() => undefined)
|
|
1029
|
+
.catch(async (error) => {
|
|
1030
|
+
logFailOpen("recovery-ops", error);
|
|
1031
|
+
// The recovery rejected before (or while) delivering its continuation
|
|
1032
|
+
// prompt, so no unit will ever carry the probe it admitted — re-arm
|
|
1033
|
+
// it instead of wedging the resource half-open.
|
|
1034
|
+
if (probeTicket !== undefined &&
|
|
1035
|
+
this.#sessionProbeTickets.get(input.sessionID) === probeTicket) {
|
|
1036
|
+
this.#sessionProbeTickets.delete(input.sessionID);
|
|
1037
|
+
this.#reportProbeOutcome(probeTicket, { success: false, category: "user_cancelled" });
|
|
1038
|
+
}
|
|
1039
|
+
if (unitID === undefined || routingTier === undefined)
|
|
1040
|
+
return;
|
|
1041
|
+
try {
|
|
1042
|
+
await this.#parked.park({
|
|
1043
|
+
id: unitID,
|
|
1044
|
+
sessionID: input.sessionID,
|
|
1045
|
+
tier: routingTier,
|
|
1046
|
+
reason: "all-blocked",
|
|
1047
|
+
detail: "continuation failed",
|
|
1048
|
+
parkedAt: this.#options.clock(),
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
catch (parkError) {
|
|
1052
|
+
logFailOpen("parked", parkError);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Owned horizontal recovery: settle → switch → continue from session
|
|
1058
|
+
* state. `tier` is the continuation unit's routing tier — recorded so the
|
|
1059
|
+
* continuation's prompt hook re-arms the admission marker (the unit must
|
|
1060
|
+
* not be re-routed: that would discard the recovery/escalation decision).
|
|
1061
|
+
*/
|
|
1062
|
+
async #ownRecovery(sessionID, model, tier) {
|
|
1063
|
+
if (tier !== undefined)
|
|
1064
|
+
this.#continuationTiers.set(sessionID, tier);
|
|
1065
|
+
const alive = await this.#isSessionAlive(sessionID);
|
|
1066
|
+
const recovery = { kind: "switch", model };
|
|
1067
|
+
const action = planContinuation({
|
|
1068
|
+
recovery,
|
|
1069
|
+
continuationPossible: alive,
|
|
1070
|
+
});
|
|
1071
|
+
try {
|
|
1072
|
+
await executeContinuation({ sessionID, action, ops: this.#ops });
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
// The continuation prompt never reached the host, so no unit will open
|
|
1076
|
+
// to consume the tier intent — drop it or it would pin the marker
|
|
1077
|
+
// wipes and mislabel a later user prompt's admission.
|
|
1078
|
+
if (tier !== undefined)
|
|
1079
|
+
this.#continuationTiers.delete(sessionID);
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// ---------- parked-work resume (safe boundary: idle) ----------
|
|
1084
|
+
async #onIdle(settings) {
|
|
1085
|
+
const records = this.#parked.list();
|
|
1086
|
+
// Override holders join the alive sweep: a dead session's override is
|
|
1087
|
+
// pruned here (its per-session steering is worthless once the session is
|
|
1088
|
+
// gone), in the same sweep that clears the other per-session maps.
|
|
1089
|
+
const overrideIDs = this.#sessionModes.snapshot().map((record) => record.sessionID);
|
|
1090
|
+
if (records.length === 0 && overrideIDs.length === 0 && this.#tierRequests.size === 0)
|
|
1091
|
+
return;
|
|
1092
|
+
// OFF sessions are not supervised: their parked records are neither
|
|
1093
|
+
// dropped nor resumed, and no alive probe runs for them.
|
|
1094
|
+
const supervised = new Set(records
|
|
1095
|
+
.map((record) => record.sessionID)
|
|
1096
|
+
.filter((sessionID) => this.#modeFor(sessionID, settings) !== "off"));
|
|
1097
|
+
const alive = new Map();
|
|
1098
|
+
for (const sessionID of new Set([...supervised, ...overrideIDs])) {
|
|
1099
|
+
alive.set(sessionID, await this.#isSessionAlive(sessionID));
|
|
1100
|
+
}
|
|
1101
|
+
// Tier-request holders join the alive sweep too: a dead session's sticky
|
|
1102
|
+
// tier request dies with its session, like the other per-session maps.
|
|
1103
|
+
for (const sessionID of this.#tierRequests.keys()) {
|
|
1104
|
+
if (!alive.has(sessionID))
|
|
1105
|
+
alive.set(sessionID, await this.#isSessionAlive(sessionID));
|
|
1106
|
+
}
|
|
1107
|
+
const plan = selectResumable(records, {
|
|
1108
|
+
tierConfigured: (tier) => configuredTiers(settings).includes(tier),
|
|
1109
|
+
sessionAlive: (sessionID) => alive.get(sessionID) === true,
|
|
1110
|
+
});
|
|
1111
|
+
for (const dropped of plan.drop) {
|
|
1112
|
+
if (this.#modeFor(dropped.sessionID, settings) === "off")
|
|
1113
|
+
continue;
|
|
1114
|
+
await this.#dropParked(dropped);
|
|
1115
|
+
}
|
|
1116
|
+
// Tier requests die with their session, like the other per-session maps.
|
|
1117
|
+
for (const sessionID of this.#tierRequests.keys()) {
|
|
1118
|
+
if (alive.get(sessionID) === false)
|
|
1119
|
+
this.#tierRequests.delete(sessionID);
|
|
1120
|
+
}
|
|
1121
|
+
// At most ONE continuation per session per sweep: prompting a session
|
|
1122
|
+
// with an active unit would supersede the user's in-flight episode (not
|
|
1123
|
+
// a safe boundary), and several back-to-back continuation prompts could
|
|
1124
|
+
// redo work an earlier one already completed (ADP-023).
|
|
1125
|
+
const attempted = new Set();
|
|
1126
|
+
for (const record of plan.attempt) {
|
|
1127
|
+
if (this.#modeFor(record.sessionID, settings) === "off")
|
|
1128
|
+
continue;
|
|
1129
|
+
if (this.#tracker.active(record.sessionID) !== undefined)
|
|
1130
|
+
continue;
|
|
1131
|
+
if (attempted.has(record.sessionID))
|
|
1132
|
+
continue;
|
|
1133
|
+
attempted.add(record.sessionID);
|
|
1134
|
+
await this.#resumeAttempt(record, settings);
|
|
1135
|
+
}
|
|
1136
|
+
for (const [sessionID, isAlive] of alive) {
|
|
1137
|
+
if (!isAlive) {
|
|
1138
|
+
this.#continuationTiers.delete(sessionID);
|
|
1139
|
+
this.#promptAdmission.delete(sessionID);
|
|
1140
|
+
this.#continuationPending.delete(sessionID);
|
|
1141
|
+
if (this.#sessionModes.get(sessionID) !== null) {
|
|
1142
|
+
try {
|
|
1143
|
+
await this.#sessionModes.set(sessionID, null);
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
logFailOpen("session-modes", error);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async #dropParked(record) {
|
|
1153
|
+
try {
|
|
1154
|
+
await this.#parked.resume(record.id);
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
logFailOpen("parked", error);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
/** One parked record's fresh routing attempt at the idle boundary. */
|
|
1161
|
+
async #resumeAttempt(record, settings) {
|
|
1162
|
+
// The parked session resumes under ITS effective mode, not the idle
|
|
1163
|
+
// report's session's mode.
|
|
1164
|
+
const mode = this.#modeFor(record.sessionID, settings);
|
|
1165
|
+
let decision;
|
|
1166
|
+
try {
|
|
1167
|
+
decision = (await this.#admit(settings, record.sessionID)).decision;
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
logFailOpen("resume", error);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (mode === "observe") {
|
|
1174
|
+
await this.#recordReceipt({
|
|
1175
|
+
sessionID: record.sessionID,
|
|
1176
|
+
unitID: record.id,
|
|
1177
|
+
tier: record.tier,
|
|
1178
|
+
decision: { kind: "observe-only", would: decision },
|
|
1179
|
+
});
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (decision.kind !== "apply") {
|
|
1183
|
+
// Still no usable resource (or the routing deferred) — stay parked.
|
|
1184
|
+
await this.#recordReceipt({
|
|
1185
|
+
sessionID: record.sessionID,
|
|
1186
|
+
unitID: record.id,
|
|
1187
|
+
tier: record.tier,
|
|
1188
|
+
decision,
|
|
1189
|
+
});
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const popped = await this.#popParked(record);
|
|
1193
|
+
if (popped === undefined)
|
|
1194
|
+
return;
|
|
1195
|
+
// The resume continuation is not re-admitted (owned continuation), so
|
|
1196
|
+
// the freshly routed admission is recorded here, unit-less; the unit
|
|
1197
|
+
// that opens next adopts it and carries the outcome.
|
|
1198
|
+
await this.#recordReceipt({
|
|
1199
|
+
sessionID: record.sessionID,
|
|
1200
|
+
tier: record.tier,
|
|
1201
|
+
decision,
|
|
1202
|
+
});
|
|
1203
|
+
// The resume continuation is an owned recovery: settle first, apply the
|
|
1204
|
+
// freshly routed model, then prompt — the episode's first LLM call must
|
|
1205
|
+
// not hit the model the work was parked away from.
|
|
1206
|
+
try {
|
|
1207
|
+
await this.#ownRecovery(record.sessionID, decision.model, record.tier);
|
|
1208
|
+
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
logFailOpen("resume", error);
|
|
1211
|
+
await this.#rePark(record);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
async #popParked(record) {
|
|
1215
|
+
try {
|
|
1216
|
+
return await this.#parked.resume(record.id);
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
logFailOpen("parked", error);
|
|
1220
|
+
return undefined;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
async #rePark(record) {
|
|
1224
|
+
try {
|
|
1225
|
+
await this.#parked.park({
|
|
1226
|
+
...record,
|
|
1227
|
+
attempts: (record.attempts ?? 0) + 1,
|
|
1228
|
+
parkedAt: this.#options.clock(),
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
logFailOpen("parked", error);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
// ---------- shared helpers ----------
|
|
1236
|
+
async #isSessionAlive(sessionID) {
|
|
1237
|
+
try {
|
|
1238
|
+
await this.#options.session.get({ sessionID });
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
async #recordReceipt(input) {
|
|
1246
|
+
try {
|
|
1247
|
+
await this.#receipts.record({ at: this.#options.clock(), ...input });
|
|
1248
|
+
}
|
|
1249
|
+
catch (error) {
|
|
1250
|
+
logFailOpen("receipts", error);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async #attachOutcome(unit, outcome) {
|
|
1254
|
+
try {
|
|
1255
|
+
await this.#receipts.attachOutcome({ sessionID: unit.sessionID, unitID: unit.id }, outcome);
|
|
1256
|
+
}
|
|
1257
|
+
catch (error) {
|
|
1258
|
+
logFailOpen("receipts", error);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
#observeHealth(event) {
|
|
1262
|
+
this.#health.observe(event);
|
|
1263
|
+
// Persist the record mirror fire-and-forget: the status surface reads
|
|
1264
|
+
// persisted records so a serving instance without the event-pump hooks
|
|
1265
|
+
// still reports true health. Failures are fail-open (ADP-031).
|
|
1266
|
+
const records = this.#health.snapshot();
|
|
1267
|
+
void this.#healthRecords.replace(records).catch((error) => {
|
|
1268
|
+
logFailOpen("health", error);
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Persist the capacity mirror after a passive observation (same instance
|
|
1273
|
+
* split rationale as the health mirror): the status surface reads persisted
|
|
1274
|
+
* readings so a serving instance without the event-pump hooks still reports
|
|
1275
|
+
* sensed capacity. Failures are fail-open (ADP-031).
|
|
1276
|
+
*/
|
|
1277
|
+
async #syncCapacityMirror() {
|
|
1278
|
+
try {
|
|
1279
|
+
await this.#capacity.refresh();
|
|
1280
|
+
const entries = [];
|
|
1281
|
+
for (const sensor of this.#capacity.snapshot()) {
|
|
1282
|
+
if (sensor.latest !== null) {
|
|
1283
|
+
entries.push({ key: sensor.resource, reading: sensor.latest });
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
await this.#capacityStore.replace(entries);
|
|
1287
|
+
}
|
|
1288
|
+
catch (error) {
|
|
1289
|
+
logFailOpen("capacity", error);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
_a = Supervisor;
|
|
1294
|
+
/** A pin is not a tier — receipts only carry a tier for tiered routing. */
|
|
1295
|
+
function routingTier(routing) {
|
|
1296
|
+
return routing.kind === "pin" ? undefined : routing.tier;
|
|
1297
|
+
}
|
|
1298
|
+
/** The unit a tracker result closed or superseded, if any. */
|
|
1299
|
+
function closedUnitID(result) {
|
|
1300
|
+
switch (result.kind) {
|
|
1301
|
+
case "closed":
|
|
1302
|
+
return result.unit.id;
|
|
1303
|
+
case "opened":
|
|
1304
|
+
return result.superseded?.id;
|
|
1305
|
+
default:
|
|
1306
|
+
return undefined;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
/** ModelRef for a normalized candidate. */
|
|
1310
|
+
function candidateRef(candidate) {
|
|
1311
|
+
return {
|
|
1312
|
+
providerID: candidate.providerID,
|
|
1313
|
+
id: candidate.modelID,
|
|
1314
|
+
...(candidate.variant !== undefined ? { variant: candidate.variant } : {}),
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
/** ResourceKey for a model ref (no account level is observable in v1). */
|
|
1318
|
+
function refKey(ref) {
|
|
1319
|
+
// The host reports base-model calls with variant "default" (verified on
|
|
1320
|
+
// 2.2.5), while the inventory enumerates the bare model plus its named
|
|
1321
|
+
// variants. Taken literally, a "default" key would orphan every
|
|
1322
|
+
// unvarianted failure record on a key no admission ever assesses — health
|
|
1323
|
+
// and breaker state would be invisible to routing.
|
|
1324
|
+
const variant = ref.variant === "default" ? undefined : ref.variant;
|
|
1325
|
+
return {
|
|
1326
|
+
providerID: ref.providerID,
|
|
1327
|
+
modelID: ref.id,
|
|
1328
|
+
...(variant !== undefined ? { variantID: variant } : {}),
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
/** Health failure event from a classification (bounded structured evidence). */
|
|
1332
|
+
function failureEvent(key, classification) {
|
|
1333
|
+
return {
|
|
1334
|
+
kind: "failure",
|
|
1335
|
+
key,
|
|
1336
|
+
category: classification.category,
|
|
1337
|
+
definitive: classification.definitive,
|
|
1338
|
+
...(classification.retry.retryAfterMs !== undefined
|
|
1339
|
+
? { retryAfterMs: classification.retry.retryAfterMs }
|
|
1340
|
+
: {}),
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
//# sourceMappingURL=supervisor.js.map
|