From 779e2d2f1052163b1cfa5d8c718c20f1c921a132 Mon Sep 17 00:00:00 2001 From: bellman Date: Sun, 24 May 2026 00:32:47 +0900 Subject: [PATCH] fix(task): capture late-arriving sessionId so TUI subagent entry is clickable (#4252) The OpenCode TUI renders background subagent session entries using `props.metadata.sessionId` as the navigation target. When the wait-loop in delegate-task and background-task tools exits before the session is assigned, but the session is created moments later (before metadata publish), the published metadata had `sessionId: undefined`, leaving the TUI entry stuck spinning with no clickable target. Add a single late-fallback `manager.getTask(task.id)?.sessionId` check between the wait-loop exit and metadata publish in both paths. This closes the narrow race window that produced the symptom in #4252. Regression test: `late-session-id-capture.test.ts` mocks the exact race (launch returns no sessionId; getTask returns it after the wait loop). Verified: - npm run build: PASS - npm test: exit code 0 (one unrelated pre-existing failure in sisyphus-task > browserProvider propagation re: agent-browser skill) --- .../background-task/create-background-task.ts | 7 ++ src/tools/delegate-task/background-task.ts | 9 +++ .../late-session-id-capture.test.ts | 70 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/tools/delegate-task/late-session-id-capture.test.ts diff --git a/src/tools/background-task/create-background-task.ts b/src/tools/background-task/create-background-task.ts index dbda87948..9ed3624b9 100644 --- a/src/tools/background-task/create-background-task.ts +++ b/src/tools/background-task/create-background-task.ts @@ -94,6 +94,13 @@ export function createBackgroundTask( await delay(WAIT_FOR_SESSION_INTERVAL_MS) } + // Capture late-arriving sessionId between the wait-loop exit and + // metadata publish so the OpenCode TUI subagent entry has a navigable + // target (issue #4252). + if (!sessionId) { + sessionId = manager.getTask(task.id)?.sessionId + } + const bgMeta = { title: args.description, metadata: { diff --git a/src/tools/delegate-task/background-task.ts b/src/tools/delegate-task/background-task.ts index 767bab764..f15211383 100644 --- a/src/tools/delegate-task/background-task.ts +++ b/src/tools/delegate-task/background-task.ts @@ -160,6 +160,15 @@ export async function executeBackgroundTask( return `Task failed to start (status: ${updatedTask.status}).\n\nTask ID: ${task.id}` } + // Capture late-arriving sessionId from the race window between wait-loop + // exit and metadata publish. Without this, a session that gets created + // moments after the wait loop returns leaves metadata.sessionId undefined, + // which makes the OpenCode TUI render the subagent entry as a perpetual + // spinner with no clickable navigation target (issue #4252). + if (!sessionId && updatedTask?.sessionId) { + sessionId = updatedTask.sessionId + } + if (sessionId) { registerBackgroundSessionContext({ sessionId, diff --git a/src/tools/delegate-task/late-session-id-capture.test.ts b/src/tools/delegate-task/late-session-id-capture.test.ts new file mode 100644 index 000000000..09edacb4f --- /dev/null +++ b/src/tools/delegate-task/late-session-id-capture.test.ts @@ -0,0 +1,70 @@ +const { describe, test, expect } = require("bun:test") + +import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types" +import type { ParentContext } from "./executor-types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" + +const MODEL = { providerID: "anthropic", modelID: "claude-sonnet-4-6" } + +function makeMockCtx(): ToolContextWithMetadata & { captured: any[] } { + const captured: any[] = [] + return { + sessionID: "ses_parent", + messageID: "msg_parent", + agent: "sisyphus", + abort: new AbortController().signal, + callID: "call_001", + metadata: async (input: any) => { captured.push(input) }, + captured, + } +} + +const parentContext: ParentContext = { + sessionID: "ses_parent", + messageID: "msg_parent", + agent: "sisyphus", + model: MODEL, +} + +describe("background-task late sessionId capture (issue #4252)", () => { + test("#given launch returns no sessionId and getTask returns one #when publishing metadata #then sessionId is captured so TUI entry is clickable", async () => { + const { executeBackgroundTask } = require("./background-task") + const ctx = makeMockCtx() + const args: DelegateTaskArgs = { + description: "deferred task", + prompt: "do it", + load_skills: [], + run_in_background: true, + subagent_type: "explore", + } + + // launch returns a pending task with no sessionId; getTask returns the + // same task with sessionId populated *after* the wait loop exits. This + // simulates the race where the subagent session is created moments after + // we stop polling, and is the exact condition that left the OpenCode TUI + // session entry stuck spinning with no click target on v4.2.3. + await executeBackgroundTask(args, ctx, unsafeTestValue({ + manager: { + launch: async () => ({ + id: "bg_late", + description: "deferred task", + agent: "explore", + status: "pending", + }), + getTask: () => ({ + id: "bg_late", + description: "deferred task", + agent: "explore", + status: "running", + sessionId: "ses_late", + }), + }, + }), parentContext, "explore", MODEL, undefined) + + const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) + expect(meta).toBeDefined() + expect(meta.metadata.sessionId).toBe("ses_late") + expect(meta.metadata.taskId).toBe("ses_late") + expect(meta.metadata.backgroundTaskId).toBe("bg_late") + }) +})