Skip to content

fix(app-shell,data-objectstack): Studio stops probing expected misses as errors (objectui#11799) - #11892

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11799-studio-expected-misses
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11799-studio-expected-misses

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Refs #11799 (legs 1 and 3 land whole; leg 2 lands for the Interfaces and Automations pillars, and the Data pillar's /layers read of a never-published object stays: see Acceptance notes)
Clause-②: no

Studio asked three questions whose expected answer is an error, so the browser console filled with red lines that hide real failures. This PR stops each one at the client, from answers the client already gets with a 200.

What changed

Leg 1. Draft existence comes from the drafts ledger. MetadataClient.getDraft() (data-objectstack) now reads GET /meta/_drafts first, through listDrafts(), the shared in-flight read objectui#11797 made. It sends GET /meta/:type/:name?state=draft only when the ledger lists the name.

  • It still resolves null for no draft and the raw envelope for a draft. No caller changes.
  • A draft saved a moment ago is found. A write through the transport drops the pending ledger read when it lands (sendWrite), so a question asked after a save never joins a read sent before it.
  • An unreadable ledger (403 without an authoring capability, 501 on a kernel without drafts, a network fault) counts as unknown, not empty, and the item read is sent as before.
  • The ledger is read whole and matched by name. The ?state=draft read finds a row under either spelling of the type, and with a package it finds that package's row or the package-less one. The ledger's type and packageId filters are exact, so a filtered ledger could miss a draft the item read serves. The editor would then open the published body over a pending draft. Unfiltered, a miss is a real absence, and a same-name row of another type only costs the item read that was always sent.
  • This sits in the client rather than the Studio caller, so every getDraft caller stops logging the 404: Studio's four loads, the Access pillar's OWD overview, ResourceEditPage, PermissionMatrixEditor and the hooks panel.

Leg 2. The published baseline is read only when it is used. In StudioDesignSurface.tsx:

  • The Interfaces leaf load and the Automations flow load read the draft first, and read GET /meta/:type/:name/layers only for an item with no pending draft. That is the one case those loads use the baseline: a served draft is taken as-is (objectui#10765). An item that was never saved always has a draft, so it no longer asks /layers, which answers 404 for a name with no layer.
  • The Interfaces app load reads no /layers for an app the published list does not hold. That app was found in the ledger, so it has never been saved. The published list is read in the same effect run, so it is current.

Leg 3. No usage read while AI is off. AiUsageIndicator reads GET /ai/usage only while the agent catalog at its own base (GET {apiBase}/agents) lists an agent. That catalog is the signal useAiSurfaceEnabled gates every AI entry point on. Its header explains why it is not discovery's services.ai: that flag is the same for every user, and a seat-less user must see AI as off. The catalog read is shared with the dock's own, because useAgents keeps one request per base in flight and caches its answer.

No REST answer, export, prop, type member or language-pack key changes.

Measured before editing (mechanism hypotheses)

  • H1, confirmed. Every Studio item load calls getDraft() beside layered() in a Promise.all, and readItemResponse turns the 404 into null. There are four such loads: the Interfaces app and leaf loads, the Data pillar object load and the Automations flow load. Since objectui#11797, listDrafts() and readItemResponse go through shareRead, and sendWrite clears it. usePendingDrafts' raw-fetch ledger read is cleared only by the publish pulse, not by a draft save. So that read would report a just-saved draft absent, and leg 1 uses the client's read instead.
  • H2, falsified. LayeredDiff.tsx fetches nothing. It renders a layered prop. The /layers reads come from the same four Studio loads (and from ResourceEditPage, EmbeddedItemEditor, PermissionMatrixEditor and PackageOwdOverviewPanel outside Studio's loads). The server answers 404 when no layer exists at all (createMetaLayeredAnswer, reason absent), which describes a draft-only item.
  • H3, partly falsified. useAiUsage has no interval. It reads on mount, on the chat engine's refresh event and on tab re-focus. The repeated 501 comes from mounts: the Studio dock is drawn while the agent catalog loads, so the indicator mounts and reads once per dock mount. On an open-edition server the framework answers /ai/agents with its empty-list courtesy and every other /ai/* route with 501 (handleAIRequest).

Network reading, before and after

This is a dev-only harness, never committed. It mounts the real pillars over a real MetadataClient whose transport is an in-memory server: 404 for ?state=draft with no draft and for /layers with no layer, 501 for /ai/* except /ai/agents. Each pillar opens one saved item, then one draft-only item.

Surface before: requests / 404-501 after: requests / 404-501
Interfaces (app, saved leaf, draft-only leaf) 9 / 3 9 / 0
Data (saved object, draft-only object) 6 / 2 7 / 1
Automations (saved flow, draft-only flow) 7 / 2 7 / 0
Studio AI dock, open edition 2 / 1 1 / 0

The misses before were the app, leaf, object and flow ?state=draft reads; the draft-only leaf, object and flow /layers reads; and /ai/usage. The one miss after is the Data pillar's /layers for the draft-only object (see Acceptance notes).

Pins

  • packages/data-objectstack/src/metadata-client.draftLedger-11799.test.ts: no draft means no ?state=draft; a listed draft gets exactly one; a draft saved a moment ago is found (a held ledger read, a save that lands, a new ledger read for the later question); control: with no write in between, the question joins the pending read; 403, 501 and a network fault send the item read; a package-less row and a plural-spelling row still lead to the item read; questions asked together share one ledger read.
  • packages/app-shell/src/views/studio-design/StudioDesignSurface.expectedMisses-11799.test.tsx: the real pillars over a real client. A saved leaf or flow with no draft makes no ?state=draft request, and an unsaved one makes one and no /layers request. A draft saved a moment ago is found when the leaf is reopened. A ledger-only app reads no /layers; control: a published app does. The Data pillar asks no draft of a saved object.
  • packages/app-shell/src/layout/__tests__/AiUsageIndicator.aiOff-11799.test.tsx: with an empty catalog, /ai/usage is never requested, on mount or after a refresh event or re-focus. Nothing is read while the catalog loads. Control: with an agent, usage is read on mount and again on a nudge.
  • Updated: metadata-client.get-envelope.test.ts A10 (its server now answers the ledger) and AiUsageIndicator.test.tsx (its cases are an AI-on viewer, so the catalog hook is stubbed to list one agent).

Reverse check, per leg. Each leg's source file was reverted to the base 31add5ff, and the reverted blob hash was checked equal to the base blob. That leg's pins were run, then the file was restored from HEAD, the restored hash was checked equal to HEAD's, and git diff HEAD was confirmed empty. Run on a473d14, leg 3 re-run on bee4e1b:

  • Leg 1 (metadata-client.ts): 16 of 27 failed, each in the predicted direction. For example, the received URL list was the bare ?state=draft read instead of the ledger, and the Studio pins received a ?state=draft 404 for the saved leaf and the saved object.
  • Leg 2 (StudioDesignSurface.tsx): 4 of 6 failed: /layers read for the unsaved leaf, the ledger-only app and the unsaved flow, and a second /layers for the reopened leaf with a draft. The Data pillar row and the published-app control stayed green, as predicted.
  • Leg 3 (AiUsageIndicator.tsx): 2 of 3 failed. The empty-catalog case received 2 usage reads where 0 is expected. The loading case failed at its precondition, because the base never reads the catalog. The control stayed green.

Gates (local, on b34f4f5, after merging origin/main at bfca7ec)

  • turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 (the closure of both touched packages): exit 0, 28/28 tasks.
  • pnpm --filter @object-ui/data-objectstack type-check and pnpm --filter @object-ui/app-shell type-check (with tsconfig.test.json): exit 0. The data-objectstack program lists both of its touched test files.
  • vitest run over packages/data-objectstack/ and 282 app-shell files: everything under studio-design/, layout/ and hooks/, every app-shell test naming getDraft, new MetadataClient(, useAiUsage or AiUsageIndicator, and viewCacheInvalidation.guard.test.tsx, the only guard test in either package. Result: 364 files, 3084 tests passed, exit 0.
  • check:control-bytes, check:new-line-citations, check:changeset-claims, check:pending-changeset-literals, check:metadata-write-doors, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:phantom-deps, check:unreferenced-sources, scripts/check-changeset-presence.mjs and scripts/check-changeset-no-major.mjs: all exit 0.
  • Declared narrowing: the rest of app-shell's 1121 test files and the repo-wide pnpm lint are CI's.

Risk and cost

  • Opening an Interfaces leaf or an Automations flow now waits for the ledger before it reads the item. That is one round trip more than the parallel reads it replaces, and it is the price of deciding from the ledger. The app load keeps its parallel reads.
  • A getDraft of an item that has a draft is now two requests: the ledger, then the item. Questions asked together share one ledger read, so the lints and the hooks panel pay one extra request per batch.
  • Rollback: revert this PR. The client and the three loads are independent, and nothing persisted changes shape.

Acceptance notes

  • The Data pillar still reads /layers for a never-published object (one 404 per open, in the table above). There, the read is the question itself. Its answer sets hasBaseline (no records table yet, so no data query) and the published field projection, and both are used even with a draft. Nothing the pillar holds answers either for certain. Its rail's package-scoped published list goes stale across a publish: the rail re-reads on the refresh pulse while the load re-runs on publishNonce, and taking that list as "never saved" would show a just-published object as unpublished. Routes: carry a published bit the load can trust, or accept the 404 there. This needs the seat's call, and is why the first line says Refs.
  • Other /layers reads of a draft-only item outside the claimed surface, which this PR leaves alone: PackageOwdOverviewPanel.tsx (its load and save reads), ObjectListViewInspector.tsx (new from objectui#11823, which reads /layers for a list view that may not exist yet), and ResourceEditPage.tsx, EmbeddedItemEditor.tsx and PermissionMatrixEditor.tsx in Setup. Their ?state=draft 404s are gone through leg 1.
  • The suggested route said "the _drafts list for the package". This PR reads the whole ledger, for the measured reason in leg 1: a package or type filter is narrower than the item read it replaces.

Session https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z (domain:ui#1, claim 6051768139).


Generated by Claude Code

claude added 3 commits October 8, 2026 04:37
- MetadataClient.getDraft() asks the drafts ledger (GET /meta/_drafts,
  the shared read objectui#11797 made) first and sends ?state=draft only
  when the ledger lists the name. An unreadable ledger is unknown, so the
  item read is sent as before. A write drops the pending ledger read, so
  a draft saved a moment ago is found.
- Studio's Interfaces leaf and Automations flow loads read /layers only
  for an item with no pending draft, the one case the baseline is used;
  the Interfaces app load reads none for an app found only in the ledger.
- AiUsageIndicator reads /ai/usage only while the agent catalog at its
  base lists an agent.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
…udio-expected-misses

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 333 chunks) 3353.3 KB 3363.9 KB
Main entry chunk (gzip) 71.0 KB 350 KB
Entry file index-BNT6GV-S.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 584.59KB 140.69KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 266.92KB 67.47KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 38.37KB 10.31KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.82KB 39.17KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 241.10KB 66.40KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 117.42KB 29.32KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 05:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 05:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 5dff027 Oct 8, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11799-studio-expected-misses branch October 8, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants