Skip to content

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

@sim590

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.tsSessionSummary.summarize() is called fire-and-forget at step 1 of the processor
  2. summarizecomputeDiffsnapshot.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):

b7fab49b6refactor(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)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions