From 0189795646632943489dbe527d005c2f685376eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Sun, 19 Apr 2026 12:44:18 +0200 Subject: [PATCH 1/2] WIP: add directive markers to instruction-generator for better agent instruction following - Add 'YOU MUST FOLLOW THESE INSTRUCTIONS' header - Add IMPORTANT, ACTION REQUIRED, CRITICAL markers to key instructions - Makes instructions stand out from contextual text in synthetic injection --- packages/core/src/instruction-generator.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/src/instruction-generator.ts b/packages/core/src/instruction-generator.ts index 7bd4b799..c525b546 100644 --- a/packages/core/src/instruction-generator.ts +++ b/packages/core/src/instruction-generator.ts @@ -107,10 +107,15 @@ export class InstructionGenerator implements IInstructionGenerator { const phaseName = capitalizePhase(phase); + // IMPORTANT: Directive markers to make instructions stand out from context let workflowSection = `--- -**Read \`${conversationContext.planFilePath}\`** for context. -- Focus on "${phaseName}" tasks, log decisions in "Key Decisions" -- Do NOT use other task/todo tools - use only the plan file for task tracking`; +### YOU MUST FOLLOW THESE INSTRUCTIONS: + +**IMPORTANT: Read \`${conversationContext.planFilePath}\`** for context. + +**ACTION REQUIRED: Focus on "${phaseName}" tasks** and log decisions in "Key Decisions" + +**CRITICAL: Do NOT use other task/todo tools** - use only the plan file for task tracking`; // Add file restriction guidance if patterns are restricted if ( From fd463b2c20fd401a060fdcbca83b4d85e2d369d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Tue, 21 Apr 2026 13:51:01 +0200 Subject: [PATCH 2/2] fix(opencode-plugin): resume phase context after session compaction Two-pronged fix for post-compaction context loss: 1. Enhance experimental.session.compacting hook to inject full phase instructions (via WhatsNextHandler) into the compaction summary so the AI retains complete workflow context even when chat.message does not fire for the synthetic auto-continue message. 2. Fix post-compaction prompt injection by switching from the blocking client.session.prompt() to client.session.promptAsync(), which fires and forgets (HTTP 204) without blocking the Effect runtime. Add a 500ms delay to let the runner state machine settle before the phase-aware 'Continue with the current phase.' message is sent. Also adds typed BusEvent discriminated union to types.ts to replace the previous unknown event type in the event hook signature. --- .beads/issues.jsonl | 6 + .beads/last-touched | 2 +- ...velopment-plan-continue-after-compact-2.md | 75 ++++++++++++ packages/opencode-plugin/src/plugin.ts | 109 +++++++++++++++++- packages/opencode-plugin/src/types.ts | 17 ++- 5 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 .vibe/development-plan-continue-after-compact-2.md diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5403ee68..b890925e 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -335,6 +335,12 @@ {"id":"responsible-vibe-31.4.1","title":"Code cleanup: check for debug/temp artifacts","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-03T09:54:03.20756+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-03T09:54:12.432805+02:00","closed_at":"2026-04-03T09:54:12.432805+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-31.4.1","depends_on_id":"responsible-vibe-31.4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-31.4.2","title":"Final test run","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-03T09:54:03.404412+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-03T10:00:00.880469+02:00","closed_at":"2026-04-03T10:00:00.880469+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-31.4.2","depends_on_id":"responsible-vibe-31.4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-31.4.3","title":"Commit changes","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-03T09:54:03.597606+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-03T10:00:00.995887+02:00","closed_at":"2026-04-03T10:00:00.995887+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-31.4.3","depends_on_id":"responsible-vibe-31.4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32","title":"responsible-vibe: minor (development-plan-continue-after-compact-2.md)","description":"Responsible vibe engineering session using minor workflow for responsible-vibe","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T11:12:19.686403+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T11:12:19.686403+02:00"} +{"id":"responsible-vibe-32.1","title":"Explore","description":"Understand the problem, analyze existing patterns, and design your approach. Consider the scope and impact of the change. **STEP 1: Analyze Requirements** - If exists: Use it to understand the required changes - Otherwise: Document requirements in your task management system **STEP 2: Review Design Approach** - If exists: Respect the design approach documented in - Otherwise: Design your approach based on the problem analysis **STEP 3: Document Decisions** - Document your analysis and design decisions - Create tasks to guide implementation - Focus on analysis and design only - do not write any code yet","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T11:12:19.962361+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T11:12:19.962361+02:00","dependencies":[{"issue_id":"responsible-vibe-32.1","depends_on_id":"responsible-vibe-32","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32.1.1","title":"Research: confirm promptAsync vs prompt behavior","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T12:56:13.271134+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T12:58:58.330246+02:00","closed_at":"2026-04-21T12:58:58.330246+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-32.1.1","depends_on_id":"responsible-vibe-32.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32.1.2","title":"Design: inject full phase instructions into compacting hook","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T12:56:13.49865+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T12:58:58.513881+02:00","closed_at":"2026-04-21T12:58:58.513881+02:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-32.1.2","depends_on_id":"responsible-vibe-32.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32.2","title":"Implement","description":"Write clean, focused code for the minor enhancement, test your changes, and prepare for commit. **STEP 1: Review Design and Requirements** - If exists: Follow your design from - Otherwise: Elaborate design options and present them to the user - If exists: Ensure the relevant requirements from are met - Otherwise: Ensure existing requirements are met based on your task context **STEP 2: Implement Changes** - Write clean, focused code for the minor enhancement - Test your changes to ensure they work correctly and don't break existing functionality **STEP 3: Prepare for Finalization** - Update task progress as needed - Prepare documentation and commit when ready","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T11:12:20.251568+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T11:12:20.251568+02:00","dependencies":[{"issue_id":"responsible-vibe-32.2","depends_on_id":"responsible-vibe-32","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"responsible-vibe-32.2","depends_on_id":"responsible-vibe-32.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"responsible-vibe-32.3","title":"Finalize","description":"Ensure code quality and documentation accuracy through systematic cleanup and review. **STEP 1: Code Cleanup** Systematically clean up development artifacts: - **Remove Debug Output**: Search for and remove all temporary debug output statements used during development. Look for language-specific debug output methods (console logging, print statements, debug output functions). Remove any debugging statements that were added for development purposes. - **Review TODO/FIXME Comments**: - Address each TODO/FIXME comment by either implementing the solution or documenting why it's deferred - Remove completed TODOs - Convert remaining TODOs to proper issue tracking if needed - **Remove Debugging Code Blocks**: - Remove temporary debugging code, test code blocks, and commented-out code - Clean up any experimental code that's no longer needed - Ensure proper error handling replaces temporary debug logging **STEP 2: Documentation Review** Review and update documentation to reflect final implementation: - **Update Long-Term Memory Documents**: Based on what was actually implemented: - If exists: Update if requirements changed during development - If exists: Update if design details were refined or changed - **Compare Against Implementation**: Review documentation against actual implemented functionality - **Update Changed Sections**: Only modify documentation sections that have functional changes - **Remove Development Progress**: Remove references to development iterations, progress notes, and temporary decisions - **Focus on Final State**: Ensure documentation describes the final implemented state, not the development process - **Ask User to Review Document Updates** **STEP 3: Final Validation** - Run existing tests to ensure cleanup didn't break functionality - Verify documentation accuracy with a final review - Ensure minor enhancement is ready for delivery - Update task progress and mark completed work as you finalize the minor enhancement","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-04-21T11:12:20.567032+02:00","created_by":"Oliver Jägle","updated_at":"2026-04-21T11:12:20.567032+02:00","dependencies":[{"issue_id":"responsible-vibe-32.3","depends_on_id":"responsible-vibe-32","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"responsible-vibe-32.3","depends_on_id":"responsible-vibe-32.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-4","title":"Responsible-Vibe Development: responsible-vibe","description":"Development session using minor workflow for responsible-vibe","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-20T08:45:26.033247+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-20T08:45:26.033247+01:00"} {"id":"responsible-vibe-4.1","title":"Explore","description":"minor workflow explore phase tasks","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-20T08:45:26.241377+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-20T08:45:26.241377+01:00","dependencies":[{"issue_id":"responsible-vibe-4.1","depends_on_id":"responsible-vibe-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} {"id":"responsible-vibe-4.1.1","title":"Analyze current verbose task management sections in plan file templates","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-01-20T08:46:16.960883+01:00","created_by":"Oliver Jägle","updated_at":"2026-01-20T08:46:58.154323+01:00","closed_at":"2026-01-20T08:46:58.154323+01:00","close_reason":"Closed","dependencies":[{"issue_id":"responsible-vibe-4.1.1","depends_on_id":"responsible-vibe-4.1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} diff --git a/.beads/last-touched b/.beads/last-touched index 2c5986f5..88e50fe6 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -responsible-vibe-31.4.3 +responsible-vibe-32.1.2 diff --git a/.vibe/development-plan-continue-after-compact-2.md b/.vibe/development-plan-continue-after-compact-2.md new file mode 100644 index 00000000..0b4e316c --- /dev/null +++ b/.vibe/development-plan-continue-after-compact-2.md @@ -0,0 +1,75 @@ +# Development Plan: responsible-vibe (continue-after-compact-2 branch) + +*Generated on 2026-04-21 by Vibe Feature MCP* +*Workflow: [minor](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/minor)* + +## Goal +After OpenCode context compaction, the AI should automatically continue with full workflow phase context. Two scenarios: +1. **Auto compaction** (`auto: true`): OpenCode creates a synthetic "Continue if you have next steps..." message, but it bypasses `chat.message` hook → phase instructions not injected. +2. **Manual compaction** (`auto: false`, triggered via UI): No synthetic continue message is created at all. Our plugin fires `client.session.prompt()` but the loop exits immediately without making an LLM call. + +## Key Decisions + +### Root cause findings (2026-04-21 deep dive) + +**Manual compaction (`auto: false`):** +- The compaction task part in the DB has `auto=0` — all observed compactions were triggered via the OpenCode TUI with `auto: false` +- Since `auto: false`, the synthetic "Continue if you have next steps..." message is NEVER created +- `session.compacted` is still published (it fires regardless of auto) +- Our plugin calls `client.session.prompt()` → POST `/session/{id}/message` → returns with `duration=0` +- The loop exits IMMEDIATELY without calling the LLM — confirmed by absence of `session.prompt step=0 loop` logs and no LLM activity in the ~1m46s window before user's manual message +- The exact cause of immediate loop exit is unclear but likely related to race conditions between the runner state machine and our prompt call timing + +**Auto compaction (`auto: true`):** +- Synthetic "Continue..." message IS created (with `synthetic: true` on the text part) +- The `chat.message` hook is NOT triggered for synthetic messages — this is the original stated problem +- AI responds but without phase context + +**Session.idle timing issue:** +- `session.idle` fires SIMULTANEOUSLY with `session.compacted` (same millisecond in logs) +- This is because after auto compaction, `session.idle` fires at the compaction step boundary +- For manual compaction: the loop at step=2 exits in 2ms after the compaction summary is created, then `session.idle` fires + +### Proposed fix approach + +**Two-pronged strategy:** + +1. **Enhance `experimental.session.compacting` hook** to inject FULL phase instructions into the compaction context/prompt. This makes the summary self-sufficient — it embeds the phase context so the AI knows what to continue even without `chat.message` firing. + +2. **Fix the post-compaction prompt injection:** + - Switch from `client.session.prompt()` (synchronous, blocks) to `client.session.promptAsync()` (fires-and-forgets, non-blocking) + - Add a small delay (e.g., 500ms) before calling to avoid race conditions with runner state transitions + - Use `client.session.promptAsync()` to inject "Continue with the current phase." so the loop runs properly without our code blocking on the HTTP response + +### Why `experimental.compaction.autocontinue` is not the solution +Setting `enabled: false` would prevent the synthetic message entirely and force reliance on our prompt — but this doesn't solve the immediate loop exit issue. + +## Notes +- All 3 compaction tasks in the DB have `auto=0` — this project uses manual compaction (via TUI) +- Session ID `ses_2516d00b9ffesNfmx4bVKPm4Hh` is a long-running session with messages from multiple days +- The `duration=0` for the HTTP `/session/{id}/message` response is NORMAL for streaming endpoints (measures time to first byte, not full response) +- The `chat.message` hook DOES fire for our `client.session.prompt()` messages — confirmed by synthetic phase instructions appearing in DB parts +- After the loop exits at step=2 (2ms), there's 1m46s of silence before user's manual message — our `promptAsync` call must have had no effect + +## Explore + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + +- [x] `responsible-vibe-32.1.1` Research: confirm promptAsync vs prompt behavior +- [x] `responsible-vibe-32.1.2` Design: inject full phase instructions into compacting hook + +## Implement + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + + +## Finalize + +### Tasks + +*Auto-synced — do not edit here, use `bd` CLI instead.* + diff --git a/packages/opencode-plugin/src/plugin.ts b/packages/opencode-plugin/src/plugin.ts index a8891037..88c183ce 100644 --- a/packages/opencode-plugin/src/plugin.ts +++ b/packages/opencode-plugin/src/plugin.ts @@ -162,6 +162,10 @@ export const WorkflowsPlugin: Plugin = async ( // Consumed and cleared by the next chat.message hook call. let bufferedInstructions: BufferedInstructions | null = null; + // Tracks sessions that just completed compaction and need a phase-aware + // continue message once the session becomes idle. + let postCompactionSession: string | null = null; + // Last-known model from chat.message hook. Cached so proceed_to_phase can // pass providerID + modelID to the summarize API (which requires them). let lastKnownModel: { providerID: string; modelID: string } | null = null; @@ -473,8 +477,10 @@ ACTION REQUIRED: Use transition_phase tool to move to a phase that allows editin /** * Hook 3: experimental.session.compacting - * Fires when session is being compacted. We provide minimal guidance on what - * to preserve and instruct the summary to end with phase continuation. + * Fires when session is being compacted. We provide full phase instructions + * so the compaction summary is self-sufficient — the AI knows exactly what + * to continue even if the chat.message hook doesn't fire for the synthetic + * auto-compaction "continue" message. */ 'experimental.session.compacting': async (hookInput, output) => { logger.debug('experimental.session.compacting hook fired', { @@ -487,14 +493,105 @@ ACTION REQUIRED: Use transition_phase tool to move to a phase that allows editin return; } + // Get full phase instructions to embed in compaction context + let phaseInstructions: string | null = null; + try { + const serverContext = await getServerContext(); + const handler = new WhatsNextHandler(); + const handlerResult = await handler.handle({}, serverContext); + if (handlerResult.success && handlerResult.data) { + phaseInstructions = stripWhatsNextReferences( + handlerResult.data.instructions + ); + } + } catch (_err) { + // Fall back to minimal guidance if instructions can't be fetched + } + output.context.push( 'Preserve: user intents, key decisions, significant changes and the reasoning why they were made. Remove tool calls, intermediate thoughts, and minor details.' ); - output.context.push( - `End summary with: "Continue ${state.phase} phase. ${state.phaseDescription || ''}"` - ); - logger.info('Injected compaction guidance', { phase: state.phase }); + if (phaseInstructions) { + output.context.push( + `Current workflow phase: ${state.phase}. After compaction, resume with full phase context:\n\n${phaseInstructions}` + ); + } else { + output.context.push( + `End summary with: "Continue ${state.phase} phase. ${state.phaseDescription || ''}"` + ); + } + + logger.info('Injected compaction guidance', { + phase: state.phase, + fullInstructions: phaseInstructions !== null, + }); + }, + + /** + * Hook 4: event + * Listens for bus events. When a session compaction completes we record it, + * then when the session becomes idle we send a real user message so the + * normal chat.message hook fires and injects phase instructions — giving the + * AI full workflow context to continue after the compaction. + * + * We intentionally do NOT suppress the default synthetic "continue" message + * (experimental.compaction.autocontinue). It may produce a first generic AI + * response, but the idle trigger below ensures a proper phase-aware follow-up. + */ + event: async ({ event }) => { + logger.debug('event hook fired', { type: event.type }); + + if (event.type === 'session.compacted') { + postCompactionSession = event.properties.sessionID as string; + logger.info('session.compacted: pending phase-aware continue', { + sessionID: postCompactionSession, + }); + return; + } + + if ( + event.type === 'session.idle' && + postCompactionSession === (event.properties.sessionID as string) + ) { + const sessionID = postCompactionSession; + postCompactionSession = null; + logger.info( + 'session.idle after compaction: sending phase-aware continue', + { sessionID } + ); + + // Wait a short time to allow the OpenCode runner state machine to + // fully settle after compaction before we fire the follow-up prompt. + await new Promise(resolve => setTimeout(resolve, 500)); + + try { + const client = input.client as { + session: { + promptAsync(params: { + path: { id: string }; + body: { parts: Array<{ type: string; text: string }> }; + }): Promise; + }; + }; + await client.session.promptAsync({ + path: { id: sessionID }, + body: { + parts: [ + { type: 'text', text: 'Continue with the current phase.' }, + ], + }, + }); + logger.info('session.idle: phase-aware continue sent (async)', { + sessionID, + }); + } catch (err) { + logger.error('session.idle: failed to send phase-aware continue', { + sessionID, + error: err instanceof Error ? err.message : String(err), + }); + } + } }, /** diff --git a/packages/opencode-plugin/src/types.ts b/packages/opencode-plugin/src/types.ts index 66e4e555..b6ea0036 100644 --- a/packages/opencode-plugin/src/types.ts +++ b/packages/opencode-plugin/src/types.ts @@ -74,9 +74,24 @@ export type ToolDefinition = { execute(args: unknown, context: ToolContext): Promise; }; +// Minimal Event types from @opencode-ai/sdk needed for the event hook +export type SessionCompactedEvent = { + type: 'session.compacted'; + properties: { sessionID: string }; +}; +export type SessionIdleEvent = { + type: 'session.idle'; + properties: { sessionID: string }; +}; +export type OtherEvent = { + type: string; + properties: Record; +}; +export type BusEvent = SessionCompactedEvent | SessionIdleEvent | OtherEvent; + // All available hooks export interface Hooks { - event?: (input: { event: unknown }) => Promise; + event?: (input: { event: BusEvent }) => Promise; config?: (input: unknown) => Promise; tool?: { [key: string]: ToolDefinition;