@ghostlytools/cli 1.2.3 → 1.2.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.
@@ -584,6 +584,7 @@ function withRequestOptions(transport, extra) {
584
584
  if (transport.subscribe) wrapped.subscribe = (listener) => transport.subscribe(listener);
585
585
  if (transport.networkChanged) wrapped.networkChanged = () => transport.networkChanged();
586
586
  if (transport.readAnsweredAt) wrapped.readAnsweredAt = (pubKeyZ32) => transport.readAnsweredAt(pubKeyZ32);
587
+ if (transport.readHeldBackFor) wrapped.readHeldBackFor = (pubKeyZ32) => transport.readHeldBackFor(pubKeyZ32);
587
588
  if (transport.onServerTime) wrapped.onServerTime = (listener) => transport.onServerTime(listener);
588
589
  if (transport.configure) wrapped.configure = (options) => transport.configure(options);
589
590
  if (transport.turnRead) wrapped.turnRead = (pubKeyZ32, options) => transport.turnRead(pubKeyZ32, options);
@@ -798,6 +799,11 @@ var isGroupSignal = (who) => who.group && !who.background && (who.signal || isWa
798
799
  var watchLane = (who) => `${+who.urgent}${+who.background}`;
799
800
  /** A group's read that is not a background one: an edge's, a private group's knock poll (`GROUP_RATION_WINDOW_MS`). */
800
801
  var isGroupRead = (who) => who.group && !who.write && !who.background;
802
+ /**
803
+ * A read that takes its turn per key with the others (`READ_TURN_MS`): a background one, and a 1:1 chat's at any pace.
804
+ * Not a community door's bell.
805
+ */
806
+ var isTurnRead = (who) => !who.write && !who.door && (who.background || !who.group);
801
807
  /** A write that goes first once the budget frees a request (see `WRITE_FIRST_MS`): a chat's or a group's, never a background one. */
802
808
  var firstWriter = (who) => !who.write || who.background ? null : who.group ? "group" : "chat";
803
809
  /**
@@ -871,6 +877,29 @@ var WATCH_SHARE = 1 / 3;
871
877
  * waits, the others wait too (`WATCH_SHARE`, `GROUP_RATION_WINDOW_MS`). One that stops asking loses it.
872
878
  */
873
879
  var WATCH_TURN_MS = 5e3;
880
+ /**
881
+ * Background reads, and a 1:1 chat's at any pace, take turns per key the same way: while a key read longer ago asked
882
+ * within this long and was not read since, a read of another key waits (a chat's, at a pace that is not the background
883
+ * one, only for another chat's key). A saved chat that is not live reads its contact every 30 s in the
884
+ * background, and one the budget refused read again only at its next poll. A request frees 60 s after the one that took
885
+ * it, which is the poll after next of the chat that took it: the same chats took every request that freed. A daemon with
886
+ * 30 chats and 5 groups, back after a restart while its groups signaled, read some contacts once in 10 min, and those
887
+ * chats stayed down although both sides were online and offering (CLI bug hunt, 2026-10-12). A link's background read
888
+ * the budget refused reads again when it frees a request (`LinkSession`), so the key whose turn it is is there to take it.
889
+ * A chat not live after a restart looks fast while its offer is out, and those reads took no turn: the budget answered
890
+ * them from the packet read before, every 2 to 8 s for 10 minutes, while the chats that were live already, the groups'
891
+ * edges and the writes took every request that freed. A quarter of a daemon's 30 chats read their contact once in 10
892
+ * minutes, their contacts online and offering, and showed them offline at 6 minutes (CLI bug hunt, 2026-10-13).
893
+ */
894
+ var READ_TURN_MS = 5e3;
895
+ /**
896
+ * A 1:1 chat's read the budget held back this long without a break is starved: it goes before the writes that wait
897
+ * (`WRITE_FIRST_MS`) and past the background share, within the minute, the key held back longest first. A daemon back
898
+ * after a restart spends its whole minute on writes (each chat's and edge's offer, its packets after the attempt), which
899
+ * go first, and requests free in bursts as the minute's first requests age out: every burst went to the writes that
900
+ * waited, and the reads of the chats that were not in an attempt never had their turn (CLI restart lab, 2026-10-13).
901
+ */
902
+ var READ_STARVED_MS = 6e4;
874
903
  /** How long a read that let an older one go first waits before it asks again. */
875
904
  var TURN_WAIT_MS = 1e3;
876
905
  /**
@@ -990,6 +1019,8 @@ var RelayTransport = class {
990
1019
  * it is under the watch share (`WATCH_SHARE`) and while the groups' reads are rationed (`GROUP_RATION_WINDOW_MS`).
991
1020
  */
992
1021
  readTurns = /* @__PURE__ */ new Map();
1022
+ /** Per key read in turn (`isTurnRead`), when a read of it last went out and when it last asked (`READ_TURN_MS`). */
1023
+ keyTurns = /* @__PURE__ */ new Map();
993
1024
  /** Groups' reads (not background ones) on each relay in the last `GROUP_RATION_WINDOW_MS`. */
994
1025
  spentGroupReads = /* @__PURE__ */ new Map();
995
1026
  /** When a group's read last found each relay's minute full: its reads there are rationed for `GROUP_RATION_MS`. */
@@ -1009,6 +1040,11 @@ var RelayTransport = class {
1009
1040
  /** Per relay and key, the newest packet that relay held back for its budget while another relay took it (`catchUpLater`). */
1010
1041
  catchUp = /* @__PURE__ */ new Map();
1011
1042
  catchUpTimer = null;
1043
+ /**
1044
+ * Per key whose reads the budget held back since its last read went out: when a request frees for it
1045
+ * (`readHeldBackFor`), how many times in a row, and since when (`READ_STARVED_MS`).
1046
+ */
1047
+ heldBack = /* @__PURE__ */ new Map();
1012
1048
  /** When the relays last answered a read of each key, for `FRESH_READ_MS`. */
1013
1049
  readAt = /* @__PURE__ */ new Map();
1014
1050
  freshReadMs;
@@ -1398,8 +1434,14 @@ var RelayTransport = class {
1398
1434
  }
1399
1435
  const resting = this.relays.every((r) => this.isCoolingDown(r, "GET") || this.breaker.blockedFor(r) > 0 || this.heldFor(r, who, Date.now(), pubKeyZ32) > 0);
1400
1436
  const known = down ? this.newest.get(pubKeyZ32) : newerPacket(this.newest.get(pubKeyZ32), this.written.get(pubKeyZ32));
1401
- if (resting && known) return known;
1402
- if (!down && budgetWait < Infinity) throw new DiscoveryBudgetError(budgetWait);
1437
+ if (resting && known) {
1438
+ if (!down && budgetWait < Infinity) this.heldBackUntil(pubKeyZ32, Date.now() + budgetWait);
1439
+ return known;
1440
+ }
1441
+ if (!down && budgetWait < Infinity) {
1442
+ this.heldBackUntil(pubKeyZ32, Date.now() + budgetWait);
1443
+ throw new DiscoveryBudgetError(budgetWait);
1444
+ }
1403
1445
  throw new Error("No Pkarr relay reachable");
1404
1446
  }
1405
1447
  /** The order a key's read asks the relays in: in turn per key (see `resolve`), relays found slow lately last. */
@@ -1466,9 +1508,25 @@ var RelayTransport = class {
1466
1508
  readAnsweredAt(pubKeyZ32) {
1467
1509
  return this.readAt.get(pubKeyZ32);
1468
1510
  }
1511
+ readHeldBackFor(pubKeyZ32) {
1512
+ const held = this.heldBack.get(pubKeyZ32);
1513
+ const left = held === void 0 ? 0 : held.until - Date.now();
1514
+ return left > 0 ? left : void 0;
1515
+ }
1516
+ heldBackUntil(pubKeyZ32, until) {
1517
+ const held = this.heldBack.get(pubKeyZ32), times = (held?.times ?? 0) + 1;
1518
+ this.heldBack.delete(pubKeyZ32);
1519
+ this.heldBack.set(pubKeyZ32, {
1520
+ until,
1521
+ times,
1522
+ since: held?.since ?? Date.now()
1523
+ });
1524
+ if (this.heldBack.size > READ_TURNS_KEPT) this.heldBack.delete(this.heldBack.keys().next().value);
1525
+ }
1469
1526
  /** The relays answered a read of this key now; answers older than `FRESH_READ_MS` are dropped as the list grows. */
1470
1527
  answeredRead(pubKeyZ32) {
1471
1528
  const now = Date.now();
1529
+ this.heldBack.delete(pubKeyZ32);
1472
1530
  this.readAt.delete(pubKeyZ32);
1473
1531
  this.readAt.set(pubKeyZ32, now);
1474
1532
  if (this.readAt.size > 64) for (const [key, at] of this.readAt) {
@@ -1584,6 +1642,7 @@ var RelayTransport = class {
1584
1642
  if (isChat(who)) this.chatNeed(key).urgentAt = now;
1585
1643
  }
1586
1644
  if (isGroupRead(who)) this.readAsked(key, who, now);
1645
+ if (isTurnRead(who)) this.turnAsked(key, who, now);
1587
1646
  if (this.heldFor(relay, who, now, key) > 0) {
1588
1647
  if (isGroupRead(who) && (this.spent.get(relay)?.length ?? 0) >= this.limitOf(relay)) this.groupsFullAt.set(relay, now);
1589
1648
  if (isChat(who)) this.chatNeed(key).refused.set(relay, now);
@@ -1607,6 +1666,12 @@ var RelayTransport = class {
1607
1666
  lane: watchLane(who)
1608
1667
  });
1609
1668
  }
1669
+ if (isTurnRead(who)) this.keyTurns.set(key, {
1670
+ servedAt: now,
1671
+ askedAt: now,
1672
+ waiting: false,
1673
+ who
1674
+ });
1610
1675
  return true;
1611
1676
  }
1612
1677
  /**
@@ -1618,7 +1683,7 @@ var RelayTransport = class {
1618
1683
  * signaling goes over the limit by its allowance (`SIGNALING_ALLOWANCE_SHARE`), and so do a few of its reads of a
1619
1684
  * contact that just went away (`DEPARTED_READS`).
1620
1685
  */
1621
- heldFor(relay, who, now = Date.now(), key) {
1686
+ heldFor(relay, who, now = Date.now(), key, turns = true) {
1622
1687
  const recent = (this.spent.get(relay) ?? []).filter((at) => now - at < 6e4);
1623
1688
  const recentBackground = (this.spentBackground.get(relay) ?? []).filter((at) => now - at < 6e4);
1624
1689
  this.spent.set(relay, recent);
@@ -1628,10 +1693,11 @@ var RelayTransport = class {
1628
1693
  const departed = isDepartedRead(who) && key !== void 0 && recent.length >= limit && this.departedReads(key, now) < 6;
1629
1694
  const share = isChatSignal(who) || departed ? SIGNALING_ALLOWANCE_SHARE : isGroupSignal(who) ? GROUP_SIGNALING_ALLOWANCE_SHARE : 0;
1630
1695
  let wait = over(recent, limit + (share ? Math.max(0, Math.floor(limit * share) - (who.write ? 0 : 1)) : 0));
1696
+ const starved = key !== void 0 && this.starved(key, who, now);
1631
1697
  const groupOver = (this.spentGroupOver.get(relay) ?? []).filter((at) => now - at < GROUP_OVER_PACE_MS);
1632
1698
  this.spentGroupOver.set(relay, groupOver);
1633
1699
  if (isGroupSignal(who) && !who.write && recent.length >= limit && groupOver.length > 0) wait = Math.max(wait, groupOver[groupOver.length - 1] + GROUP_OVER_PACE_MS - now);
1634
- if (who.background) {
1700
+ if (who.background && !starved) {
1635
1701
  wait = Math.max(wait, over(recentBackground, this.backgroundPerMinute));
1636
1702
  const signaling = this.urgentAt + SIGNALING_WINDOW_MS - now;
1637
1703
  if (signaling > 0 && !who.door) wait = Math.max(wait, Math.min(signaling, over(recentBackground, this.backgroundWhileSignaling)));
@@ -1666,8 +1732,9 @@ var RelayTransport = class {
1666
1732
  const at = this.writeWaiting.get(`${lane} ${relay}`);
1667
1733
  return at !== void 0 && now - at < 5e3 ? at + WRITE_FIRST_MS - now : 0;
1668
1734
  };
1669
- if (writer !== "chat" && !departed) wait = Math.max(wait, waiting("chat"));
1670
- if (writer === null && who.group && !pastLimit || who.background) wait = Math.max(wait, waiting("group"));
1735
+ if (writer !== "chat" && !departed && !starved) wait = Math.max(wait, waiting("chat"));
1736
+ if ((writer === null && who.group && !pastLimit || who.background) && !starved) wait = Math.max(wait, waiting("group"));
1737
+ if (turns && wait <= 0 && isTurnRead(who) && this.turnAhead(relay, key, who, now)) wait = TURN_WAIT_MS;
1671
1738
  return Math.max(wait, 0);
1672
1739
  }
1673
1740
  /** A group's read of `key` asks: it waits for its turn from now on, unless it is taken (`readsAhead`). */
@@ -1697,6 +1764,39 @@ var RelayTransport = class {
1697
1764
  }
1698
1765
  return ahead;
1699
1766
  }
1767
+ /** A read of `key` that takes turns asks: it waits for its turn from now on, unless it is taken (`turnAhead`). */
1768
+ turnAsked(key, who, now) {
1769
+ for (const [other, turn] of this.keyTurns) if (now - Math.max(turn.askedAt, turn.servedAt) >= 6e4) this.keyTurns.delete(other);
1770
+ this.keyTurns.set(key, {
1771
+ servedAt: this.keyTurns.get(key)?.servedAt ?? -Infinity,
1772
+ askedAt: now,
1773
+ waiting: true,
1774
+ who
1775
+ });
1776
+ }
1777
+ /**
1778
+ * Whether a key read in turn longer ago than `key`, still asking, would take this relay's next request: one read in the
1779
+ * background for a background read, and a chat's that is starved (`READ_STARVED_MS`) for any. One asks again after
1780
+ * the budget held it back (a link's poll does, a one-off look does not), and keeps asking within `READ_TURN_MS` of its
1781
+ * last ask, or of when it was told the budget frees a request for it (`readHeldBackFor`).
1782
+ * One the budget holds back on its own account (a group's read, for the chats' reserve) keeps no turn: a group key
1783
+ * waiting for the chats held every chat's read back (CLI lab, 2026-10-13).
1784
+ */
1785
+ turnAhead(relay, key, who, now) {
1786
+ const mine = key === void 0 ? -Infinity : this.keyTurns.get(key)?.servedAt ?? -Infinity;
1787
+ for (const [other, turn] of this.keyTurns) {
1788
+ const held = this.heldBack.get(other);
1789
+ if (other === key || !turn.waiting || turn.servedAt >= mine || !held || held.times < 2) continue;
1790
+ if (!(who.background && turn.who.background) && !this.starved(other, turn.who, now)) continue;
1791
+ if (now - Math.max(turn.askedAt, held.until) < READ_TURN_MS && this.heldFor(relay, turn.who, now, other, false) === 0) return true;
1792
+ }
1793
+ return false;
1794
+ }
1795
+ /** A 1:1 chat's read of `key` the budget has held back for `READ_STARVED_MS` without a break. */
1796
+ starved(key, who, now) {
1797
+ const since = this.heldBack.get(key)?.since;
1798
+ return !who.group && !who.write && !who.door && since !== void 0 && now - since >= READ_STARVED_MS;
1799
+ }
1700
1800
  /** This contact's departed reads past the limit in the last minute (`DEPARTED_READS`). */
1701
1801
  departedReads(key, now) {
1702
1802
  const reads = (this.departedOver.get(key) ?? []).filter((at) => now - at < 6e4);
@@ -6096,13 +6196,15 @@ var LinkSession = class {
6096
6196
  nextInMs: 0
6097
6197
  });
6098
6198
  const started = Date.now();
6199
+ let heldBackMs;
6099
6200
  try {
6100
6201
  const pace = this.pace();
6101
6202
  const signal = pace === "fast" && this.fastStepsAfter > 0 && this.rtcSignalOut !== null;
6102
6203
  const watch = !signal && this.watching();
6103
6204
  const departed = !signal && Date.now() < this.departedUntil;
6205
+ const background = pace === "background" || pace === "connected";
6104
6206
  const packet = await this.transport.resolve(this.peerPubKeyZ32, {
6105
- background: pace === "background" || pace === "connected",
6207
+ background,
6106
6208
  urgent: pace === "fast",
6107
6209
  ...signal && { signal },
6108
6210
  ...watch && { watch },
@@ -6117,6 +6219,7 @@ var LinkSession = class {
6117
6219
  const read = answered !== void 0 && answered >= started;
6118
6220
  if (read) this.lastReadAt = started;
6119
6221
  this.readMissed = !!this.transport.readAnsweredAt && !read;
6222
+ if (this.readMissed) heldBackMs = this.transport.readHeldBackFor?.(this.peerPubKeyZ32);
6120
6223
  let receivedNew = false;
6121
6224
  const batch = packet ? parseLinkRecords(packet, this.encKey) : null;
6122
6225
  if (batch && isEmptyLinkPacket(batch)) {
@@ -6205,7 +6308,8 @@ var LinkSession = class {
6205
6308
  ms: Date.now() - started,
6206
6309
  error: String(error)
6207
6310
  });
6208
- if (!isDiscoveryBudgetError(error)) {
6311
+ if (isDiscoveryBudgetError(error)) heldBackMs = error.retryInMs;
6312
+ else {
6209
6313
  this.discoveryResult("read", error);
6210
6314
  this.events.onStatus?.("error");
6211
6315
  }
@@ -6223,7 +6327,7 @@ var LinkSession = class {
6223
6327
  this.poll();
6224
6328
  } else if (this.running && !this.pollTimer) {
6225
6329
  const interval = this.nextInterval();
6226
- const wait = Math.max(MIN_POLL_GAP_MS, interval - (Date.now() - started));
6330
+ const wait = heldBackMs !== void 0 ? Math.min(interval, Math.max(BUDGET_RETRY_MIN_MS, heldBackMs)) : Math.max(MIN_POLL_GAP_MS, interval - (Date.now() - started));
6227
6331
  this.events.onPoll?.({
6228
6332
  polling: false,
6229
6333
  nextInMs: wait
@@ -9177,6 +9281,13 @@ var DHT_EXPIRY_SKEW_MS = PKARR_FUTURE_SKEW_MS;
9177
9281
  */
9178
9282
  var DHT_ISSUED_BACK_MS = 12e4;
9179
9283
  var MAX_ATTEMPTS = 8;
9284
+ /**
9285
+ * Envelope sequences are set aside this many at a time (`DhtDeliveryState.sequence` is the highest set aside): a
9286
+ * keep-alive, every four minutes per chat, and one the relays' budget turns away, every 30 s on a profile idling at
9287
+ * it, then cost no disk write of their own. A reader only needs the sequence to grow, so those a restart skips cost
9288
+ * nothing; one the budget turned away is not used again, as it may have gone out on the DHT (the CLI puts there too).
9289
+ */
9290
+ var SEQUENCE_BLOCK = 64;
9180
9291
  /** The wait before publication attempt `attempts + 1` of a text, or of a receipt the contact still asks for. */
9181
9292
  var backoff = (attempts) => Math.min(6e4, 4e3 * 2 ** attempts);
9182
9293
  /** The contact's mailbox is read this often while either side is DHT-only, and otherwise… */
@@ -9230,6 +9341,10 @@ var emptyDhtDeliveryState = () => ({
9230
9341
  var DhtDelivery = class DhtDelivery {
9231
9342
  options;
9232
9343
  state;
9344
+ /** The state as last saved: a save that would write the same row again is skipped. */
9345
+ saved;
9346
+ /** The sequence of the last envelope signed here; the state's is the highest set aside (`SEQUENCE_BLOCK`). */
9347
+ sequence;
9233
9348
  mode;
9234
9349
  running = false;
9235
9350
  ticking = false;
@@ -9284,6 +9399,8 @@ var DhtDelivery = class DhtDelivery {
9284
9399
  const { peerRejected: _peerRejected, ...state } = structuredClone(options.state ?? emptyDhtDeliveryState());
9285
9400
  this.state = state;
9286
9401
  this.mode = options.mode;
9402
+ this.sequence = state.sequence;
9403
+ if (options.state) this.saved = JSON.stringify(state);
9287
9404
  this.unsealedOut = state.sequence > 0 && !options.credentials.peerKey;
9288
9405
  this.from = identityFromSeedB64(options.params.seedB64).pubKeyZ32;
9289
9406
  this.to = options.params.peerPubKeyZ32;
@@ -9372,7 +9489,11 @@ var DhtDelivery = class DhtDelivery {
9372
9489
  return operation;
9373
9490
  }
9374
9491
  async persist(next) {
9375
- await this.options.save(structuredClone(next));
9492
+ const json = JSON.stringify(next);
9493
+ if (json !== this.saved) {
9494
+ await this.options.save(structuredClone(next));
9495
+ this.saved = json;
9496
+ }
9376
9497
  this.state = next;
9377
9498
  }
9378
9499
  sealedKey(peer) {
@@ -9547,7 +9668,7 @@ var DhtDelivery = class DhtDelivery {
9547
9668
  }
9548
9669
  packetError(message, expires, reply, edit) {
9549
9670
  try {
9550
- this.records(this.body(this.state.sequence + 1, Date.now(), expires, message, this.state.receipt?.id ?? "abcdefghijklmnopqrstuv", reply, [], edit), this.options.credentials.peerKey ?? this.participation.pubKeyZ32);
9671
+ this.records(this.body(this.sequence + 1, Date.now(), expires, message, this.state.receipt?.id ?? "abcdefghijklmnopqrstuv", reply, [], edit), this.options.credentials.peerKey ?? this.participation.pubKeyZ32);
9551
9672
  return null;
9552
9673
  } catch (error) {
9553
9674
  return error instanceof Error ? error.message : String(error);
@@ -9555,7 +9676,7 @@ var DhtDelivery = class DhtDelivery {
9555
9676
  }
9556
9677
  /** Whether a text with its hop count fits the packet and what a reader reads (`MAX_ENVELOPE_PLAINTEXT`). */
9557
9678
  roomForHops(message, expires, reply, forwarded) {
9558
- const body = this.body(this.state.sequence + 1, Date.now(), expires, message, this.state.receipt?.id ?? "abcdefghijklmnopqrstuv", reply, [], void 0, forwarded);
9679
+ const body = this.body(this.sequence + 1, Date.now(), expires, message, this.state.receipt?.id ?? "abcdefghijklmnopqrstuv", reply, [], void 0, forwarded);
9559
9680
  try {
9560
9681
  this.records(body, this.options.credentials.peerKey ?? this.participation.pubKeyZ32);
9561
9682
  } catch {
@@ -9634,7 +9755,7 @@ var DhtDelivery = class DhtDelivery {
9634
9755
  ], expires = now + DHT_MESSAGE_TTL;
9635
9756
  const hops = readForwarded(forwarded);
9636
9757
  const fits = (element, withReply, withHops) => {
9637
- const body = this.body(this.state.sequence + 1, now, expires, message, this.state.receipt?.id ?? "abcdefghijklmnopqrstuv", withReply ? reply : void 0, [], void 0, withHops ? hops : void 0, element);
9758
+ const body = this.body(this.sequence + 1, now, expires, message, this.state.receipt?.id ?? "abcdefghijklmnopqrstuv", withReply ? reply : void 0, [], void 0, withHops ? hops : void 0, element);
9638
9759
  try {
9639
9760
  this.records(body, this.options.credentials.peerKey ?? this.participation.pubKeyZ32);
9640
9761
  } catch {
@@ -9823,13 +9944,14 @@ var DhtDelivery = class DhtDelivery {
9823
9944
  if (keepAlive && this.controlDue > now) return;
9824
9945
  const expires = pending?.expires ?? now - 12e4 + CONTROL_TTL;
9825
9946
  const issued = Math.min(now, Math.max(now - DHT_ISSUED_BACK_MS, expires - (pending ? DHT_MESSAGE_TTL : CONTROL_TTL), (pending?.message[1] ?? 0) - 3e4));
9826
- const { body, records, reactions } = this.fitted(this.state.sequence + 1, issued, expires, pending?.message ?? null, receipt?.id ?? null, pending?.reply, pending?.edit, pending?.forwarded, pending?.file);
9947
+ const { body, records, reactions } = this.fitted(this.sequence + 1, issued, expires, pending?.message ?? null, receipt?.id ?? null, pending?.reply, pending?.edit, pending?.forwarded, pending?.file);
9827
9948
  const reactionsWereDue = this.reactionsDue;
9828
9949
  this.reactionsDue = !reactions && !!this.options.reactions?.().length;
9829
9950
  const identity = !reseal && this.state.peerPinned && this.state.peerPinned !== "can" && this.pinned()?.identity || this.identity;
9951
+ const sequence = body[1];
9830
9952
  await this.persist({
9831
9953
  ...this.state,
9832
- sequence: body[1],
9954
+ sequence: sequence > this.state.sequence ? sequence + SEQUENCE_BLOCK - 1 : this.state.sequence,
9833
9955
  pending: pending ? {
9834
9956
  ...pending,
9835
9957
  attempts: pending.attempts + 1,
@@ -9841,6 +9963,7 @@ var DhtDelivery = class DhtDelivery {
9841
9963
  next: now + backoff(receipt.attempts)
9842
9964
  } : this.state.receipt
9843
9965
  });
9966
+ this.sequence = sequence;
9844
9967
  const before = {
9845
9968
  lastPublish: this.lastPublish,
9846
9969
  controlDue: this.controlDue
@@ -15619,7 +15742,7 @@ var ChatFiles = class {
15619
15742
  }
15620
15743
  return;
15621
15744
  case "pf-done":
15622
- if (record.state !== "verifying") return;
15745
+ if (record.state !== "verifying" && record.confirmed !== record.file.size) return;
15623
15746
  record.confirmed = record.file.size;
15624
15747
  this.end(outgoing, "done");
15625
15748
  return;
@@ -16846,6 +16969,7 @@ var ENGINE_ERRORS = {
16846
16969
  notEnoughSats: "Not enough sats in your wallet",
16847
16970
  noSharedMint: "You share no mint with this contact",
16848
16971
  ecashAlreadySpent: "This ecash was already spent somewhere else",
16972
+ ecashBeingReceived: "This ecash is already being received",
16849
16973
  ecashOtherUnit: "This ecash is in {unit}: only sat ecash can be redeemed here",
16850
16974
  reviewedEcashSpent: "The ecash for this payment was already spent somewhere else. Nothing was sent, and your balance now shows what the mint still holds.",
16851
16975
  reviewedSatsGone: "The sats this payment was reviewed with went to another payment. Nothing was sent: review it again.",
@@ -16954,8 +17078,6 @@ var PACKET_TRANSPORTS_FAST_ATTEMPTS = 3;
16954
17078
  var DHT_PIN_GRACE_MS = 1e4;
16955
17079
  /** Back after a restart (`resume`), dials wait this long at most for the native endpoint that carried the chat to start. */
16956
17080
  var RESUME_WAIT_MS = 1e4;
16957
- /** Two dials crossing: how long the lower key's side waits for its own to connect before it takes the contact's. */
16958
- var CROSSED_WAIT_MS = 3e3;
16959
17081
  /** A WebRTC attempt under way, not yet open: what a member's packet saying it has no WebRTC ends (`peerPacketTransports`). */
16960
17082
  var WEBRTC_ATTEMPT = /* @__PURE__ */ new Set([
16961
17083
  "offering",
@@ -17296,10 +17418,15 @@ var GhostLink = class {
17296
17418
  * was the app's in the background, not a restart (`resumeAfterAbsence`).
17297
17419
  */
17298
17420
  offered;
17421
+ /**
17422
+ * The WebRTC offer this side's dial made last, fallback allowed or not: one made before the contact's record was read
17423
+ * races once it is (`raceStandingOffer`).
17424
+ */
17425
+ offerMade;
17299
17426
  /** The attempt (its epoch) whose direct transports were knocked on as its offer went out (`knockEarly`), and whether that is under way. */
17300
17427
  knockedEarly = -1;
17301
17428
  knockingEarly = false;
17302
- /** When that offer's answer was applied (`RACE_ANSWERED_MS`); 0 while the data link is not connecting on it. */
17429
+ /** When this side's offer's answer was applied (`RACE_ANSWERED_MS`); 0 while the data link is not connecting on it. */
17303
17430
  answeredAt = 0;
17304
17431
  /** This side's offer is out unanswered, or answered and not connected yet: what is ranked after WebRTC may race it. */
17305
17432
  get offerOut() {
@@ -17632,6 +17759,7 @@ var GhostLink = class {
17632
17759
  channel.close();
17633
17760
  return;
17634
17761
  }
17762
+ if (this.rtcStartedUnpinned) this.openedUnpinned.add(channel);
17635
17763
  const plan = this.switcher.pending;
17636
17764
  if (plan?.choices.includes("webrtc/1") && this.paired?.state.status === "ready") this.attachCandidate(channel, void 0, plan).catch(() => {});
17637
17765
  else if (this.activeBinding) channel.close();
@@ -17647,6 +17775,7 @@ var GhostLink = class {
17647
17775
  const was = this.lastDataLinkState;
17648
17776
  traceLink(this.myPubKeyZ32, "datalink", { state });
17649
17777
  this.trackDataLink(state);
17778
+ if (state === "offering" || state === "answering") this.rtcStartedUnpinned = !options.pairing?.credentials.peerKey && !options.pairing?.credentials.expectedPeerKey;
17650
17779
  if (this.activeBinding) return;
17651
17780
  events.onDataLinkState?.(state);
17652
17781
  if (state === "open") {
@@ -17666,9 +17795,9 @@ var GhostLink = class {
17666
17795
  }
17667
17796
  }
17668
17797
  this.answeredAt = 0;
17669
- if (state === "connecting" && was === "offering" && this.offered?.epoch === this.connectionEpoch) {
17798
+ if (state === "connecting" && was === "offering" && this.offerMade?.epoch === this.connectionEpoch) {
17670
17799
  this.answeredAt = Date.now();
17671
- this.scheduleRace(this.offered.epoch, this.offered.at);
17800
+ if (this.offered?.epoch === this.connectionEpoch) this.scheduleRace(this.offered.epoch, this.offered.at);
17672
17801
  }
17673
17802
  if (state !== "idle") return;
17674
17803
  this.dialUnseen = was === "offering" && this.offerUnsent;
@@ -18259,6 +18388,7 @@ var GhostLink = class {
18259
18388
  }
18260
18389
  if (held && heldOn && reidentified.has(heldOn) && this.restartedRedial(held, heldOn)) return;
18261
18390
  if (this.redescribedDial()) return;
18391
+ if (!this.dialing) this.raceStandingOffer();
18262
18392
  if (this.channel || this.dialing) return;
18263
18393
  if (news || !again) {
18264
18394
  this.autoConnectFailures = 0;
@@ -18836,6 +18966,10 @@ var GhostLink = class {
18836
18966
  ...resume && { resume },
18837
18967
  ...woke && { woke }
18838
18968
  } : void 0;
18969
+ this.offerMade = {
18970
+ epoch,
18971
+ at: offeredAt
18972
+ };
18839
18973
  const gate = this.dialGate;
18840
18974
  this.dialGate = void 0;
18841
18975
  const replaced = new Promise((resolve) => {
@@ -18847,10 +18981,12 @@ var GhostLink = class {
18847
18981
  traceLink(this.myPubKeyZ32, "dial-held", {});
18848
18982
  this.afterRtc = void 0;
18849
18983
  this.offered = void 0;
18984
+ this.offerMade = void 0;
18850
18985
  return;
18851
18986
  }
18852
18987
  if (this.dataLink.state !== "idle" || epoch !== this.connectionEpoch) {
18853
18988
  this.scheduleRace(epoch, offeredAt);
18989
+ this.raceStandingOffer();
18854
18990
  return;
18855
18991
  }
18856
18992
  this.rtcUnstarted();
@@ -19016,6 +19152,31 @@ var GhostLink = class {
19016
19152
  }, Math.max(0, Math.min(...at) - now));
19017
19153
  }
19018
19154
  /**
19155
+ * This side's offer went out before the contact's record was read (a joiner's early one, `dialEarly`): nothing was
19156
+ * known of what the contact runs, so it was made with no fallback and nothing raced it. Once the record says, and both
19157
+ * sides allow it, the offer races what both run as one made then would: answered and not connected `RACE_ANSWERED_MS`
19158
+ * later, the relayed ones too. Behind a VPN that drops UDP, a first pairing went live over the relayed Iroh only once
19159
+ * the attempt timed out and the next one raced, 32 to 66 s in (zero's VPN lab, 2026-10-13).
19160
+ */
19161
+ raceStandingOffer() {
19162
+ const made = this.offerMade, epoch = this.connectionEpoch;
19163
+ if (!made || made.epoch !== epoch || this.offered?.epoch === epoch || !this.offerOut || this.channel || this.stopped) return;
19164
+ if (!this.fallback || !this.peerFallback || !this.peerTransports) return;
19165
+ const relayed = relayedTransports(this.localDescriptors(), this.peerDescriptors);
19166
+ const rest = rankTransports(this.transportOffer(), this.peerTransports, relayed).filter((t) => t !== "webrtc/1");
19167
+ if (!rest.length) return;
19168
+ this.offered = {
19169
+ epoch,
19170
+ at: made.at
19171
+ };
19172
+ this.afterRtc = {
19173
+ epoch,
19174
+ rest
19175
+ };
19176
+ traceLink(this.myPubKeyZ32, "race-standing", { transports: rest });
19177
+ this.scheduleRace(epoch, made.at);
19178
+ }
19179
+ /**
19019
19180
  * Back after an absence, this side answers an offer the contact made while it was away (it dials whatever its key):
19020
19181
  * the contact looks for that answer at the pace of an offer out a while (every 8 s on the relays), and its attempt may
19021
19182
  * end before it does. The direct transports both run are knocked on meanwhile, and the relayed ones where both apps
@@ -19148,6 +19309,7 @@ var GhostLink = class {
19148
19309
  const left = next.rest.filter((t) => t !== "webrtc/1" && !due.includes(t));
19149
19310
  if (!due.length) {
19150
19311
  this.scheduleRace(epoch, offeredAt);
19312
+ this.raceStandingOffer();
19151
19313
  return;
19152
19314
  }
19153
19315
  this.afterRtc = left.length ? {
@@ -19172,6 +19334,7 @@ var GhostLink = class {
19172
19334
  if (live || epoch !== this.connectionEpoch || this.channel || this.stopped) return;
19173
19335
  if (this.dataLink.state !== "idle") {
19174
19336
  this.scheduleRace(epoch, offeredAt);
19337
+ this.raceStandingOffer();
19175
19338
  return;
19176
19339
  }
19177
19340
  const rest = this.afterRtc;
@@ -20247,8 +20410,8 @@ var GhostLink = class {
20247
20410
  * Both sides dialled at once (two apps back after a restart, each knocking): the lower key's dial carries the chat, on
20248
20411
  * both sides; the higher key's side takes it over as a replacement (`attachReplacement`). Here, on the lower key's
20249
20412
  * side, the contact's connection waits: dropped once this side's own dial connects, taken if that dial fails, or has
20250
- * not connected within `CROSSED_WAIT_MS` (the contact may be dialling an app that is not up yet, for as long as its
20251
- * dial takes to give up).
20413
+ * not connected within `CROSSED_WAIT_MS` (it stalled, or goes to an endpoint the contact no longer runs, for as long
20414
+ * as it takes to give up).
20252
20415
  */
20253
20416
  crossed = null;
20254
20417
  parkCrossed(channel, binding) {
@@ -20265,7 +20428,7 @@ var GhostLink = class {
20265
20428
  this.dialing = false;
20266
20429
  this.attempt = null;
20267
20430
  this.attach(channel, binding);
20268
- }, CROSSED_WAIT_MS);
20431
+ }, 500);
20269
20432
  channel.onClose = () => {
20270
20433
  if (this.crossed?.channel === channel) {
20271
20434
  clearTimeout(timer);
@@ -20347,6 +20510,8 @@ var GhostLink = class {
20347
20510
  dialedIn = /* @__PURE__ */ new WeakSet();
20348
20511
  /** Connections whose session started with no key pinned nor named by the invite. */
20349
20512
  openedUnpinned = /* @__PURE__ */ new WeakSet();
20513
+ /** The WebRTC offer under way was made or answered with no key pinned nor named by the invite (its channel opens later). */
20514
+ rtcStartedUnpinned = false;
20350
20515
  /**
20351
20516
  * `migration`: the channel is a candidate that takes over from the session open now once it authenticated. With a
20352
20517
  * `plan`, a live switch both sides agreed; without one, a replacement (`attachReplacement`): the contact dialled
@@ -25251,6 +25416,12 @@ var byeSigned = (f) => utf8Encode(JSON.stringify([
25251
25416
  f.e,
25252
25417
  f.ts
25253
25418
  ]));
25419
+ var leftSigned = (commit) => utf8Encode(JSON.stringify([
25420
+ "ghostly-group/1 left",
25421
+ commit.g,
25422
+ commit.e,
25423
+ commitHash(commit)
25424
+ ]));
25254
25425
  var editSigned = (f) => utf8Encode(JSON.stringify([
25255
25426
  "ghostly-group/1 edit",
25256
25427
  f.g,
@@ -25731,11 +25902,43 @@ var GroupSession = class {
25731
25902
  }];
25732
25903
  });
25733
25904
  }
25734
- remove(memberKey, now = Date.now()) {
25905
+ /**
25906
+ * A welcome for a member already in the roster that never got the first one (its chat closed while it was let in):
25907
+ * nothing is committed. It carries the secrets of the epochs it was in, from its admission on, sealed to its key.
25908
+ */
25909
+ rewelcome(memberKey) {
25735
25910
  return this.serialize(async () => {
25736
25911
  this.requireAdmin();
25737
25912
  if (!rosterHas(this.roster, memberKey) || memberKey === this.myKey) throw new Error("Not a member");
25738
- await this.commit("remove", memberKey, now);
25913
+ const chain = this.state.chain;
25914
+ let admitted = chain.length - 1;
25915
+ while (admitted >= 0 && !(chain[admitted].k === "add" && chain[admitted].s === memberKey)) admitted--;
25916
+ if (admitted < 0) throw new Error("Not a member");
25917
+ const secrets = chain.slice(admitted).filter((c) => this.state.secrets[c.e] && rosterHas(c.m, memberKey)).map((c) => ({
25918
+ e: c.e,
25919
+ s: sealSecret(memberKey, this.secret(c.e), secretAad$1(this.id, c.e, memberKey))
25920
+ }));
25921
+ const pieces = chainPieces(chain);
25922
+ const tail = pieces.pop();
25923
+ return [...pieces.map((commits) => ({
25924
+ t: "group-chain",
25925
+ g: this.id,
25926
+ commits
25927
+ })), {
25928
+ t: "group-welcome",
25929
+ g: this.id,
25930
+ name: this.name,
25931
+ commits: tail,
25932
+ secrets
25933
+ }];
25934
+ });
25935
+ }
25936
+ /** `left`: the member asked to leave (on the admin's chat), and the commit says so (`GroupCommitFrame.left`). */
25937
+ remove(memberKey, now = Date.now(), { left = false } = {}) {
25938
+ return this.serialize(async () => {
25939
+ this.requireAdmin();
25940
+ if (!rosterHas(this.roster, memberKey) || memberKey === this.myKey) throw new Error("Not a member");
25941
+ await this.commit("remove", memberKey, now, left);
25739
25942
  });
25740
25943
  }
25741
25944
  transferAdmin(memberKey, now = Date.now()) {
@@ -25752,7 +25955,7 @@ var GroupSession = class {
25752
25955
  await this.commit("rotate", void 0, now);
25753
25956
  });
25754
25957
  }
25755
- async commit(kind, subject, now) {
25958
+ async commit(kind, subject, now, left = false) {
25756
25959
  if (this.hooks.adminWork?.() === false) throw new Error(GROUP_ADMIN_OFF_ERROR);
25757
25960
  if (this.hooks.adminTurn && !await this.hooks.adminTurn()) throw engineError("groupTurnUnconfirmed");
25758
25961
  if (this.state.chain.length >= 1024) throw new Error("This group has reached its membership history limit. Create a new group.");
@@ -25776,6 +25979,8 @@ var GroupSession = class {
25776
25979
  ...draft,
25777
25980
  c: confirmationTag(epochKeys(secret, this.id, epoch).confirm, commitUntaggedHash(draft))
25778
25981
  }, this.identity.seed);
25982
+ const mark = left && kind === "remove" ? toBase64Url(sign(leftSigned(commit), this.identity.seed)) : void 0;
25983
+ if (mark) (this.state.leftMarks ??= {})[epoch] = mark;
25779
25984
  this.state.chain.push(commit);
25780
25985
  this.chainMovedAt = this.clock();
25781
25986
  this.state.secrets[epoch] = toBase64Url(secret);
@@ -25791,7 +25996,8 @@ var GroupSession = class {
25791
25996
  t: "group-commit",
25792
25997
  g: this.id,
25793
25998
  commit,
25794
- ...member ? { secret: sealSecret(key, secret, secretAad$1(this.id, epoch, key)) } : {}
25999
+ ...member ? { secret: sealSecret(key, secret, secretAad$1(this.id, epoch, key)) } : {},
26000
+ ...mark ? { left: mark } : {}
25795
26001
  });
25796
26002
  }
25797
26003
  this.hooks.changed();
@@ -26191,7 +26397,7 @@ var GroupSession = class {
26191
26397
  case "group-bye":
26192
26398
  await this.receiveBye(from, raw);
26193
26399
  break;
26194
- case "group-leave": if (this.isAdmin && this.hooks.adminWork?.() !== false && rosterHas(this.roster, from) && from !== this.myKey) await this.commitUnlessTurnUnconfirmed("remove", from);
26400
+ case "group-leave": if (this.isAdmin && this.hooks.adminWork?.() !== false && rosterHas(this.roster, from) && from !== this.myKey) await this.commitUnlessTurnUnconfirmed("remove", from, true);
26195
26401
  }
26196
26402
  return this.state.status === "active" ? this.passOn : this.state.status === "removed" ? this.passOn.filter((f) => f.t === "group-commit") : [];
26197
26403
  } finally {
@@ -26306,7 +26512,7 @@ var GroupSession = class {
26306
26512
  return;
26307
26513
  }
26308
26514
  if (bye.e < this.admittedAt(bye.k)) return;
26309
- if (this.isAdmin && this.hooks.adminWork?.() !== false && await this.commitUnlessTurnUnconfirmed("remove", bye.k)) return;
26515
+ if (this.isAdmin && this.hooks.adminWork?.() !== false && await this.commitUnlessTurnUnconfirmed("remove", bye.k, true)) return;
26310
26516
  if (from !== bye.k && this.byesPassed.get(bye.k)?.sig === bye.sig) return;
26311
26517
  this.byesPassed.set(bye.k, bye);
26312
26518
  this.took(bye);
@@ -26331,9 +26537,9 @@ var GroupSession = class {
26331
26537
  * An automatic commit (a member's leave): made, or not made when no fresh turn read says this device is the active
26332
26538
  * one (`adminTurn`). Then false, and the caller goes on as a member would. Any other refusal is thrown as before.
26333
26539
  */
26334
- async commitUnlessTurnUnconfirmed(kind, subject) {
26540
+ async commitUnlessTurnUnconfirmed(kind, subject, left = false) {
26335
26541
  try {
26336
- await this.commit(kind, subject, Date.now());
26542
+ await this.commit(kind, subject, Date.now(), left);
26337
26543
  return true;
26338
26544
  } catch (error) {
26339
26545
  if (error instanceof EngineError && error.engineCode === "groupTurnUnconfirmed") return false;
@@ -26595,6 +26801,7 @@ var GroupSession = class {
26595
26801
  const result = verifyCommit(raw, this.state.chain[e - 1] ?? null, this.id);
26596
26802
  if ("error" in result || result.commit.by !== known.by) return;
26597
26803
  if (commitHash(result.commit) !== commitHash(known)) await this.fork(`Member ${from.slice(0, 8)} holds a different membership history for epoch ${e}`);
26804
+ else if (this.takeLeftMark(known, frame.left)) await this.persist();
26598
26805
  return;
26599
26806
  }
26600
26807
  if (e > this.epoch + 1) {
@@ -26608,11 +26815,13 @@ var GroupSession = class {
26608
26815
  if (result.error.startsWith("Commit does not follow") && verifyCommitSignature(raw)?.by === this.admin) await this.fork(`Member ${from.slice(0, 8)} holds a different membership history for epoch ${e}`);
26609
26816
  return;
26610
26817
  }
26818
+ const mark = this.takeLeftMark(result.commit, frame.left) ? frame.left : void 0;
26611
26819
  await this.apply(result.commit, frame.secret);
26612
26820
  this.took({
26613
26821
  t: "group-commit",
26614
26822
  g: this.id,
26615
- commit: result.commit
26823
+ commit: result.commit,
26824
+ ...mark ? { left: mark } : {}
26616
26825
  });
26617
26826
  if (this.state.status === "active" && !this.state.secrets[this.epoch]) this.ask(from);
26618
26827
  const next = this.pendingCommits.get(this.epoch + 1);
@@ -26621,6 +26830,17 @@ var GroupSession = class {
26621
26830
  await this.receiveCommit(from, next);
26622
26831
  }
26623
26832
  }
26833
+ /** Keeps the admin's mark on a commit that took out a member who left (`GroupCommitFrame.left`) when it holds; true when new. */
26834
+ takeLeftMark(commit, mark) {
26835
+ if (commit.k !== "remove" || typeof mark !== "string" || mark.length !== 86 || !B64$1.test(mark) || this.state.leftMarks?.[commit.e]) return false;
26836
+ try {
26837
+ if (!verify(fromBase64Url(mark), leftSigned(commit), publicKeyFromZ32(commit.by))) return false;
26838
+ } catch {
26839
+ return false;
26840
+ }
26841
+ (this.state.leftMarks ??= {})[commit.e] = mark;
26842
+ return true;
26843
+ }
26624
26844
  async apply(commit, sealed) {
26625
26845
  this.state.chain.push(commit);
26626
26846
  this.chainMovedAt = this.clock();
@@ -26683,11 +26903,13 @@ var GroupSession = class {
26683
26903
  const answer = [];
26684
26904
  for (let e = frame.e + 1; e <= this.epoch; e++) {
26685
26905
  const commit = this.state.chain[e], secret = this.secret(e);
26906
+ const left = this.state.leftMarks?.[e];
26686
26907
  answer.push({
26687
26908
  t: "group-commit",
26688
26909
  g: this.id,
26689
26910
  commit,
26690
- ...secret && rosterHas(commit.m, from) ? { secret: sealSecret(from, secret, secretAad$1(this.id, e, from)) } : {}
26911
+ ...secret && rosterHas(commit.m, from) ? { secret: sealSecret(from, secret, secretAad$1(this.id, e, from)) } : {},
26912
+ ...left ? { left } : {}
26691
26913
  });
26692
26914
  }
26693
26915
  const theirs = new Set(Array.isArray(frame.secrets) ? frame.secrets.filter((n) => Number.isSafeInteger(n)) : []);
@@ -27677,6 +27899,15 @@ var CommunitySession = class {
27677
27899
  return -1;
27678
27900
  }
27679
27901
  /**
27902
+ * Someone the chain removed (not one who left) and who is not back in: nothing it signs is heard any more, even for
27903
+ * an epoch it was in. What one who left wrote while in the group is still taken and handed on, for the epochs it was
27904
+ * in (WISP 903 § Leaving and removal): its goodbye reaches the members who were away.
27905
+ */
27906
+ wasExpelled(key) {
27907
+ const i = this.outIndex(key);
27908
+ return i >= 0 && this.state.chain[i].k === "remove";
27909
+ }
27910
+ /**
27680
27911
  * What someone the chain took out is told when it comes back (WISP 903 § Leaving and removal): the commits from where
27681
27912
  * it says it is (`sync`: its tip, or its locator) up to the one that took it out, and nothing after. Later commits
27682
27913
  * are not its business (a `link` commit names the new entry key); no secret, no stored frame, no seed goes with them.
@@ -28639,7 +28870,7 @@ var CommunitySession = class {
28639
28870
  return false;
28640
28871
  }
28641
28872
  if (!verify(fromBase64Url(raw.sig), messageSigned(raw), publicKeyFromZ32(raw.s))) return false;
28642
- if (this.wasRemoved(raw.s)) return false;
28873
+ if (this.wasExpelled(raw.s)) return false;
28643
28874
  const found = this.commitByShort(raw.e, raw.h);
28644
28875
  if (!found) {
28645
28876
  this.park(from, raw);
@@ -28883,7 +29114,7 @@ var CommunitySession = class {
28883
29114
  if (stored.s === from) continue;
28884
29115
  const high = have[stored.s]?.[seenKey(stored.e, stored.h)];
28885
29116
  if (Number.isSafeInteger(high) && high >= stored.n) continue;
28886
- if (this.wasRemoved(stored.s)) continue;
29117
+ if (this.wasExpelled(stored.s)) continue;
28887
29118
  const found = this.commitByShort(stored.e, stored.h);
28888
29119
  if (!found) continue;
28889
29120
  const reads = rosterHas(this.rosterAt(found.hash) ?? [], from), mine = stored.s === this.myKey && unheard.has(frameKey(stored));
@@ -30009,4 +30240,4 @@ function appStoreDecision(held, read) {
30009
30240
  //#endregion
30010
30241
  export { parseCommunityEdit as $, receivedTimestamp as $a, identityFromSeed as $c, DHT_FILE_TOO_LARGE as $i, readGroupHave as $n, HANDOFF_PAKE as $o, vapidKeysMatch as $r, verifyEnrollProof as $s, signDidDhtPacket as $t, encodeCommunityLink as A, reactionEmoji as Aa, signTurnPacket as Ac, DHT_TEXT_CAPABILITY as Ai, resolveLightningDestination as An, webLocalFetch as Ao, FILE_LIMITS as Ar, readHandoffTurnFrame as As, DidError as At, GroupWakeLimiter as B, PR_CHECKS as Ba, normalizeRelayUrl as Bc, identityRevocationValue as Bi, INVITE_TAKEN as Bn, extractParamsFromSdp as Bo, mayPin as Br, ENROLL_DONE as Bs, didWebHost as Bt, openGroupTyping as C, readForwarded as Ca, classifyTurnRead as Cc, normalizeDomain as Ci, normalizePubkyKey as Cn, DHT_POLL_INTERVALS as Co, pickHubs as Cr, newHandoffId as Cs, parseAtprotoDidDocument as Ct, GROUP_READ_NOTE_COMMUNITY as D, ReactionWindow as Da, firstDeviceSetSecret as Dc, txtValue as Di, homeserverWebEndpoint as Dn, GhostlyHttpError as Do, readLobby as Dr, readHandoffHave as Ds, DID_DOCUMENT_MAX_BYTES as Dt, CommunitySession as E, REACTION_LIMITS as Ea, TURN_MAX as Ec, recordsName as Ei, pubkyProofPath as En, presenceSeenAt as Eo, readBeaconHead as Er, readHandoffDone as Es, verifyAtprotoRecordProof as Et, knockIdentity as F, pairedReplyAuthor as Fa, turnStatedVerifier as Fc, IDENTITY_REVOCATION_LABEL as Fi, findEndpoint as Fn, RTC_CONFIG as Fo, TypingSender as Fr, readSetUpdate as Fs, didSignedMessages as Ft, GROUP_REACTED_FRAME as G, cardLine as Ga, withRequestOptions as Gc, lastSharedWithMe as Gi, GROUP_FILE_LIMITS as Gn, LEGACY_SERVICES as Go, WAKE_CALL_INTERVAL_MS as Gr, enrollCancelReason as Gs, signingKeys as Gt, groupWakes as H, RUN_RESULTS as Ha, traceLink as Hc, newIdentityBinding as Hi, pairedMessageFrame as Hn, signalHasVideo as Ho, pinIsNewer as Hr, ENROLL_HELLO as Hs, parseDid as Ht, knockRecords as I, readReply as Ia, turnVerifierMark as Ic, IdentityCheckUnavailable as Ii, isFederationId as In, answersOffer as Io, sanitizeTypingStatus as Ir, setAckFrame as Is, didWebDocumentUrl as It, frameBytes as J, sameButtonText as Ja, createRelayPayload as Jc, validExternalKey as Ji, GROUP_WANT_NO_FRAME as Jn, HANDOFF_CANCEL as Jo, newWakeToken as Jr, enrollGrantFrame as Js, didDhtDocument as Jt, GROUP_READ_NOTE as K, cardUrl as Ka, PacketTooLargeError as Kc, PeerProofs as Ki, GROUP_HAVE_FRAME as Kn, serviceIdFromName as Ko, WakeLimiter as Kr, enrollDigits as Ks, staticDidDocument as Kt, mergeKnocks as L, replySnippet as La, DEFAULT_RELAYS as Lc, IdentityExchange as Li, parseCashuRequestPayload as Ln, buildSdpFromSignal as Lo, typingActivity as Lr, setUpdateFrame as Ls, didWebFile as Lt, decodeGroupEntryLink as M, readReaction as Ma, turnInstance as Mc, dialDescriptors as Mi, ENDPOINT as Mn, decodeControl as Mo, transferStalled as Mr, sealHandoffPiece as Ms, checkDidSignature as Mt, encodeGroupEntryLink as N, validReactionNumber as Na, turnKeys as Nc, IDENTITY_MAX_EVIDENCE as Ni, cashuRequestPayload as Nn, encodeControl as No, TYPING_KINDS as Nr, SET_ACK as Ns, didGhostlyServiceEntry as Nt, communityHasFrame as O, nextReactionNumber as Oa, nextTurnPosition as Oc, wellKnownUrl as Oi, pubkyHomeserverOf as On, formatLocalTarget as Oo, shouldBeHub as Or, readHandoffHello as Os, DID_FILE_MAX_BYTES as Ot, entryParams as P, wireReaction as Pa, turnName as Pc, IDENTITY_MAX_VALIDITY as Pi, fedimintRequestPayload as Pn, CALL_SIGNAL_MAX_AGE_MS as Po, TYPING_REFRESH_MS as Pr, SET_UPDATE as Ps, didJwsAlg as Pt, carryMentions as Q, heardTime as Qa, createIdentity as Qc, irohRelayUrl as Qi, readGroupFileMeta as Qn, HANDOFF_OFFER as Qo, checkPushEndpoint as Qr, readEnrollHello as Qs, resolveDidDht as Qt, readKnocks as R, BUTTON_ID as Ra, RelayTransport as Rc, boundVerifiedIdentity as Ri, parseFedimintRequestPayload as Rn, callRtcConfig as Ro, GROUP_PIN_FRAME as Rr, signTombstone as Rs, didWebFileNames as Rt, groupTypingEpoch as S, forwardedAgain as Sa, TURN_SETTLE_MS as Sc, nip05RootKey as Si, newPubkyProofFolder as Sn, DirectPathWatch as So, newerHead as Sr, handoffVersions as Ss, normalizeAtprotoHandle as St, COMMUNITY_LIMITS as T, GROUP_REACTION_FRAME as Ta, TURN_LAST_SEQUENCE as Tc, recordsFromWellKnown as Ti, pubkyProofFile as Tn, RELAY_POLL_INTERVALS as To, readBeacon as Tr, readHandoffCancel as Ts, AtprotoProofError as Tt, parseGroupWakeFrame as U, STATUS_CARD_LIMITS as Ua, budgetRetryMs as Uc, receivedIdentityStatus as Ui, engineError as Un, emptyLinkRecords as Uo, pinNumberHolds as Ur, ENROLL_PROOF as Us, parseDidDocument as Ut, groupWakeFrame as V, PR_STATES as Va, setLinkTraceSink as Vc, identityStatement as Vi, LIVENESS_PING_MS as Vn, parseCallSignal as Vo, nextPinNumber as Vr, ENROLL_GRANT as Vs, normalizeDid as Vt, GROUP_LIMITS as W, buttonLabelClash as Wa, isDiscoveryBudgetError as Wc, revokesIdentity as Wi, engineText as Wn, HTTP_SERVICE_PROTO as Wo, readPin as Wr, enrollCancelFrame as Ws, parseDidSignaturePaste as Wt, GROUP_EDIT_FRAME as X, arrivalKey as Xa, openRelayPayload as Xc, automaticTransport as Xi, groupFileFallback as Xn, HANDOFF_HAVE as Xo, relayRequest as Xr, enrollProofFrame as Xs, encodeDidDhtDeactivated as Xt, COMMUNITY_EDIT_FRAME as Y, statusCardText as Ya, newerPacket as Yc, TRANSPORTS as Yi, fetchesByItself as Yn, HANDOFF_DONE as Yo, parseWakeFrame as Yr, enrollHelloFrame as Ys, didDhtFromPublicKey as Yt, GROUP_EDIT_TEXT_BYTES as Z, claimedTime as Za, parseRelayPayload as Zc, irohDescriptor as Zi, isGroupMessageId as Zn, HANDOFF_HELLO as Zo, wakeRequest as Zr, readEnrollGrant as Zs, encodeDidDhtPacket as Zt, checkAppManifest as _, EDIT_BUFFER_MS as _a, newDeviceSetSecret as _c, dnsProofName as _i, sparkInvoiceDetails as _n, MEMBER_KEY as _o, freshHubs as _r, handoffOfferFrame as _s, atprotoDidMethod as _t, planAppUpdate as a, readImageMeta as aa, decodeInviteCode as ac, STATUS_CARD_CAPABILITY as ai, bytesEqual as al, SEPOLIA_TEST_USDT_FAUCET as an, checkAppRevokeStatement as ao, SIGNAL_HOLD_MS as ar, handoffAttemptAllowed as as, parseSshPublicKey as at, GROUP_TYPING_FRAME as b, dhtEditId as ba, TURN_PUT_WINDOW_MS as bc, encodeDnsQuery as bi, isPubkyKey as bn, verifyCommitSignature as bo, mergeBeacon as br, handoffRetryAfter as bs, checkAtprotoProofRecord as bt, readAppStore as c, sanitizeFileName as ca, inviteOwnership as cc, HOLD_DATED_BACK_MS as ci, toBase64Url as cl, WALLET_NETWORKS as cn, readAppRevocation as co, readGroupSignal as cr, handoffBusyFrame as cs, sshSignCommand as ct, APP_BUNDLE_LIMITS as d, VOICE_LIMITS as da, HANDOFF_CAPABILITY as dc, HoldRefusedError as di, utf8Encode as dl, formatPaymentAmount as dn, signAppRevocation as do, meshHubs as dr, handoffContextProof as ds, BitcoinAddressError as dt, DHT_MESSAGE_TTL as ea, createDeviceInvite as ec, chatAppId as ei, identityFromSeedB64 as el, ETHEREUM_USDT as en, shownTime as eo, readGroupWant as er, HANDOFF_PIECE_BYTES as es, mentionsMember as et, APP_BUNDLE_MAGIC as f, baseMime as fa, deviceEchoFrame as fc, isHoldUrl as fi, validatePaymentTarget as fn, canonicalJson as fo, pickMeshHubs as fr, handoffDataFrame as fs, decodeAnyBitcoinAddress as ft, assembleAppBundle as g, parseVideoMeta as ga, devicePingFrame as gc, decodeDnsResponse as gi, sparkAddressKind as gn, MAX_GROUP_CHAIN as go, doorHubs as gr, handoffManifestFrames as gs, ATPROTO_PROOF_SCOPE as gt, appViewOf as h, voicePeaksOf as ha, deviceLinkPairing as hc, DOMAIN_FILE_MAX_BYTES as hi, SPARK_PROVIDER as hn, GROUP_MEMBER_CAP as ho, beaconRecords as hr, handoffHelloFrame as hs, ATPROTO_PROOF_COLLECTION as ht, checkAppBeforeRun as i, parseImageMeta as ia, createChatInvite as ic, EDIT_CAPABILITY as ii, sanitizeAvatar as il, SEPOLIA_TEST_USDT as in, appRef as io, SIGNAL_FRESH_MS as ir, filePartName as is, SshSigError as it, KNOCK_TTL_MS as j, reactionIsNewer as ja, signTurnRelease as jc, capsDescriptors as ji, decodeBolt11 as jn, LIMITS as jo, transferEnded as jr, readHandoffVerified as js, checkDidJws as jt, decodeCommunityLink as k, queueReaction as ka, readTurnPacket as kc, CapsExchange as ki, requestLnurlInvoice as kn, parseLocalTarget as ko, ChatFiles as kr, readHandoffPake as ks, DID_JWS_MAX_LENGTH as kt, readAppStoreSteps as l, sanitizeMime as la, DEVICES_CAPABILITY as lc, HOLD_LIMITS as li, toZ32 as ll, assertTokenUnits as ln, readAppRevocations as lo, MESH_HUBS as lr, handoffCancelFrame as ls, verifySshSignature as lt, appDigest as m, parseVoiceMeta as ma, deviceKeyZ32 as mc, readManifest as mi, SPARK_NETWORKS as mn, readCanonicalJson as mo, beaconKeys as mr, handoffEphemeral as ms, ATPROTO_FULL_SCOPE as mt, appStoreDecision as n, DHT_FILE_CAPABILITY as na, readDeviceInvite as nc, APPS_CAPABILITY as ni, verify as nl, ONCHAIN_PROVIDER as nn, APP_STATEMENT_LIMITS as no, CarriedTransport as nr, HANDOFF_TIMINGS as ns, SSHSIG_MAX_ARMOR as nt, APP_STORE_LIMITS as o, formatFileSize as oa, encodeInviteCode as oc, UPGRADE_CAPABILITY as oi, fromBase64Url as ol, TEST_USDT_FAUCET_AMOUNT as on, isAppKey as oo, SIGNAL_REPLY_MS as or, handoffAttemptFailed as os, parseSshSignature as ot, APP_ICON_PATH as p, fileMessageText as pa, deviceEchoNonce as pc, newMailbox as pi, walletNetworkOf as pn, canonicalJsonBytes as po, COMMUNITY_TOPOLOGY as pr, handoffDigest as ps, ATPROTO_DECLARED_SCOPE as pt, GroupSession as q, checkStatusCard as qa, RELAY_PAYLOAD_MAX_BYTES as qc, emptyProofLedger as qi, GROUP_WANT_FRAME as qn, HANDOFF_ACK as qo, deviceWakeRequest as qr, enrollDoneFrame as qs, strictBase64Url as qt, appUpdateDecision as r, dhtFileId as ra, OwnInviteError as rc, BUTTONS_CAPABILITY as ri, MAX_AVATAR_LENGTH as rl, PaymentPreflightError as rn, appFingerprint as ro, GROUP_SIGNAL_FRAME as rr, HANDOFF_VERIFIED as rs, SSHSIG_NAMESPACE as rt, checkAppStoreIndex as s, safeBlobType as sa, inviteLink as sc, HOLD_CAPABILITY as si, randomBytes as sl, USDT_PUBLIC_RPC as sn, isAppRef as so, groupSignalFrame as sr, handoffAttemptSucceeded as ss, sameSshKey as st, appRevocationFor as t, peekDhtMailbox as ta, deviceInviteJoinerParams as tc, isAppVersion as ti, publicKeyFromZ32 as tl, EVM_TEST_CHAINS as tn, APP_PREFIXES as to, readGroupWantNo as tr, HANDOFF_REQUEST as ts, groupName as tt, signAppStore as u, PLAYABLE_AUDIO as ua, ENROLL_CAPABILITY as uc, HoldKeys as ui, utf8Decode as ul, assertWholeSats as un, signAppObject as uo, mayBeHub as ur, handoffContext as us, verifyBitcoinMessage as ut, isAppUrl as v, EDIT_RATE_WINDOW_MS as va, seedSigner as vc, domainTxtRecord as vi, isBitcoinAddress as vn, commitHash as vo, lobbyKeys as vr, handoffPakeFrame as vs, atprotoProofRecord as vt, sealGroupTyping as w, COMMUNITY_REACTION_FRAME as wa, turnPutSummary as wc, recordsFromTxt as wi, pubkyProofCapability as wn, EXPECT_PEER_MS as wo, rankHubs as wr, readHandoffAck as ws, shortAtprotoDid as wt, GroupTypingBoard as x, parseLinkPreview as xa, TURN_RAISE_READ_TIMEOUT_MS as xc, isPublicAddress as xi, isPubkyProofFolder as xn, edgeParams as xo, mergeLobby as xr, handoffStreamKey as xs, isAtprotoDid as xt, readAppBundle as y, RateWindow as ya, webCryptoSigner as yc, domainWellKnownFile as yi, PUBKY_PROOF_MAX_BYTES as yn, rosterHas as yo, lobbyRecords as yr, handoffReleaseFrame as ys, atprotoProofRkey as yt, GROUP_WAKE_FRAME as z, ITEM_STATES as za, currentRelays as zc, emptyIdentityLedger as zi, GhostLink as zn, callSignalHeardAt as zo, PIN_LIMITS as zr, verifySetUpdate as zs, didWebFileUrl as zt };
30011
30242
 
30012
- //# sourceMappingURL=src-_FzJkeAJ.js.map
30243
+ //# sourceMappingURL=src-CbLkHfz8.js.map