diff --git a/src/hooks/tool-pair-validator/hook.test.ts b/src/hooks/tool-pair-validator/hook.test.ts index 6b18f15f0..a6449da76 100644 --- a/src/hooks/tool-pair-validator/hook.test.ts +++ b/src/hooks/tool-pair-validator/hook.test.ts @@ -6,6 +6,7 @@ declare const expect: (value: T) => { } import { createToolPairValidatorHook } from "./hook" +import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state/state" const TOOL_RESULT_PLACEHOLDER = "Tool output unavailable (context compacted)" @@ -19,7 +20,7 @@ type TestPart = { } type TestMessage = { - info: { role: "assistant" | "user" } + info: { role: "assistant" | "user"; sessionID?: string } parts: TestPart[] } @@ -153,4 +154,46 @@ describe("createToolPairValidatorHook", () => { { type: "text", text: "continue" }, ]) }) + + it("leaves tracked subagent sessions unchanged while normal sessions still repair", async () => { + //#given + _resetForTesting() + subagentSessions.add("ses_background_1") + const backgroundMessages = [ + { + info: { role: "assistant", sessionID: "ses_background_1" }, + parts: [{ type: "tool_use", id: "toolu_background_1" }], + }, + { + info: { role: "assistant", sessionID: "ses_background_1" }, + parts: [{ type: "text", text: "background agent keeps reasoning" }], + }, + ] satisfies TestMessage[] + const originalBackgroundMessages = JSON.parse(JSON.stringify(backgroundMessages)) + const mainMessages = [ + { + info: { role: "assistant", sessionID: "ses_main_1" }, + parts: [{ type: "tool_use", id: "toolu_main_1" }], + }, + { + info: { role: "user", sessionID: "ses_main_1" }, + parts: [{ type: "text", text: "continue main session" }], + }, + ] satisfies TestMessage[] + + try { + //#when + await runTransform(backgroundMessages) + await runTransform(mainMessages) + + //#then + expect(backgroundMessages).toEqual(originalBackgroundMessages) + expect(mainMessages[1]?.parts).toEqual([ + { type: "tool_result", tool_use_id: "toolu_main_1", content: TOOL_RESULT_PLACEHOLDER }, + { type: "text", text: "continue main session" }, + ]) + } finally { + _resetForTesting() + } + }) }) diff --git a/src/hooks/tool-pair-validator/hook.ts b/src/hooks/tool-pair-validator/hook.ts index 89a76e701..72d53fee0 100644 --- a/src/hooks/tool-pair-validator/hook.ts +++ b/src/hooks/tool-pair-validator/hook.ts @@ -1,5 +1,6 @@ import type { Message, Part } from "@opencode-ai/sdk" +import { subagentSessions } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" const TOOL_RESULT_PLACEHOLDER = "Tool output unavailable (context compacted)" @@ -134,6 +135,11 @@ function getMessageID(message: TransformMessageInfo): string | undefined { return typeof candidate.id === "string" ? candidate.id : undefined } +function getMessageSessionID(message: TransformMessageInfo): string | undefined { + const candidate = message as { sessionID?: unknown } + return typeof candidate.sessionID === "string" ? candidate.sessionID : undefined +} + function repairMissingToolResults(messages: MessageWithParts[], assistantIndex: number): void { const assistantMessage = messages[assistantIndex] const toolUseIDs = extractUniqueToolUseIDs(assistantMessage.parts) @@ -173,7 +179,15 @@ export function createToolPairValidatorHook(): MessagesTransformHook { return { "experimental.chat.messages.transform": async (_input, output) => { for (let i = 0; i < output.messages.length; i++) { - if (output.messages[i].info.role !== "assistant") { + const messageInfo = output.messages[i].info + + if (messageInfo.role !== "assistant") { + continue + } + + const sessionID = getMessageSessionID(messageInfo) + if (sessionID && subagentSessions.has(sessionID)) { + log("[tool-pair-validator] Skipping repair for subagent session", { sessionID }) continue }