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.
[seam -> repo:objectui] the 80% / 100% storage banner has no renderer — cloud serves the numbers and the verdict, nothing shows them #10439
Filed by the repo:cloud execution PM seat (seat post objectstack#6026), session session_01TAUTP6Yky8QWoHUAPDKNJQ, at 2026-09-16T07:58Z, as the objectui half of cloud#2135. ⛔ Not graded or routed here beyond the lane label — that is the objectui seat's.
⚠️ Filed in this repo rather than in objectstack-ai/objectui because that repository is not reachable to this session's credential; this is the established seam carrier for that case (objectstack#18377 is the same shape).
What exists already, and what does not
The per-environment storage cap (产品负责人 adjudication 2026-09-08: Free 1 GB / Team 20 GB per environment; intercept attachment upload and bulk import only) is implemented and enforced on the cloud side:
GET /api/v1/usage/storage serves storageUsedMb / storageLimitMb and the same verdict that refuses an upload — served by StorageUsageEndpointPlugin, registered at packages/objectos-runtime/src/objectos-stack.ts:580.
An over-limit POST /api/v1/storage/upload/presigned answers 403 STORAGE_LIMIT_REACHED with an upgrade envelope. Proven end to end on the assembled host stack by cloud PR docs(skill): add the first automation eval — ADR-0044 revise loop objectstack#2300, including an ablation: with the guardrail removed the same request answers 200 with a real upload URL and a record is created on a full environment.
What has no renderer is the adjudication's warning half:
提醒:≥ 80% 环境管理员横幅「已用 X / Y」;≥ 100% 横幅升级为「已满,上传与导入已暂停」+ 升级入口。
cloud PR objectstack-ai/objectstack#2143's own landing note records that the banner renders in objectui and was out of scope there, and the cloud#2135 dev found no cloud-side owner for it. ⇒ Today a Free environment crosses 80% with nothing shown, and hits 100% by discovering that an upload fails.
What this card asks
An environment-admin banner driven by GET /api/v1/usage/storage:
≥ 80% — 「已用 X / Y」, informational.
≥ 100% — 「已满,上传与导入已暂停」 plus the upgrade entry point.
Visible to environment administrators. ⛔ Not to every member — the adjudication says 环境管理员.
⭐ Read the verdict, do not re-derive the threshold. The endpoint serves the same verdict the guardrail refuses with, precisely so that what the banner shows is what will block an upload. A UI that recomputes used/limit >= 0.8 locally will drift from the enforcement point the first time the rule changes, and the drift will be silent in the direction users notice last.
⛔ Out of scope here
The cloud side. The numbers, the endpoint, the verdict and the refusal are all landed and measured; ⛔ nothing in this card should change them. If the endpoint is missing a field the UI needs, that is a cloud card — say so rather than computing around it.
Provenance
cloud#2135 (the parent, whose runtime half is complete) · cloud#2131 (epic) · cloud PR objectstack-ai/objectstack#2143 (control plane + runtime) · cloud PR objectstack-ai/objectstack#2300 (the end-to-end proof) · docs/design/pricing.md Data volume row.
Session session_01X7HwfPLpQtCixDMrRGkSbe, acting on the maintainer instruction 「objectui 你负责迁移。fleet 中继不支持 GitHub 的 issue transfer, 应该立 skills 卡解决」 (2026-09-23).
Routing rule 1 (cross-repo-coordination): this card's fix lands entirely in objectstack-ai/objectui, and nothing is left on the objectstack side, so it moves there. The fleet relay cannot transfer issues yet: there is no transfer op, and the minted token covers a single repo. The capability is filed as objectstack-ai/objectstack#19884. This card will be transferred rather than rebuilt, so its thread and history travel with it. pm:queue → pm:blocked until then. ⛔ Do not dispatch it here.
Migrated from objectstack-ai/objectstack#18387 — pm:blocked → pm:queue, lane domain:ui · 2026-09-25T01:07Z
Acting on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「迁移卡 帮我处理」; where — the maintainer's chat with session session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-25. ⛔ Not a claim and not a dispatch.
Why it moved: the migration comment on this thread (session session_01X7HwfPLpQtCixDMrRGkSbe, 2026-09-23) routed the card here under cross-repo routing rule 1: the fix lands entirely in objectui. It was parked only on the transfer door, Blocked-by: #19884.
Labels on arrival:repo:objectui has no counterpart in this repo, so the platform dropped it. pm:blocked is replaced with pm:queue: its only condition was the transfer. domain:ui is added because the landing site is objectui's published library/apps (the ui stream). Type and priority carried over unchanged.
Claim: PM loop round 1
Session: session_015AUunPkX7UTkCH9e7AdZo1
Account: os-zhuang (the seat's linked user as GET /user answers it; the maintainer authorised this seat to run on it, verbatim 「允许你用当前账户继续开发」, seat-post marker 5864093687)
Branch: claude/issue-10439-storage-usage-banner
Worktree: objectui-issue-10439
Domain: domain:ui
Seat: domain:ui#4
File surface: packages/app-shell/src/hooks/useReadRateReading.ts (or a sibling hook in packages/app-shell/src/hooks/ if the endpoint read is shared), a new banner under packages/app-shell/src/layout/, its mount in packages/app-shell/src/console/ConsoleShell.tsx, packages/app-shell/src/hooks/index.ts, the app-shell README's read-rate section, tests beside each, new console.* rows in packages/i18n/src/locales/*.ts, one .changeset/10439-*.md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (default judgement tier; dispatch-gates --tier --repo objectstack-ai/objectui on these paths: no path-derived mandate)
Clause-②: no
Thread-read: 5824965929
Serial constraints cleared: no in-flight claim or open PR touches the read-rate hook, ReadRateBanner.tsx or ConsoleShell.tsx (claims on the 14 open lane pm:dispatched cards and the files of all 7 open objectui PRs read at 2026-09-28T05:34Z). Same-package in-flight work is on other files: objectui#8524 (this seat, useAiUsage.ts / AiUsageIndicator.tsx; disjoint), objectui#7483, objectui#10519, objectui#8725, objectui#10887. The i18n locale files are shared with objectui#8524, PR objectui#10896 and objectui#10866: ordinary concurrency, merge main before opening the PR.
Take order: a maintainer pointer, verbatim 「#8524 和 #10439 需要读 cloud 仓 你现在有权限了」 (the chat of session_015AUunPkX7UTkCH9e7AdZo1, 2026-09-28).
Clause-② reasoning:ReadRateBanner and useReadRateReading are not re-exported from the @object-ui/app-shell entry (src/index.ts, the . export), and the storage banner takes the same route: mounted by ConsoleShell, no new entry export. So no published surface widens. If the implementation needs a new entry export, this claim flips to yes and the dev reports it. The at-tier contract review is owed before enqueue anyway, on the changeset prose.
Cloud readings, objectstack-ai/cloudmain96eb092
The wire shape of GET /api/v1/usage/storage (packages/objectos-runtime/src/storage-usage-endpoint-plugin.ts, quoted):
exportinterfaceStorageUsageResponse{state: 'ok'|'warning'|'exhausted'|'unlimited'|'unknown';usedMb?: number;// absent when never meteredlimitMb?: number;// 0 = unlimited, absent = unknownfraction?: number;// usedMb / limitMb clamped to [0, 1]; absent when unknown or unlimitedwarn: boolean;// show the capacity banner (80% and up)blocked: boolean;// uploads and bulk imports are currently refusedwarnFraction: number;// the line `warn` flips at (STORAGE_WARN_FRACTION = 0.8)readRate?: ReadRateBannerReading;// already consumed by objectui's ReadRateBanner}
Correction to the card body:storageUsedMb / storageLimitMb are the registry-row fields the handler reads. The wire keys are usedMb / limitMb.
The verdict is served whole (state, warn, blocked, all from evaluateStorageQuota, the same verdict the upload guardrail refuses with), which is the card's 「read the verdict, do not re-derive the threshold」.
Every miss answers 200 with { state: 'unknown', warn: false, blocked: false, warnFraction: 0.8 }.
The handler does no role check. The environment-administrator audience is the client's to enforce, the way ReadRateBanner already gates on useWorkspaceAdminStatus.
This response carries no upgrade flag. Only the 403 STORAGE_LIMIT_REACHED envelope does (upgrade: true, storage-limit-guardrail-plugin.ts).
Premise re-measured on objectui main6a7f24e:git grep -c -E "usedMb|warnFraction|STORAGE_LIMIT_REACHED|limitMb" -- packages apps → 0. Control usage/storage → hits in useReadRateReading.ts (3), ReadRateBanner.tsx (1), the hook's test (3) and the README (1). So objectui already reads this endpoint for the read-rate half, and nothing renders the storage half.
Delivery order: as on objectui#8524, the tenant runtime image bundles objectui at cloud's .objectui-sha. Against a runtime without this route, the fetch fails soft, the same way the read-rate hook's does today.
{
"issue": 10439,
"status": "done",
"branch": "claude/issue-10439-storage-usage-banner",
"pr": "#10910",
"session": "session_015AUunPkX7UTkCH9e7AdZo1 (subagent: the parent PM seat id; container CLAUDE_CODE_REMOTE_SESSION_ID cse_015AUunPkX7UTkCH9e7AdZo1)",
"premise_still_valid": true,
"head": "efa30fc",
"summary": "Added the environment-admin storage-capacity banner, driven only by the verdict the tenant runtime serves on GET /api/v1/usage/storage. blocked renders "Storage is full: uploads and imports are paused" with the used/limit MB and an upgrade link to cloudConsoleUrl() (left out when that is empty). warn renders the used/limit MB. ok, unknown, unlimited, non-2xx and off-contract payloads render nothing. classifyStorageUsage never compares usedMb with limitMb or fraction with warnFraction. New pieces: useStorageUsageReading (contract-first parse of the flat storage half) and StorageUsageBanner, mounted in ConsoleShell beside ReadRateBanner behind the same useWorkspaceAdminStatus gate. readStorageUsage is a shared in-flight reader, so both banners cost one request. useReadRateReading now reads through it, with its three-way no-banner distinction and all existing tests unchanged. Also added: five console.storageUsage.* keys in all ten locale packs, a README subsection, and a minor changeset for app-shell and i18n. No package-entry export, so Clause-2: no holds. Claim 5864158793 names this branch. Premises re-measured: wire keys are usedMb/limitMb, and objectui main had zero readers of the storage half (target grep exit 1, control usage/storage 4 files). No cloud field gap: StorageLimitRefusal declares details.upgrade as literal true, and an unlimited plan never reaches blocked, so the upgrade entry needs no plan field. useWorkspaceAdminStatus (org owner/admin membership, admin user.role, isPlatformAdmin rung, positions org_owner/org_admin) is the gate ReadRateBanner documents for the same audience, and cloud defines no narrower tenant-console role.",
"tests": "At HEAD efa30fc, after merging origin/main twice (9f0c84a, then 7ea8118), all heavy runs through os-verify-lock. turbo run build --filter=@object-ui/app-shell^... --concurrency=2: exit 0, 28/28 tasks. pnpm --filter @object-ui/app-shell type-check: exit 0; --listFiles shows both new test files in tsconfig.test.json. pnpm exec vitest run --reporter=verbose over 28 targets (the 4 touched test files, cloudConsoleUrl-7253, the 22 files that render ConsoleShell, packages/i18n/): exit 0, Test Files 99 passed (99), Tests 1404 passed (1404). Per file: useStorageUsageReading 18/18, StorageUsageBanner 9/9, useReadRateReading 9/9 and ReadRateBanner 10/10 (unchanged), all-locales-key-parity 32/32, entry-locale-shape-7479 8/8, consoleUserPreferenceBudget 3/3. Ablation via objectstack scripts/ablation-replace.mjs in WRAP mode; anchor hit x1 to x0 and the blob changed on disk; afterwards the blob equalled HEAD and git diff HEAD was empty; no dist leg, since the tests import src by relative path. (1) Re-derive from usedMb/limitMb at 0.8: 3 failed / 15 passed (verdict pin, classifier table, and the unlimited plan, where division by a zero limit would raise a banner). (2) Drop the in-flight sharing: 1 failed / 17 passed (the one-request pin). Both went red, as expected beforehand.",
"gates": [
{
"command": "turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 && pnpm --filter @object-ui/app-shell type-check && pnpm exec vitest run (28 targets)",
"exit": 0,
"head": "efa30fc"
},
{
"command": "pnpm exec eslint --format json (19 touched .ts/.tsx files)",
"exit": 0,
"note": "0 errors, 10 warnings (9 already present at base in the two modified files, 1 new set-state-in-effect matching the read-rate hook), 0 ignored"
},
{
"command": "pnpm check:i18n-keys",
"exit": 0
},
{
"command": "pnpm check:i18n-drift",
"exit": 0
},
{
"command": "pnpm check:i18n-dead-keys",
"exit": 0
},
{
"command": "pnpm check:eager-locale-catalogues",
"exit": 2,
"note": "PREREQUISITE NOT MET (needs a built apps/console/dist bundle) -- NOT MEASURED"
},
{
"command": "pnpm check:readme-exports",
"exit": 1,
"note": "86 self-imports unjudgeable (unbuilt dist across packages) -- NOT MEASURED; the README diff adds no fenced block and no import binding"
},
{
"command": "pnpm check:vi-mock-specifiers",
"exit": 0
},
{
"command": "pnpm check:vi-mock-inherit",
"exit": 0
},
{
"command": "pnpm check:vi-mock-override-shape",
"exit": 0
},
{
"command": "pnpm check:test-path-roots",
"exit": 0
},
{
"command": "pnpm check:self-import",
"exit": 0
},
{
"command": "pnpm check:phantom-deps",
"exit": 0
},
{
"command": "pnpm check:unreferenced-sources",
"exit": 0
},
{
"command": "pnpm check:new-line-citations",
"exit": 0
},
{
"command": "pnpm check:control-bytes",
"exit": 0
},
{
"command": "pnpm check:changeset-claims",
"exit": 0
},
{
"command": "pnpm check:pending-changeset-literals",
"exit": 0
},
{
"command": "node scripts/check-changeset-presence.mjs",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs",
"exit": 0
},
{
"command": "node scripts/check-governed-queue-guard.mjs --test (21 changed paths)",
"exit": 0,
"note": "NOT GOVERNED"
}
],
"files_changed": [
".changeset/10439-storage-usage-banner.md",
"packages/app-shell/README.md",
"packages/app-shell/src/console/ConsoleShell.tsx",
"packages/app-shell/src/hooks/tests/useStorageUsageReading.test.tsx",
"packages/app-shell/src/hooks/index.ts",
"packages/app-shell/src/hooks/storageUsageEndpoint.ts",
"packages/app-shell/src/hooks/useReadRateReading.ts",
"packages/app-shell/src/hooks/useStorageUsageReading.ts",
"packages/app-shell/src/layout/StorageUsageBanner.tsx",
"packages/app-shell/src/layout/tests/StorageUsageBanner.test.tsx",
"packages/app-shell/src/layout/index.ts",
"packages/i18n/src/locales/ar.ts",
"packages/i18n/src/locales/de.ts",
"packages/i18n/src/locales/en.ts",
"packages/i18n/src/locales/es.ts",
"packages/i18n/src/locales/fr.ts",
"packages/i18n/src/locales/ja.ts",
"packages/i18n/src/locales/ko.ts",
"packages/i18n/src/locales/pt.ts",
"packages/i18n/src/locales/ru.ts",
"packages/i18n/src/locales/zh.ts"
],
"deviations": [
"Lint narrowed and declared. Package-scoped lint (pnpm --filter app-shell --filter i18n lint) hit queue-timeout (exit 99) twice while sibling objectui#8524 held the lock about 21 min for a full app-shell vitest run (pid 12765, alive, about 225% CPU). Ran eslint on the 19 touched ts/tsx files instead. Evidence: (1) the population is ESLint own --format json output, 19 files, 0 ignored; (2) the count is read from that JSON; (3) root eslint.config.js sets no parserOptions.project or projectService, loads no import-resolving plugin, and no local rule reads the disk, so no untouched file verdict can move.",
"check:eager-locale-catalogues NOT MEASURED (needs a built console bundle). The diff only adds rows inside existing catalogue literals and touches no import, locales/index.ts or registry.ts. The source-shape pin entry-locale-shape-7479 passed 8/8.",
"check:readme-exports NOT MEASURED (exit 1 from unjudgeable self-imports across unbuilt packages). The README diff adds no fenced block or import binding.",
"The file surface also includes packages/app-shell/src/hooks/storageUsageEndpoint.ts and packages/app-shell/src/layout/index.ts. Both are covered by the claim: "a sibling hook in hooks/ if the endpoint read is shared" and "a new banner under layout/".",
"resolveRuntimeApiBase moved into the shared reader and is re-exported from useReadRateReading.ts so the barrel and the existing tests stay unchanged.",
"The PR body footer uses the os-dev contract form (Generated by ... session URL), not the harness reminder form. The os-dev file says harness attribution reminders yield to it."
],
"mcp_calls": "0 -- no MCP GitHub tool was called",
"api_writes": "3 REST writes, all through the fleet relay (each is one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create POST /repos/objectstack-ai/objectui/pulls (draft #10910, relay run 36387948884); assign via label-write.mjs on /repos//issues/10910 (os-zhuang, relay run 36388017546, read-back MATCHES); comment POST /repos//issues/10439/comments (this os-dev-report). Also 5 git pushes to the branch (not REST). REST reads: issue 10439 and its comments, and the pulls list by head.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: exception: release-text · evidence: pending .changeset/9954-read-rate-banner.md says "useReadRateReading ... and ReadRateBanner ... Both are exported from @object-ui/app-shell". The package entry (packages/app-shell/src/index.ts, the only exports target) names neither: git grep -c -E "ReadRate" on that file finds nothing (exit 1); control: git grep -c ReadRateBanner on src/layout/index.ts finds 1. The text publishes verbatim into the CHANGELOG at release. carrier: whoever next edits that changeset (none claimed); for the seat to file or edit. · dedupe words: 9954 changeset exported claim, ReadRateBanner not exported, read-rate changeset package entry",
"carrier: 承接者:无 · noted, not filed: no content/docs/guide page covers the console chrome banners (read-rate, impersonation, now storage); the app-shell README is the only doc surface (AGENTS.md #2 asks for both). In PR Acceptance notes.",
"carrier: 承接者:无 · noted, not filed: cloudConsoleUrl() lands on the control plane origin, not a billing or upgrade page. That is the standing objectui#7253 limit (no control-plane-declared upgrade URL for tenant SPAs). In PR Acceptance notes."
]
}
domain:ui seat 4, session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T07:17Z. Checked against GitHub, origin/main and cloud main, not against the report.
Form: draft, base main, body first line Fixes #10439, and no other closing keyword in the body. 21 files, +989/−33, all inside the claim's file surface (the new storageUsageEndpoint.ts is the shared reader the claim allowed; layout/index.ts gains one sub-barrel line). Nothing under content/docs/releases/. Changeset .changeset/10439-storage-usage-banner.md: @object-ui/app-shell minor and @object-ui/i18n minor, no major.
CI on the head efa30fcfe8: 43 check-runs, 40 success and 3 skipped (conditional jobs), none failed. mergeable_state: clean. Governed-surface guard (scripts/check-governed-queue-guard.mjs --test, 21 paths): NOT GOVERNED. Size is well under 5000 lines.
Contract review: PASS at CONTRACT_REVIEW_TIER on this head, 5865258297. It verified Clause-②: no at the head itself: the package entry has no wildcard re-export and names none of the new symbols, and exports is . and ./styles.css only.
Spot readings (seat's own):classifyStorageUsage reads the served warn / blocked only, never fraction or warnFraction, which honours the card's 「read the verdict, do not re-derive the threshold」. The parse is contract-strict on the wire keys usedMb / limitMb. The banner is gated on useWorkspaceAdminStatus (enabled: isAdmin), so a non-admin session issues no request. The upgrade link appears only at blocked. The zh copy 「已用 X MB / Y MB」 and 「存储已满,上传与导入已暂停」 matches the adjudication text. The blocked body says reading, exporting and single-record edits still work; the seat checked cloud's storage-limit-guardrail-plugin.ts, which refuses only bulk import and the attachment-upload family, so that is true. The read-rate hook now shares one in-flight request and keeps its three-way no-banner distinction; its tests are unchanged and green.
No content/docs guide page covers the console chrome banners: Acceptance notes, not filed (a docs gap, outside the three filing classes).
cloudConsoleUrl() lands on the control-plane origin rather than a billing page: Acceptance notes, not filed (the standing objectui#7253 limit; the producer publishes no upgrade URL).
Downstream: as with objectui#8524, the storage banner reaches cloud environments only when cloud's .objectui-sha moves past this merge. That rides on the existing cloud pin card objectstack-ai/cloud#2441; a pointer is posted there.
Next: ready → auto-merge (merge queue), followed to MERGED by this seat.
What stops it: the required Spec Main Shape Gate fails every objectui merge group. Signature: packages/types/src/__tests__/report-chart-query-spec-parity.test.ts line 747, TS1360: Type 'true' does not satisfy the expected type 'false'., compiled against objectstack df3ba164a588 (merge-group commit d84888ebde, run 36391738955). This PR does not touch packages/types.
Activity
objectstack-fleet commentedon Sep 23, 2026
Migration to objectstack-ai/objectui — waiting on the transfer door · 2026-09-23T14:57Z
Blocked-by: objectstack-ai/objectstack#19884
Session
session_01X7HwfPLpQtCixDMrRGkSbe, acting on the maintainer instruction 「objectui 你负责迁移。fleet 中继不支持 GitHub 的 issue transfer, 应该立 skills 卡解决」 (2026-09-23).Routing rule 1 (cross-repo-coordination): this card's fix lands entirely in objectstack-ai/objectui, and nothing is left on the objectstack side, so it moves there. The fleet relay cannot transfer issues yet: there is no
transferop, and the minted token covers a single repo. The capability is filed as objectstack-ai/objectstack#19884. This card will be transferred rather than rebuilt, so its thread and history travel with it.pm:queue→pm:blockeduntil then. ⛔ Do not dispatch it here.Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
Migrated from objectstack-ai/objectstack#18387 —
pm:blocked→pm:queue, lanedomain:ui· 2026-09-25T01:07ZActing on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「迁移卡 帮我处理」; where — the maintainer's chat with session
session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-25. ⛔ Not a claim and not a dispatch.session_01X7HwfPLpQtCixDMrRGkSbe, 2026-09-23) routed the card here under cross-repo routing rule 1: the fix lands entirely in objectui. It was parked only on the transfer door,Blocked-by: #19884.transferop (GraphQLtransferIssue, token minted for source + target) and anissue-transferdoor, so cards filed in the wrong repo move with their history instead of being rebuilt objectstack#19884 closedcompletedon 2026-09-23, adding the fleet-writetransferop. This card was moved withscripts/pm/issue-transfer.mjsthrough the relay, so its thread and history travel with it, and the old URL redirects here.repo:objectuihas no counterpart in this repo, so the platform dropped it.pm:blockedis replaced withpm:queue: its only condition was the transfer.domain:uiis added because the landing site is objectui's published library/apps (the ui stream). Type and priority carried over unchanged.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 1
Session:
session_015AUunPkX7UTkCH9e7AdZo1Account:
os-zhuang(the seat's linked user asGET /useranswers it; the maintainer authorised this seat to run on it, verbatim 「允许你用当前账户继续开发」, seat-post marker5864093687)Branch:
claude/issue-10439-storage-usage-bannerWorktree:
objectui-issue-10439Domain:
domain:uiSeat:
domain:ui#4File surface:
packages/app-shell/src/hooks/useReadRateReading.ts(or a sibling hook inpackages/app-shell/src/hooks/if the endpoint read is shared), a new banner underpackages/app-shell/src/layout/, its mount inpackages/app-shell/src/console/ConsoleShell.tsx,packages/app-shell/src/hooks/index.ts, the app-shell README's read-rate section, tests beside each, newconsole.*rows inpackages/i18n/src/locales/*.ts, one.changeset/10439-*.md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default judgement tier;dispatch-gates --tier --repo objectstack-ai/objectuion these paths: no path-derived mandate)Clause-②: no
Thread-read: 5824965929
Serial constraints cleared: no in-flight claim or open PR touches the read-rate hook,
ReadRateBanner.tsxorConsoleShell.tsx(claims on the 14 open lanepm:dispatchedcards and the files of all 7 open objectui PRs read at 2026-09-28T05:34Z). Same-package in-flight work is on other files: objectui#8524 (this seat,useAiUsage.ts/AiUsageIndicator.tsx; disjoint), objectui#7483, objectui#10519, objectui#8725, objectui#10887. The i18n locale files are shared with objectui#8524, PR objectui#10896 and objectui#10866: ordinary concurrency, mergemainbefore opening the PR.Take order: a maintainer pointer, verbatim 「#8524 和 #10439 需要读 cloud 仓 你现在有权限了」 (the chat of
session_015AUunPkX7UTkCH9e7AdZo1, 2026-09-28).Clause-② reasoning:
ReadRateBanneranduseReadRateReadingare not re-exported from the@object-ui/app-shellentry (src/index.ts, the.export), and the storage banner takes the same route: mounted byConsoleShell, no new entry export. So no published surface widens. If the implementation needs a new entry export, this claim flips toyesand the dev reports it. The at-tier contract review is owed before enqueue anyway, on the changeset prose.Cloud readings,
objectstack-ai/cloudmain96eb092The wire shape of
GET /api/v1/usage/storage(packages/objectos-runtime/src/storage-usage-endpoint-plugin.ts, quoted):storageUsedMb/storageLimitMbare the registry-row fields the handler reads. The wire keys areusedMb/limitMb.state,warn,blocked, all fromevaluateStorageQuota, the same verdict the upload guardrail refuses with), which is the card's 「read the verdict, do not re-derive the threshold」.{ state: 'unknown', warn: false, blocked: false, warnFraction: 0.8 }.ReadRateBanneralready gates onuseWorkspaceAdminStatus.upgradeflag. Only the 403STORAGE_LIMIT_REACHEDenvelope does (upgrade: true,storage-limit-guardrail-plugin.ts).Premise re-measured on objectui
main6a7f24e:git grep -c -E "usedMb|warnFraction|STORAGE_LIMIT_REACHED|limitMb" -- packages apps→ 0. Controlusage/storage→ hits inuseReadRateReading.ts(3),ReadRateBanner.tsx(1), the hook's test (3) and the README (1). So objectui already reads this endpoint for the read-rate half, and nothing renders the storage half.Delivery order: as on objectui#8524, the tenant runtime image bundles objectui at cloud's
.objectui-sha. Against a runtime without this route, the fetch fails soft, the same way the read-rate hook's does today.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{
"issue": 10439,
"status": "done",
"branch": "claude/issue-10439-storage-usage-banner",
"pr": "#10910",
"session": "session_015AUunPkX7UTkCH9e7AdZo1 (subagent: the parent PM seat id; container CLAUDE_CODE_REMOTE_SESSION_ID cse_015AUunPkX7UTkCH9e7AdZo1)",
"premise_still_valid": true,
"head": "efa30fc",
"summary": "Added the environment-admin storage-capacity banner, driven only by the verdict the tenant runtime serves on GET /api/v1/usage/storage. blocked renders "Storage is full: uploads and imports are paused" with the used/limit MB and an upgrade link to cloudConsoleUrl() (left out when that is empty). warn renders the used/limit MB. ok, unknown, unlimited, non-2xx and off-contract payloads render nothing. classifyStorageUsage never compares usedMb with limitMb or fraction with warnFraction. New pieces: useStorageUsageReading (contract-first parse of the flat storage half) and StorageUsageBanner, mounted in ConsoleShell beside ReadRateBanner behind the same useWorkspaceAdminStatus gate. readStorageUsage is a shared in-flight reader, so both banners cost one request. useReadRateReading now reads through it, with its three-way no-banner distinction and all existing tests unchanged. Also added: five console.storageUsage.* keys in all ten locale packs, a README subsection, and a minor changeset for app-shell and i18n. No package-entry export, so Clause-2: no holds. Claim 5864158793 names this branch. Premises re-measured: wire keys are usedMb/limitMb, and objectui main had zero readers of the storage half (target grep exit 1, control usage/storage 4 files). No cloud field gap: StorageLimitRefusal declares details.upgrade as literal true, and an unlimited plan never reaches blocked, so the upgrade entry needs no plan field. useWorkspaceAdminStatus (org owner/admin membership, admin user.role, isPlatformAdmin rung, positions org_owner/org_admin) is the gate ReadRateBanner documents for the same audience, and cloud defines no narrower tenant-console role.",
"tests": "At HEAD efa30fc, after merging origin/main twice (9f0c84a, then 7ea8118), all heavy runs through os-verify-lock. turbo run build --filter=@object-ui/app-shell^... --concurrency=2: exit 0, 28/28 tasks. pnpm --filter @object-ui/app-shell type-check: exit 0; --listFiles shows both new test files in tsconfig.test.json. pnpm exec vitest run --reporter=verbose over 28 targets (the 4 touched test files, cloudConsoleUrl-7253, the 22 files that render ConsoleShell, packages/i18n/): exit 0, Test Files 99 passed (99), Tests 1404 passed (1404). Per file: useStorageUsageReading 18/18, StorageUsageBanner 9/9, useReadRateReading 9/9 and ReadRateBanner 10/10 (unchanged), all-locales-key-parity 32/32, entry-locale-shape-7479 8/8, consoleUserPreferenceBudget 3/3. Ablation via objectstack scripts/ablation-replace.mjs in WRAP mode; anchor hit x1 to x0 and the blob changed on disk; afterwards the blob equalled HEAD and git diff HEAD was empty; no dist leg, since the tests import src by relative path. (1) Re-derive from usedMb/limitMb at 0.8: 3 failed / 15 passed (verdict pin, classifier table, and the unlimited plan, where division by a zero limit would raise a banner). (2) Drop the in-flight sharing: 1 failed / 17 passed (the one-request pin). Both went red, as expected beforehand.",
"gates": [
{
"command": "turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 && pnpm --filter @object-ui/app-shell type-check && pnpm exec vitest run (28 targets)",
"exit": 0,
"head": "efa30fc"
},
{
"command": "pnpm exec eslint --format json (19 touched .ts/.tsx files)",
"exit": 0,
"note": "0 errors, 10 warnings (9 already present at base in the two modified files, 1 new set-state-in-effect matching the read-rate hook), 0 ignored"
},
{
"command": "pnpm check:i18n-keys",
"exit": 0
},
{
"command": "pnpm check:i18n-drift",
"exit": 0
},
{
"command": "pnpm check:i18n-dead-keys",
"exit": 0
},
{
"command": "pnpm check:eager-locale-catalogues",
"exit": 2,
"note": "PREREQUISITE NOT MET (needs a built apps/console/dist bundle) -- NOT MEASURED"
},
{
"command": "pnpm check:readme-exports",
"exit": 1,
"note": "86 self-imports unjudgeable (unbuilt dist across packages) -- NOT MEASURED; the README diff adds no fenced block and no import binding"
},
{
"command": "pnpm check:vi-mock-specifiers",
"exit": 0
},
{
"command": "pnpm check:vi-mock-inherit",
"exit": 0
},
{
"command": "pnpm check:vi-mock-override-shape",
"exit": 0
},
{
"command": "pnpm check:test-path-roots",
"exit": 0
},
{
"command": "pnpm check:self-import",
"exit": 0
},
{
"command": "pnpm check:phantom-deps",
"exit": 0
},
{
"command": "pnpm check:unreferenced-sources",
"exit": 0
},
{
"command": "pnpm check:new-line-citations",
"exit": 0
},
{
"command": "pnpm check:control-bytes",
"exit": 0
},
{
"command": "pnpm check:changeset-claims",
"exit": 0
},
{
"command": "pnpm check:pending-changeset-literals",
"exit": 0
},
{
"command": "node scripts/check-changeset-presence.mjs",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs",
"exit": 0
},
{
"command": "node scripts/check-governed-queue-guard.mjs --test (21 changed paths)",
"exit": 0,
"note": "NOT GOVERNED"
}
],
"files_changed": [
".changeset/10439-storage-usage-banner.md",
"packages/app-shell/README.md",
"packages/app-shell/src/console/ConsoleShell.tsx",
"packages/app-shell/src/hooks/tests/useStorageUsageReading.test.tsx",
"packages/app-shell/src/hooks/index.ts",
"packages/app-shell/src/hooks/storageUsageEndpoint.ts",
"packages/app-shell/src/hooks/useReadRateReading.ts",
"packages/app-shell/src/hooks/useStorageUsageReading.ts",
"packages/app-shell/src/layout/StorageUsageBanner.tsx",
"packages/app-shell/src/layout/tests/StorageUsageBanner.test.tsx",
"packages/app-shell/src/layout/index.ts",
"packages/i18n/src/locales/ar.ts",
"packages/i18n/src/locales/de.ts",
"packages/i18n/src/locales/en.ts",
"packages/i18n/src/locales/es.ts",
"packages/i18n/src/locales/fr.ts",
"packages/i18n/src/locales/ja.ts",
"packages/i18n/src/locales/ko.ts",
"packages/i18n/src/locales/pt.ts",
"packages/i18n/src/locales/ru.ts",
"packages/i18n/src/locales/zh.ts"
],
"deviations": [
"Lint narrowed and declared. Package-scoped lint (pnpm --filter app-shell --filter i18n lint) hit queue-timeout (exit 99) twice while sibling objectui#8524 held the lock about 21 min for a full app-shell vitest run (pid 12765, alive, about 225% CPU). Ran eslint on the 19 touched ts/tsx files instead. Evidence: (1) the population is ESLint own --format json output, 19 files, 0 ignored; (2) the count is read from that JSON; (3) root eslint.config.js sets no parserOptions.project or projectService, loads no import-resolving plugin, and no local rule reads the disk, so no untouched file verdict can move.",
"check:eager-locale-catalogues NOT MEASURED (needs a built console bundle). The diff only adds rows inside existing catalogue literals and touches no import, locales/index.ts or registry.ts. The source-shape pin entry-locale-shape-7479 passed 8/8.",
"check:readme-exports NOT MEASURED (exit 1 from unjudgeable self-imports across unbuilt packages). The README diff adds no fenced block or import binding.",
"The file surface also includes packages/app-shell/src/hooks/storageUsageEndpoint.ts and packages/app-shell/src/layout/index.ts. Both are covered by the claim: "a sibling hook in hooks/ if the endpoint read is shared" and "a new banner under layout/".",
"resolveRuntimeApiBase moved into the shared reader and is re-exported from useReadRateReading.ts so the barrel and the existing tests stay unchanged.",
"The PR body footer uses the os-dev contract form (Generated by ... session URL), not the harness reminder form. The os-dev file says harness attribution reminders yield to it."
],
"mcp_calls": "0 -- no MCP GitHub tool was called",
"api_writes": "3 REST writes, all through the fleet relay (each is one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create POST /repos/objectstack-ai/objectui/pulls (draft #10910, relay run 36387948884); assign via label-write.mjs on /repos//issues/10910 (os-zhuang, relay run 36388017546, read-back MATCHES); comment POST /repos//issues/10439/comments (this os-dev-report). Also 5 git pushes to the branch (not REST). REST reads: issue 10439 and its comments, and the pulls list by head.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: exception: release-text · evidence: pending .changeset/9954-read-rate-banner.md says "useReadRateReading ... and ReadRateBanner ... Both are exported from
@object-ui/app-shell". The package entry (packages/app-shell/src/index.ts, the only exports target) names neither: git grep -c -E "ReadRate" on that file finds nothing (exit 1); control: git grep -c ReadRateBanner on src/layout/index.ts finds 1. The text publishes verbatim into the CHANGELOG at release. carrier: whoever next edits that changeset (none claimed); for the seat to file or edit. · dedupe words: 9954 changeset exported claim, ReadRateBanner not exported, read-rate changeset package entry","carrier: 承接者:无 · noted, not filed: no content/docs/guide page covers the console chrome banners (read-rate, impersonation, now storage); the app-shell README is the only doc surface (AGENTS.md #2 asks for both). In PR Acceptance notes.",
"carrier: 承接者:无 · noted, not filed: cloudConsoleUrl() lands on the control plane origin, not a billing or upgrade page. That is the standing objectui#7253 limit (no control-plane-declared upgrade URL for tenant SPAs). In PR Acceptance notes."
]
}
objectstack-fleet commentedon Sep 28, 2026
ACCEPT — PR #10910 @
efa30fcdomain:uiseat 4,session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T07:17Z. Checked against GitHub,origin/mainand cloudmain, not against the report.main, body first lineFixes #10439, and no other closing keyword in the body. 21 files, +989/−33, all inside the claim's file surface (the newstorageUsageEndpoint.tsis the shared reader the claim allowed;layout/index.tsgains one sub-barrel line). Nothing undercontent/docs/releases/. Changeset.changeset/10439-storage-usage-banner.md:@object-ui/app-shellminor and@object-ui/i18nminor, nomajor.efa30fcfe8: 43 check-runs, 40successand 3skipped(conditional jobs), none failed.mergeable_state: clean. Governed-surface guard (scripts/check-governed-queue-guard.mjs --test, 21 paths): NOT GOVERNED. Size is well under 5000 lines.CONTRACT_REVIEW_TIERon this head,5865258297. It verifiedClause-②: noat the head itself: the package entry has no wildcard re-export and names none of the new symbols, andexportsis.and./styles.cssonly.classifyStorageUsagereads the servedwarn/blockedonly, neverfractionorwarnFraction, which honours the card's 「read the verdict, do not re-derive the threshold」. The parse is contract-strict on the wire keysusedMb/limitMb. The banner is gated onuseWorkspaceAdminStatus(enabled: isAdmin), so a non-admin session issues no request. The upgrade link appears only atblocked. Thezhcopy 「已用 X MB / Y MB」 and 「存储已满,上传与导入已暂停」 matches the adjudication text. Theblockedbody says reading, exporting and single-record edits still work; the seat checked cloud'sstorage-limit-guardrail-plugin.ts, which refuses only bulk import and the attachment-upload family, so that is true. The read-rate hook now shares one in-flight request and keeps its three-way no-banner distinction; its tests are unchanged and green..changeset/9954-read-rate-banner.mdclaims the read-rate pair is exported from the package entry: filed finding(changeset): the pending9954-read-rate-bannerchangeset saysReadRateBanneranduseReadRateReadingare exported from@object-ui/app-shell; the package entry exports neither #10913 (the seat confirmed it false:ReadRatecount 0 insrc/index.ts, control 2).content/docsguide page covers the console chrome banners: Acceptance notes, not filed (a docs gap, outside the three filing classes).cloudConsoleUrl()lands on the control-plane origin rather than a billing page: Acceptance notes, not filed (the standing objectui#7253 limit; the producer publishes no upgrade URL)..objectui-shamoves past this merge. That rides on the existing cloud pin card objectstack-ai/cloud#2441; a pointer is posted there.Next: ready → auto-merge (merge queue), followed to MERGED by this seat.
Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Parked on an external gate:
pm:dispatched→pm:blocked, work completedomain:uiseat 4,session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T07:45Z. The claim and the assignee stand: this seat still owns the landing.efa30fc, with a PASS contract review atCONTRACT_REVIEW_TIERand every check on its own head green or skipped.Spec Main Shape Gatefails every objectui merge group. Signature:packages/types/src/__tests__/report-chart-query-spec-parity.test.tsline 747,TS1360: Type 'true' does not satisfy the expected type 'false'., compiled against objectstackdf3ba164a588(merge-group commitd84888ebde, run36391738955). This PR does not touchpackages/types.JoinedReportBlockSchema, and the objectui pin tripped as its own docblock intends. The anchor card is finding(types): the Spec Main Shape Gate is red on every PR and in the merge queue: objectstack#20369 typedJoinedReportBlockSchema, soreport-chart-query-spec-parity.test.ts'sIsUnknownpin trips as its docblock intends #10916, where this ejection is recorded (5865612710). ⛔ Not re-queued: rerunning cannot change the verdict until that card's fix is onmain.Blocked-by: #10916andUnlock-action: re-check PR #10910are now in this card's body. When finding(types): the Spec Main Shape Gate is red on every PR and in the merge queue: objectstack#20369 typedJoinedReportBlockSchema, soreport-chart-query-spec-parity.test.ts'sIsUnknownpin trips as its docblock intends #10916 closes, the next step is to re-arm auto-merge on PR feat(app-shell): the environment admin's storage-capacity banner, from the served verdict (objectui#10439) #10910; it rebuilds on the fixedmainin the queue.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Unblocked:
pm:blocked→pm:dispatched, PR re-queueddomain:uiseat 4,session_015AUunPkX7UTkCH9e7AdZo1, at 2026-09-28T08:50Z.JoinedReportBlockSchema, soreport-chart-query-spec-parity.test.ts'sIsUnknownpin trips as its docblock intends #10916 closedcompleted. Its fix, PR test(types): the JoinedReportBlock pin compiles on the installed spec and on spec main, and trips at the bump (objectui#10916) #10934, merged as29c76e573c, and itsSpec Main Shape Gaterun against specmainissuccess.Unlock-action: re-check PR #10910: auto-merge was re-armed, and PR feat(app-shell): the environment admin's storage-capacity banner, from the served verdict (objectui#10439) #10910 re-entered the merge queue. The queue rebuilds it on the fixedmain. This seat follows it to MERGED. TheBlocked-by: #10916line in the body now names a closed card and is kept as history.Generated by Claude Code
feat(app-shell): the environment admin's storage-capacity banner, fro…