@threadbase-sh/streamer 1.29.2 → 1.30.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/dist/index.js CHANGED
@@ -680,6 +680,69 @@ function isProviderResumable(_provider, availabilityResumable) {
680
680
  return availabilityResumable;
681
681
  }
682
682
 
683
+ // src/services/questions/codexGateAnswers.ts
684
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
685
+ import { homedir as homedir3 } from "os";
686
+ import { dirname as dirname3, join as join5 } from "path";
687
+ function gateAnswersPath() {
688
+ const dir = process.env.THREADBASE_CONFIG_DIR ?? join5(homedir3(), ".threadbase");
689
+ return join5(dir, "gate-answers.json");
690
+ }
691
+ function loadGateAnswers() {
692
+ try {
693
+ const parsed = JSON.parse(readFileSync3(gateAnswersPath(), "utf-8"));
694
+ return parsed && typeof parsed === "object" ? parsed : {};
695
+ } catch {
696
+ return {};
697
+ }
698
+ }
699
+ function saveGateAnswer(key, value) {
700
+ const path = gateAnswersPath();
701
+ mkdirSync2(dirname3(path), { recursive: true });
702
+ writeFileSync2(path, `${JSON.stringify({ ...loadGateAnswers(), [key]: value }, null, 2)}
703
+ `);
704
+ }
705
+ function rememberedGateDigit(gate) {
706
+ const answers = loadGateAnswers();
707
+ if (gate === "hooks") {
708
+ if (answers.codexHooksGate === "trust_all") return "2";
709
+ if (answers.codexHooksGate === "continue_untrusted") return "3";
710
+ return null;
711
+ }
712
+ return answers.codexTrustGate === "yes" ? "1" : null;
713
+ }
714
+
715
+ // src/utils/debounce.ts
716
+ function debounce(fn, waitMs) {
717
+ let timer = null;
718
+ let lastArgs = null;
719
+ const run2 = () => {
720
+ timer = null;
721
+ if (lastArgs) {
722
+ const args = lastArgs;
723
+ lastArgs = null;
724
+ fn(...args);
725
+ }
726
+ };
727
+ const debounced = (...args) => {
728
+ lastArgs = args;
729
+ if (timer) clearTimeout(timer);
730
+ timer = setTimeout(run2, waitMs);
731
+ };
732
+ debounced.cancel = () => {
733
+ if (timer) clearTimeout(timer);
734
+ timer = null;
735
+ lastArgs = null;
736
+ };
737
+ debounced.flush = () => {
738
+ if (timer) {
739
+ clearTimeout(timer);
740
+ run2();
741
+ }
742
+ };
743
+ return debounced;
744
+ }
745
+
683
746
  // src/codex-pty-runner.ts
684
747
  var OUTPUT_BUFFER_MAX = 65536;
685
748
  var PTY_COLS = 120;
@@ -687,6 +750,9 @@ var PTY_ROWS = 40;
687
750
  var SCREEN_SCROLLBACK = 1e3;
688
751
  var CODEX_PROMPT_READY_TEXT = "Ready";
689
752
  var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
753
+ var CODEX_HOOKS_GATE_REGEX = /hooks need review/i;
754
+ var QUIET_DETECT_MS = 500;
755
+ var CODEX_READY_FALLBACK_MS = 8e3;
690
756
  var SUBMIT_BYTES = "\r";
691
757
  var CODEX_SUBMIT_DELAY_MS = 16;
692
758
  function digestBytes(s) {
@@ -694,6 +760,40 @@ function digestBytes(s) {
694
760
  if (escaped.length <= 200) return escaped;
695
761
  return `${escaped.slice(0, 100)}\u2026[${escaped.length - 200}B omitted]\u2026${escaped.slice(-100)}`;
696
762
  }
763
+ function gateCard(gate, lines) {
764
+ if (gate === "hooks") {
765
+ const countLine = lines.find((l) => /new or changed/i.test(l))?.trim();
766
+ return {
767
+ prompt: [
768
+ "Hooks need review",
769
+ countLine,
770
+ "Hooks can run outside the sandbox after you trust them."
771
+ ].filter(Boolean).join(" \u2014 "),
772
+ options: [
773
+ { index: 2, label: "Trust all and continue", answerKeys: "2\r" },
774
+ { index: 3, label: "Continue without trusting (hooks won't run)", answerKeys: "3\r" },
775
+ {
776
+ index: 4,
777
+ label: "Trust all and continue (remember for all projects)",
778
+ answerKeys: "4\r"
779
+ },
780
+ {
781
+ index: 5,
782
+ label: "Continue without trusting (remember for all projects)",
783
+ answerKeys: "5\r"
784
+ }
785
+ ]
786
+ };
787
+ }
788
+ return {
789
+ prompt: lines.find((l) => CODEX_TRUST_GATE_REGEX.test(l))?.trim() ?? "Do you trust the contents of this directory?",
790
+ options: [
791
+ { index: 1, label: "Yes, continue", answerKeys: "1\r" },
792
+ { index: 2, label: "No, quit", answerKeys: "2\r" },
793
+ { index: 3, label: "Yes, continue (remember for all projects)", answerKeys: "3\r" }
794
+ ]
795
+ };
796
+ }
697
797
  var pty = null;
698
798
  async function loadPty() {
699
799
  if (pty) return pty;
@@ -720,8 +820,8 @@ var CodexPtyRunner = class {
720
820
  onOutput;
721
821
  onStatusChange;
722
822
  onReady;
723
- // Accepted for shape-compatibility with PTYManagerOptions; Codex has no
724
- // detected equivalent yet (Phase 0) never invoked.
823
+ // Broadcasts Codex's blocking startup gates (directory trust, hooks review)
824
+ // as question cards; null dismisses the card once the gate leaves the screen.
725
825
  onPermissionChange;
726
826
  onLiveQuestion;
727
827
  onLiveQuestionGone;
@@ -732,8 +832,18 @@ var CodexPtyRunner = class {
732
832
  // Inputs received via sendInput() while the session was still pendingReady.
733
833
  // Flushed in arrival order once Codex reaches Ready.
734
834
  queuedInputs = /* @__PURE__ */ new Map();
735
- // Per-session debounce so the directory-trust gate's \r is only written once.
736
- trustGateAnswered = /* @__PURE__ */ new Set();
835
+ // Gate currently on a session's screen (card broadcast, unanswered). While
836
+ // set, queued-input flushes are held — a flushed digit would CONFIRM a
837
+ // dialog option — and sendKeys() intercepts remember-variant digits.
838
+ openGate = /* @__PURE__ */ new Map();
839
+ // `${sessionId}:${gate}` once a gate has been actioned (auto-answered or
840
+ // card broadcast) — dedupes repaints of the same dialog.
841
+ gateActioned = /* @__PURE__ */ new Set();
842
+ // Per-session trailing debounce re-armed on every chunk; on quiet, re-runs
843
+ // screen detection so a blocked/truncated boot still reaches ready.
844
+ quietCheckers = /* @__PURE__ */ new Map();
845
+ // Per-session flat backstop from spawn (CODEX_READY_FALLBACK_MS).
846
+ readyFallbackTimers = /* @__PURE__ */ new Map();
737
847
  // In-flight start()/startFresh() calls keyed by sessionId. A second
738
848
  // concurrent resume for the same session (double-tap, client retry) awaits
739
849
  // the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
@@ -792,6 +902,7 @@ var CodexPtyRunner = class {
792
902
  };
793
903
  this.sessions.set(sessionId, session);
794
904
  this.pendingReady.add(sessionId);
905
+ this.armReadyFallback(sessionId);
795
906
  proc.onData((data) => {
796
907
  this.handleOutput(sessionId, data);
797
908
  });
@@ -837,6 +948,7 @@ var CodexPtyRunner = class {
837
948
  };
838
949
  this.sessions.set(sessionId, session);
839
950
  this.pendingReady.add(sessionId);
951
+ this.armReadyFallback(sessionId);
840
952
  proc.onData((data) => {
841
953
  this.handleOutput(sessionId, data);
842
954
  });
@@ -846,6 +958,21 @@ var CodexPtyRunner = class {
846
958
  });
847
959
  return toPublicSession(session);
848
960
  }
961
+ // Flat backstop: if neither the "Ready" marker nor the quiet-checker settled
962
+ // the session within CODEX_READY_FALLBACK_MS of spawn, mark it ready anyway
963
+ // so start requests resolve and mobile can watch the boot live. unref() so a
964
+ // pending timer never holds the process open.
965
+ armReadyFallback(sessionId) {
966
+ const timer = setTimeout(() => {
967
+ this.readyFallbackTimers.delete(sessionId);
968
+ const session = this.sessions.get(sessionId);
969
+ if (session?.status === "running" && this.pendingReady.has(sessionId)) {
970
+ this.markReady(sessionId, session, "fallback:timeout");
971
+ }
972
+ }, CODEX_READY_FALLBACK_MS);
973
+ timer.unref?.();
974
+ this.readyFallbackTimers.set(sessionId, timer);
975
+ }
849
976
  // Write raw key bytes directly to the PTY, same as PTYManager.sendKeys.
850
977
  sendKeys(sessionId, keys) {
851
978
  const session = this.sessions.get(sessionId);
@@ -857,13 +984,47 @@ var CodexPtyRunner = class {
857
984
  session.status = "running";
858
985
  this.onStatusChange?.(toPublicSession(session));
859
986
  }
987
+ const gate = this.openGate.get(sessionId);
988
+ const digit = gate ? /^([0-9])\r?$/.exec(keys)?.[1] : void 0;
989
+ const out = gate && digit ? this.resolveGateAnswer(sessionId, gate, digit) : keys;
860
990
  this.log.info(
861
- `[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${keys.length} digest=${digestBytes(keys)}`,
862
- { event: "codex.keys_write", sessionId, byteLen: keys.length }
991
+ `[codex.keys.write] ${sessionId.slice(0, 8)} bytes=${out.length} digest=${digestBytes(out)}`,
992
+ { event: "codex.keys_write", sessionId, byteLen: out.length }
863
993
  );
864
- session.process.write(keys);
994
+ session.process.write(out);
865
995
  session.lastActivityAt = /* @__PURE__ */ new Date();
866
996
  }
997
+ // Map a gate-card digit to the PTY bytes that answer the real dialog,
998
+ // persisting the choice when the digit was a synthetic "remember for all
999
+ // projects" option (those numbers don't exist on the actual dialog and must
1000
+ // never reach codex). The trailing \r mobile sends is dropped: a digit alone
1001
+ // selects AND confirms (live-probe verified), and a stray Enter would land
1002
+ // on whatever screen follows.
1003
+ resolveGateAnswer(sessionId, gate, digit) {
1004
+ let real = digit;
1005
+ let remembered = false;
1006
+ if (gate === "hooks" && digit === "4") {
1007
+ saveGateAnswer("codexHooksGate", "trust_all");
1008
+ real = "2";
1009
+ remembered = true;
1010
+ } else if (gate === "hooks" && digit === "5") {
1011
+ saveGateAnswer("codexHooksGate", "continue_untrusted");
1012
+ real = "3";
1013
+ remembered = true;
1014
+ } else if (gate === "trust" && digit === "3") {
1015
+ saveGateAnswer("codexTrustGate", "yes");
1016
+ real = "1";
1017
+ remembered = true;
1018
+ }
1019
+ this.log.info(`[codex.gate_answer] ${sessionId.slice(0, 8)} ${gate} digit=${real}`, {
1020
+ event: "codex.gate_answer",
1021
+ sessionId,
1022
+ gate,
1023
+ digit: real,
1024
+ remembered
1025
+ });
1026
+ return real;
1027
+ }
867
1028
  sendInput(sessionId, input) {
868
1029
  const session = this.sessions.get(sessionId);
869
1030
  if (!session) throw new Error(`Session not found: ${sessionId}`);
@@ -933,8 +1094,12 @@ var CodexPtyRunner = class {
933
1094
  }, CODEX_SUBMIT_DELAY_MS);
934
1095
  }
935
1096
  // Drain any inputs sent while the session was still pendingReady, writing
936
- // them in arrival order now that Codex is Ready.
1097
+ // them in arrival order now that Codex is Ready. No-op while a gate dialog
1098
+ // is open (a flushed digit would confirm a dialog option) or while still
1099
+ // pendingReady (markReady drains it) — the gate-close path re-drives it for
1100
+ // the ready-with-gate-open case.
937
1101
  flushQueuedInputs(sessionId) {
1102
+ if (this.openGate.has(sessionId) || this.pendingReady.has(sessionId)) return;
938
1103
  const queue = this.queuedInputs.get(sessionId);
939
1104
  if (!queue || queue.length === 0) return;
940
1105
  this.queuedInputs.delete(sessionId);
@@ -979,7 +1144,7 @@ var CodexPtyRunner = class {
979
1144
  if (!session) return;
980
1145
  this.pendingReady.delete(sessionId);
981
1146
  this.queuedInputs.delete(sessionId);
982
- this.trustGateAnswered.delete(sessionId);
1147
+ this.clearSessionDetectors(sessionId);
983
1148
  try {
984
1149
  session.process.kill("SIGINT");
985
1150
  } catch {
@@ -990,6 +1155,21 @@ var CodexPtyRunner = class {
990
1155
  this.sessions.delete(sessionId);
991
1156
  this.onStatusChange?.(toPublicSession(session));
992
1157
  }
1158
+ // Drop a session's detection state: quiet-checker, ready-fallback timer,
1159
+ // gate bookkeeping — and dismiss a still-open gate card so mobile doesn't
1160
+ // keep rendering a question for a dead PTY.
1161
+ clearSessionDetectors(sessionId) {
1162
+ this.quietCheckers.get(sessionId)?.cancel();
1163
+ this.quietCheckers.delete(sessionId);
1164
+ const timer = this.readyFallbackTimers.get(sessionId);
1165
+ if (timer) clearTimeout(timer);
1166
+ this.readyFallbackTimers.delete(sessionId);
1167
+ if (this.openGate.delete(sessionId)) {
1168
+ this.onPermissionChange?.(sessionId, null);
1169
+ }
1170
+ this.gateActioned.delete(`${sessionId}:hooks`);
1171
+ this.gateActioned.delete(`${sessionId}:trust`);
1172
+ }
993
1173
  getOutput(sessionId) {
994
1174
  const session = this.sessions.get(sessionId);
995
1175
  if (!session) throw new Error(`Session not found: ${sessionId}`);
@@ -1029,10 +1209,19 @@ var CodexPtyRunner = class {
1029
1209
  }
1030
1210
  session.screen.dispose();
1031
1211
  }
1212
+ for (const sessionId of Array.from(this.quietCheckers.keys())) {
1213
+ this.clearSessionDetectors(sessionId);
1214
+ }
1215
+ for (const timer of this.readyFallbackTimers.values()) {
1216
+ clearTimeout(timer);
1217
+ }
1032
1218
  this.sessions.clear();
1033
1219
  this.pendingReady.clear();
1034
1220
  this.queuedInputs.clear();
1035
- this.trustGateAnswered.clear();
1221
+ this.openGate.clear();
1222
+ this.gateActioned.clear();
1223
+ this.quietCheckers.clear();
1224
+ this.readyFallbackTimers.clear();
1036
1225
  }
1037
1226
  handleOutput(sessionId, data) {
1038
1227
  const session = this.sessions.get(sessionId);
@@ -1047,44 +1236,92 @@ var CodexPtyRunner = class {
1047
1236
  session.screen.write(data);
1048
1237
  session.lastOutput = stripAnsi(data);
1049
1238
  this.onOutput?.(sessionId, data);
1050
- this.detectReady(sessionId, session).catch((err) => {
1239
+ this.detectScreenState(sessionId, "chunk").catch((err) => {
1051
1240
  this.log.warn("[codex.ready_detect] failed", {
1052
1241
  event: "codex.ready_detect_failed",
1053
1242
  sessionId,
1054
1243
  err
1055
1244
  });
1056
1245
  });
1246
+ let quiet = this.quietCheckers.get(sessionId);
1247
+ if (!quiet) {
1248
+ quiet = debounce(() => {
1249
+ this.detectScreenState(sessionId, "quiet").catch((err) => {
1250
+ this.log.warn("[codex.ready_detect] failed", {
1251
+ event: "codex.ready_detect_failed",
1252
+ sessionId,
1253
+ err
1254
+ });
1255
+ });
1256
+ }, QUIET_DETECT_MS);
1257
+ this.quietCheckers.set(sessionId, quiet);
1258
+ }
1259
+ quiet();
1057
1260
  }
1058
- // Renders the session's headless screen and checks for the directory-trust
1059
- // gate (answered once, debounced) and the "Ready" status-bar text. Only
1060
- // transitions to waiting_input / fires onReady when the rendered status
1061
- // line literally contains "Ready" `›` alone (visible during "Starting")
1062
- // is NOT a valid readiness signal (Phase 0).
1063
- async detectReady(sessionId, session) {
1064
- if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
1261
+ // Renders the session's headless screen and drives both detections:
1262
+ // - Gates (directory trust, hooks review) checked on EVERY pass,
1263
+ // independent of pendingReady, so a gate appearing after ready is still
1264
+ // surfaced and a gate leaving the screen closes its card.
1265
+ // - Readiness the "Ready" status-bar marker while pendingReady, plus the
1266
+ // quiet path: after QUIET_DETECT_MS of PTY silence a still-pending
1267
+ // session is marked ready anyway (`›` alone is NOT a marker — Phase 0 —
1268
+ // but a quiet boot screen is more useful to the user live than a
1269
+ // spinner, and "Ready" may be truncated off the 120-col status bar).
1270
+ async detectScreenState(sessionId, trigger) {
1271
+ const session = this.sessions.get(sessionId);
1272
+ if (!session || session.status === "idle") return;
1065
1273
  const lines = await this.getOutputLines(sessionId, PTY_ROWS);
1066
1274
  const screenText = lines.join("\n");
1067
- if (CODEX_TRUST_GATE_REGEX.test(screenText)) {
1068
- if (!this.trustGateAnswered.has(sessionId)) {
1069
- this.trustGateAnswered.add(sessionId);
1070
- this.log.info(`[codex.trust_gate] ${sessionId.slice(0, 8)} auto-answering`, {
1071
- event: "codex.trust_gate",
1072
- sessionId
1073
- });
1074
- session.process.write("\r");
1075
- }
1076
- return;
1275
+ const gate = CODEX_HOOKS_GATE_REGEX.test(screenText) ? "hooks" : CODEX_TRUST_GATE_REGEX.test(screenText) ? "trust" : null;
1276
+ if (gate) {
1277
+ this.handleGate(sessionId, session, gate, lines);
1278
+ } else if (this.openGate.delete(sessionId)) {
1279
+ this.onPermissionChange?.(sessionId, null);
1280
+ this.flushQueuedInputs(sessionId);
1077
1281
  }
1282
+ if (session.status !== "running" || !this.pendingReady.has(sessionId)) return;
1078
1283
  const lastNonBlank2 = [...lines].reverse().find((l) => l.trim() !== "") ?? "";
1079
- if (!lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) return;
1080
- this.markReady(sessionId, session);
1284
+ if (lastNonBlank2.includes(CODEX_PROMPT_READY_TEXT)) {
1285
+ this.markReady(sessionId, session, `marker:${CODEX_PROMPT_READY_TEXT}`);
1286
+ } else if (trigger === "quiet") {
1287
+ this.markReady(sessionId, session, "quiet:timeout");
1288
+ }
1081
1289
  }
1082
- markReady(sessionId, session) {
1290
+ // Answer a gate from the persisted remember-store, or surface it as a
1291
+ // question card over the permission transport. Actioned once per session and
1292
+ // gate type — repaints of the same dialog neither re-write nor re-broadcast.
1293
+ handleGate(sessionId, session, gate, lines) {
1294
+ const key = `${sessionId}:${gate}`;
1295
+ if (this.gateActioned.has(key)) return;
1296
+ this.gateActioned.add(key);
1297
+ const remembered = rememberedGateDigit(gate);
1298
+ if (remembered) {
1299
+ this.log.info(`[codex.gate_auto_answer] ${sessionId.slice(0, 8)} ${gate} \u2192 ${remembered}`, {
1300
+ event: "codex.gate_auto_answer",
1301
+ sessionId,
1302
+ gate,
1303
+ digit: remembered
1304
+ });
1305
+ session.process.write(remembered);
1306
+ return;
1307
+ }
1308
+ this.openGate.set(sessionId, gate);
1309
+ const card = gateCard(gate, lines);
1310
+ this.log.info(`[codex.gate_prompt] ${sessionId.slice(0, 8)} ${gate}`, {
1311
+ event: "codex.gate_prompt",
1312
+ sessionId,
1313
+ gate,
1314
+ prompt: card.prompt
1315
+ });
1316
+ this.onPermissionChange?.(sessionId, card);
1317
+ }
1318
+ markReady(sessionId, session, reason) {
1083
1319
  session.lastActivityAt = /* @__PURE__ */ new Date();
1084
1320
  session.status = "waiting_input";
1085
- this.log.info(`[codex.ready] ${sessionId.slice(0, 8)}`, {
1321
+ this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {
1086
1322
  event: "codex.ready",
1087
- sessionId
1323
+ sessionId,
1324
+ reason
1088
1325
  });
1089
1326
  this.onStatusChange?.(toPublicSession(session));
1090
1327
  if (this.pendingReady.has(sessionId)) {
@@ -1110,7 +1347,7 @@ var CodexPtyRunner = class {
1110
1347
  session.screen.dispose();
1111
1348
  this.sessions.delete(sessionId);
1112
1349
  this.queuedInputs.delete(sessionId);
1113
- this.trustGateAnswered.delete(sessionId);
1350
+ this.clearSessionDetectors(sessionId);
1114
1351
  }
1115
1352
  };
1116
1353
  function toPublicSession(s) {
@@ -1293,9 +1530,15 @@ function detectShellPrompt(lines) {
1293
1530
  ]
1294
1531
  };
1295
1532
  }
1296
- if (NUMBERED_RE.test(last.text)) {
1533
+ const lastNumberedIdx = (() => {
1534
+ for (let i = last.idx; i >= 0; i--) {
1535
+ if (NUMBERED_RE.test(lines[i])) return i;
1536
+ }
1537
+ return -1;
1538
+ })();
1539
+ if (lastNumberedIdx >= 0) {
1297
1540
  const options = [];
1298
- for (let i = 0; i <= last.idx; i++) {
1541
+ for (let i = 0; i <= lastNumberedIdx; i++) {
1299
1542
  const m = NUMBERED_RE.exec(lines[i]);
1300
1543
  if (!m) continue;
1301
1544
  const num = Number.parseInt(m[1], 10);
@@ -1323,37 +1566,6 @@ function detectShellPrompt(lines) {
1323
1566
  return null;
1324
1567
  }
1325
1568
 
1326
- // src/utils/debounce.ts
1327
- function debounce(fn, waitMs) {
1328
- let timer = null;
1329
- let lastArgs = null;
1330
- const run2 = () => {
1331
- timer = null;
1332
- if (lastArgs) {
1333
- const args = lastArgs;
1334
- lastArgs = null;
1335
- fn(...args);
1336
- }
1337
- };
1338
- const debounced = (...args) => {
1339
- lastArgs = args;
1340
- if (timer) clearTimeout(timer);
1341
- timer = setTimeout(run2, waitMs);
1342
- };
1343
- debounced.cancel = () => {
1344
- if (timer) clearTimeout(timer);
1345
- timer = null;
1346
- lastArgs = null;
1347
- };
1348
- debounced.flush = () => {
1349
- if (timer) {
1350
- clearTimeout(timer);
1351
- run2();
1352
- }
1353
- };
1354
- return debounced;
1355
- }
1356
-
1357
1569
  // src/pty-manager.ts
1358
1570
  var OUTPUT_BUFFER_MAX2 = 65536;
1359
1571
  var PTY_COLS2 = 120;
@@ -1361,7 +1573,7 @@ var PTY_ROWS2 = 40;
1361
1573
  var SCREEN_SCROLLBACK2 = 1e3;
1362
1574
  var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
1363
1575
  var PROMPT_MARKER_FALLBACK_MS = 1e4;
1364
- var QUIET_DETECT_MS = 500;
1576
+ var QUIET_DETECT_MS2 = 500;
1365
1577
  function buildPasteBytes(input) {
1366
1578
  return `\x1B[200~${input}\x1B[201~`;
1367
1579
  }
@@ -1861,7 +2073,7 @@ var PTYManager = class {
1861
2073
  });
1862
2074
  let quiet = this.quietCheckers.get(sessionId);
1863
2075
  if (!quiet) {
1864
- quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);
2076
+ quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS2);
1865
2077
  this.quietCheckers.set(sessionId, quiet);
1866
2078
  }
1867
2079
  quiet();
@@ -2122,7 +2334,7 @@ var LiveSessionManager = class {
2122
2334
  // src/process-discovery.ts
2123
2335
  import { execFile } from "child_process";
2124
2336
  import { platform as platform2 } from "os";
2125
- import { basename as basename4, dirname as dirname3 } from "path";
2337
+ import { basename as basename4, dirname as dirname4 } from "path";
2126
2338
  async function discoverClaudeProcesses() {
2127
2339
  if (platform2() === "win32") return discoverWindows();
2128
2340
  return discoverUnix();
@@ -2244,7 +2456,7 @@ async function getProcessInfoWindows(pid) {
2244
2456
  const startedAt = /* @__PURE__ */ new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}`);
2245
2457
  if (Number.isNaN(startedAt.getTime())) return null;
2246
2458
  const exePath = parts[3] ?? "";
2247
- const cwd = exePath ? dirname3(exePath) : "";
2459
+ const cwd = exePath ? dirname4(exePath) : "";
2248
2460
  return { cwd, args, startedAt };
2249
2461
  } catch {
2250
2462
  return null;
@@ -2276,16 +2488,16 @@ import {
2276
2488
  import { EventEmitter } from "events";
2277
2489
  import {
2278
2490
  createReadStream,
2279
- existsSync as existsSync7,
2491
+ existsSync as existsSync8,
2280
2492
  watch as fsWatch,
2281
2493
  readdirSync as readdirSync4,
2282
- readFileSync as readFileSync6,
2283
- statSync as statSync5
2494
+ readFileSync as readFileSync7,
2495
+ statSync as statSync6
2284
2496
  } from "fs";
2285
2497
  import { realpath as realpath2 } from "fs/promises";
2286
2498
  import { createServer } from "http";
2287
- import { homedir as homedir5 } from "os";
2288
- import { dirname as dirname7, join as join12 } from "path";
2499
+ import { homedir as homedir7 } from "os";
2500
+ import { dirname as dirname8, join as join15 } from "path";
2289
2501
  import { createInterface } from "readline";
2290
2502
 
2291
2503
  // node_modules/nanoid/index.js
@@ -2469,7 +2681,7 @@ async function handleSendAgentInput(sessionId, body, deps) {
2469
2681
 
2470
2682
  // src/agent/handle-start-agent-session.ts
2471
2683
  import { existsSync as existsSync4 } from "fs";
2472
- import { join as join5 } from "path";
2684
+ import { join as join6 } from "path";
2473
2685
  function validateBody(body) {
2474
2686
  if (body === null || body === void 0 || typeof body !== "object") {
2475
2687
  return { ok: false };
@@ -2499,7 +2711,7 @@ async function handleStartAgentSession(body, deps) {
2499
2711
  }
2500
2712
  let conversationId = parsed.conversationId;
2501
2713
  if (conversationId) {
2502
- const jsonlPath = join5(deps.conversationsDir, `${conversationId}.jsonl`);
2714
+ const jsonlPath = join6(deps.conversationsDir, `${conversationId}.jsonl`);
2503
2715
  if (!existsSync4(jsonlPath)) {
2504
2716
  return {
2505
2717
  status: 404,
@@ -2544,7 +2756,7 @@ async function handleStartAgentSession(body, deps) {
2544
2756
  }
2545
2757
 
2546
2758
  // src/api/app.ts
2547
- import { Hono as Hono11 } from "hono";
2759
+ import { Hono as Hono12 } from "hono";
2548
2760
 
2549
2761
  // src/api/middleware/auth.middleware.ts
2550
2762
  function isLocalRequest(remoteAddr) {
@@ -2552,6 +2764,7 @@ function isLocalRequest(remoteAddr) {
2552
2764
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
2553
2765
  }
2554
2766
  var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
2767
+ var LOCAL_ONLY_PATHS = /* @__PURE__ */ new Set(["/api/logs", "/api/logs/meta"]);
2555
2768
  var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
2556
2769
  var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
2557
2770
  var authMiddleware = (deps) => async (c, next) => {
@@ -2562,8 +2775,12 @@ var authMiddleware = (deps) => async (c, next) => {
2562
2775
  await next();
2563
2776
  return;
2564
2777
  }
2778
+ const remoteAddr = c.env.incoming?.socket?.remoteAddress;
2779
+ if (LOCAL_ONLY_PATHS.has(path) && isLocalRequest(remoteAddr)) {
2780
+ await next();
2781
+ return;
2782
+ }
2565
2783
  if (deps.localNoAuth) {
2566
- const remoteAddr = c.env.incoming?.socket?.remoteAddress;
2567
2784
  if (isLocalRequest(remoteAddr)) {
2568
2785
  await next();
2569
2786
  return;
@@ -2696,8 +2913,8 @@ var createConversationRoutes = (deps) => {
2696
2913
  import { Hono as Hono4 } from "hono";
2697
2914
 
2698
2915
  // src/version.ts
2699
- import { readFileSync as readFileSync3, realpathSync } from "fs";
2700
- import { dirname as dirname4, join as join6 } from "path";
2916
+ import { readFileSync as readFileSync4, realpathSync } from "fs";
2917
+ import { dirname as dirname5, join as join7 } from "path";
2701
2918
  var cached;
2702
2919
  function getVersion() {
2703
2920
  if (cached !== void 0) return cached;
@@ -2706,22 +2923,22 @@ function getVersion() {
2706
2923
  }
2707
2924
  function resolveVersion() {
2708
2925
  const scriptPath = process.argv[1] ?? "";
2709
- const here = scriptPath ? dirname4(scriptPath) : process.cwd();
2926
+ const here = scriptPath ? dirname5(scriptPath) : process.cwd();
2710
2927
  let realHere = here;
2711
2928
  try {
2712
- realHere = dirname4(realpathSync(scriptPath));
2929
+ realHere = dirname5(realpathSync(scriptPath));
2713
2930
  } catch {
2714
2931
  }
2715
- const searchDirs = realHere === here ? [here, join6(here, "..")] : [here, join6(here, ".."), realHere, join6(realHere, "..")];
2932
+ const searchDirs = realHere === here ? [here, join7(here, "..")] : [here, join7(here, ".."), realHere, join7(realHere, "..")];
2716
2933
  for (const dir of searchDirs) {
2717
2934
  try {
2718
- const v = readFileSync3(join6(dir, "version.txt"), "utf8").trim();
2935
+ const v = readFileSync4(join7(dir, "version.txt"), "utf8").trim();
2719
2936
  if (v) return v;
2720
2937
  } catch {
2721
2938
  }
2722
2939
  }
2723
2940
  try {
2724
- const pkg = JSON.parse(readFileSync3(join6(here, "..", "package.json"), "utf8"));
2941
+ const pkg = JSON.parse(readFileSync4(join7(here, "..", "package.json"), "utf8"));
2725
2942
  if (pkg.version) return `${pkg.version}+source`;
2726
2943
  } catch {
2727
2944
  }
@@ -2735,16 +2952,145 @@ var createHealthRoutes = () => {
2735
2952
  return app;
2736
2953
  };
2737
2954
 
2955
+ // src/api/routes/logs.routes.ts
2956
+ import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync } from "fs";
2957
+ import { join as join9 } from "path";
2958
+ import { Hono as Hono5 } from "hono";
2959
+
2960
+ // src/lifecycle/constants.ts
2961
+ import { homedir as homedir4 } from "os";
2962
+ import { join as join8 } from "path";
2963
+ var TASK_NAME = process.env.THREADBASE_TASK_NAME ?? "Threadbase";
2964
+ function installDir() {
2965
+ return process.env.THREADBASE_INSTALL_DIR ?? join8(homedir4(), ".threadbase");
2966
+ }
2967
+
2968
+ // src/api/routes/logs.routes.ts
2969
+ var logger2 = getLogger("logs-api");
2970
+ function resolveLogPath(source) {
2971
+ return join9(installDir(), "logs", `${source}.log`);
2972
+ }
2973
+ function pickDefaultSource() {
2974
+ for (const source of ["stdout", "stderr", "dev"]) {
2975
+ const p = resolveLogPath(source);
2976
+ if (existsSync5(p) && statSync(p).size > 0) return source;
2977
+ }
2978
+ return "stdout";
2979
+ }
2980
+ function readLogLines(filePath, sinceOffset, limit) {
2981
+ if (!existsSync5(filePath)) {
2982
+ return { lines: [], offset: 0, total: 0 };
2983
+ }
2984
+ const fd = openSync(filePath, "r");
2985
+ try {
2986
+ const { size } = fstatSync(fd);
2987
+ if (size === 0) return { lines: [], offset: 0, total: 0 };
2988
+ const maxBytes = Math.min(size, 2 * 1024 * 1024);
2989
+ const start = size - maxBytes;
2990
+ const buf = Buffer.alloc(maxBytes);
2991
+ readSync(fd, buf, 0, maxBytes, start);
2992
+ let text = buf.toString("utf8");
2993
+ if (start > 0) {
2994
+ const firstNl = text.indexOf("\n");
2995
+ if (firstNl >= 0) text = text.slice(firstNl + 1);
2996
+ }
2997
+ const allLines = text.split("\n").filter((line) => line.trim() && !line.startsWith("==="));
2998
+ let lines;
2999
+ let newOffset;
3000
+ if (sinceOffset > 0 && sinceOffset < allLines.length) {
3001
+ lines = allLines.slice(sinceOffset, sinceOffset + limit);
3002
+ newOffset = sinceOffset + lines.length;
3003
+ } else if (sinceOffset >= allLines.length && sinceOffset > 0) {
3004
+ lines = [];
3005
+ newOffset = allLines.length;
3006
+ } else {
3007
+ lines = allLines.slice(-limit);
3008
+ newOffset = allLines.length;
3009
+ }
3010
+ return { lines, offset: newOffset, total: allLines.length };
3011
+ } finally {
3012
+ closeSync(fd);
3013
+ }
3014
+ }
3015
+ function createLogsRoutes() {
3016
+ const app = new Hono5();
3017
+ app.get("/", (c) => {
3018
+ try {
3019
+ const sourceParam = (c.req.query("source") || "").toLowerCase();
3020
+ const source = sourceParam === "stdout" || sourceParam === "stderr" || sourceParam === "dev" ? sourceParam : pickDefaultSource();
3021
+ const logPath = resolveLogPath(source);
3022
+ const sinceOffset = parseInt(c.req.query("since") || "0", 10);
3023
+ const limit = Math.min(parseInt(c.req.query("limit") || "100", 10) || 100, 1e3);
3024
+ if (!existsSync5(logPath)) {
3025
+ return c.json({
3026
+ logs: [],
3027
+ message: `No log file found for source=${source}`,
3028
+ offset: 0,
3029
+ total: 0,
3030
+ source
3031
+ });
3032
+ }
3033
+ const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);
3034
+ const stats = statSync(logPath);
3035
+ return c.json({
3036
+ logs: lines,
3037
+ offset,
3038
+ total,
3039
+ hasMore: offset < total,
3040
+ source,
3041
+ fileSize: stats.size,
3042
+ fileModified: stats.mtime.toISOString()
3043
+ });
3044
+ } catch (error) {
3045
+ logger2.error("Failed to read logs", { error: String(error) });
3046
+ return c.json(
3047
+ {
3048
+ error: "Failed to read logs",
3049
+ logs: [],
3050
+ offset: 0,
3051
+ total: 0
3052
+ },
3053
+ 500
3054
+ );
3055
+ }
3056
+ });
3057
+ app.get("/meta", (c) => {
3058
+ try {
3059
+ const sources = ["stdout", "stderr", "dev"].map((source) => {
3060
+ const logPath = resolveLogPath(source);
3061
+ if (!existsSync5(logPath)) {
3062
+ return { source, exists: false, total: 0, fileSize: 0 };
3063
+ }
3064
+ const stats = statSync(logPath);
3065
+ return {
3066
+ source,
3067
+ exists: true,
3068
+ fileSize: stats.size,
3069
+ fileModified: stats.mtime.toISOString()
3070
+ };
3071
+ });
3072
+ return c.json({
3073
+ defaultSource: pickDefaultSource(),
3074
+ sources
3075
+ });
3076
+ } catch (error) {
3077
+ logger2.error("Failed to read log metadata", { error: String(error) });
3078
+ return c.json({ error: "Failed to read log metadata", exists: false }, 500);
3079
+ }
3080
+ });
3081
+ return app;
3082
+ }
3083
+
2738
3084
  // src/api/routes/misc.routes.ts
2739
3085
  import { spawn } from "child_process";
2740
3086
  import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
2741
- import { Hono as Hono5 } from "hono";
3087
+ import { Hono as Hono6 } from "hono";
2742
3088
  import { hostname } from "os";
2743
3089
 
2744
3090
  // src/config/update-config.ts
2745
- import { readFileSync as readFileSync4 } from "fs";
2746
- import { homedir as homedir3 } from "os";
2747
- import { join as join7 } from "path";
3091
+ import { readFileSync as readFileSync5 } from "fs";
3092
+ import { homedir as homedir5 } from "os";
3093
+ import { join as join10 } from "path";
2748
3094
  import { parse as parseYaml } from "yaml";
2749
3095
 
2750
3096
  // src/schemas/updateConfig.schema.ts
@@ -2760,12 +3106,12 @@ var UpdateConfigSchema = z.object({
2760
3106
  }).strict();
2761
3107
 
2762
3108
  // src/config/update-config.ts
2763
- var DEFAULT_CONFIG_PATH = join7(homedir3(), ".threadbase", "update.yaml");
3109
+ var DEFAULT_CONFIG_PATH = join10(homedir5(), ".threadbase", "update.yaml");
2764
3110
  function loadUpdateConfig(opts = {}) {
2765
3111
  const path = opts.path ?? DEFAULT_CONFIG_PATH;
2766
3112
  let raw;
2767
3113
  try {
2768
- raw = readFileSync4(path, "utf-8");
3114
+ raw = readFileSync5(path, "utf-8");
2769
3115
  } catch (err) {
2770
3116
  if (err.code === "ENOENT") return null;
2771
3117
  throw err;
@@ -2812,7 +3158,7 @@ function verifyWebhookSignature(body, header, secret) {
2812
3158
  }
2813
3159
  var clientLog = getLogger("client");
2814
3160
  var createMiscRoutes = (deps) => {
2815
- const app = new Hono5();
3161
+ const app = new Hono6();
2816
3162
  app.get("/api/info", (c) => {
2817
3163
  const ptyIds = deps.ptyAttachedIds();
2818
3164
  return c.json({
@@ -2888,11 +3234,11 @@ var createMiscRoutes = (deps) => {
2888
3234
  };
2889
3235
 
2890
3236
  // src/api/routes/pair.routes.ts
2891
- import { Hono as Hono6 } from "hono";
3237
+ import { Hono as Hono7 } from "hono";
2892
3238
  var ALREADY_HANDLED3 = 597;
2893
3239
  var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
2894
3240
  var createPairRoutes = (deps) => {
2895
- const app = new Hono6();
3241
+ const app = new Hono7();
2896
3242
  app.post("/start", (c) => {
2897
3243
  deps.handlePairStart(c.env.outgoing);
2898
3244
  return alreadyHandled3();
@@ -2905,11 +3251,11 @@ var createPairRoutes = (deps) => {
2905
3251
  };
2906
3252
 
2907
3253
  // src/api/routes/projects.routes.ts
2908
- import { Hono as Hono7 } from "hono";
3254
+ import { Hono as Hono8 } from "hono";
2909
3255
  var ALREADY_HANDLED4 = 597;
2910
3256
  var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
2911
3257
  var createProjectRoutes = (deps) => {
2912
- const app = new Hono7();
3258
+ const app = new Hono8();
2913
3259
  app.get("/", (c) => {
2914
3260
  const url = new URL(c.req.url);
2915
3261
  deps.handleListProjects(url, c.env.outgoing);
@@ -2924,11 +3270,11 @@ var createProjectRoutes = (deps) => {
2924
3270
  };
2925
3271
 
2926
3272
  // src/api/routes/scanner.routes.ts
2927
- import { Hono as Hono8 } from "hono";
3273
+ import { Hono as Hono9 } from "hono";
2928
3274
  var ALREADY_HANDLED5 = 597;
2929
3275
  var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
2930
3276
  var createScannerRoutes = (deps) => {
2931
- const app = new Hono8();
3277
+ const app = new Hono9();
2932
3278
  app.get("/api/search", async (c) => {
2933
3279
  const url = new URL(c.req.url);
2934
3280
  await deps.handleSearch(url, c.env.outgoing);
@@ -2938,11 +3284,11 @@ var createScannerRoutes = (deps) => {
2938
3284
  };
2939
3285
 
2940
3286
  // src/api/routes/sessions.routes.ts
2941
- import { Hono as Hono9 } from "hono";
3287
+ import { Hono as Hono10 } from "hono";
2942
3288
  var ALREADY_HANDLED6 = 597;
2943
3289
  var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
2944
3290
  var createSessionRoutes = (deps) => {
2945
- const app = new Hono9();
3291
+ const app = new Hono10();
2946
3292
  app.get("/count", (c) => {
2947
3293
  deps.handleSessionsCount(c.env.outgoing);
2948
3294
  return alreadyHandled6();
@@ -3009,9 +3355,9 @@ var createSessionRoutes = (deps) => {
3009
3355
  };
3010
3356
 
3011
3357
  // src/api/routes/ws.routes.ts
3012
- import { Hono as Hono10 } from "hono";
3358
+ import { Hono as Hono11 } from "hono";
3013
3359
  var createWsRoutes = (deps, upgradeWebSocket) => {
3014
- const app = new Hono10();
3360
+ const app = new Hono11();
3015
3361
  app.get(
3016
3362
  "/ws",
3017
3363
  upgradeWebSocket((c) => {
@@ -3039,7 +3385,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
3039
3385
 
3040
3386
  // src/api/app.ts
3041
3387
  var createHonoApp = (deps, upgradeWebSocket) => {
3042
- const app = new Hono11();
3388
+ const app = new Hono12();
3043
3389
  const httpLog = getLogger("http");
3044
3390
  app.use("*", async (c, next) => {
3045
3391
  const start = Date.now();
@@ -3068,6 +3414,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
3068
3414
  app.route("/api", createBrowseRoutes(deps));
3069
3415
  app.route("/", createScannerRoutes(deps));
3070
3416
  app.route("/internal", createProgressRoutes(deps));
3417
+ app.route("/api/logs", createLogsRoutes());
3071
3418
  if (upgradeWebSocket) {
3072
3419
  app.route("/", createWsRoutes(deps, upgradeWebSocket));
3073
3420
  }
@@ -3076,7 +3423,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
3076
3423
 
3077
3424
  // src/browse.ts
3078
3425
  import { mkdir as mkdir2, readdir, realpath, stat } from "fs/promises";
3079
- import { join as join8, resolve, sep } from "path";
3426
+ import { join as join11, resolve, sep } from "path";
3080
3427
  var BrowsePathNotFoundError = class extends Error {
3081
3428
  constructor(message) {
3082
3429
  super(message);
@@ -3114,7 +3461,7 @@ async function createDirectory(parentAbsolutePath, name) {
3114
3461
  if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
3115
3462
  throw new Error("Invalid directory name");
3116
3463
  }
3117
- const target = join8(parentAbsolutePath, name);
3464
+ const target = join11(parentAbsolutePath, name);
3118
3465
  try {
3119
3466
  const s = await stat(target);
3120
3467
  if (s.isDirectory()) throw new Error("Directory already exists");
@@ -3131,18 +3478,18 @@ import {
3131
3478
  parseJsonlLine
3132
3479
  } from "@threadbase-sh/scanner";
3133
3480
  import Database from "better-sqlite3";
3134
- import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "fs";
3481
+ import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, openSync as openSync3, readSync as readSync3, statSync as statSync3 } from "fs";
3135
3482
  import { open as openAsync } from "fs/promises";
3136
- import { dirname as dirname6 } from "path";
3483
+ import { dirname as dirname7 } from "path";
3137
3484
  import { setImmediate as yieldToEventLoop } from "timers/promises";
3138
3485
 
3139
3486
  // src/db/sqlite-migrate.ts
3140
- import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
3141
- import { dirname as dirname5, join as join9 } from "path";
3487
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
3488
+ import { dirname as dirname6, join as join12 } from "path";
3142
3489
  import { fileURLToPath as fileURLToPath2 } from "url";
3143
3490
  function getMigrationsDir2() {
3144
3491
  if (typeof import.meta !== "undefined" && import.meta.url) {
3145
- return dirname5(fileURLToPath2(import.meta.url));
3492
+ return dirname6(fileURLToPath2(import.meta.url));
3146
3493
  }
3147
3494
  return __dirname;
3148
3495
  }
@@ -3154,7 +3501,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
3154
3501
  `;
3155
3502
  function runSqliteMigrations(db, migrationsDir) {
3156
3503
  db.exec(SCHEMA_MIGRATIONS_SQL);
3157
- const dir = migrationsDir ?? join9(getMigrationsDir2(), "migrations");
3504
+ const dir = migrationsDir ?? join12(getMigrationsDir2(), "migrations");
3158
3505
  const files = readdirSync2(dir).filter((f) => f.endsWith(".sql")).sort();
3159
3506
  const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
3160
3507
  const appliedSet = new Set(appliedRows.map((r) => r.id));
@@ -3166,7 +3513,7 @@ function runSqliteMigrations(db, migrationsDir) {
3166
3513
  skipped.push(file);
3167
3514
  continue;
3168
3515
  }
3169
- const sql = readFileSync5(join9(dir, file), "utf-8");
3516
+ const sql = readFileSync6(join12(dir, file), "utf-8");
3170
3517
  const tx = db.transaction(() => {
3171
3518
  db.exec(sql);
3172
3519
  recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
@@ -3178,7 +3525,7 @@ function runSqliteMigrations(db, migrationsDir) {
3178
3525
  }
3179
3526
 
3180
3527
  // src/services/conversations/isAgentConversation.ts
3181
- import { closeSync, openSync, readSync, statSync } from "fs";
3528
+ import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "fs";
3182
3529
  var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
3183
3530
  var CHUNK_BYTES = 64 * 1024;
3184
3531
  var ENTRYPOINT_PROBE = `"entrypoint":`;
@@ -3200,12 +3547,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
3200
3547
  if (cached2 !== void 0) return cached2;
3201
3548
  let fd;
3202
3549
  try {
3203
- fd = openSync(filePath, "r");
3550
+ fd = openSync2(filePath, "r");
3204
3551
  } catch {
3205
3552
  return false;
3206
3553
  }
3207
3554
  try {
3208
- const fileSize = statSync(filePath).size;
3555
+ const fileSize = statSync2(filePath).size;
3209
3556
  if (fileSize === 0) {
3210
3557
  fileDecisionCache.set(key, false);
3211
3558
  return false;
@@ -3216,7 +3563,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
3216
3563
  let carry = "";
3217
3564
  while (offset < fileSize) {
3218
3565
  const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
3219
- const got = readSync(fd, buf, 0, toRead, offset);
3566
+ const got = readSync2(fd, buf, 0, toRead, offset);
3220
3567
  if (got <= 0) break;
3221
3568
  const chunk = carry + buf.toString("utf8", 0, got);
3222
3569
  for (const marker of markers) {
@@ -3237,7 +3584,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
3237
3584
  } catch {
3238
3585
  return false;
3239
3586
  } finally {
3240
- closeSync(fd);
3587
+ closeSync2(fd);
3241
3588
  }
3242
3589
  }
3243
3590
  function parseAgentEntrypointsEnv(raw) {
@@ -3774,7 +4121,7 @@ var ConversationCache = class _ConversationCache {
3774
4121
  if (!fileState) return null;
3775
4122
  let stat3;
3776
4123
  try {
3777
- stat3 = statSync2(filePath);
4124
+ stat3 = statSync3(filePath);
3778
4125
  } catch {
3779
4126
  return null;
3780
4127
  }
@@ -3795,17 +4142,17 @@ var ConversationCache = class _ConversationCache {
3795
4142
  );
3796
4143
  if (rows.length === 0) return { messages: [], total, fromIndex: from };
3797
4144
  const messages = [];
3798
- const fd = openSync2(filePath, "r");
4145
+ const fd = openSync3(filePath, "r");
3799
4146
  try {
3800
4147
  const state = createJsonlParseState();
3801
4148
  for (const row of rows) {
3802
4149
  const buf = Buffer.alloc(row.byte_length);
3803
- readSync2(fd, buf, 0, row.byte_length, row.byte_offset);
4150
+ readSync3(fd, buf, 0, row.byte_length, row.byte_offset);
3804
4151
  const msg = parseJsonlLine(buf.toString("utf-8"), state);
3805
4152
  if (msg) messages.push(msg);
3806
4153
  }
3807
4154
  } finally {
3808
- closeSync2(fd);
4155
+ closeSync3(fd);
3809
4156
  }
3810
4157
  return { messages, total, fromIndex: from };
3811
4158
  }
@@ -3833,14 +4180,14 @@ var ConversationCache = class _ConversationCache {
3833
4180
  isAgentFileCached(filePath) {
3834
4181
  let s;
3835
4182
  try {
3836
- s = statSync2(filePath);
4183
+ s = statSync3(filePath);
3837
4184
  } catch {
3838
4185
  return false;
3839
4186
  }
3840
4187
  return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
3841
4188
  }
3842
4189
  static open(dbPath, tailSize = 10, migrationsDir, options) {
3843
- mkdirSync2(dirname6(dbPath), { recursive: true });
4190
+ mkdirSync3(dirname7(dbPath), { recursive: true });
3844
4191
  const db = new Database(dbPath);
3845
4192
  db.pragma("journal_mode = WAL");
3846
4193
  db.pragma("foreign_keys = ON");
@@ -4021,7 +4368,7 @@ var ConversationCache = class _ConversationCache {
4021
4368
  let mtimeMs = null;
4022
4369
  let fileSize = null;
4023
4370
  try {
4024
- const s = statSync2(m.filePath);
4371
+ const s = statSync3(m.filePath);
4025
4372
  mtimeMs = s.mtimeMs;
4026
4373
  fileSize = s.size;
4027
4374
  } catch {
@@ -4080,8 +4427,8 @@ var ConversationCache = class _ConversationCache {
4080
4427
  let fileSize;
4081
4428
  let fd;
4082
4429
  try {
4083
- fileSize = statSync2(filePath).size;
4084
- fd = openSync2(filePath, "r");
4430
+ fileSize = statSync3(filePath).size;
4431
+ fd = openSync3(filePath, "r");
4085
4432
  } catch {
4086
4433
  return false;
4087
4434
  }
@@ -4094,7 +4441,7 @@ var ConversationCache = class _ConversationCache {
4094
4441
  while (pos > 0 && lines.length < this.tailSize * 4) {
4095
4442
  const toRead = Math.min(CHUNK, pos);
4096
4443
  pos -= toRead;
4097
- readSync2(fd, buf, 0, toRead, pos);
4444
+ readSync3(fd, buf, 0, toRead, pos);
4098
4445
  const chunk = buf.subarray(0, toRead).toString("utf8");
4099
4446
  const combined = chunk + partial;
4100
4447
  const parts = combined.split("\n");
@@ -4105,7 +4452,7 @@ var ConversationCache = class _ConversationCache {
4105
4452
  }
4106
4453
  if (partial) lines.push(partial);
4107
4454
  } finally {
4108
- closeSync2(fd);
4455
+ closeSync3(fd);
4109
4456
  }
4110
4457
  const msgs = [];
4111
4458
  for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
@@ -4307,7 +4654,7 @@ var ConversationCache = class _ConversationCache {
4307
4654
  * `handleGetConversation` can still serve the cached tail even when the
4308
4655
  * JSONL has been deleted.
4309
4656
  */
4310
- pruneGhostFiles(exists = existsSync5) {
4657
+ pruneGhostFiles(exists = existsSync6) {
4311
4658
  const rows = this.stmts.allFilePaths.all();
4312
4659
  const ghosts = [];
4313
4660
  const prune = this.db.transaction((ids) => {
@@ -4362,7 +4709,7 @@ var ConversationCache = class _ConversationCache {
4362
4709
  * Returns the removed IDs.
4363
4710
  */
4364
4711
  reconcileDeletions(livePaths, opts) {
4365
- const exists = opts?.exists ?? existsSync5;
4712
+ const exists = opts?.exists ?? existsSync6;
4366
4713
  const rows = this.stmts.allFilePaths.all();
4367
4714
  const removed = [];
4368
4715
  const drop = this.db.transaction((ids) => {
@@ -4591,23 +4938,23 @@ async function recordUpload(pool2, instanceId, row) {
4591
4938
  }
4592
4939
 
4593
4940
  // src/handlers/handleListProjects.ts
4594
- import { readdirSync as readdirSync3, statSync as statSync3 } from "fs";
4595
- import { homedir as homedir4 } from "os";
4596
- import { join as join10 } from "path";
4941
+ import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
4942
+ import { homedir as homedir6 } from "os";
4943
+ import { join as join13 } from "path";
4597
4944
  function decodeProjectPath(dirName) {
4598
4945
  return dirName.replace(/-/g, "/");
4599
4946
  }
4600
4947
  function handleListProjects(url, res) {
4601
4948
  const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
4602
4949
  const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
4603
- const projectsDir = join10(homedir4(), ".claude", "projects");
4950
+ const projectsDir = join13(homedir6(), ".claude", "projects");
4604
4951
  let entries;
4605
4952
  try {
4606
4953
  entries = readdirSync3(projectsDir).map((dirName) => {
4607
- const fullPath = join10(projectsDir, dirName);
4954
+ const fullPath = join13(projectsDir, dirName);
4608
4955
  let mtime = 0;
4609
4956
  try {
4610
- mtime = statSync3(fullPath).mtimeMs;
4957
+ mtime = statSync4(fullPath).mtimeMs;
4611
4958
  } catch {
4612
4959
  }
4613
4960
  const path = decodeProjectPath(dirName);
@@ -4702,7 +5049,7 @@ function seal(plaintext, recipientPublicKeyBase64) {
4702
5049
 
4703
5050
  // src/services/conversations/conversationWatcher.ts
4704
5051
  import chokidar from "chokidar";
4705
- import { statSync as statSync4 } from "fs";
5052
+ import { statSync as statSync5 } from "fs";
4706
5053
  import { open, stat as stat2 } from "fs/promises";
4707
5054
  var ConversationWatcher = class {
4708
5055
  files = /* @__PURE__ */ new Map();
@@ -4725,7 +5072,7 @@ var ConversationWatcher = class {
4725
5072
  if (this.files.has(filePath)) return;
4726
5073
  let offset;
4727
5074
  try {
4728
- offset = statSync4(filePath).size;
5075
+ offset = statSync5(filePath).size;
4729
5076
  } catch {
4730
5077
  offset = 0;
4731
5078
  }
@@ -4900,14 +5247,14 @@ function findSearchTarget(messages, query) {
4900
5247
  }
4901
5248
 
4902
5249
  // src/services/conversations/pruneAgentConversations.ts
4903
- import { existsSync as existsSync6 } from "fs";
5250
+ import { existsSync as existsSync7 } from "fs";
4904
5251
  function pruneAgentConversations(cache) {
4905
5252
  const db = cache.getDatabase();
4906
5253
  const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
4907
5254
  let pruned = 0;
4908
5255
  let missing = 0;
4909
5256
  for (const row of rows) {
4910
- if (!existsSync6(row.file_path)) {
5257
+ if (!existsSync7(row.file_path)) {
4911
5258
  missing += 1;
4912
5259
  continue;
4913
5260
  }
@@ -5250,7 +5597,7 @@ function discoveredToResponse(d, conversationId) {
5250
5597
  import { randomBytes as randomBytes3 } from "crypto";
5251
5598
  import { mkdir as mkdir3, writeFile } from "fs/promises";
5252
5599
  import heicConvert from "heic-convert";
5253
- import { join as join11 } from "path";
5600
+ import { join as join14 } from "path";
5254
5601
  var UPLOAD_DIR_NAME = ".threadbase-uploads";
5255
5602
  var MAX_BYTES = 25 * 1024 * 1024;
5256
5603
  var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
@@ -5283,9 +5630,9 @@ async function saveUploadFile(input) {
5283
5630
  }
5284
5631
  const id = `up_${randomBytes3(8).toString("hex")}`;
5285
5632
  const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
5286
- const dir = join11(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
5633
+ const dir = join14(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
5287
5634
  await mkdir3(dir, { recursive: true });
5288
- const filePath = join11(dir, `${Date.now()}-${id}-${safeName}`);
5635
+ const filePath = join14(dir, `${Date.now()}-${id}-${safeName}`);
5289
5636
  await writeFile(filePath, buffer);
5290
5637
  return {
5291
5638
  id,
@@ -5544,7 +5891,7 @@ var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
5544
5891
  var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
5545
5892
  var WS_CLOSE_UNAUTHORIZED = 4401;
5546
5893
  var REFRESH_TTL_MS = 2e3;
5547
- var START_READY_TIMEOUT_MS = 15e3;
5894
+ var START_READY_TIMEOUT_MS = 1e4;
5548
5895
  function parseIncludeAgentsEnv(raw) {
5549
5896
  if (raw === void 0) return false;
5550
5897
  const v = raw.trim().toLowerCase();
@@ -5678,12 +6025,12 @@ var StreamerServer = class {
5678
6025
  this.disableDb = config.disableDb ?? false;
5679
6026
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
5680
6027
  this.scanProfiles = config.scanProfiles;
5681
- this.codexRoots = config.codexRoots ?? [join12(homedir5(), ".codex", "sessions")];
6028
+ this.codexRoots = config.codexRoots ?? [join15(homedir7(), ".codex", "sessions")];
5682
6029
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
5683
6030
  this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
5684
6031
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
5685
6032
  this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
5686
- this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join12(homedir5(), ".threadbase", "cache");
6033
+ this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join15(homedir7(), ".threadbase", "cache");
5687
6034
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
5688
6035
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
5689
6036
  this.markScannerStaleDebounced = debounce(() => {
@@ -5724,7 +6071,7 @@ var StreamerServer = class {
5724
6071
  const seqs = cache.extendMessageIndex(
5725
6072
  filePath,
5726
6073
  spans,
5727
- statSync5(filePath),
6074
+ statSync6(filePath),
5728
6075
  readFrom,
5729
6076
  endOffset
5730
6077
  );
@@ -5885,7 +6232,7 @@ var StreamerServer = class {
5885
6232
  temporalClient,
5886
6233
  taskQueue: agentConfig.temporal.taskQueue
5887
6234
  });
5888
- const conversationsBaseDir = agentConfig.conversationsDir || join12(dirname7(this.cacheDir), "conversations");
6235
+ const conversationsBaseDir = agentConfig.conversationsDir || join15(dirname8(this.cacheDir), "conversations");
5889
6236
  conversationWriter = createConversationWriter({
5890
6237
  baseDir: conversationsBaseDir
5891
6238
  });
@@ -5983,6 +6330,38 @@ var StreamerServer = class {
5983
6330
  const lines = await this.ptyManager.getOutputLines(msg.sessionId, 200);
5984
6331
  ws.send(JSON.stringify({ type: "terminal_replay", sessionId: msg.sessionId, lines }));
5985
6332
  }
6333
+ const pendingGate = this.pendingPermission.get(msg.sessionId);
6334
+ if (pendingGate) {
6335
+ this.log.info(`[ws.replay_permission] ${msg.sessionId.slice(0, 8)}`, {
6336
+ event: "ws.replay_permission",
6337
+ sessionId: msg.sessionId
6338
+ });
6339
+ ws.send(
6340
+ JSON.stringify({
6341
+ type: "permission",
6342
+ sessionId: msg.sessionId,
6343
+ ...pendingGate.prompt ? { prompt: pendingGate.prompt } : {},
6344
+ ...pendingGate.detail ? { detail: pendingGate.detail } : {},
6345
+ options: pendingGate.options,
6346
+ ...pendingGate.cursor !== void 0 ? { cursor: pendingGate.cursor } : {}
6347
+ })
6348
+ );
6349
+ }
6350
+ const pendingQuestion = this.pendingQuestions.get(msg.sessionId);
6351
+ if (pendingQuestion) {
6352
+ this.log.info(`[ws.replay_question] ${msg.sessionId.slice(0, 8)}`, {
6353
+ event: "ws.replay_question",
6354
+ sessionId: msg.sessionId
6355
+ });
6356
+ ws.send(
6357
+ JSON.stringify({
6358
+ type: "question",
6359
+ sessionId: msg.sessionId,
6360
+ toolUseId: pendingQuestion.toolUseId,
6361
+ questions: pendingQuestion.questions
6362
+ })
6363
+ );
6364
+ }
5986
6365
  }
5987
6366
  if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
5988
6367
  if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
@@ -6153,7 +6532,7 @@ var StreamerServer = class {
6153
6532
  });
6154
6533
  try {
6155
6534
  this.cache = ConversationCache.open(
6156
- join12(this.cacheDir, "cache.db"),
6535
+ join15(this.cacheDir, "cache.db"),
6157
6536
  this.tailSize,
6158
6537
  void 0,
6159
6538
  {
@@ -6179,11 +6558,11 @@ var StreamerServer = class {
6179
6558
  if (this.scanProfiles && this.scanProfiles.length > 0) {
6180
6559
  for (const profile of this.scanProfiles) {
6181
6560
  if (profile.enabled) {
6182
- this.fileWatcher.watchDirectory(join12(profile.configDir, "projects"));
6561
+ this.fileWatcher.watchDirectory(join15(profile.configDir, "projects"));
6183
6562
  }
6184
6563
  }
6185
6564
  } else {
6186
- this.fileWatcher.watchDirectory(join12(homedir5(), ".claude", "projects"));
6565
+ this.fileWatcher.watchDirectory(join15(homedir7(), ".claude", "projects"));
6187
6566
  }
6188
6567
  } catch (err) {
6189
6568
  const message = err instanceof Error ? err.message : String(err);
@@ -6768,17 +7147,17 @@ var StreamerServer = class {
6768
7147
  return scanner;
6769
7148
  }
6770
7149
  findJsonlPath(uuid) {
6771
- const projectsDir = join12(homedir5(), ".claude", "projects");
6772
- if (!existsSync7(projectsDir)) return null;
7150
+ const projectsDir = join15(homedir7(), ".claude", "projects");
7151
+ if (!existsSync8(projectsDir)) return null;
6773
7152
  const filename = `${uuid}.jsonl`;
6774
7153
  for (const dir of readdirSync4(projectsDir)) {
6775
- const fp = join12(projectsDir, dir, filename);
6776
- if (existsSync7(fp)) return fp;
6777
- const projectDir = join12(projectsDir, dir);
7154
+ const fp = join15(projectsDir, dir, filename);
7155
+ if (existsSync8(fp)) return fp;
7156
+ const projectDir = join15(projectsDir, dir);
6778
7157
  try {
6779
7158
  for (const sub of readdirSync4(projectDir)) {
6780
- const subagentPath = join12(projectDir, sub, "subagents", filename);
6781
- if (existsSync7(subagentPath)) return subagentPath;
7159
+ const subagentPath = join15(projectDir, sub, "subagents", filename);
7160
+ if (existsSync8(subagentPath)) return subagentPath;
6782
7161
  }
6783
7162
  } catch {
6784
7163
  }
@@ -6925,7 +7304,7 @@ var StreamerServer = class {
6925
7304
  if (!conv.filePath) return false;
6926
7305
  let mtimeMs = null;
6927
7306
  try {
6928
- mtimeMs = statSync5(conv.filePath).mtimeMs;
7307
+ mtimeMs = statSync6(conv.filePath).mtimeMs;
6929
7308
  } catch {
6930
7309
  return false;
6931
7310
  }
@@ -7293,7 +7672,7 @@ var StreamerServer = class {
7293
7672
  handleGetSession(sessionId, res) {
7294
7673
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
7295
7674
  if (session) {
7296
- if (!existsSync7(session.projectPath)) {
7675
+ if (!existsSync8(session.projectPath)) {
7297
7676
  session.failureReason = `Project directory not found: ${session.projectPath}`;
7298
7677
  }
7299
7678
  json(res, 200, session);
@@ -7501,6 +7880,11 @@ var StreamerServer = class {
7501
7880
  const answerKeys = sanitizeAnswerKeys(o.answerKeys);
7502
7881
  return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
7503
7882
  });
7883
+ const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
7884
+ this.log.info(
7885
+ `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
7886
+ { event: "ws.broadcast_permission", sessionId, subscriberCount }
7887
+ );
7504
7888
  this.wsHub.broadcast({
7505
7889
  type: "permission",
7506
7890
  sessionId,
@@ -7693,7 +8077,7 @@ var StreamerServer = class {
7693
8077
  sessionStore: this.sessionStore,
7694
8078
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
7695
8079
  agentClient: this.agentClient,
7696
- conversationsDir: this.cacheDir ? join12(dirname7(this.cacheDir), "conversations") : "",
8080
+ conversationsDir: this.cacheDir ? join15(dirname8(this.cacheDir), "conversations") : "",
7697
8081
  agentConfig: this.agentConfig
7698
8082
  });
7699
8083
  json(res, result.status, result.body);
@@ -7834,9 +8218,9 @@ var StreamerServer = class {
7834
8218
  // was passed to Claude via --session-id so the filename matches from the start.
7835
8219
  watchForJsonl(sessionId, projectPath) {
7836
8220
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
7837
- const projectsDir = join12(homedir5(), ".claude", "projects", encoded);
8221
+ const projectsDir = join15(homedir7(), ".claude", "projects", encoded);
7838
8222
  const expectedFile = `${sessionId}.jsonl`;
7839
- const filePath = join12(projectsDir, expectedFile);
8223
+ const filePath = join15(projectsDir, expectedFile);
7840
8224
  const deadline = Date.now() + 12e4;
7841
8225
  let watcher = null;
7842
8226
  const cleanup = () => {
@@ -7854,12 +8238,12 @@ var StreamerServer = class {
7854
8238
  cleanup();
7855
8239
  return;
7856
8240
  }
7857
- let resolvedFilePath = existsSync7(filePath) ? filePath : null;
7858
- if (!resolvedFilePath && existsSync7(projectsDir)) {
8241
+ let resolvedFilePath = existsSync8(filePath) ? filePath : null;
8242
+ if (!resolvedFilePath && existsSync8(projectsDir)) {
7859
8243
  try {
7860
8244
  const now = Date.now();
7861
- const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync5(join12(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
7862
- if (recent) resolvedFilePath = join12(projectsDir, recent.f);
8245
+ const recent = readdirSync4(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync6(join15(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).sort((a, b) => b.mtime - a.mtime)[0];
8246
+ if (recent) resolvedFilePath = join15(projectsDir, recent.f);
7863
8247
  } catch {
7864
8248
  }
7865
8249
  }
@@ -7868,7 +8252,7 @@ var StreamerServer = class {
7868
8252
  this.sessionFileMap.set(sessionId, resolvedFilePath);
7869
8253
  this.fileWatcher.watch(resolvedFilePath);
7870
8254
  try {
7871
- const existing = readFileSync6(resolvedFilePath, "utf8").split("\n").filter(Boolean);
8255
+ const existing = readFileSync7(resolvedFilePath, "utf8").split("\n").filter(Boolean);
7872
8256
  if (existing.length > 0) {
7873
8257
  this.broadcastConversationLines(sessionId, existing);
7874
8258
  }
@@ -7907,7 +8291,7 @@ var StreamerServer = class {
7907
8291
  watchForCodexRollout(sessionId, projectPath) {
7908
8292
  const deadline = Date.now() + 12e4;
7909
8293
  const now = /* @__PURE__ */ new Date();
7910
- const dateDir = join12(
8294
+ const dateDir = join15(
7911
8295
  String(now.getFullYear()),
7912
8296
  String(now.getMonth() + 1).padStart(2, "0"),
7913
8297
  String(now.getDate()).padStart(2, "0")
@@ -7920,7 +8304,7 @@ var StreamerServer = class {
7920
8304
  };
7921
8305
  const matchesProjectPath = (candidatePath) => {
7922
8306
  try {
7923
- const firstLine = readFileSync6(candidatePath, "utf8").split("\n", 1)[0];
8307
+ const firstLine = readFileSync7(candidatePath, "utf8").split("\n", 1)[0];
7924
8308
  if (!firstLine) return null;
7925
8309
  const parsed = JSON.parse(firstLine);
7926
8310
  if (parsed?.type !== "session_meta") return null;
@@ -7948,8 +8332,8 @@ var StreamerServer = class {
7948
8332
  this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
7949
8333
  );
7950
8334
  for (const root of this.codexRoots) {
7951
- const sessionsDir = join12(root, dateDir);
7952
- if (!existsSync7(sessionsDir)) continue;
8335
+ const sessionsDir = join15(root, dateDir);
8336
+ if (!existsSync8(sessionsDir)) continue;
7953
8337
  let candidateFiles;
7954
8338
  try {
7955
8339
  candidateFiles = readdirSync4(sessionsDir).filter((f) => f.endsWith(".jsonl"));
@@ -7957,9 +8341,9 @@ var StreamerServer = class {
7957
8341
  continue;
7958
8342
  }
7959
8343
  const nowMs = Date.now();
7960
- const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync5(join12(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
8344
+ const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync6(join15(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
7961
8345
  for (const { f } of recentCandidates) {
7962
- const candidatePath = join12(sessionsDir, f);
8346
+ const candidatePath = join15(sessionsDir, f);
7963
8347
  const match = matchesProjectPath(candidatePath);
7964
8348
  if (!match) continue;
7965
8349
  if (boundElsewhere.has(match.id)) continue;
@@ -7969,7 +8353,7 @@ var StreamerServer = class {
7969
8353
  this.sessionFileMap.set(sessionId, candidatePath);
7970
8354
  this.fileWatcher.watch(candidatePath);
7971
8355
  try {
7972
- const existing = readFileSync6(candidatePath, "utf8").split("\n").filter(Boolean);
8356
+ const existing = readFileSync7(candidatePath, "utf8").split("\n").filter(Boolean);
7973
8357
  if (existing.length > 0) {
7974
8358
  this.broadcastConversationLines(sessionId, existing);
7975
8359
  }
@@ -8091,7 +8475,7 @@ var StreamerServer = class {
8091
8475
  };
8092
8476
  function classifyResumability(cwd) {
8093
8477
  if (!cwd) return { resumable: true };
8094
- if (existsSync7(cwd)) return { resumable: true };
8478
+ if (existsSync8(cwd)) return { resumable: true };
8095
8479
  const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
8096
8480
  return {
8097
8481
  resumable: false,