From f1fbde2a2002b560e4a711062cef73aa92d58bde Mon Sep 17 00:00:00 2001 From: udondan Date: Thu, 16 Apr 2026 16:41:30 +0200 Subject: [PATCH 1/8] feat(opencode-plugin): make auto-compaction configurable via WORKFLOW_AUTO_COMPACT Add WORKFLOW_AUTO_COMPACT env var to control whether session compaction is triggered on phase transition. Defaults to enabled; set to 'false' to disable. Adds a corresponding e2e test case. --- .../src/tool-handlers/proceed-to-phase.ts | 31 ++++++++----- .../opencode-plugin/test/e2e/plugin.test.ts | 46 +++++++++++++++++++ 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts index e1beb1c1..9f375f07 100644 --- a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts +++ b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts @@ -80,22 +80,29 @@ export function createProceedToPhaseTool( }); // Trigger compaction to clear prior-phase context from the LLM window. + // Skipped when WORKFLOW_AUTO_COMPACT=false; default is enabled. // Fire-and-forget: a failed compaction must never block the phase transition. // The summarize API requires providerID + modelID; we use the last-known // model from the chat.message hook (cached in the plugin closure). - const model = getModel(); - client.session - .summarize({ - path: { id: context.sessionID }, - ...(model ? { body: model } : {}), - }) - .catch(() => {}); + if (process.env['WORKFLOW_AUTO_COMPACT'] !== 'false') { + const model = getModel(); + client.session + .summarize({ + path: { id: context.sessionID }, + ...(model ? { body: model } : {}), + }) + .catch(() => {}); - logger.info('Triggered compaction after phase transition', { - phase: data.phase, - sessionID: context.sessionID, - hasModel: !!model, - }); + logger.info('Triggered compaction after phase transition', { + phase: data.phase, + sessionID: context.sessionID, + hasModel: !!model, + }); + } else { + logger.debug('Skipped compaction: WORKFLOW_AUTO_COMPACT=false', { + phase: data.phase, + }); + } // Build response with instructions (strip whats_next references) const lines: string[] = []; diff --git a/packages/opencode-plugin/test/e2e/plugin.test.ts b/packages/opencode-plugin/test/e2e/plugin.test.ts index 610fc99b..af158bd3 100644 --- a/packages/opencode-plugin/test/e2e/plugin.test.ts +++ b/packages/opencode-plugin/test/e2e/plugin.test.ts @@ -668,6 +668,52 @@ describe('OpenCode Workflows Plugin E2E', () => { expect(summarizeMock).not.toHaveBeenCalled(); }); + it('does not trigger compaction when WORKFLOW_AUTO_COMPACT=false', async () => { + process.env['WORKFLOW_AUTO_COMPACT'] = 'false'; + try { + await setupWorkflowState(testDir, { + workflowName: 'epcc', + currentPhase: 'explore', + }); + + hooks = await WorkflowsPlugin(mockInput); + + await hooks['chat.message']!( + { + sessionID: 'test-session-789', + model: { + providerID: 'github-copilot', + modelID: 'claude-sonnet-4.6', + }, + }, + { + message: { + id: 'msg-1', + sessionID: 'test-session-789', + role: 'user', + }, + parts: [], + } + ); + + const sessionID = 'test-session-789'; + await hooks.tool!.proceed_to_phase.execute( + { target_phase: 'plan', reason: 'exploration complete' }, + { sessionID } as never + ); + + // session.summarize should NOT have been called + const summarizeMock = ( + mockInput.client as { + session: { summarize: ReturnType }; + } + ).session.summarize; + expect(summarizeMock).not.toHaveBeenCalled(); + } finally { + delete process.env['WORKFLOW_AUTO_COMPACT']; + } + }); + it('fails when no workflow is active', async () => { hooks = await WorkflowsPlugin(mockInput); From 2e73579d2554422d569053791f710480f887077b Mon Sep 17 00:00:00 2001 From: udondan Date: Thu, 16 Apr 2026 16:48:09 +0200 Subject: [PATCH 2/8] fix(opencode-plugin): address review comments on WORKFLOW_AUTO_COMPACT - Normalize env var with trim/toLowerCase before comparison - Restore original env var value in test finally block instead of unconditional delete --- .../opencode-plugin/src/tool-handlers/proceed-to-phase.ts | 4 +++- packages/opencode-plugin/test/e2e/plugin.test.ts | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts index 9f375f07..d4281c2b 100644 --- a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts +++ b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts @@ -84,7 +84,9 @@ export function createProceedToPhaseTool( // Fire-and-forget: a failed compaction must never block the phase transition. // The summarize API requires providerID + modelID; we use the last-known // model from the chat.message hook (cached in the plugin closure). - if (process.env['WORKFLOW_AUTO_COMPACT'] !== 'false') { + const autoCompact = + process.env['WORKFLOW_AUTO_COMPACT']?.trim().toLowerCase(); + if (autoCompact !== 'false') { const model = getModel(); client.session .summarize({ diff --git a/packages/opencode-plugin/test/e2e/plugin.test.ts b/packages/opencode-plugin/test/e2e/plugin.test.ts index af158bd3..cb5e2e39 100644 --- a/packages/opencode-plugin/test/e2e/plugin.test.ts +++ b/packages/opencode-plugin/test/e2e/plugin.test.ts @@ -669,6 +669,7 @@ describe('OpenCode Workflows Plugin E2E', () => { }); it('does not trigger compaction when WORKFLOW_AUTO_COMPACT=false', async () => { + const originalValue = process.env['WORKFLOW_AUTO_COMPACT']; process.env['WORKFLOW_AUTO_COMPACT'] = 'false'; try { await setupWorkflowState(testDir, { @@ -710,7 +711,11 @@ describe('OpenCode Workflows Plugin E2E', () => { ).session.summarize; expect(summarizeMock).not.toHaveBeenCalled(); } finally { - delete process.env['WORKFLOW_AUTO_COMPACT']; + if (originalValue === undefined) { + delete process.env['WORKFLOW_AUTO_COMPACT']; + } else { + process.env['WORKFLOW_AUTO_COMPACT'] = originalValue; + } } }); From f3a11a492476fc79ca473d357c5b6d673012c224 Mon Sep 17 00:00:00 2001 From: udondan Date: Thu, 16 Apr 2026 16:50:20 +0200 Subject: [PATCH 3/8] style: fix prettier formatting in proceed-to-phase.ts --- .../opencode-plugin/src/tool-handlers/proceed-to-phase.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts index d4281c2b..c4718db5 100644 --- a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts +++ b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts @@ -84,8 +84,9 @@ export function createProceedToPhaseTool( // Fire-and-forget: a failed compaction must never block the phase transition. // The summarize API requires providerID + modelID; we use the last-known // model from the chat.message hook (cached in the plugin closure). - const autoCompact = - process.env['WORKFLOW_AUTO_COMPACT']?.trim().toLowerCase(); + const autoCompact = process.env['WORKFLOW_AUTO_COMPACT'] + ?.trim() + .toLowerCase(); if (autoCompact !== 'false') { const model = getModel(); client.session From e1f21d22192a0ab41667c470c3292bd6b8987334 Mon Sep 17 00:00:00 2001 From: udondan Date: Thu, 16 Apr 2026 16:55:38 +0200 Subject: [PATCH 4/8] fix(opencode-plugin): address second round of review comments - Add sessionID to skip-compaction debug log for easier troubleshooting - Ensure WORKFLOW_AUTO_COMPACT is cleared in global beforeEach alongside WORKFLOW_AGENTS so compaction tests are deterministic in any environment - Assert proceed_to_phase result contains 'plan' in opt-out test to confirm the transition succeeded (not just that summarize was skipped due to an error) --- .../src/tool-handlers/proceed-to-phase.ts | 1 + packages/opencode-plugin/test/e2e/plugin.test.ts | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts index c4718db5..33c72c32 100644 --- a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts +++ b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts @@ -104,6 +104,7 @@ export function createProceedToPhaseTool( } else { logger.debug('Skipped compaction: WORKFLOW_AUTO_COMPACT=false', { phase: data.phase, + sessionID: context.sessionID, }); } diff --git a/packages/opencode-plugin/test/e2e/plugin.test.ts b/packages/opencode-plugin/test/e2e/plugin.test.ts index cb5e2e39..2234a0a1 100644 --- a/packages/opencode-plugin/test/e2e/plugin.test.ts +++ b/packages/opencode-plugin/test/e2e/plugin.test.ts @@ -12,11 +12,13 @@ import { tmpdir } from 'node:os'; import { WorkflowsPlugin } from '../../src/plugin.js'; import type { PluginInput, Hooks, Part, UserMessage } from '../../src/types.js'; -// Ensure WORKFLOW_AGENTS is unset for the baseline test suite. -// Tests that need it set/restored manage it themselves in try/finally blocks. +// Ensure WORKFLOW_AGENTS and WORKFLOW_AUTO_COMPACT are unset for the baseline +// test suite. Tests that need them set manage it themselves in try/finally blocks. const _savedWorkflowAgents = process.env.WORKFLOW_AGENTS; +const _savedWorkflowAutoCompact = process.env.WORKFLOW_AUTO_COMPACT; beforeEach(() => { delete process.env.WORKFLOW_AGENTS; + delete process.env.WORKFLOW_AUTO_COMPACT; }); afterEach(() => { if (_savedWorkflowAgents === undefined) { @@ -24,6 +26,11 @@ afterEach(() => { } else { process.env.WORKFLOW_AGENTS = _savedWorkflowAgents; } + if (_savedWorkflowAutoCompact === undefined) { + delete process.env.WORKFLOW_AUTO_COMPACT; + } else { + process.env.WORKFLOW_AUTO_COMPACT = _savedWorkflowAutoCompact; + } }); // Test utilities @@ -698,11 +705,14 @@ describe('OpenCode Workflows Plugin E2E', () => { ); const sessionID = 'test-session-789'; - await hooks.tool!.proceed_to_phase.execute( + const proceedResult = await hooks.tool!.proceed_to_phase.execute( { target_phase: 'plan', reason: 'exploration complete' }, { sessionID } as never ); + // Verify the transition itself succeeded before checking compaction was skipped + expect(JSON.stringify(proceedResult)).toContain('plan'); + // session.summarize should NOT have been called const summarizeMock = ( mockInput.client as { From 72ef7cbe7e9eaaaabd5bad8e8c108ff40f9b78cc Mon Sep 17 00:00:00 2001 From: udondan Date: Thu, 16 Apr 2026 17:08:24 +0200 Subject: [PATCH 5/8] fix(opencode-plugin): remove redundant try/finally in WORKFLOW_AUTO_COMPACT test --- .../opencode-plugin/test/e2e/plugin.test.ts | 82 +++++++++---------- 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/packages/opencode-plugin/test/e2e/plugin.test.ts b/packages/opencode-plugin/test/e2e/plugin.test.ts index 2234a0a1..328d1ae4 100644 --- a/packages/opencode-plugin/test/e2e/plugin.test.ts +++ b/packages/opencode-plugin/test/e2e/plugin.test.ts @@ -676,57 +676,49 @@ describe('OpenCode Workflows Plugin E2E', () => { }); it('does not trigger compaction when WORKFLOW_AUTO_COMPACT=false', async () => { - const originalValue = process.env['WORKFLOW_AUTO_COMPACT']; process.env['WORKFLOW_AUTO_COMPACT'] = 'false'; - try { - await setupWorkflowState(testDir, { - workflowName: 'epcc', - currentPhase: 'explore', - }); - hooks = await WorkflowsPlugin(mockInput); + await setupWorkflowState(testDir, { + workflowName: 'epcc', + currentPhase: 'explore', + }); + + hooks = await WorkflowsPlugin(mockInput); - await hooks['chat.message']!( - { + await hooks['chat.message']!( + { + sessionID: 'test-session-789', + model: { + providerID: 'github-copilot', + modelID: 'claude-sonnet-4.6', + }, + }, + { + message: { + id: 'msg-1', sessionID: 'test-session-789', - model: { - providerID: 'github-copilot', - modelID: 'claude-sonnet-4.6', - }, + role: 'user', }, - { - message: { - id: 'msg-1', - sessionID: 'test-session-789', - role: 'user', - }, - parts: [], - } - ); - - const sessionID = 'test-session-789'; - const proceedResult = await hooks.tool!.proceed_to_phase.execute( - { target_phase: 'plan', reason: 'exploration complete' }, - { sessionID } as never - ); - - // Verify the transition itself succeeded before checking compaction was skipped - expect(JSON.stringify(proceedResult)).toContain('plan'); - - // session.summarize should NOT have been called - const summarizeMock = ( - mockInput.client as { - session: { summarize: ReturnType }; - } - ).session.summarize; - expect(summarizeMock).not.toHaveBeenCalled(); - } finally { - if (originalValue === undefined) { - delete process.env['WORKFLOW_AUTO_COMPACT']; - } else { - process.env['WORKFLOW_AUTO_COMPACT'] = originalValue; + parts: [], } - } + ); + + const sessionID = 'test-session-789'; + const proceedResult = await hooks.tool!.proceed_to_phase.execute( + { target_phase: 'plan', reason: 'exploration complete' }, + { sessionID } as never + ); + + // Verify the transition itself succeeded before checking compaction was skipped + expect(JSON.stringify(proceedResult)).toContain('plan'); + + // session.summarize should NOT have been called + const summarizeMock = ( + mockInput.client as { + session: { summarize: ReturnType }; + } + ).session.summarize; + expect(summarizeMock).not.toHaveBeenCalled(); }); it('fails when no workflow is active', async () => { From f3ca1348d840c57922ebc5f0c833821ac063b735 Mon Sep 17 00:00:00 2001 From: udondan Date: Thu, 16 Apr 2026 17:10:02 +0200 Subject: [PATCH 6/8] docs(opencode-plugin): document WORKFLOW_AUTO_COMPACT env var in README --- packages/opencode-plugin/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/opencode-plugin/README.md b/packages/opencode-plugin/README.md index e369687d..05ff1d62 100644 --- a/packages/opencode-plugin/README.md +++ b/packages/opencode-plugin/README.md @@ -90,6 +90,18 @@ When the env var is set, workflow hooks are skipped and tools throw a clear erro **When unset**, workflows are active for all agents (default behavior). +### Auto-Compaction + +When transitioning to a new phase via `proceed_to_phase`, the plugin automatically triggers a session compaction (summarize) to clear prior-phase context from the LLM window. This is enabled by default. + +Set `WORKFLOW_AUTO_COMPACT=false` to disable this behavior: + +```bash +WORKFLOW_AUTO_COMPACT=false npx opencode +``` + +**When unset or any value other than `false`**, compaction runs on every successful phase transition (default behavior). + ### Per-Agent Behavior - **Agent in filter**: Workflow instructions are injected on every message, tools work normally From f4a3ee42d2f53a814df4dc62d1a1ab8ea6a4d8de Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 16 Apr 2026 17:15:44 +0200 Subject: [PATCH 7/8] Update packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts index 33c72c32..3a9a0b43 100644 --- a/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts +++ b/packages/opencode-plugin/src/tool-handlers/proceed-to-phase.ts @@ -86,7 +86,7 @@ export function createProceedToPhaseTool( // model from the chat.message hook (cached in the plugin closure). const autoCompact = process.env['WORKFLOW_AUTO_COMPACT'] ?.trim() - .toLowerCase(); + ?.toLowerCase(); if (autoCompact !== 'false') { const model = getModel(); client.session From 6f70aa5e2e1a31582119fac405b7c0f5462c803a Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 16 Apr 2026 17:16:16 +0200 Subject: [PATCH 8/8] Update packages/opencode-plugin/test/e2e/plugin.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/opencode-plugin/test/e2e/plugin.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode-plugin/test/e2e/plugin.test.ts b/packages/opencode-plugin/test/e2e/plugin.test.ts index 328d1ae4..b8b816f8 100644 --- a/packages/opencode-plugin/test/e2e/plugin.test.ts +++ b/packages/opencode-plugin/test/e2e/plugin.test.ts @@ -13,7 +13,8 @@ import { WorkflowsPlugin } from '../../src/plugin.js'; import type { PluginInput, Hooks, Part, UserMessage } from '../../src/types.js'; // Ensure WORKFLOW_AGENTS and WORKFLOW_AUTO_COMPACT are unset for the baseline -// test suite. Tests that need them set manage it themselves in try/finally blocks. +// test suite. Per-test overrides can set them as needed, and shared cleanup below +// restores the original process environment after each test. const _savedWorkflowAgents = process.env.WORKFLOW_AGENTS; const _savedWorkflowAutoCompact = process.env.WORKFLOW_AUTO_COMPACT; beforeEach(() => {