diff --git a/packages/app/e2e/helpers/terminal-perf.ts b/packages/app/e2e/helpers/terminal-perf.ts index ca0d91436..9e191fcc5 100644 --- a/packages/app/e2e/helpers/terminal-perf.ts +++ b/packages/app/e2e/helpers/terminal-perf.ts @@ -30,6 +30,10 @@ export interface TerminalPerfDaemonClient { featureValues?: Record; initialPrompt?: string; }): Promise<{ id: string; status: string }>; + fetchAgents(options?: { scope?: "active" }): Promise<{ + entries: Array<{ agent: { id: string; cwd: string; title?: string | null } }>; + }>; + updateAgent(agentId: string, updates: { name?: string }): Promise; waitForAgentUpsert( agentId: string, predicate: (snapshot: { status: string }) => boolean, diff --git a/packages/app/e2e/workspace-agent-title-handoff.spec.ts b/packages/app/e2e/workspace-agent-title-handoff.spec.ts new file mode 100644 index 000000000..5aa2352df --- /dev/null +++ b/packages/app/e2e/workspace-agent-title-handoff.spec.ts @@ -0,0 +1,239 @@ +import { test, expect, type Page } from "./fixtures"; +import { createTempGitRepo } from "./helpers/workspace"; +import { expectComposerVisible, submitMessage } from "./helpers/composer"; +import { connectTerminalClient, type TerminalPerfDaemonClient } from "./helpers/terminal-perf"; +import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; +import { captureWsSessionFrames } from "./helpers/rename"; +import { buildHostWorkspaceRoute } from "@/utils/host-routes"; + +interface WorkspaceTabProbeRecord { + at: number; + tabs: Array<{ + testId: string; + text: string; + ariaLabel: string; + }>; +} + +interface CapturedCreateAgentFrame { + initialPrompt: string | null; + configTitle: string | null; +} + +function getServerId(): string { + const serverId = process.env.E2E_SERVER_ID; + if (!serverId) { + throw new Error("E2E_SERVER_ID is not set."); + } + return serverId; +} + +async function installWorkspaceTabProbe(page: Page): Promise { + await page.evaluate(() => { + type ProbeRecord = WorkspaceTabProbeRecord; + type ProbeWindow = Window & { + __workspaceTabTitleProbe?: { records: ProbeRecord[]; stop: () => void }; + }; + + const win = window as ProbeWindow; + win.__workspaceTabTitleProbe?.stop(); + + const records: ProbeRecord[] = []; + const isVisible = (element: Element): element is HTMLElement => { + if (!(element instanceof HTMLElement)) { + return false; + } + const rect = element.getBoundingClientRect(); + const style = window.getComputedStyle(element); + return ( + rect.width > 0 && + rect.height > 0 && + style.display !== "none" && + style.visibility !== "hidden" + ); + }; + const snapshot = () => { + records.push({ + at: performance.now(), + tabs: Array.from(document.querySelectorAll('[data-testid^="workspace-tab-"]')) + .filter(isVisible) + .map((element) => ({ + testId: element.getAttribute("data-testid") ?? "", + text: (element.textContent ?? "").replace(/\s+/g, " ").trim(), + ariaLabel: element.getAttribute("aria-label") ?? "", + })), + }); + }; + + snapshot(); + const observer = new MutationObserver(snapshot); + observer.observe(document.body, { + subtree: true, + childList: true, + characterData: true, + attributes: true, + attributeFilter: ["aria-label", "class", "data-testid", "style"], + }); + const interval = window.setInterval(snapshot, 20); + win.__workspaceTabTitleProbe = { + records, + stop: () => { + observer.disconnect(); + window.clearInterval(interval); + }, + }; + }); +} + +async function readWorkspaceTabProbe(page: Page): Promise { + return page.evaluate(() => { + type ProbeWindow = Window & { + __workspaceTabTitleProbe?: { records: WorkspaceTabProbeRecord[]; stop: () => void }; + }; + const probe = (window as ProbeWindow).__workspaceTabTitleProbe; + probe?.stop(); + return probe?.records ?? []; + }); +} + +function recordHasTabLabel(record: WorkspaceTabProbeRecord, label: string): boolean { + return record.tabs.some((tab) => tab.text.includes(label) || tab.ariaLabel.includes(label)); +} + +function createFrameStartsWithPrompt( + frame: CapturedCreateAgentFrame, + promptTitle: string, +): boolean { + return frame.initialPrompt?.startsWith(promptTitle) ?? false; +} + +function countCreateFramesForPrompt( + frames: CapturedCreateAgentFrame[], + promptTitle: string, +): number { + return frames.filter((frame) => createFrameStartsWithPrompt(frame, promptTitle)).length; +} + +function tabHasLoadingTitle(tab: WorkspaceTabProbeRecord["tabs"][number]): boolean { + return /Loading agent title|Loading\.\.\./.test(`${tab.text} ${tab.ariaLabel}`); +} + +function recordHasLoadingTitle(record: WorkspaceTabProbeRecord): boolean { + return record.tabs.some(tabHasLoadingTitle); +} + +async function waitForCreatedAgentId( + client: TerminalPerfDaemonClient, + cwd: string, +): Promise { + await expect + .poll( + async () => { + const result = await client.fetchAgents({ scope: "active" }); + return result.entries + .filter((entry) => entry.agent.cwd === cwd) + .map((entry) => entry.agent.id); + }, + { timeout: 30_000 }, + ) + .toHaveLength(1); + const result = await client.fetchAgents({ scope: "active" }); + const agent = result.entries.find((entry) => entry.agent.cwd === cwd); + if (!agent) { + throw new Error(`Expected one created agent in ${cwd}`); + } + return agent.agent.id; +} + +async function fetchActiveAgentTitle( + client: TerminalPerfDaemonClient, + agentId: string, +): Promise { + const result = await client.fetchAgents({ scope: "active" }); + return result.entries.find((entry) => entry.agent.id === agentId)?.agent.title ?? null; +} + +async function waitForPromptTabAgentActions(page: Page, promptTitle: string): Promise { + const promptTab = page.getByRole("button", { name: promptTitle }).first(); + await expect(promptTab).toBeVisible({ timeout: 15_000 }); + + const deadline = Date.now() + 15_000; + while (Date.now() < deadline) { + await promptTab.click({ button: "right" }); + const renameAction = page.getByText("Rename", { exact: true }).first(); + if (await renameAction.isVisible().catch(() => false)) { + await page.keyboard.press("Escape"); + return; + } + await page.keyboard.press("Escape").catch(() => undefined); + await page.waitForTimeout(100); + } + + throw new Error("Prompt tab did not expose agent tab actions after create handoff"); +} + +test.describe("Workspace agent title handoff", () => { + test("keeps the prompt as the optimistic tab title until the generated title arrives", async ({ + page, + }) => { + test.setTimeout(120_000); + await page.setViewportSize({ width: 1440, height: 900 }); + + const client = await connectTerminalClient(); + const repo = await createTempGitRepo("workspace-title-handoff-"); + + try { + const opened = await client.openProject(repo.path); + if (!opened.workspace) { + throw new Error(opened.error ?? "Failed to open test workspace"); + } + + const createFrames = captureWsSessionFrames(page, "create_agent_request", (inner) => { + const config = (inner.config ?? {}) as Record; + return { + initialPrompt: typeof inner.initialPrompt === "string" ? inner.initialPrompt : null, + configTitle: typeof config.title === "string" ? config.title : null, + }; + }); + + await page.goto(buildHostWorkspaceRoute(getServerId(), opened.workspace.id)); + await waitForWorkspaceTabsVisible(page); + await page.getByTestId("workspace-new-agent-tab").click(); + await expectComposerVisible(page); + + const promptTitle = "Investigate optimistic tab title handoff"; + const generatedTitle = "Generated Handoff Title"; + await installWorkspaceTabProbe(page); + await submitMessage(page, `${promptTitle}\n\nMake the UI state deterministic.`); + + const agentId = await waitForCreatedAgentId(client, repo.path); + await expect + .poll(() => countCreateFramesForPrompt(createFrames, promptTitle), { + timeout: 10_000, + }) + .toBe(1); + expect(createFrames.at(-1)).toEqual({ + initialPrompt: `${promptTitle}\n\nMake the UI state deterministic.`, + configTitle: null, + }); + + await waitForPromptTabAgentActions(page, promptTitle); + + await client.updateAgent(agentId, { name: generatedTitle }); + await expect + .poll(() => fetchActiveAgentTitle(client, agentId), { timeout: 10_000 }) + .toBe(generatedTitle); + await expect(page.getByRole("button", { name: generatedTitle }).first()).toBeVisible({ + timeout: 15_000, + }); + + const records = await readWorkspaceTabProbe(page); + expect(records.some((record) => recordHasTabLabel(record, promptTitle))).toBe(true); + expect(records.some((record) => recordHasTabLabel(record, generatedTitle))).toBe(true); + expect(records.filter(recordHasLoadingTitle)).toEqual([]); + } finally { + await client.close().catch(() => undefined); + await repo.cleanup(); + } + }); +}); diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 0bad9d636..1b1f7a793 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -1593,32 +1593,7 @@ test("setTitle bumps updatedAt and persists title in the same snapshot write", a expect(live!.updatedAt.getTime()).toBeGreaterThan(Date.parse(before!.updatedAt)); }); -test("setGeneratedTitleIfUnset preserves an existing user title", async () => { - const workdir = mkdtempSync(join(tmpdir(), "agent-manager-generated-title-preserve-")); - 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-000000000128", - }); - - const snapshot = await manager.createAgent({ - provider: "codex", - cwd: workdir, - }); - - await manager.setTitle(snapshot.id, "User title"); - await manager.setGeneratedTitleIfUnset(snapshot.id, "Generated title"); - - const after = await storage.get(snapshot.id); - expect(after?.title).toBe("User title"); -}); - -test("setGeneratedTitleIfUnset persists generated title when no title exists", async () => { +test("setGeneratedTitle persists generated title when no title exists", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-generated-title-empty-")); const storagePath = join(workdir, "agents"); const storage = new AgentStorage(storagePath, logger); @@ -1636,13 +1611,13 @@ test("setGeneratedTitleIfUnset persists generated title when no title exists", a cwd: workdir, }); - await manager.setGeneratedTitleIfUnset(snapshot.id, "Generated title"); + await manager.setGeneratedTitle(snapshot.id, "Generated title"); const after = await storage.get(snapshot.id); expect(after?.title).toBe("Generated title"); }); -test("setGeneratedTitleIfUnset ignores blank generated titles", async () => { +test("setGeneratedTitle ignores blank generated titles", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-generated-title-blank-")); const storagePath = join(workdir, "agents"); const storage = new AgentStorage(storagePath, logger); @@ -1672,7 +1647,7 @@ test("setGeneratedTitleIfUnset ignores blank generated titles", async () => { { agentId: snapshot.id, replayState: false }, ); - await manager.setGeneratedTitleIfUnset(snapshot.id, " "); + await manager.setGeneratedTitle(snapshot.id, " "); const after = await storage.get(snapshot.id); expect(after?.title).toBeNull(); @@ -1681,7 +1656,7 @@ test("setGeneratedTitleIfUnset ignores blank generated titles", async () => { expect(stateEvents).toEqual([]); }); -test("setGeneratedTitleIfUnset throws for an unknown agent", async () => { +test("setGeneratedTitle throws for an unknown agent", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-generated-title-unknown-")); const storagePath = join(workdir, "agents"); const storage = new AgentStorage(storagePath, logger); @@ -1694,7 +1669,7 @@ test("setGeneratedTitleIfUnset throws for an unknown agent", async () => { }); await expect( - manager.setGeneratedTitleIfUnset("00000000-0000-4000-8000-000000000999", "Generated title"), + manager.setGeneratedTitle("00000000-0000-4000-8000-000000000999", "Generated title"), ).rejects.toThrow("Unknown agent '00000000-0000-4000-8000-000000000999'"); }); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index c7fb4c633..6cf589f2a 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -91,7 +91,6 @@ function buildStoredAgentConfig(record: StoredAgentRecord): AgentSessionConfig { if (!record.config) { return config; } - if (record.config.title != null) config.title = record.config.title; if (record.config.modeId != null) config.modeId = record.config.modeId; if (record.config.model != null) config.model = record.config.model; if (record.config.thinkingOptionId != null) { @@ -793,6 +792,7 @@ export class AgentManager { initialPrompt?: string; env?: Record; persistSession?: boolean; + initialTitle?: string | null; }, ): Promise { const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent"); @@ -822,6 +822,7 @@ export class AgentManager { return this.registerSession(session, normalizedConfig, resolvedAgentId, { labels: options?.labels, workspaceId: options?.workspaceId, + initialTitle: options?.initialTitle, }); } @@ -1234,7 +1235,7 @@ export class AgentManager { this.emitState(agent, { persist: false }); } - async setGeneratedTitleIfUnset(agentId: string, title: string): Promise { + async setGeneratedTitle(agentId: string, title: string): Promise { const agent = this.requireAgent(agentId); const normalizedTitle = title.trim(); if (!normalizedTitle) { @@ -1242,10 +1243,7 @@ export class AgentManager { } const registry = this.requireRegistry(); - const persisted = await registry.setGeneratedTitleIfUnset(agent.id, normalizedTitle); - if (!persisted) { - return; - } + const persisted = await registry.setGeneratedTitle(agent.id, normalizedTitle); agent.updatedAt = new Date(persisted.updatedAt); this.emitState(agent, { persist: false }); @@ -2243,13 +2241,18 @@ export class AgentManager { lastUsage?: AgentUsage; lastError?: string; attention?: AttentionState; + initialTitle?: string | null; }, ): Promise { const resolvedAgentId = validateAgentId(agentId, "registerSession"); if (this.agents.has(resolvedAgentId)) { throw new Error(`Agent with id ${resolvedAgentId} already exists`); } - const initialPersistedTitle = await this.resolveInitialPersistedTitle(resolvedAgentId, config); + const initialPersistedTitle = await this.resolveInitialPersistedTitle( + resolvedAgentId, + config, + options?.initialTitle ?? null, + ); const now = new Date(); const { durableTimelineHasRows } = await this.initializeAgentTimelineForRegister({ @@ -2520,15 +2523,17 @@ export class AgentManager { private async resolveInitialPersistedTitle( agentId: string, config: AgentSessionConfig, + fallbackTitle: string | null, ): 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; + const explicitTitle = + typeof config.title === "string" && config.title.trim().length > 0 + ? config.title.trim() + : null; + return explicitTitle ?? fallbackTitle; } private async persistSnapshot( diff --git a/packages/server/src/server/agent/agent-metadata-generator.ts b/packages/server/src/server/agent/agent-metadata-generator.ts index ae687ab8f..a4d7c216f 100644 --- a/packages/server/src/server/agent/agent-metadata-generator.ts +++ b/packages/server/src/server/agent/agent-metadata-generator.ts @@ -152,7 +152,7 @@ export async function generateAndApplyAgentMetadata( if (needs.needsTitle && typeof result.title === "string") { const normalizedTitle = normalizeAutoTitle(result.title); if (normalizedTitle) { - await options.agentManager.setGeneratedTitleIfUnset(options.agentId, normalizedTitle); + await options.agentManager.setGeneratedTitle(options.agentId, normalizedTitle); } } } diff --git a/packages/server/src/server/agent/agent-metadata-generator.unit.test.ts b/packages/server/src/server/agent/agent-metadata-generator.unit.test.ts index 2e075f675..d05053b62 100644 --- a/packages/server/src/server/agent/agent-metadata-generator.unit.test.ts +++ b/packages/server/src/server/agent/agent-metadata-generator.unit.test.ts @@ -39,8 +39,8 @@ function createDeps( describe("agent metadata generator auto-title", () => { it("caps generated auto titles at 40 characters before persisting", async () => { - const setGeneratedTitleIfUnset = vi.fn().mockResolvedValue(undefined); - const manager = { setGeneratedTitleIfUnset } as unknown as AgentManager; + const setGeneratedTitle = vi.fn().mockResolvedValue(undefined); + const manager = { setGeneratedTitle } as unknown as AgentManager; const generatedTitle = "x".repeat(MAX_AUTO_AGENT_TITLE_CHARS + 25); const generateStructured = vi.fn().mockResolvedValue({ title: generatedTitle }) as NonNullable< AgentMetadataGeneratorDeps["generateStructuredAgentResponseWithFallback"] @@ -56,16 +56,16 @@ describe("agent metadata generator auto-title", () => { deps: createDeps(generateStructured), }); - expect(setGeneratedTitleIfUnset).toHaveBeenCalledTimes(1); - expect(setGeneratedTitleIfUnset).toHaveBeenCalledWith( + expect(setGeneratedTitle).toHaveBeenCalledTimes(1); + expect(setGeneratedTitle).toHaveBeenCalledWith( "agent-1", "x".repeat(MAX_AUTO_AGENT_TITLE_CHARS), ); }); it("does not generate an auto title when an explicit title is provided", async () => { - const setGeneratedTitleIfUnset = vi.fn().mockResolvedValue(undefined); - const manager = { setGeneratedTitleIfUnset } as unknown as AgentManager; + const setGeneratedTitle = vi.fn().mockResolvedValue(undefined); + const manager = { setGeneratedTitle } as unknown as AgentManager; const generateStructured = vi.fn().mockResolvedValue({ title: "Generated" }) as NonNullable< AgentMetadataGeneratorDeps["generateStructuredAgentResponseWithFallback"] >; @@ -81,12 +81,12 @@ describe("agent metadata generator auto-title", () => { }); expect(generateStructured).not.toHaveBeenCalled(); - expect(setGeneratedTitleIfUnset).not.toHaveBeenCalled(); + expect(setGeneratedTitle).not.toHaveBeenCalled(); }); it("generates titles independently from workspace branch naming", async () => { - const setGeneratedTitleIfUnset = vi.fn().mockResolvedValue(undefined); - const manager = { setGeneratedTitleIfUnset } as unknown as AgentManager; + const setGeneratedTitle = vi.fn().mockResolvedValue(undefined); + const manager = { setGeneratedTitle } as unknown as AgentManager; const generateStructured = vi .fn() .mockResolvedValue({ title: "Generated title" }) as NonNullable< @@ -111,10 +111,7 @@ describe("agent metadata generator auto-title", () => { persistSession: false, }), ); - expect(setGeneratedTitleIfUnset).toHaveBeenCalledWith( - "agent-suppressed-branch", - "Generated title", - ); + expect(setGeneratedTitle).toHaveBeenCalledWith("agent-suppressed-branch", "Generated title"); }); it.each([ @@ -176,8 +173,8 @@ async function generateTitlePromptWithConfig(config: unknown): Promise<{ prompt: writeFileSync(path.join(repoRoot, "paseo.json"), `${JSON.stringify(config)}\n`); } - const setGeneratedTitleIfUnset = vi.fn().mockResolvedValue(undefined); - const manager = { setGeneratedTitleIfUnset } as unknown as AgentManager; + const setGeneratedTitle = vi.fn().mockResolvedValue(undefined); + const manager = { setGeneratedTitle } as unknown as AgentManager; const generateStructured = vi.fn().mockResolvedValue({ title: "Generated title" }) as NonNullable< AgentMetadataGeneratorDeps["generateStructuredAgentResponseWithFallback"] >; diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index 67b278e97..28eb35378 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -229,7 +229,7 @@ export function buildStoredAgentPayload( availableModes: [], pendingPermissions: [], persistence, - title: record.title ?? record.config?.title ?? null, + title: record.title ?? null, requiresAttention: record.requiresAttention ?? false, attentionReason: record.attentionReason ?? null, attentionTimestamp: record.attentionTimestamp ?? null, @@ -319,9 +319,6 @@ function normalizePromptPreview(text: string): string | null { 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-prompt.ts b/packages/server/src/server/agent/agent-prompt.ts index 82c85797a..dc07948e2 100644 --- a/packages/server/src/server/agent/agent-prompt.ts +++ b/packages/server/src/server/agent/agent-prompt.ts @@ -205,7 +205,8 @@ export function setupFinishNotification(params: SetupFinishNotificationParams): fired = true; unsubscribe?.(); - const title = agentManager.getAgent(childAgentId)?.config?.title ?? childAgentId; + const record = await agentStorage.get(childAgentId); + const title = record?.title ?? childAgentId; const body = `Agent ${childAgentId} (${title}) ${reason}.`; await sendPromptToAgent({ diff --git a/packages/server/src/server/agent/agent-storage.test.ts b/packages/server/src/server/agent/agent-storage.test.ts index 61e56e435..a70f4b3b2 100644 --- a/packages/server/src/server/agent/agent-storage.test.ts +++ b/packages/server/src/server/agent/agent-storage.test.ts @@ -173,7 +173,6 @@ 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."); @@ -321,38 +320,26 @@ describe("AgentStorage", () => { ); }); - test("setGeneratedTitleIfUnset aborts when a user title is already set", async () => { - const agentId = "agent-generated-title-race"; - await storage.applySnapshot(createManagedAgent({ id: agentId })); - await storage.setTitle(agentId, "User title"); - - const result = await storage.setGeneratedTitleIfUnset(agentId, "Generated title"); - - expect(result).toBeNull(); - const record = await storage.get(agentId); - expect(record?.title).toBe("User title"); - }); - - test("setGeneratedTitleIfUnset with concurrent writes does not corrupt state", async () => { + test("setGeneratedTitle with concurrent writes does not corrupt state", async () => { const agentId = "agent-generated-title-concurrent"; await storage.applySnapshot(createManagedAgent({ id: agentId })); await Promise.all([ - storage.setGeneratedTitleIfUnset(agentId, "Title A"), - storage.setGeneratedTitleIfUnset(agentId, "Title B"), + storage.setGeneratedTitle(agentId, "Title A"), + storage.setGeneratedTitle(agentId, "Title B"), ]); const record = await storage.get(agentId); expect(["Title A", "Title B"]).toContain(record?.title); }); - test("setGeneratedTitleIfUnset writes the generated title only when title is empty", async () => { + test("setGeneratedTitle writes the generated title", async () => { const agentId = "agent-generated-title-empty"; await storage.applySnapshot(createManagedAgent({ id: agentId })); - const written = await storage.setGeneratedTitleIfUnset(agentId, "Generated title"); + const written = await storage.setGeneratedTitle(agentId, "Generated title"); - expect(written?.title).toBe("Generated title"); + expect(written.title).toBe("Generated title"); const record = await storage.get(agentId); expect(record?.title).toBe("Generated title"); }); diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index c747395e5..8bb673890 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -11,7 +11,6 @@ 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(), @@ -68,7 +67,6 @@ const STORED_AGENT_SCHEMA = z.object({ export type SerializableAgentConfig = Pick< AgentSessionConfig, - | "title" | "modeId" | "model" | "thinkingOptionId" @@ -232,33 +230,15 @@ export class AgentStorage { await this.upsert({ ...record, title }); } - async setGeneratedTitleIfUnset( - agentId: string, - title: string, - ): Promise { + async setGeneratedTitle(agentId: string, title: string): Promise { await this.load(); await this.waitForPendingWrite(agentId); const record = this.cache.get(agentId) ?? null; if (!record) { throw new Error(`Agent ${agentId} not found`); } - if (record.title) { - return null; - } - - // Re-drain pending writes: a concurrent setTitle may have queued between the - // first drain and here. After waiting, re-read the cache before writing. - await this.waitForPendingWrite(agentId); - const latestRecord = this.cache.get(agentId) ?? null; - if (!latestRecord) { - throw new Error(`Agent ${agentId} not found`); - } - if (latestRecord.title) { - return null; - } - const nextRecord = { - ...latestRecord, + ...record, title, updatedAt: new Date().toISOString(), }; diff --git a/packages/server/src/server/persistence-hooks.test.ts b/packages/server/src/server/persistence-hooks.test.ts index 129584b48..fe57d9082 100644 --- a/packages/server/src/server/persistence-hooks.test.ts +++ b/packages/server/src/server/persistence-hooks.test.ts @@ -31,7 +31,6 @@ describe("persistence hooks", () => { const record = createRecord({ title: "Voice agent (current)", config: { - title: "Voice agent (created)", modeId: "default", model: "gpt-5.4-mini", thinkingOptionId: "minimal", @@ -51,7 +50,6 @@ describe("persistence hooks", () => { modeId: "plan", model: "gpt-5.4-mini", thinkingOptionId: "minimal", - title: "Voice agent (created)", systemPrompt: "Use speak first.", mcpServers: { paseo: { @@ -68,7 +66,6 @@ describe("persistence hooks", () => { provider: "codex", title: "Renamed title", config: { - title: "Creation title", modeId: "default", model: "gpt-5.4-mini", systemPrompt: "Confirm and speak first.", @@ -87,7 +84,6 @@ describe("persistence hooks", () => { cwd: "/tmp/project", modeId: "plan", model: "gpt-5.4-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 ca10186de..fb679ab2b 100644 --- a/packages/server/src/server/persistence-hooks.ts +++ b/packages/server/src/server/persistence-hooks.ts @@ -68,7 +68,6 @@ export function buildConfigOverrides(record: StoredAgentRecord): Partial { const storedRecord = await this.agentStorage.get(agent.id); - const title = storedRecord?.title ?? storedRecord?.config?.title ?? null; + const title = storedRecord?.title ?? null; const payload = toAgentPayload(agent, { title }); const storedUpdatedAt = storedRecord ? resolveStoredAgentPayloadUpdatedAt(storedRecord) : null; if (storedUpdatedAt) { @@ -3145,10 +3145,7 @@ export class Session { configTitle: config.title, initialPrompt: trimmedPrompt, }); - const resolvedConfig: AgentSessionConfig = { - ...config, - ...(provisionalTitle ? { title: provisionalTitle } : {}), - }; + const resolvedConfig: AgentSessionConfig = config; const firstAgentContext: FirstAgentContext = { ...(trimmedPrompt ? { prompt: trimmedPrompt } : {}), @@ -3182,6 +3179,7 @@ export class Session { workspaceId: resolvedWorkspace.workspaceId, initialPrompt: trimmedPrompt, env, + initialTitle: provisionalTitle, }); createdAgentId = snapshot.id; await this.forwardAgentUpdate(snapshot);