@ghostlytools/cli 1.3.2 → 1.3.4

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 (73) hide show
  1. package/README.md +7 -7
  2. package/SKILL.md +2 -1
  3. package/dist/assets/{api-SYPJgFRU.js → api-BCwaCbKh.js} +2 -2
  4. package/dist/assets/{api-aup3Amud.js → api-Bp_hWAiU.js} +100 -20
  5. package/dist/assets/api-Bp_hWAiU.js.map +1 -0
  6. package/dist/assets/{apiKit-DYkVEs6r.js → apiKit-3MLUyuhL.js} +21 -6
  7. package/dist/assets/apiKit-3MLUyuhL.js.map +1 -0
  8. package/dist/assets/{apps-DcZKu29s.js → apps-B6gqk4GD.js} +1 -1
  9. package/dist/assets/{apps-Rbk2HHXb.js → apps-BXw3Ui_3.js} +91 -24
  10. package/dist/assets/apps-BXw3Ui_3.js.map +1 -0
  11. package/dist/assets/{arkade-CuWlNnCz.js → arkade-CyplNY_v.js} +2 -2
  12. package/dist/assets/{arkade-CuWlNnCz.js.map → arkade-CyplNY_v.js.map} +1 -1
  13. package/dist/assets/{backup-CJWLDHuS.js → backup-BGE4BDzf.js} +6 -6
  14. package/dist/assets/{backup-CJWLDHuS.js.map → backup-BGE4BDzf.js.map} +1 -1
  15. package/dist/assets/{backup-CSMw2GHv.js → backup-Cm8ptQQA.js} +1 -1
  16. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  17. package/dist/assets/{buttons-QEnAGWoC.js → buttons-52NT4sA_.js} +4 -4
  18. package/dist/assets/{buttons-QEnAGWoC.js.map → buttons-52NT4sA_.js.map} +1 -1
  19. package/dist/assets/buttons-PCvHRlkS.js +2 -0
  20. package/dist/assets/{envelope-K27mzoyu.js → envelope-CsRRCrL3.js} +2 -2
  21. package/dist/assets/{envelope-K27mzoyu.js.map → envelope-CsRRCrL3.js.map} +1 -1
  22. package/dist/assets/{errors-DO5z6h_l.js → errors-B-zMxbcd.js} +6 -5
  23. package/dist/assets/{errors-DO5z6h_l.js.map → errors-B-zMxbcd.js.map} +1 -1
  24. package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
  25. package/dist/assets/{ghostly_iroh_web-Dvzh3Y6E.js → ghostly_iroh_web-BsAEFIjl.js} +30 -36
  26. package/dist/assets/ghostly_iroh_web-BsAEFIjl.js.map +1 -0
  27. package/dist/assets/ghostly_iroh_web_bg.wasm +0 -0
  28. package/dist/assets/{groupFiles-DHTrTA4I.js → groupFiles-BurezP4Z.js} +47 -21
  29. package/dist/assets/{groupFiles-DHTrTA4I.js.map → groupFiles-BurezP4Z.js.map} +1 -1
  30. package/dist/assets/{host-C7hA-Za7.js → host-C74Sy5jT.js} +211 -42
  31. package/dist/assets/host-C74Sy5jT.js.map +1 -0
  32. package/dist/assets/{hyperdhtRelay-BVoWY2ua.js → hyperdhtRelay-D0MvfMzX.js} +2 -2
  33. package/dist/assets/{hyperdhtRelay-BVoWY2ua.js.map → hyperdhtRelay-D0MvfMzX.js.map} +1 -1
  34. package/dist/assets/{irohWeb-BJPk9bMn.js → irohWeb-DynqwRwl.js} +3 -3
  35. package/dist/assets/{irohWeb-BJPk9bMn.js.map → irohWeb-DynqwRwl.js.map} +1 -1
  36. package/dist/assets/{main-DC8cUkDx.js → main-DNzQOdNG.js} +168 -57
  37. package/dist/assets/main-DNzQOdNG.js.map +1 -0
  38. package/dist/assets/{mainline-CxbxpoU4.js → mainline-COqCHn1R.js} +2 -2
  39. package/dist/assets/{mainline-CxbxpoU4.js.map → mainline-COqCHn1R.js.map} +1 -1
  40. package/dist/assets/{networkReason-D42iNuUB.js → networkReason-WikfjkIw.js} +2 -2
  41. package/dist/assets/{networkReason-D42iNuUB.js.map → networkReason-WikfjkIw.js.map} +1 -1
  42. package/dist/assets/{nwc-Cm4sYgLv.js → nwc-ClsjYuyP.js} +3 -3
  43. package/dist/assets/{nwc-Cm4sYgLv.js.map → nwc-ClsjYuyP.js.map} +1 -1
  44. package/dist/assets/{nwcProvider-0D7vdPDd.js → nwcProvider-ksMYZra9.js} +2 -2
  45. package/dist/assets/{nwcProvider-0D7vdPDd.js.map → nwcProvider-ksMYZra9.js.map} +1 -1
  46. package/dist/assets/{profiles-BHc9pEYg.js → profiles-gPCjLOQq.js} +2 -2
  47. package/dist/assets/{profiles-BHc9pEYg.js.map → profiles-gPCjLOQq.js.map} +1 -1
  48. package/dist/assets/{registry-CVdSZW5l.js → registry-CJh9L_kF.js} +17 -32
  49. package/dist/assets/registry-CJh9L_kF.js.map +1 -0
  50. package/dist/assets/{registry-CLolR0kM.js → registry-S7SFF_JB.js} +1 -1
  51. package/dist/assets/{resolve-BnlEajYz.js → resolve-BJxBhU5j.js} +18 -3
  52. package/dist/assets/resolve-BJxBhU5j.js.map +1 -0
  53. package/dist/assets/{server-CJNEKxzE.js → server-C05N9q4G.js} +298 -100
  54. package/dist/assets/server-C05N9q4G.js.map +1 -0
  55. package/dist/assets/{src-yMUQvdyL.js → src-RQ6OJOUO.js} +322 -104
  56. package/dist/assets/src-RQ6OJOUO.js.map +1 -0
  57. package/dist/assets/{turnRefs-B2a8to9q.js → turnRefs-u-U75DFx.js} +2 -2
  58. package/dist/assets/{turnRefs-B2a8to9q.js.map → turnRefs-u-U75DFx.js.map} +1 -1
  59. package/dist/assets/{usdt-DesFeDiG.js → usdt-A1B3r3Mq.js} +3 -3
  60. package/dist/assets/{usdt-DesFeDiG.js.map → usdt-A1B3r3Mq.js.map} +1 -1
  61. package/dist/ghostly.mjs +1 -1
  62. package/package.json +1 -1
  63. package/dist/assets/api-aup3Amud.js.map +0 -1
  64. package/dist/assets/apiKit-DYkVEs6r.js.map +0 -1
  65. package/dist/assets/apps-Rbk2HHXb.js.map +0 -1
  66. package/dist/assets/buttons-CGRqZ7z0.js +0 -2
  67. package/dist/assets/ghostly_iroh_web-Dvzh3Y6E.js.map +0 -1
  68. package/dist/assets/host-C7hA-Za7.js.map +0 -1
  69. package/dist/assets/main-DC8cUkDx.js.map +0 -1
  70. package/dist/assets/registry-CVdSZW5l.js.map +0 -1
  71. package/dist/assets/resolve-BnlEajYz.js.map +0 -1
  72. package/dist/assets/server-CJNEKxzE.js.map +0 -1
  73. package/dist/assets/src-yMUQvdyL.js.map +0 -1
