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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user