You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Code Mode execute() receives a function expression (arrow or named) as the code argument — e.g. async () => { await tools.engram.mem_list_projects(); return "done" } — and always returns null. No errors are logged; the sandboxed runtime never invokes the function.
The runtime expects a function body (top-level statements with an explicit return). When the final expression is a function object itself, the JS engine returns that object; copyOut(undefinedAsNull) converts it to null. The body form works in every session tested.
Evidence:
177 sessions scanned (opencode.db): 816 execute calls, 71 produced null.
100% of null outputs share the same code shape: a top-level function expression (async () => ..., async (tools) => ..., or IIFE).
Zero nulls from body-form code across all sessions (816 total calls).
In-session surgical proof (seq 36–1347 body = all pass; seq 1504–1551 function expression = all null).
Live proof: async () => { return 42 } → null; const f = async () => 42; return f → null; (async () => 42)() (invoked IIFE) → 42.
Steps to reproduce
Start an opencode session.
In a Code Mode block, pass a function expression to execute:
The workaround (documented in AGENTS.md): always use body form with explicit return; to run a function, invoke it: (async () => { ... })().
This is a silent failure: no error, no warning, no log entry — the agent receives null and may think the call succeeded, while TUI confirms that tool failed.
Thanks for the detailed write-up and the numbers from your sessions!
As far as I can tell, Code Mode is doing what it was built to do here. execute treats code as a program body, and the tool description says: "Prefer an explicit return; if omitted, the final top-level expression becomes the result." With async () => { return 42 }, the final expression is the function object itself. It is never called, because nothing in the code calls it. The result is then converted the way JSON.stringify would convert it, which drops functions, so you get null. That's also why (async () => 42)() works: there the function is actually called.
I agree that a silent null is easy to misread, especially for a model that wraps its code in an arrow function. So I'm treating this as a request to improve that: for example, a clear warning or error when the result is a function (something like "did you mean to call it?"), or calling a function result automatically. Maintainers will consider it, but I can't promise anything.
Until then, your workaround is the right one: write the code as a body with an explicit return, and if you do wrap it in a function, call it, e.g. return await (async () => { ... })().
I traced this on current dev (552f8671): the final expression is passed through toBoundary, which drops function values, and execution then normalizes undefined to null. The current path is the Acorn interpreter, rather than copyOut.
I propose reporting a clear diagnostic only when the final program result is a function, with a hint such as “The program returned a function. Invoke it to run its body, or return a JSON value.” This would preserve ordinary undefined → null and nested JSON serialization behavior. Automatically calling the function could introduce unexpected tool calls or other side effects. This is separate from the parser-error enrichment discussed in #50577.
Would maintainers approve that bounded design? I can implement it with coverage for arrow/named function results, body-form returns, and explicitly invoked functions.
Activity
opencode-agent commentedon Oct 20, 2026
Thanks for the detailed write-up and the numbers from your sessions!
As far as I can tell, Code Mode is doing what it was built to do here.
executetreatscodeas a program body, and the tool description says: "Prefer an explicitreturn; if omitted, the final top-level expression becomes the result." Withasync () => { return 42 }, the final expression is the function object itself. It is never called, because nothing in the code calls it. The result is then converted the wayJSON.stringifywould convert it, which drops functions, so you getnull. That's also why(async () => 42)()works: there the function is actually called.I agree that a silent
nullis easy to misread, especially for a model that wraps its code in an arrow function. So I'm treating this as a request to improve that: for example, a clear warning or error when the result is a function (something like "did you mean to call it?"), or calling a function result automatically. Maintainers will consider it, but I can't promise anything.Related issues:
nullsymptom, but caused by a missingreturnUntil then, your workaround is the right one: write the code as a body with an explicit
return, and if you do wrap it in a function, call it, e.g.return await (async () => { ... })().Dante-dan commentedon Oct 20, 2026
I traced this on current
dev(552f8671): the final expression is passed throughtoBoundary, which drops function values, and execution then normalizesundefinedtonull. The current path is the Acorn interpreter, rather thancopyOut.I propose reporting a clear diagnostic only when the final program result is a function, with a hint such as “The program returned a function. Invoke it to run its body, or return a JSON value.” This would preserve ordinary
undefined→nulland nested JSON serialization behavior. Automatically calling the function could introduce unexpected tool calls or other side effects. This is separate from the parser-error enrichment discussed in #50577.Would maintainers approve that bounded design? I can implement it with coverage for arrow/named function results, body-form returns, and explicitly invoked functions.