codex-mobile-re 0.1.136 → 0.1.138

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/dist/index.html CHANGED
@@ -36,8 +36,8 @@
36
36
  <link rel="preload" as="font" type="font/woff2" href="/fonts/ibm-plex-sans-400.woff2" crossorigin />
37
37
  <link rel="preload" as="font" type="font/woff2" href="/fonts/ibm-plex-mono-400.woff2" crossorigin />
38
38
  <title>Codex Web</title>
39
- <script type="module" crossorigin src="/assets/index-ChD_mFyW.js"></script>
40
- <link rel="stylesheet" crossorigin href="/assets/index-CuSbagu4.css">
39
+ <script type="module" crossorigin src="/assets/index-B7NhUyvj.js"></script>
40
+ <link rel="stylesheet" crossorigin href="/assets/index-CKSFHnPK.css">
41
41
  </head>
42
42
  <body>
43
43
  <div id="app"></div>
package/dist/sw.js CHANGED
@@ -1,26 +1,49 @@
1
- const CACHE_NAME = 'codexweb-shell-v2'
1
+ // round-183:把缓存名绑到构建产物上,并让安装过程不会因为一次瞬时失败而卡死。
2
+ //
3
+ // 背景(线上现象):重部署后有些标签页会「坏一阵子,清掉应用缓存就好了」。
4
+ // 两条叠加的原因:
5
+ // 1) CACHE_NAME 写死成固定字符串(旧值以 `v2` 结尾)⇒ sw.js 的字节在两次构建之间**完全
6
+ // 相同**,浏览器不会判定 SW 需要更新,所以既不会重新 install,也不会在
7
+ // activate 里清掉旧缓存 ⇒ 旧 shell 与旧 chunk 可以一直留着(旧标签页请求
8
+ // 已下线的 chunk 时,networkFirstStatic 还会从缓存里把旧文件喂回去,
9
+ // 于是整页继续跑旧构建,而服务端早已是新版本)。
10
+ // 2) install 里用了 cache.addAll:任一资源失败就整体 reject ⇒ 新 SW 装不上、
11
+ // 旧 SW 继续服务。重部署瞬间最容易踩这个。
12
+ //
13
+ // 修法:BUILD_ID 由构建期注入(见 vite.config.ts 的 codex-sw-version 插件),
14
+ // 每次产物变化 ⇒ sw.js 字节变化 ⇒ 走标准更新流程 ⇒ activate 清掉所有非当前
15
+ // 版本的缓存;install 改成逐个 add 且各自吞错。
16
+ const BUILD_ID = 'cc0efc82b5'
17
+ const CACHE_NAME = `codexweb-shell-${BUILD_ID}`
2
18
  const APP_SHELL_PATHS = ['/', '/manifest.webmanifest']
3
19
  const STATIC_DESTINATIONS = new Set(['document', 'script', 'style', 'image', 'font'])
4
20
  const BYPASS_PREFIXES = ['/codex-api/', '/codex-local-image', '/codex-local-file', '/codex-local-browse/', '/codex-local-edit/']
5
21
 
6
22
  self.addEventListener('install', (event) => {
7
23
  event.waitUntil(
8
- caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL_PATHS)),
24
+ (async () => {
25
+ const cache = await caches.open(CACHE_NAME)
26
+ // 逐个装入:单个资源失败不该让新版本装不上(旧行为是 addAll 整体 reject)。
27
+ await Promise.all(
28
+ APP_SHELL_PATHS.map((path) => cache.add(path).catch(() => undefined)),
29
+ )
30
+ })(),
9
31
  )
10
32
  self.skipWaiting()
11
33
  })
12
34
 
13
35
  self.addEventListener('activate', (event) => {
14
36
  event.waitUntil(
15
- caches.keys().then((keys) =>
16
- Promise.all(
37
+ (async () => {
38
+ const keys = await caches.keys()
39
+ await Promise.all(
17
40
  keys
18
41
  .filter((key) => key !== CACHE_NAME)
19
42
  .map((key) => caches.delete(key)),
20
- ),
21
- ),
43
+ )
44
+ await self.clients.claim()
45
+ })(),
22
46
  )
23
- self.clients.claim()
24
47
  })
25
48
 
