Skip to content

fix(plugin-chatbot): the AI Approvals inbox shows the error, not an empty queue, and a refused poll stops (objectui#11736) - #11741

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11736-inbox-error-stops-poll
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11736-inbox-error-stops-poll

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11736

Clause-②: no. The exported usePendingActions and AiPendingActionsInbox option, return and props types gain or change no member, and no i18n key is added. A byte comparison against the base confirms this (see "Fence" below).

Implemented by the domain:ui#1 dispatch, session https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z.

What changes

On a deployment with no AI service, the open edition answers 501 on /api/v1/ai/pending-actions. The inbox showed the error alert and, under it, "No actions waiting", which reads as a live queue that happens to be empty. The hook also re-armed a fixed 5 s setInterval whatever the read answered.

  • AiPendingActionsInbox.tsx: the empty state is an answer, so it now renders only when the read answered. The branch reads rows.length === 0 && error ? null : .... A failed read leaves the alert alone. Rows, the skeleton and the alert are otherwise unchanged.
  • usePendingActions.ts: the fixed interval is replaced by a setTimeout chain. settlePoll re-arms it after each read settles, according to one fault policy, ListReadOutcome:
answer outcome next read
2xx ok after pollInterval; backoff resets
no HTTP status (network error, unreadable body), 408, 429, 5xx except 501 transient pollInterval doubled per consecutive failure, up to max(pollInterval, 120 s)
501, any other 4xx (401, 403, 404, ...) refused none: the poll stops

Every list read goes through settlePoll: the initial read, the timer tick, a manual refresh(), and the re-fetch after approve / reject. So there is one timer at a time, and a stopped poll restarts only when a read succeeds (for example the inbox's Refresh button) or when the hook's inputs change, which starts the poll over. Each read carries a generation number, so a read that outlives an unmount or an input change re-arms nothing. The 120 s ceiling was copied from MAX_BACKOFF_MS in app-shell's sharedUserFeeds.ts. The code comment says that nothing keeps the two equal.

  • The poll effect now keys only on primitives (enabled, pollInterval, apiBase, status, conversationId, limit) and reaches refresh through a ref. Before, it keyed on the useCallback identity, which AGENTS.md commandment 10 bans.

The PM's hypotheses, measured on main 5a2a6ab

  • H1 confirmed. The rows.length === 0 branch never read error.
  • H2 confirmed, plus one answer. The interval re-armed unconditionally. The status is not dropped: call() already sets err.status = res.status on the thrown error, so the classifier reads it there. No second parser was added.
  • H3: one in-repo caller. git grep -n "usePendingActions(" hits the inbox's call and the hook's own declaration and doc example; the positive control is that the inbox call shows up. useHitlInChat.ts imports only the ApproveOutcome / RejectOutcome types and does not poll. The hook is exported from the package entry, so an outside caller (the module header names a Studio traces panel) gets the same policy: it stops on a refused read and backs off on a transient one.
  • H4. The fake-timer pins below cover the stop, the backoff, the reset and the live control.

Tests

New pins. Reads are counted as fetch calls under vi.useFakeTimers():

  • usePendingActions.pollFaultPolicy-11736.test.tsx (16 cases):
    • 501, 404, 403 and 401 are each read once, and not again across ten intervals;
    • a manual refresh that succeeds restarts the poll, and one that is refused again does not;
    • an input change starts the poll over;
    • consecutive 503s come at exactly 10, 20, 40, 80, 120 and 120 s;
    • 500, 502, 504, 408, 429 and a rejected fetch back off rather than stop;
    • a success after failures resets to 5 s;
    • live control: 200 reads every 5 s;
    • an unmounted hook reads nothing more.
  • AiPendingActionsInbox.errorReplacesEmpty-11736.test.tsx (3 cases):
    • on 501 the server's message shows, "No actions waiting" is absent, and there is one read across ten intervals;
    • on a transient 503 the error shows without the empty state, and the empty state returns once a backed-off read answers 200;
    • live control: a working endpoint with an empty queue shows "No actions waiting", and the poll keeps its 5 s cadence.

Reverse verification. Both legs ran from the committed fix, restored with git checkout HEAD --, and were proven by matching HEAD blob hashes and an empty git diff HEAD. The pins import the sources relatively, so dist/ is not on the resolution path.

  • Hook reverted to the base: landing proof is settlePoll 0 and the old setInterval re-arm line 1. Result: 17 failed / 3 passed. The 3 that passed are the live controls, which must hold on both trees.
  • Inbox reverted to the base: landing proof is the error guard 0 and the bare empty branch 1. Result: 2 failed / 18 passed. Both failures are expected ... data-slot="empty-title" ... to be null with "No actions waiting" received, which is the carded defect.

Gates, run on 74b6272 (the final commit; the tree was clean)

Builds and tests went through os-verify-lock.sh, and each exit code was captured before any pipe.

  • pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-chatbot^...' build → exit 0, Scope: 9 of 47 workspace projects
  • pnpm --filter @object-ui/plugin-chatbot test → exit 0, echoes vitest run --root ../.. packages/plugin-chatbot/, Test Files 57 passed (57), Tests 590 passed (590)
  • pnpm --filter @object-ui/plugin-chatbot type-check → exit 0, echoes tsc --noEmit && tsc -p tsconfig.test.json. tsc -p tsconfig.test.json --listFiles lists both new test files.
  • pnpm check:control-bytes → exit 0, OK
  • pnpm check:new-line-citations → exit 0, VERDICT new-cross-file-line-citations: 0 new citation(s)
  • pnpm check:changeset-claims → exit 0, No pending changeset names a file this change touches.
  • pnpm check:pending-changeset-literals → exit 0, No test source names a pending changeset.
  • Re-derived from the diff:
    • node scripts/check-changeset-presence.mjs → exit 0
    • node scripts/check-changeset-no-major.mjs → exit 0
    • pnpm check:spec-symbols → exit 0
    • pnpm check:i18n-keys → exit 0
    • pnpm check:i18n-dead-keys → exit 0
    • pnpm check:test-path-roots → exit 0
    • pnpm check:unreferenced-sources → exit 0
    • pnpm check:vi-mock-specifiers → exit 0
  • Lint, narrowed to the 4 changed source and test files: eslint --no-inline-config --format json → exit 0, 4 files, 0 errors. The one warning is the pre-existing any on let body in call(). The population is the config's **/*.{ts,tsx} block, and --print-config resolves 116 to 119 rules per file. No file sets parserOptions.project or projectService, so linting is not type-aware and this diff cannot move another file's verdict through types. The repo-wide pnpm lint, the site build and the rest of the farm are left to CI.

Fence. Against base 5a2a6ab, the UsePendingActionsOptions, UsePendingActionsReturn and AiPendingActionsInboxProps declaration blocks are byte-identical, and so are src/index.tsx and the usePendingActions signature. packages/i18n has no diff.

Changeset: .changeset/11736-inbox-error-stops-poll.md, a patch for @object-ui/plugin-chatbot.

Acceptance notes

  • The route is not gated on the AI surface. Triage ruled that out for this card, so the page reached by URL now shows only the tabs, Refresh, and the remedy alert.
  • Observation from reading the code, not measured: the hook's isLoading starts false, so the first paint before the initial read's effect runs can show the empty state for a frame. Fixing it needs a return member or a different initial-state contract, which this card's fence does not allow. Carrier: none.
  • Observation from reading the code, not measured: after a read fails, items keeps the last successful rows, so a failure after a success shows the stale table (with Approve and Reject) under the alert. The card is about the empty state only, and this behaviour is unchanged. Carrier: none.
  • The console page passes the description "Polled every 5 seconds ..." to the inbox. That is still true while the endpoint answers. The text is outside this card's file surface and was not changed.

Generated by Claude Code

claude added 2 commits October 6, 2026 17:49
…mpty queue, and a refused poll stops

On a deployment with no AI service the open edition answers 501 on
/api/v1/ai/pending-actions. The inbox rendered the error alert and,
beneath it, the "No actions waiting" empty state, and usePendingActions
re-armed a fixed five-second setInterval whatever the read answered.

- AiPendingActionsInbox shows the empty state only when the read
  answered; an errored read leaves the alert alone.
- usePendingActions arms each poll from the read that just settled
  (settlePoll), through one fault policy (ListReadOutcome): a refused
  answer (501, any 4xx but 408/429) stops the poll; a transient one
  (no answer, 408, 429, 5xx but 501) doubles the delay up to a
  120 s ceiling; a success resets to pollInterval. Every list read goes
  through it: the tick, refresh(), and the re-fetch after a decision.
- The poll effect keys on primitives only and reaches refresh through a
  ref, not through useCallback's identity (AGENTS.md #10).

No export, option, return member, prop or i18n key changes.

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

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3326.2 KB 3330.4 KB
Main entry chunk (gzip) 154.3 KB 350 KB
Entry file index-DV22ISrq.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) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 578.98KB 139.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 263.36KB 66.69KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 37.51KB 10.04KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.26KB 38.97KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.28KB 65.06KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
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

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.

console(AI approvals): on a deployment with no AI service, the approvals page shows an empty queue under the 501 error and re-polls every 5 s forever

2 participants