Skip to content

settings: loadRows engine branch drops tenant/global rows for user-scope reads — cascade dies at the user level on every engine-bound deployment #11228

Closed

Description

@os-zhuang

Found while implementing #10826's getMany (the extraction preserved the behavior faithfully; this predates it — flagging rather than silently widening that PR's scope).

The divergence

SettingsService.loadRows(namespace, userId) answers differently by backend when userId != null:

  • engine branch: if (userId !== null) where.user_id = userId → the find returns ONLY rows with user_id = <userId>. Global-scope and tenant-scope rows carry user_id = NULL, so they are excluded.
  • memory branch: userId === null || r.user_id === userId || r.scope === 'tenant' || r.scope === 'global' → the user's rows PLUS tenant and global rows.

Consequence

resolveKey cascades user → tenant → global → default within the one rows array it was handed. For a user-scope key on an engine-bound deployment — which is every real deployment — a user with no personal row never sees a persisted tenant or global value: the cascade falls straight through to the manifest default. The same read against the in-memory store resolves correctly, which is exactly why the suite is green over a live defect (the fake is looser than the engine — the #4434 class, this time in a WHERE clause rather than a verb).

getMany (#11200) groups user-scope keys under the userId load and tenant/global keys under the null load, so tenant/global keys resolve fine either way; the broken path is specifically the user-scope key's fallback.

Fix shape

Make the engine WHERE mirror the memory predicate — e.g. { namespace, $or: [{ user_id: userId }, { scope: 'tenant' }, { scope: 'global' }] } — and pin it with a test that runs the same fixture through BOTH branches and asserts identical resolution (the differential harness in settings-getmany.test.ts is most of the scaffolding).

Evidence: packages/services/service-settings/src/settings-service.ts, loadRows, verified on origin/main today.

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Aug 23, 2026
  2. added theissue type on Aug 23, 2026
  3. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Triage: pm:queue, domain:services, Bug — engine WHERE excludes tenant/global rows for user-scope reads, so the declared user → tenant → global → default cascade dies at the user level on every engine-bound deployment; memory branch resolves correctly (fake-looser-than-engine, #4434 class). Fix shape is in the card (mirror the memory predicate; differential both-branches pin reusing the settings-getmany.test.ts harness). Serial constraint: settings-service.ts is hot with the #10826/#11200 getMany work — the services seat answers fold-or-serial against any in-flight settings PR at claim time.


    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

    bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:queueReady for the PM dispatch loop

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions