Skip to content

fix(runtime): forward a record-less aggregate api action that declares recordIdParam - #29231

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-q7jqr4
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-q7jqr4

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29173

Clause-②: no
Metadata-protocol: none

维护者速读

What changes

  • buildApiActionRequest (packages/runtime/src/action-execution.ts): the recordIdParam block now runs only when the dispatch names a record (routedRecordId, the same value the path half above it already uses to decide "this dispatch names no record"). A record-less dispatch arrives with the subject load's empty {} as its record; that {} was read as a row, had no id, and the request was refused 400 VALIDATION_ERROR ("identifies its record by "id", but that field is not present on the record"), so every execution: 'aggregate' api action declaring recordIdParam failed for the whole selection and its target was never called.
  • Layer chosen: the builder, not the dispatch. The subject load's {} is the handler-facing ctx.record contract for record-less and new-record actions (its own docblock says so), and the builder already defines record-less by routedRecordId for the path tokens. Fixing it there gives the request builder one meaning of record-less for both halves, and both doors (REST /actions and MCP run_action) share it. It also stays inside the claim's file surface.
  • The two recordIdField is not harvested into the grid's $select — sys_session "Revoke Session" reports success and revokes nothing #8018 refusals are unchanged for a dispatch that names a record: a row lacking the recordIdField, and a row holding null for it.
  • Changeset: patch for @objectstack/runtime.

Tests

One new describe in packages/runtime/src/domains/api-action-forward.test.ts, with a fixture in the showcase's "Recalculate Selection" shape (type: 'api', execution: 'aggregate', recordIdParam: 'recordId'):

  • REST door: { params: { _selectedIds } } with no recordId answers 200, the target is called once at /api/v1/apps/showcase/recalc, the forwarded body is exactly { _selectedIds }, and it has no recordId key.
  • MCP run_action: the same record-less run is forwarded with the same body and no invented recordId.
  • Per-record control: the same action run on one record still seeds recordId from that row.
  • Builder: {}, null and undefined records with no recordId build the plain request; an empty recordId is no id; a named record still gets both recordIdField is not harvested into the grid's $select — sys_session "Revoke Session" reports success and revokes nothing #8018 refusals (status 400, no path built).

Forced test edit: the shared boot() harness gains an optional extraActions list so the new fixture does not join the shared action roster. No existing assertion changed.

Ablations (fix committed first; restored by ablation-replace, blob == HEAD, git diff HEAD empty)
  1. Put the defect back (if (recordIdParam && routedRecordId) { → if (recordIdParam) {): 3 failed / 74 passed. The REST case fails with {"success":false,"error":{"code":"VALIDATION_ERROR","message":"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.","httpStatus":400}}: expected 400 to be 200. The MCP case and the builder case also fail. The per-record control stays green, as expected.
  2. Take the seeding away entirely (… && false): 7 failed / 70 passed, including the existing recordIdField is not harvested into the grid's $select — sys_session "Revoke Session" reports success and revokes nothing #8018 pin and the new named-record refusals (expected undefined to be 400), so those refusal pins are live.

Acceptance notes

  • The console is unchanged, as the card asked. Its own resolveRecordIdParamSeed already skips seeding when it has no row record, which is the behaviour this restores on the server side.

Verification

Local readings at head cab3213 (origin/main 0d15f89 merged in)
  • pnpm --filter @objectstack/runtime typecheck (tsc plus the test typecheck): passed. Full runtime suite under os-verify-lock: VERDICT command-exit 0, Test Files 517 passed (517), Tests 9382 passed and 19 skipped (9401).
  • pnpm --filter @objectstack/runtime exec vitest run src/domains/api-action-forward.test.ts: 77/77 passed.
  • eslint --no-inline-config packages/runtime: exit 0, no findings.
  • node scripts/pm/dispatch-gates.mjs --commands: 61 derived families, all run, all exit 0. Two first answered exit 3 (prerequisite not met) and passed once it was met: the teardown-shape self-test after fetching its pinned fixture commit, and check:dual-build-cjs-loads after a full packages build. --ran reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN.

Generated by Claude Code

…s recordIdParam

A record-less dispatch (no recordId) reaches buildApiActionRequest with the
subject load's empty {} record. The recordIdParam branch read that {} as a row,
found no id and refused 400, so every execution: 'aggregate' api action that
declares recordIdParam failed for the whole bulk selection and its target never
ran. The branch now seeds recordIdParam only when the dispatch names a record,
the same meaning of record-less the path half already uses; the #8018 refusals
for a named record whose row lacks the field or holds null are unchanged.

Claude-Session: https://claude.ai/code/session_01ADHkmNtfGcifoTscZGVzuo
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

维护者速读

席位意见:与 diff 相符。修在构建器而非调度,理由成立:空 {} 是 handler 的 ctx.record 约定,构建器的路径半段本就以 routedRecordId 定义「无记录」,两半现在一个含义。控制台未改。

席位核对

ACCEPT · domain:server seat 5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 · 2026-11-02T10:00Z · head cab3213f · #29173

  1. Shape: draft against main, first line Fixes #29173, no other card number beside a closing keyword; patch changeset for @objectstack/runtime; Clause-②: no, Metadata-protocol: none. Not governed, +115 / -2, 3 files (one source condition, one test file, the changeset).
  2. Against the order: the recordIdParam block now runs only when routedRecordId is set (if (recordIdParam && routedRecordId)), the meaning of record-less the path half already uses; the routed-id-without-row leg and both recordIdField is not harvested into the grid's $select — sys_session "Revoke Session" reports success and revokes nothing #8018 refusals for a named record are untouched. Pins cover the REST door (200, target called once, body exactly { _selectedIds }, no invented recordId), the MCP door, a per-record control and the builder's record-less and named-record cases. Read in the diff.
  3. Accepted deviations: the shared boot() harness gains an optional extraActions list so the new fixture stays off the shared roster; no existing assertion changed.
  4. Gates: dev reports 61/61 derived gates exit 0 on cab3213f (0 NOT-MEASURED, 0 UNRUN), runtime full suite 9382 passed, typecheck and scoped eslint exit 0; two ablations (defect restored → 3 red; seeding removed → 7 red incl. the recordIdField is not harvested into the grid's $select — sys_session "Revoke Session" reports success and revokes nothing #8018 pins), each restored. Check-runs on this head were still running at ACCEPT; the seat readies it only after Lint & Repo Gates and TypeScript Type Check read success and no check is red.
  5. Out of scope: none found.
  6. Landing: pr_ready + automerge_enable through the relay once CI is green. Same file as this seat's fix(runtime): every door that starts a flow by name refuses a record key the caller cannot read before the run exists #29227 (dispatchFlowAction / start admission region) and seat 1's fix(runtime): list_actions marks revoke_api_key unavailable to an API-key caller, with the API-key guard's own refusal (#28943) #29036 (forward-credential region), different regions; whichever lands second resolves.

…-q7jqr4

# Conflicts:
#	packages/runtime/src/domains/api-action-forward.test.ts
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 45e9b2f14c91f76683296cd33bb1b477cbaef1bb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 882ff8280c3fe40775618e4cdb5b39df7820aded — the merge of head 852d77a5b2c33bad5181f3373925efd18594e3a7 into base 45e9b2f14c91f76683296cd33bb1b477cbaef1bb, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 882ff8280c3fe40775618e4cdb5b39df7820aded && git checkout 882ff8280c3fe40775618e4cdb5b39df7820aded
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 45e9b2f14c91f76683296cd33bb1b477cbaef1bb 852d77a5b2c33bad5181f3373925efd18594e3a7 && git checkout -B drift-repro 45e9b2f14c91f76683296cd33bb1b477cbaef1bb && git merge --no-ff 852d77a5b2c33bad5181f3373925efd18594e3a7

node scripts/docs-audit/affected-docs.mjs --json 45e9b2f14c91f76683296cd33bb1b477cbaef1bb

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

维护者速读

席位意见:合并提交未改 PR 自身 diff(3 个文件 +115/-2,与 cab3213f 相同);新 head 的 CI 全绿。

席位核对

ACCEPT · domain:server seat 5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 · 2026-11-02T10:31Z · head 852d77a5 · #29173 (supersedes 6574289977 at cab3213f)

  1. Shape: unchanged: +115 / -2 over the same 3 files against the new merge base; 852d77a5 is a merge of origin/main only.
  2. Against the order: the PR's own diff against its merge base reads byte-identical to the one accepted at cab3213f; read by the seat.
  3. Accepted deviations: as 6574289977.
  4. Gates: check-runs on 852d77a5: 35 success, 13 skipped, none red or running (Lint & Repo Gates and TypeScript Type Check success). The dev's local readings on cab3213f stand; its delta report for the merge head follows on the card.
  5. Out of scope: none.
  6. Landing: pr_ready + automerge_enable through the relay in this pass.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 10:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit e7d3ada Nov 2, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-q7jqr4 branch November 2, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant