You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin-auth: an authenticated request reads sys_session then sys_user in two sequential rounds, because better-auth's advanced.database.joins is left unset and its fallback join runs (leg A of #10757) #24515
Part of #10757. This is its leg A, cut as a child card for domain:services by the claiming seat, as retriage 6256237052 directs ("Item 1 is plugin-auth, the domain:services half: the claiming seat cuts it as a child card"). Filed by domain:engine seat 1 (seat post #6367) · os-project-manager · session_01JtSQQ3EWsbNPzyYMug1tXy, which claims #10757's engine legs. ⛔ Not a claim on this card.
What was measured
From the reading in #10757 (os-dev-report 6255731843, by domain:services seat 1; origin/maind8e11b58e, stock showcase, local PostgreSQL 16, an authenticated GET /api/v1/data/OBJECT?$top=1):
Leg A costs 2 queries in 2 sequential rounds on every request:sys_session (better-auth findSession), then sys_user.
The cause, as observed but not evaluated:
The second round is better-auth's fallback join (@better-auth/core adapter factory.mjs:191-194, :340).
That path is taken because plugin-auth never sets advanced.database.joins: auth-manager.ts (about :3023–:3033) forwards only the cookie and CSRF keys.
whether setting advanced.database.joins on plugin-auth's better-auth instance makes findSession read sys_session and sys_user in one round, on both the SQL driver and the memory driver.
If it does, set it there, at the one place plugin-auth builds the better-auth options. ⛔ No session cache, by Fork 3.
Pin the round count on a real driver.
Pin that the served session is byte-identical: the user fields, the customSession projection, and an expired or revoked session refused as before.
If the adapter cannot carry a join on a driver the platform ships, say which, and stop with the reading.
Grade: p2, as its parent's leg. area:* is triage's.
Reader who acts: the domain:services seat. It posts the reading on this card, and the one re-reading on #10757 when its legs have landed.
Dedupe
The parent's own thread is the dedupe: #10757 (the reading 6255731843, the release 6255757782, and the retriage 6256237052, which cuts this leg) and #11633 (Fork 3). No separate card for leg A exists. Its retriage names it as the child card to cut.
Claim: PM loop round 3 · 2026-10-19T10:46Z
Session: session_01LR1cD9JqgkeqY7k9rrZH8s
Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24515-better-auth-session-join
Worktree: objectstack-issue-24515
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface: packages/plugins/plugin-auth/src/auth-manager.ts (the one place plugin-auth builds better-auth's options, the advanced block about :3023–:3033), objectql-adapter.ts only if the join needs the adapter's support, and tests beside them; a changeset; ⛔ no session cache of any kind (#11633 Fork 3); ⛔ not pnpm-workspace.yaml overrides or the better-auth version (the auth-stack version family is one hot surface); ⛔ not packages/core/** (#10757's engine legs) (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (the default tier; dispatch-gates --tier printed no path-derived mandate for this surface)
Clause-②: no
Responsibility: plugin-auth, which builds better-auth's options without advanced.database.joins, so findSession takes better-auth's fallback join (this lane) | none: no other path reads sys_session and sys_user in one round | every authenticated request pays one extra sequential round today (#10757 reading 6255731843)
Thread-read: none
Serial constraints cleared: no open PR touches auth-manager.ts or objectql-adapter.ts. PR #24513 (#15212 stage 8b, domain:spec/domain:engine epic) touches only plugin-auth test files and testkits, not these sources (a file scan of every open PR at this claim). No pm:dispatched claim names either file: #24343's surface is plugin-audit and the cli ceremony. #10757 (domain:engine, os-project-manager) holds the parent's engine legs in packages/core, which this claim keeps out of.
{
"issue": 24515,
"status": "done",
"branch": "claude/issue-24515-better-auth-session-join",
"pr": null,
"session": "session_01LR1cD9JqgkeqY7k9rrZH8s — the dispatching seat's session (mode:subagent shares it); identity = the branch named in claim 6265431151",
"premise_still_valid": false,
"summary": "Stopped with the reading, as the card directs for this case: on every driver measured (SQLite and PostgreSQL 16.15 through driver-sql, and driver-memory) better-auth's findSession costs 2 reads in 2 sequential rounds today, and exactly the same with advanced.database.joins: true live on the instance, because plugin-auth's ObjectQL adapter drops the join argument (objectql-adapter.ts:897, `join: _join`) and better-auth then runs its fallback join anyway (factory.mjs:191-194). The adapter has no one-round join to carry: ObjectQL's only relational read, `expand` on sys_session.user_id, is itself 2 statements in 2 rounds on all three drivers (a batch `id in (...)` read issued after the parent row, engine.ts:13128-13130), and query.joins / DriverCapabilities.joins were retired under ADR-0049, so no shipped driver reads a join. No code and no PR: setting the flag alone would declare a capability the runtime does not deliver; the title's causal premise (the unset flag) is falsified, and the retriage's expected floor ('4, less leg A's second round') does not hold on this engine.",
"reading": {
"tree": "objectstack-ai/objectstack at a5a781bc7 (origin/main at dispatch); better-auth 1.7.3 and @better-auth/core 1.7.3 as installed",
"harness": "a real ObjectQL + the real driver + the real AuthManager (plugin-auth identity objects, the SecurityPlugin grant tables, sys_metadata_activation), a signed-up user whose session has an active organization (the auth-prelude-query-count.pin.test.ts rig). Instruments: (1) that pin's driver wrap (each verb yields a macrotask; a call starting with nothing in flight opens a round); (2) for driver-sql, knex query/query-response windows grouped by #10757's rule (sorted by start; a query starting before the latest end of the current round joins it). Warm reads only (each measured read was preceded by one unmeasured read).",
"joins_on_how": "advanced.database.joins: true injected by a vi.mock wrapper around better-auth's betterAuth (no source edit); verified on the live instance: ctx.options.advanced.database.joins === true (stock: undefined)",
"legA_findSession": "stock and joins:true identical on all three drivers — SQLite: 2 reads / 2 rounds (timed 2), PostgreSQL 16.15: 2 / 2 (timed 2), memory: 2 / 2 (findOne:sys_session, findOne:sys_user; the memory driver's findOne delegates to its own find, recorded inside the same round). PG SQL text, both modes: select * from sys_session where token = $1 limit $2, then select * from sys_user where id = $1 limit $2 — byte-equal to rows 1-2 of reading 6255731843's tables.",
"prelude_resolveAuthzContext": "stock and joins:true identical: 8 reads / 4 rounds on SQLite, PG and memory (memory records 10 calls, the 2 extra are findOne delegating to find); timed rounds PG 4, SQLite 8 (better-sqlite3 serialises the wave on one connection)",
"objectql_expand_route": "engine.findOne('sys_session', { where: { token }, expand: { user_id: { object: 'sys_user' } } }) — the one relational read the adapter could use: 2 statements / 2 rounds on SQLite and PG (select * from sys_session where token = $1 limit $2, then select * from sys_user where id in ($1)), 2 rounds on memory; user_id expanded to the user row on all three",
"served_session": "identical key sets stock vs joins:true on each driver (session: activeOrganizationId, activeTeamId, createdAt, expiresAt, id, ipAddress, token, updatedAt, userAgent, userId; user: createdAt, email, emailVerified, id, image, name, updatedAt); the customSession projection adds positions and isPlatformAdmin in both modes"
},
"assumptions": {
"A1": "holds — auth-manager.ts:3027-3039 builds advanced from this.config.advanced and forwards only crossSubDomainCookies, useSecureCookies, disableCSRFCheck and cookiePrefix; advanced.database is never set anywhere in plugin-auth",
"A2": "partly false — better-auth 1.7.3 has no adapter-declared join-support flag. The factory passes the join to the adapter whenever options.advanced.database.joins is set (factory.mjs:561 findOne, :609 findMany) and, on output, uses data[modelName] when the adapter put it there, else runs handleFallbackJoin (factory.mjs:191-194, :342). The objectql adapter ignores the join (objectql-adapter.ts:897 findOne, :960 findMany), so the flag changes nothing but what the adapter is handed",
"A3": "PostgreSQL 16.15 was available (container binaries, private throwaway cluster on 127.0.0.1:47515) and was measured, beside SQLite (better-sqlite3) and driver-memory",
"A4": "not reached — no code changed, so no before/after refusal pair exists to measure"
},
"tests": "No code change, so no test suite applies. Measurement runs (temporary harness src/zz-m24515.tmp.test.ts, never committed, deleted; worktree git status --porcelain 0 lines, HEAD = a5a781bc7): run 1, six runs (sqlite, pg, memory) x (stock, joins:true), each `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=1 src/zz-m24515.tmp.test.ts` under os-verify-lock.sh, every run 'Test Files 1 passed (1)', lock held 244s; run 2, PG and SQLite both modes again with the knex timing added, four && -joined runs, every one 'Tests 1 passed (1)', lock held 154s. Readings quoted in the comment body. No ablation: no tested behaviour changed.",
"gates": "No diff, so no gate is owed: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` exit 2, printing 'this branch changes nothing against origin/main (merge base a5a781bc7) — nothing to derive'; no --ran reconciliation (empty list). Build for the measurement: `pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-auth^...' --filter '@objectstack/driver-memory...' build` under os-verify-lock.sh: VERDICT command-exit 0 (held 386s). No PR, so no check-runs to cite.",
"line_budget": 0,
"files_changed": [],
"deviations": [
"In-process harness instead of a full showcase HTTP boot: the same chain (better-auth findSession, the adapter factory, objectql-adapter.ts, ObjectQL, the driver) without the 52-plugin composition; the leg-A SQL text on PG matches reading 6255731843 rows 1-2 byte for byte.",
"The empty branch claude/issue-24515-better-auth-session-join pushed as rule 1's write probe stays on the remote at a5a781bc7 with no commits; no PR was opened, since the card says to stop with the reading in this case.",
"The PostgreSQL cluster lived under /var/lib/postgresql/issue-24515 (the postgres user cannot traverse the scratchpad parents); stopped with pg_ctl and its directory deleted.",
"The lock's VERDICT lines read 'batch-last-exit' because the -c strings opened with a newline; run 2's parts are && -joined, and every part's own 'Tests 1 passed (1)' line is in its log."
],
"not_measured": [
"MySQL through driver-sql, driver-mongodb, driver-sqlite-wasm and driver-turso: not booted. The mechanism is engine-level (expand is a secondary find after the parent read; no driver reads a join, DriverCapabilities.joins is retired), so the same answer is expected, but it is not a reading.",
"A full showcase boot over HTTP with a latency proxy (the parent reading's L1 shape).",
"Refusal parity (expired, revoked, deleted or banned user) before vs after: there is no after."
],
"mcp_calls": "0",
"api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/24515/comments (this os-dev-report), through scripts/pm/post-stamped.mjs and the fleet-write relay. Plus one git push (the empty branch, rule 1's probe; not a REST write). No pr_create.",
"open_questions": [
{
"question": "Leg A's second round cannot be removed from plugin-auth: no ObjectQL read and no shipped driver carries a join in one round. Which disposition for leg A?",
"options": [
"A. Close #24515 as measured, with no code. The seat's #10757 re-reading records leg A as 2 rounds under the current engine contract and corrects the retriage's expected floor: with the opt-ins on, 3 rounds is reachable only by leg 6 (the by-name sys_metadata read, already a domain:engine item on #10757), not by leg A.",
"B. File a domain:engine card: lower single-valued lookup `expand` to one JOIN statement in driver-sql, then teach objectql-adapter.ts to carry better-auth's join through `expand` and set advanced.database.joins. Cost: a new engine capability in the area ADR-0049 retired (query.joins, DriverCapabilities.joins), so an ADR; the referenced object's RLS and FLS, which expand applies today as a separate find, must hold inside the join; and every better-auth join switches path at once (findSessions, findAccountOwnerByKey, findUserByEmail with accounts, the organization plugin's list), so refusal parity must be proven on each.",
"C. Rejected: a raw JOIN inside objectql-adapter.ts through SqlDriver.getKnex(). It is a second read path that bypasses the engine (hooks, the internal-field strip, tenancy, FLS), works on SQL only, and is the workaround Prime Directive 5 forbids."
],
"recommendation": "A. Business need: #10757's 3-round floor is reachable through leg 6 alone (4 to 3), so B has no measured pull of its own; re-measure after leg 6 lands. Long-term soundness: A keeps one read path and the contract; if a one-round relation read is ever needed, B is the sustainable shape, but it is an engine capability, not an auth fix. AI-error resistance: A declares nothing; setting the flag without carriage declares a capability the runtime does not deliver (measured as a no-op). Startup focus: B is capability expansion without measured pull."
}
],
"out_of_scope_findings": [
"carrier: #10757 (the seat's re-reading after its legs land) · noted, not filed — the retriage 6256237052 expects 'With the opt-ins on, the expected floor is 3 rounds: 4, less leg A's second round'. This reading shows leg A's second round cannot be removed on this engine, so that expectation needs correcting there.",
"carrier: none (承接者:无) · noted, not filed — observation only: on driver-memory the served session omits the null-valued keys that driver-sql serves as null (user.image, session.activeTeamId). It is a driver-parity difference in the payload's shape on a dev or test driver, with no measured consumer harm."
]
}
Reading: leg A on a5a781bc7, three drivers, flag off and on
Values are driver reads / sequential rounds for one warm read. Rounds by the driver-wrap rule of auth-prelude-query-count.pin.test.ts; for driver-sql also by #10757's timing rule over the knex query windows (in brackets; SQLite serialises the prelude's concurrent wave on its one connection). On driver-memory findOne delegates to the driver's own find, so the wrap records extra calls inside the same round (in brackets).
The flag is a no-op here. With joins: true live on the instance, better-auth hands the join to the adapter (factory.mjs:561), objectql-adapter.ts:897 drops it (join: _join), and the factory's output step finds no sys_user key on the row and runs handleFallbackJoin (factory.mjs:191-194). The statements on PostgreSQL are the same two in both modes: select * from sys_session where token = $1 limit $2, then select * from sys_user where id = $1 limit $2, byte-equal to rows 1-2 of the reading 6255731843.
The adapter has nothing to carry a join with. ObjectQL's one relational read, expand, resolves a lookup by a second find over id in (...) after the parent row returns (engine.ts:13128-13130), measured above as 2 rounds on every driver. query.joins (query.zod.ts:562) and DriverCapabilities.joins (driver.zod.ts:356) were retired under ADR-0049, and no driver under packages/drivers reads a join.
So, per the card: the adapter cannot carry a join in one round on driver-sql (SQLite and PostgreSQL measured) or driver-memory (measured). driver-mongodb, driver-sqlite-wasm, driver-turso and MySQL were not booted; the mechanism is engine-level, so the same answer is expected, but it is not a reading. No session cache was considered (Fork 3), and nothing was changed.
Review and close · domain:services seat 2 · os-warren · session_01LR1cD9JqgkeqY7k9rrZH8s · 2026-10-19T11:12Z
Release: session_01LR1cD9JqgkeqY7k9rrZH8s ends claim 6265431151 · reason: the premise is falsified with a reading (dev report on this card) · destination: closed not_planned. The branch claude/issue-24515-better-auth-session-join is empty at a5a781bc7.
The reading is accepted. The card said: "If the adapter cannot carry a join on a driver the platform ships, say which, and stop with the reading." That is the case on every shipped driver.
Measured by the dev on SQLite, PostgreSQL 16.15 (through driver-sql) and driver-memory: findSession costs 2 reads in 2 sequential rounds today. It costs exactly the same with advanced.database.joins: true live on the instance.
Why, re-read by the seat on origin/main:
objectql-adapter.ts takes the join argument and drops it (join: _join at :897 for findOne and :960 for findMany). better-auth then runs its fallback join anyway.
The engine has no one-round relational read to carry the join. query.joins and DriverCapabilities.joins are retired under ADR-0049 (packages/spec/src/data/query.zod.ts:388, driver.zod.ts:356). The one relational read, expand, is itself a second statement after the parent row, as measured.
So the title's cause, "advanced.database.joins is left unset", is not the cause. Setting the flag alone would declare a capability the runtime does not deliver, measured as a no-op.
Activity
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 3 · 2026-10-19T10:46Z
Session:
session_01LR1cD9JqgkeqY7k9rrZH8sAccount:
os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24515-better-auth-session-joinWorktree:
objectstack-issue-24515Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/plugins/plugin-auth/src/auth-manager.ts(the one place plugin-auth builds better-auth's options, theadvancedblock about:3023–:3033),objectql-adapter.tsonly if the join needs the adapter's support, and tests beside them; a changeset; ⛔ no session cache of any kind (#11633 Fork 3); ⛔ notpnpm-workspace.yamloverrides or thebetter-authversion (the auth-stack version family is one hot surface); ⛔ notpackages/core/**(#10757's engine legs) (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(the default tier;dispatch-gates --tierprinted no path-derived mandate for this surface)Clause-②: no
Responsibility:
plugin-auth, which builds better-auth's options withoutadvanced.database.joins, sofindSessiontakes better-auth's fallback join (this lane) | none: no other path readssys_sessionandsys_userin one round | every authenticated request pays one extra sequential round today (#10757 reading6255731843)Thread-read: none
Serial constraints cleared: no open PR touches
auth-manager.tsorobjectql-adapter.ts. PR #24513 (#15212 stage 8b,domain:spec/domain:engineepic) touches onlyplugin-authtest files and testkits, not these sources (a file scan of every open PR at this claim). Nopm:dispatchedclaim names either file: #24343's surface isplugin-auditand the cli ceremony. #10757 (domain:engine,os-project-manager) holds the parent's engine legs inpackages/core, which this claim keeps out of.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24515, "status": "done", "branch": "claude/issue-24515-better-auth-session-join", "pr": null, "session": "session_01LR1cD9JqgkeqY7k9rrZH8s — the dispatching seat's session (mode:subagent shares it); identity = the branch named in claim 6265431151", "premise_still_valid": false, "summary": "Stopped with the reading, as the card directs for this case: on every driver measured (SQLite and PostgreSQL 16.15 through driver-sql, and driver-memory) better-auth's findSession costs 2 reads in 2 sequential rounds today, and exactly the same with advanced.database.joins: true live on the instance, because plugin-auth's ObjectQL adapter drops the join argument (objectql-adapter.ts:897, `join: _join`) and better-auth then runs its fallback join anyway (factory.mjs:191-194). The adapter has no one-round join to carry: ObjectQL's only relational read, `expand` on sys_session.user_id, is itself 2 statements in 2 rounds on all three drivers (a batch `id in (...)` read issued after the parent row, engine.ts:13128-13130), and query.joins / DriverCapabilities.joins were retired under ADR-0049, so no shipped driver reads a join. No code and no PR: setting the flag alone would declare a capability the runtime does not deliver; the title's causal premise (the unset flag) is falsified, and the retriage's expected floor ('4, less leg A's second round') does not hold on this engine.", "reading": { "tree": "objectstack-ai/objectstack at a5a781bc7 (origin/main at dispatch); better-auth 1.7.3 and @better-auth/core 1.7.3 as installed", "harness": "a real ObjectQL + the real driver + the real AuthManager (plugin-auth identity objects, the SecurityPlugin grant tables, sys_metadata_activation), a signed-up user whose session has an active organization (the auth-prelude-query-count.pin.test.ts rig). Instruments: (1) that pin's driver wrap (each verb yields a macrotask; a call starting with nothing in flight opens a round); (2) for driver-sql, knex query/query-response windows grouped by #10757's rule (sorted by start; a query starting before the latest end of the current round joins it). Warm reads only (each measured read was preceded by one unmeasured read).", "joins_on_how": "advanced.database.joins: true injected by a vi.mock wrapper around better-auth's betterAuth (no source edit); verified on the live instance: ctx.options.advanced.database.joins === true (stock: undefined)", "legA_findSession": "stock and joins:true identical on all three drivers — SQLite: 2 reads / 2 rounds (timed 2), PostgreSQL 16.15: 2 / 2 (timed 2), memory: 2 / 2 (findOne:sys_session, findOne:sys_user; the memory driver's findOne delegates to its own find, recorded inside the same round). PG SQL text, both modes: select * from sys_session where token = $1 limit $2, then select * from sys_user where id = $1 limit $2 — byte-equal to rows 1-2 of reading 6255731843's tables.", "prelude_resolveAuthzContext": "stock and joins:true identical: 8 reads / 4 rounds on SQLite, PG and memory (memory records 10 calls, the 2 extra are findOne delegating to find); timed rounds PG 4, SQLite 8 (better-sqlite3 serialises the wave on one connection)", "objectql_expand_route": "engine.findOne('sys_session', { where: { token }, expand: { user_id: { object: 'sys_user' } } }) — the one relational read the adapter could use: 2 statements / 2 rounds on SQLite and PG (select * from sys_session where token = $1 limit $2, then select * from sys_user where id in ($1)), 2 rounds on memory; user_id expanded to the user row on all three", "served_session": "identical key sets stock vs joins:true on each driver (session: activeOrganizationId, activeTeamId, createdAt, expiresAt, id, ipAddress, token, updatedAt, userAgent, userId; user: createdAt, email, emailVerified, id, image, name, updatedAt); the customSession projection adds positions and isPlatformAdmin in both modes" }, "assumptions": { "A1": "holds — auth-manager.ts:3027-3039 builds advanced from this.config.advanced and forwards only crossSubDomainCookies, useSecureCookies, disableCSRFCheck and cookiePrefix; advanced.database is never set anywhere in plugin-auth", "A2": "partly false — better-auth 1.7.3 has no adapter-declared join-support flag. The factory passes the join to the adapter whenever options.advanced.database.joins is set (factory.mjs:561 findOne, :609 findMany) and, on output, uses data[modelName] when the adapter put it there, else runs handleFallbackJoin (factory.mjs:191-194, :342). The objectql adapter ignores the join (objectql-adapter.ts:897 findOne, :960 findMany), so the flag changes nothing but what the adapter is handed", "A3": "PostgreSQL 16.15 was available (container binaries, private throwaway cluster on 127.0.0.1:47515) and was measured, beside SQLite (better-sqlite3) and driver-memory", "A4": "not reached — no code changed, so no before/after refusal pair exists to measure" }, "tests": "No code change, so no test suite applies. Measurement runs (temporary harness src/zz-m24515.tmp.test.ts, never committed, deleted; worktree git status --porcelain 0 lines, HEAD = a5a781bc7): run 1, six runs (sqlite, pg, memory) x (stock, joins:true), each `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=1 src/zz-m24515.tmp.test.ts` under os-verify-lock.sh, every run 'Test Files 1 passed (1)', lock held 244s; run 2, PG and SQLite both modes again with the knex timing added, four && -joined runs, every one 'Tests 1 passed (1)', lock held 154s. Readings quoted in the comment body. No ablation: no tested behaviour changed.", "gates": "No diff, so no gate is owed: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` exit 2, printing 'this branch changes nothing against origin/main (merge base a5a781bc7) — nothing to derive'; no --ran reconciliation (empty list). Build for the measurement: `pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-auth^...' --filter '@objectstack/driver-memory...' build` under os-verify-lock.sh: VERDICT command-exit 0 (held 386s). No PR, so no check-runs to cite.", "line_budget": 0, "files_changed": [], "deviations": [ "In-process harness instead of a full showcase HTTP boot: the same chain (better-auth findSession, the adapter factory, objectql-adapter.ts, ObjectQL, the driver) without the 52-plugin composition; the leg-A SQL text on PG matches reading 6255731843 rows 1-2 byte for byte.", "The empty branch claude/issue-24515-better-auth-session-join pushed as rule 1's write probe stays on the remote at a5a781bc7 with no commits; no PR was opened, since the card says to stop with the reading in this case.", "The PostgreSQL cluster lived under /var/lib/postgresql/issue-24515 (the postgres user cannot traverse the scratchpad parents); stopped with pg_ctl and its directory deleted.", "The lock's VERDICT lines read 'batch-last-exit' because the -c strings opened with a newline; run 2's parts are && -joined, and every part's own 'Tests 1 passed (1)' line is in its log." ], "not_measured": [ "MySQL through driver-sql, driver-mongodb, driver-sqlite-wasm and driver-turso: not booted. The mechanism is engine-level (expand is a secondary find after the parent read; no driver reads a join, DriverCapabilities.joins is retired), so the same answer is expected, but it is not a reading.", "A full showcase boot over HTTP with a latency proxy (the parent reading's L1 shape).", "Refusal parity (expired, revoked, deleted or banned user) before vs after: there is no after." ], "mcp_calls": "0", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/24515/comments (this os-dev-report), through scripts/pm/post-stamped.mjs and the fleet-write relay. Plus one git push (the empty branch, rule 1's probe; not a REST write). No pr_create.", "open_questions": [ { "question": "Leg A's second round cannot be removed from plugin-auth: no ObjectQL read and no shipped driver carries a join in one round. Which disposition for leg A?", "options": [ "A. Close #24515 as measured, with no code. The seat's #10757 re-reading records leg A as 2 rounds under the current engine contract and corrects the retriage's expected floor: with the opt-ins on, 3 rounds is reachable only by leg 6 (the by-name sys_metadata read, already a domain:engine item on #10757), not by leg A.", "B. File a domain:engine card: lower single-valued lookup `expand` to one JOIN statement in driver-sql, then teach objectql-adapter.ts to carry better-auth's join through `expand` and set advanced.database.joins. Cost: a new engine capability in the area ADR-0049 retired (query.joins, DriverCapabilities.joins), so an ADR; the referenced object's RLS and FLS, which expand applies today as a separate find, must hold inside the join; and every better-auth join switches path at once (findSessions, findAccountOwnerByKey, findUserByEmail with accounts, the organization plugin's list), so refusal parity must be proven on each.", "C. Rejected: a raw JOIN inside objectql-adapter.ts through SqlDriver.getKnex(). It is a second read path that bypasses the engine (hooks, the internal-field strip, tenancy, FLS), works on SQL only, and is the workaround Prime Directive 5 forbids." ], "recommendation": "A. Business need: #10757's 3-round floor is reachable through leg 6 alone (4 to 3), so B has no measured pull of its own; re-measure after leg 6 lands. Long-term soundness: A keeps one read path and the contract; if a one-round relation read is ever needed, B is the sustainable shape, but it is an engine capability, not an auth fix. AI-error resistance: A declares nothing; setting the flag without carriage declares a capability the runtime does not deliver (measured as a no-op). Startup focus: B is capability expansion without measured pull." } ], "out_of_scope_findings": [ "carrier: #10757 (the seat's re-reading after its legs land) · noted, not filed — the retriage 6256237052 expects 'With the opt-ins on, the expected floor is 3 rounds: 4, less leg A's second round'. This reading shows leg A's second round cannot be removed on this engine, so that expectation needs correcting there.", "carrier: none (承接者:无) · noted, not filed — observation only: on driver-memory the served session omits the null-valued keys that driver-sql serves as null (user.image, session.activeTeamId). It is a driver-parity difference in the payload's shape on a dev or test driver, with no measured consumer harm." ] }Reading: leg A on
a5a781bc7, three drivers, flag off and onValues are driver reads / sequential rounds for one warm read. Rounds by the driver-wrap rule of
auth-prelude-query-count.pin.test.ts; for driver-sql also by #10757's timing rule over the knex query windows (in brackets; SQLite serialises the prelude's concurrent wave on its one connection). On driver-memoryfindOnedelegates to the driver's ownfind, so the wrap records extra calls inside the same round (in brackets).findSession, stockfindSession,advanced.database.joins: trueresolveAuthzContextprelude, stockresolveAuthzContextprelude,joins: truefindOne(sys_session, expand user_id)joins: truelive on the instance, better-auth hands the join to the adapter (factory.mjs:561),objectql-adapter.ts:897drops it (join: _join), and the factory's output step finds nosys_userkey on the row and runshandleFallbackJoin(factory.mjs:191-194). The statements on PostgreSQL are the same two in both modes:select * from sys_session where token = $1 limit $2, thenselect * from sys_user where id = $1 limit $2, byte-equal to rows 1-2 of the reading6255731843.expand, resolves a lookup by a secondfindoverid in (...)after the parent row returns (engine.ts:13128-13130), measured above as 2 rounds on every driver.query.joins(query.zod.ts:562) andDriverCapabilities.joins(driver.zod.ts:356) were retired under ADR-0049, and no driver underpackages/driversreads a join.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Review and close ·
domain:servicesseat 2 ·os-warren·session_01LR1cD9JqgkeqY7k9rrZH8s· 2026-10-19T11:12ZRelease:
session_01LR1cD9JqgkeqY7k9rrZH8sends claim6265431151· reason: the premise is falsified with a reading (dev report on this card) · destination: closednot_planned. The branchclaude/issue-24515-better-auth-session-joinis empty ata5a781bc7.The reading is accepted. The card said: "If the adapter cannot carry a join on a driver the platform ships, say which, and stop with the reading." That is the case on every shipped driver.
driver-sql) anddriver-memory:findSessioncosts 2 reads in 2 sequential rounds today. It costs exactly the same withadvanced.database.joins: truelive on the instance.origin/main:objectql-adapter.tstakes the join argument and drops it (join: _joinat:897forfindOneand:960forfindMany). better-auth then runs its fallback join anyway.query.joinsandDriverCapabilities.joinsare retired under ADR-0049 (packages/spec/src/data/query.zod.ts:388,driver.zod.ts:356). The one relational read,expand, is itself a second statement after the parent row, as measured.advanced.database.joinsis left unset", is not the cause. Setting the flag alone would declare a capability the runtime does not deliver, measured as a no-op.not_planned. The leg is "ruled out with its reading", which is Authenticated data requests issue ~20 DB queries in ~17 sequential legs — per-request auth/session/localization/metadata resolution has no cross-request caching, costing ~1.5s/request on remote Postgres #10757's done-when for each item (retriage6256237052).expandJOIN indriver-sqlcarried by the adapter. That would be a new engine capability in the area ADR-0049 retired. It would need an ADR, RLS/FLS inside the join, and refusal parity on every better-auth join path. It has no measured pull of its own: Authenticated data requests issue ~20 DB queries in ~17 sequential legs — per-request auth/session/localization/metadata resolution has no cross-request caching, costing ~1.5s/request on remote Postgres #10757's 3-round floor is reachable through leg 6 alone. Option C, a raw knex JOIN in the adapter, is a second read path around the engine and is rejected.The maintainer may reopen this card or ask for B as its own engine card.
Generated by Claude Code