From 24a592016412824d77ffafcb4846a3d9973105f9 Mon Sep 17 00:00:00 2001 From: hoop71 Date: Wed, 25 Mar 2026 16:26:18 -0600 Subject: [PATCH 1/2] fix(session): fall back when generated titles stay default Co-authored-by: Legion Intel AI OpenCode-Session: ses_2d99c801bffe8QKwyvSpSW7goa OpenCode-Repo: yurtsai/opencode OpenCode-Branch: fix/session-title-fallback --- packages/opencode/src/session/prompt.ts | 22 +++++ packages/opencode/test/session/prompt.test.ts | 84 ++++++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b3c34539e77e..ea93ce3551fe 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1978,6 +1978,26 @@ NOTE: At any point in time through this workflow you should feel free to ask the providerID: ProviderID modelID: ModelID }) { + function fallback(msg: MessageV2.WithParts) { + const line = msg.parts + .filter((part) => part.type === "text") + .flatMap((part) => part.text.split("\n")) + .map((line) => line.trim()) + .find((line) => line.length > 0) + if (!line) return + const title = line + .replace(/[\s\-:;,.!?]+$/g, "") + .split(/\s+/) + .filter(Boolean) + .slice(0, 5) + .join(" ") + if (!title) return + return Session.setTitle({ sessionID: input.session.id, title: title.length > 100 ? title.substring(0, 97) + "..." : title }).catch((err) => { + if (NotFoundError.isInstance(err)) return + throw err + }) + } + if (input.session.parentID) return if (!Session.isDefaultTitle(input.session.title)) return @@ -2045,5 +2065,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the throw err }) } + + return fallback(firstRealUser) } } diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 7d1d42905792..232bf0086753 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -1,5 +1,5 @@ import path from "path" -import { describe, expect, test } from "bun:test" +import { describe, expect, spyOn, test } from "bun:test" import { NamedError } from "@opencode-ai/util/error" import { fileURLToPath } from "url" import { Instance } from "../../src/project/instance" @@ -9,9 +9,21 @@ import { MessageV2 } from "../../src/session/message-v2" import { SessionPrompt } from "../../src/session/prompt" import { Log } from "../../src/util/log" import { tmpdir } from "../fixture/fixture" +import { LLM } from "../../src/session/llm" +import { Filesystem } from "../../src/util/filesystem" Log.init({ print: false }) +async function loadFixture(providerID: string, modelID: string) { + const fixturePath = path.join(import.meta.dir, "../tool/fixtures/models-api.json") + const data = await Filesystem.readJson }>>(fixturePath) + const provider = data[providerID] + if (!provider) throw new Error(`Missing provider in fixture: ${providerID}`) + const model = provider.models[modelID] + if (!model) throw new Error(`Missing model in fixture: ${modelID}`) + return model +} + describe("session.prompt missing file", () => { test("does not fail the prompt when a file part is missing", async () => { await using tmp = await tmpdir({ @@ -286,3 +298,73 @@ describe("session.agent-resolution", () => { }) }, 30000) }) + +describe("session title fallback", () => { + test("falls back to the first few words when title generation returns nothing", async () => { + const model = await loadFixture("openai", "gpt-5.2") + const stream = spyOn(LLM, "stream") + .mockResolvedValueOnce({ + text: Promise.resolve(""), + } as Awaited>) + .mockResolvedValueOnce({ + fullStream: (async function* () { + yield { type: "start" } + yield { + type: "finish-step", + finishReason: "stop", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + providerMetadata: {}, + } + yield { type: "finish" } + })(), + } as unknown as Awaited>) + + await using tmp = await tmpdir({ + git: true, + config: { + enabled_providers: ["openai"], + provider: { + openai: { + options: { + apiKey: "test-openai-key", + models: { + "gpt-5.2": model, + }, + }, + }, + }, + agent: { + build: { + model: "openai/gpt-5.2", + }, + }, + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + + await SessionPrompt.prompt({ + sessionID: session.id, + agent: "build", + parts: [{ type: "text", text: "All of my sessions are names new session is the a telemetry package sideffect?" }], + }) + + expect(stream).toHaveBeenCalledTimes(2) + + let info = await Session.get(session.id) + for (let i = 0; i < 20 && info?.title !== "All of my sessions are"; i++) { + await new Promise((resolve) => setTimeout(resolve, 50)) + info = await Session.get(session.id) + } + expect(info?.title).toBe("All of my sessions are") + + await Session.remove(session.id) + }, + }) + + stream.mockRestore() + }) +}) From 44c23d7d63840108a118942a108e63848c89ac68 Mon Sep 17 00:00:00 2001 From: hoop71 Date: Wed, 25 Mar 2026 17:56:03 -0600 Subject: [PATCH 2/2] fix(session): await title fallback update ordering Co-authored-by: Legion Intel AI OpenCode-Session: ses_2d99c801bffe8QKwyvSpSW7goa OpenCode-Repo: yurtsai/opencode OpenCode-Branch: fix/session-title-fallback --- packages/opencode/src/session/prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ea93ce3551fe..84b02a087751 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -330,7 +330,7 @@ export namespace SessionPrompt { step++ if (step === 1) - ensureTitle({ + await ensureTitle({ session, modelID: lastUser.model.modelID, providerID: lastUser.model.providerID,