@nopeek/agent-bridge 0.7.4 → 0.7.5
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/dist/bot.d.ts +16 -0
- package/dist/bot.js +63 -11
- package/dist/bridge.d.ts +10 -1
- package/dist/bridge.js +112 -1
- package/dist/control.d.ts +13 -0
- package/dist/control.js +71 -2
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export declare class BotRunner {
|
|
|
21
21
|
private np;
|
|
22
22
|
private stopped;
|
|
23
23
|
private refreshTimer;
|
|
24
|
+
private lastConnectedAt;
|
|
25
|
+
private lastForcedRestart;
|
|
26
|
+
private running;
|
|
24
27
|
private seen;
|
|
25
28
|
private channelCache;
|
|
26
29
|
private allowed;
|
|
@@ -38,6 +41,19 @@ export declare class BotRunner {
|
|
|
38
41
|
private log;
|
|
39
42
|
private logErr;
|
|
40
43
|
constructor(info: BotInfo, cfg: BridgeConfig, pairing: Pairing);
|
|
44
|
+
/** Watchdog liveness: 0 while currently connected, else ms since the bot was
|
|
45
|
+
* last connected (seeded at construction so a never-connected bot ages too). */
|
|
46
|
+
msSinceHealthy(): number;
|
|
47
|
+
/** When the watchdog last force-restarted this runner (null = never). */
|
|
48
|
+
get lastForcedRestartAt(): number | null;
|
|
49
|
+
/**
|
|
50
|
+
* Watchdog hard restart: tear the SDK client down completely and rebuild it
|
|
51
|
+
* from scratch (fresh session mint + NoPeek.connect) — the proven-working
|
|
52
|
+
* path when the SDK's own reconnect wedges. Unlike stop(), this does NOT
|
|
53
|
+
* permanently stop the runner; it clears `stopped` so the connect loop
|
|
54
|
+
* actually re-enters. Idempotent enough to call every watchdog tick.
|
|
55
|
+
*/
|
|
56
|
+
forceRestart(): void;
|
|
41
57
|
/** Re-read the effective brain (e.g. after a live server backend change) so
|
|
42
58
|
* status reflects it immediately. The next message re-resolves regardless. */
|
|
43
59
|
refreshBrainKind(): void;
|
package/dist/bot.js
CHANGED
|
@@ -24,6 +24,15 @@ export class BotRunner {
|
|
|
24
24
|
np = null;
|
|
25
25
|
stopped = false;
|
|
26
26
|
refreshTimer = null;
|
|
27
|
+
// Watchdog liveness. Seeded at construction so a bot that NEVER connects still
|
|
28
|
+
// ages from birth. Set to now() whenever the socket becomes connected; while
|
|
29
|
+
// disconnected it holds the last-healthy instant, so msSinceHealthy() reports
|
|
30
|
+
// how long the bot has been down.
|
|
31
|
+
lastConnectedAt = Date.now();
|
|
32
|
+
lastForcedRestart = null;
|
|
33
|
+
// Guards against two concurrent connect loops if forceRestart() lands while a
|
|
34
|
+
// backoff retry loop is still running.
|
|
35
|
+
running = false;
|
|
27
36
|
seen = new Set();
|
|
28
37
|
channelCache = new Map();
|
|
29
38
|
// Access control: bots are PRIVATE by default. `allowed` is the set of user
|
|
@@ -76,6 +85,39 @@ export class BotRunner {
|
|
|
76
85
|
this.log = (m) => console.log(`${tag} ${m}`);
|
|
77
86
|
this.logErr = (m) => console.error(`${tag} ${m}`);
|
|
78
87
|
}
|
|
88
|
+
/** Watchdog liveness: 0 while currently connected, else ms since the bot was
|
|
89
|
+
* last connected (seeded at construction so a never-connected bot ages too). */
|
|
90
|
+
msSinceHealthy() {
|
|
91
|
+
return this.connected ? 0 : Date.now() - this.lastConnectedAt;
|
|
92
|
+
}
|
|
93
|
+
/** When the watchdog last force-restarted this runner (null = never). */
|
|
94
|
+
get lastForcedRestartAt() {
|
|
95
|
+
return this.lastForcedRestart;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Watchdog hard restart: tear the SDK client down completely and rebuild it
|
|
99
|
+
* from scratch (fresh session mint + NoPeek.connect) — the proven-working
|
|
100
|
+
* path when the SDK's own reconnect wedges. Unlike stop(), this does NOT
|
|
101
|
+
* permanently stop the runner; it clears `stopped` so the connect loop
|
|
102
|
+
* actually re-enters. Idempotent enough to call every watchdog tick.
|
|
103
|
+
*/
|
|
104
|
+
forceRestart() {
|
|
105
|
+
if (this.stopped)
|
|
106
|
+
return; // truly stopped (unpair) — never resurrect
|
|
107
|
+
this.lastForcedRestart = Date.now();
|
|
108
|
+
if (this.refreshTimer)
|
|
109
|
+
clearTimeout(this.refreshTimer);
|
|
110
|
+
this.refreshTimer = null;
|
|
111
|
+
try {
|
|
112
|
+
this.np?.close();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* best-effort */
|
|
116
|
+
}
|
|
117
|
+
this.np = null;
|
|
118
|
+
this.connected = false;
|
|
119
|
+
this.start(); // run()'s `running` guard prevents a duplicate loop
|
|
120
|
+
}
|
|
79
121
|
/** Re-read the effective brain (e.g. after a live server backend change) so
|
|
80
122
|
* status reflects it immediately. The next message re-resolves regardless. */
|
|
81
123
|
refreshBrainKind() {
|
|
@@ -251,19 +293,27 @@ export class BotRunner {
|
|
|
251
293
|
}
|
|
252
294
|
}
|
|
253
295
|
async run() {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
296
|
+
if (this.running)
|
|
297
|
+
return; // a connect loop is already active — never double it
|
|
298
|
+
this.running = true;
|
|
299
|
+
try {
|
|
300
|
+
this.log(`starting (${this.info.userId}) brain=${this.brainKind}`);
|
|
301
|
+
let delay = 2_000;
|
|
302
|
+
while (!this.stopped) {
|
|
303
|
+
try {
|
|
304
|
+
await this.connectOnce();
|
|
305
|
+
return; // connected; SDK auto-reconnects, session refresh re-enters via reconnect()
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
this.logErr(`connect failed: ${err.message} — retrying in ${delay / 1000}s`);
|
|
309
|
+
await sleep(delay);
|
|
310
|
+
delay = Math.min(delay * 2, MAX_BACKOFF_MS);
|
|
311
|
+
}
|
|
265
312
|
}
|
|
266
313
|
}
|
|
314
|
+
finally {
|
|
315
|
+
this.running = false;
|
|
316
|
+
}
|
|
267
317
|
}
|
|
268
318
|
async connectOnce() {
|
|
269
319
|
const session = await this.mintSession();
|
|
@@ -286,11 +336,13 @@ export class BotRunner {
|
|
|
286
336
|
}
|
|
287
337
|
this.np = np;
|
|
288
338
|
this.connected = true;
|
|
339
|
+
this.lastConnectedAt = Date.now();
|
|
289
340
|
this.channelCache.clear();
|
|
290
341
|
this.ownerPresence.clear();
|
|
291
342
|
this.log(`connected, device ${np.deviceId} (platform=server), store ${store.file}`);
|
|
292
343
|
np.on("connected", (() => {
|
|
293
344
|
this.connected = true;
|
|
345
|
+
this.lastConnectedAt = Date.now();
|
|
294
346
|
this.log(`ws connected`);
|
|
295
347
|
}));
|
|
296
348
|
np.on("disconnected", (() => {
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BridgeConfig, Pairing, BrainSpec, BrainBackend } from "./config.js";
|
|
2
|
-
export declare const VERSION = "0.7.
|
|
2
|
+
export declare const VERSION = "0.7.5";
|
|
3
3
|
export interface PairRequest {
|
|
4
4
|
pairingSecret: string;
|
|
5
5
|
appId: string;
|
|
@@ -39,6 +39,9 @@ export declare class BridgeApp {
|
|
|
39
39
|
readonly startedAt: number;
|
|
40
40
|
private runtimes;
|
|
41
41
|
private capabilitiesTimer;
|
|
42
|
+
private watchdogTimer;
|
|
43
|
+
private hardStaleStreak;
|
|
44
|
+
private lastWatchdogAt;
|
|
42
45
|
private stopped;
|
|
43
46
|
constructor(cfg: BridgeConfig);
|
|
44
47
|
get paired(): boolean;
|
|
@@ -48,6 +51,12 @@ export declare class BridgeApp {
|
|
|
48
51
|
start(): void;
|
|
49
52
|
stop(): void;
|
|
50
53
|
private startRuntime;
|
|
54
|
+
/** Reconnect watchdog. Sweeps every pairing's runners + control sockets on a
|
|
55
|
+
* fixed interval and force-recovers anything stuck; as a last resort, exits
|
|
56
|
+
* so the always-restart service relaunches a clean process. unref'd so it
|
|
57
|
+
* never keeps the process alive on its own. */
|
|
58
|
+
private ensureWatchdogTimer;
|
|
59
|
+
private watchdogTick;
|
|
51
60
|
/** Re-probe + report brain availability every ~5 min (once per pairing) so a
|
|
52
61
|
* login/logout on this computer surfaces in each account's picker without a
|
|
53
62
|
* reconnect. onAuthed covers initial + reconnect reports; this covers drift.
|
package/dist/bridge.js
CHANGED
|
@@ -17,9 +17,22 @@ import { resolveBrain } from "./brain.js";
|
|
|
17
17
|
import { provisionSoul, provisionHermesProfile } from "./backends.js";
|
|
18
18
|
import { reportCapabilities } from "./capabilities.js";
|
|
19
19
|
import { isBrainBackend } from "./config.js";
|
|
20
|
-
export const VERSION = "0.7.
|
|
20
|
+
export const VERSION = "0.7.5";
|
|
21
21
|
/** How often to re-probe + report brain availability to the server. */
|
|
22
22
|
const CAPABILITIES_INTERVAL_MS = 5 * 60_000;
|
|
23
|
+
// ---- Reconnect watchdog -----------------------------------------------------
|
|
24
|
+
// A layered safety net for the exact wedge seen in the field: after a network
|
|
25
|
+
// routing change all bot sockets closed with 1006 and the process's networking
|
|
26
|
+
// got stuck — the SDK's own reconnect never recovered, though a fresh process
|
|
27
|
+
// connected fine. So we don't trust internal reconnect: we (a) force a hard
|
|
28
|
+
// rebuild of any runner/control socket that's been down too long, and (b) as a
|
|
29
|
+
// last resort exit so the always-restart service relaunches a clean process.
|
|
30
|
+
/** How often the watchdog sweeps every pairing's runners + control sockets. */
|
|
31
|
+
const WATCHDOG_INTERVAL_MS = 30_000;
|
|
32
|
+
/** Down longer than this → force a hard restart (rebuild from scratch). */
|
|
33
|
+
const SOFT_STALE_MS = 90_000;
|
|
34
|
+
/** Every bot down longer than this (sustained) → exit for a clean relaunch. */
|
|
35
|
+
const HARD_STALE_MS = 5 * 60_000;
|
|
23
36
|
export class PairError extends Error {
|
|
24
37
|
code;
|
|
25
38
|
constructor(code, message) {
|
|
@@ -140,9 +153,48 @@ class PairingRuntime {
|
|
|
140
153
|
userId: b.info.userId,
|
|
141
154
|
connected: b.connected,
|
|
142
155
|
handled: b.handled,
|
|
156
|
+
msSinceHealthy: b.msSinceHealthy(),
|
|
157
|
+
lastForcedRestart: b.lastForcedRestartAt,
|
|
143
158
|
brain: resolveBrain(this.cfg, b.info.handle).kind,
|
|
144
159
|
}));
|
|
145
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Watchdog soft pass for THIS pairing. Force-restart (full rebuild) any bot
|
|
163
|
+
* that's been disconnected longer than softMs, debounced so a single bot is
|
|
164
|
+
* restarted at most once per softMs window. Also force-recreate the control
|
|
165
|
+
* socket if it's been un-authed longer than softMs (its internal backoff can
|
|
166
|
+
* wedge too). Returns each bot's msSinceHealthy so the app can evaluate the
|
|
167
|
+
* hard, process-level catch-all across all pairings.
|
|
168
|
+
*/
|
|
169
|
+
watchdogSoftPass(softMs) {
|
|
170
|
+
if (this.stopped)
|
|
171
|
+
return [];
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const liveness = [];
|
|
174
|
+
for (const b of this.bots.values()) {
|
|
175
|
+
const ms = b.msSinceHealthy();
|
|
176
|
+
liveness.push(ms);
|
|
177
|
+
if (ms <= softMs)
|
|
178
|
+
continue;
|
|
179
|
+
const last = b.lastForcedRestartAt;
|
|
180
|
+
if (last !== null && now - last < softMs)
|
|
181
|
+
continue; // debounce: one restart per window
|
|
182
|
+
console.log(`${this.tag} [watchdog] @${b.info.handle} disconnected ${Math.round(ms / 1000)}s — forcing hard restart`);
|
|
183
|
+
b.forceRestart();
|
|
184
|
+
}
|
|
185
|
+
const control = this.control;
|
|
186
|
+
if (control) {
|
|
187
|
+
const ms = control.msSinceHealthy();
|
|
188
|
+
if (ms > softMs) {
|
|
189
|
+
const last = control.lastForcedRestartAt;
|
|
190
|
+
if (last === null || now - last >= softMs) {
|
|
191
|
+
console.log(`${this.tag} [watchdog] control socket down ${Math.round(ms / 1000)}s — forcing a fresh socket`);
|
|
192
|
+
control.forceReconnect();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return liveness;
|
|
197
|
+
}
|
|
146
198
|
// ---------------------------------------------------------------- core ----
|
|
147
199
|
startBot(info) {
|
|
148
200
|
if (this.bots.has(info.userId))
|
|
@@ -229,6 +281,11 @@ export class BridgeApp {
|
|
|
229
281
|
startedAt = Date.now();
|
|
230
282
|
runtimes = [];
|
|
231
283
|
capabilitiesTimer = null;
|
|
284
|
+
watchdogTimer = null;
|
|
285
|
+
// Hard exit needs the all-bots-stale condition sustained across TWO
|
|
286
|
+
// consecutive checks — a single transient blip must never restart the process.
|
|
287
|
+
hardStaleStreak = 0;
|
|
288
|
+
lastWatchdogAt = 0;
|
|
232
289
|
stopped = false;
|
|
233
290
|
constructor(cfg) {
|
|
234
291
|
this.cfg = cfg;
|
|
@@ -261,6 +318,10 @@ export class BridgeApp {
|
|
|
261
318
|
clearInterval(this.capabilitiesTimer);
|
|
262
319
|
this.capabilitiesTimer = null;
|
|
263
320
|
}
|
|
321
|
+
if (this.watchdogTimer) {
|
|
322
|
+
clearInterval(this.watchdogTimer);
|
|
323
|
+
this.watchdogTimer = null;
|
|
324
|
+
}
|
|
264
325
|
for (const r of this.runtimes)
|
|
265
326
|
r.stop();
|
|
266
327
|
this.runtimes = [];
|
|
@@ -274,6 +335,46 @@ export class BridgeApp {
|
|
|
274
335
|
this.runtimes.push(rt);
|
|
275
336
|
rt.start();
|
|
276
337
|
this.ensureCapabilitiesTimer();
|
|
338
|
+
this.ensureWatchdogTimer();
|
|
339
|
+
}
|
|
340
|
+
/** Reconnect watchdog. Sweeps every pairing's runners + control sockets on a
|
|
341
|
+
* fixed interval and force-recovers anything stuck; as a last resort, exits
|
|
342
|
+
* so the always-restart service relaunches a clean process. unref'd so it
|
|
343
|
+
* never keeps the process alive on its own. */
|
|
344
|
+
ensureWatchdogTimer() {
|
|
345
|
+
if (this.watchdogTimer)
|
|
346
|
+
return;
|
|
347
|
+
this.watchdogTimer = setInterval(() => this.watchdogTick(), WATCHDOG_INTERVAL_MS);
|
|
348
|
+
this.watchdogTimer.unref?.();
|
|
349
|
+
}
|
|
350
|
+
watchdogTick() {
|
|
351
|
+
if (this.stopped || !this.paired) {
|
|
352
|
+
this.hardStaleStreak = 0;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
this.lastWatchdogAt = Date.now();
|
|
356
|
+
// Layers 2+3: soft force-recovery per pairing (bots + control socket).
|
|
357
|
+
let allLiveness = [];
|
|
358
|
+
for (const r of this.runtimes)
|
|
359
|
+
allLiveness = allLiveness.concat(r.watchdogSoftPass(SOFT_STALE_MS));
|
|
360
|
+
// Layer 4 (the guarantee): if EVERY bot across ALL pairings has been down
|
|
361
|
+
// longer than HARD_STALE_MS and there is at least one bot, soft recovery
|
|
362
|
+
// has failed — the process networking is wedged. Require the condition
|
|
363
|
+
// sustained over two consecutive checks, then exit for a clean relaunch.
|
|
364
|
+
const hasBots = allLiveness.length > 0;
|
|
365
|
+
const allHardStale = hasBots && allLiveness.every((ms) => ms > HARD_STALE_MS);
|
|
366
|
+
if (this.paired && hasBots && allHardStale) {
|
|
367
|
+
this.hardStaleStreak++;
|
|
368
|
+
const mins = Math.round(HARD_STALE_MS / 60_000);
|
|
369
|
+
if (this.hardStaleStreak >= 2) {
|
|
370
|
+
console.error(`[watchdog] no bot connected for ${mins}min across ${allLiveness.length} bot(s) — exiting for a clean relaunch`);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
console.error(`[watchdog] all ${allLiveness.length} bot(s) down > ${mins}min — will exit for a clean relaunch if still down next check`);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
this.hardStaleStreak = 0;
|
|
377
|
+
}
|
|
277
378
|
}
|
|
278
379
|
/** Re-probe + report brain availability every ~5 min (once per pairing) so a
|
|
279
380
|
* login/logout on this computer surfaces in each account's picker without a
|
|
@@ -342,6 +443,7 @@ export class BridgeApp {
|
|
|
342
443
|
* Returns false when a runtimeId was given but matches no pairing.
|
|
343
444
|
*/
|
|
344
445
|
unpair(runtimeId) {
|
|
446
|
+
this.hardStaleStreak = 0; // never carry a stale streak across a topology change
|
|
345
447
|
if (!runtimeId) {
|
|
346
448
|
console.log(`[bridge] unpairing ALL ${this.cfg.pairings.length} account(s) — stopping bots and forgetting the pairing secrets`);
|
|
347
449
|
for (const r of this.runtimes)
|
|
@@ -499,6 +601,15 @@ export class BridgeApp {
|
|
|
499
601
|
},
|
|
500
602
|
// Legacy flat list = every pairing's bots (pre-0.6 clients read this).
|
|
501
603
|
bots: this.runtimes.flatMap((r) => r.botStatuses()),
|
|
604
|
+
// Reconnect watchdog state (per-bot msSinceHealthy/lastForcedRestart ride
|
|
605
|
+
// each bot entry above; this summarizes the process-level guard).
|
|
606
|
+
watchdog: {
|
|
607
|
+
intervalMs: WATCHDOG_INTERVAL_MS,
|
|
608
|
+
softStaleMs: SOFT_STALE_MS,
|
|
609
|
+
hardStaleMs: HARD_STALE_MS,
|
|
610
|
+
hardStaleStreak: this.hardStaleStreak,
|
|
611
|
+
lastCheckAt: this.lastWatchdogAt || null,
|
|
612
|
+
},
|
|
502
613
|
};
|
|
503
614
|
}
|
|
504
615
|
}
|
package/dist/control.d.ts
CHANGED
|
@@ -42,12 +42,25 @@ export declare class ControlSocket {
|
|
|
42
42
|
private ws;
|
|
43
43
|
private stopped;
|
|
44
44
|
private heartbeat;
|
|
45
|
+
private reconnectTimer;
|
|
45
46
|
private delay;
|
|
47
|
+
private lastConnectedAt;
|
|
48
|
+
private lastForcedRestart;
|
|
46
49
|
/** Log tag — with several sockets running, lines must say whose they are. */
|
|
47
50
|
private tag;
|
|
48
51
|
constructor(pairing: Pairing, handlers: ControlHandlers);
|
|
49
52
|
start(): void;
|
|
50
53
|
stop(): void;
|
|
54
|
+
/** Watchdog liveness: 0 while authenticated, else ms since the last auth.ok. */
|
|
55
|
+
msSinceHealthy(): number;
|
|
56
|
+
/** When the watchdog last force-recreated this socket (null = never). */
|
|
57
|
+
get lastForcedRestartAt(): number | null;
|
|
58
|
+
/**
|
|
59
|
+
* Watchdog force-recovery: cancel any pending backoff timer, tear down the
|
|
60
|
+
* current socket and open a brand-new one immediately — bypassing the
|
|
61
|
+
* internal backoff, which can itself wedge after a network event.
|
|
62
|
+
*/
|
|
63
|
+
forceReconnect(): void;
|
|
51
64
|
private connect;
|
|
52
65
|
private handleFrame;
|
|
53
66
|
private scheduleReconnect;
|
package/dist/control.js
CHANGED
|
@@ -10,7 +10,13 @@ export class ControlSocket {
|
|
|
10
10
|
ws = null;
|
|
11
11
|
stopped = false;
|
|
12
12
|
heartbeat = null;
|
|
13
|
+
reconnectTimer = null;
|
|
13
14
|
delay = 2_000;
|
|
15
|
+
// Watchdog liveness. Seeded at construction; set to now() on every auth.ok
|
|
16
|
+
// (the only "healthy" moment for a control socket). While un-authed it holds
|
|
17
|
+
// the last-authed instant so msSinceHealthy() reports how long we've been out.
|
|
18
|
+
lastConnectedAt = Date.now();
|
|
19
|
+
lastForcedRestart = null;
|
|
14
20
|
/** Log tag — with several sockets running, lines must say whose they are. */
|
|
15
21
|
tag;
|
|
16
22
|
constructor(pairing, handlers) {
|
|
@@ -23,6 +29,14 @@ export class ControlSocket {
|
|
|
23
29
|
}
|
|
24
30
|
stop() {
|
|
25
31
|
this.stopped = true;
|
|
32
|
+
if (this.reconnectTimer) {
|
|
33
|
+
clearTimeout(this.reconnectTimer);
|
|
34
|
+
this.reconnectTimer = null;
|
|
35
|
+
}
|
|
36
|
+
if (this.heartbeat) {
|
|
37
|
+
clearInterval(this.heartbeat);
|
|
38
|
+
this.heartbeat = null;
|
|
39
|
+
}
|
|
26
40
|
try {
|
|
27
41
|
this.ws?.close();
|
|
28
42
|
}
|
|
@@ -32,6 +46,42 @@ export class ControlSocket {
|
|
|
32
46
|
this.ws = null;
|
|
33
47
|
this.connected = false;
|
|
34
48
|
}
|
|
49
|
+
/** Watchdog liveness: 0 while authenticated, else ms since the last auth.ok. */
|
|
50
|
+
msSinceHealthy() {
|
|
51
|
+
return this.connected ? 0 : Date.now() - this.lastConnectedAt;
|
|
52
|
+
}
|
|
53
|
+
/** When the watchdog last force-recreated this socket (null = never). */
|
|
54
|
+
get lastForcedRestartAt() {
|
|
55
|
+
return this.lastForcedRestart;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Watchdog force-recovery: cancel any pending backoff timer, tear down the
|
|
59
|
+
* current socket and open a brand-new one immediately — bypassing the
|
|
60
|
+
* internal backoff, which can itself wedge after a network event.
|
|
61
|
+
*/
|
|
62
|
+
forceReconnect() {
|
|
63
|
+
if (this.stopped)
|
|
64
|
+
return;
|
|
65
|
+
this.lastForcedRestart = Date.now();
|
|
66
|
+
if (this.reconnectTimer) {
|
|
67
|
+
clearTimeout(this.reconnectTimer);
|
|
68
|
+
this.reconnectTimer = null;
|
|
69
|
+
}
|
|
70
|
+
if (this.heartbeat) {
|
|
71
|
+
clearInterval(this.heartbeat);
|
|
72
|
+
this.heartbeat = null;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
this.ws?.close();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* best-effort */
|
|
79
|
+
}
|
|
80
|
+
this.ws = null;
|
|
81
|
+
this.connected = false;
|
|
82
|
+
this.delay = 2_000; // fresh start — reset backoff
|
|
83
|
+
this.connect();
|
|
84
|
+
}
|
|
35
85
|
connect() {
|
|
36
86
|
if (this.stopped)
|
|
37
87
|
return;
|
|
@@ -47,6 +97,13 @@ export class ControlSocket {
|
|
|
47
97
|
}
|
|
48
98
|
this.ws = ws;
|
|
49
99
|
ws.onopen = () => {
|
|
100
|
+
if (this.ws !== ws) {
|
|
101
|
+
try {
|
|
102
|
+
ws.close();
|
|
103
|
+
}
|
|
104
|
+
catch { /* stale */ }
|
|
105
|
+
return;
|
|
106
|
+
} // superseded
|
|
50
107
|
console.log(`${this.tag} socket open, awaiting auth.ok`);
|
|
51
108
|
// Heartbeat: the server closes sockets idle >60s; the SDK pings every
|
|
52
109
|
// 25s and so do we — otherwise the control socket flaps once a minute
|
|
@@ -76,6 +133,10 @@ export class ControlSocket {
|
|
|
76
133
|
// onclose always follows; log there.
|
|
77
134
|
};
|
|
78
135
|
ws.onclose = (ev) => {
|
|
136
|
+
// A newer socket (e.g. from a watchdog forceReconnect) already superseded
|
|
137
|
+
// this one — ignore this stale close so we never stack reconnects.
|
|
138
|
+
if (this.ws !== ws)
|
|
139
|
+
return;
|
|
79
140
|
if (this.heartbeat) {
|
|
80
141
|
clearInterval(this.heartbeat);
|
|
81
142
|
this.heartbeat = null;
|
|
@@ -92,6 +153,7 @@ export class ControlSocket {
|
|
|
92
153
|
switch (frame.type) {
|
|
93
154
|
case "auth.ok": {
|
|
94
155
|
this.connected = true;
|
|
156
|
+
this.lastConnectedAt = Date.now();
|
|
95
157
|
this.delay = 2_000; // reset backoff on a good auth
|
|
96
158
|
this.runtimeId = String(frame.runtimeId ?? "");
|
|
97
159
|
console.log(`${this.tag} authenticated as runtime ${this.runtimeId} (control=${String(frame.control)})`);
|
|
@@ -147,9 +209,16 @@ export class ControlSocket {
|
|
|
147
209
|
scheduleReconnect() {
|
|
148
210
|
if (this.stopped)
|
|
149
211
|
return;
|
|
212
|
+
// A watchdog forceReconnect() may have already opened a fresh socket; only
|
|
213
|
+
// one pending reconnect timer at a time (never stack sockets).
|
|
214
|
+
if (this.reconnectTimer)
|
|
215
|
+
return;
|
|
150
216
|
console.log(`${this.tag} reconnecting in ${this.delay / 1000}s`);
|
|
151
|
-
|
|
152
|
-
|
|
217
|
+
this.reconnectTimer = setTimeout(() => {
|
|
218
|
+
this.reconnectTimer = null;
|
|
219
|
+
this.connect();
|
|
220
|
+
}, this.delay);
|
|
221
|
+
this.reconnectTimer.unref?.();
|
|
153
222
|
this.delay = Math.min(this.delay * 2, 30_000);
|
|
154
223
|
}
|
|
155
224
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nopeek/agent-bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Run your own agents as E2EE NoPeek bots. Pairs with one-time codes (multiple accounts per computer), runs every bot each account owns, and pipes messages to any command or webhook.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|