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.
@@ -9,6 +9,7 @@ import type {
9
9
  ActivitySession,
10
10
  PartMeta,
11
11
  TodoItem,
12
+ SessionActivityResponse,
12
13
  } from "../../shared/types";
13
14
  import { parseBoulder, calculatePlanProgress } from "../storage/boulderParser";
14
15
  import {
@@ -16,7 +17,11 @@ import {
16
17
  queryMaxTimestamp,
17
18
  queryMessages,
18
19
  queryParts,
20
+ queryMessagesForSessions,
21
+ queryPartsForSessions,
19
22
  querySessions,
23
+ querySessionSubtree,
24
+ querySessionSubtreeRevision,
20
25
  queryTodos,
21
26
  } from "../storage";
22
27
  import {
@@ -27,6 +32,7 @@ import {
27
32
  isAssistantFinished,
28
33
  } from "../logic";
29
34
  import { resolveProjectDirectory } from "../utils/projectResolver";
35
+ import { createSessionContext } from "./sessionContext";
30
36
  import { getSessionHierarchy } from "./sessionService";
31
37
  import { aggregateSessionStats } from "./statsService";
32
38
  import {
@@ -36,16 +42,14 @@ import {
36
42
  getLatestAssistantMessage,
37
43
  getMostRecentPendingPart,
38
44
  } from "./parsing";
45
+ import { selectRecentRootSessions } from "./recentSessions";
39
46
  import {
40
- TWENTY_FOUR_HOURS_MS,
41
47
  MAX_SESSIONS_LIMIT,
42
48
  MAX_MESSAGES_LIMIT,
43
49
  POLL_CACHE_TTL_MS,
50
+ SESSION_SCAN_LIMIT,
51
+ MESSAGE_SCAN_LIMIT,
44
52
  } from "../../shared/constants";
45
- /** Max sessions to scan when building incremental state (internal upper bound) */
46
- const SESSION_SCAN_LIMIT = 50_000;
47
- /** Max messages per session for incremental poll cache (high to avoid missing updates) */
48
- const MESSAGE_SCAN_LIMIT = 10_000;
49
53
 
50
54
  interface IncrementalPollState {
51
55
  sessionsById: Map<string, SessionMetadata>;
@@ -72,10 +76,27 @@ export function generateETag(data: PollResponse): string {
72
76
  return `"${hash.substring(0, 16)}"`;
73
77
  }
74
78
 
79
+ export function generateSessionActivityETag(data: SessionActivityResponse): string {
80
+ const hash = createHash("sha256")
81
+ .update(
82
+ JSON.stringify({
83
+ revision: data.revision,
84
+ session: data.session,
85
+ activity: data.activity,
86
+ stats: data.stats,
87
+ }),
88
+ )
89
+ .digest("hex");
90
+ return `"${hash.substring(0, 16)}"`;
91
+ }
92
+
75
93
  type PollCacheEntry = { data: PollResponse; etag: string; timestamp: number };
94
+ type SessionActivityCacheEntry = { data: SessionActivityResponse; etag: string };
76
95
 
77
96
  const pollCacheMap = new Map<string, PollCacheEntry>();
78
97
  const pollInProgressMap = new Map<string, Promise<PollResponse>>();
98
+ const sessionActivityCacheMap = new Map<string, SessionActivityCacheEntry>();
99
+ const sessionActivityInProgressMap = new Map<string, Promise<SessionActivityResponse>>();
79
100
  const incrementalPollStateMap = new Map<string, IncrementalPollState>();
80
101
  const MAX_INCREMENTAL_STATE_ENTRIES = 10;
81
102
  let pollCacheEpoch = 0;
@@ -84,6 +105,10 @@ function cacheKey(projectId?: string): string {
84
105
  return projectId ?? "";
85
106
  }
86
107
 
108
+ function sessionActivityCacheKey(sessionId: string): string {
109
+ return sessionId;
110
+ }
111
+
87
112
  export function getPollCache(projectId?: string): PollCacheEntry | null {
88
113
  return pollCacheMap.get(cacheKey(projectId)) ?? null;
89
114
  }
@@ -104,6 +129,7 @@ export function getPollCacheEpoch() {
104
129
  export function invalidatePollCache() {
105
130
  pollCacheEpoch += 1;
106
131
  pollCacheMap.clear();
132
+ sessionActivityCacheMap.clear();
107
133
  }
108
134
 
109
135
  export function getPollInProgress(projectId?: string): Promise<PollResponse> | null {
@@ -119,6 +145,32 @@ export function setPollInProgress(promise: Promise<PollResponse> | null, project
119
145
  }
120
146
  }
121
147
 
148
+ export function getSessionActivityCache(sessionId: string): SessionActivityCacheEntry | null {
149
+ return sessionActivityCacheMap.get(sessionActivityCacheKey(sessionId)) ?? null;
150
+ }
151
+
152
+ export function setSessionActivityCache(sessionId: string, cache: SessionActivityCacheEntry | null) {
153
+ const key = sessionActivityCacheKey(sessionId);
154
+ if (cache) {
155
+ sessionActivityCacheMap.set(key, cache);
156
+ } else {
157
+ sessionActivityCacheMap.delete(key);
158
+ }
159
+ }
160
+
161
+ export function getSessionActivityInProgress(sessionId: string): Promise<SessionActivityResponse> | null {
162
+ return sessionActivityInProgressMap.get(sessionActivityCacheKey(sessionId)) ?? null;
163
+ }
164
+
165
+ export function setSessionActivityInProgress(sessionId: string, promise: Promise<SessionActivityResponse> | null) {
166
+ const key = sessionActivityCacheKey(sessionId);
167
+ if (promise) {
168
+ sessionActivityInProgressMap.set(key, promise);
169
+ } else {
170
+ sessionActivityInProgressMap.delete(key);
171
+ }
172
+ }
173
+
122
174
  export function getPollCacheTTL() {
123
175
  return POLL_CACHE_TTL_MS;
124
176
  }
@@ -159,6 +211,38 @@ function sortMessagesDescending(messages: MessageMeta[]): MessageMeta[] {
159
211
  return [...messages].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
160
212
  }
161
213
 
214
+ function groupMessagesBySession(rows: ReturnType<typeof queryMessagesForSessions>): Map<string, MessageMeta[]> {
215
+ const grouped = new Map<string, MessageMeta[]>();
216
+
217
+ for (const row of rows) {
218
+ const parsed = toMessageMeta(row);
219
+ const existing = grouped.get(parsed.sessionID);
220
+ if (existing) {
221
+ existing.push(parsed);
222
+ } else {
223
+ grouped.set(parsed.sessionID, [parsed]);
224
+ }
225
+ }
226
+
227
+ return grouped;
228
+ }
229
+
230
+ function groupPartsBySession(rows: ReturnType<typeof queryPartsForSessions>): Map<string, PartMeta[]> {
231
+ const grouped = new Map<string, PartMeta[]>();
232
+
233
+ for (const row of rows) {
234
+ const parsed = toPartMeta(row);
235
+ const existing = grouped.get(parsed.sessionID);
236
+ if (existing) {
237
+ existing.push(parsed);
238
+ } else {
239
+ grouped.set(parsed.sessionID, [parsed]);
240
+ }
241
+ }
242
+
243
+ return grouped;
244
+ }
245
+
162
246
  function loadMessagesForSession(state: IncrementalPollState, sessionId: string, forceRefresh = false): MessageMeta[] {
163
247
  if (!forceRefresh) {
164
248
  const cached = state.messagesBySessionId.get(sessionId);
@@ -229,7 +313,8 @@ function updateIncrementalState(state: IncrementalPollState, projectId?: string)
229
313
  }
230
314
  }
231
315
 
232
- if (changedSessionIds.size > 0) {
316
+ const wasFirstLoad = !hasBaseline || refreshAllSessions;
317
+ if (!wasFirstLoad && changedSessionIds.size > 0) {
233
318
  for (const sessionId of changedSessionIds) {
234
319
  loadMessagesForSession(state, sessionId, true);
235
320
  loadPartsForSession(state, sessionId, true);
@@ -262,12 +347,8 @@ export async function fetchPollData(projectId?: string): Promise<PollResponse> {
262
347
  }
263
348
 
264
349
  const now = Date.now();
265
- const twentyFourHoursAgo = now - TWENTY_FOUR_HOURS_MS;
266
350
 
267
- const recentSessions = scopedSessions.filter((session) => session.updatedAt.getTime() >= twentyFourHoursAgo);
268
- const sortedSessions = recentSessions.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
269
- const limitedSessions = sortedSessions.slice(0, MAX_SESSIONS_LIMIT);
270
- const rootSessions = limitedSessions.filter((session) => !session.parentID);
351
+ const rootSessions = selectRecentRootSessions(scopedSessions, MAX_SESSIONS_LIMIT);
271
352
 
272
353
  function getCachedMessages(id: string): MessageMeta[] {
273
354
  const cached = state.messagesBySessionId.get(id);
@@ -348,7 +429,7 @@ export async function fetchPollData(projectId?: string): Promise<PollResponse> {
348
429
  planName = boulder.planName || undefined;
349
430
  }
350
431
  } catch (err) {
351
- console.debug("Failed to parse boulder.json:", err instanceof Error ? err.message : err);
432
+ console.warn("Failed to parse boulder.json:", err instanceof Error ? err.message : err);
352
433
  }
353
434
  }
354
435
 
@@ -375,44 +456,36 @@ export async function fetchPollData(projectId?: string): Promise<PollResponse> {
375
456
  };
376
457
  }
377
458
 
378
- export async function fetchSessionDetail(sessionId: string): Promise<SessionDetail> {
379
- // Get all sessions for hierarchy context
380
- const allSessions = querySessions(undefined, undefined, SESSION_SCAN_LIMIT).map(toSessionMetadata);
381
-
382
- // Get messages for this session
383
- const messages = queryMessages(sessionId, MAX_MESSAGES_LIMIT).map(toMessageMeta);
384
-
385
- // Get activity tree (hierarchy)
386
- const activity: ActivitySession[] = await getSessionHierarchy(sessionId, allSessions);
387
-
388
- // Build messages cache for stats
389
- const messagesCache = new Map<string, MessageMeta[]>();
390
- messagesCache.set(sessionId, messages);
391
- for (const activitySession of activity) {
392
- if (!messagesCache.has(activitySession.id) && !activitySession.id.includes("-phase-")) {
393
- const childMessages = queryMessages(activitySession.id, MAX_MESSAGES_LIMIT).map(toMessageMeta);
394
- messagesCache.set(activitySession.id, childMessages);
459
+ async function buildSessionActivityResponse(sessionId: string): Promise<{
460
+ graph: SessionActivityResponse;
461
+ messagesBySession: Map<string, MessageMeta[]>;
462
+ }> {
463
+ const allSessions = querySessionSubtree(sessionId).map(toSessionMetadata);
464
+ const sessionIds = allSessions.map((session) => session.id);
465
+ const messagesBySession = groupMessagesBySession(
466
+ queryMessagesForSessions(sessionIds, MESSAGE_SCAN_LIMIT),
467
+ );
468
+ const partsBySession = groupPartsBySession(queryPartsForSessions(sessionIds));
469
+ const context = createSessionContext(allSessions, {
470
+ messagesBySession,
471
+ partsBySession,
472
+ });
473
+
474
+ for (const session of allSessions) {
475
+ if (!messagesBySession.has(session.id)) {
476
+ messagesBySession.set(session.id, []);
477
+ }
478
+ if (!partsBySession.has(session.id)) {
479
+ partsBySession.set(session.id, []);
395
480
  }
396
481
  }
397
482
 
398
- // Get todos
399
- const todos: TodoItem[] = queryTodos(sessionId).map((row) => ({
400
- content: row.content,
401
- status: row.status,
402
- priority: row.priority,
403
- position: row.position,
404
- }));
405
-
406
- // Compute stats
407
- const stats = aggregateSessionStats(activity, messagesCache);
408
-
409
- // Find the session metadata
410
- const sessionMeta = allSessions.find((s) => s.id === sessionId);
411
-
412
- // Find the root activity entry for derived status (direct match or last phase)
483
+ const activity: ActivitySession[] = await getSessionHierarchy(sessionId, allSessions, context);
484
+ const stats = aggregateSessionStats(activity, messagesBySession);
485
+ const sessionMeta = allSessions.find((session) => session.id === sessionId);
413
486
  const rootActivity =
414
- activity.find((a) => a.id === sessionId) ??
415
- activity.filter((a) => a.id.startsWith(`${sessionId}-phase-`)).pop();
487
+ activity.find((activitySession) => activitySession.id === sessionId) ??
488
+ activity.filter((activitySession) => activitySession.id.startsWith(`${sessionId}-phase-`)).pop();
416
489
 
417
490
  const session: SessionSummary = {
418
491
  id: sessionId,
@@ -428,5 +501,38 @@ export async function fetchSessionDetail(sessionId: string): Promise<SessionDeta
428
501
  createdAt: sessionMeta?.createdAt ?? new Date(),
429
502
  };
430
503
 
431
- return { session, messages, activity, todos, stats };
504
+ return {
505
+ graph: {
506
+ session,
507
+ activity,
508
+ stats,
509
+ revision: querySessionSubtreeRevision(sessionId),
510
+ },
511
+ messagesBySession,
512
+ };
513
+ }
514
+
515
+ export async function fetchSessionActivity(sessionId: string): Promise<SessionActivityResponse> {
516
+ return (await buildSessionActivityResponse(sessionId)).graph;
517
+ }
518
+
519
+ export async function fetchSessionDetail(sessionId: string): Promise<SessionDetail> {
520
+ const { graph, messagesBySession } = await buildSessionActivityResponse(sessionId);
521
+
522
+ const todos: TodoItem[] = queryTodos(sessionId).map((row) => ({
523
+ content: row.content,
524
+ status: row.status,
525
+ priority: row.priority,
526
+ position: row.position,
527
+ }));
528
+
529
+ const messages = (messagesBySession.get(sessionId) ?? []).slice(0, MAX_MESSAGES_LIMIT);
530
+
531
+ return {
532
+ session: graph.session,
533
+ messages,
534
+ activity: graph.activity,
535
+ todos,
536
+ stats: graph.stats ?? undefined,
537
+ };
432
538
  }
@@ -0,0 +1,14 @@
1
+ interface HasRootSessionFields {
2
+ parentID?: string | null;
3
+ updatedAt: Date;
4
+ }
5
+
6
+ export function selectRecentRootSessions<T extends HasRootSessionFields>(
7
+ sessions: readonly T[],
8
+ limit: number,
9
+ ): T[] {
10
+ return sessions
11
+ .filter((session) => !session.parentID)
12
+ .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
13
+ .slice(0, limit);
14
+ }
@@ -0,0 +1,97 @@
1
+ import type { SessionMetadata, MessageMeta, PartMeta } from "../../shared/types";
2
+ import { MESSAGE_SCAN_LIMIT } from "../../shared/constants";
3
+ import {
4
+ queryMessages,
5
+ queryParts,
6
+ } from "../storage/queries";
7
+ import {
8
+ toMessageMeta as parseMessageRow,
9
+ toPartMeta as parsePartRow,
10
+ } from "./parsing";
11
+
12
+ export interface SessionContext {
13
+ allowedSessionIds: Set<string>;
14
+ sessionById: Map<string, SessionMetadata>;
15
+ messagesBySession: Map<string, MessageMeta[]>;
16
+ partsBySession: Map<string, PartMeta[]>;
17
+ childrenBySession: Map<string, SessionMetadata[]>;
18
+ }
19
+
20
+ interface SessionContextSeed {
21
+ messagesBySession?: Map<string, MessageMeta[]>;
22
+ partsBySession?: Map<string, PartMeta[]>;
23
+ }
24
+
25
+ export function createSessionContext(allSessions: SessionMetadata[], seed: SessionContextSeed = {}): SessionContext {
26
+ const sessionById = new Map<string, SessionMetadata>();
27
+ const childrenBySession = new Map<string, SessionMetadata[]>();
28
+ for (const session of allSessions) {
29
+ sessionById.set(session.id, session);
30
+ if (!session.parentID) {
31
+ continue;
32
+ }
33
+
34
+ const siblings = childrenBySession.get(session.parentID);
35
+ if (siblings) {
36
+ siblings.push(session);
37
+ } else {
38
+ childrenBySession.set(session.parentID, [session]);
39
+ }
40
+ }
41
+
42
+ for (const children of childrenBySession.values()) {
43
+ children.sort((a, b) => {
44
+ const createdDiff = a.createdAt.getTime() - b.createdAt.getTime();
45
+ if (createdDiff !== 0) {
46
+ return createdDiff;
47
+ }
48
+
49
+ return a.id.localeCompare(b.id);
50
+ });
51
+ }
52
+
53
+ return {
54
+ allowedSessionIds: new Set(allSessions.map((session) => session.id)),
55
+ sessionById,
56
+ messagesBySession: new Map<string, MessageMeta[]>(seed.messagesBySession),
57
+ partsBySession: new Map<string, PartMeta[]>(seed.partsBySession),
58
+ childrenBySession,
59
+ };
60
+ }
61
+
62
+ export function getSessionFromContext(sessionId: string, context: SessionContext): SessionMetadata | undefined {
63
+ return context.sessionById.get(sessionId);
64
+ }
65
+
66
+ export function getSessionMessages(sessionId: string, context: SessionContext): MessageMeta[] {
67
+ const cached = context.messagesBySession.get(sessionId);
68
+ if (cached) {
69
+ return cached;
70
+ }
71
+
72
+ const messages = queryMessages(sessionId, MESSAGE_SCAN_LIMIT).map(parseMessageRow);
73
+ context.messagesBySession.set(sessionId, messages);
74
+ return messages;
75
+ }
76
+
77
+ export function getSessionParts(sessionId: string, context: SessionContext): PartMeta[] {
78
+ const cached = context.partsBySession.get(sessionId);
79
+ if (cached) {
80
+ return cached;
81
+ }
82
+
83
+ const parts = queryParts(sessionId).map(parsePartRow);
84
+ context.partsBySession.set(sessionId, parts);
85
+ return parts;
86
+ }
87
+
88
+ export function getSessionChildren(sessionId: string, context: SessionContext): SessionMetadata[] {
89
+ const cached = context.childrenBySession.get(sessionId);
90
+ if (cached) {
91
+ return cached;
92
+ }
93
+
94
+ const children: SessionMetadata[] = [];
95
+ context.childrenBySession.set(sessionId, children);
96
+ return children;
97
+ }
@@ -1,10 +1,6 @@
1
1
  import type {
2
2
  SessionMetadata,
3
- MessageMeta,
4
3
  ActivitySession,
5
- TreeNode,
6
- TreeEdge,
7
- SessionTree,
8
4
  PartMeta,
9
5
  SessionStatus,
10
6
  ToolCallSummary,
@@ -21,95 +17,22 @@ import {
21
17
  getSessionStatusInfo,
22
18
  type SessionStatusInfo,
23
19
  } from "../logic";
24
- import {
25
- querySessionChildren,
26
- queryMessages,
27
- queryParts,
28
- } from "../storage/queries";
29
20
  import { getStatusFromTimestamp } from "../utils/sessionStatus";
30
21
  import {
31
- toSessionMetadata as parseSessionRow,
32
- toMessageMeta as parseMessageRow,
33
- toPartMeta as parsePartRow,
34
22
  getLatestAssistantMessage,
35
23
  getMostRecentPendingPart,
36
24
  } from "./parsing";
25
+ import {
26
+ type SessionContext,
27
+ createSessionContext,
28
+ getSessionFromContext,
29
+ getSessionMessages,
30
+ getSessionParts,
31
+ getSessionChildren,
32
+ } from "./sessionContext";
37
33
 
38
34
  export { detectAgentPhases, isAssistantFinished };
39
35
 
40
- /** Max messages to load per session for hierarchy building (effectively unlimited) */
41
- const MAX_MESSAGE_QUERY_LIMIT = 100_000;
42
-
43
- interface SessionContext {
44
- allowedSessionIds: Set<string>;
45
- sessionById: Map<string, SessionMetadata>;
46
- messagesBySession: Map<string, MessageMeta[]>;
47
- partsBySession: Map<string, PartMeta[]>;
48
- childrenBySession: Map<string, SessionMetadata[]>;
49
- }
50
-
51
-
52
- function createSessionContext(allSessions: SessionMetadata[]): SessionContext {
53
- const sessionById = new Map<string, SessionMetadata>();
54
- for (const session of allSessions) {
55
- sessionById.set(session.id, session);
56
- }
57
-
58
- return {
59
- allowedSessionIds: new Set(allSessions.map((session) => session.id)),
60
- sessionById,
61
- messagesBySession: new Map<string, MessageMeta[]>(),
62
- partsBySession: new Map<string, PartMeta[]>(),
63
- childrenBySession: new Map<string, SessionMetadata[]>(),
64
- };
65
- }
66
-
67
- function getSessionFromContext(sessionId: string, context: SessionContext): SessionMetadata | undefined {
68
- return context.sessionById.get(sessionId);
69
- }
70
-
71
- function getSessionMessages(sessionId: string, context: SessionContext): MessageMeta[] {
72
- const cached = context.messagesBySession.get(sessionId);
73
- if (cached) {
74
- return cached;
75
- }
76
-
77
- const messages = queryMessages(sessionId, MAX_MESSAGE_QUERY_LIMIT).map(parseMessageRow);
78
- context.messagesBySession.set(sessionId, messages);
79
- return messages;
80
- }
81
-
82
- function getSessionParts(sessionId: string, context: SessionContext): PartMeta[] {
83
- const cached = context.partsBySession.get(sessionId);
84
- if (cached) {
85
- return cached;
86
- }
87
-
88
- const parts = queryParts(sessionId).map(parsePartRow);
89
- context.partsBySession.set(sessionId, parts);
90
- return parts;
91
- }
92
-
93
- function getSessionChildren(sessionId: string, context: SessionContext): SessionMetadata[] {
94
- const cached = context.childrenBySession.get(sessionId);
95
- if (cached) {
96
- return cached;
97
- }
98
-
99
- const children = querySessionChildren(sessionId)
100
- .map(parseSessionRow)
101
- .filter((child) => context.allowedSessionIds.has(child.id));
102
-
103
- for (const child of children) {
104
- if (!context.sessionById.has(child.id)) {
105
- context.sessionById.set(child.id, child);
106
- }
107
- }
108
-
109
- context.childrenBySession.set(sessionId, children);
110
- return children;
111
- }
112
-
113
36
 
114
37
  function countBlockingChildren(statuses: SessionStatus[]): number {
115
38
  return statuses.filter((status) => status === "working" || status === "waiting").length;
@@ -148,108 +71,14 @@ function buildToolCalls(parts: PartMeta[], messageAgent: Map<string, string>): T
148
71
  return toolCalls.slice(0, 50);
149
72
  }
150
73
 
151
- export function buildAgentHierarchy(messages: MessageMeta[]): Record<string, string[]> {
152
- const hierarchy: Record<string, string[]> = {};
153
-
154
- for (const msg of messages) {
155
- if (msg.agent && msg.parentID) {
156
- const parentMsg = messages.find((m) => m.id === msg.parentID);
157
- if (parentMsg?.agent) {
158
- if (!hierarchy[parentMsg.agent]) {
159
- hierarchy[parentMsg.agent] = [];
160
- }
161
- if (!hierarchy[parentMsg.agent].includes(msg.agent)) {
162
- hierarchy[parentMsg.agent].push(msg.agent);
163
- }
164
- }
165
- }
166
- }
167
-
168
- return hierarchy;
169
- }
170
-
171
- export async function buildSessionTree(
172
- rootSessionID: string,
173
- allSessions: SessionMetadata[]
174
- ): Promise<SessionTree> {
175
- const nodes: TreeNode[] = [];
176
- const edges: TreeEdge[] = [];
177
- const visited = new Set<string>();
178
- const context = createSessionContext(allSessions);
179
-
180
- async function processSession(sessionID: string, depth = 0) {
181
- if (depth > MAX_RECURSION_DEPTH) {
182
- console.warn(`Max recursion depth reached for session ${sessionID}`);
183
- return;
184
- }
185
- if (visited.has(sessionID)) {
186
- return;
187
- }
188
- visited.add(sessionID);
189
-
190
- const session = getSessionFromContext(sessionID, context);
191
- if (!session) {
192
- return;
193
- }
194
-
195
- const messages = getSessionMessages(sessionID, context);
196
- const lastAssistantFinished = isAssistantFinished(messages);
197
- const isSubagent = !!session.parentID;
198
- const status = getSessionStatusInfo(
199
- messages,
200
- false,
201
- undefined,
202
- undefined,
203
- lastAssistantFinished,
204
- isSubagent
205
- ).status;
206
-
207
- const lastMessage = messages.sort(
208
- (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
209
- )[0];
210
-
211
- nodes.push({
212
- id: session.id,
213
- data: {
214
- title: session.title,
215
- agent: lastMessage?.agent,
216
- model: lastMessage?.modelID,
217
- isActive: status === "working" || status === "idle",
218
- },
219
- });
220
-
221
- if (session.parentID) {
222
- edges.push({
223
- source: session.parentID,
224
- target: session.id,
225
- });
226
- await processSession(session.parentID, depth + 1);
227
- }
228
-
229
- const children = getSessionChildren(sessionID, context);
230
- await Promise.all(
231
- children.map((child) => {
232
- edges.push({
233
- source: sessionID,
234
- target: child.id,
235
- });
236
- return processSession(child.id, depth + 1);
237
- })
238
- );
239
- }
240
-
241
- await processSession(rootSessionID, 0);
242
-
243
- return { nodes, edges };
244
- }
245
-
246
74
  export async function getSessionHierarchy(
247
75
  rootSessionId: string,
248
- allSessions: SessionMetadata[]
76
+ allSessions: SessionMetadata[],
77
+ contextArg?: SessionContext,
249
78
  ): Promise<ActivitySession[]> {
250
79
  const result: ActivitySession[] = [];
251
80
  const processed = new Set<string>();
252
- const context = createSessionContext(allSessions);
81
+ const context = contextArg ?? createSessionContext(allSessions);
253
82
 
254
83
  const rootSession = getSessionFromContext(rootSessionId, context);
255
84
  if (!rootSession) return result;
@@ -320,6 +149,7 @@ export async function getSessionHierarchy(
320
149
  id: rootSession.id,
321
150
  title: rootSession.title,
322
151
  agent: latestAssistantMsg?.agent || "unknown",
152
+ nodeKind: "session",
323
153
  modelID: latestAssistantMsg?.modelID,
324
154
  providerID: latestAssistantMsg?.providerID,
325
155
  parentID: rootSession.parentID,
@@ -419,6 +249,7 @@ export async function getSessionHierarchy(
419
249
  id: virtualId,
420
250
  title: rootSession.title,
421
251
  agent: phase.agent,
252
+ nodeKind: "phase",
422
253
  modelID: latestPhaseMsg?.modelID,
423
254
  providerID: latestPhaseMsg?.providerID,
424
255
  parentID: undefined,
@@ -442,7 +273,7 @@ export async function getSessionHierarchy(
442
273
  return result;
443
274
  }
444
275
 
445
- export async function processChildSession(
276
+ async function processChildSession(
446
277
  sessionId: string,
447
278
  parentId: string,
448
279
  allSessions: SessionMetadata[],
@@ -526,6 +357,7 @@ export async function processChildSession(
526
357
  id: session.id,
527
358
  title: session.title,
528
359
  agent: latestAssistantMsg?.agent || "unknown",
360
+ nodeKind: "session",
529
361
  modelID: latestAssistantMsg?.modelID,
530
362
  providerID: latestAssistantMsg?.providerID,
531
363
  parentID: parentId,