dsh-interconnect 0.11.24 → 0.15.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
@@ -5,7 +5,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
5
5
  import { remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
6
6
  import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
7
7
 
8
- // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
8
+ // ../dsh-wt-ic-merge/vendor/cosmokit/src/misc.ts
9
9
  function isNullable(value) {
10
10
  return value === null || value === void 0;
11
11
  }
@@ -27,7 +27,7 @@ function pick(source, keys, forced) {
27
27
  return result;
28
28
  }
29
29
 
30
- // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/types.ts
30
+ // ../dsh-wt-ic-merge/vendor/cosmokit/src/types.ts
31
31
  function is(type, value) {
32
32
  if (arguments.length === 1) return (value2) => is(type, value2);
33
33
  return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
@@ -136,7 +136,7 @@ function deepEqual(a, b, strict) {
136
136
  }) ?? Object.keys({ ...a, ...b }).every((key) => deepEqual(a[key], b[key], strict));
137
137
  }
138
138
 
139
- // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/time.ts
139
+ // ../dsh-wt-ic-merge/vendor/cosmokit/src/time.ts
140
140
  var Time;
141
141
  ((Time2) => {
142
142
  Time2.millisecond = 1;
@@ -216,7 +216,7 @@ var Time;
216
216
  Time2.template = template;
217
217
  })(Time || (Time = {}));
218
218
 
219
- // ../.dsh/source/staging-20260807T143236Z/vendor/schemastery/lib/index.mjs
219
+ // ../dsh-wt-ic-merge/vendor/schemastery/lib/index.mjs
220
220
  var kSchema = Symbol.for("schemastery");
221
221
  var kValidationError = Symbol.for("ValidationError");
222
222
  globalThis.__schemastery_index__ ??= 0;
