Skip to content

[AI-1761] Codex app-server: hosted-agent runtime, transport & transport wiring - #582

Merged
realtonyyoung merged 7 commits into
mainfrom
tonyyoung/ai-1761-codex-appserver-runtime
Aug 18, 2026
Merged

realtonyyoung merged 7 commits into
mainfrom
tonyyoung/ai-1761-codex-appserver-runtime

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

Hosts unattended Codex reviewers over codex app-server (JSON-RPC 2.0 over stdio) instead of the interactive PTY, behind a codex.transport config that defaults to pty (so this is fully inert until an operator opts a daemon in). Builds directly on the launch-input layer (#578).

Closes #581. Linear AI-1761 (epic AI-1759).

What's here (4 commits)

  1. Transport — CodexAppServerConnection: newline-delimited JSON-RPC 2.0 over stdio, a lean mirror of the proven AcpConnection (id correlation, single write-gate, one resilient read loop, always-answered server requests), reusing the shared envelope records and dropping ACP's reconnect-latch machinery a one-shot unattended reviewer has no design for.
  2. Runtime — CodexAppServerHostedAgentRuntime: control-plane only (transcript stays on the hooks + kcap watch rollout path, so EmitsTerminalOutput=false). Lifecycle: spawn → initialize (delta opt-out) → hooks/list trust preflight (proceed / seed-and-restart / fail-closed) → thread/start (resolved model + deterministic thread-id identity) → per-round turn/start. WaitForTurnIdleAsync resolves from turn/completed (and unblocks on transport death). Approvals pinned to never, so any server-initiated approval request is answered with a valid on-the-wire decline (never a JSON-RPC error) and logged at Error. Client→server sends are bounded-retried on the -32001 backpressure rejection. Usage captured from thread/tokenUsage/updated; reaper fed via AgentActivityClock.
  3. Sandbox fix + live smoke — driving the real codex 0.146.0 binary caught a wire bug a fake couldn't: thread/start.sandbox is the coarse SandboxMode string, a different shape from turn/start.sandboxPolicy's object (the object form is rejected there). Fixed and re-verified. Adds a gated (KCAP_CODEX_APPSERVER_SMOKE=1) live smoke that drives the no-model handshake surface through the production connection against the installed binary in an isolated CODEX_HOME — the schema-drift tripwire for a Codex bump (skips loudly in CI, which has no codex).
  4. Composite factory + wiring — CodexHostedAgentRuntimeFactory is the single "codex" factory: routes review-flow launches to app-server when DaemonConfig.CodexAppServerActive, delegates everything else (all interactive launches, all launches under the PTY default) to the wrapped PTY factory byte-identically. CodexTransportDecision is the ONE rule (operator selection AND the pinned 0.146.0 floor), resolved once into that field and read by both the router and the codex-appserver-unattended-v1 advertisement, so the advertised policy and the transport used cannot diverge.

Testing

  • 26 new unit tests + 22 factory/decision tests, all green: transport (15), runtime lifecycle vs an in-process fake app-server (10), transport-decision truth table + factory routing (delegation, app-server route via seam, fail-closed on missing hooks).
  • Live-validated against real codex 0.146.0 (the gated smoke).
  • Existing capability-advertisement suites stay green (default config advertises codex-unattended-v1 unchanged). AOT clean (no IL2026/IL3050).
  • Grounded on the authoritative schema generated from the pinned binary.

Scope / follow-ups

  • Wires app-server for review-flow (unattended) launches only; interactive stays PTY (that's AI-1762).
  • The vendored-schema (Q7) conformance test and the server-side codex-appserver-unattended-v1 classifier (AI-1761 server half / PR 3) are follow-ups; the server already accepts the new policy string via its existing unknown-string handling, so this daemon is safe against an old server.

🤖 Generated with Claude Code

Adds CodexAppServerConnection — the newline-delimited JSON-RPC 2.0 stdio
transport the app-server hosted-agent runtime speaks over. It mirrors the
proven AcpConnection (id correlation, single write-gate, one resilient read
loop, always-answered server requests) and reuses the shared AcpRpc envelope
records, but drops ACP's reconnect-latch machinery: a hosted Codex reviewer is
a one-shot unattended run with no reconnect design, so a dead app-server just
ends the read loop, faults pending requests, and is reaped through the normal
death path.

15 pipe-driven unit tests cover correlation, out-of-order responses, error
faulting, notification fan-out, the always-answered server-request guarantee,
malformed/wrong-typed frame resilience, and read-loop-end faulting.

Part of the AI-1761 app-server hosted-agent runtime.
CodexAppServerHostedAgentRuntime drives a hosted Codex reviewer over the
app-server JSON-RPC protocol instead of the interactive PTY. It is control-plane
only — transcript recording stays on the hooks + `kcap watch` rollout path, so
EmitsTerminalOutput is false and no transcript is aggregated from the protocol
stream.

Lifecycle (StartAsync, called by the factory): spawn -> initialize (with delta
opt-out) -> hooks/list trust preflight (proceed / seed-and-restart / missing ->
fail closed) -> thread/start (resolved model + deterministic thread-id identity)
-> optional first turn/start. Each review round is a turn/start on the held
thread; WaitForTurnIdleAsync resolves from the matching turn/completed
notification (and unblocks on transport death rather than hanging). The reaper
is fed through AgentActivityClock. Approvals are pinned to `never`, so any
server-initiated approval request is answered with a valid on-the-wire decline
(never a JSON-RPC error) and logged at Error. Client->server sends are bounded-
retried on the -32001 backpressure rejection. Token usage is captured from
thread/tokenUsage/updated.

All request params and response parsing match the authoritative schema
generated from the pinned codex 0.146.0 (initialize/thread-start/turn-start/
turn-completed/hooks-list/tokenUsage). Wire shapes are AOT-safe (JsonNode ->
JsonElement, no reflection).

10 lifecycle tests drive the real runtime against an in-process FakeCodexAppServer
over pipes: handshake + model/opt-out, round settlement, initial-prompt turn,
hook seed-and-restart, missing-hook fail-closed, always-decline approval bridge,
usage capture, backpressure retry, failed-status settlement, unsupported raw
input.

Part of the AI-1761 app-server hosted-agent runtime.
Live validation against the real codex 0.146.0 binary caught a wire-shape bug a
protocol fake could not: thread/start.sandbox is the coarse SandboxMode STRING
(read-only / workspace-write / danger-full-access), a DIFFERENT shape from
turn/start.sandboxPolicy's {type:…} object. The object form is rejected by the
app-server on thread/start ("unknown variant `type`"). The per-turn sandboxPolicy
object stays the load-bearing containment; the thread default is the string.

Adds CodexAppServerPosture.RenderSandboxMode (validated kebab-case string) and
points StartThreadAsync at it. Adds a gated live smoke
(KCAP_CODEX_APPSERVER_SMOKE=1) that drives the no-model handshake surface
(initialize → hooks/list → thread/start) through the production connection
against the installed codex binary, in an isolated CODEX_HOME — it skips loudly
in CI (no codex there) and is the schema-drift tripwire for a Codex version bump.

Part of the AI-1761 app-server hosted-agent runtime.
Wires the app-server runtime into the launch path as the single codex factory.

- CodexTransportDecision: the ONE rule (operator selection AND the spike-pinned
  0.146.0 version floor, failing toward PTY on anything unparseable), resolved
  once into DaemonConfig.CodexAppServerActive and read by BOTH the launch router
  and the certification advertisement, so the advertised policy version and the
  transport actually used can never diverge.
- CodexHostedAgentRuntimeFactory: the only "codex" entry in the vendor->factory
  dictionary. Routes review-flow launches to codex app-server when active; every
  interactive launch, and all launches under the PTY default, delegate to the
  wrapped PTY factory byte-identically. Reuses CodexLauncher.Prepare (fail-closed
  hooks + TrustWorktree) + BuildAppServerLaunchArgs + the PTY env assembly, and
  spawns via a seam (real Process / shared AcpChildProcess in prod, a fake peer
  in tests). Capability advertisement delegates to the PTY factory unchanged —
  the transport swaps the launch mechanism, not the native-tool-clamp containment.
- DaemonConfig.CodexTransport (KCAP_CODEX_TRANSPORT, default pty) + the resolved
  CodexAppServerActive (probed only when app-server is selected, so a PTY daemon
  pays no startup probe). DaemonRunner advertises codex-appserver-unattended-v1
  vs codex-unattended-v1 from that one field.

22 tests: the decision truth table (floor, tolerance, case/trim), and factory
routing — PTY delegation for pty-transport and for interactive-under-active, the
app-server route producing a CodexAppServerHostedAgentRuntime via the seam, and
fail-closed on missing hooks. Existing capability-advertisement suites stay green
(default config advertises codex-unattended-v1 unchanged); AOT clean.

Part of the AI-1761 app-server hosted-agent runtime.
@linear-code

linear-code Bot commented Aug 18, 2026

Copy link
Copy Markdown

AI-1761

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add opt-in Codex app-server transport for hosted reviewers

✨ Enhancement ⚙️ Configuration changes 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Adds an opt-in JSON-RPC app-server runtime for unattended Codex reviewers.
• Routes eligible launches using one version-gated transport decision while preserving PTY defaults.
• Validates protocol behavior with pipe-based tests and a gated live smoke test.
Diagram

sequenceDiagram
    actor Operator
    participant Runner as Daemon Runner
    participant Decision as Transport Decision
    participant Factory as Codex Factory
    participant PTY as PTY Runtime
    participant Runtime as App Runtime
    participant Codex as Codex Server
    Operator->>Runner: Select transport
    Runner->>Decision: Check selection and version
    Decision-->>Runner: Active or PTY fallback
    Runner->>Factory: Provide resolved config
    alt Active review flow
        Factory->>Runtime: Start hosted reviewer
        Runtime->>Codex: Spawn over stdio
        Runtime->>Codex: Initialize and check hooks
        Runtime->>Codex: Start thread and turns
        Codex-->>Runtime: Results and notifications
    else Interactive or inactive
        Factory->>PTY: Delegate launch
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Extract a shared generic JSON-RPC connection
  • ➕ Reduces duplication between ACP and Codex transports.
  • ➕ Centralizes correlation, framing, write serialization, and malformed-frame handling.
  • ➕ Makes future stdio JSON-RPC integrations cheaper.
  • ➖ Broadens this PR into a risky refactor of the established ACP path.
  • ➖ ACP reconnect behavior and Codex one-shot semantics complicate a clean abstraction.
  • ➖ Could delay the opt-in rollout and make protocol regressions harder to isolate.

Recommendation: Keep the PR's lean Codex-specific connection for this opt-in rollout. Reusing the shared wire envelopes while avoiding ACP reconnect semantics limits risk; a generic transport should only be extracted later if another integration validates the abstraction boundary.

Files changed (13) +2354 / -8

Enhancement (5) +1174 / -8
DaemonRunner.csResolve and wire the Codex app-server transport +32/-8

Resolve and wire the Codex app-server transport

• Reads KCAP_CODEX_TRANSPORT, applies the version floor, and registers the composite Codex runtime factory. Advertises the app-server launcher policy only when the same resolved activation flag controls routing.

src/Capacitor.Cli.Daemon/DaemonRunner.cs

CodexAppServerConnection.csImplement resilient JSON-RPC stdio transport +413/-0

Implement resilient JSON-RPC stdio transport

• Adds newline-delimited JSON-RPC request correlation, serialized writes, notification dispatch, server-request responses, cancellation, and pending-request faulting. Malformed frames are isolated so one bad message cannot terminate the read loop.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs

CodexAppServerHostedAgentRuntime.csHost unattended Codex reviews through app-server +495/-0

Host unattended Codex reviews through app-server

• Implements the app-server lifecycle from initialization and hook trust preflight through threads and turns. It declines unexpected approvals, retries bounded-ingress errors, captures token usage, tracks activity, and unblocks turns when transport ends.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs

CodexHostedAgentRuntimeFactory.csRoute Codex launches between app-server and PTY +168/-0

Route Codex launches between app-server and PTY

• Adds the single Codex factory that selects app-server for activated review flows and delegates everything else unchanged to PTY. It builds the reviewer posture, environment, process, and app-server connection.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexHostedAgentRuntimeFactory.cs

CodexTransportDecision.csCentralize version-gated transport eligibility +66/-0

Centralize version-gated transport eligibility

• Defines the app-server selection rule and pinned Codex 0.146.0 floor. Version parsing tolerates common CLI prefixes and fails safely toward PTY.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexTransportDecision.cs

Bug fix (1) +11 / -0
CodexAppServerPosture.csRender the correct thread sandbox wire shape +11/-0

Render the correct thread sandbox wire shape

• Adds validation and rendering for thread/start's coarse sandbox string. This fixes use of the incompatible sandbox-policy object shape accepted only by turn/start.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerPosture.cs

Tests (6) +1157 / -0
CodexAppServerConnectionTests.csTest JSON-RPC transport concurrency and resilience +418/-0

Test JSON-RPC transport concurrency and resilience

• Covers response correlation, interleaving, errors, notifications, cancellation, server-request guarantees, raw IDs, malformed frames, and connection termination using in-memory pipes.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerConnectionTests.cs

CodexAppServerHostedAgentRuntimeTests.csTest app-server runtime lifecycle and safety behavior +206/-0

Test app-server runtime lifecycle and safety behavior

• Exercises handshake, hook trust restart and failure paths, turn completion, approval declines, usage capture, backpressure retries, failed turns, and unsupported terminal input.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerHostedAgentRuntimeTests.cs

CodexAppServerLiveSmokeTests.csAdd gated real Codex protocol smoke test +92/-0

Add gated real Codex protocol smoke test

• Runs initialize, hooks/list, and thread/start against an installed Codex app-server in an isolated CODEX_HOME. The environment-gated test detects protocol schema drift without spending a model turn.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerLiveSmokeTests.cs

CodexHostedAgentRuntimeFactoryTests.csTest composite Codex factory routing +182/-0

Test composite Codex factory routing

• Verifies PTY delegation for inactive and interactive launches and app-server routing for activated review flows. Also confirms missing hooks fail before process spawning.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs

CodexTransportDecisionTests.csTest transport selection and version floor +34/-0

Test transport selection and version floor

• Covers the transport/version truth table, tolerant version formats, exact floor behavior, and fail-safe PTY fallback.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexTransportDecisionTests.cs

FakeCodexAppServer.csAdd scriptable in-process Codex app-server +225/-0

Add scriptable in-process Codex app-server

• Provides a duplex-pipe JSON-RPC peer for runtime and factory tests. It simulates hooks, threads, turns, approvals, usage notifications, backpressure, interrupts, and protocol responses.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/FakeCodexAppServer.cs

Other (1) +12 / -0
DaemonConfig.csAdd Codex transport selection and resolved activation state +12/-0

Add Codex transport selection and resolved activation state

• Adds the operator-facing Codex transport setting, defaulting to PTY. Introduces a startup-resolved activation flag shared by routing and capability advertisement.

src/Capacitor.Cli.Daemon/DaemonConfig.cs

@qodo-code-review

qodo-code-review Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Codex logic escapes vendor folder ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
DaemonRunner directly implements the Codex-specific transport and version-floor decision outside
Harness/Codex/. This couples shared daemon startup code to vendor implementation details beyond
registration.
Code

src/Capacitor.Cli.Daemon/DaemonRunner.cs[R159-161]

+        config.CodexAppServerActive =
+            string.Equals(config.CodexTransport?.Trim(), Harness.Codex.CodexTransportDecision.AppServer, StringComparison.OrdinalIgnoreCase)
+            && Harness.Codex.CodexTransportDecision.MeetsFloor(ProbeCliVersion(config.CodexPath));
Evidence
PR Compliance ID 2 requires vendor-specific implementations to remain under Harness/<Vendor>/. The
cited DaemonRunner expression directly evaluates Codex transport selection and its version floor
in a shared file.

CLAUDE.md: Vendor-Specific Code Must Be Isolated Under Harness/&lt;Vendor&gt;/ (No Cross-Vendor Mixing)
src/Capacitor.Cli.Daemon/DaemonRunner.cs[152-161]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexTransportDecision.cs[15-43]

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

## Issue description
Codex-specific transport resolution was implemented in shared daemon startup code rather than under the vendor harness.

## Issue Context
Shared startup code should only register the vendor integration; the operator-selection and version-floor logic belongs under `Harness/Codex/`.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/DaemonRunner.cs[152-161]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexTransportDecision.cs[15-43]

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


2. Codex transport undocumented in README ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The PR adds the operator-facing KCAP_CODEX_TRANSPORT setting and a Codex version prerequisite
without documenting either in README.md. The existing daemon configuration section still lists
only KCAP_CODEX_PATH, leaving operators unable to discover or correctly enable the new transport.
Code

src/Capacitor.Cli.Daemon/DaemonRunner.cs[R152-153]

+        if (Environment.GetEnvironmentVariable("KCAP_CODEX_TRANSPORT") is { Length: > 0 } envCodexTransport)
+            config.CodexTransport = envCodexTransport;
Evidence
PR Compliance ID 11 requires README updates for new user-facing behavior and prerequisites. The new
environment variable is consumed by daemon startup, while the README's daemon configuration and
environment-variable documentation mentions only the Codex binary path and contains no
KCAP_CODEX_TRANSPORT entry.

CLAUDE.md: Keep User-Facing CLI Documentation in README.md Updated With CLI Surface Changes
src/Capacitor.Cli.Daemon/DaemonRunner.cs[152-161]
README.md[49-99]
README.md[1284-1303]

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 `KCAP_CODEX_TRANSPORT` daemon setting and its app-server version floor are absent from user-facing documentation.

## Issue Context
Update the README's getting-started guidance and daemon CLI/configuration section with the default, accepted values, review-flow scope, fallback behavior, and minimum Codex version.

## Fix Focus Areas
- README.md[49-99]
- README.md[1284-1303]
- src/Capacitor.Cli.Daemon/DaemonRunner.cs[152-161]

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


3. Failed startup leaks child ✓ Resolved 🐞 Bug ☼ Reliability
Description
StartAppServerAsync does not dispose the newly created runtime when its startup handshake throws,
even though spawning occurs before several fallible initialization and hook checks. Because the
runtime is never returned to the orchestrator, its process and protocol streams can remain alive
indefinitely.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexHostedAgentRuntimeFactory.cs[R93-98]

+        var runtime = new CodexAppServerHostedAgentRuntime(
+            spawn, launch, ctx.ActivityClock,
+            _loggerFactory.CreateLogger<CodexAppServerHostedAgentRuntime>());
+
+        await runtime.StartAsync(ct).ConfigureAwait(false);
+        return new HostedRuntimeStart(runtime, McpConfigPath: null);
Evidence
The factory constructs and starts the runtime without a failure cleanup block. Startup assigns the
child resources before awaiting initialization and can subsequently throw during hook or thread
setup, while cleanup exists only in DisposeAsync on the runtime that the caller never receives.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexHostedAgentRuntimeFactory.cs[93-98]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[132-167]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[169-187]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[438-460]

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

## Issue description
Dispose the app-server runtime when `StartAsync` fails before ownership is transferred to the orchestrator.

## Issue Context
The runtime stores the spawned process and connection before initialization, so all unsuccessful startup paths require explicit cleanup in the factory.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexHostedAgentRuntimeFactory.cs[93-98]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[132-187]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[438-460]

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


View high (5)
4. NotInParallel uses group key 📘 Rule violation ☼ Reliability
Description
The tests mutating HOME and CODEX_HOME use keyed [NotInParallel("HomeEnvVarMutation")] rather
than the required bare [NotInParallel]. They may therefore overlap with unrelated tests that
access the same process-global environment.
Code

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[114]

+    [NotInParallel("HomeEnvVarMutation")]
Evidence
PR Compliance ID 5 explicitly requires bare [NotInParallel], not only a group key, for tests
manipulating global state. Both cited tests alter HOME and CODEX_HOME while carrying only the
keyed form.

CLAUDE.md: Capture Console Output Only Via ConsoleOutput.*Capture() and Mark Tests [NotInParallel]
test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[113-125]
test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[147-157]

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

## Issue description
Tests that mutate process-global environment variables use a keyed `NotInParallel` group instead of disabling parallel execution globally.

## Issue Context
Replace both keyed annotations with bare `[NotInParallel]` as required for tests manipulating process-global state.

## Fix Focus Areas
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[113-115]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[147-149]

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


5. Runtime immediately finalizes ✓ Resolved 🐞 Bug ≡ Correctness
Description
ReadOutputAsync returns an already-completed enumerable, but AgentOrchestrator interprets
completion of every runtime's output enumerable as process termination and finalizes the agent.
Every app-server reviewer is therefore torn down immediately after registration, even while its
child process is running.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[R400-404]

+    public IAsyncEnumerable<byte[]> ReadOutputAsync(CancellationToken ct = default) => Empty(ct);
+
+    static async IAsyncEnumerable<byte[]> Empty([EnumeratorCancellation] CancellationToken ct) {
+        await Task.CompletedTask.ConfigureAwait(false);
+        yield break;
Evidence
The new implementation immediately executes yield break. The orchestrator unconditionally starts
ReadAgentOutputAsync, and its finally calls FinalizeAgentRunAsync whenever enumeration ends;
the existing ACP runtime explicitly waits for logical termination to avoid this exact failure.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[398-405]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2084-2086]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2367-2381]
src/Capacitor.Cli.Daemon/Services/AcpHostedAgentRuntime.cs[1357-1383]

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 Codex app-server runtime's empty output enumerable completes immediately, causing the orchestrator to finalize a live agent.

## Issue Context
Non-terminal runtimes must yield no bytes while keeping their enumerable open until runtime termination or caller cancellation, as the ACP runtime does.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[400-405]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2296-2381]
- src/Capacitor.Cli.Daemon/Services/AcpHostedAgentRuntime.cs[1357-1383]

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


6. TempDir paths built manually ✓ Resolved 📘 Rule violation ☼ Reliability
Description
WriteWorktreeHooks manually combines a path beneath TempDir and creates the directory and file
through System.IO. This bypasses the required TempDir.CreateDir, CreateFile, and PathTo
helpers.
Code

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[R172-174]

+        var dir = System.IO.Path.Combine(worktreePath, ".codex");
+        System.IO.Directory.CreateDirectory(dir);
+        System.IO.File.WriteAllText(System.IO.Path.Combine(dir, "hooks.json"), """
Evidence
PR Compliance ID 4 prohibits manual Path.Combine, Directory.CreateDirectory, and
File.WriteAllText beneath a TempDir. The cited helper performs all three operations using the
worktree temporary directory's raw path.

CLAUDE.md: Use Helpers TempDir for Test Directories and Dispose It Correctly
test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[118-125]
test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[171-180]

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 factory tests manually create directories and files beneath a `TempDir` path.

## Issue Context
Pass the owning `TempDir` to the helper and use its `CreateDir`, `CreateFile`, or `PathTo` APIs so cleanup and path handling remain standardized.

## Fix Focus Areas
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[118-125]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[152-157]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[171-180]

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


7. ValueKind bypasses JSON extensions 📘 Rule violation ⚙ Maintainability
Description
The new app-server parsing code performs direct JsonElement.ValueKind checks instead of using the
shared JsonElementExtensions. This violates the required AOT-safe JSON inspection pattern in
multiple production paths.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[R186-188]

+                if (root.ValueKind != JsonValueKind.Object) {
+                    _logger.LogDebug("app-server: skipping non-object frame (kind={Kind})", root.ValueKind);
+                    return;
Evidence
PR Compliance ID 6 requires JSON value inspection through JsonElementExtensions. The connection
checks root.ValueKind directly, while the runtime's new helper methods repeat direct object,
string, number, and array comparisons.

CLAUDE.md: Avoid Dynamic-Code JSON Patterns: Use JsonElementExtensions and Avoid JsonArray Collection Expressions
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[186-197]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[237-256]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[464-478]

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 Codex app-server parser directly compares `JsonElement.ValueKind` throughout its production parsing paths.

## Issue Context
Use the shared `JsonElementExtensions` helpers such as `IsObject`, `IsArray`, `IsString`, `Str`, `Num`, `Obj`, and `Arr` instead of direct kind inspection.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[186-197]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[237-256]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[464-478]

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


8. Restart permanently disables waiting ✓ Resolved 🐞 Bug ≡ Correctness
Description
_runLoopEnded is created once for the entire runtime, so the intentional hook-trust teardown
permanently completes it before the replacement child starts. All subsequent WaitForTurnIdleAsync
calls return immediately and can allow borrowed-snapshot refreshes while the replacement child's
turn is still active.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[R80-85]

+    readonly object              _turnGate = new();
+    readonly TaskCompletionSource _runLoopEnded = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+    CodexAppServerConnection? _connection;
+    IAcpProcess?              _process;
+    Task                      _runLoop = Task.CompletedTask;
Evidence
The lifetime-wide TCS is completed by every connection loop, while startup explicitly tears down and
respawns during hook seeding. WaitForTurnIdleAsync treats that permanently completed task as
successful settlement, and the orchestrator relies on this wait before refreshing borrowed
snapshots.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[80-85]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[142-150]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[190-199]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[297-310]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3334-3347]

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

## Issue description
Make the transport-ended signal specific to each spawned app-server connection rather than permanent across the runtime.

## Issue Context
Hook trust can intentionally replace the initial child, and turn waits after that restart must observe only the replacement connection's lifecycle.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[80-85]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[142-150]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[169-199]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[297-310]

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



Remediation recommended

9. Prereleases bypass version floor ✓ Resolved 🐞 Bug ≡ Correctness
Description
ParseSemver discards prerelease identifiers and compares only numeric components, so
0.146.0-rc.1 is accepted as meeting the stable 0.146.0 floor. This enables app-server for a
build below the behaviorally verified release floor.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexTransportDecision.cs[R48-51]

+        // Find the first token shaped `<digits>.<digits>[.<digits>]` so trailing/leading words
+        // (a `codex-cli` prefix, a pre-release suffix) do not defeat the parse.
+        foreach (var token in version.Split([' ', '\t', '\r', '\n', '-', '_'], StringSplitOptions.RemoveEmptyEntries)) {
+            var t = token.TrimStart('v', 'V');
Evidence
App-server activation directly depends on MeetsFloor. The parser splits tokens on - and returns
only major/minor/patch, while MeetsFloor has no prerelease comparison, causing a prerelease at the
same numeric version to pass.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexTransportDecision.cs[24-43]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexTransportDecision.cs[45-63]
test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexTransportDecisionTests.cs[8-33]

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

## Issue description
Reject prerelease versions that precede the pinned stable app-server floor.

## Issue Context
The parser currently removes text after `-`, making a release candidate numerically identical to the final stable release.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexTransportDecision.cs[24-63]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexTransportDecisionTests.cs[8-33]

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


10. Malformed requests receive no response ✓ Resolved 🐞 Bug ☼ Reliability
Description
HandleServerRequestAsync reads method as a string before entering its fallback handling, so a
non-string method throws into DispatchLineAsync, which merely logs and skips the frame. The server
receives no response for its request ID and can remain blocked waiting for one.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[R275-277]

+    async Task HandleServerRequestAsync(JsonElement root, JsonElement idElement, JsonElement methodElement, CancellationToken ct) {
+        var method        = methodElement.GetString() ?? "";
+        var paramsElement = root.TryGetProperty("params", out var p) ? p.Clone() : (JsonElement?) null;
Evidence
Frames with both id and method are classified as requests regardless of method type. GetString
then throws before response construction, and the outer dispatch catch only logs the exception,
contradicting the documented exactly-one-response guarantee.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[199-223]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[267-277]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[313-327]

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

## Issue description
Ensure malformed server requests still receive a JSON-RPC error response keyed to their original ID.

## Issue Context
Wrong-typed request fields currently throw before the response fallback logic, despite the connection's documented always-answer invariant.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[199-223]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[267-327]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexAppServerConnectionTests.cs[194-265]

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


11. Completed turn stays in-flight ✓ Resolved 🐞 Bug ☼ Reliability
Description
If turn/completed arrives before the turn/start response, the notification clears the pending
turn and sets the activity clock false, after which StartTurnAsync unconditionally sets the clock
back to true. The completed reviewer then remains classified as having a held turn and is eventually
reaped as turn_wedged instead of following normal idle handling.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[R280-283]

+        var turnId = result.TryGetProperty("turn", out var turn) ? Str(turn, "id") : null;
+        lock (_turnGate) _currentTurnId = turnId;
+
+        _clock?.SetTurnInFlight(true);
Evidence
Completion is armed before sending, but the turn ID and true clock state are installed after
awaiting the response. A notification during that await clears the TCS and sets the clock false; the
later unconditional true transition reverses it, and the reaper gives TurnInFlight agents wedge
rather than idle treatment.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[264-289]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[314-344]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[823-833]

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

## Issue description
Prevent a completion notification that precedes the start response from leaving `AgentActivityClock.TurnInFlight` set to true.

## Issue Context
The completion TCS and turn ID are synchronized under `_turnGate`, but the clock transition currently occurs after that state can already have been completed and cleared.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[264-289]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[314-344]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[801-833]

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


View medium (1)
12. Transport comment restates code ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The startup comment narrates the exact boolean decision and conditional probe performed immediately
below it instead of documenting only non-obvious rationale. Its capitalized emphasis and multi-line
implementation walkthrough violate the minimal, why-focused comment requirement.
Code

src/Capacitor.Cli.Daemon/DaemonRunner.cs[R155-158]

+        // Resolve the effective Codex transport ONCE (operator selection AND the version floor) into
+        // the field both the launch router and the certification advertisement read — see
+        // CodexTransportDecision. Only probe when app-server is actually selected, so a PTY daemon
+        // (the default) pays no startup probe.
Evidence
PR Compliance ID 12 requires comments to be minimal and focused on rationale. The cited comment
repeats the operator-selection conjunction, destination field, consumers, and conditional probing
that are already evident from the following assignment.

CLAUDE.md: Comments Must Be Minimal, Non-Redundant, and Focus on the 'Why'
src/Capacitor.Cli.Daemon/DaemonRunner.cs[155-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 transport-resolution comment paraphrases the code's mechanics across four lines.

## Issue Context
Remove the implementation narration and retain at most a concise explanation of why the version probe is skipped for the default PTY transport.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/DaemonRunner.cs[155-161]

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


Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Daemon/DaemonRunner.cs Outdated
Comment on lines +186 to +188
if (root.ValueKind != JsonValueKind.Object) {
_logger.LogDebug("app-server: skipping non-object frame (kind={Kind})", root.ValueKind);
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

2. valuekind bypasses json extensions 📘 Rule violation ⚙ Maintainability

The new app-server parsing code performs direct JsonElement.ValueKind checks instead of using the
shared JsonElementExtensions. This violates the required AOT-safe JSON inspection pattern in
multiple production paths.
Agent Prompt
## Issue description
The new Codex app-server parser directly compares `JsonElement.ValueKind` throughout its production parsing paths.

## Issue Context
Use the shared `JsonElementExtensions` helpers such as `IsObject`, `IsArray`, `IsString`, `Str`, `Num`, `Obj`, and `Arr` instead of direct kind inspection.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[186-197]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[237-256]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[464-478]

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


// ── App-server route (spawn seam; isolated HOME so TrustWorktree touches nothing real) ──────
[Test]
[NotInParallel("HomeEnvVarMutation")]

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

4. notinparallel uses group key 📘 Rule violation ☼ Reliability

The tests mutating HOME and CODEX_HOME use keyed [NotInParallel("HomeEnvVarMutation")] rather
than the required bare [NotInParallel]. They may therefore overlap with unrelated tests that
access the same process-global environment.
Agent Prompt
## Issue description
Tests that mutate process-global environment variables use a keyed `NotInParallel` group instead of disabling parallel execution globally.

## Issue Context
Replace both keyed annotations with bare `[NotInParallel]` as required for tests manipulating process-global state.

## Fix Focus Areas
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[113-115]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexHostedAgentRuntimeFactoryTests.cs[147-149]

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

Comment thread src/Capacitor.Cli.Daemon/DaemonRunner.cs
Comment thread src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Harness/Codex/CodexTransportDecision.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs Outdated
Round-1 findings from the codex code-review flow on #582 (borrowed workspace):

P1 (correctness / leaks):
- ReadOutputAsync no longer completes immediately. The orchestrator treats its
  completion as the finalize trigger, so an instant yield-break finalized (and
  killed) every reviewer seconds after launch. It now waits on a whole-runtime
  terminal signal (or the caller's cancellation), mirroring the ACP runtime.
- The terminal signal is scoped correctly across the hook-trust restart. The
  reused single TCS was permanently completed by child 1's read-loop end,
  making every WaitForTurnIdle on child 2 return immediately. Introduced
  _runtimeTerminal (fires only on the LIVE child's death / dispose, gated by a
  _restarting window), a per-child read-loop CTS so teardown unblocks the loop,
  and TeardownChildAsync now awaits the retiring loop before installing child 2.
- The turn-completion race is closed: the current turn id is unknown during the
  turn/start request window, so a stray/fast turn/completed could clear the new
  round's waiter. Early completions are now stashed by id and applied only if
  they match the started turn; a start response without a turn id is rejected;
  the turn-in-flight clock is armed before the send and cleared only on the real
  matching completion.
- The factory disposes the runtime if StartAsync throws after spawning a child
  (fail-closed hook/protocol paths), so a live Codex child is never leaked.

P2:
- turn/start now carries the requested reasoning effort (max -> xhigh mapping),
  which was silently dropped on the app-server route.
- A wrong-typed inbound server request (non-string method) is answered -32600
  with the original id instead of stranding it (the "always one response"
  invariant now covers the dispatch/validation path).
- The version-floor gate rejects prereleases / build-metadata / non-numeric
  parts (0.146.0-rc.1 is BELOW the verified 0.146.0 release), failing toward PTY
  rather than normalizing an unverified build upward into the containment-
  sensitive transport.

Adds tests for each fix; existing Codex suites + the live smoke stay green, AOT clean.

Part of the AI-1761 app-server hosted-agent runtime.
- Move the Codex transport/version-floor resolution out of DaemonRunner into
  CodexTransportDecision.ResolveActive (vendor logic stays under Harness/Codex/;
  DaemonRunner only reads the env var and registers), and trim the startup
  comment to the why (probe deferred behind the selection so a PTY daemon pays
  nothing).
- Adopt the shared JsonElementExtensions (Obj/Arr/Str/Num) in the app-server
  runtime parsing, removing the duplicate local Str/Long helpers. (The
  connection keeps direct ValueKind checks to match its sibling AcpConnection
  transport.)
- Factory tests use the TempDir helpers (CreateFile / PathTo) instead of
  System.IO.Path.Combine + Directory/File.
- Document KCAP_CODEX_TRANSPORT (default pty, app-server opt-in, 0.146.0 floor,
  review-flow-only, rollback) in the README daemon-config section.

No behavior change; 56 Codex unit tests + the live smoke stay green, AOT clean.

Part of the AI-1761 app-server hosted-agent runtime.
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Thanks — addressed the Qodo review (0 bugs, 6 rule violations) in the latest commit:

Fixed:

  1. Codex logic escapes vendor folder — moved the transport/version-floor resolution into CodexTransportDecision.ResolveActive; DaemonRunner now only reads KCAP_CODEX_TRANSPORT and registers, and the startup comment is trimmed to the why.
  2. ValueKind vs JsonElementExtensions (runtime) — the app-server runtime now uses the shared Obj/Arr/Str/Num helpers, removing its duplicate local Str/Long.
  3. TempDir paths built manually — the factory tests now use TempDir.CreateFile/PathTo.
  4. KCAP_CODEX_TRANSPORT undocumented — added to the README daemon-config section (default pty, app-server opt-in, 0.146.0 floor, review-flow-only, rollback).
  5. Comment restates code — folded into Auto-install Claude Code plugin during setup #1; the narration is gone.

Kept, with rationale:

  • (2) ValueKind in CodexAppServerConnection — the connection is a deliberately close mirror of its sibling transport Acp/AcpConnection.cs, which uses direct ValueKind checks throughout; keeping them identical keeps the two transports diffable. The extensions are adopted in the runtime, where they removed real duplication.
  • (4) [NotInParallel("HomeEnvVarMutation")] keyed — this matches the established repo pattern: the sibling CodexConfigWriterTests uses the same "HomeEnvVarMutation" key, so all HOME/CODEX_HOME-mutating tests serialize against each other under one group (a bare [NotInParallel] would coordinate differently). The CLAUDE.md bare-[NotInParallel] rule is specifically about process-global console capture (TUnit0055).

56 Codex unit tests + the gated live smoke against real codex 0.146 stay green; AOT clean.

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.

Codex app-server: hosted-agent runtime + transport + factory

1 participant