From ce8196f6c502ab23bd1ebde5b2301753b25e03a7 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:54:49 +0000 Subject: [PATCH] fix(opencode): keep reopened running conversations interruptible (#8698) An execution adopted during session initialization emitted turn_started before the manager could subscribe. Deliver the current turn to a new subscriber so the manager owns the run and routes Stop to OpenCode. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../src/server/agent/agent-manager.test.ts | 30 +++++++++++++++++++ .../agent/providers/opencode/v2/session.ts | 4 +++ 2 files changed, 34 insertions(+) diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 1e8a95eb81..a6d1f9d759 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -58,6 +58,9 @@ import type { import type { PaseoToolCatalog } from "./tools/types.js"; import type { ProviderDefinition } from "./provider-registry.js"; +import { OpenCodeV2AgentClient } from "./providers/opencode/v2/agent.js"; +import { V2Harness } from "./providers/opencode/test-utils/v2-harness.js"; + const DESKTOP_OPEN_AGENT_TAB_LABEL = getOpenAgentTabLabel("desktop-client"); const MOBILE_OPEN_AGENT_TAB_LABEL = getOpenAgentTabLabel("mobile-client"); @@ -670,6 +673,33 @@ async function startAndSteerThroughManager( return { manager, agentId: agent.id, workdir }; } +test("stops an OpenCode execution already active when its conversation is reopened", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-opencode-resume-")); + onTestFinished(() => rmSync(workdir, { recursive: true, force: true })); + const harness = new V2Harness(); + harness.info.location.directory = workdir; + harness.startExecution(); + const client = new OpenCodeV2AgentClient({ logger, runtime: harness.runtime }); + const manager = new AgentManager({ clients: { opencode: client }, logger }); + const agent = await manager.resumeAgentFromPersistence({ + provider: "opencode", + sessionId: "session", + nativeHandle: "session", + metadata: { cwd: workdir }, + }); + try { + expect(await manager.cancelAgentRun(agent.id)).toEqual({ status: "settled" }); + expect(harness.active).toBe(false); + for await (const _event of manager.streamAgent(agent.id, "continue after restart")) { + // Drain the next turn through the same interface used by callers. + } + expect(harness.prompts).toEqual(["continue after restart"]); + } finally { + await manager.closeAgent(agent.id); + await client.shutdown(); + } +}); + test("uses an injected timeline store without making it a production requirement", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-timeline-store-")); const store = new RecordingTimelineStore(); diff --git a/packages/server/src/server/agent/providers/opencode/v2/session.ts b/packages/server/src/server/agent/providers/opencode/v2/session.ts index 7d03e9baaa..9c1197fa21 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/session.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/session.ts @@ -242,6 +242,10 @@ export class OpenCodeV2Session implements AgentSession { } subscribe(callback: (event: AgentStreamEvent) => void) { this.listeners.add(callback); + // Reopening can adopt an execution during initialization, before its caller + // can subscribe. Deliver that live turn so it remains visible and interruptible. + if (this.turns.id) + callback({ type: "turn_started", provider: "opencode", turnId: this.turns.id }); return () => { this.listeners.delete(callback); };