import { expect, test } from "bun:test" import { LLMClient, LanguageModel, Message, ToolDefinition } from "@opencode/ai" import { OpenAI } from "@opencode/ai/providers" import { Agent } from "@opencode/core/agent" import { Bus } from "@opencode/core/bus" import { Database } from "@opencode/core/database/database" import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" import { llmClient } from "@opencode/core/effect/app-node-platform" import { Instructions } from "@opencode/core/instructions/index" import { PluginHooks } from "@opencode/core/plugin/hooks" import { Project } from "@opencode/core/project" import { ProjectTable } from "@opencode/core/project/sql" import { AbsolutePath } from "@opencode/core/schema" import { SessionCompaction } from "@opencode/core/session/compaction" import { SessionEvent } from "@opencode/core/session/event" import { SessionHistory } from "@opencode/core/session/history" import { SessionInbox } from "@opencode/core/session/inbox" import { InstructionState } from "@opencode/core/session/instruction-state" import { SessionMessage } from "@opencode/core/session/message" import { SessionModelRequest } from "@opencode/core/session/model-request" import { SessionProjector } from "@opencode/core/session/projector" import { SessionProviderContext } from "@opencode/core/session/provider-context" import { SessionRunnerModel } from "@opencode/core/session/runner/model" import { SessionSchema } from "@opencode/core/session/schema" import { SessionStore } from "@opencode/core/session/store" import { LayerNode } from "@opencode/util/effect/layer-node" import { DateTime, Deferred, Effect, Fiber, Schema } from "effect" import { testEffect } from "./lib/effect" const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, Bus.node, SessionProjector.node, SessionInbox.node, SessionStore.node, SessionCompaction.node, SessionModelRequest.node, PluginHooks.node, llmClient, ]), [Bus.node.replace(Bus.configured({ persist: true }))], ), ) const setup = Effect.fnUntraced(function* (options: { endpoint?: boolean } = {}) { const endpoint = options.endpoint ?? false const db = (yield* Database.Service).db const bus = yield* Bus.Service const inbox = yield* SessionInbox.Service const store = yield* SessionStore.Service const compaction = yield* SessionCompaction.Service const requests = yield* SessionModelRequest.Service const hooks = yield* PluginHooks.Service const blocked = Deferred.makeUnsafe() const hanging = Promise.withResolvers() const state = { failure: false, flaky: false, hang: false, overflow: false, calls: 0 } const bodies: Record[] = [] const headers: Headers[] = [] const server = yield* Effect.acquireRelease( Effect.sync(() => Bun.serve({ hostname: "127.0.0.1", port: 0, async fetch(request) { state.calls++ headers.push(request.headers) bodies.push( Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)))( await request.text(), ), ) if (state.hang) { Deferred.doneUnsafe(blocked, Effect.void) return hanging.promise } // Persistent failures opt out of retries so the schedule's backoff stays out of these tests. if (state.failure || state.flaky) { const retry = state.flaky state.flaky = false return Response.json( { error: { message: "fixture rate limit", type: "rate_limit_error" } }, { status: 429, headers: retry ? {} : { "x-should-retry": "false" } }, ) } const trigger = JSON.stringify(bodies.at(-1)).includes("compaction_trigger") if (state.overflow && trigger) return Response.json( { error: { message: "Your input exceeds the context window", code: "context_length_exceeded", type: "invalid_request_error", }, }, { status: 400 }, ) const checkpoint = { type: "compaction", id: `cmp_${state.calls}`, encrypted_content: `encrypted_${state.calls}`, } if (new URL(request.url).pathname.endsWith("/compact")) return Response.json({ id: "compact_endpoint", object: "response.compaction", output: [ { type: "message", role: "user", content: [{ type: "input_text", text: "endpoint retained" }] }, checkpoint, ], usage: { input_tokens: 20, output_tokens: 4, total_tokens: 24 }, }) const output = trigger ? [checkpoint] : [] return new Response( `data: ${JSON.stringify({ type: "response.completed", response: { id: `resp_${state.calls}`, status: "completed", output, usage: { input_tokens: 20, output_tokens: 4, total_tokens: 24 }, }, })}\n\n`, { headers: { "content-type": "text/event-stream" } }, ) }, }), ), (server) => Effect.sync(() => { hanging.resolve(new Response("cancelled")) void server.stop(true) }), ) const native = OpenAI.configure({ apiKey: "fixture", baseURL: server.url.toString() }).responses("gpt-5.4-mini") const model = SessionRunnerModel.resolved( endpoint ? LanguageModel.update(native, { route: native.route.with({ compact: { endpoint: native.route.compact.endpoint } }), }) : native, { capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, cost: [], limit: { context: 200_000, output: 32_000 }, compaction: { type: "native" }, }, ) const sessionID = SessionSchema.ID.create() yield* db .insert(ProjectTable) .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) .run() yield* bus.publish(SessionEvent.Created, { sessionID, projectID: Project.ID.global, location: { directory: AbsolutePath.make("/project") }, slug: "native-compaction", version: "test", }) const session = yield* store.get(sessionID) if (!session) return yield* Effect.die("Missing fixture session") const instructions = Instructions.make({ key: Instructions.Key.make("test/native"), codec: Schema.toCodecJson(Schema.String), read: Effect.succeed("Current instructions"), render: { initial: String, changed: (_previous, value) => value, removed: () => "removed" }, }) yield* InstructionState.prepare(db, bus, instructions, sessionID) yield* hooks.register("session", "model.request", (event) => Effect.sync(() => { event.headers["x-test-hook"] = event.kind }), ) yield* hooks.register("session", "http.request", (event) => Effect.sync(() => event.request.headers.set("x-http-hook", event.kind)), ) const prompt = Effect.fnUntraced(function* (text: string, synthetic = false) { const id = SessionMessage.ID.create() yield* inbox.admit({ id, sessionID, item: { type: synthetic ? "synthetic" : "user", payload: { text }, delivery: "steer" }, }) yield* bus.publish(SessionEvent.InboxDelivered, { sessionID, inboxID: id }) }) const load = Effect.gen(function* () { const history = yield* SessionHistory.preview( db, sessionID, instructions, SessionProviderContext.provenance(model) ?? "local", ) return { session, model, initial: history.initial, messages: history.messages, agent: { id: Agent.defaultID, info: Agent.Info.default(Agent.defaultID) }, tools: { definitions: [ ToolDefinition.make({ name: "read", description: "Read a file", inputSchema: { type: "object" } }), ], execute: () => Effect.die("Compaction must never dispatch tools"), }, } }) // Opens the compaction's message as the runner does when it delivers `/compact`. const compact = Effect.gen(function* () { const inputID = SessionMessage.ID.create() yield* bus.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", recent: "", inputID }) return yield* compaction.compact({ reason: "manual", context: yield* load, inputID }) }) const checkpoint = Effect.gen(function* () { const messages = (yield* load).messages const last = messages.findLast((message) => message.type === "compaction" && message.status === "completed") if (last?.type !== "compaction" || last.status !== "completed" || !last.providerContext) return yield* Effect.die("Missing native checkpoint") expect(last.summary).toBe("") expect(last.recent).toBe("") // Provider compaction has no summary, so the request usage is the only visible cost of the operation. expect(last.tokens).toMatchObject({ input: 20, output: 4 }) return last.providerContext }) return { compact, // An automatic compaction that skips the "is it due" check, which a context this small never passes. overflow: Effect.gen(function* () { return yield* compaction.compact({ reason: "overflow", context: yield* load }) }), checkpoint, prompt, load, requests, bodies, headers, state, blocked, sessionID, store, hooks, model, compaction, } }) it.live( "manual trigger persists and continues, retains earlier users repeatedly, and preserves context on failure/cancellation", () => Effect.gen(function* () { const fixture = yield* setup() yield* fixture.prompt("First real user request") yield* fixture.prompt("Synthetic context, not a user request", true) expect(yield* fixture.compact).toEqual({ status: "completed" }) const first = yield* fixture.checkpoint expect(SessionProviderContext.decode(first).map((message) => message.role)).toEqual(["user", "assistant"]) expect(JSON.stringify(first.messages)).not.toContain("Synthetic context") expect(fixture.bodies[0]).toMatchObject({ input: expect.arrayContaining([{ type: "compaction_trigger" }]), tools: [expect.objectContaining({ name: "read" })], }) expect(fixture.bodies[0]).not.toHaveProperty("context_management") expect(fixture.headers[0]?.get("x-test-hook")).toBe("compaction") expect(fixture.headers[0]?.get("x-http-hook")).toBe("compaction") yield* fixture.prompt("Second real user request") const context = yield* fixture.load const prepared = yield* fixture.requests.primary({ session: context.session, agent: context.agent.id, model: context.model, tools: context.tools, ...SessionModelRequest.baseTranscript({ ...context, agent: context.agent.info }), }) const client = yield* LLMClient.Service yield* client.generate(prepared.request, prepared.options) expect(JSON.stringify(fixture.bodies[1])).toContain("encrypted_1") expect(JSON.stringify(fixture.bodies[1])).toContain("Current instructions") expect(JSON.stringify(fixture.bodies[1])).toContain("Second real user request") expect(yield* fixture.compact).toEqual({ status: "completed" }) const second = yield* fixture.checkpoint expect( SessionProviderContext.decode(second) .filter((message) => message.role === "user") .map((message) => message.content), ).toEqual([[Message.text("First real user request")], [Message.text("Second real user request")]]) expect(JSON.stringify(second.messages)).not.toContain("encrypted_1") expect(yield* fixture.store.get(fixture.sessionID)).toMatchObject({ tokens: { input: 40, output: 8 } }) // Nothing new since the checkpoint is not compactable, exactly like a fresh local summary. expect(yield* fixture.compact).toMatchObject({ status: "failed", error: { type: "compaction.unavailable" } }) yield* fixture.prompt("Third real user request") fixture.state.failure = true expect(yield* fixture.compact).toMatchObject({ status: "failed", error: { type: "provider.rate-limit" } }) expect(fixture.state.calls).toBe(4) expect(yield* fixture.checkpoint).toEqual(second) fixture.state.failure = false fixture.state.hang = true const pending = yield* fixture.compact.pipe(Effect.forkScoped) yield* Deferred.await(fixture.blocked) yield* Fiber.interrupt(pending) expect(fixture.state.calls).toBe(5) expect(yield* fixture.checkpoint).toEqual(second) // A transient provider failure retries under the shared session policy and its plugin hook. fixture.state.hang = false const retries: PluginHooks.Domains["session"]["retry"][] = [] yield* fixture.hooks.register("session", "retry", (event) => Effect.sync(() => { retries.push(event) event.decision = { retry: true, delay: 0 } }), ) fixture.state.flaky = true expect(yield* fixture.compact).toEqual({ status: "completed" }) expect(fixture.state.calls).toBe(7) expect(retries).toMatchObject([ { agent: "build", attempt: 2, error: { type: "provider.rate-limit" }, decision: { retry: true, delay: 0 }, }, ]) expect( SessionProviderContext.decode(yield* fixture.checkpoint).filter((message) => message.role === "user"), ).toHaveLength(3) }), 15000, ) it.live("manual and automatic endpoint compaction keep the provider replacement unchanged", () => Effect.gen(function* () { const fixture = yield* setup({ endpoint: true }) yield* fixture.prompt("Original user") expect(yield* fixture.compact).toEqual({ status: "completed" }) yield* fixture.prompt("Later user") expect(yield* fixture.overflow).toEqual({ status: "completed" }) const replacement = SessionProviderContext.decode(yield* fixture.checkpoint) expect(replacement[0]?.content).toEqual([Message.text("endpoint retained")]) expect(JSON.stringify(replacement)).not.toContain("Original user") expect(fixture.state.calls).toBe(2) expect(fixture.headers[0]?.get("x-http-hook")).toBe("compaction") expect(fixture.bodies[0]).toMatchObject({ tools: [expect.objectContaining({ name: "read" })] }) expect(fixture.bodies[0]).not.toHaveProperty("context_management") }), ) it.live("automatic native failures, interruptions, and overflows retain the checkpoint", () => Effect.gen(function* () { const fixture = yield* setup() yield* fixture.prompt("Original durable request") expect(yield* fixture.compact).toEqual({ status: "completed" }) const installed = yield* fixture.checkpoint yield* fixture.prompt("Recent request") fixture.state.failure = true expect(yield* fixture.overflow).toMatchObject({ status: "failed", error: { type: "provider.rate-limit" } }) expect(fixture.state.calls).toBe(2) expect(yield* fixture.checkpoint).toEqual(installed) fixture.state.failure = false fixture.state.hang = true const pending = yield* fixture.overflow.pipe(Effect.forkScoped) yield* Deferred.await(fixture.blocked) yield* Fiber.interrupt(pending) expect((yield* fixture.load).messages.at(-1)).toMatchObject({ type: "compaction", status: "failed", error: { type: "compaction.interrupted" }, }) expect(yield* fixture.checkpoint).toEqual(installed) fixture.state.hang = false // Overflow retries natively, with the provider's window, and gives up once nothing is left to shrink. fixture.state.overflow = true expect(yield* fixture.overflow).toMatchObject({ status: "failed", error: { type: "compaction.failed" } }) expect(fixture.state.calls).toBe(4) expect(JSON.stringify(fixture.bodies[3])).toContain("encrypted_1") expect(yield* fixture.checkpoint).toEqual(installed) }), ) it.live("compaction hooks supply the summary instead of provider compaction", () => Effect.gen(function* () { const fixture = yield* setup() yield* fixture.prompt("Original user") yield* fixture.hooks.register("session", "compaction", (event) => Effect.sync(() => { event.result = { summary: "## Objective\n- hooked summary", providerState: { responseId: "plugin" }, metadata: { plugin: "custom" }, tokens: { input: 10, output: 5, reasoning: 0, cache: { read: 0, write: 0 } }, } }), ) expect(yield* fixture.compact).toEqual({ status: "completed" }) expect(fixture.state.calls).toBe(0) expect((yield* fixture.load).messages.at(-1)).toMatchObject({ type: "compaction", status: "completed", summary: "## Objective\n- hooked summary", recent: "", providerState: { responseId: "plugin" }, metadata: { plugin: "custom" }, tokens: { input: 10, output: 5 }, }) }), ) it.live("rejects request-hook route rewrites before provider compaction", () => Effect.gen(function* () { const fixture = yield* setup() yield* fixture.prompt("Original user") yield* fixture.hooks.register("session", "model.request", (event) => Effect.sync(() => { event.baseURL = "https://another.example/v1" }), ) expect(yield* fixture.compact).toMatchObject({ status: "failed", error: { type: "provider.unsupported-operation" }, }) expect(fixture.state.calls).toBe(0) }), ) test("retained user budget counts attachments and drops whole oldest messages", () => { const model = SessionRunnerModel.resolved(OpenAI.responses("gpt-5.4-mini"), { capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, cost: [], limit: { context: 200_000, output: 32_000 }, }) const user = (text: string) => SessionMessage.User.make({ id: SessionMessage.ID.create(), type: "user", text, time: { created: DateTime.makeUnsafe(0) }, }) const newest = { ...user("x".repeat(63_000 * 4)), files: [{ mime: "image/png", data: "aGVsbG8=", source: { type: "inline" as const } }], } expect(SessionCompaction.recentUserMessages([user("old"), newest], model, 64_000)).toEqual([]) expect( SessionCompaction.recentUserMessages([user("x".repeat(63_000 * 4)), { ...newest, text: "new" }], model, 64_000), ).toHaveLength(1) })