fix(state): retire workers after existing-schema scope ends

The reuse guard added in e52420e1ee validates a cached worker's original
read admission. Ending a managed existing-schema scope threw a plain Error,
so the guard propagated it instead of retiring an idle worker before an
ordinary open on the same database.

Classify ended schema scopes as StateDatabaseReadAdmissionInvalidatedError
at the schema-policy owner, retaining the diagnostic message. The existing
guard now joins idle actor retirement; active actors remain refused and
unrelated admission or cleanup failures still propagate. Strengthen the
existing expired-caller assertion to require the invalidation code.
No schema, retention, permission, or update-driver contract changes.

Reproduced the existing-schema test on main: 1 failed, 1 passed before fix.
Focused schema/worker suites: 25/25. Changed two-test file: 2/2 in 15.06s
Vitest time, 30.15s wrapper wall with one worker; no new fixtures or cases.
Linux Node 24.19.0: all 39 requested sqlite/state worker and caller-mode
files passed 405 tests; reconstructed CI shard 19 passed 913 tests across
45 files in 221.57s. Native Blacksmith Testbox command exited 0:
https://github.com/openclaw/openclaw/actions/runs/36329004971

Core tsgo, infra/state test graphs, changed-file lint/format, import-cycle
check, diff check, and independent Codex P2 autoreview passed.
This commit is contained in:
Peter Steinberger
2026-09-27 08:47:11 -07:00
parent b889408649
commit 601bb5ba37
3 changed files with 11 additions and 2 deletions
@@ -100,6 +100,9 @@ path; active work must settle before replacement. This prevents Doctor and plugi
migrations from recreating retired database paths or acquiring leases in the wrong
database. Existing update drivers and stored schemas need no migration.
Completion of a managed existing-schema scope also revokes its read admission.
Ordinary callers reopen through normal schema admission after the idle actor retires.
Each SQLite broker worker admits up to 128 running and queued requests. A busy
worker's admission queue does not consume another worker's request capacity;
independent workers continue serving their databases. Requests on the same worker
@@ -96,7 +96,10 @@ describe("existing-schema shared-state workers", () => {
type: "flows.list",
input: { ownerKey: "agent:main:expired" },
}),
).rejects.toThrow("schema admission has ended");
).rejects.toMatchObject({
code: "STATE_DATABASE_READ_ADMISSION_INVALIDATED",
message: "Existing shared-state schema admission has ended.",
});
expect(readAppVersion(databasePath)).toBe("synthetic-installed-runtime");
});
});
+4 -1
View File
@@ -5,6 +5,7 @@ import type { DatabaseSync } from "node:sqlite";
import { isPromiseLike } from "@openclaw/normalization-core/promise-like";
import { hasErrnoCode } from "../infra/errno.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import { StateDatabaseReadAdmissionInvalidatedError } from "./openclaw-state-db-async-lifecycle.js";
type ExistingSchemaScope = { path: string; canonicalPath: string; active: boolean };
const schemaPolicies = resolveGlobalSingleton(
@@ -64,7 +65,9 @@ export function withExistingOpenClawStateSchema<T>(
function assertSchemaScopeActive(scope: ExistingSchemaScope | undefined): void {
if (scope && !scope.active) {
throw new Error("Existing shared-state schema admission has ended.");
throw new StateDatabaseReadAdmissionInvalidatedError(
"Existing shared-state schema admission has ended.",
);
}
}