diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 58c1bb896..788d3f41b 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -535,6 +535,7 @@ export class AgentSession { * the session cwd changes. */ #titleSystemPrompt: string | undefined; #titleGenerationStart: (() => void) | undefined; + #titleGenerationInFlight = false; #titleGenerationAbortController = new AbortController(); #toolChoiceQueue = new ToolChoiceQueue(); @@ -6549,10 +6550,22 @@ export class AgentSession { this.#extensionRunner?.getCommand( extensionCommandSpace === -1 ? firstMessage.slice(1) : firstMessage.slice(1, extensionCommandSpace), ) !== undefined; - if (isLocalExtensionCommand || this.sessionName || $env.PI_NO_TITLE || isLowSignalTitleInput(firstMessage)) { + if ( + isLocalExtensionCommand || + this.sessionName || + this.#titleGenerationInFlight || + $env.PI_NO_TITLE || + isLowSignalTitleInput(firstMessage) + ) { return; } - (onStart ?? this.#titleGenerationStart)?.(); + this.#titleGenerationInFlight = true; + try { + (onStart ?? this.#titleGenerationStart)?.(); + } catch (error) { + this.#titleGenerationInFlight = false; + throw error; + } this.generateTitle(firstMessage) .then(async title => { // Re-check after generation so concurrent attempts cannot replace @@ -6567,6 +6580,9 @@ export class AgentSession { reason: "uncaught-auto-title-error", error: err instanceof Error ? err.message : String(err), }); + }) + .finally(() => { + this.#titleGenerationInFlight = false; }); } diff --git a/packages/coding-agent/test/agent-session-title-generation-dispose.test.ts b/packages/coding-agent/test/agent-session-title-generation-dispose.test.ts index 4bb52d2e5..eb9b87275 100644 --- a/packages/coding-agent/test/agent-session-title-generation-dispose.test.ts +++ b/packages/coding-agent/test/agent-session-title-generation-dispose.test.ts @@ -68,4 +68,42 @@ describe("AgentSession title generation disposal", () => { expect(requestSignal?.aborted).toBe(true); expect(await generation).toBeNull(); }); + + it("does not start a second auto-title request while the first is still in flight", async () => { + authStorage = await AuthStorage.create(":memory:"); + authStorage.setRuntimeApiKey("anthropic", "test-key"); + const model = getBundledModel("anthropic", "claude-sonnet-4-5"); + if (!model) throw new Error("Expected claude-sonnet-4-5 model to exist"); + + const settings = Settings.isolated({ + "compaction.enabled": false, + "providers.tinyModel": "online", + }); + settings.overrideModelRoles({ smol: `${model.provider}/${model.id}` }); + const agent = new Agent({ + getApiKey: () => "test-key", + initialState: { model, systemPrompt: ["Test"], tools: [], messages: [] }, + streamFn: createMockModel({ responses: [{ content: ["Done"] }] }).stream, + }); + session = new AgentSession({ + agent, + sessionManager: SessionManager.inMemory(), + settings, + modelRegistry: new ModelRegistry(authStorage), + }); + const started = Promise.withResolvers(); + const response = Promise.withResolvers(); + const completeSimple = vi.spyOn(ai, "completeSimple").mockImplementation(() => { + started.resolve(); + return response.promise; + }); + + session.maybeStartTitleGeneration("/skill:implement issues/07-manual-llm.md"); + await started.promise; + session.maybeStartTitleGeneration("/skill:implement issues/08-app-settings.md"); + expect(completeSimple).toHaveBeenCalledTimes(1); + + response.resolve(createAssistantMessage("manual llm")); + await response.promise; + }); });