dsh-interconnect 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -823,12 +823,8 @@ defineMethod("transform", [
823
823
 
824
824
  // src/interconnect/index.ts
825
825
  import WebSocket, { WebSocketServer } from "ws";
826
- function isSessionOwnedBySubagent(ctx, session, agent) {
827
- if (session.header.origin === "subagent") return true;
828
- const parentId = session.header.parentSession;
829
- if (parentId === void 0 || agent === void 0) return false;
830
- const parent = ctx.agents.get(parentId);
831
- return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
826
+ function isSessionOwnedBySubagent(session) {
827
+ return session.header.origin === "subagent";
832
828
  }
833
829
  function assertNever(value) {
834
830
  throw new Error(`interconnect: unhandled variant ${JSON.stringify(value)}`);
@@ -839,6 +835,7 @@ var DEFAULT_INSTANCE_ID = "dsh";
839
835
  var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
840
836
  var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
841
837
  var LINK_CHANNEL = "/interconnect/link";
838
+ var SELF_PEER_ORIGIN = "self";
842
839
  var MAX_INSTANCE_ID_CHARS = 256;
843
840
  var MAX_REQUEST_ID_CHARS = 256;
844
841
  var MAX_LISTED_SESSIONS = 100;
@@ -966,11 +963,19 @@ var InterconnectService = class extends Service {
966
963
  this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
967
964
  this.delivery = config.delivery ?? "followup";
968
965
  this.allowResume = config.allowResume ?? true;
969
- for (const origin of Object.values(config.peers ?? {})) {
966
+ for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
967
+ if (origin === SELF_PEER_ORIGIN) {
968
+ if (peerInstanceId !== this.instanceId) {
969
+ throw new Error(
970
+ `interconnect: peer "${peerInstanceId}" is configured as self, but this instance announces "${this.instanceId}"; key a self peer by the instanceId`
971
+ );
972
+ }
973
+ continue;
974
+ }
970
975
  linkUrl(trimBase(origin));
971
976
  }
972
977
  for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
973
- this.link(peerInstanceId, origin);
978
+ this.link(peerInstanceId, origin === SELF_PEER_ORIGIN ? () => this.selfOrigin() : origin);
974
979
  }
975
980
  const upgrade = {
976
981
  path: LINK_CHANNEL,
@@ -1213,7 +1218,8 @@ var InterconnectService = class extends Service {
1213
1218
  * `Config.peers` entry; the resulting state owns reconnection for the life of
1214
1219
  * the service fiber.
1215
1220
  * @param instanceId - the peer's `instanceId`.
1216
- * @param origin - receiver origin this instance dials, e.g. `http://127.0.0.1:13080`.
1221
+ * @param origin - receiver origin this instance dials (e.g.
1222
+ * `http://127.0.0.1:13080`), or a resolver for a `self` route.
1217
1223
  */
1218
1224
  link(instanceId, origin) {
1219
1225
  const state = new LinkState(
@@ -1223,11 +1229,28 @@ var InterconnectService = class extends Service {
1223
1229
  this.ctx.logger,
1224
1230
  () => this.resolveToken(),
1225
1231
  instanceId,
1226
- trimBase(origin)
1232
+ typeof origin === "string" ? trimBase(origin) : origin
1227
1233
  );
1228
1234
  this.linkStates.set(instanceId, state);
1229
1235
  state.dial();
1230
1236
  }
1237
+ /**
1238
+ * The origin of this instance's own host webserver, or undefined while no
1239
+ * listening port exists. Read on every dial rather than cached: the
1240
+ * webserver may activate after this service, its port is assigned only in
1241
+ * its listen callback (`--port 0` picks a fresh one per start), and a
1242
+ * replacement webserver would move it again. Loopback is used regardless of
1243
+ * the bind host: a server bound to `0.0.0.0` also accepts `127.0.0.1`, while
1244
+ * `0.0.0.0` itself is a bind address, not a dialable origin.
1245
+ * @returns `http://127.0.0.1:<port>`, or undefined without a listening server.
1246
+ */
1247
+ selfOrigin() {
1248
+ const server = this.ctx.get("webServer");
1249
+ if (server === void 0) return void 0;
1250
+ const port = server.port;
1251
+ if (typeof port !== "number" || port <= 0) return void 0;
1252
+ return `http://127.0.0.1:${String(port)}`;
1253
+ }
1231
1254
  /** Push one serialized lifecycle fact out to every linked peer over WS. */
1232
1255
  fanout(notification) {
1233
1256
  this.broadcast(notification);
@@ -1362,7 +1385,7 @@ var InterconnectService = class extends Service {
1362
1385
  * @returns the bounded listing.
1363
1386
  */
1364
1387
  listSessions(rowBudgetBytes) {
1365
- const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
1388
+ const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(agent.session));
1366
1389
  const sessions = [];
1367
1390
  let used = 0;
1368
1391
  for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
@@ -1389,7 +1412,8 @@ var InterconnectService = class extends Service {
1389
1412
  let title;
1390
1413
  try {
1391
1414
  const snapshot = this.ctx.get("sessionProjections")?.snapshot(agent.session);
1392
- const value = snapshot?.values.title;
1415
+ const values = snapshot?.values;
1416
+ const value = values?.title;
1393
1417
  if (typeof value === "string" && value !== "") title = value;
1394
1418
  } catch {
1395
1419
  title = void 0;
@@ -1447,7 +1471,7 @@ var InterconnectService = class extends Service {
1447
1471
  }
1448
1472
  agent = woken.agent;
1449
1473
  }
1450
- if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
1474
+ if (isSessionOwnedBySubagent(agent.session)) {
1451
1475
  return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
1452
1476
  }
1453
1477
  const sender = (() => {
@@ -1656,24 +1680,40 @@ var LinkState = class {
1656
1680
  this.logger = logger;
1657
1681
  this.resolveToken = resolveToken;
1658
1682
  this.instanceId = instanceId;
1659
- this.peer = origin;
1683
+ this.origin = origin;
1660
1684
  }
1661
1685
  dialed;
1662
1686
  closed = false;
1663
1687
  retry = 0;
1664
1688
  reconnectTimer;
1665
1689
  warnedNoToken = false;
1666
- /** Receiver origin this link dials. */
1667
- peer;
1690
+ warnedNoOrigin = false;
1668
1691
  instanceId;
1692
+ origin;
1693
+ /** The origin this link dials now; for an unresolved `self` route, the sentinel is shown. */
1694
+ get peer() {
1695
+ return typeof this.origin === "string" ? this.origin : this.origin() ?? SELF_PEER_ORIGIN;
1696
+ }
1669
1697
  /** Open the socket; reconnect is scheduled by the close handler. */
1670
1698
  dial() {
1671
1699
  if (this.closed) return;
1700
+ const origin = typeof this.origin === "string" ? this.origin : this.origin();
1701
+ if (origin === void 0) {
1702
+ if (this.retry >= 1 && !this.warnedNoOrigin) {
1703
+ this.warnedNoOrigin = true;
1704
+ this.logger.warn(
1705
+ `interconnect: peer ${this.instanceId} is configured as self but this instance has no listening webServer port yet; the link stays down and retries`
1706
+ );
1707
+ }
1708
+ this.scheduleReconnect();
1709
+ return;
1710
+ }
1711
+ this.warnedNoOrigin = false;
1672
1712
  let url;
1673
1713
  try {
1674
- url = linkUrl(this.peer);
1714
+ url = linkUrl(origin);
1675
1715
  } catch {
1676
- this.logger.warn(`interconnect: invalid peer origin ${this.peer}; link to ${this.instanceId} stays down`);
1716
+ this.logger.warn(`interconnect: invalid peer origin ${origin}; link to ${this.instanceId} stays down`);
1677
1717
  return;
1678
1718
  }
1679
1719
  void this.resolveToken().then((token) => {