Repository navigation
plugin-audit: a user's own sys_user_preference writes (ui.recent) come back to them as "Updated User Preference 'ui.recent'" inbox notifications — the console bell shows six of them and nothing else #22961
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Oct 12, 2026 objectstack-fleet commented
on Oct 12, 2026 ContributorMore actionsTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T06:58Z. ⛔ Not a claim, ⛔ not a dispatch. Grade completed. The filer'sbug·priority:p2·pm:queuestand;domain:services·area:recordsadded (plugin-audit'saudit-writers.ts).- Direction, as the card says: the inbox fan-out skips a user's own writes to per-user UI-state tables. The audit row stays if the object stays audited.
- The rule is written beside
SKIP_OBJECTS: a notification is for another actor's act or a need of the user, never the user's own UI state. - ⛔ Not a blanket mute of
sys_user_preferencefor other actors' writes, if any exist. The claim measures that.
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchand removed
on Oct 12, 2026 objectstack-fleet commented
on Oct 12, 2026 ContributorMore actionsClaim: PM loop round 2 · 2026-10-12T07:37Z
Session:session_01CeRCvvvjxbxu2iYJAc5Cce
Account:os-warren(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-22961-self-ui-state-no-notification
Worktree:objectstack-issue-22961
Domain:domain:services
Seat:domain:services#1(seat post #6021)
File surface:packages/plugins/plugin-audit/src/audit-writers.ts(the writer seam besideSKIP_OBJECTS), one test file underpackages/plugins/plugin-audit/src/for the writer, and one.changeset/22961-*.md. Stop on breach; explain in the report.
Container & model:S, not mechanical (which channel carries the bell entry, and which writes count as a user's own UI state, are measured judgments) ⇒ M treatment,mode:subagent,model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"
Clause-②: no
Responsibility:plugin-audit's own writer (this lane's code) turns a user's ownsys_user_preferencewrite into a bell entry | none: nothing else filters a user's own UI-state writes out of the bell | every console user whose navigation changes "recently accessed" reaches it (six entries on a fresh cloud staging org, objectstack-ai/cloud#2772 item 5)
Thread-read: 6119354713
Serial constraints cleared: in flight in this lane, #22913 (claim6119084257, PR #22981) touchesplugin-audit/src/audit-plugin.tsandaudit-plugin.test.ts, which is file-disjoint fromaudit-writers.ts. No open PR touchesaudit-writers.ts(all open PRs' file lists, read at this claim). #22738, the last cross-lane declarant onaudit-writers.ts, is closed. The #22870 PR 2 surface inplugin-auditiscomment-access-hooks.tsonly.Direction carried into the order is triage's grade
6119354713: the fan-out skips a user's own writes to per-user UI-state tables, the audit row stays, and ⛔ no blanket mute of other actors' writes. No maintainer ruling exists on this card. Thesys_audit_logledger is a human-floor surface for this lane (audit trail): the order fences it unchanged.
Generated by Claude Code
objectstack-fleet commented
on Oct 12, 2026 ContributorMore actionsos-dev-report
{
"issue": 22961,
"status": "done",
"branch": "claude/issue-22961-self-ui-state-no-notification",
"pr": "#22996",
"session": "session_01CeRCvvvjxbxu2iYJAc5Cce",
"premise_still_valid": true,
"summary": "A1, measured on a real engine (a throwaway probe: ObjectKernel, ObjectQLPlugin, AuditPlugin, SQLite and the shipped SysUserPreference object, not committed). A user's own ui.recent create and update each land one sys_audit_log row and one sys_activity row, and the update's summary reads 'Updated User Preference "ui.recent"'. No path turns a record write into a sys_inbox_message. service-messaging has no data hook; the emit() callers are approvals, comment mentions and the notify and HTTP nodes; subscription expansion is not wired. So the bell entries are Activity-tab rows. The card's 'inbox fan-out' mechanism was wrong, but its symptom holds. The fix, in plugin-audit/src/audit-writers.ts: a closed, module-private map PER_USER_UI_STATE_OWNER_FIELD (sys_user_preference -> user_id) beside SKIP_OBJECTS, carrying the rule comment. writeAudit skips the activity row, and the actor-name read that only feeds it, when userId (the actor_id value) is the owner on every side of the row the write carries. The ledger row is unchanged. SKIP_OBJECTS, AUDIT_EXCLUDED_OBJECTS and the package entry are untouched, so Clause-2 'no' holds: dist/index.d.ts names neither new symbol (0 hits; the control installAuditWriters has 3). A3: an org admin's wildcard modifyAllRecords passes the self-only RLS, and Setup lists the object (nav_user_preferences). Those writes, system writes and owner-changing writes keep their activity rows, and no server-side code writes the object. A2: the class is sys_user_preference alone. The console keeps ui.favorites, ui.recent and ui.flow.palette.recents there; no spec key means per-user UI state; enable.activities:false would be the blanket mute the grade forbids.",
"tests": "plugin-audit unit suite at 2632f1e (head 0337342 changes only the changeset): 'Test Files 47 passed (47)', 'Tests 750 passed (750)', VERDICT command-exit 0. typecheck (tsc, the scripts project, and check:test-typecheck over tsconfig.test.json): VERDICT command-exit 0. 7 new pins in audit-writers.test.ts: 3 own-write negatives (create, update, delete: one sys_audit_log row with action, user_id and record_id asserted, and zero sys_activity rows) and 4 controls (an ordinary object, an admin, a system write, an owner change). Ablations used scripts/ablation-replace.mjs in wrap mode on 2632f1e. The test imports ./audit-writers.js relatively, so it resolves to source and no dist is in the path. For each: anchor 1 -> 0 on disk and the blob changed; the restore proved blob == HEAD 790ceaaf9104 and an empty git diff HEAD. (1) Skip removed: 'Tests 3 failed | 4 passed', the 3 own-write pins, 'AssertionError: expected [ { object: 'sys_activity', …(1) } ] to have a length of +0 but got 1'. (2) Owner check removed (blanket mute): 'Tests 3 failed | 4 passed', the 3 other-writer controls, 'expected [] to have a length of 1 but got +0'. (3) sys_user_preference added to SKIP_OBJECTS: 'Tests 6 failed | 1 passed', with the own-write pins failing at audit-writers.test.ts:727 (the ledger length). The first attempts of (2) and (3) were no-ops: the replacement contained the anchor, the count stayed 1, the tool refused before the tests ran and restored the file. Both were re-run with disjoint anchors. Lint, scoped to the touched files: eslint --print-config claims both .ts files (6 and 5 rules); --format json reads 2 files, 0 errors, 0 warnings at 0337342; the config is never type-aware (eslint.config.mjs:327). plugin-audit's CJS dist entry loads (16 exports).",
"gates": "dispatch-gates --commands over the branch: 3 paths, 67 families. That is a superset of the order's 50; the changeset and test-file families joined. All 67 ran at 0337342. --ran: 'Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN', every recorded exit 0. A first pass at 2632f1e recorded check:dual-build-cjs-loads and check:i18n at exit 3 (PREREQUISITE NOT MET). Later in that pass, check:type-check-debt (--re-measure: turbo run typecheck, dependsOn ^build) built the whole workspace from this tree. On the head pass both read green: 107 CJS entries across 66 packages load; 9 packages' bundles are in sync. The dependency-closure build (turbo, 15 tasks) and the package's own build (check-dts-emitted 1/1) both exited 0. CI was not awaited.",
"line_budget": "n/a: no line-ratcheted surface touched. The diff is +184/-2 over 3 files, under the 3000-line human-merge threshold.",
"deviations": [
"Commit trailers use the model-free pair from AGENTS.md (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By. The PR footer uses the AGENTS.md session-URL form, not the reminder's footer text.",
"To measure A1, a throwaway real-engine probe (plugin-audit/src/zz-probe-22961.integration.test.ts) was run once in the worktree. It was deleted before the first commit and never committed.",
"Ablation legs 2 and 3 were no-ops on their first attempt (the tool refused them); both were re-run, and the readings in tests are the re-runs.",
"The unit suite and the ablations ran at 2632f1e. The head, 0337342, changes only the changeset text (activity rows leave the feed through the 14-day sys_activity retention).",
"The merge of origin/main before opening the PR was a no-op: main was still a2240c4.",
"File surface: as claimed, nothing outside it."
],
"files_changed": [
".changeset/22961-plugin-audit-own-ui-state-no-activity.md",
"packages/plugins/plugin-audit/src/audit-writers.ts",
"packages/plugins/plugin-audit/src/audit-writers.test.ts"
],
"mcp_calls": "0",
"api_writes": "2 relay strokes as objectstack-fleet[bot], each one POST /repos/objectstack-ai/objectstack/dispatches. (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #22996) and POST /repos//issues/22996/assignees (os-warren). (2) This os-dev-report comment through post-stamped -> POST /repos//issues/22961/comments. Label writes: 0 (the order names none, and a real changeset is present). git push, which is not REST: 5 pushes of the branch (the empty probe and 4 commits).",
"open_questions": [],
"out_of_scope_findings": [
"class: a (seat to judge a vs polish) · reach: public door. The card's own 2026-10-12 cloud-console staging walk shows bell Activity rows subtitled '· sys_user_preference', a raw API name. After this fix, every other activity row still shows its machine name (for example crm_lead) in that position · evidence: objectui 0f31c3a0 packages/app-shell/src/layout/InboxPopover.tsx:701 renders a.objectName, which layout/activityItemType.ts:266 fills verbatim from sys_activity.object_name, with no label lookup. The pin d758f2f2 is not in the local shallow clone · dedupe words: bell activity object_name raw; InboxPopover objectName label; activity feed machine name",
"carrier: 承接者:无 · noted, not filed. For an activity row it then skips, writeAudit still composes the summary and resolves the locale. This was already true for enable.activities:false. The work is wasted but has no observable effect; it is recorded in the PR's Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 12, 2026 ContributorMore actionsSeat ACCEPT: PR #22996 at
03373429f· landing waits on a green headdomain:servicesseat 1 ·session_01CeRCvvvjxbxu2iYJAc5Cce· 2026-10-12T08:40Z, under claim6119866779. The dev's report is theos-dev-reportabove. Checked on GitHub and onorigin/main, not from the report.The card's mechanism, corrected in public. The body's "emits the L2 event and the inbox fan-out" was wrong. The dev measured on a real engine that a user's own
ui.recentwrite lands onesys_audit_logrow and onesys_activityrow, and that no path turns a record write into asys_inbox_message. The bell entries are Activity-tab rows. The symptom holds. The fix acts on the measured channel, which was the dispatch's first branch.Checklist
- PR form: draft, base
main; the first line isFixes #22961, thenClause-②: no. No other card number is near a closing keyword. The assignee isos-warren. - Scope: 3 files, +184 / −2:
audit-writers.ts,audit-writers.test.tsand.changeset/22961-plugin-audit-own-ui-state-no-activity.md. That is the claim's surface. Nothing is undercontent/docs/releases/.check-governed-merges --pr 22996: NOT governed. - Lane fence held (the audit trail is human-floor):
- The ledger row is unchanged. The diff only gates
activityRowToWriteand the actor read that feeds it. SKIP_OBJECTSandAUDIT_EXCLUDED_OBJECTSare untouched.- Ablation 3 (the object added to
SKIP_OBJECTS) turns the ledger-length pins red. So the pins hold the fence, not just the code.
- The ledger row is unchanged. The diff only gates
- No blanket mute:
isOwnUiStateWriteanswerstrueonly when every side of the row that names an owner names the acting user. An admin write, a system write and an owner change each keep their activity row, as three CONTROL pins assert. Ablation 2 (owner check removed) turns those controls red. Clause-②: noholds: the map and the predicate are module-privateconst/function, not exported. The dev measured 0 hits indist/index.d.tsagainst a control of 3.- Changeset: a
patchfor@objectstack/plugin-audit, read sentence by sentence.- "skips the activity row when the acting user is the row's owner (
user_id)" matches the predicate. - The three kept-writer cases match the controls.
- "leave the feed when the
sys_activityretention window (14 days) removes them" matchesretention: { maxAge: '14d' }insys-activity.object.tsonorigin/main. - The RLS claim in the docblock matches
sys-user-preference.object.ts'suser_idequals{current_user_id}filter.
- "skips the activity row when the acting user is the row's owner (
- Tests: the plugin-audit suite (47 files, 750 tests) and the typecheck ran at
2632f1ee5; the head changes only the changeset. Three ablations ran, with output quoted and restores proved by blob.--ranreconciles 67 of 67. - Deviations accepted:
- A throwaway probe, never committed.
- Two ablation first attempts the tool refused before running, re-run with disjoint anchors.
- Model-free trailers.
mcp_callsis 0.
Parallel discipline: the sibling #22913 landed as
6a40a09ed. It touchesaudit-plugin.ts, so it is file-disjoint from this PR. The seat mergesmaininto this branch before landing.Out-of-scope findings
- The bell shows the raw
object_namebeside a labelled summary. Filed in the repo where the fix lands: app-shell: the bell's Activity tab shows each row's object by its API name (sys_user_preference, crm_lead) beside a summary that already uses the object's label objectui#12212. - The summary and locale are still composed for a skipped activity row: Acceptance notes (carrier: none; no observable effect, the same as
enable.activities: falsetoday).
Generated by Claude Code
- PR form: draft, base
- addedpriority:p1High: required for production / M2High: required for production / M2and removedpriority:p2Medium: important, M3Medium: important, M3
on Oct 12, 2026 objectstack-fleet commented
on Oct 12, 2026 ContributorMore actionsLanded: PR #22996 ·
domain:servicesseat 1 · 2026-10-12T09:29Zsession_01CeRCvvvjxbxu2iYJAc5Cce, under claim6119866779and ACCEPT6120755152.- Merge: the merge queue merged it at 2026-10-12T09:28:06Z as
8e4a2302aonorigin/main. Its queue branch is gone. - On
main:audit-writers.tsdeclaresPER_USER_UI_STATE_OWNER_FIELD, and the activity row is gated bymirrorsActivity. Thesys_audit_logwrite path is unchanged. - Board: the card closed
completedthroughFixes. This write removespm:dispatchedand the assignee. Domain, area, type and priority labels stay; the priority was re-graded p2 → p1 while the card was in flight. - Carried: app-shell: the bell's Activity tab shows each row's object by its API name (sys_user_preference, crm_lead) beside a summary that already uses the object's label objectui#12212 (the bell's Activity tab shows the raw
object_name). - Publication: a merge to
maindoes not publish. The fix reaches the cloud console with the next@objectstack/plugin-auditrelease and the cloud framework pin that takes it. Activity rows written before then leave the feed throughsys_activity's 14-day retention.
Generated by Claude Code
- Merge: the merge queue merged it at 2026-10-12T09:28:06Z as
Item 5 of objectstack-ai/cloud#2772. Filed from the cloud maintainer's session with the maintainer's agreement on 2026-10-12 (「同意」). ⛔ Not a claim.
Source
2026-10-12 staging walk-through of the cloud console:
cloud.objectos.app, org 「2531 hotcrm 验收」, owner account; cloud63ea887f, objectui pind758f2f2, framework pin as cloud.objectstack-shaat that commit.What the user sees
Cloud console, bell icon → 消息中心: six entries 「Updated User Preference 'ui.recent'」, subtitle 「系统通知 · sys_user_preference」, and nothing else. They are the audit trail of the console's own "Recently accessed" writes (objectui#11678 made that write happen only when the list changes, so each navigation that changes the list still writes once). A new user opens the bell and reads six machine messages about a table they have never heard of.
Mechanism (to confirm)
packages/plugins/plugin-audit/src/audit-writers.tsemits the L2 event and the inbox fan-out through the ADR-0030 single ingress for audited writes. ItsSKIP_OBJECTSlist (audit-writers.ts:166onward) excludes sessions, jobs, notifications and the inbox itself, but notsys_user_preference, so a user's own UI-state write comes back to them as a system notification. Precedent: objectstack#3402 (file-assignment noise notifications).Fix direction (recommended)
sys_user_preference(and any other per-user UI-state table) toSKIP_OBJECTS, or stop the inbox fan-out for self-authored writes on system tables; keep the audit log row if auditing needs it.Acceptance
sys_user_preferenceentries.sys_user_preferencestays audited, or the card states it is excluded.sys_user_preferenceproduces nosys_inbox_message.Related
objectstack#3402 · objectui#11678 · ADR-0030