refactor: update registry persistence to use ManagedAgent

Remove AgentSnapshot from persistence layer. AgentRegistry now accepts
ManagedAgent and uses toStoredAgentRecord for atomic config + lifecycle
persistence. Deleted obsolete recordConfig method.

- Update applySnapshot to accept ManagedAgent and use toStoredAgentRecord
- Remove recordConfig method entirely (no longer needed)
- Remove sanitizeConfig helper (handled by projection)
- Update session to stop calling deleted recordConfig
- Add ManagedAgent test fixtures to registry and persistence-hook tests
- Test config persistence, title retention, and subscription forwarding

Registry and persistence-hooks now typecheck cleanly. Expected failures
in session/mcp-server/messages (still reference AgentSnapshot).

Task 4 of 8 in agent architecture refactor.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-11-30 00:03:00 +00:00
co-authored by Claude
parent 8a708aab8b
commit 7aec973323
4 changed files with 191 additions and 188 deletions
@@ -1,37 +1,84 @@
import { describe, expect, test, beforeEach, afterEach, vi } from "vitest";
import { describe, expect, test, beforeEach, afterEach } from "vitest";
import os from "node:os";
import path from "node:path";
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
import { AgentRegistry } from "./agent-registry.js";
import type { AgentSnapshot } from "./agent-manager.js";
import type { ManagedAgent } from "./agent-manager.js";
import type {
AgentPermissionRequest,
AgentSession,
AgentSessionConfig,
} from "./agent-sdk-types.js";
function createSnapshot(overrides?: Partial<AgentSnapshot>): AgentSnapshot {
const now = new Date();
return {
id: "agent-test",
provider: "claude",
cwd: "/tmp/project",
model: null,
createdAt: now,
updatedAt: now,
lastUserMessageAt: null,
status: "idle",
sessionId: null,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
...overrides,
type ManagedAgentOverrides = Omit<
Partial<ManagedAgent>,
"config" | "pendingPermissions" | "session" | "pendingRun"
> & {
config?: Partial<AgentSessionConfig>;
pendingPermissions?: Map<string, AgentPermissionRequest>;
session?: AgentSession | null;
pendingRun?: ManagedAgent["pendingRun"];
};
function createManagedAgent(
overrides: ManagedAgentOverrides = {}
): ManagedAgent {
const now = overrides.updatedAt ?? new Date("2025-01-01T00:00:00.000Z");
const provider = overrides.provider ?? "claude";
const cwd = overrides.cwd ?? "/tmp/project";
const lifecycle = overrides.lifecycle ?? "idle";
const configOverrides = overrides.config ?? {};
const config: AgentSessionConfig = {
provider,
cwd,
modeId: configOverrides.modeId ?? "plan",
model: configOverrides.model ?? "gpt-5.1",
extra: configOverrides.extra ?? { claude: { maxThinkingTokens: 1024 } },
};
const session =
lifecycle === "closed"
? null
: overrides.session ?? ({} as AgentSession);
const pendingRun =
overrides.pendingRun ??
(lifecycle === "running" ? (async function* noop() {})() : null);
const agent: ManagedAgent = {
id: overrides.id ?? "agent-test",
provider,
cwd,
session,
sessionId: overrides.sessionId ?? "session-123",
capabilities:
overrides.capabilities ??
{
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
config,
lifecycle,
createdAt: overrides.createdAt ?? now,
updatedAt: overrides.updatedAt ?? now,
availableModes: overrides.availableModes ?? [],
currentModeId: overrides.currentModeId ?? config.modeId ?? null,
pendingPermissions:
overrides.pendingPermissions ??
new Map<string, AgentPermissionRequest>(),
pendingRun,
timeline: overrides.timeline ?? [],
persistence: overrides.persistence ?? null,
historyPrimed: overrides.historyPrimed ?? true,
lastUserMessageAt: overrides.lastUserMessageAt ?? now,
lastUsage: overrides.lastUsage,
lastError: overrides.lastError,
};
return agent;
}
describe("AgentRegistry", () => {
@@ -49,27 +96,21 @@ describe("AgentRegistry", () => {
rmSync(tmpDir, { recursive: true, force: true });
});
test("persists configs and snapshot metadata", async () => {
test("applySnapshot persists configs and snapshot metadata", async () => {
await registry.applySnapshot(
createSnapshot({
createManagedAgent({
id: "agent-1",
cwd: "/tmp/project",
currentModeId: "coding",
status: "idle",
lifecycle: "idle",
config: {
modeId: "coding",
model: "gpt-5.1",
extra: { claude: { maxThinkingTokens: 1024 } },
},
})
);
await registry.recordConfig(
"agent-1",
"claude",
"/tmp/project",
{
modeId: "coding",
model: "gpt-5.1",
extra: { claude: { maxThinkingTokens: 1024 } },
}
);
const records = await registry.list();
expect(records).toHaveLength(1);
const [record] = records;
@@ -87,7 +128,7 @@ describe("AgentRegistry", () => {
test("stores titles independently of snapshots", async () => {
await registry.applySnapshot(
createSnapshot({
createManagedAgent({
id: "agent-2",
provider: "codex",
cwd: "/tmp/second",
@@ -103,21 +144,30 @@ describe("AgentRegistry", () => {
expect(persisted?.title).toBe("Fix Login Bug");
});
test("recordConfig warns if no snapshot exists", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
await registry.recordConfig(
"agent-3",
"claude",
"/tmp/project",
{
modeId: "plan",
}
test("applySnapshot preserves custom titles while updating metadata", async () => {
const agentId = "agent-3";
await registry.applySnapshot(
createManagedAgent({
id: agentId,
lifecycle: "idle",
currentModeId: "plan",
})
);
await registry.setTitle(agentId, "Important Bug Fix");
await registry.applySnapshot(
createManagedAgent({
id: agentId,
lifecycle: "running",
currentModeId: "build",
updatedAt: new Date("2025-01-02T00:00:00.000Z"),
})
);
const record = await registry.get("agent-3");
expect(record).toBeNull();
expect(warnSpy).toHaveBeenCalled();
warnSpy.mockRestore();
const record = await registry.get(agentId);
expect(record?.title).toBe("Important Bug Fix");
expect(record?.lastModeId).toBe("build");
expect(record?.lastStatus).toBe("running");
});
test("recovers from trailing garbage in agents.json", async () => {