Found while implementing #8992 (record-view auditing) — reading audit-writers.ts to match its tenant-stamping precedence on the new read row.
The defect
packages/plugins/plugin-audit/src/audit-writers.ts, in writeAudit:
consttenantId: string|undefined=recordOrgId??sess.tenantId;
sess is HookContext.session, and that object has no tenantId key. The engine builds it in ObjectQL.buildSession (packages/objectql/src/engine.ts) as an object literal with a fixed key set — userId, organizationId, positions, accessToken, and the conditional isSystem / actor / skipTriggers / skipAutomations / preserveAudit. There is no spread of the execution context, so no other key can arrive. The blessed name is organizationId, and buildSession's own comment records the removal:
The deprecated session.tenantId alias (#3280) was removed here in v11 (#3290) — the driver-layer execCtx.tenantId knob is a separate axis and stays.
HookContext's schema agrees (packages/spec/src/data/hook.zod.ts): it declares organizationId and documents the alias as removed in the v11 major.
So the right-hand arm is dead. The expression is exactly recordOrgId.
Why the dead arm matters — it is a guard, not a nicety
The block's own comment states the consequence it exists to prevent:
Audit rows must never be written with organization_id = NULL, or the SecurityPlugin's RLS predicate hides them forever and the audit log UI reads permanently empty while writes succeed.
and names the cases the session arm was written to cover:
- objects with no organization column at all (single-tenant stacks, and ADR-0066 platform-global objects —
resolveRecordOrganizationField returns null for both); - a row whose organization column is NULL or empty.
On those, recordOrgId is undefined, the dead arm contributes nothing, and the row is stamped tenant_id: null / organization_id: null — the permanently-invisible ledger row that #8245 and #8247 are about, reached by a different route. Writes keep succeeding, so nothing anywhere reports it.
There is a second occurrence in the same file, in writeCommentMentions:
consttenantId: string|null=sess.tenantId??row.organization_id??null;
Same dead first arm. Lower stakes — it feeds resolveWriteLocale, so the symptom is a mention notification falling back to the workspace-default locale lookup with no tenant scope rather than an invisible row — but it is the same removed key.
Why it survived
The comment block above the first site is unusually thorough about precedence and even carries a directive written on the assumption that the arm is live:
⛔ Do not flip this back to sess.tenantId ?? recordOrgId.
#8707's ruling changed the ORDER of the two arms and the reasoning is recorded in full — but reordering two expressions does not evaluate either of them, so an arm that had already stopped resolving stayed invisible through a careful review of exactly this code. The #8011 shape: a declaration sitting next to a mechanism, with nothing enforcing the agreement.
Nothing goes red today. Every sys_audit_log field is readonly: true and validateRecord skips readonly fields, so a null tenant is accepted silently; and the write path is wrapped in a swallow-and-report, so even a refusal would not surface as a test failure.
Repro
git grep -n "sess.tenantId" -- packages/plugins/plugin-audit/src/audit-writers.ts
git grep -n "tenantId" -- packages/objectql/src/engine.ts | grep buildSession -A 40
Then read buildSession's object literal and confirm no tenantId key is emitted.
Suggested fix
Read sess.organizationId at both sites, keeping #8707's precedence (the record's own organization still wins). The stronger half is a detector, since this class recurs: a pin that asserts an audit row written on an object with no organization column, under a session that HAS an active organization, lands with a non-null organization_id. That assertion fails today and is what would have caught the alias removal.
Worth deciding at triage whether the same removed alias is read anywhere else — git grep -rn "session.tenantId\|sess.tenantId" across the repo — since #3290 removed it repo-wide and any other reader would be dead the same way.
Not fixed in #8992's PR
Different defect class (write-side tenant stamping vs. the read seam), and the correct form needs a decision about the detector, so it does not meet the in-place bar. #8992's own read-audit writer reads session.organizationId and is unaffected.
Found while implementing #8992 (record-view auditing) — reading
audit-writers.tsto match its tenant-stamping precedence on the newreadrow.The defect
packages/plugins/plugin-audit/src/audit-writers.ts, inwriteAudit:sessisHookContext.session, and that object has notenantIdkey. The engine builds it inObjectQL.buildSession(packages/objectql/src/engine.ts) as an object literal with a fixed key set —userId,organizationId,positions,accessToken, and the conditionalisSystem/actor/skipTriggers/skipAutomations/preserveAudit. There is no spread of the execution context, so no other key can arrive. The blessed name isorganizationId, andbuildSession's own comment records the removal:HookContext's schema agrees (packages/spec/src/data/hook.zod.ts): it declaresorganizationIdand documents the alias as removed in the v11 major.So the right-hand arm is dead. The expression is exactly
recordOrgId.Why the dead arm matters — it is a guard, not a nicety
The block's own comment states the consequence it exists to prevent:
and names the cases the session arm was written to cover:
resolveRecordOrganizationFieldreturns null for both);On those,
recordOrgIdisundefined, the dead arm contributes nothing, and the row is stampedtenant_id: null/organization_id: null— the permanently-invisible ledger row that #8245 and #8247 are about, reached by a different route. Writes keep succeeding, so nothing anywhere reports it.There is a second occurrence in the same file, in
writeCommentMentions:Same dead first arm. Lower stakes — it feeds
resolveWriteLocale, so the symptom is a mention notification falling back to the workspace-default locale lookup with no tenant scope rather than an invisible row — but it is the same removed key.Why it survived
The comment block above the first site is unusually thorough about precedence and even carries a directive written on the assumption that the arm is live:
#8707's ruling changed the ORDER of the two arms and the reasoning is recorded in full — but reordering two expressions does not evaluate either of them, so an arm that had already stopped resolving stayed invisible through a careful review of exactly this code. The #8011 shape: a declaration sitting next to a mechanism, with nothing enforcing the agreement.
Nothing goes red today. Every
sys_audit_logfield isreadonly: trueandvalidateRecordskips readonly fields, so a null tenant is accepted silently; and the write path is wrapped in a swallow-and-report, so even a refusal would not surface as a test failure.Repro
Then read
buildSession's object literal and confirm notenantIdkey is emitted.Suggested fix
Read
sess.organizationIdat both sites, keeping #8707's precedence (the record's own organization still wins). The stronger half is a detector, since this class recurs: a pin that asserts an audit row written on an object with no organization column, under a session that HAS an active organization, lands with a non-nullorganization_id. That assertion fails today and is what would have caught the alias removal.Worth deciding at triage whether the same removed alias is read anywhere else —
git grep -rn "session.tenantId\|sess.tenantId"across the repo — since #3290 removed it repo-wide and any other reader would be dead the same way.Not fixed in #8992's PR
Different defect class (write-side tenant stamping vs. the read seam), and the correct form needs a decision about the detector, so it does not meet the in-place bar. #8992's own read-audit writer reads
session.organizationIdand is unaffected.