Skip to content

fix(app-shell): keep the approval envelope + pending-action id in the chat cache - #9450

Merged
os-tesla merged 2 commits into
mainfrom
claude/issue-9232-cache-approval-envelope
Sep 14, 2026
Merged

os-tesla merged 2 commits into
mainfrom
claude/issue-9232-cache-approval-envelope

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #9232

sanitizeChatMessagesForCache rebuilds each tool part field by field — type, toolCallId, toolName, state, errorText?, output? — and wrote neither the AI SDK approval envelope nor the ObjectStack pendingActionId. state survived, so a cached approval-requested came back with the awaiting-approval card lit and nothing behind it: useHitlInChat indexes decisions on pendingActionId, so pressing Approve could only answer "No pending-action id found for this tool call". The output re-serialization covered replayOutcome / draftReview / proposedPlan only, so the id was not recoverable from the cached output either.

This is the mirror of objectui#8442, which closed the same gap in the other direction (hydratedMessagesToChatMessages, the way OUT of persisted server history). PR #9229 merged 2026-09-12, so the divergence was live on main, not prospective. The cache fallback is not a corner — readMessageCache is what renders the thread whenever the server returns no messages.

Correction in the second commit — read this first

The first commit put the pending arm first in the cachedOutput chain, arguing the four detectors were "disjoint by construction, so the order is unobservable". That argument was wrong, and a pre-existing pin caught it: AiChatPage.runtimeMessageSeam.test.tsx went red on CI shard 3.

The detectors are disjoint over one result. But draftReview and pendingActionId are independent keys on an invocation, not two readings of one result, and a turn can carry both — that fixture does. Pending-first therefore did not reorder equals; it stopped the draft envelope reaching the cache for such a turn. That is the same "Review N changes / Publish" loss on a cache-fallback reload that the other three arms exist to prevent — and the same loss this PR's own stale-cache reasoning refused to accept from a key bump. The precedence was a choice described as a non-choice.

The second commit fixes the mechanism rather than the symptom. AiChatPage.runtimeMessageSeam.test.tsx is unedited and passes.

What changed

output holds one envelope, so the three existing arms keep it exactly as before and the pending envelope is minted only when none of them claims it. The id is no longer hostage to that slot: it is also written as a cache-side part key.

The two carriers are not redundant — each reaches where the other cannot, and that is the design:

  • output is the only carrier that survives API mode's SDK store. useObjectChat's aiInitialMessages rebuilds each part from {type, toolCallId, toolName, input, output, errorText, state} and drops every other key, after which extractToolInvocations re-derives the id by re-parsing the result. A pending-only turn lands here — and that is the only shape API mode can produce, since detectDraftResult and detectPendingApproval read one parseResultEnvelope(result) and require different status values.
  • The part key is the only carrier left when a richer envelope has taken output. Local mode keeps it, because normalizeMessages passes toolInvocations through verbatim, and hydratedMessagesToChatMessages lifts it on the way back.

approval continues to ride the part key on both directions of the server path, where partApproval narrows it straight back.

No second dialect. hydratedMessagesToChatMessages consults detectPendingApproval first and only falls back to the part key. On the server path the envelope always answers, so that line behaves exactly as objectui#8442 left it; the fallback exists solely for the cache, which is the only writer of that key.

Clause-②: yes — re-declared, overturning the claim's prediction

The claim declared Clause-②: no on the basis that sanitizeChatMessagesForCache is not exported. That is still true, and for the first commit the declaration was right. The second commit changes the behaviour of hydratedMessagesToChatMessages, which packages/app-shell/src/index.ts does re-export. The signature and return type are unchanged and the change is additive — it recovers an id in a case that previously yielded none — but it moves a published function, so the declaration is re-made as yes and needs:contract-review is attached to both the card and this PR. The diff is the fact; the prediction was made before the diff existed.

check-governed-queue-guard.mjs --test on all four paths: NOT GOVERNED, none of 5 governed surfaces matched.

Stale cache: read-side tolerance, deliberately — not a version bump, not a discard

Entries already in a user's localStorage lack the new fields. The decision is to keep and read them, recorded on readMessageCache and pinned by two tests:

  1. Nothing can break on an old entry. The payload is a list of open records and both readers of the new keys already answer "absent" rather than throwing — partApproval returns undefined for a missing envelope, detectPendingApproval returns undefined for a result that is not one.
  2. A bump or discard costs more than it buys. It would blank the transcript, the draft card, the ADR-0038 chip, the plan card and the replay verdict — everything the old writer got right — to recover one affordance, on the one path that renders when the server has nothing to serve.
  3. Old entries self-heal. The cache is rewritten from runtimeMessages on every render, so the first server-backed load restores the id through the objectui#8442 path and writes it back in the new shape.

Tolerance does not invent: an old entry keeps approval-requested with no id — today's behaviour — and no id is fabricated.

Evidence

Readings at 95f2ff0, tree clean. Repo root, path-filtered, per AGENTS.md; heavy runs through scripts/pm/os-verify-lock.sh, and every verdict is the wrapper's VERDICT command-exit line or the tool's own summary line.

Scope actually run — stated plainly, because the first round's scope is how this got through.

  • pnpm test --shard=3/4, the exact CI invocation: Test Files 776 passed (776), Tests 10360 passed | 1 skipped (10361), exit 0.
  • ⚠️ That local shard did NOT contain the file that failed on CI. CI's shard 3 reported 777 files and 10368 passing tests; the local one has exactly one file and exactly eight tests fewer, and AiChatPage.runtimeMessageSeam.test.tsx has exactly 8 tests. Vitest partitions by position in the discovered file list, so a file set that differs anywhere reshuffles membership — local shard 3 is not CI's shard 3. Reported rather than presented as "shard green, therefore fixed".
  • So the previously-failing file was verified directly: pnpm exec vitest run packages/app-shell/src/console/ai/__tests__/AiChatPage.runtimeMessageSeam.test.tsx -> Test Files 1 passed (1), Tests 8 passed (8), unedited.
  • The four directly implicated files together: Test Files 4 passed (4), Tests 79 passed (79).
  • The full suite (all shards) was also launched, as the unambiguous superset; its result is reported in the comment thread if it landed after this body was written.

Typecheck — pnpm --filter @object-ui/app-shell run type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0, after pnpm --filter '@object-ui/app-shell^...' build (exit 0) supplied the workspace dist/*.d.ts. Run before that build it reports 26 TS2307, which is a precondition failure and NOT MEASURED, never a red verdict.

Gates — eleven, each exit 0: check-control-bytes, check-changeset-presence, check-changeset-claims, check-changeset-fixed, check-changeset-no-major, check-changeset-overwrite, check-test-path-roots, check-vi-mock-override-shape, check-vi-mock-specifiers, check-vi-mock-inherit, check-new-cross-file-line-citations. Plus a control-byte self-scan over all four changed files: no hits.

Lint — a declared narrowing with its three readings. (1) Population: ESLint's own configured file count is 4954. (2) The narrowed run covered 3 files (--format json): 0 errors, 36 warnings. (3) Immutability: eslint.config.js sets languageOptions to ecmaVersion + globals only — no parserOptions.project, no projectService — so no rule here is type-aware and a three-file change cannot move any untouched file's verdict. Every warning is pre-existing: the AiChatPage.tsx hunk spans lines 244-255 and the nearest warnings sit at 191 and 302, while useChatConversation.ts carries the same five react-hooks/* warnings as before, shifted only by added comment lines. Repo-wide pnpm lint is CI's run.

Ablation — three legs, each predicted in writing before it ran. The fix has two carriers, so one blanket mutation would have proved nothing about either; the legs separate them.

Leg Mutation Predicted Outcome
A delete only the output arm RED, 1 failed: the output pin. The driven pending-only test stays green exactly that — 1 failed | 45 passed (46)
B delete only the part key + the read-side fallback RED, 1 failed: the both-at-once test, first at toolPart?.pendingActionId exactly that — 1 failed | 45 passed (46), AssertionError: expected undefined to be 'pa_9232'
C delete both carriers RED, 3 failed: tests 1, 2 and 5, with 2 and 5 at their urls assertions exactly that — 3 failed | 43 passed (46), including AssertionError: expected [] to deeply equal [ Array(1) ]

Leg A is the honest one and its prediction says so in advance: at the hydration boundary the part key alone still carries the id, so these tests cannot see output's loss. output's necessity is for the API-mode SDK store rebuild, which no test here crosses — it is argued from the source, not measured. Leg B is the one that matters this round: it proves the new carrier is load-bearing rather than decoration.

Mutation proved on disk, never off an editor exit code: anchor greps went 1 to 0 for each deleted anchor, and blob hashes moved (2459da19 to 69e417d5 / 95924f2f / e12412cc on the writer; 5fa6843d to 84493432 on the reader, unchanged in leg A as expected since leg A does not touch it). Restore proved by state after every leg: git diff HEAD empty, 0 lines. The script carries trap ... EXIT INT TERM with absolute repo-root paths, restores with git checkout HEAD -- FILE, and refuses to start unless both files are already at HEAD — which it did refuse, correctly, on the first attempt before the patch was committed.

Not measured: browser behaviour, and output's necessity for the API-mode store (see leg A).

Acceptance notes

A targeted dedup search was run before recording these: REST /search/* is refused for this session by the egress proxy (HTTP 403, "sessions are bound to their configured repositories"), so one MCP search_issues call was used instead and is declared here. Its first hit was objectui#9232 itself, the lit control proving the query reaches this surface; no open card names either observation.

  • The AI SDK approval envelope is currently write-only on every path. extractToolInvocations does not lift part.approval, and aiInitialMessages drops it re-entering the SDK store; grep finds no consumer of tool.approval anywhere. No observable symptom, so declared-but-unconsumed rather than a defect. Noted, not filed. Successor: objectui#8426, which the @object-ui/types docstring names as the follow-up that makes the envelope load-bearing. This PR writes it anyway, for parity with what the server path persists.
  • isAwaitingApproval lights Approve / Reject without requiring pendingActionId. An invocation with the state but no id renders live buttons whose only outcome is the handled error in decide(). That handler is deliberate and user-facing rather than a crash, so this is UX polish, not a defect. Noted, not filed. Successor: objectui#2477, the open pm:queue umbrella for Console AI chat UX follow-ups.
  • Correcting a finding from the first round. It recorded that no label write was owed, on the reading that nothing in .github/workflows reads a PR label. The reading still holds mechanically, but it was the wrong frame: the Clause-② flip makes needs:contract-review owed as a review routing signal to a human, not as a gate input. It is attached to both the card and this PR, and both were read back to confirm.

Produced by Claude Code, seat session https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa. The session id identifies the SEAT, not this individual change.


Generated by Claude Code

… chat cache (objectui#9232)

`sanitizeChatMessagesForCache` rebuilds each tool part field by field and wrote
neither the AI SDK `approval` envelope nor the ObjectStack `pendingActionId`.
`state` survived, so a cached `approval-requested` came back with the
awaiting-approval card lit and nothing behind it: `useHitlInChat` indexes on
`pendingActionId`, so Approve could only answer "No pending-action id found for
this tool call". The mirror of objectui#8442, which closed the same gap on the
way OUT of persisted server history.

The id travels asymmetrically, and the fix follows that rather than flattening
it. `approval` is a persisted PART key on the server path, so the cache writes
it as one and `partApproval` reads it straight back. `pendingActionId` is never
a part key anywhere, so it round-trips through a new
`pendingApprovalToCachedResult` inverse that re-mints the minimal
`{ status: 'pending_approval', pendingActionId }` envelope
`detectPendingApproval` re-parses — one parse for one contract, no second
reader on the cache side.

Entries written by the old shape are kept and read, not discarded: both readers
of the new keys already answer "absent" rather than throwing, a version bump
would blank the transcript and cards the old writer did keep, and old entries
self-heal on the next server-backed load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.3 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-D0VhSLgX.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) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 544.89KB 130.48KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.27KB 65.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
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) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 0.20KB 0.18KB
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) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

…placing the draft envelope

Patch round on objectui#9232. The first version put the pending-approval arm
FIRST in the `cachedOutput` chain, on the argument that the four detectors are
"disjoint by construction, so the order is unobservable". That argument was
wrong and `AiChatPage.runtimeMessageSeam.test.tsx` already pinned the
counter-example: the detectors are disjoint over one RESULT, but `draftReview`
and `pendingActionId` are independent KEYS on an invocation and a turn can
carry both. Pending-first therefore stopped the draft envelope reaching the
cache for such a turn — the same "Review N changes / Publish" loss on a
cache-fallback reload that the other three arms exist to prevent, and the same
loss this card's own stale-cache reasoning refused to accept from a key bump.

`output` holds one envelope, so the three existing arms keep it exactly as
before and the pending envelope is minted only when none of them claims it.
The id is no longer hostage to that slot: it is also written as a cache-side
part key, and `hydratedMessagesToChatMessages` consults `detectPendingApproval`
FIRST and only falls back to the key. On the server path the envelope always
answers, so that line behaves exactly as objectui#8442 left it and no second
dialect of the contract is introduced.

The two carriers are not redundant — each reaches where the other cannot.
`output` is the only one that survives API mode's SDK store, because
`aiInitialMessages` rebuilds each part from {type,toolCallId,toolName,input,
output,errorText,state} and drops every other key; the part key is the only one
left when a richer envelope has taken `output`, and local mode keeps it because
`normalizeMessages` passes `toolInvocations` through verbatim.

The falsified precedence pin is replaced by a both-at-once pin that drives the
round trip: the draft card comes back AND Approve reaches the pending action.
The over-reaching prose claim is replaced by the narrow claim that is true, as
an executable pin with lit controls: over ONE result the two envelopes are
mutually exclusive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.3 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-CZHDDSPc.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) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 544.89KB 130.48KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.27KB 65.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
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) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 0.20KB 0.18KB
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) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator Author

Contract review

Head reviewed: 95f2ff0
Seat: domain:ui (os-tesla) · default judgment tier — the contract-review tier and its fuse bind only the spec and skills seats.

Implemented-by: claude/issue-9232-cache-approval-envelope
Reviewed-by: session_011QreXiyMEqKLN4U5daMPVa

⚠️ Read the timing note at the end before relying on this record.

① Derived judgments

  1. ⭐ hydratedMessagesToChatMessages — the published surface, and the dev is right to have flipped the declaration. Verified independently: packages/app-shell/src/index.ts:284 re-exports it. Round 1's Clause-②: no was correct for commit 1 (whose only writer, sanitizeChatMessagesForCache, is unexported) and went stale at commit 2. Catching that against its own earlier declaration, unprompted, is the behaviour the clause exists to produce.
    Judged sound. Signature and return type unchanged. The change is detectPendingApproval(result)?.pendingActionId ?? partString(part, 'pendingActionId') — the ?? fires only when the left side is already undefined, so no caller can observe a value change; the only observable difference is an id appearing where the function previously yielded none. On the server path the envelope always answers and the line behaves exactly as objectui#8442 left it, so ⛔ no second dialect is introduced.
  2. The new cache-side part key is written by sanitizeChatMessagesForCache, which is not exported (index.ts re-exports exactly four symbols from that module and this is not among them). Not a published surface.
  3. AiChatPage.tsx is app-local, not a package export surface.
  4. Two carriers, ⛔ not redundancy. Each reaches where the other cannot, and the diff matches that claim: output is the only carrier surviving API mode's SDK store (aiInitialMessages rebuilds each part from a fixed key set and drops the rest), while the part key is the only one left when a richer envelope has taken output. ⇒ the accept set is not widened; a second channel is added for a value that was already contractually meant to be there.

② Semver

'@object-ui/app-shell': patch. Judged correct, and the reasoning is not "it's small": no API surface is added, removed or retyped, and the behaviour change is a defect repair restoring the parity objectui#8442 established in the other direction. The only changed output is the recovery of a value the cached data already contained. ⛔ A minor would be wrong here — nothing new is offered to a caller.

③ Boundary flags

  • File-surface breach, declared rather than buried: the claim scoped the dev to packages/app-shell/src/hooks/; the read-side lift required packages/app-shell/src/console/ai/AiChatPage.tsx. Accepted — the patch-round instruction was explicitly to fix the mechanism and ⛔ not to accept a reorder that merely made the old test pass, which cannot be done from the writer alone. 12 lines at one site. Flagging it was correct.
  • NOT MEASURED, both correctly refused a green: (a) the full suite — launched, then killed by the dev at ~50 min by PID, never pkill -f, because it had become the long lock-holder starving sibling agents. ⭐ The dev read the log (ELIFECYCLE, Terminated, FULL_WRAPPER_EXIT=130, no summary) rather than trusting a harness notification that said "completed (exit code 0)" — that was the wrapper's exit, not the suite's. Reporting a killed run as killed is exactly right. (b) output's necessity for the API-mode store: argued from source, not measured, and labelled as such.
  • ⭐ A process finding this seat owes back: vitest --shard=N/4 is not reproducible between CI and this container — the dev's local shard 3 held 776 files / 10360 tests against CI's 777 / 10368, exactly the one file and eight tests of AiChatPage.runtimeMessageSeam.test.tsx, because vitest partitions by position in the discovered file list. ⇒ any future dispatch saying "reproduce the failing shard locally" silently under-covers unless the failing FILE is named and run directly. The dev verified that file directly instead, and unedited. This goes into this seat's dispatch template.

Verdict

PASS.

⚠️ Timing — recorded against this seat, not the dev

This record post-dates the enqueue, which is the wrong order: PR #9450 was enqueued at 01:58:50Z on a Clause-②: no that was true of commit 1 and stale at commit 2, and the flip to yes reached this seat only in the dev's report afterwards. On reading it the seat did both required actions immediately — auto_merge disabled and the PR converted to draft at 02:13:33Z — and then answered the dequeue question with a positive probe rather than the absence of a removal event: refs/heads/gh-readonly-queue/main/pr-9450-… was still present (control: the same endpoint matched heads/main), so the entry had not been evicted.

⇒ The rule this breached is the enqueue gate, ⛔ not the review: the substance was reviewed and passes. The lesson is that a Clause-② declaration is only as current as the last commit it was made against — a patch round can flip it, and this seat did not re-read it before re-landing.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Clause-② carriers stripped — PASS. Review record: comment 5658088896, judged at head 95f2ff0 (unchanged since). Both carriers removed in one stroke — card objectui#9232 and this PR — per 清标即落地.

⚠️ Recorded because the order was wrong and the record should say so: this PR was enqueued at 01:58:50Z on a Clause-②: no that was true of commit 1 and stale at commit 2. The dev's own re-declaration to yes (correct — hydratedMessagesToChatMessages is re-exported at packages/app-shell/src/index.ts:284) reached this seat only afterwards. The gate breached is the enqueue gate, ⛔ not the review: the substance is reviewed and passes.

⭐ Platform fact, new and worth carrying: converting the PR to draft and disabling auto-merge did NOT evict its merge-queue entry. refs/heads/gh-readonly-queue/main/pr-9450-… was still present afterwards (lit control: the same endpoint matched heads/main, and a second entry for another PR was listed alongside). ⇒ the emergency dequeue documented as "转 draft 与 disable 都做" is not sufficient on its own in this repo, and the rule's insistence on answering the dequeue question with a positive probe — rather than with the absence of a removed_from_merge_queue event — is exactly what surfaced it. The timeline never showed a removal event at all.

⇒ With the PASS on record and both carriers clear, the entry is now legitimately landable; the PR is returned to ready.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 14, 2026 02:16
Merged via the queue into main with commit 5c75dd6 Sep 14, 2026
37 checks passed
@os-tesla
os-tesla deleted the claude/issue-9232-cache-approval-envelope branch September 14, 2026 02:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants