Skip to content

fix(objectql): the boot hydration ERROR names the storage remedies when no driver serves sys_metadata - #23698

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-23636-no-storage-hydration-error
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-23636-no-storage-hydration-error

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23636

Clause-②: no

What this changes

When sys_metadata cannot be read at boot, ObjectQLPlugin.restoreMetadataFromDb logs one error line: what was not restored, and the fix. The fix half was always "check the datasource behind sys_metadata — connection, credentials, and that the table exists". On a deployment with no datasource declared and no driver registered, such as a production os serve with no OS_DATABASE_URL, there is no datasource to check and no credential to fix. The operator was sent to a store nobody configured, with the real remedies absent from the line (they sit on the CLI's ⚠ No storage: line only, which prints earlier).

Now the line says which situation it is in. When nothing serves sys_metadata it says "no storage is configured" and names the three storage remedies, worded exactly as the CLI's ⚠ No storage: line names them. When a datasource exists and its read failed, the line is byte for byte what it was. Level (error) and the consequence sentence are the same in both arms.

Before / after (the logged text)

Before, in every case:

sys_metadata could NOT be read at boot — persisted metadata was NOT restored, and this kernel will keep reporting healthy. Every overlay object, view, app, permission and hook stored in the database is absent from the SchemaRegistry for the life of this process: registry lookups answer "not declared" rather than "unavailable", so unknown-column query guards, hooks and relationships silently degrade, and overlay objects get neither a synced table nor a metadata bridge. Authoring against this kernel writes on top of state it never loaded. Fix: check the datasource behind sys_metadata — connection, credentials, and that the table exists — then restart. A store that merely has not been provisioned yet is NOT this case; that first-boot path stays quiet.

After, when no datasource is declared and no driver serves sys_metadata:

sys_metadata could NOT be read at boot — no storage is configured: no datasource is declared and no driver is registered, so nothing serves sys_metadata. Persisted metadata was NOT restored, and this kernel will keep reporting healthy. Every overlay object, view, app, permission and hook stored in the database is absent from the SchemaRegistry for the life of this process: [same consequence sentence as above]. Fix: set OS_DATABASE_URL (e.g. file:./data/app.db, or a postgres:// or mysql:// URL); mount a storage driver in the stack's plugins (e.g. new DriverPlugin(driver) from @objectstack/runtime); or run with --dev (an in-memory SQLite store, for development) — then restart. A store that merely has not been provisioned yet is NOT this case; that first-boot path stays quiet.

After, when a datasource exists and its read failed: unchanged from "Before".

How the arm is chosen

New private sysMetadataHasNoStorage(), asked only once the read has already failed (a healthy boot pays nothing). It reads the engine's own routing, never the text of the error the read raised. It answers "no storage" only when all three are empty:

  • getDriverForObject('sys_metadata'): a driver serves it, so the read itself failed: datasource problem.
  • resolveEffectiveDatasource('sys_metadata'): it is bound (explicit, mapping, lifecycle or package) to a named datasource whose driver is missing: that datasource is declared, datasource problem.
  • listUnavailableDatasources(): the connection layer recorded a declared datasource it could not use (policy-refused, or a connect failure the operator waved through): datasource problem. This one matters because such a boot also has no registered driver, so drivers alone cannot tell it from "no storage".

The three remedies live in one private static readonly NO_STORAGE_REMEDIES beside the method, with a note that the CLI line is their authority.

File surface: restoreMetadataFromDb region of packages/objectql/src/plugin.ts, the pins, one changeset. Nothing in packages/cli, the start-order region, onMetadataMutation or followRuntimeObjectCopy.

Pins and ablation

New packages/objectql/src/plugin-restore-metadata-no-storage.test.ts (6 cases), over a real ObjectQL engine in every case:

  • (a) no storage: the line says "no storage is configured", names the three remedies verbatim, does not contain "check the datasource behind sys_metadata" or "credentials", keeps the consequence sentence, the counts in the meta slot and the single error.
  • (a, reach) the REAL ObjectStackProtocolImplementation over a driverless engine: the boot read really fails (DB hydration skipped: … No driver available for object 'sys_metadata') and the line is the no-storage one.
  • (b) CONTROL, the datasource arm unchanged, byte for byte: a registered driver whose read fails; the same with the real protocol and a driver that refuses with an authentication error; sys_metadata bound to a named datasource with no driver; a declared datasource marked unavailable by the connection layer.

The existing plugin-restore-metadata-outage.test.ts used ql: {} as its engine on the premise "the engine is untouched by this path". That premise no longer holds, so its makePlugin() now builds a real engine with one registered driver (the "a datasource exists and its read failed" shape those cases always meant). Its nine assertions are unchanged.

Ablation, direction predicted before running and observed (mutation applied with scripts/ablation-replace.mjs: anchor hit once, blob changed, restored to the HEAD blob with git diff HEAD empty; the subject is src/plugin.ts imported directly, so no rebuild applies):

mutation (a) pins (b) controls existing outage pins
verdict always false (branch condition ablated) 2 RED 4 green 9 green
verdict always true 2 green 4 RED 9 green
drop the getDriverForObject probe green 2 RED (both registered-driver controls) not run
drop the resolveEffectiveDatasource probe green 1 RED (named binding) not run
drop the listUnavailableDatasources probe green 1 RED (unavailable datasource) not run

So each probe is held by its own control, and no (a) pin passes by accident.

Verification (every reading at head f273dab)

  • pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/plugin-restore-metadata-no-storage.test.ts src/plugin-restore-metadata-outage.test.ts: 15 passed.
  • pnpm --filter @objectstack/objectql test: 424 files, 8129 tests passed.
  • pnpm --filter @objectstack/objectql typecheck (after the last test edit): exit 0; tsc --noEmit clean and check:test-typecheck OK with the ledger unchanged (39 files, 233 errors held). The new test file is in tsconfig.test.json's program (1 hit) and not in tsconfig.json's (0), which is the layer that compiles tests.
  • eslint --no-inline-config on the three changed source and test files: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 64 commands, plus the 53 artifact-roster and 11 wide-population families it prints; 127 distinct commands, all run: 123 exit 0. Two exit-3 prerequisites (check:dual-build-cjs-loads, check:published-readme-exports) were rerun after a full pnpm build and are exit 0; check:type-check-debt hit my 280 s runner window on the first pass and is exit 0 on its rerun. dispatch-gates --ran reconciles 64 derived, 64 run, 0 unrun.
  • check-partof-closing-keyword needs a pull request body to judge, so it first answered exit 2 (not wired) and was rerun with this body as PR_BODY: exit 0.
  • NOT MEASURED, each for its reason: check-closing-target-claim and check-single-claim-paths (exit 2: they read the pull request and the card through the API, and a local run has no pull request yet; CI supplies it) and pm/check-governed-queue-guard (exit 1: it reads a workflow event payload; this diff touches no governed surface).

Acceptance notes

  • The three remedies are now spelled in two places: the CLI's ⚠ No storage: line and NO_STORAGE_REMEDIES here. Nothing pins them equal, and sharing one constant would cross into packages/cli, outside this card's surface. Noted, not filed (drift not yet exercised).
  • The remedies name CLI inputs (OS_DATABASE_URL, --dev). A host that boots a kernel by hand with no driver reads the same three; "mount a storage driver" is the one that applies to it.
  • The line is worded as of the failed read, as it always was. A plugin that registers a driver in its start() later does not rewrite what this process already restored.

Generated by Claude Code

…en no driver serves sys_metadata

When sys_metadata cannot be read at boot and the engine holds no datasource and
no driver, the ERROR now says no storage is configured and names the three
storage fixes the CLI's no-storage line names, instead of sending the operator
to "check the datasource behind sys_metadata". The datasource remedy stays for
every case where a datasource exists. The verdict is read from the engine's
routing, never from the error text; level and consequence are unchanged.

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 5 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 3bc407d3072565e620711d8ad2a715c8922e2610.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3bc407d3072565e620711d8ad2a715c8922e2610 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f07d7e1beaceaef0ef36d091f5d023268a5f2bd3 — the merge of head f273dab8e3133a92f52949b8aefa68ef24db2fd3 into base 3bc407d3072565e620711d8ad2a715c8922e2610, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f07d7e1beaceaef0ef36d091f5d023268a5f2bd3 && git checkout f07d7e1beaceaef0ef36d091f5d023268a5f2bd3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3bc407d3072565e620711d8ad2a715c8922e2610 f273dab8e3133a92f52949b8aefa68ef24db2fd3 && git checkout -B drift-repro 3bc407d3072565e620711d8ad2a715c8922e2610 && git merge --no-ff f273dab8e3133a92f52949b8aefa68ef24db2fd3

node scripts/docs-audit/affected-docs.mjs --json 3bc407d3072565e620711d8ad2a715c8922e2610

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3bc407d3072565e620711d8ad2a715c8922e2610 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 16:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 15, 2026 16:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 30c33e3 Oct 15, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23636-no-storage-hydration-error branch October 15, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants