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 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..3a9a0b43 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,33 @@ 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(() => {}); + const autoCompact = process.env['WORKFLOW_AUTO_COMPACT'] + ?.trim() + ?.toLowerCase(); + if (autoCompact !== '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, + sessionID: context.sessionID, + }); + } // 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..b8b816f8 100644 --- a/packages/opencode-plugin/test/e2e/plugin.test.ts +++ b/packages/opencode-plugin/test/e2e/plugin.test.ts @@ -12,11 +12,14 @@ 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. 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(() => { delete process.env.WORKFLOW_AGENTS; + delete process.env.WORKFLOW_AUTO_COMPACT; }); afterEach(() => { if (_savedWorkflowAgents === undefined) { @@ -24,6 +27,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 @@ -668,6 +676,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'; + + 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'; + 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 () => { hooks = await WorkflowsPlugin(mockInput);