@remits/remits-cli 0.1.163 → 0.1.164

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/README.md CHANGED
@@ -139,6 +139,10 @@ remits-cli install --skills --overwrite true
139
139
  - The service groups sessions by `baseUrl` so one websocket connection can service multiple authenticated accounts on the same Remits environment.
140
140
  - Websocket subscriptions are deduplicated by user topic. If multiple authenticated accounts share the same websocket topic, the listener subscribes once and maps that topic back to all related accounts.
141
141
  - The daemon keeps STOMP heartbeats enabled and runs a maintenance watchdog that recreates unhealthy websocket connections.
142
+ - Terminal server authentication/authorization errors suspend WebSocket retries and show
143
+ `authentication_required`. Reauthenticate to replace the managed client's credentials; the watchdog
144
+ does not revive rejected credentials. Transient failures use jittered exponential backoff up to
145
+ 60 seconds. Test watchers keep HTTP polling; restart local-AI workers after reauthentication.
142
146
  - If the machine sleeps, the network drops, or auth sessions change while the daemon is already running, the service now attempts to reconnect and resubscribe automatically once connectivity returns.
143
147
  - Support tickets are the primary unit of dispatched work. A serving agent launches one fresh worker process per routed ticket.
144
148
  - Agent sessions register themselves with `remits-cli agent register` or `remits-cli agent serve`; `serve` also starts the local supervisor.
package/index.js CHANGED
@@ -4,18 +4,18 @@
4
4
  ## Table of Contents
5
5
 
6
6
  - L22 Runtime Bootstrap And Shared State
7
- - L124 Sessions, Account Resolution, And Production Guards
8
- - L630 Local State, Workspaces, And Verification Evidence
9
- - L2668 Account Repos, Guide Sync, And Platform Repo
10
- - L3508 Component Discovery And HTTP Logging
11
- - L4276 Skill Delivery And TOC Resolution
12
- - L4591 Auth And Component Staging
13
- - L5223 Component Summaries, Status, And Sync Gates
14
- - L7547 Branches, Promotion, Commit, And Test Runs
15
- - L9791 Tokens, Tools, Verification, And Config
16
- - L11308 Service Dashboard And WebSocket Listener
17
- - L13808 Agent And Ticket Workflows
18
- - L18069 Help, Auto Update, And Command Dispatch
7
+ - L125 Sessions, Account Resolution, And Production Guards
8
+ - L631 Local State, Workspaces, And Verification Evidence
9
+ - L2669 Account Repos, Guide Sync, And Platform Repo
10
+ - L3509 Component Discovery And HTTP Logging
11
+ - L4277 Skill Delivery And TOC Resolution
12
+ - L4592 Auth And Component Staging
13
+ - L5224 Component Summaries, Status, And Sync Gates
14
+ - L7548 Branches, Promotion, Commit, And Test Runs
15
+ - L9802 Tokens, Tools, Verification, And Config
16
+ - L11319 Service Dashboard And WebSocket Listener
17
+ - L13829 Agent And Ticket Workflows
18
+ - L18090 Help, Auto Update, And Command Dispatch
19
19
  */
20
20
 
21
21
  /*
@@ -32,6 +32,7 @@ const axios = require('axios');
32
32
  const openModule = require('open');
33
33
  const { Client } = require('@stomp/stompjs');
34
34
  const WebSocket = require('ws');
35
+ const { installWebsocketRetry } = require('./lib/websocket-retry');
35
36
  const YAML = require('yaml');
36
37
 
37
38
  Object.assign(global, { WebSocket });
@@ -9144,7 +9145,16 @@ function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
9144
9145
  const emit = options.stderr ? console.error : console.log;
9145
9146
  const client = new Client({
9146
9147
  brokerURL: webSocketUrl(baseUrl),
9147
- connectHeaders: { 'X-Auth-Token': options.token || '' },
9148
+ connectHeaders: {
9149
+ 'X-Auth-Token': options.token || '',
9150
+ 'x-remits-ws-label': 'cli-test',
9151
+ 'x-remits-client-kind': 'cli',
9152
+ 'x-remits-tab-id': 'cli:' + process.pid + ':test:' + crypto.randomUUID(),
9153
+ 'x-remits-origin': 'remits-cli://test',
9154
+ 'x-remits-path': 'test/watch',
9155
+ 'x-remits-location': normalizeBaseUrl(baseUrl),
9156
+ 'x-remits-broker': webSocketUrl(baseUrl)
9157
+ },
9148
9158
  reconnectDelay: 3000,
9149
9159
  heartbeatIncoming: 10000,
9150
9160
  heartbeatOutgoing: 10000,
@@ -9176,6 +9186,7 @@ function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
9176
9186
  });
9177
9187
  };
9178
9188
 
9189
+ installWebsocketRetry(client, { onBlocked: code => emit('[ws] Authentication required (' + code + '); HTTP polling continues.') });
9179
9190
  client.activate();
9180
9191
  return () => {
9181
9192
  client.deactivate();
@@ -13538,6 +13549,7 @@ function createManagedWebsocketClient(baseUrl, urlSessions) {
13538
13549
  discardWebsocketOnCommFailure: true,
13539
13550
  connectHeaders: {
13540
13551
  'x-remits-ws-label': 'remits-cli',
13552
+ 'x-remits-client-kind': 'cli',
13541
13553
  'x-remits-ws-scope': headerScope,
13542
13554
  'x-remits-user-id': headerUserIds.length === 1 ? headerUserIds[0] : '',
13543
13555
  'x-remits-account-id': headerAccountIds.length === 1 ? headerAccountIds[0] : '',
@@ -13643,7 +13655,7 @@ function createManagedWebsocketClient(baseUrl, urlSessions) {
13643
13655
  ensureRuntimeWebsocketEntry(normalizedBaseUrl);
13644
13656
  runtimeState.websocket[normalizedBaseUrl] = {
13645
13657
  ...runtimeState.websocket[normalizedBaseUrl],
13646
- status: 'disconnected',
13658
+ status: client.remitsAuthBlocked ? 'authentication_required' : 'disconnected',
13647
13659
  disconnectedAt: new Date().toISOString()
13648
13660
  };
13649
13661
  console.log('[listener] Disconnected from', normalizedBaseUrl, '- will reconnect');
@@ -13673,7 +13685,7 @@ function createManagedWebsocketClient(baseUrl, urlSessions) {
13673
13685
  ensureRuntimeWebsocketEntry(normalizedBaseUrl);
13674
13686
  runtimeState.websocket[normalizedBaseUrl] = {
13675
13687
  ...runtimeState.websocket[normalizedBaseUrl],
13676
- status: 'disconnected',
13688
+ status: client.remitsAuthBlocked ? 'authentication_required' : 'disconnected',
13677
13689
  disconnectedAt: new Date().toISOString(),
13678
13690
  lastError: detail
13679
13691
  };
@@ -13700,6 +13712,12 @@ function createManagedWebsocketClient(baseUrl, urlSessions) {
13700
13712
  };
13701
13713
 
13702
13714
  managedClient.client = client;
13715
+ installWebsocketRetry(client, { onBlocked: code => {
13716
+ runtimeState.websocket[normalizedBaseUrl] = {
13717
+ ...runtimeState.websocket[normalizedBaseUrl], status: 'authentication_required', lastError: code
13718
+ };
13719
+ updateRuntimeServiceState();
13720
+ } });
13703
13721
  return managedClient;
13704
13722
  }
13705
13723
 
@@ -13745,6 +13763,9 @@ async function runManagedWebsocketWatchdog() {
13745
13763
  if (!managedClient || !managedClient.client) {
13746
13764
  continue;
13747
13765
  }
13766
+ // A session-file change replaces the client. The watchdog must not revive rejected credentials
13767
+ // or defeat the normal transport backoff while it is waiting to reconnect.
13768
+ if (managedClient.client.remitsAuthBlocked || managedClient.client.remitsRetryPending) continue;
13748
13769
  const state = ensureRuntimeWebsocketEntry(managedClient.baseUrl);
13749
13770
  const socket = managedClient.client.webSocket;
13750
13771
  const readyState = socket && typeof socket.readyState === 'number' ? socket.readyState : null;
package/lib/local-ai.js CHANGED
@@ -6,6 +6,7 @@ const fs = require('node:fs');
6
6
  const path = require('node:path');
7
7
  const crypto = require('node:crypto');
8
8
  const { spawn } = require('node:child_process');
9
+ const { installWebsocketRetry } = require('./websocket-retry');
9
10
 
10
11
  const MAX_BYTES = 8 * 1024 * 1024;
11
12
  const AGENTS = ['manual', 'codex', 'claude', 'gemini'];
@@ -623,6 +624,9 @@ async function command({ subcommand, flags, context, root, axios, Client, WebSoc
623
624
  console.error('Local AI WebSocket unavailable; authenticated polling continues.');
624
625
  }
625
626
  });
627
+ installWebsocketRetry(socket, { onBlocked: code => {
628
+ console.error(`Local AI WebSocket authentication required (${code}); reauthenticate and restart the worker.`);
629
+ } });
626
630
  socket.activate();
627
631
  }
628
632
  await supervisor.tick();
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ // The server sends stable, credential-free codes. A transport close code alone (including 1002)
4
+ // cannot distinguish expired credentials from a transient broker failure.
5
+ const TERMINAL_CODES = new Set(['credentials_missing', 'credentials_rejected', 'credentials_malformed',
6
+ 'session_required', 'topic_denied', 'destination_denied', 'recipient_denied']);
7
+
8
+ function terminalCode(frame) {
9
+ const headers = frame?.headers || {};
10
+ const code = headers['x-remits-error-code'];
11
+ return TERMINAL_CODES.has(code) ? code : null;
12
+ }
13
+
14
+ function installWebsocketRetry(client, { onBlocked = () => {}, random = Math.random,
15
+ setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
16
+ const base = Math.max(1, Number(client.reconnectDelay) || 5000);
17
+ let failures = 0, wait = 0, timer, release;
18
+ const cancelWait = () => {
19
+ if (timer) clearTimer(timer);
20
+ timer = null;
21
+ if (release) release();
22
+ release = null;
23
+ client.remitsRetryPending = false;
24
+ };
25
+ const before = client.beforeConnect;
26
+ client.beforeConnect = async () => {
27
+ if (client.remitsAuthBlocked) { await client.deactivate(); return; }
28
+ if (wait > 0) {
29
+ client.remitsRetryPending = true;
30
+ await new Promise(resolve => { release = resolve; timer = setTimer(cancelWait, wait); });
31
+ // A stop/replacement may have resolved the wait; never read or send credentials afterwards.
32
+ if (!client.active || client.remitsAuthBlocked) return;
33
+ }
34
+ client.remitsRetryPending = false;
35
+ if (before) await before();
36
+ };
37
+ const connected = client.onConnect;
38
+ client.onConnect = frame => {
39
+ failures = 0; wait = 0; client.remitsRetryPending = false;
40
+ if (connected) connected(frame);
41
+ };
42
+ const closed = client.onWebSocketClose;
43
+ client.onWebSocketClose = event => {
44
+ const ceiling = Math.min(60000, base * Math.pow(2, Math.min(++failures, 5)));
45
+ // STOMP already waits base ms. Add jitter/backoff before its next CONNECT using public APIs.
46
+ wait = Math.max(0, Math.floor(ceiling / 2 + random() * ceiling / 2) - base);
47
+ client.remitsRetryPending = !client.remitsAuthBlocked;
48
+ if (closed) closed(event);
49
+ };
50
+ const error = client.onStompError;
51
+ client.onStompError = frame => {
52
+ const code = terminalCode(frame);
53
+ if (code) {
54
+ client.remitsAuthBlocked = code;
55
+ cancelWait();
56
+ // deactivate changes active synchronously, before STOMP schedules the close reconnect.
57
+ Promise.resolve(client.deactivate({ force: true })).catch(() => {});
58
+ }
59
+ if (error) error(frame);
60
+ if (code) onBlocked(code);
61
+ };
62
+ const deactivate = client.deactivate.bind(client);
63
+ client.deactivate = options => { cancelWait(); return deactivate(options); };
64
+ return client;
65
+ }
66
+
67
+ module.exports = { installWebsocketRetry, terminalCode };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.163",
3
+ "version": "0.1.164",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -13,6 +13,7 @@
13
13
  "index.js",
14
14
  "lib/sync-operation.js",
15
15
  "lib/local-ai.js",
16
+ "lib/websocket-retry.js",
16
17
  "README.md",
17
18
  "scripts/prepare-index-toc.js",
18
19
  "skills/remits-cli/SKILL.md",