Skip to content

fix(codemode): let search be awaited or chained like a Promise - #59981

Merged
rekram1-node merged 2 commits into
devfrom
forgiving-search
Oct 28, 2026
Merged

rekram1-node merged 2 commits into
devfrom
forgiving-search

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Follow-up to #50417

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Problem. In Code Mode, search(...) returns its result directly while every catalog tool returns a Promise. Models often treat search like the others. await search(...) already worked, but search(...).then(...), .catch(...) and .finally(...) threw "is not a function", and DeepSeek spiraled on that. #50417 worked around it with a prompt sentence telling models search is synchronous.

Change. I made the interpreter accept every style, then removed the sentence from the prompt.

call before after
search(q).items works works
await search(q) works works
search(q).then/catch/finally(...) throws "is not a function" works
Promise.all([search(a), search(b)]) works works

How it works: the search result keeps its plain data, but its prototype gets non-enumerable then, catch and finally. They resolve a real Promise with a plain copy of the result, so await stops after one step instead of unwrapping forever.

 search(args)
   run the search tool (sync, counted like before)
-  return result
+  return result with an inherited, hidden then/catch/finally

JSON.stringify, Object.keys, spread, for-in, console.log and returning the result all look the same as before. This lives in @opencode/codemode and only applies to search, not catalog tools.

Errors. Bad input (for example search({ limit: "ten" })) still throws at the call, same as today. So search(q).items and try { await search(q) } catch both see the error, but a chained .catch(...) does not, because the throw happens before .catch runs. Routing errors into .catch would need search to return a rejected Promise on failure, which breaks the sync style (try { search(bad) } catch would stop catching, and property reads would need special cases). Search input is all optional, so this path is rare.

Prompt. I removed "search is synchronous. Call it without await; it does not return a Promise." from packages/core/src/codemode/instructions.ts. searchSignature already shows a plain return type, and nothing else in the prompt says search is synchronous.

Prompt cache. The Code Mode block is re-rendered per request, so existing sessions miss the prompt cache once after deploy. #57627 changes the same prompt. Whichever merges second needs a trivial rebase, and if they ship in the same release the two changes share that one cache miss.

Out of scope. Catalog tool calls and tools.search(...) are unchanged. They still return real Promises.

How did you verify your code works?

  • packages/codemode: bun typecheck passes, bun test 1472 pass, 0 fail. New tests in test/tool-paths.test.ts cover every call style above, check that data views don't show the methods, and check that invalid input still throws. Two older tests used search(...).catch as their example of a missing method, so I switched them to .filter.

  • packages/core: bun typecheck passes. bun test test/codemode: 23 pass.

  • Probe over OpenRouter with the real rendered Code Mode prompt (partial catalog, 60 tools), the real execute description and the real interpreter. 5 tasks x 6 reps per arm, up to 5 executes per trial. Arms: old prompt + old runtime, new prompt + old runtime, new prompt + new runtime.

    model arm pass await search chained .then/.catch failed executes per trial
    deepseek-v4.1-flash old prompt, old runtime 100% 0% 0% 0.03
    deepseek-v4.1-flash new prompt, old runtime 100% 93% 0% 0.00
    deepseek-v4.1-flash new prompt, new runtime 100% 93% 0% 0.00
    gemma-4-31b-it old prompt, old runtime 100% 97% 0% 0.37
    gemma-4-31b-it new prompt, old runtime 93% 93% 0% 0.33
    gemma-4-31b-it new prompt, new runtime 97% 90% 0% 0.37

    Without the sentence DeepSeek goes back to await search(...), which is harmless. No trial chained .then or .catch and no execute failed on search in any arm, so the probe didn't reproduce the original chaining case. The runtime change covers it when it happens. The Gemma misses all hit the execute cap on a two-step task and have nothing to do with search. Gemma's failed executes are guessed tool names before searching, and that rate didn't change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@opencodejr opencodejr 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.

Looks good. I reproduced both failures on the base: search(q).then/.catch threw "is not a function", and tools.search(q).items threw "un-awaited Promise". With this PR, every call style in the table works. Awaiting, structuredClone, passing the result to a tool and returning it from an async function all behave the same as on the base. Invalid input still throws at the call, and tools.search(bad).catch now throws instead of catching, as the description says. packages/codemode tests pass (1469 pass, 0 fail), packages/core Code Mode tests pass, and both packages type-check (tsc).

Public API is unchanged; this only affects Code Mode internals and the prompt text.

One side effect, not blocking: programs can now see the change through Object.getPrototypeOf(result) === Object.prototype (now false) and typeof result.then (now "function"). That seems acceptable for a search result.

@rekram1-node
rekram1-node merged commit 711802c into dev Oct 28, 2026
15 checks passed
@rekram1-node
rekram1-node deleted the forgiving-search branch October 28, 2026 20:31
@amrkmn amrkmn mentioned this pull request Oct 29, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant