ocwatch 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ import type {
2
+ SessionMetadata,
3
+ TreeNode,
4
+ TreeEdge,
5
+ SessionTree,
6
+ } from "../../shared/types";
7
+ import { MAX_RECURSION_DEPTH } from "../../shared/constants";
8
+ import {
9
+ isAssistantFinished,
10
+ getSessionStatusInfo,
11
+ } from "../logic";
12
+ import {
13
+ createSessionContext,
14
+ getSessionFromContext,
15
+ getSessionMessages,
16
+ getSessionChildren,
17
+ } from "./sessionContext";
18
+
19
+ export async function buildSessionTree(
20
+ rootSessionID: string,
21
+ allSessions: SessionMetadata[]
22
+ ): Promise<SessionTree> {
23
+ const nodes: TreeNode[] = [];
24
+ const edges: TreeEdge[] = [];
25
+ const visited = new Set<string>();
26
+ const context = createSessionContext(allSessions);
27
+
28
+ async function processSession(sessionID: string, depth = 0) {
29
+ if (depth > MAX_RECURSION_DEPTH) {
30
+ console.warn(`Max recursion depth reached for session ${sessionID}`);
31
+ return;
32
+ }
33
+ if (visited.has(sessionID)) {
34
+ return;
35
+ }
36
+ visited.add(sessionID);
37
+
38
+ const session = getSessionFromContext(sessionID, context);
39
+ if (!session) {
40
+ return;
41
+ }
42
+
43
+ const messages = getSessionMessages(sessionID, context);
44
+ const lastAssistantFinished = isAssistantFinished(messages);
45
+ const isSubagent = !!session.parentID;
46
+ const status = getSessionStatusInfo(
47
+ messages,
48
+ false,
49
+ undefined,
50
+ undefined,
51
+ lastAssistantFinished,
52
+ isSubagent
53
+ ).status;
54
+
55
+ const lastMessage = messages.sort(
56
+ (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
57
+ )[0];
58
+
59
+ nodes.push({
60
+ id: session.id,
61
+ data: {
62
+ title: session.title,
63
+ agent: lastMessage?.agent,
64
+ model: lastMessage?.modelID,
65
+ isActive: status === "working" || status === "idle",
66
+ },
67
+ });
68
+
69
+ if (session.parentID) {
70
+ edges.push({
71
+ source: session.parentID,
72
+ target: session.id,
73
+ });
74
+ await processSession(session.parentID, depth + 1);
75
+ }
76
+
77
+ const children = getSessionChildren(sessionID, context);
78
+ await Promise.all(
79
+ children.map((child) => {
80
+ edges.push({
81
+ source: sessionID,
82
+ target: child.id,
83
+ });
84
+ return processSession(child.id, depth + 1);
85
+ })
86
+ );
87
+ }
88
+
89
+ await processSession(rootSessionID, 0);
90
+
91
+ return { nodes, edges };
92
+ }
@@ -22,15 +22,8 @@ function getDbPath(): string {
22
22
  }
23
23
 
24
24
  function configureConnectionPragmas(db: Database): void {
25
- db.query("PRAGMA busy_timeout = 5000;").run();
26
- db.query("PRAGMA cache_size = -20000;").run();
27
-
28
- try {
29
- db.query("PRAGMA journal_mode = WAL;").run();
30
- } catch (error) {
31
- const reason = error instanceof Error ? error.message : String(error);
32
- console.warn(`[storage/db] Failed to enforce WAL journal mode: ${reason}`);
33
- }
25
+ db.query(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};`).run();
26
+ db.query(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE};`).run();
34
27
  }
35
28
 
36
29
  export function checkDbExists(): boolean {
@@ -68,4 +61,3 @@ export function closeDb(): void {
68
61
  dbSingleton.close();
69
62
  dbSingleton = undefined;
70
63
  }
71
-
@@ -2,20 +2,26 @@ export { checkDbExists, closeDb, getDb } from "./db";
2
2
  export {
3
3
  queryMaxTimestamp,
4
4
  queryMessages,
5
+ queryMessagesForSessions,
5
6
  queryPart,
6
7
  queryParts,
8
+ queryPartsForSessions,
7
9
  queryProjects,
10
+ queryProjectByWorktree,
11
+ queryProjectSummaries,
8
12
  querySession,
9
13
  querySessionChildren,
14
+ querySessionSubtree,
15
+ querySessionSubtreeRevision,
10
16
  querySessions,
11
17
  queryTodos,
12
18
  listProjects,
13
- listAllSessions,
14
19
  } from "./queries";
15
20
  export type {
16
21
  DbMessageRow,
17
22
  DbPartRow,
18
23
  DbProjectRow,
24
+ DbProjectSummaryRow,
19
25
  DbSessionRow,
20
26
  DbTodoRow,
21
27
  } from "./queries";
@@ -1,7 +1,5 @@
1
1
  import type { Database, Statement } from "bun:sqlite";
2
2
  import { getDb } from "./db";
3
- import type { SessionMetadata } from "../../shared/types";
4
- import { toSessionMetadata } from "../services/parsing";
5
3
 
6
4
  export interface DbProjectRow {
7
5
  id: string;
@@ -58,17 +56,28 @@ export interface DbTodoRow {
58
56
  timeUpdated: number;
59
57
  }
60
58
 
59
+ export interface DbProjectSummaryRow {
60
+ id: string;
61
+ worktree: string;
62
+ sessionCount: number;
63
+ lastActivityAt: number;
64
+ }
65
+
61
66
  let cachedDb: Database | null | undefined;
62
67
 
63
68
  let queryProjectsStmt: Statement<DbProjectRow, []> | null = null;
64
69
  let querySessionsStmt: Statement<DbSessionRow, [string | null, number | null, number]> | null = null;
65
70
  let querySessionStmt: Statement<DbSessionRow, [string]> | null = null;
66
71
  let querySessionChildrenStmt: Statement<DbSessionRow, [string]> | null = null;
72
+ let querySessionSubtreeStmt: Statement<DbSessionRow, [string]> | null = null;
67
73
  let queryMessagesStmt: Statement<DbMessageRow, [string, number]> | null = null;
68
74
  let queryPartsStmt: Statement<DbPartRow, [string]> | null = null;
69
75
  let queryPartStmt: Statement<DbPartRow, [string]> | null = null;
70
76
  let queryTodosStmt: Statement<DbTodoRow, [string]> | null = null;
71
77
  let queryMaxTimestampStmt: Statement<{ maxTimestamp: number | null }, []> | null = null;
78
+ let querySessionSubtreeRevisionStmt: Statement<{ maxTimestamp: number | null }, [string]> | null = null;
79
+ let queryProjectByWorktreeStmt: Statement<DbProjectRow, [string]> | null = null;
80
+ let queryProjectSummariesStmt: Statement<DbProjectSummaryRow, []> | null = null;
72
81
 
73
82
  function getReadyDb(): Database | null {
74
83
  const db = getDb();
@@ -145,6 +154,50 @@ function getReadyDb(): Database | null {
145
154
  ORDER BY time_created ASC
146
155
  `);
147
156
 
157
+ querySessionSubtreeStmt = db.query<DbSessionRow, [string]>(`
158
+ WITH RECURSIVE subtree AS (
159
+ SELECT
160
+ id,
161
+ project_id AS projectID,
162
+ parent_id AS parentID,
163
+ slug,
164
+ directory,
165
+ title,
166
+ version,
167
+ time_created AS timeCreated,
168
+ time_updated AS timeUpdated
169
+ FROM session
170
+ WHERE id = ?1
171
+
172
+ UNION ALL
173
+
174
+ SELECT
175
+ s.id,
176
+ s.project_id AS projectID,
177
+ s.parent_id AS parentID,
178
+ s.slug,
179
+ s.directory,
180
+ s.title,
181
+ s.version,
182
+ s.time_created AS timeCreated,
183
+ s.time_updated AS timeUpdated
184
+ FROM session s
185
+ INNER JOIN subtree st ON s.parent_id = st.id
186
+ )
187
+ SELECT
188
+ id,
189
+ projectID,
190
+ parentID,
191
+ slug,
192
+ directory,
193
+ title,
194
+ version,
195
+ timeCreated,
196
+ timeUpdated
197
+ FROM subtree
198
+ ORDER BY timeCreated ASC, id ASC
199
+ `);
200
+
148
201
  queryMessagesStmt = db.query<DbMessageRow, [string, number]>(`
149
202
  SELECT
150
203
  id,
@@ -225,6 +278,55 @@ function getReadyDb(): Database | null {
225
278
  )
226
279
  `);
227
280
 
281
+ querySessionSubtreeRevisionStmt = db.query<{ maxTimestamp: number | null }, [string]>(`
282
+ WITH RECURSIVE subtree AS (
283
+ SELECT id
284
+ FROM session
285
+ WHERE id = ?1
286
+
287
+ UNION ALL
288
+
289
+ SELECT s.id
290
+ FROM session s
291
+ INNER JOIN subtree st ON s.parent_id = st.id
292
+ )
293
+ SELECT MAX(ts) AS maxTimestamp
294
+ FROM (
295
+ SELECT MAX(time_updated) AS ts FROM session WHERE id IN (SELECT id FROM subtree)
296
+ UNION ALL
297
+ SELECT MAX(time_updated) AS ts FROM message WHERE session_id IN (SELECT id FROM subtree)
298
+ UNION ALL
299
+ SELECT MAX(time_updated) AS ts FROM part WHERE session_id IN (SELECT id FROM subtree)
300
+ )
301
+ `);
302
+
303
+ queryProjectByWorktreeStmt = db.query<DbProjectRow, [string]>(`
304
+ SELECT
305
+ id,
306
+ name,
307
+ worktree,
308
+ vcs,
309
+ commands,
310
+ sandboxes,
311
+ time_created AS timeCreated,
312
+ time_updated AS timeUpdated
313
+ FROM project
314
+ WHERE worktree = ?1
315
+ LIMIT 1
316
+ `);
317
+
318
+ queryProjectSummariesStmt = db.query<DbProjectSummaryRow, []>(`
319
+ SELECT
320
+ p.id,
321
+ p.worktree,
322
+ COUNT(s.id) AS sessionCount,
323
+ COALESCE(MAX(s.time_updated), p.time_updated) AS lastActivityAt
324
+ FROM project p
325
+ LEFT JOIN session s ON s.project_id = p.id
326
+ GROUP BY p.id
327
+ ORDER BY lastActivityAt DESC
328
+ `);
329
+
228
330
  return db;
229
331
  }
230
332
 
@@ -268,6 +370,15 @@ export function querySessionChildren(sessionId: string): DbSessionRow[] {
268
370
  return querySessionChildrenStmt.all(sessionId);
269
371
  }
270
372
 
373
+ export function querySessionSubtree(sessionId: string): DbSessionRow[] {
374
+ const db = getReadyDb();
375
+ if (!db || !querySessionSubtreeStmt) {
376
+ return [];
377
+ }
378
+
379
+ return querySessionSubtreeStmt.all(sessionId);
380
+ }
381
+
271
382
  export function queryMessages(sessionId: string, limit = 100): DbMessageRow[] {
272
383
  const db = getReadyDb();
273
384
  if (!db || !queryMessagesStmt) {
@@ -286,6 +397,76 @@ export function queryParts(sessionId: string): DbPartRow[] {
286
397
  return queryPartsStmt.all(sessionId);
287
398
  }
288
399
 
400
+ export function queryMessagesForSessions(sessionIds: string[], limitPerSession = 100): DbMessageRow[] {
401
+ const db = getReadyDb();
402
+ if (!db || sessionIds.length === 0) {
403
+ return [];
404
+ }
405
+
406
+ const placeholders = sessionIds.map((_, index) => `?${index + 1}`).join(", ");
407
+ const limitParam = `?${sessionIds.length + 1}`;
408
+ const statement = db.query<DbMessageRow, (string | number)[]>(`
409
+ SELECT
410
+ id,
411
+ sessionID,
412
+ timeCreated,
413
+ timeUpdated,
414
+ role,
415
+ agent,
416
+ data
417
+ FROM (
418
+ SELECT
419
+ id,
420
+ session_id AS sessionID,
421
+ time_created AS timeCreated,
422
+ time_updated AS timeUpdated,
423
+ json_extract(data, '$.role') AS role,
424
+ json_extract(data, '$.agent') AS agent,
425
+ data,
426
+ ROW_NUMBER() OVER (
427
+ PARTITION BY session_id
428
+ ORDER BY time_created DESC, id DESC
429
+ ) AS rowNumber
430
+ FROM message
431
+ WHERE session_id IN (${placeholders})
432
+ )
433
+ WHERE rowNumber <= ${limitParam}
434
+ ORDER BY sessionID ASC, timeCreated DESC, id DESC
435
+ `);
436
+
437
+ return statement.all(...sessionIds, limitPerSession);
438
+ }
439
+
440
+ export function queryPartsForSessions(sessionIds: string[]): DbPartRow[] {
441
+ const db = getReadyDb();
442
+ if (!db || sessionIds.length === 0) {
443
+ return [];
444
+ }
445
+
446
+ const placeholders = sessionIds.map((_, index) => `?${index + 1}`).join(", ");
447
+ const statement = db.query<DbPartRow, string[]>(`
448
+ SELECT
449
+ id,
450
+ message_id AS messageID,
451
+ session_id AS sessionID,
452
+ time_created AS timeCreated,
453
+ time_updated AS timeUpdated,
454
+ json_extract(data, '$.type') AS type,
455
+ json_extract(data, '$.tool') AS tool,
456
+ CASE
457
+ WHEN json_type(data, '$.state') = 'text' THEN json_extract(data, '$.state')
458
+ WHEN json_type(data, '$.state.type') = 'text' THEN json_extract(data, '$.state.type')
459
+ ELSE NULL
460
+ END AS state,
461
+ data
462
+ FROM part
463
+ WHERE session_id IN (${placeholders})
464
+ ORDER BY sessionID ASC, timeCreated DESC, id DESC
465
+ `);
466
+
467
+ return statement.all(...sessionIds);
468
+ }
469
+
289
470
  export function queryPart(partId: string): DbPartRow | null {
290
471
  const db = getReadyDb();
291
472
  if (!db || !queryPartStmt) {
@@ -313,13 +494,35 @@ export function queryMaxTimestamp(): number {
313
494
  return Number(queryMaxTimestampStmt.get()?.maxTimestamp ?? 0);
314
495
  }
315
496
 
497
+ export function querySessionSubtreeRevision(sessionId: string): number {
498
+ const db = getReadyDb();
499
+ if (!db || !querySessionSubtreeRevisionStmt) {
500
+ return 0;
501
+ }
502
+
503
+ return Number(querySessionSubtreeRevisionStmt.get(sessionId)?.maxTimestamp ?? 0);
504
+ }
505
+
316
506
 
317
507
  export function listProjects(): string[] {
318
508
  const projects = queryProjects();
319
509
  return projects.map((p) => p.id);
320
510
  }
321
511
 
322
- export function listAllSessions(): SessionMetadata[] {
323
- const sessions = querySessions(undefined, undefined, 10000);
324
- return sessions.map(toSessionMetadata);
512
+ export function queryProjectByWorktree(directory: string): DbProjectRow | null {
513
+ const db = getReadyDb();
514
+ if (!db || !queryProjectByWorktreeStmt) {
515
+ return null;
516
+ }
517
+
518
+ return queryProjectByWorktreeStmt.get(directory);
519
+ }
520
+
521
+ export function queryProjectSummaries(): DbProjectSummaryRow[] {
522
+ const db = getReadyDb();
523
+ if (!db || !queryProjectSummariesStmt) {
524
+ return [];
525
+ }
526
+
527
+ return queryProjectSummariesStmt.all();
325
528
  }
@@ -1,5 +1,5 @@
1
1
  import { stat } from "node:fs/promises";
2
- import { listAllSessions } from "../storage";
2
+ import { queryProjects } from "../storage";
3
3
  import type { SessionMetadata } from "../../shared/types";
4
4
 
5
5
  async function directoryExists(directory: string): Promise<boolean> {
@@ -15,8 +15,14 @@ export async function resolveProjectDirectory(
15
15
  projectId: string,
16
16
  preloadedSessions?: SessionMetadata[],
17
17
  ): Promise<string | null> {
18
- const allSessions = preloadedSessions ?? listAllSessions();
19
- const directory = allSessions.find((session) => session.projectID === projectId)?.directory;
18
+ let directory: string | undefined;
19
+
20
+ if (preloadedSessions) {
21
+ directory = preloadedSessions.find((session) => session.projectID === projectId)?.directory;
22
+ } else {
23
+ const projects = queryProjects();
24
+ directory = projects.find((p) => p.id === projectId)?.worktree;
25
+ }
20
26
 
21
27
  if (!directory) {
22
28
  return null;
@@ -3,7 +3,7 @@
3
3
  * Determines session status based on message timestamps and tool call state
4
4
  */
5
5
 
6
- import type { MessageMeta, SessionStatus } from "../../shared/types";
6
+ import type { SessionStatus } from "../../shared/types";
7
7
  import { isPendingToolCall } from "../logic/activityLogic";
8
8
  import { getSessionStatusInfo } from "../logic/sessionLogic";
9
9
 
@@ -14,24 +14,6 @@ export type { WaitingReason, SessionStatusInfo } from "../logic/sessionLogic";
14
14
  // Thresholds in milliseconds
15
15
  const WORKING_THRESHOLD = 30 * 1000; // 30 seconds
16
16
  const COMPLETED_THRESHOLD = 5 * 60 * 1000; // 5 minutes
17
- export function getSessionStatus(
18
- messages: MessageMeta[],
19
- hasPendingToolCall: boolean = false,
20
- lastToolCompletedAt?: Date,
21
- workingChildCount?: number,
22
- lastAssistantFinished?: boolean,
23
- isSubagent: boolean = false
24
- ): SessionStatus {
25
- return getSessionStatusInfo(
26
- messages,
27
- hasPendingToolCall,
28
- lastToolCompletedAt,
29
- workingChildCount,
30
- lastAssistantFinished,
31
- isSubagent
32
- ).status;
33
- }
34
-
35
17
  /**
36
18
  * Get status from timestamp directly (for simpler cases)
37
19
  * Used when you only have the updatedAt timestamp
@@ -5,14 +5,12 @@ export const sessionIdSchema = z.string().regex(/^ses_[a-zA-Z0-9]+$/, 'Invalid s
5
5
 
6
6
  export const partIdSchema = z.string().min(1, 'Part ID required');
7
7
 
8
+ export const projectIdSchema = z.string().regex(/^[a-zA-Z0-9_-]+$/, 'Invalid project ID format');
9
+
8
10
  export const pollQuerySchema = z.object({
9
11
  sessionId: sessionIdSchema.optional(),
10
12
  });
11
13
 
12
- export function validateParam<T>(schema: z.ZodSchema<T>, value: unknown): T {
13
- return schema.parse(value);
14
- }
15
-
16
14
  export type ValidationResult<T> = { success: true; value: T } | { success: false; response: Response };
17
15
 
18
16
  export function validateWithResponse<T>(
@@ -1,4 +1,4 @@
1
- import { existsSync, watch, type FSWatcher } from "node:fs";
1
+ import { existsSync, statSync, watch, type FSWatcher } from "node:fs";
2
2
  import { basename, join } from "node:path";
3
3
  import { EventEmitter } from "node:events";
4
4
  import { homedir } from "node:os";
@@ -23,6 +23,7 @@ export class Watcher extends EventEmitter {
23
23
  private readonly projectPath: string;
24
24
  private dbWatcherTarget: string | null = null;
25
25
  private boulderWatcherTarget: string | null = null;
26
+ private lastKnownBoulderSignature: string | null = null;
26
27
  private isRunning: boolean = false;
27
28
 
28
29
  constructor(options: WatcherOptions = {}) {
@@ -144,15 +145,24 @@ export class Watcher extends EventEmitter {
144
145
  this.handleBoulderEvent(eventType, filename);
145
146
  });
146
147
  this.boulderWatcherTarget = preferredTarget;
148
+ this.lastKnownBoulderSignature = this.getBoulderSignature();
147
149
  }
148
150
 
149
151
  private handleBoulderEvent(eventType: string, filename: string | null): void {
152
+ const currentSignature = this.getBoulderSignature();
153
+ const signatureChanged = currentSignature !== this.lastKnownBoulderSignature;
154
+
150
155
  if (this.boulderWatcherTarget === this.boulderDirPath) {
151
- if (!filename || filename !== "boulder.json") {
156
+ if (filename && filename !== "boulder.json" && !signatureChanged) {
157
+ return;
158
+ }
159
+
160
+ if (!filename && !signatureChanged) {
152
161
  return;
153
162
  }
154
163
  }
155
164
 
165
+ this.lastKnownBoulderSignature = currentSignature;
156
166
  this.emitDebouncedChange(eventType, ".sisyphus/boulder.json");
157
167
 
158
168
  if (eventType === "rename") {
@@ -168,6 +178,32 @@ export class Watcher extends EventEmitter {
168
178
  }
169
179
  }
170
180
 
181
+ private getBoulderSignature(): string | null {
182
+ try {
183
+ const stats = statSync(this.boulderPath);
184
+ return `${stats.size}:${stats.mtimeMs}`;
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ private pollForBoulderChanges(): void {
191
+ const currentSignature = this.getBoulderSignature();
192
+ if (currentSignature === this.lastKnownBoulderSignature) {
193
+ return;
194
+ }
195
+
196
+ this.lastKnownBoulderSignature = currentSignature;
197
+ this.emitDebouncedChange("change", ".sisyphus/boulder.json");
198
+
199
+ const preferredTarget = existsSync(this.boulderPath)
200
+ ? this.boulderPath
201
+ : this.boulderDirPath;
202
+ if (preferredTarget !== this.boulderWatcherTarget) {
203
+ this.rebindBoulderWatcherSafe();
204
+ }
205
+ }
206
+
171
207
  private startRebindLoop(): void {
172
208
  if (this.rebindTimer) {
173
209
  clearInterval(this.rebindTimer);
@@ -178,9 +214,10 @@ export class Watcher extends EventEmitter {
178
214
  return;
179
215
  }
180
216
 
217
+ this.pollForBoulderChanges();
181
218
  this.rebindDbWatcherSafe();
182
219
  this.rebindBoulderWatcherSafe();
183
- }, 1000);
220
+ }, 100);
184
221
  }
185
222
 
186
223
  private rebindDbWatcherSafe(): void {
@@ -226,6 +263,7 @@ export class Watcher extends EventEmitter {
226
263
  this.boulderWatcherTarget = null;
227
264
  }
228
265
 
266
+ this.lastKnownBoulderSignature = null;
229
267
  this.isRunning = false;
230
268
  this.emit("stopped");
231
269
  }
@@ -5,9 +5,6 @@
5
5
  export const DEFAULT_PORT = 50234 as const;
6
6
  export const API_BASE_URL = `http://localhost:${DEFAULT_PORT}`;
7
7
 
8
- // Time constants
9
- export const TWENTY_FOUR_HOURS_MS = 86400000 as const;
10
-
11
8
  // API limits
12
9
  export const MAX_SESSIONS_LIMIT = 20 as const;
13
10
  /** Messages returned per session in API responses (client-facing limit) */
@@ -21,3 +18,10 @@ export const RINGBUFFER_CAPACITY = 1000 as const;
21
18
 
22
19
  // Session hierarchy depth limit
23
20
  export const MAX_RECURSION_DEPTH = 10 as const;
21
+
22
+ /** Internal upper bound for session queries (not client-facing) */
23
+ export const SESSION_SCAN_LIMIT = 50_000 as const;
24
+ /** Internal upper bound for message queries per session (not client-facing) */
25
+ export const MESSAGE_SCAN_LIMIT = 10_000 as const;
26
+ /** Cooldown between notifications for the same session (ms) */
27
+ export const NOTIFICATION_COOLDOWN_MS = 10_000 as const;
@@ -1,2 +1,5 @@
1
1
  export const VERSION = "0.1.0";
2
2
  export const PROJECT_NAME = "OCWatch";
3
+
4
+ export { synthesizeActivityItems } from './utils/activityUtils';
5
+ export { RingBuffer } from './utils/RingBuffer';
@@ -74,11 +74,13 @@ export interface MessageMeta {
74
74
  * Includes agent info and hierarchy
75
75
  */
76
76
  export type SessionActivityType = "tool" | "reasoning" | "patch" | "waiting-tools" | "waiting-user" | "idle";
77
+ export type ActivityNodeKind = "session" | "phase";
77
78
 
78
79
  export interface ActivitySession {
79
80
  id: string;
80
81
  title: string;
81
82
  agent: string;
83
+ nodeKind?: ActivityNodeKind;
82
84
  modelID?: string;
83
85
  providerID?: string;
84
86
  parentID?: string;
@@ -126,17 +128,6 @@ export interface PartMeta {
126
128
  patchFiles?: string[];
127
129
  }
128
130
 
129
- /**
130
- * AgentInfo represents an active agent
131
- */
132
- export interface AgentInfo {
133
- name: string;
134
- mode: string;
135
- modelID: string;
136
- active: boolean;
137
- sessionID: string;
138
- }
139
-
140
131
  /**
141
132
  * ToolCall represents a tool invocation
142
133
  */
@@ -214,41 +205,6 @@ export type ActivityItem =
214
205
  | AgentSpawnActivity
215
206
  | AgentCompleteActivity;
216
207
 
217
- /**
218
- * BurstEntry represents a burst of tool call activity
219
- * Groups consecutive tool calls from the same agent with aggregated metrics
220
- */
221
- export interface BurstEntry {
222
- id: string;
223
- type: "burst";
224
- agentName: string;
225
- items: ToolCallActivity[];
226
- toolBreakdown: Record<string, number>;
227
- durationMs: number;
228
- firstTimestamp: Date;
229
- lastTimestamp: Date;
230
- pendingCount: number;
231
- errorCount: number;
232
- }
233
-
234
- /**
235
- * MilestoneEntry represents a significant event in the activity stream
236
- * (agent spawn or agent complete)
237
- */
238
- export interface MilestoneEntry {
239
- id: string;
240
- type: "milestone";
241
- item: AgentSpawnActivity | AgentCompleteActivity;
242
- }
243
-
244
- /**
245
- * StreamEntry is a union type for activity stream entries
246
- * Represents either a burst of tool calls or a milestone event
247
- */
248
- export type StreamEntry = BurstEntry | MilestoneEntry;
249
-
250
- export { synthesizeActivityItems } from '../utils/activityUtils';
251
-
252
208
  /**
253
209
  * PlanProgress represents progress on a plan
254
210
  */
@@ -284,6 +240,13 @@ export interface SessionStats {
284
240
  modelBreakdown: ModelTokens[];
285
241
  }
286
242
 
243
+ export interface SessionActivityResponse {
244
+ session: SessionSummary;
245
+ activity: ActivitySession[];
246
+ stats: SessionStats | null;
247
+ revision: number;
248
+ }
249
+
287
250
  /**
288
251
  * Boulder represents the current plan state
289
252
  */
@@ -359,5 +322,3 @@ export interface PollResponse {
359
322
  planName?: string;
360
323
  lastUpdate: number;
361
324
  }
362
-
363
- export { RingBuffer } from '../utils/RingBuffer';