diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index eceba3800..b15013937 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -408,6 +408,36 @@ describe("AgentManager", () => { expect(afterHydrate).toEqual(beforeReload); }); + test("reloadAgentSession preserves current title when config title is unset", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-title-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + const manager = new AgentManager({ + clients: { + codex: new TestAgentClient(), + }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000126", + }); + + const snapshot = await manager.createAgent({ + provider: "codex", + cwd: workdir, + }); + await manager.setTitle(snapshot.id, "Generated title"); + + const beforeReload = await storage.get(snapshot.id); + expect(beforeReload?.title).toBe("Generated title"); + expect(beforeReload?.config?.title).toBeUndefined(); + + await manager.reloadAgentSession(snapshot.id); + + const afterReload = await storage.get(snapshot.id); + expect(afterReload?.title).toBe("Generated title"); + expect(afterReload?.config?.title).toBeUndefined(); + }); + test("reloadAgentSession cancels active run and resumes existing session once thread_started is observed", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-active-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index ce2227b61..a343404be 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -1434,6 +1434,10 @@ export class AgentManager { if (this.agents.has(resolvedAgentId)) { throw new Error(`Agent with id ${resolvedAgentId} already exists`); } + const initialPersistedTitle = await this.resolveInitialPersistedTitle( + resolvedAgentId, + config + ); const now = new Date(); const initialTimeline = options?.timeline ? [...options.timeline] : []; @@ -1494,7 +1498,7 @@ export class AgentManager { this.previousStatuses.set(resolvedAgentId, managed.lifecycle); await this.refreshRuntimeInfo(managed); await this.persistSnapshot(managed, { - title: config.title ?? null, + title: initialPersistedTitle, }); this.emitState(managed); @@ -1506,6 +1510,20 @@ export class AgentManager { return { ...managed }; } + private async resolveInitialPersistedTitle( + agentId: string, + config: AgentSessionConfig + ): Promise { + const existing = await this.registry?.get(agentId); + if (existing) { + return existing.title ?? null; + } + if (Object.prototype.hasOwnProperty.call(config, "title")) { + return config.title ?? null; + } + return null; + } + private buildTimelineRowsFromItems( items: readonly AgentTimelineItem[], startSeq: number, diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index f86aa5559..d80e0f543 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -138,6 +138,9 @@ function buildSerializableConfig( config: AgentSessionConfig ): SerializableAgentConfig | null { const serializable: SerializableAgentConfig = {}; + if (Object.prototype.hasOwnProperty.call(config, "title")) { + serializable.title = config.title ?? null; + } if (config.modeId) { serializable.modeId = config.modeId; } diff --git a/packages/server/src/server/agent/agent-storage.test.ts b/packages/server/src/server/agent/agent-storage.test.ts index c3868167c..17bad2832 100644 --- a/packages/server/src/server/agent/agent-storage.test.ts +++ b/packages/server/src/server/agent/agent-storage.test.ts @@ -36,6 +36,7 @@ function createManagedAgent( const config: AgentSessionConfig = { provider, cwd, + title: configOverrides.title, modeId: configOverrides.modeId ?? "plan", model: configOverrides.model ?? "gpt-5.1", extra: configOverrides.extra ?? { claude: { maxThinkingTokens: 1024 } }, @@ -118,6 +119,7 @@ describe("AgentStorage", () => { currentModeId: "coding", lifecycle: "idle", config: { + title: "Initial title", modeId: "coding", model: "gpt-5.1", systemPrompt: "Be terse and explicit.", @@ -137,6 +139,7 @@ describe("AgentStorage", () => { expect(records).toHaveLength(1); const [record] = records; expect(record.provider).toBe("claude"); + expect(record.config?.title).toBe("Initial title"); expect(record.config?.modeId).toBe("coding"); expect(record.config?.model).toBe("gpt-5.1"); expect(record.config?.systemPrompt).toBe("Be terse and explicit."); diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index fa9cbfff7..5b751d618 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -11,6 +11,7 @@ import type { AgentSessionConfig } from "./agent-sdk-types.js"; const SERIALIZABLE_CONFIG_SCHEMA = z .object({ + title: z.string().nullable().optional(), modeId: z.string().nullable().optional(), model: z.string().nullable().optional(), thinkingOptionId: z.string().nullable().optional(), @@ -64,7 +65,13 @@ const STORED_AGENT_SCHEMA = z.object({ export type SerializableAgentConfig = Pick< AgentSessionConfig, - "modeId" | "model" | "thinkingOptionId" | "extra" | "systemPrompt" | "mcpServers" + | "title" + | "modeId" + | "model" + | "thinkingOptionId" + | "extra" + | "systemPrompt" + | "mcpServers" >; export type StoredAgentRecord = z.infer; diff --git a/packages/server/src/server/persistence-hooks.test.ts b/packages/server/src/server/persistence-hooks.test.ts index a7b3aa819..cc8b30c06 100644 --- a/packages/server/src/server/persistence-hooks.test.ts +++ b/packages/server/src/server/persistence-hooks.test.ts @@ -145,8 +145,9 @@ describe("persistence hooks", () => { test("buildConfigOverrides carries systemPrompt and mcpServers", () => { const record = createRecord({ - title: "Voice agent", + title: "Voice agent (current)", config: { + title: "Voice agent (created)", modeId: "default", model: "gpt-5.1-codex-mini", thinkingOptionId: "minimal", @@ -166,7 +167,7 @@ describe("persistence hooks", () => { modeId: "plan", model: "gpt-5.1-codex-mini", thinkingOptionId: "minimal", - title: "Voice agent", + title: "Voice agent (created)", systemPrompt: "Use speak first.", mcpServers: { paseo: { @@ -181,7 +182,9 @@ describe("persistence hooks", () => { test("buildSessionConfig includes persisted systemPrompt and mcpServers", () => { const record = createRecord({ provider: "codex", + title: "Renamed title", config: { + title: "Creation title", modeId: "default", model: "gpt-5.1-codex-mini", systemPrompt: "Confirm and speak first.", @@ -200,6 +203,7 @@ describe("persistence hooks", () => { cwd: "/tmp/project", modeId: "plan", model: "gpt-5.1-codex-mini", + title: "Creation title", systemPrompt: "Confirm and speak first.", mcpServers: { paseo: { diff --git a/packages/server/src/server/persistence-hooks.ts b/packages/server/src/server/persistence-hooks.ts index 7f56e6841..a0332fc13 100644 --- a/packages/server/src/server/persistence-hooks.ts +++ b/packages/server/src/server/persistence-hooks.ts @@ -54,7 +54,7 @@ export function buildConfigOverrides( modeId: record.lastModeId ?? record.config?.modeId ?? undefined, model: record.config?.model ?? undefined, thinkingOptionId: record.config?.thinkingOptionId ?? undefined, - title: record.title ?? undefined, + title: record.config?.title ?? undefined, extra: record.config?.extra ?? undefined, systemPrompt: record.config?.systemPrompt ?? undefined, mcpServers: record.config?.mcpServers ?? undefined, diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index fa555cb62..48e51d804 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1725,7 +1725,6 @@ export class Session { } await this.agentStorage.applySnapshot(liveAgent, { - title: liveAgent.config.title ?? null, internal: liveAgent.internal, }) archivedRecord = await this.agentStorage.get(agentId) @@ -2397,7 +2396,17 @@ export class Session { private async handleCreateAgentRequest( msg: Extract ): Promise { - const { config, worktreeName, requestId, initialPrompt, outputSchema, git, images, labels } = + const { + config, + worktreeName, + requestId, + initialPrompt, + clientMessageId, + outputSchema, + git, + images, + labels, + } = msg this.sessionLogger.info( { cwd: config.cwd, provider: config.provider, worktreeName }, @@ -2432,7 +2441,7 @@ export class Session { await this.handleSendAgentMessage( snapshot.id, trimmedPrompt, - uuidv4(), + clientMessageId ?? uuidv4(), images, outputSchema ? { outputSchema } : undefined )