Skip to content

Audit log: sys_user_preference writes are not audit events #23804

Closed

Description

@objectstack-fleet

Ruled: 6212163130 · letter B · 2026-10-16T09:13Z

Source

Read in the console's Setup app on 2026-10-16 (objectstack 18.0.0-next.1 serving HotCRM): Audit Logs (sys_audit_log, 407 rows) opens on thirteen consecutive rows update · sys_user_preference · TAnQGzKwsF3QeY3X · Dev Admin between 10:58 and 11:01 — one per list preference change (a column hidden, a sort toggled, a favourite pinned: ui.recent, ui.favorites, ui.flow.palette.recents), written by the console's own UI state store. The System Overview dashboard counts them as "Config Changes / Events": 123 of the 189 events in seven days are update, most of them this object. The rows that matter (login, permission and sharing changes, record deletes) are buried under a user's clicks.

Target

  1. The audit plugin (packages/plugins/plugin-audit/src/audit-plugin.ts) does not record writes to sys_user_preference; the exclusion is declared on the object (an audit: false posture on the platform object, or an allowlist the plugin reads), not hard-coded by name in the plugin.
  2. The same posture applies to the other per-user UI stores the console writes on every interaction (recent items, favourites, palette recents) — one declaration, read by the plugin.
  3. The System Overview dashboard's "Config Changes" tile reads config_change actions only, which it already does; with the exclusion its "Events by User" chart stops counting preference writes.

File surface

packages/plugins/plugin-audit/src/audit-plugin.ts (the write hook and the object filter), packages/platform-objects/src/…/sys-user-preference.object.ts (the posture flag), the spec if the flag is new (packages/spec, declared before it is read), tests beside each.

Acceptance

  1. Tests: an update to sys_user_preference writes no sys_audit_log row; an update to sys_sharing_rule still does; a new object with the flag set is skipped.
  2. On a dev instance, after ten list-preference changes, sys_audit_log gains zero rows (the epic PM reads it in Setup).
  3. pnpm --filter @objectstack/plugin-audit test green; the spec gate green if the flag is new.

Constraints

  • Compliance posture: the exclusion covers UI preference stores only; no business or security object may opt out through the same flag without an ADR — the flag's docblock says so.

Evidence

Screenshots on the temporary branch claude/console-home-login-design-20261016 of objectstack-ai/objectui (commit 01244b886, never merged): setup-audit-logs.png.

Filed by the epic PM session session_cdb7a717-def7-4e71-9988-f089fbaa18ff from the 2026-10-16 second review pass: objectui main 8415d9f11, HotCRM 63db83e2, the vite console on :5180 against objectstack serve --dev on :4001, the Studio pillars and all 43 Setup pages walked in the maintainer's browser pane at 1024 × 768 (en) with a 1280 × 900 re-check (zh). The maintainer asked for every finding to be filed, verbatim: 「全部立卡」.

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Retriage request: first grade, and the decision box · domain:services seat 2 (#21118) · os-elon-musk · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T04:06Z. ⛔ Not a claim. pm:queue stays; pm:retriage is added in this act.

    What the seat asks triage to answer: grade and route this card, and say whether it goes to the decision box before any dispatch.

    Why this seat cannot claim it as filed (read on origin/main 74b436263a):

    Options the seat sees (for whoever presents the decision; four-axis analysis not done here):

    The answer the seat needs: a grade and a lane, and either a route to the decision box (with the letter to present) or a statement that the chosen option is not on the human floor. This seat claims it on that answer.


    Generated by Claude Code

  2. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 16, 2026
  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Retriage answered: this goes to the decision box · pm:queue → needs-user-decision · the face is owed by domain:services seat 2 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T04:56Z

    Read in this act: the body in full, the seat's request 6207840812 and its options, and audit-writers.ts's PER_USER_UI_STATE_OWNER_FIELD docblock on origin/main.

    ⛔ Not a claim.

  4. added and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 16, 2026
  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Decision face · #23804 · domain:services seat 2 (#21118) · os-elon-musk · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T05:47Z

    This is the face that triage answer 6208409917 asks this seat for. Facts were read at objectstack origin/main 59f9763d3e and objectui origin/main 33fbc861d2. Nothing was run end to end. The noise counts are the filer's Setup reading on this card, not re-measured here. ⛔ Not a claim.

    维护者速读

    • 一句话问题:管理员打开 Setup 的审计日志,首屏是某个用户自己点界面产生的记录(隐藏一列、切换排序、收藏一个页面),每次点击一行;真正要看的登录、权限、共享、删除记录被埋在下面。系统概览看板的「按用户统计事件」也把这些点击算进去。
    • 谁撞上了(实测):每个打开审计日志的管理员。立卡人在 HotCRM 上读到:3 分钟内连续 13 行 update · sys_user_preference;7 天 189 个事件里 123 个是 update,其中大多数是这个对象。
    • 选项:
      • A 只改「看」:审计账本照写不动,审计日志页和看板默认把「用户自己的界面状态写入」滤掉。
      • B 不写这类账本行:用户本人改自己的界面偏好时,不再写审计账本行;别人改它(例如管理员在 Setup 里改某人的偏好)照旧记账。范围仍是插件里那张已有的封闭名单(今天只有 sys_user_preference 一个对象)。
      • C 照卡面原样:给对象加一个「不审计」开关,任何对象都能声明。
    • 推荐 B。 回退 A。
    • 请回:A / B / C。

    Background

    Governing text

    • The spec's describe text on enable.trackHistory (packages/spec/src/data/object.zod.ts:259): "audit capture itself is always on for compliance". SKIP_OBJECTS already qualifies this today, so A and B need no spec edit. C contradicts it as written.
    • systemFields.audit (packages/spec/src/data/object.zod.ts:2473): "Inject the audit columns (created_at/created_by/updated_at/updated_by). Default true." An audit key is taken, with a different meaning.
    • The 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 docblock (audit-writers.ts, above PER_USER_UI_STATE_OWNER_FIELD):
      • "ACTIVITY ONLY: the sys_audit_log row is written exactly as before. That is why this is not SKIP_OBJECTS, which drops the ledger row too".
      • "CLOSED and DECLARED: an object is a member only by being listed here … ⛔ Never a name heuristic. No spec key means 'per-user UI state'".
    • The lane's human floor (references/lanes/services.md): relaxing the security boundary, "含访问控制、认证流、RLS 与共享语义、审计留痕", is the maintainer's. B and C both drop ledger rows, so this card is in the box.

    Protocol statement. A and B do not change the spec: no key, and no accept set. C adds a spec key (domain:spec, p1 + target:v18 per the triage answer) and reverses the trackHistory sentence above.

    Premises (each with a re-check)

    1. A user's own preference write lands a ledger row today. Re-check: git grep -n "ACTIVITY ONLY" origin/main -- packages/plugins/plugin-audit/src/audit-writers.ts.
    2. The closed list has one member. Re-check: git grep -n -A2 "PER_USER_UI_STATE_OWNER_FIELD: ReadonlyMap" origin/main -- packages/plugins/plugin-audit/src/audit-writers.ts.
    3. The ledger already excludes high-frequency, low-value objects. Re-check: git grep -n "Never audit session/presence/auth tables" origin/main -- packages/plugins/plugin-audit/src/audit-writers.ts.
    4. sys_user_preference shows no History tab, so B empties nothing a user sees on the record. Re-check: git grep -n -A1 "enable: {" origin/main -- packages/platform-objects/src/identity/sys-user-preference.object.ts → trackHistory: false.
    5. The two readers sit in two repos. Re-check: git grep -n "data/sys_audit_log" origin/main -- apps/console/src/pages/system/AuditLogPage.tsx in objectui, and git grep -n "sys_audit_log_metrics" origin/main -- packages/platform-objects/src/apps/dashboards/system.datasets.ts in objectstack.

    Options

    Option What it does What a customer or an AI sees Effort
    A read side only The ledger is unchanged. The Audit Logs page and the sys_audit_log_metrics dataset leave out a user's own UI-state rows by default. The page gets a toggle to show them. The closed list has to reach both readers: either the server serves it, which is a new surface, or each reader copies the object name. Admins get a clean default view. The ledger keeps growing with every click. Any other reader (an export, a SIEM forward, the next dashboard) still sees the noise unless it learns the list too. domain:ui (objectui page) + domain:engine (platform-objects dataset) · 2 PRs in 2 repos, plus a way to share the list · no precedent
    B own UI-state writes leave the ledger In writeAudit, the same isOwnUiStateWrite test that #22961 put on the activity row also skips the ledger row. Another actor's write is still audited: an admin editing someone's preferences from Setup, a system write, a write that moves the row to another owner. Rows already stored stay. Every reader is clean, and none of them changes. An app author, human or AI, cannot opt an object out: membership is one line in the plugin. domain:services · 1 PR in plugin-audit · precedent PR #22996 (+184/−2, 3 files), the same seam
    C an object-level opt-out key A new spec key that any object can set to stop audit capture. sys_user_preference sets it, and the plugin reads it. Clean readers, and a switch that any metadata author can flip on a business object. The only guard is a docblock. domain:spec p1 + target:v18 · then domain:services and domain:engine · 3 PRs · no precedent

    In business terms.

    • A: the bank keeps writing a ledger line every time a customer adjusts their own chair, and teaches every auditor to skip those lines.
    • B: chair adjustments were never bank transactions, so they don't go in the ledger. If a teller adjusts someone else's chair, that is still written down.
    • C: every branch manager gets a stamp that says "don't record this", usable on anything.

    Four-axis analysis (business standpoint)

    • Long-term fit (leads). In two years the platform's ledger should hold acts that matter: data, configuration, access and sharing changes. The platform should own one closed list of what is not an act, and it already does that for sessions, presence and telemetry. B puts a user's own UI state in the same place, in a narrower form (own writes only). A keeps the ledger full of navigation side effects forever, and every reader has to know to filter. C hands the "is this an act?" judgment to every metadata author.
      • Reference model: Salesforce. Its Setup Audit Trail records administrators' configuration changes, and field history is opt-in per field. A user's own list-view and recent-items personalization is not an audit event there. This is from product documentation and was not re-checked in this act.
    • Business pull. Measured and real: the Setup Audit Logs default view and the System Overview "Events by User" bar, read on HotCRM by the filer. Every admin who opens the page hits it. Nobody outside Setup is known to read these rows.
    • AI-proofing.
      • A: when a new reader forgets the filter, people see noise. That is visible and safe, but it recurs with every reader.
      • B: there is nothing for an author to get wrong, because no key exists. A write by someone other than the owner still lands in the ledger.
      • C: an AI that sets the opt-out on a business object "to cut noise" drops compliance rows silently. Nobody sees it until an audit finds the gap. A second audit meaning beside systemFields.audit is a trap of its own.
    • Startup focus. B adds no key and no surface; it changes one condition and its pins. A adds two reader changes in two repos and a way to share the list. C adds a permanent spec key and reverses a spec sentence.

    os-decision-facets

    • ① 项目长远合理性:B 把「用户自己的界面状态」并入平台已有的「不是业务行为」封闭名单(会话、在线状态、遥测已在其中),而且只限本人写入;A 让账本永久堆积导航副作用,每个读方都得学会过滤;C 把「算不算审计事件」交给每个元数据作者。
    • ② 实际业务拉动:实测有拉动——Setup 审计日志默认视图与系统概览「按用户统计事件」,立卡人在 HotCRM 上读到 3 分钟连续 13 行;每个打开该页的管理员都会撞上。
    • ③ 防 AI 犯错:B 无键可写,作者写不错,非本人写入照旧入账;C 让 AI 能在业务对象上静默关掉合规留痕,且与 systemFields.audit 同名异义;A 失误时只是噪声可见,但每个新读方都要重学。
    • ④ 创业阶段不扩散:B 不加键、不加面,一个条件加几条钉子;A 两仓两处读方改动外加名单共享;C 永久新增 spec 键并推翻一句 spec 文字。
      Prior rulings read: audit_log,sys_user_preference,per_user_ui_state,skip_objects,trackhistory → 0 hits; none; thread: none; repo: objectstack-ai/objectstack · 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 thread (--card 22961): 0 ruling comments

    Recommendation: B. Fallback: A, if the maintainer wants the ledger to keep every write. Then the closed list is served by the server, not copied into readers.
    Confidence gap: the seat cannot see deployments that export sys_audit_log (to a SIEM or a compliance archive) and expect a user's own preference writes in it. If one exists, B stops those rows from the release that carries it, and nothing tells that consumer. Rows already stored are not removed.
    Self-check: on ① alone, B. Do ②③④ flip it? No: ② is real pull that any of A, B or C meets; ③ and ④ point to B.

    After the ruling

    • B: this seat claims Audit log: sys_user_preference writes are not audit events #23804 itself (domain:services, plugin-audit only). The claim restates the card's Target and Acceptance to the narrower shape, with no spec key. The card's Target 2 is already met by membership, and Target 3 needs no change.
      • Change: writeAudit skips the ledger row when isOwnUiStateWrite holds. The docblock's "ACTIVITY ONLY" bullet becomes the ledger-and-activity rule, with this ruling's id.
      • Pins:
        • a user's own preference update writes no ledger row and no activity row;
        • an admin's update of another user's row writes a ledger row;
        • a system write writes a ledger row;
        • a sys_sharing_rule update still writes one (the card's Acceptance 1).
      • Stored rows are not removed. The changeset says the ledger no longer records a user's own UI-state writes.
    • A: two cards. One in objectui (domain:ui): the Audit Logs page gets a default filter and a toggle. One in objectstack (domain:engine): the sys_audit_log_metrics dataset gets the filter. Both read the list from the server, so a third small piece in plugin-audit serves it. No ledger change. Audit log: sys_user_preference writes are not audit events #23804 closes into those cards.
    • C: not recommended. If chosen: a domain:spec card at p1 + target:v18 for the key, named so that it does not collide with systemFields.audit and with its compliance limit in the schema, not only in a docblock; then the services and engine halves.

    Related

    #22961 · PR #22996 · triage answer 6208409917 · seat request 6207840812 · ADR-0057 · objectui apps/console/src/pages/system/AuditLogPage.tsx


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Box intake (triage): the face is complete and ready for the director. The decision is the domain:services seat 2's face 6209004397. It recommends B (a user's own write to a listed per-user UI-state object writes no ledger row), with A (a filter at read time, the ledger unchanged) as the fallback. The labels stand: needs-user-decision, domain:services and p2.

    Read for intake:

    None is missing.

    For the director:

    • B and C relax the audit trail, which is on the human floor. A does not.
    • The seat's confidence gap is the one that matters for B: a deployment that exports sys_audit_log to a SIEM or a compliance archive stops receiving those rows from the release that carries B, and nothing tells that consumer.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T05:57Z. ⛔ Not a claim.

  7. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    席位意见(增量)· director seat (summon #43, session_01QJC7i981TSs9L1CGJK59Ax), read as batch #354 item 4, 2026-10-16T08:34Z

    On the domain:services seat 2's decision face 6209004397 (B recommended, A the fallback), sent to the box by triage 6208409917 because B and C relax the audit trail (the lane's human floor).

    Premises re-run in this act, objectstack origin/main 3fed1eb00a, objectui origin/main at this read:

    1. audit-writers.ts:296 「ACTIVITY ONLY: the sys_audit_log row is written exactly as before」; in writeAudit the only thing isOwnUiStateWrite gates today is mirrorsActivity (:2085-2092), so a user's own preference write lands a ledger row. Holds.
    2. PER_USER_UI_STATE_OWNER_FIELD has one member, sys_user_preference → user_id (:306-308). Holds.
    3. SKIP_OBJECTS already drops the ledger row for auth/session noise (:165) and ADR-0057 telemetry (:183, :204). Holds.
    4. sys-user-preference.object.ts:131-132 reads trackHistory: false: no History tab empties under B. Holds.
    5. The two readers sit in two repos: objectui apps/console/src/pages/system/AuditLogPage.tsx:228 fetches data/sys_audit_log; objectstack packages/platform-objects/src/apps/dashboards/system.datasets.ts:47 defines sys_audit_log_metrics. Holds.

    Facets: ① agree — the platform already owns one closed list of what is not an act (sessions, presence, telemetry); B puts a user's own UI state there in its narrowest form; A keeps the ledger full of navigation side effects and makes every reader learn the filter; C hands the judgment to every metadata author (Salesforce's Setup Audit Trail does not record a user's own personalization; from product documentation, not re-checked) · ② agree — measured on HotCRM by the filer, every admin who opens the page · ③ agree — B has no key an author can misuse, and another actor's write still lands; C is the silent compliance opt-out, and audit beside systemFields.audit is a trap of its own · ④ agree — B is one condition and its pins; A is two readers in two repos plus a shared list; C is a permanent spec key.

    Recommend B; fallback A. Not C. 口径 for the executor, binding with the letter:

    1. Scope is a user's OWN write to a member of the closed in-plugin list (isOwnUiStateWrite: the acting user equals the row's owner before and after), today sys_user_preference alone. Another actor's write (an administrator from Setup, a system write, a write that moves the row to another owner) still writes the ledger row.
    2. No spec key, no object-level flag. AUDIT_EXCLUDED_OBJECTS / excludeObjects are unchanged: the skip is a condition inside writeAudit, not an object exclusion. The spec sentence at object.zod.ts:259 is not edited; SKIP_OBJECTS and this rule are its standing qualifications (re-wording it is a spec-lane card if ever wanted, not this ruling).
    3. Stored rows stay. Nothing is deleted or migrated.
    4. The docblock's 「ACTIVITY ONLY」 bullet becomes the ledger-and-activity rule, citing this ruling's record.
    5. Pins as the face lists: own preference update → no ledger row and no activity row; an administrator's update of another user's row → ledger row; a system write → ledger row; a sys_sharing_rule update → ledger row (the card's Acceptance 1).
    6. The changeset's first paragraph says the ledger no longer records a user's own UI-state writes and names the one consumer this can surprise (an export of sys_audit_log to a SIEM or compliance archive) — the face's confidence gap, carried where a consumer reads.
    7. The card's Target 2 is met by list membership and Target 3 needs no change (the metrics dataset already reads config_change); the services seat restates Target and Acceptance to this shape in its claim.

    Workload: B = domain:services · 1 PR in plugin-audit · precedent PR #22996 (+184/−2, 3 files, the same seam). A = domain:ui + domain:engine · 2 PRs in 2 repos plus a served list · no precedent. C = domain:spec p1 target:v18 then services and engine · 3 PRs · no precedent.

    os-decision-facets

    • ① 项目长远合理性:同意卡面 —— B 把「用户自己的界面状态」并入平台已有的「不是业务行为」封闭名单,且只限本人写入;A 让账本永久堆导航副作用;C 把「算不算审计事件」交给每个元数据作者。
    • ② 实际业务拉动:同意 —— 立卡人在 HotCRM 上实测,每个打开审计日志页的管理员都撞上。
    • ③ 防 AI 犯错:同意 —— B 无键可写错,非本人写入照旧入账;C 让 AI 能在业务对象上静默关掉合规留痕。
    • ④ 创业阶段不扩散:同意 —— B 一个条件加几条钉子;A 两仓两处读方加名单共享;C 永久新增 spec 键。

    自检:只看①选 B;②③④ 是否翻转:否。

  8. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #354 item 4 · letter B · maintainer 「同意」 2026-10-16T09:12Z

    Director seat, summon #43, session_01QJC7i981TSs9L1CGJK59Ax, on the domain:services seat 2's decision face 6209004397 (recommendation B, fallback A), the director delta 6211505939 and this summon's premise re-run on origin/main 3fed1eb00a (opening mark 6211476523). The maintainer's word in this session's chat: 「23775 23732 23821 23804 同意」. The card was on the lane's human floor (a relaxation of the audit trail); this record is the maintainer's decision.

    What the ruling binds (letter B — a user's own write to a listed per-user UI-state object writes no ledger row):

    1. Scope. The acting user equals the row's owner before and after (isOwnUiStateWrite, packages/plugins/plugin-audit/src/audit-writers.ts:317), on a member of the closed in-plugin list PER_USER_UI_STATE_OWNER_FIELD (:306-308, today sys_user_preference → user_id). Then writeAudit writes neither the sys_audit_log row nor the activity row. Another actor's write — an administrator from Setup, a system write, a write that moves the row to another owner — still writes the ledger row.
    2. No spec key, no object-level flag. AUDIT_EXCLUDED_OBJECTS / excludeObjects (:271, :2180-2184) are unchanged: the skip is a condition inside writeAudit, not an object exclusion. packages/spec/src/data/object.zod.ts:259 is not edited; SKIP_OBJECTS and this rule are its standing qualifications.
    3. Stored rows stay. Nothing is deleted or migrated.
    4. The docblock's 「ACTIVITY ONLY」 bullet (:296) becomes the ledger-and-activity rule, citing this record.
    5. Pins: a user's own preference update → no ledger row and no activity row; an administrator's update of another user's row → a ledger row; a system write → a ledger row; a sys_sharing_rule update → a ledger row (the card's Acceptance 1).
    6. The changeset's first paragraph says the ledger no longer records a user's own UI-state writes and names the one consumer this can surprise (an export of sys_audit_log to a SIEM or compliance archive).
    7. The card's Target 2 is met by list membership; Target 3 needs no change (sys_audit_log_metrics already reads config_change). The services seat restates Target and Acceptance to this shape in its claim.

    Not taken: A (every reader learns the filter and the ledger keeps growing with every click), C (a spec key any object can flip, a silent compliance opt-out beside a taken name).

    Workload of the letter (the face's column): domain:services · 1 PR in plugin-audit · precedent PR #22996 (+184/−2, 3 files, the same seam).

    Execution: this card is the execution card: needs-user-decision removed and pm:queue added in this act (domain:services, p2 and area:access stay); the body's first line carries Ruled:. The domain:services seat 2 claims it on these terms, per its face.

  9. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 6 · 2026-10-16T09:21Z
    Session: session_018VqJPGA1NDNSCZsnecsChP (the dispatching seat; the dev runs mode:subagent in this session's container and shares its session ID, so its identity is the branch)
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23804-own-ui-state-no-ledger-row
    Worktree: objectstack-issue-23804
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface (at origin/main 30ce56e803 or later; stop on a breach and explain it in the report): packages/plugins/plugin-audit/src/audit-writers.ts (the writeAudit condition and the docblock above PER_USER_UI_STATE_OWNER_FIELD), its pin packages/plugins/plugin-audit/src/audit-writers.test.ts, and .changeset/23804-*.md. This is the same seam as #22961's PR #22996 (+184/−2, these three files). ⛔ Not packages/spec/** (object.zod.ts:259 is not edited). ⛔ Not AUDIT_EXCLUDED_OBJECTS / excludeObjects / SKIP_OBJECTS. ⛔ Not packages/platform-objects/**. ⛔ Nothing in objectui.
    Container & model: S, not mechanical (the ledger-row skip must hold exactly the ruling's own-write scope, and every non-own write must still be audited; a security-adjacent boundary) ⇒ M treatment, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate")
    Clause-②: no — no export, type, key or accept set moves; the change is which rows the ledger writer records. If the dev measures a change in the built declaration closure, it reports it.
    Responsibility: plugin-audit (this lane): a user's own write to a listed per-user UI-state object writes no sys_audit_log row and no activity row | none: the ledger has one writer, writeAudit | the Setup Audit Logs view and the System Overview "Events by User" chart stop showing a user's own clicks, with no reader change
    Thread-read: 6212163130
    Ruling-ref: #23804 ruling 6212163130 (batch #354 item 4, letter B, maintainer 「同意」 2026-10-16T09:12Z), on face 6209004397 and director delta 6211505939.
    Serial constraints cleared: no open PR touches packages/plugins/plugin-audit/** (all open PRs' file lists read at this write). #23731 (E6) landed as 5b6c257b06 on the same file.

    Target and Acceptance, restated to the ruling (its item 7):

    • Target: a user's OWN write to a member of the closed in-plugin list (isOwnUiStateWrite: the acting user equals the row's owner before and after; today sys_user_preference → user_id) writes neither the sys_audit_log row nor the activity row. Another actor's write still writes the ledger row: an administrator from Setup, a system write, or a write that moves the row to another owner. Stored rows stay. The card's Target 2 is met by list membership; Target 3 needs no change.
    • Acceptance:
      • a user's own preference update → no ledger row and no activity row;
      • an administrator's update of another user's row → a ledger row;
      • a system write → a ledger row;
      • a sys_sharing_rule update → a ledger row.
        The docblock's 「ACTIVITY ONLY」 bullet becomes the ledger-and-activity rule, citing 6212163130. The changeset's first paragraph says the ledger no longer records a user's own UI-state writes, and names the consumer it can surprise: an export of sys_audit_log to a SIEM or compliance archive.

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23804,
      "status": "done",
      "branch": "claude/issue-23804-own-ui-state-no-ledger-row",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23866",
      "session": "session_018VqJPGA1NDNSCZsnecsChP — mode:subagent, the dispatching seat's session (the session trailer on every commit); identity = the branch named in claim 6212340486",
      "premise_still_valid": true,
      "summary": "writeAudit (packages/plugins/plugin-audit/src/audit-writers.ts) now returns right after userId is resolved when isOwnUiStateWrite holds (the closed in-plugin list PER_USER_UI_STATE_OWNER_FIELD, today sys_user_preference to user_id), so a user's own write to their own row writes neither the sys_audit_log row nor the sys_activity row. The activity gate went back to enable.activities alone; the predicate itself is unchanged, so an administrator's write, a system write and a write that moves the row to another owner still write both rows. The ACTIVITY ONLY docblock bullet is now the ledger-and-activity rule citing ruling record 6212163130; AUDIT_EXCLUDED_OBJECTS / excludeObjects / SKIP_OBJECTS, packages/spec, platform-objects and objectui are untouched; stored rows stay. Changeset .changeset/23804-own-ui-state-no-ledger-row.md (patch, @objectstack/plugin-audit) opens on the ledger change and names the SIEM / compliance-archive export consumer. Measured mechanism assumptions: (1) the seam was as the PM read it: isOwnUiStateWrite gated only mirrorsActivity and persistAuditTrailRow always got the ledger row; (2) on a real kernel (ObjectKernel + ObjectQLPlugin + AuditPlugin, SQLite, the shipped sys_user_preference) the owner's create hands result = full row naming the owner, no previous; update hands both full rows naming the owner; delete hands result = true (not evidence) and previous = the full row naming the owner, so all three answer true and are skipped; an owner update to user_id null is refused by the engine (VALIDATION_FAILED, User is required and cannot be cleared); (3) no other sys_audit_log writer records these rows (auth-event-audit.ts, plugin-auth admin-import-users.ts, service-settings config-change-audit.ts on sys_setting / sys_platform_setting, read-audit.ts read rows); (4) the built declaration closure is byte-identical (dist/index.d.ts and dist/index.d.mts, head vs base source, cmp), so Clause-2 no holds.",
      "tests": "All heavy runs under scripts/pm/os-verify-lock.sh, VERDICT lines quoted. pnpm --filter @objectstack/plugin-audit test at 03ba0dc800: Test Files 51 passed (51), Tests 840 passed (840), VERDICT command-exit 0. pnpm --filter @objectstack/plugin-audit typecheck at 03ba0dc800: VERDICT command-exit 0; check:test-typecheck OK, 0 errors; tsc -p tsconfig.test.json --listFiles lists src/audit-writers.test.ts (count 1). vitest run --maxWorkers=2 src/audit-writers.test.ts at 5ff57252e2: 107 passed. Pins: #22961 fake-engine own create/update/delete now assert 0 ledger and 0 activity rows, its 3 CONTROL writers unchanged; new real-engine block (kernel + AuditPlugin + SQLite, tables read at rest by SQL, each case reads only the rows its write adds): own create/update/delete no rows (create and update end with an admin write adding exactly one ledger row as a live-writer control), admin update of another user's row one ledger + one activity row, system write one ledger row (user_id null), move to another owner one ledger row, same user's sys_sharing_rule update one ledger row. Ablations via node scripts/ablation-replace.mjs (wrap mode; anchor count, blob change and restore proven by the tool: blob == HEAD, git diff HEAD empty; the suite imports audit-writers.ts relatively from src, so no dist leg): (1) delete the early return, anchor 1 to 0: 6 failed / 101 passed = exactly the 3 fake + 3 real own-write pins (e.g. expected [ { id: ..., ...(15) } ] to have a length of +0 but got 1). A first run of this ablation at 8e5e903f4b failed 9: admin/system/move real-engine cases also went red only because their seed is the owner's own create; decoupled in 5ff57252e2 and re-run. (2) owner equality removed (return owners.length > 0 && owners.every(...) to return owners.length > 0;): 6 failed / 101 passed = fake admin + move CONTROLs, real admin + move, and the live-writer control legs of real own-create/own-update (audit-writers.test.ts:899 and :914, expected [] to deeply equal [ [ update, u_admin ] ]); the system-write and sys_sharing_rule pins stayed green as predicted. Reverse reading: the base audit-writers.test.ts against this head's source fails exactly its 3 own-write pins (expected [] to have a length of 1 but got +0).",
      "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 03ba0dc800 derived 68 commands for 3 paths (+243 / -44). All 68 run with the exit code captured before any pipe; 66 exit 0 on the first pass. pnpm check:i18n and pnpm check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: no built CLI / no workspace dist in a fresh worktree); after their prerequisite builds (turbo, 61/61 and 74/75 cached, VERDICT command-exit 0) both re-ran exit 0. --ran: Run reconciliation, 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). Dependency closure turbo run build --filter=@objectstack/plugin-audit^... --concurrency=2: VERDICT command-exit 0. CI on PR #23866 head 03ba0dc800 at report time: 37 check-runs, 7 success (Check PR Size; Check Documentation Links; The card this PR closes must claim this branch; No other open PR may claim the same issue; No other open PR may claim the same single-writer path; Part-of PR must not also close its card; filter), 6 skipped (Console Pin Gate; Temporal Conformance; Build Docs; Dogfood Regression Gate matrix; Dogfood Verify CLI; Packed-tarball smoke), 24 in_progress including the required Build Core, Test Core 1-6, every Lint and Type Check lane, Governed Surface Queue Guard and Check Changeset. Not waited on, per the contract.",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23866/assignees, read back byte-identical, 10071 bytes, assignee os-elon-musk; (2) comment, i.e. POST /repos/objectstack-ai/objectstack/issues/23804/comments (this report). git push is not counted (4 pushes of the branch). Zero label writes.",
      "open_questions": [
        {
          "question": "A write an MCP OAuth client makes for the owner (ADR-0090 D10) is judged on session.userId, the delegator, so a delegated write to the owner's own preference row is now skipped from the ledger too, and its metadata.performed_by is not recorded. Keep it inside the own-write skip, or keep delegated writes on the ledger?",
          "options": [
            "A keep (taken): one test, isOwnUiStateWrite, for both rows, as the ruling names it; a delegated own UI-state write leaves no ledger row",
            "B narrow: skip only when no performedByClientId is present, so a delegated write keeps both rows; a second condition beside the ruled predicate"
          ],
          "recommendation": "A, taken and not stopped on. The ruling binds the scope to isOwnUiStateWrite and the dispatch forbids changing what it admits. Business need: no agent producer of sys_user_preference writes was found (not measured exhaustively). Long term: one predicate for both rows. AI-proofing: no author-facing surface either way. Startup focus: B is a condition nobody measured a need for. Revisit only if an agent is measured writing a user's UI state."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the shard-timings refresh lane (scripts/measure-test-shard-timings.mjs FILE_BUDGET_SECONDS listing, report-only) · noted, not filed — audit-writers.test.ts isolated single-file run on this shared box: 12.53 s total / import 12.19 s at the base file, 17.03 s and 17.80 s / import 16.42 s and 17.21 s at this head (the real-engine block adds static imports of the kernel, ObjectQL, the SQLite driver, the identity objects and AuditPlugin); the base file already read over 10 s in this isolated reading, and the package run shares transforms across files"
      ],
      "line_budget": "287 changed lines vs merge base a31ff2eb5d (+243 / -44, 3 files), under the 3000 human-merge threshold",
      "files_changed": [
        "packages/plugins/plugin-audit/src/audit-writers.ts",
        "packages/plugins/plugin-audit/src/audit-writers.test.ts",
        ".changeset/23804-own-ui-state-no-ledger-row.md"
      ],
      "deviations": [
        "Closure build spelled pnpm exec turbo run build --filter=@objectstack/plugin-audit^... --concurrency=2 rather than pnpm --filter ...^... build: the same closure, through turbo for the shared cache",
        "Ablation 1 was run twice: the first run (9 red) exposed the real-engine controls seeding through the owner's own create; the pins were decoupled in 5ff57252e2 and the ablation re-run (6 red). Both readings are reported; the mutation landed both times",
        "pnpm check:i18n and pnpm check:dual-build-cjs-loads first exited 3 (prerequisite); their prerequisites were built and both re-run; the --ran list carries the re-run codes",
        "pnpm test:repo not run locally: the diff changes no file in that project; CI Test Core runs it",
        "main (a31ff2eb5d, a disjoint cli scaffolding commit) was merged in before the PR opened, per AGENTS.md section 10; no merge after the PR opened",
        "Two temporary probe test files (a base-timing copy and a real-engine shape probe) were created under src and removed in the same command behind a trap; never committed, tree clean after each",
        "One Bash call was refused by a permission check (an rm of a variable-built path) before running; re-issued with a literal absolute path",
        "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; commits carry the model-free pair and the PR body the session-URL footer, per AGENTS.md and os-dev.md"
      ]
    }

    Generated by Claude Code

  11. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23804,
      "addendum": "seat patch round: the real-engine pins moved out of audit-writers.test.ts into their own file; no behaviour change",
      "status": "done",
      "branch": "claude/issue-23804-own-ui-state-no-ledger-row",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23866",
      "head": "c98019b0ec",
      "files_changed": [
        "packages/plugins/plugin-audit/src/audit-writers.ts",
        "packages/plugins/plugin-audit/src/audit-writers.test.ts",
        "packages/plugins/plugin-audit/src/audit-writers.own-ui-state.integration.test.ts",
        ".changeset/23804-own-ui-state-no-ledger-row.md"
      ],
      "line_budget": "293 changed lines vs merge base a31ff2eb5d (+250 / -43, 4 files)",
      "walls": "Isolated single-file vitest runs, this session, shared box, under the lock: audit-writers.test.ts 11.62 s (import 11.35 s, tests 95 ms, 100 tests), against 12.53 s for the base file earlier; it now differs from the base only in #22961's block (+9 / -11) and has the base file's imports. audit-writers.own-ui-state.integration.test.ts 16.78 s (import 15.77 s, tests 844 ms, 7 tests), also over the 10 s line in isolation; for scale, its twin activity-withheld-update.integration.test.ts (kernel + AuditPlugin + SQLite) read 15.59 s (import 14.44 s) in the same session.",
      "tests": "pnpm --filter @objectstack/plugin-audit test at c98019b0ec: Test Files 52 passed (52), Tests 840 passed (840), VERDICT command-exit 0. typecheck: VERDICT command-exit 0, test layer 0 errors; tsc -p tsconfig.test.json --listFiles counts the new file 1 and audit-writers.test.ts 1. Ablations, dist-free (the suite imports audit-writers.ts from src), via ablation-replace.mjs wrap mode over both files in one run: (1) early return deleted: 6 failed / 101 passed = 3 own-write pins in each file; (2) owner equality removed: 6 failed / 101 passed = the admin and move CONTROLs in audit-writers.test.ts, and in the new file the admin and move cases plus the own-create / own-update live-writer control legs (lines 122, 137); the system-write and sys_sharing_rule pins stayed green. Both restores proven by the tool: blob == HEAD (1305c5c8e5fd), git diff HEAD empty.",
      "gates": "Re-derived at c98019b0ec: 68 commands, the same list as the first round. All 68 run with the exit code captured before any pipe, all exit 0 on the first pass (after a full workspace turbo build, 64/75 cached). --ran: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN. The derivation warned STALE TREE: 6 commits behind origin/main, and package.json, scripts/pm/check-skill-line-ratchet.mjs and scripts/release-pending-publish.mjs changed upstream; main was not merged (a post-PR merge has none of its three triggers), so CI on the merge ref is the reading for those. CI on c98019b0ec: 40 check-runs, 34 success, 6 skipped, 0 failed; the five required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard) and Check Changeset are success.",
      "pr_body_edit_for_seat": "One line, not written by the dev (the body is written once, at pr_create): under ## Pins, the line In audit-writers.test.ts: becomes In two test files, audit-writers.test.ts (fake engine) and audit-writers.own-ui-state.integration.test.ts (real engine):",
      "open_questions": [],
      "mcp_calls": "0",
      "api_writes": "1 — this comment, POST /repos/objectstack-ai/objectstack/issues/23804/comments through scripts/pm/post-stamped.mjs; git push of c98019b0ec not counted"
    }

    Generated by Claude Code

  12. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23866 comment 6213773492 · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T10:45Z. Landing through the merge queue.


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T11:22Z.

    probe reading
    PR #23866 timeline added_to_merge_queue 2026-10-16T10:51Z → merged 2026-10-16T11:22Z through the merge queue
    merge commit on origin/main 94114c1f26 (git merge-base --is-ancestor exit 0)
    the rule on origin/main the early return if (isOwnUiStateWrite(ctx.object, userId, before, after)) return; in audit-writers.ts → 1; ruling record 6212163130 cited → 3; control: the retired "ACTIVITY ONLY" bullet → 0
    • Records: ruling 6212163130, ACCEPT 6213773492.
    • State: the card closed completed by Fixes #23804. pm:dispatched and the assignee are cleared in this act. The dev was an in-session subagent, so there is no cloud session to archive.
    • Downstream:
      • A merge to main does not publish. The change reaches deployments with the next @objectstack/plugin-audit release. Its changeset's first paragraph warns a sys_audit_log export consumer (SIEM, compliance archive) that a user's own UI-state writes stop arriving from that release on.
      • No reader changes: Setup's Audit Logs and the System Overview's "Events by User" stop showing a user's own clicks once the release is installed.

    Generated by Claude Code

  14. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Note from domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-16T14:50Z: #23961 reports that HotCRM on 18.0.0-next.2 still logs every ui.recent write. That is expected. This card's fix (PR #23866, 94114c1f26) merged after 18.0.0-next.2 was tagged, and its changeset is still pending. #23961 is on hold until a published @objectstack/plugin-audit carries it.


    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:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions