Skip to content

TUI hangs indefinitely when resuming sessions with large file diffs (formatPatch/structuredPatch blocks event loop) #23362

Description

@sim590

Description

When resuming certain sessions that contain many file modifications and resending a message, the TUI freezes completely (no response to ESC or any input). The hang is deterministic and reproducible.

Root Cause

The hang is caused by formatPatch(structuredPatch(..., { context: Number.MAX_SAFE_INTEGER })) from the diff npm package, called synchronously inside Snapshot.diffFull().

Call chain

  1. prompt.ts — SessionSummary.summarize() is called fire-and-forget at step 1 of the processor
  2. summarize → computeDiff → snapshot.diffFull(from, to)
  3. diffFull calls formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) synchronously for each changed file
  4. structuredPatch with Number.MAX_SAFE_INTEGER context lines blocks the main thread indefinitely on sessions with many file modifications
  5. The TUI freezes — the event loop is blocked, all threads end up in FUTEX_WAIT

Evidence

  • strace confirms async deadlock: all threads idle in FUTEX_WAIT, 0% CPU
  • Commenting out SessionSummary.summarize() in prompt.ts → fixes the hang
  • Replacing formatPatch(structuredPatch(...)) with git diff subprocess → fixes the hang
  • Replacing with an inline string template (no diff package) → fixes the hang

Regression Commit

Identified via git bisect (9 steps between v1.3.3 and HEAD):

b7fab49b6 — refactor(snapshot): store unified patches in file diffs (#21244)

Parent commit 463318486 is the last good commit.

Proposed Fix

Replace the synchronous formatPatch(structuredPatch(...)) call with a git diff subprocess:

const diff = yield* git(
  [...quote, ...args(["diff", "--no-ext-diff", "--no-renames", from, to, "--", ...run.map((r) => r.file)])],
  { cwd: state.directory },
)
const patches = new Map<string, string>()
if (diff.code === 0 && diff.text.trim()) {
  const parts = diff.text.split(/^(?=diff --git )/m)
  for (const part of parts) {
    if (!part.trim()) continue
    const match = part.match(/^diff --git a\/(.+?) b\//)
    if (match) patches.set(match[1], part.trim())
  }
}

This runs in a subprocess (non-blocking), is faster for large diffs, and eliminates the problematic diff package dependency for this code path.

Reproduction

  1. Have a session with many file modifications (e.g. an agent session that edited 10+ files)
  2. Fork that session
  3. Resend the last message
  4. The TUI freezes immediately and never recovers

Environment

  • Linux x86_64, Bun
  • OpenCode versions after commit b7fab49b6 (post v1.3.3)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions