Skip to content

perf(plugin-chatbot): stop rebuilding the chat transport on every render (#4187) - #5603

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-4187-chat-transport-memo
Aug 21, 2026
Merged

perf(plugin-chatbot): stop rebuilding the chat transport on every render (#4187)#5603
os-sales merged 2 commits into
mainfrom
claude/issue-4187-chat-transport-memo

Conversation

@os-sales

@os-salesos-sales commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes#4187

1. Premise re-measured against the installed 4.0.68 — still dormant, severity unchanged

The card's whole "waste-only" framing was written against @ai-sdk/react@4.0.59; the package now carries ^4.0.68, which is what fired the hold's restart condition. I read the installed build rather than inheriting the analysis: node_modules/.pnpm/@ai-sdk+react@4.0.68_react@19.2.8_zod@4.4.3/node_modules/@ai-sdk/react/dist/index.js, version field 4.0.68.

Q: does useChat still store the transport in a ref and delegate through a getTransport() indirection? Yes, verbatim (dist/index.js:301-322):

constlatestRef=useRef2({});if(!("chat"inoptions)){latestRef.current={onToolCall: ...,onData: ...,onFinish: ...,onError: ...,sendAutomaticallyWhen: ...,transport: options.transport};}letdefaultTransport;constgetTransport=()=>{ ... returnlatestRef.current.transport!=null ? latestRef.current.transport : ... };constchatOptions={ ...options,transport: {sendMessages: (sendOptions)=>getTransport().sendMessages(sendOptions),reconnectToStream: (reconnectOptions)=>getTransport().reconnectToStream(reconnectOptions)}, ... };

latestRef.current is reassigned during every render and getTransport() is called at send time, so the transport is late-bound.

Q: is Chat recreation still keyed only on chat / id, or does transport identity now participate? Still only chat / id — transport identity does not participate (dist/index.js:347):

constshouldRecreateChat="chat"inoptions&&options.chat!==chatRef.current||"id"inoptions&&options.id!=null&&chatRef.current.id!==options.id;

useObjectChat passes transport, messages and onError and neither chat nor id, so the Chat object — and the message list with it — survives every transport rebuild.

Conclusion: this is NOT a live per-render thread reset. 4.0.68 behaves exactly as the card described 4.0.59, so the severity is unchanged: construction waste only, nothing a user hits. The restart condition fired on the version string, not on a behaviour change. What the bump does confirm is the card's own argument for fixing it anyway — the safety is a property of the SDK's internals that a future release can withdraw silently, and the memo gives a reviewer no warning because it is already written as though it works.

2. The fix

packages/plugin-chatbot/src/useObjectChat.ts. The caller's body and headers move out of the transport useMemo dep list and are read through refs at send time, the idiom the hook already uses two dozen lines above for the live model (modelRef) and the handoff conversation id (parentConvRef):

  • bodyRef / headersRef are assigned on every render, beside the existing refs;
  • the transport constructor keeps only the hook-owned keys (conversationId, model, systemPrompt, stream) and no longer takes headers;
  • prepareSendMessagesRequest spreads bodyRef.current under the request body and returns the caller's headers merged into the SDK's base headers;
  • the dep list drops to [isApiMode, api, model, systemPrompt, streamingEnabled, conversationId].

Call-site memoization was considered and rejected upstream of this PR as unenforceable; this is the only one of the two a future caller cannot silently undo by forgetting a useMemo of its own.

Merge order is preserved in both directions, which is the part of the diff most able to regress quietly:

  • body — the SDK hands prepareSendMessagesRequest its own body already merged as constructor-body over per-send body, so spreading the caller's live body underneath it reproduces the old precedence exactly: caller body loses to the hook-owned keys, which lose to a per-send body. Pinned by the "hook-owned keys winning over the caller body" test.
  • headers — returning headers from prepareSendMessagesRequest REPLACES the SDK's base headers rather than merging with them, so the caller's headers are re-merged underneath the base set. A per-send header still wins, as it did when the caller's headers were the transport's own headers option.

One consequence, and it is recorded in the file rather than only here:headers leaving the constructor also takes them off reconnectToStream, which reads the constructor's headers through prepareReconnectToStreamRequest and never runs prepareSendMessagesRequest. That path has no consumer in this repo — grep -rn "resumeStream\|reconnectToStream" over packages/ and apps/ returns nothing outside node_modules — so nothing regresses today. Because a PR body is not what the next person reads, the invariant now sits as a comment on the exact line the headers option used to occupy, naming what a future resumeStream consumer has to merge and where.

3. The sampling-timing answer (the one real behavioural difference)

Which values does a send observe after the change? Those of the most recent completed render, read at send time. bodyRef.current / headersRef.current are assigned during every render; prepareSendMessagesRequest runs synchronously at the top of HttpChatTransport.sendMessages, which is itself reached through getTransport() at that instant. So the pair a send serializes is always the last-rendered pair, spread at send time.

What did it observe before? The transport spread body/headers into itself at CONSTRUCTION time, so a send serialized the values of the last render that rebuilt the transport. With both in the dep list and every caller passing a fresh literal, that was in practice also the last render — which is exactly why nothing changes for any caller that exists today. But the guarantee was incidental: it held only because the memo never hit. A caller with a stable body identity got a value frozen at the first construction.

Net: the new read is never staler than the old one and is now unconditional rather than a side effect of the memo missing. It is pinned by useObjectChat.transportIdentity.test.tsx, whose fourth case holds one object identity across the whole test — with a stable identity the memo never re-ran even before this change, so the value a send observes is decided purely by when it is read. That case fails on main with expected 1 to be 2 and passes here.

4. Tests

New: packages/plugin-chatbot/src/__tests__/useObjectChat.transportIdentity.test.tsx, five cases, counting new DefaultChatTransport(...) with a construct-trap Proxy around the real class so the hook still talks to the genuine transport.

  1. builds ONE transport across renders that pass fresh body/headers literals — the fix itself;
  2. still rebuilds the transport when a memoized dep really changes — the memo is not simply disarmed;
  3. a send carries the body/headers of the MOST RECENT render — contract pin; green before and after, stated as such rather than passed off as a regression guard;
  4. reads body at SEND time, not at transport-construction time — the sampling change;
  5. keeps the hook-owned keys winning over the caller body — the merge order above.

Reverse-verification

Both ablations restored through trap ... EXIT INT TERM, each mutation proved on disk with an anchored grep -c in both directions before reading any result.

A — the deps put back on the memo list (}, [isApiMode, api, headers, body, model, ...]);): fixed-form count 1 then 0, ablated-form count 0 then 1, git diff --stat1 file changed, 1 insertion(+), 1 deletion(-). Case 1 fails with AssertionError: expected 4 to be 1 — four renders, four constructions — and 4 of 5 still pass. Restored, git status --porcelain empty.

B — the whole hook reverted to origin/main (git checkout origin/main -- packages/plugin-chatbot/src/useObjectChat.ts): fix marker bodyRef.current = body; count 1 then 0, pre-fix marker headers: { ...headers }, count 0 then 1. Cases 1 and 4 fail (expected 4 to be 1, expected 1 to be 2 — the frozen construction-time body), cases 2, 3 and 5 pass, which is the predicted split. git diff --stat printed nothing for this leg because git checkout ref -- path writes the index too, so that diff was staged; the two-directional grep is the load-bearing proof. Restored, and the tree verified byte-identical to HEAD afterwards: git status --porcelain and git diff HEAD --name-only both empty.

No ablation for the invariant comment in section 2 — a comment has nothing executable to pin.

Gates, all at 72bd9df02 with a clean tree

Exit codes captured before any pipe; each line is the gate's own verdict. The three gates that can see a comment change were re-run on the new head after the review addition: type-check, lint and the package's vitest suite, all reported below at 72bd9df02. The remaining rows were measured at 0cd066a7c, whose only difference from this head is six lines of comment in one file.

GateExitVerdict
pnpm exec vitest run packages/plugin-chatbot/0Test Files 23 passed (23) / Tests 326 passed (326)
the new file alone, --reporter=verbose0packages/plugin-chatbot/src/__tests__/useObjectChat.transportIdentity.test.tsx named on all 5 lines, Tests 5 passed (5)
consumer tests (app-shell chat/AI hooks, ChatDock, studio dock, components palette)0Test Files 7 passed (7) / Tests 75 passed (75)
pnpm --filter @object-ui/plugin-chatbot type-check0tsc --noEmit && tsc -p tsconfig.test.json clean
pnpm --filter @object-ui/plugin-chatbot lint086 problems (0 errors, 86 warnings)
node scripts/check-control-bytes.mjs0check-control-bytes: OK (scanned 4655 tracked text file(s); skipped 85 binary)
node scripts/check-changeset-presence.mjs02 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-no-major.mjs0No changeset declares a major bump.
node scripts/check-changeset-fixed.mjs0All workspace packages are in the changeset fixed group.
node scripts/check-type-check-coverage.mjs0type-check coverage: 45/46 ... 41/41 packages compile their tests
node scripts/check-lint-coverage.mjs0lint coverage: 46/46 packages linted, 0 with outstanding errors
node scripts/check-phantom-dependencies.mjs0Every in-scope import is declared by the package that publishes it.
node scripts/check-package-self-import.mjs0No package names itself inside its own src/.
node scripts/check-spec-symbol-derivation.mjs01289 files scanned against 4912 spec export names
node scripts/check-i18n-call-site-keys.mjs0Every in-scope call-site key resolves against the en pack (2918 keys)
node scripts/check-i18n-en-drift.mjs0No en value changed in this range.
node scripts/check-skills-paths.mjs095/96 stated path(s) resolve across 18 guide file(s); 1 baselined
node scripts/check-node-esm-load.mjs --specifiers-only0no un-ledgered package emits an extensionless relative specifier

Derived from the workflows against this diff rather than from a handed-down list; check-changeset-fixed, check-phantom-dependencies, check-package-self-import, check-node-esm-load --specifiers-only, check-spec-symbol-derivation, both i18n gates and check-skills-paths are additions the dispatch list did not name.

The 86 lint warnings are the package's pre-existing count plus two of a class this very file already emits four times: react-hooks/refs "Cannot access refs during render" at useObjectChat.ts:460 and :462, the same warning the sibling idiom modelRef.current = model draws at :423. Left unsuppressed for consistency with the refs beside them, and lint still exits 0. eslint . --format json inside the package: 55 files, 0 errors, 86 warnings; the new test file contributes 0 of each.

Narrowing declared. Two repo-wide runs were left to CI. pnpm lint is turbo run lint, which fans the same eslint . out to each of 46 packages; I ran the one package whose files changed, and the flat config extends tseslint.configs.recommended with no project or projectService, so type-aware linting is off and this diff cannot move the verdict on a file it does not contain. pnpm test is sharded repo-wide in CI; I ran the changed package in full plus every test file outside it that names plugin-chatbot or useObjectChat. check-eager-closure-budget exits 2 here for a reason unrelated to this change — it reads apps/console/dist/eager-closure.json, written by a console vite build, and reports itself as a broken gauge, not a passing budget when absent; it belongs to the performance-budget job. The doc-snippet and doc-component gates were not run: no exported signature, option or type changed, so no documented snippet can compile differently.

One changeset, patch, @object-ui/plugin-chatbot. No release notes touched.

Generated by Claude Code

…der (#4187)
`useObjectChat` memoized its `DefaultChatTransport` on a dep list that
included the caller's `body` and `headers`. Both are object props and every
caller passes a fresh literal each render, so the memo never hit: a transport
was constructed on every render of every chat surface, which during a
streaming turn is once per token batch.
Both are now read through refs inside `prepareSendMessagesRequest` and are
gone from the dep list -- the idiom this hook already uses for the live model
(`modelRef`) and the handoff conversation id (`parentConvRef`), and the only
one of the two candidate fixes a future caller cannot silently undo by
forgetting a `useMemo` of its own.
Premise re-measured against the installed `@ai-sdk/react@4.0.68` rather than
inherited from the 4.0.59 the card was written against: `useChat` still keeps
the transport in `latestRef` and delegates through `getTransport()`, and
`shouldRecreateChat` is still keyed only on `chat`/`id`, neither of which this
hook passes. So the thread was never at risk and the rebuild was pure waste,
exactly as filed.
The one behavioural difference is when the two values are sampled: a send
reads them at send time, so it observes the most recent render's values rather
than those of the last render that happened to rebuild the transport -- never
staler than before, and now unconditional. Merge order is preserved in both
directions: the hook-owned body keys still win over the caller's body, and a
per-send header still wins over the caller's headers.
`useObjectChat.transportIdentity.test.tsx` pins one construction across
renders, the rebuild that a real dep change must still cause, the sampling
timing, and the key precedence.
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.7 KB3867.2 KB
Main entry chunk (gzip)151.5 KB350 KB
Entry fileindex-rqI5FsYZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.99KB113.73KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)238.85KB60.13KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.53KB32.97KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.50KB20.68KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

…ransport site (#4187)
The PR body is not what the next person reads. Moving `headers` out of the
`DefaultChatTransport` constructor also took them off `reconnectToStream`,
which reads the constructor's `headers` through
`prepareReconnectToStreamRequest` and never runs
`prepareSendMessagesRequest`. No consumer in this repo resumes a stream
today, so nothing is missing them -- but whoever wires up `resumeStream`
later would get an unauthenticated resume with no reason to suspect this
hook.
Comment only, at the exact line the `headers` option used to occupy. No
behaviour, no test, nothing executable to pin.
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.7 KB3867.2 KB
Main entry chunk (gzip)151.5 KB350 KB
Entry fileindex-rqI5FsYZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.99KB113.73KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)238.85KB60.13KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.53KB32.97KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.50KB20.68KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT (card #4187)

Gates. 22 named check runs read individually for completed + success at head 72bd9df0: 19 success, 3 skippedTest (coverage), the unexpanded Test (coverage shard …/4) matrix placeholder, and dependabot. All four real shards, Type Check, Lint, Build & E2E, Build Docs, Bundle Analysis, Live E2E, both doc type checks, Control Byte Scan, Skill Guide Path Check, Internal Docs Link Check and all three changeset checks green.

The measurement this card actually turned on

The card's entire "dormant, waste-only" framing was written against @ai-sdk/react@^4.0.59, and the hold's restart condition had fired because the package now carries ^4.0.68. If 4.0.68 had started keying Chat on transport identity, this stops being a perf nit and becomes a per-render thread reset — a severity change, not a detail.

Measured against the installed build rather than assumed:

  • transport still held in latestRef and reached through a getTransport() indirection (dist/index.js:301-322)
  • shouldRecreateChat still keyed only on chat / id (dist/index.js:347), and useObjectChat passes neither

So the restart condition fired on the version string, not on a behaviour change, and the severity is unchanged. That is the honest answer rather than the dramatic one, and it is what lets this land as a hygiene fix instead of a hotfix.

Three things the implementer got right that I did not ask for

  1. The header-merge trap. Returning headers from prepareSendMessagesRequestreplaces the SDK's base headers rather than merging them. Spotting that is the difference between this change being invisible and it silently dropping every base header on each send. They re-merged, and preserved merge order in both directions — caller body still loses to the hook-owned keys, a per-send header still beats the caller's.

  2. Refusing to count a non-guard as evidence. Case 3 is green both before and after the fix. They flagged it as a contract pin rather than folding it into the ablation tally. A test that cannot go red is not evidence, and saying so unprompted is worth more than the test.

  3. Naming why an expected proof came back empty. Leg B's git diff --stat was empty because git checkout ref -- path writes the index too. The easy move is to quietly lean on the other evidence; instead they said the diff was staged and pointed at the two-directional grep as the load-bearing proof for that leg.

The one thing I asked for

The reconnectToStream consequence — headers leaving the constructor also takes them off the reconnect path — was documented only in the PR body. A PR body is not what someone wiring up stream resumption in six months will read. It now sits at the code site, and better than I specified: it names prepareReconnectToStreamRequest as the hook that reads the constructor's headers, so a future consumer is told where to merge rather than merely warned that something is missing. Six lines, no behaviour, inside the existing fence.

Notes

Bundle.plugin-chatbot moves 181.21 → 181.41 KB raw (43.14 → 43.22 KB gz) against the baseline two concurrent PRs report. Part is the two refs and the re-merge; part may be the six comment lines, since tsconfig.base.json sets removeComments: false deliberately. I am not going to pretend to separate those without a real-build measurement, and it does not matter here — the eager-closure budget passes with ~81 KB of headroom.

Lint. Two new react-hooks warnings at :460/:462, identical in kind to four the same file already draws at :423. Left unsuppressed for consistency, which is the right call — a suppression here would be noise that hides the next real one.

#5605 filed for the maxToolRoundtrips enforce-or-remove shape, correctly labelled at filing.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 17:32
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit a31adc6Aug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-4187-chat-transport-memo branch August 21, 2026 17:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useObjectChat rebuilds its DefaultChatTransport on every render — the memo's body dep is an inline literal every caller recreates (finding, dormant)

1 participant

@os-sales