Skip to content

Codex app-server: orchestrator deferred-first-turn source-claim sequence - #612

Merged
realtonyyoung merged 4 commits into
mainfrom
tonyyoung/ai-1762-orchestrator-source-claim
Aug 19, 2026
Merged

realtonyyoung merged 4 commits into
mainfrom
tonyyoung/ai-1762-orchestrator-source-claim

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

What

The daemon driver that wires the hosted-Codex source-claim handshake (server AcpSessionSourceClaim/ConfirmSessionLaunch, merged in kcap-server #1530) into the launch lifecycle. This is the next §2.5 piece after the runtime contract (#606).

Dormant until the activation slice sets deferFirstTurn on CodexHostedAgentRuntimeFactory — the production factory is unchanged, so no shipping launch is affected. The fake test runtime exercises the whole sequence.

The sequence (fired fire-and-forget after RegisterAgentAsync, for a runtime whose RequiresSourceClaimBeforeFirstTurn is true)

  1. AcpSessionSourceClaim — durable claim (server binds + writes the ownership ledger row).
  2. On Rejected (or a pre-source-claim server's method-not-found HubException) → coded launch failure + teardown.
  3. On Bound → start the transcript forwarder without re-binding (the claim superseded AcpSessionStarted) → BeginFirstTurnAsync (dispatch the held first turn) → confirm in a background loop.

Pieces

  • StartForwarderAfterBind — factored out of StartAcpForwardingAsync (its post-bind body: liveness/TOCTOU guards, local reconnect-binding registration, forwarder build+run). The claim path reuses it verbatim without the AcpSessionStarted bind. The daemon forwarder ignores the claim's AcceptedSeq — resume is server-driven via the AcpBatchAck, exactly as before. The 8 existing forwarding tests still pass, proving the factoring is behaviour-preserving.
  • StartEnvelopeSourcedSessionAsync — the sequence above; every step gated on the agent's AcpCts so a finalize aborts cleanly; contains all its own faults (no outer catch).
  • FailEnvelopeSourcedLaunchAsync = CleanupAgentAsync (single-flight — disposes the runtime, releases the slot, unregisters) + LaunchFailedAsync — the proven launch outer-catch composition, never FinalizeAgentRunAsync (which classifies a still-live process by exit code).
  • ConfirmSessionLaunchLoopAsync — background confirm, retrying only transient failures on a paced cadence for the agent's lifetime and never tearing it down (Confirmed/AlreadyConfirmed done; Superseded/NotFound stop; a lost confirm leaves the row provisional for the server's own recovery to settle).
  • HandleLaunchAgent branches on RequiresSourceClaimBeforeFirstTurn; Cursor keeps the single-phase bind-then-forward path.

Tests

AgentOrchestratorSourceClaimTests (3), asserting through the deterministic AcpCallOrder/signal seams (never Task.Delay):

  • happy path: claims → forwards without re-binding (AcpSessionStartedCalls empty, no bind:) → first-turns once → confirms with the claim token;
  • Rejected claim → teardown (unregister + dispose), no first turn, no confirm;
  • method-not-found → coded launch failure + teardown.

Test doubles extended: CaptureServerConnection (source-claim/confirm raw seams), FakeAcpRuntime + SpyAcpHostedAgentRuntimeFactory (deferred-first-turn).

Next

The activation slice flips deferFirstTurn/emitEnvelopeTranscript on the factory + wires Transcript: runtime + the codex-appserver-unattended-v2 capability + guard-1 env marker + the server guard-2 source-refusal — making this whole path live.

🤖 Generated with Claude Code

The daemon driver that wires the hosted-Codex source-claim handshake into the launch
lifecycle. Dormant until the activation slice sets deferFirstTurn on the factory (the
production factory is unchanged; the fake test runtime exercises the whole sequence).

- Factor StartAcpForwardingAsync's post-bind body (liveness/TOCTOU guards, local binding
  registration, forwarder build+run) into a shared StartForwarderAfterBind, so the
  source-claim path reuses it WITHOUT re-binding (the claim supersedes AcpSessionStarted).
  The daemon forwarder ignores the claim's AcceptedSeq — resume is server-driven via the
  AcpBatchAck, exactly as before.
- StartEnvelopeSourcedSessionAsync (fired fire-and-forget after RegisterAgentAsync for a
  runtime whose RequiresSourceClaimBeforeFirstTurn is true): AcpSessionSourceClaim →
  (Rejected/method-not-found: coded launch failure + teardown | Bound: start forwarder
  without re-binding → BeginFirstTurnAsync → confirm). Every step gated on the agent's
  AcpCts so a finalize aborts cleanly; contains all its own faults.
- FailEnvelopeSourcedLaunchAsync = CleanupAgentAsync (single-flight: disposes the runtime,
  releases the slot, unregisters) + LaunchFailedAsync — the proven outer-catch composition,
  never FinalizeAgentRunAsync (which classifies a still-live process by exit code).
- ConfirmSessionLaunchLoopAsync: background confirm, retrying only transient failures on a
  paced cadence for the agent's lifetime and NEVER tearing it down (Confirmed/AlreadyConfirmed
  done; Superseded/NotFound stop; a lost confirm leaves the row provisional for the server's
  own recovery to settle).
- HandleLaunchAgent branches on RequiresSourceClaimBeforeFirstTurn; Cursor keeps the
  single-phase bind-then-forward path.

Tests: AgentOrchestratorSourceClaimTests (3 — happy path claims-then-forwards-without-rebind-
then-first-turns-then-confirms, Rejected teardown, method-not-found teardown), asserting
through the deterministic AcpCallOrder/signal seams. The 8 existing forwarding tests still
green (the factoring is behaviour-preserving). CaptureServerConnection/FakeAcpRuntime/
SpyAcpHostedAgentRuntimeFactory gain the source-claim/confirm/deferred-first-turn doubles.
@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

AI-1762

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Orchestrate deferred first turns with ACP source claims

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Sequences durable claims, transcript forwarding, deferred first turns, and launch confirmation.
• Preserves bind-first forwarding for runtimes without the deferred-turn contract.
• Tests successful claims and teardown for rejected or unsupported claims.
Diagram

sequenceDiagram
    participant O as Agent Orchestrator
    participant S as Server API
    participant F as ACP Forwarder
    participant R as Hosted Runtime
    O->>S: Register agent
    O->>S: Source claim
    alt Claim bound
        O->>F: Start without rebind
        O->>R: Begin first turn
        loop Until terminal
            O->>S: Confirm launch
        end
    else Rejected or faulted
        O->>R: Cleanup runtime
        O->>S: Report launch failure
    end
Loading
High-Level Assessment

The approach is appropriate: the orchestrator already owns server communication, agent registration, cancellation, and teardown, so it is the correct layer for sequencing the claim and first turn. Extracting the existing post-bind forwarding logic avoids duplicate binding while preserving the legacy path; moving the handshake into the runtime would duplicate lifecycle responsibilities and complicate cleanup.

Files changed (5) +376 / -39

Enhancement (1) +180 / -38
AgentOrchestrator.csAdd deferred-first-turn source-claim orchestration +180/-38

Add deferred-first-turn source-claim orchestration

• Branches deferred runtimes into a claim-before-first-turn sequence while retaining legacy binding for other runtimes. Extracts shared post-bind forwarding, adds coded teardown for claim failures, and retries launch confirmation without affecting agent lifetime.

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

Tests (4) +196 / -1
AgentOrchestratorSourceClaimTests.csTest source-claim launch sequencing and failures +122/-0

Test source-claim launch sequencing and failures

• Adds deterministic tests proving claim, forwarding, first-turn, and confirmation ordering without duplicate binding. Also verifies rejected and unsupported claims dispose and unregister the agent without dispatching a first turn.

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

CaptureServerConnection.csAdd source-claim and confirmation test seams +45/-0

Add source-claim and confirmation test seams

• Captures claim and confirmation calls, ownership tokens, ordering, and completion signals. Supports configurable claim failures and confirmation outcomes for deterministic orchestrator tests.

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

FakeAcpRuntime.csModel deferred first-turn runtime behavior +21/-0

Model deferred first-turn runtime behavior

• Allows the fake runtime to advertise the source-claim requirement and records first-turn dispatches. It can also inject first-turn failures for lifecycle testing.

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

SpyAcpHostedAgentRuntimeFactory.csConfigure deferred runtime behavior in tests +8/-1

Configure deferred runtime behavior in tests

• Threads deferred-first-turn and dispatch-failure settings from the spy factory into created fake runtimes.

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

…lts (Copilot review)

It is awaited from the fire-and-forget source-claim task outside any try, so a teardown
fault (a cleanup or a LaunchFailed hub error) would escape as an unobserved task exception.
Wrap its body in try/catch + log; document the no-throw invariant.
@qodo-code-review

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Finalizer overwrites claim failure ✓ Resolved 🐞 Bug ≡ Correctness
Description
FailEnvelopeSourcedLaunchAsync disposes the runtime without first marking the agent terminal,
causing the already-running output reader's finalizer to classify and emit another Completed or
Failed status concurrently with LaunchFailedAsync. A later Completed status can clear the
coded source-claim failure and present a rejected launch as successful.
Code

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[R3825-3826]

+        await CleanupAgentAsync(agentId);
+        await _server.LaunchFailedAsync(agentId, reason);
Evidence
The launch starts ReadAgentOutputAsync independently, whose finally always enters
FinalizeAgentRunAsync. The new helper then disposes and unpublishes the runtime without setting a
terminal status; the finalizer's guard therefore permits exit-code classification and subsequent
status/run-stop emissions even after the source-claim failure report.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2115-2116]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2402-2410]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2519-2579]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4279-4301]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4339-4351]

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

## Issue description
Source-claim failure cleanup races the normal read-loop finalizer. Establish a terminal failed state and coordinate teardown before disposing the runtime so the finalizer cannot emit a later non-failure status or overwrite the coded launch failure.

## Issue Context
`ReadAgentOutputAsync` is already running when the asynchronous claim fails, and runtime disposal completes its read stream. `CleanupStarted` only prevents duplicate resource cleanup; it does not prevent final status, run-stop, or session-end emissions.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3821-3827]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2402-2410]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2519-2579]
- test/Capacitor.Cli.Daemon.Tests.Unit/Services/AgentOrchestratorSourceClaimTests.cs[59-121]

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


2. Codex logic escapes vendor harness ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The shared AgentOrchestrator now hard-codes a Codex-specific launch-failure reason outside
Harness/Codex. This couples shared orchestration code to one vendor and violates the required
vendor/driver separation.
Code

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3791]

+            await FailEnvelopeSourcedLaunchAsync(agent.Id, "codex app-server: source claim rejected");
Evidence
PR Compliance ID 1 requires vendor-specific implementation details to live under
Harness/<Vendor>/. The changed shared service explicitly describes this as a hosted-Codex path and
emits the hard-coded reason codex app-server: source claim rejected, despite residing under
Services/.

CLAUDE.md: Vendor-specific harness code must live only under Harness/<Vendor>/ and shared code must stay outside Harness
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2078-2084]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3787-3792]

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

## Issue description
`AgentOrchestrator` contains a hard-coded Codex-specific launch-failure reason even though shared code must remain vendor-neutral.

## Issue Context
The source-claim sequence may remain shared when expressed through runtime capabilities, but vendor-specific descriptions or behavior must be supplied from `Harness/Codex` through an appropriate abstraction.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3787-3793]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexHostedAgentRuntimeFactory.cs[1-1]

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


3. First-turn failure leaks agent ✓ Resolved 🐞 Bug ☼ Reliability
Description
A non-cancellation failure from forwarder setup or BeginFirstTurnAsync is only logged, leaving the
registered agent, child process, slot, and provisional source claim alive indefinitely.
BeginFirstTurnAsync can fault after unsealing and dispatching the turn, so the runtime does not
necessarily exit and trigger normal cleanup.
Code

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[R3810-3813]

+        } catch (Exception ex) {
+            // Forwarder-setup or first-turn failure: the confirm never fires, so the server closes the
+            // still-provisional session via Rule 2. Log and leave the agent's own lifecycle to reap it.
            LogAcpBindFailed(ex, agent.Id);
Evidence
The changed catch returns without invoking the newly added failure helper. The production runtime
unseals before awaiting its dispatch task, while runtime disposal only occurs through cleanup,
proving that a failed response can leave a still-running registered process without any cleanup
trigger.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3796-3814]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3817-3826]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[276-285]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4279-4301]

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

## Issue description
Post-claim forwarder setup and first-turn failures are swallowed after logging, leaving the registered runtime and process alive. Route these failures through a coded, single-flight teardown while preserving cancellation behavior and provisional-claim recovery semantics.

## Issue Context
`BeginFirstTurnAsync` unseals the dispatcher before awaiting the first-turn response, so a fault does not imply that the process exited. Teardown must also account for the reconnect binding that may already have been registered.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3796-3814]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[3821-3827]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[276-285]
- test/Capacitor.Cli.Daemon.Tests.Unit/Services/AgentOrchestratorSourceClaimTests.cs[20-121]

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


Grey Divider

Context sources
Review mode: ⚖️ Balanced: This adds substantial asynchronous launch-lifecycle and server-handshake logic with teardown, cancellation, forwarding, and confirmation behavior; it is risky but focused enough for one careful review rather than redundant extended passes.

Grey Divider

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

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

…view)

A BeginFirstTurnAsync failure after a successful claim previously only logged, leaving a
zombie agent holding a slot while the server's Rule-2 expiry closed the provisional row.
Tear the agent down instead (symmetric with the claim-failure path, immediate slot release);
the provisional row is still closed by Rule 2. New test: first-turn failure → LaunchFailed +
unregister + dispose, no confirm.
…reason (qodo)

- FailEnvelopeSourcedLaunchAsync now SetAgentStatus(Failed) BEFORE CleanupAgentAsync disposes
  the runtime — disposal ends the read loop, whose FinalizeAgentRunAsync would otherwise
  classify by exit code and emit a Completed/Failed status that races and could MASK the coded
  launch failure. A pre-set Failed makes that classification a no-op (the same guard the
  launch-verdict path uses). qodo #3 (finalizer overwrites claim failure).
