Skip to content

fix(core): share one service graph between the TUI runtime and HTTP listeners - #51495

Open
dhaern wants to merge 2 commits into
anomalyco:devfrom
dhaern:fix/tui-single-service-graph
Open

dhaern wants to merge 2 commits into
anomalyco:devfrom
dhaern:fix/tui-single-service-graph

Conversation

@dhaern

@dhaern dhaern commented Sep 26, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #32161

Type of change

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

What does this PR do?

When the TUI also starts an HTTP listener (--port, --hostname or mDNS), the process builds two independent Effect service graphs: the listener uses a fresh MemoMap in startListener, and everything else uses AppRuntime. The listener's first request and checkUpgrade then load the same directory at the same time, so the project bootstraps twice: two creating instance and two fromDirectory logs, plugin init running twice, and a second graph left resident.

startListener now builds with the process MemoMap, so the listener shares the app graph with AppRuntime. The layers the listener owns (routes, router, websocket tracker, the CORS Vary fix) are built with Layer.fresh, so auth is still re-read per listener.

The second commit fixes a hang on the same path. If the loading fiber is interrupted (for example the listener scope closes during bootstrap), the InstanceStore Deferred never settled and disposeAll waited on it forever. completeLoad now settles the Deferred on every exit, waiters see the interruption, and the entry is removed.

This differs from #35998, which shares the InstanceContext across graphs. Each graph would still bootstrap its own per-directory state, so the duplicate work is deferred and not removed.

#32161 was closed by the inactivity bot after 60 days, but the problem is still on dev: startListener still builds with its own Layer.makeMemoMapUnsafe().

How did you verify your code works?

  • bun test --timeout 30000 test/server/httpapi-listen.test.ts test/project/instance.test.ts in packages/opencode: 31 pass. bun run typecheck passes.
  • Copying only these two test files onto the PR base commit makes 7 of the 31 fail: five in httpapi-listen.test.ts (the three orderings of the shared-instance test, the listener-restart test and the websocket test) and two InstanceStore interruption tests in instance.test.ts.
  • Measured on an earlier revision of this branch, before a small later change that only adds one middleware registration per router: with a listener, a headless boot logged 2 plugin inits on the base build and 1 with this change (18 of 18 runs). Without a listener it logged 1 in both. A real TUI under a pty with --port 0 logged creating instance twice on the base build and once with this change (3 of 3). I did not repeat these runs on the final commit. Logs from the base build, one run with --port 0 and one without: external-double-init.logs.log, r1-noport.log.
  • Timings from the same headless runs with a listener (n=18 per arm, 95% bootstrap CI): first HTTP 200 -53 ms [-67, -33], instance load -21 ms [-35, -12], boot CPU -0.09 s [-0.135, -0.05]. Every interval in the no-listener controls includes 0. I make no claims about idle CPU, RSS, TUI startup or renderer CPU. Full summary: bench-summary.md

Screenshots / recordings

N/A, non-UI change.

Checklist

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

@github-actions

Copy link
Copy Markdown
Contributor

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

@dhaern dhaern changed the title fix(opencode): share one service graph between the TUI runtime and HTTP listeners fix(core): share one service graph between the TUI runtime and HTTP listeners Sep 26, 2026
@dhaern

dhaern commented Sep 30, 2026

Copy link
Copy Markdown
Author

@nexxeln you were assigned to #32161, which this PR closes. The inactivity bot closed the issue, but dev still bootstraps the project twice when the TUI starts a listener. @jlongster this touches the layer graph and InstanceStore you worked on. Could one of you take a look when you have time?

The production diff is +64/-42 across three files. The other 244 added lines are tests. There are two independent commits: the first makes the listener share the app graph with AppRuntime, and the second makes InstanceStore settle its Deferred when a load is interrupted. I can split the second into its own PR if that is easier to review.

The test, typecheck and nix-eval workflows show action_required, so they have not run. Locally the two changed test files pass (31 tests) and bun run typecheck is clean. The branch is 31 commits behind dev, and none of them touch these files.

The original fresh boundary omitted router-registering middleware. Keep corsVaryFix fresh per listener while retaining shared app services, and build Default without persistent test auth.
@dhaern
dhaern force-pushed the fix/tui-single-service-graph branch from 7580445 to d3c74c6 Compare September 30, 2026 06:23

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.

TUI startup initializes the same instance twice

1 participant