1- import type { DatabaseSync } from "node:sqlite" ;
2- import type { SessionRow , SessionWithAgents } from "../../shared/agent-db-contract.js" ;
1+ import type { DatabaseSync , SQLInputValue } from "node:sqlite" ;
2+ import type {
3+ SessionPage ,
4+ SessionPageRequest ,
5+ SessionRow ,
6+ SessionWithAgents ,
7+ } from "../../shared/agent-db-contract.js" ;
38
49// Terminal session statuses (vendor + canonical AgentSession vocabulary). A
510// session not in this set is treated as active. Writes are owned by
611// `lifecycle.ts`; this store is read-only.
712const TERMINAL_STATUSES = "('completed', 'abandoned', 'error')" ;
813const TERMINAL_STATUS_SET = new Set ( [ "completed" , "abandoned" , "error" ] ) ;
9- const SESSION_DETAILS_CTES = `
10- WITH agent_counts AS (
11- SELECT session_id, COUNT(*) as agent_count
12- FROM agents
13- GROUP BY session_id
14- ),
15- event_counts AS (
16- SELECT session_id, COUNT(*) as event_count
17- FROM events
18- GROUP BY session_id
19- ),
20- token_totals AS (
21- SELECT
22- session_id,
23- COALESCE(SUM(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)), 0) as total_tokens
24- FROM token_usage
25- GROUP BY session_id
26- )
14+ const MAX_SESSION_PAGE_LIMIT = 100 ;
15+ const DEFAULT_SESSION_PAGE_LIMIT = 25 ;
16+ const SESSION_DETAIL_SELECT = `
17+ SELECT
18+ s.*,
19+ (SELECT COUNT(*) FROM agents a WHERE a.session_id = s.id) as agent_count,
20+ (SELECT COUNT(*) FROM events e WHERE e.session_id = s.id) as event_count,
21+ (
22+ SELECT COALESCE(SUM(COALESCE(t.input_tokens, 0) + COALESCE(t.output_tokens, 0)), 0)
23+ FROM token_usage t
24+ WHERE t.session_id = s.id
25+ ) as total_tokens
26+ FROM sessions s
2727` ;
2828
2929export function createSessionStore ( db : DatabaseSync ) {
@@ -32,32 +32,18 @@ export function createSessionStore(db: DatabaseSync) {
3232 const getActiveStmt = db . prepare (
3333 `SELECT * FROM sessions WHERE status NOT IN ${ TERMINAL_STATUSES } ORDER BY started_at DESC` ,
3434 ) ;
35+ const getDetailsByIdStmt = db . prepare ( `
36+ ${ SESSION_DETAIL_SELECT }
37+ WHERE s.id = ?
38+ ` ) ;
3539
3640 const getActiveWithDetailsStmt = db . prepare ( `
37- ${ SESSION_DETAILS_CTES }
38- SELECT
39- s.*,
40- COALESCE(ac.agent_count, 0) as agent_count,
41- COALESCE(ec.event_count, 0) as event_count,
42- COALESCE(tt.total_tokens, 0) as total_tokens
43- FROM sessions s
44- LEFT JOIN agent_counts ac ON ac.session_id = s.id
45- LEFT JOIN event_counts ec ON ec.session_id = s.id
46- LEFT JOIN token_totals tt ON tt.session_id = s.id
41+ ${ SESSION_DETAIL_SELECT }
4742 WHERE s.status NOT IN ${ TERMINAL_STATUSES }
4843 ORDER BY s.started_at DESC
4944 ` ) ;
5045 const getHistoricalWithDetailsStmt = db . prepare ( `
51- ${ SESSION_DETAILS_CTES }
52- SELECT
53- s.*,
54- COALESCE(ac.agent_count, 0) as agent_count,
55- COALESCE(ec.event_count, 0) as event_count,
56- COALESCE(tt.total_tokens, 0) as total_tokens
57- FROM sessions s
58- LEFT JOIN agent_counts ac ON ac.session_id = s.id
59- LEFT JOIN event_counts ec ON ec.session_id = s.id
60- LEFT JOIN token_totals tt ON tt.session_id = s.id
46+ ${ SESSION_DETAIL_SELECT }
6147 WHERE s.status IN ${ TERMINAL_STATUSES }
6248 ORDER BY s.started_at DESC
6349 ` ) ;
@@ -97,6 +83,54 @@ export function createSessionStore(db: DatabaseSync) {
9783 } ) ;
9884 }
9985
86+ function coercePageRequest ( request : SessionPageRequest | undefined ) : {
87+ limit : number ;
88+ offset : number ;
89+ status : string | null ;
90+ q : string | null ;
91+ } {
92+ const requestedLimit = request ?. limit ;
93+ const limit = typeof requestedLimit === "number" && Number . isInteger ( requestedLimit )
94+ ? Math . min ( Math . max ( requestedLimit , 1 ) , MAX_SESSION_PAGE_LIMIT )
95+ : DEFAULT_SESSION_PAGE_LIMIT ;
96+ const requestedOffset = request ?. offset ;
97+ const offset = typeof requestedOffset === "number" && Number . isInteger ( requestedOffset )
98+ ? Math . max ( requestedOffset , 0 )
99+ : 0 ;
100+ const status =
101+ typeof request ?. status === "string" && request . status . length > 0
102+ ? request . status
103+ : null ;
104+ const q =
105+ typeof request ?. q === "string" && request . q . trim ( ) . length > 0
106+ ? request . q . trim ( )
107+ : null ;
108+ return { limit, offset, status, q } ;
109+ }
110+
111+ function pageWhereClause ( status : string | null , q : string | null ) : {
112+ whereSql : string ;
113+ params : SQLInputValue [ ] ;
114+ } {
115+ const where : string [ ] = [ ] ;
116+ const params : SQLInputValue [ ] = [ ] ;
117+ if ( status === "waiting" ) {
118+ where . push ( "s.status NOT IN ('completed', 'abandoned', 'error') AND s.awaiting_input_since IS NOT NULL" ) ;
119+ } else if ( status && status !== "all" ) {
120+ where . push ( "s.status = ?" ) ;
121+ params . push ( status ) ;
122+ }
123+ if ( q ) {
124+ const like = `%${ q } %` ;
125+ where . push ( "(s.id LIKE ? OR s.name LIKE ? OR s.cwd LIKE ? OR s.model LIKE ?)" ) ;
126+ params . push ( like , like , like , like ) ;
127+ }
128+ return {
129+ whereSql : where . length > 0 ? `WHERE ${ where . join ( " AND " ) } ` : "" ,
130+ params,
131+ } ;
132+ }
133+
100134 return {
101135 getById ( id : string ) : SessionRow | undefined {
102136 return toRow ( getByIdStmt . get ( id ) as Record < string , unknown > | undefined ) ;
@@ -110,6 +144,11 @@ export function createSessionStore(db: DatabaseSync) {
110144 return rowsToList ( getActiveStmt . all ( ) as Record < string , unknown > [ ] ) ;
111145 } ,
112146
147+ getDetailsById ( id : string ) : SessionWithAgents | undefined {
148+ const row = getDetailsByIdStmt . get ( id ) as Record < string , unknown > | undefined ;
149+ return row ? detailRowsToList ( [ row ] ) [ 0 ] : undefined ;
150+ } ,
151+
113152 getActiveWithDetails ( ) : SessionWithAgents [ ] {
114153 return detailRowsToList (
115154 getActiveWithDetailsStmt . all ( ) as Record < string , unknown > [ ] ,
@@ -133,6 +172,27 @@ export function createSessionStore(db: DatabaseSync) {
133172 ] ;
134173 } ,
135174
175+ getPage ( request ?: SessionPageRequest ) : SessionPage {
176+ const { limit, offset, status, q } = coercePageRequest ( request ) ;
177+ const { whereSql, params } = pageWhereClause ( status , q ) ;
178+ const totalRow = db . prepare (
179+ `SELECT COUNT(*) as count FROM sessions s ${ whereSql } ` ,
180+ ) . get ( ...params ) as { count : number } ;
181+ const rows = db . prepare ( `
182+ ${ SESSION_DETAIL_SELECT }
183+ ${ whereSql }
184+ ORDER BY s.started_at DESC
185+ LIMIT ? OFFSET ?
186+ ` ) . all ( ...params , limit , offset ) as Record < string , unknown > [ ] ;
187+
188+ return {
189+ sessions : detailRowsToList ( rows ) ,
190+ total : totalRow . count ,
191+ limit,
192+ offset,
193+ } ;
194+ } ,
195+
136196 invalidateHistoricalDetails ( ) : void {
137197 historicalDetailsCache = null ;
138198 } ,
0 commit comments