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.
- package/assets/dsh-interconnect.md +8 -3
- package/dsh.plugin.json +1 -1
- package/lib/index.js +67 -98
- package/lib/interconnect/index.js +67 -98
- package/lib/interconnect/index.js.map +2 -2
- package/lib/tool-interconnect/index.js +69 -100
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +33 -21
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +13 -18
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +5 -1
|
@@ -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
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
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.
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
1252
|
-
if (
|
|
1253
|
-
|
|
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
|
-
|
|
1597
|
-
dialEpoch = 0;
|
|
1600
|
+
dialed;
|
|
1598
1601
|
closed = false;
|
|
1599
1602
|
retry = 0;
|
|
1600
1603
|
reconnectTimer;
|
|
1601
1604
|
warnedNoToken = false;
|
|
1602
|
-
/**
|
|
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
|
|
1624
|
+
if (this.closed) return;
|
|
1644
1625
|
this.scheduleReconnect();
|
|
1645
1626
|
return;
|
|
1646
1627
|
}
|
|
1647
|
-
if (this.closed
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
1689
|
-
if (
|
|
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
|
|
1697
|
-
if (
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
|
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",
|