diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index ee634cd05de..be28f323a21 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -393,26 +393,27 @@ export const layer = Layer.effect( error: { type: "compaction.unavailable", message: "Nothing to compact yet" }, inputID: input.inputID, }) - const resolved = yield* input.resolveModel(input.session).pipe( - Effect.catch((cause) => - failed({ - sessionID: input.session.id, - reason: "manual", - error: toSessionError(cause), - inputID: input.inputID, - }), - ), + return yield* input.resolveModel(input.session).pipe( + Effect.matchEffect({ + onFailure: (cause) => + failed({ + sessionID: input.session.id, + reason: "manual", + error: toSessionError(cause), + inputID: input.inputID, + }), + onSuccess: (resolved) => + execute({ + session: input.session, + resolved, + prepare: input.prepare, + reason: "manual", + inputID: input.inputID, + started: input.started, + ...content, + }), + }), ) - if ("status" in resolved) return resolved - return yield* execute({ - session: input.session, - resolved, - prepare: input.prepare, - reason: "manual", - inputID: input.inputID, - started: input.started, - ...content, - }) }) return Service.of({ transform: state.transform, diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index c3a3ba6f6ee..8c743d76a34 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -20,6 +20,8 @@ import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { App } from "@opencode-ai/core/app" import { Agent } from "@opencode-ai/core/agent" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -301,6 +303,54 @@ it.effect("manual compaction summarizes short context instead of no-op", () => }), ) +it.effect("manual compaction records model resolution failures without calling the model", () => + Effect.gen(function* () { + requests = [] + const compaction = yield* SessionCompaction.Service + const store = yield* SessionStore.Service + const sessionID = Session.ID.make("ses_manual_resolution_failure") + const session = yield* insertSession(sessionID) + const modelRequests = yield* SessionModelRequest.Service + const inputID = SessionMessage.ID.make("msg_manual_resolution_failure") + + expect( + yield* compaction.compactManual({ + session, + resolveModel: () => + Effect.fail( + new SessionRunnerModel.ModelUnavailableError({ + providerID: Provider.ID.make("test"), + modelID: Model.ID.make("missing"), + }), + ), + prepare: modelRequests.prepare, + messages: [ + { + id: SessionMessage.ID.create(), + type: "user", + text: "Summarize this conversation.", + time: { created: DateTime.makeUnsafe(0) }, + }, + ], + inputID, + }), + ).toEqual({ + status: "failed", + error: { type: "provider.no-route", message: "Model unavailable: test/missing" }, + }) + expect(requests).toHaveLength(0) + expect(yield* store.context(sessionID)).toMatchObject([ + { + id: inputID, + type: "compaction", + status: "failed", + reason: "manual", + error: { type: "provider.no-route", message: "Model unavailable: test/missing" }, + }, + ]) + }), +) + it.effect("forked session compaction reuses the fork root prompt cache key", () => Effect.gen(function* () { requests = []