Repository navigation
fix(runtime): a self-hosted restart reads the kernel's own sys_metadata back into the registry - #20100
Conversation
createStandaloneStack stamps environmentId 'env_local', and ObjectQLPlugin.start() read any environment id as a per-project kernel and skipped reading sys_metadata at boot. Objects authored at runtime therefore answered 404 OBJECT_NOT_FOUND on the data API after every self-hosted restart, while their rows were still stored. Declare it instead of deducing it, beside runPlatformMigrations: a new optional hydrateMetadataFromDb config field, default true, passed to ObjectQLPlugin. The package-restart acceptance pin's probe 2 is promoted from it.fails to it. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
…showed is vacuous The skip line is not printed at the pin's log level, so the negative assertion stayed green on the unfixed build. The declaration it stood for is pinned in packages/runtime instead. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
…andalone-hydration
check:slot-lookup refuses a service lookup erased to any; the lookups now pass ObjectQL, and the registry read names the item shape it reads. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 42ab9d28765db80526f139a2833a0a1959de6343 && git checkout 42ab9d28765db80526f139a2833a0a1959de6343
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d624002eb907a92aa9e72d463c2e3ab78697d85c 46fd71b1e605a3cb5434b36c40169db538eb5f4c && git checkout -B drift-repro d624002eb907a92aa9e72d463c2e3ab78697d85c && git merge --no-ff 46fd71b1e605a3cb5434b36c40169db538eb5f4c
node scripts/docs-audit/affected-docs.mjs --json d624002eb907a92aa9e72d463c2e3ab78697d85c
|
…ig key Both clauses of the plugin option's caution are properties of createStandaloneStack itself: it constructs the plugin (a fresh ObjectQL and SchemaRegistry) and composes the only datasource sys_metadata routes to, a direct driver. No caller can make either clause false, so the stack passes hydrateMetadataFromDb: true and StandaloneStackConfigSchema gains no key. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
Face review (post-hoc, released)Served-tier: Scope: the changeset (and named docs) of PR #20100, released; this review cannot block anything and exists to find false released prose. Sentence verdicts(
Still true on origin/main?Yes. FindingNONE at the correction bar. The one overstatement (sentence 9, Reviewed-by: VERDICT: CLEAN Generated by Claude Code |
Fixes #20071
Clause-②: no
What was wrong
createStandaloneStack(packages/runtime/src/standalone-stack.ts) is the composition behindos dev/os serve/os start. It stampsenvironmentId: 'env_local'(or whateverOS_ENVIRONMENT_IDnames), and it builtnew ObjectQLPlugin({ environmentId, runPlatformMigrations })with nohydrateMetadataFromDb.ObjectQLPlugin.start()hydrates onlyif (this.environmentId === undefined || this.hydrateMetadataFromDb)(packages/objectql/src/plugin.ts:767). So every self-hosted boot:sys_metadata;An object published at runtime then answered
404 OBJECT_NOT_FOUNDon the data API after a restart, while its row was still stored. This is the same deduction that the[#9380]note in the same file records forrunPlatformMigrations.The fix: a declaration, not a deduction
createStandaloneStackconstructsnew ObjectQLPlugin({ environmentId, runPlatformMigrations: cfg.runPlatformMigrations ?? true, hydrateMetadataFromDb: true }). The[#20071]comment at that line carries the caution check below.StandaloneStackConfigSchemaand the publishedStandaloneStackConfigtype are unchanged.kernel:readymigrations inassembleMetadataProtocolnever arm on a self-hosted boot — the standalone stack stampsenvironmentId = 'proj_local', and the gate asks forundefined#9380's docblock pointer "line ~567 below" now reads "createStandaloneStackbelow".packages/objectql/**andpackages/spec/**are untouched.Opt-out: none. No caller of
createStandaloneStackcan make either clause of the caution false; adding a key later is its own card, declaredClause-②: yes (widening).The plugin's caution, clause by clause
The caution is on
ObjectQLPluginOptions.hydrateMetadataFromDb(plugin.ts:174-177): "Set this ONLY when the kernel's registry is per-instance isolated ANDsys_metadatalives on the kernel's own local driver (no control-plane proxy)". Both clauses are properties ofcreateStandaloneStackitself, not of its caller.ObjectQLPluginand passes noql, soinit()runsthis.ql = new ObjectQL(hostCtx)(plugin.ts:397).ObjectQLowns its registry:private _registry: SchemaRegistry = new SchemaRegistry();(engine.ts:3061, whose comment says "Each engine now owns its registry so kernels are fully isolated").SchemaRegistry(registry.ts:1819) has no static members.sys_metadataon the kernel's own local driver: holds.SysMetadataObject(packages/metadata-core/src/objects/sys-metadata.object.ts:17) declares nodatasource, so it routes to the default driver.DefaultDatasourcePluginasdefault(ADR-0062 D1: "every unbound object routes to it").databaseDriverkind the function dispatches (memory,sqlite,sqlite-wasm,postgres,mysql,mongodb,turso) is a direct driver, never a control-plane proxy.this.engine.find('sys_metadata', { where: { state: 'active', organization_id: null } })(loadMetaFromDbinpackages/metadata-protocol/src/protocol.ts), through that same engine.Stop valve: every caller of
createStandaloneStack, measuredgit grep createStandaloneStackata4ca69a9finds three production callers, plus tests and fixtures:sys_metadatapackages/cli/src/commands/serve.ts:2740os serve/os dev/os startwith a configdefaultdriverpackages/runtime/src/default-host.ts:163(createDefaultHostConfig)packages/cli/src/utils/schema-migrate.ts:303(bootSchemaStack)os migrate */os meta *commandsNo caller has a proxied or non-local
sys_metadata, so the stop valve does not fire.Outside this repository:
createStandaloneStackreturns onlyobjectstack-ai/duly: tests and seed scripts of an ordinary standalone app on its own database.objectstack-ai/cloudis NOT MEASURED. Control leg: the same search forhydrateMetadataFromDbreturns 0 hits outside this repository, although the plugin's own docblock says the cloud single-env tenant runtime sets that option. So the search cannot see that repository.Why
bootSchemaStackhydrates tooRuling ① lets a read-only one-shot boot turn hydration off "if it genuinely does not need it". Measured, the one-shots need it, or are neutral:
os migrate plan: its unmanaged-table sweep (packages/cli/src/utils/unmanaged-tables.tsheader) says the question "is only answerable when the composed object set actually MIRRORS what this deployment'sos serveboot registers". After this PR,os serveregisters runtime-authored objects.os migrate files-to-references: it refuses an empty scan because "this command's verdict is what later authorises irreversible behaviour". Without hydration it would silently skip the file fields of runtime-authored objects.loadMetaFromDbisengine.findplus registry registration plus log lines. A boot that defers DDL still defers the Phase-3 tables of what it hydrated.c945f282:duplicates.integration.test.ts(boot included, database byte-identical after the run),platform-migrations-arming.integration.test.tsand the sixschema-migrate*/unmanaged-tablesintegration suites all pass. The stack's construction is the sametrueat the current head.Ruling ④: the false log line
"Project kernel — skipping sys_metadata hydration" is now unreachable on this stack. It sits only in the
elseof the gate above, andhydrateMetadataFromDbis a literaltruethat no caller can change. The new unit test pins the flag under every way an environment id is stamped.Observed directly:
Metadata restored from database to SchemaRegistry {"loaded":2,"errors":0,"invalid":0}and no skip line.No edit to
packages/objectqlwas needed.Reach beyond one object (mechanism assumption 6)
packages/runtime/src/standalone-stack-hydrate-metadata.test.tsboots the real stack twice on one SQLite file:objectrow and an env-wideapprow through the protocol, and inserts one record.The
appis not in the registry on the boot that wrote it. On this composition, the protocol's write-through for non-object types returns early on a kernel with an environment id (see Acceptance notes). So boot hydration is the only thing that puts it there, which makes it a sharp second leg.Org-scoped rows are deliberately not asserted.
loadMetaFromDbreadsorganization_id IS NULLonly (ADR-0005: per-org overlays are served on demand).Diagnostics hydration now surfaces at boot, reported and not suppressed.
loadMetaFromDband itsreportUnhydratableOrgScopedRows(#6190) now run on every standalone boot:[Protocol] [metadata_field_type_refused] …aterror, or[Protocol] Failed to hydrate TYPE/NAME: …/[Protocol] [metadata_spec_invalid] …atwarn;None of these fired in any suite run here. Against a real install's
sys_metadatathey are NOT MEASURED; the changeset names these lines for upgraders.The pin
packages/cli/test/package-restart-acceptance.integration.test.ts: probe 2'sit.failsis promoted to a plainit, and the file's header now describes the fixed state.A log-line assertion I added beside it was removed in this PR. The ablation leg showed it stays green on the unfixed build, because
os servedoes not print that INFO line at the pin's log level.Tests (head
46fd71b1)The pin:
pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/package-restart-acceptance.integration.test.tsgivesTests 5 passed (5).ablation-dist-preflightshows the runtimedistcarries the literal.Unit test:
pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/standalone-stack-hydrate-metadata.test.tsgivesTests 2 passed (2).Runtime suite:
pnpm --filter @objectstack/runtime testgivesTest Files 279 passed (279),Tests 3906 passed | 1 skipped (3907).Typecheck:
pnpm --filter @objectstack/runtime typecheckexits 0, withcheck:test-typecheck: OK … 27 file(s) / 191 error(s) / 69 pinned signature(s)(ledger unchanged).Lint: full
pnpm lint(eslint . --no-inline-config) exits 0.Gates: all 60 commands that
node scripts/pm/dispatch-gates.mjs --commandsderives at46fd71b1exit 0. The--ranreconciliation reads "60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN".At
c945f282, before this revision:Test Files 224 passed (224),Tests 3158 passed (3158);pnpm --filter @objectstack/cli typecheck: exit 0;--project integrationon 10 files (the pin,duplicates,meta.stored-flow-resolution,platform-migrations-arming, the fiveschema-migrate*files andunmanaged-tables):Test Files 10 passed (10),Tests 38 passed (38).This revision touches no CLI file, and the rest of the CLI integration project is declared to CI.
Ablation at
46fd71b1, from committed state. The mutation flipped the literal'struetofalsethroughnode scripts/ablation-replace.mjs, anchored on the construction line.b10937d6todc0eab77, and on-disk countstrue=0 false=1.dist: after rebuilding@objectstack/runtime,ablation-dist-preflightreports thetruespelling absent from all 6 dist files and the flipped spelling present in 2.1 failed | 4 passed. Probe 2 fails with "GET /data/leave_request after a restart: {"error":"Object 'leave_request' is not registered","code":"OBJECT_NOT_FOUND"} … expected 404 to be 200".b10937d6== HEAD, andgit status --porcelainis empty;falseabsent from all 6;Acceptance notes (observations, not filed)
packages/metadata-protocol/src/protocol.tsapplyRegistryWriteThroughreturns early for every non-object type whenthis.environmentId !== undefined. That is the same deduction class, one package over. On a standalone kernel, a runtime-savedappis absent from the registry on the boot that wrote it, until a listing or the next boot hydrates it; the new unit test's boot 1 measured this. No user-visible failure was measured, so it is recorded here, not filed.packages/objectql/src/plugin.ts(the Phase-2 bridge comment) andloadMetaFromDb's comment still callSchemaRegistrya process-wide singleton, whileengine.ts:3054-3061says each engine now owns its own. Both files are read-only for this lane.Relations
#17676 remains open: that card belongs to the engine seat, and this PR delivers the runtime half that its acceptance pin (PR #20069) waits on.
Generated by Claude Code