ocwatch 0.4.0 → 0.5.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/package.json +1 -1
- package/src/client/dist/assets/{index-BYMVif3u.js → index-CzAaOuyw.js} +14 -14
- package/src/client/dist/index.html +1 -1
- package/src/server/__tests__/helpers/testDb.ts +220 -0
- package/src/server/index.ts +20 -5
- 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 +34 -45
- package/src/server/routes/projects.ts +4 -4
- package/src/server/routes/sessions.ts +107 -68
- package/src/server/routes/sse.ts +10 -4
- package/src/server/services/parsing.ts +211 -0
- package/src/server/services/pollService.ts +292 -114
- package/src/server/services/sessionService.ts +178 -106
- package/src/server/storage/db.ts +71 -0
- package/src/server/storage/index.ts +22 -0
- package/src/server/storage/queries.ts +325 -0
- package/src/server/utils/projectResolver.ts +2 -2
- package/src/server/utils/sessionStatus.ts +4 -70
- package/src/server/watcher.ts +187 -82
- package/src/shared/constants.ts +1 -0
- package/src/shared/types/index.ts +39 -5
- 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,57 @@
|
|
|
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
|
+
} from "../../shared/types";
|
|
5
13
|
import { parseBoulder, calculatePlanProgress } from "../storage/boulderParser";
|
|
6
|
-
import {
|
|
7
|
-
|
|
14
|
+
import {
|
|
15
|
+
checkDbExists,
|
|
16
|
+
queryMaxTimestamp,
|
|
17
|
+
queryMessages,
|
|
18
|
+
queryParts,
|
|
19
|
+
querySessions,
|
|
20
|
+
queryTodos,
|
|
21
|
+
} from "../storage";
|
|
22
|
+
import {
|
|
23
|
+
getSessionActivityState,
|
|
24
|
+
generateActivityMessage,
|
|
25
|
+
deriveActivityType,
|
|
26
|
+
getSessionStatusInfo,
|
|
27
|
+
isAssistantFinished,
|
|
28
|
+
} from "../logic";
|
|
8
29
|
import { resolveProjectDirectory } from "../utils/projectResolver";
|
|
9
|
-
import {
|
|
30
|
+
import { getSessionHierarchy } from "./sessionService";
|
|
10
31
|
import { aggregateSessionStats } from "./statsService";
|
|
11
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
toSessionMetadata,
|
|
34
|
+
toMessageMeta,
|
|
35
|
+
toPartMeta,
|
|
36
|
+
getLatestAssistantMessage,
|
|
37
|
+
getMostRecentPendingPart,
|
|
38
|
+
} from "./parsing";
|
|
39
|
+
import {
|
|
40
|
+
TWENTY_FOUR_HOURS_MS,
|
|
41
|
+
MAX_SESSIONS_LIMIT,
|
|
42
|
+
MAX_MESSAGES_LIMIT,
|
|
43
|
+
POLL_CACHE_TTL_MS,
|
|
44
|
+
} 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;
|
|
12
49
|
|
|
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];
|
|
50
|
+
interface IncrementalPollState {
|
|
51
|
+
sessionsById: Map<string, SessionMetadata>;
|
|
52
|
+
messagesBySessionId: Map<string, MessageMeta[]>;
|
|
53
|
+
partsBySessionId: Map<string, PartMeta[]>;
|
|
54
|
+
lastTimestamp: number;
|
|
23
55
|
}
|
|
24
56
|
|
|
25
57
|
function sessionPriority(session: SessionMetadata): number {
|
|
@@ -31,27 +63,25 @@ function sessionPriority(session: SessionMetadata): number {
|
|
|
31
63
|
}
|
|
32
64
|
|
|
33
65
|
export function generateETag(data: PollResponse): string {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const hash = createHash("sha256")
|
|
42
|
-
.update(JSON.stringify(dataForHash))
|
|
43
|
-
.digest("hex");
|
|
44
|
-
return `"${hash.substring(0, 16)}"`;
|
|
66
|
+
const dataForHash = {
|
|
67
|
+
sessions: data.sessions,
|
|
68
|
+
activeSessionId: data.activeSessionId,
|
|
69
|
+
planProgress: data.planProgress,
|
|
70
|
+
};
|
|
71
|
+
const hash = createHash("sha256").update(JSON.stringify(dataForHash)).digest("hex");
|
|
72
|
+
return `"${hash.substring(0, 16)}"`;
|
|
45
73
|
}
|
|
46
74
|
|
|
47
75
|
type PollCacheEntry = { data: PollResponse; etag: string; timestamp: number };
|
|
48
76
|
|
|
49
77
|
const pollCacheMap = new Map<string, PollCacheEntry>();
|
|
50
78
|
const pollInProgressMap = new Map<string, Promise<PollResponse>>();
|
|
79
|
+
const incrementalPollStateMap = new Map<string, IncrementalPollState>();
|
|
80
|
+
const MAX_INCREMENTAL_STATE_ENTRIES = 10;
|
|
51
81
|
let pollCacheEpoch = 0;
|
|
52
82
|
|
|
53
83
|
function cacheKey(projectId?: string): string {
|
|
54
|
-
return projectId ??
|
|
84
|
+
return projectId ?? "";
|
|
55
85
|
}
|
|
56
86
|
|
|
57
87
|
export function getPollCache(projectId?: string): PollCacheEntry | null {
|
|
@@ -93,76 +123,179 @@ export function getPollCacheTTL() {
|
|
|
93
123
|
return POLL_CACHE_TTL_MS;
|
|
94
124
|
}
|
|
95
125
|
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
126
|
+
function getIncrementalState(projectId?: string): IncrementalPollState {
|
|
127
|
+
const key = cacheKey(projectId);
|
|
128
|
+
const existing = incrementalPollStateMap.get(key);
|
|
129
|
+
if (existing) {
|
|
130
|
+
return existing;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Evict oldest entry if at capacity
|
|
134
|
+
if (incrementalPollStateMap.size >= MAX_INCREMENTAL_STATE_ENTRIES) {
|
|
135
|
+
let oldestKey: string | undefined;
|
|
136
|
+
let oldestTimestamp = Infinity;
|
|
137
|
+
for (const [k, v] of incrementalPollStateMap) {
|
|
138
|
+
if (v.lastTimestamp < oldestTimestamp) {
|
|
139
|
+
oldestTimestamp = v.lastTimestamp;
|
|
140
|
+
oldestKey = k;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (oldestKey !== undefined) {
|
|
144
|
+
incrementalPollStateMap.delete(oldestKey);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const created: IncrementalPollState = {
|
|
149
|
+
sessionsById: new Map(),
|
|
150
|
+
messagesBySessionId: new Map(),
|
|
151
|
+
partsBySessionId: new Map(),
|
|
152
|
+
lastTimestamp: 0,
|
|
153
|
+
};
|
|
154
|
+
incrementalPollStateMap.set(key, created);
|
|
155
|
+
return created;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sortMessagesDescending(messages: MessageMeta[]): MessageMeta[] {
|
|
159
|
+
return [...messages].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function loadMessagesForSession(state: IncrementalPollState, sessionId: string, forceRefresh = false): MessageMeta[] {
|
|
163
|
+
if (!forceRefresh) {
|
|
164
|
+
const cached = state.messagesBySessionId.get(sessionId);
|
|
165
|
+
if (cached) {
|
|
166
|
+
return cached;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const messages = queryMessages(sessionId, MESSAGE_SCAN_LIMIT).map(toMessageMeta);
|
|
171
|
+
const sorted = sortMessagesDescending(messages);
|
|
172
|
+
state.messagesBySessionId.set(sessionId, sorted);
|
|
173
|
+
return sorted;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function loadPartsForSession(state: IncrementalPollState, sessionId: string, forceRefresh = false): PartMeta[] {
|
|
177
|
+
if (!forceRefresh) {
|
|
178
|
+
const cached = state.partsBySessionId.get(sessionId);
|
|
179
|
+
if (cached) {
|
|
180
|
+
return cached;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const parts = queryParts(sessionId).map(toPartMeta);
|
|
185
|
+
state.partsBySessionId.set(sessionId, parts);
|
|
186
|
+
return parts;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function updateIncrementalState(state: IncrementalPollState, projectId?: string): void {
|
|
190
|
+
const currentMaxTimestamp = queryMaxTimestamp();
|
|
191
|
+
const hasBaseline = state.sessionsById.size > 0;
|
|
192
|
+
const hasIncrementalWindow = hasBaseline && currentMaxTimestamp > state.lastTimestamp;
|
|
193
|
+
|
|
194
|
+
let refreshAllSessions = !hasBaseline;
|
|
195
|
+
const changedSessionIds = new Set<string>();
|
|
196
|
+
|
|
197
|
+
if (hasIncrementalWindow) {
|
|
198
|
+
const changedSessions = querySessions(projectId, state.lastTimestamp, SESSION_SCAN_LIMIT);
|
|
199
|
+
if (changedSessions.length === 0) {
|
|
200
|
+
refreshAllSessions = true;
|
|
201
|
+
} else {
|
|
202
|
+
for (const row of changedSessions) {
|
|
203
|
+
const session = toSessionMetadata(row);
|
|
204
|
+
state.sessionsById.set(session.id, session);
|
|
205
|
+
changedSessionIds.add(session.id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (refreshAllSessions) {
|
|
211
|
+
state.sessionsById.clear();
|
|
212
|
+
const allRows = querySessions(projectId, undefined, SESSION_SCAN_LIMIT);
|
|
213
|
+
for (const row of allRows) {
|
|
214
|
+
const session = toSessionMetadata(row);
|
|
215
|
+
state.sessionsById.set(session.id, session);
|
|
216
|
+
changedSessionIds.add(session.id);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const activeSessionIds = new Set(state.sessionsById.keys());
|
|
220
|
+
for (const sessionKey of state.messagesBySessionId.keys()) {
|
|
221
|
+
if (!activeSessionIds.has(sessionKey)) {
|
|
222
|
+
state.messagesBySessionId.delete(sessionKey);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const sessionKey of state.partsBySessionId.keys()) {
|
|
226
|
+
if (!activeSessionIds.has(sessionKey)) {
|
|
227
|
+
state.partsBySessionId.delete(sessionKey);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (changedSessionIds.size > 0) {
|
|
233
|
+
for (const sessionId of changedSessionIds) {
|
|
234
|
+
loadMessagesForSession(state, sessionId, true);
|
|
235
|
+
loadPartsForSession(state, sessionId, true);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
state.lastTimestamp = currentMaxTimestamp;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function fetchPollData(projectId?: string): Promise<PollResponse> {
|
|
243
|
+
const dbExists = checkDbExists();
|
|
244
|
+
if (!dbExists) {
|
|
245
|
+
incrementalPollStateMap.delete(cacheKey(projectId));
|
|
99
246
|
return {
|
|
100
247
|
sessions: [],
|
|
101
|
-
|
|
248
|
+
activeSessionId: null,
|
|
102
249
|
planProgress: null,
|
|
103
|
-
messages: [],
|
|
104
|
-
activitySessions: [],
|
|
105
250
|
lastUpdate: Date.now(),
|
|
106
251
|
};
|
|
107
252
|
}
|
|
108
253
|
|
|
109
|
-
const
|
|
110
|
-
|
|
254
|
+
const state = getIncrementalState(projectId);
|
|
255
|
+
updateIncrementalState(state, projectId);
|
|
256
|
+
|
|
257
|
+
const scopedSessions = Array.from(state.sessionsById.values());
|
|
111
258
|
let selectedProjectDirectory: string | null = null;
|
|
112
259
|
|
|
113
260
|
if (projectId) {
|
|
114
|
-
selectedProjectDirectory = await resolveProjectDirectory(projectId,
|
|
115
|
-
scopedSessions = allSessions.filter((session) => session.projectID === projectId);
|
|
261
|
+
selectedProjectDirectory = await resolveProjectDirectory(projectId, scopedSessions);
|
|
116
262
|
}
|
|
117
263
|
|
|
118
264
|
const now = Date.now();
|
|
119
265
|
const twentyFourHoursAgo = now - TWENTY_FOUR_HOURS_MS;
|
|
120
266
|
|
|
121
|
-
const recentSessions = scopedSessions.filter(
|
|
122
|
-
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const sortedSessions = recentSessions.sort(
|
|
126
|
-
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
|
|
127
|
-
);
|
|
128
|
-
|
|
267
|
+
const recentSessions = scopedSessions.filter((session) => session.updatedAt.getTime() >= twentyFourHoursAgo);
|
|
268
|
+
const sortedSessions = recentSessions.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
129
269
|
const limitedSessions = sortedSessions.slice(0, MAX_SESSIONS_LIMIT);
|
|
130
|
-
const rootSessions = limitedSessions.filter(
|
|
270
|
+
const rootSessions = limitedSessions.filter((session) => !session.parentID);
|
|
131
271
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!messagesCache.has(id)) {
|
|
137
|
-
messagesCache.set(id, await listMessages(id));
|
|
138
|
-
}
|
|
139
|
-
return messagesCache.get(id)!;
|
|
272
|
+
function getCachedMessages(id: string): MessageMeta[] {
|
|
273
|
+
const cached = state.messagesBySessionId.get(id);
|
|
274
|
+
if (cached) return cached;
|
|
275
|
+
return loadMessagesForSession(state, id);
|
|
140
276
|
}
|
|
141
277
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
partsCache.set(id, await getPartsForSession(id));
|
|
145
|
-
}
|
|
146
|
-
return partsCache.get(id)!;
|
|
278
|
+
function getCachedParts(id: string): PartMeta[] {
|
|
279
|
+
return loadPartsForSession(state, id);
|
|
147
280
|
}
|
|
148
281
|
|
|
149
282
|
const sessionsWithAgent = await Promise.all(
|
|
150
283
|
rootSessions.map(async (session) => {
|
|
151
|
-
const messages =
|
|
284
|
+
const messages = getCachedMessages(session.id);
|
|
152
285
|
const latestAssistantMsg = getLatestAssistantMessage(messages);
|
|
153
|
-
const parts =
|
|
286
|
+
const parts = getCachedParts(session.id);
|
|
154
287
|
const activityState = getSessionActivityState(parts);
|
|
155
288
|
const lastAssistantFinished = isAssistantFinished(messages);
|
|
156
|
-
|
|
289
|
+
|
|
157
290
|
const statusInfo = getSessionStatusInfo(
|
|
158
291
|
messages,
|
|
159
292
|
activityState.hasPendingToolCall,
|
|
160
293
|
activityState.lastToolCompletedAt || undefined,
|
|
161
294
|
undefined,
|
|
162
|
-
lastAssistantFinished
|
|
295
|
+
lastAssistantFinished,
|
|
163
296
|
);
|
|
164
297
|
const status = statusInfo.status;
|
|
165
|
-
|
|
298
|
+
|
|
166
299
|
const pendingPart = getMostRecentPendingPart(parts);
|
|
167
300
|
const currentAction = generateActivityMessage(
|
|
168
301
|
activityState,
|
|
@@ -170,11 +303,17 @@ export async function fetchPollData(sessionId?: string, projectId?: string): Pro
|
|
|
170
303
|
false,
|
|
171
304
|
status,
|
|
172
305
|
pendingPart,
|
|
173
|
-
statusInfo.waitingReason
|
|
306
|
+
statusInfo.waitingReason,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const activityType = deriveActivityType(
|
|
310
|
+
activityState,
|
|
311
|
+
lastAssistantFinished,
|
|
312
|
+
false,
|
|
313
|
+
status,
|
|
314
|
+
statusInfo.waitingReason,
|
|
174
315
|
);
|
|
175
|
-
|
|
176
|
-
const activityType = deriveActivityType(activityState, lastAssistantFinished, false, status, statusInfo.waitingReason);
|
|
177
|
-
|
|
316
|
+
|
|
178
317
|
return {
|
|
179
318
|
...session,
|
|
180
319
|
agent: latestAssistantMsg?.agent || null,
|
|
@@ -184,22 +323,19 @@ export async function fetchPollData(sessionId?: string, projectId?: string): Pro
|
|
|
184
323
|
activityType,
|
|
185
324
|
currentAction,
|
|
186
325
|
};
|
|
187
|
-
})
|
|
326
|
+
}),
|
|
188
327
|
);
|
|
189
328
|
|
|
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
|
-
}
|
|
329
|
+
const activeEnriched = [...sessionsWithAgent].sort((a, b) => {
|
|
330
|
+
const priorityDiff = sessionPriority(b) - sessionPriority(a);
|
|
331
|
+
if (priorityDiff !== 0) {
|
|
332
|
+
return priorityDiff;
|
|
333
|
+
}
|
|
334
|
+
return b.updatedAt.getTime() - a.updatedAt.getTime();
|
|
335
|
+
})[0];
|
|
336
|
+
|
|
337
|
+
const activeSessionId =
|
|
338
|
+
activeEnriched && sessionPriority(activeEnriched) > 0 ? activeEnriched.id : null;
|
|
203
339
|
|
|
204
340
|
let planProgress: PlanProgress | null = null;
|
|
205
341
|
let planName: string | undefined;
|
|
@@ -212,43 +348,85 @@ export async function fetchPollData(sessionId?: string, projectId?: string): Pro
|
|
|
212
348
|
planName = boulder.planName || undefined;
|
|
213
349
|
}
|
|
214
350
|
} catch (err) {
|
|
215
|
-
console.debug(
|
|
351
|
+
console.debug("Failed to parse boulder.json:", err instanceof Error ? err.message : err);
|
|
216
352
|
}
|
|
217
353
|
}
|
|
218
354
|
|
|
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
|
-
}
|
|
355
|
+
const sessions: SessionSummary[] = sessionsWithAgent.map((s) => ({
|
|
356
|
+
id: s.id,
|
|
357
|
+
projectID: s.projectID,
|
|
358
|
+
title: s.title,
|
|
359
|
+
status: s.status,
|
|
360
|
+
activityType: s.activityType,
|
|
361
|
+
currentAction: s.currentAction,
|
|
362
|
+
agent: s.agent,
|
|
363
|
+
modelID: s.modelID,
|
|
364
|
+
providerID: s.providerID,
|
|
365
|
+
updatedAt: s.updatedAt,
|
|
366
|
+
createdAt: s.createdAt,
|
|
367
|
+
}));
|
|
243
368
|
|
|
244
369
|
return {
|
|
245
|
-
sessions
|
|
246
|
-
|
|
370
|
+
sessions,
|
|
371
|
+
activeSessionId,
|
|
247
372
|
planProgress,
|
|
248
373
|
planName,
|
|
249
|
-
messages,
|
|
250
|
-
activitySessions,
|
|
251
|
-
sessionStats,
|
|
252
374
|
lastUpdate: now,
|
|
253
375
|
};
|
|
254
376
|
}
|
|
377
|
+
|
|
378
|
+
export async function fetchSessionDetail(sessionId: string): Promise<SessionDetail> {
|
|
379
|
+
// Get all sessions for hierarchy context
|
|
380
|
+
const allSessions = querySessions(undefined, undefined, SESSION_SCAN_LIMIT).map(toSessionMetadata);
|
|
381
|
+
|
|
382
|
+
// Get messages for this session
|
|
383
|
+
const messages = queryMessages(sessionId, MAX_MESSAGES_LIMIT).map(toMessageMeta);
|
|
384
|
+
|
|
385
|
+
// Get activity tree (hierarchy)
|
|
386
|
+
const activity: ActivitySession[] = await getSessionHierarchy(sessionId, allSessions);
|
|
387
|
+
|
|
388
|
+
// Build messages cache for stats
|
|
389
|
+
const messagesCache = new Map<string, MessageMeta[]>();
|
|
390
|
+
messagesCache.set(sessionId, messages);
|
|
391
|
+
for (const activitySession of activity) {
|
|
392
|
+
if (!messagesCache.has(activitySession.id) && !activitySession.id.includes("-phase-")) {
|
|
393
|
+
const childMessages = queryMessages(activitySession.id, MAX_MESSAGES_LIMIT).map(toMessageMeta);
|
|
394
|
+
messagesCache.set(activitySession.id, childMessages);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Get todos
|
|
399
|
+
const todos: TodoItem[] = queryTodos(sessionId).map((row) => ({
|
|
400
|
+
content: row.content,
|
|
401
|
+
status: row.status,
|
|
402
|
+
priority: row.priority,
|
|
403
|
+
position: row.position,
|
|
404
|
+
}));
|
|
405
|
+
|
|
406
|
+
// Compute stats
|
|
407
|
+
const stats = aggregateSessionStats(activity, messagesCache);
|
|
408
|
+
|
|
409
|
+
// Find the session metadata
|
|
410
|
+
const sessionMeta = allSessions.find((s) => s.id === sessionId);
|
|
411
|
+
|
|
412
|
+
// Find the root activity entry for derived status (direct match or last phase)
|
|
413
|
+
const rootActivity =
|
|
414
|
+
activity.find((a) => a.id === sessionId) ??
|
|
415
|
+
activity.filter((a) => a.id.startsWith(`${sessionId}-phase-`)).pop();
|
|
416
|
+
|
|
417
|
+
const session: SessionSummary = {
|
|
418
|
+
id: sessionId,
|
|
419
|
+
projectID: sessionMeta?.projectID ?? "",
|
|
420
|
+
title: sessionMeta?.title ?? sessionId,
|
|
421
|
+
status: rootActivity?.status,
|
|
422
|
+
activityType: rootActivity?.activityType,
|
|
423
|
+
currentAction: rootActivity?.currentAction,
|
|
424
|
+
agent: rootActivity?.agent ?? null,
|
|
425
|
+
modelID: rootActivity?.modelID ?? null,
|
|
426
|
+
providerID: rootActivity?.providerID ?? null,
|
|
427
|
+
updatedAt: sessionMeta?.updatedAt ?? new Date(),
|
|
428
|
+
createdAt: sessionMeta?.createdAt ?? new Date(),
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
return { session, messages, activity, todos, stats };
|
|
432
|
+
}
|