Repository navigation
Description
domain:devx#1 · os-warren · session_01JQwxbXSZs9PBUc2H8z6mtB
Filing gate: ① product defect with a named landing point and a reproduction (reach: the public action door POST /api/v1/actions/OBJECT/ACTION and the console bulk bar, measured on 2 fresh browser loads and by a forged API call).
Business reason: an admin selects rows on a list and runs a bulk action declared execution: 'aggregate'. That is the whole point of an aggregate action: one request for the selection. Today every such api action that also declares recordIdParam fails for every row ("Succeeded 0 / N · Failed N"), and the target is never called. This is a regression against wave 3 (#27438 PASS): it appeared when the console started sending declared same-origin api actions through the action door (#27611). It turns a P1 checklist item red for the 18.3.0 release.
Reader who acts: the lane that owns packages/runtime/src/action-execution.ts (server). It treats a record-less dispatch as having no row, as the branch's own comment describes, and adds a regression test for an aggregate api action with recordIdParam.
Dedupe: "identifies its record by" 0 hits; recordIdParam aggregate 5 (#7420, #15705, #23128, #7823 closed, all other symptoms; #28981 a run record); bulk action aggregate api 400 85 (run records and unrelated cards). Nearest is #23128 (closed: the showcase recalc endpoint answered 500), which is a different cause on the target, not the door.
QA-source: #29172 · records-forms.list-view-capabilities · A8
What happens
On the showcase task list's "Bulk Actions" view, "Recalculate Selection" (showcase_recalc_selection, type: 'api', execution: 'aggregate', target: '/api/v1/apps/showcase/recalc', recordIdParam: 'recordId') is refused by the action door:
POST /api/v1/actions/showcase_task/showcase_recalc_selection {"params":{"_selectedIds":["ID1","ID2"]}} → 400 VALIDATION_ERROR — Action "Recalculate Selection" identifies its record by "id", but that field is not present on the record — running without it would send a request that names no record.
The bulk dialog then reads "Succeeded 0 / 3 · Failed 3". Measured at framework 7fa9873f (main, 2026-11-02) on a stock showcase boot, Console built from the same tree.
Reproduction
- Boot the showcase and sign in as admin.
- Open
/_console/apps/com.example.showcase/showcase_task/view/bulk_actions, tick a few rows, click Recalculate Selection, then Run. - Expected: one aggregate request carrying every selected id reaches the recalc target and succeeds.
- Actual: the request above answers 400, and the target is never called.
Controls:
- The same body plus
recordIdanswers 200. POST /api/v1/apps/showcase/recalc{"_selectedIds":[…]}called directly answers 200 (selected: 2, acted: 2).- The per-record bulk action on the same bar (Mark Done) succeeds.
Mechanism (read at 7fa9873)
packages/runtime/src/action-execution.ts, in buildApiActionRequest, around L1428–1458. The recordIdParam branch picks its row as input.record && typeof input.record === 'object' ? input.record : …. A record-less dispatch arrives with input.record = {}, because the subject-record load answers an empty object when there is no record id and the api-forward path passes it on. {} is truthy, so the branch treats it as a row, finds no id and refuses. The comment above the branch says the opposite: "Without a row (a record-less dispatch) only id can be seeded, from the routed recordId; anything else has no source and the request goes without it."
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: records-forms.list-view-capabilities
Triage:
domain:server· p1 ·pm:queue· 2026-11-02T09:07Z⛔ Not a claim.
type: 'api'action click throughPOST /actions/:object/:actioninstead of fetching itstargetdirectly (ruling on #27506, step 3 of 3) #27611, so it is p1 under the ruling that release-first work is p1. The fix lands in packages/runtime/src/action-execution.ts buildApiActionRequest, where the empty {} subject record of a record-less dispatch is read as a row. Location and repro are named, so pm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop round 2 · card thread · 2026-11-02T09:12Z
Session:
session_01ADHkmQKwKFCp1m6Pb6Q8c1(the seat); the card thread is named in a follow-up claim once it reports its branchAccount:
os-steve(the seat's linked user; the card's assignee)Branch: pending — the card thread's harness-pinned branch, written into a follow-up claim before the dev starts
Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: no maintainer ruling on this card; triage grade p1 (6573248507: release-first work, a P1 checklist clause red in the 18.3.0 run).
File surface:
packages/runtime/src/action-execution.ts, the api request builder'srecordIdParamseeding only (buildApiActionRequest), its tests, changeset. ⛔ Nopackages/spec/**; ⛔ not the forward-credential region or the subject-load refusal of the same file.Container & model:
S,mode:thread, model: the thread's default judgement tier,effort: high(p1)Clause-②: no
Metadata-protocol: none
Responsibility: the code is the runtime action door's api request builder · a record-less aggregate dispatch is read as a row with no id field, so every aggregate api action declaring
recordIdParamis refused 400 and its target never runs · reached by every bulk action of that shape from the console bulk bar and the action doorThread-read: 6573248507
Premise: holds (read at
origin/main692e0dc8b9: therecordIdParambranch treats any objectinput.recordas a row, so an empty subject record answers "identifies its record by id")Serial constraints cleared: same file as this seat's #29050 (subject-load refusal region) and seat 1's open PR #29036 (forward-credential region, hunks near
apiForwardCredentialRefusal); region-disjoint; the dev mergesorigin/mainbefore opening the PR and the second to land resolves.Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop round 2 · card thread · branch named · 2026-11-02T09:15Z (supersedes 6573350589, which had the branch pending)
Session:
session_01ADHkmQKwKFCp1m6Pb6Q8c1(the seat); the card thread issession_01ADHkmNtfGcifoTscZGVzuoAccount:
os-steve(the seat's linked user; the card's assignee)Branch:
claude/project-thread-q7jqr4(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: no maintainer ruling on this card; triage grade p1 (6573248507: release-first work, a P1 checklist clause red in the 18.3.0 run).
File surface:
packages/runtime/src/action-execution.ts, the api request builder'srecordIdParamseeding only (buildApiActionRequest), its tests, changeset. ⛔ Nopackages/spec/**; ⛔ not the forward-credential region or the subject-load refusal of the same file.Container & model:
S,mode:thread, model: the thread's default judgement tier,effort: high(p1)Clause-②: no
Metadata-protocol: none
Responsibility: the code is the runtime action door's api request builder · a record-less aggregate dispatch is read as a row with no id field, so every aggregate api action declaring
recordIdParamis refused 400 and its target never runs · reached by every bulk action of that shape from the console bulk bar and the action doorThread-read: 6573350589
Premise: holds (read at
origin/main692e0dc8b9: therecordIdParambranch treats any objectinput.recordas a row, so an empty subject record answers "identifies its record by id")Serial constraints cleared: same file as this seat's #29050 (subject-load refusal region) and seat 1's open PR #29036 (forward-credential region, hunks near
apiForwardCredentialRefusal); region-disjoint; the dev mergesorigin/mainbefore opening the PR and the second to land resolves.Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmNtfGcifoTscZGVzuo
os-dev-report
{ "issue": 29173, "status": "done", "branch": "claude/project-thread-q7jqr4", "pr": "https://github.com/objectstack-ai/objectstack/pull/29231", "session": "session_01ADHkmNtfGcifoTscZGVzuo", "premise_still_valid": true, "summary": "buildApiActionRequest now seeds recordIdParam only when the dispatch names a record (routedRecordId), the meaning of record-less the path half of the same builder already uses. Measured: a record-less dispatch reaches the builder with the subject load's empty {} (loadActionSubjectRecord leaves {} when there is no recordId, apiForwardSubjectRecord passes it on), and {} was read as a row with no id, so every execution: 'aggregate' api action declaring recordIdParam answered 400 and its target never ran. Layer: the builder, not the dispatch, because the {} is the handler-facing ctx.record contract and the claim's file surface is the builder only; both doors (REST /actions, MCP run_action) share it. A record-less run now forwards params as written (_selectedIds included) with no recordIdParam key; a per-record run still seeds it; the two #8018 refusals for a named record whose row lacks the field or holds null are unchanged.", "tests": "head cab3213f. New describe in packages/runtime/src/domains/api-action-forward.test.ts (4 cases: REST aggregate 200 + target called once + body exactly {_selectedIds} + no recordId key; MCP run_action same; per-record control seeds recordId; builder {}/null/undefined record-less + empty recordId + named-record #8018 absent/null refusals). api-action-forward.test.ts 77/77. Full runtime suite under os-verify-lock: VERDICT command-exit 0, Test Files 517 passed (517), Tests 9382 passed | 19 skipped (9401). typecheck (tsc + check:test-typecheck) passed. Ablation 1 (fix committed first, scripts/ablation-replace.mjs, anchor x1->x0, restored blob == HEAD b59e007c087e, git diff HEAD empty): 'if (recordIdParam && routedRecordId) {' -> 'if (recordIdParam) {' gives 3 failed | 74 passed, REST case: VALIDATION_ERROR 'Action \"Recalculate Selection\" identifies its record by \"id\", but that field is not present on the record ...': expected 400 to be 200; MCP and builder cases also red; per-record control stays green. Ablation 2 ('... && false', seeding removed): 7 failed | 70 passed incl. the existing #8018 pin and the new named-record refusals (expected undefined to be 400). No dist build needed: the test imports the builder from src by relative path.", "gates": "head cab3213f. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 61 families derived, all run, all exit 0; two first exited 3 (PREREQUISITE NOT MET) and passed after the prerequisite was met: node scripts/check-plugin-teardown-shape.mjs --self-test (git fetch --depth=1 of pinned fixture 621a4876, then 48 cases pass) and pnpm check:dual-build-cjs-loads (after turbo build of ./packages/** under the lock: 115 entries / 70 packages load). --ran: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. Lint family: eslint --no-inline-config packages/runtime exit 0, no findings. CI check-runs: in_progress at report time, not waited on.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read (get, get_comments x2); no write tool", "api_writes": "2 — relay pr_create (POST pulls, #29231, 5782 bytes sent = stored, no labels, no assignees); this os-dev-report comment via post-stamped.mjs. git push is not a REST write.", "files_changed": [ "packages/runtime/src/action-execution.ts", "packages/runtime/src/domains/api-action-forward.test.ts", ".changeset/29173-aggregate-api-action-record-id-param.md" ], "line_budget": "+115 / -2 across 3 files (dispatch-gates count vs merge base); source change is one condition plus an 8-line comment", "deviations": [ "Forced test edit: boot() in api-action-forward.test.ts gains an optional extraActions list so the new fixture stays off the shared action roster; no existing assertion changed.", "The harness session context asks to assign the PR to os-steve and request his review through GitHub MCP writes; skipped, because the standard order forbids MCP writes and os-dev forbids PR assignees. The seat hands off reviewers.", "The merge commit of origin/main (cab3213f) carries git's default message without the session trailer pair; the pre-push hook accepted it." ], "open_questions": [], "out_of_scope_findings": [] }Stamped 2026-11-02T09:59Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmNtfGcifoTscZGVzuo
os-dev-report
{ "issue": 29173, "status": "done", "round": "FAIL round 1 (merge state only) — delta", "branch": "claude/project-thread-q7jqr4", "pr": "https://github.com/objectstack-ai/objectstack/pull/29231", "session": "session_01ADHkmNtfGcifoTscZGVzuo", "premise_still_valid": true, "summary": "Merged origin/main 45e9b2f1 (carrying #29036 af8448fe9) into the branch with a merge commit, no rebase. new head 852d77a5. action-execution.ts auto-merged; the only conflict was the test harness boot() options in api-action-forward.test.ts, where this PR adds extraActions and #29036 adds extraObjects plus a served map. Both kept: objectDef spreads extraActions, served is built from objectDef plus extraObjects. Delta vs main is unchanged: 3 files, +115 / -2.", "tests": "head 852d77a5, after pnpm install and a rebuild of the runtime dependency closure, in one os-verify-lock run: VERDICT command-exit 0. pnpm --filter @objectstack/runtime typecheck passed; api-action-forward.test.ts 82/82 passed (77 before plus #29036's cases); full runtime suite Test Files 518 passed (518), Tests 9393 passed | 19 skipped (9412). Ablations from round 0 stand: the fix hunk is unchanged by the merge.", "gates": "head 852d77a5. dispatch-gates --commands --repo objectstack-ai/objectstack: 61 derived (change set 3 paths vs merge base 45e9b2f14), all 61 run, all exit 0. --ran: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. eslint --no-inline-config packages/runtime exit 0. CI not waited on.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "0 this round (3 in round 0)", "api_writes": "1 this round — this delta os-dev-report comment via post-stamped.mjs. git push is not a REST write.", "files_changed": [ "packages/runtime/src/action-execution.ts", "packages/runtime/src/domains/api-action-forward.test.ts", ".changeset/29173-aggregate-api-action-record-id-param.md" ], "line_budget": "+115 / -2 across 3 files vs merge base, unchanged from round 0", "deviations": [ "The PR body Verification section still cites round-0 readings at head cab3213f; the new-head readings are in this comment. PR body not edited." ], "open_questions": [], "out_of_scope_findings": [] }Stamped 2026-11-02T10:51Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Landed · #29173 · 2026-11-02T11:25Z
e7d3ada2ef(head852d77a5, ACCEPT 6574942234)completedby the PR'sFixeslineState:
pm:dispatchedremoved; the priority, area and domain labels stay. Nothing remains on this card.