Skip to content

fix(core): break filesystem cycle in compiled prompts - #48397

Open
kernel-oops wants to merge 1 commit into
anomalyco:devfrom
kernel-oops:compiled-filesystem-cycle
Open

kernel-oops wants to merge 1 commit into
anomalyco:devfrom
kernel-oops:compiled-filesystem-cycle

Conversation

@kernel-oops

@kernel-oops kernel-oops commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Fixes #48372. Related to #44946; this complements that Bun upgrade rather than duplicating its runtime pin change. The upgrade is useful for fixes such as oven-sh/bun#35356 (GC timer CPU usage), but compiled prompt preparation also needs checking.

Type of change

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

What does this PR do?

Remove the runtime import from filesystem search back to the filesystem service. Schema constructors now come directly from the schema package; service input imports remain type-only.

The service depends on the search node. With Bun 1.4.2, the compiled cycle can capture an undefined dependency: health passes, but the first prompt fails in SystemPrompt.environment with TypeError: undefined is not an object (evaluating 'a.name'), before any provider request.

Add an isolated compiled regression and a path-filtered Linux CI job using Bun 1.4.2. The runner exercises authenticated session creation, a streamed request for the actual read tool, completion, persisted tool output and a second turn. All provider responses are deterministic and local.

How did you verify your code works?

Fresh upstream dev at 193de13a88d62a6409c6d385831180f1def527dc, Linux x64/glibc, Bun 1.4.2, native minified/split builds with embedded web UI:

  • Before: health passes; first prompt fails with the error above; zero provider requests.
  • After: compiled regression passes; exactly three provider requests. The embedded runtime is checked by the runner.
  • Frozen-lockfile installation; no lockfile changes.
  • Core filesystem/location/read tests: 46 pass, also 46 pass with FFF disabled.
  • Isolated processor/permission/config/storage tests: 345 pass, 3 platform skips. An initial run inherited local configuration and failed; the clean-environment rerun passed.
  • Core and OpenCode typechecks, formatting, Python syntax, workflow YAML and inline shell checks pass.

Soak update: the operator reports a few days of normal server and TUI use with excellent CPU usage and no noticed issues. This is the custom 1.18.30-kernel-oops-bun142-promptfix1 build with Bun 1.4.2, now used for normal projects, not pristine upstream or a controlled multi-day test. The clean-upstream regression results above are separate.

Windows runner support, other binary targets, real providers and CPU benchmarks were not tested by this PR's isolated regression. Broader platform coverage and upstream review remain outstanding.

Screenshots / recordings

Not applicable.

Checklist

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

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@vidit19sharma

Copy link
Copy Markdown

Independent confirmation of this fix on macOS arm64, which I think is the platform gap left by the verification above (Linux x64/glibc).

I hit #48372 after a routine brew upgrade to 1.18.30 and debugged it from scratch before finding this PR; I landed on the same root cause and an equivalent patch, so this is a genuinely independent reproduction rather than a re-run of the same steps.

Environment

  • macOS 15 (Darwin 25.6.0), arm64
  • Bun 1.4.2
  • dev at 95daf90

The undefined dependency is directly observable

Rather than driving a full prompt, the bad dependency can be observed at the graph level. Forcing the hazardous evaluation order (search before filesystem) and bundling is enough:

import "./src/filesystem/search"
import { FileSystem } from "./src/filesystem"
const deps = (FileSystem.node as any).dependencies
console.log(deps.map((d: any) => d?.name ?? "<UNDEFINED>"))
bun build ./probe.ts --target=bun --outfile=probe.js && bun run probe.js

Before (unpatched dev):

[ "@opencode/FileSystem", "@opencode/Location", "<UNDEFINED>" ]

With this PR's search.ts change applied:

[ "@opencode/FileSystem", "@opencode/Location", "@opencode/v2/FileSystem/Search" ]

That <UNDEFINED> is what later reaches resolve in layer-node.ts:

{ cache, resolve: (node) => replacementMap.get(node.name) ?? node }

node.name on the undefined entry is the minified a.name in the reported stack.

Note this only manifests bundled. Run from source, the same cycle throws the much clearer ReferenceError: Cannot access 'node' before initialization — which is likely why it escaped pre-release testing.

Also checked, with this PR applied

  • tsc --noEmit -p packages/core — exit 0
  • bun test packages/core/test/filesystem/ — 36 pass, 0 fail

This appears to be the only instance of the pattern

Since the failure class is "a deps: array read at module-eval time references a namespace imported from a cycle partner", I scanned for other occurrences: build the runtime import graph (excluding import type), find modules whose top-level deps: [...] dereferences X.node, and flag those where X is mutually reachable with the module.

Across 669 modules under packages/core/src and packages/opencode/src, that reports exactly one hazard — filesystem.ts → filesystem/search.ts — and zero once this patch is applied. So the targeted fix looks complete rather than one instance of a wider pattern.

Possible lighter-weight regression test

The bundle probe above needs no provider, credentials, network, or session, and runs in about a second. It may be worth considering alongside (or instead of) the full compiled runner, since it asserts the actual invariant — no undefined in a compiled layer graph — and would catch any future reintroduction of this cycle anywhere in the graph, not just this module pair.

A related thought for a separate change: LayerNode.compile could validate dependencies and throw undefined dependency in node <name> instead of dereferencing .name. That would have turned this into a one-line diagnosis rather than a silent Unexpected server error. Happy to open that separately if it would be useful.

Impact

Worth noting this is a full outage on the affected build — every prompt fails before any provider request, and the surfaced error is a generic Unexpected server error, so it reads as an auth or provider problem. Several people in #48372 understandably suspected their credentials first. Pinning to 1.18.29 is an effective workaround in the meantime.

@kernel-oops

Copy link
Copy Markdown
Contributor Author

Thanks @vidit19sharma — this independent reproduction and before/after validation materially close the macOS arm64 validation gap. The direct graph observation is particularly useful: it matches the compiled-dependency invariant this fix protects — every dependency entry must be a defined node. The source/bundled difference also explains the misleading symptom: source execution catches the premature access with a temporal-dead-zone ReferenceError, whereas the bundled evaluation captures an undefined dependency that only fails later at node.name.

The lighter bundle probe is a strong candidate to complement the full compiled runner: it checks the graph invariant directly and cheaply, while the runner retains end-to-end coverage of the compiled prompt/tool path. Thanks also for your scan across 669 modules finding only this hazard and none after the patch; that is useful additional evidence, though I have not independently reproduced the scan.

Agreed that dependency validation in LayerNode.compile would make diagnosis much clearer. Please do open that as a separate follow-up issue/PR rather than expanding the scope here.

bmwiedemann pushed a commit to bmwiedemann/openSUSE that referenced this pull request Sep 14, 2026
https://build.opensuse.org/request/show/1377875
by user pluskalm + anag_factory
opencode 1.18.30: two fixes, supersedes 1377865 (same patch, one more change).
- Every prompt failed with "undefined is not an object (evaluating 'a.name')" (boo#1280159): circular import between core/src/filesystem.ts and filesystem/search.ts, which bun 1.4.2's bundler orders so a layer dependency is undefined. Backport of upstream PR anomalyco/opencode#48397 (search.ts only). Reproduced on the Factory:ARM RPM in a Tumbleweed container; the patched build answers the same prompt.
- NO_BRP_STRIP_DEBUG=true in %install: brp-15-strip-debug runs binutils strip (not %__strip) on any ELF `file` calls "not stripped", which the compiled binary is whenever the bun it was copied from kept its .symtab (any project without the debuginfo flag); that drops the appended payload and %check prints bun's ver
otavio added a commit to otavio/nix-config that referenced this pull request Sep 16, 2026
nixpkgs bumped bun to 1.4.2, and opencode 1.18.30 bundled with it fails
every prompt with "Cannot read properties of undefined (reading 'name')"
from its layer resolver. Building it with bun 1.3.13 restores a working
binary until the upstream fix (anomalyco/opencode#48397) lands.

Assisted-by: Claude Code (claude-opus-5)
@fdoooch

fdoooch commented Sep 20, 2026

Copy link
Copy Markdown

Additional validation on Linux x86_64 / glibc 2.39 with Bun 1.4.1 (4661e494f), including a successful model response through Omnigent 0.14.0.

Source baseline: OpenCode v1.18.31, commit 014614d35b397775e5d397a490fc72368c894ec2. I applied the filesystem/search.ts import/type changes from this PR; no dependency or lockfile changes. This validates that source fix backported onto the tag, rather than the entire PR branch.

Both before/after builds used Bun 1.4.1 and the standard native build with minification, splitting and embedded web UI:

bun install --frozen-lockfile
OPENCODE_VERSION=1.18.31+patched.2 OPENCODE_CHANNEL=latest \
  bun run packages/opencode/script/build.ts --single --skip-install

The unpatched build was labelled 1.18.31+patched.1; the fixed build is 1.18.31+patched.2.

Before/after prompt check

Using isolated working/config/data/cache directories and a deterministic local OpenAI-compatible SSE endpoint:

Build CLI result Requests received by the local provider Assistant response
Before the import fix Exit 1; Unexpected server error 0 None
After the import fix Exit 0 2 ROOTCAUSE_OK

The failing full Omnigent run logged TypeError: undefined is not an object (evaluating 'a.name') in SystemPrompt.environment. The health/version checks and TUI startup passed even in the broken build. Submitting a prompt was necessary to expose the failure. The local provider check covered streamed text completion, not tool execution.

I also reproduced the dependency-graph probe described above with Bun 1.4.1: importing search before filesystem and bundling captured <UNDEFINED> as the third dependency of FileSystem.node. Changing the search module's runtime import to @opencode-ai/schema/filesystem, with the service input imports type-only, changed that entry to @opencode/v2/FileSystem/Search.

Checks after applying the fix

  • From packages/core, bun run test: 1,098 passed, 0 failed, 3,008 assertions across 144 files. Configuration/cache/data directories were isolated for the run.
  • From packages/core, bun run typecheck: passed.
  • Actual omnigent opencode --mini launch through its daemon/runner, opencode serve, and attached terminal: the prompt Reply with exactly OPENCODE_OMNIGENT_OK. Do not use tools or access files. returned OPENCODE_OMNIGENT_OK from the configured model. The server reported 1.18.31+patched.2, and the prompt failure seen before the patch did not recur. The test session was stopped afterward.
  • Three sequential TUI attachments through Omnigent's OpenCodeNativeServer reused the same two native libraries in an isolated temporary directory: 2 → 2 → 2 .so files, with unchanged filenames and sizes. Rebuilding with newer Bun was motivated by accumulation of extracted native libraries, so preserving that behavior while restoring prompt processing was also checked.

This adds Bun 1.4.1 and a real-model Omnigent integration check to the reported validation. It is a bounded smoke/regression check, not a long-running soak test or coverage of other platforms.

ooesili added a commit to ooesili/dotfiles that referenced this pull request Sep 20, 2026
johnnymo87 added a commit to johnnymo87/opencode-patched that referenced this pull request Sep 21, 2026
- Cite the local reproduction of the 1.4.1+ splitting crash rather than
  oven-sh/bun#42837's own matrix, whose PASS criterion is an /api/agent
  200 -- the same smoke test this comment says returns 200 on a broken
  binary. Add anomalyco/opencode#48397, which traces the crash to a
  filesystem import cycle and is where the fix may actually land.
- Say what staying on 1.4.0 costs (the 1.4.1 fetch/dns fixes forgone),
  and why patching build.ts to `splitting: false` to reach 1.4.2 is
  worse than waiting: it puts us on a bundle shape nobody upstream runs.
- Scope the re-bump rule to every platform a deployed host consumes, not
  just linux, and record that darwin has no agent-turn coverage today.
- Narrow the new tripwire to the one test that exercises wrapSSE (the
  five headerTimeout tests return early before that path, and one races
  a 500 ms margin on a cold runner), record that it was proved
  non-vacuous on CI, and say plainly that the aisdk.ts wrapper is
  patched but not tripwired.
- apply.sh: the sunset is 'when upstream already contains #44944', at
  which point dropping the patch is mandatory. The previous wording read
  as an absolute ban while the pin is >= 1.4.0.
johnnymo87 added a commit to johnnymo87/opencode-patched that referenced this pull request Sep 21, 2026
…ibopentui.so leak (#54)

* ci: build releases with bun 1.4.2 to stop the /tmp libopentui.so leak

`bun build --compile` bakes the bun runtime, and therefore its
embedded-file extractor, into the shipped binary, so the bun version this
workflow pins is what every launch of the released binary runs.

@opentui/core's platform package resolves its native library through
`import("./libopentui.so", { with: { type: "file" } })`, which makes the
13.35 MB shared object an embedded file that bun must materialize on disk
before the TUI can dlopen it. bun < 1.4.0 wrote a fresh
`$TMPDIR/.<16 hex>-<8 digit>.so` on every launch and never unlinked it,
not even on clean exit. Measured on a shared box on 2026-09-21: 1626
orphaned copies totalling 19.84 GB, accruing 1.1-2.1 GB/day across ~15
concurrent attach TUIs.

That is oven-sh/bun#40076 (closed 2026-08-22; its report names opencode's
libopentui.so as the real-world case), a duplicate of #29585, fixed by
#29587 in bun 1.4.0: an embedded file now extracts once to a
content-hashed, reused `$TMPDIR/.bun-{uid}-{hash}.{ext}`.

Nothing above bun can fix this. The extraction is not opencode's code and
not @OpenTui's; @opentui/core 0.4.5 is what upstream opencode dev still
pins as of 2026-09-21, and upstream opencode still declares
`packageManager: bun@1.3.14`, so this pin deliberately no longer matches
upstream's. The prior rule ("match upstream's packageManager") is
replaced by the same gate it always implied: a bun bump ships only after
a `dry_run: true` run of this workflow on the bumped branch is green.

Verified locally on aarch64 with the #40076 repro (a 69 KB lib.so
imported `with { type: "file" }` and dlopen'd, compiled and run 3x):
bun 1.3.3 left three files, one per launch, in /tmp and ignored $TMPDIR;
bun 1.4.2 left one reused `$TMPDIR/.bun-1000-3a39a40e08be468e.so`.

Refs: workstation-o5s1.24, workstation-o5s1.27

* fix: pin bun 1.4.0, not 1.4.2, and carry the SSE cancel-rejection backport

Two corrections to the previous commit, both from evidence that the bump
alone would have shipped broken binaries.

1. 1.4.2 -> 1.4.0. bun 1.4.1 rewrote `--splitting`, and
   packages/opencode/script/build.ts compiles with
   `minify: true, splitting: true`. A 1.4.1- or 1.4.2-compiled opencode
   binary crashes on the first agent turn with `TypeError: undefined is
   not an object (evaluating 'node.name')` in
   packages/core/src/effect/layer-node.ts -- reported against stock
   upstream `dev` on darwin-arm64 in anomalyco/opencode#44946 (the
   upstream bun-bump PR, still open for this reason) and tracked as
   oven-sh/bun#42837, where a 1.4.0-compiled binary passes and a 1.4.1
   one fails. 1.4.0 carries the embedded-file fix (#29587) and predates
   the splitting rewrite. Verified locally that a 1.4.0-compiled binary
   reuses one `$TMPDIR/.bun-{uid}-{hash}.so` across launches, same as
   1.4.2.

   That regression is invisible to a `--version` smoke test and to the
   named test steps here, so the comment's "dry_run green is enough"
   rule went with it: a bun bump now also requires executing the dry
   run's linux artifact for one real agent turn.

2. New patches/sse-cancel-rejection.patch, a backport of upstream
   #44944 (merged 2026-09-02, after v1.18.18, so not on the line the
   cron hold keeps us on). Under bun 1.4, `reader.cancel()` on a fetch
   body that was just aborted rejects; both SSE chunk-timeout wrappers
   discard that promise with `void`, which is an unhandled rejection,
   which bun answers by killing the process. chunkTimeout is armed on
   four providers in the deployed config, so one stalled stream would
   take a serve and every session it owns down with it. The two hunks
   are byte-identical to upstream's; upstream's third hunk is a
   test-only flake fix and is deliberately not carried.

   build-release now names the upstream test that goes red without the
   patch under bun 1.4 (test/provider/header-timeout.test.ts, per
   #44943), so neither half of this change can regress unnoticed.

Refs: workstation-o5s1.24, workstation-o5s1.27

* docs: sharpen the bun-pin and tripwire comments after review

- Cite the local reproduction of the 1.4.1+ splitting crash rather than
  oven-sh/bun#42837's own matrix, whose PASS criterion is an /api/agent
  200 -- the same smoke test this comment says returns 200 on a broken
  binary. Add anomalyco/opencode#48397, which traces the crash to a
  filesystem import cycle and is where the fix may actually land.
- Say what staying on 1.4.0 costs (the 1.4.1 fetch/dns fixes forgone),
  and why patching build.ts to `splitting: false` to reach 1.4.2 is
  worse than waiting: it puts us on a bundle shape nobody upstream runs.
- Scope the re-bump rule to every platform a deployed host consumes, not
  just linux, and record that darwin has no agent-turn coverage today.
- Narrow the new tripwire to the one test that exercises wrapSSE (the
  five headerTimeout tests return early before that path, and one races
  a 500 ms margin on a cold runner), record that it was proved
  non-vacuous on CI, and say plainly that the aisdk.ts wrapper is
  patched but not tripwired.
- apply.sh: the sunset is 'when upstream already contains #44944', at
  which point dropping the patch is mandatory. The previous wording read
  as an absolute ban while the pin is >= 1.4.0.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SystemPrompt.environment

3 participants