Skip to content

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

@hotlong

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; cloud 63ea887f, objectui pin d758f2f2, framework pin as cloud .objectstack-sha at 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.

notification center

Mechanism (to confirm)

packages/plugins/plugin-audit/src/audit-writers.ts emits the L2 event and the inbox fan-out through the ADR-0030 single ingress for audited writes. Its SKIP_OBJECTS list (audit-writers.ts:166 onward) excludes sessions, jobs, notifications and the inbox itself, but not sys_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)

  1. Add sys_user_preference (and any other per-user UI-state table) to SKIP_OBJECTS, or stop the inbox fan-out for self-authored writes on system tables; keep the audit log row if auditing needs it.
  2. Write the rule down next to the list: a notification is for something another actor did or something that needs the user; the user's own UI-state writes never qualify.

Acceptance

  1. Fresh user, navigate five console pages, open the bell: zero sys_user_preference entries.
  2. The audit log still records the write if sys_user_preference stays audited, or the card states it is excluded.
  3. Unit test on the writer: a write to sys_user_preference produces no sys_inbox_message.

Related

objectstack#3402 · objectui#11678 · ADR-0030

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    Contributor

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T06:58Z. ⛔ Not a claim, ⛔ not a dispatch. Grade completed. The filer's bug · priority:p2 · pm:queue stand; domain:services · area:records added (plugin-audit's audit-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_preference for other actors' writes, if any exist. The claim measures that.
  2. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 2 · 2026-10-12T07:37Z
    Session: session_01CeRCvvvjxbxu2iYJAc5Cce
    Account: os-warren (the seat's linked user as GET /user answers 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 beside SKIP_OBJECTS), one test file under packages/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 own sys_user_preference write 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 (claim 6119084257, PR #22981) touches plugin-audit/src/audit-plugin.ts and audit-plugin.test.ts, which is file-disjoint from audit-writers.ts. No open PR touches audit-writers.ts (all open PRs' file lists, read at this claim). #22738, the last cross-lane declarant on audit-writers.ts, is closed. The #22870 PR 2 surface in plugin-audit is comment-access-hooks.ts only.

    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. The sys_audit_log ledger is a human-floor surface for this lane (audit trail): the order fences it unchanged.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    Contributor

    os-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

  4. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    Contributor

    Seat ACCEPT: PR #22996 at 03373429f · landing waits on a green head

    domain:services seat 1 · session_01CeRCvvvjxbxu2iYJAc5Cce · 2026-10-12T08:40Z, under claim 6119866779. The dev's report is the os-dev-report above. Checked on GitHub and on origin/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.recent write lands one sys_audit_log row and one sys_activity row, and that no path turns a record write into a sys_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 is Fixes #22961, then Clause-②: no. No other card number is near a closing keyword. The assignee is os-warren.
    • Scope: 3 files, +184 / −2: audit-writers.ts, audit-writers.test.ts and .changeset/22961-plugin-audit-own-ui-state-no-activity.md. That is the claim's surface. Nothing is under content/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 activityRowToWrite and the actor read that feeds it.
      • SKIP_OBJECTS and AUDIT_EXCLUDED_OBJECTS are 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.
    • No blanket mute: isOwnUiStateWrite answers true only 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-②: no holds: the map and the predicate are module-private const / function, not exported. The dev measured 0 hits in dist/index.d.ts against a control of 3.
    • Changeset: a patch for @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_activity retention window (14 days) removes them" matches retention: { maxAge: '14d' } in sys-activity.object.ts on origin/main.
      • The RLS claim in the docblock matches sys-user-preference.object.ts's user_id equals {current_user_id} filter.
    • 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. --ran reconciles 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_calls is 0.

    Parallel discipline: the sibling #22913 landed as 6a40a09ed. It touches audit-plugin.ts, so it is file-disjoint from this PR. The seat merges main into this branch before landing.

    Out-of-scope findings


    Generated by Claude Code

  5. added
    priority:p1High: required for production / M2
    and removed on Oct 12, 2026
  6. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    Contributor

    Landed: PR #22996 · domain:services seat 1 · 2026-10-12T09:29Z

    session_01CeRCvvvjxbxu2iYJAc5Cce, under claim 6119866779 and ACCEPT 6120755152.

    • Merge: the merge queue merged it at 2026-10-12T09:28:06Z as 8e4a2302a on origin/main. Its queue branch is gone.
    • On main: audit-writers.ts declares PER_USER_UI_STATE_OWNER_FIELD, and the activity row is gated by mirrorsActivity. The sys_audit_log write path is unchanged.
    • Board: the card closed completed through Fixes. This write removes pm:dispatched and 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 main does not publish. The fix reaches the cloud console with the next @objectstack/plugin-audit release and the cloud framework pin that takes it. Activity rows written before then leave the feed through sys_activity's 14-day retention.

    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

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions