fix(server): harden worktree archive with parallel teardown

Fan out agent close + terminal kill concurrently with Promise.allSettled
so one failure never leaves the worktree half-dead. Await real PTY exit
with SIGHUP→SIGKILL escalation before rm to avoid EBUSY races. Tolerate
missing repoRoot and retry directory removal with backoff. Purge stale
client workspace layout/tab state after successful archive.
This commit is contained in:
Mohamed Boudra
2026-04-17 15:50:48 +07:00
parent ed2dc08443
commit d6585f7c3a
9 changed files with 537 additions and 65 deletions
@@ -1,6 +1,11 @@
import { create } from "zustand";
import { useSessionStore } from "@/stores/session-store";
import { queryClient } from "@/query/query-client";
import {
buildWorkspaceTabPersistenceKey,
useWorkspaceLayoutStore,
} from "@/stores/workspace-layout-store";
import { useWorkspaceTabsStore } from "@/stores/workspace-tabs-store";
const SUCCESS_DISPLAY_MS = 1000;
@@ -123,6 +128,19 @@ function removeWorktreeFromCachedLists(input: { serverId: string; worktreePath:
);
}
function purgeArchivedWorkspaceState(input: { serverId: string; worktreePath: string }): void {
const serverId = input.serverId.trim();
const workspaceId = input.worktreePath.trim();
if (!serverId || !workspaceId) {
return;
}
const workspaceKey = buildWorkspaceTabPersistenceKey({ serverId, workspaceId });
if (workspaceKey) {
useWorkspaceLayoutStore.getState().purgeWorkspace(workspaceKey);
}
useWorkspaceTabsStore.getState().purgeWorkspace({ serverId, workspaceId });
}
const successTimers = new Map<string, ReturnType<typeof setTimeout>>();
const inFlight = new Map<string, Promise<unknown>>();
@@ -320,6 +338,7 @@ export const useCheckoutGitActionsStore = create<CheckoutGitActionsStoreState>()
}
removeWorktreeFromCachedLists({ serverId, worktreePath });
invalidateWorktreeList();
purgeArchivedWorkspaceState({ serverId, worktreePath });
},
});
},
@@ -99,6 +99,7 @@ interface WorkspaceLayoutStore {
unpinAgent: (workspaceKey: string, agentId: string) => void;
hideAgent: (workspaceKey: string, agentId: string) => void;
unhideAgent: (workspaceKey: string, agentId: string) => void;
purgeWorkspace: (workspaceKey: string) => void;
}
const MAX_TREE_DEPTH = 4;
@@ -669,6 +670,37 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
};
});
},
purgeWorkspace: (workspaceKey) => {
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
if (!normalizedWorkspaceKey) {
return;
}
set((state) => {
const hasAny =
normalizedWorkspaceKey in state.layoutByWorkspace ||
normalizedWorkspaceKey in state.splitSizesByWorkspace ||
normalizedWorkspaceKey in state.pinnedAgentIdsByWorkspace ||
normalizedWorkspaceKey in state.hiddenAgentIdsByWorkspace;
if (!hasAny) {
return state;
}
const { [normalizedWorkspaceKey]: _layout, ...layoutByWorkspace } =
state.layoutByWorkspace;
const { [normalizedWorkspaceKey]: _splits, ...splitSizesByWorkspace } =
state.splitSizesByWorkspace;
const { [normalizedWorkspaceKey]: _pinned, ...pinnedAgentIdsByWorkspace } =
state.pinnedAgentIdsByWorkspace;
const { [normalizedWorkspaceKey]: _hidden, ...hiddenAgentIdsByWorkspace } =
state.hiddenAgentIdsByWorkspace;
return {
layoutByWorkspace,
splitSizesByWorkspace,
pinnedAgentIdsByWorkspace,
hiddenAgentIdsByWorkspace,
};
});
},
}),
{
name: "workspace-layout-state",
@@ -93,6 +93,7 @@ type WorkspaceTabsState = {
}) => string | null;
reorderTabs: (input: { serverId: string; workspaceId: string; tabIds: string[] }) => void;
getWorkspaceTabs: (input: { serverId: string; workspaceId: string }) => WorkspaceTab[];
purgeWorkspace: (input: { serverId: string; workspaceId: string }) => void;
};
export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
@@ -326,6 +327,31 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
}
return get().uiTabsByWorkspace[key] ?? [];
},
purgeWorkspace: ({ serverId, workspaceId }) => {
const key = buildWorkspaceTabPersistenceKey({ serverId, workspaceId });
if (!key) {
return;
}
set((state) => {
if (
!(key in state.uiTabsByWorkspace) &&
!(key in state.tabOrderByWorkspace) &&
!(key in state.focusedTabIdByWorkspace)
) {
return state;
}
const { [key]: _tabs, ...remainingUiTabsByWorkspace } = state.uiTabsByWorkspace;
const { [key]: _order, ...remainingTabOrderByWorkspace } = state.tabOrderByWorkspace;
const { [key]: _focused, ...remainingFocusedTabIdByWorkspace } =
state.focusedTabIdByWorkspace;
return {
...state,
uiTabsByWorkspace: remainingUiTabsByWorkspace,
tabOrderByWorkspace: remainingTabOrderByWorkspace,
focusedTabIdByWorkspace: remainingFocusedTabIdByWorkspace,
};
});
},
}),
{
name: "workspace-tabs-state",