You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
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 #11736
Filing gate: ① a reproducible product defect. Users read the page as a live, empty approval queue on a deployment that has none, and the page never stops calling a dead endpoint. This is the honest-degradation contract that ai.console-ai-surface-gating scores against.
Reader: objectui triage's first touch (grade and route), then the domain:ui execution seat that claims it. One PR.
Filed by the triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015, session_01AavokzJ5DndAwitDXvKy4U). It is the Extracted: disposition for the last fail on qa-run objectstack-ai/objectstack#21782 without a card. The defect row is K2 in objectstack's docs/qa/platform-checklist/FOLLOW-UPS.md, marked "UX-integrity — safe to file". Its sibling K1 was objectui#11674 (closed completed). ⛔ Not graded here. ⛔ Not a claim.
Dedupe: MCP search_issues on objectui, open and closed, for "console AI surface 501 alert empty queue poll never backs off". 3 hits, all closed, each a different defect: objectui#11666, objectui#8523, objectui#2477.
What happens
Repro, from the record. It was seen on two runs, and verifier VF4 confirmed it at the pin.
Boot with no AI service. The open edition answers 501 on /api/v1/ai/pending-actions.
Sign in as an admin, open /_console/apps/setup/system/ai-approvals, and watch for 15 s.
What you see:
The error alert renders, and beneath it "No actions waiting": an empty-queue state that reads as a live, empty approval queue.
pending-actions is re-polled at about +0, +4.9 and +10 s, with no backoff, for as long as the page is open.
the empty state at :373 is the rows.length === 0 branch, and it does not consult error.
packages/plugin-chatbot/src/usePendingActions.ts:295: setInterval(() => { void refresh(); }, pollInterval) re-arms whatever the last fetch answered.
apps/console/src/AppContent.tsx:207: system/ai-approvals is still registered with no AI-surface gate, so the page is reachable by URL. Since objectui#10507 removed SystemHubPage, no console surface links to it; that is revision 2 of the checklist item.
Done when
When the request has errored, the inbox shows the error and no empty-queue state.
A refused poll (501, or any non-retryable answer) stops or backs off. It does not re-arm at the fixed interval forever.
Pins cover both, plus a live control: with a working endpoint, an empty queue still shows "No actions waiting" and polling continues.
Open for the grader: whether the route itself should be gated on the AI surface (useAiSurfaceEnabled), as every sibling AI entry point is. The done-when above does not depend on that answer.
Path: ② the capabilities an end user meets in the app — AI approvals on a deployment without AI | 缺项 (ai.console-ai-surface-gating A6 is an expected-fail) | P3
Triage: first grade, bug · priority:p3 · domain:ui · area:ai · pm:queue. The inbox reads its own error, and a refused poll stops
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T17:02Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in packages/plugin-chatbot/src/AiPendingActionsInbox.tsx and usePendingActions.ts ⇒ domain:ui; rationale: both halves are console rendering and polling, and the server's 501 is the honest answer.
Verified at filing on objectui main0cfe7721e7: the empty state at :373 ignores error, and the interval at usePendingActions.ts:295 re-arms unconditionally.
Why p3: the page is reached only by URL (no console surface links to it since objectui#10507), and only on a deployment without the AI service. It misleads, but it changes no data.
Direction:
an error replaces the empty state;
a non-retryable answer (501, 404, 403) stops the poll. A transient failure backs off, within a cap.
The hook keeps one fault policy, so the chatbot's other callers of usePendingActions get the same behaviour.
The open question, answered: ⛔ the route is not gated in this card. Gating it on useAiSurfaceEnabled would be a new gate, and new gates default to no. With the two fixes, the page reached by URL tells the truth, which is all the honest-degradation contract asks.
Claim: PM loop round 2
Session: session_01DrKzdPdyLLBW3qpZ4vtk7z
Account: huangyiirene
Branch: claude/issue-11736-inbox-error-stops-poll
Worktree: objectui-issue-11736
Domain: domain:ui
Seat: domain:ui#1
File surface: packages/plugin-chatbot/src/AiPendingActionsInbox.tsx (the error alert about :327 and the empty-state branch about :373); packages/plugin-chatbot/src/usePendingActions.ts (the poll interval about :295 and the fetch that sets error); the tests beside them; and .changeset/11736-*.md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)
Clause-②: no
Responsibility: objectui's own chatbot plugin produces it — the inbox's empty-state branch ignores the hook's error, and the hook's interval re-arms unconditionally | no platform path covers it: the server's 501 is the honest answer, and the console has no AI-surface gate on this route | an admin who opens the route by URL on a deployment without the AI service reaches it (no console surface links to it since objectui#10507); the QA run objectstack-ai/objectstack#21782 reached it
Thread-read: 6021313545
Serial constraints cleared: none. No open objectui PR touches packages/plugin-chatbot/** (all open PRs' file lists were read), and no in-flight claim of any domain:ui seat names it. Dispatch is serial for this seat (maintainer instruction, seat post 6020143345); nothing else of this seat is in flight. Read 2026-10-06T17:39Z.
Why Clause-②: no: an errored request stops showing an empty queue, and a refused poll stops re-arming. That is runtime behaviour inside the plugin. It widens no accepted input, and the fence keeps the published surface still: usePendingActions and AiPendingActionsInbox are exported from @object-ui/plugin-chatbot's entry (src/index.tsx about :416–:426), so their option, return and props types must not gain or change a member, and no i18n key is added. If the fix cannot be done inside that fence, the dev stops and reports, and the seat re-claims with yes.
{
"issue": 11736,
"status": "done",
"branch": "claude/issue-11736-inbox-error-stops-poll",
"pr": "https://github.com/objectstack-ai/objectui/pull/11741",
"session": "session_01DrKzdPdyLLBW3qpZ4vtk7z — subagent of the domain:ui#1 PM (parent's id); the relay derived the same id from the container (CLAUDE_CODE_REMOTE_SESSION_ID cse_01DrKzdPdyLLBW3qpZ4vtk7z)",
"premise_still_valid": true,
"summary": "AiPendingActionsInbox now renders the empty state only when the read answered; an errored read leaves the error alert alone (branch reads `rows.length === 0 && error ? null : ...`). usePendingActions replaces the unconditional setInterval with a setTimeout chain re-armed by settlePoll after every list read (tick, initial, manual refresh(), post-approve/reject re-fetch) under one fault policy ListReadOutcome: refused (501, any 4xx but 408/429) stops the poll; transient (no HTTP status, 408, 429, 5xx but 501) doubles the delay up to max(pollInterval, 120 s); ok resets to pollInterval; a stopped poll restarts only on a successful read or an input change, and a per-effect generation keeps a read that outlives unmount/input change from re-arming. The poll effect now keys on primitives only and reaches refresh through a ref (AGENTS.md commandment 10). Fence held: no exported option/return/props member, entry export or i18n key changed (byte-compared against base). For the PM: on landing, the QA loop step on the card (append ai.console-ai-surface-gating to objectstack-ai/objectstack#22017's retest list) is the accepting seat's.",
"hypotheses": {
"H1": "CONFIRMED on main 5a2a6ab: the rows.length === 0 branch never read `error`.",
"H2": "CONFIRMED, and answered: the interval re-armed unconditionally; the fetch layer does NOT drop the status — call() already sets err.status = res.status on the thrown Error, so classifyListFailure reads it there; no second parser.",
"H3": "FALSIFIED in part: there are no other in-repo callers. `git grep -n \"usePendingActions(\"` → AiPendingActionsInbox.tsx (the one call; positive control) + usePendingActions.ts's own declaration and JSDoc example. useHitlInChat.ts imports only the ApproveOutcome/RejectOutcome types and never polls. The hook is a package-entry export, so any outside caller (the module header names a Studio traces panel) now stops on a refused read, backs off on a transient one, and keeps its interval on success — the same policy as the inbox.",
"H4": "DONE: fake-timer pins count fetch calls — refused statuses read once across 10 intervals; 503 delays exactly 10/20/40/80/120/120 s; success after failures resets to 5 s; live control polls every 5 s and shows 'No actions waiting'."
},
"tests": {
"head": "74b6272 (git rev-parse --short HEAD in the worktree; the union below ran on it, tree clean; the earlier union on 2b29e69 was re-run after the changeset-wording commit)",
"gates": [
"pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-chatbot^...' build (os-verify-lock) → exit 0 → 'Scope: 9 of 47 workspace projects' / 'VERDICT command-exit 0'",
"pnpm --filter @object-ui/plugin-chatbot test (os-verify-lock; echoes 'vitest run --root ../.. packages/plugin-chatbot/') → exit 0 → 'Test Files 57 passed (57)' 'Tests 590 passed (590)'",
"pnpm --filter @object-ui/plugin-chatbot type-check (os-verify-lock; echoes 'tsc --noEmit && tsc -p tsconfig.test.json') → exit 0 → 'VERDICT command-exit 0'; tsc -p tsconfig.test.json --listFiles lists both new test files (2 of 2)",
"pnpm check:control-bytes → exit 0 → 'check-control-bytes: OK (scanned 7700 tracked text file(s); skipped 85 binary).'",
"pnpm check:new-line-citations → exit 0 → 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0'",
"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.'",
"ADDED (re-derived from the diff): node scripts/check-changeset-presence.mjs → exit 0 → '4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/11736-inbox-error-stops-poll.md.'",
"ADDED: node scripts/check-changeset-no-major.mjs → exit 0 → 'No changeset declares a `major` bump.'",
"ADDED: pnpm check:spec-symbols → exit 0 (hook imports spec types)",
"ADDED: pnpm check:i18n-keys → exit 0; pnpm check:i18n-dead-keys → exit 0 (inbox file touched; no key changed)",
"ADDED: pnpm check:test-path-roots → exit 0; pnpm check:unreferenced-sources → exit 0 → 'Every shipped source file in every covered package is reachable.'; pnpm check:vi-mock-specifiers → exit 0",
"Lint, proven narrowing: eslint --no-inline-config --format json over the 4 changed .ts/.tsx files → exit 0 → 4 files, 0 errors, 1 warning (pre-existing no-explicit-any on `let body: any` in call(), present at base). Population: eslint.config.js '**/*.{ts,tsx}' block, --print-config resolves 116-119 rules per file. Invariance: no file's config sets parserOptions.project or projectService (not type-aware), so the diff cannot move an untouched file's verdict through types. Repo-wide pnpm lint is CI's.",
"Fence: UsePendingActionsOptions / UsePendingActionsReturn / AiPendingActionsInboxProps blocks byte-identical to base 5a2a6ab (1035/642/1077 bytes), src/index.tsx identical, usePendingActions signature identical, packages/i18n diff 0 lines → fence script exit 0",
"NOT RUN: check:eager-closure — the diff adds 0 import lines to src, so no import edge moved; declared to CI. The site build (ci.yml lists packages/plugin-chatbot/) and the rest of the farm are CI's."
],
"ablation": "Both legs run from the committed fix (2b29e69), trap-restored with `git checkout HEAD --` on absolute paths, restore proven by HEAD blob hash match + empty `git diff HEAD`; pins import ../usePendingActions and ../AiPendingActionsInbox relatively, so dist/ is not on the resolution path (no rebuild needed). Hook leg (hook reverted to base; landing proof settlePoll 0, old setInterval re-arm line 1): 'Tests 17 failed | 3 passed (20)' — the 3 passing are the live controls. Inbox leg (inbox reverted to base; landing proof error-guard 0, bare empty branch 1): 'Tests 2 failed | 18 passed (20)', both 'AssertionError: expected ... data-slot=\"empty-title\" ... to be null' receiving 'No actions waiting'. Direction observed: turn red, as predicted.",
"files_changed": [
"packages/plugin-chatbot/src/usePendingActions.ts",
"packages/plugin-chatbot/src/AiPendingActionsInbox.tsx",
"packages/plugin-chatbot/src/__tests__/usePendingActions.pollFaultPolicy-11736.test.tsx (new, 16 cases)",
"packages/plugin-chatbot/src/__tests__/AiPendingActionsInbox.errorReplacesEmpty-11736.test.tsx (new, 3 cases)",
".changeset/11736-inbox-error-stops-poll.md (new, patch @object-ui/plugin-chatbot)"
],
"deviations": [
"Conflict named, os-dev definition followed: the harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; commits carry the model-free pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md / os-dev, and the PR body ends with the session-URL footer.",
"A second commit (74b6272) only rewords one changeset line ('any other 4xx than' → 'any 4xx except'); the whole gate union was re-run on it.",
"No write was refused by the permission layer; zero label writes (labels plugin/tests on the PR came from labeler.yml)."
]
},
"mcp_calls": "0 — no MCP tool was called",
"api_writes": "3 REST writes, all relayed as objectstack-fleet[bot] via POST /repos/objectstack-ai/objectstack/dispatches (204): (1) pr_create → run 37508348497 executed POST /repos/objectstack-ai/objectui/pulls (draft #11741; read-back 8510 bytes sent = stored); (2) label-write.mjs --assign → run 37508465341 added assignee huangyiirene to objectui#11741 (read-back MATCHES; labels untouched); (3) this os-dev-report comment via post-stamped.mjs → POST /repos/objectstack-ai/objectui/issues/11736/comments. Plus git push of claude/issue-11736-inbox-error-stops-poll (3 pushes of the same branch = one budget item; not REST). Reads: single-card REST GETs only (issue, comments, PR).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in PR Acceptance notes, not filed — read-only inference, NOT MEASURED: usePendingActions' isLoading starts false, so the inbox may paint the empty state for one frame before the first read's effect runs; closing it needs a return member or an initial-state contract change, outside this card's fence · dedupe words: inbox first paint, isLoading initial false, empty state flash",
"carrier: 承接者:无 · noted in PR Acceptance notes, not filed — read-only inference, NOT MEASURED: after a failed read the hook keeps the last successful items, so a failure after a success shows the stale table (with Approve/Reject) under the alert; behaviour unchanged by this PR · dedupe words: stale rows under error, usePendingActions items kept on error",
"carrier: 承接者:无 · noted in PR Acceptance notes, not filed — the console page AiPendingActionsPage passes the literal description 'Polled every 5 seconds ...'; still true while the endpoint answers; outside the claim's file surface · dedupe words: AiPendingActionsPage description polled every 5 seconds"
]
}
domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · reviewed against the PR diff and check runs, read 2026-10-06T18:09Z. Report: os-dev-report on this card.
Shape: draft → main; body line 1 Fixes #11736; Clause-②: no at line start; PR assignee huangyiirene (= card).
Scope: 5 files, all inside the claim's surface:
usePendingActions.ts;
AiPendingActionsInbox.tsx;
two new pin files;
one changeset.
+494/−9. Nothing under content/docs/releases/.
Fence held, checked on the diff: the claim forbade any change to the exported types. The diff adds only module-private names: ListReadOutcome, classifyListFailure, PollSchedule, settlePoll and refs inside the hook. It does not touch the option, return or props types, and no i18n key moves. The dev's byte comparison of the three exported type blocks against base is in the PR body.
Mechanism checked on the diff:
Inbox:rows.length === 0 && error ? null, so the empty state needs an answered read.
Hook: the fixed setInterval is gone. Each read settles into settlePoll:
refused (a numeric status that is 501 or any 4xx other than 408/429) arms nothing;
transient (no status, 408, 429, or a 5xx other than 501) arms pollInterval · 2^failures, capped at max(pollInterval, 120 s);
ok resets the backoff.
A generation counter drops reads that settle after an unmount or an input change.
The effect's dependency list is now primitives only (refresh is read through a ref).
The status comes from the existing err.status that call() sets, so there is no second parser (H2).
Changeset (@object-ui/plugin-chatbot patch). Sentences checked against the diff:
"shows the empty state only when the read answered": matches.
"A refused read stops the poll … 501, or any 4xx except 408 and 429": matches classifyListFailure.
"The delay doubles with each consecutive failure, up to 120 seconds, or up to pollInterval if that is longer": matches settlePoll.
"A manual refresh() that succeeds, the re-fetch after approve or reject, or a change to the hook's options starts it again": matches, because every read routes through settlePoll, and an input change bumps the generation.
H3 corrected by the dev, accepted: the inbox is the only in-repo caller. The hook is exported, so an outside caller gets the same policy.
Tests: 16 fake-timer pins on the hook (a refused status is read once across 10 intervals; the backoff ladder is 10/20/40/80/120/120 s; success resets it; a live control polls every 5 s) and 3 inbox pins. Two ablations from committed state each turned red at their own pins (17 failed, and 2 failed), with the live controls green; the restore was proven by the blob hash. Local plugin-chatbot suite: 57 files / 590 tests. type-check passes, with the script name echoed.
CI at 74b6272: 42 runs, still converging (11 in_progress at this read); no completed run is red. Enqueue waits for every check to be green.
Out-of-scope findings: all three are Acceptance notes, read from code and not measured:
a possible one-frame empty state before the first read (isLoading starts false; closing it needs a return-type change the fence forbids);
rows from the last good read stay under the alert after a later failure;
the console page's "Polled every 5 seconds" description.
Activity
objectstack-fleet commentedon Oct 6, 2026
Path: ② the capabilities an end user meets in the app — AI approvals on a deployment without AI | 缺项 (
ai.console-ai-surface-gatingA6 is an expected-fail) | P3Triage: first grade,
bug·priority:p3·domain:ui·area:ai·pm:queue. The inbox reads its own error, and a refused poll stopsTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T17:02Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugin-chatbot/src/AiPendingActionsInbox.tsxandusePendingActions.ts⇒domain:ui; rationale: both halves are console rendering and polling, and the server's501is the honest answer.main0cfe7721e7: the empty state at:373ignoreserror, and the interval atusePendingActions.ts:295re-arms unconditionally.501,404,403) stops the poll. A transient failure backs off, within a cap.usePendingActionsget the same behaviour.useAiSurfaceEnabledwould be a new gate, and new gates default to no. With the two fixes, the page reached by URL tells the truth, which is all the honest-degradation contract asks.ai.console-ai-surface-gatingto qa(checklist): park the blocked partial-coverage gaps of the 17.7 pre-release runs (#21720, #21721, #21782, #21784) — each gets a fixture recipe, a pin, a re-pointed clause or a recorded knownGap objectstack#22017's retest list, and the item's clause 6 expected-fail wording is updated there.objectstack-fleet commentedon Oct 6, 2026
Claim: PM loop round 2
Session:
session_01DrKzdPdyLLBW3qpZ4vtk7zAccount:
huangyiireneBranch:
claude/issue-11736-inbox-error-stops-pollWorktree:
objectui-issue-11736Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/plugin-chatbot/src/AiPendingActionsInbox.tsx(the error alert about:327and the empty-state branch about:373);packages/plugin-chatbot/src/usePendingActions.ts(the poll interval about:295and the fetch that setserror); the tests beside them; and.changeset/11736-*.md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)Clause-②: no
Responsibility:
objectui's own chatbot plugin produces it — the inbox's empty-state branch ignores the hook's error, and the hook's interval re-arms unconditionally | no platform path covers it: the server's 501 is the honest answer, and the console has no AI-surface gate on this route | an admin who opens the route by URL on a deployment without the AI service reaches it (no console surface links to it since objectui#10507); the QA run objectstack-ai/objectstack#21782 reached itThread-read: 6021313545
Serial constraints cleared:
none. No open objectui PR touchespackages/plugin-chatbot/**(all open PRs' file lists were read), and no in-flight claim of anydomain:uiseat names it. Dispatch is serial for this seat (maintainer instruction, seat post6020143345); nothing else of this seat is in flight. Read 2026-10-06T17:39Z.Why
Clause-②: no: an errored request stops showing an empty queue, and a refused poll stops re-arming. That is runtime behaviour inside the plugin. It widens no accepted input, and the fence keeps the published surface still:usePendingActionsandAiPendingActionsInboxare exported from@object-ui/plugin-chatbot's entry (src/index.tsxabout:416–:426), so their option, return and props types must not gain or change a member, and no i18n key is added. If the fix cannot be done inside that fence, the dev stops and reports, and the seat re-claims withyes.objectstack-fleet commentedon Oct 6, 2026
os-dev-report
{ "issue": 11736, "status": "done", "branch": "claude/issue-11736-inbox-error-stops-poll", "pr": "https://github.com/objectstack-ai/objectui/pull/11741", "session": "session_01DrKzdPdyLLBW3qpZ4vtk7z — subagent of the domain:ui#1 PM (parent's id); the relay derived the same id from the container (CLAUDE_CODE_REMOTE_SESSION_ID cse_01DrKzdPdyLLBW3qpZ4vtk7z)", "premise_still_valid": true, "summary": "AiPendingActionsInbox now renders the empty state only when the read answered; an errored read leaves the error alert alone (branch reads `rows.length === 0 && error ? null : ...`). usePendingActions replaces the unconditional setInterval with a setTimeout chain re-armed by settlePoll after every list read (tick, initial, manual refresh(), post-approve/reject re-fetch) under one fault policy ListReadOutcome: refused (501, any 4xx but 408/429) stops the poll; transient (no HTTP status, 408, 429, 5xx but 501) doubles the delay up to max(pollInterval, 120 s); ok resets to pollInterval; a stopped poll restarts only on a successful read or an input change, and a per-effect generation keeps a read that outlives unmount/input change from re-arming. The poll effect now keys on primitives only and reaches refresh through a ref (AGENTS.md commandment 10). Fence held: no exported option/return/props member, entry export or i18n key changed (byte-compared against base). For the PM: on landing, the QA loop step on the card (append ai.console-ai-surface-gating to objectstack-ai/objectstack#22017's retest list) is the accepting seat's.", "hypotheses": { "H1": "CONFIRMED on main 5a2a6ab: the rows.length === 0 branch never read `error`.", "H2": "CONFIRMED, and answered: the interval re-armed unconditionally; the fetch layer does NOT drop the status — call() already sets err.status = res.status on the thrown Error, so classifyListFailure reads it there; no second parser.", "H3": "FALSIFIED in part: there are no other in-repo callers. `git grep -n \"usePendingActions(\"` → AiPendingActionsInbox.tsx (the one call; positive control) + usePendingActions.ts's own declaration and JSDoc example. useHitlInChat.ts imports only the ApproveOutcome/RejectOutcome types and never polls. The hook is a package-entry export, so any outside caller (the module header names a Studio traces panel) now stops on a refused read, backs off on a transient one, and keeps its interval on success — the same policy as the inbox.", "H4": "DONE: fake-timer pins count fetch calls — refused statuses read once across 10 intervals; 503 delays exactly 10/20/40/80/120/120 s; success after failures resets to 5 s; live control polls every 5 s and shows 'No actions waiting'." }, "tests": { "head": "74b6272 (git rev-parse --short HEAD in the worktree; the union below ran on it, tree clean; the earlier union on 2b29e69 was re-run after the changeset-wording commit)", "gates": [ "pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-chatbot^...' build (os-verify-lock) → exit 0 → 'Scope: 9 of 47 workspace projects' / 'VERDICT command-exit 0'", "pnpm --filter @object-ui/plugin-chatbot test (os-verify-lock; echoes 'vitest run --root ../.. packages/plugin-chatbot/') → exit 0 → 'Test Files 57 passed (57)' 'Tests 590 passed (590)'", "pnpm --filter @object-ui/plugin-chatbot type-check (os-verify-lock; echoes 'tsc --noEmit && tsc -p tsconfig.test.json') → exit 0 → 'VERDICT command-exit 0'; tsc -p tsconfig.test.json --listFiles lists both new test files (2 of 2)", "pnpm check:control-bytes → exit 0 → 'check-control-bytes: OK (scanned 7700 tracked text file(s); skipped 85 binary).'", "pnpm check:new-line-citations → exit 0 → 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0'", "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.'", "ADDED (re-derived from the diff): node scripts/check-changeset-presence.mjs → exit 0 → '4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/11736-inbox-error-stops-poll.md.'", "ADDED: node scripts/check-changeset-no-major.mjs → exit 0 → 'No changeset declares a `major` bump.'", "ADDED: pnpm check:spec-symbols → exit 0 (hook imports spec types)", "ADDED: pnpm check:i18n-keys → exit 0; pnpm check:i18n-dead-keys → exit 0 (inbox file touched; no key changed)", "ADDED: pnpm check:test-path-roots → exit 0; pnpm check:unreferenced-sources → exit 0 → 'Every shipped source file in every covered package is reachable.'; pnpm check:vi-mock-specifiers → exit 0", "Lint, proven narrowing: eslint --no-inline-config --format json over the 4 changed .ts/.tsx files → exit 0 → 4 files, 0 errors, 1 warning (pre-existing no-explicit-any on `let body: any` in call(), present at base). Population: eslint.config.js '**/*.{ts,tsx}' block, --print-config resolves 116-119 rules per file. Invariance: no file's config sets parserOptions.project or projectService (not type-aware), so the diff cannot move an untouched file's verdict through types. Repo-wide pnpm lint is CI's.", "Fence: UsePendingActionsOptions / UsePendingActionsReturn / AiPendingActionsInboxProps blocks byte-identical to base 5a2a6ab (1035/642/1077 bytes), src/index.tsx identical, usePendingActions signature identical, packages/i18n diff 0 lines → fence script exit 0", "NOT RUN: check:eager-closure — the diff adds 0 import lines to src, so no import edge moved; declared to CI. The site build (ci.yml lists packages/plugin-chatbot/) and the rest of the farm are CI's." ], "ablation": "Both legs run from the committed fix (2b29e69), trap-restored with `git checkout HEAD --` on absolute paths, restore proven by HEAD blob hash match + empty `git diff HEAD`; pins import ../usePendingActions and ../AiPendingActionsInbox relatively, so dist/ is not on the resolution path (no rebuild needed). Hook leg (hook reverted to base; landing proof settlePoll 0, old setInterval re-arm line 1): 'Tests 17 failed | 3 passed (20)' — the 3 passing are the live controls. Inbox leg (inbox reverted to base; landing proof error-guard 0, bare empty branch 1): 'Tests 2 failed | 18 passed (20)', both 'AssertionError: expected ... data-slot=\"empty-title\" ... to be null' receiving 'No actions waiting'. Direction observed: turn red, as predicted.", "files_changed": [ "packages/plugin-chatbot/src/usePendingActions.ts", "packages/plugin-chatbot/src/AiPendingActionsInbox.tsx", "packages/plugin-chatbot/src/__tests__/usePendingActions.pollFaultPolicy-11736.test.tsx (new, 16 cases)", "packages/plugin-chatbot/src/__tests__/AiPendingActionsInbox.errorReplacesEmpty-11736.test.tsx (new, 3 cases)", ".changeset/11736-inbox-error-stops-poll.md (new, patch @object-ui/plugin-chatbot)" ], "deviations": [ "Conflict named, os-dev definition followed: the harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; commits carry the model-free pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md / os-dev, and the PR body ends with the session-URL footer.", "A second commit (74b6272) only rewords one changeset line ('any other 4xx than' → 'any 4xx except'); the whole gate union was re-run on it.", "No write was refused by the permission layer; zero label writes (labels plugin/tests on the PR came from labeler.yml)." ] }, "mcp_calls": "0 — no MCP tool was called", "api_writes": "3 REST writes, all relayed as objectstack-fleet[bot] via POST /repos/objectstack-ai/objectstack/dispatches (204): (1) pr_create → run 37508348497 executed POST /repos/objectstack-ai/objectui/pulls (draft #11741; read-back 8510 bytes sent = stored); (2) label-write.mjs --assign → run 37508465341 added assignee huangyiirene to objectui#11741 (read-back MATCHES; labels untouched); (3) this os-dev-report comment via post-stamped.mjs → POST /repos/objectstack-ai/objectui/issues/11736/comments. Plus git push of claude/issue-11736-inbox-error-stops-poll (3 pushes of the same branch = one budget item; not REST). Reads: single-card REST GETs only (issue, comments, PR).", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — read-only inference, NOT MEASURED: usePendingActions' isLoading starts false, so the inbox may paint the empty state for one frame before the first read's effect runs; closing it needs a return member or an initial-state contract change, outside this card's fence · dedupe words: inbox first paint, isLoading initial false, empty state flash", "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — read-only inference, NOT MEASURED: after a failed read the hook keeps the last successful items, so a failure after a success shows the stale table (with Approve/Reject) under the alert; behaviour unchanged by this PR · dedupe words: stale rows under error, usePendingActions items kept on error", "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — the console page AiPendingActionsPage passes the literal description 'Polled every 5 seconds ...'; still true while the endpoint answers; outside the claim's file surface · dedupe words: AiPendingActionsPage description polled every 5 seconds" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Review: ACCEPT — PR objectui#11741 (head
74b6272)domain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· reviewed against the PR diff and check runs, read 2026-10-06T18:09Z. Report:os-dev-reporton this card.Shape: draft →
main; body line 1Fixes #11736;Clause-②: noat line start; PR assigneehuangyiirene(= card).Scope: 5 files, all inside the claim's surface:
usePendingActions.ts;AiPendingActionsInbox.tsx;+494/−9. Nothing under
content/docs/releases/.Fence held, checked on the diff: the claim forbade any change to the exported types. The diff adds only module-private names:
ListReadOutcome,classifyListFailure,PollSchedule,settlePolland refs inside the hook. It does not touch the option, return or props types, and no i18n key moves. The dev's byte comparison of the three exported type blocks against base is in the PR body.Mechanism checked on the diff:
rows.length === 0 && error ? null, so the empty state needs an answered read.setIntervalis gone. Each read settles intosettlePoll:refused(a numericstatusthat is501or any4xxother than408/429) arms nothing;transient(nostatus,408,429, or a5xxother than501) armspollInterval · 2^failures, capped atmax(pollInterval, 120 s);okresets the backoff.refreshis read through a ref).err.statusthatcall()sets, so there is no second parser (H2).Changeset (
@object-ui/plugin-chatbotpatch). Sentences checked against the diff:501, or any4xxexcept408and429": matchesclassifyListFailure.pollIntervalif that is longer": matchessettlePoll.refresh()that succeeds, the re-fetch afterapproveorreject, or a change to the hook's options starts it again": matches, because every read routes throughsettlePoll, and an input change bumps the generation.H3 corrected by the dev, accepted: the inbox is the only in-repo caller. The hook is exported, so an outside caller gets the same policy.
Tests: 16 fake-timer pins on the hook (a refused status is read once across 10 intervals; the backoff ladder is 10/20/40/80/120/120 s; success resets it; a live control polls every 5 s) and 3 inbox pins. Two ablations from committed state each turned red at their own pins (17 failed, and 2 failed), with the live controls green; the restore was proven by the blob hash. Local
plugin-chatbotsuite: 57 files / 590 tests.type-checkpasses, with the script name echoed.CI at
74b6272: 42 runs, still converging (11in_progressat this read); no completed run is red. Enqueue waits for every check to be green.Out-of-scope findings: all three are Acceptance notes, read from code and not measured:
isLoadingstarts false; closing it needs a return-type change the fence forbids);On landing: this seat appends
ai.console-ai-surface-gatingto qa(checklist): park the blocked partial-coverage gaps of the 17.7 pre-release runs (#21720, #21721, #21782, #21784) — each gets a fixture recipe, a pin, a re-pointed clause or a recorded knownGap objectstack#22017's retest list, as the card asks.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Landed: PR objectui#11741 →
maind39ac2edomain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· verified 2026-10-06T18:40Z.d39ac2eis one squash commit on parentded4494. It changes 5 files, +494/−9, the same as the PR.git diff 74b6272 d39ac2eover those paths is 0 bytes: the landed bytes are the reviewed head's bytes.main:settlePollhitsusePendingActions.ts.completedby the PR'sFixes.pm:dispatchedwas removed and read back.ai.console-ai-surface-gatingwas appended to qa(checklist): park the blocked partial-coverage gaps of the 17.7 pre-release runs (#21720, #21721, #21782, #21784) — each gets a fixture recipe, a pin, a re-pointed clause or a recorded knownGap objectstack#22017's retest list (§5, trigger 2) in a comment there, as the card asked.Generated by Claude Code
fix(plugin-chatbot): the AI Approvals inbox shows the error, not an e…