Skip to content

fix(claude): preserve native titles and add a remote fallback#1080

Open
techotaku39 wants to merge 2 commits into
tiann:mainfrom
techotaku39:fix/claude-title-fallback
Open

fix(claude): preserve native titles and add a remote fallback#1080
techotaku39 wants to merge 2 commits into
tiann:mainfrom
techotaku39:fix/claude-title-fallback

Conversation

@techotaku39

Copy link
Copy Markdown
Contributor

Title

fix(claude): preserve native titles and add a remote fallback

Summary

  • preserve Claude Code's native ai-title metadata for local interactive sessions
  • keep legacy transcript summary events as a fallback without overriding an existing title
  • give remote SDK sessions a deterministic title after their first normal result when Claude did not call HAPI's change_title tool
  • preserve manual session names and existing agent-generated titles

Problem

Claude sessions could remain named after their working directory in HAPI even after the first task completed.

The two Claude launch paths fail differently:

  • Native interactive Claude Code writes concise generated titles as transcript records shaped like {"type":"ai-title","aiTitle":"..."}, but HAPI's transcript schema and scanner did not recognize those records.
  • Claude SDK sessions do not reliably emit native ai-title records, and the model does not always follow HAPI's prompt to call change_title.

As a result, local sessions could discard an available native title, while remote sessions had no deterministic fallback at all.

Changes

Local Claude sessions

  • accept ai-title records in RawJSONLinesSchema
  • give ai-title records stable scanner keys
  • translate native ai-title metadata into HAPI's existing summary-title update
  • do not render title metadata as a visible chat message
  • continue accepting legacy summary records

Remote Claude sessions

  • report the initial normal user message after the first result
  • if the session still has neither metadata.name nor metadata.summary, normalize that message and store it as a fallback title
  • collapse whitespace and cap fallback titles at 80 characters
  • skip /clear, /compact, and /plan command handling

Precedence

The change does not replace:

  1. a user-supplied metadata.name
  2. an existing metadata.summary, including a successful change_title call

This also closes the race where a title tool call succeeds during the first turn: the remote fallback checks the latest local metadata before writing.

Scope

This PR restores native AI-generated titles where Claude Code provides them and prevents untitled remote sessions from staying on the directory fallback.

It intentionally does not add periodic topic detection, automatic retitling after later turns, or extra model calls. Those behaviors have separate cost, lifecycle, and configuration considerations.

Testing

  • bun typecheck
  • bun run test:cli
  • bun run test:web
  • bun run test:shared
  • targeted Claude title tests:
    • claudeRemote.test.ts
    • claudeRemoteLauncher.test.ts
    • claudeLocalLauncher.test.ts
    • types.test.ts
    • sessionScanner.test.ts
    • sessionTitleFallback.test.ts
  • manual verification against a real HAPI Hub/runner deployment:
    • remote SDK session receives a first-message fallback
    • existing/manual titles are preserved
    • native transcript ai-title format verified from local Claude Code sessions

bun run test:hub currently reports 487 passing tests and 7 unrelated Windows failures in the existing Cursor ACP verification/migration tests. This PR does not modify Hub or Cursor code.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Fallback titles can leak into the chat timeline and overwrite a real title — the new fallback path calls sendClaudeSessionMessage, which emits a normal message before updating metadata. Web currently normalizes summary outputs into visible agent-event blocks (web/src/chat/normalizeAgent.ts:455, web/src/chat/reducerTimeline.ts:840), so native ai-title/first-message fallback titles become transcript events instead of metadata-only updates. The title guard is also evaluated before ApiSessionClient.updateMetadata() enters its async lock/ack path (cli/src/api/apiSession.ts:907), so a concurrent change_title write that has not acked yet can still be overwritten by the fallback. Evidence: cli/src/claude/utils/sessionTitleFallback.ts:23, cli/src/claude/claudeLocalLauncher.ts:25.
    Suggested fix:
    export function applySessionTitleFallback(
        client: Pick<ApiSessionClient, 'updateMetadata'>,
        message: string
    ): boolean {
        const title = createSessionTitleFallback(message)
        if (!title) return false
    
        client.updateMetadata((metadata) => {
            if (metadata.name?.trim() || metadata.summary?.text.trim()) return metadata
            return {
                ...metadata,
                summary: { text: title, updatedAt: Date.now() }
            }
        })
        return true
    }
    Then route both ai-title and native summary fallback handling through that metadata-only helper instead of sendClaudeSessionMessage().

Questions

  • None.

Summary

  • Review mode: initial
  • One Major issue found: the title fallback uses the chat-message path, so it can create visible title events and race with explicit title writes. Residual risk: I did not run the test suite because this review must not execute PR code.

Testing

  • Not run (automation)

HAPI Bot

Comment thread cli/src/claude/utils/sessionTitleFallback.ts Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • No issues found at >=80% confidence in the added/modified lines.

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • Previous Major finding appears addressed: title fallback now writes through updateMetadata() and the guard runs inside the metadata updater. Residual risk: I did not execute PR code/tests, so runtime verification depends on CI and the contributor's reported targeted tests.

Testing

  • Not run (automation)

HAPI Bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant