Skip to content

test(server): support replay gates for ACP providers - #13594

Merged
juliusmarminge merged 3 commits into
t3code/codex-turn-mappingfrom
v2/grok-gates
Sep 25, 2026
Merged

juliusmarminge merged 3 commits into
t3code/codex-turn-mappingfrom
v2/grok-gates

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

ACP replay had no way to express frames that arrive after a turn settles. The replay agent emits every recorded inbound frame as soon as the one before it is sent, so a background task that outlives its root turn could not be replayed. The Codex and Claude testkits already honor release_replay_gate; the ACP harnesses ignored it. Recording had the matching gap: the recorder stopped when T3's run settled, before Grok's own wake turns.

Replay

  • Gates. makeAcpReplayRuntime takes the scenario's ProviderReplayGate and holds the replay agent's stdout lines at their labels. The agent writes one line per emit_inbound entry in transcript order, so the Nth line carries the Nth entry's label. Lines after a held one wait with it, which keeps wire order. The Grok and ACP registry harnesses pass the gate through and release everything on teardown, like the Codex harness. This is the existing gate model (beforeEmit / release); nothing new in the orchestrator.
  • Continuations. The Grok replay harness never gave the adapter ProviderContinuationRequests, so post-settle continuation was off in every Grok replay (postSettleContinuationEnabled was false). It now uses the same queue the continuation worker drains, like production. The registry adapter has no continuation support, in production either, so its harness only gains gates.
  • Finish timers. The adapter's finish debounce (3 s) and safety holds (25 s / 60 s) run on the replay TestClock. A new finish_held_run step finishes a run held open for background work on a receipt, not on wall-time quiet: the ACP adapter reports each armed finish debounce through a test hook (onDeferredFinalizeScheduled), which the Grok harness records on the gate. The step waits for that receipt, advances the TestClock by exactly the reported debounce, and returns once the run reaches its status. If a later frame re-arms the debounce instead, it takes the next receipt. No real-time polling window is left.
  • Teardown. The scenario releases every gate when it ends, whether it passed or failed. Before this, a failing gated fixture left frames held, and teardown waited on them until the test timed out.

Recording

  • The scenario still runs on the replay TestClock so dispatch order matches replay. The Grok session (openSession and its turn methods) runs on the wall clock, so the adapter's timers behave as they do live. Previously the recording stalled: the debounce waited on a TestClock nobody advanced.
  • The recording stays open (inside the scenario, before teardown) until Grok is idle. That means no session has a queued or running prompt (x.ai/queue/changed against turn_completed), no background task runs (background_tasks), every spawned subagent finished (subagent_spawned against subagent_finished), and 5 s pass without a frame. Without this, the transcript missed Grok's own wake turns.
  • Labels carry the prompt id of the frame they belong to (_meta.promptId, or turn_completed.prompt_id), and background task ids are normalized like session ids. A fixture can therefore gate on the start of a specific Grok wake turn.
  • A fixture that uses gates records its dispatches only: live, Grok and wall time pace the run. Other fixtures record with all their steps, as before.

Source

Grok schedules its own wake turns, and these are what the gates hold (xai-org/grok-build at f0e3be1):

  • task-completed-<task id>: crates/codegen/xai-grok-shell/src/tools/notification_bridge.rs ~423, after _x.ai/task_completed
  • notifications-<uuid v7>: batched monitor events, crates/codegen/xai-grok-shell/src/session/acp_session_impl/notification_drain.rs ~753
  • subagent-completed-<subagent id>: crates/codegen/xai-grok-shell/src/agent/subagent/spawn.rs ~524

Fixture: grok_monitor

Recorded live from Grok 1.0.41: a Monitor on for i in 1 2 3; do sleep 8; echo tick $i; done, and the root turn ends with ROOT_DONE while it runs. Replay asserts:

  • run 1 is held open; each tick shows the monitor running with the output so far
  • the monitor completes with all three ticks only at _x.ai/task_completed, and run 1 settles after that
  • Grok's reply to the finished monitor (held at a gate until run 1 settled) is replayed as a provider continuation, run 2 (agent:provider), the same way Claude and Codex background wakes project

With #13557's normalizer fix reverted, this fixture fails: the monitor completed before tick 1.

Verification

  • vp test run on OrchestratorReplayFixtures.integration.test.ts, .contract.test.ts, AcpAdapterV2.test.ts, XAiAcpExtension.test.ts, ProviderSwitch.integration.test.ts: 322 passed. The Grok replays passed on 3 consecutive runs.
  • All existing Grok and registry replays pass unchanged. The gate, finish-receipt and continuation wiring are inert when a fixture doesn't use them.
  • A failing gated fixture (a mutated grok_monitor that waits on a status it never reaches while a frame is held): with the teardown release, the step fails with OrchestratorV2ScenarioStepError once its own 60 s wait expires, and teardown is immediate. Without it, the same failure took 254 s and ended in two Test timed out in 120000ms.
  • vp exec tsc --noEmit -p . in apps/server: 0 error TS / warning TS. vp run knip:check: clean. vp lint on touched files: no new warnings. Diff grepped for renamed service imports and raw data in errors: none.
  • Not run: repo-wide suites.

Model: Claude Opus 5.5 (Claude Code)

🤖 Generated with Claude Code


Devin Review

@macroscopeapp

macroscopeapp Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Macroscope skipped reviewing this pull request. Per-review cost limit exceeded (workspace setting).

This review would cost an estimated $17.50, which exceeds your per-review limit of $15.00.

The top 3 files driving up this estimate:

File Diff Size Estimate
apps/server/src/orchestration-v2/testkit/fixtures/grok_monitor/grok_transcript.ndjson 312.63KB $15.63
apps/server/scripts/record-grok-acp-replay-fixture.ts 11.41KB $0.57
apps/server/src/orchestration-v2/testkit/OrchestratorScenario.ts 4.78KB $0.24

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 25, 2026
import { GROK_ACP_CANCEL_META } from "../../provider/acp/GrokAcpSupport.ts";
import { makeXAiPromptCompletionRuntime } from "../../provider/acp/XAiAcpExtension.ts";
import { layer as idAllocatorLayer, IdAllocatorV2 } from "../IdAllocator.ts";
import { ProviderContinuationRequests } from "../ProviderContinuationRequests.ts";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new service dependency uses a named import at the adapter boundary. Please import the module as * as ProviderContinuationRequests and acquire ProviderContinuationRequests.ProviderContinuationRequests to retain its public service namespace.

Posted via Macroscope — Effect Service Conventions

import { ACP_PROTOCOL } from "../src/orchestration-v2/Adapters/AcpAdapterV2.ts";
import * as IdAllocator from "../src/orchestration-v2/IdAllocator.ts";
import type { ProviderAdapterV2SessionRuntime } from "../src/orchestration-v2/ProviderAdapter.ts";
import { ProviderContinuationRequests } from "../src/orchestration-v2/ProviderContinuationRequests.ts";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new service dependency uses a named import at the recorder's adapter boundary. Please import the module as * as ProviderContinuationRequests and acquire ProviderContinuationRequests.ProviderContinuationRequests.

Posted via Macroscope — Effect Service Conventions

@macroscopeapp

macroscopeapp Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Would Approve

Macroscope's review found this PR approvable — This PR adds replay-gate and background-wake coverage for ACP/Grok test harnesses, with production adapter behavior unchanged unless optional test hooks are explicitly supplied. The large addition is chiefly fixture data and test infrastructure, with no product-default, schema, security, billing, deployment, or static-analysis changes.

Not approved because:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 4.9 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 20.4 KiB — 29.3 KiB ✅
Codex Live turn messages — 2 — 8 ✅
Claude Total thread wire — 4.9 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 20.8 KiB — 29.3 KiB ✅
Claude Live turn messages — 2 — 8 ✅

Baseline: unavailable · PR result: ca51972 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 106.1 KiB
  • Claude decoded thread snapshot: 106.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

ACP replay emitted every recorded inbound frame as soon as the one before
it was sent, so it could not express frames that arrive after a turn
settles. The Grok and ACP registry harnesses now honor the scenario's
replay gates like the Codex and Claude testkits: the runtime holds the
replay agent's inbound lines at their transcript labels until the
scenario releases them, and releases everything on teardown.

The Grok replay harness also wires the continuation request queue into
the adapter, as production does, so a fixture that runs the continuation
worker replays Grok's own wake turns as continuation runs. Before this,
post-settle continuation was off in every Grok replay.

The adapter's finish debounce runs on the replay test clock. A new
`advanceClockWhenQuiet` option on `await_run_status` advances the clock
each time no event has been stored for a short wall-clock window, so a
run held open for background work settles once its provider is quiet.

Recording background work:
- The scenario still runs on the replay test clock so dispatch order
  matches replay; the Grok session runs on wall time, so the adapter's
  finish debounce and safety holds behave as they do live.
- The recording stays open until no Grok session has a queued or running
  prompt, no background task runs, and every spawned subagent finished,
  so Grok's own wake turns (`task-completed-*`, `notifications-*`,
  `subagent-completed-*`) are captured.
- Frame labels carry the prompt id they belong to, and background task
  ids are normalized like session ids, so a fixture can gate on the
  start of one of Grok's wake turns.
- A fixture that gates records its dispatches only; live, Grok and wall
  time pace the run.

grok_monitor is the first gated ACP fixture: a live Grok Monitor that
outlives its root turn, held open until `_x.ai/task_completed`, with
Grok's reply to the finished monitor replayed as a continuation run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
juliusmarminge and others added 2 commits September 25, 2026 03:19
Review fixes for the ACP replay gates:

- A failing step left recorded frames held at a gate, and the harness's
  release finalizer only ran after provider shutdown, which waited on
  those frames: a failing gated fixture hung for two 120 s timeouts with
  no diagnostic. The scenario now releases every gate when it ends, so
  the step's own error reports in about a minute instead.
- `advanceClockWhenQuiet` polled wall time for a quiet window. It is
  replaced with a receipt: the adapter's new `onDeferredFinalizeScheduled`
  test hook reports each armed finish debounce, the Grok replay harness
  records it on the replay gate, and a `finish_held_run` step advances
  the test clock by exactly that debounce until the run settles. A rearm
  supersedes the previous receipt. The debounce is named
  (ACP_DEFERRED_FINALIZE_DEBOUNCE) and its rationale moved with it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@juliusmarminge
juliusmarminge merged commit fed2604 into t3code/codex-turn-mapping Sep 25, 2026
24 checks passed
@juliusmarminge
juliusmarminge deleted the v2/grok-gates branch September 25, 2026 22:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant