route DaemonClient through a DaemonTransport seam (T-331)

The UI's backend client connected straight to the workspace unix
socket, hard-coding the local shape. It now talks JSON-lines through a
DaemonTransport (new lib/src/ipc/transport.dart, Flutter-free), with
LocalSocketTransport reproducing today's connect byte-for-byte — zero
behavior change, proven by the untouched client test suite plus new
seam tests driving the client over an in-memory transport.

This is the slot the SSH-remote backend (T-329/Q-23) plugs into:
request correlation, reconnect/backoff, and event forwarding live
above the seam and won't change when the endpoint is remote.
main.dart's swapIpcServer becomes swapBackend per the same plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 03:04:58 +02:00
co-authored by Claude Fable 5
parent c3dd1d3e3f
commit 051ceea3b2
10 changed files with 311 additions and 59 deletions
+10 -10
View File
@@ -132,8 +132,8 @@ Future<void> main() async {
McpServer? mcpServer;
final ipcLog = Logger();
// IPC-server swaps must run one-at-a-time — see the swapIpcServer wrapper
// below doSwapIpcServer for why. (T-352)
// Backend swaps must run one-at-a-time — see the swapBackend wrapper
// below doSwapBackend for why. (T-352)
Future<void> swapChain = Future<void>.value();
// Teardown of the service set behind the currently-served dispatcher
@@ -142,7 +142,7 @@ Future<void> main() async {
// workspace's watchers into the new one's bus (T-367).
Future<void> Function()? activeSubsystemTeardown;
Future<void> doSwapIpcServer(DaemonDispatcher dispatcher, Future<void> Function() teardown, Directory workRoot) async {
Future<void> doSwapBackend(DaemonDispatcher dispatcher, Future<void> Function() teardown, 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
@@ -216,8 +216,8 @@ Future<void> main() async {
// 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<void> swapIpcServer(DaemonDispatcher dispatcher, Future<void> Function() teardown, Directory workRoot) {
final next = swapChain.then((_) => doSwapIpcServer(dispatcher, teardown, workRoot));
Future<void> swapBackend(DaemonDispatcher dispatcher, Future<void> Function() teardown, Directory workRoot) {
final next = swapChain.then((_) => doSwapBackend(dispatcher, teardown, workRoot));
// A failed swap must not break the chain for the next one.
swapChain = next.catchError((Object _) {});
return next;
@@ -350,20 +350,20 @@ Future<void> main() async {
final workRoot = startupWorkRoot;
final (dispatcher, teardown) = buildDispatcher(events, toolchain, workRoot, arrangement, panels);
// Build the client at the workspace's socket path. The
// server is started below (swapIpcServer) which the
// server is started below (swapBackend) which the
// client will then auto-connect to via its reconnect
// loop. autoStartDaemonClient:false means we own the
// lifecycle here.
final client = DaemonClient(socketPath: workspaceSocketPath(workRoot.path), log: log, events: events);
final client = DaemonClient.unixSocket(socketPath: workspaceSocketPath(workRoot.path), log: log, events: events);
ipcClient = client;
// start() synchronously marks the client "connecting" (so
// requests issued during the startup window park for the
// socket instead of failing) and arms the reconnect loop.
// swapIpcServer then binds the server and reconnectAt makes
// swapBackend then binds the server and reconnectAt makes
// the connect immediate. _connect's already-connected guard
// keeps these two paths from opening a second socket.
unawaited(client.start());
unawaited(swapIpcServer(dispatcher, teardown, workRoot));
unawaited(swapBackend(dispatcher, teardown, workRoot));
return client;
},
onProjectOpen: kIsWeb
@@ -374,7 +374,7 @@ Future<void> main() async {
final panels = kernelPanels;
if (bus == null || arrangement == null || panels == null) return;
final (dispatcher, teardown) = buildDispatcher(bus, toolchain, Directory(path), arrangement, panels);
await swapIpcServer(dispatcher, teardown, Directory(path));
await swapBackend(dispatcher, teardown, Directory(path));
},
);
// Expose the reader nav to the `clide status` snapshot (T-221). Boot