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
|
@@ -3,100 +3,17 @@
|
|
|
3
3
|
* Determines session status based on message timestamps and tool call state
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
import { isPendingToolCall } from "../
|
|
6
|
+
import type { SessionStatus } from "../../shared/types";
|
|
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
|
-
export function getSessionStatus(
|
|
24
|
-
messages: MessageMeta[],
|
|
25
|
-
hasPendingToolCall: boolean = false,
|
|
26
|
-
lastToolCompletedAt?: Date,
|
|
27
|
-
workingChildCount?: number,
|
|
28
|
-
lastAssistantFinished?: boolean,
|
|
29
|
-
isSubagent: boolean = false
|
|
30
|
-
): SessionStatus {
|
|
31
|
-
return getSessionStatusInfo(
|
|
32
|
-
messages,
|
|
33
|
-
hasPendingToolCall,
|
|
34
|
-
lastToolCompletedAt,
|
|
35
|
-
workingChildCount,
|
|
36
|
-
lastAssistantFinished,
|
|
37
|
-
isSubagent
|
|
38
|
-
).status;
|
|
39
|
-
}
|
|
40
|
-
|
|
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
17
|
/**
|
|
101
18
|
* Get status from timestamp directly (for simpler cases)
|
|
102
19
|
* Used when you only have the updatedAt timestamp
|
|
@@ -120,4 +37,3 @@ export function getStatusFromTimestamp(
|
|
|
120
37
|
return "completed";
|
|
121
38
|
}
|
|
122
39
|
}
|
|
123
|
-
|
package/src/server/validation.ts
CHANGED
|
@@ -5,14 +5,12 @@ export const sessionIdSchema = z.string().regex(/^ses_[a-zA-Z0-9]+$/, 'Invalid s
|
|
|
5
5
|
|
|
6
6
|
export const partIdSchema = z.string().min(1, 'Part ID required');
|
|
7
7
|
|
|
8
|
+
export const projectIdSchema = z.string().regex(/^[a-zA-Z0-9_-]+$/, 'Invalid project ID format');
|
|
9
|
+
|
|
8
10
|
export const pollQuerySchema = z.object({
|
|
9
11
|
sessionId: sessionIdSchema.optional(),
|
|
10
12
|
});
|
|
11
13
|
|
|
12
|
-
export function validateParam<T>(schema: z.ZodSchema<T>, value: unknown): T {
|
|
13
|
-
return schema.parse(value);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
14
|
export type ValidationResult<T> = { success: true; value: T } | { success: false; response: Response };
|
|
17
15
|
|
|
18
16
|
export function validateWithResponse<T>(
|
package/src/server/watcher.ts
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Uses fs.watch() to detect changes and trigger cache invalidation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { watch, type FSWatcher } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, statSync, watch, type FSWatcher } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
8
3
|
import { EventEmitter } from "node:events";
|
|
9
|
-
import {
|
|
4
|
+
import { homedir } from "node:os";
|
|
10
5
|
|
|
11
6
|
export interface WatcherOptions {
|
|
7
|
+
dbPath?: string;
|
|
12
8
|
storagePath?: string;
|
|
13
9
|
projectPath?: string;
|
|
14
10
|
debounceMs?: number;
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
export class Watcher extends EventEmitter {
|
|
18
|
-
private
|
|
14
|
+
private dbWatcher: FSWatcher | null = null;
|
|
15
|
+
private boulderWatcher: FSWatcher | null = null;
|
|
19
16
|
private debounceTimer: Timer | null = null;
|
|
17
|
+
private rebindTimer: Timer | null = null;
|
|
20
18
|
private readonly debounceMs: number;
|
|
21
|
-
private readonly
|
|
19
|
+
private readonly dbPath: string;
|
|
20
|
+
private readonly walPath: string;
|
|
21
|
+
private readonly boulderPath: string;
|
|
22
|
+
private readonly boulderDirPath: string;
|
|
22
23
|
private readonly projectPath: string;
|
|
24
|
+
private dbWatcherTarget: string | null = null;
|
|
25
|
+
private boulderWatcherTarget: string | null = null;
|
|
26
|
+
private lastKnownBoulderSignature: string | null = null;
|
|
23
27
|
private isRunning: boolean = false;
|
|
24
28
|
|
|
25
29
|
constructor(options: WatcherOptions = {}) {
|
|
26
30
|
super();
|
|
27
|
-
this.
|
|
31
|
+
this.dbPath = resolveDbPath(options);
|
|
32
|
+
this.walPath = `${this.dbPath}-wal`;
|
|
28
33
|
this.projectPath = options.projectPath || process.cwd();
|
|
29
|
-
this.
|
|
34
|
+
this.boulderPath = join(this.projectPath, ".sisyphus", "boulder.json");
|
|
35
|
+
this.boulderDirPath = join(this.projectPath, ".sisyphus");
|
|
36
|
+
this.debounceMs = options.debounceMs ?? 100;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
start(): void {
|
|
@@ -36,58 +43,10 @@ export class Watcher extends EventEmitter {
|
|
|
36
43
|
|
|
37
44
|
this.isRunning = true;
|
|
38
45
|
|
|
39
|
-
const sessionDir = join(this.storagePath, "opencode", "storage", "session");
|
|
40
|
-
const messageDir = join(this.storagePath, "opencode", "storage", "message");
|
|
41
|
-
|
|
42
46
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.handleChange.bind(this)
|
|
47
|
-
);
|
|
48
|
-
this.watchers.push(sessionWatcher);
|
|
49
|
-
|
|
50
|
-
const messageWatcher = watch(
|
|
51
|
-
messageDir,
|
|
52
|
-
{ recursive: true },
|
|
53
|
-
this.handleChange.bind(this)
|
|
54
|
-
);
|
|
55
|
-
this.watchers.push(messageWatcher);
|
|
56
|
-
|
|
57
|
-
const partDir = join(this.storagePath, "opencode", "storage", "part");
|
|
58
|
-
try {
|
|
59
|
-
const partWatcher = watch(
|
|
60
|
-
partDir,
|
|
61
|
-
{ recursive: true },
|
|
62
|
-
this.handleChange.bind(this)
|
|
63
|
-
);
|
|
64
|
-
this.watchers.push(partWatcher);
|
|
65
|
-
} catch {
|
|
66
|
-
// part directory may not exist yet
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const boulderDir = join(this.projectPath, ".sisyphus");
|
|
70
|
-
try {
|
|
71
|
-
const boulderWatcher = watch(
|
|
72
|
-
boulderDir,
|
|
73
|
-
{ recursive: true },
|
|
74
|
-
this.handleChange.bind(this)
|
|
75
|
-
);
|
|
76
|
-
this.watchers.push(boulderWatcher);
|
|
77
|
-
} catch {
|
|
78
|
-
// boulder directory may not exist yet
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const projectWatcher = watch(
|
|
83
|
-
this.projectPath,
|
|
84
|
-
{ recursive: true },
|
|
85
|
-
this.handleProjectChange.bind(this)
|
|
86
|
-
);
|
|
87
|
-
this.watchers.push(projectWatcher);
|
|
88
|
-
} catch {
|
|
89
|
-
// project watcher may fail on unsupported environments
|
|
90
|
-
}
|
|
47
|
+
this.rebindDbWatcher();
|
|
48
|
+
this.rebindBoulderWatcher();
|
|
49
|
+
this.startRebindLoop();
|
|
91
50
|
|
|
92
51
|
this.emit("started");
|
|
93
52
|
} catch (error) {
|
|
@@ -96,15 +55,7 @@ export class Watcher extends EventEmitter {
|
|
|
96
55
|
}
|
|
97
56
|
}
|
|
98
57
|
|
|
99
|
-
private
|
|
100
|
-
if (!filename) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!filename.endsWith(".json")) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
58
|
+
private emitDebouncedChange(eventType: string, filename: string): void {
|
|
108
59
|
if (this.debounceTimer) {
|
|
109
60
|
clearTimeout(this.debounceTimer);
|
|
110
61
|
}
|
|
@@ -115,20 +66,174 @@ export class Watcher extends EventEmitter {
|
|
|
115
66
|
}, this.debounceMs);
|
|
116
67
|
}
|
|
117
68
|
|
|
118
|
-
private
|
|
119
|
-
if (
|
|
69
|
+
private getPreferredDbWatchTarget(): string {
|
|
70
|
+
if (existsSync(this.walPath)) {
|
|
71
|
+
return this.walPath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return this.dbPath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private rebindDbWatcher(): void {
|
|
78
|
+
const preferredTarget = this.getPreferredDbWatchTarget();
|
|
79
|
+
if (this.dbWatcher && this.dbWatcherTarget === preferredTarget) {
|
|
120
80
|
return;
|
|
121
81
|
}
|
|
122
82
|
|
|
123
|
-
if (
|
|
83
|
+
if (this.dbWatcher) {
|
|
84
|
+
this.dbWatcher.close();
|
|
85
|
+
this.dbWatcher = null;
|
|
86
|
+
this.dbWatcherTarget = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!existsSync(preferredTarget)) {
|
|
124
90
|
return;
|
|
125
91
|
}
|
|
126
92
|
|
|
127
|
-
|
|
93
|
+
try {
|
|
94
|
+
this.dbWatcher = watch(preferredTarget, (eventType, filename) => {
|
|
95
|
+
this.handleDbEvent(eventType, filename);
|
|
96
|
+
});
|
|
97
|
+
this.dbWatcherTarget = preferredTarget;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
100
|
+
if (code === "ENOENT") {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private handleDbEvent(eventType: string, _filename: string | null): void {
|
|
108
|
+
const watchedTarget = this.dbWatcherTarget;
|
|
109
|
+
if (!watchedTarget) {
|
|
128
110
|
return;
|
|
129
111
|
}
|
|
130
112
|
|
|
131
|
-
this.
|
|
113
|
+
this.emitDebouncedChange(eventType, basename(watchedTarget));
|
|
114
|
+
|
|
115
|
+
if (eventType === "rename" || !existsSync(watchedTarget)) {
|
|
116
|
+
this.rebindDbWatcherSafe();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (this.getPreferredDbWatchTarget() !== watchedTarget) {
|
|
121
|
+
this.rebindDbWatcherSafe();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private rebindBoulderWatcher(): void {
|
|
126
|
+
const preferredTarget = existsSync(this.boulderPath)
|
|
127
|
+
? this.boulderPath
|
|
128
|
+
: this.boulderDirPath;
|
|
129
|
+
|
|
130
|
+
if (this.boulderWatcher && this.boulderWatcherTarget === preferredTarget) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.boulderWatcher) {
|
|
135
|
+
this.boulderWatcher.close();
|
|
136
|
+
this.boulderWatcher = null;
|
|
137
|
+
this.boulderWatcherTarget = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!existsSync(preferredTarget)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.boulderWatcher = watch(preferredTarget, (eventType, filename) => {
|
|
145
|
+
this.handleBoulderEvent(eventType, filename);
|
|
146
|
+
});
|
|
147
|
+
this.boulderWatcherTarget = preferredTarget;
|
|
148
|
+
this.lastKnownBoulderSignature = this.getBoulderSignature();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private handleBoulderEvent(eventType: string, filename: string | null): void {
|
|
152
|
+
const currentSignature = this.getBoulderSignature();
|
|
153
|
+
const signatureChanged = currentSignature !== this.lastKnownBoulderSignature;
|
|
154
|
+
|
|
155
|
+
if (this.boulderWatcherTarget === this.boulderDirPath) {
|
|
156
|
+
if (filename && filename !== "boulder.json" && !signatureChanged) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!filename && !signatureChanged) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.lastKnownBoulderSignature = currentSignature;
|
|
166
|
+
this.emitDebouncedChange(eventType, ".sisyphus/boulder.json");
|
|
167
|
+
|
|
168
|
+
if (eventType === "rename") {
|
|
169
|
+
this.rebindBoulderWatcherSafe();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const preferredTarget = existsSync(this.boulderPath)
|
|
174
|
+
? this.boulderPath
|
|
175
|
+
: this.boulderDirPath;
|
|
176
|
+
if (preferredTarget !== this.boulderWatcherTarget) {
|
|
177
|
+
this.rebindBoulderWatcherSafe();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private getBoulderSignature(): string | null {
|
|
182
|
+
try {
|
|
183
|
+
const stats = statSync(this.boulderPath);
|
|
184
|
+
return `${stats.size}:${stats.mtimeMs}`;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private pollForBoulderChanges(): void {
|
|
191
|
+
const currentSignature = this.getBoulderSignature();
|
|
192
|
+
if (currentSignature === this.lastKnownBoulderSignature) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.lastKnownBoulderSignature = currentSignature;
|
|
197
|
+
this.emitDebouncedChange("change", ".sisyphus/boulder.json");
|
|
198
|
+
|
|
199
|
+
const preferredTarget = existsSync(this.boulderPath)
|
|
200
|
+
? this.boulderPath
|
|
201
|
+
: this.boulderDirPath;
|
|
202
|
+
if (preferredTarget !== this.boulderWatcherTarget) {
|
|
203
|
+
this.rebindBoulderWatcherSafe();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private startRebindLoop(): void {
|
|
208
|
+
if (this.rebindTimer) {
|
|
209
|
+
clearInterval(this.rebindTimer);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.rebindTimer = setInterval(() => {
|
|
213
|
+
if (!this.isRunning) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.pollForBoulderChanges();
|
|
218
|
+
this.rebindDbWatcherSafe();
|
|
219
|
+
this.rebindBoulderWatcherSafe();
|
|
220
|
+
}, 100);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private rebindDbWatcherSafe(): void {
|
|
224
|
+
try {
|
|
225
|
+
this.rebindDbWatcher();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.warn('[watcher] Failed to rebind DB watcher:', error instanceof Error ? error.message : error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private rebindBoulderWatcherSafe(): void {
|
|
232
|
+
try {
|
|
233
|
+
this.rebindBoulderWatcher();
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.warn('[watcher] Failed to rebind boulder watcher:', error instanceof Error ? error.message : error);
|
|
236
|
+
}
|
|
132
237
|
}
|
|
133
238
|
|
|
134
239
|
stop(): void {
|
|
@@ -141,20 +246,58 @@ export class Watcher extends EventEmitter {
|
|
|
141
246
|
this.debounceTimer = null;
|
|
142
247
|
}
|
|
143
248
|
|
|
144
|
-
|
|
145
|
-
|
|
249
|
+
if (this.rebindTimer) {
|
|
250
|
+
clearInterval(this.rebindTimer);
|
|
251
|
+
this.rebindTimer = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (this.dbWatcher) {
|
|
255
|
+
this.dbWatcher.close();
|
|
256
|
+
this.dbWatcher = null;
|
|
257
|
+
this.dbWatcherTarget = null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (this.boulderWatcher) {
|
|
261
|
+
this.boulderWatcher.close();
|
|
262
|
+
this.boulderWatcher = null;
|
|
263
|
+
this.boulderWatcherTarget = null;
|
|
146
264
|
}
|
|
147
265
|
|
|
148
|
-
this.
|
|
266
|
+
this.lastKnownBoulderSignature = null;
|
|
149
267
|
this.isRunning = false;
|
|
150
268
|
this.emit("stopped");
|
|
151
269
|
}
|
|
152
270
|
|
|
271
|
+
close(): void {
|
|
272
|
+
this.stop();
|
|
273
|
+
}
|
|
274
|
+
|
|
153
275
|
getIsRunning(): boolean {
|
|
154
276
|
return this.isRunning;
|
|
155
277
|
}
|
|
156
278
|
}
|
|
157
279
|
|
|
158
|
-
|
|
159
|
-
|
|
280
|
+
function resolveDbPath(options: WatcherOptions): string {
|
|
281
|
+
if (options.dbPath) {
|
|
282
|
+
return normalizeDbPathInput(options.dbPath);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (options.storagePath) {
|
|
286
|
+
return join(options.storagePath, "opencode", "opencode.db");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const storageRoot = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
|
|
290
|
+
return join(storageRoot, "opencode", "opencode.db");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function normalizeDbPathInput(dbPathOrStoragePath: string): string {
|
|
294
|
+
if (dbPathOrStoragePath.endsWith(".db")) {
|
|
295
|
+
return dbPathOrStoragePath;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return join(dbPathOrStoragePath, "opencode", "opencode.db");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function createWatcher(dbPath?: string, projectPath?: string): Watcher {
|
|
302
|
+
return new Watcher({ dbPath, projectPath });
|
|
160
303
|
}
|
package/src/shared/constants.ts
CHANGED
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
export const DEFAULT_PORT = 50234 as const;
|
|
6
6
|
export const API_BASE_URL = `http://localhost:${DEFAULT_PORT}`;
|
|
7
7
|
|
|
8
|
-
// Time constants
|
|
9
|
-
export const TWENTY_FOUR_HOURS_MS = 86400000 as const;
|
|
10
|
-
|
|
11
8
|
// API limits
|
|
12
9
|
export const MAX_SESSIONS_LIMIT = 20 as const;
|
|
10
|
+
/** Messages returned per session in API responses (client-facing limit) */
|
|
13
11
|
export const MAX_MESSAGES_LIMIT = 100 as const;
|
|
14
12
|
|
|
15
13
|
// Cache TTL
|
|
@@ -20,3 +18,10 @@ export const RINGBUFFER_CAPACITY = 1000 as const;
|
|
|
20
18
|
|
|
21
19
|
// Session hierarchy depth limit
|
|
22
20
|
export const MAX_RECURSION_DEPTH = 10 as const;
|
|
21
|
+
|
|
22
|
+
/** Internal upper bound for session queries (not client-facing) */
|
|
23
|
+
export const SESSION_SCAN_LIMIT = 50_000 as const;
|
|
24
|
+
/** Internal upper bound for message queries per session (not client-facing) */
|
|
25
|
+
export const MESSAGE_SCAN_LIMIT = 10_000 as const;
|
|
26
|
+
/** Cooldown between notifications for the same session (ms) */
|
|
27
|
+
export const NOTIFICATION_COOLDOWN_MS = 10_000 as const;
|
package/src/shared/index.ts
CHANGED
|
@@ -21,6 +21,36 @@ export interface SessionMetadata {
|
|
|
21
21
|
updatedAt: Date;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* SessionSummary is a lightweight session representation used in poll responses
|
|
26
|
+
* and as the summary field in SessionDetail
|
|
27
|
+
*/
|
|
28
|
+
export interface SessionSummary {
|
|
29
|
+
id: string;
|
|
30
|
+
projectID: string;
|
|
31
|
+
title: string;
|
|
32
|
+
status?: SessionStatus;
|
|
33
|
+
activityType?: SessionActivityType;
|
|
34
|
+
currentAction?: string | null;
|
|
35
|
+
agent?: string | null;
|
|
36
|
+
modelID?: string | null;
|
|
37
|
+
providerID?: string | null;
|
|
38
|
+
updatedAt: Date;
|
|
39
|
+
createdAt: Date;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* SessionDetail is the full detail response for a single session
|
|
44
|
+
* Contains summary, messages, activity tree, todos, and optional stats
|
|
45
|
+
*/
|
|
46
|
+
export interface SessionDetail {
|
|
47
|
+
session: SessionSummary;
|
|
48
|
+
messages: MessageMeta[];
|
|
49
|
+
activity: ActivitySession[];
|
|
50
|
+
todos: TodoItem[];
|
|
51
|
+
stats?: SessionStats;
|
|
52
|
+
}
|
|
53
|
+
|
|
24
54
|
/**
|
|
25
55
|
* MessageMeta represents a message in a session
|
|
26
56
|
*/
|
|
@@ -44,11 +74,13 @@ export interface MessageMeta {
|
|
|
44
74
|
* Includes agent info and hierarchy
|
|
45
75
|
*/
|
|
46
76
|
export type SessionActivityType = "tool" | "reasoning" | "patch" | "waiting-tools" | "waiting-user" | "idle";
|
|
77
|
+
export type ActivityNodeKind = "session" | "phase";
|
|
47
78
|
|
|
48
79
|
export interface ActivitySession {
|
|
49
80
|
id: string;
|
|
50
81
|
title: string;
|
|
51
82
|
agent: string;
|
|
83
|
+
nodeKind?: ActivityNodeKind;
|
|
52
84
|
modelID?: string;
|
|
53
85
|
providerID?: string;
|
|
54
86
|
parentID?: string;
|
|
@@ -96,17 +128,6 @@ export interface PartMeta {
|
|
|
96
128
|
patchFiles?: string[];
|
|
97
129
|
}
|
|
98
130
|
|
|
99
|
-
/**
|
|
100
|
-
* AgentInfo represents an active agent
|
|
101
|
-
*/
|
|
102
|
-
export interface AgentInfo {
|
|
103
|
-
name: string;
|
|
104
|
-
mode: string;
|
|
105
|
-
modelID: string;
|
|
106
|
-
active: boolean;
|
|
107
|
-
sessionID: string;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
131
|
/**
|
|
111
132
|
* ToolCall represents a tool invocation
|
|
112
133
|
*/
|
|
@@ -184,41 +205,6 @@ export type ActivityItem =
|
|
|
184
205
|
| AgentSpawnActivity
|
|
185
206
|
| AgentCompleteActivity;
|
|
186
207
|
|
|
187
|
-
/**
|
|
188
|
-
* BurstEntry represents a burst of tool call activity
|
|
189
|
-
* Groups consecutive tool calls from the same agent with aggregated metrics
|
|
190
|
-
*/
|
|
191
|
-
export interface BurstEntry {
|
|
192
|
-
id: string;
|
|
193
|
-
type: "burst";
|
|
194
|
-
agentName: string;
|
|
195
|
-
items: ToolCallActivity[];
|
|
196
|
-
toolBreakdown: Record<string, number>;
|
|
197
|
-
durationMs: number;
|
|
198
|
-
firstTimestamp: Date;
|
|
199
|
-
lastTimestamp: Date;
|
|
200
|
-
pendingCount: number;
|
|
201
|
-
errorCount: number;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* MilestoneEntry represents a significant event in the activity stream
|
|
206
|
-
* (agent spawn or agent complete)
|
|
207
|
-
*/
|
|
208
|
-
export interface MilestoneEntry {
|
|
209
|
-
id: string;
|
|
210
|
-
type: "milestone";
|
|
211
|
-
item: AgentSpawnActivity | AgentCompleteActivity;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* StreamEntry is a union type for activity stream entries
|
|
216
|
-
* Represents either a burst of tool calls or a milestone event
|
|
217
|
-
*/
|
|
218
|
-
export type StreamEntry = BurstEntry | MilestoneEntry;
|
|
219
|
-
|
|
220
|
-
export { synthesizeActivityItems } from '../utils/activityUtils';
|
|
221
|
-
|
|
222
208
|
/**
|
|
223
209
|
* PlanProgress represents progress on a plan
|
|
224
210
|
*/
|
|
@@ -229,6 +215,13 @@ export interface PlanProgress {
|
|
|
229
215
|
tasks: Array<{ description: string; completed: boolean }>;
|
|
230
216
|
}
|
|
231
217
|
|
|
218
|
+
export interface TodoItem {
|
|
219
|
+
content: string;
|
|
220
|
+
status: string;
|
|
221
|
+
priority: string;
|
|
222
|
+
position: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
232
225
|
/**
|
|
233
226
|
* ModelTokens represents token usage for a specific model
|
|
234
227
|
*/
|
|
@@ -247,6 +240,13 @@ export interface SessionStats {
|
|
|
247
240
|
modelBreakdown: ModelTokens[];
|
|
248
241
|
}
|
|
249
242
|
|
|
243
|
+
export interface SessionActivityResponse {
|
|
244
|
+
session: SessionSummary;
|
|
245
|
+
activity: ActivitySession[];
|
|
246
|
+
stats: SessionStats | null;
|
|
247
|
+
revision: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
250
|
/**
|
|
251
251
|
* Boulder represents the current plan state
|
|
252
252
|
*/
|
|
@@ -316,14 +316,9 @@ export interface AgentPhase {
|
|
|
316
316
|
* Contains current session state, plan progress, and activity data
|
|
317
317
|
*/
|
|
318
318
|
export interface PollResponse {
|
|
319
|
-
sessions:
|
|
320
|
-
|
|
319
|
+
sessions: SessionSummary[];
|
|
320
|
+
activeSessionId: string | null;
|
|
321
321
|
planProgress: PlanProgress | null;
|
|
322
322
|
planName?: string;
|
|
323
|
-
messages: MessageMeta[];
|
|
324
|
-
activitySessions: ActivitySession[];
|
|
325
|
-
sessionStats?: SessionStats;
|
|
326
323
|
lastUpdate: number;
|
|
327
324
|
}
|
|
328
|
-
|
|
329
|
-
export { RingBuffer } from '../utils/RingBuffer';
|
|
@@ -12,8 +12,9 @@ export function synthesizeActivityItems(
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
sessions.forEach((session) => {
|
|
15
|
-
if (session.parentID
|
|
16
|
-
const parent = sessionMap.get(session.parentID)
|
|
15
|
+
if (session.parentID) {
|
|
16
|
+
const parent = sessionMap.get(session.parentID);
|
|
17
|
+
if (!parent) return;
|
|
17
18
|
items.push({
|
|
18
19
|
id: `spawn-${session.id}`,
|
|
19
20
|
type: "agent-spawn",
|