@dev-anywhere/relay 0.6.0 → 0.6.1

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-CXT4gEaR.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:"aa5f2f91104adcc66cc14f85c755800f"},{url:"assets/index-Dmm_ocq_.css",revision:null},{url:"assets/index-CXT4gEaR.js",revision:null},{url:"assets/fvad-mBAhtF0y.js",revision:null},{url:"assets/fvad-DYxbW2my.wasm",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")});
@@ -888,9 +888,9 @@ var relayControlDefinitions = [
888
888
  sessionId: IdSchema.optional()
889
889
  }, "client_to_proxy"),
890
890
  // 会话历史浏览
891
- control("session_history_request", RequestIdShape, "client_to_proxy"),
891
+ control("session_history_request", RequiredRequestIdShape, "client_to_proxy"),
892
892
  control("session_history_response", {
893
- ...RequestIdShape,
893
+ ...RequiredRequestIdShape,
894
894
  ...RequestErrorShape,
895
895
  success: z9.boolean(),
896
896
  sessions: z9.array(HistorySessionSchema)
@@ -1781,7 +1781,7 @@ function rejectNotRegistered(ws) {
1781
1781
  })
1782
1782
  );
1783
1783
  }
1784
- function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, remoteFileBridge) {
1784
+ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, sessionHistoryRoutes, chaos, remoteFileBridge) {
1785
1785
  const proxyWs = ws;
1786
1786
  proxyWs.isAlive = true;
1787
1787
  proxyWs.on("pong", () => {
@@ -1829,7 +1829,10 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, r
1829
1829
  const { proxyId, name } = result.message;
1830
1830
  const status = registry.registerProxy(proxyId, proxyWs, name);
1831
1831
  proxyWs.proxyId = proxyId;
1832
- if (status === "reconnected") ptySnapshotRoutes.clearProxy(proxyId);
1832
+ if (status === "reconnected") {
1833
+ ptySnapshotRoutes.clearProxy(proxyId);
1834
+ sessionHistoryRoutes.clearProxy(proxyId);
1835
+ }
1833
1836
  logger.info({ proxyId, status }, "Proxy registered");
1834
1837
  proxyWs.send(
1835
1838
  serializeControl({
@@ -1846,6 +1849,7 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, r
1846
1849
  if (result.kind === "control" && result.message.type === "proxy_disconnect") {
1847
1850
  if (proxyWs.proxyId) {
1848
1851
  ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
1852
+ sessionHistoryRoutes.clearProxy(proxyWs.proxyId);
1849
1853
  notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
1850
1854
  registry.unregisterProxy(proxyWs.proxyId);
1851
1855
  logger.info(
@@ -1931,6 +1935,56 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, r
1931
1935
  );
1932
1936
  return;
1933
1937
  }
1938
+ if (result.message.type === "session_history_response") {
1939
+ if (!proxyWs.proxyId) {
1940
+ rejectNotRegistered(proxyWs);
1941
+ return;
1942
+ }
1943
+ const route = sessionHistoryRoutes.resolve(
1944
+ proxyWs.proxyId,
1945
+ result.message.requestId,
1946
+ proxyWs
1947
+ );
1948
+ if (route.kind === "matched") {
1949
+ let delivered = 0;
1950
+ for (const target of route.targets) {
1951
+ if (target.clientWs.readyState !== WebSocket4.OPEN) continue;
1952
+ const response = serializeControl({
1953
+ ...result.message,
1954
+ requestId: target.requestId
1955
+ });
1956
+ if (chaos) {
1957
+ chaos.send(target.clientWs, response, {
1958
+ direction: "proxy_to_client",
1959
+ type: result.message.type
1960
+ });
1961
+ } else {
1962
+ target.clientWs.send(response);
1963
+ }
1964
+ delivered += 1;
1965
+ }
1966
+ logger.debug(
1967
+ {
1968
+ proxyId: proxyWs.proxyId,
1969
+ upstreamRequestId: result.message.requestId,
1970
+ waiterCount: route.targets.length,
1971
+ delivered,
1972
+ success: result.message.success
1973
+ },
1974
+ "Session history response fanned out"
1975
+ );
1976
+ return;
1977
+ }
1978
+ logger.debug(
1979
+ {
1980
+ proxyId: proxyWs.proxyId,
1981
+ requestId: result.message.requestId,
1982
+ route: route.kind
1983
+ },
1984
+ "Unmatched session history response dropped"
1985
+ );
1986
+ return;
1987
+ }
1934
1988
  if (isProxyToClientRelayControlType(result.message.type)) {
1935
1989
  if (!proxyWs.proxyId) {
1936
1990
  rejectNotRegistered(proxyWs);
@@ -1990,6 +2044,7 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, r
1990
2044
  return;
1991
2045
  }
1992
2046
  ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
2047
+ sessionHistoryRoutes.clearProxy(proxyWs.proxyId);
1993
2048
  notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
1994
2049
  try {
1995
2050
  registry.transitionProxy(proxyWs.proxyId, "online", "offline");
@@ -2910,7 +2965,7 @@ function sendRemoteFileUploadUrlFailure(ws, requestId, sessionId, error, errorCo
2910
2965
  })
2911
2966
  );
2912
2967
  }
2913
- function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
2968
+ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, sessionHistoryRoutes, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
2914
2969
  const clientWs = ws;
2915
2970
  clientWs.isAlive = true;
2916
2971
  registry.addClientWs(clientWs, connectionInfo);
@@ -3150,6 +3205,60 @@ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, chaos,
3150
3205
  }
3151
3206
  const proxyWs = registry.getProxy(targetProxyId);
3152
3207
  if (proxyWs && proxyWs.readyState === WebSocket7.OPEN) {
3208
+ if (msg.type === "session_history_request") {
3209
+ const registration = sessionHistoryRoutes.register(
3210
+ targetProxyId,
3211
+ msg.requestId,
3212
+ clientWs,
3213
+ proxyWs
3214
+ );
3215
+ if (registration.kind === "duplicate") {
3216
+ logger.debug(
3217
+ { proxyId: targetProxyId, requestId: msg.requestId },
3218
+ "Duplicate session history request suppressed"
3219
+ );
3220
+ return;
3221
+ }
3222
+ if (registration.kind === "joined") {
3223
+ logger.debug(
3224
+ {
3225
+ proxyId: targetProxyId,
3226
+ requestId: msg.requestId,
3227
+ upstreamRequestId: registration.upstreamRequestId
3228
+ },
3229
+ "Session history request joined active upstream flight"
3230
+ );
3231
+ return;
3232
+ }
3233
+ if (registration.kind !== "leader") {
3234
+ logger.warn(
3235
+ { proxyId: targetProxyId, requestId: msg.requestId, registration },
3236
+ "Session history response route rejected"
3237
+ );
3238
+ clientWs.send(
3239
+ JSON.stringify({
3240
+ type: "relay_error",
3241
+ requestId: msg.requestId,
3242
+ code: RelayErrorCode.INVALID_MESSAGE,
3243
+ message: registration.kind === "collision" ? "Session history requestId is already in use" : "Too many pending session history requests"
3244
+ })
3245
+ );
3246
+ return;
3247
+ }
3248
+ const upstreamRequest = serializeControl({
3249
+ type: "session_history_request",
3250
+ requestId: registration.upstreamRequestId
3251
+ });
3252
+ if (chaos) {
3253
+ chaos.send(proxyWs, upstreamRequest, {
3254
+ direction: "client_to_proxy",
3255
+ type: msg.type
3256
+ });
3257
+ } else {
3258
+ proxyWs.send(upstreamRequest);
3259
+ }
3260
+ return;
3261
+ }
3153
3262
  if (msg.type === "session_subscribe") {
3154
3263
  const registration = ptySnapshotRoutes.register(
3155
3264
  targetProxyId,
@@ -3269,6 +3378,7 @@ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, chaos,
3269
3378
  });
3270
3379
  clientWs.on("close", (code, reason) => {
3271
3380
  ptySnapshotRoutes.abandonSocket(clientWs);
3381
+ sessionHistoryRoutes.abandonSocket(clientWs);
3272
3382
  registry.removeClientWs(clientWs);
3273
3383
  if (clientWs.clientId) {
3274
3384
  registry.unbindClientSocket(clientWs.clientId, clientWs);
@@ -4608,6 +4718,217 @@ var PtySnapshotRouteRegistry = class {
4608
4718
  }
4609
4719
  };
4610
4720
 
4721
+ // src/session-history-route-registry.ts
4722
+ import { nanoid as nanoid4 } from "nanoid";
4723
+ var DEFAULT_MAX_ENTRIES2 = 4096;
4724
+ var DEFAULT_MAX_PENDING_PER_CLIENT2 = 64;
4725
+ var DEFAULT_PENDING_TTL_MS2 = 2e4;
4726
+ var DEFAULT_JOIN_WINDOW_MS = 8e3;
4727
+ var DEFAULT_TOMBSTONE_TTL_MS2 = 6e4;
4728
+ function routeKey2(proxyId, requestId) {
4729
+ return JSON.stringify([proxyId, requestId]);
4730
+ }
4731
+ var SessionHistoryRouteRegistry = class {
4732
+ routes = /* @__PURE__ */ new Map();
4733
+ flights = /* @__PURE__ */ new Map();
4734
+ flightTombstones = /* @__PURE__ */ new Map();
4735
+ maxEntries;
4736
+ maxPendingPerClient;
4737
+ pendingTtlMs;
4738
+ joinWindowMs;
4739
+ tombstoneTtlMs;
4740
+ now;
4741
+ upstreamRequestIdFactory;
4742
+ upstreamRequestSequence = 0;
4743
+ constructor(options = {}) {
4744
+ this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES2);
4745
+ this.maxPendingPerClient = Math.max(
4746
+ 1,
4747
+ options.maxPendingPerClient ?? DEFAULT_MAX_PENDING_PER_CLIENT2
4748
+ );
4749
+ this.pendingTtlMs = Math.max(1, options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS2);
4750
+ this.joinWindowMs = Math.max(
4751
+ 1,
4752
+ Math.min(options.joinWindowMs ?? DEFAULT_JOIN_WINDOW_MS, this.pendingTtlMs)
4753
+ );
4754
+ this.tombstoneTtlMs = Math.max(1, options.tombstoneTtlMs ?? DEFAULT_TOMBSTONE_TTL_MS2);
4755
+ this.now = options.now ?? Date.now;
4756
+ this.upstreamRequestIdFactory = options.upstreamRequestIdFactory ?? (() => {
4757
+ this.upstreamRequestSequence += 1;
4758
+ return `relay-history-${this.upstreamRequestSequence.toString(36)}-${nanoid4(12)}`;
4759
+ });
4760
+ }
4761
+ register(proxyId, requestId, clientWs, proxyWs) {
4762
+ const now = this.now();
4763
+ this.pruneExpired(now);
4764
+ const key = routeKey2(proxyId, requestId);
4765
+ const existing = this.routes.get(key);
4766
+ if (existing) {
4767
+ if (existing.state === "pending" && existing.clientWs === clientWs && existing.proxyWs === proxyWs) {
4768
+ return { kind: "duplicate" };
4769
+ }
4770
+ return { kind: "collision" };
4771
+ }
4772
+ let clientPendingCount = 0;
4773
+ for (const route of this.routes.values()) {
4774
+ if (route.state === "pending" && route.clientWs === clientWs) {
4775
+ clientPendingCount += 1;
4776
+ if (clientPendingCount >= this.maxPendingPerClient) {
4777
+ return { kind: "client_capacity_exceeded" };
4778
+ }
4779
+ }
4780
+ }
4781
+ if (this.routes.size >= this.maxEntries && !this.evictOldestRouteTombstone()) {
4782
+ return { kind: "capacity_exceeded" };
4783
+ }
4784
+ let flight = this.flights.get(proxyId);
4785
+ if (flight && flight.proxyWs !== proxyWs) {
4786
+ this.expireFlight(flight, now);
4787
+ flight = void 0;
4788
+ }
4789
+ let kind;
4790
+ if (!flight || flight.joinUntil <= now) {
4791
+ const replacement = this.createFlight(proxyId, proxyWs, now);
4792
+ if (flight) this.migrateFlight(flight, replacement, now);
4793
+ this.flights.set(proxyId, replacement);
4794
+ flight = replacement;
4795
+ kind = "leader";
4796
+ } else {
4797
+ kind = "joined";
4798
+ }
4799
+ const routeExpiresAt = now + this.pendingTtlMs;
4800
+ flight.requestIds.add(requestId);
4801
+ flight.expiresAt = Math.max(flight.expiresAt, routeExpiresAt);
4802
+ this.routes.set(key, {
4803
+ state: "pending",
4804
+ proxyId,
4805
+ proxyWs,
4806
+ clientWs,
4807
+ upstreamRequestId: flight.upstreamRequestId,
4808
+ expiresAt: routeExpiresAt
4809
+ });
4810
+ return { kind, upstreamRequestId: flight.upstreamRequestId };
4811
+ }
4812
+ resolve(proxyId, requestId, proxyWs) {
4813
+ const now = this.now();
4814
+ this.pruneExpired(now);
4815
+ const flight = this.flights.get(proxyId);
4816
+ if (!flight || flight.upstreamRequestId !== requestId) {
4817
+ return this.flightTombstones.has(routeKey2(proxyId, requestId)) ? { kind: "tombstone" } : { kind: "unmatched" };
4818
+ }
4819
+ if (flight.proxyWs !== proxyWs) return { kind: "stale_proxy" };
4820
+ this.flights.delete(proxyId);
4821
+ this.setFlightTombstone(flight, now);
4822
+ const targets = [];
4823
+ for (const externalRequestId of flight.requestIds) {
4824
+ const key = routeKey2(proxyId, externalRequestId);
4825
+ const route = this.routes.get(key);
4826
+ if (!route || route.state !== "pending" || route.proxyWs !== proxyWs || route.upstreamRequestId !== requestId) {
4827
+ continue;
4828
+ }
4829
+ this.routes.set(key, {
4830
+ state: "tombstone",
4831
+ proxyId,
4832
+ expiresAt: now + this.tombstoneTtlMs
4833
+ });
4834
+ targets.push({ clientWs: route.clientWs, requestId: externalRequestId });
4835
+ }
4836
+ return { kind: "matched", targets };
4837
+ }
4838
+ abandonSocket(clientWs) {
4839
+ const now = this.now();
4840
+ this.pruneExpired(now);
4841
+ for (const [key, route] of this.routes) {
4842
+ if (route.state === "pending" && route.clientWs === clientWs) {
4843
+ this.routes.set(key, {
4844
+ state: "tombstone",
4845
+ proxyId: route.proxyId,
4846
+ expiresAt: now + this.tombstoneTtlMs
4847
+ });
4848
+ }
4849
+ }
4850
+ }
4851
+ clearProxy(proxyId) {
4852
+ for (const [key, route] of this.routes) {
4853
+ if (route.proxyId === proxyId) this.routes.delete(key);
4854
+ }
4855
+ this.flights.delete(proxyId);
4856
+ for (const [key, tombstone] of this.flightTombstones) {
4857
+ if (tombstone.proxyId === proxyId) this.flightTombstones.delete(key);
4858
+ }
4859
+ }
4860
+ createFlight(proxyId, proxyWs, now) {
4861
+ return {
4862
+ proxyId,
4863
+ proxyWs,
4864
+ upstreamRequestId: this.upstreamRequestIdFactory(),
4865
+ requestIds: /* @__PURE__ */ new Set(),
4866
+ joinUntil: now + this.joinWindowMs,
4867
+ expiresAt: now + this.pendingTtlMs
4868
+ };
4869
+ }
4870
+ migrateFlight(previous, replacement, now) {
4871
+ if (this.flights.get(previous.proxyId) === previous) this.flights.delete(previous.proxyId);
4872
+ this.setFlightTombstone(previous, now);
4873
+ for (const externalRequestId of previous.requestIds) {
4874
+ const key = routeKey2(previous.proxyId, externalRequestId);
4875
+ const route = this.routes.get(key);
4876
+ if (!route || route.state !== "pending" || route.proxyWs !== previous.proxyWs || route.upstreamRequestId !== previous.upstreamRequestId) {
4877
+ continue;
4878
+ }
4879
+ route.upstreamRequestId = replacement.upstreamRequestId;
4880
+ replacement.requestIds.add(externalRequestId);
4881
+ replacement.expiresAt = Math.max(replacement.expiresAt, route.expiresAt);
4882
+ }
4883
+ }
4884
+ expireFlight(flight, now) {
4885
+ if (this.flights.get(flight.proxyId) === flight) this.flights.delete(flight.proxyId);
4886
+ this.setFlightTombstone(flight, now);
4887
+ for (const externalRequestId of flight.requestIds) {
4888
+ const key = routeKey2(flight.proxyId, externalRequestId);
4889
+ const route = this.routes.get(key);
4890
+ if (route?.state === "pending" && route.upstreamRequestId === flight.upstreamRequestId) {
4891
+ this.routes.delete(key);
4892
+ }
4893
+ }
4894
+ }
4895
+ setFlightTombstone(flight, now) {
4896
+ const key = routeKey2(flight.proxyId, flight.upstreamRequestId);
4897
+ if (!this.flightTombstones.has(key) && this.flightTombstones.size >= this.maxEntries) {
4898
+ const oldest = this.flightTombstones.keys().next().value;
4899
+ if (oldest !== void 0) this.flightTombstones.delete(oldest);
4900
+ }
4901
+ this.flightTombstones.set(key, {
4902
+ proxyId: flight.proxyId,
4903
+ expiresAt: now + this.tombstoneTtlMs
4904
+ });
4905
+ }
4906
+ evictOldestRouteTombstone() {
4907
+ let oldestTombstoneKey;
4908
+ let oldestTombstoneExpiry = Number.POSITIVE_INFINITY;
4909
+ for (const [candidateKey, route] of this.routes) {
4910
+ if (route.state === "tombstone" && route.expiresAt < oldestTombstoneExpiry) {
4911
+ oldestTombstoneKey = candidateKey;
4912
+ oldestTombstoneExpiry = route.expiresAt;
4913
+ }
4914
+ }
4915
+ if (!oldestTombstoneKey) return false;
4916
+ this.routes.delete(oldestTombstoneKey);
4917
+ return true;
4918
+ }
4919
+ pruneExpired(now) {
4920
+ for (const flight of this.flights.values()) {
4921
+ if (flight.expiresAt <= now) this.expireFlight(flight, now);
4922
+ }
4923
+ for (const [key, route] of this.routes) {
4924
+ if (route.expiresAt <= now) this.routes.delete(key);
4925
+ }
4926
+ for (const [key, tombstone] of this.flightTombstones) {
4927
+ if (tombstone.expiresAt <= now) this.flightTombstones.delete(key);
4928
+ }
4929
+ }
4930
+ };
4931
+
4611
4932
  // src/server.ts
4612
4933
  var MODULE_DIR = dirname3(fileURLToPath2(import.meta.url));
4613
4934
  var PACKAGED_FONTS_DIR = resolve(MODULE_DIR, "../assets/fonts");
@@ -4653,6 +4974,7 @@ function createRelayServer(options) {
4653
4974
  }
4654
4975
  const registry = new RelayRegistry();
4655
4976
  const ptySnapshotRoutes = new PtySnapshotRouteRegistry();
4977
+ const sessionHistoryRoutes = new SessionHistoryRouteRegistry();
4656
4978
  const remoteFileBridge = new RemoteFileBridge({ registry, logger });
4657
4979
  const voiceConfigStore = createVoiceConfigStore({
4658
4980
  dataDir,
@@ -4796,7 +5118,15 @@ function createRelayServer(options) {
4796
5118
  socket.destroy();
4797
5119
  });
4798
5120
  proxyWss.on("connection", (ws) => {
4799
- handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, relayChaos, remoteFileBridge);
5121
+ handleProxyConnection(
5122
+ ws,
5123
+ registry,
5124
+ logger,
5125
+ ptySnapshotRoutes,
5126
+ sessionHistoryRoutes,
5127
+ relayChaos,
5128
+ remoteFileBridge
5129
+ );
4800
5130
  });
4801
5131
  clientWss.on("connection", (ws, request) => {
4802
5132
  handleClientConnection(
@@ -4804,6 +5134,7 @@ function createRelayServer(options) {
4804
5134
  registry,
4805
5135
  logger,
4806
5136
  ptySnapshotRoutes,
5137
+ sessionHistoryRoutes,
4807
5138
  relayChaos,
4808
5139
  voiceConfigStore,
4809
5140
  voiceProviders,
@@ -4899,4 +5230,4 @@ export {
4899
5230
  parseRelayChaosFromEnv,
4900
5231
  createRelayServer
4901
5232
  };
4902
- //# sourceMappingURL=chunk-7YWKWUNZ.js.map
5233
+ //# sourceMappingURL=chunk-TWQN6HNC.js.map