diff --git a/packages/server/src/server/agent/agent-archive.test.ts b/packages/server/src/server/agent/agent-archive.test.ts new file mode 100644 index 000000000..c4dd34b7e --- /dev/null +++ b/packages/server/src/server/agent/agent-archive.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from "vitest"; + +import { buildArchivedAgentRecord } from "./agent-archive.js"; +import type { StoredAgentRecord } from "./agent-storage.js"; + +const BASE_RECORD: StoredAgentRecord = { + id: "agent-1", + provider: "codex", + cwd: "/workspace/project", + createdAt: "2025-01-01T00:00:00.000Z", + updatedAt: "2025-01-02T00:00:00.000Z", + labels: {}, + lastStatus: "idle", + config: null, +}; + +test("archives a stored agent without changing terminal statuses", () => { + const statuses: Array = ["idle", "error", "closed"]; + + for (const status of statuses) { + const archived = buildArchivedAgentRecord( + { ...BASE_RECORD, lastStatus: status }, + { archivedAt: "2025-01-03T00:00:00.000Z" }, + ); + + expect(archived.lastStatus).toBe(status); + expect(archived.archivedAt).toBe("2025-01-03T00:00:00.000Z"); + expect(archived.updatedAt).toBe(BASE_RECORD.updatedAt); + } +}); + +test("archives busy stored agents as idle", () => { + const statuses: Array = ["initializing", "running"]; + + for (const status of statuses) { + const archived = buildArchivedAgentRecord( + { ...BASE_RECORD, lastStatus: status }, + { archivedAt: "2025-01-03T00:00:00.000Z" }, + ); + + expect(archived.lastStatus).toBe("idle"); + } +}); + +test("clears persisted attention when archiving", () => { + const archived = buildArchivedAgentRecord( + { + ...BASE_RECORD, + requiresAttention: true, + attentionReason: "finished", + attentionTimestamp: "2025-01-02T12:00:00.000Z", + }, + { archivedAt: "2025-01-03T00:00:00.000Z" }, + ); + + expect(archived).toMatchObject({ + requiresAttention: false, + attentionReason: null, + attentionTimestamp: null, + }); +}); + +test("can stamp updatedAt to the archive timestamp", () => { + const archived = buildArchivedAgentRecord(BASE_RECORD, { + archivedAt: "2025-01-03T00:00:00.000Z", + updatedAt: "2025-01-03T00:00:00.000Z", + }); + + expect(archived.updatedAt).toBe("2025-01-03T00:00:00.000Z"); +}); diff --git a/packages/server/src/server/agent/agent-archive.ts b/packages/server/src/server/agent/agent-archive.ts new file mode 100644 index 000000000..7e35d98f8 --- /dev/null +++ b/packages/server/src/server/agent/agent-archive.ts @@ -0,0 +1,30 @@ +import type { StoredAgentRecord } from "./agent-storage.js"; + +export type ArchivedStoredAgentRecord = StoredAgentRecord & { archivedAt: string }; + +interface BuildArchivedAgentRecordOptions { + archivedAt?: string; + updatedAt?: string; +} + +export function buildArchivedAgentRecord( + record: StoredAgentRecord, + options?: BuildArchivedAgentRecordOptions, +): ArchivedStoredAgentRecord { + const archivedAt = options?.archivedAt ?? new Date().toISOString(); + return { + ...record, + archivedAt, + updatedAt: options?.updatedAt ?? record.updatedAt, + lastStatus: normalizeArchivedStatus(record.lastStatus), + requiresAttention: false, + attentionReason: null, + attentionTimestamp: null, + }; +} + +function normalizeArchivedStatus( + status: StoredAgentRecord["lastStatus"], +): StoredAgentRecord["lastStatus"] { + return status === "running" || status === "initializing" ? "idle" : status; +} diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 62afd20b7..5d047f0f4 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -35,6 +35,7 @@ import type { ListPersistedAgentsOptions, PersistedAgentDescriptor, } from "./agent-sdk-types.js"; +import { buildArchivedAgentRecord, type ArchivedStoredAgentRecord } from "./agent-archive.js"; import type { StoredAgentRecord, AgentStorage } from "./agent-storage.js"; import { InMemoryAgentTimelineStore, @@ -66,7 +67,6 @@ const STORED_AGENT_CAPABILITIES: AgentCapabilityFlags = { }; type TimeoutResult = "completed" | "timed_out"; -type ArchivedStoredAgentRecord = StoredAgentRecord & { archivedAt: string }; interface TimeoutOptions { operation: Promise; @@ -1071,19 +1071,7 @@ export class AgentManager { private async markRecordArchived(record: StoredAgentRecord): Promise { const registry = this.requireRegistry(); const archivedAt = new Date().toISOString(); - const normalizedStatus = - record.lastStatus === "running" || record.lastStatus === "initializing" - ? "idle" - : record.lastStatus; - const archivedRecord: ArchivedStoredAgentRecord = { - ...record, - archivedAt, - updatedAt: archivedAt, - lastStatus: normalizedStatus, - requiresAttention: false, - attentionReason: null, - attentionTimestamp: null, - }; + const archivedRecord = buildArchivedAgentRecord(record, { archivedAt, updatedAt: archivedAt }); await registry.upsert(archivedRecord); @@ -1259,19 +1247,7 @@ export class AgentManager { throw new Error(`Agent not found: ${agentId}`); } - const normalizedStatus = - record.lastStatus === "running" || record.lastStatus === "initializing" - ? "idle" - : record.lastStatus; - - const nextRecord: StoredAgentRecord = { - ...record, - archivedAt, - lastStatus: normalizedStatus, - requiresAttention: false, - attentionReason: null, - attentionTimestamp: null, - }; + const nextRecord = buildArchivedAgentRecord(record, { archivedAt }); await registry.upsert(nextRecord); await this.archiveNativeSessionBestEffort(record.provider, record.persistence);