dsh-interconnect 0.11.12 → 0.11.14

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.
@@ -5,6 +5,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
5
5
  import { Service } from "@deepseek-ai/cordis";
6
6
  import { createHash, randomUUID, timingSafeEqual as constantTimeEqual } from "node:crypto";
7
7
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
8
+ import { remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
8
9
  import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
9
10
 
10
11
  // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
@@ -948,6 +949,8 @@ var InterconnectService = class extends Service {
948
949
  heartbeatTimer;
949
950
  /** Peer identity each live socket announced via its `hello` frame, if any. */
950
951
  peerOf = /* @__PURE__ */ new WeakMap();
952
+ /** Configured peer an outbound dial owns a socket for; absent on inbound sockets. */
953
+ dialedPeerOf = /* @__PURE__ */ new WeakMap();
951
954
  /** Sender each local session last received a send from, keyed by local session id. */
952
955
  senders = /* @__PURE__ */ new Map();
953
956
  /** In-flight frames sent over a peer link, keyed by `reqId`, awaiting a correlated result. */
@@ -1179,50 +1182,16 @@ var InterconnectService = class extends Service {
1179
1182
  });
1180
1183
  }
1181
1184
  /**
1182
- * Add a peer route at runtime. Returns a disposer that removes it. Re-adding
1183
- * an existing instanceId re-routes it to the new origin.
1184
- * @param instanceId - the peer's `instanceId`.
1185
- * @param origin - origin this instance dials to reach that peer.
1186
- * @returns disposer removing the peer route.
1187
- */
1188
- subscribe(instanceId, origin) {
1189
- const linked = this.link(instanceId, origin);
1190
- const dialedOrigin = trimBase(origin);
1191
- return () => {
1192
- const state = this.linkStates.get(instanceId);
1193
- if (state === linked && state.peer === dialedOrigin) {
1194
- state.close();
1195
- this.linkStates.delete(instanceId);
1196
- }
1197
- };
1198
- }
1199
- /**
1200
- * Remove a peer route, closing its outbound link.
1201
- * @param instanceId - the peer route to remove.
1202
- */
1203
- unsubscribe(instanceId) {
1204
- const state = this.linkStates.get(instanceId);
1205
- if (state !== void 0) state.close();
1206
- this.linkStates.delete(instanceId);
1207
- }
1208
- /**
1209
- * Open (and, on drop, re-open) a persistent WebSocket link to a peer. Local
1210
- * events stream over the link in real time, and events the peer pushes are
1211
- * surfaced as `interconnect/event`. Repeating for the same instanceId
1212
- * re-routes the link to the new origin.
1185
+ * Dial one configured peer route. Activation calls this once per
1186
+ * `Config.peers` entry; the resulting state owns reconnection for the life of
1187
+ * the service fiber.
1213
1188
  * @param instanceId - the peer's `instanceId`.
1214
1189
  * @param origin - receiver origin this instance dials, e.g. `http://127.0.0.1:13080`.
1215
- * @returns a handle closing the link and cancelling reconnection.
1216
1190
  */
1217
1191
  link(instanceId, origin) {
1218
- const existing = this.linkStates.get(instanceId);
1219
- if (existing !== void 0 && !existing.isClosed()) {
1220
- existing.reroute(trimBase(origin));
1221
- return existing;
1222
- }
1223
1192
  const state = new LinkState(
1224
1193
  (socket) => {
1225
- this.attachSocket(socket);
1194
+ this.attachDialedSocket(socket, instanceId);
1226
1195
  },
1227
1196
  this.ctx.logger,
1228
1197
  () => this.resolveToken(),
@@ -1231,26 +1200,47 @@ var InterconnectService = class extends Service {
1231
1200
  );
1232
1201
  this.linkStates.set(instanceId, state);
1233
1202
  state.dial();
1234
- return state;
1235
1203
  }
1236
1204
  /** Push one serialized lifecycle fact out to every linked peer over WS. */
1237
1205
  fanout(notification) {
1238
1206
  this.broadcast(notification);
1239
1207
  }
1240
- /** Push one fact over every live WebSocket link, dropping closed sockets. */
1208
+ /**
1209
+ * Push one fact over every live WebSocket link, dropping closed sockets.
1210
+ *
1211
+ * A bidirectional pair owns two sockets to the same peer (this instance's
1212
+ * dialed link plus the peer's inbound one), so one event must leave over one
1213
+ * of them. Which one is decided by local ownership, not by the peer: a dialed
1214
+ * socket is a configured peer's link and always sends. An inbound socket is
1215
+ * skipped only when a dialed link to the id that socket itself announced is
1216
+ * open, so an announcement can suppress nothing but a duplicate the
1217
+ * controlled link already carries — an impostor cannot drop an event for a
1218
+ * peer, and without a dialed link every inbound socket receives it.
1219
+ *
1220
+ * That holds once a peer's announcement has been seen. Between this
1221
+ * instance's dialed link opening and the peer's `hello` arriving on its
1222
+ * inbound socket, the inbound socket is not yet attributable, so an event
1223
+ * emitted in that window leaves over both sockets and reaches that peer
1224
+ * twice. The duplicate is transient and never a dropped event.
1225
+ */
1241
1226
  broadcast(notification) {
1242
1227
  if (this.sockets.size === 0) return;
1243
1228
  const frame = { type: "event", notification };
1244
1229
  const encoded = JSON.stringify(frame);
1245
- const sent = /* @__PURE__ */ new Set();
1230
+ const covered = /* @__PURE__ */ new Set();
1246
1231
  for (const socket of this.sockets) {
1247
1232
  if (socket.readyState !== WebSocket.OPEN) {
1248
1233
  this.sockets.delete(socket);
1249
1234
  continue;
1250
1235
  }
1251
- const peer = this.peerOf.get(socket);
1252
- if (peer !== void 0 && sent.has(peer)) continue;
1253
- if (peer !== void 0) sent.add(peer);
1236
+ const dialedPeer = this.dialedPeerOf.get(socket);
1237
+ if (dialedPeer !== void 0) covered.add(dialedPeer);
1238
+ }
1239
+ for (const socket of this.sockets) {
1240
+ if (this.dialedPeerOf.get(socket) === void 0) {
1241
+ const announced = this.peerOf.get(socket);
1242
+ if (announced !== void 0 && covered.has(announced)) continue;
1243
+ }
1254
1244
  socket.send(encoded);
1255
1245
  }
1256
1246
  }
@@ -1381,6 +1371,9 @@ var InterconnectService = class extends Service {
1381
1371
  this.ctx.logger.info(
1382
1372
  `interconnect: resume refused for ${payload.sessionId}: ${error instanceof Error ? error.message : String(error)}`
1383
1373
  );
1374
+ if (remoteErrorOf(error)?.code === "session/agent-busy") {
1375
+ return { reason: "session-owned-by-subagent" };
1376
+ }
1384
1377
  return { reason: "resume-failed" };
1385
1378
  }
1386
1379
  }
@@ -1487,6 +1480,17 @@ var InterconnectService = class extends Service {
1487
1480
  this.attachSocket(websocket);
1488
1481
  });
1489
1482
  }
1483
+ /**
1484
+ * Install frame + liveness handling on one outbound socket the dial owns for
1485
+ * a configured peer, recording that local ownership before anything else can
1486
+ * observe the socket.
1487
+ * @param websocket - the dialed socket.
1488
+ * @param peerInstanceId - the configured peer this instance dials.
1489
+ */
1490
+ attachDialedSocket(websocket, peerInstanceId) {
1491
+ this.dialedPeerOf.set(websocket, peerInstanceId);
1492
+ this.attachSocket(websocket);
1493
+ }
1490
1494
  /**
1491
1495
  * Install frame + liveness handling on one socket and add it to the live
1492
1496
  * pool. Used by both the server half (accepted upgrade) and the client half
@@ -1593,40 +1597,17 @@ var LinkState = class {
1593
1597
  this.instanceId = instanceId;
1594
1598
  this.peer = origin;
1595
1599
  }
1596
- socket;
1597
- dialEpoch = 0;
1600
+ dialed;
1598
1601
  closed = false;
1599
1602
  retry = 0;
1600
1603
  reconnectTimer;
1601
1604
  warnedNoToken = false;
1602
- /** Dial-registered handlers, removed by reference on close/reroute so the pool-cleanup handlers attachSocket added stay alive. */
1603
- dialListeners;
1604
- // `peer` is mutable so `reroute` can point the link at a new origin.
1605
+ /** Receiver origin this link dials. */
1605
1606
  peer;
1606
1607
  instanceId;
1607
- /** Point this link at a different origin; re-dials immediately. */
1608
- reroute(origin) {
1609
- if (origin === this.peer) return;
1610
- this.peer = origin;
1611
- this.dialEpoch += 1;
1612
- if (this.reconnectTimer !== void 0) {
1613
- clearTimeout(this.reconnectTimer);
1614
- this.reconnectTimer = void 0;
1615
- }
1616
- const socket = this.socket;
1617
- if (socket !== void 0) {
1618
- this.removeDialListeners(socket);
1619
- socket.on("error", () => {
1620
- });
1621
- socket.terminate();
1622
- this.socket = void 0;
1623
- }
1624
- this.dial();
1625
- }
1626
1608
  /** Open the socket; reconnect is scheduled by the close handler. */
1627
1609
  dial() {
1628
1610
  if (this.closed) return;
1629
- const epoch = this.dialEpoch;
1630
1611
  let url;
1631
1612
  try {
1632
1613
  url = linkUrl(this.peer);
@@ -1640,11 +1621,11 @@ var LinkState = class {
1640
1621
  this.warnedNoToken = true;
1641
1622
  this.logger.warn(`interconnect: no shared token configured; peer ${this.instanceId} link stays down, retrying until one is set`);
1642
1623
  }
1643
- if (this.closed || epoch !== this.dialEpoch) return;
1624
+ if (this.closed) return;
1644
1625
  this.scheduleReconnect();
1645
1626
  return;
1646
1627
  }
1647
- if (this.closed || epoch !== this.dialEpoch) return;
1628
+ if (this.closed) return;
1648
1629
  const socket = new WebSocket(url, {
1649
1630
  headers: { authorization: `Bearer ${token}` },
1650
1631
  maxPayload: MAX_LINK_FRAME_BYTES,
@@ -1655,7 +1636,6 @@ var LinkState = class {
1655
1636
  // a reconnect — and ws owns the timer, so nothing leaks on teardown.
1656
1637
  handshakeTimeout: LINK_HANDSHAKE_TIMEOUT_MS
1657
1638
  });
1658
- this.socket = socket;
1659
1639
  const onOpen = () => {
1660
1640
  this.retry = 0;
1661
1641
  this.attachSocket(socket);
@@ -1666,49 +1646,38 @@ var LinkState = class {
1666
1646
  };
1667
1647
  const onError = () => {
1668
1648
  };
1669
- this.dialListeners = { open: onOpen, close: onClose, error: onError };
1649
+ this.dialed = { socket, open: onOpen, close: onClose, error: onError };
1670
1650
  socket.once("open", onOpen);
1671
1651
  socket.once("close", onClose);
1672
1652
  socket.on("error", onError);
1673
1653
  }).catch(() => {
1674
- if (this.closed || epoch !== this.dialEpoch) return;
1654
+ if (this.closed) return;
1675
1655
  this.scheduleReconnect();
1676
1656
  });
1677
1657
  }
1678
- /** Whether this link has been closed; a closed state is never reused by `link()`. */
1679
- isClosed() {
1680
- return this.closed;
1681
- }
1682
1658
  /** Whether this link currently holds an open socket that can carry frames. */
1683
1659
  writable() {
1684
- return this.socket !== void 0 && this.socket.readyState === WebSocket.OPEN;
1660
+ const dialed = this.dialed;
1661
+ return dialed !== void 0 && dialed.socket.readyState === WebSocket.OPEN;
1685
1662
  }
1686
1663
  /** Write one frame over this link's outbound socket; false when not open. */
1687
1664
  sendFrame(frame) {
1688
- const socket = this.socket;
1689
- if (socket === void 0 || socket.readyState !== WebSocket.OPEN) return false;
1690
- socket.send(JSON.stringify(frame));
1665
+ const dialed = this.dialed;
1666
+ if (dialed === void 0 || dialed.socket.readyState !== WebSocket.OPEN) return false;
1667
+ dialed.socket.send(JSON.stringify(frame));
1691
1668
  return true;
1692
1669
  }
1693
1670
  close() {
1694
1671
  this.closed = true;
1695
1672
  if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
1696
- const socket = this.socket;
1697
- if (socket !== void 0) {
1698
- this.removeDialListeners(socket);
1699
- socket.on("error", () => {
1700
- });
1701
- socket.terminate();
1702
- }
1703
- }
1704
- /** Detach the dial-registered handlers from a socket before terminating it. */
1705
- removeDialListeners(socket) {
1706
- const listeners = this.dialListeners;
1707
- if (listeners === void 0) return;
1708
- socket.removeListener("open", listeners.open);
1709
- socket.removeListener("close", listeners.close);
1710
- socket.removeListener("error", listeners.error);
1711
- this.dialListeners = void 0;
1673
+ const dialed = this.dialed;
1674
+ if (dialed === void 0) return;
1675
+ dialed.socket.removeListener("open", dialed.open);
1676
+ dialed.socket.removeListener("close", dialed.close);
1677
+ dialed.socket.removeListener("error", dialed.error);
1678
+ dialed.socket.on("error", () => {
1679
+ });
1680
+ dialed.socket.terminate();
1712
1681
  }
1713
1682
  scheduleReconnect() {
1714
1683
  const delay = Math.min(3e4, 1e3 * 2 ** this.retry);
@@ -1822,7 +1791,7 @@ function apply(ctx) {
1822
1791
  case "resume-refused":
1823
1792
  return `not delivered: ${value.instance} does not allow waking persisted sessions`;
1824
1793
  case "resume-failed":
1825
- return `not delivered: could not wake "${_args.sessionId}" on ${value.instance} (no such persisted session, or another owner holds it)`;
1794
+ return `not delivered: could not wake "${_args.sessionId}" on ${value.instance} (no persisted session under that id, or the wake itself failed)`;
1826
1795
  case "session-owned-by-subagent":
1827
1796
  return `not delivered: "${_args.sessionId}" is a subagent's session on ${value.instance} \u2014 its parent agent owns delivery, so reach it through that parent`;
1828
1797
  default:
@@ -1945,7 +1914,7 @@ function apply(ctx) {
1945
1914
  }));
1946
1915
  ctx.tools.register(defineTool({
1947
1916
  name: "interconnect_reply",
1948
- description: `Deliver one text message back to the peer that this session last received an interconnect message from. Only the reply text is needed: the replying session is this agent's own, and the outbound target (the sending instance id and session id) is recalled from the message this session received, so you do not ask for an address twice. A session that never received a message through interconnect \u2014 or received one without a sender identity \u2014 reports reason "no-sender-known".`,
1917
+ description: `Deliver one text message back to the peer that this session last received an interconnect message from. Only the reply text is needed: the replying session is this agent's own, and the outbound target (the sending instance id and session id) is recalled from the message this session received, so you do not ask for an address twice. The recalled target is the most recent sender only: a later message from another peer replaces it, and restarting this instance forgets it until a new message arrives, so name the intended recipient in the reply text when more than one peer may message this session. A session that never received a message through interconnect \u2014 or received one without a sender identity \u2014 reports reason "no-sender-known".`,
1949
1918
  parameters: {
1950
1919
  text: {
1951
1920
  type: "string",