Preserve the complete store, artwork, telemetry, input, media and agent route table alongside the newly landed VR utilities and performance HUD.
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Keep the union of server routes, desktop resources and responsive controls. Preserve OpenXR install defaults and telemetry hooks alongside library artwork. Adapt the resource test to single-file entries and avoid a completed-refresh race in the F-Droid test.
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Also from review: a SteamVR build without the timing exports can't break status
(AttributeError), and the device test class runs when the file is run directly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stop always calls systemctl and treats exit 5 (unit already collected)
as done, so there is no is-active/stop race. Cleanup never masks the
copy error, the play ssh timeout covers the remote worst case, and
tests cover stop exit codes and systemd-run stderr reporting.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Stop is a no-op when the collected player unit is already gone
(raw systemctl stop exits 5 on the Frame; verified 2026-09-29).
- Surface systemd-run stderr when the player can't start.
- Keep the copy error if the cleanup ssh also fails; reject upload
names that the play path can never accept.
- Allow 60 s for play (ffprobe 30 s + systemd-run 15 s remote).
- Docs: four-hour cap is unconditional; no delete action yet; fix a
garbled timing sentence.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
HTTPServer.server_bind calls socket.getfqdn, which stalled past the MCP
backend's 10-second startup window on GitHub's macOS runners.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The live API rejects mimes=image/jpeg on /logos and /icons, so every logo and
icon lookup fell back to generated art. Found with a real key: SuperTux now
gets grid, wide, hero and logo; Beat Saber all five.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Title removal ran the Steam shortcut tidy-up before steamos-delete, which
finds the Proton prefix through that shortcut, so compatdata was left behind
(e2e caught it). steamos-delete runs first again; art/collection tidy-up after.
- Test fixtures are byte-exact: never convert line endings (a text-looking
fixture APK got CRLF on Windows and failed its SHA-256).
- Read index.html/artwork-settings.js as UTF-8 in tests; app-data backup and
OBB shell tests run only on POSIX (they exercise the Frame-side scripts).
- e2e expects the icon under artwork/ now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Final review follow-up. A failed Thread.start() leaked a resolver slot (four
failures disabled artwork lookups). GIF graphic-control blocks must have the
fixed 4-byte payload (otherwise dropped) and an image with no pixel data is
rejected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The handshake runs after the watchdog can reach the TLS socket, with the
remaining time as timeout, and the watchdog shuts the socket with the plain
socket method. At most four lookups that outlived their deadline may run; more
fail at once with a clear error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The screen and first frame must be at most 4096x4096 and the frame inside the
screen; anything malformed or truncated is rejected. Only a minimal
single-frame GIF (header, screen, colour table, graphic control, first
image) reaches the Frame's Chromium, however many frames the source has.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Matching the APK path in command lines could hit an unrelated process, and
the pgid file had a registration race. The launcher keeps the flock (not
inherited by Lepton) and, holding it, stops only lepton-steamlaunch-<instance>,
whose name is this app's alone. A Lepton host process may linger briefly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Automatic backfill touches only entries marked art_pending at install (a
devkit title Steam registered later) and fills only slots Steam has no art
for: no name, exe, VR flag or icon changes, no clearing. Older installs
without the flag are left alone and refreshed only when the user asks.
- Android remove takes the install lock that install and refresh hold, so a
refresh in progress can't recreate a removed app; a refresh after removal
finds it not installed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Name resolution runs in a thread within the budget, a watchdog shuts the
socket at the deadline, and the body is read one receive at a time with the
remaining time as timeout. SteamGridDB goes through the same bounded fetch,
without redirects.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The lock isn't inherited by Lepton, so a launcher killed before Lepton made its
container left an untracked Lepton that a new Play could overlap. The launcher
records its child's process group and, once it holds the lock, ends a recorded
group that is still running this app.apk (never an unrelated reused id).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GitHub's opengraph preview is repo text, stats and an identicon; as a Steam hero
it looked broken. GitHub entries no longer default to it (the store draws its
fallback, Steam gets generated art). Source GIFs (common gameplay captures) are
accepted; the Frame's Chromium draws the first frame. Open Saber Plus uses its
gameplay GIF as banner. Verified on the Frame: hero/wide now show gameplay.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- 'Refresh artwork' (settings) and the API's refresh-art --all cover devkit
titles as well as Android apps; frame_titles.py gains refresh-art ID|--all.
- Apps and titles without complete Steam artwork are flagged (art_missing):
the app shows 'Add artwork', and the CLIs' list prints the refresh command.
- When the app lists them and Steam answers, Frame Control re-applies their
art in the background (at most every five minutes), e.g. for a title Steam
registered after an install made while it wasn't running.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Remove: collections and artwork clearing are best effort in Steam's JS, and
the host carries on to delete the files when Steam isn't running (Android
apps and devkit titles).
- Devkit titles: the shortcut is found by devkit id, the saved id, or an
executable/start folder inside the title's folder; never by display name.
Steam's overviews don't carry devkit_gameid (checked on the Frame
2026-09-28), so 'list' now reads exe/start dir from app details.
- Photo-based grid/wide/hero slots render as JPEG (a noise-heavy 3840x1240
hero was over 12 MiB as PNG on the Frame); the logo stays transparent PNG.
Rendering gets 75 s and retries once with generated art. Each slot is
cleared before it is set, since Steam keeps .png and .jpg side by side.
- Devkit titles keep their own VR flag (vr=None skips SetShortcutIsVR) and
their Sideloaded collection.
- A failed title install's cleanup can't replace the original error.
- refresh_art for devkit titles (frame_titles.refresh_art).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Any failure of a source image or SteamGridDB (HTTPException, odd JSON) now
becomes a warning and generated art, never an aborted install; refresh-art
--all reports each app and carries on.
- URL artwork goes through apk_sources._images: public addresses only, at most
three redirects, and one overall deadline for all of an install's fetches.
- PNGs are checked from their header only (any depth or interlace; Steam's
Chromium decodes them), JPEGs may have trailing padding, and 4K screenshots
are within limits. The slow pure-Python decoder is gone.
- SteamGridDB title matching keeps letters of every script and never matches
on an empty name. One warning per source slot, not per candidate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Once flock is held no launcher owns a running container (a SIGKILLed launcher
left it), so it is stopped and the launch continues. fd 9 is closed for the
Lepton child so it can't keep the lock held.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The locked publication step also refuses a v1 index once v2 was accepted, so
an overlapping v1 fallback can't replace a v2 cache at an equal timestamp.
- A cached APK is touched before hashing; if it vanishes, it's downloaded again.
- Only the app prunes (at start and after store downloads), since claims are
in-process; the CLIs never prune.
- The CLI joins background refreshes on error exits too.
- The Windows lock loop retries only contention errors.
The concurrent-publication test now uses real flock contention.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolve CLI usage and POST table conflicts. Store installs now hand the
source's own image URLs (icon, banner, screenshots) to frame_android.install
as Steam artwork; before, they passed UI proxy paths (or nothing), so every
store install fell back to generated art.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two clients restoring the same package could each swap directories and then
delete the other's pre-restore copy. The swap and retention cleanup now run
under flock on .<package>.restore.lock.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deletion re-stats under a lock shared with touch() (F-Droid cache reuse) and
claim()/release() (held by the store around install), so a reused or
installing APK is never removed from an out-of-date scan.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A query arriving between the queue handover and the completion event could be
queued with nobody to start it, leaving the source 'loading' forever.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The final timestamp recheck, cache write and state update now run under a file
lock (flock, or msvcrt on Windows), so the CLI and the app can't publish
indexes out of order. The CLI joins background refreshes before exiting so an
expired index doesn't stay expired.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Backups no longer abort on a symlink: it is left out and listed (path and
target) in manifest.json, now written last. A hard link is stored as a copy of
its file. Restore removes older pre-restore copies of the same package.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
install_obb needs the app's running instance, which doesn't exist straight after
install, so the store no longer calls it there. The install result says the app
needs its game data; after opening the app once, 'Add game data' copies the
downloaded OBB files (a background job).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adding a repository downloads and verifies its whole index, so it now runs as a
job (runJob in the UI) and reports 'Trusted on first use: <fingerprint>' when
no pin was given. fdroidrepos:// links pass the server check, as documented.
Jobs report SourceError messages without a 'SourceError:' prefix.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Runs after each APK download and at server start. APKs used in the last hour
are kept; an F-Droid cache hit refreshes the APK's mtime.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Searches no longer wait for (or fail on) a refresh of an expired index; the
store notes which sources show saved listings. A failed refresh keeps the old
index and is retried after 10 minutes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A host that answers 403/429 is left alone until its Retry-After (or GitHub's
rate-limit reset; default 10 minutes). Meanwhile cached data is served, or the
source reports 'limited' with its own name, e.g. 'GitHub is limiting requests;
try again in 10 minutes'. Covers _web reads/downloads and F-Droid fetches.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each repository's newest accepted index timestamp is stored and older indexes
are refused. index-v1.jar is only a fallback while no v2 index has been
accepted. entry.jar must use SHA-2; the recorded IzzyOnDroid entry.jar is
SHA-256 and still verifies. Tests sign JARs with a throwaway key.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A search for a different query while a source is busy now queues (newest wins)
instead of being dropped, and warm() uses the browse limit so the first browse
reuses it. set_enabled calls the source before taking search._lock. A
SourceLimited error reports the source as 'limited'.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- F-Droid: percent-encode repo file names (a '#' in one screenshot name broke
the whole main repo); a bad image name drops that image, not the app.
- Search: sources still fetching report 'loading' (UI says so and refreshes
quietly); indexes warm up at server start; page-only SideQuest is not
searched and appears as a 'Browse SideQuest' link instead of an error.
- Browse (empty query) ranks VR, artwork and recent updates first; the F-Droid
archive is off by default (old versions only).
- Throttled sources fall back to their last cached copy; per-host message.
- Curated GitHub list gains Open Saber Plus (MIT) with icon and screenshots.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Any unexpected error while launching clears the launch and reports it, so
the pad can always be started again; the temporary stderr file is made
inside the handled path and a failure reading it is tolerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
discard() swallows OSError as well as Failure, so a launch that fails and
can't remove its copy still reports the error and can be started again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The agent holds its copy from its first status line on and tidies it up
however it ends. Before that (a launch error, or stopped before the agent
ran) the server removes the copy itself. discard() only ever removes
incoming copies, never the iPhone bundle's own. The retry race test waits
for both contenders' decisions instead of sleeping.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A start turned off while copying removes the copy instead of starting an
agent that would be killed before it could tidy up.
- A local read error while copying removes the partial copy too.
- The retry race test holds the new start until the retry has decided, so
it fails every time without the fix (checked 3/3), not by luck.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review round 12: a failed USB-C tunnel now retries the normal path; an
existing HostKeyAlias wins; --host with --usb is rejected; the Steam
desktop-streaming claim is now 'untested' (Valve documents the desktop
showing when a game loses focus); the Show/cleanup overlap test blocks
for real (it fails without the lock). Verified live: with the cable out,
the route is the normal path.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Each start keeps its copy (holding a lock on it) until it ends, however
it ends, then removes it; a restart can still unpack it.
- The server removes a partial copy when copying fails.
- Other copies are removed only when unlocked and over an hour old.
- Tests: a start racing the need-packages retry (one agent, not two), a
restart that must unpack its copy, a held copy surviving the sweep.
Both regression tests fail without their fix.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>