diff --git a/packages/server/src/server/agent/providers/claude/agent.sub-agent-replay.test.ts b/packages/server/src/server/agent/providers/claude/agent.sub-agent-replay.test.ts
index 666e701649..9fc82d6de8 100644
--- a/packages/server/src/server/agent/providers/claude/agent.sub-agent-replay.test.ts
+++ b/packages/server/src/server/agent/providers/claude/agent.sub-agent-replay.test.ts
@@ -422,6 +422,237 @@ describe("ClaudeAgentSession persisted subagent replay", () => {
});
});
+ describe("a background subagent", () => {
+ // Shapes recorded from Claude Code 2.1.x: the launch returns at once with an async result,
+ // and a child stopped by an interrupt ends on the interrupt placeholder, not an end_turn.
+ function backgroundLaunch(): string[] {
+ return [
+ parentEntry([
+ {
+ type: "tool_use",
+ id: TOOL_USE_ID,
+ name: "Agent",
+ input: {
+ subagent_type: "general-purpose",
+ description: "probe sleeper",
+ run_in_background: true,
+ },
+ },
+ ]),
+ JSON.stringify({
+ type: "user",
+ sessionId: "replay-session",
+ timestamp: "2026-07-26T06:27:48.000Z",
+ message: {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: TOOL_USE_ID,
+ content: [
+ {
+ type: "text",
+ text: `Async agent launched successfully.\nagentId: ${AGENT_ID}`,
+ },
+ ],
+ },
+ ],
+ },
+ toolUseResult: { isAsync: true, status: "async_launched", agentId: AGENT_ID },
+ }),
+ ];
+ }
+
+ function readStatus(event: { status?: string }): string | undefined {
+ return event.status;
+ }
+
+ function interruptedChild(): string[] {
+ return [
+ JSON.stringify({
+ type: "assistant",
+ isSidechain: true,
+ agentId: AGENT_ID,
+ timestamp: "2026-07-26T06:27:50.000Z",
+ message: {
+ role: "assistant",
+ content: [{ type: "tool_use", id: "toolu_child_bash", name: "Bash", input: {} }],
+ stop_reason: "tool_use",
+ },
+ }),
+ JSON.stringify({
+ type: "user",
+ isSidechain: true,
+ agentId: AGENT_ID,
+ timestamp: "2026-07-26T06:28:05.000Z",
+ message: {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "toolu_child_bash",
+ content: "The user doesn't want to proceed with this tool use.",
+ is_error: true,
+ },
+ ],
+ },
+ }),
+ JSON.stringify({
+ type: "user",
+ isSidechain: true,
+ agentId: AGENT_ID,
+ timestamp: "2026-07-26T06:28:05.001Z",
+ message: {
+ role: "user",
+ content: [{ type: "text", text: "[Request interrupted by user for tool use]" }],
+ },
+ }),
+ ];
+ }
+
+ test.each([
+ ["the meta sidecar", JSON.stringify({ toolUseId: TOOL_USE_ID })],
+ ["the legacy scrape", null],
+ ])("is canceled when an interrupt stopped it, linked through %s", async (_link, meta) => {
+ writeSession({ parentLines: backgroundLaunch(), meta, sidechainLines: interruptedChild() });
+
+ expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({
+ id: TOOL_USE_ID,
+ status: "canceled",
+ });
+ });
+
+ test("stays running when the child was given a new prompt after the interrupt", async () => {
+ writeSession({
+ parentLines: backgroundLaunch(),
+ meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
+ sidechainLines: [
+ ...interruptedChild(),
+ JSON.stringify({
+ type: "user",
+ isSidechain: true,
+ agentId: AGENT_ID,
+ timestamp: "2026-07-26T06:29:00.000Z",
+ message: { role: "user", content: [{ type: "text", text: "Carry on with the task." }] },
+ }),
+ ],
+ });
+
+ const statuses = upserts(await replayDescriptors()).map(readStatus);
+ expect(statuses).not.toContain("canceled");
+ expect(statuses).toContain("running");
+ });
+
+ test.each(["interrupted", "running"])(
+ "ignores an API request copy of a %s child's launch result",
+ async (state) => {
+ const launch = backgroundLaunch();
+ const result = JSON.parse(launch[1]!);
+ writeSession({
+ parentLines: [
+ ...launch,
+ JSON.stringify({
+ type: "api-request-blob",
+ sessionId: "replay-session",
+ hash: "request-copy",
+ message: result.message,
+ }),
+ ],
+ meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
+ sidechainLines:
+ state === "interrupted"
+ ? interruptedChild()
+ : [sidechainEntry({ stopReason: "tool_use" })],
+ });
+ expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({
+ id: TOOL_USE_ID,
+ status: state === "interrupted" ? "canceled" : "running",
+ });
+ },
+ );
+
+ test("stays running while the child has not finished", async () => {
+ writeSession({
+ parentLines: backgroundLaunch(),
+ meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
+ sidechainLines: [sidechainEntry({ stopReason: "tool_use" })],
+ });
+
+ const statuses = upserts(await replayDescriptors()).map(readStatus);
+ expect(statuses).not.toContain("completed");
+ expect(statuses).toContain("running");
+ });
+
+ function taskNotification(status: string, form = "user"): string {
+ const text = `\n${AGENT_ID}\n${TOOL_USE_ID}\n${status}\nBackground task ended\n`;
+ const records = {
+ user: { message: { role: "user", content: [{ type: "text", text }] } },
+ system: {
+ subtype: "task_notification",
+ task_id: AGENT_ID,
+ tool_use_id: TOOL_USE_ID,
+ status,
+ },
+ "queue-operation": { operation: "enqueue", content: text },
+ };
+ return JSON.stringify({
+ type: form,
+ timestamp: "2026-07-26T06:29:00.000Z",
+ ...records[form as keyof typeof records],
+ });
+ }
+
+ for (const form of ["user", "system", "queue-operation"]) {
+ test.each([
+ ["completed", "completed"],
+ ["failed", "failed"],
+ ["killed", "canceled"],
+ ["stopped", "canceled"],
+ ])(
+ `restores a %s task notification from ${form} when the child stop_reason is null`,
+ async (status, expected) => {
+ writeSession({
+ parentLines: [...backgroundLaunch(), taskNotification(status, form)],
+ meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
+ sidechainLines: [sidechainEntry({ stopReason: null })],
+ });
+ expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({
+ id: TOOL_USE_ID,
+ status: expected,
+ });
+ },
+ );
+ }
+
+ test("prefers a stopped notification over the child's end_turn", async () => {
+ writeSession({
+ parentLines: [...backgroundLaunch(), taskNotification("stopped")],
+ meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
+ sidechainLines: [sidechainEntry({ stopReason: "end_turn" })],
+ });
+ expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({ status: "canceled" });
+ });
+
+ test("does not invent an outcome for an unknown notification status", async () => {
+ writeSession({
+ parentLines: [...backgroundLaunch(), taskNotification("unknown")],
+ meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
+ sidechainLines: [sidechainEntry({ stopReason: null })],
+ });
+ expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({ status: "running" });
+ });
+
+ test("completes from the child's own end_turn", async () => {
+ writeSession({
+ parentLines: backgroundLaunch(),
+ meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
+ sidechainLines: [sidechainEntry({ stopReason: "end_turn" })],
+ });
+
+ expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({ status: "completed" });
+ });
+ });
+
test("does not replay a skill-spawned subagent the parent never named", async () => {
// The reported bug. A /code-review subagent has no Task tool_use, so its sidecar carries only
// agentType and its id appears nowhere in the parent — it used to replay as running forever,
diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts
index 6516ff81b7..e2cd9dab8f 100644
--- a/packages/server/src/server/agent/providers/claude/agent.ts
+++ b/packages/server/src/server/agent/providers/claude/agent.ts
@@ -33,7 +33,9 @@ import {
mapTaskNotificationSystemRecordToToolCall,
mapTaskNotificationUserContentToToolCall,
readTaskNotificationToolUseIdFromHistoryRecord,
+ readTaskNotificationFromHistoryRecord,
} from "./task-notification-tool-call.js";
+import { isClaudeInterruptPlaceholderText } from "./interrupt-placeholder.js";
import {
getClaudeContextWindowMaxTokens,
getClaudeModelsWithSettings,
@@ -56,6 +58,8 @@ import {
type ClaudeHookObservationInput,
} from "./subagents/live-source.js";
import { observeReplaySubagents, type ClaudeReplayParentFacts } from "./subagents/replay-source.js";
+import { mapTaskStatus } from "./subagents/task-status.js";
+import type { ProviderSubagentStatus } from "../../provider-subagents/store.js";
import { foldSubagentObservations, type SubagentObservation } from "./subagents/observation.js";
import { observeReplayWorkflows } from "./subagents/workflow-replay-source.js";
import { limitAgentTimelineItemContent } from "../../agent-timeline-content.js";
@@ -425,7 +429,6 @@ const CLAUDE_ROOT_ONLY_COMMANDS = new Set([
"usage",
]);
const INTERRUPT_TOOL_USE_PLACEHOLDER = "[Request interrupted by user for tool use]";
-const INTERRUPT_PLACEHOLDER_PATTERN = /^\[Request interrupted by user(?:[^\]]*)\]$/;
const NO_RESPONSE_REQUESTED_PLACEHOLDER = "No response requested.";
const STEER_SUPERSEDED_PERMISSION_MESSAGE =
"The user answered with a message instead of approving. Their message follows.";
@@ -789,11 +792,6 @@ function normalizeClaudeTranscriptText(value: unknown): string | null {
return normalized.length > 0 ? normalized : null;
}
-function isClaudeInterruptPlaceholderText(value: unknown): boolean {
- const normalized = normalizeClaudeTranscriptText(value);
- return normalized !== null && INTERRUPT_PLACEHOLDER_PATTERN.test(normalized);
-}
-
function isClaudeNoResponsePlaceholderText(value: unknown): boolean {
return normalizeClaudeTranscriptText(value) === NO_RESPONSE_REQUESTED_PLACEHOLDER;
}
@@ -6262,33 +6260,34 @@ async function readClaudeReplayParentFacts(
entries: Iterable | AsyncIterable,
): Promise {
const toolCalls = new Map();
- const linksByAgentId = new Map();
- const outcomes = new Map();
+ const linksByAgentId = new Map();
+ const outcomes: Map = new Map();
for await (const entry of entries) {
+ const notification = readTaskNotificationFromHistoryRecord(entry);
+ const status = mapTaskStatus(notification?.status ?? undefined);
+ if (notification?.toolUseId && status) {
+ outcomes.set(notification.toolUseId, { status });
+ }
const content = toObjectRecord(entry.message)?.content;
if (!Array.isArray(content)) continue;
for (const value of content) {
const block = toObjectRecord(value);
+ const declaration = readClaudeReplayTaskDeclaration(entry.type, block);
+ if (declaration) toolCalls.set(declaration.id, declaration.identity);
if (
- block?.type === "tool_use" &&
- (block.name === "Task" || block.name === "Agent") &&
- typeof block.id === "string"
- ) {
- const input = toObjectRecord(block.input);
- const title = readNonEmptyString(input?.name) ?? readNonEmptyString(input?.subagent_type);
- const description = readNonEmptyString(input?.description);
- toolCalls.set(block.id, {
- ...(title ? { title } : {}),
- ...(description ? { description } : {}),
+ entry.type !== "user" ||
+ block?.type !== "tool_result" ||
+ typeof block.tool_use_id !== "string"
+ )
+ continue;
+ if (toObjectRecord(entry.toolUseResult)?.status !== "async_launched")
+ outcomes.set(block.tool_use_id, {
+ status: block.is_error === true ? "failed" : "completed",
});
- }
- if (block?.type !== "tool_result" || typeof block.tool_use_id !== "string") continue;
- outcomes.set(block.tool_use_id, { failed: block.is_error === true });
const agentId = readLegacyClaudeSubagentId(block.content);
if (agentId)
linksByAgentId.set(agentId, {
toolCallId: block.tool_use_id,
- failed: block.is_error === true,
});
}
}
@@ -6299,6 +6298,29 @@ async function readClaudeReplayParentFacts(
};
}
+function readClaudeReplayTaskDeclaration(
+ entryType: unknown,
+ block: Record | undefined,
+): {
+ id: string;
+ identity: { title?: string; description?: string };
+} | null {
+ if (
+ entryType !== "assistant" ||
+ block?.type !== "tool_use" ||
+ (block.name !== "Task" && block.name !== "Agent") ||
+ typeof block.id !== "string"
+ )
+ return null;
+ const input = toObjectRecord(block.input);
+ const title = readNonEmptyString(input?.name) ?? readNonEmptyString(input?.subagent_type);
+ const description = readNonEmptyString(input?.description);
+ return {
+ id: block.id,
+ identity: { ...(title ? { title } : {}), ...(description ? { description } : {}) },
+ };
+}
+
function readLegacyClaudeSubagentId(content: unknown): string | undefined {
return /agentId:\s*([\w-]+)/.exec(JSON.stringify(content))?.[1];
}
diff --git a/packages/server/src/server/agent/providers/claude/interrupt-placeholder.ts b/packages/server/src/server/agent/providers/claude/interrupt-placeholder.ts
new file mode 100644
index 0000000000..f74c899234
--- /dev/null
+++ b/packages/server/src/server/agent/providers/claude/interrupt-placeholder.ts
@@ -0,0 +1,6 @@
+const INTERRUPT_PLACEHOLDER_PATTERN = /^\[Request interrupted by user(?:[^\]]*)\]$/;
+
+/** The text Claude Code records in place of a turn that an interrupt stopped. */
+export function isClaudeInterruptPlaceholderText(value: unknown): boolean {
+ return typeof value === "string" && INTERRUPT_PLACEHOLDER_PATTERN.test(value.trim());
+}
diff --git a/packages/server/src/server/agent/providers/claude/subagents/live-source.ts b/packages/server/src/server/agent/providers/claude/subagents/live-source.ts
index 2149fae139..af941aa24b 100644
--- a/packages/server/src/server/agent/providers/claude/subagents/live-source.ts
+++ b/packages/server/src/server/agent/providers/claude/subagents/live-source.ts
@@ -1,7 +1,8 @@
+import type { ProviderSubagentStatus } from "../../../provider-subagents/store.js";
+import { mapTaskStatus } from "./task-status.js";
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import type { AgentMetadata } from "../../../agent-sdk-types.js";
-import type { ProviderSubagentStatus } from "../../../provider-subagents/store.js";
import { resolveObservedClaudeModelId } from "../models.js";
import type { SubagentObservation } from "./observation.js";
import {
@@ -120,28 +121,6 @@ function readString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
-/**
- * The descriptor has no paused or pending state, and neither is terminal, so both read as
- * running. `killed` is a cancellation, not a failure — the child was stopped, it did not error.
- */
-function mapTaskStatus(status: string | undefined): ProviderSubagentStatus | undefined {
- switch (status) {
- case "pending":
- case "running":
- case "paused":
- return "running";
- case "completed":
- return "completed";
- case "failed":
- return "failed";
- case "killed":
- case "stopped":
- return "canceled";
- default:
- return undefined;
- }
-}
-
export interface ClaudeTaskProtocolSourceInput {
/**
* The parent's Task tool input, by tool_use id.
diff --git a/packages/server/src/server/agent/providers/claude/subagents/replay-source.test.ts b/packages/server/src/server/agent/providers/claude/subagents/replay-source.test.ts
index f5bbf52422..3bde4b813b 100644
--- a/packages/server/src/server/agent/providers/claude/subagents/replay-source.test.ts
+++ b/packages/server/src/server/agent/providers/claude/subagents/replay-source.test.ts
@@ -36,7 +36,7 @@ function parentWithTaskCall(failed = false): ClaudeReplayParentFacts {
],
]),
linksByAgentId: new Map(),
- outcomesByToolCallId: new Map([[TOOL_USE_ID, { failed }]]),
+ outcomesByToolCallId: new Map([[TOOL_USE_ID, { status: failed ? "failed" : "completed" }]]),
};
}
@@ -117,7 +117,7 @@ describe("observeReplaySubagents", () => {
[nestedToolUseId, { title: "Explore", description: "Nested audit" }],
]),
linksByAgentId: new Map(),
- outcomesByToolCallId: new Map([[nestedToolUseId, { failed: false }]]),
+ outcomesByToolCallId: new Map([[nestedToolUseId, { status: "completed" }]]),
},
},
{
@@ -173,8 +173,8 @@ describe("observeReplaySubagents", () => {
it("falls back to the scraped link when there is no meta file", async () => {
const parent: ClaudeReplayParentFacts = {
toolCalls: new Map([[TOOL_USE_ID, { title: "Explore" }]]),
- linksByAgentId: new Map([[AGENT_ID, { toolCallId: TOOL_USE_ID, failed: false }]]),
- outcomesByToolCallId: new Map(),
+ linksByAgentId: new Map([[AGENT_ID, { toolCallId: TOOL_USE_ID }]]),
+ outcomesByToolCallId: new Map([[TOOL_USE_ID, { status: "completed" }]]),
};
const [declared, ...rest] = (
@@ -263,8 +263,8 @@ describe("observeReplaySubagents", () => {
subagents: [{ agentId: AGENT_ID, meta: null, entries: [] }],
parent: {
toolCalls: new Map([[TOOL_USE_ID, { title: "Explore" }]]),
- linksByAgentId: new Map([[AGENT_ID, { toolCallId: TOOL_USE_ID, failed: true }]]),
- outcomesByToolCallId: new Map(),
+ linksByAgentId: new Map([[AGENT_ID, { toolCallId: TOOL_USE_ID }]]),
+ outcomesByToolCallId: new Map([[TOOL_USE_ID, { status: "failed" }]]),
},
convertEntry: () => [],
})
diff --git a/packages/server/src/server/agent/providers/claude/subagents/replay-source.ts b/packages/server/src/server/agent/providers/claude/subagents/replay-source.ts
index 2a3ca74846..02cbb783f0 100644
--- a/packages/server/src/server/agent/providers/claude/subagents/replay-source.ts
+++ b/packages/server/src/server/agent/providers/claude/subagents/replay-source.ts
@@ -3,6 +3,7 @@ import { z } from "zod";
import type { AgentTimelineItem } from "../../../agent-sdk-types.js";
import { normalizeProviderReplayTimestamp } from "../../../provider-history-timestamps.js";
import type { ProviderSubagentStatus } from "../../../provider-subagents/store.js";
+import { isClaudeInterruptPlaceholderText } from "../interrupt-placeholder.js";
import { resolveObservedClaudeModelId } from "../models.js";
import type { SubagentObservation } from "./observation.js";
import { buildClaudeSubagentSubtitle, type ClaudeSubagentUsage } from "./presentation.js";
@@ -139,9 +140,12 @@ export interface ClaudeReplayParentFacts {
/** Task `tool_use` id -> identity declared in the parent's tool input. */
toolCalls: ReadonlyMap;
/** agentId -> tool call, recovered by scraping tool-result text. Legacy fallback only. */
- linksByAgentId: ReadonlyMap;
- /** Task `tool_use` id -> outcome. Usable once meta.json supplies the link directly. */
- outcomesByToolCallId: ReadonlyMap;
+ linksByAgentId: ReadonlyMap;
+ /**
+ * Task `tool_use` id -> outcome, whichever way the child was linked. A background launch has
+ * none: its immediate result only says the child started.
+ */
+ outcomesByToolCallId: ReadonlyMap;
}
interface ParentLink {
@@ -177,27 +181,14 @@ function resolveParentLink(
parent: ClaudeReplayParentFacts,
): ParentLink | null {
const metaToolUseId = subagent.meta?.toolUseId?.trim();
- if (metaToolUseId && parent.toolCalls.has(metaToolUseId)) {
- const outcome = parent.outcomesByToolCallId.get(metaToolUseId);
- let status: ProviderSubagentStatus | null = null;
- if (outcome) status = outcome.failed ? "failed" : "completed";
- return {
- id: metaToolUseId,
- toolCallId: metaToolUseId,
- status,
- };
- }
+ const toolCallId =
+ metaToolUseId && parent.toolCalls.has(metaToolUseId)
+ ? metaToolUseId
+ : parent.linksByAgentId.get(subagent.agentId)?.toolCallId;
+ if (!toolCallId || !parent.toolCalls.has(toolCallId)) return null;
- const scraped = parent.linksByAgentId.get(subagent.agentId);
- if (scraped && parent.toolCalls.has(scraped.toolCallId)) {
- return {
- id: scraped.toolCallId,
- toolCallId: scraped.toolCallId,
- status: scraped.failed ? "failed" : "completed",
- };
- }
-
- return null;
+ const status = parent.outcomesByToolCallId.get(toolCallId)?.status ?? null;
+ return { id: toolCallId, toolCallId, status };
}
/**
@@ -276,9 +267,7 @@ async function observeSubagent(
lastTimestamp = timestamp;
Object.assign(runtime, readRuntime(entry));
usage = readUsage(entry) ?? usage;
- if (entry.type === "assistant") {
- childTerminalStatus = entry.message?.stop_reason === "end_turn" ? "completed" : null;
- }
+ childTerminalStatus = readChildTerminalStatus(entry, childTerminalStatus);
recordReplayToolOwners(toolOwners, entry, link.id);
for (const item of await convertEntry(entry)) {
timeline.push({ kind: "timeline", id: link.id, item, ...(timestamp ? { timestamp } : {}) });
@@ -308,6 +297,30 @@ async function observeSubagent(
return observations;
}
+/** Advance the child's fallback outcome without retaining transcript entries. */
+function readChildTerminalStatus(
+ entry: ClaudeReplayEntry,
+ previous: ProviderSubagentStatus | null,
+): ProviderSubagentStatus | null {
+ if (entry.type === "assistant") {
+ return entry.message?.stop_reason === "end_turn" ? "completed" : null;
+ }
+ if (entry.type !== "user") return previous;
+ const texts = readUserTexts(entry);
+ if (texts.length === 0) return previous;
+ return texts.some(isClaudeInterruptPlaceholderText) ? "canceled" : null;
+}
+
+/** Tool results carry no user text and do not change the child's terminal status. */
+function readUserTexts(entry: ClaudeReplayEntry): string[] {
+ const content = entry.message?.content;
+ if (typeof content === "string") return [content];
+ if (!Array.isArray(content)) return [];
+ return content.flatMap((block: { type?: unknown; text?: unknown } | null) =>
+ block?.type === "text" && typeof block.text === "string" ? [block.text] : [],
+ );
+}
+
function recordReplayToolOwners(
owners: Map,
entry: ClaudeReplayEntry,
diff --git a/packages/server/src/server/agent/providers/claude/subagents/task-status.ts b/packages/server/src/server/agent/providers/claude/subagents/task-status.ts
new file mode 100644
index 0000000000..a488ce12d7
--- /dev/null
+++ b/packages/server/src/server/agent/providers/claude/subagents/task-status.ts
@@ -0,0 +1,23 @@
+import type { ProviderSubagentStatus } from "../../../provider-subagents/store.js";
+
+/**
+ * The descriptor has no paused or pending state, and neither is terminal, so both read as
+ * running. `killed` is a cancellation, not a failure — the child was stopped, it did not error.
+ */
+export function mapTaskStatus(status: string | undefined): ProviderSubagentStatus | undefined {
+ switch (status) {
+ case "pending":
+ case "running":
+ case "paused":
+ return "running";
+ case "completed":
+ return "completed";
+ case "failed":
+ return "failed";
+ case "killed":
+ case "stopped":
+ return "canceled";
+ default:
+ return undefined;
+ }
+}
diff --git a/packages/server/src/server/agent/providers/claude/task-notification-tool-call.ts b/packages/server/src/server/agent/providers/claude/task-notification-tool-call.ts
index c8e38bf918..76ad89d8ef 100644
--- a/packages/server/src/server/agent/providers/claude/task-notification-tool-call.ts
+++ b/packages/server/src/server/agent/providers/claude/task-notification-tool-call.ts
@@ -407,19 +407,21 @@ export function mapTaskNotificationSystemRecordToToolCall(
}
export function readTaskNotificationToolUseIdFromHistoryRecord(record: unknown): string | null {
+ return readTaskNotificationFromHistoryRecord(record)?.toolUseId ?? null;
+}
+
+export function readTaskNotificationFromHistoryRecord(
+ record: unknown,
+): TaskNotificationEnvelope | null {
const parsedRecord = TaskNotificationHistoryRecordSchema.safeParse(record);
- if (!parsedRecord.success) {
- return null;
- }
+ if (!parsedRecord.success) return null;
if (parsedRecord.data.type === "user" && parsedRecord.data.message) {
- return (
- parseTaskNotificationFromUserContent({
- content: parsedRecord.data.message.content,
- messageId: parsedRecord.data.uuid ?? parsedRecord.data.message_id,
- })?.toolUseId ?? null
- );
+ return parseTaskNotificationFromUserContent({
+ content: parsedRecord.data.message.content,
+ messageId: parsedRecord.data.uuid ?? parsedRecord.data.message_id,
+ });
}
- return parseTaskNotificationFromSystemRecord(record)?.toolUseId ?? null;
+ return parseTaskNotificationFromSystemRecord(record);
}
export function coerceTaskNotificationHistoryRecordToSystemMessage(