@dev-anywhere/relay 0.5.46 → 0.6.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.
@@ -889,7 +889,12 @@ var relayControlDefinitions = [
889
889
  }, "client_to_proxy"),
890
890
  // 会话历史浏览
891
891
  control("session_history_request", RequestIdShape, "client_to_proxy"),
892
- control("session_history_response", { ...RequestIdShape, sessions: z9.array(HistorySessionSchema) }, "proxy_to_client"),
892
+ control("session_history_response", {
893
+ ...RequestIdShape,
894
+ ...RequestErrorShape,
895
+ success: z9.boolean(),
896
+ sessions: z9.array(HistorySessionSchema)
897
+ }, "proxy_to_client"),
893
898
  // PTY 语义状态,从 Envelope 迁移到 Control 层
894
899
  control("pty_state", { sessionId: IdSchema, payload: PtyStatePayloadSchema }, "proxy_to_client"),
895
900
  // Provider 语义状态,来自 Claude/Codex hook 等结构化事件,不从 PTY 字节推断
@@ -1045,7 +1050,7 @@ var relayControlDefinitions = [
1045
1050
  }))
1046
1051
  }),
1047
1052
  // PTY 会话订阅,client -> proxy,触发 terminal serialize() 返回当前状态
1048
- control("session_subscribe", { sessionId: IdSchema, requestId: IdSchema.optional() }, "client_to_proxy"),
1053
+ control("session_subscribe", { sessionId: IdSchema, requestId: IdSchema }, "client_to_proxy"),
1049
1054
  // PTY 会话快照,proxy -> client,serialize() 的全量终端状态
1050
1055
  control("session_snapshot", {
1051
1056
  sessionId: IdSchema,
@@ -1053,7 +1058,7 @@ var relayControlDefinitions = [
1053
1058
  rows: z9.number().int().positive(),
1054
1059
  data: z9.string(),
1055
1060
  outputSeq: z9.number().int().nonnegative(),
1056
- requestId: IdSchema.optional()
1061
+ requestId: IdSchema
1057
1062
  }, "proxy_to_client")
1058
1063
  ];
1059
1064
  var relayControlSchemas = relayControlDefinitions.map((definition) => definition.schema);
@@ -1776,7 +1781,7 @@ function rejectNotRegistered(ws) {
1776
1781
  })
1777
1782
  );
1778
1783
  }
1779
- function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1784
+ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, remoteFileBridge) {
1780
1785
  const proxyWs = ws;
1781
1786
  proxyWs.isAlive = true;
1782
1787
  proxyWs.on("pong", () => {
@@ -1806,7 +1811,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1806
1811
  const clients = registry.getClientsForProxy(proxyWs.proxyId);
1807
1812
  for (const clientWs of clients) {
1808
1813
  if (clientWs.readyState === WebSocket4.OPEN) {
1809
- clientWs.send(data);
1814
+ clientWs.send(data, { binary: true, compress: false });
1810
1815
  }
1811
1816
  }
1812
1817
  return;
@@ -1824,6 +1829,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1824
1829
  const { proxyId, name } = result.message;
1825
1830
  const status = registry.registerProxy(proxyId, proxyWs, name);
1826
1831
  proxyWs.proxyId = proxyId;
1832
+ if (status === "reconnected") ptySnapshotRoutes.clearProxy(proxyId);
1827
1833
  logger.info({ proxyId, status }, "Proxy registered");
1828
1834
  proxyWs.send(
1829
1835
  serializeControl({
@@ -1839,6 +1845,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1839
1845
  }
1840
1846
  if (result.kind === "control" && result.message.type === "proxy_disconnect") {
1841
1847
  if (proxyWs.proxyId) {
1848
+ ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
1842
1849
  notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
1843
1850
  registry.unregisterProxy(proxyWs.proxyId);
1844
1851
  logger.info(
@@ -1895,6 +1902,35 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1895
1902
  remoteFileBridge?.handleProxyControl(proxyWs.proxyId, result.message);
1896
1903
  return;
1897
1904
  }
1905
+ if (proxyWs.proxyId && result.message.type === "session_snapshot") {
1906
+ const route = ptySnapshotRoutes.resolve(
1907
+ proxyWs.proxyId,
1908
+ result.message.sessionId,
1909
+ result.message.requestId,
1910
+ proxyWs
1911
+ );
1912
+ if (route.kind === "matched" && route.clientWs.readyState === WebSocket4.OPEN) {
1913
+ if (chaos) {
1914
+ chaos.send(route.clientWs, raw, {
1915
+ direction: "proxy_to_client",
1916
+ type: result.message.type
1917
+ });
1918
+ } else {
1919
+ route.clientWs.send(raw);
1920
+ }
1921
+ return;
1922
+ }
1923
+ logger.debug(
1924
+ {
1925
+ proxyId: proxyWs.proxyId,
1926
+ sessionId: result.message.sessionId,
1927
+ requestId: result.message.requestId,
1928
+ route: route.kind
1929
+ },
1930
+ "Unmatched PTY snapshot dropped"
1931
+ );
1932
+ return;
1933
+ }
1898
1934
  if (isProxyToClientRelayControlType(result.message.type)) {
1899
1935
  if (!proxyWs.proxyId) {
1900
1936
  rejectNotRegistered(proxyWs);
@@ -1953,6 +1989,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1953
1989
  );
1954
1990
  return;
1955
1991
  }
1992
+ ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
1956
1993
  notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
1957
1994
  try {
1958
1995
  registry.transitionProxy(proxyWs.proxyId, "online", "offline");
@@ -2873,7 +2910,7 @@ function sendRemoteFileUploadUrlFailure(ws, requestId, sessionId, error, errorCo
2873
2910
  })
2874
2911
  );
2875
2912
  }
2876
- function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
2913
+ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
2877
2914
  const clientWs = ws;
2878
2915
  clientWs.isAlive = true;
2879
2916
  registry.addClientWs(clientWs, connectionInfo);
@@ -3113,6 +3150,48 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
3113
3150
  }
3114
3151
  const proxyWs = registry.getProxy(targetProxyId);
3115
3152
  if (proxyWs && proxyWs.readyState === WebSocket7.OPEN) {
3153
+ if (msg.type === "session_subscribe") {
3154
+ const registration = ptySnapshotRoutes.register(
3155
+ targetProxyId,
3156
+ msg.sessionId,
3157
+ msg.requestId,
3158
+ clientWs,
3159
+ proxyWs
3160
+ );
3161
+ if (registration === "duplicate") {
3162
+ logger.debug(
3163
+ { proxyId: targetProxyId, sessionId: msg.sessionId, requestId: msg.requestId },
3164
+ "Duplicate PTY snapshot subscribe suppressed"
3165
+ );
3166
+ return;
3167
+ }
3168
+ if (registration !== "registered" && registration !== "retry_due") {
3169
+ logger.warn(
3170
+ {
3171
+ proxyId: targetProxyId,
3172
+ sessionId: msg.sessionId,
3173
+ requestId: msg.requestId,
3174
+ registration
3175
+ },
3176
+ "PTY snapshot subscribe route rejected"
3177
+ );
3178
+ clientWs.send(
3179
+ JSON.stringify({
3180
+ type: "relay_error",
3181
+ requestId: msg.requestId,
3182
+ code: RelayErrorCode.INVALID_MESSAGE,
3183
+ message: registration === "collision" ? "PTY snapshot requestId is already in use" : "Too many pending PTY snapshot requests"
3184
+ })
3185
+ );
3186
+ return;
3187
+ }
3188
+ if (registration === "retry_due") {
3189
+ logger.debug(
3190
+ { proxyId: targetProxyId, sessionId: msg.sessionId, requestId: msg.requestId },
3191
+ "Retrying unanswered PTY snapshot subscribe"
3192
+ );
3193
+ }
3194
+ }
3116
3195
  if (chaos) chaos.send(proxyWs, raw, { direction: "client_to_proxy", type: msg.type });
3117
3196
  else proxyWs.send(raw);
3118
3197
  } else {
@@ -3189,6 +3268,7 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
3189
3268
  }
3190
3269
  });
3191
3270
  clientWs.on("close", (code, reason) => {
3271
+ ptySnapshotRoutes.abandonSocket(clientWs);
3192
3272
  registry.removeClientWs(clientWs);
3193
3273
  if (clientWs.clientId) {
3194
3274
  registry.unbindClientSocket(clientWs.clientId, clientWs);
@@ -4095,7 +4175,7 @@ var RemoteFileBridge = class {
4095
4175
  req.pause();
4096
4176
  const frame = encodeFileStreamFrame(uploadId, pending.chunkSeq, Buffer.from(chunk));
4097
4177
  pending.chunkSeq += 1;
4098
- proxyWs.send(frame, { binary: true }, (err) => {
4178
+ proxyWs.send(frame, { binary: true, compress: false }, (err) => {
4099
4179
  if (err) {
4100
4180
  this.failUpload(uploadId, 502, err.message);
4101
4181
  return;
@@ -4411,10 +4491,138 @@ function mountWebApp(app, options) {
4411
4491
  return true;
4412
4492
  }
4413
4493
 
4494
+ // src/pty-snapshot-route-registry.ts
4495
+ var DEFAULT_MAX_ENTRIES = 4096;
4496
+ var DEFAULT_MAX_PENDING_PER_CLIENT = 64;
4497
+ var DEFAULT_RETRY_FORWARD_INTERVAL_MS = 25e3;
4498
+ var DEFAULT_PENDING_TTL_MS = 5 * 6e4;
4499
+ var DEFAULT_TOMBSTONE_TTL_MS = 12e4;
4500
+ function routeKey(proxyId, sessionId, requestId) {
4501
+ return JSON.stringify([proxyId, sessionId, requestId]);
4502
+ }
4503
+ var PtySnapshotRouteRegistry = class {
4504
+ routes = /* @__PURE__ */ new Map();
4505
+ maxEntries;
4506
+ maxPendingPerClient;
4507
+ retryForwardIntervalMs;
4508
+ pendingTtlMs;
4509
+ tombstoneTtlMs;
4510
+ now;
4511
+ constructor(options = {}) {
4512
+ this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES);
4513
+ this.maxPendingPerClient = Math.max(
4514
+ 1,
4515
+ options.maxPendingPerClient ?? DEFAULT_MAX_PENDING_PER_CLIENT
4516
+ );
4517
+ this.retryForwardIntervalMs = Math.max(
4518
+ 1,
4519
+ options.retryForwardIntervalMs ?? DEFAULT_RETRY_FORWARD_INTERVAL_MS
4520
+ );
4521
+ this.pendingTtlMs = Math.max(1, options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS);
4522
+ this.tombstoneTtlMs = Math.max(1, options.tombstoneTtlMs ?? DEFAULT_TOMBSTONE_TTL_MS);
4523
+ this.now = options.now ?? Date.now;
4524
+ }
4525
+ register(proxyId, sessionId, requestId, clientWs, proxyWs) {
4526
+ const now = this.now();
4527
+ this.pruneExpired(now);
4528
+ const key = routeKey(proxyId, sessionId, requestId);
4529
+ const existing = this.routes.get(key);
4530
+ if (existing) {
4531
+ if (existing.state === "pending" && existing.clientWs === clientWs && existing.proxyWs === proxyWs) {
4532
+ if (now - existing.lastForwardedAt >= this.retryForwardIntervalMs) {
4533
+ existing.lastForwardedAt = now;
4534
+ return "retry_due";
4535
+ }
4536
+ return "duplicate";
4537
+ }
4538
+ return "collision";
4539
+ }
4540
+ let clientPendingCount = 0;
4541
+ for (const route of this.routes.values()) {
4542
+ if (route.state === "pending" && route.clientWs === clientWs) {
4543
+ clientPendingCount += 1;
4544
+ if (clientPendingCount >= this.maxPendingPerClient) {
4545
+ return "client_capacity_exceeded";
4546
+ }
4547
+ }
4548
+ }
4549
+ if (this.routes.size >= this.maxEntries) {
4550
+ let oldestTombstoneKey;
4551
+ let oldestTombstoneExpiry = Number.POSITIVE_INFINITY;
4552
+ for (const [candidateKey, route] of this.routes) {
4553
+ if (route.state === "tombstone" && route.expiresAt < oldestTombstoneExpiry) {
4554
+ oldestTombstoneKey = candidateKey;
4555
+ oldestTombstoneExpiry = route.expiresAt;
4556
+ }
4557
+ }
4558
+ if (oldestTombstoneKey) this.routes.delete(oldestTombstoneKey);
4559
+ else return "capacity_exceeded";
4560
+ }
4561
+ this.routes.set(key, {
4562
+ state: "pending",
4563
+ proxyId,
4564
+ proxyWs,
4565
+ clientWs,
4566
+ lastForwardedAt: now,
4567
+ expiresAt: now + this.pendingTtlMs
4568
+ });
4569
+ return "registered";
4570
+ }
4571
+ resolve(proxyId, sessionId, requestId, proxyWs) {
4572
+ const now = this.now();
4573
+ this.pruneExpired(now);
4574
+ const key = routeKey(proxyId, sessionId, requestId);
4575
+ const route = this.routes.get(key);
4576
+ if (!route) return { kind: "unmatched" };
4577
+ if (route.state === "tombstone") return { kind: "tombstone" };
4578
+ if (route.proxyWs !== proxyWs) return { kind: "stale_proxy" };
4579
+ this.routes.set(key, {
4580
+ state: "tombstone",
4581
+ proxyId,
4582
+ expiresAt: now + this.tombstoneTtlMs
4583
+ });
4584
+ return { kind: "matched", clientWs: route.clientWs };
4585
+ }
4586
+ abandonSocket(clientWs) {
4587
+ const now = this.now();
4588
+ this.pruneExpired(now);
4589
+ for (const [key, route] of this.routes) {
4590
+ if (route.state === "pending" && route.clientWs === clientWs) {
4591
+ this.routes.set(key, {
4592
+ state: "tombstone",
4593
+ proxyId: route.proxyId,
4594
+ expiresAt: now + this.tombstoneTtlMs
4595
+ });
4596
+ }
4597
+ }
4598
+ }
4599
+ clearProxy(proxyId) {
4600
+ for (const [key, route] of this.routes) {
4601
+ if (route.proxyId === proxyId) this.routes.delete(key);
4602
+ }
4603
+ }
4604
+ pruneExpired(now) {
4605
+ for (const [key, route] of this.routes) {
4606
+ if (route.expiresAt <= now) this.routes.delete(key);
4607
+ }
4608
+ }
4609
+ };
4610
+
4414
4611
  // src/server.ts
4415
4612
  var MODULE_DIR = dirname3(fileURLToPath2(import.meta.url));
4416
4613
  var PACKAGED_FONTS_DIR = resolve(MODULE_DIR, "../assets/fonts");
4417
4614
  var PACKAGED_WEB_DIR = resolve(MODULE_DIR, "../assets/web");
4615
+ var DATA_CHANNEL_MAX_PAYLOAD_BYTES = 10 * 1024 * 1024;
4616
+ var DATA_CHANNEL_COMPRESSION = {
4617
+ clientNoContextTakeover: true,
4618
+ serverNoContextTakeover: true,
4619
+ threshold: 32 * 1024,
4620
+ concurrencyLimit: 4,
4621
+ zlibDeflateOptions: {
4622
+ level: 3,
4623
+ memLevel: 7
4624
+ }
4625
+ };
4418
4626
  function firstHeaderValue(value) {
4419
4627
  if (Array.isArray(value)) return value[0];
4420
4628
  return value;
@@ -4444,6 +4652,7 @@ function createRelayServer(options) {
4444
4652
  );
4445
4653
  }
4446
4654
  const registry = new RelayRegistry();
4655
+ const ptySnapshotRoutes = new PtySnapshotRouteRegistry();
4447
4656
  const remoteFileBridge = new RemoteFileBridge({ registry, logger });
4448
4657
  const voiceConfigStore = createVoiceConfigStore({
4449
4658
  dataDir,
@@ -4513,8 +4722,16 @@ function createRelayServer(options) {
4513
4722
  mountWebApp(app, { webAssetDir, logger });
4514
4723
  }
4515
4724
  const httpServer = createServer(app);
4516
- const proxyWss = new WebSocketServer({ noServer: true });
4517
- const clientWss = new WebSocketServer({ noServer: true });
4725
+ const proxyWss = new WebSocketServer({
4726
+ noServer: true,
4727
+ maxPayload: DATA_CHANNEL_MAX_PAYLOAD_BYTES,
4728
+ perMessageDeflate: DATA_CHANNEL_COMPRESSION
4729
+ });
4730
+ const clientWss = new WebSocketServer({
4731
+ noServer: true,
4732
+ maxPayload: DATA_CHANNEL_MAX_PAYLOAD_BYTES,
4733
+ perMessageDeflate: DATA_CHANNEL_COMPRESSION
4734
+ });
4518
4735
  const voiceAsrWss = new WebSocketServer({ noServer: true });
4519
4736
  const voiceTtsWss = new WebSocketServer({ noServer: true });
4520
4737
  httpServer.on("upgrade", (request, socket, head) => {
@@ -4579,13 +4796,14 @@ function createRelayServer(options) {
4579
4796
  socket.destroy();
4580
4797
  });
4581
4798
  proxyWss.on("connection", (ws) => {
4582
- handleProxyConnection(ws, registry, logger, relayChaos, remoteFileBridge);
4799
+ handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, relayChaos, remoteFileBridge);
4583
4800
  });
4584
4801
  clientWss.on("connection", (ws, request) => {
4585
4802
  handleClientConnection(
4586
4803
  ws,
4587
4804
  registry,
4588
4805
  logger,
4806
+ ptySnapshotRoutes,
4589
4807
  relayChaos,
4590
4808
  voiceConfigStore,
4591
4809
  voiceProviders,
@@ -4681,4 +4899,4 @@ export {
4681
4899
  parseRelayChaosFromEnv,
4682
4900
  createRelayServer
4683
4901
  };
4684
- //# sourceMappingURL=chunk-7KWO37WD.js.map
4902
+ //# sourceMappingURL=chunk-7YWKWUNZ.js.map