diff --git a/packages/core/src/session/model-request.ts b/packages/core/src/session/model-request.ts index ebde0f9250..7881c33041 100644 --- a/packages/core/src/session/model-request.ts +++ b/packages/core/src/session/model-request.ts @@ -354,10 +354,7 @@ export const layer = Layer.effect( : undefined const options: StreamOptions = { ...(http ? { http } : {}), - ...(input.webSocket === "session" && - webSocket && - !hasHttpHooks && - resolved.capabilities.responsesWebsockets === true + ...(input.webSocket === "session" && webSocket && !hasHttpHooks ? { webSocket: transport.bind(session.id) } : {}), } diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index 5ff3b166f8..cbf42ac24d 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -181,7 +181,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider item, reuseToolProviderMetadata ? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState) - : sameProvider && item.executed === true && item.providerResultState !== undefined + : sameProvider && item.providerResultState !== undefined ? providerMetadata(providerMetadataKey, item.providerResultState) : undefined, ) diff --git a/packages/core/src/session/title.ts b/packages/core/src/session/title.ts index fc256e2049..32c83f081b 100644 --- a/packages/core/src/session/title.ts +++ b/packages/core/src/session/title.ts @@ -78,7 +78,7 @@ const attempt = Effect.fn("SessionTitle.attempt")(function* ( }, contextHooks: false, }) - const streamed = yield* dependencies.llm.stream(prepared.request, prepared.options).pipe( + yield* dependencies.llm.stream(prepared.request, prepared.options).pipe( Stream.runForEach((event) => { if (LLMEvent.is.providerError(event)) failed = true if (LLMEvent.is.textDelta(event)) chunks.push(event.text) @@ -88,12 +88,15 @@ const attempt = Effect.fn("SessionTitle.attempt")(function* ( } return Effect.void }), - Effect.as(true), - Effect.catchTag("AI.Error", () => Effect.succeed(false)), + Effect.catchTag("AI.Error", () => + Effect.sync(() => { + failed = true + }), + ), Effect.onInterrupt(() => recordUsage.pipe(Effect.asVoid)), ) yield* recordUsage - if (!streamed || failed) return + if (failed) return return chunks .join("") .split("\n") diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts index 701bbb9d04..0178d39ec5 100644 --- a/packages/core/test/session-title.test.ts +++ b/packages/core/test/session-title.test.ts @@ -1,5 +1,13 @@ import { beforeEach, expect } from "bun:test" -import { LLMClient, LLMEvent, LanguageModel, SystemPart, type LLMRequest } from "@opencode-ai/ai" +import { + AIError, + LLMClient, + LLMEvent, + LanguageModel, + SystemPart, + TransportReason, + type LLMRequest, +} from "@opencode-ai/ai" import { OpenAIChat } from "@opencode-ai/ai/protocols" import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" @@ -69,7 +77,7 @@ const successfulTitle = () => reason: { normalized: "stop" }, }), ) -let titleStream: () => Stream.Stream = successfulTitle +let titleStream: () => Stream.Stream = successfulTitle const client = Layer.mock(LLMClient.Service)({ stream: (request: LLMRequest) => { requests.push(request) @@ -460,6 +468,37 @@ it.effect("retries after a failed title request", () => }), ) +it.effect("does not rename after a failed title stream", () => + Effect.gen(function* () { + const agentService = yield* Agent.Service + yield* agentService.transform((editor) => { + editor.update(Agent.ID.make("title"), (agent) => { + agent.mode = "primary" + agent.hidden = true + agent.system = "You are a title generator." + }) + }) + const sessionID = Session.ID.make("ses_title_stream_failure") + yield* insertSession(sessionID) + yield* prompt(sessionID, "Fail this title stream") + titleStream = () => + Stream.fail( + new AIError({ + module: "test", + method: "stream", + reason: new TransportReason({ message: "Disconnected", transport: "http", operation: "request" }), + }), + ) + + const title = yield* SessionTitle.Service + yield* title.generateForFirstPrompt(sessionID) + + const store = yield* SessionStore.Service + expect(requests).toHaveLength(1) + expect((yield* store.get(sessionID))?.title).toBeUndefined() + }), +) + it.effect("keeps session context hooks away from title requests", () => Effect.gen(function* () { requests = []