Repository navigation
fix(plugin-auth): write the session last_activity_at heartbeat on every deployment - #25398
Conversation
…yment (WIP) Claude-Session: https://claude.ai/code/session_01URyDu4ZGxRVsi6YiRz5Z8g Co-authored-by: Claude <noreply@anthropic.com>
… 60 s memo Claude-Session: https://claude.ai/code/session_01URyDu4ZGxRVsi6YiRz5Z8g Co-authored-by: Claude <noreply@anthropic.com>
…artbeat; changeset Claude-Session: https://claude.ai/code/session_01URyDu4ZGxRVsi6YiRz5Z8g Co-authored-by: Claude <noreply@anthropic.com>
…oth limits off Claude-Session: https://claude.ai/code/session_01URyDu4ZGxRVsi6YiRz5Z8g Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URyDu4ZGxRVsi6YiRz5Z8g Co-authored-by: Claude <noreply@anthropic.com>
…ine contract Claude-Session: https://claude.ai/code/session_01URyDu4ZGxRVsi6YiRz5Z8g Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older
|
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Tesla · project thread
Fixes #25343
Clause-②: no
Metadata-protocol: none
The second and last half of #25343. The column half (
user_agentandlast_activity_aton the Sessions list views) landed as #25368. This half makeslast_activity_atactually get written on a default deployment, so the Last Activity column is filled.Ruling: 6358844136, letter A (maintainer 「同意」, 2026-10-23), per analysis 6357589247.
What changes
packages/plugins/plugin-auth/src/auth-manager-password-session-policy.ts,enforceSessionControls(run per authenticated request fromcustomSession):last_activity_atwrite is no longer insideif (idleMin > 0). It is still at most one write per session per minute (SESSION_HEARTBEAT_INTERVAL_MS, 60 s, the same threshold as before).SessionHeartbeatMemo) remembers when each session'ssys_sessionrow was last read and skips thefindOne(and so the write) for 60 s after it. The memo is bounded atSESSION_HEARTBEAT_MEMO_MAX(10,000) sessions, least recently read evicted first; an evicted session is simply read once more. It is held in aWeakMapkeyed by the data engine, so in a running process it is the one engine's memo, and a second engine (another manager, a test double) never answers from it.logDurabilityDegradationon the heartbeat update) is unchanged: same text, same meta, only re-indented one level.auth-manager.ts: theenforceSessionControlsdocblock no longer says "No-op when both are off" (comment only).Measured cost
Under the default config, per live session per minute:
sys_sessiononce and, whenlast_activity_atis older than a minute, writes it once. Every other request in that minute reads nothing extra (the memo).packages/core/src/security/request-grants-memo.ts) declines when a write lands inside the request, and the heartbeat is such a write. Measured insession-grants-resolved-once.test.tsfor an organization member: the declined request reads 6 more grant rows than a memo-served one (twice.reads.length - once.reads.length, the existing pin). So the price is about 6 extra grant reads per session per minute, on top of the one read and one write.main(docs/adr/0127-*.mdaround :125):sys_sessionis deliberately outside the grants cache's watched set, so the heartbeat does not retire the authorization cache. The cost above is the per-request memo, not the cache.Verification
session-heartbeat-default-config.test.ts, 11 cases: heartbeat written under the default config (first request, and again after more than 60 s); the memo skips the read and the write within 60 s and reads again at 60 s + 1 ms; a row touched by another process 20 s ago is read but not written; per session; per engine; a failed read is not memoised and its warning names the heartbeat; a revoked session is read once a minute and never touched; the memo cap evicts the least recently read; with an idle timeout or an absolute max on, two requests a second apart both read.auth-manager.test.ts: the pin "is a no-op when idle + absolute are both off" asserted thatfindOneis never called, which is the behaviour the ruling reverses. It is rewritten in place to "revokes nothing but still writes the heartbeat". The idle-timeout and absolute-max pins beside it are unchanged and green.session-grants-resolved-once.test.ts: its warm-up asserted that the warm request writes nothing. A session's first request now writes the heartbeat, so the warm-up makes one request per session and expects exactlyupdate sys_sessionfrom each; the measured requests after it write nothing, as before, and every count in the file is unchanged. The pin "a write inside the request: the memo declines" gains a default-config row beside its idle-timeout row.idleMin > 0 &&put back in front of the heartbeat condition (scripts/ablation-replace.mjs, anchor 1 to 0, blobb6da2ab9to8519f0aa; the tests importsrc/, nodist/on the path). 15 cases went red, including both "heartbeat written under the default config" pins, the rewritten no-op pin and the default-config grants-memo row. Restored: blob back tob6da2ab9,git diff HEADempty.pnpm --filter @objectstack/plugin-auth test: 188 files, 3538 passed, 10 skipped (at302d5e29; the two commits after it change onlysession-heartbeat-default-config.test.tsand the ledger below, and the three touched suites re-ran ataf4a5fe5: 326/326).pnpm --filter @objectstack/plugin-auth typecheck(tsc, examples,check:test-typecheck) exit 0 ataf4a5fe5; the test-typecheck debt ledger is unchanged.findOne/updatewithassertEngineFindOnePredicate/assertEngineUpdateDispatch, andscripts/engine-double-contract.pinned.jsongains its two rows (check-engine-double-contract.mjs --write, 0 lost).af4a5fe5:dispatch-gates --commandsderived 79;--ranwith recorded exit codes: 79 accounted, 78 run, 1 NOT MEASURED (node scripts/check-plugin-teardown-shape.mjs --self-testexits 3, it needs an unshallowed clone).pnpm check:dts-closureandpnpm check:dual-build-cjs-loadsfirst exited 1 on a localpackages/plugins/organizations/distwith noindex.d.ts(a package this diff does not touch); afterpnpm --filter @objectstack/organizations build --forceboth exit 0.pnpm check:durability-log-levelandpnpm check:engine-double-contractexit 0.Acceptance notes
last_activity_at.user_agentshown as browser and OS is the objectui polish item noted on the card, not part of this PR.Generated by Claude Code