Summary
Every new session receives the static title "New session - [timestamp]" and it never updates to reflect the conversation content. The project chat history becomes a wall of identical titles, making it impossible to find or distinguish past sessions.
Context
- Extension: harmoniqs.amicode v0.2.1
- Engine: v1.18.10-amicode.4 (base: v1.18.10 of sst/opencode)
- OS: Darwin 25.5.0 arm64
Root Cause Analysis
Title generation in packages/opencode/src/session/prompt.ts fires exactly once, gated on step === 1 (line 1285). Multiple code paths silently prevent this from ever succeeding:
-
Silent-turn guard pre-increments step (line 1208). If the model's first response is reasoning-only (no visible text or tool calls — common with Claude Opus on Bedrock), the guard does step++; continue before the title trigger at line 1284 is reached. By the time we get there, step is already ≥1 and the post-increment makes step === 1 false.
-
Prose-question guard (line 1265) has the same effect — another step++; continue that races the title trigger.
-
Single-message guard inside title() (line 208). The function bails if userMessages.length !== 1, meaning that if the trigger fires late (after a second user message), it's permanently dead.
-
No retry. Title generation is attempted at most once. Effect.ignore swallows all errors — empty LLM responses, transient Bedrock failures, concurrency limits — with no recovery path.
Approach
Decouple title generation from the step counter and add bounded retries.
Approaches Considered
| Approach |
Pros |
Cons |
Fix step counter only — don't increment step in the guards |
Minimal diff |
Fragile: future guards could re-break it; doesn't address silent LLM failures |
| Decouple only — own boolean flag, fire once |
Clean separation of concerns |
Still one-shot: a single transient failure = permanent default title |
| Decouple + bounded retry (chosen) |
Handles all failure modes; converges quickly |
Slightly more code; up to 3 forked LLM calls per session (capped) |
Scope
Two changes in packages/opencode/src/session/prompt.ts, no other files affected.
Acceptance Criteria
Key Decisions
1. Replace step === 1 with a dedicated titleAttempts counter
Location: line ~1091 (initialization) and lines 1284–1291 (trigger)
// At initialization (alongside `let step = 0`)
let titleAttempts = 0
// At the trigger point (replacing `if (step === 1)`)
if (Session.isDefaultTitle(session.title) && titleAttempts < 3) {
titleAttempts++
yield* title({
session,
modelID: lastUser.model.modelID,
providerID: lastUser.model.providerID,
history: msgs,
}).pipe(Effect.ignore, Effect.forkIn(scope))
}
The counter is checked every loop pass (including tool-call cycles), so retries fire immediately without waiting for a new user message. The isDefaultTitle check prevents wasted calls after success.
2. Relax the single-message guard in title()
Location: line 208
// Before:
if (input.history.filter(real).length !== 1) return
// After:
if (input.history.filter(real).length < 1) return
This allows retries to succeed even after the second user message arrives. Double-titling is already prevented by the isDefaultTitle check on line 202.
Constraints & Invariants
Effect.ignore stays — title generation remains non-critical and silent on failure
- Model selection logic unchanged (
getSmallModel → fallback to main model)
- Title generation remains forked (never blocks the main response)
- Maximum 3 attempts is a hard cap (not configurable)
- The
<think> tag stripping and 100-char truncation logic are untouched
Prior Art
The upstream sst/opencode has the same step === 1 pattern and is likely affected by the same bug when used with reasoning models that produce silent turns. This fix is fork-local but could be upstreamed.
Summary
Every new session receives the static title "New session - [timestamp]" and it never updates to reflect the conversation content. The project chat history becomes a wall of identical titles, making it impossible to find or distinguish past sessions.
Context
Root Cause Analysis
Title generation in
packages/opencode/src/session/prompt.tsfires exactly once, gated onstep === 1(line 1285). Multiple code paths silently prevent this from ever succeeding:Silent-turn guard pre-increments
step(line 1208). If the model's first response is reasoning-only (no visible text or tool calls — common with Claude Opus on Bedrock), the guard doesstep++; continuebefore the title trigger at line 1284 is reached. By the time we get there,stepis already ≥1 and the post-increment makesstep === 1false.Prose-question guard (line 1265) has the same effect — another
step++; continuethat races the title trigger.Single-message guard inside
title()(line 208). The function bails ifuserMessages.length !== 1, meaning that if the trigger fires late (after a second user message), it's permanently dead.No retry. Title generation is attempted at most once.
Effect.ignoreswallows all errors — empty LLM responses, transient Bedrock failures, concurrency limits — with no recovery path.Approach
Decouple title generation from the step counter and add bounded retries.
Approaches Considered
stepin the guardsScope
Two changes in
packages/opencode/src/session/prompt.ts, no other files affected.Acceptance Criteria
Key Decisions
1. Replace
step === 1with a dedicatedtitleAttemptscounterLocation: line ~1091 (initialization) and lines 1284–1291 (trigger)
The counter is checked every loop pass (including tool-call cycles), so retries fire immediately without waiting for a new user message. The
isDefaultTitlecheck prevents wasted calls after success.2. Relax the single-message guard in
title()Location: line 208
This allows retries to succeed even after the second user message arrives. Double-titling is already prevented by the
isDefaultTitlecheck on line 202.Constraints & Invariants
Effect.ignorestays — title generation remains non-critical and silent on failuregetSmallModel→ fallback to main model)<think>tag stripping and 100-char truncation logic are untouchedPrior Art
The upstream
sst/opencodehas the samestep === 1pattern and is likely affected by the same bug when used with reasoning models that produce silent turns. This fix is fork-local but could be upstreamed.