History

fix(orchestration-v2): preserve subagent progress state

Shivam Sharma committed 3 months ago

Commit c0a38521 parent 3f829675 files · +71 −15Browse files

Unified · Split

5 files
  1. apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +2 −2
  2. apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +24 −0
  3. apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +30 −12
  4. apps/server/src/orchestration-v2/testkit/fixtures/claude_workflow/claude_transcript.ndjson +1 −1
  5. apps/server/src/orchestration-v2/testkit/fixtures/claude_workflow/output.ts +14 −0
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+2−2
@@ -2790,11 +2790,11 @@ export function makeClaudeAdapterV2( · 2789 unmodified lines
2790 status: input.status,2790 status: input.status,
2791 title: "Subagent progress",2791 title: "Subagent progress",
2792 startedAt: progressStartedAt,2792 startedAt: progressStartedAt,
2793 completedAt: input.status === "running" ? null : now,2793 completedAt: settled ? now : null,
2794 updatedAt: now,2794 updatedAt: now,
2795 type: "reasoning",2795 type: "reasoning",
2796 text: progress,2796 text: progress,
2797 streaming: input.status === "running",2797 streaming: !settled,
2798 },2798 },
2799 });2799 });
2800 }2800 }
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+24−0
@@ -3577,6 +3577,21 @@ describe("CodexAdapterV2 post-settle continuation", () => { · 3576 unmodified lines
3577 assert.equal(firstCompletion?.subagent.activationCount, 1);3577 assert.equal(firstCompletion?.subagent.activationCount, 1);
3578 assert.equal(firstCompletion?.subagent.usage?.totalTokens, 100);3578 assert.equal(firstCompletion?.subagent.usage?.totalTokens, 100);
3579 assert.isNull(firstCompletion?.subagent.currentActivationId);3579 assert.isNull(firstCompletion?.subagent.currentActivationId);
3580 const firstActivationUpdates = harness
3581 .subagentActivationUpdates()
3582 .filter((event) => event.activation.ordinal === 1);
3583 const firstActivation = firstActivationUpdates.at(-1)?.activation;
3584 assert.equal(firstActivation?.ordinal, 1);
3585 assert.equal(firstActivation?.status, "completed");
3586 assert.equal(firstActivation?.usage?.totalTokens, 100);
3587 assert.equal(
3588 firstActivation?.completedAt,
3589 firstActivationUpdates.at(-2)?.activation.completedAt,
3590 );
3591 assert.equal(
3592 firstActivation?.updatedAt,
3593 firstActivationUpdates.at(-2)?.activation.updatedAt,
3594 );
3580 assert.lengthOf(3595 assert.lengthOf(
3581 new Set(3596 new Set(
3582 settledUpdates3597 settledUpdates
@@ -3629,6 +3644,15 @@ describe("CodexAdapterV2 post-settle continuation", () => { · 46 unmodified lines
3629 [RESUME_CHILD_TURN_1, RESUME_CHILD_TURN_2],3644 [RESUME_CHILD_TURN_1, RESUME_CHILD_TURN_2],
3630 );3645 );
3631 assert.equal(activations.at(-1)?.activation.status, "completed");3646 assert.equal(activations.at(-1)?.activation.status, "completed");
3647 assert.equal(activations.at(-1)?.activation.usage?.totalTokens, 80);
3648 assert.equal(
3649 activations.at(-1)?.activation.completedAt,
3650 activations.at(-2)?.activation.completedAt,
3651 );
3652 assert.equal(
3653 activations.at(-1)?.activation.updatedAt,
3654 activations.at(-2)?.activation.updatedAt,
3655 );
3632 assert.isFalse(yield* harness.hasPendingBackgroundWork);3656 assert.isFalse(yield* harness.hasPendingBackgroundWork);
3633 assert.lengthOf(harness.terminalEvents(), 1);3657 assert.lengthOf(harness.terminalEvents(), 1);
3634 assert.lengthOf(harness.continuationRequests, 0);3658 assert.lengthOf(harness.continuationRequests, 0);
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+30−12
@@ -1517,6 +1517,9 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi · 1516 unmodified lines
1517 const pendingRootTurns = yield* Ref.make(new Map<string, ProviderAdapterV2TurnInput>());1517 const pendingRootTurns = yield* Ref.make(new Map<string, ProviderAdapterV2TurnInput>());
1518 const turnWaiters = yield* Ref.make(new Map<string, Deferred.Deferred<void, never>>());1518 const turnWaiters = yield* Ref.make(new Map<string, Deferred.Deferred<void, never>>());
1519 const subagentThreads = yield* Ref.make(new Map<string, CodexSubagentThreadContext>());1519 const subagentThreads = yield* Ref.make(new Map<string, CodexSubagentThreadContext>());
1520 const subagentActivationsByNativeTurnId = yield* Ref.make(
1521 new Map<string, OrchestrationV2SubagentActivation>(),
1522 );
1520 const pendingSubagentTurns = yield* Ref.make(1523 const pendingSubagentTurns = yield* Ref.make(
1521 new Map<string, ReadonlyArray<PendingCodexSubagentTurnStarted>>(),1524 new Map<string, ReadonlyArray<PendingCodexSubagentTurnStarted>>(),
1522 );1525 );
@@ -1958,6 +1961,9 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi · 435 unmodified lines
1958 updatedAt: turn.startedAt,1961 updatedAt: turn.startedAt,
1959 } satisfies OrchestrationV2SubagentActivation;1962 } satisfies OrchestrationV2SubagentActivation;
1960 activeContext.subagentActivation = activation;1963 activeContext.subagentActivation = activation;
1964 yield* Ref.update(subagentActivationsByNativeTurnId, (current) =>
1965 new Map(current).set(turn.nativeTurnId, activation),
1966 );
1961 yield* Ref.update(activeTurns, (current) => {1967 yield* Ref.update(activeTurns, (current) => {
1962 const updated = new Map(current);1968 const updated = new Map(current);
1963 updated.set(turn.nativeTurnId, activeContext);1969 updated.set(turn.nativeTurnId, activeContext);
@@ -3380,30 +3386,36 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi · 1416 unmodified lines
3380 });3386 });
3381 3387
3382 const activeContext = (yield* Ref.get(activeTurns)).get(payload.turnId);3388 const activeContext = (yield* Ref.get(activeTurns)).get(payload.turnId);
3383 if (3389 const activation =
3384 activeContext === undefined ||3390 activeContext?.subagent === subagent
3385 activeContext.subagent !== subagent ||3391 ? activeContext.subagentActivation
3386 activeContext.subagentActivation === null3392 : ((yield* Ref.get(subagentActivationsByNativeTurnId)).get(payload.turnId) ?? null);
3387 ) {3393 if (activation === null) {
3388 return;3394 return;
3389 }3395 }
3390 const last = payload.tokenUsage.last;3396 const last = payload.tokenUsage.last;
3391 const activation = {3397 const now = yield* DateTime.now;
3392 ...activeContext.subagentActivation,3398 const updatedActivation = {
3393 usage: {3399 ...activation,
3400 usage: mergeCumulativeSubagentUsage(activation.usage, {
3394 totalTokens: last.totalTokens,3401 totalTokens: last.totalTokens,
3395 inputTokens: last.inputTokens,3402 inputTokens: last.inputTokens,
3396 cachedInputTokens: last.cachedInputTokens,3403 cachedInputTokens: last.cachedInputTokens,
3397 outputTokens: last.outputTokens,3404 outputTokens: last.outputTokens,
3398 reasoningOutputTokens: last.reasoningOutputTokens,3405 reasoningOutputTokens: last.reasoningOutputTokens,
3399 },3406 }),
3400 updatedAt: yield* DateTime.now,3407 updatedAt: activation.completedAt === null ? now : activation.updatedAt,
3401 } satisfies OrchestrationV2SubagentActivation;3408 } satisfies OrchestrationV2SubagentActivation;
3402 activeContext.subagentActivation = activation;3409 if (activeContext?.subagent === subagent) {
3410 activeContext.subagentActivation = updatedActivation;
3411 }
3412 yield* Ref.update(subagentActivationsByNativeTurnId, (current) =>
3413 new Map(current).set(payload.turnId, updatedActivation),
3414 );
3403 yield* emitProviderEvent({3415 yield* emitProviderEvent({
3404 type: "subagent_activation.updated",3416 type: "subagent_activation.updated",
3405 driver: CODEX_PROVIDER,3417 driver: CODEX_PROVIDER,
3406 activation,3418 activation: updatedActivation,
3407 });3419 });
3408 }).pipe(Effect.orDie),3420 }).pipe(Effect.orDie),
3409 );3421 );
@@ -3435,6 +3447,9 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi · 25 unmodified lines
3435 updatedAt: now,3447 updatedAt: now,
3436 } satisfies OrchestrationV2SubagentActivation;3448 } satisfies OrchestrationV2SubagentActivation;
3437 activeContext.subagentActivation = activation;3449 activeContext.subagentActivation = activation;
3450 yield* Ref.update(subagentActivationsByNativeTurnId, (current) =>
3451 new Map(current).set(activeContext.nativeTurnId, activation),
3452 );
3438 yield* emitProviderEvent({3453 yield* emitProviderEvent({
3439 type: "subagent_activation.updated",3454 type: "subagent_activation.updated",
3440 driver: CODEX_PROVIDER,3455 driver: CODEX_PROVIDER,
@@ -4392,6 +4407,9 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi · 951 unmodified lines
4392 updatedAt: input.completedAt,4407 updatedAt: input.completedAt,
4393 } satisfies OrchestrationV2SubagentActivation;4408 } satisfies OrchestrationV2SubagentActivation;
4394 input.context.subagentActivation = activation;4409 input.context.subagentActivation = activation;
4410 yield* Ref.update(subagentActivationsByNativeTurnId, (current) =>
4411 new Map(current).set(input.nativeTurnId, activation),
4412 );
4395 yield* emitProviderEvent({4413 yield* emitProviderEvent({
4396 type: "subagent_activation.updated",4414 type: "subagent_activation.updated",
4397 driver: CODEX_PROVIDER,4415 driver: CODEX_PROVIDER,
apps/server/src/orchestration-v2/testkit/fixtures/claude_workflow/claude_transcript.ndjson+1−1
@@ -4,7 +4,7 @@ · 3 unmodified lines
4{"type": "emit_inbound", "label": "system", "frame": {"type": "system", "subtype": "init", "agents": [], "apiKeySource": "none", "claude_code_version": "2.1.183", "cwd": "/tmp/claude-replay-claude_workflow", "tools": [], "mcp_servers": [], "model": "claude-sonnet-4-6", "permissionMode": "bypassPermissions", "slash_commands": [], "output_style": "default", "skills": [], "plugins": [], "fast_mode_state": "off", "uuid": "af3bf4cc-1385-4e19-a5fa-fcbe2a0ca201", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}4{"type": "emit_inbound", "label": "system", "frame": {"type": "system", "subtype": "init", "agents": [], "apiKeySource": "none", "claude_code_version": "2.1.183", "cwd": "/tmp/claude-replay-claude_workflow", "tools": [], "mcp_servers": [], "model": "claude-sonnet-4-6", "permissionMode": "bypassPermissions", "slash_commands": [], "output_style": "default", "skills": [], "plugins": [], "fast_mode_state": "off", "uuid": "af3bf4cc-1385-4e19-a5fa-fcbe2a0ca201", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
5{"type": "emit_inbound", "label": "assistant:commentary", "frame": {"type": "assistant", "message": {"model": "claude-sonnet-4-6", "id": "msg_01claudeworkflow1", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "Starting the two-phase workflow."}], "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 1, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1, "service_tier": "standard", "inference_geo": "not_available"}, "context_management": null}, "parent_tool_use_id": null, "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201", "uuid": "97898e28-ee4e-4fcf-80c3-5c790c502201"}}5{"type": "emit_inbound", "label": "assistant:commentary", "frame": {"type": "assistant", "message": {"model": "claude-sonnet-4-6", "id": "msg_01claudeworkflow1", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "Starting the two-phase workflow."}], "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 1, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1, "service_tier": "standard", "inference_geo": "not_available"}, "context_management": null}, "parent_tool_use_id": null, "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201", "uuid": "97898e28-ee4e-4fcf-80c3-5c790c502201"}}
6{"type": "emit_inbound", "label": "task_started:local_workflow", "frame": {"type": "system", "subtype": "task_started", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "description": "Research and implement", "task_type": "local_workflow", "prompt": "Research the parser, then implement the fix.", "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73201", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}6{"type": "emit_inbound", "label": "task_started:local_workflow", "frame": {"type": "system", "subtype": "task_started", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "description": "Research and implement", "task_type": "local_workflow", "prompt": "Research the parser, then implement the fix.", "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73201", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
7{"type": "emit_inbound", "label": "task_progress:phase-1", "frame": {"type": "system", "subtype": "task_progress", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "description": "Research phase running", "usage": {"input_tokens": 300, "output_tokens": 200, "total_tokens": 500, "tool_uses": 4}, "workflow_progress": [{"type": "workflow_phase", "index": 0, "title": "Research"}, {"type": "workflow_phase", "index": 1, "title": "Implement"}, {"type": "workflow_agent", "index": 0, "label": "Researcher", "state": "active", "phaseIndex": 0, "attempt": 0, "model": "claude-sonnet-4-6", "lastToolName": "Read", "tokens": 200, "toolCalls": 2}], "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73202", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}7{"type": "emit_inbound", "label": "task_progress:phase-1", "frame": {"type": "system", "subtype": "task_progress", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "description": "Research phase running", "usage": {"input_tokens": 300, "output_tokens": 200, "total_tokens": 500, "tool_uses": 4}, "workflow_progress": [{"type": "workflow_phase", "index": 0, "title": "Research"}, {"type": "workflow_phase", "index": 1, "title": "Implement"}, {"type": "workflow_agent", "index": 0, "label": "Researcher", "state": "waiting", "phaseIndex": 0, "attempt": 0, "model": "claude-sonnet-4-6", "lastToolName": "Read", "tokens": 200, "toolCalls": 2}], "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73202", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
8{"type": "emit_inbound", "label": "task_progress:phase-2", "frame": {"type": "system", "subtype": "task_progress", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "description": "Implement phase running", "usage": {"input_tokens": 700, "output_tokens": 500, "total_tokens": 1200, "tool_uses": 9}, "workflow_progress": [{"type": "workflow_phase", "index": 0, "title": "Research"}, {"type": "workflow_phase", "index": 1, "title": "Implement"}, {"type": "workflow_agent", "index": 0, "label": "Researcher", "state": "done", "phaseIndex": 0, "attempt": 0, "model": "claude-sonnet-4-6", "lastToolName": "Read", "tokens": 400, "toolCalls": 3}, {"type": "workflow_agent", "index": 1, "label": "Implementer", "state": "active", "phase_index": 1, "attempt": 0, "model": "claude-opus-4-1", "last_tool_name": "Edit", "tokens": 500, "tool_calls": 4}], "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73203", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}8{"type": "emit_inbound", "label": "task_progress:phase-2", "frame": {"type": "system", "subtype": "task_progress", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "description": "Implement phase running", "usage": {"input_tokens": 700, "output_tokens": 500, "total_tokens": 1200, "tool_uses": 9}, "workflow_progress": [{"type": "workflow_phase", "index": 0, "title": "Research"}, {"type": "workflow_phase", "index": 1, "title": "Implement"}, {"type": "workflow_agent", "index": 0, "label": "Researcher", "state": "done", "phaseIndex": 0, "attempt": 0, "model": "claude-sonnet-4-6", "lastToolName": "Read", "tokens": 400, "toolCalls": 3}, {"type": "workflow_agent", "index": 1, "label": "Implementer", "state": "active", "phase_index": 1, "attempt": 0, "model": "claude-opus-4-1", "last_tool_name": "Edit", "tokens": 500, "tool_calls": 4}], "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73203", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
9{"type": "emit_inbound", "label": "task_notification:local_workflow", "frame": {"type": "system", "subtype": "task_notification", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "status": "completed", "output_file": "", "summary": "Workflow finished both phases.", "usage": {"input_tokens": 900, "output_tokens": 600, "total_tokens": 1500, "tool_uses": 11}, "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73204", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}9{"type": "emit_inbound", "label": "task_notification:local_workflow", "frame": {"type": "system", "subtype": "task_notification", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "status": "completed", "output_file": "", "summary": "Workflow finished both phases.", "usage": {"input_tokens": 900, "output_tokens": 600, "total_tokens": 1500, "tool_uses": 11}, "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73204", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
10{"type": "emit_inbound", "label": "assistant:commentary", "frame": {"type": "assistant", "message": {"model": "claude-sonnet-4-6", "id": "msg_01claudeworkflow2", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "claude workflow fixture complete"}], "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 1, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1, "service_tier": "standard", "inference_geo": "not_available"}, "context_management": null}, "parent_tool_use_id": null, "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201", "uuid": "89be9e65-097b-47e8-849b-be723386b201"}}10{"type": "emit_inbound", "label": "assistant:commentary", "frame": {"type": "assistant", "message": {"model": "claude-sonnet-4-6", "id": "msg_01claudeworkflow2", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "claude workflow fixture complete"}], "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 1, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1, "service_tier": "standard", "inference_geo": "not_available"}, "context_management": null}, "parent_tool_use_id": null, "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201", "uuid": "89be9e65-097b-47e8-849b-be723386b201"}}
apps/server/src/orchestration-v2/testkit/fixtures/claude_workflow/output.ts+14−0
@@ -72,6 +72,20 @@ export function assertClaudeWorkflowOutput( · 71 unmodified lines
72 0,72 0,
73 "a member without membership would render outside its workflow",73 "a member without membership would render outside its workflow",
74 );74 );
75 const waitingProgressItem = result.domainEvents.find(
76 (event) =>
77 event.type === "turn-item.updated" &&
78 event.payload.type === "reasoning" &&
79 event.payload.text === "Read" &&
80 event.payload.status === "waiting",
81 );
82 assert.isDefined(waitingProgressItem);
83 if (waitingProgressItem?.type === "turn-item.updated") {
84 assert.isNull(waitingProgressItem.payload.completedAt);
85 assert.isTrue(
86 waitingProgressItem.payload.type === "reasoning" && waitingProgressItem.payload.streaming,
87 );
88 }
75 // The coordinator's usage already covers its members, so the panel subtracts89 // The coordinator's usage already covers its members, so the panel subtracts
76 // theirs from it. That only stays non-negative if the provider's totals are90 // theirs from it. That only stays non-negative if the provider's totals are
77 // carried through unmodified.91 // carried through unmodified.