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); };