@frod.io/bridge 0.7.2 → 0.7.3

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 (2) hide show
  1. package/dist/cli.js +62 -11
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // @frod.io/bridge 0.7.2. One file; do not edit: the source is apps/bridge/src in the platform repository.
2
+ // @frod.io/bridge 0.7.3. One file; do not edit: the source is apps/bridge/src in the platform repository.
3
3
  import { createRequire as __createRequire } from 'node:module';
4
4
  const require = __createRequire(import.meta.url);
5
5
  var __create = Object.create;
@@ -6481,6 +6481,9 @@ function workdirRequestFor(spec) {
6481
6481
  }
6482
6482
 
6483
6483
  // src/connection.ts
6484
+ var RECONCILE_INTERVAL_MS = 6e4;
6485
+ var RECONCILE_WAIT_MS = 1500;
6486
+ var STOP_BOUND_MS = 1e4;
6484
6487
  var HostConnection = class {
6485
6488
  constructor(o) {
6486
6489
  this.o = o;
@@ -6498,6 +6501,7 @@ var HostConnection = class {
6498
6501
  /** Which runtime started each session — a turn must reach the one holding it. */
6499
6502
  sessionAdapter = /* @__PURE__ */ new Map();
6500
6503
  attempt = 0;
6504
+ reconcileTimer = null;
6501
6505
  connectedAt = null;
6502
6506
  /** Requests this computer sent to the pod (night 2: permissions), by id. */
6503
6507
  nextRequestId = 1;
@@ -6593,6 +6597,7 @@ var HostConnection = class {
6593
6597
  const reason = reasonBuf.toString();
6594
6598
  this.connectedAt = null;
6595
6599
  this.ws = null;
6600
+ this.stopReconciling();
6596
6601
  this.failPending(`the connection closed (${code})`);
6597
6602
  void this.endAllSessions(`connection closed (${code}${reason ? ` ${reason}` : ""})`);
6598
6603
  if (final && (code === 4403 || code === 4404 || final.reason.startsWith("Another computer") || final.reason.startsWith("This computer left") || final.reason.startsWith("This computer was disconnected") || /access was revoked/.test(final.reason))) {
@@ -6634,6 +6639,8 @@ var HostConnection = class {
6634
6639
  const hosted = hostedAgentsWords({ bindings, agentName: this.o.config.agentName });
6635
6640
  log.event("host.welcome", { host: params.hostName ?? this.o.config.name, host_id: params.hostId ?? null, person: params.person?.name ?? null, workspace: params.workspaceId, agents: bindings.map((b) => `${b.agentName}: ${bindingWords(b)}`), ping_ms: params.pingIntervalMs });
6636
6641
  log.info(`Connected \u2014 ${this.o.config.name} hosts ${hosted}`);
6642
+ this.reconcile("welcome");
6643
+ this.startReconciling();
6637
6644
  if (typeof params.hostId === "string") {
6638
6645
  this.o.onWelcome?.({ hostId: params.hostId, hostName: String(params.hostName ?? this.o.config.name), person: params.person ?? null, bindings, bridge: params.bridge });
6639
6646
  }
@@ -6673,7 +6680,13 @@ var HostConnection = class {
6673
6680
  const spec = params;
6674
6681
  if (this.draining || this.stopping) return fail(`${this.o.config.name} is restarting${this.draining ? " to update itself" : ""} \u2014 try again in a moment`);
6675
6682
  if (this.sessions.size >= this.o.config.maxSessions && !this.sessions.has(sid)) {
6676
- return fail(`${this.o.config.name} is busy (${this.sessions.size} of ${this.o.config.maxSessions} sessions)`);
6683
+ const before = this.sessions.size;
6684
+ this.reconcile("desk full");
6685
+ await sleep(RECONCILE_WAIT_MS);
6686
+ log.event("session.reconciled", { session: sid, before, after: this.sessions.size, max: this.o.config.maxSessions });
6687
+ if (this.sessions.size >= this.o.config.maxSessions) {
6688
+ return fail(`${this.o.config.name} is busy (${this.sessions.size} of ${this.o.config.maxSessions} sessions)`);
6689
+ }
6677
6690
  }
6678
6691
  this.sessions.add(sid);
6679
6692
  this.sessionsChanged();
@@ -6774,7 +6787,10 @@ var HostConnection = class {
6774
6787
  return;
6775
6788
  }
6776
6789
  if (method === "session/stop") {
6777
- await this.adapterOf(sid).stop(sid);
6790
+ const reason = typeof params.reason === "string" ? params.reason : "stopped";
6791
+ if (reason === "unknown_session") log.event("session.forgotten_by_pod", { session: sid });
6792
+ await this.adapterOf(sid).stop(sid, reason);
6793
+ this.forget(sid);
6778
6794
  return;
6779
6795
  }
6780
6796
  if (method === "session/permission_decided") {
@@ -6869,15 +6885,50 @@ var HostConnection = class {
6869
6885
  }
6870
6886
  async endAllSessions(reason) {
6871
6887
  if (this.sessions.size === 0) return;
6872
- log.warn(`Stopping ${this.sessions.size} session(s): ${reason}`);
6873
- for (const sid of [...this.sessions]) {
6874
- await this.adapterOf(sid).stop(sid, "host_disconnected").catch(() => {
6875
- });
6876
- this.sessions.delete(sid);
6877
- this.checkouts.delete(sid);
6888
+ const sids = [...this.sessions];
6889
+ log.warn(`Stopping ${sids.length} session(s): ${reason}`);
6890
+ for (const sid of sids) this.forget(sid, { keepAdapter: true });
6891
+ await Promise.all(sids.map(async (sid) => {
6892
+ const adapter = this.sessionAdapter.get(sid) ?? this.adapterOf(sid);
6878
6893
  this.sessionAdapter.delete(sid);
6894
+ await Promise.race([
6895
+ adapter.stop(sid, "host_disconnected").catch(() => {
6896
+ }),
6897
+ sleep(STOP_BOUND_MS).then(() => log.warn(`[${sid.slice(0, 8)}] did not stop within ${STOP_BOUND_MS / 1e3} s \u2014 slot freed anyway`))
6898
+ ]);
6899
+ }));
6900
+ }
6901
+ /** Free a session's slot and everything this computer remembered about it. Idempotent. */
6902
+ forget(sid, opts = {}) {
6903
+ const had = this.sessions.delete(sid);
6904
+ this.checkouts.delete(sid);
6905
+ this.doorCheckouts.forget(sid);
6906
+ this.sessionDirs.delete(sid);
6907
+ if (!opts.keepAdapter) this.sessionAdapter.delete(sid);
6908
+ if (had) this.sessionsChanged();
6909
+ }
6910
+ /**
6911
+ * No tails: ask the pod about every lane this computer holds. A known
6912
+ * session gets silence; a forgotten one gets `session/stop` back, which
6913
+ * frees its slot through the ordinary path. Sent only while connected.
6914
+ */
6915
+ reconcile(why) {
6916
+ if (this.sessions.size === 0 || !this.ws || this.ws.readyState !== wrapper_default.OPEN) return;
6917
+ log.event("sessions.reconcile", { why, held: this.sessions.size, max: this.o.config.maxSessions });
6918
+ for (const sid of this.sessions) {
6919
+ this.send({ sid, msg: { jsonrpc: "2.0", method: "session/heartbeat", params: { sessionId: sid } } });
6920
+ }
6921
+ }
6922
+ startReconciling() {
6923
+ this.stopReconciling();
6924
+ this.reconcileTimer = setInterval(() => this.reconcile("interval"), RECONCILE_INTERVAL_MS);
6925
+ this.reconcileTimer.unref?.();
6926
+ }
6927
+ stopReconciling() {
6928
+ if (this.reconcileTimer) {
6929
+ clearInterval(this.reconcileTimer);
6930
+ this.reconcileTimer = null;
6879
6931
  }
6880
- this.sessionsChanged();
6881
6932
  }
6882
6933
  send(frame) {
6883
6934
  const ws = this.ws;
@@ -7779,7 +7830,7 @@ function samplesInWords(samples) {
7779
7830
  }
7780
7831
 
7781
7832
  // src/cli.ts
7782
- var VERSION = "0.7.2";
7833
+ var VERSION = "0.7.3";
7783
7834
  function arg(flag, argv) {
7784
7835
  const i = argv.indexOf(flag);
7785
7836
  return i >= 0 ? argv[i + 1] : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frod.io/bridge",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
5
  "description": "Your computer is a desk: hosts a Frod.io agent's sessions with your own Claude Code, Codex or Grok Build. Installs and updates itself.",
6
6
  "license": "UNLICENSED",