From ace8cff083b4d0c6b1549e42fe66a7152a05efd8 Mon Sep 17 00:00:00 2001 From: ChuanKang Kk <2040168455@qq.com> Date: Mon, 6 Jul 2026 00:18:14 +0800 Subject: [PATCH] fix(opencode): handle session title generation failures with retry Three fixes for ensureTitle(): 1. Fix model resolution: wrap getSmallModel/getModel in Effect.option so resolution errors don't silently defect the fiber. 2. Fix LLM stream: replace Effect.orDie with Effect.catchAll so API errors produce a warning log instead of terminating the background fiber. 3. Fix call site: replace Effect.ignore with Effect.catchCause so unexpected defects are logged instead of silently swallowed. Additionally: - Remove small:true flag so title uses the current conversation model. - Add retry path: if first LLM call produces no title, sleep 10 seconds then retry with full conversation history for better context. Fixes #13710 --- packages/opencode/src/session/prompt.ts | 92 ++++++++++++++++++++----- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index eb116f6b960f..854c53e09d52 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -215,10 +215,19 @@ const layer = Layer.effect( const ag = yield* agents.get("title") if (!ag) return - const mdl = ag.model - ? yield* provider.getModel(ag.model.providerID, ag.model.modelID) - : ((yield* provider.getSmallModel(input.providerID)) ?? - (yield* provider.getModel(input.providerID, input.modelID))) + const mdlOpt = yield* Effect.fnUntraced(function* () { + if (ag.model) return yield* provider.getModel(ag.model.providerID, ag.model.modelID) + return (yield* provider.getSmallModel(input.providerID)) ?? + (yield* provider.getModel(input.providerID, input.modelID)) + })().pipe( + Effect.tapError((err) => Effect.logWarning("title model resolution failed", { error: err })), + Effect.option, + ) + if (Option.isNone(mdlOpt)) { + yield* Effect.logWarning("title skipped: no model available") + return + } + const mdl = mdlOpt.value const msgs = onlySubtasks ? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }] : yield* MessageV2.toModelMessagesEffect(context, mdl) @@ -227,7 +236,6 @@ const layer = Layer.effect( agent: ag, user: firstInfo, system: [], - small: true, tools: {}, model: mdl, sessionID: input.session.id, @@ -238,18 +246,61 @@ const layer = Layer.effect( Stream.filter(LLMEvent.is.textDelta), Stream.map((e) => e.text), Stream.mkString, - Effect.orDie, + Effect.catchAll((err) => + Effect.logWarning("title generation LLM call failed", { error: err }).pipe(Effect.as("")) + ), ) - const cleaned = text - .replace(/[\s\S]*?<\/think>\s*/g, "") - .split("\n") - .map((line) => line.trim()) - .find((line) => line.length > 0) - if (!cleaned) return - const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned - yield* sessions - .setTitle({ sessionID: input.session.id, title: t }) - .pipe(Effect.catchCause((cause) => Effect.logError("failed to generate title", { error: Cause.squash(cause) }))) + const extractTitle = (raw: string) => { + const t = raw + .replace(/[\s\S]*?<\/think>\s*/g, "") + .split("\n") + .map((line) => line.trim()) + .find((line) => line.length > 0) + if (!t) return + return t.length > 100 ? t.substring(0, 97) + "..." : t + } + const setTitle = (title: string) => + sessions + .setTitle({ sessionID: input.session.id, title }) + .pipe(Effect.catchCause((cause) => Effect.logError("failed to persist title", { error: Cause.squash(cause) }))) + const first = extractTitle(text) + if (first) { + yield* setTitle(first) + return + } + yield* Effect.logInfo("title generation deferred, will retry after delay") + yield* Effect.sleep("10 seconds") + const fresh = yield* sessions.get(input.session.id).pipe(Effect.option) + if (Option.isNone(fresh) || !Session.isDefaultTitle(fresh.value.title)) return + const retryMsgs = yield* sessions.messages({ sessionID: input.session.id }).pipe( + Effect.catchAll((err) => + Effect.logWarning("title retry: failed to fetch messages", { error: err }).pipe(Effect.as([] as SessionV1.WithParts[])) + ), + ) + if (retryMsgs.length === 0) return + const retryModelMsgs = yield* MessageV2.toModelMessagesEffect(retryMsgs, mdl) + const retryText = yield* llm + .stream({ + agent: ag, + user: firstInfo, + system: [], + tools: {}, + model: mdl, + sessionID: input.session.id, + retries: 2, + messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...retryModelMsgs], + }) + .pipe( + Stream.filter(LLMEvent.is.textDelta), + Stream.map((e) => e.text), + Stream.mkString, + Effect.catchAll((err) => + Effect.logWarning("title generation retry LLM call failed", { error: err }).pipe(Effect.as("")) + ), + ) + const second = extractTitle(retryText) + if (!second) return + yield* setTitle(second) }) const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: { @@ -1136,7 +1187,14 @@ const layer = Layer.effect( modelID: lastUser.model.modelID, providerID: lastUser.model.providerID, history: msgs, - }).pipe(Effect.ignore, Effect.forkIn(scope)) + }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("title generation background task failed", { + error: Cause.squash(cause), + }), + ), + Effect.forkIn(scope), + ) const model = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID) const task = tasks.pop()