Skip to content

BUG: All new sessions get default title "New session - []" — chat history is indiscernible #138

Description

@jeonghun-jj-lee

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:

  1. 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.

  2. Prose-question guard (line 1265) has the same effect — another step++; continue that races the title trigger.

  3. 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.

  4. 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

  • Sessions receive a meaningful auto-generated title after the first user message
  • Title generation succeeds even when silent-turn or prose-question guards fire before the main loop body
  • A transient LLM failure on the first attempt does not permanently prevent title generation
  • Title generation stops retrying after success (no wasted API calls)
  • Maximum 3 title-generation attempts per session

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.

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions