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
Commit 431327b
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-auth): write the session last_activity_at heartbeat on every deployment (#25398)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoRcazFf3zbHR5zgBPMo64bA)_Fixes#25343
Clause-②: no
Metadata-protocol: none
The second and last half of #25343. The column half (`user_agent` and
`last_activity_at` on the Sessions list views) landed as #25368. This
half makes `last_activity_at` actually 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 from
`customSession`):
- **The heartbeat runs on every deployment.** The early return when the
idle and absolute limits are both off (the default) is gone, and the
`last_activity_at` write is no longer inside `if (idleMin > 0)`. It is
still at most one write per session per minute
(`SESSION_HEARTBEAT_INTERVAL_MS`, 60 s, the same threshold as before).
- **With both limits off, the read is throttled too.** A per-process,
per-session memo (`SessionHeartbeatMemo`) remembers when each session's
`sys_session` row was last read and skips the `findOne` (and so the
write) for 60 s after it. The memo is bounded at
`SESSION_HEARTBEAT_MEMO_MAX` (10,000) sessions, least recently read
evicted first; an evicted session is simply read once more. It is held
in a `WeakMap` keyed 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.
- **With a limit on, nothing about enforcement changes.** The memo is
not consulted, so every request still reads the row, exactly as
revocation needs. The heartbeat now also runs when only the absolute
limit is configured (before, that configuration wrote no heartbeat
either).
- A failed read is not memoised, so the next request reads again. When
both limits are off, the read-failure warning now names what was lost
(the heartbeat) instead of saying the idle and absolute limits were not
evaluated, which on that deployment would name controls nobody turned
on. With a limit on, the message is unchanged.
- The write-failure path (`logDurabilityDegradation` on the heartbeat
update) is unchanged: same text, same meta, only re-indented one level.
`auth-manager.ts`: the `enforceSessionControls` docblock no longer says
"No-op when both are off" (comment only).
## Measured cost
Under the default config, per live session per minute:
- **One request** reads `sys_session` once and, when `last_activity_at`
is older than a minute, writes it once. Every other request in that
minute reads nothing extra (the memo).
- **That same request resolves the caller's grants twice.** Core's
request-grants 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 in `session-grants-resolved-once.test.ts` for 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.
- ADR-0127 D8 is confirmed on `main` (`docs/adr/0127-*.md` around :125):
`sys_session` is 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
- New `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 that `findOne` is 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
exactly `update sys_session` from 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.
- Ablation: `idleMin > 0 &&` put back in front of the heartbeat
condition (`scripts/ablation-replace.mjs`, anchor 1 to 0, blob
`b6da2ab9` to `8519f0aa`; the tests import `src/`, no `dist/` 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 to `b6da2ab9`, `git diff HEAD`
empty.
- `pnpm --filter @objectstack/plugin-auth test`: 188 files, 3538 passed,
10 skipped (at `302d5e29`; the two commits after it change only
`session-heartbeat-default-config.test.ts` and the ledger below, and the
three touched suites re-ran at `af4a5fe5`: 326/326). `pnpm --filter
@objectstack/plugin-auth typecheck` (tsc, examples,
`check:test-typecheck`) exit 0 at `af4a5fe5`; the test-typecheck debt
ledger is unchanged.
- The new suite's engine double opens `findOne` / `update` with
`assertEngineFindOnePredicate` / `assertEngineUpdateDispatch`, and
`scripts/engine-double-contract.pinned.json` gains its two rows
(`check-engine-double-contract.mjs --write`, 0 lost).
- Gates at `af4a5fe5`: `dispatch-gates --commands` derived 79; `--ran`
with recorded exit codes: 79 accounted, 78 run, 1 NOT MEASURED (`node
scripts/check-plugin-teardown-shape.mjs --self-test` exits 3, it needs
an unshallowed clone). `pnpm check:dts-closure` and `pnpm
check:dual-build-cjs-loads` first exited 1 on a local
`packages/plugins/organizations/dist` with no `index.d.ts` (a package
this diff does not touch); after `pnpm --filter
@objectstack/organizations build --force` both exit 0. `pnpm
check:durability-log-level` and `pnpm check:engine-double-contract` exit
0.
## Acceptance notes
- The heartbeat's write-failure message still explains its consequence
in idle-timeout terms ("this session will be revoked as idle EARLIER").
On a deployment with no idle timeout the real consequence is only a
stale Last Activity. The dispatch asked for that path to stay exactly as
it is, so it is unchanged here.
- The memo is per process, as ruled. On a multi-node deployment a
session served by several nodes can be read once a minute per node; the
write is still guarded by the row's own `last_activity_at`.
- The raw `user_agent` shown as browser and OS is the objectui polish
item noted on the card, not part of this PR.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01URyDu4ZGxRVsi6YiRz5Z8g)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The session activity heartbeat now runs on every deployment. `sys_session.last_activity_at` is "the last request on this session", and Setup → Sessions shows it so an administrator can tell two of one user's sessions apart. Until now it was written only while `sessionIdleTimeoutMinutes` was above 0, so on a default deployment (no idle or absolute limit) the Last Activity column stayed empty.
9
+
10
+
It is still written at most once a minute per session. When neither the idle nor the absolute limit is configured, the `sys_session` read that feeds it is throttled too: a per-process memo, per session and bounded in size, skips the read for 60 seconds after one. With a limit configured, every request still reads the session row, as enforcement needs.
0 commit comments