@remits/remits-cli 0.1.162 → 0.1.163
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/index.js +5 -5
- package/lib/local-ai.js +42 -6
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -4160,7 +4160,7 @@ const WORKLOAD_BUSY_MAX_RETRIES = 6;
|
|
|
4160
4160
|
* How long to wait before retrying a request the platform refused as `WORKLOAD_BUSY`, or null when the
|
|
4161
4161
|
* failure is anything else (or the retries are spent).
|
|
4162
4162
|
*
|
|
4163
|
-
* The platform admits only a few heavyweight requests
|
|
4163
|
+
* The platform admits only a few heavyweight requests at once and answers the rest with HTTP 429
|
|
4164
4164
|
* BEFORE doing any work, so repeating the identical request is safe — which is not true of any other
|
|
4165
4165
|
* failure, and is why this matches on the code rather than on the status alone. The wait starts at the
|
|
4166
4166
|
* server's Retry-After and grows, with jitter so several refused agents do not come back in step.
|
|
@@ -4186,7 +4186,7 @@ async function postRidingOutBusy(api, endpoint, payload) {
|
|
|
4186
4186
|
} catch (err) {
|
|
4187
4187
|
const delayMs = workloadBusyRetryDelayMs(err, attempt, maxRetries, Math.random);
|
|
4188
4188
|
if (delayMs == null) throw err;
|
|
4189
|
-
console.error('Platform
|
|
4189
|
+
console.error('Platform is busy with ' + (err.response.data.workload || 'other work') +
|
|
4190
4190
|
'; retrying in ' + Math.round(delayMs / 1000) + 's (' + (attempt + 1) + '/' + maxRetries + ').');
|
|
4191
4191
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
4192
4192
|
}
|
|
@@ -9228,7 +9228,7 @@ function testRunOptionsFromFlags(flags = {}) {
|
|
|
9228
9228
|
// How long a later shard of a parallel run waits for a free suite slot before the launch gives up.
|
|
9229
9229
|
const SHARD_ADMISSION_WAIT_MS = 30 * 60 * 1000;
|
|
9230
9230
|
|
|
9231
|
-
// A start the platform refused because every suite slot
|
|
9231
|
+
// A start the platform refused because every suite slot is taken (safe to repeat: no work began).
|
|
9232
9232
|
function shardStartBusy(err) {
|
|
9233
9233
|
const response = err && err.response;
|
|
9234
9234
|
return !!(response && response.status === 429 && response.data && response.data.code === 'WORKLOAD_BUSY');
|
|
@@ -9340,7 +9340,7 @@ async function testCommand(flags) {
|
|
|
9340
9340
|
};
|
|
9341
9341
|
// Shards are started one after another (each start returns at once) and then run side by side on the platform.
|
|
9342
9342
|
const shardStarts = [];
|
|
9343
|
-
// The platform admits only a few suites
|
|
9343
|
+
// The platform admits only a few suites at once and refuses the rest as WORKLOAD_BUSY. Once one
|
|
9344
9344
|
// of our shards is running it holds a slot it will give back, so a later shard keeps waiting for one (bounded)
|
|
9345
9345
|
// instead of giving up after the usual retries and leaving a group that can never finish.
|
|
9346
9346
|
const shardWaitDeadline = Date.now() + SHARD_ADMISSION_WAIT_MS;
|
|
@@ -9354,7 +9354,7 @@ async function testCommand(flags) {
|
|
|
9354
9354
|
const data = err && err.response && err.response.data;
|
|
9355
9355
|
if (shardStarts.length && shardStartBusy(err) && Date.now() < shardWaitDeadline) {
|
|
9356
9356
|
console.error('Shard ' + (shardStarts.length + 1) + ' of ' + runOptions.parallel + ' is waiting for a free suite slot ' +
|
|
9357
|
-
'(
|
|
9357
|
+
'(the platform runs a limited number of suites at once; the shards already running free them).');
|
|
9358
9358
|
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
9359
9359
|
continue;
|
|
9360
9360
|
}
|
package/lib/local-ai.js
CHANGED
|
@@ -354,6 +354,13 @@ function describe(state) {
|
|
|
354
354
|
return parts.length ? ` (${parts.join('; ')})` : '';
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
// HTTP polling recovers what a WebSocket notification missed, renews worker presence (it expires after 90
|
|
358
|
+
// seconds) and notices cancelled requests. With a live socket and nothing in flight, notifications carry
|
|
359
|
+
// delivery and a slow poll only renews presence; otherwise poll fast. Idle `ai serve` processes had been
|
|
360
|
+
// the largest share of all requests on the CLI node (2026-10-23: one request every 2s each, around the clock).
|
|
361
|
+
const FAST_POLL_MS = 2000;
|
|
362
|
+
const IDLE_POLL_MS = 30000;
|
|
363
|
+
|
|
357
364
|
class Supervisor {
|
|
358
365
|
constructor({ api, dir, worker, agent, model, log = console.error, run = runNative, clock = Date.now, onStop }) {
|
|
359
366
|
Object.assign(this, { api, dir, worker, agent, model, log, run, clock, onStop });
|
|
@@ -361,14 +368,23 @@ class Supervisor {
|
|
|
361
368
|
this.busySessions = new Set();
|
|
362
369
|
this.stopping = false;
|
|
363
370
|
this.polling = false;
|
|
371
|
+
this.socketReady = false;
|
|
364
372
|
this.lastContact = clock();
|
|
365
373
|
}
|
|
366
374
|
|
|
375
|
+
nextPollDelayMs() {
|
|
376
|
+
return this.socketReady && this.active.size === 0 ? IDLE_POLL_MS : FAST_POLL_MS;
|
|
377
|
+
}
|
|
378
|
+
|
|
367
379
|
async register() { return this.api('register', { ...this.worker, agent: this.agent }); }
|
|
368
380
|
|
|
369
381
|
async tick() {
|
|
370
|
-
if (this.
|
|
382
|
+
if (this.stopping) return;
|
|
383
|
+
// A wakeup during a poll gets ONE follow-up poll: that poll's answer may predate the notification,
|
|
384
|
+
// and the next scheduled poll can be 30 seconds away.
|
|
385
|
+
if (this.polling) { this.pollAgain = true; return; }
|
|
371
386
|
this.polling = true;
|
|
387
|
+
this.pollAgain = false;
|
|
372
388
|
try {
|
|
373
389
|
const work = await this.api('work');
|
|
374
390
|
this.lastContact = this.clock();
|
|
@@ -402,6 +418,7 @@ class Supervisor {
|
|
|
402
418
|
this.onStop?.();
|
|
403
419
|
}
|
|
404
420
|
} finally { this.polling = false; }
|
|
421
|
+
if (this.pollAgain && !this.stopping) await this.tick();
|
|
405
422
|
}
|
|
406
423
|
|
|
407
424
|
async handle(summary, task) {
|
|
@@ -565,7 +582,15 @@ async function command({ subcommand, flags, context, root, axios, Client, WebSoc
|
|
|
565
582
|
console.error(`Local AI serving ${baseUrl} account ${accountId} ${dataMode}: ${agent}, worker ${worker.workerId}, capacity ${maxConcurrent}. Ctrl-C to stop.`);
|
|
566
583
|
process.once('SIGINT', stop);
|
|
567
584
|
process.once('SIGTERM', stop);
|
|
568
|
-
|
|
585
|
+
const schedule = () => {
|
|
586
|
+
if (supervisor.stopping) return;
|
|
587
|
+
timer = setTimeout(async () => {
|
|
588
|
+
try { await supervisor.tick(); } finally { schedule(); }
|
|
589
|
+
}, supervisor.nextPollDelayMs());
|
|
590
|
+
};
|
|
591
|
+
// A socket change re-paces at once, so losing the socket never leaves a 30-second gap.
|
|
592
|
+
const repace = () => { clearTimeout(timer); schedule(); };
|
|
593
|
+
schedule();
|
|
569
594
|
const topic = session.websocketTopic || String(session.user.uuid || '').replace(/-/g, '');
|
|
570
595
|
if (topic) {
|
|
571
596
|
socket = new Client({
|
|
@@ -577,23 +602,34 @@ async function command({ subcommand, flags, context, root, axios, Client, WebSoc
|
|
|
577
602
|
connectHeaders: { 'X-Auth-Token': session.token,
|
|
578
603
|
'x-remits-ws-label': 'remits-cli-ai', 'x-remits-user-id': String(session.user.id) },
|
|
579
604
|
onConnect: () => {
|
|
605
|
+
supervisor.socketReady = true;
|
|
606
|
+
repace();
|
|
580
607
|
socket.subscribe('/topic/messages/' + topic, (message) => {
|
|
581
608
|
try {
|
|
582
609
|
const event = JSON.parse(message.body);
|
|
583
610
|
if (event.type === 'cli_ai_request' && event.payload?.workerId === worker.workerId &&
|
|
584
|
-
String(event.payload.accountId) === String(accountId) && event.payload.dataMode === dataMode)
|
|
611
|
+
String(event.payload.accountId) === String(accountId) && event.payload.dataMode === dataMode) {
|
|
612
|
+
// A request that starts here needs the fast poll at once, to notice a cancellation.
|
|
613
|
+
supervisor.tick().finally(repace);
|
|
614
|
+
}
|
|
585
615
|
} catch (_) { /* HTTP polling is authoritative. */ }
|
|
586
616
|
});
|
|
587
617
|
supervisor.tick();
|
|
588
618
|
},
|
|
589
|
-
|
|
619
|
+
onWebSocketClose: () => { supervisor.socketReady = false; repace(); },
|
|
620
|
+
onStompError: () => {
|
|
621
|
+
supervisor.socketReady = false;
|
|
622
|
+
repace();
|
|
623
|
+
console.error('Local AI WebSocket unavailable; authenticated polling continues.');
|
|
624
|
+
}
|
|
590
625
|
});
|
|
591
626
|
socket.activate();
|
|
592
627
|
}
|
|
593
628
|
await supervisor.tick();
|
|
594
629
|
await stopped;
|
|
595
630
|
} finally {
|
|
596
|
-
|
|
631
|
+
if (supervisor) supervisor.stopping = true;
|
|
632
|
+
clearTimeout(timer);
|
|
597
633
|
process.removeListener('SIGINT', stop);
|
|
598
634
|
process.removeListener('SIGTERM', stop);
|
|
599
635
|
if (socket) await socket.deactivate({ force: true });
|
|
@@ -602,5 +638,5 @@ async function command({ subcommand, flags, context, root, axios, Client, WebSoc
|
|
|
602
638
|
}
|
|
603
639
|
}
|
|
604
640
|
|
|
605
|
-
module.exports = { command, help, Supervisor, runNative, nativeCommand, consumeEvent, promptFor, parseDecision,
|
|
641
|
+
module.exports = { command, help, Supervisor, FAST_POLL_MS, IDLE_POLL_MS, runNative, nativeCommand, consumeEvent, promptFor, parseDecision,
|
|
606
642
|
validateDecision, decisionSchema, offeredTools, continuation, assertTextRequest, stateDirectory, acquireLock, read, write, identifier };
|