@@ -1,9 +1,9 @@
1
- import { $o as parseCallSignal, Jo as answersOffer, Ko as CALL_SIGNAL_MAX_AGE_MS, Qo as extractParamsFromSdp, Xo as callRtcConfig, Yo as buildSdpFromSignal, Zo as callSignalHeardAt, ai as checkPushEndpoint, el as setLinkTraceSink, es as signalHasVideo, ki as isPublicAddress, qo as RTC_CONFIG, yl as fromBase64Url } from "./src-yMUQvdyL.js";
2
- import { a as fedimintSdkOn, r as irohRelayProblem } from "./irohWeb-BJPk9bMn.js";
3
- import { r as asCliError, t as CliError } from "./errors-DO5z6h_l.js";
4
- import { a as findChat } from "./apiKit-DYkVEs6r.js";
5
- import { _ as flushCardUpdates, c as paymentJson, d as nodeLocalFetch, f as chatJson, g as resumeHolds, h as groupMessageJson, l as reportAppFrames, m as groupJson, p as chatMessageJson, r as callApi, u as servedApps } from "./api-aup3Amud.js";
6
- import { n as RESTORED_MARK, r as acquireLock } from "./profiles-BHc9pEYg.js";
1
+ import { $o as extractParamsFromSdp, Ai as isPublicAddress, Jo as RTC_CONFIG, Qo as callSignalHeardAt, Xo as buildSdpFromSignal, Yo as answersOffer, Zo as callRtcConfig, es as parseCallSignal, oi as checkPushEndpoint, qo as CALL_SIGNAL_MAX_AGE_MS, tl as setLinkTraceSink, ts as signalHasVideo, xl as fromBase64Url } from "./src-RQ6OJOUO.js";
2
+ import { a as fedimintSdkOn, r as irohRelayProblem } from "./irohWeb-DynqwRwl.js";
3
+ import { r as asCliError, t as CliError } from "./errors-B-zMxbcd.js";
4
+ import { a as findChat } from "./apiKit-3MLUyuhL.js";
5
+ import { _ as flushCardUpdates, c as paymentJson, d as nodeLocalFetch, f as chatJson, g as resumeHolds, h as groupMessageJson, l as reportAppFrames, m as groupJson, p as chatMessageJson, r as callApi, u as servedApps } from "./api-Bp_hWAiU.js";
6
+ import { n as RESTORED_MARK, r as acquireLock } from "./profiles-gPCjLOQq.js";
7
7
  import { r as privateFolder, t as ownFolder } from "./privateFolder-Bk9C9oFv.js";
8
8
  import { createRequire } from "node:module";
9
9
  import { appendFileSync, chmodSync, closeSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
@@ -418,11 +418,13 @@ var CallAudio = class {
418
418
  this.schedule();
419
419
  }
420
420
  stop() {
421
- this.stopped = true;
422
421
  if (this.timer) clearTimeout(this.timer);
423
422
  this.timer = null;
424
- this.options.encoder.delete?.();
425
- this.options.decoder.delete?.();
423
+ if (this.stopped) return;
424
+ this.stopped = true;
425
+ for (const codec of [this.options.encoder, this.options.decoder]) try {
426
+ codec.delete?.();
427
+ } catch {}
426
428
  }
427
429
  /** Sends the frame that is due now. Exposed for tests, which drive the clock themselves. */
428
430
  tick() {
@@ -551,6 +553,57 @@ var CallAudio = class {
551
553
  //#endregion
552
554
  //#region src/calls/media.ts
553
555
  var loading = null;
556
+ var OPUS_APPLICATION_VOIP = 2048;
557
+ var OPUS_SET_BITRATE = 4002;
558
+ /** The largest packet a codec reads or writes (opusscript's MAX_PACKET_SIZE). */
559
+ var MAX_PACKET_BYTES = 3828;
560
+ /**
561
+ * Room for a frame of PCM as the module reads and writes it, one byte of PCM in each 16-bit element: a concealment (an
562
+ * empty packet) writes 120 ms at 48 kHz, 5,760 samples, 23,040 bytes, at any rate.
563
+ */
564
+ var PCM_ROOM_BYTES = 23040;
565
+ /** The most PCM a decode hands back, as opusscript's: its largest frame, 60 ms at 48 kHz (CallAudio's `conceal`). */
566
+ var MAX_PCM_OUT = 5760;
567
+ /**
568
+ * An Opus encoder or decoder on opusscript's libopus, with buffers that hold what libopus writes. opusscript's own
569
+ * wrapper (0.1.1) keeps 5,760 bytes for its PCM and reaches it through views at twice the buffer's address, past it: a
570
+ * 20 ms frame at 48 kHz takes 3,840 bytes there and a concealment 23,040. With three calls or more, as calls ended and
571
+ * others started, one codec's audio landed on another's state: noise, then "memory access out of bounds" in every
572
+ * call until the daemon restarted. The bytes in and out are the same as opusscript's.
573
+ */
574
+ function opusCodec(native, rate) {
575
+ const handler = new native.OpusScriptHandler(rate, 1, OPUS_APPLICATION_VOIP);
576
+ const pcm = native._malloc(PCM_ROOM_BYTES), packet = native._malloc(MAX_PACKET_BYTES);
577
+ let deleted = false;
578
+ const codec = {
579
+ encode(frame, frameSize) {
580
+ if (deleted || frame.length * 2 > PCM_ROOM_BYTES) throw new Error("Encode error: Bad argument");
581
+ native.HEAPU16.set(frame, pcm / 2);
582
+ const length = handler._encode(pcm, frame.length, packet, frameSize);
583
+ if (length < 0) throw new Error(`Encode error: ${length}`);
584
+ return Buffer.from(native.HEAPU8.subarray(packet, packet + length));
585
+ },
586
+ decode(data) {
587
+ if (deleted || data.length > MAX_PACKET_BYTES) throw new Error("Decode error: Bad argument");
588
+ native.HEAPU8.set(data, packet);
589
+ const samples = handler._decode(packet, data.length, pcm);
590
+ if (samples < 0) throw new Error(`Decode error: ${samples}`);
591
+ return Buffer.from(native.HEAPU16.subarray(pcm / 2, pcm / 2 + Math.min(samples * 2, MAX_PCM_OUT)));
592
+ },
593
+ delete() {
594
+ if (deleted) return;
595
+ deleted = true;
596
+ native.OpusScriptHandler.destroy_handler(handler);
597
+ native._free(pcm);
598
+ native._free(packet);
599
+ }
600
+ };
601
+ if (handler._encoder_ctl(OPUS_SET_BITRATE, 32e3) < 0) {
602
+ codec.delete?.();
603
+ throw new Error("Encoder CTL error");
604
+ }
605
+ return codec;
606
+ }
554
607
  /** The media stack, or why calls are not available here (the reason the call buttons of the contact's app show). */
555
608
  function loadCallStack() {
556
609
  return loading ??= (async () => {
@@ -558,12 +611,8 @@ function loadCallStack() {
558
611
  try {
559
612
  const ndc = await import("node-datachannel");
560
613
  const lib = ndc.PeerConnection ? ndc : ndc.default;
561
- const OpusScript = (await import("opusscript")).default;
562
- const opus = (rate) => {
563
- const codec = new OpusScript(rate, 1, OpusScript.Application.VOIP);
564
- codec.setBitrate(32e3);
565
- return codec;
566
- };
614
+ const native = createRequire(import.meta.url)("opusscript/build/opusscript_native_wasm.js")();
615
+ const opus = (rate) => opusCodec(native, rate);
567
616
  opus(48e3).delete?.();
568
617
  return {
569
618
  ndc: lib,
@@ -612,7 +661,8 @@ function describePair(pair) {
612
661
  var refuseAnswers = Number(process.env.GHOSTLY_CALL_REFUSE_ANSWERS) || 0;
613
662
  /** How long a description waits for its candidates: a server reflexive one and a little after it, or this long. */
614
663
  var GATHER_MS = 5e3;
615
- var SETTLE_MS = 400;
664
+ /** The signal keeps the first server reflexive candidate only: the apps' calls and the chats' data link settle as long. */
665
+ var SETTLE_MS = 150;
616
666
  /**
617
667
  * With a candidate in (a host one comes in milliseconds), the server reflexive one is waited for this long at most,
618
668
  * not the whole of `GATHER_MS`: a STUN server that answers does in tens of milliseconds, and one that never does (UDP
@@ -629,8 +679,9 @@ var HOST_GATHER_MS = 2e3;
629
679
  async function openConnection(stack, options, offer, held = false) {
630
680
  const { ndc } = stack;
631
681
  const bind = process.env.GHOSTLY_CALL_BIND;
682
+ const iceServers = callIceServers(options.iceServers);
632
683
  const pc = new ndc.PeerConnection("ghostly-call", {
633
- iceServers: callIceServers(options.iceServers),
684
+ iceServers,
634
685
  ...bind ? { bindAddress: bind } : {}
635
686
  });
636
687
  const born = Date.now();
@@ -674,7 +725,7 @@ async function openConnection(stack, options, offer, held = false) {
674
725
  videoTrack = pc.addTrack(video);
675
726
  videoTrack.onMessage(() => {});
676
727
  }
677
- const gathered = gathering(pc);
728
+ const gathered = gathering(pc, { turn: callUsesTurn(iceServers) });
678
729
  if (offer) pc.setRemoteDescription(buildSdpFromSignal(held ? {
679
730
  ...offer,
680
731
  c: []
@@ -864,8 +915,18 @@ function mdnsNote(signal, log) {
864
915
  lookup(host, { all: true }).then((found) => log(`${host} resolves here to ${found.map((a) => a.address).join(", ")}`), () => log(`${host} does not resolve here: the call connects only if the contact's checks reach this side`));
865
916
  }
866
917
  }
867
- /** Resolves once the local description has its candidates (see GATHER_MS and HOST_GATHER_MS). */
868
- function gathering(pc) {
918
+ /** Whether a call's ICE servers (`callIceServers`) have a TURN server: its relay candidate is then the one waited for. */
919
+ function callUsesTurn(servers) {
920
+ return servers.some((server) => typeof server !== "string");
921
+ }
922
+ /**
923
+ * Resolves once the local description has its candidates (see GATHER_MS and HOST_GATHER_MS). `turn`: the connection
924
+ * has a TURN server, and its relay candidate is waited for rather than the reflexive one, as the apps' calls do
925
+ * (`waitForIceGathering`): an allocation takes two round trips, and a TURN server 100 ms away gave its relay after the
926
+ * description had gone without it, on the networks where only the relay connects.
927
+ */
928
+ function gathering(pc, { turn = false } = {}) {
929
+ const wanted = turn ? / typ relay/ : / typ srflx/;
869
930
  return new Promise((resolve) => {
870
931
  let settle = null;
871
932
  let any = false;
@@ -884,7 +945,7 @@ function gathering(pc) {
884
945
  });
885
946
  pc.onLocalCandidate((candidate) => {
886
947
  any = true;
887
- if (!settle && / typ srflx/.test(candidate)) settle = setTimeout(done, SETTLE_MS);
948
+ if (!settle && wanted.test(candidate)) settle = setTimeout(done, SETTLE_MS);
888
949
  });
889
950
  });
890
951
  }
@@ -1413,7 +1474,11 @@ var CallManager = class {
1413
1474
  this.calls.delete(call.id);
1414
1475
  if (call.direction === "out" && call.connectedAt === null && call.state === "ringing" && (reason === "hangup" || reason === "unanswered")) this.unansweredAt.set(call.chat, this.now);
1415
1476
  if (report) process.stderr.write(`ghostly: call ${call.id}: ended (${reason})${call.media ? `, ice ${call.media.ice.state}` : ""}\n`);
1416
- call.media?.close();
1477
+ try {
1478
+ call.media?.close();
1479
+ } catch (error) {
1480
+ process.stderr.write(`ghostly: call ${call.id}: closing its media failed: ${error instanceof Error ? error.message : String(error)}\n`);
1481
+ }
1417
1482
  this.stopEarly(call);
1418
1483
  call.early?.close();
1419
1484
  call.early = null;
@@ -1828,6 +1893,34 @@ var EventHub = class {
1828
1893
  this.journalLines = kept.length;
1829
1894
  this.journalBytes = bytes;
1830
1895
  }
1896
+ /**
1897
+ * What was removed here leaves the journal too: the journal is written again without the events that name it (a
1898
+ * line that names it and cannot be read goes as well), and a journal set aside as unreadable, which cannot be sorted,
1899
+ * is deleted. `ids`: what was removed (a message's id, a chat's or a group's); `gone`: whether an event is of it.
1900
+ */
1901
+ forget(ids, gone) {
1902
+ if (!ids.length) return;
1903
+ try {
1904
+ rmSync(this.journalPath + ".unreadable", { force: true });
1905
+ if (!existsSync(this.journalPath)) return;
1906
+ const lines = journalTail(this.journalPath).lines;
1907
+ const kept = lines.filter((line) => {
1908
+ if (!ids.some((id) => line.includes(JSON.stringify(id)))) return true;
1909
+ try {
1910
+ return !gone(JSON.parse(line));
1911
+ } catch {
1912
+ return false;
1913
+ }
1914
+ });
1915
+ if (kept.length === lines.length) return;
1916
+ writeFileSync(this.journalPath + ".tmp", kept.length ? kept.join("\n") + "\n" : "", { mode: 384 });
1917
+ renameSync(this.journalPath + ".tmp", this.journalPath);
1918
+ this.journalLines = kept.length;
1919
+ this.journalBytes = kept.reduce((bytes, line) => bytes + Buffer.byteLength(line) + 1, 0);
1920
+ } catch (error) {
1921
+ process.stderr.write(`ghostly: event journal error: ${error instanceof Error ? error.message : String(error)}\n`);
1922
+ }
1923
+ }
1831
1924
  handle(message) {
1832
1925
  switch (message.kind) {
1833
1926
  case "state":
@@ -1903,11 +1996,12 @@ var EventHub = class {
1903
1996
  ...typingFields(shape)
1904
1997
  });
1905
1998
  }
1906
- for (const id of [...this.chats.keys()]) {
1907
- if (links.has(id)) continue;
1999
+ const removedChats = [...this.chats.keys()].filter((id) => !links.has(id));
2000
+ for (const id of removedChats) {
1908
2001
  this.chats.delete(id);
1909
2002
  if (!quiet) this.emit("chat.removed", `chat.removed:${id}`, { chat: id });
1910
2003
  }
2004
+ this.forget(removedChats, (event) => removedChats.includes(event.chat) && event.type !== "chat.removed");
1911
2005
  const groups = new Map(state.groups.map((group) => [group.id, group]));
1912
2006
  for (const [id, group] of groups) {
1913
2007
  const shape = groupShape(group), before = this.groups.get(id);
@@ -1946,11 +2040,12 @@ var EventHub = class {
1946
2040
  member
1947
2041
  });
1948
2042
  }
1949
- for (const id of [...this.groups.keys()]) {
1950
- if (groups.has(id)) continue;
2043
+ const removedGroups = [...this.groups.keys()].filter((id) => !groups.has(id));
2044
+ for (const id of removedGroups) {
1951
2045
  this.groups.delete(id);
1952
2046
  if (!quiet) this.emit("group.removed", `group.removed:${id}`, { group: id });
1953
2047
  }
2048
+ this.forget(removedGroups, (event) => removedGroups.includes(event.group) && event.type !== "group.removed");
1954
2049
  for (const [id, payment] of Object.entries(state.payments ?? {})) {
1955
2050
  const shape = `${payment.state}|${payment.error ?? ""}|${payment.closed ? 1 : 0}`, before = this.payments.get(id);
1956
2051
  this.payments.set(id, shape);
@@ -2205,10 +2300,12 @@ var EventHub = class {
2205
2300
  });
2206
2301
  }
2207
2302
  }
2303
+ const gone = [];
2208
2304
  for (const id of deleted ?? [...known.keys()]) {
2209
2305
  if (present.has(id) || !known.has(id)) continue;
2210
2306
  known.delete(id);
2211
2307
  changes.push([id, null]);
2308
+ gone.push(id);
2212
2309
  if (!quiet) this.emit(group ? "group.deleted" : "message.deleted", `${group ? "group" : "message"}.deleted:${group ?? chat}:${id}`, group ? {
2213
2310
  group,
2214
2311
  messageId: id
@@ -2218,6 +2315,8 @@ var EventHub = class {
2218
2315
  });
2219
2316
  }
2220
2317
  if (changes.length) this.persist(chat, changes);
2318
+ const whole = !!group && !deleted && messages.length === 0 && gone.length > 0;
2319
+ this.forget(whole ? [group] : gone, (event) => (group ? event.group === group : event.chat === chat) && !event.type.endsWith(".deleted") && (whole ? "message" in event || "messageId" in event || "replyId" in event : gone.includes(event.messageId) || gone.includes(event.message?.id) || gone.includes(event.replyId)));
2221
2320
  for (const [fileId, messageId] of files) this.flushFile(fileId, messageId);
2222
2321
  }
2223
2322
  /**
@@ -2411,6 +2510,45 @@ function journalTail(path) {
2411
2510
  }
2412
2511
  }
2413
2512
  //#endregion
2513
+ //#region src/joinLog.ts
2514
+ /**
2515
+ * A join through a group's link, as a daemon says it in its log (stderr): the steps of the knock and of the entry
2516
+ * session, one short line each, from the engine's join trace (packages/browser/src/engine/joinTrace.ts). Without them a
2517
+ * join that stalled left nothing in the admin's log to tell a knock never seen from a session that never opened
2518
+ * (2026-10-16). The reads of the knocks, every few seconds, and an edge's own steps are not said.
2519
+ */
2520
+ var SAID = {
2521
+ "join.start": "joining through its link",
2522
+ "knock.published": "knock left",
2523
+ "knock.seen": "a knock seen: opening an entry session",
2524
+ "link.start": "entry session starting",
2525
+ "link.presence": "entry session: the other side's packet seen",
2526
+ "link.offering": "entry session: offering a connection",
2527
+ "link.answering": "entry session: answering a connection",
2528
+ "link.connecting": "entry session: connecting",
2529
+ "link.open": "entry session open",
2530
+ "link.idle": "entry session not open",
2531
+ "link.native": "entry session: WebRTC connected nothing, going on over a native transport",
2532
+ "invite.sent": "invitation sent on the entry session",
2533
+ "invite.received": "invitation received on the entry session: accepting",
2534
+ "welcome.sent": "welcome sent",
2535
+ "welcome.received": "welcome received: in the group"
2536
+ };
2537
+ /** The log line for one join-trace line, or null when it is not said. */
2538
+ function joinLogLine(traced) {
2539
+ let step;
2540
+ try {
2541
+ step = JSON.parse(traced);
2542
+ } catch {
2543
+ return null;
2544
+ }
2545
+ if (typeof step.step !== "string" || typeof step.g !== "string" || !Object.hasOwn(SAID, step.step)) return null;
2546
+ const role = step.role === "host" || step.role === "guest" ? step.role : void 0;
2547
+ if (step.step.startsWith("link.") && !role) return null;
2548
+ const at = typeof step.t === "number" ? ` at ${new Date(step.t).toISOString()}` : "";
2549
+ return `ghostly: group ${step.g}: ${SAID[step.step]}${role ? ` (${role === "host" ? "letting in" : "joining"})` : ""}${at}`;
2550
+ }
2551
+ //#endregion
2414
2552
  //#region src/runtime/fedimint.ts
2415
2553
  /**
2416
2554
  * The Fedimint client on Node (WISP 1100 § Wallet SDKs on Node): the app's own SDK and WebAssembly, each database in
@@ -4412,10 +4550,12 @@ async function startRuntime(paths, options = {}) {
4412
4550
  const pinned = pkarrRelays();
4413
4551
  const pinnedIroh = irohRelays();
4414
4552
  const trace = process.env.GHOSTLY_LINK_TRACE;
4415
- if (trace) {
4416
- setLinkTraceSink((line) => appendFileSync(trace, line + "\n", { mode: 384 }));
4417
- (await import("./joinTrace-D-fnt2p3.js")).setJoinTraceSink((line) => appendFileSync(trace, line + "\n", { mode: 384 }));
4418
- }
4553
+ if (trace) setLinkTraceSink((line) => appendFileSync(trace, line + "\n", { mode: 384 }));
4554
+ if (trace || options.logJoins) (await import("./joinTrace-D-fnt2p3.js")).setJoinTraceSink((line) => {
4555
+ if (trace) appendFileSync(trace, line + "\n", { mode: 384 });
4556
+ const said = options.logJoins ? joinLogLine(line) : null;
4557
+ if (said) process.stderr.write(said + "\n");
4558
+ });
4419
4559
  const store = await openPersistentIndexedDb(paths.db);
4420
4560
  const restored = join(paths.dir, RESTORED_MARK);
4421
4561
  if (existsSync(restored)) {
@@ -4438,17 +4578,17 @@ async function startRuntime(paths, options = {}) {
4438
4578
  if (testMints) (await import("./mints-CqTdA4Gk.js")).declareTestMints(testMints.split(","));
4439
4579
  await installEventSource();
4440
4580
  await preloadWalletModules();
4441
- (await import("./ghostly_iroh_web-Dvzh3Y6E.js")).initSync({ module: irohWasmBytes() });
4581
+ (await import("./ghostly_iroh_web-BsAEFIjl.js")).initSync({ module: irohWasmBytes() });
4442
4582
  const [{ registerFileBytes }, { NodeFileBytes }] = await Promise.all([import("./fileBytes-CgbibpbV.js"), import("./fileBytes-CfSXjTFa.js")]);
4443
4583
  registerFileBytes("native", async () => new NodeFileBytes(paths.files), true);
4444
- const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./server-CJNEKxzE.js"), import("./endpoint-BOTFMpfS.js")]);
4584
+ const [{ EngineServer }, { createHyperEndpoint }] = await Promise.all([import("./server-C05N9q4G.js"), import("./endpoint-BOTFMpfS.js")]);
4445
4585
  const network = hyperdhtNetwork();
4446
4586
  const dht = mainlineNetwork();
4447
- const mainline = dht.off ? null : new (await (import("./mainline-CxbxpoU4.js"))).Mainline({
4587
+ const mainline = dht.off ? null : new (await (import("./mainline-COqCHn1R.js"))).Mainline({
4448
4588
  bootstrap: dht.bootstrap,
4449
4589
  host: dht.host
4450
4590
  });
4451
- const transport = mainline || pinned ? new (await (import("./mainline-CxbxpoU4.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
4591
+ const transport = mainline || pinned ? new (await (import("./mainline-COqCHn1R.js"))).RelaysAndDht(mainline, pinned ? { relays: pinned } : {}, !!pinned) : null;
4452
4592
  const apps = servedApps(paths.dir).length > 0;
4453
4593
  const server = new EngineServer({
4454
4594
  ...transport ? { transport } : {},
@@ -4496,6 +4636,13 @@ async function startRuntime(paths, options = {}) {
4496
4636
  /** The longest request line the daemon reads (WISP 1100 § Framing). */
4497
4637
  var MAX_LINE = 16777216;
4498
4638
  /**
4639
+ * The most a connection may leave unread (answers and events the daemon wrote that its client has not taken): past
4640
+ * it, the connection ends. A `listen` stopped with Ctrl+Z, or a client that stops reading, kept every later event in
4641
+ * the daemon's memory until it read again (155 MB after 6,000 long texts); `listen` reconnects after the last seq it
4642
+ * took, and the journal still has the rest. A replay is the journal's size at most and does not count against it.
4643
+ */
4644
+ var MAX_UNREAD = 8388608;
4645
+ /**
4499
4646
  * How many of a chat's newest messages the start reads to see whether the events are in step with its history: the
4500
4647
  * whole history of every chat and group, read at every start, took a daemon's start and its memory with every message
4501
4648
  * the profile ever stored (1 s and 70 MB at 100,000).
@@ -4510,7 +4657,10 @@ async function openHost(paths, mode, version, options = {}) {
4510
4657
  const release = acquireLock(paths);
4511
4658
  let runtime;
4512
4659
  try {
4513
- runtime = await startRuntime(paths, options);
4660
+ runtime = await startRuntime(paths, mode === "daemon" ? {
4661
+ ...options,
4662
+ logJoins: true
4663
+ } : options);
4514
4664
  } catch (error) {
4515
4665
  release();
4516
4666
  throw error;
@@ -4654,12 +4804,23 @@ function ownerOnlyUmask() {
4654
4804
  }
4655
4805
  function connection(ctx, socket) {
4656
4806
  const unsubscribes = [];
4657
- const write = (value) => {
4658
- if (!socket.destroyed) socket.write(JSON.stringify(value) + "\n");
4807
+ let limit = MAX_UNREAD;
4808
+ const unsubscribe = () => {
4809
+ for (const off of unsubscribes.splice(0)) off();
4810
+ };
4811
+ const write = (value, replayed = false) => {
4812
+ if (socket.destroyed || socket.writableEnded) return;
4813
+ if (!replayed && socket.writableLength > limit) {
4814
+ unsubscribe();
4815
+ socket.end();
4816
+ return;
4817
+ }
4818
+ socket.write(JSON.stringify(value) + "\n");
4659
4819
  };
4660
- socket.on("close", () => {
4661
- for (const off of unsubscribes) off();
4820
+ socket.on("drain", () => {
4821
+ limit = MAX_UNREAD;
4662
4822
  });
4823
+ socket.on("close", unsubscribe);
4663
4824
  socket.on("error", () => {});
4664
4825
  const lines = createInterface({
4665
4826
  input: socket,
@@ -4692,6 +4853,13 @@ function connection(ctx, socket) {
4692
4853
  return;
4693
4854
  }
4694
4855
  if (request.method === "events.subscribe") {
4856
+ if (unsubscribes.length) {
4857
+ write({
4858
+ id,
4859
+ error: new CliError("bad_request", "This connection is subscribed already: open another one for a new stream").toJSON()
4860
+ });
4861
+ return;
4862
+ }
4695
4863
  const params = request.params ?? {};
4696
4864
  const since = typeof params.since === "number" ? params.since : ctx.hub.lastSeq;
4697
4865
  let replay;
@@ -4719,9 +4887,10 @@ function connection(ctx, socket) {
4719
4887
  });
4720
4888
  let last = since;
4721
4889
  for (const event of replay) {
4722
- write({ event });
4890
+ write({ event }, true);
4723
4891
  last = event.seq;
4724
4892
  }
4893
+ limit = Math.max(limit, socket.writableLength + MAX_UNREAD);
4725
4894
  replaying = false;
4726
4895
  for (const event of queued) if (event.seq > last) write({ event });
4727
4896
  return;
@@ -4736,6 +4905,6 @@ function connection(ctx, socket) {
4736
4905
  });
4737
4906
  }
4738
4907
  //#endregion
4739
- export { BASELINE_NEWEST, MAX_LINE, keepServing, openHost, serve };
4908
+ export { BASELINE_NEWEST, MAX_LINE, MAX_UNREAD, keepServing, openHost, serve };
4740
4909
 
4741
- //# sourceMappingURL=host-C7hA-Za7.js.map
4910
+ //# sourceMappingURL=host-C74Sy5jT.js.map