BUILD-REPORT-FLT128: unavailable-mode hard gate + detector + jobClass¶
Branch: wt/WT-6022/FLT-128 (worktree fd-FLT128, off origin/master @ 86a620b). Not merged to
master.
What was built¶
FLT-128 — the stateless execution-time hard gate¶
AvailabilityGate.Evaluate(jobClass, signals, now)— pure static function, zero I/O (src/FleetDispatch.Core/Availability/AvailabilityGate.cs). Rules:controlalways runs; an active manual override wins over live signals in either direction; OBS running refuses everything butcontrol;trivialsurvives a gaming block; SteamRunningAppID != 0or an allowlisted process refusesheavy_cpu/heavy_gpu/focus_risk.- Wired as the first check (before
GovernorGate.ShouldDefer) in both execution chokepoints: QueueService.ListenOnceAsync— refusal leaves the job file untouched ininbox/(never moved toprocessing/), exactly like a governor defer. NewListenSummary.Unavailablecounter, logged distinctly fromDeferred.DispatchServer's/dispatchhandler — refusal returns HTTP 503 with{deferred:true, unavailable:true, reason, detail, blockLevel}; nothing is written to outbox/done, so the sender's own retry is the requeue (same contract the governor's 503 already uses).- Re-reads live signals fresh on every job via
IAvailabilitySignalsProvider.GetSignals()— never trusts the detector's cached/hysteresis'd state.
FLT-129 — the detector (presence loop) + heartbeat publish¶
AvailabilityDetectorLogic.Evaluate— pure hysteresis logic (gaming: 90s clear-quiet grace; streaming: 120s; any trip resets the relevant timer).AvailabilityDetectorwraps it in an always-on ~4s poll loop, run as its own task inserve(separate from the gated queue-drain task, per spec — the listener keeps detecting re-availability even while every job is refused).- Signals:
WindowsAvailabilitySignalsProvider—HKCU\Software\Valve\Steam\RunningAppID(never gated behind GPU), best-effort game-name resolution viaappmanifest_<id>.acf,obs64/obs32process presence, a local JSON allowlist for non-Steam games (FleetDispatchPaths.GameAllowlistPath), and the manual override file (ManualOverrideFile, plain JSON I/O, not OS-gated). - Published as a new
availabilityobject on/health(this repo's WorkerHeartbeat):{available, mode, reason, detail, blockLevel, since}— seedocs/availability-contract.mdfor the exact shared shape.
jobClass¶
DispatchJob.JobClass(nullable string) added to the envelope.JobClassification.Parsedefaults unknown/unset toheavy_cpu(fail closed).- Signed: appended to
JobSigner.CanonicalString(an unsigned field would let a queue-write-only actor relabel a job tocontrolto dodge the gate). This is canonical v2 —DispatchJob.CurrentSchemaVersionbumped1 → 2, so an out-of-date listener names the skew (unsupported-schema) instead of a blind HMAC failure, exactly per the existing FLT-70 mechanism. --job-classCLI flag onenqueue/send(validated, case-insensitive, refuses an unknown value at enqueue).SteerService.FoldPendingcarriesJobClassthrough a steer-triggered rebuild.
Build + test¶
dotnet build -warnaserror → Build succeeded. 0 Warning(s). 0 Error(s).
dotnet test → Passed! Failed: 0, Passed: 436, Skipped: 1, Total: 437
The 1 skip (AgentRunLiveAgyTests...) is pre-existing and unrelated (a live-agy e2e test that
skips without the real agy CLI on this box). 60 of the 436 passing tests are new
(tests/FleetDispatch.Tests/Availability/*), covering: gate refusal for gaming/streaming/manual
per jobClass, control-always-runs, trivial-survives-gaming-not-streaming, unknown-jobClass defaults
to heavy_cpu, manual-override expiry (both values, not just "available"), grace-timer hold/reset
math (continuous-poll simulation), QueueService/DispatchServer integration (requeue-in-inbox / 503
+ no-outbox-write), /health surfacing the detector's published state, and
JobSigner.CanonicalString/tamper coverage for the new signed field.
Files¶
src/FleetDispatch.Core/Availability/(new):JobClass.cs,AvailabilitySignals.cs,IAvailabilitySignalsProvider.cs,ManualOverrideFile.cs,AvailabilityGate.cs,WorkerAvailability.cs,AvailabilityDetectorLogic.cs,AvailabilityDetector.cs,WindowsAvailabilitySignalsProvider.cs- Modified:
DispatchJob.cs,JobSigner.cs,FleetDispatchPaths.cs,QueueService.cs,DispatchServer.cs,SteerService.cs,FleetDispatch.Core.csproj,Program.cs docs/availability-contract.md(new) — the shared contract Mission Control's builder implements against.tests/FleetDispatch.Tests/Availability/(new, 8 files) +SigningTests.cs(updated for the new canonical shape).
Shared contract (see docs/availability-contract.md for full detail)¶
availabilityobject on WorkerHeartbeat (/health):{available, mode, reason, detail, blockLevel, since}.- Manual override file:
{DispatchRoot}/availability/{PC}.override.json = {pc, override: "auto"|"unavailable"|"available", expiresAt}. Any override with a pastexpiresAt→ treated as"auto"(both values, not just"available"— a design decision documented in the contract doc).
TODOs / known gaps (documented in docs/availability-contract.md)¶
- P2 fullscreen+GPU heuristic for an unlisted, non-Steam game — explicitly deferred per the FLT-129 spec (GPU-polling dependency + false-positive risk not worth taking on this pass).
- FLT-112 pause-in-flight: the hard gate only intercepts before a job starts. A
heavy_cpujob already running when gaming/streaming begins is not paused or killed by this change — pausing an in-flight job needs its own cooperative-yield design and is out of scope here. - Non-queued direct-execution paths are ungated:
serve's own repo-scan timer loop (RunRepoScanLoopAsync) calls the scanner directly, bypassingIJobExecutorentirely — neither this gate norGovernorGateapply to it (pre-existing gap, not introduced here). Flagged in the contract doc with the specific fix if it's ever wanted. - No admin UI for the local game-process allowlist file (hand-edited JSON today).
Commit¶
wt/WT-6022/FLT-128, not merged/pushed to master. Explicit git add of touched paths, no git add -A.