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
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import type { Database, Statement } from "bun:sqlite";
|
|
2
|
+
import { getDb } from "./db";
|
|
3
|
+
import type { SessionMetadata } from "../../shared/types";
|
|
4
|
+
import { toSessionMetadata } from "../services/parsing";
|
|
5
|
+
|
|
6
|
+
export interface DbProjectRow {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
worktree: string;
|
|
10
|
+
vcs: string | null;
|
|
11
|
+
commands: string | null;
|
|
12
|
+
sandboxes: string | null;
|
|
13
|
+
timeCreated: number;
|
|
14
|
+
timeUpdated: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DbSessionRow {
|
|
18
|
+
id: string;
|
|
19
|
+
projectID: string;
|
|
20
|
+
parentID: string | null;
|
|
21
|
+
slug: string | null;
|
|
22
|
+
directory: string;
|
|
23
|
+
title: string;
|
|
24
|
+
version: string | null;
|
|
25
|
+
timeCreated: number;
|
|
26
|
+
timeUpdated: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DbMessageRow {
|
|
30
|
+
id: string;
|
|
31
|
+
sessionID: string;
|
|
32
|
+
timeCreated: number;
|
|
33
|
+
timeUpdated: number;
|
|
34
|
+
role: string | null;
|
|
35
|
+
agent: string | null;
|
|
36
|
+
data: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DbPartRow {
|
|
40
|
+
id: string;
|
|
41
|
+
messageID: string;
|
|
42
|
+
sessionID: string;
|
|
43
|
+
timeCreated: number;
|
|
44
|
+
timeUpdated: number;
|
|
45
|
+
type: string | null;
|
|
46
|
+
tool: string | null;
|
|
47
|
+
state: string | null;
|
|
48
|
+
data: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DbTodoRow {
|
|
52
|
+
sessionID: string;
|
|
53
|
+
content: string;
|
|
54
|
+
status: string;
|
|
55
|
+
priority: string;
|
|
56
|
+
position: number;
|
|
57
|
+
timeCreated: number;
|
|
58
|
+
timeUpdated: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let cachedDb: Database | null | undefined;
|
|
62
|
+
|
|
63
|
+
let queryProjectsStmt: Statement<DbProjectRow, []> | null = null;
|
|
64
|
+
let querySessionsStmt: Statement<DbSessionRow, [string | null, number | null, number]> | null = null;
|
|
65
|
+
let querySessionStmt: Statement<DbSessionRow, [string]> | null = null;
|
|
66
|
+
let querySessionChildrenStmt: Statement<DbSessionRow, [string]> | null = null;
|
|
67
|
+
let queryMessagesStmt: Statement<DbMessageRow, [string, number]> | null = null;
|
|
68
|
+
let queryPartsStmt: Statement<DbPartRow, [string]> | null = null;
|
|
69
|
+
let queryPartStmt: Statement<DbPartRow, [string]> | null = null;
|
|
70
|
+
let queryTodosStmt: Statement<DbTodoRow, [string]> | null = null;
|
|
71
|
+
let queryMaxTimestampStmt: Statement<{ maxTimestamp: number | null }, []> | null = null;
|
|
72
|
+
|
|
73
|
+
function getReadyDb(): Database | null {
|
|
74
|
+
const db = getDb();
|
|
75
|
+
if (!db) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (cachedDb === db) {
|
|
80
|
+
return db;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cachedDb = db;
|
|
84
|
+
queryProjectsStmt = db.query<DbProjectRow, []>(`
|
|
85
|
+
SELECT
|
|
86
|
+
id,
|
|
87
|
+
name,
|
|
88
|
+
worktree,
|
|
89
|
+
vcs,
|
|
90
|
+
commands,
|
|
91
|
+
sandboxes,
|
|
92
|
+
time_created AS timeCreated,
|
|
93
|
+
time_updated AS timeUpdated
|
|
94
|
+
FROM project
|
|
95
|
+
ORDER BY time_updated DESC
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
querySessionsStmt = db.query<DbSessionRow, [string | null, number | null, number]>(`
|
|
99
|
+
SELECT
|
|
100
|
+
id,
|
|
101
|
+
project_id AS projectID,
|
|
102
|
+
parent_id AS parentID,
|
|
103
|
+
slug,
|
|
104
|
+
directory,
|
|
105
|
+
title,
|
|
106
|
+
version,
|
|
107
|
+
time_created AS timeCreated,
|
|
108
|
+
time_updated AS timeUpdated
|
|
109
|
+
FROM session
|
|
110
|
+
WHERE (?1 IS NULL OR project_id = ?1)
|
|
111
|
+
AND (?2 IS NULL OR time_updated > ?2)
|
|
112
|
+
ORDER BY time_updated DESC
|
|
113
|
+
LIMIT ?3
|
|
114
|
+
`);
|
|
115
|
+
|
|
116
|
+
querySessionStmt = db.query<DbSessionRow, [string]>(`
|
|
117
|
+
SELECT
|
|
118
|
+
id,
|
|
119
|
+
project_id AS projectID,
|
|
120
|
+
parent_id AS parentID,
|
|
121
|
+
slug,
|
|
122
|
+
directory,
|
|
123
|
+
title,
|
|
124
|
+
version,
|
|
125
|
+
time_created AS timeCreated,
|
|
126
|
+
time_updated AS timeUpdated
|
|
127
|
+
FROM session
|
|
128
|
+
WHERE id = ?1
|
|
129
|
+
LIMIT 1
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
querySessionChildrenStmt = db.query<DbSessionRow, [string]>(`
|
|
133
|
+
SELECT
|
|
134
|
+
id,
|
|
135
|
+
project_id AS projectID,
|
|
136
|
+
parent_id AS parentID,
|
|
137
|
+
slug,
|
|
138
|
+
directory,
|
|
139
|
+
title,
|
|
140
|
+
version,
|
|
141
|
+
time_created AS timeCreated,
|
|
142
|
+
time_updated AS timeUpdated
|
|
143
|
+
FROM session
|
|
144
|
+
WHERE parent_id = ?1
|
|
145
|
+
ORDER BY time_created ASC
|
|
146
|
+
`);
|
|
147
|
+
|
|
148
|
+
queryMessagesStmt = db.query<DbMessageRow, [string, number]>(`
|
|
149
|
+
SELECT
|
|
150
|
+
id,
|
|
151
|
+
session_id AS sessionID,
|
|
152
|
+
time_created AS timeCreated,
|
|
153
|
+
time_updated AS timeUpdated,
|
|
154
|
+
json_extract(data, '$.role') AS role,
|
|
155
|
+
json_extract(data, '$.agent') AS agent,
|
|
156
|
+
data
|
|
157
|
+
FROM message
|
|
158
|
+
WHERE session_id = ?1
|
|
159
|
+
ORDER BY time_created DESC
|
|
160
|
+
LIMIT ?2
|
|
161
|
+
`);
|
|
162
|
+
|
|
163
|
+
queryPartsStmt = db.query<DbPartRow, [string]>(`
|
|
164
|
+
SELECT
|
|
165
|
+
id,
|
|
166
|
+
message_id AS messageID,
|
|
167
|
+
session_id AS sessionID,
|
|
168
|
+
time_created AS timeCreated,
|
|
169
|
+
time_updated AS timeUpdated,
|
|
170
|
+
json_extract(data, '$.type') AS type,
|
|
171
|
+
json_extract(data, '$.tool') AS tool,
|
|
172
|
+
CASE
|
|
173
|
+
WHEN json_type(data, '$.state') = 'text' THEN json_extract(data, '$.state')
|
|
174
|
+
WHEN json_type(data, '$.state.type') = 'text' THEN json_extract(data, '$.state.type')
|
|
175
|
+
ELSE NULL
|
|
176
|
+
END AS state,
|
|
177
|
+
data
|
|
178
|
+
FROM part
|
|
179
|
+
WHERE session_id = ?1
|
|
180
|
+
ORDER BY time_created DESC
|
|
181
|
+
`);
|
|
182
|
+
|
|
183
|
+
queryPartStmt = db.query<DbPartRow, [string]>(`
|
|
184
|
+
SELECT
|
|
185
|
+
id,
|
|
186
|
+
message_id AS messageID,
|
|
187
|
+
session_id AS sessionID,
|
|
188
|
+
time_created AS timeCreated,
|
|
189
|
+
time_updated AS timeUpdated,
|
|
190
|
+
json_extract(data, '$.type') AS type,
|
|
191
|
+
json_extract(data, '$.tool') AS tool,
|
|
192
|
+
CASE
|
|
193
|
+
WHEN json_type(data, '$.state') = 'text' THEN json_extract(data, '$.state')
|
|
194
|
+
WHEN json_type(data, '$.state.type') = 'text' THEN json_extract(data, '$.state.type')
|
|
195
|
+
ELSE NULL
|
|
196
|
+
END AS state,
|
|
197
|
+
data
|
|
198
|
+
FROM part
|
|
199
|
+
WHERE id = ?1
|
|
200
|
+
LIMIT 1
|
|
201
|
+
`);
|
|
202
|
+
|
|
203
|
+
queryTodosStmt = db.query<DbTodoRow, [string]>(`
|
|
204
|
+
SELECT
|
|
205
|
+
session_id AS sessionID,
|
|
206
|
+
content,
|
|
207
|
+
status,
|
|
208
|
+
priority,
|
|
209
|
+
position,
|
|
210
|
+
time_created AS timeCreated,
|
|
211
|
+
time_updated AS timeUpdated
|
|
212
|
+
FROM todo
|
|
213
|
+
WHERE session_id = ?1
|
|
214
|
+
ORDER BY position ASC, time_created ASC
|
|
215
|
+
`);
|
|
216
|
+
|
|
217
|
+
queryMaxTimestampStmt = db.query<{ maxTimestamp: number | null }, []>(`
|
|
218
|
+
SELECT MAX(ts) AS maxTimestamp
|
|
219
|
+
FROM (
|
|
220
|
+
SELECT MAX(time_updated) AS ts FROM session
|
|
221
|
+
UNION ALL
|
|
222
|
+
SELECT MAX(time_updated) AS ts FROM message
|
|
223
|
+
UNION ALL
|
|
224
|
+
SELECT MAX(time_updated) AS ts FROM part
|
|
225
|
+
)
|
|
226
|
+
`);
|
|
227
|
+
|
|
228
|
+
return db;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function queryProjects(): DbProjectRow[] {
|
|
232
|
+
const db = getReadyDb();
|
|
233
|
+
if (!db || !queryProjectsStmt) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return queryProjectsStmt.all();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function querySessions(
|
|
241
|
+
projectId?: string,
|
|
242
|
+
since?: number,
|
|
243
|
+
limit = 20,
|
|
244
|
+
): DbSessionRow[] {
|
|
245
|
+
const db = getReadyDb();
|
|
246
|
+
if (!db || !querySessionsStmt) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return querySessionsStmt.all(projectId ?? null, since ?? null, limit);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function querySession(sessionId: string): DbSessionRow | null {
|
|
254
|
+
const db = getReadyDb();
|
|
255
|
+
if (!db || !querySessionStmt) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return querySessionStmt.get(sessionId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function querySessionChildren(sessionId: string): DbSessionRow[] {
|
|
263
|
+
const db = getReadyDb();
|
|
264
|
+
if (!db || !querySessionChildrenStmt) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return querySessionChildrenStmt.all(sessionId);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function queryMessages(sessionId: string, limit = 100): DbMessageRow[] {
|
|
272
|
+
const db = getReadyDb();
|
|
273
|
+
if (!db || !queryMessagesStmt) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return queryMessagesStmt.all(sessionId, limit);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function queryParts(sessionId: string): DbPartRow[] {
|
|
281
|
+
const db = getReadyDb();
|
|
282
|
+
if (!db || !queryPartsStmt) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return queryPartsStmt.all(sessionId);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function queryPart(partId: string): DbPartRow | null {
|
|
290
|
+
const db = getReadyDb();
|
|
291
|
+
if (!db || !queryPartStmt) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return queryPartStmt.get(partId);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function queryTodos(sessionId: string): DbTodoRow[] {
|
|
299
|
+
const db = getReadyDb();
|
|
300
|
+
if (!db || !queryTodosStmt) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return queryTodosStmt.all(sessionId);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function queryMaxTimestamp(): number {
|
|
308
|
+
const db = getReadyDb();
|
|
309
|
+
if (!db || !queryMaxTimestampStmt) {
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Number(queryMaxTimestampStmt.get()?.maxTimestamp ?? 0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
export function listProjects(): string[] {
|
|
318
|
+
const projects = queryProjects();
|
|
319
|
+
return projects.map((p) => p.id);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function listAllSessions(): SessionMetadata[] {
|
|
323
|
+
const sessions = querySessions(undefined, undefined, 10000);
|
|
324
|
+
return sessions.map(toSessionMetadata);
|
|
325
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
|
-
import { listAllSessions } from "../storage
|
|
2
|
+
import { listAllSessions } from "../storage";
|
|
3
3
|
import type { SessionMetadata } from "../../shared/types";
|
|
4
4
|
|
|
5
5
|
async function directoryExists(directory: string): Promise<boolean> {
|
|
@@ -15,7 +15,7 @@ export async function resolveProjectDirectory(
|
|
|
15
15
|
projectId: string,
|
|
16
16
|
preloadedSessions?: SessionMetadata[],
|
|
17
17
|
): Promise<string | null> {
|
|
18
|
-
const allSessions = preloadedSessions ??
|
|
18
|
+
const allSessions = preloadedSessions ?? listAllSessions();
|
|
19
19
|
const directory = allSessions.find((session) => session.projectID === projectId)?.directory;
|
|
20
20
|
|
|
21
21
|
if (!directory) {
|
|
@@ -4,22 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { MessageMeta, SessionStatus } from "../../shared/types";
|
|
7
|
-
import { isPendingToolCall } from "../
|
|
7
|
+
import { isPendingToolCall } from "../logic/activityLogic";
|
|
8
|
+
import { getSessionStatusInfo } from "../logic/sessionLogic";
|
|
8
9
|
|
|
9
10
|
export type { SessionStatus };
|
|
10
|
-
export { isPendingToolCall };
|
|
11
|
-
export type WaitingReason
|
|
12
|
-
|
|
13
|
-
export interface SessionStatusInfo {
|
|
14
|
-
status: SessionStatus;
|
|
15
|
-
waitingReason?: WaitingReason;
|
|
16
|
-
}
|
|
11
|
+
export { isPendingToolCall, getSessionStatusInfo };
|
|
12
|
+
export type { WaitingReason, SessionStatusInfo } from "../logic/sessionLogic";
|
|
17
13
|
|
|
18
14
|
// Thresholds in milliseconds
|
|
19
15
|
const WORKING_THRESHOLD = 30 * 1000; // 30 seconds
|
|
20
16
|
const COMPLETED_THRESHOLD = 5 * 60 * 1000; // 5 minutes
|
|
21
|
-
const GRACE_PERIOD = 5 * 1000; // 5 seconds
|
|
22
|
-
|
|
23
17
|
export function getSessionStatus(
|
|
24
18
|
messages: MessageMeta[],
|
|
25
19
|
hasPendingToolCall: boolean = false,
|
|
@@ -38,65 +32,6 @@ export function getSessionStatus(
|
|
|
38
32
|
).status;
|
|
39
33
|
}
|
|
40
34
|
|
|
41
|
-
export function getSessionStatusInfo(
|
|
42
|
-
messages: MessageMeta[],
|
|
43
|
-
hasPendingToolCall: boolean = false,
|
|
44
|
-
lastToolCompletedAt?: Date,
|
|
45
|
-
workingChildCount?: number,
|
|
46
|
-
lastAssistantFinished?: boolean,
|
|
47
|
-
isSubagent: boolean = false
|
|
48
|
-
): SessionStatusInfo {
|
|
49
|
-
if (hasPendingToolCall) {
|
|
50
|
-
return { status: "working" };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (workingChildCount && workingChildCount > 0) {
|
|
54
|
-
return { status: "waiting", waitingReason: "children" };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Calculate time since last message early (needed for multiple checks)
|
|
58
|
-
let timeSinceLastMessage = Infinity;
|
|
59
|
-
if (messages && messages.length > 0) {
|
|
60
|
-
const lastMessage = messages.reduce((latest, msg) =>
|
|
61
|
-
msg.createdAt.getTime() > latest.createdAt.getTime() ? msg : latest
|
|
62
|
-
);
|
|
63
|
-
timeSinceLastMessage = Date.now() - lastMessage.createdAt.getTime();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Assistant finished turn - only applies for RECENT sessions (< 5 min)
|
|
67
|
-
// Old sessions (>= 5 min) fall through to time-based status instead of showing "waiting"
|
|
68
|
-
if (lastAssistantFinished && timeSinceLastMessage < COMPLETED_THRESHOLD) {
|
|
69
|
-
if (isSubagent) {
|
|
70
|
-
return { status: "completed" };
|
|
71
|
-
}
|
|
72
|
-
return { status: "waiting", waitingReason: "user" };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Grace period after tool completion
|
|
76
|
-
if (lastToolCompletedAt) {
|
|
77
|
-
const now = Date.now();
|
|
78
|
-
const timeSinceToolCompleted = now - lastToolCompletedAt.getTime();
|
|
79
|
-
if (timeSinceToolCompleted < GRACE_PERIOD) {
|
|
80
|
-
return { status: "working" };
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Time-based status from message timestamps
|
|
85
|
-
if (!messages || messages.length === 0) {
|
|
86
|
-
return { status: "completed" };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (timeSinceLastMessage < WORKING_THRESHOLD) {
|
|
90
|
-
return { status: "working" };
|
|
91
|
-
} else if (timeSinceLastMessage < COMPLETED_THRESHOLD) {
|
|
92
|
-
return { status: "idle" };
|
|
93
|
-
} else {
|
|
94
|
-
return { status: "completed" };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
35
|
/**
|
|
101
36
|
* Get status from timestamp directly (for simpler cases)
|
|
102
37
|
* Used when you only have the updatedAt timestamp
|
|
@@ -120,4 +55,3 @@ export function getStatusFromTimestamp(
|
|
|
120
55
|
return "completed";
|
|
121
56
|
}
|
|
122
57
|
}
|
|
123
|
-
|