fix: harden slash-command push and tasks-index sync against transient failures#25
Conversation
… failures - io.ts: sendAvailableCommandsDeferred now retries at 50/300/1000ms so the notification survives slow client warm-up (Zed drops it if the session view is not ready by the first 50ms fire); failures are warn()'d not swallowed. - tasks-index.ts: upsertSessionTask retries on SQLITE_BUSY contention with the App's Electron host; failures surface via warn() instead of being hidden behind ZCODE_ACP_DEBUG. - tasks-index.test.ts: regression tests for busy retry + terminal failure.
There was a problem hiding this comment.
Code Review
This pull request introduces retry logic for SQLite operations in tasks-index.ts to handle database contention (SQLITE_BUSY/SQLITE_LOCKED) and updates sendAvailableCommandsDeferred to send multiple deferred notifications to accommodate slow client warm-up. The review feedback highlights a potential race condition in the deferred notifications that could overwrite newer commands, which can be resolved by tracking and clearing active timeouts. Additionally, the reviewer suggests refining the SQLite busy error detection regex to avoid false positives on file paths, and extracting a shared withRetry helper to deduplicate and apply retry logic consistently across both task upserts and session title updates.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Address gemini-code-assist review feedback: 1. io.ts: track per-session deferred timeouts in a module-level Map so a newer sendAvailableCommandsDeferred call cancels still-pending timers from a prior call. Prevents a stale command list from overwriting the newest one when session/new → resume → load fire in rapid succession. 2. tasks-index.ts: extract withSqliteRetry helper shared by upsertSessionTask and updateSessionTitle. Both write to tasks-index.sqlite and are equally exposed to SQLITE_BUSY contention with the App's Electron host — previously only upsert retried. 3. tasks-index.ts: tighten isBusyError regex from /\b(busy|locked)\b/i to /database is (busy|locked)/i. The original could false-positive on a filesystem path containing those words (e.g. /Users/busy_bee/). Verified the message format against a real node:sqlite v22 throw: 'Error: database is locked', code ERR_SQLITE_ERROR. 4. tasks-index.ts: handle SQLITE_BUSY thrown at connection open time, not just during prepare/run. The constructor can throw when the DB is locked at open, so the retry loop wraps new Sqlite() inside try. Also adds a regression test for updateSessionTitle's busy retry path.
|
Thanks @gemini-codeassist for the review — all four points addressed in commit
All checks pass: |
Root cause
Two unrelated intermittent bugs reported by the user.
Bug 1: slash commands occasionally not shown
sendAvailableCommandsDeferredfiredavailable_commands_updateexactly once aftersetTimeout(50ms). Zed initialises the session view onNewSessionResponse; if the view wasn't ready by the 50ms mark, the notification was dropped and the/completion menu stayed empty.session/resumeandsession/loadgo through the same deferred path. The "restart fixes it" symptom matches: the secondsession/newhits a warmed-up client where 50ms is enough.Bug 2: sessions intermittently fail to sync to the ZCode App UI
newSession'supsertSessionTaskis fire-and-forget (void, not awaited). The App's Electron host also writes totasks-index.sqlite, so contended writes can surface asSQLITE_BUSYeven with the 5s timeout — and the failure was swallowed by a quietlog()(only visible underZCODE_ACP_DEBUG=1), making it look random.Fix
src/handlers/io.tssendAvailableCommandsDeferrednow fires the notification three times (50ms / 300ms / 1000ms) instead of once.available_commands_updateis overwrite-semantics (not additive), so repeats are harmless — the client keeps the latest snapshot. Each timer is stillunref()'d. Failures are nowwarn()'d instead of silently swallowed.src/tasks-index.tsupsertSessionTaskwraps the write in a busy-retry loop (up to 3 attempts, 200ms / 400ms backoff).isBusyErrormatches "busy" / "locked" in the error message (covers SQLITE_BUSY code 5 and SQLITE_LOCKED code 6 without reaching into unstable error-code properties). Terminal failure is nowwarn()'d (user-perceivable — the session is invisible in the App UI) rather than hidden behindZCODE_ACP_DEBUG.updateSessionTitle's catch is upgraded towarn()for the same reason.All retries are self-contained inside
tasks-index.ts; thevoid upsertSessionTask(...)call site insession.tsis unchanged.Tests
tests/tasks-index.test.tsgains 2 cases (10 → 12):retries on SQLITE_BUSY and writes once contention clears— fake DB throws busy twice, then succeeds; asserts the row is written.gives up and returns false when busy persists past retries— all attempts busy; assertsfalsereturn, no row, and the skipped message surfaces on stderr.Verification
pnpm typecheck✅pnpm test✅ 274/274 passedpnpm lint✅ 0 errors (remaining warnings are pre-existing sort-imports)pnpm prettier --writeon the 3 changed files ✅Out of scope
session/fork(src/index.ts:133) also misses bothsendAvailableCommandsDeferredandupsertSessionTask. Per the user's report it's a secondary path, so left untouched in this PR — easy 5-line follow-up if needed.