Skip to content

The three kernel:ready migrations in assembleMetadataProtocol never arm on a self-hosted boot — the standalone stack stamps environmentId = 'proj_local', and the gate asks for undefined #9380

Description

@os-zhuang

Found while implementing #8928 (the duplicate-identifier report), where the question "does running this command repair the install first?" had to be answered by measurement rather than by reading. The answer turned out to be no, and the reason is this defect rather than anything about that command.

The gate, and what stands on it

assembleMetadataProtocol (packages/metadata-protocol/src/plugin.ts) arms three migrations on kernel:ready, all inside one if (environmentId === undefined) block:

The gate's own comment says what it is for: "platform / standalone kernels own their local sys_metadata; per-project (cloud) kernels source metadata from the control plane and must NOT provision these tables locally." So standalone is meant to be on the inside of this gate.

Measured: standalone is on the outside

packages/runtime/src/standalone-stack.ts line 515:

constenvironmentId=cfg.environmentId??process.env.OS_ENVIRONMENT_ID??'proj_local';

Every boot that goes through createStandaloneStackos dev, os serve, os start, and the whole os migrate family — therefore reaches assembleMetadataProtocol with environmentId = 'proj_local', not undefined, and the block never runs.

Measured on origin/main @ 90c5285cb, by instrumenting the built @objectstack/metadata-protocol and booting the real standalone stack against a SQLite fixture carrying the #8686 damage (a __global__ counter at 38 beside an org_x counter at 4, untenanted seed rows, one organization row):

INFO Protocol service registered [MP-JS] envId="proj_local" hookType=function
BEFORE sequences: [{crm_case,__global__,38},{crm_case,org_x,4}]
AFTER sequences: [{crm_case,__global__,38},{crm_case,org_x,4}]
VERDICT: boot mutated the pre-repair evidence = false

Two controls, so this is not a fixture that simply cannot be repaired:

  1. calling backfillSeedTenancy(resolveSeedTenancyExec(ql)) by hand on the same booted engine returns status: 'applied', stamps the movable rows, merges the counter to 38 and deletes the __global__ row;
  2. a probe plugin registering its own kernel:ready handler on the same boot fires and repairs — so ctx.hook works, kernel:ready is triggered by kernel.bootstrap(), and the seam is usable at that moment. Only the shipped registration is missing.

Why it matters

For #8686 specifically: its own header calls the kernel:ready migration the half that "repairs an install that is ALREADY in that state, which covers every existing deployment". On a self-hosted install it covers none. The only surviving path is the sys_organization-create handoff in runtime/src/app-plugin.ts, which fires when an organization is first created — i.e. the fresh-install half. An install that already had its organization when it upgraded gets neither, and keeps minting duplicates.

The same reasoning applies to the two index migrations beside it.

Not fixed here, deliberately

#8928's file surface is packages/cli/src/commands/**, and the fix is a decision about a gate two packages away — whether the standalone default should be undefined, whether the gate should test something other than environmentId (what it actually wants is "is this kernel the owner of its local platform tables"), or whether the three migrations should be armed from a different seam. Worth noting that flipping this on changes what a plain os migrate plan does to a database: those three migrations would then run on every one-shot CLI boot, so whichever way it is fixed, the read-only commands need to be considered in the same breath.

Verification hooks that already exist: packages/cli/src/commands/migrate/duplicates.integration.test.ts (from #8928) boots the real stack over a fixture carrying this exact damage and asserts the database is unchanged afterwards. When this gate is fixed, that assertion is where the change in behaviour will surface first.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions