Skip to content

fix: background tabs survive updates — immediate re-escrow + replay head repair - #310

Merged
arzafran merged 2 commits into
mainfrom
fix/update-resets-background-tabs
Aug 20, 2026
Merged

fix: background tabs survive updates — immediate re-escrow + replay head repair#310
arzafran merged 2 commits into
mainfrom
fix/update-resets-background-tabs

Conversation

@arzafran

Copy link
Copy Markdown
Member

What this does

Fixes this morning's report: after a Programa update, every terminal except the active one came back reset with dead agents, and the reset tabs rendered torn scrollback with literal escape-sequence fragments. Background tabs now keep their live processes across updates, and fallback replays no longer print corrupt heads.

Summary

  • Why agents died: a session revived at restore only re-escrowed when its runtime surface was created, and creation is deferred until the tab is first shown. A tab never viewed during a run held its child's only pty fd in app memory — the next update closed it and SIGHUPed the agent. The production diagnostics log shows it exactly: the 21:05 relaunch revived 8/8 sessions; the next morning's relaunch revived 1/8, and the 7 not_escrowed fallbacks are precisely the panel ids minted by the previous revival.
  • Fix 1: TerminalSurface escrows the revive descriptor's fd at construction; SessionWALStore.stampDeferredReviveEscrow records the escrow facts before full WAL registration; startWriter hydrates durable meta facts from disk instead of clobbering them (also fixes a latent clobber on runtime-surface recreation).
  • Fix 2: preparedText strips orphaned CSI parameter tails (38;114m) from the replay head — WAL rotation/ring cuts can remove the ESC prefix, making the fragment invisible to every existing sanitizer.
  • Observability: escrow.reattach early_reescrow outcome=ok/failed in the release diagnostics log — the next update on a dogfooding machine proves the fix from the log alone.

Review order: commit 1 (TerminalSurface/SessionWALStore), then commit 2 (replay head repair, self-contained).

Test Plan

  • Unit tests green: SessionWALDeferredReviveEscrowTests, ScrollbackSeedOrphanedHeadTests
  • Manual: update-relaunch twice in a row without clicking background tabs; second relaunch shows outcome=revived for all tabs in ~/Library/Logs/Programa/diagnostics.log
  • Manual: fallback-restored tab shows clean scrollback head (no literal NN;NNm fragments)

…e the next update

A session revived at restore only re-escrowed when its runtime surface was
created, and surface creation is deferred until the tab's view is in a window
(TerminalSurface.attachSurface: view.window == nil -> defer). A revived panel
in a tab never shown during that app run held its child's only pty fd in app
memory; the next update closed it and SIGHUPed the agent. Production
diagnostics show it exactly: the 2026-08-19 21:05 relaunch revived 8/8
sessions, the 2026-08-20 11:26 relaunch revived 1/8 — the other 7 fell back
not_escrowed, and their session ids are precisely the panel ids minted by the
previous night's revival. One update of protection, then death.

Three changes:
- TerminalSurface escrows the revive descriptor's fd at construction (dup +
  hand to the holder), not at realization. hasAttemptedSessionEscrow keeps
  the realization-path escrow one-shot.
- SessionWALStore.stampDeferredReviveEscrow records the escrow facts
  (escrowed/socketPath/token/childPID) before the WAL writer's full
  registration exists, creating the session dir + meta.json if needed —
  reattach's guard requires all four fields.
- startWriter hydrates durable meta facts from an existing meta.json instead
  of clobbering them, so the eventual full registration (tab shown later, or
  runtime-surface recreation) preserves what the stamp wrote. This also fixes
  a latent clobber: any surface recreation previously wiped escrow state from
  meta.json.

Observability: escrow.reattach early_reescrow outcome=ok/failed in the
release diagnostics log.
The WAL byte stream can begin mid-escape-sequence — log rotation and ring
overruns cut at byte boundaries. When the cut lands inside a CSI sequence the
surviving tail ("38;114m") has no ESC byte, so positioningSanitizedText and
the ANSI-safe truncation (which only guards its own length-cap cut, and only
runs above the cap) both pass it through, and it renders literally at the head
of every fallback-restored terminal — the torn rendering in today's
update-reset report. preparedText now strips a bare parameter-tail head
(requires a ; or ? in the fragment so prose like "1m 30s" and "42x42"
survives) before the rest of the pipeline.
@arzafran
arzafran merged commit 08095b8 into main Aug 20, 2026
10 checks passed
@arzafran
arzafran deleted the fix/update-resets-background-tabs branch August 20, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant