feat(orchestration-v2): add native subagent and workflow observability - #4664
feat(orchestration-v2): add native subagent and workflow observability#4664shivamhwp wants to merge 61 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 |
940e8fb to
c3a50f8
Compare
d2ef6c7 to
adb1e9b
Compare
c3a50f8 to
8c6a796
Compare
adb1e9b to
39912b0
Compare
8c6a796 to
103b937
Compare
39912b0 to
6bc3a8d
Compare
103b937 to
31adb36
Compare
6bc3a8d to
05fe44c
Compare
31adb36 to
b270c47
Compare
05fe44c to
7458298
Compare
b270c47 to
daf1a88
Compare
7458298 to
4daeac4
Compare
daf1a88 to
1dce464
Compare
4daeac4 to
095cab7
Compare
1dce464 to
4c169ed
Compare
095cab7 to
1d37a6f
Compare
4c169ed to
70d5111
Compare
1d37a6f to
6cf67a2
Compare
70d5111 to
c78d0ff
Compare
6cf67a2 to
5e3c535
Compare
c78d0ff to
a997806
Compare
5e3c535 to
2430736
Compare
…n the turn-start test The rebase reconstruction of this file left each service module imported twice — once as a namespace and once for its tag. At a service boundary the namespace shape is the convention, so the duplicate named imports go and their uses read through the module. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0d57bbd to
eb31f1a
Compare
| providerDriver = support.subagent?.driver ?? item.driver; | ||
| providerInstanceId = support.subagent?.providerInstanceId ?? item.providerInstanceId; | ||
| model = support.subagent?.model ?? model; | ||
| } else if (item.type === "fork") { |
There was a problem hiding this comment.
🟡 Medium threads/thread-work-log.tsx:139
Subagent activity rows render an enabled-looking but permanently disabled Open CTA, so users cannot navigate to the child thread. Because this branch was removed while threadActivity.ts still exposes subagent items to ThreadActivityThreadRow, targetThreadId stays null; restore the item.type === "subagent" handling using support.subagent?.childThreadId ?? item.childThreadId and its provider metadata.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/threads/thread-work-log.tsx around line 139:
Subagent activity rows render an enabled-looking but permanently disabled `Open` CTA, so users cannot navigate to the child thread. Because this branch was removed while `threadActivity.ts` still exposes subagent items to `ThreadActivityThreadRow`, `targetThreadId` stays `null`; restore the `item.type === "subagent"` handling using `support.subagent?.childThreadId ?? item.childThreadId` and its provider metadata.
| ? {} | ||
| : { | ||
| kind: "workflow", | ||
| roleFallback: "workflow-coordinator", |
There was a problem hiding this comment.
🟡 Medium Adapters/ClaudeAdapterV2.ts:4481
Workflow coordinators that were first seen as ordinary subagents remain labeled with their generic role in the Agents UI when subagent_type is absent. updateClaudeSubagentNode only rewrites task.role when input.agentType is defined, so this roleFallback is ignored during the later workflow upgrade; apply the fallback when updating an existing task as well.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts around line 4481:
Workflow coordinators that were first seen as ordinary subagents remain labeled with their generic role in the Agents UI when `subagent_type` is absent. `updateClaudeSubagentNode` only rewrites `task.role` when `input.agentType` is defined, so this `roleFallback` is ignored during the later workflow upgrade; apply the fallback when updating an existing task as well.
| Effect.gen(function* () { | ||
| const existing = context.input.existingSubagents ?? []; | ||
| if (existing.length === 0) return; | ||
| yield* Ref.update(subagentThreads, (current) => { |
There was a problem hiding this comment.
🟡 Medium Adapters/CodexAdapterV2.ts:1621
Recovered child turn/started events are dropped when they arrive before seedExistingSubagents, so the child activation is never created or projected and reuse attribution is lost after startup. Seeding populates subagentThreads but does not drain pendingSubagentTurns; subsequent registration takes the existing !== undefined early return instead of replaying the queued turn. Drain and replay pending turns for recovered threads during seeding (or in the existing-thread path).
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts around line 1621:
Recovered child `turn/started` events are dropped when they arrive before `seedExistingSubagents`, so the child activation is never created or projected and reuse attribution is lost after startup. Seeding populates `subagentThreads` but does not drain `pendingSubagentTurns`; subsequent registration takes the `existing !== undefined` early return instead of replaying the queued turn. Drain and replay pending turns for recovered threads during seeding (or in the existing-thread path).
| if (subagent === undefined || payload.tokenUsage.total.totalTokens <= 0) { | ||
| return; | ||
| } | ||
| if (subagent.task.currentActivationId !== null) { |
There was a problem hiding this comment.
🟡 Medium Adapters/CodexAdapterV2.ts:3568
A trailing thread/tokenUsage/updated frame updates subagentActivationsByNativeTurnId but leaves subagent.task.usage and aggregate agent totals stale, so the final token usage is omitted. The task update is gated by currentActivationId before the handler merges the trailing frame; emit the task update after merging the activation, regardless of whether the activation is still current.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts around line 3568:
A trailing `thread/tokenUsage/updated` frame updates `subagentActivationsByNativeTurnId` but leaves `subagent.task.usage` and aggregate agent totals stale, so the final token usage is omitted. The task update is gated by `currentActivationId` before the handler merges the trailing frame; emit the task update after merging the activation, regardless of whether the activation is still current.
There was a problem hiding this comment.
Two small consistency findings in the changed web UI; everything else (the new Agents CTA row's cursor/focus contract, the lineage panel's disconnect fallback, the panel model rename) looks consistent with the surrounding component system.
Posted via Macroscope — UI Consistency
|
|
||
| <ThreadLineageRowList hiddenCount={hiddenCount} onShowMore={showMore}> | ||
| {visibleRows.length === 0 ? ( | ||
| <div className="flex h-9 items-center gap-2.5 rounded-lg px-2.5 text-[13px] text-muted-foreground"> |
There was a problem hiding this comment.
This new static row hand-rolls the thread-details panel's row geometry instead of reusing the named treatment the panel already exports (THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS), and it drifts from it: no border border-transparent and the icon misses -mx-0.5, so the icon sits ~1px and the label ~3px right of every other row in the panel (the lineage link rows and the workspace/environment locked rows all include both). Consider reusing the shared row + icon classes and keeping only the muted informational color at the call site:
<div className={cn(THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS, "flex items-center font-normal text-muted-foreground")}>
<BotIcon aria-hidden className={THREAD_DETAILS_PANEL_ICON_CLASS} />
<span>Agent session connected</span>
</div>(THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS and THREAD_DETAILS_PANEL_ICON_CLASS need adding to the existing ./threadDetailsPanelStyles import.)
Posted via Macroscope — UI Consistency
| detail={detail} | ||
| badge={item.status} | ||
| threadId={item.childThreadId} | ||
| threadId={null} |
There was a problem hiding this comment.
subagent is the only item type that passes expandedDetail, so with threadId={null} the disclosure variant of RelatedThreadCard can never render its open-thread button — but its summary still carries pr-11, which was there purely to reserve space for that absolutely positioned button. The result is ~44px of dead space on every expandable subagent row, with the chevron floating well short of the right edge and out of line with the collapsed variant's trailing badge. Consider making the reserve conditional on the action existing:
<summary
aria-label={`Show full result for ${props.title}`}
- className="flex min-w-0 cursor-pointer list-none items-center gap-2 px-3 py-2 pr-11 text-left transition-colors hover:bg-muted/50 [&::-webkit-details-marker]:hidden"
+ className={`flex min-w-0 cursor-pointer list-none items-center gap-2 px-3 py-2 ${threadId === null ? "" : "pr-11"} text-left transition-colors hover:bg-muted/50 [&::-webkit-details-marker]:hidden`}
>Posted via Macroscope — UI Consistency
This adds native subagent and workflow observability to Orchestrator V2. It is based directly on the V2 implementation in PR #2829; no V1 runtime or compatibility data source remains in this PR.
Why this PR exists
Orchestrator V2 has its own event model, projection, provider boundary, recovery path, and thread lifecycle. When V2 ships, provider-native child work needs V2-owned identities and activations so the Agents experience stays accurate through reuse, interruption, restart, nesting, and background completion.
Before and after
The comparison below is against the current
t3code/codex-turn-mappingV2 base, not main/V1.Responsive web verification at 390×844:
What changes
V2 domain and persistence. Reusable subagent identities and immutable per-activation records live in the V2 projection. Migration 050 persists activations, and schema defaults keep older V2 payloads readable without a backfill.
Provider-native projection. Claude, Codex, Cursor, OpenCode, and ACP/Grok emit V2 subagent and activation lifecycle updates, including role, usage, recent activity, workflow membership, activation counts, idle state, and provider-reported nesting.
Reuse and recovery. Reused agents bind to the run that reactivates them. Activation proof prevents stale post-interrupt traffic from attaching to a later run, and recovery retains the cumulative usage baseline.
V2-only client state. The Agents panel derives directly from
OrchestrationV2Subagent[]. The V1 activity fold, dual-source fallback, and obsolete tests are removed. V2 timeline items collapse into one CTA per workflow or direct-spawn batch.Internal thread visibility. One shared V2 predicate controls filtering on server, web, and mobile. Hidden child deep links resolve through the existing targeted projection RPC and redirect to their parent without adding child shells to the global stream. Unfiltered thread refs remain available for cleanup so hidden worktrees are not mistaken for orphans.
Explicitly not included
Current stack
2d623ac6b41ad1ed25c0473474699866083e0b65(t3code/codex-turn-mapping)b2bae43908a497a7ef2989bf665555be40d2a716Verification
ProviderSessionManager.test.ts:836error on the untouched V2 baseProvider limitation: if a provider reports a nested agent only inside its parent's final text and emits no lifecycle frame, T3 shows that summary but does not invent a nested agent row.
Built with Codex (GPT-5.6 Sol) through T3 Code.
Note
Add subagent and workflow observability to OrchestrationV2 with activation tracking and timeline CTA rows
subagent_activation.updateddomain and provider events, persisted in a neworchestration_v2_projection_subagent_activationstable (migration 050) and returned as part of thread projections.OrchestrationV2Subagentschema withkind,role,usage,currentActivationId,activationCount,recentActivity, andworkflow/workflowMembershipfields; older rows decode with populated defaults.idlepost-completion status.childThreadIdlinks and merging descendant projections viamergeSubagentTreeProjection/routeSubagentTreeEvent.subagentor forked from a node) are now filtered out of shell snapshots, sidebar, thread lists, and hooks viaisOrchestrationV2InternalSubagentThread; deep-links redirect to the parent thread on web and mobile.collapseSubagentTimelineEntries, showing aggregated counts, token totals, and an open-agents call-to-action.listAllThreadRefsWebSocket RPC that returns all thread references including hidden subagents, used for accurate worktree cleanup.Macroscope summarized 513b603.
Note
High Risk
Large orchestration and multi-adapter changes affect run attribution, settlement, and thread visibility; regressions could mis-route events, leak hidden threads, or mishandle resumed subagents after restart.
Overview
Adds per-activation subagent lifecycle across V2 provider adapters (Claude, Codex, Cursor, ACP, etc.): subagents gain kind/role/usage/workflow fields, emit
subagent_activation.updated, and use shared observability helpers. Adapters handle reuse after idle or restart by seedingexistingSubagents, rebinding runs, guarding registry races (Claude), mapping Codex collab states toidlefor reusable agents, and folding token usage into activations.Mobile hides internal subagent threads via
isOrchestrationV2InternalSubagentThread(list + deep-link redirect to parent), delays “thread unavailable” to avoid flashing over redirects, drops stale subagent edges from the relationships banner, and trackssubagentActivationsin the environment cache.Server adds
listAllThreadRefsauth scope and a Codex replay scenario for subagent reuse after idle. Nested subagent lifecycle can be routed to the root thread projection (SubagentTreeProjection) so the parent fleet stays complete without copying child transcripts.Reviewed by Cursor Bugbot for commit 513b603. Bugbot is set up for automated code reviews on this repo. Configure here.