Skip to content

tracking: OpenCode v1.18.30 — complete native build from real TypeScript (no stripping, no bundle), startup faster than bun #10107

Description

@proggeramlug

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.ts and its complete dependency graph natively: no feature-removal patch, no bun build pre-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)

Blocker tickets (this set)

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

Oracle: 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

  1. --version prints 1.18.30 (with defines) / local (without); --help byte-identical to the oracle.
  2. run "say hi" with a real key round-trips one Anthropic completion; serve answers /global/health.
  3. TUI first frame renders (opentui via bun:ffi, Solid JSX compiled, worker running).
  4. Shell tool (tree-sitter bash), image attach (photon), MCP server with outputSchema, watcher.
  5. Startup: --version user CPU and RSS below the oracle on the quiet Mac mini (see 08).
  6. CI keeps a pinned v1.18.30 corpus fixture reporting compile time, size and smoke status.

Non-blockers recorded for completeness

Landing log

Activity

  1. added
    enhancementNew capability or improvement
    parityCompatibility gap with Node.js, ECMAScript, or the supported ecosystem
    on Sep 12, 2026
  2. proggeramlug commented on Sep 13, 2026

    @proggeramlug
    ContributorAuthor

    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=undefined for #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-x64 v1.18.30 binary, identical clean HOME/XDG dirs, /usr/bin/time.

    rung output oracle wall / user / RSS perry wall / user / RSS
    --version IDENTICAL 0.45 s / 0.41 s / 193 MB 2.47 s / 2.04 s / 610 MB
    --help IDENTICAL 0.32 s / 0.39 s / 205 MB 31.6 s / 31.2 s / 2.39 GB
    run --help IDENTICAL 0.32 s 20.1 s / 19.8 s / 1.76 GB
    models --help, agent --help, mcp --help, providers --help, serve --help IDENTICAL 0.32 s 5.6–8.8 s / 0.75–0.95 GB
    completion IDENTICAL 0.32 s 2.1 s / 618 MB
    models oracle lists 7 models perry: Error: Unexpected error / Cannot read properties of undefined (reading 'Service'), rc=1, no log lines even with --print-logs --log-level DEBUG
    run --model anthropic/claude-sonnet-4-5 … (bogus key) oracle: > build · claude-sonnet-4-5 + Error: API key is invalid. perry: same 'Service' TypeError before any log line

    So every pure-CLI path is byte-identical; the first runtime divergence is one X.Service read on an undefined namespace 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.

    --help under PERRY_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.

  3. proggeramlug commented on Sep 13, 2026

    @proggeramlug
    ContributorAuthor

    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=undefined because it started before the landing.)

    Ladder against the official linux-x64 v1.18.30 binary: --version, --help, run/models/agent/mcp/providers/serve --help, completion all 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, models and serve now fail later, still before the first creating instance log line, with

    Cannot read properties of undefined (reading 'size')
    

    plus three node-style warnings bun never prints:

    (node:PID) Warning: Accessing non-existent property 'ANY' of module exports inside circular dependency        ×2
    (node:PID) Warning: Accessing non-existent property 'getVercelDataDir' of module exports inside circular dependency
    

    (@vercel/oidc auth-config.js↔token-util.js is a real CJS cycle; the ANY reader 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.

  4. proggeramlug commented on Sep 13, 2026

    @proggeramlug
    ContributorAuthor

    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 / serve die before the first log line with Error: Unexpected error / of is not a function. Reduced to #10210: inside a static getter of a class extending a call expression (effect v4 Context.Service<…>()(id)), this.of(x) is not found on the call path although the property read finds it, and this is the holder object rather than the receiver. OpenCode hits it in src/effect/config-service.ts (static get layer() → tag.of(config)) while AppRuntime builds RuntimeFlags.

  5. proggeramlug commented on Sep 13, 2026

    @proggeramlug
    ContributorAuthor

    #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, run with a bogus key, serve, TUI) run next against that binary.

  6. proggeramlug commented on Sep 13, 2026

    @proggeramlug
    ContributorAuthor

    Runtime wall root cause (2026-09-13 evening): #10222. A symbol-keeping relink + gdb backtrace put the of is not a function throw in packages/core/src/fs-util.ts: perry lowers classes declared inside a TypeScript namespace incompletely (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 by AppRuntime for every command, so models, run, serve and 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-hir lower/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.

  7. proggeramlug commented on Sep 13, 2026

    @proggeramlug
    ContributorAuthor

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

  8. proggeramlug commented on Sep 13, 2026

    @proggeramlug
    ContributorAuthor

    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 of wall 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.

  9. proggeramlug commented on Sep 14, 2026

    @proggeramlug
    ContributorAuthor

    #10236 (TUI worker path) → PR #10239, mergeable; the focused OpenCode check now reports Worker entry: src/cli/tui/worker.ts instead of the unresolvable warning. It goes into the next compiler lineage right after the current recompile's rungs.

  10. proggeramlug commented on Sep 14, 2026

    @proggeramlug
    ContributorAuthor

    Runtime wall next is not a function root-caused and fixed: PR #10250. drizzle-orm's applyEffectWrapper copies effect's prototype (with [Symbol.iterator]) onto query classes via Object.assign(Class.prototype, …); perry did not see symbol-keyed properties written to a declared class prototype from instances, so yield* 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.

  11. proggeramlug commented on Sep 15, 2026

    @proggeramlug
    ContributorAuthor

    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 bun export 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 naming isSuperandfilename` — 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. models regressed to failing on the current lineage via #10290, which #10283 uncovered rather than caused.

  12. proggeramlug commented on Sep 15, 2026

    @proggeramlug
    ContributorAuthor

    TUI wall identified and fixed: #10300.

    ./tui.sh against the v8 binary got past the $perryfs/ dlopen wall and died with a bare TypeError: on is not a function. PERRY_DISPATCH_DIAG=1 named the tower but described the receiver only as object/pointer; breaking on report_dispatch_miss in gdb and calling js_console_log_dynamic($xmm0.v2_double[0]) printed it:

    InternalKeyHandler { renderableHandlers: Map(0) {} }
    

    with the frame above it:

    #3 …_opentui_core_chunk_bun_tkm837n2_js__CliRenderer_constructor
    #6 …__createCliRenderer
    #7 perry_closure_opencode_packages_tui_src_app_tsx
    

    i.e. this._keyHandler = new InternalKeyHandler; + this._keyHandler.on("keypress", …) in CliRenderer'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 inline new lowering emits it at the call site — but a cross-module new calls the defining module's standalone <class>_constructor symbol, which never emitted it. For a bare export class D1 extends EventEmitter {} that symbol is literally vmovq; vmovq; ret.

    Affects every base in NativeInstanceBase: EventEmitter, Map, Set, WeakMap, WeakSet, Event, CustomEvent, AsyncLocalStorage, AsyncResource, DOMException, EventEmitterAsyncResource. In this build it hits CliRenderer, EditBuffer, TerminalConsoleCache, BaseRenderable, KeyHandler/InternalKeyHandler and TreeSitterClient — all EventEmitter subclasses declared in one @opentui/core chunk and constructed from another.

    Fix in codegen/method.rs (emit the base init in the synthesized standalone ctor) plus a walk relaxation in native_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 seeded Map/Set is 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 exported class Sized extends Array {} yields length 0 cross-module (bun: 3).

  13. proggeramlug commented on Sep 15, 2026

    @proggeramlug
    ContributorAuthor

    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 createCliRenderer no longer throws. Remaining blocker is #10310, which turns out to block the run --model … rung too:

    • TUI: tui bootstrap failed { error: '(number).delete is not a function' } — opts.headers.delete("Content-Type") in the generated SDK client, where opts.headers is the raw handle number.
    • run --model …: Headers constructor: init is not iterable (received 0x4014000000000000) — the same handle, 5, reaching stdlib's js_headers_init_from_value from 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.

  14. proggeramlug commented on Sep 15, 2026

    @proggeramlug
    ContributorAuthor

    models is 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):

    rung result
    --version, --help, run --help, models --help, agent --help, mcp --help, providers --help, completion IDENTICAL
    models IDENTICAL (newly unblocked)
    run --model … broken by #10310
    TUI renderer constructs; bootstrap blocked by #10310

    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):

    bun perry RSS bun → perry
    --version 0.33 s 1.83 s 195 MB → 572 MB
    --help 0.31 s 3.23 s 208 MB → 564 MB
    models 0.98 s 28.95 s 469 MB → 3741 MB

    The models row is the one worth attacking next for #10106 — 29× and 8× the memory, against 5-10× on the trivial rungs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew capability or improvementparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystem

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions