Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 91 additions & 11 deletions src/adapters/opencode.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,62 @@ const path = require('node:path');

const HOOKS_DIR = ${JSON.stringify(hooksDirAbsolute)};

// 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 = 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.

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.


// Read recent entries from jsonl tail (avoid full-file parse for large graphs)
function readRecentEntries(graphPath, maxEntries) {
try {
var stat = require('fs').statSync(graphPath);
var tailBytes = Math.min(stat.size, 64 * 1024);
var fd = require('fs').openSync(graphPath, 'r');
var buf = Buffer.alloc(tailBytes);
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 []; }
}

// Format outcome with correct [+]/[-] prefix and score handling
function formatOutcome(e) {
var ok = e.outcome && e.outcome.status === 'success';
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 (ok ? '[+]' : '[-]') + ' ' + (e.timestamp || 'unknown') + ' score=' + score + ' signals=' + JSON.stringify(signals) + note;
}

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

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.

if (!graphPath || !require('fs').existsSync(graphPath)) return null;
var currentDir = runtimePaths.resolveProjectDir();
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.';

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.

} catch(e) { return null; }
}

function runHook(scriptName, payload, timeoutMs) {
try {
const result = spawnSync('node', [path.join(HOOKS_DIR, scriptName)], {
Expand All @@ -47,33 +103,57 @@ function runHook(scriptName, payload, timeoutMs) {
}
}

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;
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();
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);
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.

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

}
evolverMark('E-inject', 'text=' + (text ? text.length : 0));
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.

}
},
// session.idle -> record outcome
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);
}, 10000);
}
},
'tool.execute.after': async (input, output) => {
if (!input || typeof input.tool !== 'string') return;
if (input.tool !== 'write' && input.tool !== 'edit') return;
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) || {},
}, 2000);
}, 5000);
},
});

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

Expand Down Expand Up @@ -220,7 +300,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) {
Expand Down
4 changes: 2 additions & 2 deletions test/adapters.opencode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);

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.

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.

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.

});

it('produces source that parses as valid JavaScript', () => {
Expand Down Expand Up @@ -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',
);

Expand Down