From 17030b9a0d8a2f11511aa6406918829b36e2d1cd Mon Sep 17 00:00:00 2001 From: PeterPonyu Date: Thu, 14 May 2026 21:32:48 -0400 Subject: [PATCH] Prevent subagent repair from corrupting background sessions Guard the tool-pair validator when transformed assistant messages belong to a tracked subagent session, while keeping normal main-session orphaned tool_use repair intact. Constraint: code-yeongyu/oh-my-openagent#3996 reports background Oracle sessions hanging after synthetic placeholder tool_result insertion. Rejected: disable tool-pair-validator globally | would regress main-session compaction/orphaned tool_use repair. Confidence: high Scope-risk: narrow Directive: Keep subagent skip coverage and normal repair coverage together when changing tool-pair validation. Tested: bun test src/hooks/tool-pair-validator/hook.test.ts src/plugin/messages-transform.test.ts Tested: bun run typecheck Not-tested: live Oracle background task e2e; no remote push or PR comment performed. --- src/hooks/tool-pair-validator/hook.test.ts | 45 +++++++++++++++++++++- src/hooks/tool-pair-validator/hook.ts | 16 +++++++- 2 files changed, 59 insertions(+), 2 deletions(-) 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 }