Merge branch 'worktree-agent-ac87121c6a7d9842a' into home-signal
9 files+494−1030/9 viewed
| 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.