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.
console: after a refused switch to an organization the user does not belong to, the reloaded header still names the previous organization while the server reports NO_ACTIVE_ORGANIZATION #12698
Filing gate ① — a user-visible state mismatch, measured once live. reach: on the stock showcase (objectstack 2034975a, console at .objectui-shadaeb77fc), a forged set-active to an organization the user is not a member of is refused; after a reload the console header still names the previous organization, while the server reports NO_ACTIVE_ORGANIZATION for the session. The header claims a workspace the server does not have active, so org-scoped screens act on a context the user cannot see is gone. The refusal itself is correct; nothing is exposed.
Found by objectstack's full platform-checklist run (objectstack-ai/objectstack#23878), identity-auth slice — report comment 6216631966 on #23878, record objectstack-ai/objectstack#23943 — where the runner listed it as an unfiled observation. Filed by the objectstack domain:devx seat 1 PM (objectstack seat post #6023) · os-sales · session_014eFvbvGK4FjXvscggDNTD4, on the maintainer's pointer that reported problems were left unfiled. ⛔ Not a claim; lane and grade are triage's. The runner did not check the mechanism against objectui main.
Reader who acts: the objectui seat that owns packages/auth / the console shell header.
Where to look first (a question for the dev, not a verified cause)
packages/auth/src/AuthProvider.tsx:635-667, refreshOrganizations: it asks the server for the active organization only if (orgs.length > 0 && !activeOrganization), and with exactly one membership it re-activates orgs[0] itself (the ADR-0081 single-membership repair). Measure first whether the header's organization comes from a client-held value (activeOrganization state, ActiveOrganizationStorage) that a server-side "no active org" answer never clears, or from that repair — and whether the repair then actually re-stamps the server session.
packages/app-shell/src/console/ConsoleShell.tsx:383-405 reads activeOrganization for the header.
Done when
After a refused switch and a reload, the header names the organization the server reports as active, or shows the no-active-organization state the shell already has (ConsoleShell.tsx:405) — never a stale one.
A pin drives the refused-switch-then-reload sequence and asserts header and server agree.
Dedupe
MCP search_issues on objectstack-ai/objectui, closed included, at this filing: header shows previous organization after refused set-active organization switch NO_ACTIVE_ORGANIZATION stale org label → 3 hits: #5719, #5731, #5750 (all closed; neighbouring organization-switch state defects, none this one).
Dedupe words: stale organization header after refused switch; NO_ACTIVE_ORGANIZATION header mismatch; set-active refused header.
Why p2: after a refused switch, the header names a workspace the server no longer has active, so org-scoped screens act on a context the user cannot see is gone. The refusal itself is right, and nothing is exposed.
Lane:domain:ui. The header reads the server's active-organization answer after reload. ⛔ It does not re-assert the cached one.
Claim: PM loop round 3 (priority:p2, area:identity; triage first grade 6218366640)
Session: session_01XVp19E1MYUTFM3dnkwz8gB
Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-12698-stale-active-org
Worktree: objectui-issue-12698
Domain: domain:ui
Seat: domain:ui#2
File surface: packages/auth/src/AuthProvider.tsx (refreshOrganizations and how the active organization is derived after a reload); packages/auth/src/ActiveOrganizationStorage.ts only if the stale value is held there; packages/app-shell/src/console/ConsoleShell.tsx only where the header reads the active organization; tests beside each; one .changeset/12698-*.md (stop on breach; explain in the report). ⛔ Not LoginForm.tsx or SocialSignInButtons.tsx (open PR objectui#12761).
Container & model: S, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui reads no path-derived mandate for these paths)
Clause-②: no
Responsibility: objectui @object-ui/auth: the client keeps or re-derives an active organization the server no longer reports (measured first: a client-held value never cleared, or the single-membership repair) | the server's own NO_ACTIVE_ORGANIZATION answer, which the shell already has a state for (ConsoleShell.tsx :405) | any member whose session loses its active organization (a refused switch, a removed membership); seen live on the stock showcase by objectstack-ai/objectstack#23878
Thread-read: 6218366640
Serial constraints cleared: scanned every open objectui PR's file list just now. The only packages/auth PR is objectui#12761 (seat 3's objectui#12682: LoginForm.tsx, SocialSignInButtons.tsx), which is disjoint from this surface. This seat's in-flight work touches the marketplace page (PR #12763) and the grid seam (PR #12756), both disjoint. Revised 2026-10-16T19:05Z (seat ruling on the dev's open question in os-dev report 6222585880): the surface adds packages/app-shell/src/layout/WorkspaceSwitcher.tsx and packages/app-shell/src/layout/CurrentOrganizationIndicator.tsx, with tests beside them. The measured cause is two client guesses: the stored id survives a server "none" (fixed in AuthProvider), and both header components render activeOrganization ?? orgList[0]. Ruling: option A. Drop the ?? orgList[0] fallback in both. With no server-active organization, the header shows the existing no-organization-context state, and org-scoped routes keep redirecting through RequireOrganization. No new copy, no new state, no export change. The same PR then closes the card. Serial check: no open objectui PR touches either component (scanned 2026-10-16T19:04Z).
Claimed at: 2026-10-16T18:43Z
{
"issue": 12698,
"status": "blocked",
"branch": "claude/issue-12698-stale-active-org",
"pr": "https://github.com/objectstack-ai/objectui/pull/12765",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB",
"premise_still_valid": true,
"summary": "Read the dispatch order (md5 e20574ae702e16fabca5481c622a1827); the newest Claim, comment 6222368818, names this branch. Root cause, measured with the real AuthProvider and the real header components against a mocked server that holds no active organization. better-auth 1.7.2 set-active nulls the session's active org BEFORE it refuses a non-member, so the server truly holds none. The client then gets two things wrong. (1) The header guesses. WorkspaceSwitcher.tsx:63 and CurrentOrganizationIndicator.tsx:61 render `activeOrganization ?? orgList[0]`. With two memberships, AuthProvider held null and the switcher still read 'Plant A'. With one membership and the repair refused, the indicator still read 'Plant B'. (2) The stored id survives. AuthProvider.refreshOrganizations heard 'none' and left ActiveOrganizationStorage = org_b, which kept stamping X-Tenant-ID and the MetadataProvider cache scope. A1 is false: activeOrganization is not hydrated from storage on load (it starts as useState(null)), and the server is asked once on reload. The ADR-0081 repair is already truthful: one set-active call, and the server's answer is adopted. Draft PR #12765 fixes (2) inside the surface: AuthProvider clears ActiveOrganizationStorage when the server-asking branch ends with no active organization, including a refused repair. A guard on activeOrgIdRef keeps an in-flight user switch from being wiped. Pins (a)-(d) are added, and the ablation turns (a) red. BLOCKED on (1): the fix is to drop the `?? orgList[0]` fallback in packages/app-shell/src/layout/WorkspaceSwitcher.tsx and CurrentOrganizationIndicator.tsx, with pins beside them. Both files are outside the dispatched file surface ('Anything else: stop and report'), so I did not edit them. The PR therefore says `Part of #12698`, not `Fixes`. That conflicts with the dispatch's `Fixes #12698`; I followed the role file's rule that a merge which leaves a half open must not close the card. Resume from claude/issue-12698-stale-active-org@d45dbf32f once the PM extends the surface.",
"tests": "All at d45dbf32f unless noted; exit codes recorded in scratchpad issue-12698/gates.txt. New pin packages/auth/src/__tests__/staleActiveOrg-12698.test.tsx: 6 passed (a two-memberships, a end-to-end forge-then-boot, b repair re-stamps once, b repair refused, c control with no extra call, d control user switch). Ablation: the clear() line was replaced by a marker. The marker grep -c went 0 to 1, numstat 1 1, and three tests went red: (a)x2 and (b-refused) failed; (b), (c) and (d) stayed green. Restore used git checkout HEAD --, the HEAD blob 8f04201076f0 matched the restored file, and git diff HEAD was empty; a trap covered EXIT INT TERM. The test imports ../AuthProvider from src, so no dist was involved. Whole package: vitest run --maxWorkers=1 packages/auth/ exit 0, 32 files, 303 tests. pnpm --filter @object-ui/auth type-check exit 0, and tsconfig.test.json lists the new test (--listFiles count 1). The first type-check exited 2 on a wrong type import; that was fixed in de1e61191. The closure build --filter '@object-ui/auth^...' matched no projects, so it ran nothing. ESLint narrowed to the 2 changed files (population per package eslint . under the root eslint.config.js; count read from --format json): 0 errors and 4 warnings, all present on base 7ead04904 shifted by +12 lines. There is no type-aware linting (no parserOptions.project or projectService), so untouched files' verdicts are invariant. Battery, all exit 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, check-hand-rolled-comment-mask.mjs, check-changeset-fixed.mjs. check:spec-floors was not run (not required). The root-cause measurement was a scratch app-shell test (real AuthProvider plus WorkspaceSwitcher and CurrentOrganizationIndicator), kept out of the tree at scratchpad issue-12698/zzMeasure12698.test.tsx. CI: in_progress, not awaited.",
"mcp_calls": "0 — no MCP GitHub tools used",
"api_writes": "3 — git push of the branch (4 pushes: empty branch, then 3 commits); fleet-write relay pr_create (POST /repos/objectstack-ai/objectui/pulls, draft, plus POST /issues/12765/assignees baozhoutao); fleet-write relay comment (POST /repos/objectstack-ai/objectui/issues/12698/comments, this report). No label writes: objectui path labels belong to labeler.yml and the dispatch named none.",
"gates": "type-check 0 · auth vitest dir 0 · pin 0 · eslint narrowed 0 (warnings = base) · pre-tsc battery 16/16 exit 0 · check-changeset-fixed 0 · closure build: no projects matched",
"files_changed": [".changeset/12698-stale-active-org.md", "packages/auth/src/AuthProvider.tsx", "packages/auth/src/__tests__/staleActiveOrg-12698.test.tsx"],
"deviations": "PR body says `Part of #12698` instead of the dispatch's `Fixes #12698`, because the header half is unshipped (role file Definition of done). I ran `git stash list` once, a read-only listing; no stash was created. A temporary copy of base AuthProvider was linted inside packages/auth/src for the warning comparison and deleted at once; it was never staged or committed.",
"open_questions": [
{
"question": "Extend #12698's file surface to packages/app-shell/src/layout/WorkspaceSwitcher.tsx and CurrentOrganizationIndicator.tsx (plus tests beside them), and decide what the header shows with memberships but no server-active organization.",
"options": [
"A: drop the `?? orgList[0]` fallback in both components and render nothing when activeOrganization is null. WorkspaceSwitcher's existing 'No organization context' branch already does this. Org-scoped routes redirect to /organizations through RequireOrganization. No new copy, no export change, about 2 lines plus pins.",
"B: drop the fallback, but have WorkspaceSwitcher (two or more memberships) render a neutral 'Choose organization' trigger with the list. This keeps the switch affordance on /organizations and /, but it adds an i18n key in en and zh plus a new state, about 30 lines.",
"C: leave the header as is. The card's done-when stays unmet."
],
"recommendation": "A. Real business need: the measured defect is the header naming an org the server does not hold, and A removes exactly that; nothing pulls for a new trigger state yet. Long-term soundness: the server is the authority, and A deletes a client guess instead of reshaping it. Preventing AI-authoring errors: nothing in metadata changes either way, and A removes a lenient consumer fallback (`??`). Startup focus: A adds no surface or copy, while B grows chrome without demand."
}
],
"out_of_scope_findings": [
"carrier: whoever takes the header half of #12698 · refreshOrganizations skips the server when the membership list is empty, so a stored id with zero memberships is not cleared; not measured as reachable · noted in PR Acceptance notes, not filed"
]
}
PR #12765 merged through the merge queue as 6e141d9, which is on main. Verified by content: all 7 of the PR's files on origin/main are blob-identical to the accepted head a405599. The squash commit's message names no model.
What landed: after a refused organization switch and a reload, the console follows the server's answer.
AuthProvider clears the stored organization id when the server reports no active organization, so the old id no longer stamps X-Tenant-ID or keys the metadata cache.
The header's switcher and current-organization indicator name only the server-active organization. They no longer fall back to the first membership.
The ADR-0081 single-membership repair is unchanged: it was already truthful.
Acceptance note, not filed: a stored id with zero memberships is not cleared, because refreshOrganizations skips the server call then. It was not measured as reachable.
pm:dispatched leaves with this note. The card closed completed on merge.
Activity
objectstack-fleet commentedon Oct 16, 2026
Triage: first grade,
bug·priority:p2·domain:ui·area:identity·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T15:03ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
domain:ui. The header reads the server's active-organization answer after reload. ⛔ It does not re-assert the cached one.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 3 (
priority:p2,area:identity; triage first grade6218366640)Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12698-stale-active-orgWorktree:
objectui-issue-12698Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/auth/src/AuthProvider.tsx(refreshOrganizationsand how the active organization is derived after a reload);packages/auth/src/ActiveOrganizationStorage.tsonly if the stale value is held there;packages/app-shell/src/console/ConsoleShell.tsxonly where the header reads the active organization; tests beside each; one.changeset/12698-*.md(stop on breach; explain in the report). ⛔ NotLoginForm.tsxorSocialSignInButtons.tsx(open PR objectui#12761).Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate for these paths)Clause-②: no
Responsibility:
objectui @object-ui/auth: the client keeps or re-derives an active organization the server no longer reports (measured first: a client-held value never cleared, or the single-membership repair) | the server's own NO_ACTIVE_ORGANIZATION answer, which the shell already has a state for (ConsoleShell.tsx :405) | any member whose session loses its active organization (a refused switch, a removed membership); seen live on the stock showcase by objectstack-ai/objectstack#23878Thread-read: 6218366640
Serial constraints cleared: scanned every open objectui PR's file list just now. The only
packages/authPR is objectui#12761 (seat 3's objectui#12682:LoginForm.tsx,SocialSignInButtons.tsx), which is disjoint from this surface. This seat's in-flight work touches the marketplace page (PR #12763) and the grid seam (PR #12756), both disjoint.Revised 2026-10-16T19:05Z (seat ruling on the dev's open question in os-dev report
6222585880): the surface addspackages/app-shell/src/layout/WorkspaceSwitcher.tsxandpackages/app-shell/src/layout/CurrentOrganizationIndicator.tsx, with tests beside them. The measured cause is two client guesses: the stored id survives a server "none" (fixed inAuthProvider), and both header components renderactiveOrganization ?? orgList[0]. Ruling: option A. Drop the?? orgList[0]fallback in both. With no server-active organization, the header shows the existing no-organization-context state, and org-scoped routes keep redirecting throughRequireOrganization. No new copy, no new state, no export change. The same PR then closes the card. Serial check: no open objectui PR touches either component (scanned 2026-10-16T19:04Z).Claimed at: 2026-10-16T18:43Z
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{ "issue": 12698, "status": "blocked", "branch": "claude/issue-12698-stale-active-org", "pr": "https://github.com/objectstack-ai/objectui/pull/12765", "session": "session_01XVp19E1MYUTFM3dnkwz8gB", "premise_still_valid": true, "summary": "Read the dispatch order (md5 e20574ae702e16fabca5481c622a1827); the newest Claim, comment 6222368818, names this branch. Root cause, measured with the real AuthProvider and the real header components against a mocked server that holds no active organization. better-auth 1.7.2 set-active nulls the session's active org BEFORE it refuses a non-member, so the server truly holds none. The client then gets two things wrong. (1) The header guesses. WorkspaceSwitcher.tsx:63 and CurrentOrganizationIndicator.tsx:61 render `activeOrganization ?? orgList[0]`. With two memberships, AuthProvider held null and the switcher still read 'Plant A'. With one membership and the repair refused, the indicator still read 'Plant B'. (2) The stored id survives. AuthProvider.refreshOrganizations heard 'none' and left ActiveOrganizationStorage = org_b, which kept stamping X-Tenant-ID and the MetadataProvider cache scope. A1 is false: activeOrganization is not hydrated from storage on load (it starts as useState(null)), and the server is asked once on reload. The ADR-0081 repair is already truthful: one set-active call, and the server's answer is adopted. Draft PR #12765 fixes (2) inside the surface: AuthProvider clears ActiveOrganizationStorage when the server-asking branch ends with no active organization, including a refused repair. A guard on activeOrgIdRef keeps an in-flight user switch from being wiped. Pins (a)-(d) are added, and the ablation turns (a) red. BLOCKED on (1): the fix is to drop the `?? orgList[0]` fallback in packages/app-shell/src/layout/WorkspaceSwitcher.tsx and CurrentOrganizationIndicator.tsx, with pins beside them. Both files are outside the dispatched file surface ('Anything else: stop and report'), so I did not edit them. The PR therefore says `Part of #12698`, not `Fixes`. That conflicts with the dispatch's `Fixes #12698`; I followed the role file's rule that a merge which leaves a half open must not close the card. Resume from claude/issue-12698-stale-active-org@d45dbf32f once the PM extends the surface.", "tests": "All at d45dbf32f unless noted; exit codes recorded in scratchpad issue-12698/gates.txt. New pin packages/auth/src/__tests__/staleActiveOrg-12698.test.tsx: 6 passed (a two-memberships, a end-to-end forge-then-boot, b repair re-stamps once, b repair refused, c control with no extra call, d control user switch). Ablation: the clear() line was replaced by a marker. The marker grep -c went 0 to 1, numstat 1 1, and three tests went red: (a)x2 and (b-refused) failed; (b), (c) and (d) stayed green. Restore used git checkout HEAD --, the HEAD blob 8f04201076f0 matched the restored file, and git diff HEAD was empty; a trap covered EXIT INT TERM. The test imports ../AuthProvider from src, so no dist was involved. Whole package: vitest run --maxWorkers=1 packages/auth/ exit 0, 32 files, 303 tests. pnpm --filter @object-ui/auth type-check exit 0, and tsconfig.test.json lists the new test (--listFiles count 1). The first type-check exited 2 on a wrong type import; that was fixed in de1e61191. The closure build --filter '@object-ui/auth^...' matched no projects, so it ran nothing. ESLint narrowed to the 2 changed files (population per package eslint . under the root eslint.config.js; count read from --format json): 0 errors and 4 warnings, all present on base 7ead04904 shifted by +12 lines. There is no type-aware linting (no parserOptions.project or projectService), so untouched files' verdicts are invariant. Battery, all exit 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, check-hand-rolled-comment-mask.mjs, check-changeset-fixed.mjs. check:spec-floors was not run (not required). The root-cause measurement was a scratch app-shell test (real AuthProvider plus WorkspaceSwitcher and CurrentOrganizationIndicator), kept out of the tree at scratchpad issue-12698/zzMeasure12698.test.tsx. CI: in_progress, not awaited.", "mcp_calls": "0 — no MCP GitHub tools used", "api_writes": "3 — git push of the branch (4 pushes: empty branch, then 3 commits); fleet-write relay pr_create (POST /repos/objectstack-ai/objectui/pulls, draft, plus POST /issues/12765/assignees baozhoutao); fleet-write relay comment (POST /repos/objectstack-ai/objectui/issues/12698/comments, this report). No label writes: objectui path labels belong to labeler.yml and the dispatch named none.", "gates": "type-check 0 · auth vitest dir 0 · pin 0 · eslint narrowed 0 (warnings = base) · pre-tsc battery 16/16 exit 0 · check-changeset-fixed 0 · closure build: no projects matched", "files_changed": [".changeset/12698-stale-active-org.md", "packages/auth/src/AuthProvider.tsx", "packages/auth/src/__tests__/staleActiveOrg-12698.test.tsx"], "deviations": "PR body says `Part of #12698` instead of the dispatch's `Fixes #12698`, because the header half is unshipped (role file Definition of done). I ran `git stash list` once, a read-only listing; no stash was created. A temporary copy of base AuthProvider was linted inside packages/auth/src for the warning comparison and deleted at once; it was never staged or committed.", "open_questions": [ { "question": "Extend #12698's file surface to packages/app-shell/src/layout/WorkspaceSwitcher.tsx and CurrentOrganizationIndicator.tsx (plus tests beside them), and decide what the header shows with memberships but no server-active organization.", "options": [ "A: drop the `?? orgList[0]` fallback in both components and render nothing when activeOrganization is null. WorkspaceSwitcher's existing 'No organization context' branch already does this. Org-scoped routes redirect to /organizations through RequireOrganization. No new copy, no export change, about 2 lines plus pins.", "B: drop the fallback, but have WorkspaceSwitcher (two or more memberships) render a neutral 'Choose organization' trigger with the list. This keeps the switch affordance on /organizations and /, but it adds an i18n key in en and zh plus a new state, about 30 lines.", "C: leave the header as is. The card's done-when stays unmet." ], "recommendation": "A. Real business need: the measured defect is the header naming an org the server does not hold, and A removes exactly that; nothing pulls for a new trigger state yet. Long-term soundness: the server is the authority, and A deletes a client guess instead of reshaping it. Preventing AI-authoring errors: nothing in metadata changes either way, and A removes a lenient consumer fallback (`??`). Startup focus: A adds no surface or copy, while B grows chrome without demand." } ], "out_of_scope_findings": [ "carrier: whoever takes the header half of #12698 · refreshOrganizations skips the server when the membership list is empty, so a stored id with zero memberships is not cleared; not measured as reachable · noted in PR Acceptance notes, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landed ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-16T19:45ZPR #12765 merged through the merge queue as
6e141d9, which is onmain. Verified by content: all 7 of the PR's files onorigin/mainare blob-identical to the accepted heada405599. The squash commit's message names no model.What landed: after a refused organization switch and a reload, the console follows the server's answer.
AuthProviderclears the stored organization id when the server reports no active organization, so the old id no longer stampsX-Tenant-IDor keys the metadata cache.Acceptance note, not filed: a stored id with zero memberships is not cleared, because
refreshOrganizationsskips the server call then. It was not measured as reachable.pm:dispatchedleaves with this note. The card closedcompletedon merge.Generated by Claude Code
fix(auth): drop the stored active organization when the server report…