Skip to content

Mirror each harness's own session title, including renames - #1230

Merged
realtonyyoung merged 39 commits into
mainfrom
claude-tyoung/ai-3335-harness-titles
Sep 29, 2026
Merged

realtonyyoung merged 39 commits into
mainfrom
claude-tyoung/ai-3335-harness-titles

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

Closes #1229 — AI-3335

What & why

Capacitor should show the title each harness itself shows, including operator renames, with generated titles only as a backstop. The CLI now reads each harness's own title store (Codex session_index.jsonl, Copilot workspace.yaml, Cursor chat/composer records, Kiro {id}.json, Antigravity conversation_summaries.db), posts changes to the server's new POST /hooks/harness-title with a rename/auto kind and a change time, forwards OpenCode title changes into the watched transcript, imports titles through the same route, pushes hosted Claude renames from the daemon, and stops LLM titling once a harness title is known.

Where to look

  • Change time: a store's own change time is sent on every read; otherwise a change is timed by the watcher's previous read and the first read after start is untimed, so any error favours a Capacitor Regenerate.
  • An older server answers a bare 404; every caller falls back to /hooks/set-title. A coded session_not_found is retried (import waits up to ~60 s for projection).

Verification

  • HarnessTitleClientTests 19/19, WatchCommandTests + WatchHarnessTitleTests 94/94, TitleResolveLoopTests + AgentOrchestratorNativeTitleTests 34/34, ClaudeNativeTitleTests 15/15, import integration classes 19/19.
  • dotnet publish -c Release for kcap and kcap-daemon: no IL2xxx/IL3xxx warnings.
  • Store layouts checked read-only against real installs on macOS (Antigravity summaries DB, Cursor chat ids, OpenCode SDK event types).

🤖 Generated with Claude Code

realtonyyoung and others added 19 commits September 29, 2026 12:44
A coded 404 means the session isn't visible yet and must not be
treated as route-missing, or a visible-session post would silently
fall back to the older route instead of surfacing as refused.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cursor's meta.json updatedAtMs moves with ordinary conversation
activity on a live install, not just renames, so CursorChatTitle
never reports a change time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pure tracker/detector live in Cli.Core (not Cli) since WatchState
does — Cli.Core cannot reference a type from the Cli assembly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Retries a coded session_not_found for ~60s so a title beats the
projection lag instead of racing it, falling back to set-title on an
older server. OpenCode's cancellation-recovery contract had to defer
this call's OperationCanceledException the same way it already defers
one from a descendant, or the terminal session-end re-close never ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plugin, not the server, owns the placeholder filter: an untitled
session's title must never masquerade as a rename downstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The periodic pump raced ImportHarnessTitle.PostAsync's per-attempt
CancellationTokenSource, built from the same FakeTimeProvider it
advanced blind of any in-flight request. A single big jump per
attempt raced the opposite way: it could land before that attempt's
Task.Delay timer even existed, stranding the test for a full backoff
it would never see advanced again. Small nudges after each observed
request close both windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EventSessionDeleted carries properties.info, not properties.sessionID like
the events this handler otherwise reads; without the fallback the generic
sid guard dropped every session.deleted before memory/coldStarts/lastTitle
cleanup ran, leaking one entry per session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SendWithRetryAsync's per-attempt CancellationTokenSource shares
whatever TimeProvider it's handed, so a single clock driving the
retry backoff in a test could still race that timeout under a
sustained stall. The internal overload lets tests hand backoff a
FakeTimeProvider and HTTP the real one; production still passes one
provider for both, so its own behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The harness's own title (rename or auto) is pushed whenever it changes,
independent of the server's title, since it is authoritative for what
the harness itself calls the session; the generated fallback still
converges only while the server is silent and no native title exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rename back to an earlier value is a switch, not a re-append — timing
it by first occurrence let a later revert lose to an intervening value.
Refused pushes stop retrying; Failed/SessionNotFound keep retrying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ClaudeNativeTitle's own static cache grew unbounded across a long-lived
daemon, keyed by lifetime-total sessions. It is stateless again; the
skip-when-unchanged memo now lives in AgentTitleState, dropped with
the agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-conversation conversations/<id>.db has no conversation_summaries table,
so every read there failed silently. Connections are unpooled so no handle
outlives a read and blocks Antigravity replacing its own file on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…itles

Only 400, 403 and 422 settle a value; 401, 408, 429 and server faults are Failed,
so the daemon and watcher retry them. An older server stores set-title verbatim,
so the fallback clamps to 120.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ceholders

OpenCode's placeholder title carries milliseconds, so it is matched by prefix as
the server and plugin match it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The IDE keys composerData:<id> in globalStorage/state.vscdb by the same id as the
agent transcript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alues

The post runs inline in the watch loop, so it gets half the heartbeat threshold;
an unsettled value is re-sent at most every 30s rather than on every drain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The watcher sends the same rename uncapped, so a daemon cap would make one rename
reach the server as two values; only the local display is capped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also reads Kiro's own files without denying it write access.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cquisition

The server normalises whitespace before its 200 clamp, so a client-side cut made
one long rename arrive as two values; only a 4096 safety cap remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Sep 29, 2026

Copy link
Copy Markdown

AI-3335

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Mirror harness session titles and renames across CLI and daemon

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Read harness-owned titles and renames from local stores and watched transcripts.
• Post title kind and change time through a new hook, with older-server fallback.
• Preserve native titles during imports and hosted sessions; generate titles only when none exists.
Diagram

graph TD
  Stores["Harness Stores"] --> Watcher["CLI Watcher"] --> Client["Title Client"] --> Server["Capacitor Server"]
  Stores --> Import["Session Import"] --> Client
  Transcript["Title Transcripts"] --> Watcher
  Transcript --> Daemon["Hosted Daemon"] --> Client
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Extract every title from server-side transcripts
  • ➕ Could avoid polling and posting titles already embedded in transcripts.
  • ➖ Cannot observe independent vendor stores or store-only renames.
  • ➖ Would couple server extractors to vendor-local formats and availability.
2. Reuse set-title exclusively
  • ➕ Requires fewer client-side protocol changes.
  • ➖ Loses rename versus auto provenance and change times.
  • ➖ Cannot apply the new harness-title behavior while retaining compatibility.

Recommendation: Keep vendor-specific reads at the CLI and send their results through the dedicated hook. The shared client preserves provenance and older-server compatibility; transcript-only extraction or set-title alone cannot cover the same cases.

Files changed (54) +2624 / -218

Enhancement (32) +1038 / -153
HarnessTitleTracker.csTrack observed title changes and retryable posts +47/-0

Track observed title changes and retryable posts

• Tracks the last store read, pending post, and settled value. Uses recorded change times when available or the previous read as a lower bound.

