- A durable-by-log job is not ended for the table until it has settled
(no pack segments, or `settled`): between unswept_n reaching 0 and the
settle, its last Status{unswept=0} and the segments' removal are still to
come, and an eviction or the 10 s reap there left the sender's settle wait
to time out and the segments behind. C test for that window.
- create() checks the retiring ids first and evicts only when the create it
makes room for will go through (the id is not retiring, and the retiring
list has room for the victim's id too), so no victim is destroyed for
nothing. The retiring list holds 2 x AVA1_MAX_JOBS ids, which makes its
"full is unreachable" comment true with evictions.
- The victim is destroyed on a thread of its own (ava1_job_put_nowait), off
the JobOpen path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
The Files screen uploads a pick of files one AVA1 job per file, all on the
engine's one shared session. The console's job table holds 32 jobs, and an
upload that ended stayed listed (with its job thread and workers) until its
session ended: only parked jobs were ever reaped, and a session is parked only
when it ends. So the 33rd file of a pick got ERR_BUSY "too many jobs, or this
job is still closing" for 12 retries and failed, and every later file did the
same until the helper was replaced (a new session). Reproduced on the Phat
(FW 13.60): file 24 of the Switch-like set failed busy, and the table never
drained while the session lived.
- A finished upload or download (not a copy, op or console-to-console send)
stamps concluded_ms once its JobDone is out (or the peer's came in).
- The reaper lets such a job go 10 s after that, attached or not, unless it
still has files settling (unswept), as SPEC §11.7 intended.
- A JobOpen that finds the table full takes the slot of the concluded
transfer that ended longest ago and nobody else holds, instead of BUSY.
Tests: C unit test of the eviction and reap rules (wire_upload), and a host
test of 40 single-file jobs in a row on one session (failed at file 32 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
- HearBridge discovery has an 8 s overall deadline, tries the remembered
port first and stops after the first connect timeout; reads can't
outlast the deadline.
- The HearBridge routes refuse a console address with whitespace,
control characters or URL syntax before any request; routes are
tested at the axum level (unknown action and bad host are 400s).
- HearBridge is read every 15 s while its card is collapsed, 5 s with
Manage open; the User-Agent comment now says what it does and doesn't
avoid. A failed scan no longer waits for results.
- Marker checks wait for the catalogue and stand aside during transfers.
- HearBridge's card has its own loading line; summaries re-read after
an Update.
- Removed the seven translations the old tabs used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
HearBridge (Bluetooth headset audio) gets an integration card on On your
PS5. The engine talks to it for the app (its page sends no CORS
headers): new /api/ps5/hearbridge/{status,saved,devices,action} routes
find it on 8090..8095, read its status with the token stripped, and
pass only volume, mute, night mode, latency, reconnect, scan, select
and stop, each checked against the ranges HearBridge takes. The token
is read fresh per change and never leaves the engine.
The card shows the stream state, headset, battery, link quality,
delay, codec, volume and game profile; Manage has the volume and
latency sliders, mute, night mode, reconnect, scan and connect, saved
headsets, recent events and a link to HearBridge's own page for
pairing, EQ and backups. Stop uses HearBridge's clean stop. An absent
or older HearBridge shows "HearBridge didn't answer" with the page
link. A card opened by a link now scrolls into view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
The weekly ASan soak failed once at round 582 with 14 -> 16 descriptors
after 581 flat rounds: a connection teardown still finishing after the
fixed 300 ms settle. Poll for up to 10 s like the job and pack checks;
a real leak never settles back and still fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
Helper: files.prefs_get/prefs_set (148/149) reach files_prefs.c, the same
file the console page's /api/files/prefs uses; files.handoff_put/take
(150/151) keep one Show on TV / Show on PC slot per direction in helper
memory, gone after two minutes, emptied by a take (files_handoff.c, host
self-test). The page gets /api/files/handoff/take {for:"console"} and
/api/files/handoff/put {to:"pc"}; /api/jobs adds "uploading", the roots
of up to 8 running uploads, for the Files badge.
Engine: GET /api/ps5/files/prefs, POST .../prefs/set, .../handoff/put,
.../handoff/take. An older helper answers unknown method, reported as
{"supported":false} so the PC app hides the features.
console files: shared favourites and Show on TV / Show on PC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
- Registered-only titles are added only when app.db could not be read in
full; the helper's registered list is every /user/app folder, so with a
complete read a folder app.db does not list is a leftover, not a game.
NPXS ids are excluded, and a candidate needs a mount.lnk source or a
param.json/param.sfo in its sce_sys.
- app.db pages carry the table's row count (SQL reader only); the engine
marks the list truncated when it changes between pages.
- A SQL walk that ends on anything but SQLITE_DONE reports truncated.
- FAQ: the phone guide is phrased as a question; references updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
APP_DB_QUERY answered at most 256 rows, and fewer when its 64 KiB reply
filled, with nothing saying the list was cut. A title installed by a
homebrew installer (no /user/appmeta folder) that sorted past that point
disappeared from the installed list.
Payload: app.db.query takes an optional {"offset","limit"} window and
answers offset/next/more/truncated; a full buffer now ends the page with
more=true instead of closing the list. An empty request is page one, so
older engines keep working.
Engine: appdb_query follows the pages (at most 32) and marks the list
truncated when it could not finish (or an older helper returned a full
256). The installed list reads app.db once per request, adds registered
titles whose app folder exists, reports incomplete, and a truncated list
never proves a title absent for install verification.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
ava1_jobs_release_seen(me): every finished operation or copy owned by
the console page keeps only the grace (AVA1_OP_GRACE_MS) after the page
first read it finished, instead of a copy holding one of the 32 shared
job slots for the full 10-minute park age. Operations reuse
ava1_op_status_delivered; copies are restamped and the reaper keeps a
delivered copy only for the grace. A paired PC's jobs are never touched.
ava1-ctest: the 32-job cap with list_all (full table refuses a 33rd for
the page and a PC; releasing the page's own makes room, PC jobs stay)
and the grace (page jobs gone after ~10 s, PC op and copy still listed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
Some dumps ship Media/Modules/Il2cppUserAssemblies.prx while the eboot loads
%s/Modules/Il2CppUserAssemblies.prx. exFAT and PFS ignore case, so the game
plays from an image; /data and UFS2 do not, and the game crashed (SIGSEGV,
RIP 0) a second after starting as a folder.
Folder uploads and Convert's game-folder and .ffpkg outputs now rename such
files to the eboot's spelling (only path-shaped references in the eboot count);
the source is left alone, and Convert's checks list the renames.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
When the helper's port and the loader both refuse, or the session connect
gets no TCP answer, require_in asks DDP (2 SRCH within ~800 ms). A 620
reply is the new token console_rest_mode ("The PS5 is in rest mode. Wake
it, and the helper can be sent again."). No reply after a timeout stays
ava1_unreachable with a plainer message. DDP 200 or silence after a
refusal keeps helper_not_ava1 as before. A verdict DDP explained is
cached with the pool refusal for 12 s (PROBED_REFUSAL_TTL, past the
client's 10 s poll); other refusals keep their 5 s / 30 s TTLs. The probe
is injectable (Pool::with_power_probe) and skips loopback hosts.
GET /api/ps5/status failures answer {error, reason, power}. Upload,
download and relay stalls on a console DDP calls asleep fail with
console_rest_mode instead of the bare "no durable progress".
payload_check (Tauri and the browser twin) passes reason and power
through; payloadCheck returns them as optional fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
Engine: POST /api/ps5/helper/stop { addr } sends node.shutdown over the
paired AVA1 session (payload_lifecycle::shutdown_running_payload) and waits
for the helper's port to go quiet before answering {state: "stopped"};
{state: "not_running"} when nothing answered. Refused with 409
{reason: "busy"} while this engine runs a transfer, download, relay,
console file operation or install against that console (a per-console busy
count held by each job, plus the install slot); 502 {reason: "refused" |
"still_running"} otherwise.
Client: "Stop helper" beside Send/Resend on the Connection screen, behind a
confirm. A stop marks the console "stopped by you" (persisted in
localStorage, so desktop and browser builds keep it across restarts). The
reconnect loop, a queue's mid-run recovery and unattended queue runs (wake
resume, re-attach after restart) leave a marked console alone. Send helper,
Check, the setup wizard, Quick bring-up and starting an upload clear the
mark, as does a helper seen answering a minute after the stop. Connection
and the screen gate show "Stopped" rather than a missing helper. Strings in
all 21 locales.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
Engine: POST /api/folder/build unpacks a game image (or, once the pkg
reader lands, a debug-key .pkg) into <out>/<name>. It runs as a job with
Write and Verify stages; every path is checked before anything is written
(.., absolute, empty components, names this OS cannot hold, file/folder
clashes, case clashes on a case-insensitive drive); data streams in 8 MiB
chunks with cancel per chunk; the folder is written as <name>.partial/
(marked so the output-folder sweep can tell it from a user's own) and
renamed only after the read-back: every size and empty folder, and the
SHA-256 of a seeded sample (or every file) re-read from a freshly opened
source. An existing target, a plain folder source and a shortfall of
free space (files + 64 MiB) are refused before the job starts.
Client: a third "What to make" tile, A game folder. A folder source
disables Make with "This is already a folder"; .pkg is accepted as a
source; then is keep only; the finished card shows the folder and Show in
folder. The queue, prefs, pipeline and stage list know the folder kind.
Strings in all 21 locales.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
A fake package built with another passcode is refused the same way as a retail one, so the
message says both. tree_extract writes any source tree out as a folder, for checking real
packages and images by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
pkg_source opens a package built with the debug key as a SourceTree,
registered in source::open (and saved servers) for the pkg extension. It
streams: outer blocks are decrypted on demand and checked against the
container's digests, the inner image is read through its layout
descriptor (the Kraken/stored block layout and the flat layout with
stored or zlib metadata), and only the metadata region is expanded
whole. A small LRU keeps sequential reads from decoding a block twice.
The artwork the container carries in the image's place is restored
under sce_sys/.
Retail packages, native packages made with another passcode, PS4
packages, unknown layouts, truncated and damaged packages are refused
with the reason in plain words.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
lzma-rust2's range coder reads a failed byte as 1, so any read error on the archive file
surfaces as "dist overflow", the same as a damaged stream. Since 7.0.22 that is classed as
corruption and never retried, so a read that failed once (a phone's storage under load) ended
a 70 GB upload as "the archive is damaged". The archive file is now read through a tap that
keeps the first real read error, and when the decoder then reports corruption, that read
error is what the job sees: an ordinary I/O error the upload retries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3cPwhp3UrzayBfyy9PDcN
The C interop tests build fs_jobs.c, which registers the unpack op; its LVD mount has no
host counterpart, so the host gets a refusal. Its .part -> image rename is a sibling
rename and goes in cross_device.h's audit table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3cPwhp3UrzayBfyy9PDcN
Pausing a routed send now ends both consoles' halves, so its journal is free
to resume from any device with any helper. The driving engine keeps a shared
record of each send on the receiving console (/data/ps5upload/c2c/<tx>.json)
with a 45 s lease refreshed every 15 s; another device lists those sends
(GET /api/transfer/ps5-to-ps5/remote), takes one over or watches a direct one
(POST /api/transfer/ps5-to-ps5 with adopt: take | watch, same tx_id), and the
engine that had it stops driving without ending anything ("taken over by").
Helper: a new caps bit, CAP_SHARED_JOBS (4). job.status / job.cancel answer
any paired device for uploads and downloads (operations and local copies stay
owner-only), and a JobOpen moves a parked upload of another device to the
opener; an attached one is still refused. SPEC §18.5-18.6.
Test connection in Send to another console runs the console's net.reach probe
from the sending console to the dial target (plus the relay legs, and an
optional real 4 KiB direct send). Tasks shows "Sends from other devices" with
Watch / Take over / Resume here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3cPwhp3UrzayBfyy9PDcN
Files gets "Decompress to .exfat…" on a .ffpfsc again. The first build (pulled
before 7.0.23) inflated the blocks in the helper with vendored zlib, and the helper
died with SIGBUS within minutes of loading it; once the console froze. This one has
no decoder: the helper mounts the container read-only the way ShadowMount+ mounts a
.ffpfsc (LVD image type 9, read-only single-save flags, 4096-byte sectors, pfs with
mkeymode=AC), so the kernel decodes it, and copies the file inside to <name>.part,
then renames it in the same folder. The kernel lists that file rounded up to whole
64 KiB blocks; only the inode's real length is copied.
The checks from the first build stay: the container's block table is read before the
kernel sees it, a destination that exists and a drive without room for the image plus
1 GiB are refused, the size and exFAT boot sector are checked before the rename, and
Stop removes the .part and the mount. One unpack runs at a time. The mount is not in
the mount table the helper sees, so it is recorded on the /mnt tmpfs (owner pid, LVD
unit, .part) and a helper that starts after one died mid-copy releases it; a record of
a live instance is left alone.
Measured on the FW 13.60 Phat: a 315 MB image (127 MB container) in 3.5 s and a 4.5 GB
one (3.0 GB container) in 65 s, both byte-identical to the source; Stop, a takeover
mid-copy and a stale record all left no mount, LVD unit or .part behind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3cPwhp3UrzayBfyy9PDcN
A process-wide hub fed from set_job's terminal write and the unified install's
finish path: running transfers and installs with their progress, and a ring of
the last 64 endings with a monotonic seq so the app's background watcher sees
each one once. POST /api/activity/label lets the client name a job.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3cPwhp3UrzayBfyy9PDcN
The management path dropped the job path's specific message (helper port closed with the
loader up, or no loader at all, or a helper too old: #447) and always said "not running or
is an old version". Status, process list and every other management call now carry it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3cPwhp3UrzayBfyy9PDcN