Skip to content

Commit ace8cff

Browse files
committed
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
1 parent be73f46 commit ace8cff

1 file changed

Lines changed: 75 additions & 17 deletions

File tree

packages/opencode/src/session/prompt.ts

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,19 @@ const layer = Layer.effect(
215215

216216
const ag = yield* agents.get("title")
217217
if (!ag) return
218-
const mdl = ag.model
219-
? yield* provider.getModel(ag.model.providerID, ag.model.modelID)
220-
: ((yield* provider.getSmallModel(input.providerID)) ??
221-
(yield* provider.getModel(input.providerID, input.modelID)))
218+
const mdlOpt = yield* Effect.fnUntraced(function* () {
219+
if (ag.model) return yield* provider.getModel(ag.model.providerID, ag.model.modelID)
220+
return (yield* provider.getSmallModel(input.providerID)) ??
221+
(yield* provider.getModel(input.providerID, input.modelID))
222+
})().pipe(
223+
Effect.tapError((err) => Effect.logWarning("title model resolution failed", { error: err })),
224+
Effect.option,
225+
)
226+
if (Option.isNone(mdlOpt)) {
227+
yield* Effect.logWarning("title skipped: no model available")
228+
return
229+
}
230+
const mdl = mdlOpt.value
222231
const msgs = onlySubtasks
223232
? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
224233
: yield* MessageV2.toModelMessagesEffect(context, mdl)
@@ -227,7 +236,6 @@ const layer = Layer.effect(
227236
agent: ag,
228237
user: firstInfo,
229238
system: [],
230-
small: true,
231239
tools: {},
232240
model: mdl,
233241
sessionID: input.session.id,
@@ -238,18 +246,61 @@ const layer = Layer.effect(
238246
Stream.filter(LLMEvent.is.textDelta),
239247
Stream.map((e) => e.text),
240248
Stream.mkString,
241-
Effect.orDie,
249+
Effect.catchAll((err) =>
250+
Effect.logWarning("title generation LLM call failed", { error: err }).pipe(Effect.as(""))
251+
),
242252
)
243-
const cleaned = text
244-
.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
245-
.split("\n")
246-
.map((line) => line.trim())
247-
.find((line) => line.length > 0)
248-
if (!cleaned) return
249-
const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
250-
yield* sessions
251-
.setTitle({ sessionID: input.session.id, title: t })
252-
.pipe(Effect.catchCause((cause) => Effect.logError("failed to generate title", { error: Cause.squash(cause) })))
253+
const extractTitle = (raw: string) => {
254+
const t = raw
255+
.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
256+
.split("\n")
257+
.map((line) => line.trim())
258+
.find((line) => line.length > 0)
259+
if (!t) return
260+
return t.length > 100 ? t.substring(0, 97) + "..." : t
261+
}
262+
const setTitle = (title: string) =>
263+
sessions
264+
.setTitle({ sessionID: input.session.id, title })
265+
.pipe(Effect.catchCause((cause) => Effect.logError("failed to persist title", { error: Cause.squash(cause) })))
266+
const first = extractTitle(text)
267+
if (first) {
268+
yield* setTitle(first)
269+
return
270+
}
271+
yield* Effect.logInfo("title generation deferred, will retry after delay")
272+
yield* Effect.sleep("10 seconds")
273+
const fresh = yield* sessions.get(input.session.id).pipe(Effect.option)
274+
if (Option.isNone(fresh) || !Session.isDefaultTitle(fresh.value.title)) return
275+
const retryMsgs = yield* sessions.messages({ sessionID: input.session.id }).pipe(
276+
Effect.catchAll((err) =>
277+
Effect.logWarning("title retry: failed to fetch messages", { error: err }).pipe(Effect.as([] as SessionV1.WithParts[]))
278+
),
279+
)
280+
if (retryMsgs.length === 0) return
281+
const retryModelMsgs = yield* MessageV2.toModelMessagesEffect(retryMsgs, mdl)
282+
const retryText = yield* llm
283+
.stream({
284+
agent: ag,
285+
user: firstInfo,
286+
system: [],
287+
tools: {},
288+
model: mdl,
289+
sessionID: input.session.id,
290+
retries: 2,
291+
messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...retryModelMsgs],
292+
})
293+
.pipe(
294+
Stream.filter(LLMEvent.is.textDelta),
295+
Stream.map((e) => e.text),
296+
Stream.mkString,
297+
Effect.catchAll((err) =>
298+
Effect.logWarning("title generation retry LLM call failed", { error: err }).pipe(Effect.as(""))
299+
),
300+
)
301+
const second = extractTitle(retryText)
302+
if (!second) return
303+
yield* setTitle(second)
253304
})
254305

255306
const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
@@ -1136,7 +1187,14 @@ const layer = Layer.effect(
11361187
modelID: lastUser.model.modelID,
11371188
providerID: lastUser.model.providerID,
11381189
history: msgs,
1139-
}).pipe(Effect.ignore, Effect.forkIn(scope))
1190+
}).pipe(
1191+
Effect.catchCause((cause) =>
1192+
Effect.logWarning("title generation background task failed", {
1193+
error: Cause.squash(cause),
1194+
}),
1195+
),
1196+
Effect.forkIn(scope),
1197+
)
11401198

11411199
const model = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID)
11421200
const task = tasks.pop()

0 commit comments

Comments
 (0)