From ea5f6ddfd7a243d969ca23d17aca5ff4a58b981f Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 17 May 2026 00:08:56 +0900 Subject: [PATCH] fix(call-omo-agent): register bootstrap and session agent before sync prompt dispatch call_omo_agent sync path created the child OpenCode session and went straight into promptAsync without registering child session agent, session tools, or bootstrap state. If first dispatch failed before any durable user message persisted, runtime fallback could not reconstruct the original prompt or the agent identity for that child session. Bind setSessionAgent and setSessionTools to the child session id with the same tool restrictions that the prompt body sends, register a delegated child session bootstrap with the prompt text, fallback chain, and prompt tools, then clean bootstrap + session tools in finally for sessions this call created. --- .../call-omo-agent/sync-executor.test.ts | 61 ++++++++++++++++++- src/tools/call-omo-agent/sync-executor.ts | 50 +++++++++++---- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/src/tools/call-omo-agent/sync-executor.test.ts b/src/tools/call-omo-agent/sync-executor.test.ts index 3b0e0db5f..58d56a16d 100644 --- a/src/tools/call-omo-agent/sync-executor.test.ts +++ b/src/tools/call-omo-agent/sync-executor.test.ts @@ -1,5 +1,5 @@ import { unsafeTestValue } from "../../../test-support/unsafe-test-value" -const { describe, test, expect, mock } = require("bun:test") +import { describe, test, expect, mock } from "bun:test" type ExecuteSync = typeof import("./sync-executor").executeSync @@ -13,6 +13,7 @@ type PromptAsyncInput = { variant?: string temperature?: number topP?: number + maxOutputTokens?: number options?: Record } } @@ -342,6 +343,64 @@ describe("executeSync", () => { expect(deps.setSessionFallbackChain).toHaveBeenCalledWith("ses-fallback", fallbackChain) }) + test("registers child-session bootstrap and tracked prompt state before sync prompt dispatch", async () => { + //#given + const executeSync = await importExecuteSync() + const { _resetForTesting, getSessionAgent } = require("../../features/claude-code-session-state") + const { clearAllDelegatedChildSessionBootstrap, getDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap") + const { clearSessionTools, getSessionTools } = require("../../shared/session-tools-store") + const deps = createDependencies({ + createOrGetSession: mock(async () => ({ sessionID: "ses-call-bootstrap", isNew: true })), + }) + const toolContext = createToolContext() + const observed: Array<{ + agent: string | undefined + tools: Record | undefined + bootstrap: ReturnType + }> = [] + const recorder = createPromptAsyncRecorder(async () => { + observed.push({ + agent: getSessionAgent("ses-call-bootstrap"), + tools: getSessionTools("ses-call-bootstrap"), + bootstrap: getDelegatedChildSessionBootstrap("ses-call-bootstrap"), + }) + return { data: {} } + }) + const args = { + subagent_type: "explore", + description: "bootstrap state", + prompt: "collect bootstrap evidence", + run_in_background: false, + } + const fallbackChain = [ + { providers: ["openai"], model: "gpt-5.4", variant: "high" }, + ] + + try { + //#when + await executeSync( + args, + toolContext, + createContext(recorder.promptAsync) as never, + deps, + fallbackChain + ) + + //#then + expect(observed[0]?.agent).toBe("explore") + expect(observed[0]?.tools?.question).toBe(false) + expect(observed[0]?.tools?.task).toBe(false) + expect(observed[0]?.bootstrap?.retryParts[0]?.text).toContain("collect bootstrap evidence") + expect(observed[0]?.bootstrap?.tools?.question).toBe(false) + expect(observed[0]?.bootstrap?.fallbackChain?.[0]?.model).toBe("gpt-5.4") + expect(getDelegatedChildSessionBootstrap("ses-call-bootstrap")).toBeUndefined() + } finally { + clearAllDelegatedChildSessionBootstrap() + clearSessionTools() + _resetForTesting() + } + }) + test("returns dedicated agent-not-found error with task metadata", async () => { //#given const executeSync = await importExecuteSync() diff --git a/src/tools/call-omo-agent/sync-executor.ts b/src/tools/call-omo-agent/sync-executor.ts index dfb47b62b..59a802263 100644 --- a/src/tools/call-omo-agent/sync-executor.ts +++ b/src/tools/call-omo-agent/sync-executor.ts @@ -1,15 +1,20 @@ -import type { CallOmoAgentArgs } from "./types" import type { PluginInput } from "@opencode-ai/plugin" -import { subagentSessions, syncSubagentSessions } from "../../features/claude-code-session-state" -import { getAgentToolRestrictions, log } from "../../shared" -import { applySessionPromptParams } from "../../shared/session-prompt-params-helpers" -import type { DelegatedModelConfig } from "../../shared/model-resolution-types" -import type { FallbackEntry } from "../../shared/model-requirements" -import { getAgentDisplayName, stripAgentListSortPrefix } from "../../shared/agent-display-names" +import { setSessionAgent, subagentSessions, syncSubagentSessions } from "../../features/claude-code-session-state" import { promptAsyncAfterSessionIdle } from "../../hooks/shared/prompt-async-gate" +import { getAgentToolRestrictions, log } from "../../shared" +import { getAgentDisplayName, stripAgentListSortPrefix } from "../../shared/agent-display-names" +import { + clearDelegatedChildSessionBootstrap, + registerDelegatedChildSessionBootstrap, +} from "../../shared/delegated-child-session-bootstrap" +import type { FallbackEntry } from "../../shared/model-requirements" +import type { DelegatedModelConfig } from "../../shared/model-resolution-types" +import { applySessionPromptParams } from "../../shared/session-prompt-params-helpers" +import { deleteSessionTools, setSessionTools } from "../../shared/session-tools-store" import { waitForCompletion } from "./completion-poller" import { processMessages } from "./message-processor" import { createOrGetSession } from "./session-creator" +import type { CallOmoAgentArgs } from "./types" type SessionWithPromptAsync = { promptAsync: (opts: { path: { id: string }; body: Record }) => Promise @@ -58,6 +63,14 @@ function buildPromptGenerationParams(model: DelegatedModelConfig | undefined): R } } +function buildSyncPromptTools(agent: string): Record { + return { + ...getAgentToolRestrictions(agent), + task: false, + question: false, + } +} + export async function executeSync( args: CallOmoAgentArgs, toolContext: { @@ -105,6 +118,16 @@ export async function executeSync( log(`[call_omo_agent] Sending prompt to session ${sessionID}`) log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100)) const normalizedSubagentType = stripAgentListSortPrefix(args.subagent_type) + const promptAgent = getAgentDisplayName(normalizedSubagentType) + const promptTools = buildSyncPromptTools(normalizedSubagentType) + setSessionAgent(sessionID, promptAgent) + setSessionTools(sessionID, promptTools) + registerDelegatedChildSessionBootstrap({ + sessionID, + promptText: args.prompt, + fallbackChain, + tools: promptTools, + }) try { if (!hasPromptAsync(ctx.client.session)) { @@ -119,12 +142,8 @@ export async function executeSync( input: { path: { id: sessionID }, body: { - agent: getAgentDisplayName(normalizedSubagentType), - tools: { - ...getAgentToolRestrictions(normalizedSubagentType), - task: false, - question: false, - }, + agent: promptAgent, + tools: promptTools, parts: [{ type: "text", text: args.prompt }], ...(model ? { model: { providerID: model.providerID, modelID: model.modelID } } : {}), ...(model?.variant ? { variant: model.variant } : {}), @@ -160,9 +179,14 @@ export async function executeSync( deps.clearSessionFallbackChain(sessionID) } + if (sessionID) { + clearDelegatedChildSessionBootstrap(sessionID) + } + if (sessionID && createdSessionForExecution) { subagentSessions.delete(sessionID) syncSubagentSessions.delete(sessionID) + deleteSessionTools(sessionID) } } }