@agent-relay/wrapper 0.1.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/dist/__fixtures__/claude-outputs.d.ts +49 -0
- package/dist/__fixtures__/claude-outputs.d.ts.map +1 -0
- package/dist/__fixtures__/claude-outputs.js +443 -0
- package/dist/__fixtures__/claude-outputs.js.map +1 -0
- package/dist/__fixtures__/codex-outputs.d.ts +9 -0
- package/dist/__fixtures__/codex-outputs.d.ts.map +1 -0
- package/dist/__fixtures__/codex-outputs.js +94 -0
- package/dist/__fixtures__/codex-outputs.js.map +1 -0
- package/dist/__fixtures__/gemini-outputs.d.ts +19 -0
- package/dist/__fixtures__/gemini-outputs.d.ts.map +1 -0
- package/dist/__fixtures__/gemini-outputs.js +144 -0
- package/dist/__fixtures__/gemini-outputs.js.map +1 -0
- package/dist/__fixtures__/index.d.ts +68 -0
- package/dist/__fixtures__/index.d.ts.map +1 -0
- package/dist/__fixtures__/index.js +44 -0
- package/dist/__fixtures__/index.js.map +1 -0
- package/dist/auth-detection.d.ts +49 -0
- package/dist/auth-detection.d.ts.map +1 -0
- package/dist/auth-detection.js +199 -0
- package/dist/auth-detection.js.map +1 -0
- package/dist/base-wrapper.d.ts +225 -0
- package/dist/base-wrapper.d.ts.map +1 -0
- package/dist/base-wrapper.js +572 -0
- package/dist/base-wrapper.js.map +1 -0
- package/dist/client.d.ts +254 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +801 -0
- package/dist/client.js.map +1 -0
- package/dist/id-generator.d.ts +35 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +60 -0
- package/dist/id-generator.js.map +1 -0
- package/dist/idle-detector.d.ts +110 -0
- package/dist/idle-detector.d.ts.map +1 -0
- package/dist/idle-detector.js +304 -0
- package/dist/idle-detector.js.map +1 -0
- package/dist/inbox.d.ts +37 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +73 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +236 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +1238 -0
- package/dist/parser.js.map +1 -0
- package/dist/prompt-composer.d.ts +67 -0
- package/dist/prompt-composer.d.ts.map +1 -0
- package/dist/prompt-composer.js +168 -0
- package/dist/prompt-composer.js.map +1 -0
- package/dist/relay-pty-orchestrator.d.ts +407 -0
- package/dist/relay-pty-orchestrator.d.ts.map +1 -0
- package/dist/relay-pty-orchestrator.js +1885 -0
- package/dist/relay-pty-orchestrator.js.map +1 -0
- package/dist/shared.d.ts +201 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +341 -0
- package/dist/shared.js.map +1 -0
- package/dist/stuck-detector.d.ts +161 -0
- package/dist/stuck-detector.d.ts.map +1 -0
- package/dist/stuck-detector.js +402 -0
- package/dist/stuck-detector.js.map +1 -0
- package/dist/tmux-resolver.d.ts +55 -0
- package/dist/tmux-resolver.d.ts.map +1 -0
- package/dist/tmux-resolver.js +175 -0
- package/dist/tmux-resolver.js.map +1 -0
- package/dist/tmux-wrapper.d.ts +345 -0
- package/dist/tmux-wrapper.d.ts.map +1 -0
- package/dist/tmux-wrapper.js +1747 -0
- package/dist/tmux-wrapper.js.map +1 -0
- package/dist/trajectory-integration.d.ts +292 -0
- package/dist/trajectory-integration.d.ts.map +1 -0
- package/dist/trajectory-integration.js +979 -0
- package/dist/trajectory-integration.js.map +1 -0
- package/dist/wrapper-types.d.ts +41 -0
- package/dist/wrapper-types.d.ts.map +1 -0
- package/dist/wrapper-types.js +7 -0
- package/dist/wrapper-types.js.map +1 -0
- package/package.json +63 -0
- package/src/__fixtures__/claude-outputs.ts +471 -0
- package/src/__fixtures__/codex-outputs.ts +99 -0
- package/src/__fixtures__/gemini-outputs.ts +151 -0
- package/src/__fixtures__/index.ts +47 -0
- package/src/auth-detection.ts +244 -0
- package/src/base-wrapper.test.ts +540 -0
- package/src/base-wrapper.ts +741 -0
- package/src/client.test.ts +262 -0
- package/src/client.ts +984 -0
- package/src/id-generator.test.ts +71 -0
- package/src/id-generator.ts +69 -0
- package/src/idle-detector.test.ts +390 -0
- package/src/idle-detector.ts +370 -0
- package/src/inbox.test.ts +233 -0
- package/src/inbox.ts +89 -0
- package/src/index.ts +170 -0
- package/src/parser.regression.test.ts +251 -0
- package/src/parser.test.ts +1359 -0
- package/src/parser.ts +1477 -0
- package/src/prompt-composer.test.ts +219 -0
- package/src/prompt-composer.ts +231 -0
- package/src/relay-pty-orchestrator.test.ts +1027 -0
- package/src/relay-pty-orchestrator.ts +2270 -0
- package/src/shared.test.ts +221 -0
- package/src/shared.ts +454 -0
- package/src/stuck-detector.test.ts +303 -0
- package/src/stuck-detector.ts +511 -0
- package/src/tmux-resolver.test.ts +104 -0
- package/src/tmux-resolver.ts +207 -0
- package/src/tmux-wrapper.test.ts +316 -0
- package/src/tmux-wrapper.ts +2010 -0
- package/src/trajectory-detection.test.ts +151 -0
- package/src/trajectory-integration.ts +1261 -0
- package/src/wrapper-types.ts +45 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { StuckDetector, type StuckEvent } from './stuck-detector';
|
|
3
|
+
|
|
4
|
+
describe('StuckDetector', () => {
|
|
5
|
+
let detector: StuckDetector;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
detector = new StuckDetector({
|
|
10
|
+
checkIntervalMs: 1000, // 1 second for faster tests
|
|
11
|
+
extendedIdleMs: 5000, // 5 seconds for faster tests
|
|
12
|
+
toolLoopThreshold: 5, // Lower threshold for testing
|
|
13
|
+
toolLoopWindowMs: 10000, // 10 second window
|
|
14
|
+
outputFloodLinesPerMinute: 100, // Lower threshold for testing
|
|
15
|
+
outputFloodMinDurationMs: 2000, // 2 seconds
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
detector.stop();
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('extended_idle detection', () => {
|
|
25
|
+
it('should detect extended idle after configured duration', () => {
|
|
26
|
+
const stuckEvents: StuckEvent[] = [];
|
|
27
|
+
detector.on('stuck', (event) => stuckEvents.push(event));
|
|
28
|
+
|
|
29
|
+
detector.start();
|
|
30
|
+
|
|
31
|
+
// Advance time past idle threshold
|
|
32
|
+
vi.advanceTimersByTime(6000);
|
|
33
|
+
|
|
34
|
+
expect(stuckEvents).toHaveLength(1);
|
|
35
|
+
expect(stuckEvents[0].reason).toBe('extended_idle');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should not trigger if output is received', () => {
|
|
39
|
+
const stuckEvents: StuckEvent[] = [];
|
|
40
|
+
detector.on('stuck', (event) => stuckEvents.push(event));
|
|
41
|
+
|
|
42
|
+
detector.start();
|
|
43
|
+
|
|
44
|
+
// Send output before idle threshold
|
|
45
|
+
vi.advanceTimersByTime(3000);
|
|
46
|
+
detector.onOutput('some output');
|
|
47
|
+
|
|
48
|
+
// Advance more time
|
|
49
|
+
vi.advanceTimersByTime(3000);
|
|
50
|
+
|
|
51
|
+
expect(stuckEvents).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('tool_loop detection', () => {
|
|
56
|
+
it('should detect when same file is operated on repeatedly', () => {
|
|
57
|
+
// Use higher loop threshold to avoid output_loop triggering first
|
|
58
|
+
const toolLoopDetector = new StuckDetector({
|
|
59
|
+
checkIntervalMs: 1000,
|
|
60
|
+
extendedIdleMs: 60000,
|
|
61
|
+
loopThreshold: 100, // Very high to prevent output_loop
|
|
62
|
+
toolLoopThreshold: 5,
|
|
63
|
+
toolLoopWindowMs: 30000,
|
|
64
|
+
});
|
|
65
|
+
const stuckEvents: StuckEvent[] = [];
|
|
66
|
+
toolLoopDetector.on('stuck', (event) => stuckEvents.push(event));
|
|
67
|
+
|
|
68
|
+
toolLoopDetector.start();
|
|
69
|
+
|
|
70
|
+
// Simulate repeated Write operations to the same file
|
|
71
|
+
// Each with unique content to avoid output_loop
|
|
72
|
+
for (let i = 0; i < 6; i++) {
|
|
73
|
+
toolLoopDetector.onOutput(`⏺ Write(~/Projects/test/file.ts) - change ${i}\nSome different content ${i}`);
|
|
74
|
+
vi.advanceTimersByTime(500);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Trigger check
|
|
78
|
+
vi.advanceTimersByTime(1000);
|
|
79
|
+
|
|
80
|
+
expect(stuckEvents).toHaveLength(1);
|
|
81
|
+
expect(stuckEvents[0].reason).toBe('tool_loop');
|
|
82
|
+
expect(stuckEvents[0].targetFile).toContain('file.ts');
|
|
83
|
+
expect(stuckEvents[0].toolName).toBe('Write');
|
|
84
|
+
|
|
85
|
+
toolLoopDetector.stop();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should detect mixed tool operations on same file', () => {
|
|
89
|
+
// Use higher loop threshold to avoid output_loop triggering first
|
|
90
|
+
const mixedToolDetector = new StuckDetector({
|
|
91
|
+
checkIntervalMs: 1000,
|
|
92
|
+
extendedIdleMs: 60000,
|
|
93
|
+
loopThreshold: 100, // Very high to prevent output_loop
|
|
94
|
+
toolLoopThreshold: 5,
|
|
95
|
+
toolLoopWindowMs: 30000,
|
|
96
|
+
});
|
|
97
|
+
const stuckEvents: StuckEvent[] = [];
|
|
98
|
+
mixedToolDetector.on('stuck', (event) => stuckEvents.push(event));
|
|
99
|
+
|
|
100
|
+
mixedToolDetector.start();
|
|
101
|
+
|
|
102
|
+
// Simulate Read and Write to the same file with different surrounding content
|
|
103
|
+
mixedToolDetector.onOutput('Processing step 1\n⏺ Read(~/Projects/test/file.ts)\nContent A');
|
|
104
|
+
mixedToolDetector.onOutput('Processing step 2\n⏺ Write(~/Projects/test/file.ts)\nContent B');
|
|
105
|
+
mixedToolDetector.onOutput('Processing step 3\n⏺ Read(~/Projects/test/file.ts)\nContent C');
|
|
106
|
+
mixedToolDetector.onOutput('Processing step 4\n⏺ Write(~/Projects/test/file.ts)\nContent D');
|
|
107
|
+
mixedToolDetector.onOutput('Processing step 5\n⏺ Read(~/Projects/test/file.ts)\nContent E');
|
|
108
|
+
|
|
109
|
+
vi.advanceTimersByTime(1000);
|
|
110
|
+
|
|
111
|
+
expect(stuckEvents).toHaveLength(1);
|
|
112
|
+
expect(stuckEvents[0].reason).toBe('tool_loop');
|
|
113
|
+
|
|
114
|
+
mixedToolDetector.stop();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should not trigger for different files', () => {
|
|
118
|
+
const stuckEvents: StuckEvent[] = [];
|
|
119
|
+
detector.on('stuck', (event) => stuckEvents.push(event));
|
|
120
|
+
|
|
121
|
+
detector.start();
|
|
122
|
+
|
|
123
|
+
// Simulate operations on different files
|
|
124
|
+
detector.onOutput('⏺ Write(~/Projects/test/file1.ts)');
|
|
125
|
+
detector.onOutput('⏺ Write(~/Projects/test/file2.ts)');
|
|
126
|
+
detector.onOutput('⏺ Write(~/Projects/test/file3.ts)');
|
|
127
|
+
detector.onOutput('⏺ Write(~/Projects/test/file4.ts)');
|
|
128
|
+
detector.onOutput('⏺ Write(~/Projects/test/file5.ts)');
|
|
129
|
+
|
|
130
|
+
vi.advanceTimersByTime(1000);
|
|
131
|
+
|
|
132
|
+
expect(stuckEvents).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should prune old invocations outside window', () => {
|
|
136
|
+
// Directly test the pruning behavior via getToolInvocations
|
|
137
|
+
const testDetector = new StuckDetector({
|
|
138
|
+
checkIntervalMs: 100000, // Very long - don't auto-check
|
|
139
|
+
extendedIdleMs: 100000,
|
|
140
|
+
toolLoopThreshold: 10,
|
|
141
|
+
toolLoopWindowMs: 5000, // 5 second window
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
testDetector.start();
|
|
145
|
+
|
|
146
|
+
// Add operations
|
|
147
|
+
testDetector.onOutput('⏺ Write(~/test/file.ts)\n');
|
|
148
|
+
testDetector.onOutput('⏺ Write(~/test/file.ts)\n');
|
|
149
|
+
testDetector.onOutput('⏺ Write(~/test/file.ts)\n');
|
|
150
|
+
|
|
151
|
+
expect(testDetector.getToolInvocations()).toHaveLength(3);
|
|
152
|
+
|
|
153
|
+
// Move past the window
|
|
154
|
+
vi.advanceTimersByTime(6000);
|
|
155
|
+
|
|
156
|
+
// Add new output to trigger pruning
|
|
157
|
+
testDetector.onOutput('⏺ Read(~/test/other.ts)\n');
|
|
158
|
+
|
|
159
|
+
// Old invocations should be pruned, only new one remains
|
|
160
|
+
const invocations = testDetector.getToolInvocations();
|
|
161
|
+
expect(invocations).toHaveLength(1);
|
|
162
|
+
expect(invocations[0].tool).toBe('Read');
|
|
163
|
+
|
|
164
|
+
testDetector.stop();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('output_flood detection', () => {
|
|
169
|
+
it('should detect abnormally high output rate', () => {
|
|
170
|
+
// Create detector with specific flood settings
|
|
171
|
+
const floodDetector = new StuckDetector({
|
|
172
|
+
checkIntervalMs: 1000,
|
|
173
|
+
extendedIdleMs: 100000, // Very long idle to not trigger
|
|
174
|
+
outputFloodLinesPerMinute: 100,
|
|
175
|
+
outputFloodMinDurationMs: 2000, // 2 seconds min
|
|
176
|
+
toolLoopThreshold: 1000, // Very high to not trigger
|
|
177
|
+
});
|
|
178
|
+
const stuckEvents: StuckEvent[] = [];
|
|
179
|
+
floodDetector.on('stuck', (event) => stuckEvents.push(event));
|
|
180
|
+
|
|
181
|
+
floodDetector.start();
|
|
182
|
+
|
|
183
|
+
// Generate lots of output lines immediately
|
|
184
|
+
// 1000 lines in 3 seconds = 20000 lines/minute
|
|
185
|
+
const manyLines = Array(1000).fill('output line').join('\n');
|
|
186
|
+
floodDetector.onOutput(manyLines);
|
|
187
|
+
|
|
188
|
+
// Wait past minimum duration and trigger check
|
|
189
|
+
vi.advanceTimersByTime(3000);
|
|
190
|
+
|
|
191
|
+
expect(stuckEvents).toHaveLength(1);
|
|
192
|
+
expect(stuckEvents[0].reason).toBe('output_flood');
|
|
193
|
+
expect(stuckEvents[0].linesPerMinute).toBeGreaterThan(100);
|
|
194
|
+
|
|
195
|
+
floodDetector.stop();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should not trigger before minimum duration', () => {
|
|
199
|
+
const stuckEvents: StuckEvent[] = [];
|
|
200
|
+
detector.on('stuck', (event) => stuckEvents.push(event));
|
|
201
|
+
|
|
202
|
+
detector.start();
|
|
203
|
+
|
|
204
|
+
// Generate lots of output before minimum duration
|
|
205
|
+
const manyLines = Array(300).fill('output line').join('\n');
|
|
206
|
+
detector.onOutput(manyLines);
|
|
207
|
+
|
|
208
|
+
vi.advanceTimersByTime(1000);
|
|
209
|
+
|
|
210
|
+
// Should not trigger yet
|
|
211
|
+
expect(stuckEvents).toHaveLength(0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should not trigger for normal output rates', () => {
|
|
215
|
+
const stuckEvents: StuckEvent[] = [];
|
|
216
|
+
detector.on('stuck', (event) => stuckEvents.push(event));
|
|
217
|
+
|
|
218
|
+
detector.start();
|
|
219
|
+
|
|
220
|
+
// Wait past minimum duration
|
|
221
|
+
vi.advanceTimersByTime(2500);
|
|
222
|
+
|
|
223
|
+
// Generate moderate output
|
|
224
|
+
detector.onOutput('line 1\nline 2\nline 3\n');
|
|
225
|
+
|
|
226
|
+
vi.advanceTimersByTime(1000);
|
|
227
|
+
|
|
228
|
+
expect(stuckEvents).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('unstuck emission', () => {
|
|
233
|
+
it('should emit unstuck when output resumes after being stuck', () => {
|
|
234
|
+
const stuckEvents: StuckEvent[] = [];
|
|
235
|
+
const unstuckEvents: { timestamp: number }[] = [];
|
|
236
|
+
|
|
237
|
+
detector.on('stuck', (event) => stuckEvents.push(event));
|
|
238
|
+
detector.on('unstuck', (event) => unstuckEvents.push(event));
|
|
239
|
+
|
|
240
|
+
detector.start();
|
|
241
|
+
|
|
242
|
+
// Become stuck (extended idle)
|
|
243
|
+
vi.advanceTimersByTime(6000);
|
|
244
|
+
expect(stuckEvents).toHaveLength(1);
|
|
245
|
+
|
|
246
|
+
// Resume output
|
|
247
|
+
detector.onOutput('back to work');
|
|
248
|
+
|
|
249
|
+
expect(unstuckEvents).toHaveLength(1);
|
|
250
|
+
expect(detector.getIsStuck()).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('getOutputStats', () => {
|
|
255
|
+
it('should return accurate output statistics', () => {
|
|
256
|
+
detector.start();
|
|
257
|
+
|
|
258
|
+
detector.onOutput('line 1\nline 2\nline 3\n');
|
|
259
|
+
vi.advanceTimersByTime(30000); // 30 seconds
|
|
260
|
+
|
|
261
|
+
const stats = detector.getOutputStats();
|
|
262
|
+
|
|
263
|
+
expect(stats.lineCount).toBe(3);
|
|
264
|
+
expect(stats.durationMs).toBeGreaterThanOrEqual(30000);
|
|
265
|
+
expect(stats.linesPerMinute).toBeLessThan(10); // 3 lines in 30 seconds = 6 lines/min
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('getToolInvocations', () => {
|
|
270
|
+
it('should return tracked tool invocations', () => {
|
|
271
|
+
detector.start();
|
|
272
|
+
|
|
273
|
+
detector.onOutput('⏺ Write(~/test/file.ts)');
|
|
274
|
+
detector.onOutput('⏺ Read(~/test/other.ts)');
|
|
275
|
+
|
|
276
|
+
const invocations = detector.getToolInvocations();
|
|
277
|
+
|
|
278
|
+
expect(invocations).toHaveLength(2);
|
|
279
|
+
expect(invocations[0].tool).toBe('Write');
|
|
280
|
+
expect(invocations[0].target).toContain('file.ts');
|
|
281
|
+
expect(invocations[1].tool).toBe('Read');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('reset', () => {
|
|
286
|
+
it('should clear all state', () => {
|
|
287
|
+
detector.start();
|
|
288
|
+
|
|
289
|
+
// Generate some activity
|
|
290
|
+
detector.onOutput('⏺ Write(~/test/file.ts)');
|
|
291
|
+
detector.onOutput('line 1\nline 2\n');
|
|
292
|
+
|
|
293
|
+
expect(detector.getToolInvocations()).toHaveLength(1);
|
|
294
|
+
expect(detector.getOutputStats().lineCount).toBe(2);
|
|
295
|
+
|
|
296
|
+
detector.reset();
|
|
297
|
+
|
|
298
|
+
expect(detector.getToolInvocations()).toHaveLength(0);
|
|
299
|
+
expect(detector.getOutputStats().lineCount).toBe(0);
|
|
300
|
+
expect(detector.getIsStuck()).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|