Skip to content

[AI-1398] Capture Cursor ACP native session title (daemon)#331

Merged
alexeyzimarev merged 3 commits into
mainfrom
ai-1398-cursor-title
Jul 19, 2026
Merged

[AI-1398] Capture Cursor ACP native session title (daemon)#331
alexeyzimarev merged 3 commits into
mainfrom
ai-1398-cursor-title

Conversation

@realtonyyoung

@realtonyyoung realtonyyoung commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Problem

When dogfooding a hosted Cursor agent, the session title always showed as a random 8-char hex id. cursor-agent acp auto-titles its session and emits a session_info_update sessionUpdate carrying {title} (e.g. {"sessionUpdate":"session_info_update","title":"Shell Reporter"}), but the daemon dropped it as AcpUpdateKind.Unknown, so the title never reached the server.

Change (daemon half)

  • AcpHostedAgentRuntime.Reduce() maps session_info_update → a new AcpUpdateKind.SessionInfo carrying the title on a new AcpSessionUpdate.Title field.
  • AcpEventTranslator.Translate() emits a SessionTitle envelope (new AcpEventKind, string value session_title) when a non-blank title is present; a title-less info update is dropped.
  • Adds AcpEventKind.SessionTitle to the daemon-local Core contract, mirrored to the server-side Capacitor.Server.Core.Acp.AcpEventKind (kcap-server companion PR).

Tests

  • AcpHostedAgentRuntimeTestssession_info_update reduces to SessionInfo with the captured title.
  • AcpEventTranslatorTestsSessionInfo + title → SessionTitle envelope; blank/null/whitespace title → null.
  • AcpEventEnvelopeWireCompatTests — locks the session_title wire value.
  • FakeAcpAgent.BuildSessionInfoUpdate helper (probe-confirmed shape).

Full CLI unit suite green (3349/3351, 2 gated live E2E skipped).

Companion PR

kcap-server #1123: applies the SessionTitle envelope to the hosted-agent UI (durable title event + live agent-card prompt sync). Safe to merge in either order — an unknown kind is ignored by whichever side is behind.

cursor-agent's ACP stream auto-titles the session via a
`session_info_update` sessionUpdate carrying `{title}`. The daemon dropped
it as AcpUpdateKind.Unknown, so hosted-Cursor agents showed their random
8-char id as the session title.

Reduce() now maps `session_info_update` to a new AcpUpdateKind.SessionInfo
carrying the title; AcpEventTranslator emits a SessionTitle envelope (new
AcpEventKind, mirrored to the server contract) when a non-blank title is
present. The server half consumes this to update the displayed prompt.

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

linear-code Bot commented Jul 19, 2026

Copy link
Copy Markdown

AI-1398

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Capture ACP session_info_update titles as SessionTitle events in daemon

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Map ACP session_info_update frames to SessionInfo updates carrying the agent-authored title.
• Translate SessionInfo titles into a new SessionTitle event envelope for server/UI consumption.
• Add unit tests and wire-compat coverage to lock the "session_title" contract value.
Diagram

graph TD
  A{{"Cursor ACP stream"}} --> B["AcpHostedAgentRuntime.Reduce()"] --> C["AcpSessionUpdate\n(SessionInfo + Title)"] --> D["AcpEventTranslator.Translate()"] --> E["AcpEventEnvelope\n(SessionTitle)"] --> F{{"Server/UI"}}
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Extend an existing envelope kind to carry optional session title
  • ➕ Avoids introducing a new AcpEventKind value and wire-compat locking
  • ➕ Reduces coordination required with the server contract
  • ➖ Blurs semantics of the existing event kind and complicates consumers
  • ➖ Harder to enforce 'only when title is present' behavior cleanly
2. Emit a generic SessionInfo envelope with structured payload (future-proof)
  • ➕ Scales better if session_info_update gains more fields later
  • ➕ Avoids proliferation of narrowly-scoped envelope kinds
  • ➖ Requires a richer payload schema (breaking/expanding contracts) rather than simple string Text
  • ➖ More work for both daemon and server; higher review/rollout risk
3. Keep reducer as Unknown and parse title opportunistically in translator from Raw JSON
  • ➕ Minimizes model surface area changes (no new AcpUpdateKind/field)
  • ➕ Potentially faster to implement initially
  • ➖ More brittle (translator depends on raw JSON shape)
  • ➖ Harder to test and reason about; duplicates parsing responsibilities

Recommendation: The PR’s approach is the best tradeoff for now: a strongly-typed reducer mapping plus a dedicated SessionTitle envelope keeps responsibilities clean (parsing in Reduce, presentation in Translate), and the wire value is locked by a compat test. The main alternative worth considering—generic SessionInfo payloads—adds avoidable schema complexity given the current single-field need.

Files changed (8) +82 / -6

Enhancement (4) +29 / -6
Models.csAdd SessionTitle ACP event kind constant +1/-0

Add SessionTitle ACP event kind constant

• Introduces a new daemon-local core contract constant AcpEventKind.SessionTitle with wire value "session_title". This enables emitting a dedicated event envelope for session title updates.

src/Capacitor.Cli.Core/Models.cs

AcpEventTranslator.csTranslate SessionInfo updates into SessionTitle envelopes +15/-0

Translate SessionInfo updates into SessionTitle envelopes

• Extends translation logic to handle AcpUpdateKind.SessionInfo by emitting an AcpEventEnvelope of kind SessionTitle when Title is non-blank. Title-less SessionInfo updates are dropped to avoid generating content-free transcript events.

src/Capacitor.Cli.Daemon/Acp/AcpEventTranslator.cs

AcpSessionUpdate.csModel session_info_update as SessionInfo with optional Title field +8/-6

Model session_info_update as SessionInfo with optional Title field

• Adds AcpUpdateKind.SessionInfo and extends AcpSessionUpdate with a nullable Title field to carry the agent-authored session title. Updates enum documentation to reflect probe-confirmed SessionInfo presence on the wire.

src/Capacitor.Cli.Daemon/Acp/AcpSessionUpdate.cs

AcpHostedAgentRuntime.csReduce session_info_update frames and capture title +5/-0

Reduce session_info_update frames and capture title

• Updates the ACP reducer to map "session_info_update" into AcpUpdateKind.SessionInfo and extract the "title" field into AcpSessionUpdate.Title. Unknown variants continue to reduce safely to Unknown.

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

Tests (4) +53 / -0
AcpEventEnvelopeWireCompatTests.csLock SessionTitle wire value via compat test +1/-0

Lock SessionTitle wire value via compat test

• Adds an assertion that AcpEventKind.SessionTitle matches the expected wire string "session_title". This prevents accidental contract drift between daemon and server.

test/Capacitor.Cli.Tests.Unit/Acp/AcpEventEnvelopeWireCompatTests.cs

AcpEventTranslatorTests.csAdd SessionInfo→SessionTitle translation tests +23/-0

Add SessionInfo→SessionTitle translation tests

• Adds coverage ensuring SessionInfo with a real title produces a SessionTitle envelope carrying the title and metadata. Also verifies null/empty/whitespace titles translate to null.

test/Capacitor.Cli.Tests.Unit/Acp/AcpEventTranslatorTests.cs

AcpHostedAgentRuntimeTests.csAdd reducer test for session_info_update title capture +18/-0

Add reducer test for session_info_update title capture

• Adds a harness test that feeds a probe-shaped session_info_update notification and asserts Reduce outputs Kind=SessionInfo with the captured Title. Validates end-to-end reduction through the runtime update channel.

test/Capacitor.Cli.Tests.Unit/Acp/AcpHostedAgentRuntimeTests.cs

FakeAcpAgent.csAdd helper to build probe-confirmed session_info_update JSON +11/-0

Add helper to build probe-confirmed session_info_update JSON

• Introduces FakeAcpAgent.BuildSessionInfoUpdate to generate the probe-confirmed JSON shape {"sessionUpdate":"session_info_update","title":"..."}. Improves test readability and ensures consistent fixture construction.

test/Capacitor.Cli.Tests.Unit/Acp/FakeAcpAgent.cs

@qodo-code-review

qodo-code-review Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. XML comment mentions ai-688 ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
A new XML comment references a Linear-style issue identifier via
docs/ai-688-cursor-prototype-findings.md. This violates the requirement to avoid Linear issue
numbers in source comments for readability/public-repo hygiene.
Code

test/Capacitor.Cli.Tests.Unit/Acp/FakeAcpAgent.cs[R580-584]

+    /// <summary>
+    /// Probe-confirmed <c>session_info_update</c> variant (agent session auto-titling):
+    /// <c>{"sessionUpdate":"session_info_update","title":"..."}</c> — captured verbatim in
+    /// docs/ai-688-cursor-prototype-findings.md.
+    /// </summary>
Evidence
PR Compliance ID 4 prohibits including Linear issue identifiers in code comments. The added XML
comment explicitly references docs/ai-688-cursor-prototype-findings.md, embedding the Linear-style
ai-688 identifier.

CLAUDE.md: Keep comments minimal; prefer self-explanatory code and avoid Linear issue numbers in comments
test/Capacitor.Cli.Tests.Unit/Acp/FakeAcpAgent.cs[580-584]

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 newly-added XML comment includes a Linear-style issue identifier (`ai-688`) via a doc filename reference, which is disallowed.

## Issue Context
Compliance requires avoiding Linear issue numbers in source comments; use a GitHub issue number if an issue reference is unavoidable, or remove the issue reference entirely.

