ocwatch 0.3.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/README.md +6 -0
- package/package.json +1 -1
- package/src/client/dist/assets/index-BIu7r5_5.css +1 -0
- package/src/client/dist/assets/index-CzAaOuyw.js +50 -0
- package/src/client/dist/index.html +2 -2
- 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/shared/utils/formatTime.ts +3 -1
- package/src/client/dist/assets/index-27vUxwIP.css +0 -1
- package/src/client/dist/assets/index-B1aj6-ff.js +0 -41
- package/src/server/storage/messageParser.ts +0 -169
- package/src/server/storage/partParser.ts +0 -532
- package/src/server/storage/sessionParser.ts +0 -180
- package/src/shared/utils/burstGrouping.ts +0 -99
|
@@ -1,83 +1,151 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
SessionMetadata,
|
|
3
|
-
MessageMeta,
|
|
4
|
-
ActivitySession,
|
|
5
|
-
TreeNode,
|
|
6
|
-
TreeEdge,
|
|
7
|
-
SessionTree,
|
|
8
|
-
AgentPhase,
|
|
1
|
+
import type {
|
|
2
|
+
SessionMetadata,
|
|
3
|
+
MessageMeta,
|
|
4
|
+
ActivitySession,
|
|
5
|
+
TreeNode,
|
|
6
|
+
TreeEdge,
|
|
7
|
+
SessionTree,
|
|
9
8
|
PartMeta,
|
|
10
9
|
SessionStatus,
|
|
10
|
+
ToolCallSummary,
|
|
11
11
|
} from "../../shared/types";
|
|
12
12
|
import { MAX_RECURSION_DEPTH } from "../../shared/constants";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import {
|
|
14
|
+
formatCurrentAction,
|
|
15
|
+
isPendingToolCall,
|
|
16
|
+
generateActivityMessage,
|
|
16
17
|
getSessionActivityState,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
deriveActivityType,
|
|
19
|
+
detectAgentPhases,
|
|
20
|
+
isAssistantFinished,
|
|
21
|
+
getSessionStatusInfo,
|
|
22
|
+
type SessionStatusInfo,
|
|
23
|
+
} from "../logic";
|
|
24
|
+
import {
|
|
25
|
+
querySessionChildren,
|
|
26
|
+
queryMessages,
|
|
27
|
+
queryParts,
|
|
28
|
+
} from "../storage/queries";
|
|
29
|
+
import { getStatusFromTimestamp } from "../utils/sessionStatus";
|
|
30
|
+
import {
|
|
31
|
+
toSessionMetadata as parseSessionRow,
|
|
32
|
+
toMessageMeta as parseMessageRow,
|
|
33
|
+
toPartMeta as parsePartRow,
|
|
34
|
+
getLatestAssistantMessage,
|
|
35
|
+
getMostRecentPendingPart,
|
|
36
|
+
} from "./parsing";
|
|
37
|
+
|
|
38
|
+
export { detectAgentPhases, isAssistantFinished };
|
|
39
|
+
|
|
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);
|
|
27
56
|
}
|
|
28
57
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|
|
32
76
|
|
|
33
|
-
|
|
77
|
+
const messages = queryMessages(sessionId, MAX_MESSAGE_QUERY_LIMIT).map(parseMessageRow);
|
|
78
|
+
context.messagesBySession.set(sessionId, messages);
|
|
79
|
+
return messages;
|
|
34
80
|
}
|
|
35
81
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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;
|
|
40
91
|
}
|
|
41
92
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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;
|
|
46
111
|
}
|
|
47
112
|
|
|
113
|
+
|
|
48
114
|
function countBlockingChildren(statuses: SessionStatus[]): number {
|
|
49
115
|
return statuses.filter((status) => status === "working" || status === "waiting").length;
|
|
50
116
|
}
|
|
51
117
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
118
|
+
|
|
119
|
+
function buildToolCalls(parts: PartMeta[], messageAgent: Map<string, string>): ToolCallSummary[] {
|
|
120
|
+
const toolCalls = parts
|
|
121
|
+
.filter((part) => part.type === "tool" && part.tool)
|
|
122
|
+
.map((part): ToolCallSummary => {
|
|
123
|
+
let state: "pending" | "complete" | "error" = "complete";
|
|
124
|
+
if (isPendingToolCall(part)) {
|
|
125
|
+
state = "pending";
|
|
126
|
+
} else if (part.state === "error" || part.state === "failed") {
|
|
127
|
+
state = "error";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
id: part.id,
|
|
132
|
+
name: part.tool || "unknown",
|
|
133
|
+
state,
|
|
134
|
+
summary: formatCurrentAction(part) || part.tool || "Unknown tool",
|
|
135
|
+
input: part.input || {},
|
|
136
|
+
error: part.error,
|
|
137
|
+
timestamp: (part.completedAt || part.startedAt)?.toISOString() || "",
|
|
138
|
+
agentName: messageAgent.get(part.messageID) || "unknown",
|
|
71
139
|
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
toolCalls.sort((a, b) => {
|
|
143
|
+
const timeA = new Date(a.timestamp).getTime();
|
|
144
|
+
const timeB = new Date(b.timestamp).getTime();
|
|
145
|
+
return timeB - timeA;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return toolCalls.slice(0, 50);
|
|
81
149
|
}
|
|
82
150
|
|
|
83
151
|
export function buildAgentHierarchy(messages: MessageMeta[]): Record<string, string[]> {
|
|
@@ -107,6 +175,7 @@ export async function buildSessionTree(
|
|
|
107
175
|
const nodes: TreeNode[] = [];
|
|
108
176
|
const edges: TreeEdge[] = [];
|
|
109
177
|
const visited = new Set<string>();
|
|
178
|
+
const context = createSessionContext(allSessions);
|
|
110
179
|
|
|
111
180
|
async function processSession(sessionID: string, depth = 0) {
|
|
112
181
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
@@ -118,15 +187,22 @@ export async function buildSessionTree(
|
|
|
118
187
|
}
|
|
119
188
|
visited.add(sessionID);
|
|
120
189
|
|
|
121
|
-
const session =
|
|
190
|
+
const session = getSessionFromContext(sessionID, context);
|
|
122
191
|
if (!session) {
|
|
123
192
|
return;
|
|
124
193
|
}
|
|
125
194
|
|
|
126
|
-
const messages =
|
|
195
|
+
const messages = getSessionMessages(sessionID, context);
|
|
127
196
|
const lastAssistantFinished = isAssistantFinished(messages);
|
|
128
197
|
const isSubagent = !!session.parentID;
|
|
129
|
-
const status =
|
|
198
|
+
const status = getSessionStatusInfo(
|
|
199
|
+
messages,
|
|
200
|
+
false,
|
|
201
|
+
undefined,
|
|
202
|
+
undefined,
|
|
203
|
+
lastAssistantFinished,
|
|
204
|
+
isSubagent
|
|
205
|
+
).status;
|
|
130
206
|
|
|
131
207
|
const lastMessage = messages.sort(
|
|
132
208
|
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
|
|
@@ -150,7 +226,7 @@ export async function buildSessionTree(
|
|
|
150
226
|
await processSession(session.parentID, depth + 1);
|
|
151
227
|
}
|
|
152
228
|
|
|
153
|
-
const children =
|
|
229
|
+
const children = getSessionChildren(sessionID, context);
|
|
154
230
|
await Promise.all(
|
|
155
231
|
children.map((child) => {
|
|
156
232
|
edges.push({
|
|
@@ -173,15 +249,15 @@ export async function getSessionHierarchy(
|
|
|
173
249
|
): Promise<ActivitySession[]> {
|
|
174
250
|
const result: ActivitySession[] = [];
|
|
175
251
|
const processed = new Set<string>();
|
|
252
|
+
const context = createSessionContext(allSessions);
|
|
176
253
|
|
|
177
|
-
const rootSession =
|
|
254
|
+
const rootSession = getSessionFromContext(rootSessionId, context);
|
|
178
255
|
if (!rootSession) return result;
|
|
179
256
|
|
|
180
|
-
const rootMessages =
|
|
257
|
+
const rootMessages = getSessionMessages(rootSessionId, context);
|
|
181
258
|
const phases = detectAgentPhases(rootMessages);
|
|
182
|
-
const childSessions =
|
|
259
|
+
const childSessions = getSessionChildren(rootSessionId, context);
|
|
183
260
|
|
|
184
|
-
// Build messageAgent map for tool calls
|
|
185
261
|
const messageAgent = new Map<string, string>();
|
|
186
262
|
for (const msg of rootMessages) {
|
|
187
263
|
if (msg.agent) {
|
|
@@ -191,30 +267,28 @@ export async function getSessionHierarchy(
|
|
|
191
267
|
|
|
192
268
|
if (phases.length <= 1) {
|
|
193
269
|
const latestAssistantMsg = getLatestAssistantMessage(rootMessages);
|
|
194
|
-
|
|
270
|
+
|
|
195
271
|
const totalTokens = rootMessages
|
|
196
272
|
.filter((m) => m.tokens !== undefined)
|
|
197
273
|
.reduce((sum, m) => sum + (m.tokens || 0), 0);
|
|
198
274
|
|
|
199
|
-
const parts =
|
|
275
|
+
const parts = getSessionParts(rootSessionId, context);
|
|
200
276
|
const activityState = getSessionActivityState(parts);
|
|
201
|
-
|
|
277
|
+
|
|
202
278
|
const childStatuses = await Promise.all(
|
|
203
279
|
childSessions.map(async (child) => {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
getPartsForSession(child.id),
|
|
207
|
-
]);
|
|
280
|
+
const childMessages = getSessionMessages(child.id, context);
|
|
281
|
+
const childParts = getSessionParts(child.id, context);
|
|
208
282
|
const childActivityState = getSessionActivityState(childParts);
|
|
209
283
|
const childLastAssistantFinished = isAssistantFinished(childMessages);
|
|
210
|
-
return
|
|
284
|
+
return getSessionStatusInfo(
|
|
211
285
|
childMessages,
|
|
212
286
|
childActivityState.hasPendingToolCall,
|
|
213
287
|
childActivityState.lastToolCompletedAt || undefined,
|
|
214
288
|
undefined,
|
|
215
289
|
childLastAssistantFinished,
|
|
216
290
|
true
|
|
217
|
-
);
|
|
291
|
+
).status;
|
|
218
292
|
})
|
|
219
293
|
);
|
|
220
294
|
const workingChildCount = countBlockingChildren(childStatuses);
|
|
@@ -240,7 +314,7 @@ export async function getSessionHierarchy(
|
|
|
240
314
|
);
|
|
241
315
|
const activityType = deriveActivityType(activityState, rootLastAssistantFinished, false, status, statusInfo.waitingReason);
|
|
242
316
|
|
|
243
|
-
const toolCalls =
|
|
317
|
+
const toolCalls = buildToolCalls(parts, messageAgent);
|
|
244
318
|
|
|
245
319
|
result.push({
|
|
246
320
|
id: rootSession.id,
|
|
@@ -262,11 +336,11 @@ export async function getSessionHierarchy(
|
|
|
262
336
|
processed.add(rootSessionId);
|
|
263
337
|
|
|
264
338
|
for (const child of childSessions) {
|
|
265
|
-
await processChildSession(child.id, rootSession.id, allSessions, result, processed, 1);
|
|
339
|
+
await processChildSession(child.id, rootSession.id, allSessions, result, processed, 1, context);
|
|
266
340
|
}
|
|
267
341
|
} else {
|
|
268
|
-
const rootParts =
|
|
269
|
-
const allToolCalls =
|
|
342
|
+
const rootParts = getSessionParts(rootSessionId, context);
|
|
343
|
+
const allToolCalls = buildToolCalls(rootParts, messageAgent);
|
|
270
344
|
|
|
271
345
|
for (let i = 0; i < phases.length; i++) {
|
|
272
346
|
const phase = phases[i];
|
|
@@ -274,31 +348,29 @@ export async function getSessionHierarchy(
|
|
|
274
348
|
const virtualId = `${rootSessionId}-phase-${i}-${phase.agent}`;
|
|
275
349
|
|
|
276
350
|
const phaseMessages = rootMessages.filter(
|
|
277
|
-
m => m.role ===
|
|
351
|
+
(m) => m.role === "assistant" && m.agent === phase.agent &&
|
|
278
352
|
m.createdAt >= phase.startTime && m.createdAt <= phase.endTime
|
|
279
353
|
);
|
|
280
354
|
const latestPhaseMsg = phaseMessages[phaseMessages.length - 1];
|
|
281
355
|
|
|
282
|
-
const phaseChildren = childSessions.filter(child =>
|
|
356
|
+
const phaseChildren = childSessions.filter((child) =>
|
|
283
357
|
child.createdAt >= phase.startTime && child.createdAt < nextPhaseStart
|
|
284
358
|
);
|
|
285
359
|
|
|
286
360
|
const childStatuses = await Promise.all(
|
|
287
361
|
phaseChildren.map(async (child) => {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
getPartsForSession(child.id),
|
|
291
|
-
]);
|
|
362
|
+
const childMessages = getSessionMessages(child.id, context);
|
|
363
|
+
const childParts = getSessionParts(child.id, context);
|
|
292
364
|
const childActivityState = getSessionActivityState(childParts);
|
|
293
365
|
const childLastAssistantFinished = isAssistantFinished(childMessages);
|
|
294
|
-
return
|
|
366
|
+
return getSessionStatusInfo(
|
|
295
367
|
childMessages,
|
|
296
368
|
childActivityState.hasPendingToolCall,
|
|
297
369
|
childActivityState.lastToolCompletedAt || undefined,
|
|
298
370
|
undefined,
|
|
299
371
|
childLastAssistantFinished,
|
|
300
372
|
true
|
|
301
|
-
);
|
|
373
|
+
).status;
|
|
302
374
|
})
|
|
303
375
|
);
|
|
304
376
|
const workingChildCount = countBlockingChildren(childStatuses);
|
|
@@ -341,7 +413,7 @@ export async function getSessionHierarchy(
|
|
|
341
413
|
? deriveActivityType(phaseActivityState, phaseLastAssistantFinished, false, status, statusInfo.waitingReason)
|
|
342
414
|
: "idle";
|
|
343
415
|
|
|
344
|
-
const toolCalls = allToolCalls.filter(tc => tc.agentName === phase.agent);
|
|
416
|
+
const toolCalls = allToolCalls.filter((tc) => tc.agentName === phase.agent);
|
|
345
417
|
|
|
346
418
|
result.push({
|
|
347
419
|
id: virtualId,
|
|
@@ -362,7 +434,7 @@ export async function getSessionHierarchy(
|
|
|
362
434
|
});
|
|
363
435
|
|
|
364
436
|
for (const child of phaseChildren) {
|
|
365
|
-
await processChildSession(child.id, virtualId, allSessions, result, processed, 1);
|
|
437
|
+
await processChildSession(child.id, virtualId, allSessions, result, processed, 1, context);
|
|
366
438
|
}
|
|
367
439
|
}
|
|
368
440
|
}
|
|
@@ -376,7 +448,8 @@ export async function processChildSession(
|
|
|
376
448
|
allSessions: SessionMetadata[],
|
|
377
449
|
result: ActivitySession[],
|
|
378
450
|
processed: Set<string>,
|
|
379
|
-
depth = 0
|
|
451
|
+
depth = 0,
|
|
452
|
+
contextArg?: SessionContext
|
|
380
453
|
): Promise<void> {
|
|
381
454
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
382
455
|
console.warn(`Max recursion depth reached for child session ${sessionId}`);
|
|
@@ -385,10 +458,11 @@ export async function processChildSession(
|
|
|
385
458
|
if (processed.has(sessionId)) return;
|
|
386
459
|
processed.add(sessionId);
|
|
387
460
|
|
|
388
|
-
const
|
|
461
|
+
const context = contextArg ?? createSessionContext(allSessions);
|
|
462
|
+
const session = getSessionFromContext(sessionId, context);
|
|
389
463
|
if (!session) return;
|
|
390
464
|
|
|
391
|
-
const messages =
|
|
465
|
+
const messages = getSessionMessages(sessionId, context);
|
|
392
466
|
const latestAssistantMsg = getLatestAssistantMessage(messages);
|
|
393
467
|
|
|
394
468
|
const totalTokens = messages
|
|
@@ -402,26 +476,24 @@ export async function processChildSession(
|
|
|
402
476
|
}
|
|
403
477
|
}
|
|
404
478
|
|
|
405
|
-
const parts =
|
|
479
|
+
const parts = getSessionParts(sessionId, context);
|
|
406
480
|
const activityState = getSessionActivityState(parts);
|
|
407
|
-
|
|
408
|
-
const childSessions =
|
|
481
|
+
|
|
482
|
+
const childSessions = getSessionChildren(sessionId, context);
|
|
409
483
|
const childStatuses = await Promise.all(
|
|
410
484
|
childSessions.map(async (child) => {
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
getPartsForSession(child.id),
|
|
414
|
-
]);
|
|
485
|
+
const childMessages = getSessionMessages(child.id, context);
|
|
486
|
+
const childParts = getSessionParts(child.id, context);
|
|
415
487
|
const childActivityState = getSessionActivityState(childParts);
|
|
416
488
|
const childLastAssistantFinished = isAssistantFinished(childMessages);
|
|
417
|
-
return
|
|
489
|
+
return getSessionStatusInfo(
|
|
418
490
|
childMessages,
|
|
419
491
|
childActivityState.hasPendingToolCall,
|
|
420
492
|
childActivityState.lastToolCompletedAt || undefined,
|
|
421
493
|
undefined,
|
|
422
494
|
childLastAssistantFinished,
|
|
423
495
|
true
|
|
424
|
-
);
|
|
496
|
+
).status;
|
|
425
497
|
})
|
|
426
498
|
);
|
|
427
499
|
const workingChildCount = countBlockingChildren(childStatuses);
|
|
@@ -448,7 +520,7 @@ export async function processChildSession(
|
|
|
448
520
|
);
|
|
449
521
|
const activityType = deriveActivityType(activityState, lastAssistantFinished, true, status, statusInfo.waitingReason);
|
|
450
522
|
|
|
451
|
-
const toolCalls =
|
|
523
|
+
const toolCalls = buildToolCalls(parts, messageAgent);
|
|
452
524
|
|
|
453
525
|
result.push({
|
|
454
526
|
id: session.id,
|
|
@@ -470,7 +542,7 @@ export async function processChildSession(
|
|
|
470
542
|
|
|
471
543
|
await Promise.all(
|
|
472
544
|
childSessions.map((child) =>
|
|
473
|
-
processChildSession(child.id, session.id, allSessions, result, processed, depth + 1)
|
|
545
|
+
processChildSession(child.id, session.id, allSessions, result, processed, depth + 1, context)
|
|
474
546
|
)
|
|
475
547
|
);
|
|
476
548
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const SQLITE_BUSY_TIMEOUT_MS = 5000;
|
|
7
|
+
const SQLITE_CACHE_SIZE = -20000;
|
|
8
|
+
|
|
9
|
+
let dbSingleton: Database | null | undefined;
|
|
10
|
+
|
|
11
|
+
function getStorageRootPath(): string {
|
|
12
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
13
|
+
if (xdgDataHome) {
|
|
14
|
+
return xdgDataHome;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return join(homedir(), ".local", "share");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDbPath(): string {
|
|
21
|
+
return join(getStorageRootPath(), "opencode", "opencode.db");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function configureConnectionPragmas(db: Database): void {
|
|
25
|
+
db.query("PRAGMA busy_timeout = 5000;").run();
|
|
26
|
+
db.query("PRAGMA cache_size = -20000;").run();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
db.query("PRAGMA journal_mode = WAL;").run();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
32
|
+
console.warn(`[storage/db] Failed to enforce WAL journal mode: ${reason}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function checkDbExists(): boolean {
|
|
37
|
+
return existsSync(getDbPath());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getDb(): Database | null {
|
|
41
|
+
if (dbSingleton !== undefined) {
|
|
42
|
+
return dbSingleton;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!checkDbExists()) {
|
|
46
|
+
dbSingleton = null;
|
|
47
|
+
return dbSingleton;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const db = new Database(getDbPath(), { readonly: true });
|
|
52
|
+
configureConnectionPragmas(db);
|
|
53
|
+
dbSingleton = db;
|
|
54
|
+
return dbSingleton;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
57
|
+
console.warn(`[storage/db] Failed to open SQLite database: ${reason}`);
|
|
58
|
+
dbSingleton = null;
|
|
59
|
+
return dbSingleton;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function closeDb(): void {
|
|
64
|
+
if (!dbSingleton) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
dbSingleton.close();
|
|
69
|
+
dbSingleton = undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { checkDbExists, closeDb, getDb } from "./db";
|
|
2
|
+
export {
|
|
3
|
+
queryMaxTimestamp,
|
|
4
|
+
queryMessages,
|
|
5
|
+
queryPart,
|
|
6
|
+
queryParts,
|
|
7
|
+
queryProjects,
|
|
8
|
+
querySession,
|
|
9
|
+
querySessionChildren,
|
|
10
|
+
querySessions,
|
|
11
|
+
queryTodos,
|
|
12
|
+
listProjects,
|
|
13
|
+
listAllSessions,
|
|
14
|
+
} from "./queries";
|
|
15
|
+
export type {
|
|
16
|
+
DbMessageRow,
|
|
17
|
+
DbPartRow,
|
|
18
|
+
DbProjectRow,
|
|
19
|
+
DbSessionRow,
|
|
20
|
+
DbTodoRow,
|
|
21
|
+
} from "./queries";
|
|
22
|
+
export { parseBoulder, calculatePlanProgress } from "./boulderParser";
|