fix(cost): bill a session outside every workspace to its Linux account's user

A script's claude -p in a temp folder (the SG Flow eval runner strips the
pane's env) had no workspace, so its spend reached nobody's Today, the Users
page or a spend quota — ~$57 of a $74 day was missing from the status bar.

sessions.account records the Linux account a session ran as (the hook's
peer uid, else its folder for poller-made and old rows); a session no
workspace owns is owned by that account's user — a member's to them, the
host's to the admin ticked 'Owns this server's host account' (or the only
admin). Workspace ownership still wins.
This commit is contained in:
2026-10-30 08:50:34 +07:00
parent c28395402b
commit 783b7f4101
16 changed files with 331 additions and 16 deletions
+11 -1
View File
@@ -732,7 +732,17 @@ An admin limits what each user may spend, in dollars, on the Users page
every account change; each one re-applies at once (`sweep`).
Spend is the users page's own figure: the tokens of the sessions the user
OWNS through a workspace, priced by `calculateCost`. The Users table shows
OWNS, priced by `calculateCost`. A session is owned through its workspace;
one no workspace owns — a script's `claude -p` in a temp folder, a CLI in a
plain terminal — is owned by the user of the Linux account it ran as
(`sessions.account`, `server/lib/session-account.js`: the hook's peer uid,
else the session's folder): a member's account bills to that member, the
host's to the admin ticked **Owns this server's host account** in Edit user
(`PATCH /api/users/:id` `{hostAccount: true}`, at most one), or with none
ticked to the only admin. The same rule feeds the status bar's **Today**.
A process that drops the pane's environment also drops
`AGENTHUB_QUOTA_FILE`, so its prompts are not refused by the hook — but its
spend still counts, and stops the user's next session. The Users table shows
each user's spend against their quota; the user sees their own as **Quota N%**
in the status bar (`GET /api/me/quota`).