| ... | ... | @@ -380,26 +380,26 @@ interface ClaudeTaskAgentState { |
| 380 | 380 | } |
| 381 | 381 | |
| 382 | 382 | /** |
| 383 | | * How many racing snapshot models to buffer per session. A snapshot whose |
| 384 | | * task_started never arrives would otherwise pin its entry for the session's |
| 383 | * How many entries each task_started lookup map buffers per session. An entry |
| 384 | * whose task_started never arrives would otherwise pin it for the session's |
| 385 | 385 | * lifetime; oldest entries evict first. |
| 386 | 386 | */ |
| 387 | | const PENDING_TASK_MODEL_CAP = 64; |
| 387 | const PENDING_TASK_ENTRY_CAP = 256; |
| 388 | 388 | /** How long Stop waits for Claude to abort a turn before killing the process. */ |
| 389 | 389 | const CLAUDE_INTERRUPT_GRACE = "3 seconds"; |
| 390 | 390 | |
| 391 | 391 | /** |
| 392 | | * Buffers a subagent snapshot's authoritative model under its |
| 393 | | * parent_tool_use_id, for snapshots that beat their task_started to the |
| 394 | | * stream. task_started consumes the entry when it registers the task. |
| 392 | * Buffers a value that a later task_started reads by tool_use_id (a racing |
| 393 | * snapshot model, or a subagent's tool_use owner). task_started consumes the |
| 394 | * entry when it registers the task. |
| 395 | 395 | */ |
| 396 | | function rememberPendingTaskModel( |
| 396 | function rememberPendingTaskEntry( |
| 397 | 397 | pending: Map<string, string>, |
| 398 | | parentToolUseId: string, |
| 399 | | model: string, |
| 398 | toolUseId: string, |
| 399 | value: string, |
| 400 | 400 | ): void { |
| 401 | | pending.set(parentToolUseId, model); |
| 402 | | if (pending.size > PENDING_TASK_MODEL_CAP) { |
| 401 | pending.set(toolUseId, value); |
| 402 | if (pending.size > PENDING_TASK_ENTRY_CAP) { |
| 403 | 403 | const oldest = pending.keys().next(); |
| 404 | 404 | if (!oldest.done) { |
| 405 | 405 | pending.delete(oldest.value); |
| ... | ... | @@ -434,9 +434,16 @@ interface ClaudeSessionContext { |
| 434 | 434 | /** |
| 435 | 435 | * Authoritative subagent models from assistant snapshots that arrived before |
| 436 | 436 | * their task_started registered the task, keyed by parent_tool_use_id. |
| 437 | | * Written through `rememberPendingTaskModel`, consumed by task_started. |
| 437 | * Written through `rememberPendingTaskEntry`, consumed by task_started. |
| 438 | 438 | */ |
| 439 | 439 | readonly pendingTaskModels: Map<string, string>; |
| 440 | /** |
| 441 | * tool_use_id → parent_tool_use_id for tool calls made inside a subagent. |
| 442 | * The SDK forwards those calls only as assistant snapshots, never as stream |
| 443 | * events, so task_started reads this to find the owner of a task launched |
| 444 | * from inside a subagent. |
| 445 | */ |
| 446 | readonly subagentToolParents: Map<string, string>; |
| 440 | 447 | /** |
| 441 | 448 | * Last emitted workflow-member fingerprint per member slot. A coordinator |
| 442 | 449 | * task_progress repeats the FULL member array every tick; without a |
| ... | ... | @@ -3370,18 +3377,45 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3370 | 3377 | const owningTaskId = agentIdForParentToolUse(context.taskAgents, assistantParentToolUseId); |
| 3371 | 3378 | const snapshotModel = trimmedString(message.message.model); |
| 3372 | 3379 | const owningAgent = owningTaskId ? context.taskAgents.get(owningTaskId) : undefined; |
| 3373 | | if (snapshotModel) { |
| 3374 | | if (owningAgent) { |
| 3380 | const snapshotContent = Array.isArray(message.message.content) ? message.message.content : []; |
| 3381 | for (const block of snapshotContent) { |
| 3382 | if (block.type === "tool_use") { |
| 3383 | rememberPendingTaskEntry(context.subagentToolParents, block.id, assistantParentToolUseId); |
| 3384 | } |
| 3385 | } |
| 3386 | if (snapshotModel && owningAgent) { |
| 3387 | if (owningAgent.model !== snapshotModel) { |
| 3375 | 3388 | owningAgent.model = snapshotModel; |
| 3376 | | } else { |
| 3377 | | // The snapshot beat its task_started (or its tool_use_id was never |
| 3378 | | // recorded): hold the model until the task registers. |
| 3379 | | rememberPendingTaskModel( |
| 3380 | | context.pendingTaskModels, |
| 3381 | | assistantParentToolUseId, |
| 3382 | | snapshotModel, |
| 3383 | | ); |
| 3389 | // Push the correction now. The next task row can be minutes away |
| 3390 | // when the subagent blocks on a long foreground tool. |
| 3391 | const stamp = yield* makeEventStamp(); |
| 3392 | yield* offerRuntimeEvent({ |
| 3393 | type: "task.updated", |
| 3394 | eventId: stamp.eventId, |
| 3395 | provider: PROVIDER, |
| 3396 | createdAt: stamp.createdAt, |
| 3397 | threadId: context.session.threadId, |
| 3398 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 3399 | payload: { |
| 3400 | taskId: RuntimeTaskId.make(owningAgent.taskId), |
| 3401 | ...taskLinkageFor(context.taskAgents, owningAgent.taskId), |
| 3402 | }, |
| 3403 | providerRefs: nativeProviderRefs(context), |
| 3404 | raw: { |
| 3405 | source: "claude.sdk.message", |
| 3406 | method: "claude/assistant", |
| 3407 | payload: message, |
| 3408 | }, |
| 3409 | }); |
| 3384 | 3410 | } |
| 3411 | } else if (snapshotModel) { |
| 3412 | // The snapshot beat its task_started (or its tool_use_id was never |
| 3413 | // recorded): hold the model until the task registers. |
| 3414 | rememberPendingTaskEntry( |
| 3415 | context.pendingTaskModels, |
| 3416 | assistantParentToolUseId, |
| 3417 | snapshotModel, |
| 3418 | ); |
| 3385 | 3419 | } |
| 3386 | 3420 | context.lastAssistantUuid = message.uuid; |
| 3387 | 3421 | yield* updateResumeCursor(context); |
| ... | ... | @@ -3735,15 +3769,23 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3735 | 3769 | }); |
| 3736 | 3770 | return; |
| 3737 | 3771 | case "task_started": { |
| 3738 | | // A task launched by a tool that itself ran inside a subagent (the |
| 3739 | | // in-flight tool carries agentId from parent_tool_use_id) is |
| 3740 | | // agent-internal: a subagent's background shell, not parent work. |
| 3741 | | const launchingTool = message.tool_use_id |
| 3742 | | ? Array.from(context.inFlightTools.values()).find( |
| 3743 | | (tool) => tool.itemId === message.tool_use_id, |
| 3744 | | ) |
| 3772 | // A task launched by a tool that itself ran inside a subagent is |
| 3773 | // agent-internal: a subagent's background shell, not parent work. A |
| 3774 | // streamed tool carries agentId from parent_tool_use_id; a tool seen |
| 3775 | // only in a subagent snapshot resolves through subagentToolParents. |
| 3776 | const toolUseId = message.tool_use_id; |
| 3777 | const launchingTool = toolUseId |
| 3778 | ? Array.from(context.inFlightTools.values()).find((tool) => tool.itemId === toolUseId) |
| 3745 | 3779 | : undefined; |
| 3746 | | const owningAgentId = launchingTool?.agentId; |
| 3780 | const launchParentToolUseId = toolUseId |
| 3781 | ? context.subagentToolParents.get(toolUseId) |
| 3782 | : undefined; |
| 3783 | if (toolUseId) { |
| 3784 | context.subagentToolParents.delete(toolUseId); |
| 3785 | } |
| 3786 | const owningAgentId = |
| 3787 | launchingTool?.agentId ?? |
| 3788 | agentIdForParentToolUse(context.taskAgents, launchParentToolUseId); |
| 3747 | 3789 | if ( |
| 3748 | 3790 | context.turnState && |
| 3749 | 3791 | classifyTaskAgentKind({ |
| ... | ... | @@ -3754,13 +3796,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3754 | 3796 | context.turnState.hasSubagents = true; |
| 3755 | 3797 | } |
| 3756 | 3798 | // Model/effort: the Agent tool's input carries explicit overrides; |
| 3757 | | // absent ones inherit the session's selection (SDK behavior). |
| 3758 | | // Subagent assistant snapshots refine model with the authoritative API |
| 3759 | | // id: one that already arrived is buffered and outranks the seed here, |
| 3760 | | // later ones refine the record in place. AgentInput.effort may be a |
| 3761 | | // named level or an integer. |
| 3799 | // absent ones inherit the owning subagent's model, else the session's |
| 3800 | // selection (SDK behavior). Subagent assistant snapshots refine model |
| 3801 | // with the authoritative API id: one that already arrived is buffered |
| 3802 | // and outranks the seed here, later ones refine the record in place. |
| 3803 | // AgentInput.effort may be a named level or an integer. |
| 3762 | 3804 | const launchInput = launchingTool?.input; |
| 3763 | | const toolUseId = message.tool_use_id; |
| 3764 | 3805 | const bufferedModel = toolUseId ? context.pendingTaskModels.get(toolUseId) : undefined; |
| 3765 | 3806 | if (toolUseId) { |
| 3766 | 3807 | context.pendingTaskModels.delete(toolUseId); |
| ... | ... | @@ -3768,6 +3809,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3768 | 3809 | const model = |
| 3769 | 3810 | bufferedModel ?? |
| 3770 | 3811 | trimmedString(launchInput?.model) ?? |
| 3812 | (owningAgentId ? context.taskAgents.get(owningAgentId)?.model : undefined) ?? |
| 3771 | 3813 | trimmedString(context.session.model ?? undefined); |
| 3772 | 3814 | const rawLaunchEffort = launchInput?.effort; |
| 3773 | 3815 | const effort = |
| ... | ... | @@ -4471,6 +4513,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 4471 | 4513 | const claudeTasks = new Map<string, ClaudeTaskState>(); |
| 4472 | 4514 | const taskAgents = new Map<string, ClaudeTaskAgentState>(); |
| 4473 | 4515 | const pendingTaskModels = new Map<string, string>(); |
| 4516 | const subagentToolParents = new Map<string, string>(); |
| 4474 | 4517 | const workflowMemberFingerprints = new Map<string, string>(); |
| 4475 | 4518 | const liveTaskIds = new Set<string>(); |
| 4476 | 4519 | |
| ... | ... | @@ -5078,6 +5121,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 5078 | 5121 | claudeTasks, |
| 5079 | 5122 | taskAgents, |
| 5080 | 5123 | pendingTaskModels, |
| 5124 | subagentToolParents, |
| 5081 | 5125 | workflowMemberFingerprints, |
| 5082 | 5126 | liveTaskIds, |
| 5083 | 5127 | turnState: undefined, |