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
native_job_notify turns true only after a successful /api/activity poll, so a
remote engine on an older version leaves job notifications with the page. The
BgStrings parity test reads the Rust file through Vite (no node types in the
client tsconfig); a clippy fix in the tests.
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
- lib/bgWork.ts names the engine jobs the page starts (queue and Upload screen
uploads, installs, File System downloads), tells the host whether the page is
showing, the notification setting and the notification texts in the person's
language, and passes the host's bg-tick / bg-job-finished to the queue.
- lib/wakeableSleep.ts: the upload queue's and install polling's sleeps end on
the host's clock when the hidden window's timers are throttled; a finished job
ends the status-poll sleeps at once. Settle delays still wait their full time.
- Job-outcome notifications are tagged jobOutcome; the OS mirror skips them
when the host reports native_job_notify (false in the browser build: the old
behaviour).
- keepAwakeHold.ts holds the host's background-work hold, refreshed every
minute, while a transfer runs or the upload queue is draining.
- 17 bg_* strings in all 21 languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3cPwhp3UrzayBfyy9PDcN
bg_activity.rs polls the engine's /api/activity every second from Rust, so a
hidden or frozen WebView no longer decides whether work is running or has
ended. A pure step() turns each snapshot into start/update/finished/stop
actions: the first poll only records seq, an engine restart resets it, a
renderer hold refreshed within 5 minutes counts as work, and nothing active for
30 s stops. Finished jobs are emitted to the page (bg-job-finished, plus a
bg-tick heartbeat while work runs) and announced through the OS when the app is
in the background, in the language the page sends.
Android: a connectedDevice foreground service (dataSync as fallback) with a
partial wake lock and a Wi-Fi lock keeps the process and its in-process engine
alive, with an ongoing progress notification and a separate result channel. The
Kotlin class, manifest entries and R8 keep rules are written by
android-postinit-patch.sh. JNI_OnLoad and the context helpers move to
android_jni.rs so both callers share them; app classes load through the
context's class loader. The mobile engine start is now once per process, as
Activity recreation can run setup again.
Desktop: notifications through the notification plugin's Rust API when the
main window is hidden, minimised or unfocused; backgroundThrottling disabled on
the window (macOS 14+).
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
A stream install was declared done when the bytes served added up to the
package size. The PS5 re-reads ranges, so on a 37.6 MB package ten 64 KiB
header re-reads equalled a 640 KiB range it had not fetched yet. The
session was released, the console's request for that range got 410, and
the PS5 download failed after ps5upload reported it installed. Coverage is
now an exact interval set, and delivery completes only when every byte
has been served.
"Nothing answered on the ELF loader port :9021" at install start was the
client's background elfldr swap: :9021 is closed for about a second while
the new elfldr replaces the old. The swap, the installer bring-up and the
payload restore now share a per-console lock, and a refused loader port is
retried for 8 s (no bare pre-probe).
Verified on FW 13.60, Phat and Pro: the engine held until the gap was
fetched, with no 410s; an install started inside a swap waited and
succeeded; an 820 MB package installed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3cPwhp3UrzayBfyy9PDcN
- Receiver writes sce_sys/param.json and param.sfo as .ava-hold and puts them in
place when the job finishes: ShadowMount+ registered a folder whose eboot.bin
was still arriving, failed twice and skipped the title for good (repro on
13.60).
- AVA1 SkipProgress (0x30): a sender reading past data the receiver holds (a
resumed solid .rar/.7z) keeps the job alive; the console ended it as stalled
after 900 s on every retry.
- RAR5 unknown-size entries no longer wrap the unpacked total.
- Upload queue: stalled rows offer Restart transfer, stopped/interrupted rows
offer Resume, "Retry failed uploads" actually runs them and is translated as
an action.
- Payload Manager tab: Update for payloads stored from the Catalog, keeping the
boot-list place; repository installs are left to Payload Manager.
- Files: drag folders (and, without drag-out, files) onto a folder or the
breadcrumb to move them.
- README links the compose file from the feature list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik
Saving settings re-checked every root still existed, so once one was
gone nothing could be saved, removing that root included. Only roots
being added are checked now. A scan skips a root it can't open instead
of failing, keeps its caches, and lists it in missing_roots; Collection
names it and Folders marks it "Not found, skipped".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik
13.60's libSceUserService exports no create call, and the old path
(GetInitialUser + SetUserName) only renamed the initial user; in a
payload GetInitialUser fails with 0x80940004. user.create now answers
with "add the user on the PS5" and touches nothing; Profile says so
instead of showing the button.
Also fix the worker-commit crash tests' race: the sender now waits for
credit and stops once the job stopped or finished on its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik
New helper method net.ports (147, frame 265/266): binds each TCP and UDP
port and closes it at once (never connects, so no loader sees a silent
client), then binds 127.0.0.1 and the LAN address to tell network,
console-only and outgoing sockets apart. The socket sysctls
(net.inet.*.pcblist, kern.file) answer ENOENT on FW 13.60. A full pass
takes ~890 ms on the Phat. The engine names well-known ports and the
Payloads screen lists them, with Open only for web UIs and unknown ports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik
This reverts commit 1f27c1a560.
Every helper build that linked the vendored zlib died within minutes on a
FW 13.60 Phat with SIGBUS in a system library touching helper code at
zlib's inflateEnd (logged by the new fault detail: addr=0x200161159,
rip=0x88007fe60), most often right after an app kill. One such build was
running when the console went down during a game launch and did not come
back. The same helper without zlib ran through repeated launches and kills.
The decompress op goes back out until it can run without linking zlib into
the helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik
"[fatal] signal 10 outside any request" said what died but not where. Faults
(SIGSEGV, SIGBUS, SIGILL) now go through a SA_SIGINFO handler that first lets
the hw guard recover a Sony getter, then writes the faulting address, si_code,
the instruction pointer and where main() loaded, so the instruction can be
found in the ELF. Everything stays async-signal-safe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik
The Files queue row showed speed and time left, but the task the same
transfer drives in the Tasks view had only its bar: the bridge passed bytes
and never the rate the queue and the single-file transfer already compute.
Both now pass the rate and, when the total is known, the time left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik
The updater downloads the new package and opens its folder. On Bazzite and
other rpm-ostree systems that left people to work out `rpm-ostree uninstall`
plus `rpm-ostree install` by hand. A packaged Linux copy now gets the one
command to run, with a Copy button: `rpm-ostree uninstall <pkg> --install
<rpm> && systemctl reboot` on rpm-ostree (the package name read from the rpm
database), `sudo dnf install` for rpm, `sudo apt install` for deb. The README's
Bazzite row says the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU1Z2mdhAxBrH1yb48pxik