ocwatch 0.4.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.
Files changed (41) hide show
  1. package/README.md +22 -3
  2. package/package.json +4 -4
  3. package/src/client/dist/assets/GraphView-BZV40eAE.css +1 -0
  4. package/src/client/dist/assets/GraphView-KWCCGYb2.js +9 -0
  5. package/src/client/dist/assets/graph-Cw_XSlvx.js +7 -0
  6. package/src/client/dist/assets/index-CbgYG3pJ.js +23 -0
  7. package/src/client/dist/assets/index-CgDCc8Mm.css +1 -0
  8. package/src/client/dist/assets/motion-CGUGF2CN.js +9 -0
  9. package/src/client/dist/index.html +4 -2
  10. package/src/server/__tests__/helpers/testDb.ts +220 -0
  11. package/src/server/index.ts +27 -27
  12. package/src/server/logic/activityLogic.ts +260 -0
  13. package/src/server/logic/index.ts +2 -0
  14. package/src/server/logic/sessionLogic.ts +107 -0
  15. package/src/server/routes/parts.ts +9 -7
  16. package/src/server/routes/poll.ts +32 -46
  17. package/src/server/routes/projects.ts +10 -27
  18. package/src/server/routes/sessions.ts +159 -68
  19. package/src/server/routes/sse.ts +10 -4
  20. package/src/server/services/parsing.ts +211 -0
  21. package/src/server/services/pollService.ts +400 -116
  22. package/src/server/services/recentSessions.ts +14 -0
  23. package/src/server/services/sessionContext.ts +97 -0
  24. package/src/server/services/sessionService.ts +97 -193
  25. package/src/server/services/sessionTree.ts +92 -0
  26. package/src/server/storage/db.ts +63 -0
  27. package/src/server/storage/index.ts +28 -0
  28. package/src/server/storage/queries.ts +528 -0
  29. package/src/server/utils/projectResolver.ts +9 -3
  30. package/src/server/utils/sessionStatus.ts +5 -89
  31. package/src/server/validation.ts +2 -4
  32. package/src/server/watcher.ts +225 -82
  33. package/src/shared/constants.ts +8 -3
  34. package/src/shared/index.ts +3 -0
  35. package/src/shared/types/index.ts +48 -53
  36. package/src/shared/utils/activityUtils.ts +3 -2
  37. package/src/client/dist/assets/index-BIu7r5_5.css +0 -1
  38. package/src/client/dist/assets/index-BYMVif3u.js +0 -50
  39. package/src/server/storage/messageParser.ts +0 -169
  40. package/src/server/storage/partParser.ts +0 -532
  41. package/src/server/storage/sessionParser.ts +0 -180
@@ -3,100 +3,17 @@
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";
7
- import { isPendingToolCall } from "../storage/partParser";
6
+ import type { SessionStatus } from "../../shared/types";
7
+ import { isPendingToolCall } from "../logic/activityLogic";
8
+ import { getSessionStatusInfo } from "../logic/sessionLogic";
8
9
 
9
10
  export type { SessionStatus };
10
- export { isPendingToolCall };
11
- export type WaitingReason = "user" | "children";
12
-
13
- export interface SessionStatusInfo {
14
- status: SessionStatus;
15
- waitingReason?: WaitingReason;
16
- }
11
+ export { isPendingToolCall, getSessionStatusInfo };
12
+ export type { WaitingReason, SessionStatusInfo } from "../logic/sessionLogic";
17
13
 
18
14
  // Thresholds in milliseconds
19
15
  const WORKING_THRESHOLD = 30 * 1000; // 30 seconds
20
16
  const COMPLETED_THRESHOLD = 5 * 60 * 1000; // 5 minutes
21
- const GRACE_PERIOD = 5 * 1000; // 5 seconds
22
-
23
- export function getSessionStatus(
24
- messages: MessageMeta[],
25
- hasPendingToolCall: boolean = false,
26
- lastToolCompletedAt?: Date,
27
- workingChildCount?: number,
28
- lastAssistantFinished?: boolean,
29
- isSubagent: boolean = false
30
- ): SessionStatus {
31
- return getSessionStatusInfo(
32
- messages,
33
- hasPendingToolCall,
34
- lastToolCompletedAt,
35
- workingChildCount,
36
- lastAssistantFinished,
37
- isSubagent
38
- ).status;
39
- }
40
-
41
- export function getSessionStatusInfo(
42
- messages: MessageMeta[],
43
- hasPendingToolCall: boolean = false,
44
- lastToolCompletedAt?: Date,
45
- workingChildCount?: number,
46
- lastAssistantFinished?: boolean,
47
- isSubagent: boolean = false
48
- ): SessionStatusInfo {
49
- if (hasPendingToolCall) {
50
- return { status: "working" };
51
- }
52
-
53
- if (workingChildCount && workingChildCount > 0) {
54
- return { status: "waiting", waitingReason: "children" };
55
- }
56
-
57
- // Calculate time since last message early (needed for multiple checks)
58
- let timeSinceLastMessage = Infinity;
59
- if (messages && messages.length > 0) {
60
- const lastMessage = messages.reduce((latest, msg) =>
61
- msg.createdAt.getTime() > latest.createdAt.getTime() ? msg : latest
62
- );
63
- timeSinceLastMessage = Date.now() - lastMessage.createdAt.getTime();
64
- }
65
-
66
- // Assistant finished turn - only applies for RECENT sessions (< 5 min)
67
- // Old sessions (>= 5 min) fall through to time-based status instead of showing "waiting"
68
- if (lastAssistantFinished && timeSinceLastMessage < COMPLETED_THRESHOLD) {
69
- if (isSubagent) {
70
- return { status: "completed" };
71
- }
72
- return { status: "waiting", waitingReason: "user" };
73
- }
74
-
75
- // Grace period after tool completion
76
- if (lastToolCompletedAt) {
77
- const now = Date.now();
78
- const timeSinceToolCompleted = now - lastToolCompletedAt.getTime();
79
- if (timeSinceToolCompleted < GRACE_PERIOD) {
80
- return { status: "working" };
81
- }
82
- }
83
-
84
- // Time-based status from message timestamps
85
- if (!messages || messages.length === 0) {
86
- return { status: "completed" };
87
- }
88
-
89
- if (timeSinceLastMessage < WORKING_THRESHOLD) {
90
- return { status: "working" };
91
- } else if (timeSinceLastMessage < COMPLETED_THRESHOLD) {
92
- return { status: "idle" };
93
- } else {
94
- return { status: "completed" };
95
- }
96
- }
97
-
98
-
99
-
100
17
  /**
101
18
  * Get status from timestamp directly (for simpler cases)
102
19
  * Used when you only have the updatedAt timestamp
@@ -120,4 +37,3 @@ export function getStatusFromTimestamp(
120
37
  return "completed";
121
38
  }
122
39
  }
123
-
@@ -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,32 +1,39 @@
1
- /**
2
- * Watcher - File system watcher for OpenCode storage directories
3
- * Uses fs.watch() to detect changes and trigger cache invalidation
4
- */
5
-
6
- import { watch, type FSWatcher } from "node:fs";
7
- import { join } from "node:path";
1
+ import { existsSync, statSync, watch, type FSWatcher } from "node:fs";
2
+ import { basename, join } from "node:path";
8
3
  import { EventEmitter } from "node:events";
9
- import { getStoragePath } from "./storage/sessionParser";
4
+ import { homedir } from "node:os";
10
5
 
11
6
  export interface WatcherOptions {
7
+ dbPath?: string;
12
8
  storagePath?: string;
13
9
  projectPath?: string;
14
10
  debounceMs?: number;
15
11
  }
16
12
 
