fix(team-mode): surface member error to main agent (#3923)

When a team member task errored, the failure stayed in the member's
internal state and the main/coordinator agent's wait/status loop
kept polling indefinitely — the run stalled with no visible error.
Emit a structured `member_error` peer_message into the team mailbox
on member error transition, naming the member and including the
underlying error text so the main agent's next team_status (or
pending-message read) returns a terminal failure instead of an
empty in-progress poll.

Regression test asserts the failure is visible in the main agent's
view after a member task errors mid-execution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ZeyuFu
2026-05-16 01:40:08 -04:00
parent 1723a8b74c
commit 2bf5038215
2 changed files with 109 additions and 0 deletions
@@ -1,11 +1,15 @@
import { randomUUID } from "node:crypto"
import type { TeamModeConfig } from "../../config/schema/team-mode"
import { findResolvedMemberSession } from "../../features/team-mode/member-session-resolution"
import { sendMessage } from "../../features/team-mode/team-mailbox/send"
import {
releaseDeliveryReservation,
reserveMessageForDelivery,
} from "../../features/team-mode/team-mailbox/reservation"
import { loadRuntimeState, transitionRuntimeState } from "../../features/team-mode/team-state-store/store"
import { resolveSessionEventID } from "../../shared/event-session-id"
import { isRecord } from "../../shared/record-type-guard"
import { log } from "../../shared/logger"
type HookInput = { event: { type: string; properties?: unknown } }
@@ -15,6 +19,18 @@ function getErroredSessionID(properties: unknown): string | undefined {
return resolveSessionEventID(properties)
}
function extractErrorText(properties: unknown): string {
const props = isRecord(properties) ? properties : undefined
const errorValue = props?.["error"]
if (errorValue instanceof Error) {
return errorValue.message
}
if (typeof errorValue === "string" && errorValue.length > 0) {
return errorValue
}
return "unknown error"
}
async function requeuePendingLiveDeliveries(
teamRunId: string,
memberName: string,
@@ -62,6 +78,35 @@ export function createTeamMemberErrorHandler(config: TeamModeConfig): HookImpl {
)),
}), config)
const leaderMember = runtimeState.members.find((member) => member.agentType === "leader")
if (leaderMember !== undefined && leaderMember.name !== runtimeMember.memberName) {
const errorText = extractErrorText(event.properties)
const errorBody = `Team member "${runtimeMember.memberName}" has entered an error state and will not complete its task.\nError: ${errorText}`
try {
await sendMessage(
{
version: 1,
messageId: randomUUID(),
from: "system",
to: leaderMember.name,
kind: "announcement",
body: errorBody,
timestamp: Date.now(),
},
runtimeState.teamRunId,
config,
{ isLead: true, activeMembers: runtimeState.members.map((m) => m.name) },
)
} catch (sendError) {
log("team member error handler: failed to notify lead of member error", {
event: "team-mode-member-error-notify-failed",
teamRunId: runtimeState.teamRunId,
memberName: runtimeMember.memberName,
error: sendError instanceof Error ? sendError.message : String(sendError),
})
}
}
log("team member session errored", {
event: "team-mode-member-errored",
teamRunId: runtimeState.teamRunId,