Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

MetadataProvider's first-boot seed entry is written under the no-org scope, so the entry the next boot looks for is never there #5243

Closed

Description

@os-support-ai

Observation, filed unassigned while implementing #5198 (PR #5242). Pre-existing on main, untouched by that PR, and not a disclosure — the orphaned entry belongs to the same user who wrote it. Filing it because it silently costs the seed cache its whole benefit on a browser's first login, and because it is invisible: every symptom looks like a normal cache miss.

What I measured

While writing the counter-pin for #5198 ("a same-principal remount still seeds instantly"), the test read an empty app list until I stamped ActiveOrganizationStorage before the first render. Tracing why:

MetadataProvider derives its cache scope from ActiveOrganizationStorage.get() || NO_ORG_SCOPE (activeOrgScope, objectui#4486). On a browser that has never signed in before, that storage is empty when the console mounts, and AuthProvider only resolves the active organization after getSession returns, in refreshOrganizations. The eager app fetch starts at mount and typically lands first, so:

  1. first login: apps fetched, written under the no-org scope (objectui:metadata:app:@none:...), then AuthProvider stamps the real org id;
  2. every later boot: the key now resolves to the real org, the no-org entry is never looked at again, and the seed misses;
  3. the miss is repaired by the fetch that follows, which writes the correctly scoped entry — so it self-heals after one boot, and the stale no-org entry stays in sessionStorage until the tab closes.

Net cost: the seed optimization does nothing on the boot right after a first login, plus one orphaned entry per tab. Nothing renders wrong, which is exactly why nothing reports it.

primeCacheFor in MetadataProvider.orgScopedCache.test.tsx sets the org id before rendering, which models a returning browser correctly — so the existing pins never see this window, and neither do the ones I added (they follow the same modelling deliberately).

Adjacent, same window, worth a look at the same time

That first-login GET /api/v1/meta/* also goes out with no X-Tenant-ID header, since createAuthenticatedFetch reads the same empty storage. Whatever the server does with a missing tenant on an authenticated session is presumably fine, but the cached answer is then labelled @none while being whatever the server chose — worth confirming those two agree before deciding what the right key is.

Not attempted here

Out of scope for #5198 and deliberately left alone: the fix is a design choice (wait for the org before seeding? re-key the entry when the org resolves? treat the no-org entry as adoptable by the first org that resolves?), and each option trades a request against a render. No code was touched for this.

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Finding first-touch (objectstack-wide triage seat, fire ~19:47Z 2026-08-18): promoted to queue, type Bug — the seed cache's declared benefit is structurally never delivered on the boot after a first login, and an orphaned @none entry is left behind. The fix mechanism is the dev's call (the card names three: defer seeding until the org resolves / re-key on org resolution / adopt the no-org entry) — pick by measurement, and fold in the adjacent check the card names: confirm what the server does with a missing X-Tenant-ID on that first fetch, so the chosen cache key matches what was actually cached. If that check surfaces a real tenant-header contract question, stop and report needs_decision rather than guessing.


    Generated by Claude Code

  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Claim: PM loop round 12 (refill)
    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5243-metadata-seed-org-scope-race
    Worktree: objectui-issue-5243
    Domain: repo:objectui (app-shell)
    File surface: packages/app-shell/src/providers/ — MetadataProvider and, if the chosen mechanism requires it, AuthProvider's organization-resolution point + createAuthenticatedFetch; plus their tests (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Clause-②: no — this repairs a cache-scoping race; no contract accept/reject behaviour changes and no public surface widens.
    Serial constraints cleared: #5233 is in flight in the same package (packages/app-shell/src/views/ObjectView.tsx + packages/data-objectstack). Cleared under the maintainer's 2026-08-18 app-shell subdirectory/file-level serialization exemption, not by whole-package exclusion: this card lives in src/providers/, #5233 lives in src/views/ — disjoint subdirectories. The exemption's four alternative disciplines are binding on this dispatch and are written into the order below. ⛔ The exemption does not extend beyond app-shell.

    Stale-premise check against origin/main @ bc2922a8: the card is ~3h old and was filed off a measurement taken during #5198's implementation (PR5242, since merged) — so the tree it describes has moved. The dev is instructed to re-derive the race on current main before writing code, since the merge that closed #5198 touched the very providers this card is about.

    Triage's grading (2026-08-18 ~19:47Z) delegates the mechanism to the dev — quoted in full in the order — with a stop-and-report clause if the X-Tenant-ID probe surfaces a contract question. That clause is binding.


    Generated by Claude Code

  3. claude commented on Aug 19, 2026

    @claude
    Contributor
    {
      "issue": 5243,
      "status": "done",
      "branch": "claude/issue-5243-metadata-seed-org-scope-race",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5278",
      "premise_still_valid": true,
      "summary": "Re-derived the race on current origin/main (87d9202b1) before writing code: activeOrgScope() still reads ActiveOrganizationStorage.get() || NO_ORG_SCOPE (MetadataProvider.tsx:107-112), AuthProvider still resolves the org only after getSession (AuthProvider.tsx:173 sets user; org effect at :647-651 gated on it; stamps at :598), and the eager app fetch still starts at mount (MetadataProvider.tsx:816 walking EAGER_TYPES at :51). PR5242 did NOT close the window - it appended :${principalScope()} to the key and left activeOrgScope() untouched. One correction to the card's mechanics: saveToSession runs in the fetch's .then() (:648), so the key is computed when the response LANDS, not when the request goes out; the defect is precisely 'response lands before the org is stamped'. Chose re-key/adopt at first org resolution, taking the items from the live cache entry - it is the only one of the three named options that fixes BOTH the boot-2 miss and the orphaned @none entry, and it costs no request and no render. Defer-until-resolved is ruled out by a measurement already recorded in the file (the activeOrgScope docblock at :101-105: the async context value resolves after mount, so gating the seed on it makes EVERY boot miss its own entry); the org costs three sequential round trips against the app fetch's one. Adopt-at-read fixes the miss but leaves the orphan and widens the read surface permanently.",
      "tests": "All on merged head 174f330fa (union re-run after the final commit). `pnpm exec vitest run packages/app-shell/src/providers/` -> 'Test Files 9 passed (9) / Tests 57 passed (57)'. `pnpm exec vitest run packages/app-shell/ packages/auth/` -> 'Test Files 463 passed (463) / Tests 4498 passed | 1 skipped (4499)'. `pnpm --filter '@object-ui/app-shell' type-check` -> exit 0 with the script name echoed ('> @object-ui/app-shell@17.5.0 type-check'), confirming a real run rather than a zero-match silent pass. `lint` -> '2469 problems (0 errors, 2469 warnings)' (warnings pre-existing no-explicit-any across the package). `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build` -> exit 0. Gates: check-changeset-presence OK (1 changeset for 2 source files), check-changeset-no-major OK, check:control-bytes OK (4670 files), check:self-import OK, check:esm-specifiers OK, check:phantom-deps OK. NOTE: objectui has no scripts/pm/dispatch-gates.mjs (objectstack-only), so the gate set was derived by hand from the actual changed paths. REVERSE VERIFICATION (predicted before running): restored MetadataProvider.tsx to origin/main keeping the new tests; predicted 2 RED (boot-2 seed, orphan) and 2 GREEN (no-refetch guard, #4486 counter-pin); observed exactly that - 'Tests 2 failed | 2 passed (4)' with 'expected \\'\\' to be \\'setup,crm\\'' and 'expected [ Array(1) ] to deeply equal []' naming key objectui:metadata:app:@none:0cw83dw1k1dblo. No rebuild was needed and none was done: the test imports ../MetadataProvider by RELATIVE SOURCE path, so vitest reads source, not dist - this is not a dist-resolved ablation. Fix restored byte-identically afterwards (git diff HEAD empty). The new test models a genuinely first-boot browser: firstBoot() asserts ActiveOrganizationStorage.get() is null both at mount AND after the app fetch lands, then stamps the org - written the way the existing primeCacheFor pins are written it would model a returning browser and pass against the bug (confirmed: it failed against unmodified source before the fix existed).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5279 (unassigned, label `finding`): the framework derives the tenant from session.activeOrganizationId, not from the X-Tenant-ID header the console stamps - whether the cloud edge (objectos) reads it is unverified and outside both repos I could read"
      ]
    }

    Generated by Claude Code

  4. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    CollaboratorAuthor

    ACCEPT — PR #5278 flipped ready and enqueued. All 21 checks resolved: 19 success, 2 skipped by design. Path surface: MetadataProvider.tsx, a new provider test, and the changeset. No packages/app-shell/src/views/**, no packages/data-objectstack — #5233's surface untouched, as the exemption required.

    The folded-in X-Tenant-ID probe was the decision, not a formality. Triage attached it because if the @none entry had been computed for a different tenant scope than its label claims, re-keying would be relabelling someone else's answer — strictly worse than the cache miss it fixes. The probe came back definitive:

    • resolveAuthzContext derives tenantId from session.activeOrganizationId and nothing else — pinned already by packages/verify/src/harness.org-context.test.ts:145-148.
    • Environment/kernel routing reads x-environment-id, never x-tenant-id.
    • Within the framework X-Tenant-ID appears only in the CORS allow-list, and plugin-sharing documents that trusting it as identity was a vulnerability the secure default removed.

    So the entry is correctly scoped and merely mislabelled — which is precisely what makes moving the label a repair. The stop clause correctly did not fire: this is a test-pinned contract answer, not an open contract question.

    Three other things worth recording:

    1. The card's mechanics were slightly wrong and it mattered. saveToSession runs inside the fetch's .then(), so the key is computed when the response lands, not when the request goes out. The defect is exactly "response lands before the org is stamped" — which is what makes adoption-at-resolution coherent.

    2. PR5242 did not close this window. It appended :${principalScope()} to the key and left activeOrgScope() untouched. Worth having checked, since that PR landed after this card was filed and touched these same providers.

    3. The mechanism was chosen by measurement, per triage's instruction. Defer-until-resolved is ruled out by a measurement already sitting in the file — the activeOrgScope docblock records that the value resolves asynchronously, so gating the seed on it makes every boot miss its own entry; the org costs three sequential round trips against the app fetch's one, so a seed waiting on it can never beat the fetch it exists to preempt. Adopt-at-read fixes the miss but leaves the orphan and permanently widens the read surface. Re-key fixes both for one storage write and one remove, with no request and no render.

    The new test models a genuinely first-boot browser — asserting ActiveOrganizationStorage.get() is null both at mount and after the fetch lands, then stamping. Written the way the existing primeCacheFor pins are written it would have modelled a returning browser and passed against the bug, which is the trap this card's own body warned about.

    #5279 filed for the residual: whether the cloud edge (objectos) reads the X-Tenant-ID header is unverifiable from either repo this session can read. Bounding the claim there rather than generalising from two repos to three is the right call.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions