A player had to launch Rocket League twice: the first launch streamed the "Processing Vulkan shaders" dialog and then dropped, ten seconds in. The host did that to itself.
What the log shows
From a field report on 2026-08-22 (Linux, KDE/kwin, H265/NVENC), times in 18:39:
t
event
24.816
host spawns steam steam://rungameid/252950
25.822
that command hands off and exits — lease falls back to recognizing the game by scan (kind=matched)
26.832
the launched game is running … procs=1 — the scan matched one process, 2 s in
~31.9
that process disappears
34.873
the launched game exited reason=the game exited → APP_EXITED, stream torn down, sent=864
reason=the game exited pins the branch exactly: the phase-2 arm that fires when the process scan has seen nothing for EXIT_CONFIRM (3 s). Not the child-exit path, not a provider report.
Root cause
DetectSpec::steam_appid asserted that reaper SteamLaunch AppId=<appid> has a lifetime "exactly the game's". It does not — that reaper belongs to the appid, not the game. Steam wraps its pre-launch work for a title in one too (shader pre-caching most visibly), so a launch is a chain of appid-tagged trees and only the last of them is the game.
That is fatal because the two watcher phases are patient in opposite directions:
Phase 1 (game not yet seen) waits START_GRACE = 300 s, and expiring ends nothing.
Phase 2 (game seen once) waits EXIT_CONFIRM = 3 s, and expiring ends the session.
A single sighting on a single poll latched the lease from the first into the second, permanently. When that tree exited with the game still starting, phase 2 called it the game exiting.
Linux has nothing to catch it: procscan::running_hint is Windows-only (returns None unconditionally), and no provider reports runstate for Steam — so an appid scan with three seconds of slack is the entire liveness signal. The second launch works because cached shaders mean there is no pre-launch tree to latch onto.
Steam's registry.vdf was considered as the Linux equivalent of the Windows veto and rejected: RunningAppID is no longer set on modern Steam Linux (ValveSoftware/steam-for-linux#9672) and the per-app Running key is unreliable.
The change
Two layers, because only one of them can be certain:
procscan/linux.rs — the appid recipe now rejects a SteamLaunch AppId= reaper whose payload is fossilize_replay. Steam's shader replayer is never a game, and it is the one pre-launch tree that can be identified positively.
gamelease.rs — a scan match must be seen continuously for SHIM_WINDOW before it latches out of phase 1. This is the rule already applied to a spawned child ("a launcher about to hand off looks exactly like the game for its first few seconds"); the scan side never had it. It bounds the pre-launch trees nobody has named yet, at the cost of a few seconds of GameRunning latency. Exit detection is untouched, and a provider report still latches immediately — that is the launcher's own statement, not an inference from a lookalike process.
procscan::names() — the "game is running" line now logs which processes were adopted. procs=1 is precisely why this report could not be closed from its log.
DetectSpec's doc comment is corrected, since that assumption is what the code was built on.
Known ceiling
Named in the code: a pre-launch tree that outlives SHIM_WINDOW still latches. Layer 1 handles the shader case specifically; if a longer-lived one turns up, the new names= field will identify it.
The log records procs=1 without naming the process, so it cannot be proven from the log alone that the match was the shader job specifically. Both layers are built to hold either way.
Tests
procscan: the shader reaper and the game reaper — same appid, same wrapper — resolve to only the game.
gamelease: a Linux regression test (#[ignore]d like its siblings, ~11 s) drives a real short-lived matching process and asserts the session-ending action never fires.
Verification status
rustfmt --check passes on all five files under the CI-identical toolchain, and both changed rules were verified natively against verbatim copies (including a replay of the incident's second-by-second sequence, which no longer latches). The full cargo test -p punktfunk-host --lib in the Linux CI image did not converge locally — the emulated Docker VM was saturated — so this PR's ci / rust run is the first real compile. The Windows name_of helper is not compiled by PR CI at all; windows-host.yml should be dispatched on this branch.
No API surface is touched, so no OpenAPI regeneration.
A player had to launch Rocket League twice: the first launch streamed the "Processing Vulkan shaders" dialog and then dropped, ten seconds in. The host did that to itself.
## What the log shows
From a field report on 2026-08-22 (Linux, KDE/kwin, H265/NVENC), times in `18:39`:
| t | event |
|---|---|
| `24.816` | host spawns `steam steam://rungameid/252950` |
| `25.822` | that command hands off and exits — lease falls back to recognizing the game by scan (`kind=matched`) |
| `26.832` | `the launched game is running … procs=1` — the scan matched **one** process, 2 s in |
| `~31.9` | that process disappears |
| `34.873` | `the launched game exited reason=the game exited` → `APP_EXITED`, stream torn down, `sent=864` |
`reason=the game exited` pins the branch exactly: the phase-2 arm that fires when the process scan has seen nothing for `EXIT_CONFIRM` (3 s). Not the child-exit path, not a provider report.
## Root cause
`DetectSpec::steam_appid` asserted that `reaper SteamLaunch AppId=<appid>` has a lifetime "exactly the game's". It does not — that reaper belongs to the **appid**, not the game. Steam wraps its *pre-launch* work for a title in one too (shader pre-caching most visibly), so a launch is a chain of appid-tagged trees and only the last of them is the game.
That is fatal because the two watcher phases are patient in opposite directions:
- **Phase 1** (game not yet seen) waits `START_GRACE` = 300 s, and expiring ends *nothing*.
- **Phase 2** (game seen once) waits `EXIT_CONFIRM` = 3 s, and expiring **ends the session**.
A single sighting on a single poll latched the lease from the first into the second, permanently. When that tree exited with the game still starting, phase 2 called it the game exiting.
Linux has nothing to catch it: `procscan::running_hint` is Windows-only (returns `None` unconditionally), and no provider reports runstate for Steam — so an appid scan with three seconds of slack is the entire liveness signal. The second launch works because cached shaders mean there is no pre-launch tree to latch onto.
Steam's `registry.vdf` was considered as the Linux equivalent of the Windows veto and rejected: `RunningAppID` is no longer set on modern Steam Linux (ValveSoftware/steam-for-linux#9672) and the per-app `Running` key is unreliable.
## The change
Two layers, because only one of them can be certain:
1. **`procscan/linux.rs`** — the appid recipe now rejects a `SteamLaunch AppId=` reaper whose payload is `fossilize_replay`. Steam's shader replayer is never a game, and it is the one pre-launch tree that can be identified positively.
2. **`gamelease.rs`** — a scan match must be seen *continuously* for `SHIM_WINDOW` before it latches out of phase 1. This is the rule already applied to a spawned child ("a launcher about to hand off looks *exactly* like the game for its first few seconds"); the scan side never had it. It bounds the pre-launch trees nobody has named yet, at the cost of a few seconds of `GameRunning` latency. Exit detection is untouched, and a provider report still latches immediately — that is the launcher's own statement, not an inference from a lookalike process.
3. **`procscan::names()`** — the "game is running" line now logs *which* processes were adopted. `procs=1` is precisely why this report could not be closed from its log.
`DetectSpec`'s doc comment is corrected, since that assumption is what the code was built on.
## Known ceiling
Named in the code: a pre-launch tree that outlives `SHIM_WINDOW` still latches. Layer 1 handles the shader case specifically; if a longer-lived one turns up, the new `names=` field will identify it.
The log records `procs=1` without naming the process, so it cannot be proven from the log alone that the match was the shader job specifically. Both layers are built to hold either way.
## Tests
- `procscan`: the shader reaper and the game reaper — same appid, same wrapper — resolve to only the game.
- `gamelease`: a Linux regression test (`#[ignore]`d like its siblings, ~11 s) drives a real short-lived matching process and asserts the session-ending action never fires.
## Verification status
`rustfmt --check` passes on all five files under the CI-identical toolchain, and both changed rules were verified natively against verbatim copies (including a replay of the incident's second-by-second sequence, which no longer latches). **The full `cargo test -p punktfunk-host --lib` in the Linux CI image did not converge locally** — the emulated Docker VM was saturated — so this PR's `ci / rust` run is the first real compile. The Windows `name_of` helper is not compiled by PR CI at all; `windows-host.yml` should be dispatched on this branch.
No API surface is touched, so no OpenAPI regeneration.
A player had to launch Rocket League twice: the first launch streamed the
"Processing Vulkan shaders" dialog and then dropped, ten seconds in. The host
did that to itself.
`reaper SteamLaunch AppId=<appid>` is the *appid's* wrapper, not the game's.
Steam wraps its pre-launch work for a title in one too, so a launch is a chain
of appid-tagged trees and only the last is the game. The lease matched the
first tree two seconds in, and that single sighting latched it out of the start
phase (START_GRACE, five minutes, ending nothing) into the exit watch
(EXIT_CONFIRM, three seconds, ending the session). When the tree exited with
the game still starting, the watch called it the game exiting and closed the
connection with APP_EXITED.
Linux has nothing to catch that: `procscan::running_hint` is Windows-only, and
no provider reports runstate for Steam, so an appid scan with three seconds of
slack is the whole signal. (Steam's registry.vdf is not an option — RunningAppID
is no longer set on modern Steam Linux, and the per-app Running key is
unreliable.)
Two layers, because only one of them can be certain:
* The matcher now rejects a `SteamLaunch AppId=` reaper whose payload is
`fossilize_replay` — Steam's shader replayer, never a game.
* A scan match must be seen continuously for SHIM_WINDOW before it latches.
This is the rule already applied to a spawned child ("a launcher about to
hand off looks exactly like the game for its first few seconds"); the scan
side never had it. It bounds the pre-launch trees nobody has named yet, at
the cost of a few seconds of GameRunning latency. Exit detection is
untouched, and a provider report still latches immediately — that is the
launcher's own statement, not an inference from a lookalike.
The log said `procs=1` and never which process, which is what made this
unclosable from a log alone; `procscan::names` puts that on the line.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A player had to launch Rocket League twice: the first launch streamed the "Processing Vulkan shaders" dialog and then dropped, ten seconds in. The host did that to itself.
What the log shows
From a field report on 2026-08-22 (Linux, KDE/kwin, H265/NVENC), times in
18:39:24.816steam steam://rungameid/25295025.822kind=matched)26.832the launched game is running … procs=1— the scan matched one process, 2 s in~31.934.873the launched game exited reason=the game exited→APP_EXITED, stream torn down,sent=864reason=the game exitedpins the branch exactly: the phase-2 arm that fires when the process scan has seen nothing forEXIT_CONFIRM(3 s). Not the child-exit path, not a provider report.Root cause
DetectSpec::steam_appidasserted thatreaper SteamLaunch AppId=<appid>has a lifetime "exactly the game's". It does not — that reaper belongs to the appid, not the game. Steam wraps its pre-launch work for a title in one too (shader pre-caching most visibly), so a launch is a chain of appid-tagged trees and only the last of them is the game.That is fatal because the two watcher phases are patient in opposite directions:
START_GRACE= 300 s, and expiring ends nothing.EXIT_CONFIRM= 3 s, and expiring ends the session.A single sighting on a single poll latched the lease from the first into the second, permanently. When that tree exited with the game still starting, phase 2 called it the game exiting.
Linux has nothing to catch it:
procscan::running_hintis Windows-only (returnsNoneunconditionally), and no provider reports runstate for Steam — so an appid scan with three seconds of slack is the entire liveness signal. The second launch works because cached shaders mean there is no pre-launch tree to latch onto.Steam's
registry.vdfwas considered as the Linux equivalent of the Windows veto and rejected:RunningAppIDis no longer set on modern Steam Linux (ValveSoftware/steam-for-linux#9672) and the per-appRunningkey is unreliable.The change
Two layers, because only one of them can be certain:
procscan/linux.rs— the appid recipe now rejects aSteamLaunch AppId=reaper whose payload isfossilize_replay. Steam's shader replayer is never a game, and it is the one pre-launch tree that can be identified positively.gamelease.rs— a scan match must be seen continuously forSHIM_WINDOWbefore it latches out of phase 1. This is the rule already applied to a spawned child ("a launcher about to hand off looks exactly like the game for its first few seconds"); the scan side never had it. It bounds the pre-launch trees nobody has named yet, at the cost of a few seconds ofGameRunninglatency. Exit detection is untouched, and a provider report still latches immediately — that is the launcher's own statement, not an inference from a lookalike process.procscan::names()— the "game is running" line now logs which processes were adopted.procs=1is precisely why this report could not be closed from its log.DetectSpec's doc comment is corrected, since that assumption is what the code was built on.Known ceiling
Named in the code: a pre-launch tree that outlives
SHIM_WINDOWstill latches. Layer 1 handles the shader case specifically; if a longer-lived one turns up, the newnames=field will identify it.The log records
procs=1without naming the process, so it cannot be proven from the log alone that the match was the shader job specifically. Both layers are built to hold either way.Tests
procscan: the shader reaper and the game reaper — same appid, same wrapper — resolve to only the game.gamelease: a Linux regression test (#[ignore]d like its siblings, ~11 s) drives a real short-lived matching process and asserts the session-ending action never fires.Verification status
rustfmt --checkpasses on all five files under the CI-identical toolchain, and both changed rules were verified natively against verbatim copies (including a replay of the incident's second-by-second sequence, which no longer latches). The fullcargo test -p punktfunk-host --libin the Linux CI image did not converge locally — the emulated Docker VM was saturated — so this PR'sci / rustrun is the first real compile. The Windowsname_ofhelper is not compiled by PR CI at all;windows-host.ymlshould be dispatched on this branch.No API surface is touched, so no OpenAPI regeneration.
A player had to launch Rocket League twice: the first launch streamed the "Processing Vulkan shaders" dialog and then dropped, ten seconds in. The host did that to itself. `reaper SteamLaunch AppId=<appid>` is the *appid's* wrapper, not the game's. Steam wraps its pre-launch work for a title in one too, so a launch is a chain of appid-tagged trees and only the last is the game. The lease matched the first tree two seconds in, and that single sighting latched it out of the start phase (START_GRACE, five minutes, ending nothing) into the exit watch (EXIT_CONFIRM, three seconds, ending the session). When the tree exited with the game still starting, the watch called it the game exiting and closed the connection with APP_EXITED. Linux has nothing to catch that: `procscan::running_hint` is Windows-only, and no provider reports runstate for Steam, so an appid scan with three seconds of slack is the whole signal. (Steam's registry.vdf is not an option — RunningAppID is no longer set on modern Steam Linux, and the per-app Running key is unreliable.) Two layers, because only one of them can be certain: * The matcher now rejects a `SteamLaunch AppId=` reaper whose payload is `fossilize_replay` — Steam's shader replayer, never a game. * A scan match must be seen continuously for SHIM_WINDOW before it latches. This is the rule already applied to a spawned child ("a launcher about to hand off looks exactly like the game for its first few seconds"); the scan side never had it. It bounds the pre-launch trees nobody has named yet, at the cost of a few seconds of GameRunning latency. Exit detection is untouched, and a provider report still latches immediately — that is the launcher's own statement, not an inference from a lookalike. The log said `procs=1` and never which process, which is what made this unclosable from a log alone; `procscan::names` puts that on the line.