Fix bulk close archiving for stored agents

This commit is contained in:
Mohamed Boudra
2026-04-03 18:49:59 +07:00
parent 763cfa9333
commit f924d33076
2 changed files with 206 additions and 4 deletions
+44 -4
View File
@@ -2003,17 +2003,53 @@ export class Session {
});
}
private async archiveStoredAgentForClose(
agentId: string,
): Promise<{ agentId: string; archivedAt: string }> {
const existing = await this.agentStorage.get(agentId);
if (!existing) {
throw new Error(`Agent not found: ${agentId}`);
}
if (existing.archivedAt) {
return {
agentId,
archivedAt: existing.archivedAt,
};
}
const archivedAt = new Date().toISOString();
const normalizedStatus =
existing.lastStatus === "running" || existing.lastStatus === "initializing"
? "idle"
: existing.lastStatus;
await this.agentStorage.upsert({
...existing,
archivedAt,
updatedAt: archivedAt,
lastStatus: normalizedStatus,
requiresAttention: false,
attentionReason: null,
attentionTimestamp: null,
});
return { agentId, archivedAt };
}
private async archiveAgentForClose(
agentId: string,
): Promise<{ agentId: string; archivedAt: string }> {
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
if (this.agentManager.getAgent(agentId)) {
const liveAgent = this.agentManager.getAgent(agentId);
if (liveAgent) {
await this.interruptAgentIfRunning(agentId);
await this.agentManager.clearAgentAttention(agentId).catch(() => undefined);
await this.agentManager.archiveAgent(agentId);
} else {
await this.archiveStoredAgentForClose(agentId);
}
const { archivedAt } = await this.agentManager.archiveAgent(agentId);
const archivedRecord = await this.agentStorage.get(agentId);
if (!archivedRecord) {
throw new Error(`Agent not found in storage after archive: ${agentId}`);
@@ -2043,7 +2079,11 @@ export class Session {
await this.emitWorkspaceUpdateForCwd(payload.cwd);
}
return { agentId, archivedAt };
if (!archivedRecord.archivedAt) {
throw new Error(`Agent missing archivedAt after archive: ${agentId}`);
}
return { agentId, archivedAt: archivedRecord.archivedAt };
}
private async handleCloseItemsRequest(msg: CloseItemsRequest): Promise<void> {
@@ -193,6 +193,9 @@ describe("workspace aggregation", () => {
agentStorage: {
list: async () => [archivedRecord],
get: async (agentId: string) => (agentId === archivedRecord.id ? archivedRecord : null),
upsert: async (record: typeof archivedRecord) => {
Object.assign(archivedRecord, record);
},
} as any,
projectRegistry: {
initialize: async () => {},
@@ -428,6 +431,165 @@ describe("workspace aggregation", () => {
});
});
test("close_items_request archives stored agents that are not currently loaded", async () => {
const emitted: Array<{ type: string; payload: any }> = [];
const sessionLogger = {
child: () => sessionLogger,
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const liveArchivedAt = "2026-04-01T00:00:00.000Z";
const storedAgentId = "agent-stored";
const liveRecord = {
...makeAgent({
id: "agent-live",
cwd: "/tmp/repo",
status: "idle",
updatedAt: "2026-03-01T12:00:00.000Z",
}),
archivedAt: null as string | null,
};
const storedRecord = {
...makeAgent({
id: storedAgentId,
cwd: "/tmp/repo",
status: "idle",
updatedAt: "2026-03-01T12:05:00.000Z",
}),
archivedAt: null as string | null,
};
const upsertStoredRecord = vi.fn(async (record: typeof storedRecord) => {
if (record.id === storedAgentId) {
storedRecord.archivedAt = record.archivedAt;
storedRecord.updatedAt = record.updatedAt;
storedRecord.status = record.status;
storedRecord.requiresAttention = record.requiresAttention;
storedRecord.attentionReason = record.attentionReason;
storedRecord.attentionTimestamp = record.attentionTimestamp;
}
});
const session = new Session({
clientId: "test-client",
onMessage: (message) => emitted.push(message as any),
logger: sessionLogger as any,
downloadTokenStore: {} as any,
pushTokenStore: {} as any,
paseoHome: "/tmp/paseo-test",
agentManager: {
subscribe: () => () => {},
listAgents: () => [],
getAgent: (agentId: string) => (agentId === "agent-live" ? { id: agentId } : null),
archiveAgent: async (agentId: string) => {
if (agentId !== "agent-live") {
throw new Error(`Unexpected live archive: ${agentId}`);
}
liveRecord.archivedAt = liveArchivedAt;
liveRecord.updatedAt = liveArchivedAt;
return { archivedAt: liveArchivedAt };
},
clearAgentAttention: async () => {},
notifyAgentState: () => {},
} as any,
agentStorage: {
list: async () => [],
get: async (agentId: string) => {
if (agentId === "agent-live") {
return liveRecord;
}
if (agentId === storedAgentId) {
return storedRecord;
}
return null;
},
upsert: upsertStoredRecord,
} as any,
projectRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
workspaceRegistry: {
initialize: async () => {},
existsOnDisk: async () => true,
list: async () => [],
get: async () => null,
upsert: async () => {},
archive: async () => {},
remove: async () => {},
} as any,
checkoutDiffManager: {
subscribe: async () => ({
initial: { cwd: "/tmp", files: [], error: null },
unsubscribe: () => {},
}),
scheduleRefreshForCwd: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
checkoutDiffWatcherCount: 0,
checkoutDiffFallbackRefreshTargetCount: 0,
}),
dispose: () => {},
} as any,
createAgentMcpTransport: async () => {
throw new Error("not used");
},
stt: null,
tts: null,
terminalManager: {
killTerminal: vi.fn(),
subscribeTerminalsChanged: () => () => {},
} as any,
}) as any;
session.agentUpdatesSubscription = {
subscriptionId: "sub-agents",
filter: { includeArchived: true },
isBootstrapping: false,
pendingUpdatesByAgentId: new Map(),
};
session.buildProjectPlacement = async (cwd: string) => ({
projectKey: cwd,
projectName: "repo",
checkout: {
cwd,
isGit: false,
currentBranch: null,
remoteUrl: null,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
},
});
session.interruptAgentIfRunning = vi.fn();
await session.handleMessage({
type: "close_items_request",
agentIds: ["agent-live", storedAgentId],
terminalIds: [],
requestId: "req-close-stored",
});
expect(upsertStoredRecord).toHaveBeenCalledTimes(1);
expect(storedRecord.archivedAt).toEqual(expect.any(String));
expect(emitted.find((message) => message.type === "close_items_response")?.payload).toEqual({
agents: [
{ agentId: "agent-live", archivedAt: liveArchivedAt },
{ agentId: storedAgentId, archivedAt: storedRecord.archivedAt },
],
terminals: [],
requestId: "req-close-stored",
});
expect(sessionLogger.warn).not.toHaveBeenCalled();
});
test("close_items_request continues after an archive failure", async () => {
const emitted: Array<{ type: string; payload: any }> = [];
const sessionLogger = {