@threadbase-sh/streamer 1.43.0 → 1.44.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/cli.cjs CHANGED
@@ -4637,11 +4637,11 @@ var require_tools = __commonJS({
4637
4637
  }
4638
4638
  }
4639
4639
  }
4640
- function buildFormatters(level, bindings, log12) {
4640
+ function buildFormatters(level, bindings, log13) {
4641
4641
  return {
4642
4642
  level,
4643
4643
  bindings,
4644
- log: log12
4644
+ log: log13
4645
4645
  };
4646
4646
  }
4647
4647
  function normalizeDestFileDescriptor(destination) {
@@ -5023,11 +5023,11 @@ var require_proto = __commonJS({
5023
5023
  }
5024
5024
  } else instance[serializersSym] = serializers;
5025
5025
  if (options.hasOwnProperty("formatters")) {
5026
- const { level, bindings: chindings, log: log12 } = options.formatters;
5026
+ const { level, bindings: chindings, log: log13 } = options.formatters;
5027
5027
  instance[formattersSym] = buildFormatters(
5028
5028
  level || formatters.level,
5029
5029
  chindings || resetChildingsFormatter,
5030
- log12 || formatters.log
5030
+ log13 || formatters.log
5031
5031
  );
5032
5032
  } else {
5033
5033
  instance[formattersSym] = buildFormatters(
@@ -6097,6 +6097,9 @@ var require_pino = __commonJS({
6097
6097
  });
6098
6098
 
6099
6099
  // src/logger.ts
6100
+ function defaultDest() {
6101
+ return process.stdout.isTTY ? "console" : "pino";
6102
+ }
6100
6103
  function emit(pinoChild, level, msg, fields, dest) {
6101
6104
  if (dest === "pino" || dest === "both") {
6102
6105
  if (fields && Object.keys(fields).length > 0) pinoChild[level](fields, msg);
@@ -6109,11 +6112,11 @@ function emit(pinoChild, level, msg, fields, dest) {
6109
6112
  }
6110
6113
  function build(pinoChild) {
6111
6114
  return {
6112
- debug: (m2, f2, d = "both") => emit(pinoChild, "debug", m2, f2, d),
6113
- info: (m2, f2, d = "both") => emit(pinoChild, "info", m2, f2, d),
6114
- warn: (m2, f2, d = "both") => emit(pinoChild, "warn", m2, f2, d),
6115
- error: (m2, f2, d = "both") => emit(pinoChild, "error", m2, f2, d),
6116
- log: (lvl, m2, f2, d = "both") => emit(pinoChild, lvl, m2, f2, d),
6115
+ debug: (m2, f2, d = defaultDest()) => emit(pinoChild, "debug", m2, f2, d),
6116
+ info: (m2, f2, d = defaultDest()) => emit(pinoChild, "info", m2, f2, d),
6117
+ warn: (m2, f2, d = defaultDest()) => emit(pinoChild, "warn", m2, f2, d),
6118
+ error: (m2, f2, d = defaultDest()) => emit(pinoChild, "error", m2, f2, d),
6119
+ log: (lvl, m2, f2, d = defaultDest()) => emit(pinoChild, lvl, m2, f2, d),
6117
6120
  pino: pinoChild
6118
6121
  };
6119
6122
  }
@@ -6219,6 +6222,12 @@ var init_feature_flags = __esm({
6219
6222
  description: "Seed the session list at boot with sessions a previous streamer run left behind, so a restart leaves them one tap from resuming instead of silently gone. On by default, with a kill switch: it changes what GET /api/sessions contains.",
6220
6223
  default: true,
6221
6224
  env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
6225
+ },
6226
+ {
6227
+ id: "ptyHost",
6228
+ description: "Keep live PTYs in a separate host process so a streamer restart can reconnect without restarting the agents. Off by default until cross-platform behavior is qualified.",
6229
+ default: false,
6230
+ env: "THREADBASE_FEATURE_PTY_HOST"
6222
6231
  }
6223
6232
  ];
6224
6233
  }
@@ -8197,7 +8206,7 @@ var require_merge = __commonJS({
8197
8206
  var require_addPairToJSMap = __commonJS({
8198
8207
  "node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports2) {
8199
8208
  "use strict";
8200
- var log12 = require_log();
8209
+ var log13 = require_log();
8201
8210
  var merge2 = require_merge();
8202
8211
  var stringify = require_stringify();
8203
8212
  var identity = require_identity();
@@ -8246,7 +8255,7 @@ var require_addPairToJSMap = __commonJS({
8246
8255
  let jsonStr = JSON.stringify(strKey);
8247
8256
  if (jsonStr.length > 40)
8248
8257
  jsonStr = jsonStr.substring(0, 36) + '..."';
8249
- log12.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
8258
+ log13.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
8250
8259
  ctx.mapKeyWarned = true;
8251
8260
  }
8252
8261
  return strKey;
@@ -13662,7 +13671,7 @@ var require_public_api = __commonJS({
13662
13671
  var composer = require_composer();
13663
13672
  var Document = require_Document();
13664
13673
  var errors = require_errors();
13665
- var log12 = require_log();
13674
+ var log13 = require_log();
13666
13675
  var identity = require_identity();
13667
13676
  var lineCounter = require_line_counter();
13668
13677
  var parser = require_parser();
@@ -13714,7 +13723,7 @@ var require_public_api = __commonJS({
13714
13723
  const doc = parseDocument(src, options);
13715
13724
  if (!doc)
13716
13725
  return null;
13717
- doc.warnings.forEach((warning) => log12.warn(doc.options.logLevel, warning));
13726
+ doc.warnings.forEach((warning) => log13.warn(doc.options.logLevel, warning));
13718
13727
  if (doc.errors.length > 0) {
13719
13728
  if (doc.options.logLevel !== "silent")
13720
13729
  throw doc.errors[0];
@@ -32964,11 +32973,11 @@ var require_type_helpers = __commonJS({
32964
32973
  function isRecord(value) {
32965
32974
  return typeof value === "object" && value !== null;
32966
32975
  }
32967
- function hasOwnProperty(record2, prop) {
32968
- return prop in record2;
32976
+ function hasOwnProperty(record3, prop) {
32977
+ return prop in record3;
32969
32978
  }
32970
- function hasOwnProperties(record2, props) {
32971
- return props.every((prop) => prop in record2);
32979
+ function hasOwnProperties(record3, props) {
32980
+ return props.every((prop) => prop in record3);
32972
32981
  }
32973
32982
  function isError(error51) {
32974
32983
  return isRecord(error51) && typeof error51.name === "string" && typeof error51.message === "string" && (error51.stack == null || typeof error51.stack === "string");
@@ -37230,7 +37239,7 @@ var require_logging = __commonJS({
37230
37239
  _logVerbosity = verbosity;
37231
37240
  };
37232
37241
  exports2.setLoggerVerbosity = setLoggerVerbosity;
37233
- var log12 = (severity, ...args) => {
37242
+ var log13 = (severity, ...args) => {
37234
37243
  let logFunction;
37235
37244
  if (severity >= _logVerbosity) {
37236
37245
  switch (severity) {
@@ -37252,7 +37261,7 @@ var require_logging = __commonJS({
37252
37261
  }
37253
37262
  }
37254
37263
  };
37255
- exports2.log = log12;
37264
+ exports2.log = log13;
37256
37265
  var tracersString = (_d = (_c = process.env.GRPC_NODE_TRACE) !== null && _c !== void 0 ? _c : process.env.GRPC_TRACE) !== null && _d !== void 0 ? _d : "";
37257
37266
  var enabledTracers = /* @__PURE__ */ new Set();
37258
37267
  var disabledTracers = /* @__PURE__ */ new Set();
@@ -38663,9 +38672,9 @@ var require_service_config = __commonJS({
38663
38672
  throw new Error("No matching service config found");
38664
38673
  }
38665
38674
  function extractAndSelectServiceConfig(txtRecord, percentage) {
38666
- for (const record2 of txtRecord) {
38667
- if (record2.length > 0 && record2[0].startsWith("grpc_config=")) {
38668
- const recordString = record2.join("").substring("grpc_config=".length);
38675
+ for (const record3 of txtRecord) {
38676
+ if (record3.length > 0 && record3[0].startsWith("grpc_config=")) {
38677
+ const recordString = record3.join("").substring("grpc_config=".length);
38669
38678
  const recordJson = JSON.parse(recordString);
38670
38679
  return validateAndSelectCanaryConfig(recordJson, percentage);
38671
38680
  }
@@ -100690,11 +100699,11 @@ var require_out = __commonJS({
100690
100699
  async.read(path2, getSettings(optionsOrSettingsOrCallback), callback);
100691
100700
  }
100692
100701
  exports2.stat = stat7;
100693
- function statSync14(path2, optionsOrSettings) {
100702
+ function statSync15(path2, optionsOrSettings) {
100694
100703
  const settings = getSettings(optionsOrSettings);
100695
100704
  return sync.read(path2, settings);
100696
100705
  }
100697
- exports2.statSync = statSync14;
100706
+ exports2.statSync = statSync15;
100698
100707
  function getSettings(settingsOrOptions = {}) {
100699
100708
  if (settingsOrOptions instanceof settings_1.default) {
100700
100709
  return settingsOrOptions;
@@ -102581,6 +102590,14 @@ var require_out4 = __commonJS({
102581
102590
  });
102582
102591
 
102583
102592
  // src/platform.ts
102593
+ function isWindowsExecutablePath(path2) {
102594
+ const dot = path2.lastIndexOf(".");
102595
+ if (dot < 0) return false;
102596
+ return WINDOWS_EXECUTABLE_EXTENSIONS.has(path2.slice(dot).toLowerCase());
102597
+ }
102598
+ function clearClaudeExeCache() {
102599
+ _claudeExe = void 0;
102600
+ }
102584
102601
  function resolveClaudeExe() {
102585
102602
  if (_claudeExe !== void 0) return _claudeExe;
102586
102603
  if (isWindows2) {
@@ -102589,7 +102606,7 @@ function resolveClaudeExe() {
102589
102606
  encoding: "utf-8",
102590
102607
  windowsHide: true,
102591
102608
  timeout: 3e3
102592
- }).trim().split("\n")[0].trim();
102609
+ }).trim().split("\n").map((line) => line.trim()).find(isWindowsExecutablePath);
102593
102610
  if (found) {
102594
102611
  _claudeExe = found;
102595
102612
  return _claudeExe;
@@ -102638,6 +102655,9 @@ function resolveClaudeExe() {
102638
102655
  _claudeExe = "claude";
102639
102656
  return _claudeExe;
102640
102657
  }
102658
+ function clearCodexExeCache() {
102659
+ _codexExe = void 0;
102660
+ }
102641
102661
  function resolveCodexExe() {
102642
102662
  if (_codexExe !== void 0) return _codexExe;
102643
102663
  if (isWindows2) {
@@ -102646,7 +102666,7 @@ function resolveCodexExe() {
102646
102666
  encoding: "utf-8",
102647
102667
  windowsHide: true,
102648
102668
  timeout: 3e3
102649
- }).trim().split("\n")[0].trim();
102669
+ }).trim().split("\n").map((line) => line.trim()).find(isWindowsExecutablePath);
102650
102670
  if (found) {
102651
102671
  _codexExe = found;
102652
102672
  return _codexExe;
@@ -102695,7 +102715,7 @@ function resolveCodexExe() {
102695
102715
  _codexExe = "codex";
102696
102716
  return _codexExe;
102697
102717
  }
102698
- var import_child_process, import_fs13, import_os6, import_path13, isWindows2, _claudeExe, _codexExe;
102718
+ var import_child_process, import_fs13, import_os6, import_path13, isWindows2, WINDOWS_EXECUTABLE_EXTENSIONS, _claudeExe, _codexExe;
102699
102719
  var init_platform = __esm({
102700
102720
  "src/platform.ts"() {
102701
102721
  "use strict";
@@ -102704,6 +102724,7 @@ var init_platform = __esm({
102704
102724
  import_os6 = require("os");
102705
102725
  import_path13 = require("path");
102706
102726
  isWindows2 = (0, import_os6.platform)() === "win32";
102727
+ WINDOWS_EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([".exe", ".cmd", ".bat"]);
102707
102728
  }
102708
102729
  });
102709
102730
 
@@ -113213,20 +113234,26 @@ var init_codex_pty_runner = __esm({
113213
113234
  async doStart(sessionId, options) {
113214
113235
  const nodePty = await loadPty();
113215
113236
  const projectName = options.projectName ?? (0, import_path21.basename)(options.projectPath);
113216
- const proc = nodePty.spawn(
113217
- resolveCodexExe(),
113218
- // `sessionId` stays the runner's map key — only argv carries the
113219
- // provider-side id, so a resumed Codex session keeps the placeholder id
113220
- // its client already navigated to.
113221
- ["resume", options.resumeId ?? sessionId, "--cd", options.projectPath, "--no-alt-screen"],
113222
- {
113223
- name: "xterm-256color",
113224
- cols: PTY_COLS,
113225
- rows: PTY_ROWS,
113226
- cwd: options.projectPath,
113227
- env: process.env
113228
- }
113229
- );
113237
+ let proc;
113238
+ try {
113239
+ proc = nodePty.spawn(
113240
+ resolveCodexExe(),
113241
+ // `sessionId` stays the runner's map key — only argv carries the
113242
+ // provider-side id, so a resumed Codex session keeps the placeholder id
113243
+ // its client already navigated to.
113244
+ ["resume", options.resumeId ?? sessionId, "--cd", options.projectPath, "--no-alt-screen"],
113245
+ {
113246
+ name: "xterm-256color",
113247
+ cols: PTY_COLS,
113248
+ rows: PTY_ROWS,
113249
+ cwd: options.projectPath,
113250
+ env: process.env
113251
+ }
113252
+ );
113253
+ } catch (err) {
113254
+ clearCodexExeCache();
113255
+ throw err;
113256
+ }
113230
113257
  const session = {
113231
113258
  id: sessionId,
113232
113259
  provider: CODEX_CLI_PROVIDER2,
@@ -113269,13 +113296,19 @@ var init_codex_pty_runner = __esm({
113269
113296
  if (options.systemPrompt) {
113270
113297
  args.push(options.systemPrompt);
113271
113298
  }
113272
- const proc = nodePty.spawn(resolveCodexExe(), args, {
113273
- name: "xterm-256color",
113274
- cols: PTY_COLS,
113275
- rows: PTY_ROWS,
113276
- cwd: options.projectPath,
113277
- env: process.env
113278
- });
113299
+ let proc;
113300
+ try {
113301
+ proc = nodePty.spawn(resolveCodexExe(), args, {
113302
+ name: "xterm-256color",
113303
+ cols: PTY_COLS,
113304
+ rows: PTY_ROWS,
113305
+ cwd: options.projectPath,
113306
+ env: process.env
113307
+ });
113308
+ } catch (err) {
113309
+ clearCodexExeCache();
113310
+ throw err;
113311
+ }
113279
113312
  const session = {
113280
113313
  id: sessionId,
113281
113314
  provider: CODEX_CLI_PROVIDER2,
@@ -113730,6 +113763,394 @@ var init_codex_pty_runner = __esm({
113730
113763
  }
113731
113764
  });
113732
113765
 
113766
+ // src/pty-host/protocol.ts
113767
+ function isHostEvent(message) {
113768
+ return "type" in message && message.type === "event";
113769
+ }
113770
+ function reviveSession(raw2) {
113771
+ const s3 = { ...raw2 };
113772
+ for (const field of SESSION_DATE_FIELDS) {
113773
+ const value = s3[field];
113774
+ if (typeof value === "string") s3[field] = new Date(value);
113775
+ }
113776
+ return s3;
113777
+ }
113778
+ function encodeMessage(message) {
113779
+ return `${JSON.stringify(message)}
113780
+ `;
113781
+ }
113782
+ var PTY_HOST_PROTOCOL_VERSION, SESSION_DATE_FIELDS, LineDecoder;
113783
+ var init_protocol = __esm({
113784
+ "src/pty-host/protocol.ts"() {
113785
+ "use strict";
113786
+ PTY_HOST_PROTOCOL_VERSION = 2;
113787
+ SESSION_DATE_FIELDS = [
113788
+ "startedAt",
113789
+ "completedAt",
113790
+ "statusUpdatedAt",
113791
+ "lastActivityAt",
113792
+ "firstMessageAt",
113793
+ "lastMessageAt"
113794
+ ];
113795
+ LineDecoder = class {
113796
+ buffer = "";
113797
+ push(chunk) {
113798
+ this.buffer += chunk;
113799
+ const lines = this.buffer.split("\n");
113800
+ this.buffer = lines.pop() ?? "";
113801
+ return lines.filter((line) => line.length > 0);
113802
+ }
113803
+ };
113804
+ }
113805
+ });
113806
+
113807
+ // src/pty-host/remote-session-runner.ts
113808
+ var PtyHostProtocolMismatchError, HOST_HEARTBEAT_INTERVAL_MS, HOST_HEARTBEAT_REQUEST_TIMEOUT_MS, HOST_SHUTDOWN_REQUEST_TIMEOUT_MS, RemoteSessionRunner;
113809
+ var init_remote_session_runner = __esm({
113810
+ "src/pty-host/remote-session-runner.ts"() {
113811
+ "use strict";
113812
+ init_protocol();
113813
+ PtyHostProtocolMismatchError = class extends Error {
113814
+ constructor(hostVersion, streamerVersion) {
113815
+ super(
113816
+ `pty-host protocol ${hostVersion} is incompatible with streamer protocol ${streamerVersion}`
113817
+ );
113818
+ this.hostVersion = hostVersion;
113819
+ this.streamerVersion = streamerVersion;
113820
+ this.name = "PtyHostProtocolMismatchError";
113821
+ }
113822
+ hostVersion;
113823
+ streamerVersion;
113824
+ };
113825
+ HOST_HEARTBEAT_INTERVAL_MS = 1e4;
113826
+ HOST_HEARTBEAT_REQUEST_TIMEOUT_MS = 5e3;
113827
+ HOST_SHUTDOWN_REQUEST_TIMEOUT_MS = 1e3;
113828
+ RemoteSessionRunner = class _RemoteSessionRunner {
113829
+ transport;
113830
+ options;
113831
+ decoder = new LineDecoder();
113832
+ nextRequestId = 1;
113833
+ pending = /* @__PURE__ */ new Map();
113834
+ /** The mirror. Rebuilt wholesale by `status`, patched by events. */
113835
+ sessions = /* @__PURE__ */ new Map();
113836
+ /** Ring buffers, fed by `output` events so `getOutput` stays synchronous. */
113837
+ output = /* @__PURE__ */ new Map();
113838
+ inputHistory = /* @__PURE__ */ new Map();
113839
+ /** Fixed for a session's lifetime, so only spawn and status carry it. */
113840
+ pids = /* @__PURE__ */ new Map();
113841
+ closed = false;
113842
+ heartbeatTimer = null;
113843
+ heartbeatInFlight = false;
113844
+ /**
113845
+ * The only supported way to build one: a runner whose mirror has not been
113846
+ * seeded yet would answer `hasSession` with a confident, wrong `false` for
113847
+ * every session the host is holding — which reads as "the agent is gone" and
113848
+ * routes the user to start a new one.
113849
+ */
113850
+ static async connect(transport, options = {}) {
113851
+ const runner = new _RemoteSessionRunner(transport, options);
113852
+ const status = await runner.readStatus();
113853
+ if (status.protocolVersion !== PTY_HOST_PROTOCOL_VERSION) {
113854
+ try {
113855
+ await runner.request({ type: "shutdown-host" }, HOST_SHUTDOWN_REQUEST_TIMEOUT_MS);
113856
+ } catch (err) {
113857
+ options.logger?.warn("[pty-host] incompatible host did not acknowledge shutdown", {
113858
+ event: "pty_host.shutdown_failed",
113859
+ err
113860
+ });
113861
+ } finally {
113862
+ runner.dispose();
113863
+ }
113864
+ throw new PtyHostProtocolMismatchError(status.protocolVersion, PTY_HOST_PROTOCOL_VERSION);
113865
+ }
113866
+ await runner.request({ type: "subscribe" });
113867
+ runner.refreshMirror(status);
113868
+ return runner;
113869
+ }
113870
+ constructor(transport, options) {
113871
+ this.transport = transport;
113872
+ this.options = options;
113873
+ transport.onLine((line) => this.handleLine(line));
113874
+ transport.onClose(() => this.handleClose());
113875
+ }
113876
+ // ─── Transport plumbing ──────────────────────────────────────────
113877
+ handleLine(line) {
113878
+ for (const complete of this.decoder.push(line)) {
113879
+ let message;
113880
+ try {
113881
+ message = JSON.parse(complete);
113882
+ } catch {
113883
+ this.options.logger?.warn("[pty-host] dropped unparseable message", {
113884
+ event: "pty_host.bad_message"
113885
+ });
113886
+ continue;
113887
+ }
113888
+ if (isHostEvent(message)) {
113889
+ this.handleEvent(message);
113890
+ continue;
113891
+ }
113892
+ const waiter = this.pending.get(message.id);
113893
+ if (!waiter) continue;
113894
+ this.pending.delete(message.id);
113895
+ if (waiter.timeout) clearTimeout(waiter.timeout);
113896
+ if (message.ok) waiter.resolve(message.result);
113897
+ else waiter.reject(new Error(message.error));
113898
+ }
113899
+ }
113900
+ /**
113901
+ * Fail every in-flight request when the socket drops.
113902
+ *
113903
+ * Without this each one stays pending forever and the caller — a session
113904
+ * start, an input write — hangs rather than erroring. PR 9 adds reconnection;
113905
+ * until then a dropped host is a hard failure that says so.
113906
+ */
113907
+ handleClose() {
113908
+ if (this.closed) return;
113909
+ this.closed = true;
113910
+ this.stopHeartbeat();
113911
+ const err = new Error("pty-host connection closed");
113912
+ for (const waiter of this.pending.values()) {
113913
+ if (waiter.timeout) clearTimeout(waiter.timeout);
113914
+ waiter.reject(err);
113915
+ }
113916
+ this.pending.clear();
113917
+ }
113918
+ request(body, timeoutMs) {
113919
+ if (this.closed) return Promise.reject(new Error("pty-host connection closed"));
113920
+ const id = this.nextRequestId++;
113921
+ return new Promise((resolve4, reject) => {
113922
+ const timeout = timeoutMs === void 0 ? null : setTimeout(() => {
113923
+ if (!this.pending.delete(id)) return;
113924
+ reject(new Error(`${body.type} timed out after ${timeoutMs}ms`));
113925
+ }, timeoutMs);
113926
+ timeout?.unref?.();
113927
+ this.pending.set(id, { resolve: resolve4, reject, timeout });
113928
+ this.transport.send(encodeMessage({ ...body, id }));
113929
+ });
113930
+ }
113931
+ /**
113932
+ * Fire-and-forget for the synchronous parts of `SessionRunner`.
113933
+ *
113934
+ * `sendKeys`, `cancel`, `killPid` and `putOnHold` all return void, so there is
113935
+ * no channel to report a failure through even if we waited for one. The
113936
+ * response is still consumed — an unhandled rejection would take the process
113937
+ * down over a keystroke that failed to land.
113938
+ */
113939
+ fireAndForget(body) {
113940
+ this.request(body).catch((err) => {
113941
+ this.options.logger?.warn("[pty-host] request failed", {
113942
+ event: "pty_host.request_failed",
113943
+ type: body.type,
113944
+ err
113945
+ });
113946
+ });
113947
+ }
113948
+ async readStatus() {
113949
+ return await this.request({ type: "status" });
113950
+ }
113951
+ refreshMirror(status) {
113952
+ this.sessions = /* @__PURE__ */ new Map();
113953
+ this.pids = /* @__PURE__ */ new Map();
113954
+ for (const entry of status.sessions) {
113955
+ const session = reviveSession(entry.session);
113956
+ this.sessions.set(session.id, session);
113957
+ this.pids.set(session.id, entry.pid);
113958
+ }
113959
+ }
113960
+ async heartbeat(state, timeoutMs = HOST_HEARTBEAT_REQUEST_TIMEOUT_MS) {
113961
+ await this.request({ type: "heartbeat", ...state }, timeoutMs);
113962
+ }
113963
+ startHeartbeat(getState, intervalMs = HOST_HEARTBEAT_INTERVAL_MS) {
113964
+ this.stopHeartbeat();
113965
+ const send = () => {
113966
+ if (this.closed || this.heartbeatInFlight) return;
113967
+ this.heartbeatInFlight = true;
113968
+ void Promise.resolve().then(() => this.heartbeat(getState())).catch((err) => {
113969
+ if (this.closed) return;
113970
+ this.options.logger?.warn("[pty-host] heartbeat failed", {
113971
+ event: "pty_host.heartbeat_failed",
113972
+ err
113973
+ });
113974
+ }).finally(() => {
113975
+ this.heartbeatInFlight = false;
113976
+ });
113977
+ };
113978
+ send();
113979
+ this.heartbeatTimer = setInterval(send, intervalMs);
113980
+ this.heartbeatTimer.unref?.();
113981
+ }
113982
+ stopHeartbeat() {
113983
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
113984
+ this.heartbeatTimer = null;
113985
+ }
113986
+ // ─── Events ──────────────────────────────────────────────────────
113987
+ handleEvent(event) {
113988
+ switch (event.event) {
113989
+ case "output": {
113990
+ this.output.set(event.sessionId, (this.output.get(event.sessionId) ?? "") + event.data);
113991
+ this.options.onOutput?.(event.sessionId, event.data);
113992
+ break;
113993
+ }
113994
+ case "status-change": {
113995
+ const session = reviveSession(event.session);
113996
+ if (session.status === "idle" && session.completedAt != null) {
113997
+ this.sessions.delete(session.id);
113998
+ this.pids.delete(session.id);
113999
+ this.output.delete(session.id);
114000
+ this.inputHistory.delete(session.id);
114001
+ } else {
114002
+ this.sessions.set(session.id, session);
114003
+ }
114004
+ this.options.onStatusChange?.(session);
114005
+ break;
114006
+ }
114007
+ case "ready": {
114008
+ const session = reviveSession(event.session);
114009
+ this.sessions.set(session.id, session);
114010
+ this.options.onReady?.(session);
114011
+ break;
114012
+ }
114013
+ case "permission-change":
114014
+ this.options.onPermissionChange?.(event.sessionId, event.gate);
114015
+ break;
114016
+ case "live-question":
114017
+ this.options.onLiveQuestion?.(event.sessionId, event.questions);
114018
+ break;
114019
+ case "live-question-gone":
114020
+ this.options.onLiveQuestionGone?.(event.sessionId);
114021
+ break;
114022
+ case "user-message": {
114023
+ const history = this.inputHistory.get(event.sessionId) ?? [];
114024
+ history.push({ text: event.text, ts: event.ts });
114025
+ this.inputHistory.set(event.sessionId, history);
114026
+ this.options.onUserMessage?.(event.sessionId, event.text, event.ts);
114027
+ break;
114028
+ }
114029
+ case "exit": {
114030
+ this.sessions.delete(event.sessionId);
114031
+ this.pids.delete(event.sessionId);
114032
+ this.output.delete(event.sessionId);
114033
+ this.inputHistory.delete(event.sessionId);
114034
+ break;
114035
+ }
114036
+ }
114037
+ }
114038
+ // ─── SessionRunner ───────────────────────────────────────────────
114039
+ async start(sessionId, options) {
114040
+ const provider = options.provider ?? "claude-code";
114041
+ return this.adopt(await this.request({ type: "spawn", provider, sessionId, options }));
114042
+ }
114043
+ async startFresh(options) {
114044
+ const provider = options.provider ?? "claude-code";
114045
+ return this.adopt(await this.request({ type: "spawn", provider, sessionId: null, options }));
114046
+ }
114047
+ /**
114048
+ * Take a spawn answer into the mirror.
114049
+ *
114050
+ * The pid lands here and nowhere else in the live path: `recordSessionSpawn`
114051
+ * reads it immediately after start to write the durable registry row, and a
114052
+ * null there costs the next boot its ability to probe whether the agent
114053
+ * outlived us.
114054
+ */
114055
+ adopt(raw2) {
114056
+ const entry = raw2;
114057
+ const session = reviveSession(entry.session);
114058
+ this.sessions.set(session.id, session);
114059
+ this.pids.set(session.id, entry.pid);
114060
+ return session;
114061
+ }
114062
+ /**
114063
+ * Returns the mirror's promptCount, optimistically incremented.
114064
+ *
114065
+ * The interface is synchronous, so there is no way to return the host's
114066
+ * authoritative count. The increment matches what an in-process runner does
114067
+ * for the same call, and the next `status-change` event overwrites it — so a
114068
+ * mirror that guessed wrong is corrected within one round trip rather than
114069
+ * drifting.
114070
+ */
114071
+ sendInput(sessionId, input) {
114072
+ const session = this.requireSession(sessionId);
114073
+ this.fireAndForget({ type: "write", sessionId, input });
114074
+ session.promptCount += 1;
114075
+ return session.promptCount;
114076
+ }
114077
+ sendKeys(sessionId, keys) {
114078
+ this.requireSession(sessionId);
114079
+ this.fireAndForget({ type: "keys", sessionId, keys });
114080
+ }
114081
+ cancel(sessionId) {
114082
+ this.fireAndForget({ type: "cancel", sessionId });
114083
+ }
114084
+ killPid(pid) {
114085
+ this.fireAndForget({ type: "kill", pid });
114086
+ }
114087
+ putOnHold(sessionId) {
114088
+ this.fireAndForget({ type: "kill", sessionId, hold: true });
114089
+ this.sessions.delete(sessionId);
114090
+ this.pids.delete(sessionId);
114091
+ }
114092
+ getOutput(sessionId) {
114093
+ this.requireSession(sessionId);
114094
+ return this.output.get(sessionId) ?? "";
114095
+ }
114096
+ async getOutputLines(sessionId, maxLines) {
114097
+ const result = await this.request({ type: "replay", sessionId, maxLines });
114098
+ if (typeof result.output === "string") this.output.set(sessionId, result.output);
114099
+ return result.lines;
114100
+ }
114101
+ /**
114102
+ * Synchronous, so it answers from the mirror rather than the host.
114103
+ *
114104
+ * Seeded lazily: `user-message` events append as they arrive, and a session
114105
+ * this streamer did not start has none until `hydrateInputHistory` fetches
114106
+ * them. Empty is the same answer an in-process runner gives for an unknown
114107
+ * session, so a caller cannot tell "none yet" from "not fetched" — which is
114108
+ * why the fetch is explicit rather than hidden behind this getter.
114109
+ */
114110
+ getInputHistory(sessionId) {
114111
+ return this.inputHistory.get(sessionId) ?? [];
114112
+ }
114113
+ /** Pull a session's recorded messages from the host into the mirror. */
114114
+ async hydrateInputHistory(sessionId) {
114115
+ const result = await this.request({
114116
+ type: "input-history",
114117
+ sessionId
114118
+ });
114119
+ this.inputHistory.set(sessionId, result.history);
114120
+ return result.history;
114121
+ }
114122
+ getPid(sessionId) {
114123
+ return this.pids.get(sessionId) ?? null;
114124
+ }
114125
+ getSession(sessionId) {
114126
+ return this.sessions.get(sessionId) ?? null;
114127
+ }
114128
+ hasSession(sessionId) {
114129
+ return this.sessions.has(sessionId);
114130
+ }
114131
+ listSessions() {
114132
+ return [...this.sessions.values()];
114133
+ }
114134
+ /**
114135
+ * Drops this streamer's connection and nothing else.
114136
+ *
114137
+ * Emphatically NOT the in-process `dispose()`, which signals every child. The
114138
+ * entire point of the host is that its PTYs outlive the streamer, so tearing
114139
+ * them down here would spend the feature to implement a method name.
114140
+ */
114141
+ dispose() {
114142
+ this.handleClose();
114143
+ this.transport.close();
114144
+ }
114145
+ requireSession(sessionId) {
114146
+ const session = this.sessions.get(sessionId);
114147
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
114148
+ return session;
114149
+ }
114150
+ };
114151
+ }
114152
+ });
114153
+
113733
114154
  // src/services/questions/detectPermissionGate.ts
113734
114155
  function permissionContentKey(gate) {
113735
114156
  return `${gate.prompt ?? ""}::${gate.detail ?? ""}::${gate.options.map((o) => `${o.index}.${o.label}`).join(",")}::${gate.cursor ?? ""}`;
@@ -114159,13 +114580,19 @@ var init_pty_manager = __esm({
114159
114580
  sessionId
114160
114581
  ];
114161
114582
  args.push(...buildFlagArgs(options.claudeFlags, options.claudeExtraArgs));
114162
- const proc = nodePty.spawn(resolveClaudeExe(), args, {
114163
- name: "xterm-256color",
114164
- cols: 120,
114165
- rows: 40,
114166
- cwd: options.projectPath,
114167
- env: buildSpawnEnv()
114168
- });
114583
+ let proc;
114584
+ try {
114585
+ proc = nodePty.spawn(resolveClaudeExe(), args, {
114586
+ name: "xterm-256color",
114587
+ cols: 120,
114588
+ rows: 40,
114589
+ cwd: options.projectPath,
114590
+ env: buildSpawnEnv()
114591
+ });
114592
+ } catch (err) {
114593
+ clearClaudeExeCache();
114594
+ throw err;
114595
+ }
114169
114596
  const session = {
114170
114597
  id: sessionId,
114171
114598
  provider: CLAUDE_CODE_PROVIDER2,
@@ -114220,13 +114647,19 @@ var init_pty_manager = __esm({
114220
114647
  args.push("--system-prompt", options.systemPrompt);
114221
114648
  }
114222
114649
  args.push(...buildFlagArgs(options.claudeFlags, options.claudeExtraArgs));
114223
- const proc = nodePty.spawn(resolveClaudeExe(), args, {
114224
- name: "xterm-256color",
114225
- cols: 120,
114226
- rows: 40,
114227
- cwd: options.projectPath,
114228
- env: buildSpawnEnv()
114229
- });
114650
+ let proc;
114651
+ try {
114652
+ proc = nodePty.spawn(resolveClaudeExe(), args, {
114653
+ name: "xterm-256color",
114654
+ cols: 120,
114655
+ rows: 40,
114656
+ cwd: options.projectPath,
114657
+ env: buildSpawnEnv()
114658
+ });
114659
+ } catch (err) {
114660
+ clearClaudeExeCache();
114661
+ throw err;
114662
+ }
114230
114663
  const session = {
114231
114664
  id: sessionId,
114232
114665
  provider: CLAUDE_CODE_PROVIDER2,
@@ -114775,15 +115208,34 @@ var init_live_session_manager = __esm({
114775
115208
  import_path23 = require("path");
114776
115209
  init_codex_pty_runner();
114777
115210
  init_providers();
115211
+ init_remote_session_runner();
114778
115212
  init_pty_manager();
114779
115213
  LiveSessionManager = class {
114780
115214
  runners;
115215
+ remoteRunner = null;
115216
+ options;
114781
115217
  constructor(options = {}) {
115218
+ this.options = options;
114782
115219
  this.runners = /* @__PURE__ */ new Map([
114783
115220
  [CLAUDE_CODE_PROVIDER2, new PTYManager(options)],
114784
115221
  [CODEX_CLI_PROVIDER2, new CodexPtyRunner(options)]
114785
115222
  ]);
114786
115223
  }
115224
+ async useRemoteRunner(transport) {
115225
+ const remote = await RemoteSessionRunner.connect(transport, this.options);
115226
+ await Promise.all(
115227
+ remote.listSessions().map((session) => remote.hydrateInputHistory(session.id))
115228
+ );
115229
+ for (const runner of this.runners.values()) runner.dispose();
115230
+ this.remoteRunner = remote;
115231
+ return remote.listSessions();
115232
+ }
115233
+ isRemote() {
115234
+ return this.remoteRunner !== null;
115235
+ }
115236
+ startRemoteHeartbeat(getState) {
115237
+ this.remoteRunner?.startHeartbeat(getState);
115238
+ }
114787
115239
  async start(sessionId, options) {
114788
115240
  const provider = options.provider ?? CLAUDE_CODE_PROVIDER2;
114789
115241
  const runner = this.assertSupportedProvider(provider, options.projectPath);
@@ -114804,7 +115256,7 @@ var init_live_session_manager = __esm({
114804
115256
  this.runnerFor(sessionId).cancel(sessionId);
114805
115257
  }
114806
115258
  killPid(pid) {
114807
- for (const runner of this.runners.values()) {
115259
+ for (const runner of this.activeRunners()) {
114808
115260
  runner.killPid(pid);
114809
115261
  }
114810
115262
  }
@@ -114814,13 +115266,13 @@ var init_live_session_manager = __esm({
114814
115266
  // every runner rather than throwing; this matches the pre-extraction
114815
115267
  // behavior of delegating straight through with no existence check.
114816
115268
  putOnHold(sessionId) {
114817
- for (const runner of this.runners.values()) {
115269
+ for (const runner of this.activeRunners()) {
114818
115270
  if (runner.hasSession(sessionId) || runner.getSession(sessionId)) {
114819
115271
  runner.putOnHold(sessionId);
114820
115272
  return;
114821
115273
  }
114822
115274
  }
114823
- for (const runner of this.runners.values()) {
115275
+ for (const runner of this.activeRunners()) {
114824
115276
  runner.putOnHold(sessionId);
114825
115277
  }
114826
115278
  }
@@ -114834,7 +115286,7 @@ var init_live_session_manager = __esm({
114834
115286
  return this.runnerFor(sessionId).getInputHistory(sessionId);
114835
115287
  }
114836
115288
  getSession(sessionId) {
114837
- for (const runner of this.runners.values()) {
115289
+ for (const runner of this.activeRunners()) {
114838
115290
  const session = runner.getSession(sessionId);
114839
115291
  if (session) return session;
114840
115292
  }
@@ -114844,23 +115296,23 @@ var init_live_session_manager = __esm({
114844
115296
  // best-effort basis, so an unknown session must return null rather than
114845
115297
  // throw the way the input-routing methods do.
114846
115298
  getPid(sessionId) {
114847
- for (const runner of this.runners.values()) {
115299
+ for (const runner of this.activeRunners()) {
114848
115300
  const pid = runner.getPid(sessionId);
114849
115301
  if (pid != null) return pid;
114850
115302
  }
114851
115303
  return null;
114852
115304
  }
114853
115305
  hasSession(sessionId) {
114854
- for (const runner of this.runners.values()) {
115306
+ for (const runner of this.activeRunners()) {
114855
115307
  if (runner.hasSession(sessionId)) return true;
114856
115308
  }
114857
115309
  return false;
114858
115310
  }
114859
115311
  listSessions() {
114860
- return Array.from(this.runners.values()).flatMap((runner) => runner.listSessions());
115312
+ return this.activeRunners().flatMap((runner) => runner.listSessions());
114861
115313
  }
114862
115314
  dispose() {
114863
- for (const runner of this.runners.values()) {
115315
+ for (const runner of this.activeRunners()) {
114864
115316
  runner.dispose();
114865
115317
  }
114866
115318
  }
@@ -114868,12 +115320,13 @@ var init_live_session_manager = __esm({
114868
115320
  // this is a linear scan across hasSession()/getSession() rather than a
114869
115321
  // separate session→provider index — see task-1-brief.md.
114870
115322
  runnerFor(sessionId) {
114871
- for (const runner of this.runners.values()) {
115323
+ for (const runner of this.activeRunners()) {
114872
115324
  if (runner.hasSession(sessionId) || runner.getSession(sessionId)) return runner;
114873
115325
  }
114874
115326
  throw new Error(`Session not found: ${sessionId}`);
114875
115327
  }
114876
115328
  assertSupportedProvider(provider, projectPath) {
115329
+ if (this.remoteRunner) return this.remoteRunner;
114877
115330
  const runner = this.runners.get(provider);
114878
115331
  if (runner) return runner;
114879
115332
  const err = new Error(
@@ -114882,7 +115335,184 @@ var init_live_session_manager = __esm({
114882
115335
  err.statusCode = 501;
114883
115336
  throw err;
114884
115337
  }
115338
+ activeRunners() {
115339
+ return this.remoteRunner ? [this.remoteRunner] : [...this.runners.values()];
115340
+ }
115341
+ };
115342
+ }
115343
+ });
115344
+
115345
+ // src/pty-host/socket.ts
115346
+ var socket_exports = {};
115347
+ __export(socket_exports, {
115348
+ broadcast: () => broadcast,
115349
+ connectToHost: () => connectToHost,
115350
+ hostSocketPath: () => hostSocketPath,
115351
+ listenForStreamers: () => listenForStreamers,
115352
+ probePtyHostStatus: () => probePtyHostStatus,
115353
+ socketTransport: () => socketTransport
115354
+ });
115355
+ function hostSocketPath(instanceId) {
115356
+ if (process.platform === "win32") {
115357
+ return `\\\\.\\pipe\\threadbase-pty-host-${instanceId}`;
115358
+ }
115359
+ const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".threadbase");
115360
+ return (0, import_node_path10.join)(dir, "run", `pty-host-${instanceId}.sock`);
115361
+ }
115362
+ function socketTransport(socket) {
115363
+ socket.setEncoding("utf8");
115364
+ return {
115365
+ send(line) {
115366
+ socket.write(line);
115367
+ },
115368
+ onLine(handler) {
115369
+ socket.on("data", (chunk) => handler(chunk));
115370
+ },
115371
+ onClose(handler) {
115372
+ let handled = false;
115373
+ const handleClose = () => {
115374
+ if (handled) return;
115375
+ handled = true;
115376
+ handler();
115377
+ };
115378
+ socket.once("close", handleClose);
115379
+ socket.once("error", handleClose);
115380
+ },
115381
+ close() {
115382
+ socket.destroy();
115383
+ }
115384
+ };
115385
+ }
115386
+ function connectToHost(socketPath) {
115387
+ return new Promise((resolve4, reject) => {
115388
+ const socket = (0, import_node_net.createConnection)(socketPath);
115389
+ socket.once("error", reject);
115390
+ socket.once("connect", () => {
115391
+ socket.removeListener("error", reject);
115392
+ resolve4(socketTransport(socket));
115393
+ });
115394
+ });
115395
+ }
115396
+ async function probePtyHostStatus(socketPath, timeoutMs = 1e3) {
115397
+ let transport;
115398
+ let connectTimedOut = false;
115399
+ let connectTimer;
115400
+ const connecting = connectToHost(socketPath);
115401
+ try {
115402
+ transport = await Promise.race([
115403
+ connecting,
115404
+ new Promise((_2, reject) => {
115405
+ connectTimer = setTimeout(() => {
115406
+ connectTimedOut = true;
115407
+ reject(new Error(`status timed out after ${timeoutMs}ms`));
115408
+ }, timeoutMs);
115409
+ connectTimer.unref?.();
115410
+ })
115411
+ ]);
115412
+ } catch (err) {
115413
+ if (connectTimer) clearTimeout(connectTimer);
115414
+ if (connectTimedOut)
115415
+ void connecting.then(
115416
+ (late) => late.close(),
115417
+ () => {
115418
+ }
115419
+ );
115420
+ return { reachable: false, error: err instanceof Error ? err.message : String(err) };
115421
+ }
115422
+ if (connectTimer) clearTimeout(connectTimer);
115423
+ return await new Promise((resolve4) => {
115424
+ const decoder = new LineDecoder();
115425
+ let settled = false;
115426
+ const finish = (result) => {
115427
+ if (settled) return;
115428
+ settled = true;
115429
+ clearTimeout(timer);
115430
+ transport.close();
115431
+ resolve4(result);
114885
115432
  };
115433
+ const timer = setTimeout(
115434
+ () => finish({ reachable: false, error: `status timed out after ${timeoutMs}ms` }),
115435
+ timeoutMs
115436
+ );
115437
+ timer.unref?.();
115438
+ transport.onClose(() => finish({ reachable: false, error: "pty-host connection closed" }));
115439
+ transport.onLine((chunk) => {
115440
+ for (const line of decoder.push(chunk)) {
115441
+ let message;
115442
+ try {
115443
+ message = JSON.parse(line);
115444
+ } catch {
115445
+ finish({ reachable: false, error: "pty-host returned an invalid status response" });
115446
+ return;
115447
+ }
115448
+ if (isHostEvent(message) || message.id !== 1) continue;
115449
+ if (!message.ok) {
115450
+ finish({ reachable: false, error: message.error });
115451
+ return;
115452
+ }
115453
+ const status = message.result;
115454
+ if (typeof status?.protocolVersion !== "number" || !Array.isArray(status.sessions)) {
115455
+ finish({ reachable: false, error: "pty-host returned an invalid status response" });
115456
+ return;
115457
+ }
115458
+ finish({
115459
+ reachable: true,
115460
+ protocolVersion: status.protocolVersion,
115461
+ sessionCount: status.sessions.length
115462
+ });
115463
+ }
115464
+ });
115465
+ try {
115466
+ transport.send(encodeMessage({ id: 1, type: "status" }));
115467
+ } catch (err) {
115468
+ finish({ reachable: false, error: err instanceof Error ? err.message : String(err) });
115469
+ }
115470
+ });
115471
+ }
115472
+ async function listenForStreamers(socketPath, handlers) {
115473
+ if (process.platform !== "win32") {
115474
+ const { mkdirSync: mkdirSync16, rmSync: rmSync6 } = await import("fs");
115475
+ const { dirname: dirname18 } = await import("path");
115476
+ mkdirSync16(dirname18(socketPath), { recursive: true });
115477
+ try {
115478
+ const probe = await connectToHost(socketPath);
115479
+ probe.close();
115480
+ throw new Error(`a pty-host is already listening on ${socketPath}`);
115481
+ } catch (err) {
115482
+ if (err.message?.startsWith("a pty-host is already listening")) throw err;
115483
+ rmSync6(socketPath, { force: true });
115484
+ }
115485
+ }
115486
+ const server = (0, import_node_net.createServer)((socket) => {
115487
+ const transport = socketTransport(socket);
115488
+ const dispose = handlers.onConnection(transport);
115489
+ transport.onClose(dispose);
115490
+ });
115491
+ return new Promise((resolve4, reject) => {
115492
+ server.once("error", reject);
115493
+ server.listen(socketPath, () => {
115494
+ server.removeListener("error", reject);
115495
+ resolve4(server);
115496
+ });
115497
+ });
115498
+ }
115499
+ function broadcast(transports, message) {
115500
+ const line = encodeMessage(message);
115501
+ for (const t of transports) {
115502
+ try {
115503
+ t.send(line);
115504
+ } catch {
115505
+ }
115506
+ }
115507
+ }
115508
+ var import_node_net, import_node_os5, import_node_path10;
115509
+ var init_socket = __esm({
115510
+ "src/pty-host/socket.ts"() {
115511
+ "use strict";
115512
+ import_node_net = require("net");
115513
+ import_node_os5 = require("os");
115514
+ import_node_path10 = require("path");
115515
+ init_protocol();
114886
115516
  }
114887
115517
  });
114888
115518
 
@@ -125828,7 +126458,7 @@ function uidScope() {
125828
126458
  function resolveOwnLabel() {
125829
126459
  if (process.env.LAUNCHD_LABEL) return process.env.LAUNCHD_LABEL;
125830
126460
  try {
125831
- (0, import_node_child_process4.execFileSync)("launchctl", ["print", `${uidScope()}/homebrew.mxcl.tb-streamer`], {
126461
+ (0, import_node_child_process5.execFileSync)("launchctl", ["print", `${uidScope()}/homebrew.mxcl.tb-streamer`], {
125832
126462
  stdio: "ignore"
125833
126463
  });
125834
126464
  return "homebrew.mxcl.tb-streamer";
@@ -125838,7 +126468,7 @@ function resolveOwnLabel() {
125838
126468
  }
125839
126469
  function isLabelLoaded(label) {
125840
126470
  try {
125841
- (0, import_node_child_process4.execFileSync)("launchctl", ["list", label], { stdio: "ignore" });
126471
+ (0, import_node_child_process5.execFileSync)("launchctl", ["list", label], { stdio: "ignore" });
125842
126472
  return true;
125843
126473
  } catch {
125844
126474
  return false;
@@ -125858,7 +126488,7 @@ function detectConflictingAgents() {
125858
126488
  }
125859
126489
  }
125860
126490
  try {
125861
- const out = (0, import_node_child_process4.execFileSync)("launchctl", ["list"], {
126491
+ const out = (0, import_node_child_process5.execFileSync)("launchctl", ["list"], {
125862
126492
  stdio: ["ignore", "pipe", "ignore"]
125863
126493
  }).toString();
125864
126494
  for (const line of out.split("\n")) {
@@ -125891,11 +126521,11 @@ function formatConflictMessage(conflicts) {
125891
126521
  );
125892
126522
  return lines.join("\n");
125893
126523
  }
125894
- var import_node_child_process4, KNOWN_STREAMER_LABELS, LEGACY_PATTERN;
126524
+ var import_node_child_process5, KNOWN_STREAMER_LABELS, LEGACY_PATTERN;
125895
126525
  var init_conflict_check = __esm({
125896
126526
  "src/lifecycle/conflict-check.ts"() {
125897
126527
  "use strict";
125898
- import_node_child_process4 = require("child_process");
126528
+ import_node_child_process5 = require("child_process");
125899
126529
  init_constants();
125900
126530
  KNOWN_STREAMER_LABELS = [
125901
126531
  "com.ronen.threadbase",
@@ -125923,7 +126553,7 @@ function uidScope2() {
125923
126553
  function resolveLoadedLabel() {
125924
126554
  if (process.env.LAUNCHD_LABEL) return process.env.LAUNCHD_LABEL;
125925
126555
  try {
125926
- (0, import_node_child_process5.execFileSync)("launchctl", ["print", `${uidScope2()}/${BREW_LAUNCHD_LABEL2}`], {
126556
+ (0, import_node_child_process6.execFileSync)("launchctl", ["print", `${uidScope2()}/${BREW_LAUNCHD_LABEL2}`], {
125927
126557
  stdio: "ignore"
125928
126558
  });
125929
126559
  return BREW_LAUNCHD_LABEL2;
@@ -125936,7 +126566,7 @@ function fullTarget() {
125936
126566
  }
125937
126567
  function isAgentLoaded() {
125938
126568
  try {
125939
- (0, import_node_child_process5.execFileSync)("launchctl", ["list", resolveLoadedLabel()], { stdio: "ignore" });
126569
+ (0, import_node_child_process6.execFileSync)("launchctl", ["list", resolveLoadedLabel()], { stdio: "ignore" });
125940
126570
  return true;
125941
126571
  } catch {
125942
126572
  return false;
@@ -125944,7 +126574,7 @@ function isAgentLoaded() {
125944
126574
  }
125945
126575
  function bootoutAgent(timeoutMs = 2e3) {
125946
126576
  try {
125947
- (0, import_node_child_process5.execFileSync)("launchctl", ["bootout", fullTarget()], { stdio: "ignore" });
126577
+ (0, import_node_child_process6.execFileSync)("launchctl", ["bootout", fullTarget()], { stdio: "ignore" });
125948
126578
  } catch {
125949
126579
  }
125950
126580
  const deadline = Date.now() + timeoutMs;
@@ -125955,7 +126585,7 @@ function bootoutAgent(timeoutMs = 2e3) {
125955
126585
  }
125956
126586
  function bootstrapAgent(plistPath, opts) {
125957
126587
  try {
125958
- (0, import_node_child_process5.execFileSync)("launchctl", ["bootstrap", uidScope2(), plistPath], {
126588
+ (0, import_node_child_process6.execFileSync)("launchctl", ["bootstrap", uidScope2(), plistPath], {
125959
126589
  stdio: ["ignore", "ignore", "pipe"]
125960
126590
  });
125961
126591
  } catch (err) {
@@ -125968,18 +126598,18 @@ function bootstrapAgent(plistPath, opts) {
125968
126598
  }
125969
126599
  }
125970
126600
  function kickstartAgent() {
125971
- (0, import_node_child_process5.execFileSync)("launchctl", ["kickstart", "-k", fullTarget()], { stdio: "ignore" });
126601
+ (0, import_node_child_process6.execFileSync)("launchctl", ["kickstart", "-k", fullTarget()], { stdio: "ignore" });
125972
126602
  }
125973
126603
  function darwinPlistPath() {
125974
126604
  return `${process.env.HOME}/Library/LaunchAgents/${resolveLoadedLabel()}.plist`;
125975
126605
  }
125976
126606
  function getLogPaths() {
125977
- const dir = (0, import_node_path24.join)(installDir(), "logs");
125978
- return { stdout: (0, import_node_path24.join)(dir, "stdout.log"), stderr: (0, import_node_path24.join)(dir, "stderr.log") };
126607
+ const dir = (0, import_node_path25.join)(installDir(), "logs");
126608
+ return { stdout: (0, import_node_path25.join)(dir, "stdout.log"), stderr: (0, import_node_path25.join)(dir, "stderr.log") };
125979
126609
  }
125980
126610
  function getAgentPid() {
125981
126611
  try {
125982
- const out = (0, import_node_child_process5.execFileSync)("launchctl", ["list"], {
126612
+ const out = (0, import_node_child_process6.execFileSync)("launchctl", ["list"], {
125983
126613
  stdio: ["ignore", "pipe", "ignore"]
125984
126614
  }).toString();
125985
126615
  const label = resolveLoadedLabel();
@@ -125995,12 +126625,12 @@ function getAgentPid() {
125995
126625
  return null;
125996
126626
  }
125997
126627
  }
125998
- var import_node_child_process5, import_node_path24, BREW_LAUNCHD_LABEL2;
126628
+ var import_node_child_process6, import_node_path25, BREW_LAUNCHD_LABEL2;
125999
126629
  var init_launchd = __esm({
126000
126630
  "src/lifecycle/launchd.ts"() {
126001
126631
  "use strict";
126002
- import_node_child_process5 = require("child_process");
126003
- import_node_path24 = require("path");
126632
+ import_node_child_process6 = require("child_process");
126633
+ import_node_path25 = require("path");
126004
126634
  init_constants();
126005
126635
  BREW_LAUNCHD_LABEL2 = "homebrew.mxcl.tb-streamer";
126006
126636
  }
@@ -126031,7 +126661,7 @@ function readMarker() {
126031
126661
  const parsed = JSON.parse(raw2);
126032
126662
  return MarkerSchema.parse(parsed);
126033
126663
  } catch (err) {
126034
- log7.warn(`marker at ${markerPath()} is malformed; treating as absent`, {
126664
+ log8.warn(`marker at ${markerPath()} is malformed; treating as absent`, {
126035
126665
  error: err instanceof Error ? err.message : String(err)
126036
126666
  });
126037
126667
  return null;
@@ -126039,7 +126669,7 @@ function readMarker() {
126039
126669
  }
126040
126670
  function writeMarker(marker) {
126041
126671
  MarkerSchema.parse(marker);
126042
- (0, import_node_fs22.mkdirSync)((0, import_node_path25.dirname)(markerPath()), { recursive: true });
126672
+ (0, import_node_fs22.mkdirSync)((0, import_node_path26.dirname)(markerPath()), { recursive: true });
126043
126673
  const tmp = `${markerPath()}.tmp`;
126044
126674
  (0, import_node_fs22.writeFileSync)(tmp, `${JSON.stringify(marker, null, 2)}
126045
126675
  `, { mode: 384 });
@@ -126048,16 +126678,16 @@ function writeMarker(marker) {
126048
126678
  function clearMarker() {
126049
126679
  if ((0, import_node_fs22.existsSync)(markerPath())) (0, import_node_fs22.rmSync)(markerPath());
126050
126680
  }
126051
- var import_node_fs22, import_node_path25, log7;
126681
+ var import_node_fs22, import_node_path26, log8;
126052
126682
  var init_marker = __esm({
126053
126683
  "src/lifecycle/marker.ts"() {
126054
126684
  "use strict";
126055
126685
  import_node_fs22 = require("fs");
126056
- import_node_path25 = require("path");
126686
+ import_node_path26 = require("path");
126057
126687
  init_logger();
126058
126688
  init_constants();
126059
126689
  init_marker_schema();
126060
- log7 = getLogger("lifecycle.marker");
126690
+ log8 = getLogger("lifecycle.marker");
126061
126691
  }
126062
126692
  });
126063
126693
 
@@ -126072,7 +126702,7 @@ __export(task_scheduler_exports, {
126072
126702
  kickstartAgent: () => kickstartAgent2
126073
126703
  });
126074
126704
  function ps2(command) {
126075
- return (0, import_node_child_process6.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], {
126705
+ return (0, import_node_child_process7.execFileSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], {
126076
126706
  stdio: ["ignore", "pipe", "ignore"]
126077
126707
  }).toString();
126078
126708
  }
@@ -126118,11 +126748,11 @@ function getAgentPid2() {
126118
126748
  return null;
126119
126749
  }
126120
126750
  }
126121
- var import_node_child_process6;
126751
+ var import_node_child_process7;
126122
126752
  var init_task_scheduler = __esm({
126123
126753
  "src/lifecycle/task-scheduler.ts"() {
126124
126754
  "use strict";
126125
- import_node_child_process6 = require("child_process");
126755
+ import_node_child_process7 = require("child_process");
126126
126756
  init_constants();
126127
126757
  }
126128
126758
  });
@@ -126189,6 +126819,34 @@ var init_check_sqlite_abi = __esm({
126189
126819
  }
126190
126820
  });
126191
126821
 
126822
+ // src/lifecycle/log-cap.ts
126823
+ var log_cap_exports = {};
126824
+ __export(log_cap_exports, {
126825
+ LOG_CAP_BYTES: () => LOG_CAP_BYTES,
126826
+ truncateOversizedLogs: () => truncateOversizedLogs
126827
+ });
126828
+ function truncateOversizedLogs(maxBytes = LOG_CAP_BYTES, paths = Object.values(getLogPaths())) {
126829
+ const truncated = [];
126830
+ for (const file2 of paths) {
126831
+ try {
126832
+ if ((0, import_node_fs24.statSync)(file2).size <= maxBytes) continue;
126833
+ (0, import_node_fs24.truncateSync)(file2, 0);
126834
+ truncated.push(file2);
126835
+ } catch {
126836
+ }
126837
+ }
126838
+ return truncated;
126839
+ }
126840
+ var import_node_fs24, LOG_CAP_BYTES;
126841
+ var init_log_cap = __esm({
126842
+ "src/lifecycle/log-cap.ts"() {
126843
+ "use strict";
126844
+ import_node_fs24 = require("fs");
126845
+ init_launchd();
126846
+ LOG_CAP_BYTES = 32 * 1024 * 1024;
126847
+ }
126848
+ });
126849
+
126192
126850
  // src/lifecycle/prompt.ts
126193
126851
  var prompt_exports = {};
126194
126852
  __export(prompt_exports, {
@@ -126267,11 +126925,11 @@ The supervised prod streamer is already holding port ${prodPort}.
126267
126925
  // src/lifecycle/prefs.ts
126268
126926
  function readPrefs() {
126269
126927
  const path2 = prefsPath();
126270
- if (!(0, import_node_fs24.existsSync)(path2)) return { repos: {} };
126928
+ if (!(0, import_node_fs25.existsSync)(path2)) return { repos: {} };
126271
126929
  try {
126272
- return PrefsSchema.parse(JSON.parse((0, import_node_fs24.readFileSync)(path2, "utf8")));
126930
+ return PrefsSchema.parse(JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8")));
126273
126931
  } catch (err) {
126274
- log9.warn(`prefs at ${path2} are malformed; treating as empty`, {
126932
+ log10.warn(`prefs at ${path2} are malformed; treating as empty`, {
126275
126933
  error: err instanceof Error ? err.message : String(err)
126276
126934
  });
126277
126935
  return { repos: {} };
@@ -126279,11 +126937,11 @@ function readPrefs() {
126279
126937
  }
126280
126938
  function savePrefs(p2) {
126281
126939
  const path2 = prefsPath();
126282
- (0, import_node_fs24.mkdirSync)((0, import_node_path26.dirname)(path2), { recursive: true });
126940
+ (0, import_node_fs25.mkdirSync)((0, import_node_path27.dirname)(path2), { recursive: true });
126283
126941
  const tmp = `${path2}.tmp`;
126284
- (0, import_node_fs24.writeFileSync)(tmp, `${JSON.stringify(p2, null, 2)}
126942
+ (0, import_node_fs25.writeFileSync)(tmp, `${JSON.stringify(p2, null, 2)}
126285
126943
  `, { mode: 384 });
126286
- (0, import_node_fs24.renameSync)(tmp, path2);
126944
+ (0, import_node_fs25.renameSync)(tmp, path2);
126287
126945
  }
126288
126946
  function writePrefForRepo(repoToplevel, choice) {
126289
126947
  const p2 = readPrefs();
@@ -126304,18 +126962,18 @@ function forgetRepo(repoToplevel) {
126304
126962
  }
126305
126963
  function forgetAll() {
126306
126964
  const path2 = prefsPath();
126307
- if ((0, import_node_fs24.existsSync)(path2)) (0, import_node_fs24.rmSync)(path2);
126965
+ if ((0, import_node_fs25.existsSync)(path2)) (0, import_node_fs25.rmSync)(path2);
126308
126966
  }
126309
- var import_node_fs24, import_node_path26, log9, PrefsSchema;
126967
+ var import_node_fs25, import_node_path27, log10, PrefsSchema;
126310
126968
  var init_prefs = __esm({
126311
126969
  "src/lifecycle/prefs.ts"() {
126312
126970
  "use strict";
126313
- import_node_fs24 = require("fs");
126314
- import_node_path26 = require("path");
126971
+ import_node_fs25 = require("fs");
126972
+ import_node_path27 = require("path");
126315
126973
  init_zod();
126316
126974
  init_logger();
126317
126975
  init_constants();
126318
- log9 = getLogger("lifecycle.prefs");
126976
+ log10 = getLogger("lifecycle.prefs");
126319
126977
  PrefsSchema = external_exports.object({
126320
126978
  repos: external_exports.record(
126321
126979
  external_exports.string(),
@@ -126381,7 +127039,7 @@ function detectProdActive() {
126381
127039
  }
126382
127040
  function isPortInUse(port) {
126383
127041
  return new Promise((resolve4) => {
126384
- const srv = (0, import_node_net.createServer)();
127042
+ const srv = (0, import_node_net2.createServer)();
126385
127043
  srv.once("error", () => resolve4(true));
126386
127044
  srv.once("listening", () => srv.close(() => resolve4(false)));
126387
127045
  srv.listen(port, "127.0.0.1");
@@ -126394,7 +127052,7 @@ async function tryBind(start, offset) {
126394
127052
  if (offset >= 50) return start;
126395
127053
  const port = start + offset;
126396
127054
  const free = await new Promise((resolve4) => {
126397
- const srv = (0, import_node_net.createServer)();
127055
+ const srv = (0, import_node_net2.createServer)();
126398
127056
  srv.once("error", () => resolve4(false));
126399
127057
  srv.once("listening", () => srv.close(() => resolve4(true)));
126400
127058
  srv.listen(port, "127.0.0.1");
@@ -126418,7 +127076,7 @@ function takeoverProd(opts) {
126418
127076
  `prod is already suspended by dev pid ${existing.devPid} (since ${existing.suspendedAt}). Stop that dev session first, or run 'tb-streamer prod doctor'.`
126419
127077
  );
126420
127078
  }
126421
- log10.info(`stale marker found (pid ${existing.devPid} is gone) \u2014 clearing and proceeding`);
127079
+ log11.info(`stale marker found (pid ${existing.devPid} is gone) \u2014 clearing and proceeding`);
126422
127080
  }
126423
127081
  getSupervisor().bootoutAgent();
126424
127082
  writeMarker({
@@ -126433,7 +127091,7 @@ function takeoverProd(opts) {
126433
127091
  const m2 = readMarker();
126434
127092
  if (m2 && m2.devPid === process.pid) {
126435
127093
  writeMarker({ ...m2, userHeld: true });
126436
- log10.info(
127094
+ log11.info(
126437
127095
  `prod is suspended (userHeld). Run 'tb-streamer prod start' to restore the supervised instance.`
126438
127096
  );
126439
127097
  }
@@ -126452,23 +127110,23 @@ function takeoverProd(opts) {
126452
127110
  });
126453
127111
  process.on("uncaughtException", (err) => {
126454
127112
  flipUserHeld();
126455
- log10.error(`uncaught: ${err.message}`);
127113
+ log11.error(`uncaught: ${err.message}`);
126456
127114
  process.exit(1);
126457
127115
  });
126458
127116
  process.on("exit", () => {
126459
127117
  flipUserHeld();
126460
127118
  });
126461
127119
  }
126462
- var import_node_net, log10;
127120
+ var import_node_net2, log11;
126463
127121
  var init_dev_takeover = __esm({
126464
127122
  "src/lifecycle/dev-takeover.ts"() {
126465
127123
  "use strict";
126466
- import_node_net = require("net");
127124
+ import_node_net2 = require("net");
126467
127125
  init_logger();
126468
127126
  init_marker();
126469
127127
  init_platform2();
126470
127128
  init_prefs();
126471
- log10 = getLogger("lifecycle.dev-takeover");
127129
+ log11 = getLogger("lifecycle.dev-takeover");
126472
127130
  }
126473
127131
  });
126474
127132
 
@@ -126479,7 +127137,7 @@ __export(repo_exports, {
126479
127137
  });
126480
127138
  function getGitToplevel(cwd) {
126481
127139
  try {
126482
- return (0, import_node_child_process8.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
127140
+ return (0, import_node_child_process9.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
126483
127141
  cwd,
126484
127142
  stdio: ["ignore", "pipe", "ignore"]
126485
127143
  }).toString().trim();
@@ -126487,11 +127145,11 @@ function getGitToplevel(cwd) {
126487
127145
  return null;
126488
127146
  }
126489
127147
  }
126490
- var import_node_child_process8;
127148
+ var import_node_child_process9;
126491
127149
  var init_repo = __esm({
126492
127150
  "src/lifecycle/repo.ts"() {
126493
127151
  "use strict";
126494
- import_node_child_process8 = require("child_process");
127152
+ import_node_child_process9 = require("child_process");
126495
127153
  }
126496
127154
  });
126497
127155
 
@@ -126541,39 +127199,16 @@ var init_setKey = __esm({
126541
127199
  }
126542
127200
  });
126543
127201
 
126544
- // src/pty-host/protocol.ts
126545
- function isHostEvent(message) {
126546
- return "type" in message && message.type === "event";
126547
- }
126548
- function encodeMessage(message) {
126549
- return `${JSON.stringify(message)}
126550
- `;
126551
- }
126552
- var PTY_HOST_PROTOCOL_VERSION, LineDecoder;
126553
- var init_protocol = __esm({
126554
- "src/pty-host/protocol.ts"() {
126555
- "use strict";
126556
- PTY_HOST_PROTOCOL_VERSION = 1;
126557
- LineDecoder = class {
126558
- buffer = "";
126559
- push(chunk) {
126560
- this.buffer += chunk;
126561
- const lines = this.buffer.split("\n");
126562
- this.buffer = lines.pop() ?? "";
126563
- return lines.filter((line) => line.length > 0);
126564
- }
126565
- };
126566
- }
126567
- });
126568
-
126569
127202
  // src/pty-host/host.ts
126570
127203
  var host_exports = {};
126571
127204
  __export(host_exports, {
127205
+ HOST_HEARTBEAT_TIMEOUT_MS: () => HOST_HEARTBEAT_TIMEOUT_MS,
126572
127206
  HOST_IDLE_AFTER_MS: () => HOST_IDLE_AFTER_MS,
126573
127207
  HOST_IDLE_SWEEP_MS: () => HOST_IDLE_SWEEP_MS,
127208
+ HOST_ORPHAN_SWEEP_MS: () => HOST_ORPHAN_SWEEP_MS,
126574
127209
  SessionHost: () => SessionHost
126575
127210
  });
126576
- var HOST_IDLE_SWEEP_MS, HOST_IDLE_AFTER_MS, SessionHost;
127211
+ var HOST_IDLE_SWEEP_MS, HOST_IDLE_AFTER_MS, HOST_HEARTBEAT_TIMEOUT_MS, HOST_ORPHAN_SWEEP_MS, SessionHost;
126577
127212
  var init_host = __esm({
126578
127213
  "src/pty-host/host.ts"() {
126579
127214
  "use strict";
@@ -126582,6 +127217,8 @@ var init_host = __esm({
126582
127217
  init_protocol();
126583
127218
  HOST_IDLE_SWEEP_MS = 5 * 60 * 1e3;
126584
127219
  HOST_IDLE_AFTER_MS = 6 * 60 * 60 * 1e3;
127220
+ HOST_HEARTBEAT_TIMEOUT_MS = 3e4;
127221
+ HOST_ORPHAN_SWEEP_MS = 1e4;
126585
127222
  SessionHost = class {
126586
127223
  runner;
126587
127224
  log;
@@ -126591,9 +127228,19 @@ var init_host = __esm({
126591
127228
  lastAgentChunkAt = /* @__PURE__ */ new Map();
126592
127229
  idleTimer = null;
126593
127230
  idleAfterMs;
127231
+ onShutdown;
127232
+ heartbeatTimeoutMs;
127233
+ heartbeatLeases = /* @__PURE__ */ new Map();
127234
+ lastRegistryHeartbeat = null;
127235
+ orphanTimer = null;
127236
+ onOrphaned;
127237
+ orphaned = false;
126594
127238
  constructor(options = {}) {
126595
127239
  this.log = options.logger ?? getLogger("pty-host");
126596
127240
  this.idleAfterMs = options.idleAfterMs ?? HOST_IDLE_AFTER_MS;
127241
+ this.onShutdown = options.onShutdown;
127242
+ this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? HOST_HEARTBEAT_TIMEOUT_MS;
127243
+ this.onOrphaned = options.onOrphaned;
126597
127244
  this.runner = new LiveSessionManager({
126598
127245
  logger: this.log,
126599
127246
  onOutput: (sessionId, data) => {
@@ -126609,6 +127256,11 @@ var init_host = __esm({
126609
127256
  });
126610
127257
  this.idleTimer = setInterval(() => this.reapIdle(), options.idleSweepMs ?? HOST_IDLE_SWEEP_MS);
126611
127258
  this.idleTimer.unref?.();
127259
+ this.orphanTimer = setInterval(
127260
+ () => this.reapOrphan(),
127261
+ options.orphanSweepMs ?? HOST_ORPHAN_SWEEP_MS
127262
+ );
127263
+ this.orphanTimer.unref?.();
126612
127264
  }
126613
127265
  /**
126614
127266
  * Attach a connected streamer. Returns the disposer for its disconnect.
@@ -126621,6 +127273,7 @@ var init_host = __esm({
126621
127273
  transport.onLine((chunk) => this.handleChunk(transport, chunk));
126622
127274
  return () => {
126623
127275
  this.subscribers.delete(transport);
127276
+ this.heartbeatLeases.delete(transport);
126624
127277
  this.log.info("[pty-host] streamer disconnected; sessions kept", {
126625
127278
  event: "pty_host.streamer_disconnected",
126626
127279
  subscribers: this.subscribers.size
@@ -126648,6 +127301,7 @@ var init_host = __esm({
126648
127301
  try {
126649
127302
  const result = await this.handle(transport, request);
126650
127303
  transport.send(encodeMessage({ id: request.id, ok: true, result }));
127304
+ if (request.type === "shutdown-host") this.onShutdown?.();
126651
127305
  } catch (err) {
126652
127306
  transport.send(
126653
127307
  encodeMessage({
@@ -126672,6 +127326,14 @@ var init_host = __esm({
126672
127326
  protocolVersion: PTY_HOST_PROTOCOL_VERSION,
126673
127327
  sessions: this.runner.listSessions().map((session) => this.toHostSession(session))
126674
127328
  };
127329
+ case "heartbeat":
127330
+ this.heartbeatLeases.set(transport, Date.now());
127331
+ this.lastRegistryHeartbeat = {
127332
+ registryState: request.registryState,
127333
+ referencedSessionIds: [...request.referencedSessionIds],
127334
+ at: Date.now()
127335
+ };
127336
+ return {};
126675
127337
  case "spawn": {
126676
127338
  const session = request.sessionId === null ? await this.runner.startFresh(request.options) : await this.runner.start(request.sessionId, {
126677
127339
  ...request.options,
@@ -126696,6 +127358,8 @@ var init_host = __esm({
126696
127358
  }
126697
127359
  return {};
126698
127360
  }
127361
+ case "shutdown-host":
127362
+ return {};
126699
127363
  case "replay": {
126700
127364
  const lines = await this.runner.getOutputLines(request.sessionId, request.maxLines);
126701
127365
  return { lines, output: this.runner.getOutput(request.sessionId) };
@@ -126751,6 +127415,23 @@ var init_host = __esm({
126751
127415
  sessionCount() {
126752
127416
  return this.runner.listSessions().length;
126753
127417
  }
127418
+ reapOrphan(now = Date.now()) {
127419
+ if (this.orphaned) return false;
127420
+ for (const lastSeen of this.heartbeatLeases.values()) {
127421
+ if (now - lastSeen <= this.heartbeatTimeoutMs) return false;
127422
+ }
127423
+ const registry2 = this.lastRegistryHeartbeat;
127424
+ if (registry2?.registryState !== "known") return false;
127425
+ if (now - registry2.at <= this.heartbeatTimeoutMs) return false;
127426
+ if (registry2.referencedSessionIds.length > 0) return false;
127427
+ if (this.sessionCount() > 0) return false;
127428
+ this.orphaned = true;
127429
+ this.log.info("[pty-host] releasing unreferenced idle host", {
127430
+ event: "pty_host.orphan_reap"
127431
+ });
127432
+ this.onOrphaned?.();
127433
+ return true;
127434
+ }
126754
127435
  /**
126755
127436
  * Shut the host down, signalling its children.
126756
127437
  *
@@ -126761,104 +127442,16 @@ var init_host = __esm({
126761
127442
  dispose() {
126762
127443
  if (this.idleTimer) clearInterval(this.idleTimer);
126763
127444
  this.idleTimer = null;
127445
+ if (this.orphanTimer) clearInterval(this.orphanTimer);
127446
+ this.orphanTimer = null;
126764
127447
  this.subscribers.clear();
127448
+ this.heartbeatLeases.clear();
126765
127449
  this.runner.dispose();
126766
127450
  }
126767
127451
  };
126768
127452
  }
126769
127453
  });
126770
127454
 
126771
- // src/pty-host/socket.ts
126772
- var socket_exports = {};
126773
- __export(socket_exports, {
126774
- broadcast: () => broadcast,
126775
- connectToHost: () => connectToHost,
126776
- hostSocketPath: () => hostSocketPath,
126777
- listenForStreamers: () => listenForStreamers,
126778
- socketTransport: () => socketTransport
126779
- });
126780
- function hostSocketPath(instanceId) {
126781
- if (process.platform === "win32") {
126782
- return `\\\\.\\pipe\\threadbase-pty-host-${instanceId}`;
126783
- }
126784
- const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_node_path27.join)((0, import_node_os6.homedir)(), ".threadbase");
126785
- return (0, import_node_path27.join)(dir, "run", `pty-host-${instanceId}.sock`);
126786
- }
126787
- function socketTransport(socket) {
126788
- socket.setEncoding("utf8");
126789
- return {
126790
- send(line) {
126791
- socket.write(line);
126792
- },
126793
- onLine(handler) {
126794
- socket.on("data", (chunk) => handler(chunk));
126795
- },
126796
- onClose(handler) {
126797
- socket.once("close", handler);
126798
- socket.once("error", handler);
126799
- },
126800
- close() {
126801
- socket.destroy();
126802
- }
126803
- };
126804
- }
126805
- function connectToHost(socketPath) {
126806
- return new Promise((resolve4, reject) => {
126807
- const socket = (0, import_node_net2.createConnection)(socketPath);
126808
- socket.once("error", reject);
126809
- socket.once("connect", () => {
126810
- socket.removeListener("error", reject);
126811
- resolve4(socketTransport(socket));
126812
- });
126813
- });
126814
- }
126815
- async function listenForStreamers(socketPath, handlers) {
126816
- if (process.platform !== "win32") {
126817
- const { mkdirSync: mkdirSync16, rmSync: rmSync6 } = await import("fs");
126818
- const { dirname: dirname18 } = await import("path");
126819
- mkdirSync16(dirname18(socketPath), { recursive: true });
126820
- try {
126821
- const probe = await connectToHost(socketPath);
126822
- probe.close();
126823
- throw new Error(`a pty-host is already listening on ${socketPath}`);
126824
- } catch (err) {
126825
- if (err.message?.startsWith("a pty-host is already listening")) throw err;
126826
- rmSync6(socketPath, { force: true });
126827
- }
126828
- }
126829
- const server = (0, import_node_net2.createServer)((socket) => {
126830
- const transport = socketTransport(socket);
126831
- const dispose = handlers.onConnection(transport);
126832
- socket.once("close", dispose);
126833
- });
126834
- return new Promise((resolve4, reject) => {
126835
- server.once("error", reject);
126836
- server.listen(socketPath, () => {
126837
- server.removeListener("error", reject);
126838
- resolve4(server);
126839
- });
126840
- });
126841
- }
126842
- function broadcast(transports, message) {
126843
- const line = encodeMessage(message);
126844
- for (const t of transports) {
126845
- try {
126846
- t.send(line);
126847
- } catch {
126848
- }
126849
- }
126850
- }
126851
- var import_node_net2, import_node_os6, import_node_path27;
126852
- var init_socket = __esm({
126853
- "src/pty-host/socket.ts"() {
126854
- "use strict";
126855
- import_node_net2 = require("net");
126856
- import_node_os6 = require("os");
126857
- import_node_path27 = require("path");
126858
- init_protocol();
126859
- }
126860
- });
126861
-
126862
127455
  // node_modules/dotenv/config.js
126863
127456
  (function() {
126864
127457
  require_main().config(
@@ -134628,7 +135221,7 @@ var REF_PREFIX = "ref: refs/heads/";
134628
135221
  var MAX_DEPTH = 6;
134629
135222
  function readGitBranch(projectPath) {
134630
135223
  if (!projectPath) return null;
134631
- const log12 = getLogger2();
135224
+ const log13 = getLogger2();
134632
135225
  let dir = projectPath;
134633
135226
  let depth = 0;
134634
135227
  while (depth < MAX_DEPTH) {
@@ -134637,11 +135230,11 @@ function readGitBranch(projectPath) {
134637
135230
  const content = (0, import_fs2.readFileSync)(headPath, "utf-8").trim();
134638
135231
  if (content.startsWith(REF_PREFIX)) {
134639
135232
  const branch = content.slice(REF_PREFIX.length);
134640
- log12.trace({ projectPath, dir, branch }, "git: branch resolved");
135233
+ log13.trace({ projectPath, dir, branch }, "git: branch resolved");
134641
135234
  return branch;
134642
135235
  }
134643
135236
  if (content.length >= 7) {
134644
- log12.trace({ projectPath, dir }, "git: detached HEAD");
135237
+ log13.trace({ projectPath, dir }, "git: detached HEAD");
134645
135238
  return "(detached)";
134646
135239
  }
134647
135240
  return null;
@@ -134652,7 +135245,7 @@ function readGitBranch(projectPath) {
134652
135245
  dir = parent;
134653
135246
  depth++;
134654
135247
  }
134655
- log12.trace({ projectPath }, "git: no .git found within depth");
135248
+ log13.trace({ projectPath }, "git: no .git found within depth");
134656
135249
  return null;
134657
135250
  }
134658
135251
  function generateMatches(meta3, query) {
@@ -134945,8 +135538,8 @@ function cleanSystemTags(text) {
134945
135538
  return text.replace(SYSTEM_TAG_RE, "").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
134946
135539
  }
134947
135540
  async function parseMeta(filePath, account, tier) {
134948
- const log12 = getLogger2();
134949
- log12.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
135541
+ const log13 = getLogger2();
135542
+ log13.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
134950
135543
  const state = initialReducerState();
134951
135544
  const fileStream = (0, import_fs3.createReadStream)(filePath);
134952
135545
  const rl = (0, import_readline.createInterface)({ input: fileStream, crlfDelay: Infinity });
@@ -134963,22 +135556,22 @@ async function parseMeta(filePath, account, tier) {
134963
135556
  reduceLine(state, entry, tier);
134964
135557
  }
134965
135558
  } catch (err) {
134966
- log12.warn({ filePath, err }, "parseMeta: read failed");
135559
+ log13.warn({ filePath, err }, "parseMeta: read failed");
134967
135560
  return null;
134968
135561
  }
134969
135562
  if (state.badJsonLines > 0) {
134970
- log12.warn(
135563
+ log13.warn(
134971
135564
  { filePath, badJsonLines: state.badJsonLines },
134972
135565
  "parseMeta: skipped malformed JSON lines"
134973
135566
  );
134974
135567
  }
134975
135568
  const meta3 = finalizeMeta(state, filePath, account, tier);
134976
- if (!meta3) log12.trace({ filePath }, "parseMeta: no messages");
135569
+ if (!meta3) log13.trace({ filePath }, "parseMeta: no messages");
134977
135570
  return meta3;
134978
135571
  }
134979
135572
  async function parseConversation(filePath, account) {
134980
- const log12 = getLogger2();
134981
- log12.trace({ filePath, account }, "parseConversation: start");
135573
+ const log13 = getLogger2();
135574
+ log13.trace({ filePath, account }, "parseConversation: start");
134982
135575
  const messages = [];
134983
135576
  let badJsonLines = 0;
134984
135577
  const textParts = [];
@@ -135011,17 +135604,17 @@ async function parseConversation(filePath, account) {
135011
135604
  }
135012
135605
  }
135013
135606
  } catch (err) {
135014
- log12.warn({ filePath, err }, "parseConversation: read failed");
135607
+ log13.warn({ filePath, err }, "parseConversation: read failed");
135015
135608
  return null;
135016
135609
  }
135017
135610
  if (badJsonLines > 0) {
135018
- log12.warn({ filePath, badJsonLines }, "parseConversation: skipped malformed JSON lines");
135611
+ log13.warn({ filePath, badJsonLines }, "parseConversation: skipped malformed JSON lines");
135019
135612
  }
135020
135613
  if (messages.length === 0) {
135021
- log12.trace({ filePath }, "parseConversation: no messages");
135614
+ log13.trace({ filePath }, "parseConversation: no messages");
135022
135615
  return null;
135023
135616
  }
135024
- log12.debug({ filePath, messageCount: messages.length }, "parseConversation: complete");
135617
+ log13.debug({ filePath, messageCount: messages.length }, "parseConversation: complete");
135025
135618
  applyTeamInfo(messages, state);
135026
135619
  return {
135027
135620
  id: filePath,
@@ -135286,15 +135879,15 @@ async function detectDefaultProfile() {
135286
135879
  };
135287
135880
  }
135288
135881
  async function loadProfiles(configPath) {
135289
- const log12 = getLogger2();
135882
+ const log13 = getLogger2();
135290
135883
  try {
135291
135884
  const resolved = resolveConfigDir(configPath);
135292
135885
  const data = await (0, import_promises4.readFile)((0, import_path5.join)(resolved, PROFILES_FILE), "utf-8");
135293
135886
  const profiles = JSON.parse(data);
135294
- log12.debug({ configPath, count: profiles.length }, "profiles: loaded");
135887
+ log13.debug({ configPath, count: profiles.length }, "profiles: loaded");
135295
135888
  return profiles;
135296
135889
  } catch (err) {
135297
- log12.debug({ configPath, err }, "profiles: load failed, using default");
135890
+ log13.debug({ configPath, err }, "profiles: load failed, using default");
135298
135891
  const defaultProfile = await detectDefaultProfile();
135299
135892
  return [defaultProfile];
135300
135893
  }
@@ -135305,7 +135898,7 @@ function canonicalPath(p2) {
135305
135898
  var CodexCliProvider = class {
135306
135899
  name = CODEX_CLI_PROVIDER;
135307
135900
  async discover(roots) {
135308
- const log12 = getLogger2();
135901
+ const log13 = getLogger2();
135309
135902
  const results = [];
135310
135903
  for (const root of roots) {
135311
135904
  let paths;
@@ -135317,7 +135910,7 @@ var CodexCliProvider = class {
135317
135910
  unique: true
135318
135911
  });
135319
135912
  } catch (err) {
135320
- log12.warn({ root, err }, "codex discovery: glob failed");
135913
+ log13.warn({ root, err }, "codex discovery: glob failed");
135321
135914
  continue;
135322
135915
  }
135323
135916
  for (const filePath of paths) {
@@ -135325,7 +135918,7 @@ var CodexCliProvider = class {
135325
135918
  const s3 = await (0, import_promises5.stat)(filePath);
135326
135919
  if (s3.size > 0) results.push({ filePath: canonicalPath(filePath), account: "codex" });
135327
135920
  } catch (err) {
135328
- log12.warn({ filePath, err }, "codex discovery: stat failed");
135921
+ log13.warn({ filePath, err }, "codex discovery: stat failed");
135329
135922
  }
135330
135923
  }
135331
135924
  }
@@ -135469,7 +136062,7 @@ function getShortProjectName3(fullPath) {
135469
136062
  return fullPath.split("/").filter(Boolean).slice(-3).join("/");
135470
136063
  }
135471
136064
  async function parseCodexConversation(filePath, account) {
135472
- const log12 = getLogger2();
136065
+ const log13 = getLogger2();
135473
136066
  const messages = [];
135474
136067
  const textParts = [];
135475
136068
  let sessionId = "";
@@ -135505,7 +136098,7 @@ async function parseCodexConversation(filePath, account) {
135505
136098
  if (role === "user") lastUserText = text;
135506
136099
  }
135507
136100
  } catch (err) {
135508
- log12.warn({ filePath, err }, "parseCodexConversation: read failed");
136101
+ log13.warn({ filePath, err }, "parseCodexConversation: read failed");
135509
136102
  return null;
135510
136103
  }
135511
136104
  if (messages.length === 0) return null;
@@ -135527,7 +136120,7 @@ async function parseCodexConversation(filePath, account) {
135527
136120
  var EXCLUDED_SEGMENTS = ["/memory/", "/tool-results/"];
135528
136121
  var STAT_CONCURRENCY = 32;
135529
136122
  async function discoverJsonlFiles(dirs, onProgress) {
135530
- const log12 = getLogger2();
136123
+ const log13 = getLogger2();
135531
136124
  const results = [];
135532
136125
  for (const { projectsDir, account } of dirs) {
135533
136126
  let filePaths;
@@ -135538,7 +136131,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
135538
136131
  dot: false
135539
136132
  });
135540
136133
  } catch (err) {
135541
- log12.warn({ projectsDir, account, err }, "discovery: glob failed");
136134
+ log13.warn({ projectsDir, account, err }, "discovery: glob failed");
135542
136135
  continue;
135543
136136
  }
135544
136137
  const filtered = filePaths.filter((fp) => !EXCLUDED_SEGMENTS.some((seg) => fp.includes(seg)));
@@ -135553,7 +136146,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
135553
136146
  const s3 = await (0, import_promises6.stat)(filePath);
135554
136147
  return { filePath, size: s3.size };
135555
136148
  } catch (err) {
135556
- log12.warn({ filePath, err }, "discovery: stat failed");
136149
+ log13.warn({ filePath, err }, "discovery: stat failed");
135557
136150
  return { filePath, size: -1 };
135558
136151
  }
135559
136152
  })
@@ -135569,7 +136162,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
135569
136162
  }
135570
136163
  }
135571
136164
  }
135572
- log12.debug(
136165
+ log13.debug(
135573
136166
  {
135574
136167
  projectsDir,
135575
136168
  account,
@@ -135583,7 +136176,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
135583
136176
  );
135584
136177
  onProgress?.(results.length);
135585
136178
  }
135586
- log12.debug({ totalFiles: results.length, dirs: dirs.length }, "discovery: complete");
136179
+ log13.debug({ totalFiles: results.length, dirs: dirs.length }, "discovery: complete");
135587
136180
  return results;
135588
136181
  }
135589
136182
  var ThreadbaseProvider = class {
@@ -135893,7 +136486,7 @@ function classify(filePath, existing) {
135893
136486
  return { change: "reindex", stat: stat42 };
135894
136487
  }
135895
136488
  async function parseMetaWithProvider(provider, filePath, account, tier) {
135896
- const log12 = getLogger2();
136489
+ const log13 = getLogger2();
135897
136490
  const acc = provider.createEmptyAccumulator();
135898
136491
  const rl = (0, import_readline3.createInterface)({
135899
136492
  input: (0, import_fs11.createReadStream)(filePath),
@@ -135911,11 +136504,11 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
135911
136504
  try {
135912
136505
  provider.reduceEntry(acc, entry, tier);
135913
136506
  } catch (err) {
135914
- log12.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
136507
+ log13.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
135915
136508
  }
135916
136509
  }
135917
136510
  } catch (err) {
135918
- log12.warn({ filePath, provider: provider.name, err }, "provider parse: read failed");
136511
+ log13.warn({ filePath, provider: provider.name, err }, "provider parse: read failed");
135919
136512
  return null;
135920
136513
  }
135921
136514
  return provider.finalize(acc, filePath, account, tier);
@@ -136089,8 +136682,8 @@ CREATE INDEX IF NOT EXISTS idx_scanned_dirs_parent_root ON scanned_dirs(parent_r
136089
136682
  function runMigrations(db) {
136090
136683
  const current = db.pragma("user_version", { simple: true });
136091
136684
  if (current >= SCHEMA_VERSION) return;
136092
- const log12 = getLogger2();
136093
- log12.info({ from: current, to: SCHEMA_VERSION }, "migrations: applying");
136685
+ const log13 = getLogger2();
136686
+ log13.info({ from: current, to: SCHEMA_VERSION }, "migrations: applying");
136094
136687
  if (current >= 1 && current < 2 && tableExists(db, "conversations")) {
136095
136688
  for (const [col, ddl] of [
136096
136689
  [
@@ -136132,7 +136725,7 @@ function openDatabase(dbPath) {
136132
136725
  }
136133
136726
  var FULL_RECONCILE_EVERY_N_SCANS = 20;
136134
136727
  async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
136135
- const log12 = getLogger2();
136728
+ const log13 = getLogger2();
136136
136729
  if (options.fullRescan) {
136137
136730
  return discoverJsonlFiles(dirs);
136138
136731
  }
@@ -136174,7 +136767,7 @@ async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
136174
136767
  }
136175
136768
  }
136176
136769
  }
136177
- log12.debug(
136770
+ log13.debug(
136178
136771
  { totalFiles: results.length, dirs: dirs.length },
136179
136772
  "dir-watermark: gated discovery complete"
136180
136773
  );
@@ -136680,7 +137273,7 @@ var PersistentEngine = class {
136680
137273
  // changed since the last index, and upsert their metadata. Returns the number
136681
137274
  // of files seen on disk this pass (the scan "scanned" count).
136682
137275
  async indexAll(activeProfiles, options) {
136683
- const log12 = getLogger2();
137276
+ const log13 = getLogger2();
136684
137277
  const tier = resolveTier(options.tier ?? "standard", options.tiers);
136685
137278
  const enabled = options.providers ?? [CLAUDE_CODE_PROVIDER];
136686
137279
  const discovered = [];
@@ -136744,7 +137337,7 @@ var PersistentEngine = class {
136744
137337
  for (const path2 of this.files.activePathsByAccounts([...coveredAccounts])) {
136745
137338
  if (!seen.has(path2)) this.markDeleted(path2);
136746
137339
  }
136747
- log12.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
137340
+ log13.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
136748
137341
  return { scanned };
136749
137342
  }
136750
137343
  // (Re)index a single file. Classifies the change vs. the persisted cursor:
@@ -136755,7 +137348,7 @@ var PersistentEngine = class {
136755
137348
  // alongside the meta so callers (refreshFile) can keep, extend, or evict
136756
137349
  // their own per-file caches without re-stat'ing the file (racy) themselves.
136757
137350
  async indexFile(rawFilePath, account, tierName, customTiers, resolveGitBranch, force = false, provider) {
136758
- const log12 = getLogger2();
137351
+ const log13 = getLogger2();
136759
137352
  const tier = resolveTier(tierName, customTiers);
136760
137353
  const filePath = canonicalPath(rawFilePath);
136761
137354
  const existing = this.files.getByPath(filePath);
@@ -136786,7 +137379,7 @@ var PersistentEngine = class {
136786
137379
  try {
136787
137380
  result = await tailReduce(filePath, startOffset, startLine, state, tier);
136788
137381
  } catch (err) {
136789
- log12.warn({ filePath, err }, "persistent: tail read failed");
137382
+ log13.warn({ filePath, err }, "persistent: tail read failed");
136790
137383
  return { meta: null, change };
136791
137384
  }
136792
137385
  const meta3 = finalizeMeta(state, filePath, account, tier);
@@ -136829,7 +137422,7 @@ var PersistentEngine = class {
136829
137422
  )
136830
137423
  );
136831
137424
  }
136832
- log12.debug(
137425
+ log13.debug(
136833
137426
  { filePath, change, bytesRead: result.newOffset - startOffset, msgs: meta3.messageCount },
136834
137427
  "persistent: indexed file"
136835
137428
  );
@@ -136841,7 +137434,7 @@ var PersistentEngine = class {
136841
137434
  // so the next pass classifies an unchanged file as "unchanged" and skips it;
136842
137435
  // any change reparses from 0 again. No reducer_state is persisted.
136843
137436
  async indexFileWithProvider(provider, filePath, account, tier, stat42, resolveGitBranch) {
136844
- const log12 = getLogger2();
137437
+ const log13 = getLogger2();
136845
137438
  const meta3 = await parseMetaWithProvider(provider, filePath, account, tier);
136846
137439
  if (!meta3) {
136847
137440
  this.markDeleted(filePath);
@@ -136869,7 +137462,7 @@ var PersistentEngine = class {
136869
137462
  });
136870
137463
  });
136871
137464
  upsert();
136872
- log12.debug(
137465
+ log13.debug(
136873
137466
  { filePath, provider: provider.name, msgs: meta3.messageCount },
136874
137467
  "persistent: indexed provider file"
136875
137468
  );
@@ -136983,7 +137576,7 @@ var FileWatcher = class {
136983
137576
  // Resolves once every underlying chokidar watcher has finished its initial
136984
137577
  // scan, so the caller knows subsequent FS changes will be observed.
136985
137578
  async start() {
136986
- const log12 = getLogger2();
137579
+ const log13 = getLogger2();
136987
137580
  const ready = [];
136988
137581
  for (const profile of this.profiles) {
136989
137582
  const dir = getProjectsDir(profile);
@@ -136995,7 +137588,7 @@ var FileWatcher = class {
136995
137588
  watcher.on("add", (p2) => this.dispatch(p2, profile.id, "add")).on("change", (p2) => this.dispatch(p2, profile.id, "change")).on("unlink", (p2) => this.dispatch(p2, profile.id, "unlink"));
136996
137589
  ready.push(new Promise((resolve4) => watcher.once("ready", () => resolve4())));
136997
137590
  this.watchers.push(watcher);
136998
- log12.debug({ dir, account: profile.id }, "watcher: watching");
137591
+ log13.debug({ dir, account: profile.id }, "watcher: watching");
136999
137592
  }
137000
137593
  await Promise.all(ready);
137001
137594
  }
@@ -137042,14 +137635,14 @@ var IndexQueue = class {
137042
137635
  async drain() {
137043
137636
  if (this.running) return;
137044
137637
  this.running = true;
137045
- const log12 = getLogger2();
137638
+ const log13 = getLogger2();
137046
137639
  while (this.pending.size > 0) {
137047
137640
  const [path2, job] = this.pending.entries().next().value;
137048
137641
  this.pending.delete(path2);
137049
137642
  try {
137050
137643
  await this.process(job);
137051
137644
  } catch (err) {
137052
- log12.warn({ filePath: job.filePath, err }, "index-queue: job failed");
137645
+ log13.warn({ filePath: job.filePath, err }, "index-queue: job failed");
137053
137646
  }
137054
137647
  }
137055
137648
  this.running = false;
@@ -137158,13 +137751,13 @@ var ConversationScanner = class {
137158
137751
  // active metas and run the identical filter/sort/view/paginate pipeline as
137159
137752
  // the in-memory path — guaranteeing an identical ScanResult shape.
137160
137753
  async scanPersistent(activeProfiles, options) {
137161
- const log12 = getLogger2();
137754
+ const log13 = getLogger2();
137162
137755
  const startedAt = Date.now();
137163
137756
  const engine = this.engine();
137164
137757
  const { scanned } = await engine.indexAll(activeProfiles, options);
137165
137758
  const allMetas = engine.allActive();
137166
137759
  const { conversations, total } = this.finalize(allMetas, options);
137167
- log12.info(
137760
+ log13.info(
137168
137761
  { scanned, kept: allMetas.length, filteredTotal: total, elapsedMs: Date.now() - startedAt },
137169
137762
  "scan: complete (persistent)"
137170
137763
  );
@@ -137191,10 +137784,10 @@ var ConversationScanner = class {
137191
137784
  return { conversations, total };
137192
137785
  }
137193
137786
  async scanInMemory(activeProfiles, options) {
137194
- const log12 = getLogger2();
137787
+ const log13 = getLogger2();
137195
137788
  const startedAt = Date.now();
137196
137789
  const tier = this.lastTier;
137197
- log12.info(
137790
+ log13.info(
137198
137791
  {
137199
137792
  activeProfiles: activeProfiles.length,
137200
137793
  tier: tier.name,
@@ -137248,7 +137841,7 @@ var ConversationScanner = class {
137248
137841
  return meta3;
137249
137842
  } catch (err) {
137250
137843
  parseFailures++;
137251
- log12.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
137844
+ log13.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
137252
137845
  return null;
137253
137846
  }
137254
137847
  })
@@ -137268,12 +137861,12 @@ var ConversationScanner = class {
137268
137861
  options.onBatch?.(batchMetas);
137269
137862
  }
137270
137863
  scanned += batch.length;
137271
- log12.debug({ scanned, totalFiles, batchKept: batchMetas.length }, "scan: batch complete");
137864
+ log13.debug({ scanned, totalFiles, batchKept: batchMetas.length }, "scan: batch complete");
137272
137865
  options.onProgress?.(scanned, totalFiles);
137273
137866
  }
137274
137867
  const { conversations, total } = this.finalize(allMetas, options);
137275
137868
  const elapsedMs = Date.now() - startedAt;
137276
- log12.info(
137869
+ log13.info(
137277
137870
  {
137278
137871
  totalFiles,
137279
137872
  scanned,
@@ -137287,13 +137880,13 @@ var ConversationScanner = class {
137287
137880
  return { conversations, total, scanned };
137288
137881
  }
137289
137882
  async search(query, options = {}) {
137290
- const log12 = getLogger2();
137291
- log12.debug({ query, indexSize: this.indexer.getDocumentCount() }, "search: start");
137883
+ const log13 = getLogger2();
137884
+ log13.debug({ query, indexSize: this.indexer.getDocumentCount() }, "search: start");
137292
137885
  let results;
137293
137886
  if (this.persistent) {
137294
137887
  const engine = this.engine();
137295
137888
  if (engine.conversations.count() === 0) {
137296
- log12.debug("search: persistent index empty, triggering scan");
137889
+ log13.debug("search: persistent index empty, triggering scan");
137297
137890
  const profiles = await this.resolveProfiles(options.profiles);
137298
137891
  const activeProfiles = profiles.filter((p2) => p2.enabled && p2.scanHistory !== false);
137299
137892
  await engine.indexAll(activeProfiles, { ...options, limit: void 0, offset: void 0 });
@@ -137306,7 +137899,7 @@ var ConversationScanner = class {
137306
137899
  }));
137307
137900
  } else {
137308
137901
  if (this.indexer.getDocumentCount() === 0) {
137309
- log12.debug("search: index empty, triggering scan");
137902
+ log13.debug("search: index empty, triggering scan");
137310
137903
  await this.scan({ ...options, limit: void 0, offset: void 0 });
137311
137904
  }
137312
137905
  results = this.indexer.search(query, {
@@ -137349,23 +137942,23 @@ var ConversationScanner = class {
137349
137942
  const limit = options.limit ?? 50;
137350
137943
  const offset = options.offset ?? 0;
137351
137944
  const sliced = results.slice(offset, offset + limit);
137352
- log12.debug({ query, matched: results.length, returned: sliced.length }, "search: complete");
137945
+ log13.debug({ query, matched: results.length, returned: sliced.length }, "search: complete");
137353
137946
  return sliced;
137354
137947
  }
137355
137948
  async getConversation(id, _options) {
137356
- const log12 = getLogger2();
137949
+ const log13 = getLogger2();
137357
137950
  const cid = canonicalPath(id);
137358
137951
  const cached4 = this.conversationLRU.get(cid);
137359
137952
  if (cached4) {
137360
- log12.debug({ id }, "getConversation: cache hit");
137953
+ log13.debug({ id }, "getConversation: cache hit");
137361
137954
  return cached4.conversation;
137362
137955
  }
137363
137956
  const meta3 = this.persistent ? this.engine().getByIdOrSession(cid) : this.metadataCache.get(cid) ?? this.resolveSessionId(cid);
137364
137957
  if (!meta3) {
137365
- log12.debug({ id }, "getConversation: not found in metadata");
137958
+ log13.debug({ id }, "getConversation: not found in metadata");
137366
137959
  return null;
137367
137960
  }
137368
- log12.debug({ id, filePath: meta3.filePath }, "getConversation: cache miss, parsing");
137961
+ log13.debug({ id, filePath: meta3.filePath }, "getConversation: cache miss, parsing");
137369
137962
  try {
137370
137963
  if (this.persistent && meta3.provider !== CODEX_CLI_PROVIDER) {
137371
137964
  const parsed = await parseConversationResumable(meta3.filePath, meta3.account);
@@ -137378,7 +137971,7 @@ var ConversationScanner = class {
137378
137971
  }
137379
137972
  return conversation;
137380
137973
  } catch (err) {
137381
- log12.warn({ id, filePath: meta3.filePath, err }, "getConversation: parse failed");
137974
+ log13.warn({ id, filePath: meta3.filePath, err }, "getConversation: parse failed");
137382
137975
  return null;
137383
137976
  }
137384
137977
  }
@@ -137464,7 +138057,7 @@ var ConversationScanner = class {
137464
138057
  return refresh;
137465
138058
  }
137466
138059
  async doRefreshFile(filePath, account) {
137467
- const log12 = getLogger2();
138060
+ const log13 = getLogger2();
137468
138061
  if (this.persistent) {
137469
138062
  const engine = this.engine();
137470
138063
  const previous2 = engine.getByIdOrSession(filePath);
@@ -137491,7 +138084,7 @@ var ConversationScanner = class {
137491
138084
  } else if (change === "appended") {
137492
138085
  await this.extendCachedConversations(cacheKeys, filePath, meta22.account);
137493
138086
  }
137494
- log12.debug({ filePath, change, kept: !!meta22 }, "refreshFile: updated persistent index");
138087
+ log13.debug({ filePath, change, kept: !!meta22 }, "refreshFile: updated persistent index");
137495
138088
  return meta22;
137496
138089
  }
137497
138090
  const previous = this.metadataCache.get(filePath);
@@ -137500,7 +138093,7 @@ var ConversationScanner = class {
137500
138093
  try {
137501
138094
  meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier);
137502
138095
  } catch (err) {
137503
- log12.warn({ filePath, err }, "refreshFile: parseMeta threw");
138096
+ log13.warn({ filePath, err }, "refreshFile: parseMeta threw");
137504
138097
  meta3 = null;
137505
138098
  }
137506
138099
  const evict = (m2) => {
@@ -137516,7 +138109,7 @@ var ConversationScanner = class {
137516
138109
  this.removeFromSessionIndex(previous);
137517
138110
  this.indexer.removeDocument(previous.id);
137518
138111
  }
137519
- log12.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
138112
+ log13.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
137520
138113
  return null;
137521
138114
  }
137522
138115
  meta3.gitBranch = readGitBranch(meta3.projectPath);
@@ -137529,7 +138122,7 @@ var ConversationScanner = class {
137529
138122
  } else {
137530
138123
  this.indexer.addDocument(meta3);
137531
138124
  }
137532
- log12.debug(
138125
+ log13.debug(
137533
138126
  { filePath, messageCount: meta3.messageCount },
137534
138127
  "refreshFile: updated in-memory indexes"
137535
138128
  );
@@ -137938,14 +138531,14 @@ function createConversationWriter(opts) {
137938
138531
  }
137939
138532
  const file2 = (0, import_node_path6.join)(baseDir, `${args.sessionId}.jsonl`);
137940
138533
  await (0, import_promises10.mkdir)((0, import_node_path6.dirname)(file2), { recursive: true });
137941
- const record2 = {
138534
+ const record3 = {
137942
138535
  role: "assistant",
137943
138536
  turnId: args.turnId,
137944
138537
  content: args.content,
137945
138538
  timestamp: Date.now(),
137946
138539
  ...args.reviewerOverruled ? { reviewerOverruled: true } : {}
137947
138540
  };
137948
- const line = `${JSON.stringify(record2)}
138541
+ const line = `${JSON.stringify(record3)}
137949
138542
  `;
137950
138543
  await (0, import_promises10.appendFile)(file2, line, { encoding: "utf8" });
137951
138544
  }
@@ -142259,21 +142852,53 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
142259
142852
  };
142260
142853
 
142261
142854
  // src/api/app.ts
142855
+ var ALREADY_HANDLED7 = 597;
142856
+ function summarizeQuery(query) {
142857
+ const keys = Object.keys(query).sort();
142858
+ if (keys.length === 0) return void 0;
142859
+ return keys.map((k2) => `${k2}=${/^-?\d+$/.test(query[k2]) ? query[k2] : "_"}`).join("&");
142860
+ }
142861
+ function countResponseBytes(res) {
142862
+ let bytes = 0;
142863
+ const add = (chunk) => {
142864
+ if (typeof chunk === "string") bytes += Buffer.byteLength(chunk);
142865
+ else if (chunk instanceof Uint8Array) bytes += chunk.byteLength;
142866
+ };
142867
+ const write = res.write;
142868
+ const end = res.end;
142869
+ res.write = function(...args) {
142870
+ add(args[0]);
142871
+ return write.apply(this, args);
142872
+ };
142873
+ res.end = function(...args) {
142874
+ add(args[0]);
142875
+ return end.apply(this, args);
142876
+ };
142877
+ return () => bytes;
142878
+ }
142262
142879
  var createHonoApp = (deps, upgradeWebSocket) => {
142263
142880
  const app = new Hono2();
142264
142881
  const httpLog = getLogger("http");
142265
142882
  app.use("*", async (c, next) => {
142266
142883
  const start = Date.now();
142267
142884
  const ua2 = c.req.header("user-agent") ?? "";
142885
+ const outgoing = c.env?.outgoing;
142886
+ const bytesWritten = outgoing && countResponseBytes(outgoing);
142268
142887
  await next();
142269
142888
  if (!deps.logMenubarRequests && c.req.header("x-client") === "menubar") return;
142270
142889
  const ms2 = Date.now() - start;
142271
- httpLog.info(`[req] ${c.req.method} ${c.req.path} \u2192 ${c.res.status} ${ms2}ms`, {
142890
+ const handled = c.res.status === ALREADY_HANDLED7 && outgoing !== void 0;
142891
+ const status = handled ? outgoing.statusCode : c.res.status;
142892
+ const qs2 = summarizeQuery(c.req.query());
142893
+ const bytes = handled && bytesWritten ? bytesWritten() : void 0;
142894
+ httpLog.info(`[req] ${c.req.method} ${c.req.path} \u2192 ${status} ${ms2}ms`, {
142272
142895
  method: c.req.method,
142273
142896
  path: c.req.path,
142274
- status: c.res.status,
142897
+ status,
142275
142898
  ms: ms2,
142276
142899
  ua: ua2,
142900
+ ...qs2 ? { qs: qs2 } : {},
142901
+ ...bytes === void 0 ? {} : { bytes },
142277
142902
  event: "http.request"
142278
142903
  });
142279
142904
  });
@@ -142366,6 +142991,75 @@ var import_promises12 = require("fs/promises");
142366
142991
  var import_path17 = require("path");
142367
142992
  var import_promises13 = require("timers/promises");
142368
142993
 
142994
+ // src/db/query-timing.ts
142995
+ init_logger();
142996
+ var log3 = getLogger("db");
142997
+ var DEFAULT_SLOW_QUERY_MS = 35;
142998
+ var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
142999
+ function deriveLabel(sql) {
143000
+ const verb = /^\s*(\w+)/.exec(sql)?.[1]?.toLowerCase() ?? "sql";
143001
+ const table = /(?:from|into|update)\s+([A-Za-z_]\w*)/i.exec(sql)?.[1] ?? "?";
143002
+ return `${verb}:${table}`;
143003
+ }
143004
+ function resolveSlowMs() {
143005
+ const raw2 = process.env.THREADBASE_DB_SLOW_QUERY_MS;
143006
+ if (raw2 === void 0 || raw2 === "") return DEFAULT_SLOW_QUERY_MS;
143007
+ const parsed = Number(raw2);
143008
+ return Number.isFinite(parsed) ? parsed : DEFAULT_SLOW_QUERY_MS;
143009
+ }
143010
+ function record2(label, ms2, rows, slowMs) {
143011
+ if (slowMs > 0 && ms2 >= slowMs) {
143012
+ log3.warn(
143013
+ `[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
143014
+ { event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
143015
+ "pino"
143016
+ );
143017
+ return;
143018
+ }
143019
+ if (log3.pino.isLevelEnabled("debug")) {
143020
+ log3.debug(
143021
+ `[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
143022
+ { event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
143023
+ "pino"
143024
+ );
143025
+ }
143026
+ }
143027
+ function rowsOf(method, result) {
143028
+ if (method === "all") return Array.isArray(result) ? result.length : 0;
143029
+ if (method === "run") return result?.changes ?? 0;
143030
+ return result === void 0 ? 0 : 1;
143031
+ }
143032
+ function instrumentDatabase(db, options = {}) {
143033
+ const slowMs = options.slowMs ?? resolveSlowMs();
143034
+ const prepare = db.prepare.bind(db);
143035
+ db.prepare = ((sql) => {
143036
+ const stmt = prepare(sql);
143037
+ const box = { label: deriveLabel(sql) };
143038
+ Object.defineProperty(stmt, LABEL, { value: box, configurable: true });
143039
+ for (const method of ["get", "all", "run"]) {
143040
+ const original = stmt[method].bind(stmt);
143041
+ Object.defineProperty(stmt, method, {
143042
+ configurable: true,
143043
+ writable: true,
143044
+ value: (...args) => {
143045
+ const started = performance.now();
143046
+ const result = original(...args);
143047
+ record2(box.label, performance.now() - started, rowsOf(method, result), slowMs);
143048
+ return result;
143049
+ }
143050
+ });
143051
+ }
143052
+ return stmt;
143053
+ });
143054
+ return db;
143055
+ }
143056
+ function labelStatements(statements) {
143057
+ for (const [name, stmt] of Object.entries(statements)) {
143058
+ const box = stmt?.[LABEL];
143059
+ if (box) box.label = name;
143060
+ }
143061
+ }
143062
+
142369
143063
  // src/db/sqlite-migrate.ts
142370
143064
  var import_fs15 = require("fs");
142371
143065
  var import_path15 = require("path");
@@ -142412,6 +143106,7 @@ function runSqliteMigrations(db, migrationsDir) {
142412
143106
  }
142413
143107
 
142414
143108
  // src/conversation-cache.ts
143109
+ init_logger();
142415
143110
  init_providers();
142416
143111
 
142417
143112
  // src/services/conversations/isAgentConversation.ts
@@ -142579,6 +143274,7 @@ CREATE TABLE IF NOT EXISTS session_names (
142579
143274
  updated_at INTEGER NOT NULL
142580
143275
  );
142581
143276
  `;
143277
+ var cacheLog = getLogger("cache");
142582
143278
  var ConversationCache = class _ConversationCache {
142583
143279
  db;
142584
143280
  tailSize;
@@ -142808,6 +143504,7 @@ var ConversationCache = class _ConversationCache {
142808
143504
  "SELECT COUNT(*) as cnt FROM conversation_message_index WHERE conversation_id = ?"
142809
143505
  )
142810
143506
  };
143507
+ labelStatements(this.stmts);
142811
143508
  }
142812
143509
  /**
142813
143510
  * Expose the underlying handle so projects/cache_metadata repositories can
@@ -142971,6 +143668,7 @@ var ConversationCache = class _ConversationCache {
142971
143668
  return walk;
142972
143669
  }
142973
143670
  async runBackfill(filePath) {
143671
+ const startedAt = performance.now();
142974
143672
  const convId = _ConversationCache.conversationIdForFile(filePath);
142975
143673
  this.deleteFileIndex(filePath, convId);
142976
143674
  this.indexParseState.delete(filePath);
@@ -143031,6 +143729,18 @@ var ConversationCache = class _ConversationCache {
143031
143729
  last_message_index: nextIndex - 1
143032
143730
  });
143033
143731
  this.indexParseState.set(filePath, state);
143732
+ const ms2 = Math.round(performance.now() - startedAt);
143733
+ cacheLog.info(
143734
+ `[cache] offset-index backfilled ${convId} ${ms2}ms`,
143735
+ {
143736
+ event: "offset_index.backfill_ok",
143737
+ conversationId: convId,
143738
+ ms: ms2,
143739
+ rows: nextIndex,
143740
+ bytes: stat7.size
143741
+ },
143742
+ "pino"
143743
+ );
143034
143744
  }
143035
143745
  /**
143036
143746
  * Windowed detail read straight from the offset index — the hot path.
@@ -143116,7 +143826,7 @@ var ConversationCache = class _ConversationCache {
143116
143826
  }
143117
143827
  static open(dbPath, tailSize = 10, migrationsDir, options) {
143118
143828
  (0, import_fs17.mkdirSync)((0, import_path17.dirname)(dbPath), { recursive: true });
143119
- const db = new import_better_sqlite32.default(dbPath);
143829
+ const db = instrumentDatabase(new import_better_sqlite32.default(dbPath));
143120
143830
  db.pragma("journal_mode = WAL");
143121
143831
  db.pragma("foreign_keys = ON");
143122
143832
  return new _ConversationCache(db, tailSize, migrationsDir, options);
@@ -144218,7 +144928,7 @@ var RuntimeStore = class _RuntimeStore {
144218
144928
  }
144219
144929
  db;
144220
144930
  static open(dbPath, migrationsDir) {
144221
- const db = new import_better_sqlite33.default(dbPath);
144931
+ const db = instrumentDatabase(new import_better_sqlite33.default(dbPath));
144222
144932
  db.pragma("journal_mode = WAL");
144223
144933
  runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
144224
144934
  return new _RuntimeStore(db);
@@ -144357,14 +145067,14 @@ var PairTokenStore = class {
144357
145067
  };
144358
145068
  }
144359
145069
  consume(token) {
144360
- const record2 = this.current;
144361
- if (!record2 || record2.token !== token) return { ok: false, reason: "unknown" };
144362
- if (Date.now() > record2.expiresAt) {
145070
+ const record3 = this.current;
145071
+ if (!record3 || record3.token !== token) return { ok: false, reason: "unknown" };
145072
+ if (Date.now() > record3.expiresAt) {
144363
145073
  this.current = null;
144364
145074
  return { ok: false, reason: "expired" };
144365
145075
  }
144366
- if (record2.used) return { ok: false, reason: "used" };
144367
- record2.used = true;
145076
+ if (record3.used) return { ok: false, reason: "used" };
145077
+ record3.used = true;
144368
145078
  return { ok: true };
144369
145079
  }
144370
145080
  peek() {
@@ -144653,6 +145363,42 @@ async function readGitBranch2(dir) {
144653
145363
 
144654
145364
  // src/server.ts
144655
145365
  init_providers();
145366
+ init_remote_session_runner();
145367
+
145368
+ // src/pty-host/spawn-host.ts
145369
+ var import_node_child_process3 = require("child_process");
145370
+ init_socket();
145371
+ var HOST_READY_TIMEOUT_MS = 5e3;
145372
+ var HOST_POLL_INTERVAL_MS = 50;
145373
+ async function connectOrSpawnHost(options) {
145374
+ const socketPath = hostSocketPath(options.instanceId);
145375
+ try {
145376
+ return await connectToHost(socketPath);
145377
+ } catch {
145378
+ }
145379
+ spawnDetachedHost(socketPath, options.entryPoint);
145380
+ const deadline = Date.now() + (options.timeoutMs ?? HOST_READY_TIMEOUT_MS);
145381
+ let lastError;
145382
+ while (Date.now() < deadline) {
145383
+ try {
145384
+ return await connectToHost(socketPath);
145385
+ } catch (err) {
145386
+ lastError = err;
145387
+ await new Promise((r) => setTimeout(r, HOST_POLL_INTERVAL_MS));
145388
+ }
145389
+ }
145390
+ throw new Error(
145391
+ `pty-host did not accept a connection on ${socketPath} within ${options.timeoutMs ?? HOST_READY_TIMEOUT_MS}ms` + (lastError instanceof Error ? `: ${lastError.message}` : "")
145392
+ );
145393
+ }
145394
+ function spawnDetachedHost(socketPath, entryPoint) {
145395
+ const child = (0, import_node_child_process3.spawn)(
145396
+ process.execPath,
145397
+ [entryPoint ?? process.argv[1], "pty-host", "--socket", socketPath],
145398
+ { detached: true, stdio: "ignore" }
145399
+ );
145400
+ child.unref();
145401
+ }
144656
145402
 
144657
145403
  // src/seal.ts
144658
145404
  var import_tweetnacl = __toESM(require_nacl_fast(), 1);
@@ -144752,10 +145498,10 @@ function fingerprintOf(ids) {
144752
145498
  return `sha256:${(0, import_crypto11.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
144753
145499
  }
144754
145500
  var CacheIntegrityMonitor = class {
144755
- constructor(cache, wsHub, log12, cacheDir, rescan, runDuringReset) {
145501
+ constructor(cache, wsHub, log13, cacheDir, rescan, runDuringReset) {
144756
145502
  this.cache = cache;
144757
145503
  this.wsHub = wsHub;
144758
- this.log = log12;
145504
+ this.log = log13;
144759
145505
  this.cacheDir = cacheDir;
144760
145506
  this.rescan = rescan;
144761
145507
  this.runDuringReset = runDuringReset;
@@ -145591,7 +146337,7 @@ function deriveProjectChatTitle(input) {
145591
146337
  var import_node_crypto3 = require("crypto");
145592
146338
  var import_node_http2 = require("http2");
145593
146339
  init_logger();
145594
- var log3 = getLogger("apns");
146340
+ var log4 = getLogger("apns");
145595
146341
  var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
145596
146342
  var APNS_MAX_PAYLOAD_BYTES = 4096;
145597
146343
  var JWT_TTL_SECONDS = 3e3;
@@ -145675,7 +146421,7 @@ var ApnsClient = class {
145675
146421
  }
145676
146422
  const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
145677
146423
  session.on("error", (err) => {
145678
- log3.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
146424
+ log4.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
145679
146425
  });
145680
146426
  this.session = session;
145681
146427
  return session;
@@ -145763,7 +146509,7 @@ function truncateLastOutput(raw2) {
145763
146509
  }
145764
146510
 
145765
146511
  // src/services/push/liveActivityNotifier.ts
145766
- var log4 = getLogger("live-activity");
146512
+ var log5 = getLogger("live-activity");
145767
146513
  function contentStateForSession(args) {
145768
146514
  const status = toLiveActivityStatus(args.session.status);
145769
146515
  if (!status) return null;
@@ -145823,7 +146569,7 @@ var LiveActivityNotifier = class {
145823
146569
  }
145824
146570
  await this.maybeSendName(session);
145825
146571
  } catch (err) {
145826
- log4.error("live_activity.notify_failed", {
146572
+ log5.error("live_activity.notify_failed", {
145827
146573
  event: "live_activity.notify_failed",
145828
146574
  sessionId: session.id,
145829
146575
  status: session.status,
@@ -145845,7 +146591,7 @@ var LiveActivityNotifier = class {
145845
146591
  });
145846
146592
  this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
145847
146593
  if (outcome.attempted > 0) {
145848
- log4.info("live_activity.updated", {
146594
+ log5.info("live_activity.updated", {
145849
146595
  event: "live_activity.updated",
145850
146596
  sessionId: session.id,
145851
146597
  status: contentState.status,
@@ -145869,7 +146615,7 @@ var LiveActivityNotifier = class {
145869
146615
  });
145870
146616
  open3.sessionNameSent = true;
145871
146617
  if (outcome.attempted > 0) {
145872
- log4.info("live_activity.updated", {
146618
+ log5.info("live_activity.updated", {
145873
146619
  event: "live_activity.updated",
145874
146620
  sessionId: session.id,
145875
146621
  status: contentState.status,
@@ -145888,7 +146634,7 @@ var LiveActivityNotifier = class {
145888
146634
  if (!contentState) return;
145889
146635
  const outcome = await this.sender.end({ sessionId: session.id, contentState });
145890
146636
  if (outcome.attempted > 0) {
145891
- log4.info("live_activity.ended", {
146637
+ log5.info("live_activity.ended", {
145892
146638
  event: "live_activity.ended",
145893
146639
  sessionId: session.id,
145894
146640
  ...outcome
@@ -145906,7 +146652,7 @@ init_logger();
145906
146652
 
145907
146653
  // src/services/push/liveActivitySender.ts
145908
146654
  init_logger();
145909
- var log5 = getLogger("live-activity");
146655
+ var log6 = getLogger("live-activity");
145910
146656
  var ACTIVITY_MAX_LIFETIME_MS = 8 * 60 * 60 * 1e3;
145911
146657
  function buildActivityKitPayload(args) {
145912
146658
  return {
@@ -145970,7 +146716,7 @@ var LiveActivitySender = class {
145970
146716
  );
145971
146717
  for (const { row, result, error: error51 } of results) {
145972
146718
  if (error51) {
145973
- log5.error("live_activity.send_failed", {
146719
+ log6.error("live_activity.send_failed", {
145974
146720
  event: "live_activity.send_failed",
145975
146721
  sessionId: args.sessionId,
145976
146722
  activityId: row.activity_id,
@@ -145991,7 +146737,7 @@ var LiveActivitySender = class {
145991
146737
  this.repo.expire(row.token, now);
145992
146738
  outcome.retired += 1;
145993
146739
  }
145994
- log5.warn("live_activity.send_rejected", {
146740
+ log6.warn("live_activity.send_rejected", {
145995
146741
  event: "live_activity.send_rejected",
145996
146742
  sessionId: args.sessionId,
145997
146743
  activityId: row.activity_id,
@@ -146036,7 +146782,7 @@ var LiveActivitySender = class {
146036
146782
  };
146037
146783
 
146038
146784
  // src/services/push/liveActivityRenewal.ts
146039
- var log6 = getLogger("live-activity");
146785
+ var log7 = getLogger("live-activity");
146040
146786
  var RENEWAL_LEAD_MS = 30 * 60 * 1e3;
146041
146787
  var MAX_TIMER_MS = 60 * 60 * 1e3;
146042
146788
  function renewalDueAt(row) {
@@ -146084,7 +146830,7 @@ var LiveActivityRenewalScheduler = class {
146084
146830
  await this.renew(row, now);
146085
146831
  }
146086
146832
  } catch (err) {
146087
- log6.error("live_activity.renewal_sweep_failed", {
146833
+ log7.error("live_activity.renewal_sweep_failed", {
146088
146834
  event: "live_activity.renewal_sweep_failed",
146089
146835
  err: String(err)
146090
146836
  });
@@ -146113,7 +146859,7 @@ var LiveActivityRenewalScheduler = class {
146113
146859
  if (!session || !status) {
146114
146860
  this.deps.repo.claimRenewal(row.token, now);
146115
146861
  this.deps.repo.expire(row.token, now);
146116
- log6.info("live_activity.renewal_skipped", {
146862
+ log7.info("live_activity.renewal_skipped", {
146117
146863
  event: "live_activity.renewal_skipped",
146118
146864
  sessionId: row.session_id,
146119
146865
  activityId: row.activity_id,
@@ -146148,7 +146894,7 @@ var LiveActivityRenewalScheduler = class {
146148
146894
  startedAt,
146149
146895
  now
146150
146896
  });
146151
- log6.info("live_activity.renewed", {
146897
+ log7.info("live_activity.renewed", {
146152
146898
  event: "live_activity.renewed",
146153
146899
  sessionId: session.id,
146154
146900
  activityId: row.activity_id,
@@ -146158,7 +146904,7 @@ var LiveActivityRenewalScheduler = class {
146158
146904
  replacementRequested: started
146159
146905
  });
146160
146906
  } catch (err) {
146161
- log6.error("live_activity.renewal_failed", {
146907
+ log7.error("live_activity.renewal_failed", {
146162
146908
  event: "live_activity.renewal_failed",
146163
146909
  sessionId: session.id,
146164
146910
  activityId: row.activity_id,
@@ -146683,6 +147429,12 @@ var SessionStore = class {
146683
147429
  removeManaged(sessionId) {
146684
147430
  return this.managed.delete(sessionId);
146685
147431
  }
147432
+ /**
147433
+ * The **live** stored record — mutating it mutates the store. Paired with
147434
+ * `get()`, which hands back a throwaway response copy. Prefer
147435
+ * `updateManaged()` for writes; this is for readers that need the internal
147436
+ * shape (Date fields, `rehydrated`, …) rather than the wire shape.
147437
+ */
146686
147438
  getManaged(sessionId) {
146687
147439
  return this.managed.get(sessionId) ?? null;
146688
147440
  }
@@ -146692,12 +147444,21 @@ var SessionStore = class {
146692
147444
  this.discovered.set(proc.pid, proc);
146693
147445
  }
146694
147446
  }
147447
+ /**
147448
+ * The **live** stored records — mutating an element mutates the store. Paired
147449
+ * with `list()`, which hands back throwaway response copies.
147450
+ */
146695
147451
  listManaged() {
146696
147452
  return Array.from(this.managed.values());
146697
147453
  }
146698
147454
  // Build the session list: live PTY sessions (managed) merged with externally
146699
147455
  // discovered Claude processes. Managed sessions keyed by JSONL UUID take
146700
147456
  // priority — discovered processes with the same UUID are skipped.
147457
+ //
147458
+ // Returns freshly constructed response objects, NOT references into the
147459
+ // store — hence `Readonly`: writing to one changes nothing, so the compiler
147460
+ // refuses it. Persist state with `updateManaged()`; to decorate a response,
147461
+ // build a new object (`{ ...s, … }`) as `withReconciledLifecycle` does.
146701
147462
  list(ptyAttachedIds) {
146702
147463
  const results = [];
146703
147464
  const seenIds = /* @__PURE__ */ new Set();
@@ -146713,6 +147474,9 @@ var SessionStore = class {
146713
147474
  }
146714
147475
  return results;
146715
147476
  }
147477
+ // A freshly constructed response object, NOT a reference into the store —
147478
+ // hence `Readonly`, for the same reason as `list()` above. Use `getManaged()`
147479
+ // when you want the live record.
146716
147480
  get(sessionId, ptyAttachedIds) {
146717
147481
  const managed = this.managed.get(sessionId);
146718
147482
  if (managed) return managedToResponse(managed, ptyAttachedIds.has(sessionId));
@@ -146804,8 +147568,8 @@ function managedToResponse(s3, ptyAttached) {
146804
147568
  // behind it — `resumable`, and `historical` rather than `managed`
146805
147569
  // (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).
146806
147570
  lifecycle: ptyAttached ? "attached" : s3.rehydrated ? "resumable" : s3.failureReason != null ? "failed" : "completed",
146807
- lifecycleSource: ptyAttached ? "spawn" : s3.rehydrated ? "reconcile" : "exit",
146808
- // We spawned it, so `status` is the authoritative signal — no inferred
147571
+ lifecycleSource: ptyAttached ? s3.reconciled ? "reconcile" : "spawn" : s3.rehydrated ? "reconcile" : "exit",
147572
+ // We own its PTY, so `status` is the authoritative signal — no inferred
146809
147573
  // `activity` is attached for managed sessions.
146810
147574
  ownership: s3.rehydrated ? "historical" : "managed",
146811
147575
  projectPath: s3.projectPath,
@@ -148038,7 +148802,7 @@ var StreamerServer = class {
148038
148802
  if (!this.managedSessionsRepo) return [];
148039
148803
  let verdicts = [];
148040
148804
  try {
148041
- const rows = this.managedSessionsRepo.listNonTerminal();
148805
+ const rows = this.managedSessionsRepo.listNonTerminal().filter((row) => !this.ptyManager.hasSession(row.session_id));
148042
148806
  if (rows.length === PROBE_SET_MAX) {
148043
148807
  this.log.warn(
148044
148808
  `[reconcile] probe set hit its cap of ${PROBE_SET_MAX} \u2014 older rows skipped`,
@@ -148285,6 +149049,26 @@ var StreamerServer = class {
148285
149049
  if (session.provider !== CODEX_CLI_PROVIDER2) return session.id;
148286
149050
  return session.boundConversationId ?? session.projectPath;
148287
149051
  }
149052
+ /** Restore registry-only metadata after the host mirror has been adopted. */
149053
+ refreshHostedSessionsFromRegistry() {
149054
+ for (const session of this.ptyManager.listSessions()) {
149055
+ const row = this.managedSessionsRepo?.get(session.id);
149056
+ const merged = {
149057
+ ...session,
149058
+ reconciled: true,
149059
+ ...row?.project_id != null && { projectId: row.project_id },
149060
+ ...row?.session_name != null && { sessionName: row.session_name },
149061
+ ...row?.bound_conversation_id != null && {
149062
+ boundConversationId: row.bound_conversation_id
149063
+ },
149064
+ ...row?.resumed_from_conversation_id != null && {
149065
+ resumedFromConversationId: row.resumed_from_conversation_id
149066
+ }
149067
+ };
149068
+ this.sessionStore.addManaged(merged);
149069
+ void this.watchConversationFile(session.id, merged.boundConversationId ?? session.id);
149070
+ }
149071
+ }
148288
149072
  /**
148289
149073
  * Mirror a freshly-spawned session into the durable registry (C1 Phase 2).
148290
149074
  *
@@ -148380,6 +149164,7 @@ var StreamerServer = class {
148380
149164
  * of waiting on the interval.
148381
149165
  */
148382
149166
  reapIdleSessions(now = Date.now()) {
149167
+ if (this.ptyManager.isRemote()) return [];
148383
149168
  const reaped = [];
148384
149169
  for (const session of this.ptyManager.listSessions()) {
148385
149170
  if (session.status === "running") continue;
@@ -148481,6 +149266,40 @@ var StreamerServer = class {
148481
149266
  return true;
148482
149267
  }
148483
149268
  async listen(port, opts) {
149269
+ if (this.featureFlags.ptyHost) {
149270
+ try {
149271
+ let sessions = null;
149272
+ for (let attempt = 0; attempt < 2; attempt += 1) {
149273
+ const transport = await connectOrSpawnHost({
149274
+ instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0, import_os13.hostname)()
149275
+ });
149276
+ try {
149277
+ sessions = await this.ptyManager.useRemoteRunner(transport);
149278
+ break;
149279
+ } catch (err) {
149280
+ if (!(err instanceof PtyHostProtocolMismatchError) || attempt > 0) throw err;
149281
+ this.log.info(`[pty-host] replaced incompatible protocol ${err.hostVersion}`, {
149282
+ event: "pty_host.protocol_replaced",
149283
+ hostVersion: err.hostVersion,
149284
+ streamerVersion: err.streamerVersion
149285
+ });
149286
+ }
149287
+ }
149288
+ if (!sessions) throw new Error("pty-host replacement did not produce a compatible host");
149289
+ for (const session of sessions) {
149290
+ this.sessionStore.addManaged({ ...session, reconciled: true });
149291
+ }
149292
+ this.log.info(`[pty-host] re-adopted ${sessions.length} live session(s)`, {
149293
+ event: "pty_host.sessions_adopted",
149294
+ sessions: sessions.length
149295
+ });
149296
+ } catch (err) {
149297
+ this.log.error(
149298
+ "[pty-host] could not attach; falling back to in-process PTYs for this run",
149299
+ { event: "pty_host.attach_failed", err }
149300
+ );
149301
+ }
149302
+ }
148484
149303
  const dbConfig = this.disableDb ? null : getDbConfig();
148485
149304
  if (dbConfig) {
148486
149305
  this.dbPool = await createPool(dbConfig);
@@ -148495,8 +149314,10 @@ var StreamerServer = class {
148495
149314
  this.log.info("Database migrations applied", { event: "db.migrations_applied" });
148496
149315
  }
148497
149316
  await this.bindWithRetry(port);
148498
- this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
148499
- this.idleReaperTimer.unref?.();
149317
+ if (!this.ptyManager.isRemote()) {
149318
+ this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
149319
+ this.idleReaperTimer.unref?.();
149320
+ }
148500
149321
  const warmUp = new Promise((resolveWarm) => {
148501
149322
  {
148502
149323
  this.log.info(`Streamer server listening on port ${port}`, {
@@ -148514,6 +149335,15 @@ var StreamerServer = class {
148514
149335
  { error: message, abiMismatch, path: this.runtimeDbPath, event: "runtime.open_failed" }
148515
149336
  );
148516
149337
  }
149338
+ if (this.ptyManager.isRemote()) {
149339
+ this.ptyManager.startRemoteHeartbeat(() => {
149340
+ if (!this.managedSessionsRepo) {
149341
+ return { registryState: "unknown", referencedSessionIds: [] };
149342
+ }
149343
+ const referencedSessionIds = this.ptyManager.listSessions().filter((session) => this.managedSessionsRepo?.get(session.id)?.completed_at == null).map((session) => session.id);
149344
+ return { registryState: "known", referencedSessionIds };
149345
+ });
149346
+ }
148517
149347
  try {
148518
149348
  this.cache = ConversationCache.open(
148519
149349
  (0, import_path29.join)(this.cacheDir, "cache.db"),
@@ -148590,6 +149420,7 @@ var StreamerServer = class {
148590
149420
  );
148591
149421
  this.scannerPersistenceDisabled = true;
148592
149422
  }
149423
+ if (this.ptyManager.isRemote()) this.refreshHostedSessionsFromRegistry();
148593
149424
  void this.reconcilePreviousSessions().then(async (v2) => {
148594
149425
  const recoverableRows = this.rehydratePreviousSessions(v2);
148595
149426
  await this.autoResumePreviousSessions(recoverableRows);
@@ -148795,7 +149626,8 @@ var StreamerServer = class {
148795
149626
  }
148796
149627
  this.lastAgentChunkAt.clear();
148797
149628
  this.terminalSeq.clear();
148798
- this.recordShutdownState();
149629
+ if (this.ptyManager.isRemote()) this.ptyManager.dispose();
149630
+ else this.recordShutdownState();
148799
149631
  this.markScannerStaleDebounced.cancel();
148800
149632
  await Promise.all([...this.inFlightCacheWrites]);
148801
149633
  await Promise.all([...this.allScanners].map((s3) => s3.close()));
@@ -148803,7 +149635,7 @@ var StreamerServer = class {
148803
149635
  this.scanner = null;
148804
149636
  this.cache?.close();
148805
149637
  this.runtimeStore?.close();
148806
- this.ptyManager.dispose();
149638
+ if (!this.ptyManager.isRemote()) this.ptyManager.dispose();
148807
149639
  this.fileWatcher.dispose();
148808
149640
  this.externalTails.clear();
148809
149641
  this.wsHub.dispose();
@@ -149853,6 +150685,16 @@ var StreamerServer = class {
149853
150685
  const windowStart = Math.max(0, beforeIndex - scanLimit);
149854
150686
  const indexWindow = scanLimit > 0 && !hasAnchor && indexFilePath && this.cache ? this.cache.readMessageWindow(indexFilePath, windowStart, beforeIndex) : null;
149855
150687
  if (!indexWindow && indexFilePath && this.cache && !hasAnchor) {
150688
+ this.log.info(
150689
+ `[server] offset-index miss ${id} \u2192 scanner fallback`,
150690
+ {
150691
+ event: "offset_index.miss",
150692
+ conversationId: id,
150693
+ fromIndex: windowStart,
150694
+ toIndex: beforeIndex
150695
+ },
150696
+ "pino"
150697
+ );
149856
150698
  this.trackCacheWrite(
149857
150699
  this.cache.backfillIndex(indexFilePath).catch((err) => {
149858
150700
  this.log.warn("offset-index.backfill_failed", {
@@ -150127,14 +150969,15 @@ var StreamerServer = class {
150127
150969
  }
150128
150970
  async handleGetSession(sessionId, res) {
150129
150971
  if (this.rejectIfWarmingUp(res)) return;
150130
- const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
150131
- if (session) {
150132
- if (!(0, import_fs30.existsSync)(session.projectPath)) {
150133
- session.failureReason = `Project directory not found: ${session.projectPath}`;
150134
- }
150135
- const reconciled = this.withReconciledLifecycle([session])[0];
150136
- session.lifecycle = reconciled.lifecycle;
150137
- session.lifecycleSource = reconciled.lifecycleSource;
150972
+ const base = this.sessionStore.get(sessionId, this.ptyAttachedIds());
150973
+ if (base) {
150974
+ const reconciled = this.withReconciledLifecycle([base])[0];
150975
+ const session = {
150976
+ ...base,
150977
+ ...(0, import_fs30.existsSync)(base.projectPath) ? {} : { failureReason: `Project directory not found: ${base.projectPath}` },
150978
+ lifecycle: reconciled.lifecycle,
150979
+ lifecycleSource: reconciled.lifecycleSource
150980
+ };
150138
150981
  if (this.ptyManager.hasSession(sessionId)) {
150139
150982
  try {
150140
150983
  const lines = await this.ptyManager.getOutputLines(sessionId, 10);
@@ -150297,31 +151140,37 @@ var StreamerServer = class {
150297
151140
  this.sessionStore.addManaged(session);
150298
151141
  this.recordSessionSpawn(session);
150299
151142
  void this.watchConversationFile(sessionId, historyId);
150300
- const response = this.sessionStore.get(session.id, this.ptyAttachedIds());
150301
151143
  this.enrichResumedSessionAsync(sessionId, projectPath, conv);
151144
+ const response = this.sessionStore.get(session.id, this.ptyAttachedIds());
150302
151145
  return { ok: true, alreadyRunning: false, session, response };
150303
151146
  }
150304
151147
  enrichResumedSessionAsync(sessionId, projectPath, conv) {
150305
151148
  try {
150306
- const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
150307
- if (!session) return;
151149
+ if (!this.sessionStore.getManaged(sessionId)) return;
150308
151150
  if (conv) {
150309
- session.sessionName = conv.sessionName ?? void 0;
150310
- session.messageCount = conv.messageCount ?? 0;
150311
- session.account = conv.account ?? void 0;
150312
- session.filePath = conv.filePath ?? void 0;
151151
+ this.sessionStore.updateManaged(sessionId, {
151152
+ sessionName: conv.sessionName ?? void 0,
151153
+ messageCount: conv.messageCount ?? 0,
151154
+ account: conv.account ?? void 0,
151155
+ filePath: conv.filePath ?? void 0
151156
+ });
150313
151157
  }
150314
151158
  if (!this.cache || !this.projectsRepo || !this.conversationsRepo) return;
150315
151159
  const cached4 = this.cache.getMetaById(sessionId);
150316
151160
  if (cached4) {
150317
- session.model = cached4.model ?? void 0;
150318
- session.preview = cached4.preview ?? void 0;
150319
151161
  const first = cached4.firstMessage ? JSON.parse(cached4.firstMessage) : null;
150320
151162
  const last = cached4.lastMessage ? JSON.parse(cached4.lastMessage) : null;
150321
- session.firstMessageText = first?.text ?? void 0;
150322
- session.firstMessageAt = first?.timestamp ? new Date(first.timestamp).toISOString() : void 0;
150323
- session.lastMessageText = last?.text ?? void 0;
150324
- session.lastMessageAt = last?.timestamp ? new Date(last.timestamp).toISOString() : void 0;
151163
+ this.sessionStore.updateManaged(sessionId, {
151164
+ model: cached4.model ?? void 0,
151165
+ preview: cached4.preview ?? void 0,
151166
+ firstMessageText: first?.text ?? void 0,
151167
+ // parseIsoDateOrNull, not `new Date()`: an unparseable cached
151168
+ // timestamp must land as absent, not as an Invalid Date that
151169
+ // managedToResponse would throw on when it calls .toISOString().
151170
+ firstMessageAt: parseIsoDateOrNull(first?.timestamp) ?? void 0,
151171
+ lastMessageText: last?.text ?? void 0,
151172
+ lastMessageAt: parseIsoDateOrNull(last?.timestamp) ?? void 0
151173
+ });
150325
151174
  }
150326
151175
  let resolvedProjectId = cached4?.projectId ?? null;
150327
151176
  if (!resolvedProjectId) {
@@ -150333,8 +151182,10 @@ var StreamerServer = class {
150333
151182
  });
150334
151183
  }
150335
151184
  if (resolvedProjectId) {
150336
- session.projectId = resolvedProjectId;
150337
- session.resumedFromConversationId = sessionId;
151185
+ this.sessionStore.updateManaged(sessionId, {
151186
+ projectId: resolvedProjectId,
151187
+ resumedFromConversationId: sessionId
151188
+ });
150338
151189
  }
150339
151190
  } catch (err) {
150340
151191
  console.error(`[enrichResumedSessionAsync] ${sessionId}:`, err);
@@ -151611,7 +152462,7 @@ function isBrewInstall(scriptPath = process.argv[1] ?? "") {
151611
152462
  // src/updater/download.ts
151612
152463
  var import_node_crypto5 = require("crypto");
151613
152464
  var import_node_fs10 = require("fs");
151614
- var import_node_path10 = require("path");
152465
+ var import_node_path11 = require("path");
151615
152466
  var import_promises17 = require("stream/promises");
151616
152467
 
151617
152468
  // src/updater/manifest.ts
@@ -151671,7 +152522,7 @@ async function downloadAndVerify(opts) {
151671
152522
  `Manifest references ${artifact.filename} but it is not attached to the release`
151672
152523
  );
151673
152524
  }
151674
- (0, import_node_fs10.mkdirSync)((0, import_node_path10.dirname)(targetPath), { recursive: true });
152525
+ (0, import_node_fs10.mkdirSync)((0, import_node_path11.dirname)(targetPath), { recursive: true });
151675
152526
  const res = await fetch(asset.browserDownloadUrl);
151676
152527
  if (!res.ok || !res.body) {
151677
152528
  throw new Error(`Failed to download ${artifact.filename}: ${res.status} ${res.statusText}`);
@@ -151710,23 +152561,23 @@ async function downloadAndVerify(opts) {
151710
152561
  }
151711
152562
 
151712
152563
  // src/updater/paths.ts
151713
- var import_node_os5 = require("os");
151714
- var import_node_path11 = require("path");
151715
- var THREADBASE_ROOT = (0, import_node_path11.join)((0, import_node_os5.homedir)(), ".threadbase");
151716
- var RELEASES_DIR = (0, import_node_path11.join)(THREADBASE_ROOT, "releases");
151717
- var CURRENT_SYMLINK = (0, import_node_path11.join)(THREADBASE_ROOT, "current");
151718
- var DOWNLOAD_DIR = (0, import_node_path11.join)(THREADBASE_ROOT, "releases", ".tmp");
152564
+ var import_node_os6 = require("os");
152565
+ var import_node_path12 = require("path");
152566
+ var THREADBASE_ROOT = (0, import_node_path12.join)((0, import_node_os6.homedir)(), ".threadbase");
152567
+ var RELEASES_DIR = (0, import_node_path12.join)(THREADBASE_ROOT, "releases");
152568
+ var CURRENT_SYMLINK = (0, import_node_path12.join)(THREADBASE_ROOT, "current");
152569
+ var DOWNLOAD_DIR = (0, import_node_path12.join)(THREADBASE_ROOT, "releases", ".tmp");
151719
152570
  function releaseDir(version2) {
151720
- return (0, import_node_path11.join)(RELEASES_DIR, version2);
152571
+ return (0, import_node_path12.join)(RELEASES_DIR, version2);
151721
152572
  }
151722
152573
  function downloadPath(version2, filename) {
151723
- return (0, import_node_path11.join)(DOWNLOAD_DIR, `${version2}-${filename}`);
152574
+ return (0, import_node_path12.join)(DOWNLOAD_DIR, `${version2}-${filename}`);
151724
152575
  }
151725
152576
 
151726
152577
  // src/updater/restart.ts
151727
- var import_node_child_process3 = require("child_process");
152578
+ var import_node_child_process4 = require("child_process");
151728
152579
  var import_node_util3 = require("util");
151729
- var execFileP = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
152580
+ var execFileP = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
151730
152581
  var BREW_LAUNCHD_LABEL = "homebrew.mxcl.tb-streamer";
151731
152582
  var DEPLOY_LAUNCHD_LABEL = "com.ronen.threadbase";
151732
152583
  function resolveDefaultLabel() {
@@ -151876,11 +152727,11 @@ async function waitForRestartHealth(opts) {
151876
152727
 
151877
152728
  // src/updater/stamp-version.ts
151878
152729
  var import_node_fs11 = require("fs");
151879
- var import_node_path12 = require("path");
152730
+ var import_node_path13 = require("path");
151880
152731
  function stampVersionTxt(destDir, version2) {
151881
- const distDir = (0, import_node_path12.join)(destDir, "dist");
152732
+ const distDir = (0, import_node_path13.join)(destDir, "dist");
151882
152733
  (0, import_node_fs11.mkdirSync)(distDir, { recursive: true });
151883
- (0, import_node_fs11.writeFileSync)((0, import_node_path12.join)(distDir, "version.txt"), versionStamp(version2));
152734
+ (0, import_node_fs11.writeFileSync)((0, import_node_path13.join)(distDir, "version.txt"), versionStamp(version2));
151884
152735
  }
151885
152736
  function versionStamp(version2) {
151886
152737
  return `${version2}+update
@@ -151889,7 +152740,7 @@ function versionStamp(version2) {
151889
152740
 
151890
152741
  // src/updater/swap.ts
151891
152742
  var import_node_fs12 = require("fs");
151892
- var import_node_path13 = require("path");
152743
+ var import_node_path14 = require("path");
151893
152744
  var import_semver2 = __toESM(require_semver2(), 1);
151894
152745
  var KEEP_LAST_N = 2;
151895
152746
  function swapCurrent(version2) {
@@ -151905,7 +152756,7 @@ function swapCurrent(version2) {
151905
152756
  (0, import_node_fs12.rmSync)(CURRENT_SYMLINK, { recursive: true, force: true });
151906
152757
  }
151907
152758
  (0, import_node_fs12.renameSync)(tmp, CURRENT_SYMLINK);
151908
- (0, import_node_fs12.copyFileSync)((0, import_node_path13.join)(CURRENT_SYMLINK, "dist", "cli.cjs"), (0, import_node_path13.join)(THREADBASE_ROOT, "cli.js"));
152759
+ (0, import_node_fs12.copyFileSync)((0, import_node_path14.join)(CURRENT_SYMLINK, "dist", "cli.cjs"), (0, import_node_path14.join)(THREADBASE_ROOT, "cli.js"));
151909
152760
  publishVersionTxt(target, version2);
151910
152761
  return;
151911
152762
  }
@@ -151915,17 +152766,17 @@ function swapCurrent(version2) {
151915
152766
  }
151916
152767
  (0, import_node_fs12.symlinkSync)(target, tmpLink);
151917
152768
  (0, import_node_fs12.renameSync)(tmpLink, CURRENT_SYMLINK);
151918
- const cliJs = (0, import_node_path13.join)(THREADBASE_ROOT, "cli.js");
152769
+ const cliJs = (0, import_node_path14.join)(THREADBASE_ROOT, "cli.js");
151919
152770
  const tmpCliJs = `${cliJs}.new`;
151920
152771
  if ((0, import_node_fs12.existsSync)(tmpCliJs) || lstatSafeIsSymlink(tmpCliJs)) (0, import_node_fs12.unlinkSync)(tmpCliJs);
151921
- (0, import_node_fs12.symlinkSync)((0, import_node_path13.join)(target, "dist", "cli.cjs"), tmpCliJs);
152772
+ (0, import_node_fs12.symlinkSync)((0, import_node_path14.join)(target, "dist", "cli.cjs"), tmpCliJs);
151922
152773
  (0, import_node_fs12.renameSync)(tmpCliJs, cliJs);
151923
152774
  publishVersionTxt(target, version2);
151924
152775
  }
151925
152776
  function publishVersionTxt(target, version2) {
151926
- const dest = (0, import_node_path13.join)(THREADBASE_ROOT, "version.txt");
152777
+ const dest = (0, import_node_path14.join)(THREADBASE_ROOT, "version.txt");
151927
152778
  try {
151928
- (0, import_node_fs12.copyFileSync)((0, import_node_path13.join)(target, "dist", "version.txt"), dest);
152779
+ (0, import_node_fs12.copyFileSync)((0, import_node_path14.join)(target, "dist", "version.txt"), dest);
151929
152780
  } catch {
151930
152781
  (0, import_node_fs12.writeFileSync)(dest, versionStamp(version2));
151931
152782
  }
@@ -151964,7 +152815,7 @@ var import_fs31 = __toESM(require("fs"), 1);
151964
152815
  var import_node_events3 = require("events");
151965
152816
  var import_node_stream2 = __toESM(require("stream"), 1);
151966
152817
  var import_node_string_decoder = require("string_decoder");
151967
- var import_node_path14 = __toESM(require("path"), 1);
152818
+ var import_node_path15 = __toESM(require("path"), 1);
151968
152819
  var import_node_fs13 = __toESM(require("fs"), 1);
151969
152820
  var import_path30 = require("path");
151970
152821
  var import_events4 = require("events");
@@ -151972,27 +152823,27 @@ var import_assert = __toESM(require("assert"), 1);
151972
152823
  var import_buffer2 = require("buffer");
151973
152824
  var Ps = __toESM(require("zlib"), 1);
151974
152825
  var import_zlib = __toESM(require("zlib"), 1);
151975
- var import_node_path15 = require("path");
151976
152826
  var import_node_path16 = require("path");
152827
+ var import_node_path17 = require("path");
151977
152828
  var import_fs32 = __toESM(require("fs"), 1);
151978
152829
  var import_fs33 = __toESM(require("fs"), 1);
151979
152830
  var import_path31 = __toESM(require("path"), 1);
151980
- var import_node_path17 = require("path");
152831
+ var import_node_path18 = require("path");
151981
152832
  var import_path32 = __toESM(require("path"), 1);
151982
152833
  var import_node_fs14 = __toESM(require("fs"), 1);
151983
152834
  var import_node_assert = __toESM(require("assert"), 1);
151984
152835
  var import_node_crypto6 = require("crypto");
151985
152836
  var import_node_fs15 = __toESM(require("fs"), 1);
151986
- var import_node_path18 = __toESM(require("path"), 1);
152837
+ var import_node_path19 = __toESM(require("path"), 1);
151987
152838
  var import_fs34 = __toESM(require("fs"), 1);
151988
152839
  var import_node_fs16 = __toESM(require("fs"), 1);
151989
- var import_node_path19 = __toESM(require("path"), 1);
152840
+ var import_node_path20 = __toESM(require("path"), 1);
151990
152841
  var import_node_fs17 = __toESM(require("fs"), 1);
151991
152842
  var import_promises18 = __toESM(require("fs/promises"), 1);
151992
- var import_node_path20 = __toESM(require("path"), 1);
151993
- var import_node_path21 = require("path");
152843
+ var import_node_path21 = __toESM(require("path"), 1);
152844
+ var import_node_path22 = require("path");
151994
152845
  var import_node_fs18 = __toESM(require("fs"), 1);
151995
- var import_node_path22 = __toESM(require("path"), 1);
152846
+ var import_node_path23 = __toESM(require("path"), 1);
151996
152847
  var zr = Object.defineProperty;
151997
152848
  var Ur = (s3, t) => {
151998
152849
  for (var e in t) zr(s3, e, { get: t[e], enumerable: true });
@@ -152886,12 +153737,12 @@ var F = class {
152886
153737
  }
152887
153738
  };
152888
153739
  var un = (s3, t) => {
152889
- let i = s3, r = "", n, o = import_node_path15.posix.parse(s3).root || ".";
153740
+ let i = s3, r = "", n, o = import_node_path16.posix.parse(s3).root || ".";
152890
153741
  if (Buffer.byteLength(i) < 100) n = [i, r, false];
152891
153742
  else {
152892
- r = import_node_path15.posix.dirname(i), i = import_node_path15.posix.basename(i);
153743
+ r = import_node_path16.posix.dirname(i), i = import_node_path16.posix.basename(i);
152893
153744
  do
152894
- Buffer.byteLength(i) <= 100 && Buffer.byteLength(r) <= t ? n = [i, r, false] : Buffer.byteLength(i) > 100 && Buffer.byteLength(r) <= t ? n = [i.slice(0, 99), r, true] : (i = import_node_path15.posix.join(import_node_path15.posix.basename(r), i), r = import_node_path15.posix.dirname(r));
153745
+ Buffer.byteLength(i) <= 100 && Buffer.byteLength(r) <= t ? n = [i, r, false] : Buffer.byteLength(i) > 100 && Buffer.byteLength(r) <= t ? n = [i.slice(0, 99), r, true] : (i = import_node_path16.posix.join(import_node_path16.posix.basename(r), i), r = import_node_path16.posix.dirname(r));
152895
153746
  while (r !== o && n === void 0);
152896
153747
  n || (n = [s3.slice(0, 99), "", true]);
152897
153748
  }
@@ -152937,7 +153788,7 @@ var ft = class s {
152937
153788
  if (t === "") return Buffer.allocUnsafe(0);
152938
153789
  let e = Buffer.byteLength(t), i = 512 * Math.ceil(1 + e / 512), r = Buffer.allocUnsafe(i);
152939
153790
  for (let n = 0; n < 512; n++) r[n] = 0;
152940
- new F({ path: ("PaxHeader/" + (0, import_node_path16.basename)(this.path ?? "")).slice(0, 99), mode: this.mode || 420, uid: this.uid, gid: this.gid, size: e, mtime: this.mtime, type: this.global ? "GlobalExtendedHeader" : "ExtendedHeader", linkpath: "", uname: this.uname || "", gname: this.gname || "", devmaj: 0, devmin: 0, atime: this.atime, ctime: this.ctime }).encode(r), r.write(t, 512, e, "utf8");
153791
+ new F({ path: ("PaxHeader/" + (0, import_node_path17.basename)(this.path ?? "")).slice(0, 99), mode: this.mode || 420, uid: this.uid, gid: this.gid, size: e, mtime: this.mtime, type: this.global ? "GlobalExtendedHeader" : "ExtendedHeader", linkpath: "", uname: this.uname || "", gname: this.gname || "", devmaj: 0, devmin: 0, atime: this.atime, ctime: this.ctime }).encode(r), r.write(t, 512, e, "utf8");
152941
153792
  for (let n = e + 512; n < r.length; n++) r[n] = 0;
152942
153793
  return r;
152943
153794
  }
@@ -153393,7 +154244,7 @@ var Ct = K(Mn, Bn, (s3) => new rt(s3), (s3) => new rt(s3), (s3, t) => {
153393
154244
  t?.length && Qi(s3, t), s3.noResume || vn(s3);
153394
154245
  });
153395
154246
  var Ji = (s3, t, e) => (s3 &= 4095, e && (s3 = (s3 | 384) & -19), t && (s3 & 256 && (s3 |= 64), s3 & 32 && (s3 |= 8), s3 & 4 && (s3 |= 1)), s3);
153396
- var { isAbsolute: zn, parse: qs } = import_node_path17.win32;
154247
+ var { isAbsolute: zn, parse: qs } = import_node_path18.win32;
153397
154248
  var ce = (s3) => {
153398
154249
  let t = "", e = qs(s3);
153399
154250
  for (; zn(s3) || e.root; ) {
@@ -154102,11 +154953,11 @@ var $n = (s3, t) => {
154102
154953
  };
154103
154954
  var fr = (s3, t) => {
154104
154955
  t.forEach((e) => {
154105
- e.charAt(0) === "@" ? Ct({ file: import_node_path14.default.resolve(s3.cwd, e.slice(1)), sync: true, noResume: true, onReadEntry: (i) => s3.add(i) }) : s3.add(e);
154956
+ e.charAt(0) === "@" ? Ct({ file: import_node_path15.default.resolve(s3.cwd, e.slice(1)), sync: true, noResume: true, onReadEntry: (i) => s3.add(i) }) : s3.add(e);
154106
154957
  }), s3.end();
154107
154958
  };
154108
154959
  var dr = async (s3, t) => {
154109
- for (let e of t) e.charAt(0) === "@" ? await Ct({ file: import_node_path14.default.resolve(String(s3.cwd), e.slice(1)), noResume: true, onReadEntry: (i) => {
154960
+ for (let e of t) e.charAt(0) === "@" ? await Ct({ file: import_node_path15.default.resolve(String(s3.cwd), e.slice(1)), noResume: true, onReadEntry: (i) => {
154110
154961
  s3.add(i);
154111
154962
  } }) : s3.add(e);
154112
154963
  s3.end();
@@ -154144,13 +154995,13 @@ var Ei = (s3, t, e, i) => {
154144
154995
  });
154145
154996
  };
154146
154997
  var io = (s3, t, e, i, r) => {
154147
- if (t.isDirectory()) Es(import_node_path19.default.resolve(s3, t.name), e, i, (n) => {
154998
+ if (t.isDirectory()) Es(import_node_path20.default.resolve(s3, t.name), e, i, (n) => {
154148
154999
  if (n) return r(n);
154149
- let o = import_node_path19.default.resolve(s3, t.name);
155000
+ let o = import_node_path20.default.resolve(s3, t.name);
154150
155001
  Ei(o, e, i, r);
154151
155002
  });
154152
155003
  else {
154153
- let n = import_node_path19.default.resolve(s3, t.name);
155004
+ let n = import_node_path20.default.resolve(s3, t.name);
154154
155005
  Ei(n, e, i, r);
154155
155006
  }
154156
155007
  };
@@ -154171,7 +155022,7 @@ var Es = (s3, t, e, i) => {
154171
155022
  });
154172
155023
  };
154173
155024
  var so = (s3, t, e, i) => {
154174
- t.isDirectory() && ws(import_node_path19.default.resolve(s3, t.name), e, i), ps(import_node_path19.default.resolve(s3, t.name), e, i);
155025
+ t.isDirectory() && ws(import_node_path20.default.resolve(s3, t.name), e, i), ps(import_node_path20.default.resolve(s3, t.name), e, i);
154175
155026
  };
154176
155027
  var ws = (s3, t, e) => {
154177
155028
  let i;
@@ -154221,12 +155072,12 @@ var gr = (s3, t, e) => {
154221
155072
  };
154222
155073
  if (s3 === d) return no(s3, S2);
154223
155074
  if (l) return import_promises18.default.mkdir(s3, { mode: r, recursive: true }).then((E) => S2(null, E ?? void 0), S2);
154224
- let D = f(import_node_path20.default.relative(d, s3)).split("/");
155075
+ let D = f(import_node_path21.default.relative(d, s3)).split("/");
154225
155076
  Ss(d, D, r, c, d, void 0, S2);
154226
155077
  };
154227
155078
  var Ss = (s3, t, e, i, r, n, o) => {
154228
155079
  if (t.length === 0) return o(null, n);
154229
- let h = t.shift(), a = f(import_node_path20.default.resolve(s3 + "/" + h));
155080
+ let h = t.shift(), a = f(import_node_path21.default.resolve(s3 + "/" + h));
154230
155081
  import_node_fs17.default.mkdir(a, e, br(a, t, e, i, r, n, o));
154231
155082
  };
154232
155083
  var br = (s3, t, e, i, r, n, o) => (h) => {
@@ -154260,9 +155111,9 @@ var _r = (s3, t) => {
154260
155111
  };
154261
155112
  if (s3 === c) return oo(c), d();
154262
155113
  if (a) return d(import_node_fs17.default.mkdirSync(s3, { mode: i, recursive: true }) ?? void 0);
154263
- let T2 = f(import_node_path20.default.relative(c, s3)).split("/"), D;
155114
+ let T2 = f(import_node_path21.default.relative(c, s3)).split("/"), D;
154264
155115
  for (let E = T2.shift(), x = c; E && (x += "/" + E); E = T2.shift()) {
154265
- x = f(import_node_path20.default.resolve(x));
155116
+ x = f(import_node_path21.default.resolve(x));
154266
155117
  try {
154267
155118
  import_node_fs17.default.mkdirSync(x, i), D = D || x;
154268
155119
  } catch {
@@ -154291,14 +155142,14 @@ var ho = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
154291
155142
  var ao = ho === "win32";
154292
155143
  var lo = (s3) => s3.split("/").slice(0, -1).reduce((e, i) => {
154293
155144
  let r = e.at(-1);
154294
- return r !== void 0 && (i = (0, import_node_path21.join)(r, i)), e.push(i || "/"), e;
155145
+ return r !== void 0 && (i = (0, import_node_path22.join)(r, i)), e.push(i || "/"), e;
154295
155146
  }, []);
154296
155147
  var yi = class {
154297
155148
  #t = /* @__PURE__ */ new Map();
154298
155149
  #i = /* @__PURE__ */ new Map();
154299
155150
  #s = /* @__PURE__ */ new Set();
154300
155151
  reserve(t, e) {
154301
- t = ao ? ["win32 parallelization disabled"] : t.map((r) => ut((0, import_node_path21.join)(Tr(r))));
155152
+ t = ao ? ["win32 parallelization disabled"] : t.map((r) => ut((0, import_node_path22.join)(Tr(r))));
154302
155153
  let i = new Set(t.map((r) => lo(r)).reduce((r, n) => r.concat(n)));
154303
155154
  this.#i.set(e, { dirs: i, paths: t });
154304
155155
  for (let r of t) {
@@ -154438,7 +155289,7 @@ var Xt = class extends rt {
154438
155289
  if (t.preserveOwner) throw new TypeError("cannot preserve owner in archive and also set owner explicitly");
154439
155290
  this.uid = t.uid, this.gid = t.gid, this.setOwner = true;
154440
155291
  } else this.uid = void 0, this.gid = void 0, this.setOwner = false;
154441
- this.preserveOwner = t.preserveOwner === void 0 && typeof t.uid != "number" ? process.getuid?.() === 0 : !!t.preserveOwner, this.processUid = (this.preserveOwner || this.setOwner) && process.getuid ? process.getuid() : void 0, this.processGid = (this.preserveOwner || this.setOwner) && process.getgid ? process.getgid() : void 0, this.maxDepth = typeof t.maxDepth == "number" ? t.maxDepth : mo, this.forceChown = t.forceChown === true, this.win32 = !!t.win32 || Te, this.newer = !!t.newer, this.keep = !!t.keep, this.noMtime = !!t.noMtime, this.preservePaths = !!t.preservePaths, this.unlink = !!t.unlink, this.cwd = f(import_node_path18.default.resolve(t.cwd || process.cwd())), this.strip = Number(t.strip) || 0, this.processUmask = this.chmod ? typeof t.processUmask == "number" ? t.processUmask : Lr() : 0, this.umask = typeof t.umask == "number" ? t.umask : this.processUmask, this.dmode = t.dmode || 511 & ~this.umask, this.fmode = t.fmode || 438 & ~this.umask, this.on("entry", (e) => this[Dr](e));
155292
+ this.preserveOwner = t.preserveOwner === void 0 && typeof t.uid != "number" ? process.getuid?.() === 0 : !!t.preserveOwner, this.processUid = (this.preserveOwner || this.setOwner) && process.getuid ? process.getuid() : void 0, this.processGid = (this.preserveOwner || this.setOwner) && process.getgid ? process.getgid() : void 0, this.maxDepth = typeof t.maxDepth == "number" ? t.maxDepth : mo, this.forceChown = t.forceChown === true, this.win32 = !!t.win32 || Te, this.newer = !!t.newer, this.keep = !!t.keep, this.noMtime = !!t.noMtime, this.preservePaths = !!t.preservePaths, this.unlink = !!t.unlink, this.cwd = f(import_node_path19.default.resolve(t.cwd || process.cwd())), this.strip = Number(t.strip) || 0, this.processUmask = this.chmod ? typeof t.processUmask == "number" ? t.processUmask : Lr() : 0, this.umask = typeof t.umask == "number" ? t.umask : this.processUmask, this.dmode = t.dmode || 511 & ~this.umask, this.fmode = t.fmode || 438 & ~this.umask, this.on("entry", (e) => this[Dr](e));
154442
155293
  }
154443
155294
  warn(t, e, i = {}) {
154444
155295
  return (t === "TAR_BAD_ARCHIVE" || t === "TAR_ABORT") && (i.recoverable = false), super.warn(t, e, i);
@@ -154452,7 +155303,7 @@ var Xt = class extends rt {
154452
155303
  let [n, o] = ce(i), h = o.replaceAll(/\\/g, "/").split("/");
154453
155304
  if (h.includes("..") || Te && /^[a-z]:\.\.$/i.test(h[0] ?? "")) {
154454
155305
  if (e === "path" || r === "Link") return this.warn("TAR_ENTRY_ERROR", `${e} contains '..'`, { entry: t, [e]: i }), false;
154455
- let a = import_node_path18.default.posix.dirname(t.path), l = import_node_path18.default.posix.normalize(import_node_path18.default.posix.join(a, h.join("/")));
155306
+ let a = import_node_path19.default.posix.dirname(t.path), l = import_node_path19.default.posix.normalize(import_node_path19.default.posix.join(a, h.join("/")));
154456
155307
  if (l.startsWith("../") || l === "..") return this.warn("TAR_ENTRY_ERROR", `${e} escapes extraction directory`, { entry: t, [e]: i }), false;
154457
155308
  }
154458
155309
  return n && (t[e] = String(o), this.warn("TAR_ENTRY_INFO", `stripping ${n} from absolute ${e}`, { entry: t, [e]: i })), true;
@@ -154470,12 +155321,12 @@ var Xt = class extends rt {
154470
155321
  }
154471
155322
  if (isFinite(this.maxDepth) && i.length > this.maxDepth) return this.warn("TAR_ENTRY_ERROR", "path excessively deep", { entry: t, path: e, depth: i.length, maxDepth: this.maxDepth }), false;
154472
155323
  if (!this[Rs](t, "path") || !this[Rs](t, "linkpath")) return false;
154473
- if (t.absolute = import_node_path18.default.isAbsolute(t.path) ? f(import_node_path18.default.resolve(t.path)) : f(import_node_path18.default.resolve(this.cwd, t.path)), !this.preservePaths && typeof t.absolute == "string" && t.absolute.indexOf(this.cwd + "/") !== 0 && t.absolute !== this.cwd) return this.warn("TAR_ENTRY_ERROR", "path escaped extraction target", { entry: t, path: f(t.path), resolvedPath: t.absolute, cwd: this.cwd }), false;
155324
+ if (t.absolute = import_node_path19.default.isAbsolute(t.path) ? f(import_node_path19.default.resolve(t.path)) : f(import_node_path19.default.resolve(this.cwd, t.path)), !this.preservePaths && typeof t.absolute == "string" && t.absolute.indexOf(this.cwd + "/") !== 0 && t.absolute !== this.cwd) return this.warn("TAR_ENTRY_ERROR", "path escaped extraction target", { entry: t, path: f(t.path), resolvedPath: t.absolute, cwd: this.cwd }), false;
154474
155325
  if (t.absolute === this.cwd && t.type !== "Directory" && t.type !== "GNUDumpDir") return false;
154475
155326
  if (this.win32) {
154476
- let { root: r } = import_node_path18.default.win32.parse(String(t.absolute));
155327
+ let { root: r } = import_node_path19.default.win32.parse(String(t.absolute));
154477
155328
  t.absolute = r + ts(String(t.absolute).slice(r.length));
154478
- let { root: n } = import_node_path18.default.win32.parse(t.path);
155329
+ let { root: n } = import_node_path19.default.win32.parse(t.path);
154479
155330
  t.path = n + ts(t.path.slice(n.length));
154480
155331
  }
154481
155332
  return true;
@@ -154563,13 +155414,13 @@ var Xt = class extends rt {
154563
155414
  t.unsupported = true, this.warn("TAR_ENTRY_UNSUPPORTED", `unsupported entry type: ${t.type}`, { entry: t }), t.resume();
154564
155415
  }
154565
155416
  [Ar](t, e) {
154566
- let i = f(import_node_path18.default.relative(this.cwd, import_node_path18.default.resolve(import_node_path18.default.dirname(String(t.absolute)), String(t.linkpath)))).split("/");
155417
+ let i = f(import_node_path19.default.relative(this.cwd, import_node_path19.default.resolve(import_node_path19.default.dirname(String(t.absolute)), String(t.linkpath)))).split("/");
154567
155418
  this[Re](t, this.cwd, i, () => this[gi](t, String(t.linkpath), "symlink", e), (r) => {
154568
155419
  this[O](r, t), e();
154569
155420
  });
154570
155421
  }
154571
155422
  [Ir](t, e) {
154572
- let i = f(import_node_path18.default.resolve(this.cwd, String(t.linkpath))), r = f(String(t.linkpath)).split("/");
155423
+ let i = f(import_node_path19.default.resolve(this.cwd, String(t.linkpath))), r = f(String(t.linkpath)).split("/");
154573
155424
  this[Re](t, this.cwd, r, () => this[gi](t, i, "link", e), (n) => {
154574
155425
  this[O](n, t), e();
154575
155426
  });
@@ -154577,10 +155428,10 @@ var Xt = class extends rt {
154577
155428
  [Re](t, e, i, r, n) {
154578
155429
  let o = i.shift();
154579
155430
  if (this.preservePaths || o === void 0) return r();
154580
- let h = import_node_path18.default.resolve(e, o);
155431
+ let h = import_node_path19.default.resolve(e, o);
154581
155432
  import_node_fs15.default.lstat(h, (a, l) => {
154582
155433
  if (a) return r();
154583
- if (l?.isSymbolicLink()) return n(new St(h, import_node_path18.default.resolve(h, i.join("/"))));
155434
+ if (l?.isSymbolicLink()) return n(new St(h, import_node_path19.default.resolve(h, i.join("/"))));
154584
155435
  this[Re](t, h, i, r, n);
154585
155436
  });
154586
155437
  }
@@ -154614,7 +155465,7 @@ var Xt = class extends rt {
154614
155465
  });
154615
155466
  }, n = () => {
154616
155467
  if (t.absolute !== this.cwd) {
154617
- let h = f(import_node_path18.default.dirname(String(t.absolute)));
155468
+ let h = f(import_node_path19.default.dirname(String(t.absolute)));
154618
155469
  if (h !== this.cwd) return this[yt](h, this.dmode, (a) => {
154619
155470
  if (a) {
154620
155471
  this[O](a, t), i();
@@ -154689,7 +155540,7 @@ var xe = class extends Xt {
154689
155540
  this[Oe] = true;
154690
155541
  }
154691
155542
  if (t.absolute !== this.cwd) {
154692
- let n = f(import_node_path18.default.dirname(String(t.absolute)));
155543
+ let n = f(import_node_path19.default.dirname(String(t.absolute)));
154693
155544
  if (n !== this.cwd) {
154694
155545
  let o = this[yt](n, this.dmode);
154695
155546
  if (o) return this[O](o, t);
@@ -154789,10 +155640,10 @@ var xe = class extends Xt {
154789
155640
  if (this.preservePaths || i.length === 0) return r();
154790
155641
  let o = e;
154791
155642
  for (let h of i) {
154792
- o = import_node_path18.default.resolve(o, h);
155643
+ o = import_node_path19.default.resolve(o, h);
154793
155644
  let [a, l] = ye(() => import_node_fs15.default.lstatSync(o));
154794
155645
  if (a) return r();
154795
- if (l.isSymbolicLink()) return n(new St(o, import_node_path18.default.resolve(e, i.join("/"))));
155646
+ if (l.isSymbolicLink()) return n(new St(o, import_node_path19.default.resolve(e, i.join("/"))));
154796
155647
  }
154797
155648
  r();
154798
155649
  }
@@ -154896,11 +155747,11 @@ var go = (s3, t) => {
154896
155747
  };
154897
155748
  var bo = (s3, t) => {
154898
155749
  t.forEach((e) => {
154899
- e.charAt(0) === "@" ? Ct({ file: import_node_path22.default.resolve(s3.cwd, e.slice(1)), sync: true, noResume: true, onReadEntry: (i) => s3.add(i) }) : s3.add(e);
155750
+ e.charAt(0) === "@" ? Ct({ file: import_node_path23.default.resolve(s3.cwd, e.slice(1)), sync: true, noResume: true, onReadEntry: (i) => s3.add(i) }) : s3.add(e);
154900
155751
  }), s3.end();
154901
155752
  };
154902
155753
  var _o = async (s3, t) => {
154903
- for (let e of t) e.charAt(0) === "@" ? await Ct({ file: import_node_path22.default.resolve(String(s3.cwd), e.slice(1)), noResume: true, onReadEntry: (i) => s3.add(i) }) : s3.add(e);
155754
+ for (let e of t) e.charAt(0) === "@" ? await Ct({ file: import_node_path23.default.resolve(String(s3.cwd), e.slice(1)), noResume: true, onReadEntry: (i) => s3.add(i) }) : s3.add(e);
154904
155755
  s3.end();
154905
155756
  };
154906
155757
  var vt = K(yo, go, () => {
@@ -154930,8 +155781,8 @@ async function unpackTarball(opts) {
154930
155781
 
154931
155782
  // src/updater/update-log.ts
154932
155783
  var import_node_fs20 = require("fs");
154933
- var import_node_path23 = require("path");
154934
- var LOG_PATH = (0, import_node_path23.join)(THREADBASE_ROOT, "update.log");
155784
+ var import_node_path24 = require("path");
155785
+ var LOG_PATH = (0, import_node_path24.join)(THREADBASE_ROOT, "update.log");
154935
155786
  function appendUpdateLog(line) {
154936
155787
  try {
154937
155788
  (0, import_node_fs20.mkdirSync)(THREADBASE_ROOT, { recursive: true });
@@ -155046,15 +155897,19 @@ async function runInstall(opts) {
155046
155897
  }
155047
155898
 
155048
155899
  // cli/prod.ts
155049
- var import_node_child_process7 = require("child_process");
155900
+ var import_node_child_process8 = require("child_process");
155050
155901
  var import_node_fs23 = require("fs");
155902
+ init_auth();
155903
+ init_feature_flags();
155051
155904
  init_conflict_check();
155052
155905
  init_constants();
155053
155906
  init_launchd();
155054
155907
  init_marker();
155055
155908
  init_platform2();
155056
155909
  init_logger();
155057
- var log8 = getLogger("prod");
155910
+ init_protocol();
155911
+ init_socket();
155912
+ var log9 = getLogger("prod");
155058
155913
  function clearSupervisorLogs() {
155059
155914
  let paths;
155060
155915
  try {
@@ -155103,9 +155958,10 @@ async function runProdStatus() {
155103
155958
  marker: readMarker()
155104
155959
  };
155105
155960
  }
155106
- async function runProdDoctor(opts) {
155961
+ async function runProdDoctor(opts, deps = {}) {
155107
155962
  const findings = [];
155108
155963
  const repairs = [];
155964
+ let ptyHost;
155109
155965
  const marker = readMarker();
155110
155966
  if (marker && !marker.userHeld && !isPidAlive(marker.devPid)) {
155111
155967
  findings.push(`stale marker (dev pid ${marker.devPid} dead)`);
@@ -155129,7 +155985,23 @@ async function runProdDoctor(opts) {
155129
155985
  );
155130
155986
  }
155131
155987
  }
155132
- return { findings, repairs };
155988
+ const featureFlags = deps.featureFlags ?? resolveFeatureFlags({ yaml: loadFeatureFlags() });
155989
+ if (featureFlags.ptyHost) {
155990
+ const probe = deps.probePtyHost ?? (() => probePtyHostStatus(hostSocketPath(getInstanceId())));
155991
+ try {
155992
+ ptyHost = await probe();
155993
+ } catch (err) {
155994
+ ptyHost = { reachable: false, error: err instanceof Error ? err.message : String(err) };
155995
+ }
155996
+ if (!ptyHost.reachable) {
155997
+ findings.push(`pty-host is not reachable: ${ptyHost.error}`);
155998
+ } else if (ptyHost.protocolVersion !== PTY_HOST_PROTOCOL_VERSION) {
155999
+ findings.push(
156000
+ `pty-host protocol ${ptyHost.protocolVersion} does not match streamer protocol ${PTY_HOST_PROTOCOL_VERSION}`
156001
+ );
156002
+ }
156003
+ }
156004
+ return { findings, repairs, ...ptyHost ? { ptyHost } : {} };
155133
156005
  }
155134
156006
  var defaultSpawnTail = ({ files, lines, follow }) => {
155135
156007
  const existing = files.filter((f2) => (0, import_node_fs23.existsSync)(f2));
@@ -155143,7 +156015,7 @@ var defaultSpawnTail = ({ files, lines, follow }) => {
155143
156015
  if (follow) args.push("-F");
155144
156016
  args.push(...existing);
155145
156017
  return new Promise((resolve4) => {
155146
- const child = (0, import_node_child_process7.spawn)("tail", args, { stdio: ["ignore", "inherit", "inherit"] });
156018
+ const child = (0, import_node_child_process8.spawn)("tail", args, { stdio: ["ignore", "inherit", "inherit"] });
155147
156019
  child.on("error", (err) => resolve4({ ok: false, message: `tail failed: ${err.message}` }));
155148
156020
  child.on("exit", (code) => resolve4({ ok: code === 0 }));
155149
156021
  });
@@ -155211,12 +156083,12 @@ function registerProdCommands(program3) {
155211
156083
  prod.command("start").description("Restore prod after a user-held suspension").option("--clear-logs", "Clear existing stdout + stderr logs", false).action(async (opts) => {
155212
156084
  if (opts.clearLogs === true) clearSupervisorLogs();
155213
156085
  const r = await runProdStart();
155214
- log8.info(r.message, void 0, "console");
156086
+ log9.info(r.message, void 0, "console");
155215
156087
  if (!r.ok) process.exitCode = 1;
155216
156088
  });
155217
156089
  prod.command("stop").description("Unload the launchd agent (prod will not auto-restart)").action(async () => {
155218
156090
  const r = await runProdStop();
155219
- log8.info(r.message, void 0, "console");
156091
+ log9.info(r.message, void 0, "console");
155220
156092
  });
155221
156093
  prod.command("status").description("Report whether prod is supervised, suspended, or down").action(async () => {
155222
156094
  const s3 = await runProdStatus();
@@ -155225,7 +156097,7 @@ function registerProdCommands(program3) {
155225
156097
  `pid: ${s3.agentPid ?? "(none)"}`,
155226
156098
  s3.marker ? `marker: ${s3.marker.userHeld ? "userHeld (intentional stop)" : "dev-suspended"}, devPid=${s3.marker.devPid}, port=${s3.marker.port}, repo=${s3.marker.repoToplevel}` : "marker: none"
155227
156099
  ];
155228
- log8.info(parts.join("\n "), void 0, "console");
156100
+ log9.info(parts.join("\n "), void 0, "console");
155229
156101
  });
155230
156102
  prod.command("restart").description("Stop + restart the supervised streamer (re-reads service definition)").option("--clear-logs", "Clear existing stdout + stderr logs", false).action(async (opts) => {
155231
156103
  const sup = getSupervisor();
@@ -155234,17 +156106,24 @@ function registerProdCommands(program3) {
155234
156106
  sup.bootoutAgent();
155235
156107
  sup.bootstrapAgent(specPath, { afterBootout: true });
155236
156108
  const what = process.platform === "darwin" ? `agent restarted from ${specPath}` : `task '${TASK_NAME}' restarted`;
155237
- log8.info(what, void 0, "console");
156109
+ log9.info(what, void 0, "console");
155238
156110
  });
155239
- prod.command("doctor").description("Detect & repair stale markers, missing agent, plist drift").option("--fix", "Apply repairs (default is dry-run)", false).action(async (opts) => {
156111
+ prod.command("doctor").description("Detect stale markers, missing agents, and pty-host health").option("--fix", "Apply repairs (default is dry-run)", false).action(async (opts) => {
155240
156112
  const r = await runProdDoctor({ fix: opts.fix === true });
155241
- log8.info(`findings: ${r.findings.length === 0 ? "(none)" : ""}`, void 0, "console");
155242
- for (const f2 of r.findings) log8.info(` - ${f2}`, void 0, "console");
156113
+ if (r.ptyHost) {
156114
+ log9.info(
156115
+ r.ptyHost.reachable ? `pty-host: reachable, protocol=${r.ptyHost.protocolVersion}, sessions=${r.ptyHost.sessionCount}` : `pty-host: unreachable (${r.ptyHost.error})`,
156116
+ void 0,
156117
+ "console"
156118
+ );
156119
+ }
156120
+ log9.info(`findings: ${r.findings.length === 0 ? "(none)" : ""}`, void 0, "console");
156121
+ for (const f2 of r.findings) log9.info(` - ${f2}`, void 0, "console");
155243
156122
  if (r.repairs.length) {
155244
- log8.info(`repairs:`, void 0, "console");
155245
- for (const fix of r.repairs) log8.info(` - ${fix}`, void 0, "console");
156123
+ log9.info(`repairs:`, void 0, "console");
156124
+ for (const fix of r.repairs) log9.info(` - ${fix}`, void 0, "console");
155246
156125
  } else if (!opts.fix && r.findings.length > 0) {
155247
- log8.info(`(re-run with --fix to apply repairs)`, void 0, "console");
156126
+ log9.info(`(re-run with --fix to apply repairs)`, void 0, "console");
155248
156127
  }
155249
156128
  });
155250
156129
  prod.command("logs").description("Tail the supervised streamer's stdout + stderr log files").option("-n, --lines <count>", "Seed with the last N lines (default 50)", "50").option("--no-follow", "Print last N lines and exit (do not follow)").option("--errors-only", "Tail only stderr", false).option("--clear", "Truncate stdout + stderr logs in place, then exit", false).action(async (opts) => {
@@ -155255,14 +156134,14 @@ function registerProdCommands(program3) {
155255
156134
  errorsOnly: opts.errorsOnly === true,
155256
156135
  clear: opts.clear === true
155257
156136
  });
155258
- if (r.message) log8.info(r.message, void 0, "console");
156137
+ if (r.message) log9.info(r.message, void 0, "console");
155259
156138
  if (!r.ok) process.exitCode = 1;
155260
156139
  });
155261
156140
  program3.addCommand(prod);
155262
156141
  }
155263
156142
 
155264
156143
  // cli/index.ts
155265
- var log11 = getLogger("cli");
156144
+ var log12 = getLogger("cli");
155266
156145
  var program2 = new Command();
155267
156146
  program2.name("threadbase-streamer").description("PTY session management, WebSocket streaming, and REST API server for Claude Code").version(getVersion());
155268
156147
  program2.command("serve").description("Start the streamer server").option("-p, --port <number>", "Port to listen on", "8766").option("--api-key <key>", "API key for authentication").option("--local-no-auth", "Skip auth for localhost requests", false).option("-v, --verbose", "Verbose output", false).option("--log-menubar-requests", "Log /healthz requests from the menubar app", false).option("--browse-root <path>", "Root directory for file browsing").option(
@@ -155304,14 +156183,14 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155304
156183
  const { checkSqliteAbi: checkSqliteAbi2 } = await Promise.resolve().then(() => (init_check_sqlite_abi(), check_sqlite_abi_exports));
155305
156184
  checkSqliteAbi2();
155306
156185
  } catch (err) {
155307
- log11.error(err instanceof Error ? err.message : String(err), void 0, "console");
156186
+ log12.error(err instanceof Error ? err.message : String(err), void 0, "console");
155308
156187
  process.exit(1);
155309
156188
  }
155310
156189
  if (opts.multiAgentFlow) {
155311
156190
  process.env.MULTI_AGENT_FLOW = "true";
155312
156191
  }
155313
156192
  if (opts.defaultPermissionMode !== void 0 && !isPermissionMode(opts.defaultPermissionMode)) {
155314
- log11.error(
156193
+ log12.error(
155315
156194
  `Invalid --default-permission-mode: ${opts.defaultPermissionMode} (expected one of ${PERMISSION_MODES.join(", ")})`,
155316
156195
  void 0,
155317
156196
  "console"
@@ -155327,7 +156206,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155327
156206
  const value = eq === -1 ? true : entry.slice(eq + 1);
155328
156207
  const def = findFlag(id);
155329
156208
  if (!def) {
155330
- log11.error(
156209
+ log12.error(
155331
156210
  `Invalid --claude-flag id: ${id} (expected one of ${CLAUDE_FLAGS.map((f2) => f2.id).join(", ")})`,
155332
156211
  void 0,
155333
156212
  "console"
@@ -155344,7 +156223,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155344
156223
  claudeFlags = validateFlagValues(raw2);
155345
156224
  for (const id of Object.keys(raw2)) {
155346
156225
  if (!(id in claudeFlags)) {
155347
- log11.error(`Invalid value for --claude-flag ${id}`, void 0, "console");
156226
+ log12.error(`Invalid value for --claude-flag ${id}`, void 0, "console");
155348
156227
  process.exit(1);
155349
156228
  }
155350
156229
  }
@@ -155353,13 +156232,13 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155353
156232
  if (Array.isArray(opts.feature) && opts.feature.length > 0) {
155354
156233
  const parsed = parseFeatureFlagArgs(opts.feature);
155355
156234
  if (parsed.errors.length > 0) {
155356
- for (const error51 of parsed.errors) log11.error(`--feature: ${error51}`, void 0, "console");
156235
+ for (const error51 of parsed.errors) log12.error(`--feature: ${error51}`, void 0, "console");
155357
156236
  process.exit(1);
155358
156237
  }
155359
156238
  featureFlags = parsed.values;
155360
156239
  }
155361
156240
  if (opts.defaultEffort !== void 0 && !isEffortLevel(opts.defaultEffort)) {
155362
- log11.error(
156241
+ log12.error(
155363
156242
  `Invalid --default-effort: ${opts.defaultEffort} (expected one of ${EFFORT_LEVELS.join(", ")})`,
155364
156243
  void 0,
155365
156244
  "console"
@@ -155370,7 +156249,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155370
156249
  if (opts.ptyGracePeriodMs !== void 0) {
155371
156250
  const parsed = Number(opts.ptyGracePeriodMs);
155372
156251
  if (!Number.isInteger(parsed) || parsed < 0) {
155373
- log11.error(
156252
+ log12.error(
155374
156253
  `Invalid --pty-grace-period-ms: ${opts.ptyGracePeriodMs} (expected a non-negative integer; 0 disables auto-hold)`,
155375
156254
  void 0,
155376
156255
  "console"
@@ -155386,11 +156265,17 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155386
156265
  const publicUrl = opts.publicUrl ?? loadPublicUrl() ?? null;
155387
156266
  const isProdInvocation = opts.prod === true || process.ppid === 1;
155388
156267
  if (!isProdInvocation) appendDevSessionMarker();
156268
+ if (isProdInvocation) {
156269
+ const { truncateOversizedLogs: truncateOversizedLogs2 } = await Promise.resolve().then(() => (init_log_cap(), log_cap_exports));
156270
+ for (const f2 of truncateOversizedLogs2()) {
156271
+ log12.info(`truncated oversized log at boot: ${f2}`, { path: f2, event: "log.truncated" });
156272
+ }
156273
+ }
155389
156274
  if (process.platform === "darwin") {
155390
156275
  const { detectConflictingAgents: detectConflictingAgents2, formatConflictMessage: formatConflictMessage2 } = await Promise.resolve().then(() => (init_conflict_check(), conflict_check_exports));
155391
156276
  const conflicts = detectConflictingAgents2();
155392
156277
  if (conflicts.length > 0) {
155393
- log11.warn(formatConflictMessage2(conflicts), void 0, "console");
156278
+ log12.warn(formatConflictMessage2(conflicts), void 0, "console");
155394
156279
  }
155395
156280
  }
155396
156281
  let resolvedDefaultPermissionMode = opts.defaultPermissionMode;
@@ -155405,7 +156290,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155405
156290
  resolvedAutoResumeOnBoot = await interactiveAutoResumePrompt2();
155406
156291
  setAutoResumeOnBoot(resolvedAutoResumeOnBoot);
155407
156292
  } else if (resolvedAutoResumeOnBoot === void 0) {
155408
- log11.info(
156293
+ log12.info(
155409
156294
  "auto_resume_on_boot is not set; interrupted sessions will wait for you to resume them.\nSet `auto_resume_on_boot: true` in ~/.threadbase/server.yaml to resume them automatically.",
155410
156295
  void 0,
155411
156296
  "console"
@@ -155455,15 +156340,15 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155455
156340
  await server.listen(resolvedPort);
155456
156341
  {
155457
156342
  const v2 = getVersion();
155458
- log11.info(`Threadbase Streamer v${v2}`, { version: v2, port: resolvedPort });
156343
+ log12.info(`Threadbase Streamer v${v2}`, { version: v2, port: resolvedPort });
155459
156344
  }
155460
- log11.info(`Listening on http://localhost:${resolvedPort}`, {
156345
+ log12.info(`Listening on http://localhost:${resolvedPort}`, {
155461
156346
  url: `http://localhost:${resolvedPort}`
155462
156347
  });
155463
- log11.info(`WebSocket at ws://localhost:${resolvedPort}/ws`, {
156348
+ log12.info(`WebSocket at ws://localhost:${resolvedPort}/ws`, {
155464
156349
  wsUrl: `ws://localhost:${resolvedPort}/ws`
155465
156350
  });
155466
- log11.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
156351
+ log12.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
155467
156352
  try {
155468
156353
  await printServerBanner({
155469
156354
  port: resolvedPort,
@@ -155473,15 +156358,15 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155473
156358
  });
155474
156359
  } catch (err) {
155475
156360
  const message = err instanceof Error ? err.message : String(err);
155476
- log11.warn(`(skipped pairing QR: ${message})`, { reason: message });
155477
- log11.info(
156361
+ log12.warn(`(skipped pairing QR: ${message})`, { reason: message });
156362
+ log12.info(
155478
156363
  printUrlBanner({ url: resolveServerUrl({ publicUrl, port: resolvedPort }) }),
155479
156364
  void 0,
155480
156365
  "console"
155481
156366
  );
155482
156367
  }
155483
156368
  const shutdown = async () => {
155484
- log11.info("Shutting down...");
156369
+ log12.info("Shutting down...");
155485
156370
  await server.close();
155486
156371
  process.exit(0);
155487
156372
  };
@@ -155489,7 +156374,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155489
156374
  process.on("SIGINT", shutdown);
155490
156375
  process.on("SIGTERM", shutdown);
155491
156376
  process.on("uncaughtException", (err) => {
155492
- log11.error(`uncaught: ${err.message}`, {
156377
+ log12.error(`uncaught: ${err.message}`, {
155493
156378
  error: err.message,
155494
156379
  stack: err.stack,
155495
156380
  event: "process.uncaught"
@@ -155498,7 +156383,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155498
156383
  });
155499
156384
  process.on("unhandledRejection", (reason) => {
155500
156385
  const msg = reason instanceof Error ? reason.message : String(reason);
155501
- log11.error(`unhandled rejection: ${msg}`, {
156386
+ log12.error(`unhandled rejection: ${msg}`, {
155502
156387
  error: msg,
155503
156388
  event: "process.unhandled_rejection"
155504
156389
  });
@@ -155523,10 +156408,10 @@ program2.command("cache").description("Manage the local SQLite conversation cach
155523
156408
  const f2 = dbPath + suffix;
155524
156409
  if (existsSync16(f2)) {
155525
156410
  rmSync6(f2);
155526
- log11.info(`Deleted ${f2}`, { path: f2 }, "console");
156411
+ log12.info(`Deleted ${f2}`, { path: f2 }, "console");
155527
156412
  }
155528
156413
  }
155529
- log11.info("Cache cleared. Restart the server to rebuild.", void 0, "console");
156414
+ log12.info("Cache cleared. Restart the server to rebuild.", void 0, "console");
155530
156415
  })
155531
156416
  );
155532
156417
  program2.command("pair").description("Print a pairing QR code (server must already be running)").option("-p, --port <number>", "Port the server is listening on", "8766").action(async (opts) => {
@@ -155551,7 +156436,7 @@ program2.command("set-key [key]").description("Set the streamer API key in ~/.th
155551
156436
  program2.command("update").description("Check for streamer updates from GitHub Releases and install them").option("--check", "Check only; do not install", false).option("--version <version>", "Pin to a specific release tag").option("--allow-major", "Allow a major-version bump", false).option("--force", "Skip the active-session defer check", false).option("--dry-run", "Print what would be installed without writing to disk", false).option("-p, --port <number>", "Port of the running streamer for active-session check", "8766").action(async (opts) => {
155552
156437
  const cfg = loadUpdateConfig();
155553
156438
  if (!cfg) {
155554
- log11.warn(
156439
+ log12.warn(
155555
156440
  `No update config found at ${DEFAULT_CONFIG_PATH}. Create one with at least 'github_repo: owner/name' to enable updates.`,
155556
156441
  void 0,
155557
156442
  "console"
@@ -155570,11 +156455,11 @@ program2.command("update").description("Check for streamer updates from GitHub R
155570
156455
  appendUpdateLog(
155571
156456
  `[check] current=${result2.current} latest=${result2.latest ?? "none"} status=${result2.reason}`
155572
156457
  );
155573
- log11.info(`Current : ${result2.current}`, void 0, "console");
155574
- log11.info(`Latest : ${result2.latest ?? "(none)"}`, void 0, "console");
155575
- log11.info(`Channel : ${cfg.channel}`, void 0, "console");
155576
- log11.info(`Diff : ${result2.diff ?? "(none)"}`, void 0, "console");
155577
- log11.info(`Status : ${result2.reason}`, void 0, "console");
156458
+ log12.info(`Current : ${result2.current}`, void 0, "console");
156459
+ log12.info(`Latest : ${result2.latest ?? "(none)"}`, void 0, "console");
156460
+ log12.info(`Channel : ${cfg.channel}`, void 0, "console");
156461
+ log12.info(`Diff : ${result2.diff ?? "(none)"}`, void 0, "console");
156462
+ log12.info(`Status : ${result2.reason}`, void 0, "console");
155578
156463
  return;
155579
156464
  }
155580
156465
  const port = Number.parseInt(opts.port, 10);
@@ -155590,20 +156475,20 @@ program2.command("update").description("Check for streamer updates from GitHub R
155590
156475
  });
155591
156476
  switch (result.kind) {
155592
156477
  case "no-op":
155593
- log11.info(`Current : ${result.current}`, void 0, "console");
155594
- log11.info(`Latest : ${result.latest ?? "(none)"}`, void 0, "console");
155595
- log11.info(`Status : ${result.reason}`, void 0, "console");
156478
+ log12.info(`Current : ${result.current}`, void 0, "console");
156479
+ log12.info(`Latest : ${result.latest ?? "(none)"}`, void 0, "console");
156480
+ log12.info(`Status : ${result.reason}`, void 0, "console");
155596
156481
  break;
155597
156482
  case "unsupported-install":
155598
- log11.warn(result.reason, void 0, "console");
156483
+ log12.warn(result.reason, void 0, "console");
155599
156484
  process.exitCode = 2;
155600
156485
  break;
155601
156486
  case "deferred":
155602
- log11.warn(`Deferred: ${result.reason}`, void 0, "console");
156487
+ log12.warn(`Deferred: ${result.reason}`, void 0, "console");
155603
156488
  process.exitCode = 2;
155604
156489
  break;
155605
156490
  case "dry-run":
155606
- log11.info(
156491
+ log12.info(
155607
156492
  `Would install ${result.latest} from ${result.tarballUrl}`,
155608
156493
  void 0,
155609
156494
  "console"
@@ -155611,28 +156496,28 @@ program2.command("update").description("Check for streamer updates from GitHub R
155611
156496
  break;
155612
156497
  case "installed":
155613
156498
  if (result.restart.method.startsWith("failed:")) {
155614
- log11.error(
156499
+ log12.error(
155615
156500
  `Installed ${result.installed} on disk, but the running service was not updated. Restart: ${result.restart.method}.`,
155616
156501
  void 0,
155617
156502
  "console"
155618
156503
  );
155619
156504
  process.exitCode = 1;
155620
156505
  } else {
155621
- log11.info(
156506
+ log12.info(
155622
156507
  `Installed ${result.installed} (was ${result.previous}). Restart: ${result.restart.method}.`,
155623
156508
  void 0,
155624
156509
  "console"
155625
156510
  );
155626
156511
  }
155627
156512
  if (result.pruned.length > 0) {
155628
- log11.info(`Pruned old releases: ${result.pruned.join(", ")}`, void 0, "console");
156513
+ log12.info(`Pruned old releases: ${result.pruned.join(", ")}`, void 0, "console");
155629
156514
  }
155630
156515
  break;
155631
156516
  }
155632
156517
  } catch (err) {
155633
156518
  const message = err instanceof Error ? err.message : String(err);
155634
156519
  appendUpdateLog(`[error] ${message}`);
155635
- log11.error(`Update failed: ${message}`, { error: message }, "console");
156520
+ log12.error(`Update failed: ${message}`, { error: message }, "console");
155636
156521
  process.exitCode = 1;
155637
156522
  }
155638
156523
  });
@@ -155644,21 +156529,32 @@ program2.command("pty-host").description(
155644
156529
  ).action(async (opts) => {
155645
156530
  const { SessionHost: SessionHost2 } = await Promise.resolve().then(() => (init_host(), host_exports));
155646
156531
  const { listenForStreamers: listenForStreamers2 } = await Promise.resolve().then(() => (init_socket(), socket_exports));
155647
- const host = new SessionHost2({ logger: getLogger("pty-host") });
155648
156532
  let server;
156533
+ let host;
156534
+ host = new SessionHost2({
156535
+ logger: getLogger("pty-host"),
156536
+ onShutdown: () => {
156537
+ host.dispose();
156538
+ server.close(() => process.exit(0));
156539
+ },
156540
+ onOrphaned: () => {
156541
+ host.dispose();
156542
+ server.close(() => process.exit(0));
156543
+ }
156544
+ });
155649
156545
  try {
155650
156546
  server = await listenForStreamers2(opts.socket, {
155651
156547
  onConnection: (transport) => host.accept(transport)
155652
156548
  });
155653
156549
  } catch (err) {
155654
- log11.error(
156550
+ log12.error(
155655
156551
  `pty-host could not listen on ${opts.socket}: ${err instanceof Error ? err.message : String(err)}`,
155656
156552
  void 0,
155657
156553
  "console"
155658
156554
  );
155659
156555
  process.exit(1);
155660
156556
  }
155661
- log11.info(`pty-host listening on ${opts.socket}`, {
156557
+ log12.info(`pty-host listening on ${opts.socket}`, {
155662
156558
  event: "pty_host.listening",
155663
156559
  socket: opts.socket
155664
156560
  });
@@ -155712,7 +156608,7 @@ async function printServerBanner({
155712
156608
  }) {
155713
156609
  const url2 = resolveServerUrl({ publicUrl, port });
155714
156610
  if (!includeQr) {
155715
- log11.info(printUrlBanner({ url: url2 }), void 0, "console");
156611
+ log12.info(printUrlBanner({ url: url2 }), void 0, "console");
155716
156612
  return;
155717
156613
  }
155718
156614
  const res = await fetch(`http://localhost:${port}/api/pair/start`, {
@@ -155727,9 +156623,9 @@ async function printServerBanner({
155727
156623
  const expSeconds = Math.floor(expiresAt / 1e3);
155728
156624
  const payload = `threadbase://pair?url=${encodeURIComponent(url2)}&token=${token}&exp=${expSeconds}`;
155729
156625
  const qr2 = await generateQr(payload);
155730
- log11.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
155731
- log11.info(`Pair URL: ${payload}`, void 0, "console");
155732
- log11.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
156626
+ log12.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
156627
+ log12.info(`Pair URL: ${payload}`, void 0, "console");
156628
+ log12.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
155733
156629
  }
155734
156630
  /*! Bundled license information:
155735
156631