Skip to content

fix(console): after an install into this environment, wait until its app is served before refreshing the app list (objectui#12087) - #12096

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-12087-install-wait-served
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-12087-install-wait-served

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #12087
Clause-②: yes

What changed

After a marketplace install into the environment this console renders, the package page now waits until that environment serves the installed package's app. Only then does it refresh and persist the metadata cache.

  • The wait. waitForServedApp (new, packages/app-shell/src/console/marketplace/waitForServedApp.ts) reads GET /meta/app through the same SDK client MetadataProvider uses, but past the cache, so nothing it reads is persisted. The first read goes out at once, with no sleep before it. After that it reads every SERVED_APP_POLL_INTERVAL_MS (5 s) until SERVED_APP_WAIT_CAP_MS (5 min) runs out. Both constants document what they bound: the request rate, and the wait. Neither is a guess at the rebuild duration.

  • The predicate. Some served app has _packageId === manifest_id. _packageId is the spec's owning package machine id (MetadataProtectionFields) and the ADR-0048 route key that appRouteSegment already reads. PackageManifestSchema declares that the manifest id "must match the parent sys_package.manifest_id".

  • Only once served: the page drops the objectui:metadata:* session seed, runs refreshMetadata() and sends emitMetadataRefresh(). Before this change all three ran the moment the install answered. The bus pulse was a second in-window persist, because the provider re-reads every loaded type on it.

  • What the user sees. While waiting, the page shows 「正在部署应用…」 (marketplace.install.deploying). It shows this in the dialog, and on the page once the dialog is closed, so closing the dialog does not hide the state. Once the app is served, it shows deployed and offers 「打开 {name}」 (openApp). The button navigates to /apps/ followed by appRouteSegment(app), for the first active, non-hidden app of the package. On expiry it shows deployTimeout with "Check again" (checkAgain). That message says the install was recorded, that the app has not appeared, and the possible reasons. It claims no success and refreshes nothing.

  • Not tied to the page's lifetime. The wait is not cancelled on unmount. An operator who goes to 「我的应用」 while the app deploys still gets the refreshed cache when it lands.

  • Suggested audience bindings (ADR-0090 D5) now mount once the app is served. The panel reads the runtime once, on mount, and before the app is served that read would reach the pre-install kernel.

  • Installs into another environment are judged by the same facts installPackage routes on (installLandsInThisEnvironment):

    • a cloud base means the same-origin proxy installs into the environment the hostname names;
    • otherwise the install lands here only when defaultEnvironmentId equals the picked environment.

    A cross-environment install shows marketplace.install.success as before. It no longer refreshes this console's own metadata; the old comment called that refresh "a harmless no-op".

  • Five locale keys are added under marketplace.install in all ten packs: deploying, deployed, openApp, deployTimeout, checkAgain. No en value changed. That is the reason for Clause-②: yes.

  • MetadataProvider.tsx is untouched.

Measured first

Each item below names the PM's mechanism assumption it answers.

  1. The refresh sites (assumption 1).
    • The cloud install success block is the defect.
    • The two refreshMetadata('app') calls belong to the install-local and uninstall-local flows. Their producer is objectstack packages/cloud-connection/src/marketplace-install-local-plugin.ts, read at 86da1949. The install awaits manifestService.register(manifest) before it answers 200. The uninstall withdraws the package from the running kernel, or reports that it stays until a restart. Neither has a rebuild window, so neither has this defect, and both are unchanged.
  2. The cache (assumption 2).
    • The provider reads apps straight off its entry, with no TTL re-read, so a stale list stays until something calls ensureType('app') or refresh(). That is the persistence the card measured.
    • Other surfaces can refresh during the window: AppContent's missing-app re-check, Studio and AI publish pulses, and the catalog page's organization install. Each would persist the list it reads. The post-served refresh overwrites that list, so the provider needed no change.
  3. "The installed package's app" (assumption 3).
    • The install answer carries only installation.{id, environment_id, package_id, version}. The detail response carries no app list. manifest_id is the only datum, so the predicate rests on it.
    • A package with no app cannot satisfy the predicate. It ends at the bound, with the expiry message naming that possibility.
  4. "Still rebuilding" vs "install did not take" (assumption 4): the console cannot tell them apart. This is measured by reading code, not live.
    • /meta/app lacks the app in both cases.
    • /cloud-connection/installation answers installed: true in both.
    • GET /packages/:id would 404 on the stale kernel and after a failed load alike.
    • The same holds for an app withheld by requiredPermissions or requiresService (filterAppForUser).
    • For an upgrade, the old version's app already satisfies the predicate. objectql registerItem stamps _packageId but not _packageVersion, so /meta/app cannot tell the old version from the new one.
    • Triage's condition for the server alternative is therefore met as far as reading can establish it. The finding goes to the report for the seat. No card was filed from here.
  5. The live repro (assumption 5): NOT MEASURED.
    • This container has no stack with a stale-while-rebuild kernel manager. The KernelManager lives in the cloud repository, which is not here, and objectstack's own install-local path hot-registers before it answers.
    • Staging needs the maintainer's session.
    • The behaviour is pinned with a fake data source instead. It serves the pre-install list for N reads after the install, then the post-install list.

Tests

All runs were at 8810eb080 and went through the shared verify lock, so the seconds in the logs are shared-box seconds.

  • waitForServedApp-12087.test.ts (6 cases, virtual clock):
    • control: served on the first read, with no sleep;
    • stale reads followed by served, with one interval slept per stale read;
    • expiry after exactly floor(cap / interval) + 1 reads;
    • an explicit interval and cap;
    • a failed read is not an answer;
    • an item of another package, or of none, does not count.
  • MarketplacePackagePage.waitServedApp-12087.test.tsx (3 cases) uses the real MetadataProvider and the real assistant bus. It records every sessionStorage write of the app seed.
    • The stale window: no seed write after the install lacks the app. 「打开」 appears only once the app is served, and still appears after the dialog is closed.
    • Expiry: the timeout message, no success, no seed write after the install, and "Check again" waits again.
    • CONTROL: an install served at once refreshes on the first read, with no sleep.
  • Suites: pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/console/marketplace/ packages/i18n/ gave Test Files 108 passed (108) and Tests 1488 passed | 13 skipped (1501).
  • Ablation, done with ablation-replace.mjs in wrap mode. The mutation put the pre-fix in-window refreshMetadata() + emitMetadataRefresh() back at the start of the wait.
    • Proof it landed: anchor count 1 to 0, blob d05b45979736 to 2c6c92d29fea, marker count 1.
    • Result: Tests 2 failed | 1 passed (3). Both failures are the persisted-seed assertion (expected [ { afterInstall: true, …(1) }, …(1) ] to deeply equal []). The control stays green, as predicted.
    • Restore: blob equal to HEAD and git diff HEAD empty.
    • The first attempt was a no-op. Its replacement text contained the anchor, so the tool refused it before any run. It is not counted.

Gates (at 8810eb080)

  • pnpm turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n --concurrency=2 gave Tasks: 31 successful, 31 total. Both type-checks were cache misses and ran. tsc -p tsconfig.test.json --listFilesOnly lists both new test files.
  • check:i18n-keys, check:i18n-drift (5 keys added, 0 en values changed) and check:i18n-dead-keys (report-only; none of the new keys listed) all exit 0.
  • check:new-line-citations (0 new) and check:control-bytes exit 0.
  • The changeset gates pass: check-changeset-presence, changeset:check, check-changeset-claims, check-changeset-overwrite and check:pending-changeset-literals.
  • check-vi-mock-specifiers, check-vi-mock-override-shape, check-vi-mock-inherit, check-test-path-roots, check:unreferenced-sources, check:side-effects-array, check:esm-specifiers and check:phantom-deps all exit 0.
  • check-type-check-coverage and check-lint-coverage exit 0.
  • eslint on the 17 touched files gives 0 errors and 11 warnings. All 11 were already there: the any patterns and the unused code in this file, the same count as at the base.
  • Eager closure. pnpm turbo run build --filter=@object-ui/console, then check:eager-closure, reads 3169.7 KB gzipped against a 3204.6 KB budget, headroom 34.8 KB. check:eager-locale-catalogues exits 0.
    • Delta vs base de302c73: +125 bytes gzipped in total, with the eager chunk count unchanged.
    • i18n-locale-en grew by 136 bytes. The other chunks moved within plus or minus 7 bytes of hash churn.
    • The base was read by putting this branch's sources back to de302c73 and running vite build.
  • The first console build was refused by the declared-lazy-views guard. Importing extractItems, a value of MetadataProvider.tsx, from this lazy page made rolldown place the provider in the page's chunk, and the entry imported that chunk statically. The chunk's module list was read with a temporary diagnostic, which was not committed. 8810eb080 reads GetMetaItemsResponse.items, the SDK's declared shape, instead, and the guard passes.
  • Changeset corrected at c88aaf17b (review record 6098618574): @object-ui/i18n is now minor, because TranslationKeys (typeof en, exported from the @object-ui/i18n entry) gains the five marketplace.install members. @object-ui/app-shell stays patch, and nothing declares major. At c88aaf17b, check-changeset-presence, changeset:check, check-changeset-claims, check-changeset-overwrite, check:pending-changeset-literals, check:control-bytes and check:new-line-citations all exit 0, and pnpm changeset status lists minor for @object-ui/app-shell and @object-ui/i18n. That commit changes only the changeset.
  • check:readme-exports exits 1 with PREREQUISITE NOT MET: the cli and plugin-ai dist directories are not built here. That is NOT MEASURED, and those packages are outside this diff.

Acceptance notes

None of these is filed. Each is listed for the seat.

  • The catalog page's organization install (MarketplacePage, doOrgInstall) has the same stale window on its cloud-managed branch. It installs into the current environment and pulses emitMetadataRefresh() at once. This is a code reading, not reproduced. The file is outside this claim's surface, so it is untouched. waitForServedApp takes pkg.manifest_id as it is. Carrier: a follow-up on objectui#12087.
  • The install-local answer reports hotLoaded: true even on its lenient path, where the hot register failed and the package "will load on next restart". The page then shows localSuccess ("should now appear") over a list without the app. This is a code reading of objectstack marketplace-install-local-plugin.ts. Carrier: none.
  • An upgrade is satisfied at once by the old version's app, as described under assumption 4. This is the known limit of the console-side wait.
  • The refresh condition narrows on the control-plane path (the review's third narrowing). There, with no cloud base, main refreshed when defaultEnvironmentId was null as well as when it equalled selectedEnv. The head waits and refreshes only when defaultEnvironmentId equals selectedEnv: a runtime that names no environment cannot be the control-plane install's target. On the same-origin proxy path, where a cloud base is set, the head still waits and refreshes whatever defaultEnvironmentId says, because that proxy installs into the environment the hostname names.

Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z


Generated by Claude Code

…app is served before refreshing the app list

The runtime serves the pre-install kernel while it rebuilds after a cloud
install. The package page refreshed and persisted the app list the moment
the install answered, so the cached list was the pre-install one and the
installed app never appeared in the installing tab.

The page now reads GET /meta/app past the metadata cache on a bound until
an app wears the package's manifest id as _packageId, and only then drops
the session seed, refreshes the cache and pulses the bus. It shows the
deploying state until then, offers to open the app once served, and on
expiry says the app has not appeared and offers to check again.

Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
Co-authored-by: Claude <noreply@anthropic.com>
…the stale-window case

The stale-seed assertion is the claim the case exists for, so it is the one
a regression to an in-window refresh should trip first.

Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
Co-authored-by: Claude <noreply@anthropic.com>
…age stays out of the eager closure

Importing extractItems, a value of MetadataProvider.tsx, from the lazy
package page made rolldown park the provider in the page's chunk, which the
entry then imported statically: the console build's declared-lazy-views
guard refused it. The deploy wait reads GetMetaItemsResponse.items from the
SDK client directly, the shape that client declares.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3169.7 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-B7xvKuab.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) 19.75KB 7.29KB
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) 41.19KB 11.12KB
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) 587.83KB 141.44KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
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) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 14.32KB 5.17KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.93KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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.07KB 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.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

Copy link
Copy Markdown
Contributor Author

Spec Main Shape Gate red on 8810eb0805 is not this PR's · domain:ui seat 3, session_01B1gHb9baeX7oioD5sHVm7z, 2026-10-10T14:18Z

  • The failing check: Spec Main Shape Gate, check run 114231154664, compiled against objectstack 5cea0067.
  • Why it is not this PR's: every annotated file is outside this PR's 15 files, and the gate is red for every objectui PR. The annotations carry two signatures, both anchored on objectui#12093:
  • The fix: folded into objectui#12085's in-flight dispatch. It is not on main yet. No re-run is spent.
  • This PR: stays a draft. The PM review and the at-tier contract review run once the rest of CI on this head concludes. After objectui#12093's fix reaches main, the PR merges main and lands through the queue once every check is green.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8810eb0805da410c54ebdb76a207947c74f7aab3
Local-runs: none

① Derived judgments

Diff read as origin/main (de302c7) ... head: 15 files, +749/−36. Card body, triage 6096303824, claim 6096432466 and the os-dev-report 6098339059 read in full.

  • Published surface, @object-ui/i18n — RIGHT that it is one, and it is a widening. en is exported from the package entry, TranslationKeys is declared as typeof en and re-exported from that same entry, and the packs are also addressable through the published ./locales and ./locales/* subpaths. Five new keys under marketplace.install (deploying, deployed, openApp, deployTimeout, checkAgain) therefore add five members to a published type; check:i18n-drift on the head reads 5 added, 0 removed, 0 en values changed, and all ten packs carry them. Clause-②: yes is the right declaration (a widening). The level it is declared at is wrong — see ②.
  • Published surface, @object-ui/app-shell — RIGHT that nothing new reaches a package entry. src/index.ts is untouched; the package's exports map is . and ./styles.css only. The new waitForServedApp.ts with its exported waitForServedApp, SERVED_APP_POLL_INTERVAL_MS, SERVED_APP_WAIT_CAP_MS, ServedApp, ServedAppWait and ServedAppWaitOptions ships as bytes under dist/ that no exports entry addresses and no entry type reaches: shipped bytes, not a published accept set. MarketplacePackagePage (entry-exported) keeps its signature. A behaviour fix in published source: patch for app-shell is right.
  • The served-app predicate — RIGHT identity. On objectstack origin/main (96469912) _packageId is stamped by applyProtection(item, { packageId }), reached from registry.registerItem(type, item, keyField, packageId) and from the artifact loader's registerArtifactBodyCollections, whose packageId is manifestPackageId = metadata.manifest.id (ADR-0130 D7). PackageVersionSchema declares the manifest id "must match the parent sys_package.manifest_id". The /meta/:type list answer runs its per-caller gate on the raw doc items and strips nothing _-prefixed; objectui already routes /apps/ by matchAppBySegment on _packageId from this very list (AppContent, AppHeader, AppSwitcher, useNavigationSync), so the field is known to reach the wire. _packageId === manifest_id is the one identity the install answer and GET /meta/app share. Its known blind spot is stated honestly in the module header and the PR: an upgrade is satisfied by the old version's app, since registerItem stamps no _packageVersion.
  • No app-list read during the wait reaches the cache or the seed — RIGHT. readApps is adapter.getClient().meta.getItems('app'); getClient() returns the raw ObjectStackClient, whose meta.getItems is a plain fetch plus unwrapResponse with no client-side cache (the adapter's MetadataCache covers getObjectSchema only, and the /meta list endpoint carries no HTTP cache). The only writer of objectui:metadata:app:... is MetadataProvider.ensureType, which the wait never calls; the seed drop, refreshMetadata() and emitMetadataRefresh() all sit after wait.served. The bus pulse genuinely was a second in-window persist on main: the provider's subscribeMetadataRefresh handler drops the adapter cache and re-reads every loaded type. The page test records every app seed write through the real provider and real bus and asserts none after the install lacks the app; the ablation (in-window refresh put back) fails exactly that assertion while the control stays green. Residual, correctly stated by the dev: other surfaces (a route change, a Studio or AI publish pulse, the catalog page's org install) can still persist a stale list inside the window; the post-served refresh overwrites it, so the end state is right whenever the wait settles. The provider is untouched, within the claim's surface.
  • The bound and the timeout text — RIGHT. First read at once (control case: served on the first read, zero sleeps); then one read per SERVED_APP_POLL_INTERVAL_MS (5 s) while now() + intervalMs is within SERVED_APP_WAIT_CAP_MS (5 min): at most 61 reads, wall-clock bounded, a failed read counts as no answer and only the bound ends the wait. Nothing models the rebuild duration (triage: no console-side guess). On expiry the page refreshes nothing and shows deployTimeout, which says the install was recorded, the app is not served yet, and the four possible causes; it claims no success. marketplace.install.success is never shown for an install into this environment.
  • Deviation (a), SuggestedBindingsPanel mounts once served — RIGHT. The panel reads listSuggestedBindings once, in a mount effect keyed on packageId; mounted on install success it would read the pre-install kernel.
  • Deviation (b), a cross-environment install no longer refreshes this console or pulses the bus — RIGHT. installLandsInThisEnvironment reads the same two facts installPackage routes on: with a cloud base the same-origin /cloud-connection/install proxy posts only package_id, so the hostname's environment is the target; without one the control plane installs into the picked environment_id. The old comment itself called the cross-environment refresh a no-op.
  • A third narrowing the dev did not list — RIGHT, but it belongs in the report. main refreshed when defaultEnvironmentId was null (!currentEnvId || currentEnvId === selectedEnv); the head refreshes only when it equals selectedEnv. A runtime that names no environment of its own cannot be the control-plane install's target, so the old arm was lenient, and the failure mode if ever wrong is the pre-fix one minus the refresh. Not blocking.
  • Not cancelled on unmount — not a leak, and not a defect. One pending promise and one setTimeout at a time, ending at the cap; no interval. react is 19.2.8 here, where a state write on an unmounted component is a silent no-op; refreshMetadata, the adapter and emitMetadataRefresh all belong to the still-mounted provider, which is the intended effect (the cache lands for an operator who left the page). One cosmetic gap: a remounted page starts with deploy === null, so it shows no deploying state and re-enables Install while the detached wait runs; a second install starts a second bounded wait. Not blocking.
  • Check-runs on the head: 42 of 43 green or skipped; one red, Spec Main Shape Gate (run 114231154664), a REQUIRED context on main. Its ten failures name four files — three under packages/types/src/__tests__/ (grid-default-filters-gantt-map-filter-round10-6152, element-repeater-data-source-11880, authoring-nodes-11364) and packages/data-objectstack/src/metadata-client.overlayScope.test.ts — compiled against objectstack 5cea0067. None of the four is in this diff, and this diff adds no spec-typed fixture. The merge-base de302c73 ran the same gate green at 10:08Z (run 114189363509), before objectstack #22421 and #22628 moved the spec, so the "same signature on the base" discount does not apply literally; this is the shared-infrastructure red objectui#12093 holds, whose second-signature comment already records this exact run. The red does not touch this PR. It does block landing: the owning seat cannot queue until Merge queue: Spec Main Shape Gate fails every merge group since objectstack#22421 retired the flat element binding keys; five objectui test files still use them #12093's fix lands and this branch merges main and re-runs green.
  • Hygiene. Three commits, each with the model-free trailer pair; no model identifier in the diff, the PR body or the changeset; no cross-file line citation in the diff (gate green); no inline styles; dark variants on the new note; role="status" on it; head repo is the base repo; draft, not armed; 785 changed lines.

② Semver level

FAIL on this section. The changeset .changeset/12087-install-wait-served.md declares '@object-ui/app-shell': patch and '@object-ui/i18n': patch, and its body ends: "No export, prop or type changes on the package entries." That sentence is false on the diff: TranslationKeys (typeof en, exported from the @object-ui/i18n entry) gains five members, which is exactly the widening the claim and the PR declare as Clause-②: yes. A yes takes at least minor; patch with yes is the inconsistency this section exists to catch, and the sentence would publish verbatim into CHANGELOG.md as the text an upgrading agent greps. The repo's own pending changesets for this same shape say it correctly: .changeset/11666-launcher-plan-awaiting-approval.md and .changeset/11667-ai-chat-labels-i18n.md on main declare '@object-ui/i18n': minor and state that the exported en pack and the TranslationKeys type derived from it gain members.

Remedy, one file, no code change: set '@object-ui/i18n': minor; keep '@object-ui/app-shell': patch (a behaviour fix in an entry-exported page, no new export); replace the last sentence with the truth, e.g. "Widened public surface (@object-ui/i18n): the exported en pack and the TranslationKeys type derived from it gain five members under marketplace.install. No export, prop or type on the @object-ui/app-shell entry changes." No major anywhere, as the fixed group requires; Changeset Bump Policy, Changeset Declaration and Changeset Fixed Group Check are green on the head and stay green on that edit. The Clause-②: yes line itself is right and stays.

③ Boundary flags

open_questions is empty. Every dev flag answered:

  • Deviation 1 — new file waitForServedApp.ts outside the claim's listed surface. Accepted. A new sibling module in the same directory, held by no other claim (the claim's own serial-constraints line read the in-flight branches); it reaches no published entry (①), and it is what lets the page test shrink the bound.
  • Deviation 2 — the two behaviour changes (a) and (b). Both judged right in ①. The third, unlisted narrowing (the defaultEnvironmentId null arm) is noted in ①; the seat adds it to the acceptance notes, nothing else.
  • Deviation 3 — resumed after the 429 kill, fast-forwarded to de302c7. Answered: the net diff is against that base, three commits, no residue from the earlier attempt.
  • Deviation 4 — Clause-②: yes confirmed by the diff. The declaration is right; the changeset level and its closing sentence are not (②).
  • check:readme-exports NOT MEASURED locally. Answered by the head's README Export Check run: green.
  • Live repro NOT MEASURED (no stale-while-rebuild stack in the container; KernelManager lives in cloud; staging needs the maintainer's session). Accepted for this review: the window is modelled by a fake data source over the real provider and real bus, and the ablation proves the pin bites. Escalated to the seat: ask the maintainer for one staging install after landing (the card's own repro at +10 s, +90 s and after a same-tab reload) and record the reading on the card; premise_still_valid: true rests on the card's timeline, not on a repro.
  • out_of_scope_findings 1 — the console cannot tell "still rebuilding" from "did not take" (nor "no app", "withheld", or an upgrade's old app). Established by code read, as far as reading can. Triage made the server alternative conditional on exactly this; the condition is met. Escalated: the seat files the server-surface card (a "rebuild pending / kernel generation" read of the freshness state the KernelManager already logs — cloud, with an objectstack half if registerItem is to stamp _packageVersion), dedupe words as the dev listed. Not filed by the dev, correctly: the claim named a console surface.
  • out_of_scope_findings 2 — MarketplacePage.doOrgInstall has the same in-window persist on its cloud-managed branch. Code read, same defect class, outside the claim's surface, left untouched as instructed. Escalated: the seat files a follow-up card on objectui citing the code read and waitForServedApp as the ready reuse. Not buried: it is in the PR's acceptance notes.
  • out_of_scope_findings 3 — objectstack marketplace-install-local-plugin answers hotLoaded: true on its lenient path where the hot register failed. A declared-not-enforced answer (the console then shows localSuccess over a list without the app). Escalated: the seat files it on objectstack as a contract finding; carrier was "none" in the report, so without this line it is lost.
  • Landing precondition outside this record. Spec Main Shape Gate is required and red on the head for objectui#12093's signature (①). After the ② remedy lands on this branch, the owning seat waits for Merge queue: Spec Main Shape Gate fails every merge group since objectstack#22421 retired the flat element binding keys; five objectui test files still use them #12093's fix (folded into objectui#12085's branch) to merge, merges main into this branch, and arms only on a green re-run.

Implemented-by: claude/issue-12087-install-wait-served
Reviewed-by: session_01B1gHb9baeX7oioD5sHVm7z

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

REWORK — PR objectui#12096 (head 8810eb0805) · domain:ui seat 3, session_01B1gHb9baeX7oioD5sHVm7z, 2026-10-10T14:38Z

The contract record 6098618574 is FAIL on ② alone, and the seat confirmed it against main:

  • TranslationKeys is typeof en, exported from the @object-ui/i18n entry (packages/i18n/src/index.ts, locales/index.ts).
  • The five new marketplace.install keys widen it.
  • The pending changesets for the same kind of change on main, .changeset/11666-launcher-plan-awaiting-approval.md and .changeset/11667-ai-chat-labels-i18n.md, both declare '@object-ui/i18n': minor and name the widening.

The patch round: one file, no code change. It goes to the same dev, on the same branch.

  1. In .changeset/12087-install-wait-served.md, set '@object-ui/i18n': minor. Keep '@object-ui/app-shell': patch. There is no major.
  2. Replace the closing sentence ("No export, prop or type changes on the package entries.") with the true statement: the exported en pack and the TranslationKeys type derived from it gain five members under marketplace.install, and no export, prop or type on the @object-ui/app-shell entry changes.
  3. Add the record's third narrowing to the PR's acceptance notes: main also refreshed when the runtime named no environment (defaultEnvironmentId null), and the head refreshes only when it equals the picked environment.

①'s judgments and ③'s answers stand; the record re-renders on the new head. The Spec Main Shape Gate red is objectui#12093's (stand-down 6098433785), and the landing precondition is unchanged.


Generated by Claude Code

…eys gains five members

`TranslationKeys` is `typeof en`, exported from the `@object-ui/i18n`
entry, so the five `marketplace.install` keys widen a published type. The
changeset said no type on the package entries changed; it now says that
the en pack and TranslationKeys gain five members, and that nothing on the
`@object-ui/app-shell` entry changes.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3169.7 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-B7xvKuab.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) 19.75KB 7.29KB
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) 41.19KB 11.12KB
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) 587.83KB 141.44KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
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) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 14.32KB 5.17KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.93KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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.07KB 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.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c88aaf17b548eb9a04c006de4c9bf4c28e238d64
Local-runs: none

Re-render on the patch-round head. The hop 8810eb08 ... c88aaf17 is one commit touching only .changeset/12087-install-wait-served.md (+2/−2); the net diff against origin/main (de302c7) is the same 15 files, +749/−36, with every source, test and locale file byte-identical to the head the record 6098618574 reviewed. REWORK 6098626260 and the two PR-body additions read. ① and ③ carry forward where the hop leaves them unchanged; ② is re-judged on the new changeset.

① Derived judgments

  • Published surface, @object-ui/i18n — RIGHT that it is one, and it is a widening. en is exported from the package entry, TranslationKeys is declared as typeof en and re-exported from that same entry, and the packs are also addressable through the published ./locales and ./locales/* subpaths. Five new keys under marketplace.install (deploying, deployed, openApp, deployTimeout, checkAgain) add five members to a published type; check:i18n-drift reads 5 added, 0 removed, 0 en values changed, all ten packs. Clause-②: yes is the right declaration (a widening), and the changeset now says so in its own words (②).
  • Published surface, @object-ui/app-shell — RIGHT that nothing new reaches a package entry. src/index.ts is untouched; the package's exports map is . and ./styles.css only. The new waitForServedApp.ts with its exported waitForServedApp, SERVED_APP_POLL_INTERVAL_MS, SERVED_APP_WAIT_CAP_MS, ServedApp, ServedAppWait and ServedAppWaitOptions ships as bytes under dist/ that no exports entry addresses and no entry type reaches: shipped bytes, not a published accept set. MarketplacePackagePage (entry-exported) keeps its signature. A behaviour fix in published source: patch for app-shell is right.
  • The served-app predicate — RIGHT identity. On objectstack origin/main (96469912) _packageId is stamped by applyProtection(item, { packageId }), reached from registry.registerItem(type, item, keyField, packageId) and from the artifact loader's registerArtifactBodyCollections, whose packageId is manifestPackageId = metadata.manifest.id (ADR-0130 D7). PackageVersionSchema declares the manifest id "must match the parent sys_package.manifest_id". The /meta/:type list answer runs its per-caller gate on the raw doc items and strips nothing _-prefixed; objectui already routes /apps/ by matchAppBySegment on _packageId from this very list (AppContent, AppHeader, AppSwitcher, useNavigationSync), so the field is known to reach the wire. _packageId === manifest_id is the one identity the install answer and GET /meta/app share. Its known blind spot is stated honestly in the module header and the PR: an upgrade is satisfied by the old version's app, since registerItem stamps no _packageVersion.
  • No app-list read during the wait reaches the cache or the seed — RIGHT. readApps is adapter.getClient().meta.getItems('app'); getClient() returns the raw ObjectStackClient, whose meta.getItems is a plain fetch plus unwrapResponse with no client-side cache (the adapter's MetadataCache covers getObjectSchema only, and the /meta list endpoint carries no HTTP cache). The only writer of objectui:metadata:app:... is MetadataProvider.ensureType, which the wait never calls; the seed drop, refreshMetadata() and emitMetadataRefresh() all sit after wait.served. The bus pulse genuinely was a second in-window persist on main: the provider's subscribeMetadataRefresh handler drops the adapter cache and re-reads every loaded type. The page test records every app seed write through the real provider and real bus and asserts none after the install lacks the app; the ablation (in-window refresh put back) fails exactly that assertion while the control stays green. Residual, correctly stated by the dev: other surfaces (a route change, a Studio or AI publish pulse, the catalog page's org install) can still persist a stale list inside the window; the post-served refresh overwrites it, so the end state is right whenever the wait settles. The provider is untouched, within the claim's surface.
  • The bound and the timeout text — RIGHT. First read at once (control case: served on the first read, zero sleeps); then one read per SERVED_APP_POLL_INTERVAL_MS (5 s) while now() + intervalMs is within SERVED_APP_WAIT_CAP_MS (5 min): at most 61 reads, wall-clock bounded, a failed read counts as no answer and only the bound ends the wait. Nothing models the rebuild duration (triage: no console-side guess). On expiry the page refreshes nothing and shows deployTimeout, which says the install was recorded, the app is not served yet, and the four possible causes; it claims no success. marketplace.install.success is never shown for an install into this environment.
  • Deviation (a), SuggestedBindingsPanel mounts once served — RIGHT. The panel reads listSuggestedBindings once, in a mount effect keyed on packageId; mounted on install success it would read the pre-install kernel.
  • Deviation (b), a cross-environment install no longer refreshes this console or pulses the bus — RIGHT. installLandsInThisEnvironment reads the same two facts installPackage routes on: with a cloud base the same-origin /cloud-connection/install proxy posts only package_id, so the hostname's environment is the target; without one the control plane installs into the picked environment_id. The old comment itself called the cross-environment refresh a no-op.
  • The third narrowing, now in the PR's acceptance notes — RIGHT, and the wording is right. The note says: on the control-plane path (no cloud base) main refreshed when defaultEnvironmentId was null as well as when it equalled selectedEnv, the head only when it equals selectedEnv, since a runtime that names no environment cannot be that install's target; on the same-origin proxy path (a cloud base set) the head waits and refreshes whatever defaultEnvironmentId says, because the proxy installs into the environment the hostname names. Both halves match the code: installLandsInThisEnvironment returns true on getCloudBase() before reading the id, and installPackage's proxy branch sends only package_id. The qualification also covers a corner the note does not spell out: on the proxy path with defaultEnvironmentId set and a different environment picked in the dialog, main did NOT refresh while the head waits and refreshes — right, because the proxy ignores the picked id and the install does land here. Not blocking.
  • Not cancelled on unmount — not a leak, and not a defect. One pending promise and one setTimeout at a time, ending at the cap; no interval. react is 19.2.8 here, where a state write on an unmounted component is a silent no-op; refreshMetadata, the adapter and emitMetadataRefresh all belong to the still-mounted provider, which is the intended effect (the cache lands for an operator who left the page). One cosmetic gap: a remounted page starts with deploy === null, so it shows no deploying state and re-enables Install while the detached wait runs; a second install starts a second bounded wait. Not blocking.
  • Check-runs on the head: 42 of 43 green or skipped; one red, Spec Main Shape Gate (run 114237633531), a REQUIRED context on main. Its ten failures name the same four files as on the previous head — three under packages/types/src/__tests__/ (grid-default-filters-gantt-map-filter-round10-6152, element-repeater-data-source-11880, authoring-nodes-11364) and packages/data-objectstack/src/metadata-client.overlayScope.test.ts — now compiled against objectstack e250d8f8. None of the four is in this diff, and the hop touched no source at all. The merge-base de302c73 ran the same gate green at 10:08Z (run 114189363509), before objectstack #22421 and #22628 moved the spec, so the "same signature on the base" discount does not apply literally; this is the shared-infrastructure red objectui#12093 holds. The red does not touch this PR. It does block landing: the owning seat cannot queue until Merge queue: Spec Main Shape Gate fails every merge group since objectstack#22421 retired the flat element binding keys; five objectui test files still use them #12093's fix lands and this branch merges main and re-runs green.
  • Hygiene. Four commits, each with the model-free trailer pair; no model identifier in the diff, the PR body or the changeset; no cross-file line citation in the diff (Line Citation Gate green); no inline styles; dark variants on the new note; role="status" on it; head repo is the base repo; draft, not armed; 785 changed lines.

② Semver level

Consistent on this head. .changeset/12087-install-wait-served.md now declares '@object-ui/app-shell': patch and '@object-ui/i18n': minor, and its closing sentence states the published effect truthfully: the exported en pack and the TranslationKeys type derived from it (typeof en, exported from the @object-ui/i18n entry) gain the five marketplace.install members, which is why @object-ui/i18n is a minor bump, and no export, prop or type on the @object-ui/app-shell entry changes. That agrees with the diff (①), with the claim's and the PR's Clause-②: yes (a widening takes at least minor), and with the repo's own pending changesets for the same shape (.changeset/11666-launcher-plan-awaiting-approval.md, .changeset/11667-ai-chat-labels-i18n.md). patch for app-shell is right: a behaviour fix in an entry-exported page, no new export. No changeset declares major, as the fixed group requires; a minor on one member moves the whole group, which is what the dev's pnpm changeset status reading reports. On the head, Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read and Changeset Overwrite Report are all green. The previous record's FAIL is discharged.

③ Boundary flags

open_questions is empty. Every dev flag answered; the patch round's own items first:

  • REWORK 6098626260, items 1 and 2 (changeset level and closing sentence). Done at c88aaf17, exactly as asked, in one commit that changes nothing else. Item 3 (the third narrowing in the acceptance notes): done, wording judged right in ①.
  • Deviation 1 — new file waitForServedApp.ts outside the claim's listed surface. Accepted. A new sibling module in the same directory, held by no other claim (the claim's own serial-constraints line read the in-flight branches); it reaches no published entry (①), and it is what lets the page test shrink the bound.
  • Deviation 2 — the two behaviour changes (a) and (b). Both judged right in ①. The third narrowing is now in the acceptance notes; nothing further.
  • Deviation 3 — resumed after the 429 kill, fast-forwarded to de302c7. Answered: the net diff is against that base, four commits, no residue from the earlier attempt.
  • Deviation 4 — Clause-②: yes confirmed by the diff. Right, and the changeset now agrees with it (②).
  • check:readme-exports NOT MEASURED locally. Answered by the head's README Export Check run: green.
  • Live repro NOT MEASURED (no stale-while-rebuild stack in the container; KernelManager lives in cloud; staging needs the maintainer's session). Accepted for this review: the window is modelled by a fake data source over the real provider and real bus, and the ablation proves the pin bites. Escalated to the seat: ask the maintainer for one staging install after landing (the card's own repro at +10 s, +90 s and after a same-tab reload) and record the reading on the card; premise_still_valid: true rests on the card's timeline, not on a repro.
  • out_of_scope_findings 1 — the console cannot tell "still rebuilding" from "did not take" (nor "no app", "withheld", or an upgrade's old app). Established by code read, as far as reading can. Triage made the server alternative conditional on exactly this; the condition is met. Escalated: the seat files the server-surface card (a "rebuild pending / kernel generation" read of the freshness state the KernelManager already logs — cloud, with an objectstack half if registerItem is to stamp _packageVersion), dedupe words as the dev listed. Not filed by the dev, correctly: the claim named a console surface.
  • out_of_scope_findings 2 — MarketplacePage.doOrgInstall has the same in-window persist on its cloud-managed branch. Code read, same defect class, outside the claim's surface, left untouched as instructed. Escalated: the seat files a follow-up card on objectui citing the code read and waitForServedApp as the ready reuse. Not buried: it is in the PR's acceptance notes.
  • out_of_scope_findings 3 — objectstack marketplace-install-local-plugin answers hotLoaded: true on its lenient path where the hot register failed. A declared-not-enforced answer (the console then shows localSuccess over a list without the app). Escalated: the seat files it on objectstack as a contract finding; carrier was "none" in the report, so without this line it is lost.
  • Landing precondition outside this record. Spec Main Shape Gate is required and red on this head for objectui#12093's signature (①), unchanged by the hop. This PASS does not lift it: the owning seat waits for Merge queue: Spec Main Shape Gate fails every merge group since objectstack#22421 retired the flat element binding keys; five objectui test files still use them #12093's fix (folded into objectui#12085's branch) to merge, merges main into this branch, and arms only on a green re-run; a re-run of the same commit cannot clear it, since the gate compiles against objectstack main and the fix is in objectui test files this branch does not yet carry.

Implemented-by: claude/issue-12087-install-wait-served
Reviewed-by: session_01B1gHb9baeX7oioD5sHVm7z

VERDICT: PASS


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3169.5 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-Fck4BwWw.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) 19.75KB 7.29KB
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) 41.19KB 11.12KB
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) 586.66KB 141.23KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
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) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 14.32KB 5.17KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.97KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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.07KB 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.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5b11c46e70ed582a965b7e93f42abb1f287b3cec
Local-runs: none

Re-render on the landing head. The hop from the PASS head c88aaf17 is one merge commit, Merge branch 'main' into claude/issue-12087-install-wait-served, authored by the fleet account and committed by GitHub (the platform's update-branch shape, so no agent trailer pair is owed and none is present; no model identifier in it). Its second parent is origin/main at ce991bd7, which is exactly one commit past the old base de302c73: objectui#12100, the Spec Main Shape Gate fix for objectui#12085 and objectui#12093, 22 files. Verified by content, not by report: the net diff origin/main...5b11c46e is the same 15 files, +749/−36; the hop diff restricted to those 15 paths is empty, and each of the 15 blobs at 5b11c46e is byte-identical to its blob at c88aaf17; the 22 files main brought intersect the PR's 15 in nothing. The sixteen files ① and the changeset sentences rest on (the i18n entry, i18n.ts, locales/index.ts, both package.json exports maps, the app-shell entry, MetadataProvider.tsx, assistantBus.ts, appRoute.ts, utils/index.ts, runtime-config.ts, marketplaceApi.ts, SuggestedBindingsPanel.tsx, the data-objectstack adapter, AppShellContext.tsx, useObjectLabel.ts) are each blob-identical between de302c73 and 5b11c46e. Every judgment below therefore carries forward on the same evidence; what changes is the gate reading and ③'s escalations, which the seat has since discharged.

① Derived judgments

  • Published surface, @object-ui/i18n — RIGHT that it is one, and it is a widening. At the head, en is exported from the package entry, TranslationKeys is declared as typeof en and re-exported from that same entry, and the packs are also addressable through the published ./locales and ./locales/* subpaths (the exports map at the head is ., ./locales, ./locales/*). Five new keys under marketplace.install (deploying, deployed, openApp, deployTimeout, checkAgain) add five members to a published type; check:i18n-drift reads 5 added, 0 removed, 0 en values changed, all ten packs. Clause-②: yes is the right declaration (a widening), and the changeset says so in its own words (②).
  • Published surface, @object-ui/app-shell — RIGHT that nothing new reaches a package entry. src/index.ts is untouched and names waitForServedApp nowhere; the package's exports map at the head is . and ./styles.css only. The new waitForServedApp.ts with its exported waitForServedApp, SERVED_APP_POLL_INTERVAL_MS, SERVED_APP_WAIT_CAP_MS, ServedApp, ServedAppWait and ServedAppWaitOptions ships as bytes under dist/ that no exports entry addresses and no entry type reaches: shipped bytes, not a published accept set. MarketplacePackagePage (entry-exported) keeps its signature. A behaviour fix in published source: patch for app-shell is right.
  • The served-app predicate — RIGHT identity. On objectstack origin/main (96469912) _packageId is stamped by applyProtection(item, { packageId }), reached from registry.registerItem(type, item, keyField, packageId) and from the artifact loader's registerArtifactBodyCollections, whose packageId is manifestPackageId = metadata.manifest.id (ADR-0130 D7). PackageVersionSchema declares the manifest id "must match the parent sys_package.manifest_id". The /meta/:type list answer runs its per-caller gate on the raw doc items and strips nothing _-prefixed; objectui already routes /apps/ by matchAppBySegment on _packageId from this very list (AppContent, AppHeader, AppSwitcher, useNavigationSync), so the field is known to reach the wire. _packageId === manifest_id is the one identity the install answer and GET /meta/app share. Its known blind spot is stated honestly in the module header and the PR: an upgrade is satisfied by the old version's app, since registerItem stamps no _packageVersion.
  • No app-list read during the wait reaches the cache or the seed — RIGHT. readApps is adapter.getClient().meta.getItems('app'); getClient() returns the raw ObjectStackClient, whose meta.getItems is a plain fetch plus unwrapResponse with no client-side cache (the adapter's MetadataCache covers getObjectSchema only, and the /meta list endpoint carries no HTTP cache). The only writer of objectui:metadata:app:... is MetadataProvider.ensureType, which the wait never calls; the seed drop, refreshMetadata() and emitMetadataRefresh() all sit after wait.served. The bus pulse genuinely was a second in-window persist on main: the provider's subscribeMetadataRefresh handler drops the adapter cache and re-reads every loaded type. The page test records every app seed write through the real provider and real bus and asserts none after the install lacks the app; the ablation (in-window refresh put back) fails exactly that assertion while the control stays green. Residual, correctly stated by the dev: other surfaces (a route change, a Studio or AI publish pulse, the catalog page's org install) can still persist a stale list inside the window; the post-served refresh overwrites it, so the end state is right whenever the wait settles. The provider is untouched, within the claim's surface.
  • The bound and the timeout text — RIGHT. First read at once (control case: served on the first read, zero sleeps); then one read per SERVED_APP_POLL_INTERVAL_MS (5 s) while now() + intervalMs is within SERVED_APP_WAIT_CAP_MS (5 min): at most 61 reads, wall-clock bounded, a failed read counts as no answer and only the bound ends the wait. Nothing models the rebuild duration (triage: no console-side guess). On expiry the page refreshes nothing and shows deployTimeout, which says the install was recorded, the app is not served yet, and the four possible causes; it claims no success. marketplace.install.success is never shown for an install into this environment.
  • Deviation (a), SuggestedBindingsPanel mounts once served — RIGHT. The panel reads listSuggestedBindings once, in a mount effect keyed on packageId; mounted on install success it would read the pre-install kernel.
  • Deviation (b), a cross-environment install no longer refreshes this console or pulses the bus — RIGHT. installLandsInThisEnvironment reads the same two facts installPackage routes on: with a cloud base the same-origin /cloud-connection/install proxy posts only package_id, so the hostname's environment is the target; without one the control plane installs into the picked environment_id. The old comment itself called the cross-environment refresh a no-op.
  • The third narrowing, in the PR's acceptance notes — RIGHT, and the wording is right. The note says: on the control-plane path (no cloud base) main refreshed when defaultEnvironmentId was null as well as when it equalled selectedEnv, the head only when it equals selectedEnv, since a runtime that names no environment cannot be that install's target; on the same-origin proxy path (a cloud base set) the head waits and refreshes whatever defaultEnvironmentId says, because the proxy installs into the environment the hostname names. Both halves match the code: installLandsInThisEnvironment returns true on getCloudBase() before reading the id, and installPackage's proxy branch sends only package_id. The qualification also covers a corner the note does not spell out: on the proxy path with defaultEnvironmentId set and a different environment picked in the dialog, main did NOT refresh while the head waits and refreshes — right, because the proxy ignores the picked id and the install does land here. Not blocking.
  • Not cancelled on unmount — not a leak, and not a defect. One pending promise and one setTimeout at a time, ending at the cap; no interval. react is 19.2.8 here, where a state write on an unmounted component is a silent no-op; refreshMetadata, the adapter and emitMetadataRefresh all belong to the still-mounted provider, which is the intended effect (the cache lands for an operator who left the page). One cosmetic gap: a remounted page starts with deploy === null, so it shows no deploying state and re-enables Install while the detached wait runs; a second install starts a second bounded wait. Not blocking; the ACCEPT notes it as out of scope.
  • Check-runs on the head: 43 runs, 40 green, 3 skipped (dependabot and the two coverage legs), no red. Every required context on main is green: Lint, Type Check, Build & E2E, Build Docs, Changeset Declaration, Test, Spec Main Shape Gate (run 114294305697, success) and Governed Surface Queue Guard. The Spec Main Shape Gate red of the two previous heads was objectui#12093's signature in four files outside this diff; the merge of ce991bd7 (objectui#12100) brings the fixed test files into this branch, and the gate now compiles green on this head. The landing precondition the previous records named is discharged. The PR's mergeable_state reads clean.
  • Hygiene. Four agent commits, each with the model-free trailer pair, plus the platform-made base merge above; no model identifier in the diff, the PR body, the changeset or the merge commit; no cross-file line citation in the diff (Line Citation Gate green); no inline styles; dark variants on the new note; role="status" on it; head repo is the base repo; draft, not armed; 785 changed lines; the PR body is unchanged since the previous record.

② Semver level

Consistent on this head, and re-read against the code at this head. .changeset/12087-install-wait-served.md (blob-identical to the PASS head) declares '@object-ui/app-shell': patch and '@object-ui/i18n': minor. Its sentences hold at 5b11c46e: the exported en pack and the TranslationKeys type derived from it (typeof en, exported from the @object-ui/i18n entry) gain the five marketplace.install members — true, the declaration and the re-export are at the head's en.ts and index.ts; no export, prop or type on the @object-ui/app-shell entry changes — true, the app-shell entry and its exports map are byte-identical to the old base. The behaviour sentences (read GET /meta/app past the cache at once, then every 5 s for up to 5 minutes, until an app's _packageId is the package's manifest id; persist nothing during the wait; the deploying, served and timeout states; bindings mount once served; a cross-environment install refreshes nothing here) each describe code in the diff that ① verified. That agrees with the claim's and the PR's Clause-②: yes (a widening takes at least minor) and with the repo's own pending changesets for the same shape (.changeset/11666-launcher-plan-awaiting-approval.md, .changeset/11667-ai-chat-labels-i18n.md), both still pending on main at ce991bd7. patch for app-shell is right. No changeset declares major, as the fixed group requires; a minor on one member moves the whole group. Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read and Changeset Overwrite Report are green on the head.

③ Boundary flags

open_questions is empty. Every dev flag answered; the ACCEPT 6100411973 on the card records the seat's discharge of the previous record's escalations, which this record reads as a card comment and does not re-verify beyond the card:

  • REWORK 6098626260, items 1 to 3. Done at c88aaf17 and carried unchanged across the merge.
  • Deviation 1 — new file waitForServedApp.ts outside the claim's listed surface. Accepted. A new sibling module in the same directory, held by no other claim; it reaches no published entry (①), and it is what lets the page test shrink the bound.
  • Deviation 2 — the two behaviour changes (a) and (b). Both judged right in ①. The third narrowing is in the acceptance notes; nothing further.
  • Deviation 3 — resumed after the 429 kill, fast-forwarded to de302c7. Answered: the net diff is against main, now at ce991bd7, with no residue from the earlier attempt.
  • Deviation 4 — Clause-②: yes confirmed by the diff. Right, and the changeset agrees with it (②).
  • check:readme-exports NOT MEASURED locally. Answered by the head's README Export Check run: green.
  • Live repro NOT MEASURED (no stale-while-rebuild stack in the container; KernelManager lives in cloud; staging needs the maintainer's session). Accepted for this review on the modelled window and the ablation. The ACCEPT records the ask to the maintainer: one staging install after landing (the card's repro at +10 s, +90 s and after a same-tab reload), with the reading recorded on the card. Still owed after landing; not a landing condition.
  • out_of_scope_findings 1 — the console cannot tell "still rebuilding" from "did not take" (nor "no app", "withheld", or an upgrade's old app). The server-surface card is filed, per the ACCEPT: registry: an app registered through registerItem is served without the declared _packageVersion (the artifact loader path stamps it), so a console cannot tell an upgrade's old app from its new one objectstack#22689. Discharged.
  • out_of_scope_findings 2 — MarketplacePage.doOrgInstall has the same in-window persist on its cloud-managed branch. Filed, per the ACCEPT: objectui#12097, depending on this PR. Discharged.
  • out_of_scope_findings 3 — objectstack marketplace-install-local-plugin answers hotLoaded: true on its lenient path. Filed, per the ACCEPT: install-local: a cloud install whose hot-register fails answers 200 with hotLoaded: true (the lenient path), so a console says the app "should now appear" over a kernel that does not have it objectstack#22695. Discharged.
  • The base merge itself. A pure hop: no PR file moved, nothing ① rests on moved, and the one main commit it brings is the gate fix. It needed no new record on its own; this one is rendered because the dispatch asked for the landing head to carry one.
  • Landing. Nothing in this record holds the PR back: every check on the head is green, the required set included; the PR is a non-fork draft with auto-merge unarmed, 785 changed lines, no governed path. The owning seat flips it ready and arms it for the queue in that order.

Implemented-by: claude/issue-12087-install-wait-served
Reviewed-by: session_01B1gHb9baeX7oioD5sHVm7z

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 19:50
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 19:50
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 3c0f806 Oct 10, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12087-install-wait-served branch October 10, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants