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
@@ -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,187 +1,92 @@
1
- import type {
2
- SessionMetadata,
3
- MessageMeta,
4
- ActivitySession,
5
- TreeNode,
6
- TreeEdge,
7
- SessionTree,
8
- AgentPhase,
1
+ import type {
2
+ SessionMetadata,
3
+ ActivitySession,
9
4
  PartMeta,
10
5
  SessionStatus,
6
+ ToolCallSummary,
11
7
  } from "../../shared/types";
12
8
  import { MAX_RECURSION_DEPTH } from "../../shared/constants";
13
- import { listMessages } from "../storage/messageParser";
14
- import {
15
- getPartsForSession,
9
+ import {
10
+ formatCurrentAction,
11
+ isPendingToolCall,
12
+ generateActivityMessage,
16
13
  getSessionActivityState,
17
- isPendingToolCall,
18
- getToolCallsForSession,
19
- generateActivityMessage
20
- } from "../storage/partParser";
21
- import { getSessionStatus, getSessionStatusInfo, getStatusFromTimestamp, type SessionStatusInfo } from "../utils/sessionStatus";
22
- import { deriveActivityType } from "../storage/partParser";
23
-
24
- export function isAssistantFinished(messages: MessageMeta[]): boolean {
25
- if (messages.length === 0) {
26
- return false;
27
- }
28
-
29
- const lastMessage = messages.reduce((latest, current) =>
30
- current.createdAt.getTime() > latest.createdAt.getTime() ? current : latest
31
- );
32
-
33
- return lastMessage.role === "assistant" && lastMessage.finish === "stop";
34
- }
14
+ deriveActivityType,
15
+ detectAgentPhases,
16
+ isAssistantFinished,
17
+ getSessionStatusInfo,
18
+ type SessionStatusInfo,
19
+ } from "../logic";
20
+ import { getStatusFromTimestamp } from "../utils/sessionStatus";
21
+ import {
22
+ getLatestAssistantMessage,
23
+ getMostRecentPendingPart,
24
+ } from "./parsing";
25
+ import {
26
+ type SessionContext,
27
+ createSessionContext,
28
+ getSessionFromContext,
29
+ getSessionMessages,
30
+ getSessionParts,
31
+ getSessionChildren,
32
+ } from "./sessionContext";
33
+
34
+ export { detectAgentPhases, isAssistantFinished };
35
35
 
36
- function getLatestAssistantMessage(messages: MessageMeta[]): MessageMeta | undefined {
37
- return messages
38
- .filter((message) => message.role === "assistant")
39
- .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
40
- }
41
-
42
- function getMostRecentPendingPart(parts: PartMeta[]): PartMeta | undefined {
43
- return parts
44
- .filter((part) => isPendingToolCall(part))
45
- .sort((a, b) => (b.startedAt?.getTime() || 0) - (a.startedAt?.getTime() || 0))[0];
46
- }
47
36
 
48
37
  function countBlockingChildren(statuses: SessionStatus[]): number {
49
38
  return statuses.filter((status) => status === "working" || status === "waiting").length;
50
39
  }
51
40
 
52
- export function detectAgentPhases(messages: MessageMeta[]): AgentPhase[] {
53
- const sorted = messages
54
- .filter(m => m.role === 'assistant' && m.agent)
55
- .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
56
-
57
- if (sorted.length === 0) return [];
58
-
59
- const phases: AgentPhase[] = [];
60
- let currentPhase: AgentPhase | null = null;
61
-
62
- for (const msg of sorted) {
63
- if (!currentPhase || currentPhase.agent !== msg.agent) {
64
- if (currentPhase) phases.push(currentPhase);
65
- currentPhase = {
66
- agent: msg.agent!,
67
- startTime: msg.createdAt,
68
- endTime: msg.createdAt,
69
- tokens: msg.tokens || 0,
70
- messageCount: 1,
71
- };
72
- } else {
73
- currentPhase.endTime = msg.createdAt;
74
- currentPhase.tokens += msg.tokens || 0;
75
- currentPhase.messageCount++;
76
- }
77
- }
78
- if (currentPhase) phases.push(currentPhase);
79
-
80
- return phases;
81
- }
82
-
83
- export function buildAgentHierarchy(messages: MessageMeta[]): Record<string, string[]> {
84
- const hierarchy: Record<string, string[]> = {};
85
41
 
86
- for (const msg of messages) {
87
- if (msg.agent && msg.parentID) {
88
- const parentMsg = messages.find((m) => m.id === msg.parentID);
89
- if (parentMsg?.agent) {
90
- if (!hierarchy[parentMsg.agent]) {
91
- hierarchy[parentMsg.agent] = [];
92
- }
93
- if (!hierarchy[parentMsg.agent].includes(msg.agent)) {
94
- hierarchy[parentMsg.agent].push(msg.agent);
95
- }
42
+ function buildToolCalls(parts: PartMeta[], messageAgent: Map<string, string>): ToolCallSummary[] {
43
+ const toolCalls = parts
44
+ .filter((part) => part.type === "tool" && part.tool)
45
+ .map((part): ToolCallSummary => {
46
+ let state: "pending" | "complete" | "error" = "complete";
47
+ if (isPendingToolCall(part)) {
48
+ state = "pending";
49
+ } else if (part.state === "error" || part.state === "failed") {
50
+ state = "error";
96
51
  }
97
- }
98
- }
99
-
100
- return hierarchy;
101
- }
102
52
 
103
- export async function buildSessionTree(
104
- rootSessionID: string,
105
- allSessions: SessionMetadata[]
106
- ): Promise<SessionTree> {
107
- const nodes: TreeNode[] = [];
108
- const edges: TreeEdge[] = [];
109
- const visited = new Set<string>();
110
-
111
- async function processSession(sessionID: string, depth = 0) {
112
- if (depth > MAX_RECURSION_DEPTH) {
113
- console.warn(`Max recursion depth reached for session ${sessionID}`);
114
- return;
115
- }
116
- if (visited.has(sessionID)) {
117
- return;
118
- }
119
- visited.add(sessionID);
120
-
121
- const session = allSessions.find((s) => s.id === sessionID);
122
- if (!session) {
123
- return;
124
- }
125
-
126
- const messages = await listMessages(sessionID);
127
- const lastAssistantFinished = isAssistantFinished(messages);
128
- const isSubagent = !!session.parentID;
129
- const status = getSessionStatus(messages, false, undefined, undefined, lastAssistantFinished, isSubagent);
130
-
131
- const lastMessage = messages.sort(
132
- (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
133
- )[0];
134
-
135
- nodes.push({
136
- id: session.id,
137
- data: {
138
- title: session.title,
139
- agent: lastMessage?.agent,
140
- model: lastMessage?.modelID,
141
- isActive: status === "working" || status === "idle",
142
- },
53
+ return {
54
+ id: part.id,
55
+ name: part.tool || "unknown",
56
+ state,
57
+ summary: formatCurrentAction(part) || part.tool || "Unknown tool",
58
+ input: part.input || {},
59
+ error: part.error,
60
+ timestamp: (part.completedAt || part.startedAt)?.toISOString() || "",
61
+ agentName: messageAgent.get(part.messageID) || "unknown",
62
+ };
143
63
  });
144
64
 
145
- if (session.parentID) {
146
- edges.push({
147
- source: session.parentID,
148
- target: session.id,
149
- });
150
- await processSession(session.parentID, depth + 1);
151
- }
152
-
153
- const children = allSessions.filter((s) => s.parentID === sessionID);
154
- await Promise.all(
155
- children.map((child) => {
156
- edges.push({
157
- source: sessionID,
158
- target: child.id,
159
- });
160
- return processSession(child.id, depth + 1);
161
- })
162
- );
163
- }
164
-
165
- await processSession(rootSessionID, 0);
65
+ toolCalls.sort((a, b) => {
66
+ const timeA = new Date(a.timestamp).getTime();
67
+ const timeB = new Date(b.timestamp).getTime();
68
+ return timeB - timeA;
69
+ });
166
70
 
167
- return { nodes, edges };
71
+ return toolCalls.slice(0, 50);
168
72
  }
169
73
 
170
74
  export async function getSessionHierarchy(
171
75
  rootSessionId: string,
172
- allSessions: SessionMetadata[]
76
+ allSessions: SessionMetadata[],
77
+ contextArg?: SessionContext,
173
78
  ): Promise<ActivitySession[]> {
174
79
  const result: ActivitySession[] = [];
175
80
  const processed = new Set<string>();
81
+ const context = contextArg ?? createSessionContext(allSessions);
176
82
 
177
- const rootSession = allSessions.find((s) => s.id === rootSessionId);
83
+ const rootSession = getSessionFromContext(rootSessionId, context);
178
84
  if (!rootSession) return result;
179
85
 
180
- const rootMessages = await listMessages(rootSessionId);
86
+ const rootMessages = getSessionMessages(rootSessionId, context);
181
87
  const phases = detectAgentPhases(rootMessages);
182
- const childSessions = allSessions.filter((s) => s.parentID === rootSessionId);
88
+ const childSessions = getSessionChildren(rootSessionId, context);
183
89
 
184
- // Build messageAgent map for tool calls
185
90
  const messageAgent = new Map<string, string>();
186
91
  for (const msg of rootMessages) {
187
92
  if (msg.agent) {
@@ -191,30 +96,28 @@ export async function getSessionHierarchy(
191
96
 
192
97
  if (phases.length <= 1) {
193
98
  const latestAssistantMsg = getLatestAssistantMessage(rootMessages);
194
-
99
+
195
100
  const totalTokens = rootMessages
196
101
  .filter((m) => m.tokens !== undefined)
197
102
  .reduce((sum, m) => sum + (m.tokens || 0), 0);
198
103
 
199
- const parts = await getPartsForSession(rootSessionId);
104
+ const parts = getSessionParts(rootSessionId, context);
200
105
  const activityState = getSessionActivityState(parts);
201
-
106
+
202
107
  const childStatuses = await Promise.all(
203
108
  childSessions.map(async (child) => {
204
- const [childMessages, childParts] = await Promise.all([
205
- listMessages(child.id),
206
- getPartsForSession(child.id),
207
- ]);
109
+ const childMessages = getSessionMessages(child.id, context);
110
+ const childParts = getSessionParts(child.id, context);
208
111
  const childActivityState = getSessionActivityState(childParts);
209
112
  const childLastAssistantFinished = isAssistantFinished(childMessages);
210
- return getSessionStatus(
113
+ return getSessionStatusInfo(
211
114
  childMessages,
212
115
  childActivityState.hasPendingToolCall,
213
116
  childActivityState.lastToolCompletedAt || undefined,
214
117
  undefined,
215
118
  childLastAssistantFinished,
216
119
  true
217
- );
120
+ ).status;
218
121
  })
219
122
  );
220
123
  const workingChildCount = countBlockingChildren(childStatuses);
@@ -240,12 +143,13 @@ export async function getSessionHierarchy(
240
143
  );
241
144
  const activityType = deriveActivityType(activityState, rootLastAssistantFinished, false, status, statusInfo.waitingReason);
242
145
 
243
- const toolCalls = await getToolCallsForSession(rootSessionId, messageAgent, undefined, parts);
146
+ const toolCalls = buildToolCalls(parts, messageAgent);
244
147
 
245
148
  result.push({
246
149
  id: rootSession.id,
247
150
  title: rootSession.title,
248
151
  agent: latestAssistantMsg?.agent || "unknown",
152
+ nodeKind: "session",
249
153
  modelID: latestAssistantMsg?.modelID,
250
154
  providerID: latestAssistantMsg?.providerID,
251
155
  parentID: rootSession.parentID,
@@ -262,11 +166,11 @@ export async function getSessionHierarchy(
262
166
  processed.add(rootSessionId);
263
167
 
264
168
  for (const child of childSessions) {
265
- await processChildSession(child.id, rootSession.id, allSessions, result, processed, 1);
169
+ await processChildSession(child.id, rootSession.id, allSessions, result, processed, 1, context);
266
170
  }
267
171
  } else {
268
- const rootParts = await getPartsForSession(rootSessionId);
269
- const allToolCalls = await getToolCallsForSession(rootSessionId, messageAgent, undefined, rootParts);
172
+ const rootParts = getSessionParts(rootSessionId, context);
173
+ const allToolCalls = buildToolCalls(rootParts, messageAgent);
270
174
 
271
175
  for (let i = 0; i < phases.length; i++) {
272
176
  const phase = phases[i];
@@ -274,31 +178,29 @@ export async function getSessionHierarchy(
274
178
  const virtualId = `${rootSessionId}-phase-${i}-${phase.agent}`;
275
179
 
276
180
  const phaseMessages = rootMessages.filter(
277
- m => m.role === 'assistant' && m.agent === phase.agent &&
181
+ (m) => m.role === "assistant" && m.agent === phase.agent &&
278
182
  m.createdAt >= phase.startTime && m.createdAt <= phase.endTime
279
183
  );
280
184
  const latestPhaseMsg = phaseMessages[phaseMessages.length - 1];
281
185
 
282
- const phaseChildren = childSessions.filter(child =>
186
+ const phaseChildren = childSessions.filter((child) =>
283
187
  child.createdAt >= phase.startTime && child.createdAt < nextPhaseStart
284
188
  );
285
189
 
286
190
  const childStatuses = await Promise.all(
287
191
  phaseChildren.map(async (child) => {
288
- const [childMessages, childParts] = await Promise.all([
289
- listMessages(child.id),
290
- getPartsForSession(child.id),
291
- ]);
192
+ const childMessages = getSessionMessages(child.id, context);
193
+ const childParts = getSessionParts(child.id, context);
292
194
  const childActivityState = getSessionActivityState(childParts);
293
195
  const childLastAssistantFinished = isAssistantFinished(childMessages);
294
- return getSessionStatus(
196
+ return getSessionStatusInfo(
295
197
  childMessages,
296
198
  childActivityState.hasPendingToolCall,
297
199
  childActivityState.lastToolCompletedAt || undefined,
298
200
  undefined,
299
201
  childLastAssistantFinished,
300
202
  true
301
- );
203
+ ).status;
302
204
  })
303
205
  );
304
206
  const workingChildCount = countBlockingChildren(childStatuses);
@@ -341,12 +243,13 @@ export async function getSessionHierarchy(
341
243
  ? deriveActivityType(phaseActivityState, phaseLastAssistantFinished, false, status, statusInfo.waitingReason)
342
244
  : "idle";
343
245
 
344
- const toolCalls = allToolCalls.filter(tc => tc.agentName === phase.agent);
246
+ const toolCalls = allToolCalls.filter((tc) => tc.agentName === phase.agent);
345
247
 
346
248
  result.push({
347
249
  id: virtualId,
348
250
  title: rootSession.title,
349
251
  agent: phase.agent,
252
+ nodeKind: "phase",
350
253
  modelID: latestPhaseMsg?.modelID,
351
254
  providerID: latestPhaseMsg?.providerID,
352
255
  parentID: undefined,
@@ -362,7 +265,7 @@ export async function getSessionHierarchy(
362
265
  });
363
266
 
364
267
  for (const child of phaseChildren) {
365
- await processChildSession(child.id, virtualId, allSessions, result, processed, 1);
268
+ await processChildSession(child.id, virtualId, allSessions, result, processed, 1, context);
366
269
  }
367
270
  }
368
271
  }
@@ -370,13 +273,14 @@ export async function getSessionHierarchy(
370
273
  return result;
371
274
  }
372
275
 
373
- export async function processChildSession(
276
+ async function processChildSession(
374
277
  sessionId: string,
375
278
  parentId: string,
376
279
  allSessions: SessionMetadata[],
377
280
  result: ActivitySession[],
378
281
  processed: Set<string>,
379
- depth = 0
282
+ depth = 0,
283
+ contextArg?: SessionContext
380
284
  ): Promise<void> {
381
285
  if (depth > MAX_RECURSION_DEPTH) {
382
286
  console.warn(`Max recursion depth reached for child session ${sessionId}`);
@@ -385,10 +289,11 @@ export async function processChildSession(
385
289
  if (processed.has(sessionId)) return;
386
290
  processed.add(sessionId);
387
291
 
388
- const session = allSessions.find((s) => s.id === sessionId);
292
+ const context = contextArg ?? createSessionContext(allSessions);
293
+ const session = getSessionFromContext(sessionId, context);
389
294
  if (!session) return;
390
295
 
391
- const messages = await listMessages(sessionId);
296
+ const messages = getSessionMessages(sessionId, context);
392
297
  const latestAssistantMsg = getLatestAssistantMessage(messages);
393
298
 
394
299
  const totalTokens = messages
@@ -402,26 +307,24 @@ export async function processChildSession(
402
307
  }
403
308
  }
404
309
 
405
- const parts = await getPartsForSession(sessionId);
310
+ const parts = getSessionParts(sessionId, context);
406
311
  const activityState = getSessionActivityState(parts);
407
-
408
- const childSessions = allSessions.filter((s) => s.parentID === sessionId);
312
+
313
+ const childSessions = getSessionChildren(sessionId, context);
409
314
  const childStatuses = await Promise.all(
410
315
  childSessions.map(async (child) => {
411
- const [childMessages, childParts] = await Promise.all([
412
- listMessages(child.id),
413
- getPartsForSession(child.id),
414
- ]);
316
+ const childMessages = getSessionMessages(child.id, context);
317
+ const childParts = getSessionParts(child.id, context);
415
318
  const childActivityState = getSessionActivityState(childParts);
416
319
  const childLastAssistantFinished = isAssistantFinished(childMessages);
417
- return getSessionStatus(
320
+ return getSessionStatusInfo(
418
321
  childMessages,
419
322
  childActivityState.hasPendingToolCall,
420
323
  childActivityState.lastToolCompletedAt || undefined,
421
324
  undefined,
422
325
  childLastAssistantFinished,
423
326
  true
424
- );
327
+ ).status;
425
328
  })
426
329
  );
427
330
  const workingChildCount = countBlockingChildren(childStatuses);
@@ -448,12 +351,13 @@ export async function processChildSession(
448
351
  );
449
352
  const activityType = deriveActivityType(activityState, lastAssistantFinished, true, status, statusInfo.waitingReason);
450
353
 
451
- const toolCalls = await getToolCallsForSession(sessionId, messageAgent, undefined, parts);
354
+ const toolCalls = buildToolCalls(parts, messageAgent);
452
355
 
453
356
  result.push({
454
357
  id: session.id,
455
358
  title: session.title,
456
359
  agent: latestAssistantMsg?.agent || "unknown",
360
+ nodeKind: "session",
457
361
  modelID: latestAssistantMsg?.modelID,
458
362
  providerID: latestAssistantMsg?.providerID,
459
363
  parentID: parentId,
@@ -470,7 +374,7 @@ export async function processChildSession(
470
374
 
471
375
  await Promise.all(
472
376
  childSessions.map((child) =>
473
- processChildSession(child.id, session.id, allSessions, result, processed, depth + 1)
377
+ processChildSession(child.id, session.id, allSessions, result, processed, depth + 1, context)
474
378
  )
475
379
  );
476
380
  }
@@ -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
+ }