Skip to content
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
no setter. A method's receiver (`self`, or whatever the def calls it), a tuple's items, group
rows and a saved stack's values cannot be changed, and VS Code does not offer Set Value on them.

### Fixed

- **Starting GemDB from two places at once starts it once.** A cell run while the database was
starting on its own, cells in two notebooks, or a cell and **GemDB: Start** each ran the whole
start: two stone starts, two installs of Python support, two MCP servers. Now whatever asks while
a start is under way in the same window waits for that start and gets its result, unless the
database was stopped while it started: then it waits for the stop and starts the database again.
- **GemDB asks about shared memory once, even when two things need it.** A cell run while the
first-run question about shared memory was still open queued a second copy of it, and answering
both could open two `sudo` terminals. Now the second waits for the first answer. Running
**GemDB: Configure Shared Memory** while that setup is already running uses its terminal instead
of opening another, and so does answering the question while the command's setup is running.
- **The MCP server is installed and started once when GemDB starts.** Opening a window on a running
database while a cell started it could install the MCP server's classes twice and start two
servers on one port. Now the second waits for the first.

## [1.6.0] - 2026-10-07

The engine is unchanged at GemStone 4.0.0.a4, but start with a fresh database: delete
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ measured about when to ask for shared memory.
whether the user pressed Start or just ran a notebook cell. It finishes any
outstanding preparation, prompts for shared memory, starts the processes, files
Grail in, and brings the MCP server up. New entry points that need a database
should call it rather than checking and asking.
should call it rather than checking and asking. Within a window it is
single-flight (`singleFlight.ts`): a second caller waits for the call already
in progress and gets its result, and only the first caller reports telemetry —
except after a Stop: a caller other than auto-start arriving once a Stop began
during that bring-up waits for the Stop and starts again.

## CI

Expand Down
17 changes: 14 additions & 3 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,15 @@ Memory** while shared memory was still too low, or when they ran **GemDB: Keep
the Database Running After Logout** while RemoveIPC was unset. The dialog comes
back every time the database is needed until the user agrees, but a failure is
sent once for each trigger, outcome and `missing` until setup works or the
database next starts. Running either command is not deduped: every run is
sent.
database next starts. That rule does not apply to either command: every run
made while the setting was still unchanged is sent. A request that arrives
while the dialog, or the shared-memory setup it started, is already under way
in the same window waits for it and sends nothing, so one dialog is one event,
carrying the trigger of whatever opened it. The command is not such a request:
run during the dialog's setup, **GemDB: Configure Shared Memory** shares its
terminal but sends its own event, and a dialog that comes up while the
command's setup is running is shown and sent as usual, its "Configure" joining
that terminal rather than opening a second one.

| Property | Values |
| --------- | ----------------------------------------------------------------------------- |
Expand All @@ -167,7 +174,11 @@ sent.
The database was brought up, or failed to come up. Sent only when something
actually happened. Running a cell when the database is already up sends
nothing, and a failure is sent once for each trigger until the database next
starts.
starts. A request that arrives while a start is already under way in the same
window waits for it and sends nothing, so one start is one event, carrying the
trigger of whatever began it. The exception is a request that arrives after
the user stopped the database during that start: it waits for the stop and
then asks again, so the start that follows is an event of its own.

| Property / measure | Values |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
Expand Down
25 changes: 24 additions & 1 deletion src/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,16 @@ export const window = {
dispose(): void;
} {
__terminals.push(nameOrOptions as FakeTerminalOptions);
return { show: () => {}, sendText: () => {}, dispose: () => {} };
const terminal = { show: () => {}, sendText: () => {}, dispose: () => {} };
__openTerminals.push(terminal);
return terminal;
},
/** Fires when a test calls `__closeTerminal`, as the editor does when the user closes one. */
onDidCloseTerminal(listener: (terminal: unknown) => void): Disposable {
__closeTerminalListeners.push(listener);
return new Disposable(() => {
__closeTerminalListeners.splice(__closeTerminalListeners.indexOf(listener), 1);
});
},
/**
* Progress is a pass-through here: run the callback and return its result.
Expand Down Expand Up @@ -489,6 +498,20 @@ export interface FakeTerminalOptions {
};
}
export const __terminals: FakeTerminalOptions[] = [];
/** The terminal objects `createTerminal` handed back, oldest first. */
export const __openTerminals: object[] = [];
const __closeTerminalListeners: Array<(terminal: unknown) => void> = [];
/** Close a terminal the way the editor reports it to `onDidCloseTerminal`. */
export function __closeTerminal(terminal: object): void {
for (const listener of [...__closeTerminalListeners]) listener(terminal);
}
/**
* Close every terminal still open, so a test that failed half way does not
* leave a setup script waiting for the next test to finish it.
*/
export function __closeAllTerminals(): void {
for (const terminal of __openTerminals.splice(0)) __closeTerminal(terminal);
}

export const workspace = {
notebookDocuments: [] as unknown[],
Expand Down
Loading
Loading