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
|
@@ -1,25 +1,61 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type {
|
|
3
|
+
PollResponse,
|
|
4
|
+
SessionSummary,
|
|
5
|
+
SessionDetail,
|
|
6
|
+
SessionMetadata,
|
|
7
|
+
MessageMeta,
|
|
8
|
+
PlanProgress,
|
|
9
|
+
ActivitySession,
|
|
10
|
+
PartMeta,
|
|
11
|
+
TodoItem,
|
|
12
|
+
SessionActivityResponse,
|
|
13
|
+
} from "../../shared/types";
|
|
5
14
|
import { parseBoulder, calculatePlanProgress } from "../storage/boulderParser";
|
|
6
|
-
import {
|
|
7
|
-
|
|
15
|
+
import {
|
|
16
|
+
checkDbExists,
|
|
17
|
+
queryMaxTimestamp,
|
|
18
|
+
queryMessages,
|
|
19
|
+
queryParts,
|
|
20
|
+
queryMessagesForSessions,
|
|
21
|
+
queryPartsForSessions,
|
|
22
|
+
querySessions,
|
|
23
|
+
querySessionSubtree,
|
|
24
|
+
querySessionSubtreeRevision,
|
|
25
|
+
queryTodos,
|
|
26
|
+
} from "../storage";
|
|
27
|
+
import {
|
|
28
|
+
getSessionActivityState,
|
|
29
|
+
generateActivityMessage,
|
|
30
|
+
deriveActivityType,
|
|
31
|
+
getSessionStatusInfo,
|
|
32
|
+
isAssistantFinished,
|
|
33
|
+
} from "../logic";
|
|
8
34
|
import { resolveProjectDirectory } from "../utils/projectResolver";
|
|
9
|
-
import {
|
|
35
|
+
import { createSessionContext } from "./sessionContext";
|
|
36
|
+
import { getSessionHierarchy } from "./sessionService";
|
|
10
37
|
import { aggregateSessionStats } from "./statsService";
|
|
11
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
toSessionMetadata,
|
|
40
|
+
toMessageMeta,
|
|
41
|
+
toPartMeta,
|
|
42
|
+
getLatestAssistantMessage,
|
|
43
|
+
getMostRecentPendingPart,
|
|
44
|
+
} from "./parsing";
|
|
45
|
+
import { selectRecentRootSessions } from "./recentSessions";
|
|
46
|
+
import {
|
|
47
|
+
MAX_SESSIONS_LIMIT,
|
|
48
|
+
MAX_MESSAGES_LIMIT,
|
|
49
|
+
POLL_CACHE_TTL_MS,
|
|
50
|
+
SESSION_SCAN_LIMIT,
|
|
51
|
+
MESSAGE_SCAN_LIMIT,
|
|
52
|
+
} from "../../shared/constants";
|
|
12
53
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function getMostRecentPendingPart(parts: PartMeta[]): PartMeta | undefined {
|
|
20
|
-
return parts
|
|
21
|
-
.filter((part) => isPendingToolCall(part))
|
|
22
|
-
.sort((a, b) => (b.startedAt?.getTime() || 0) - (a.startedAt?.getTime() || 0))[0];
|
|
54
|
+
interface IncrementalPollState {
|
|
55
|
+
sessionsById: Map<string, SessionMetadata>;
|
|
56
|
+
messagesBySessionId: Map<string, MessageMeta[]>;
|
|
57
|
+
partsBySessionId: Map<string, PartMeta[]>;
|
|
58
|
+
lastTimestamp: number;
|
|
23
59
|
}
|
|
24
60
|
|
|
25
61
|
function sessionPriority(session: SessionMetadata): number {
|
|
@@ -31,27 +67,46 @@ function sessionPriority(session: SessionMetadata): number {
|
|
|
31
67
|
}
|
|
32
68
|
|
|
33
69
|
export function generateETag(data: PollResponse): string {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
70
|
+
const dataForHash = {
|
|
71
|
+
sessions: data.sessions,
|
|
72
|
+
activeSessionId: data.activeSessionId,
|
|
73
|
+
planProgress: data.planProgress,
|
|
74
|
+
};
|
|
75
|
+
const hash = createHash("sha256").update(JSON.stringify(dataForHash)).digest("hex");
|
|
76
|
+
return `"${hash.substring(0, 16)}"`;
|
|
77
|
+
}
|
|
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)}"`;
|
|
45
91
|
}
|
|
46
92
|
|
|
47
93
|
type PollCacheEntry = { data: PollResponse; etag: string; timestamp: number };
|
|
94
|
+
type SessionActivityCacheEntry = { data: SessionActivityResponse; etag: string };
|
|
48
95
|
|
|
49
96
|
const pollCacheMap = new Map<string, PollCacheEntry>();
|
|
50
97
|
const pollInProgressMap = new Map<string, Promise<PollResponse>>();
|
|
98
|
+
const sessionActivityCacheMap = new Map<string, SessionActivityCacheEntry>();
|
|
99
|
+
const sessionActivityInProgressMap = new Map<string, Promise<SessionActivityResponse>>();
|
|
100
|
+
const incrementalPollStateMap = new Map<string, IncrementalPollState>();
|
|
101
|
+
const MAX_INCREMENTAL_STATE_ENTRIES = 10;
|
|
51
102
|
let pollCacheEpoch = 0;
|
|
52
103
|
|
|
53
104
|
function cacheKey(projectId?: string): string {
|
|
54
|
-
return projectId ??
|
|
105
|
+
return projectId ?? "";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sessionActivityCacheKey(sessionId: string): string {
|
|
109
|
+
return sessionId;
|
|
55
110
|
}
|
|
56
111
|
|
|
57
112
|
export function getPollCache(projectId?: string): PollCacheEntry | null {
|
|
@@ -74,6 +129,7 @@ export function getPollCacheEpoch() {
|
|
|
74
129
|
export function invalidatePollCache() {
|
|
75
130
|
pollCacheEpoch += 1;
|
|
76
131
|
pollCacheMap.clear();
|
|
132
|
+
sessionActivityCacheMap.clear();
|
|
77
133
|
}
|
|
78
134
|
|
|
79
135
|
export function getPollInProgress(projectId?: string): Promise<PollResponse> | null {
|
|
@@ -89,80 +145,238 @@ export function setPollInProgress(promise: Promise<PollResponse> | null, project
|
|
|
89
145
|
}
|
|
90
146
|
}
|
|
91
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
|
+
|
|
92
174
|
export function getPollCacheTTL() {
|
|
93
175
|
return POLL_CACHE_TTL_MS;
|
|
94
176
|
}
|
|
95
177
|
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
178
|
+
function getIncrementalState(projectId?: string): IncrementalPollState {
|
|
179
|
+
const key = cacheKey(projectId);
|
|
180
|
+
const existing = incrementalPollStateMap.get(key);
|
|
181
|
+
if (existing) {
|
|
182
|
+
return existing;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Evict oldest entry if at capacity
|
|
186
|
+
if (incrementalPollStateMap.size >= MAX_INCREMENTAL_STATE_ENTRIES) {
|
|
187
|
+
let oldestKey: string | undefined;
|
|
188
|
+
let oldestTimestamp = Infinity;
|
|
189
|
+
for (const [k, v] of incrementalPollStateMap) {
|
|
190
|
+
if (v.lastTimestamp < oldestTimestamp) {
|
|
191
|
+
oldestTimestamp = v.lastTimestamp;
|
|
192
|
+
oldestKey = k;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (oldestKey !== undefined) {
|
|
196
|
+
incrementalPollStateMap.delete(oldestKey);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const created: IncrementalPollState = {
|
|
201
|
+
sessionsById: new Map(),
|
|
202
|
+
messagesBySessionId: new Map(),
|
|
203
|
+
partsBySessionId: new Map(),
|
|
204
|
+
lastTimestamp: 0,
|
|
205
|
+
};
|
|
206
|
+
incrementalPollStateMap.set(key, created);
|
|
207
|
+
return created;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function sortMessagesDescending(messages: MessageMeta[]): MessageMeta[] {
|
|
211
|
+
return [...messages].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
212
|
+
}
|
|
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
|
+
|
|
246
|
+
function loadMessagesForSession(state: IncrementalPollState, sessionId: string, forceRefresh = false): MessageMeta[] {
|
|
247
|
+
if (!forceRefresh) {
|
|
248
|
+
const cached = state.messagesBySessionId.get(sessionId);
|
|
249
|
+
if (cached) {
|
|
250
|
+
return cached;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const messages = queryMessages(sessionId, MESSAGE_SCAN_LIMIT).map(toMessageMeta);
|
|
255
|
+
const sorted = sortMessagesDescending(messages);
|
|
256
|
+
state.messagesBySessionId.set(sessionId, sorted);
|
|
257
|
+
return sorted;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function loadPartsForSession(state: IncrementalPollState, sessionId: string, forceRefresh = false): PartMeta[] {
|
|
261
|
+
if (!forceRefresh) {
|
|
262
|
+
const cached = state.partsBySessionId.get(sessionId);
|
|
263
|
+
if (cached) {
|
|
264
|
+
return cached;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const parts = queryParts(sessionId).map(toPartMeta);
|
|
269
|
+
state.partsBySessionId.set(sessionId, parts);
|
|
270
|
+
return parts;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function updateIncrementalState(state: IncrementalPollState, projectId?: string): void {
|
|
274
|
+
const currentMaxTimestamp = queryMaxTimestamp();
|
|
275
|
+
const hasBaseline = state.sessionsById.size > 0;
|
|
276
|
+
const hasIncrementalWindow = hasBaseline && currentMaxTimestamp > state.lastTimestamp;
|
|
277
|
+
|
|
278
|
+
let refreshAllSessions = !hasBaseline;
|
|
279
|
+
const changedSessionIds = new Set<string>();
|
|
280
|
+
|
|
281
|
+
if (hasIncrementalWindow) {
|
|
282
|
+
const changedSessions = querySessions(projectId, state.lastTimestamp, SESSION_SCAN_LIMIT);
|
|
283
|
+
if (changedSessions.length === 0) {
|
|
284
|
+
refreshAllSessions = true;
|
|
285
|
+
} else {
|
|
286
|
+
for (const row of changedSessions) {
|
|
287
|
+
const session = toSessionMetadata(row);
|
|
288
|
+
state.sessionsById.set(session.id, session);
|
|
289
|
+
changedSessionIds.add(session.id);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (refreshAllSessions) {
|
|
295
|
+
state.sessionsById.clear();
|
|
296
|
+
const allRows = querySessions(projectId, undefined, SESSION_SCAN_LIMIT);
|
|
297
|
+
for (const row of allRows) {
|
|
298
|
+
const session = toSessionMetadata(row);
|
|
299
|
+
state.sessionsById.set(session.id, session);
|
|
300
|
+
changedSessionIds.add(session.id);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const activeSessionIds = new Set(state.sessionsById.keys());
|
|
304
|
+
for (const sessionKey of state.messagesBySessionId.keys()) {
|
|
305
|
+
if (!activeSessionIds.has(sessionKey)) {
|
|
306
|
+
state.messagesBySessionId.delete(sessionKey);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const sessionKey of state.partsBySessionId.keys()) {
|
|
310
|
+
if (!activeSessionIds.has(sessionKey)) {
|
|
311
|
+
state.partsBySessionId.delete(sessionKey);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const wasFirstLoad = !hasBaseline || refreshAllSessions;
|
|
317
|
+
if (!wasFirstLoad && changedSessionIds.size > 0) {
|
|
318
|
+
for (const sessionId of changedSessionIds) {
|
|
319
|
+
loadMessagesForSession(state, sessionId, true);
|
|
320
|
+
loadPartsForSession(state, sessionId, true);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
state.lastTimestamp = currentMaxTimestamp;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function fetchPollData(projectId?: string): Promise<PollResponse> {
|
|
328
|
+
const dbExists = checkDbExists();
|
|
329
|
+
if (!dbExists) {
|
|
330
|
+
incrementalPollStateMap.delete(cacheKey(projectId));
|
|
99
331
|
return {
|
|
100
332
|
sessions: [],
|
|
101
|
-
|
|
333
|
+
activeSessionId: null,
|
|
102
334
|
planProgress: null,
|
|
103
|
-
messages: [],
|
|
104
|
-
activitySessions: [],
|
|
105
335
|
lastUpdate: Date.now(),
|
|
106
336
|
};
|
|
107
337
|
}
|
|
108
338
|
|
|
109
|
-
const
|
|
110
|
-
|
|
339
|
+
const state = getIncrementalState(projectId);
|
|
340
|
+
updateIncrementalState(state, projectId);
|
|
341
|
+
|
|
342
|
+
const scopedSessions = Array.from(state.sessionsById.values());
|
|
111
343
|
let selectedProjectDirectory: string | null = null;
|
|
112
344
|
|
|
113
345
|
if (projectId) {
|
|
114
|
-
selectedProjectDirectory = await resolveProjectDirectory(projectId,
|
|
115
|
-
scopedSessions = allSessions.filter((session) => session.projectID === projectId);
|
|
346
|
+
selectedProjectDirectory = await resolveProjectDirectory(projectId, scopedSessions);
|
|
116
347
|
}
|
|
117
348
|
|
|
118
349
|
const now = Date.now();
|
|
119
|
-
const twentyFourHoursAgo = now - TWENTY_FOUR_HOURS_MS;
|
|
120
350
|
|
|
121
|
-
const
|
|
122
|
-
(s) => s.updatedAt.getTime() >= twentyFourHoursAgo
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const sortedSessions = recentSessions.sort(
|
|
126
|
-
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const limitedSessions = sortedSessions.slice(0, MAX_SESSIONS_LIMIT);
|
|
130
|
-
const rootSessions = limitedSessions.filter(s => !s.parentID);
|
|
351
|
+
const rootSessions = selectRecentRootSessions(scopedSessions, MAX_SESSIONS_LIMIT);
|
|
131
352
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!messagesCache.has(id)) {
|
|
137
|
-
messagesCache.set(id, await listMessages(id));
|
|
138
|
-
}
|
|
139
|
-
return messagesCache.get(id)!;
|
|
353
|
+
function getCachedMessages(id: string): MessageMeta[] {
|
|
354
|
+
const cached = state.messagesBySessionId.get(id);
|
|
355
|
+
if (cached) return cached;
|
|
356
|
+
return loadMessagesForSession(state, id);
|
|
140
357
|
}
|
|
141
358
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
partsCache.set(id, await getPartsForSession(id));
|
|
145
|
-
}
|
|
146
|
-
return partsCache.get(id)!;
|
|
359
|
+
function getCachedParts(id: string): PartMeta[] {
|
|
360
|
+
return loadPartsForSession(state, id);
|
|
147
361
|
}
|
|
148
362
|
|
|
149
363
|
const sessionsWithAgent = await Promise.all(
|
|
150
364
|
rootSessions.map(async (session) => {
|
|
151
|
-
const messages =
|
|
365
|
+
const messages = getCachedMessages(session.id);
|
|
152
366
|
const latestAssistantMsg = getLatestAssistantMessage(messages);
|
|
153
|
-
const parts =
|
|
367
|
+
const parts = getCachedParts(session.id);
|
|
154
368
|
const activityState = getSessionActivityState(parts);
|
|
155
369
|
const lastAssistantFinished = isAssistantFinished(messages);
|
|
156
|
-
|
|
370
|
+
|
|
157
371
|
const statusInfo = getSessionStatusInfo(
|
|
158
372
|
messages,
|
|
159
373
|
activityState.hasPendingToolCall,
|
|
160
374
|
activityState.lastToolCompletedAt || undefined,
|
|
161
375
|
undefined,
|
|
162
|
-
lastAssistantFinished
|
|
376
|
+
lastAssistantFinished,
|
|
163
377
|
);
|
|
164
378
|
const status = statusInfo.status;
|
|
165
|
-
|
|
379
|
+
|
|
166
380
|
const pendingPart = getMostRecentPendingPart(parts);
|
|
167
381
|
const currentAction = generateActivityMessage(
|
|
168
382
|
activityState,
|
|
@@ -170,11 +384,17 @@ export async function fetchPollData(sessionId?: string, projectId?: string): Pro
|
|
|
170
384
|
false,
|
|
171
385
|
status,
|
|
172
386
|
pendingPart,
|
|
173
|
-
statusInfo.waitingReason
|
|
387
|
+
statusInfo.waitingReason,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const activityType = deriveActivityType(
|
|
391
|
+
activityState,
|
|
392
|
+
lastAssistantFinished,
|
|
393
|
+
false,
|
|
394
|
+
status,
|
|
395
|
+
statusInfo.waitingReason,
|
|
174
396
|
);
|
|
175
|
-
|
|
176
|
-
const activityType = deriveActivityType(activityState, lastAssistantFinished, false, status, statusInfo.waitingReason);
|
|
177
|
-
|
|
397
|
+
|
|
178
398
|
return {
|
|
179
399
|
...session,
|
|
180
400
|
agent: latestAssistantMsg?.agent || null,
|
|
@@ -184,22 +404,19 @@ export async function fetchPollData(sessionId?: string, projectId?: string): Pro
|
|
|
184
404
|
activityType,
|
|
185
405
|
currentAction,
|
|
186
406
|
};
|
|
187
|
-
})
|
|
407
|
+
}),
|
|
188
408
|
);
|
|
189
409
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
} else if (activeEnriched) {
|
|
201
|
-
activeSession = rootSessions.find(s => s.id === activeEnriched.id) ?? null;
|
|
202
|
-
}
|
|
410
|
+
const activeEnriched = [...sessionsWithAgent].sort((a, b) => {
|
|
411
|
+
const priorityDiff = sessionPriority(b) - sessionPriority(a);
|
|
412
|
+
if (priorityDiff !== 0) {
|
|
413
|
+
return priorityDiff;
|
|
414
|
+
}
|
|
415
|
+
return b.updatedAt.getTime() - a.updatedAt.getTime();
|
|
416
|
+
})[0];
|
|
417
|
+
|
|
418
|
+
const activeSessionId =
|
|
419
|
+
activeEnriched && sessionPriority(activeEnriched) > 0 ? activeEnriched.id : null;
|
|
203
420
|
|
|
204
421
|
let planProgress: PlanProgress | null = null;
|
|
205
422
|
let planName: string | undefined;
|
|
@@ -212,43 +429,110 @@ export async function fetchPollData(sessionId?: string, projectId?: string): Pro
|
|
|
212
429
|
planName = boulder.planName || undefined;
|
|
213
430
|
}
|
|
214
431
|
} catch (err) {
|
|
215
|
-
console.
|
|
432
|
+
console.warn("Failed to parse boulder.json:", err instanceof Error ? err.message : err);
|
|
216
433
|
}
|
|
217
434
|
}
|
|
218
435
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
233
|
-
messages = sortedMessages.slice(0, MAX_MESSAGES_LIMIT);
|
|
234
|
-
|
|
235
|
-
activitySessions = await getSessionHierarchy(targetSessionId, scopedSessions);
|
|
236
|
-
|
|
237
|
-
for (const session of activitySessions) {
|
|
238
|
-
await getCachedMessages(session.id);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
sessionStats = aggregateSessionStats(activitySessions, messagesCache);
|
|
242
|
-
}
|
|
436
|
+
const sessions: SessionSummary[] = sessionsWithAgent.map((s) => ({
|
|
437
|
+
id: s.id,
|
|
438
|
+
projectID: s.projectID,
|
|
439
|
+
title: s.title,
|
|
440
|
+
status: s.status,
|
|
441
|
+
activityType: s.activityType,
|
|
442
|
+
currentAction: s.currentAction,
|
|
443
|
+
agent: s.agent,
|
|
444
|
+
modelID: s.modelID,
|
|
445
|
+
providerID: s.providerID,
|
|
446
|
+
updatedAt: s.updatedAt,
|
|
447
|
+
createdAt: s.createdAt,
|
|
448
|
+
}));
|
|
243
449
|
|
|
244
450
|
return {
|
|
245
|
-
sessions
|
|
246
|
-
|
|
451
|
+
sessions,
|
|
452
|
+
activeSessionId,
|
|
247
453
|
planProgress,
|
|
248
454
|
planName,
|
|
249
|
-
messages,
|
|
250
|
-
activitySessions,
|
|
251
|
-
sessionStats,
|
|
252
455
|
lastUpdate: now,
|
|
253
456
|
};
|
|
254
457
|
}
|
|
458
|
+
|
|
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, []);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const activity: ActivitySession[] = await getSessionHierarchy(sessionId, allSessions, context);
|
|
484
|
+
const stats = aggregateSessionStats(activity, messagesBySession);
|
|
485
|
+
const sessionMeta = allSessions.find((session) => session.id === sessionId);
|
|
486
|
+
const rootActivity =
|
|
487
|
+
activity.find((activitySession) => activitySession.id === sessionId) ??
|
|
488
|
+
activity.filter((activitySession) => activitySession.id.startsWith(`${sessionId}-phase-`)).pop();
|
|
489
|
+
|
|
490
|
+
const session: SessionSummary = {
|
|
491
|
+
id: sessionId,
|
|
492
|
+
projectID: sessionMeta?.projectID ?? "",
|
|
493
|
+
title: sessionMeta?.title ?? sessionId,
|
|
494
|
+
status: rootActivity?.status,
|
|
495
|
+
activityType: rootActivity?.activityType,
|
|
496
|
+
currentAction: rootActivity?.currentAction,
|
|
497
|
+
agent: rootActivity?.agent ?? null,
|
|
498
|
+
modelID: rootActivity?.modelID ?? null,
|
|
499
|
+
providerID: rootActivity?.providerID ?? null,
|
|
500
|
+
updatedAt: sessionMeta?.updatedAt ?? new Date(),
|
|
501
|
+
createdAt: sessionMeta?.createdAt ?? new Date(),
|
|
502
|
+
};
|
|
503
|
+
|
|
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
|
+
};
|
|
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
|
+
}
|