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.
- package/README.md +22 -3
- package/package.json +4 -4
- package/src/client/dist/assets/GraphView-BZV40eAE.css +1 -0
- package/src/client/dist/assets/GraphView-KWCCGYb2.js +9 -0
- package/src/client/dist/assets/graph-Cw_XSlvx.js +7 -0
- package/src/client/dist/assets/index-CbgYG3pJ.js +23 -0
- package/src/client/dist/assets/index-CgDCc8Mm.css +1 -0
- package/src/client/dist/assets/motion-CGUGF2CN.js +9 -0
- package/src/client/dist/index.html +4 -2
- package/src/server/__tests__/helpers/testDb.ts +220 -0
- package/src/server/index.ts +27 -27
- package/src/server/logic/activityLogic.ts +260 -0
- package/src/server/logic/index.ts +2 -0
- package/src/server/logic/sessionLogic.ts +107 -0
- package/src/server/routes/parts.ts +9 -7
- package/src/server/routes/poll.ts +32 -46
- package/src/server/routes/projects.ts +10 -27
- package/src/server/routes/sessions.ts +159 -68
- package/src/server/routes/sse.ts +10 -4
- package/src/server/services/parsing.ts +211 -0
- package/src/server/services/pollService.ts +400 -116
- package/src/server/services/recentSessions.ts +14 -0
- package/src/server/services/sessionContext.ts +97 -0
- package/src/server/services/sessionService.ts +97 -193
- package/src/server/services/sessionTree.ts +92 -0
- package/src/server/storage/db.ts +63 -0
- package/src/server/storage/index.ts +28 -0
- package/src/server/storage/queries.ts +528 -0
- package/src/server/utils/projectResolver.ts +9 -3
- package/src/server/utils/sessionStatus.ts +5 -89
- package/src/server/validation.ts +2 -4
- package/src/server/watcher.ts +225 -82
- package/src/shared/constants.ts +8 -3
- package/src/shared/index.ts +3 -0
- package/src/shared/types/index.ts +48 -53
- package/src/shared/utils/activityUtils.ts +3 -2
- package/src/client/dist/assets/index-BIu7r5_5.css +0 -1
- package/src/client/dist/assets/index-BYMVif3u.js +0 -50
- package/src/server/storage/messageParser.ts +0 -169
- package/src/server/storage/partParser.ts +0 -532
- 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
|
-
|
|
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 {
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
import {
|
|
10
|
+
formatCurrentAction,
|
|
11
|
+
isPendingToolCall,
|
|
12
|
+
generateActivityMessage,
|
|
16
13
|
getSessionActivityState,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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 =
|
|
83
|
+
const rootSession = getSessionFromContext(rootSessionId, context);
|
|
178
84
|
if (!rootSession) return result;
|
|
179
85
|
|
|
180
|
-
const rootMessages =
|
|
86
|
+
const rootMessages = getSessionMessages(rootSessionId, context);
|
|
181
87
|
const phases = detectAgentPhases(rootMessages);
|
|
182
|
-
const childSessions =
|
|
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 =
|
|
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
|
|
205
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
269
|
-
const allToolCalls =
|
|
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 ===
|
|
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
|
|
289
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
292
|
+
const context = contextArg ?? createSessionContext(allSessions);
|
|
293
|
+
const session = getSessionFromContext(sessionId, context);
|
|
389
294
|
if (!session) return;
|
|
390
295
|
|
|
391
|
-
const messages =
|
|
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 =
|
|
310
|
+
const parts = getSessionParts(sessionId, context);
|
|
406
311
|
const activityState = getSessionActivityState(parts);
|
|
407
|
-
|
|
408
|
-
const childSessions =
|
|
312
|
+
|
|
313
|
+
const childSessions = getSessionChildren(sessionId, context);
|
|
409
314
|
const childStatuses = await Promise.all(
|
|
410
315
|
childSessions.map(async (child) => {
|
|
411
|
-
const
|
|
412
|
-
|
|
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
|
|
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 =
|
|
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
|
+
}
|