@threadbase-sh/streamer 1.42.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,10 +115335,187 @@ 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
+ }
114885
115341
  };
114886
115342
  }
114887
115343
  });
114888
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);
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();
115516
+ }
115517
+ });
115518
+
114889
115519
  // node_modules/tweetnacl/nacl-fast.js
114890
115520
  var require_nacl_fast = __commonJS({
114891
115521
  "node_modules/tweetnacl/nacl-fast.js"(exports2, module2) {
@@ -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,
@@ -146439,6 +147185,34 @@ function paginate(results, offset, limit) {
146439
147185
  };
146440
147186
  }
146441
147187
 
147188
+ // src/services/sessions/autoResumeOnBoot.ts
147189
+ var AUTO_RESUME_WINDOW_MS = 15 * 60 * 1e3;
147190
+ var AUTO_RESUME_MAX = 5;
147191
+ var AUTO_RESUME_CONCURRENCY = 2;
147192
+ var AUTO_RESUME_STAGGER_MS = 500;
147193
+ function autoResumeSkipReason(row, opts) {
147194
+ if (row.status_source !== "shutdown") return "not_shutdown";
147195
+ if (row.status !== "running" && row.status !== "waiting_input") return "not_interrupted";
147196
+ if (opts.now - row.status_updated_at > AUTO_RESUME_WINDOW_MS) return "too_old";
147197
+ if (!opts.projectExists(row.project_path)) return "project_missing";
147198
+ if (resumeIdForRow(row) == null) return "resume_identity_missing";
147199
+ return null;
147200
+ }
147201
+ function planAutoResume(rows, opts) {
147202
+ const eligible = [];
147203
+ const skipped = [];
147204
+ for (const row of rows) {
147205
+ const reason = autoResumeSkipReason(row, opts);
147206
+ if (reason) skipped.push({ row, reason });
147207
+ else eligible.push(row);
147208
+ }
147209
+ return {
147210
+ attempts: eligible.slice(0, AUTO_RESUME_MAX),
147211
+ skipped,
147212
+ overflow: eligible.slice(AUTO_RESUME_MAX)
147213
+ };
147214
+ }
147215
+
146442
147216
  // src/services/sessions/conversationBusy.ts
146443
147217
  var import_fs29 = require("fs");
146444
147218
  var RESUME_BUSY_WINDOW_MS = 12e4;
@@ -146655,6 +147429,12 @@ var SessionStore = class {
146655
147429
  removeManaged(sessionId) {
146656
147430
  return this.managed.delete(sessionId);
146657
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
+ */
146658
147438
  getManaged(sessionId) {
146659
147439
  return this.managed.get(sessionId) ?? null;
146660
147440
  }
@@ -146664,12 +147444,21 @@ var SessionStore = class {
146664
147444
  this.discovered.set(proc.pid, proc);
146665
147445
  }
146666
147446
  }
147447
+ /**
147448
+ * The **live** stored records — mutating an element mutates the store. Paired
147449
+ * with `list()`, which hands back throwaway response copies.
147450
+ */
146667
147451
  listManaged() {
146668
147452
  return Array.from(this.managed.values());
146669
147453
  }
146670
147454
  // Build the session list: live PTY sessions (managed) merged with externally
146671
147455
  // discovered Claude processes. Managed sessions keyed by JSONL UUID take
146672
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.
146673
147462
  list(ptyAttachedIds) {
146674
147463
  const results = [];
146675
147464
  const seenIds = /* @__PURE__ */ new Set();
@@ -146685,6 +147474,9 @@ var SessionStore = class {
146685
147474
  }
146686
147475
  return results;
146687
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.
146688
147480
  get(sessionId, ptyAttachedIds) {
146689
147481
  const managed = this.managed.get(sessionId);
146690
147482
  if (managed) return managedToResponse(managed, ptyAttachedIds.has(sessionId));
@@ -146776,8 +147568,8 @@ function managedToResponse(s3, ptyAttached) {
146776
147568
  // behind it — `resumable`, and `historical` rather than `managed`
146777
147569
  // (docs/plans/live-sessions-persistence-plan.md §4, Phase 1).
146778
147570
  lifecycle: ptyAttached ? "attached" : s3.rehydrated ? "resumable" : s3.failureReason != null ? "failed" : "completed",
146779
- lifecycleSource: ptyAttached ? "spawn" : s3.rehydrated ? "reconcile" : "exit",
146780
- // 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
146781
147573
  // `activity` is attached for managed sessions.
146782
147574
  ownership: s3.rehydrated ? "historical" : "managed",
146783
147575
  projectPath: s3.projectPath,
@@ -147312,6 +148104,7 @@ var StreamerServer = class {
147312
148104
  disableDb = false;
147313
148105
  // Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
147314
148106
  skipStartupWarmup;
148107
+ autoResumeOnBoot;
147315
148108
  browseRoot = null;
147316
148109
  publicUrl = null;
147317
148110
  browserCors;
@@ -147428,6 +148221,7 @@ var StreamerServer = class {
147428
148221
  this.verbose = config2.verbose ?? false;
147429
148222
  this.disableDb = config2.disableDb ?? false;
147430
148223
  this.skipStartupWarmup = config2.skipStartupWarmup ?? false;
148224
+ this.autoResumeOnBoot = config2.autoResumeOnBoot ?? false;
147431
148225
  this.scannerPersistenceDisabled = config2.scannerPersistent === false;
147432
148226
  this.scanProfiles = config2.scanProfiles;
147433
148227
  this.codexRoots = config2.codexRoots ?? [(0, import_path29.join)((0, import_os13.homedir)(), ".codex", "sessions")];
@@ -147908,16 +148702,19 @@ var StreamerServer = class {
147908
148702
  broadcastOrUnicastSessionList(req) {
147909
148703
  const clientId = req.headers["x-client-id"];
147910
148704
  const ws2 = typeof clientId === "string" ? this.clientIdToWs.get(clientId) : void 0;
147911
- const payload = {
147912
- type: "session_list",
147913
- sessions: this.withReconciledLifecycle(this.sessionStore.list(this.ptyAttachedIds()))
147914
- };
148705
+ const payload = this.sessionListPayload();
147915
148706
  if (ws2) {
147916
148707
  this.wsHub.unicast(ws2, payload);
147917
148708
  } else {
147918
148709
  this.wsHub.broadcast(payload);
147919
148710
  }
147920
148711
  }
148712
+ sessionListPayload() {
148713
+ return {
148714
+ type: "session_list",
148715
+ sessions: this.withReconciledLifecycle(this.sessionStore.list(this.ptyAttachedIds()))
148716
+ };
148717
+ }
147921
148718
  /**
147922
148719
  * Overlay boot-reconciliation verdicts onto session responses.
147923
148720
  *
@@ -148005,7 +148802,7 @@ var StreamerServer = class {
148005
148802
  if (!this.managedSessionsRepo) return [];
148006
148803
  let verdicts = [];
148007
148804
  try {
148008
- const rows = this.managedSessionsRepo.listNonTerminal();
148805
+ const rows = this.managedSessionsRepo.listNonTerminal().filter((row) => !this.ptyManager.hasSession(row.session_id));
148009
148806
  if (rows.length === PROBE_SET_MAX) {
148010
148807
  this.log.warn(
148011
148808
  `[reconcile] probe set hit its cap of ${PROBE_SET_MAX} \u2014 older rows skipped`,
@@ -148091,7 +148888,7 @@ var StreamerServer = class {
148091
148888
  * by id rather than duplicating it.
148092
148889
  */
148093
148890
  rehydratePreviousSessions(verdicts) {
148094
- if (!this.featureFlags.sessionRehydration || !this.managedSessionsRepo) return;
148891
+ if (!this.managedSessionsRepo) return [];
148095
148892
  try {
148096
148893
  const now = Date.now();
148097
148894
  const rows = this.managedSessionsRepo.listRecoverable({
@@ -148100,7 +148897,7 @@ var StreamerServer = class {
148100
148897
  });
148101
148898
  const truncated = rows.length > REHYDRATE_MAX;
148102
148899
  const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;
148103
- if (candidates.length === 0) return;
148900
+ if (!this.featureFlags.sessionRehydration || candidates.length === 0) return candidates;
148104
148901
  const verdictById = new Map(verdicts.map((v2) => [v2.sessionId, v2]));
148105
148902
  let rehydrated = 0;
148106
148903
  const skippedBy = {};
@@ -148135,12 +148932,107 @@ var StreamerServer = class {
148135
148932
  skippedBy,
148136
148933
  truncated
148137
148934
  });
148935
+ return candidates;
148138
148936
  } catch (err) {
148139
148937
  this.log.warn("[rehydrate] failed to rehydrate previous sessions", {
148140
148938
  event: "sessions.rehydrate_failed",
148141
148939
  err
148142
148940
  });
148941
+ return [];
148942
+ }
148943
+ }
148944
+ /** Resume only the recent sessions the user explicitly allowed us to start at boot. */
148945
+ async autoResumePreviousSessions(rows) {
148946
+ if (!this.autoResumeOnBoot) return;
148947
+ const plan = planAutoResume(rows, { now: Date.now(), projectExists: import_fs30.existsSync });
148948
+ const skippedBy = {};
148949
+ for (const { row, reason } of plan.skipped) {
148950
+ skippedBy[reason] = (skippedBy[reason] ?? 0) + 1;
148951
+ this.log.debug(`[auto-resume] skipped ${row.session_id}: ${reason}`, {
148952
+ event: "sessions.auto_resume_skipped",
148953
+ sessionId: row.session_id,
148954
+ reason
148955
+ });
148143
148956
  }
148957
+ if (plan.skipped.length > 0) {
148958
+ this.log.info(
148959
+ `[auto-resume] left ${plan.skipped.length} ineligible session(s) for manual resume`,
148960
+ {
148961
+ event: "sessions.auto_resume_skipped",
148962
+ skipped: plan.skipped.length,
148963
+ skippedBy
148964
+ }
148965
+ );
148966
+ }
148967
+ for (const row of plan.overflow) {
148968
+ this.log.info(`[auto-resume] left ${row.session_id} for manual resume: ceiling reached`, {
148969
+ event: "sessions.auto_resume_skipped",
148970
+ sessionId: row.session_id,
148971
+ reason: "ceiling_reached"
148972
+ });
148973
+ }
148974
+ let resumed = 0;
148975
+ let failed = 0;
148976
+ const inFlight = /* @__PURE__ */ new Set();
148977
+ let started = 0;
148978
+ const resume = async (row) => {
148979
+ try {
148980
+ const outcome = await this.resumeSession({
148981
+ sessionId: row.session_id,
148982
+ projectName: row.project_name,
148983
+ branch: row.branch
148984
+ });
148985
+ if (!outcome.ok) {
148986
+ failed++;
148987
+ this.log.info(`[auto-resume] skipped ${row.session_id}: ${outcome.reason}`, {
148988
+ event: "sessions.auto_resume_skipped",
148989
+ sessionId: row.session_id,
148990
+ reason: outcome.reason,
148991
+ ...outcome.reason === "conversation_busy" && {
148992
+ detectedBy: outcome.detectedBy,
148993
+ lastActivityMs: outcome.lastActivityMs,
148994
+ likelyOwner: outcome.likelyOwner
148995
+ }
148996
+ });
148997
+ return;
148998
+ }
148999
+ resumed++;
149000
+ this.log.info(`[auto-resume] resumed ${row.session_id}`, {
149001
+ event: "sessions.auto_resume_succeeded",
149002
+ sessionId: row.session_id,
149003
+ alreadyRunning: outcome.alreadyRunning
149004
+ });
149005
+ } catch (err) {
149006
+ failed++;
149007
+ this.log.warn(`[auto-resume] failed to resume ${row.session_id}`, {
149008
+ event: "sessions.auto_resume_failed",
149009
+ sessionId: row.session_id,
149010
+ err
149011
+ });
149012
+ }
149013
+ };
149014
+ for (const row of plan.attempts) {
149015
+ while (inFlight.size >= AUTO_RESUME_CONCURRENCY) {
149016
+ await Promise.race(inFlight);
149017
+ }
149018
+ if (started > 0) {
149019
+ await new Promise((resolve4) => setTimeout(resolve4, AUTO_RESUME_STAGGER_MS));
149020
+ }
149021
+ const task = resume(row);
149022
+ inFlight.add(task);
149023
+ void task.then(() => inFlight.delete(task));
149024
+ started++;
149025
+ }
149026
+ await Promise.all(inFlight);
149027
+ if (resumed > 0) this.wsHub.broadcast(this.sessionListPayload());
149028
+ this.log.info(`[auto-resume] completed boot recovery: ${resumed} resumed`, {
149029
+ event: "sessions.auto_resume_completed",
149030
+ attempted: plan.attempts.length,
149031
+ resumed,
149032
+ failed,
149033
+ ineligible: plan.skipped.length,
149034
+ overflow: plan.overflow.length
149035
+ });
148144
149036
  }
148145
149037
  /**
148146
149038
  * Pick a token guaranteed to appear in the spawned process's argv, for the
@@ -148157,6 +149049,26 @@ var StreamerServer = class {
148157
149049
  if (session.provider !== CODEX_CLI_PROVIDER2) return session.id;
148158
149050
  return session.boundConversationId ?? session.projectPath;
148159
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
+ }
148160
149072
  /**
148161
149073
  * Mirror a freshly-spawned session into the durable registry (C1 Phase 2).
148162
149074
  *
@@ -148252,6 +149164,7 @@ var StreamerServer = class {
148252
149164
  * of waiting on the interval.
148253
149165
  */
148254
149166
  reapIdleSessions(now = Date.now()) {
149167
+ if (this.ptyManager.isRemote()) return [];
148255
149168
  const reaped = [];
148256
149169
  for (const session of this.ptyManager.listSessions()) {
148257
149170
  if (session.status === "running") continue;
@@ -148353,6 +149266,40 @@ var StreamerServer = class {
148353
149266
  return true;
148354
149267
  }
148355
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
+ }
148356
149303
  const dbConfig = this.disableDb ? null : getDbConfig();
148357
149304
  if (dbConfig) {
148358
149305
  this.dbPool = await createPool(dbConfig);
@@ -148367,8 +149314,10 @@ var StreamerServer = class {
148367
149314
  this.log.info("Database migrations applied", { event: "db.migrations_applied" });
148368
149315
  }
148369
149316
  await this.bindWithRetry(port);
148370
- this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
148371
- this.idleReaperTimer.unref?.();
149317
+ if (!this.ptyManager.isRemote()) {
149318
+ this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
149319
+ this.idleReaperTimer.unref?.();
149320
+ }
148372
149321
  const warmUp = new Promise((resolveWarm) => {
148373
149322
  {
148374
149323
  this.log.info(`Streamer server listening on port ${port}`, {
@@ -148386,6 +149335,15 @@ var StreamerServer = class {
148386
149335
  { error: message, abiMismatch, path: this.runtimeDbPath, event: "runtime.open_failed" }
148387
149336
  );
148388
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
+ }
148389
149347
  try {
148390
149348
  this.cache = ConversationCache.open(
148391
149349
  (0, import_path29.join)(this.cacheDir, "cache.db"),
@@ -148462,8 +149420,10 @@ var StreamerServer = class {
148462
149420
  );
148463
149421
  this.scannerPersistenceDisabled = true;
148464
149422
  }
148465
- void this.reconcilePreviousSessions().then((v2) => {
148466
- this.rehydratePreviousSessions(v2);
149423
+ if (this.ptyManager.isRemote()) this.refreshHostedSessionsFromRegistry();
149424
+ void this.reconcilePreviousSessions().then(async (v2) => {
149425
+ const recoverableRows = this.rehydratePreviousSessions(v2);
149426
+ await this.autoResumePreviousSessions(recoverableRows);
148467
149427
  this.pruneTerminalSessions();
148468
149428
  });
148469
149429
  if (this.skipStartupWarmup) {
@@ -148666,7 +149626,8 @@ var StreamerServer = class {
148666
149626
  }
148667
149627
  this.lastAgentChunkAt.clear();
148668
149628
  this.terminalSeq.clear();
148669
- this.recordShutdownState();
149629
+ if (this.ptyManager.isRemote()) this.ptyManager.dispose();
149630
+ else this.recordShutdownState();
148670
149631
  this.markScannerStaleDebounced.cancel();
148671
149632
  await Promise.all([...this.inFlightCacheWrites]);
148672
149633
  await Promise.all([...this.allScanners].map((s3) => s3.close()));
@@ -148674,7 +149635,7 @@ var StreamerServer = class {
148674
149635
  this.scanner = null;
148675
149636
  this.cache?.close();
148676
149637
  this.runtimeStore?.close();
148677
- this.ptyManager.dispose();
149638
+ if (!this.ptyManager.isRemote()) this.ptyManager.dispose();
148678
149639
  this.fileWatcher.dispose();
148679
149640
  this.externalTails.clear();
148680
149641
  this.wsHub.dispose();
@@ -149724,6 +150685,16 @@ var StreamerServer = class {
149724
150685
  const windowStart = Math.max(0, beforeIndex - scanLimit);
149725
150686
  const indexWindow = scanLimit > 0 && !hasAnchor && indexFilePath && this.cache ? this.cache.readMessageWindow(indexFilePath, windowStart, beforeIndex) : null;
149726
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
+ );
149727
150698
  this.trackCacheWrite(
149728
150699
  this.cache.backfillIndex(indexFilePath).catch((err) => {
149729
150700
  this.log.warn("offset-index.backfill_failed", {
@@ -149998,14 +150969,15 @@ var StreamerServer = class {
149998
150969
  }
149999
150970
  async handleGetSession(sessionId, res) {
150000
150971
  if (this.rejectIfWarmingUp(res)) return;
150001
- const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
150002
- if (session) {
150003
- if (!(0, import_fs30.existsSync)(session.projectPath)) {
150004
- session.failureReason = `Project directory not found: ${session.projectPath}`;
150005
- }
150006
- const reconciled = this.withReconciledLifecycle([session])[0];
150007
- session.lifecycle = reconciled.lifecycle;
150008
- 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
+ };
150009
150981
  if (this.ptyManager.hasSession(sessionId)) {
150010
150982
  try {
150011
150983
  const lines = await this.ptyManager.getOutputLines(sessionId, 10);
@@ -150168,31 +151140,37 @@ var StreamerServer = class {
150168
151140
  this.sessionStore.addManaged(session);
150169
151141
  this.recordSessionSpawn(session);
150170
151142
  void this.watchConversationFile(sessionId, historyId);
150171
- const response = this.sessionStore.get(session.id, this.ptyAttachedIds());
150172
151143
  this.enrichResumedSessionAsync(sessionId, projectPath, conv);
151144
+ const response = this.sessionStore.get(session.id, this.ptyAttachedIds());
150173
151145
  return { ok: true, alreadyRunning: false, session, response };
150174
151146
  }
150175
151147
  enrichResumedSessionAsync(sessionId, projectPath, conv) {
150176
151148
  try {
150177
- const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
150178
- if (!session) return;
151149
+ if (!this.sessionStore.getManaged(sessionId)) return;
150179
151150
  if (conv) {
150180
- session.sessionName = conv.sessionName ?? void 0;
150181
- session.messageCount = conv.messageCount ?? 0;
150182
- session.account = conv.account ?? void 0;
150183
- 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
+ });
150184
151157
  }
150185
151158
  if (!this.cache || !this.projectsRepo || !this.conversationsRepo) return;
150186
151159
  const cached4 = this.cache.getMetaById(sessionId);
150187
151160
  if (cached4) {
150188
- session.model = cached4.model ?? void 0;
150189
- session.preview = cached4.preview ?? void 0;
150190
151161
  const first = cached4.firstMessage ? JSON.parse(cached4.firstMessage) : null;
150191
151162
  const last = cached4.lastMessage ? JSON.parse(cached4.lastMessage) : null;
150192
- session.firstMessageText = first?.text ?? void 0;
150193
- session.firstMessageAt = first?.timestamp ? new Date(first.timestamp).toISOString() : void 0;
150194
- session.lastMessageText = last?.text ?? void 0;
150195
- 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
+ });
150196
151174
  }
150197
151175
  let resolvedProjectId = cached4?.projectId ?? null;
150198
151176
  if (!resolvedProjectId) {
@@ -150204,8 +151182,10 @@ var StreamerServer = class {
150204
151182
  });
150205
151183
  }
150206
151184
  if (resolvedProjectId) {
150207
- session.projectId = resolvedProjectId;
150208
- session.resumedFromConversationId = sessionId;
151185
+ this.sessionStore.updateManaged(sessionId, {
151186
+ projectId: resolvedProjectId,
151187
+ resumedFromConversationId: sessionId
151188
+ });
150209
151189
  }
150210
151190
  } catch (err) {
150211
151191
  console.error(`[enrichResumedSessionAsync] ${sessionId}:`, err);
@@ -151482,7 +152462,7 @@ function isBrewInstall(scriptPath = process.argv[1] ?? "") {
151482
152462
  // src/updater/download.ts
151483
152463
  var import_node_crypto5 = require("crypto");
151484
152464
  var import_node_fs10 = require("fs");
151485
- var import_node_path10 = require("path");
152465
+ var import_node_path11 = require("path");
151486
152466
  var import_promises17 = require("stream/promises");
151487
152467
 
151488
152468
  // src/updater/manifest.ts
@@ -151542,7 +152522,7 @@ async function downloadAndVerify(opts) {
151542
152522
  `Manifest references ${artifact.filename} but it is not attached to the release`
151543
152523
  );
151544
152524
  }
151545
- (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 });
151546
152526
  const res = await fetch(asset.browserDownloadUrl);
151547
152527
  if (!res.ok || !res.body) {
151548
152528
  throw new Error(`Failed to download ${artifact.filename}: ${res.status} ${res.statusText}`);
@@ -151581,23 +152561,23 @@ async function downloadAndVerify(opts) {
151581
152561
  }
151582
152562
 
151583
152563
  // src/updater/paths.ts
151584
- var import_node_os5 = require("os");
151585
- var import_node_path11 = require("path");
151586
- var THREADBASE_ROOT = (0, import_node_path11.join)((0, import_node_os5.homedir)(), ".threadbase");
151587
- var RELEASES_DIR = (0, import_node_path11.join)(THREADBASE_ROOT, "releases");
151588
- var CURRENT_SYMLINK = (0, import_node_path11.join)(THREADBASE_ROOT, "current");
151589
- 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");
151590
152570
  function releaseDir(version2) {
151591
- return (0, import_node_path11.join)(RELEASES_DIR, version2);
152571
+ return (0, import_node_path12.join)(RELEASES_DIR, version2);
151592
152572
  }
151593
152573
  function downloadPath(version2, filename) {
151594
- return (0, import_node_path11.join)(DOWNLOAD_DIR, `${version2}-${filename}`);
152574
+ return (0, import_node_path12.join)(DOWNLOAD_DIR, `${version2}-${filename}`);
151595
152575
  }
151596
152576
 
151597
152577
  // src/updater/restart.ts
151598
- var import_node_child_process3 = require("child_process");
152578
+ var import_node_child_process4 = require("child_process");
151599
152579
  var import_node_util3 = require("util");
151600
- 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);
151601
152581
  var BREW_LAUNCHD_LABEL = "homebrew.mxcl.tb-streamer";
151602
152582
  var DEPLOY_LAUNCHD_LABEL = "com.ronen.threadbase";
151603
152583
  function resolveDefaultLabel() {
@@ -151747,11 +152727,11 @@ async function waitForRestartHealth(opts) {
151747
152727
 
151748
152728
  // src/updater/stamp-version.ts
151749
152729
  var import_node_fs11 = require("fs");
151750
- var import_node_path12 = require("path");
152730
+ var import_node_path13 = require("path");
151751
152731
  function stampVersionTxt(destDir, version2) {
151752
- const distDir = (0, import_node_path12.join)(destDir, "dist");
152732
+ const distDir = (0, import_node_path13.join)(destDir, "dist");
151753
152733
  (0, import_node_fs11.mkdirSync)(distDir, { recursive: true });
151754
- (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));
151755
152735
  }
151756
152736
  function versionStamp(version2) {
151757
152737
  return `${version2}+update
@@ -151760,7 +152740,7 @@ function versionStamp(version2) {
151760
152740
 
151761
152741
  // src/updater/swap.ts
151762
152742
  var import_node_fs12 = require("fs");
151763
- var import_node_path13 = require("path");
152743
+ var import_node_path14 = require("path");
151764
152744
  var import_semver2 = __toESM(require_semver2(), 1);
151765
152745
  var KEEP_LAST_N = 2;
151766
152746
  function swapCurrent(version2) {
@@ -151776,7 +152756,7 @@ function swapCurrent(version2) {
151776
152756
  (0, import_node_fs12.rmSync)(CURRENT_SYMLINK, { recursive: true, force: true });
151777
152757
  }
151778
152758
  (0, import_node_fs12.renameSync)(tmp, CURRENT_SYMLINK);
151779
- (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"));
151780
152760
  publishVersionTxt(target, version2);
151781
152761
  return;
151782
152762
  }
@@ -151786,17 +152766,17 @@ function swapCurrent(version2) {
151786
152766
  }
151787
152767
  (0, import_node_fs12.symlinkSync)(target, tmpLink);
151788
152768
  (0, import_node_fs12.renameSync)(tmpLink, CURRENT_SYMLINK);
151789
- const cliJs = (0, import_node_path13.join)(THREADBASE_ROOT, "cli.js");
152769
+ const cliJs = (0, import_node_path14.join)(THREADBASE_ROOT, "cli.js");
151790
152770
  const tmpCliJs = `${cliJs}.new`;
151791
152771
  if ((0, import_node_fs12.existsSync)(tmpCliJs) || lstatSafeIsSymlink(tmpCliJs)) (0, import_node_fs12.unlinkSync)(tmpCliJs);
151792
- (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);
151793
152773
  (0, import_node_fs12.renameSync)(tmpCliJs, cliJs);
151794
152774
  publishVersionTxt(target, version2);
151795
152775
  }
151796
152776
  function publishVersionTxt(target, version2) {
151797
- const dest = (0, import_node_path13.join)(THREADBASE_ROOT, "version.txt");
152777
+ const dest = (0, import_node_path14.join)(THREADBASE_ROOT, "version.txt");
151798
152778
  try {
151799
- (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);
151800
152780
  } catch {
151801
152781
  (0, import_node_fs12.writeFileSync)(dest, versionStamp(version2));
151802
152782
  }
@@ -151835,7 +152815,7 @@ var import_fs31 = __toESM(require("fs"), 1);
151835
152815
  var import_node_events3 = require("events");
151836
152816
  var import_node_stream2 = __toESM(require("stream"), 1);
151837
152817
  var import_node_string_decoder = require("string_decoder");
151838
- var import_node_path14 = __toESM(require("path"), 1);
152818
+ var import_node_path15 = __toESM(require("path"), 1);
151839
152819
  var import_node_fs13 = __toESM(require("fs"), 1);
151840
152820
  var import_path30 = require("path");
151841
152821
  var import_events4 = require("events");
@@ -151843,27 +152823,27 @@ var import_assert = __toESM(require("assert"), 1);
151843
152823
  var import_buffer2 = require("buffer");
151844
152824
  var Ps = __toESM(require("zlib"), 1);
151845
152825
  var import_zlib = __toESM(require("zlib"), 1);
151846
- var import_node_path15 = require("path");
151847
152826
  var import_node_path16 = require("path");
152827
+ var import_node_path17 = require("path");
151848
152828
  var import_fs32 = __toESM(require("fs"), 1);
151849
152829
  var import_fs33 = __toESM(require("fs"), 1);
151850
152830
  var import_path31 = __toESM(require("path"), 1);
151851
- var import_node_path17 = require("path");
152831
+ var import_node_path18 = require("path");
151852
152832
  var import_path32 = __toESM(require("path"), 1);
151853
152833
  var import_node_fs14 = __toESM(require("fs"), 1);
151854
152834
  var import_node_assert = __toESM(require("assert"), 1);
151855
152835
  var import_node_crypto6 = require("crypto");
151856
152836
  var import_node_fs15 = __toESM(require("fs"), 1);
151857
- var import_node_path18 = __toESM(require("path"), 1);
152837
+ var import_node_path19 = __toESM(require("path"), 1);
151858
152838
  var import_fs34 = __toESM(require("fs"), 1);
151859
152839
  var import_node_fs16 = __toESM(require("fs"), 1);
151860
- var import_node_path19 = __toESM(require("path"), 1);
152840
+ var import_node_path20 = __toESM(require("path"), 1);
151861
152841
  var import_node_fs17 = __toESM(require("fs"), 1);
151862
152842
  var import_promises18 = __toESM(require("fs/promises"), 1);
151863
- var import_node_path20 = __toESM(require("path"), 1);
151864
- var import_node_path21 = require("path");
152843
+ var import_node_path21 = __toESM(require("path"), 1);
152844
+ var import_node_path22 = require("path");
151865
152845
  var import_node_fs18 = __toESM(require("fs"), 1);
151866
- var import_node_path22 = __toESM(require("path"), 1);
152846
+ var import_node_path23 = __toESM(require("path"), 1);
151867
152847
  var zr = Object.defineProperty;
151868
152848
  var Ur = (s3, t) => {
151869
152849
  for (var e in t) zr(s3, e, { get: t[e], enumerable: true });
@@ -152757,12 +153737,12 @@ var F = class {
152757
153737
  }
152758
153738
  };
152759
153739
  var un = (s3, t) => {
152760
- 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 || ".";
152761
153741
  if (Buffer.byteLength(i) < 100) n = [i, r, false];
152762
153742
  else {
152763
- 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);
152764
153744
  do
152765
- 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));
152766
153746
  while (r !== o && n === void 0);
152767
153747
  n || (n = [s3.slice(0, 99), "", true]);
152768
153748
  }
@@ -152808,7 +153788,7 @@ var ft = class s {
152808
153788
  if (t === "") return Buffer.allocUnsafe(0);
152809
153789
  let e = Buffer.byteLength(t), i = 512 * Math.ceil(1 + e / 512), r = Buffer.allocUnsafe(i);
152810
153790
  for (let n = 0; n < 512; n++) r[n] = 0;
152811
- 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");
152812
153792
  for (let n = e + 512; n < r.length; n++) r[n] = 0;
152813
153793
  return r;
152814
153794
  }
@@ -153264,7 +154244,7 @@ var Ct = K(Mn, Bn, (s3) => new rt(s3), (s3) => new rt(s3), (s3, t) => {
153264
154244
  t?.length && Qi(s3, t), s3.noResume || vn(s3);
153265
154245
  });
153266
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);
153267
- var { isAbsolute: zn, parse: qs } = import_node_path17.win32;
154247
+ var { isAbsolute: zn, parse: qs } = import_node_path18.win32;
153268
154248
  var ce = (s3) => {
153269
154249
  let t = "", e = qs(s3);
153270
154250
  for (; zn(s3) || e.root; ) {
@@ -153973,11 +154953,11 @@ var $n = (s3, t) => {
153973
154953
  };
153974
154954
  var fr = (s3, t) => {
153975
154955
  t.forEach((e) => {
153976
- 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);
153977
154957
  }), s3.end();
153978
154958
  };
153979
154959
  var dr = async (s3, t) => {
153980
- 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) => {
153981
154961
  s3.add(i);
153982
154962
  } }) : s3.add(e);
153983
154963
  s3.end();
@@ -154015,13 +154995,13 @@ var Ei = (s3, t, e, i) => {
154015
154995
  });
154016
154996
  };
154017
154997
  var io = (s3, t, e, i, r) => {
154018
- 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) => {
154019
154999
  if (n) return r(n);
154020
- let o = import_node_path19.default.resolve(s3, t.name);
155000
+ let o = import_node_path20.default.resolve(s3, t.name);
154021
155001
  Ei(o, e, i, r);
154022
155002
  });
154023
155003
  else {
154024
- let n = import_node_path19.default.resolve(s3, t.name);
155004
+ let n = import_node_path20.default.resolve(s3, t.name);
154025
155005
  Ei(n, e, i, r);
154026
155006
  }
154027
155007
  };
@@ -154042,7 +155022,7 @@ var Es = (s3, t, e, i) => {
154042
155022
  });
154043
155023
  };
154044
155024
  var so = (s3, t, e, i) => {
154045
- 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);
154046
155026
  };
154047
155027
  var ws = (s3, t, e) => {
154048
155028
  let i;
@@ -154092,12 +155072,12 @@ var gr = (s3, t, e) => {
154092
155072
  };
154093
155073
  if (s3 === d) return no(s3, S2);
154094
155074
  if (l) return import_promises18.default.mkdir(s3, { mode: r, recursive: true }).then((E) => S2(null, E ?? void 0), S2);
154095
- let D = f(import_node_path20.default.relative(d, s3)).split("/");
155075
+ let D = f(import_node_path21.default.relative(d, s3)).split("/");
154096
155076
  Ss(d, D, r, c, d, void 0, S2);
154097
155077
  };
154098
155078
  var Ss = (s3, t, e, i, r, n, o) => {
154099
155079
  if (t.length === 0) return o(null, n);
154100
- 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));
154101
155081
  import_node_fs17.default.mkdir(a, e, br(a, t, e, i, r, n, o));
154102
155082
  };
154103
155083
  var br = (s3, t, e, i, r, n, o) => (h) => {
@@ -154131,9 +155111,9 @@ var _r = (s3, t) => {
154131
155111
  };
154132
155112
  if (s3 === c) return oo(c), d();
154133
155113
  if (a) return d(import_node_fs17.default.mkdirSync(s3, { mode: i, recursive: true }) ?? void 0);
154134
- 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;
154135
155115
  for (let E = T2.shift(), x = c; E && (x += "/" + E); E = T2.shift()) {
154136
- x = f(import_node_path20.default.resolve(x));
155116
+ x = f(import_node_path21.default.resolve(x));
154137
155117
  try {
154138
155118
  import_node_fs17.default.mkdirSync(x, i), D = D || x;
154139
155119
  } catch {
@@ -154162,14 +155142,14 @@ var ho = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
154162
155142
  var ao = ho === "win32";
154163
155143
  var lo = (s3) => s3.split("/").slice(0, -1).reduce((e, i) => {
154164
155144
  let r = e.at(-1);
154165
- 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;
154166
155146
  }, []);
154167
155147
  var yi = class {
154168
155148
  #t = /* @__PURE__ */ new Map();
154169
155149
  #i = /* @__PURE__ */ new Map();
154170
155150
  #s = /* @__PURE__ */ new Set();
154171
155151
  reserve(t, e) {
154172
- 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))));
154173
155153
  let i = new Set(t.map((r) => lo(r)).reduce((r, n) => r.concat(n)));
154174
155154
  this.#i.set(e, { dirs: i, paths: t });
154175
155155
  for (let r of t) {
@@ -154309,7 +155289,7 @@ var Xt = class extends rt {
154309
155289
  if (t.preserveOwner) throw new TypeError("cannot preserve owner in archive and also set owner explicitly");
154310
155290
  this.uid = t.uid, this.gid = t.gid, this.setOwner = true;
154311
155291
  } else this.uid = void 0, this.gid = void 0, this.setOwner = false;
154312
- 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));
154313
155293
  }
154314
155294
  warn(t, e, i = {}) {
154315
155295
  return (t === "TAR_BAD_ARCHIVE" || t === "TAR_ABORT") && (i.recoverable = false), super.warn(t, e, i);
@@ -154323,7 +155303,7 @@ var Xt = class extends rt {
154323
155303
  let [n, o] = ce(i), h = o.replaceAll(/\\/g, "/").split("/");
154324
155304
  if (h.includes("..") || Te && /^[a-z]:\.\.$/i.test(h[0] ?? "")) {
154325
155305
  if (e === "path" || r === "Link") return this.warn("TAR_ENTRY_ERROR", `${e} contains '..'`, { entry: t, [e]: i }), false;
154326
- 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("/")));
154327
155307
  if (l.startsWith("../") || l === "..") return this.warn("TAR_ENTRY_ERROR", `${e} escapes extraction directory`, { entry: t, [e]: i }), false;
154328
155308
  }
154329
155309
  return n && (t[e] = String(o), this.warn("TAR_ENTRY_INFO", `stripping ${n} from absolute ${e}`, { entry: t, [e]: i })), true;
@@ -154341,12 +155321,12 @@ var Xt = class extends rt {
154341
155321
  }
154342
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;
154343
155323
  if (!this[Rs](t, "path") || !this[Rs](t, "linkpath")) return false;
154344
- 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;
154345
155325
  if (t.absolute === this.cwd && t.type !== "Directory" && t.type !== "GNUDumpDir") return false;
154346
155326
  if (this.win32) {
154347
- 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));
154348
155328
  t.absolute = r + ts(String(t.absolute).slice(r.length));
154349
- let { root: n } = import_node_path18.default.win32.parse(t.path);
155329
+ let { root: n } = import_node_path19.default.win32.parse(t.path);
154350
155330
  t.path = n + ts(t.path.slice(n.length));
154351
155331
  }
154352
155332
  return true;
@@ -154434,13 +155414,13 @@ var Xt = class extends rt {
154434
155414
  t.unsupported = true, this.warn("TAR_ENTRY_UNSUPPORTED", `unsupported entry type: ${t.type}`, { entry: t }), t.resume();
154435
155415
  }
154436
155416
  [Ar](t, e) {
154437
- 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("/");
154438
155418
  this[Re](t, this.cwd, i, () => this[gi](t, String(t.linkpath), "symlink", e), (r) => {
154439
155419
  this[O](r, t), e();
154440
155420
  });
154441
155421
  }
154442
155422
  [Ir](t, e) {
154443
- 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("/");
154444
155424
  this[Re](t, this.cwd, r, () => this[gi](t, i, "link", e), (n) => {
154445
155425
  this[O](n, t), e();
154446
155426
  });
@@ -154448,10 +155428,10 @@ var Xt = class extends rt {
154448
155428
  [Re](t, e, i, r, n) {
154449
155429
  let o = i.shift();
154450
155430
  if (this.preservePaths || o === void 0) return r();
154451
- let h = import_node_path18.default.resolve(e, o);
155431
+ let h = import_node_path19.default.resolve(e, o);
154452
155432
  import_node_fs15.default.lstat(h, (a, l) => {
154453
155433
  if (a) return r();
154454
- 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("/"))));
154455
155435
  this[Re](t, h, i, r, n);
154456
155436
  });
154457
155437
  }
@@ -154485,7 +155465,7 @@ var Xt = class extends rt {
154485
155465
  });
154486
155466
  }, n = () => {
154487
155467
  if (t.absolute !== this.cwd) {
154488
- let h = f(import_node_path18.default.dirname(String(t.absolute)));
155468
+ let h = f(import_node_path19.default.dirname(String(t.absolute)));
154489
155469
  if (h !== this.cwd) return this[yt](h, this.dmode, (a) => {
154490
155470
  if (a) {
154491
155471
  this[O](a, t), i();
@@ -154560,7 +155540,7 @@ var xe = class extends Xt {
154560
155540
  this[Oe] = true;
154561
155541
  }
154562
155542
  if (t.absolute !== this.cwd) {
154563
- let n = f(import_node_path18.default.dirname(String(t.absolute)));
155543
+ let n = f(import_node_path19.default.dirname(String(t.absolute)));
154564
155544
  if (n !== this.cwd) {
154565
155545
  let o = this[yt](n, this.dmode);
154566
155546
  if (o) return this[O](o, t);
@@ -154660,10 +155640,10 @@ var xe = class extends Xt {
154660
155640
  if (this.preservePaths || i.length === 0) return r();
154661
155641
  let o = e;
154662
155642
  for (let h of i) {
154663
- o = import_node_path18.default.resolve(o, h);
155643
+ o = import_node_path19.default.resolve(o, h);
154664
155644
  let [a, l] = ye(() => import_node_fs15.default.lstatSync(o));
154665
155645
  if (a) return r();
154666
- 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("/"))));
154667
155647
  }
154668
155648
  r();
154669
155649
  }
@@ -154767,11 +155747,11 @@ var go = (s3, t) => {
154767
155747
  };
154768
155748
  var bo = (s3, t) => {
154769
155749
  t.forEach((e) => {
154770
- 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);
154771
155751
  }), s3.end();
154772
155752
  };
154773
155753
  var _o = async (s3, t) => {
154774
- 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);
154775
155755
  s3.end();
154776
155756
  };
154777
155757
  var vt = K(yo, go, () => {
@@ -154801,8 +155781,8 @@ async function unpackTarball(opts) {
154801
155781
 
154802
155782
  // src/updater/update-log.ts
154803
155783
  var import_node_fs20 = require("fs");
154804
- var import_node_path23 = require("path");
154805
- 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");
154806
155786
  function appendUpdateLog(line) {
154807
155787
  try {
154808
155788
  (0, import_node_fs20.mkdirSync)(THREADBASE_ROOT, { recursive: true });
@@ -154917,15 +155897,19 @@ async function runInstall(opts) {
154917
155897
  }
154918
155898
 
154919
155899
  // cli/prod.ts
154920
- var import_node_child_process7 = require("child_process");
155900
+ var import_node_child_process8 = require("child_process");
154921
155901
  var import_node_fs23 = require("fs");
155902
+ init_auth();
155903
+ init_feature_flags();
154922
155904
  init_conflict_check();
154923
155905
  init_constants();
154924
155906
  init_launchd();
154925
155907
  init_marker();
154926
155908
  init_platform2();
154927
155909
  init_logger();
154928
- var log8 = getLogger("prod");
155910
+ init_protocol();
155911
+ init_socket();
155912
+ var log9 = getLogger("prod");
154929
155913
  function clearSupervisorLogs() {
154930
155914
  let paths;
154931
155915
  try {
@@ -154974,9 +155958,10 @@ async function runProdStatus() {
154974
155958
  marker: readMarker()
154975
155959
  };
154976
155960
  }
154977
- async function runProdDoctor(opts) {
155961
+ async function runProdDoctor(opts, deps = {}) {
154978
155962
  const findings = [];
154979
155963
  const repairs = [];
155964
+ let ptyHost;
154980
155965
  const marker = readMarker();
154981
155966
  if (marker && !marker.userHeld && !isPidAlive(marker.devPid)) {
154982
155967
  findings.push(`stale marker (dev pid ${marker.devPid} dead)`);
@@ -155000,7 +155985,23 @@ async function runProdDoctor(opts) {
155000
155985
  );
155001
155986
  }
155002
155987
  }
155003
- 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 } : {} };
155004
156005
  }
155005
156006
  var defaultSpawnTail = ({ files, lines, follow }) => {
155006
156007
  const existing = files.filter((f2) => (0, import_node_fs23.existsSync)(f2));
@@ -155014,7 +156015,7 @@ var defaultSpawnTail = ({ files, lines, follow }) => {
155014
156015
  if (follow) args.push("-F");
155015
156016
  args.push(...existing);
155016
156017
  return new Promise((resolve4) => {
155017
- 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"] });
155018
156019
  child.on("error", (err) => resolve4({ ok: false, message: `tail failed: ${err.message}` }));
155019
156020
  child.on("exit", (code) => resolve4({ ok: code === 0 }));
155020
156021
  });
@@ -155082,12 +156083,12 @@ function registerProdCommands(program3) {
155082
156083
  prod.command("start").description("Restore prod after a user-held suspension").option("--clear-logs", "Clear existing stdout + stderr logs", false).action(async (opts) => {
155083
156084
  if (opts.clearLogs === true) clearSupervisorLogs();
155084
156085
  const r = await runProdStart();
155085
- log8.info(r.message, void 0, "console");
156086
+ log9.info(r.message, void 0, "console");
155086
156087
  if (!r.ok) process.exitCode = 1;
155087
156088
  });
155088
156089
  prod.command("stop").description("Unload the launchd agent (prod will not auto-restart)").action(async () => {
155089
156090
  const r = await runProdStop();
155090
- log8.info(r.message, void 0, "console");
156091
+ log9.info(r.message, void 0, "console");
155091
156092
  });
155092
156093
  prod.command("status").description("Report whether prod is supervised, suspended, or down").action(async () => {
155093
156094
  const s3 = await runProdStatus();
@@ -155096,7 +156097,7 @@ function registerProdCommands(program3) {
155096
156097
  `pid: ${s3.agentPid ?? "(none)"}`,
155097
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"
155098
156099
  ];
155099
- log8.info(parts.join("\n "), void 0, "console");
156100
+ log9.info(parts.join("\n "), void 0, "console");
155100
156101
  });
155101
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) => {
155102
156103
  const sup = getSupervisor();
@@ -155105,17 +156106,24 @@ function registerProdCommands(program3) {
155105
156106
  sup.bootoutAgent();
155106
156107
  sup.bootstrapAgent(specPath, { afterBootout: true });
155107
156108
  const what = process.platform === "darwin" ? `agent restarted from ${specPath}` : `task '${TASK_NAME}' restarted`;
155108
- log8.info(what, void 0, "console");
156109
+ log9.info(what, void 0, "console");
155109
156110
  });
155110
- 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) => {
155111
156112
  const r = await runProdDoctor({ fix: opts.fix === true });
155112
- log8.info(`findings: ${r.findings.length === 0 ? "(none)" : ""}`, void 0, "console");
155113
- 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");
155114
156122
  if (r.repairs.length) {
155115
- log8.info(`repairs:`, void 0, "console");
155116
- 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");
155117
156125
  } else if (!opts.fix && r.findings.length > 0) {
155118
- 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");
155119
156127
  }
155120
156128
  });
155121
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) => {
@@ -155126,14 +156134,14 @@ function registerProdCommands(program3) {
155126
156134
  errorsOnly: opts.errorsOnly === true,
155127
156135
  clear: opts.clear === true
155128
156136
  });
155129
- if (r.message) log8.info(r.message, void 0, "console");
156137
+ if (r.message) log9.info(r.message, void 0, "console");
155130
156138
  if (!r.ok) process.exitCode = 1;
155131
156139
  });
155132
156140
  program3.addCommand(prod);
155133
156141
  }
155134
156142
 
155135
156143
  // cli/index.ts
155136
- var log11 = getLogger("cli");
156144
+ var log12 = getLogger("cli");
155137
156145
  var program2 = new Command();
155138
156146
  program2.name("threadbase-streamer").description("PTY session management, WebSocket streaming, and REST API server for Claude Code").version(getVersion());
155139
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(
@@ -155175,14 +156183,14 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155175
156183
  const { checkSqliteAbi: checkSqliteAbi2 } = await Promise.resolve().then(() => (init_check_sqlite_abi(), check_sqlite_abi_exports));
155176
156184
  checkSqliteAbi2();
155177
156185
  } catch (err) {
155178
- 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");
155179
156187
  process.exit(1);
155180
156188
  }
155181
156189
  if (opts.multiAgentFlow) {
155182
156190
  process.env.MULTI_AGENT_FLOW = "true";
155183
156191
  }
155184
156192
  if (opts.defaultPermissionMode !== void 0 && !isPermissionMode(opts.defaultPermissionMode)) {
155185
- log11.error(
156193
+ log12.error(
155186
156194
  `Invalid --default-permission-mode: ${opts.defaultPermissionMode} (expected one of ${PERMISSION_MODES.join(", ")})`,
155187
156195
  void 0,
155188
156196
  "console"
@@ -155198,7 +156206,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155198
156206
  const value = eq === -1 ? true : entry.slice(eq + 1);
155199
156207
  const def = findFlag(id);
155200
156208
  if (!def) {
155201
- log11.error(
156209
+ log12.error(
155202
156210
  `Invalid --claude-flag id: ${id} (expected one of ${CLAUDE_FLAGS.map((f2) => f2.id).join(", ")})`,
155203
156211
  void 0,
155204
156212
  "console"
@@ -155215,7 +156223,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155215
156223
  claudeFlags = validateFlagValues(raw2);
155216
156224
  for (const id of Object.keys(raw2)) {
155217
156225
  if (!(id in claudeFlags)) {
155218
- log11.error(`Invalid value for --claude-flag ${id}`, void 0, "console");
156226
+ log12.error(`Invalid value for --claude-flag ${id}`, void 0, "console");
155219
156227
  process.exit(1);
155220
156228
  }
155221
156229
  }
@@ -155224,13 +156232,13 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155224
156232
  if (Array.isArray(opts.feature) && opts.feature.length > 0) {
155225
156233
  const parsed = parseFeatureFlagArgs(opts.feature);
155226
156234
  if (parsed.errors.length > 0) {
155227
- 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");
155228
156236
  process.exit(1);
155229
156237
  }
155230
156238
  featureFlags = parsed.values;
155231
156239
  }
155232
156240
  if (opts.defaultEffort !== void 0 && !isEffortLevel(opts.defaultEffort)) {
155233
- log11.error(
156241
+ log12.error(
155234
156242
  `Invalid --default-effort: ${opts.defaultEffort} (expected one of ${EFFORT_LEVELS.join(", ")})`,
155235
156243
  void 0,
155236
156244
  "console"
@@ -155241,7 +156249,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155241
156249
  if (opts.ptyGracePeriodMs !== void 0) {
155242
156250
  const parsed = Number(opts.ptyGracePeriodMs);
155243
156251
  if (!Number.isInteger(parsed) || parsed < 0) {
155244
- log11.error(
156252
+ log12.error(
155245
156253
  `Invalid --pty-grace-period-ms: ${opts.ptyGracePeriodMs} (expected a non-negative integer; 0 disables auto-hold)`,
155246
156254
  void 0,
155247
156255
  "console"
@@ -155257,11 +156265,17 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155257
156265
  const publicUrl = opts.publicUrl ?? loadPublicUrl() ?? null;
155258
156266
  const isProdInvocation = opts.prod === true || process.ppid === 1;
155259
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
+ }
155260
156274
  if (process.platform === "darwin") {
155261
156275
  const { detectConflictingAgents: detectConflictingAgents2, formatConflictMessage: formatConflictMessage2 } = await Promise.resolve().then(() => (init_conflict_check(), conflict_check_exports));
155262
156276
  const conflicts = detectConflictingAgents2();
155263
156277
  if (conflicts.length > 0) {
155264
- log11.warn(formatConflictMessage2(conflicts), void 0, "console");
156278
+ log12.warn(formatConflictMessage2(conflicts), void 0, "console");
155265
156279
  }
155266
156280
  }
155267
156281
  let resolvedDefaultPermissionMode = opts.defaultPermissionMode;
@@ -155276,7 +156290,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155276
156290
  resolvedAutoResumeOnBoot = await interactiveAutoResumePrompt2();
155277
156291
  setAutoResumeOnBoot(resolvedAutoResumeOnBoot);
155278
156292
  } else if (resolvedAutoResumeOnBoot === void 0) {
155279
- log11.info(
156293
+ log12.info(
155280
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.",
155281
156295
  void 0,
155282
156296
  "console"
@@ -155326,15 +156340,15 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155326
156340
  await server.listen(resolvedPort);
155327
156341
  {
155328
156342
  const v2 = getVersion();
155329
- log11.info(`Threadbase Streamer v${v2}`, { version: v2, port: resolvedPort });
156343
+ log12.info(`Threadbase Streamer v${v2}`, { version: v2, port: resolvedPort });
155330
156344
  }
155331
- log11.info(`Listening on http://localhost:${resolvedPort}`, {
156345
+ log12.info(`Listening on http://localhost:${resolvedPort}`, {
155332
156346
  url: `http://localhost:${resolvedPort}`
155333
156347
  });
155334
- log11.info(`WebSocket at ws://localhost:${resolvedPort}/ws`, {
156348
+ log12.info(`WebSocket at ws://localhost:${resolvedPort}/ws`, {
155335
156349
  wsUrl: `ws://localhost:${resolvedPort}/ws`
155336
156350
  });
155337
- log11.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
156351
+ log12.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
155338
156352
  try {
155339
156353
  await printServerBanner({
155340
156354
  port: resolvedPort,
@@ -155344,15 +156358,15 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155344
156358
  });
155345
156359
  } catch (err) {
155346
156360
  const message = err instanceof Error ? err.message : String(err);
155347
- log11.warn(`(skipped pairing QR: ${message})`, { reason: message });
155348
- log11.info(
156361
+ log12.warn(`(skipped pairing QR: ${message})`, { reason: message });
156362
+ log12.info(
155349
156363
  printUrlBanner({ url: resolveServerUrl({ publicUrl, port: resolvedPort }) }),
155350
156364
  void 0,
155351
156365
  "console"
155352
156366
  );
155353
156367
  }
155354
156368
  const shutdown = async () => {
155355
- log11.info("Shutting down...");
156369
+ log12.info("Shutting down...");
155356
156370
  await server.close();
155357
156371
  process.exit(0);
155358
156372
  };
@@ -155360,7 +156374,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155360
156374
  process.on("SIGINT", shutdown);
155361
156375
  process.on("SIGTERM", shutdown);
155362
156376
  process.on("uncaughtException", (err) => {
155363
- log11.error(`uncaught: ${err.message}`, {
156377
+ log12.error(`uncaught: ${err.message}`, {
155364
156378
  error: err.message,
155365
156379
  stack: err.stack,
155366
156380
  event: "process.uncaught"
@@ -155369,7 +156383,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
155369
156383
  });
155370
156384
  process.on("unhandledRejection", (reason) => {
155371
156385
  const msg = reason instanceof Error ? reason.message : String(reason);
155372
- log11.error(`unhandled rejection: ${msg}`, {
156386
+ log12.error(`unhandled rejection: ${msg}`, {
155373
156387
  error: msg,
155374
156388
  event: "process.unhandled_rejection"
155375
156389
  });
@@ -155394,10 +156408,10 @@ program2.command("cache").description("Manage the local SQLite conversation cach
155394
156408
  const f2 = dbPath + suffix;
155395
156409
  if (existsSync16(f2)) {
155396
156410
  rmSync6(f2);
155397
- log11.info(`Deleted ${f2}`, { path: f2 }, "console");
156411
+ log12.info(`Deleted ${f2}`, { path: f2 }, "console");
155398
156412
  }
155399
156413
  }
155400
- 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");
155401
156415
  })
155402
156416
  );
155403
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) => {
@@ -155422,7 +156436,7 @@ program2.command("set-key [key]").description("Set the streamer API key in ~/.th
155422
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) => {
155423
156437
  const cfg = loadUpdateConfig();
155424
156438
  if (!cfg) {
155425
- log11.warn(
156439
+ log12.warn(
155426
156440
  `No update config found at ${DEFAULT_CONFIG_PATH}. Create one with at least 'github_repo: owner/name' to enable updates.`,
155427
156441
  void 0,
155428
156442
  "console"
@@ -155441,11 +156455,11 @@ program2.command("update").description("Check for streamer updates from GitHub R
155441
156455
  appendUpdateLog(
155442
156456
  `[check] current=${result2.current} latest=${result2.latest ?? "none"} status=${result2.reason}`
155443
156457
  );
155444
- log11.info(`Current : ${result2.current}`, void 0, "console");
155445
- log11.info(`Latest : ${result2.latest ?? "(none)"}`, void 0, "console");
155446
- log11.info(`Channel : ${cfg.channel}`, void 0, "console");
155447
- log11.info(`Diff : ${result2.diff ?? "(none)"}`, void 0, "console");
155448
- 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");
155449
156463
  return;
155450
156464
  }
155451
156465
  const port = Number.parseInt(opts.port, 10);
@@ -155461,20 +156475,20 @@ program2.command("update").description("Check for streamer updates from GitHub R
155461
156475
  });
155462
156476
  switch (result.kind) {
155463
156477
  case "no-op":
155464
- log11.info(`Current : ${result.current}`, void 0, "console");
155465
- log11.info(`Latest : ${result.latest ?? "(none)"}`, void 0, "console");
155466
- 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");
155467
156481
  break;
155468
156482
  case "unsupported-install":
155469
- log11.warn(result.reason, void 0, "console");
156483
+ log12.warn(result.reason, void 0, "console");
155470
156484
  process.exitCode = 2;
155471
156485
  break;
155472
156486
  case "deferred":
155473
- log11.warn(`Deferred: ${result.reason}`, void 0, "console");
156487
+ log12.warn(`Deferred: ${result.reason}`, void 0, "console");
155474
156488
  process.exitCode = 2;
155475
156489
  break;
155476
156490
  case "dry-run":
155477
- log11.info(
156491
+ log12.info(
155478
156492
  `Would install ${result.latest} from ${result.tarballUrl}`,
155479
156493
  void 0,
155480
156494
  "console"
@@ -155482,28 +156496,28 @@ program2.command("update").description("Check for streamer updates from GitHub R
155482
156496
  break;
155483
156497
  case "installed":
155484
156498
  if (result.restart.method.startsWith("failed:")) {
155485
- log11.error(
156499
+ log12.error(
155486
156500
  `Installed ${result.installed} on disk, but the running service was not updated. Restart: ${result.restart.method}.`,
155487
156501
  void 0,
155488
156502
  "console"
155489
156503
  );
155490
156504
  process.exitCode = 1;
155491
156505
  } else {
155492
- log11.info(
156506
+ log12.info(
155493
156507
  `Installed ${result.installed} (was ${result.previous}). Restart: ${result.restart.method}.`,
155494
156508
  void 0,
155495
156509
  "console"
155496
156510
  );
155497
156511
  }
155498
156512
  if (result.pruned.length > 0) {
155499
- log11.info(`Pruned old releases: ${result.pruned.join(", ")}`, void 0, "console");
156513
+ log12.info(`Pruned old releases: ${result.pruned.join(", ")}`, void 0, "console");
155500
156514
  }
155501
156515
  break;
155502
156516
  }
155503
156517
  } catch (err) {
155504
156518
  const message = err instanceof Error ? err.message : String(err);
155505
156519
  appendUpdateLog(`[error] ${message}`);
155506
- log11.error(`Update failed: ${message}`, { error: message }, "console");
156520
+ log12.error(`Update failed: ${message}`, { error: message }, "console");
155507
156521
  process.exitCode = 1;
155508
156522
  }
155509
156523
  });
@@ -155515,21 +156529,32 @@ program2.command("pty-host").description(
155515
156529
  ).action(async (opts) => {
155516
156530
  const { SessionHost: SessionHost2 } = await Promise.resolve().then(() => (init_host(), host_exports));
155517
156531
  const { listenForStreamers: listenForStreamers2 } = await Promise.resolve().then(() => (init_socket(), socket_exports));
155518
- const host = new SessionHost2({ logger: getLogger("pty-host") });
155519
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
+ });
155520
156545
  try {
155521
156546
  server = await listenForStreamers2(opts.socket, {
155522
156547
  onConnection: (transport) => host.accept(transport)
155523
156548
  });
155524
156549
  } catch (err) {
155525
- log11.error(
156550
+ log12.error(
155526
156551
  `pty-host could not listen on ${opts.socket}: ${err instanceof Error ? err.message : String(err)}`,
155527
156552
  void 0,
155528
156553
  "console"
155529
156554
  );
155530
156555
  process.exit(1);
155531
156556
  }
155532
- log11.info(`pty-host listening on ${opts.socket}`, {
156557
+ log12.info(`pty-host listening on ${opts.socket}`, {
155533
156558
  event: "pty_host.listening",
155534
156559
  socket: opts.socket
155535
156560
  });
@@ -155583,7 +156608,7 @@ async function printServerBanner({
155583
156608
  }) {
155584
156609
  const url2 = resolveServerUrl({ publicUrl, port });
155585
156610
  if (!includeQr) {
155586
- log11.info(printUrlBanner({ url: url2 }), void 0, "console");
156611
+ log12.info(printUrlBanner({ url: url2 }), void 0, "console");
155587
156612
  return;
155588
156613
  }
155589
156614
  const res = await fetch(`http://localhost:${port}/api/pair/start`, {
@@ -155598,9 +156623,9 @@ async function printServerBanner({
155598
156623
  const expSeconds = Math.floor(expiresAt / 1e3);
155599
156624
  const payload = `threadbase://pair?url=${encodeURIComponent(url2)}&token=${token}&exp=${expSeconds}`;
155600
156625
  const qr2 = await generateQr(payload);
155601
- log11.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
155602
- log11.info(`Pair URL: ${payload}`, void 0, "console");
155603
- 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");
155604
156629
  }
155605
156630
  /*! Bundled license information:
155606
156631