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.
- 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/index.ts +9 -24
- package/src/server/routes/poll.ts +6 -9
- package/src/server/routes/projects.ts +9 -26
- package/src/server/routes/sessions.ts +66 -14
- package/src/server/services/pollService.ts +153 -47
- package/src/server/services/recentSessions.ts +14 -0
- package/src/server/services/sessionContext.ts +97 -0
- package/src/server/services/sessionService.ts +15 -183
- package/src/server/services/sessionTree.ts +92 -0
- package/src/server/storage/db.ts +2 -10
- package/src/server/storage/index.ts +7 -1
- package/src/server/storage/queries.ts +208 -5
- package/src/server/utils/projectResolver.ts +9 -3
- package/src/server/utils/sessionStatus.ts +1 -19
- package/src/server/validation.ts +2 -4
- package/src/server/watcher.ts +41 -3
- package/src/shared/constants.ts +7 -3
- package/src/shared/index.ts +3 -0
- package/src/shared/types/index.ts +9 -48
- 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-CzAaOuyw.js +0 -50
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
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((
|
|
415
|
-
activity.filter((
|
|
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 {
|
|
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
|
-
|
|
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,
|