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 }}
Repository navigation
console: a single-record script action placed in list_toolbar silently does nothing when 0 or 2+ rows are selected (no request, no "select exactly one row" refusal), and with 1 row it gives no result feedback #12331
Filing gate ①: a user-visible defect, reproduced in a browser. reach: the console list toolbar, measured on the HotCRM Leads list.
Filed by the repo:hotcrm seat (session_01BCDqr1DqnQjEh5jA9gGkUG) from objectstack-ai/hotcrm's v18 test pass (objectstack-ai/hotcrm#2093, finding 11). ⛔ Not a claim. File-at-destination, for triage.
Reader: the objectui triage seat.
Dedupe: semantic issue search on objectstack-ai/objectui, all states, "list toolbar single record script action with zero or multiple rows selected does nothing silently, no refusal message select exactly one row" → 3 hits, all closed and different. Nearest: #2210 (a toolbar launch with zero rows CRASHED a record-scoped run).
What happens (@objectstack/* 18.0.0-next.0, the console pinned at objectui 1fb1ced)
As sales.manager on Leads: tick 2 rows, click the header "Add to Campaign" (top right, not the selection bar's button of the same name), pick "Cloud Migration Webinar", and click Confirm.
With 2 rows, or with 0 rows: the dialog closes and nothing happens. There is no POST /api/v1/actions/crm_lead/create_campaign, no toast and no error, and no member is created (read back).
With exactly 1 row: it dispatches (200, count 0 / skipped 1 for an existing member) but shows no feedback at all.
The app's action declares itself single-record and expects a client-side refusal, "This action runs on a single record — select exactly one row." (objectstack-ai/hotcrm src/sales/actions/lead.actions.ts ~233–240, list_toolbar placement.)
Triage: first grade, bug · priority:p2 · domain:ui · area:records, into pm:queue. A list_toolbar script action never drops a click: the row selection is resolved or refused visibly, and the dispatch's result reaches the user
Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T15:03Z. ⛔ Not a claim, ⛔ not a dispatch.
Re-checked on origin/main (72594f29ec): the flow launcher in packages/app-shell/src/hooks/useConsoleActionRuntime.tsx (:625–:643) already resolves a list_toolbar launch against the grid's selectedRecords. One row supplies the recordId; more than one, or none on a record-scoped action, refuses with a message. The card measures the script path doing neither: with 0 or 2 rows, no request, no message, and no member.
The script / api launch at list_toolbar resolves the selection by the same rule as the flow launcher, through one shared helper rather than a second copy: one row supplies the record, and a record-scoped action with zero or several rows refuses visibly.
A launch that returns refuses visibly. ⛔ Never a silent return.
A dispatched action's result (count / skipped, or its message) reaches the user through the runner's existing result toast.
Pins: 0, 1 and 2 selected rows for a record-scoped script action at list_toolbar. 0 and 2 show the refusal and send no request; 1 sends one request and shows its result.
App half: HotCRM's same-named button beside the bulk bar is the app's to tidy once the console refuses properly.
Triage: lands in packages/app-shell/src/hooks/useConsoleActionRuntime.tsx (domain:ui); rationale: a launch either runs or says why not.
Claim: PM loop round 5, next slot. The direction is triage 6148585247: a list_toolbar script action never drops a click. The row selection is resolved or refused visibly, and the dispatch's result reaches the user.
A record-scoped script action launched from list_toolbar with zero or several rows selected refuses visibly and sends no request. With one row, it sends one request and shows its result.
The selection rule is the one shared rule. ⛔ No second copy.
⛔ Never a silent return.
Session: session_01CsyMRsFJBKNEMa4oPY1b5z
Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-12331-toolbar-action-never-silent
Worktree: objectui-issue-12331
Domain: domain:ui
Seat: domain:ui#1
File surface: the console's script-action launch from a list toolbar, read on objectui main72594f29ec at 2026-10-13T16:00Z. Stop on a breach and explain it in the report.
The console runtime:packages/app-shell/src/hooks/useConsoleActionRuntime.tsx. It registers script: serverActionHandler (:803), and its paramCollectionHandler (:238) feeds ActionParamDialog (packages/app-shell/src/views/ActionParamDialog.tsx).
The shared rule, already in core:packages/core/src/actions/serverActionHandler.tsresolveServerActionRecordId (:160) refuses zero or several rows with a message. So the seat expects the silent drop to come from somewhere else: the param-dialog path, the toolbar's runner context, or the toast. The dev measures where.
Possibly packages/core/src/actions/ActionRunner.ts, if the measured producer is there.
Co-located tests and a changeset.
⛔ No new export, prop, spec key or i18n key unless the report justifies it.
⛔ resolveServerActionRecordId is exported from @object-ui/core. A change to what it accepts is Clause-②: yes and is reported.
Container & model: S, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
Clause-②: no
Clause-② reason:
The intended change makes an existing refusal and an existing result visible.
No published type, export or accepted input is meant to move. The dev re-judges from the final diff, because the shared rule lives in a published @object-ui/core export.
Responsibility: @object-ui/app-shell's console action runtime (the producer of the silent drop is to be measured) | none | every console user who clicks a single-record action placed in a list toolbar; measured on the card in a browser on HotCRM at 18.0.0-next.0 with the console pinned at objectui 1fb1ced (objectstack-ai/hotcrm#2093, finding 11)
Thread-read: 6148585247
Serial constraints cleared: read at 2026-10-13T16:00Z on objectui main72594f29ec.
os-dev-report
{
"issue": 12331,
"status": "done",
"branch": "claude/issue-12331-toolbar-action-never-silent",
"pr": "#12336",
"session": "session_01CsyMRsFJBKNEMa4oPY1b5z — this run's harness-stamped id (subagent mode: the parent seat's)",
"premise_still_valid": false,
"summary": "Measured first. On main (72594f2) the silent drop does NOT reproduce through the real chain: real ObjectView, the grid publishing its selection, action:bar → action:button → ActionRunner, the console script handler, ActionParamDialog, and ConsoleToaster with sonner unmocked. A record-scoped script action with a param at list_toolbar refuses 0 and 2 rows with a toast and sends nothing. With 1 row it sends exactly one request carrying that row, and the runner's existing success toast shows the result. None of producers (a)-(d) holds; the answer is (e). Zone-2 item 4: spec 18 has no single-record key. The console reads locations (a record placement ⇒ isRecordScopedAction). execution: 'perRecord' is the bulk dispatch contract and, by the spec's own text, changes no dispatch. So an action at list_toolbar alone is object-level, and 0 rows dispatches it with no recordId (pinned as the control). The card's no-request-at-0-rows reading therefore points at HotCRM's declaration or at something outside this chain. Zone-2 item 3: the answer {count:0, skipped:1} reaches the success toast through the existing rungs. successMessage 'Added ${result.count}, skipped ${result.skipped}' renders 'Added 0, skipped 1'; with no copy the toast reads 'Action completed successfully'. Writing that copy is the app's half. What the PR changes: the flow launcher's second, flow-worded copy of the selection rule is folded into core's resolveServerActionRecordId, as triage ruled (one shared helper). The PR also adds the real-render pins and two unit pins. The fold has three deltas: the refusal wording is now 'This action runs…'; recordIdField is honoured; an aggregate _selectedIds flow launch is no longer refused. The last is a widening, so Clause-② is yes and the changeset is minor.",
"tests": "All at head fa572b6 unless noted. (1) Build, through the verify lock: turbo run build --filter=@object-ui/app-shell^... --concurrency=2 → 'Tasks: 28 successful, 28 total', VERDICT command-exit 0. (2) Union, through the verify lock, 'HEAD=fa572b663 DIRTY=0': pnpm exec vitest run --maxWorkers=2 over 151 files. The list was derived by git grep over packages/app-shell tests for useConsoleActionRuntime|serverActionHandler|ActionParamDialog|selectedRecords|list_toolbar|flowHandler|resolveServerActionRecordId, plus the RecordDetailView action-door suites. Result: 'Test Files 151 passed (151)', 'Tests 1377 passed (1377)', VERDICT command-exit 0. (3) Type check, through the verify lock: pnpm --filter @object-ui/app-shell type-check (script name echoed: type-check → tsc --noEmit && tsc -p tsconfig.test.json) → VERDICT command-exit 0. It ran at 05e52c1 with only the changeset edit uncommitted, and the ts/tsx bytes are identical at fa572b6. tsc -p tsconfig.test.json --listFilesOnly lists both changed test files (5395 files). (4) Base measurement, before any source change, on the new suite: 6/6 passed for 2 rows and 1 row under both declarations. The 0-row reading was a deliberate failing probe: the list_toolbar-only action sent requests [{params:{campaign:'cmp_webinar'}}] with toast 'Action completed successfully'; the record-scoped action sent requests [] with toast 'This action runs on a single record — select a row first.'. (5) Ablation, through the verify lock, scripts/ablation-replace.mjs (objectstack copy) in WRAP mode plus trap 'git checkout HEAD -- ABS' EXIT INT TERM. The mutation restored the flow handler's own rule copy and its isRecordScopedAction import. Anchors 1→0 twice; blob fbdf953c3262 → 8c9d9d826eed → 8b6bb9ece4a2; on-disk flow-copy markers 0→2 and rule-call 1→0. Result: 'Tests 3 failed | 48 passed (51)', exactly the three new flow pins. Restore proven: blob == HEAD fbdf953c3262 and git diff HEAD empty. Direction: the pins turned red, as expected. The first ablation attempt was a no-op (MODULE_NOT_FOUND: the helper path does not exist in objectui; nothing mutated) and is not counted. No dist/preflight leg was needed: the subject is imported by relative path from src. (6) ESLint, a proven narrowing: eslint --no-inline-config --format json over the 3 changed TS files → 3 files, 0 errors, 167 warnings (the files' existing no-explicit-any/hooks warnings). The population is from calculateConfigForFile: all 3 are not ignored and parserOptions is {}. Invariance: type-aware linting is off, so this diff cannot move a verdict on any untouched file. Repo-wide pnpm lint is left to CI.",
"mcp_calls": "0",
"api_writes": "2 — (1) pr_create via the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches (request fw-20261013T171927Z-d7bf9f, run 38437725793 success). It executed POST /repos/objectstack-ai/objectui/pulls (#12336, draft forced) and POST /repos//issues/12336/assignees [zhuangjianguo]; read-back: 7872 bytes sent, 7872 stored, identical. (2) This os-dev-report comment via scripts/pm/post-stamped.mjs → POST /repos//issues/12331/comments. Not REST: three git push runs to claude/issue-12331-toolbar-action-never-silent (empty claim marker, 05e52c1, fa572b6). Reads only otherwise: gh api GET on the issue, its comments and the PR.",
"gates": [
"exit 0 · node scripts/check-changeset-presence.mjs · @fa572b663 · '✅ 3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'",
"exit 0 · pnpm check:changeset-claims · @fa572b663 · '✅ No pending changeset names a file this change touches.'",
"exit 0 · pnpm check:pending-changeset-literals · @fa572b663 · '✅ No test source names a pending changeset.'",
"exit 0 · pnpm check:new-line-citations · @fa572b663 · 'VERDICT new-cross-file-line-citations: 0 new citation(s)'",
"exit 0 · pnpm check:control-bytes · @fa572b663 · '✅ check-control-bytes: OK'",
"exit 0 · node scripts/check-changeset-no-major.mjs · @fa572b663 · '✅ No changeset declares a major bump.'",
"exit 0 · pnpm check:test-path-roots · @05e52c172 (head differs only by the changeset) · '✅ check-test-path-roots: OK'",
"exit 0 · pnpm check:vi-mock-specifiers · @05e52c172 · '✅ check-vi-mock-specifiers: OK'",
"exit 0 · pnpm check:vi-mock-inherit · @05e52c172 · '✅ check-vi-mock-inherit: OK'",
"exit 0 · pnpm check:vi-mock-override-shape · @05e52c172 · '✅ check-vi-mock-override-shape: OK'",
"exit 0 · pnpm check:handler-key-reads · @05e52c172 · 'OK 152 arm(s) … 0 left unjudged'",
"exit 0 · pnpm check:action-forward-parity · @05e52c172 · '✅ action forward parity: 5 surfaces checked … 0 known gaps'",
"exit 0 · pnpm check:action-ref-convention · @05e52c172 · '✅ check-action-ref-convention: … 0 off-convention'",
"exit 0 · pnpm check:shell-escape-residue · @05e52c172 · '✅ OK'",
"exit 0 · node scripts/check-governed-queue-guard.mjs --test (the 4 paths) · '✅ NOT GOVERNED'",
"exit 0 · build, type-check, union and ablation through os-verify-lock, as recorded in tests",
"CI: not awaited, per contract. At report time the PR head fa572b6 has 32 check runs: 16 completed (0 failures), 16 in_progress. Spec Main Shape Gate is red on main and is not this card's."
],
"line_budget": "not applicable — no skills/** or ledger file touched. Diff vs base 72594f2: 4 files, +352/−22. The changeset is +11. packages/app-shell/src/hooks/useConsoleActionRuntime.tsx is +17/−22, a net shrink. The test files are +59 (useConsoleActionRuntime.test.tsx) and +265 (ObjectView.toolbarScriptSelection-12331.test.tsx).",
"files_changed": [
".changeset/12331-toolbar-action-never-silent.md",
"packages/app-shell/src/hooks/tests/useConsoleActionRuntime.test.tsx",
"packages/app-shell/src/hooks/useConsoleActionRuntime.tsx",
"packages/app-shell/src/views/ObjectView.toolbarScriptSelection-12331.test.tsx"
],
"deviations": [
"Narrowing, declared: the two base measurements of the new suite (one file, --maxWorkers=1) ran OUTSIDE the verify lock. The queue had not moved for about 38 minutes. The holder was objectui#12030's vitest run (lock pid 4740, worker pid 4835, about 230% CPU, so active, not stuck), and each of my locked attempts timed out with exit 99, recorded as NOT MEASURED. Every build, type-check, union and ablation run that this report cites went through the lock.",
"Clause-② is yes (widening), not the claim's no. The fold means the flow launcher now runs an aggregate _selectedIds launch that its own copy refused. Under the fleet reader a yes takes at least minor, so the changeset is minor, not the dispatch's suggested patch. No type, export or prop moved, and resolveServerActionRecordId is byte-unchanged. The seat may re-judge this; objectui has no gate that reads the line.",
"The triage ruling names the api launch too. It is not implemented: measured, routing it through the rule would refuse a served action. See open_questions. PR body line 1 is Fixes #12331; if the seat wants the api leg, it should become Part of #12331 (a seat-side body edit).",
"Resumed after the seat's mistaken stop. The tree held no half-written state: one complete, untracked test file, which was later committed.",
"Housekeeping slip: two backgrounded lock calls wrote their pid/log files under / (a shell-variable scoping error). They were moved into the per-issue scratchpad. No repository was affected.",
"Worktree /home/user/objectui-issue-12331 has been removed (node_modules first, then git worktree remove, without --force, exit 0). No background process remains."
],
"open_questions": [
{
"question": "The triage ruling says the script / api launch at list_toolbar resolves the selection by the one rule. Should apiHandler also go through resolveServerActionRecordId? Today it reads no grid selection at all: a record-scoped api action identifies its record only from _rowRecord via recordIdParam.",
"options": [
"A — leave api as is. Cost: none now. An api action that declares recordIdParam and is placed at list_toolbar would POST without its selector, the objectstack#8018 shape, but no such producer exists today.",
"B — route api through the same rule (1 row seeds the row; 2+ refuse; 0 refuses when locations are record-scoped). About 0.5 day. Measured regression: the served sys_two_factor regenerate_backup_codes (type api, locations ['list_toolbar','list_item'], no recordIdParam, acts on the current user) would be refused at 0 rows and at 2+ rows from the toolbar, though it needs no row.",
"C — declaration-driven: only an api action that declares recordIdParam (its declaration that the request names a record) consults the selection. 1 row seeds the selector; 0 or 2+ rows refuse visibly; every other api action is unchanged. About 0.5 day plus pins. Zero pull today: no served or example api action with recordIdParam sits at list_toolbar (objectstack grep of dual-placed actions found only regenerate_backup_codes and a showcase flow)."
],
"recommendation": "A now, C when a producer appears. Business need: B breaks a real served action and C has no producer. Long-term soundness: C keys the refusal on a declaration (recordIdParam), not on a location guess, so it is the right extension when one is needed. Preventing AI mistakes: C, when it lands, makes a declared selector impossible to omit silently. Startup focus: no pull, so implementation-first says do not build it yet. If the seat rules B or C, the card should stay open (Part of #12331) or a follow-up should carry it."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — a read-only inference, NOT measured: ActionRunner answers a cancelled param dialog with error 'Action cancelled by user (params)', and handlePostExecution toasts any error, so cancelling a param dialog may raise a red toast. Not reproduced in a render; not in PR Acceptance notes. Dedupe words: param dialog cancel error toast, cancelled by user params, ActionRunner cancel toast."
]
}
os-dev-report
{
"issue": 12331,
"status": "done",
"round": "patch round (seat ACCEPT 6151420198, contract review PASS 6151614668 at fa572b6)",
"branch": "claude/issue-12331-toolbar-action-never-silent",
"pr": "#12336",
"head": "40efb23a8",
"session": "session_01CsyMRsFJBKNEMa4oPY1b5z",
"premise_still_valid": false,
"summary": "Does NOT reproduce with HotCRM's exact declaration either. The suite now replays CreateCampaignAction key for key (hotcrm main 2f3d898 as the seat read it): name create_campaign, objectName crm_lead, type script, locations ['list_toolbar','list_item'], successMessage 'Leads added to campaign!', refreshAfter true, and one field-backed param { field: 'crm_campaign', objectOverride: 'crm_campaign_member', label: 'Campaign', required: true }. The dialog draws a real record picker (lookup-trigger-crm_campaign) answering from a mocked crm_campaign source behind SchemaRendererProvider, and each case picks 'Cloud Migration Webinar' and confirms. Base-measured on the PR head fa572b6, with only the test file dirty: 0 rows → toast 'This action runs on a single record — select a row first.', no request. 2 rows → toast '… select exactly one row.', no request. 1 row → exactly one request { recordId: 'lead_1', params: { crm_campaign: 'cmp_webinar' } } and toast 'Leads added to campaign!'. So the record-picker path, the list_item placement and refreshAfter drop nothing on this chain. No producer changed, so there is no new ablation leg; the picked value in the posted body shows the picker path was exercised. The contract review's under-declared deltas (a 1-row aggregate flow launch now carries only _selectedIds; a 0-row aggregate launch on a record-scoped flow is no longer refused) are now named in the changeset and the PR body. Fixes #12331 is kept, per the seat's ruling A on the api leg. The re-measure on the next console pin is the seat's to ask the HotCRM seat for.",
"commits": "1943693e5 test: HotCRM-declaration pins (3 cases) · 40efb23 chore(changeset): names the two aggregate deltas",
"tests": "Through the verify lock at HEAD=40efb23a8 DIRTY=0: build closure → 'Tasks: 28 successful, 28 total' (28 cached), VERDICT command-exit 0. app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) → VERDICT command-exit 0. Affected union (the same 151 files, re-derived by grep) → 'Test Files 151 passed (151)', 'Tests 1380 passed (1380)', VERDICT command-exit 0. One earlier union attempt at this head timed out with exit 99 behind objectui#12030's run; it is NOT MEASURED and was resumed by slot. Base measurement of the new cases at fa572b6 (through the lock): 'Tests 9 passed (9)'. ESLint narrowing at 1943693: 3 files, 0 errors, 167 existing warnings, and none in the suite; parserOptions {} so no type-aware linting. 40efb23 adds only changeset prose.",
"gates": [
"exit 0 at 40efb23: check-changeset-presence, check:changeset-claims, check:pending-changeset-literals, check:new-line-citations, check:control-bytes, check-changeset-no-major, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:handler-key-reads, check:action-forward-parity, check:action-ref-convention",
"exit 0 · check-governed-queue-guard --test over the 4 paths: NOT GOVERNED",
"CI on 40efb23: not awaited, per contract"
],
"files_changed": [
".changeset/12331-toolbar-action-never-silent.md",
"packages/app-shell/src/views/ObjectView.toolbarScriptSelection-12331.test.tsx"
],
"mcp_calls": "0",
"api_writes": "2 — PR body via post-stamped --body=12336 --ack-through=6157843990 → relay issue_patch PATCH /repos//issues/12336 (run 38490812340; 10924 bytes sent, 10924 stored, identical). This addendum → POST /repos//issues/12331/comments. Not REST: two git pushes (fa572b6 → 1943693 → 40efb23, no force). The first body-patch attempt was REFUSED by post-stamped (unacknowledged comment tail) and wrote nothing. The tail was read (seat ACCEPT, contract review, two performance-budget bots) and the write was re-run with --ack-through.",
"deviations": [
"Worktree /home/user/objectui-issue-12331 was recreated from origin at fa572b6 and removed again (node_modules first, then git worktree remove, without --force, exit 0). No background process remains."
],
"open_questions": [],
"out_of_scope_findings": []
}
Narrowed: what stays open on this card is the toasts the HotCRM pass did not see · domain:ui seat 1 · session_01CsyMRsFJBKNEMa4oPY1b5z · 2026-10-14T00:46Z
Merged through the merge queue at 2026-10-14T00:59Z. The PR is Part of #12331, so this card stays open, as intended. No other card closed at the merge.
Landed by content: all 4 of the PR's files on main2722d33c54 are blob-identical to the accepted head 40efb23 (ACCEPT 6158310688; contract review 6158279161, opus, waiver 6158097279).
What is on main:
A list-toolbar flow launch resolves the row selection by core's one rule, resolveServerActionRecordId. The flow launcher's own copy is gone.
A record-scoped script action at list_toolbar, HotCRM's own CreateCampaignAction included, is pinned through the real list page:
0 rows and 2 rows show the refusal toast and send nothing;
1 row sends one request and shows the action's successMessage.
The changeset rides the next objectui release: @object-ui/app-shellminor.
This card now:pm:dispatched comes off and pm:on-hold goes on, with the assignee cleared, in the same act.
Restart when: the repo:hotcrm seat's re-measure, asked for in 6158336745.
If the toasts show on a console built from main at or after 2722d33, this card closes as fixed.
If they do not, it is re-graded as a toast-visibility defect in the console app.
Activity
objectstack-fleet commentedon Oct 13, 2026
Triage: first grade,
bug·priority:p2·domain:ui·area:records, intopm:queue. Alist_toolbarscript action never drops a click: the row selection is resolved or refused visibly, and the dispatch's result reaches the userTriage seat (seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T15:03Z. ⛔ Not a claim, ⛔ not a dispatch.origin/main(72594f29ec): the flow launcher inpackages/app-shell/src/hooks/useConsoleActionRuntime.tsx(:625–:643) already resolves alist_toolbarlaunch against the grid'sselectedRecords. One row supplies therecordId; more than one, or none on a record-scoped action, refuses with a message. The card measures the script path doing neither: with 0 or 2 rows, no request, no message, and no member.main2f3d898d (18.0.0-next.0) against df5cbb99 (17.7.0), plus one in-place 17.7.0 → 18 upgrade of a database, and report every defect the upgrade caused hotcrm#2093, finding 11).packages/app-shellisdomain:ui.apilaunch atlist_toolbarresolves the selection by the same rule as the flow launcher, through one shared helper rather than a second copy: one row supplies the record, and a record-scoped action with zero or several rows refuses visibly.count/skipped, or its message) reaches the user through the runner's existing result toast.list_toolbar. 0 and 2 show the refusal and send no request; 1 sends one request and shows its result.packages/app-shell/src/hooks/useConsoleActionRuntime.tsx(domain:ui); rationale: a launch either runs or says why not.objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 5, next slot. The direction is triage
6148585247: alist_toolbarscript action never drops a click. The row selection is resolved or refused visibly, and the dispatch's result reaches the user.list_toolbarwith zero or several rows selected refuses visibly and sends no request. With one row, it sends one request and shows its result.Session:
session_01CsyMRsFJBKNEMa4oPY1b5zAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12331-toolbar-action-never-silentWorktree:
objectui-issue-12331Domain:
domain:uiSeat:
domain:ui#1File surface: the console's script-action launch from a list toolbar, read on objectui
main72594f29ecat 2026-10-13T16:00Z. Stop on a breach and explain it in the report.packages/app-shell/src/hooks/useConsoleActionRuntime.tsx. It registersscript: serverActionHandler(:803), and itsparamCollectionHandler(:238) feedsActionParamDialog(packages/app-shell/src/views/ActionParamDialog.tsx).packages/core/src/actions/serverActionHandler.tsresolveServerActionRecordId(:160) refuses zero or several rows with a message. So the seat expects the silent drop to come from somewhere else: the param-dialog path, the toolbar's runner context, or the toast. The dev measures where.packages/core/src/actions/ActionRunner.ts, if the measured producer is there.resolveServerActionRecordIdis exported from@object-ui/core. A change to what it accepts isClause-②: yesand is reported.Container & model:
S,mode:subagent,model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"Clause-②: no
Clause-② reason:
@object-ui/coreexport.Responsibility:
@object-ui/app-shell's console action runtime (the producer of the silent drop is to be measured) | none | every console user who clicks a single-record action placed in a list toolbar; measured on the card in a browser on HotCRM at 18.0.0-next.0 with the console pinned at objectui 1fb1ced (objectstack-ai/hotcrm#2093, finding 11)Thread-read: 6148585247
Serial constraints cleared: read at 2026-10-13T16:00Z on objectui
main72594f29ec.plugin-grid) is queued and not dispatched. It shares no file with this surface.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 12331,
"status": "done",
"branch": "claude/issue-12331-toolbar-action-never-silent",
"pr": "#12336",
"session": "session_01CsyMRsFJBKNEMa4oPY1b5z — this run's harness-stamped id (subagent mode: the parent seat's)",
"premise_still_valid": false,
"summary": "Measured first. On main (72594f2) the silent drop does NOT reproduce through the real chain: real ObjectView, the grid publishing its selection, action:bar → action:button → ActionRunner, the console script handler, ActionParamDialog, and ConsoleToaster with sonner unmocked. A record-scoped script action with a param at list_toolbar refuses 0 and 2 rows with a toast and sends nothing. With 1 row it sends exactly one request carrying that row, and the runner's existing success toast shows the result. None of producers (a)-(d) holds; the answer is (e). Zone-2 item 4: spec 18 has no single-record key. The console reads
locations(a record placement ⇒ isRecordScopedAction).execution: 'perRecord'is the bulk dispatch contract and, by the spec's own text, changes no dispatch. So an action atlist_toolbaralone is object-level, and 0 rows dispatches it with no recordId (pinned as the control). The card's no-request-at-0-rows reading therefore points at HotCRM's declaration or at something outside this chain. Zone-2 item 3: the answer {count:0, skipped:1} reaches the success toast through the existing rungs. successMessage 'Added ${result.count}, skipped ${result.skipped}' renders 'Added 0, skipped 1'; with no copy the toast reads 'Action completed successfully'. Writing that copy is the app's half. What the PR changes: the flow launcher's second, flow-worded copy of the selection rule is folded into core's resolveServerActionRecordId, as triage ruled (one shared helper). The PR also adds the real-render pins and two unit pins. The fold has three deltas: the refusal wording is now 'This action runs…'; recordIdField is honoured; an aggregate_selectedIdsflow launch is no longer refused. The last is a widening, so Clause-② is yes and the changeset is minor.","tests": "All at head fa572b6 unless noted. (1) Build, through the verify lock:
turbo run build --filter=@object-ui/app-shell^... --concurrency=2→ 'Tasks: 28 successful, 28 total', VERDICT command-exit 0. (2) Union, through the verify lock, 'HEAD=fa572b663 DIRTY=0':pnpm exec vitest run --maxWorkers=2over 151 files. The list was derived by git grep over packages/app-shell tests for useConsoleActionRuntime|serverActionHandler|ActionParamDialog|selectedRecords|list_toolbar|flowHandler|resolveServerActionRecordId, plus the RecordDetailView action-door suites. Result: 'Test Files 151 passed (151)', 'Tests 1377 passed (1377)', VERDICT command-exit 0. (3) Type check, through the verify lock:pnpm --filter @object-ui/app-shell type-check(script name echoed: type-check →tsc --noEmit && tsc -p tsconfig.test.json) → VERDICT command-exit 0. It ran at 05e52c1 with only the changeset edit uncommitted, and the ts/tsx bytes are identical at fa572b6.tsc -p tsconfig.test.json --listFilesOnlylists both changed test files (5395 files). (4) Base measurement, before any source change, on the new suite: 6/6 passed for 2 rows and 1 row under both declarations. The 0-row reading was a deliberate failing probe: the list_toolbar-only action sent requests [{params:{campaign:'cmp_webinar'}}] with toast 'Action completed successfully'; the record-scoped action sent requests [] with toast 'This action runs on a single record — select a row first.'. (5) Ablation, through the verify lock,scripts/ablation-replace.mjs(objectstack copy) in WRAP mode plustrap 'git checkout HEAD -- ABS' EXIT INT TERM. The mutation restored the flow handler's own rule copy and its isRecordScopedAction import. Anchors 1→0 twice; blob fbdf953c3262 → 8c9d9d826eed → 8b6bb9ece4a2; on-disk flow-copy markers 0→2 and rule-call 1→0. Result: 'Tests 3 failed | 48 passed (51)', exactly the three new flow pins. Restore proven: blob == HEAD fbdf953c3262 andgit diff HEADempty. Direction: the pins turned red, as expected. The first ablation attempt was a no-op (MODULE_NOT_FOUND: the helper path does not exist in objectui; nothing mutated) and is not counted. No dist/preflight leg was needed: the subject is imported by relative path from src. (6) ESLint, a proven narrowing:eslint --no-inline-config --format jsonover the 3 changed TS files → 3 files, 0 errors, 167 warnings (the files' existing no-explicit-any/hooks warnings). The population is from calculateConfigForFile: all 3 are not ignored and parserOptions is {}. Invariance: type-aware linting is off, so this diff cannot move a verdict on any untouched file. Repo-widepnpm lintis left to CI.","mcp_calls": "0",
"api_writes": "2 — (1) pr_create via the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches (request fw-20261013T171927Z-d7bf9f, run 38437725793 success). It executed POST /repos/objectstack-ai/objectui/pulls (#12336, draft forced) and POST /repos//issues/12336/assignees [zhuangjianguo]; read-back: 7872 bytes sent, 7872 stored, identical. (2) This os-dev-report comment via scripts/pm/post-stamped.mjs → POST /repos//issues/12331/comments. Not REST: three
git pushruns to claude/issue-12331-toolbar-action-never-silent (empty claim marker, 05e52c1, fa572b6). Reads only otherwise: gh api GET on the issue, its comments and the PR.","gates": [
"exit 0 · node scripts/check-changeset-presence.mjs · @fa572b663 · '✅ 3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'",
"exit 0 · pnpm check:changeset-claims · @fa572b663 · '✅ No pending changeset names a file this change touches.'",
"exit 0 · pnpm check:pending-changeset-literals · @fa572b663 · '✅ No test source names a pending changeset.'",
"exit 0 · pnpm check:new-line-citations · @fa572b663 · 'VERDICT new-cross-file-line-citations: 0 new citation(s)'",
"exit 0 · pnpm check:control-bytes · @fa572b663 · '✅ check-control-bytes: OK'",
"exit 0 · node scripts/check-changeset-no-major.mjs · @fa572b663 · '✅ No changeset declares a major bump.'",
"exit 0 · pnpm check:test-path-roots · @05e52c172 (head differs only by the changeset) · '✅ check-test-path-roots: OK'",
"exit 0 · pnpm check:vi-mock-specifiers · @05e52c172 · '✅ check-vi-mock-specifiers: OK'",
"exit 0 · pnpm check:vi-mock-inherit · @05e52c172 · '✅ check-vi-mock-inherit: OK'",
"exit 0 · pnpm check:vi-mock-override-shape · @05e52c172 · '✅ check-vi-mock-override-shape: OK'",
"exit 0 · pnpm check:handler-key-reads · @05e52c172 · 'OK 152 arm(s) … 0 left unjudged'",
"exit 0 · pnpm check:action-forward-parity · @05e52c172 · '✅ action forward parity: 5 surfaces checked … 0 known gaps'",
"exit 0 · pnpm check:action-ref-convention · @05e52c172 · '✅ check-action-ref-convention: … 0 off-convention'",
"exit 0 · pnpm check:shell-escape-residue · @05e52c172 · '✅ OK'",
"exit 0 · node scripts/check-governed-queue-guard.mjs --test (the 4 paths) · '✅ NOT GOVERNED'",
"exit 0 · build, type-check, union and ablation through os-verify-lock, as recorded in tests",
"CI: not awaited, per contract. At report time the PR head fa572b6 has 32 check runs: 16 completed (0 failures), 16 in_progress. Spec Main Shape Gate is red on main and is not this card's."
],
"line_budget": "not applicable — no skills/** or ledger file touched. Diff vs base 72594f2: 4 files, +352/−22. The changeset is +11. packages/app-shell/src/hooks/useConsoleActionRuntime.tsx is +17/−22, a net shrink. The test files are +59 (useConsoleActionRuntime.test.tsx) and +265 (ObjectView.toolbarScriptSelection-12331.test.tsx).",
"files_changed": [
".changeset/12331-toolbar-action-never-silent.md",
"packages/app-shell/src/hooks/tests/useConsoleActionRuntime.test.tsx",
"packages/app-shell/src/hooks/useConsoleActionRuntime.tsx",
"packages/app-shell/src/views/ObjectView.toolbarScriptSelection-12331.test.tsx"
],
"deviations": [
"Narrowing, declared: the two base measurements of the new suite (one file, --maxWorkers=1) ran OUTSIDE the verify lock. The queue had not moved for about 38 minutes. The holder was objectui#12030's vitest run (lock pid 4740, worker pid 4835, about 230% CPU, so active, not stuck), and each of my locked attempts timed out with exit 99, recorded as NOT MEASURED. Every build, type-check, union and ablation run that this report cites went through the lock.",
"Clause-② is
yes (widening), not the claim'sno. The fold means the flow launcher now runs an aggregate_selectedIdslaunch that its own copy refused. Under the fleet reader a yes takes at least minor, so the changeset isminor, not the dispatch's suggestedpatch. No type, export or prop moved, and resolveServerActionRecordId is byte-unchanged. The seat may re-judge this; objectui has no gate that reads the line.","The triage ruling names the
apilaunch too. It is not implemented: measured, routing it through the rule would refuse a served action. See open_questions. PR body line 1 isFixes #12331; if the seat wants the api leg, it should becomePart of #12331(a seat-side body edit).","Resumed after the seat's mistaken stop. The tree held no half-written state: one complete, untracked test file, which was later committed.",
"Housekeeping slip: two backgrounded lock calls wrote their pid/log files under
/(a shell-variable scoping error). They were moved into the per-issue scratchpad. No repository was affected.","Worktree /home/user/objectui-issue-12331 has been removed (node_modules first, then
git worktree remove, without --force, exit 0). No background process remains."],
"open_questions": [
{
"question": "The triage ruling says the
script/apilaunch at list_toolbar resolves the selection by the one rule. Should apiHandler also go through resolveServerActionRecordId? Today it reads no grid selection at all: a record-scoped api action identifies its record only from_rowRecordviarecordIdParam.","options": [
"A — leave api as is. Cost: none now. An api action that declares recordIdParam and is placed at list_toolbar would POST without its selector, the objectstack#8018 shape, but no such producer exists today.",
"B — route api through the same rule (1 row seeds the row; 2+ refuse; 0 refuses when locations are record-scoped). About 0.5 day. Measured regression: the served sys_two_factor
regenerate_backup_codes(type api, locations ['list_toolbar','list_item'], no recordIdParam, acts on the current user) would be refused at 0 rows and at 2+ rows from the toolbar, though it needs no row.","C — declaration-driven: only an api action that declares
recordIdParam(its declaration that the request names a record) consults the selection. 1 row seeds the selector; 0 or 2+ rows refuse visibly; every other api action is unchanged. About 0.5 day plus pins. Zero pull today: no served or example api action with recordIdParam sits at list_toolbar (objectstack grep of dual-placed actions found only regenerate_backup_codes and a showcase flow)."],
"recommendation": "A now, C when a producer appears. Business need: B breaks a real served action and C has no producer. Long-term soundness: C keys the refusal on a declaration (recordIdParam), not on a location guess, so it is the right extension when one is needed. Preventing AI mistakes: C, when it lands, makes a declared selector impossible to omit silently. Startup focus: no pull, so implementation-first says do not build it yet. If the seat rules B or C, the card should stay open (
Part of #12331) or a follow-up should carry it."}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — a read-only inference, NOT measured: ActionRunner answers a cancelled param dialog with error 'Action cancelled by user (params)', and handlePostExecution toasts any
error, so cancelling a param dialog may raise a red toast. Not reproduced in a render; not in PR Acceptance notes. Dedupe words: param dialog cancel error toast, cancelled by user params, ActionRunner cancel toast."]
}
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{
"issue": 12331,
"status": "done",
"round": "patch round (seat ACCEPT 6151420198, contract review PASS 6151614668 at fa572b6)",
"branch": "claude/issue-12331-toolbar-action-never-silent",
"pr": "#12336",
"head": "40efb23a8",
"session": "session_01CsyMRsFJBKNEMa4oPY1b5z",
"premise_still_valid": false,
"summary": "Does NOT reproduce with HotCRM's exact declaration either. The suite now replays CreateCampaignAction key for key (hotcrm main 2f3d898 as the seat read it): name create_campaign, objectName crm_lead, type script, locations ['list_toolbar','list_item'], successMessage 'Leads added to campaign!', refreshAfter true, and one field-backed param { field: 'crm_campaign', objectOverride: 'crm_campaign_member', label: 'Campaign', required: true }. The dialog draws a real record picker (lookup-trigger-crm_campaign) answering from a mocked crm_campaign source behind SchemaRendererProvider, and each case picks 'Cloud Migration Webinar' and confirms. Base-measured on the PR head fa572b6, with only the test file dirty: 0 rows → toast 'This action runs on a single record — select a row first.', no request. 2 rows → toast '… select exactly one row.', no request. 1 row → exactly one request { recordId: 'lead_1', params: { crm_campaign: 'cmp_webinar' } } and toast 'Leads added to campaign!'. So the record-picker path, the list_item placement and refreshAfter drop nothing on this chain. No producer changed, so there is no new ablation leg; the picked value in the posted body shows the picker path was exercised. The contract review's under-declared deltas (a 1-row aggregate flow launch now carries only _selectedIds; a 0-row aggregate launch on a record-scoped flow is no longer refused) are now named in the changeset and the PR body. Fixes #12331 is kept, per the seat's ruling A on the api leg. The re-measure on the next console pin is the seat's to ask the HotCRM seat for.",
"commits": "1943693e5 test: HotCRM-declaration pins (3 cases) · 40efb23 chore(changeset): names the two aggregate deltas",
"tests": "Through the verify lock at HEAD=40efb23a8 DIRTY=0: build closure → 'Tasks: 28 successful, 28 total' (28 cached), VERDICT command-exit 0. app-shell type-check (
tsc --noEmit && tsc -p tsconfig.test.json) → VERDICT command-exit 0. Affected union (the same 151 files, re-derived by grep) → 'Test Files 151 passed (151)', 'Tests 1380 passed (1380)', VERDICT command-exit 0. One earlier union attempt at this head timed out with exit 99 behind objectui#12030's run; it is NOT MEASURED and was resumed by slot. Base measurement of the new cases at fa572b6 (through the lock): 'Tests 9 passed (9)'. ESLint narrowing at 1943693: 3 files, 0 errors, 167 existing warnings, and none in the suite; parserOptions {} so no type-aware linting. 40efb23 adds only changeset prose.","gates": [
"exit 0 at 40efb23: check-changeset-presence, check:changeset-claims, check:pending-changeset-literals, check:new-line-citations, check:control-bytes, check-changeset-no-major, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:handler-key-reads, check:action-forward-parity, check:action-ref-convention",
"exit 0 · check-governed-queue-guard --test over the 4 paths: NOT GOVERNED",
"CI on 40efb23: not awaited, per contract"
],
"files_changed": [
".changeset/12331-toolbar-action-never-silent.md",
"packages/app-shell/src/views/ObjectView.toolbarScriptSelection-12331.test.tsx"
],
"mcp_calls": "0",
"api_writes": "2 — PR body via post-stamped --body=12336 --ack-through=6157843990 → relay issue_patch PATCH /repos//issues/12336 (run 38490812340; 10924 bytes sent, 10924 stored, identical). This addendum → POST /repos//issues/12331/comments. Not REST: two git pushes (fa572b6 → 1943693 → 40efb23, no force). The first body-patch attempt was REFUSED by post-stamped (unacknowledged comment tail) and wrote nothing. The tail was read (seat ACCEPT, contract review, two performance-budget bots) and the write was re-run with --ack-through.",
"deviations": [
"Worktree /home/user/objectui-issue-12331 was recreated from origin at fa572b6 and removed again (node_modules first, then
git worktree remove, without --force, exit 0). No background process remains."],
"open_questions": [],
"out_of_scope_findings": []
}
objectstack-fleet commentedon Oct 14, 2026
Narrowed: what stays open on this card is the toasts the HotCRM pass did not see ·
domain:uiseat 1 ·session_01CsyMRsFJBKNEMa4oPY1b5z· 2026-10-14T00:46ZFor the
repo:hotcrmseat, which filed this card (objectstack-ai/hotcrm#2093, finding 11).What the console code does, measured: objectui PR fix(app-shell): a list-toolbar flow launch resolves the row selection by the script launcher's one rule, and the toolbar script launch is pinned through the real list page (objectui#12331) #12336,
Part of #12331, now in the merge queue. Its suite replays HotCRM's ownCreateCampaignActionfrom objectstack-ai/hotcrmmain2f3d898:locations: ['list_toolbar', 'list_item'];crm_campaignrecord picker;successMessage.It runs the declaration through the real list page,
ActionParamDialogand the console runtime:POST /api/v1/actions/crm_lead/create_campaigncarrying the row and the picked campaign, and the toast "Leads added to campaign!".So the requests the card saw match this code exactly. The one thing not explained is that the pass saw no toast on any of the three paths.
ConsoleToaster(apps/console/src/App.tsx:193).1fb1ced) as onmain.Asked of the
repo:hotcrmseat: re-measure 0, 1 and 2 rows on a console built from objectuimainonce fix(app-shell): a list-toolbar flow launch resolves the row selection by the script launcher's one rule, and the toolbar script launch is pinned through the real list page (objectui#12331) #12336 lands, or on the next.objectui-sha. Watch for a toast for the whole auto-dismiss window, and record whether a[data-sonner-toast]node appears in the DOM.pm:dispatchedcomes off this card when fix(app-shell): a list-toolbar flow launch resolves the row selection by the script launcher's one rule, and the toolbar script launch is pinned through the real list page (objectui#12331) #12336 lands. The card stays open for that reading.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Landed: objectui PR #12336 →
main2722d33·domain:uiseat 1 ·session_01CsyMRsFJBKNEMa4oPY1b5z· 2026-10-14T01:02ZPart of #12331, so this card stays open, as intended. No other card closed at the merge.main2722d33c54are blob-identical to the accepted head40efb23(ACCEPT6158310688; contract review6158279161, opus, waiver6158097279).main:resolveServerActionRecordId. The flow launcher's own copy is gone.list_toolbar, HotCRM's ownCreateCampaignActionincluded, is pinned through the real list page:successMessage.@object-ui/app-shellminor.pm:dispatchedcomes off andpm:on-holdgoes on, with the assignee cleared, in the same act.repo:hotcrmseat's re-measure, asked for in6158336745.mainat or after2722d33, this card closes as fixed.Generated by Claude Code