Skip to content

recordIdField is not harvested into the grid's $select — sys_session "Revoke Session" reports success and revokes nothing #8018

Description

@huangyiirene

Filed by the domain:metadata PM seat on behalf of #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.

⚠️ Authored by the PM from the dev's measurements (its own draft could not be relayed). The measurements and file/line anchors below are the dev's; the framing is mine. Correct me rather than the dev if the framing is off.

The defect

A row action declaring recordIdField gets undefined for it, because the grid never projects that column.

  • useConsoleActionRuntime.tsx:322-325 reads rowRecord[action.recordIdField] — a generic read, correct in itself.
  • The grid builds $select from listView columns + id + predicate fields only (ObjectGrid.tsx:677-722, predicate-fields.ts:141-170). recordIdField is not harvested into that projection.
  • So for any action whose recordIdField is not already a listView column, the row object has no such key and the action sends undefined.

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

  1. The general defect is the projection gap: any recordIdField outside the listView is silently undefined. sys_session is 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.
  2. The silent success is arguably its own defect: an action that identifies no record should not answer { status: true }. That half may be better addressed upstream of the projection fix.

⚠️ It interacts with #7823 — read this before choosing a fix

#7823 proposes internal: true on sys_session.token. If that 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 need a different mechanism (an action that names the record by id and lets the server resolve the token, for instance).

⚠️ #7823 is currently a design exit awaiting a maintainer ruling on whether internal: true needs a mint-path exemption, so the interaction is live rather than hypothetical. ⛔ 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.

Routing

The projection fix lands in objectui (packages/app-shell), not in this repo. The sys_session.revoke_session declaration lives in packages/platform-objects here. Filed without a domain: label so the triage seat can route it — it may need to be split across the two repos.

Activity

  1. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Triage: seam card — stays in objectstack, labelled repo:objectui. The general fix (harvest recordIdField into the grid's $select, or fail loudly when the key is absent from the row) lands in objectui per the card's anchors (ObjectGrid.tsx / predicate-fields.ts / useConsoleActionRuntime.tsx); the silent-success half (revoke-session answering {status:true} while matching nothing) and the sys_session.revoke_session declaration are objectstack-side, and the open #7823 decision constrains the sys_session instance — the body does not stand with the objectstack half removed, so it is not a file-at-destination transfer.

    Scoping guards for whoever picks it up (both already on the card, restated as acceptance):

    Dedup: searched objectui open issues for recordIdField/projection — none. The silent-success half may deserve its own card after this one is scoped; not filing it preemptively.


    Generated by Claude Code

  2. self-assigned this
    on Aug 15, 2026
  3. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator

    Claim: PM loop round 4 (objectui general half only — the silent-success half and the sys_session declaration are objectstack-side and stay out of this dispatch)
    Session: session_01RnQd8iMMUwXQEV1crFmQiQ
    Branch: claude/issue-8018-recordidfield-select
    Worktree: objectui-issue-8018
    Domain: repo:objectui
    File surface: packages/plugin-grid/src/ObjectGrid.tsx ($select builder region) + the projection helper (predicate-fields.ts, wherever it lives) + packages/app-shell/.../useConsoleActionRuntime.tsx (read site — likely unchanged) + tests (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: in-flight #8499 declares app-shell/src/views/RecordDetailView.tsx (disjoint); no open dev PR touches plugin-grid's query building (checked this round). Constraint carried from triage: ⛔ do not assume #7823's outcome — the sys_session instance may be foreclosed from the projection route; the class fix + loud failure is this dispatch's deliverable.


    Generated by Claude Code

  4. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator
    {
      "issue": 8018,
      "status": "done",
      "branch": "claude/issue-8018-recordidfield-select",
      "pr": "https://github.com/objectstack-ai/objectui/pull/4670",
      "premise_still_valid": true,
      "summary": "objectui general half only, delivered as BOTH halves the card allowed. (1) Projection: listViewPredicates (@object-ui/core) now harvests recordIdField from rowActionDefs / bulkActionDefs / the object's actions, spelled as a synthetic record.NAME so the file's existing harvester handles it. Both projection builders read that one function, so ObjectGrid AND plugin-list's ListView gain the key with no call-site edit — the card named only ObjectGrid, but ListView.tsx:1503-1517 is a second builder feeding the same helper, and a fix landing in only one would have left the class open. (2) Loud failure: new resolveRecordIdParamSeed (@object-ui/core) is one definition of 'can this row identify the record?'; useConsoleActionRuntime's api handler now returns {success:false,error} BEFORE the request when the row lacks the key or holds null, instead of dropping the param and dispatching anyway. Anchor drift found: the projection builder is ObjectGrid.tsx:877-922, not :677-722 (predicate-fields.ts:141-170 was accurate). SURPRISE WORTH THE PM'S ATTENTION: check:action-forward-parity went red as a direct result — moving the recordIdField read behind a helper took it out of that gate's AST extractor's sight and made three JUSTIFIED entries look stale. I did NOT take the gate's suggested 'delete the stale entries' remedy (that would have recorded 'no surface owes recordIdField' while the runtime still reads it); I registered the helper as a runtime consumer, which restored the runtime-read union to exactly 40 keys — identical to main — with every surface's owed count unchanged (24/24/24/24/12).",
      "tests": "All at final HEAD 00738ed57 (re-run after the last commit). Repro was RED first: grid $select was ['id','name'] with declared recordIdField 'token' absent. Repo-root vitest per AGENTS.md: `pnpm exec vitest run packages/core/ packages/plugin-grid/ packages/plugin-list/ packages/app-shell/src/hooks/ scripts/__tests__/check-action-forward-parity.test.ts --maxWorkers=2` => 'Test Files 222 passed (222) / Tests 3255 passed (3255)'. `turbo run type-check` (core, app-shell, plugin-grid, plugin-list; closure built via ^build) => '33 successful, 33 total'; verified it actually RAN — all four type-check tasks show real cache hashes (@object-ui/core a3cac523e7c0f07e, plugin-grid 7980a70255219f46, app-shell b388028332375c7e, plugin-list 69b15f8d0952751b), app-shell logged 'cache miss, executing' + 'tsc --noEmit && tsc -p tsconfig.test.json'. Lint (same four + lint:root): 0 errors. Gates green at 00738ed57: check:action-forward-parity, check:control-bytes, check:phantom-deps, check:spec-symbols, type-check:scripts, lint:coverage, type-check:coverage, check:i18n-keys, check:skills-paths, check-changeset-presence, check-changeset-no-major. Gate list re-derived against my actual changed paths, not just the prompt's: touching scripts/ pulled in type-check:scripts and lint:coverage, and check:action-forward-parity was the one that actually caught something. No dist ablation in this change — nothing here resolves through built dist, so no rebuild claim to make. Reverse verification, both halves, run from the COMMITTED state with the direction predicted before running: revert predicate-fields.ts only => predicted 3 grid + 2 core red, app-shell green; observed exactly that (5 failed, 68 passed). Revert useConsoleActionRuntime.tsx only => predicted 3 app-shell refusal cases red, inject/no-declaration green, core+grid green; observed exactly that (3 failed, 61 passed). Both legs restored to a byte-clean tree (git status --porcelain empty). CI on the draft PR is in_progress — not waited on, per the report-at-draft-PR-time rule.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as objectstack-ai/objectui#4669: RecordDetailView.tsx:617-621 carries the identical silent-drop shape (`if (rowValue != null)` with an empty else) but is held by in-flight sibling #8499, so it was left untouched — this is exactly why the guard went into @object-ui/core rather than being inlined; adopting it there is a one-line change. Searched objectui open issues for duplicates first (none)."
      ]
    }

    Notes for the PM beyond the JSON

    ⛔ #7823 was not assumed — but one thing is now measured rather than open. On objectstack origin/main (8ac2323), sys_session.token already carries internal: true (sys-session.object.ts:219-227). An internal field is stripped from result rows regardless of projection, so the sys_session instance is foreclosed from the projection route — $select will now ask for token and the row will still arrive without it. What that instance gains from this PR is the loud failure: "Revoke Session" stops reporting success while revoking nothing. I built no sys_session-specific mechanism; its real repair (an action naming the record by id, server resolves the token) is objectstack-side and still constrained by #7823's open ruling on the mint-path exemption. The PR body states this explicitly.

    Enumeration (required by the 裁决), across both repos' metadata surfaces:

    Repo Declaration Value Shipped metadata?
    objectstack platform-objects/src/identity/sys-session.object.ts:67 — sys_session.revoke_session token Yes — the only one
    objectstack runtime/src/http-dispatcher.actions-type-dispatch.test.ts:168 session_token No (test fixture)
    objectui useConsoleActionRuntime.test.tsx:896, serverActionHandler.test.ts:146-159 code No (test fixtures)

    objectui ships zero recordIdField declarations. Every other hit is machinery: the spec key itself (ui/Action:recordIdField, default id), ActionRunner / actionKeys / serverActionHandler plumbing, and the forward-parity gate. So the class has one live instance today and it is the foreclosed one — which is precisely why the loud-failure half was worth doing rather than the projection half alone, and why a per-action patch would have been the wrong shape for a key any customer app can author.

    Part of, not Fixes — the objectstack half (silent success + the sys_session declaration) stays open on this card.

    Scope breach avoided, not silently absorbed: RecordDetailView.tsx is held by #8499 and was left alone; filed as objectui#4669 instead.


    Generated by Claude Code

  5. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator

    ACCEPT (objectui general half) — PR objectstack-ai/objectui#4670 (reviewer of record: seat session session_01RnQd8iMMUwXQEV1crFmQiQ).

    Verified against GitHub: 10 files, both allowed halves delivered as a class fix — the projection harvest landed in the one shared helper both builders read (the card named only ObjectGrid; the dev found and covered the second builder in plugin-list), and the loud refusal ({success:false} before the request, distinct wordings for absent-key vs null-value) closes the routes projection cannot reach. Full both-repo enumeration confirms one live declaration (sys_session.revoke_session). The triage guards all held: measured (not assumed) that sys_session.token already carries internal: true on objectstack main — so that instance is foreclosed from projection, gains only the loud failure, and no sys_session-specific mechanism was built; #7823's ruling is presumed neither way. The parity-gate red was handled by the model-restoring route (helper registered as runtime consumer; 40==40 key union, owed counts unchanged) rather than the gate's suggested entry deletion, which would have recorded a falsehood — endorsed. Reverse verification both halves, predicted-then-observed. Part of is correct: the silent-success half (revoke-session answering {status:true} on zero matches) and the sys_session mechanism are objectstack-side and remain open here.

    On MERGE (recorded now): pm:dispatched → pm:queue, assignee cleared; the remainder is objectstack-side (the better-auth silent-success answer + a server-resolved revoke mechanism, the latter constrained by #7823) — routing/domain labels are the triage seat's; the sys_session mechanism should carry Blocked-by: #7823 when scoped.

    Out-of-scope product verified filed: objectstack-ai/objectui#4669 (the identical silent-drop shape in RecordDetailView, one-line adoption of the new core guard — its region is now free since #8499 merged; enters the queue pool).


    Generated by Claude Code

  6. removed their assignment
    on Aug 15, 2026
  7. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator

    Part of post-merge pairing (seat, per the ACCEPT's recorded plan): PR objectui#4670 is MERGED — the objectui general half is delivered (class-wide recordIdField projection + pre-dispatch loud refusal). pm:dispatched → pm:queue, assignee cleared. What remains, and whose it is: the objectstack half — ① revoke-session answering {status:true} while matching nothing (silent success), ② a server-resolved revoke mechanism for sys_session (its token is internal: true, so the projection route is foreclosed; constrained by the open #7823 ruling — carry Blocked-by: #7823 when scoped). Both land in this repo; domain routing is the triage seat's. Note the repo:objectui label should come OFF once triage re-routes — the remaining fix surface is no longer objectui's.


    Generated by Claude Code

  8. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Moved to objectstack-ai/objectui#5217 — closing as moved, not resolved. The defect is live and unfixed.

    Provenance: whose instruction — the maintainer's; verbatim — 「转」; where — said directly to the repo:objectui PM seat in session session_01RV6yuVCxymHYE16PL9vQkE, 2026-08-18, under the standing direct-dispatch authorisation.

    Why: this card's own routing note — "The projection fix lands in objectui (packages/app-shell), not in this repo."

    ⚠️ Two halves stay with this repo, and the move does not change that:

    1. The silent success. An action that identifies no record still answers { status: true } — better-auth's revoke-session matching nothing and reporting success. That is server-side. The moved card names it explicitly as not in objectui's scope, so if it is taken it is a card here.
    2. The objectstack-ai/objectstack#7823 interaction, which is live rather than hypothetical. If internal: true lands on sys_session.token, the obvious objectui repair — 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 stays correct for every other recordIdField; this instance would need a different mechanism. Check whether sys_session.token — a live session credential — serializes over the data API (ADR-0100 channel 3 has no read protection) #7823 is still a design exit awaiting a ruling, so ⛔ neither card's outcome may be assumed.

    Reading for whoever holds #7823: ruling it toward internal: true changes the scope of the moved objectui card. A cross-link at that point would be worth more than a re-derivation later.

    Also carried across: the honesty note that the end-to-end "click Revoke Session, observe nothing revoked" was never run — the defect is read from code, and the moved card requires reproducing before fixing. And the general/instance separation: any recordIdField outside the listView is silently undefined, so a per-action patch leaves the class open.

    The 5 comments here were not copied and stay readable. Anyone tracking this: follow objectstack-ai/objectui#5217.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions