@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.
Files changed (115) hide show
  1. package/dist/__fixtures__/claude-outputs.d.ts +49 -0
  2. package/dist/__fixtures__/claude-outputs.d.ts.map +1 -0
  3. package/dist/__fixtures__/claude-outputs.js +443 -0
  4. package/dist/__fixtures__/claude-outputs.js.map +1 -0
  5. package/dist/__fixtures__/codex-outputs.d.ts +9 -0
  6. package/dist/__fixtures__/codex-outputs.d.ts.map +1 -0
  7. package/dist/__fixtures__/codex-outputs.js +94 -0
  8. package/dist/__fixtures__/codex-outputs.js.map +1 -0
  9. package/dist/__fixtures__/gemini-outputs.d.ts +19 -0
  10. package/dist/__fixtures__/gemini-outputs.d.ts.map +1 -0
  11. package/dist/__fixtures__/gemini-outputs.js +144 -0
  12. package/dist/__fixtures__/gemini-outputs.js.map +1 -0
  13. package/dist/__fixtures__/index.d.ts +68 -0
  14. package/dist/__fixtures__/index.d.ts.map +1 -0
  15. package/dist/__fixtures__/index.js +44 -0
  16. package/dist/__fixtures__/index.js.map +1 -0
  17. package/dist/auth-detection.d.ts +49 -0
  18. package/dist/auth-detection.d.ts.map +1 -0
  19. package/dist/auth-detection.js +199 -0
  20. package/dist/auth-detection.js.map +1 -0
  21. package/dist/base-wrapper.d.ts +225 -0
  22. package/dist/base-wrapper.d.ts.map +1 -0
  23. package/dist/base-wrapper.js +572 -0
  24. package/dist/base-wrapper.js.map +1 -0
  25. package/dist/client.d.ts +254 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/client.js +801 -0
  28. package/dist/client.js.map +1 -0
  29. package/dist/id-generator.d.ts +35 -0
  30. package/dist/id-generator.d.ts.map +1 -0
  31. package/dist/id-generator.js +60 -0
  32. package/dist/id-generator.js.map +1 -0
  33. package/dist/idle-detector.d.ts +110 -0
  34. package/dist/idle-detector.d.ts.map +1 -0
  35. package/dist/idle-detector.js +304 -0
  36. package/dist/idle-detector.js.map +1 -0
  37. package/dist/inbox.d.ts +37 -0
  38. package/dist/inbox.d.ts.map +1 -0
  39. package/dist/inbox.js +73 -0
  40. package/dist/inbox.js.map +1 -0
  41. package/dist/index.d.ts +37 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +47 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/parser.d.ts +236 -0
  46. package/dist/parser.d.ts.map +1 -0
  47. package/dist/parser.js +1238 -0
  48. package/dist/parser.js.map +1 -0
  49. package/dist/prompt-composer.d.ts +67 -0
  50. package/dist/prompt-composer.d.ts.map +1 -0
  51. package/dist/prompt-composer.js +168 -0
  52. package/dist/prompt-composer.js.map +1 -0
  53. package/dist/relay-pty-orchestrator.d.ts +407 -0
  54. package/dist/relay-pty-orchestrator.d.ts.map +1 -0
  55. package/dist/relay-pty-orchestrator.js +1885 -0
  56. package/dist/relay-pty-orchestrator.js.map +1 -0
  57. package/dist/shared.d.ts +201 -0
  58. package/dist/shared.d.ts.map +1 -0
  59. package/dist/shared.js +341 -0
  60. package/dist/shared.js.map +1 -0
  61. package/dist/stuck-detector.d.ts +161 -0
  62. package/dist/stuck-detector.d.ts.map +1 -0
  63. package/dist/stuck-detector.js +402 -0
  64. package/dist/stuck-detector.js.map +1 -0
  65. package/dist/tmux-resolver.d.ts +55 -0
  66. package/dist/tmux-resolver.d.ts.map +1 -0
  67. package/dist/tmux-resolver.js +175 -0
  68. package/dist/tmux-resolver.js.map +1 -0
  69. package/dist/tmux-wrapper.d.ts +345 -0
  70. package/dist/tmux-wrapper.d.ts.map +1 -0
  71. package/dist/tmux-wrapper.js +1747 -0
  72. package/dist/tmux-wrapper.js.map +1 -0
  73. package/dist/trajectory-integration.d.ts +292 -0
  74. package/dist/trajectory-integration.d.ts.map +1 -0
  75. package/dist/trajectory-integration.js +979 -0
  76. package/dist/trajectory-integration.js.map +1 -0
  77. package/dist/wrapper-types.d.ts +41 -0
  78. package/dist/wrapper-types.d.ts.map +1 -0
  79. package/dist/wrapper-types.js +7 -0
  80. package/dist/wrapper-types.js.map +1 -0
  81. package/package.json +63 -0
  82. package/src/__fixtures__/claude-outputs.ts +471 -0
  83. package/src/__fixtures__/codex-outputs.ts +99 -0
  84. package/src/__fixtures__/gemini-outputs.ts +151 -0
  85. package/src/__fixtures__/index.ts +47 -0
  86. package/src/auth-detection.ts +244 -0
  87. package/src/base-wrapper.test.ts +540 -0
  88. package/src/base-wrapper.ts +741 -0
  89. package/src/client.test.ts +262 -0
  90. package/src/client.ts +984 -0
  91. package/src/id-generator.test.ts +71 -0
  92. package/src/id-generator.ts +69 -0
  93. package/src/idle-detector.test.ts +390 -0
  94. package/src/idle-detector.ts +370 -0
  95. package/src/inbox.test.ts +233 -0
  96. package/src/inbox.ts +89 -0
  97. package/src/index.ts +170 -0
  98. package/src/parser.regression.test.ts +251 -0
  99. package/src/parser.test.ts +1359 -0
  100. package/src/parser.ts +1477 -0
  101. package/src/prompt-composer.test.ts +219 -0
  102. package/src/prompt-composer.ts +231 -0
  103. package/src/relay-pty-orchestrator.test.ts +1027 -0
  104. package/src/relay-pty-orchestrator.ts +2270 -0
  105. package/src/shared.test.ts +221 -0
  106. package/src/shared.ts +454 -0
  107. package/src/stuck-detector.test.ts +303 -0
  108. package/src/stuck-detector.ts +511 -0
  109. package/src/tmux-resolver.test.ts +104 -0
  110. package/src/tmux-resolver.ts +207 -0
  111. package/src/tmux-wrapper.test.ts +316 -0
  112. package/src/tmux-wrapper.ts +2010 -0
  113. package/src/trajectory-detection.test.ts +151 -0
  114. package/src/trajectory-integration.ts +1261 -0
  115. 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
+ });