diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index 2ab41094..e811a744 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -1860,3 +1860,5 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-126', 'status', 'in_progress', 'done', NULL, '2026-05-18 16:04:29', '2026-05-18 16:04:29', '2026-05-18 16:04:29', NULL, 'b6c783f599585e55ecc36f69ccbcefb1', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-127', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 09:57:18', '2026-05-19 09:57:18', '2026-05-19 09:57:18', NULL, '64a14cb5d8b9274fd9d72d1e7474040c', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-127', 'status', 'in_progress', 'done', NULL, '2026-05-19 10:03:26', '2026-05-19 10:03:26', '2026-05-19 10:03:26', NULL, 'b8bf877d2057f7baa5acf6af7a13d529', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-128', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 10:05:09', '2026-05-19 10:05:09', '2026-05-19 10:05:09', NULL, '763a80ec25b5fe712e5ad44b75dbb22f', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-128', 'status', 'in_progress', 'done', NULL, '2026-05-19 10:06:46', '2026-05-19 10:06:46', '2026-05-19 10:06:46', NULL, 'cc53b46764f28515d735aa77c8eeef2c', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index babb841d..b85b966c 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -2144,3 +2144,15 @@ Acceptance: 5. Falls back to in-process direct dispatch if the perf delta is unacceptable; surface the finding in a Q-record before doing so. Source: T-99 sketch. Depends on T-124 (server must exist). Blocks T-128 (legacy IPC cleanup).', 'done', 'high', NULL, NULL, NULL, '2026-05-18 11:59:02', '2026-05-19 10:03:26', NULL, 'af4a706880181411b2aecf51d90ea748', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-128', 'task', 'T-99', 'delete IsolateClient + Backend + backend_entry.dart; collapse service registration', 'Fifth slice of T-99(a). Pure cleanup once T-127 lands. + +Remove the third unused IPC path entirely. Today three implementations coexist: DaemonClient (socket — gets a real server in T-124), InProcessClient (deleted in T-127), and IsolateClient + Backend + backend_entry.dart (a backend isolate path that was never wired through). After T-127, none of those should still be referenced. + +Also: consolidate the two duplicate service-wiring sites into one (currently main.dart''s buildDispatcher and the kernel facade both register subsystem handlers; one should own it). + +Acceptance: +1. lib/kernel/src/ipc/isolate_client.dart, lib/src/daemon/backend.dart, lib/src/daemon/backend_entry.dart removed. +2. Single service-registration site; no duplicate registerPaneCommands / registerFilesCommands / etc. across files. +3. flutter analyze + full test suite green. + +Source: T-99 sketch. Depends on T-127.', 'done', 'medium', NULL, NULL, NULL, '2026-05-18 11:59:06', '2026-05-19 10:06:46', NULL, '481c51b17b027b5a04279a0ef6a3a029', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index da073752..10e7f708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. - `lib/kernel/src/ipc/in_process.dart` (`InProcessClient`) — replaced by the socket-loopback `DaemonClient` (T-127). +- `lib/kernel/src/backend.dart`, `lib/kernel/src/backend_entry.dart`, + `lib/kernel/src/ipc/isolate_client.dart` — the third unused IPC + path (a backend-isolate model that was never wired through). Only + the socket model survives now (T-128). ### Fixed diff --git a/lib/kernel/src/backend.dart b/lib/kernel/src/backend.dart deleted file mode 100644 index be242d55..00000000 --- a/lib/kernel/src/backend.dart +++ /dev/null @@ -1,138 +0,0 @@ -/// Manages the backend isolate lifecycle. -/// -/// Two-phase boot: -/// 1. [Backend.spawn] — starts the isolate, resolves toolchain (binary checks only). -/// 2. [Backend.openProject] — initializes services for a specific project root. -library; - -import 'dart:async'; -import 'dart:isolate'; - -import 'package:clide/kernel/src/backend_entry.dart'; -import 'package:clide/kernel/src/ipc/isolate_client.dart'; -import 'package:clide/kernel/src/toolchain.dart'; - -class Backend { - Backend._({ - required this.client, - required this.toolchain, - required SendPort backendRequestPort, - required Isolate isolate, - required ReceivePort receivePort, - }) : _backendRequestPort = backendRequestPort, - _isolate = isolate, - _receivePort = receivePort; - - final IsolateClient client; - final Toolchain toolchain; - final SendPort _backendRequestPort; - final Isolate _isolate; - final ReceivePort _receivePort; - - Completer? _projectCompleter; - final Map> _validateCompleters = {}; - int _validateId = 0; - - /// Spawn the backend isolate. Returns when the toolchain is resolved. - /// No services are active yet — call [openProject] to activate. - static Future spawn({ - required IsolateClient Function(SendPort backendPort) clientFactory, - String? hintRoot, - }) async { - final receivePort = ReceivePort(); - final completer = Completer(); - - late final IsolateClient client; - late final Isolate isolate; - late final SendPort backendRequestPort; - late final Backend backend; - - receivePort.listen((message) { - if (message is Map) { - final type = message['type'] as String?; - if (type == 'ready') { - backendRequestPort = message['requestPort'] as SendPort; - client = clientFactory(backendRequestPort); - - final tcData = message['toolchain'] as Map; - final toolchain = Toolchain(); - toolchain.applyResolved(ResolvedPaths( - git: tcData['git'] as String?, - pql: tcData['pql'] as String?, - tmux: tcData['tmux'] as String?, - shell: tcData['shell'] as String?, - gitEnv: (tcData['gitEnv'] as Map?)?.cast(), - )); - - backend = Backend._( - client: client, - toolchain: toolchain, - backendRequestPort: backendRequestPort, - isolate: isolate, - receivePort: receivePort, - ); - - if (!completer.isCompleted) completer.complete(backend); - } else if (type == 'project.validated') { - final id = message['id'] as String; - final root = message['root'] as String?; - final c = backend._validateCompleters.remove(id); - if (c != null && !c.isCompleted) c.complete(root); - } else if (type == 'project.ready') { - // Update toolchain with project-specific paths. - final tcData = message['toolchain'] as Map; - backend.toolchain.applyResolved(ResolvedPaths( - git: tcData['git'] as String?, - pql: tcData['pql'] as String?, - tmux: tcData['tmux'] as String?, - shell: tcData['shell'] as String?, - gitEnv: (tcData['gitEnv'] as Map?)?.cast(), - )); - backend._projectCompleter?.complete(); - backend._projectCompleter = null; - } else { - // Response or event — forward to the client. - client.handleMessage(message); - } - } - }); - - isolate = await Isolate.spawn( - backendEntry, - BackendBootMessage(frontendPort: receivePort.sendPort, hintRoot: hintRoot), - ); - - return completer.future; - } - - /// Validate a path as a git repo. Returns the repo root or null. - /// Runs git rev-parse in the backend isolate (no main-thread I/O). - Future validateProject(String path) { - final id = '${_validateId++}'; - final c = Completer(); - _validateCompleters[id] = c; - _backendRequestPort.send({ - 'type': 'project.validate', - 'path': path, - 'id': id, - }); - return c.future; - } - - /// Activate a project. The backend (re)initializes all services - /// for the given root directory. Returns when services are ready. - Future openProject(String path) { - _projectCompleter = Completer(); - _backendRequestPort.send({ - 'type': 'project.open', - 'path': path, - }); - return _projectCompleter!.future; - } - - /// Shut down the backend isolate. - void dispose() { - _isolate.kill(priority: Isolate.beforeNextEvent); - _receivePort.close(); - } -} diff --git a/lib/kernel/src/backend_entry.dart b/lib/kernel/src/backend_entry.dart deleted file mode 100644 index 3b4f45d7..00000000 --- a/lib/kernel/src/backend_entry.dart +++ /dev/null @@ -1,154 +0,0 @@ -/// Backend isolate entry point. -/// -/// Two-phase boot: -/// 1. Resolve toolchain (find binaries) → report ready. -/// 2. On `project.open` message → initialize services for the project. -/// -/// The dispatcher only registers command handlers after a project is -/// activated. IPC requests arriving before that get an error response. -library; - -import 'dart:io'; -import 'dart:isolate'; - -import 'package:clide/kernel/src/toolchain.dart'; -import 'package:clide/src/daemon/dispatcher.dart'; -import 'package:clide/src/daemon/editor_commands.dart'; -import 'package:clide/src/daemon/files_commands.dart'; -import 'package:clide/src/daemon/git_commands.dart'; -import 'package:clide/src/daemon/pane_commands.dart'; -import 'package:clide/src/daemon/pql_commands.dart'; -import 'package:clide/src/editor/registry.dart' show EditorRegistry; -import 'package:clide/src/git/client.dart'; -import 'package:clide/src/ipc/envelope.dart'; -import 'package:clide/src/ipc/schema_v1.dart'; -import 'package:clide/src/panes/event_sink.dart'; -import 'package:clide/src/panes/registry.dart'; -import 'package:clide/src/pql/client.dart'; - -/// Message sent from main isolate to bootstrap the backend. -class BackendBootMessage { - const BackendBootMessage({required this.frontendPort, this.hintRoot}); - final SendPort frontendPort; - - /// Optional path hint for initial toolchain resolution (e.g. CLIDE_PROJECT). - /// Used to find project-local binaries like dugite before a project opens. - final String? hintRoot; -} - -/// Top-level entry point for the backend isolate. -void backendEntry(BackendBootMessage boot) { - final frontendPort = boot.frontendPort; - final requestPort = ReceivePort(); - final eventSink = _IsolateEventSink(frontendPort); - final dispatcher = DaemonDispatcher(); - late Toolchain toolchain; - - // Phase 1: resolve toolchain — just find binaries, don't init services. - // Dugite is resolved against the install dir; per T-98 the project - // root is never inspected during toolchain resolution. - toolchain = Toolchain(); - toolchain.applyResolved(resolveToolchainPaths()); - - // Listen for messages from the frontend. - requestPort.listen((message) async { - if (message is! Map) return; - final type = message['type'] as String?; - - if (type == 'project.validate') { - // Validate a path as a git repo. Runs git rev-parse in the backend - // isolate (safe from the merged thread). Returns the repo root or null. - final path = message['path'] as String; - final id = message['id'] as String; - try { - final r = await Process.run(toolchain.git, ['rev-parse', '--show-toplevel'], workingDirectory: path, environment: toolchain.gitEnv); - if (r.exitCode == 0) { - final root = (r.stdout as String).trim(); - frontendPort.send({'type': 'project.validated', 'id': id, 'root': root}); - } else { - frontendPort.send({'type': 'project.validated', 'id': id, 'root': null}); - } - } catch (_) { - frontendPort.send({'type': 'project.validated', 'id': id, 'root': null}); - } - } else if (type == 'project.open') { - // Phase 2: (re)initialize services for the given project. - final projectPath = message['path'] as String; - final workDir = Directory(projectPath); - - // Re-resolve toolchain. Project path is not inspected (T-98); - // dugite still comes from the install dir + env override. - toolchain = Toolchain(); - toolchain.applyResolved(resolveToolchainPaths()); - - // Clear existing handlers and re-register with new project. - dispatcher.clear(); - - final filesService = FilesService(root: workDir, events: eventSink); - registerFilesCommands(dispatcher, filesService); - - final editorRegistry = EditorRegistry(events: eventSink, workspaceRoot: workDir); - registerEditorCommands(dispatcher, editorRegistry); - - final gitClient = GitClient(toolchain: toolchain, workDir: workDir); - registerGitCommands(dispatcher, gitClient, eventSink); - - final pql = PqlClient(workDir: workDir, toolchain: toolchain); - registerPqlCommands(dispatcher, pql); - - final paneRegistry = PaneRegistry(events: eventSink); - registerPaneCommands(dispatcher, paneRegistry); - - // Tell the frontend the project is active. - frontendPort.send({ - 'type': 'project.ready', - 'path': projectPath, - 'toolchain': _serializeToolchain(toolchain), - }); - } else { - // IPC request — dispatch if we have handlers. - final req = IpcRequest.fromJson(message); - if (dispatcher.isEmpty) { - frontendPort.send(IpcResponse.err( - id: req.id, - error: IpcError( - code: IpcExitCode.toolError, - kind: IpcErrorKind.toolError, - message: 'No project active', - hint: 'Open a project first', - ), - ).toJson()); - } else { - final resp = await dispatcher.dispatch(req); - frontendPort.send(resp.toJson()); - } - } - }); - - // Send ready with toolchain state and request port. - frontendPort.send({ - 'type': 'ready', - 'requestPort': requestPort.sendPort, - 'toolchain': _serializeToolchain(toolchain), - }); -} - -Map _serializeToolchain(Toolchain tc) => { - 'git': tc.git, - 'pql': tc.pql, - 'tmux': tc.tmux, - 'shell': tc.shell, - 'gitEnv': tc.gitEnv, - 'missing': tc.missing, - }; - -/// Sends IPC events to the frontend via SendPort. -class _IsolateEventSink implements DaemonEventSink { - _IsolateEventSink(this._port); - final SendPort _port; - - @override - void emit(IpcEvent event) { - _port.send(event.toJson()); - } -} diff --git a/lib/kernel/src/ipc/isolate_client.dart b/lib/kernel/src/ipc/isolate_client.dart deleted file mode 100644 index e9035f33..00000000 --- a/lib/kernel/src/ipc/isolate_client.dart +++ /dev/null @@ -1,71 +0,0 @@ -/// IPC client that sends requests to a backend isolate via SendPort. -/// -/// Replaces [InProcessClient] for production use. The backend isolate -/// owns the [DaemonDispatcher] and all subprocess/file-I/O services. -/// Requests and responses travel as serialized Maps over SendPort, -/// reusing the existing IPC protocol (IpcRequest/IpcResponse/IpcEvent). -library; - -import 'dart:async'; -import 'dart:isolate'; - -import 'package:clide/clide.dart'; -import 'package:clide/kernel/src/events/bus.dart'; -import 'package:clide/kernel/src/events/types.dart'; -import 'package:clide/kernel/src/ipc/client.dart'; - -class IsolateClient extends DaemonClient { - IsolateClient({ - required super.log, - required super.events, - required SendPort backendPort, - }) : _backendPort = backendPort, - _events = events, - super(socketPath: ''); - - final SendPort _backendPort; - final DaemonBus _events; - - /// The event bus that receives events from the backend. - DaemonBus get events => _events; - final Map> _pending = {}; - int _nextId = 0; - - /// Called by [Backend] to feed incoming messages from the backend isolate. - void handleMessage(Map msg) { - final type = msg['type'] as String?; - switch (type) { - case 'response': - final resp = IpcResponse.fromJson(msg); - final c = _pending.remove(resp.id); - if (c != null && !c.isCompleted) c.complete(resp); - case 'event': - final evt = IpcEvent.fromJson(msg); - _events.emit(DaemonEvent( - subsystem: evt.subsystem, - kind: evt.kind, - data: evt.data, - ts: evt.timestamp, - )); - } - } - - @override - bool get isConnected => true; - - @override - Future start() async {} - - @override - Future stop() async {} - - @override - Future request(String cmd, {Map args = const {}}) { - final id = '${_nextId++}'; - final req = IpcRequest(id: id, cmd: cmd, args: args); - final c = Completer(); - _pending[id] = c; - _backendPort.send(req.toJson()); - return c.future; - } -}