17
13
  export class Watcher extends EventEmitter {
18
- private watchers: FSWatcher[] = [];
14
+ private dbWatcher: FSWatcher | null = null;
15
+ private boulderWatcher: FSWatcher | null = null;
19
16
  private debounceTimer: Timer | null = null;
17
+ private rebindTimer: Timer | null = null;
20
18
  private readonly debounceMs: number;
21
- private readonly storagePath: string;
19
+ private readonly dbPath: string;
20
+ private readonly walPath: string;
21
+ private readonly boulderPath: string;
22
+ private readonly boulderDirPath: string;
22
23
  private readonly projectPath: string;
24
+ private dbWatcherTarget: string | null = null;
25
+ private boulderWatcherTarget: string | null = null;
26
+ private lastKnownBoulderSignature: string | null = null;
23
27
  private isRunning: boolean = false;
24
28
 
25
29
  constructor(options: WatcherOptions = {}) {
26
30
  super();
27
- this.storagePath = options.storagePath || getStoragePath();
31
+ this.dbPath = resolveDbPath(options);
32
+ this.walPath = `${this.dbPath}-wal`;
28
33
  this.projectPath = options.projectPath || process.cwd();
29
- this.debounceMs = options.debounceMs || 100;
34
+ this.boulderPath = join(this.projectPath, ".sisyphus", "boulder.json");
35
+ this.boulderDirPath = join(this.projectPath, ".sisyphus");
36
+ this.debounceMs = options.debounceMs ?? 100;
30
37
  }
31
38
 
32
39
  start(): void {
@@ -36,58 +43,10 @@ export class Watcher extends EventEmitter {
36
43
 
37
44
  this.isRunning = true;
38
45
 
39
- const sessionDir = join(this.storagePath, "opencode", "storage", "session");
40
- const messageDir = join(this.storagePath, "opencode", "storage", "message");
41
-
42
46
  try {
43
- const sessionWatcher = watch(
44
- sessionDir,
45
- { recursive: true },
46
- this.handleChange.bind(this)
47
- );
48
- this.watchers.push(sessionWatcher);
49
-
50
- const messageWatcher = watch(
51
- messageDir,
52
- { recursive: true },
53
- this.handleChange.bind(this)
54
- );
55
- this.watchers.push(messageWatcher);
56
-
57
- const partDir = join(this.storagePath, "opencode", "storage", "part");
58
- try {
59
- const partWatcher = watch(
60
- partDir,
61
- { recursive: true },
62
- this.handleChange.bind(this)
63
- );
64
- this.watchers.push(partWatcher);
65
- } catch {
66
- // part directory may not exist yet
67
- }
68
-
69
- const boulderDir = join(this.projectPath, ".sisyphus");
70
- try {
71
- const boulderWatcher = watch(
72
- boulderDir,
73
- { recursive: true },
74
- this.handleChange.bind(this)
75
- );
76
- this.watchers.push(boulderWatcher);
77
- } catch {
78
- // boulder directory may not exist yet
79
- }
80
-
81
- try {
82
- const projectWatcher = watch(
83
- this.projectPath,
84
- { recursive: true },
85
- this.handleProjectChange.bind(this)
86
- );
87
- this.watchers.push(projectWatcher);
88
- } catch {
89
- // project watcher may fail on unsupported environments
90
- }
47
+ this.rebindDbWatcher();
48
+ this.rebindBoulderWatcher();
49
+ this.startRebindLoop();
91
50
 
92
51
  this.emit("started");
93
52
  } catch (error) {
@@ -96,15 +55,7 @@ export class Watcher extends EventEmitter {
96
55
  }
97
56
  }
98
57
 
99
- private handleChange(eventType: string, filename: string | null): void {
100
- if (!filename) {
101
- return;
102
- }
103
-
104
- if (!filename.endsWith(".json")) {
105
- return;
106
- }
107
-
58
+ private emitDebouncedChange(eventType: string, filename: string): void {
108
59
  if (this.debounceTimer) {
109
60
  clearTimeout(this.debounceTimer);
110
61
  }
@@ -115,20 +66,174 @@ export class Watcher extends EventEmitter {
115
66
  }, this.debounceMs);
116
67
  }
117
68
 
118
- private handleProjectChange(eventType: string, filename: string | null): void {
119
- if (!filename) {
69
+ private getPreferredDbWatchTarget(): string {
70
+ if (existsSync(this.walPath)) {
71
+ return this.walPath;
72
+ }
73
+
74
+ return this.dbPath;
75
+ }
76
+
77
+ private rebindDbWatcher(): void {
78
+ const preferredTarget = this.getPreferredDbWatchTarget();
79
+ if (this.dbWatcher && this.dbWatcherTarget === preferredTarget) {
120
80
  return;
121
81
  }
122
82
 
123
- if (!filename.endsWith(".json")) {
83
+ if (this.dbWatcher) {
84
+ this.dbWatcher.close();
85
+ this.dbWatcher = null;
86
+ this.dbWatcherTarget = null;
87
+ }
88
+
89
+ if (!existsSync(preferredTarget)) {
124
90
  return;
125
91
  }
126
92
 
127
- if (!filename.includes(".sisyphus") && !filename.includes("boulder")) {
93
+ try {
94
+ this.dbWatcher = watch(preferredTarget, (eventType, filename) => {
95
+ this.handleDbEvent(eventType, filename);
96
+ });
97
+ this.dbWatcherTarget = preferredTarget;
98
+ } catch (error) {
99
+ const code = (error as NodeJS.ErrnoException).code;
100
+ if (code === "ENOENT") {
101
+ return;
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ private handleDbEvent(eventType: string, _filename: string | null): void {
108
+ const watchedTarget = this.dbWatcherTarget;
109
+ if (!watchedTarget) {
128
110
  return;
129
111
  }
130
112
 
131
- this.handleChange(eventType, filename);
113
+ this.emitDebouncedChange(eventType, basename(watchedTarget));
114
+
115
+ if (eventType === "rename" || !existsSync(watchedTarget)) {
116
+ this.rebindDbWatcherSafe();
117
+ return;
118
+ }
119
+
120
+ if (this.getPreferredDbWatchTarget() !== watchedTarget) {
121
+ this.rebindDbWatcherSafe();
122
+ }
123
+ }
124
+
125
+ private rebindBoulderWatcher(): void {
126
+ const preferredTarget = existsSync(this.boulderPath)
127
+ ? this.boulderPath
128
+ : this.boulderDirPath;
129
+
130
+ if (this.boulderWatcher && this.boulderWatcherTarget === preferredTarget) {
131
+ return;
132
+ }
133
+
134
+ if (this.boulderWatcher) {
135
+ this.boulderWatcher.close();
136
+ this.boulderWatcher = null;
137
+ this.boulderWatcherTarget = null;
138
+ }
139
+
140
+ if (!existsSync(preferredTarget)) {
141
+ return;
142
+ }
143
+
144
+ this.boulderWatcher = watch(preferredTarget, (eventType, filename) => {
145
+ this.handleBoulderEvent(eventType, filename);
146
+ });
147
+ this.boulderWatcherTarget = preferredTarget;
148
+ this.lastKnownBoulderSignature = this.getBoulderSignature();
149
+ }
150
+
151
+ private handleBoulderEvent(eventType: string, filename: string | null): void {
152
+ const currentSignature = this.getBoulderSignature();
153
+ const signatureChanged = currentSignature !== this.lastKnownBoulderSignature;
154
+
155
+ if (this.boulderWatcherTarget === this.boulderDirPath) {
156
+ if (filename && filename !== "boulder.json" && !signatureChanged) {
157
+ return;
158
+ }
159
+
160
+ if (!filename && !signatureChanged) {
161
+ return;
162
+ }
163
+ }
164
+
165
+ this.lastKnownBoulderSignature = currentSignature;
166
+ this.emitDebouncedChange(eventType, ".sisyphus/boulder.json");
167
+
168
+ if (eventType === "rename") {
169
+ this.rebindBoulderWatcherSafe();
170
+ return;
171
+ }
172
+
173
+ const preferredTarget = existsSync(this.boulderPath)
174
+ ? this.boulderPath
175
+ : this.boulderDirPath;
176
+ if (preferredTarget !== this.boulderWatcherTarget) {
177
+ this.rebindBoulderWatcherSafe();
178
+ }
179
+ }
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
+
207
+ private startRebindLoop(): void {
208
+ if (this.rebindTimer) {
209
+ clearInterval(this.rebindTimer);
210
+ }
211
+
212
+ this.rebindTimer = setInterval(() => {
213
+ if (!this.isRunning) {
214
+ return;
215
+ }
216
+
217
+ this.pollForBoulderChanges();
218
+ this.rebindDbWatcherSafe();
219
+ this.rebindBoulderWatcherSafe();
220
+ }, 100);
221
+ }
222
+
223
+ private rebindDbWatcherSafe(): void {
224
+ try {
225
+ this.rebindDbWatcher();
226
+ } catch (error) {
227
+ console.warn('[watcher] Failed to rebind DB watcher:', error instanceof Error ? error.message : error);
228
+ }
229
+ }
230
+
231
+ private rebindBoulderWatcherSafe(): void {
232
+ try {
233
+ this.rebindBoulderWatcher();
234
+ } catch (error) {
235
+ console.warn('[watcher] Failed to rebind boulder watcher:', error instanceof Error ? error.message : error);
236
+ }
132
237
  }
133
238
 
134
239
  stop(): void {
@@ -141,20 +246,58 @@ export class Watcher extends EventEmitter {
141
246
  this.debounceTimer = null;
142
247
  }
143
248
 
144
- for (const watcher of this.watchers) {
145
- watcher.close();
249
+ if (this.rebindTimer) {
250
+ clearInterval(this.rebindTimer);
251
+ this.rebindTimer = null;
252
+ }
253
+
254
+ if (this.dbWatcher) {
255
+ this.dbWatcher.close();
256
+ this.dbWatcher = null;
257
+ this.dbWatcherTarget = null;
258
+ }
259
+
260
+ if (this.boulderWatcher) {
261
+ this.boulderWatcher.close();
262
+ this.boulderWatcher = null;
263
+ this.boulderWatcherTarget = null;
146
264
  }
147
265
 
148
- this.watchers = [];
266
+ this.lastKnownBoulderSignature = null;
149
267
  this.isRunning = false;
150
268
  this.emit("stopped");
151
269
  }
152
270
 
271
+ close(): void {
272
+ this.stop();
273
+ }
274
+
153
275
  getIsRunning(): boolean {
154
276
  return this.isRunning;
155
277
  }
156
278
  }
157
279
 
158
- export function createWatcher(storagePath?: string, projectPath?: string): Watcher {
159
- return new Watcher({ storagePath, projectPath });
280
+ function resolveDbPath(options: WatcherOptions): string {
281
+ if (options.dbPath) {
282
+ return normalizeDbPathInput(options.dbPath);
283
+ }
284
+
285
+ if (options.storagePath) {
286
+ return join(options.storagePath, "opencode", "opencode.db");
287
+ }
288
+
289
+ const storageRoot = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
290
+ return join(storageRoot, "opencode", "opencode.db");
291
+ }
292
+
293
+ function normalizeDbPathInput(dbPathOrStoragePath: string): string {
294
+ if (dbPathOrStoragePath.endsWith(".db")) {
295
+ return dbPathOrStoragePath;
296
+ }
297
+
298
+ return join(dbPathOrStoragePath, "opencode", "opencode.db");
299
+ }
300
+
301
+ export function createWatcher(dbPath?: string, projectPath?: string): Watcher {
302
+ return new Watcher({ dbPath, projectPath });
160
303
  }
@@ -5,11 +5,9 @@
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;
10
+ /** Messages returned per session in API responses (client-facing limit) */
13
11
  export const MAX_MESSAGES_LIMIT = 100 as const;
14
12
 
15
13
  // Cache TTL
@@ -20,3 +18,10 @@ export const RINGBUFFER_CAPACITY = 1000 as const;
20
18
 
21
19
  // Session hierarchy depth limit
22
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';
@@ -21,6 +21,36 @@ export interface SessionMetadata {
21
21
  updatedAt: Date;
22
22
  }
23
23
 
24
+ /**
25
+ * SessionSummary is a lightweight session representation used in poll responses
26
+ * and as the summary field in SessionDetail
27
+ */
28
+ export interface SessionSummary {
29
+ id: string;
30
+ projectID: string;
31
+ title: string;
32
+ status?: SessionStatus;
33
+ activityType?: SessionActivityType;
34
+ currentAction?: string | null;
35
+ agent?: string | null;
36
+ modelID?: string | null;
37
+ providerID?: string | null;
38
+ updatedAt: Date;
39
+ createdAt: Date;
40
+ }
41
+
42
+ /**
43
+ * SessionDetail is the full detail response for a single session
44
+ * Contains summary, messages, activity tree, todos, and optional stats
45
+ */
46
+ export interface SessionDetail {
47
+ session: SessionSummary;
48
+ messages: MessageMeta[];
49
+ activity: ActivitySession[];
50
+ todos: TodoItem[];
51
+ stats?: SessionStats;
52
+ }
53
+
24
54
  /**
25
55
  * MessageMeta represents a message in a session
26
56
  */
@@ -44,11 +74,13 @@ export interface MessageMeta {
44
74
  * Includes agent info and hierarchy
45
75
  */
46
76
  export type SessionActivityType = "tool" | "reasoning" | "patch" | "waiting-tools" | "waiting-user" | "idle";
77
+ export type ActivityNodeKind = "session" | "phase";
47
78
 
48
79
  export interface ActivitySession {
49
80
  id: string;
50
81
  title: string;
51
82
  agent: string;
83
+ nodeKind?: ActivityNodeKind;
52
84
  modelID?: string;
53
85
  providerID?: string;
54
86
  parentID?: string;
@@ -96,17 +128,6 @@ export interface PartMeta {
96
128
  patchFiles?: string[];
97
129
  }
98
130
 
99
- /**
100
- * AgentInfo represents an active agent
101
- */
102
- export interface AgentInfo {
103
- name: string;
104
- mode: string;
105
- modelID: string;
106
- active: boolean;
107
- sessionID: string;
108
- }
109
-
110
131
  /**
111
132
  * ToolCall represents a tool invocation
112
133
  */
@@ -184,41 +205,6 @@ export type ActivityItem =
184
205
  | AgentSpawnActivity
185
206
  | AgentCompleteActivity;
186
207
 
187
- /**
188
- * BurstEntry represents a burst of tool call activity
189
- * Groups consecutive tool calls from the same agent with aggregated metrics
190
- */
191
- export interface BurstEntry {
192
- id: string;
193
- type: "burst";
194
- agentName: string;
195
- items: ToolCallActivity[];
196
- toolBreakdown: Record<string, number>;
197
- durationMs: number;
198
- firstTimestamp: Date;
199
- lastTimestamp: Date;
200
- pendingCount: number;
201
- errorCount: number;
202
- }
203
-
204
- /**
205
- * MilestoneEntry represents a significant event in the activity stream
206
- * (agent spawn or agent complete)
207
- */
208
- export interface MilestoneEntry {
209
- id: string;
210
- type: "milestone";
211
- item: AgentSpawnActivity | AgentCompleteActivity;
212
- }
213
-
214
- /**
215
- * StreamEntry is a union type for activity stream entries
216
- * Represents either a burst of tool calls or a milestone event
217
- */
218
- export type StreamEntry = BurstEntry | MilestoneEntry;
219
-
220
- export { synthesizeActivityItems } from '../utils/activityUtils';
221
-
222
208
  /**
223
209
  * PlanProgress represents progress on a plan
224
210
  */
@@ -229,6 +215,13 @@ export interface PlanProgress {
229
215
  tasks: Array<{ description: string; completed: boolean }>;
230
216
  }
231
217
 
218
+ export interface TodoItem {
219
+ content: string;
220
+ status: string;
221
+ priority: string;
222
+ position: number;
223
+ }
224
+
232
225
  /**
233
226
  * ModelTokens represents token usage for a specific model
234
227
  */
@@ -247,6 +240,13 @@ export interface SessionStats {
247
240
  modelBreakdown: ModelTokens[];
248
241
  }
249
242
 
243
+ export interface SessionActivityResponse {
244
+ session: SessionSummary;
245
+ activity: ActivitySession[];
246
+ stats: SessionStats | null;
247
+ revision: number;
248
+ }
249
+
250
250
  /**
251
251
  * Boulder represents the current plan state
252
252
  */
@@ -316,14 +316,9 @@ export interface AgentPhase {
316
316
  * Contains current session state, plan progress, and activity data
317
317
  */
318
318
  export interface PollResponse {
319
- sessions: SessionMetadata[];
320
- activeSession: SessionMetadata | null;
319
+ sessions: SessionSummary[];
320
+ activeSessionId: string | null;
321
321
  planProgress: PlanProgress | null;
322
322
  planName?: string;
323
- messages: MessageMeta[];
324
- activitySessions: ActivitySession[];
325
- sessionStats?: SessionStats;
326
323
  lastUpdate: number;
327
324
  }
328
-
329
- export { RingBuffer } from '../utils/RingBuffer';
@@ -12,8 +12,9 @@ export function synthesizeActivityItems(
12
12
  });
13
13
 
14
14
  sessions.forEach((session) => {
15
- if (session.parentID && sessionMap.has(session.parentID)) {
16
- const parent = sessionMap.get(session.parentID)!;
15
+ if (session.parentID) {
16
+ const parent = sessionMap.get(session.parentID);
17
+ if (!parent) return;
17
18
  items.push({
18
19
  id: `spawn-${session.id}`,
19
20
  type: "agent-spawn",