From 87445a2ef3ee8a3d1f700b4fa3570be907a9a935 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 31 Mar 2026 17:02:56 -0700 Subject: [PATCH] fix: add missing run_in_background to resume snippet and fix background launch detection --- ...ol-execute-after-background-launch.test.ts | 98 +++++++++++++++++++ src/hooks/atlas/tool-execute-after.ts | 1 + src/hooks/task-resume-info/hook.ts | 2 +- src/hooks/task-resume-info/index.test.ts | 15 +++ 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/hooks/atlas/tool-execute-after-background-launch.test.ts diff --git a/src/hooks/atlas/tool-execute-after-background-launch.test.ts b/src/hooks/atlas/tool-execute-after-background-launch.test.ts new file mode 100644 index 000000000..d7c56fdb3 --- /dev/null +++ b/src/hooks/atlas/tool-execute-after-background-launch.test.ts @@ -0,0 +1,98 @@ +/// + +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { existsSync, mkdirSync, rmSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import type { PluginInput } from "@opencode-ai/plugin" +import { createOpencodeClient, type Project } from "@opencode-ai/sdk" + +const isCallerOrchestratorMock = mock(async () => true) +const collectGitDiffStatsMock = mock(() => { + throw new Error("background launches should not trigger verification") +}) + +mock.module("../../shared/session-utils", () => ({ + isCallerOrchestrator: isCallerOrchestratorMock, +})) + +mock.module("../../shared/git-worktree", () => ({ + collectGitDiffStats: collectGitDiffStatsMock, + formatFileChanges: mock(() => "No file changes"), +})) + +const { createToolExecuteAfterHandler } = await import("./tool-execute-after") + +describe("createToolExecuteAfterHandler background launch detection", () => { + let testDirectory = "" + + beforeEach(() => { + testDirectory = join(tmpdir(), `atlas-background-launch-${crypto.randomUUID()}`) + + if (!existsSync(testDirectory)) { + mkdirSync(testDirectory, { recursive: true }) + } + + isCallerOrchestratorMock.mockClear() + collectGitDiffStatsMock.mockClear() + }) + + afterEach(() => { + if (testDirectory && existsSync(testDirectory)) { + rmSync(testDirectory, { recursive: true, force: true }) + } + }) + + function createHandler() { + const project = { + id: "project-1", + worktree: testDirectory, + time: { + created: Date.now(), + }, + } satisfies Project + + const ctx = { + client: createOpencodeClient(), + project, + directory: testDirectory, + worktree: testDirectory, + serverUrl: new URL("https://example.com"), + $: Bun.$, + } satisfies PluginInput + + return createToolExecuteAfterHandler({ + ctx, + pendingFilePaths: new Map(), + pendingTaskRefs: new Map(), + autoCommit: true, + getState: () => ({ promptFailureCount: 0 }), + }) + } + + describe("#given a call_omo_agent background launch result", () => { + describe("#when tool.execute.after handles it", () => { + it("#then it should treat the launch as still running", async () => { + const handler = createHandler() + const output = { + title: "call_omo_agent", + output: "Background agent task launched successfully.", + metadata: { + sessionId: "ses_child123", + }, + } + + await handler( + { + tool: "call_omo_agent", + sessionID: "ses_parent", + }, + output, + ) + + expect(output.output).toBe("Background agent task launched successfully.") + expect(collectGitDiffStatsMock).not.toHaveBeenCalled() + }) + }) + }) +}) diff --git a/src/hooks/atlas/tool-execute-after.ts b/src/hooks/atlas/tool-execute-after.ts index 9a463534c..81d887948 100644 --- a/src/hooks/atlas/tool-execute-after.ts +++ b/src/hooks/atlas/tool-execute-after.ts @@ -118,6 +118,7 @@ export function createToolExecuteAfterHandler(input: { } const isBackgroundLaunch = outputStr.includes("Background task launched") || outputStr.includes("Background task continued") || outputStr.includes("Background delegate launched") + || outputStr.includes("Background agent task launched") if (isBackgroundLaunch) { return } diff --git a/src/hooks/task-resume-info/hook.ts b/src/hooks/task-resume-info/hook.ts index 4eb65dc8f..1774aef6a 100644 --- a/src/hooks/task-resume-info/hook.ts +++ b/src/hooks/task-resume-info/hook.ts @@ -30,7 +30,7 @@ export function createTaskResumeInfoHook() { output.output = outputText.trimEnd() + - `\n\nto continue: task(session_id="${sessionId}", load_skills=[], prompt="...")` + `\n\nto continue: task(session_id="${sessionId}", load_skills=[], run_in_background=false, prompt="...")` } return { diff --git a/src/hooks/task-resume-info/index.test.ts b/src/hooks/task-resume-info/index.test.ts index 200e29af0..2d10ef757 100644 --- a/src/hooks/task-resume-info/index.test.ts +++ b/src/hooks/task-resume-info/index.test.ts @@ -1,3 +1,5 @@ +/// + import { describe, it, expect } from "bun:test" import { createTaskResumeInfoHook } from "./index" @@ -60,6 +62,19 @@ describe("createTaskResumeInfoHook", () => { expect(output.output).toContain("to continue:") expect(output.output).toContain("ses_abc123") }) + + it("#then should include run_in_background in resume info", async () => { + const input = createInput("call_omo_agent") + const output = { + title: "delegate_task", + output: "Task completed.\nSession ID: ses_abc123", + metadata: {}, + } + + await afterHook(input, output) + + expect(output.output).toContain("run_in_background=false") + }) }) })