Skip to content

Support a typeable agent terminal and local fork bases in the daemon - #1314

Merged
rsergeenko merged 7 commits into
mainfrom
feature/ai-3173-terminal-raw-input
Oct 5, 2026
Merged

rsergeenko merged 7 commits into
mainfrom
feature/ai-3173-terminal-raw-input

Conversation

@rsergeenko

Copy link
Copy Markdown
Contributor

AI-3173 — no GitHub issue for this work.

What & why

The server's redesigned agent page lets a viewer with Full access type straight into a hosted agent's terminal, and fork a session from any turn. The daemon now takes raw keystrokes through a new single-record SendRawInput hub method, advertised by a trailing supports_raw_input on connect. It also starts a fork's worktree from a local commit or a local branch before falling back to fetching origin, advertised by supports_branch_base_ref.

Where to look

WorktreeManager.CreateAsync: a hub-supplied base ref is checked with git check-ref-format --branch (exact echo, no leading -) before any git call or directory is created; that also closes an option injection into the origin fetch. UnixPtyProcess: the master fd moved behind a SafeHandle, so input that arrives while an agent is being torn down is dropped instead of written to a closed, possibly reused fd.

Verification

Reverting the PTY disposal guard makes both new disposal tests fail: 4 and 38 bytes land in the file that reused the closed fd. Ran live against a dev server with the daemon in Docker: typed into a hosted Claude agent's terminal from the browser, and a branch-head fork started from the source agent's local branch head rather than the default branch.

Teardown keeps the agent registered while it disposes the runtime, so the PTY master's close is deferred behind a SafeHandle refcount: a write racing dispose can no longer land on a reused fd.
@linear-code

linear-code Bot commented Oct 5, 2026

Copy link
Copy Markdown

AI-3173

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Support hosted terminal input and local fork base refs

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Route raw browser keystrokes to hosted agent terminals when the daemon advertises support.
• Start fork worktrees from local commits or branches before fetching from origin.
• Prevent late terminal input from reaching disposed PTYs or reused file descriptors.
Diagram

graph TD
  Hub["Server hub"] --> Connection["Server connection"] --> Orchestrator["Agent orchestrator"] --> Runtime["Hosted runtime"] --> Pty["Guarded PTY"]
  Connection --> Worktree["Worktree manager"] --> Base{"Local base?"} -->|"Local commit or branch"| Git["Git worktree"]
  Base -->|"Fetch from origin"| Git
Loading
High-Level Assessment

Keep the dedicated raw-byte command and local-first worktree resolution. Reusing the text-input command would not preserve terminal keystrokes, while a fetch-only fork cannot use unpushed local history; capability flags maintain compatibility with older daemons.

Files changed (13) +614 / -29

Enhancement (4) +134 / -2
Models.csDefine raw-input command and daemon capability flags +17/-1

Define raw-input command and daemon capability flags

• Adds a source-generated JSON contract for base64-encoded terminal bytes and optional connect flags for raw input and local branch base refs.

src/Capacitor.Cli.Core/Models.cs

AgentOrchestrator.csDeliver eligible raw input and reject input during teardown +52/-0

Deliver eligible raw input and reject input during teardown

• Decodes hub-delivered bytes for public, live agents and updates awaiting-input state on submit. Drops malformed or unsupported input and prevents raw input and special keys from reaching closing agents.

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

ServerConnection.csRegister raw-input dispatch and advertise capabilities +8/-1

Register raw-input dispatch and advertise capabilities

• Registers the SendRawInput hub method and advertises raw input only when a handler is wired. Advertises support for locally resolved branch base refs.

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

WorktreeManager.csResolve validated fork bases locally before fetching +57/-0

Resolve validated fork bases locally before fetching

• Rejects invalid hub-supplied base refs before creating directories. Starts worktrees from available local commits or branch heads, retaining the origin-fetch fallback and logging the selected source.

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

Bug fix (3) +54 / -24
PtyMasterHandle.csProtect the Unix PTY master descriptor with a SafeHandle +34/-0

Protect the Unix PTY master descriptor with a SafeHandle

• Holds the descriptor open during writes and resizes, deferring its close until active users release it.

src/Capacitor.Cli.Daemon/Pty/Unix/PtyMasterHandle.cs

UnixPtyProcess.csDrop Unix PTY input after disposal begins +12/-13

Drop Unix PTY input after disposal begins

• Routes writes and resizes through the SafeHandle and makes disposal idempotent, preventing late writes to a reused descriptor.

src/Capacitor.Cli.Daemon/Pty/Unix/UnixPtyProcess.cs

ConPtyProcess.csIgnore input to a closing Windows pseudo console +8/-11

Ignore input to a closing Windows pseudo console

• Unifies string and byte writes and drops input when disposal has begun or the input stream has closed.

src/Capacitor.Cli.Daemon/Pty/Windows/ConPtyProcess.cs

Refactor (1) +1 / -1
AgentOrchestrator.LocalIpc.csShare submit detection with hub-delivered input +1/-1

Share submit detection with hub-delivered input

• Makes the existing carriage-return and newline check available to the new raw-input handler.

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

Tests (5) +425 / -2
UnixPtyProcessDisposalTests.csTest Unix PTY writes against reused descriptors +88/-0

Test Unix PTY writes against reused descriptors

• Checks that writes and resizes after disposal, including writes racing teardown, never reach a file assigned the former PTY descriptor.

test/Capacitor.Cli.Daemon.Tests.Unit/Pty/Unix/UnixPtyProcessDisposalTests.cs

FakeHostedAgentRuntime.csCapture raw input and special keys in the test runtime +19/-2

Capture raw input and special keys in the test runtime

• Records delivered bytes and keys and can simulate a runtime without raw-input support.

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

SendRawInputTests.csCover raw-input delivery and teardown guards +147/-0

Cover raw-input delivery and teardown guards

• Tests byte delivery, agent eligibility, malformed payloads, unsupported runtimes, submit tracking, and rejection of input and special keys during teardown.

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

ServerConnectionAdvertisementTests.csVerify capability advertisement and wire serialization +52/-0

Verify capability advertisement and wire serialization

• Checks handler-dependent raw-input advertisement, branch-base advertisement, and snake_case JSON fields for capabilities and the raw-input command.

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

WorktreeManagerTests.csTest local fork bases, fetch fallback, and ref rejection +119/-0

Test local fork bases, fetch fallback, and ref rejection

• Covers local-only commits and branches, missing-local-base fetches, existing remote refs, and rejection of invalid or option-like refs before worktree creation.

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

@qodo-code-review

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Dropped keystrokes appear delivered 🐞 Bug ≡ Correctness
Description
HandleSendRawInput returns on rejected payloads or unavailable agents and only logs unsupported
runtimes, without reporting the command's DispatchId through SendInputRejectedAsync. When any of
those conditions drops a dispatch, the server has no rejection report, unlike the existing
structured-input path.
Code

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[R4490-4493]

+        if (!_agents.TryGetValue(cmd.AgentId, out var agent)) {
+            LogSendRawInputUnknownAgent(cmd.AgentId, _agents.Count);
+            return;
+        }
Relevance

●●● Strong

Recent input-handling precedent accepts explicit reporting when dispatched input is dropped rather
than silently disappearing.

PR-#348
PR-#737

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The raw command requires a dispatch ID, while every early return and the unsupported-runtime catch
omit the rejection call that structured input uses to tell the server a dispatch was dropped.

src/Capacitor.Cli.Core/Models.cs[2097-2103]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4489-4517]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4087-4121]
src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[945-960]

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

## Issue description
Raw-input commands carry dispatch IDs but are silently dropped for unknown, private, closing, malformed, empty, or unsupported destinations.
## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4489-4518]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4087-4121]
## Recommended Fix
Report each failed raw-input dispatch using the existing bounded rejection mechanism and an appropriate drop reason; cover the drop paths in tests.

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

Dismiss ↗ | View ↗



Remediation recommended

2. Web viewers can type into protected review agents ✓ Resolved
Description
HandleSendRawInput rejects private and closing agents but does not check agent.Kind before
writing raw bytes to the runtime. Server-dispatched keystrokes such as Ctrl-C, Escape, or /exit
can therefore reach PR-review and review-flow PTYs, while the local attach path discards stdin for
every non-Default agent.
Code

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[R4495-4497]

+        if (agent.IsPrivate) return;
+
+        if (IsClosingToInput(agent)) return;
Relevance

●●● Strong

Very recent precedent accepts enforcing kind-based protection for typed input reaching review
agents.

PR-#1309

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
HandleSendRawInput writes raw bytes without a launch-kind check. By contrast, local attach calls
AttachClientLoopAsync with readOnly: agent.Kind != LaunchKind.Default and drops stdin for
read-only agents; review-flow launches can use PTY runtimes that accept raw writes.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs[403-403]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4489-4518]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4489-4517]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs[399-404]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs[478-482]
src/Capacitor.Cli.Daemon/Services/PtyHostedAgentRuntime.cs[85-93]
src/Capacitor.Cli.Daemon/Harness/Claude/ClaudeLauncher.cs[20-23]
PR-#1309

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

## Issue description
Server-origin raw PTY input is not gated by agent kind, allowing keystrokes to reach review and review-flow agents despite the local attach path treating non-Default agents as read-only.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4489-4517]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs[399-404]

## Recommended Fix
In `HandleSendRawInput`, check the launch kind before decoding or writing raw bytes. Add `if (agent.Kind != LaunchKind.Default) return;` with a debug log next to the `IsPrivate` check to match the local attach policy; at minimum, refuse `LaunchKind.ReviewFlow`. Test that raw input is not delivered to a review-flow PTY.

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


3. Uppercase local commit forks fail ✗ Dismissed
Description
IsFullSha recognizes only lowercase hexadecimal digits, so an uppercase commit ID never reaches
CommitExistsLocallyAsync. A fork based on an uppercase ID for a local-only commit instead attempts
an origin fetch, which cannot supply that commit.
Code

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[R1518-1519]

+    static bool IsFullSha(string value) =>
+        value.Length == 40 && value.All(c => c is >= '0' and <= '9' or >= 'a' and <= 'f');
Relevance

●●● Strong

Deterministic hexadecimal validation bug can prevent valid uppercase local commit forks.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new predicate excludes A–F, gates the local object lookup, and falls through to an origin fetch
when it returns false.

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[243-267]
src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[272-285]
src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[1518-1524]
test/Capacitor.Cli.Daemon.Tests.Unit/Services/WorktreeManagerTests.cs[71-89]

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 local-commit fast path skips uppercase hexadecimal commit IDs and falls back to fetching local-only commits from origin.
## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[1518-1524]
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[243-267]
## Recommended Fix
Accept both uppercase and lowercase hex digits when identifying full commit IDs, and test an uppercase ID for a commit unavailable from origin.

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


4. Commit subjects lack GitHub issue references ✗ Dismissed
Description
All three supplied commit subjects begin with [AI-3173] and do not end with a parenthesized GitHub
issue reference such as (#123). They also use bracketed Linear-style metadata instead of the
required single imperative clause followed by one GitHub issue number.
Code

src/Capacitor.Cli.Core/Models.cs[R2099-2103]

+public readonly record struct SendRawInputCommand(
+        string AgentId,
+        string Data,
+        Guid   DispatchId
+    );
Relevance

●●● Strong

Explicit repository commit-format rule directly matches the supplied noncompliant subjects.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The supplied commit subjects are `[AI-3173] Accept raw terminal input and start worktrees from a
local commit, [AI-3173] Resolve a fork's base branch on the daemon's own machine, and [AI-3173]
Drop terminal input that arrives after the agent's PTY closed`; none matches the required pattern
ending in (#digits).

Rule 2897961: Enforce single-clause imperative commit subject with GitHub issue reference and 80-char limit

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 supplied commit subjects use `[AI-3173]` and omit the required trailing GitHub issue reference, so they do not match the repository's commit-subject format.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Models.cs[2099-2103]

## Recommended Fix
Rewrite each commit subject as one imperative clause followed by exactly one GitHub issue reference in parentheses, for example `Add raw terminal input support (#1234)`. If no GitHub issue exists, create or obtain an explicit issue reference rather than fabricating one.

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


View medium (7)
5. PR description omits the required issue link 📘 Rule violation § Compliance
Description
The description's first reference line says AI-3173 — no GitHub issue for this work. and contains
neither a GitHub closing keyword with an issue number nor both references on one line. Reviewers
therefore cannot use the description to identify a linked GitHub issue for this change.
Code

src/Capacitor.Cli.Core/Models.cs[R2099-2103]

+public readonly record struct SendRawInputCommand(
+        string AgentId,
+        string Data,
+        Guid   DispatchId
+    );
Relevance

●●● Strong

Explicit repository PR-description rule directly matches the missing GitHub issue reference.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The supplied PR description begins with AI-3173 — no GitHub issue for this work. and provides no
Closes, Fixes, or Resolves reference paired with a GitHub issue number.

Rule 2897991: PR description must contain both GitHub and Linear issue references; PR title must not contain issue IDs

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 PR description does not contain a single reference line combining a GitHub closing keyword, a GitHub issue number, and the Linear issue key.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Models.cs[2099-2103]

## Recommended Fix
Add a dedicated first-line reference such as `Closes #1234 AI-3173`, using the actual GitHub issue number and retaining the Linear key on the same line. Do not put the two references on separate lines or in the PR title.

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

Dismiss ↗ | View ↗


6. Failed PTY start can close an unrelated file ✓ Resolved
Description
UnixPtyProcess assigns masterFd to an owning PtyMasterHandle before constructing _reader,
while Spawn's failure path still closes the raw descriptor through Abandon. If reader
construction throws, the orphaned handle can later finalize and close that descriptor number again
after it has been reused by an unrelated file or socket.
Code

src/Capacitor.Cli.Daemon/Pty/Unix/UnixPtyProcess.cs[23]

+        _master       = new(masterFd);
Relevance

●●● Strong

Matching recent PTY construction precedent accepted cleanup for reader-start failures after native
spawn.

PR-#1024

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The constructor creates _master = new(masterFd) before _reader = new(masterFd, ...), and reader
construction can throw while starting its thread. Spawn catches constructor failures and calls
Abandon, which closes masterFd; the owning SafeHandle remains finalizable, and its
ReleaseHandle independently calls close on the same descriptor number.

src/Capacitor.Cli.Daemon/Pty/Unix/UnixPtyProcess.cs[167-194]
src/Capacitor.Cli.Daemon/Pty/Unix/PtyMasterHandle.cs[9-33]
src/Capacitor.Cli.Daemon/Pty/Unix/UnixPtyProcess.cs[20-26]
src/Capacitor.Cli.Daemon/Pty/Unix/UnixPtyReaderThread.cs[24-29]
src/Capacitor.Cli.Daemon/Pty/Unix/UnixPtyProcess.cs[163-194]
src/Capacitor.Cli.Daemon/Pty/Unix/PtyMasterHandle.cs[8-11]
src/Capacitor.Cli.Daemon/Pty/Unix/PtyMasterHandle.cs[33-33]
PR-#1024

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

## Issue description
If `UnixPtyProcess` construction fails after `_master` takes ownership of `masterFd`, `Abandon` closes the descriptor while the orphaned handle can later close that descriptor number again after reuse.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Pty/Unix/UnixPtyProcess.cs[20-27]
- src/Capacitor.Cli.Daemon/Pty/Unix/UnixPtyProcess.cs[163-194]

## Recommended Fix
Make descriptor ownership explicit during construction so the spawn failure path closes it only once, while still killing and reaping the spawned child. Create `_master` only after `_reader` has been constructed, or catch constructor failures and call `_master.SetHandleAsInvalid()` (or suppress its finalizer) before rethrowing so that only `Abandon` closes the descriptor.

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


7. Temporary test files bypass cleanup ✓ Resolved
Description
FdReuse creates its temporary directory with Directory.CreateTempSubdirectory and deletes it
directly instead of using the shared TempDir lifecycle helper. The new PTY disposal tests
therefore manage temporary files outside the repository's helper-based cleanup path, including the
directory and all files created beneath it.
Code

test/Capacitor.Cli.Daemon.Tests.Unit/Pty/Unix/UnixPtyProcessDisposalTests.cs[66]

+        readonly string               _dir     = Directory.CreateTempSubdirectory("kcap-fd-reuse-").FullName;
Relevance

●●● Strong

Recent tests consistently route temporary filesystem state through shared helpers; this direct
directory bypass is the same accepted rule violation.

PR-#1271
PR-#1207
PR-#803

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new FdReuse helper directly creates a temporary directory at line 66, creates files beneath it
at lines 71-76, and manually deletes the directory at line 85. The test project already references
the shared helpers project, whose TempDir type owns temporary-directory creation and cleanup, so
this changed test code bypasses the required helper.

Rule 2767472: Use TempDir helper for all temporary filesystem state in tests
test/Capacitor.Cli.Daemon.Tests.Unit/Pty/Unix/UnixPtyProcessDisposalTests.cs[66-85]
test/Capacitor.Tests.Helpers/TempDir.cs[18-90]

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

## Issue description
`FdReuse` directly creates and deletes a temporary directory instead of using the `TempDir` helper required for temporary filesystem state in tests. This bypasses the shared helper's cleanup lifecycle.

## Fix Focus Areas
- test/Capacitor.Cli.Daemon.Tests.Unit/Pty/Unix/UnixPtyProcessDisposalTests.cs[66-85]

## Recommended Fix
Replace the direct `Directory.CreateTempSubdirectory` and `Directory.Delete` lifecycle with a `TempDir` instance. Create the numbered files beneath the helper-owned root using its path helpers where applicable, and dispose the `TempDir` from `FdReuse.Dispose` after releasing the file handles.

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


8. Daemon capability notes retain release history 📘 Rule violation ⚙ Maintainability
Description
The DaemonConnect comments describe compatibility as coming from daemons that “predate” the
capability fields instead of stating only the current wire behavior. These historical qualifiers are
attached to the newly added capability documentation and will become stale as supported daemon
versions change.
Code

src/Capacitor.Cli.Core/Models.cs[R2195-2200]

+        // Advertises the SendRawInput handler. False from a daemon predating it; the server then
+        // never sends raw input.
+        bool                                        SupportsRawInput = false,
+        // A launch's base ref may name a local branch, which the daemon resolves on its own machine
+        // before falling back to a fetch. False from a daemon predating it, whose fetch-only path
+        // cannot see a branch that exists only here.
Relevance

●●● Strong

Recent precedents accept removing historical qualifiers and keeping comments focused on durable
current behavior.

PR-#972
PR-#875
PR-#638

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added comments explicitly describe daemons as “predating” the new fields, which is historical
change narration rather than a current invariant.

Rule 2897915: Avoid time-sensitive or process-reference metadata in code comments
src/Capacitor.Cli.Core/Models.cs[2195-2200]

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

## Issue description
The new `DaemonConnect` comments use historical wording about daemons predating the fields, which violates the requirement to keep comments focused on current behavior rather than change history.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Models.cs[2195-2200]

## Recommended Fix
Rewrite the comments to describe the observable protocol behavior directly: unsupported daemons omit or send false capability flags, and the server does not send raw input or local branch-base requests when the capability is unavailable. Remove the historical “predating” wording.

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

Dismiss ↗ | View ↗


9. Browser keystrokes never reach daemons ✗ Dismissed
Description
ServerConnection advertises SupportsRawInput and registers SendRawInput, but kcap-server
neither reads that capability nor sends that hub method. Its terminal-input transport still sends
only text through SendInput, so the new byte-input handler cannot serve the browser terminal with
this server version.
Code

src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[254]

+        _hub.On<SendRawInputCommand>("SendRawInput", cmd => SafeInvoke("SendRawInput", () => OnSendRawInput?.Invoke(cmd)));
Relevance

●● Moderate

Cross-repository protocol mismatch is plausible, but no close repository precedent establishes
acceptance.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR registers the receiving method and advertises its capability; the server connect contract
ends before that capability, and its daemon-bound transport sends only the older input methods.

kcap-cli -> kcap-server
src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[251-254]
src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[672-677]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public.Abstractions/Agents/Daemon/DaemonConnectArgs.cs [75-83]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public/PublicHubTransport.cs [51-55]

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

## Issue description
The daemon accepts raw terminal bytes, but the pinned server has no route that sends them.
## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[251-254]
- /cross_repos/kcap-server/src/Capacitor.Api.Public/PublicHubTransport.cs[51-55]
- /cross_repos/kcap-server/src/Capacitor.Api.Public.Abstractions/Agents/Daemon/DaemonConnectArgs.cs[75-83]
## Recommended Fix
Coordinate a kcap-server change that reads and retains the capability, authorizes browser input, and sends the matching base64 command to capable daemons. Deploy the server route alongside the daemon capability.

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


10. Forks cannot use local branches 🔗 Cross-repo conflict ≡ Correctness
Description
BuildDaemonConnect advertises SupportsBranchBaseRef, and CreateAsync can resolve a supplied
local branch, but kcap-server has no corresponding capability field and its ordinary launch
construction never supplies BaseRef. Until the server selects a capable daemon and sends the
source branch as the launch base, browser-initiated forks cannot exercise this new path.
Code

src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[677]

+            SupportsBranchBaseRef: true);
Relevance

●● Moderate

Cross-repository launch-contract mismatch is plausible, but evidence is external and lacks close
acceptance precedent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The daemon advertises branch-base support and resolves local branches, while the server's connect
contract lacks the flag and its ordinary launch sends a command without BaseRef.

kcap-cli -> kcap-server
src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[675-677]
src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[256-265]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Public.Abstractions/Agents/Daemon/DaemonConnectArgs.cs [75-83]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/Sessions/AgentStoreDataService.cs [1490-1515]

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

## Issue description
The daemon can start from a local branch, but the pinned server does not send one for hosted-agent launches.
## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[675-677]
- /cross_repos/kcap-server/src/Capacitor.Api.Public.Abstractions/Agents/Daemon/DaemonConnectArgs.cs[75-83]
- /cross_repos/kcap-server/src/Capacitor.Server/Sessions/AgentStoreDataService.cs[1490-1515]
## Recommended Fix
Update kcap-server to retain the branch-base capability and, for a fork, pass its source branch through `LaunchAgentCommand.BaseRef` only when the selected daemon supports it.

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

Dismiss ↗ | View ↗


11. Fork can start from a stale local branch ✗ Dismissed
Description
CreateAsync now resolves refs/heads/<baseRef> locally and uses it before trying the origin
fetch. Any launch whose BaseRef names a branch the daemon also has locally, such as main or a PR
head branch, now starts from the possibly outdated or diverged local tip instead of origin's current
head.
Code

src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[R256-260]

+                // An agent's branch exists only on its own machine, where origin cannot supply it.
+                if (await ResolveLocalBranchAsync(repoPath, baseRef) is { } branchHead) {
+                    await WithWorktreeMetadataGate(repoPath, time, () =>
+                        RunGit(repoPath, GitTimeout, time, noHooks,
+                            "worktree", "add", "--no-checkout", "-B", branch, worktreePath, branchHead));
Relevance

●● Moderate

Stale local-branch semantics are potentially intentional for daemon-local forks, making this
behavior-sensitive.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
cmd.BaseRef is passed straight to CreateAsync for every non-review launch, including
review-flow. Before this change a branch name always went through the origin fetch. The new
local-branch lookup runs first for every caller, not only forks.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2353-2392]
src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[1537-1542]

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 local-branch lookup now runs before the origin fetch for every base ref. Launches naming a branch that also exists locally get a stale local tip instead of origin's current head.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs[256-266]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2353-2392]

## Recommended Fix
Add an explicit `preferLocalBranch` parameter to `CreateAsync`, set only for fork launches, or try the origin fetch first and fall back to the local branch only when the fetch fails.

ⓘ 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: ff4af1f2) — View relationship
Review mode: Auto: 🧠 Deep: This is a high-density behavioral change spanning PTY lifecycle safety, hub protocol/capability negotiation, raw-input routing, and Git worktree/ref resolution, creating multiple independent paths where a redundant review could catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread test/Capacitor.Cli.Daemon.Tests.Unit/Pty/Unix/UnixPtyProcessDisposalTests.cs Outdated
Comment on lines +2195 to +2200
// Advertises the SendRawInput handler. False from a daemon predating it; the server then
// never sends raw input.
bool SupportsRawInput = false,
// A launch's base ref may name a local branch, which the daemon resolves on its own machine
// before falling back to a fetch. False from a daemon predating it, whose fetch-only path
// cannot see a branch that exists only here.

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

3. Daemon capability notes retain release history 📘 Rule violation ⚙ Maintainability

The DaemonConnect comments describe compatibility as coming from daemons that “predate” the
capability fields instead of stating only the current wire behavior. These historical qualifiers are
attached to the newly added capability documentation and will become stale as supported daemon
versions change.
Agent Prompt
## Issue description
The new `DaemonConnect` comments use historical wording about daemons predating the fields, which violates the requirement to keep comments focused on current behavior rather than change history.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Models.cs[2195-2200]

## Recommended Fix
Rewrite the comments to describe the observable protocol behavior directly: unsupported daemons omit or send false capability flags, and the server does not send raw input or local branch-base requests when the capability is unavailable. Remove the historical “predating” wording.

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

Dismiss ↗ | View ↗

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

An older daemon still connects without this field, so naming what its absence means is a live compatibility constraint, which the comment rules keep.

Comment thread src/Capacitor.Cli.Core/Models.cs
Comment on lines +2099 to +2103
public readonly record struct SendRawInputCommand(
string AgentId,
string Data,
Guid DispatchId
);

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

5. Pr description omits the required issue link 📘 Rule violation § Compliance

The description's first reference line says AI-3173 — no GitHub issue for this work. and contains
neither a GitHub closing keyword with an issue number nor both references on one line. Reviewers
therefore cannot use the description to identify a linked GitHub issue for this change.
Agent Prompt
## Issue description
The PR description does not contain a single reference line combining a GitHub closing keyword, a GitHub issue number, and the Linear issue key.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Models.cs[2099-2103]

