Repository navigation
Run one bring-up at a time within a window - #117
Merged
Merged
Conversation
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
force-pushed
the
mfernandez/one-bring-up-at-a-time
branch
from
October 13, 2026 16:42
973d42c to
fb953b8
Compare
The prompt, the shared-memory script, the bring-up and the MCP step are module-level single flights, and nothing reset them between tests. A test that failed with a modal unanswered, a terminal open or a deferred unresolved handed its flight to every later test, which joined it and timed out, so one failure read as many. oneOsPrompt.test.ts now cancels any pending modal and closes every terminal (a new __closeAllTerminals in the vscode mock) after each test, for a few rounds so a follow-on step gets drained too, before the spies are restored. oneBringUp.test.ts records every deferred it hands out and resolves them all after each test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prompt and "GemDB: Configure Shared Memory" coordinated by peeking at each other's single flight: a prompt that saw the command's script running waited for it instead of probing and asking, and the command joined the whole prompt when one was under way. Both shortcuts skipped steps. Now each runs its normal sequence and they share only the script. The prompt always probes, shows the modal, starts or joins the script, then offers RemoveIPC. The command probes, starts or joins the script, probes again and reports, and never looks at the prompt. This closes the gaps the shortcuts opened: - a cell no longer waits on the command's sudo terminal when memory is already enough; it answers alreadyConfigured at once - the command runs its script even when the modal it overlapped is cancelled - a prompt that joins the command's script still offers RemoveIPC on Linux - the command no longer waits on the RemoveIPC terminal after its script - a cell that showed no modal no longer answers configured, which counted as GemDB having prompted - the command's first probe is used again rather than wasted The command now always sends its own osConfigPrompted event when memory was short, as on main, including when it joined the prompt's script. A modal can appear while the command's terminal is open, but "Configure" joins that terminal, so there is still one sudo prompt. SingleFlight loses its `current` getter, which nothing reads any more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop takes no lock, so it can land on a bring-up already under way. A cell run or Start pressed after that Stop joined the window's bring-up and took its result, which described a database Stop had since taken down: the stone stayed stopped, the cell failed at login or met "GemDB is not running", and the joiner never ran allowAutoStart, so auto-start stayed suppressed. On main that caller ran a bring-up of its own and started the stone again. stop() now counts the Stops begun in this window and keeps the one under way; the bring-up's owner records the count when it starts. A caller that joins with a different count, meaning a Stop began after the bring-up did, waits for the shared result (true or false), waits for that Stop, and runs ensureRunning again. A Stop pressed after the caller joined still wins. Each rerun needs another Stop, so this cannot loop. No process check is involved: isRunning blocks the extension host. allowAutoStart moves from the bring-up body into ensureRunning, so a joiner retracts an earlier Stop too. Auto-start is the exception: it is GemDB's own doing, and calling it there would undo a Stop pressed while auto-start waits on another window's setup. A Stop cancelled at its "Stop Anyway" modal is harmless: a caller that joined after it waits for the modal, then reruns, finds the stone up, and only brings back the MCP server Stop had already taken down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The telemetry doc said the command waited for the prompt's setup and sent nothing; it now shares the script's terminal and sends its own event, and a dialog that opens while the command's setup runs is shown and sent as usual. databaseStarted, the changelog and CLAUDE.md now say that a caller arriving after a Stop on the bring-up starts again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MatiasFernandez
marked this pull request as ready for review
October 14, 2026 16:52
jgfoster
approved these changes
Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Inside one window,
ensureRunningdidn'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 adatabaseStartedevent from each. Separately, the first-run shared-memory modal and a cell's modal could both show, and both could run thesudoscript.Closes part of #111.
What
A small
singleFlighthelper (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.runSetupalready did this inline and now uses the helper. One commit for each of the first four:ensureRunning. A joiner waits for the whole call, from the shared-memory prompt to the MCP start. Only the caller that started the call reportsdatabaseStarted, under its own trigger, for failures as well as successes.ensureOsConfiguredis single-flight, soosConfigPromptedis sent once, under the first trigger. Thesudoscript is single-flight across the prompt and GemDB: Configure Shared Memory, so only one terminal opens. Item 6 reworks how the prompt and the command share it.ensureMcpServing.resumeMcpServingat activation andensureRunningcan no longer install and start the router twice.After review
SingleFlight.currentis gone, so no flight looks into another's slot.allowAutoStart()moved intoensureRunningso callers that join count too, again except auto-start itself.docs/telemetry.md, the CHANGELOG andCLAUDE.mddescribe these exceptions.The CHANGELOG has a Fixed entry for each of 2–4.
Worth checking
trigger: autoStart. That's deliberate: one bring-up gives one event.osConfigPromptedwhen memory was short, even when it joined the prompt's script, as onmain. So one script run can give two events: one for the modal and one for the command.sudoterminal is open: its "Configure" joins that terminal, so there is still only one terminal.ensureRunningjoins an MCP start thatresumeMcpServingbegan, 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.src/__mocks__/vscode.tsnow hasonDidCloseTerminal, plus__closeTerminaland__closeAllTerminals, so the tests can finish and clean upsudoterminals.Known gaps (planned, not in this PR)
All of these are tracked as separate packages of the same concurrency work:
sudoterminal left open or a hung MCP start makes every entry point in the window wait. Running one bring-up at a time trades this for no more duplicate bring-ups. The cancellable wait in Setup and bring-up under the root lock #113 addresses it.main.sudocredentials), "Configure" opens a second terminal one after the other (plan I/H25).reinstallGrailstill runs its own file-in instead of joining a bring-up.startedStoneis reported as true when another process had already started the stone.lock.ts.activation.test.tsmocks bothlifecycleandosConfig, so it can't reach the real wrappers. The same case is covered one layer down inoneOsPrompt.test.ts, with thefirstRunandnotebooktriggers overlapping.Testing
singleFlight.test.ts.oneBringUp.test.ts: overlapping bring-up, shared failure, fresh run after settle, overlapping MCP start, plus the Stop cases: a cell after Stop during the MCP step and during a failed file-in, no Stop in between, a Stop after joining, a Stop cancelled at its modal, and auto-start joining after a Stop.oneOsPrompt.test.ts: overlapping prompts, the command and the prompt overlapping in either order, a declined modal not cancelling the command, memory already enough while the command's terminal is open, and the Linux RemoveIPC offer and its terminal.npm test(798 passed), lint, format check and both typechecks all pass. The single-flight test files passed 20 runs in a row, and 20 more with every CPU core busy.npm run test:integrationpassed (112 passed, none skipped) with the engine, the payload and the test extent built.<root>/grail/.gemdb-grail-stampto 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 oneGemDB is already starting; waiting for it.from the second notebook. Withgemdb.mcp.enabledon (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.scripts/unset-os-config.sh), a cell opened one modal. Pressing Configure opened onesudoterminal. While it waited for the password, a second notebook's cell joined the bring-up, and GemDB: Configure Shared Memory joined 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. After commit 6 the command also sends its ownosConfigPromptedevent.oneOsPrompt.test.ts.🤖 Generated with Claude Code