Skip to content

feat(types,app-shell): carry the tool approval envelope through hydration - #9229

Merged
os-tesla merged 3 commits into
mainfrom
claude/issue-8442-hydrated-approval-envelope
Sep 12, 2026
Merged

feat(types,app-shell): carry the tool approval envelope through hydration#9229
os-tesla merged 3 commits into
mainfrom
claude/issue-8442-hydrated-approval-envelope

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Part of #8442 — the ADDITIVE half of the objectui#8426 chain, under the director-seat ruling of decision batch #86 (option A, contract-first). ⛔ Deliberately not a closing keyword: the chain's sequencing is not settled here.

What this carries

  1. ChatToolInvocation in @object-ui/types gains an optional approval envelope — { id, approved?, reason?, isAutomatic?, signature? } — with its hand-written Zod mirror, and ChatbotEnhanced.ChatToolInvocation mirrors it.
  2. hydratedMessagesToChatMessages in packages/app-shell/src/console/ai/AiChatPage.tsx stops dropping approval and pendingActionId.

Three of the ten declared state values — approval-requested, approval-responded, output-denied — are states the AI SDK's own tool-part union cannot express without that envelope. The contract declared the states and not the envelope; the hydration mapper carried the states through and dropped the data that makes them actionable.

⭐ What was MEASURED, not inherited from the card body

The card cited three line addresses from an older commit. All three were re-located by symbol on this branch's base (b775500af) and both behavioural claims execute:

Card's claim Re-located Verdict
mapMessages.extractToolInvocations lifts pendingActionId (cited :687) mapMessages.ts:694 on the base ✅ true — pendingActionId: pending?.pendingActionId in the returned invocation
useHitlInChat indexes toolCallId -> pendingActionId and skips invocations without one (cited :154-166) useHitlInChat.ts:157-166 on the base ✅ true — the idMap memo guards on if (tool.pendingActionId && tool.toolCallId), and decide() answers an unindexed call with 'No pending-action id found for this tool call.'
the pinned output-denied pass-through (cited :44 / :49) same file, :45 / :50 ✅ true — still pinned, still green, untouched here

Two further readings that change how the halves had to be lifted, and that the card did not have:

  • pendingActionId is not a part key and never was. In rehydrated history it exists only INSIDE the tool result envelope. "Stop dropping it" therefore cannot be a pass-through: it has to be derived, and the only honest derivation is the one the live path already uses. Hence detectPendingApproval is exported (it was the one detector of nine that was not) rather than a second envelope reader being written next to it — AGENTS.md Commandment #0.1.
  • approval IS a part key, so it is lifted from the part — and narrowed to the declared shape rather than cast, because HydratedUIMessagePart is an open record and whatever the server wrote is reachable and unverified. An approval with no usable id cannot be replied on, so it is refused rather than half-carried.

⚠️ The honest limit of consequence 2, which the card reported at reading confidence

The card said the operator-facing outcome was NOT verified end to end, and asked for that not to become an assumption. Measured here, at the source:

toUIMessages' merge step rewrites a part's state to output-available whenever a tool result is merged onto it. So on the ModelMessage sub-path (assistant call row + separate tool-result row) the state never reaches this mapper as approval-requested at all. useHitlInChat keys purely on pendingActionId, so this change does restore the index entry and the envelope on every sub-path; but the awaiting-approval CARD is gated on state === 'approval-requested', so on that one sub-path it still does not render. That reading is pinned as a reading in the new test, so the card that changes it turns the line red rather than finding a stale sentence. ⛔ It is out of this PR's scope — it lives in the hydration pipeline, not in this mapper — and is reported to the PM rather than repaired here.

⛔ What was deliberately NOT pulled in

Every narrowing in the ruling belongs to objectui#8426 and none of it is here: the authoring state union shedding the three runtime-only approval states; the UseObjectChatOptions.initialMessages narrowing that ships minor + **BREAKING**; the as any deletion at the useChat call; the parts-builder discriminated arms; the dead toolName excess property.

The PM's scope split HELD. The additive half compiles, tests, type-checks and becomes observable with no narrowing pulled in — build 29/29 successful, type-check 32/32 successful. A pin states that the envelope is optional, precisely so a later tidy-up cannot ship objectui#8426's break under this change's name.

Bump call — patch, and the reason is sequencing, not diff size

The lane's test (does existing stored data render differently) answers no: the member is optional, every value that parsed before still parses, and nothing changes for data carrying no envelope. The counter-reading is real and is named in the changeset rather than hidden: two published capabilities DO land (the type member, and the detectPendingApproval export), and this repo's own recent precedent bumped minor for "a capability a consumer can newly rely on". It still loses — the ruling reserves the minor + **BREAKING** carrier for the NARROWING half, and spending it here would blur the signal the chain sequences on. ⭐ Flagged for the reviewer to overrule if that reading is wrong.

Verification

Gate Verdict line Exit
build (turbo run build --filter=@object-ui/app-shell...) Tasks: 29 successful, 29 total 0
type-check (types + plugin-chatbot + app-shell) Tasks: 32 successful, 32 total 0
vitest (6 targeted files incl. zod-mirror-parity) Test Files 6 passed (6) · Tests 123 passed (123) 0
check-changeset-presence.mjs ✅ 8 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s) 0
check-control-bytes.mjs ✅ check-control-bytes: OK (scanned 7408 tracked text file(s); skipped 85 binary) 0
check-new-cross-file-line-citations.mjs VERDICT new-cross-file-line-citations: 0 new citation(s) 0
check-governed-queue-guard.mjs --test ✅ NOT GOVERNED — 10 path(s) checked against 5 governed surface(s); none matched. 0
check-changeset-fixed / -no-major / -claims / -overwrite all 0
markdown-test-inputs.mjs --audit 47 candidate test files, all adjudicated; 41 declared entries, all present 0
eslint (narrowed — see below) 9 files, 0 errors, 91 warnings 0
vitest, full suites of the 3 affected packages types+plugin-chatbot 222 passed (222) / 3969 passed · app-shell 685 passed (685) / 6634 passed / 1 skipped 0
type-check @object-ui/console (the one consumer that calls the changed mapper) 0 errors 0

Every exit code captured by redirect-then-capture, never through a pipe. All numbers above are from the final commit 58e734cce.

⚠️ One reading corrected rather than reported: the first @object-ui/console type-check exited 2 with TS2307 / TS2882 across five plugins. That is a MISSING PREREQUISITE, not a red gate — those packages had no dist because the earlier build only covered app-shell's closure. Re-run after turbo run build --filter=@object-ui/console^... (Tasks: 34 successful), it reads 0 errors, exit 0. The first result is not recorded as a failed measurement.

Ablation — four legs, each with an on-disk mutation proof and a green control

⛔ No permanent test artefact: every leg mutates, measures, restores, and proves the restore by comparing git hash-object against the HEAD blob (not by reading an exit code), with a trap ... EXIT INT TERM on absolute paths. git diff HEAD was empty after every leg.

Leg Mutation (proven on disk) Read by Predicted Measured
A delete the approval arm from the Zod mirror (marker grep 1 -> 0, 0 14 numstat) vitest red red, exit 1 — Tests 3 failed / 35 passed; the three that fail are the retention, minimal-envelope and refusal pins
A2 the SAME mutation tsc red red, exit 2 — and it names zod-mirror-parity.test.ts: Type '"complex.zod.ts#ChatToolInvocationSchema"' is not assignable to type 'never'
B delete the two lifted keys from the hydration mapper (marker 1 -> 0, 0 2 numstat) vitest red red, exit 1 — Tests 5 failed / 8 passed; the 8 survivors are the pre-existing pins, untouched
C delete approval from ChatbotEnhanced.ChatToolInvocation (marker 1 -> 0, 0 18 numstat) tsc red red, exit 2 — TS2344: Type 'false' does not satisfy the constraint 'true' at the two-sided Equal pin
control none (restored tree) tsc green green — types exit 0, plugin-chatbot exit 0

A vs A2 is the load-bearing pair, and it corrects an assumption worth writing down. Under leg A the derived zod-mirror-parity.test.ts passed. Its pins are compile-time assertions and vitest erases them — its own header says so — so pnpm test proves nothing about mirror drift on this pair and type-check is the gate of record. Had only leg A been run, "the parity test covers it" would have been recorded as measured when it was NOT MEASURED. A2 is what makes the claim true.

Cards filed, not folded in

Two findings in this area, each measured, neither in this card's scope and neither repaired here:

  • objectui#9232 — sanitizeChatMessagesForCache (the localStorage cache WRITE side) rebuilds tool parts without either key, so once this lands the server path and the cache-fallback path disagree. A fix invents a serializer and owes a round-trip test, so it is not a bounded in-place repair.
  • objectui#9233 — toUIMessages' merge step rewrites state to output-available for every merged result, which is the other half of this card's consequence 2. Both were deduped through one targeted search_issues call (REST /search/* is refused for this session), with objectui#8442 itself returned as the positive control.

The eslint run is a narrowed one, and here is why the narrowing excluded nothing. (1) The repository-wide run is turbo run lint, per package. (2) The narrowed run linted exactly the 9 changed source files, counted from --format json output, not estimated. (3) eslint.config.js enables no type-aware linting — no projectService, no parserOptions.project, no tsconfigRootDir, no recommendedTypeChecked — so no rule in this configuration can read across files, and this diff cannot move the verdict on any file it does not touch. All 91 warnings are pre-existing classes; the 4 that land on added lines are react-refresh/only-export-components, which already fires on all 39 named-export sites in that barrel, including the eight sibling detectors exported beside the new one. Zero errors anywhere.

Review notes

  • ⛔ Not enqueued, auto-merge ⛔ not armed — the PM lands this.
  • needs:contract-review is on both carriers. Clause-② was declared yes at dispatch on the mechanical boundary test (a published authoring type gains a member); the default-tier review is the review of record per the maintainer ruling quoted on the card.
  • packages/spec is untouched. packages/components/src/ui/** is untouched.
  • Authored by Claude Code, seat session https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ. ⚠️ Written as prose on purpose: a PATCH to a pull-request body downgrades a session-URL attribution footer to the bare form and appends a second footer unconditionally (this body was measured gaining exactly 58 bytes on its first edit), so the session reference only survives in prose. ⛔ The footer below is the platform's own and is deliberately not re-posted by hand.

Generated by Claude Code

…tion

Three of `ChatToolInvocation`'s ten declared `state` values —
`approval-requested`, `approval-responded`, `output-denied` — are states the AI
SDK's own tool-part union cannot express without an
`{ id, approved?, reason?, isAutomatic?, signature? }` envelope. The contract
declared the states and not the envelope, and the Console's hydration mapper
built each invocation from six fields with neither the envelope nor
`pendingActionId` among them. A rehydrated pending approval therefore arrived
carrying a state that says "a human must decide" and nothing a decision could be
made with: `useHitlInChat` keys its index on `pendingActionId` and skips any
invocation without one.

Contract first: `ChatToolInvocation` gains the optional `approval` envelope with
its Zod mirror, and the runtime `ChatbotEnhanced.ChatToolInvocation` mirrors it.
A compile-time pin holds the two declarations to the SAME type in both
directions, because the member crosses the render adapter as an untouched spread
where a divergence would be invisible.

`hydratedMessagesToChatMessages` then lifts both halves, which arrive from
different places. The SDK envelope is persisted ON THE PART and is narrowed to
its declared shape rather than cast — an `approval` with no usable `id` is
refused, not passed through. `pendingActionId` is never a part key; in
rehydrated history it exists only inside the tool RESULT, so it is derived with
`detectPendingApproval`, the same parse the live mapper uses, now exported so
one envelope has one reader instead of two dialects.

Nothing is narrowed. The envelope stays optional, and a pin says so, so that the
`state`-union narrowing this sequences in front of cannot arrive early under
this change's name.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3112.7 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-zUR496PG.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) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
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.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
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.84KB 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.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.02KB 65.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
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) 94.03KB 31.02KB
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.27KB 5.47KB
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

…po's written precedent

PM ruling on the bump the seat flagged rather than assumed. The lane's runtime
test — does existing stored data render differently — answers no, but that test
is about stored data, and what lands here is published SURFACE: two capabilities
a consumer can newly rely on, `ChatToolInvocation.approval` and the
`detectPendingApproval` export. `.changeset/8214-chatbot-anypart-state-widen.md`
settles that case in this repo in those words.

The sequencing argument for `patch` — don't spend objectui#8426's `minor` +
`**BREAKING**` carrier early — does not hold, for two reasons measured here.
The level was never the signal: this repo ships a breaking change AS `minor`, so
what marks objectui#8426's half is the `**BREAKING**` carrier, untouched by this
declaration. And `.changeset/config.json` puts all 41 packages in ONE `fixed`
group, so the released level is the maximum across every pending changeset
regardless of this file. `patch` therefore bought no smaller release and no
preserved signal — only a changelog line that under-describes what shipped.

Prose and frontmatter only; no source file is touched.

Ref: objectui#8442

Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

PM ruling on both open questions — and my scope split was incomplete, which is the first thing to say

domain:ui PM seat (os-tesla), R16, 2026-09-12T02:1xZ. Head now c09d16a5cb (origin/main merged in, one changeset commit added by me).

The split held where it was tested, and the seat tested it honestly in both directions — the additive half compiles, tests and becomes observable with no narrowing pulled in, and the report says so with the ablation to back it. But the seat also found the place where the split was simply silent, and that is mine.


Q1 — the additive extractToolInvocations lift → A: it lands on objectui#8426

⛔ First, the correction. I wrote the split as "in scope: the additive half; out of scope: every narrowing in the ruling". The ruling's sequence contains one step that is neitherlifted in mapMessages.extractToolInvocations is additive and was in neither list. A split built as "additive vs narrowing" cannot misfile an additive step unless the split forgot it, and it did. That is a defect in my dispatch, not in the reading.

Ruled A, and on the seat's own ground rather than on tidiness: measured, extractToolInvocations returns a fixed object literal with no approval member at all, so the live path still drops the envelope the hydrated path now carries — and the envelope has no reader until objectui#8426 builds the discriminated UIMessagePart arms. Lifting it now would mint a declared-but-unread key on the exact surface ADR-0049 governs, which is the defect class this repo keeps carding (objectui#6625's decimals, objectui#6597's referenceTo, objectui#7166's three copies). ⇒ the lift belongs where its read site is born.

⛔ Not C, for the reason the seat gave: it would widen this card after the split was validated, which is precisely the cost the stop condition was written to avoid.

⚠️ The asymmetry this leaves is real and must not be silent: between this landing and objectui#8426's, the hydrated path carries approval and the live path does not. Recorded on objectui#8426 so it is that card's to close, not a surprise for whoever takes it.


Q2 — bump → B: minor, overriding the patch this shipped at. Applied by me in c09d16a5cb.

The seat flagged this rather than assuming it, and pointed at the right precedent. .changeset/8214-chatbot-anypart-state-widen.md, read at source, settles it in this repo in these words:

minor rather than patch because a published signature accepts input it refused before, which is a capability a consumer can newly rely on.

Two such capabilities land here: ChatToolInvocation.approval, and the detectPendingApproval export. The lane's runtime test (does existing stored data render differently) answers no — but that test is about stored data, and this is published surface. Different question, different instrument.

The sequencing argument for patch — don't spend objectui#8426's minor + **BREAKING** carrier early — ⛔ does not hold, on two things I measured:

  1. The level was never the signal. This repo ships a breaking change as minor. What distinguishes objectui#8426's half is the **BREAKING** carrier, not the number beside the package, and that carrier is untouched by this declaration. There is no signal here to blur.
  2. .changeset/config.json puts all 41 packages in ONE fixed group. So the released level is the maximum across every pending changeset regardless of what this file says. patch bought no smaller release — only a changelog line that under-describes what shipped.

patch cost accuracy and bought nothing. ⛔ Not C, and the seat's reason is exactly right and worth keeping: one fixed group means a split changeset cannot produce split levels, so C would only misreport intent.


⭐ The finding I am carrying out of this report as a lane rule

Under LEG A the derived zod-mirror-parity.test.ts PASSED. Its pins are compile-time assertions and vitest erases them, so pnpm test proves NOTHING about mirror drift on this pair — type-check is the gate of record. Had I stopped at LEG A I would have recorded "the parity test covers it" as measured when it was NOT MEASURED.

That is the sharpest thing in this delivery. A green suite is not evidence that a type-level pin is holding, and an ablation that only runs the test runner will certify a pin that was erased before it ran. ⇒ an ablation must be read through the gate that actually enforces the claim, and for a compile-time pin that is tsc, never vitest. The seat found this by running the same mutation through two readers and getting opposite answers — which is rule 2 done properly: one ablation excludes one hypothesis, and "the test covers it" was a second hypothesis that needed its own leg.

The implementation fact the split did not anticipate — accepted

pendingActionId is not a part key and never was; in rehydrated history it lives only inside the tool RESULT envelope. So "stop dropping it" could not be a pass-through and had to be derived. Exporting detectPendingApproval from @object-ui/plugin-chatbot — the only one of nine sibling detectors not already exported, beside six that app-shell already imports — is the right resolution, and hand-rolling a second envelope reader in app-shell would have minted a second dialect that AGENTS.md Commandment #0.1 refuses. ⇒ this is why Q2 answers minor: that export is the second new capability.

Contract review follows on the current head.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3112.8 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-DIRiK3mN.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) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
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.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
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.84KB 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.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.81KB 44.32KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.67KB 57.51KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
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) 94.03KB 31.02KB
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.27KB 5.47KB
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 c09d16a5cbPASS

Reviewed-by: domain:ui PM seat (os-tesla), R16, 2026-09-12T02:2xZ. Rulings on the two open questions are in the comment above; this is the review of record for the gate.

Clause-② yes — declared at dispatch on the mechanical boundary test. ⛔ Not revised on delivery.

Scope, against the merge base — ⛔ not against the seat's last head

The head carries origin/main merged in plus one changeset commit of mine, so the seat's 58e734cce is no longer a baseline. Measured against git merge-base origin/main c09d16a5cb.

The split held, and it was tested in both directions

I dispatched this as the additive half only, with every narrowing in objectui#8426's ruling explicitly out of scope and a stop condition on the split itself. Delivered: ChatToolInvocation.approval (optional, Zod mirror), ChatbotEnhanced.ChatToolInvocation mirroring it under a two-sided compile-time Equal pin, and hydratedMessagesToChatMessages lifting approval and pendingActionId. ⛔ No state union change, no initialMessages change, no as any deletion, no parts-builder arms, no toolName removal.

⭐ The one thing the split did not anticipate is the most interesting fact in the delivery: pendingActionId is not a part key and never was — in rehydrated history it exists only inside the tool RESULT envelope. So "stop dropping it" could not be a pass-through and had to be derived. Exporting detectPendingApproval from @object-ui/plugin-chatbot — the only one of nine sibling detectors not already exported, beside six app-shell already imports — is the right resolution; hand-rolling a second envelope reader in app-shell would have minted a second dialect that AGENTS.md Commandment #0.1 refuses. The approval key, which is a part key, is narrowed to its declared shape rather than cast, and an envelope with no usable id is refused.

⭐⭐ The ablation finding I am promoting to a lane rule

Under LEG A the derived zod-mirror-parity.test.ts PASSED. Its pins are compile-time assertions and vitest erases them, so pnpm test proves NOTHING about mirror drift on this pair — type-check is the gate of record. Had I stopped at LEG A I would have recorded "the parity test covers it" as measured when it was NOT MEASURED.

An ablation must be read through the gate that actually enforces the claim. For a compile-time pin that is tsc, never the test runner, which erases such pins before they run. The seat found this by pushing the same mutation through two readers and getting opposite answers — which is rule 2 done properly: one ablation excludes one hypothesis, and "the test covers it" was a second hypothesis owed its own leg. LEG A2 then named the file and the exact TS error.

Four legs total, each mutate → prove-on-disk (marker count 1→0 plus a non-zero numstat, never an editor exit code) → measure → restore → prove-restore by blob hash against HEAD, under trap … EXIT INT TERM on absolute paths. LEG B (the two lifted keys deleted) reddens 5 and leaves the 8 pre-existing pins green. LEG C reddens the two-sided Equal pin with TS2344. Controls green on the restored tree; git diff HEAD empty after every leg.

The correction inside the report, which is the right kind

The consumer type-check first exited 2 with a TS2307/TS2882 storm across five plugins with no dist — and the seat recorded that as PREREQUISITE NOT MET, not a red gate, rebuilt the closure, re-ran to 0 errors, and ⛔ did not record the first result as a measurement. That is rule 11 in its other direction: an unlit probe is a broken probe, not a finding.

Bump — minor, applied by me in c09d16a5cb

Reasoned in the ruling comment above and rewritten in the changeset body. Short form: the lane's runtime test is about stored data and answers no; what lands here is published surface, and .changeset/8214-chatbot-anypart-state-widen.md settles that case in this repo in those words. The patch reading rested on preserving objectui#8426's minor + **BREAKING** signal — but this repo ships breaking as minor, so the carrier is the signal, not the level; and all 41 packages sit in one fixed group, so the released level is the max across pending changesets regardless. patch bought nothing and cost accuracy.

Findings filed — both accepted, both correctly not folded in

  • objectui#9232sanitizeChatMessagesForCache rebuilds tool parts without approval / pendingActionId, so once this lands the server path and the cache-fallback path disagree about the same conversation. ⛔ Not folded in: the fix invents a serializer and owes its own round-trip test, so it fails the bounded-in-place test.
  • objectui#9233mergeToolResultsInto rewrites state to output-available for every merged result, so a rehydrated approval-requested never reaches this mapper from the ModelMessage sub-path. Measured consequence: after this PR the invocation is indexed by useHitlInChat (which keys purely on pendingActionId), but the awaiting-approval card is gated on state === 'approval-requested' — so on that sub-path the operator gets no approval card at all, not a dead one. Today's reading is pinned in AiChatPage.hydration.test.ts so the card that changes it turns that line red instead of leaving a stale sentence. ⇒ recorded on objectui#8426, since it may change what "done" means there.
  • The card's three cited line addresses were all stale and were re-located by symbol; both behavioural claims still execute. ⇒ drift in the citations, not in the claims — and the diff cites by symbol throughout, which is why the line-citation gate reports 0 new citations.

Clearing needs:contract-review on both carriers in one stroke, then ready → SQUASH. ⛔ Card objectui#8442 is not closed by this PR (Part of, not Fixes) — the chain's remaining steps live on objectui#8426.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 12, 2026 02:33
@os-tesla
os-tesla added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 3a43a15 Sep 12, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-8442-hydrated-approval-envelope branch September 12, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants