Skip to content

bug: TUI session hangs with InstanceRef not provided rejection after LSP initialization (v1.15.2) #27880

Closed

Description

@ririnto

Description

In TUI mode, after a tool (e.g., apply_patch) modifies a file, the LSP server initializes and immediately triggers an unhandled InstanceRef not provided rejection. The session appears to hang (stuck at "Preparing patch..." or similar) after this error.

The root cause is in packages/opencode/src/lsp/lsp.ts:294:

Bus.publish(Event.Updated, {})

This static Bus.publish call is made from inside an Effect.promise callback (line 215), where there is no Effect fiber context. Bus.publish internally calls InstanceState.get → InstanceState.context → yield* InstanceRef, which fails because no fiber context is available.

This is the same class of bug as #27829, #27833, #27838, #27857 — Effect.runPromise or equivalent inside Effect.promise losing fiber context. Those were fixed in v1.15.2 (commits 88363f1, 6d2219e) but the lsp.ts call site was missed.

While a targeted fix (providing InstanceRef via .pipe(Effect.provideService(InstanceRef, ctx)) at the call site) would address this specific instance, PR #25694 proposes an infrastructure-level fix at InstanceState.get() that ensures InstanceRef is always propagated into ScopedCache lookups regardless of how the caller provides it. That approach would prevent this entire class of bugs from recurring across any call site that goes through InstanceState.

Log output

INFO  service=permission permission=edit pattern=Sisyphus.md evaluated
INFO  service=format file=Sisyphus.md formatting
INFO  service=bus type=file.edited publishing
INFO  service=lsp file=Sisyphus.md touching file
INFO  service=lsp serverID=remark spawned lsp server
INFO  service=lsp.client serverID=remark sending initialize
INFO  service=lsp.client serverID=remark initialized
ERROR service=default e=InstanceRef not provided rejection

Steps to reproduce

  1. Start opencode TUI (opencode)
  2. Have a session where the model uses a tool that modifies a file (e.g., apply_patch)
    • This can be a resumed/continued session — the bug is not session-state dependent
    • In my case, it was a .md file being edited
  3. After the tool applies changes, the post-edit hook triggers format → LSP touch → LSP spawn
  4. Observe InstanceRef not provided rejection in logs (~/.local/share/opencode/log/)
  5. The session may hang or become unresponsive after this point

LSP configuration

{
  "lsp": {
    "remark": {
      "command": ["bunx", "--use-system-ca", "remark-language-server", "--stdio"],
      "extensions": [".markdown", ".md"]
    }
  }
}

OpenCode version

1.15.2

Operating System

Linux (WSL2)

Terminal

zsh

Activity

  1. masterkain commented on May 16, 2026

    @masterkain

    Adding another data point: I can confirm this still happens on OpenCode 1.15.3.

    I have three local crash logs from the same flow. Two were on 1.15.1, but the important one was already on 1.15.3: both the parent TUI session and the child subagent session logged version=1.15.3 before the failure.

    Sanitized sequence from the 1.15.3 log:

    service=session ... version=1.15.3 ... agent=<main agent>
    service=session ... version=1.15.3 ... parentID=<parent-session> title=<subagent task>
    service=lsp serverID=elixir-ls ... spawned lsp server
    service=lsp.client serverID=elixir-ls initialized
    ERROR service=default e=InstanceRef not provided rejection
    ERROR service=session.processor session.id=<child-session> ... error=Aborted process
    

    This was in TUI/subagent usage, with elixir-ls being initialized after the subagent flow touched project files. So v1.15.3 looks like it fixed the earlier async command / agent generation call sites, but not this LSP-after-initialize path.

  2. ririnto commented on May 16, 2026

    @ririnto
    Author

    Until Next Release ...

    ~/.config/opencode/opencode.json

    {
        "lsp": false
    }
  3. masterkain commented on May 16, 2026

    @masterkain

    but I love the smell of lsp in the morning

  4. CloudSmithBrandon commented on May 16, 2026

    @CloudSmithBrandon

    Experiencing the same bug with biome and marksman LSPs on opencode-ai v1.15.3 (Windows). Disabling LSPs as workaround. Watching for the fix in PR #25694.

  5. zentetsukenz commented on May 16, 2026

    @zentetsukenz

    I have the same issue and tried debug a little bit. This following paragraph is the analysis from Opus 4.7. Could be worthwhile putting into a context for fixing this crashing issue.

    Opus 4.7 analysis

    Adding another v1.15.1 data point with a different escalation path: same root-cause signature, but in our case it terminates the whole TUI process via Bun SIGTRAP rather than just logging the rejection.

    Environment

    • opencode 1.15.1 (also reproduced on 1.15.3)
    • bundled Bun v1.3.13 (bf2e2cec)
    • macOS 26.3.1 (Apple M2)
    • terminal: Ghostty / xterm-ghostty
    • Reproduced on two separate M2 machines, both running curl-installed opencode
      v1.15.0 does not exhibit this — confirmed by downgrade (opencode upgrade 1.15.0) on both machines.

    Trigger

    TUI session with an active subagent (Sisyphus - Ultraworker parent → @explore subagent). Pressing Ctrl+X to switch into the running subagent session reliably triggers the chain. On the second occurrence, the parent TUI crashes to terminal with a Bun panic.

    Log signature (sanitized, from ~/.local/share/opencode/log/)

    INFO  service=session  id=ses_<parent>  version=1.15.1  agent=Sisyphus - Ultraworker  created
    INFO  service=session  id=ses_<child>   version=1.15.1  parentID=ses_<parent>  title=Map auth module surface (@explore subagent)  created
    ...
    INFO  service=lsp.client  serverID=elixir-ls  path=/.../lib/.../router.ex     workspace/didChangeWatchedFiles
    INFO  service=lsp.client  serverID=elixir-ls  path=/.../lib/.../scope.ex      workspace/didChangeWatchedFiles
    ERROR service=default     e=InstanceRef not provided rejection
    INFO  service=session.prompt  session.id=ses_<child>   cancel
    INFO  service=session.prompt  session.id=ses_<parent>  cancel
    INFO  service=lsp.client  serverID=elixir-ls  path=/.../lib/.../user_auth.ex  workspace/didChangeWatchedFiles
    ERROR service=session.processor  session.id=ses_<parent>  error=Aborted process
    ERROR service=session.processor  session.id=ses_<parent>  error=Aborted process
    

    So this matches the exact path you identified: LSP client initialised → workspace/didChangeWatchedFiles → InstanceRef not provided from inside Bus.publish in lsp.ts. Same call site, elixir-ls instead of remark.

    Escalation: process-level SIGTRAP

    On the next user input after this rejection, the whole TUI crashes with a Bun panic instead of a clean rejection:

    Bun v1.3.13 (bf2e2cec) macOS Silicon
    macOS v26.3.1
    panic: Segmentation fault at address 0x13D30
    SIGTRAP
    

    Stack (from bun.report):

    parcel-watcher.node
    napi_call_threadsafe_function
    ThreadSafeFunction.scheduleDispatch
    ConcurrentTask.createFrom
    

    Bun report: https://bun.report/1.3.13/Ma1bf2e2cemxHuhoqC_2+iydmmqhZ_____A2Agz+E
    My read on the escalation:

    • lsp.ts:294 rejection aborts the session processor mid-flight while @parcel/watcher still holds an NAPI threadsafe function reference back into the Effect runtime.
    • The next callback from the native watcher thread fires napi_call_threadsafe_function into a callback whose surrounding Effect scope has been torn down, dereferencing freed memory inside Bun's ConcurrentTask.createFrom.
    • This is consistent with the NAPI deferred-finalizer bug class Bun fixed in 1.3.14 (release notes). opencode bundles 1.3.13, so the underlying NAPI race is latent — v1.15.0 happens to avoid tripping it; the v1.15.1 InstanceRef rejection path appears to trip it on subagent transition.
      The parcel-watcher callback site in packages/opencode/src/file/watcher.ts also changed in v1.15.1 (InstanceState.bind → EffectBridge.make().bind), which is the other call site I'd suspect of dangling-callback-after-scope-end. Worth checking whether the bridge finalizer can run while parcel's native thread still has the callback pointer in flight.

    Why this is a separate failure mode worth tracking

    The "rejection + hang" surface you described in the OP is bad UX but recoverable. The variant I'm seeing crashes the entire bun process, taking the parent TUI and any running subagents down with it — auth/session state survives, but mid-flight work is lost and there is no .ips dump (process exits via SIGTRAP without handing off to Apple CrashReporter).
    Happy to file this as a separate issue specifically targeting the SIGTRAP escalation if that's preferable to keeping it bundled here. Either way, the infrastructure-level fix in InstanceState.get() (PR #25694) plus auditing remaining Effect.promise callsites that call InstanceState-backed APIs (notably packages/opencode/src/file/watcher.ts) would close both surfaces.

  6. srivenkateswaran6002 commented on May 17, 2026

    @srivenkateswaran6002

    is this issue still open for contributions ? I can try and come up with a fix.

  7. intpfx commented on May 17, 2026

    @intpfx

    Adding another reproduction case with additional diagnostic detail — this time with Deno LSP + TSX files in headless mode (not TUI).

    Environment

    • opencode: 1.15.3 (Homebrew, macOS 26.4.1, Apple Silicon)
    • LSP triggered: deno (for .tsx files)
    • Mode: opencode run --format json (headless, non-TUI)
    • Agent: Hephaestus (oh-my-opencode) with deepseek/deepseek-v4-pro

    Reproduction (100% reliable)

    cd /path/to/project
    opencode run --format json "read apps/web/islands/BrandToggle.tsx" --dir .

    Smaller files in the same project succeed:

    • deno.json (28 lines JSON) → ✅ exit 0, full text response
    • BrandToggle.tsx (147 lines TSX) → ❌ exit 1, no text

    JSON event stream comparison

    Success case (deno.json, 28 lines):

    step_start → tool_use(read) → step_finish(tool-calls, tokens: 48323in/61out)
    → step_start → text(response) → step_finish(stop) ✅
    

    Failure case (BrandToggle.tsx, 147 lines):

    step_start → tool_use(read) → (process exits with code 1, no further events) ❌
    

    No step_finish, no text, no error event — the process terminates silently.

    Debug log capture

    INFO  service=lsp.server  serverID=deno  root=/path/to/project/apps/web  spawned lsp server
    INFO  service=lsp.client  serverID=deno  starting client
    INFO  service=lsp.client  serverID=deno  sending initialize
    DEBUG service=lsp.client  serverID=deno  text=Starting Deno language server...
      version: 2.7.14 (release, aarch64-apple-darwin)
    INFO  service=lsp.client  serverID=deno  initialized
    ERROR service=default     e=InstanceRef not provided rejection   ← crash point
    

    The Deno LSP initializes successfully, then the rejection fires immediately after initialized.

    Notes

    • The Hermes Agent opencode plugin tool was initially suspected, but direct CLI usage proves the bug is in opencode itself
    • The --format json event stream is cut off before any step_finish event, so downstream tooling (like the Hermes plugin) correctly reports status: error, exit_code: 1 but gets no actionable error message — the error only surfaces in opencode's own debug logs
    • Workaround "lsp": false in ~/.config/opencode/opencode.json confirmed effective, but obviously degrades the coding experience
    • Watching PR fix: propagate InstanceRef into ScopedCache lookup in InstanceState.get() #25694

    Token usage (for the successful small-file case, for reference)

    The LLM call that succeeds shows the agent correctly processes the tool output:

    {"type":"step_finish","tokens":{"total":57399,"input":2039,"output":200,"reasoning":120,"cache":{"write":0,"read":55040}}}

    So the failure is definitely at the framework level (LSP → DI), not an LLM API issue.

  8. ririnto commented on May 17, 2026

    @ririnto
    Author

    I tested PR #25694 locally against a minimal LSP reproduction, and it does not appear to fix this specific LSP-after-initialize failure path.

    Setup:

    • Based on current dev
    • Local source checkout reports version=local at runtime; packages/opencode/package.json is 1.15.3
    • Environment has the oh-my-openagent plugin enabled/loaded
    • Applied PR fix: propagate InstanceRef into ScopedCache lookup in InstanceState.get() #25694 locally (InstanceState.get() captures ctx and provides InstanceRef around ScopedCache.get)
    • Added a project-local opencode.json that enables a fake stdio LSP for a .repro file
    • Disabled unrelated MCPs to keep the log focused on the LSP path

    Reproduction 1, direct LSP debug command:

    timeout --kill-after=2s 12s bun --conditions=browser src/index.ts --print-logs --log-level DEBUG debug lsp diagnostics ../../lsp-instance-ref.repro

    Result:

    service=lsp serverID=fake-lsp ... spawned lsp server
    service=lsp.client serverID=fake-lsp starting client
    service=lsp.client serverID=fake-lsp sending initialize
    service=lsp.client serverID=fake-lsp initialized
    ERROR service=default e=InstanceRef not provided rejection
    EXIT_STATUS=1
    

    Reproduction 2, actual opencode run --format json read-tool path:

    timeout --kill-after=2s 35s bun --conditions=browser src/index.ts --print-logs --log-level DEBUG run --format json --dir ../.. "Use the read tool to read the file lsp-instance-ref.repro. Do not use shell commands."

    Result:

    tool=read ... file=/tmp/opencode-pr25694-27880/lsp-instance-ref.repro
    service=lsp file=/tmp/opencode-pr25694-27880/lsp-instance-ref.repro touching file
    service=lsp serverID=fake-lsp ... spawned lsp server
    service=lsp.client serverID=fake-lsp starting client
    service=lsp.client serverID=fake-lsp sending initialize
    service=lsp.client serverID=fake-lsp initialized
    ERROR service=default e=InstanceRef not provided rejection
    EXIT_STATUS=1
    

    I also reproduced the same path via a Sisyphus primary agent delegating to a Sisyphus-Junior subagent. In that case the subagent performed the read, then hit the same LSP initialization failure.

    My read is that PR #25694 is still useful hardening for InstanceState.get() / ScopedCache.get(...), but this issue’s direct LSP path needs an additional fix in packages/opencode/src/lsp/lsp.ts.

    The failure still matches the Bus.publish(Event.Updated, {}) call inside the Effect.promise(async () => ...) block. At that point the code has crossed into plain async JS, and the module-level Bus.publish runtime re-entry no longer has the original InstanceRef.

    So PR #25694 can preserve an InstanceRef once InstanceState.get() has one, but it cannot synthesize one for this separate runtime re-entry.

    A targeted fix would be to capture the instance context before the async boundary and explicitly provide InstanceRef when publishing Event.Updated, e.g.:

    import { InstanceRef } from "@/effect/instance-ref"
    import { makeRuntime } from "@/effect/run-service"
    
    const busRuntime = makeRuntime(Bus.Service, Bus.layer)
    
    // inside getClients(), after `const ctx = yield* InstanceState.context`
    void busRuntime.runPromise((bus) =>
      bus.publish(Event.Updated, {}).pipe(
        Effect.provideService(InstanceRef, ctx),
      ),
    )

    This mirrors the existing safe pattern in packages/opencode/src/lsp/client.ts, where LSP callbacks publish bus events through a runtime while explicitly providing the captured instance context.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions