Repository navigation
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 intoOct 6, 2026
Conversation
…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>
Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
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
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-11736-inbox-error-stops-poll
branch
October 6, 2026 18:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11736
Clause-②: no. The exported
usePendingActionsandAiPendingActionsInboxoption, 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#1dispatch, sessionhttps://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z.What changes
On a deployment with no AI service, the open edition answers
501on/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 ssetIntervalwhatever the read answered.AiPendingActionsInbox.tsx: the empty state is an answer, so it now renders only when the read answered. The branch readsrows.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 asetTimeoutchain.settlePollre-arms it after each read settles, according to one fault policy,ListReadOutcome:okpollInterval; backoff resets408,429,5xxexcept501transientpollIntervaldoubled per consecutive failure, up tomax(pollInterval, 120 s)501, any other4xx(401,403,404, ...)refusedEvery list read goes through
settlePoll: the initial read, the timer tick, a manualrefresh(), and the re-fetch afterapprove/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 fromMAX_BACKOFF_MSin app-shell'ssharedUserFeeds.ts. The code comment says that nothing keeps the two equal.enabled,pollInterval,apiBase,status,conversationId,limit) and reachesrefreshthrough a ref. Before, it keyed on theuseCallbackidentity, which AGENTS.md commandment 10 bans.The PM's hypotheses, measured on
main5a2a6abrows.length === 0branch never readerror.call()already setserr.status = res.statuson the thrown error, so the classifier reads it there. No second parser was added.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.tsimports only theApproveOutcome/RejectOutcometypes 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.Tests
New pins. Reads are counted as
fetchcalls undervi.useFakeTimers():usePendingActions.pollFaultPolicy-11736.test.tsx(16 cases):501,404,403and401are each read once, and not again across ten intervals;503s come at exactly 10, 20, 40, 80, 120 and 120 s;500,502,504,408,429and a rejectedfetchback off rather than stop;AiPendingActionsInbox.errorReplacesEmpty-11736.test.tsx(3 cases):501the server's message shows, "No actions waiting" is absent, and there is one read across ten intervals;503the error shows without the empty state, and the empty state returns once a backed-off read answers200;Reverse verification. Both legs ran from the committed fix, restored with
git checkout HEAD --, and were proven by matching HEAD blob hashes and an emptygit diff HEAD. The pins import the sources relatively, sodist/is not on the resolution path.settlePoll0 and the oldsetIntervalre-arm line 1. Result: 17 failed / 3 passed. The 3 that passed are the live controls, which must hold on both trees.expected ... data-slot="empty-title" ... to be nullwith "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 projectspnpm --filter @object-ui/plugin-chatbot test→ exit 0, echoesvitest run --root ../.. packages/plugin-chatbot/,Test Files 57 passed (57),Tests 590 passed (590)pnpm --filter @object-ui/plugin-chatbot type-check→ exit 0, echoestsc --noEmit && tsc -p tsconfig.test.json.tsc -p tsconfig.test.json --listFileslists both new test files.pnpm check:control-bytes→ exit 0,OKpnpm 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.node scripts/check-changeset-presence.mjs→ exit 0node scripts/check-changeset-no-major.mjs→ exit 0pnpm check:spec-symbols→ exit 0pnpm check:i18n-keys→ exit 0pnpm check:i18n-dead-keys→ exit 0pnpm check:test-path-roots→ exit 0pnpm check:unreferenced-sources→ exit 0pnpm check:vi-mock-specifiers→ exit 0eslint --no-inline-config --format json→ exit 0, 4 files, 0 errors. The one warning is the pre-existinganyonlet bodyincall(). The population is the config's**/*.{ts,tsx}block, and--print-configresolves 116 to 119 rules per file. No file setsparserOptions.projectorprojectService, so linting is not type-aware and this diff cannot move another file's verdict through types. The repo-widepnpm lint, the site build and the rest of the farm are left to CI.Fence. Against base
5a2a6ab, theUsePendingActionsOptions,UsePendingActionsReturnandAiPendingActionsInboxPropsdeclaration blocks are byte-identical, and so aresrc/index.tsxand theusePendingActionssignature.packages/i18nhas no diff.Changeset:
.changeset/11736-inbox-error-stops-poll.md, apatchfor@object-ui/plugin-chatbot.Acceptance notes
isLoadingstartsfalse, 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.itemskeeps 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.Generated by Claude Code