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.
@@ -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/sessionParser";
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 ?? await listAllSessions();
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 "../storage/partParser";
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 = "user" | "children";
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
-