Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions apps/desktop/src/main/services/chat/agentChatService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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* () {
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/main/services/chat/agentChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -427,7 +443,9 @@ export function createOrchestrationService(deps: OrchestrationServiceDeps) {
suspended: false,
};
runs.set(runId, runtime);
return runtime;
}
relocateRuntimeBundlePath(runtime, bundlePath);
return runtime;
}

Expand Down Expand Up @@ -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,
Expand Down
Loading