- The rejected-claim reason is now vendor-neutral (no 'codex app-server:' prefix in shared
  orchestration code). qodo #1 (vendor/driver separation).
- qodo #2 (first-turn leak) was already addressed in the prior commit (tear down on a
  first-turn dispatch failure).

Tests 4/4 + 8 forwarding regression still green.
@realtonyyoung
realtonyyoung merged commit 7eb34b0 into main Aug 19, 2026
6 checks passed
@realtonyyoung
realtonyyoung deleted the tonyyoung/ai-1762-orchestrator-source-claim branch August 19, 2026 16:24
realtonyyoung added a commit that referenced this pull request Aug 19, 2026
…turn (qodo)

qodo reliability bug (latent in #612, armed by this activation): a launch that
finalized during the source-claim / forwarder setup could still release the held
first turn. StartForwarderAfterBind aborts silently when finalize cancelled acpCts
and/or removed the agent, but StartEnvelopeSourcedSessionAsync then called
BeginFirstTurnAsync unconditionally, and BeginFirstTurnAsync unsealed the dispatcher
(sending turn/start) BEFORE observing its token — so turn/start could race the
teardown.

Two-part fix: BeginFirstTurnAsync now ThrowIfCancellationRequested BEFORE Unseal (the
dispatch runs on CancellationToken.None and can't be recalled, so this is the only
safe gate); and StartEnvelopeSourcedSessionAsync re-checks the SAME liveness
StartForwarderAfterBind guards on (cancelled OR agent-removed) before dispatching, so
the agent-removed-but-token-live arm is covered too. Both land in the OCE/return arms
that already tear down or log-and-abort. Added Cancelled_BeginFirstTurn_does_not_
dispatch_the_held_turn. Also trimmed the activation test comment qodo flagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
realtonyyoung added a commit that referenced this pull request Aug 19, 2026
…erver (#624)

* [AI-1762] §2.6: activate envelope-sourced transcript for hosted app-server

Flip the app-server path from the dormant hook-sourced transcript to the live
envelope source. One envelopeSourced decision in StartAppServerAsync now drives all
four coupled facts so they can never desync: the guard-1 marker (BuildEnv →
KCAP_HOSTED_APPSERVER, stands the rollout watcher down), the runtime's envelope
emission (emitEnvelopeTranscript), the deferred first turn behind the source claim
(deferFirstTurn), and the forwarder wiring (Transcript: runtime on HostedRuntimeStart).

Everything downstream was already built and merged and keys off these: the
orchestrator drains the runtime's envelopes through AcpTranscriptForwarder and, seeing
RequiresSourceClaimBeforeFirstTurn, runs source-claim → BeginFirstTurn → confirm
(#612); guard-2 refuses any stray hook write server-side (#1547); death-close is the
existing finalize → EndAgentSession, and the 60-min rollout-idle fallback disengages
automatically because the stamped marker suppresses the hook's watcher (#617). So this
is a four-value flip in one method, not new machinery.

Scope: this activates the app-server path, which today is reviewers only (UsesAppServer
stays `active && IsReviewFlow`) — the interactive routing flip is deliberately the LAST
step, after §2.6 lifecycle + §2.7. Gated behind codex.transport=app-server (default
pty), so a default daemon is byte-unchanged; requires a server carrying #1530/#1547
(server-before-CLI); and AI-2110's live cert validates it before app-server becomes the
default. Only the dormant-marker factory test inverts (now asserts the marker is
stamped, the Transcript is wired, and the first turn is deferred).

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

* [AI-1762] §2.6: a cancelled source-claim must not dispatch the first turn (qodo)

qodo reliability bug (latent in #612, armed by this activation): a launch that
finalized during the source-claim / forwarder setup could still release the held
first turn. StartForwarderAfterBind aborts silently when finalize cancelled acpCts
and/or removed the agent, but StartEnvelopeSourcedSessionAsync then called
BeginFirstTurnAsync unconditionally, and BeginFirstTurnAsync unsealed the dispatcher
(sending turn/start) BEFORE observing its token — so turn/start could race the
teardown.

Two-part fix: BeginFirstTurnAsync now ThrowIfCancellationRequested BEFORE Unseal (the
dispatch runs on CancellationToken.None and can't be recalled, so this is the only
safe gate); and StartEnvelopeSourcedSessionAsync re-checks the SAME liveness
StartForwarderAfterBind guards on (cancelled OR agent-removed) before dispatching, so
the agent-removed-but-token-live arm is covered too. Both land in the OCE/return arms
that already tear down or log-and-abort. Added Cancelled_BeginFirstTurn_does_not_
dispatch_the_held_turn. Also trimmed the activation test comment qodo flagged.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant