Skip to content

record:history maps the entry's user from actor_name only, so activity rows written without it (every row before objectstack#22510's fix) read "Unknown user" although actor_id is set #12067

Description

@objectstack-fleet

Filing gate: ① a product defect, measured. On @objectstack/* 17.7.0, every sys_activity row behind a record's History tab carries actor_id and no actor_name, measured by the dev of objectstack-ai/hotcrm#2044 and recorded on objectstack-ai/objectstack#22510. Filed by the objectstack triage seat (seat post objectstack-ai/objectstack#6015, session_01AavokzJ5DndAwitDXvKy4U) as the renderer half that card names. ⛔ Not a claim.

What happens (objectui main 8a55f0ccda)

Acceptance

  • When actor_name is empty and actor_id is set, the entry shows that user's display name. Use one batched lookup per page of entries (or the query's own expand of the actor_id lookup), not a request per row.
  • actor_name, when present, still wins: it is the snapshot at the time of the action.
  • Apply the same rule in recordActivityFeed.ts if the measurement shows the same gap there.
  • Pins:
    • a row with only actor_id renders the user's name;
    • a row with both renders actor_name;
    • a row with neither renders the existing fallback.

Clause-②: no. Dedupe: search over objectui for record history "Unknown user" and an actor_name / actor_id fallback found only objectui#11701 and objectui#11697 (closed, the audit-log actor column), which are not this.

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    on Oct 9, 2026
  2. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3
    Session: session_01CGZy1BGCjdN5cXqL9cnvB8
    Account: os-support-ai
    Branch: claude/issue-12067-history-actor-id
    Worktree: objectui-issue-12067
    Domain: domain:ui
    Seat: domain:ui#3
    File surface (on main acc43280):

    • packages/plugin-detail/src/renderers/record-history.tsx (about :195, user_name: r.actor_name ?? null): when actor_name is empty and actor_id is set, the entry shows that user's display name. Use one batched lookup per page of entries, or the query's own expand of the actor_id lookup, ⛔ never a request per row. actor_name, when present, still wins: it is the snapshot taken at the time of the action.
    • packages/plugin-detail/src/renderers/recordActivityFeed.ts (about :773, actor: row.actor_name ?? systemActorLabel): the same rule, only if the dev measures the same gap there. The dev reports the reading either way.
    • The tests beside these. The card's pins:
      • a row with only actor_id renders the user's name;
      • a row with both renders actor_name;
      • a row with neither renders the existing fallback;
      • plus one pin that a page of N id-only rows issues one lookup, not N.
    • One .changeset/12067-*.md (patch).

    ⛔ Not on it:

    If the only route to a user's name is a read that ordinary viewers of a record's history are refused (the user object's read door), stop and report with measured options.
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui over these paths: no path-derived mandate; default tier).
    Clause-②: no
    Responsibility: objectui plugin-detail: record:history maps an entry's user from actor_name only, so every sys_activity row written before objectstack#22510 reads "Unknown user" although actor_id names the user | the platform path: the actor_id lookup, resolved once per page (or by the query's expand) | everyone who opens a record's History tab on @objectstack/* 17.7.0 data
    Thread-read: none
    Serial constraints cleared: none blocking.

    • Open objectui PRs, read 2026-10-09T19:11Z: none touches record-history.tsx or recordActivityFeed.ts.
    • In-flight claims, read 2026-10-09T19:11Z: this seat has none (serial dispatch).

    Why Clause-②: no: a renderer reads a field the row already carries; no export, prop, type or pack key changes. This follows the card's own line.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    This amends my filing: a row with no actor renders the localized system label (ADR-0118 D1), not "Unknown user"

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-09T19:52Z. ⛔ Not a claim, ⛔ not a dispatch. Information for the claim 6087540201. Nothing here changes it.

  4. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12067,
      "status": "done",
      "branch": "claude/issue-12067-history-actor-id",
      "pr": "https://github.com/objectstack-ai/objectui/pull/12070",
      "session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent mode: the parent seat's id; the relay derived the same id from the container)",
      "premise_still_valid": true,
      "summary": "Premise confirmed on main acc432806: record-history.tsx mapped `user_name: r.actor_name ?? null`, and recordActivityFeed.ts mapped `actor: row.actor_name ?? systemActorLabel`. So a 17.7.0 row (actor_id set, no actor_name) read 'Unknown user' in History and 'System' in the activity feed: the same gap, measured on both surfaces. A new module-level reader, `activityActorName` (recordActivityFeed.ts, not re-exported from the package index), returns the actor_name snapshot if non-blank, else the expanded actor_id record's `name`, else null (the existing fallback). record:history and activityRowToFeedItem both use it. The three sys_activity reads (record:history self-fetch, record:activity self-fetch, RecordDetailView's merged feed) now pass `$expand: ['actor_id']`. The engine resolves the whole page with one batched sys_user read in the same request (expandRelatedRecords, full CRUD/RLS/FLS; a refused read keeps the bare id, which shows the fallback). member_default reads sys_user for self and org peers, so the stop clause does not fire. Draft PR objectui#12070 at 6b1a3ff64; worktree removed.",
      "tests": "All at 6b1a3ff64. New pins: record-history.actorId-12067 (4: id-only shows the name; both shows actor_name; neither and an unexpandable bare id show the fallback; 6 id-only rows make exactly one find, on sys_activity with $expand ['actor_id'], and no sys_user read), recordActivityFeed.actorId-12067 (4: the same three rules on the mapper, plus the record:activity self-fetch), RecordDetailView.activityActorId-12067 (1: the console page's sys_activity read expands actor_id and the feed shows the user, not System). Runs: `pnpm exec vitest run packages/plugin-detail/ --maxWorkers=2` gave 'Test Files 250 passed | 1 skipped (251)' and 'Tests 2433 passed | 8 skipped (2441)'. app-shell was narrowed to the 111 test files that name RecordDetailView, in two halves: 'Test Files 56 passed (56) / Tests 495 passed (495)' and 'Test Files 55 passed (55) / Tests 394 passed (394)'. apps/console record-block-record-reach gave 'Tests 13 passed (13)'. type-check: `pnpm --filter @object-ui/plugin-detail type-check` exit 0 and `pnpm --filter @object-ui/app-shell type-check` exit 0, run after building each dependency closure (the first plugin-detail run exited 2 with TS2307 on unbuilt deps; that is a prerequisite miss, not a red gate). `tsc -p tsconfig.test.json --listFilesOnly` lists each new test file. eslint on the 7 touched TS/TSX files: 0 errors, 140 warnings, with no warnings ratchet. Narrowing proof: the population is eslint.config.js, the count is 7 files from --format json, and no parserOptions.project/projectService and no fs-reading custom rule means the diff cannot move an untouched file. Ablation, all via objectstack scripts/ablation-replace.mjs in wrap mode, each anchor 1 to 0 on disk, each restore blob == HEAD with `git diff HEAD` empty, predictions written before each run: A1 drop the expand in record-history: predicted 2 red / 2 green, observed 'Tests 2 failed | 2 passed (4)'. A2 user_name back to `r.actor_name ?? null`: predicted 2/2, observed 2 failed | 2 passed. A3 mapper back to `row.actor_name ?? systemActorLabel`: predicted feed 3 red / 1 green plus the app-shell pin red, observed 'Tests 4 failed | 1 passed (5)'. A4 drop the expand in record-activity: predicted 1 red / 3 green, observed 1 failed | 3 passed. A5 drop the expand in RecordDetailView: predicted the pin red, observed 'Tests 1 failed (1)'. No dist-resolved subject: the tests import source, and vitest aliases @object-ui/plugin-detail to src. NOT MEASURED: check:doc-snippets and check:doc-examples (reason: prerequisite, 'THE GATE COULD NOT RUN', packages not built; the docs diff adds 0 fenced blocks); the console eager-closure budget (reason: needs a console build; CI Bundle Analysis); the full app-shell suite of 1245 files (reason: foreground cap; CI).",
      "mcp_calls": "0. No MCP GitHub tool was called.",
      "api_writes": "3, all through the fleet relay as objectstack-fleet[bot]. Each is one POST /repos/objectstack-ai/objectstack/dispatches carrying one action. (1) pr_create, POST /repos/objectstack-ai/objectui/pulls (draft), created objectui#12070, run 37983085233, read-back identical 9390/9390 bytes. (2) label-write --assign os-support-ai, POST /repos/objectstack-ai/objectui/issues/12070/assignees, run 37983154212, read-back MATCHES. (3) this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/12067/comments, through post-stamped.mjs. Zero label writes: the PR's 4 labels are labeler.yml's. The 2 `git push` calls (empty write probe, then 6b1a3ff64) are not REST writes. Reads only otherwise: gh api GET of the card, the claim comment, objectstack#22510, objectui#11701 and #11697, the sys-activity object file at plugin-audit@17.0.0, and the PR read-back.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed (no class: it gives no wrong answer at a public door). ObjectStackAdapter.find routes `$expand`/`$search` to rawFindWithPopulate, which skips the SDK route's missingResources 404 memo. On a deployment with no sys_activity (no audit plugin), each mount of these three reads now repeats a 404 read instead of remembering the first. The visible answer is unchanged: a 404 is not a refusal (classifyLoadError), so the history and the feed stay empty. Recorded in PR Acceptance notes. Dedupe words: missingResources memo, expand route repeats 404, rawFindWithPopulate missing resource, sys_activity 404 per mount"
      ],
      "gates": [
        { "command": "pnpm exec vitest run packages/plugin-detail/ --maxWorkers=2", "exit": 0, "verdict": "Test Files 250 passed | 1 skipped (251); VERDICT command-exit 0" },
        { "command": "pnpm exec vitest run (111 app-shell files naming RecordDetailView, half A)", "exit": 0, "verdict": "Test Files 56 passed (56); VERDICT command-exit 0" },
        { "command": "pnpm exec vitest run (111 app-shell files naming RecordDetailView, half B)", "exit": 0, "verdict": "Test Files 55 passed (55); VERDICT command-exit 0" },
        { "command": "pnpm exec vitest run apps/console/src/__tests__/record-block-record-reach.test.tsx", "exit": 0, "verdict": "Tests 13 passed (13)" },
        { "command": "pnpm --filter @object-ui/plugin-detail type-check", "exit": 0, "verdict": "tsc --noEmit && tsc -p tsconfig.test.json, no output; VERDICT command-exit 0" },
        { "command": "pnpm --filter @object-ui/app-shell type-check", "exit": 0, "verdict": "tsc --noEmit && tsc -p tsconfig.test.json, no output; VERDICT command-exit 0" },
        { "command": "pnpm exec eslint --format json (7 touched TS/TSX files)", "exit": 0, "verdict": "files 7 errors 0 warnings 140" },
        { "command": "pnpm check:control-bytes", "exit": 0, "verdict": "check-control-bytes: OK (scanned 8262 tracked text file(s); skipped 85 binary)" },
        { "command": "pnpm check:test-path-roots", "exit": 0, "verdict": "check-test-path-roots: OK" },
        { "command": "pnpm check:changeset-claims", "exit": 0, "verdict": "No pending changeset names a file this change touches." },
        { "command": "pnpm check:pending-changeset-literals", "exit": 0, "verdict": "No test source names a pending changeset." },
        { "command": "pnpm check:new-line-citations", "exit": 0, "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0" },
        { "command": "pnpm check:vi-mock-specifiers", "exit": 0, "verdict": "check-vi-mock-specifiers: OK" },
        { "command": "pnpm check:vi-mock-inherit", "exit": 0, "verdict": "check-vi-mock-inherit: OK" },
        { "command": "pnpm check:vi-mock-override-shape", "exit": 0, "verdict": "check-vi-mock-override-shape: OK" },
        { "command": "pnpm check:doc-types", "exit": 0, "verdict": "Every documented component type is registered." },
        { "command": "pnpm check:doc-fences", "exit": 0, "verdict": "check:doc-fences — every TypeScript block in 227 document(s) is fenced" },
        { "command": "node scripts/check-changeset-presence.mjs", "exit": 0, "verdict": "7 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s): .changeset/12067-history-actor-id.md." },
        { "command": "node scripts/check-changeset-no-major.mjs", "exit": 0, "verdict": "No changeset declares a `major` bump." },
        { "command": "node scripts/check-changeset-fixed.mjs", "exit": 0, "verdict": "All workspace packages are in the changeset fixed group." },
        { "command": "node scripts/check-changeset-overwrite.mjs", "exit": 0, "verdict": "No pre-existing changeset was modified or deleted." },
        { "command": "node scripts/check-governed-queue-guard.mjs --test (9 paths)", "exit": 0, "verdict": "NOT GOVERNED — 9 path(s) checked against 5 governed surface(s); none matched." },
        { "command": "pnpm check:doc-snippets", "exit": 2, "verdict": "NOT MEASURED — prerequisite: unbuilt packages (unbuilt-package rows); docs diff adds 0 fenced blocks" },
        { "command": "pnpm check:doc-examples", "exit": 2, "verdict": "NOT MEASURED — THE GATE COULD NOT RUN — the packages these examples import are not built" }
      ],
      "deviations": [
        "The diff goes beyond the claim's file list, and the PR body says so. It adds one `$expand: ['actor_id']` line each to packages/plugin-detail/src/renderers/record-activity.tsx and packages/app-shell/src/views/RecordDetailView.tsx, plus the app-shell pin RecordDetailView.activityActorId-12067.test.tsx and one paragraph in content/docs/plugins/plugin-detail.mdx (AGENTS.md #2). Why: the claim admits the rule into recordActivityFeed.ts once the gap is measured, and the gap was measured. The rule takes effect only where the read expands actor_id, and leaving RecordDetailView out would make the console page and the record:activity block disagree about the same row. No export, prop, type or pack key changed, so Clause-② no holds. The seat may amend the claim's file surface.",
        "The one changeset (.changeset/12067-history-actor-id.md, patch) declares two packages, @object-ui/plugin-detail and @object-ui/app-shell, because app-shell source changed.",
        "app-shell tests were narrowed to the 111 files that name RecordDetailView, not the whole 1245-file suite (foreground cap). The full suite is CI's.",
        "Server-side behaviour ($expand, the expand gate, sys_user RLS) was measured by reading objectstack source at 6befe19c and the sys-activity object file at @objectstack/plugin-audit@17.0.0. There was no live backend boot: the sister checkout is unbuilt, and objectui installs no objectql/runtime.",
        "Commit trailers follow objectui AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude). The harness reminder's model-named Co-Authored-By line was not used, per that file's precedence rule."
      ],
      "files_changed": [
        ".changeset/12067-history-actor-id.md",
        "content/docs/plugins/plugin-detail.mdx",
        "packages/app-shell/src/views/RecordDetailView.activityActorId-12067.test.tsx",
        "packages/app-shell/src/views/RecordDetailView.tsx",
        "packages/plugin-detail/src/renderers/__tests__/record-history.actorId-12067.test.tsx",
        "packages/plugin-detail/src/renderers/__tests__/recordActivityFeed.actorId-12067.test.tsx",
        "packages/plugin-detail/src/renderers/record-activity.tsx",
        "packages/plugin-detail/src/renderers/record-history.tsx",
        "packages/plugin-detail/src/renderers/recordActivityFeed.ts"
      ],
      "line_budget": "not applicable (no skills/** surface)"
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T20:09Z. PR objectui#12070, head 6b1a3ff6.

    • PR shape:
      • Draft against main, based on acc43280. git merge-tree against main acc43280 is clean.
      • First line Fixes #12067, the body's only closing keyword. Clause-②: no at line start. One patch changeset, on @object-ui/plugin-detail and @object-ui/app-shell.
      • The commits carry only the model-free trailer pair. Assignee os-support-ai, set with no refusal.
    • Scope: 9 files, +454/−2, on the claim 6087540201. The governed-queue guard reads NOT GOVERNED.
      • Beyond the listed files, accepted:

        The claim admitted the rule into recordActivityFeed.ts once the gap was measured, and the dev measured it: an id-only row read "System" in the feed. The rule takes effect only where the read expands actor_id, so leaving those reads out would make the console page and the record:activity block disagree about the same row.

    • What lands:
      • One reader, activityActorName, module-level in recordActivityFeed.ts and not re-exported. It returns the actor_name snapshot if it is not blank, else the expanded actor_id record's name, else null, the existing fallback. Both record:history and the activity feed's mapper use it.
      • The three sys_activity reads (record:history's self-fetch, record:activity's self-fetch and RecordDetailView's merged feed) pass $expand: ['actor_id']. The engine resolves a page with one batched sys_user read in the same request, under full CRUD, RLS and FLS. A refused read keeps the bare id, which shows the fallback.
      • The stop clause did not fire: member_default reads sys_user for self and org peers (read from objectstack source; not live-booted).
    • Gates:
      • plugin-detail: 250 files and 2,433 tests passed. The 111 app-shell files that name RecordDetailView: 889 tests passed. The console's record-block-record-reach: 13 passed.
      • The plugin-detail and app-shell type-checks exit 0. eslint shows 0 errors.
      • Ablation A1–A5 each went red as predicted (2, 2, 4, 1 and 1), with every restore proven blob-equal.
      • CI on 6b1a3ff6: 43 runs, 40 success, 3 skipped (the expected three), 0 failure.
    • Contract review: not required (Clause-②: no).

    Finding, noted and not filed (no wrong answer at a public door): ObjectStackAdapter.find sends $expand reads through rawFindWithPopulate, which skips the SDK route's missing-resource memo. On a deployment with no sys_activity (no audit plugin), each mount of these reads now repeats a 404 instead of remembering the first. The history and feed stay empty, as before.

    Landing: ready + auto-merge through the queue, now. Its merge closes objectui#12067.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T20:29Z. PR objectui#12070 merged through the queue as e391f876 at 2026-10-09T20:27Z; its Fixes #12067 closed this card.

    • The merge:
      • One parent, acc43280, an ancestor of origin/main.
      • The landed tree equals git merge-tree of that parent and the accepted head 6b1a3ff6 (tree 6af020d4 on both).
      • 9 files, +454/−2, all nine blobs equal to the accepted head's.
    • Content check at e391f876:
      • record-history.tsx maps user_name: activityActorName(r), once, and the old r.actor_name ?? null mapping appears 0 times.
      • RecordDetailView.tsx passes $expand: ['actor_id'], once.
    • State: pm:dispatched is removed in this act; the card is closed completed. ACCEPT 6088434626.

    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

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions