Skip to content

fix(opencode): comprehensive compatibility fix for opencode 1.18.x#605

Closed
LeoNardo-LB wants to merge 2 commits into
EvoMap:mainfrom
LeoNardo-LB:fix/opencode-final
Closed

fix(opencode): comprehensive compatibility fix for opencode 1.18.x#605
LeoNardo-LB wants to merge 2 commits into
EvoMap:mainfrom
LeoNardo-LB:fix/opencode-final

Conversation

@LeoNardo-LB

@LeoNardo-LB LeoNardo-LB commented Jul 21, 2026

Copy link
Copy Markdown

Summary

The opencode adapter generated by evolver setup-hooks --platform=opencode does not actually work on opencode 1.18.x. This PR fixes 5 independent layers of incompatibility discovered through systematic debugging with breakpoint diagnostics.

Environment

  • OS: Windows Server 2022
  • opencode: 1.18.4 (scoop)
  • Runtime: Bun (embedded in opencode; typeof Bun === 'object')
  • evolver: 1.92.1 (npm)

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(), then readV1Plugin() checks mod.default for { id, server }. The original module.exports = { Evolver }; module.exports.default = Evolver lacks a server field.

Fix: const pluginModule = { id: "evolver", server: Evolver }; module.exports = pluginModule; module.exports.default = pluginModule;

Layer 2: session.created event timing

Symptom: session.created never 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.message hook (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 event hook signature is (input) => Promise<void> — return values are discarded.

Fix: Use chat.message hook which can mutate output.parts (forwarded to LLM).

Layer 4: Bun spawnSync unreliable with script files on Windows

Symptom: spawnSync('node', ['session-start.js']) returns ETIMEDOUT.

Diagnostic self-test inside the Bun process:

spawnSync('node', ['-e', 'code'])     OK     (inline code)
spawnSync('node', ['script.js'])      ETIMEDOUT  (script file)

Fix: Inline memory reader using shared modules (_runtimePaths, _memoryFiltering) from the hooks directory. No spawnSync needed 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)

# Finding Status
1 Premature memoryInjected latch Fixed: set after successful injection
2 Unscoped inline memory Fixed: require shared modules for workspace filtering
3 spawnSync timeout stall Fixed: inline-first, spawnSync as fallback
4 DIAG logging Retained for maintainer verification
5 Skipped side effects Fixed: fire-and-forget spawnSync after inline
6 Zero score as N/A Fixed: !== undefined check
7 Full graph parse Fixed: tail-read 64KB
8 Duplicated helpers Fixed: require from hooks dir
9 Unsafe parts shape Verified via breakpoint diagnostics
10 Incomplete path search Fixed: findMemoryGraph()
11 Failed outcomes marked [+] Fixed: [+]/[-] based on status
12 Test assertions outdated Fixed: updated

Local Verification

[Evolution Memory] Recent 3 outcomes (3 success, 0 failed):
[+] 2026-07-21 score=0.8 signals=[perf_bottleneck,...] ...
✅ Memory injected into output.parts (723 chars)
✅ Dedup works (second message: parts=1, no re-inject)
✅ Workspace filtering via filterRelevantOutcomes

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.js now exports { id: "evolver", server: Evolver } (with default pointing at the same object) so opencode’s Bun import() / v1 plugin shape accepts it. Verify also treats mod.server as the loadable factory, not only Evolver.

Memory injection moves from session.created / event to chat.message, which can prepend text to output.parts on the first user message (with a one-shot memoryInjected guard). The event handler only handles session.idleevolver-session-end.js.

Adds an inline memory path that requires hook helpers _runtimePaths and _memoryFiltering, tail-reads the JSONL graph (64KB), formats outcomes with [+]/[-] and proper score handling, and falls back to spawnSync session-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.log markers 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.

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 10 potential issues.

Fix All in Cursor

❌ 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.

Comment thread src/adapters/opencode.js
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');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39210a5. Configure here.

Comment thread src/adapters/opencode.js
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.';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39210a5. Configure here.

Comment thread src/adapters/opencode.js
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) {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39210a5. Configure here.

Comment thread src/adapters/opencode.js
if (text && output && Array.isArray(output.parts)) {
output.parts.unshift({ type: "text", text: text });
memoryInjected = true;
evolverMark('E1-injected', 'parts=' + output.parts.length);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39210a5. Configure here.

Comment thread src/adapters/opencode.js
session_id: input && input.sessionID,
}, 10000);
text = result && (result.agent_message || result.additionalContext);
evolverMark('D1-spawnSync', 'text=' + (text ? text.length : 0));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39210a5. Configure here.

Comment thread src/adapters/opencode.js
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'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Triggered by team rule: Mandatory review after completing all tasks

Reviewed by Cursor Bugbot for commit 39210a5. Configure here.

Comment thread src/adapters/opencode.js
try {
var evolverRoot = runtimePaths.findEvolverRoot();
if (!evolverRoot) return null;
var graphPath = runtimePaths.findMemoryGraph(evolverRoot);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 39210a5. Configure here.

@LeoNardo-LB

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant