Skip to content

fix: stop terminal resize loop on dead shell session#2095

Merged
charlesvien merged 2 commits into
mainfrom
posthog-code/fix-terminal-resize-loop-on-dead-session
May 7, 2026
Merged

fix: stop terminal resize loop on dead shell session#2095
charlesvien merged 2 commits into
mainfrom
posthog-code/fix-terminal-resize-loop-on-dead-session

Conversation

@posthog
Copy link
Copy Markdown
Contributor

@posthog posthog Bot commented May 7, 2026

Problem

The renderer would freeze when a task tab containing a finished action-setup-* shell stayed open. Symptoms in the renderer logs:

[error] (terminal-manager) Failed to sync initial terminal size: TRPCClientError: Shell session action-setup-...-0 not found
[error] (terminal-manager) Failed to resize shell: TRPCClientError: Shell session action-setup-...-0 not found
ResizeObserver loop completed with undelivered notifications.

The pattern: setup script finishes, the pty is gone on the main side, but the renderer keeps the TerminalInstance around so its scrollback buffer remains visible. TerminalManager.handleExit only emitted an "exit" event — it never reset instance.isReady or disconnected the ResizeObserver set up in attach(). The observer's callback only guards on instance.isReady, so every layout shift fires trpcClient.shell.resize against the dead session, the call rejects, the catch logs, the failed call mutates layout, and the observer fires again. Tight enough to wedge the renderer.

Changes

In TerminalManager.handleExit:

  • set instance.isReady = false so the resize guard short-circuits
  • disconnect the ResizeObserver and null it out

The instance itself is still kept until destroy() so the buffer stays visible.

How did you test this?

  • Confirmed the call sites: Terminal.tsx invokes terminalManager.handleExit from the shell.onExit tRPC subscription, which is exactly when the main process emits the exit. After this fix, the renderer-side state for that session is consistent with the main side.
  • Reasoned through attach() for re-mount: if the user re-runs the setup, initializeSession will set isReady = true again and attach() creates a fresh ResizeObserver, so resizing still works.
  • biome check passes on the file.

Publish to changelog?

no

🤖 Agent context

Diagnosed from a renderer log dump showing tight repetition of Failed to resize shell: Shell session ... not found for an action-setup-* session id. Cross-referenced handleExit (no isReady reset, no observer disconnect) with the attach() ResizeObserver callback (only guards on instance.isReady). The fix was scoped to the smallest change that breaks the feedback loop without altering buffer-retention behavior.


Created with PostHog Code

When a shell pty exits, TerminalManager.handleExit only emitted an "exit"
event — it never reset instance.isReady or disconnected the
ResizeObserver. The instance is kept around so the buffer remains
viewable, but the next layout shift caused the ResizeObserver to fire
shell.resize against a session the main process had already discarded.
That call rejects, the catch logs, the failed call updates layout, and
the observer fires again. Tight enough to wedge the renderer when the
tab stays attached after a setup script finishes.

Set isReady to false and disconnect the ResizeObserver in handleExit so
no further resize calls go out for the dead session. The instance
itself is still kept until destroy() so the terminal buffer remains
visible.

Generated-By: PostHog Code
Task-Id: 39eb3a58-c962-49af-a1ed-2c621ba04d6e
@pauldambra pauldambra marked this pull request as ready for review May 7, 2026 20:41
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 7, 2026

Reviews (1): Last reviewed commit: "fix: stop terminal resize loop on dead s..." | Re-trigger Greptile

Copy link
Copy Markdown
Member

@pauldambra pauldambra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated comment by QA Swarm — not written by a human

Multi-perspective review by qa-team (frontend + reliability + 2 generalists), paul-reviewer, xp-reviewer. See inline comments and the summary comment below.

Comment thread apps/code/src/renderer/features/terminal/services/TerminalManager.ts Outdated
Comment thread apps/code/src/renderer/features/terminal/services/TerminalManager.ts Outdated
@pauldambra
Copy link
Copy Markdown
Member

Note

🤖 Automated comment by QA Swarm — not written by a human

Multi-perspective review: qa-team (frontend + reliability + 2 generalists), paul-reviewer, xp-reviewer

Verdict: 💬 APPROVE WITH NITS

The fix is correct and well-described. The diagnosis is exemplary — the commit message walks the next maintainer through the feedback loop precisely, and the diff does what it claims. No CRITICAL/HIGH findings were surfaced. The MEDIUM items are about hardening (extract a helper, lock the fix in with a test, future-proof a flag overload) and a pre-existing race that this PR doesn't introduce or worsen.

Key findings

🟡 MEDIUM

  • Triplicated observer-disconnect snippetif (instance.resizeObserver) { … disconnect(); … = null; } now appears in attach(), detach(), and handleExit(). ThreeStrikesAndYouRefactor — extract a disconnectResizeObserver(instance) helper. (qa-team/generalist-a + paul + xp)
  • No tests for TerminalManager — this regression is small and unit-testable; lock the fix in. (qa-team/generalist-a + qa-team/generalist-b + xp)
  • Pre-existing race in initializeSession — if a shell exits during startup, init resolves after handleExit and unconditionally sets isReady = true, fires one shell.resize against the dead session, and emits ready. Not introduced here, but worth a follow-up. (qa-team/reliability + qa-team/generalist-b)

🟢 LOW

  • isReady is now overloaded to mean both "session initialized" and "session still alive." A dedicated isExited / isAlive field communicates intent more precisely and protects against silent regression. (qa-team/generalist-a + qa-team/reliability)
  • Comment vs. ComposedMethod — the new comment earns its keep, but a markSessionDead(instance) helper would let it shrink or vanish. (qa-team/generalist-a + paul + xp)
  • handleExit doing two things — teardown + emit. Still small and legible, but pairing the helper above makes the shape explicit. (paul + xp)

NIT / out-of-scope observations (rolled up here rather than inline)

  • onData has the same shape of bug — typing into a tab whose pty has exited still hits trpcClient.shell.write.mutate and logs "Failed to write to shell" per keystroke (renderer line 195-203). Now that isReady is the canonical "backend is alive" signal, gating onData on it would close the matching half. (paul)
  • Observability — the loop was previously visible via log.error("Failed to resize shell") per fire; that signal goes away. The exit event presumably gets handled upstream, just sanity-checking we haven't muted the only smoke alarm for shell-exited-while-attached. (paul)
  • Belt-and-braces — setting isReady = false AND disconnecting the observer are both useful (the flag also guards the initializeSession race). Two locks on the same door, intentionally. (xp)

Convergence

Finding Reviewers Confidence
Extract disconnectResizeObserver helper qa-team/generalist-a, paul, xp High
Add a unit test for the loop fix qa-team/generalist-a, qa-team/generalist-b, xp High
Comment vs. ComposedMethod qa-team/generalist-a, paul, xp High
initializeSession race (pre-existing) qa-team/reliability, qa-team/generalist-b Medium
isReady overloading qa-team/generalist-a, qa-team/reliability Medium
handleExit doing two things paul, xp Medium

Reviewer summaries

Reviewer Assessment
🔍 qa-team LOW base risk per scoring rules, two MEDIUM items (pre-existing race, missing tests). Fix is correct; re-attach after exit is safe because handleResize is gated on isReady.
👤 paul "diagnosis is clean and the commit message is excellent. main thing nagging me is that onData has the same 'writes to a dead session and logs forever' problem — feels like that handler should consult isReady too. ship as you see fit."
📐 xp "Smell is duplication — third strike on the resize-observer dance. Extract a helper, then markSessionDead(instance); this.emit('exit', ...) reads at one level of abstraction. Bigger latent issue is the absence of any tests for TerminalManager. Ship the fix, then refactor."

Automated by QA Swarm — not a human review

Three call sites in TerminalManager were repeating the same observer
disconnect dance: attach(), detach(), and the new handleExit() block
from the prior commit. Extract a private disconnectResizeObserver helper
and call it from all three. Behaviour is unchanged.

Motivated by qa-swarm feedback (3 reviewers converged on this dedup).

Generated-By: PostHog Code
Task-Id: c406af31-2216-42c6-a282-7fd75721a14d
@pauldambra pauldambra added the Stamphog This will request an autostamp by stamphog on small changes label May 7, 2026
@pauldambra pauldambra requested a review from a team May 7, 2026 21:04
@charlesvien charlesvien merged commit 87d2269 into main May 7, 2026
15 checks passed
@charlesvien charlesvien deleted the posthog-code/fix-terminal-resize-loop-on-dead-session branch May 7, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Stamphog This will request an autostamp by stamphog on small changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants