Repository navigation
record page History: an activity row with no actor at all (actor_id and actor_name both null: seed and system writes) reads "Unknown user" with a "?" avatar — it reads System #12527
Description
Filing gate ① (a product defect, measured on main), filed on the maintainer's word (chat, 2026-10-15, session session_019YFzJp4o8Ck4NsT1toC4UP, seat account os-bill): the seat ran a functional audit of the HotCRM console on objectui main 720f6d4 (report, private to the maintainer: https://claude.ai/artifact/XPrn5eZMsQaKUgasMJEMmR, finding F9, figure 10), and the maintainer answered 「同意开卡,使用 pm-dispatch 创建卡片」. Quota note: the audit's cards exceed the filing gate's three-per-fire quota on that word, which approved the report's card list as a set.
Parent: objectstack-ai/objectstack#25945. The third pin of #12067 (closed by PR #12070) covers the row this card measures: "a row with neither renders the existing fallback". This card changes what that fallback says. Blocks nothing; nothing blocks it.
Reader who acts: the domain:ui lane's next dispatch (seat post #9800), one dev round in @object-ui/plugin-detail (HistoryTimeline.tsx, renderers/record-history.tsx) and the ten @object-ui/i18n packs, size S.
Dedupe (MCP search_issues, scope objectstack-ai/objectui, all states, read 2026-10-15T14:14Z): "record history timeline Unknown user for system or seed writes actor missing show System" → 3, all closed: #12067 (rows with actor_id and no actor_name read Unknown user; fixed by PR #12070, which resolves actor_id), #11701, #11697 (the audit log's actor column). None is this card: #12067 left the no-actor row on the fallback by design and this card names that row.
What happens
The HotCRM lead "Mira Costa" → History shows three entries, each with a "?" avatar and the name "Unknown user": 「UPDATED · Lead Owner: ∅ → Dev Admin」, 「UPDATED · Updated Lead "Mira Costa - Atlas Construction"」, 「CREATED · Created Lead "Mira Costa - Atlas Construction"」. A reader is told a person they cannot identify changed the record; the writer was the seed's owner backfill, with no user in context.
Readings (objectui origin/main 720f6d4, read 2026-10-15T14:15Z)
- API reading (HotCRM backend, admin):
GET /api/v1/data/sys_activity?filter=["object_name","=","crm_lead"]&sort=-timestamp→ 28 rows, each withactor_id: null,actor_name: null,actor_avatar_url: null,created_by: null;summary"Lead Owner: ∅ → Dev Admin",metadata{"old":{"owner_id":null},"new":{"owner_id":"ZgyhVWT4…"}}. A login'ssys_userrow, by contrast, carriesactor_idandactor_name"Dev Admin". packages/plugin-detail/src/renderers/record-history.tsxline 147 readsschema.unknownUserText; line 217 passes it toHistoryTimeline;HistoryTimeline.tsxlines 60, 168 and 213: the fallback label for an entry with no user, defaulting to the "Unknown user" string (i18nunknownUseratpackages/i18n/src/locales/en.tsline 1468 is the shorter "Unknown" used elsewhere).- PR fix(plugin-detail): name the actor of an activity row that carries only actor_id (objectui#12067) #12070 (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): an entry with
actor_idonly now resolves the user's name; an entry with neither keeps the fallback.
What to build
- An entry whose row carries neither
actor_idnoractor_nameis a system write: it renders the label "System" (new i18n key in all ten packs, e.g.detail.history.systemActor) with a system glyph (a cog or the platform mark) in place of the "?" avatar, never a person-shaped unknown. - "Unknown user" stays for the only case that is unknown: an
actor_idthe user lookup cannot resolve (a deleted user). unknownUserTextkeeps overriding the unknown case; a newsystemActorTextoverride is not added unless the schema already has a slot for it.
Acceptance
HistoryTimelinetests: a row with no actor renders "System" and the system glyph; a row with an unresolvableactor_idrenders "Unknown user"; the 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 pins (resolvedactor_id,actor_namewins) unchanged.- Browser reading by the landing seat on the HotCRM lead above: the three seed entries read "System".
pnpm testfrom the repo root green on the affected files;check:i18n-keysgreen; a changeset (patch) for@object-ui/plugin-detailand@object-ui/i18n.
Out of scope
- The writer (objectstack's seed and hooks setting an actor); the audit log browser's actor column (audit log: the actor column shows a truncated raw user id and the actor filter is free text, not a user lookup (split from objectui#11697) #11701).
Attribution: written by the domain:ui seat in session session_019YFzJp4o8Ck4NsT1toC4UP (account os-bill) on the maintainer's words quoted above.
Screenshots (added 2026-10-15T14:41Z, on the maintainer's word 「专用分支存图,卡里嵌 raw 链接」)
The images live on the attachment branch claude/ui-audit-2026-10-15-shots of objectstack-ai/objectui (commit 918a251, never merged; index at https://github.com/objectstack-ai/objectui/blob/918a2510a54bd6875b90eb6996d098c3ec0451dd/docs/design/console-audit-2026-10-15/README.md). Full set and figure numbers: the audit report linked above.
History tab: three "Unknown user" entries with "?" avatars for the seed's writes

Activity
objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 1
Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12527-history-system-actorWorktree:
objectui-issue-12527Domain:
domain:uiSeat:
domain:ui#2(the card names the lane's next dispatch under seat 3's post #9800, which is vacant)File surface:
packages/plugin-detail/src/HistoryTimeline.tsxandpackages/plugin-detail/src/renderers/record-history.tsx(the no-actor label and glyph),packages/i18n/src/locales/*.ts(one new key in all ten packs), the tests beside each, and one.changeset/12527-*.md(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: no
Responsibility:
objectui's record History timeline, which labels an entry with neither actor_id nor actor_name as "Unknown user" | none: PR objectstack-ai/objectui#12070 resolves an actor_id, and an entry with no actor at all keeps the person-shaped fallback | every reader of a record page's History whose rows were written by the system (measured on HotCRM: every seeded lead's owner backfill and create rows)Thread-read: none
Serial constraints cleared: at
mainb51242899, no in-flight claim or open PR touchesHistoryTimeline.tsx,record-history.tsxor the i18n packs' history keys. objectui#12529 (claimed alongside,RecordDetailView.tsx) and objectui#12530 (this seat,sharedUserFeeds.ts) are disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 12527,
"status": "done",
"branch": "claude/issue-12527-history-system-actor",
"pr": "#12547",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB",
"premise_still_valid": true,
"summary": "HistoryTimeline now has three actor faces: a resolved name; no name and no user_id = System (detail.systemActor) with a lucide Cog glyph in place of the ? avatar; a user_id with no name = unknownUserText or Unknown user with ?. The record:history self-fetch now carries each row's actor id (bare actor_id or the expanded user record's id) onto HistoryEntry.user_id, which the type already declared, so the timeline can tell the two apart; activityActorName (#12067) untouched. No systemActorText override (no schema slot). Deviation: no new i18n key. detail.systemActor already exists, is translated in all ten packs and is what record:activity renders for the same no-actor row; reusing it avoids forking one word across ten packs, so no locale pack is touched and the changeset covers @object-ui/plugin-detail only.",
"tests": "HEAD c18550e. vitest HistoryTimeline.test.tsx + record-history.{actorId-12067,contractEnvelope-6726,rowLimitRefusal-10005}: Test Files 4 passed, Tests 39 passed. vitest --maxWorkers=2 packages/plugin-detail/ (lock): Test Files 271 passed | 1 skipped, Tests 2630 passed | 14 skipped, VERDICT command-exit 0. Ablation (fix committed first; objectstack ablation-replace.mjs, anchor 1to0, blob 4adf08c8to20a50380): isSystemActor forced false gives 3 failed | 10 passed (two System pins in HistoryTimeline.test.tsx + the re-ruled #12067 third pin); Unknown-user pin stays green as expected; restore verified blob == HEAD 4adf08c8, git diff HEAD empty. Tests import HistoryTimeline by relative src path, so no dist rebuild was in the ablation path. Closure build pnpm --filter '@object-ui/plugin-detail^...' build exit 0; type-check plugin-detail + i18n exit 0 (both legs echoed type-check: Done). ESLint narrowed to the 4 changed ts/tsx files (count from --format json): 0 errors, warnings pre-existing (record-history.tsx 8 at base, 8 now); config has no type-aware linting, so untouched files cannot change verdict.",
"gates": {
"check:phantom-deps": 0, "check:self-import": 0, "check:unreferenced-sources": 0, "check:doc-example-readers": 0,
"check:handler-key-reads": 0, "check:side-effects-array": 0, "check:element-data-source-declaration": 0,
"check:spec-symbols": 0, "check:action-forward-parity": 0, "check:designer-field-key-parity": 0,
"check:icon-record-names": 0, "check:i18n-keys": 0, "check:i18n-drift": 0, "check:i18n-designer-parity": 0,
"check:control-bytes": 0, "check-changeset-fixed": 0, "check-changeset-presence": 0,
"type-check plugin-detail+i18n": 0, "closure build": 0, "vitest touched": 0, "vitest plugin-detail": 0, "eslint narrowed": 0,
"CI": "in_progress (not awaited, per contract)"
},
"line_budget": "n/a (no skills/** or governed surface touched)",
"files_changed": [
"packages/plugin-detail/src/HistoryTimeline.tsx",
"packages/plugin-detail/src/renderers/record-history.tsx",
"packages/plugin-detail/src/tests/HistoryTimeline.test.tsx",
"packages/plugin-detail/src/renderers/tests/record-history.actorId-12067.test.tsx",
".changeset/12527-history-system-actor.md"
],
"deviations": [
"Section-1 ruling said one NEW i18n key in all ten packs; measured that detail.systemActor already exists, translated in all ten packs and used by record:activity for the same row. Reused it: no locale file edited, changeset is @object-ui/plugin-detail patch only (dispatch named @object-ui/i18n patch too). The ruling's end (properly translated System in all ten packs) holds; a zh pin proves the pack lookup. If the seat wants a distinct detail.history.* key anyway, that is a ten-pack follow-up the seat can order.",
"The #12067 third pin (row with neither renders the existing fallback) is changed, not unchanged: the card re-rules exactly that row. The other #12067 pins are untouched.",
"objectstack scripts/pm/dispatch-gates.mjs not run: it answers for the objectstack tree only; objectui gate list taken from the dispatch.",
"Lock: first closure-build attempt was queue-timeout exit 99 (holder: another objectui worktree's vitest); re-acquired under the same slot and ran green."
],
"mcp_calls": "0",
"api_writes": "2 relay strokes + 2 git pushes: fleet-write dispatch pr_create (POST /repos/objectstack-ai/objectui/pulls draft + POST issues/12547/assignees) to PR #12547, read-back 4614 bytes identical, assignee baozhoutao; fleet-write dispatch comment (POST /repos//issues/12527/comments, this report). git push x2 (empty branch probe, c18550e). Zero label writes (dispatch named none; objectui labels via labeler.yml).",
"open_questions": [],
"out_of_scope_findings": []
}
objectstack-fleet commentedon Oct 15, 2026
Review: ACCEPT — objectui PR #12547 ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T16:45ZReviewed on GitHub at head
c18550e, against the card's What to build 1–3 and Acceptance, and the claim (6196021892), not against the dev's report.main. Its body opensFixes objectstack-ai/objectui#12527, the only closing keyword, followed byClause-②: no. 5 files.HistoryTimeline:user_id: System, with a lucideCogglyph in place of the "?" avatar;user_idwith no resolvable name:unknownUserText, or "Unknown user", with "?" (card item 2).record:history's self-fetch now carries each row's actor id ontoHistoryEntry.user_id, a field the type already declared. That is the bareactor_idor the expanded user record'sid, and it is what lets the timeline tell the two cases apart. 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'sactivityActorNameis untouched.systemActorTextoverride was added, because the schema has no slot for one (card item 3).detail.systemActorinstead: the seat read it in all ten packs (en "System", zh "系统", ar "النظام", de "System", es "Sistema", fr "Système", ja "システム", ko "시스템", pt "Sistema", ru "Система"), andrecord:activityalready renders it for the same no-actor row. One word is not forked across ten packs, so no locale file changes, and a zh pin proves the pack lookup.actor_idrenders "Unknown user".isSystemActorfalse fails 3 pins, and the Unknown-user pin stays green. Restore was proven by blob hash.@object-ui/plugin-detail: patchonly, since no i18n pack changed.packages/plugin-detail/gives 2630 passed and 14 skipped under the lock. The battery exits 0, includingcheck:i18n-keysandcheck:i18n-drift.filter=latest, 37 runs), all 16 test shards included.Next:
pr_ready+automerge_enable.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T17:01Zmain165f8ff. The card closed by the PR'sFixes objectstack-ai/objectui#12527, which is the only closing keyword.origin/main, all 5 files the PR touches are blob-identical to the accepted headc18550e. They areHistoryTimeline.tsx,record-history.tsx, the two pin files and.changeset/12527-history-system-actor.md.pm:dispatchedis removed in the same stroke as this comment.Generated by Claude Code
fix(plugin-detail): a History entry with no actor reads "System", not…