fix(orchestrator): Keep agent output attached after a mid-turn steer - #4547
fix(orchestrator): Keep agent output attached after a mid-turn steer#4547mwolson wants to merge 223 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
ApprovabilityVerdict: Needs human review This PR introduces significant runtime behavior changes to turn management in the Claude adapter, including new timeout mechanisms, state tracking for pending steers, and claim-based turn finalization to handle mid-turn steering scenarios. The complexity of the orchestration changes warrants human review. You can customize Macroscope's approvability policy. Learn more. |
f386094 to
32c2849
Compare
2e0d977 to
34d8cac
Compare
34d8cac to
c6cb7e6
Compare
c6cb7e6 to
48e1a70
Compare
48e1a70 to
a436602
Compare
a436602 to
3d442e6
Compare
3d442e6 to
ad1da43
Compare
ad1da43 to
333efe8
Compare
fbc8365 to
73f7755
Compare
…sPinned Main owns migration numbering: 036_ProjectionThreadsPinned landed on main, so the v2 migrations shift from 036-044 to 037-045. Release path runs all of main's migrations first, then the v2 stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Port thread pinning (pingdotgg#5312) into the orchestration-v2 command pipeline: thread.pin/unpin commands, thread.pinned/unpinned events, pinnedAt on the v2 thread state and projected shells, promotion semantics (pin clears settle/snooze, settle clears pin) matching the v1 decider, and client pin/unpin operations in the v2 dispatch style. - Port the regenerated-title context anchoring (pingdotgg#5365) into ThreadTitleRegenerationService: pin the first user message ahead of the retained tail when the digest is truncated. - Re-apply the right-panel controls positioning from pingdotgg#5260 to the v2 ChatView title bar controls. - Repair merge artifacts: committed conflict markers in BranchToolbar, duplicate capability keys, duplicate CommandPalette import, v1 turn naming in DiffPanel's focus-refresh effect, onSend signature merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Match progress button spacing and single-line height to static git actions
rerere replayed stale resolutions during the rebase and committed nested conflict markers in several files. Restore the branch-intended v2 shapes and re-graft main's compatible additions (pending-card opacity comments, theme-editor keybinding test, mobile scroll re-arm effects from pingdotgg#5566). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eysetIndex Main owns migration numbering: 037_ProjectionTurnsKeysetIndex landed on main (pingdotgg#5493), so the v2 migrations shift from 037-045 to 038-046. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native subagent observability (pingdotgg#5219), wired per its spec's v2 merge plan: - getWorkflowScript RPC re-homed onto the v2 WS surface (contracts, rpc group, ws handler, auth scope, client atom). - AgentsPanel fed by the spec's mapper swap: projectedSubagentsToRuntime maps orchestration-v2 subagent entities into the panel model; deriveAgentPanelModel's v2Projection leg is now live and the v1 fold never runs. Agents surface wired into ChatView + RightPanelTabs. Other ports and reconciliations: - Shell reconnect-loop fix (pingdotgg#5561) ported into the v2 shell sync (same-session resubscribes resume from the in-memory cursor), with the cursor-resume regression test adapted to v2 fixtures. - Mobile end-follow latch (pingdotgg#5566) ported onto the v2 ThreadFeed. - Claude ede_diagnostic interrupt classification (pingdotgg#5557) ported into ClaudeAdapterV2 (aborted_tools/aborted_streaming => interrupted; CLI telemetry never becomes the failure banner). pingdotgg#5559 needs no v2 port (unknown system subtypes are already ignored). - Plan sidebar removed from the v2 ChatView/ChatComposer per main's plans-fold-into-chat rework (pingdotgg#5558); rightPanelStore stays at main's surface set. - SettingsPanels rebuilt as main's refactored version plus the branch's composer-context setting; sidebar snooze respects the time format (pingdotgg#4438 follow-through). - v1-only leftovers deleted: zombie v1 adapters/ingestion/tests the v2 rewrite removes, the v1-bound transfer-budget CI harness (pingdotgg#5350, needs a v2 rebuild), and main's v1 client pagination machinery (pingdotgg#5493 client side; the 037 keyset migration is kept — server-side v2 windowing is a follow-up). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase kept the LegendList 3.3.3 upgrade and patch from pingdotgg#5449 and the mobile end-follow latch from pingdotgg#5566, but the v2 MessagesTimeline/ChatView still carried the branch's blunt any-gesture-breaks-follow listeners. Port main's pingdotgg#5566 web mechanics onto the v2 follow architecture: - resolveTimelineIsAtEnd measures the 40px follow re-arm band from real geometry (contentLength/scroll/scrollLength minus the composer inset), keeping the isNearEnd fallback for older state shapes. - Follow now breaks only on gestures that can actually leave the live edge: upward wheel with overflowing content, touch drags that exited the end band, scrollbar drags vs content clicks, and keyboard navigation (PageUp/Home/ArrowUp) — previously keyboard scrolling never broke follow and the next stream chunk yanked the view back down. - Listener attach retries across frames so a thread switch cannot mount the list without its opt-out listeners. Deliberately not ported: pingdotgg#5449's shouldRestorePosition disclosure anchoring and follow-gated maintainScrollAtEnd — the v2 timeline keeps maintainScrollAtEnd={false} with its own follow scrolls and anchor system; flipping that core is a separate change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gdotgg#5449) Complete the pingdotgg#5449 architecture on the v2 timeline, following the LegendList author's direction to lean on the list's native mechanisms instead of app-side scroll layers: - maintainScrollAtEnd is enabled and owned by LegendList, gated off only while the user reads history (liveFollowEnabled), while a sent turn anchors near the top (anchoredEndSpace), or during the two-frame settle of a fold toggle. - maintainVisibleContentPosition compensates size changes natively ({data, size, shouldRestorePosition}); fold toggles anchor compensation to the toggled row via a disclosure anchor key, so the trigger stays under the pointer instead of the viewport chasing the end. - ChatView's hand-rolled streaming follow (double-rAF scrollToEnd on every data change) is gone; the app now only owns streaming adjustments during anchored-end-space mode, mirroring main. - timelineLiveFollowEnabled state mirrors the follow refs so the render-visible gate switches native follow off when a gesture breaks follow and back on when the viewport returns to the end band. Timeline tests updated to assert the native-ownership invariants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Keep success feedback visible in the Git action control for 10 seconds - Move the running elapsed timer into the panel menu slot
…s with v2 Post-rebase reconciliation sweep: - Sidebar: main's folded Sidebar.tsx/Sidebar.logic.ts adapted to v2 shells (latestRun/runtime naming, waiting status instead of monitoring), with subagent-thread filtering and main's pinned-reorder helpers re-exported - Pinned drag reorder (pingdotgg#5581) ported into v2: thread.pin orderKey + thread.pin.reorder command, thread.pin-reordered event, Orchestrator fold, ProjectionStore/Maintenance, client-runtime commands and shell mapping - Project favicon (pingdotgg#4849-era) and defaultThreadEnvMode flowed through v2 contracts (OrchestrationProjectShell, application event payloads) - ChatView: main's pingdotgg#5592 header props, pull-request right-panel surfaces, liveAgentCount badge (pingdotgg#5745) wired into the v2 panel layout - enableAssistantStreaming -> enableLegacyTokenStreaming rename applied to v2 RunExecutionService and replay testkit - Removed v1 zombies resurrected by the rebase (provider service/reaper/ ingestion + v1 layer tests, server.test.ts, integration harness) - routeTree: main's tree + branch's /settings/scheduled-tasks route - Misc marker-sweep syntax repairs (rpc.ts, entities.ts, localApi.test.ts, rightPanelStore.test.ts, GitManager.test.ts, mobile model menu helpers) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 038-040 Main released ProjectionThreadsPinOrderKey (038), ProjectionProjectsDefaultThreadEnvMode (039) and ProjectionProjectFaviconPath (040), so the branch-private v2 stack shifts up by three. Registry ids were already 41-49; this renames the files and identifiers to match and updates the ledger expectations and through-id boundaries in the migration tests (released boundary 37 -> 40). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- a6c9b41 (agents open pasted images): ClaudeAdapterV2 now grants the attachments dir alongside cwd via additionalDirectories and appends '[Attached ... is saved at: path]' lines to the turn text so tools can dereference pasted images (pixels alone are not tool-readable). - 5bb8c03 (settle leaves monitors running): thread.settle now joins archive/delete in the provider-session detach set, so PR monitors, dev servers and subagent fleets stop when the user parks the thread. The settle guard already rejects active runs, and serialized dispatch closes the re-engage race the v1 fix handled with onlyIfSettled. - e70cdb4 (Claude resume handshakes) and 2c7267a (reaper vs live background subagents) are already covered structurally in v2: results are turn-scoped with explicit zero-turn handshake drops, and idle release is pinned while background work is pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e panel-visibility merge The keep-both merge nested main's plan-surface migration test inside a branch popover test and dropped the threadPanelVisibilityByThreadKey key from the migration results. Restore main's test body and include the branch's (empty) visibility map in the expected persisted shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n text Follow-up to the pingdotgg#5757 port: start and steer turns now append the '[Attached ... is saved at: path]' line, so the adapter fixtures assert it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
22bd872 to
a27c1cc
Compare
The round-3 reconciliation took main's ChatHeader wholesale and wired its full prop set, resurrecting the scripts/open-in/git-actions cluster the branch had deliberately relocated into the thread panel. Restore the 79-line slim header (project favicon + name + thread title) and its minimal ChatView call. pingdotgg#5592's header actions stay a documented v2 follow-up, as decided in round 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-apply pingdotgg#4547 stranded-output continuation and the full mid-tool steer handoff path that pure pingdotgg#5388 checkout dropped on the CTM pin move.
- Add ready, merge, and conflict-resolution actions to the PR row - Share pull request action and handoff logic with the detail panel - Fix thread details scrolling and row alignment
Steering a Claude turn while a tool was running settled the run while the agent was still working. The reply to the steer then had no live turn to attach to, so it was buffered and, with nothing asking for a continuation, silently dropped: the thread went quiet and returned to Ready with the steer unanswered. Two independent defects produced that. The steer guard keyed on one exact string. `isClaudeActiveSteeringAbortResult` matched only `terminal_reason: "aborted_streaming"`, which the CLI reports when a steer interrupts a streaming assistant message. A steer that lands while a tool is running is queued instead and delivered when the tool result arrives, and the CLI ends that native turn with `aborted_tools`, answering the steer in the next one. That result did not match, so the turn finalized while the CLI kept working. A newly recorded fixture captures the shape from a real session: `subtype: success`, `terminal_reason: "aborted_tools"`, empty text, followed by a fresh native turn carrying the answer. `isClaudeSteeringHandoffResult` replaces it. While a steer is outstanding, a result hands off when the CLI cut the turn short (`aborted_streaming` or `aborted_tools`, including on the error result that is how the first of those arrives) or when the turn ended cleanly with nothing to say. A result carrying text has answered something and still terminalizes, so a steer the CLI absorbs into the running turn does not hold the run open; `max_turns`, `background_requested`, `tool_deferred` and the hook reasons are real terminals even as an empty success. Each accepted steer swallows exactly one result, so the turn still ends on the result that answers it, and the handoff is decided before anything else reads the result so it cannot seed the turn's fallback assistant text. Because a handoff makes the turn depend on a native turn that has not opened yet, it is bounded: if no frame reaches the turn within 60 seconds it settles as a failure rather than leaving the run running for the rest of the session. A failure, not a quiet completion, because an accepted steer that was never answered is exactly the silence this change exists to stop. Buffered assistant text could also be stranded. When a run terminalizes early while the query is still live, later frames go to the wake buffer, which only requested a continuation for a tracked task notification, a running subagent, or a result. Ordinary post-settle assistant text asked for nothing, so it sat in the buffer until the session recycled. Assistant text now requests a continuation on its own. Tool_use and tool_result frames stay gated: they are the model working rather than speaking, and the notification that follows them carries the wake detail. Also record replay `stream_event` frames, which every query receives since `includePartialMessages` landed, and allow a fixture to hold its steer until the target run reports a given turn item so a mid-tool steer can be replayed deterministically.
6025800 to
f3363da
Compare
72e3863 to
a186d64
Compare
Re-apply pingdotgg#4547 stranded-output continuation and the full mid-tool steer handoff path that pure pingdotgg#5388 checkout dropped on the CTM pin move.
Re-apply pingdotgg#4547 stranded-output continuation and the full mid-tool steer handoff path that pure pingdotgg#5388 checkout dropped on the CTM pin move.
a186d64 to
5b1a115
Compare
|
Absorbed into #5459 settled-thread lifecycle. That PR is now the stability consolidation and includes this mid-turn steer output fix on current t3code/codex-turn-mapping. |
Carried from another PR
fix(orchestrator): Hide Stop after runtime parksis carried for#4378 orchestrator-v2-background-waiting.
Review those commits there, not here.
fix(orchestrator): Keep agent output attached after a mid-turn steerandfix(orchestrator): Preserve delayed wake summariesbelong to this PR.#4378 orchestrator-v2-background-waiting
is included because both PRs change the same continuation gate in
bufferWakeMessage. Ont3code/codex-turn-mappingthat gate reads!isPendingTaskNotification && !isPendingSubagentNotification && message.type !== "result";the dependency rewrites the surrounding region and adds an
isNativeOpaqueWakeFrameclause that holds assistant and user frames until abackground-task notification has been buffered. Written against the base alone,
this PR's change would conflict with that rewrite and could drop the dependency
clause.
Stacking it makes the intended end state explicit: the two conditions are
additive, so a stranded assistant frame opens a continuation while the
background-notification gating stays intact. If
#4378 orchestrator-v2-background-waiting
merges first, drop its commits from this branch and the remaining diff applies
unchanged.
Summary
Steering a Claude turn while a tool is running settles the run while the agent
is still working. The reply to the steer then has no live turn to attach to, so
it is buffered and, with nothing asking for a continuation, silently dropped:
the thread goes quiet and returns to Ready with the steer unanswered.
There are three independent defects behind that. Fixing only the steering
handoff or only the post-settle wake leaves a real failure standing, so this
fixes the complete path.
Problem and Fix
isClaudeActiveSteeringAbortResultmatched onlyterminal_reason: "aborted_streaming", which the CLI reports when a steer interrupts a streaming message. A steer landing while a tool runs is queued instead and delivered when the tool result arrives, and the CLI ends that native turn withaborted_tools, answering the steer in the next one. That result did not match, so the turn finalized while the CLI kept working.isClaudeSteeringHandoffResultrecognises a handoff by delivery state: the CLI cut the turn short (aborted_streamingoraborted_tools), or the turn ended cleanly with nothing to say. Each accepted steer swallows exactly one result, so the turn still ends on the result that answers it.A result carrying text still terminalizes the turn even with a steer
outstanding, and
max_turns,background_requested,tool_deferredand thehook reasons are real terminals even as an empty success. Only
completed, oran older CLI that omits the field, means another native turn is coming.
Validation
message_steering_mid_toolis a newly recorded transcript from a real session,not a hand-written one. It captures the shape the old guard missed:
subtype: success,terminal_reason: "aborted_tools", empty text, then a fresh nativeturn carrying the answer. Recording it needed a
steerAfter: "tool_use"mode inthe replay recorder,
waitForTurnItemTypeon fixture steer steps so thereplayed steer waits for the tool item instead of racing the transcript, and
stream_eventacceptance in the replay decoder, since every query setsincludePartialMessagesand fixtures recorded before that option have none.Each major path was confirmed to fail with its corresponding fix reverted:
message_steering_mid_toolreplay fixture: without the guard fix the steer'sanswer never reaches the projection.
terminalizes;
max_turnsstill terminalizes; the bound settles a silent turn.add its summary before the two-second fallback, and post-settle tool frames
request nothing.
does not leak fallback ownership, and the no-background path offers
immediately.
The focused Claude adapter suite passes 72 tests.
vp check, the fulltypecheck, and
git diff --checkpass on the current branch.Live-tested on a packaged desktop build. Steering during a ~60s command, the run
now stays live 65 seconds past the steer's delivery and ends only after the last
steered reply, where it previously went terminal within about 75ms of delivery.
A second steer sent while the agent is still working is accepted rather than
refused with
thread_not_sendable, and 105 status samples across the turn showone transition, running to completed, with no blank window and no rescue
continuation run. A four-step steered instruction executed in full.
Manual re-test scenarios, in the published guide at
https://nam7nt0rbtm6.postplan.dev:
Current CI Note
Test, Release Smoke, Mobile Native Static Analysis, Cursor Bugbot, CodeRabbit,
and Macroscope Correctness pass on the current head. Check is red only from the
known Vite+ stdout panic while printing warnings.
Macroscope Effect Service Conventions remains red from a claim that the queued
status rationale was removed. The rationale is still immediately above
deriveThreadActivityRun, and the review thread has the exact sourcereference.
Note
Medium Risk
Changes core Claude turn lifecycle, wake/continuation gating, and steer counting; mistakes could leave runs stuck, double-terminal, or drop background wake detail, though coverage is heavy.
Overview
Fixes steering during a running tool and assistant text that arrives after an early turn settle so steered answers and late output are not lost on quiet threads.
Steer handoffs: Replaces the
aborted_streaming-only guard withisClaudeSteeringHandoffResult, includingaborted_toolsand empty clean successes. Pending steers are counted per turn; each handoff result is swallowed instead of terminalizing. A 60s bound fails the turn if the CLI never opens the follow-up native turn.finalizeActiveTurnclaims the active turn first to avoid duplicate terminals.Stranded output: After settlement, assistant text in the wake buffer can trigger a continuation (immediate when no wake-eligible background work, otherwise a 2s wait for task notification detail). Post-settle tool frames still buffer without offering early. Same-thread query replacement no longer clears buffered output and continuation state the same way.
Test/replay: Adds
message_steering_mid_toolfixture and recording modeactive_steering_mid_tool(steerAfter: tool_use, fixturewaitForTurnItemType, replay acceptsstream_event). Extensive adapter tests cover handoff, timeout, continuations, and process replacement.Reviewed by Cursor Bugbot for commit f3363da. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix agent output detachment after mid-turn steer in ClaudeAdapterV2
aborted_streaminghandoff check with a broader classifier inisClaudeSteeringHandoffResultthat also handlesaborted_toolsand empty-text completed results, preventing premature turn terminalization after a mid-tool steer.Mapinstead of aSet) and defers finalization until a real answer arrives or a 60sboundSteeringHandofftimeout fires, at which point the turn fails withsteer_handoff_timeout.finalizeActiveTurnto prevent duplicate terminal emissions.message_steering_mid_toolreplay fixture and associated test cases covering handoff survival, timeout settlement, stranded output continuation, and process replacement scenarios.Macroscope summarized f3363da.