## Fix Focus Areas
- test/Capacitor.Cli.Tests.Unit/Acp/FakeAcpAgent.cs[580-584]

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



Remediation recommended

2. Non-string title drops update ✓ Resolved 🐞 Bug ☼ Reliability
Description
AcpHostedAgentRuntime.Reduce() reads session_info_update.title via GetStringOrNull(), which calls
JsonElement.GetString() without verifying the JSON value is a string; if title is present but
non-string, this throws and AcpConnection will skip the entire notification frame. This turns a
schema-drift/malformed agent payload into silently lost session/update events instead of treating
the title as absent.
Code

src/Capacitor.Cli.Daemon/Services/AcpHostedAgentRuntime.cs[R674-677]

+            "session_info_update" => new AcpSessionUpdate(
+                AcpUpdateKind.SessionInfo,
+                Title: GetStringOrNull(update, "title"),
+                Raw: update),
Evidence
The new reducer mapping for "session_info_update" reads title using `GetStringOrNull(update,
"title"), and GetStringOrNull calls value.GetString()` when the property exists; AcpConnection
documents and implements a catch that treats wrong-typed fields (throwing out of GetString()) as a
reason to skip the entire frame.

src/Capacitor.Cli.Daemon/Services/AcpHostedAgentRuntime.cs[639-690]
src/Capacitor.Cli.Daemon/Acp/AcpConnection.cs[226-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
`session_info_update` now extracts `title` using `GetStringOrNull(update, "title")`, but `GetStringOrNull` calls `JsonElement.GetString()` whenever the property exists. `GetString()` throws when the property exists but is not a JSON string (e.g., number/object/array), which causes the whole notification frame to be skipped by the JSON-RPC read loop.

### Issue Context
This PR newly introduces parsing of the `title` field for `session_info_update`, so it creates a new failure mode for schema-invalid frames that previously would have reduced to `Unknown` without touching `title`.

### Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AcpHostedAgentRuntime.cs[639-690]

### Suggested fix
Update `GetStringOrNull` (or introduce a new helper and use it for `title`) to:
- Return the string only when `value.ValueKind == JsonValueKind.String`.
- Return `null` for other kinds (including `Null`) rather than calling `GetString()`.

This makes `Reduce()` resilient to wrong-typed `title` without relying on outer catch-and-skip behavior.

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


Grey Divider

Qodo Logo

Comment thread test/Capacitor.Cli.Tests.Unit/Acp/FakeAcpAgent.cs
Comment thread src/Capacitor.Cli.Daemon/Services/AcpHostedAgentRuntime.cs

@realtonyyoung realtonyyoung left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Summary: 1 actionable finding: keep standalone session-title updates from splitting chunk aggregation.

ToolIsError: update.ToolIsError,
TimestampIso: timestampIso);

case AcpUpdateKind.SessionInfo:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[P2] Preserve the open chunk run across SessionInfo

AggregateUpdate routes this new kind through its default branch, which calls FlushOpenRunLocked() before invoking this translator. Therefore a session_info_update that arrives between two agent_message_chunk (or thought-chunk) frames splits one contiguous run into two AssistantText/AssistantThinking envelopes. Since this title is standalone metadata and must not perturb chunk aggregation, handle SessionInfo separately in AggregateUpdate so it emits the title without closing _openRunKind, and add an interleaving runtime test (chunk → session info → chunk).

AggregateUpdate sent the new SessionInfo kind through its default branch,
which calls FlushOpenRunLocked() before translating — so a session_info_update
interleaved between two message/thought chunks split one contiguous assistant
run into two transcript envelopes. Session-title metadata is not transcript
content and must not perturb chunk aggregation: SessionInfo now emits its
envelope standalone, leaving the open run untouched, so the surrounding chunks
still coalesce into one AssistantText/AssistantThinking envelope.

Adds a chunk → session_info → chunk regression test.

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

Copy link
Copy Markdown
Collaborator Author

NO FINDINGS

- GetStringOrNull now guards ValueKind == String before calling GetString(),
  which throws on a non-string JSON value (number/object/array). A schema-drift
  session_info_update with a wrong-typed title would otherwise bubble an
  exception up and make the read loop skip the entire notification frame; a
  wrong-typed field is now treated as absent (Title=null). Benefits every field
  the reducer reads (toolCallId/kind/status/title).
- Drop the Linear-issue-numbered doc filename from a test XML comment (repo
  source-comment hygiene).

Adds a non-string-title reduce regression test.

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

Copy link
Copy Markdown
Collaborator Author

NO FINDINGS

@alexeyzimarev
alexeyzimarev merged commit ca6ff16 into main Jul 19, 2026
6 checks passed
@alexeyzimarev
alexeyzimarev deleted the ai-1398-cursor-title branch July 19, 2026 21:13
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.

2 participants