From d97783bd5c937d371bc24f6b50f0a45d3671b891 Mon Sep 17 00:00:00 2001 From: LeoNardo <58056860+LeoNardo-LB@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:24:21 +0800 Subject: [PATCH 1/4] fix(opencode): full compatibility for opencode 1.18.x 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 --- src/adapters/opencode.js | 66 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/adapters/opencode.js b/src/adapters/opencode.js index 831dca32..41fdefda 100644 --- a/src/adapters/opencode.js +++ b/src/adapters/opencode.js @@ -33,6 +33,36 @@ const path = require('node:path'); const HOOKS_DIR = ${JSON.stringify(hooksDirAbsolute)}; +// opencode 1.18.x: Bun spawnSync with script files is unreliable (ETIMEDOUT on Windows). +// Inline memory reading as fallback - pure fs+JSON, works in any JS runtime. +function readEvolverMemoryInline() { + try { + var os = require("os"); + var evolverRoot = process.env.EVOLVER_SETTINGS_DIR || (os.homedir() + "/.evolver"); + var graphPath = evolverRoot + "/memory/evolution/memory_graph.jsonl"; + if (!require("fs").existsSync(graphPath)) return null; + var raw = require("fs").readFileSync(graphPath, "utf8").trim(); + if (!raw) return null; + var entries = raw.split("\n").map(function(l) { try { return JSON.parse(l); } catch(e) { return null; } }).filter(Boolean); + if (entries.length === 0) return null; + var recent = entries.slice(-5); + var sc = recent.filter(function(e) { return e.outcome && e.outcome.status === "success"; }).length; + var fc = recent.filter(function(e) { return e.outcome && e.outcome.status === "failed"; }).length; + var lines = recent.map(function(e) { + var t = e.timestamp || "unknown"; + var s = e.score !== undefined ? e.score : (e.outcome && e.outcome.score) || "N/A"; + var sig = e.signals || (e.outcome && e.outcome.signals) || []; + var n = e.outcome && e.outcome.note ? " " + e.outcome.note : ""; + return "[+] " + t + " score=" + s + " signals=" + JSON.stringify(sig) + n; + }); + return "[Evolution Memory] Recent " + recent.length + " outcomes (" + sc + " success, " + fc + " failed):\n" + lines.join("\n") + "\n\nUse successful approaches. Avoid repeating failed patterns."; + } catch(e) { return null; } +} + +// 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) {} + function runHook(scriptName, payload, timeoutMs) { try { const result = spawnSync('node', [path.join(HOOKS_DIR, scriptName)], { @@ -47,7 +77,36 @@ function runHook(scriptName, payload, timeoutMs) { } } +var memoryInjected = false; + const Evolver = async () => ({ + // opencode 1.18.x: chat.message hook can inject memory into output.parts + // event hook signature is (input) => void, it cannot return values to LLM. + "chat.message": async (input, output) => { + if (memoryInjected) return; + memoryInjected = true; + // DIAG: mark handler entry + try { require("fs").appendFileSync(evolverMarkPath, new Date().toISOString() + " [C] chat.message\n"); } catch (e) {} + // Try spawnSync first (works on Claude Code / Cursor) + var result = runHook("evolver-session-start.js", { + session_id: input && input.sessionID, + }, 10000); + var text = result && (result.agent_message || result.additionalContext); + // DIAG: mark spawn result + try { require("fs").appendFileSync(evolverMarkPath, new Date().toISOString() + " [D] spawnSync text=" + (text ? text.length : 0) + "\n"); } catch (e) {} + // Fallback: inline memory reading (Bun spawnSync unreliable on Windows) + if (!text) { + text = readEvolverMemoryInline(); + try { require("fs").appendFileSync(evolverMarkPath, new Date().toISOString() + " [D1] inline text=" + (text ? text.length : 0) + "\n"); } catch (e) {} + } + // DIAG: mark final inject + 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 }); + } + }, + + // Keep event hook for backward compatibility (older opencode versions) event: async ({ event }) => { if (!event || typeof event.type !== 'string') return; if (event.type === 'session.created') { @@ -72,8 +131,9 @@ const Evolver = async () => ({ }, }); -module.exports = { Evolver }; -module.exports.default = Evolver; +const evolverPluginModule = { id: "evolver", server: Evolver }; +module.exports = evolverPluginModule; +module.exports.default = evolverPluginModule; `; } @@ -220,7 +280,7 @@ function verify({ configRoot }) { // failure here is a guaranteed failure under opencode too. delete require.cache[require.resolve(pluginPath)]; const mod = require(pluginPath); - const fn = mod && (mod.Evolver || mod.default); + const fn = mod && (mod.server || mod.Evolver || mod.default); pluginLoadable = typeof fn === 'function'; if (!pluginLoadable) pluginLoadError = 'no Evolver/default function export'; } catch (err) { From 157682a406fa0003b58612e2a5f228f0d3261a08 Mon Sep 17 00:00:00 2001 From: LeoNardo <58056860+LeoNardo-LB@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:22:44 +0800 Subject: [PATCH 2/4] fix(opencode): systematic fix addressing all Bugbot findings #1: memoryInjected set only after successful injection #2: workspace-aware filtering via shared _runtimePaths + _memoryFiltering #3: inline-first (0ms), spawnSync as fallback #5: best-effort spawnSync for side effects after inline success #6: score !== undefined (handles score=0) #7: tail-read 64KB instead of full-file parse #8: require shared modules instead of duplicating helpers #10: findMemoryGraph for multi-path search #11: [+]/[-] prefix based on outcome.status #12: updated test assertions for new export format --- src/adapters/opencode.js | 171 ++++++++++++++++++++++++++++++++------- 1 file changed, 142 insertions(+), 29 deletions(-) diff --git a/src/adapters/opencode.js b/src/adapters/opencode.js index 41fdefda..064065ac 100644 --- a/src/adapters/opencode.js +++ b/src/adapters/opencode.js @@ -12,53 +12,166 @@ const EVOLVER_MARKER = ''; const PLUGIN_HEADER = '// _evolver_managed: true (do not remove this line)'; function buildPluginSource(hooksDirAbsolute) { - // The plugin shells out to the same three Node scripts that every other - // evolver adapter uses, so the runtime behavior stays consistent across - // hosts. opencode loads .js files synchronously at startup and invokes - // the default-exported async factory with a context object. return `${PLUGIN_HEADER} -// Auto-generated by \`evolver setup-hooks --platform=opencode\`. +// Auto-generated by `evolver setup-hooks --platform=opencode`. // See https://opencode.ai/docs/plugins/ for the plugin API. // // This plugin wires opencode events to the three evolver hook scripts: -// session.created -> evolver-session-start.js (inject memory) +// chat.message -> inline memory injection + evolver-session-start.js // tool.execute.after -> evolver-signal-detect.js (detect signals on writes/edits) // session.idle -> evolver-session-end.js (record outcome) // -// Each script is a stdin/stdout JSON filter -- nothing else is shared with -// opencode, so removing this file fully detaches evolver from opencode. +// Architecture note: opencode 1.18.x loads plugins via Bun's import(), then +// readV1Plugin() checks mod.default for { id, server }. The event hook returns +// void and cannot inject content into the LLM context, so memory injection +// uses the chat.message hook which can mutate output.parts. +// +// On Windows, Bun's spawnSync with script files is unreliable (ETIMEDOUT), +// so memory is read inline via shared modules as the primary path, with +// spawnSync as fallback for platforms where it works (Claude Code / Cursor). const { spawnSync } = require('node:child_process'); +const fs = require('node:fs'); const path = require('node:path'); const HOOKS_DIR = ${JSON.stringify(hooksDirAbsolute)}; -// opencode 1.18.x: Bun spawnSync with script files is unreliable (ETIMEDOUT on Windows). -// Inline memory reading as fallback - pure fs+JSON, works in any JS runtime. +// Require shared modules from hooks directory (deployed by setup-hooks). +// These are side-effect-free pure function modules. +const runtimePaths = require(path.join(HOOKS_DIR, '_runtimePaths')); +const memoryFiltering = require(path.join(HOOKS_DIR, '_memoryFiltering')); + +// DIAG: temporary diagnostic markers (remove after maintainer verification) +var evolverMarkPath = path.join(path.dirname(HOOKS_DIR), '.evolver-diag.log'); +function mark(id, data) { + try { fs.appendFileSync(evolverMarkPath, new Date().toISOString() + ' [' + id + '] ' + (data || '') + '\\n'); } catch (e) {} +} +mark('A-module-loaded', 'Bun=' + (typeof Bun !== 'undefined')); + +// Read recent entries from jsonl tail (avoid full-file parse for large graphs). +function readRecentEntries(graphPath, maxEntries) { + try { + var stat = fs.statSync(graphPath); + var tailBytes = Math.min(stat.size, 64 * 1024); + var fd = fs.openSync(graphPath, 'r'); + var buf = Buffer.alloc(tailBytes); + fs.readSync(fd, buf, 0, tailBytes, Math.max(0, stat.size - tailBytes)); + fs.closeSync(fd); + var lines = buf.toString('utf8').trim().split('\\n'); + return lines.slice(-maxEntries).map(function(l) { + try { return JSON.parse(l); } catch (e) { return null; } + }).filter(Boolean); + } catch (e) { return []; } +} + +// Format outcome with correct [+]/[-] prefix and score handling. +function formatOutcome(e) { + var ok = e.outcome && e.outcome.status === 'success'; + var prefix = ok ? '[+]' : '[-]'; + var score = (e.outcome && e.outcome.score !== undefined && e.outcome.score !== null) + ? e.outcome.score + : (e.score !== undefined && e.score !== null ? e.score : 'N/A'); + var signals = e.signals || (e.outcome && e.outcome.signals) || []; + var note = e.outcome && e.outcome.note ? ' ' + e.outcome.note : ''; + return prefix + ' ' + (e.timestamp || 'unknown') + ' score=' + score + ' signals=' + JSON.stringify(signals) + note; +} + +// Inline memory reader using shared modules for workspace-aware filtering. function readEvolverMemoryInline() { try { - var os = require("os"); - var evolverRoot = process.env.EVOLVER_SETTINGS_DIR || (os.homedir() + "/.evolver"); - var graphPath = evolverRoot + "/memory/evolution/memory_graph.jsonl"; - if (!require("fs").existsSync(graphPath)) return null; - var raw = require("fs").readFileSync(graphPath, "utf8").trim(); - if (!raw) return null; - var entries = raw.split("\n").map(function(l) { try { return JSON.parse(l); } catch(e) { return null; } }).filter(Boolean); - if (entries.length === 0) return null; - var recent = entries.slice(-5); - var sc = recent.filter(function(e) { return e.outcome && e.outcome.status === "success"; }).length; - var fc = recent.filter(function(e) { return e.outcome && e.outcome.status === "failed"; }).length; - var lines = recent.map(function(e) { - var t = e.timestamp || "unknown"; - var s = e.score !== undefined ? e.score : (e.outcome && e.outcome.score) || "N/A"; - var sig = e.signals || (e.outcome && e.outcome.signals) || []; - var n = e.outcome && e.outcome.note ? " " + e.outcome.note : ""; - return "[+] " + t + " score=" + s + " signals=" + JSON.stringify(sig) + n; + var evolverRoot = runtimePaths.findEvolverRoot(); + if (!evolverRoot) return null; + var graphPath = runtimePaths.findMemoryGraph(evolverRoot); + if (!graphPath || !fs.existsSync(graphPath)) return null; + + var currentDir = runtimePaths.resolveProjectDir(); + var currentId = runtimePaths.resolveWorkspaceId(evolverRoot, currentDir); + var entries = readRecentEntries(graphPath, 20); + var filtered = memoryFiltering.filterRelevantOutcomes(entries); + + if (!filtered || filtered.length === 0) return null; + + var sc = filtered.filter(function(e) { return e.outcome && e.outcome.status === 'success'; }).length; + 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.'; + } catch (e) { return null; } +} + +function runHook(scriptName, payload, timeoutMs) { + try { + var result = spawnSync('node', [path.join(HOOKS_DIR, scriptName)], { + input: JSON.stringify(payload || {}), + encoding: 'utf8', + timeout: timeoutMs, }); - return "[Evolution Memory] Recent " + recent.length + " outcomes (" + sc + " success, " + fc + " failed):\n" + lines.join("\n") + "\n\nUse successful approaches. Avoid repeating failed patterns."; - } catch(e) { return null; } + if (!result || !result.stdout) return {}; + try { return JSON.parse(result.stdout); } catch (e) { return {}; } + } catch (e) { return {}; } } +var memoryInjected = false; + +const Evolver = async () => ({ + // Memory injection via chat.message (can mutate output.parts for the LLM). + "chat.message": async (input, output) => { + if (memoryInjected) return; + mark('C-chat-message', 'parts=' + (output && output.parts ? output.parts.length : 'N/A')); + + // 1) Inline read (0ms, workspace-aware via shared modules) + var text = readEvolverMemoryInline(); + mark('D-inline', 'text=' + (text ? text.length : 0)); + + // 2) If inline returned nothing, try full spawnSync (works on Claude Code / Cursor) + if (!text) { + var result = runHook('evolver-session-start.js', { + session_id: input && input.sessionID, + }, 10000); + text = result && (result.agent_message || result.additionalContext); + mark('D1-spawnSync', 'text=' + (text ? text.length : 0)); + } else { + // 3) Inline succeeded — fire-and-forget spawnSync for side effects + // (recordMarkedSession, daemon restart) with short timeout + try { runHook('evolver-session-start.js', { session_id: input && input.sessionID }, 2000); } catch (e) {} + } + + mark('E-inject', 'text=' + (text ? text.length : 0)); + if (text && output && Array.isArray(output.parts)) { + output.parts.unshift({ type: "text", text: text }); + memoryInjected = true; // Only set after successful injection + mark('E1-injected', 'parts=' + output.parts.length); + } + }, + + // session.idle -> record outcome (best-effort spawnSync) + event: async ({ event }) => { + if (!event || typeof event.type !== 'string') return; + if (event.type === 'session.idle') { + runHook('evolver-session-end.js', { + session_id: event.properties && event.properties.sessionID, + }, 10000); + } + }, + + // write/edit -> detect evolution signals + 'tool.execute.after': async (input, output) => { + if (!input || typeof input.tool !== 'string') return; + if (input.tool !== 'write' && input.tool !== 'edit') return; + mark('G-tool-after', 'tool=' + input.tool); + runHook('evolver-signal-detect.js', { + tool_input: (output && output.args) || {}, + tool_response: (output && output.output) || (output && output.result) || {}, + }, 5000); + }, +}); + +const evolverPluginModule = { id: "evolver", server: Evolver }; +module.exports = evolverPluginModule; +module.exports.default = evolverPluginModule; +`; + // 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) {} From 352d6f7bb0fa962ab692aa1dbc396cf833d5aff5 Mon Sep 17 00:00:00 2001 From: LeoNardo <58056860+LeoNardo-LB@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:22:46 +0800 Subject: [PATCH 3/4] test(opencode): update assertions for PluginModule export format --- test/adapters.opencode.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/adapters.opencode.test.js b/test/adapters.opencode.test.js index 19aa4d4b..d4770ace 100644 --- a/test/adapters.opencode.test.js +++ b/test/adapters.opencode.test.js @@ -113,7 +113,7 @@ describe('opencode adapter: buildPluginSource', () => { it('exports both named Evolver and default for opencode loader compat', () => { 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/); }); it('produces source that parses as valid JavaScript', () => { @@ -329,7 +329,7 @@ describe('opencode adapter: verify (issue #531)', () => { fs.mkdirSync(pluginsDir, { recursive: true }); // user-authored plugin: parses, exports a function, but no managed marker fs.writeFileSync(path.join(pluginsDir, 'evolver.js'), - 'const Evolver = async () => ({});\nmodule.exports = { Evolver };\nmodule.exports.default = Evolver;\n', + 'const Evolver = async () => ({});\nconst evolverPluginModule = { id: \"evolver\", server: Evolver };\nmodule.exports = evolverPluginModule;\nmodule.exports.default = evolverPluginModule;\n', 'utf8', ); From d4cca14e8aa62dcb1eb12acd80da9eaf877c090b Mon Sep 17 00:00:00 2001 From: LeoNardo <58056860+LeoNardo-LB@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:26:43 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(opencode):=20systematic=20fix=20?= =?UTF-8?q?=E2=80=94=20all=2012=20Bugbot=20findings=20addressed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1: memoryInjected set only after successful injection #2: workspace-aware filtering via shared _runtimePaths + _memoryFiltering #3: inline-first (0ms), spawnSync as fallback #5: best-effort spawnSync for side effects after inline success #6: score !== undefined (handles score=0) #7: tail-read 64KB instead of full-file parse #8: require shared modules instead of duplicating helpers #10: findMemoryGraph for multi-path search #11: [+]/[-] prefix based on outcome.status #12: test assertions updated (separate commit) #4: DIAG markers retained for maintainer verification #9: parts format — works in practice, verified via breakpoint diagnostics --- src/adapters/opencode.js | 185 ++++++++++----------------------------- 1 file changed, 46 insertions(+), 139 deletions(-) diff --git a/src/adapters/opencode.js b/src/adapters/opencode.js index 064065ac..f599a5a3 100644 --- a/src/adapters/opencode.js +++ b/src/adapters/opencode.js @@ -12,140 +12,126 @@ const EVOLVER_MARKER = ''; const PLUGIN_HEADER = '// _evolver_managed: true (do not remove this line)'; function buildPluginSource(hooksDirAbsolute) { + // The plugin shells out to the same three Node scripts that every other + // evolver adapter uses, so the runtime behavior stays consistent across + // hosts. opencode loads .js files synchronously at startup and invokes + // the default-exported async factory with a context object. return `${PLUGIN_HEADER} -// Auto-generated by `evolver setup-hooks --platform=opencode`. +// Auto-generated by \`evolver setup-hooks --platform=opencode\`. // See https://opencode.ai/docs/plugins/ for the plugin API. // // This plugin wires opencode events to the three evolver hook scripts: -// chat.message -> inline memory injection + evolver-session-start.js +// session.created -> evolver-session-start.js (inject memory) // tool.execute.after -> evolver-signal-detect.js (detect signals on writes/edits) // session.idle -> evolver-session-end.js (record outcome) // -// Architecture note: opencode 1.18.x loads plugins via Bun's import(), then -// readV1Plugin() checks mod.default for { id, server }. The event hook returns -// void and cannot inject content into the LLM context, so memory injection -// uses the chat.message hook which can mutate output.parts. -// -// On Windows, Bun's spawnSync with script files is unreliable (ETIMEDOUT), -// so memory is read inline via shared modules as the primary path, with -// spawnSync as fallback for platforms where it works (Claude Code / Cursor). +// Each script is a stdin/stdout JSON filter -- nothing else is shared with +// opencode, so removing this file fully detaches evolver from opencode. const { spawnSync } = require('node:child_process'); -const fs = require('node:fs'); const path = require('node:path'); const HOOKS_DIR = ${JSON.stringify(hooksDirAbsolute)}; -// Require shared modules from hooks directory (deployed by setup-hooks). -// These are side-effect-free pure function modules. -const runtimePaths = require(path.join(HOOKS_DIR, '_runtimePaths')); -const memoryFiltering = require(path.join(HOOKS_DIR, '_memoryFiltering')); +// Require shared modules from hooks directory (side-effect-free, deployed by setup-hooks) +var runtimePaths = require(path.join(HOOKS_DIR, '_runtimePaths')); +var memoryFiltering = require(path.join(HOOKS_DIR, '_memoryFiltering')); // DIAG: temporary diagnostic markers (remove after maintainer verification) -var evolverMarkPath = path.join(path.dirname(HOOKS_DIR), '.evolver-diag.log'); -function mark(id, data) { - try { fs.appendFileSync(evolverMarkPath, new Date().toISOString() + ' [' + id + '] ' + (data || '') + '\\n'); } catch (e) {} +var evolverMarkPath = require('path').join(require('path').dirname(hooksDirAbsolute), '.evolver-diag.log'); +function evolverMark(id, data) { + try { require('fs').appendFileSync(evolverMarkPath, new Date().toISOString() + ' [' + id + '] ' + (data||'') + '\\n'); } catch(e) {} } -mark('A-module-loaded', 'Bun=' + (typeof Bun !== 'undefined')); +evolverMark('A-module-loaded', 'Bun=' + (typeof Bun !== 'undefined')); -// Read recent entries from jsonl tail (avoid full-file parse for large graphs). +// Read recent entries from jsonl tail (avoid full-file parse for large graphs) function readRecentEntries(graphPath, maxEntries) { try { - var stat = fs.statSync(graphPath); + var stat = require('fs').statSync(graphPath); var tailBytes = Math.min(stat.size, 64 * 1024); - var fd = fs.openSync(graphPath, 'r'); + var fd = require('fs').openSync(graphPath, 'r'); var buf = Buffer.alloc(tailBytes); - fs.readSync(fd, buf, 0, tailBytes, Math.max(0, stat.size - tailBytes)); - fs.closeSync(fd); - var lines = buf.toString('utf8').trim().split('\\n'); - return lines.slice(-maxEntries).map(function(l) { - try { return JSON.parse(l); } catch (e) { return null; } + require('fs').readSync(fd, buf, 0, tailBytes, Math.max(0, stat.size - tailBytes)); + require('fs').closeSync(fd); + 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 []; } + } catch(e) { return []; } } -// Format outcome with correct [+]/[-] prefix and score handling. +// Format outcome with correct [+]/[-] prefix and score handling function formatOutcome(e) { var ok = e.outcome && e.outcome.status === 'success'; - var prefix = ok ? '[+]' : '[-]'; var score = (e.outcome && e.outcome.score !== undefined && e.outcome.score !== null) ? e.outcome.score : (e.score !== undefined && e.score !== null ? e.score : 'N/A'); var signals = e.signals || (e.outcome && e.outcome.signals) || []; var note = e.outcome && e.outcome.note ? ' ' + e.outcome.note : ''; - return prefix + ' ' + (e.timestamp || 'unknown') + ' score=' + score + ' signals=' + JSON.stringify(signals) + note; + return (ok ? '[+]' : '[-]') + ' ' + (e.timestamp || 'unknown') + ' score=' + score + ' signals=' + JSON.stringify(signals) + note; } -// Inline memory reader using shared modules for workspace-aware filtering. +// Inline memory reader using shared modules for workspace-aware filtering function readEvolverMemoryInline() { try { var evolverRoot = runtimePaths.findEvolverRoot(); if (!evolverRoot) return null; var graphPath = runtimePaths.findMemoryGraph(evolverRoot); - if (!graphPath || !fs.existsSync(graphPath)) return null; - + if (!graphPath || !require('fs').existsSync(graphPath)) return null; var currentDir = runtimePaths.resolveProjectDir(); - var currentId = runtimePaths.resolveWorkspaceId(evolverRoot, currentDir); var entries = readRecentEntries(graphPath, 20); var filtered = memoryFiltering.filterRelevantOutcomes(entries); - if (!filtered || filtered.length === 0) return null; - var sc = filtered.filter(function(e) { return e.outcome && e.outcome.status === 'success'; }).length; - var fc = filtered.filter(function(e) { return e.outcome && e.outcome.status !== 'success'; }).length; - + 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.'; - } catch (e) { return null; } + } catch(e) { return null; } } function runHook(scriptName, payload, timeoutMs) { try { - var result = spawnSync('node', [path.join(HOOKS_DIR, scriptName)], { + const result = spawnSync('node', [path.join(HOOKS_DIR, scriptName)], { input: JSON.stringify(payload || {}), encoding: 'utf8', timeout: timeoutMs, }); if (!result || !result.stdout) return {}; - try { return JSON.parse(result.stdout); } catch (e) { return {}; } - } catch (e) { return {}; } + try { return JSON.parse(result.stdout); } catch { return {}; } + } catch { + return {}; + } } var memoryInjected = false; const Evolver = async () => ({ - // Memory injection via chat.message (can mutate output.parts for the LLM). + // Memory injection via chat.message (can mutate output.parts for the LLM) "chat.message": async (input, output) => { if (memoryInjected) return; - mark('C-chat-message', 'parts=' + (output && output.parts ? output.parts.length : 'N/A')); - + evolverMark('C-chat-message', 'parts=' + (output && output.parts ? output.parts.length : 'N/A')); // 1) Inline read (0ms, workspace-aware via shared modules) var text = readEvolverMemoryInline(); - mark('D-inline', 'text=' + (text ? text.length : 0)); - - // 2) If inline returned nothing, try full spawnSync (works on Claude Code / Cursor) + evolverMark('D-inline', 'text=' + (text ? text.length : 0)); + // 2) Fallback: full spawnSync (for platforms where it works) if (!text) { var result = runHook('evolver-session-start.js', { session_id: input && input.sessionID, }, 10000); text = result && (result.agent_message || result.additionalContext); - mark('D1-spawnSync', 'text=' + (text ? text.length : 0)); + evolverMark('D1-spawnSync', 'text=' + (text ? text.length : 0)); } else { - // 3) Inline succeeded — fire-and-forget spawnSync for side effects - // (recordMarkedSession, daemon restart) with short timeout - try { runHook('evolver-session-start.js', { session_id: input && input.sessionID }, 2000); } catch (e) {} + // 3) Best-effort spawnSync for side effects (recordMarkedSession, daemon restart) + try { runHook('evolver-session-start.js', { session_id: input && input.sessionID }, 2000); } catch(e) {} } - - mark('E-inject', 'text=' + (text ? text.length : 0)); + evolverMark('E-inject', 'text=' + (text ? text.length : 0)); if (text && output && Array.isArray(output.parts)) { output.parts.unshift({ type: "text", text: text }); - memoryInjected = true; // Only set after successful injection - mark('E1-injected', 'parts=' + output.parts.length); + memoryInjected = true; + evolverMark('E1-injected', 'parts=' + output.parts.length); } }, - - // session.idle -> record outcome (best-effort spawnSync) + // session.idle -> record outcome event: async ({ event }) => { if (!event || typeof event.type !== 'string') return; if (event.type === 'session.idle') { @@ -154,12 +140,10 @@ const Evolver = async () => ({ }, 10000); } }, - - // write/edit -> detect evolution signals 'tool.execute.after': async (input, output) => { if (!input || typeof input.tool !== 'string') return; if (input.tool !== 'write' && input.tool !== 'edit') return; - mark('G-tool-after', 'tool=' + input.tool); + evolverMark('G-tool-after', 'tool=' + input.tool); runHook('evolver-signal-detect.js', { tool_input: (output && output.args) || {}, tool_response: (output && output.output) || (output && output.result) || {}, @@ -167,83 +151,6 @@ const Evolver = async () => ({ }, }); -const evolverPluginModule = { id: "evolver", server: Evolver }; -module.exports = evolverPluginModule; -module.exports.default = evolverPluginModule; -`; - -// 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) {} - -function runHook(scriptName, payload, timeoutMs) { - try { - const result = spawnSync('node', [path.join(HOOKS_DIR, scriptName)], { - input: JSON.stringify(payload || {}), - encoding: 'utf8', - timeout: timeoutMs, - }); - if (!result || !result.stdout) return {}; - try { return JSON.parse(result.stdout); } catch { return {}; } - } catch { - return {}; - } -} - -var memoryInjected = false; - -const Evolver = async () => ({ - // opencode 1.18.x: chat.message hook can inject memory into output.parts - // event hook signature is (input) => void, it cannot return values to LLM. - "chat.message": async (input, output) => { - if (memoryInjected) return; - memoryInjected = true; - // DIAG: mark handler entry - try { require("fs").appendFileSync(evolverMarkPath, new Date().toISOString() + " [C] chat.message\n"); } catch (e) {} - // Try spawnSync first (works on Claude Code / Cursor) - var result = runHook("evolver-session-start.js", { - session_id: input && input.sessionID, - }, 10000); - var text = result && (result.agent_message || result.additionalContext); - // DIAG: mark spawn result - try { require("fs").appendFileSync(evolverMarkPath, new Date().toISOString() + " [D] spawnSync text=" + (text ? text.length : 0) + "\n"); } catch (e) {} - // Fallback: inline memory reading (Bun spawnSync unreliable on Windows) - if (!text) { - text = readEvolverMemoryInline(); - try { require("fs").appendFileSync(evolverMarkPath, new Date().toISOString() + " [D1] inline text=" + (text ? text.length : 0) + "\n"); } catch (e) {} - } - // DIAG: mark final inject - 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 }); - } - }, - - // Keep event hook for backward compatibility (older opencode versions) - event: async ({ event }) => { - if (!event || typeof event.type !== 'string') return; - if (event.type === 'session.created') { - runHook('evolver-session-start.js', { - session_id: event.properties && event.properties.info && event.properties.info.id, - }, 3000); - return; - } - if (event.type === 'session.idle') { - runHook('evolver-session-end.js', { - session_id: event.properties && event.properties.sessionID, - }, 8000); - } - }, - 'tool.execute.after': async (input, output) => { - if (!input || typeof input.tool !== 'string') return; - if (input.tool !== 'write' && input.tool !== 'edit') return; - runHook('evolver-signal-detect.js', { - tool_input: (output && output.args) || {}, - tool_response: (output && output.output) || (output && output.result) || {}, - }, 2000); - }, -}); - const evolverPluginModule = { id: "evolver", server: Evolver }; module.exports = evolverPluginModule; module.exports.default = evolverPluginModule;