@dev-anywhere/relay 0.6.0 → 0.6.2

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.
@@ -44,7 +44,7 @@
44
44
  <link rel="icon" type="image/svg+xml" href="/brand-icon.svg" />
45
45
  <link rel="icon" type="image/png" sizes="64x64" href="/pwa-64x64.png" />
46
46
  <link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
47
- <script type="module" crossorigin src="/assets/index-CUkAA_Bz.js"></script>
47
+ <script type="module" crossorigin src="/assets/index-Bz4c0NSL.js"></script>
48
48
  <link rel="stylesheet" crossorigin href="/assets/index-Dmm_ocq_.css">
49
49
  <link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
50
50
  <body class="bg-background text-foreground">
package/assets/web/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const c=e=>i(e,t),f={module:{uri:t},exports:o,require:c};s[t]=Promise.all(n.map(e=>f[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-10a3128e"],function(e){"use strict";importScripts("notification-sw.js"),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"notification-sw.js",revision:"ee0be62f4c1f36625b92680b3060becb"},{url:"index.html",revision:"1241d3c81a0acfba2487ecc2bd2ea8ac"},{url:"assets/index-Dmm_ocq_.css",revision:null},{url:"assets/index-CUkAA_Bz.js",revision:null},{url:"assets/fvad-DYxbW2my.wasm",revision:null},{url:"assets/fvad-BVUkJ2pD.js",revision:null},{url:"assets/__vite-browser-external-BIHI7g3E.js",revision:null},{url:"brand-icon.svg",revision:"efe8505a7b7ef12cbb3a4bcd286254bd"},{url:"maskable-icon-512x512.png",revision:"e762ff07cdedcd78f8650f59c6881727"},{url:"pwa-192x192.png",revision:"4e4603ce0f22528eded553ba7f2ffbc5"},{url:"pwa-512x512.png",revision:"e762ff07cdedcd78f8650f59c6881727"},{url:"pwa-64x64.png",revision:"d322160ad1dc08ba9e133cb3d296ae63"},{url:"manifest.webmanifest",revision:"4daa295c44052f4ce59a1fcfd6f06ab8"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/proxy(\/|$|\?)/,/^\/client(\/|$|\?)/,/^\/voice(\/|$|\?)/,/^\/fonts\//,/^\/health/,/^\/status/,/^\/api\//]})),e.registerRoute(/^\/fonts\/.*\.(woff2|woff|ttf|otf|css)$/,new e.CacheFirst({cacheName:"dev-anywhere-fonts",plugins:[new e.ExpirationPlugin({maxEntries:40,maxAgeSeconds:15552e3})]}),"GET")});
1
+ if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const c=e=>i(e,t),f={module:{uri:t},exports:o,require:c};s[t]=Promise.all(n.map(e=>f[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-10a3128e"],function(e){"use strict";importScripts("notification-sw.js"),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"notification-sw.js",revision:"ee0be62f4c1f36625b92680b3060becb"},{url:"index.html",revision:"0a117ca9dcfb6e34e8c330ca09a8f33c"},{url:"assets/index-Dmm_ocq_.css",revision:null},{url:"assets/index-Bz4c0NSL.js",revision:null},{url:"assets/fvad-DYxbW2my.wasm",revision:null},{url:"assets/fvad-CTkpWO2a.js",revision:null},{url:"assets/__vite-browser-external-BIHI7g3E.js",revision:null},{url:"brand-icon.svg",revision:"efe8505a7b7ef12cbb3a4bcd286254bd"},{url:"maskable-icon-512x512.png",revision:"e762ff07cdedcd78f8650f59c6881727"},{url:"pwa-192x192.png",revision:"4e4603ce0f22528eded553ba7f2ffbc5"},{url:"pwa-512x512.png",revision:"e762ff07cdedcd78f8650f59c6881727"},{url:"pwa-64x64.png",revision:"d322160ad1dc08ba9e133cb3d296ae63"},{url:"manifest.webmanifest",revision:"4daa295c44052f4ce59a1fcfd6f06ab8"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/proxy(\/|$|\?)/,/^\/client(\/|$|\?)/,/^\/voice(\/|$|\?)/,/^\/fonts\//,/^\/health/,/^\/status/,/^\/api\//]})),e.registerRoute(/^\/fonts\/.*\.(woff2|woff|ttf|otf|css)$/,new e.CacheFirst({cacheName:"dev-anywhere-fonts",plugins:[new e.ExpirationPlugin({maxEntries:40,maxAgeSeconds:15552e3})]}),"GET")});
@@ -542,7 +542,9 @@ var ControlErrorCode = {
542
542
  PATH_ACCESS_DENIED: "PATH_ACCESS_DENIED",
543
543
  PROXY_OFFLINE: "PROXY_OFFLINE",
544
544
  SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
545
+ SESSION_ALREADY_ACTIVE: "SESSION_ALREADY_ACTIVE",
545
546
  PROVIDER_UNSUPPORTED: "PROVIDER_UNSUPPORTED",
547
+ APPROVAL_POLICY_UNSUPPORTED: "APPROVAL_POLICY_UNSUPPORTED",
546
548
  WORKER_START_FAILED: "WORKER_START_FAILED",
547
549
  PROCESS_START_FAILED: "PROCESS_START_FAILED",
548
550
  UNKNOWN: "UNKNOWN"
@@ -888,9 +890,9 @@ var relayControlDefinitions = [
888
890
  sessionId: IdSchema.optional()
889
891
  }, "client_to_proxy"),
890
892
  // 会话历史浏览
891
- control("session_history_request", RequestIdShape, "client_to_proxy"),
893
+ control("session_history_request", RequiredRequestIdShape, "client_to_proxy"),
892
894
  control("session_history_response", {
893
- ...RequestIdShape,
895
+ ...RequiredRequestIdShape,
894
896
  ...RequestErrorShape,
895
897
  success: z9.boolean(),
896
898
  sessions: z9.array(HistorySessionSchema)
@@ -951,7 +953,8 @@ var relayControlDefinitions = [
951
953
  // 重连或换设备不会隐式重排既有终端内容。
952
954
  cols: z9.number().int().positive().max(PTY_INITIAL_MAX_COLS).optional(),
953
955
  rows: z9.number().int().positive().max(PTY_INITIAL_MAX_ROWS).optional(),
954
- // 透传给 claude CLI --permission-mode, undefined 时 proxy 兜底为 "default"
956
+ // Provider 级审批策略。Claude 未提供时仍使用其 default;Codex 未提供时尊重
957
+ // 用户本机配置,不由 DEV Anywhere 注入覆盖值。
955
958
  permissionMode: z9.enum(["default", "auto", "acceptEdits", "plan", "bypassPermissions", "dontAsk"]).optional()
956
959
  }, "client_to_proxy"),
957
960
  control("session_create_response", {
@@ -964,8 +967,18 @@ var relayControlDefinitions = [
964
967
  mode: z9.enum(sessionModeValues).optional(),
965
968
  provider: z9.enum(providerValues).optional(),
966
969
  ptyOwner: z9.enum(ptyOwnerValues).optional(),
970
+ // Codex 原生 thread 被其他进程持有时返回真实锁持有 PID;无法探测时省略。
971
+ activeWriterPid: z9.number().int().positive().optional(),
967
972
  ...RequestErrorShape
968
973
  }, "proxy_to_client"),
974
+ // 会话创建响应已经成功返回、页面已进入终端后,Provider 仍可能在 bootstrap 阶段
975
+ // 非零退出。把可识别的运行时错误结构化推给 Web,避免关键错误只留在终端闪屏里。
976
+ control("session_runtime_error", {
977
+ sessionId: IdSchema,
978
+ error: z9.string(),
979
+ errorCode: ControlErrorCodeSchema,
980
+ activeWriterPid: z9.number().int().positive().optional()
981
+ }, "proxy_to_client"),
969
982
  // 客户端请求会话历史消息,client -> proxy
970
983
  control("session_messages_request", {
971
984
  ...RequestIdShape,
@@ -1781,7 +1794,7 @@ function rejectNotRegistered(ws) {
1781
1794
  })
1782
1795
  );
1783
1796
  }
1784
- function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, remoteFileBridge) {
1797
+ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, sessionHistoryRoutes, chaos, remoteFileBridge) {
1785
1798
  const proxyWs = ws;
1786
1799
  proxyWs.isAlive = true;
1787
1800
  proxyWs.on("pong", () => {
@@ -1829,7 +1842,10 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, r
1829
1842
  const { proxyId, name } = result.message;
1830
1843
  const status = registry.registerProxy(proxyId, proxyWs, name);
1831
1844
  proxyWs.proxyId = proxyId;
1832
- if (status === "reconnected") ptySnapshotRoutes.clearProxy(proxyId);
1845
+ if (status === "reconnected") {
1846
+ ptySnapshotRoutes.clearProxy(proxyId);
1847
+ sessionHistoryRoutes.clearProxy(proxyId);
1848
+ }
1833
1849
  logger.info({ proxyId, status }, "Proxy registered");
1834
1850
  proxyWs.send(
1835
1851
  serializeControl({
@@ -1846,6 +1862,7 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, r
1846
1862
  if (result.kind === "control" && result.message.type === "proxy_disconnect") {
1847
1863
  if (proxyWs.proxyId) {
1848
1864
  ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
1865
+ sessionHistoryRoutes.clearProxy(proxyWs.proxyId);
1849
1866
  notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
1850
1867
  registry.unregisterProxy(proxyWs.proxyId);
1851
1868
  logger.info(
@@ -1931,6 +1948,56 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, r
1931
1948
  );
1932
1949
  return;
1933
1950
  }
1951
+ if (result.message.type === "session_history_response") {
1952
+ if (!proxyWs.proxyId) {
1953
+ rejectNotRegistered(proxyWs);
1954
+ return;
1955
+ }
1956
+ const route = sessionHistoryRoutes.resolve(
1957
+ proxyWs.proxyId,
1958
+ result.message.requestId,
1959
+ proxyWs
1960
+ );
1961
+ if (route.kind === "matched") {
1962
+ let delivered = 0;
1963
+ for (const target of route.targets) {
1964
+ if (target.clientWs.readyState !== WebSocket4.OPEN) continue;
1965
+ const response = serializeControl({
1966
+ ...result.message,
1967
+ requestId: target.requestId
1968
+ });
1969
+ if (chaos) {
1970
+ chaos.send(target.clientWs, response, {
1971
+ direction: "proxy_to_client",
1972
+ type: result.message.type
1973
+ });
1974
+ } else {
1975
+ target.clientWs.send(response);
1976
+ }
1977
+ delivered += 1;
1978
+ }
1979
+ logger.debug(
1980
+ {
1981
+ proxyId: proxyWs.proxyId,
1982
+ upstreamRequestId: result.message.requestId,
1983
+ waiterCount: route.targets.length,
1984
+ delivered,
1985
+ success: result.message.success
1986
+ },
1987
+ "Session history response fanned out"
1988
+ );
1989
+ return;
1990
+ }
1991
+ logger.debug(
1992
+ {
1993
+ proxyId: proxyWs.proxyId,
1994
+ requestId: result.message.requestId,
1995
+ route: route.kind
1996
+ },
1997
+ "Unmatched session history response dropped"
1998
+ );
1999
+ return;
2000
+ }
1934
2001
  if (isProxyToClientRelayControlType(result.message.type)) {
1935
2002
  if (!proxyWs.proxyId) {
1936
2003
  rejectNotRegistered(proxyWs);
@@ -1990,6 +2057,7 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, r
1990
2057
  return;
1991
2058
  }
1992
2059
  ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
2060
+ sessionHistoryRoutes.clearProxy(proxyWs.proxyId);
1993
2061
  notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
1994
2062
  try {
1995
2063
  registry.transitionProxy(proxyWs.proxyId, "online", "offline");
@@ -2910,7 +2978,7 @@ function sendRemoteFileUploadUrlFailure(ws, requestId, sessionId, error, errorCo
2910
2978
  })
2911
2979
  );
2912
2980
  }
2913
- function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
2981
+ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, sessionHistoryRoutes, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
2914
2982
  const clientWs = ws;
2915
2983
  clientWs.isAlive = true;
2916
2984
  registry.addClientWs(clientWs, connectionInfo);
@@ -3150,6 +3218,60 @@ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, chaos,
3150
3218
  }
3151
3219
  const proxyWs = registry.getProxy(targetProxyId);
3152
3220
  if (proxyWs && proxyWs.readyState === WebSocket7.OPEN) {
3221
+ if (msg.type === "session_history_request") {
3222
+ const registration = sessionHistoryRoutes.register(
3223
+ targetProxyId,
3224
+ msg.requestId,
3225
+ clientWs,
3226
+ proxyWs
3227
+ );
3228
+ if (registration.kind === "duplicate") {
3229
+ logger.debug(
3230
+ { proxyId: targetProxyId, requestId: msg.requestId },
3231
+ "Duplicate session history request suppressed"
3232
+ );
3233
+ return;
3234
+ }
3235
+ if (registration.kind === "joined") {
3236
+ logger.debug(
3237
+ {
3238
+ proxyId: targetProxyId,
3239
+ requestId: msg.requestId,
3240
+ upstreamRequestId: registration.upstreamRequestId
3241
+ },
3242
+ "Session history request joined active upstream flight"
3243
+ );
3244
+ return;
3245
+ }
3246
+ if (registration.kind !== "leader") {
3247
+ logger.warn(
3248
+ { proxyId: targetProxyId, requestId: msg.requestId, registration },
3249
+ "Session history response route rejected"
3250
+ );
3251
+ clientWs.send(
3252
+ JSON.stringify({
3253
+ type: "relay_error",
3254
+ requestId: msg.requestId,
3255
+ code: RelayErrorCode.INVALID_MESSAGE,
3256
+ message: registration.kind === "collision" ? "Session history requestId is already in use" : "Too many pending session history requests"
3257
+ })
3258
+ );
3259
+ return;
3260
+ }
3261
+ const upstreamRequest = serializeControl({
3262
+ type: "session_history_request",
3263
+ requestId: registration.upstreamRequestId
3264
+ });
3265
+ if (chaos) {
3266
+ chaos.send(proxyWs, upstreamRequest, {
3267
+ direction: "client_to_proxy",
3268
+ type: msg.type
3269
+ });
3270
+ } else {
3271
+ proxyWs.send(upstreamRequest);
3272
+ }
3273
+ return;
3274
+ }
3153
3275
  if (msg.type === "session_subscribe") {
3154
3276
  const registration = ptySnapshotRoutes.register(
3155
3277
  targetProxyId,
@@ -3269,6 +3391,7 @@ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, chaos,
3269
3391
  });
3270
3392
  clientWs.on("close", (code, reason) => {
3271
3393
  ptySnapshotRoutes.abandonSocket(clientWs);
3394
+ sessionHistoryRoutes.abandonSocket(clientWs);
3272
3395
  registry.removeClientWs(clientWs);
3273
3396
  if (clientWs.clientId) {
3274
3397
  registry.unbindClientSocket(clientWs.clientId, clientWs);
@@ -4608,6 +4731,217 @@ var PtySnapshotRouteRegistry = class {
4608
4731
  }
4609
4732
  };
4610
4733
 
4734
+ // src/session-history-route-registry.ts
4735
+ import { nanoid as nanoid4 } from "nanoid";
4736
+ var DEFAULT_MAX_ENTRIES2 = 4096;
4737
+ var DEFAULT_MAX_PENDING_PER_CLIENT2 = 64;
4738
+ var DEFAULT_PENDING_TTL_MS2 = 2e4;
4739
+ var DEFAULT_JOIN_WINDOW_MS = 8e3;
4740
+ var DEFAULT_TOMBSTONE_TTL_MS2 = 6e4;
4741
+ function routeKey2(proxyId, requestId) {
4742
+ return JSON.stringify([proxyId, requestId]);
4743
+ }
4744
+ var SessionHistoryRouteRegistry = class {
4745
+ routes = /* @__PURE__ */ new Map();
4746
+ flights = /* @__PURE__ */ new Map();
4747
+ flightTombstones = /* @__PURE__ */ new Map();
4748
+ maxEntries;
4749
+ maxPendingPerClient;
4750
+ pendingTtlMs;
4751
+ joinWindowMs;
4752
+ tombstoneTtlMs;
4753
+ now;
4754
+ upstreamRequestIdFactory;
4755
+ upstreamRequestSequence = 0;
4756
+ constructor(options = {}) {
4757
+ this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES2);
4758
+ this.maxPendingPerClient = Math.max(
4759
+ 1,
4760
+ options.maxPendingPerClient ?? DEFAULT_MAX_PENDING_PER_CLIENT2
4761
+ );
4762
+ this.pendingTtlMs = Math.max(1, options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS2);
4763
+ this.joinWindowMs = Math.max(
4764
+ 1,
4765
+ Math.min(options.joinWindowMs ?? DEFAULT_JOIN_WINDOW_MS, this.pendingTtlMs)
4766
+ );
4767
+ this.tombstoneTtlMs = Math.max(1, options.tombstoneTtlMs ?? DEFAULT_TOMBSTONE_TTL_MS2);
4768
+ this.now = options.now ?? Date.now;
4769
+ this.upstreamRequestIdFactory = options.upstreamRequestIdFactory ?? (() => {
4770
+ this.upstreamRequestSequence += 1;
4771
+ return `relay-history-${this.upstreamRequestSequence.toString(36)}-${nanoid4(12)}`;
4772
+ });
4773
+ }
4774
+ register(proxyId, requestId, clientWs, proxyWs) {
4775
+ const now = this.now();
4776
+ this.pruneExpired(now);
4777
+ const key = routeKey2(proxyId, requestId);
4778
+ const existing = this.routes.get(key);
4779
+ if (existing) {
4780
+ if (existing.state === "pending" && existing.clientWs === clientWs && existing.proxyWs === proxyWs) {
4781
+ return { kind: "duplicate" };
4782
+ }
4783
+ return { kind: "collision" };
4784
+ }
4785
+ let clientPendingCount = 0;
4786
+ for (const route of this.routes.values()) {
4787
+ if (route.state === "pending" && route.clientWs === clientWs) {
4788
+ clientPendingCount += 1;
4789
+ if (clientPendingCount >= this.maxPendingPerClient) {
4790
+ return { kind: "client_capacity_exceeded" };
4791
+ }
4792
+ }
4793
+ }
4794
+ if (this.routes.size >= this.maxEntries && !this.evictOldestRouteTombstone()) {
4795
+ return { kind: "capacity_exceeded" };
4796
+ }
4797
+ let flight = this.flights.get(proxyId);
4798
+ if (flight && flight.proxyWs !== proxyWs) {
4799
+ this.expireFlight(flight, now);
4800
+ flight = void 0;
4801
+ }
4802
+ let kind;
4803
+ if (!flight || flight.joinUntil <= now) {
4804
+ const replacement = this.createFlight(proxyId, proxyWs, now);
4805
+ if (flight) this.migrateFlight(flight, replacement, now);
4806
+ this.flights.set(proxyId, replacement);
4807
+ flight = replacement;
4808
+ kind = "leader";
4809
+ } else {
4810
+ kind = "joined";
4811
+ }
4812
+ const routeExpiresAt = now + this.pendingTtlMs;
4813
+ flight.requestIds.add(requestId);
4814
+ flight.expiresAt = Math.max(flight.expiresAt, routeExpiresAt);
4815
+ this.routes.set(key, {
4816
+ state: "pending",
4817
+ proxyId,
4818
+ proxyWs,
4819
+ clientWs,
4820
+ upstreamRequestId: flight.upstreamRequestId,
4821
+ expiresAt: routeExpiresAt
4822
+ });
4823
+ return { kind, upstreamRequestId: flight.upstreamRequestId };
4824
+ }
4825
+ resolve(proxyId, requestId, proxyWs) {
4826
+ const now = this.now();
4827
+ this.pruneExpired(now);
4828
+ const flight = this.flights.get(proxyId);
4829
+ if (!flight || flight.upstreamRequestId !== requestId) {
4830
+ return this.flightTombstones.has(routeKey2(proxyId, requestId)) ? { kind: "tombstone" } : { kind: "unmatched" };
4831
+ }
4832
+ if (flight.proxyWs !== proxyWs) return { kind: "stale_proxy" };
4833
+ this.flights.delete(proxyId);
4834
+ this.setFlightTombstone(flight, now);
4835
+ const targets = [];
4836
+ for (const externalRequestId of flight.requestIds) {
4837
+ const key = routeKey2(proxyId, externalRequestId);
4838
+ const route = this.routes.get(key);
4839
+ if (!route || route.state !== "pending" || route.proxyWs !== proxyWs || route.upstreamRequestId !== requestId) {
4840
+ continue;
4841
+ }
4842
+ this.routes.set(key, {
4843
+ state: "tombstone",
4844
+ proxyId,
4845
+ expiresAt: now + this.tombstoneTtlMs
4846
+ });
4847
+ targets.push({ clientWs: route.clientWs, requestId: externalRequestId });
4848
+ }
4849
+ return { kind: "matched", targets };
4850
+ }
4851
+ abandonSocket(clientWs) {
4852
+ const now = this.now();
4853
+ this.pruneExpired(now);
4854
+ for (const [key, route] of this.routes) {
4855
+ if (route.state === "pending" && route.clientWs === clientWs) {
4856
+ this.routes.set(key, {
4857
+ state: "tombstone",
4858
+ proxyId: route.proxyId,
4859
+ expiresAt: now + this.tombstoneTtlMs
4860
+ });
4861
+ }
4862
+ }
4863
+ }
4864
+ clearProxy(proxyId) {
4865
+ for (const [key, route] of this.routes) {
4866
+ if (route.proxyId === proxyId) this.routes.delete(key);
4867
+ }
4868
+ this.flights.delete(proxyId);
4869
+ for (const [key, tombstone] of this.flightTombstones) {
4870
+ if (tombstone.proxyId === proxyId) this.flightTombstones.delete(key);
4871
+ }
4872
+ }
4873
+ createFlight(proxyId, proxyWs, now) {
4874
+ return {
4875
+ proxyId,
4876
+ proxyWs,
4877
+ upstreamRequestId: this.upstreamRequestIdFactory(),
4878
+ requestIds: /* @__PURE__ */ new Set(),
4879
+ joinUntil: now + this.joinWindowMs,
4880
+ expiresAt: now + this.pendingTtlMs
4881
+ };
4882
+ }
4883
+ migrateFlight(previous, replacement, now) {
4884
+ if (this.flights.get(previous.proxyId) === previous) this.flights.delete(previous.proxyId);
4885
+ this.setFlightTombstone(previous, now);
4886
+ for (const externalRequestId of previous.requestIds) {
4887
+ const key = routeKey2(previous.proxyId, externalRequestId);
4888
+ const route = this.routes.get(key);
4889
+ if (!route || route.state !== "pending" || route.proxyWs !== previous.proxyWs || route.upstreamRequestId !== previous.upstreamRequestId) {
4890
+ continue;
4891
+ }
4892
+ route.upstreamRequestId = replacement.upstreamRequestId;
4893
+ replacement.requestIds.add(externalRequestId);
4894
+ replacement.expiresAt = Math.max(replacement.expiresAt, route.expiresAt);
4895
+ }
4896
+ }
4897
+ expireFlight(flight, now) {
4898
+ if (this.flights.get(flight.proxyId) === flight) this.flights.delete(flight.proxyId);
4899
+ this.setFlightTombstone(flight, now);
4900
+ for (const externalRequestId of flight.requestIds) {
4901
+ const key = routeKey2(flight.proxyId, externalRequestId);
4902
+ const route = this.routes.get(key);
4903
+ if (route?.state === "pending" && route.upstreamRequestId === flight.upstreamRequestId) {
4904
+ this.routes.delete(key);
4905
+ }
4906
+ }
4907
+ }
4908
+ setFlightTombstone(flight, now) {
4909
+ const key = routeKey2(flight.proxyId, flight.upstreamRequestId);
4910
+ if (!this.flightTombstones.has(key) && this.flightTombstones.size >= this.maxEntries) {
4911
+ const oldest = this.flightTombstones.keys().next().value;
4912
+ if (oldest !== void 0) this.flightTombstones.delete(oldest);
4913
+ }
4914
+ this.flightTombstones.set(key, {
4915
+ proxyId: flight.proxyId,
4916
+ expiresAt: now + this.tombstoneTtlMs
4917
+ });
4918
+ }
4919
+ evictOldestRouteTombstone() {
4920
+ let oldestTombstoneKey;
4921
+ let oldestTombstoneExpiry = Number.POSITIVE_INFINITY;
4922
+ for (const [candidateKey, route] of this.routes) {
4923
+ if (route.state === "tombstone" && route.expiresAt < oldestTombstoneExpiry) {
4924
+ oldestTombstoneKey = candidateKey;
4925
+ oldestTombstoneExpiry = route.expiresAt;
4926
+ }
4927
+ }
4928
+ if (!oldestTombstoneKey) return false;
4929
+ this.routes.delete(oldestTombstoneKey);
4930
+ return true;
4931
+ }
4932
+ pruneExpired(now) {
4933
+ for (const flight of this.flights.values()) {
4934
+ if (flight.expiresAt <= now) this.expireFlight(flight, now);
4935
+ }
4936
+ for (const [key, route] of this.routes) {
4937
+ if (route.expiresAt <= now) this.routes.delete(key);
4938
+ }
4939
+ for (const [key, tombstone] of this.flightTombstones) {
4940
+ if (tombstone.expiresAt <= now) this.flightTombstones.delete(key);
4941
+ }
4942
+ }
4943
+ };
4944
+
4611
4945
  // src/server.ts
4612
4946
  var MODULE_DIR = dirname3(fileURLToPath2(import.meta.url));
4613
4947
  var PACKAGED_FONTS_DIR = resolve(MODULE_DIR, "../assets/fonts");
@@ -4653,6 +4987,7 @@ function createRelayServer(options) {
4653
4987
  }
4654
4988
  const registry = new RelayRegistry();
4655
4989
  const ptySnapshotRoutes = new PtySnapshotRouteRegistry();
4990
+ const sessionHistoryRoutes = new SessionHistoryRouteRegistry();
4656
4991
  const remoteFileBridge = new RemoteFileBridge({ registry, logger });
4657
4992
  const voiceConfigStore = createVoiceConfigStore({
4658
4993
  dataDir,
@@ -4796,7 +5131,15 @@ function createRelayServer(options) {
4796
5131
  socket.destroy();
4797
5132
  });
4798
5133
  proxyWss.on("connection", (ws) => {
4799
- handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, relayChaos, remoteFileBridge);
5134
+ handleProxyConnection(
5135
+ ws,
5136
+ registry,
5137
+ logger,
5138
+ ptySnapshotRoutes,
5139
+ sessionHistoryRoutes,
5140
+ relayChaos,
5141
+ remoteFileBridge
5142
+ );
4800
5143
  });
4801
5144
  clientWss.on("connection", (ws, request) => {
4802
5145
  handleClientConnection(
@@ -4804,6 +5147,7 @@ function createRelayServer(options) {
4804
5147
  registry,
4805
5148
  logger,
4806
5149
  ptySnapshotRoutes,
5150
+ sessionHistoryRoutes,
4807
5151
  relayChaos,
4808
5152
  voiceConfigStore,
4809
5153
  voiceProviders,
@@ -4899,4 +5243,4 @@ export {
4899
5243
  parseRelayChaosFromEnv,
4900
5244
  createRelayServer
4901
5245
  };
4902
- //# sourceMappingURL=chunk-7YWKWUNZ.js.map
5246
+ //# sourceMappingURL=chunk-SDL4V2RE.js.map