Work: signals count what sessions cost, who uses which harness, and stalled as no activity
Home and the Workspace overview now show what harness sessions cost and who has taken up which harness, and the contract already carried the fields; the service filled none of them. Stalled meant "open over a week", which flags long-running work that is moving and misses work that quietly stopped. - A week's session cost sums sessions.cost_usd of sessions started in it, subagents included (each reports only its own transcript). - A pull request's share splits each session's cost evenly among the pull requests it counts with: its own ties, or for an untied subagent its root session's. - Harnesses: people's root sessions by repository and harness, all history, under the name on their latest. - Stalled: open at the cut, opened at least three days before, and no activity in the three days before it: comments of any kind (timeline events included; pushes are not one), the latest push, acceptance checks, statuses and check runs on the head commit, session entries, and tied sessions or their subagents running then. Only the pulls that can be stalled are read, each source by its own index. - 0054 indexes sessions by start, for the week cost and day counts. Part of #157
- G1t-Session:
- ses_01m573jkygehmtasvj8b9g6p9f
| 50 | 50 | pub review_seconds: f64, | |
| 51 | 51 | /// Pull requests open when the week ended (now, for the current week), | |
| 52 | 52 | /// opened at least three days before, with no activity in the three | |
| 53 | − | /// days before it ended: no push, comment, review, timeline event, | |
| 54 | − | /// check or session entry. | |
| 53 | + | /// days before it ended: no comment, review or timeline event, no push | |
| 54 | + | /// (only its latest is known), no check or status on its head commit | |
| 55 | + | /// (only its current head is known) or acceptance check, no session | |
| 56 | + | /// entry, and no tied session (or its subagent) running. | |
| 55 | 57 | pub stalled: u32, | |
| 56 | 58 | /// What the harness sessions started in the week cost, as they | |
| 57 | 59 | /// reported it (estimated at list price from their token usage). | |
| ⋯ | |||
| 110 | 112 | pub item: Option<u32>, | |
| 111 | 113 | pub milestone: Option<u32>, | |
| 112 | 114 | /// Its share of what the harness sessions tied to it cost: each | |
| 113 | − | /// session's cost split evenly among the pull requests it is tied to. | |
| 115 | + | /// session's cost split evenly among the pull requests it is tied to | |
| 116 | + | /// (a subagent tied to none counts with its root session's). Unknown | |
| 117 | + | /// costs count as nothing. | |
| 114 | 118 | pub session_cost_usd: f64, | |
| 115 | 119 | } | |
| 116 | 120 | ||
| 1862 | 1862 | /** | |
| 1863 | 1863 | * One repository's week from UTC day `start`; empty weeks are left out. | |
| 1864 | 1864 | * `stalled`: open when the week ended (now, for this week), opened three | |
| 1865 | − | * days or more before, and no activity (push, comment, review, timeline | |
| 1866 | − | * event, check, session entry) in the three days before it ended. | |
| 1865 | + | * days or more before, and no activity (comment, review, timeline event, | |
| 1866 | + | * its latest push, a check or status on its current head, an acceptance | |
| 1867 | + | * check, a session entry, a tied session or its subagent running) in the | |
| 1868 | + | * three days before it ended. | |
| 1867 | 1869 | * `sessionCostUsd`: what the harness sessions started in the week cost, | |
| 1868 | 1870 | * estimated at list price from the token usage they reported. | |
| 1869 | 1871 | */ | |
| ⋯ | |||
| 1896 | 1898 | mergedAt: string | null; | |
| 1897 | 1899 | item: number | null; | |
| 1898 | 1900 | milestone: number | null; | |
| 1899 | − | /** Its share of its harness sessions' cost: each session's split evenly among the pull requests it is tied to. */ | |
| 1901 | + | /** | |
| 1902 | + | * Its share of its harness sessions' cost: each session's split evenly | |
| 1903 | + | * among the pull requests it is tied to (a subagent tied to none counts | |
| 1904 | + | * with its root session's). | |
| 1905 | + | */ | |
| 1900 | 1906 | sessionCostUsd: number; | |
| 1901 | 1907 | }; | |
| 1902 | 1908 | /** One person's use of one harness in one repository, root sessions only. */ | |
| 1 | + | -- Sessions by when they started, for workspace_signals: what the sessions | |
| 2 | + | -- started in each week cost (signals/weeks.sql) and how many started each | |
| 3 | + | -- day (signals/days.sql), without reading every session of a repository. | |
| 4 | + | CREATE INDEX IF NOT EXISTS sessions_by_start ON sessions (repo_id, started_at); |
| 20 | 20 | //! - **Draft time**: for an agent's pull request whose first tied session | |
| 21 | 21 | //! started before it opened, the seconds from that start to its opening, | |
| 22 | 22 | //! counted in the week it opened. | |
| 23 | − | //! - **Stalled** in a week: opened more than seven days before the week | |
| 24 | − | //! ended (now, when that is earlier) and not finished by then: not | |
| 25 | − | //! merged, and not closed (a closed one's `updated_at` stands in for when | |
| 26 | − | //! it closed). Drafts count. | |
| 23 | + | //! - **Stalled** in a week: at its cut (when the week ended, or now when | |
| 24 | + | //! that is earlier), open (not merged, and not closed, before it: a | |
| 25 | + | //! closed one's `updated_at` stands in for when it closed), opened at | |
| 26 | + | //! least three days before, and with no activity in the three days | |
| 27 | + | //! before it. Drafts count. Activity is a comment of any kind on it | |
| 28 | + | //! (timeline events among them: labels, assignees, review requests, | |
| 29 | + | //! milestones, ready, draft, closed, reopened, base changes, the merge | |
| 30 | + | //! queue, auto-merge, branches; a push is no timeline event), its | |
| 31 | + | //! latest push (the only one `head_pushed_at` keeps), its acceptance | |
| 32 | + | //! checks (`check_runs`, from created to finished), statuses and check | |
| 33 | + | //! runs on its head commit (the only commit a pull keeps, so a past | |
| 34 | + | //! week reads the current head's), entries recorded on it, and a | |
| 35 | + | //! session tied to it or a subagent of one running then (from its start | |
| 36 | + | //! to its last heartbeat: an approximation, busy or not). | |
| 37 | + | //! - A week's **session cost**: what the sessions started in it cost, | |
| 38 | + | //! subagents and all (each reports only its own transcript's). | |
| 39 | + | //! - A pull request's **session cost**: each session's own cost (unknown | |
| 40 | + | //! as 0), split evenly among the pull requests it counts with: the ones | |
| 41 | + | //! it is tied to, as above, or, for a subagent tied to none, the ones its | |
| 42 | + | //! root session is tied to. Only the repositories read count. | |
| 27 | 43 | //! - A day's **reviews** are comments of any kind on a pull request by a | |
| 28 | 44 | //! person other than its owner (not g1t's agent, a workspace agent or g1t | |
| 29 | 45 | //! itself); **reworked**, the ones of them asking for changes. | |
| 30 | 46 | //! - **Waiting**: open (not draft) pull requests with no first review. | |
| 31 | 47 | //! - **Authors** are owners who are people: not g1t's agent, a workspace | |
| 32 | 48 | //! agent (`agt_…`), a workspace (`wsp_…`) or g1t. | |
| 49 | + | //! - **Harnesses**: people's root sessions (`parent_id` unset) by | |
| 50 | + | //! repository and harness over all history, under the name on their | |
| 51 | + | //! latest; robots are left out as for authors. | |
| 33 | 52 | use std::collections::BTreeSet; | |
| 34 | 53 | ||
| 35 | 54 | use g1t_contracts::identity::AGENT_ID; | |
| 36 | 55 | use g1t_contracts::repos::{ReadableArgs, Repo}; | |
| 37 | 56 | use g1t_contracts::time::{parse_rfc3339, rfc3339}; | |
| 38 | 57 | use g1t_contracts::work::{ | |
| 39 | − | SignalAuthor, SignalDay, SignalItem, SignalMilestone, SignalPull, SignalWeek, State, WaitingPull, WorkspaceSignals, | |
| 40 | − | WorkspaceSignalsArgs, | |
| 58 | + | SignalAuthor, SignalDay, SignalHarness, SignalItem, SignalMilestone, SignalPull, SignalWeek, State, WaitingPull, | |
| 59 | + | WorkspaceSignals, WorkspaceSignalsArgs, | |
| 41 | 60 | }; | |
| 42 | 61 | use serde::{Deserialize, Serialize}; | |
| 43 | 62 | use worker::Result; | |
| ⋯ | |||
| 60 | 79 | pub(crate) const WAITING: &str = concat!(include_str!("signals/base.sql"), include_str!("signals/waiting.sql")); | |
| 61 | 80 | pub(crate) const PULLS: &str = concat!(include_str!("signals/base.sql"), include_str!("signals/pulls.sql")); | |
| 62 | 81 | pub(crate) const AUTHORS: &str = include_str!("signals/authors.sql"); | |
| 82 | + | pub(crate) const HARNESSES: &str = include_str!("signals/harnesses.sql"); | |
| 63 | 83 | /// ?1 the workspace. | |
| 64 | 84 | pub(crate) const MILESTONES: &str = "SELECT number, title, state, due_on, created_at, closed_at | |
| 65 | 85 | FROM milestones WHERE workspace = ?1 ORDER BY number"; | |
| ⋯ | |||
| 191 | 211 | merged_at: Option<String>, | |
| 192 | 212 | item: Option<u32>, | |
| 193 | 213 | milestone: Option<u32>, | |
| 214 | + | session_cost_usd: f64, | |
| 194 | 215 | } | |
| 195 | 216 | ||
| 196 | 217 | impl From<PullRow> for SignalPull { | |
| ⋯ | |||
| 207 | 228 | merged_at: row.merged_at, | |
| 208 | 229 | item: row.item, | |
| 209 | 230 | milestone: row.milestone, | |
| 231 | + | session_cost_usd: row.session_cost_usd, | |
| 210 | 232 | } | |
| 211 | 233 | } | |
| 212 | 234 | } | |
| ⋯ | |||
| 251 | 273 | let results = self | |
| 252 | 274 | .timing | |
| 253 | 275 | .db( | |
| 254 | − | 6, | |
| 276 | + | 7, | |
| 255 | 277 | self.db.batch(vec![ | |
| 256 | 278 | self.db.prepare(WEEKS).bind(&[ids.clone(), window.weeks.into()])?, | |
| 257 | 279 | self.db.prepare(DAYS).bind(&[ | |
| ⋯ | |||
| 265 | 287 | .bind(&[ids.clone(), workspace.clone(), window.since.into()])?, | |
| 266 | 288 | self.db | |
| 267 | 289 | .prepare(AUTHORS) | |
| 268 | − | .bind(&[ids, window.recent_from.into(), window.end.into()])?, | |
| 290 | + | .bind(&[ids.clone(), window.recent_from.into(), window.end.into()])?, | |
| 291 | + | self.db.prepare(HARNESSES).bind(&[ids])?, | |
| 269 | 292 | self.db.prepare(MILESTONES).bind(&[workspace])?, | |
| 270 | 293 | ]), | |
| 271 | 294 | ) | |
| 272 | 295 | .await?; | |
| 273 | − | let [weeks, days, waiting, pulls, authors, milestones] = results.as_slice() else { | |
| 296 | + | let [weeks, days, waiting, pulls, authors, harnesses, milestones] = results.as_slice() else { | |
| 274 | 297 | return Err(worker::Error::RustError("workspace_signals: missing results".into())); | |
| 275 | 298 | }; | |
| 276 | 299 | let mut waiting: Vec<WaitingPull> = waiting | |
| ⋯ | |||
| 286 | 309 | .into_iter() | |
| 287 | 310 | .filter(|author| is_person(&author.owner_id)) | |
| 288 | 311 | .collect(); | |
| 312 | + | let harnesses: Vec<SignalHarness> = harnesses | |
| 313 | + | .results::<SignalHarness>()? | |
| 314 | + | .into_iter() | |
| 315 | + | .filter(|harness| is_person(&harness.owner_id)) | |
| 316 | + | .collect(); | |
| 289 | 317 | let (items, milestones) = if in_workspace { | |
| 290 | 318 | let numbers = named_items(&pulls, &waiting); | |
| 291 | 319 | let visible = items::visible_numbered(self, &a.workspace, &numbers, viewer.as_ref()).await?; | |
| ⋯ | |||
| 319 | 347 | pulls, | |
| 320 | 348 | pulls_capped, | |
| 321 | 349 | authors, | |
| 350 | + | harnesses, | |
| 322 | 351 | items, | |
| 323 | 352 | milestones, | |
| 324 | 353 | }) | |
| 27 | 27 | -- session started before it opened, the seconds in between. | |
| 28 | 28 | pr AS ( | |
| 29 | 29 | SELECT p.id, p.repo_id, p.number, p.title, p.status, p.item_id, p.milestone, | |
| 30 | − | p.created_at, p.updated_at, p.merged_at, | |
| 30 | + | p.created_at, p.updated_at, p.merged_at, p.head_commit, p.head_pushed_at, | |
| 31 | 31 | COALESCE(p.requested_by_id, p.author_id) AS owner_id, | |
| 32 | 32 | CASE WHEN p.requested_by_id IS NULL THEN p.author_name ELSE p.requested_by_name END AS owner_name, | |
| 33 | 33 | (p.agent = 'agent' OR p.author_id = 'usr_g1t_agent' OR f.pull_id IS NOT NULL) AS agent, |
| 1 | + | -- ?1 the repositories' ids as a JSON array. Each person's root sessions | |
| 2 | + | -- (a subagent is its parent's) in each repository over its whole history, | |
| 3 | + | -- by harness: when they first started one and how many, under the name on | |
| 4 | + | -- their latest session there. Agents and g1t are left out by the caller | |
| 5 | + | -- (signals.rs `is_person`). | |
| 6 | + | WITH r AS ( | |
| 7 | + | SELECT repo_id, actor_id, actor_name, harness, started_at FROM sessions | |
| 8 | + | WHERE repo_id IN (SELECT value FROM json_each(?1)) AND parent_id IS NULL | |
| 9 | + | ), | |
| 10 | + | -- The name on each one's latest session (SQLite takes the bare column from | |
| 11 | + | -- the row MAX picks). | |
| 12 | + | named AS ( | |
| 13 | + | SELECT repo_id, actor_id, actor_name, MAX(started_at) FROM r GROUP BY repo_id, actor_id | |
| 14 | + | ) | |
| 15 | + | SELECT r.repo_id, r.actor_id AS owner_id, named.actor_name AS owner_name, r.harness, | |
| 16 | + | MIN(r.started_at) AS first_at, COUNT(*) AS sessions | |
| 17 | + | FROM r JOIN named ON named.repo_id = r.repo_id AND named.actor_id = r.actor_id | |
| 18 | + | GROUP BY r.repo_id, r.actor_id, r.harness | |
| 19 | + | ORDER BY r.repo_id, first_at, r.harness |
| 4 | 4 | -- request's milestone is its work item's, else its own when that is one | |
| 5 | 5 | -- of the workspace's (until the move to work items finishes, a pull | |
| 6 | 6 | -- request may still hold its repository's old number). | |
| 7 | + | -- | |
| 8 | + | -- A pull request's session cost is its share of what its sessions cost: | |
| 9 | + | -- the sessions tied to it as `tied` ties them, and the subagents of those | |
| 10 | + | -- that are not tied to anything themselves (a subagent counts with its | |
| 11 | + | -- root session's ties). Each session's own cost (NULL as 0) is split | |
| 12 | + | -- evenly among the pull requests it counts with. | |
| 13 | + | , | |
| 14 | + | -- Every session of the repositories, with its root session. | |
| 15 | + | anc(id, root) AS ( | |
| 16 | + | SELECT id, id FROM sessions WHERE repo_id IN (SELECT value FROM json_each(?1)) AND parent_id IS NULL | |
| 17 | + | UNION SELECT s.id, anc.root FROM anc JOIN sessions s ON s.parent_id = anc.id | |
| 18 | + | WHERE s.repo_id IN (SELECT value FROM json_each(?1)) | |
| 19 | + | ), | |
| 20 | + | -- The pull requests each session counts with. | |
| 21 | + | counts_with(session_id, pull_id) AS MATERIALIZED ( | |
| 22 | + | SELECT session_id, pull_id FROM tied | |
| 23 | + | UNION SELECT anc.id, t.pull_id FROM anc JOIN tied t ON t.session_id = anc.root | |
| 24 | + | WHERE anc.id != anc.root AND NOT EXISTS (SELECT 1 FROM tied o WHERE o.session_id = anc.id) | |
| 25 | + | ), | |
| 26 | + | share(pull_id, usd) AS ( | |
| 27 | + | SELECT c.pull_id, TOTAL(COALESCE(s.cost_usd, 0) / n.pulls) | |
| 28 | + | FROM counts_with c JOIN sessions s ON s.id = c.session_id | |
| 29 | + | JOIN (SELECT session_id, COUNT(*) AS pulls FROM counts_with GROUP BY session_id) n ON n.session_id = c.session_id | |
| 30 | + | GROUP BY c.pull_id | |
| 31 | + | ) | |
| 7 | 32 | SELECT pr.repo_id, pr.number, pr.title, pr.status, pr.agent, pr.owner_id, pr.owner_name, | |
| 8 | 33 | pr.created_at, pr.merged_at, i.number AS item, | |
| 9 | 34 | COALESCE(i.milestone, | |
| 10 | − | (SELECT m.number FROM milestones m WHERE m.workspace = ?2 AND m.number = pr.milestone)) AS milestone | |
| 35 | + | (SELECT m.number FROM milestones m WHERE m.workspace = ?2 AND m.number = pr.milestone)) AS milestone, | |
| 36 | + | COALESCE(share.usd, 0.0) AS session_cost_usd | |
| 11 | 37 | FROM pr LEFT JOIN work_items i ON i.id = pr.item_id AND i.workspace = ?2 | |
| 38 | + | LEFT JOIN share ON share.pull_id = pr.id | |
| 12 | 39 | WHERE pr.created_at >= ?3 OR (pr.status = 'merged' AND pr.merged_at >= ?3) | |
| 13 | 40 | ORDER BY pr.created_at DESC, pr.repo_id, pr.number DESC | |
| 14 | 41 | LIMIT 3001 |
| 1 | − | use g1t_contracts::work::{SignalAuthor, SignalDay, SignalPull, SignalWeek, WaitingPull, WorkspaceSignalsArgs}; | |
| 1 | + | use g1t_contracts::work::{ | |
| 2 | + | SignalAuthor, SignalDay, SignalHarness, SignalPull, SignalWeek, WaitingPull, WorkspaceSignalsArgs, | |
| 3 | + | }; | |
| 2 | 4 | use rusqlite::types::ValueRef; | |
| 3 | 5 | use rusqlite::{Connection, ToSql, params}; | |
| 4 | 6 | use serde::de::DeserializeOwned; | |
| 5 | 7 | ||
| 6 | − | use super::{AUTHORS, DAYS, PULLS, PullRow, WAITING, WEEKS, WaitingRow, Window, is_person}; | |
| 8 | + | use super::{AUTHORS, DAYS, HARNESSES, PULLS, PullRow, WAITING, WEEKS, WaitingRow, Window, is_person}; | |
| 7 | 9 | ||
| 8 | 10 | const IDS: &str = r#"["a"]"#; | |
| 9 | 11 | ||
| ⋯ | |||
| 13 | 15 | "CREATE TABLE pulls (id TEXT, repo_id TEXT, number INTEGER, title TEXT DEFAULT 'A change', status TEXT, | |
| 14 | 16 | item_id TEXT, milestone INTEGER, agent TEXT DEFAULT 'person', author_id TEXT DEFAULT 'usr_ana', | |
| 15 | 17 | author_name TEXT DEFAULT 'ana', requested_by_id TEXT, requested_by_name TEXT, | |
| 16 | − | source_branch TEXT, fork_repo_id TEXT, head_repo_id TEXT, | |
| 18 | + | source_branch TEXT, fork_repo_id TEXT, head_repo_id TEXT, head_commit TEXT, head_pushed_at TEXT, | |
| 17 | 19 | created_at TEXT, updated_at TEXT, merged_at TEXT); | |
| 18 | − | CREATE TABLE comments (repo_id TEXT, number INTEGER, author_id TEXT, agent_id TEXT, verdict TEXT, created_at TEXT); | |
| 19 | − | CREATE TABLE sessions (id TEXT, repo_id TEXT, pull INTEGER, started_at TEXT); | |
| 20 | + | CREATE TABLE comments (repo_id TEXT, number INTEGER, author_id TEXT, agent_id TEXT, verdict TEXT, created_at TEXT, | |
| 21 | + | kind TEXT NOT NULL DEFAULT 'comment'); | |
| 22 | + | CREATE TABLE sessions (id TEXT, repo_id TEXT, pull INTEGER, started_at TEXT, parent_id TEXT, | |
| 23 | + | actor_id TEXT DEFAULT 'usr_ana', actor_name TEXT DEFAULT 'ana', harness TEXT DEFAULT 'claude_code', | |
| 24 | + | last_seen_at TEXT, cost_usd REAL); | |
| 25 | + | CREATE TABLE session_entries (pull_id TEXT, seq INTEGER, at TEXT); | |
| 26 | + | CREATE TABLE check_runs (id TEXT, pull_id TEXT, head_commit TEXT, created_at TEXT, finished_at TEXT); | |
| 27 | + | CREATE TABLE commit_statuses (repo_id TEXT, sha TEXT, context TEXT, updated_at TEXT); | |
| 28 | + | CREATE TABLE commit_check_runs (id TEXT, repo_id TEXT, head_sha TEXT, created_at TEXT, updated_at TEXT); | |
| 20 | 29 | CREATE TABLE session_links (session_id TEXT, repo_id TEXT, kind TEXT, number INTEGER); | |
| 21 | 30 | CREATE TABLE session_touches (session_id TEXT, repo_id TEXT, kind TEXT, value TEXT); | |
| 22 | 31 | CREATE TABLE work_items (id TEXT, workspace TEXT, number INTEGER, title TEXT, state TEXT, | |
| ⋯ | |||
| 48 | 57 | ||
| 49 | 58 | fn comment(db: &Connection, number: u32, author: &str, agent: Option<&str>, verdict: Option<&str>, at: &str) { | |
| 50 | 59 | db.execute( | |
| 51 | − | "INSERT INTO comments VALUES ('a', ?1, ?2, ?3, ?4, ?5)", | |
| 60 | + | "INSERT INTO comments (repo_id, number, author_id, agent_id, verdict, created_at) VALUES ('a', ?1, ?2, ?3, ?4, ?5)", | |
| 52 | 61 | params![number, author, agent, verdict, at], | |
| 53 | 62 | ) | |
| 54 | 63 | .unwrap(); | |
| ⋯ | |||
| 181 | 190 | "UPDATE pulls SET agent = 'agent' WHERE number = 1; | |
| 182 | 191 | UPDATE pulls SET author_id = 'usr_g1t_agent', requested_by_id = 'usr_ana', requested_by_name = 'ana' WHERE number = 2; | |
| 183 | 192 | -- 3 by a link, 4 by the session saying so, 5 by a push to its branch. | |
| 184 | − | INSERT INTO sessions VALUES ('ses_3', 'a', NULL, '2026-10-06T11:00:00Z'); | |
| 193 | + | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_3', 'a', NULL, '2026-10-06T11:00:00Z'); | |
| 185 | 194 | INSERT INTO session_links VALUES ('ses_3', 'a', 'pull', 3); | |
| 186 | − | INSERT INTO sessions VALUES ('ses_4', 'a', 4, '2026-10-06T13:00:00Z'); | |
| 187 | − | INSERT INTO sessions VALUES ('ses_5', 'a', NULL, '2026-10-06T11:30:00Z'); | |
| 188 | − | INSERT INTO sessions VALUES ('ses_5b', 'a', NULL, '2026-10-06T10:00:00.500Z'); | |
| 195 | + | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_4', 'a', 4, '2026-10-06T13:00:00Z'); | |
| 196 | + | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_5', 'a', NULL, '2026-10-06T11:30:00Z'); | |
| 197 | + | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_5b', 'a', NULL, '2026-10-06T10:00:00.500Z'); | |
| 189 | 198 | INSERT INTO session_touches VALUES ('ses_5', 'a', 'branch', 'branch-5'); | |
| 190 | 199 | INSERT INTO session_touches VALUES ('ses_5b', 'a', 'branch', 'branch-5'); | |
| 191 | 200 | -- 6's branch is on a fork: a push to the same name is not its. | |
| 192 | 201 | UPDATE pulls SET fork_repo_id = 'fork' WHERE number = 6; | |
| 193 | − | INSERT INTO sessions VALUES ('ses_6', 'a', NULL, '2026-10-06T11:00:00Z'); | |
| 202 | + | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_6', 'a', NULL, '2026-10-06T11:00:00Z'); | |
| 194 | 203 | INSERT INTO session_touches VALUES ('ses_6', 'a', 'branch', 'branch-6'); | |
| 195 | 204 | -- A session of another repository with the same number is not its. | |
| 196 | − | INSERT INTO sessions VALUES ('ses_x', 'b', 6, '2026-10-06T11:00:00Z');", | |
| 205 | + | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_x', 'b', 6, '2026-10-06T11:00:00Z');", | |
| 197 | 206 | ) | |
| 198 | 207 | .unwrap(); | |
| 199 | 208 | pull(&db, 7, "open", "2026-10-06T12:00:00Z", None, "2026-10-06T12:00:00Z"); | |
| ⋯ | |||
| 287 | 296 | } | |
| 288 | 297 | ||
| 289 | 298 | #[test] | |
| 290 | − | fn stalled_is_open_over_a_week_when_the_week_ended() { | |
| 291 | − | let db = schema(); | |
| 292 | − | // The second week ends at 2026-10-12T00:00:00Z: over seven days open | |
| 293 | − | // by then is opened before 10-05. | |
| 294 | − | pull(&db, 1, "open", "2026-10-05T00:00:00Z", None, "2026-10-05T00:00:00Z"); | |
| 295 | − | pull(&db, 2, "open", "2026-10-04T23:59:59Z", None, "2026-10-04T23:59:59Z"); | |
| 296 | − | // Merged after the week ended: still open when it did. | |
| 297 | − | pull( | |
| 298 | − | &db, | |
| 299 | − | 3, | |
| 300 | − | "merged", | |
| 301 | − | "2026-09-01T00:00:00Z", | |
| 302 | − | Some("2026-10-12T00:00:00Z"), | |
| 303 | − | "2026-10-12T00:00:00Z", | |
| 304 | − | ); | |
| 305 | − | // Merged and closed before it ended. | |
| 306 | − | pull( | |
| 307 | − | &db, | |
| 308 | − | 4, | |
| 309 | − | "merged", | |
| 310 | − | "2026-09-01T00:00:00Z", | |
| 311 | − | Some("2026-10-11T23:00:00Z"), | |
| 312 | − | "2026-10-11T23:00:00Z", | |
| 313 | − | ); | |
| 314 | − | pull(&db, 5, "closed", "2026-09-01T00:00:00Z", None, "2026-10-11T23:00:00Z"); | |
| 315 | − | // Closed after it ended. | |
| 316 | − | pull(&db, 6, "closed", "2026-09-01T00:00:00Z", None, "2026-10-13T00:00:00Z"); | |
| 317 | − | pull(&db, 7, "draft", "2026-09-27T00:00:00Z", None, "2026-09-27T00:00:00Z"); | |
| 318 | − | let found = weeks(&db, "2026-10-18T00:00:00Z"); | |
| 319 | − | let stalled: Vec<(&str, u32)> = found.iter().map(|w| (w.start.as_str(), w.stalled)).collect(); | |
| 320 | − | // The first week ends on 10-05: 3 to 6 open since 09-01, and 7 opened | |
| 321 | − | // a second over seven days before. The second: 2, 3, 6 and 7. | |
| 322 | − | assert_eq!(stalled, [("2026-09-28", 5), ("2026-10-05", 4)]); | |
| 323 | − | ||
| 324 | − | // Read mid-week, the current week ends now, 10-08T12:00: 3, 4, 5 and | |
| 325 | − | // 6 were not finished by then, 7 is old enough, 2 is not, and 1 at | |
| 326 | − | // exactly seven days is not either. | |
| 327 | − | db.execute( | |
| 328 | − | "UPDATE pulls SET created_at = '2026-10-01T12:00:00Z' WHERE number = 1", | |
| 329 | − | [], | |
| 330 | − | ) | |
| 331 | − | .unwrap(); | |
| 332 | − | assert_eq!(in_week(&weeks(&db, "2026-10-08T12:00:00Z"), "2026-10-05").stalled, 5); | |
| 333 | − | db.execute( | |
| 334 | − | "UPDATE pulls SET created_at = '2026-10-01T11:59:59Z' WHERE number = 1", | |
| 335 | − | [], | |
| 336 | − | ) | |
| 337 | − | .unwrap(); | |
| 338 | − | assert_eq!(in_week(&weeks(&db, "2026-10-08T12:00:00Z"), "2026-10-05").stalled, 6); | |
| 339 | − | } | |
| 340 | − | ||
| 341 | − | #[test] | |
| 342 | 299 | fn a_first_review_is_a_persons_other_than_the_owner() { | |
| 343 | 300 | let db = schema(); | |
| 344 | 301 | pull(&db, 1, "open", "2026-10-06T00:00:00Z", None, "2026-10-06T00:00:00Z"); | |
| ⋯ | |||
| 516 | 473 | pull(&db, 2, "open", "2026-10-08T10:00:00Z", None, "2026-10-08T10:00:00Z"); | |
| 517 | 474 | db.execute_batch( | |
| 518 | 475 | "UPDATE pulls SET agent = 'agent' WHERE number = 1; | |
| 519 | − | INSERT INTO sessions VALUES ('ses_1', 'a', NULL, '2026-10-10T09:00:00Z'); | |
| 520 | − | INSERT INTO sessions VALUES ('ses_2', 'b', NULL, '2026-10-10T09:00:00Z');", | |
| 476 | + | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_1', 'a', NULL, '2026-10-10T09:00:00Z'); | |
| 477 | + | INSERT INTO sessions (id, repo_id, pull, started_at) VALUES ('ses_2', 'b', NULL, '2026-10-10T09:00:00Z');", | |
| 521 | 478 | ) | |
| 522 | 479 | .unwrap(); | |
| 523 | 480 | comment(&db, 1, "usr_bo", None, Some("request_changes"), "2026-10-10T01:00:00Z"); | |
| ⋯ | |||
| 562 | 519 | } | |
| 563 | 520 | assert!(is_person("usr_ana")); | |
| 564 | 521 | } | |
| 522 | + | ||
| 523 | + | /// Whether pull request 1 (open since 09-01, head `abc`) is stalled in the | |
| 524 | + | /// week of 10-05, read on `now`, after `setup`. | |
| 525 | + | fn stalled_after(now: &str, setup: &str) -> u32 { | |
| 526 | + | let db = schema(); | |
| 527 | + | pull(&db, 1, "open", "2026-09-01T00:00:00Z", None, "2026-09-01T00:00:00Z"); | |
| 528 | + | db.execute_batch("UPDATE pulls SET head_commit = 'abc'").unwrap(); | |
| 529 | + | db.execute_batch(setup).unwrap(); | |
| 530 | + | // A week with nothing in it is left out. | |
| 531 | + | weeks(&db, now) | |
| 532 | + | .iter() | |
| 533 | + | .find(|week| week.start == "2026-10-05") | |
| 534 | + | .map_or(0, |week| week.stalled) | |
| 535 | + | } | |
| 536 | + | ||
| 537 | + | #[test] | |
| 538 | + | fn stalled_is_open_with_nothing_in_the_three_days_before_the_cut() { | |
| 539 | + | // The week of 10-05 is over on 10-18: its cut is 10-12T00:00, and the | |
| 540 | + | // three days before it start 10-09T00:00. | |
| 541 | + | let at = |setup: &str| stalled_after("2026-10-18T00:00:00Z", setup); | |
| 542 | + | assert_eq!(at(""), 1, "nothing at all"); | |
| 543 | + | let comment = |kind: &str, when: &str| { | |
| 544 | + | format!( | |
| 545 | + | "INSERT INTO comments (repo_id, number, author_id, kind, created_at) VALUES ('a', 1, 'usr_bo', '{kind}', '{when}')" | |
| 546 | + | ) | |
| 547 | + | }; | |
| 548 | + | assert_eq!(at(&comment("comment", "2026-10-10T00:00:00Z")), 0, "a comment"); | |
| 549 | + | assert_eq!( | |
| 550 | + | at(&comment("event", "2026-10-09T00:00:00Z")), | |
| 551 | + | 0, | |
| 552 | + | "an event exactly three days before" | |
| 553 | + | ); | |
| 554 | + | assert_eq!( | |
| 555 | + | at(&comment("comment", "2026-10-08T23:59:59Z")), | |
| 556 | + | 1, | |
| 557 | + | "only before the three days" | |
| 558 | + | ); | |
| 559 | + | assert_eq!(at(&comment("comment", "2026-10-12T00:00:00Z")), 1, "only at the cut"); | |
| 560 | + | assert_eq!( | |
| 561 | + | at( | |
| 562 | + | "INSERT INTO comments (repo_id, number, author_id, created_at) VALUES ('a', 2, 'usr_bo', '2026-10-10T00:00:00Z')" | |
| 563 | + | ), | |
| 564 | + | 1, | |
| 565 | + | "another pull request's comment" | |
| 566 | + | ); | |
| 567 | + | assert_eq!( | |
| 568 | + | at("UPDATE pulls SET head_pushed_at = '2026-10-11T00:00:00Z'"), | |
| 569 | + | 0, | |
| 570 | + | "a push" | |
| 571 | + | ); | |
| 572 | + | assert_eq!( | |
| 573 | + | at("UPDATE pulls SET head_pushed_at = '2026-10-01T00:00:00Z'"), | |
| 574 | + | 1, | |
| 575 | + | "an old push" | |
| 576 | + | ); | |
| 577 | + | assert_eq!( | |
| 578 | + | at("INSERT INTO session_entries VALUES ('pr_1', 1, '2026-10-10T00:00:00Z')"), | |
| 579 | + | 0, | |
| 580 | + | "a session entry" | |
| 581 | + | ); | |
| 582 | + | assert_eq!( | |
| 583 | + | at("INSERT INTO check_runs VALUES ('chk', 'pr_1', 'abc', '2026-10-05T00:00:00Z', '2026-10-10T00:00:00Z')"), | |
| 584 | + | 0, | |
| 585 | + | "acceptance checks running then" | |
| 586 | + | ); | |
| 587 | + | assert_eq!( | |
| 588 | + | at("INSERT INTO commit_statuses VALUES ('a', 'abc', 'CI', '2026-10-10T00:00:00Z')"), | |
| 589 | + | 0, | |
| 590 | + | "a status on its head" | |
| 591 | + | ); | |
| 592 | + | assert_eq!( | |
| 593 | + | at("INSERT INTO commit_statuses VALUES ('a', 'old', 'CI', '2026-10-10T00:00:00Z')"), | |
| 594 | + | 1, | |
| 595 | + | "a status on another commit" | |
| 596 | + | ); | |
| 597 | + | assert_eq!( | |
| 598 | + | at("INSERT INTO commit_check_runs VALUES ('ck', 'a', 'abc', '2026-10-08T00:00:00Z', '2026-10-09T06:00:00Z')"), | |
| 599 | + | 0, | |
| 600 | + | "a check run on its head" | |
| 601 | + | ); | |
| 602 | + | let session = "INSERT INTO sessions (id, repo_id, pull, started_at, last_seen_at) | |
| 603 | + | VALUES ('ses_1', 'a', 1, '2026-10-01T00:00:00Z',"; | |
| 604 | + | assert_eq!( | |
| 605 | + | at(&format!("{session} '2026-10-09T12:00:00Z')")), | |
| 606 | + | 0, | |
| 607 | + | "a session running then" | |
| 608 | + | ); | |
| 609 | + | assert_eq!( | |
| 610 | + | at(&format!("{session} '2026-10-08T00:00:00Z')")), | |
| 611 | + | 1, | |
| 612 | + | "a session over before" | |
| 613 | + | ); | |
| 614 | + | assert_eq!( | |
| 615 | + | at(&format!( | |
| 616 | + | "{session} '2026-10-01T00:00:00Z'); | |
| 617 | + | INSERT INTO sessions (id, repo_id, parent_id, started_at, last_seen_at) | |
| 618 | + | VALUES ('ses_2', 'a', 'ses_1', '2026-10-09T00:00:00Z', '2026-10-10T00:00:00Z');" | |
| 619 | + | )), | |
| 620 | + | 0, | |
| 621 | + | "its session's subagent running then" | |
| 622 | + | ); | |
| 623 | + | ||
| 624 | + | // Opened at least three days before the cut. | |
| 625 | + | assert_eq!( | |
| 626 | + | at("UPDATE pulls SET created_at = '2026-10-09T00:00:00Z'"), | |
| 627 | + | 1, | |
| 628 | + | "exactly three days" | |
| 629 | + | ); | |
| 630 | + | assert_eq!( | |
| 631 | + | at("UPDATE pulls SET created_at = '2026-10-09T00:00:01Z'"), | |
| 632 | + | 0, | |
| 633 | + | "a second less" | |
| 634 | + | ); | |
| 635 | + | // Open at the cut; drafts count. | |
| 636 | + | assert_eq!(at("UPDATE pulls SET status = 'draft'"), 1, "a draft"); | |
| 637 | + | assert_eq!( | |
| 638 | + | at("UPDATE pulls SET status = 'merged', merged_at = '2026-10-11T00:00:00Z'"), | |
| 639 | + | 0, | |
| 640 | + | "merged before the cut" | |
| 641 | + | ); | |
| 642 | + | assert_eq!( | |
| 643 | + | at("UPDATE pulls SET status = 'merged', merged_at = '2026-10-12T00:00:00Z'"), | |
| 644 | + | 1, | |
| 645 | + | "merged at the cut" | |
| 646 | + | ); | |
| 647 | + | assert_eq!( | |
| 648 | + | at("UPDATE pulls SET status = 'closed', updated_at = '2026-10-11T00:00:00Z'"), | |
| 649 | + | 0, | |
| 650 | + | "closed before" | |
| 651 | + | ); | |
| 652 | + | assert_eq!( | |
| 653 | + | at("UPDATE pulls SET status = 'closed', updated_at = '2026-10-13T00:00:00Z'"), | |
| 654 | + | 1, | |
| 655 | + | "closed after" | |
| 656 | + | ); | |
| 657 | + | ||
| 658 | + | // Read mid-week, the cut is now, 10-08T12:00: the three days before it | |
| 659 | + | // start 10-05T12:00. | |
| 660 | + | let mid = |setup: &str| stalled_after("2026-10-08T12:00:00Z", setup); | |
| 661 | + | assert_eq!(mid(&comment("comment", "2026-10-05T12:00:00Z")), 0); | |
| 662 | + | assert_eq!(mid(&comment("comment", "2026-10-05T11:59:59Z")), 1); | |
| 663 | + | assert_eq!(mid("UPDATE pulls SET created_at = '2026-10-05T12:00:00Z'"), 1); | |
| 664 | + | assert_eq!(mid("UPDATE pulls SET created_at = '2026-10-05T12:00:01Z'"), 0); | |
| 665 | + | } | |
| 666 | + | ||
| 667 | + | #[test] | |
| 668 | + | fn stalled_counts_each_week_at_its_own_cut() { | |
| 669 | + | let db = schema(); | |
| 670 | + | pull(&db, 1, "open", "2026-09-01T00:00:00Z", None, "2026-09-01T00:00:00Z"); | |
| 671 | + | pull(&db, 2, "open", "2026-09-01T00:00:00Z", None, "2026-09-01T00:00:00Z"); | |
| 672 | + | // 1 was busy before the first week's cut (10-05), 2 before the second's. | |
| 673 | + | comment(&db, 1, "usr_bo", None, None, "2026-10-04T00:00:00Z"); | |
| 674 | + | comment(&db, 2, "usr_bo", None, None, "2026-10-11T00:00:00Z"); | |
| 675 | + | let found = weeks(&db, "2026-10-18T00:00:00Z"); | |
| 676 | + | let stalled: Vec<(&str, u32)> = found.iter().map(|w| (w.start.as_str(), w.stalled)).collect(); | |
| 677 | + | assert_eq!(stalled, [("2026-09-28", 1), ("2026-10-05", 1)]); | |
| 678 | + | } | |
| 679 | + | ||
| 680 | + | #[test] | |
| 681 | + | fn a_weeks_session_cost_is_what_its_sessions_cost() { | |
| 682 | + | let db = schema(); | |
| 683 | + | db.execute_batch( | |
| 684 | + | "INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_1', 'a', '2026-10-06T00:00:00.000Z', 1.5); | |
| 685 | + | INSERT INTO sessions (id, repo_id, parent_id, started_at, cost_usd) | |
| 686 | + | VALUES ('ses_2', 'a', 'ses_1', '2026-10-07T00:00:00.000Z', 0.25); | |
| 687 | + | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_3', 'a', '2026-10-08T00:00:00.000Z', NULL); | |
| 688 | + | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_4', 'a', '2026-10-04T23:59:59.999Z', 2); | |
| 689 | + | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_5', 'a', '2026-10-05T00:00:00.000Z', 0.5); | |
| 690 | + | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_6', 'a', '2026-10-12T00:00:00.000Z', 7); | |
| 691 | + | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_7', 'b', '2026-10-06T00:00:00.000Z', 100);", | |
| 692 | + | ) | |
| 693 | + | .unwrap(); | |
| 694 | + | let found = weeks(&db, "2026-10-18T00:00:00Z"); | |
| 695 | + | let costs: Vec<(&str, f64, u32)> = found | |
| 696 | + | .iter() | |
| 697 | + | .map(|w| (w.start.as_str(), w.session_cost_usd, w.merged)) | |
| 698 | + | .collect(); | |
| 699 | + | // No pull requests at all: the weeks show for their cost alone. A | |
| 700 | + | // subagent's counts; one of no known cost counts as nothing. | |
| 701 | + | assert_eq!(costs, [("2026-09-28", 2.0, 0), ("2026-10-05", 2.25, 0)]); | |
| 702 | + | ||
| 703 | + | // A week whose sessions cost nothing known is left out. | |
| 704 | + | db.execute_batch("UPDATE sessions SET cost_usd = NULL WHERE id = 'ses_4'") | |
| 705 | + | .unwrap(); | |
| 706 | + | let found = weeks(&db, "2026-10-18T00:00:00Z"); | |
| 707 | + | assert_eq!( | |
| 708 | + | found.iter().map(|w| w.start.as_str()).collect::<Vec<_>>(), | |
| 709 | + | ["2026-10-05"] | |
| 710 | + | ); | |
| 711 | + | } | |
| 712 | + | ||
| 713 | + | #[test] | |
| 714 | + | fn a_pull_requests_session_cost_is_its_share() { | |
| 715 | + | let db = schema(); | |
| 716 | + | for number in 1..=4 { | |
| 717 | + | pull( | |
| 718 | + | &db, | |
| 719 | + | number, | |
| 720 | + | "open", | |
| 721 | + | "2026-10-06T00:00:00Z", | |
| 722 | + | None, | |
| 723 | + | "2026-10-06T00:00:00Z", | |
| 724 | + | ); | |
| 725 | + | } | |
| 726 | + | db.execute_batch( | |
| 727 | + | "-- A root session tied to 1 by saying so and to 2 by a link. | |
| 728 | + | INSERT INTO sessions (id, repo_id, pull, started_at, cost_usd) VALUES ('ses_r', 'a', 1, '2026-10-06T00:00:00Z', 4); | |
| 729 | + | INSERT INTO session_links VALUES ('ses_r', 'a', 'pull', 2); | |
| 730 | + | -- Its subagent tied to nothing counts with it; so does that one's. | |
| 731 | + | INSERT INTO sessions (id, repo_id, parent_id, started_at, cost_usd) | |
| 732 | + | VALUES ('ses_a', 'a', 'ses_r', '2026-10-06T00:00:00Z', 1); | |
| 733 | + | INSERT INTO sessions (id, repo_id, parent_id, started_at, cost_usd) | |
| 734 | + | VALUES ('ses_c', 'a', 'ses_a', '2026-10-06T00:00:00Z', 2); | |
| 735 | + | -- A subagent tied itself, by a push to 3's branch, counts with that. | |
| 736 | + | INSERT INTO sessions (id, repo_id, parent_id, started_at, cost_usd) | |
| 737 | + | VALUES ('ses_b', 'a', 'ses_r', '2026-10-06T00:00:00Z', 3); | |
| 738 | + | INSERT INTO session_touches VALUES ('ses_b', 'a', 'branch', 'branch-3'); | |
| 739 | + | -- A session whose cost is not known. | |
| 740 | + | INSERT INTO sessions (id, repo_id, pull, started_at, cost_usd) VALUES ('ses_n', 'a', 3, '2026-10-06T00:00:00Z', NULL); | |
| 741 | + | -- A session tied to nothing counts with nothing. | |
| 742 | + | INSERT INTO sessions (id, repo_id, started_at, cost_usd) VALUES ('ses_x', 'a', '2026-10-06T00:00:00Z', 9);", | |
| 743 | + | ) | |
| 744 | + | .unwrap(); | |
| 745 | + | let mut costs: Vec<(u32, f64)> = pulls(&db).iter().map(|p| (p.number, p.session_cost_usd)).collect(); | |
| 746 | + | costs.sort_by_key(|(number, _)| *number); | |
| 747 | + | assert_eq!(costs, [(1, 3.5), (2, 3.5), (3, 3.0), (4, 0.0)]); | |
| 748 | + | } | |
| 749 | + | ||
| 750 | + | #[test] | |
| 751 | + | fn harnesses_are_peoples_root_sessions() { | |
| 752 | + | let db = schema(); | |
| 753 | + | db.execute_batch( | |
| 754 | + | "INSERT INTO sessions (id, repo_id, started_at, harness, actor_name) | |
| 755 | + | VALUES ('s1', 'a', '2026-01-01T00:00:00Z', 'claude_code', 'ana'); | |
| 756 | + | INSERT INTO sessions (id, repo_id, started_at, harness, actor_name) | |
| 757 | + | VALUES ('s2', 'a', '2026-05-01T00:00:00Z', 'claude_code', 'ana-new'); | |
| 758 | + | INSERT INTO sessions (id, repo_id, started_at, harness, actor_name) | |
| 759 | + | VALUES ('s3', 'a', '2026-03-01T00:00:00Z', 'codex', 'ana'); | |
| 760 | + | -- A subagent is its parent's: not counted, and not when they first used it. | |
| 761 | + | INSERT INTO sessions (id, repo_id, parent_id, started_at, harness) | |
| 762 | + | VALUES ('s4', 'a', 's1', '2025-12-01T00:00:00Z', 'claude_code'); | |
| 763 | + | INSERT INTO sessions (id, repo_id, parent_id, started_at, harness) | |
| 764 | + | VALUES ('s5', 'a', 's1', '2025-12-01T00:00:00Z', 'cursor'); | |
| 765 | + | -- Robots, and another repository. | |
| 766 | + | INSERT INTO sessions (id, repo_id, started_at, actor_id) VALUES ('s6', 'a', '2026-01-01T00:00:00Z', 'usr_g1t_agent'); | |
| 767 | + | INSERT INTO sessions (id, repo_id, started_at, actor_id) VALUES ('s7', 'a', '2026-01-01T00:00:00Z', 'agt_margo'); | |
| 768 | + | INSERT INTO sessions (id, repo_id, started_at) VALUES ('s8', 'b', '2025-01-01T00:00:00Z');", | |
| 769 | + | ) | |
| 770 | + | .unwrap(); | |
| 771 | + | let found: Vec<SignalHarness> = rows::<SignalHarness>(&db, HARNESSES, &[&IDS]) | |
| 772 | + | .into_iter() | |
| 773 | + | .filter(|harness| is_person(&harness.owner_id)) | |
| 774 | + | .collect(); | |
| 775 | + | let shown: Vec<(&str, &str, &str, &str, &str, u32)> = found | |
| 776 | + | .iter() | |
| 777 | + | .map(|h| { | |
| 778 | + | ( | |
| 779 | + | h.repo_id.as_str(), | |
| 780 | + | h.owner_id.as_str(), | |
| 781 | + | h.owner_name.as_str(), | |
| 782 | + | h.harness.as_str(), | |
| 783 | + | h.first_at.as_str(), | |
| 784 | + | h.sessions, | |
| 785 | + | ) | |
| 786 | + | }) | |
| 787 | + | .collect(); | |
| 788 | + | assert_eq!( | |
| 789 | + | shown, | |
| 790 | + | [ | |
| 791 | + | ("a", "usr_ana", "ana-new", "claude_code", "2026-01-01T00:00:00Z", 2), | |
| 792 | + | ("a", "usr_ana", "ana-new", "codex", "2026-03-01T00:00:00Z", 1) | |
| 793 | + | ] | |
| 794 | + | ); | |
| 795 | + | } | |
| 9 | 9 | , | |
| 10 | 10 | w AS ( | |
| 11 | 11 | SELECT json_extract(value, '$.start') AS start, | |
| 12 | + | json_extract(value, '$.from') AS lo_at, json_extract(value, '$.to') AS hi_at, | |
| 12 | 13 | julianday(json_extract(value, '$.from')) AS lo, | |
| 13 | 14 | julianday(json_extract(value, '$.to')) AS hi, | |
| 14 | 15 | julianday(json_extract(value, '$.cut')) AS cut | |
| 15 | 16 | FROM json_each(?2) | |
| 16 | 17 | ), | |
| 18 | + | -- The earliest and the latest cut. | |
| 19 | + | cuts AS (SELECT MIN(cut) AS lo, MAX(cut) AS hi FROM w), | |
| 17 | 20 | x AS MATERIALIZED ( | |
| 18 | − | SELECT pr.repo_id, pr.status, pr.agent, pr.draft_seconds, | |
| 21 | + | SELECT pr.id, pr.repo_id, pr.number, pr.status, pr.agent, pr.draft_seconds, pr.head_commit, | |
| 19 | 22 | julianday(pr.created_at) AS c, julianday(pr.merged_at) AS m, julianday(pr.updated_at) AS u, | |
| 23 | + | julianday(pr.head_pushed_at) AS pushed, | |
| 20 | 24 | julianday((SELECT MIN(cm.created_at) FROM comments cm | |
| 21 | 25 | WHERE cm.repo_id = pr.repo_id AND cm.number = pr.number | |
| 22 | 26 | AND cm.verdict IN ('approve', 'request_changes') | |
| ⋯ | |||
| 27 | 31 | AND cm.author_id != pr.owner_id AND cm.author_id != 'usr_g1t_agent' AND cm.agent_id IS NULL | |
| 28 | 32 | AND julianday(cm.created_at) < julianday(pr.merged_at))) AS reworked | |
| 29 | 33 | FROM pr | |
| 34 | + | ), | |
| 35 | + | -- The pull requests that may be stalled at some cut: opened three days | |
| 36 | + | -- before the latest, and not finished before the earliest (a closed one's | |
| 37 | + | -- updated_at stands in for when it closed, which pulls do not keep). | |
| 38 | + | -- Only their activity is read, each by its own index. | |
| 39 | + | cand AS MATERIALIZED ( | |
| 40 | + | SELECT x.* FROM x, cuts | |
| 41 | + | WHERE x.c <= cuts.hi - 3 AND NOT COALESCE(x.m < cuts.lo, 0) | |
| 42 | + | AND NOT (x.status = 'closed' AND COALESCE(x.u < cuts.lo, 0)) | |
| 43 | + | ), | |
| 44 | + | -- What each of them saw, as spans [t0, t1] (a moment where t0 = t1). | |
| 45 | + | -- CROSS JOIN keeps each read starting from the few candidates. | |
| 46 | + | act(pull_id, t0, t1) AS MATERIALIZED ( | |
| 47 | + | -- Comments of every kind: reviews, and the timeline's events (labels, | |
| 48 | + | -- assignees, review requests, milestones, ready, draft, closed, | |
| 49 | + | -- reopened, base changes, the merge queue, auto-merge, branches deleted | |
| 50 | + | -- and restored). Pushes are no timeline event. | |
| 51 | + | SELECT k.id, julianday(cm.created_at), julianday(cm.created_at) | |
| 52 | + | FROM cand k CROSS JOIN comments cm ON cm.repo_id = k.repo_id AND cm.number = k.number | |
| 53 | + | -- Its latest push, the only one pulls keep. | |
| 54 | + | UNION ALL SELECT k.id, k.pushed, k.pushed FROM cand k WHERE k.pushed IS NOT NULL | |
| 55 | + | -- Entries recorded on it. | |
| 56 | + | UNION ALL SELECT k.id, julianday(e.at), julianday(e.at) FROM cand k CROSS JOIN session_entries e ON e.pull_id = k.id | |
| 57 | + | -- Its acceptance checks, from queued to finished. | |
| 58 | + | UNION ALL SELECT k.id, julianday(cr.created_at), julianday(COALESCE(cr.finished_at, cr.created_at)) | |
| 59 | + | FROM cand k CROSS JOIN check_runs cr ON cr.pull_id = k.id | |
| 60 | + | -- Statuses and check runs on its head commit, the one commit pulls keep | |
| 61 | + | -- (a status keeps only when it last changed). | |
| 62 | + | UNION ALL SELECT k.id, julianday(st.updated_at), julianday(st.updated_at) | |
| 63 | + | FROM cand k CROSS JOIN commit_statuses st ON st.repo_id = k.repo_id AND st.sha = k.head_commit | |
| 64 | + | UNION ALL SELECT k.id, julianday(ck.created_at), julianday(ck.updated_at) | |
| 65 | + | FROM cand k CROSS JOIN commit_check_runs ck ON ck.repo_id = k.repo_id AND ck.head_sha = k.head_commit | |
| 66 | + | -- The sessions tied to it, and their subagents, from start to last | |
| 67 | + | -- heartbeat: a session that ran then counts, busy or not. | |
| 68 | + | UNION ALL SELECT k.id, julianday(s.started_at), julianday(s.last_seen_at) | |
| 69 | + | FROM cand k CROSS JOIN tied t ON t.pull_id = k.id CROSS JOIN sessions s ON s.id = t.session_id | |
| 70 | + | UNION ALL SELECT k.id, julianday(s.started_at), julianday(s.last_seen_at) | |
| 71 | + | FROM cand k CROSS JOIN tied t ON t.pull_id = k.id CROSS JOIN sessions s ON s.parent_id = t.session_id | |
| 72 | + | ), | |
| 73 | + | -- Each week a pull request saw something in the three days before its cut. | |
| 74 | + | busy AS MATERIALIZED ( | |
| 75 | + | SELECT DISTINCT a.pull_id, w.start FROM act a JOIN w ON a.t0 < w.cut AND a.t1 >= w.cut - 3 | |
| 76 | + | ), | |
| 77 | + | pw AS ( | |
| 78 | + | SELECT x.repo_id, w.start, | |
| 79 | + | COUNT(CASE WHEN x.status = 'merged' AND x.m >= w.lo AND x.m < w.hi THEN 1 END) AS merged, | |
| 80 | + | COUNT(CASE WHEN x.status = 'merged' AND x.m >= w.lo AND x.m < w.hi AND x.agent THEN 1 END) AS agent_merged, | |
| 81 | + | COUNT(CASE WHEN x.status = 'merged' AND x.m >= w.lo AND x.m < w.hi AND x.reworked THEN 1 END) AS agent_reworked, | |
| 82 | + | -- Opened in the week by an agent whose session started first. | |
| 83 | + | COUNT(CASE WHEN x.agent AND x.draft_seconds IS NOT NULL AND x.c >= w.lo AND x.c < w.hi THEN 1 END) AS agent_drafted, | |
| 84 | + | TOTAL(CASE WHEN x.agent AND x.c >= w.lo AND x.c < w.hi THEN x.draft_seconds END) AS agent_draft_seconds, | |
| 85 | + | COUNT(CASE WHEN x.r >= w.lo AND x.r < w.hi THEN 1 END) AS reviewed, | |
| 86 | + | TOTAL(CASE WHEN x.r >= w.lo AND x.r < w.hi THEN MAX(0, (x.r - x.c) * 86400) END) AS review_seconds, | |
| 87 | + | 0 AS stalled, 0.0 AS session_cost_usd | |
| 88 | + | FROM x CROSS JOIN w | |
| 89 | + | GROUP BY x.repo_id, w.start | |
| 90 | + | ), | |
| 91 | + | -- Open at the cut (not merged, and not closed, before it), opened at | |
| 92 | + | -- least three days before, and nothing in the three days before it. | |
| 93 | + | sx AS ( | |
| 94 | + | SELECT k.repo_id, w.start, 0 AS merged, 0 AS agent_merged, 0 AS agent_reworked, 0 AS agent_drafted, | |
| 95 | + | 0.0 AS agent_draft_seconds, 0 AS reviewed, 0.0 AS review_seconds, COUNT(*) AS stalled, 0.0 AS session_cost_usd | |
| 96 | + | FROM cand k CROSS JOIN w LEFT JOIN busy b ON b.pull_id = k.id AND b.start = w.start | |
| 97 | + | WHERE b.pull_id IS NULL AND k.c <= w.cut - 3 | |
| 98 | + | AND NOT COALESCE(k.m < w.cut, 0) | |
| 99 | + | AND NOT (k.status = 'closed' AND COALESCE(k.u < w.cut, 0)) | |
| 100 | + | GROUP BY k.repo_id, w.start | |
| 101 | + | ), | |
| 102 | + | -- What the sessions started in the week cost, subagents and all: each | |
| 103 | + | -- reports only its own transcript's. | |
| 104 | + | sw AS ( | |
| 105 | + | SELECT s.repo_id, w.start, 0 AS merged, 0 AS agent_merged, 0 AS agent_reworked, 0 AS agent_drafted, | |
| 106 | + | 0.0 AS agent_draft_seconds, 0 AS reviewed, 0.0 AS review_seconds, 0 AS stalled, | |
| 107 | + | TOTAL(s.cost_usd) AS session_cost_usd | |
| 108 | + | FROM sessions s JOIN w ON s.started_at >= w.lo_at AND s.started_at < w.hi_at | |
| 109 | + | WHERE s.repo_id IN (SELECT value FROM json_each(?1)) | |
| 110 | + | AND s.started_at >= (SELECT MIN(lo_at) FROM w) AND s.started_at < (SELECT MAX(hi_at) FROM w) | |
| 111 | + | GROUP BY s.repo_id, w.start | |
| 30 | 112 | ) | |
| 31 | − | SELECT x.repo_id, w.start, | |
| 32 | − | COUNT(CASE WHEN x.status = 'merged' AND x.m >= w.lo AND x.m < w.hi THEN 1 END) AS merged, | |
| 33 | − | COUNT(CASE WHEN x.status = 'merged' AND x.m >= w.lo AND x.m < w.hi AND x.agent THEN 1 END) AS agent_merged, | |
| 34 | − | COUNT(CASE WHEN x.status = 'merged' AND x.m >= w.lo AND x.m < w.hi AND x.reworked THEN 1 END) AS agent_reworked, | |
| 35 | − | -- Opened in the week by an agent whose session started first. | |
| 36 | − | COUNT(CASE WHEN x.agent AND x.draft_seconds IS NOT NULL AND x.c >= w.lo AND x.c < w.hi THEN 1 END) AS agent_drafted, | |
| 37 | − | TOTAL(CASE WHEN x.agent AND x.c >= w.lo AND x.c < w.hi THEN x.draft_seconds END) AS agent_draft_seconds, | |
| 38 | − | COUNT(CASE WHEN x.r >= w.lo AND x.r < w.hi THEN 1 END) AS reviewed, | |
| 39 | − | TOTAL(CASE WHEN x.r >= w.lo AND x.r < w.hi THEN MAX(0, (x.r - x.c) * 86400) END) AS review_seconds, | |
| 40 | − | -- Open over seven days when the week ended, and not finished by then: | |
| 41 | − | -- not merged, and not closed (a closed one's updated_at stands in for | |
| 42 | − | -- when it closed, which pulls do not keep). | |
| 43 | − | COUNT(CASE WHEN x.c < w.cut - 7 | |
| 44 | − | AND NOT COALESCE(x.m < w.cut, 0) | |
| 45 | − | AND NOT (x.status = 'closed' AND COALESCE(x.u < w.cut, 0)) THEN 1 END) AS stalled | |
| 46 | − | FROM x CROSS JOIN w | |
| 47 | − | GROUP BY x.repo_id, w.start | |
| 48 | − | HAVING merged + agent_drafted + reviewed + stalled > 0 | |
| 49 | − | ORDER BY w.start, x.repo_id | |
| 113 | + | SELECT repo_id, start, SUM(merged) AS merged, SUM(agent_merged) AS agent_merged, | |
| 114 | + | SUM(agent_reworked) AS agent_reworked, SUM(agent_drafted) AS agent_drafted, | |
| 115 | + | TOTAL(agent_draft_seconds) AS agent_draft_seconds, SUM(reviewed) AS reviewed, | |
| 116 | + | TOTAL(review_seconds) AS review_seconds, SUM(stalled) AS stalled, TOTAL(session_cost_usd) AS session_cost_usd | |
| 117 | + | FROM (SELECT * FROM pw UNION ALL SELECT * FROM sx UNION ALL SELECT * FROM sw) | |
| 118 | + | GROUP BY repo_id, start | |
| 119 | + | HAVING SUM(merged) + SUM(agent_drafted) + SUM(reviewed) + SUM(stalled) > 0 OR TOTAL(session_cost_usd) > 0 | |
| 120 | + | ORDER BY start, repo_id | |
Comments
Sign in to comment.