@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 +4 -0
- package/index.js +36 -15
- package/lib/local-ai.js +4 -0
- package/lib/websocket-retry.js +67 -0
- package/package.json +2 -1
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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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: {
|
|
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.
|
|
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",
|