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

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
syntaqxcommitted Parent0ba9169Browse 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.