Repository navigation
fix(plugin-audit): a user's own sys_user_preference write no longer reaches their bell as a sys_activity row - #22996
Conversation
… UI-state write A user's own write to their own sys_user_preference row (the console's ui.recent / ui.favorites / palette recents) was mirrored into sys_activity, and the bell's Activity tab handed it back to them as "Updated User Preference 'ui.recent'" on every navigation. The writer now keeps the sys_audit_log row and skips only the activity row when the acting user owns the row. The class is a closed, declared map beside SKIP_OBJECTS; other actors' writes keep their activity rows. Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c24b031694e12fde9175bc15608431b7ab810baa && git checkout c24b031694e12fde9175bc15608431b7ab810baa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6a40a09ed952f83b1f765d47cd76aee4a41f6672 5adbe6e7555699708080bd70726bfdd6bc54df12 && git checkout -B drift-repro 6a40a09ed952f83b1f765d47cd76aee4a41f6672 && git merge --no-ff 5adbe6e7555699708080bd70726bfdd6bc54df12
node scripts/docs-audit/affected-docs.mjs --json 6a40a09ed952f83b1f765d47cd76aee4a41f6672
|
Fixes #22961
Clause-②: no
A user's own write to their own
sys_user_preferencerow (the console'sui.recent,ui.favoritesand palette recents) no longer adds asys_activityrow, so it no longer reaches the user's bell asUpdated User Preference "ui.recent". Thesys_audit_logrow for the same write is unchanged.What the bell entries are, measured
The card marked its mechanism "to confirm". Measured on
origin/maina2240c4dc:sys_activityrows, not inbox messages. A throwaway probe (not committed) used the realObjectKernel,ObjectQLPlugin,AuditPlugin, SQLite and the shippedSysUserPreferenceobject. It created and then updated a user's ownui.recentrow, acting as that user. Each write produced onesys_audit_logrow and onesys_activityrow. The update's summary wasUpdated User Preference "ui.recent"(the card's text), withactor_idset to the user. The objectui bell's Activity tab lists the newestsys_activityrows the caller may read. Its second line istimeAgo · object_name, which matches the card's· sys_user_preferencesubtitle; notification rows show no object name.sys_inbox_message.service-messagingregisters no data hook. The only callers ofemit()are approvals, comment @mentions, the automationnotifynode and the HTTP nodes. Subscription expansion (sys_notification_subscription) is not wired, and its object docblock says so. So the inbox half of the card's acceptance item 3 already holds onmain, and the activity mirror is the channel to change.sys_user_preference_self,user_id == current_user.id). An organization admin's wildcardmodifyAllRecordspasses that RLS, and Setup lists the object (nav_user_preferences), so an admin can edit another user's row. No server-side code writes the object. These writers keep today's rows.sys_user_preference: objectui'sConsoleShellbinds itsui.favorites,ui.recentandui.flow.palette.recentsadapters to it. No other per-user UI-state object exists. No spec key means "per-user UI state". The existingenable.activities: falsemutes every writer, which is wider than this rule.The change
In
packages/plugins/plugin-audit/src/audit-writers.ts:Beside
SKIP_OBJECTS, a closed, module-private map,PER_USER_UI_STATE_OWNER_FIELD(sys_user_preferencemaps touser_id). Its comment writes the rule down: what reaches a user's bell is another actor's act or something the user needs, never the user's own UI state.In
writeAudit, the writer keeps the ledger row and skips the activity row when both of these hold:userId, theactor_idvalue) is the owner on every side of the row the write carries.It also skips the actor-name read, which only feeds the activity row. This is the same seam as the existing
enable.activitiesopt-out.Not touched:
SKIP_OBJECTS;AUDIT_EXCLUDED_OBJECTS, which the hook registration andread-audit.tsread; the package entry.dist/index.d.tsnames neither new symbol (0 hits; the controlinstallAuditWritershas 3). Thesys_audit_logrow a preference write produces is unchanged.Pins (
audit-writers.test.ts)sys_user_preferencerow each produce exactly onesys_audit_logrow (action,user_idandrecord_idasserted) and zerosys_activityrows.actor_id:Ablations ran through
scripts/ablation-replace.mjsin wrap mode. For each one the tool showed the anchor count going 1 to 0 on disk and the blob changing. It then proved the restore: blob equal toHEADand an emptygit diff HEAD. They ran on2632f1ee5, whose plugin-audit sources are byte-identical to the head:mirrorsActivity = activitiesEnabled):Tests 3 failed | 4 passed. The three own-write pins fail withAssertionError: expected [ { object: 'sys_activity', …(1) } ] to have a length of +0 but got 1.Tests 3 failed | 4 passed. The three other-writer controls fail withAssertionError: expected [] to have a length of 1 but got +0.sys_user_preferenceadded toSKIP_OBJECTS):Tests 6 failed | 1 passed. The own-write pins fail ataudit-writers.test.ts:727, thesys_audit_loglength assertion.The first attempts at ablations 2 and 3 were no-ops. Their replacement text contained the anchor, so
ablation-replacesaw the anchor count stay at 1, refused before running the tests, and restored the file. Both were re-run with anchors the replacement does not contain. The readings above are from the re-runs.The card's acceptance item 1 (a browser walk) was not done. The pins carry it at the writer seam that produces the rows.
Verification
@objectstack/plugin-auditunit suite, at2632f1ee5(later commits change only the changeset):Test Files 47 passed (47),Tests 750 passed (750).pnpm --filter @objectstack/plugin-audit typecheck(tsc, the scripts project andcheck:test-typecheckovertsconfig.test.json): exit 0.turbo run build --filter=@objectstack/plugin-audit^..., 15 tasks) and the package's own build (check-dts-emitted: 1/1 declaration file present): exit 0.node scripts/pm/dispatch-gates.mjs --commandsover this branch (3 paths, 67 families), every command run and reconciled with--ran.03373429f:Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN, with every recorded exit code 0.2632f1ee5recordedcheck:dual-build-cjs-loadsandcheck:i18nat exit 3 (PREREQUISITE NOT MET), because other packages had nodist/. Later in that pass,check:type-check-debtbuilt the whole workspace throughturbo run typecheck.--print-config: 6 and 5 rules); it claims nothing for the changeset.md.eslint --no-inline-config --format jsonover the two files: 2 files, 0 errors, 0 warnings, at03373429f.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules, stated at its line 327), so this diff cannot change the result for any file it does not touch.origin/mainwas merged before opening the PR. It was stilla2240c4dc(remote tip read withls-remote), so the merge was a no-op.Acceptance notes
sys_activityretention (lifecycle.retention.maxAge: '14d') removes them. The changeset says so.enable.activities: false. That work is wasted but has no observable effect. Not filed.sys_activity.object_nameverbatim (objectuiInboxPopover.tsx, filled byactivityItemType.ts). Every other activity row therefore still shows a machine name such ascrm_leadrather than the object's label. This is reported to the seat as a candidate in objectui. It is not in this PR.Generated by Claude Code