Skip to content

Run one bring-up at a time within a window - #117

Draft
MatiasFernandez wants to merge 4 commits into
mainfrom
mfernandez/one-bring-up-at-a-time
Draft

MatiasFernandez wants to merge 4 commits into
mainfrom
mfernandez/one-bring-up-at-a-time

Conversation

@MatiasFernandez

@MatiasFernandez MatiasFernandez commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Inside one window, ensureRunning didn't share a call that was already running. Auto-start plus a cell, two notebooks, or a cell plus Start each ran the whole bring-up at once: two stone starts, two Grail file-ins, two MCP installs and starts, and a databaseStarted event from each. Separately, the first-run shared-memory modal and a cell's modal could both show, and both could run the sudo script.

Closes part of #111.

What

A small singleFlight helper (src/singleFlight.ts). The first caller starts the work, and anyone who arrives before it finishes joins it and gets the same result, including a rejection. runSetup already did this inline and now uses the helper. One commit for each:

  1. Helper. No change in behaviour.
  2. ensureRunning. A joiner waits for the whole call, from the shared-memory prompt to the MCP start. Only the caller that started the call reports databaseStarted, under its own trigger, for failures as well as successes.
  3. Shared memory. ensureOsConfigured is single-flight, so osConfigPrompted is sent once, under the first trigger. The shared-memory script is single-flight across the prompt and GemDB: Configure Shared Memory. If one of them arrives while the other's script is running, it waits, checks shared memory again, and opens no modal and no second terminal. Only the caller that ran the script reports.
  4. ensureMcpServing. resumeMcpServing at activation and ensureRunning can no longer install and start the router twice.

The CHANGELOG has a Fixed entry for each of 2–4. docs/telemetry.md and CLAUDE.md describe the joining behaviour.

Worth checking

  • Telemetry semantics: a start that auto-start began and a cell joined is reported as trigger: autoStart. That's deliberate: one bring-up gives one event.
  • Joiners see less progress: a joiner doesn't get its own progress notification for the shared work. For example, if ensureRunning joins an MCP start that resumeMcpServing began, its "Starting GemDB" notification never says "Installing the MCP server…". A cell that is waiting still shows VS Code's pending icon, so the user can see the run was accepted.
  • The test mock gained terminals: src/__mocks__/vscode.ts now has onDidCloseTerminal and a __closeTerminal helper so the tests can finish a sudo terminal.

Known gaps (planned, not in this PR)

All of these are tracked as separate packages of the same concurrency work:

  • Cross-window races are untouched. Two windows can still each ask about shared memory, or both try to start. Everything here is per extension host.
  • reinstallGrail still runs its own file-in instead of joining a bring-up.
  • startedStone is reported as true when another process had already started the stone.
  • Nothing here touches the locks in lock.ts.
  • One planned test is missing: an activation-level test for "a cell during the first-run modal leads to one modal". activation.test.ts mocks both lifecycle and osConfig, so it can't reach the real wrappers. The same case is covered one layer down in oneOsPrompt.test.ts, with the firstRun and notebook triggers overlapping.

Testing

  • New unit tests: singleFlight.test.ts, oneBringUp.test.ts (overlapping bring-up, shared failure, fresh run after settle, overlapping MCP start), and oneOsPrompt.test.ts (overlapping prompts, the command during the prompt's script and the reverse, a fresh modal after settle). I checked each by removing the single-flight it covers and watching it fail.
  • npm test (771 passed), lint, format check and both typechecks all pass. npm run test:integration passed with the engine, the payload and the test extent built (112 passed, none skipped).
  • Manual checks, done on macOS arm64:
    • Two notebooks, one bring-up. I stopped the database, deleted <root>/grail/.gemdb-grail-stamp to make Grail file in again, and ran a cell in each notebook during the Grail install. I saw one "Starting GemDB" notification, one stone and NetLDI start, one Grail file-in, and one GemDB is already starting; waiting for it. from the second notebook. With gemdb.mcp.enabled on (it's off by default), I also saw one MCP install and one router fork. Both cells showed VS Code's pending icon while they waited, and both ran once the bring-up finished.
    • Shared memory, prompt then command. With shared memory lowered (scripts/unset-os-config.sh), a cell opened one modal. Pressing Configure opened one sudo terminal. While it waited for the password, a second notebook's cell joined the bring-up and GemDB: Configure Shared Memory waited for the script. Neither opened a modal or a second terminal. After the password, I saw one "Shared memory configured.", and the database came up and both cells ran.
    • Shared memory, command then cell. Running the command first and then a cell while its terminal was open gave no modal and no second terminal. The cell ran once the script finished.
    • Not reachable by hand: a cell during the first-run modal. The modal blocks the rest of the window, so no cell can start while it's open. That overlap stays covered by oneOsPrompt.test.ts.

🤖 Generated with Claude Code

MatiasFernandez and others added 4 commits October 13, 2026 13:39
runSetup already let a second caller join the setup in progress; the
pattern moves into src/singleFlight.ts so ensureRunning, the
shared-memory prompt and MCP serving can use the same one. No change in
behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auto-start and a cell, two notebooks, or a cell and Start each ran the
whole of ensureRunning at the same time: two startProcesses, two
account checks, two Grail file-ins, two MCP installs and starts, and a
databaseStarted for each. ensureRunning is now single-flight within a
window. A caller arriving mid-flight joins the call in progress and gets
its result; only the caller that started it reports, under its own
trigger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first-run prompt and a cell's ensureRunning each opened a modal;
VS Code queued the second, and answering both could run the sudo script
twice. ensureOsConfigured is now single-flight, so a second caller takes
the first one's answer and osConfigPrompted is sent once, under the
first caller's trigger.

The shared-memory script is single-flight too, whether the prompt or
"GemDB: Configure Shared Memory" runs it. The command during a prompt
waits for it and shows its usual message without a second terminal; a
prompt during the command's script waits, checks again and returns
without a modal. Only the caller that ran the script reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
resumeMcpServing at activation and ensureRunning both reach
ensureMcpServing, and on a running stone they could install the MCP
classes twice and race to fork two routers onto one port.
ensureMcpServing is now single-flight; a second caller waits for the
first and gets its result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MatiasFernandez
MatiasFernandez force-pushed the mfernandez/one-bring-up-at-a-time branch from 973d42c to fb953b8 Compare October 13, 2026 16:42

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.

1 participant