From ed73c83335ec21fc5246047fb654155bd1dd4da1 Mon Sep 17 00:00:00 2001 From: ismeth Date: Wed, 6 May 2026 14:27:58 +0200 Subject: [PATCH] feat(session): add summary opt-out config --- packages/opencode/src/config/config.ts | 4 ++ packages/opencode/src/config/session.ts | 16 +++++ packages/opencode/src/session/processor.ts | 13 ++-- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/test/config/config.test.ts | 20 ++++++ packages/opencode/test/session/prompt.test.ts | 66 ++++++++++++++++++- 6 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 packages/opencode/src/config/session.ts diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3a933f81e967..aacdd0397ce2 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -38,6 +38,7 @@ import { ConfigPermission } from "./permission" import { ConfigPlugin } from "./plugin" import { ConfigProvider } from "./provider" import { ConfigServer } from "./server" +import { ConfigSession } from "./session" import { ConfigSkills } from "./skills" import { ConfigVariable } from "./variable" import { Npm } from "@opencode-ai/core/npm" @@ -108,6 +109,9 @@ export const Info = Schema.Struct({ server: Schema.optional(ConfigServer.Server).annotate({ description: "Server configuration for opencode serve and web commands", }), + session: Schema.optional(ConfigSession.Info).annotate({ + description: "Session behavior configuration", + }), command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({ description: "Command configuration, see https://opencode.ai/docs/commands", }), diff --git a/packages/opencode/src/config/session.ts b/packages/opencode/src/config/session.ts new file mode 100644 index 000000000000..6a82b3df45c9 --- /dev/null +++ b/packages/opencode/src/config/session.ts @@ -0,0 +1,16 @@ +import { Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" + +export const Info = Schema.Struct({ + summarize: Schema.optional(Schema.Boolean).annotate({ + description: + "Enable automatic session diff summarization during prompt processing. When false, opencode skips background session summary and per-message diff updates. Defaults to true.", + }), +}) + .annotate({ identifier: "SessionConfig" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) + +export type Info = Schema.Schema.Type + +export * as ConfigSession from "./session" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index f22da92927d2..12e85e4e9be3 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -496,12 +496,13 @@ export const layer: Layer.Layer< } ctx.snapshot = undefined } - yield* summary - .summarize({ - sessionID: ctx.sessionID, - messageID: ctx.assistantMessage.parentID, - }) - .pipe(Effect.ignore, Effect.forkIn(scope)) + if ((yield* config.get()).session?.summarize !== false) + yield* summary + .summarize({ + sessionID: ctx.sessionID, + messageID: ctx.assistantMessage.parentID, + }) + .pipe(Effect.ignore, Effect.forkIn(scope)) if ( !ctx.assistantMessage.summary && isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index fef8c438366c..68c3b41c1cd3 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1542,7 +1542,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the }) } - if (step === 1) + if (step === 1 && (yield* config.get()).session?.summarize !== false) yield* summary.summarize({ sessionID, messageID: lastUser.id }).pipe(Effect.ignore, Effect.forkIn(scope)) if (step > 1 && lastFinished) { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 0a522b085049..4a3150e80b25 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -161,6 +161,26 @@ test("loads JSON config file", async () => { }) }) +test("loads session summarize config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await writeConfig(dir, { + $schema: "https://opencode.ai/config.json", + session: { + summarize: false, + }, + }) + }, + }) + await WithInstance.provide({ + directory: tmp.path, + fn: async () => { + const config = await load() + expect(config.session?.summarize).toBe(false) + }, + }) +}) + test("loads shell config field", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index c5170f346492..858074657f01 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -50,10 +50,15 @@ import { reply, TestLLMServer } from "../lib/llm-server" void Log.init({ print: false }) +let summarizeCalls = 0 + const summary = Layer.succeed( SessionSummary.Service, SessionSummary.Service.of({ - summarize: () => Effect.void, + summarize: () => + Effect.sync(() => { + summarizeCalls++ + }), diff: () => Effect.succeed([]), computeDiff: () => Effect.succeed([]), }), @@ -254,6 +259,13 @@ function providerCfg(url: string) { } } +function providerCfgWithSession(url: string, session: Config.Info["session"]) { + return { + ...providerCfg(url), + session, + } +} + const user = Effect.fn("test.user")(function* (sessionID: SessionID, text: string) { const session = yield* Session.Service const msg = yield* session.updateMessage({ @@ -415,6 +427,58 @@ it.live("prompt emits v2 prompted and synthetic events", () => ), ) +it.live("loop schedules automatic summaries by default", () => + provideTmpdirServer( + Effect.fnUntraced(function* ({ llm }) { + summarizeCalls = 0 + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + yield* prompt.prompt({ + sessionID: chat.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello" }], + }) + yield* llm.text("world") + + yield* prompt.loop({ sessionID: chat.id }) + yield* Effect.sleep("10 millis") + expect(summarizeCalls).toBe(2) + }), + { git: true, config: providerCfg }, + ), +) + +it.live("loop skips session summary when session.summarize is false", () => + provideTmpdirServer( + Effect.fnUntraced(function* ({ llm }) { + summarizeCalls = 0 + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + yield* prompt.prompt({ + sessionID: chat.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello" }], + }) + yield* llm.text("world") + + yield* prompt.loop({ sessionID: chat.id }) + yield* Effect.sleep("10 millis") + expect(summarizeCalls).toBe(0) + }), + { git: true, config: (url) => providerCfgWithSession(url, { summarize: false }) }, + ), +) + it.live("static loop returns assistant text through local provider", () => provideTmpdirServer( Effect.fnUntraced(function* ({ llm }) {