@@ -820,12 +820,8 @@ defineMethod("transform", [
820
820
 
821
821
  // src/interconnect/index.ts
822
822
  import WebSocket, { WebSocketServer } from "ws";
823
- function isSessionOwnedBySubagent(ctx, session, agent) {
824
- if (session.header.origin === "subagent") return true;
825
- const parentId = session.header.parentSession;
826
- if (parentId === void 0 || agent === void 0) return false;
827
- const parent = ctx.agents.get(parentId);
828
- return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
823
+ function isSessionOwnedBySubagent(session) {
824
+ return session.header.origin === "subagent";
829
825
  }
830
826
  function assertNever(value) {
831
827
  throw new Error(`interconnect: unhandled variant ${JSON.stringify(value)}`);
@@ -836,6 +832,7 @@ var DEFAULT_INSTANCE_ID = "dsh";
836
832
  var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
837
833
  var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
838
834
  var LINK_CHANNEL = "/interconnect/link";
835
+ var SELF_PEER_ORIGIN = "self";
839
836
  var MAX_INSTANCE_ID_CHARS = 256;
840
837
  var MAX_REQUEST_ID_CHARS = 256;
841
838
  var MAX_LISTED_SESSIONS = 100;
@@ -963,11 +960,19 @@ var InterconnectService = class extends Service {
963
960
  this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
964
961
  this.delivery = config.delivery ?? "followup";
965
962
  this.allowResume = config.allowResume ?? true;
966
- for (const origin of Object.values(config.peers ?? {})) {
963
+ for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
964
+ if (origin === SELF_PEER_ORIGIN) {
965
+ if (peerInstanceId !== this.instanceId) {
966
+ throw new Error(
967
+ `interconnect: peer "${peerInstanceId}" is configured as self, but this instance announces "${this.instanceId}"; key a self peer by the instanceId`
968
+ );
969
+ }
970
+ continue;
971
+ }
967
972
  linkUrl(trimBase(origin));
968
973
  }
969
974
  for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
970
- this.link(peerInstanceId, origin);
975
+ this.link(peerInstanceId, origin === SELF_PEER_ORIGIN ? () => this.selfOrigin() : origin);
971
976
  }
972
977
  const upgrade = {
973
978
  path: LINK_CHANNEL,
@@ -1108,7 +1113,7 @@ var InterconnectService = class extends Service {
1108
1113
  * Returns the peer identity when reachable, or undefined when the link is
1109
1114
  * not up or no answer arrives.
1110
1115
  * @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
1111
- * @returns the peer's identity, or undefined when the link is not up or no answer arrives.
1116
+ * @returns the peer's identity, or undefined when there is no usable answer.
1112
1117
  */
1113
1118
  async ping(instanceId) {
1114
1119
  return this.queryRequest(instanceId, { kind: "ping" });
@@ -1122,16 +1127,33 @@ var InterconnectService = class extends Service {
1122
1127
  async list(instanceId) {
1123
1128
  return this.queryRequest(instanceId, { kind: "list" });
1124
1129
  }
1130
+ /**
1131
+ * The peers this instance configures, each with the current state of the link
1132
+ * it dials, in configuration order. This is how a caller tells an id this
1133
+ * instance cannot reach at all (`peer-not-configured`) from one whose link is
1134
+ * merely down (`unreachable`), which the bare `undefined` of `ping`/`list`
1135
+ * cannot express. A configured peer whose link is `down` is still a valid
1136
+ * target: its reconnect runs on its own backoff, so it may answer later.
1137
+ * @returns one row per configured peer.
1138
+ */
1139
+ peers() {
1140
+ return [...this.linkStates].map(([instanceId, state]) => ({
1141
+ instanceId,
1142
+ origin: state.peer,
1143
+ link: state.writable() ? "up" : "down"
1144
+ }));
1145
+ }
1125
1146
  /**
1126
1147
  * Send a `msg` frame over the live link to a peer and resolve with its
1127
- * correlated `msg-result`. Undefined when the peer has no live link here
1128
- * (not configured, or the link is down) — sends can only address configured+
1129
- * connected peers by design — and a `message-too-large` result when the
1130
- * encoded frame would not fit the link.
1148
+ * correlated `msg-result`. A `peer-not-configured` result when this instance
1149
+ * configures no route for that id, undefined when the route exists but its
1150
+ * link is down, and a `message-too-large` result when the encoded frame would
1151
+ * not fit the link.
1131
1152
  */
1132
1153
  async msgRequest(instanceId, kind, payload) {
1133
1154
  const state = this.linkStates.get(instanceId);
1134
- if (state === void 0 || !state.writable()) return void 0;
1155
+ if (state === void 0) return { delivered: false, instance: instanceId, reason: "peer-not-configured" };
1156
+ if (!state.writable()) return void 0;
1135
1157
  const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
1136
1158
  const frame = {
1137
1159
  type: "msg",
@@ -1193,7 +1215,8 @@ var InterconnectService = class extends Service {
1193
1215
  * `Config.peers` entry; the resulting state owns reconnection for the life of
1194
1216
  * the service fiber.
1195
1217
  * @param instanceId - the peer's `instanceId`.
1196
- * @param origin - receiver origin this instance dials, e.g. `http://127.0.0.1:13080`.
1218
+ * @param origin - receiver origin this instance dials (e.g.
1219
+ * `http://127.0.0.1:13080`), or a resolver for a `self` route.
1197
1220
  */
1198
1221
  link(instanceId, origin) {
1199
1222
  const state = new LinkState(
@@ -1203,11 +1226,28 @@ var InterconnectService = class extends Service {
1203
1226
  this.ctx.logger,
1204
1227
  () => this.resolveToken(),
1205
1228
  instanceId,
1206
- trimBase(origin)
1229
+ typeof origin === "string" ? trimBase(origin) : origin
1207
1230
  );
1208
1231
  this.linkStates.set(instanceId, state);
1209
1232
  state.dial();
1210
1233
  }
1234
+ /**
1235
+ * The origin of this instance's own host webserver, or undefined while no
1236
+ * listening port exists. Read on every dial rather than cached: the
1237
+ * webserver may activate after this service, its port is assigned only in
1238
+ * its listen callback (`--port 0` picks a fresh one per start), and a
1239
+ * replacement webserver would move it again. Loopback is used regardless of
1240
+ * the bind host: a server bound to `0.0.0.0` also accepts `127.0.0.1`, while
1241
+ * `0.0.0.0` itself is a bind address, not a dialable origin.
1242
+ * @returns `http://127.0.0.1:<port>`, or undefined without a listening server.
1243
+ */
1244
+ selfOrigin() {
1245
+ const server = this.ctx.get("webServer");
1246
+ if (server === void 0) return void 0;
1247
+ const port = server.port;
1248
+ if (typeof port !== "number" || port <= 0) return void 0;
1249
+ return `http://127.0.0.1:${String(port)}`;
1250
+ }
1211
1251
  /** Push one serialized lifecycle fact out to every linked peer over WS. */
1212
1252
  fanout(notification) {
1213
1253
  this.broadcast(notification);
@@ -1342,7 +1382,7 @@ var InterconnectService = class extends Service {
1342
1382
  * @returns the bounded listing.
1343
1383
  */
1344
1384
  listSessions(rowBudgetBytes) {
1345
- const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
1385
+ const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(agent.session));
1346
1386
  const sessions = [];
1347
1387
  let used = 0;
1348
1388
  for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
@@ -1369,7 +1409,8 @@ var InterconnectService = class extends Service {
1369
1409
  let title;
1370
1410
  try {
1371
1411
  const snapshot = this.ctx.get("sessionProjections")?.snapshot(agent.session);
1372
- const value = snapshot?.values.title;
1412
+ const values = snapshot?.values;
1413
+ const value = values?.title;
1373
1414
  if (typeof value === "string" && value !== "") title = value;
1374
1415
  } catch {
1375
1416
  title = void 0;
@@ -1427,7 +1468,7 @@ var InterconnectService = class extends Service {
1427
1468
  }
1428
1469
  agent = woken.agent;
1429
1470
  }
1430
- if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
1471
+ if (isSessionOwnedBySubagent(agent.session)) {
1431
1472
  return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
1432
1473
  }
1433
1474
  const sender = (() => {
@@ -1636,24 +1677,40 @@ var LinkState = class {
1636
1677
  this.logger = logger;
1637
1678
  this.resolveToken = resolveToken;
1638
1679
  this.instanceId = instanceId;
1639
- this.peer = origin;
1680
+ this.origin = origin;
1640
1681
  }
1641
1682
  dialed;
1642
1683
  closed = false;
1643
1684
  retry = 0;
1644
1685
  reconnectTimer;
1645
1686
  warnedNoToken = false;
1646
- /** Receiver origin this link dials. */
1647
- peer;
1687
+ warnedNoOrigin = false;
1648
1688
  instanceId;
1689
+ origin;
1690
+ /** The origin this link dials now; for an unresolved `self` route, the sentinel is shown. */
1691
+ get peer() {
1692
+ return typeof this.origin === "string" ? this.origin : this.origin() ?? SELF_PEER_ORIGIN;
1693
+ }
1649
1694
  /** Open the socket; reconnect is scheduled by the close handler. */
1650
1695
  dial() {
1651
1696
  if (this.closed) return;
1697
+ const origin = typeof this.origin === "string" ? this.origin : this.origin();
1698
+ if (origin === void 0) {
1699
+ if (this.retry >= 1 && !this.warnedNoOrigin) {
1700
+ this.warnedNoOrigin = true;
1701
+ this.logger.warn(
1702
+ `interconnect: peer ${this.instanceId} is configured as self but this instance has no listening webServer port yet; the link stays down and retries`
1703
+ );
1704
+ }
1705
+ this.scheduleReconnect();
1706
+ return;
1707
+ }
1708
+ this.warnedNoOrigin = false;
1652
1709
  let url;
1653
1710
  try {
1654
- url = linkUrl(this.peer);
1711
+ url = linkUrl(origin);
1655
1712
  } catch {
1656
- this.logger.warn(`interconnect: invalid peer origin ${this.peer}; link to ${this.instanceId} stays down`);
1713
+ this.logger.warn(`interconnect: invalid peer origin ${origin}; link to ${this.instanceId} stays down`);
1657
1714
  return;
1658
1715
  }
1659
1716
  void this.resolveToken().then((token) => {
@@ -5,7 +5,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
5
5
  import { remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
6
6
  import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
7
7
 
8
- // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
8
+ // ../dsh-wt-ic-merge/vendor/cosmokit/src/misc.ts
9
9
  function isNullable(value) {
10
10
  return value === null || value === void 0;
11
11
  }
@@ -27,7 +27,7 @@ function pick(source, keys, forced) {
27
27
  return result;
28
28
  }
29
29
 
30
- // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/types.ts
30
+ // ../dsh-wt-ic-merge/vendor/cosmokit/src/types.ts
31
31
  function is(type, value) {
32
32
  if (arguments.length === 1) return (value2) => is(type, value2);
33
33
  return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
@@ -136,7 +136,7 @@ function deepEqual(a, b, strict) {
136
136
  }) ?? Object.keys({ ...a, ...b }).every((key) => deepEqual(a[key], b[key], strict));
137
137
  }
138
138
 
139
- // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/time.ts
139
+ // ../dsh-wt-ic-merge/vendor/cosmokit/src/time.ts
140
140
  var Time;
141
141
  ((Time2) => {
142
142
  Time2.millisecond = 1;
@@ -216,7 +216,7 @@ var Time;
216
216
  Time2.template = template;
217
217
  })(Time || (Time = {}));
218
218
 
219
- // ../.dsh/source/staging-20260807T143236Z/vendor/schemastery/lib/index.mjs
219
+ // ../dsh-wt-ic-merge/vendor/schemastery/lib/index.mjs
220
220
  var kSchema = Symbol.for("schemastery");
221
221
  var kValidationError = Symbol.for("ValidationError");
222
222
  globalThis.__schemastery_index__ ??= 0;
@@ -820,12 +820,8 @@ defineMethod("transform", [
820
820
 
821
821
  // src/interconnect/index.ts
822
822
  import WebSocket, { WebSocketServer } from "ws";
823
- function isSessionOwnedBySubagent(ctx, session, agent) {
824
- if (session.header.origin === "subagent") return true;
825
- const parentId = session.header.parentSession;
826
- if (parentId === void 0 || agent === void 0) return false;
827
- const parent = ctx.agents.get(parentId);
828
- return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
823
+ function isSessionOwnedBySubagent(session) {
824
+ return session.header.origin === "subagent";
829
825
  }
830
826
  function assertNever(value) {
831
827
  throw new Error(`interconnect: unhandled variant ${JSON.stringify(value)}`);
@@ -836,6 +832,7 @@ var DEFAULT_INSTANCE_ID = "dsh";
836
832
  var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
837
833
  var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
838
834
  var LINK_CHANNEL = "/interconnect/link";
835
+ var SELF_PEER_ORIGIN = "self";
839
836
  var MAX_INSTANCE_ID_CHARS = 256;
840
837
  var MAX_REQUEST_ID_CHARS = 256;
841
838
  var MAX_LISTED_SESSIONS = 100;
@@ -963,11 +960,19 @@ var InterconnectService = class extends Service {
963
960
  this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
964
961
  this.delivery = config.delivery ?? "followup";
965
962
  this.allowResume = config.allowResume ?? true;
966
- for (const origin of Object.values(config.peers ?? {})) {
963
+ for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
964
+ if (origin === SELF_PEER_ORIGIN) {
965
+ if (peerInstanceId !== this.instanceId) {
966
+ throw new Error(
967
+ `interconnect: peer "${peerInstanceId}" is configured as self, but this instance announces "${this.instanceId}"; key a self peer by the instanceId`
968
+ );
969
+ }
970
+ continue;
971
+ }
967
972
  linkUrl(trimBase(origin));
968
973
  }
969
974
  for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
970
- this.link(peerInstanceId, origin);
975
+ this.link(peerInstanceId, origin === SELF_PEER_ORIGIN ? () => this.selfOrigin() : origin);
971
976
  }
972
977
  const upgrade = {
973
978
  path: LINK_CHANNEL,
@@ -1108,7 +1113,7 @@ var InterconnectService = class extends Service {
1108
1113
  * Returns the peer identity when reachable, or undefined when the link is
1109
1114
  * not up or no answer arrives.
1110
1115
  * @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
1111
- * @returns the peer's identity, or undefined when the link is not up or no answer arrives.
1116
+ * @returns the peer's identity, or undefined when there is no usable answer.
1112
1117
  */
1113
1118
  async ping(instanceId) {
1114
1119
  return this.queryRequest(instanceId, { kind: "ping" });
@@ -1122,16 +1127,33 @@ var InterconnectService = class extends Service {
1122
1127
  async list(instanceId) {
1123
1128
  return this.queryRequest(instanceId, { kind: "list" });
1124
1129
  }
1130
+ /**
1131
+ * The peers this instance configures, each with the current state of the link
1132
+ * it dials, in configuration order. This is how a caller tells an id this
1133
+ * instance cannot reach at all (`peer-not-configured`) from one whose link is
1134
+ * merely down (`unreachable`), which the bare `undefined` of `ping`/`list`
1135
+ * cannot express. A configured peer whose link is `down` is still a valid
1136
+ * target: its reconnect runs on its own backoff, so it may answer later.
1137
+ * @returns one row per configured peer.
1138
+ */
1139
+ peers() {
1140
+ return [...this.linkStates].map(([instanceId, state]) => ({
1141
+ instanceId,
1142
+ origin: state.peer,
1143
+ link: state.writable() ? "up" : "down"
1144
+ }));
1145
+ }
1125
1146
  /**
1126
1147
  * Send a `msg` frame over the live link to a peer and resolve with its
1127
- * correlated `msg-result`. Undefined when the peer has no live link here
1128
- * (not configured, or the link is down) — sends can only address configured+
1129
- * connected peers by design — and a `message-too-large` result when the
1130
- * encoded frame would not fit the link.
1148
+ * correlated `msg-result`. A `peer-not-configured` result when this instance
1149
+ * configures no route for that id, undefined when the route exists but its
1150
+ * link is down, and a `message-too-large` result when the encoded frame would
1151
+ * not fit the link.
1131
1152
  */
1132
1153
  async msgRequest(instanceId, kind, payload) {
1133
1154
  const state = this.linkStates.get(instanceId);
1134
- if (state === void 0 || !state.writable()) return void 0;
1155
+ if (state === void 0) return { delivered: false, instance: instanceId, reason: "peer-not-configured" };
1156
+ if (!state.writable()) return void 0;
1135
1157
  const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
1136
1158
  const frame = {
1137
1159
  type: "msg",
@@ -1193,7 +1215,8 @@ var InterconnectService = class extends Service {
1193
1215
  * `Config.peers` entry; the resulting state owns reconnection for the life of
1194
1216
  * the service fiber.
1195
1217
  * @param instanceId - the peer's `instanceId`.
1196
- * @param origin - receiver origin this instance dials, e.g. `http://127.0.0.1:13080`.
1218
+ * @param origin - receiver origin this instance dials (e.g.
1219
+ * `http://127.0.0.1:13080`), or a resolver for a `self` route.
1197
1220
  */
1198
1221
  link(instanceId, origin) {
1199
1222
  const state = new LinkState(
@@ -1203,11 +1226,28 @@ var InterconnectService = class extends Service {
1203
1226
  this.ctx.logger,
1204
1227
  () => this.resolveToken(),
1205
1228
  instanceId,
1206
- trimBase(origin)
1229
+ typeof origin === "string" ? trimBase(origin) : origin
1207
1230
  );
1208
1231
  this.linkStates.set(instanceId, state);
1209
1232
  state.dial();
1210
1233
  }
1234
+ /**
1235
+ * The origin of this instance's own host webserver, or undefined while no
1236
+ * listening port exists. Read on every dial rather than cached: the
1237
+ * webserver may activate after this service, its port is assigned only in
1238
+ * its listen callback (`--port 0` picks a fresh one per start), and a
1239
+ * replacement webserver would move it again. Loopback is used regardless of
1240
+ * the bind host: a server bound to `0.0.0.0` also accepts `127.0.0.1`, while
1241
+ * `0.0.0.0` itself is a bind address, not a dialable origin.
1242
+ * @returns `http://127.0.0.1:<port>`, or undefined without a listening server.
1243
+ */
1244
+ selfOrigin() {
1245
+ const server = this.ctx.get("webServer");
1246
+ if (server === void 0) return void 0;
1247
+ const port = server.port;
1248
+ if (typeof port !== "number" || port <= 0) return void 0;
1249
+ return `http://127.0.0.1:${String(port)}`;
1250
+ }
1211
1251
  /** Push one serialized lifecycle fact out to every linked peer over WS. */
1212
1252
  fanout(notification) {
1213
1253
  this.broadcast(notification);
@@ -1342,7 +1382,7 @@ var InterconnectService = class extends Service {
1342
1382
  * @returns the bounded listing.
1343
1383
  */
1344
1384
  listSessions(rowBudgetBytes) {
1345
- const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
1385
+ const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(agent.session));
1346
1386
  const sessions = [];
1347
1387
  let used = 0;
1348
1388
  for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
@@ -1369,7 +1409,8 @@ var InterconnectService = class extends Service {
1369
1409
  let title;
1370
1410
  try {
1371
1411
  const snapshot = this.ctx.get("sessionProjections")?.snapshot(agent.session);
1372
- const value = snapshot?.values.title;
1412
+ const values = snapshot?.values;
1413
+ const value = values?.title;
1373
1414
  if (typeof value === "string" && value !== "") title = value;
1374
1415
  } catch {
1375
1416
  title = void 0;
@@ -1427,7 +1468,7 @@ var InterconnectService = class extends Service {
1427
1468
  }
1428
1469
  agent = woken.agent;
1429
1470
  }
1430
- if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
1471
+ if (isSessionOwnedBySubagent(agent.session)) {
1431
1472
  return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
1432
1473
  }
1433
1474
  const sender = (() => {
@@ -1636,24 +1677,40 @@ var LinkState = class {
1636
1677
  this.logger = logger;
1637
1678
  this.resolveToken = resolveToken;
1638
1679
  this.instanceId = instanceId;
1639
- this.peer = origin;
1680
+ this.origin = origin;
1640
1681
  }
1641
1682
  dialed;
1642
1683
  closed = false;
1643
1684
  retry = 0;
1644
1685
  reconnectTimer;
1645
1686
  warnedNoToken = false;
1646
- /** Receiver origin this link dials. */
1647
- peer;
1687
+ warnedNoOrigin = false;
1648
1688
  instanceId;
1689
+ origin;
1690
+ /** The origin this link dials now; for an unresolved `self` route, the sentinel is shown. */
1691
+ get peer() {
1692
+ return typeof this.origin === "string" ? this.origin : this.origin() ?? SELF_PEER_ORIGIN;
1693
+ }
1649
1694
  /** Open the socket; reconnect is scheduled by the close handler. */
1650
1695
  dial() {
1651
1696
  if (this.closed) return;
1697
+ const origin = typeof this.origin === "string" ? this.origin : this.origin();
1698
+ if (origin === void 0) {
1699
+ if (this.retry >= 1 && !this.warnedNoOrigin) {
1700
+ this.warnedNoOrigin = true;
1701
+ this.logger.warn(
1702
+ `interconnect: peer ${this.instanceId} is configured as self but this instance has no listening webServer port yet; the link stays down and retries`
1703
+ );
1704
+ }
1705
+ this.scheduleReconnect();
1706
+ return;
1707
+ }
1708
+ this.warnedNoOrigin = false;
1652
1709
  let url;
1653
1710
  try {
1654
- url = linkUrl(this.peer);
1711
+ url = linkUrl(origin);
1655
1712
  } catch {
1656
- this.logger.warn(`interconnect: invalid peer origin ${this.peer}; link to ${this.instanceId} stays down`);
1713
+ this.logger.warn(`interconnect: invalid peer origin ${origin}; link to ${this.instanceId} stays down`);
1657
1714
  return;
1658
1715
  }
1659
1716
  void this.resolveToken().then((token) => {