diff --git a/apps/desktop/src/main/services/chat/agentChatService.test.ts b/apps/desktop/src/main/services/chat/agentChatService.test.ts index ef5e416cc..9b45bf1b2 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.test.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.test.ts @@ -2822,6 +2822,63 @@ describe("createAgentChatService", () => { } }); + it("repoints orchestration bundle path when lane placement changes", async () => { + const { orchestrationService, created } = await createLoadedOrchestrationRun("S-lead-placement"); + const movedWorktree = path.join(tmpRoot, "lane-vm-mirror"); + fs.mkdirSync(movedWorktree, { recursive: true }); + try { + const { service, laneService } = createService({ + getOrchestrationService: () => orchestrationService, + }); + const session = await service.createSession({ + laneId: "lane-1", + provider: "claude", + model: "sonnet", + modelId: "anthropic/claude-sonnet-4-6", + interactionMode: "orchestrator-lead", + orchestrationRunId: created.runId, + orchestrationRole: "lead", + orchestrationBundlePath: created.manifest.bundlePath, + }); + + const lanes = await laneService.list(); + const lane1 = lanes.find((entry: { id: string }) => entry.id === "lane-1"); + expect(lane1).toBeTruthy(); + lane1.worktreePath = movedWorktree; + vi.mocked(laneService.getLaneBaseAndBranch).mockImplementation((laneId: string) => { + const lane = lanes.find((entry: { id: string }) => entry.id === laneId); + if (!lane) { + return { + baseRef: "main", + branchRef: "feature/selected", + worktreePath: tmpRoot, + laneType: "feature", + runtimePlacement: "local", + }; + } + return { + baseRef: "main", + branchRef: lane.branchRef, + worktreePath: lane.worktreePath, + laneType: lane.laneType, + runtimePlacement: "local", + }; + }); + + service.handleLanePlacementChanged({ + laneId: "lane-1", + from: "macos-vm", + to: "local", + }); + + const expectedBundlePath = path.join(movedWorktree, ".ade", "orchestration", created.runId); + expect(readPersistedChatState(session.id).orchestrationBundlePath).toBe(expectedBundlePath); + expect(orchestrationService.getBundlePathForRun(created.runId)).toBe(expectedBundlePath); + } finally { + await orchestrationService.dispose(); + } + }); + it("attaches ADE orchestration tools to OpenCode orchestrator sessions through MCP", async () => { vi.mocked(streamText).mockReturnValue({ fullStream: (async function* () { diff --git a/apps/desktop/src/main/services/chat/agentChatService.ts b/apps/desktop/src/main/services/chat/agentChatService.ts index 4b522b061..1f99f1cf4 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.ts @@ -23351,6 +23351,17 @@ export function createAgentChatService(args: { error: error instanceof Error ? error.message : String(error), }); } + const runId = managed.session.orchestrationRunId?.trim(); + const worktree = managed.laneWorktreePath?.trim(); + if (runId && worktree) { + const nextBundlePath = path.join(worktree, ".ade", "orchestration", runId); + const currentBundlePath = managed.session.orchestrationBundlePath?.trim(); + if (currentBundlePath !== nextBundlePath) { + managed.session.orchestrationBundlePath = nextBundlePath; + persistChatState(managed); + getOrchestrationService?.()?.relocateRunBundle(runId, nextBundlePath); + } + } const message = event.to === "local" ? "Lane detached from Mac VM; further turns run locally." : "Lane attached to Mac VM; further turns run inside the VM at /Volumes/My Shared Files."; diff --git a/apps/desktop/src/main/services/orchestration/orchestrationService.test.ts b/apps/desktop/src/main/services/orchestration/orchestrationService.test.ts index 438db49ac..8fe445aaf 100644 --- a/apps/desktop/src/main/services/orchestration/orchestrationService.test.ts +++ b/apps/desktop/src/main/services/orchestration/orchestrationService.test.ts @@ -209,6 +209,24 @@ describe("orchestrationService", () => { await restarted.dispose(); }); + it("relocates in-memory runtime when the lane worktree bundle path changes", async () => { + const svc = createOrchestrationService({ resolveLaneWorktree: () => lane }); + const movedWorktree = path.join(lane, "vm-mirror-worktree"); + await fsp.mkdir(movedWorktree, { recursive: true }); + const { runId, manifest } = await svc.runCreate({ + laneId: "L-1", + leadSessionId: "S-lead", + bundleRoot: lane, + title: "Placement move", + }); + const movedBundlePath = path.join(movedWorktree, ".ade", "orchestration", runId); + expect(svc.getBundlePathForRun(runId)).toBe(manifest.bundlePath); + + svc.relocateRunBundle(runId, movedBundlePath); + expect(svc.getBundlePathForRun(runId)).toBe(movedBundlePath); + await svc.dispose(); + }); + it("skips stale discovery index entries whose manifest is gone", async () => { const svc = createOrchestrationService({ resolveLaneWorktree: () => lane }); const stale = await svc.runCreate({ diff --git a/apps/desktop/src/main/services/orchestration/orchestrationService.ts b/apps/desktop/src/main/services/orchestration/orchestrationService.ts index fcc885085..5f3f8b1ec 100644 --- a/apps/desktop/src/main/services/orchestration/orchestrationService.ts +++ b/apps/desktop/src/main/services/orchestration/orchestrationService.ts @@ -407,6 +407,22 @@ export function createOrchestrationService(deps: OrchestrationServiceDeps) { .join(","); } + function relocateRuntimeBundlePath(runtime: RunRuntime, bundlePath: string): void { + if (runtime.bundlePath === bundlePath) return; + if (runtime.watcher) { + void runtime.watcher.close().catch(() => undefined); + runtime.watcher = null; + } + if (runtime.watcherDebounceTimer) { + clearTimeout(runtime.watcherDebounceTimer); + runtime.watcherDebounceTimer = null; + } + runtime.bundlePath = bundlePath; + runtime.manifest = null; + runtime.planMd = null; + runtime.suspended = false; + } + function getOrCreateRuntime( runId: string, bundlePath: string, @@ -427,7 +443,9 @@ export function createOrchestrationService(deps: OrchestrationServiceDeps) { suspended: false, }; runs.set(runId, runtime); + return runtime; } + relocateRuntimeBundlePath(runtime, bundlePath); return runtime; } @@ -1442,8 +1460,13 @@ export function createOrchestrationService(deps: OrchestrationServiceDeps) { }); } + function relocateRunBundle(runId: string, bundlePath: string): void { + getOrCreateRuntime(runId, bundlePath); + } + return { runCreate, + relocateRunBundle, bundleRead, manifestReadSection, manifestPatch,