dsh-interconnect 0.7.0 → 0.10.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.
@@ -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,70 +834,78 @@ 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
- sender: Schema.object({
853
- baseUrl: Schema.string(),
854
- instanceId: Schema.string(),
855
- sessionId: Schema.string()
856
- }),
857
- delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
858
- resume: Schema.boolean()
859
- });
860
- var replyPayloadSchema = Schema.object({
861
- sessionId: Schema.string(),
862
- text: Schema.string(),
863
- delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
864
- resume: Schema.boolean()
865
- });
866
- var eventPayloadSchema = Schema.object({
867
- sender: Schema.string(),
868
- notification: notificationSchema
869
- });
870
874
  var InterconnectService = class extends Service {
871
875
  static inject = ["webServer", "agents", "credentials"];
872
876
  static Config = Schema.object({
873
877
  instanceId: Schema.string().default("dsh"),
874
- selfBaseUrl: Schema.string(),
875
878
  requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
876
- peers: Schema.array(Schema.string()).default([]),
879
+ peers: Schema.object({}).default({}),
877
880
  delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
878
881
  allowResume: Schema.boolean().default(true)
879
882
  });
880
883
  instanceId;
881
- selfBaseUrl;
882
884
  requestTimeoutMs;
883
885
  delivery;
884
886
  allowResume;
885
- peers = /* @__PURE__ */ new Set();
886
887
  subscriptions = [];
887
888
  server = new WebSocketServer({ noServer: true });
888
889
  sockets = /* @__PURE__ */ new Set();
890
+ /** Outbound peer links keyed by the peer's `instanceId`. */
889
891
  linkStates = /* @__PURE__ */ new Map();
890
892
  heartbeatTimer;
891
893
  /** Peer identity each live socket announced via its `hello` frame, if any. */
892
894
  peerOf = /* @__PURE__ */ new WeakMap();
893
895
  /** Sender each local session last received a send from, keyed by local session id. */
894
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;
895
900
  constructor(ctx, config) {
896
901
  super(ctx, "interconnect");
897
902
  this.instanceId = config.instanceId;
898
- this.selfBaseUrl = config.selfBaseUrl;
899
903
  this.requestTimeoutMs = config.requestTimeoutMs;
900
904
  this.delivery = config.delivery ?? "followup";
901
905
  this.allowResume = config.allowResume ?? true;
902
- for (const peer of config.peers ?? []) this.peers.add(trimBase(peer));
903
- const route = {
904
- kind: "prefix",
905
- path: INTERCONNECT_CHANNEL,
906
- handler: (req, res) => this.handle(req, res)
907
- };
908
- 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
+ }
909
909
  const upgrade = {
910
910
  path: LINK_CHANNEL,
911
911
  handler: (req, socket, head) => {
@@ -967,13 +967,12 @@ var InterconnectService = class extends Service {
967
967
  }, "interconnect: websocket teardown");
968
968
  }
969
969
  /**
970
- * This instance's identity to attach to outbound messages, when configured.
971
- * Absent (`selfBaseUrl` unset) outbound sends are not attributable and the
972
- * peer cannot reply to them.
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.
973
972
  * @param sessionId - the local session that is sending, used as the reply target.
974
973
  */
975
974
  selfSender(sessionId) {
976
- return this.selfBaseUrl === void 0 ? void 0 : { baseUrl: this.selfBaseUrl, instanceId: this.instanceId, sessionId };
975
+ return { instanceId: this.instanceId, sessionId };
977
976
  }
978
977
  async send(request) {
979
978
  const payload = {
@@ -983,14 +982,14 @@ var InterconnectService = class extends Service {
983
982
  ...request.delivery === void 0 ? {} : { delivery: request.delivery },
984
983
  ...request.resume === void 0 ? {} : { resume: request.resume }
985
984
  };
986
- const result = await this.post(request.baseUrl, "send", payload);
985
+ const result = await this.msgRequest(request.instanceId, "send", payload);
987
986
  return result ?? { delivered: false, instance: this.instanceId, reason: "unreachable" };
988
987
  }
989
988
  /**
990
989
  * Deliver one text message back to the peer that a local session last
991
990
  * received a send from. `sessionId` names the LOCAL replying session; the
992
- * outbound target is the `sender` that session recorded, resolved to a
993
- * `send` against the recalled baseUrl and remote session id.
991
+ * outbound target is the `sender` that session recorded, addressed through
992
+ * this instance's own link to the sender's instance.
994
993
  */
995
994
  async reply(request) {
996
995
  const sender = this.senders.get(request.sessionId);
@@ -1003,68 +1002,137 @@ var InterconnectService = class extends Service {
1003
1002
  ...request.delivery === void 0 ? {} : { delivery: request.delivery },
1004
1003
  ...request.resume === void 0 ? {} : { resume: request.resume },
1005
1004
  // Attribute the reply to this instance so the peer, in turn, can reply
1006
- // back — chaining the conversation. Absent when we have no self origin.
1007
- ...this.selfBaseUrl === void 0 ? {} : { sender: this.selfSender(request.sessionId) }
1005
+ // back — chaining the conversation.
1006
+ sender: this.selfSender(request.sessionId)
1008
1007
  };
1009
- const result = await this.post(sender.baseUrl, "send", payload);
1008
+ const result = await this.msgRequest(sender.instanceId, "send", payload);
1010
1009
  return result ?? { delivered: false, instance: this.instanceId, reason: "unreachable" };
1011
1010
  }
1012
- async ping(baseUrl) {
1013
- return this.post(baseUrl, "ping", {});
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" });
1014
1018
  }
1015
1019
  /**
1016
- * List the peer's live sessions so a caller can discover a valid `send`
1017
- * target. `undefined` on transport or auth failure, matching `ping`.
1020
+ * List a peer instance's live sessions over its persistent link. Undefined on
1021
+ * transport failure, matching `ping`.
1018
1022
  */
1019
- async list(baseUrl) {
1020
- return this.post(baseUrl, "list", {});
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;
1021
1030
  }
1022
1031
  /**
1023
- * Add a peer origin to the event fan-out set at runtime. Returns a disposer
1024
- * that removes it. Duplicate origins are idempotent.
1025
- * @param peer - receiver origin, e.g. `http://127.0.0.1:3080`.
1026
- * @returns disposer removing the peer from the fan-out set.
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.
1027
1036
  */
1028
- subscribe(peer) {
1029
- const trimmed = trimBase(peer);
1030
- this.peers.add(trimmed);
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
+ });
1090
+ }
1091
+ /**
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.
1097
+ */
1098
+ subscribe(instanceId, origin) {
1099
+ this.link(instanceId, origin);
1031
1100
  return () => {
1032
- this.peers.delete(trimmed);
1101
+ this.forgetLink(instanceId);
1033
1102
  };
1034
1103
  }
