dsh-interconnect 0.6.4 → 0.9.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.
- package/README.md +79 -13
- package/lib/index.js +315 -253
- package/lib/interconnect/index.js +315 -253
- package/lib/interconnect/index.js.map +3 -3
- package/lib/tool-interconnect/index.js +93 -10
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +74 -28
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +137 -13
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
// src/interconnect/index.ts
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
-
import {
|
|
4
|
-
clientRequestSchema,
|
|
5
|
-
serverResponseSchema,
|
|
6
|
-
RpcId
|
|
7
|
-
} from "@deepseek-ai/dsh-host-apiproxy/api";
|
|
8
3
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
9
4
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
5
|
import { hasApiRemoteSubagentOwner } from "@deepseek-ai/dsh-api-remotes";
|
|
@@ -826,12 +821,9 @@ defineMethod("transform", [
|
|
|
826
821
|
import WebSocket, { WebSocketServer } from "ws";
|
|
827
822
|
|
|
828
823
|
// src/interconnect/types.ts
|
|
829
|
-
var INTERCONNECT_CHANNEL = "/interconnect";
|
|
830
824
|
var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
|
|
831
825
|
|
|
832
826
|
// src/interconnect/index.ts
|
|
833
|
-
var INVALID_REQUEST_RPC_ID = RpcId("invalid-request");
|
|
834
|
-
var MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
835
827
|
var PLUGIN_SOURCE = "dsh-interconnect";
|
|
836
828
|
var LINK_CHANNEL = "/interconnect/link";
|
|
837
829
|
var notificationSchema = Schema.union([
|
|
@@ -842,26 +834,49 @@ var notificationSchema = Schema.union([
|
|
|
842
834
|
Schema.object({ kind: Schema.const("session/disposed"), sessionId: Schema.string() }),
|
|
843
835
|
Schema.object({ kind: Schema.const("subagent/end"), provider: Schema.string(), childSessionId: Schema.string(), stopReason: Schema.string() })
|
|
844
836
|
]);
|
|
837
|
+
var senderSchema = Schema.object({ instanceId: Schema.string(), sessionId: Schema.string() });
|
|
838
|
+
var messageSchema = Schema.union([
|
|
839
|
+
Schema.object({
|
|
840
|
+
kind: Schema.const("send"),
|
|
841
|
+
sessionId: Schema.string(),
|
|
842
|
+
text: Schema.string(),
|
|
843
|
+
sender: senderSchema,
|
|
844
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
845
|
+
resume: Schema.boolean()
|
|
846
|
+
}),
|
|
847
|
+
Schema.object({
|
|
848
|
+
kind: Schema.const("reply"),
|
|
849
|
+
sessionId: Schema.string(),
|
|
850
|
+
text: Schema.string(),
|
|
851
|
+
sender: senderSchema,
|
|
852
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
853
|
+
resume: Schema.boolean()
|
|
854
|
+
})
|
|
855
|
+
]);
|
|
856
|
+
var querySchema = Schema.union([
|
|
857
|
+
Schema.object({ kind: Schema.const("ping") }),
|
|
858
|
+
Schema.object({ kind: Schema.const("list") }),
|
|
859
|
+
Schema.object({ kind: Schema.const("event"), notification: notificationSchema })
|
|
860
|
+
]);
|
|
845
861
|
var linkFrameSchema = Schema.union([
|
|
846
862
|
Schema.object({ type: Schema.const("hello"), sender: Schema.string() }),
|
|
847
|
-
Schema.object({ type: Schema.const("event"), notification: notificationSchema })
|
|
863
|
+
Schema.object({ type: Schema.const("event"), notification: notificationSchema }),
|
|
864
|
+
Schema.object({ type: Schema.const("msg"), reqId: Schema.string(), message: messageSchema }),
|
|
865
|
+
Schema.object({ type: Schema.const("msg-result"), reqId: Schema.string(), result: Schema.object({
|
|
866
|
+
delivered: Schema.boolean(),
|
|
867
|
+
instance: Schema.string(),
|
|
868
|
+
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
869
|
+
reason: Schema.string()
|
|
870
|
+
}) }),
|
|
871
|
+
Schema.object({ type: Schema.const("query"), reqId: Schema.string(), query: querySchema }),
|
|
872
|
+
Schema.object({ type: Schema.const("query-result"), reqId: Schema.string(), result: Schema.any() })
|
|
848
873
|
]);
|
|
849
|
-
var sendPayloadSchema = Schema.object({
|
|
850
|
-
sessionId: Schema.string(),
|
|
851
|
-
text: Schema.string(),
|
|
852
|
-
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
|
|
853
|
-
resume: Schema.boolean()
|
|
854
|
-
});
|
|
855
|
-
var eventPayloadSchema = Schema.object({
|
|
856
|
-
sender: Schema.string(),
|
|
857
|
-
notification: notificationSchema
|
|
858
|
-
});
|
|
859
874
|
var InterconnectService = class extends Service {
|
|
860
875
|
static inject = ["webServer", "agents", "credentials"];
|
|
861
876
|
static Config = Schema.object({
|
|
862
877
|
instanceId: Schema.string().default("dsh"),
|
|
863
878
|
requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
|
|
864
|
-
peers: Schema.
|
|
879
|
+
peers: Schema.object({}).default({}),
|
|
865
880
|
delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
|
|
866
881
|
allowResume: Schema.boolean().default(true)
|
|
867
882
|
});
|
|
@@ -869,27 +884,28 @@ var InterconnectService = class extends Service {
|
|
|
869
884
|
requestTimeoutMs;
|
|
870
885
|
delivery;
|
|
871
886
|
allowResume;
|
|
872
|
-
peers = /* @__PURE__ */ new Set();
|
|
873
887
|
subscriptions = [];
|
|
874
888
|
server = new WebSocketServer({ noServer: true });
|
|
875
889
|
sockets = /* @__PURE__ */ new Set();
|
|
890
|
+
/** Outbound peer links keyed by the peer's `instanceId`. */
|
|
876
891
|
linkStates = /* @__PURE__ */ new Map();
|
|
877
892
|
heartbeatTimer;
|
|
878
893
|
/** Peer identity each live socket announced via its `hello` frame, if any. */
|
|
879
894
|
peerOf = /* @__PURE__ */ new WeakMap();
|
|
895
|
+
/** Sender each local session last received a send from, keyed by local session id. */
|
|
896
|
+
senders = /* @__PURE__ */ new Map();
|
|
897
|
+
/** In-flight frames sent over a peer link, keyed by `reqId`, awaiting a correlated result. */
|
|
898
|
+
pendingMessages = /* @__PURE__ */ new Map();
|
|
899
|
+
reqIdCounter = 0;
|
|
880
900
|
constructor(ctx, config) {
|
|
881
901
|
super(ctx, "interconnect");
|
|
882
902
|
this.instanceId = config.instanceId;
|
|
883
903
|
this.requestTimeoutMs = config.requestTimeoutMs;
|
|
884
904
|
this.delivery = config.delivery ?? "followup";
|
|
885
905
|
this.allowResume = config.allowResume ?? true;
|
|
886
|
-
for (const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
path: INTERCONNECT_CHANNEL,
|
|
890
|
-
handler: (req, res) => this.handle(req, res)
|
|
891
|
-
};
|
|
892
|
-
ctx.effect(() => ctx.webServer.register(route), "interconnect: /interconnect route");
|
|
906
|
+
for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
|
|
907
|
+
this.link(peerInstanceId, origin);
|
|
908
|
+
}
|
|
893
909
|
const upgrade = {
|
|
894
910
|
path: LINK_CHANNEL,
|
|
895
911
|
handler: (req, socket, head) => {
|
|
@@ -950,72 +966,173 @@ var InterconnectService = class extends Service {
|
|
|
950
966
|
this.server.close();
|
|
951
967
|
}, "interconnect: websocket teardown");
|
|
952
968
|
}
|
|
969
|
+
/**
|
|
970
|
+
* This instance's identity to attach to outbound messages. Always present:
|
|
971
|
+
* it needs no address, only this instance's id and the calling session.
|
|
972
|
+
* @param sessionId - the local session that is sending, used as the reply target.
|
|
973
|
+
*/
|
|
974
|
+
selfSender(sessionId) {
|
|
975
|
+
return { instanceId: this.instanceId, sessionId };
|
|
976
|
+
}
|
|
953
977
|
async send(request) {
|
|
954
978
|
const payload = {
|
|
955
979
|
sessionId: request.sessionId,
|
|
956
980
|
text: request.text,
|
|
981
|
+
...request.sender === void 0 ? {} : { sender: request.sender },
|
|
957
982
|
...request.delivery === void 0 ? {} : { delivery: request.delivery },
|
|
958
983
|
...request.resume === void 0 ? {} : { resume: request.resume }
|
|
959
984
|
};
|
|
960
|
-
const result = await this.
|
|
985
|
+
const result = await this.msgRequest(request.instanceId, "send", payload);
|
|
986
|
+
return result ?? { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Deliver one text message back to the peer that a local session last
|
|
990
|
+
* received a send from. `sessionId` names the LOCAL replying session; the
|
|
991
|
+
* outbound target is the `sender` that session recorded, addressed through
|
|
992
|
+
* this instance's own link to the sender's instance.
|
|
993
|
+
*/
|
|
994
|
+
async reply(request) {
|
|
995
|
+
const sender = this.senders.get(request.sessionId);
|
|
996
|
+
if (sender === void 0) {
|
|
997
|
+
return { delivered: false, instance: this.instanceId, reason: "no-sender-known" };
|
|
998
|
+
}
|
|
999
|
+
const payload = {
|
|
1000
|
+
sessionId: sender.sessionId,
|
|
1001
|
+
text: request.text,
|
|
1002
|
+
...request.delivery === void 0 ? {} : { delivery: request.delivery },
|
|
1003
|
+
...request.resume === void 0 ? {} : { resume: request.resume },
|
|
1004
|
+
// Attribute the reply to this instance so the peer, in turn, can reply
|
|
1005
|
+
// back — chaining the conversation.
|
|
1006
|
+
sender: this.selfSender(request.sessionId)
|
|
1007
|
+
};
|
|
1008
|
+
const result = await this.msgRequest(sender.instanceId, "send", payload);
|
|
961
1009
|
return result ?? { delivered: false, instance: this.instanceId, reason: "unreachable" };
|
|
962
1010
|
}
|
|
963
|
-
|
|
964
|
-
|
|
1011
|
+
/**
|
|
1012
|
+
* Probe a peer instance for liveness and identity over its persistent link.
|
|
1013
|
+
* Returns the peer identity when reachable, or undefined when the link is
|
|
1014
|
+
* not up or no answer arrives.
|
|
1015
|
+
*/
|
|
1016
|
+
async ping(instanceId) {
|
|
1017
|
+
return this.queryRequest(instanceId, { kind: "ping" });
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* List a peer instance's live sessions over its persistent link. Undefined on
|
|
1021
|
+
* transport failure, matching `ping`.
|
|
1022
|
+
*/
|
|
1023
|
+
async list(instanceId) {
|
|
1024
|
+
return this.queryRequest(instanceId, { kind: "list" });
|
|
1025
|
+
}
|
|
1026
|
+
/** The origin this instance dials to reach a configured peer, or undefined. */
|
|
1027
|
+
originOf(instanceId) {
|
|
1028
|
+
const state = this.linkStates.get(instanceId);
|
|
1029
|
+
return state?.peer;
|
|
965
1030
|
}
|
|
966
1031
|
/**
|
|
967
|
-
*
|
|
968
|
-
*
|
|
1032
|
+
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1033
|
+
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
1034
|
+
* (not configured, or the link is down) — sends can only address configured+
|
|
1035
|
+
* connected peers by design.
|
|
969
1036
|
*/
|
|
970
|
-
async
|
|
971
|
-
|
|
1037
|
+
async msgRequest(instanceId, kind, payload) {
|
|
1038
|
+
const state = this.linkStates.get(instanceId);
|
|
1039
|
+
if (state === void 0 || !state.writable()) return void 0;
|
|
1040
|
+
const reqId = `m${++this.reqIdCounter}-${crypto.randomUUID()}`;
|
|
1041
|
+
const message = {
|
|
1042
|
+
kind,
|
|
1043
|
+
sessionId: payload.sessionId,
|
|
1044
|
+
text: payload.text,
|
|
1045
|
+
...payload.sender === void 0 ? {} : { sender: payload.sender },
|
|
1046
|
+
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1047
|
+
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1048
|
+
};
|
|
1049
|
+
return this.waitForResult(reqId, (failure) => {
|
|
1050
|
+
const wrote = state.sendFrame({ type: "msg", reqId, message });
|
|
1051
|
+
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
/** Send a `query` frame over the live link to a peer and resolve its result. */
|
|
1055
|
+
async queryRequest(instanceId, query) {
|
|
1056
|
+
const state = this.linkStates.get(instanceId);
|
|
1057
|
+
if (state === void 0 || !state.writable()) return void 0;
|
|
1058
|
+
const reqId = `q${++this.reqIdCounter}-${crypto.randomUUID()}`;
|
|
1059
|
+
return this.waitForResult(reqId, (failure) => {
|
|
1060
|
+
const wrote = state.sendFrame({ type: "query", reqId, query });
|
|
1061
|
+
if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while querying`));
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
/** Settle a `reqId` frame against its `*-result`, timing out at `requestTimeoutMs`. */
|
|
1065
|
+
waitForResult(reqId, send) {
|
|
1066
|
+
return new Promise((resolve2, reject) => {
|
|
1067
|
+
const timer = setTimeout(() => {
|
|
1068
|
+
this.pendingMessages.delete(reqId);
|
|
1069
|
+
reject(new Error(`interconnect: no result for ${reqId} within ${this.requestTimeoutMs}ms`));
|
|
1070
|
+
}, this.requestTimeoutMs);
|
|
1071
|
+
this.pendingMessages.set(reqId, { timer });
|
|
1072
|
+
const pending = this.pendingMessages.get(reqId);
|
|
1073
|
+
if (pending === void 0) return;
|
|
1074
|
+
pending.resolve = (result) => {
|
|
1075
|
+
clearTimeout(timer);
|
|
1076
|
+
this.pendingMessages.delete(reqId);
|
|
1077
|
+
resolve2(result);
|
|
1078
|
+
};
|
|
1079
|
+
pending.reject = (error) => {
|
|
1080
|
+
clearTimeout(timer);
|
|
1081
|
+
this.pendingMessages.delete(reqId);
|
|
1082
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1083
|
+
};
|
|
1084
|
+
send((error) => {
|
|
1085
|
+
clearTimeout(timer);
|
|
1086
|
+
this.pendingMessages.delete(reqId);
|
|
1087
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
972
1090
|
}
|
|
973
1091
|
/**
|
|
974
|
-
* Add a peer
|
|
975
|
-
*
|
|
976
|
-
* @param
|
|
977
|
-
* @
|
|
1092
|
+
* Add a peer route at runtime. Returns a disposer that removes it. Re-adding
|
|
1093
|
+
* an existing instanceId re-routes it to the new origin.
|
|
1094
|
+
* @param instanceId - the peer's `instanceId`.
|
|
1095
|
+
* @param origin - origin this instance dials to reach that peer.
|
|
1096
|
+
* @returns disposer removing the peer route.
|
|
978
1097
|
*/
|
|
979
|
-
subscribe(
|
|
980
|
-
|
|
981
|
-
this.peers.add(trimmed);
|
|
1098
|
+
subscribe(instanceId, origin) {
|
|
1099
|
+
this.link(instanceId, origin);
|
|
982
1100
|
return () => {
|
|
983
|
-
this.
|
|
1101
|
+
this.forgetLink(instanceId);
|
|
984
1102
|
};
|
|
985
1103
|
}
|
|
986
|
-
/** Remove a peer
|
|
987
|
-
unsubscribe(
|
|
988
|
-
this.
|
|
1104
|
+
/** Remove a peer route, closing its outbound link. */
|
|
1105
|
+
unsubscribe(instanceId) {
|
|
1106
|
+
const state = this.linkStates.get(instanceId);
|
|
1107
|
+
if (state !== void 0) state.close();
|
|
1108
|
+
this.linkStates.delete(instanceId);
|
|
989
1109
|
}
|
|
990
1110
|
/**
|
|
991
1111
|
* Open (and, on drop, re-open) a persistent WebSocket link to a peer. Local
|
|
992
1112
|
* events stream over the link in real time, and events the peer pushes are
|
|
993
|
-
* surfaced as `interconnect/event`. Repeating for the same
|
|
994
|
-
*
|
|
995
|
-
* @param
|
|
1113
|
+
* surfaced as `interconnect/event`. Repeating for the same instanceId
|
|
1114
|
+
* re-routes the link to the new origin.
|
|
1115
|
+
* @param instanceId - the peer's `instanceId`.
|
|
1116
|
+
* @param origin - receiver origin this instance dials, e.g. `http://127.0.0.1:13080`.
|
|
996
1117
|
* @returns a handle closing the link and cancelling reconnection.
|
|
997
1118
|
*/
|
|
998
|
-
link(
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1119
|
+
link(instanceId, origin) {
|
|
1120
|
+
const existing = this.linkStates.get(instanceId);
|
|
1121
|
+
if (existing !== void 0) {
|
|
1122
|
+
existing.reroute(trimBase(origin));
|
|
1123
|
+
return existing;
|
|
1124
|
+
}
|
|
1125
|
+
const state = new LinkState(this, instanceId, trimBase(origin));
|
|
1126
|
+
this.linkStates.set(instanceId, state);
|
|
1004
1127
|
state.dial();
|
|
1005
1128
|
return state;
|
|
1006
1129
|
}
|
|
1007
1130
|
/** Remove a closed outbound link's state so a later `link` re-dials fresh. */
|
|
1008
|
-
forgetLink(
|
|
1009
|
-
this.linkStates.delete(
|
|
1131
|
+
forgetLink(instanceId) {
|
|
1132
|
+
this.linkStates.delete(instanceId);
|
|
1010
1133
|
}
|
|
1011
|
-
/**
|
|
1134
|
+
/** Push one serialized lifecycle fact out to every linked peer over WS. */
|
|
1012
1135
|
fanout(notification) {
|
|
1013
|
-
const payload = { sender: this.instanceId, notification };
|
|
1014
|
-
for (const peer of this.peers) {
|
|
1015
|
-
void this.post(peer, "event", payload).catch((error) => {
|
|
1016
|
-
this.ctx.logger.warn(`interconnect: event fan-out to ${peer} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
1136
|
this.broadcast(notification);
|
|
1020
1137
|
}
|
|
1021
1138
|
/** Push one fact over every live WebSocket link, dropping closed sockets. */
|
|
@@ -1031,123 +1148,47 @@ var InterconnectService = class extends Service {
|
|
|
1031
1148
|
socket.send(encoded);
|
|
1032
1149
|
}
|
|
1033
1150
|
}
|
|
1034
|
-
/**
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
await this.dispatch(req, res);
|
|
1050
|
-
}
|
|
1051
|
-
/** Parse the ClientRequest envelope and route to the named endpoint. */
|
|
1052
|
-
async dispatch(req, res) {
|
|
1053
|
-
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
1054
|
-
const endpoint = endpointFromPath(INTERCONNECT_CHANNEL, pathname);
|
|
1055
|
-
if (req.method !== "POST" || endpoint === void 0) {
|
|
1056
|
-
res.writeHead(404);
|
|
1057
|
-
res.end("not found");
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
const mediaType = (req.headers["content-type"] ?? "").split(";", 1)[0]?.trim().toLowerCase();
|
|
1061
|
-
if (mediaType !== "application/json") {
|
|
1062
|
-
res.writeHead(415);
|
|
1063
|
-
res.end("content type must be application/json");
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
const body = await readBody(req);
|
|
1067
|
-
if (body === void 0) {
|
|
1068
|
-
res.writeHead(413);
|
|
1069
|
-
res.end("body too large");
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
let envelope;
|
|
1073
|
-
try {
|
|
1074
|
-
envelope = JSON.parse(body);
|
|
1075
|
-
} catch {
|
|
1076
|
-
res.writeHead(400);
|
|
1077
|
-
res.end("body is not JSON");
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
const parsed = clientRequestSchema.safeParse(envelope);
|
|
1081
|
-
const rpcId = typeof envelope.rpcId === "string" ? RpcId(envelope.rpcId) : INVALID_REQUEST_RPC_ID;
|
|
1082
|
-
if (!parsed.success) {
|
|
1083
|
-
writeError(res, rpcId, {
|
|
1084
|
-
code: "bad-request",
|
|
1085
|
-
message: "invalid client-request message",
|
|
1086
|
-
details: { issues: parsed.error.issues }
|
|
1087
|
-
});
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
if (parsed.data.method !== endpoint) {
|
|
1091
|
-
writeError(res, rpcId, {
|
|
1092
|
-
code: "bad-request",
|
|
1093
|
-
message: `method ${JSON.stringify(parsed.data.method)} does not match endpoint ${JSON.stringify(endpoint)}`,
|
|
1094
|
-
details: { issues: [] }
|
|
1151
|
+
/**
|
|
1152
|
+
* Dispatch one inbound `msg` frame: deliver a `send`/`reply` and answer the
|
|
1153
|
+
* correlated `msg-result` on the same socket. Errors are surfaced as the
|
|
1154
|
+
* result so a throwing handler cannot escape the socket's message callback.
|
|
1155
|
+
*/
|
|
1156
|
+
async handleMsgFrame(socket, reqId, message) {
|
|
1157
|
+
let result;
|
|
1158
|
+
if (message.kind === "reply") {
|
|
1159
|
+
result = await this.replyForSession({
|
|
1160
|
+
sessionId: message.sessionId,
|
|
1161
|
+
text: message.text,
|
|
1162
|
+
...message.delivery === void 0 ? {} : { delivery: message.delivery },
|
|
1163
|
+
...message.resume === void 0 ? {} : { resume: message.resume }
|
|
1095
1164
|
});
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
res.writeHead(404);
|
|
1104
|
-
res.end("not found");
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
if (error instanceof InvalidPayloadError) {
|
|
1108
|
-
writeError(res, rpcId, {
|
|
1109
|
-
code: "bad-request",
|
|
1110
|
-
message: error.message,
|
|
1111
|
-
details: { issues: [] }
|
|
1112
|
-
});
|
|
1113
|
-
return;
|
|
1114
|
-
}
|
|
1115
|
-
this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
1116
|
-
writeError(res, rpcId, {
|
|
1117
|
-
code: "internal",
|
|
1118
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1119
|
-
details: {}
|
|
1165
|
+
} else {
|
|
1166
|
+
result = await this.deliver({
|
|
1167
|
+
sessionId: message.sessionId,
|
|
1168
|
+
text: message.text,
|
|
1169
|
+
...message.sender === void 0 ? {} : { sender: message.sender },
|
|
1170
|
+
...message.delivery === void 0 ? {} : { delivery: message.delivery },
|
|
1171
|
+
...message.resume === void 0 ? {} : { resume: message.resume }
|
|
1120
1172
|
});
|
|
1121
1173
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
return this.listSessions();
|
|
1139
|
-
}
|
|
1140
|
-
if (endpoint === "event") {
|
|
1141
|
-
let eventPayload;
|
|
1142
|
-
try {
|
|
1143
|
-
eventPayload = Schema.resolve(payload, eventPayloadSchema, {})[0];
|
|
1144
|
-
} catch (error) {
|
|
1145
|
-
throw new InvalidPayloadError(`event payload invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1146
|
-
}
|
|
1147
|
-
this.receiveEvent(eventPayload);
|
|
1148
|
-
return { accepted: true };
|
|
1174
|
+
this.sendFrame(socket, { type: "msg-result", reqId, result });
|
|
1175
|
+
}
|
|
1176
|
+
/** Dispatch one inbound `query` frame (ping/list/event) and answer on the socket. */
|
|
1177
|
+
async handleQueryFrame(socket, reqId, query) {
|
|
1178
|
+
let result;
|
|
1179
|
+
if (query.kind === "ping") {
|
|
1180
|
+
result = { pong: true, instance: this.instanceId };
|
|
1181
|
+
} else if (query.kind === "list") {
|
|
1182
|
+
result = this.listSessions();
|
|
1183
|
+
} else {
|
|
1184
|
+
const payload = {
|
|
1185
|
+
sender: this.peerOf.get(socket) ?? "unknown-peer",
|
|
1186
|
+
notification: query.notification
|
|
1187
|
+
};
|
|
1188
|
+
this.receiveEvent(payload);
|
|
1189
|
+
result = { accepted: true };
|
|
1149
1190
|
}
|
|
1150
|
-
|
|
1191
|
+
this.sendFrame(socket, { type: "query-result", reqId, result });
|
|
1151
1192
|
}
|
|
1152
1193
|
/** Surface one remote notification to local listeners and the log. */
|
|
1153
1194
|
receiveEvent(eventPayload) {
|
|
@@ -1226,12 +1267,21 @@ var InterconnectService = class extends Service {
|
|
|
1226
1267
|
if (hasApiRemoteSubagentOwner(this.ctx, agent.session, agent)) {
|
|
1227
1268
|
return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
|
|
1228
1269
|
}
|
|
1270
|
+
const sender = (() => {
|
|
1271
|
+
const candidate = payload.sender;
|
|
1272
|
+
return candidate === void 0 || typeof candidate.instanceId !== "string" ? void 0 : candidate;
|
|
1273
|
+
})();
|
|
1274
|
+
if (sender !== void 0) {
|
|
1275
|
+
this.senders.set(payload.sessionId, sender);
|
|
1276
|
+
}
|
|
1229
1277
|
const message = createUserMessage({
|
|
1230
1278
|
source: {
|
|
1231
1279
|
kind: "plugin",
|
|
1232
1280
|
plugin: PLUGIN_SOURCE,
|
|
1233
1281
|
form: "notice",
|
|
1234
|
-
summary: boundContextSummary(
|
|
1282
|
+
summary: boundContextSummary(
|
|
1283
|
+
sender === void 0 ? `interconnect handoff delivered on instance ${this.instanceId}` : `interconnect handoff from ${sender.instanceId} (session ${sender.sessionId}) delivered on instance ${this.instanceId}`
|
|
1284
|
+
)
|
|
1235
1285
|
},
|
|
1236
1286
|
content: [{ type: "text", text: payload.text }]
|
|
1237
1287
|
});
|
|
@@ -1249,35 +1299,18 @@ var InterconnectService = class extends Service {
|
|
|
1249
1299
|
}
|
|
1250
1300
|
return { delivered: true, instance: this.instanceId, delivery: mode };
|
|
1251
1301
|
}
|
|
1252
|
-
/**
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
payload
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const response = await fetch(url, {
|
|
1265
|
-
method: "POST",
|
|
1266
|
-
headers: {
|
|
1267
|
-
"content-type": "application/json",
|
|
1268
|
-
authorization: `Bearer ${token}`
|
|
1269
|
-
},
|
|
1270
|
-
body: JSON.stringify(envelope),
|
|
1271
|
-
signal: AbortSignal.timeout(this.requestTimeoutMs)
|
|
1272
|
-
});
|
|
1273
|
-
if (!response.ok) return void 0;
|
|
1274
|
-
const body = await response.json();
|
|
1275
|
-
if (body.result?.ok !== true) return void 0;
|
|
1276
|
-
return body.result.value;
|
|
1277
|
-
} catch (error) {
|
|
1278
|
-
this.ctx.logger.warn(`interconnect: ${endpoint} to ${baseUrl} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1279
|
-
return void 0;
|
|
1280
|
-
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Inbound `reply` dispatch: forward a request to {link reply}, resolving the
|
|
1304
|
+
* outbound sender for the named local session. Kept separate from {link
|
|
1305
|
+
* deliver} so the two wire endpoints stay distinct in the request pipeline.
|
|
1306
|
+
*/
|
|
1307
|
+
async replyForSession(payload) {
|
|
1308
|
+
return this.reply({
|
|
1309
|
+
sessionId: payload.sessionId,
|
|
1310
|
+
text: payload.text,
|
|
1311
|
+
...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
|
|
1312
|
+
...payload.resume === void 0 ? {} : { resume: payload.resume }
|
|
1313
|
+
});
|
|
1281
1314
|
}
|
|
1282
1315
|
async resolveToken() {
|
|
1283
1316
|
const credential = await this.ctx.credentials.resolve(credentialRef(INTERCONNECT_TOKEN_REF));
|
|
@@ -1350,36 +1383,82 @@ var InterconnectService = class extends Service {
|
|
|
1350
1383
|
this.peerOf.set(socket, frame.sender);
|
|
1351
1384
|
return;
|
|
1352
1385
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1386
|
+
if (frame.type === "event") {
|
|
1387
|
+
const sender = this.peerOf.get(socket) ?? "unknown-peer";
|
|
1388
|
+
try {
|
|
1389
|
+
this.receiveEvent({ sender, notification: frame.notification });
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
this.ctx.logger.warn(`interconnect: listener for a ${frame.notification.kind} event from ${sender} threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
1392
|
+
}
|
|
1393
|
+
return;
|
|
1358
1394
|
}
|
|
1395
|
+
if (frame.type === "msg-result") {
|
|
1396
|
+
const pending = this.pendingMessages.get(frame.reqId);
|
|
1397
|
+
if (pending !== void 0) {
|
|
1398
|
+
;
|
|
1399
|
+
pending.resolve?.(frame.result);
|
|
1400
|
+
}
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (frame.type === "query-result") {
|
|
1404
|
+
const pending = this.pendingMessages.get(frame.reqId);
|
|
1405
|
+
if (pending !== void 0) {
|
|
1406
|
+
;
|
|
1407
|
+
pending.resolve?.(frame.result);
|
|
1408
|
+
}
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (frame.type === "msg") {
|
|
1412
|
+
void Promise.resolve().then(async () => {
|
|
1413
|
+
await this.handleMsgFrame(socket, frame.reqId, frame.message);
|
|
1414
|
+
}).catch((error) => {
|
|
1415
|
+
this.ctx.logger.warn(`interconnect: msg ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
1416
|
+
void this.sendFrame(socket, {
|
|
1417
|
+
type: "msg-result",
|
|
1418
|
+
reqId: frame.reqId,
|
|
1419
|
+
result: { delivered: false, instance: this.instanceId, reason: "unreachable" }
|
|
1420
|
+
});
|
|
1421
|
+
});
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
void Promise.resolve().then(async () => {
|
|
1425
|
+
await this.handleQueryFrame(socket, frame.reqId, frame.query);
|
|
1426
|
+
}).catch((error) => {
|
|
1427
|
+
this.ctx.logger.warn(`interconnect: query ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
1428
|
+
});
|
|
1359
1429
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
}
|
|
1366
|
-
};
|
|
1367
|
-
var InvalidPayloadError = class extends Error {
|
|
1368
|
-
constructor(message) {
|
|
1369
|
-
super(message);
|
|
1370
|
-
this.name = "InvalidPayloadError";
|
|
1430
|
+
/** Write one frame to a socket, returning false when the socket cannot take it. */
|
|
1431
|
+
sendFrame(socket, frame) {
|
|
1432
|
+
if (socket.readyState !== WebSocket.OPEN) return false;
|
|
1433
|
+
socket.send(JSON.stringify(frame));
|
|
1434
|
+
return true;
|
|
1371
1435
|
}
|
|
1372
1436
|
};
|
|
1373
1437
|
var LinkState = class {
|
|
1374
|
-
constructor(owner,
|
|
1438
|
+
constructor(owner, instanceId, origin) {
|
|
1375
1439
|
this.owner = owner;
|
|
1376
|
-
this.
|
|
1440
|
+
this.instanceId = instanceId;
|
|
1441
|
+
this.peer = origin;
|
|
1377
1442
|
}
|
|
1378
1443
|
socket;
|
|
1379
1444
|
closed = false;
|
|
1380
1445
|
retry = 0;
|
|
1381
1446
|
reconnectTimer;
|
|
1447
|
+
// `peer` is mutable so `reroute` can point the link at a new origin.
|
|
1382
1448
|
peer;
|
|
1449
|
+
instanceId;
|
|
1450
|
+
/** Point this link at a different origin; re-dials immediately. */
|
|
1451
|
+
reroute(origin) {
|
|
1452
|
+
if (origin === this.peer) return;
|
|
1453
|
+
this.peer = origin;
|
|
1454
|
+
const socket = this.socket;
|
|
1455
|
+
if (socket !== void 0) {
|
|
1456
|
+
socket.removeAllListeners();
|
|
1457
|
+
socket.terminate();
|
|
1458
|
+
this.socket = void 0;
|
|
1459
|
+
}
|
|
1460
|
+
this.dial();
|
|
1461
|
+
}
|
|
1383
1462
|
/** Open the socket; reconnect is scheduled by the close handler. */
|
|
1384
1463
|
dial() {
|
|
1385
1464
|
if (this.closed) return;
|
|
@@ -1406,6 +1485,17 @@ var LinkState = class {
|
|
|
1406
1485
|
this.scheduleReconnect();
|
|
1407
1486
|
});
|
|
1408
1487
|
}
|
|
1488
|
+
/** Whether this link currently holds an open socket that can carry frames. */
|
|
1489
|
+
writable() {
|
|
1490
|
+
return this.socket !== void 0 && this.socket.readyState === WebSocket.OPEN;
|
|
1491
|
+
}
|
|
1492
|
+
/** Write one frame over this link's outbound socket; false when not open. */
|
|
1493
|
+
sendFrame(frame) {
|
|
1494
|
+
const socket = this.socket;
|
|
1495
|
+
if (socket === void 0 || socket.readyState !== WebSocket.OPEN) return false;
|
|
1496
|
+
socket.send(JSON.stringify(frame));
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1409
1499
|
close() {
|
|
1410
1500
|
this.closed = true;
|
|
1411
1501
|
this.owner.forgetLink(this.peer);
|
|
@@ -1425,21 +1515,6 @@ var LinkState = class {
|
|
|
1425
1515
|
}, delay);
|
|
1426
1516
|
}
|
|
1427
1517
|
};
|
|
1428
|
-
function endpointFromPath(channel, pathname) {
|
|
1429
|
-
if (!pathname.startsWith(`${channel}/`)) return void 0;
|
|
1430
|
-
return pathname.slice(channel.length + 1);
|
|
1431
|
-
}
|
|
1432
|
-
async function readBody(req) {
|
|
1433
|
-
let size = 0;
|
|
1434
|
-
const chunks = [];
|
|
1435
|
-
for await (const chunk of req) {
|
|
1436
|
-
const buffer = chunk;
|
|
1437
|
-
size += buffer.byteLength;
|
|
1438
|
-
if (size > MAX_REQUEST_BODY_BYTES) return void 0;
|
|
1439
|
-
chunks.push(buffer);
|
|
1440
|
-
}
|
|
1441
|
-
return Buffer.concat(chunks).toString();
|
|
1442
|
-
}
|
|
1443
1518
|
function timingSafeEqual(a, b) {
|
|
1444
1519
|
if (a.length !== b.length) return false;
|
|
1445
1520
|
let diff = 0;
|
|
@@ -1451,21 +1526,8 @@ function timingSafeEqual(a, b) {
|
|
|
1451
1526
|
function trimBase(baseUrl) {
|
|
1452
1527
|
return baseUrl.replace(/\/+$/, "");
|
|
1453
1528
|
}
|
|
1454
|
-
function writeFull(res, rpcId, result) {
|
|
1455
|
-
const body = serverResponseSchema.parse({
|
|
1456
|
-
type: "server-response",
|
|
1457
|
-
rpcId,
|
|
1458
|
-
result
|
|
1459
|
-
});
|
|
1460
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
1461
|
-
res.end(JSON.stringify(body));
|
|
1462
|
-
}
|
|
1463
|
-
function writeError(res, rpcId, error) {
|
|
1464
|
-
writeFull(res, rpcId, { ok: false, error });
|
|
1465
|
-
}
|
|
1466
1529
|
var interconnect_default = InterconnectService;
|
|
1467
1530
|
export {
|
|
1468
|
-
INTERCONNECT_CHANNEL,
|
|
1469
1531
|
INTERCONNECT_TOKEN_REF,
|
|
1470
1532
|
InterconnectService,
|
|
1471
1533
|
interconnect_default as default
|