fix(opencode): comprehensive compatibility fix for opencode 1.18.x#604
fix(opencode): comprehensive compatibility fix for opencode 1.18.x#604LeoNardo-LB wants to merge 4 commits into
Conversation
1. PluginModule export with explicit .default 2. chat.message hook (event hook cannot return values to LLM) 3. Inline readEvolverMemoryInline (Bun spawnSync unreliable on Windows) 4. Diagnostic markers for author verification 5. verify() updated for new export format
|
|
||
| // DIAG: diagnostic markers (remove after verification) | ||
| var evolverMarkPath = require("path").join(HOOKS_DIR, "..", ".evolver-diag.log"); | ||
| try { require("fs").appendFileSync(evolverMarkPath, new Date().toISOString() + " [A] module loaded\n"); } catch(e) {} |
There was a problem hiding this comment.
Diagnostic logging left in plugin
Medium Severity
The plugin ships with temporary diagnostic markers (DIAG) that write logs to .evolver-diag.log on module load and during chat.message hook execution. This introduces debug side effects for users.
Triggered by team rule: Mandatory review after completing all tasks
Reviewed by Cursor Bugbot for commit d97783b. Configure here.
| try { require("fs").appendFileSync(evolverMarkPath, new Date().toISOString() + " [E] inject text=" + (text ? text.length : 0) + "\n"); } catch (e) {} | ||
| if (text && output && Array.isArray(output.parts)) { | ||
| output.parts.unshift({ type: "text", text: text }); | ||
| } |
There was a problem hiding this comment.
Unsafe parts injection shape
Medium Severity
chat.message unshifts a bare { type: "text", text } into output.parts with no sessionID or part ids. Upstream opencode still reports intermittent SyncEvent.run: "sessionID" required crashes for this exact pattern, and the PR's [E] marker is logged before the unshift so it does not prove the mutation is safe.
Reviewed by Cursor Bugbot for commit d97783b. Configure here.
EvoMap#1: memoryInjected set only after successful injection EvoMap#2: workspace-aware filtering via shared _runtimePaths + _memoryFiltering EvoMap#3: inline-first (0ms), spawnSync as fallback #5: best-effort spawnSync for side effects after inline success #6: score !== undefined (handles score=0) EvoMap#7: tail-read 64KB instead of full-file parse EvoMap#8: require shared modules instead of duplicating helpers EvoMap#10: findMemoryGraph for multi-path search EvoMap#11: [+]/[-] prefix based on outcome.status EvoMap#12: updated test assertions for new export format
EvoMap#1: memoryInjected set only after successful injection EvoMap#2: workspace-aware filtering via shared _runtimePaths + _memoryFiltering EvoMap#3: inline-first (0ms), spawnSync as fallback #5: best-effort spawnSync for side effects after inline success #6: score !== undefined (handles score=0) EvoMap#7: tail-read 64KB instead of full-file parse EvoMap#8: require shared modules instead of duplicating helpers EvoMap#10: findMemoryGraph for multi-path search EvoMap#11: [+]/[-] prefix based on outcome.status EvoMap#12: test assertions updated (separate commit) EvoMap#4: DIAG markers retained for maintainer verification EvoMap#9: parts format — works in practice, verified via breakpoint diagnostics
|
Thanks for the thorough review! All 12 findings have been addressed in commit d4cca14: High:
Medium:
Low:
The key architectural change: instead of reimplementing session-start.js logic inline, the plugin now requires the shared helper modules that setup-hooks already deploys to ~/.opencode/hooks/. This ensures workspace filtering, path resolution, and outcome formatting are identical to the hook script path. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 7 potential issues.
There are 9 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d4cca14. 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 undefined identifier
High Severity
The generated plugin references hooksDirAbsolute in its diagnostic logging path, but this variable is not defined in the emitted file. This causes a ReferenceError during module evaluation, preventing the plugin from loading correctly.
Reviewed by Cursor Bugbot for commit d4cca14. Configure here.
| var evolverRoot = runtimePaths.findEvolverRoot(); | ||
| if (!evolverRoot) return null; | ||
| var graphPath = runtimePaths.findMemoryGraph(evolverRoot); | ||
| if (!graphPath || !require('fs').existsSync(graphPath)) return null; |
There was a problem hiding this comment.
Null root skips memory fallback
High Severity
The readEvolverMemoryInline function exits early if runtimePaths.findEvolverRoot() returns null. This bypasses findMemoryGraph's fallback to the user's home directory, resulting in empty inline memory for environments unable to resolve the package root, a regression from the prior behavior.
Reviewed by Cursor Bugbot for commit d4cca14. 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.
Deferred flag retries spawn every message
High Severity
The memoryInjected flag is only set when memory is successfully injected into output.parts. If memory is unavailable or output.parts is missing, the flag remains false, causing the chat.message handler to repeatedly execute blocking spawnSync calls. This leads to multi-second stalls on subsequent messages, particularly on Windows/Bun.
Reviewed by Cursor Bugbot for commit d4cca14. 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 blocks first message
Medium Severity
The chat.message handler still runs spawnSync for evolver-session-start.js side effects even after successful inline memory reading. On Windows/Bun, this spawnSync often times out, stalling the handler for up to 2 seconds and preventing session recording from completing.
Reviewed by Cursor Bugbot for commit d4cca14. Configure here.
| tool_input: (output && output.args) || {}, | ||
| tool_response: (output && output.output) || (output && output.result) || {}, | ||
| }, 2000); | ||
| }, 5000); |
There was a problem hiding this comment.
Verify omits required helper scripts
Medium Severity
The generated plugin now hard-requires _runtimePaths.js and _memoryFiltering.js at load time, but verify() still only checks the three entry hook scripts. A hooks dir missing those helpers can report hook_scripts_present as ok while plugin_loadable fails with a generic MODULE_NOT_FOUND.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d4cca14. Configure here.
| return buf.toString('utf8').trim().split('\\n').slice(-maxEntries).map(function(l) { | ||
| try { return JSON.parse(l); } catch(e) { return null; } | ||
| }).filter(Boolean); | ||
| } catch(e) { return []; } |
There was a problem hiding this comment.
File descriptor leak on read errors
Medium Severity
readRecentEntries opens the memory graph with openSync but only calls closeSync on the success path. If readSync or later work throws, the outer catch returns [] without closing the fd, so repeated chat.message failures can leak descriptors in the long-lived opencode process.
Reviewed by Cursor Bugbot for commit d4cca14. 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 test still passes
Medium Severity
This commit removes the session.created event handler, but the test still looks for session.created near evolver-session-start.js. That regex only matches the outdated header comment, so the suite keeps passing while the real injection path is now chat.message.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d4cca14. Configure here.
|
Replaced by a clean PR with squashed commits. See new PR. |


Summary
After the export-format fix in #603 was verified to load without errors, a deeper investigation revealed that the plugin still did not actually function on opencode 1.18.x. This PR addresses 5 independent layers of incompatibility discovered through systematic debugging.
Each fix includes diagnostic markers (writing to
~/.opencode/.evolver-diag.log) so the maintainer can verify on their own environment before merging. Once verified, the DIAG lines can be stripped.Environment
typeof Bun === 'object',process.version = 'v24.3.0')@evomap/evolver: 1.92.1 (npm)D:\Develop\Scoop\apps\nodejs\current\node.exe)Layer 1: CJS-ESM export format
Symptom:
Plugin export is not a function— plugin silently skipped.Root cause: opencode loads plugins via
import(), thenreadV1Plugin()checksmod.default:The original
module.exports = { Evolver }; module.exports.default = Evolverresults inmod.defaultbeing the entiremodule.exportsobject ({ Evolver, default: Evolver }), which lacks aserverfield.readV1Pluginreturnsundefinedin detect mode.Fix: Switch to explicit PluginModule format:
Also updated
verify()to checkmod.server || mod.Evolver || mod.default.Layer 2: session.created event timing
Symptom: Even after Layer 1 fix,
session.creatednever appears in the event hook (0 out of 1718 events observed).Root cause: opencode startup sequence is:
session.createdeventevents.listenThe plugin misses
session.createdbecause it fires before the listener is registered. This is a design timing difference from Claude Code / Cursor.Layer 3: event hook cannot return values to LLM
Symptom: Even if
session.createdwere received, the memory injection would not work.Root cause: opencode's
eventhook signature is(input: { event }) => Promise<void>. The return value is not consumed. The original code callsrunHook()but discards the result:On Claude Code / Cursor, the hook script stdout is directly injected into the agent context. opencode does not do this.
Fix: Add a
chat.messagehook that can mutateoutput.parts(which opencode forwards to the LLM):Layer 4: Bun spawnSync with script files is unreliable on Windows
Symptom:
spawnSync('node', ['session-start.js'], ...)returnsETIMEDOUTinside the opencode Bun process.Evidence (from diagnostic self-test inside the Bun process):
Root cause: Bun's
spawnSyncon Windows may not correctly passinput(stdin) to the child process when executing a script file. The hook scripts have stdin watchdogs (1.5s timeout in session-start.js), and if stdin is not delivered, they wait out the watchdog, then hit additional blocking logic, eventually exceeding the overallspawnSynctimeout.Fix: Add
readEvolverMemoryInline()— a pure fs+JSON implementation that reads~/.evolver/memory/evolution/memory_graph.jsonldirectly, with nospawnSyncdependency. Used as fallback whenspawnSyncreturns empty.Layer 5: process.execPath is opencode.exe, not a JS runtime
Symptom:
spawnSync(process.execPath, ...)fails.Root cause:
process.execPathinside opencode =D:\Develop\Scoop\apps\opencode\1.18.4\opencode.exe. This is the opencode CLI binary, not Node or Bun.opencode.exe script.jsdoes not execute JS — it interprets the path as a CLI argument.Diagnostic markers
This PR includes diagnostic writes to
~/.opencode/.evolver-diag.log:[A]module loaded (withtypeof Bun)[C]chat.message handler entered[D]spawnSync result text length[D1]inline fallback result[E]final injected text length[G]tool.execute.after triggeredThese are marked with
// DIAG:comments and should be removed once verified.Verification steps for the maintainer
If all markers show expected values, the DIAG lines can be stripped and this PR is ready to merge.
Related
Note
Medium Risk
Changes when and how evolution memory reaches the model and alters plugin export shape for opencode’s loader; behavior differs on Bun/Windows where spawnSync to hook scripts is unreliable.
Overview
Makes the generated opencode plugin load under 1.18.x and actually inject evolution memory into the LLM, instead of relying on
session.createdand discardedeventhook results.The plugin now exports
{ id: "evolver", server: Evolver }(withdefaultpointing at the same object) so opencode’s v1 loader findsserver.verify()acceptsmod.serveras well as legacyEvolver/default.Memory moves to a
chat.messagehandler that prepends text tooutput.partsonce per process. It prefersreadEvolverMemoryInline()(tail-readmemory_graph.jsonl, filter via deployed_runtimePaths/_memoryFiltering), then falls back toevolver-session-start.jsviaspawnSyncwhen inline returns nothing; when inline succeeds it still runs session-start briefly for side effects. Thesession.createdbranch is removed fromevent(onlysession.idle→ session-end remains). Hook timeouts for session-end and signal-detect are increased.Temporary
// DIAGwrites to.evolver-diag.logunder.opencodefor maintainer verification. Tests were updated for the new default export shape in one place; thebuildPluginSourceexport test may still expect the old{ Evolver }export pattern.Reviewed by Cursor Bugbot for commit d4cca14. Bugbot is set up for automated code reviews on this repo. Configure here.