1035
- /** Remove a peer origin from the event fan-out set. */
1036
- unsubscribe(peer) {
1037
- this.peers.delete(trimBase(peer));
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);
1038
1109
  }
1039
1110
  /**
1040
1111
  * Open (and, on drop, re-open) a persistent WebSocket link to a peer. Local
1041
1112
  * events stream over the link in real time, and events the peer pushes are
1042
- * surfaced as `interconnect/event`. Repeating for the same peer returns the
1043
- * existing handle.
1044
- * @param peer - receiver origin, e.g. `http://127.0.0.1:3080` (dialed as `ws:`).
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`.
1045
1117
  * @returns a handle closing the link and cancelling reconnection.
1046
1118
  */
1047
- link(peer) {
1048
- const trimmed = trimBase(peer);
1049
- const existing = this.linkStates.get(trimmed);
1050
- if (existing !== void 0) return existing;
1051
- const state = new LinkState(this, trimmed);
1052
- this.linkStates.set(trimmed, state);
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);
1053
1127
  state.dial();
1054
1128
  return state;
1055
1129
  }
1056
1130
  /** Remove a closed outbound link's state so a later `link` re-dials fresh. */
1057
- forgetLink(peer) {
1058
- this.linkStates.delete(peer);
1131
+ forgetLink(instanceId) {
1132
+ this.linkStates.delete(instanceId);
1059
1133
  }
1060
- /** Fan one serialized lifecycle fact out to every registered peer, fire-and-forget. */
1134
+ /** Push one serialized lifecycle fact out to every linked peer over WS. */
1061
1135
  fanout(notification) {
1062
- const payload = { sender: this.instanceId, notification };
1063
- for (const peer of this.peers) {
1064
- void this.post(peer, "event", payload).catch((error) => {
1065
- this.ctx.logger.warn(`interconnect: event fan-out to ${peer} failed: ${error instanceof Error ? error.message : String(error)}`);
1066
- });
1067
- }
1068
1136
  this.broadcast(notification);
1069
1137
  }
1070
1138
  /** Push one fact over every live WebSocket link, dropping closed sockets. */
@@ -1080,132 +1148,47 @@ var InterconnectService = class extends Service {
1080
1148
  socket.send(encoded);
1081
1149
  }
1082
1150
  }
1083
- /** Inbound route handler: bearer auth first, then envelope dispatch. */
1084
- async handle(req, res) {
1085
- const token = await this.resolveToken();
1086
- if (token === void 0) {
1087
- res.writeHead(403);
1088
- res.end("forbidden");
1089
- return;
1090
- }
1091
- const expected = `Bearer ${token}`;
1092
- const header = req.headers.authorization;
1093
- if (header === void 0 || !timingSafeEqual(header, expected)) {
1094
- res.writeHead(401);
1095
- res.end("unauthorized");
1096
- return;
1097
- }
1098
- await this.dispatch(req, res);
1099
- }
1100
- /** Parse the ClientRequest envelope and route to the named endpoint. */
1101
- async dispatch(req, res) {
1102
- const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
1103
- const endpoint = endpointFromPath(INTERCONNECT_CHANNEL, pathname);
1104
- if (req.method !== "POST" || endpoint === void 0) {
1105
- res.writeHead(404);
1106
- res.end("not found");
1107
- return;
1108
- }
1109
- const mediaType = (req.headers["content-type"] ?? "").split(";", 1)[0]?.trim().toLowerCase();
1110
- if (mediaType !== "application/json") {
1111
- res.writeHead(415);
1112
- res.end("content type must be application/json");
1113
- return;
1114
- }
1115
- const body = await readBody(req);
1116
- if (body === void 0) {
1117
- res.writeHead(413);
1118
- res.end("body too large");
1119
- return;
1120
- }
1121
- let envelope;
1122
- try {
1123
- envelope = JSON.parse(body);
1124
- } catch {
1125
- res.writeHead(400);
1126
- res.end("body is not JSON");
1127
- return;
1128
- }
1129
- const parsed = clientRequestSchema.safeParse(envelope);
1130
- const rpcId = typeof envelope.rpcId === "string" ? RpcId(envelope.rpcId) : INVALID_REQUEST_RPC_ID;
1131
- if (!parsed.success) {
1132
- writeError(res, rpcId, {
1133
- code: "bad-request",
1134
- message: "invalid client-request message",
1135
- details: { issues: parsed.error.issues }
1136
- });
1137
- return;
1138
- }
1139
- if (parsed.data.method !== endpoint) {
1140
- writeError(res, rpcId, {
1141
- code: "bad-request",
1142
- message: `method ${JSON.stringify(parsed.data.method)} does not match endpoint ${JSON.stringify(endpoint)}`,
1143
- 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 }
1144
1164
  });
1145
- return;
1146
- }
1147
- try {
1148
- const value = await this.runEndpoint(endpoint, parsed.data.payload);
1149
- writeFull(res, rpcId, { ok: true, value });
1150
- } catch (error) {
1151
- if (error instanceof UnknownEndpointError) {
1152
- res.writeHead(404);
1153
- res.end("not found");
1154
- return;
1155
- }
1156
- if (error instanceof InvalidPayloadError) {
1157
- writeError(res, rpcId, {
1158
- code: "bad-request",
1159
- message: error.message,
1160
- details: { issues: [] }
1161
- });
1162
- return;
1163
- }
1164
- this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
1165
- writeError(res, rpcId, {
1166
- code: "internal",
1167
- message: error instanceof Error ? error.message : String(error),
1168
- 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 }
1169
1172
  });
1170
1173
  }
1171
- }
1172
- /** Dispatch one authenticated business call. */
1173
- async runEndpoint(endpoint, payload) {
1174
- if (endpoint === "ping") {
1175
- return { pong: true, instance: this.instanceId };
1176
- }
1177
- if (endpoint === "send") {
1178
- let parsed;
1179
- try {
1180
- parsed = Schema.resolve(payload, sendPayloadSchema, {})[0];
1181
- } catch (error) {
1182
- throw new InvalidPayloadError(`send payload invalid: ${error instanceof Error ? error.message : String(error)}`);
1183
- }
1184
- return this.deliver(parsed);
1185
- }
1186
- if (endpoint === "reply") {
1187
- let parsed;
1188
- try {
1189
- parsed = Schema.resolve(payload, replyPayloadSchema, {})[0];
1190
- } catch (error) {
1191
- throw new InvalidPayloadError(`reply payload invalid: ${error instanceof Error ? error.message : String(error)}`);
1192
- }
1193
- return this.replyForSession(parsed);
1194
- }
1195
- if (endpoint === "list") {
1196
- return this.listSessions();
1197
- }
1198
- if (endpoint === "event") {
1199
- let eventPayload;
1200
- try {
1201
- eventPayload = Schema.resolve(payload, eventPayloadSchema, {})[0];
1202
- } catch (error) {
1203
- throw new InvalidPayloadError(`event payload invalid: ${error instanceof Error ? error.message : String(error)}`);
1204
- }
1205
- this.receiveEvent(eventPayload);
1206
- 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 };
1207
1190
  }
1208
- throw new UnknownEndpointError(endpoint);
1191
+ this.sendFrame(socket, { type: "query-result", reqId, result });
1209
1192
  }
1210
1193
  /** Surface one remote notification to local listeners and the log. */
1211
1194
  receiveEvent(eventPayload) {
@@ -1286,7 +1269,7 @@ var InterconnectService = class extends Service {
1286
1269
  }
1287
1270
  const sender = (() => {
1288
1271
  const candidate = payload.sender;
1289
- return candidate === void 0 || typeof candidate.baseUrl !== "string" ? void 0 : candidate;
1272
+ return candidate === void 0 || typeof candidate.instanceId !== "string" ? void 0 : candidate;
1290
1273
  })();
1291
1274
  if (sender !== void 0) {
1292
1275
  this.senders.set(payload.sessionId, sender);
@@ -1329,35 +1312,6 @@ var InterconnectService = class extends Service {
1329
1312
  ...payload.resume === void 0 ? {} : { resume: payload.resume }
1330
1313
  });
1331
1314
  }
1332
- async post(baseUrl, endpoint, payload) {
1333
- const token = await this.resolveToken();
1334
- if (token === void 0) return void 0;
1335
- const url = `${trimBase(baseUrl)}${INTERCONNECT_CHANNEL}/${endpoint}`;
1336
- const envelope = {
1337
- type: "client-request",
1338
- rpcId: crypto.randomUUID(),
1339
- method: endpoint,
1340
- payload
1341
- };
1342
- try {
1343
- const response = await fetch(url, {
1344
- method: "POST",
1345
- headers: {
1346
- "content-type": "application/json",
1347
- authorization: `Bearer ${token}`
1348
- },
1349
- body: JSON.stringify(envelope),
1350
- signal: AbortSignal.timeout(this.requestTimeoutMs)
1351
- });
1352
- if (!response.ok) return void 0;
1353
- const body = await response.json();
1354
- if (body.result?.ok !== true) return void 0;
1355
- return body.result.value;
1356
- } catch (error) {
1357
- this.ctx.logger.warn(`interconnect: ${endpoint} to ${baseUrl} failed: ${error instanceof Error ? error.message : String(error)}`);
1358
- return void 0;
1359
- }
1360
- }
1361
1315
  async resolveToken() {
1362
1316
  const credential = await this.ctx.credentials.resolve(credentialRef(INTERCONNECT_TOKEN_REF));
1363
1317
  return credential === void 0 || credential.value.length === 0 ? void 0 : credential.value;
@@ -1429,36 +1383,82 @@ var InterconnectService = class extends Service {
1429
1383
  this.peerOf.set(socket, frame.sender);
1430
1384
  return;
1431
1385
  }
1432
- const sender = this.peerOf.get(socket) ?? "unknown-peer";
1433
- try {
1434
- this.receiveEvent({ sender, notification: frame.notification });
1435
- } catch (error) {
1436
- this.ctx.logger.warn(`interconnect: listener for a ${frame.notification.kind} event from ${sender} threw: ${error instanceof Error ? error.message : String(error)}`);
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;
1437
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
+ });
1438
1429
  }
1439
- };
1440
- var UnknownEndpointError = class extends Error {
1441
- constructor(endpoint) {
1442
- super(`unknown interconnect endpoint ${JSON.stringify(endpoint)}`);
1443
- this.name = "UnknownEndpointError";
1444
- }
1445
- };
1446
- var InvalidPayloadError = class extends Error {
1447
- constructor(message) {
1448
- super(message);
1449
- 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;
1450
1435
  }
1451
1436
  };
1452
1437
  var LinkState = class {
1453
- constructor(owner, peer) {
1438
+ constructor(owner, instanceId, origin) {
1454
1439
  this.owner = owner;
1455
- this.peer = peer;
1440
+ this.instanceId = instanceId;
1441
+ this.peer = origin;
1456
1442
  }
1457
1443
  socket;
1458
1444
  closed = false;
1459
1445
  retry = 0;
1460
1446
  reconnectTimer;
1447
+ // `peer` is mutable so `reroute` can point the link at a new origin.
1461
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
+ }
1462
1462
  /** Open the socket; reconnect is scheduled by the close handler. */
1463
1463
  dial() {
1464
1464
  if (this.closed) return;
@@ -1485,6 +1485,17 @@ var LinkState = class {
1485
1485
  this.scheduleReconnect();
1486
1486
  });
1487
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
+ }
1488
1499
  close() {
1489
1500
  this.closed = true;
1490
1501
  this.owner.forgetLink(this.peer);
@@ -1504,21 +1515,6 @@ var LinkState = class {
1504
1515
  }, delay);
1505
1516
  }
1506
1517
  };
1507
- function endpointFromPath(channel, pathname) {
1508
- if (!pathname.startsWith(`${channel}/`)) return void 0;
1509
- return pathname.slice(channel.length + 1);
1510
- }
1511
- async function readBody(req) {
1512
- let size = 0;
1513
- const chunks = [];
1514
- for await (const chunk of req) {
1515
- const buffer = chunk;
1516
- size += buffer.byteLength;
1517
- if (size > MAX_REQUEST_BODY_BYTES) return void 0;
1518
- chunks.push(buffer);
1519
- }
1520
- return Buffer.concat(chunks).toString();
1521
- }
1522
1518
  function timingSafeEqual(a, b) {
1523
1519
  if (a.length !== b.length) return false;
1524
1520
  let diff = 0;
@@ -1530,21 +1526,8 @@ function timingSafeEqual(a, b) {
1530
1526
  function trimBase(baseUrl) {
1531
1527
  return baseUrl.replace(/\/+$/, "");
1532
1528
  }
1533
- function writeFull(res, rpcId, result) {
1534
- const body = serverResponseSchema.parse({
1535
- type: "server-response",
1536
- rpcId,
1537
- result
1538
- });
1539
- res.writeHead(200, { "content-type": "application/json" });
1540
- res.end(JSON.stringify(body));
1541
- }
1542
- function writeError(res, rpcId, error) {
1543
- writeFull(res, rpcId, { ok: false, error });
1544
- }
1545
1529
  var index_default = InterconnectService;
1546
1530
  export {
1547
- INTERCONNECT_CHANNEL,
1548
1531
  INTERCONNECT_TOKEN_REF,
1549
1532
  InterconnectService,
1550
1533
  index_default as default