fix(app): reopen every selected archived History agent

History entries without workspace IDs returned before restoration, and a second archived agent sharing an in-flight workspace restore was dropped. Preserve both agent-only and deferred reopen intent.
This commit is contained in:
Mohamed Boudra
2026-07-16 15:11:06 +02:00
parent 4d4e708c5c
commit b1e9ac0009
4 changed files with 161 additions and 39 deletions
@@ -2,7 +2,11 @@ import { router, type Href } from "expo-router";
import { useSessionStore } from "@/stores/session-store";
import { buildWorkspaceTabPersistenceKey } from "@/stores/workspace-tabs-store";
import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime";
import { resolveNavigateToAgent, type NavigateToAgentInput } from "./resolve";
import {
resolveNavigateToAgent,
type HistoryRestoreTarget,
type NavigateToAgentInput,
} from "./resolve";
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
export type { NavigateToAgentInput } from "./resolve";
@@ -13,34 +17,31 @@ export type { NavigateToAgentInput } from "./resolve";
// to avoid flashing a false "failed" on a capable daemon doing slow real work.
const RESTORE_TIMEOUT_MS = 30000;
interface PendingWorkspaceRestore {
target: HistoryWorkspaceRestore;
interface PendingHistoryRestore {
target: HistoryRestoreTarget;
unsubscribe: () => void;
}
interface HistoryWorkspaceRestore {
serverId: string;
agentId: string;
workspaceId: string;
agentArchived: boolean;
}
const pendingHydrationRestores = new Map<string, PendingHistoryRestore>();
const pendingAgentReopens = new Map<string, PendingHistoryRestore>();
const pendingWorkspaceRestores = new Map<string, PendingWorkspaceRestore>();
function restoreWhenWorkspacesHydrate(target: HistoryWorkspaceRestore): void {
function restoreWhenWorkspacesHydrate(target: HistoryRestoreTarget): void {
const { serverId, workspaceId } = target;
if (!workspaceId) {
return;
}
const key = buildWorkspaceTabPersistenceKey({ serverId, workspaceId });
if (!key) {
return;
}
const pending = pendingWorkspaceRestores.get(key);
const pending = pendingHydrationRestores.get(key);
if (pending) {
pending.target = target;
return;
}
const restore: PendingWorkspaceRestore = {
const restore: PendingHistoryRestore = {
target,
unsubscribe: () => {},
};
@@ -51,15 +52,50 @@ function restoreWhenWorkspacesHydrate(target: HistoryWorkspaceRestore): void {
}
restore.unsubscribe();
pendingWorkspaceRestores.delete(key);
pendingHydrationRestores.delete(key);
if (session) {
restoreArchivedWorkspace(restore.target);
restoreHistoryEntry(restore.target);
}
});
pendingWorkspaceRestores.set(key, restore);
pendingHydrationRestores.set(key, restore);
}
function restoreArchivedWorkspace(target: HistoryWorkspaceRestore): void {
function reopenAgentAfterWorkspaceRestore(target: HistoryRestoreTarget): void {
const { serverId, workspaceId } = target;
if (!workspaceId) {
return;
}
const key = buildWorkspaceTabPersistenceKey({ serverId, workspaceId });
if (!key) {
return;
}
const pending = pendingAgentReopens.get(key);
if (pending) {
pending.target = target;
return;
}
const restore: PendingHistoryRestore = {
target,
unsubscribe: () => {},
};
restore.unsubscribe = useSessionStore.subscribe((state) => {
const session = state.sessions[serverId];
if (session?.restoringWorkspaces.get(workspaceId) === "restoring") {
return;
}
restore.unsubscribe();
pendingAgentReopens.delete(key);
if (session?.workspaces.has(workspaceId)) {
restoreHistoryEntry(restore.target);
}
});
pendingAgentReopens.set(key, restore);
}
function restoreHistoryEntry(target: HistoryRestoreTarget): void {
const { serverId, agentId, workspaceId, agentArchived } = target;
const snapshot = getHostRuntimeStore().getSnapshot(serverId);
const client = snapshot?.client ?? null;
@@ -72,7 +108,24 @@ function restoreArchivedWorkspace(target: HistoryWorkspaceRestore): void {
if (!session) {
return;
}
const liveAgent = session.agents.get(agentId) ?? session.agentDetails.get(agentId);
const shouldReopenAgent = agentArchived && (!liveAgent || Boolean(liveAgent.archivedAt));
if (!workspaceId) {
if (shouldReopenAgent) {
client.refreshAgent(agentId).catch((error) => {
console.error("[HistoryRestore] Failed to reopen archived agent", {
serverId,
agentId,
error,
});
});
}
return;
}
if (session.restoringWorkspaces.get(workspaceId) === "restoring") {
if (shouldReopenAgent) {
reopenAgentAfterWorkspaceRestore(target);
}
return;
}
// An empty workspace map is not authoritative during startup. Keep the route on
@@ -87,8 +140,6 @@ function restoreArchivedWorkspace(target: HistoryWorkspaceRestore): void {
// must restore even when its agent survived unarchived; an archived agent must
// still reopen when its workspace survived.
if (session.workspaces.has(workspaceId)) {
const liveAgent = session.agents.get(agentId) ?? session.agentDetails.get(agentId);
const shouldReopenAgent = agentArchived && (!liveAgent || Boolean(liveAgent.archivedAt));
if (shouldReopenAgent) {
client.refreshAgent(agentId).catch((error) => {
console.error("[HistoryRestore] Failed to reopen archived agent", {
@@ -136,6 +187,6 @@ export function navigateToAgent(input: NavigateToAgentInput): string {
router.navigate(route as Href);
},
navigateToWorkspace,
restoreArchivedWorkspace,
restoreHistoryEntry,
});
}
@@ -19,7 +19,7 @@ interface RecordedTabNav extends NavigateToWorkspaceInput {}
interface RecordedRestore {
serverId: string;
agentId: string;
workspaceId: string;
workspaceId: string | null;
agentArchived: boolean;
}
@@ -45,7 +45,7 @@ function createFakeNavigators(target: AgentNavTarget): {
tabNavigations.push(input);
return `/h/${input.serverId}/workspace/${input.workspaceId}`;
},
restoreArchivedWorkspace: (input) => {
restoreHistoryEntry: (input) => {
restores.push(input);
},
},
@@ -147,6 +147,31 @@ describe("resolveNavigateToAgent", () => {
expect(hostNavigations).toHaveLength(1);
});
it("reopens an archived History agent even when it has no workspaceId", () => {
const { deps, restores, hostNavigations } = createFakeNavigators({
agentWorkspaceId: null,
});
resolveNavigateToAgent(
{
serverId: SERVER_ID,
agentId: AGENT_ID,
restoreWorkspace: { agentArchived: true },
},
deps,
);
expect(restores).toEqual([
{
serverId: SERVER_ID,
agentId: AGENT_ID,
workspaceId: null,
agentArchived: true,
},
]);
expect(hostNavigations).toEqual([{ route: "/h/server-1/agent/agent-1" }]);
});
it("falls back to the host agent route when the agent has no workspaceId", () => {
const { deps, hostNavigations, tabNavigations } = createFakeNavigators({
agentWorkspaceId: null,
@@ -21,16 +21,18 @@ export interface AgentNavTarget {
agentWorkspaceId: string | null | undefined;
}
export interface HistoryRestoreTarget {
serverId: string;
agentId: string;
workspaceId: string | null;
agentArchived: boolean;
}
export interface NavigateToAgentDeps {
readAgentNavTarget: (input: { serverId: string; agentId: string }) => AgentNavTarget;
navigateToHostAgent: (route: string) => void;
navigateToWorkspace: (input: NavigateToWorkspaceInput) => string;
restoreArchivedWorkspace: (input: {
serverId: string;
agentId: string;
workspaceId: string;
agentArchived: boolean;
}) => void;
restoreHistoryEntry: (input: HistoryRestoreTarget) => void;
}
export function resolveNavigateToAgent(
@@ -42,14 +44,8 @@ export function resolveNavigateToAgent(
deps.readAgentNavTarget({ serverId: input.serverId, agentId: input.agentId }).agentWorkspaceId;
const workspaceId = normalizeWorkspaceOpaqueId(agentWorkspaceId);
if (!workspaceId) {
const route = buildHostAgentDetailRoute(input.serverId, input.agentId);
deps.navigateToHostAgent(route);
return route;
}
if (input.restoreWorkspace) {
deps.restoreArchivedWorkspace({
deps.restoreHistoryEntry({
serverId: input.serverId,
agentId: input.agentId,
workspaceId,
@@ -57,6 +53,12 @@ export function resolveNavigateToAgent(
});
}
if (!workspaceId) {
const route = buildHostAgentDetailRoute(input.serverId, input.agentId);
deps.navigateToHostAgent(route);
return route;
}
return deps.navigateToWorkspace({
serverId: input.serverId,
workspaceId,
@@ -25,14 +25,15 @@ import { navigateToAgent } from "./index";
const SERVER_ID = "remote-server";
const AGENT_ID = "00000000-0000-4000-8000-000000000001";
const SECOND_AGENT_ID = "00000000-0000-4000-8000-000000000002";
const WORKSPACE_ID = "wks_history_restore";
const MISSING_WORKTREE_CWD = "/home/tester/.paseo/worktrees/missing-history-worktree";
function agent(archivedAt: Date | null): Agent {
function agent(archivedAt: Date | null, id = AGENT_ID): Agent {
const createdAt = new Date("2026-01-01T00:00:00.000Z");
return {
serverId: SERVER_ID,
id: AGENT_ID,
id,
provider: "codex",
status: "idle",
createdAt,
@@ -117,9 +118,13 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => {
});
function openFromHistory(entry = { agentArchived: true }): void {
openHistoryAgent(AGENT_ID, entry);
}
function openHistoryAgent(agentId: string, entry = { agentArchived: true }): void {
const input = {
serverId: SERVER_ID,
agentId: AGENT_ID,
agentId,
workspaceId: WORKSPACE_ID,
restoreWorkspace: entry,
};
@@ -150,6 +155,25 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => {
expect(refreshAgent).toHaveBeenCalledTimes(1);
});
it("reopens the selected archived agent after another agent restores its workspace", () => {
const store = useSessionStore.getState();
store.setAgents(SERVER_ID, (prev) => {
const next = new Map(prev);
next.set(SECOND_AGENT_ID, agent(new Date("2026-01-02T00:00:00.000Z"), SECOND_AGENT_ID));
return next;
});
refreshAgent.mockImplementation(() => new Promise(() => {}));
openFromHistory();
openHistoryAgent(SECOND_AGENT_ID);
expect(refreshAgent.mock.calls).toEqual([[AGENT_ID]]);
store.mergeWorkspaces(SERVER_ID, [workspace()]);
expect(refreshAgent.mock.calls).toEqual([[AGENT_ID], [SECOND_AGENT_ID]]);
});
it("restores a History-opened closed agent whose archived workspace is missing", () => {
const store = useSessionStore.getState();
store.setAgents(SERVER_ID, (prev) => {
@@ -166,6 +190,26 @@ describe("restoreArchivedWorkspace via navigateToAgent", () => {
expect(status()).toBe("restoring");
});
it("reopens an archived History agent without a workspace identity", () => {
const store = useSessionStore.getState();
store.setAgents(SERVER_ID, (prev) => {
const next = new Map(prev);
next.delete(AGENT_ID);
return next;
});
refreshAgent.mockImplementation(() => new Promise(() => {}));
navigateToAgent({
serverId: SERVER_ID,
agentId: AGENT_ID,
restoreWorkspace: { agentArchived: true },
});
expect(refreshAgent).toHaveBeenCalledTimes(1);
expect(refreshAgent).toHaveBeenCalledWith(AGENT_ID);
expect(status()).toBeNull();
});
it("does not restore during ordinary active-workspace navigation", () => {
const store = useSessionStore.getState();
store.setAgents(SERVER_ID, (prev) => {