agent-yes 1.122.2 → 1.123.0

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 (56) hide show
  1. package/default.config.yaml +19 -0
  2. package/dist/{SUPPORTED_CLIS-BTu2brih.js → SUPPORTED_CLIS-B4O2cFlt.js} +2 -2
  3. package/dist/SUPPORTED_CLIS-DHkqGoNv.js +8 -0
  4. package/dist/{agent-yes.config-z-IPzH5U.js → agent-yes.config-D6ycMApr.js} +2 -65
  5. package/dist/cli.js +6 -6
  6. package/dist/configShared-C5QaNPnz.js +71 -0
  7. package/dist/{globalPidIndex-gZuTvTBs.js → globalPidIndex-C7r2m6s7.js} +19 -20
  8. package/dist/index.js +4 -4
  9. package/dist/pidStore-C4c2O15q.js +5 -0
  10. package/dist/{pidStore-B5vBu8Px.js → pidStore-CGKIhaJO.js} +5 -4
  11. package/dist/reaper-BLVA780B.js +3 -0
  12. package/dist/{reaper-Dj8R7ltI.js → reaper-BkjPN7mw.js} +24 -2
  13. package/dist/{remotes-CpGcTr7A.js → remotes-BRCDVnR7.js} +1 -1
  14. package/dist/{remotes-D2fqaRU8.js → remotes-D8GvSbhf.js} +1 -1
  15. package/dist/{schedule-DgRrdA_n.js → schedule-DULdIkU9.js} +7 -7
  16. package/dist/{serve-tn7ZetZs.js → serve-r_2v9EKc.js} +202 -58
  17. package/dist/{setup-dZhgpNse.js → setup-DHa6fX8M.js} +3 -3
  18. package/dist/{share-CksllWW-.js → share-YuM6-Q6A.js} +78 -4
  19. package/dist/{subcommands-D9BWZilr.js → subcommands-B13Kto-u.js} +647 -32
  20. package/dist/subcommands-Tv6AwUkD.js +7 -0
  21. package/dist/{tray-DjCIyakK.js → tray-BVnJLThD.js} +1 -1
  22. package/dist/{ts-CIf0uaR7.js → ts-DgukRoEI.js} +10 -7
  23. package/dist/{versionChecker-DjxKi4qe.js → versionChecker-BqOr1YqC.js} +2 -2
  24. package/dist/{workspaceConfig-XP2NEWmV.js → workspaceConfig-BJO4fzEn.js} +1 -1
  25. package/lab/ui/console-logic.js +222 -10
  26. package/lab/ui/icon.svg +5 -0
  27. package/lab/ui/index.html +689 -14
  28. package/lab/ui/landing.html +276 -0
  29. package/lab/ui/manifest.webmanifest +14 -0
  30. package/lab/ui/sw.js +56 -0
  31. package/package.json +5 -1
  32. package/ts/agentTree.spec.ts +92 -0
  33. package/ts/agentTree.ts +149 -0
  34. package/ts/configShared.ts +4 -0
  35. package/ts/globalPidIndex.ts +28 -20
  36. package/ts/idleWaiter.spec.ts +7 -1
  37. package/ts/index.ts +9 -0
  38. package/ts/lsWatch.spec.ts +61 -0
  39. package/ts/lsWatch.ts +94 -0
  40. package/ts/needsInput.spec.ts +55 -0
  41. package/ts/needsInput.ts +68 -0
  42. package/ts/pidStore.ts +3 -0
  43. package/ts/reaper.spec.ts +26 -2
  44. package/ts/reaper.ts +25 -0
  45. package/ts/resultEnvelope.spec.ts +43 -0
  46. package/ts/resultEnvelope.ts +88 -0
  47. package/ts/serve.ts +276 -41
  48. package/ts/share.ts +156 -3
  49. package/ts/subcommands.ts +0 -0
  50. package/ts/todoParse.spec.ts +68 -0
  51. package/ts/todoParse.ts +88 -0
  52. package/ts/utils.spec.ts +4 -1
  53. package/dist/SUPPORTED_CLIS-DcOKE9Nz.js +0 -8
  54. package/dist/pidStore-7y1cTcAE.js +0 -5
  55. package/dist/reaper-HqcUms2d.js +0 -3
  56. package/dist/subcommands-D8sHibKu.js +0 -6
@@ -1,17 +1,19 @@
1
- import "./ts-CIf0uaR7.js";
1
+ import "./ts-DgukRoEI.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import { r as getInstalledPackage } from "./versionChecker-DjxKi4qe.js";
4
- import "./pidStore-B5vBu8Px.js";
5
- import "./globalPidIndex-gZuTvTBs.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BTu2brih.js";
7
- import "./remotes-D2fqaRU8.js";
8
- import { c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, m as snapshotStatus, r as controlCodeFromName, u as readNotes } from "./subcommands-D9BWZilr.js";
3
+ import { r as getInstalledPackage } from "./versionChecker-BqOr1YqC.js";
4
+ import "./pidStore-CGKIhaJO.js";
5
+ import { a as updateGlobalPidStatus } from "./globalPidIndex-C7r2m6s7.js";
6
+ import { t as pgidForWrapper } from "./reaper-BkjPN7mw.js";
7
+ import "./configShared-C5QaNPnz.js";
8
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-B4O2cFlt.js";
9
+ import "./remotes-D8GvSbhf.js";
10
+ import { f as readNotes, g as snapshotStatus, m as resolveOne, o as extractTaskCounts, p as renderRawLog, r as controlCodeFromName, u as listRecords, v as writeToIpc } from "./subcommands-B13Kto-u.js";
9
11
  import yargs from "yargs";
10
- import { mkdir, open, readFile, writeFile } from "fs/promises";
12
+ import { mkdir, open, readFile, stat, writeFile } from "fs/promises";
11
13
  import { homedir, hostname, userInfo } from "os";
12
14
  import path from "path";
13
15
  import { fileURLToPath } from "node:url";
14
- import { watch } from "node:fs";
16
+ import { renameSync, watch, writeFileSync } from "node:fs";
15
17
  import { randomBytes, timingSafeEqual } from "crypto";
16
18
 
17
19
  //#region ts/serve.ts
@@ -22,6 +24,11 @@ function agentYesHome() {
22
24
  function tokenPath() {
23
25
  return path.join(agentYesHome(), ".serve-token");
24
26
  }
27
+ function heartbeatPath() {
28
+ return path.join(agentYesHome(), ".serve-heartbeat");
29
+ }
30
+ const HEARTBEAT_WRITE_MS = 5e3;
31
+ const HEARTBEAT_STALE_MS = 15e3;
25
32
  async function loadOrCreateToken(tokenFlag) {
26
33
  if (tokenFlag) return tokenFlag;
27
34
  try {
@@ -244,6 +251,16 @@ async function cmdServeDaemon(sub, args) {
244
251
  ]);
245
252
  }
246
253
  const serveArgv = ayServeArgv(effArgs);
254
+ const oxmgrHealth = effArgs.some((a) => a.startsWith("--webrtc") || a.startsWith("--share")) && mgr.id === "oxmgr" ? [
255
+ "--health-cmd",
256
+ ayServeArgv(["healthcheck"]).join(" "),
257
+ "--health-interval",
258
+ "10",
259
+ "--health-timeout",
260
+ "5",
261
+ "--health-max-failures",
262
+ "3"
263
+ ] : [];
247
264
  const startArgv = mgr.id === "oxmgr" ? [
248
265
  mgr.bin,
249
266
  "start",
@@ -251,7 +268,10 @@ async function cmdServeDaemon(sub, args) {
251
268
  "--name",
252
269
  DAEMON_NAME,
253
270
  "--restart",
254
- "always"
271
+ "always",
272
+ "--max-restarts",
273
+ "1000000",
274
+ ...oxmgrHealth
255
275
  ] : [
256
276
  mgr.bin,
257
277
  "start",
@@ -395,6 +415,18 @@ Options:
395
415
  }
396
416
  const sub = rest[0];
397
417
  if (sub === "status") return cmdServeStatus(rest.slice(1));
418
+ if (sub === "healthcheck") {
419
+ try {
420
+ const raw = (await readFile(heartbeatPath(), "utf-8")).trim();
421
+ const ts = Number(raw);
422
+ const age = Date.now() - ts;
423
+ if (raw.length > 0 && Number.isFinite(ts) && ts > 0 && age > HEARTBEAT_STALE_MS) {
424
+ process.stderr.write(`unhealthy: serve heartbeat stale by ${age}ms\n`);
425
+ return 1;
426
+ }
427
+ } catch {}
428
+ return 0;
429
+ }
398
430
  if (sub === "install" || sub === "uninstall" || sub === "logs") return cmdServeDaemon(sub, rest.slice(1));
399
431
  const argv = await yargs(rest).usage("Usage: ay serve [options]").option("port", {
400
432
  type: "number",
@@ -482,21 +514,29 @@ Options:
482
514
  return null;
483
515
  }
484
516
  };
485
- const GIT_TTL_MS = 5e3;
486
- const gitCache = /* @__PURE__ */ new Map();
487
- const gitStatus = async (cwd) => {
488
- if (!cwd) return null;
489
- const now = Date.now();
490
- const hit = gitCache.get(cwd);
491
- if (hit && now - hit.at < GIT_TTL_MS) return hit.val;
492
- let val = null;
517
+ const taskCache = /* @__PURE__ */ new Map();
518
+ const logTasks = async (logFile) => {
519
+ if (!logFile) return null;
493
520
  try {
494
- const proc = Bun.spawn([
495
- "git",
496
- "status",
497
- "--porcelain",
498
- "--branch"
499
- ], {
521
+ const { size, mtimeMs } = await stat(logFile);
522
+ const hit = taskCache.get(logFile);
523
+ if (hit && hit.size === size && hit.mtimeMs === mtimeMs) return hit.tasks;
524
+ const tasks = await extractTaskCounts(logFile);
525
+ taskCache.set(logFile, {
526
+ size,
527
+ mtimeMs,
528
+ tasks
529
+ });
530
+ return tasks;
531
+ } catch {
532
+ return null;
533
+ }
534
+ };
535
+ const GIT_DEBOUNCE_MS = 800;
536
+ const GIT_SAFETY_MS = 6e4;
537
+ const runGit = async (args, cwd) => {
538
+ try {
539
+ const proc = Bun.spawn(["git", ...args], {
500
540
  cwd,
501
541
  stdout: "pipe",
502
542
  stderr: "ignore",
@@ -504,35 +544,89 @@ Options:
504
544
  });
505
545
  const out = await new Response(proc.stdout).text();
506
546
  await proc.exited;
507
- if (proc.exitCode === 0) {
508
- const lines = out.split("\n");
509
- const h = /^## (.+)$/.exec(lines[0] ?? "")?.[1] ?? "";
510
- const unborn = /^No commits yet on (.+)$/.exec(h);
511
- const branch = unborn ? unborn[1] : /^(.+?)(?:\.\.\.|\s|$)/.exec(h)?.[1] || null;
512
- const ahead = Number(/\bahead (\d+)/.exec(h)?.[1] ?? 0);
513
- const behind = Number(/\bbehind (\d+)/.exec(h)?.[1] ?? 0);
514
- const changed = lines.slice(1).filter((l) => l.trim().length > 0).length;
515
- val = {
516
- branch,
517
- dirty: changed > 0,
518
- changed,
519
- ahead,
520
- behind
521
- };
547
+ return proc.exitCode === 0 ? out : null;
548
+ } catch {
549
+ return null;
550
+ }
551
+ };
552
+ const parseGitStatus = (out) => {
553
+ const lines = out.split("\n");
554
+ const h = /^## (.+)$/.exec(lines[0] ?? "")?.[1] ?? "";
555
+ const unborn = /^No commits yet on (.+)$/.exec(h);
556
+ const branch = unborn ? unborn[1] : /^(.+?)(?:\.\.\.|\s|$)/.exec(h)?.[1] || null;
557
+ const ahead = Number(/\bahead (\d+)/.exec(h)?.[1] ?? 0);
558
+ const behind = Number(/\bbehind (\d+)/.exec(h)?.[1] ?? 0);
559
+ const changed = lines.slice(1).filter((l) => l.trim().length > 0).length;
560
+ return {
561
+ branch,
562
+ dirty: changed > 0,
563
+ changed,
564
+ ahead,
565
+ behind
566
+ };
567
+ };
568
+ const rootOfCwd = /* @__PURE__ */ new Map();
569
+ const resolveRoot = async (cwd) => {
570
+ const cached = rootOfCwd.get(cwd);
571
+ if (cached !== void 0) return cached;
572
+ const root = (await runGit(["rev-parse", "--show-toplevel"], cwd) ?? "").trim();
573
+ rootOfCwd.set(cwd, root);
574
+ return root;
575
+ };
576
+ const repoWatch = /* @__PURE__ */ new Map();
577
+ const recompute = (root, rw) => {
578
+ if (rw.timer) return;
579
+ rw.timer = setTimeout(async () => {
580
+ rw.timer = null;
581
+ if (rw.busy) return void recompute(root, rw);
582
+ rw.busy = true;
583
+ try {
584
+ const out = await runGit([
585
+ "status",
586
+ "--porcelain",
587
+ "--branch"
588
+ ], root);
589
+ if (out != null) rw.val = parseGitStatus(out);
590
+ } finally {
591
+ rw.busy = false;
522
592
  }
593
+ }, GIT_DEBOUNCE_MS);
594
+ };
595
+ const ensureRepoWatch = (root) => {
596
+ const existing = repoWatch.get(root);
597
+ if (existing) return existing;
598
+ const rw = {
599
+ val: null,
600
+ busy: false,
601
+ timer: null
602
+ };
603
+ repoWatch.set(root, rw);
604
+ recompute(root, rw);
605
+ const onChange = (file) => {
606
+ if (file.includes(".agent-yes") || file.includes("node_modules") || file.endsWith(".lock")) return;
607
+ recompute(root, rw);
608
+ };
609
+ try {
610
+ watch(root, { recursive: true }, (_e, f) => onChange(String(f ?? "")));
523
611
  } catch {
524
- val = null;
612
+ try {
613
+ watch(path.join(root, ".git"), (_e, f) => onChange(".git/" + String(f ?? "")));
614
+ } catch {}
525
615
  }
526
- gitCache.set(cwd, {
527
- at: now,
528
- val
529
- });
530
- return val;
616
+ setInterval(() => recompute(root, rw), GIT_SAFETY_MS);
617
+ return rw;
618
+ };
619
+ const gitStatus = async (cwd) => {
620
+ if (!cwd) return null;
621
+ const root = await resolveRoot(cwd);
622
+ if (!root) return null;
623
+ return ensureRepoWatch(root).val;
531
624
  };
532
625
  const withMeta = async (r) => ({
533
626
  ...r,
534
627
  title: await logTitle(r.log_file),
535
- git: r.status === "exited" ? null : await gitStatus(r.cwd)
628
+ git: r.status === "exited" ? null : await gitStatus(r.cwd),
629
+ tasks: r.status === "exited" ? null : await logTasks(r.log_file)
536
630
  });
537
631
  const apiFetch = async (req) => {
538
632
  if (!checkAuth(req, token)) return new Response("Unauthorized", { status: 401 });
@@ -791,6 +885,46 @@ Options:
791
885
  return new Response(e.message, { status: 404 });
792
886
  }
793
887
  }
888
+ if (req.method === "POST" && p === "/api/kill") {
889
+ let body;
890
+ try {
891
+ body = await req.json();
892
+ } catch {
893
+ return new Response("invalid JSON body", { status: 400 });
894
+ }
895
+ const keyword = body.keyword;
896
+ if (!keyword || typeof keyword !== "string") return new Response("missing keyword", { status: 400 });
897
+ if (process.platform === "win32") return new Response("force-kill unsupported on a Windows serve", { status: 501 });
898
+ try {
899
+ const record = await resolveOne(keyword, defaultOpts({ all: true }));
900
+ const killed = [];
901
+ const sig = (target, label) => {
902
+ if (!target || target <= 1) return;
903
+ try {
904
+ process.kill(target, "SIGKILL");
905
+ killed.push(label);
906
+ } catch {}
907
+ };
908
+ const pgid = await pgidForWrapper(record.wrapper_pid ?? 0);
909
+ if (pgid && pgid > 1) try {
910
+ process.kill(-pgid, "SIGKILL");
911
+ killed.push(`group ${pgid}`);
912
+ } catch {}
913
+ sig(record.pid, `pid ${record.pid}`);
914
+ if (record.wrapper_pid && record.wrapper_pid !== record.pid) sig(record.wrapper_pid, `wrapper ${record.wrapper_pid}`);
915
+ await updateGlobalPidStatus(record.pid, {
916
+ status: "exited",
917
+ exit_reason: "force-killed via console"
918
+ }).catch(() => {});
919
+ return Response.json({
920
+ ok: true,
921
+ pid: record.pid,
922
+ killed
923
+ });
924
+ } catch (e) {
925
+ return new Response(e.message, { status: 404 });
926
+ }
927
+ }
794
928
  const resizeM = /^\/api\/resize\/(.+)$/.exec(p);
795
929
  if (req.method === "POST" && resizeM) {
796
930
  const keyword = decodeURIComponent(resizeM[1]);
@@ -918,7 +1052,7 @@ Options:
918
1052
  const webrtcVal = argv.webrtc ?? argv.share;
919
1053
  const explicitUrl = typeof webrtcVal === "string" && webrtcVal.startsWith("webrtc://") ? webrtcVal : void 0;
920
1054
  try {
921
- const { startShare, loadOrCreateShareRoom } = await import("./share-CksllWW-.js");
1055
+ const { startShare, loadOrCreateShareRoom } = await import("./share-YuM6-Q6A.js");
922
1056
  const linkFile = path.join(process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes"), ".share-link");
923
1057
  const announce = async (room, link, rotated) => {
924
1058
  const lead = rotated ? "the room was rejected by signaling (stale generation) — rotated to a fresh link" : "shared over WebRTC — open this link (the token is eaten from the URL on open)";
@@ -945,22 +1079,32 @@ Options:
945
1079
  if (!wantHttp) return 1;
946
1080
  }
947
1081
  }
1082
+ let heartbeat;
1083
+ if (wantWebrtc) {
1084
+ const stamp = () => {
1085
+ try {
1086
+ const tmp = `${heartbeatPath()}.tmp`;
1087
+ writeFileSync(tmp, String(Date.now()));
1088
+ renameSync(tmp, heartbeatPath());
1089
+ } catch {}
1090
+ };
1091
+ stamp();
1092
+ heartbeat = setInterval(stamp, HEARTBEAT_WRITE_MS);
1093
+ }
948
1094
  process.stdout.write(`(Ctrl-C to stop)\n`);
1095
+ const shutdown = (resolve) => {
1096
+ if (heartbeat) clearInterval(heartbeat);
1097
+ closeShare?.();
1098
+ server?.stop();
1099
+ resolve();
1100
+ };
949
1101
  await new Promise((resolve) => {
950
- process.on("SIGINT", () => {
951
- closeShare?.();
952
- server?.stop();
953
- resolve();
954
- });
955
- process.on("SIGTERM", () => {
956
- closeShare?.();
957
- server?.stop();
958
- resolve();
959
- });
1102
+ process.on("SIGINT", () => shutdown(resolve));
1103
+ process.on("SIGTERM", () => shutdown(resolve));
960
1104
  });
961
1105
  return 0;
962
1106
  }
963
1107
 
964
1108
  //#endregion
965
1109
  export { cmdServe };
966
- //# sourceMappingURL=serve-tn7ZetZs.js.map
1110
+ //# sourceMappingURL=serve-r_2v9EKc.js.map
@@ -1,4 +1,4 @@
1
- import { r as setWorkspaceRoot, t as getWorkspaceRoot } from "./workspaceConfig-XP2NEWmV.js";
1
+ import { r as setWorkspaceRoot, t as getWorkspaceRoot } from "./workspaceConfig-BJO4fzEn.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import { stdin, stdout } from "node:process";
4
4
  import { createInterface } from "node:readline/promises";
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
32
32
  if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
33
33
  if (noShare) return 0;
34
34
  process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
35
- const { cmdServe } = await import("./serve-tn7ZetZs.js");
35
+ const { cmdServe } = await import("./serve-r_2v9EKc.js");
36
36
  return cmdServe([
37
37
  "install",
38
38
  "--share",
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
42
42
 
43
43
  //#endregion
44
44
  export { cmdSetup };
45
- //# sourceMappingURL=setup-dZhgpNse.js.map
45
+ //# sourceMappingURL=setup-DHa6fX8M.js.map
@@ -181,6 +181,13 @@ const SUB = "ay-signal-1";
181
181
  const DEFAULT_SIGHOST = "s.agent-yes.com";
182
182
  const HOST_HEARTBEAT_MS = 2e4;
183
183
  const SIG_REFRESH_MS = 4 * 6e4;
184
+ const MAX_PEER_SETUP_FAILURES = 3;
185
+ const PEER_JOIN_GAP_MS = 300;
186
+ const STARTPEER_TIMEOUT_MS = 1e4;
187
+ const MAX_PEER_JOIN_QUEUE = 50;
188
+ const IDLE_RESTART_UPTIME_MS = 25 * 6e4;
189
+ const HARD_RESTART_UPTIME_MS = 45 * 6e4;
190
+ const IDLE_RESTART_CHECK_MS = 6e4;
184
191
  const STUN = [{ urls: "stun:stun.l.google.com:19302" }];
185
192
  let iceCache = null;
186
193
  async function getIceServers() {
@@ -313,7 +320,7 @@ async function startShare(opts) {
313
320
  if (!v2) throw new Error("refusing to host an unencrypted room — delete ~/.agent-yes/.share-room to rotate to an encrypted link");
314
321
  let S = firstS;
315
322
  const wsScheme = host.startsWith("localhost") || host.startsWith("127.") ? "ws" : "wss";
316
- const ui = host === "s.agent-yes.com" ? "https://agent-yes.com" : "http://localhost:7778";
323
+ const ui = host === "s.agent-yes.com" ? "https://agent-yes.com/w" : "http://localhost:7778/w";
317
324
  const suffix = host === "s.agent-yes.com" ? "" : "@" + host;
318
325
  const mkLink = () => `${ui}/#${room}:${MARKER}${S}${suffix}`;
319
326
  let authToken = await deriveAuthToken(S, room, host);
@@ -342,6 +349,40 @@ async function startShare(opts) {
342
349
  const peers = /* @__PURE__ */ new Map();
343
350
  let closed = false;
344
351
  let currentWs;
352
+ let peerSetupFailures = 0;
353
+ const peerJoinQueue = [];
354
+ let drainingPeerJoins = false;
355
+ const drainPeerJoins = async () => {
356
+ if (drainingPeerJoins) return;
357
+ drainingPeerJoins = true;
358
+ try {
359
+ while (!closed && peerJoinQueue.length) {
360
+ const peerId = peerJoinQueue.shift();
361
+ const ws = currentWs;
362
+ if (!ws) continue;
363
+ try {
364
+ let timer;
365
+ const setup = startPeer(ws, peerId);
366
+ setup.catch(() => {});
367
+ await Promise.race([setup, new Promise((_, reject) => {
368
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("startPeer timeout")), STARTPEER_TIMEOUT_MS);
369
+ })]).finally(() => clearTimeout(timer));
370
+ peerSetupFailures = 0;
371
+ } catch (err) {
372
+ peerSetupFailures++;
373
+ process.stderr.write(`[share] peer setup failed (${peerSetupFailures}/${MAX_PEER_SETUP_FAILURES}): ${err?.message ?? err}\n`);
374
+ closePeer(peerId);
375
+ if (peerSetupFailures >= MAX_PEER_SETUP_FAILURES) {
376
+ process.stderr.write("[share] WebRTC stack wedged after repeated peer-setup failures — exiting so the service manager restarts with a fresh stack\n");
377
+ process.exit(1);
378
+ }
379
+ }
380
+ if (peerJoinQueue.length) await new Promise((r) => setTimeout(r, PEER_JOIN_GAP_MS));
381
+ }
382
+ } finally {
383
+ drainingPeerJoins = false;
384
+ }
385
+ };
345
386
  const connectSignaling = (onReady) => {
346
387
  if (closed) return;
347
388
  const ws = new WebSocket(`${wsScheme}://${host}/${room}`, [SUB]);
@@ -394,8 +435,13 @@ async function startShare(opts) {
394
435
  lastRecv = Date.now();
395
436
  const m = JSON.parse(ev.data);
396
437
  if (m.type === "pong") return;
397
- if (m.type === "peer-join") startPeer(ws, m.peer).catch(() => {});
398
- else if (m.type === "answer") {
438
+ if (m.type === "peer-join") {
439
+ const pid = String(m.peer);
440
+ if (!peers.has(pid) && !peerJoinQueue.includes(pid) && peerJoinQueue.length < MAX_PEER_JOIN_QUEUE) {
441
+ peerJoinQueue.push(pid);
442
+ drainPeerJoins();
443
+ }
444
+ } else if (m.type === "answer") {
399
445
  const peer = peers.get(m.from);
400
446
  if (!peer) return;
401
447
  try {
@@ -490,6 +536,16 @@ async function startShare(opts) {
490
536
  };
491
537
  const offer = await pc.createOffer();
492
538
  await pc.setLocalDescription(offer);
539
+ if (peers.get(peerId) !== peer) {
540
+ try {
541
+ peer.pc.close();
542
+ } catch {}
543
+ return;
544
+ }
545
+ if (ws.readyState !== WebSocket.OPEN) {
546
+ closePeer(peerId);
547
+ return;
548
+ }
493
549
  ws.send(JSON.stringify({
494
550
  type: "offer",
495
551
  to: peerId,
@@ -609,8 +665,26 @@ async function startShare(opts) {
609
665
  }
610
666
  }
611
667
  await new Promise((resolve) => connectSignaling(resolve));
668
+ const startedAt = Date.now();
669
+ const proactiveRestart = process.stdout.isTTY ? void 0 : setInterval(() => {
670
+ if (closed) return;
671
+ const up = Date.now() - startedAt;
672
+ if (peers.size === 0 && up > IDLE_RESTART_UPTIME_MS) {
673
+ process.stderr.write("[share] proactive restart (idle): refreshing the WebRTC stack\n");
674
+ process.exit(0);
675
+ } else if (up > HARD_RESTART_UPTIME_MS) {
676
+ process.stderr.write("[share] proactive restart (max uptime): closing peers, refreshing the WebRTC stack\n");
677
+ try {
678
+ close();
679
+ } finally {
680
+ setTimeout(() => process.exit(0), 250);
681
+ }
682
+ }
683
+ }, IDLE_RESTART_CHECK_MS);
684
+ proactiveRestart?.unref?.();
612
685
  const close = () => {
613
686
  closed = true;
687
+ if (proactiveRestart) clearInterval(proactiveRestart);
614
688
  try {
615
689
  currentWs?.close();
616
690
  } catch {}
@@ -625,4 +699,4 @@ async function startShare(opts) {
625
699
 
626
700
  //#endregion
627
701
  export { loadOrCreateShareRoom, startShare };
628
- //# sourceMappingURL=share-CksllWW-.js.map
702
+ //# sourceMappingURL=share-YuM6-Q6A.js.map