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