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.
- package/assets/dsh-interconnect.md +10 -5
- package/dsh.plugin.json +1 -1
- package/lib/index.js +61 -98
- package/lib/interconnect/index.js +61 -98
- package/lib/interconnect/index.js.map +2 -2
- package/lib/tool-interconnect/index.js +62 -99
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +27 -21
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +8 -16
- 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,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
|
-
/**
|
|
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
|
|
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
|
|
1252
|
-
if (
|
|
1253
|
-
|
|
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
|
-
|
|
1597
|
-
dialEpoch = 0;
|
|
1594
|
+
dialed;
|
|
1598
1595
|
closed = false;
|
|
1599
1596
|
retry = 0;
|
|
1600
1597
|
reconnectTimer;
|
|
1601
1598
|
warnedNoToken = false;
|
|
1602
|
-
/**
|
|
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
|
|
1618
|
+
if (this.closed) return;
|
|
1644
1619
|
this.scheduleReconnect();
|
|
1645
1620
|
return;
|
|
1646
1621
|
}
|
|
1647
|
-
if (this.closed
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
1689
|
-
if (
|
|
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
|
|
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;
|
|
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",
|