Skip to content

Adopt server session titles in the desktop app - #787

Merged
alexeyzimarev merged 12 commits into
mainfrom
capacitor/agent-fb146fa0dd764b
Sep 6, 2026
Merged

alexeyzimarev merged 12 commits into
mainfrom
capacitor/agent-fb146fa0dd764b

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Closes #780 — AI-2527

What & why

The web labels sessions with the server's title; the desktop app never read it, so the same session was labeled differently in the two places. The daemon now resolves a title per hosted agent — native transcript title (Claude's ai-title lines, with the older summary shape still accepted), then the server's title, then at most one local generation — and carries it in the existing AgentStatusDto.Title field, so the rail, workspace header, Home cards, and tray all upgrade over the same status lane. Locally resolved titles are pushed through /hooks/set-title, and the server's title stays authoritative, so both UIs converge on the identical string.

Where to look

TitleResolveLoop's two guards: a server title that prefixes the launch prompt is treated as the watcher's initial echo (not adopted, doesn't block the push), and a failed server read is not "silence" — it must never trigger a paid generation for a session the watcher already titled.

Verification

  • dotnet run per suite: Core 3020, CLI 3984, App 1416, Integration 245 — all green; daemon suite green except two environmental failures (installed codex 0.153.0 vs vendored 0.147.0 schema pin; a PSI env-absence assert that trips under any kcap-hosted shell) and known CPU-contention flakes that pass in isolation.
  • dotnet publish -c Release on kcap and kcap-daemon: no IL2026/IL3050 warnings.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-05T17:15:33.750878Z 0eb2637 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Adopt server session titles across the desktop app

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Resolves hosted session titles from transcripts, server state, or one delayed local generation.
• Synchronizes local titles to the server and broadcasts resolved titles through daemon status.
• Displays consistent titles across Home cards and tray entries, with fallback labels.
Diagram

graph TD
  Transcript["Claude transcript"] --> Extractor["Native extractor"] --> Resolver["Title resolver"] --> Status["Agent status"] --> Desktop["Home and tray"]
  Server[("Session server")] --> Resolver
  Resolver --> Server
  Generator["Local generator"] --> Resolver
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fetch titles directly in the desktop app
  • ➕ Avoids adding a daemon background resolver.
  • ➕ Lets the UI request current server data on demand.
  • ➖ Duplicates title logic across desktop consumers.
  • ➖ Breaks offline and private-agent behavior.
  • ➖ Requires separate refresh, authentication, and failure handling outside the status lane.
2. Rely exclusively on watcher-generated server titles
  • ➕ Eliminates local daemon generation and transcript scanning.
  • ➕ Keeps the server as the sole title source.
  • ➖ Leaves unrecorded and private agents without upgraded titles.
  • ➖ Cannot immediately use vendor-native transcript titles.
  • ➖ Degrades desktop titles during watcher delays or server outages.

Recommendation: Keep the PR's centralized daemon resolver. It preserves the existing AgentStatusDto distribution path, supports private and unrecorded agents locally, avoids duplicate generation after failed server reads, and converges desktop and web titles without adding UI-specific networking.

Files changed (23) +1132 / -240

Enhancement (8) +359 / -4
SessionCardViewModel.csLead Home cards with resolved session titles +5/-1

Lead Home cards with resolved session titles

• Uses the daemon-provided session title when available and moves repository and vendor context to a new subtitle. Existing repository/vendor labeling remains the fallback.

src/Capacitor.App/ViewModels/SessionCardViewModel.cs

TrayViewModel.csPrefix tray entries with session titles +8/-1

Prefix tray entries with session titles

• Adds resolved titles to native tray labels and truncates titles beyond 40 characters before appending agent metadata.

src/Capacitor.App/ViewModels/TrayViewModel.cs

HomeView.axamlBind Home card subtitles to resolved metadata +1/-1

Bind Home card subtitles to resolved metadata

• Displays the new computed subtitle while retaining the full repository path as tooltip content.

src/Capacitor.App/Views/HomeView.axaml

ClaudeNativeTitle.csExtract native titles from Claude transcripts +44/-0

Extract native titles from Claude transcripts

• Reads live JSONL transcripts without blocking writers and selects the latest nonblank ai-title or legacy summary. Malformed and unreadable input is ignored, and output is capped at 120 characters.

src/Capacitor.Cli.Core/Harness/Claude/ClaudeNativeTitle.cs

AgentOrchestrator.LocalIpc.csPublish resolved titles through daemon status +1/-1

Publish resolved titles through daemon status

• Prefers an agent's resolved title over its prompt-derived launch title when creating AgentStatusDto snapshots.

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

AgentOrchestrator.csWire periodic title resolution into agent orchestration +64/-0

Wire periodic title resolution into agent orchestration

• Adds per-agent resolved-title state, status pulses, a 60-second resolver loop, title source adapters, private-session isolation, and timer disposal. The resolver is guarded so individual or loop-level failures do not terminate background processing.

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

TitleResolveLoop.csImplement the hosted-agent title resolution ladder +175/-0

Implement the hosted-agent title resolution ladder

• Resolves server, native, and generated titles with server authority, prompt-echo filtering, a five-minute generation grace period, and one generation attempt per agent. Pushes local titles to recorded sessions, retries failed pushes, and isolates failures by agent.

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

TitleServerPort.csAdd the server title read and synchronization adapter +61/-0

Add the server title read and synchronization adapter

• Reads titles from session summaries and posts local titles through /hooks/set-title. Distinguishes genuine title absence from authentication, transport, and server failures to prevent unnecessary generation.

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

Refactor (5) +160 / -149
TitleGeneration.csCentralize shared session title generation in Core +152/-0

Centralize shared session title generation in Core

• Moves vendor-specific headless generation, prompt construction, title cleanup, refusal detection, and safe logging into Core for reuse by CLI and daemon callers.

src/Capacitor.Cli.Core/TitleGeneration.cs

ImportCommand.csUse Core title generation for imports +1/-1

Use Core title generation for imports

• Routes imported-session title generation through the shared TitleGeneration implementation.

src/Capacitor.Cli/Commands/ImportCommand.cs

TitleGenerator.csRetain only CLI transcript-side title helpers +5/-146

Retain only CLI transcript-side title helpers

• Removes generation and cleanup logic now hosted in Core. Keeps transcript context extraction and internal sub-session detection, referencing Core's shared title prompt prefix.

src/Capacitor.Cli/Commands/TitleGenerator.cs

WatchCommand.csUse shared title generation in the watcher +1/-1

Use shared title generation in the watcher

• Switches watcher-generated session titles to the Core TitleGeneration service without changing retry behavior.

src/Capacitor.Cli/Commands/WatchCommand.cs

WhatsDoneCommand.csReuse Core's headless summarizer system prompt +1/-1

Reuse Core's headless summarizer system prompt

• References the shared Core system prompt for Claude-based what's-done generation.

src/Capacitor.Cli/Commands/WhatsDoneCommand.cs

Tests (8) +585 / -85
SessionCardViewModelTests.csTest Home card title and fallback presentation +27/-0

Test Home card title and fallback presentation

• Verifies resolved titles lead cards while repository and vendor move below, and confirms legacy labels remain when no title exists.

test/Capacitor.App.Tests.Unit/SessionCardViewModelTests.cs

TrayViewModelTests.csTest tray title prefixes and truncation +28/-0

Test tray title prefixes and truncation

• Covers resolved-title tray labels and the 39-character-plus-ellipsis truncation applied to long native menu rows.

test/Capacitor.App.Tests.Unit/TrayViewModelTests.cs

ClaudeNativeTitleTests.csCover Claude native transcript title extraction +85/-0

Cover Claude native transcript title extraction

• Tests latest-title selection, legacy summaries, malformed and blank records, missing files, live writer sharing, and length limits.

test/Capacitor.Cli.Core.Tests.Unit/Harness/Claude/ClaudeNativeTitleTests.cs

TitleGenerationTests.csCover shared title cleanup and logging behavior +91/-0

Cover shared title cleanup and logging behavior

• Moves and expands tests for markdown cleanup, length caps, refusal rejection, prompt-prefix stability, and safe log sanitization.

test/Capacitor.Cli.Core.Tests.Unit/TitleGenerationTests.cs

AgentResolvedTitleTests.csTest resolved-title status overrides and pulses +113/-0

Test resolved-title status overrides and pulses

• Verifies resolved titles replace prompt seeds in status snapshots and trigger exactly one notification when changed.

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

TitleResolveLoopTests.csTest title precedence, generation, and convergence +233/-0

Test title precedence, generation, and convergence

• Covers native revisions, server authority, prompt echoes, grace-period generation, unreadable servers, push retries, failure isolation, and departed-agent state cleanup.

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

ImportResolveReposSubSessionTests.csReference Core's title prompt in import tests +2/-1

Reference Core's title prompt in import tests

• Updates sub-session recognition coverage to use the relocated shared title prompt prefix.

test/Capacitor.Cli.Tests.Unit/Commands/ImportResolveReposSubSessionTests.cs

TitleGeneratorTests.csNarrow CLI title tests to transcript helper behavior +6/-84

Narrow CLI title tests to transcript helper behavior

• Removes tests relocated with generation logic to Core and retains coverage for title, what's-done, and unrelated prompt recognition.

test/Capacitor.Cli.Tests.Unit/Commands/TitleGeneratorTests.cs

Documentation (2) +28 / -2
CHANGES.mdDocument desktop title resolution and convergence rules +24/-0

Document desktop title resolution and convergence rules

• Explains the resolution ladder, prompt-echo detection, server failure semantics, local title synchronization, and private-agent behavior.

docs/CHANGES.md

StatusIpc.csClarify the evolving agent title contract +4/-2

Clarify the evolving agent title contract

• Documents that AgentStatusDto.Title begins as a prompt seed and may be upgraded by native, server, or generated title resolution.

src/Capacitor.Cli.Core/LocalIpc/StatusIpc.cs

@qodo-code-review

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Malformed reads trigger generation ✓ Resolved 🐞 Bug ☼ Reliability
Description
GetTitleAsync treats every 2xx response with an unparsable or empty body as a successful null
title. The resolver consequently treats an unreadable server as silent and can spend a local
generation call after the grace period, violating the intended no-generation-on-read-failure
behavior.
Code

src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[R34-37]

+            return outcome.StatusCode switch {
+                >= 200 and < 300 => outcome.Body?.Title,
+                404              => null, // not registered server-side (yet): silence, not failure
+                _                => throw new HttpRequestException($"session summary read failed: {outcome.StatusCode}"),
Relevance

●●● Strong

Malformed response handling should fail closed; recent history accepts defensive parsing for
malformed server data.

PR-#331
PR-#347

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
WorkContextClient returns a 2xx outcome even when ReadAsync produces a null body after JSON/read
failures. The new adapter maps that null body to a null title, which marks serverReadOk true; the
new loop then permits generation when it remains null past the grace period.

src/Capacitor.Cli.Core/WorkItems/WorkContextClient.cs[30-49]
src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[32-38]
src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[102-121]

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 2xx session-summary response whose body cannot be deserialized is currently returned as a null title. This makes `TitleResolveLoop` interpret an unreadable server response as confirmed silence and invoke paid local generation after the grace period.

## Issue Context
`WorkContextClient` intentionally represents malformed successful JSON with a successful status and `Body == null`. The title adapter must instead turn that state into a read failure so the loop preserves its distinction between server silence and server unavailability.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[32-38]
- src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[98-121]

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


2. Failed reads still overwrite titles ✓ Resolved 🐞 Bug ≡ Correctness
Description
When the server title read fails, serverReadOk remains false but the push condition ignores it and
treats serverReal == null as confirmed silence. A native title can therefore be posted over an
existing authoritative server title when the summary endpoint fails but the title hook remains
available.
Code

src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[R133-136]

+        if (serverReal is null && local is not null && local != state.PushedTitle && agent.SessionId is { } sid) {
+            var pushed = false;
+            try {
+                pushed = await _server.PushTitleAsync(sid, local, ct);
Relevance

●●● Strong

The PR explicitly requires failed reads to remain non-silent; the guard omission is a direct
correctness bug.

PR-#347

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The read path sets serverReadOk only after GetTitleAsync returns successfully and leaves it
false on exceptions. Although generation requires that flag, the later convergence condition checks
only serverReal, so any available native/generated title is pushed after a failed read; the port
explicitly throws for authentication and non-404 server failures.

src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[98-122]
src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[131-142]
src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[25-38]

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 failed server title read is currently treated as sufficient reason to push a local title. This can overwrite an existing authoritative server title even though the resolver never established that the server was silent.

## Issue Context
Generation already checks `serverReadOk`, but convergence pushes do not. Preserve retries after confirmed successful reads while suppressing pushes following auth, transport, or summary-route failures.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[98-110]
- src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[131-142]

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


3. Title push uses raw ID ✓ Resolved 🐞 Bug ≡ Correctness
Description
PushTitleAsync validates that a canonical session ID exists but sends the original ID in the hook
payload. Dashed or whitespace-padded IDs are consequently read under the server's canonical key but
pushed under a different identifier, preventing title convergence.
Code

src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[R50-53]

+            var payload = new JsonObject {
+                ["session_id"] = sessionId,
+                ["title"]      = title,
+            };
Relevance

●●● Strong

Canonicalization is an established repository pattern, and sending the raw identifier breaks title
convergence.

PR-#253

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
WorkContextIds.CanonicalSessionId explicitly defines the server key as trimmed and dash-stripped,
and WorkContextClient uses that canonical value for summary reads. The new push method only
null-checks canonicalization and then places the unmodified sessionId in its JSON payload.

src/Capacitor.Cli.Core/WorkItems/WorkContextIds.cs[3-12]
src/Capacitor.Cli.Core/WorkItems/WorkContextClient.cs[23-26]
src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[42-56]
src/Capacitor.Cli/ArgParsing.cs[45-51]

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 title push validates a canonical session ID but discards it and posts the raw identifier. The server files sessions under the trimmed, dashless key, so noncanonical runtime IDs may update no session or the wrong key.

## Issue Context
Capture the result of `WorkContextIds.CanonicalSessionId` and use that value in the `session_id` payload, matching summary reads and existing environment-based set-title behavior.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[42-56]

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



Remediation recommended

4. Generation ignores shutdown cancellation ✓ Resolved 🐞 Bug ☼ Reliability
Description
GenerateTitleForAsync receives the resolver cancellation token but never forwards it to the new
shared generator, so an active Claude or Codex title subprocess survives daemon shutdown until its
timeout. This delays shutdown and defeats the runners' cooperative cancellation and process-kill
behavior.
Code

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[R4838-4841]

+    async Task<string?> GenerateTitleForAsync(TitleAgentView agent, CancellationToken ct) {
+        var result = await TitleGeneration.GenerateAsync(
+            agent.Prompt!, null, msg => _logger.LogDebug("Title generation ({AgentId}): {Message}", agent.Id, msg),
+            _config.Profiles.Resolution.Profile, _home,
Relevance

●●● Strong

Recent shutdown and cancellation precedents favor propagating caller tokens into long-running
subprocess operations.

PR-#149
PR-#506

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The resolver supplies its shutdown token to its generation delegate, but the delegate does not use
it and TitleGeneration.GenerateAsync has no token parameter. Both underlying runners support
token-aware process termination, so the new call path leaves that support unreachable.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4813-4820]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4838-4844]
src/Capacitor.Cli.Core/TitleGeneration.cs[64-81]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeCliRunner.cs[44-50]
src/Capacitor.Cli.Core/Harness/Codex/CodexCliRunner.cs[140-152]
PR-#640

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 daemon title-generation callback accepts a cancellation token from the shutdown-controlled resolve loop, but it is unused. The shared generation API and both vendor runner calls need to accept and forward this token so shutdown kills an in-flight subprocess promptly.

## Issue Context
`ClaudeCliRunner` and `CodexCliRunner` already implement external cancellation by killing their process trees and propagating `OperationCanceledException`; the new title path simply does not supply the token.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4838-4844]
- src/Capacitor.Cli.Core/TitleGeneration.cs[64-81]

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


5. TitleResolveLoop probes credentials periodically ✗ Dismissed 📘 Rule violation ⛨ Security
Description
The new periodic title-resolution path creates an authenticated client for every hosted session,
potentially discovering, minting, or refreshing credentials without an explicit borrowed launch.
This violates the prohibition on background credential probing.
Code

src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[25]

+        var (client, status) = await HttpClientExtensions.CreateClientWithAuthStatusAsync(configRoot, profiles, _baseUrl, ct);
Relevance

●●● Strong

The finding directly conflicts with the explicit prohibition on proactive daemon credential probing.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2406691 prohibits periodic daemon jobs from probing or obtaining credentials. The PR starts
RunTitleResolveLoopAsync as a background loop, and TitleServerPort.GetTitleAsync calls
CreateClientWithAuthStatusAsync; that shared helper performs provider discovery and can mint
machine credentials or resolve refresh-aware stored tokens.

Rule 2406691: Daemon must not proactively obtain or persist credentials
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4803-4818]
src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[20-32]
src/Capacitor.Cli.Core/HttpClientExtensions.cs[98-123]
src/Capacitor.Cli.Core/HttpClientExtensions.cs[153-159]

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 periodic title resolver calls `CreateClientWithAuthStatusAsync`, which can discover, mint, load, or refresh credentials outside an explicit borrowed-launch request.

## Issue Context
`RunTitleResolveLoopAsync` runs every 60 seconds. Server-title synchronization should not initiate credential acquisition from this background job; use an already-authorized connection or defer synchronization until an explicitly permitted operation provides authorization.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs[20-45]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4803-4818]

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


6. Transcripts are repeatedly rescanned ✗ Dismissed 🐞 Bug ➹ Performance
Description
ClaudeNativeTitle.TryExtract synchronously rereads every transcript from byte zero on every
60-second resolver tick, making title resolution consume unbounded cumulative I/O as live
transcripts grow. Because agents are processed serially and extraction has no cancellation check, a
large transcript also delays every later agent and shutdown of the resolver tick.
Code

src/Capacitor.Cli.Core/Harness/Claude/ClaudeNativeTitle.cs[R19-22]

+            using var stream = new FileStream(transcriptPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+            using var reader = new StreamReader(stream);
+
+            while (reader.ReadLine() is { } line) {
Relevance

●● Moderate

Performance concern is plausible, but no close historical precedent confirms acceptance of
transcript-scan optimization.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The extractor opens the file from its beginning and calls ReadLine until EOF without an offset or
cancellation token. The daemon wires this synchronous extractor for every Claude agent, and
TickAsync awaits each agent sequentially before advancing.

src/Capacitor.Cli.Core/Harness/Claude/ClaudeNativeTitle.cs[14-42]
src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[69-96]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4829-4836]

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

## Issue description
Native-title extraction performs a complete synchronous scan of each growing Claude transcript every minute. This creates repeated unbounded I/O and delays other agents in the sequential resolver.

## Issue Context
Track a per-transcript offset and latest title, scan only newly completed lines, and support cancellation. Handle truncation/replacement by resetting cached state safely.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/Claude/ClaudeNativeTitle.cs[14-42]
- src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[69-96]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4835-4836]

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



Informational

7. Truncation splits Unicode titles 🐞 Bug ≡ Correctness
Description
Normalize truncates titles by UTF-16 code-unit index, so a cutoff between an emoji's surrogate
halves creates malformed text that is then applied to status payloads and title pushes. Native
extraction and tray-label truncation introduce the same boundary error.
Code

src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[R145-148]

+    static string? Normalize(string? title) {
+        if (string.IsNullOrWhiteSpace(title)) return null;
+        var trimmed = title.Trim();
+        return trimmed.Length > 120 ? trimmed[..120] : trimmed;
Relevance

● Weak

A nearly identical surrogate-pair truncation finding was recently rejected for prompt-title slicing.

PR-#666

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new normalization, native extraction, and tray formatting all use direct [..N] slicing. The
existing prompt-title implementation explicitly adjusts its cutoff when the boundary is a high
surrogate, demonstrating that status-facing truncation must preserve surrogate pairs.

src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[145-149]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeNativeTitle.cs[35-42]
src/Capacitor.App/ViewModels/TrayViewModel.cs[272-278]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[90-101]

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

## Issue description
New title truncation slices UTF-16 strings at fixed indexes and can leave a lone surrogate when the boundary crosses a supplementary Unicode character. Such malformed titles can break encoding or display in status, HTTP, and native-menu consumers.

## Issue Context
Use a shared surrogate-safe truncation helper, following the existing prompt-title implementation. Preserve the current maximum lengths and ellipsis behavior.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs[145-149]
- src/Capacitor.Cli.Core/Harness/Claude/ClaudeNativeTitle.cs[35-42]
- src/Capacitor.App/ViewModels/TrayViewModel.cs[272-278]

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


Grey Divider

Context sources
✅ Compliance rules (platform): 58 rules
Review mode: 🧠 Deep: This is a broad, behavior-changing daemon/UI feature spanning title precedence, server synchronization, paid LLM generation, transcript parsing, HTTP/auth handling, background timing, and multiple status consumers, creating many independent, easy-to-miss failure modes.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs
Comment thread src/Capacitor.Cli.Daemon/Services/TitleResolveLoop.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs
Comment thread src/Capacitor.Cli.Daemon/Services/TitleServerPort.cs
Comment thread src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0eb263794b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

var serverTitle = Normalize(await _server.GetTitleAsync(sessionId, ct));
serverReadOk = true;

if (serverTitle is not null && !IsPromptEcho(serverTitle, agent.Prompt)) serverReal = serverTitle;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve later native-title revisions

When a Claude transcript initially yields title A, the first tick pushes A to the server; once a later tick reads A back here, it becomes serverReal. If Claude subsequently revises its ai-title to B, serverReal ?? native keeps applying A and the non-null server value prevents B from being pushed, so the documented last native title never reaches either the desktop or server. Distinguish the resolver's own PushedTitle from an independently changed server title so a newer native value can advance it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 76e16de — a server title equal to the loop's own PushedTitle no longer counts as an independent server title, so a later native revision applies and pushes; pinned by The_loops_own_pushed_title_does_not_block_a_native_revision (with An_independent_server_title_still_wins_over_a_native_revision as the control).

var line = raw.Trim();
if (line.Length == 0) continue;

return line.StartsWith(t, StringComparison.Ordinal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Match only actual prompt-seed forms

When a genuine generated server title happens to be a prefix of the launch prompt—for example, title Fix login timeout for prompt Fix login timeout by adding retries—this classifies it as the watcher's seed and ignores it. The watcher emits only the full short first line or its near-80-character truncation, not arbitrary shorter prefixes, so this can trigger a duplicate local generation after five minutes and overwrite an already valid server title. Compare against the exact watcher/daemon seed forms instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 76e16de — a bare prefix only counts as an echo when it carries the truncation ellipsis; otherwise only an exact first-line match does. Pinned by A_short_real_title_that_prefixes_the_prompt_is_still_adopted.

Comment on lines +4839 to +4842
var result = await TitleGeneration.GenerateAsync(
agent.Prompt!, null, msg => _logger.LogDebug("Title generation ({AgentId}): {Message}", agent.Id, msg),
_config.Profiles.Resolution.Profile, _home,
vendor: agent.Vendor == "codex" ? "codex" : "claude");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Forward shutdown cancellation to title generation

When shutdown begins during a local title generation, this method receives the daemon cancellation token but does not pass it through TitleGeneration.GenerateAsync to the Claude/Codex runners, even though those runners support cancellation and kill their subprocesses. The headless CLI can therefore continue running—and potentially consuming a paid request—after daemon teardown starts, until its 15–30 second timeout; thread ct through the shared generation helper.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 76e16de — the token now threads through TitleGeneration.GenerateAsync to both runners.

A server title equal to the loop's own push is not authoritative, a failed
summary read blocks the push as well as generation, only ellipsis-marked
prefixes count as prompt echoes, and cancellation now reaches the headless
title subprocess.
)

A per-tick authority let an outage tick demote the applied title, and a push
whose ack was lost let the loop adopt its own title as independent server
authority.
#780)

A confirmed push suppressed reconvergence even after the server was observed
silent again, and a single attempt slot let a delayed echo of an older
unacked push pass the ours-filter and freeze as authority.
Clear-on-overflow erased the whole provenance window (even on a duplicate
retry), a transient extraction gap demoted the ladder to the generated
fallback, and a title landing during the long model call could be
overwritten by the fallback's push.
@alexeyzimarev
alexeyzimarev force-pushed the capacitor/agent-fb146fa0dd764b branch from fd126d0 to ead36c6 Compare September 6, 2026 10:04
@alexeyzimarev
alexeyzimarev merged commit 6e0317a into main Sep 6, 2026
6 checks passed
@alexeyzimarev
alexeyzimarev deleted the capacitor/agent-fb146fa0dd764b branch September 6, 2026 17:12
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.

Adopt server session titles in the desktop app

1 participant