@nopeek/agent-bridge 0.7.5 → 0.7.6
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/backends.js +12 -2
- package/dist/bridge.d.ts +1 -1
- package/dist/bridge.js +19 -9
- package/dist/cli.js +18 -1
- package/dist/service.d.ts +12 -0
- package/dist/service.js +55 -0
- package/package.json +1 -1
package/dist/backends.js
CHANGED
|
@@ -364,6 +364,7 @@ export function hermesBrain(cfg) {
|
|
|
364
364
|
out += `${line}\n`;
|
|
365
365
|
onChunk?.(`${line}\n`);
|
|
366
366
|
};
|
|
367
|
+
let timedOut = false;
|
|
367
368
|
const finish = () => {
|
|
368
369
|
if (settled)
|
|
369
370
|
return;
|
|
@@ -381,10 +382,11 @@ export function hermesBrain(cfg) {
|
|
|
381
382
|
// "No session found" can land on stderr in some hermes builds.
|
|
382
383
|
if (!out.trim() && HERMES_NO_SESSION.test(stderr.trim()))
|
|
383
384
|
noSession = true;
|
|
384
|
-
resolvePromise({ reply: out.trim(), noSession, sessionId, stderr });
|
|
385
|
+
resolvePromise({ reply: out.trim(), noSession, sessionId, stderr, timedOut });
|
|
385
386
|
};
|
|
386
387
|
const timer = setTimeout(() => {
|
|
387
388
|
console.error(`${tag} timed out after ${cfg.brainTimeoutMs / 1000}s, killing`);
|
|
389
|
+
timedOut = true;
|
|
388
390
|
child.kill("SIGKILL");
|
|
389
391
|
finish();
|
|
390
392
|
}, cfg.brainTimeoutMs);
|
|
@@ -401,7 +403,7 @@ export function hermesBrain(cfg) {
|
|
|
401
403
|
console.error(`${tag} spawn error: ${err.message}`);
|
|
402
404
|
if (!settled) {
|
|
403
405
|
settled = true;
|
|
404
|
-
resolvePromise({ reply: "", noSession: false, sessionId: null, stderr: err.message });
|
|
406
|
+
resolvePromise({ reply: "", noSession: false, sessionId: null, stderr: err.message, timedOut: false });
|
|
405
407
|
}
|
|
406
408
|
});
|
|
407
409
|
child.on("close", () => {
|
|
@@ -448,6 +450,14 @@ export function hermesBrain(cfg) {
|
|
|
448
450
|
if (!run.reply) {
|
|
449
451
|
if (run.stderr.trim())
|
|
450
452
|
console.error(`${tag} stderr: ${run.stderr.slice(0, 1000)}`);
|
|
453
|
+
// Distinguish the actual failure instead of always blaming "no
|
|
454
|
+
// authenticated provider" — that message was previously hardcoded for
|
|
455
|
+
// EVERY empty-reply cause (including a plain timeout on a slow cold
|
|
456
|
+
// start), which misdirects the user to run a command that isn't the fix.
|
|
457
|
+
if (run.timedOut) {
|
|
458
|
+
const secs = Math.round(cfg.brainTimeoutMs / 1000);
|
|
459
|
+
return `⚠️ My brain took longer than ${secs}s to respond and I had to give up — this can happen on the very first message while a session/provider is warming up. Try messaging me again.`;
|
|
460
|
+
}
|
|
451
461
|
return "⚠️ My brain isn't reachable right now — Hermes has no authenticated provider on the host. Run 'hermes model' there, then message me again.";
|
|
452
462
|
}
|
|
453
463
|
return run.reply;
|
package/dist/bridge.d.ts
CHANGED
package/dist/bridge.js
CHANGED
|
@@ -17,7 +17,7 @@ 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.6";
|
|
21
21
|
/** How often to re-probe + report brain availability to the server. */
|
|
22
22
|
const CAPABILITIES_INTERVAL_MS = 5 * 60_000;
|
|
23
23
|
// ---- Reconnect watchdog -----------------------------------------------------
|
|
@@ -33,6 +33,11 @@ const WATCHDOG_INTERVAL_MS = 30_000;
|
|
|
33
33
|
const SOFT_STALE_MS = 90_000;
|
|
34
34
|
/** Every bot down longer than this (sustained) → exit for a clean relaunch. */
|
|
35
35
|
const HARD_STALE_MS = 5 * 60_000;
|
|
36
|
+
/** A SINGLE bot down this long despite repeated forced restarts → exit too.
|
|
37
|
+
* Covers a bot wedged for a reason forceRestart() can't fix from inside the
|
|
38
|
+
* process (e.g. a stuck OS-level DNS resolution) while OTHER bots are fine —
|
|
39
|
+
* the all-bots-down HARD_STALE_MS check alone would never catch that case. */
|
|
40
|
+
const ULTRA_STALE_MS = 10 * 60_000;
|
|
36
41
|
export class PairError extends Error {
|
|
37
42
|
code;
|
|
38
43
|
constructor(code, message) {
|
|
@@ -357,20 +362,25 @@ export class BridgeApp {
|
|
|
357
362
|
let allLiveness = [];
|
|
358
363
|
for (const r of this.runtimes)
|
|
359
364
|
allLiveness = allLiveness.concat(r.watchdogSoftPass(SOFT_STALE_MS));
|
|
360
|
-
// Layer 4 (the guarantee):
|
|
361
|
-
//
|
|
362
|
-
// has
|
|
363
|
-
//
|
|
365
|
+
// Layer 4 (the guarantee): exit for a clean relaunch when soft recovery has
|
|
366
|
+
// clearly failed — either EVERY bot is down past HARD_STALE_MS, or a SINGLE
|
|
367
|
+
// bot has been down past ULTRA_STALE_MS despite repeated forceRestart()
|
|
368
|
+
// calls (a bot wedged for a reason forceRestart can't fix from inside the
|
|
369
|
+
// process — e.g. a stuck OS-level DNS resolution — while other bots are
|
|
370
|
+
// fine; the all-down check alone would never catch that). Require the
|
|
371
|
+
// condition sustained over two consecutive checks, then exit.
|
|
364
372
|
const hasBots = allLiveness.length > 0;
|
|
365
373
|
const allHardStale = hasBots && allLiveness.every((ms) => ms > HARD_STALE_MS);
|
|
366
|
-
|
|
374
|
+
const anyUltraStale = hasBots && allLiveness.some((ms) => ms > ULTRA_STALE_MS);
|
|
375
|
+
if (this.paired && hasBots && (allHardStale || anyUltraStale)) {
|
|
367
376
|
this.hardStaleStreak++;
|
|
368
|
-
const mins = Math.round(HARD_STALE_MS / 60_000);
|
|
377
|
+
const mins = Math.round((allHardStale ? HARD_STALE_MS : ULTRA_STALE_MS) / 60_000);
|
|
378
|
+
const reason = allHardStale ? `no bot connected for ${mins}min across ${allLiveness.length} bot(s)` : `a bot stuck down > ${mins}min`;
|
|
369
379
|
if (this.hardStaleStreak >= 2) {
|
|
370
|
-
console.error(`[watchdog]
|
|
380
|
+
console.error(`[watchdog] ${reason} — exiting for a clean relaunch`);
|
|
371
381
|
process.exit(1);
|
|
372
382
|
}
|
|
373
|
-
console.error(`[watchdog]
|
|
383
|
+
console.error(`[watchdog] ${reason} — will exit for a clean relaunch if still stuck next check`);
|
|
374
384
|
}
|
|
375
385
|
else {
|
|
376
386
|
this.hardStaleStreak = 0;
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { loadConfig, HELP } from "./config.js";
|
|
9
9
|
import { BridgeApp, VERSION } from "./bridge.js";
|
|
10
10
|
import { startLocalApi } from "./localapi.js";
|
|
11
|
-
import { installService, uninstallService, printStatus } from "./service.js";
|
|
11
|
+
import { installService, uninstallService, printStatus, checkForUpdate } from "./service.js";
|
|
12
12
|
// ---------------------------------------------------------------- guards ----
|
|
13
13
|
// The bridge must never die to a stray rejection deep inside a WS/crypto
|
|
14
14
|
// callback — one flaky bot cannot take the fleet down.
|
|
@@ -22,6 +22,14 @@ if (typeof WebSocket === "undefined" || !globalThis.crypto?.subtle) {
|
|
|
22
22
|
console.error(`@nopeek/agent-bridge needs Node >= 22 (global WebSocket + fetch + WebCrypto). Current: ${process.version}`);
|
|
23
23
|
process.exit(1);
|
|
24
24
|
}
|
|
25
|
+
// Every log line gets an ISO timestamp. The service log otherwise has no time
|
|
26
|
+
// axis at all — investigating an incident (e.g. "which network blip closed
|
|
27
|
+
// every bot's socket, and when did the watchdog recover it") is only possible
|
|
28
|
+
// if log lines can be correlated against system events by clock time.
|
|
29
|
+
for (const level of ["log", "error", "warn"]) {
|
|
30
|
+
const orig = console[level].bind(console);
|
|
31
|
+
console[level] = (...args) => orig(`[${new Date().toISOString()}]`, ...args);
|
|
32
|
+
}
|
|
25
33
|
// ----------------------------------------------------------- subcommands ----
|
|
26
34
|
const argv = process.argv.slice(2);
|
|
27
35
|
const SUBCOMMANDS = new Set(["install", "uninstall", "status", "run", "help"]);
|
|
@@ -100,6 +108,15 @@ catch (err) {
|
|
|
100
108
|
process.exit(1);
|
|
101
109
|
}
|
|
102
110
|
app.start();
|
|
111
|
+
// Periodic self-update: a published fix (e.g. a reconnect-reliability bug)
|
|
112
|
+
// otherwise sits unused on already-installed bridges until someone manually
|
|
113
|
+
// reinstalls. Check shortly after startup, then every 6h; each check exits the
|
|
114
|
+
// process on a successful update so the always-restart service (KeepAlive)
|
|
115
|
+
// relaunches running the new code. No-op on install methods it doesn't
|
|
116
|
+
// recognize (see checkForUpdate) — never touches a dev checkout.
|
|
117
|
+
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60_000;
|
|
118
|
+
setTimeout(() => void checkForUpdate(VERSION, cfg.homeDir), 60_000).unref();
|
|
119
|
+
setInterval(() => void checkForUpdate(VERSION, cfg.homeDir), UPDATE_CHECK_INTERVAL_MS).unref();
|
|
103
120
|
const shutdown = (signal) => {
|
|
104
121
|
console.log(`[bridge] ${signal} — shutting down`);
|
|
105
122
|
app.stop();
|
package/dist/service.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { BridgeConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Periodic self-update: compares the running version against npm's published
|
|
4
|
+
* `latest` and, if newer, re-installs using the SAME method this process is
|
|
5
|
+
* actually running from (global npm root, or the private <home>/app copy —
|
|
6
|
+
* see resolveEntrypoint above) and exits so the always-restart service
|
|
7
|
+
* relaunches running the fresh code. Only acts on an install method it
|
|
8
|
+
* recognizes — a pnpm-linked dev checkout or anything unrecognized is left
|
|
9
|
+
* alone (safety over completeness). This is how a published fix (like a
|
|
10
|
+
* reconnect-reliability bug) actually reaches already-installed bridges
|
|
11
|
+
* instead of sitting unused until someone manually reinstalls.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkForUpdate(currentVersion: string, homeDir: string): Promise<void>;
|
|
2
14
|
export declare function installService(cfg: BridgeConfig, noOpen?: boolean): Promise<void>;
|
|
3
15
|
export declare function uninstallService(): void;
|
|
4
16
|
export declare function printStatus(port: number): Promise<void>;
|
package/dist/service.js
CHANGED
|
@@ -32,6 +32,61 @@ function resolveEntrypoint(homeDir) {
|
|
|
32
32
|
throw new Error(`expected ${entry} after install — not found`);
|
|
33
33
|
return entry;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Periodic self-update: compares the running version against npm's published
|
|
37
|
+
* `latest` and, if newer, re-installs using the SAME method this process is
|
|
38
|
+
* actually running from (global npm root, or the private <home>/app copy —
|
|
39
|
+
* see resolveEntrypoint above) and exits so the always-restart service
|
|
40
|
+
* relaunches running the fresh code. Only acts on an install method it
|
|
41
|
+
* recognizes — a pnpm-linked dev checkout or anything unrecognized is left
|
|
42
|
+
* alone (safety over completeness). This is how a published fix (like a
|
|
43
|
+
* reconnect-reliability bug) actually reaches already-installed bridges
|
|
44
|
+
* instead of sitting unused until someone manually reinstalls.
|
|
45
|
+
*/
|
|
46
|
+
export async function checkForUpdate(currentVersion, homeDir) {
|
|
47
|
+
let latest;
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch("https://registry.npmjs.org/@nopeek/agent-bridge/latest", {
|
|
50
|
+
signal: AbortSignal.timeout(10_000),
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
return;
|
|
54
|
+
const body = (await res.json());
|
|
55
|
+
latest = body.version ?? "";
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return; // offline / registry unreachable — try again next check
|
|
59
|
+
}
|
|
60
|
+
if (!latest || latest === currentVersion)
|
|
61
|
+
return;
|
|
62
|
+
const self = realpathSync(process.argv[1]);
|
|
63
|
+
let installArgs = null;
|
|
64
|
+
try {
|
|
65
|
+
const globalRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf8" }).trim();
|
|
66
|
+
if (globalRoot && self.startsWith(globalRoot))
|
|
67
|
+
installArgs = ["install", "-g", "@nopeek/agent-bridge@latest"];
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* npm not on PATH / no permission to query — fall through to the private-copy check */
|
|
71
|
+
}
|
|
72
|
+
if (!installArgs) {
|
|
73
|
+
const privateHome = join(homeDir, "app");
|
|
74
|
+
if (self.startsWith(privateHome))
|
|
75
|
+
installArgs = ["install", "--prefix", privateHome, "@nopeek/agent-bridge@latest"];
|
|
76
|
+
}
|
|
77
|
+
if (!installArgs) {
|
|
78
|
+
console.log(`[update] v${latest} is available (running v${currentVersion}) but this install (${self}) isn't a recognized global/private copy — skipping auto-update`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(`[update] v${latest} available (running v${currentVersion}) — updating via 'npm ${installArgs.join(" ")}'`);
|
|
82
|
+
const r = spawnSync("npm", installArgs, { stdio: "inherit" });
|
|
83
|
+
if (r.status !== 0) {
|
|
84
|
+
console.error(`[update] npm install failed (exit ${r.status}) — will retry next check`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.log(`[update] updated to v${latest} — exiting for the service to relaunch with the new version`);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
35
90
|
function launchctl(args, ignoreFailure = false) {
|
|
36
91
|
try {
|
|
37
92
|
execFileSync("launchctl", args, { stdio: "pipe" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nopeek/agent-bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
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",
|