opencode-swarm-plugin 0.42.8 → 0.43.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,240 @@
1
+ /**
2
+ * Replay Tools - Event replay with timing simulation
3
+ *
4
+ * TDD GREEN: Minimal implementation to pass tests
5
+ */
6
+
7
+ import { readFileSync } from "node:fs";
8
+ import { existsSync } from "node:fs";
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export type ReplaySpeed = "1x" | "2x" | "instant";
15
+
16
+ export interface ReplayEvent {
17
+ session_id: string;
18
+ epic_id: string;
19
+ timestamp: string;
20
+ event_type: "DECISION" | "VIOLATION" | "OUTCOME" | "COMPACTION";
21
+ decision_type?: string;
22
+ violation_type?: string;
23
+ outcome_type?: string;
24
+ payload: Record<string, unknown>;
25
+ delta_ms: number;
26
+ }
27
+
28
+ export interface ReplayFilter {
29
+ type?: Array<"DECISION" | "VIOLATION" | "OUTCOME" | "COMPACTION">;
30
+ agent?: string;
31
+ since?: Date;
32
+ until?: Date;
33
+ }
34
+
35
+ // ============================================================================
36
+ // fetchEpicEvents - Read JSONL and calculate deltas
37
+ // ============================================================================
38
+
39
+ export async function fetchEpicEvents(
40
+ epicId: string,
41
+ sessionFile: string,
42
+ ): Promise<ReplayEvent[]> {
43
+ // Handle non-existent files
44
+ if (!existsSync(sessionFile)) {
45
+ return [];
46
+ }
47
+
48
+ try {
49
+ const content = readFileSync(sessionFile, "utf-8");
50
+ const lines = content.trim().split("\n").filter(Boolean);
51
+
52
+ if (lines.length === 0) {
53
+ return [];
54
+ }
55
+
56
+ // Parse JSONL
57
+ const events = lines.map((line) => JSON.parse(line));
58
+
59
+ // Sort by timestamp (chronological order)
60
+ events.sort(
61
+ (a, b) =>
62
+ new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
63
+ );
64
+
65
+ // Calculate delta_ms between events
66
+ const replayEvents: ReplayEvent[] = [];
67
+ let prevTime = 0;
68
+
69
+ for (const event of events) {
70
+ const currTime = new Date(event.timestamp).getTime();
71
+ const delta_ms = prevTime === 0 ? 0 : currTime - prevTime;
72
+
73
+ replayEvents.push({
74
+ ...event,
75
+ delta_ms,
76
+ });
77
+
78
+ prevTime = currTime;
79
+ }
80
+
81
+ return replayEvents;
82
+ } catch (error) {
83
+ // Handle errors (e.g., invalid JSON)
84
+ return [];
85
+ }
86
+ }
87
+
88
+ // ============================================================================
89
+ // filterEvents - Filter by type/agent/time (AND logic)
90
+ // ============================================================================
91
+
92
+ export function filterEvents(
93
+ events: ReplayEvent[],
94
+ filter: ReplayFilter,
95
+ ): ReplayEvent[] {
96
+ return events.filter((event) => {
97
+ // Filter by event type
98
+ if (filter.type && !filter.type.includes(event.event_type)) {
99
+ return false;
100
+ }
101
+
102
+ // Filter by agent name (from payload)
103
+ if (filter.agent) {
104
+ const payload = event.payload as any;
105
+ if (payload.agent_name !== filter.agent) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ // Filter by time range (since)
111
+ if (filter.since) {
112
+ const eventTime = new Date(event.timestamp);
113
+ if (eventTime < filter.since) {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ // Filter by time range (until)
119
+ if (filter.until) {
120
+ const eventTime = new Date(event.timestamp);
121
+ if (eventTime > filter.until) {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ return true;
127
+ });
128
+ }
129
+
130
+ // ============================================================================
131
+ // replayWithTiming - Async generator with speed control
132
+ // ============================================================================
133
+
134
+ export async function* replayWithTiming(
135
+ events: ReplayEvent[],
136
+ speed: ReplaySpeed,
137
+ ): AsyncGenerator<ReplayEvent> {
138
+ if (events.length === 0) {
139
+ return;
140
+ }
141
+
142
+ const startTime = Date.now();
143
+ let cumulativeDelay = 0;
144
+
145
+ for (const event of events) {
146
+ // Calculate target time for this event
147
+ cumulativeDelay += event.delta_ms;
148
+
149
+ let targetDelay = cumulativeDelay;
150
+ if (speed === "2x") {
151
+ targetDelay = targetDelay / 2;
152
+ } else if (speed === "instant") {
153
+ targetDelay = 0;
154
+ }
155
+
156
+ // Calculate actual delay needed (accounting for time already elapsed)
157
+ const elapsed = Date.now() - startTime;
158
+ const delay = targetDelay - elapsed;
159
+
160
+ // Wait for the delay if needed (with small buffer for overhead)
161
+ if (delay > 3) {
162
+ // Subtract 3ms buffer to account for async overhead
163
+ const adjustedDelay = delay - 3;
164
+
165
+ // Use Bun.sleep for more precise timing in Bun runtime
166
+ if (typeof Bun !== "undefined" && typeof Bun.sleep === "function") {
167
+ await Bun.sleep(adjustedDelay);
168
+ } else {
169
+ await new Promise((resolve) => setTimeout(resolve, adjustedDelay));
170
+ }
171
+ }
172
+
173
+ // Yield the event
174
+ yield event;
175
+ }
176
+ }
177
+
178
+ // ============================================================================
179
+ // formatReplayEvent - ANSI colors + box-drawing + relationships
180
+ // ============================================================================
181
+
182
+ export function formatReplayEvent(event: ReplayEvent): string {
183
+ // ANSI color codes
184
+ const colors = {
185
+ DECISION: "\x1b[34m", // Blue
186
+ VIOLATION: "\x1b[31m", // Red
187
+ OUTCOME: "\x1b[32m", // Green
188
+ COMPACTION: "\x1b[33m", // Yellow
189
+ reset: "\x1b[0m",
190
+ gray: "\x1b[90m",
191
+ };
192
+
193
+ const color = colors[event.event_type] || colors.reset;
194
+
195
+ // Format timestamp (remove 'Z' suffix and put it BEFORE color codes for regex match)
196
+ const timestamp = new Date(event.timestamp)
197
+ .toISOString()
198
+ .split("T")[1]
199
+ .replace("Z", "");
200
+ const timePrefix = `[${timestamp}]`;
201
+
202
+ // Extract relevant payload fields
203
+ const payload = event.payload as any;
204
+ const beadId = payload.bead_id || "";
205
+ const agentName = payload.agent_name || "";
206
+ const strategyUsed = payload.strategy_used || "";
207
+ const subtaskCount = payload.subtask_count;
208
+
209
+ // Build formatted output with box-drawing characters
210
+ // Put timestamp BEFORE any color codes so regex matches
211
+ let output = `${timePrefix} `;
212
+ output += `${color}┌─ ${event.event_type}${colors.reset}\n`;
213
+
214
+ // Epic relationship
215
+ output += `${colors.gray}│${colors.reset} epic: ${event.epic_id}\n`;
216
+
217
+ // Bead relationship (if present)
218
+ if (beadId) {
219
+ output += `${colors.gray}│${colors.reset} bead: ${beadId}\n`;
220
+ }
221
+
222
+ // Agent (if present)
223
+ if (agentName) {
224
+ output += `${colors.gray}│${colors.reset} agent: ${agentName}\n`;
225
+ }
226
+
227
+ // Strategy (if present)
228
+ if (strategyUsed) {
229
+ output += `${colors.gray}│${colors.reset} strategy: ${strategyUsed}\n`;
230
+ }
231
+
232
+ // Subtask count (if present)
233
+ if (subtaskCount !== undefined) {
234
+ output += `${colors.gray}│${colors.reset} subtasks: ${subtaskCount}\n`;
235
+ }
236
+
237
+ output += `${colors.gray}└─${colors.reset}`;
238
+
239
+ return output;
240
+ }