26
49
  self.addEventListener('fetch', (event) => {
@@ -50,9 +73,12 @@ async function networkFirstNavigation(request) {
50
73
  const cache = await caches.open(CACHE_NAME)
51
74
  try {
52
75
  const response = await fetch(request)
53
- cache.put('/', response.clone())
76
+ if (response.ok) {
77
+ cache.put('/', response.clone())
78
+ }
54
79
  return response
55
80
  } catch {
81
+ // 仅真正的离线才回退缓存;此时缓存一定是本构建写入的(缓存名带 BUILD_ID)。
56
82
  return (await cache.match('/')) || Response.error()
57
83
  }
58
84
  }
package/dist-cli/index.js CHANGED
@@ -7,7 +7,7 @@ import { readFile as readFile20, stat as stat19, writeFile as writeFile20 } from
7
7
  import { homedir as homedir11, networkInterfaces } from "os";
8
8
  import { isAbsolute as isAbsolute13, join as join32, resolve as resolve8 } from "path";
9
9
  import { spawn as spawn9 } from "child_process";
10
- import { createInterface as createInterface2 } from "readline/promises";
10
+ import { createInterface as createInterface3 } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
12
  import { dirname as dirname12 } from "path";
13
13
  import { get as httpsGet } from "https";
@@ -10712,10 +10712,36 @@ import { readFile as readFile10, stat as stat11 } from "fs/promises";
10712
10712
  // src/server/bridge/session.ts
10713
10713
  import { mkdir as mkdir8, readFile as readFile9, rename as rename2, rm as rm6, stat as stat9, writeFile as writeFile8 } from "fs/promises";
10714
10714
  import { dirname as dirname4, isAbsolute as isAbsolute4, join as join17 } from "path";
10715
+
10716
+ // src/server/bridge/sessionLogScan.ts
10717
+ import { createReadStream as createReadStream3 } from "fs";
10718
+ import { createInterface } from "readline";
10719
+ var SESSION_LOG_READ_CHUNK_BYTES = 64 * 1024;
10720
+ async function* iterateSessionLogLines(sessionPath) {
10721
+ const stream = createReadStream3(sessionPath, {
10722
+ encoding: "utf8",
10723
+ highWaterMark: SESSION_LOG_READ_CHUNK_BYTES
10724
+ });
10725
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
10726
+ try {
10727
+ for await (const line of reader) yield line;
10728
+ } finally {
10729
+ reader.close();
10730
+ stream.destroy();
10731
+ }
10732
+ }
10733
+ async function readSessionLogFirstLine(sessionPath) {
10734
+ for await (const line of iterateSessionLogLines(sessionPath)) return line;
10735
+ return "";
10736
+ }
10737
+
10738
+ // src/server/bridge/session.ts
10715
10739
  var SESSION_SKILL_INPUT_CACHE_LIMIT = 64;
10716
10740
  var sessionSkillInputCache = /* @__PURE__ */ new Map();
10741
+ var sessionSkillInputScanInFlight = /* @__PURE__ */ new Map();
10717
10742
  var SESSION_LOG_RECOVERY_CACHE_LIMIT = 64;
10718
10743
  var sessionLogRecoveryCache = /* @__PURE__ */ new Map();
10744
+ var sessionLogRecoveryScanInFlight = /* @__PURE__ */ new Map();
10719
10745
  function rememberSessionLogRecovery(sessionPath, size, mtimeMs, recoverable) {
10720
10746
  sessionLogRecoveryCache.delete(sessionPath);
10721
10747
  sessionLogRecoveryCache.set(sessionPath, { size, mtimeMs, recoverable });
@@ -10733,10 +10759,10 @@ function parseSessionSkillText(value) {
10733
10759
  if (!name || !path) return null;
10734
10760
  return { name, path };
10735
10761
  }
10736
- function buildSessionSkillInputsByTurn(sessionLogRaw) {
10762
+ async function buildSessionSkillInputsByTurn(lines) {
10737
10763
  let currentTurnId = "";
10738
10764
  const skillsByTurnId = /* @__PURE__ */ new Map();
10739
- for (const line of sessionLogRaw.split("\n")) {
10765
+ for await (const line of lines) {
10740
10766
  if (!line.trim()) continue;
10741
10767
  let row = null;
10742
10768
  try {
@@ -10780,18 +10806,30 @@ async function readCachedSessionSkillInputsByTurn(sessionPath) {
10780
10806
  if (cached && cached.size === sessionStat.size && cached.mtimeMs === sessionStat.mtimeMs) {
10781
10807
  return cached.skillsByTurnId;
10782
10808
  }
10783
- const sessionLogRaw = await readFile9(sessionPath, "utf8");
10784
- const skillsByTurnId = buildSessionSkillInputsByTurn(sessionLogRaw);
10785
- sessionSkillInputCache.set(sessionPath, {
10786
- size: sessionStat.size,
10787
- mtimeMs: sessionStat.mtimeMs,
10788
- skillsByTurnId
10789
- });
10790
- if (sessionSkillInputCache.size > SESSION_SKILL_INPUT_CACHE_LIMIT) {
10791
- const oldestKey = sessionSkillInputCache.keys().next().value;
10792
- if (oldestKey) sessionSkillInputCache.delete(oldestKey);
10809
+ const scanKey = `${sessionPath}\0${sessionStat.size}\0${sessionStat.mtimeMs}`;
10810
+ const inFlight = sessionSkillInputScanInFlight.get(scanKey);
10811
+ if (inFlight) return await inFlight;
10812
+ const scan = (async () => {
10813
+ const skillsByTurnId = await buildSessionSkillInputsByTurn(iterateSessionLogLines(sessionPath));
10814
+ sessionSkillInputCache.delete(sessionPath);
10815
+ sessionSkillInputCache.set(sessionPath, {
10816
+ size: sessionStat.size,
10817
+ mtimeMs: sessionStat.mtimeMs,
10818
+ skillsByTurnId
10819
+ });
10820
+ while (sessionSkillInputCache.size > SESSION_SKILL_INPUT_CACHE_LIMIT) {
10821
+ const oldestKey = sessionSkillInputCache.keys().next().value;
10822
+ if (!oldestKey) break;
10823
+ sessionSkillInputCache.delete(oldestKey);
10824
+ }
10825
+ return skillsByTurnId;
10826
+ })();
10827
+ sessionSkillInputScanInFlight.set(scanKey, scan);
10828
+ try {
10829
+ return await scan;
10830
+ } finally {
10831
+ sessionSkillInputScanInFlight.delete(scanKey);
10793
10832
  }
10794
- return skillsByTurnId;
10795
10833
  }
10796
10834
  function mergeSessionSkillInputsIntoTurnsFromMap(turns, skillsByTurnId) {
10797
10835
  const turnIds = /* @__PURE__ */ new Set();
@@ -10988,7 +11026,7 @@ function parseApplyPatchInput(input) {
10988
11026
  }
10989
11027
  return changes;
10990
11028
  }
10991
- function buildSessionFileChangeFallback(threadReadPayload, sessionLogRaw) {
11029
+ async function buildSessionFileChangeFallback(threadReadPayload, lines) {
10992
11030
  const payload = asRecord(threadReadPayload);
10993
11031
  const thread = asRecord(payload?.thread);
10994
11032
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
@@ -11002,7 +11040,7 @@ function buildSessionFileChangeFallback(threadReadPayload, sessionLogRaw) {
11002
11040
  }
11003
11041
  const collectedByTurnId = /* @__PURE__ */ new Map();
11004
11042
  let currentTurnId = "";
11005
- for (const line of sessionLogRaw.split("\n")) {
11043
+ for await (const line of lines) {
11006
11044
  if (!line.trim()) continue;
11007
11045
  let row = null;
11008
11046
  try {
@@ -11082,12 +11120,12 @@ function isRecoverableToolPayload(payload) {
11082
11120
  if (payload.type === "function_call") return name === "exec_command" || name === "shell_command";
11083
11121
  return payload.type === "custom_tool_call" && name === "apply_patch";
11084
11122
  }
11085
- function buildSessionItemOrder(sessionLogRaw, turnIds) {
11123
+ async function buildSessionItemOrder(lines, turnIds) {
11086
11124
  let currentTurnId = "";
11087
11125
  const orderByTurnId = /* @__PURE__ */ new Map();
11088
11126
  const callIdToCommand = /* @__PURE__ */ new Map();
11089
11127
  let sawRecoverableToolRow = false;
11090
- for (const line of sessionLogRaw.split("\n")) {
11128
+ for await (const line of lines) {
11091
11129
  if (!line.trim()) continue;
11092
11130
  let row = null;
11093
11131
  try {
@@ -11201,10 +11239,10 @@ function extractFilePathsFromCommand(cmd, cwd) {
11201
11239
  }
11202
11240
  return [...new Set(paths)];
11203
11241
  }
11204
- function collectFileChangesForTurns(sessionLogRaw, turnIdsToRevert, cwd) {
11242
+ async function collectFileChangesForTurns(lines, turnIdsToRevert, cwd) {
11205
11243
  let currentTurnId = "";
11206
11244
  const infoByTurnId = /* @__PURE__ */ new Map();
11207
- for (const line of sessionLogRaw.split("\n")) {
11245
+ for await (const line of lines) {
11208
11246
  if (!line.trim()) continue;
11209
11247
  let row = null;
11210
11248
  try {
@@ -11703,9 +11741,18 @@ async function mergeSessionCommandsIntoTurnsFromPath(turns, sessionPath) {
11703
11741
  if (cached && cached.size === sessionStat.size && cached.mtimeMs === sessionStat.mtimeMs && !cached.recoverable) {
11704
11742
  return turns;
11705
11743
  }
11706
- const sessionLogRaw = await readFile9(sessionPath, "utf8");
11707
- const { orderByTurnId, sawRecoverableToolRow } = buildSessionItemOrder(sessionLogRaw, turnIds);
11708
- rememberSessionLogRecovery(sessionPath, sessionStat.size, sessionStat.mtimeMs, sawRecoverableToolRow);
11744
+ const scanKey = `${sessionPath}\0${sessionStat.size}\0${sessionStat.mtimeMs}\0${[...turnIds].sort().join(",")}`;
11745
+ let scan = sessionLogRecoveryScanInFlight.get(scanKey);
11746
+ if (!scan) {
11747
+ scan = (async () => {
11748
+ const { orderByTurnId: orderByTurnId2, sawRecoverableToolRow } = await buildSessionItemOrder(iterateSessionLogLines(sessionPath), turnIds);
11749
+ rememberSessionLogRecovery(sessionPath, sessionStat.size, sessionStat.mtimeMs, sawRecoverableToolRow);
11750
+ return orderByTurnId2;
11751
+ })();
11752
+ sessionLogRecoveryScanInFlight.set(scanKey, scan);
11753
+ void scan.catch(() => void 0).finally(() => sessionLogRecoveryScanInFlight.delete(scanKey));
11754
+ }
11755
+ const orderByTurnId = await scan;
11709
11756
  return mergeSessionCommandsIntoTurnsFromOrder(turns, orderByTurnId);
11710
11757
  }
11711
11758
  async function mergeSessionCommandsIntoThreadResult(result) {
@@ -12001,8 +12048,8 @@ function handleThreadHttpRequest(req, res, url, deps) {
12001
12048
  return true;
12002
12049
  }
12003
12050
  try {
12004
- const sessionLogRaw = await readFile10(sessionPath, "utf8");
12005
- setJson2(res, 200, { data: buildSessionFileChangeFallback(threadReadResult, sessionLogRaw) });
12051
+ const data = await buildSessionFileChangeFallback(threadReadResult, iterateSessionLogLines(sessionPath));
12052
+ setJson2(res, 200, { data });
12006
12053
  } catch {
12007
12054
  setJson2(res, 200, { data: [] });
12008
12055
  }
@@ -12415,10 +12462,10 @@ async function handleTelegramHttpRequest(req, res, url, deps) {
12415
12462
  }
12416
12463
 
12417
12464
  // src/server/bridge/threadPreferencesRoutes.ts
12418
- import { createReadStream as createReadStream3 } from "fs";
12465
+ import { createReadStream as createReadStream4 } from "fs";
12419
12466
  import { readFile as readFile13, stat as stat12, writeFile as writeFile12 } from "fs/promises";
12420
12467
  import { join as join21 } from "path";
12421
- import { createInterface } from "readline";
12468
+ import { createInterface as createInterface2 } from "readline";
12422
12469
  var MAX_THREAD_TITLES = 500;
12423
12470
  var MAX_REASONING_MESSAGES_PER_THREAD = 20;
12424
12471
  var MAX_TURN_DURATIONS_PER_THREAD = 200;
@@ -12692,8 +12739,8 @@ function getSessionIndexFileSignature(stats) {
12692
12739
  }
12693
12740
  async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
12694
12741
  const latestById = /* @__PURE__ */ new Map();
12695
- const input = createReadStream3(sessionIndexPath, { encoding: "utf8" });
12696
- const lines = createInterface({
12742
+ const input = createReadStream4(sessionIndexPath, { encoding: "utf8" });
12743
+ const lines = createInterface2({
12697
12744
  input,
12698
12745
  crlfDelay: Infinity
12699
12746
  });
@@ -12908,7 +12955,7 @@ data: ${JSON.stringify({ ok: true })}
12908
12955
  // src/server/bridge/projectZip.ts
12909
12956
  import { randomUUID as randomUUID3 } from "crypto";
12910
12957
  import { existsSync as existsSync8 } from "fs";
12911
- import { mkdir as mkdir10, readFile as readFile15, realpath as realpath3, utimes, writeFile as writeFile14 } from "fs/promises";
12958
+ import { mkdir as mkdir10, realpath as realpath3, utimes, writeFile as writeFile14 } from "fs/promises";
12912
12959
  import { basename as basename7, dirname as dirname5, extname as extname3, isAbsolute as isAbsolute7, join as join23, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
12913
12960
 
12914
12961
  // src/server/bridge/importedSessions.ts
@@ -13495,13 +13542,13 @@ async function collectProjectChatZipEntries(projectRoot) {
13495
13542
  for (const root of roots) {
13496
13543
  for await (const sessionPath of walkFiles(root.disk)) {
13497
13544
  if (extname3(sessionPath) !== ".jsonl") continue;
13498
- let raw = "";
13545
+ let metaLine = "";
13499
13546
  try {
13500
- raw = await readFile15(sessionPath, "utf8");
13547
+ metaLine = await readSessionLogFirstLine(sessionPath);
13501
13548
  } catch {
13502
13549
  continue;
13503
13550
  }
13504
- const sessionCwd = readSessionMetaCwd(raw);
13551
+ const sessionCwd = readSessionMetaCwd(metaLine);
13505
13552
  if (!sessionCwd) continue;
13506
13553
  let canonicalSessionCwd = "";
13507
13554
  try {
@@ -13512,7 +13559,7 @@ async function collectProjectChatZipEntries(projectRoot) {
13512
13559
  if (!isSameOrDescendantPath(canonicalSessionCwd, canonicalProjectRoot)) continue;
13513
13560
  const rel = relative3(root.disk, sessionPath).split(sep3).join("/");
13514
13561
  const zipPath = `${root.zip}/${rel}`;
13515
- const sessionId = readSessionMetaId(raw);
13562
+ const sessionId = readSessionMetaId(metaLine);
13516
13563
  const stateMetadata = sessionId ? stateDbThreadMetadata.get(sessionId) : void 0;
13517
13564
  const title = readNonEmptyString(stateMetadata?.title) || (sessionId ? readNonEmptyString(threadTitles.titles[sessionId]) : "");
13518
13565
  if (title) exportedTitles[zipPath] = title;
@@ -15521,6 +15568,7 @@ var MERGEABLE_ITEM_TYPES = /* @__PURE__ */ new Set([
15521
15568
  "commandExecution",
15522
15569
  "fileChange"
15523
15570
  ]);
15571
+ var THREAD_READ_SNAPSHOT_LIMIT = 8;
15524
15572
  var AppServerProcess = class {
15525
15573
  constructor() {
15526
15574
  this.process = null;
@@ -15722,7 +15770,13 @@ var AppServerProcess = class {
15722
15770
  return buffer.slice(-limit);
15723
15771
  }
15724
15772
  storeThreadReadSnapshot(threadId, snapshot) {
15773
+ this.lastThreadReadSnapshotByThreadId.delete(threadId);
15725
15774
  this.lastThreadReadSnapshotByThreadId.set(threadId, snapshot);
15775
+ while (this.lastThreadReadSnapshotByThreadId.size > THREAD_READ_SNAPSHOT_LIMIT) {
15776
+ const oldestThreadId = this.lastThreadReadSnapshotByThreadId.keys().next().value;
15777
+ if (!oldestThreadId) break;
15778
+ this.lastThreadReadSnapshotByThreadId.delete(oldestThreadId);
15779
+ }
15726
15780
  this.invalidateBoundedThreadTurnPageCache(threadId);
15727
15781
  }
15728
15782
  getLastThreadReadSnapshot(threadId) {
@@ -16959,14 +17013,17 @@ function createCodexBridgeMiddleware() {
16959
17013
  setJson(res, 200, { reverted: 0, errors: [], message: "No turns to revert" });
16960
17014
  return;
16961
17015
  }
16962
- let sessionLogRaw;
17016
+ let turnInfos;
16963
17017
  try {
16964
- sessionLogRaw = await readFile19(sessionPath, "utf8");
17018
+ turnInfos = await collectFileChangesForTurns(
17019
+ iterateSessionLogLines(sessionPath),
17020
+ turnIdsToRevert,
17021
+ cwd
17022
+ );
16965
17023
  } catch {
16966
17024
  setJson(res, 200, { reverted: 0, errors: ["Could not read session log"], message: "Session log unreadable" });
16967
17025
  return;
16968
17026
  }
16969
- const turnInfos = collectFileChangesForTurns(sessionLogRaw, turnIdsToRevert, cwd);
16970
17027
  if (turnInfos.size === 0) {
16971
17028
  setJson(res, 200, { changed: 0, errors: [], message: action === "redo" ? "No file changes to redo" : "No file changes to revert" });
16972
17029
  return;
@@ -17458,6 +17515,57 @@ function createAuthSession(password) {
17458
17515
 
17459
17516
  // src/server/httpServer.ts
17460
17517
  import { WebSocketServer } from "ws";
17518
+
17519
+ // src/server/bridge/wsHeartbeat.ts
17520
+ var WS_HEARTBEAT_INTERVAL_MS = 25e3;
17521
+ function attachWebSocketHeartbeat(socket, options = {}) {
17522
+ const intervalMs = options.intervalMs ?? WS_HEARTBEAT_INTERVAL_MS;
17523
+ const onDead = options.onDead ?? (() => {
17524
+ try {
17525
+ socket.terminate();
17526
+ } catch {
17527
+ }
17528
+ });
17529
+ let alive = true;
17530
+ let stopped = false;
17531
+ const handlePong = () => {
17532
+ alive = true;
17533
+ };
17534
+ const stop = () => {
17535
+ if (stopped) return;
17536
+ stopped = true;
17537
+ clearInterval(timer);
17538
+ socket.off?.("pong", handlePong);
17539
+ socket.off?.("close", stop);
17540
+ socket.off?.("error", stop);
17541
+ };
17542
+ const timer = setInterval(() => {
17543
+ if (stopped) return;
17544
+ if (!alive) {
17545
+ stop();
17546
+ onDead();
17547
+ return;
17548
+ }
17549
+ alive = false;
17550
+ try {
17551
+ socket.ping();
17552
+ } catch {
17553
+ stop();
17554
+ onDead();
17555
+ }
17556
+ }, intervalMs);
17557
+ const unrefable = timer;
17558
+ if (typeof unrefable.unref === "function") unrefable.unref();
17559
+ socket.on("pong", handlePong);
17560
+ socket.on("close", stop);
17561
+ socket.on("error", stop);
17562
+ return stop;
17563
+ }
17564
+ function attachHeartbeatToWebSocket(ws) {
17565
+ return attachWebSocketHeartbeat(ws);
17566
+ }
17567
+
17568
+ // src/server/httpServer.ts
17461
17569
  var __dirname = dirname11(fileURLToPath(import.meta.url));
17462
17570
  var distDir = join31(__dirname, "..", "dist");
17463
17571
  var spaEntryFile = join31(distDir, "index.html");
@@ -17723,6 +17831,7 @@ function createServer(options = {}) {
17723
17831
  });
17724
17832
  });
17725
17833
  wss.on("connection", (ws) => {
17834
+ attachHeartbeatToWebSocket(ws);
17726
17835
  ws.send(JSON.stringify({ method: "ready", params: { ok: true }, atIso: (/* @__PURE__ */ new Date()).toISOString() }));
17727
17836
  const unsubscribe = bridge.subscribeNotifications((notification) => {
17728
17837
  if (ws.readyState !== 1) return;
@@ -17878,7 +17987,7 @@ async function shouldInstallCloudflaredInteractively() {
17878
17987
  console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
17879
17988
  return false;
17880
17989
  }
17881
- const prompt = createInterface2({ input: process.stdin, output: process.stdout });
17990
+ const prompt = createInterface3({ input: process.stdin, output: process.stdout });
17882
17991
  try {
17883
17992
  const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
17884
17993
  const normalized = answer.trim().toLowerCase();