@@ -63,7 +63,7 @@ import type { Settings } from "../config/settings.js";
6363import { resolveEffortForRole } from "../provider/reasoning-effort.js" ;
6464import { isCodexProviderName } from "../config/codex-providers.js" ;
6565import { buildDispatchBrief , type TaskIntent } from "./report.js" ;
66- import type { SubAgentSessionStore } from "./session-store.js" ;
66+ import { DEFAULT_CANCEL_REASON , type SubAgentSessionStore } from "./session-store.js" ;
6767import type {
6868 NestedDispatchDeps ,
6969 RunSubAgentParams ,
@@ -75,6 +75,8 @@ import { cleanupSubAgentWorktree, createSubAgentWorktree, WorktreeError } from "
7575import { NOOP_TELEMETRY , type Telemetry } from "../telemetry/index.js" ;
7676import { classifyAgentName } from "../telemetry/classify.js" ;
7777import type { DirectorPackage } from "../agent/directors/types.js" ;
78+ import { formatSubAgentTaskAuthFailureMessage } from "./inference-auth-failure.js" ;
79+ import { isSubAgentCancelError } from "./dispose.js" ;
7880
7981const log = getLogger ( [ LOG_NAMESPACE_ROOT , "subagent" , "agent-fleet" ] ) ;
8082
@@ -718,11 +720,24 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool {
718720 } )
719721 . catch ( ( err ) => {
720722 // Always terminalize fleetRecords — including pre-progress cancel that
721- // rethrows with no salvage — so wait_agents does not hang. fail()
722- // no-ops when cancel already flipped the strip status.
723- const message = err instanceof Error ? err . message : String ( err ) ;
724- deps . fleetRecords . reject ( session . id , message ) ;
725- deps . sessions . fail ( session . id , message ) ;
723+ // rethrows with no salvage — so wait_agents does not hang. Prefer
724+ // cancel semantics over fail when the strip already cancelled or the
725+ // throw is an AbortError (legacy task() parent contract).
726+ const alreadyCancelled = deps . sessions . get ( session . id ) ?. status === "cancelled" ;
727+ if ( alreadyCancelled || isSubAgentCancelError ( err , childCtl . signal ) ) {
728+ if ( ! alreadyCancelled ) {
729+ deps . sessions . cancel ( session . id , DEFAULT_CANCEL_REASON ) ;
730+ }
731+ const message = err instanceof Error ? err . message : String ( err ) ;
732+ deps . fleetRecords . reject ( session . id , message ) ;
733+ return ;
734+ }
735+ // Auth failures keep the actionable Re-authenticate wording that
736+ // task()'s fused path surfaces via formatSubAgentTaskAuthFailureMessage.
737+ const authMessage = formatSubAgentTaskAuthFailureMessage ( description , err ) ;
738+ const failReason = authMessage ?? ( err instanceof Error ? err . message : String ( err ) ) ;
739+ deps . fleetRecords . reject ( session . id , failReason ) ;
740+ deps . sessions . fail ( session . id , failReason ) ;
726741 } )
727742 . finally ( ( ) => {
728743 telemetry . capture ( "subagent_end" , {
0 commit comments