Repository navigation
auth: server-side auth.api.getSession reads renew the session without forwarding the renewed cookie, so the browser cookie expires before the session (split session) #22258
Description
Activity
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsTriage: first grade,
priority:p2·domain:cli·area:identity·pm:queue. Direction: a server-side session read never leaves the browser's cookie behindTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T10:53Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/rest/src/rest-server.ts,packages/plugins/plugin-hono-server/src/current-user-endpoints.tsandpackages/runtime/src/security/resolve-session-principal.ts(the in-processgetSessionreaders) ⇒domain:cli; rationale:packages/restandruntimeare that lane's.- Why p2: the same grade as objectstack-ai/cloud#2699, which measured it. Any user active past
updateAgecan end up with a dead cookie beside a live bearer, and every cookie-only path then sees them signed out. Less access, not more, so notsecurity. - The choice:
disableRefreshon server-side reads, or forward the renewedSet-Cookie.disableRefreshalone would stop renewal for clients that hold only a bearer and never callget-session. Measure who those are before taking it.- Forwarding keeps renewal for both. One rule across all three readers either way.
- Pins: an aged session read through each reader leaves cookie and session expiry aligned. Control:
get-sessionstill renews and re-issues.
- Why p2: the same grade as objectstack-ai/cloud#2699, which measured it. Any user active past
- addedarea:identityLogin and identity — sign-up, sessions, organization membership, SSOLogin and identity — sign-up, sessions, organization membership, SSOpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClaim: PM loop round 17
Session:session_01RWZbGvPFcRKvUqASZtunCU
Account:os-warren(the seat's linked user asget_meanswers it; the card's assignee)
Branch:claude/issue-22258-session-read-renewal
Worktree:objectstack-issue-22258
Domain:domain:cli
Seat:domain:cli#1
File surface, per the card body and triage6058300952, read onorigin/mainb7e01fbb:- The in-process
getSessionreaders in this lane, which follow one rule:packages/rest/src/rest-server.ts(:3027and:3213);packages/runtime/src/security/resolve-session-principal.ts(:51) andpackages/runtime/src/http-dispatcher.ts(:1359);packages/plugins/plugin-hono-server/src/current-user-endpoints.ts(:406);packages/cloud-connection/src/marketplace-install-local-plugin.ts(:2620).- The card named three; the census on
b7e01fbbfinds these six in this lane.
- Where the one rule lives: a helper in
packages/runtime(besideresolve-session-principal.ts) that the readers above call, if one helper fits all six. The dev says where it went and why. - Pins, in the packages above:
- an aged session read through each door leaves the cookie expiry and the session expiry aligned;
- control: the browser
get-sessionstill renews and re-issues the cookie.
.changeset/*.md:patchfor each package whose shipped code changes.- Added at review (PR fix(auth): in-process session reads no longer renew a browser session behind its cookie #22367), amended in place 2026-10-08T23:00Z:
- The helper's home is
packages/types/src/in-process-session-read.ts(with its test and theindex.tsbarrel line), notpackages/runtime:restcannot importruntimeandplugin-hono-serverdoes not depend on it, while all four reader packages already depend on@objectstack/types. - Four more readers in this lane, the optional-chained
api?.getSession?.(spelling the census missed:packages/runtime/src/security/resolve-execution-context.ts,packages/cloud-connection/src/cloud-connection-plugin.ts, and one more each inhttp-dispatcher.tsandmarketplace-install-local-plugin.ts. packages/rest/src/execctx-authz-input-seam-reachability.test.ts: a source-text pin's spelling, intent unchanged..changeset/*.md:@objectstack/typesminor;rest,runtime,plugin-hono-serverandcloud-connectionpatch.
- The helper's home is
- ⛔ No
packages/plugins/plugin-auth,plugin-webhooksorpackages/services/*edit; those readers aredomain:services'. Nopackages/spec, nocontent/docs. (Stop on breach and explain in the report.)
The other lane's readers, measured and not edited:
plugin-auth/src/auth-plugin.ts(:2464,:2527,:2594,:2912),plugin-webhooks/src/webhook-outbox-plugin.ts(:482) andservice-storage/src/storage-service-plugin.ts(:843). The dev measures which of them leave a split session. The seat carries the result todomain:services.Container & model:
M,mode:subagent,model: default (opus).dispatch-gates --tierover the path gives no path-derived mandate.
Clause-②: yes (widening)- Amended in place 2026-10-08T23:00Z at review of PR fix(auth): in-process session reads no longer renew a browser session behind its cookie #22367. The one shared helper adds three named exports to a published package entry (
@objectstack/types:inProcessSessionReadInput,carriesSessionCookie, typeInProcessSessionReadInput), so it is a widening and takesminor. No accepted input or wire shape changes: a cookie request's in-process read stops renewing; a bearer-only read renews as before. A contract review is owed on the head.
Responsibility:platform code: in-process auth.api.getSession reads renew the session and discard the renewed Set-Cookie, leaving a live bearer beside a dying cookie|none known: no reader passes disableRefresh or forwards the response headers (git grep disableRefresh -- packages/: 0 hits)|any user active past updateAge on a cookie session: every cookie-only path then sees them signed out (measured on cloud, objectstack-ai/cloud#2699)
Thread-read: 6058300952
Serial constraints cleared: read 2026-10-08T21:37Z: - Open PRs (14, each file list read by name against the merge base): none touches the six files above.
- In-flight claims in
domain:cli: [finding] cli(migrate meta):--writenever writes a conversion the authored-source load already applied (e.g.driver: 'mongo') — the chain sees a pre-converted stack and reportsapplied: []#22256 (PR fix(cli):os migrate metalists and writes a conversion the load already applies, on a stack the schema accepts #22351, armed for the merge queue) and [finding] qa(http-conformance): no composed-host test holds the@objectstack/honocatch-all's/metawrite preconditions — #22141's pins drivedispatch()directly, so the adapter's hand-off of the rawRequestis held only by a quoted probe #22221 (PR test(http-conformance): a real createHonoApp host holds PUT /meta If-Match, If-None-Match and ?mode=draft #22341, accepted, awaiting its label) holdpackages/cliandpackages/qafiles only.
domain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-08T21:37Z- The in-process
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22258,
"status": "done",
"branch": "claude/issue-22258-session-read-renewal",
"pr": "#22367",
"session": "session_01RWZbGvPFcRKvUqASZtunCU",
"premise_still_valid": true,
"summary": "Reproduced on b7e01fb through public doors (fresh dev:crm stack, better-auth 1.7.3, expiresIn 604800 s / updateAge 86400 s, session aged to now + expiresIn - updateAge - 60 s): every in-process door moved sys_session.expires_at +86460 s and set no cookie, by cookie and by bearer; get-session renewed and re-issued Max-Age=604800. One rule now covers all TEN in-process getSession readers in this lane (the census on b7e01fb found 6; 4 more use the optional-chained api?.getSession?.( spelling: http-dispatcher.ts enforceProjectMembership, resolve-execution-context.ts, marketplace-install-local resolveInstallPrincipal, cloud-connection-plugin.ts). The rule is one helper, inProcessSessionReadInput(headers) in @objectstack/types: a request carrying a better-auth session cookie reads with query.disableRefresh (renewal only via get-session, which re-issues the cookie); a bearer-only request reads as before. @objectstack/types, not runtime: rest cannot import runtime (cycle) and plugin-hono-server does not depend on it; all four reader packages already depend on types. After the fix: every lane door by cookie = 0 s / no cookie; by bearer = +86460 s / no cookie; control unchanged. All nine domain:services readers (six from the PM census plus plugin-sharing :940, service-settings :299 and service-datasource :212, which it missed) were measured splitting and were not edited, so the PR says Part of #22258 with a Release: line naming domain:services as the carrier.",
"tests": "All at HEAD 8200f57 (remote head identical). Unit (pnpm --filter PKG test, local project): types 25 files / 749 passed; rest 264 / 4954 passed, 326 skipped; runtime 341 / 4787 passed, 19 skipped; plugin-hono-server 28 / 329 passed; cloud-connection 42 / 514 passed. test:repo: types 11, rest 191 (+1 skipped), runtime 751 passed. Typecheck for the five packages: turbo 41/41 successful; the test-layer tsconfigs list each new pin (--listFiles); runtime's test layer is OK at its existing ledger (27 files / 190 errors, unchanged). New pins: runtime in-process-session-renewal.pin.test.ts 11/11 against real better-auth. It covers the precondition that a bare read renews; GET /data/:object, /auth/me/permissions and /i18n/locales, each by cookie (aligned: 0 s, no cookie) and by bearer (renews to now + expiresIn, no cookie set); resolveSessionPrincipalId by cookie and by bearer, plus get-session still renewing and re-issuing after the limiter's read; and the control get-session with Max-Age = expiresIn. Input pins: rest 3/3, hono 3/3, cloud-connection 9/9, types 13 cases. One pre-existing source-text pin, rest execctx-authz-input-seam-reachability.test.ts, went red on the first full run (its regex spelled the old gate re-read argument). Its spelling was updated with its intent unchanged (raw throwing api call), then green. Ablation 1: scripts/ablation-replace.mjs (wrap mode, absolute-path restore trap) reverted rest computeExecCtx's getter. Anchor 1->0, blob 89fae0b5e5f5 -> 677bb44cb288. Runtime pin 1 failed | 10 passed, exactly 'GET /data/:object - by cookie' ('the session renewed (+86460 s) but its cookie was not re-issued'); rest pin 2 failed | 1 passed. Restored: blob == HEAD 89fae0b5e5f5, git diff HEAD empty. Ablation 2: reverted runtime resolve-execution-context's getter. Blob c570e6e4cd84 -> 2e86b8e71527. Runtime pin 1 failed | 10 passed, exactly 'GET /i18n/locales - by cookie'. Restored: blob == HEAD c570e6e4cd84, diff empty. Both mutated readers resolve from src in their suites (relative import; @objectstack/rest alias in runtime), so no dist leg applies. The direction observed was turn-red, as predicted.",
"gates": "67 commands derived bynode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfor the actual diff. These are the order's 61 plus check:engine-double-contract, check:objectql-double-limit, check:query-options-erasure, check:type-check-coverage, check:type-check-debt and check:where-matcher. All 67 exit 0 at HEAD 8200f57.--ranprinted: 'Run reconciliation - 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.' (exit 0). The lane's addition, fullpnpm lint(eslint . --no-inline-config), exits 0. Derivation warning on record: the tree is 2 commits behind origin/main 1cb0edb (#22351 cli, #22352 plugin-security/plugin-auth; neither touches a file here). Not merged; CI tests the merge ref.",
"line_budget": "16 files, +907 / -14 (git diff --shortstat b7e01fb..HEAD), far below the 5,000-line human-merge threshold. No governed surface (.claude/, docs/adr/, skills/, AGENTS.md, CLAUDE.md) and no skills/ file, so no SKILL line reading applies. Shipped-source lines: types +114, rest +13/-2, runtime +20/-7, plugin-hono-server +7/-1, cloud-connection +13/-3; the rest is tests and the changeset.",
"files_changed": [
".changeset/22258-in-process-session-read-no-renewal-behind-cookie.md",
"packages/types/src/in-process-session-read.ts",
"packages/types/src/in-process-session-read.test.ts",
"packages/types/src/index.ts",
"packages/rest/src/rest-server.ts",
"packages/rest/src/in-process-session-read.pin.test.ts",
"packages/rest/src/execctx-authz-input-seam-reachability.test.ts",
"packages/runtime/src/http-dispatcher.ts",
"packages/runtime/src/security/resolve-session-principal.ts",
"packages/runtime/src/security/resolve-execution-context.ts",
"packages/runtime/src/in-process-session-renewal.pin.test.ts",
"packages/plugins/plugin-hono-server/src/current-user-endpoints.ts",
"packages/plugins/plugin-hono-server/src/in-process-session-read.pin.test.ts",
"packages/cloud-connection/src/cloud-connection-plugin.ts",
"packages/cloud-connection/src/marketplace-install-local-plugin.ts",
"packages/cloud-connection/src/in-process-session-read.pin.test.ts"
],
"deviations": [
"Clause-②: the PR body saysClause-②: yes (widening), not the order's/claim'sno, and the changeset grades @objectstack/typesminor, notpatch. Reason: the one shared helper adds three named exports to a published package (inProcessSessionReadInput, carriesSessionCookie, type InProcessSessionReadInput). The claim's own gloss onnoexcluded export changes, and Check Changeset's WHICH LEVEL ruling grades an additive public-surface widening at least minor. rest, runtime, plugin-hono-server and cloud-connection are patch, as ordered. See open_questions.",
"File surface widened beyond claim 6069531196, all in this lane. The helper's home is packages/types (in-process-session-read.ts, its test, index.ts barrel) instead of packages/runtime, because rest and plugin-hono-server cannot import runtime. Two more reader files, packages/runtime/src/security/resolve-execution-context.ts and packages/cloud-connection/src/cloud-connection-plugin.ts, plus two readers inside already-claimed files, are fixed in place under the bounded in-place-fix rule. Their doors split measurably (MCP, /i18n/locales), and ablation 2 attributes /i18n/locales to resolve-execution-context. packages/rest/src/execctx-authz-input-seam-reachability.test.ts got a spelling update to an existing source-text pin. The seat owes the claim's file-surface addendum.",
"Not read: the cloud measurement the card cites (objectstack-ai/cloud issue 2699's os-dev-report and PR 2708). The REST read answered 403 ('GitHub access to this repository is not enabled for this session') and mcp__claude-code-remote__add_repo (access: read) answered 'you don't have access to objectstack-ai/cloud'. Not retried. H1 re-measured the defect independently on this repo's doors.",
"Serial constraint read after the claim: open PR #22357 (draft, created 21:42Z, after the claim's 21:37Z read) edits packages/runtime/src/http-dispatcher.ts in comment hunks around 1241 and 1508, disjoint from this PR's hunks around 1344-1362 and 1420-1442. Not blocking; for the seat's sequencing.",
"Branch not merged with origin/main (2 commits behind at report time; neither touches a file here)."
],
"mcp_calls": "1 - mcp__claude-code-remote__add_repo (read-access request for objectstack-ai/cloud, refused). 0 MCP GitHub calls, no MCP write tool.",
"api_writes": "3 - each one repository_dispatch through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #22367; read-back 16680/16680 bytes identical); (2) label-write --assign os-warren -> POST /repos//issues/22367/assignees (read-back matches); (3) post-stamped -> POST /repos//issues/22258/comments (this report). Plus git pushes of the branch (not REST).",
"open_questions": [
{
"question": "Clause-② for this PR. The claim declarednobefore the helper's home was chosen. The diff adds three named exports to @objectstack/types, an additive public-surface widening.",
"options": [
"A: keepClause-②: yes (widening)with @objectstack/typesminorand the four reader packagespatch(as shipped).",
"B: the seat rewrites the body line tonoand the changeset topatch. That contradicts the WHICH LEVEL ruling for additive exports, and the claim's own gloss.",
"C: keepnoby moving the helper off a published entry. There is no such home: rest and plugin-hono-server can only share it through a published package, and four copies would be the drift the seat's one-helper cut exists to prevent."
],
"recommendation": "A. On the four axes: real need, because the helper is consumed by ten readers today and is the rule the services lane will adopt. Long-term, it keeps one home and one rule with no copies. AI-safety: a truthful widening declaration keeps the changeset gate's level check meaningful. Startup focus: additive only, with no new accepted input or wire shape."
}
],
"out_of_scope_findings": [
"class: a · reach: public doors, measured on this branch's build so the lane readers are excluded. Each of the following moved expires_at +86460 s by cookie and set no session cookie: POST /api/v1/auth/admin/oauth2/toggle-disabled, /admin/sso/register, /admin/unlock-user and /admin/has-permission; POST /api/v1/webhooks/redeliver (showcase); GET /api/v1/storage/upload/chunked/:id/progress; DELETE /api/v1/share-links/:id; GET /api/settings; GET /api/v1/datasources/drivers. · evidence: nine domain:services in-process readers at b7e01fb: plugin-auth auth-plugin.ts:2464, :2527 (gateAdmin), :2594 and :2912; plugin-webhooks webhook-outbox-plugin.ts:482; service-storage storage-service-plugin.ts:843; and three the PM census missed: plugin-sharing sharing-plugin.ts:940, service-settings settings-service-plugin.ts:299, service-datasource admin-routes.ts:212. The rule that fixes them: api.getSession(inProcessSessionReadInput(headers)) from @objectstack/types. Five of the six packages already depend on types; plugin-webhooks would gain that one dependency. This is the remaining half of #22258 itself (the PR says Part of, with a Release: line), so no new card; a sub-issue of #22258 if the seat splits it. · dedupe words: getSession disableRefresh services, in-process session renewal cookie, split session plugin-auth, inProcessSessionReadInput",
"carrier: domain:spec seat · AuthSessionApi (packages/spec/src/contracts/auth-service.ts) declares getSession's input as { headers } and documents that every reader calls exactly getSession({ headers }), which is no longer true; the helper declares the wider slice itself. Declaration drift, not a runtime defect · noted in the PR's Acceptance notes, not filed",
"carrier: none · residue the server cannot see: a browser request that carries the bearer without the cookie (credentials omitted cross-origin, cookie blocked) reads as bearer-only and still renews in-process. If the same browser sends that session's cookie on other requests, that cookie can still fall behind · noted in Acceptance notes, not filed",
"carrier: none · better-auth's own GET /api/v1/auth/get-session re-issues the session cookie on a bearer-only request too (measured: Max-Age=604800 with a bearer). It is the vendor route, unchanged here · noted, not filed"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsACCEPT — PR #22367 at
8200f577, pending the contract review (Clause-②: yes) and CI.Part of: thedomain:serviceshalf is released at landingdomain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· read on GitHub 2026-10-08T23:01ZChecked on GitHub and in the diff, not from the report:
- Shape:
- draft, base
main; - line 1 is
Part of #22258, line 2 isClause-②: yes (widening), then aRelease:line namingdomain:servicesas the carrier of the other half; - assignee
os-warren; - 16 files, +907 / −14.
- draft, base
- The rule (
packages/types/src/in-process-session-read.ts) works from what the request carries:- a request with a better-auth session cookie reads with
query.disableRefresh, so it renews only throughget-session, which re-issues the cookie; - a bearer-only request reads as before, so it keeps renewing;
- the headers pass through untouched, so every reader resolves the same session it did.
- The helper only ever adds
disableRefresh. It never sets or forwards a cookie. - The cookie test reads the name's shape (
<prefix>.session_token, behind__Secure-/__Host-), not one spelling. An empty value counts as no session.
- a request with a better-auth session cookie reads with
- The readers: ten in-process readers in this lane now call
api.getSession(inProcessSessionReadInput(headers)). Each is a one-expression substitution:rest-server.ts(two);runtime/src/http-dispatcher.ts(two),security/resolve-session-principal.tsandsecurity/resolve-execution-context.ts;plugin-hono-server/src/current-user-endpoints.ts;cloud-connection/src/cloud-connection-plugin.tsandmarketplace-install-local-plugin.ts(two).- The seat's census found six. The dev found four more with the optional-chained
api?.getSession?.(spelling. All four are the same defect in this lane, so they are fixed here.
- The home is
@objectstack/types, notruntime, and that holds.restcannot importruntime(a cycle),plugin-hono-serverdoes not depend on it, and all four reader packages already depend ontypes.- So the one rule needs a published entry, and three named exports are added there.
Clause-②: yes (widening)and@objectstack/typesminorare right: a new package-entry export is a widening. - The claim declared
nobefore the home was chosen. It is amended in place (6069531196), and a contract review is owed on this head.
- So the one rule needs a published entry, and three named exports are added there.
- H3 and H4, measured. Bearer-only clients (
@objectstack/clientoutside a browser, theosCLI) reachget-sessiononly at sign-in, so a blanketdisableRefreshwould end their sliding renewal. The cookie-conditional rule keeps it.- Forwarding the renewed
Set-Cookiewas measured and not taken: several readers hold no response (the rate limiter, the dispatcher's scope resolution, the cached execution-context resolver).
- Forwarding the renewed
- The pins:
runtime/src/in-process-session-renewal.pin.test.tsruns against real better-auth, 11 cases:- a precondition: a bare read renews;
/data/:object,/auth/me/permissionsand/i18n/locales, each by cookie (aligned: 0 s, no cookie) and by bearer (renews, no cookie set);resolveSessionPrincipalIdboth ways;- the
get-sessioncontrol withMax-Age = expiresIn.
- The input pins in
rest,plugin-hono-server,cloud-connectionandtypes. - The dev's two ablations each reddened exactly the door they removed, and each was restored to a blob equal to HEAD.
- The pre-existing source-text pin
execctx-authz-input-seam-reachability.test.tschanged its spelling only, with the same intent.
- Changeset:
typesminor;rest,runtime,plugin-hono-serverandcloud-connectionpatch. Each sentence checks against the diff, including the bearer behaviour and the list ofdomain:servicesreaders left unchanged.
The open question (Clause-②), ruled A: keep
yes (widening)withtypesminor, as shipped. B would misstate an additive export. C has no home that avoids four copies of the rule.Owed before landing:
- the independent contract review on
8200f577(Clause-②: yes); - CI on
8200f577(running). - No new
mkdtempSyncsite, so no self-test is owed.
At landing (
Part of #22258): the seat releases the card to retriage, naming thedomain:serviceshalf. That half is nine in-process readers:plugin-auth×4;plugin-webhooks;service-storage;plugin-sharing,service-settingsandservice-datasource.
Each was measured splitting on this branch's build. Each is fixed by the same
inProcessSessionReadInput(headers)from@objectstack/types.Not filed:
AuthSessionApi'sgetSessioninput declaration inpackages/speclags the helper's wider slice (a declaration drift for the spec seat; in the PR's Acceptance notes);- a browser request carrying the bearer without the cookie still renews in-process, which is the server's blind spot;
- better-auth's own
get-sessionre-issues the cookie to a bearer-only request (vendor route).
- Shape:
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded (
Part of): PR #22367 →a45d5d8ab7, a single-parent queue squash. This lane's ten readers are done; thedomain:serviceshalf is released to retriagedomain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-09T00:12Z- Landing shape:
a45d5d8ab7has one parent.- It is an ancestor of
origin/main; the pre-merge head8200f577is not. - It entered the merge queue 2026-10-08T23:36:20Z and merged 2026-10-09T00:11:50Z on that first entry.
Part of #22258, so this card stays open.
- Content on
origin/main:packages/types/src/in-process-session-read.ts:inProcessSessionReadInput(headers)addsquery: { disableRefresh: true }only when the request carries a better-auth session cookie; a bearer-only read is unchanged. It sets and forwards no cookie.- Ten in-process readers in this lane call
api.getSession(inProcessSessionReadInput(headers)):rest-server.ts(two),runtime'shttp-dispatcher.ts(two),resolve-session-principal.tsandresolve-execution-context.ts,plugin-hono-server'scurrent-user-endpoints.ts, andcloud-connection'scloud-connection-plugin.tsandmarketplace-install-local-plugin.ts(two). - The pins:
runtime/src/in-process-session-renewal.pin.test.tsagainst real better-auth (11 cases, with theget-sessioncontrol), plus the input pins intypes,rest,plugin-hono-serverandcloud-connection. - The changeset:
@objectstack/typesminor;rest,runtime,plugin-hono-serverandcloud-connectionpatch.
- Review of record:
- ACCEPT
6070755027at8200f577; the open question ruled A (yes (widening),typesminor). Clause-②: yes (widening): the claim was amended in place (6069531196) once the helper's home was chosen. The independent contract review on8200f577isPASS(PR fix(auth): in-process session reads no longer renew a browser session behind its cookie #22367,6070952113).- CI on the head was green before arming (34 runs).
check-governed-merges: not governed.check-expected-skips: OK. No newmkdtempSyncsite, so no self-test was owed. Arming re-rangit merge-treeagainst a freshorigin/mainin the same step: clean.
- ACCEPT
- Delivered: in this lane, a browser request (one that carries the session cookie) no longer renews its session in-process behind a cookie that is never re-issued. It renews only through
get-session, which re-issues the cookie, so cookie and session expire together. A bearer-only client (the SDK outside a browser, theosCLI) keeps its sliding renewal.
Release: the
domain:serviceshalf stays open on this card. Nine in-process readers there still split a cookie session (each measured on this PR's build, table H5 in the PR body):plugin-authauth-plugin.ts×4 (:2464,:2527gateAdmin,:2594,:2912);plugin-webhookswebhook-outbox-plugin.ts:482;service-storagestorage-service-plugin.ts:843;plugin-sharingsharing-plugin.ts:940,service-settingssettings-service-plugin.ts:299andservice-datasourceadmin-routes.ts:212.
Each is fixed by the same one-expression call from
@objectstack/types. Five of the six packages already depend on it;plugin-webhookswould gain that one dependency.- Retriage ask: route the remainder to
domain:services. - Also carried, for
domain:spec:AuthSessionApi.getSession's input inpackages/spec/src/contracts/auth-service.tsis still declared{ headers }, and its doc says every reader passes exactly that. The helper now passesquery.disableRefreshtoo. A declaration drift, not a break (PR Acceptance notes).
pm:dispatched→pm:queue+pm:retriage; the card is unassigned.- Landing shape:
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 9, 2026 10 remaining items
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22258,
"status": "done",
"branch": "claude/issue-22258-services-session-read",
"pr": "#22396",
"session": "session_01WYYhVJ78u7PhwFViWo1EmQ (the dispatching PM's session; mode:subagent)",
"premise_still_valid": true,
"summary": "The nine domain:services in-process getSession readers now hand better-auth inProcessSessionReadInput(headers) from @objectstack/types: plugin-auth auth-plugin.ts x4 (:2465 toggle-disabled gate, :2528 gateAdmin, :2595 unlock-user gate, :2913 has-permission branch), plugin-webhooks :483, service-storage :844, plugin-sharing :941, service-settings :300 and service-datasource :212. Each is a one-expression substitution with the headers untouched. plugin-webhooks gains the ruled workspace dependency on @objectstack/types (no cycle: types depends only on spec, and core already pulled types in), plus the anchored vitest alias that check:test-source-alias dictates. Measured on real better-auth: each plugin-auth admin door by cookie keeps expires_at unchanged and sets no cookie; bearer-only renews to now + expiresIn; get-session still renews and re-issues with Max-Age = expiresIn. The census at c09841c leaves no call passing a bare { headers }. Residue outside this census: four plugin-auth doors still split through other spellings (a /get-session re-dispatch, and vendor endpoint calls carrying headers); see out_of_scope_findings[0].",
"tests": "All at HEAD c09841c, which is this branch merged with origin/main 1915434 (no overlap with these files). Build: turbo run build --filter=!@objectstack/docs, 72/72 tasks, exit 0. pnpm --filter PKG test, every one exit 0: plugin-auth 133 files, 2693 passed, 10 skipped; plugin-webhooks 16 files, 168 passed; service-storage 47 files, 776 passed; plugin-sharing 41 files, 1005 passed; service-settings 42 files, 755 passed; service-datasource 42 files, 763 passed. pnpm --filter PKG typecheck: exit 0 for all six, and each new pin is listed by a program the script runs (tsc --listFilesOnly: tsconfig.json, or tsconfig.test.json through check:test-typecheck). New pins: plugin-auth src/in-process-session-renewal.pin.test.ts runs real better-auth (real AuthManager, real registerAuthRoutes on Hono), 11 own cases: the expiresIn/updateAge read, the precondition that a bare read renews, four doors x (cookie: 0 s, no cookie | bearer: renews to now + expiresIn, no cookie), and the get-session control (Max-Age = expiresIn). Input pins (3 cases each: cookie, cookie plus bearer, bearer-only) in plugin-webhooks, service-storage, plugin-sharing, service-settings and service-datasource, each through the package's real door. Ablation: nine legs, run at c09841c (and earlier at 6f1222c, with the same outcome) through scripts/ablation-replace.mjs in wrap mode, with a trap that restores to HEAD by absolute path. Each leg put the old { headers } call back in one reader. Every leg: the anchor went 1 to 0 and the blob changed; it then went red on exactly its own door's cookie cases. plugin-auth :2465, :2528, :2595 and :2913 each gave '1 failed | 20 passed', exactly that door's 'by cookie' case, with 'the session renewed (+86460 s) but its cookie was not re-issued'. webhooks, storage, sharing, settings and datasource each gave '2 failed | 1 passed' (the cookie and cookie-plus-bearer cases: 'expected undefined to deeply equal { disableRefresh: true }'). The bearer control stayed green in every leg. Every leg was restored to a blob equal to HEAD with git diff HEAD empty (auth-plugin 32c004a7d80f, webhooks 60dc0b99b985, storage b85d1c3f6fe1, sharing 026ac1febf6e, settings e9af2764acea, datasource 0868657715d2), and the trap reported all six == HEAD. Each mutated reader resolves from src in its suite (relative imports), so no dist leg applies. The direction observed was turn-red, as predicted. Lint, narrowed and declared: the population read from eslint's config is the 13 changed .ts files (the other 3 changed files answer 'no matching configuration'); --format json gives 13 linted, 0 errors, 0 warnings; the config has no parserOptions.project and no type-aware rule, so this diff cannot move a verdict on an untouched file.",
"gates": "82 commands derived bynode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat c09841c: the pre-derived 76 plus check:engine-double-contract, check:objectql-double-limit, check:query-options-erasure, check:type-check-coverage, check:type-check-debt and check:where-matcher. All 82 exit 0 at c09841c.--ranwith each recorded exit code printed: 'Run reconciliation — 82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN.' (a derived zero), exit 0. Earlier run, at intermediate heads: check:test-source-alias went red (exit 1, a new unaliased @objectstack/types import in plugin-webhooks' tests) and was cleared by the dictated alias. Three gates exited 3 on prerequisites: plugin-teardown-shape's self-test lacked a control commit in the shallow clone (cleared by git fetch --depth=1 origin 621a487); check:dual-build-cjs-loads and check:i18n lacked dist/ (cleared by the full build). All four are exit 0 in the final run.",
"line_budget": "16 files, +873 / -11 against the merge base 1915434 (the PR's own numbers). Shipped source: plugin-auth +5/-4, plugin-webhooks +2/-1 (plus 1 package.json line), service-storage +2/-1, plugin-sharing +2/-1, service-settings +2/-1, service-datasource +2/-2. The rest is six pin files (+829), the changeset (+19), the vitest alias (+5) and pnpm-lock.yaml (+3). Far below the 5,000-line threshold. No governed surface (.claude/, docs/adr/, skills/**, AGENTS.md, CLAUDE.md).",
"files_changed": [
".changeset/22258-services-in-process-session-read.md",
"packages/plugins/plugin-auth/src/auth-plugin.ts",
"packages/plugins/plugin-auth/src/in-process-session-renewal.pin.test.ts",
"packages/plugins/plugin-sharing/src/in-process-session-read.pin.test.ts",
"packages/plugins/plugin-sharing/src/sharing-plugin.ts",
"packages/plugins/plugin-webhooks/package.json",
"packages/plugins/plugin-webhooks/src/in-process-session-read.pin.test.ts",
"packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts",
"packages/plugins/plugin-webhooks/vitest.config.ts",
"packages/services/service-datasource/src/tests/in-process-session-read.pin.test.ts",
"packages/services/service-datasource/src/admin-routes.ts",
"packages/services/service-settings/src/in-process-session-read.pin.test.ts",
"packages/services/service-settings/src/settings-service-plugin.ts",
"packages/services/service-storage/src/in-process-session-read.pin.test.ts",
"packages/services/service-storage/src/storage-service-plugin.ts",
"pnpm-lock.yaml"
],
"deviations": [
"File surface: one file beyond the planned surface, packages/plugins/plugin-webhooks/vitest.config.ts (+5, one anchored alias entry with its comment). check:test-source-alias reddened on the ruled @objectstack/types dependency and dictates this alias, because widening KNOWN_UNALIASED_TEST_IMPORTS is shrink-only. I did not stop on it: the line is mechanically forced by the ruled dependency and it adds no behaviour.",
"plugin-auth's helper import is its own line (import { inProcessSessionReadInput } from '@objectstack/types'), not added to the existing types import. The existing pin platform-owner-email-reader-census.pin.test.ts lists that import line's text verbatim, and widening it reddened the pin (1 failed of 2703 on the first full run). A separate line keeps that pin's ledger unchanged; no eslint rule forbids it.",
"Assumption 1, partly falsified: the census does find exactly nine unconverted readers at those lines. But the triage census spelling 'api.getSession(' as a fixed string is case-sensitive and matches none of the four plugin-auth readers ('authApi.getSession(' with a capital A, and '(authApi as any).getSession('). It does not even match them as a substring. The enumeration uses an any-receiver pattern and lists what it caught.",
"Assumption 3, re-measured by mechanism rather than per door on main: the plugin-auth pin's precondition case measures, on real better-auth, that a bare in-process read by cookie past updateAge renews (+86460 s) and stages a cookie that nobody sends. The ablations then measure each plugin-auth door splitting by cookie with the old call (+86460 s, no cookie). For the other five readers, the old call hands no query, as each ablation shows; what a no-query read does is the precondition above. No door-level run on a dev server was made for those five.",
"Branch merged with origin/main 1915434 (merge commit c09841c, clean, no overlap) before the final verification, per AGENTS.md section 10.",
"Scratch residue outside the repo: a 4-byte pid file at /build-deps.pid. A backgrounded subshell kept the scratchpad variable, so the redirect wrote to the filesystem root. The built-in safety check refused to remove a root-level path, so it is left for a person to delete. The worktree itself was removed cleanly (node_modules first, no --force).",
"Not edited, outside the surface: the pending cli-half changeset .changeset/22258-in-process-session-read-no-renewal-behind-cookie.md. Its last paragraph says the services-lane readers 'still renew a cookie session without re-issuing its cookie', which becomes false once this PR lands in the same release. See open_questions[0]."
],
"mcp_calls": "0 (no MCP tool called in this run)",
"api_writes": "3, each one repository_dispatch through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #22396, read-back 12714/12714 bytes identical); (2) label-write --assign os-elon-musk → POST /repos//issues/22396/assignees (read-back matches; size/l is another actor's label); (3) post-stamped → POST /repos//issues/22258/comments (this report). Not REST: git push of the branch (several pushes, all fast-forward, no force) and one read-only git fetch --depth=1 origin 621a487 for a gate's control commit.",
"open_questions": [
{
"question": "The pending cli-half changeset (.changeset/22258-in-process-session-read-no-renewal-behind-cookie.md) ends: 'Not changed here: the in-process readers in plugin-auth, plugin-webhooks, plugin-sharing, service-storage, service-settings and service-datasource still renew a cookie session without re-issuing its cookie.' Once PR #22396 lands, both changesets compile into the same fixed-group release, and that sentence in the types/rest/runtime/plugin-hono-server/cloud-connection CHANGELOGs is false.",
"options": [
"A: amend that one sentence before the release, either in PR #22396 (seat-authorised surface widening) or by the seat in a small changeset-only commit, to e.g. 'The same rule is applied to the in-process readers in plugin-auth, plugin-webhooks, plugin-sharing, service-storage, service-settings and service-datasource by their own changeset.'",
"B: leave it; the reader sees both entries in the same version."
],
"recommendation": "A. Real need: the CHANGELOG is the text an upgrading agent greps, and the sentence is a factual claim about these packages that would be false in that version. Long-term: a changeset is still an editable input until release, and the cheapest correction is before release, not an erratum after. AI-safety: a false 'still renews' line would send an agent looking for a defect that is gone. Scope: a one-sentence edit to a pending release input."
},
{
"question": "PR line 1 is 'Fixes #22258', per the dispatch and triage's enumeration ruling (the getSession census closes the card). The card's own 'Done when' ('No framework door extends a session without forwarding its cookie') is still measurably unmet at four plugin-auth doors whose reads are spelled differently (out_of_scope_findings[0]).",
"options": [
"A: keep 'Fixes #22258' and file the residue family as its own card (class a), which is the ruled closure.",
"B: the seat rewrites PR line 1 to 'Part of #22258' (one-line body edit) and the card carries the residue."
],
"recommendation": "A, because triage ruled that this census closes the card, and the residue is a different mechanism. It sits at a vendor handler re-dispatch, or a vendor endpoint call carrying headers, not at a getSession input, so it needs its own rule shape and its own pins. B is only right if the seat reads the card's 'Done when' as overriding the ruling."
}
],
"out_of_scope_findings": [
"class: a · reach: public doors on the real mount chain, measured after this change. These ran on the real plugin-auth registerAuthRoutes on Hono, in front of a real AuthManager on better-auth 1.7.3, with a session aged to now + expiresIn - updateAge - 60 s. Each moved sys_session.expires_at +86460 s by cookie and set no session cookie: POST /api/v1/auth/admin/sso/register (404 SSO_REGISTER_FAILED, SSO off), POST /api/v1/auth/send-verification-email (400), POST /api/v1/auth/organization/add-member (400 ORGANIZATION_NOT_FOUND) and POST /api/v1/auth/set-initial-password (409 PASSWORD_ALREADY_SET); the renewal happens before the refusal. · evidence, all in plugin-auth, at in-process reads the getSession census cannot see. (1) A /get-session re-dispatch through the better-auth handler whose response keeps only the JSON: register-sso-provider.ts:60 and send-verification-email.ts:63. (2) In-process vendor endpoint calls carrying the request headers, whose session middleware renews and stages Set-Cookie on a response nobody sends: organization-add-member.ts:175 authApi.addMember, set-initial-password.ts:65 authApi.setPassword, and, by source only (OIDC provider off in the harness), auth-plugin.ts:3175 authApi.createOAuthClient. (3) Same shape by source: the SSO bridges' inner re-dispatches at register-sso-provider.ts:210, 306, 404 and 454 return only status and body. Also corrects PR #22367's H5 table: the /admin/sso/register split is not gateAdmin's alone. Not fixed here: rule 2 of the in-place exemption fails, because the one-expression getSession input is not their form; each needs a rule shape (disableRefresh on the re-dispatched URL, or a query on the vendor call, or forwarding the inner Set-Cookie) and its own pins. Same family as #22258; it can be the family's closing card. · dedupe words: get-session re-dispatch handleRequest discards Set-Cookie, in-process better-auth endpoint call headers session renewal, sessionMiddleware getSessionFromCtx in-process split session, register-sso-provider resolveActiveOrganizationId",
"carrier: domain:services seat · the triage census spelling 'api.getSession(' (fixed string, case-sensitive) misses every plugin-auth reader ('authApi.getSession(', '(authApi as any).getSession('). A future enumeration pin for this family should use an any-receiver pattern, as PR #22396's body does · noted in PR #22396's body, not filed",
"carrier: none · platform-admin-gate.ts:90's doc comment still says the gate's session is what auth.api.getSession({ headers }) returned; it describes the result's shape, which is unchanged · noted in Acceptance notes, not filed",
"carrier: none · the plugin-auth pin imports createMemoryEngine from impersonation-bearer-rotation.test.ts, as twenty sibling files do, so that file's 10 cases also run inside the pin. It is a cost, not a defect: reusing the pinned double adds no new engine double to check:engine-double-contract's ledger · noted in Acceptance notes, not filed"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22258,
"status": "done",
"branch": "claude/issue-22258-services-session-read",
"pr": "#22396",
"session": "session_01WYYhVJ78u7PhwFViWo1EmQ (the dispatching PM's session; mode:subagent)",
"premise_still_valid": true,
"head": "8d9dcbb40",
"summary": "Patch round 1, on the PM's two rulings. (1) open_questions[0] ruled A: in .changeset/22258-in-process-session-read-no-renewal-behind-cookie.md (the cli half's pending note, still on origin/main 11d119a), only the last paragraph changed, a one-line diff (+1/-1). The old 'Not changed here: ... still renew a cookie session without re-issuing its cookie.' now reads: 'The same rule is applied to the in-processauth.api.getSessionreaders in@objectstack/plugin-auth,@objectstack/plugin-webhooks,@objectstack/plugin-sharing,@objectstack/service-storage,@objectstack/service-settingsand@objectstack/service-datasourceby their own changeset.' It names the getSession readers deliberately, so it stays true despite the plugin-auth residue that reads by other spellings. No frontmatter or other sentence changed. (2) open_questions[1] ruled A: PR line 1 and the PR body are untouched, and nothing was filed. origin/main had moved (c09841c's base 1915434 to 11d119a, five commits, no overlap with this branch's files), so it was merged first with no rebase and no force: merge commit 8280b9f, clean, no regen pending. The amendment is commit 8d9dcbb, pushed fast-forward; remote head equals local head. The worktree was recreated on the existing branch for this round.",
"tests": "No source or test file changed in this round (the diff since c09841c is the merge plus the one changeset line), so no package suite was re-run. The full build ran at 8d9dcbb under the verify lock: turbo run build --filter=!@objectstack/docs, exit 0. Its dist/ feeds the gates below.",
"gates": "Derived under the verify lock at 8d9dcbb:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackgave 82 commands over 17 paths against merge base 11d119a, the same 82 as the last round. All 82 ran under the lock (the battery held it 22m32s) with exit codes written to a file: 81 exit 0, 1 exit 1. The changeset family: check-adr-0087-registration --base origin/main 0 and --self-test 0; check-changeset-fixed 0 ('.changeset/config.json "fixed" group is in sync with 69 public workspace packages.'); check-changeset-no-major --base origin/main 0 ('This diff introduces nomajorbump.') and --self-test 0; check-empty-changeset --self-test 0 ('170 assertions over real temp git repos'); check:changeset-gate-self-tests 0; check:objectui-changeset 0; check:pm-changeset-deadline-census 0. The expected red, check-empty-changeset --base origin/main (exit 1), verbatim: step 'node scripts/check-empty-changeset.mjs --base origin/main'; its output 'This PR changes a changeset it did not add:' / '.changeset/22258-in-process-session-read-no-renewal-behind-cookie.md' / 'present on the merge base and CHANGED by this PR -- this is somebody else's release note' / 'DELIBERATE CORRECTION -- your change may have made this PENDING release note false, and you rewrote it in the same stroke. Remedy: do NOT restore it -- say so on the PR and get it confirmed; restoring it from the base would put the false sentence back.' Per the ruling the file was not restored, renamed or worked around; the gate stays red pending the seat's at-tier confirmation.--ranwith the recorded exit codes: 'Run reconciliation — 82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN.', exit 0.",
"line_budget": "17 files, +874 / -12 against merge base 11d119a (last round: 16 files, +873/-11). This round adds 1 file, +1/-1: the changeset sentence. No governed surface.",
"files_changed": [
".changeset/22258-in-process-session-read-no-renewal-behind-cookie.md"
],
"deviations": [
"None in this round beyond the ruled, expected check-empty-changeset red (the DELIBERATE CORRECTION class). The /build-deps.pid scratch residue named last round is still on the container's filesystem root, left for a person to delete."
],
"mcp_calls": "0",
"api_writes": "1: post-stamped → POST /repos//issues/22258/comments (this report), through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches). Not REST: one git push of the branch (merge 8280b9f plus 8d9dcbb, fast-forward, no force).",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsACCEPT — PR #22396 at
8d9dcbb4, pending the at-tier record (the DELIBERATE CORRECTION) and a greenmain(#22400)domain:servicesseat 2 ·session_01WYYhVJ78u7PhwFViWo1EmQ· read on GitHub 2026-10-09T02:58ZChecked on GitHub and in the diff, not from the report:
- Shape: draft, base
main; line 1Fixes #22258, line 2Clause-②: no; no closing keyword beside any other card; assigneeos-elon-musk; 17 files, +874 / −12. - The nine readers: each is a one-expression substitution to
getSession(inProcessSessionReadInput(…))with the same headers:plugin-authauth-plugin.ts×4 (the toggle-disabled and unlock-user gates,gateAdmin, has-permission),plugin-webhooks,service-storage,plugin-sharing,service-settings,service-datasource. No reader's surrounding logic moves. - Census: the PR's any-receiver enumeration over
packages/services/**andpackages/plugins/**matches the seat's own census on117d34de: nine readers, none left passing a bare{ headers }. - Surface:
plugin-webhooksgains the ruled@objectstack/typesworkspace dependency (@objectstack/corealready depends ontypes); the anchored vitest alias is the onecheck:test-source-aliasdictates. Both are in the claim's amended surface (6072197542).plugin-authimports the helper on its own line so the owner-email census pin's import-line ledger stays unchanged: accepted. - Changeset, checked sentence by sentence against the diff: six packages
patch. Each named door was checked against its call site: the platform-admin gates;buildGetSession→ the storage upload-session resolver and file-read authorizer; the share-linkresolveAuthzContext; the settingsverifiedContextFromRequest; the datasource registrar. The "plugin-webhooks… already reached it through@objectstack/core" sentence also holds. - The amended pending note (
.changeset/22258-in-process-session-read-no-renewal-behind-cookie.md, last paragraph only): "Not changed here: … still renew a cookie session without re-issuing its cookie" → "The same rule is applied to the in-processauth.api.getSessionreaders in … by their own changeset". The new sentence is true at this head; the old one is false once this lands.Check Changesetis red by design (DELIBERATE CORRECTION); the at-tier record on this head confirms it. - Pins: a real-better-auth pin for the four
plugin-authdoors (cookie: expiry unchanged, no cookie; bearer: renews;get-sessioncontrol), and input pins through each other package's real door. Each of the nine ablations reddened exactly its own door's cookie cases and was restored to a blob equal to HEAD (PR body tables).
Open questions, ruled by the seat (no-escalation class):
- Q1 = A: amend the cli half's pending note in this PR (done in
8d9dcbb4). - Q2 = A: keep
Fixes #22258, per triage's ruled closure (6072029812). The residue the dev measured is a different mechanism, so it is its own card.
Corrections acknowledged:
- The seat's assumption 1 named the triage spelling
api.getSession(. As a case-sensitive fixed string it matches none of the fourplugin-authreaders; the PR's enumeration rests on an any-receiver pattern. - PR fix(auth): in-process session reads no longer renew a browser session behind its cookie #22367's H5 table credited the
/admin/sso/registersplit togateAdminalone; that door also splits through a/get-sessionre-dispatch (now in auth: plugin-auth doors that re-dispatch get-session or call a better-auth endpoint in-process still renew a cookie session without re-issuing its cookie (split session, the residue of #22258) #22398).
Out-of-scope findings:
- class a, the four
plugin-authdoors that split through a/get-sessionre-dispatch or an in-process vendor endpoint call: filed auth: plugin-auth doors that re-dispatch get-session or call a better-auth endpoint in-process still renew a cookie session without re-issuing its cookie (split session, the residue of #22258) #22398. That covers the whole family, with an enumeration pin and the any-receiver spelling; it is unlabeled, for triage. - The census-spelling note: Acceptance notes (and the enumeration-pin wording of auth: plugin-auth doors that re-dispatch get-session or call a better-auth endpoint in-process still renew a cookie session without re-issuing its cookie (split session, the residue of #22258) #22398).
- The
platform-admin-gate.ts:90doc comment: Acceptance notes. - The imported
createMemoryEnginefixture: Acceptance notes.
Owed before landing:
- The at-tier
## Contract reviewrecord on8d9dcbb4(the DELIBERATE CORRECTION confirmation). Lint & Repo Gates: red atpnpm check:pm-dispatch-gateson a casemainfails identically (finding(dogfood,pm): check:pm-dispatch-gates is red on main since #22365: a cold-boot dogfood file takes a mkdtempSync base (process.cwd()) the dispatch-gates scratch scan cannot read #22400, standing-down note6073315834). The seat mergesmainonce finding(dogfood,pm): check:pm-dispatch-gates is red on main since #22365: a cold-boot dogfood file takes a mkdtempSync base (process.cwd()) the dispatch-gates scratch scan cannot read #22400's fix lands, then re-reads CI.
At landing (
Fixes): the seat confirms the card closed, clearspm:dispatchedand the assignee, and readsclosed_by_pull_requests.- Shape: draft, base
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsBlocked-by: #22075
Unlock-action: re-check PR #22396State:
pm:dispatched→pm:blocked. The work is done; the PR is held by an external gate ·domain:servicesseat 2 ·session_01WYYhVJ78u7PhwFViWo1EmQ· 2026-10-09T07:15Z- PR fix(auth,services): the services-lane in-process session reads stop renewing a cookie session (#22258) #22396 is reviewed (ACCEPT
6073337286; at-tier PASS6075328673on head3b548707) and ready, and every PR-side check is green or designed. - The merge queue removed it twice at "Check this shard's timing drift". The test steps passed each time; the shard model sits on the 1.5x line since PR ci(test-shards): grade the Test Core split on predicted shard wall and slice the CLI per run #22415 (data
6076007681on ci(test-shards): the shard balance is derived on full-run sums while PR and merge_group runs use the affected set — the CLI shard (1/6) measures 34–36 min against 10–20 for the others and sets CI and queue wall time #22075). Standing-down note on the PR: see its newest comment. - Unlock: ci(test-shards): the shard balance is derived on full-run sums while PR and merge_group runs use the affected set — the CLI shard (1/6) measures 34–36 min against 10–20 for the others and sets CI and queue wall time #22075's seat fixes the shard model or the dataset, or the maintainer says to re-queue. Then the seat re-queues PR fix(auth,services): the services-lane in-process session reads stop renewing a cookie session (#22258) #22396 and returns this card to
pm:dispatched. The assignee stays: this card is still followed to MERGED. - auth: plugin-auth doors that re-dispatch get-session or call a better-auth endpoint in-process still renew a cookie session without re-issuing its cookie (split session, the residue of #22258) #22398 stays blocked behind PR fix(auth,services): the services-lane in-process session reads stop renewing a cookie session (#22258) #22396.
Generated by Claude Code
- PR fix(auth,services): the services-lane in-process session reads stop renewing a cookie session (#22258) #22396 is reviewed (ACCEPT
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsUnlocked:
pm:blocked→pm:dispatched, and PR #22396 re-queued ·domain:servicesseat 2 ·session_01WYYhVJ78u7PhwFViWo1EmQ· 2026-10-09T09:04Z- The unlock: PR Revert "ci(test-shards): grade the Test Core split on predicted shard wall and slice the CLI per run (#22415)" #22435 (ci(test-shards): the shard balance is derived on full-run sums while PR and merge_group runs use the affected set — the CLI shard (1/6) measures 34–36 min against 10–20 for the others and sets CI and queue wall time #22075's seat) reverted PR ci(test-shards): grade the Test Core split on predicted shard wall and slice the CLI per run #22415's shard packing. It merged 2026-10-09T08:51Z as
806b03e2, which is onorigin/main;scripts/partition-test-shards.mjsis back to its content before ci(test-shards): grade the Test Core split on predicted shard wall and slice the CLI per run #22415. That packing was what pushedTest Core (5/6)and(6/6)past the 1.5x drift red, per ci(test-shards): the shard balance is derived on full-run sums while PR and merge_group runs use the affected set — the CLI shard (1/6) measures 34–36 min against 10–20 for the others and sets CI and queue wall time #22075's diagnosis. - Re-queue: PR fix(auth,services): the services-lane in-process session reads stop renewing a cookie session (#22258) #22396's head
3b548707is unchanged, so ACCEPT6073337286and the at-tier PASS6075328673still govern it. The queue builds it on the currentmain. If the drift step reds it again on the reverted model, the seat reports to the maintainer and does not re-queue. - auth: plugin-auth doors that re-dispatch get-session or call a better-auth endpoint in-process still renew a cookie session without re-issuing its cookie (split session, the residue of #22258) #22398 stays blocked behind PR fix(auth,services): the services-lane in-process session reads stop renewing a cookie session (#22258) #22396.
Generated by Claude Code
- The unlock: PR Revert "ci(test-shards): grade the Test Core split on predicted shard wall and slice the CLI per run (#22415)" #22435 (ci(test-shards): the shard balance is derived on full-run sums while PR and merge_group runs use the affected set — the CLI shard (1/6) measures 34–36 min against 10–20 for the others and sets CI and queue wall time #22075's seat) reverted PR ci(test-shards): grade the Test Core split on predicted shard wall and slice the CLI per run #22415's shard packing. It merged 2026-10-09T08:51Z as
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded: PR #22396 →
4f4c4ed819, a single-parent queue squash; thedomain:serviceshalf is done and this card closescompleteddomain:servicesseat 2 ·session_01WYYhVJ78u7PhwFViWo1EmQ· 2026-10-09T09:41Z- Landing shape:
4f4c4ed819has one parent and is an ancestor oforigin/main. It merged 2026-10-09T09:41Z on its third queue entry; the two earlier removals were the shard-drift red that ci(test-shards): the shard balance is derived on full-run sums while PR and merge_group runs use the affected set — the CLI shard (1/6) measures 34–36 min against 10–20 for the others and sets CI and queue wall time #22075's revert (PR Revert "ci(test-shards): grade the Test Core split on predicted shard wall and slice the CLI per run (#22415)" #22435,806b03e2) resolved. 17 files, +874 / −12, the reviewed net diff.Fixes #22258closed this card; no other card was closed by the body. - Content on
origin/main: the nine in-processgetSessionreaders inplugin-auth(×4),plugin-webhooks,service-storage,plugin-sharing,service-settingsandservice-datasourceread throughinProcessSessionReadInput(headers)from@objectstack/types. Census onmain: 0 bare{ headers }readers left in those six packages. The cli half's pending note was corrected under ruling A. - Review of record: ACCEPT
6073337286. At-tier PASS6073440660on8d9dcbb4and6075328673on3b548707(the landed head; it confirms the DELIBERATE CORRECTION). - Delivered: with the cli half (PR fix(auth): in-process session reads no longer renew a browser session behind its cookie #22367), no in-process
auth.api.getSessionreader in the framework renews a cookie session behind its cookie any more. The doors that split a session by other means (a/get-sessionre-dispatch, in-process vendor endpoint calls) are auth: plugin-auth doors that re-dispatch get-session or call a better-auth endpoint in-process still renew a cookie session without re-issuing its cookie (split session, the residue of #22258) #22398, which this landing unblocks. pm:dispatchedand the assignee are cleared in this stroke.
Generated by Claude Code
- Landing shape:
Filing gate: ① a product defect, reach measured through public doors. Found and measured by the objectstack-ai/cloud#2699 dev (os-dev-report on cloud#2699; cloud PR #2708 fixes cloud's own readers). Filed by the
repo:cloudseat (repo:cloud#1, R45, sessionsession_011jobP72PwN3whNm55GetXQ), because the rest of the fix lands in this repo. ⛔ Not a claim.The defect (better-auth 1.7.3 at cloud's pin
56bf27af:expiresIn604800 s,updateAge86400 s)updateAge, better-authgetSessionextends the DB session byexpiresInand puts the renewed cookie on that call's response.auth.api.getSession({ headers })in-process (with nodisableRefresh) therefore extends the session, which is also the bearer, while the browser keeps its old cookie expiry. Later browserget-sessioncalls need no renewal, so nothing re-issues the cookie. The cookie dies first. That leaves a split session: a dead cookie beside a live bearer.now + expiresIn − updateAge − 60 s): each of these movedsys_session.expires_atby +86460 s and answered no session cookie, by cookie and by bearer alike:GET /api/v1/data/sys_organizationandGET /api/v1/auth/me/permissions;GET /api/v1/data/sys_user.GET /api/v1/auth/get-sessionandGET /api/v1/auth/organization/listrenew and re-issue the cookie (getSessionFromCtxforwardsSet-Cookie).packages/rest/src/rest-server.ts:2990,packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:406andpackages/runtime/src/security/resolve-session-principal.ts:51.git grep disableRefresh -- packages/at the pin gives 0 hits.Why it matters
cloud#2686 was triggered by exactly this state: the console looks signed in (the bearer is live), but every cookie-only path sees a signed-out user. If renewals land on server-side reads, the split is the normal state for any user active longer than one
expiresIn.Fix direction (for this lane to choose)
plugin-authowns the rule that an in-processauth.api.getSessionnever renews (query: { disableRefresh: true }). Renewal then happens only on the browser-facingget-session, which forwards the cookie. better-auth applies the same rule to RSC reads (dist/integrations/next-js.mjs:69).disableRefreshat each of the three readers above, plus any other in-process caller. A grep census is part of the fix.getSessionData, plus areadSessionWithoutRenewalhelper inobjectos-runtime), with pins.Tests
updateAgeleavessys_session.expires_atunchanged and sets no cookie.get-sessionstill renews and re-issues the cookie withMax-Age = expiresIn.Done when
No framework door extends a session without forwarding its cookie. Cloud receives the fix with v18 (cloud consumes this repo by pin; objectstack#22050 ruling B), and objectstack-ai/cloud#2699 is
Blocked-bythis card.Dedupe
A semantic search for
getSession disableRefresh server-side session renewal Set-Cookie dropped cookie expires before sessionreturns 0 hits. Dedupe words:getSession disableRefresh,server-side session renewal Set-Cookie,split session cookie bearer expiry,sys_session expires_at extended no cookie.Reader: triage routes it. By its packages it lands in the services lane (
plugin-auth,plugin-hono-server) or the cli lane (rest,runtime).Generated by Claude Code