Files

34 KiB
Raw Permalink Blame History

P2 Steam React framework: evidence log

Status

Review R1: all findings fixed and verified live (session 3, 2026-10-07 12:30–14:05 UTC). Milestones: M1 reached, M3 reached, card complete (M4 does not apply to P2; M5 is V2's). The nine findings of P2-review-R1.md are fixed in device/rt/02-react.js (rt.react.version 3) and device/rt/03-react-lab.js; contract contracts/react.md is v3 (§11 lists the behaviour changes). The R1 regression pass found and fixed one more defect: after ui.modal closed, Steam left focus parked (vr-null-tree), so RX-3's "focus back on the same poster" failed; ui.modal now gives focus back (D-P2-7). Final pass on the final 02-react.js: 22 results, all PASS (RX-1 … RX-7, RX-OV, RX-HEAL, RX-FN, RX-REMOVE, RX-TARGETS, and the new RX-OPT, RX-RETRY, RX-FAIL, RX-OPEN; RX-1, RX-3 and RX-7 also under --mode laser and --mode pad with P3's wp.p3), JSON in docs/phase2/wp/P2-cmp.json; shot shots/p2_p2_failroute.png. One RX-OPT run (13:46) failed because its timers never fired and did not reproduce (see the evidence row). Nothing is blocked.

Open: REQ P2->Coordinator, P2->V1, P2->V2 (release hand-off of actionsLive, finding m4) and P2->P1 (prune fired scoped timers from the cleanup list; minor); all under Requests. Done by P1 this session: a module whose install() failed with a transient ready() error is re-installed (up to 4 times). Answered this session: REQ C2a->P2 #11. How to re-run any test: python glass.py js --flags reactLab "__LGS_RT.react.lab.run('RX-OPT', L)" (one locked step per test; RX-5, RX-6, RX-REMOVE, RX-OPT and RX-RETRY refuse while another module has live T3 registrations).

M1 reached (2026-10-07). Contract docs/phase2/contracts/react.md written.

Session 2 (2026-10-07, 09:00–11:00 UTC; times in this log are UTC, as the lab's JSON prints them). device/rt/02-react.js and device/rt/03-react-lab.js are complete and tested live on build 11094443. Every card test passes: RX-1 to RX-7 (RX-1 and RX-3 also under --mode laser and --mode pad), plus five lab tests for builds the RX list does not cover: RX-OV (overrides), RX-HEAL (self-healing), RX-FN (bare-function patch on PagedSettings), RX-TARGETS (statusPill, appButtons), RX-REMOVE (the whole removal). Contract contracts/react.md is v2 (changes in its §11). REQ P1->P2 (flag react) is done.

M3 reached (2026-10-07): runtime code complete behind its flag (react is the install gate and kill switch, verified), and the package's own tests pass in both input modes. M4 does not apply to P2 (it has no layers, plates or popups). M5 is V2's. Card complete: every Build is in, every acceptance test passes; nothing is blocked.

How to re-run: python glass.py js --flags reactLab "__LGS_RT.react.lab.run('RX-1', L)" (one test per locked step; RX-5, RX-6 and RX-REMOVE refuse while another module has live T3 patches). Another agent's runtime reload during a step removes react-lab mid-test (Cannot read properties of null): just run it again.

Open: REQ P2->P1 (the class index is cached even when it was built too early after a renderer restart). REQ P2->P10 answered. device/proto/react_proto.js stays until M5 (PLAN §6).

Review R1 (P2-review-R1.md): what was fixed

All nine findings were accepted (none was wrong) and fixed in session 3 (2026-10-07, 12:30–14:00 UTC). Tests are in the evidence table below (rows marked R1); their JSON is in docs/phase2/wp/P2-cmp.json.

# Finding Fix Test
M1 A on a DialogButton / MenuItem counted as a lab click, so T3 actions never ran from the gamepad untrustedPointer() exempts Steam's own programmatic click (untrusted PointerEvent click, pointerType '', pointerId -1/0/undefined): what HTMLElement.click() dispatches for A. Lab clicks are MouseEvents and stay synthetic; locked steps stay in test mode through P10's action logger. Rule in react.md §8 reason 3, plus a "Pass the event" paragraph RX-7 gamepad phase: A on a DialogButton, A on a ui.menu item (both: no synthetic pointer event, logged only because test mode is on), a lab MouseEvent click on the same button (still synthetic), spies 0 calls
M2 An optional patch.byProps never attached when it was the module's only registration syncWatchers() counts pending handles; a bounded late-attach series (scoped rt.setTimeout at 120, 400, 1000, 2500 ms) after each navigation, window add/show and the registration; cancelled when nothing is pending and on removal. status().watching, status().lateTimers, patch.list()[].late. Cost: one fiber walk per attempt, 0.1–1.2 ms (3694 fibers, measured 12:50) RX-OPT: appButtons registered on Home as the only registration attaches on the game page ("navigation +120 ms"), pagedSettings on entering Settings; watchers off after removal
M3 Error page collapsed to the window's left edge on a real route Root width: 100%, box-sizing: border-box, 48 px side padding, text-align: center RX-FAIL on /library/lgs/lab/raw (a real routes.add page, no lab wrapper): root 1280 wide, title and Back centres at x 640 (= window centre), title x 439 ≥ 48; shot shots/p2_p2_failroute.png
m4 actionsLive had no hand-off to V1 or the release checklist REQs to the coordinator (S-row, §4.6 item), V1 (defaults.json at release) and V2 (shipped-state check) under Requests — (requests)
m5 appButtons stopped matching once patched util.fnSrc reads through our trampolines (__lgsOrig, also for a class prototype.render) RX-OPT: a second layer on appButtons matches (1 fwd), both layers run after a remount
m6 lab.open() never reported an error The fixture's ErrorBoundary (new onError prop, keyed per open) records the error; open() returns {mounted: false, error}; new fixture boom RX-OPEN
m7 A failed ready() stayed sticky for transient causes Missing required finders stay sticky; "route switch not found", "no React fiber" and webpack not loaded are transient (retryAfterMs, attempt), retried by the next ready() after 1, 2, 4, 8, 16, then 30 s, reusing the finder result. REQ P2->P1 to retry consumers' failed installs RX-RETRY (test hook test.noSwitch)
m8 Error page title off the type ramp, inline literals Title = Title 2 (--lgs-type-title2, 700 30 px), colour --lgs-text-1, Back font --lgs-type-headline, height --lgs-btn, all with the token values as fallbacks for the theme-off state RX-FAIL: title 30 px / 700; Back 240 × 60, radius 30 px, 24 px / 600, outline none, border 0
m9 Contract and evidence nits rt.react.version = 3 (contract v3, §11); §9 lists the runners correctly; JSON now kept in P2-cmp.json; the "both input modes" row corrected (below) —

| + | Found by the R1 regression pass, not in the review: RX-3 failed 3/3 (12:58, 13:06, 13:13): after the modal closed, Steam parked focus in vr-null-tree and FocusApplicationRoot() brought the page back with no focused node, so the poster lost focus | ui.modal gives focus back (D-P2-7): on Steam's closeModal, the page tree's Activate() + TakeFocus() | RX-3 now also requires focus back by itself within 800 ms (no FocusApplicationRoot() fallback), and starts on a poster | | + | RX-OV measured its "before" count during Steam's route transition (1645 = Home 802 + AllGames 843, 13:01) | Runner waits for a stable node count (600 ms) before each count | RX-OV | | + | The route switch logged "our children failed … reading 'children'" 4 times during RX-OV (13:01, caught: Steam's page rendered). Not seen again: RX-OV at 13:43 and at 14:01, whose step also read the react error log (only the fixture's forced render error) | switchRender hands a call without a props object straight to Steam's switch, and the log now keeps the stack | RX-OV (no such log entry since) |

Also done this session: REQ C2a->P2 #11 (ui.loc fills %N$s).

Contract changes

  • v1 (M1): first version.
  • v2 (2026-10-07, session 2): additions only, listed in contracts/react.md §11. Two clarifications a caller may notice: ui.modal/ui.confirm return a Promise of the instance (v1 said "the instance"); routes.override's steamChildren is null for routes Steam declares without children (/chat). New rule 9: a patch.byProps wrap must not call hooks.
  • v3 (2026-10-07, session 3, review R1): see react.md §11. Behaviour changes a caller may notice: Steam's programmatic click (A on a DialogButton or MenuItem) is no longer a synthetic event, so actions run from the gamepad once actionsLive is on; optional patches keep looking when they are a module's only registration; transient ready() failures are retried after a backoff; ui.loc fills %N$s; util.fnSrc reads through our trampolines. rt.react.version is now 3.

Evidence

Rows before review R1 (sessions 1 and 2) say "JSON": that JSON was printed by the step and not kept (PLAN §7.7 asks for it); the R1 pass below re-ran every test on the final code and keeps its JSON in docs/phase2/wp/P2-cmp.json (key = test id, .laser/.pad for --mode, .p3 with wp.p3).

Test Command Result Shot / JSON Date Steam build
RX-6 python glass.py js --flags reactLab "...use('react-lab').run('RX-6')" PASS: the one scan (2618 factories, 24 finders, each with exactly 1 candidate) 92, 79, 76 ms (max 92 <= 250); a second ready() 0 ms; install() itself 0.1 ms (lazy) JSON in this log 2026-10-07 09:10 11094443
RX-5 same, run('RX-5') PASS: with Focusable broken by test.breakFinder, routes.add throws cannot find Focusable (fail closed; T3 off); ready() stays failed; nothing patched (patchedLeft 0, no route); after reset() ready again in 84 ms JSON 2026-10-07 09:10 11094443
RX-4 same, run('RX-4') PASS: Steam's ConfirmModal (no-op props) rendered on /library/lgs/lab (15 nodes), unmounted, route left, no route registered JSON 2026-10-07 09:11 11094443
RX-1 same, run('RX-1') (SR §6 selftest, ported, 2 runs) PASS: 19/19, 19/19; route, D-pad grid moves (11), laser click, A, ≡ opens Steam's context menu, B closes it, B leaves; patchedLeft 0, no page or route left, patchedLive 0 JSON 2026-10-07 09:12 11094443
RX-7 same, run('RX-7') PASS 16/16: test mode on (logger, actions.test, actionsLive unset); identities: launchNonSteam = SteamClient.Apps.LaunchNonSteamApp, the "+" program row (module 34493) calls it with strCmdline; the "+" window row calls module 92102's p.SteamVR.DashboardDesktopWindowClicked = ours; primary uses the tile menu's own exports (43394 AH primary action, jy run) called by GetPrimaryActionMenuItem; navigate = the main instance's Navigate. Each action logged, never run (spies on LaunchNonSteamApp, RunGame, StreamGame, OpenInstallWizard, DashboardDesktopWindowClicked, Navigate: 0 calls); an untrusted click is test mode by itself; an unknown app is refused; route unchanged; the runtime action log has every call JSON 2026-10-07 09:12 11094443
RX-2 same, run('RX-2', L) PASS 6/6: plusButton matches 1 component (the bar "+" button, a mobx observer memo, tag 15); patch.rerender forced it at once through the observer's own path (1 wrapper call); the "+" popup opened with the OPEN recipe is unchanged against unpatched (L.snap/L.diff: 0 issues, 0 moved, 51/51 nodes) and so is the bar (13/13); patchedLeft 0 after removal JSON 2026-10-07 09:20 11094443
RX-3 same, run('RX-3', L) PASS 8/8: ui.modal over /library/tab/AllGames: modal in main, route unchanged, the library root is the same node, node count 843 -> 841 only because Steam's focus-dependent %{FastScrollOverlay} (2 nodes) leaves while focus is in the modal (back to 843 after); .gpfocus in the modal after FocusApplicationRoot(); B calls our onCancel and closes it; focus back on the same poster (app 1147940, read from fiber props); route restored JSON 2026-10-07 09:30 11094443
RX-OV (extra) same, run('RX-OV', L) PASS 8/8: override of /library/home renders with the real match and keeps Steam's Home inside (761 nodes); removal brings Steam's Home back (824 -> 802 nodes, within 5 %); a throwing override shows Steam's Home (ErrorBoundary fallback = steamChildren); the achievements route (function children) gets our wrapper and Steam's children get the real route props (appid); an undeclared path throws; patchedLeft 0 JSON 2026-10-07 09:30 11094443
RX-HEAL (extra) same, run('RX-HEAL') PASS 3/3: with the switch patch dropped as on a remount (test.dropSwitchPatch: 2 fiber types), navigating into our path re-patched it before render and our page showed (patchedLive 1); patchedLeft 0 JSON 2026-10-07 09:31 11094443
RX-REMOVE (extra) same, run('RX-REMOVE') PASS 6/6: a route, an override, the memo patch and the PagedSettings bare-function patch live (element factories hooked), then the module's own removal (test.cycle(), what lgs off runs): patchedLeft 0, restored 4, Steam's jsx and createElement are the originals, our route left (-> /library/home), a stale /library/lgs/… entry falls back to Steam's library, state fresh (unscanned) JSON 2026-10-07 09:31 11094443
RX-FN (extra) same, run('RX-FN', L) PASS 5/5 (09:42, twice; 10:10 in the final pass): PagedSettings (pagedSettings) matches 1 bare function component (kind fn); after leaving Settings and entering again the patched render runs (element substitution); /settings/system unchanged against unpatched (0 issues, 0 moved, 219/219 nodes; when gamepad focus settles on different elements in the two shots the footer legend is left out, noted in the result); removed while mounted: patchedLeft 0; after removal the wrap never runs again. The first run at 09:31 failed only on the footer legend (entry path differed): fixed in the test JSON 2026-10-07 10:10 11094443
RX-TARGETS (extra) same, run('RX-TARGETS', L) PASS 6/6 at 09:36: statusPill matches 1 component (kind fn, rerender forced it through its observer hook), bar unchanged (13/13); appButtons matches 1 component (kind fwd, the ActionRow; PlaySection excluded by onNav), patched render ran 11 times after a remount, /library/app/2379780 unchanged (117/117); patchedLeft 0 after each removal. In the final pass (10:12) the page comparison failed only on the footer legend (focus settled differently); the test now compares as RX-FN does JSON 2026-10-07 09:36 11094443
Both input modes rx.py --mode laser RX-1 RX-3, then --mode pad (each = glass.py js --flags reactLab --mode <m> "…run(id, L)") PASS: RX-1 19/19, 19/19 and RX-3 8/8 with lgs-input-laser, and again with lgs-input-pad on main (classes fallback; P3's rt.input was off). Correction (review R1 m9): the class stub changes nothing P2 reads, so these runs only show the classes do not disturb P2. The two input paths are covered inside the tests themselves: RX-1 drives a laser click and D-pad/A/B/≡ on the same page, and since R1 RX-7 drives A on a DialogButton and a ui.menu item (gamepad) next to a lab click. The R1 pass repeats RX-1, RX-3 and RX-7 under --mode with P3's input module on (wp.p3) (not kept) 2026-10-07 09:44–09:46 11094443
Kill switch glass.py js --flags react=false,reactLab "…module('react')…", then a plain step PASS: react off (flag react is off), react-lab blocked (dependency react is not installed), __LGS_RT.react gone; after the step react installed again with the alias JSON 2026-10-07 09:47 11094443
Lab fixture shot glass.py shot main p2_p2_lab_confirm --settle 0.4 --flags reactLab --pre "…react.lab.open('ConfirmModal')" Steam's ConfirmModal rendered on our lab route inside Steam's header and footer, themed as the glass alert (P4/C1c CSS) shots/p2_p2_lab_confirm.png 2026-10-07 09:48 11094443
Error page shot glass.py shot main p2_p2_failpage --settle 0.4 --flags reactLab --pre "…lab.fixture('boom', throwing component); lab.open('boom')" ui.ErrorBoundary's fallback: title (English UI) and a focused Back capsule (#Button_Back), Steam's header and footer intact, footer shows A Select / B Back. Back was a 427 px full-width bar at 09:49; re-shot at 10:45 after the fix: a 240 × 60 capsule, focused, under C1a's new header and bottom ornament (D-P2-6) shots/p2_p2_failpage.png 2026-10-07 10:45 11094443
Final pass (after the last code change) rx.py RX-1 … RX-REMOVE (one locked step per test) RX-1 19/19 ×2, RX-2 6/6, RX-3 8/8, RX-4 3/3, RX-5 4/4, RX-7 16/16, RX-OV 8/8, RX-HEAL 3/3, RX-FN 5/5 PASS. RX-6 FAIL under load: 490, 474, 478 ms (the Steam renderer was at 90 % CPU, load average 3.8, other agents' steps); see the re-run below. RX-TARGETS: legend-only difference (fixed, re-run below). RX-REMOVE: not run (lab: lock busy for 240 s) JSON 2026-10-07 09:55–10:12 11094443
RX-6 (scan rewritten) glass.py js --flags reactLab "…run('RX-6')" twice, after replacing the one big alternation regex with one anchor needle per finder PASS: 165, 144, 145 ms and 143, 146, 145 ms (max 165 <= 250) on the same busy device where the regex scan took 466–494 ms (10:20–10:29: FAIL); cached ready() 0–0.1 ms; all 24 finders still exactly 1 candidate. Benchmark on the device: stringify 9 ms, the 21 MB corpus is two-byte, one includes pass ≈ 5 ms per needle JSON 2026-10-07 10:36 11094443
RX-TARGETS, RX-REMOVE re-run rx.py RX-TARGETS RX-REMOVE PASS 6/6 (10:24) and PASS 6/6 (10:27: removal report patchedLeft 0, restored 4) JSON 2026-10-07 10:27 11094443
Second full pass (after the scan rewrite) rx.py RX-1 … RX-REMOVE RX-1 19/19 ×2, RX-2 6/6, RX-3 8/8, RX-4 3/3, RX-5 4/4, RX-6 3/3, RX-7 16/16, RX-FN 5/5, RX-REMOVE 6/6 PASS. Not run: RX-OV (lab: lock busy for 240 s), RX-HEAL (__LGS_RT is not defined: the SharedJSContext renderer had just restarted). RX-TARGETS: statusPill matched nothing, fail closed, because the theme's class index had been rebuilt with 3 modules after that restart (REQ P2->P1); statusPill now reads the class from Steam's bar CSS module (finder BarClasses, 25 finders) JSON 2026-10-07 10:39–10:51 11094443
Final code re-run (BarClasses finder) one glass.py js --flags reactLab "__LGS_RT.react.lab.run('RX-n', L)" step per test RX-6 PASS (3/3, the scan with 25 finders), RX-TARGETS 6/6, RX-OV 8/8, RX-HEAL 3/3, RX-2 6/6, RX-5 4/4 PASS. Code under test: 02-react.js md5 b5e1acc3…, 03-react-lab.js ebe27c13… (the files in the tree at the end of session 2). After it: no route, override or patch of ours, patchedLeft 0, element factories not hooked, react-lab off, no session flags, no menu or modal open JSON 2026-10-07 10:54–10:57 11094443
M1 stubs load node --check device/rt/02-react.js device/rt/03-react-lab.js; on the Frame: eval of both files without any runtime (no-op), then with a test runtime shim (window.__LGS_RT_TEST, scratch only): install react-lab (pulls react), status(), ready(), remove Parse OK. Without a runtime: nothing defined. With the shim: install 0 ms, ready() throws lgs-react: M1 stub, not implemented yet, remove returns patchedLeft: 0 for both, shim gone — 2026-10-07 11094443
R1 RX-7 (gamepad phase added) rx.py RX-7 = glass.py js --flags reactLab "__LGS_RT.react.lab.run('RX-7', L)" PASS 22/22: the 16 earlier checks, plus: gamepad focus on a lab DialogButton; A on it reaches the action as the gamepad path (PointerEvent click, trusted false, pointerType '', pointerId -1; reasons only the test switches, no synthetic pointer event; logged); gamepad focus in a ui.menu sheet and A on its item: the same; a lab MouseEvent click on the button is still synthetic pointer event; spies 0 calls. Also under --mode laser and --mode pad with wp.p3: 22/22 each P2-cmp.json RX-7, RX-7.laser.p3, RX-7.pad.p3 2026-10-07 13:42, 13:55, 13:56 11094443
R1 RX-OPT (M2, m5) rx.py RX-OPT PASS 11/11: optional appButtons registered on Home as the only registration (count 0, pending, watching true, 4 late timers); after the registration series nothing attached and it is still watching (0 timers left); on /library/app/2379780 attached by "navigation +120 ms" (1 fwd), its render ran 5 times; a second layer on appButtons matches (1 fwd) and both layers run after a remount; removal leaves patchedLeft 0, watching false; optional pagedSettings registered on Home attaches on entering /settings/system (1 fn), its render ran; removal clean, element factories unhooked P2-cmp.json RX-OPT 2026-10-07 14:00 11094443
R1 RX-OPT failure at 13:46 (not reproduced) same, in the final pass right after RX-REMOVE and RX-TARGETS FAIL 7/11: the late-attach timers never fired (lateTimers stayed 4), so appButtons stayed pending. Not reproduced: timers fired before and after test.cycle() in a probe at 13:58, and RX-REMOVE → RX-TARGETS → RX-OPT passed in sequence at 13:59–14:00 on a fresh runtime. A module scope that P1 marks dead (removal, or a reload by another agent during the step) silences its timers exactly this way; the runner now records the runtime start time, the module state and the module's warnings at start and end P2-cmp.json failedRuns 2026-10-07 13:46 11094443
R1 RX-FAIL (M3, m8) rx.py RX-FAIL PASS 8/8 on /library/lgs/lab/raw (a real routes.add page, no lab wrapper): root 1280 × 640 spans its parent; title and Back centres x 640 = window centre; title x 439 (≥ 48 inset); title 30 px / 700 (Title 2); Back 240 × 60, radius 30 px, 24 px / 600, outline none, border 0; DOM focus on Back at entry and .gpfocus on it after one D-pad press (the navigation source was not the gamepad after another agent's laser step); B leaves P2-cmp.json RX-FAIL 2026-10-07 13:52 11094443
R1 Error page shot on a real route glass.py shot main p2_p2_failroute --settle 0.8 --flags reactLab --pre "…lab.openRaw('boom') …" Title and the focused Back capsule on the window's centre line, Steam's header and C1a's bottom ornament (Select A / Back B) intact; no outline. Replaces the lab-route shot as evidence (the lab route's own centring hid M3) shots/p2_p2_failroute.png 2026-10-07 12:53 11094443
R1 RX-RETRY (m7) rx.py RX-RETRY PASS 5/5: with test.noSwitch(true) ready() throws a transient error (retry after 1000 ms); within the backoff the same error object is thrown again; routes.add still fails closed (nothing patched); after the backoff it retries (next backoff 2000 ms, attempt 2); once the switch is back, ready() succeeds by itself without reset() in 3 ms (finders reused) P2-cmp.json RX-RETRY 2026-10-07 13:52 11094443
R1 RX-OPEN (m6) rx.py RX-OPEN PASS 3/3: the throwing fixture gives {mounted: false, error: 'react-lab: forced render error (fixture boom)'}; the next fixture on the same route mounts cleanly (mounted: true, error: null); closed, no lab route left P2-cmp.json RX-OPEN 2026-10-07 13:52 11094443
R1 RX-3 (with D-P2-7) rx.py RX-3 PASS 9/9: starts on a poster (app 1147940), modal over /library/tab/AllGames, route root the same node, .gpfocus in the modal, B closes it, focus back on the same poster by itself within 800 ms (new check), node count back to 852. Also --mode laser/--mode pad with wp.p3: 9/9 each. Before D-P2-7: 3/3 FAIL (12:58, 13:06, 13:13; see the review table) P2-cmp.json RX-3, RX-3.laser.p3, RX-3.pad.p3 2026-10-07 13:39, 13:53, 13:56 11094443
R1 Regression pass (final code) one rx.py step per test RX-1 19/19 ×2 (also laser and pad with wp.p3: 19/19 ×2 each), RX-2 6/6, RX-4 3/3, RX-5 4/4, RX-6 2/2 (scan within 250 ms), RX-OV 8/8, RX-HEAL 3/3, RX-FN 5/5, RX-REMOVE 6/6 (re-run at 13:59 after the runner waits for Steam's exiting page; the 13:44 run failed only because that page was still in the DOM at +1.2 s), RX-TARGETS 6/6. 22 results, all ok P2-cmp.json (summary, results, failedRuns) 2026-10-07 13:37–14:00 11094443
R1 Device left as found after the last step No route, override or patch of ours (routes.list() empty, patch.list() empty), react-lab off, no session flags of ours. Native mode was on during the last steps (another agent's native-session; P2 did no native work) — 2026-10-07 14:02 11094443

Decisions taken without the user (PLAN §1.17 rule)

  • D-P2-1 Actions are dry-run by default. Flag actionsLive, built-in default false: every rt.react.actions call is logged, never run, until V1 sets actionsLive: true in device/defaults.json at release (hand-off: REQ P2->Coordinator, P2->V1, P2->V2 below). Also in test mode whatever that flag says: P1's action-logger switch, actions.test(true), an untrusted (synthetic) mouse or pointer event other than Steam's own programmatic click (A on a DialogButton or MenuItem, review R1 M1), or an unreadable test state. Reason: ~20 agents drive the live UI during the build; a launch must never happen by accident (never-list).
  • D-P2-2 Lazy scan. install() only creates rt.react; the one source scan runs on the first call that needs it. With no T3 caller enabled nothing is scanned or patched.
  • D-P2-3 actions vs nav. actions.navigate is for user-intent jumps (game pages, library tabs) and is logged in test mode; nav.go is plain navigation inside our own views and always runs.
  • D-P2-4 Bare function components are patched through Steam's element factories. A component with no shared holder (PagedSettings, the status pill) would lose a fiber type patch on every remount. While such a patch exists, jsx, jsxs and createElement of Steam's own React substitute our trampoline for that one function (one Map lookup per element; RX-FN, RX-REMOVE). Safe default: the hook exists only while a bare-function patch does, removal restores the originals (checked by patchedLeft), and the cost is one possible remount of the live instance when a patch is added or removed.
  • D-P2-5 rerender uses only Steam's own update paths. forceUpdate() for classes and mobx's own stateVersion + onStoreChange() for observers; nothing else is forced (no state is changed to provoke a render).
  • D-P2-6 The error page's title is English-only. Steam has no token for "This view could not be shown"; per PLAN §1.15 the title is drawn only when the UI language starts with en, and the focused Back capsule (#Button_Back) is always there.
  • D-P2-7 ui.modal gives focus back. After a modal closes, Steam (VR) parks gamepad focus in vr-null-tree and its own FocusApplicationRoot() reactivates the page with no focused node (live 12:58–13:13: RX-3 3/3). When the modal closes through Steam's closeModal, P2 calls the page tree's own Activate() and TakeFocus() (Steam's functions; the tree remembers its last child), only if the route is unchanged, the page is still mounted and focus is lost or still on the closing modal; at most two tries over 1 s. Safe default: it moves focus only from "nowhere" back to where it was, and opts.restoreFocus: false turns it off per call.

Requests

  • REQ P2->Coordinator: PLAN.md (yours) has no row for P2's release switch. Under D-P2-1 every T3 action (rt.react.actions: Home and "+" launches, the game page's Play, Search's Open, launcher rows) is logged, never run until the flag actionsLive is true in device/defaults.json. HA AT-4/AT-13 and the §4.5 ledger's static check all pass while every launch is dead, so nothing would catch a release that forgets it. Please add (1) a §1.17 row, for example "S26 | T3 actions run (launch, Play, Open) | D-P2-1, react.md §8 | Built dry-run for the build; V1 turns it on at release after RX-7 (incl. its gamepad phase), HA AT-4 and AT-13 pass | Off during the build, On at release | actionsLive", and (2) a §4.6 item: "defaults.json has actionsLive: true, and V2's shipped-state run shows rt.react.actions.mode() = live outside lab steps".

    • Coordinator (2026-10-07, round R2): added as proposed: §1.17 S26 (actionsLive, off during the build, on at release), §4.6 item 9 (with your V2 reading: reasons exactly ['runtime action logger on'] inside the lab step), and a §4.5 rule that keeps T3 launch rows partial until that check reads live (PLAN §1.19 R2-9).
  • REQ P2->V1: at release, set "actionsLive": true in device/defaults.json (your file), once RX-7 (with its gamepad phase), HA AT-4 and AT-13 pass on the release build. Without it every T3 launch stays a logged dry run (D-P2-1; react.md §8 reason 2). Check after: python glass.py js "__LGS_RT.react.actions.mode()" outside any --flags step gives {mode: 'live', reasons: []} (P10's lab step turns the action logger on, so inside a step it says test).

  • REQ P2->V2: add a shipped-state check to the verification suite: with the shipped defaults.json and no lab overlay, __LGS_RT.react.actions.mode() must be live (reasons []), and mode(ev) for Steam's own programmatic click (a PointerEvent('click') with pointerType '') must also be live. A test result there means every T3 launch is dead in the product. The read itself is a lab step, which turns the action logger on, so read actions.mode().reasons and accept exactly ['runtime action logger on'] as the shipped state (any other reason fails).

  • REQ P2->P1: rt.react.ready() now marks failures that clear by themselves (Steam's UI still mounting after a SharedJSContext restart: main route switch not found, no React fiber in the main window) with err.transient === true and err.retryAfterMs, and retries them itself (react.md v3 §1). A consumer whose install() threw such an error stays failed until the next lgs on (runtime.md §2: "Stays failed until the next lgs on"). Please re-run the install of a module that failed with an error whose transient === true (it may be wrapped: check err.transient or err.cause.transient), after retryAfterMs (at least 1 s), at most 4 times, logging each attempt; stop when the runtime is torn down. Until then the window is narrow (P1's lgs on waits for Steam's UI first), so this is a robustness item, not a blocker.

    • P1 (2026-10-07 09:25): done in device/rt/00-rt.js: a failed install() whose error (or an error on its cause chain, 4 deep) has transient === true is retried after max(1000, retryAfterMs) ms, at most 4 times, each attempt logged (transient failure; retry n/4 in … ms, reason and retries in status()); not after teardown, not after an install timeout; dependents install once it does. Offline h_r1.js C17–C22 (wrapped cause, dependent, cap, no retry after teardown); live RT-2 with rt.stub.transient (installed after exactly 2 retries, 09:19). Contract runtime.md §2.
  • REQ P2->P1: device/rt/00-rt.js scope.setTimeout (about line 521) registers its off with track(), which pushes it onto the module's cleanups array for the module's whole life; a timer that fired (or was cancelled) leaves its entry behind (done = true, never removed). P2 now runs short scoped timer series (optional-patch late attach: 4 timers per navigation while a patch is pending; modal focus restore: up to 4 per modal; the source-cache drop), so a long session grows the array by a few entries per navigation. Please drop an entry from cleanups when its off runs (or prune entries with done periodically). Minor (a closure per entry), not a blocker.

    • P1 (2026-10-07 11:20, maintenance session): done in device/rt/00-rt.js makeScope: the tracked list is a Set of live subscriptions, and every off() removes itself when it runs, so a fired or cancelled rt.setTimeout (and any tracked off() a module calls itself: rt.listen, rt.on, rt.flags.on, ...) leaves it at once; removal still undoes the live ones newest first. status().modules[].subscriptions now counts live ones. Offline (h_maint.js, P1 harness + 4 new checks): 200 fired timers → back to the baseline count, 50 cancelled timers + a listener + a bus subscription → pruned (and the listener really removed), removal order and a pending timer that never fires; the same checks fail on the old code (200 / 252 left). Live on the Frame with the new file: RT-1, RT-4, RT-I PASS (10:54-10:58), RT-2 PASS (11:02). Contract runtime.md §3.2 and changelog.
  • REQ P2->P1: device/lgs_core.js:30 keeps window.__LGS_INDEX for the life of the JS context. On 2026-10-07 about 10:50 the SharedJSContext renderer restarted (new steamwebhelper pid, 2614 factories instead of 2618; Steam itself kept running) and the next lgs on built the index while Steam's UI was still loading: __LGS_INDEX.size was 3 instead of 550, glass.py status listed 735 unresolved tokens, and every later lgs on reused that index, so the theme stayed mostly unstyled for everyone (it was rebuilt by someone about 10:58). Please rebuild when the cached index is implausibly small (for example size < 200) or was built in another renderer (store Object.keys(req.m).length with it and compare), and retry the build when it finds fewer than 200 CSS modules.

    • P1 (2026-10-07 08:20, review R1 M1): done. The index is cached only when plausible (>= 200 CSS modules, at most half the theme's tokens unresolved) and only while current (same factory count as when built; a new chunk rebuilds it); a bad cached one (also from an older build) is dropped and rebuilt by the next lgs on, and by off + on. lgs on first waits for Steam's UI and a plausible harvest (backoff 2, 4, 8, then 15 s, 120 s), logging each attempt to /tmp/lgs/lgs.log, and changes nothing when it gives up. lgs status reports index: {state, size, factories, current, cached}. Live test RT-I (planted 3-module cache rebuilt; forced short harvest gives up without changes). Contract runtime.md §5.
  • REQ P2->P10: a runtime reload (lgs on / reload by another agent, which takes no lab lock) during a locked step with --flags bakes the step's /tmp/lgs/flags.json into the new runtime's session layer. When the step exits, lab.py restores the file but the runtime is never told, so the flag stays on for everyone until the next reload (seen 3 times on 2026-10-07 between 09:41 and 09:46 with reactLab, which then left react-lab installed outside any step; P2 cleared it with __LGS_RT.flags.setSession({})). Please, at the lock exit, also apply the restored file content live: __LGS_RT.flags.setSession(<restored object>) (P1's lgs flags does the same), or read it back with lgs.flag_layers().

    • P10 (2026-10-07 06:35): done: at every lock exit that wrote /tmp/lgs/flags.json, after restoring the file the lab calls __LGS_RT.flags.setSession(<restored content, or {}>) so a runtime reloaded mid-step drops the step's flags.