Repository navigation
Commit fa00ebf
fix(runtime): a self-hosted restart reads the kernel's own sys_metadata back into the registry (#20100)
Fixes #20071
Clause-②: no
## What was wrong
`createStandaloneStack` (`packages/runtime/src/standalone-stack.ts`) is
the composition behind `os dev` / `os serve` / `os start`. It stamps
`environmentId: 'env_local'` (or whatever `OS_ENVIRONMENT_ID` names),
and it built `new ObjectQLPlugin({ environmentId, runPlatformMigrations
})` with no `hydrateMetadataFromDb`.
`ObjectQLPlugin.start()` hydrates only `if (this.environmentId ===
undefined || this.hydrateMetadataFromDb)`
(`packages/objectql/src/plugin.ts:767`). So every self-hosted boot:
- skipped reading `sys_metadata`;
- logged "Project kernel — skipping sys_metadata hydration (metadata
sourced from artifact)".
An object published at runtime then answered `404 OBJECT_NOT_FOUND` on
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 for
`runPlatformMigrations`.
## The fix: a declaration, not a deduction
- `createStandaloneStack` constructs `new ObjectQLPlugin({
environmentId, runPlatformMigrations: cfg.runPlatformMigrations ?? true,
hydrateMetadataFromDb: true })`. The `[#20071]` comment at that line
carries the caution check below.
- `StandaloneStackConfigSchema` and the published
`StandaloneStackConfig` type are unchanged.
- The only other edit in the file: #9380's docblock pointer "line ~567
below" now reads "`createStandaloneStack` below".
- `packages/objectql/**` and `packages/spec/**` are untouched.
Opt-out: none. No caller of `createStandaloneStack` can make either
clause of the caution false; adding a key later is its own card,
declared `Clause-②: 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 AND `sys_metadata` lives on the kernel's own local
driver (no control-plane proxy)". Both clauses are properties of
`createStandaloneStack` itself, not of its caller.
1. **Per-instance isolated registry: holds.**
- The function constructs a fresh `ObjectQLPlugin` and passes no `ql`,
so `init()` runs `this.ql = new ObjectQL(hostCtx)` (`plugin.ts:397`).
- Each `ObjectQL` owns 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.
2. **`sys_metadata` on the kernel's own local driver: holds.**
- `SysMetadataObject`
(`packages/metadata-core/src/objects/sys-metadata.object.ts:17`)
declares no `datasource`, so it routes to the default driver.
- The function composes exactly one datasource,
`DefaultDatasourcePlugin` as `default` (ADR-0062 D1: "every unbound
object routes to it").
- Every `databaseDriver` kind the function dispatches (`memory`,
`sqlite`, `sqlite-wasm`, `postgres`, `mysql`, `mongodb`, `turso`) is a
direct driver, never a control-plane proxy.
- The hydration read is `this.engine.find('sys_metadata', { where: {
state: 'active', organization_id: null } })` (`loadMetaFromDb` in
`packages/metadata-protocol/src/protocol.ts`), through that same engine.
## Stop valve: every caller of `createStandaloneStack`, measured
`git grep createStandaloneStack` at `a4ca69a9` finds three production
callers, plus tests and fixtures:
| caller | what it is | `sys_metadata` | hydrates after this PR |
|:--|:--|:--|:--|
| `packages/cli/src/commands/serve.ts:2740` | `os serve` / `os dev` /
`os start` with a config | the stack's own `default` driver | yes
(declared) |
| `packages/runtime/src/default-host.ts:163` (`createDefaultHostConfig`)
| artifact-only boot | same | yes (declared) |
| `packages/cli/src/utils/schema-migrate.ts:303` (`bootSchemaStack`) |
the one-shot funnel for 13 `os migrate *` / `os meta *` commands | same
| yes (declared), see below |
| runtime and cli tests and fixtures | memory or sqlite files | same |
yes (declared) |
No caller has a proxied or non-local `sys_metadata`, so the stop valve
does not fire.
Outside this repository:
- An org-wide code search for `createStandaloneStack` returns only
`objectstack-ai/duly`: tests and seed scripts of an ordinary standalone
app on its own database.
- `objectstack-ai/cloud` is **NOT MEASURED**. Control leg: the same
search for `hydrateMetadataFromDb` returns 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.
- Either way, both caution clauses hold inside the function, so an
unmeasured embedder gets the same composition.
## Why `bootSchemaStack` hydrates too
Ruling ① 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.ts` header) says the question
"is only answerable when the composed object set actually MIRRORS what
this deployment's `os serve` boot registers". After this PR, `os serve`
registers 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.
- **The read writes nothing:** `loadMetaFromDb` is `engine.find` plus
registry registration plus log lines. A boot that defers DDL still
defers the Phase-3 tables of what it hydrated.
- **Measured at `c945f282`:** `duplicates.integration.test.ts` (boot
included, database byte-identical after the run),
`platform-migrations-arming.integration.test.ts` and the six
`schema-migrate*` / `unmanaged-tables` integration suites all pass. The
stack's construction is the same `true` at 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 `else` of the gate above, and
`hydrateMetadataFromDb` is a literal `true` that no caller can change.
The new unit test pins the flag under every way an environment id is
stamped.
Observed directly:
- on the unfixed source, the unit test's two boots each printed that
line once;
- on the fix, the second boot prints `Metadata restored from database to
SchemaRegistry {"loaded":2,"errors":0,"invalid":0}` and no skip line.
No edit to `packages/objectql` was needed.
## Reach beyond one object (mechanism assumption 6)
`packages/runtime/src/standalone-stack-hydrate-metadata.test.ts` boots
the real stack twice on one SQLite file:
- **Boot 1** writes an env-wide `object` row **and** an env-wide `app`
row through the protocol, and inserts one record.
- **Boot 2** has both rows in the registry, and the record reads back
through the engine.
The `app` is 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. `loadMetaFromDb` reads
`organization_id IS NULL` only (ADR-0005: per-org overlays are served on
demand).
Diagnostics hydration now surfaces at boot, reported and not suppressed.
`loadMetaFromDb` and its `reportUnhydratableOrgScopedRows` (#6190) now
run on every standalone boot:
- a stored row that cannot register prints `[Protocol]
[metadata_field_type_refused] …` at `error`, or `[Protocol] Failed to
hydrate TYPE/NAME: …` / `[Protocol] [metadata_spec_invalid] …` at
`warn`;
- org-scoped rows of types that are not per-org overridable get one
aggregated line.
None of these fired in any suite run here. Against a real install's
`sys_metadata` they are NOT MEASURED; the changeset names these lines
for upgraders.
## The pin
`packages/cli/test/package-restart-acceptance.integration.test.ts`:
probe 2's `it.fails` is promoted to a plain `it`, 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
serve` does 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.ts` gives `Tests 5
passed (5)`. `ablation-dist-preflight` shows the runtime `dist` carries
the literal.
- **Unit test:** `pnpm --filter @objectstack/runtime exec vitest run
--maxWorkers=2 src/standalone-stack-hydrate-metadata.test.ts` gives
`Tests 2 passed (2)`.
- **Runtime suite:** `pnpm --filter @objectstack/runtime test` gives
`Test Files 279 passed (279)`, `Tests 3906 passed | 1 skipped (3907)`.
- **Typecheck:** `pnpm --filter @objectstack/runtime typecheck` exits 0,
with `check: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
--commands` derives at `46fd71b1` exit 0. The `--ran` reconciliation
reads "60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN".
- **At `c945f282`**, before this revision:
- the CLI unit project: `Test Files 224 passed (224)`, `Tests 3158
passed (3158)`;
- `pnpm --filter @objectstack/cli typecheck`: exit 0;
- CLI `--project integration` on 10 files (the pin, `duplicates`,
`meta.stored-flow-resolution`, `platform-migrations-arming`, the five
`schema-migrate*` files and `unmanaged-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's `true` to `false` through `node
scripts/ablation-replace.mjs`, anchored on the construction line.
- **Mutation landed:** anchor x1 to x0, blob `b10937d6` to `dc0eab77`,
and on-disk counts `true=0 false=1`.
- **Reached `dist`:** after rebuilding `@objectstack/runtime`,
`ablation-dist-preflight` reports the `true` spelling absent from all 6
dist files and the flipped spelling present in 2.
- **Unit test:** 2 failed. The restart row fails at "hydr_widget is
registered after the restart: expected undefined", the registry check
just before its data read.
- **Pin:** `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".
- **Restore:**
- blob `b10937d6` == HEAD, and `git status --porcelain` is empty;
- runtime rebuilt: the literal is present in 2 dist files and `false`
absent from all 6;
- the unit test gives 2 passed and the pin 5 passed.
## Acceptance notes (observations, not filed)
- **Same deduction, non-object write-through:**
`packages/metadata-protocol/src/protocol.ts` `applyRegistryWriteThrough`
returns early for every non-object type when `this.environmentId !==
undefined`. That is the same deduction class, one package over. On a
standalone kernel, a runtime-saved `app` is 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.
- **Comment drift:** `packages/objectql/src/plugin.ts` (the Phase-2
bridge comment) and `loadMetaFromDb`'s comment still call
`SchemaRegistry` a process-wide singleton, while `engine.ts:3054-3061`
says 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](https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent aa04ea2 commit fa00ebf
4 files changed
Lines changed: 296 additions & 34 deletions
File tree
- .changeset
- packages
- cli/test
- runtime/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
Lines changed: 24 additions & 31 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | | - | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| |||
377 | 377 | | |
378 | 378 | | |
379 | 379 | | |
380 | | - | |
381 | | - | |
382 | | - | |
383 | | - | |
384 | | - | |
385 | | - | |
386 | | - | |
387 | | - | |
388 | | - | |
389 | | - | |
390 | | - | |
391 | | - | |
392 | | - | |
393 | | - | |
394 | | - | |
395 | | - | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
396 | 389 | | |
Lines changed: 186 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
223 | 223 | | |
224 | 224 | | |
225 | 225 | | |
226 | | - | |
227 | | - | |
| 226 | + | |
| 227 | + | |
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
| |||
754 | 754 | | |
755 | 755 | | |
756 | 756 | | |
757 | | - | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
758 | 793 | | |
759 | 794 | | |
760 | 795 | | |
| |||
0 commit comments