From ca79f93a4c2b4ea82b1a7076e554c6f7eae4e2da Mon Sep 17 00:00:00 2001 From: Anton Fedotov Date: Thu, 18 Jun 2026 20:10:16 +0300 Subject: [PATCH 1/2] fix(core): apply compaction env overrides --- packages/core/src/config.ts | 15 +++++-- packages/core/src/flag/flag.ts | 8 +++- packages/core/test/config-env.test.ts | 60 +++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 packages/core/test/config-env.test.ts diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 26cd3720d9d9..05c012c6252c 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -22,6 +22,7 @@ import { ConfigProvider } from "./config/provider" import { ConfigReference } from "./config/reference" import { ConfigToolOutput } from "./config/tool-output" import { ConfigWatcher } from "./config/watcher" +import { Flag } from "./flag/flag" import { ConfigV1 } from "./v1/config/config" import { ConfigMigrateV1 } from "./v1/config/migrate" @@ -199,7 +200,15 @@ export const layer = Layer.effect( const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie) // Apply general settings first and more specific settings last: // global config, project files, then `.opencode` files. - const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()] + const envCompaction = new ConfigCompaction.Info({ + ...(Flag.OPENCODE_DISABLE_AUTOCOMPACT ? { auto: false } : {}), + ...(Flag.OPENCODE_DISABLE_PRUNE ? { prune: false } : {}), + }) + const envConfig = + envCompaction.auto === undefined && envCompaction.prune === undefined + ? [] + : [new Document({ type: "document", info: new Info({ compaction: envCompaction }) })] + const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat(), ...envConfig] // Rules use the opposite order so a user-global rule can override a // repository rule. Statement order inside each file stays unchanged. yield* policy.load( @@ -210,9 +219,7 @@ export const layer = Layer.effect( ) return Service.of({ - entries: Effect.fn("Config.entries")(function* () { - return configs - }), + entries: Effect.fn("Config.entries")(() => Effect.succeed(configs)), }) }), ) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index a0eb78a13e2a..1bfd13d3154e 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -22,10 +22,8 @@ export const Flag = { OPENCODE_CONFIG_CONTENT: process.env["OPENCODE_CONFIG_CONTENT"], OPENCODE_DISABLE_AUTOUPDATE: truthy("OPENCODE_DISABLE_AUTOUPDATE"), OPENCODE_ALWAYS_NOTIFY_UPDATE: truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE"), - OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"), OPENCODE_DISABLE_TERMINAL_TITLE: truthy("OPENCODE_DISABLE_TERMINAL_TITLE"), OPENCODE_SHOW_TTFD: truthy("OPENCODE_SHOW_TTFD"), - OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"), OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"), OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"), OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], @@ -51,6 +49,12 @@ export const Flag = { // Evaluated at access time (not module load) because tests, the CLI, and // external tooling set these env vars at runtime. + get OPENCODE_DISABLE_PRUNE() { + return truthy("OPENCODE_DISABLE_PRUNE") + }, + get OPENCODE_DISABLE_AUTOCOMPACT() { + return truthy("OPENCODE_DISABLE_AUTOCOMPACT") + }, get OPENCODE_DISABLE_PROJECT_CONFIG() { return truthy("OPENCODE_DISABLE_PROJECT_CONFIG") }, diff --git a/packages/core/test/config-env.test.ts b/packages/core/test/config-env.test.ts new file mode 100644 index 000000000000..a7fa5b123730 --- /dev/null +++ b/packages/core/test/config-env.test.ts @@ -0,0 +1,60 @@ +import path from "path" +import fs from "fs/promises" +import { expect, test } from "bun:test" +import { Effect, Layer } from "effect" +import { Config } from "@opencode-ai/core/config" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { Global } from "@opencode-ai/core/global" +import { Location } from "@opencode-ai/core/location" +import { Policy } from "@opencode-ai/core/policy" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "./fixture/location" +import { tmpdir } from "./fixture/tmpdir" + +function testLayer(directory: string) { + return Config.locationLayer.pipe( + Layer.provide(FSUtil.defaultLayer), + Layer.provide(Global.layerWith({ config: path.join(directory, "global") })), + Layer.provide( + Layer.succeed( + Location.Service, + Location.Service.of( + location( + { directory: AbsolutePath.make(directory) }, + { projectDirectory: AbsolutePath.make(directory) }, + ), + ), + ), + ), + ) +} + +test("applies compaction disable env flags as highest-priority overrides", async () => { + const tmp = await tmpdir() + const previous = { + OPENCODE_DISABLE_AUTOCOMPACT: process.env.OPENCODE_DISABLE_AUTOCOMPACT, + OPENCODE_DISABLE_PRUNE: process.env.OPENCODE_DISABLE_PRUNE, + } + process.env.OPENCODE_DISABLE_AUTOCOMPACT = "true" + process.env.OPENCODE_DISABLE_PRUNE = "true" + + try { + await fs.writeFile(path.join(tmp.path, "opencode.json"), JSON.stringify({ compaction: { auto: true, prune: true } })) + + const documents = await Effect.runPromise( + Config.Service.pipe( + Effect.flatMap((config) => config.entries()), + Effect.map((entries) => entries.filter((entry) => entry.type === "document")), + Effect.provide(testLayer(tmp.path)), + ), + ) + + expect(documents.at(-1)?.info.compaction).toMatchObject({ auto: false, prune: false }) + } finally { + if (previous.OPENCODE_DISABLE_AUTOCOMPACT === undefined) delete process.env.OPENCODE_DISABLE_AUTOCOMPACT + else process.env.OPENCODE_DISABLE_AUTOCOMPACT = previous.OPENCODE_DISABLE_AUTOCOMPACT + if (previous.OPENCODE_DISABLE_PRUNE === undefined) delete process.env.OPENCODE_DISABLE_PRUNE + else process.env.OPENCODE_DISABLE_PRUNE = previous.OPENCODE_DISABLE_PRUNE + await tmp[Symbol.asyncDispose]() + } +}) From 9c37639c1a03447dd8051b02446ededc8d1955eb Mon Sep 17 00:00:00 2001 From: Anton Fedotov Date: Thu, 18 Jun 2026 20:10:31 +0300 Subject: [PATCH 2/2] fix(core): skip overflow compaction when disabled --- packages/core/src/session/compaction.ts | 1 + packages/core/test/session-compaction.test.ts | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 5229949cb958..924652a9bfd0 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -175,6 +175,7 @@ export const buildPrompt = (input: { readonly previousSummary?: string; readonly export const make = (dependencies: Dependencies) => { const config = settings(dependencies.config) const compactAfterOverflow = Effect.fn("SessionCompaction.compactAfterOverflow")(function* (input: Input) { + if (!config.auto) return false const context = input.model.route.defaults.limits?.context if (context === undefined || context <= 0) return false const output = input.request.generation?.maxTokens ?? input.model.route.defaults.limits?.output ?? 0 diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index e91c89c00954..cdfabf3483bf 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -1,5 +1,59 @@ import { expect, test } from "bun:test" +import { LLM, LLMEvent, Message, Model, type LLMRequest } from "@opencode-ai/llm" +import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat" +import { Config } from "@opencode-ai/core/config" +import { ConfigCompaction } from "@opencode-ai/core/config/compaction" +import { EventV2 } from "@opencode-ai/core/event" import { SessionCompaction } from "@opencode-ai/core/session/compaction" +import { SessionMessage } from "@opencode-ai/core/session/message" +import { SessionSchema } from "@opencode-ai/core/session/schema" +import { DateTime, Effect, Stream } from "effect" + +const compactModel = Model.make({ + id: "compact", + provider: "fake", + route: OpenAIChat.route.with({ limits: { context: 20_000, output: 1_000 } }), +}) + +function eventRecorder(published: string[]): EventV2.Interface { + return EventV2.Service.of({ + publish: (definition, data) => + Effect.sync(() => { + published.push(definition.type) + return { id: EventV2.ID.create(), type: definition.type, data } + }), + subscribe: () => Stream.empty, + all: () => Stream.empty, + aggregateEvents: () => Stream.empty, + sync: () => Effect.succeed(Effect.void), + listen: () => Effect.succeed(Effect.void), + beforeCommit: () => Effect.void, + project: () => Effect.void, + replay: () => Effect.void, + replayAll: () => Effect.succeed(undefined), + remove: () => Effect.void, + claim: () => Effect.void, + }) +} + +function overflowInput(request: LLMRequest) { + return { + sessionID: SessionSchema.ID.create(), + model: compactModel, + request, + entries: [ + { + seq: 1, + message: new SessionMessage.User({ + id: SessionMessage.ID.create(), + type: "user", + text: "Earlier question ".repeat(700), + time: { created: DateTime.makeUnsafe(1) }, + }), + }, + ], + } +} test("compaction describes tool media without embedding base64", () => { const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" @@ -16,3 +70,33 @@ test("compaction describes tool media without embedding base64", () => { expect(serialized).toBe("Image read successfully\n[Attached image/png: pixel.png]") expect(serialized).not.toContain(base64) }) + +test("provider overflow recovery does not compact when automatic compaction is disabled", async () => { + const published: string[] = [] + const compaction = SessionCompaction.make({ + events: eventRecorder(published), + llm: { + stream: () => Stream.fromIterable([LLMEvent.textDelta({ id: "summary", text: "## Goal\n- Should not compact" })]), + }, + config: [ + new Config.Document({ + type: "document", + info: new Config.Info({ compaction: new ConfigCompaction.Info({ auto: false }) }), + }), + ], + }) + + const compacted = await Effect.runPromise( + compaction.compactAfterOverflow( + overflowInput( + LLM.request({ + model: compactModel, + messages: [Message.user("Continue")], + }), + ), + ), + ) + + expect(compacted).toBe(false) + expect(published).toEqual([]) +})