From 7d951a247f3639e7a4ebb88d41e6e2cd88dd63fc Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 11 Jun 2026 08:44:17 +0200 Subject: [PATCH] main: serialize IPC-server swaps so the repo bind wins (T-352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2.3.1 fix (re-fetch the pql sidebars on ProjectOpened) only helped the picker-first path, where the project opens after the window is up. With sticky-startup the project opens during boot, before the panes mount and subscribe, so they never received the event — the sidebars stayed broken. Root cause is a race in the IPC-server lifecycle. The boot factory fires swapIpcServer(launchCwd) with unawaited(); the project-open flow then fires swapIpcServer(repo). Each swap stops the live server, binds a new one, and reconnects the daemon client. Unserialized, the two interleave and the late-finishing boot swap can clobber the repo bind, reconnecting the client to the launch-CWD (HOME) socket. The daemon's PqlClient (and git/files) then run against the wrong workspace, so the first pql.tickets.list hits a stale/global pql.db and errors ("ticket_deps.blocker_record_id missing — pql.db is from an earlier schema"). A manual refresh worked because by then things had settled. Chain every swap on a serialization Future so they apply in call order; the repo swap is issued last and therefore wins. Kept the pane re-fetch from 2.3.1 — it still covers genuine mid-session project switches. Verified app boot is unaffected (test/app_test.dart green). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 12 ++++++++++++ lib/main.dart | 22 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30dbefcd..44750eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,18 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ## [Unreleased] +### Fixed + +- **Ticket and decision sidebars reliably load on first open (real fix).** The + 2.3.1 re-fetch-on-open helped only when a project is picked *after* the window + is up; with sticky-startup the project opens during boot, before the panes + mount, so they never saw the event. The underlying cause was a race: the boot + IPC-server swap (to the launch CWD) and the project-open swap (to the repo) + ran concurrently, and the late-finishing boot swap could clobber the repo + bind — leaving the daemon's pql/git/files pointed at the launch directory + (HOME) and the sidebars erroring on a stale/global pql.db. Swaps are now + serialized so the repo bind always wins. (T-352) + ## [2.3.1] — 2026-06-10 ### Fixed diff --git a/lib/main.dart b/lib/main.dart index 5a57f695..da24b6e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -116,7 +116,11 @@ Future main() async { McpServer? mcpServer; final ipcLog = Logger(); - Future swapIpcServer(DaemonDispatcher dispatcher, Directory workRoot) async { + // IPC-server swaps must run one-at-a-time — see the swapIpcServer wrapper + // below doSwapIpcServer for why. (T-352) + Future swapChain = Future.value(); + + Future doSwapIpcServer(DaemonDispatcher dispatcher, Directory workRoot) async { if (kIsWeb) return; // Already serving this exact workspace? Reuse the live server. // The startup factory binds the launch CWD, then the project-open @@ -173,6 +177,22 @@ Future main() async { } } + // Serialize IPC-server swaps. The boot factory fires a swap to the launch + // CWD with unawaited(); the project-open flow then fires another to the + // real repo. Unserialized, the two interleave and the late-finishing boot + // swap can clobber the repo bind — reconnecting the daemon client to the + // launch-CWD (HOME) socket, so pql/git/files run against the wrong + // workspace. That surfaced as the ticket/decision sidebars failing on first + // load (stale/global pql.db) yet working after a manual refresh. Chaining + // every swap makes them apply in call order; the repo swap is issued last + // and therefore wins. (T-352) + Future swapIpcServer(DaemonDispatcher dispatcher, Directory workRoot) { + final next = swapChain.then((_) => doSwapIpcServer(dispatcher, workRoot)); + // A failed swap must not break the chain for the next one. + swapChain = next.catchError((Object _) {}); + return next; + } + DaemonDispatcher buildDispatcher( DaemonBus events, Toolchain tc,