J121: Quiet background tabs with immediate terminal recovery #317

Open
opened 2026-11-03 03:07:26 +00:00 by kayg · 16 comments
Owner

Job J121: keep background tabs quiet and make return responsive.

Owner direction: “we need to strike a balance here, must not cause unnecessary drain but at the same time make returning to a backgrounded tab instantly responsive.”

Design proposal

  • Move idle keepalive to server-originated WebSocket protocol Ping frames on the existing ordered TCP link. Browsers answer natively, without a JavaScript timer or application-level probe. Keep the current D300 adaptive RTT/jitter allowance and two-miss retirement rule; do not add an out-of-band transport.
  • A hidden page clears all client liveness timers and sends no client-initiated probes. Keep its last rendered terminal screen. Visible-idle also has no client liveness timer.
  • On visibilitychange, focus, pageshow (including bfcache restore), or first input, coalesce activation into one immediate application-level probe. Keep the cached screen painted while it runs. If it expires under the D300 allowance, retire the route and reconnect immediately without first-attempt backoff. Send the D265 resume token in the first application message after the WebSocket opens.
  • Preserve input order and deduplication using the existing per-stream sequence/ack protocol. Confirm that input entered during recovery is delivered once.

Plan and estimate: inspect the route liveness and input replay contracts; add failing-first client/server and browser regression tests; implement the narrow liveness change; run focused browser specs and scripts/verify fast; record visible-idle/hidden wakeups and return latency after 5 min, 30 min, 2 h, and a server restart while hidden. Estimate 4–6 hours of implementation and gates, plus the requested 2-hour hidden soak and shared build-slot wait. A phone battery profile is best-effort if the available browser VM cannot model a phone power profile.

Job J121: keep background tabs quiet and make return responsive. Owner direction: “we need to strike a balance here, must not cause unnecessary drain but at the same time make returning to a backgrounded tab instantly responsive.” Design proposal - Move idle keepalive to server-originated WebSocket protocol Ping frames on the existing ordered TCP link. Browsers answer natively, without a JavaScript timer or application-level probe. Keep the current D300 adaptive RTT/jitter allowance and two-miss retirement rule; do not add an out-of-band transport. - A hidden page clears all client liveness timers and sends no client-initiated probes. Keep its last rendered terminal screen. Visible-idle also has no client liveness timer. - On visibilitychange, focus, pageshow (including bfcache restore), or first input, coalesce activation into one immediate application-level probe. Keep the cached screen painted while it runs. If it expires under the D300 allowance, retire the route and reconnect immediately without first-attempt backoff. Send the D265 resume token in the first application message after the WebSocket opens. - Preserve input order and deduplication using the existing per-stream sequence/ack protocol. Confirm that input entered during recovery is delivered once. Plan and estimate: inspect the route liveness and input replay contracts; add failing-first client/server and browser regression tests; implement the narrow liveness change; run focused browser specs and `scripts/verify fast`; record visible-idle/hidden wakeups and return latency after 5 min, 30 min, 2 h, and a server restart while hidden. Estimate 4–6 hours of implementation and gates, plus the requested 2-hour hidden soak and shared build-slot wait. A phone battery profile is best-effort if the available browser VM cannot model a phone power profile.
Author
Owner

Design validation and estimate, posted before code:

The client currently parks its liveness timer while hidden, but retains a recurring timer/probe path when visible and idle. The server already has WebSocket transport Ping handling; a WebSocket control Ping still travels on the existing ordered TCP connection, so this avoids an out-of-band path. I will move idle keepalive to server-originated transport Ping/Pong, correlate those replies against D300's measured route lease, and retain D300's first-miss doubt / second-miss retirement. The wake path remains one immediate ordered application Ping on return, with the cached screen left painted. A missed wake probe retires the route at the D300 allowance and starts the first resume attempt without backoff; the resume token remains the first application message after open per D265. Existing stream sequence/ack state should preserve any input entered during that probe; a browser regression will prove single ordered delivery.

No contract or decision change is proposed. I will add failing-first tests for zero idle client timers/probes, immediate return probing, dead-link resume, and input replay, then run the focused browser specs and scripts/verify fast. I will record a before/after table for visible idle and hidden wakeups, plus live-terminal return at 5 min, 30 min, 2 h, and after a server restart while hidden. Estimate: 4–6 hours implementation and gates, plus the requested 2-hour hidden soak and shared build-slot wait. Phone battery profiling is best-effort if the available VM cannot model phone power.

Design validation and estimate, posted before code: The client currently parks its liveness timer while hidden, but retains a recurring timer/probe path when visible and idle. The server already has WebSocket transport Ping handling; a WebSocket control Ping still travels on the existing ordered TCP connection, so this avoids an out-of-band path. I will move idle keepalive to server-originated transport Ping/Pong, correlate those replies against D300's measured route lease, and retain D300's first-miss doubt / second-miss retirement. The wake path remains one immediate ordered application Ping on return, with the cached screen left painted. A missed wake probe retires the route at the D300 allowance and starts the first resume attempt without backoff; the resume token remains the first application message after open per D265. Existing stream sequence/ack state should preserve any input entered during that probe; a browser regression will prove single ordered delivery. No contract or decision change is proposed. I will add failing-first tests for zero idle client timers/probes, immediate return probing, dead-link resume, and input replay, then run the focused browser specs and `scripts/verify fast`. I will record a before/after table for visible idle and hidden wakeups, plus live-terminal return at 5 min, 30 min, 2 h, and after a server restart while hidden. Estimate: 4–6 hours implementation and gates, plus the requested 2-hour hidden soak and shared build-slot wait. Phone battery profiling is best-effort if the available VM cannot model phone power.
Author
Owner

Started implementation in branch j121-link-liveness, worktree ~/Developer/Worktrees/infraternal-J121, based on origin/dev at 7ad8a9c967bb62b70e27d56d5c9544bfc61eb6d3.

Initial code finding (origin/dev at base SHA): apps/tracer/src/connection.ts owns a single livenessTimer; scheduleLiveness() returns for hidden pages but reschedules while visible after a correlated Ping/Pong. crates/infraternal-server/src/websocket.rs already parses WebSocket transport Ping and Pong frames, but server-originated transport Ping/Pong is not yet used for keepalive. D300's browser route already carries an adaptive two-miss lease. I will add failing-first tests around these behaviors before changing the implementation.

Started implementation in branch `j121-link-liveness`, worktree `~/Developer/Worktrees/infraternal-J121`, based on `origin/dev` at `7ad8a9c967bb62b70e27d56d5c9544bfc61eb6d3`. Initial code finding (origin/dev at base SHA): `apps/tracer/src/connection.ts` owns a single `livenessTimer`; `scheduleLiveness()` returns for hidden pages but reschedules while visible after a correlated Ping/Pong. `crates/infraternal-server/src/websocket.rs` already parses WebSocket transport Ping and Pong frames, but server-originated transport Ping/Pong is not yet used for keepalive. D300's browser route already carries an adaptive two-miss lease. I will add failing-first tests around these behaviors before changing the implementation.
Author
Owner

Finding with failing-first evidence (compute01, base 7ad8a9c96): added focused regressions before implementation. bun test apps/tracer/src/connection-idle.test.ts -t "visible idle" fails because clientTimers is 1 after 60 s of visible-idle virtual time (expected 0). bun test apps/tracer/src/connection-idle.test.ts -t "first input" fails because the first Input sends 0 immediate probes (expected 1). Both are the intended baseline failures.

Finding with failing-first evidence (compute01, base `7ad8a9c96`): added focused regressions before implementation. `bun test apps/tracer/src/connection-idle.test.ts -t "visible idle"` fails because `clientTimers` is 1 after 60 s of visible-idle virtual time (expected 0). `bun test apps/tracer/src/connection-idle.test.ts -t "first input"` fails because the first `Input` sends 0 immediate probes (expected 1). Both are the intended baseline failures.
Author
Owner

Rebased the J121 worktree onto current origin/dev at 1e1ddfb633; branch remains j121-link-liveness. Design validation: server WebSocket control-frame Ping/Pong handling already exists, and the browser route uses D300 adaptive allowances with two-miss retirement. The route currently sends muxed Ping frames while visible and suspends them on the hidden visibility notice. I will move those probes to native WebSocket control frames, keep the D300 two-miss deadline, and preserve the one-probe return path so one delayed reply does not retire a healthy route.

Rebased the J121 worktree onto current origin/dev at 1e1ddfb633e5689f06d42decc427a1828f1ab307; branch remains j121-link-liveness. Design validation: server WebSocket control-frame Ping/Pong handling already exists, and the browser route uses D300 adaptive allowances with two-miss retirement. The route currently sends muxed Ping frames while visible and suspends them on the hidden visibility notice. I will move those probes to native WebSocket control frames, keep the D300 two-miss deadline, and preserve the one-probe return path so one delayed reply does not retire a healthy route.
Author
Owner

Design check before the next validation: D300(C) leaves probes outside the ordered application stream as a separate design question. The proposed native WebSocket Ping keeps the existing TCP connection, but the probe is a WebSocket control frame outside the multiplexed application stream. This J121 task explicitly asks to evaluate and implement that path, so I will test it as a scoped experiment without editing D300; D300's adaptive deadline and second-miss retirement remain unchanged. The protocol tests cover payload correlation and invalid lengths, and the real browser spec will verify native Pong and return recovery.

Design check before the next validation: D300(C) leaves probes outside the ordered application stream as a separate design question. The proposed native WebSocket Ping keeps the existing TCP connection, but the probe is a WebSocket control frame outside the multiplexed application stream. This J121 task explicitly asks to evaluate and implement that path, so I will test it as a scoped experiment without editing D300; D300's adaptive deadline and second-miss retirement remain unchanged. The protocol tests cover payload correlation and invalid lengths, and the real browser spec will verify native Pong and return recovery.
Author
Owner

Progress: rebased j121-link-liveness from 1e1ddfb to current origin/dev 7add126405; the seven worktree edits restored cleanly. Both queues are occupied. At check time compute01 had 4 busy slots and 4 queued (next position 5); dev-02 had 2 busy slots and 5 queued (next position 6), so I will use compute01. Next: lockfile install, focused client/server checks, then the real browser spec.

Progress: rebased j121-link-liveness from 1e1ddfb to current origin/dev 7add126405a9205bb817a154af11fad2c438cd45; the seven worktree edits restored cleanly. Both queues are occupied. At check time compute01 had 4 busy slots and 4 queued (next position 5); dev-02 had 2 busy slots and 5 queued (next position 6), so I will use compute01. Next: lockfile install, focused client/server checks, then the real browser spec.
Author
Owner

Queue update before the first gated run: compute01 now has 4/4 slots busy with 6 queued (next position 7); dev-02 has 2 busy with 5 queued (next position 6). I will run the tracer install and focused checks on dev-02 to take the shorter queue; it will use the same 7add126 base.

Queue update before the first gated run: compute01 now has 4/4 slots busy with 6 queued (next position 7); dev-02 has 2 busy with 5 queued (next position 6). I will run the tracer install and focused checks on dev-02 to take the shorter queue; it will use the same 7add126 base.
Author
Owner

Queue recheck: dev-02 still has six queued across two full slots; compute01 now has three queued across four full slots. I am canceling only J121's queued dev-02 ticket and moving the focused run to compute01, whose j121 worktree is already synced to 7add126 with the same seven edits.

Queue recheck: dev-02 still has six queued across two full slots; compute01 now has three queued across four full slots. I am canceling only J121's queued dev-02 ticket and moving the focused run to compute01, whose j121 worktree is already synced to 7add126 with the same seven edits.
Author
Owner

Finding from the first locked tracer check on compute01 ( succeeded): reported TS2554 at connection-idle.test.ts lines 143, 159, 194, 273 and 294: Expected 1 argument, but got 2. The new assertions passed human-readable messages unsupported by Bun's matcher type. I am removing those optional messages and rerunning the check.

Finding from the first locked tracer check on compute01 ( succeeded): reported TS2554 at connection-idle.test.ts lines 143, 159, 194, 273 and 294: Expected 1 argument, but got 2. The new assertions passed human-readable messages unsupported by Bun's matcher type. I am removing those optional messages and rerunning the check.
Author
Owner

Finding from the first locked tracer check on compute01 (bun install --frozen-lockfile succeeded): bun run check reported TS2554 at connection-idle.test.ts lines 143, 159, 194, 273 and 294: Expected 1 argument, but got 2. The new .toBe assertions passed human-readable messages unsupported by Bun's matcher type. I am removing those optional messages and rerunning the check.

Finding from the first locked tracer check on compute01 (bun install --frozen-lockfile succeeded): bun run check reported TS2554 at connection-idle.test.ts lines 143, 159, 194, 273 and 294: Expected 1 argument, but got 2. The new .toBe assertions passed human-readable messages unsupported by Bun's matcher type. I am removing those optional messages and rerunning the check.
Author
Owner

Focused result on compute01: bun install --frozen-lockfile checked 38 installs across 69 packages with no changes; bun run check passed; bun test src/connection-idle.test.ts reported 41 pass, 0 fail, 113 expect() calls. The initial TS2554 issue and fix are recorded above. Moving to the focused server Ping/Pong test before the browser soak.

Focused result on compute01: bun install --frozen-lockfile checked 38 installs across 69 packages with no changes; bun run check passed; bun test src/connection-idle.test.ts reported 41 pass, 0 fail, 113 expect() calls. The initial TS2554 issue and fix are recorded above. Moving to the focused server Ping/Pong test before the browser soak.
Author
Owner

J121c takeover (Claude Opus, on code-minipc01). Codex stopped at about 07:00 UTC on a 429.

State found:

  • Branch j121-link-liveness had never been pushed. Seven uncommitted files were in the code-host worktree ~/Developer/Worktrees/infraternal-J121 at base 7add12640. compute01 ~/worktrees/j121 has the same content; the dev-02 copy is older (it still has the TS2554 matcher messages). I committed the code-host copy verbatim as WIP 816071d99 and pushed it. My worktree is now ~/Developer/Worktrees/infraternal-j121c; the old one is detached, nothing deleted.
  • Last evidence (compute01:~/.cache/codex-scratch/j121/evidence/): tracer unit tests pass, 41/41. The new socket test hidden_browser_route_stays_live_on_native_websocket_pongs FAILS with socket test stopped before its observed event (90 s, J121_RUN_EXIT=101).
  • Processes: one orphan watcher (compute01 PID 518758, ppid 1, about 6 h old) was looping forever on a log that had already finished. I killed it by PID. Nothing else of J121's was running. compute01 target dir: ~/worktrees/j121/target (5.5 GB).

Design finding, needs a correction before more tests. On a browser route the server's next probe is due 26 ms (SERIAL_GAP) after each answer, so it probes about 30 times a second. On origin/dev a hidden page sends connection.visibility hidden, and Liveness::set_suspended stops server probes, so a hidden tab is silent today. The WIP removes set_suspended, so a hidden tab would get about 30 native WebSocket pings a second. Page JS stays asleep, but the browser network process and a phone's radio never get to idle. That is the "unnecessary drain" the owner ruled out. The visible-idle win of the WIP (no client timer, native pong answered without JS) is real and stays.

Remaining plan (estimate about 4 h plus build-slot queues):

  1. Rebase onto origin/dev 7c082bddf (29 commits ahead).
  2. Server: keep native control-frame pings for visible browser routes, and restore hidden suspension (no probes, lease parked; visibility restarts a fresh lease). Rewrite the failing socket test to: hidden gives no server probes within N s; visible gives native ping, and a native pong renews the lease.
  3. Client: keep the WIP (no idle timers, one coalesced probe on return or first input, immediate no-backoff reconnect, resume token first as in D265, input kept through the sequence/ack replay).
  4. Measure before (origin/dev) and after in a real Chromium against a real server build: wakeups (JS timer and message callbacks, WS frames, server pings sent) and renderer CPU while hidden and while visible idle, plus time to the first interactive frame and the first terminal byte on return (link alive, and after a server restart while hidden). The 5 min / 30 min / 2 h soak runs after that, if time allows.
  5. Atomic commits (server, client, tests), then scripts/verify fast through build-slot, with the result quoted here.
**J121c takeover (Claude Opus, on code-minipc01).** Codex stopped at about 07:00 UTC on a 429. State found: - Branch `j121-link-liveness` had **never been pushed**. Seven uncommitted files were in the code-host worktree `~/Developer/Worktrees/infraternal-J121` at base `7add12640`. compute01 `~/worktrees/j121` has the same content; the dev-02 copy is older (it still has the TS2554 matcher messages). I committed the code-host copy verbatim as WIP `816071d99` and pushed it. My worktree is now `~/Developer/Worktrees/infraternal-j121c`; the old one is detached, nothing deleted. - Last evidence (`compute01:~/.cache/codex-scratch/j121/evidence/`): tracer unit tests pass, 41/41. The new socket test `hidden_browser_route_stays_live_on_native_websocket_pongs` **FAILS** with `socket test stopped before its observed event` (90 s, `J121_RUN_EXIT=101`). - Processes: one orphan watcher (compute01 PID 518758, ppid 1, about 6 h old) was looping forever on a log that had already finished. I killed it by PID. Nothing else of J121's was running. compute01 target dir: `~/worktrees/j121/target` (5.5 GB). **Design finding, needs a correction before more tests.** On a browser route the server's next probe is due 26 ms (`SERIAL_GAP`) after each answer, so it probes about 30 times a second. On origin/dev a hidden page sends `connection.visibility hidden`, and `Liveness::set_suspended` stops server probes, so **a hidden tab is silent today**. The WIP removes `set_suspended`, so a hidden tab would get about 30 native WebSocket pings a second. Page JS stays asleep, but the browser network process and a phone's radio never get to idle. That is the "unnecessary drain" the owner ruled out. The visible-idle win of the WIP (no client timer, native pong answered without JS) is real and stays. Remaining plan (estimate about 4 h plus build-slot queues): 1. Rebase onto origin/dev `7c082bddf` (29 commits ahead). 2. Server: keep native control-frame pings for **visible** browser routes, and restore hidden suspension (no probes, lease parked; visibility restarts a fresh lease). Rewrite the failing socket test to: hidden gives no server probes within N s; visible gives native ping, and a native pong renews the lease. 3. Client: keep the WIP (no idle timers, one coalesced probe on return or first input, immediate no-backoff reconnect, resume token first as in D265, input kept through the sequence/ack replay). 4. Measure **before (origin/dev) and after** in a real Chromium against a real server build: wakeups (JS timer and message callbacks, WS frames, server pings sent) and renderer CPU while hidden and while visible idle, plus time to the first interactive frame and the first terminal byte on return (link alive, and after a server restart while hidden). The 5 min / 30 min / 2 h soak runs after that, if time allows. 5. Atomic commits (server, client, tests), then `scripts/verify fast` through build-slot, with the result quoted here.
Author
Owner

J121c progress (head d94a6112b on j121-link-liveness):

Server. Hidden suspension is restored: a hidden browser route gets no probe at all, native or application. Visible routes are probed with native WebSocket Ping control frames carrying the 16-byte correlation. The new socket test browser_route_probes_natively_while_visible_and_not_at_all_while_hidden covers both phases plus the return. Why the WIP version hung: FrozenSocketTime stops Tokio time, the test never advanced it, the 25 ms probe was never due, and the 90 s watchdog fired. The first rewrite also tripped on attach-time frames reaching the fence; that's fixed now, rerun pending.

Client bug found in the WIP (a real one). When a correlated reply arrived during the zero-delay confirmation turn, scheduleLiveness() cleared the timer but left livenessConfirmationPending = true. Every later scheduleLiveness() then returned early, so no later return or input probe ever armed a deadline, and a dead link would never be noticed. Fixed: the no-pending branch also drops the confirmation flag and bumps its token. This came out of porting dev's dropped test "a queued correlated reply cancels confirmation without letting a duplicate renew it".

Tests. The WIP had removed 15 client lease tests. I ported the ones that guard the same state machine to on-demand probes: confirmation reset by reconnect, close or dispose; a stale confirmation across reconnect; duplicate, wrong-timestamp and unknown-nonce replies cannot replenish grace; bounded stall grace; continuous typing; a replacement route's own grace; the queued-pong task turn. Four other WIP-unrun files encoded the old 25 ms cadence (connection-restart, connection-liveness, connection-probe-scheduling-review) and are updated to the on-demand contract.

  • Branch: bun test src gives 656 pass, 7 fail.
  • Unmodified dev 7c082bddf: the same 7 fail (663 pass). They're pre-existing and unrelated: the startup-line offer tests and the reconnect-snapshot camera test.

Measurement harness finding. The real-server relay (real/server.ts) used a ws client that auto-answers server Pings, so native pings never reached Chromium. The WIP's browser spec could therefore not see server probes at all. The relay now forwards Ping and Pong control frames with the same one-way delay as data. The new real/background-tab.spec.ts measures idle and hidden cost (renderer main-thread tasks, timer fires, renderer task ms, browser and server CPU ms, frames by kind) and, on return, the first live frame and the first terminal byte. It does this after 5 s and 30 s hidden, after a server restart while hidden, and on a half-open link. TRACER_MEASURE_ONLY=1 runs it against dev for the baseline.

Running now on compute01 (one slot, log ~/.cache/codex-scratch/j121c/run-all.log): branch static build, tracer check and tests, 3× background-tab, mobile-lease, then the dev baseline build and 3× background-tab. Target dir: compute01:~/worktrees/j121/target (shared by both builds).

J121c progress (head `d94a6112b` on `j121-link-liveness`): **Server.** Hidden suspension is restored: a hidden browser route gets no probe at all, native or application. Visible routes are probed with native WebSocket Ping control frames carrying the 16-byte correlation. The new socket test `browser_route_probes_natively_while_visible_and_not_at_all_while_hidden` covers both phases plus the return. Why the WIP version hung: `FrozenSocketTime` stops Tokio time, the test never advanced it, the 25 ms probe was never due, and the 90 s watchdog fired. The first rewrite also tripped on attach-time frames reaching the fence; that's fixed now, rerun pending. **Client bug found in the WIP (a real one).** When a correlated reply arrived during the zero-delay confirmation turn, `scheduleLiveness()` cleared the timer but left `livenessConfirmationPending = true`. Every later `scheduleLiveness()` then returned early, so **no later return or input probe ever armed a deadline**, and a dead link would never be noticed. Fixed: the no-pending branch also drops the confirmation flag and bumps its token. This came out of porting dev's dropped test "a queued correlated reply cancels confirmation without letting a duplicate renew it". **Tests.** The WIP had removed 15 client lease tests. I ported the ones that guard the same state machine to on-demand probes: confirmation reset by reconnect, close or dispose; a stale confirmation across reconnect; duplicate, wrong-timestamp and unknown-nonce replies cannot replenish grace; bounded stall grace; continuous typing; a replacement route's own grace; the queued-pong task turn. Four other WIP-unrun files encoded the old 25 ms cadence (`connection-restart`, `connection-liveness`, `connection-probe-scheduling-review`) and are updated to the on-demand contract. - Branch: `bun test src` gives 656 pass, 7 fail. - Unmodified dev 7c082bddf: the same 7 fail (663 pass). They're pre-existing and unrelated: the startup-line offer tests and the reconnect-snapshot camera test. **Measurement harness finding.** The real-server relay (`real/server.ts`) used a `ws` client that **auto-answers** server Pings, so native pings never reached Chromium. The WIP's browser spec could therefore not see server probes at all. The relay now forwards Ping and Pong control frames with the same one-way delay as data. The new `real/background-tab.spec.ts` measures idle and hidden cost (renderer main-thread tasks, timer fires, renderer task ms, browser and server CPU ms, frames by kind) and, on return, the first live frame and the first terminal byte. It does this after 5 s and 30 s hidden, after a server restart while hidden, and on a half-open link. `TRACER_MEASURE_ONLY=1` runs it against dev for the baseline. Running now on compute01 (one slot, log `~/.cache/codex-scratch/j121c/run-all.log`): branch static build, tracer check and tests, 3× background-tab, mobile-lease, then the dev baseline build and 3× background-tab. Target dir: `compute01:~/worktrees/j121/target` (shared by both builds).
Author
Owner

J121c progress: branch j121-link-liveness is now three atomic commits on 7c082bddf, head fe2c81aa4 (the WIP commits are squashed into them):

  1. 6fc34f615 server: browser routes probed with native WebSocket pings; hidden routes stay silent.
  2. a319a3ac4 tracer: the link is proved on demand, not on a timer; reconnect at once on return. Includes the stuck-confirmation fix and the ported lease tests. Also updates contracts/wire.md §9.
  3. fe2c81aa4 tracer real: background-tab measurement spec; the relay passes control frames.

Finding: compute01 cannot run real-server browser specs right now. The unchanged real/mobile-lease.spec.ts on unmodified dev 7c082bddf never gets a live terminal. The window stays connecting and the host pill says "reconnecting". The relay frame log shows Hello, Welcome, Subscribe, Call/Result and 230 Ping/Pong, but no AttachTerm: the page never sees the local host online. The embedded agent process starts and stays alive. A short INFRATERNAL_TEST_STATE_ROOT doesn't change it. dev-02 has passing real-spec runs from today (11:24), so the browser measurement moved there. It uses native aarch64 release builds of base and branch, built the same way: ~/.cache/codex-scratch/j121c/d02.log on dev-02. The verify fast gate on fe2c81aa4 is queued on compute01 (~/.cache/codex-scratch/j121c/gate.log).

Client unit tests on the branch (bun test src, compute01): 656 pass, 7 fail. The same 7 fail on unmodified dev (startup-line offer and reconnect-snapshot camera tests), so they're pre-existing.

J121c progress: branch `j121-link-liveness` is now three atomic commits on `7c082bddf`, head **`fe2c81aa4`** (the WIP commits are squashed into them): 1. `6fc34f615` server: browser routes probed with native WebSocket pings; hidden routes stay silent. 2. `a319a3ac4` tracer: the link is proved on demand, not on a timer; reconnect at once on return. Includes the stuck-confirmation fix and the ported lease tests. Also updates `contracts/wire.md` §9. 3. `fe2c81aa4` tracer real: background-tab measurement spec; the relay passes control frames. **Finding: compute01 cannot run real-server browser specs right now.** The unchanged `real/mobile-lease.spec.ts` on **unmodified dev 7c082bddf** never gets a live terminal. The window stays `connecting` and the host pill says "reconnecting". The relay frame log shows Hello, Welcome, Subscribe, Call/Result and 230 Ping/Pong, but **no AttachTerm**: the page never sees the local host online. The embedded agent process starts and stays alive. A short `INFRATERNAL_TEST_STATE_ROOT` doesn't change it. dev-02 has passing real-spec runs from today (11:24), so the browser measurement moved there. It uses native aarch64 release builds of base and branch, built the same way: `~/.cache/codex-scratch/j121c/d02.log` on dev-02. The `verify fast` gate on fe2c81aa4 is queued on compute01 (`~/.cache/codex-scratch/j121c/gate.log`). Client unit tests on the branch (`bun test src`, compute01): 656 pass, 7 fail. The same 7 fail on unmodified dev (startup-line offer and reconnect-snapshot camera tests), so they're pre-existing.
Author
Owner

J121c measurement: before (dev 7c082bddf) vs after (branch), real Chromium + real server, dev-02.

Setup: apps/tracer/real/background-tab.spec.ts with 75 ms each way through the relay and a mobile viewport. Both builds are native aarch64 release binaries built the same way; the binaries differ (e9f147be… branch, d97d0409… base). Three interleaved runs per side, TRACER_BACKGROUND_LINK_ONLY=1. Values are median [min–max] over 10 s samples. The raw lines are in dev-02:~/.cache/codex-scratch/j121c/d02c.log and d02c-metrics.txt.

before (dev) after (branch)
Visible idle, page timer fires / 10 s 300 [282–402] 2 [2–2]
Visible idle, client application Pings / 10 s 236 [232–313] 0
Visible idle, application frames received / 10 s 233 [232–314] 0
Visible idle, renderer main-thread task ms / 10 s 92.7 [68.9–201.3] 19.8 [10.9–27.3]
Visible idle, server CPU ms / 10 s 30 [30–40] 0
Visible idle, browser CPU ms / 10 s (all processes) 1480 [1120–2770] 1870 [850–2270] (overlapping, noisy)
Hidden (running), timer fires / probes / frames / server CPU 0 / 0 / 0 / 0 0 / 0 / 0 / 0
Hidden, renderer task ms / 10 s 211.5 [200–230] 455 [224–487] (see note)
Hidden + frozen 5 s and 30 s, frames received 0 0
Return after 5 s hidden, time until the link is proved (Pong) 161 ms 162 ms
Return after 30 s hidden, link proved 169 ms 160 ms
Return after a server restart while hidden, until the new socket's Welcome 162 ms 163 ms
Return on a half-open link (socket open, server gone), until reconnected 2248 ms [2235–2249] 1591 ms [1540–1620]
First painted frame after return (cached screen) 3.6–16.8 ms 4.9–17.1 ms

The return times are round trips at 75 ms each way: about one RTT (≈150 ms) when the link is up. The half-open gain comes from keeping the learned RTT and reconnecting without backoff: the D300 allowance (about two misses × (2·srtt + 4·rttvar + 75 ms)) instead of the 2 s startup grace.

Not measured, and why.

  • First terminal byte / echo on return. On dev 7c082bddf the real fixture cannot open a local terminal on either dev VM. ssh.session.open is refused with {"error":"terminal unavailable","reason":"invalid"}: the agent's open_requested → logins::ensure (per-person login) returns PermissionDenied/InvalidInput for the unprivileged fixture user. Unmodified real/mobile-lease.spec.ts fails the same way on dev. The spec has the echo measurement and the input-once check; they run as soon as the fixture can open a terminal. Input-once is covered by the unit test "input sent during a return probe follows it once and keeps its cumulative sequence".
  • Server native-ping cost while visible. The server probes only routes with an attached terminal, so without a terminal neither build sends server probes. On the branch those probes are WebSocket control frames the browser answers without page JS. Covered by the socket test.
  • Perf VM. I copied both x86 builds and trees to infraternal-perf-01-backup01 and queued them under perf-run. The VM was held by another job's mesh soak (j20/j88d, with j103c queued behind it), so I cancelled my run rather than leave it waiting.
  • Soak (5 min / 30 min / 2 h). Not run. Hidden traffic is zero on both builds (suspension), so a long hidden period changes nothing on the wire; only the NAT or OS lifetime of the socket matters, and that is the half-open or restart row.

Note on hidden renderer time. Hidden shows 0 timers and 0 frames on both builds, but branch renderer task time was higher in 2 of 3 runs (455 and 487 vs about 211 ms). Hidden here is emulated: visibilityState is overridden, so rendering still runs. dev-02 had another job in its second slot, and browser CPU swings widely in both columns (850–2770 ms). I have not explained this delta. It needs a clean perf-VM rerun before anyone relies on it either way.

**J121c measurement: before (dev `7c082bddf`) vs after (branch), real Chromium + real server, dev-02.** Setup: `apps/tracer/real/background-tab.spec.ts` with 75 ms each way through the relay and a mobile viewport. Both builds are native aarch64 release binaries built the same way; the binaries differ (`e9f147be…` branch, `d97d0409…` base). Three interleaved runs per side, `TRACER_BACKGROUND_LINK_ONLY=1`. Values are median [min–max] over 10 s samples. The raw lines are in `dev-02:~/.cache/codex-scratch/j121c/d02c.log` and `d02c-metrics.txt`. | | before (dev) | after (branch) | |---|---|---| | **Visible idle**, page timer fires / 10 s | 300 [282–402] | **2** [2–2] | | Visible idle, client application Pings / 10 s | 236 [232–313] | **0** | | Visible idle, application frames received / 10 s | 233 [232–314] | **0** | | Visible idle, renderer main-thread task ms / 10 s | 92.7 [68.9–201.3] | **19.8** [10.9–27.3] | | Visible idle, server CPU ms / 10 s | 30 [30–40] | **0** | | Visible idle, browser CPU ms / 10 s (all processes) | 1480 [1120–2770] | 1870 [850–2270] (overlapping, noisy) | | **Hidden** (running), timer fires / probes / frames / server CPU | 0 / 0 / 0 / 0 | 0 / 0 / 0 / 0 | | Hidden, renderer task ms / 10 s | 211.5 [200–230] | 455 [224–487] (see note) | | Hidden + frozen 5 s and 30 s, frames received | 0 | 0 | | **Return after 5 s hidden**, time until the link is proved (Pong) | 161 ms | 162 ms | | Return after 30 s hidden, link proved | 169 ms | 160 ms | | Return after a server restart while hidden, until the new socket's Welcome | 162 ms | 163 ms | | **Return on a half-open link** (socket open, server gone), until reconnected | 2248 ms [2235–2249] | **1591 ms** [1540–1620] | | First painted frame after return (cached screen) | 3.6–16.8 ms | 4.9–17.1 ms | The return times are round trips at 75 ms each way: about one RTT (≈150 ms) when the link is up. The half-open gain comes from keeping the learned RTT and reconnecting without backoff: the D300 allowance (about two misses × (2·srtt + 4·rttvar + 75 ms)) instead of the 2 s startup grace. **Not measured, and why.** - **First terminal byte / echo on return.** On dev `7c082bddf` the real fixture cannot open a local terminal on either dev VM. `ssh.session.open` is refused with `{"error":"terminal unavailable","reason":"invalid"}`: the agent's `open_requested` → `logins::ensure` (per-person login) returns PermissionDenied/InvalidInput for the unprivileged fixture user. Unmodified `real/mobile-lease.spec.ts` fails the same way on dev. The spec has the echo measurement and the input-once check; they run as soon as the fixture can open a terminal. Input-once is covered by the unit test "input sent during a return probe follows it once and keeps its cumulative sequence". - **Server native-ping cost while visible.** The server probes only routes with an attached terminal, so without a terminal neither build sends server probes. On the branch those probes are WebSocket control frames the browser answers without page JS. Covered by the socket test. - **Perf VM.** I copied both x86 builds and trees to `infraternal-perf-01-backup01` and queued them under `perf-run`. The VM was held by another job's mesh soak (j20/j88d, with j103c queued behind it), so I cancelled my run rather than leave it waiting. - **Soak (5 min / 30 min / 2 h).** Not run. Hidden traffic is zero on both builds (suspension), so a long hidden period changes nothing on the wire; only the NAT or OS lifetime of the socket matters, and that is the half-open or restart row. **Note on hidden renderer time.** Hidden shows 0 timers and 0 frames on both builds, but branch renderer task time was higher in 2 of 3 runs (455 and 487 vs about 211 ms). Hidden here is emulated: `visibilityState` is overridden, so rendering still runs. dev-02 had another job in its second slot, and browser CPU swings widely in both columns (850–2770 ms). I have not explained this delta. It needs a clean perf-VM rerun before anyone relies on it either way.
Author
Owner

J121c finished. Branch j121-link-liveness, head ea7b4ca6c14e94c737cf1de87c6194a36450edff, three atomic commits on 7c082bddf:

  1. dabeccdd9 server: browser routes probed with native WebSocket pings; hidden routes stay silent.
  2. fe5e4261f tracer: link proved on demand, not on a timer; reconnect at once on return. Includes the stuck-confirmation fix, the ported lease tests and contracts/wire.md §9.
  3. ea7b4ca6c tracer real: background-tab measurement spec (with link-only mode); the relay passes control frames.

Fixes since the previous gate attempt on fe2c81aa4, which failed and was superseded:

  • a clippy match_same_arms on the WIP's empty TransportPong arm;
  • two socket tests (restart_open_liveness_review, transient_authority_stall) that treated a native Ping as a closed socket.

scripts/verify fast (compute01, nix develop -c, through build-slot), step and result lines verbatim from compute01:~/.cache/codex-scratch/j121c/gate.log:

ea7b4ca6c14e94c737cf1de87c6194a36450edff
==> Repository artifact size gate
==> Release deployment functional checks
PASS proxy_forwarding samples=3 timed=false
==> fmt
==> VT native fmt
==> CLI plugin compile-out
==> clippy
==> SSH runtime test helper
==> real-binary fixture paths
==> test
==> SSH inode reuse fixture
==> shim process-per-user
==> cargo-deny
==> gate architecture
==> static builds
PT_INTERP: absent; size: 22476432 bytes; baseline 22418256; delta +58176 bytes (report only, D243 amended 2026-11-01)
PT_INTERP: absent; size: 20492632 bytes; baseline 20453280; delta +39352 bytes (report only, D243 amended 2026-11-01)
SKIP key memory: aarch64-unknown-linux-musl (host x86_64; native execution required)
x86_64-unknown-linux-musl: +102400 bytes accepted with delta; zero/negative deltas reported; missing/wrong/dynamic/PT_INTERP/inspection defects rejected PASS
aarch64-unknown-linux-musl: +102400 bytes accepted with delta; zero/negative deltas reported; missing/wrong/dynamic/PT_INTERP/inspection defects rejected PASS
==> Compiler metadata generator checks
==> Private server fixture startup cleanup
==> Vendored Ghostty artifact checksums
==> TypeScript protocol compiler
==> Tracer compiler, precompression and bundle transfer budget
==> Rust stream oracle (for the differential test)
==> TypeScript protocol tests
VERIFY_EXIT=0
[tail]

 48 pass
 0 fail
 77248 expect() calls
Ran 48 tests across 12 files. [14.00s]
VERIFY_EXIT=0

Static size against an origin/dev 7c082bddf static build from the same toolchain (x86_64 musl): 22,475,792 → 22,476,432 bytes, +640 B. The gate's "+58,176 B" is measured against its stored baseline. aarch64: 20,492,632 B; no same-base aarch64 static build to diff.

Measurements are in the comment above. Not done: the first-terminal-byte metric, a perf-VM rerun and the long soak, for the reasons given there.

Owner decisions needed:

  • Real-server fixture vs per-person logins. On dev the fixture cannot open a local terminal as an unprivileged user (logins::ensure → "invalid"). Every real-browser terminal spec (mobile-lease, the new background-tab) is blocked on both dev VMs until it is fixed. That belongs in its own job.
  • Visible-idle dead-link detection. By the issue's design a visible page has no client timer. While idle, a silently dead link is noticed only on the next return, focus or input (then within the D300 allowance), or by the server's native probes on its side. The window does not show "reconnecting" during that idle gap. The alternative is a slow client timer, which costs page wakeups.

Cleanup: dev-02 and perf-VM targets and copies removed; the compute01 target is removed after this comment. Logs are kept under ~/.cache/codex-scratch/j121c/ on compute01 and dev-02.

**J121c finished.** Branch `j121-link-liveness`, head **`ea7b4ca6c14e94c737cf1de87c6194a36450edff`**, three atomic commits on `7c082bddf`: 1. `dabeccdd9` server: browser routes probed with native WebSocket pings; hidden routes stay silent. 2. `fe5e4261f` tracer: link proved on demand, not on a timer; reconnect at once on return. Includes the stuck-confirmation fix, the ported lease tests and `contracts/wire.md` §9. 3. `ea7b4ca6c` tracer real: background-tab measurement spec (with link-only mode); the relay passes control frames. Fixes since the previous gate attempt on `fe2c81aa4`, which failed and was superseded: - a clippy `match_same_arms` on the WIP's empty `TransportPong` arm; - two socket tests (`restart_open_liveness_review`, `transient_authority_stall`) that treated a native Ping as a closed socket. `scripts/verify fast` (compute01, `nix develop -c`, through build-slot), step and result lines verbatim from `compute01:~/.cache/codex-scratch/j121c/gate.log`: ``` ea7b4ca6c14e94c737cf1de87c6194a36450edff ==> Repository artifact size gate ==> Release deployment functional checks PASS proxy_forwarding samples=3 timed=false ==> fmt ==> VT native fmt ==> CLI plugin compile-out ==> clippy ==> SSH runtime test helper ==> real-binary fixture paths ==> test ==> SSH inode reuse fixture ==> shim process-per-user ==> cargo-deny ==> gate architecture ==> static builds PT_INTERP: absent; size: 22476432 bytes; baseline 22418256; delta +58176 bytes (report only, D243 amended 2026-11-01) PT_INTERP: absent; size: 20492632 bytes; baseline 20453280; delta +39352 bytes (report only, D243 amended 2026-11-01) SKIP key memory: aarch64-unknown-linux-musl (host x86_64; native execution required) x86_64-unknown-linux-musl: +102400 bytes accepted with delta; zero/negative deltas reported; missing/wrong/dynamic/PT_INTERP/inspection defects rejected PASS aarch64-unknown-linux-musl: +102400 bytes accepted with delta; zero/negative deltas reported; missing/wrong/dynamic/PT_INTERP/inspection defects rejected PASS ==> Compiler metadata generator checks ==> Private server fixture startup cleanup ==> Vendored Ghostty artifact checksums ==> TypeScript protocol compiler ==> Tracer compiler, precompression and bundle transfer budget ==> Rust stream oracle (for the differential test) ==> TypeScript protocol tests VERIFY_EXIT=0 [tail] 48 pass 0 fail 77248 expect() calls Ran 48 tests across 12 files. [14.00s] VERIFY_EXIT=0 ``` Static size against an origin/dev `7c082bddf` static build from the same toolchain (x86_64 musl): 22,475,792 → 22,476,432 bytes, **+640 B**. The gate's "+58,176 B" is measured against its stored baseline. aarch64: 20,492,632 B; no same-base aarch64 static build to diff. Measurements are in the comment above. Not done: the first-terminal-byte metric, a perf-VM rerun and the long soak, for the reasons given there. Owner decisions needed: - **Real-server fixture vs per-person logins.** On dev the fixture cannot open a local terminal as an unprivileged user (`logins::ensure` → "invalid"). Every real-browser terminal spec (`mobile-lease`, the new `background-tab`) is blocked on both dev VMs until it is fixed. That belongs in its own job. - **Visible-idle dead-link detection.** By the issue's design a visible page has no client timer. While idle, a silently dead link is noticed only on the next return, focus or input (then within the D300 allowance), or by the server's native probes on its side. The window does not show "reconnecting" during that idle gap. The alternative is a slow client timer, which costs page wakeups. Cleanup: dev-02 and perf-VM targets and copies removed; the compute01 target is removed after this comment. Logs are kept under `~/.cache/codex-scratch/j121c/` on compute01 and dev-02.
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#317
No description provided.