@link-assistant/agent 0.16.16 → 0.16.17
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/package.json +4 -2
- package/src/cli/continuous-mode.js +32 -12
- package/src/cli/input-queue.js +6 -3
- package/src/flag/flag.ts +9 -0
- package/src/index.js +22 -2
- package/src/provider/retry-fetch.ts +16 -0
- package/src/server/server.ts +3 -1
- package/src/session/retry.ts +2 -0
- package/src/util/timeout.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/agent",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.17",
|
|
4
4
|
"description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "bun run src/index.js",
|
|
12
|
-
"test": "bun test",
|
|
12
|
+
"test": "bun test tests/*.test.js tests/*.test.ts",
|
|
13
|
+
"test:integration": "bun test tests/integration/basic.test.js",
|
|
13
14
|
"lint": "eslint .",
|
|
14
15
|
"lint:fix": "eslint . --fix",
|
|
15
16
|
"format": "prettier --write .",
|
|
@@ -117,6 +118,7 @@
|
|
|
117
118
|
"eslint": "^9.38.0",
|
|
118
119
|
"eslint-config-prettier": "^10.1.8",
|
|
119
120
|
"eslint-plugin-prettier": "^5.5.4",
|
|
121
|
+
"eslint-plugin-promise": "^7.2.1",
|
|
120
122
|
"husky": "^9.1.7",
|
|
121
123
|
"lint-staged": "^16.2.6",
|
|
122
124
|
"prettier": "^3.6.2"
|
|
@@ -377,15 +377,24 @@ export async function runContinuousServerMode(
|
|
|
377
377
|
|
|
378
378
|
// Wait for stdin to end (EOF or close)
|
|
379
379
|
await new Promise((resolve) => {
|
|
380
|
+
let resolved = false;
|
|
381
|
+
const safeResolve = () => {
|
|
382
|
+
if (!resolved) {
|
|
383
|
+
resolved = true;
|
|
384
|
+
resolve();
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
380
388
|
const checkRunning = setInterval(() => {
|
|
381
389
|
if (!stdinReader.isRunning()) {
|
|
382
390
|
clearInterval(checkRunning);
|
|
383
391
|
// Wait for any pending messages to complete
|
|
384
392
|
const waitForPending = () => {
|
|
385
393
|
if (!isProcessing && pendingMessages.length === 0) {
|
|
386
|
-
|
|
394
|
+
safeResolve();
|
|
387
395
|
} else {
|
|
388
|
-
|
|
396
|
+
// Use .unref() to prevent keeping the event loop alive (#213)
|
|
397
|
+
setTimeout(waitForPending, 100).unref();
|
|
389
398
|
}
|
|
390
399
|
};
|
|
391
400
|
waitForPending();
|
|
@@ -394,8 +403,8 @@ export async function runContinuousServerMode(
|
|
|
394
403
|
// Allow process to exit naturally when no other work remains
|
|
395
404
|
checkRunning.unref();
|
|
396
405
|
|
|
397
|
-
//
|
|
398
|
-
|
|
406
|
+
// Handle SIGINT — use 'once' to avoid accumulating handlers (#213)
|
|
407
|
+
const sigintHandler = () => {
|
|
399
408
|
outputStatus(
|
|
400
409
|
{
|
|
401
410
|
type: 'status',
|
|
@@ -404,8 +413,9 @@ export async function runContinuousServerMode(
|
|
|
404
413
|
compactJson
|
|
405
414
|
);
|
|
406
415
|
clearInterval(checkRunning);
|
|
407
|
-
|
|
408
|
-
}
|
|
416
|
+
safeResolve();
|
|
417
|
+
};
|
|
418
|
+
process.once('SIGINT', sigintHandler);
|
|
409
419
|
});
|
|
410
420
|
} finally {
|
|
411
421
|
if (stdinReader) {
|
|
@@ -596,15 +606,24 @@ export async function runContinuousDirectMode(
|
|
|
596
606
|
|
|
597
607
|
// Wait for stdin to end (EOF or close)
|
|
598
608
|
await new Promise((resolve) => {
|
|
609
|
+
let resolved = false;
|
|
610
|
+
const safeResolve = () => {
|
|
611
|
+
if (!resolved) {
|
|
612
|
+
resolved = true;
|
|
613
|
+
resolve();
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
599
617
|
const checkRunning = setInterval(() => {
|
|
600
618
|
if (!stdinReader.isRunning()) {
|
|
601
619
|
clearInterval(checkRunning);
|
|
602
620
|
// Wait for any pending messages to complete
|
|
603
621
|
const waitForPending = () => {
|
|
604
622
|
if (!isProcessing && pendingMessages.length === 0) {
|
|
605
|
-
|
|
623
|
+
safeResolve();
|
|
606
624
|
} else {
|
|
607
|
-
|
|
625
|
+
// Use .unref() to prevent keeping the event loop alive (#213)
|
|
626
|
+
setTimeout(waitForPending, 100).unref();
|
|
608
627
|
}
|
|
609
628
|
};
|
|
610
629
|
waitForPending();
|
|
@@ -613,8 +632,8 @@ export async function runContinuousDirectMode(
|
|
|
613
632
|
// Allow process to exit naturally when no other work remains
|
|
614
633
|
checkRunning.unref();
|
|
615
634
|
|
|
616
|
-
//
|
|
617
|
-
|
|
635
|
+
// Handle SIGINT — use 'once' to avoid accumulating handlers (#213)
|
|
636
|
+
const sigintHandler = () => {
|
|
618
637
|
outputStatus(
|
|
619
638
|
{
|
|
620
639
|
type: 'status',
|
|
@@ -623,8 +642,9 @@ export async function runContinuousDirectMode(
|
|
|
623
642
|
compactJson
|
|
624
643
|
);
|
|
625
644
|
clearInterval(checkRunning);
|
|
626
|
-
|
|
627
|
-
}
|
|
645
|
+
safeResolve();
|
|
646
|
+
};
|
|
647
|
+
process.once('SIGINT', sigintHandler);
|
|
628
648
|
});
|
|
629
649
|
} finally {
|
|
630
650
|
if (stdinReader) {
|
package/src/cli/input-queue.js
CHANGED
|
@@ -178,11 +178,13 @@ export function createContinuousStdinReader(options = {}) {
|
|
|
178
178
|
isRunning = false;
|
|
179
179
|
};
|
|
180
180
|
|
|
181
|
+
const handleError = () => {
|
|
182
|
+
isRunning = false;
|
|
183
|
+
};
|
|
184
|
+
|
|
181
185
|
process.stdin.on('data', handleData);
|
|
182
186
|
process.stdin.on('end', handleEnd);
|
|
183
|
-
process.stdin.on('error',
|
|
184
|
-
isRunning = false;
|
|
185
|
-
});
|
|
187
|
+
process.stdin.on('error', handleError);
|
|
186
188
|
|
|
187
189
|
return {
|
|
188
190
|
queue: inputQueue,
|
|
@@ -191,6 +193,7 @@ export function createContinuousStdinReader(options = {}) {
|
|
|
191
193
|
inputQueue.flush();
|
|
192
194
|
process.stdin.removeListener('data', handleData);
|
|
193
195
|
process.stdin.removeListener('end', handleEnd);
|
|
196
|
+
process.stdin.removeListener('error', handleError);
|
|
194
197
|
},
|
|
195
198
|
isRunning: () => isRunning,
|
|
196
199
|
};
|
package/src/flag/flag.ts
CHANGED
|
@@ -194,4 +194,13 @@ export namespace Flag {
|
|
|
194
194
|
export function setCompactJson(value: boolean) {
|
|
195
195
|
_compactJson = value;
|
|
196
196
|
}
|
|
197
|
+
|
|
198
|
+
// Retry on rate limits - when disabled, 429 responses are returned immediately without retrying
|
|
199
|
+
// Enabled by default. Use --no-retry-on-rate-limits in integration tests to avoid waiting for rate limits.
|
|
200
|
+
export let RETRY_ON_RATE_LIMITS = true;
|
|
201
|
+
|
|
202
|
+
// Allow setting retry-on-rate-limits mode programmatically (e.g., from CLI --retry-on-rate-limits flag)
|
|
203
|
+
export function setRetryOnRateLimits(value: boolean) {
|
|
204
|
+
RETRY_ON_RATE_LIMITS = value;
|
|
205
|
+
}
|
|
197
206
|
}
|
package/src/index.js
CHANGED
|
@@ -309,7 +309,12 @@ async function runAgentMode(argv, request) {
|
|
|
309
309
|
},
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
-
// Explicitly exit to ensure process terminates
|
|
312
|
+
// Explicitly exit to ensure process terminates (#213)
|
|
313
|
+
Log.Default.info(() => ({
|
|
314
|
+
message: 'Agent exiting',
|
|
315
|
+
hasError,
|
|
316
|
+
uptimeSeconds: Math.round(process.uptime()),
|
|
317
|
+
}));
|
|
313
318
|
process.exit(hasError ? 1 : 0);
|
|
314
319
|
}
|
|
315
320
|
|
|
@@ -387,7 +392,12 @@ async function runContinuousAgentMode(argv) {
|
|
|
387
392
|
},
|
|
388
393
|
});
|
|
389
394
|
|
|
390
|
-
// Explicitly exit to ensure process terminates
|
|
395
|
+
// Explicitly exit to ensure process terminates (#213)
|
|
396
|
+
Log.Default.info(() => ({
|
|
397
|
+
message: 'Agent exiting',
|
|
398
|
+
hasError,
|
|
399
|
+
uptimeSeconds: Math.round(process.uptime()),
|
|
400
|
+
}));
|
|
391
401
|
process.exit(hasError ? 1 : 0);
|
|
392
402
|
}
|
|
393
403
|
|
|
@@ -708,6 +718,12 @@ async function main() {
|
|
|
708
718
|
description:
|
|
709
719
|
'Maximum total retry time in seconds for rate limit errors (default: 604800 = 7 days)',
|
|
710
720
|
})
|
|
721
|
+
.option('retry-on-rate-limits', {
|
|
722
|
+
type: 'boolean',
|
|
723
|
+
description:
|
|
724
|
+
'Retry AI completions API requests when rate limited (HTTP 429). Use --no-retry-on-rate-limits in integration tests to fail fast instead of waiting.',
|
|
725
|
+
default: true,
|
|
726
|
+
})
|
|
711
727
|
.option('output-response-model', {
|
|
712
728
|
type: 'boolean',
|
|
713
729
|
description: 'Include model info in step_finish output',
|
|
@@ -902,6 +918,10 @@ async function main() {
|
|
|
902
918
|
if (argv['summarize-session'] === true) {
|
|
903
919
|
Flag.setSummarizeSession(true);
|
|
904
920
|
}
|
|
921
|
+
// retry-on-rate-limits is enabled by default, only set if explicitly disabled
|
|
922
|
+
if (argv['retry-on-rate-limits'] === false) {
|
|
923
|
+
Flag.setRetryOnRateLimits(false);
|
|
924
|
+
}
|
|
905
925
|
await Log.init({
|
|
906
926
|
print: Flag.OPENCODE_VERBOSE,
|
|
907
927
|
level: Flag.OPENCODE_VERBOSE ? 'DEBUG' : 'INFO',
|
|
@@ -166,6 +166,8 @@ export namespace RetryFetch {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
const timeout = setTimeout(resolve, ms);
|
|
169
|
+
// Prevent sleep timer from keeping event loop alive (#213)
|
|
170
|
+
if (timeout.unref) timeout.unref();
|
|
169
171
|
|
|
170
172
|
if (signal) {
|
|
171
173
|
const abortHandler = () => {
|
|
@@ -224,6 +226,8 @@ export namespace RetryFetch {
|
|
|
224
226
|
)
|
|
225
227
|
);
|
|
226
228
|
}, remainingTimeout);
|
|
229
|
+
// Prevent timer from keeping event loop alive after cleanup (#213)
|
|
230
|
+
if (globalTimeoutId.unref) globalTimeoutId.unref();
|
|
227
231
|
timers.push(globalTimeoutId);
|
|
228
232
|
|
|
229
233
|
// Periodically check if user canceled (every 10 seconds)
|
|
@@ -244,6 +248,8 @@ export namespace RetryFetch {
|
|
|
244
248
|
// Check immediately and then every 10 seconds
|
|
245
249
|
checkUserCancellation();
|
|
246
250
|
const intervalId = setInterval(checkUserCancellation, 10_000);
|
|
251
|
+
// Prevent interval from keeping event loop alive after cleanup (#213)
|
|
252
|
+
if ((intervalId as any).unref) (intervalId as any).unref();
|
|
247
253
|
timers.push(intervalId as unknown as NodeJS.Timeout);
|
|
248
254
|
}
|
|
249
255
|
|
|
@@ -364,6 +370,16 @@ export namespace RetryFetch {
|
|
|
364
370
|
return response;
|
|
365
371
|
}
|
|
366
372
|
|
|
373
|
+
// If retry on rate limits is disabled, return 429 immediately
|
|
374
|
+
if (!Flag.RETRY_ON_RATE_LIMITS) {
|
|
375
|
+
log.info(() => ({
|
|
376
|
+
message:
|
|
377
|
+
'rate limit retry disabled (--no-retry-on-rate-limits), returning 429',
|
|
378
|
+
sessionID,
|
|
379
|
+
}));
|
|
380
|
+
return response;
|
|
381
|
+
}
|
|
382
|
+
|
|
367
383
|
// Check if we're within the global retry timeout
|
|
368
384
|
const elapsed = Date.now() - startTime;
|
|
369
385
|
if (elapsed >= maxRetryTimeout) {
|
package/src/server/server.ts
CHANGED
|
@@ -246,7 +246,9 @@ export namespace Server {
|
|
|
246
246
|
const server = Bun.serve({
|
|
247
247
|
port: opts.port,
|
|
248
248
|
hostname: opts.hostname,
|
|
249
|
-
|
|
249
|
+
// Use default idle timeout (255s) instead of 0 (infinite) to prevent
|
|
250
|
+
// keeping the event loop alive after server.stop(). See #213.
|
|
251
|
+
idleTimeout: 255,
|
|
250
252
|
fetch: App().fetch,
|
|
251
253
|
});
|
|
252
254
|
return server;
|
package/src/session/retry.ts
CHANGED
|
@@ -123,6 +123,8 @@ export namespace SessionRetry {
|
|
|
123
123
|
export async function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
124
124
|
return new Promise((resolve, reject) => {
|
|
125
125
|
const timeout = setTimeout(resolve, ms);
|
|
126
|
+
// Prevent sleep timer from keeping event loop alive (#213)
|
|
127
|
+
if (timeout.unref) timeout.unref();
|
|
126
128
|
signal.addEventListener(
|
|
127
129
|
'abort',
|
|
128
130
|
() => {
|
package/src/util/timeout.ts
CHANGED
|
@@ -9,6 +9,8 @@ export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
|
9
9
|
timeout = setTimeout(() => {
|
|
10
10
|
reject(new Error(`Operation timed out after ${ms}ms`));
|
|
11
11
|
}, ms);
|
|
12
|
+
// Prevent timeout from keeping the event loop alive (#213)
|
|
13
|
+
timeout.unref();
|
|
12
14
|
}),
|
|
13
15
|
]);
|
|
14
16
|
}
|