+
);
diff --git a/src/common/utils/subagentFailureEnvelope.test.ts b/src/common/utils/subagentFailureEnvelope.test.ts
new file mode 100644
index 0000000000..ce5d36bd2f
--- /dev/null
+++ b/src/common/utils/subagentFailureEnvelope.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, test } from "bun:test";
+import { formatSubagentFailureUserMessage } from "@/node/services/taskWorkspaceSeam";
+import { parseSubagentFailureEnvelope } from "./subagentFailureEnvelope";
+
+const failure = {
+ childWorkspaceId: "task-123",
+ agentType: "exec",
+ errorType: "workspace_turn_superseded",
+ errorMessage: "New input took over.\nThe workspace continues.",
+};
+
+describe("parseSubagentFailureEnvelope", () => {
+ test("round-trips producer messages with each combination of optional execution metadata", () => {
+ for (const metadata of [
+ {},
+ { executionId: "wst_123" },
+ { executionVersion: "wst_123:interrupted:2026-09-04T12:04:40.370Z" },
+ { executionId: "wst_123", executionVersion: "wst_123:failed:2026-09-04T12:04:40.370Z" },
+ ]) {
+ expect(
+ parseSubagentFailureEnvelope(formatSubagentFailureUserMessage({ ...failure, ...metadata }))
+ ).toEqual({
+ taskId: failure.childWorkspaceId,
+ agentType: failure.agentType,
+ errorType: failure.errorType,
+ errorMessage: failure.errorMessage,
+ ...metadata,
+ });
+ }
+ });
+
+ test("preserves delimiter examples and whitespace inside error messages", () => {
+ const errorMessage = ` Diagnostic:\n${formatSubagentFailureUserMessage(failure)}\n trailing `;
+ expect(
+ parseSubagentFailureEnvelope(formatSubagentFailureUserMessage({ ...failure, errorMessage }))
+ ?.errorMessage
+ ).toBe(errorMessage);
+ });
+
+ test("rejects incomplete envelopes, empty required fields, and surrounding content", () => {
+ const valid = formatSubagentFailureUserMessage(failure);
+ for (const content of [
+ "ordinary message",
+ valid.replace("", ""),
+ valid.replace("
task-123", "
"),
+ valid.replace(failure.errorMessage, " "),
+ `Before\n${valid}`,
+ `${valid}\nAfter`,
+ ]) {
+ expect(parseSubagentFailureEnvelope(content)).toBeNull();
+ }
+ });
+});
diff --git a/src/common/utils/subagentFailureEnvelope.ts b/src/common/utils/subagentFailureEnvelope.ts
new file mode 100644
index 0000000000..2f1b1f3c21
--- /dev/null
+++ b/src/common/utils/subagentFailureEnvelope.ts
@@ -0,0 +1,32 @@
+export interface SubagentFailureEnvelope {
+ taskId: string;
+ agentType: string;
+ errorType: string;
+ errorMessage: string;
+ executionVersion?: string;
+ executionId?: string;
+}
+
+/** Parse the persisted failure protocol without changing the model-facing message. */
+export function parseSubagentFailureEnvelope(content: string): SubagentFailureEnvelope | null {
+ // Match the entire producer envelope so malformed or mixed-content messages remain visible as-is.
+ // The error body is greedy: embedded protocol examples must not truncate the actual diagnostic.
+ const match =
+ /^
\n([^\n<>]+)<\/task_id>\n(?:([^\n<>]+)<\/execution_version>\n)?(?:([^\n<>]+)<\/execution_id>\n)?([^\n<>]+)<\/agent_type>\n([^\n<>]+)<\/error_type>\n\n([\s\S]+)\n<\/error_message>\nThis sub-agent task failed terminally and will not produce a report\. Do not re-await it\.\n<\/mux_subagent_failure>$/.exec(
+ content
+ );
+ if (!match) return null;
+
+ const [, taskId, executionVersion, executionId, agentType, errorType, errorMessage] = match;
+ if (![taskId, agentType, errorType, errorMessage].every((field) => field.trim().length > 0)) {
+ return null;
+ }
+ return {
+ taskId,
+ agentType,
+ errorType,
+ errorMessage,
+ ...(executionVersion ? { executionVersion } : {}),
+ ...(executionId ? { executionId } : {}),
+ };
+}