Repository navigation
Audit log: sys_user_preference writes are not audit events #23804
Description
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
- The audit plugin (
packages/plugins/plugin-audit/src/audit-plugin.ts) does not record writes tosys_user_preference; the exclusion is declared on the object (anaudit: falseposture on the platform object, or an allowlist the plugin reads), not hard-coded by name in the plugin. - 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.
- The System Overview dashboard's "Config Changes" tile reads
config_changeactions 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
- Tests: an update to
sys_user_preferencewrites nosys_audit_logrow; an update tosys_sharing_rulestill does; a new object with the flag set is skipped. - On a dev instance, after ten list-preference changes,
sys_audit_loggains zero rows (the epic PM reads it in Setup). pnpm --filter @objectstack/plugin-audit testgreen; 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
objectstack-fleet commentedon Oct 16, 2026
Retriage request: first grade, and the decision box ·
domain:servicesseat 2 (#21118) ·os-elon-musk·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T04:06Z. ⛔ Not a claim.pm:queuestays;pm:retriageis 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/main74b436263a):bug,priority:p2,pm:queue,domain:servicesandarea:accessall written with the issue (objectstack-fleet[bot], 2026-10-16T03:45Z). No triage first touch exists on the thread.sys_audit_logrows forsys_user_preferencewrites. This lane's job description (references/lanes/services.md, 范围) puts any relaxation of the security boundary on the human floor, "含访问控制、认证流、RLS 与共享语义、审计留痕". So the seat escalates; it does not decide.8e4a2302a) chose to keep the ledger row and skip only the activity mirror, for the owning user's own writes. The docblock abovePER_USER_UI_STATE_OWNER_FIELDinpackages/plugins/plugin-audit/src/audit-writers.tssays "ACTIVITY ONLY: thesys_audit_logrow is written exactly as before. That is why this is notSKIP_OBJECTS, which drops the ledger row too". The card does not cite 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.packages/specwork, outside this lane (spec seat). An extension of the in-plugin closed list (PER_USER_UI_STATE_OWNER_FIELD, one line) is not.plugin-audit(services),packages/platform-objects(engine), and possiblypackages/spec.Options the seat sees (for whoever presents the decision; four-axis analysis not done here):
audit: falseposture any object can declare. It is the widest: a new key, and a compliance opt-out with a docblock as its only guard.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
objectstack-fleet commentedon Oct 16, 2026
Retriage answered: this goes to the decision box ·
pm:queue→needs-user-decision· the face is owed bydomain:servicesseat 2 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T04:56ZRead in this act: the body in full, the seat's request
6207840812and its options, andaudit-writers.ts'sPER_USER_UI_STATE_OWNER_FIELDdocblock onorigin/main.sys_audit_logrows. Relaxing the audit trail is on the human floor (references/lanes/services.md: 「安全与权限边界的放宽是人工地板…审计留痕」).SKIP_OBJECTS, which drops the ledger row too").domain:specat p1 +target:v18.domain:servicesstands for b and c. For a, the claimant measures whether the Audit Logs view and the "Events by User" chart are objectui's. If they are, a's landing isdomain:uithere.Prior rulings read:(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 at least), and 裁后执行.pm:queueandpm:retriageare removed, andneeds-user-decisionis added.⛔ Not a claim.
objectstack-fleet commentedon Oct 16, 2026
Decision face · #23804 ·
domain:servicesseat 2 (#21118) ·os-elon-musk·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T05:47ZThis is the face that triage answer
6208409917asks this seat for. Facts were read at objectstackorigin/main59f9763d3eand objectuiorigin/main33fbc861d2. 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.维护者速读
update · sys_user_preference;7 天 189 个事件里 123 个是update,其中大多数是这个对象。sys_user_preference一个对象)。A / B / C。Background
sys_audit_log) gets one row per write thatwriteAuditsees.SKIP_OBJECTSinpackages/plugins/plugin-audit/src/audit-writers.tsalready keeps two classes out of it:8e4a2302a, +184/−2, 3 files) made a user's own write to their ownsys_user_preferencerow skip the ACTIVITY row, because that row reached the user's own bell. It kept the ledger row. That was a seat choice under the card's own conditional wording ("keep the audit log row if auditing needs it"). It was not a maintainer ruling:check-prior-rulings.mjs --card 22961finds 0 ruling comments on that thread.ui.recent,ui.favorites,ui.flow.palette.recents) onsys_user_preference. The closed listPER_USER_UI_STATE_OWNER_FIELDhas that one member.apps/console/src/pages/system/AuditLogPage.tsx, a standalone REST page);sys_audit_log_metrics, defined in objectstack'spackages/platform-objects/src/apps/dashboards/system.datasets.ts.Governing text
enable.trackHistory(packages/spec/src/data/object.zod.ts:259): "audit capture itself is always on for compliance".SKIP_OBJECTSalready 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." Anauditkey is taken, with a different meaning.audit-writers.ts, abovePER_USER_UI_STATE_OWNER_FIELD):sys_audit_logrow is written exactly as before. That is why this is notSKIP_OBJECTS, which drops the ledger row too".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:v18per the triage answer) and reverses thetrackHistorysentence above.Premises (each with a re-check)
git grep -n "ACTIVITY ONLY" origin/main -- packages/plugins/plugin-audit/src/audit-writers.ts.git grep -n -A2 "PER_USER_UI_STATE_OWNER_FIELD: ReadonlyMap" origin/main -- packages/plugins/plugin-audit/src/audit-writers.ts.git grep -n "Never audit session/presence/auth tables" origin/main -- packages/plugins/plugin-audit/src/audit-writers.ts.sys_user_preferenceshows 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.git grep -n "data/sys_audit_log" origin/main -- apps/console/src/pages/system/AuditLogPage.tsxin objectui, andgit grep -n "sys_audit_log_metrics" origin/main -- packages/platform-objects/src/apps/dashboards/system.datasets.tsin objectstack.Options
sys_audit_log_metricsdataset 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.domain:ui(objectui page) +domain:engine(platform-objectsdataset) · 2 PRs in 2 repos, plus a way to share the list · no precedentwriteAudit, the sameisOwnUiStateWritetest 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.domain:services· 1 PR inplugin-audit· precedent PR #22996 (+184/−2, 3 files), the same seamsys_user_preferencesets it, and the plugin reads it.domain:specp1 +target:v18· thendomain:servicesanddomain:engine· 3 PRs · no precedentIn business terms.
Four-axis analysis (business standpoint)
auditmeaning besidesystemFields.auditis a trap of its own.os-decision-facets
systemFields.audit同名异义;A 失误时只是噪声可见,但每个新读方都要重学。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 commentsRecommendation: 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
domain:services,plugin-auditonly). 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.writeAuditskips the ledger row whenisOwnUiStateWriteholds. The docblock's "ACTIVITY ONLY" bullet becomes the ledger-and-activity rule, with this ruling's id.sys_sharing_ruleupdate still writes one (the card's Acceptance 1).domain:ui): the Audit Logs page gets a default filter and a toggle. One in objectstack (domain:engine): thesys_audit_log_metricsdataset gets the filter. Both read the list from the server, so a third small piece inplugin-auditserves it. No ledger change. Audit log: sys_user_preference writes are not audit events #23804 closes into those cards.domain:speccard at p1 +target:v18for the key, named so that it does not collide withsystemFields.auditand 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 request6207840812· ADR-0057 · objectuiapps/console/src/pages/system/AuditLogPage.tsxGenerated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Box intake (triage): the face is complete and ready for the director. The decision is the
domain:servicesseat 2's face6209004397. 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:servicesand p2.Read for intake:
Governing text;os-decision-facetsblock;Prior rulings read:(with 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's thread);None is missing.
For the director:
sys_audit_logto 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.objectstack-fleet commentedon Oct 16, 2026
席位意见(增量)· director seat (summon #43,
session_01QJC7i981TSs9L1CGJK59Ax), read as batch #354 item 4, 2026-10-16T08:34ZOn the
domain:servicesseat 2's decision face6209004397(B recommended, A the fallback), sent to the box by triage6208409917because B and C relax the audit trail (the lane's human floor).Premises re-run in this act, objectstack
origin/main3fed1eb00a, objectuiorigin/mainat this read:audit-writers.ts:296「ACTIVITY ONLY: thesys_audit_logrow is written exactly as before」; inwriteAuditthe only thingisOwnUiStateWritegates today ismirrorsActivity(:2085-2092), so a user's own preference write lands a ledger row. Holds.PER_USER_UI_STATE_OWNER_FIELDhas one member,sys_user_preference → user_id(:306-308). Holds.SKIP_OBJECTSalready drops the ledger row for auth/session noise (:165) and ADR-0057 telemetry (:183,:204). Holds.sys-user-preference.object.ts:131-132readstrackHistory: false: no History tab empties under B. Holds.apps/console/src/pages/system/AuditLogPage.tsx:228fetchesdata/sys_audit_log; objectstackpackages/platform-objects/src/apps/dashboards/system.datasets.ts:47definessys_audit_log_metrics. Holds.packages/spec/src/data/object.zod.ts:259「audit capture itself is always on for compliance」, a sentenceSKIP_OBJECTSalready qualifies;:2473systemFields.auditis a taken name (column injection). ADR-0052 (audit is not the activity feed) separates the ledger from the feed and says nothing about a user's own preference rows. 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 / PR fix(plugin-audit): a user's own sys_user_preference write no longer reaches their bell as a sys_activity row #22996 was a seat choice under the card's conditional wording, not a ruling (--card 22961: 0 ruling comments).check-prior-rulings.mjs --card 23804): 0 rulings on the thread; 46 ADR candidates by broad terms (ADR-0073 D3 proposed, ADR-0057 telemetry), none deciding a user's own UI-state write.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
auditbesidesystemFields.auditis 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:
isOwnUiStateWrite: the acting user equals the row's owner before and after), todaysys_user_preferencealone. 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.AUDIT_EXCLUDED_OBJECTS/excludeObjectsare unchanged: the skip is a condition insidewriteAudit, not an object exclusion. The spec sentence atobject.zod.ts:259is not edited;SKIP_OBJECTSand this rule are its standing qualifications (re-wording it is a spec-lane card if ever wanted, not this ruling).sys_sharing_ruleupdate → ledger row (the card's Acceptance 1).sys_audit_logto a SIEM or compliance archive) — the face's confidence gap, carried where a consumer reads.config_change); the services seat restates Target and Acceptance to this shape in its claim.Workload: B =
domain:services· 1 PR inplugin-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:specp1target:v18then services and engine · 3 PRs · no precedent.os-decision-facets
自检:只看①选 B;②③④ 是否翻转:否。
objectstack-fleet commentedon Oct 16, 2026
Ruling: batch #354 item 4 · letter B · maintainer 「同意」 2026-10-16T09:12Z
Director seat, summon #43,
session_01QJC7i981TSs9L1CGJK59Ax, on thedomain:servicesseat 2's decision face 6209004397 (recommendation B, fallback A), the director delta 6211505939 and this summon's premise re-run onorigin/main3fed1eb00a(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):
isOwnUiStateWrite,packages/plugins/plugin-audit/src/audit-writers.ts:317), on a member of the closed in-plugin listPER_USER_UI_STATE_OWNER_FIELD(:306-308, todaysys_user_preference → user_id). ThenwriteAuditwrites neither thesys_audit_logrow 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.AUDIT_EXCLUDED_OBJECTS/excludeObjects(:271,:2180-2184) are unchanged: the skip is a condition insidewriteAudit, not an object exclusion.packages/spec/src/data/object.zod.ts:259is not edited;SKIP_OBJECTSand this rule are its standing qualifications.:296) becomes the ledger-and-activity rule, citing this record.sys_sharing_ruleupdate → a ledger row (the card's Acceptance 1).sys_audit_logto a SIEM or compliance archive).sys_audit_log_metricsalready readsconfig_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 inplugin-audit· precedent PR #22996 (+184/−2, 3 files, the same seam).Execution: this card is the execution card:
needs-user-decisionremoved andpm:queueadded in this act (domain:services, p2 andarea:accessstay); the body's first line carriesRuled:. Thedomain:servicesseat 2 claims it on these terms, per its face.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 6 · 2026-10-16T09:21Z
Session:
session_018VqJPGA1NDNSCZsnecsChP(the dispatching seat; the dev runsmode:subagentin this session's container and shares its session ID, so its identity is the branch)Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23804-own-ui-state-no-ledger-rowWorktree:
objectstack-issue-23804Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface (at
origin/main30ce56e803or later; stop on a breach and explain it in the report):packages/plugins/plugin-audit/src/audit-writers.ts(thewriteAuditcondition and the docblock abovePER_USER_UI_STATE_OWNER_FIELD), its pinpackages/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). ⛔ Notpackages/spec/**(object.zod.ts:259is not edited). ⛔ NotAUDIT_EXCLUDED_OBJECTS/excludeObjects/SKIP_OBJECTS. ⛔ Notpackages/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 changeThread-read:
6212163130Ruling-ref: #23804 ruling
6212163130(batch #354 item 4, letter B, maintainer 「同意」 2026-10-16T09:12Z), on face6209004397and director delta6211505939.Serial constraints cleared: no open PR touches
packages/plugins/plugin-audit/**(all open PRs' file lists read at this write). #23731 (E6) landed as5b6c257b06on the same file.Target and Acceptance, restated to the ruling (its item 7):
isOwnUiStateWrite: the acting user equals the row's owner before and after; todaysys_user_preference → user_id) writes neither thesys_audit_logrow 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.sys_sharing_ruleupdate → 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 ofsys_audit_logto a SIEM or compliance archive.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
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
objectstack-fleet commentedon Oct 16, 2026
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
objectstack-fleet commentedon Oct 16, 2026
ACCEPT → PR #23866 comment 6213773492 ·
domain:servicesseat 2 ·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T10:45Z. Landing through the merge queue.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landed ·
domain:servicesseat 2 ·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T11:22Z.added_to_merge_queue2026-10-16T10:51Z →merged2026-10-16T11:22Z through the merge queueorigin/main94114c1f26(git merge-base --is-ancestorexit 0)origin/mainif (isOwnUiStateWrite(ctx.object, userId, before, after)) return;inaudit-writers.ts→ 1; ruling record6212163130cited → 3; control: the retired "ACTIVITY ONLY" bullet → 06212163130, ACCEPT6213773492.completedbyFixes #23804.pm:dispatchedand the assignee are cleared in this act. The dev was an in-session subagent, so there is no cloud session to archive.maindoes not publish. The change reaches deployments with the next@objectstack/plugin-auditrelease. Its changeset's first paragraph warns asys_audit_logexport consumer (SIEM, compliance archive) that a user's own UI-state writes stop arriving from that release on.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Note from
domain:servicesseat 2 ·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T14:50Z: #23961 reports that HotCRM on 18.0.0-next.2 still logs everyui.recentwrite. That is expected. This card's fix (PR #23866,94114c1f26) merged after18.0.0-next.2was tagged, and its changeset is still pending. #23961 is on hold until a published@objectstack/plugin-auditcarries it.Generated by Claude Code