Canvas layout status sticks on "Not saved — keep this tab open" on a real server #395

Open
opened 2026-11-06 12:23:02 +00:00 by kayg · 14 comments
Owner

J305 fix job.

Problem. The canvas layout-save status "Not saved — keep this tab open" sticks on a real server.

  • J299 on test.ssh.kayg.org (74b9c4d80, #305 comment 82162): while a refused-shell window is open, the notice stays for at least 20 s and clears when that window closes (compute01:~claude/.cache/codex-scratch/j299/pending.log).
  • J298 (#389 comment 82186): real/stale-note-257-review fails on clean origin/dev on the same notice once its missing watchLayout call is patched; later reruns of real/stale-screen-d309-review.spec.ts hit it too (compute01:~claude/j298-real*.log).

Scope.

  1. Find why the save fails or stalls while a window is refused / ended / reattaching (server rejects? client retries wrongly? status never clears?).
  2. Test first: a real-server spec reproducing the stuck status, plus a server test if the server rejects the save.
  3. Fix the cause (do not hide the notice).
  4. Add the missing watchLayout to real/stale-note-257-review.spec.ts.

Branch fix-layout-not-saved from origin/dev. Gate: verify quick, then affected real specs 5× with one worker.

J305 fix job. **Problem.** The canvas layout-save status "Not saved — keep this tab open" sticks on a real server. - J299 on test.ssh.kayg.org (74b9c4d80, #305 comment 82162): while a refused-shell window is open, the notice stays for at least 20 s and clears when that window closes (`compute01:~claude/.cache/codex-scratch/j299/pending.log`). - J298 (#389 comment 82186): `real/stale-note-257-review` fails on clean origin/dev on the same notice once its missing `watchLayout` call is patched; later reruns of `real/stale-screen-d309-review.spec.ts` hit it too (`compute01:~claude/j298-real*.log`). **Scope.** 1. Find why the save fails or stalls while a window is refused / ended / reattaching (server rejects? client retries wrongly? status never clears?). 2. Test first: a real-server spec reproducing the stuck status, plus a server test if the server rejects the save. 3. Fix the cause (do not hide the notice). 4. Add the missing `watchLayout` to `real/stale-note-257-review.spec.ts`. Branch `fix-layout-not-saved` from origin/dev. Gate: `verify quick`, then affected real specs 5× with one worker.
Author
Owner

J305 start. Branch fix-layout-not-saved, base origin/dev 7b14fbb664cbfb9b2b9b8a56f724d924424accdb.

First lead (code reading). sync.ts marks every terminal window without a sessionId as unresolved; LayoutJournal.next() holds that window's open/split intent (and so everything queued behind it) until the session id arrives. A refused shell never gets one, so its intent waits forever and store.pending stays true. Closing the window cancels the unsent birth in capture(), which matches J299's "clears when that window closes". J298's sticking happens in enrol() on the first fresh shell, with an older server binary (2be58ce2c); that may be a second cause, to be reproduced on a dev-built server.

Plan (~3 h).

  1. Build a dev musl server + tracer on compute01 and reproduce both sightings with real specs (refused shell; the 257/d309 enrol path). 45 min.
  2. Red tests: a real spec for the refused window, plus unit tests on the store/journal; a server test if the server rejects anything. 45 min.
  3. Fix the cause (a refused window with no session leaves the saved layout instead of blocking the queue, if that is what it is), add watchLayout to the 257 spec. 45 min.
  4. verify quick, affected real specs 5× with one worker, evidence here. 45 min.
J305 start. Branch `fix-layout-not-saved`, base origin/dev `7b14fbb664cbfb9b2b9b8a56f724d924424accdb`. **First lead (code reading).** `sync.ts` marks every terminal window without a `sessionId` as *unresolved*; `LayoutJournal.next()` holds that window's `open`/`split` intent (and so everything queued behind it) until the session id arrives. A refused shell never gets one, so its intent waits forever and `store.pending` stays true. Closing the window cancels the unsent birth in `capture()`, which matches J299's "clears when that window closes". J298's sticking happens in `enrol()` on the first fresh shell, with an older server binary (2be58ce2c); that may be a second cause, to be reproduced on a dev-built server. **Plan (~3 h).** 1. Build a dev musl server + tracer on compute01 and reproduce both sightings with real specs (refused shell; the 257/d309 enrol path). 45 min. 2. Red tests: a real spec for the refused window, plus unit tests on the store/journal; a server test if the server rejects anything. 45 min. 3. Fix the cause (a refused window with no session leaves the saved layout instead of blocking the queue, if that is what it is), add `watchLayout` to the 257 spec. 45 min. 4. `verify quick`, affected real specs 5× with one worker, evidence here. 45 min.
Author
Owner

Findings (evidence on compute01). Two separate causes. Neither is the server refusing a save: every canvas.layout.apply in the traces got ok=true.

1. Refused shell (J299): the client held the save forever. A new terminal window has no session id until its shell starts. sync.ts marks it unresolved, and LayoutJournal.next() holds its open intent, and every edit behind it, until the id arrives. A refused shell never gets an id, so store.pending stayed true and the status stayed up. Closing the window cancelled the unsent birth, so it cleared then. The server was never asked.

  • Red on origin/dev tracer + dev server (~claude/j305-red.log): the extended real/rootless-refusal.spec.ts fails at both viewports with Expected: 0 / Received: 1 for the notice, 10 s after the refusal.
  • Fix (140548589): an in-memory openRefused flag on the window model. A refused window stays on screen with its strip but leaves the saved document, so nothing waits. A later successful shell is saved as an ordinary birth. Unit test in sync.test.ts.

2. J298's enrol failures: not a stuck save, a doubled notice card. With watchLayout patched in, the 257 spec failed 5/10 on origin/dev (~claude/j298-real* equivalent: ~claude/j305-red257.log). A diagnostic (~claude/j305-diag.log, 6/6 failing) shows every layout call confirmed (pending: [], revision 3) at the failure. The toBeHidden() throws at once on a strict-mode violation: "resolved to 2 elements". At sign-in, three edits land within ~500 ms, so the status shows, hides, shows. mountToast.show() only reused live cards, so each re-show built a second card beside the one still in its 190 ms exit: two identical notices stacked.

  • Fix (78e6cb914): show() takes a leaving card with the same key back (exit cancelled, open transition reverses). Unit test in toast-stack.test.ts. Red before the fix: Expected length: 1, Received length: 2.

Also d7474aff3: watchLayout in stale-note-257-review.spec.ts.

Not changed: the status still appears for every edit while it waits for its receipt (D312). Delaying it by a round trip would reduce flicker at 300 ms RTT, but that's the owner's call. Question for kayg: should "Not saved — keep this tab open" wait about one round trip before it shows? I'd recommend yes, since an edit that's confirmed in one RTT isn't really "not saved". Safe default: unchanged.

**Findings (evidence on compute01).** Two separate causes. Neither is the server refusing a save: every `canvas.layout.apply` in the traces got `ok=true`. **1. Refused shell (J299): the client held the save forever.** A new terminal window has no session id until its shell starts. `sync.ts` marks it *unresolved*, and `LayoutJournal.next()` holds its `open` intent, and every edit behind it, until the id arrives. A refused shell never gets an id, so `store.pending` stayed true and the status stayed up. Closing the window cancelled the unsent birth, so it cleared then. The server was never asked. - Red on origin/dev tracer + dev server (`~claude/j305-red.log`): the extended `real/rootless-refusal.spec.ts` fails at both viewports with `Expected: 0 / Received: 1` for the notice, 10 s after the refusal. - Fix (140548589): an in-memory `openRefused` flag on the window model. A refused window stays on screen with its strip but leaves the saved document, so nothing waits. A later successful shell is saved as an ordinary birth. Unit test in `sync.test.ts`. **2. J298's enrol failures: not a stuck save, a doubled notice card.** With `watchLayout` patched in, the 257 spec failed 5/10 on origin/dev (`~claude/j298-real*` equivalent: `~claude/j305-red257.log`). A diagnostic (`~claude/j305-diag.log`, 6/6 failing) shows every layout call confirmed (`pending: []`, revision 3) at the failure. The `toBeHidden()` throws at once on a strict-mode violation: "resolved to 2 elements". At sign-in, three edits land within ~500 ms, so the status shows, hides, shows. `mountToast.show()` only reused *live* cards, so each re-show built a second card beside the one still in its 190 ms exit: two identical notices stacked. - Fix (78e6cb914): `show()` takes a leaving card with the same key back (exit cancelled, open transition reverses). Unit test in `toast-stack.test.ts`. Red before the fix: `Expected length: 1, Received length: 2`. Also d7474aff3: `watchLayout` in `stale-note-257-review.spec.ts`. Not changed: the status still appears for every edit while it waits for its receipt (D312). Delaying it by a round trip would reduce flicker at 300 ms RTT, but that's the owner's call. **Question for kayg:** should "Not saved — keep this tab open" wait about one round trip before it shows? I'd recommend yes, since an edit that's confirmed in one RTT isn't really "not saved". Safe default: unchanged.
Author
Owner

Real-server evidence, 5× with one worker (compute01; server = musl release built from this branch at d7474aff3..78e6cb914, whose server code is identical to origin/dev; tracer built at 78e6cb914). Command: playwright test -c playwright.real.config.ts --workers=1 --repeat-each=5 real/rootless-refusal.spec.ts real/stale-note-257-review.spec.ts real/stale-screen-d309-review.spec.ts real/layout-per-tab.spec.ts. Log: compute01:~claude/j305-green5.log.

  ✓   3 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (8.7s)
  ✓  21 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (7.6s)
  ✓  39 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (12.6s)
  ✓  57 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (7.8s)
  ✓  75 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (9.5s)
  90 passed (20.1m)
REAL_EXIT=0

That's 18 tests × 5: rootless refusal desktop/phone, all 10 cases of the 257 spec, both d309 tests, and the 4 layout-per-tab tests (which assert exactly one notice). Every one passed 5/5, with no flaky retries (retries: 0).

Before (origin/dev 7b14fbb66 tracer, same server):

  • ~claude/j305-red.log: rootless refusal 2/2 failed, with the notice still visible 10 s after the refusal (Expected: 0, Received: 1).
  • ~claude/j305-red257.log: 257 spec with watchLayout, 5/10 failed (strict mode violation … resolved to 2 elements).
  • ~claude/j305-diag.log: diagnostic, 6/6 failed with every layout call confirmed.

Since then, the branch merged origin/dev 48358a5fa (tracer change: only canvas/windows/frame.ts) and added 9358d72b4 (doc comments only).

Screenshots: refused window after the fix, desktop and phone, with no "Not saved" notice (attached).

**Real-server evidence, 5× with one worker** (compute01; server = musl release built from this branch at d7474aff3..78e6cb914, whose server code is identical to origin/dev; tracer built at 78e6cb914). Command: `playwright test -c playwright.real.config.ts --workers=1 --repeat-each=5 real/rootless-refusal.spec.ts real/stale-note-257-review.spec.ts real/stale-screen-d309-review.spec.ts real/layout-per-tab.spec.ts`. Log: `compute01:~claude/j305-green5.log`. ``` ✓ 3 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (8.7s) ✓ 21 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (7.6s) ✓ 39 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (12.6s) ✓ 57 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (7.8s) ✓ 75 real/layout-per-tab.spec.ts:103:1 › failed logout preserves the signed-in queue; successful logout invalidates peers and late completions (9.5s) 90 passed (20.1m) REAL_EXIT=0 ``` That's 18 tests × 5: rootless refusal desktop/phone, all 10 cases of the 257 spec, both d309 tests, and the 4 layout-per-tab tests (which assert exactly one notice). Every one passed 5/5, with no flaky retries (`retries: 0`). **Before (origin/dev 7b14fbb66 tracer, same server):** - `~claude/j305-red.log`: rootless refusal 2/2 failed, with the notice still visible 10 s after the refusal (`Expected: 0, Received: 1`). - `~claude/j305-red257.log`: 257 spec with `watchLayout`, 5/10 failed (`strict mode violation … resolved to 2 elements`). - `~claude/j305-diag.log`: diagnostic, 6/6 failed with every layout call confirmed. Since then, the branch merged origin/dev 48358a5fa (tracer change: only `canvas/windows/frame.ts`) and added 9358d72b4 (doc comments only). Screenshots: refused window after the fix, desktop and phone, with no "Not saved" notice (attached).
Author
Owner

J305 done. Branch fix-layout-not-saved, head 9358d72b4686bcc9e5d17f9536dc2604a037c5c4 (pushed, not merged).

Commits on top of origin/dev:

  • d7474aff3 test(tracer): install the layout observer in the #257 real spec
  • 140548589 fix(canvas): a refused shell's window no longer holds layout saves
  • 78e6cb914 fix(ui): a toast shown again while leaving reuses its card
  • 5eabddb8e merge origin/dev 48358a5fa
  • 9358d72b4 docs(canvas): doc comments for WindowModel and createDocumentSync (D345)

Cause, short version: the server never refused a save. (1) Refused shell: the client held the window's birth forever, waiting for a session id that never comes. (2) J298's enrol failures: a doubled notice card during the sign-in burst of edits, which tripped Playwright's strict mode. Details are in comment 82289. No server test: the server isn't involved, and every apply in the traces came back ok=true.

scripts/verify fast on compute01 at 9358d72b46 (log compute01:~claude/j305-fast.log, FAST_EXIT=0):

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| migration names           | PASS         |
| migration tooling tests   | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| Traefik component manifest | PASS         |
| release component publication | PASS         |
| test                      | PASS         |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| privileged integration    | SKIP         |
| probe release exclusion   | PASS         |
| nightly fuzz reporter     | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+

==> Compiler metadata generator checks

Static sizes (report only): x86_64 27777072 B, aarch64 24994464 B. This branch changes no Rust code, so the delta against origin/dev is 0. The "baseline" deltas in the log are against the old D243 baseline.

verify quick before that: all 10 rows PASS (~claude/j305-quick.log). The first quick run failed fmt on 5 server files that were also unformatted on unmodified origin/dev 7b14fbb66; merging dev 48358a5fa cleared it.

Real specs, 5×, one worker: 90/90 passed (comment 82364, ~claude/j305-green5.log).

Open question for kayg (safe default: unchanged): should "Not saved — keep this tab open" wait about one round trip before it shows, so an edit confirmed within one RTT never shows it? Today it shows on every edit, briefly (D312).

Not done: J299's exact flow on test.ssh.kayg.org was not rerun. That needs this branch deployed there. The rootless real spec reproduces the same state locally (red before the fix, green 5/5 after).

Cleanup: target dir and the baseline worktree on compute01 deleted. Logs and evidence kept in ~claude/j305-*.log and ~claude/j305-evidence/.

**J305 done.** Branch `fix-layout-not-saved`, head `9358d72b4686bcc9e5d17f9536dc2604a037c5c4` (pushed, not merged). Commits on top of origin/dev: - d7474aff3 test(tracer): install the layout observer in the #257 real spec - 140548589 fix(canvas): a refused shell's window no longer holds layout saves - 78e6cb914 fix(ui): a toast shown again while leaving reuses its card - 5eabddb8e merge origin/dev 48358a5fa - 9358d72b4 docs(canvas): doc comments for WindowModel and createDocumentSync (D345) **Cause, short version:** the server never refused a save. (1) Refused shell: the client held the window's birth forever, waiting for a session id that never comes. (2) J298's enrol failures: a doubled notice card during the sign-in burst of edits, which tripped Playwright's strict mode. Details are in comment 82289. No server test: the server isn't involved, and every apply in the traces came back `ok=true`. **`scripts/verify fast`** on compute01 at 9358d72b4686bcc9e5d17f9536dc2604a037c5c4 (log `compute01:~claude/j305-fast.log`, FAST_EXIT=0): ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | Nix Cargo vendoring | PASS | | fmt | PASS | | migration names | PASS | | migration tooling tests | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | Traefik component manifest | PASS | | release component publication | PASS | | test | PASS | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | privileged integration | SKIP | | probe release exclusion | PASS | | nightly fuzz reporter | PASS | | static builds | PASS | +---------------------------+--------------+ ==> Compiler metadata generator checks ``` Static sizes (report only): x86_64 27777072 B, aarch64 24994464 B. This branch changes no Rust code, so the delta against origin/dev is 0. The "baseline" deltas in the log are against the old D243 baseline. `verify quick` before that: all 10 rows PASS (`~claude/j305-quick.log`). The first quick run failed `fmt` on 5 server files that were also unformatted on unmodified origin/dev 7b14fbb66; merging dev 48358a5fa cleared it. **Real specs, 5×, one worker:** 90/90 passed (comment 82364, `~claude/j305-green5.log`). **Open question for kayg** (safe default: unchanged): should "Not saved — keep this tab open" wait about one round trip before it shows, so an edit confirmed within one RTT never shows it? Today it shows on every edit, briefly (D312). **Not done:** J299's exact flow on test.ssh.kayg.org was not rerun. That needs this branch deployed there. The rootless real spec reproduces the same state locally (red before the fix, green 5/5 after). Cleanup: target dir and the baseline worktree on compute01 deleted. Logs and evidence kept in `~claude/j305-*.log` and `~claude/j305-evidence/`.
Author
Owner

J305 D358 progress. Branch head 06c53cb40 (one commit on top of the dev merge 279484834, force-pushed while amending it).

  • Unit tests (pending-notice.test.ts): 5/5 pass. Red before: the module was missing.
  • Double spec test/pending-notice.spec.ts. Red on the old code (~claude/j305-d358-red.log): the notice flashed on a normal resize (Received: 1), and with a held save it showed after 60 ms. Green after (~claude/j305-d358-green.log): 2/2.
  • verify quick: QUICK_EXIT=0 (~claude/j305-quick.log).
  • Real set, once, one worker (~claude/j305-final2.log): 25 passed, 5 failed.
    • Four failures predate D358. They fail identically on 279484834, the branch head without D358 (~claude/j305-pred358.log): layout-phone "does not move or animate", both layout-session-boundary tests, and layout-session-incarnation. On origin/dev 37c93637e they fail earlier, on the doubled-card strict-mode error this branch fixes (~claude/j305-basecheck.log). These are separate, previously hidden failures and are not touched here.
    • layout-phone "10 desktop windows" passes without D358 but fails with it. Cause 1 is in the test helper: settled() used the notice as its "something still waits" signal, which D358 delays. That's fixed in the commit: settled() now waits until the server's copy holds every drawn window. One window still stays unconfirmed for over 15 s after a split, and I'm diagnosing why (~claude/j305-diag2.log).
J305 D358 progress. Branch head `06c53cb40` (one commit on top of the dev merge 279484834, force-pushed while amending it). - Unit tests (`pending-notice.test.ts`): 5/5 pass. Red before: the module was missing. - Double spec `test/pending-notice.spec.ts`. Red on the old code (`~claude/j305-d358-red.log`): the notice flashed on a normal resize (`Received: 1`), and with a held save it showed after 60 ms. Green after (`~claude/j305-d358-green.log`): 2/2. - `verify quick`: QUICK_EXIT=0 (`~claude/j305-quick.log`). - Real set, once, one worker (`~claude/j305-final2.log`): 25 passed, 5 failed. - Four failures predate D358. They fail identically on 279484834, the branch head without D358 (`~claude/j305-pred358.log`): layout-phone "does not move or animate", both layout-session-boundary tests, and layout-session-incarnation. On origin/dev 37c93637e they fail earlier, on the doubled-card strict-mode error this branch fixes (`~claude/j305-basecheck.log`). These are separate, previously hidden failures and are not touched here. - layout-phone "10 desktop windows" passes without D358 but fails with it. Cause 1 is in the test helper: `settled()` used the notice as its "something still waits" signal, which D358 delays. That's fixed in the commit: `settled()` now waits until the server's copy holds every drawn window. One window still stays unconfirmed for over 15 s after a split, and I'm diagnosing why (`~claude/j305-diag2.log`).
Author
Owner

J305: compute01 maintenance (02:15–02:45 IST, 10 → 16 vCPUs). My queued diagnostic ~claude/j305-diag2.sh never got a slot, and the restart will cut it. That's not a test result. Nothing of mine was running. I'll resubmit once, in the foreground, after "compute01 back" appears on #402. Pending: why one split window stays out of the server's layout copy in layout-phone "10 desktop windows" (comment 82895).

J305: compute01 maintenance (02:15–02:45 IST, 10 → 16 vCPUs). My queued diagnostic `~claude/j305-diag2.sh` never got a slot, and the restart will cut it. That's not a test result. Nothing of mine was running. I'll resubmit once, in the foreground, after "compute01 back" appears on #402. Pending: why one split window stays out of the server's layout copy in layout-phone "10 desktop windows" (comment 82895).
Author
Owner

J305: the restart (02:43 IST) cut the earlier queued diagnostic j305-diag2, which never ran. Resubmitted once at 02:46 IST (~claude/j305-diag2.log).

J305: the restart (02:43 IST) cut the earlier queued diagnostic `j305-diag2`, which never ran. Resubmitted once at 02:46 IST (`~claude/j305-diag2.log`).
Author
Owner

J305 D358 done. Branch fix-layout-not-saved, head 06c53cb4054637c78af2c42a6981aa2f70a3ff4c (pushed, not merged). One new commit, 06c53cb40, on top of the merge of origin/dev 20d169210 (279484834).

What it does. "Not saved — keep this tab open" shows only when an edit is still unsaved after about 1 s, and stays until every waiting edit is saved (D358). canvas/pending-notice.ts arms a timer only while an edit waits and the notice is not up yet, so no timer runs at idle or while the notice shows. The clock starts at the first waiting edit. The double gains POST /__mock/layout/hold. The real specs' settled() helper now waits until every drawn console window is in the server's confirmed layout, because the notice is no longer an instant "still waiting" signal.

Test-first evidence (compute01)

  • Unit, pending-notice.test.ts: 5/5 pass. Red before: module missing.
  • Double, test/pending-notice.spec.ts. Red on the old code (~claude/j305-d358-red.log): the notice flashed on a normal resize (Expected: 0, Received: 1), and a held save showed it after 60 ms (Expected: >= 900). Green (~claude/j305-d358-green.log): 2/2.

verify quick at 06c53cb40 (~claude/j305-quick.log):

| scope guard               | PASS         |
| migration names           | PASS         |
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| ui literals               | PASS         |
| tracer typecheck          | PASS         |
QUICK_EXIT=0

Real specs, once, one worker (~claude/j305-final2.log; server built from this branch): 25 passed, 5 failed. None of the 5 is caused by D358:

  • Four fail identically on 279484834, the branch without D358 (~claude/j305-pred358.log): layout-phone "does not move or animate", both layout-session-boundary tests (disconnect: the offline socket-close count stays 0), and layout-session-incarnation (it expects the "Not saved … were lost" sign-in text but gets "Your sign-in changed …"). On origin/dev 37c93637e all four fail earlier, on the doubled-card strict-mode error that 78e6cb914 fixes (~claude/j305-basecheck.log). So they are older failures that dev was hiding; they need their own look.
  • layout-phone "10 desktop windows": a split's shell is refused with reason unavailable ("… is offline or cannot reach this server") on the local server. That window rightly stays out of the saved layout, so the phone shows fewer windows. This also reproduces 4/4 on the pre-D358 head (~claude/j305-diag2base.log). Opened as #411.
  • Passing: rootless refusal (2), the 257 spec (10), d309 (2), layout-per-tab (4), layout-engines-review, layout-phone at 1 and 4 windows, both refused-split toast tests, warm offline edit, and layout-session-binding-review.

compute01's restart (02:43 IST) cut a queued diagnostic, which was resubmitted once (comment 83013). Cleanup: the target dir, the baseline worktree and the diagnostic specs on compute01 are deleted; logs are kept.

**J305 D358 done.** Branch `fix-layout-not-saved`, head `06c53cb4054637c78af2c42a6981aa2f70a3ff4c` (pushed, not merged). One new commit, 06c53cb40, on top of the merge of origin/dev 20d169210 (279484834). **What it does.** "Not saved — keep this tab open" shows only when an edit is still unsaved after about 1 s, and stays until every waiting edit is saved (D358). `canvas/pending-notice.ts` arms a timer only while an edit waits and the notice is not up yet, so no timer runs at idle or while the notice shows. The clock starts at the first waiting edit. The double gains `POST /__mock/layout/hold`. The real specs' `settled()` helper now waits until every drawn console window is in the server's confirmed layout, because the notice is no longer an instant "still waiting" signal. **Test-first evidence (compute01)** - Unit, `pending-notice.test.ts`: 5/5 pass. Red before: module missing. - Double, `test/pending-notice.spec.ts`. Red on the old code (`~claude/j305-d358-red.log`): the notice flashed on a normal resize (`Expected: 0, Received: 1`), and a held save showed it after 60 ms (`Expected: >= 900`). Green (`~claude/j305-d358-green.log`): 2/2. **`verify quick`** at 06c53cb40 (`~claude/j305-quick.log`): ``` | scope guard | PASS | | migration names | PASS | | Nix Cargo vendoring | PASS | | fmt | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | ui literals | PASS | | tracer typecheck | PASS | QUICK_EXIT=0 ``` **Real specs, once, one worker** (`~claude/j305-final2.log`; server built from this branch): **25 passed, 5 failed.** None of the 5 is caused by D358: - Four fail identically on 279484834, the branch without D358 (`~claude/j305-pred358.log`): layout-phone "does not move or animate", both layout-session-boundary tests (`disconnect`: the offline socket-close count stays 0), and layout-session-incarnation (it expects the "Not saved … were lost" sign-in text but gets "Your sign-in changed …"). On origin/dev 37c93637e all four fail earlier, on the doubled-card strict-mode error that 78e6cb914 fixes (`~claude/j305-basecheck.log`). So they are older failures that dev was hiding; they need their own look. - layout-phone "10 desktop windows": a split's shell is refused with reason `unavailable` ("… is offline or cannot reach this server") on the local server. That window rightly stays out of the saved layout, so the phone shows fewer windows. This also reproduces 4/4 on the pre-D358 head (`~claude/j305-diag2base.log`). Opened as #411. - Passing: rootless refusal (2), the 257 spec (10), d309 (2), layout-per-tab (4), layout-engines-review, layout-phone at 1 and 4 windows, both refused-split toast tests, warm offline edit, and layout-session-binding-review. compute01's restart (02:43 IST) cut a queued diagnostic, which was resubmitted once (comment 83013). Cleanup: the target dir, the baseline worktree and the diagnostic specs on compute01 are deleted; logs are kept.
Author
Owner

J331 review of fix-layout-not-saved @ 06c53cb4054637c78af2c42a6981aa2f70a3ff4c (vs origin/dev fba745051): APPROVE. No P1. Four P2/P3 notes below; none blocks D358. I ran no verify; I read J305's gate output (comment 83050) and ran only the specs my findings needed. Logs are on compute01 ~claude/j331/ (run.log, run2.log, C-*.log, D-*.log, shots/, shots2/).

1. Behaviour of the notice: confirmed

  • Code read of canvas/pending-notice.ts: the timer exists only while pending() is true and the notice is not up, and it starts at the first waiting edit (not re-armed per edit), so a steady stream still warns. When the queue empties, the timer is cleared and the notice hidden. dispose() is wired in both teardown paths in product.ts. At idle nothing runs: update() is a no-op when pending() is false (it is called on each RTT sample via #pendingChanged, but only clears an already-empty timer).
  • Unit tests: pending-notice.test.ts + sync.test.ts 11/11, toast-stack.test.ts 9/9 (run.log, section A).
  • Double spec test/pending-notice.spec.ts ×5: 10/10 passed (run.log, B1).
  • My own harness (test/j331-shots.spec.ts, desktop 1440×900, ink and paper; in-page timestamps in shots2/*.json):
    • a held save shows the notice 1005 ms after pointer-up (paper run: similar); nothing visible at 400 ms; it clears 224–343 ms after the hold is released; shown exactly once;
    • an 80-step, ~2 s continuous drag against a responsive server: 0 flashes;
    • a stream of 6 held edits every ~300 ms: notice shown at 1774 ms from the start of the first edit's drag (the drag itself takes ~650 ms), so about 1.1 s after the first edit. It warns while the queue never empties.

2. settled() helper: not vacuous, but weaker than the notice in two ways (P3)

  • It is not vacuous: the new "drawn windows ⊆ server-confirmed windows" poll is what turns layout-phone "10 desktop windows" red (a refused split's window never reaches the server), so it does catch real failures. pending.length == 0 and the toHaveCount(0) notice check remain.
  • P3-a. The poll is one-directional and only sees sent ops. An edit still queued and unsent (the old notice covered store.pending, which includes those), or a window close not yet confirmed (the window leaves the drawn set but stays in the canonical set), passes settled(). Suggest comparing the drawn set to the canonical set for equality, or exposing store.pending to the helper.
  • P3-b. real/layout-review.spec.ts:22 and real/layout-sync.spec.ts:20 have private copies of settled() that still use toBeHidden() on the notice. That check now passes for the first second whatever is queued. They are still backed by pending.length, so they are not hollow, but the duplicates should use the shared helper.

3. Pre-existing failures (run on origin/dev fba745051, 279484834 = head without D358, and head; C-*.log)

  • layout-phone "does not move or animate": confirmed pre-existing. Same assertion and identical 5-line diff (CSSTransition transform ui-conn-g …) on all three refs.
  • layout-session-incarnation: pre-existing, but not identical on dev. On dev it fails earlier on the doubled-card strict-mode error this branch fixes. On 279484834 and on head it fails with Expected "Not saved. Changes waiting to save were lost…" / Received "Your sign-in changed. Your last changes may not have saved…", which does not involve D358. 852e15b76 made the product say the second text for receipt-confirmed edits and left this spec stale.
  • layout-phone "10 desktop windows" (#411): flaky on every ref, not caused by D358. 3 runs per ref (D-*.log): dev 1 pass / 2 fail (toBeHidden, the stuck notice itself); 279484834 1/2 (Expected: 8 / Received: 7, then Expected: 1 / Received: 0); head 1/2 (the helper's "console windows the server has not confirmed"). (A single earlier run passed on dev and failed on 279484834, which made me suspect 140548589. The repeats show it is flaky on dev too.) The failure mode does differ: on dev it is the original stuck notice, on the branch it is the refused window left out of the saved layout (by design, 140548589).
  • P2 (merge hygiene, not blocking). Merging leaves 5 real specs red (4 old + #411). Each is tracked or pre-existing. Make sure #411 and a stale-spec issue for incarnation/boundary/phone-animate exist so they are not forgotten.

4. Screenshots (production tracer build, test double, /__mock/layout/hold)

Attached: notice up, ink and paper, at 1440×900, 393×852 and 375×667, plus one "400 ms after the edit" capture with no notice.

  • TASTE checks: it uses the shared toast in its standard place (no rogue notification); icon and text share a centre line (also in the two-line 375 wrap); left edges match the stacked toast beneath it on phones; contrast fine in both themes; safe area respected (bottom bar clear). Desktop: the toast sits inside the focused window's lower edge, as every toast does.
  • P3-c (cosmetic). At 375 px the message wraps to two lines ("Not saved — keep this / tab open") while at 393 px it fits on one. Worth a look at the toast's horizontal padding, not a D358 matter.
  • On the faint "400 ms" phone capture the card is already fading in. A 3× phone screenshot takes several hundred ms, so that is not proof of an early show; the in-page timestamps above (desktop) are. I did not get in-page timestamps for the phone runs (see below).

5. Gap in what I could prove (P2, needs the author)

My phone captures use canvas:split:down on the test double. There the new windows never get a session ("The terminal session ended." ×4, windows stuck connecting), and after I released the hold the notice stayed for over 10 s (all four phone runs failed my "clears" assertion; the notice was correctly shown first). This is probably the double not starting split shells, and it is no worse than dev. But it is the same class as #395: a new window whose shell ends (not refused) before it reports a session id still holds its birth, so the notice stays. 140548589 only handles the refusal path. Please confirm on a real server whether an ended-at-start shell can clear itself; if not, it needs openRefused-style handling too (or a follow-up issue).

Also, J305's comments 82895 and 83050 say layout-phone "10 desktop windows" passes without D358 and also "reproduces 4/4 on the pre-D358 head". It is flaky (see §3); the wording should say that.

**J331 review of `fix-layout-not-saved` @ `06c53cb4054637c78af2c42a6981aa2f70a3ff4c` (vs origin/dev fba745051): APPROVE.** No P1. Four P2/P3 notes below; none blocks D358. I ran no `verify`; I read J305's gate output (comment 83050) and ran only the specs my findings needed. Logs are on compute01 `~claude/j331/` (`run.log`, `run2.log`, `C-*.log`, `D-*.log`, `shots/`, `shots2/`). ## 1. Behaviour of the notice: confirmed - Code read of `canvas/pending-notice.ts`: the timer exists only while `pending()` is true and the notice is not up, and it starts at the first waiting edit (not re-armed per edit), so a steady stream still warns. When the queue empties, the timer is cleared and the notice hidden. `dispose()` is wired in both teardown paths in `product.ts`. At idle nothing runs: `update()` is a no-op when `pending()` is false (it is called on each RTT sample via `#pendingChanged`, but only clears an already-empty timer). - Unit tests: `pending-notice.test.ts` + `sync.test.ts` 11/11, `toast-stack.test.ts` 9/9 (`run.log`, section A). - Double spec `test/pending-notice.spec.ts` ×5: 10/10 passed (`run.log`, B1). - My own harness (`test/j331-shots.spec.ts`, desktop 1440×900, ink and paper; in-page timestamps in `shots2/*.json`): - a held save shows the notice **1005 ms after pointer-up** (paper run: similar); nothing visible at 400 ms; it clears 224–343 ms after the hold is released; shown exactly once; - an 80-step, ~2 s continuous drag against a responsive server: 0 flashes; - a stream of 6 held edits every ~300 ms: notice shown at 1774 ms from the start of the first edit's drag (the drag itself takes ~650 ms), so about 1.1 s after the first edit. It warns while the queue never empties. ## 2. `settled()` helper: not vacuous, but weaker than the notice in two ways (P3) - It is not vacuous: the new "drawn windows ⊆ server-confirmed windows" poll is what turns layout-phone "10 desktop windows" red (a refused split's window never reaches the server), so it does catch real failures. `pending.length == 0` and the `toHaveCount(0)` notice check remain. - **P3-a.** The poll is one-directional and only sees *sent* ops. An edit still queued and unsent (the old notice covered `store.pending`, which includes those), or a window close not yet confirmed (the window leaves the drawn set but stays in the canonical set), passes `settled()`. Suggest comparing the drawn set to the canonical set for equality, or exposing `store.pending` to the helper. - **P3-b.** `real/layout-review.spec.ts:22` and `real/layout-sync.spec.ts:20` have private copies of `settled()` that still use `toBeHidden()` on the notice. That check now passes for the first second whatever is queued. They are still backed by `pending.length`, so they are not hollow, but the duplicates should use the shared helper. ## 3. Pre-existing failures (run on origin/dev fba745051, 279484834 = head without D358, and head; `C-*.log`) - **layout-phone "does not move or animate": confirmed pre-existing.** Same assertion and identical 5-line diff (`CSSTransition transform ui-conn-g …`) on all three refs. - **layout-session-incarnation: pre-existing, but not identical on dev.** On dev it fails earlier on the doubled-card strict-mode error this branch fixes. On 279484834 and on head it fails with `Expected "Not saved. Changes waiting to save were lost…" / Received "Your sign-in changed. Your last changes may not have saved…"`, which does not involve D358. `852e15b76` made the product say the second text for receipt-confirmed edits and left this spec stale. - **layout-phone "10 desktop windows" (#411): flaky on every ref, not caused by D358.** 3 runs per ref (`D-*.log`): dev 1 pass / 2 fail (`toBeHidden`, the stuck notice itself); 279484834 1/2 (`Expected: 8 / Received: 7`, then `Expected: 1 / Received: 0`); head 1/2 (the helper's "console windows the server has not confirmed"). (A single earlier run passed on dev and failed on 279484834, which made me suspect `140548589`. The repeats show it is flaky on dev too.) The failure mode does differ: on dev it is the original stuck notice, on the branch it is the refused window left out of the saved layout (by design, `140548589`). - **P2 (merge hygiene, not blocking).** Merging leaves 5 real specs red (4 old + #411). Each is tracked or pre-existing. Make sure #411 and a stale-spec issue for incarnation/boundary/phone-animate exist so they are not forgotten. ## 4. Screenshots (production tracer build, test double, `/__mock/layout/hold`) Attached: notice up, ink and paper, at 1440×900, 393×852 and 375×667, plus one "400 ms after the edit" capture with no notice. - TASTE checks: it uses the shared toast in its standard place (no rogue notification); icon and text share a centre line (also in the two-line 375 wrap); left edges match the stacked toast beneath it on phones; contrast fine in both themes; safe area respected (bottom bar clear). Desktop: the toast sits inside the focused window's lower edge, as every toast does. - **P3-c (cosmetic).** At 375 px the message wraps to two lines ("Not saved — keep this / tab open") while at 393 px it fits on one. Worth a look at the toast's horizontal padding, not a D358 matter. - On the faint "400 ms" phone capture the card is already fading in. A 3× phone screenshot takes several hundred ms, so that is not proof of an early show; the in-page timestamps above (desktop) are. I did not get in-page timestamps for the phone runs (see below). ## 5. Gap in what I could prove (P2, needs the author) My phone captures use `canvas:split:down` on the test double. There the new windows never get a session ("The terminal session ended." ×4, windows stuck connecting), and after I released the hold the notice stayed for over 10 s (all four phone runs failed my "clears" assertion; the notice was correctly shown first). This is probably the double not starting split shells, and it is no worse than dev. But it is the same class as #395: a new window whose shell *ends* (not *refused*) before it reports a session id still holds its birth, so the notice stays. `140548589` only handles the refusal path. Please confirm on a real server whether an ended-at-start shell can clear itself; if not, it needs `openRefused`-style handling too (or a follow-up issue). Also, J305's comments 82895 and 83050 say layout-phone "10 desktop windows" passes without D358 and also "reproduces 4/4 on the pre-D358 head". It is flaky (see §3); the wording should say that.
Author
Owner

J305 corrections and follow-ups (after J331's review, comment 83291)

Correction: layout-phone "10 desktop windows" (#411) is flaky on every ref. My comments 82895 and 83050 said it passes without D358, and that the refusal "reproduces 4/4 on the pre-D358 head". Both overstate it. J331 ran it 3× per ref: origin/dev, 279484834 (without D358) and 06c53cb40 each gave 1 pass and 2 fails, with different failure modes. My one earlier pre-D358 pass was luck. The 4/4 figure is only about my diagnostic's rapid-split refusal; the spec itself is flaky on every ref. #411 tracks it.

Correction: the four older red specs (layout-phone "does not move or animate", both layout-session-boundary tests, layout-session-incarnation) are J332's, under #411 part B (comments 83062 and 83079; branch fix-layout-real-specs-411). I'm not opening new issues for them.

Follow-ups from J331, not on this branch:

  • P3-a / P3-b: real/layout-review.spec.ts:22 and real/layout-sync.spec.ts:20 keep private copies of settled() that still use the notice's toBeHidden(). With D358 that check passes during the first second whatever is queued. Both should use the shared helper. The shared helper also only compares one way and only sees sent edits: an unsent queued edit, or an unconfirmed close, passes it. It should check the drawn and saved window sets for equality, or read the store's pending state.
  • P3-c: at 375 px the notice wraps to two lines ("Not saved — keep this / tab open"); at 393 px it fits on one. Check the toast's horizontal padding.

J331's P2 (a shell that ends right at start), in progress: on the client, a new shell's session id comes from the ssh.session.open result (terminal.ts:1186), before any attach or exit, so a real shell that exits at once should still resolve. J331's double case looks different: "The terminal session ended." is the double closing the socket, not a shell exit. New real spec real/ended-at-start-layout.spec.ts (ce367bc3a) types exit and exit 3 the instant each split is made. It checks that the layout settles, that the saved layout equals the drawn windows, and that the notice never appears. It's queued on compute01 (build, then 3× one worker, then verify fast on the same head if green): ~claude/j305-ended.log.

**J305 corrections and follow-ups (after J331's review, comment 83291)** **Correction: layout-phone "10 desktop windows" (#411) is flaky on every ref.** My comments 82895 and 83050 said it passes without D358, and that the refusal "reproduces 4/4 on the pre-D358 head". Both overstate it. J331 ran it 3× per ref: origin/dev, 279484834 (without D358) and 06c53cb40 each gave 1 pass and 2 fails, with different failure modes. My one earlier pre-D358 pass was luck. The 4/4 figure is only about my diagnostic's rapid-split refusal; the spec itself is flaky on every ref. #411 tracks it. **Correction: the four older red specs** (layout-phone "does not move or animate", both layout-session-boundary tests, layout-session-incarnation) are J332's, under **#411 part B** (comments 83062 and 83079; branch `fix-layout-real-specs-411`). I'm not opening new issues for them. **Follow-ups from J331, not on this branch:** - **P3-a / P3-b:** `real/layout-review.spec.ts:22` and `real/layout-sync.spec.ts:20` keep private copies of `settled()` that still use the notice's `toBeHidden()`. With D358 that check passes during the first second whatever is queued. Both should use the shared helper. The shared helper also only compares one way and only sees sent edits: an unsent queued edit, or an unconfirmed close, passes it. It should check the drawn and saved window sets for equality, or read the store's pending state. - **P3-c:** at 375 px the notice wraps to two lines ("Not saved — keep this / tab open"); at 393 px it fits on one. Check the toast's horizontal padding. **J331's P2 (a shell that ends right at start), in progress:** on the client, a new shell's session id comes from the `ssh.session.open` result (`terminal.ts:1186`), before any attach or exit, so a real shell that exits at once should still resolve. J331's double case looks different: "The terminal session ended." is the double closing the socket, not a shell exit. New real spec `real/ended-at-start-layout.spec.ts` (ce367bc3a) types `exit` and `exit 3` the instant each split is made. It checks that the layout settles, that the saved layout equals the drawn windows, and that the notice never appears. It's queued on compute01 (build, then 3× one worker, then `verify fast` on the same head if green): `~claude/j305-ended.log`.
Author
Owner

J305 progress on J331's P2 (a shell that ends at start), branch head 825b1255d (the spec commit, amended):

  • Run 1 (~claude/j305-ended.log): 3/3 red on the spec itself. exit was typed before the new window had an input, so it reached no shell.
  • Run 2 (~claude/j305-ended2.log): exit typed into the new window's input. 1/3 passed the whole flow: every window saved, the saved layout equal to the drawn windows, and the notice never shown. The other 2 failed only on the spec's assumption that exit always closes its window. When the exit races the attach, the window may stay open, ended.
  • Run 3 (~claude/j305-ended3.log): 3/3 red on sequencing. The next split started from the window that had just exited and was still closing.
  • The spec now refocuses the original live shell before each split, finds the new window by id, accepts "closed or ended" for exit, and keeps the strict checks: the layout settles, saved equals drawn, and the notice never appears. Requeued (~claude/j305-ended4.log), with verify fast in the same slot if green.

So far nothing points at a save that fails to resolve on the real server: every failure was the spec's own step, and the one complete run passed.

J305 progress on J331's P2 (a shell that ends at start), branch head `825b1255d` (the spec commit, amended): - Run 1 (`~claude/j305-ended.log`): 3/3 red on the spec itself. `exit` was typed before the new window had an input, so it reached no shell. - Run 2 (`~claude/j305-ended2.log`): `exit` typed into the new window's input. **1/3 passed the whole flow:** every window saved, the saved layout equal to the drawn windows, and the notice never shown. The other 2 failed only on the spec's assumption that `exit` always closes its window. When the exit races the attach, the window may stay open, ended. - Run 3 (`~claude/j305-ended3.log`): 3/3 red on sequencing. The next split started from the window that had just exited and was still closing. - The spec now refocuses the original live shell before each split, finds the new window by id, accepts "closed or ended" for `exit`, and keeps the strict checks: the layout settles, saved equals drawn, and the notice never appears. Requeued (`~claude/j305-ended4.log`), with `verify fast` in the same slot if green. So far nothing points at a save that fails to resolve on the real server: every failure was the spec's own step, and the one complete run passed.
Author
Owner

J305: J331's P2 (a shell that ends right at start) on the real server: no bug, no fix needed.

Setup: the release binary built at 06c53cb40 on compute01. A spec splits from the first live shell and types exit / exit 3 into each new window's input the moment it exists. The diagnostic copy also logs layout traffic (~claude/j305-diag3.log).

Result:

  • Every shell that started and then exited got its session id from the open result, before its exit. The window's layout save was sent and confirmed, and the later close of an exited window was saved too. The notice never appeared (toasts: [], observer pending: []). The session id arrives with the ssh.session.open result (terminal.ts:1186), so a real shell can't end before its window knows its session.
  • The only window that never reached the saved layout was a split whose shell open the server refused. It's left out of the saved layout by design (140548589), so it holds nothing. That refusal is #411. Excerpt (ms, run 1):
5007 down Result 14 ok=true {"account":"claude","fallback":false,"note":"","requested_account":"claude","session_id":"4e22c90e-b0ea-41fc-a34a-0d912a4cd713"}
5299 down Result 17 ok=true {"account":"claude","fallback":false,"note":"","requested_account":"claude","session_id":"c1261736-4e22-4002-bfc5-ccd8fbc39f0d"}
5558 up Call 20 canvas.layout.apply {"session_incarnation":"O-y-ReFlN9U6o64LPdgB8q20A7EvJlmGuwFVs6DuxX4","op_id":"18022978-30ce-4933-904b-0a2992550ac1","base_revision":5,"intent":{"type":"close","wind
5687 down Result 21 ok=true {"account":"claude","fallback":false,"note":"","requested_account":"claude","session_id":"30a545ce-381f-4dce-85cd-6bdd25286fce"}
5966 down Result 24 ok=false {"error":"terminal unavailable","reason":"unavailable"}
6048 down Result 31 ok=false {"error":"terminal unavailable","reason":"unavailable"}
6237 up Call 32 canvas.layout.apply {"session_incarnation":"O-y-ReFlN9U6o64LPdgB8q20A7EvJlmGuwFVs6DuxX4","op_id":"8f17c4fd-8338-4020-9521-7be26c6e7705","base_revision":13,"intent":{"type":"close","win
  • J331's double case is a different path: "The terminal session ended." is the double closing the socket (plainCloseReason), not a shell exit.

Seen along the way, not fixed here:

  • In 1 of about 16 attempts, exit 3 typed into a brand-new window's input the moment it appeared never ran; the shell stayed live (~claude/j305-diag3.log, run 2). That suggests keys typed during attach can be lost. A follow-up candidate; I haven't investigated it.
  • A clean exit racing the attach leaves the window "ended" for a moment before its close arrives (Calls 20 and 32 above). That's fine.

Branch: the spec (real/ended-at-start-layout.spec.ts) can't pass reliably while #411's refusals and that typing race remain. I moved it off this branch to j305-ended-at-start-spec (fe00e3edd), for #411 / J332 to land once refusals are fixed. fix-layout-not-saved is back at the approved 06c53cb4054637c78af2c42a6981aa2f70a3ff4c (force-pushed; it never left my branch), and verify fast is queued there in the foreground (~claude/j305-fast2.log). Earlier runs: ~claude/j305-ended{,2,3,4,5}.log, each red on the spec's own steps or on #411.

**J305: J331's P2 (a shell that ends right at start) on the real server: no bug, no fix needed.** Setup: the release binary built at 06c53cb40 on compute01. A spec splits from the first live shell and types `exit` / `exit 3` into each new window's input the moment it exists. The diagnostic copy also logs layout traffic (`~claude/j305-diag3.log`). **Result:** - Every shell that started and then exited got its session id from the open result, before its exit. The window's layout save was sent and confirmed, and the later close of an exited window was saved too. The notice never appeared (`toasts: []`, observer `pending: []`). The session id arrives with the `ssh.session.open` result (`terminal.ts:1186`), so a real shell can't end before its window knows its session. - The **only** window that never reached the saved layout was a split whose shell open the server **refused**. It's left out of the saved layout by design (140548589), so it holds nothing. That refusal is #411. Excerpt (ms, run 1): ``` 5007 down Result 14 ok=true {"account":"claude","fallback":false,"note":"","requested_account":"claude","session_id":"4e22c90e-b0ea-41fc-a34a-0d912a4cd713"} 5299 down Result 17 ok=true {"account":"claude","fallback":false,"note":"","requested_account":"claude","session_id":"c1261736-4e22-4002-bfc5-ccd8fbc39f0d"} 5558 up Call 20 canvas.layout.apply {"session_incarnation":"O-y-ReFlN9U6o64LPdgB8q20A7EvJlmGuwFVs6DuxX4","op_id":"18022978-30ce-4933-904b-0a2992550ac1","base_revision":5,"intent":{"type":"close","wind 5687 down Result 21 ok=true {"account":"claude","fallback":false,"note":"","requested_account":"claude","session_id":"30a545ce-381f-4dce-85cd-6bdd25286fce"} 5966 down Result 24 ok=false {"error":"terminal unavailable","reason":"unavailable"} 6048 down Result 31 ok=false {"error":"terminal unavailable","reason":"unavailable"} 6237 up Call 32 canvas.layout.apply {"session_incarnation":"O-y-ReFlN9U6o64LPdgB8q20A7EvJlmGuwFVs6DuxX4","op_id":"8f17c4fd-8338-4020-9521-7be26c6e7705","base_revision":13,"intent":{"type":"close","win ``` - J331's double case is a different path: "The terminal session ended." is the double closing the socket (`plainCloseReason`), not a shell exit. **Seen along the way, not fixed here:** - In 1 of about 16 attempts, `exit 3` typed into a brand-new window's input the moment it appeared never ran; the shell stayed live (`~claude/j305-diag3.log`, run 2). That suggests keys typed during attach can be lost. A follow-up candidate; I haven't investigated it. - A clean `exit` racing the attach leaves the window "ended" for a moment before its close arrives (Calls 20 and 32 above). That's fine. **Branch:** the spec (`real/ended-at-start-layout.spec.ts`) can't pass reliably while #411's refusals and that typing race remain. I moved it off this branch to `j305-ended-at-start-spec` (fe00e3edd), for #411 / J332 to land once refusals are fixed. `fix-layout-not-saved` is back at the approved `06c53cb4054637c78af2c42a6981aa2f70a3ff4c` (force-pushed; it never left my branch), and `verify fast` is queued there in the foreground (`~claude/j305-fast2.log`). Earlier runs: `~claude/j305-ended{,2,3,4,5}.log`, each red on the spec's own steps or on #411.
Author
Owner

J305 scripts/verify fast on the approved head, compute01, foreground through build-slot. Log: compute01:~claude/j305-fast2.log.

FAST_HEAD: 06c53cb4054637c78af2c42a6981aa2f70a3ff4c
LAUNCHER: cargo 1.99.0 (5f94df478 2026-08-27)

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| migration names           | PASS         |
| migration tooling tests   | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| Traefik component manifest | PASS         |
| release component publication | PASS         |
| test                      | FAIL (101)   |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| privileged integration    | SKIP         |
| probe release exclusion   | PASS         |
| nightly fuzz reporter     | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+
FAST_EXIT=1

running 1 test
test active_helper_silent_status_retires_after_control_deadline ... FAILED

failures:

---- active_helper_silent_status_retires_after_control_deadline stdout ----

thread 'active_helper_silent_status_retires_after_control_deadline' (3985012) panicked at crates/plugins/desktop/tests/active_control_deadline_review.rs:135:78:
called `Result::unwrap()` on an `Err` value: Error("EOF while parsing a value", line: 1, column: 0)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    active_helper_silent_status_retires_after_control_deadline

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s

PT_INTERP: absent; size: 27782800 bytes; baseline 22418256; delta +5364544 bytes (report only, D243 amended 2026-11-01)
PT_INTERP: absent; size: 24997464 bytes; baseline 20453280; delta +4544184 bytes (report only, D243 amended 2026-11-01)
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

Result: FAIL (101) in the test row only; every other row passes. The one failing test is infraternal-plugin-desktop active_helper_silent_status_retires_after_control_deadline (its helper reply was empty). This branch changes no Rust (git diff origin/dev...06c53cb40 -- crates is empty), and dev's fix for that test (2f254771f) is already in this head. I'm now running that test alone 3× on origin/dev 3644950b8 and 3× on this head, same target layout (~claude/j305-desk.log), to show whether it fails on dev too.

Static sizes (report only): x86_64 27782800 B, aarch64 24997464 B. Since no Rust changed, this branch adds nothing over dev.

**J305 `scripts/verify fast` on the approved head**, compute01, foreground through build-slot. Log: `compute01:~claude/j305-fast2.log`. ``` FAST_HEAD: 06c53cb4054637c78af2c42a6981aa2f70a3ff4c LAUNCHER: cargo 1.99.0 (5f94df478 2026-08-27) +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | Nix Cargo vendoring | PASS | | fmt | PASS | | migration names | PASS | | migration tooling tests | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | Traefik component manifest | PASS | | release component publication | PASS | | test | FAIL (101) | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | privileged integration | SKIP | | probe release exclusion | PASS | | nightly fuzz reporter | PASS | | static builds | PASS | +---------------------------+--------------+ FAST_EXIT=1 running 1 test test active_helper_silent_status_retires_after_control_deadline ... FAILED failures: ---- active_helper_silent_status_retires_after_control_deadline stdout ---- thread 'active_helper_silent_status_retires_after_control_deadline' (3985012) panicked at crates/plugins/desktop/tests/active_control_deadline_review.rs:135:78: called `Result::unwrap()` on an `Err` value: Error("EOF while parsing a value", line: 1, column: 0) note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: active_helper_silent_status_retires_after_control_deadline test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s PT_INTERP: absent; size: 27782800 bytes; baseline 22418256; delta +5364544 bytes (report only, D243 amended 2026-11-01) PT_INTERP: absent; size: 24997464 bytes; baseline 20453280; delta +4544184 bytes (report only, D243 amended 2026-11-01) 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 ``` Result: **FAIL (101)** in the `test` row only; every other row passes. The one failing test is `infraternal-plugin-desktop` `active_helper_silent_status_retires_after_control_deadline` (its helper reply was empty). This branch changes no Rust (`git diff origin/dev...06c53cb40 -- crates` is empty), and dev's fix for that test (2f254771f) is already in this head. I'm now running that test alone 3× on origin/dev 3644950b8 and 3× on this head, same target layout (`~claude/j305-desk.log`), to show whether it fails on dev too. Static sizes (report only): x86_64 27782800 B, aarch64 24997464 B. Since no Rust changed, this branch adds nothing over dev.
Author
Owner

J305: the verify fast failure on 06c53cb40 (comment 84059) is a flake, not this branch.

cargo test -p infraternal-plugin-desktop --features source --test active_control_deadline_review, one run at a time, each running exactly 1 test (active_helper_silent_status_retires_after_control_deadline):

ref host passed failed
head 06c53cb4054637c78af2c42a6981aa2f70a3ff4c dev-02 (aarch64) 10 0
origin/dev 3644950b8e4b5dddeefba350f97a3892189ca193 dev-02 (aarch64) 10 0
head 06c53cb40 compute01 (x86_64) 5 0
origin/dev 3644950b8 compute01 (x86_64) 5 0

Logs: infraternal-dev-02-std:~claude/j305-desk.log, infraternal-dev-01-compute01:~claude/j305-desk2.log. (~claude/j305-desk.log on compute01 ran 0 tests because it lacked --features source; ignore it.)

The branch changes no Rust. The test didn't fail on dev in any run, so per the task no issue opened. One likely cause, for whoever touches the test next: at crates/plugins/desktop/tests/active_control_deadline_review.rs:127-135 the test waits until the helper's received file exists, then parses it at once. A file created but not yet written gives exactly the "EOF while parsing a value, line 1, column 0" seen in the gate, and a loaded host (compute01 during the gate) makes that window wider. Its single failure is in compute01:~claude/j305-fast2.log (lines 2786–2801).

Cleanup: dev-02's worktrees and target dir are deleted, and so are compute01's j305 target and baseline worktree.

**J305: the `verify fast` failure on 06c53cb40 (comment 84059) is a flake, not this branch.** `cargo test -p infraternal-plugin-desktop --features source --test active_control_deadline_review`, one run at a time, each running exactly 1 test (`active_helper_silent_status_retires_after_control_deadline`): | ref | host | passed | failed | |---|---|---|---| | head `06c53cb4054637c78af2c42a6981aa2f70a3ff4c` | dev-02 (aarch64) | **10** | 0 | | origin/dev `3644950b8e4b5dddeefba350f97a3892189ca193` | dev-02 (aarch64) | **10** | 0 | | head 06c53cb40 | compute01 (x86_64) | 5 | 0 | | origin/dev 3644950b8 | compute01 (x86_64) | 5 | 0 | Logs: `infraternal-dev-02-std:~claude/j305-desk.log`, `infraternal-dev-01-compute01:~claude/j305-desk2.log`. (`~claude/j305-desk.log` on compute01 ran 0 tests because it lacked `--features source`; ignore it.) The branch changes no Rust. The test didn't fail on dev in any run, so per the task **no issue opened**. One likely cause, for whoever touches the test next: at `crates/plugins/desktop/tests/active_control_deadline_review.rs:127-135` the test waits until the helper's `received` file *exists*, then parses it at once. A file created but not yet written gives exactly the "EOF while parsing a value, line 1, column 0" seen in the gate, and a loaded host (compute01 during the gate) makes that window wider. Its single failure is in `compute01:~claude/j305-fast2.log` (lines 2786–2801). Cleanup: dev-02's worktrees and target dir are deleted, and so are compute01's j305 target and baseline worktree.
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#395
No description provided.