@threadbase-sh/streamer 1.58.1 → 1.58.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -851,9 +851,24 @@ import { basename } from "path";
851
851
 
852
852
  // src/platform.ts
853
853
  import { execFileSync } from "child_process";
854
- import { existsSync } from "fs";
854
+ import { accessSync, constants, existsSync, statSync } from "fs";
855
855
  import { homedir as homedir2, platform } from "os";
856
- import { join as join4 } from "path";
856
+ import { delimiter, join as join4 } from "path";
857
+
858
+ // src/providers.ts
859
+ var CLAUDE_CODE_PROVIDER = "claude-code";
860
+ var CODEX_CLI_PROVIDER = "codex-cli";
861
+ function isProviderName(value) {
862
+ return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;
863
+ }
864
+ function coerceProviderForRunner(value) {
865
+ return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER;
866
+ }
867
+ function isProviderResumable(_provider, availabilityResumable) {
868
+ return availabilityResumable;
869
+ }
870
+
871
+ // src/platform.ts
857
872
  var isWindows = platform() === "win32";
858
873
  var WINDOWS_EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([".exe", ".cmd", ".bat"]);
859
874
  function isWindowsExecutablePath(path) {
@@ -983,18 +998,38 @@ function resolveCodexExe() {
983
998
  _codexExe = "codex";
984
999
  return _codexExe;
985
1000
  }
986
-
987
- // src/providers.ts
988
- var CLAUDE_CODE_PROVIDER = "claude-code";
989
- var CODEX_CLI_PROVIDER = "codex-cli";
990
- function isProviderName(value) {
991
- return value === CLAUDE_CODE_PROVIDER || value === CODEX_CLI_PROVIDER;
1001
+ function isExecutableFile(path) {
1002
+ try {
1003
+ if (!statSync(path).isFile()) return false;
1004
+ accessSync(path, constants.X_OK);
1005
+ return true;
1006
+ } catch {
1007
+ return false;
1008
+ }
992
1009
  }
993
- function coerceProviderForRunner(value) {
994
- return isProviderName(value) ? value : CLAUDE_CODE_PROVIDER;
1010
+ function locateExecutable(exe) {
1011
+ if (/[\\/]/.test(exe)) return isExecutableFile(exe) ? exe : null;
1012
+ const names = isWindows ? [
1013
+ ...isWindowsExecutablePath(exe) ? [exe] : [],
1014
+ ...[...WINDOWS_EXECUTABLE_EXTENSIONS].map((ext) => `${exe}${ext}`)
1015
+ ] : [exe];
1016
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
1017
+ if (!dir) continue;
1018
+ for (const name of names) {
1019
+ const candidate = join4(dir, name);
1020
+ if (isExecutableFile(candidate)) return candidate;
1021
+ }
1022
+ }
1023
+ return null;
995
1024
  }
996
- function isProviderResumable(_provider, availabilityResumable) {
997
- return availabilityResumable;
1025
+ function locateProviderExe(provider) {
1026
+ const isCodex = provider === CODEX_CLI_PROVIDER;
1027
+ const found = locateExecutable(isCodex ? resolveCodexExe() : resolveClaudeExe());
1028
+ if (found === null) {
1029
+ if (isCodex) clearCodexExeCache();
1030
+ else clearClaudeExeCache();
1031
+ }
1032
+ return found;
998
1033
  }
999
1034
 
1000
1035
  // src/pty-shared.ts
@@ -3621,11 +3656,13 @@ var LiveSessionManager = class {
3621
3656
  async start(sessionId, options) {
3622
3657
  const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
3623
3658
  const runner = this.assertSupportedProvider(provider, options.projectPath);
3659
+ this.assertProviderInstalled(provider);
3624
3660
  return runner.start(sessionId, options);
3625
3661
  }
3626
3662
  async startFresh(options) {
3627
3663
  const provider = options.provider ?? CLAUDE_CODE_PROVIDER;
3628
3664
  const runner = this.assertSupportedProvider(provider, options.projectPath);
3665
+ this.assertProviderInstalled(provider);
3629
3666
  return runner.startFresh(options);
3630
3667
  }
3631
3668
  /**
@@ -3644,6 +3681,7 @@ var LiveSessionManager = class {
3644
3681
  err.statusCode = 501;
3645
3682
  throw err;
3646
3683
  }
3684
+ this.assertProviderInstalled(provider);
3647
3685
  return runner.startFork(options);
3648
3686
  }
3649
3687
  sendInput(sessionId, input) {
@@ -3725,6 +3763,30 @@ var LiveSessionManager = class {
3725
3763
  }
3726
3764
  throw new Error(`Session not found: ${sessionId}`);
3727
3765
  }
3766
+ /**
3767
+ * Refuse before spawning when the provider's CLI is not on this machine.
3768
+ *
3769
+ * Without this the spawn "succeeds": on POSIX execvp fails inside the forked
3770
+ * child, so a session appears, exits ~12ms later with code 1 and no output,
3771
+ * and the caller is told only that it "exited before becoming ready" — or,
3772
+ * on the Claude resume path, is told nothing at all, since that path answers
3773
+ * 200 before the process has had a chance to die. Every start route funnels
3774
+ * through here, so one check covers start, resume, adopt and fork.
3775
+ *
3776
+ * 503, not 500: the request was well-formed and the fault is this machine's
3777
+ * environment. `code` is what mobile branches on (it reads `errBody.code`),
3778
+ * and `PROVIDER_NOT_INSTALLED` is a remediation string it already knows.
3779
+ */
3780
+ assertProviderInstalled(provider) {
3781
+ if (locateProviderExe(provider) !== null) return;
3782
+ const command = provider === CODEX_CLI_PROVIDER ? "codex" : "claude";
3783
+ const err = new Error(
3784
+ `The ${command} command was not found on this server. Install the ${provider} CLI, or make sure it is on the PATH the streamer runs with.`
3785
+ );
3786
+ err.statusCode = 503;
3787
+ err.code = "PROVIDER_NOT_INSTALLED";
3788
+ throw err;
3789
+ }
3728
3790
  assertSupportedProvider(provider, projectPath) {
3729
3791
  if (this.remoteRunner) return this.remoteRunner;
3730
3792
  const runner = this.runners.get(provider);
@@ -4407,7 +4469,9 @@ var corsMiddleware = (configValue) => {
4407
4469
  // src/api/middleware/error.middleware.ts
4408
4470
  var errorMiddleware = (err, c) => {
4409
4471
  const message = err instanceof Error ? err.message : "Internal server error";
4410
- return c.json({ error: message }, 500);
4472
+ const { statusCode, code } = err;
4473
+ const status = typeof statusCode === "number" && statusCode >= 400 && statusCode <= 599 ? statusCode : 500;
4474
+ return c.json(typeof code === "string" ? { error: message, code } : { error: message }, status);
4411
4475
  };
4412
4476
 
4413
4477
  // src/api/routes/backup.routes.ts
@@ -4962,24 +5026,26 @@ function computeBootToken() {
4962
5026
  }
4963
5027
 
4964
5028
  // src/api/routes/diagnostics.routes.ts
4965
- function providerCheck(name, resolve2) {
5029
+ function providerCheck(name) {
4966
5030
  try {
4967
- const exe = resolve2();
4968
- return {
4969
- id: `provider:${name}`,
4970
- status: "ok",
4971
- summary: `${name} CLI is installed.`,
4972
- remediation: "NONE",
4973
- detail: { location: redactPath(exe) }
4974
- };
5031
+ const exe = locateProviderExe(name);
5032
+ if (exe !== null) {
5033
+ return {
5034
+ id: `provider:${name}`,
5035
+ status: "ok",
5036
+ summary: `${name} CLI is installed.`,
5037
+ remediation: "NONE",
5038
+ detail: { location: redactPath(exe) }
5039
+ };
5040
+ }
4975
5041
  } catch {
4976
- return {
4977
- id: `provider:${name}`,
4978
- status: "failed",
4979
- summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
4980
- remediation: "PROVIDER_NOT_INSTALLED"
4981
- };
4982
5042
  }
5043
+ return {
5044
+ id: `provider:${name}`,
5045
+ status: "failed",
5046
+ summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
5047
+ remediation: "PROVIDER_NOT_INSTALLED"
5048
+ };
4983
5049
  }
4984
5050
  var createDiagnosticsRoutes = (deps) => {
4985
5051
  const app = new Hono8();
@@ -4992,8 +5058,8 @@ var createDiagnosticsRoutes = (deps) => {
4992
5058
  remediation: "NONE",
4993
5059
  detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
4994
5060
  });
4995
- checks.push(providerCheck("claude-code", resolveClaudeExe));
4996
- checks.push(providerCheck("codex-cli", resolveCodexExe));
5061
+ checks.push(providerCheck(CLAUDE_CODE_PROVIDER));
5062
+ checks.push(providerCheck(CODEX_CLI_PROVIDER));
4997
5063
  const cacheAlert = deps.cacheMonitor()?.healthzField();
4998
5064
  checks.push(
4999
5065
  cacheAlert ? {
@@ -5098,7 +5164,7 @@ var createHealthRoutes = (deps) => {
5098
5164
  };
5099
5165
 
5100
5166
  // src/api/routes/logs.routes.ts
5101
- import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync } from "fs";
5167
+ import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync as statSync2 } from "fs";
5102
5168
  import { join as join8 } from "path";
5103
5169
  import { Hono as Hono10 } from "hono";
5104
5170
 
@@ -5118,7 +5184,7 @@ function resolveLogPath(source) {
5118
5184
  function pickDefaultSource() {
5119
5185
  for (const source of ["stdout", "stderr", "dev"]) {
5120
5186
  const p = resolveLogPath(source);
5121
- if (existsSync5(p) && statSync(p).size > 0) return source;
5187
+ if (existsSync5(p) && statSync2(p).size > 0) return source;
5122
5188
  }
5123
5189
  return "stdout";
5124
5190
  }
@@ -5176,7 +5242,7 @@ function createLogsRoutes() {
5176
5242
  });
5177
5243
  }
5178
5244
  const { lines, offset, total } = readLogLines(logPath, sinceOffset, limit);
5179
- const stats = statSync(logPath);
5245
+ const stats = statSync2(logPath);
5180
5246
  return c.json({
5181
5247
  logs: lines,
5182
5248
  offset,
@@ -5206,7 +5272,7 @@ function createLogsRoutes() {
5206
5272
  if (!existsSync5(logPath)) {
5207
5273
  return { source, exists: false, total: 0, fileSize: 0 };
5208
5274
  }
5209
- const stats = statSync(logPath);
5275
+ const stats = statSync2(logPath);
5210
5276
  return {
5211
5277
  source,
5212
5278
  exists: true,
@@ -5584,7 +5650,7 @@ function publicKeyOf(privateKey) {
5584
5650
 
5585
5651
  // src/services/push/apnsClient.ts
5586
5652
  import { createSign } from "crypto";
5587
- import { connect, constants } from "http2";
5653
+ import { connect, constants as constants2 } from "http2";
5588
5654
  var log = getLogger("apns");
5589
5655
  var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
5590
5656
  var APNS_MAX_PAYLOAD_BYTES = 4096;
@@ -5691,27 +5757,27 @@ var ApnsClient = class {
5691
5757
  }
5692
5758
  const session = this.getSession();
5693
5759
  const headers = {
5694
- [constants.HTTP2_HEADER_METHOD]: "POST",
5695
- [constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
5696
- [constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
5760
+ [constants2.HTTP2_HEADER_METHOD]: "POST",
5761
+ [constants2.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
5762
+ [constants2.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
5697
5763
  "apns-push-type": "liveactivity",
5698
5764
  "apns-topic": this.topic,
5699
5765
  "apns-priority": String(args.priority ?? 10),
5700
5766
  ...args.expirationSeconds != null && {
5701
5767
  "apns-expiration": String(args.expirationSeconds)
5702
5768
  },
5703
- [constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
5704
- [constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
5769
+ [constants2.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
5770
+ [constants2.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
5705
5771
  };
5706
5772
  return new Promise((resolve2, reject) => {
5707
5773
  const req = session.request(headers);
5708
5774
  req.setTimeout(args.timeoutMs ?? 1e4, () => {
5709
- req.close(constants.NGHTTP2_CANCEL);
5775
+ req.close(constants2.NGHTTP2_CANCEL);
5710
5776
  resolve2({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
5711
5777
  });
5712
5778
  let status = 0;
5713
5779
  req.on("response", (resHeaders) => {
5714
- status = Number(resHeaders[constants.HTTP2_HEADER_STATUS] ?? 0);
5780
+ status = Number(resHeaders[constants2.HTTP2_HEADER_STATUS] ?? 0);
5715
5781
  });
5716
5782
  const chunks = [];
5717
5783
  req.on("data", (chunk) => chunks.push(chunk));
@@ -6065,7 +6131,10 @@ function parseVersionOutput(output) {
6065
6131
  const match = output.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
6066
6132
  return match ? match[0] : null;
6067
6133
  }
6134
+ var versionByExe = /* @__PURE__ */ new Map();
6068
6135
  function runVersion(exe) {
6136
+ const cached3 = versionByExe.get(exe);
6137
+ if (cached3 !== void 0) return Promise.resolve(cached3);
6069
6138
  const viaShell = isWindows && /\.(?:cmd|bat)$/i.test(exe);
6070
6139
  const file = viaShell ? `"${exe}"` : exe;
6071
6140
  return new Promise((resolve2) => {
@@ -6075,7 +6144,9 @@ function runVersion(exe) {
6075
6144
  { timeout: VERSION_TIMEOUT_MS, shell: viaShell, windowsHide: true },
6076
6145
  (err, stdout, stderr) => {
6077
6146
  if (err && !stdout && !stderr) return resolve2(null);
6078
- resolve2(parseVersionOutput(`${stdout}${stderr}`));
6147
+ const version = parseVersionOutput(`${stdout}${stderr}`);
6148
+ if (version !== null) versionByExe.set(exe, version);
6149
+ resolve2(version);
6079
6150
  }
6080
6151
  );
6081
6152
  });
@@ -6119,13 +6190,16 @@ function compareSemver(a, b) {
6119
6190
  if (pb.pre === null) return -1;
6120
6191
  return pa.pre < pb.pre ? -1 : 1;
6121
6192
  }
6122
- async function providerHealth(name, resolveExe, detect = runVersion) {
6193
+ async function providerHealth(name, locateExe = () => locateProviderExe(name), detect = runVersion) {
6123
6194
  const verifiedAgainst = VERIFIED_AGAINST[name];
6124
6195
  const capabilities = capabilitiesFor(name);
6125
- let exe;
6196
+ let exe = null;
6126
6197
  try {
6127
- exe = resolveExe();
6198
+ exe = locateExe();
6128
6199
  } catch {
6200
+ exe = null;
6201
+ }
6202
+ if (exe === null) {
6129
6203
  return {
6130
6204
  name,
6131
6205
  available: false,
@@ -6162,8 +6236,8 @@ var createProviderRoutes = () => {
6162
6236
  const app = new Hono14();
6163
6237
  app.get("/", async (c) => {
6164
6238
  const providers = await Promise.all([
6165
- providerHealth(CLAUDE_CODE_PROVIDER, resolveClaudeExe),
6166
- providerHealth(CODEX_CLI_PROVIDER, resolveCodexExe)
6239
+ providerHealth(CLAUDE_CODE_PROVIDER),
6240
+ providerHealth(CODEX_CLI_PROVIDER)
6167
6241
  ]);
6168
6242
  return c.json({ providers });
6169
6243
  });
@@ -6394,7 +6468,7 @@ import {
6394
6468
  parseJsonlLine
6395
6469
  } from "@threadbase-sh/scanner";
6396
6470
  import Database from "better-sqlite3";
6397
- import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync3, readSync as readSync3, statSync as statSync3 } from "fs";
6471
+ import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync3, readSync as readSync3, statSync as statSync4 } from "fs";
6398
6472
  import { open as openAsync } from "fs/promises";
6399
6473
  import { dirname as dirname8 } from "path";
6400
6474
  import { setImmediate as yieldToEventLoop } from "timers/promises";
@@ -6512,7 +6586,7 @@ function runSqliteMigrations(db, migrationsDir) {
6512
6586
  }
6513
6587
 
6514
6588
  // src/services/conversations/isAgentConversation.ts
6515
- import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "fs";
6589
+ import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync3 } from "fs";
6516
6590
  var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
6517
6591
  var CHUNK_BYTES = 64 * 1024;
6518
6592
  var ENTRYPOINT_PROBE = `"entrypoint":`;
@@ -6539,7 +6613,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
6539
6613
  return false;
6540
6614
  }
6541
6615
  try {
6542
- const fileSize = statSync2(filePath).size;
6616
+ const fileSize = statSync3(filePath).size;
6543
6617
  if (fileSize === 0) {
6544
6618
  fileDecisionCache.set(key, false);
6545
6619
  return false;
@@ -7177,7 +7251,7 @@ var ConversationCache = class _ConversationCache {
7177
7251
  if (!fileState) return null;
7178
7252
  let stat3;
7179
7253
  try {
7180
- stat3 = statSync3(filePath);
7254
+ stat3 = statSync4(filePath);
7181
7255
  } catch {
7182
7256
  return null;
7183
7257
  }
@@ -7236,7 +7310,7 @@ var ConversationCache = class _ConversationCache {
7236
7310
  isAgentFileCached(filePath) {
7237
7311
  let s;
7238
7312
  try {
7239
- s = statSync3(filePath);
7313
+ s = statSync4(filePath);
7240
7314
  } catch {
7241
7315
  return false;
7242
7316
  }
@@ -7447,7 +7521,7 @@ var ConversationCache = class _ConversationCache {
7447
7521
  let mtimeMs = null;
7448
7522
  let fileSize = null;
7449
7523
  try {
7450
- const s = statSync3(m.filePath);
7524
+ const s = statSync4(m.filePath);
7451
7525
  mtimeMs = s.mtimeMs;
7452
7526
  fileSize = s.size;
7453
7527
  } catch {
@@ -7507,7 +7581,7 @@ var ConversationCache = class _ConversationCache {
7507
7581
  let fileSize;
7508
7582
  let fd;
7509
7583
  try {
7510
- fileSize = statSync3(filePath).size;
7584
+ fileSize = statSync4(filePath).size;
7511
7585
  fd = openSync3(filePath, "r");
7512
7586
  } catch {
7513
7587
  return false;
@@ -9503,7 +9577,7 @@ async function findRolloutOwner(rolloutPath, options = {}) {
9503
9577
  }
9504
9578
 
9505
9579
  // src/services/sessions/conversationBusy.ts
9506
- import { statSync as statSync4 } from "fs";
9580
+ import { statSync as statSync5 } from "fs";
9507
9581
 
9508
9582
  // src/utils/canonicalizeProjectPath.ts
9509
9583
  function canonicalizeProjectPath(projectPath) {
@@ -9527,7 +9601,7 @@ function conversationBusy(input) {
9527
9601
  let lastActivityMs = null;
9528
9602
  if (input.jsonlPath) {
9529
9603
  try {
9530
- const mtimeMs = statSync4(input.jsonlPath).mtimeMs;
9604
+ const mtimeMs = statSync5(input.jsonlPath).mtimeMs;
9531
9605
  const age = now - mtimeMs;
9532
9606
  lastActivityMs = Math.max(0, age);
9533
9607
  const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
@@ -10642,7 +10716,10 @@ var SessionHandlers = class {
10642
10716
  });
10643
10717
  this.sessionStore.addManaged(session);
10644
10718
  this.registryBoot.recordSessionSpawn(session);
10645
- const { outcome } = await this.deps.waitForStartupOutcome(session.id, START_READY_TIMEOUT_MS);
10719
+ const { outcome, session: settled } = await this.deps.waitForStartupOutcome(
10720
+ session.id,
10721
+ START_READY_TIMEOUT_MS
10722
+ );
10646
10723
  const current = this.sessionStore.get(session.id, this.deps.ptyAttachedIds());
10647
10724
  if (outcome === "ready" && current) {
10648
10725
  json(res, 200, { session: current });
@@ -10650,7 +10727,7 @@ var SessionHandlers = class {
10650
10727
  json(res, 502, {
10651
10728
  id: session.id,
10652
10729
  status: "idle",
10653
- error: current.failureReason ?? "Session exited before becoming ready"
10730
+ error: settled?.failureReason ?? "Session exited before becoming ready"
10654
10731
  });
10655
10732
  } else {
10656
10733
  json(res, 202, { id: session.id, status: "pending" });
@@ -10664,11 +10741,16 @@ var SessionHandlers = class {
10664
10741
  } catch (err) {
10665
10742
  const message = err instanceof Error ? err.message : "Failed to start session";
10666
10743
  const statusCode = typeof err.statusCode === "number" ? err.statusCode : 500;
10744
+ const code = err.code;
10667
10745
  this.log.error(`[start] failed to start session: ${message}`, {
10668
10746
  event: "session.start_failed",
10669
10747
  error: message
10670
10748
  });
10671
- json(res, statusCode, { error: message });
10749
+ json(
10750
+ res,
10751
+ statusCode,
10752
+ typeof code === "string" ? { error: message, code } : { error: message }
10753
+ );
10672
10754
  }
10673
10755
  }
10674
10756
  async handleSetSessionName(sessionId, req, res) {
@@ -11158,7 +11240,7 @@ var RuntimeStore = class _RuntimeStore {
11158
11240
  };
11159
11241
 
11160
11242
  // src/external-tails.ts
11161
- import { statSync as statSync5 } from "fs";
11243
+ import { statSync as statSync6 } from "fs";
11162
11244
  var EXTERNAL_TAIL_RECENCY_MS = RESUME_BUSY_WINDOW_MS;
11163
11245
  var EXTERNAL_TAIL_MAX = 32;
11164
11246
  var EXTERNAL_TAIL_IDLE_MS = 3e5;
@@ -11191,7 +11273,7 @@ var ExternalTailManager = class {
11191
11273
  if (this.isManagedTailPath(key)) return;
11192
11274
  let mtimeMs;
11193
11275
  try {
11194
- mtimeMs = statSync5(filePath).mtimeMs;
11276
+ mtimeMs = statSync6(filePath).mtimeMs;
11195
11277
  } catch {
11196
11278
  return;
11197
11279
  }
@@ -11496,7 +11578,7 @@ function spawnDetachedHost(socketPath, entryPoint) {
11496
11578
  import {
11497
11579
  ConversationScanner
11498
11580
  } from "@threadbase-sh/scanner";
11499
- import { statSync as statSync7 } from "fs";
11581
+ import { statSync as statSync8 } from "fs";
11500
11582
  import { homedir as homedir10 } from "os";
11501
11583
  import { join as join20 } from "path";
11502
11584
 
@@ -11597,14 +11679,14 @@ function refreshConversationCache(deps) {
11597
11679
  }
11598
11680
 
11599
11681
  // src/services/conversations/shouldRefreshProjectsFromHdd.ts
11600
- import { readdirSync as readdirSync4, statSync as statSync6 } from "fs";
11682
+ import { readdirSync as readdirSync4, statSync as statSync7 } from "fs";
11601
11683
  import { homedir as homedir9 } from "os";
11602
11684
  import { join as join19 } from "path";
11603
11685
  var DEFAULT_PROJECTS_DIR = join19(homedir9(), ".claude", "projects");
11604
11686
  function maxProjectsTreeMtimeMs(projectsDir) {
11605
11687
  let maxMs;
11606
11688
  try {
11607
- maxMs = statSync6(projectsDir).mtimeMs;
11689
+ maxMs = statSync7(projectsDir).mtimeMs;
11608
11690
  } catch {
11609
11691
  return null;
11610
11692
  }
@@ -11612,7 +11694,7 @@ function maxProjectsTreeMtimeMs(projectsDir) {
11612
11694
  for (const ent of readdirSync4(projectsDir, { withFileTypes: true })) {
11613
11695
  if (!ent.isDirectory()) continue;
11614
11696
  try {
11615
- const childMs = statSync6(join19(projectsDir, ent.name)).mtimeMs;
11697
+ const childMs = statSync7(join19(projectsDir, ent.name)).mtimeMs;
11616
11698
  if (childMs > maxMs) maxMs = childMs;
11617
11699
  } catch {
11618
11700
  }
@@ -11824,7 +11906,7 @@ var ScannerManager = class {
11824
11906
  if (!conv.filePath) return false;
11825
11907
  let mtimeMs = null;
11826
11908
  try {
11827
- mtimeMs = statSync7(conv.filePath).mtimeMs;
11909
+ mtimeMs = statSync8(conv.filePath).mtimeMs;
11828
11910
  } catch {
11829
11911
  return false;
11830
11912
  }
@@ -12063,10 +12145,10 @@ function seal(plaintext, recipientPublicKeyBase64) {
12063
12145
  }
12064
12146
 
12065
12147
  // src/server-wiring.ts
12066
- import { statSync as statSync9 } from "fs";
12148
+ import { statSync as statSync10 } from "fs";
12067
12149
 
12068
12150
  // src/handlers/handleListProjects.ts
12069
- import { closeSync as closeSync4, openSync as openSync4, readdirSync as readdirSync5, readSync as readSync4, statSync as statSync8 } from "fs";
12151
+ import { closeSync as closeSync4, openSync as openSync4, readdirSync as readdirSync5, readSync as readSync4, statSync as statSync9 } from "fs";
12070
12152
  import { homedir as homedir11 } from "os";
12071
12153
  import { join as join21 } from "path";
12072
12154
  var HEAD_BYTES = 64 * 1024;
@@ -12112,7 +12194,7 @@ function handleListProjects(url, res) {
12112
12194
  const fullPath = join21(projectsDir, dirName);
12113
12195
  let mtime = 0;
12114
12196
  try {
12115
- mtime = statSync8(fullPath).mtimeMs;
12197
+ mtime = statSync9(fullPath).mtimeMs;
12116
12198
  } catch {
12117
12199
  }
12118
12200
  return { dirName: String(dirName), mtime };
@@ -12146,7 +12228,7 @@ function createConversationWatcherEvents(deps) {
12146
12228
  const seqs = cache.extendMessageIndex(
12147
12229
  filePath,
12148
12230
  spans,
12149
- statSync9(filePath),
12231
+ statSync10(filePath),
12150
12232
  readFrom,
12151
12233
  endOffset
12152
12234
  );
@@ -12193,7 +12275,7 @@ function createConversationWatcherEvents(deps) {
12193
12275
  },
12194
12276
  onConversationChanged: (filePath) => {
12195
12277
  try {
12196
- statSync9(filePath);
12278
+ statSync10(filePath);
12197
12279
  } catch {
12198
12280
  deps.externalTailManager().handleJsonlDeleted(filePath);
12199
12281
  return;
@@ -12291,7 +12373,15 @@ function createLiveSessionOptions(deps) {
12291
12373
  // grace-timer/idle-reaper hold (statusSource "shutdown") apart from a
12292
12374
  // genuine process exit ("process-exit"), and reports both as
12293
12375
  // `lifecycle: "completed"`. See managedToResponse in session-store.ts.
12294
- ...session.statusSource != null && { statusSource: session.statusSource }
12376
+ ...session.statusSource != null && { statusSource: session.statusSource },
12377
+ // Why a session died, not just that it did. Without this the store's
12378
+ // copy has no failureReason, so managedToResponse falls through to
12379
+ // `lifecycle: "completed"` (see session-store.ts) and a session that
12380
+ // never started — missing CLI, missing project dir — is reported to
12381
+ // every client as one that finished normally. Guarded like its
12382
+ // neighbours: a later transition must not blank a recorded failure.
12383
+ ...session.failureReason != null && { failureReason: session.failureReason },
12384
+ ...session.failureCode != null && { failureCode: session.failureCode }
12295
12385
  });
12296
12386
  deps.managedSessionsRepo()?.recordStatus(
12297
12387
  session.id,
@@ -12546,7 +12636,7 @@ function saveAlertState(state) {
12546
12636
  }
12547
12637
 
12548
12638
  // src/services/cache-integrity/backup.ts
12549
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync6, statSync as statSync10, unlinkSync } from "fs";
12639
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync6, statSync as statSync11, unlinkSync } from "fs";
12550
12640
  import { join as join23 } from "path";
12551
12641
  var DEFAULT_RETAIN = 3;
12552
12642
  function retainCount() {
@@ -12565,7 +12655,7 @@ async function backupCacheDb(db, cacheDir) {
12565
12655
  const retain = retainCount();
12566
12656
  const backups = readdirSync6(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
12567
12657
  const full = join23(backupsDir, f);
12568
- return { full, mtime: statSync10(full).mtimeMs };
12658
+ return { full, mtime: statSync11(full).mtimeMs };
12569
12659
  }).sort((a, b) => b.mtime - a.mtime);
12570
12660
  for (const stale of backups.slice(retain)) {
12571
12661
  if (existsSync11(stale.full)) unlinkSync(stale.full);
@@ -12831,7 +12921,7 @@ var CacheIntegrityMonitor = class {
12831
12921
 
12832
12922
  // src/services/conversations/conversationWatcher.ts
12833
12923
  import chokidar from "chokidar";
12834
- import { statSync as statSync11 } from "fs";
12924
+ import { statSync as statSync12 } from "fs";
12835
12925
  import { open, stat as stat2 } from "fs/promises";
12836
12926
  var ConversationWatcher = class {
12837
12927
  files = /* @__PURE__ */ new Map();
@@ -12857,7 +12947,7 @@ var ConversationWatcher = class {
12857
12947
  if (this.files.has(key)) return;
12858
12948
  let offset;
12859
12949
  try {
12860
- offset = statSync11(filePath).size;
12950
+ offset = statSync12(filePath).size;
12861
12951
  } catch {
12862
12952
  offset = 0;
12863
12953
  }
@@ -14532,7 +14622,7 @@ function discoveredToResponse(d, conversationId) {
14532
14622
  }
14533
14623
 
14534
14624
  // src/session-watchers.ts
14535
- import { existsSync as existsSync15, watch as fsWatch, readdirSync as readdirSync7, readFileSync as readFileSync10, statSync as statSync12 } from "fs";
14625
+ import { existsSync as existsSync15, watch as fsWatch, readdirSync as readdirSync7, readFileSync as readFileSync10, statSync as statSync13 } from "fs";
14536
14626
  import { homedir as homedir13 } from "os";
14537
14627
  import { basename as basename6, join as join24 } from "path";
14538
14628
  var SessionWatchers = class {
@@ -14639,7 +14729,7 @@ var SessionWatchers = class {
14639
14729
  if (!resolvedFilePath && existsSync15(projectsDir)) {
14640
14730
  try {
14641
14731
  const now = Date.now();
14642
- const match = readdirSync7(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync12(join24(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
14732
+ const match = readdirSync7(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync13(join24(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
14643
14733
  ({ f }) => basename6(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join24(projectsDir, f)) === sessionId
14644
14734
  ).sort((a, b) => b.mtime - a.mtime)[0];
14645
14735
  if (match) resolvedFilePath = join24(projectsDir, match.f);
@@ -14736,7 +14826,7 @@ var SessionWatchers = class {
14736
14826
  continue;
14737
14827
  }
14738
14828
  const nowMs = Date.now();
14739
- const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync12(join24(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
14829
+ const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync13(join24(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
14740
14830
  for (const { f } of recentCandidates) {
14741
14831
  const candidatePath = join24(sessionsDir, f);
14742
14832
  const match = matchesProjectPath(candidatePath);
@@ -15746,6 +15836,41 @@ var StreamerServer = class {
15746
15836
  json(res, 503, body);
15747
15837
  return true;
15748
15838
  }
15839
+ /**
15840
+ * Say which provider CLIs this machine can actually launch.
15841
+ *
15842
+ * The operator cannot discover this case unaided: under launchd/Task
15843
+ * Scheduler the service inherits a stripped PATH, so a CLI that works
15844
+ * perfectly in their terminal is invisible to the service, and every session
15845
+ * start dies milliseconds in. `/api/diagnostics` answers it too, but only for
15846
+ * someone who already suspects it.
15847
+ *
15848
+ * Availability only, never a version — `--version` costs a process spawn per
15849
+ * provider (85ms for claude here) and belongs on the first request that wants
15850
+ * it, not on boot.
15851
+ *
15852
+ * Called AFTER the port is bound, which is not cosmetic. This is the first
15853
+ * caller of the exe resolvers in the process, so the memo is cold by
15854
+ * definition and each provider pays one synchronous `which` / `where.exe`
15855
+ * (platform.ts) with a 3s timeout. On POSIX that is 3ms found, 7ms missing.
15856
+ * Windows is the risk — `where.exe` is slower, `execFileSync` blocks the
15857
+ * event loop, and Task Scheduler's stripped PATH is exactly where a miss
15858
+ * pays the full timeout — so the worst case is ~6s of two blocking lookups.
15859
+ * After `listen()` that delays the first requests on a box that cannot start
15860
+ * a session anyway; before it, it would have delayed binding the port.
15861
+ */
15862
+ logProviderAvailability() {
15863
+ for (const provider of [CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER]) {
15864
+ if (locateProviderExe(provider)) {
15865
+ this.log.info(`Provider ${provider}: found`, { event: "config.provider", provider });
15866
+ } else {
15867
+ this.log.warn(`Provider ${provider}: not found on PATH \u2014 sessions cannot start`, {
15868
+ event: "config.provider_missing",
15869
+ provider
15870
+ });
15871
+ }
15872
+ }
15873
+ }
15749
15874
  async listen(port, opts) {
15750
15875
  if (this.featureFlags.ptyHost) {
15751
15876
  try {
@@ -15806,6 +15931,7 @@ var StreamerServer = class {
15806
15931
  event: "server.listening",
15807
15932
  ...this.host !== void 0 && { host: this.host }
15808
15933
  });
15934
+ this.logProviderAvailability();
15809
15935
  try {
15810
15936
  this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
15811
15937
  this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());