fix(server): unify worktree archive paths
This commit is contained in:
@@ -652,12 +652,19 @@ describe("create_agent MCP tool", () => {
|
|||||||
const workspaceGitService = {
|
const workspaceGitService = {
|
||||||
getSnapshot: vi.fn(async () => null),
|
getSnapshot: vi.fn(async () => null),
|
||||||
};
|
};
|
||||||
|
const archiveWorkspaceRecord = vi.fn(async () => undefined);
|
||||||
|
const emitWorkspaceUpdatesForCwds = vi.fn(async () => undefined);
|
||||||
|
const emitSessionMessage = vi.fn();
|
||||||
const server = await createAgentMcpServer({
|
const server = await createAgentMcpServer({
|
||||||
agentManager,
|
agentManager,
|
||||||
agentStorage,
|
agentStorage,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
createPaseoWorktree: createPaseoWorktreeForMcpTest({ paseoHome, broadcasts: [] }),
|
createPaseoWorktree: createPaseoWorktreeForMcpTest({ paseoHome, broadcasts: [] }),
|
||||||
workspaceGitService: workspaceGitService as any,
|
workspaceGitService: workspaceGitService as any,
|
||||||
|
archiveWorkspaceRecord,
|
||||||
|
emitWorkspaceUpdatesForCwds,
|
||||||
|
emitSessionMessage,
|
||||||
|
github: createGitHubServiceStub(),
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
const createTool = (server as any)._registeredTools["create_worktree"];
|
const createTool = (server as any)._registeredTools["create_worktree"];
|
||||||
@@ -678,6 +685,10 @@ describe("create_agent MCP tool", () => {
|
|||||||
force: true,
|
force: true,
|
||||||
reason: "archive-worktree",
|
reason: "archive-worktree",
|
||||||
});
|
});
|
||||||
|
expect(archiveWorkspaceRecord).toHaveBeenCalledWith(created.structuredContent.worktreePath);
|
||||||
|
expect(Array.from(emitWorkspaceUpdatesForCwds.mock.calls[0]?.[0] ?? [])).toEqual([
|
||||||
|
created.structuredContent.worktreePath,
|
||||||
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
await rm(tempDir, { recursive: true, force: true });
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ import {
|
|||||||
appendTimelineItemIfAgentKnown,
|
appendTimelineItemIfAgentKnown,
|
||||||
emitLiveTimelineItemIfAgentKnown,
|
emitLiveTimelineItemIfAgentKnown,
|
||||||
} from "./timeline-append.js";
|
} from "./timeline-append.js";
|
||||||
import { deletePaseoWorktree, type WorktreeConfig } from "../../utils/worktree.js";
|
import { getPaseoWorktreesRoot, type WorktreeConfig } from "../../utils/worktree.js";
|
||||||
|
import {
|
||||||
|
archivePaseoWorktree,
|
||||||
|
killTerminalsUnderPath,
|
||||||
|
type ArchivePaseoWorktreeDependencies,
|
||||||
|
} from "../paseo-worktree-archive-service.js";
|
||||||
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
||||||
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
|
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
|
||||||
import type { VoiceCallerContext, VoiceSpeakHandler } from "../voice-types.js";
|
import type { VoiceCallerContext, VoiceSpeakHandler } from "../voice-types.js";
|
||||||
@@ -61,6 +66,7 @@ import type {
|
|||||||
CreatePaseoWorktreeResult,
|
CreatePaseoWorktreeResult,
|
||||||
} from "../paseo-worktree-service.js";
|
} from "../paseo-worktree-service.js";
|
||||||
import { toWorktreeRequestError } from "../worktree-errors.js";
|
import { toWorktreeRequestError } from "../worktree-errors.js";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
export interface AgentMcpServerOptions {
|
export interface AgentMcpServerOptions {
|
||||||
agentManager: AgentManager;
|
agentManager: AgentManager;
|
||||||
@@ -71,6 +77,9 @@ export interface AgentMcpServerOptions {
|
|||||||
providerRegistry?: Record<AgentProvider, ProviderDefinition> | null;
|
providerRegistry?: Record<AgentProvider, ProviderDefinition> | null;
|
||||||
github?: GitHubService;
|
github?: GitHubService;
|
||||||
workspaceGitService?: Pick<WorkspaceGitService, "getSnapshot" | "listWorktrees">;
|
workspaceGitService?: Pick<WorkspaceGitService, "getSnapshot" | "listWorktrees">;
|
||||||
|
archiveWorkspaceRecord?: ArchivePaseoWorktreeDependencies["archiveWorkspaceRecord"];
|
||||||
|
emitWorkspaceUpdatesForCwds?: ArchivePaseoWorktreeDependencies["emitWorkspaceUpdatesForCwds"];
|
||||||
|
emitSessionMessage?: ArchivePaseoWorktreeDependencies["emit"];
|
||||||
createPaseoWorktree?: CreatePaseoWorktreeFn;
|
createPaseoWorktree?: CreatePaseoWorktreeFn;
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
/**
|
/**
|
||||||
@@ -1733,24 +1742,58 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
},
|
},
|
||||||
async ({ cwd, worktreePath, worktreeSlug }) => {
|
async ({ cwd, worktreePath, worktreeSlug }) => {
|
||||||
const repoRoot = resolveScopedCwd(cwd, { required: true });
|
const repoRoot = resolveScopedCwd(cwd, { required: true });
|
||||||
await deletePaseoWorktree({
|
if (!worktreePath && !worktreeSlug) {
|
||||||
cwd: repoRoot,
|
throw new Error("worktreePath or worktreeSlug is required");
|
||||||
worktreePath,
|
|
||||||
worktreeSlug,
|
|
||||||
paseoHome: options.paseoHome,
|
|
||||||
});
|
|
||||||
options.github?.invalidate({ cwd: repoRoot });
|
|
||||||
try {
|
|
||||||
await options.workspaceGitService?.getSnapshot(repoRoot, {
|
|
||||||
force: true,
|
|
||||||
reason: "archive-worktree",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
options.logger.warn(
|
|
||||||
{ err: error, cwd: repoRoot },
|
|
||||||
"Failed to force-refresh workspace git snapshot after archiving worktree",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if (!options.github) {
|
||||||
|
throw new Error("GitHub service is required to archive worktrees");
|
||||||
|
}
|
||||||
|
if (!options.workspaceGitService) {
|
||||||
|
throw new Error("WorkspaceGitService is required to archive worktrees");
|
||||||
|
}
|
||||||
|
if (!options.archiveWorkspaceRecord) {
|
||||||
|
throw new Error("Workspace registry archiver is required to archive worktrees");
|
||||||
|
}
|
||||||
|
if (!options.emitWorkspaceUpdatesForCwds) {
|
||||||
|
throw new Error("Workspace update emitter is required to archive worktrees");
|
||||||
|
}
|
||||||
|
if (!options.emitSessionMessage) {
|
||||||
|
throw new Error("Session message emitter is required to archive worktrees");
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath =
|
||||||
|
worktreePath ??
|
||||||
|
join(await getPaseoWorktreesRoot(repoRoot, options.paseoHome), worktreeSlug!);
|
||||||
|
|
||||||
|
await archivePaseoWorktree(
|
||||||
|
{
|
||||||
|
paseoHome: options.paseoHome,
|
||||||
|
github: options.github,
|
||||||
|
workspaceGitService: options.workspaceGitService,
|
||||||
|
agentManager,
|
||||||
|
agentStorage,
|
||||||
|
archiveWorkspaceRecord: options.archiveWorkspaceRecord,
|
||||||
|
emit: options.emitSessionMessage,
|
||||||
|
emitWorkspaceUpdatesForCwds: options.emitWorkspaceUpdatesForCwds,
|
||||||
|
isPathWithinRoot: isSameOrDescendantPath,
|
||||||
|
killTerminalsUnderPath: (rootPath) =>
|
||||||
|
killTerminalsUnderPath(
|
||||||
|
{
|
||||||
|
terminalManager: terminalManager ?? null,
|
||||||
|
isPathWithinRoot: isSameOrDescendantPath,
|
||||||
|
killTrackedTerminal: () => {},
|
||||||
|
sessionLogger: childLogger,
|
||||||
|
},
|
||||||
|
rootPath,
|
||||||
|
),
|
||||||
|
sessionLogger: childLogger,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targetPath,
|
||||||
|
repoRoot,
|
||||||
|
requestId: "mcp:archive_worktree",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [],
|
content: [],
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ import { LoopService } from "./loop-service.js";
|
|||||||
import { ScheduleService } from "./schedule/service.js";
|
import { ScheduleService } from "./schedule/service.js";
|
||||||
import { DaemonConfigStore } from "./daemon-config-store.js";
|
import { DaemonConfigStore } from "./daemon-config-store.js";
|
||||||
import { WorkspaceGitServiceImpl } from "./workspace-git-service.js";
|
import { WorkspaceGitServiceImpl } from "./workspace-git-service.js";
|
||||||
|
import { archivePersistedWorkspaceRecord } from "./workspace-archive-service.js";
|
||||||
|
import { wrapSessionMessage, type SessionOutboundMessage } from "./messages.js";
|
||||||
import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js";
|
import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js";
|
||||||
import { createConnectionOfferV2, encodeOfferToFragmentUrl } from "./connection-offer.js";
|
import { createConnectionOfferV2, encodeOfferToFragmentUrl } from "./connection-offer.js";
|
||||||
import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js";
|
import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js";
|
||||||
@@ -493,6 +495,34 @@ export async function createPaseoDaemon(
|
|||||||
if (mcpEnabled) {
|
if (mcpEnabled) {
|
||||||
const agentMcpRoute = "/mcp/agents";
|
const agentMcpRoute = "/mcp/agents";
|
||||||
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
||||||
|
const archiveWorkspaceRecordForMcp = async (workspaceId: string) => {
|
||||||
|
const sessions = wsServer?.listActiveSessions() ?? [];
|
||||||
|
if (sessions.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
sessions.map((session) =>
|
||||||
|
session.archiveWorkspaceRecordForExternalMutation(workspaceId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await archivePersistedWorkspaceRecord({
|
||||||
|
workspaceId,
|
||||||
|
workspaceRegistry,
|
||||||
|
projectRegistry,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const emitWorkspaceUpdatesForMcpArchive = async (cwds: Iterable<string>) => {
|
||||||
|
const cwdList = Array.from(cwds);
|
||||||
|
await Promise.all(
|
||||||
|
(wsServer?.listActiveSessions() ?? []).map((session) =>
|
||||||
|
session.emitWorkspaceUpdatesForExternalCwds(cwdList),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const emitMcpArchiveSessionMessage = (message: SessionOutboundMessage) => {
|
||||||
|
wsServer?.broadcast(wrapSessionMessage(message));
|
||||||
|
};
|
||||||
|
|
||||||
const createAgentMcpTransport = async (callerAgentId?: string) => {
|
const createAgentMcpTransport = async (callerAgentId?: string) => {
|
||||||
const agentMcpServer = await createAgentMcpServer({
|
const agentMcpServer = await createAgentMcpServer({
|
||||||
@@ -505,6 +535,9 @@ export async function createPaseoDaemon(
|
|||||||
providerRegistry,
|
providerRegistry,
|
||||||
github,
|
github,
|
||||||
workspaceGitService,
|
workspaceGitService,
|
||||||
|
archiveWorkspaceRecord: archiveWorkspaceRecordForMcp,
|
||||||
|
emitWorkspaceUpdatesForCwds: emitWorkspaceUpdatesForMcpArchive,
|
||||||
|
emitSessionMessage: emitMcpArchiveSessionMessage,
|
||||||
createPaseoWorktree: async (input, serviceOptions) => {
|
createPaseoWorktree: async (input, serviceOptions) => {
|
||||||
const coreDeps = createWorktreeCoreDeps(github);
|
const coreDeps = createWorktreeCoreDeps(github);
|
||||||
const result = await createPaseoWorktree(input, {
|
const result = await createPaseoWorktree(input, {
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import type { Logger } from "pino";
|
||||||
|
|
||||||
|
import type { AgentManager } from "./agent/agent-manager.js";
|
||||||
|
import type { AgentStorage, StoredAgentRecord } from "./agent/agent-storage.js";
|
||||||
|
import type { SessionOutboundMessage } from "./messages.js";
|
||||||
|
import type { WorkspaceGitService } from "./workspace-git-service.js";
|
||||||
|
import { normalizeWorkspaceId as normalizePersistedWorkspaceId } from "./workspace-registry-model.js";
|
||||||
|
import type { GitHubService } from "../services/github-service.js";
|
||||||
|
import { deletePaseoWorktree, resolvePaseoWorktreeRootForCwd } from "../utils/worktree.js";
|
||||||
|
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
||||||
|
|
||||||
|
type EmitSessionMessage = (message: SessionOutboundMessage) => void;
|
||||||
|
|
||||||
|
export type ArchivePaseoWorktreeDependencies = {
|
||||||
|
paseoHome?: string;
|
||||||
|
github: GitHubService;
|
||||||
|
workspaceGitService: Pick<WorkspaceGitService, "getSnapshot">;
|
||||||
|
agentManager: Pick<AgentManager, "listAgents" | "closeAgent">;
|
||||||
|
agentStorage: Pick<AgentStorage, "list" | "remove">;
|
||||||
|
archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
|
||||||
|
emit: EmitSessionMessage;
|
||||||
|
emitWorkspaceUpdatesForCwds: (cwds: Iterable<string>) => Promise<void>;
|
||||||
|
isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
|
||||||
|
killTerminalsUnderPath: (rootPath: string) => Promise<void>;
|
||||||
|
sessionLogger?: Logger;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KillTerminalsUnderPathDependencies = {
|
||||||
|
isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
|
||||||
|
killTrackedTerminal: (terminalId: string, options?: { emitExit: boolean }) => void;
|
||||||
|
detachTerminalStream?: (terminalId: string, options: { emitExit: boolean }) => void;
|
||||||
|
sessionLogger: Logger;
|
||||||
|
terminalManager: TerminalManager | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function archivePaseoWorktree(
|
||||||
|
dependencies: ArchivePaseoWorktreeDependencies,
|
||||||
|
options: {
|
||||||
|
targetPath: string;
|
||||||
|
repoRoot: string | null;
|
||||||
|
worktreesRoot?: string;
|
||||||
|
requestId: string;
|
||||||
|
},
|
||||||
|
): Promise<string[]> {
|
||||||
|
let targetPath = options.targetPath;
|
||||||
|
const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
|
||||||
|
paseoHome: dependencies.paseoHome,
|
||||||
|
});
|
||||||
|
if (resolvedWorktree) {
|
||||||
|
targetPath = resolvedWorktree.worktreePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedAgents = new Set<string>();
|
||||||
|
const affectedWorkspaceCwds = new Set<string>([targetPath]);
|
||||||
|
const affectedWorkspaceIds = new Set<string>([normalizePersistedWorkspaceId(targetPath)]);
|
||||||
|
|
||||||
|
const liveAgents = dependencies.agentManager
|
||||||
|
.listAgents()
|
||||||
|
.filter((agent) => dependencies.isPathWithinRoot(targetPath, agent.cwd));
|
||||||
|
for (const agent of liveAgents) {
|
||||||
|
removedAgents.add(agent.id);
|
||||||
|
affectedWorkspaceCwds.add(agent.cwd);
|
||||||
|
affectedWorkspaceIds.add(normalizePersistedWorkspaceId(agent.cwd));
|
||||||
|
}
|
||||||
|
|
||||||
|
let storedRecords: StoredAgentRecord[] = [];
|
||||||
|
try {
|
||||||
|
storedRecords = await dependencies.agentStorage.list();
|
||||||
|
} catch (error) {
|
||||||
|
dependencies.sessionLogger?.warn(
|
||||||
|
{ err: error, targetPath },
|
||||||
|
"Failed to list stored agents during worktree archive; continuing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const matchingStoredRecords = storedRecords.filter((record) =>
|
||||||
|
dependencies.isPathWithinRoot(targetPath, record.cwd),
|
||||||
|
);
|
||||||
|
for (const record of matchingStoredRecords) {
|
||||||
|
removedAgents.add(record.id);
|
||||||
|
affectedWorkspaceCwds.add(record.cwd);
|
||||||
|
affectedWorkspaceIds.add(normalizePersistedWorkspaceId(record.cwd));
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentIdsToRemoveFromStorage = new Set<string>([
|
||||||
|
...liveAgents.map((agent) => agent.id),
|
||||||
|
...matchingStoredRecords.map((record) => record.id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const teardownResults = await Promise.allSettled([
|
||||||
|
...liveAgents.map((agent) => dependencies.agentManager.closeAgent(agent.id)),
|
||||||
|
dependencies.killTerminalsUnderPath(targetPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const result of teardownResults) {
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
dependencies.sessionLogger?.warn(
|
||||||
|
{ err: result.reason, targetPath },
|
||||||
|
"Worktree teardown step failed during archive; continuing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentIdsToRemove = Array.from(agentIdsToRemoveFromStorage);
|
||||||
|
const storageRemovalResults = await Promise.allSettled(
|
||||||
|
agentIdsToRemove.map((agentId) => dependencies.agentStorage.remove(agentId)),
|
||||||
|
);
|
||||||
|
storageRemovalResults.forEach((result, index) => {
|
||||||
|
if (result.status === "fulfilled") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dependencies.sessionLogger?.warn(
|
||||||
|
{
|
||||||
|
err: result.reason,
|
||||||
|
agentId: agentIdsToRemove[index],
|
||||||
|
targetPath,
|
||||||
|
},
|
||||||
|
"Failed to remove archived worktree agent from storage; continuing",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await deletePaseoWorktree({
|
||||||
|
cwd: options.repoRoot,
|
||||||
|
worktreePath: targetPath,
|
||||||
|
worktreesRoot: options.worktreesRoot,
|
||||||
|
paseoHome: dependencies.paseoHome,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.repoRoot) {
|
||||||
|
try {
|
||||||
|
await dependencies.workspaceGitService.getSnapshot(options.repoRoot, {
|
||||||
|
force: true,
|
||||||
|
reason: "archive-worktree",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
dependencies.sessionLogger?.warn(
|
||||||
|
{ err: error, cwd: options.repoRoot },
|
||||||
|
"Failed to force-refresh workspace git snapshot after archiving worktree",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cwd of affectedWorkspaceCwds) {
|
||||||
|
dependencies.github.invalidate({ cwd });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const workspaceId of affectedWorkspaceIds) {
|
||||||
|
try {
|
||||||
|
await dependencies.archiveWorkspaceRecord(workspaceId);
|
||||||
|
} catch (error) {
|
||||||
|
dependencies.sessionLogger?.warn(
|
||||||
|
{ err: error, workspaceId },
|
||||||
|
"Failed to archive workspace record; worktree FS already removed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const agentId of removedAgents) {
|
||||||
|
dependencies.emit({
|
||||||
|
type: "agent_deleted",
|
||||||
|
payload: {
|
||||||
|
agentId,
|
||||||
|
requestId: options.requestId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await dependencies.emitWorkspaceUpdatesForCwds(affectedWorkspaceCwds);
|
||||||
|
|
||||||
|
return Array.from(removedAgents);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function killTerminalsUnderPath(
|
||||||
|
dependencies: KillTerminalsUnderPathDependencies,
|
||||||
|
rootPath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const terminalManager = dependencies.terminalManager;
|
||||||
|
if (!terminalManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalIds: string[] = [];
|
||||||
|
const terminalDirectories = [...terminalManager.listDirectories()];
|
||||||
|
for (const terminalCwd of terminalDirectories) {
|
||||||
|
if (!dependencies.isPathWithinRoot(rootPath, terminalCwd)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const terminals = await terminalManager.getTerminals(terminalCwd);
|
||||||
|
for (const terminal of terminals) {
|
||||||
|
terminalIds.push(terminal.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
dependencies.sessionLogger.warn(
|
||||||
|
{ err: error, cwd: terminalCwd },
|
||||||
|
"Failed to enumerate worktree terminals during archive",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terminalIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(
|
||||||
|
terminalIds.map(async (terminalId) => {
|
||||||
|
try {
|
||||||
|
dependencies.detachTerminalStream?.(terminalId, { emitExit: true });
|
||||||
|
await terminalManager.killTerminalAndWait(terminalId, {
|
||||||
|
gracefulTimeoutMs: 2000,
|
||||||
|
forceTimeoutMs: 1500,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
dependencies.sessionLogger.warn(
|
||||||
|
{ err: error, terminalId },
|
||||||
|
"Terminal kill escalation failed during archive; proceeding anyway",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -165,6 +165,7 @@ import { DownloadTokenStore } from "./file-download/token-store.js";
|
|||||||
import { PushTokenStore } from "./push/token-store.js";
|
import { PushTokenStore } from "./push/token-store.js";
|
||||||
import { type WorktreeConfig } from "../utils/worktree.js";
|
import { type WorktreeConfig } from "../utils/worktree.js";
|
||||||
import { runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js";
|
import { runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js";
|
||||||
|
import { archivePersistedWorkspaceRecord } from "./workspace-archive-service.js";
|
||||||
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
|
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
|
||||||
import type { ScriptRouteStore } from "./script-proxy.js";
|
import type { ScriptRouteStore } from "./script-proxy.js";
|
||||||
import {
|
import {
|
||||||
@@ -211,8 +212,8 @@ import {
|
|||||||
handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest,
|
handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest,
|
||||||
handlePaseoWorktreeListRequest as handleWorktreeListRequest,
|
handlePaseoWorktreeListRequest as handleWorktreeListRequest,
|
||||||
handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage,
|
handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage,
|
||||||
killTerminalsUnderPath as killWorktreeTerminalsUnderPath,
|
|
||||||
} from "./worktree-session.js";
|
} from "./worktree-session.js";
|
||||||
|
import { killTerminalsUnderPath as killWorktreeTerminalsUnderPath } from "./paseo-worktree-archive-service.js";
|
||||||
import { toWorktreeWireError } from "./worktree-errors.js";
|
import { toWorktreeWireError } from "./worktree-errors.js";
|
||||||
|
|
||||||
const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
|
const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
|
||||||
@@ -907,6 +908,14 @@ export class Session {
|
|||||||
await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], { skipReconcile: true });
|
await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], { skipReconcile: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async archiveWorkspaceRecordForExternalMutation(workspaceId: string): Promise<void> {
|
||||||
|
await this.archiveWorkspaceRecord(workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async emitWorkspaceUpdatesForExternalCwds(cwds: Iterable<string>): Promise<void> {
|
||||||
|
await this.emitWorkspaceUpdatesForCwds(cwds);
|
||||||
|
}
|
||||||
|
|
||||||
async warmWorkspaceGitDataForWorkspace(workspace: PersistedWorkspaceRecord): Promise<void> {
|
async warmWorkspaceGitDataForWorkspace(workspace: PersistedWorkspaceRecord): Promise<void> {
|
||||||
await this.primeWorkspaceGitWatchFingerprintForWorkspace(workspace);
|
await this.primeWorkspaceGitWatchFingerprintForWorkspace(workspace);
|
||||||
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
|
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
|
||||||
@@ -6403,24 +6412,20 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async archiveWorkspaceRecord(workspaceId: string, archivedAt?: string): Promise<void> {
|
private async archiveWorkspaceRecord(workspaceId: string, archivedAt?: string): Promise<void> {
|
||||||
const existingWorkspace = await this.workspaceRegistry.get(workspaceId);
|
const existingWorkspace = await archivePersistedWorkspaceRecord({
|
||||||
if (!existingWorkspace || existingWorkspace.archivedAt) {
|
workspaceId,
|
||||||
|
archivedAt,
|
||||||
|
workspaceRegistry: this.workspaceRegistry,
|
||||||
|
projectRegistry: this.projectRegistry,
|
||||||
|
});
|
||||||
|
if (!existingWorkspace) {
|
||||||
this.removeWorkspaceGitSubscription(workspaceId);
|
this.removeWorkspaceGitSubscription(workspaceId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextArchivedAt = archivedAt ?? new Date().toISOString();
|
|
||||||
await this.workspaceRegistry.archive(workspaceId, nextArchivedAt);
|
|
||||||
await this.removeWorkspaceGitWatchTarget(existingWorkspace.cwd);
|
await this.removeWorkspaceGitWatchTarget(existingWorkspace.cwd);
|
||||||
this.scriptRuntimeStore?.removeForWorkspace(existingWorkspace.cwd);
|
this.scriptRuntimeStore?.removeForWorkspace(existingWorkspace.cwd);
|
||||||
this.removeWorkspaceGitSubscription(workspaceId);
|
this.removeWorkspaceGitSubscription(workspaceId);
|
||||||
|
|
||||||
const siblingWorkspaces = (await this.workspaceRegistry.list()).filter(
|
|
||||||
(workspace) => workspace.projectId === existingWorkspace.projectId && !workspace.archivedAt,
|
|
||||||
);
|
|
||||||
if (siblingWorkspaces.length === 0) {
|
|
||||||
await this.projectRegistry.archive(existingWorkspace.projectId, nextArchivedAt);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async reconcileAndEmitWorkspaceUpdates(): Promise<void> {
|
private async reconcileAndEmitWorkspaceUpdates(): Promise<void> {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type {
|
||||||
|
PersistedWorkspaceRecord,
|
||||||
|
ProjectRegistry,
|
||||||
|
WorkspaceRegistry,
|
||||||
|
} from "./workspace-registry.js";
|
||||||
|
|
||||||
|
export async function archivePersistedWorkspaceRecord(input: {
|
||||||
|
workspaceId: string;
|
||||||
|
workspaceRegistry: Pick<WorkspaceRegistry, "get" | "list" | "archive">;
|
||||||
|
projectRegistry: Pick<ProjectRegistry, "archive">;
|
||||||
|
archivedAt?: string;
|
||||||
|
}): Promise<PersistedWorkspaceRecord | null> {
|
||||||
|
const existingWorkspace = await input.workspaceRegistry.get(input.workspaceId);
|
||||||
|
if (!existingWorkspace) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingWorkspace.archivedAt) {
|
||||||
|
return existingWorkspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
const archivedAt = input.archivedAt ?? new Date().toISOString();
|
||||||
|
await input.workspaceRegistry.archive(input.workspaceId, archivedAt);
|
||||||
|
const activeSiblings = (await input.workspaceRegistry.list()).filter(
|
||||||
|
(workspace) => workspace.projectId === existingWorkspace.projectId && !workspace.archivedAt,
|
||||||
|
);
|
||||||
|
if (activeSiblings.length === 0) {
|
||||||
|
await input.projectRegistry.archive(existingWorkspace.projectId, archivedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingWorkspace;
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@ import pino, { type Logger } from "pino";
|
|||||||
|
|
||||||
import type { SessionOutboundMessage, WorkspaceDescriptorPayload } from "./messages.js";
|
import type { SessionOutboundMessage, WorkspaceDescriptorPayload } from "./messages.js";
|
||||||
import { ScriptRouteStore } from "./script-proxy.js";
|
import { ScriptRouteStore } from "./script-proxy.js";
|
||||||
|
import { archivePaseoWorktree } from "./paseo-worktree-archive-service.js";
|
||||||
import {
|
import {
|
||||||
archivePaseoWorktree,
|
|
||||||
buildAgentSessionConfig,
|
buildAgentSessionConfig,
|
||||||
handlePaseoWorktreeArchiveRequest,
|
handlePaseoWorktreeArchiveRequest,
|
||||||
handlePaseoWorktreeListRequest,
|
handlePaseoWorktreeListRequest,
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import type { Logger } from "pino";
|
|||||||
import { basename } from "node:path";
|
import { basename } from "node:path";
|
||||||
|
|
||||||
import type { AgentSessionConfig } from "./agent/agent-sdk-types.js";
|
import type { AgentSessionConfig } from "./agent/agent-sdk-types.js";
|
||||||
import type { AgentManager } from "./agent/agent-manager.js";
|
|
||||||
import type { AgentStorage, StoredAgentRecord } from "./agent/agent-storage.js";
|
|
||||||
import {
|
import {
|
||||||
type AgentAttachment,
|
type AgentAttachment,
|
||||||
type GitSetupOptions,
|
type GitSetupOptions,
|
||||||
@@ -14,7 +12,6 @@ import {
|
|||||||
} from "./messages.js";
|
} from "./messages.js";
|
||||||
import type { PersistedWorkspaceRecord } from "./workspace-registry.js";
|
import type { PersistedWorkspaceRecord } from "./workspace-registry.js";
|
||||||
import type { WorkspaceGitService } from "./workspace-git-service.js";
|
import type { WorkspaceGitService } from "./workspace-git-service.js";
|
||||||
import { normalizeWorkspaceId as normalizePersistedWorkspaceId } from "./workspace-registry-model.js";
|
|
||||||
import {
|
import {
|
||||||
applyWorktreeSetupProgressEvent,
|
applyWorktreeSetupProgressEvent,
|
||||||
buildWorktreeSetupDetail,
|
buildWorktreeSetupDetail,
|
||||||
@@ -28,10 +25,8 @@ import type { GitHubService } from "../services/github-service.js";
|
|||||||
import type { CheckoutExistingBranchResult } from "../utils/checkout-git.js";
|
import type { CheckoutExistingBranchResult } from "../utils/checkout-git.js";
|
||||||
import { expandTilde } from "../utils/path.js";
|
import { expandTilde } from "../utils/path.js";
|
||||||
import {
|
import {
|
||||||
deletePaseoWorktree,
|
|
||||||
getWorktreeSetupCommands,
|
getWorktreeSetupCommands,
|
||||||
isPaseoOwnedWorktreeCwd,
|
isPaseoOwnedWorktreeCwd,
|
||||||
resolvePaseoWorktreeRootForCwd,
|
|
||||||
resolveWorktreeRuntimeEnv,
|
resolveWorktreeRuntimeEnv,
|
||||||
runWorktreeSetupCommands,
|
runWorktreeSetupCommands,
|
||||||
slugify,
|
slugify,
|
||||||
@@ -45,6 +40,10 @@ import type {
|
|||||||
CreatePaseoWorktreeInput,
|
CreatePaseoWorktreeInput,
|
||||||
CreatePaseoWorktreeResult,
|
CreatePaseoWorktreeResult,
|
||||||
} from "./paseo-worktree-service.js";
|
} from "./paseo-worktree-service.js";
|
||||||
|
import {
|
||||||
|
archivePaseoWorktree,
|
||||||
|
type ArchivePaseoWorktreeDependencies,
|
||||||
|
} from "./paseo-worktree-archive-service.js";
|
||||||
import { toWorktreeWireError } from "./worktree-errors.js";
|
import { toWorktreeWireError } from "./worktree-errors.js";
|
||||||
|
|
||||||
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
|
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
|
||||||
@@ -82,20 +81,6 @@ type BuildAgentSessionConfigDependencies = {
|
|||||||
github?: Pick<GitHubService, "invalidate">;
|
github?: Pick<GitHubService, "invalidate">;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ArchivePaseoWorktreeDependencies = {
|
|
||||||
paseoHome?: string;
|
|
||||||
github: GitHubService;
|
|
||||||
workspaceGitService: WorkspaceGitService;
|
|
||||||
agentManager: Pick<AgentManager, "listAgents" | "closeAgent">;
|
|
||||||
agentStorage: Pick<AgentStorage, "list" | "remove">;
|
|
||||||
archiveWorkspaceRecord: (workspaceId: string) => Promise<void>;
|
|
||||||
emit: EmitSessionMessage;
|
|
||||||
emitWorkspaceUpdatesForCwds: (cwds: Iterable<string>) => Promise<void>;
|
|
||||||
isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
|
|
||||||
killTerminalsUnderPath: (rootPath: string) => Promise<void>;
|
|
||||||
sessionLogger?: Logger;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CreatePaseoWorktreeInBackgroundDependencies = {
|
type CreatePaseoWorktreeInBackgroundDependencies = {
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
emitWorkspaceUpdateForCwd: (cwd: string, options?: { dedupeGitState?: boolean }) => Promise<void>;
|
emitWorkspaceUpdateForCwd: (cwd: string, options?: { dedupeGitState?: boolean }) => Promise<void>;
|
||||||
@@ -136,14 +121,6 @@ type HandleCreatePaseoWorktreeRequestDependencies = {
|
|||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type KillTerminalsUnderPathDependencies = {
|
|
||||||
isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
|
|
||||||
killTrackedTerminal: (terminalId: string, options?: { emitExit: boolean }) => void;
|
|
||||||
detachTerminalStream?: (terminalId: string, options: { emitExit: boolean }) => void;
|
|
||||||
sessionLogger: Logger;
|
|
||||||
terminalManager: TerminalManager | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function buildAgentSessionConfig(
|
export async function buildAgentSessionConfig(
|
||||||
dependencies: BuildAgentSessionConfigDependencies,
|
dependencies: BuildAgentSessionConfigDependencies,
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
@@ -368,151 +345,13 @@ export async function handlePaseoWorktreeListRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function archivePaseoWorktree(
|
|
||||||
dependencies: ArchivePaseoWorktreeDependencies,
|
|
||||||
options: {
|
|
||||||
targetPath: string;
|
|
||||||
repoRoot: string | null;
|
|
||||||
worktreesRoot?: string;
|
|
||||||
requestId: string;
|
|
||||||
},
|
|
||||||
): Promise<string[]> {
|
|
||||||
let targetPath = options.targetPath;
|
|
||||||
const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
|
|
||||||
paseoHome: dependencies.paseoHome,
|
|
||||||
});
|
|
||||||
if (resolvedWorktree) {
|
|
||||||
targetPath = resolvedWorktree.worktreePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const removedAgents = new Set<string>();
|
|
||||||
const affectedWorkspaceCwds = new Set<string>([targetPath]);
|
|
||||||
const affectedWorkspaceIds = new Set<string>([normalizePersistedWorkspaceId(targetPath)]);
|
|
||||||
|
|
||||||
const liveAgents = dependencies.agentManager
|
|
||||||
.listAgents()
|
|
||||||
.filter((agent) => dependencies.isPathWithinRoot(targetPath, agent.cwd));
|
|
||||||
for (const agent of liveAgents) {
|
|
||||||
removedAgents.add(agent.id);
|
|
||||||
affectedWorkspaceCwds.add(agent.cwd);
|
|
||||||
affectedWorkspaceIds.add(normalizePersistedWorkspaceId(agent.cwd));
|
|
||||||
}
|
|
||||||
|
|
||||||
let storedRecords: StoredAgentRecord[] = [];
|
|
||||||
try {
|
|
||||||
storedRecords = await dependencies.agentStorage.list();
|
|
||||||
} catch (error) {
|
|
||||||
dependencies.sessionLogger?.warn(
|
|
||||||
{ err: error, targetPath },
|
|
||||||
"Failed to list stored agents during worktree archive; continuing",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const matchingStoredRecords = storedRecords.filter((record) =>
|
|
||||||
dependencies.isPathWithinRoot(targetPath, record.cwd),
|
|
||||||
);
|
|
||||||
for (const record of matchingStoredRecords) {
|
|
||||||
removedAgents.add(record.id);
|
|
||||||
affectedWorkspaceCwds.add(record.cwd);
|
|
||||||
affectedWorkspaceIds.add(normalizePersistedWorkspaceId(record.cwd));
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentIdsToRemoveFromStorage = new Set<string>([
|
|
||||||
...liveAgents.map((agent) => agent.id),
|
|
||||||
...matchingStoredRecords.map((record) => record.id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Fan out agent close + terminal teardown concurrently. We never let a
|
|
||||||
// per-item failure abort the archive — the FS delete must still run so the
|
|
||||||
// worktree doesn't get stuck half-dead.
|
|
||||||
const teardownResults = await Promise.allSettled([
|
|
||||||
...liveAgents.map((agent) => dependencies.agentManager.closeAgent(agent.id)),
|
|
||||||
dependencies.killTerminalsUnderPath(targetPath),
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (const result of teardownResults) {
|
|
||||||
if (result.status === "rejected") {
|
|
||||||
dependencies.sessionLogger?.warn(
|
|
||||||
{ err: result.reason, targetPath },
|
|
||||||
"Worktree teardown step failed during archive; continuing",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Agent storage removal runs after closeAgent so file handles on the agent
|
|
||||||
// state file are released; still allSettled so a single bad record can't
|
|
||||||
// derail the rest.
|
|
||||||
const agentIdsToRemove = Array.from(agentIdsToRemoveFromStorage);
|
|
||||||
const storageRemovalResults = await Promise.allSettled(
|
|
||||||
agentIdsToRemove.map((agentId) => dependencies.agentStorage.remove(agentId)),
|
|
||||||
);
|
|
||||||
storageRemovalResults.forEach((result, index) => {
|
|
||||||
if (result.status === "fulfilled") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dependencies.sessionLogger?.warn(
|
|
||||||
{
|
|
||||||
err: result.reason,
|
|
||||||
agentId: agentIdsToRemove[index],
|
|
||||||
targetPath,
|
|
||||||
},
|
|
||||||
"Failed to remove archived worktree agent from storage; continuing",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await deletePaseoWorktree({
|
|
||||||
cwd: options.repoRoot,
|
|
||||||
worktreePath: targetPath,
|
|
||||||
worktreesRoot: options.worktreesRoot,
|
|
||||||
paseoHome: dependencies.paseoHome,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options.repoRoot) {
|
|
||||||
try {
|
|
||||||
await dependencies.workspaceGitService.getSnapshot(options.repoRoot, {
|
|
||||||
force: true,
|
|
||||||
reason: "archive-worktree",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dependencies.sessionLogger?.warn(
|
|
||||||
{ err: error, cwd: options.repoRoot },
|
|
||||||
"Failed to force-refresh workspace git snapshot after archiving worktree",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const cwd of affectedWorkspaceCwds) {
|
|
||||||
dependencies.github.invalidate({ cwd });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const workspaceId of affectedWorkspaceIds) {
|
|
||||||
try {
|
|
||||||
await dependencies.archiveWorkspaceRecord(workspaceId);
|
|
||||||
} catch (error) {
|
|
||||||
dependencies.sessionLogger?.warn(
|
|
||||||
{ err: error, workspaceId },
|
|
||||||
"Failed to archive workspace record; worktree FS already removed",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const agentId of removedAgents) {
|
|
||||||
dependencies.emit({
|
|
||||||
type: "agent_deleted",
|
|
||||||
payload: {
|
|
||||||
agentId,
|
|
||||||
requestId: options.requestId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await dependencies.emitWorkspaceUpdatesForCwds(affectedWorkspaceCwds);
|
|
||||||
|
|
||||||
return Array.from(removedAgents);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handlePaseoWorktreeArchiveRequest(
|
export async function handlePaseoWorktreeArchiveRequest(
|
||||||
dependencies: Omit<ArchivePaseoWorktreeDependencies, "emitWorkspaceUpdatesForCwds"> & {
|
dependencies: Omit<
|
||||||
|
ArchivePaseoWorktreeDependencies,
|
||||||
|
"emitWorkspaceUpdatesForCwds" | "workspaceGitService"
|
||||||
|
> & {
|
||||||
emit: EmitSessionMessage;
|
emit: EmitSessionMessage;
|
||||||
|
workspaceGitService: Pick<WorkspaceGitService, "getSnapshot" | "listWorktrees">;
|
||||||
emitWorkspaceUpdatesForCwds: (cwds: Iterable<string>) => Promise<void>;
|
emitWorkspaceUpdatesForCwds: (cwds: Iterable<string>) => Promise<void>;
|
||||||
},
|
},
|
||||||
msg: Extract<SessionInboundMessage, { type: "paseo_worktree_archive_request" }>,
|
msg: Extract<SessionInboundMessage, { type: "paseo_worktree_archive_request" }>,
|
||||||
@@ -533,6 +372,9 @@ export async function handlePaseoWorktreeArchiveRequest(
|
|||||||
}
|
}
|
||||||
targetPath = match.path;
|
targetPath = match.path;
|
||||||
}
|
}
|
||||||
|
if (!targetPath) {
|
||||||
|
throw new Error("worktreePath could not be resolved");
|
||||||
|
}
|
||||||
|
|
||||||
const ownership = await isPaseoOwnedWorktreeCwd(targetPath, {
|
const ownership = await isPaseoOwnedWorktreeCwd(targetPath, {
|
||||||
paseoHome: dependencies.paseoHome,
|
paseoHome: dependencies.paseoHome,
|
||||||
@@ -779,53 +621,3 @@ export async function runWorktreeSetupInBackground(
|
|||||||
await dependencies.emitWorkspaceUpdateForCwd(worktree.worktreePath);
|
await dependencies.emitWorkspaceUpdateForCwd(worktree.worktreePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function killTerminalsUnderPath(
|
|
||||||
dependencies: KillTerminalsUnderPathDependencies,
|
|
||||||
rootPath: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const terminalManager = dependencies.terminalManager;
|
|
||||||
if (!terminalManager) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const terminalIds: string[] = [];
|
|
||||||
const terminalDirectories = [...terminalManager.listDirectories()];
|
|
||||||
for (const terminalCwd of terminalDirectories) {
|
|
||||||
if (!dependencies.isPathWithinRoot(rootPath, terminalCwd)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const terminals = await terminalManager.getTerminals(terminalCwd);
|
|
||||||
for (const terminal of terminals) {
|
|
||||||
terminalIds.push(terminal.id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
dependencies.sessionLogger.warn(
|
|
||||||
{ err: error, cwd: terminalCwd },
|
|
||||||
"Failed to enumerate worktree terminals during archive",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (terminalIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.allSettled(
|
|
||||||
terminalIds.map(async (terminalId) => {
|
|
||||||
try {
|
|
||||||
dependencies.detachTerminalStream?.(terminalId, { emitExit: true });
|
|
||||||
await terminalManager.killTerminalAndWait(terminalId, {
|
|
||||||
gracefulTimeoutMs: 2000,
|
|
||||||
forceTimeoutMs: 1500,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dependencies.sessionLogger.warn(
|
|
||||||
{ err: error, terminalId },
|
|
||||||
"Terminal kill escalation failed during archive; proceeding anyway",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user