diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index e6e38f560..e478a2ea2 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -315,6 +315,65 @@ const hasOpenCode = isBinaryInstalled("opencode"); }); describe("OpenCode adapter context-window normalization", () => { + test("close reconciliation aborts then archives upstream session", async () => { + const abort = vi.fn().mockResolvedValue({ data: true, error: undefined }); + const update = vi.fn().mockResolvedValue({ + data: { id: "session-1", time: { archived: Date.now() } }, + error: undefined, + }); + + await __openCodeInternals.reconcileOpenCodeSessionClose({ + client: { + session: { + abort, + update, + }, + } as never, + sessionId: "session-1", + directory: "/tmp/project", + logger: createTestLogger(), + }); + + expect(abort).toHaveBeenCalledWith({ + sessionID: "session-1", + directory: "/tmp/project", + }); + expect(update).toHaveBeenCalledTimes(1); + expect(update).toHaveBeenCalledWith({ + sessionID: "session-1", + directory: "/tmp/project", + time: { + archived: expect.any(Number), + }, + }); + }); + + test("close reconciliation still archives when abort returns an error", async () => { + const abort = vi.fn().mockResolvedValue({ + data: undefined, + error: { data: {}, errors: [], success: false }, + }); + const update = vi.fn().mockResolvedValue({ + data: { id: "session-1", time: { archived: Date.now() } }, + error: undefined, + }); + + await __openCodeInternals.reconcileOpenCodeSessionClose({ + client: { + session: { + abort, + update, + }, + } as never, + sessionId: "session-1", + directory: "/tmp/project", + logger: createTestLogger(), + }); + + expect(abort).toHaveBeenCalledTimes(1); + expect(update).toHaveBeenCalledTimes(1); + }); + test("builds OpenCode file parts for image prompt blocks", () => { expect( __openCodeInternals.buildOpenCodePromptParts([ diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index cc3ca96e8..3e036bd8c 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -231,6 +231,73 @@ function normalizeTurnFailureError(error: unknown): string { return normalized.length > 0 ? normalized : "Unknown error"; } +function isOpenCodeNotFoundError(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "name" in error && + (error as { name?: unknown }).name === "NotFoundError" + ); +} + +async function reconcileOpenCodeSessionClose(params: { + client: Pick; + sessionId: string; + directory: string; + logger: Logger; +}): Promise { + const { client, sessionId, directory, logger } = params; + + try { + const response = await client.session.abort({ + sessionID: sessionId, + directory, + }); + if (response.error && !isOpenCodeNotFoundError(response.error)) { + logger.warn( + { + sessionId, + error: normalizeTurnFailureError(response.error), + }, + "Failed to abort OpenCode session during close", + ); + } + } catch (error) { + logger.warn( + { + sessionId, + error: normalizeTurnFailureError(error), + }, + "Failed to abort OpenCode session during close", + ); + } + + try { + const response = await client.session.update({ + sessionID: sessionId, + directory, + time: { archived: Date.now() }, + }); + if (response.error && !isOpenCodeNotFoundError(response.error)) { + logger.warn( + { + sessionId, + error: normalizeTurnFailureError(response.error), + }, + "Failed to archive OpenCode session during close", + ); + } + } catch (error) { + logger.warn( + { + sessionId, + error: normalizeTurnFailureError(error), + }, + "Failed to archive OpenCode session during close", + ); + } +} + function isFatalOpenCodeRetryMessage(message: string | null | undefined): boolean { const normalized = typeof message === "string" ? message.trim().toLowerCase() : ""; if (!normalized) { @@ -570,6 +637,7 @@ export const __openCodeInternals = { hasNormalizedOpenCodeUsage, mergeOpenCodeStepFinishUsage, parseOpenCodeModelLookupKey, + reconcileOpenCodeSessionClose, resolveOpenCodeModelLookupKeyFromAssistantMessage, resolveOpenCodeSelectedModelContextWindow, }; @@ -1932,6 +2000,12 @@ class OpenCodeAgentSession implements AgentSession { async close(): Promise { this.abortController?.abort(); + await reconcileOpenCodeSessionClose({ + client: this.client, + sessionId: this.sessionId, + directory: this.config.cwd, + logger: this.logger, + }); this.subscribers.clear(); this.activeForegroundTurnId = null; }