Repository navigation
tracking: OpenCode v1.18.30 — complete native build from real TypeScript (no stripping, no bundle), startup faster than bun #10107
Copy link
Copy link
Open
Labels
enhancementNew capability or improvementNew capability or improvementparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystemCompatibility gap with Node.js, ECMAScript, or the supported ecosystem
Description
Supersedes the pinned-revision tracker #8506 (fc80874, 2026-08-20). Everything below was measured on 2026-09-12 against anomalyco/opencode tag v1.18.30 (commit 3104c14, 2026-09-09, bun 1.3.14) with perry origin/main dc0d876.
Goal
- Compile
packages/opencode/src/index.tsand its complete dependency graph natively: no feature-removal patch, nobun buildpre-pass, no minified input, no JavaScript engine at runtime. - The CLI, headless
run,serve, the full OpenTUI TUI, providers, MCP, Code Mode, shell tool, image, watcher, plugins (see decision ticket), mDNS and web UI must work. - Startup of the native binary must beat the official bun-compiled binary on the same host (
opencode --version: bun = 0.54 s user CPU, 1.0–1.5 s wall, 185 MB RSS on the loaded dev Mac).
Reference facts (see secret-tests/opencode-1.18.30-inventory/README.md)
- Graph from the entry under the bun condition: 4,063 modules (1,105 first-party, 2,958 from 498 packages), 53 MiB source, 107
.tsx, 9.wasm, 3 Worker entrypoints. - Every eval /
new Functionsite was classified (34 sites, 12 packages): none is on the startup or normal-session path; live-but-cold: ajv (MCP outputSchema validators) and fastify+find-my-way+fast-json-stringify (only insideopencode-gitlab-auth's OAuth callback server). Perry's default-on dyn-eval interpreter covers both. No stripping is needed for eval. - Bun surface already covered by landed perry work: bun:ffi (runtime: complete C-ABI FFI layer #8704), bun:sqlite (feat(sqlite): add bun:sqlite compatibility #8525), N-API
@parcel/watcher(runtime: Node-API (N-API) host for prebuilt napi addons — tracker #8523/runtime: add native @parcel/watcher compatibility facade #8532), Bun globals (bun-compat: Bun globals + "bun" module shim pack (Bun.stringWidth, Bun.file/write, Bun.stdin.text, Bun.hash) #6560/fix(bun): complete OpenCode compatibility coverage #8537), Worker entries (AOT: compile and register Web Worker entry modules used by full-source OpenCode #8509), asset packaging (feat(compile): package generated asset modules #8534), transpileModule for Code Mode (feat(compat): add native TypeScript transpileModule subset #8535), full-graph compile (fix: compile large TypeScript source graphs natively #8383/fix(compile): support full OpenCode source builds #9133, Windows only so far).
Blocker tickets (this set)
- compile: Solid "universal" JSX transform for OpenTUI at compile time (moduleName
@opentui/solid) + solid-js condition swap #10099 — compile-time Solid JSX transform targeting@opentui/solid(+ solid-js export-condition swap) - runtime(bun-compat): provide
plugin/Bun.pluginas an inert registrar so OpenTUI's runtime-plugin support installs #10100 —bunmodule:plugin()/Bun.pluginno-op registrar (TUI startup calls it) - compile: build-time defines (
--define, Bun.build semantics) and the no-define fallbacks OpenCode relies on (import.meta.resolve,new URL(..., import.meta.url)) #10101 — build-time defines (--define) and the no-define fallbacks:import.meta.resolve(incl. Bun 2-arg form),new URL(x, import.meta.url)worker paths - runtime/AOT (refines #8508): Emscripten main+side-module loading (web-tree-sitter + grammars) and wasm-bindgen (photon) for OpenCode v1.18.30 #10102 — WASM: Emscripten main+side modules (tree-sitter + grammars) and wasm-bindgen (photon) — refines runtime/AOT: support OpenCode's WebAssembly modules and packaged .wasm assets #8508 (PR fix(wasm): support Emscripten main and side modules #10138 open)
- AOT/runtime: verify the three Worker entrypoints of OpenCode v1.18.30 end-to-end (TUI server worker, OpenTUI parser worker, thread-stream) #10103 — Worker entrypoints of v1.18.30 verified end-to-end (TUI worker, opentui parser worker) — closes the unticked AOT: compile and register Web Worker entry modules used by full-source OpenCode #8509 boxes
- runtime: dynamic
import(path, { with: { type: "toml" } })of a runtime file path must parse TOML like Bun (OpenCode legacy config migration) #10104 — runtime dynamicimport()with{ with: { type: "toml" } }(legacy config migration) - DECISION: runtime-loaded JavaScript in the native OpenCode build — plugins, custom tools, non-bundled providers, TUI plugins #10105 — DECISION: runtime-loaded plugins / custom tools / non-bundled providers (the AOT boundary)
- perf: OpenCode native binary startup must beat the bun binary — attribute and remove the module-init cost (measured 13× bun CPU on the Aug 21 build) #10106 — startup performance: --help = string-width/RegExp construction (perf(regexp): constructing a RegExp recompiles the pattern every time (14 KB emoji-regex: 631 µs vs 2 µs in bun, 274×) — string-width makes
opencode --helpcost 31 s #10179 → PR perf(regex): cache compiled programs and speed up test/replace #10193); --version = eager init of 7,897 modules (bun tree-shakes to 4,063) → compile: prune modules reachable only through unused re-exports of side-effect-free packages (Bun's graph: 4,063 modules; perry compiles and initializes 7,897 for the same OpenCode entry) #10180 - compile: a multi-MB JSON object literal supplied via --define (OpenCode's models.dev snapshot) never finishes codegen — lower large JSON defines through JSON.parse like #8418 #10151 — large JSON define (models.dev snapshot) stalls codegen — PR fix(hir): keep large JSON defines as serialized data #10161 landed via train Merge train 176: #10161 #10172 (v0.5.1547); follow-up cliff for mid-size record literals: compile: mid-size record literals (~10k–24k nodes) take minutes and GBs on the ordinary lowering path (records-3200: 342 s / 1.9 GB) — the cliff just below #10161's compact cutoff #10173
- codegen: split-unit skeleton references a string constant's .bytes that another unit owns (
use of undefined value '@…_.str.67.bytes') — @opentui/core chunk-node-q0cwyvm9.js, the one failing module in the OpenCode v1.18.30 graph #10152 — split-unit codegen bug in @opentui/core chunk-node (only failing module) — PR fix(codegen): resolve orphan dispatch bytes in split units #10154 (lane, astra) open; verified on the full graph by the 2026-09-13 perrymaster compile - compile(cjs): named import of a Babel-style getter re-export (
Object.defineProperty(exports, "transformAsync", { get })in @babel/core) binds to a non-existentperry_fn_symbol — the one unresolved link symbol in the OpenCode v1.18.30 graph #10153 — CJS getter re-export (@babel/core transformAsync) binds to a missing perry_fn_ symbol; the only unresolved symbol at link — PR fix(cjs): preserve live getter re-export bindings #10156 (lane, astra) open - modules:
export * as X from "./self"is undefined on a dynamic import() namespace (key present, value undefined) — blocks every OpenCode command at the effect-cmd bootstrap #10160 — dynamic import() namespace:export * as X from "./self"value undefined (blocks run/models/serve/TUI bootstrap) — PR fix(codegen): publish self namespace re-exports as live accessors (#10160) #10162 (fix verified on the 2-module repro; full-graph run pending) - compile(cjs): esbuild
__exportgetter helper inside a require cycle is not seen as defining the exports — perry prints node's 'Accessing non-existent property … inside circular dependency' warning (@vercel/oidc via @ai-sdk/gateway; semver range↔comparator) where node and bun are silent #10178 — esbuild __export getters in a CJS cycle: perry-only circular-dependency warnings (@vercel/oidc, semver) at every command start - compile(call):
ns.member(...nonLiteralArray)— a namespace-import member call with a spread argument evaluates to the function instead of calling it; breaks effect's Layer.mergeAll and every OpenCode command #10197 — namespace-member call with spread args is not called (effect Layer.mergeAll → the 'size' bootstrap wall) — PR fix(codegen): route const rest exports through the closure spread call (#10197) #10199 (one-condition fix + regression test; full-graph run pending) - runtime: complete PTY and child-process behavior for full OpenCode shell, LSP, and formatter paths #8512 — PTY / child-process depth for shell tool, LSP, formatters (existing, still open; PR fix(runtime): add ConPTY and keep PTY/process input responsive #10132 landed via train merge train: land #10115, #10125-#10135 (twelve PRs) #10139, issue kept open)
Harness recipe (macOS, this repo)
cd secret-tests/vendor/repos/opencode-v1.18.30 # clean tag checkout, no patches
bun install --frozen-lockfile --ignore-scripts # tree-sitter-powershell's node-gyp script fails; its .wasm is what opencode uses
cd packages/opencode
perry compile src/index.ts --output ../../../../../opencode-1.18.30-native \
--cache-dir ../../../../../opencode-1.18.30-cache # conditions must be ["bun","node"] like script/build.tsOracle: gh release download v1.18.30 -R anomalyco/opencode -p 'opencode-darwin-arm64.zip' (the official bun binary; homebrew ships 1.17.7).
Acceptance ladder
--versionprints1.18.30(with defines) /local(without);--helpbyte-identical to the oracle.run "say hi"with a real key round-trips one Anthropic completion;serveanswers/global/health.- TUI first frame renders (opentui via bun:ffi, Solid JSX compiled, worker running).
- Shell tool (tree-sitter bash), image attach (photon), MCP server with outputSchema, watcher.
- Startup:
--versionuser CPU and RSS below the oracle on the quiet Mac mini (see 08). - CI keeps a pinned v1.18.30 corpus fixture reporting compile time, size and smoke status.
Non-blockers recorded for completeness
@ai-sdk/provider-utilssniffsFunction.prototype.toString.call(fetch)for "internal/deps/undici" — falls back toglobalThis.fetchunder perry, benign.- fastify checks
fn.constructor.name === 'AsyncFunction'(GitLab login path only) — needs node-shaped function names (Function .name and Function.prototype.toString leak compiler internals (Made$4,__anon_class_8,[native code]) #9413 landed). - Compile-cost only:
typescript@5.8.2(9 MiB, Code Mode),prettier(generate cmd),@babel/*(only because of the runtime Solid plugin — becomes dead once 01/02 land), threeentitiesversions with generated decode tables, js-yaml 3 + esprima. - Nested duplicate versions (zod 4.1.8/4.4.3, semver ×2, @ai-sdk/provider-utils ×3) rely on compiler: preserve resolved package-instance identity in the full OpenCode dependency graph #8516 instance identity.
Landing log
- 2026-09-12: merge train merge train: land #10115, #10125-#10135 (twelve PRs) #10139 landed fix(runtime): load data files through dynamic import attributes #10128 (TOML/data imports), feat: support build-time defines and import.meta.resolve for OpenCode #10129 (defines + import.meta.resolve), fix(bun): support inert runtime plugin registration #10130 (bun plugin), fix(runtime): explain native dynamic import limitations #10131 (dynamic-import limitation message), fix(runtime): add ConPTY and keep PTY/process input responsive #10132 (PTY/ConPTY), fix(worker_threads): clone Uint8Array payloads across workers #10133 (worker Uint8Array clone), fix(compile): support OpenTUI Solid universal JSX #10135 (Solid universal JSX). Open: fix(wasm): support Emscripten main and side modules #10138 (WASM), perf: OpenCode native binary startup must beat the bun binary — attribute and remove the module-init cost (measured 13× bun CPU on the Aug 21 build) #10106 (startup, no PR), runtime: complete PTY and child-process behavior for full OpenCode shell, LSP, and formatter paths #8512 (PTY follow-ups).
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew capability or improvementNew capability or improvementparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystemCompatibility gap with Node.js, ECMAScript, or the supported ecosystem
Activity
proggeramlug commentedon Sep 13, 2026
2026-09-13 first full native build + ladder (perrymaster, linux-x64, perry origin/main 5603d63 + PR #10154's codegen patch, interim alias for #10153,
--define OPENCODE_MODELS_DEV=undefinedfor #10151)Compile: 7,813 modules, 0 JavaScript fallback, COMPILE_RC=0, 1.27 GB binary (~80 min at 6 module jobs × 2 LLVM workers; perry's default 2×2 is far too conservative for this graph).
Oracle = official
opencode-linux-x64v1.18.30 binary, identical cleanHOME/XDG dirs,/usr/bin/time.--version--helprun --helpmodels --help,agent --help,mcp --help,providers --help,serve --helpcompletionmodelsError: Unexpected error / Cannot read properties of undefined (reading 'Service'), rc=1, no log lines even with--print-logs --log-level DEBUGrun --model anthropic/claude-sonnet-4-5 …(bogus key)> build · claude-sonnet-4-5+Error: API key is invalid.'Service'TypeError before any log lineSo every pure-CLI path is byte-identical; the first runtime divergence is one
X.Serviceread on anundefinednamespace in the shared instance bootstrap (effect-cmd.ts→import("@/effect/app-runtime")), which blocks every real command. A probe importing app-runtime's 54 namespace imports is compiling to name the culprit; a debug-symbols build follows for the startup profile.--helpunderPERRY_GC_DIAG=1:arena_live=1,576,988,816, 89 incremental cycles / 93,750 steps,[gc-time] wall_us=30,478,027 step_us=14,691,589 share_permille=527— half of the 30 s is GC over a 1.6 GB live heap that bun never builds (bun peaks at 205 MB RSS). Startup work for #10106 starts from there once symbols exist.proggeramlug commentedon Sep 13, 2026
2026-09-13 second full build: all three new fixes verified on the real graph
perry = origin/main 5603d63 + PR #10154 (#10152) + PR #10156 (#10153) + PR #10162 (#10160), pinned copy; no alias, no stubs: 7,897 modules native, 0 JS,
COMPILE_RC=0, 1.30 GB. (#10161 for #10151 landed meanwhile via train #10172; this build still used--define OPENCODE_MODELS_DEV=undefinedbecause it started before the landing.)Ladder against the official linux-x64 v1.18.30 binary:
--version,--help,run/models/agent/mcp/providers/serve --help,completionall byte-identical (perry CPU 2.2 s … 27 s vs 0.4 s, unchanged picture, see #10106).Bootstrap progress: the
'Service'crash (#10160) is gone;run,modelsandservenow fail later, still before the firstcreating instancelog line, withplus three node-style warnings bun never prints:
(
@vercel/oidcauth-config.js↔token-util.jsis a real CJS cycle; theANYreader is being traced.) A debug-symbols build (PERRY_DEBUG_SYMBOLS=1) is compiling so the'size'site can be taken from a gdb backtrace instead of another 70-minute probe compile.proggeramlug commentedon Sep 13, 2026
2026-09-13 ladder 4 (compiler = main 5603d63 + #10154 + #10156 + #10162 + PR #10199): the full v1.18.30 graph still compiles natively (7,897 modules, 0 JS fallback, no aliases/stubs); all eight CLI rungs (
--version,--help, every<cmd> --help,completion) are byte-identical to the official bun binary.Next wall (after #10160
'Service'and #10197'size'):models/run/servedie before the first log line withError: Unexpected error / of is not a function. Reduced to #10210: inside a static getter of a class extending a call expression (effect v4Context.Service<…>()(id)),this.of(x)is not found on the call path although the property read finds it, andthisis the holder object rather than the receiver. OpenCode hits it insrc/effect/config-service.ts(static get layer()→tag.of(config)) whileAppRuntimebuildsRuntimeFlags.proggeramlug commentedon Sep 13, 2026
#10210 fixed by PR #10213 (verified: probe matrix + new regression test + sibling class tests). The compiler is re-pinned with it and the full v1.18.30 graph is recompiling; the runtime rungs (
models,runwith a bogus key,serve, TUI) run next against that binary.proggeramlug commentedon Sep 13, 2026
Runtime wall root cause (2026-09-13 evening): #10222. A symbol-keeping relink + gdb backtrace put the
of is not a functionthrow inpackages/core/src/fs-util.ts: perry lowers classes declared inside a TypeScriptnamespaceincompletely (class not published on the namespace object, static field initializers never run, call-expression heritage never registered).namespace FSUtil { export class Service extends Context.Service<…>()("@opencode/FileSystem") {} }is built byAppRuntimefor every command, somodels,run,serveand the TUI all die there. Three OpenCode files, six classes (core/src/fs-util.ts,core/src/util/effect-flock.ts,core/src/ripgrep/binary.ts). An 8-line repro is on #10222; a codex lane is on the fix (perry-hirlower/module_decl/namespace.rs).#10210 (PR #10213) was real but not this wall; with it and #10199 folded into the pinned compiler every CLI rung stays byte-identical to bun.
proggeramlug commentedon Sep 13, 2026
#10222 → PR #10231. The compiler is being re-based on current main (0.5.1561) plus the four open fix PRs (#10156, #10199, #10213, #10231), then the full v1.18.30 graph recompiles without the models.dev define workaround (#10161 is on main) and the runtime rungs run again.
proggeramlug commentedon Sep 13, 2026
Update: with main 0.5.1561 + #10156 + #10199 + #10213 + #10231 the binary is 837 MB (was 1237), all eight CLI rungs stay byte-identical, and the
ofwall is gone. The bootstrap then hit a second namespace gap (exported namespace functions used as values,next is not a function) — fixed as a follow-up commit on #10231. The graph is recompiling with that compiler now; the TUI's next wall is the worker path helper (#10236, lane running); module pruning for startup (#10180) has a lane running.proggeramlug commentedon Sep 14, 2026
#10236 (TUI worker path) → PR #10239, mergeable; the focused OpenCode check now reports
Worker entry: src/cli/tui/worker.tsinstead of the unresolvable warning. It goes into the next compiler lineage right after the current recompile's rungs.fix(hir): lower classes declared inside TypeScript namespaces complet…
proggeramlug commentedon Sep 14, 2026
Runtime wall
next is not a functionroot-caused and fixed: PR #10250. drizzle-orm'sapplyEffectWrappercopies effect's prototype (with[Symbol.iterator]) onto query classes viaObject.assign(Class.prototype, …); perry did not see symbol-keyed properties written to a declared class prototype from instances, soyield* db.run(…)in core's Database layer threw for every command. Found via per-layer bisect → Database → a probe inside packages/core (build OK, first query fails, bun passes) → pure repro. Open fix PRs, all ready + mergeable: #10156, #10193, #10239, #10245, #10250. Full graph recompiling now with main 0.5.1564 + those five.fix(fetch): accept a Proxy as a Headers record init
fix(compile): initialize the cycle partner of a dynamically imported …
proggeramlug commentedon Sep 15, 2026
Status update on the terminal-interface blocker chain. Five compiler defects found and fixed so far, each revealing the next: #10278/PR #10279 (deferred cycle partner), #10280 (Proxy array and request headers), #10178/PR #10282 (esbuild export getters in a CJS cycle), #10281/PR #10283 (the
bunexport condition was ignored), #10286/PR #10288 (import alias claimed by another module's export name).The terminal interface now gets past all five and fails later with
Class extends value is not a constructor", alongside new circular-dependency warnings namingisSuperandfilename` — Babel's CommonJS cycles, reached through the Solid runtime transform. Same family as #10178, different shape.Command line: nine rungs byte-identical to the official binary.
modelsregressed to failing on the current lineage via #10290, which #10283 uncovered rather than caused.fix(resolve): rank the bun export condition above node for --platform…
fix(compile): key imported_vars by the local name, not the origin exp…
proggeramlug commentedon Sep 15, 2026
TUI wall identified and fixed: #10300.
./tui.shagainst the v8 binary got past the$perryfs/dlopen wall and died with a bareTypeError: on is not a function.PERRY_DISPATCH_DIAG=1named the tower but described the receiver only asobject/pointer; breaking onreport_dispatch_missin gdb and callingjs_console_log_dynamic($xmm0.v2_double[0])printed it:with the frame above it:
i.e.
this._keyHandler = new InternalKeyHandler;+this._keyHandler.on("keypress", …)inCliRenderer's constructor.Root cause is general and has nothing to do with opentui: a class with no own constructor whose chain reaches a native instance-stamped base loses that base when constructed from another module. The install rides on
super(); a ctor-less class writes none, so the inlinenewlowering emits it at the call site — but a cross-modulenewcalls the defining module's standalone<class>_constructorsymbol, which never emitted it. For a bareexport class D1 extends EventEmitter {}that symbol is literallyvmovq; vmovq; ret.Affects every base in
NativeInstanceBase:EventEmitter,Map,Set,WeakMap,WeakSet,Event,CustomEvent,AsyncLocalStorage,AsyncResource,DOMException,EventEmitterAsyncResource. In this build it hitsCliRenderer,EditBuffer,TerminalConsoleCache,BaseRenderable,KeyHandler/InternalKeyHandlerandTreeSitterClient— allEventEmittersubclasses declared in one@opentui/corechunk and constructed from another.Fix in
codegen/method.rs(emit the base init in the synthesized standalone ctor) plus a walk relaxation innative_instance_base_in_chain. A 15-cell probe covering all the bases, two-level chains, an ancestor that owns its ctor (no double-init) and seededMap/Setis now byte-identical to bun.Running rung ladder + TUI against a full rebuild now.
Adjacent, filed as part of #10300 but deliberately NOT fixed there:
new Sized(3)on an exportedclass Sized extends Array {}yieldslength 0cross-module (bun: 3).fix(compile): support `class X extends LRUCache`
proggeramlug commentedon Sep 15, 2026
TUI status after the five fixes landed in a full build (v10): the renderer now constructs and the terminal setup sequences match the oracle — exit status 0 where it was 256, and
createCliRendererno longer throws. Remaining blocker is #10310, which turns out to block therun --model …rung too:tui bootstrap failed { error: '(number).delete is not a function' }—opts.headers.delete("Content-Type")in the generated SDK client, whereopts.headersis the raw handle number.run --model …:Headers constructor: init is not iterable (received 0x4014000000000000)— the same handle, 5, reaching stdlib'sjs_headers_init_from_valuefrom the ext's constructor.Eight of the nine CLI rungs remain byte-identical to the official binary (
--version,--help,run --help,models --help,agent --help,mcp --help,providers --help,completion); the ninth is the one #10310 broke.proggeramlug commentedon Sep 15, 2026
modelsis now byte-identical to the official binary (227 B stdout, rc=0 both) — it was blocked on #10290 and the handle-gate fix cleared it.Rung ladder against the v10 build (five fixes: #10300, #10302, #10290, #10303, #10293):
--version,--help,run --help,models --help,agent --help,mcp --help,providers --help,completionmodelsrun --model …9 of 10 rungs byte-identical. #10310 is the only thing standing between here and the TUI, and it is also the only thing breaking the tenth rung.
Timings on the Linux build host (warm, single run — not a benchmark host, so treat as shape not measurement):
--version--helpmodelsThe
modelsrow is the one worth attacking next for #10106 — 29× and 8× the memory, against 5-10× on the trivial rungs.fix(ffi): dlopen an embedded asset library by materializing it
fix(stdlib): let bun:sqlite query/run/transaction reach the Database …
fix(hir): keep the reified receiver for member reads through `global`
fix(ext-fetch): own the Headers surface this crate already half-imple…
fix(compile): support `class X extends LRUCache`
fix(runtime): run a getter when dispatching `x.g(args)` (#10893)
fix(runtime): run a getter when dispatching `x.g(args)` (#10893)
fix(runtime): run a getter when dispatching `x.g(args)` (#10893)
fix(runtime): run a getter when dispatching `x.g(args)` (#10893)