From a field report (Discord, 2026-08-16 — Windows host 0.29.0 + iPad 0.30.0): quitting a game mid-stream left the session up and the web console showing it as running forever, with no setting that made any difference.
Ten work items came out of that conversation. All of them are here except cross-client parity (C7), deferred.
The bug was not a stale console
A lease with nothing to recognise its game by set its own state to Running, on the reasoning that the host had just launched it. That collapsed three different situations into one indistinguishable row — a game being watched, a game that quit and was never noticed, and a game the host cannot see at all — and session_on_game_exit can never fire for a lease nothing is watching, which is why no setting helped.
Windows reached that state far more easily than Linux, for a reason worth naming: the host holds no Child there, and the pid CreateProcessAsUserW hands back was logged and discarded. A title whose provider published no detect hint therefore had nothing identifying it, while the same title on Linux was fully tracked through its child.
Carrying that pid closes two further defects that were never separately reported:
with an empty spec the Windows terminate ladder found no pids, so "End" silently did nothing;
a launch that adopted nothing answers Liveness::Unknown, falls back to the 90-second in-flight window, and past it starts a second copy — which is why "click the game that is already running" resumed on Linux and relaunched on Windows.
Host
GameState::Untracked — a lease nothing is watching says so, in /status, the console card and the tray label. Keyed on is anything watching this rather than on the lease kind, so a nested gamescope lease (whose exit the capture loop catches) still correctly reads running.
LeaseRequest::spawned carries the pid Windows already knew, pinned to its start time by a new Scanner::resolve so a recycled pid cannot impersonate it. It takes the same lifetime rules the owned child gets — shim reclassification included — and feeds the Windows terminate ladder.
game_on_new_launch (keep | end, default keep) — close this client's previous game before starting a different one. Its own axis rather than a fourth game_on_session_end value: wanting a game to survive a disconnect says nothing about wanting it kept when you deliberately pick another. Four safety rules, made pure and unit-tested — never another client's game, never one the player started themselves, never the title being launched, and never a record whose liveness is merely Unknown.
Apple client — the loop the report was really about
Browse → play → quit → browse:
tapping a paired host opens its library; "Stream the Desktop" moves to the card menu
the catalog is cached per host and rendered immediately, marked stale, so a sleeping host still shows its titles (only art was cached before; the catalog was fetched live every visit)
opening the library wakes the host and retries across the boot window, so it is warm by the time a title is picked — waking was bound to connecting, which is too late to help
titles already up are badged Resume and sorted first, read from /status, which was already on the paired-cert lane — no new host API
the grid returns to where you were, remembered as the last title opened rather than a pixel offset, so it survives a rotation, a resize and a host gaining titles
Verification
Lane
Result
punktfunk-host (Linux container)
fmt, clippy --all-targets -D warnings, plain build, 597 tests green
OpenAPI
regenerated; drift test re-run green. Diff is purely additive
Web console
tsc --noEmit clean, 740 messages en + de
Apple
swift build + 347 tests on macOS, iOS and tvOS typechecked (CI compiles neither)
Two calls worth a second opinion
game_on_new_launch ships default-off. The lease's documented posture is that nothing is ended unless the operator asked, and ending a game costs unsaved progress — so I did not reverse that on my own. One line if you want end as the default.
No client confirmation prompt. The client cannot read /session/settings (admin lane only), so it cannot know whether to warn; an honest prompt needs a launch-intent flag on the wire, and that ABI change collides with the in-flight ex10/ABI-21 work.
Not in scope
C7, cross-client parity — Android, the shared console UI and webOS need the same treatment once the Apple interaction settles. The shared console UI has no running-game concept in its model at all, so C2/C3 are new surface there rather than a port.
⚠ Unrelated, but it will bite anyone building the Apple client: the PunktfunkCore.xcframework checked into working trees is stale (its header stops at punktfunk_connect_ex9; the sources need ex10/ex11), so swift build fails on any branch until it is rebuilt with CMAKE_POLICY_VERSION_MINIMUM=3.5 IPHONEOS_DEPLOYMENT_TARGET=17.0 BUILD_IOS=1 BUILD_TVOS=1 bash scripts/build-xcframework.sh.
From a field report (Discord, 2026-08-16 — Windows host 0.29.0 + iPad 0.30.0): quitting a game mid-stream left the session up and the web console showing it as **running** forever, with no setting that made any difference.
Ten work items came out of that conversation. All of them are here except cross-client parity (C7), deferred.
## The bug was not a stale console
A lease with nothing to recognise its game by set its **own** state to `Running`, on the reasoning that the host had just launched it. That collapsed three different situations into one indistinguishable row — a game being watched, a game that quit and was never noticed, and a game the host cannot see at all — and `session_on_game_exit` can never fire for a lease nothing is watching, which is why no setting helped.
Windows reached that state far more easily than Linux, for a reason worth naming: the host holds no `Child` there, and the pid `CreateProcessAsUserW` hands back was **logged and discarded**. A title whose provider published no detect hint therefore had *nothing* identifying it, while the same title on Linux was fully tracked through its child.
Carrying that pid closes two further defects that were never separately reported:
- with an empty spec the Windows terminate ladder found no pids, so **"End" silently did nothing**;
- a launch that adopted nothing answers `Liveness::Unknown`, falls back to the 90-second in-flight window, and past it starts a **second copy** — which is why "click the game that is already running" resumed on Linux and relaunched on Windows.
## Host
- **`GameState::Untracked`** — a lease nothing is watching says so, in `/status`, the console card and the tray label. Keyed on *is anything watching this* rather than on the lease kind, so a nested gamescope lease (whose exit the capture loop catches) still correctly reads `running`.
- **`LeaseRequest::spawned`** carries the pid Windows already knew, pinned to its start time by a new `Scanner::resolve` so a recycled pid cannot impersonate it. It takes the same lifetime rules the owned child gets — shim reclassification included — and feeds the Windows terminate ladder.
- **`game_on_new_launch` (`keep` | `end`, default `keep`)** — close this client's previous game before starting a different one. Its own axis rather than a fourth `game_on_session_end` value: wanting a game to survive a disconnect says nothing about wanting it kept when you deliberately pick another. Four safety rules, made pure and unit-tested — never another client's game, never one the player started themselves, never the title being launched, and never a record whose liveness is merely `Unknown`.
## Apple client — the loop the report was really about
Browse → play → quit → browse:
- tapping a paired host **opens its library**; "Stream the Desktop" moves to the card menu
- the **catalog is cached** per host and rendered immediately, marked stale, so a sleeping host still shows its titles (only *art* was cached before; the catalog was fetched live every visit)
- opening the library **wakes the host** and retries across the boot window, so it is warm by the time a title is picked — waking was bound to *connecting*, which is too late to help
- titles already up are badged **Resume** and sorted first, read from `/status`, which was already on the paired-cert lane — **no new host API**
- the grid **returns to where you were**, remembered as the last title opened rather than a pixel offset, so it survives a rotation, a resize and a host gaining titles
## Verification
| Lane | Result |
|---|---|
| `punktfunk-host` (Linux container) | fmt, clippy `--all-targets -D warnings`, plain build, **597 tests** green |
| OpenAPI | regenerated; drift test re-run green. Diff is purely additive |
| Web console | `tsc --noEmit` clean, 740 messages en + de |
| Apple | `swift build` + **347 tests** on macOS, **iOS and tvOS typechecked** (CI compiles neither) |
## Two calls worth a second opinion
- **`game_on_new_launch` ships default-off.** The lease's documented posture is that nothing is ended unless the operator asked, and ending a game costs unsaved progress — so I did not reverse that on my own. One line if you want `end` as the default.
- **No client confirmation prompt.** The client cannot read `/session/settings` (admin lane only), so it cannot know whether to warn; an honest prompt needs a launch-intent flag on the wire, and that ABI change collides with the in-flight ex10/ABI-21 work.
## Not in scope
C7, cross-client parity — Android, the shared console UI and webOS need the same treatment once the Apple interaction settles. The shared console UI has no running-game concept in its model at all, so C2/C3 are new surface there rather than a port.
⚠ Unrelated, but it will bite anyone building the Apple client: the `PunktfunkCore.xcframework` checked into working trees is **stale** (its header stops at `punktfunk_connect_ex9`; the sources need `ex10`/`ex11`), so `swift build` fails on any branch until it is rebuilt with `CMAKE_POLICY_VERSION_MINIMUM=3.5 IPHONEOS_DEPLOYMENT_TARGET=17.0 BUILD_IOS=1 BUILD_TVOS=1 bash scripts/build-xcframework.sh`.
From a field report (Discord, 2026-08-16, Windows host 0.29.0 + iPad 0.30.0): quitting a game
mid-stream left the session up and the web console showing it as "running" forever, with no
setting that made any difference.
The console was not merely out of date — it was asserting something the host had no way to know.
A lease with nothing to recognise its game by set its OWN state to `Running`, on the reasoning
that the host had just launched it. That made three different situations indistinguishable: a
game being watched, a game that quit and was never noticed, and a game the host cannot see at
all. `session_on_game_exit` can never fire for a lease nothing is watching, which is why no
setting helped.
Windows reached that state far more easily than Linux, and for a reason worth naming: the host
never holds a `Child` there, and the pid `CreateProcessAsUserW` hands back was logged and
discarded. So a title whose provider published no detect hint had *nothing* identifying it — its
exit went unseen and `POST /game/end` had no pid to signal, while the same title on Linux was
fully tracked through its child.
Host:
* `GameState::Untracked` — a lease that nothing is watching says so, in `/status`, the console
card and the tray label. Keyed on "is anything watching this" rather than on the lease kind, so
a nested gamescope lease (whose exit the capture loop catches) still correctly reads `running`.
* `LeaseRequest::spawned` carries the pid Windows already knew, pinned to its start time by a new
`Scanner::resolve` so a recycled pid cannot impersonate it. It takes the same lifetime rules the
owned child gets, shim reclassification included, and feeds the Windows terminate ladder.
* `game_on_new_launch` (keep|end, default keep): close this client's previous game before starting
a different one. Its own axis rather than a fourth `game_on_session_end` value — wanting a game
to survive a disconnect says nothing about wanting it kept when you deliberately pick another.
Four safety rules, made pure and unit-tested: never another client's game, never one the player
started themselves, never the title being launched, and never a record whose liveness is merely
Unknown.
The same pid fix closes a second defect: a launch that adopted nothing answers `Unknown`, falls
back to the 90-second in-flight window, and past it starts a SECOND copy. That is why "click the
game that is already running" resumed on Linux and relaunched on Windows.
Apple client — the loop the report was really about (browse, play, quit, browse):
* tapping a paired host opens its library; "Stream the Desktop" moves to the card menu
* the catalog is cached per host and rendered immediately, marked stale, so a sleeping host still
shows its titles (only art was cached before; the catalog was fetched live every visit)
* opening the library wakes the host and retries across the boot window, so it is warm by the time
a title is picked — waking was bound to CONNECTING, which is too late to help
* titles already up are badged Resume and sorted first, read from `/status` (already on the
paired-cert lane — no new host API)
* the grid returns to where you were, remembered as the last title opened rather than a pixel
offset, which survives a rotation, a resize and a host gaining titles
Verified: Linux container gate over punktfunk-host — fmt, clippy --all-targets -D warnings, a
plain build and 597 tests green, openapi regenerated and its drift test passing. Web console tsc
clean, 740 messages en+de. Apple swift build + 347 tests on macOS, and iOS + tvOS typechecked
(CI compiles neither).
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.
From a field report (Discord, 2026-08-16 — Windows host 0.29.0 + iPad 0.30.0): quitting a game mid-stream left the session up and the web console showing it as running forever, with no setting that made any difference.
Ten work items came out of that conversation. All of them are here except cross-client parity (C7), deferred.
The bug was not a stale console
A lease with nothing to recognise its game by set its own state to
Running, on the reasoning that the host had just launched it. That collapsed three different situations into one indistinguishable row — a game being watched, a game that quit and was never noticed, and a game the host cannot see at all — andsession_on_game_exitcan never fire for a lease nothing is watching, which is why no setting helped.Windows reached that state far more easily than Linux, for a reason worth naming: the host holds no
Childthere, and the pidCreateProcessAsUserWhands back was logged and discarded. A title whose provider published no detect hint therefore had nothing identifying it, while the same title on Linux was fully tracked through its child.Carrying that pid closes two further defects that were never separately reported:
Liveness::Unknown, falls back to the 90-second in-flight window, and past it starts a second copy — which is why "click the game that is already running" resumed on Linux and relaunched on Windows.Host
GameState::Untracked— a lease nothing is watching says so, in/status, the console card and the tray label. Keyed on is anything watching this rather than on the lease kind, so a nested gamescope lease (whose exit the capture loop catches) still correctly readsrunning.LeaseRequest::spawnedcarries the pid Windows already knew, pinned to its start time by a newScanner::resolveso a recycled pid cannot impersonate it. It takes the same lifetime rules the owned child gets — shim reclassification included — and feeds the Windows terminate ladder.game_on_new_launch(keep|end, defaultkeep) — close this client's previous game before starting a different one. Its own axis rather than a fourthgame_on_session_endvalue: wanting a game to survive a disconnect says nothing about wanting it kept when you deliberately pick another. Four safety rules, made pure and unit-tested — never another client's game, never one the player started themselves, never the title being launched, and never a record whose liveness is merelyUnknown.Apple client — the loop the report was really about
Browse → play → quit → browse:
/status, which was already on the paired-cert lane — no new host APIVerification
punktfunk-host(Linux container)--all-targets -D warnings, plain build, 597 tests greentsc --noEmitclean, 740 messages en + deswift build+ 347 tests on macOS, iOS and tvOS typechecked (CI compiles neither)Two calls worth a second opinion
game_on_new_launchships default-off. The lease's documented posture is that nothing is ended unless the operator asked, and ending a game costs unsaved progress — so I did not reverse that on my own. One line if you wantendas the default./session/settings(admin lane only), so it cannot know whether to warn; an honest prompt needs a launch-intent flag on the wire, and that ABI change collides with the in-flight ex10/ABI-21 work.Not in scope
C7, cross-client parity — Android, the shared console UI and webOS need the same treatment once the Apple interaction settles. The shared console UI has no running-game concept in its model at all, so C2/C3 are new surface there rather than a port.
⚠ Unrelated, but it will bite anyone building the Apple client: the
PunktfunkCore.xcframeworkchecked into working trees is stale (its header stops atpunktfunk_connect_ex9; the sources needex10/ex11), soswift buildfails on any branch until it is rebuilt withCMAKE_POLICY_VERSION_MINIMUM=3.5 IPHONEOS_DEPLOYMENT_TARGET=17.0 BUILD_IOS=1 BUILD_TVOS=1 bash scripts/build-xcframework.sh.