Repository navigation
fix(app-shell,data-objectstack): Studio stops probing expected misses as errors (objectui#11799) - #11892
Merged
objectstack-fleet[bot] merged 3 commits intoOct 8, 2026
Conversation
- 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
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
Contributor
✅ Console Performance Budget
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
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-11799-studio-expected-misses
branch
October 8, 2026 05:33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #11799 (legs 1 and 3 land whole; leg 2 lands for the Interfaces and Automations pillars, and the Data pillar's
/layersread 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 readsGET /meta/_draftsfirst, throughlistDrafts(), the shared in-flight read objectui#11797 made. It sendsGET /meta/:type/:name?state=draftonly when the ledger lists the name.nullfor no draft and the raw envelope for a draft. No caller changes.sendWrite), so a question asked after a save never joins a read sent before it.?state=draftread 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'stypeandpackageIdfilters 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.getDraftcaller stops logging the 404: Studio's four loads, the Access pillar's OWD overview,ResourceEditPage,PermissionMatrixEditorand the hooks panel.Leg 2. The published baseline is read only when it is used. In
StudioDesignSurface.tsx:GET /meta/:type/:name/layersonly 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./layersfor 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.
AiUsageIndicatorreadsGET /ai/usageonly while the agent catalog at its own base (GET {apiBase}/agents) lists an agent. That catalog is the signaluseAiSurfaceEnabledgates every AI entry point on. Its header explains why it is not discovery'sservices.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, becauseuseAgentskeeps 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)
getDraft()besidelayered()in aPromise.all, andreadItemResponseturns the 404 intonull. 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()andreadItemResponsego throughshareRead, andsendWriteclears 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.LayeredDiff.tsxfetches nothing. It renders alayeredprop. The/layersreads come from the same four Studio loads (and fromResourceEditPage,EmbeddedItemEditor,PermissionMatrixEditorandPackageOwdOverviewPaneloutside Studio's loads). The server answers 404 when no layer exists at all (createMetaLayeredAnswer, reasonabsent), which describes a draft-only item.useAiUsagehas 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/agentswith 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
MetadataClientwhose transport is an in-memory server: 404 for?state=draftwith no draft and for/layerswith no layer, 501 for/ai/*except/ai/agents. Each pillar opens one saved item, then one draft-only item.The misses before were the app, leaf, object and flow
?state=draftreads; the draft-only leaf, object and flow/layersreads; and/ai/usage. The one miss after is the Data pillar's/layersfor 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=draftrequest, and an unsaved one makes one and no/layersrequest. 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/usageis 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.metadata-client.get-envelope.test.tsA10 (its server now answers the ledger) andAiUsageIndicator.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 fromHEAD, the restored hash was checked equal toHEAD's, andgit diff HEADwas confirmed empty. Run ona473d14, leg 3 re-run onbee4e1b:metadata-client.ts): 16 of 27 failed, each in the predicted direction. For example, the received URL list was the bare?state=draftread instead of the ledger, and the Studio pins received a?state=draft404 for the saved leaf and the saved object.StudioDesignSurface.tsx): 4 of 6 failed:/layersread for the unsaved leaf, the ledger-only app and the unsaved flow, and a second/layersfor the reopened leaf with a draft. The Data pillar row and the published-app control stayed green, as predicted.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 mergingorigin/mainatbfca7ec)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-checkandpnpm --filter @object-ui/app-shell type-check(withtsconfig.test.json): exit 0. The data-objectstack program lists both of its touched test files.vitest runoverpackages/data-objectstack/and 282 app-shell files: everything understudio-design/,layout/andhooks/, every app-shell test naminggetDraft,new MetadataClient(,useAiUsageorAiUsageIndicator, andviewCacheInvalidation.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.mjsandscripts/check-changeset-no-major.mjs: all exit 0.pnpm lintare CI's.Risk and cost
getDraftof 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.Acceptance notes
/layersfor a never-published object (one 404 per open, in the table above). There, the read is the question itself. Its answer setshasBaseline(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 onpublishNonce, 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./layersreads 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/layersfor a list view that may not exist yet), andResourceEditPage.tsx,EmbeddedItemEditor.tsxandPermissionMatrixEditor.tsxin Setup. Their?state=draft404s are gone through leg 1._draftslist 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