src/Capacitor.Cli.Core/Commands/HarnessTitleTracker.cs

TranscriptTitleLines.csDetect native titles embedded in transcripts +59/-0

Detect native titles embedded in transcripts

• Recognizes title records for Claude, Pi, Gemini, and OpenCode so the watcher can stop fallback generation; excludes OpenCode's placeholder.

src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs

AntigravityPaths.csLocate Antigravity's root-level summaries database +17/-11

Locate Antigravity's root-level summaries database

• Shares transcript-path validation between conversation database lookup and the new summary-database lookup.

src/Capacitor.Cli.Core/Harness/Antigravity/AntigravityPaths.cs

CodexSessionIndexTitle.csRead Codex names from the session index +69/-0

Read Codex names from the session index

• Finds the latest matching session-index entry, including its rename timestamp, while avoiding rescans of an unchanged file.

src/Capacitor.Cli.Core/Harness/Codex/CodexSessionIndexTitle.cs

CursorChatTitle.csRead Cursor per-chat titles +47/-0

Read Cursor per-chat titles

• Looks up the session's chat metadata across workspaces and ignores the unnamed default. Does not treat activity timestamps as rename times.

src/Capacitor.Cli.Core/Harness/Cursor/CursorChatTitle.cs

CursorPaths.csExpose Cursor chat and composer-store paths +10/-0

Expose Cursor chat and composer-store paths

• Adds paths for per-chat metadata and the IDE global-state database used by title readers.

src/Capacitor.Cli.Core/Harness/Cursor/CursorPaths.cs

OpenCodeExtensionInstaller.csStream OpenCode title changes into watched transcripts +23/-5

Stream OpenCode title changes into watched transcripts

• Handles session updates by appending deduplicated title records for top-level sessions. Filters placeholders and cleans up title state on deletion.

src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs

FirstTitleStore.csSelect the first available title store +15/-0

Select the first available title store

• Combines ordered stores for harnesses such as Cursor that may keep a session title in multiple places.

src/Capacitor.Cli.Core/Harness/Titles/FirstTitleStore.cs

IHarnessTitleStore.csDefine the harness title-store contract +10/-0

Define the harness title-store contract

• Defines best-effort title reads and whether a store records title-change times.

src/Capacitor.Cli.Core/Harness/Titles/IHarnessTitleStore.cs

StoreTitle.csRepresent a title read from a harness store +6/-0

Represent a title read from a harness store

• Carries title text, auto-or-rename provenance, and an optional recorded change time.

src/Capacitor.Cli.Core/Harness/Titles/StoreTitle.cs

HarnessTitleClient.csPost harness titles with legacy-route fallback +90/-0

Post harness titles with legacy-route fallback

• Sends provenance and optional UTC change time to the new hook. Distinguishes a coded session-not-found response from a route-missing 404 and falls back to set-title only for the latter.

src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs

HarnessTitleHook.csDefine the harness-title request body +11/-0

Define the harness-title request body

• Serializes session ID, title, kind, and optional change time under the hook's JSON field names.

src/Capacitor.Cli.Core/Http/HarnessTitleHook.cs

HarnessTitleKind.csDistinguish auto titles from renames +8/-0

Distinguish auto titles from renames

• Adds the shared title-provenance enum used by readers and HTTP callers.

src/Capacitor.Cli.Core/Http/HarnessTitleKind.cs

HarnessTitleOutcome.csClassify harness-title post results +18/-0

Classify harness-title post results

• Separates posted, session-not-found, route-missing, refused, and retryable-failure outcomes.

src/Capacitor.Cli.Core/Http/HarnessTitleOutcome.cs

HarnessTitlePost.csCarry title provenance into posts +5/-0

Carry title provenance into posts

• Defines the shared title, kind, and optional change-time value passed between producers and the HTTP client.

src/Capacitor.Cli.Core/Http/HarnessTitlePost.cs

Models.csRetain watcher title state and register hook JSON +12/-0

Retain watcher title state and register hook JSON

• Adds title tracking, retry timing, and native-title detection to watch state. Registers the new request body for JSON serialization.

src/Capacitor.Cli.Core/Models.cs

AgentOrchestrator.csForward hosted Claude title provenance +5/-2

Forward hosted Claude title provenance

• Converts extracted Claude titles into native-title posts and supplies the generated-title sender to the resolve loop.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs

ServerConnection.csSend daemon-generated titles over the hub +15/-0

Send daemon-generated titles over the hub

• Adds a best-effort UpdateTitle call for generated fallback titles, keeping them separate from native-title HTTP posts.

src/Capacitor.Cli.Daemon/Services/ServerConnection.cs

TitleResolveLoop.csPrioritize and push hosted native title changes +117/-45

Prioritize and push hosted native title changes

• Pushes revised native titles independently of the current server display title, while generated titles remain fallback-only. Retains retry outcomes and skips native extraction for unchanged transcript files.

src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs

TitleServerPort.csRoute daemon native titles through the shared client +9/-19

Route daemon native titles through the shared client

• Replaces direct set-title posting with the harness-title client, including canonical session IDs and legacy fallback.

src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs

ImportTitleNotRecorded.csReport titles not recorded during import +8/-0

Report titles not recorded during import

• Adds an import warning for refused, failed, or persistently unprojected title posts.

src/Capacitor.Cli/Commands/ImportTitleNotRecorded.cs

WatchCommand.csPoll and post harness titles during watching +98/-4

Poll and post harness titles during watching

• Reads stores before the first transcript drain and on later polls, retries unsettled posts, and suppresses LLM titling once a native title is seen.

src/Capacitor.Cli/Commands/WatchCommand.cs

AntigravitySummaryTitle.csRead Antigravity auto titles from SQLite +49/-0

Read Antigravity auto titles from SQLite

• Reads the matching conversation summary from the root database using a read-only, unpooled connection.

src/Capacitor.Cli/Harness/Antigravity/AntigravitySummaryTitle.cs

CopilotImportSource.csPreserve Copilot title provenance on import +17/-23

Preserve Copilot title provenance on import

• Reads workspace.yaml's user-named flag and forwards imported names as renames or auto titles through the shared import route.

src/Capacitor.Cli/Harness/Copilot/CopilotImportSource.cs

CopilotWorkspaceTitle.csRead Copilot workspace titles +18/-0

Read Copilot workspace titles

• Uses workspace.yaml name and user-named status to return the current title and its provenance.

src/Capacitor.Cli/Harness/Copilot/CopilotWorkspaceTitle.cs

CursorComposerTitle.csRead Cursor composer titles from SQLite +49/-0

Read Cursor composer titles from SQLite

• Queries the session's composer record in the IDE global-state database, excluding blank and default names.

src/Capacitor.Cli/Harness/Cursor/CursorComposerTitle.cs

KiroImportSource.csImport Kiro titles through the new route +12/-23

Import Kiro titles through the new route

• Forwards Kiro's auto title with projection-aware retries instead of direct set-title posting. Allows shared reads of its session sidecar.

src/Capacitor.Cli/Harness/Kiro/KiroImportSource.cs

KiroSessionTitle.csRead Kiro session sidecar titles +19/-0

Read Kiro session sidecar titles

• Reads the existing per-session JSON sidecar and classifies its title as auto-generated.

src/Capacitor.Cli/Harness/Kiro/KiroSessionTitle.cs

HarnessTitleStores.csWire vendor title readers to session watchers +36/-0

Wire vendor title readers to session watchers

• Selects Codex, Copilot, Kiro, Cursor, or Antigravity stores from the watched session; excludes subagents and vendors with inline titles.

src/Capacitor.Cli/Harness/Titles/HarnessTitleStores.cs

ImportHarnessTitle.csRetry imported titles across projection lag +65/-0

Retry imported titles across projection lag

• Shares title posting among importers and retries coded session-not-found responses for roughly a minute before warning.

src/Capacitor.Cli/Harness/Titles/ImportHarnessTitle.cs

ClaudeNativeTitle.csExtract Claude renames with provenance and time +69/-21

Extract Claude renames with provenance and time

• Makes custom-title renames take precedence over AI and legacy summary titles. Derives rename change time conservatively from preceding transcript timestamps while retaining capped display extraction.

src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeNativeTitle.cs

ClaudeTitle.csRepresent extracted Claude native titles +5/-0

Represent extracted Claude native titles

• Carries the full title, rename status, and optional change time for daemon posting.

src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitle.cs

Bug fix (1) +20 / -13
OpenCodeImportSource.csImport OpenCode titles without placeholders +20/-13

Import OpenCode titles without placeholders

• Posts non-placeholder titles as renames through the shared route. Defers cancellation during title posting so terminal session-end recovery still runs.

src/Capacitor.Cli/Harness/OpenCode/OpenCodeImportSource.cs

Tests (21) +1566 / -52
HarnessTitleTrackerTests.csTest title-change timing and settlement +99/-0

Test title-change timing and settlement

• Covers first reads, recorded and inferred times, failed-post retries, kind changes, and returning to a prior value.

test/Capacitor.Cli.Core.Tests.Unit/Commands/HarnessTitleTrackerTests.cs

TranscriptTitleLinesTests.csTest inline title detection +18/-0

Test inline title detection

• Checks recognized vendor title records, irrelevant lines, and OpenCode placeholder exclusion.

test/Capacitor.Cli.Core.Tests.Unit/Commands/TranscriptTitleLinesTests.cs

CodexSessionIndexTitleTests.csTest Codex session-index reads +88/-0

Test Codex session-index reads

• Covers latest-entry selection, timestamps, malformed lines, ID forms, concurrent writers, and unchanged-file caching.

test/Capacitor.Cli.Core.Tests.Unit/Harness/Codex/CodexSessionIndexTitleTests.cs

CursorChatTitleTests.csTest Cursor chat metadata reads +71/-0

Test Cursor chat metadata reads

• Covers workspace lookup, default and missing titles, transcript-derived IDs, and reading alongside a writer.

test/Capacitor.Cli.Core.Tests.Unit/Harness/Cursor/CursorChatTitleTests.cs

HarnessTitleClientTests.csTest harness-title protocol and fallback +214/-0

Test harness-title protocol and fallback

• Verifies payloads, coded versus bare 404s, refusals, transient failures, legacy clamping, whole-title sends, and timeouts.

test/Capacitor.Cli.Core.Tests.Unit/Http/HarnessTitleClientTests.cs

AgentOrchestratorNativeTitleTests.csTest hosted Claude title conversion +38/-0

Test hosted Claude title conversion

• Checks rename timestamps, untimed AI titles, and exclusion of non-Claude vendors.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/AgentOrchestratorNativeTitleTests.cs

TitleResolveLoopTests.csTest daemon title priority, posting, and caching +204/-49

Test daemon title priority, posting, and caching

• Covers native changes against existing server titles, generated fallback isolation, retry outcomes, display precedence, and transcript-stat caching.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/TitleResolveLoopTests.cs

CopilotImportSourceImportTests.csVerify Copilot imports post rename provenance +9/-1

Verify Copilot imports post rename provenance

• Marks the fixture user-named and checks the harness-title attempt carries rename kind before legacy fallback.

test/Capacitor.Cli.Tests.Integration/CopilotImportSourceImportTests.cs

KiroImportSourceImportTests.csVerify Kiro imports post auto provenance +8/-0

Verify Kiro imports post auto provenance

• Checks the harness-title attempt identifies Kiro's title as auto before legacy fallback.

test/Capacitor.Cli.Tests.Integration/KiroImportSourceImportTests.cs

OpenCodeImportSourceImportTests.csVerify OpenCode import titles and placeholder exclusion +31/-0

Verify OpenCode import titles and placeholder exclusion

• Checks rename provenance and confirms a newly seeded placeholder triggers neither title route.

test/Capacitor.Cli.Tests.Integration/OpenCodeImportSourceImportTests.cs

AntigravitySummaryTitleTests.csTest Antigravity summary-database title reads +100/-0

Test Antigravity summary-database title reads

• Covers root database location, auto provenance, missing rows, concurrent access, and connection release.

test/Capacitor.Cli.Tests.Unit/Harness/Antigravity/AntigravitySummaryTitleTests.cs

CopilotImportSourceTests.csAssert Copilot's default naming classification +1/-0

Assert Copilot's default naming classification

• Checks that workspace metadata without user_named is not classified as a user rename.

test/Capacitor.Cli.Tests.Unit/Harness/Copilot/CopilotImportSourceTests.cs

CopilotWorkspaceTitleTests.csTest Copilot workspace title reads +51/-0

Test Copilot workspace title reads

• Covers user-named and auto classifications, missing names, and reads alongside a writer.

test/Capacitor.Cli.Tests.Unit/Harness/Copilot/CopilotWorkspaceTitleTests.cs

CursorComposerTitleTests.csTest Cursor composer-database title reads +74/-0

Test Cursor composer-database title reads

• Covers named, default, missing, and unrelated records plus access while the database is open elsewhere.

test/Capacitor.Cli.Tests.Unit/Harness/Cursor/CursorComposerTitleTests.cs

KiroImportSourceTests.csTest shared Kiro sidecar reads +11/-0

Test shared Kiro sidecar reads

• Checks that reading the sidecar does not deny another process write access.

test/Capacitor.Cli.Tests.Unit/Harness/Kiro/KiroImportSourceTests.cs

KiroSessionTitleTests.csTest Kiro sidecar title reads +42/-0

Test Kiro sidecar title reads

• Covers auto provenance, blank and missing titles, and reads alongside a writer.

test/Capacitor.Cli.Tests.Unit/Harness/Kiro/KiroSessionTitleTests.cs

OpenCodeSessionStartMemoryTests.csCheck OpenCode extension title-event behavior +47/-2

Check OpenCode extension title-event behavior

• Asserts title-record emission, child and placeholder filtering, and session-deletion cleanup in the installed extension.

test/Capacitor.Cli.Tests.Unit/Harness/OpenCode/OpenCodeSessionStartMemoryTests.cs

HarnessTitleStoresTests.csVerify title-store selection for each harness +108/-0

Verify title-store selection for each harness

• Tests store paths and Cursor precedence against vendor-shaped layouts, plus exclusions for subagents and inline-title vendors.

test/Capacitor.Cli.Tests.Unit/Harness/Titles/HarnessTitleStoresTests.cs

ImportHarnessTitleTests.csTest import title retries and compatibility +138/-0

Test import title retries and compatibility

• Exercises projection-lag retries, eventual warnings, and older-server fallback using an HTTP test server.

test/Capacitor.Cli.Tests.Unit/Harness/Titles/ImportHarnessTitleTests.cs

WatchHarnessTitleTests.csTest watcher posting and LLM suppression +141/-0

Test watcher posting and LLM suppression

• Covers retry spacing, changed-value posting, refusals, timeout bounds, read failures, and suppression when a store title exists.

test/Capacitor.Cli.Tests.Unit/WatchHarnessTitleTests.cs

ClaudeNativeTitleTests.csTest Claude rename precedence and timing +73/-0

Test Claude rename precedence and timing

• Covers custom-title precedence, legacy summaries, missing and nested timestamps, rename-backs, and uncapped post titles.

test/Capacitor.Models.Transcripts.Tests.Unit/Harness/Claude/ClaudeNativeTitleTests.cs

A failed post may still have committed, so a return to the acknowledged value is
re-sent; OpenCode title lines carry a time only when OpenCode supplied one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@qodo-code-review

qodo-code-review Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Copied titles suppress title generation ✓ Resolved
Description
ClaudeTitleLine.CarriesTitle accepts an ai-title without checking its sessionId, and
WatchCommand then permanently suppresses generated titles. When a resumed Claude session contains
copied history from another session, kcap-server rejects that title because its session ID differs
from the batch ID, leaving the current session without that title or a generated replacement.
Code

src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs[R18-20]

+            switch (root.Str("type")) {
+                case "ai-title":
+                    return !string.IsNullOrWhiteSpace(root.Str("aiTitle"));
Relevance

●●● Strong

Matches the PR’s stated intent: only server-recorded harness titles should suppress fallback
generation.

PR-#1045

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI marks any nonblank Claude ai-title as recorded and uses that mark to stop generation, while
the server explicitly rejects an ai-title with a missing or mismatched session ID.

kcap-cli -> kcap-server
src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs[18-23]
src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[12-15]
src/Capacitor.Cli/Commands/WatchCommand.cs[2235-2236]
src/Capacitor.Cli/Commands/WatchCommand.cs[3499-3505]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Services/Sessions/SessionWriter.TranscriptPipeline.cs [170-188]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The watcher counts a copied Claude `ai-title` as recorded even though kcap-server rejects titles whose `sessionId` does not match the batch session.

## Fix Focus Areas
- src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs[18-23]
- src/Capacitor.Cli/Commands/WatchCommand.cs[2235-2236]
- src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[12-15]

## Recommended Fix
Pass the watched session ID into Claude title classification and apply the same canonical session-ID comparison as kcap-server before marking an `ai-title` as recorded. Add a resumed-session test with a copied title from another session.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Lost server titles are never restored ✗ Dismissed
Description
TitleResolveLoop retains PushedNative and PushedGenerated after a successful push, without
clearing either when a later server read no longer finds the title. If the server subsequently loses
that title while the agent remains active, unchanged native and generated titles are both excluded
from future pushes.
Code

src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[200]

+            if (outcome is HarnessTitleOutcome.Posted or HarnessTitleOutcome.Refused) state.PushedNative = native;
Relevance

●●● Strong

Persistent push markers can suppress recovery after server state loss, matching accepted
state-reconstruction and delivery findings.

PR-#526
PR-#1163

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both push branches suppress repeats using persistent successful-push markers; the successful
server-read path updates ServerTitle but no longer clears those markers when the observed title
changes or disappears.

src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[184-216]
src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[222-241]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A successful title push remains suppressed even after a later server read shows that its title is gone.
## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[184-216]
- src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[222-241]
## Recommended Fix
Rearm the relevant successful-push marker when a successful server read establishes that the pushed title is no longer present, while preserving the attempted-title bookkeeping used to identify server echoes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Claude sessions trigger duplicate titles ✗ Dismissed
Description
TranscriptTitleLines.CarriesHarnessTitle recognizes Claude ai-title and custom-title records
but omits the legacy summary record that ClaudeNativeTitle still accepts as an auto title. When
an older Claude transcript contains only that valid summary title, draining leaves
HarnessTitleSeen false and the threshold path can start LLM title generation.
Code

src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[12]

+        "claude" => (line.Contains("\"ai-title\"") || line.Contains("\"custom-title\"")) && ClaudeTitle(line),
Relevance

●●● Strong

Direct legacy-format omission can trigger duplicate LLM titles; recent parser compatibility fixes
are consistently accepted.

PR-#1182
PR-#974

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new inline-title detector excludes summary, while the new Claude reader explicitly continues
to treat it as a native title. The watch loop relies exclusively on this detector to set the
suppression flag before evaluating the LLM-title condition.

src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[11-26]
src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeNativeTitle.cs[40-76]
src/Capacitor.Cli/Commands/WatchCommand.cs[2223-2233]
src/Capacitor.Cli/Commands/WatchCommand.cs[3488-3491]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Claude legacy `{"type":"summary","summary":...}` records are valid native titles but do not suppress watcher-side LLM title generation.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[11-26]

## Recommended Fix
Extend the Claude prefilter and `ClaudeTitle` switch to recognize `type == "summary"` when its `summary` value is non-blank, matching `ClaudeNativeTitle`'s existing legacy-title handling. Add a regression test for this record shape.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (2)
4. Harness renames leave old titles unchanged ✓ Resolved
Description
PostOrFallBackAsync drops Kind when /hooks/harness-title is missing and reports a successful
/hooks/set-title response as Posted. The pinned kcap-server has no new route and projects the
legacy request as vendor-title, which only fills an empty title, so renames of already titled
sessions do not appear.
Code

src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[R57-63]

+        var payload = new JsonObject { ["session_id"] = sessionId, ["title"] = Clamp(post.Title, SetTitleMax) };
+
+        try {
+            using var content = new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json");
+            using var resp    = await client.PostWithRetryAsync($"{baseUrl}/hooks/set-title", content, time, remaining, ct);
+
+            if (resp.IsSuccessStatusCode) return HarnessTitleOutcome.Posted;
Relevance

●● Moderate

Fallback rename semantics depend on older server behavior; the compatibility concern is credible but
cross-repository and version-specific.

PR-#745
PR-#977

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR sends only session ID and title on fallback; the server has no harness-title route, maps
set-title to vendor-title, and retains an existing title for that source.

kcap-cli -> kcap-server
src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[25-34]
src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[52-63]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public/Hooks/HookEndpoints.cs [168-186]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public/Hooks/SessionActivityHookHandlers.cs [374-390]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Services/ReadModels/SessionSummaryFold.cs [445-464]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pinned server lacks the new route, and its legacy set-title route cannot replace an existing title.
## Fix Focus Areas
- src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[49-63]
- /cross_repos/kcap-server/src/Capacitor.Api.Public/Hooks/HookEndpoints.cs[168-186]
- /cross_repos/kcap-server/src/Capacitor.Api.Public/Hooks/SessionActivityHookHandlers.cs[374-390]
## Recommended Fix
Deploy a server route that accepts title kind and change time before relying on rename posts. Do not treat a legacy set-title success as confirmation that a rename was applied.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. OpenCode titles never reach the server ✓ Resolved
Description
OpenCodeExtensionInstaller appends session_title records, and TranscriptTitleLines recognizes
both those records and Claude custom-title records as existing titles without posting their values
through a title hook. When either record appears, the watcher stops generating a title, while the
pinned kcap-server’s transcript title extraction handles only Claude ai-title records, leaving the
OpenCode title unrecorded or a transcript-only Claude rename unapplied.
Code

src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[R458-460]

+                  lastTitle.set(id, title)
+                  mkdirSync(dir, { recursive: true })
+                  appendFileSync(file(id), JSON.stringify({ type: "session_title", title, time: info?.time?.updated ?? Date.now() }) + "\n")
Relevance

●● Moderate

Cross-repository title propagation is semantically plausible, but depends on pinned server behavior
and intended compatibility.

PR-#977
PR-#1016

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI writes OpenCode session_title records and recognizes them alongside Claude custom-title
records as known titles, which causes the watcher to suppress generated titles. The cited server
transcript pipeline extracts title updates only from Claude ai-title records, so neither of those
other record types reaches a server title update through that path.

kcap-cli -> kcap-server
src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[451-460]
src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[35-40]
src/Capacitor.Cli/Commands/WatchCommand.cs[2222-2231]
src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[12-25]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Services/Sessions/SessionWriter.TranscriptPipeline.cs [318-335]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Services/Sessions/SessionWriter.TranscriptPipeline.cs [175-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
OpenCode `session_title` and Claude `custom-title` records suppress client-generated titles without becoming server title updates.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[451-460]
- src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[12-25]
- src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[35-40]
- src/Capacitor.Cli/Commands/WatchCommand.cs[2222-2231]
- /cross_repos/kcap-server/src/Capacitor.Server.Services/Sessions/SessionWriter.TranscriptPipeline.cs[175-185]
- /cross_repos/kcap-server/src/Capacitor.Server.Services/Sessions/SessionWriter.TranscriptPipeline.cs[318-335]

## Recommended Fix
Ensure recognized title records become server title updates before they suppress generated titles. Post OpenCode title changes through a compatible title hook or coordinate server ingestion of `session_title` records; for Claude `custom-title`, coordinate server extraction with rename semantics or post the records through a rename-capable hook.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. A title comment narrates past writes ✓ Resolved
Description
ClaudeTitleLine describes the legacy summary record as one that earlier versions wrote, using
change-history narration instead of stating the current compatibility constraint. When a maintainer
reads this beside CarriesTitle, the comment does not make explicit that persisted Claude
transcripts can still supply this record.
Code

src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs[R7-8]

+/// <c>{"type":"summary","summary":…}</c> earlier versions wrote, which only a server with harness titles records (the
+/// summary ranking below an <c>ai-title</c>). Those two count only once that server is confirmed.</summary>
Relevance

●●● Strong

Recent precedents accept replacing historical comment narration with present-tense invariants.

PR-#1029
PR-#1054

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added comment says earlier versions wrote the legacy shape, while the cited rule excludes
change-history narration from comments.

Rule 2897915: Avoid time-sensitive or process-reference metadata in code comments
src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs[5-8]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The comment describes past writing behavior rather than the current reason to recognize legacy summary records.

## Fix Focus Areas
- src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs[5-8]

## Recommended Fix
State that persisted Claude Code transcripts can contain the legacy summary shape and that the classifier must continue recognizing it when the server supports harness titles.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Formatted Claude summaries trigger titling ✗ Dismissed
Description
ClaudeTitleLine.CarriesTitle requires the literal "type":"summary" before parsing JSON, so it
skips a valid summary record with whitespace around the colon even though the title extractor
accepts it. When a watched transcript contains such a record, ObserveTitleLine leaves
InlineHarnessTitleSeen unset and the watcher can start generating an LLM title despite the summary
being available.
Code

src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs[16]

+        if (!line.Contains("\"ai-title\"") && !line.Contains("\"custom-title\"") && !line.Contains("\"type\":\"summary\"")) return false;
Relevance

●●● Strong

This is a deterministic parsing bug; accepted Claude parsing fixes address valid transcript
variants.

PR-#1045
PR-#1182

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new literal guard rejects {"type": "summary", "summary": "Title"} before JSON parsing. The
existing native extractor parses summary records by their JSON properties, while the watcher sets
InlineHarnessTitleSeen only when classification succeeds and otherwise permits LLM generation when
its threshold conditions are met.

src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs[14-25]
src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeNativeTitle.cs[40-60]
src/Capacitor.Cli/Commands/WatchCommand.cs[3498-3513]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A valid JSON summary such as `{"type": "summary", "summary": "Title"}` fails the literal prefilter and does not prevent redundant LLM title generation.
## Fix Focus Areas
- src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs[14-26]
- test/Capacitor.Models.Transcripts.Tests.Unit/Harness/Claude/ClaudeTitleLineTests.cs[7-16]
## Recommended Fix
Use a whitespace-independent prefilter for summaries, such as checking for `"summary"` before parsing, and rely on the parsed `type` and title field for validation. Add a test with whitespace around the JSON separators.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. OpenCode retains titles after sessions end ✓ Resolved
Description
The session.updated handler adds each session’s title to lastTitle, but the map is cleared only
when that session is deleted and has no size limit. Sessions that become idle without being deleted
leave their IDs and titles in memory for the lifetime of a long-running plugin process.
Code

src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[464]

+                  lastTitle.set(id, title)
Relevance

●●● Strong

Recent history accepts memory-lifetime and bounded-cache fixes; unbounded session-title retention is
a concrete resource leak.

PR-#1181
PR-#1102

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new handler inserts an entry after each successful append. The only removal is in
session.deleted; the session.idle path handles normal idle sessions without removing their
entries. The existing memory-fragment cache in the same plugin has an explicit 64-session bound, but
the title map does not.

src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[88-105]
src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[451-476]
src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[479-505]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The OpenCode plugin retains every successfully written session title until a deletion event, so ordinary idle sessions accumulate in its process-wide deduplication map.
## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[88-105]
- src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[451-476]
## Recommended Fix
Apply a size limit or expiry policy to `lastTitle`, while retaining deletion-event cleanup. Evict old entries before inserting new ones; an evicted session may safely append its title again if it resumes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (8)
9. Older servers can double title timeouts ✓ Resolved
Description
PostOrFallBackAsync subtracts elapsed time from a nullable timeout, leaving remaining null
when callers omit the argument. On a bare 404 from an older server, the fallback request then
receives a fresh default HTTP timeout rather than the remainder of the first request's budget.
Code

src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[R54-55]

+        var remaining = timeout - time.GetElapsedTime(started);
+        if (remaining <= TimeSpan.Zero) return HarnessTitleOutcome.Failed;
Relevance

●●● Strong

Nullable timeout propagation can give fallback requests an independent budget, a concrete
deadline-accounting defect.

PR-#1097
PR-#767

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The daemon calls without an explicit timeout. Nullable subtraction leaves the fallback timeout null,
which the HTTP extension replaces with its independent default.

src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[43-58]
src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[44-61]
src/Capacitor.Cli.Core/HttpClientExtensions.cs[96-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An omitted timeout gives each compatibility request its own default timeout instead of one shared budget.
## Fix Focus Areas
- src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[44-61]
## Recommended Fix
Resolve the default timeout before the first request, then subtract elapsed time from that non-null budget before issuing the fallback.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Mid-session titles can remain unwatched ✗ Dismissed
Description
The OpenCode session.updated handler appends a title line but does not call ensureStarted for
that session. If the plugin starts after session creation and receives a title update without a
subsequent start-triggering request or idle event, it creates a transcript file with no watcher to
forward the title.
Code

src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[R459-461]

+                  mkdirSync(dir, { recursive: true })
+                  appendFileSync(file(id), JSON.stringify({ type: "session_title", title, time: info?.time?.updated ?? Date.now() }) + "\n")
+                  return
Relevance

●●● Strong

A title update creating an unwatched transcript is a concrete lifecycle gap; watcher startup
coverage is regularly accepted.

PR-#1181
PR-#256

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new branch returns immediately after appending; the existing lifecycle branch invokes
ensureStarted, and the CLI starts a watcher only through that session-start path.

src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[451-493]
src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[421-434]
src/Capacitor.Cli/Commands/Harness/OpenCodeHookCommand.cs[199-207]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A title update can write an OpenCode transcript without initiating the watcher that consumes it.
## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[451-461]
- src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[478-493]
## Recommended Fix
Route top-level title updates through the existing classified, idempotent session-start path so an update received mid-session also has a watcher.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Shutdown can miss a final harness rename ✓ Resolved
Description
WatchCommand polls the title store before and during its regular drain loop, but does not poll it
during the final shutdown drain. A store-backed title that changes after the last regular poll is
absent from the transcript drain and is never posted before the watcher exits.
Code

src/Capacitor.Cli/Commands/WatchCommand.cs[R833-835]

+                if (titleStore is not null && (drained.Count > 0 || time.GetUtcNow() - state.LastHarnessTitleRead >= HarnessTitleRetryGap)) {
+                    state.LastHarnessTitleRead = time.GetUtcNow();
+                    await PostHarnessTitleAsync(titleStore, sessionId, state, cts.Token, TouchHeartbeat);
Relevance

●●● Strong

Shutdown omission can lose final externally sourced changes; recent WatchCommand drain and lifecycle
gaps were accepted.

PR-#1181
PR-#291

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added store calls occur only in the main loop. Shutdown drains and spools transcript lines
without another store call, while the affected vendors obtain titles from separate stores.

src/Capacitor.Cli/Commands/WatchCommand.cs[820-836]
src/Capacitor.Cli/Commands/WatchCommand.cs[940-983]
src/Capacitor.Cli/Harness/Titles/HarnessTitleStores.cs[18-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The watcher drains final transcript lines but does not read the separate harness title store once more before exiting.
## Fix Focus Areas
- src/Capacitor.Cli/Commands/WatchCommand.cs[820-836]
- src/Capacitor.Cli/Commands/WatchCommand.cs[940-983]
## Recommended Fix
Perform a bounded final title-store read and post during shutdown, alongside the final drain, so late store changes have a delivery opportunity.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. A failed title write blocks its retry ✓ Resolved
Description
The OpenCode update handler records a title in lastTitle before appendFileSync succeeds, and its
surrounding catch discards write errors. If that append fails transiently, subsequent updates
carrying the same title pass the deduplication check without writing it, even though no transcript
line was delivered.
Code

src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[R457-460]

+                  if (lastTitle.get(id) === title) return
+                  lastTitle.set(id, title)
+                  mkdirSync(dir, { recursive: true })
+                  appendFileSync(file(id), JSON.stringify({ type: "session_title", title, time: info?.time?.updated ?? Date.now() }) + "\n")
Relevance

●●● Strong

Deduplication state preceding fallible I/O is a concrete retry-loss bug, matching recent accepted
reliability findings.

PR-#1163
PR-#1054

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new title branch updates its deduplication map before the potentially failing append, whereas
the existing message writer explicitly marks entries seen only after a successful append.

src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[447-461]
src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[224-247]
src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[496-503]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The OpenCode title cache marks a value written before the transcript append has succeeded.
## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs[451-461]
## Recommended Fix
Move the `lastTitle` update after a successful append, leaving the title eligible for a later event if filesystem writing fails.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Transient title responses end import attempts ✓ Resolved
Description
HarnessTitleClient.PostAsync calls PostWithRetryAsync without enabling retryable-status
handling, so an HTTP 408, 429 or server error becomes Failed after its first response.
ImportHarnessTitle retries only SessionNotFound, so an import encountering one of those
transient responses stops trying to deliver its title.
Code

src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[27]

+            using var resp    = await client.PostWithRetryAsync($"{baseUrl}/hooks/harness-title", content, time, timeout, ct);
Relevance

●●● Strong

Transient HTTP statuses bypass retry handling, directly losing title delivery; accepted queue
reliability precedents support this.

PR-#1163
PR-#526

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The extension defaults retryStatuses to false and returns retryable responses immediately in that
mode; the import loop only continues for coded session-not-found outcomes.

src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[25-36]
src/Capacitor.Cli.Core/HttpClientExtensions.cs[92-104]
src/Capacitor.Cli.Core/HttpClientExtensions.cs[279-283]
src/Capacitor.Cli/Harness/Titles/ImportHarnessTitle.cs[40-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Retryable HTTP statuses are returned immediately by the new harness-title request, and imports do not retry the resulting failure.
## Fix Focus Areas
- src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs[25-36]
- src/Capacitor.Cli/Harness/Titles/ImportHarnessTitle.cs[40-56]
## Recommended Fix
Enable bounded retryable-status handling for harness-title posts, preserving the distinct treatment of coded session-not-found responses and permanent refusals.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Import users get outdated title guidance ✓ Resolved
Description
kcap import now posts Kiro’s own stored title, but the unchanged README says Kiro leaves title
derivation to the server. The normal historical-import documentation also describes the old title
behavior for other harnesses, so readers cannot use it to understand the command’s new default.
Code

src/Capacitor.Cli/Harness/Kiro/KiroImportSource.cs[R321-323]

+            await ImportHarnessTitle.PostAsync(
+                ctx.HttpClient, _time, ctx.BaseUrl, classification.SessionId,
+                new HarnessTitlePost(title, HarnessTitleKind.Auto, null),
Relevance

●●● Strong

Recent precedents consistently accept README updates for user-facing CLI behavior changes and import
semantics.

PR-#629
PR-#677
PR-#240

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added Kiro import call posts a stored title, while the README’s import-command section still
says Kiro leaves title derivation to the server. No README patch is included in this PR.

Rule 2270057: Keep CLI documentation in README.md in sync with user-facing CLI changes
src/Capacitor.Cli/Harness/Kiro/KiroImportSource.cs[316-324]
README.md[970-982]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The README’s normal historical-import section describes title behavior that this change replaces.

## Fix Focus Areas
- README.md[970-982]
- src/Capacitor.Cli/Harness/Kiro/KiroImportSource.cs[316-324]

## Recommended Fix
Update the README’s import-command guidance to explain which harness titles are imported from native stores, when generated titles are used, and how `--skip-title` affects the new behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Kiro test skips injected temp fixture ✓ Resolved
Description
Usage_sidecar_reads_while_a_writer_holds_it constructs a TempDir locally instead of receiving a
public required [TempDir] property. This new filesystem test therefore manages its own directory
despite the test project’s fixture-injection pattern.
Code

test/Capacitor.Cli.Tests.Unit/Harness/Kiro/KiroImportSourceTests.cs[70]

+        using var tmp = new TempDir();
Relevance

●● Moderate

TempDir injection findings have mixed outcomes, though recent accepted precedents support enforcing
the fixture pattern.

PR-#1178
PR-#1054
PR-#1138

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added test explicitly constructs new TempDir(), whereas the checklist requires test classes
needing TempDir to use a public required [TempDir] property.

Rule 2808173: Use injected [TempDir] public required property in test classes instead of manual fields
test/Capacitor.Cli.Tests.Unit/Harness/Kiro/KiroImportSourceTests.cs[67-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new Kiro test creates its own TempDir instead of using the test class’s injected fixture.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/Harness/Kiro/KiroImportSourceTests.cs[67-75]

## Recommended Fix
Add a public required `[TempDir]` property to the test class and use it to create the sidecar file. Remove the local `new TempDir()` allocation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. Four title parsers sit outside harnesses ✓ Resolved
Description
TranscriptTitleLines parses raw Claude, Pi, Gemini and OpenCode transcript records in
Capacitor.Cli.Core.Commands instead of the vendors’ transcript harness directories. When the
watcher drains a line, it invokes these parsers directly, leaving vendor field interpretation in a
shared command component.
Code

src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[R43-45]

+    static bool TryParse(string line, out JsonElement root) {
+        try {
+            using var doc = JsonDocument.Parse(line);
Relevance

●● Moderate

Architecture rule conflicts with the stated Cli.Core dependency constraint; historical structural
rule findings are often rejected.

PR-#1140
PR-#1112

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new CLI Core file parses raw JSON and interprets title fields for four named vendors; the
watcher calls it on drained transcript lines. Both checklist items require vendor-specific parsing
to remain in the designated harness location.

Rule 2762984: Place vendor-specific harness code only in the correct Harness/&lt;Vendor&gt;/ assembly and directory
Rule 3114705: Vendor transcript formats must be parsed only in designated harness directory
src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[11-45]
src/Capacitor.Cli/Commands/WatchCommand.cs[2222-2225]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Raw vendor transcript title formats are parsed in a shared CLI command component rather than their designated transcript harness directories.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs[11-45]
- src/Capacitor.Cli/Commands/WatchCommand.cs[2222-2225]

## Recommended Fix
Move each vendor’s raw-line parsing and field interpretation into its `Capacitor.Models.Transcripts/Harness/<Vendor>/` directory, expose the title-presence result, and have the watcher consume that result.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
  Explored: repo: kurrent-io/kcap-server (sha: a7359eac) — View relationship
Review mode: 🚀 Fast: This is a small, localized Claude title-detection behavior change with focused tests and no security, API, schema, or broad cross-cutting risk.

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs Outdated
Comment thread src/Capacitor.Cli/Harness/Kiro/KiroImportSource.cs
Comment thread test/Capacitor.Cli.Tests.Unit/Harness/Kiro/KiroImportSourceTests.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs Outdated
Comment thread src/Capacitor.Cli/Commands/WatchCommand.cs
Comment thread src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs Outdated
Comment thread src/Capacitor.Cli.Core/Commands/TranscriptTitleLines.cs Outdated
Comment thread src/Capacitor.Cli.Core/Http/HarnessTitleClient.cs Outdated
Comment thread src/Capacitor.Cli.Core/Harness/OpenCode/OpenCodeExtensionInstaller.cs Outdated
realtonyyoung and others added 6 commits September 29, 2026 16:02
…udget

Import has no later tick, so it opts into status retries; the watcher and daemon retry on their own cadence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Compatibility fix in 40bcb61. On a server without /hooks/harness-title, a harness title turned off LLM titling even though that server never recorded it.

That happened in two ways. A Claude custom-title, Pi, Gemini or OpenCode title line in the transcript counted as a title, and so did a store title sent through the set-title fallback, which only fills an empty title there. Such a session kept its raw first-message title.

  • Once per watcher run, before the first drain, the watcher probes the server. It posts to /hooks/harness-title with an empty session id: a 400 means the route exists, a bare 404 means it does not, and anything else is unknown and is probed again after 30s.
  • A Claude ai-title line still turns LLM titling off on every server, because every server records it.
  • The other transcript title lines turn it off only on a server with the route. While the probe answer is unknown, LLM titling waits.
  • A store title turns it off only when /hooks/harness-title answered with success. A post the fallback took returns the new outcome PostedToLegacyRoute. It settles the value, so it is not re-sent, but it does not turn LLM titling off.

/agentic_review follows.

@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

/agentic_review

An inconclusive probe may never be retried before a short session ends, so only a server known to
have harness titles lets an inline title stop LLM titling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 40bcb61

realtonyyoung and others added 5 commits September 29, 2026 16:28
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Qodo re-review (at c686683): open findings, addressed on 692caf3f.

  1. Copied titles suppress title generation. Fixed in 93e6a93. ClaudeTitleLine.CarriesTitle now takes the watched session id. It counts an ai-title or custom-title line only when the line's own sessionId names that session, compared in canonical dashless GUID form. This is the same check ClaudeHarnessTitleExtractor in kcap-server #2147 applies, so title lines copied in from a resumed session's history no longer stop LLM titling. Covered by the new ClaudeTitleLineTests.Counts_only_a_title_line_of_the_watched_session and a new TranscriptTitleLinesTests case.
  2. Lost server titles are never restored. Not changed, as answered in the previous round. The server has no path that deletes a session title. A read that no longer shows a pushed title means the projection has not caught up yet, and re-arming on that read would re-post during every lag window. For the native lane, the spec forbids re-posting an unchanged native value, because an old rename left in the transcript must not override a later Regenerate.
  3. Claude sessions trigger duplicate titles. Not changed, as answered in the previous round. No server records the legacy summary shape: ClaudeHarnessTitleExtractor takes only ai-title and custom-title. Letting it suppress LLM titling would leave the session with no title at all. ClaudeTitleLineTests pins the exclusion.
  4. Harness renames leave old titles unchanged. Not changed, but the fallback is now reported honestly. A server without /hooks/harness-title has no route that can carry rename semantics, and #2147 adds that route. Since 40bcb61, a set-title fallback success is no longer reported as Posted. It returns PostedToLegacyRoute, which is not counted as a recorded harness title, so LLM titling continues for that session.
  5. OpenCode retains titles after sessions end. Fixed in 692caf3. The plugin's lastTitle dedupe map is now capped at 64 sessions, like its memory cache. The oldest insertion is evicted first and session.deleted still removes entries. An evicted session only writes its title line again, which the watcher dedupes. Test: Plugin_bounds_the_title_dedupe_map.
  6. Mid-session titles can remain unwatched. Not changed, as answered in the previous round. session.updated also fires when a past session is renamed from the session list, so starting a watcher from it would send session-start for a session that may already have ended. The title line is kept in the transcript file, and the next ensureStarted trigger (a request's cold-start heal, or an idle) starts the watcher, which ships it.

Also in this push, from our own review: 1bc3769. When a poll takes up a store read left over from an earlier poll, it now reads the store again while its budget allows. The shutdown poll therefore cannot settle for a stale value. The blocked-read tests now order their steps with signals instead of timing.

@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

/agentic_review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Qodo re-review (at c686683): open findings, addressed on 692caf3f.

  1. Copied titles suppress title generation. Fixed in 93e6a93. ClaudeTitleLine.CarriesTitle now takes the watched session id. It counts an ai-title or custom-title line only when the line's own sessionId names that session, compared in canonical dashless GUID form. This is the same check ClaudeHarnessTitleExtractor in kcap-server #2147 applies, so title lines copied in from a resumed session's history no longer stop LLM titling. Covered by the new ClaudeTitleLineTests.Counts_only_a_title_line_of_the_watched_session and a new TranscriptTitleLinesTests case.
  2. Lost server titles are never restored. Not changed, as answered in the previous round. The server has no path that deletes a session title. A read that no longer shows a pushed title means the projection has not caught up yet, and re-arming on that read would re-post during every lag window. For the native lane, the spec forbids re-posting an unchanged native value, because an old rename left in the transcript must not override a later Regenerate.
  3. Claude sessions trigger duplicate titles. Not changed, as answered in the previous round. No server records the legacy summary shape: ClaudeHarnessTitleExtractor takes only ai-title and custom-title. Letting it suppress LLM titling would leave the session with no title at all. ClaudeTitleLineTests pins the exclusion.
  4. Harness renames leave old titles unchanged. Not changed, but the fallback is now reported honestly. A server without /hooks/harness-title has no route that can carry rename semantics, and #2147 adds that route. Since 40bcb61, a set-title fallback success is no longer reported as Posted. It returns PostedToLegacyRoute, which is not counted as a recorded harness title, so LLM titling continues for that session.
  5. OpenCode retains titles after sessions end. Fixed in 692caf3. The plugin's lastTitle dedupe map is now capped at 64 sessions, like its memory cache. The oldest insertion is evicted first and session.deleted still removes entries. An evicted session only writes its title line again, which the watcher dedupes. Test: Plugin_bounds_the_title_dedupe_map.
  6. Mid-session titles can remain unwatched. Not changed, as answered in the previous round. session.updated also fires when a past session is renamed from the session list, so starting a watcher from it would send session-start for a session that may already have ended. The title line is kept in the transcript file, and the next ensureStarted trigger (a request's cold-start heal, or an idle) starts the watcher, which ships it.

Also in this push, from our own review: 1bc3769. When a poll takes up a store read left over from an earlier poll, it now reads the store again while its budget allows. The shutdown poll therefore cannot settle for a stale value. The blocked-read tests now order their steps with signals instead of timing.

Thanks for the update. Based on the current Qodo findings state:

  • finding 1 is implemented by the session-scoped Claude title-line check.
  • finding 2 remains active: the implementation intentionally treats a missing echoed title as projection lag and does not restore it.
  • finding 3 remains active: legacy Claude summary lines are intentionally excluded, so they do not suppress LLM titling.
  • finding 4 and finding 6 are implemented, including the legacy-route outcome and bounded OpenCode dedupe map.
  • finding 8 remains active: the current cold-start/idle healing path does not guarantee that every mid-session rename is watched immediately.

The stale-read and shutdown-poll changes are covered by the added synchronization-focused tests. No dismissal or implementation action was requested here.

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5ec2a22

…ords

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

/agentic_review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs Outdated
Comment thread src/Capacitor.Models.Transcripts/Harness/Claude/ClaudeTitleLine.cs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit a184ba6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 92fa60f

…cing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 71b8ca4

@realtonyyoung
realtonyyoung merged commit 862cb1d into main Sep 29, 2026
8 checks passed
@realtonyyoung
realtonyyoung deleted the claude-tyoung/ai-3335-harness-titles branch September 29, 2026 22:56
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.

Mirror each harness's own session title, including renames

1 participant