hub: presence write audit
tsk-14f4b0cv304p done Maity Coder Claude 001 (maity-coder-claude-001)
Measure, read-only, how often an agent's calls write membership or instance last_seen, or any presence row, per minute: an idle wait loop vs. active work; and whether anything persists per heartbeat. Numbers as a note on this card. Report only, no product code; Oliver's inbox decisions on presence and metrics build on it.
Result (card notes, §6.16, are not on the hub yet, so the report is here)
Presence write audit (main 00ad5388), measured on a scratch hub (SQLite, test client, a statement listener counting INSERT/UPDATE/DELETE per table and the rows each changed). Read-only: no product code changed.
Per call: what an authenticated call writes
| Call | Statements | Rows changed | Which rows |
|---|---|---|---|
| Agent long-poll GET …/events?wait=… (what maity wait sends), idle, nothing to read | 5 | 3 | credential.last_used_at, membership.last_seen_at, agent_session.last_seen_at; plus one UPDATE card SET claim_session_id … and one UPDATE agent_session SET acknowledged_seq … that change 0 rows in the steady state |
| Agent plain read (wait=0) | 6 | 3 | the same; the lease renewal runs as well, 0 rows without a claim |
| Agent call while holding a claim (e.g. GET /me) | 5 | 4 | the three presence rows plus card.lease_until of each held card |
| POST /me/resume | 10 | 9 | ends the old session and inserts a new agent_session, inserts session_started into event, plus credential, membership, claims |
| Human dashboard read (e.g. GET …/tasks) | 4 | 3 | human.last_seen_at, human_session.last_seen_at, membership.last_seen_at; the membership update is WHERE human_id = ?, so it writes every active membership of that human (N rows with N projects) |
Per minute
- Idle agent in `maity wait`: the CLI long-polls with
wait=55(cli.pyLONG_POLL), so about 1.1 calls/min. That is ~3.3 row updates and ~5.5 write statements per minute per idle agent, all three presence timestamps. With 10 idle agents: ~33 rows/min, ~2,000/h. - Active agent: every call writes 3 presence rows, plus one row per held card for the lease. At a typical 5–20 calls/min that is 15–60 presence row updates/min, plus 5–20 lease rows per held card.
- Human with the dashboard open: 3 + (N−1) rows per request. The web client reads the open project's events every 5 s (
EVENTS_MS), the counts every 15 s (POLL_MS, at least one request) and long-polls notifications at 55 s: ≥17 requests/min, so ≥51 presence rows/min per open tab with one project, plus 17 more per further project. The largest writer by far. - Runner: same pattern (
runner_credential.last_used_at,runner.last_seen_at,runner_session.last_seen_at, caller.py ~429–441), with its 55 s long-poll; not separately measured.
Does anything persist per heartbeat? No. A long-poll only updates rows in place: no INSERT, no event, no history row, so nothing grows with idle time. What grows is per resume: one agent_session row and one session_started event each.
Observations for the decision (no change proposed here):
- The three presence timestamps are written on every call, with no throttling (e.g. skip the write when the stored value is younger than N seconds).
- Two statements per long-poll match 0 rows but are still executed: the
claim_session_idupdate and theacknowledged_sequpdate. On InnoDB a 0-row UPDATE still takes and releases locks on the probed index range. - A human request writes all of that human's memberships.
credential.last_used_at,membership.last_seen_atandagent_session.last_seen_atlargely carry the same instant for an agent;last_used_viais the only extra fact.
Not measured: real PostgreSQL/MariaDB timings (Docker is not reachable for me on the VM right now); the counts poll may issue more than one request per round.