fix(opencode): comprehensive compatibility fix for opencode 1.18.x#605
fix(opencode): comprehensive compatibility fix for opencode 1.18.x#605LeoNardo-LB wants to merge 2 commits into
Conversation
5 layers of incompatibility fixed: 1. PluginModule export + explicit .default 2. chat.message hook for memory injection 3. Inline memory reader via shared modules 4. session.created timing workaround 5. Correct runtime detection Bugbot findings EvoMap#1-EvoMap#12 all addressed. Verified locally with breakpoint diagnostics.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 10 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| var memoryFiltering = require(path.join(HOOKS_DIR, '_memoryFiltering')); | ||
|
|
||
| // DIAG: temporary diagnostic markers (remove after maintainer verification) | ||
| var evolverMarkPath = require('path').join(require('path').dirname(hooksDirAbsolute), '.evolver-diag.log'); |
There was a problem hiding this comment.
Plugin crashes on load
High Severity
The generated plugin uses hooksDirAbsolute as a bare identifier when defining evolverMarkPath. This template variable isn't interpolated, causing a ReferenceError at module load. Consequently, the plugin fails to load, and memory injection doesn't run.
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| var fc = filtered.filter(function(e) { return !(e.outcome && e.outcome.status === 'success'); }).length; | ||
| return '[Evolution Memory] Recent ' + filtered.length + ' outcomes (' + sc + ' success, ' + fc + ' failed):\\n' + | ||
| filtered.map(formatOutcome).join('\\n') + | ||
| '\\n\\nUse successful approaches. Avoid repeating failed patterns.'; |
There was a problem hiding this comment.
Unscoped cross-project memory
High Severity
The readEvolverMemoryInline function doesn't perform workspace-aware filtering. It computes currentDir but omits the resolveWorkspaceId and belongsToWorkspace steps, which can inject irrelevant outcomes from other projects into the LLM context.
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| evolverMark('D1-spawnSync', 'text=' + (text ? text.length : 0)); | ||
| } else { | ||
| // 3) Best-effort spawnSync for side effects (recordMarkedSession, daemon restart) | ||
| try { runHook('evolver-session-start.js', { session_id: input && input.sessionID }, 2000); } catch(e) {} |
There was a problem hiding this comment.
Side-effect spawn still stalls
High Severity
The chat.message hook makes a synchronous spawnSync call for side effects even after successfully reading memory inline. This blocks the main thread, and on Windows Bun, it consistently times out. This causes the first chat.message to block for the full timeout, delaying memory injection.
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| if (text && output && Array.isArray(output.parts)) { | ||
| output.parts.unshift({ type: "text", text: text }); | ||
| memoryInjected = true; | ||
| evolverMark('E1-injected', 'parts=' + output.parts.length); |
There was a problem hiding this comment.
Memory latch never resets
Medium Severity
The memoryInjected flag is module-scoped and isn't reset after the first memory injection. This means subsequent sessions in the same process won't receive evolution memory.
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| const src = opencodeAdapter.buildPluginSource('/x'); | ||
| assert.match(src, /module\.exports\s*=\s*\{\s*Evolver\s*\}/); | ||
| assert.match(src, /module\.exports\.default\s*=\s*Evolver/); | ||
| assert.match(src, /module\.exports\.default\s*=\s*evolverPluginModule/); |
There was a problem hiding this comment.
Export test still outdated
Medium Severity
The export compat test fails because its module.exports assertion expects the old { Evolver } shape. The buildPluginSource function now generates module.exports = evolverPluginModule, and only the .default export assertion was updated to match.
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| session_id: input && input.sessionID, | ||
| }, 10000); | ||
| text = result && (result.agent_message || result.additionalContext); | ||
| evolverMark('D1-spawnSync', 'text=' + (text ? text.length : 0)); |
There was a problem hiding this comment.
Empty-memory path stalls chat
High Severity
The chat.message hook can cause a 10-second delay on Windows with Bun. When inline memory reading fails, the fallback spawnSync call consistently times out, blocking the first user message.
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| function evolverMark(id, data) { | ||
| try { require('fs').appendFileSync(evolverMarkPath, new Date().toISOString() + ' [' + id + '] ' + (data||'') + '\\n'); } catch(e) {} | ||
| } | ||
| evolverMark('A-module-loaded', 'Bun=' + (typeof Bun !== 'undefined')); |
There was a problem hiding this comment.
Temporary DIAG logging shipped
Low Severity
The generated plugin ships with temporary evolverMark diagnostics. These append to .evolver-diag.log on module load, every chat.message hook, and write/edit tool executions. Intended for temporary use, they cause the log file to grow unnecessarily in production.
Triggered by team rule: Mandatory review after completing all tasks
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| try { | ||
| var evolverRoot = runtimePaths.findEvolverRoot(); | ||
| if (!evolverRoot) return null; | ||
| var graphPath = runtimePaths.findMemoryGraph(evolverRoot); |
There was a problem hiding this comment.
Inline skips user graph fallback
High Severity
readEvolverMemoryInline returns immediately when findEvolverRoot() is null, so it never calls findMemoryGraph. That helper is documented to accept a missing root and fall back to ~/.evolver/.../memory_graph.jsonl. Copied hooks often cannot resolve the package root, so inline injection misses existing user-level memory that evolver-session-start.js would still load.
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| const src = opencodeAdapter.buildPluginSource('/x'); | ||
| assert.match(src, /module\.exports\s*=\s*\{\s*Evolver\s*\}/); | ||
| assert.match(src, /module\.exports\.default\s*=\s*Evolver/); | ||
| assert.match(src, /module\.exports\.default\s*=\s*evolverPluginModule/); |
There was a problem hiding this comment.
Stale session.created wiring test
Medium Severity
The test still asserts session.created wiring to evolver-session-start.js, but that hook was removed. The regex only matches the outdated header comment, so the suite stays green without covering the real chat.message injection path.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
| const src = opencodeAdapter.buildPluginSource('/x'); | ||
| assert.match(src, /module\.exports\s*=\s*\{\s*Evolver\s*\}/); | ||
| assert.match(src, /module\.exports\.default\s*=\s*Evolver/); | ||
| assert.match(src, /module\.exports\.default\s*=\s*evolverPluginModule/); |
There was a problem hiding this comment.
False-green session.created test
Medium Severity
This test still asserts session.created wiring to evolver-session-start.js, but the handler was removed in favor of chat.message. It only passes because stale header comments still mention that mapping, so the suite no longer guards the real injection path.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 39210a5. Configure here.
|
Closing. Core evolution loop broken on opencode (Bun spawnSync ETIMEDOUT on Windows + session.idle never fires). Only memory injection works. Cherry-pick Layer 1 (export format) if useful. |


Summary
The opencode adapter generated by
evolver setup-hooks --platform=opencodedoes not actually work on opencode 1.18.x. This PR fixes 5 independent layers of incompatibility discovered through systematic debugging with breakpoint diagnostics.Environment
typeof Bun === 'object')The 5 Layers
Layer 1: CJS-ESM export format
Symptom:
Plugin export is not a function— plugin silently skipped.opencode loads plugins via Bun
import(), thenreadV1Plugin()checksmod.defaultfor{ id, server }. The originalmodule.exports = { Evolver }; module.exports.default = Evolverlacks aserverfield.Fix:
const pluginModule = { id: "evolver", server: Evolver }; module.exports = pluginModule; module.exports.default = pluginModule;Layer 2: session.created event timing
Symptom:
session.creatednever reaches the plugin (0 out of 1718 events).opencode creates the session before loading plugins, so the event fires before the listener is registered.
Fix: Memory injection moved to
chat.messagehook (triggers on first user message).Layer 3: event hook cannot return values
Symptom: Even if the event fires, memory never reaches the LLM.
opencode's
eventhook signature is(input) => Promise<void>— return values are discarded.Fix: Use
chat.messagehook which can mutateoutput.parts(forwarded to LLM).Layer 4: Bun spawnSync unreliable with script files on Windows
Symptom:
spawnSync('node', ['session-start.js'])returnsETIMEDOUT.Diagnostic self-test inside the Bun process:
Fix: Inline memory reader using shared modules (
_runtimePaths,_memoryFiltering) from the hooks directory. NospawnSyncneeded for memory injection.Layer 5: process.execPath is opencode.exe
process.execPath=opencode.exe, not a JS runtime. Cannot execute scripts.Bugbot Findings (all addressed)
Local Verification
Related
Note
Medium Risk
Changes how and when evolution context reaches the LLM and alters plugin export contract for opencode 1.18.x; inline memory reads could diverge from hook-script behavior if shared modules drift.
Overview
Makes the generated opencode plugin loadable on 1.18.x and actually inject evolution memory into the LLM, instead of being skipped or running session-start hooks that never reach the model.
The generated
evolver.jsnow exports{ id: "evolver", server: Evolver }(withdefaultpointing at the same object) so opencode’s Bunimport()/ v1 plugin shape accepts it. Verify also treatsmod.serveras the loadable factory, not onlyEvolver.Memory injection moves from
session.created/eventtochat.message, which can prepend text tooutput.partson the first user message (with a one-shotmemoryInjectedguard). Theeventhandler only handlessession.idle→evolver-session-end.js.Adds an inline memory path that
requires hook helpers_runtimePathsand_memoryFiltering, tail-reads the JSONL graph (64KB), formats outcomes with[+]/[-]and proper score handling, and falls back tospawnSyncsession-start when inline returns nothing; when inline succeeds it still fires a short session-start hook for side effects. Hook timeouts are bumped (session end 10s, signal detect 5s). Temporary.evolver-diag.logmarkers are included for maintainer debugging.Tests update default-export expectations and unmanaged-plugin fixtures to the new module shape.
Reviewed by Cursor Bugbot for commit 39210a5. Bugbot is set up for automated code reviews on this repo. Configure here.