ocwatch 0.1.1
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/LICENSE +21 -0
- package/README.md +45 -0
- package/package.json +57 -0
- package/src/client/dist/assets/index-BN8enf1I.css +1 -0
- package/src/client/dist/assets/index-qN9nCkAq.js +41 -0
- package/src/client/dist/index.html +14 -0
- package/src/client/dist/vite.svg +1 -0
- package/src/server/cli.ts +84 -0
- package/src/server/index.ts +78 -0
- package/src/server/middleware/error.ts +36 -0
- package/src/server/routes/health.ts +7 -0
- package/src/server/routes/index.ts +18 -0
- package/src/server/routes/parts.ts +19 -0
- package/src/server/routes/plan.ts +15 -0
- package/src/server/routes/poll.ts +77 -0
- package/src/server/routes/projects.ts +34 -0
- package/src/server/routes/sessions.ts +118 -0
- package/src/server/routes/sse.ts +91 -0
- package/src/server/services/pollService.ts +220 -0
- package/src/server/services/sessionService.ts +476 -0
- package/src/server/services/statsService.ts +53 -0
- package/src/server/storage/boulderParser.ts +113 -0
- package/src/server/storage/messageParser.ts +169 -0
- package/src/server/storage/partParser.ts +519 -0
- package/src/server/storage/sessionParser.ts +180 -0
- package/src/server/utils/sessionStatus.ts +123 -0
- package/src/server/validation.ts +34 -0
- package/src/server/watcher.ts +160 -0
- package/src/shared/constants.ts +22 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/types/index.ts +326 -0
- package/src/shared/utils/RingBuffer.ts +79 -0
- package/src/shared/utils/activityUtils.ts +66 -0
- package/src/shared/utils/burstGrouping.ts +99 -0
- package/src/shared/utils/formatTime.ts +27 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RingBuffer - Generic circular buffer with fixed capacity
|
|
3
|
+
* Automatically drops oldest items when capacity is exceeded
|
|
4
|
+
*
|
|
5
|
+
* Used for storing recent logs and tool calls with a maximum size limit.
|
|
6
|
+
* Ported from Go implementation: internal/state/state.go:50-90
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { RINGBUFFER_CAPACITY } from "../constants";
|
|
10
|
+
|
|
11
|
+
export class RingBuffer<T> {
|
|
12
|
+
private buffer: T[] = [];
|
|
13
|
+
private capacity: number;
|
|
14
|
+
private head: number = 0;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a new RingBuffer with specified capacity
|
|
18
|
+
* @param capacity Maximum number of items to store (default: RINGBUFFER_CAPACITY)
|
|
19
|
+
*/
|
|
20
|
+
constructor(capacity: number = RINGBUFFER_CAPACITY) {
|
|
21
|
+
this.capacity = Math.max(1, capacity);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add an item to the buffer
|
|
26
|
+
* If buffer is full, drops the oldest item
|
|
27
|
+
* @param item The item to add
|
|
28
|
+
*/
|
|
29
|
+
push(item: T): void {
|
|
30
|
+
if (this.buffer.length < this.capacity) {
|
|
31
|
+
this.buffer.push(item);
|
|
32
|
+
} else {
|
|
33
|
+
// Buffer is full, overwrite oldest item
|
|
34
|
+
this.buffer[this.head] = item;
|
|
35
|
+
this.head = (this.head + 1) % this.capacity;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all items in order (oldest to newest)
|
|
41
|
+
* @returns Array of all items in chronological order
|
|
42
|
+
*/
|
|
43
|
+
getAll(): T[] {
|
|
44
|
+
if (this.buffer.length < this.capacity) {
|
|
45
|
+
// Buffer not full yet, return as-is
|
|
46
|
+
return [...this.buffer];
|
|
47
|
+
}
|
|
48
|
+
// Buffer is full, reconstruct in order starting from head
|
|
49
|
+
return [
|
|
50
|
+
...this.buffer.slice(this.head),
|
|
51
|
+
...this.buffer.slice(0, this.head),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the last n items (newest first)
|
|
57
|
+
* @param n Number of items to retrieve
|
|
58
|
+
* @returns Array of last n items in reverse chronological order
|
|
59
|
+
*/
|
|
60
|
+
getLatest(n: number): T[] {
|
|
61
|
+
const all = this.getAll();
|
|
62
|
+
return all.slice(Math.max(0, all.length - n)).reverse();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Clear all items from the buffer
|
|
67
|
+
*/
|
|
68
|
+
clear(): void {
|
|
69
|
+
this.buffer = [];
|
|
70
|
+
this.head = 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get current number of items in the buffer
|
|
75
|
+
*/
|
|
76
|
+
get size(): number {
|
|
77
|
+
return this.buffer.length;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ActivitySession, ActivityItem } from '../types';
|
|
2
|
+
|
|
3
|
+
export function synthesizeActivityItems(
|
|
4
|
+
sessions: ActivitySession[]
|
|
5
|
+
): ActivityItem[] {
|
|
6
|
+
const items: ActivityItem[] = [];
|
|
7
|
+
const sessionMap = new Map<string, ActivitySession>();
|
|
8
|
+
const seenToolCallIds = new Set<string>();
|
|
9
|
+
|
|
10
|
+
sessions.forEach((session) => {
|
|
11
|
+
sessionMap.set(session.id, session);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
sessions.forEach((session) => {
|
|
15
|
+
if (session.parentID && sessionMap.has(session.parentID)) {
|
|
16
|
+
const parent = sessionMap.get(session.parentID)!;
|
|
17
|
+
items.push({
|
|
18
|
+
id: `spawn-${session.id}`,
|
|
19
|
+
type: "agent-spawn",
|
|
20
|
+
timestamp: session.createdAt,
|
|
21
|
+
agentName: parent.agent,
|
|
22
|
+
spawnedAgentName: session.agent,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (session.toolCalls && session.toolCalls.length > 0) {
|
|
27
|
+
session.toolCalls.forEach((toolCall) => {
|
|
28
|
+
if (seenToolCallIds.has(toolCall.id)) return;
|
|
29
|
+
seenToolCallIds.add(toolCall.id);
|
|
30
|
+
|
|
31
|
+
items.push({
|
|
32
|
+
id: toolCall.id,
|
|
33
|
+
type: "tool-call",
|
|
34
|
+
timestamp: new Date(toolCall.timestamp),
|
|
35
|
+
agentName: toolCall.agentName,
|
|
36
|
+
toolName: toolCall.name,
|
|
37
|
+
state: toolCall.state,
|
|
38
|
+
summary: toolCall.summary,
|
|
39
|
+
input: toolCall.input,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (session.status === "completed") {
|
|
45
|
+
const durationMs = session.updatedAt
|
|
46
|
+
? new Date(session.updatedAt).getTime() -
|
|
47
|
+
new Date(session.createdAt).getTime()
|
|
48
|
+
: undefined;
|
|
49
|
+
|
|
50
|
+
items.push({
|
|
51
|
+
id: `complete-${session.id}`,
|
|
52
|
+
type: "agent-complete",
|
|
53
|
+
timestamp: session.updatedAt,
|
|
54
|
+
agentName: session.agent,
|
|
55
|
+
status: session.status,
|
|
56
|
+
durationMs,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
items.sort(
|
|
62
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return items;
|
|
66
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivityItem,
|
|
3
|
+
BurstEntry,
|
|
4
|
+
MilestoneEntry,
|
|
5
|
+
StreamEntry,
|
|
6
|
+
ToolCallActivity,
|
|
7
|
+
} from "../types";
|
|
8
|
+
|
|
9
|
+
function isMilestone(item: ActivityItem): boolean {
|
|
10
|
+
if (item.type === "agent-spawn" || item.type === "agent-complete") {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return item.type === "tool-call" && item.state === "error";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createBurstEntry(items: ToolCallActivity[]): BurstEntry {
|
|
18
|
+
const first = items[0]!;
|
|
19
|
+
const last = items[items.length - 1]!;
|
|
20
|
+
const toolBreakdown: Record<string, number> = {};
|
|
21
|
+
|
|
22
|
+
let pendingCount = 0;
|
|
23
|
+
let errorCount = 0;
|
|
24
|
+
|
|
25
|
+
for (const item of items) {
|
|
26
|
+
toolBreakdown[item.toolName] = (toolBreakdown[item.toolName] ?? 0) + 1;
|
|
27
|
+
|
|
28
|
+
if (item.state === "pending") {
|
|
29
|
+
pendingCount += 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (item.state === "error") {
|
|
33
|
+
errorCount += 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id: first.id,
|
|
39
|
+
type: "burst",
|
|
40
|
+
agentName: first.agentName,
|
|
41
|
+
items,
|
|
42
|
+
toolBreakdown,
|
|
43
|
+
durationMs: new Date(last.timestamp).getTime() - new Date(first.timestamp).getTime(),
|
|
44
|
+
firstTimestamp: new Date(first.timestamp),
|
|
45
|
+
lastTimestamp: new Date(last.timestamp),
|
|
46
|
+
pendingCount,
|
|
47
|
+
errorCount,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function groupIntoBursts(items: ActivityItem[]): StreamEntry[] {
|
|
52
|
+
const entries: StreamEntry[] = [];
|
|
53
|
+
let currentBurstItems: ToolCallActivity[] = [];
|
|
54
|
+
const chronologicalItems = [...items].sort(
|
|
55
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const flushCurrentBurst = () => {
|
|
59
|
+
if (currentBurstItems.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
entries.push(createBurstEntry(currentBurstItems));
|
|
64
|
+
currentBurstItems = [];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
for (const item of chronologicalItems) {
|
|
68
|
+
if (isMilestone(item)) {
|
|
69
|
+
flushCurrentBurst();
|
|
70
|
+
|
|
71
|
+
const milestone: MilestoneEntry = {
|
|
72
|
+
id: item.id,
|
|
73
|
+
type: "milestone",
|
|
74
|
+
item,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
entries.push(milestone);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (item.type !== "tool-call") {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const currentAgentName = currentBurstItems[0]?.agentName;
|
|
86
|
+
const shouldStartNewBurst =
|
|
87
|
+
currentBurstItems.length > 0 && currentAgentName !== item.agentName;
|
|
88
|
+
|
|
89
|
+
if (shouldStartNewBurst) {
|
|
90
|
+
flushCurrentBurst();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
currentBurstItems.push(item);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
flushCurrentBurst();
|
|
97
|
+
|
|
98
|
+
return entries;
|
|
99
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** "<1m", "3m", "2h", "3d", or locale date */
|
|
2
|
+
export function formatRelativeTime(date: Date | string): string {
|
|
3
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
const diff = now - d.getTime();
|
|
6
|
+
const minutes = Math.floor(diff / 60000);
|
|
7
|
+
const hours = Math.floor(minutes / 60);
|
|
8
|
+
|
|
9
|
+
if (minutes < 1) return '<1m';
|
|
10
|
+
if (minutes < 60) return `${minutes}m`;
|
|
11
|
+
if (hours < 24) return `${hours}h`;
|
|
12
|
+
const days = Math.floor(hours / 24);
|
|
13
|
+
if (days < 7) return `${days}d`;
|
|
14
|
+
return d.toLocaleDateString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** "just now", "3m ago", "2h ago", "3d ago" */
|
|
18
|
+
export function formatRelativeTimeVerbose(date: Date | string): string {
|
|
19
|
+
const d = new Date(date);
|
|
20
|
+
const now = new Date();
|
|
21
|
+
const diffInSeconds = Math.max(0, Math.floor((now.getTime() - d.getTime()) / 1000));
|
|
22
|
+
|
|
23
|
+
if (diffInSeconds < 60) return 'just now';
|
|
24
|
+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
|
|
25
|
+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
|
|
26
|
+
return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
27
|
+
}
|