@remits/remits-cli 0.1.161 → 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 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 per instance and answers the rest with HTTP 429
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 instance is busy with ' + (err.response.data.workload || 'other work') +
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 on the instance is taken (safe to repeat: no work began).
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 per instance at once and refuses the rest as WORKLOAD_BUSY. Once one
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
- '(this platform instance runs a limited number of suites at once; the shards already running free them).');
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.polling || this.stopping) return;
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
- timer = setInterval(() => supervisor.tick(), 2000);
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) supervisor.tick();
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
- onStompError: () => console.error('Local AI WebSocket unavailable; authenticated polling continues.')
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
- clearInterval(timer);
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.161",
3
+ "version": "0.1.163",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,