@link-assistant/agent 0.16.14 → 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/provider.ts +22 -15
- package/src/provider/retry-fetch.ts +16 -0
- package/src/server/server.ts +3 -1
- package/src/session/index.ts +42 -2
- 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',
|
package/src/provider/provider.ts
CHANGED
|
@@ -1272,22 +1272,26 @@ export namespace Provider {
|
|
|
1272
1272
|
}
|
|
1273
1273
|
}
|
|
1274
1274
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1275
|
+
// Use direct (non-lazy) logging for HTTP request/response to ensure output
|
|
1276
|
+
// is not lost when piped through external process wrappers (e.g., solve.mjs).
|
|
1277
|
+
// The verbose check is already done above, so lazy evaluation is not needed here.
|
|
1278
|
+
// See: https://github.com/link-assistant/agent/issues/211
|
|
1279
|
+
log.info('HTTP request', {
|
|
1277
1280
|
providerID: provider.id,
|
|
1278
1281
|
method,
|
|
1279
1282
|
url,
|
|
1280
1283
|
headers: sanitizedHeaders,
|
|
1281
1284
|
bodyPreview,
|
|
1282
|
-
})
|
|
1285
|
+
});
|
|
1283
1286
|
|
|
1284
1287
|
const startMs = Date.now();
|
|
1285
1288
|
try {
|
|
1286
1289
|
const response = await innerFetch(input, init);
|
|
1287
1290
|
const durationMs = Date.now() - startMs;
|
|
1288
1291
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1292
|
+
// Use direct (non-lazy) logging to ensure HTTP response details are captured
|
|
1293
|
+
// See: https://github.com/link-assistant/agent/issues/211
|
|
1294
|
+
log.info('HTTP response', {
|
|
1291
1295
|
providerID: provider.id,
|
|
1292
1296
|
method,
|
|
1293
1297
|
url,
|
|
@@ -1295,7 +1299,7 @@ export namespace Provider {
|
|
|
1295
1299
|
statusText: response.statusText,
|
|
1296
1300
|
durationMs,
|
|
1297
1301
|
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
1298
|
-
})
|
|
1302
|
+
});
|
|
1299
1303
|
|
|
1300
1304
|
// Log response body for debugging provider failures
|
|
1301
1305
|
// For streaming responses (SSE/event-stream), tee() the stream so the AI SDK
|
|
@@ -1335,14 +1339,15 @@ export namespace Provider {
|
|
|
1335
1339
|
}
|
|
1336
1340
|
}
|
|
1337
1341
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1342
|
+
// Use direct (non-lazy) logging for stream body
|
|
1343
|
+
// See: https://github.com/link-assistant/agent/issues/211
|
|
1344
|
+
log.info('HTTP response body (stream)', {
|
|
1340
1345
|
providerID: provider.id,
|
|
1341
1346
|
url,
|
|
1342
1347
|
bodyPreview: truncated
|
|
1343
1348
|
? bodyPreview + `... [truncated]`
|
|
1344
1349
|
: bodyPreview,
|
|
1345
|
-
})
|
|
1350
|
+
});
|
|
1346
1351
|
} catch {
|
|
1347
1352
|
// Ignore logging errors — do not affect the SDK stream
|
|
1348
1353
|
}
|
|
@@ -1362,12 +1367,13 @@ export namespace Provider {
|
|
|
1362
1367
|
? bodyText.slice(0, responseBodyMaxChars) +
|
|
1363
1368
|
`... [truncated, total ${bodyText.length} chars]`
|
|
1364
1369
|
: bodyText;
|
|
1365
|
-
|
|
1366
|
-
|
|
1370
|
+
// Use direct (non-lazy) logging for non-streaming body
|
|
1371
|
+
// See: https://github.com/link-assistant/agent/issues/211
|
|
1372
|
+
log.info('HTTP response body', {
|
|
1367
1373
|
providerID: provider.id,
|
|
1368
1374
|
url,
|
|
1369
1375
|
bodyPreview,
|
|
1370
|
-
})
|
|
1376
|
+
});
|
|
1371
1377
|
return new Response(bodyText, {
|
|
1372
1378
|
status: response.status,
|
|
1373
1379
|
statusText: response.statusText,
|
|
@@ -1379,8 +1385,9 @@ export namespace Provider {
|
|
|
1379
1385
|
return response;
|
|
1380
1386
|
} catch (error) {
|
|
1381
1387
|
const durationMs = Date.now() - startMs;
|
|
1382
|
-
|
|
1383
|
-
|
|
1388
|
+
// Use direct (non-lazy) logging for error path
|
|
1389
|
+
// See: https://github.com/link-assistant/agent/issues/211
|
|
1390
|
+
log.error('HTTP request failed', {
|
|
1384
1391
|
providerID: provider.id,
|
|
1385
1392
|
method,
|
|
1386
1393
|
url,
|
|
@@ -1389,7 +1396,7 @@ export namespace Provider {
|
|
|
1389
1396
|
error instanceof Error
|
|
1390
1397
|
? { name: error.name, message: error.message }
|
|
1391
1398
|
: String(error),
|
|
1392
|
-
})
|
|
1399
|
+
});
|
|
1393
1400
|
throw error;
|
|
1394
1401
|
}
|
|
1395
1402
|
};
|
|
@@ -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/index.ts
CHANGED
|
@@ -630,9 +630,14 @@ export namespace Session {
|
|
|
630
630
|
}
|
|
631
631
|
| undefined;
|
|
632
632
|
|
|
633
|
+
// Check if standard usage has valid data (inputTokens or outputTokens defined)
|
|
634
|
+
// Also check for zero-valued tokens (some providers set them to 0 instead of undefined)
|
|
633
635
|
const standardUsageIsEmpty =
|
|
634
|
-
input.usage.inputTokens === undefined &&
|
|
635
|
-
|
|
636
|
+
(input.usage.inputTokens === undefined &&
|
|
637
|
+
input.usage.outputTokens === undefined) ||
|
|
638
|
+
(input.usage.inputTokens === 0 &&
|
|
639
|
+
input.usage.outputTokens === 0 &&
|
|
640
|
+
!input.usage.totalTokens);
|
|
636
641
|
|
|
637
642
|
// If standard usage is empty but openrouter metadata has usage, use it as source
|
|
638
643
|
let effectiveUsage = input.usage;
|
|
@@ -658,6 +663,41 @@ export namespace Session {
|
|
|
658
663
|
};
|
|
659
664
|
}
|
|
660
665
|
|
|
666
|
+
// If still empty, try providerMetadata.anthropic.usage as fallback
|
|
667
|
+
// Some providers (e.g., opencode using @ai-sdk/anthropic) return usage in
|
|
668
|
+
// Anthropic-specific metadata with snake_case keys (input_tokens, output_tokens)
|
|
669
|
+
// while the standard AI SDK usage object remains empty.
|
|
670
|
+
// See: https://github.com/link-assistant/agent/issues/211
|
|
671
|
+
if (standardUsageIsEmpty && !openrouterUsage) {
|
|
672
|
+
const anthropicUsage = input.metadata?.['anthropic']?.['usage'] as
|
|
673
|
+
| {
|
|
674
|
+
input_tokens?: number;
|
|
675
|
+
output_tokens?: number;
|
|
676
|
+
cache_creation_input_tokens?: number;
|
|
677
|
+
cache_read_input_tokens?: number;
|
|
678
|
+
}
|
|
679
|
+
| undefined;
|
|
680
|
+
|
|
681
|
+
if (
|
|
682
|
+
anthropicUsage &&
|
|
683
|
+
(anthropicUsage.input_tokens || anthropicUsage.output_tokens)
|
|
684
|
+
) {
|
|
685
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
686
|
+
log.debug(() => ({
|
|
687
|
+
message:
|
|
688
|
+
'Standard usage empty, falling back to anthropic provider metadata',
|
|
689
|
+
anthropicUsage: JSON.stringify(anthropicUsage),
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
effectiveUsage = {
|
|
693
|
+
...input.usage,
|
|
694
|
+
inputTokens: anthropicUsage.input_tokens ?? 0,
|
|
695
|
+
outputTokens: anthropicUsage.output_tokens ?? 0,
|
|
696
|
+
cachedInputTokens: anthropicUsage.cache_read_input_tokens ?? 0,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
661
701
|
// Extract top-level cachedInputTokens
|
|
662
702
|
const topLevelCachedInputTokens = safeNum(
|
|
663
703
|
toNumber(effectiveUsage.cachedInputTokens, 'cachedInputTokens')
|
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
|
}
|