Skip to content

[orchestrator-v2] fix(orchestrator): Harden Grok v2 settlement, background task lifecycle, steer visibility, and images#3578

Merged
juliusmarminge merged 1 commit into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/grok-v2
Jul 20, 2026
Merged

[orchestrator-v2] fix(orchestrator): Harden Grok v2 settlement, background task lifecycle, steer visibility, and images#3578
juliusmarminge merged 1 commit into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/grok-v2

Conversation

@mwolson

@mwolson mwolson commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #3580

Summary

Harden Grok orchestration v2 so root turns complete correctly, steered user
messages stay visible, Stop then continue recovers from a poisoned ACP child,
async spawn_subagent / monitor project real lifecycle (not start ACKs), late
post-settle Grok traffic can land as a continuation run instead of being
dropped, screenshot / image attachments actually reach Grok instead of failing
turn start, and interrupt cleanup actually contains and reaps native Grok work
(cgroup kill / owned process ledger) instead of leaving zombie shells.

Follow-up commits soften non-Stop interrupts: steer / restart_active now cancels
and re-prompts the same live session instead of killing the CLI, a steer on a
settled held turn preserves the runtime (running subagents survive), and user
Stop keeps the unchanged hard kill + respawn.

Primary commits on fix/grok-v2 above t3code/codex-turn-mapping (the
feature story reviewers should start with):

  1. Grok specialty settlement / tools / post-settle continuation / images
  2. Interrupt process containment and cleanup hardening

Follow-up commits may land on top of those two. Treat them as reviewable
delta fixes, not a rewrite of the primary story. Integration snapshots should
still merge the whole branch, not tip-only cherry-pick.

The shared provider-continuation plumbing (C0) already landed in the base branch
alongside the Claude and Codex consumers.

What you'll notice

Steered messages stay visible

When you steer an in-flight Grok run, your message no longer flashes away
between server events. The timeline keeps the optimistic user row until the
committed user_message turn item lands.

Steering also works while a run is held open for background work: pre-fix,
steering a settled turn that deferred finalize was holding (a subagent still
running) froze the thread at Working, the steer text never reached Grok, and
later messages queued forever.

Grok multi-tool turns finish with tools and final text

Grok often emits a short assistant preamble, then tools, then more text.
Speculative idle settlement used to complete the turn after a short quiet and
call session/cancel, which froze T3 projection while the CLI kept working.
This PR disables Grok idle settle and terminalizes from real provider signals.

Stop then continue recovers instead of task_already_running

After you Stop a Grok run mid-turn, the next message restarts the Grok ACP child
before the recovery prompt, clearing zombie shell state.

Interrupt cleanup contains native work

Stop is not only an ACP cancel. On Linux with cgroup v2, delegated Grok launches
join a unique child cgroup and interrupt uses cgroup.kill as the authoritative
cleanup. When cgroup containment is unavailable, the owned-process ledger is the
fallback (Darwin process groups / Windows taskkill retained). ACP cancel is
generation-aware; unexpected transport death, double-fork descendants, and
teardown races are covered by focused tests.

Steers no longer kill the Grok process

The interrupt matrix after the follow-up commits:

Interrupt Behavior
User Stop Unchanged: hard cgroup kill + respawn, session/load on the next turn
Mid-prompt steer / restart_active Soft: session/cancel + same-session re-prompt; native context survives without a session/load replay
Settled steer (held turn) Preserve: no cancel at all; the runtime and its running subagents survive

Grok's session/cancel is detach-and-continue: the CLI reaps the foreground
shell but re-runs the interrupted command as a background task, and only the
model can kill it (via kill_command_or_subagent). Product call: the steered
model, not the client, decides whether that detached work dies, matching the
CLI's own behavior. The adapter tracks _x.ai/task_backgrounded /
task_completed into background-task mutations and tombstones model-killed
tasks so wake machinery stays truthful, and a direct Stop quarantines any late
lifecycle noise from the stopped run.

spawn_subagent and monitor project real work

Async spawn_subagent no longer completes on the background ACK alone. T3 binds
the child id, keeps the subagent running, shows durable subagent.result when
available, and hydrates from TaskOutput / get_command_or_subagent_output when
needed. monitor stays running through the start ACK, streams as command
execution, and completes from Bash exit codes or monitor-end text.

Late “will report when finished” can wake again

After the root turn settles (including when a subagent card already has a
result), Grok may still stream a late root follow-up (tool fetch + confirmation)
without a new session/prompt from T3. That traffic is buffered and attached to
a provider continuation run so the UI can go idle, then show Working again
with the follow-up, instead of dropping frames after finalize.

Long-running monitors initially made this path spam: Grok narrates every
monitor event (~3s apart, wider than the ~2s finalize quiet window), and each
burst opened its own continuation run with its own synthetic "Background task
completed." user row (4+ observed live). Continuation offers are now fully
suppressed while a tracked background task is still running: per-event agent
commentary AND tool re-reports buffer (Grok re-reports a running monitor as
Bash frames that already carry exit_code: 0 mid-stream, so a terminal-looking
tool status is not evidence the task ended), and exactly one continuation run
opens once the monitor actually ends, draining the buffered commentary.

In-turn monitors do not open a redundant wake

When Grok waits on a monitor and reports the listing in the same root turn
(no post-settle wait), late CLI re-reports and residual agent chatter must not
open synthetic "Background task completed." continuation runs. The follow-up
commit marks those tools handled mid-turn and suppresses residual agent-message
offers once handled work is done and nothing is still running.

Settled turns still get the injected monitor report

When Grok settles first ("I'll report when the monitor ends") and the CLI later
injects a monitor-event turn, the report could vanish: injected turns emit no
turn_completed, and the deferred-finalize debounce could fire while Grok was
still generating the report, dropping the chunk into an already finalized turn
(thread a8e8b0a9 run 5). The run now holds until the report streams (or a 25s
safety elapses), so the listing lands as a normal assistant message.

Screenshots and image attachments work on Grok

Attaching a screenshot to a Grok turn no longer fails immediately with a generic
provider turn start error. Grok’s ACP initialize still reports
promptCapabilities.image: false, but the agent accepts and vision-processes
image content blocks. The Grok flavor now opts in via supportsImagePrompts so
attachments are sent as ACP image blocks instead of being rejected before
session/prompt.

Problem and Fix

Problem and Why it Happened Fix
Steered user text vanished briefly on the v2 timeline (optimistic row keyed off messages that reordered against turn items) Keep the optimistic user row until the committed user_message turn item lands
Multi-tool Grok turns completed after the preamble; tools and final text never showed while the CLI kept working Disable speculative idle settle / silent session/cancel for Grok; terminalize only on real provider signals
Root turns hung Working when session/prompt stayed open without a reliable complete path Race the prompt RPC against root-matched turn_completed (and legacy prompt-complete) for the same prompt id
Stop mid-turn, then send again, wedged on Working / task_already_running Restart the Grok ACP child on interrupt recovery before the next prompt
Stop left native shells and descendants alive (ACP cancel alone did not contain the process tree) Linux cgroup kill when available; owned-process ledger fallback elsewhere
spawn_subagent showed completed in ~1ms on the background ACK; child work never bound Keep async spawn running, bind the child, project result / hydrate when available
monitor stayed spinning forever or never streamed output Keep start ACKs running; stream events into the tool; complete from exit codes / end notices
After “I'll report when finished,” late root follow-up never appeared (or every monitor burst opened its own synthetic “Background task completed.” run) Buffer post-settle wake traffic; suppress offers while background work still runs; open one continuation when the work actually ends and drain the buffer
When Grok fetched monitor output itself (long TaskOutput), the only end signal never cleared “still running,” so no continuation ever opened Treat TaskOutput completion as a real end signal post-settle and open the single continuation
Settled-then-injected monitor reports vanished (report streamed into an already-finalized turn) Hold finalize until the injected report streams (or a short safety timeout)
Steering a run held open for background work froze Working; the steer never reached Grok Finalize a prompt-settled held turn without waiting for a cancel ack, then restart into the steer
After a steer restart, still-running subagents spun forever (lineage maps were empty on the fresh turn) Carry live subagent lineages across the same-session restart so terminal signals still land
Subagent stayed running forever when the agent answered with text only and never called get_command Terminalize from the root “Background subagent … completed/failed” reminder
Persistent monitors kept the whole root run Working indefinitely Exclude persistent: true monitors from deferred-finalize holds; they continue post-settle
Screenshots / image attachments failed Grok turn start instantly while Codex accepted the same image Opt Grok into image prompt blocks even when ACP advertises image: false
In-turn monitor already reported, then late CLI re-reports opened many synthetic "Background task completed." runs Mark mid-turn terminal background tools as handled; suppress residual agent-message offers when handled work is idle
Every steer hard-killed the Grok CLI (restartRuntimeOnEveryInterrupt), discarding native session context and any running subagents even when the user was only redirecting Non-Stop interrupts go soft: session/cancel + same-session re-prompt; a steer on a settled held turn skips the cancel entirely and preserves the runtime
Grok session/cancel detaches and re-runs the interrupted command as a background task; nothing tracked that work, so wake machinery could misread pending state Track _x.ai/task_backgrounded / task_completed into background-task mutations; tombstone tasks the model kills via kill_command_or_subagent (no task_completed is emitted for kills)
Late background task lifecycle after a direct Stop could re-pin wake machinery (activeSessionId still points at the stopped session until the next turn respawns) Drop background task mutations while stoppedRunQuarantine is set, and ignore child-session mutations that must not gate root wake state
Stop after a settled soft steer had no active turn context, so the orphaned Grok runtime survived teardown and kept running Contain the orphan on Stop: drain under the callback permit, quarantine the stopped run, bump the generation, then process-group kill; the next turn respawns fresh
A settled-steer classification could race the prompt RPC still settling on the wire and send a redundant session/cancel, which Grok answers by killing detached background work promptWireSettled deferred succeeds the moment the prompt RPC resolves; settled classification also accepts wire settlement and skips the cancel
Orphan Stop left pending approval/elicitation requests unresolved, wedging the composer ("Resolve this approval request to continue") until server restart Cancel pending runtime requests during orphan Stop teardown
Orphan Stop left carried-over subagents spinning forever (terminalization needed the turn context the soft steer had already cleared) Context-free carryover terminalization emits interrupted node / subagent / turn_item events during orphan teardown
An interrupted carryover subagent card lost its streamed text (terminal result used stale task.result) and fell back to showing the spawn prompt Merge streamed assistantText into the terminal result with the same rule as live emission
A soft steer projected a run_interrupt_result whose parent run_interrupt_request only hard Stop emits, leaving a dangling parent link and letting timelines fold a continuing steered run as interrupted Superseded attempts project no interrupt items; the linked request/result pair is reserved for real user Stops
An interrupted command card kept Grok Bash's mid-stream exit_code: 0 re-report, reading as a clean success exit Project exit codes only from terminal native tool statuses; interrupted commands carry no exit code

Defensive Fixes

Problem and Why it Happened Fix
Hard Stop raced transport death: interrupt effects retried “turn is not active” until the outbox exhausted Idempotent interrupt when already cleared; concurrent hard teardown awaits the barrier; finalize the turn before process-group kill
Restart control effects hung polling for terminalization when the live session was already gone Treat missing session as already stopped; skip the interrupt wait and continue detach/start
Non-retryable “not active” matching on provider-turn.restart could swallow a failed replacement start Limit non-retry completion to pure interrupt races; restart stays retryable through start
Sticky continuationRequested after a failed or skipped dispatch blocked further offers and pinned idle Clear the flag when this generation finishes a dispatch attempt (success or failure); invalidate on every startTurn
Thought-only post-settle chunks opened synthetic continuation runs Offer only on assistant text or terminal tool status; thoughts may still buffer for attach
Post-settle agent text was dropped when activeTurn was null and in-turn-handled task ids remained Only suppress while a finalized context is still held; null activeTurn goes to the wake buffer
Coalesced <monitor-event> chunks dropped later progress / end notices Apply every monitor-event, batched <monitor> block, and end notice in the chunk
Multi-task_ids TaskOutput hydrated only the first id Prefer the result/header task id; otherwise complete each requested id
Text envelopes with Exit Code: -1 read as success Match optional signed exit codes as failed
Idle pin re-arm could stamp pin state onto a replacement session or advanced generation Stamp only when generation and runtime still match; re-arm in-place without self-interrupting the idle fiber
Session load idle detection refreshed on unrelated sessions (multi-session setups) Load gate is scoped to the session being loaded
Byte-array tool outputs lost leading/trailing whitespace that string outputs kept Decode byte text without trimming
TaskOutput observed as still running cleared the hydration hold early Only non-running completions clear the hydration hold
Late re-report of an already in-turn-handled TaskOutput pinned idle via a wake buffer that never drained Drop already-handled frames from the wake buffer unless a continuation is already outstanding
Runtime teardown polled OS state with Effect.sleep, which it.effect TestClock freezes, hanging the CI Test job with no output Containment teardown polls run on the wall clock explicitly
Dead t3-acp-<pid>-* cgroup lease directories accumulated when a server process died before cleanup Sweep stale sibling leases (dead pid, populated 0) before creating a new lease
Carryover terminalization resolved the parent thread id through a providerTurns lookup whose null fallback was actually the child's thread id Pin the parent provider thread id on the subagent at spawn; drop the lookup and fallback
Hard Stop keeps the run active until the provider reports interrupted, so a steer or selection restart could supersede the attempt first and the R4 supersede early-return dropped the result, leaving the Stop's request item unpaired Supersede path emits the result iff the run's request item exists (hasRunInterruptRequest callback threaded like shouldFinalizeRun)
The R5 paired result carried the superseded attempt's nodeId, so the shared superseded-interrupt filter hid it from every visible timeline and the Stop request still dangled on screen Hide a superseded run_interrupt_result only when unpaired (no run_interrupt_request for the same run); legacy plain-steer results in existing DBs stay hidden
The R5 request check never expired, so later plain steers on a once-stopped run re-upserted the same result item Callback renamed to hasUnpairedRunInterruptRequest: emits only when the request item exists and the derived result item is absent
A promptId-less _x.ai/session/prompt_complete settled the first pending prompt for the session, which is the wrong one whenever more than one is pending Fallback matches only when exactly one prompt is pending for the session; ambiguous notifications are ignored and prompts settle via their promptId-bearing paths
When a get_output result contained only <subagent_meta>/<subagent_result> blocks, stripping left nothing and the code fell back to the raw string, leaking machine tags into subagent cards and monitor output resultFromGetOutputTool returns null when the cleaned output is empty
The env picker's lock counted only visible user_message turn items, which hydrate later than the projection, so an existing thread briefly unlocked env/branch overrides on load and right after a run settled activeMessageCount takes the max of committed turn-item ids and serverProjection.messages.length, which hydrates atomically with the projection
V2EventTimelineRow early-returns lifecycle items (including subagents) to V2LifecycleRow, leaving an unreachable subagent presentation branch that review bots kept flagging Remove the dead v2EventPresentation subagent branch and its unreachable styling conditions; the live V2LifecycleRow path is untouched

Root-matched terminal signal (live evidence)

Current Grok CLI emits completion as:

{
  "method": "_x.ai/session/update",
  "params": {
    "sessionId": "<root session>",
    "update": {
      "sessionUpdate": "turn_completed",
      "prompt_id": "t3-xai-prompt-N",
      "stop_reason": "end_turn"
    }
  }
}

Matching rules:

  • Root sessionId equals the active ACP session
  • prompt_id equals the pending T3-injected id (t3-xai-prompt-N)
  • Ignore task-completed-* prompt ids (background tools)
  • Keep legacy top-level _x.ai/session/prompt_complete for older builds

Do not treat idle silence or subagent completions as root terminalization.

Server mechanics

Area Behavior
Idle settle Off for Grok (settleRootTurnWhenIdle: false); speculative idle complete stays disabled
Prompt race session/prompt RPC result or root-matched turn_completed / legacy prompt_complete
Deferred finalize Holds the root turn while monitors hydrate or subagents are still running; short quiet after last rearm (~2s) so a slightly late first post-hydration chunk is not dropped; a monitor end notice on a settled turn additionally holds finalize until the injected report streams (25s safety), and hydration holds carry a 60s safety
Interrupt restart User Stop requests runtime restart; next startTurn spawns a fresh Grok process. Non-Stop interrupts no longer restart the runtime (restartRuntimeOnEveryInterrupt dropped from the Grok flavor)
Interrupt cleanup Linux cgroup kill when available; otherwise owned PID ledger; Darwin process groups; Windows taskkill
Steer restart Does not force full process respawn; a mid-turn steer cancels + re-prompts, while a prompt-settled held turn finalizes directly (there is no cancel ack to wait for) and live subagent lineages carry into the restarted turn
spawn_subagent ACK stays running + binds child; TaskOutput / get_command hydrates; durable subagent.result when available
monitor Start ACK stays running; Bash exit_code or monitor-end text completes; projected as command execution
Post-settle continuation Buffer meaningful root updates when activeTurn is null; offer only on completion-like evidence (terminal tool status after normalization, or agent text with no tracked background task still running); monitor-ended reminders and TaskOutput completion frames are the genuine end signals that clear tracked task ids (ended ids are tombstoned against straggler re-adds); attach mode skips session/prompt and drains the buffer
Image attachments Grok flavor supportsImagePrompts: true overrides ACP promptCapabilities.image:false so screenshots are sent as image content blocks
Background task tracking _x.ai/task_backgrounded / task_completed become background-task mutations; kill_command_or_subagent tombstones the killed task; mutations are dropped while the Stop quarantine is active or when they belong to a child session

Design decisions (interrupt complexity; safe to simplify later)

Two choices below are deliberate and reviewable. If maintainers want less
surface area, each has a known downgrade path and a concrete fidelity trade-off.

1. Linux cgroup join wrapper (production spawn path)

Current landing: before exec of the real Grok/ACP binary on Linux, spawn
is rewritten through process.execPath + a small inline -e script
(wrapCommandForLinuxCgroup). The shim writes its PID into the leased
cgroup.procs, verifies /proc/self/cgroup, strips wrapper env, then
process.execves into the real command so membership sticks to the same PID.
This is only used when cgroup v2 containment is available; otherwise spawn is
unwrapped and interrupt falls back to the owned-process ledger (reduced
guarantee). Mac/Windows never use this path (process group / taskkill).

Why not PATH node: the wrapper uses the running Electron/Node binary
(process.execPath + ELECTRON_RUN_AS_NODE under desktop), so packaged
AppImages do not require a separate system Node install.

Preferred cleanup if inline -e is too opaque: replace the string with a
small checked-in acp-cgroup-enter.mjs next to the server (same execPath
invocation, no temp files, no C toolchain for production). Same semantics;
easier to review and diff.

Heavier alternative: checked-in C helper binary shipped in the desktop
artifact. Clearer argv contract and no execve dependency on the Electron
Node build, but adds a real packaging/build matrix. Not chosen for this PR.

Complexity you can drop: if cgroup pre-exec join is too much, ship only the
ledger / process-group fallback on Linux too. Trade-off: double-fork /
reparented descendants can escape kill (the portable ledger comment already
calls this out). Headless interrupt packs would get flakier on adversarial
shell shapes.

2. Pthread spawn test helper (.c, test-only)

Current landing: keep apps/server/scripts/acp-thread-spawn-helper.c. One
Linux live test compiles it with cc -pthread to fork a child from a
non-leader pthread, then asserts childrenOf(parent) still finds that
child. That exercises the production scan of every
/proc/<pid>/task/<tid>/children entry (main-thread-only scans miss this).

Local: if cc is missing, the test soft-skips (suite stays green).

CI: Blacksmith Ubuntu runners mirror GitHub’s Ubuntu image and often
already have gcc, but that is not a repo contract. The Test job therefore
installs build-essential so cc is present and the fixture runs instead
of silently no-oping:

# .github/workflows/ci.yml (Test job)
sudo apt-get install -y --no-install-recommends build-essential

Simpler alternative (drop complexity): replace the helper with a plain
Node/.mjs spawn("sleep", …) and rename the test to “finds a spawned child.”
No compiler, always runs. Fidelity loss: a regression that only walks the
main task’s children file (not every thread) can still pass a normal spawn
fixture and only fail the pthread case. Other double-fork / cgroup-kill tests
remain; only this multi-thread discovery edge case goes soft.

Not a good substitute without rethinking the assertion: pure JS cannot
faithfully create “non-leader pthread then fork” without native code.

Validation

  • vp check: format clean on changed files; monorepo lint warnings only (known
    Vite+ stdout panic can flake the full wrapper after lint)
  • vp run typecheck: clean
  • vp test apps/web/src/components/ChatView.logic.test.ts: pass
  • vp test apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts:
    pass (includes image capability override)
  • vp test apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts: pass
    (wake-evidence filters, continuation offer gating, the monitor start ACK
    normalization case, steer-during-hold interrupt finalize, subagent lineage
    carry-over across a steering restart, injected-report hold, interrupt
    cleanup fixtures, soft mid-prompt cancel + same-session re-prompt, direct
    Stop dropping late background task mutations, and the production Grok flag
    set still hard-killing and respawning on user Stop)
  • vp test apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts:
    pass (cgroup / process-tree interrupt evidence; pthread fixture needs cc,
    provided in CI via build-essential on the Test job; soft-skips locally if
    cc is absent)
  • vp test apps/server/src/orchestration-v2/ProviderSessionManager.test.ts:
    pass (idle pin with C0)
  • vp test apps/server/src/provider/acp/XAiAcpExtension.test.ts: pass (async
    spawn, TaskOutput, Monitor variant cases)
  • Headless live suites (parent live-scenarios, private serve against this
    branch / restacked AppImage):
    • grok-interrupt-fast: direct Stop + restart_active (the restart_active
      pack rewritten for soft semantics: process retained, one initialize, no
      session/load, cancel + re-prompt, codeword carry-forward proving
      same-session context; detach handling recorded as evidence, with a
      branch-conditional -detach variant)
    • grok-interrupt-soak: held subagent, mid-prompt steer during a subagent
      hold (new pack sampling the cancel-kills-subagent edge), duplicate Stop,
      leader exit, queued follow-up (scheduler gate); re-run green on the C4 tip
  • Desktop AppImage + mobile from-the-top interrupt pass (alairo project, one
    Grok thread titled from scenario 1 "Live-test direct Stop…", 2026-07-17 on
    orchestrator-v2 AppImage @ current snapshot; projection checked in userdata
    DB for thread 6c9994b7-bb04-4fb7-a9a8-e95688a6b7d7):
    • Scenario 1 direct Stop + INTERRUPTED_OK: pass
    • Scenario 2 steer / restart_active + RESTART_ACTIVE_OK: pass
    • Scenario 3 queue contrast (Ctrl/⌘+Enter only) + QUEUE_CONTRAST_OK: pass
      (one late-queue practice attempt finished the sleep without a queue; retry
      with queue while Working passed)
    • Scenario 4 rapid duplicate Stop + DUPLICATE_STOP_OK: pass
    • Scenario 5 queue then Stop: pass on mobile (desktop still hides Stop
      while queued; headless grok-interrupt-queued-followup remains the
      automated contract)
    • Scenario 6 Stop on plan/question card: pass on mobile (desktop still
      hides Stop while the card is open)
    • Scenario 7 Stop while subagent holds root + HOLD_FOLLOW_OK: pass (subagent
      interrupted, no HOLD_SUB_DONE / forbidden shell marker)
  • Earlier desktop-only pass (2026-07-16) covered 1–3 and 7 under the previous
    scenario numbering; 4–5 were blocked on desktop Stop UI as above.
  • Desktop AppImage smoke after Pass A rebuild (alairo, new thread
    16092557-…, 2026-07-17, scenarios 1 and 7 only):
    • Scenario 1 direct Stop + INTERRUPTED_OK: pass (sleep interrupted; no
      SHOULD_NOT_FINISH_CMD output)
    • Scenario 7 subagent hold Stop + HOLD_FOLLOW_OK: pass (subagent
      interrupted; no HOLD_SUB_DONE / forbidden shell marker)
  • Live private serve and desktop AppImage restack (trees/orchestrator-v2):
    • Multi-tool settlement, multiturn, monitors, subagents, post-settle packs,
      steer-during-hold, screenshot attach as previously reported on this PR
  • Review-round fixes (R1-R3): orphan-containment it.live test with the
    production Grok flag set (now also covering carryover subagent
    terminalization, streamed-text preservation, and the parent thread id pin),
    settled-soft wire-race test, cgroup lease sweep unit test; full server suite
    (1369 tests) and typecheck green locally, and the CI Test job green on the
    branch tip after each round
  • R4: new RunExecutionService tests pin steer supersede emitting no interrupt
    items while hard Stop keeps the linked pair; AcpAdapterV2 unit + it.live
    tests pin no exitCode on interrupted commands (mock agent now re-reports
    Grok's mid-stream exit_code: 0) and real exit codes on completed ones;
    message_steering grok/cursor fixtures and the selection-restart integration
    test assert zero run_interrupt_* on supersede; full server suite (1373
    tests) and typecheck green locally
  • R5: new RunExecutionService test pins the Stop-then-steer supersede case
    (seeded request + interrupted terminal emits the paired result with the
    request as parent); existing supersede/hard-stop pins unchanged; full
    server suite (1374 tests) and typecheck green locally
  • R6: shared timeline predicate tests pin paired-superseded visible /
    unpaired-superseded hidden / terminal visible; new RunExecutionService
    test pins the already-paired supersede emitting nothing; full server
    suite (1375 tests), packages/shared and packages/client-runtime
    suites, and typecheck green locally across all three packages
  • R7: new XAiAcpExtension tests pin the ambiguous promptId-less
    prompt_complete no-op (two pending prompts, neither settles) and the
    meta-only get_output yielding null result / empty appendOutput; full
    server suite (1377 tests), server + web typecheck, and vp check
    green locally
  • Second desktop AppImage manual pass (2026-07-19, rebuilt snapshot with R4,
    thread b6eb3501-…): steer-while-held, direct Stop, steer mid-command,
    subagent-hold Stop, and steer-then-Stop orphan containment all pass;
    projection DB audit confirms exactly one linked request/result pair per
    hard Stop, zero interrupt items on the three steers, zero interrupted
    commands with an exitCode, and the R3 streamed-text + parent-pin behavior
    intact

Out of Scope

Client presentation and desktop chrome that are not part of this Grok
settlement / interrupt PR. Called out so maintainers can track separate UI work
if desired; projection and run status still settle correctly.

Topic Notes
Desktop: no Stop while a message is queued After Ctrl/⌘+Enter queues a follow-up, red Stop does not appear; Queue panel only offers reorder / Steer (promote-to-steer). Queue-then-Stop is headless + mobile until desktop chrome is fixed.
Desktop: no Stop while an approval/question card is open Plan-mode cards hide Stop. Mid-card interrupt is mobile-testable and headless-adjacent; not desktop-manual.
Mobile: trailing work-log tool rows after a mobile Stop After a Stop initiated on the phone, iOS often shows a collapsed work log with +N previous tool calls (interrupted tools, Ask, etc.). Mobile keeps neutral tool rows in the work group; desktop filters neutrals out. Presentation difference only.
Mobile: interrupt lifecycle chrome missing for desktop-initiated Stops Server projection still writes run_interrupt_request / run_interrupt_result for Stops from desktop (verified in userdata). On mobile, those Interrupted rows are not shown for desktop-initiated Stops—not merely collapsed under +N previous tool calls (expanding the fold still does not surface them the way a live mobile Stop does). Live mobile Stops can show an X Interrupted work-log row and a You stopped after … fold. Incomplete multi-client / mobile lifecycle parity; not a missing server interrupt.
Mobile: no desktop-style --- Run interrupted --- divider Desktop renders run_interrupt_result as a full-width system divider and leaves interrupted turns unfolded. Mobile never uses that divider chrome.
workEntryPreview prefers detail over file_change filenames UI polish; expand path still lists files.
Subagent status badge styling via lifecycle early return Lifecycle row already shows a badge; extra badge path is unreachable.
Env-locking user-only count keyed off visible turn items Steer visibility intentionally uses visible user turn items; brief projection gap only.
Prompt-complete dedup without promptId / low-risk Grok session edge cases Serialized root prompts limit blast radius; not expanded in this PR.
Empty successful Bash forced inProgress until native output Intentional for provisional ACKs and post-settle monitor re-reports; narrower gate is future work.

Known limitations

  • After a real terminal signal, the hung session/prompt fiber may still log
    Interrupt when cancelled locally; runs complete with full content (log
    noise).

  • Builds that emit neither prompt RPC completion nor turn_completed /
    prompt_complete can still under-settle (no silent idle cancel fallback).

  • After Stop, a tool in the old shell may finish briefly before process kill
    (interruptPromptOnCancel: false); recovery still succeeds on the fresh
    child.

  • Subagent child-session tool calls (tools inside the child) are not fully
    projected as nested tool items; assistant text + final hydration / result card
    are covered.

  • Subagent wall time is provider-variable. A trivial “sleep ~5s then print a
    token” general-purpose spawn is often ~10–20s end-to-end, but the child agent
    can occasionally take much longer (minute-scale outliers observed). A long
    Working state usually reflects child runtime, not the ~2s deferred-finalize
    quiet window.

  • The root model may poll get_command_or_subagent_output with long
    timeouts, so the root run’s Working state can cover the entire child wait even
    when the subagent card is the durable success signal.

  • Non-persistent background work still holds Working. Deferred finalize
    keeps the root run open while a spawned subagent or a non-persistent monitor
    tool row is still running. Persistent monitors (persistent: true) no
    longer hold that gate; they settle the root and continue via post-settle
    continuation. Claude-style “go idle then reopen when background finishes” for
    non-persistent work is still future work. Content that arrives during a hold
    (CLI-injected monitor report) is delivered by the injected-report hold, so
    this is about run status staying Working, not about lost output.

  • Continuation dispatch is by threadId (the request also carries driver
    and providerThreadId detail for future scoping).

  • Buffered per-event commentary drains into the continuation run as several
    small assistant messages (one per burst). That is faithful to what the
    provider emitted; coalescing the display would be UI-side follow-up work.

  • Soft-interrupt detach tracking is best-effort. Grok's session/cancel
    detaches and re-runs the interrupted foreground command to full duration as
    a background task; no client-side ACP method kills it (only the model can,
    via kill_command_or_subagent). _x.ai/task_backgrounded can arrive late
    (~16s observed) or not at all (the model's poll-and-report shape emits
    none), so lifecycle tracking begins only when a notification arrives.
    Residual frames in that gap are accepted best-effort noise per the product
    call above; live packs record detach handling as evidence rather than
    pinning it.

  • Fast steers can trigger a model-side no-op flood. Steering within a
    couple of seconds of a reply while background work is still held (send a
    short message right after the settle reply with a subagent still running)
    can push Grok into emitting no-op run_terminal_command (true) calls
    about once per second until interrupted; the same loop shows up without
    steering after monitor-event injections (154 no-ops observed in one live
    thread, 207 in another). The steer text and background completions still
    land once the loop breaks or the run is stopped. This is xAI model
    behavior, not orchestrator state; recovery is Stop.

  • Desktop UI: no Stop while a message is queued or an approval/question card
    is open.
    See Out of Scope (mobile can still run those paths; headless
    covers queue-then-Stop).

  • Mobile vs desktop interrupt presentation. Trailing work-log rows after a
    mobile Stop, missing --- Run interrupted --- dividers, and no Interrupted
    lifecycle chrome on mobile for desktop-initiated Stops
    (not just collapsed)
    are client parity gaps only; server still projects interrupt items. See
    Out of Scope.

  • Grok exact-reply EOS token leakage (model, not T3). On short
    “reply with exactly TOKEN, no tools” recovery prompts, Grok sometimes
    returns TOKEN plus a literal end-of-sequence marker. Observed 2026-07-16
    on desktop AppImage after subagent-hold Stop:

    1. New Grok thread, model grok-4.5.
    2. Prompt: spawn one general-purpose subagent that runs
      sleep 30 && echo HOLD_SHOULD_NOT_MATTER and should return
      HOLD_SUB_DONE; root waits for the subagent.
    3. While root + subagent are still Working, press Stop.
    4. Send: Reply with exactly HOLD_FOLLOW_OK. No tools.
    5. Assistant final text was HOLD_FOLLOW_OK<|eos|> (ASCII <|eos|>
      appended). Projection stored that string verbatim; subagent was
      correctly interrupted with no forbidden shell marker.

    Other exact-reply markers in the same session (INTERRUPTED_OK,
    RESTART_ACTIVE_OK, DUPLICATE_STOP_OK) were clean. Treat as xAI/Grok
    model leakage suitable for upstream Grok team follow-up if desired; not a
    T3 interrupt/projection bug.

  • Stuck Working after hard interrupt on a poisoned session is reduced, not
    fully gone.
    This PR hardens the common Stop races (idempotent interrupt,
    finalize-before-kill, missing-session restart, sticky continuation flag,
    process-group reap). Residual risk remains if a held turn keeps a
    background/lineage entry that can no longer receive a terminal signal after
    restart (no overall force-finalize cap on that path). Fresh threads and the
    headless steer pack are the reliable recovery; candidate follow-ups are a
    bounded hold cap and reconciling carried subagent / background-tool state on
    interrupt.

Note

Harden Grok v2 prompt settlement, process containment, background task lifecycle, steer visibility, and image prompt support

  • Rewrites makeXAiPromptCompletionRuntime in XAiAcpExtension.ts to race both _x.ai/session/prompt_complete and _x.ai/session/update (turn_completed) notifications for prompt settlement, interrupting in-flight prompt RPC fibers on external completion.
  • Adds Linux cgroup-v2 process containment and POSIX process-tree ownership to AcpSessionRuntime, with platform-specific termination (cgroup kill on Linux, process-group on others); Grok runtime enables detached process-group on all platforms and descendant tracking on Linux.
  • Overhauls AcpAdapterV2 with soft/hard interrupt semantics (soft preserves runtime on settled turns; hard tears down with process-group termination on Stop), post-settle wake buffering for continuation runs, native transport request correlation with admission/acknowledgement sequencing, and deferred finalization for background tool/subagent activity.
  • Fixes isOrchestrationV2SupersededInterrupt in orchestrationV2Timeline.ts to only hide run_interrupt_result items when unpaired (no matching run_interrupt_request), correcting steer visibility in superseded attempts.
  • Adds acpSupportsImagePrompts to allow Grok flavor to enable image prompt blocks regardless of capability negotiation.
  • Risk: acpRootTurnIsIdle always returns false, unconditionally disabling speculative idle settlement; callers that relied on local idle settle will not trigger completion from that path.

Macroscope summarized 542c87b.


Note

Low Risk
Changes are limited to CI workflow and test/mock scripts; no production orchestration or runtime paths are modified in this diff.

Overview
Adds CI test infrastructure so ACP process-tree live tests always compile their pthread fixture, and expands the mock ACP agent plus a small C spawn helper to exercise Grok-style interrupt, monitor, and post-settle behavior in automated tests.

The Test job now installs build-essential and verifies cc is on PATH, so AcpSessionRuntime.processTree tests that compile acp-thread-spawn-helper.c run in CI instead of soft-skipping when no compiler is present.

acp-mock-agent.ts gains many env-flag scenarios: cancel-as-detach (task_backgrounded / task_completed), residual session/permission/elicitation callbacks after teardown, long-running terminal tools with optional real bash/sleep children (PID file, TERM-ignoring, detached session), duplicate empty successful Bash updates, post-settle and in-turn monitor + TaskOutput flows (including late duplicate updates), form/URL elicitation, and hang/exit variants around permission and cancel.

acp-thread-spawn-helper.c is a test-only program that forks sleep from a worker pthread and writes tid child_pid to a path, so process-tree discovery can be asserted for children not visible from the main thread alone.

Reviewed by Cursor Bugbot for commit 542c87b. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 852efc54-1c5a-472b-a05a-a9e96d1db6fe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jun 27, 2026
@mwolson
mwolson marked this pull request as ready for review June 27, 2026 00:43
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

4 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
Comment thread apps/server/src/provider/acp/XAiAcpExtension.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
Comment thread apps/server/src/provider/acp/XAiAcpExtension.ts
@mwolson
mwolson force-pushed the fix/grok-v2 branch 3 times, most recently from 4c4d0dd to 8f6ec37 Compare June 27, 2026 01:18
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jun 27, 2026
Comment thread apps/server/src/provider/acp/XAiAcpExtension.ts
Comment thread apps/server/src/provider/acp/XAiAcpExtension.ts Outdated
Comment thread apps/server/src/provider/acp/AcpSessionRuntime.ts Outdated
Comment thread apps/server/src/provider/acp/XAiAcpExtension.ts
Comment on lines +359 to +367
if (typeof exitCode === "number") {
if (exitCode === 0 && (xAiToolOutputText(withTitle)?.trim().length ?? 0) === 0) {
return { ...withTitle, status: "inProgress" };
}
return {
...withTitle,
status: exitCode === 0 ? "completed" : "failed",
};
}

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.

🟠 High acp/XAiAcpExtension.ts:359

normalizeXAiAcpToolCallState forces every successful Bash result with empty output to inProgress, so legitimately completed silent commands like true, test, or sleep (which produce { type: "Bash", exit_code: 0 } with no text) stay spinning forever instead of becoming completed. The comment says this is only for provisional permission ACKs, but the code has no way to distinguish those from real empty-output completions. When non-terminal tool items hold finalization, this also keeps the run open indefinitely. Consider restricting the inProgress override to a narrower signal (e.g. only when the original status was not already completed) or removing it entirely so empty-output exit-0 Bash results settle as completed.

    if (typeof exitCode === "number") {
-      if (exitCode === 0 && (xAiToolOutputText(withTitle)?.trim().length ?? 0) === 0) {
-        return { ...withTitle, status: "inProgress" };
-      }
      return {
        ...withTitle,
        status: exitCode === 0 ? "completed" : "failed",
      };
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/XAiAcpExtension.ts around lines 359-367:

`normalizeXAiAcpToolCallState` forces every successful Bash result with empty output to `inProgress`, so legitimately completed silent commands like `true`, `test`, or `sleep` (which produce `{ type: "Bash", exit_code: 0 }` with no text) stay spinning forever instead of becoming `completed`. The comment says this is only for provisional permission ACKs, but the code has no way to distinguish those from real empty-output completions. When non-terminal tool items hold finalization, this also keeps the run open indefinitely. Consider restricting the `inProgress` override to a narrower signal (e.g. only when the original status was not already `completed`) or removing it entirely so empty-output exit-0 Bash results settle as `completed`.

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.

Leaving this as-is for now (not taking the suggested removal).

The empty exit-0 Bash → inProgress override is intentional and covers more than provisional permission ACKs:

  1. Provisional / permission-style ACKs — Grok often emits a completed Bash frame with empty output before real stdout streams. Completing the row too early is wrong for that shape (covered by the unit test keeps empty successful Bash updates running until native output or turn settlement).
  2. Post-settle monitor re-reports — finished monitors can reappear as Bash frames with empty/minimal rawInput and a generic title, so monitor detection cannot match. Forcing empty exit-0 Bash to completed would re-open the “row spins or wake mis-classifies” class of bugs this heuristic was added to contain (see the comment on normalizeXAiAcpToolCallState).

Silent real commands (true, finished sleep with no stdout) can stay non-terminal until turn settlement; that is the known trade-off. A safer future fix is a narrower gate (e.g. only when the tool looks like a provisional “Ran command” ACK, or is already a tracked background/monitor task), not “always complete on empty exit 0.”

Happy to revisit if we have a concrete stuck-row repro that outweighs those regressions.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/server/src/provider/acp/AcpSessionRuntime.ts
Comment thread apps/server/scripts/acp-mock-agent.ts

@macroscopeapp macroscopeapp Bot left a comment

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.

Effect service conventions review found one convention deviation in the ACP session-runtime service failure introduced by this PR. Details inline.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/provider/acp/AcpSessionRuntime.ts Outdated
Comment thread apps/server/src/provider/acp/AcpSessionRuntime.ts Outdated
Comment thread apps/server/src/provider/acp/AcpSessionRuntime.ts
Comment thread apps/server/src/provider/acp/AcpSessionRuntime.ts
Comment thread apps/server/src/provider/acp/XAiAcpExtension.ts Outdated
@mwolson
mwolson force-pushed the fix/grok-v2 branch 2 times, most recently from ddfedee to e84f666 Compare July 17, 2026 00:35
Comment thread apps/server/scripts/acp-mock-agent.ts
@mwolson
mwolson force-pushed the fix/grok-v2 branch 2 times, most recently from ba852af to 0583e10 Compare July 17, 2026 00:58
Comment thread apps/server/src/provider/acp/AcpSessionRuntime.ts Outdated
@mwolson
mwolson marked this pull request as draft July 17, 2026 01:31
Comment thread apps/server/src/orchestration-v2/EffectWorker.ts
Comment thread apps/server/src/provider/acp/AcpSessionRuntime.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts Outdated
Comment thread apps/server/src/orchestration-v2/RunExecutionService.ts

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 6 total unresolved issues (including 4 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 838d38d. Configure here.

Comment thread apps/server/src/orchestration-v2/RunExecutionService.ts
Comment thread apps/server/src/orchestration-v2/ProviderTurnStartService.ts
Comment on lines +442 to +458
hasUnpairedRunInterruptRequest: () =>
projectionStore.getThreadProjection(projection.thread.id).pipe(
Effect.map((current) => {
const requestId = idAllocator.derive.runSignalTurnItem({
runId: run.id,
signal: "interrupt-request",
});
const resultId = idAllocator.derive.runSignalTurnItem({
runId: run.id,
signal: "interrupt-result",
});
const hasRequest = current.turnItems.some((item) => item.id === requestId);
const hasResult = current.turnItems.some((item) => item.id === resultId);
return hasRequest && !hasResult;
}),
Effect.catchCause(() => Effect.succeed(false)),
),

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.

🟡 Medium orchestration-v2/ProviderTurnStartService.ts:442

hasUnpairedRunInterruptRequest catches every getThreadProjection failure and returns false, so a transient projection read error makes an outstanding interrupt-request appear paired when it is not. RunExecutionService then skips writing the run_interrupt_result, and because the error was swallowed there is no retry — the Stop request stays permanently unpaired in the persisted state. Consider propagating the read failure instead of catching it, so the enclosing operation can retry rather than silently treating a failed lookup as a negative result.

Suggested change
hasUnpairedRunInterruptRequest: () =>
projectionStore.getThreadProjection(projection.thread.id).pipe(
Effect.map((current) => {
const requestId = idAllocator.derive.runSignalTurnItem({
runId: run.id,
signal: "interrupt-request",
});
const resultId = idAllocator.derive.runSignalTurnItem({
runId: run.id,
signal: "interrupt-result",
});
const hasRequest = current.turnItems.some((item) => item.id === requestId);
const hasResult = current.turnItems.some((item) => item.id === resultId);
return hasRequest && !hasResult;
}),
Effect.catchCause(() => Effect.succeed(false)),
),
hasUnpairedRunInterruptRequest: () =>
projectionStore.getThreadProjection(projection.thread.id).pipe(
Effect.map((current) => {
const requestId = idAllocator.derive.runSignalTurnItem({
runId: run.id,
signal: "interrupt-request",
});
const resultId = idAllocator.derive.runSignalTurnItem({
runId: run.id,
signal: "interrupt-result",
});
const hasRequest = current.turnItems.some((item) => item.id === requestId);
const hasResult = current.turnItems.some((item) => item.id === resultId);
return hasRequest && !hasResult;
}),
),
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/ProviderTurnStartService.ts around lines 442-458:

`hasUnpairedRunInterruptRequest` catches every `getThreadProjection` failure and returns `false`, so a transient projection read error makes an outstanding interrupt-request appear paired when it is not. `RunExecutionService` then skips writing the `run_interrupt_result`, and because the error was swallowed there is no retry — the Stop request stays permanently unpaired in the persisted state. Consider propagating the read failure instead of catching it, so the enclosing operation can retry rather than silently treating a failed lookup as a negative result.

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.

🟡 Medium acp/XAiAcpExtension.ts:280

isXAiPersistentMonitor misclassifies a persistent monitor as non-persistent when the structured rawOutput has type: "Monitor" but omits the persistent field. In that case the function returns false immediately at line 281 without checking the rawInput.persistent fallback, so a monitor that is actually persistent (per rawInput) is treated as non-persistent and can hold root-turn finalization indefinitely. The structured branch should only short-circuit when rawOutput.persistent is actually a boolean; otherwise it should fall through to the rawInput check.

  if (rawOutput !== undefined && nonEmptyString(rawOutput.type)?.toLowerCase() === "monitor") {
-    return rawOutput.persistent === true;
+    if (typeof rawOutput.persistent === "boolean") {
+      return rawOutput.persistent;
+    }
  }
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/XAiAcpExtension.ts around lines 280-282445546767:

`isXAiPersistentMonitor` misclassifies a persistent monitor as non-persistent when the structured `rawOutput` has `type: "Monitor"` but omits the `persistent` field. In that case the function returns `false` immediately at line 281 without checking the `rawInput.persistent` fallback, so a monitor that is actually persistent (per `rawInput`) is treated as non-persistent and can hold root-turn finalization indefinitely. The structured branch should only short-circuit when `rawOutput.persistent` is actually a boolean; otherwise it should fall through to the `rawInput` check.

Settle root turns from provider signals and preserve post-settle continuations.

Track asynchronous subagent and monitor work, allow image prompts, and keep steered messages visible.

Separate soft steering from hard Stop while containing and reaping native process trees.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants