Skip to content

fix: resolve symlinks in Instance cache to prevent duplicate contexts#16651

Open
jmylchreest wants to merge 2 commits intoanomalyco:devfrom
jmylchreest:fix/tui-symlink-instance
Open

fix: resolve symlinks in Instance cache to prevent duplicate contexts#16651
jmylchreest wants to merge 2 commits intoanomalyco:devfrom
jmylchreest:fix/tui-symlink-instance

Conversation

@jmylchreest
Copy link

@jmylchreest jmylchreest commented Mar 8, 2026

Issue for this PR

Fixes #16647
Fixes #15482
Fixes #16659
Related: #16522, #16641

Type of change

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

What does this PR do?

Filesystem.resolve() used path.resolve() which normalizes ./.. segments but does not resolve symlinks. When opencode runs from a symlinked directory, Instance.provide() creates duplicate contexts for the same physical directory because two different path strings produce two separate cache entries.

Because the Bus pub/sub system is Instance-scoped (via Instance.state()), events published on one Instance (where the LLM session runs) are never received by subscribers on the other Instance (where the SSE endpoint listens). This causes a completely blank TUI after sending prompts.

Fix: Move symlink resolution (realpathSync) into Filesystem.resolve() so all callers benefit — not just Instance.provide() and Instance.reload(). Falls back to the unresolved path only for ENOENT (path doesn't exist yet); all other errors (EACCES, ELOOP, ENOTDIR) are rethrown to avoid silently masking broken paths.

This is complementary to #16641 which canonicalizes at the caller (thread.ts) level.

How did you verify your code works?

  • Reproduced the bug by running opencode from ~/src-office (a symlink to ~/dtkr4-cnjjf/...)
  • Confirmed blank TUI after sending prompt (events never bridged between instances)
  • Applied fix, rebuilt, and verified prompts and responses work correctly
  • Added tests for symlink resolution, non-existent path fallback, ELOOP, EACCES, and ENOTDIR
  • bun test test/util/filesystem.test.ts — 55 tests pass (including 5 new)
  • bun tsc --noEmit — no type errors

Checklist

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

@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

The following comment was made by an LLM, it may be inaccurate:

I found one potentially related PR:

PR #15483: "fix: symlink path resolution causing duplicate instances (#15482)"

Note: The current PR (#16651) explicitly mentions this is complementary to #16641 and references issues #16647 and #15482. PR #15483 appears to be a prior attempt at fixing the same symlink/duplicate instance problem. You may want to review if #15483 was merged or closed, and whether the current PR represents an improved or different solution.

@jmylchreest jmylchreest force-pushed the fix/tui-symlink-instance branch 2 times, most recently from 3684976 to f4f02cc Compare March 8, 2026 23:48
Copy link

@brndnblck brndnblck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review migrated from #16650 — the slimmed-down scope here is much appreciated. Comments on the instance.ts changes still apply.

@brndnblck
Copy link

Thank you for jumping on this @jmylchreest

@jmylchreest jmylchreest force-pushed the fix/tui-symlink-instance branch from f4f02cc to 790f4c6 Compare March 8, 2026 23:52
@jmylchreest
Copy link
Author

Updated to move this logic into resolve(), it's working from what I can see in all my local tests, plus the test condition I think covers it. I've not tested under WSL, I can't, nor MacOS directly - but I trust they're OK.

…stance contexts

Filesystem.resolve() used path.resolve() which normalizes path segments
but does not resolve symlinks. When opencode runs from a symlinked
directory, Instance.provide() could create duplicate contexts for the
same physical directory, causing Bus event isolation and a blank TUI.

Move symlink resolution (realpathSync) into Filesystem.resolve() so all
callers benefit. Falls back to the unresolved path if the target does
not exist, matching the guard pattern in normalizePath().

Fixes anomalyco#16647
Fixes anomalyco#15482
@jmylchreest jmylchreest force-pushed the fix/tui-symlink-instance branch from 790f4c6 to 3903347 Compare March 9, 2026 00:05
@brndnblck
Copy link

I'm validating on MacOS right now and I'll post here in a moment with results.

@brndnblck
Copy link

Confirming this resolves the issue on MacOS and the full test suite passes 🎉

@brndnblck
Copy link

I added #16659 and #16660 related to this PR (however, pre-existing) but I don't know if we want to try and address them in the same push or keep this fix localized since its impacting all MacOS users using symlinks.

The improvement in #16661 can definitely be shipped separately.

@jmylchreest
Copy link
Author

#16659 I think makes sense to add, I'll push an update to the branch to test now. I personally think 16660 is a separate issue.

The bare catch swallowed all realpathSync errors (EACCES, ELOOP,
ENOTDIR), silently falling back to the unresolved path. This could
re-introduce duplicate Instance contexts for broken symlinks.

Now only ENOENT is caught (path doesn't exist yet); all other errors
propagate to the caller.

Fixes anomalyco#16659
jmylchreest added a commit to jmylchreest/opencode that referenced this pull request Mar 9, 2026
Instance.containsPath() compared Instance.directory (always canonical
after anomalyco#16651) against an uncanonicalized filepath argument. When callers
passed a symlinked path, the lexical comparison failed even though the
path resolved to a location inside the project.

This caused false negatives in bash.ts, external-directory.ts, and
file/index.ts — triggering unnecessary external_directory permission
prompts or rejecting valid file reads via symlinked paths.

Fix: resolve the filepath through Filesystem.resolve() before comparing,
so both sides use canonical paths.

Adds tests for: symlinks inside project, external symlinks to project,
symlinks escaping project, dangling symlinks, and symlink cycles.

Fixes anomalyco#16660
jmylchreest added a commit to jmylchreest/opencode that referenced this pull request Mar 9, 2026
Instance.containsPath() compared Instance.directory (always canonical
after anomalyco#16651) against an uncanonicalized filepath argument. When callers
passed a symlinked path, the lexical comparison failed even though the
path resolved to a location inside the project.

This caused false negatives in bash.ts, external-directory.ts, and
file/index.ts — triggering unnecessary external_directory permission
prompts or rejecting valid file reads via symlinked paths.

Fix: resolve the filepath through Filesystem.resolve() before comparing,
so both sides use canonical paths.

Adds tests for: symlinks inside project, external symlinks to project,
symlinks escaping project, dangling symlinks, and symlink cycles.

Fixes anomalyco#16660
@jmylchreest
Copy link
Author

raised #16665 which should resolve #16660 , depends on this PR to be applied as well to be reliable.

jmylchreest added a commit to jmylchreest/opencode that referenced this pull request Mar 9, 2026
…omalyco#16651

Tests for external symlinks, symlinks escaping project, and ELOOP
propagation probe at runtime whether Filesystem.resolve() follows
symlinks. They skip gracefully on dev (before anomalyco#16651) and activate
once realpathSync lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants