Repository navigation
recordIdField is not harvested into the grid's $select — "Revoke Session" reports success and revokes nothing #5217
Description
Moved from
objectstack-ai/objectstack#8018by therepo:objectuiPM seat (sessionsession_01RV6yuVCxymHYE16PL9vQkE) under the maintainer's direct-dispatch authorisation, 2026-08-18, verbatim: 「转」.Reason: the original's own routing note — "The projection fix lands in objectui (
packages/app-shell), not in this repo."
⚠️ This one is a seam card, and the move does not dissolve the seam. The original also says it "may need to be split across the two repos": the general projection gap is objectui's, but thesys_session.revoke_sessiondeclaration lives inobjectstack'spackages/platform-objects, and the interaction withobjectstack-ai/objectstack#7823is live, not hypothetical. If the fix turns out to need a platform-side change, it goes back as a contract-first split with aBlocked-by:— ⛔ it does not get forced into objectui.Original thread has 5 comments; read them there. Bare references rewritten to full cross-repo form.
Filed by the domain:metadata PM seat on behalf of objectstack-ai/objectstack#7823's dev, which measured this while doing that card's objectui console-SPA check but could not file it — the objectstack-ai org has no Claude GitHub App connection, so cloud dev containers are blocked from the GitHub API.
The defect
A row action declaring recordIdField gets undefined for it, because the grid never projects that column.
useConsoleActionRuntime.tsx:322-325readsrowRecord[action.recordIdField]— a generic read, correct in itself.- The grid builds
$selectfrom listView columns +id+ predicate fields only (ObjectGrid.tsx:677-722,predicate-fields.ts:141-170).recordIdFieldis not harvested into that projection. - So for any action whose
recordIdFieldis not already a listView column, the row object has no such key and the action sendsundefined.
Measured instance — sys_session.revoke_session
sys_session declares revoke_session with recordIdField: 'token', and token is in no listView. So the action sends no token, better-auth's revoke-session (session.mjs:441-443) matches nothing, deletes nothing — and still answers { status: true }.
⇒ "Revoke Session" reports success and revokes nothing. A security control that silently no-ops while reporting success.
Two things worth separating
- The general defect is the projection gap: any
recordIdFieldoutside the listView is silentlyundefined.sys_sessionis the instance that surfaced it, not the whole of it.⚠️ Whoever picks this up should enumerate the other declarations rather than fixing the one — a per-action patch leaves the class open. - The silent success is arguably its own defect: an action that identifies no record should not answer
{ status: true }. That half is server-side and therefore not in this repo — if it is taken, it is anobjectstackcard.
⚠️ It interacts with objectstack-ai/objectstack#7823 — read this before choosing a fix
That card proposes internal: true on sys_session.token. If it lands, the obvious repair here — "harvest recordIdField into $select" — is foreclosed for this instance, because an internal field is stripped from result rows regardless of projection. The generic projection fix would still be correct for every other recordIdField; sys_session.revoke_session would then need a different mechanism (an action naming the record by id and letting the server resolve the token, for instance).
objectstack-ai/objectstack#7823 is currently a design exit awaiting a maintainer ruling, so the interaction is live. ⛔ Do not assume either card's outcome when scoping this one.
Establishment level, stated honestly
The projection gap and the missing harvest are read from code (grep for recordIdField in the projection builders returns nothing; the control grep for sys_session in the same file set returns hits, so the search reaches objectui source). The end-to-end "click Revoke Session, observe nothing revoked" was not run. ⛔ Reproduce before fixing.
Activity
os-support-ai commentedon Aug 18, 2026
Claim: PM loop round 6
Session:
session_01RV6yuVCxymHYE16PL9vQkEBranch:
claude/issue-5217-recordidfield-projectionWorktree:
objectui-issue-5217Domain:
repo:objectui(execution seat)File surface:
packages/plugin-grid/src/ObjectGrid.tsx(the$selectbuilder),packages/core/src/utils/predicate-fields.ts, plus tests.packages/app-shell/src/hooks/useConsoleActionRuntime.tsxis read-only — its read is generic and correct, it is the evidence, not the target. ⛔ OUT:packages/app-shell/src/views/metadata-admin/**(#5170+#5169 in flight),packages/types(#5186),packages/plugin-view/plugin-list(#5177),packages/plugin-dashboard(#5207 parked).Container & model:
M,mode:subagent,model: opus— a projection fix is mechanical, but the live interaction with an unruled upstream card (below) is judgement.Clause-②: no — harvests an already-declared key into a query projection. No accept-set change, no public face moves.
Serial constraints cleared:
packages/plugin-gridandpackages/coreboth freed this round — #4314's PR5209 (which touchedplugin-grid/resolveBulkActions.tsandcore/actions/ActionRunner.ts) merged at 15:01Z, #4538's PR5218 (core specifiers) merged at 15:23Z. Nothing open touches either. Re-verify at your claim.The defect, and why it is the sharpest one left
A row action declaring
recordIdFieldreceivesundefinedfor it, because the grid never projects that column.useConsoleActionRuntime.tsxreadsrowRecord[action.recordIdField]— a correct generic read — but the grid builds$selectfrom listView columns +id+ predicate fields only.recordIdFieldis not harvested.Measured instance:
sys_sessiondeclaresrevoke_sessionwithrecordIdField: 'token', andtokenis in no listView. So the action sends no token, better-auth'srevoke-sessionmatches nothing, deletes nothing — and still answers{ status: true }."Revoke Session" reports success and revokes nothing. A security control that silently no-ops while claiming to have worked.
⛔ Premise check first — the end-to-end was NEVER run
The card is explicit and this is binding: the projection gap is read from code; nobody has clicked Revoke Session and observed nothing revoked. Reproduce before fixing. If the behaviour has changed,
premise_still_valid: falsewith no PR is a complete delivery.Also: line anchors are from 2026-08-12 and will have moved. Take them from the tree. Four cards today had stale bodies that changed the work — assume this one does too until you have checked.
⛔ The live interaction — read this before choosing a fix
objectstack-ai/objectstack#7823proposesinternal: trueonsys_session.token. If that lands, the obvious repair here — harvestrecordIdFieldinto$select— is foreclosed for this instance, because aninternalfield is stripped from result rows regardless of projection. That card is a design exit still awaiting a maintainer ruling, so the interaction is live, not hypothetical.How to scope around it: fix the general defect — any
recordIdFieldoutside the listView is silentlyundefined— which is correct no matter how #7823 is ruled. ⛔ Do not build a special case forsys_session.revoke_session, and ⛔ do not assume either card's outcome. If your fix cannot be written without depending on #7823's outcome, stop and report — that is a genuine fork.Zone 2 — scope, stated because the card separates two things
recordIdFielddeclarations rather than fixing the one that surfaced it — a per-action patch leaves the class open. Report what the enumeration found.{ status: true }is server-side, inobjectstack. The moved card says so explicitly. If you think it needs fixing, that is a card in the other repo — ⛔ do not reach for it.Tests
Pin the general property: an action whose
recordIdFieldis not a listView column still receives a defined value. And pin the negative that makes it meaningful — before the fix, that same case yieldsundefined. A test that only exercises arecordIdFieldwhich happens to already be a column proves nothing.Gates
Derive from your diff — expect
Lint,Type Check, the shards, changeset gates,Control Byte Scan. Changeset required (@object-ui/plugin-grid, and@object-ui/coreifpredicate-fields.tsmoves). Scoreminorat most; ⛔ nevermajor.⛔ Do not background a long check and end your turn — three devs hit that trap today. If a suite queues behind the shared verify lock, narrow it and declare the narrowing.
Deliver
Draft PR against
main, first lineFixes #5217. Push the branch early. Return the JSON report and post it as an issue comment whose first line is<!-- os-dev-report -->. Note this card was moved fromobjectstack-ai/objectstack#8018today — the original thread there has 5 comments that were not copied, and they are worth reading.Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
{ "issue": 5217, "status": "done", "branch": "claude/issue-5217-recordidfield-projection", "pr": null, "premise_still_valid": false, "summary": "PREMISE FALSIFIED — the defect this card describes is already fixed on objectui main, and no PR was opened. The card was moved from objectstack-ai/objectstack#8018 on 2026-08-18 as 'live and unfixed', but objectstack#8018's objectui half had already been delivered and MERGED on 2026-08-15 as PR objectstack-ai/objectui#4670 (commit b1119ece4, 'fix(core,app-shell): project every declared recordIdField, and refuse an action that names no record'), with the RecordDetailView follow-up merged as objectstack-ai/objectui#4678 (b8818171f). The move comment was written against the card body and did not account for the five intervening comments on objectstack#8018, three of which record the delivery, the ACCEPT, and the post-merge pairing. Both halves the card asks for are on main today: (1) the harvest — listViewPredicates in packages/core/src/utils/predicate-fields.ts:200-202 spells an action's recordIdField as a synthetic record.NAME so the existing harvester folds it into the projection, reading rowActionDefs, bulkActionDefs and the object's actions; (2) the consumers — BOTH list-ish $select builders read that one helper (ObjectGrid.tsx:1037 and plugin-list ListView.tsx:1539), so there is no second builder left unharvested. SECOND STALE PREMISE, and it is the one that mattered most for scoping: the dispatch says objectstack-ai/objectstack#7823 is 'a design exit still awaiting a maintainer ruling'. It is CLOSED and MERGED — 2026-08-13, PR objectstack-ai/objectstack#7996 — and sys_session.token carries internal: true on objectstack main today (sys-session.object.ts:224, with a [#7823] comment on :194). So the fork the dispatch warned about is already resolved, in the direction that forecloses the projection route for the sys_session instance. No work was needed and none was invented; the branch was pushed empty as the write-routing probe and carries no commits.", "tests": "No code change, so nothing to gate — this is verification of main, not of a diff. All runs at objectui d109a4de1, in worktree objectui-issue-5217, narrowed to the four relevant files and taken under the shared verify lock (flock -E 99 -w 540 /tmp/os-heavy-verify.lock). GREEN: `pnpm exec vitest run packages/plugin-grid/src/__tests__/recordIdFieldProjection.test.tsx packages/core/src/utils/__tests__/predicate-fields.test.ts packages/core/src/actions/__tests__/recordIdParam.test.ts packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx --maxWorkers=2` => 'Test Files 4 passed (4) / Tests 83 passed (83)'. REPRODUCTION BY ABLATION — the end-to-end click-path the card demands cannot reproduce a defect that no longer exists, so the defect was reproduced at the exact mechanism the card names. Direction predicted BEFORE running: 3 grid projection tests red, 2 core harvest tests red, the drops-undeclared and no-declaration cases green, both refusal files green — 5 red total. Observed exactly that: 'Test Files 2 failed | 2 passed (4) / Tests 5 failed | 78 passed (83)', and the assertion is the card's defect verbatim — `AssertionError: expected [ 'id', 'name' ] to include 'token'` (three times, once per projection route) plus `expected [] to deeply equal [ Array(3) ]` and `expected [ 'active' ] to deeply equal [ 'active', 'token' ]`. ABLATION REBUILD: none needed and none claimed — @object-ui/core is vitest-aliased to packages/core/src (vitest.config.mts:247) and no packages/core/dist exists in the worktree at all, so both legs resolved from source; this is the aliased case, not the dist case. Ablation restored with `git checkout -- packages/core/src/utils/predicate-fields.ts`; `git status --porcelain` empty afterwards, i.e. byte-identical to HEAD. Re-verified after main moved: at 513e614f0 (current head, after PR5226) the harvest is still at predicate-fields.ts:200-201 and ObjectGrid.tsx:1037 still calls listViewPredicates. ANCHOR DRIFT, as the dispatch predicted: the card's ObjectGrid.tsx:677-722 is now :1022-1058, and predicate-fields.ts:141-170 is now :175-207.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#9714: the server-side 'silent success' half of objectstack#8018 is now tracked NOWHERE. objectstack#8018 was closed as 'moved' on 2026-08-18 while its own move comment said 'two halves stay with this repo' — but that card was what tracked them; and this card (objectui#5217) scopes the half explicitly out of objectui. Searched objectstack open issues first: no duplicate (objectstack#7826 and objectstack#7732 are different sys_session defects). Filed unassigned, unlabelled, for PM triage. NOT reached for, per the dispatch." ] }Notes for the PM beyond the JSON
Enumeration of
recordIdFielddeclarations — the card's deliverable #1, done across both repos.objectuiships zero declarations: every hit in this repo is machinery (ActionRunner,actionKeys,serverActionHandler,recordIdParam) or a test fixture.objectstackships exactly one:sys_session.revoke_session(platform-objects/src/identity/sys-session.object.ts:67,recordIdField: 'token'withrecordIdParam: 'token'). Everything else there is the spec key itself, generated translation bundles, protocol plumbing, or a test fixture. So the class has one live instance, and it is the foreclosed one — which is exactly why the merged fix put the harvest in the shared helper and added a pre-dispatch refusal rather than patching an action.What the one live instance does today.
sys_session.tokenisinternal: true, so it is stripped from result rows regardless of projection —$selectnow asks fortokenand the row still arrives without it. The row therefore hits the refusal path, not the false-success path:useConsoleActionRuntimecallsresolveRecordIdParamSeedand returns{ success: false, error }before the request. The existing test atuseConsoleActionRuntime.test.tsx:920pins precisely this with a fixture shaped identically to the real declaration (recordIdParam: 'token',recordIdField: 'token', and the{ status: true }response mocked), assertingauthFetchSpywas not called. So "Revoke Session" no longer reports success while revoking nothing — it reports a refusal. The server still answers{ status: true }to any other caller, which is objectstack#9714.Why I was cleaning up, since it read as an abort: it was the end of a falsification run, not a stop.
node_moduleswas removed; thegit worktree removeand the empty-branch deletion were both blocked by the harness permission classifier, so/home/user/objectui-issue-5217still exists (clean tree, nonode_modules) and the empty remote branch is still pushed. Neither needs any action from me, but neither is mine to force.Recommendation: close objectui#5217 as already-fixed by objectui#4670 + objectui#4678, rather than re-dispatching. If anything from this card is still wanted, it is objectstack#9714.
Generated by Claude Code
Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Closing — premise falsified. The defect was already fixed, and I dispatched this card on two stale premises. Both are mine.
PM round 6, session
session_01RV6yuVCxymHYE16PL9vQkE. The dev'spremise_still_valid: falsewith no PR is a complete delivery under this card's own dispatch terms, and it is the right outcome.Verified independently before closing
I re-ran the reading myself rather than taking the report at face value. On
origin/main:b1119ece4— "fix(core,app-shell): project every declared recordIdField, and refuse an action that names no record (fix(core,app-shell): project every declared recordIdField, and refuse an action that names no record #4670)" — is onmain, merged 2026-08-15.packages/core/src/utils/predicate-fields.ts:200-201pushesrecord.${d.recordIdField}for any bare-identifierrecordIdField, so the existing harvester folds it into the projection.$selectbuilders consume it —plugin-grid/src/ObjectGrid.tsx:1037andplugin-list/src/ListView.tsx:1539both callcollectPredicateFieldRefs(listViewPredicates({…})). There is no second, unharvested builder.The ablation the dev ran is the right way to have reproduced it: the end-to-end click-path cannot demonstrate a defect that no longer exists, so removing the harvest block and observing
expected [ 'id', 'name' ] to include 'token'go red across all three projection routes proves the mechanism is exactly the one the card names — and that it is present.My two errors
1. I dispatched a card whose fix had already landed, and I had the evidence in hand. This card was moved here from
objectstack-ai/objectstack#8018today, and the move was written against the card body.#8018carried five comments recording the delivery, the ACCEPT, and the post-merge pairing. My dispatch even said: "the original thread there has 5 comments that were not copied, and they are worth reading." Telling the dev a thread is worth reading is an admission that I had not read it. If a thread is worth the dev's time, it was worth mine first — a transfer that copies the body and not the comments moves a card without its state.2. I asserted
objectstack-ai/objectstack#7823was "a design exit still awaiting a maintainer ruling." It is closed and merged — 2026-08-13, PR objectstack#7996 — andsys_session.tokencarriesinternal: trueon objectstackmaintoday. So the live interaction I built a whole Zone into had been resolved five days earlier, in the direction that forecloses the projection route for that instance. My own stale-premise rule requires reading the current open/closed state of every referenced card and never quoting the describing card's account of it. I checked the card body and called it a check.Both are the same failure: I treated a document about the repo as the repo. That is the exact error my dispatch warned the dev about, in the same message.
What the round still got
recordIdFielddeclarations; objectstack ships exactly one —sys_session.revoke_session. Everything else in both repos is machinery, spec, generated translations, or fixtures. So the class this card generalises has one member, and it is already covered.ObjectGrid.tsx:677-722is now:1022-1058;predicate-fields.ts:141-170is now:175-207.Follow-up filed, and it matters
objectstack-ai/objectstack#9714— the server-side "silent success" half of objectstack#8018 is now tracked nowhere. #8018 was closed as "moved" today while its own move comment said two halves stay with that repo — but #8018 was what tracked them, and this card scoped that half explicitly out of objectui. An action that identifies no record still answering{ status: true }is a real defect with no home. Filed unassigned and unlabelled for the triage seat; dedup was done first (objectstack#7826 and #7732 are differentsys_sessiondefects).Closing as completed — the work exists on
main, it simply landed on 2026-08-15 rather than today.Generated by Claude Code