dsh-interconnect 0.11.11 → 0.11.13

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,41 @@ 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
+ */
1241
1220
  broadcast(notification) {
1242
1221
  if (this.sockets.size === 0) return;
1243
1222
  const frame = { type: "event", notification };
1244
1223
  const encoded = JSON.stringify(frame);
1245
- const sent = /* @__PURE__ */ new Set();
1224
+ const covered = /* @__PURE__ */ new Set();
1246
1225
  for (const socket of this.sockets) {
1247
1226
  if (socket.readyState !== WebSocket.OPEN) {
1248
1227
  this.sockets.delete(socket);
1249
1228
  continue;
1250
1229
  }
1251
- const peer = this.peerOf.get(socket);
1252
- if (peer !== void 0 && sent.has(peer)) continue;
1253
- if (peer !== void 0) sent.add(peer);
1230
+ const dialedPeer = this.dialedPeerOf.get(socket);
1231
+ if (dialedPeer !== void 0) covered.add(dialedPeer);
1232
+ }
1233
+ for (const socket of this.sockets) {
1234
+ if (this.dialedPeerOf.get(socket) === void 0) {
1235
+ const announced = this.peerOf.get(socket);
1236
+ if (announced !== void 0 && covered.has(announced)) continue;
1237
+ }
1254
1238
  socket.send(encoded);
1255
1239
  }
1256
1240
  }
@@ -1381,6 +1365,9 @@ var InterconnectService = class extends Service {
1381
1365
  this.ctx.logger.info(
1382
1366
  `interconnect: resume refused for ${payload.sessionId}: ${error instanceof Error ? error.message : String(error)}`
1383
1367
  );
1368
+ if (remoteErrorOf(error)?.code === "session/agent-busy") {
1369
+ return { reason: "session-owned-by-subagent" };
1370
+ }
1384
1371
  return { reason: "resume-failed" };
1385
1372
  }
1386
1373
  }
@@ -1487,6 +1474,17 @@ var InterconnectService = class extends Service {
1487
1474
  this.attachSocket(websocket);
1488
1475
  });
1489
1476
  }
1477
+ /**
1478
+ * Install frame + liveness handling on one outbound socket the dial owns for
1479
+ * a configured peer, recording that local ownership before anything else can
1480
+ * observe the socket.
1481
+ * @param websocket - the dialed socket.
1482
+ * @param peerInstanceId - the configured peer this instance dials.
1483
+ */
1484
+ attachDialedSocket(websocket, peerInstanceId) {
1485
+ this.dialedPeerOf.set(websocket, peerInstanceId);
1486
+ this.attachSocket(websocket);
1487
+ }
1490
1488
  /**
1491
1489
  * Install frame + liveness handling on one socket and add it to the live
1492
1490
  * pool. Used by both the server half (accepted upgrade) and the client half
@@ -1593,40 +1591,17 @@ var LinkState = class {
1593
1591
  this.instanceId = instanceId;
1594
1592
  this.peer = origin;
1595
1593
  }
1596
- socket;
1597
- dialEpoch = 0;
1594
+ dialed;
1598
1595
  closed = false;
1599
1596
  retry = 0;
1600
1597
  reconnectTimer;
1601
1598
  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.
1599
+ /** Receiver origin this link dials. */
1605
1600
  peer;
1606
1601
  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
1602
  /** Open the socket; reconnect is scheduled by the close handler. */
1627
1603
  dial() {
1628
1604
  if (this.closed) return;
1629
- const epoch = this.dialEpoch;
1630
1605
  let url;
1631
1606
  try {
1632
1607
  url = linkUrl(this.peer);
@@ -1640,11 +1615,11 @@ var LinkState = class {
1640
1615
  this.warnedNoToken = true;
1641
1616
  this.logger.warn(`interconnect: no shared token configured; peer ${this.instanceId} link stays down, retrying until one is set`);
1642
1617
  }
1643
- if (this.closed || epoch !== this.dialEpoch) return;
1618
+ if (this.closed) return;
1644
1619
  this.scheduleReconnect();
1645
1620
  return;
1646
1621
  }
1647
- if (this.closed || epoch !== this.dialEpoch) return;
1622
+ if (this.closed) return;
1648
1623
  const socket = new WebSocket(url, {
1649
1624
  headers: { authorization: `Bearer ${token}` },
1650
1625
  maxPayload: MAX_LINK_FRAME_BYTES,
@@ -1655,7 +1630,6 @@ var LinkState = class {
1655
1630
  // a reconnect — and ws owns the timer, so nothing leaks on teardown.
1656
1631
  handshakeTimeout: LINK_HANDSHAKE_TIMEOUT_MS
1657
1632
  });
1658
- this.socket = socket;
1659
1633
  const onOpen = () => {
1660
1634
  this.retry = 0;
1661
1635
  this.attachSocket(socket);
@@ -1666,49 +1640,38 @@ var LinkState = class {
1666
1640
  };
1667
1641
  const onError = () => {
1668
1642
  };
1669
- this.dialListeners = { open: onOpen, close: onClose, error: onError };
1643
+ this.dialed = { socket, open: onOpen, close: onClose, error: onError };
1670
1644
  socket.once("open", onOpen);
1671
1645
  socket.once("close", onClose);
1672
1646
  socket.on("error", onError);
1673
1647
  }).catch(() => {
1674
- if (this.closed || epoch !== this.dialEpoch) return;
1648
+ if (this.closed) return;
1675
1649
  this.scheduleReconnect();
1676
1650
  });
1677
1651
  }
1678
- /** Whether this link has been closed; a closed state is never reused by `link()`. */
1679
- isClosed() {
1680
- return this.closed;
1681
- }
1682
1652
  /** Whether this link currently holds an open socket that can carry frames. */
1683
1653
  writable() {
1684
- return this.socket !== void 0 && this.socket.readyState === WebSocket.OPEN;
1654
+ const dialed = this.dialed;
1655
+ return dialed !== void 0 && dialed.socket.readyState === WebSocket.OPEN;
1685
1656
  }
1686
1657
  /** Write one frame over this link's outbound socket; false when not open. */
1687
1658
  sendFrame(frame) {
1688
- const socket = this.socket;
1689
- if (socket === void 0 || socket.readyState !== WebSocket.OPEN) return false;
1690
- socket.send(JSON.stringify(frame));
1659
+ const dialed = this.dialed;
1660
+ if (dialed === void 0 || dialed.socket.readyState !== WebSocket.OPEN) return false;
1661
+ dialed.socket.send(JSON.stringify(frame));
1691
1662
  return true;
1692
1663
  }
1693
1664
  close() {
1694
1665
  this.closed = true;
1695
1666
  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;
1667
+ const dialed = this.dialed;
1668
+ if (dialed === void 0) return;
1669
+ dialed.socket.removeListener("open", dialed.open);
1670
+ dialed.socket.removeListener("close", dialed.close);
1671
+ dialed.socket.removeListener("error", dialed.error);
1672
+ dialed.socket.on("error", () => {
1673
+ });
1674
+ dialed.socket.terminate();
1712
1675
  }
1713
1676
  scheduleReconnect() {
1714
1677
  const delay = Math.min(3e4, 1e3 * 2 ** this.retry);
@@ -1945,7 +1908,7 @@ function apply(ctx) {
1945
1908
  }));
1946
1909
  ctx.tools.register(defineTool({
1947
1910
  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".`,
1911
+ 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
1912
  parameters: {
1950
1913
  text: {
1951
1914
  type: "string",