From 05f35982d2a2a3a8e740e51ac79efe69bb0f7faf Mon Sep 17 00:00:00 2001 From: Hugo Josefson Date: Fri, 21 Aug 2026 00:47:18 +0200 Subject: [PATCH 1/2] feat(session): add suffix compaction --- packages/client/src/generated/types.ts | 153 +++++++ packages/core/src/config/compaction.ts | 3 + .../core/src/session/compaction-suffix.ts | 131 ++++++ packages/core/src/session/compaction.ts | 264 +++++++++---- packages/core/src/session/message-updater.ts | 3 + packages/core/src/session/projector.ts | 1 + packages/core/src/v1/config/config.ts | 3 + packages/core/src/v1/config/migrate.ts | 1 + packages/core/test/config/config.test.ts | 13 + packages/core/test/session-compaction.test.ts | 374 ++++++++++++++++++ packages/core/test/session-projector.test.ts | 30 ++ packages/opencode/src/session/compaction.ts | 347 ++++++++++++---- packages/opencode/src/session/llm.ts | 1 + packages/opencode/src/session/llm/request.ts | 12 +- packages/opencode/src/session/overflow.ts | 2 +- packages/opencode/src/session/processor.ts | 20 + packages/opencode/src/session/prompt.ts | 165 ++++---- packages/opencode/test/event-manifest.test.ts | 2 +- .../opencode/test/provider/transform.test.ts | 7 +- .../opencode/test/session/compaction.test.ts | 341 +++++++++++++++- packages/schema/src/index.ts | 1 + .../schema/src/session-compaction-event.ts | 3 + packages/schema/src/session-compaction.ts | 38 ++ packages/schema/src/session-event.ts | 18 + packages/schema/src/session-message.ts | 2 + packages/schema/src/v1/session.ts | 2 + packages/schema/test/contract-hygiene.test.ts | 25 ++ packages/schema/test/event-manifest.test.ts | 10 +- packages/sdk/js/src/v2/gen/types.gen.ts | 143 +++++++ packages/sdk/openapi.json | 373 +++++++++++++++++ packages/tui/src/context/data.tsx | 2 + .../session/compaction-diagnostics.test.ts | 36 ++ .../routes/session/compaction-diagnostics.ts | 32 ++ packages/tui/src/routes/session/index.tsx | 23 +- packages/web/src/content/docs/config.mdx | 4 + packages/web/src/content/docs/plugins.mdx | 1 + 36 files changed, 2333 insertions(+), 253 deletions(-) create mode 100644 packages/core/src/session/compaction-suffix.ts create mode 100644 packages/schema/src/session-compaction.ts create mode 100644 packages/tui/src/routes/session/compaction-diagnostics.test.ts create mode 100644 packages/tui/src/routes/session/compaction-diagnostics.ts diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 3b3188c8742a..95a20e6d04ab 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -669,6 +669,21 @@ export type SessionsContextOutput = { readonly reason: "auto" | "manual" readonly summary: string readonly recent: string + readonly diagnostics?: { + readonly requested?: "prepend" | "suffix" + readonly used?: "prepend" | "suffix" + readonly fallback?: + | "context" + | "plugin_prompt" + | "provider_tool" + | "provider_error" + | "tool_choice" + | "tool_call" + | "empty_summary" + | "invalid_summary" + readonly durationMs?: number + readonly tokens?: { readonly input?: number; readonly cached?: number; readonly output?: number } + } readonly id: string readonly metadata?: { readonly [x: string]: JsonValue } readonly time: { readonly created: number } @@ -1075,6 +1090,7 @@ export type SessionsHistoryOutput = { readonly sessionID: string readonly messageID: string readonly reason: "auto" | "manual" + readonly requested?: "prepend" | "suffix" } } | { @@ -1090,6 +1106,59 @@ export type SessionsHistoryOutput = { readonly reason: "auto" | "manual" readonly text: string readonly recent: string + readonly diagnostics?: { + readonly requested?: "prepend" | "suffix" + readonly used?: "prepend" | "suffix" + readonly fallback?: + | "context" + | "plugin_prompt" + | "provider_tool" + | "provider_error" + | "tool_choice" + | "tool_call" + | "empty_summary" + | "invalid_summary" + readonly durationMs?: number + readonly tokens?: { readonly input?: number; readonly cached?: number; readonly output?: number } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly type: "session.next.compaction.failed" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly reason: "auto" | "manual" + readonly failure: + | "context" + | "plugin_prompt" + | "provider_tool" + | "provider_error" + | "tool_choice" + | "tool_call" + | "empty_summary" + | "invalid_summary" + | "interrupted" + readonly diagnostics: { + readonly requested?: "prepend" | "suffix" + readonly used?: "prepend" | "suffix" + readonly fallback?: + | "context" + | "plugin_prompt" + | "provider_tool" + | "provider_error" + | "tool_choice" + | "tool_call" + | "empty_summary" + | "invalid_summary" + readonly durationMs?: number + readonly tokens?: { readonly input?: number; readonly cached?: number; readonly output?: number } + } } } | { @@ -1533,6 +1602,7 @@ export type SessionsEventsOutput = readonly sessionID: string readonly messageID: string readonly reason: "auto" | "manual" + readonly requested?: "prepend" | "suffix" } } | { @@ -1548,6 +1618,59 @@ export type SessionsEventsOutput = readonly reason: "auto" | "manual" readonly text: string readonly recent: string + readonly diagnostics?: { + readonly requested?: "prepend" | "suffix" + readonly used?: "prepend" | "suffix" + readonly fallback?: + | "context" + | "plugin_prompt" + | "provider_tool" + | "provider_error" + | "tool_choice" + | "tool_call" + | "empty_summary" + | "invalid_summary" + readonly durationMs?: number + readonly tokens?: { readonly input?: number; readonly cached?: number; readonly output?: number } + } + } + } + | { + readonly id: string + readonly metadata?: { readonly [x: string]: unknown } + readonly type: "session.next.compaction.failed" + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number } + readonly location?: { readonly directory: string; readonly workspaceID?: string } + readonly data: { + readonly timestamp: number + readonly sessionID: string + readonly messageID: string + readonly reason: "auto" | "manual" + readonly failure: + | "context" + | "plugin_prompt" + | "provider_tool" + | "provider_error" + | "tool_choice" + | "tool_call" + | "empty_summary" + | "invalid_summary" + | "interrupted" + readonly diagnostics: { + readonly requested?: "prepend" | "suffix" + readonly used?: "prepend" | "suffix" + readonly fallback?: + | "context" + | "plugin_prompt" + | "provider_tool" + | "provider_error" + | "tool_choice" + | "tool_call" + | "empty_summary" + | "invalid_summary" + readonly durationMs?: number + readonly tokens?: { readonly input?: number; readonly cached?: number; readonly output?: number } + } } } | { @@ -1747,6 +1870,21 @@ export type SessionsMessageOutput = { readonly reason: "auto" | "manual" readonly summary: string readonly recent: string + readonly diagnostics?: { + readonly requested?: "prepend" | "suffix" + readonly used?: "prepend" | "suffix" + readonly fallback?: + | "context" + | "plugin_prompt" + | "provider_tool" + | "provider_error" + | "tool_choice" + | "tool_call" + | "empty_summary" + | "invalid_summary" + readonly durationMs?: number + readonly tokens?: { readonly input?: number; readonly cached?: number; readonly output?: number } + } readonly id: string readonly metadata?: { readonly [x: string]: JsonValue } readonly time: { readonly created: number } @@ -1919,6 +2057,21 @@ export type MessagesListOutput = { readonly reason: "auto" | "manual" readonly summary: string readonly recent: string + readonly diagnostics?: { + readonly requested?: "prepend" | "suffix" + readonly used?: "prepend" | "suffix" + readonly fallback?: + | "context" + | "plugin_prompt" + | "provider_tool" + | "provider_error" + | "tool_choice" + | "tool_call" + | "empty_summary" + | "invalid_summary" + readonly durationMs?: number + readonly tokens?: { readonly input?: number; readonly cached?: number; readonly output?: number } + } readonly id: string readonly metadata?: { readonly [x: string]: JsonValue } readonly time: { readonly created: number } diff --git a/packages/core/src/config/compaction.ts b/packages/core/src/config/compaction.ts index 3c5960c83556..e70d7b0cc832 100644 --- a/packages/core/src/config/compaction.ts +++ b/packages/core/src/config/compaction.ts @@ -8,6 +8,9 @@ export class Keep extends Schema.Class("ConfigV2.Compaction.Keep")({ }) {} export class Info extends Schema.Class("ConfigV2.Compaction")({ + mode: Schema.Literals(["prepend", "suffix"]).pipe(Schema.optional).annotate({ + description: "Compaction request mode. Suffix is experimental and off by default; prepend remains the default.", + }), auto: Schema.Boolean.pipe(Schema.optional), prune: Schema.Boolean.pipe(Schema.optional), keep: Keep.pipe(Schema.optional), diff --git a/packages/core/src/session/compaction-suffix.ts b/packages/core/src/session/compaction-suffix.ts new file mode 100644 index 000000000000..c55c25f80d0b --- /dev/null +++ b/packages/core/src/session/compaction-suffix.ts @@ -0,0 +1,131 @@ +export * as SessionCompactionSuffix from "./compaction-suffix" + +export const SUMMARY_OUTPUT_TOKENS = 4_096 + +export const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside