Skip to content

Stop remote agents and answer their prompts from the desktop app - #874

Merged
alexeyzimarev merged 53 commits into
mainfrom
desktop-remote-control-slice2
Sep 11, 2026
Merged

alexeyzimarev merged 53 commits into
mainfrom
desktop-remote-control-slice2

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Closes #805 — AI-2553

What & why

Agents on the user's other machines are visible in the desktop app but read-only: a remote session's prompts can only be answered in the web UI, and a remote agent cannot be stopped. The app now stops a remote agent through the hub and answers permission prompts and questions over the server's permission-response route, each on the lane that delivered it, while the local socket keeps answering the local daemon's own. The daemon publishes its local↔server request-id mapping on the local permission wire, so a prompt heard on both lanes renders as one card and, without proof of correlation, as two — never zero. Local ACP-hosted agents' questions, which only the server carries, render too. Opening a remote row shows a card host with the session's authorization lifecycle: access watch, chat join, reconnect re-check, revocation.

Where to look

PermissionService's lane-scoped cache: a local settlement retires its claimed server twin, and a lost daemon subscription drops local cards so their answerable twins surface. docs/CHANGES.md records the three invariants. Cold-start pips for a session never opened still need the server's pending-interrupts seed (AI-2537).

Verification

App 1692/1692; Cli.Core 3162 passed, 9 skipped; Remote.Models 7/7; Daemon 3055 passed, 38 skipped, 1 failed (CodexAppServerSchemaConformanceTests: installed codex 0.154.0 vs the vendored pin; the diff touches no daemon file it reads). dotnet publish src/Capacitor.Cli/Capacitor.Cli.csproj -c Release 2>&1 | grep -E 'IL[23][01][0-9]{2}' prints nothing.

alexeyzimarev and others added 25 commits September 10, 2026 13:14
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
)

A server item is shadowed only by a live local item claiming its exact server id, and a local settlement retires that twin: the daemon has answered the hook, so the server copy is moot even if the relay fails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The socket that could answer those cards is gone; the server twins they were shadowing carry handles that still work.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Access transitions publish under the access service's lock, so both branches hand off to the pool before touching the permission cache's own lock.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…805)

A pending ping names no request id, so the set is filled by reconciling the session's stream headlessly; a response naming an id the set never held re-reconciles rather than trusting the count.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Access is the server's own verdict, so an empty pane can only ever mean no cards. The three card templates now live in one application-level dictionary, so the chat pane and this host cannot drift apart.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server lane is built before the action service because a remote stop goes over the hub, and the permission service after the directory because a server-lane item names a session that only the directory's map turns into an agent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The origin lookup and the workspace factory are two reads of a cache the directory recomputes on background threads, so both have to tolerate the row being gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…805)

A missing hub method, a serialization mismatch and a persistent auth fault all rendered as the lane being down. The reason is remembered per entry so the retry ladder does not restate it every few seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Immutable snapshots keep IndexOf and Count at the call sites, so the reading tests are unchanged; a plain List both dropped entries and threw mid-enumeration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The elicitation push bound its options to a record with required members, so one option missing an id dropped the whole invocation; it is parsed leniently now, the way the permission push already was.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 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-10T20:55:39.377182Z df18f73 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

Control remote agents and answer prompts from the desktop

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Enables desktop stopping of remote agents through the server hub.
• Answers local and remote prompts exclusively on their originating transport lanes.
• Reconciles authorization, duplicate prompts, and attention across session lifecycle changes.
Diagram

sequenceDiagram
    actor U as User
    participant UI as Desktop Cards
    participant C as Permission Cache
    participant D as Local Daemon
    participant A as Session Access
    participant H as Server Hub
    participant API as Session HTTP
    participant R as Remote Agent
    A->>H: Watch and join
    R->>H: Prompt event
    H->>C: Server-lane prompt
    D->>C: Local prompt and server ID
    C-->>UI: Deduplicated cards
    U->>UI: Answer or stop
    alt Local prompt
        UI->>D: Socket response
    else Remote prompt
        UI->>API: HTTP response
        API->>R: Apply answer
    else Remote stop
        UI->>H: Stop request
        H->>R: Stop agent
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Route every prompt through the server
  • ➕ Removes dual-lane cache and correlation logic
  • ➕ Provides one response protocol
  • ➖ Makes local prompt handling depend on server availability
  • ➖ Requires broader daemon and server contract changes
  • ➖ Regresses the existing low-latency local path
2. Deduplicate by session and tool metadata
  • ➕ Avoids adding the server request ID to local IPC
  • ➕ Requires fewer daemon changes
  • ➖ Can merge unrelated simultaneous prompts
  • ➖ Can hide the only answerable card
  • ➖ Violates the fail-open correctness requirement
3. Maintain separate local and remote card stores
  • ➕ Simplifies each transport-specific cache
  • ➕ Reduces shared-cache synchronization
  • ➖ Duplicates card projection and attention logic
  • ➖ Makes correlated card identity and settlement harder to coordinate
  • ➖ Increases risk of showing duplicate prompts

Recommendation: Keep the PR's lane-scoped shared cache with explicit daemon-published correlation. It preserves the reliable local path, routes every answer through its valid transport, and fails open when correlation is unavailable; the added complexity is justified and well covered by focused lifecycle, race, wire, HTTP, and UI tests.

Files changed (100) +8094 / -499

Enhancement (59) +2254 / -177
App.axamlRegister shared pending-card templates +3/-0

Register shared pending-card templates

• Merges the reusable pending-card resource dictionary into application resources.

src/Capacitor.App/App.axaml

App.axaml.csCompose the remote session control graph +71/-16

Compose the remote session control graph

• Wires session access, server permission feeds, attention tracking, remote workspaces, lane-aware actions, and orderly disposal into application startup.

src/Capacitor.App/App.axaml.cs

AcpAnswer.csModel ACP question answers +3/-0

Model ACP question answers

• Adds an answer record carrying selected option IDs and optional free text.

src/Capacitor.App/Services/AcpAnswer.cs

AcpElicitation.csModel ACP elicitation constraints +7/-0

Model ACP elicitation constraints

• Adds the prompt, options, selection mode, and selection bounds used by ACP question cards.

src/Capacitor.App/Services/AcpElicitation.cs

AgentActionService.csRoute remote stops through the server hub +33/-4

Route remote stops through the server hub

• Adds origin-aware stop dispatch, shared in-flight gating, and user-facing handling for remote hub outcomes.

src/Capacitor.App/Services/AgentActionService.cs

AgentDirectory.csMap sessions to current agents +29/-0

Map sessions to current agents

• Publishes a replaying session-to-agent map and resolves vendors by session, preferring local rows for unproven twins.

src/Capacitor.App/Services/AgentDirectory.cs

AgentRow.csCarry session IDs on agent rows +5/-5

Carry session IDs on agent rows

• Adds the local or remote session identifier needed for prompt routing and access subscriptions.

src/Capacitor.App/Services/AgentRow.cs

HubCallOutcome.csClassify server hub invocation outcomes +13/-0

Classify server hub invocation outcomes

• Introduces explicit success, disconnected, denied, and failed results for hub operations.

src/Capacitor.App/Services/HubCallOutcome.cs

IPermissionService.csExpand the lane-aware permission API +14/-29

Expand the lane-aware permission API

• Adds lane counts and ACP answer operations while moving pending-request modeling into its own type.

src/Capacitor.App/Services/IPermissionService.cs

IServerLane.csExpose remote-control hub operations +9/-0

Expose remote-control hub operations

• Adds permission broadcasts, access-change signals, session subscription calls, and remote stop invocation.

src/Capacitor.App/Services/IServerLane.cs

InterruptReconciliation.csFold session events into pending interrupts +86/-0

Fold session events into pending interrupts

• Parses session detail events into unresolved Claude and ACP prompts while respecting resolutions and session termination.

src/Capacitor.App/Services/InterruptReconciliation.cs

NoRemoteAgents.csImplement no-server remote-control defaults +13/-0

Implement no-server remote-control defaults

• Extends the inert server lane with never-emitting streams and disconnected invocation outcomes.

src/Capacitor.App/Services/NoRemoteAgents.cs

PendingInterrupt.csRepresent reconciled session interrupts +15/-0

Represent reconciled session interrupts

• Defines interrupt kinds and identifies transcript-only questions that cannot be answered over HTTP.

src/Capacitor.App/Services/PendingInterrupt.cs

PendingPermissionRequest.csModel lane-scoped pending prompts +92/-0

Model lane-scoped pending prompts

• Introduces stable lane-qualified keys, server correlation metadata, ACP prompt data, and server reconciliation factories.

src/Capacitor.App/Services/PendingPermissionRequest.cs

PermissionLane.csIdentify prompt delivery lanes +4/-0

Identify prompt delivery lanes

• Defines local and server lanes so responses cannot cross transport boundaries.

src/Capacitor.App/Services/PermissionLane.cs

PermissionRespondedPing.csModel server settlement notifications +4/-0

Model server settlement notifications

• Represents per-request and session-wide permission response notifications.

src/Capacitor.App/Services/PermissionRespondedPing.cs

PermissionService.csUnify local and server prompts safely +242/-31

Unify local and server prompts safely

• Scopes pending requests by lane, deduplicates only proven twins, restores server cards after local loss, and dispatches answers through the originating transport.

src/Capacitor.App/Services/PermissionService.cs

ServerConnectionService.csSurface server prompt traffic and control calls +49/-0

Surface server prompt traffic and control calls

• Registers typed SignalR broadcasts and implements classified hub invokes for access, chat subscriptions, and stopping agents.

src/Capacitor.App/Services/ServerConnectionService.cs

ServerElicitationRequest.csModel server ACP elicitation pushes +7/-0

Model server ACP elicitation pushes

• Adds the session-scoped question payload delivered by SignalR.

src/Capacitor.App/Services/ServerElicitationRequest.cs

ServerPermissionFeed.csFeed server prompts into the permission cache +80/-0

Feed server prompts into the permission cache

• Combines live pushes, settlement notifications, access transitions, and session-detail reconciliation with generation-based race protection.

src/Capacitor.App/Services/ServerPermissionFeed.cs

ServerPermissionRequest.csNormalize server permission pushes +35/-0

Normalize server permission pushes

• Parses object-or-string tool input and validates optional ACP option arrays without dropping malformed pushes.

src/Capacitor.App/Services/ServerPermissionRequest.cs

ServerRespondOutcome.csClassify permission-response results +7/-0

Classify permission-response results

• Defines applied, already-settled, rejected, unauthorized, and unreachable server outcomes.

src/Capacitor.App/Services/ServerRespondOutcome.cs

ServerSessionHttp.csAdd authenticated session HTTP operations +71/-0

Add authenticated session HTTP operations

• Posts permission answers and reads session detail while mapping HTTP, authentication, network, and cancellation outcomes.

src/Capacitor.App/Services/ServerSessionHttp.cs

SessionAccessLease.csAdd reference-counted session access leases +24/-0

Add reference-counted session access leases

• Exposes replaying access state and idempotent release for shared per-session subscriptions.

src/Capacitor.App/Services/SessionAccessLease.cs

SessionAccessService.csManage the remote session access lifecycle +186/-0

Manage the remote session access lifecycle

• Owns access watches and chat joins with sharing, retries, reconnect checks, revocation handling, and stale-completion cleanup.

src/Capacitor.App/Services/SessionAccessService.cs

SessionAccessState.csDefine remote session access states +5/-0

Define remote session access states

• Distinguishes establishing, established, denied, and temporarily unavailable access.

src/Capacitor.App/Services/SessionAccessState.cs

SessionAttentionTracker.csTrack remote pending requests by session +157/-0

Track remote pending requests by session

• Reconciles request-ID sets from server pings and session details with debounce, retry, and reconnect recovery.

src/Capacitor.App/Services/SessionAttentionTracker.cs

SessionDetailFetch.csDefine session HTTP delegates and outcomes +10/-0

Define session HTTP delegates and outcomes

• Adds typed detail-fetch results plus delegates for detail reads and permission responses.

src/Capacitor.App/Services/SessionDetailFetch.cs

AcpOptionViewModel.csRepresent selectable ACP options +33/-0

Represent selectable ACP options

• Preserves option identity separately from labels and supports immediate picks or multi-selection toggles.

src/Capacitor.App/ViewModels/AcpOptionViewModel.cs

AcpQuestionCardViewModel.csAdd interactive ACP question cards +94/-0

Add interactive ACP question cards

• Supports single-select, bounded multi-select, and free-text questions with submission state and lane-specific errors.

src/Capacitor.App/ViewModels/AcpQuestionCardViewModel.cs

ISessionWorkspace.csGeneralize the main-window workspace slot +7/-0

Generalize the main-window workspace slot

• Defines the common identity and teardown contract for local and remote session workspaces.

src/Capacitor.App/ViewModels/ISessionWorkspace.cs

MainWindowViewModel.csRoute sessions to local or remote workspaces +36/-12

Route sessions to local or remote workspaces

• Selects workspace type from agent origin and avoids opening stale, missing, or duplicate sessions.

src/Capacitor.App/ViewModels/MainWindowViewModel.cs

PendingCardViewModel.csAdd lane keys and shared error messages +12/-0

Add lane keys and shared error messages

• Exposes stable cache keys and centralizes transport-specific answer failure text.

src/Capacitor.App/ViewModels/PendingCardViewModel.cs

PendingCardsViewModel.csShare pending cards across session hosts +64/-0

Share pending cards across session hosts

• Filters and projects one agent's prompt cache into sorted Claude, ACP, and permission cards on the UI thread.

src/Capacitor.App/ViewModels/PendingCardsViewModel.cs

PermissionCardViewModel.csSupport option-bearing ACP permissions +14/-4

Support option-bearing ACP permissions

• Renders agent-defined options, submits selected option IDs, and applies shared server-aware error messages.

src/Capacitor.App/ViewModels/PermissionCardViewModel.cs

RailRepoViewModel.csPropagate remote stale state through repositories +3/-2

Propagate remote stale state through repositories

• Passes lane staleness and in-app remote opening callbacks into nested worktree rows.

src/Capacitor.App/ViewModels/RailRepoViewModel.cs

RailSessionViewModel.csOpen and dim remote session rows +11/-4

Open and dim remote session rows

• Routes remote rows into the desktop workspace and exposes stale-lane state for visual dimming.

src/Capacitor.App/ViewModels/RailSessionViewModel.cs

RailWorktreeViewModel.csPropagate remote session behavior through worktrees +2/-2

Propagate remote session behavior through worktrees

• Threads stale state and remote workspace callbacks into session view models.

src/Capacitor.App/ViewModels/RailWorktreeViewModel.cs

RemoteSessionViewModel.csHost remote session controls and cards +147/-0

Host remote session controls and cards

• Manages session access leases, prompt visibility, live row lifecycle, remote stop, and web-opening actions.

src/Capacitor.App/ViewModels/RemoteSessionViewModel.cs

SessionRailViewModel.csWire remote sessions into rail navigation +5/-2

Wire remote sessions into rail navigation

• Marshals server staleness to nested rows and replaces web-only remote opening with desktop navigation.

src/Capacitor.App/ViewModels/SessionRailViewModel.cs

TrayModels.csRepresent remote attention in the tray +11/-4

Represent remote attention in the tray

• Adds agent origin and remote session attention entries to tray projections.

src/Capacitor.App/ViewModels/TrayModels.cs

TrayViewModel.csSurface actionable remote attention in the tray +68/-40

Surface actionable remote attention in the tray

• Gates attention by the relevant lane, lists remote waiting sessions, and routes their stop and web actions correctly.

src/Capacitor.App/ViewModels/TrayViewModel.cs

WorkspaceViewModel.csProvide card panes without terminals +5/-6

Provide card panes without terminals

• Implements the shared workspace interface and creates chat/card hosting for every resolved local session.

src/Capacitor.App/ViewModels/WorkspaceViewModel.cs

Converters.csAdd stale-row opacity conversion +12/-0

Add stale-row opacity conversion

• Adds a converter that dims disconnected remote rows without hiding them.

src/Capacitor.App/Views/Converters.cs

MainWindow.axamlRender local and remote workspace types +5/-2

Render local and remote workspace types

• Adds a remote-session template alongside the existing local workspace template.

src/Capacitor.App/Views/MainWindow.axaml

PendingCardTemplates.axamlCentralize pending-card presentation +278/-0

Centralize pending-card presentation

• Defines reusable templates for permissions, Claude questions, and ACP questions across local and remote hosts.

src/Capacitor.App/Views/PendingCardTemplates.axaml

RemoteSessionView.axamlAdd the remote session card host +56/-0

Add the remote session card host

• Displays remote session status, access messages, prompt cards, stop, and web actions.

src/Capacitor.App/Views/RemoteSessionView.axaml

RemoteSessionView.axaml.csInitialize the remote session view +11/-0

Initialize the remote session view

• Adds the code-behind required to load the remote session control.

src/Capacitor.App/Views/RemoteSessionView.axaml.cs

SessionRailView.axamlDim stale remote rail rows +1/-0

Dim stale remote rail rows

• Binds session-row opacity to the remote lane's stale state.

src/Capacitor.App/Views/SessionRailView.axaml

WorkspaceView.axamlShow card panes for non-terminal sessions +4/-13

Show card panes for non-terminal sessions

• Makes chat/card visibility independent of terminal availability.

src/Capacitor.App/Views/WorkspaceView.axaml

PermissionIpc.csCarry server correlation IDs over local IPC +4/-1

Carry server correlation IDs over local IPC

• Adds a backward-compatible nullable server request ID to local pending-permission frames.

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

LocalPermissionBridge.csPublish permission request correlation +2/-0

Publish permission request correlation

• Associates the local request with its server request ID as soon as the server leg starts.

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

PermissionPromptBroker.csRebroadcast correlated pending prompts +12/-0

Rebroadcast correlated pending prompts

• Updates still-pending entries with server IDs and replays the mapping to current and future subscribers.

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

AcpInteractionOption.csAdd the ACP option wire model +16/-0

Add the ACP option wire model

• Defines identity, display, behavior, and selection-bound fields with explicit JSON names.

src/Capacitor.Remote.Models/AcpInteractionOption.cs

PermissionResponsePayload.csAdd the permission response wire payload +24/-0

Add the permission response wire payload

• Models permission, edited-input, option-selection, and free-text responses with omitted unset members.

src/Capacitor.Remote.Models/PermissionResponsePayload.cs

RemoteModelsJsonContext.csRegister new remote wire types +5/-0

Register new remote wire types

• Adds source-generated JSON metadata for ACP, response, and session-detail records.

src/Capacitor.Remote.Models/RemoteModelsJsonContext.cs

RemoteWire.csAdd access tokens and response behaviors +11/-0

Add access tokens and response behaviors

• Defines session visibility diagnostics and canonical permission-response behavior values.

src/Capacitor.Remote.Models/RemoteWire.cs

SessionDetailDto.csModel session detail responses +12/-0

Model session detail responses

• Adds the session identity, termination, event cursor, and event collection used for reconciliation.

src/Capacitor.Remote.Models/SessionDetailDto.cs

SessionEventDto.csModel canonical session events +15/-0

Model canonical session events

• Supports both payload and data bodies while excluding the derived body from serialization.

src/Capacitor.Remote.Models/SessionEventDto.cs

Bug fix (1) +2 / -1
MainWindow.axaml.csLimit PR foreground handling to local workspaces +2/-1

Limit PR foreground handling to local workspaces

• Avoids treating remote card hosts as local workspaces with pull-request readers.

src/Capacitor.App/Views/MainWindow.axaml.cs

Refactor (3) +13 / -246
ChatTabViewModel.csExtract the shared pending-card pipeline +9/-35

Extract the shared pending-card pipeline

• Delegates card projection to PendingCardsViewModel and keys transcript withdrawal tracking by lane-qualified request keys.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs

QuestionCardViewModel.csUse shared prompt failure messages +1/-2

Use shared prompt failure messages

• Adopts the common local and server transport error mapping for Claude questions.

src/Capacitor.App/ViewModels/QuestionCardViewModel.cs

ChatTabView.axamlConsume shared prompt card templates +3/-209

Consume shared prompt card templates

• Removes duplicated inline templates and references the application-wide permission and question templates.

src/Capacitor.App/Views/ChatTabView.axaml

Tests (34) +2076 / -75
AcpQuestionCardViewModelTests.csTest ACP question interaction behavior +89/-0

Test ACP question interaction behavior

• Covers duplicate labels, single and multi-selection, free text, transport errors, and disposal safety.

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

AgentActionServiceTests.csTest remote stop routing and outcomes +90/-2

Test remote stop routing and outcomes

• Verifies hub-only dispatch, notifications, missing connections, denials, failures, and duplicate-stop suppression.

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

AgentDirectoryTests.csTest session-to-agent directory mapping +27/-4

Test session-to-agent directory mapping

• Covers session IDs, remote rows, vendor lookup, and local precedence for ambiguous twins.

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

AgentRowTests.csTest session ID projection onto rows +25/-0

Test session ID projection onto rows

• Ensures both local and remote row factories retain their source session IDs.

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

FakeAgentDirectory.csAdd an editable agent directory fake +32/-0

Add an editable agent directory fake

• Provides test-controlled rows, staleness, session mapping, and vendor lookup.

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

FakePermissionService.csExtend the permission service fake +58/-17

Extend the permission service fake

• Uses lane-qualified keys and scripts ACP answers, option picks, summaries, and conclusive cache eviction.

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

FakeServerLane.csExtend the server lane fake +52/-0

Extend the server lane fake

• Adds remote-control streams, scripted invokes, and thread-safe immutable call logs.

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

FakeServerLaneTests.csTest concurrent fake lane logging +38/-0

Test concurrent fake lane logging

• Verifies hub-call logs remain complete and enumerable under concurrent readers and writers.

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

HomeViewModelTests.csUpdate the home directory test double +6/-0

Update the home directory test double

• Implements the new session mapping and vendor lookup interface members.

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

HubTestHost.csSupport remote-control hub tests +31/-0

Support remote-control hub tests

• Adds test hub methods, visibility handlers, and call tracking for stop, watch, join, and leave operations.

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

InterruptReconciliationTests.csTest session interrupt reconciliation +77/-0

Test session interrupt reconciliation

• Covers event folding, ACP classification, transcript-only questions, alternate bodies, casing, and session termination.

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

MainWindowViewModelTests.csTest origin-aware workspace routing +122/-2

Test origin-aware workspace routing

• Verifies remote host selection and teardown while preserving local routing and rejecting missing rows.

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

PendingCardsViewModelTests.csTest the shared card projection +55/-0

Test the shared card projection

• Covers per-agent filtering, lane coexistence, card type selection, empty state, and instance-preserving refreshes.

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

PermissionCardViewModelTests.csTest ACP permission option selection +17/-0

Test ACP permission option selection

• Verifies option-bearing permissions hide generic actions and submit the selected option ID.

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

PermissionServiceTests.csTest lane-scoped permission semantics +230/-13

Test lane-scoped permission semantics

• Extensively covers correlation, shadowing, lane loss, settlements, generations, HTTP answers, ACP payloads, and session mapping.

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

RailSessionViewModelTests.csTest remote rail navigation and staleness +42/-17

Test remote rail navigation and staleness

• Updates constructors and verifies remote opening, machine badges, attention, and stale-row behavior.

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

RailWorktreeViewModelTests.csUpdate worktree tests for stale state +3/-1

Update worktree tests for stale state

• Supplies the new remote staleness observable to worktree fixtures.

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

RemoteAgentsServiceHttpFetchTests.csTest authenticated remote-agent fetching +31/-0

Test authenticated remote-agent fetching

• Uses WireMock to verify agent deserialization and unauthorized response handling.

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

RemoteSessionViewModelTests.csTest the remote session lifecycle +145/-0

Test the remote session lifecycle

• Covers access establishment, revocation, reconnects, late session IDs, terminal rows, lease movement, and card visibility.

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

RemoteSessionViewSmokeTests.csSmoke-test the remote card host +121/-0

Smoke-test the remote card host

• Renders shared ACP templates headlessly and verifies access denial replaces cards with an explanatory banner.

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

ServerConnectionServiceTests.csTest remote-control SignalR contracts +91/-0

Test remote-control SignalR contracts

• Covers typed broadcasts, lenient input parsing, malformed options, hub invocation, denial classification, and disconnected behavior.

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

ServerPermissionFeedTests.csTest server prompt ingestion and reconciliation +135/-0

Test server prompt ingestion and reconciliation

• Covers live pushes, settlements, reconciliation races, denials, failed fetches, ended sessions, and identity changes.

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

ServerSessionHttpTests.csTest session HTTP seams +89/-0

Test session HTTP seams

• Uses authenticated WireMock requests to verify response status mapping, cancellation, detail parsing, and missing sessions.

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

SessionAccessServiceTests.csTest session access ownership and recovery +151/-0

Test session access ownership and recovery

• Covers ordered watch/join calls, denial, retries, reconnects, shared leases, disposal races, diagnostics, and cleanup.

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

SessionAttentionTrackerTests.csTest request-set-based remote attention +101/-0

Test request-set-based remote attention

• Verifies multi-request clearing, dirty-state persistence, reconnect reconciliation, retries, and unknown settlement recovery.

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

TrayViewModelTests.csTest remote tray attention and actions +80/-0

Test remote tray attention and actions

• Covers lane-gated state, remote waiting entries, server prompt attention, and profile-correct web links.

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

WorkspaceFixtures.csAdd asynchronous polling support +8/-0

Add asynchronous polling support

• Adds a task-returning condition overload for lifecycle and access tests.

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

WorkspaceNavigationTests.csAdapt navigation tests to workspace abstraction +1/-1

Adapt navigation tests to workspace abstraction

• Casts the generalized workspace slot when accessing local terminal test hooks.

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

WorkspaceViewModelTests.csTest card hosting without a terminal +20/-5

Test card hosting without a terminal

• Verifies chat/card panes are created on the first agent DTO even when no transcript projection or terminal exists.

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

WorkspaceViewSmokeTests.csUpdate non-terminal workspace rendering expectations +6/-13

Update non-terminal workspace rendering expectations

• Confirms sessions without a PTY still display their chat/card surface and end-state banner.

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

PermissionWireContractsTests.csTest local correlation wire compatibility +18/-0

Test local correlation wire compatibility

• Verifies the nullable server request ID is emitted and remains backward-compatible when absent.

test/Capacitor.Cli.Core.Tests.Unit/LocalIpc/PermissionWireContractsTests.cs

LocalPermissionBridgeInteractiveTests.csTest daemon publication of server request IDs +22/-0

Test daemon publication of server request IDs

• Ensures local subscribers receive correlation updates before the server decision resolves.

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

PermissionPromptBrokerTests.csTest pending prompt correlation replay +31/-0

Test pending prompt correlation replay

• Verifies correlation updates reach current and late subscribers but never revive settled prompts.

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

PermissionWireTests.csTest remote permission wire records +32/-0

Test remote permission wire records

• Covers snake-case response serialization, ACP option bounds, and session event detail deserialization.

test/Capacitor.Remote.Models.Tests.Unit/PermissionWireTests.cs

Documentation (2) +3748 / -0
CHANGES.mdDocument remote-control invariants +30/-0

Document remote-control invariants

• Documents lane-authoritative responses, correlation-only deduplication, and request-set-based remote attention, including the remaining cold-start limitation.

docs/CHANGES.md

2026-09-10-desktop-remote-daemons-slice2.mdAdd the remote-control implementation plan +3718/-0

Add the remote-control implementation plan

• Provides the detailed architecture, contracts, implementation tasks, test strategy, and verification steps for this slice.

docs/superpowers/plans/2026-09-10-desktop-remote-daemons-slice2.md

Other (1) +1 / -0
Capacitor.App.Tests.Unit.csprojAdd WireMock test support +1/-0

Add WireMock test support

• References WireMock.Net for authenticated HTTP integration tests.

test/Capacitor.App.Tests.Unit/Capacitor.App.Tests.Unit.csproj

@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: df18f735b1

ℹ️ 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".

Comment on lines +338 to +342
internal void DropServerForSession(string sessionId) {
lock (_lock) {
if (_disposed) return;
foreach (var item in _cache.Items.Where(i => i.Lane == PermissionLane.Server && i.SessionId == sessionId).ToList()) _cache.Remove(item.Key);
foreach (var key in _shadowed.Where(kv => kv.Value.SessionId == sessionId).Select(kv => kv.Key).ToList()) _shadowed.Remove(key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Invalidate reconciliations when session access is denied

When a SessionAccessChanged recheck denies access while an earlier Established reconciliation is still fetching details, this method removes the cards without advancing _sessionGenerations. The delayed fetch can therefore pass the generation check in ReplaceServerForSession and restore the revoked session's prompts, including their tool input, after the UI has entered the access-lost state. Increment the session generation as part of this drop so every pre-denial fetch is discarded.

Useful? React with 👍 / 👎.

Comment on lines +328 to +330
var keep = items.Select(i => i.Key).ToHashSet(StringComparer.Ordinal);
foreach (var stale in _cache.Items.Where(i => i.Lane == PermissionLane.Server && i.SessionId == sessionId && !keep.Contains(i.Key)).ToList())
_cache.Remove(stale.Key);

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 live requests that arrive during reconciliation

If a PermissionRequested push arrives after the detail endpoint has taken its snapshot but before ReconcileAsync applies that snapshot, the pushed item is absent from keep and is removed here. The org-wide PermissionPending ping only drives SessionAttentionTracker, so no subsequent event is guaranteed to restore the card, leaving a genuinely pending prompt unavailable in the desktop app. The replacement needs an event/version boundary or must retain live upserts newer than the fetched snapshot.

Useful? React with 👍 / 👎.

Comment on lines +91 to +94
foreach (var change in changes) {
if (change.Key != row.Key) continue;
if (change.Reason == ChangeReason.Remove) { SessionEnded = true; Release(); continue; }
Apply(change.Current);

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 Follow origin changes instead of ending the remote workspace

When the local daemon reconnects and AgentDirectory proves that this remote row is its twin, the directory removes remote:{id} and exposes local:{id} even though the logical agent is still running. Treating that removal as an ended session leaves the open pane falsely ended; clicking the new local rail row cannot recover it because MainWindowViewModel.OpenSession returns early for the same agent ID. The workspace must distinguish an origin transition from a logical-agent removal and switch to the winning row.

Useful? React with 👍 / 👎.

@qodo-code-review

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

Copy link
Copy Markdown

Code Review by Qodo

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

⚠️ 14 lower-priority findings omitted to fit the comment size limit; re-run the review or view the findings in the Qodo portal.

Grey Divider


Action required

1. Answered prompts reappear after a drop ✓ Resolved 🐞 Bug ☼ Reliability
Description
SendResolveAsync calls ConcludeLocal with only the local request ID, so settlement can no longer
discover the correlated server ID after DropLocalLane removes that local cache entry. If the
independent permission subscription drops while the one-shot resolve call is awaiting its successful
acknowledgment, the restored server twin remains visible and answerable even though the daemon
already settled it.
Code

src/Capacitor.App/Services/PermissionService.cs[140]

+        ConcludeLocal(target.RequestId);
Relevance

●●● Strong

Concrete asynchronous cache race can resurrect answered prompts, directly threatening the PR’s
stated settlement invariant.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Resolve and subscription operations use separate local sockets, allowing stream loss to restore the
server twin before the resolve acknowledgment returns. ConcludeLocal then tombstones only the
local key because its sole server-ID lookup is through the now-absent cache item.

src/Capacitor.App/Services/PermissionService.cs[129-142]
src/Capacitor.App/Services/PermissionService.cs[173-180]
src/Capacitor.App/Services/PermissionService.cs[199-220]
src/Capacitor.App/Services/PermissionService.cs[259-280]
src/Capacitor.Cli.Core/LocalIpc/LocalControlOps.cs[120-132]
src/Capacitor.Cli.Core/LocalIpc/PermissionSubscription.cs[20-44]
src/Capacitor.Cli.Daemon/Services/PermissionIpc.cs[53-67]

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 local answer does not retire its correlated server twin when the local subscription disappears while the resolve request is in flight. `ConcludeLocal` tries to recover the server ID from a cache entry that `DropLocalLane` has already removed.

## Fix Focus Areas
- src/Capacitor.App/Services/PermissionService.cs[129-140]
- src/Capacitor.App/Services/PermissionService.cs[259-280]

## Recommended Fix
Pass the submitted target's `ServerRequestId` into local conclusion and tombstone that server key even when the local cache lookup no longer succeeds. Retain cache lookup as the fallback for stream-driven settlements, and add a test where lane loss restores the twin before the local acknowledgment arrives.

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


2. Remote tray stops target local agents ✓ Resolved 🐞 Bug ≡ Correctness
Description
StopAgentCommand finds the current tray entry by Id alone even though remote attention entries
are appended alongside local entries and the native menu passes only that ID. With a local entry
before a remote same-ID entry, choosing Stop from the remote submenu obtains the local origin and
sends the local daemon stop instead.
Code

src/Capacitor.App/ViewModels/TrayViewModel.cs[R99-102]

        StopAgentCommand = ReactiveCommand.Create<string>(id => {
            var entry = MenuModel.Agents.FirstOrDefault(a => a.Id == id);
-            actions.RequestStop(id, entry?.Label ?? id, entry?.Kind ?? "");
+            actions.RequestStop(id, entry?.Label ?? id, entry?.Kind ?? "", entry?.Origin ?? AgentOrigin.Local);
+        });
Relevance

●●● Strong

Recent accepted session-action findings support preserving origin-specific routing instead of
ID-only dispatch.

PR-#790

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The directory model permits same logical IDs on its two sources. Remote tray entries are added after
local entries, but the menu adapter supplies just entry.Id; the command subsequently takes the
first matching entry, which is the local one in that ordering.

src/Capacitor.App/Services/AgentDirectory.cs[22-27]
src/Capacitor.App/ViewModels/TrayViewModel.cs[99-108]
src/Capacitor.App/ViewModels/TrayViewModel.cs[356-369]
src/Capacitor.App/Views/TrayMenuBuilder.cs[46-54]

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 tray menu command parameter contains only an agent ID, so a click cannot reliably recover whether the selected entry was local or remote.

Fix Focus Areas
- src/Capacitor.App/ViewModels/TrayViewModel.cs[99-108]
- src/Capacitor.App/ViewModels/TrayModels.cs[64-78]

Recommended Fix
Make tray commands receive a source-scoped entry identity, such as the full `TrayAgentEntry` or a row key containing both origin and ID. Have the menu builder pass that value as the command parameter, then dispatch Stop and Open in web from its explicit origin without searching `MenuModel.Agents` by ID.

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


3. Remote sessions stop receiving prompts ✓ Resolved 🐞 Bug ☼ Reliability
Description
EstablishAsync lets a superseded attempt issue UnsubscribeFromChatAsync after its own chat join
succeeds, without coordinating that cleanup with a newer attempt's join. An access-change ping or
reconnect can start the newer attempt while the old call is in flight, so the delayed unsubscribe
removes the active session group membership while the lease remains established.
Code

src/Capacitor.App/Services/SessionAccessService.cs[R148-150]

+        if (report is not null) Console.Error.WriteLine(report);
+        if (unsubscribeStale) _ = _lane.UnsubscribeFromChatAsync(entry.SessionId, CancellationToken.None);
+    }
Relevance

●●● Strong

Recent accepted concurrency findings address stale asynchronous cleanup and lifecycle races.

PR-#831
PR-#730

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Access-change notifications immediately supersede the entry's attempt, while each attempt
independently awaits watch then chat registration. A stale completion is prevented from publishing
state, but still schedules an unconditional unsubscribe outside the lock; the hub operation targets
the current live connection rather than the old attempt, allowing it to remove the newer
subscription.

src/Capacitor.App/Services/SessionAccessService.cs[41-45]
src/Capacitor.App/Services/SessionAccessService.cs[96-123]
src/Capacitor.App/Services/SessionAccessService.cs[131-150]
src/Capacitor.App/Services/ServerConnectionService.cs[270-289]

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 stale establishment attempt can unsubscribe the chat group after a newer attempt has successfully subscribed to it. This leaves the session access state established but stops permission and chat pushes for that session.

## Fix Focus Areas
- src/Capacitor.App/Services/SessionAccessService.cs[96-149]

## Recommended Fix
Serialize join and leave operations for each session, including stale-attempt cleanup and lease release, so a stale unsubscribe cannot run after a newer chat subscribe. Alternatively, after a required stale cleanup completes, re-establish the still-current entry through the same serialized operation path before reporting it established.

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


View high (9)
4. Revoked sessions regain answer cards ✓ Resolved 🐞 Bug ⛨ Security
Description
DropServerForSession removes cards without changing the session generation that an already-running
reconciliation captured. If access is denied while that detail read is in flight,
ReplaceServerForSession accepts the old generation afterward and recreates cards for the revoked
session.
Code

src/Capacitor.App/Services/PermissionService.cs[R338-342]

+    internal void DropServerForSession(string sessionId) {
+        lock (_lock) {
+            if (_disposed) return;
+            foreach (var item in _cache.Items.Where(i => i.Lane == PermissionLane.Server && i.SessionId == sessionId).ToList()) _cache.Remove(item.Key);
+            foreach (var key in _shadowed.Where(kv => kv.Value.SessionId == sessionId).Select(kv => kv.Key).ToList()) _shadowed.Remove(key);
Relevance

●●● Strong

Revocation must invalidate in-flight reconciliation; otherwise security-sensitive prompt cards can
reappear.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Established reconciliation and denied cleanup are separately fire-and-forget scheduled. The cache
accepts a result based only on generation, and the denial path leaves that generation unchanged.

src/Capacitor.App/Services/ServerPermissionFeed.cs[41-46]
src/Capacitor.App/Services/ServerPermissionFeed.cs[50-66]
src/Capacitor.App/Services/PermissionService.cs[325-343]
src/Capacitor.App/Services/SessionAccessService.cs[131-145]

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 in-flight reconciliation can repopulate server permission cards after the session access transition has denied access.

Fix Focus Areas
- src/Capacitor.App/Services/PermissionService.cs[325-343]
- src/Capacitor.App/Services/ServerPermissionFeed.cs[41-46]

Recommended Fix
Increment the session generation before removing cards in DropServerForSession, so any reconciliation started before revocation is rejected. Preserve the no-tombstone behavior so a later legitimate re-establishment can reconcile the session again.

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


5. Shared viewers lose pending prompts 🔗 Cross-repo conflict ≡ Correctness
Description
SendServerAsync treats every response-route 404 as an already-decided request and removes the
card, although kcap-server also returns 404 when a Full-access viewer is not the session owner. When
a non-owner opens a Full-shared session, the server delivers permission and elicitation pushes to
that viewer, but answering one removes the desktop card while leaving the underlying agent request
pending.
Code

src/Capacitor.App/Services/PermissionService.cs[R150-152]

+            case ServerRespondKind.NotPending:
+                lock (_lock) { if (!_disposed) ConcludeServerKey(target.RequestId); }
+                return new(PermissionResolveKind.AlreadyDecided, null);
Relevance

●●● Strong

Concrete shared-viewer authorization mismatch can hide still-pending prompts, a core feature
correctness issue.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR inserts server-delivered prompts into the answerable cache and removes them whenever the
response seam reports NotPending; its HTTP adapter maps all 404 responses to that result.
kcap-server permits any viewer whose resolved sharing level is Full to subscribe to prompt delivery,
while its response handler separately requires ownership and returns 404 for every non-owner,
proving that a delivered request can be rejected without having been settled.

src/Capacitor.App/Services/ServerPermissionFeed.cs[30-35]
src/Capacitor.App/Services/ServerSessionHttp.cs[25-30]
src/Capacitor.App/Services/PermissionService.cs[144-152]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/Sessions/VisibilityService.cs [138-149]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/Sessions/CapacitorHub.cs [5716-5725]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/Sessions/SessionHookHandlers.cs [2403-2405]

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

## Issue description
Full-access non-owner viewers can subscribe to session prompts, but kcap-server's permission-response endpoint only accepts the session owner and disguises that refusal as 404. The desktop interprets 404 as an already-settled request and removes the card even though the agent remains blocked.

## Fix Focus Areas
- src/Capacitor.App/Services/PermissionService.cs[144-152]
- src/Capacitor.App/Services/ServerPermissionFeed.cs[30-35]
- /cross_repos/kcap-server/src/Capacitor.Server/Sessions/SessionHookHandlers.cs[2403-2405]
- /cross_repos/kcap-server/src/Capacitor.Server/Sessions/CapacitorHub.cs[5716-5725]

## Recommended Fix
Coordinate the authorization contract between the repositories. Either allow Full-access viewers who receive prompt payloads to use the response endpoint, or restrict prompt subscription and delivery to owners and expose that denial to the desktop; also ensure an authorization refusal is distinguishable from a genuinely settled request so the desktop does not discard a still-pending card.

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


6. New server prompts disappear after delivery ✓ Resolved 🐞 Bug ≡ Correctness
Description
ReplaceServerForSession removes every server item omitted from a fetched snapshot, while
UpsertServer does not advance that session's generation. A live permission push that arrives after
an older reconciliation request starts but before its result is applied is therefore removed as
stale, leaving the user without the prompt card.
Code

src/Capacitor.App/Services/PermissionService.cs[R327-330]

+            if (_disposed || generation != SessionGeneration(sessionId)) return;
+            var keep = items.Select(i => i.Key).ToHashSet(StringComparer.Ordinal);
+            foreach (var stale in _cache.Items.Where(i => i.Lane == PermissionLane.Server && i.SessionId == sessionId && !keep.Contains(i.Key)).ToList())
+                _cache.Remove(stale.Key);
Relevance

●●● Strong

Stale reconciliation deleting live pushes is a concrete race against the PR’s prompt-delivery
invariant.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Live pushes and reconciliations execute independently, but the generation check only changes for a
session-wide settlement. Consequently, an old reconciliation result can delete a live item it did
not contain.

src/Capacitor.App/Services/ServerPermissionFeed.cs[30-37]
src/Capacitor.App/Services/ServerPermissionFeed.cs[50-66]
src/Capacitor.App/Services/PermissionService.cs[292-299]
src/Capacitor.App/Services/PermissionService.cs[315-333]

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 reconciliation snapshot can overwrite a newer live permission or elicitation push because live upserts do not invalidate the fetch generation.

Fix Focus Areas
- src/Capacitor.App/Services/PermissionService.cs[292-334]
- src/Capacitor.App/Services/ServerPermissionFeed.cs[50-66]

Recommended Fix
Track a per-session revision for server-lane mutations and capture it before reconciliation. Reject a reconciliation result if a live upsert, settlement, access drop, or other server-lane mutation for that session occurred after the fetch began; alternatively merge the reconciliation result without deleting items added after its captured revision.

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


7. Remote rows open the local session 🐞 Bug ≡ Correctness
Description
RailSessionViewModel.OpenCommand passes only row.Id, and the production callbacks both invoke
MainWindowViewModel.OpenSession, which resolves that ID local-first. When the directory retains
both source-scoped rows for an unproven same-ID pair, clicking the remote row opens the local
terminal workspace and both rows can appear selected.
Code

src/Capacitor.App/ViewModels/RailSessionViewModel.cs[74]

+        OpenCommand = ReactiveCommand.Create(() => (IsRemote ? openRemote : openLocal)(row.Id));
Relevance

●●● Strong

Recent accepted session-identity findings support preventing misleading action routing for protected
or mismatched rows.

PR-#790

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The directory explicitly keeps local and remote rows independently for an unproven same-ID pairing.
The rail routes a remote row through a callback that only accepts the ID, while production wires
both callbacks to the same ID-only method and its origin resolver checks local before remote.

src/Capacitor.App/Services/AgentDirectory.cs[22-27]
src/Capacitor.App/ViewModels/RailSessionViewModel.cs[59-75]
src/Capacitor.App/App.axaml.cs[1048-1052]
src/Capacitor.App/App.axaml.cs[589-594]
src/Capacitor.App/ViewModels/MainWindowViewModel.cs[435-444]

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

Issue description
Remote rail actions discard the row's source identity before workspace routing, so a local row wins whenever local and remote rows share an ID.

Fix Focus Areas
- src/Capacitor.App/ViewModels/RailSessionViewModel.cs[74-74]
- src/Capacitor.App/ViewModels/SessionRailViewModel.cs[93-96]
- src/Capacitor.App/ViewModels/MainWindowViewModel.cs[429-444]

Recommended Fix
Pass a source-scoped row key or an explicit `AgentOrigin` through the rail open callback and make `MainWindowViewModel` select the corresponding workspace directly. Store selection using the same source-scoped identity so only the opened row is highlighted.

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


8. Remote model tests fail to build 🐞 Bug ≡ Correctness
Description
Session_detail_reads_events_with_payload_and_data dereferences the nullable
SessionDetailDto.Events property without a null-forgiving operator on its final assertion.
Nullable analysis emits CS8602 for this expression, and the repository promotes warnings to errors,
so the Remote.Models unit-test project cannot compile.
Code

test/Capacitor.Remote.Models.Tests.Unit/PermissionWireTests.cs[30]

+        await Assert.That(detail.Events[0].Payload!.Value.GetProperty("kind").GetString()).IsEqualTo("permission");
Relevance

●●● Strong

Nullable dereference under warnings-as-errors is a deterministic build failure requiring correction.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The model declares Events nullable, while the newly added assertion accesses it without null
suppression. Repository-wide nullable warnings are errors.

src/Capacitor.Remote.Models/SessionDetailDto.cs[7-11]
test/Capacitor.Remote.Models.Tests.Unit/PermissionWireTests.cs[24-30]
Directory.Build.props[2-10]

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

Issue description
`SessionDetailDto.Events` is declared nullable, but the final assertion in the new contract test dereferences it without null suppression. With nullable warnings treated as errors, this produces CS8602 and prevents the test project from building.

Fix Focus Areas
- test/Capacitor.Remote.Models.Tests.Unit/PermissionWireTests[29-30]

Recommended Fix
Use `detail.Events![0]` in the final assertion as well, or assign `detail.Events!` to a local non-null variable before both assertions.

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


9. A new account sees old session prompts ✓ Resolved 🐞 Bug ⛨ Security
Description
The subject-change handler calls ClearServerLane, but that clear does not invalidate
reconciliations started under the previous subject. When an old authenticated detail request
completes after another subject connects, its unchanged generation permits ReplaceServerForSession
to restore the prior account's prompt cards.
Code

src/Capacitor.App/Services/ServerPermissionFeed.cs[R25-28]

+        lane.Status.Where(s => s.State == ServerLaneState.Connected && s.Subject is not null)
+            .Subscribe(s => {
+                if (_subject is not null && _subject != s.Subject) permissions.ClearServerLane();
+                _subject = s.Subject;
Relevance

●●● Strong

Subject changes must invalidate old responses to prevent cross-account prompt leakage, a direct
security issue.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Changing connected subjects clears current cache entries only. Since the in-flight fetch uses a
lifetime-wide token and replacement validates only an unchanged per-session generation, a response
authorized for the old subject can be inserted after the clear.

src/Capacitor.App/Services/ServerPermissionFeed.cs[25-29]
src/Capacitor.App/Services/ServerPermissionFeed.cs[50-66]
src/Capacitor.App/Services/PermissionService.cs[325-351]

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 reconciliation started while one authenticated subject was connected can restore its session cards after the app clears the server lane for a different subject.

Fix Focus Areas
- src/Capacitor.App/Services/ServerPermissionFeed.cs[25-29]
- src/Capacitor.App/Services/PermissionService.cs[325-351]

Recommended Fix
Make ClearServerLane invalidate every outstanding server reconciliation, such as by advancing a global server-lane epoch included in each reconciliation token or advancing all tracked session generations. Require ReplaceServerForSession to validate that epoch before it removes or inserts items.

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


10. One stop can block a different agent ✓ Resolved 🐞 Bug ≡ Correctness
Description
AgentActionService.RequestStop keys _inFlight solely by agentId before deciding whether to run
the local or remote stop path. When source-scoped local and remote rows have that same ID but are
distinct agents, starting either operation silently drops the other request and disables both
controls until the first call completes.
Code

src/Capacitor.App/Services/AgentActionService.cs[R82-88]

+    public void RequestStop(string agentId, string label, string kind, AgentOrigin origin = AgentOrigin.Local) {
        lock (_lock) {
            if (_inFlight.Contains(agentId)) return;
            _inFlight = _inFlight.Add(agentId);
            _stopsInFlight.OnNext(_inFlight);
        }
-        _ = Task.Run(() => RunStopAsync(agentId, label, kind));
+        _ = Task.Run(() => origin == AgentOrigin.Remote ? RunRemoteStopAsync(agentId, label) : RunStopAsync(agentId, label, kind));
Relevance

●●● Strong

Clear source-scoping correctness bug directly conflicts with the PR’s local and remote control
intent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The directory's contract distinguishes rows by source because same IDs can coexist, whereas the
newly shared local/remote stop gate stores only the ID and only dispatches by origin after admission
to that gate. The UI stop controls also consume this ID-only in-flight set.

src/Capacitor.App/Services/AgentDirectory.cs[22-29]
src/Capacitor.App/Services/AgentActionService.cs[77-88]
src/Capacitor.App/Services/AgentActionService.cs[108-112]
src/Capacitor.App/ViewModels/RemoteSessionViewModel.cs[99-104]
src/Capacitor.App/ViewModels/WorkspaceViewModel.cs[150-161]

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 stop in-flight set treats local and remote agents with the same logical ID as one operation even though the directory retains them as separate source-scoped rows.

Fix Focus Areas
- src/Capacitor.App/Services/AgentActionService.cs[82-88]
- src/Capacitor.App/Services/AgentActionService.cs[108-112]
- src/Capacitor.App/ViewModels/TrayModels.cs[64-69]

Recommended Fix
Use a source-scoped stop key composed from `AgentOrigin` and agent ID for insertion, removal, and control enablement. Thread the same key through local and remote workspace and tray projections so only the command actually in progress is gated.

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


11. Some new prompts never reach the desktop ✓ Resolved 🐞 Bug ☼ Reliability
Description
BuildDaemonGraph starts serverLane before constructing SessionAttentionTracker and
ServerPermissionFeed, although their permission observables are non-replaying subjects. If the
connection completes and a pending-interrupt broadcast arrives during that gap, the tracker never
marks the session dirty and the feed never receives the request, leaving cold-start attention and
cards absent until another event occurs.
Code

src/Capacitor.App/App.axaml.cs[R527-530]

+        var sessionAccess = new SessionAccessService(serverLane, TimeProvider.System);
+        var permissionFeed = new ServerPermissionFeed(
+            serverLane, sessionAccess, permissions, readDetail, directory.VendorOfSession, TimeProvider.System);
+        var attention = new SessionAttentionTracker(serverLane, readDetail, TimeProvider.System);
Relevance

●●● Strong

Recent accepted precedents favor fixing startup races and missed observable events affecting UI
state.

PR-#831
PR-#856

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
serverLane.Start() launches its connection loop before the PR constructs the two consumers.
Incoming hub broadcasts synchronously call OnNext on plain Subject instances, which retain no
state for subscribers installed later. SessionAttentionTracker only schedules its session-detail
reconciliation after receiving PermissionPending, so missing that event means there is no fallback
work to discover the pending request.

src/Capacitor.App/App.axaml.cs[497-530]
src/Capacitor.App/Services/ServerConnectionService.cs[31-34]
src/Capacitor.App/Services/ServerConnectionService.cs[71-75]
src/Capacitor.App/Services/ServerConnectionService.cs[221-233]
src/Capacitor.App/Services/SessionAttentionTracker.cs[35-53]
src/Capacitor.App/Services/ServerPermissionFeed.cs[25-47]

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 server lane can connect and publish permission events before the newly-added permission feed and attention tracker subscribe. Those observables do not replay earlier broadcasts, so an event received in this startup window is permanently missed.

## Fix Focus Areas
- src/Capacitor.App/App.axaml.cs[497-530]
- src/Capacitor.App/Services/ServerConnectionService.cs[31-34]
- src/Capacitor.App/Services/SessionAttentionTracker.cs[35-42]
- src/Capacitor.App/Services/ServerPermissionFeed.cs[25-47]

## Recommended Fix
Construct and subscribe the session-access, permission-feed, and attention-tracker graph before calling `serverLane.Start()`. Add a startup-order test that emits a pending notification immediately on connection and verifies that attention is reconciled and surfaced.

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


12. Reconnected sessions stay unanswerable ✓ Resolved 🐞 Bug ≡ Correctness
Description
RemoteSessionViewModel sets SessionEnded when its directory row is removed, but Apply never
clears that flag when the same nonterminal row returns. A transient empty remote snapshot followed
by a reconnect re-adds the same keyed row, yet its cards remain hidden and its Stop command remains
disabled.
Code

src/Capacitor.App/ViewModels/RemoteSessionViewModel.cs[93]

+                    if (change.Reason == ChangeReason.Remove) { SessionEnded = true; Release(); continue; }
Relevance

●●● Strong

Recent accepted findings favor resetting stale view-model state across reconnects and asynchronous
refreshes.

PR-#766
PR-#730

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Remote rows retain the same remote:{agentId} key across removal and re-addition, and directory
snapshots may legitimately remove then restore them. The view model keeps the existing workspace
open, while ShowsCards and Stop availability both continue to depend on the never-reset ended
flag.

src/Capacitor.App/ViewModels/RemoteSessionViewModel.cs[67-69]
src/Capacitor.App/ViewModels/RemoteSessionViewModel.cs[88-123]
src/Capacitor.App/Services/AgentRow.cs[28-37]
src/Capacitor.App/Services/AgentDirectory.cs[117-147]
src/Capacitor.App/Services/RemoteAgentsService.cs[102-125]

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 open remote workspace permanently retains `SessionEnded = true` after its directory row is transiently removed and later re-added. This keeps permission cards hidden and Stop disabled for the recovered session.

## Fix Focus Areas
- src/Capacitor.App/ViewModels/RemoteSessionViewModel.cs[88-127]

## Recommended Fix
When `Apply` receives a nonterminal row, reset `SessionEnded` to false before restoring or acquiring session access. Preserve the ended state only for an actual terminal status, and add coverage for remove-then-readd of the same remote row key.

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



Remediation recommended

13. Interrupt parsing bypasses helpers 📘 Rule violation ⚙ Maintainability
Description
Options, Elem, Read, and Int classify JSON values through ValueKind property patterns
instead of the shared shape helpers. Reconciliation of session-detail events reaches these checks
for arrays, objects, nulls, strings, and numbers.
Code

src/Capacitor.App/Services/InterruptReconciliation.cs[R82-85]

+        obj is { ValueKind: JsonValueKind.Object } o ? (o.Prop(snake) ?? o.Prop(camel)) is { ValueKind: not JsonValueKind.Null } e ? e : null : null;
+    static string? Read(JsonElement obj, string snake, string camel) => Elem(obj, snake, camel) is { ValueKind: JsonValueKind.String } e ? e.GetString() : null;
+    static bool? Bool(JsonElement obj, string snake, string camel) => Elem(obj, snake, camel) is { } e && e.ValueKind is JsonValueKind.True or JsonValueKind.False ? e.GetBoolean() : null;
+    static int? Int(JsonElement obj, string snake, string camel) => Elem(obj, snake, camel) is { ValueKind: JsonValueKind.Number } e && e.TryGetInt32(out var i) ? i : null;
Relevance

●●● Strong

Explicit repository rule requires shared JSON helpers; replacing direct ValueKind checks is a
deterministic convention fix.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2270023 includes pattern matching on ValueKind. The changed reconciliation helpers use direct
patterns even though the shared extension class supplies equivalent helpers for the represented
shapes.

Rule 2270023: Use JsonElementExtensions helpers instead of direct JsonValueKind comparisons
src/Capacitor.App/Services/InterruptReconciliation.cs[68-85]
src/Capacitor.Models.Transcripts/JsonElementExtensions.cs[9-13]

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

## Issue description
Interrupt reconciliation reimplements JSON shape classification with direct `ValueKind` patterns.

## Fix Focus Areas
- src/Capacitor.App/Services/InterruptReconciliation.cs[68-85]

## Recommended Fix
Use `IsArray`, `IsObject`, `IsNull`, `IsString`, and `IsNumber` for the corresponding checks; add and use a shared boolean helper if needed.

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


14. Test directory misroutes twin sessions ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
FakeAgentDirectory.SessionAgents uses ToDictionary and VendorOfSession selects an unordered
first row, rather than applying the production directory's local-row precedence. When valid local
and remote rows share a session ID, the map throws instead of resolving the local agent and the
vendor can differ, so permission-routing tests built on this fake cannot exercise production
behavior.
Code

test/Capacitor.App.Tests.Unit/FakeAgentDirectory.cs[R20-22]

+        Rows.Connect().QueryWhenChanged(q => (IReadOnlyDictionary<string, string>)q.Items
+                .Where(r => r.SessionId is { Length: > 0 })
+                .ToDictionary(r => r.SessionId!, r => r.Id, StringComparer.Ordinal))
Relevance

●●● Strong

The fake deterministically throws on valid shared-session rows instead of matching production
precedence.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The fake permits both source-scoped row types but cannot represent their valid shared-session state.
Production explicitly orders local rows first and uses TryAdd to resolve that state, and the
resulting map is used to associate server-lane permission cards with logical agent IDs.

test/Capacitor.App.Tests.Unit/FakeAgentDirectory.cs[19-26]
src/Capacitor.App/Services/AgentDirectory.cs[82-98]
src/Capacitor.App/Services/PermissionService.cs[354-363]
src/Capacitor.App/Services/AgentRow.cs[19-37]

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

Issue description
`FakeAgentDirectory` does not preserve `IAgentDirectory`'s behavior when local and remote rows claim the same session: its session map throws on duplicate keys, and its vendor lookup has no local-row precedence.

Fix Focus Areas
- test/Capacitor.App.Tests.Unit/FakeAgentDirectory.cs[19-26]

Recommended Fix
Build the session map by ordering rows by `Origin` and retaining the first row for each non-empty session ID, as production does. Apply the same local-first ordering to `VendorOfSession`, and retain the empty-map behavior for no matching sessions.

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


15. Late prompts evade settlement coverage ✓ Resolved 🐞 Bug ☼ Reliability
Description
Server_settlement_by_id_clears_the_twin_and_the_claiming_local_item writes the late pending event
and then relies on a fixed 50 ms delay before inspecting the cache. The stream writer only queues
that event for an asynchronous service loop, so a delayed loop lets the assertion pass before it
would expose a regression that accepts the tombstoned request.
Code

test/Capacitor.App.Tests.Unit/PermissionServiceTests.cs[R389-391]

+        h.Stream.EmitPending(Dto("l1", serverRequestId: "srv-1"));
+        await Task.Delay(50);
+        await Assert.That(h.View.Count).IsEqualTo(0);
Relevance

●●● Strong

A fixed delay does not deterministically prove asynchronous stream processing completed before
assertions.

PR-#856

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
EmitPending only enqueues to an unbounded channel, while the production service consumes the
subscription asynchronously. The tombstone guard is evaluated only when that queued event reaches
UpsertLocal, which the delay does not prove.

test/Capacitor.App.Tests.Unit/PermissionServiceTests.cs[27-38]
test/Capacitor.App.Tests.Unit/PermissionServiceTests.cs[382-391]
src/Capacitor.App/Services/PermissionService.cs[199-220]
src/Capacitor.App/Services/PermissionService.cs[228-245]

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 settlement test asserts after a fixed delay rather than after the late permission event has been consumed, allowing the test to pass while the relevant event remains queued.

Fix Focus Areas
- test/Capacitor.App.Tests.Unit/PermissionServiceTests.cs[389-391]

Recommended Fix
Add a consumption synchronization point to `FakePermissionStream` or the harness, then await it after emitting the late pending event before asserting the cache remains empty. Do not use a fixed delay as proof that `PermissionService` evaluated the tombstone.

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


View medium (1)
16. A fake documents test coverage ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The comment above SessionAgents says the members are not exercised by the fake's one test. That
statement depends on the current test inventory and becomes stale whenever another test starts using
the fake.
Code

test/Capacitor.App.Tests.Unit/HomeViewModelTests.cs[1410]

+        // Not exercised by this fake's one test (the read-order race is the whole point of it).
Relevance

●●● Strong

Recent precedents reject comments documenting current test coverage because they become stale.

PR-#863
PR-#834

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2762993 allows comments for non-obvious behavior-critical constraints, while this comment
records temporary test-coverage status. The following members are straightforward default
implementations.

Rule 2762993: Restrict comments to documenting non-obvious, behavior‑critical constraints
test/Capacitor.App.Tests.Unit/HomeViewModelTests.cs[1410-1413]

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 fake contains a comment about which members the current test happens to exercise rather than a behavioral constraint.

## Fix Focus Areas
- test/Capacitor.App.Tests.Unit/HomeViewModelTests.cs[1410-1413]

## Recommended Fix
Remove the comment, or replace it with a durable explanation of why these default values are required for the race scenario.

ⓘ 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: 01e241aa)
Review mode: 🧠 Deep: This is a dense, broad cross-layer change spanning remote control, permission correlation, authorization lifecycle, reconnect/revocation handling, UI state, wire contracts, and many independent logic sites where redundant review could catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +82 to +85
obj is { ValueKind: JsonValueKind.Object } o ? (o.Prop(snake) ?? o.Prop(camel)) is { ValueKind: not JsonValueKind.Null } e ? e : null : null;
static string? Read(JsonElement obj, string snake, string camel) => Elem(obj, snake, camel) is { ValueKind: JsonValueKind.String } e ? e.GetString() : null;
static bool? Bool(JsonElement obj, string snake, string camel) => Elem(obj, snake, camel) is { } e && e.ValueKind is JsonValueKind.True or JsonValueKind.False ? e.GetBoolean() : null;
static int? Int(JsonElement obj, string snake, string camel) => Elem(obj, snake, camel) is { ValueKind: JsonValueKind.Number } e && e.TryGetInt32(out var i) ? i : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

13. Interrupt parsing bypasses helpers 📘 Rule violation ⚙ Maintainability

Options, Elem, Read, and Int classify JSON values through ValueKind property patterns
instead of the shared shape helpers. Reconciliation of session-detail events reaches these checks
for arrays, objects, nulls, strings, and numbers.
Agent Prompt
## Issue description
Interrupt reconciliation reimplements JSON shape classification with direct `ValueKind` patterns.

## Fix Focus Areas
- src/Capacitor.App/Services/InterruptReconciliation.cs[68-85]

## Recommended Fix
Use `IsArray`, `IsObject`, `IsNull`, `IsString`, and `IsNumber` for the corresponding checks; add and use a shared boolean helper if needed.

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

Comment on lines +73 to +74
/// Behaviors the permission-response route accepts.
public static class PermissionBehaviors {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

14. Permission constants share a source file 📘 Rule violation ⚙ Maintainability

PermissionBehaviors is added as another top-level type in RemoteWire.cs, which already contains
WireTokens and other wire types. Later changes to permission behavior constants therefore extend
an aggregate file rather than a source file named for their primary type.
Agent Prompt
## Issue description
The new permission behavior type is another primary top-level type in an existing aggregate source file.

## Fix Focus Areas
- src/Capacitor.Remote.Models/RemoteWire.cs[73-77]

## Recommended Fix
Move `PermissionBehaviors` into a new `PermissionBehaviors.cs` file in the same namespace.

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

Comment on lines +8 to +10
public enum PendingInterruptKind { ClaudePermission, AcpPermission, AcpQuestion, TranscriptQuestion }

public sealed record PendingInterrupt(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

24. Interrupt types share one file 📘 Rule violation ⚙ Maintainability

PendingInterruptKind and PendingInterrupt are both public top-level types in
PendingInterrupt.cs. The enum and record are independently reusable declarations rather than tiny
implementations of a private hierarchy.
Agent Prompt
## Issue description
The pending-interrupt source file declares a public enum alongside its public record.

## Fix Focus Areas
- src/Capacitor.App/Services/PendingInterrupt.cs[8-15]

## Recommended Fix
Move `PendingInterruptKind` into `PendingInterruptKind.cs` and retain `PendingInterrupt` in the existing file.

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

Comment on lines +14 to +16
/// Builds the app's real authenticated HTTP lane against a WireMock server.
static class WireMockLane {
public static async Task<(ICapacitorHttpClient Http, ProfileContext Profiles, ServiceProvider Provider)> BuildAsync(WireMockServer server, ConfigRoot root) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

15. Server test helper shares the test file 📘 Rule violation ⚙ Maintainability

WireMockLane is introduced as a top-level helper beside ServerSessionHttpTests in the same
source file. It is not nested, private, part of a small type hierarchy, or a descriptor used only by
a registry.
Agent Prompt
## Issue description
The new server-session test file contains both a top-level HTTP fixture helper and the test class.

## Fix Focus Areas
- test/Capacitor.App.Tests.Unit/ServerSessionHttpTests.cs[14-29]

## Recommended Fix
Move `WireMockLane` into `WireMockLane.cs`, or make it a private nested helper inside the test class.

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

Comment on lines +8 to +10
/// ServerRequestId is the server's id for the same request once the daemon's server leg holds
/// one — null until then, and always null from a daemon that predates it — so a client hearing
/// both lanes can pair the two copies without guessing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

16. Compatibility comment narrates history 📘 Rule violation ⚙ Maintainability

The PermissionPendingDto documentation says ServerRequestId is null for a daemon that “predates”
the field. The compatibility requirement is understandable as a nullable wire contract without
describing the relative age of daemon versions.
Agent Prompt
## Issue description
The new wire comment uses historical narration about daemons that predate the field.

## Fix Focus Areas
- src/Capacitor.Cli.Core/LocalIpc/PermissionIpc.cs[8-10]

## Recommended Fix
Rewrite the comment in present tense, stating which currently supported daemon payloads can omit the field and why consumers must accept null.

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

Comment thread test/Capacitor.App.Tests.Unit/FakeAgentDirectory.cs Outdated
RemoteModelsJsonContext.Default.SessionDetailDto)!;
await Assert.That(detail.LastEventNumber).IsEqualTo(3L);
await Assert.That(detail.Events![0].EventType).IsEqualTo("InterruptIssued");
await Assert.That(detail.Events[0].Payload!.Value.GetProperty("kind").GetString()).IsEqualTo("permission");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

11. Remote model tests fail to build 🐞 Bug ≡ Correctness

Session_detail_reads_events_with_payload_and_data dereferences the nullable
SessionDetailDto.Events property without a null-forgiving operator on its final assertion.
Nullable analysis emits CS8602 for this expression, and the repository promotes warnings to errors,
so the Remote.Models unit-test project cannot compile.
Agent Prompt
Issue description
`SessionDetailDto.Events` is declared nullable, but the final assertion in the new contract test dereferences it without null suppression. With nullable warnings treated as errors, this produces CS8602 and prevents the test project from building.

Fix Focus Areas
- test/Capacitor.Remote.Models.Tests.Unit/PermissionWireTests[29-30]

Recommended Fix
Use `detail.Events![0]` in the final assertion as well, or assign `detail.Events!` to a local non-null variable before both assertions.

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

Comment thread test/Capacitor.App.Tests.Unit/FakePermissionService.cs Outdated
Comment thread test/Capacitor.App.Tests.Unit/PermissionServiceTests.cs Outdated
Comment on lines +150 to +152
case ServerRespondKind.NotPending:
lock (_lock) { if (!_disposed) ConcludeServerKey(target.RequestId); }
return new(PermissionResolveKind.AlreadyDecided, null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

12. Shared viewers lose pending prompts 🔗 Cross-repo conflict ≡ Correctness

SendServerAsync treats every response-route 404 as an already-decided request and removes the
card, although kcap-server also returns 404 when a Full-access viewer is not the session owner. When
a non-owner opens a Full-shared session, the server delivers permission and elicitation pushes to
that viewer, but answering one removes the desktop card while leaving the underlying agent request
pending.
Agent Prompt
## Issue description
Full-access non-owner viewers can subscribe to session prompts, but kcap-server's permission-response endpoint only accepts the session owner and disguises that refusal as 404. The desktop interprets 404 as an already-settled request and removes the card even though the agent remains blocked.

## Fix Focus Areas
- src/Capacitor.App/Services/PermissionService.cs[144-152]
- src/Capacitor.App/Services/ServerPermissionFeed.cs[30-35]
- /cross_repos/kcap-server/src/Capacitor.Server/Sessions/SessionHookHandlers.cs[2403-2405]
- /cross_repos/kcap-server/src/Capacitor.Server/Sessions/CapacitorHub.cs[5716-5725]

## Recommended Fix
Coordinate the authorization contract between the repositories. Either allow Full-access viewers who receive prompt payloads to use the response endpoint, or restrict prompt subscription and delivery to owners and expose that denial to the desktop; also ensure an authorization refusal is distinguishable from a genuinely settled request so the desktop does not discard a still-pending card.

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

alexeyzimarev and others added 25 commits September 11, 2026 10:56
A row removed because the local daemon proved the twin is not an ended session: the host reports the origin change instead, and the window opens the local workspace for the same id.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The two lanes allocate agent ids independently, so one id on each is two agents: the in-flight stop set and the tray's command parameter are keyed by lane, and a rail click names the lane of the row it came from.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fakes' session map and withdraw now follow the production rules they stand in for: a duplicate session id resolves local instead of throwing, and a server-lane withdraw is refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A session with no card, no shadow and no generation of its own had nothing for the clear to move, so the epoch retires every fetch in flight at once. Only the reconciliation is gated on it: a live push already belongs to the new subject.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tray reads the lane from the clicked key rather than from an entry that may already be gone, a host whose row moved to this machine stops offering cards and Stop, and a set that outlived a disconnect stays marked owed so a response cannot strand it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An ACP-hosted agent's question reaches the app only over the server lane, in its session's chat group, so a local workspace needs that lease as well. A local agent the server never registered is refused the watch; nothing here reads the verdict, which is what keeps the refusal invisible.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cross-lane dedup fails open, so a same-id local/remote pair is two unrelated agents. Filtering on the agent id alone put one lane's card under the other's header, where answering it would act on a process the user never opened.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A same-user reconnect moves neither the session generation nor the lane epoch, and the live sequence only protects newer additions, so an older fetch still in flight could re-add a card the reconnect's own reconciliation had proved settled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The chat group is shared per session, so a superseded-but-successful attempt handing it back dropped the membership the reopened lease was relying on: Established, no retry armed, and no payloads arriving. Attempt numbering guards the published verdict, not subscription ownership.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cross-lane dedup fails open, so a same-id local row can hold an unrelated agent. Treating its mere presence as proof told the user this agent was now hosted locally and pointed them at that other process; the session id is the evidence already carried on both rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The live factory parses the elicitation payload and the reconciled arm did not, so a question raised before its workspace joined would restore as a generic Allow/Deny and settle carrying no updated_input at all.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The daemon accepts an empty-string enum value as an offered id and resolves the answer by exact match, so rejecting it degraded a live selection question into a free-text card whose answer cannot satisfy that contract. Only an absent or null id makes an option unusable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The lane stays Connected across a re-auth, so reading its status as a connected boolean swallowed the subject change whole: the previous account's session sets and published pips survived, and the fetch it had started went on filling them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The origin-change routing test modelled a twin with no session id on either row, which is no longer evidence of one. RemoteHost.New keeps a one-argument overload rather than an optional parameter: the factory takes it as a method group, and that conversion needs an exact signature.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dering-dusk

main's new PR-context smoke test reads members ISessionWorkspace deliberately does not expose, so the call site narrows to WorkspaceViewModel rather than the interface widening.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Testing ownership and calling the hub were two steps: a replacement attempt claimed and joined the group between them, and the superseded attempt then handed back the membership the live lease was receiving payloads on. The gate is held across the hub call and never with the state lock.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Testing the attempt and committing to the cache were two steps: a reconnect established a newer attempt between them, and the older fetch handed back the request that attempt's own reconciliation had just proved gone. The gate spans the test and the commit, never the fetch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The denial dispatched an unconditional drop. Delayed behind the grant that replaced it, that drop deleted the cards the grant's own reconciliation had proved pending, or moved the cache generation under that fetch and made its valid result be discarded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A session id is unique only within one server, and Claude's derive from transcript filenames that imports preserve, so matching a server-lane item on the id alone let a local workspace show another server's prompts and answer its process over HTTP. The directory already proves both facts, so the view models consume its evidence rather than re-deriving identity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dering-dusk

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The scope flag and the session id reach the filter from the directory's recompute and the daemon pump, so a predicate-driven add or remove ran Transform and SortAndBind on those threads, mutating a bound collection and the chat tab's request map off the UI thread. Scheduling only the cache input leaves that path uncovered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The bump stays where the transition is observed: that arrival order is what makes an older attempt stale, and moving it into the gate would let queue order redefine which verdict is newer. Gating the capture is what closes the window — a drop landing between a grant's marker and its commit left the cache rejecting a result whose attempt was still current.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A session id is unique only within one server, so while the local daemon reports another one its rows name different sessions: the lookups stamped a server session's cards and its rail pip onto an unrelated local agent. The flag is an input to the map rather than only to its consumers, so a flip republishes it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A takeover verdict needs current local authority — the socket up and the agent still live on it — because a server verdict that ends the session retires the remote row exactly as a takeover does. The pane keeps the ended verdict it was given rather than reporting a move to a process that is no longer running the session.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dering-dusk

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alexeyzimarev
alexeyzimarev merged commit 144e767 into main Sep 11, 2026
7 checks passed
@alexeyzimarev
alexeyzimarev deleted the desktop-remote-control-slice2 branch September 11, 2026 15:45
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.

Desktop shell: control remote agents — stop, permission and question cards

1 participant