diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts index d1bd68cb19d4..98616b096086 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts @@ -234,7 +234,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { body: "", }, }), - argsMustContain: '--settings {"alwaysThinkingEnabled":false}', + argsMustContain: '--settings {"verbose":false,"alwaysThinkingEnabled":false}', argsMustNotContain: "--effort", }, (textGeneration) => @@ -266,7 +266,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { body: "Body", }, }), - argsMustContain: '--effort max --settings {"fastMode":true}', + argsMustContain: '--effort max --settings {"verbose":false,"fastMode":true}', }, (textGeneration) => Effect.gen(function* () { @@ -377,4 +377,68 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { }), ), ); + + it.effect("pins verbose output off even without other Claude settings", () => + withFakeClaudeEnv( + { + output: JSON.stringify({ + structured_output: { + title: "Reconnect failures after restart", + }, + }), + argsMustContain: '--settings {"verbose":false}', + }, + (textGeneration) => + Effect.gen(function* () { + const generated = yield* textGeneration.generateThreadTitle({ + cwd: process.cwd(), + message: "Please investigate reconnect failures after restarting the session.", + modelSelection: { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-sonnet-4-6", + }, + }); + + expect(generated.title).toBe(sanitizeThreadTitle("Reconnect failures after restart")); + }), + ), + ); + + // Guards the assumption behind `verbose: false`: should the CLI ever stream + // events despite the pinned setting, this fails loudly instead of silently. + it.effect("fails when the CLI streams events instead of the envelope", () => + withFakeClaudeEnv( + { + output: JSON.stringify([ + { type: "system", subtype: "init", session_id: "abc" }, + { type: "assistant", message: { role: "assistant", content: [] } }, + { type: "user", message: { role: "user", content: [] } }, + { type: "rate_limit_event", rate_limit: { status: "allowed" } }, + { + type: "result", + subtype: "success", + is_error: false, + result: '{"title":"Reconnect failures after restart"}', + structured_output: { title: "Reconnect failures after restart" }, + }, + ]), + }, + (textGeneration) => + Effect.gen(function* () { + const error = yield* Effect.flip( + textGeneration.generateThreadTitle({ + cwd: process.cwd(), + message: "Name this thread.", + modelSelection: { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-sonnet-4-6", + }, + }), + ); + + expect(error._tag).toBe("TextGenerationError"); + expect(error.detail).toContain("unexpected output format"); + }), + ), + ); }); diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.ts index aa3e59e2bf21..ba4bef1f9f29 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.ts @@ -143,6 +143,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu const fastMode = fastModeDescriptor?.type === "boolean" ? fastModeDescriptor.currentValue : undefined; const settings = { + // Pinned: a user-level `verbose` makes the CLI print a stream-event array + // instead of the single JSON envelope we parse. + verbose: false, ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}), ...(fastMode ? { fastMode: true } : {}), ...(ultracode ? { ultracode: true } : {}),