## Recommended Fix
Add a dedicated first-line reference such as `Closes #1234 AI-3173`, using the actual GitHub issue number and retaining the Linear key on the same line. Do not put the two references on separate lines or in the PR title.

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

Dismiss ↗ | View ↗

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is no GitHub issue for this work; the reference line names the Linear key only, as the template allows when one half does not exist.

Comment on lines +4490 to +4493
if (!_agents.TryGetValue(cmd.AgentId, out var agent)) {
LogSendRawInputUnknownAgent(cmd.AgentId, _agents.Count);
return;
}

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

1. Dropped keystrokes appear delivered 🐞 Bug ≡ Correctness

HandleSendRawInput returns on rejected payloads or unavailable agents and only logs unsupported
runtimes, without reporting the command's DispatchId through SendInputRejectedAsync. When any of
those conditions drops a dispatch, the server has no rejection report, unlike the existing
structured-input path.
Agent Prompt
## Issue description
Raw-input commands carry dispatch IDs but are silently dropped for unknown, private, closing, malformed, empty, or unsupported destinations.
## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4489-4518]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4087-4121]
## Recommended Fix
Report each failed raw-input dispatch using the existing bounded rejection mechanism and an appropriate drop reason; cover the drop paths in tests.

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

Dismiss ↗ | View ↗

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Raw input is fire-and-forget like special keys: the server keeps no pending state for a raw dispatch that a rejection could retire, so a report would change nothing until the server's burst recording learns to take one back.

Comment thread src/Capacitor.Cli.Daemon/Pty/Unix/UnixPtyProcess.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/WorktreeManager.cs
Comment thread src/Capacitor.Cli.Daemon/Services/ServerConnection.cs
VendorModels: _config.VendorModels);
VendorModels: _config.VendorModels,
SupportsRawInput: AdvertisesRawInput,
SupportsBranchBaseRef: true);

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

11. Forks cannot use local branches 🔗 Cross-repo conflict ≡ Correctness

BuildDaemonConnect advertises SupportsBranchBaseRef, and CreateAsync can resolve a supplied
local branch, but kcap-server has no corresponding capability field and its ordinary launch
construction never supplies BaseRef. Until the server selects a capable daemon and sends the
source branch as the launch base, browser-initiated forks cannot exercise this new path.
Agent Prompt
## Issue description
The daemon can start from a local branch, but the pinned server does not send one for hosted-agent launches.
## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[675-677]
- /cross_repos/kcap-server/src/Capacitor.Api.Public.Abstractions/Agents/Daemon/DaemonConnectArgs.cs[75-83]
- /cross_repos/kcap-server/src/Capacitor.Server/Sessions/AgentStoreDataService.cs[1490-1515]
## Recommended Fix
Update kcap-server to retain the branch-base capability and, for a fork, pass its source branch through `LaunchAgentCommand.BaseRef` only when the selected daemon supports it.

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

Dismiss ↗ | View ↗

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The paired kcap-server PR #2244 keeps supports_branch_base_ref and sends a fork's source branch as BaseRef only to a daemon that advertises it, refusing the fork otherwise.

The server's flow-participant check fails open once a run ends, so the daemon refuses keys and raw input to review flows itself; PR-review agents still take typing. A failed PTY start disowns the handle before closing the fd, or its finalizer would close a reused number.
@rsergeenko rsergeenko self-assigned this Oct 5, 2026

Copy link
Copy Markdown
Collaborator

NO FINDINGS — reviewed the raw-input daemon path, PTY lifetime handling, and local fork-base selection on the current head. I did not build or run tests.

@rsergeenko
rsergeenko merged commit 6fd3abb into main Oct 5, 2026
4 of 8 checks passed
@rsergeenko
rsergeenko deleted the feature/ai-3173-terminal-raw-input branch October 5, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants