Skip to content

TUI input thread freezes for 5-10 s while Zed editor-context poll waits on SQLite lock #28123

Description

@pgilmore

Description

Summary

opencode's Zed editor-context fallback (introduced in #24352, refined in #24656) opens Zed's SQLite database on the TUI's main thread and polls it at ~1 Hz. When Zed is running concurrently, Zed holds the WAL lock and opencode's read enters SQLite's busy_handler exponential-backoff loop. Because that loop runs on the same thread that does epoll_wait on stdin, keystrokes are not read for the duration of the backoff (~5 s with default busy_timeout). After the timeout, the subsequent pread64() on the WAL file faults pages in from disk → folio_wait_bit_common → another 1-4 s of D-state. End-to-end, the TUI becomes unresponsive in bursts of 5-10 s, intermittently, whenever Zed happens to be active.

This affects any user who runs opencode in a terminal while Zed is open in the same workspace (a very common setup, since the feature exists specifically to bridge them).

Versions

  • opencode 1.15.4
  • Zed 1.1.8
  • Linux 6.8.0 (Ubuntu 24.04.4) — Zed's DB lives on a bind-mounted host filesystem; same behavior reproduces on a plain ext4 mount
  • bun (embedded in opencode bundle)

Reproduction

  1. Open Zed on a workspace and edit a file (so Zed is regularly writing WAL).
  2. In any terminal, launch opencode in the same or a sibling project.
  3. Type continuously. Within minutes, observe sporadic 5-10 s freezes where keystrokes stop being echoed/processed.

Reproduces 100% on Linux when both are open and Zed is actively writing.

Evidence

strace -p <opencode-main-tid> -tt -T -y excerpt during a freeze:

17:54:49.160653 clock_nanosleep(CLOCK_REALTIME, 0, {tv_nsec=175071000}, NULL) = 0 <0.175283>
17:54:49.337335 clock_nanosleep(CLOCK_REALTIME, 0, {tv_nsec=180336000}, NULL) = 0 <0.180516>
17:54:49.519029 clock_nanosleep(CLOCK_REALTIME, 0, {tv_nsec=185679000}, NULL) = 0 <0.185891>
17:54:49.706627 clock_nanosleep(CLOCK_REALTIME, 0, {tv_nsec=191100000}, NULL) = 0 <0.191308>
17:54:50.030317 pread64(43</home/pgilmore/.local/share/zed/db/0-stable/db.sqlite-wal>,
                       ..., 4120, 2146552) = 4120 <0.000124>

The 175ms → 180 → 185 → 191 sequence is SQLite's busy_handler exponential backoff against a contended WAL lock — happening on opencode's main thread, which is the same thread blocked on epoll_wait(stdin). Over 34 s of trace, that thread made 22,352 syscalls against db.sqlite-wal.

Concurrent per-200ms thread-state sampling caught the main thread (TID 5415) in uninterruptible disk wait for 4.2 s straight while waiting for the page-cache fault:

17:51:46.326 → 17:51:50.537  TID 5415  state=D  wchan=folio_wait_bit_common

Both stalls — the busy-wait sleep loop and the D-state read — happen on the input thread, which is why keystrokes are dropped.

Root cause

The bundled code at packages/opencode/src/cli/cmd/tui/context/editor-zed.ts opens Zed's SQLite db with bun:sqlite and runs queries synchronously from what appears to be the TUI's main fiber. Bundled excerpt:

function RD() {
  return [
    process.env.OPENCODE_ZED_DB,
    path.join(os.homedir(), "Library/Application Support/Zed/db/0-stable/db.sqlite"),
    path.join(os.homedir(), ".local/share/zed/db/0-stable/db.sqlite"),
  ].filter(Boolean).find(p => fs.statSync(p)?.isFile?.());
}

…called on every poll tick. The query path opens the live Zed db (no ?mode=ro or PRAGMA query_only), which means Zed's exclusive WAL lock blocks it.

Workaround

Setting OPENCODE_ZED_DB to a path of a quiet, empty SQLite file (or any file that isn't a real db) sidesteps the bug — the query throws no such table, opencode's try/catch returns {type:"unavailable"}, no contention. The Zed integration is effectively disabled. There's currently no documented config-file or CLI flag to disable this feature; the env var is the only knob.

Suggested fixes (any one would resolve it)

  1. Open the db with mode=ro&immutable=1 (or the bun:sqlite equivalent). Zed-side WAL writes won't contend with an immutable reader. Plus a short busy_timeout (e.g. 50 ms) so even unexpected contention can't stall the input thread.
  2. Move the poll off the main fiber. Run it in a worker; post the result back via a channel. The TUI's input loop must never wait on file I/O.
  3. Skip the SQLite fallback when the primary websocket bridge is connected. PR feat(tui): read Zed editor context from state db #24352 describes the SQLite path as a fallback for when the websocket is unavailable, but the strace shows the poll runs continuously regardless. A "websocket connected → don't poll SQLite" gate would eliminate the issue for the common case (Zed running with the websocket bridge).
  4. Add a documented opt-out. A config key like experimental.zedEditorContext: false would let users disable this without relying on the undocumented OPENCODE_ZED_DB env var.

Happy to test any candidate fix on the reproducer.

Plugins

None

OpenCode version

1.15.4

Steps to reproduce

Prerequisites

  • Linux (kernel ≥ 5.x). Should also reproduce on macOS via the Library/Application Support/Zed/... path, but I've only verified Linux.
  • opencode ≥ 1.15 installed.
  • Zed installed (any recent version; tested with 1.1.8).
  • strace, awk, grep for objective verification.

Steps

  1. Start Zed and start writing.

zed ~/somedir & # open any directory in Zed
In Zed, open a file and begin editing — keep typing/saving so Zed is actively writing to its WAL.

  1. In a separate terminal, launch opencode in any project and start an interaction.

cd ~/some-other-project
opencode
Then type a prompt and press enter (so opencode has an active session that wants editor context).

  1. Confirm opencode has opened Zed's db.

OC_PID=$(pgrep -f "opencode/bin/opencode" | head -1)
ls -l /proc/$OC_PID/fd 2>/dev/null | grep -i 'zed/db.*sqlite'
Expected: at least one fd pointing at ~/.local/share/zed/db/0-stable/db.sqlite (or the macOS path).

  1. Watch the main thread state for D-state (uninterruptible disk wait) and SQLite busy-loop sleeps.

In a third terminal, run this sampler — it samples opencode's main thread state every 200 ms:

OC_PID=$(pgrep -f "opencode/bin/opencode" | head -1)
while kill -0 "$OC_PID" 2>/dev/null; do
ts=$(date +%H:%M:%S.%3N)
state=$(awk '{s=$0; sub(/.*) /,"",s); print substr(s,1,1)}' /proc/$OC_PID/stat 2>/dev/null)
wchan=$(cat /proc/$OC_PID/wchan 2>/dev/null)
printf '%s %s %s\n' "$ts" "$state" "$wchan"
sleep 0.2
done | tee /tmp/oc-sampler.log

  1. In opencode, keep typing. Within a few minutes of normal use you should see runs of consecutive samples where the main thread is in one of:
  • D folio_wait_bit_common — kernel waiting for file pages (the pread64 on the WAL).
  • S hrtimer_nanosleep — SQLite's busy_handler exponential backoff.

The freeze visible to the user (keystrokes stop being processed) lines up exactly with these runs. Single freeze events typically span 5-10 s.

  1. (Optional) Strace the main thread to see the syscalls directly.

sudo strace -p $OC_PID -tt -T -y -s 200 -e trace=pread64,clock_nanosleep,nanosleep
-o /tmp/oc.strace
Run for ~60 s, then:

How many syscalls hit Zed's WAL during the trace?

grep -c 'zed/db/0-stable/db.sqlite-wal' /tmp/oc.strace

The SQLite busy-handler backoff pattern (growing sleeps in the 100-200ms range):

grep clock_nanosleep /tmp/oc.strace | grep -oE 'tv_nsec=[0-9]+' | head -20

Expected: many thousands of reads against db.sqlite-wal over a minute, and a series of clock_nanosleep calls whose tv_nsec grows geometrically (e.g. 175000000 → 180000000 → 185000000 →
191000000 → …) — SQLite's default busy-handler.

Workaround that confirms the diagnosis

Restart opencode with the env var pointed at an empty file:

touch /.opencode-zed-stub.sqlite
OPENCODE_ZED_DB=
/.opencode-zed-stub.sqlite opencode

Re-run the sampler. With this in place: no D folio_wait_bit_common, no hrtimer_nanosleep runs, no perceptible freezes — even with Zed actively writing in the background. This isolates
the cause to the Zed-db poll specifically.

Screenshot and/or share link

No response

Operating System

Ubuntu 24.04 LTS

Terminal

Superset / Tilix

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