Skip to content

fix(orchestrator): Skip blank subagent opening messages - #5456

Open
mwolson wants to merge 208 commits into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/claude-subagent-empty-prompt-message
Open

fix(orchestrator): Skip blank subagent opening messages#5456
mwolson wants to merge 208 commits into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/claude-subagent-empty-prompt-message

Conversation

@mwolson

@mwolson mwolson commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Carried from another PR

  • fix(orchestration): Preserve Claude subagent attribution after settle
    carried for #5388 claude-postsettle-attribution.
    Review it there, not here.
  • fix(orchestrator): Skip blank subagent opening messages — this PR.

Summary

  • Stop provider-native subagent child threads from opening with an empty or
    whitespace-only user message under the "Sent by another agent" label.
  • Gate every subagent opening message with a shared trim-aware check, and emit
    Claude's opening message the first time the task prompt has real text rather
    than on first registration.
  • One unique commit above fix(orchestration): Preserve Claude subagent attribution after settle #5388 claude-postsettle-attribution on
    t3code/codex-turn-mapping.

Problem and Fix

Problem and Why it Happened Fix
A Claude Agent launch whose prompt is whitespace-only still produced a child user message. The Agent tool accepts it, task_started carries it, and the emission site had no emptiness guard, so the web timeline rendered an empty bubble. Shared hasSubagentPromptText trims before deciding. Every makeSubagentConversationArtifacts caller with role: "user" goes through it (Claude, Codex, Cursor, ACP).
A task_progress frame can register a Claude subagent before any prompt-bearing frame. Registration fell back to prompt: input.prompt ?? "", and the opening message was tied to first registration, so it was emitted as "" and never rewritten when a later task_started supplied the real prompt. Claude emits the opening message when hasSubagentPromptText(task.prompt) && !hasSubagentPromptText(priorTask?.prompt), so a late prompt still opens the child exactly once and a prompt change from one non-empty value to another does not re-emit.
Codex only checked input.prompt.length > 0, which still admits whitespace. Cursor and ACP had no guard. Codex's length check is replaced by the shared predicate; Cursor and ACP pick up the same gate.

Validation

  • New replay fixture claude_subagent_empty_prompt covers both routes (whitespace Agent prompt and task_progress before task_started). Confirmed fail before the emission fix and pass after.
  • Replay suites (OrchestratorReplayFixtures.integration / .contract, ClaudeReplayFixtures.integration, CodexReplayFixtures.integration): 89 tests, 1 skipped.
  • Adapter and projection suites (Claude, Claude testkit, Codex, Cursor, Cursor testkit, ACP, ACP registry, Grok, SubagentProjection): 274 tests.
  • vp check: passed.
  • vp run typecheck: passed all 15 packages.
  • Isolated private serve live check against this branch: a whitespace Agent launch produced a completed subagent row with prompt: " " and a child thread with no user message; a sibling ordinary-prompt child held exactly one :prompt message. Zero trim-empty user-role projection messages in the isolated DB.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes (not applicable: server-side projection only)
  • I included a video for animation/interaction changes (not applicable)

Note

Low Risk
Server-side projection-only change with a narrow guard; low risk to auth, data, or core runtime paths.

Overview
Subagent child threads no longer get an opening user message when the task prompt is empty or whitespace-only, avoiding empty “Sent by another agent” bubbles in the timeline.

A shared hasSubagentPromptText (trim-aware) gates makeSubagentConversationArtifacts for user-role opening messages across provider adapters. In this diff, ACP only emits message.updated / turn_item.updated for the :prompt item when update.prompt has real text after trim.

Related adapter behavior (same PR): Claude defers the opening message until the first frame with non-empty prompt (not first registration); Codex drops a raw length check in favor of the shared helper; Cursor and ACP gain the same guard.

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

juliusmarminge and others added 30 commits August 5, 2026 15:51
Co-authored-by: codex <codex@users.noreply.github.com>
- Initialize provider as unchecked in a pending state
- Update initial probe message to reflect session-local status
- Type the runtime effect with `Scope`
- Build the ACP session runtime without wrapping it in `Effect.scoped`
- Use strict TurnId and ProviderItemId parsing in Codex session routing
- Decode in-memory stdio chunks in streaming mode to avoid split UTF-8 corruption
- Transfer session-owned scopes into adapter state
- Ensure runtime scopes close on stop and startup failure
- Add regression coverage for scoped lifecycle cleanup
- Close the managed native event logger when the adapter layer tears down
- Make session runtime close idempotent with an atomic closed flag
- Add coverage for flushing thread native logs on shutdown
- Use codex app-server snapshots for auth, models, and skills
- Remove legacy CLI/config discovery paths and related helpers
- Update tests for the new provider status flow
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
- Document the target orchestration graph, IDs, lifecycles, and capability model
- Add Codex app-server probe fixtures and update the probe test harness
- Introduce orchestration v2 service interfaces and error types
- Add replay runtime, fixtures, and integration coverage
- Update shared contracts and probe transcripts

Co-authored-by: codex <codex@users.noreply.github.com>
- Add Codex adapter and replay harness wiring
- Introduce in-memory orchestration projections and provider registry
- Expand orchestration contracts for turn and runtime events
Co-authored-by: codex <codex@users.noreply.github.com>
- Add context transfer IDs, schemas, and projections
- Support cheap fork creation and Codex native fork rollback
- Cover fork idempotency and replay behavior in tests
- Track remaining projection, context transfer, rollback, capability, and subagent work
- Clarify current V2 baseline and debugger-only follow-ups
- Map fork and merge-back turns into stored handoffs and transfer resolutions
- Add shell snapshot projection support plus coverage tests
- Update replay fixtures and web contracts for the new turn flow
Co-authored-by: codex <codex@users.noreply.github.com>
- Move Codex replay recording into `apps/server`
- Add Claude Agent SDK replay fixtures and test harness
- Update orchestration-v2 fixture scenarios and docs
- Move Claude provider runtime logic into its own module
- Share the SDK query runner between live and replay paths
- Add replay driver error wrapping for unexpected failures
- persist the selected model on run records
- surface run model selection in the debug UI
- update replay fixtures and contracts for the new field
- Record Claude SDK transcripts across multiple prompts and restart/query modes
- Add approval and tool-call replay coverage for new orchestration fixtures
- Update Claude adapter testkit to model open/prompt/permission frames
- Derive Claude SDK query options from runtime policy
- Add read-only replay fixture and policy mapping tests
- Reuse shared approval-policy fixtures across orchestrator tests

Co-authored-by: codex <codex@users.noreply.github.com>
- add active steering and interrupt-restart replay fixtures
- update Claude adapter/orchestrator turn handling for steering
- refresh replay and integration test coverage
- add interrupt and mid-tool replay fixtures for Claude and Codex
- log Claude Agent SDK protocol frames to native event traces
- project Codex commandExecution start events into orchestration updates
- Map Cursor SDK agents and runs to V2 thread and turn lifecycles
- Update MCP capability, tool, and testing guidance for SDK-based injection
Co-authored-by: codex <codex@users.noreply.github.com>
juliusmarminge and others added 26 commits August 5, 2026 16:11
Follow-ups from the main merge:

- Migration numbering: main's 035_ProjectionThreadTitleRegeneration is
  released and keeps its id; this branch's unreleased v2 migrations shift
  to 036-044 instead (a released migration can never be renumbered).
- thread.metadata.update accepts regenerateTitle: true arms an in-flight
  titleRegeneration marker on the thread payload (requestId + startedAt),
  a landing title or explicit false clears it. The marker projects onto
  thread shells and through the client-runtime shell model, so the
  sidebar's Regenerating state works unchanged.
- New ThreadTitleRegenerationService worker reacts to armed markers on
  the live domain-event stream (so ws, MCP, and mobile dispatches all
  behave the same): builds a newest-first conversation digest from the
  v2 projection (8k-char budget, retained attachments — ported from the
  v1 reactor), generates via TextGeneration, and lands the title with a
  follow-up metadata update. Failures clear the marker and log.
- The server advertises threadTitleRegeneration again, and the client
  updateThreadMetadata dispatches regenerateTitle-only updates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The turn item materialized when a queued run is promoted was emitted
with inputIntent "turn_start", but the deterministic replay driver (and
the recorded queued_turn fixtures) expect "queued_turn" — the mismatch
stalled replay before the queued run could start, leaving run 1 waiting
and run 2 queued forever in all five queued_turn fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Effect service conventions: ThreadManagementThreadNotSendableError
carried an Archived/NoSteerableRun reason union and switched on
reason._tag inside the message getter. Modelled as two error classes
(ThreadManagementThreadArchivedError, ThreadManagementNoSteerableRunError);
both still map to the MCP thread_not_sendable failure code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Prevent the title row from shifting when output is revealed
- Preserve panel and compact button sizing
- Schema.UnknownFromJsonString -> Schema.fromJsonString(Schema.Unknown)
- SchemaIssue.InvalidValue single-argument form in checkpointDiff
- McpServerClient requires protocolVersion

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Required for follow-up requests by the 2025-06-18 MCP HTTP transport
that effect beta.103 enforces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The thread-panel mapping change replaced the compact className on the
Run and Add controls with the isPanel conditional and dropped the
non-panel icon-compact classes that the responsive test asserts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the deleted v1 ProviderCommandReactor title coverage onto the v2
service: marker arming/clearing via thread.metadata.update, superseded
requestId no-ops, digest-driven regeneration, the "New thread" and
unchanged-title fallbacks, generation failure, and missing initial
messages, plus unit tests for formatThreadTitleContext.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hoist inline Schema compiles to module scope, drop unused
imports/vars, stabilize react-markdown component identities via a
module-scope factory, remove useless spreads, and use data-derived
keys for release-note bullets. No behavior changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The codex resume test was the only replay scenario without a
runtimePolicyOverride, so its checkpoint scope cwd fell back to
process.cwd() and baseline capture ran real git over the entire
checkout. Locally the capture short-circuits on checkpoint refs left
behind by earlier runs; on a fresh CI checkout it is a cold multi-second
capture that outlives the scenario wait budget, failing await_thread_idle
while the run is still mid-checkpoint. Point the fixture's turn/start
frames at the <workspace> placeholder and checkpoint a throwaway git
workspace like every other replay test.

Scenario waits are also wall-clock-bounded now: the iteration budget
counts event-loop turns, which burn at full speed while git/fixture IO
is in flight, so exhaustion additionally requires a 60s real-time
deadline to pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r's scope (pingdotgg#5406)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- Keep the git action control disabled when the branch is up to date
- Omit open PR menu entries and remove their link-opening behavior
- Update logic tests for the new states
…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>
A provider-native subagent could open its child thread with an empty or
whitespace-only user message, which renders as an empty bubble under the
"Sent by another agent" label. The Claude adapter emitted the opening
prompt whenever the subagent was new, and a task_progress frame can
register a subagent before any frame carries its prompt, so the message
was emitted with the "" default and never rewritten when the real prompt
arrived on a later task_started.

A shared hasSubagentPromptText guard now gates every subagent opening
message. The Claude adapter emits it the first time the task prompt
actually has text rather than on first registration, so a late prompt
still opens the child thread exactly once. Codex's existing length check
becomes trim-aware, and Cursor and ACP pick up the same guard.
@coderabbitai

coderabbitai Bot commented Aug 5, 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 Plus

Run ID: 780c5d1e-0f63-4320-a3ce-9608ba6976d1

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

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 vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 5, 2026
@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch from 25de21d to 0af2a6e Compare August 7, 2026 12:10
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.

6 participants