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
prompt.ts — SessionSummary.summarize() is called fire-and-forget at step 1 of the processor
summarize → computeDiff → snapshot.diffFull(from, to)
diffFull calls formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) synchronously for each changed file
structuredPatch with Number.MAX_SAFE_INTEGER context lines blocks the main thread indefinitely on sessions with many file modifications
- 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
- Have a session with many file modifications (e.g. an agent session that edited 10+ files)
- Fork that session
- Resend the last message
- The TUI freezes immediately and never recovers
Environment
- Linux x86_64, Bun
- OpenCode versions after commit
b7fab49b6 (post v1.3.3)
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 thediffnpm package, called synchronously insideSnapshot.diffFull().Call chain
prompt.ts—SessionSummary.summarize()is called fire-and-forget at step 1 of the processorsummarize→computeDiff→snapshot.diffFull(from, to)diffFullcallsformatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))synchronously for each changed filestructuredPatchwithNumber.MAX_SAFE_INTEGERcontext lines blocks the main thread indefinitely on sessions with many file modificationsFUTEX_WAITEvidence
FUTEX_WAIT, 0% CPUSessionSummary.summarize()inprompt.ts→ fixes the hangformatPatch(structuredPatch(...))withgit diffsubprocess → fixes the hangdiffpackage) → fixes the hangRegression Commit
Identified via
git bisect(9 steps betweenv1.3.3and HEAD):b7fab49b6—refactor(snapshot): store unified patches in file diffs (#21244)Parent commit
463318486is the last good commit.Proposed Fix
Replace the synchronous
formatPatch(structuredPatch(...))call with agit diffsubprocess:This runs in a subprocess (non-blocking), is faster for large diffs, and eliminates the problematic
diffpackage dependency for this code path.Reproduction
Environment
b7fab49b6(post v1.3.3)