From c64a8340a658bc05d195f67cd873ed383ca549b5 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 17 Sep 2026 21:58:40 +0530 Subject: [PATCH 1/3] refactor(core): move native compaction mechanisms into a plugin --- AGENTS.md | 1 + packages/core/src/plugin/compaction.ts | 34 ++++++++ packages/core/src/plugin/internal.ts | 7 ++ packages/core/src/session/compaction.ts | 68 +++++++++++----- .../test/session-native-compaction.test.ts | 77 ++++++++++++++++++- packages/core/test/session-runner.test.ts | 3 + 6 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 packages/core/src/plugin/compaction.ts diff --git a/AGENTS.md b/AGENTS.md index 0a6c4fb23638..257175fc05c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -184,6 +184,7 @@ const table = sqliteTable("session", { - Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics. - Preserve one explicit `llm.stream(request)` call per Physical Attempt and reload projected history before durable continuation. A logical Step may use generic pre-output retries, one full-context retry after continuation rejection, incomplete-stream continuation, or one overflow-compaction rebuild. Generic retries retain the logical step number and do not consume another agent-step allowance. Do not delegate orchestration to an in-memory tool loop. - Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. A write-ahead execution claim marks a process-local busy period for restart recovery: terminal completion, failure, or user interruption releases it, while shutdown interruption and process death preserve it. Startup recovery resumes claimed top-level Sessions with durable per-execution attempt accounting. The claim is a recovery marker, not clustered ownership, fencing, or an exactly-once guarantee. +- Keep native compaction mechanisms out of `SessionCompaction`. Plugins register `native` strategies through the `SessionCompaction` editor that turn a prepared request into a replacement window (the built-in `NativeCompactionPlugin` handles `@opencode/ai` compaction operations); later registrations win. Core owns the provider-mode decision, route provenance, the retry policy, overflow recovery, interruption, usage accounting, and checkpoint persistence. - Keep delivery vocabulary explicit. Prompts steer by default. At safe step boundaries, steered compaction takes priority up to the first steered move control; other steers retain enqueue order. At an idle boundary, steers take priority; otherwise exactly one queued item delivers before the runner reevaluates continuation. Inbox items may be cancelled or changed between queue and steer before delivery. Promoting new user input resets the selected agent's step allowance; a batch of steers resets it once. - One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle. - Keep event replay ownership separate from clustered Session execution ownership. diff --git a/packages/core/src/plugin/compaction.ts b/packages/core/src/plugin/compaction.ts new file mode 100644 index 000000000000..ed9ccd944e88 --- /dev/null +++ b/packages/core/src/plugin/compaction.ts @@ -0,0 +1,34 @@ +export * as NativeCompactionPlugin from "./compaction.js" + +import { LLMClient, Message } from "@opencode/ai" +import { define } from "@opencode/plugin/effect/plugin" +import { Effect } from "effect" +import { SessionCompaction } from "../session/compaction.js" +import type { PluginInternal } from "./internal.js" + +/** + * Native compaction for routes that expose typed compaction operations. A streamed trigger is + * preferred because it travels the session's normal request path and keeps recent real user input + * ahead of the checkpoint; endpoint-only routes use the standalone compaction endpoint as returned. + */ +export const Plugin = define({ + id: "opencode.compaction.native", + effect: Effect.fn("NativeCompactionPlugin")(function* () { + const llm = yield* LLMClient.Service + const compaction = yield* SessionCompaction.Service + yield* compaction.transform((editor) => { + editor.native((input) => { + const request = input.request + if (LLMClient.canCompact(request, { mechanism: "trigger" })) + return Effect.gen(function* () { + const retained = yield* input.retained + const result = yield* llm.compact(request, { ...input.options, mechanism: "trigger" }) + return { replacement: [...retained, Message.assistant(result.checkpoint)], usage: result.usage } + }) + if (LLMClient.canCompact(request)) + return llm.compact(request, { mechanism: "endpoint", http: input.options.http }) + return undefined + }) + }) + }), +} satisfies PluginInternal.InternalPlugin) diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index e450b82b77c5..aa50d2169787 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -1,5 +1,6 @@ export * as PluginInternal from "./internal.js" +import { LLMClient } from "@opencode/ai" import type { Plugin } from "@opencode/plugin/effect/plugin" import { LayerNode } from "@opencode/util/effect/layer-node" import { httpClient } from "@opencode/util/effect/app-node-platform" @@ -12,6 +13,7 @@ import { Provider } from "../provider.js" import { Command } from "../command.js" import { Config } from "../config.js" import { Credential } from "../credential.js" +import { llmClient } from "../effect/app-node-platform.js" import { ConfigAgentPlugin } from "../config/plugin/agent.js" import { ConfigCommandPlugin } from "../config/plugin/command.js" import { ConfigCompactionPlugin } from "../config/plugin/compaction.js" @@ -84,6 +86,7 @@ import { WriteTool } from "../tool/plugin/write.js" import { AgentPlugin } from "./agent.js" import BrowserPlugin from "@opencode/plugin-browser" import { CommandPlugin } from "./command.js" +import { NativeCompactionPlugin } from "./compaction.js" import { IdentityPlugin } from "./identity.js" import { PlanPlugin } from "./plan.js" import { ModelsDevPlugin } from "./models-dev.js" @@ -120,6 +123,7 @@ const services = [ Integration.Service, Job.Service, KV.Service, + LLMClient.Service, Location.Service, ModelsDev.Service, Mcp.Service, @@ -171,6 +175,7 @@ export const requirements = LayerNode.group([ Integration.node, Job.node, KV.node, + llmClient, Location.node, ModelsDev.node, Mcp.node, @@ -212,6 +217,8 @@ const pre = [ SkillPlugin.Plugin, VcsHgPlugin.Plugin, ModelsDevPlugin, + // Provider plugins register after the generic strategy so a later, provider-specific one wins. + NativeCompactionPlugin.Plugin, ...ProviderPlugins, ...WebSearchPlugins, PatchTool.Plugin, diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 57620a14a95b..36a77c3e6253 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -10,7 +10,9 @@ import { LLMRequest, Message, type ContentPart, + type Usage, } from "@opencode/ai" +import type { StreamOptions } from "@opencode/ai/route" import type { SessionCompactionResult } from "@opencode/plugin/effect/session" import { SessionError } from "@opencode/schema/session-error" import { Context, Effect, Layer, Stream } from "effect" @@ -91,8 +93,33 @@ export type Settings = { tokens: number } +export type NativeInput = { + /** The prepared compaction request, after model request hooks and route compatibility checks. */ + readonly request: LLMRequest + readonly options: StreamOptions + /** + * Whole, real user messages from the durable transcript within the retained-token allowance. + * Mechanisms whose response carries only a checkpoint place it after these. + */ + readonly retained: Effect.Effect> +} + +export type NativeResult = { + readonly replacement: ReadonlyArray + readonly usage?: Usage +} + +/** + * Produces the provider's replacement window for a prepared request, or `undefined` when the + * route offers no mechanism this strategy handles. Core owns provenance, retries, overflow + * recovery, and persistence of the returned window. + */ +export type NativeStrategy = (input: NativeInput) => Effect.Effect | undefined + export type Editor = { configure: (settings: Partial) => void + /** Later registrations take precedence over earlier ones. */ + native: (strategy: NativeStrategy) => void } export type AutoInput = { @@ -380,15 +407,18 @@ export const layer = Layer.effect( const llm = yield* LLMClient.Service const db = (yield* Database.Service).db - const state = State.create({ + const state = State.create({ name: "session-compaction", - initial: () => ({ auto: true, buffer: DEFAULT_BUFFER, tokens: DEFAULT_KEEP_TOKENS }), + initial: () => ({ auto: true, buffer: DEFAULT_BUFFER, tokens: DEFAULT_KEEP_TOKENS, native: [] }), editor: (editor) => ({ configure: (settings) => { if (settings.auto !== undefined) editor.auto = settings.auto if (settings.buffer !== undefined) editor.buffer = settings.buffer if (settings.tokens !== undefined) editor.tokens = settings.tokens }, + native: (strategy) => { + editor.native.push(strategy) + }, }), }) const failed = Effect.fnUntraced(function* (input: SessionEvent.Compaction.Failed["data"]) { @@ -504,6 +534,20 @@ export const layer = Layer.effect( return yield* reject( "Provider compaction requires the endpoint in provider/model settings, not a model.request rewrite", ) + // Model resolution admits provider policies only for routes with a compaction operation; a plugin + // still has to claim the mechanism, so a missing strategy is a configuration failure, not a defect. + const retained = original(context.session.id).pipe( + Effect.map((messages) => retainUsers(messages, context.model, state.get().tokens)), + ) + const native = state + .get() + .native.toReversed() + .map((strategy) => strategy({ request, options: prepared.options, retained })) + .find((effect) => effect !== undefined) + if (!native) + return yield* reject( + `No plugin provides native compaction for ${request.model.provider}/${request.model.route.id}`, + ) const transient = SessionRunnerRetry.transient(yield* SessionRunnerRetry.policy(context.session.id), { agent: context.agent.id, model: context.model.ref, @@ -514,25 +558,7 @@ export const layer = Layer.effect( Effect.gen(function* () { // Transient provider failures retry like any other request; only a known automatic overflow permits // local recovery, and nothing is installed until the provider returns a checkpoint. - const result = yield* restore( - Effect.gen(function* () { - if (LLMClient.canCompact(request, { mechanism: "trigger" })) { - const retained = retainUsers(yield* original(context.session.id), context.model, state.get().tokens) - const result = yield* llm - .compact(request, { ...prepared.options, mechanism: "trigger" }) - .pipe(transient) - return { replacement: [...retained, Message.assistant(result.checkpoint)], usage: result.usage } - } - if (LLMClient.canCompact(request)) - return yield* llm - .compact(request, { mechanism: "endpoint", http: prepared.options.http }) - .pipe(transient) - // Model resolution admits provider policies only for routes with a compaction operation. - return yield* Effect.die( - new Error(`${request.model.provider}/${request.model.route.id} has no compaction operation`), - ) - }), - ) + const result = yield* restore(native.pipe(transient)) const usage = result.usage ? SessionUsage.record(result.usage, context.model.cost) : undefined if (usage) yield* bus.publish(SessionEvent.UsageRecorded, { diff --git a/packages/core/test/session-native-compaction.test.ts b/packages/core/test/session-native-compaction.test.ts index 127bfc778521..01782ce0b6af 100644 --- a/packages/core/test/session-native-compaction.test.ts +++ b/packages/core/test/session-native-compaction.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { LLMClient, LanguageModel, Message, ToolDefinition } from "@opencode/ai" +import { LLMClient, LanguageModel, Message, ToolDefinition, Usage } from "@opencode/ai" import { OpenAI } from "@opencode/ai/providers" import { Agent } from "@opencode/core/agent" import { Bus } from "@opencode/core/bus" @@ -7,6 +7,7 @@ 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 { NativeCompactionPlugin } from "@opencode/core/plugin/compaction" import { PluginHooks } from "@opencode/core/plugin/hooks" import { Project } from "@opencode/core/project" import { ProjectTable } from "@opencode/core/project/sql" @@ -24,8 +25,9 @@ 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 { DateTime, Deferred, Effect, Exit, Fiber, Schema, Scope } from "effect" import { testEffect } from "./lib/effect" +import { host } from "./plugin/host" const it = testEffect( AppNodeBuilder.build( @@ -44,7 +46,8 @@ const it = testEffect( ), ) -const setup = Effect.fnUntraced(function* (endpoint = false) { +const setup = Effect.fnUntraced(function* (options: { endpoint?: boolean; plugin?: boolean } = {}) { + const endpoint = options.endpoint ?? false const db = (yield* Database.Service).db const bus = yield* Bus.Service const inbox = yield* SessionInbox.Service @@ -185,6 +188,7 @@ const setup = Effect.fnUntraced(function* (endpoint = false) { render: { initial: String, changed: (_previous, value) => value, removed: () => "removed" }, }) yield* InstructionState.prepare(db, bus, instructions, sessionID) + if (options.plugin !== false) yield* NativeCompactionPlugin.Plugin.effect(host()) yield* hooks.register("session", "model.request", (event) => Effect.sync(() => { event.headers["x-test-hook"] = event.kind @@ -261,6 +265,7 @@ const setup = Effect.fnUntraced(function* (endpoint = false) { store, hooks, model, + compaction, } }) @@ -348,7 +353,7 @@ it.live( it.live("manual and automatic endpoint compaction keep the provider replacement unchanged", () => Effect.gen(function* () { - const fixture = yield* setup(true) + const fixture = yield* setup({ endpoint: true }) yield* fixture.prompt("Original user") expect(yield* fixture.compact).toEqual({ status: "completed" }) expect(yield* fixture.automatic).toEqual({ status: "completed" }) @@ -445,6 +450,70 @@ it.live("rejects request-hook route rewrites before provider compaction", () => }), ) +it.live("provider compaction requires a registered native strategy and follows the plugin scope", () => + Effect.gen(function* () { + const fixture = yield* setup({ plugin: false }) + yield* fixture.prompt("Original user") + expect(yield* fixture.compact).toMatchObject({ + status: "failed", + error: { type: "provider.unsupported-operation", message: expect.stringContaining("openai/openai-responses") }, + }) + expect(fixture.state.calls).toBe(0) + const scope = yield* Scope.make() + yield* NativeCompactionPlugin.Plugin.effect(host()).pipe(Scope.provide(scope)) + expect(yield* fixture.compact).toEqual({ status: "completed" }) + expect(fixture.state.calls).toBe(1) + yield* Scope.close(scope, Exit.void) + yield* fixture.prompt("After unload") + expect(yield* fixture.compact).toMatchObject({ + status: "failed", + error: { type: "provider.unsupported-operation" }, + }) + expect(fixture.state.calls).toBe(1) + }), +) + +it.live("later native strategies take precedence and declining ones fall through", () => + Effect.gen(function* () { + const fixture = yield* setup() + yield* fixture.prompt("Original user") + const seen: string[] = [] + yield* fixture.compaction.transform((editor) => { + editor.native((input) => { + seen.push(input.request.model.route.id) + return undefined + }) + }) + expect(yield* fixture.compact).toEqual({ status: "completed" }) + expect(seen).toEqual(["openai-responses"]) + expect(fixture.state.calls).toBe(1) + yield* fixture.prompt("Second user") + yield* fixture.compaction.transform((editor) => { + editor.native((input) => + Effect.map(input.retained, (retained) => ({ + replacement: [...retained, Message.assistant("plugin window")], + usage: new Usage({ inputTokens: 7, nonCachedInputTokens: 7, outputTokens: 3 }), + })), + ) + }) + expect(yield* fixture.compact).toEqual({ status: "completed" }) + expect(fixture.state.calls).toBe(1) + const installed = (yield* fixture.load).messages.findLast( + (message) => message.type === "compaction" && message.status === "completed", + ) + if (installed?.type !== "compaction" || installed.status !== "completed" || !installed.providerContext) + return yield* Effect.die("Missing plugin checkpoint") + expect(installed.tokens).toMatchObject({ input: 7, output: 3 }) + expect(installed.providerContext.provenance).toEqual(SessionProviderContext.provenance(fixture.model)!) + const replacement = SessionProviderContext.decode(installed.providerContext) + expect(replacement.filter((message) => message.role === "user").map((message) => message.content)).toEqual([ + [Message.text("Original user")], + [Message.text("Second user")], + ]) + expect(replacement.at(-1)?.content).toEqual([Message.text("plugin window")]) + }), +) + 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"] }, diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index fdf160825662..24997fbaecf9 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -56,6 +56,7 @@ import { Plugin } from "@opencode/core/plugin" import { PluginHooks } from "@opencode/core/plugin/hooks" import { OptimizePlugin } from "@opencode/core/plugin/optimize" import { IdentityPlugin } from "@opencode/core/plugin/identity" +import { NativeCompactionPlugin } from "@opencode/core/plugin/compaction" import { QuestionTool } from "@opencode/core/tool/plugin/question" import { Agent } from "@opencode/core/agent" import { Config } from "@opencode/core/config" @@ -470,6 +471,7 @@ const layer = Layer.unwrap( Config.node, Snapshot.node, SessionCompaction.node, + LayerNodePlatform.llmClient, SessionRunnerLLM.node, SessionExecution.node, Session.node, @@ -523,6 +525,7 @@ const setup = Effect.gen(function* () { discard: true, }) yield* IdentityPlugin.Plugin.effect(pluginHost) + yield* NativeCompactionPlugin.Plugin.effect(pluginHost) yield* agents.transform((editor) => editor.update(Agent.ID.make("build"), (agent) => { agent.mode = "primary" From ec70b5bf5a02cd4f8b78321fc8f8a082c1842117 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 17 Sep 2026 22:33:30 +0530 Subject: [PATCH 2/3] refactor(core): trim native compaction seam and tests --- packages/core/src/plugin/compaction.ts | 6 +- packages/core/src/plugin/internal.ts | 1 - packages/core/src/session/compaction.ts | 30 ++++----- .../test/session-native-compaction.test.ts | 63 ++++--------------- 4 files changed, 26 insertions(+), 74 deletions(-) diff --git a/packages/core/src/plugin/compaction.ts b/packages/core/src/plugin/compaction.ts index ed9ccd944e88..dacea91f6be1 100644 --- a/packages/core/src/plugin/compaction.ts +++ b/packages/core/src/plugin/compaction.ts @@ -6,11 +6,7 @@ import { Effect } from "effect" import { SessionCompaction } from "../session/compaction.js" import type { PluginInternal } from "./internal.js" -/** - * Native compaction for routes that expose typed compaction operations. A streamed trigger is - * preferred because it travels the session's normal request path and keeps recent real user input - * ahead of the checkpoint; endpoint-only routes use the standalone compaction endpoint as returned. - */ +/** Native compaction for routes with typed compaction operations: streamed trigger when available, else the endpoint. */ export const Plugin = define({ id: "opencode.compaction.native", effect: Effect.fn("NativeCompactionPlugin")(function* () { diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index aa50d2169787..b89fc1f3e187 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -217,7 +217,6 @@ const pre = [ SkillPlugin.Plugin, VcsHgPlugin.Plugin, ModelsDevPlugin, - // Provider plugins register after the generic strategy so a later, provider-specific one wins. NativeCompactionPlugin.Plugin, ...ProviderPlugins, ...WebSearchPlugins, diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 36a77c3e6253..3d9ae97a1f49 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -94,13 +94,10 @@ export type Settings = { } export type NativeInput = { - /** The prepared compaction request, after model request hooks and route compatibility checks. */ + /** Prepared after model request hooks and route provenance checks. */ readonly request: LLMRequest readonly options: StreamOptions - /** - * Whole, real user messages from the durable transcript within the retained-token allowance. - * Mechanisms whose response carries only a checkpoint place it after these. - */ + /** Whole, real user messages within the retained-token allowance, for checkpoint-only mechanisms. */ readonly retained: Effect.Effect> } @@ -109,16 +106,12 @@ export type NativeResult = { readonly usage?: Usage } -/** - * Produces the provider's replacement window for a prepared request, or `undefined` when the - * route offers no mechanism this strategy handles. Core owns provenance, retries, overflow - * recovery, and persistence of the returned window. - */ +/** Returns the provider's replacement window, or `undefined` when this strategy has no mechanism for the route. */ export type NativeStrategy = (input: NativeInput) => Effect.Effect | undefined export type Editor = { configure: (settings: Partial) => void - /** Later registrations take precedence over earlier ones. */ + /** Later registrations take precedence. */ native: (strategy: NativeStrategy) => void } @@ -534,15 +527,18 @@ export const layer = Layer.effect( return yield* reject( "Provider compaction requires the endpoint in provider/model settings, not a model.request rewrite", ) - // Model resolution admits provider policies only for routes with a compaction operation; a plugin - // still has to claim the mechanism, so a missing strategy is a configuration failure, not a defect. - const retained = original(context.session.id).pipe( - Effect.map((messages) => retainUsers(messages, context.model, state.get().tokens)), - ) const native = state .get() .native.toReversed() - .map((strategy) => strategy({ request, options: prepared.options, retained })) + .map((strategy) => + strategy({ + request, + options: prepared.options, + retained: original(context.session.id).pipe( + Effect.map((messages) => retainUsers(messages, context.model, state.get().tokens)), + ), + }), + ) .find((effect) => effect !== undefined) if (!native) return yield* reject( diff --git a/packages/core/test/session-native-compaction.test.ts b/packages/core/test/session-native-compaction.test.ts index 01782ce0b6af..a7c7ee375383 100644 --- a/packages/core/test/session-native-compaction.test.ts +++ b/packages/core/test/session-native-compaction.test.ts @@ -25,7 +25,7 @@ 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, Exit, Fiber, Schema, Scope } from "effect" +import { DateTime, Deferred, Effect, Fiber, Schema } from "effect" import { testEffect } from "./lib/effect" import { host } from "./plugin/host" @@ -450,7 +450,7 @@ it.live("rejects request-hook route rewrites before provider compaction", () => }), ) -it.live("provider compaction requires a registered native strategy and follows the plugin scope", () => +it.live("provider compaction fails without a native strategy and persists a registered strategy's window", () => Effect.gen(function* () { const fixture = yield* setup({ plugin: false }) yield* fixture.prompt("Original user") @@ -458,59 +458,20 @@ it.live("provider compaction requires a registered native strategy and follows t status: "failed", error: { type: "provider.unsupported-operation", message: expect.stringContaining("openai/openai-responses") }, }) - expect(fixture.state.calls).toBe(0) - const scope = yield* Scope.make() - yield* NativeCompactionPlugin.Plugin.effect(host()).pipe(Scope.provide(scope)) - expect(yield* fixture.compact).toEqual({ status: "completed" }) - expect(fixture.state.calls).toBe(1) - yield* Scope.close(scope, Exit.void) - yield* fixture.prompt("After unload") - expect(yield* fixture.compact).toMatchObject({ - status: "failed", - error: { type: "provider.unsupported-operation" }, - }) - expect(fixture.state.calls).toBe(1) - }), -) - -it.live("later native strategies take precedence and declining ones fall through", () => - Effect.gen(function* () { - const fixture = yield* setup() - yield* fixture.prompt("Original user") - const seen: string[] = [] - yield* fixture.compaction.transform((editor) => { - editor.native((input) => { - seen.push(input.request.model.route.id) - return undefined - }) - }) - expect(yield* fixture.compact).toEqual({ status: "completed" }) - expect(seen).toEqual(["openai-responses"]) - expect(fixture.state.calls).toBe(1) - yield* fixture.prompt("Second user") yield* fixture.compaction.transform((editor) => { - editor.native((input) => - Effect.map(input.retained, (retained) => ({ - replacement: [...retained, Message.assistant("plugin window")], - usage: new Usage({ inputTokens: 7, nonCachedInputTokens: 7, outputTokens: 3 }), - })), + editor.native(() => + Effect.succeed({ + replacement: [Message.assistant("plugin window")], + usage: new Usage({ nonCachedInputTokens: 20, outputTokens: 4 }), + }), ) }) expect(yield* fixture.compact).toEqual({ status: "completed" }) - expect(fixture.state.calls).toBe(1) - const installed = (yield* fixture.load).messages.findLast( - (message) => message.type === "compaction" && message.status === "completed", - ) - if (installed?.type !== "compaction" || installed.status !== "completed" || !installed.providerContext) - return yield* Effect.die("Missing plugin checkpoint") - expect(installed.tokens).toMatchObject({ input: 7, output: 3 }) - expect(installed.providerContext.provenance).toEqual(SessionProviderContext.provenance(fixture.model)!) - const replacement = SessionProviderContext.decode(installed.providerContext) - expect(replacement.filter((message) => message.role === "user").map((message) => message.content)).toEqual([ - [Message.text("Original user")], - [Message.text("Second user")], - ]) - expect(replacement.at(-1)?.content).toEqual([Message.text("plugin window")]) + expect(fixture.state.calls).toBe(0) + const installed = yield* fixture.checkpoint + expect(installed.provenance).toEqual(SessionProviderContext.provenance(fixture.model)!) + expect(SessionProviderContext.decode(installed)).toEqual([Message.assistant("plugin window")]) + expect(yield* fixture.store.get(fixture.sessionID)).toMatchObject({ tokens: { input: 20, output: 4 } }) }), ) From efd76a7365d137c80d682223035736cab8fe5d4c Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 17 Sep 2026 22:48:00 +0530 Subject: [PATCH 3/3] chore(core): drop redundant compaction comments --- packages/core/src/plugin/compaction.ts | 1 - packages/core/src/session/compaction.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/core/src/plugin/compaction.ts b/packages/core/src/plugin/compaction.ts index dacea91f6be1..8b80afb7c480 100644 --- a/packages/core/src/plugin/compaction.ts +++ b/packages/core/src/plugin/compaction.ts @@ -6,7 +6,6 @@ import { Effect } from "effect" import { SessionCompaction } from "../session/compaction.js" import type { PluginInternal } from "./internal.js" -/** Native compaction for routes with typed compaction operations: streamed trigger when available, else the endpoint. */ export const Plugin = define({ id: "opencode.compaction.native", effect: Effect.fn("NativeCompactionPlugin")(function* () { diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 3d9ae97a1f49..a495da6fa5a4 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -94,7 +94,6 @@ export type Settings = { } export type NativeInput = { - /** Prepared after model request hooks and route provenance checks. */ readonly request: LLMRequest readonly options: StreamOptions /** Whole, real user messages within the retained-token allowance, for checkpoint-only mechanisms. */