feat(hub): persist agent turns and report them through hapi usage

Whether a session's agent is thinking lived only in the session cache,
so once a turn ended nothing could say how long the agent had worked,
and a hub restart forgot even the turn in progress. Tools that track
time (Weft, for one) had no way to see when agents on other machines
were busy.

Schema v28 adds session_turns: a row opens when a session starts
thinking (a prompt queued through the hub, the agent reporting it,
or a patch) and closes when it stops, the session ends, or its
keepalives expire, at the last keepalive. Rows are only written on
those transitions, never per keepalive. Turns move with a session
when it is merged away.

At startup the hub closes turns left open by the previous process at
the session's newest message. If the agent carries on thinking within
10 minutes of the restart, the same turn continues, so the work between
that message and the restart isn't lost. The deadline counts from the
restart, and a keepalive or patch reporting idle, the session ending,
or a new prompt ends the chance: those start a new turn.

GET /api/activity returns, for the caller's namespace, each session's
turns in a window (default 24 h, at most 31 days) with what identifies
it (flavor, agent session id, machine, path, title) and the permission
prompts and questions it waited on, taken from agent_state.

`hapi usage [--since] [--until] [--json]` prints a summary or a
hapi.activity.v1 report for other tools, with distinct exit codes for
bad arguments (2), missing or refused credentials (3) and a hub too
old to answer (4). The summary counts only the part of each turn
inside the window and subtracts overlapping waits once. It reuses the
peer commands' token exchange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
weishu
2026-10-29 08:48:06 +08:00
co-authored by Claude Opus 5.5
parent b4be7c2d93
commit 23122ccbc0
33 changed files with 1465 additions and 23 deletions
+40
View File
@@ -1134,3 +1134,43 @@ export type UsageSummaryResponse = {
byModel: UsageSummaryBucket[]
updatedAt: number
}
/** A stretch an agent was working: from `thinking` turning on to it turning
* off. `end` is null while the turn is still going. */
export type AgentActivityTurn = {
start: number
end: number | null
endReason: 'idle' | 'session_end' | 'expired' | 'hub_restart' | null
startedBy: 'hub_prompt' | 'agent' | 'patch'
}
/** The agent waiting on the user inside a turn: a permission prompt or a
* question. `end` is null while it is still waiting. */
export type AgentActivityWait = {
start: number
end: number | null
kind: 'permission' | 'input'
tool: string
}
export type AgentActivitySession = {
id: string
flavor: string | null
/** The agent's own session id (Claude session, Codex thread, ...). */
agentSessionId: string | null
machineId: string | null
host: string | null
machineName: string | null
path: string | null
title: string | null
turns: AgentActivityTurn[]
waits: AgentActivityWait[]
}
/** GET /api/activity: when agents in the caller's namespace were working. */
export type AgentActivityResponse = {
since: number
until: number
generatedAt: number
sessions: AgentActivitySession[]
}