fix(hub): close owned session store during shutdown

This commit is contained in:
abeatrix
2026-10-14 02:13:08 -07:00
parent d790a40fd8
commit fc90cb0ce8
3 changed files with 36 additions and 5 deletions
+4
View File
@@ -1252,3 +1252,7 @@ reports active plugin requests in both `session.list` activity and `hub.status`;
replacement and recovery checks count these requests even when no chat is attached.
Admission is drained before activity is checked, preventing new plugin work from
starting between the idle check and retirement.
The Hub owns the SQLite session store it creates for its default local runtime
and closes that store after runtime shutdown. An injected session host retains
ownership of its persistence resources.
@@ -281,6 +281,7 @@ export class HubServerTransport implements NativeHubTransport {
private readonly settings: CoreSettingsService;
private readonly pluginCommands: HubPluginCommandService;
private readonly sessionSearch: SessionHistorySearchService;
private readonly sessionStore?: SqliteSessionStore;
private readonly cronService?: CronService;
private readonly sessionHost: RuntimeHost &
Partial<PendingPromptsRuntimeService & CommandExecutionRuntimeService>;
@@ -306,14 +307,17 @@ export class HubServerTransport implements NativeHubTransport {
}),
);
});
this.sessionHost =
options.sessionHost ??
new LocalRuntimeHost({
sessionService: new CoreSessionService(new SqliteSessionStore()),
if (options.sessionHost) {
this.sessionHost = options.sessionHost;
} else {
this.sessionStore = new SqliteSessionStore();
this.sessionHost = new LocalRuntimeHost({
sessionService: new CoreSessionService(this.sessionStore),
fetch: options.fetch,
logger: options.logger,
telemetry: options.telemetry,
});
}
this.sessionSearch = new SessionHistorySearchService(
this.sessionHost,
options.sessionSearchOptions ??
@@ -764,7 +768,12 @@ export class HubServerTransport implements NativeHubTransport {
await this.pluginCommands.dispose();
await this.sessionSearch.dispose();
await this.tasks.dispose();
await this.sessionHost.dispose("hub_server_stop");
try {
await this.sessionHost.dispose("hub_server_stop");
} finally {
// The Hub owns the default store; injected runtimes own theirs.
this.sessionStore?.close();
}
await this.schedules.dispose();
if (this.cronService) {
try {
@@ -2,6 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { SqliteSessionStore } from "../../services/storage/sqlite-session-store";
import { localHubHasNoActiveSessions, NodeHubClient } from "../client";
import { HubPluginClient } from "../client/plugin-client";
import { createLocalHubScheduleRuntimeHandlers } from "../daemon/runtime-handlers";
@@ -205,3 +206,20 @@ it("reports a marketplace install as busy without an attached chat", async () =>
.payload?.idle,
).toBe(true);
});
it("closes the Hub-owned session database before removing its directory", async () => {
const opened = vi.spyOn(SqliteSessionStore.prototype, "getRawDb");
try {
await transport.handleCommand({ version: "v1", command: "session.list" });
const db = opened.mock.results.find(
(result) => result.type === "return",
)?.value;
if (!db)
throw new Error("Expected session.list to open the session database");
await transport.stop();
stopped = true;
expect(() => db.prepare("SELECT 1").get()).toThrow();
} finally {
opened.mockRestore();
}
});