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/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.array(Schema.string()).default([]),
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 peer of config.peers ?? []) this.peers.add(trimBase(peer));
887
- const route = {
888
- kind: "prefix",
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.post(request.baseUrl, "send", payload);
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
- async ping(baseUrl) {
964
- 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" });
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
- * List the peer's live sessions so a caller can discover a valid `send`
968
- * target. `undefined` on transport or auth failure, matching `ping`.
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 list(baseUrl) {
971
- return this.post(baseUrl, "list", {});
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 origin to the event fan-out set at runtime. Returns a disposer
975
- * that removes it. Duplicate origins are idempotent.
976
- * @param peer - receiver origin, e.g. `http://127.0.0.1:3080`.
977
- * @returns disposer removing the peer from the fan-out set.
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(peer) {
980
- const trimmed = trimBase(peer);
981
- this.peers.add(trimmed);
1098
+ subscribe(instanceId, origin) {
1099
+ this.link(instanceId, origin);
982
1100
  return () => {
983
- this.peers.delete(trimmed);
1101
+ this.forgetLink(instanceId);
984
1102
  };
985
1103
  }
986
- /** Remove a peer origin from the event fan-out set. */
987
- unsubscribe(peer) {
988
- 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);
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 peer returns the
994
- * existing handle.
995
- * @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`.
996
1117
  * @returns a handle closing the link and cancelling reconnection.
997
1118
  */
998
- link(peer) {
999
- const trimmed = trimBase(peer);
1000
- const existing = this.linkStates.get(trimmed);
1001
- if (existing !== void 0) return existing;
1002
- const state = new LinkState(this, trimmed);
1003
- 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);
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(peer) {
1009
- this.linkStates.delete(peer);
1131
+ forgetLink(instanceId) {
1132
+ this.linkStates.delete(instanceId);
1010
1133
  }
1011
- /** 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. */
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
- /** Inbound route handler: bearer auth first, then envelope dispatch. */
1035
- async handle(req, res) {
1036
- const token = await this.resolveToken();
1037
- if (token === void 0) {
1038
- res.writeHead(403);
1039
- res.end("forbidden");
1040
- return;
1041
- }
1042
- const expected = `Bearer ${token}`;
1043
- const header = req.headers.authorization;
1044
- if (header === void 0 || !timingSafeEqual(header, expected)) {
1045
- res.writeHead(401);
1046
- res.end("unauthorized");
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
- return;
1097
- }
1098
- try {
1099
- const value = await this.runEndpoint(endpoint, parsed.data.payload);
1100
- writeFull(res, rpcId, { ok: true, value });
1101
- } catch (error) {
1102
- if (error instanceof UnknownEndpointError) {
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
- /** Dispatch one authenticated business call. */
1124
- async runEndpoint(endpoint, payload) {
1125
- if (endpoint === "ping") {
1126
- return { pong: true, instance: this.instanceId };
1127
- }
1128
- if (endpoint === "send") {
1129
- let parsed;
1130
- try {
1131
- parsed = Schema.resolve(payload, sendPayloadSchema, {})[0];
1132
- } catch (error) {
1133
- throw new InvalidPayloadError(`send payload invalid: ${error instanceof Error ? error.message : String(error)}`);
1134
- }
1135
- return this.deliver(parsed);
1136
- }
1137
- if (endpoint === "list") {
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
- throw new UnknownEndpointError(endpoint);
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(`interconnect handoff delivered on instance ${this.instanceId}`)
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
- /** POST one business call to a peer instance; undefined on transport/auth failure. */
1253
- async post(baseUrl, endpoint, payload) {
1254
- const token = await this.resolveToken();
1255
- if (token === void 0) return void 0;
1256
- const url = `${trimBase(baseUrl)}${INTERCONNECT_CHANNEL}/${endpoint}`;
1257
- const envelope = {
1258
- type: "client-request",
1259
- rpcId: crypto.randomUUID(),
1260
- method: endpoint,
1261
- payload
1262
- };
1263
- try {
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
- const sender = this.peerOf.get(socket) ?? "unknown-peer";
1354
- try {
1355
- this.receiveEvent({ sender, notification: frame.notification });
1356
- } catch (error) {
1357
- 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;
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
- var UnknownEndpointError = class extends Error {
1362
- constructor(endpoint) {
1363
- super(`unknown interconnect endpoint ${JSON.stringify(endpoint)}`);
1364
- this.name = "UnknownEndpointError";
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, peer) {
1438
+ constructor(owner, instanceId, origin) {
1375
1439
  this.owner = owner;
1376
- this.peer = peer;
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