Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Merge branch 'worktree-agent-ac87121c6a7d9842a' into home-signal

syntaqxcommitted Parentsf43f125178a953Browse files
9 files+494−1030/9 viewed
+7−3
5050 pub review_seconds: f64,
5151 /// Pull requests open when the week ended (now, for the current week),
5252 /// 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.
5557 pub stalled: u32,
5658 /// What the harness sessions started in the week cost, as they
5759 /// reported it (estimated at list price from their token usage).
110112 pub item: Option<u32>,
111113 pub milestone: Option<u32>,
112114 /// 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.
114118 pub session_cost_usd: f64,
115119 }
116120
+9−3
18621862 /**
18631863 * One repository's week from UTC day `start`; empty weeks are left out.
18641864 * `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.
18671869 * `sessionCostUsd`: what the harness sessions started in the week cost,
18681870 * estimated at list price from the token usage they reported.
18691871 */
18961898 mergedAt: string | null;
18971899 item: number | null;
18981900 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+ */
19001906 sessionCostUsd: number;
19011907 };
19021908 /** One person's use of one harness in one repository, root sessions only. */
+4−0
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);
+38−9
2020 //! - **Draft time**: for an agent's pull request whose first tied session
2121 //! started before it opened, the seconds from that start to its opening,
2222 //! 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.
2743 //! - A day's **reviews** are comments of any kind on a pull request by a
2844 //! person other than its owner (not g1t's agent, a workspace agent or g1t
2945 //! itself); **reworked**, the ones of them asking for changes.
3046 //! - **Waiting**: open (not draft) pull requests with no first review.
3147 //! - **Authors** are owners who are people: not g1t's agent, a workspace
3248 //! 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.
3352 use std::collections::BTreeSet;
3453
3554 use g1t_contracts::identity::AGENT_ID;
3655 use g1t_contracts::repos::{ReadableArgs, Repo};
3756 use g1t_contracts::time::{parse_rfc3339, rfc3339};
3857 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,
4160 };
4261 use serde::{Deserialize, Serialize};
4362 use worker::Result;
6079 pub(crate) const WAITING: &str = concat!(include_str!("signals/base.sql"), include_str!("signals/waiting.sql"));
6180 pub(crate) const PULLS: &str = concat!(include_str!("signals/base.sql"), include_str!("signals/pulls.sql"));
6281 pub(crate) const AUTHORS: &str = include_str!("signals/authors.sql");
82+pub(crate) const HARNESSES: &str = include_str!("signals/harnesses.sql");
6383 /// ?1 the workspace.
6484 pub(crate) const MILESTONES: &str = "SELECT number, title, state, due_on, created_at, closed_at
6585 FROM milestones WHERE workspace = ?1 ORDER BY number";
191211 merged_at: Option<String>,
192212 item: Option<u32>,
193213 milestone: Option<u32>,
214+ session_cost_usd: f64,
194215 }
195216
196217 impl From<PullRow> for SignalPull {
207228 merged_at: row.merged_at,
208229 item: row.item,
209230 milestone: row.milestone,
231+ session_cost_usd: row.session_cost_usd,
210232 }
211233 }
212234 }
251273 let results = self
252274 .timing
253275 .db(
254− 6,
276+ 7,
255277 self.db.batch(vec![
256278 self.db.prepare(WEEKS).bind(&[ids.clone(), window.weeks.into()])?,
257279 self.db.prepare(DAYS).bind(&[
265287 .bind(&[ids.clone(), workspace.clone(), window.since.into()])?,
266288 self.db
267289 .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])?,
269292 self.db.prepare(MILESTONES).bind(&[workspace])?,
270293 ]),
271294 )
272295 .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 {
274297 return Err(worker::Error::RustError("workspace_signals: missing results".into()));
275298 };
276299 let mut waiting: Vec<WaitingPull> = waiting
286309 .into_iter()
287310 .filter(|author| is_person(&author.owner_id))
288311 .collect();
312+ let harnesses: Vec<SignalHarness> = harnesses
313+ .results::<SignalHarness>()?
314+ .into_iter()
315+ .filter(|harness| is_person(&harness.owner_id))
316+ .collect();
289317 let (items, milestones) = if in_workspace {
290318 let numbers = named_items(&pulls, &waiting);
291319 let visible = items::visible_numbered(self, &a.workspace, &numbers, viewer.as_ref()).await?;
319347 pulls,
320348 pulls_capped,
321349 authors,
350+ harnesses,
322351 items,
323352 milestones,
324353 })
+1−1
2727 -- session started before it opened, the seconds in between.
2828 pr AS (
2929 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,
3131 COALESCE(p.requested_by_id, p.author_id) AS owner_id,
3232 CASE WHEN p.requested_by_id IS NULL THEN p.author_name ELSE p.requested_by_name END AS owner_name,
3333 (p.agent = 'agent' OR p.author_id = 'usr_g1t_agent' OR f.pull_id IS NOT NULL) AS agent,
+19−0
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
+28−1
44 -- request's milestone is its work item's, else its own when that is one
55 -- of the workspace's (until the move to work items finishes, a pull
66 -- 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+)
732 SELECT pr.repo_id, pr.number, pr.title, pr.status, pr.agent, pr.owner_id, pr.owner_name,
833 pr.created_at, pr.merged_at, i.number AS item,
934 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
1137 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
1239 WHERE pr.created_at >= ?3 OR (pr.status = 'merged' AND pr.merged_at >= ?3)
1340 ORDER BY pr.created_at DESC, pr.repo_id, pr.number DESC
1441 LIMIT 3001
+297−66
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+};
24 use rusqlite::types::ValueRef;
35 use rusqlite::{Connection, ToSql, params};
46 use serde::de::DeserializeOwned;
57
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};
79
810 const IDS: &str = r#"["a"]"#;
911
1315 "CREATE TABLE pulls (id TEXT, repo_id TEXT, number INTEGER, title TEXT DEFAULT 'A change', status TEXT,
1416 item_id TEXT, milestone INTEGER, agent TEXT DEFAULT 'person', author_id TEXT DEFAULT 'usr_ana',
1517 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,
1719 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);
2029 CREATE TABLE session_links (session_id TEXT, repo_id TEXT, kind TEXT, number INTEGER);
2130 CREATE TABLE session_touches (session_id TEXT, repo_id TEXT, kind TEXT, value TEXT);
2231 CREATE TABLE work_items (id TEXT, workspace TEXT, number INTEGER, title TEXT, state TEXT,
4857
4958 fn comment(db: &Connection, number: u32, author: &str, agent: Option<&str>, verdict: Option<&str>, at: &str) {
5059 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)",
5261 params![number, author, agent, verdict, at],
5362 )
5463 .unwrap();
181190 "UPDATE pulls SET agent = 'agent' WHERE number = 1;
182191 UPDATE pulls SET author_id = 'usr_g1t_agent', requested_by_id = 'usr_ana', requested_by_name = 'ana' WHERE number = 2;
183192 -- 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');
185194 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');
189198 INSERT INTO session_touches VALUES ('ses_5', 'a', 'branch', 'branch-5');
190199 INSERT INTO session_touches VALUES ('ses_5b', 'a', 'branch', 'branch-5');
191200 -- 6's branch is on a fork: a push to the same name is not its.
192201 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');
194203 INSERT INTO session_touches VALUES ('ses_6', 'a', 'branch', 'branch-6');
195204 -- 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');",
197206 )
198207 .unwrap();
199208 pull(&db, 7, "open", "2026-10-06T12:00:00Z", None, "2026-10-06T12:00:00Z");
287296 }
288297
289298 #[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]
342299 fn a_first_review_is_a_persons_other_than_the_owner() {
343300 let db = schema();
344301 pull(&db, 1, "open", "2026-10-06T00:00:00Z", None, "2026-10-06T00:00:00Z");
516473 pull(&db, 2, "open", "2026-10-08T10:00:00Z", None, "2026-10-08T10:00:00Z");
517474 db.execute_batch(
518475 "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');",
521478 )
522479 .unwrap();
523480 comment(&db, 1, "usr_bo", None, Some("request_changes"), "2026-10-10T01:00:00Z");
562519 }
563520 assert!(is_person("usr_ana"));
564521 }
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+}
+91−20
99 ,
1010 w AS (
1111 SELECT json_extract(value, '$.start') AS start,
12+ json_extract(value, '$.from') AS lo_at, json_extract(value, '$.to') AS hi_at,
1213 julianday(json_extract(value, '$.from')) AS lo,
1314 julianday(json_extract(value, '$.to')) AS hi,
1415 julianday(json_extract(value, '$.cut')) AS cut
1516 FROM json_each(?2)
1617 ),
18+-- The earliest and the latest cut.
19+cuts AS (SELECT MIN(cut) AS lo, MAX(cut) AS hi FROM w),
1720 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,
1922 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,
2024 julianday((SELECT MIN(cm.created_at) FROM comments cm
2125 WHERE cm.repo_id = pr.repo_id AND cm.number = pr.number
2226 AND cm.verdict IN ('approve', 'request_changes')
2731 AND cm.author_id != pr.owner_id AND cm.author_id != 'usr_g1t_agent' AND cm.agent_id IS NULL
2832 AND julianday(cm.created_at) < julianday(pr.merged_at))) AS reworked
2933 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
30112 )
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.