@openscout/scout 0.2.57 → 0.2.60

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.
@@ -5703,6 +5703,20 @@ var init_support_paths = () => {};
5703
5703
  import { existsSync as existsSync4, statSync as statSync2 } from "fs";
5704
5704
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
5705
5705
  import { dirname as dirname3, join as join5 } from "path";
5706
+ function buildHarnessResumeCommand(entry, sessionId, cwd) {
5707
+ if (!entry.resume)
5708
+ return null;
5709
+ const parts = [entry.resume.command, entry.resume.sessionFlag, sessionId];
5710
+ if (cwd && entry.resume.cwdFlag) {
5711
+ parts.push(entry.resume.cwdFlag, cwd);
5712
+ }
5713
+ return parts.join(" ");
5714
+ }
5715
+ function findHarnessEntry(harness) {
5716
+ if (!harness)
5717
+ return null;
5718
+ return BUILT_IN_HARNESS_CATALOG.find((e) => e.harness === harness || e.name === harness) ?? null;
5719
+ }
5706
5720
  async function writeHarnessCatalogOverrides(overrides, overridePath = resolveOpenScoutSupportPaths().harnessCatalogPath) {
5707
5721
  await mkdir2(dirname3(overridePath), { recursive: true });
5708
5722
  const payload = {
@@ -5761,6 +5775,11 @@ var init_harness_catalog = __esm(() => {
5761
5775
  loginCommand: "claude login",
5762
5776
  notReadyMessage: "Claude is installed but not authenticated yet."
5763
5777
  },
5778
+ resume: {
5779
+ command: "claude",
5780
+ sessionFlag: "--resume",
5781
+ cwdFlag: "--cwd"
5782
+ },
5764
5783
  capabilities: ["chat", "invoke", "deliver", "summarize", "review"]
5765
5784
  },
5766
5785
  {
@@ -5792,6 +5811,11 @@ var init_harness_catalog = __esm(() => {
5792
5811
  loginCommand: "codex login",
5793
5812
  notReadyMessage: "Codex is installed but not authenticated yet."
5794
5813
  },
5814
+ resume: {
5815
+ command: "codex",
5816
+ sessionFlag: "--thread",
5817
+ cwdFlag: "--cwd"
5818
+ },
5795
5819
  capabilities: ["chat", "invoke", "deliver", "review", "execute"]
5796
5820
  }
5797
5821
  ];
@@ -8056,7 +8080,8 @@ function buildManagedAgentShellExports(options) {
8056
8080
  // packages/runtime/src/claude-stream-json.ts
8057
8081
  import { randomUUID } from "crypto";
8058
8082
  import { spawn as spawn3 } from "child_process";
8059
- import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "fs/promises";
8083
+ import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
8084
+ import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
8060
8085
  import { join as join13 } from "path";
8061
8086
  function resolveClaudeStreamJsonOutput(result, fallbackParts) {
8062
8087
  const trimmedResult = result?.trim();
@@ -8092,6 +8117,53 @@ function parseJsonObject(value) {
8092
8117
  return {};
8093
8118
  }
8094
8119
  }
8120
+ function readSessionCatalogSync(runtimeDirectory) {
8121
+ const catalogPath = join13(runtimeDirectory, SESSION_CATALOG_FILENAME);
8122
+ try {
8123
+ if (!existsSync9(catalogPath)) {
8124
+ const legacyPath = join13(runtimeDirectory, "claude-session-id.txt");
8125
+ if (existsSync9(legacyPath)) {
8126
+ const legacyId = readFileSync5(legacyPath, "utf8").trim();
8127
+ if (legacyId) {
8128
+ return {
8129
+ activeSessionId: legacyId,
8130
+ sessions: [{ id: legacyId, startedAt: Date.now(), cwd: "" }]
8131
+ };
8132
+ }
8133
+ }
8134
+ return { activeSessionId: null, sessions: [] };
8135
+ }
8136
+ const raw2 = readFileSync5(catalogPath, "utf8").trim();
8137
+ if (!raw2)
8138
+ return { activeSessionId: null, sessions: [] };
8139
+ const parsed = JSON.parse(raw2);
8140
+ return {
8141
+ activeSessionId: parsed.activeSessionId ?? null,
8142
+ sessions: Array.isArray(parsed.sessions) ? parsed.sessions : []
8143
+ };
8144
+ } catch {
8145
+ return { activeSessionId: null, sessions: [] };
8146
+ }
8147
+ }
8148
+ async function readSessionCatalog(runtimeDirectory) {
8149
+ return readSessionCatalogSync(runtimeDirectory);
8150
+ }
8151
+ async function writeSessionCatalog(runtimeDirectory, catalog) {
8152
+ const catalogPath = join13(runtimeDirectory, SESSION_CATALOG_FILENAME);
8153
+ await writeFile4(catalogPath, JSON.stringify(catalog, null, 2) + `
8154
+ `);
8155
+ }
8156
+ function catalogRecordSession(catalog, sessionId, cwd) {
8157
+ const now = Date.now();
8158
+ const sessions = catalog.sessions.map((s) => s.id === catalog.activeSessionId && !s.endedAt ? { ...s, endedAt: now } : s);
8159
+ if (!sessions.some((s) => s.id === sessionId)) {
8160
+ sessions.push({ id: sessionId, startedAt: now, cwd });
8161
+ }
8162
+ while (sessions.length > SESSION_CATALOG_MAX_ENTRIES) {
8163
+ sessions.shift();
8164
+ }
8165
+ return { activeSessionId: sessionId, sessions };
8166
+ }
8095
8167
  function appendActionOutput(blockState, output) {
8096
8168
  if (!blockState || blockState.block.type !== "action" || !output) {
8097
8169
  return;
@@ -8495,7 +8567,7 @@ function buildClaudeStreamJsonSessionSnapshot(raw2, options, targetClaudeSession
8495
8567
 
8496
8568
  class ClaudeStreamJsonSession {
8497
8569
  options;
8498
- sessionStatePath;
8570
+ catalogDirectory;
8499
8571
  stdoutLogPath;
8500
8572
  stderrLogPath;
8501
8573
  process = null;
@@ -8506,7 +8578,7 @@ class ClaudeStreamJsonSession {
8506
8578
  lastConfigSignature;
8507
8579
  constructor(options) {
8508
8580
  this.options = options;
8509
- this.sessionStatePath = join13(options.runtimeDirectory, "claude-session-id.txt");
8581
+ this.catalogDirectory = options.runtimeDirectory;
8510
8582
  this.stdoutLogPath = join13(options.logsDirectory, "stdout.log");
8511
8583
  this.stderrLogPath = join13(options.logsDirectory, "stderr.log");
8512
8584
  this.lastConfigSignature = this.configSignature(options);
@@ -8612,7 +8684,9 @@ class ClaudeStreamJsonSession {
8612
8684
  }
8613
8685
  if (options.resetSession) {
8614
8686
  this.claudeSessionId = null;
8615
- await rm3(this.sessionStatePath, { force: true });
8687
+ const catalog = await readSessionCatalog(this.catalogDirectory);
8688
+ catalog.activeSessionId = null;
8689
+ await writeSessionCatalog(this.catalogDirectory, catalog);
8616
8690
  }
8617
8691
  }
8618
8692
  configSignature(options) {
@@ -8641,7 +8715,8 @@ class ClaudeStreamJsonSession {
8641
8715
  await mkdir4(this.options.runtimeDirectory, { recursive: true });
8642
8716
  await mkdir4(this.options.logsDirectory, { recursive: true });
8643
8717
  await writeFile4(join13(this.options.runtimeDirectory, "prompt.txt"), this.options.systemPrompt);
8644
- this.claudeSessionId = await readOptionalFile2(this.sessionStatePath);
8718
+ const catalog = await readSessionCatalog(this.catalogDirectory);
8719
+ this.claudeSessionId = catalog.activeSessionId;
8645
8720
  const args = [
8646
8721
  "--verbose",
8647
8722
  "--print",
@@ -8720,8 +8795,11 @@ class ClaudeStreamJsonSession {
8720
8795
  const nextSessionId = event.session_id ?? event.sessionId ?? null;
8721
8796
  if (nextSessionId && nextSessionId !== this.claudeSessionId) {
8722
8797
  this.claudeSessionId = nextSessionId;
8723
- writeFile4(this.sessionStatePath, `${nextSessionId}
8724
- `);
8798
+ (async () => {
8799
+ const catalog = await readSessionCatalog(this.catalogDirectory);
8800
+ const updated = catalogRecordSession(catalog, nextSessionId, this.options.cwd);
8801
+ await writeSessionCatalog(this.catalogDirectory, updated);
8802
+ })();
8725
8803
  }
8726
8804
  return;
8727
8805
  }
@@ -8794,37 +8872,137 @@ async function answerClaudeStreamJsonQuestion(options, input) {
8794
8872
  }
8795
8873
  async function getClaudeStreamJsonAgentSnapshot(options) {
8796
8874
  const stdoutLogPath = join13(options.logsDirectory, "stdout.log");
8797
- const sessionStatePath = join13(options.runtimeDirectory, "claude-session-id.txt");
8798
- const [rawLog, persistedSessionId] = await Promise.all([
8875
+ const [rawLog, catalog] = await Promise.all([
8799
8876
  readOptionalFile2(stdoutLogPath),
8800
- readOptionalFile2(sessionStatePath)
8877
+ readSessionCatalog(options.runtimeDirectory)
8801
8878
  ]);
8802
8879
  if (!rawLog) {
8803
8880
  return null;
8804
8881
  }
8805
- return buildClaudeStreamJsonSessionSnapshot(rawLog, options, persistedSessionId);
8882
+ return buildClaudeStreamJsonSessionSnapshot(rawLog, options, catalog.activeSessionId);
8806
8883
  }
8807
8884
  async function shutdownClaudeStreamJsonAgent(options, shutdownOptions = {}) {
8808
8885
  const key = sessionKey(options);
8809
8886
  const session = sessions.get(key);
8810
8887
  if (!session) {
8811
8888
  if (shutdownOptions.resetSession) {
8812
- await rm3(join13(options.runtimeDirectory, "claude-session-id.txt"), { force: true });
8889
+ const catalog = await readSessionCatalog(options.runtimeDirectory);
8890
+ catalog.activeSessionId = null;
8891
+ await writeSessionCatalog(options.runtimeDirectory, catalog);
8813
8892
  }
8814
8893
  return;
8815
8894
  }
8816
8895
  sessions.delete(key);
8817
8896
  await session.shutdown(shutdownOptions);
8818
8897
  }
8819
- var sessions;
8898
+ var SESSION_CATALOG_FILENAME = "session-catalog.json", SESSION_CATALOG_MAX_ENTRIES = 64, sessions;
8820
8899
  var init_claude_stream_json = __esm(() => {
8821
8900
  sessions = new Map;
8822
8901
  });
8823
8902
 
8824
8903
  // packages/runtime/src/codex-app-server.ts
8825
8904
  import { spawn as spawn4 } from "child_process";
8826
- import { access as access3, appendFile as appendFile3, constants as constants4, mkdir as mkdir5, readFile as readFile6, rm as rm4, writeFile as writeFile5 } from "fs/promises";
8905
+ import { constants as constants4 } from "fs";
8906
+ import { access as access3, appendFile as appendFile3, mkdir as mkdir5, readFile as readFile6, rm as rm4, writeFile as writeFile5 } from "fs/promises";
8827
8907
  import { delimiter as delimiter3, join as join14 } from "path";
8908
+ function normalizeCodexModelValue(value) {
8909
+ const trimmed = value?.trim();
8910
+ return trimmed ? trimmed : null;
8911
+ }
8912
+ function encodeCodexModelConfig(model) {
8913
+ return `model=${JSON.stringify(model)}`;
8914
+ }
8915
+ function parseCodexModelConfig(value) {
8916
+ const trimmed = value?.trim();
8917
+ if (!trimmed) {
8918
+ return null;
8919
+ }
8920
+ const separatorIndex = trimmed.indexOf("=");
8921
+ if (separatorIndex <= 0) {
8922
+ return null;
8923
+ }
8924
+ const key = trimmed.slice(0, separatorIndex).trim();
8925
+ if (key !== "model") {
8926
+ return null;
8927
+ }
8928
+ const rawValue = trimmed.slice(separatorIndex + 1).trim();
8929
+ if (!rawValue) {
8930
+ return null;
8931
+ }
8932
+ if (rawValue.startsWith('"') && rawValue.endsWith('"') || rawValue.startsWith("'") && rawValue.endsWith("'")) {
8933
+ return rawValue.slice(1, -1) || null;
8934
+ }
8935
+ return rawValue;
8936
+ }
8937
+ function normalizeCodexAppServerLaunchArgs(launchArgs) {
8938
+ const args = Array.isArray(launchArgs) ? launchArgs.map((entry) => String(entry).trim()).filter(Boolean) : [];
8939
+ const normalized = [];
8940
+ for (let index = 0;index < args.length; index += 1) {
8941
+ const current = args[index] ?? "";
8942
+ if (current === "--model" || current === "-m") {
8943
+ const model = normalizeCodexModelValue(args[index + 1]);
8944
+ if (model) {
8945
+ normalized.push("-c", encodeCodexModelConfig(model));
8946
+ index += 1;
8947
+ continue;
8948
+ }
8949
+ normalized.push(current);
8950
+ continue;
8951
+ }
8952
+ if (current.startsWith("--model=")) {
8953
+ const model = normalizeCodexModelValue(current.slice("--model=".length));
8954
+ if (model) {
8955
+ normalized.push("-c", encodeCodexModelConfig(model));
8956
+ continue;
8957
+ }
8958
+ }
8959
+ if (current.startsWith("-m=")) {
8960
+ const model = normalizeCodexModelValue(current.slice(3));
8961
+ if (model) {
8962
+ normalized.push("-c", encodeCodexModelConfig(model));
8963
+ continue;
8964
+ }
8965
+ }
8966
+ if (current === "-c" || current === "--config") {
8967
+ const next = args[index + 1];
8968
+ if (typeof next === "string") {
8969
+ const model = parseCodexModelConfig(next);
8970
+ normalized.push(current === "--config" ? "--config" : "-c", model ? encodeCodexModelConfig(model) : next);
8971
+ index += 1;
8972
+ continue;
8973
+ }
8974
+ }
8975
+ if (current.startsWith("--config=")) {
8976
+ const value = current.slice("--config=".length);
8977
+ const model = parseCodexModelConfig(value);
8978
+ normalized.push(model ? `--config=${encodeCodexModelConfig(model)}` : current);
8979
+ continue;
8980
+ }
8981
+ normalized.push(current);
8982
+ }
8983
+ return normalized;
8984
+ }
8985
+ function readCodexAppServerModelFromLaunchArgs(launchArgs) {
8986
+ const normalized = normalizeCodexAppServerLaunchArgs(launchArgs);
8987
+ for (let index = 0;index < normalized.length; index += 1) {
8988
+ const current = normalized[index] ?? "";
8989
+ if (current === "-c" || current === "--config") {
8990
+ const model = parseCodexModelConfig(normalized[index + 1]);
8991
+ if (model) {
8992
+ return model;
8993
+ }
8994
+ index += 1;
8995
+ continue;
8996
+ }
8997
+ if (current.startsWith("--config=")) {
8998
+ const model = parseCodexModelConfig(current.slice("--config=".length));
8999
+ if (model) {
9000
+ return model;
9001
+ }
9002
+ }
9003
+ }
9004
+ return null;
9005
+ }
8828
9006
  function isCodexThreadGlobalMessage(message) {
8829
9007
  const result = message.result;
8830
9008
  const thread = result?.thread;
@@ -9627,6 +9805,13 @@ function isMissingCodexRolloutError2(error) {
9627
9805
  const message = errorMessage3(error).toLowerCase();
9628
9806
  return message.includes("no rollout found for thread id");
9629
9807
  }
9808
+ function resolveCodexCompletionGraceMs() {
9809
+ const parsed = Number.parseInt(process.env.OPENSCOUT_CODEX_COMPLETION_GRACE_MS ?? "", 10);
9810
+ if (Number.isFinite(parsed) && parsed >= 0) {
9811
+ return parsed;
9812
+ }
9813
+ return 60000;
9814
+ }
9630
9815
 
9631
9816
  class CodexAppServerSession {
9632
9817
  options;
@@ -9816,7 +10001,7 @@ class CodexAppServerSession {
9816
10001
  systemPrompt: options.systemPrompt,
9817
10002
  threadId: options.threadId ?? null,
9818
10003
  requireExistingThread: options.requireExistingThread === true,
9819
- launchArgs: Array.isArray(options.launchArgs) ? options.launchArgs : []
10004
+ launchArgs: normalizeCodexAppServerLaunchArgs(options.launchArgs)
9820
10005
  });
9821
10006
  }
9822
10007
  async ensureStarted() {
@@ -9838,6 +10023,7 @@ class CodexAppServerSession {
9838
10023
  await mkdir5(this.options.logsDirectory, { recursive: true });
9839
10024
  await writeFile5(join14(this.options.runtimeDirectory, "prompt.txt"), this.options.systemPrompt);
9840
10025
  const codexExecutable = await resolveCodexExecutable2();
10026
+ const launchArgs = normalizeCodexAppServerLaunchArgs(this.options.launchArgs);
9841
10027
  const env = buildManagedAgentEnvironment({
9842
10028
  agentName: this.options.agentName,
9843
10029
  currentDirectory: this.options.cwd,
@@ -9848,7 +10034,8 @@ class CodexAppServerSession {
9848
10034
  ...buildScoutMcpCodexLaunchArgs({
9849
10035
  currentDirectory: this.options.cwd,
9850
10036
  env
9851
- })
10037
+ }),
10038
+ ...launchArgs
9852
10039
  ], {
9853
10040
  cwd: this.options.cwd,
9854
10041
  env,
@@ -9946,6 +10133,8 @@ class CodexAppServerSession {
9946
10133
  turnId: "",
9947
10134
  startedAt: Date.now(),
9948
10135
  timer: null,
10136
+ graceTimer: null,
10137
+ timedOutAt: null,
9949
10138
  messageOrder: [],
9950
10139
  messageByItemId: new Map,
9951
10140
  resolve: resolve5,
@@ -9953,20 +10142,43 @@ class CodexAppServerSession {
9953
10142
  watchers: []
9954
10143
  };
9955
10144
  turn.timer = setTimeout(() => {
9956
- this.interrupt().catch(() => {
9957
- return;
9958
- });
9959
- if (this.activeTurn === turn) {
9960
- this.activeTurn = null;
9961
- }
9962
- for (const watcher of this.drainTurnWatchers(turn)) {
9963
- watcher.reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
9964
- }
9965
- reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
10145
+ this.scheduleTurnTimeout(turn, timeoutMs);
9966
10146
  }, timeoutMs);
9967
10147
  this.activeTurn = turn;
9968
10148
  return turn;
9969
10149
  }
10150
+ buildTurnTimeoutError(timeoutMs) {
10151
+ return new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`);
10152
+ }
10153
+ scheduleTurnTimeout(turn, timeoutMs) {
10154
+ if (turn.timedOutAt !== null) {
10155
+ return;
10156
+ }
10157
+ turn.timedOutAt = Date.now();
10158
+ this.interrupt().catch(() => {
10159
+ return;
10160
+ });
10161
+ const graceMs = resolveCodexCompletionGraceMs();
10162
+ if (graceMs <= 0) {
10163
+ this.rejectTimedOutTurn(turn, timeoutMs);
10164
+ return;
10165
+ }
10166
+ turn.graceTimer = setTimeout(() => {
10167
+ this.rejectTimedOutTurn(turn, timeoutMs);
10168
+ }, graceMs);
10169
+ }
10170
+ rejectTimedOutTurn(turn, timeoutMs) {
10171
+ if (this.activeTurn !== turn) {
10172
+ this.clearActiveTurn(turn);
10173
+ return;
10174
+ }
10175
+ this.clearActiveTurn(turn);
10176
+ const error = this.buildTurnTimeoutError(timeoutMs);
10177
+ for (const watcher of this.drainTurnWatchers(turn)) {
10178
+ watcher.reject(error);
10179
+ }
10180
+ turn.reject(error);
10181
+ }
9970
10182
  addTurnWatcher(turn, resolve5, reject, timeoutMs) {
9971
10183
  const watcher = {
9972
10184
  resolve: resolve5,
@@ -10000,6 +10212,9 @@ class CodexAppServerSession {
10000
10212
  if (turn.timer) {
10001
10213
  clearTimeout(turn.timer);
10002
10214
  }
10215
+ if (turn.graceTimer) {
10216
+ clearTimeout(turn.graceTimer);
10217
+ }
10003
10218
  if (this.activeTurn === turn) {
10004
10219
  this.activeTurn = null;
10005
10220
  }
@@ -10435,7 +10650,7 @@ function buildInvocationCollaborationContextPrompt(invocation) {
10435
10650
 
10436
10651
  // packages/runtime/src/broker-service.ts
10437
10652
  import { spawnSync } from "child_process";
10438
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
10653
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
10439
10654
  import { homedir as homedir11 } from "os";
10440
10655
  import { basename as basename4, dirname as dirname10, join as join15, resolve as resolve5 } from "path";
10441
10656
  import { fileURLToPath as fileURLToPath5 } from "url";
@@ -10474,7 +10689,7 @@ function runtimePackageDir() {
10474
10689
  return resolve5(moduleDir, "..");
10475
10690
  }
10476
10691
  function isInstalledRuntimePackageDir(candidate) {
10477
- return existsSync9(join15(candidate, "package.json")) && existsSync9(join15(candidate, "bin", "openscout-runtime.mjs"));
10692
+ return existsSync10(join15(candidate, "package.json")) && existsSync10(join15(candidate, "bin", "openscout-runtime.mjs"));
10478
10693
  }
10479
10694
  function findGlobalRuntimeDir() {
10480
10695
  const candidates = [
@@ -10505,7 +10720,7 @@ function findWorkspaceRuntimeDir(startDir) {
10505
10720
  let current = resolve5(startDir);
10506
10721
  while (true) {
10507
10722
  const candidate = join15(current, "packages", "runtime");
10508
- if (existsSync9(join15(candidate, "package.json")) && existsSync9(join15(candidate, "src"))) {
10723
+ if (existsSync10(join15(candidate, "package.json")) && existsSync10(join15(candidate, "src"))) {
10509
10724
  return candidate;
10510
10725
  }
10511
10726
  const parent = dirname10(current);
@@ -10519,18 +10734,18 @@ function resolveBunExecutable2() {
10519
10734
  if (explicit && explicit.trim().length > 0) {
10520
10735
  return explicit;
10521
10736
  }
10522
- if (basename4(process.execPath).startsWith("bun") && existsSync9(process.execPath)) {
10737
+ if (basename4(process.execPath).startsWith("bun") && existsSync10(process.execPath)) {
10523
10738
  return process.execPath;
10524
10739
  }
10525
10740
  const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
10526
10741
  for (const entry of pathEntries) {
10527
10742
  const candidate = join15(entry, "bun");
10528
- if (existsSync9(candidate)) {
10743
+ if (existsSync10(candidate)) {
10529
10744
  return candidate;
10530
10745
  }
10531
10746
  }
10532
10747
  const homeBun = join15(homedir11(), ".bun", "bin", "bun");
10533
- if (existsSync9(homeBun)) {
10748
+ if (existsSync10(homeBun)) {
10534
10749
  return homeBun;
10535
10750
  }
10536
10751
  return "bun";
@@ -10720,10 +10935,10 @@ function launchctlPath() {
10720
10935
  return "/bin/launchctl";
10721
10936
  }
10722
10937
  function readLogLines(path) {
10723
- if (!existsSync9(path)) {
10938
+ if (!existsSync10(path)) {
10724
10939
  return [];
10725
10940
  }
10726
- return readFileSync5(path, "utf8").split(`
10941
+ return readFileSync6(path, "utf8").split(`
10727
10942
  `).map((line) => line.trim()).filter(Boolean);
10728
10943
  }
10729
10944
  function isPackageScriptBanner(line) {
@@ -10820,7 +11035,7 @@ async function brokerServiceStatus(config = resolveBrokerServiceConfig()) {
10820
11035
  ensureServiceDirectories(config);
10821
11036
  const launchctl = inspectLaunchctl(config);
10822
11037
  const health = await fetchHealthSnapshot(config);
10823
- const installed = existsSync9(config.launchAgentPath);
11038
+ const installed = existsSync10(config.launchAgentPath);
10824
11039
  const lastLogLine = health.reachable ? readLastLogLine([config.stdoutLogPath, config.stderrLogPath]) : readLastLogLine([config.stderrLogPath, config.stdoutLogPath]);
10825
11040
  return {
10826
11041
  label: config.label,
@@ -10882,7 +11097,7 @@ async function restartBrokerService(config = resolveBrokerServiceConfig()) {
10882
11097
  }
10883
11098
  async function uninstallBrokerService(config = resolveBrokerServiceConfig()) {
10884
11099
  await stopBrokerService(config);
10885
- if (existsSync9(config.launchAgentPath)) {
11100
+ if (existsSync10(config.launchAgentPath)) {
10886
11101
  rmSync2(config.launchAgentPath, { force: true });
10887
11102
  }
10888
11103
  return brokerServiceStatus(config);
@@ -10975,6 +11190,7 @@ __export(exports_local_agents, {
10975
11190
  resolveLocalAgentIdentity: () => resolveLocalAgentIdentity,
10976
11191
  resolveLocalAgentByName: () => resolveLocalAgentByName,
10977
11192
  renderLocalAgentSystemPromptTemplate: () => renderLocalAgentSystemPromptTemplate,
11193
+ normalizeLocalAgentSystemPrompt: () => normalizeLocalAgentSystemPrompt,
10978
11194
  loadRegisteredLocalAgentBindings: () => loadRegisteredLocalAgentBindings,
10979
11195
  listLocalAgents: () => listLocalAgents,
10980
11196
  isLocalAgentSessionAlive: () => isLocalAgentSessionAlive,
@@ -10998,7 +11214,7 @@ __export(exports_local_agents, {
10998
11214
  });
10999
11215
  import { randomUUID as randomUUID2 } from "crypto";
11000
11216
  import { execFileSync as execFileSync2, execSync } from "child_process";
11001
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
11217
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
11002
11218
  import { mkdir as mkdir6, rm as rm5, stat as stat4, writeFile as writeFile6 } from "fs/promises";
11003
11219
  import { basename as basename5, dirname as dirname11, join as join16, resolve as resolve6 } from "path";
11004
11220
  import { fileURLToPath as fileURLToPath6 } from "url";
@@ -11011,10 +11227,10 @@ function resolveProjectsRoot(projectPath) {
11011
11227
  }
11012
11228
  try {
11013
11229
  const supportPaths = resolveOpenScoutSupportPaths();
11014
- if (!existsSync10(supportPaths.settingsPath)) {
11230
+ if (!existsSync11(supportPaths.settingsPath)) {
11015
11231
  return dirname11(projectPath);
11016
11232
  }
11017
- const raw2 = JSON.parse(readFileSync6(supportPaths.settingsPath, "utf8"));
11233
+ const raw2 = JSON.parse(readFileSync7(supportPaths.settingsPath, "utf8"));
11018
11234
  const workspaceRoot = raw2.discovery?.workspaceRoots?.find((entry) => typeof entry === "string" && entry.trim().length > 0);
11019
11235
  return workspaceRoot ? resolve6(workspaceRoot) : dirname11(projectPath);
11020
11236
  } catch {
@@ -11030,8 +11246,14 @@ function nowSeconds2() {
11030
11246
  function normalizeBrokerTimestamp(value) {
11031
11247
  return value > 10000000000 ? Math.floor(value / 1000) : value;
11032
11248
  }
11249
+ function scoutCliPath() {
11250
+ return join16(OPENSCOUT_REPO_ROOT, "packages", "cli", "bin", "scout.mjs");
11251
+ }
11252
+ function legacyNodeBrokerRelayCommand() {
11253
+ return `node ${JSON.stringify(scoutCliPath())}`;
11254
+ }
11033
11255
  function brokerRelayCommand() {
11034
- return `node ${JSON.stringify(join16(OPENSCOUT_REPO_ROOT, "packages", "cli", "bin", "scout.mjs"))}`;
11256
+ return `bun ${JSON.stringify(scoutCliPath())}`;
11035
11257
  }
11036
11258
  function sleep3(ms) {
11037
11259
  return new Promise((resolve7) => setTimeout(resolve7, ms));
@@ -11053,7 +11275,7 @@ function resolveScoutSkillPath() {
11053
11275
  join16(process.env.HOME ?? "", ".agents", "skills", "relay-agent-comms", "SKILL.md")
11054
11276
  ];
11055
11277
  for (const path of candidatePaths) {
11056
- if (existsSync10(path)) {
11278
+ if (existsSync11(path)) {
11057
11279
  return path;
11058
11280
  }
11059
11281
  }
@@ -11249,7 +11471,7 @@ function buildLegacyBrokerBackedRelayPrompt(agentId, projectName, projectPath, r
11249
11471
  function legacyLocalAgentSystemPromptCandidates(agentId, projectName, projectPath) {
11250
11472
  const relayHub = resolveRelayHub();
11251
11473
  const brokerUrl = resolveBrokerUrl();
11252
- const relayCommandBases = ["openscout relay", brokerRelayCommand()];
11474
+ const relayCommandBases = ["openscout relay", brokerRelayCommand(), legacyNodeBrokerRelayCommand()];
11253
11475
  const projectPathCandidates = projectPath.endsWith("/") ? [projectPath, projectPath.slice(0, -1)] : [projectPath, `${projectPath}/`];
11254
11476
  const candidates = new Set;
11255
11477
  for (const pathCandidate of projectPathCandidates) {
@@ -11260,12 +11482,28 @@ function legacyLocalAgentSystemPromptCandidates(agentId, projectName, projectPat
11260
11482
  }
11261
11483
  return Array.from(candidates);
11262
11484
  }
11485
+ function generatedLocalAgentSystemPromptCandidates(agentId, projectName, projectPath) {
11486
+ const baseContext = buildLocalAgentTemplateContext(agentId, projectName, projectPath);
11487
+ const relayCommands = [brokerRelayCommand(), legacyNodeBrokerRelayCommand()];
11488
+ const transportModes = [undefined, "codex_app_server", "claude_stream_json"];
11489
+ const candidates = new Set;
11490
+ for (const relayCommand of relayCommands) {
11491
+ const context = {
11492
+ ...baseContext,
11493
+ relayCommand
11494
+ };
11495
+ for (const transport of transportModes) {
11496
+ candidates.add(renderLocalAgentSystemPromptTemplate(buildLocalAgentSystemPromptTemplate(), context, transport ? { transport } : {}));
11497
+ }
11498
+ }
11499
+ return Array.from(candidates);
11500
+ }
11263
11501
  function normalizeLocalAgentSystemPrompt(agentId, projectName, projectPath, systemPrompt) {
11264
11502
  const trimmed = systemPrompt?.trim();
11265
11503
  if (!trimmed) {
11266
11504
  return;
11267
11505
  }
11268
- if (legacyLocalAgentSystemPromptCandidates(agentId, projectName, projectPath).includes(trimmed)) {
11506
+ if (legacyLocalAgentSystemPromptCandidates(agentId, projectName, projectPath).includes(trimmed) || generatedLocalAgentSystemPromptCandidates(agentId, projectName, projectPath).includes(trimmed)) {
11269
11507
  return;
11270
11508
  }
11271
11509
  return trimmed;
@@ -11352,6 +11590,128 @@ function normalizeLocalAgentCapabilities(value) {
11352
11590
  function normalizeLocalAgentLaunchArgs(value) {
11353
11591
  return Array.isArray(value) ? value.map((entry) => String(entry).trim()).filter(Boolean) : [];
11354
11592
  }
11593
+ function normalizeRequestedModel(value) {
11594
+ const trimmed = value?.trim();
11595
+ return trimmed ? trimmed : undefined;
11596
+ }
11597
+ function readClaudeLaunchModel(launchArgs) {
11598
+ for (let index = 0;index < launchArgs.length; index += 1) {
11599
+ const current = launchArgs[index] ?? "";
11600
+ if (current === "--model") {
11601
+ const next = launchArgs[index + 1]?.trim();
11602
+ return next || undefined;
11603
+ }
11604
+ if (current.startsWith("--model=")) {
11605
+ const next = current.slice("--model=".length).trim();
11606
+ return next || undefined;
11607
+ }
11608
+ }
11609
+ return;
11610
+ }
11611
+ function normalizeLaunchArgsForHarness(harness, value) {
11612
+ const normalized = normalizeLocalAgentLaunchArgs(value);
11613
+ if (harness === "codex") {
11614
+ return normalizeCodexAppServerLaunchArgs(normalized);
11615
+ }
11616
+ return normalized;
11617
+ }
11618
+ function readLaunchModelForHarness(harness, launchArgs) {
11619
+ if (harness === "codex") {
11620
+ return readCodexAppServerModelFromLaunchArgs(launchArgs) ?? undefined;
11621
+ }
11622
+ if (harness === "claude") {
11623
+ return readClaudeLaunchModel(launchArgs ?? []);
11624
+ }
11625
+ return;
11626
+ }
11627
+ function stripLaunchModelForHarness(harness, launchArgs) {
11628
+ if (harness === "codex") {
11629
+ const next = [];
11630
+ const normalized = normalizeCodexAppServerLaunchArgs(launchArgs);
11631
+ for (let index = 0;index < normalized.length; index += 1) {
11632
+ const current = normalized[index] ?? "";
11633
+ if (current === "-c" || current === "--config") {
11634
+ const value = normalized[index + 1];
11635
+ if (readCodexAppServerModelFromLaunchArgs(value ? [current, value] : [current])) {
11636
+ index += 1;
11637
+ continue;
11638
+ }
11639
+ next.push(current);
11640
+ if (value) {
11641
+ next.push(value);
11642
+ index += 1;
11643
+ }
11644
+ continue;
11645
+ }
11646
+ if (current.startsWith("--config=")) {
11647
+ if (readCodexAppServerModelFromLaunchArgs([current])) {
11648
+ continue;
11649
+ }
11650
+ }
11651
+ next.push(current);
11652
+ }
11653
+ return next;
11654
+ }
11655
+ if (harness === "claude") {
11656
+ const next = [];
11657
+ const normalized = normalizeLocalAgentLaunchArgs(launchArgs);
11658
+ for (let index = 0;index < normalized.length; index += 1) {
11659
+ const current = normalized[index] ?? "";
11660
+ if (current === "--model") {
11661
+ index += 1;
11662
+ continue;
11663
+ }
11664
+ if (current.startsWith("--model=")) {
11665
+ continue;
11666
+ }
11667
+ next.push(current);
11668
+ }
11669
+ return next;
11670
+ }
11671
+ return normalizeLocalAgentLaunchArgs(launchArgs);
11672
+ }
11673
+ function buildLaunchArgsForRequestedModel(harness, model) {
11674
+ if (harness === "codex") {
11675
+ return normalizeCodexAppServerLaunchArgs(["--model", model]);
11676
+ }
11677
+ if (harness === "claude") {
11678
+ return ["--model", model];
11679
+ }
11680
+ return [];
11681
+ }
11682
+ function applyRequestedModelToLaunchArgs(harness, launchArgs, model) {
11683
+ const normalized = normalizeLaunchArgsForHarness(harness, launchArgs);
11684
+ const requestedModel = normalizeRequestedModel(model);
11685
+ if (!requestedModel) {
11686
+ return normalized;
11687
+ }
11688
+ return [
11689
+ ...stripLaunchModelForHarness(harness, normalized),
11690
+ ...buildLaunchArgsForRequestedModel(harness, requestedModel)
11691
+ ];
11692
+ }
11693
+ function defaultHarnessForOverride(override, fallback = "claude") {
11694
+ return normalizeManagedHarness2(override.defaultHarness ?? override.runtime?.harness, fallback);
11695
+ }
11696
+ function launchArgsForOverrideHarness(override, harness) {
11697
+ const profileLaunchArgs = override.harnessProfiles?.[harness]?.launchArgs;
11698
+ if (profileLaunchArgs) {
11699
+ return normalizeLaunchArgsForHarness(harness, profileLaunchArgs);
11700
+ }
11701
+ if (harness === defaultHarnessForOverride(override, harness)) {
11702
+ return normalizeLaunchArgsForHarness(harness, override.launchArgs);
11703
+ }
11704
+ return [];
11705
+ }
11706
+ function overrideHarnessProfile(override, harness) {
11707
+ const profile = override.harnessProfiles?.[harness];
11708
+ return {
11709
+ cwd: normalizeProjectPath(profile?.cwd || override.runtime?.cwd || override.projectRoot || process.cwd()),
11710
+ transport: normalizeLocalAgentTransport(profile?.transport ?? override.runtime?.transport, harness),
11711
+ sessionId: normalizeTmuxSessionName2(profile?.sessionId, `${override.agentId}-${harness}`),
11712
+ launchArgs: launchArgsForOverrideHarness(override, harness)
11713
+ };
11714
+ }
11355
11715
  function normalizeManagedHarness2(value, fallback) {
11356
11716
  return value === "codex" ? "codex" : value === "claude" ? "claude" : fallback;
11357
11717
  }
@@ -11367,7 +11727,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
11367
11727
  cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
11368
11728
  transport: normalizeLocalAgentTransport(profile.transport, harness),
11369
11729
  sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
11370
- launchArgs: normalizeLocalAgentLaunchArgs(profile.launchArgs)
11730
+ launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
11371
11731
  };
11372
11732
  }
11373
11733
  const runtimeHarness = normalizeManagedHarness2(record.harness, defaultHarness);
@@ -11376,7 +11736,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
11376
11736
  cwd: normalizeProjectPath(record.cwd || process.cwd()),
11377
11737
  transport: normalizeLocalAgentTransport(record.transport, runtimeHarness),
11378
11738
  sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${runtimeHarness}`),
11379
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
11739
+ launchArgs: normalizeLaunchArgsForHarness(runtimeHarness, record.launchArgs)
11380
11740
  };
11381
11741
  }
11382
11742
  if (!nextProfiles[defaultHarness]) {
@@ -11384,7 +11744,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
11384
11744
  cwd: normalizeProjectPath(record.cwd || process.cwd()),
11385
11745
  transport: normalizeLocalAgentTransport(record.transport, defaultHarness),
11386
11746
  sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${defaultHarness}`),
11387
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
11747
+ launchArgs: normalizeLaunchArgsForHarness(defaultHarness, record.launchArgs)
11388
11748
  };
11389
11749
  }
11390
11750
  for (const harness of ["claude", "codex"]) {
@@ -11396,7 +11756,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
11396
11756
  cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
11397
11757
  transport: normalizeLocalAgentTransport(profile.transport, harness),
11398
11758
  sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
11399
- launchArgs: normalizeLocalAgentLaunchArgs(profile.launchArgs)
11759
+ launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
11400
11760
  };
11401
11761
  }
11402
11762
  return nextProfiles;
@@ -11420,14 +11780,14 @@ function recordForHarness(record, harnessOverride) {
11420
11780
  tmuxSession: fallbackSessionId,
11421
11781
  cwd: fallbackCwd,
11422
11782
  transport: fallbackTransport,
11423
- launchArgs: normalizeLocalAgentLaunchArgs(normalized.launchArgs),
11783
+ launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs),
11424
11784
  harnessProfiles: {
11425
11785
  ...normalized.harnessProfiles,
11426
11786
  [selectedHarness]: {
11427
11787
  cwd: fallbackCwd,
11428
11788
  transport: fallbackTransport,
11429
11789
  sessionId: fallbackSessionId,
11430
- launchArgs: normalizeLocalAgentLaunchArgs(normalized.launchArgs)
11790
+ launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs)
11431
11791
  }
11432
11792
  }
11433
11793
  };
@@ -11469,7 +11829,7 @@ function normalizeLocalAgentRecord(agentId, record) {
11469
11829
  harnessProfiles,
11470
11830
  transport: activeProfile?.transport ?? normalizeLocalAgentTransport(record.transport, harness),
11471
11831
  capabilities: normalizeLocalAgentCapabilities(record.capabilities),
11472
- launchArgs: activeProfile?.launchArgs ?? normalizeLocalAgentLaunchArgs(record.launchArgs)
11832
+ launchArgs: activeProfile?.launchArgs ?? normalizeLaunchArgsForHarness(harness, record.launchArgs)
11473
11833
  };
11474
11834
  }
11475
11835
  function localAgentStatusFromRecord(agentId, record, source) {
@@ -11538,7 +11898,7 @@ function relayAgentOverrideFromLocalAgentRecord(agentId, record, existing) {
11538
11898
  source: existing?.source && existing.source !== "inferred" ? existing.source : "manual",
11539
11899
  startedAt: normalizedRecord.startedAt,
11540
11900
  systemPrompt: normalizedRecord.systemPrompt,
11541
- launchArgs: normalizeLocalAgentLaunchArgs(normalizedRecord.launchArgs),
11901
+ launchArgs: normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs),
11542
11902
  capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
11543
11903
  defaultHarness: normalizeManagedHarness2(normalizedRecord.defaultHarness, "claude"),
11544
11904
  harnessProfiles: normalizedRecord.harnessProfiles,
@@ -11563,7 +11923,7 @@ function buildLocalAgentConfigState(agentId, record) {
11563
11923
  sessionId: record.tmuxSession,
11564
11924
  wakePolicy: "on_demand"
11565
11925
  },
11566
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs),
11926
+ launchArgs: normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(record.harness), record.launchArgs),
11567
11927
  capabilities: normalizeLocalAgentCapabilities(record.capabilities),
11568
11928
  applyMode: "restart",
11569
11929
  templateHint: LOCAL_AGENT_SYSTEM_PROMPT_TEMPLATE_HINT
@@ -11617,11 +11977,13 @@ async function getLocalAgentEndpointSessionSnapshot(endpoint) {
11617
11977
  async function ensureLocalSessionEndpointOnline(endpoint) {
11618
11978
  if (endpoint.transport === "codex_app_server") {
11619
11979
  await ensureCodexAppServerAgentOnline(buildCodexEndpointSessionOptions(endpoint));
11620
- return;
11980
+ return {};
11621
11981
  }
11622
11982
  if (endpoint.transport === "claude_stream_json") {
11623
- await ensureClaudeStreamJsonAgentOnline(buildClaudeEndpointSessionOptions(endpoint));
11983
+ const result = await ensureClaudeStreamJsonAgentOnline(buildClaudeEndpointSessionOptions(endpoint));
11984
+ return { externalSessionId: result.sessionId };
11624
11985
  }
11986
+ return {};
11625
11987
  }
11626
11988
  async function shutdownLocalSessionEndpoint(endpoint) {
11627
11989
  if (endpoint.transport === "codex_app_server") {
@@ -11687,7 +12049,7 @@ function buildCodexAgentSessionOptions(agentName, record, systemPrompt) {
11687
12049
  systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "codex_app_server" }),
11688
12050
  runtimeDirectory: relayAgentRuntimeDirectory(agentName),
11689
12051
  logsDirectory: relayAgentLogsDirectory(agentName),
11690
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
12052
+ launchArgs: normalizeLaunchArgsForHarness("codex", record.launchArgs)
11691
12053
  };
11692
12054
  }
11693
12055
  function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
@@ -11698,7 +12060,7 @@ function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
11698
12060
  systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "claude_stream_json" }),
11699
12061
  runtimeDirectory: relayAgentRuntimeDirectory(agentName),
11700
12062
  logsDirectory: relayAgentLogsDirectory(agentName),
11701
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
12063
+ launchArgs: normalizeLaunchArgsForHarness("claude", record.launchArgs)
11702
12064
  };
11703
12065
  }
11704
12066
  function endpointMetadataString(endpoint, key) {
@@ -11903,7 +12265,7 @@ function buildLocalAgentBootstrapPrompt(_harness, _systemPrompt, initialMessage)
11903
12265
  return initialMessage;
11904
12266
  }
11905
12267
  function buildLocalAgentLaunchCommand(agentName, record, projectPath, promptFile, workerScript) {
11906
- const extraArgs = shellQuoteArguments(normalizeLocalAgentLaunchArgs(record.launchArgs));
12268
+ const extraArgs = shellQuoteArguments(normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(record.harness), record.launchArgs));
11907
12269
  if (normalizeLocalAgentHarness(record.harness) === "codex") {
11908
12270
  return `exec bash ${JSON.stringify(workerScript ?? join16(relayAgentRuntimeDirectory(agentName), "codex-worker.sh"))}`;
11909
12271
  }
@@ -12059,7 +12421,7 @@ async function ensureLocalAgentOnline(agentName, record) {
12059
12421
  await new Promise((resolve7) => setTimeout(resolve7, 100));
12060
12422
  }
12061
12423
  if (!isLocalAgentSessionAlive(normalizedRecord.tmuxSession)) {
12062
- const stderrTail = existsSync10(stderrLogFile) ? readFileSync6(stderrLogFile, "utf8").trim().split(/\r?\n/).slice(-10).join(`
12424
+ const stderrTail = existsSync11(stderrLogFile) ? readFileSync7(stderrLogFile, "utf8").trim().split(/\r?\n/).slice(-10).join(`
12063
12425
  `).trim() : "";
12064
12426
  throw new Error(stderrTail ? `Relay agent ${agentName} failed to stay online:
12065
12427
  ${stderrTail}` : `Relay agent ${agentName} failed to stay online.`);
@@ -12275,6 +12637,7 @@ async function startLocalAgent(input) {
12275
12637
  const effectiveHarness = normalizeManagedHarness2(preferredHarness ?? configDefaultHarness, "claude");
12276
12638
  const transport = normalizeLocalAgentTransport(undefined, effectiveHarness);
12277
12639
  const sessionId = normalizeTmuxSessionName2(undefined, `${instance.id}-${effectiveHarness}`);
12640
+ const launchArgs = applyRequestedModelToLaunchArgs(effectiveHarness, [], input.model);
12278
12641
  const existingForInstance = overrides[instance.id];
12279
12642
  if (existingForInstance && normalizeProjectPath(existingForInstance.projectRoot) !== projectRoot) {
12280
12643
  throw new Error(`Another agent is already registered as ${instance.id} at ${existingForInstance.projectRoot}. ` + `Two clones of the same project on the same branch cannot both register here; ` + `rename one of the checkouts or switch its branch before running scout up.`);
@@ -12288,13 +12651,14 @@ async function startLocalAgent(input) {
12288
12651
  projectConfigPath: coldProjectConfigPath,
12289
12652
  source: "manual",
12290
12653
  startedAt: nowSeconds2(),
12654
+ launchArgs,
12291
12655
  defaultHarness: effectiveHarness,
12292
12656
  harnessProfiles: {
12293
12657
  [effectiveHarness]: {
12294
12658
  cwd: effectiveCwd ?? projectRoot,
12295
12659
  transport,
12296
12660
  sessionId,
12297
- launchArgs: []
12661
+ launchArgs
12298
12662
  }
12299
12663
  },
12300
12664
  runtime: {
@@ -12315,6 +12679,9 @@ async function startLocalAgent(input) {
12315
12679
  throw new Error(`Another agent is already registered as ${instance.id} at ${existingForInstance.projectRoot}. ` + `Two clones of the same project on the same branch cannot both register here; ` + `rename one of the checkouts or switch its branch before running scout up.`);
12316
12680
  }
12317
12681
  const resolvedHarness = preferredHarness ?? matchingOverride.runtime?.harness;
12682
+ const nextHarness = normalizeManagedHarness2(resolvedHarness, "claude");
12683
+ const nextDefaultHarness = preferredHarness ? normalizeManagedHarness2(preferredHarness, "claude") : defaultHarnessForOverride(matchingOverride, "claude");
12684
+ const nextLaunchArgs = applyRequestedModelToLaunchArgs(nextHarness, launchArgsForOverrideHarness(matchingOverride, nextHarness), input.model);
12318
12685
  overrides[instance.id] = {
12319
12686
  agentId: instance.id,
12320
12687
  definitionId: requestedDefinitionId,
@@ -12325,10 +12692,16 @@ async function startLocalAgent(input) {
12325
12692
  source: "manual",
12326
12693
  startedAt: matchingOverride.startedAt ?? nowSeconds2(),
12327
12694
  systemPrompt: matchingOverride.systemPrompt,
12328
- launchArgs: matchingOverride.launchArgs,
12695
+ launchArgs: nextHarness === nextDefaultHarness ? nextLaunchArgs : matchingOverride.launchArgs,
12329
12696
  capabilities: matchingOverride.capabilities,
12330
- defaultHarness: normalizeManagedHarness2(preferredHarness ?? matchingOverride.defaultHarness, "claude"),
12331
- harnessProfiles: matchingOverride.harnessProfiles,
12697
+ defaultHarness: nextDefaultHarness,
12698
+ harnessProfiles: {
12699
+ ...matchingOverride.harnessProfiles ?? {},
12700
+ [nextHarness]: {
12701
+ ...overrideHarnessProfile(matchingOverride, nextHarness),
12702
+ launchArgs: nextLaunchArgs
12703
+ }
12704
+ },
12332
12705
  runtime: {
12333
12706
  cwd: effectiveCwd ?? matchingOverride.runtime?.cwd ?? matchingProjectRoot,
12334
12707
  harness: resolvedHarness,
@@ -12340,6 +12713,22 @@ async function startLocalAgent(input) {
12340
12713
  await writeRelayAgentOverrides(overrides);
12341
12714
  targetAgentId = instance.id;
12342
12715
  } else {
12716
+ if (input.model) {
12717
+ const existingHarness = normalizeManagedHarness2(preferredHarness ?? matchingOverride.defaultHarness ?? matchingOverride.runtime?.harness, "claude");
12718
+ const nextLaunchArgs = applyRequestedModelToLaunchArgs(existingHarness, launchArgsForOverrideHarness(matchingOverride, existingHarness), input.model);
12719
+ overrides[matchingAgentId] = {
12720
+ ...matchingOverride,
12721
+ launchArgs: existingHarness === defaultHarnessForOverride(matchingOverride, existingHarness) ? nextLaunchArgs : matchingOverride.launchArgs,
12722
+ harnessProfiles: {
12723
+ ...matchingOverride.harnessProfiles ?? {},
12724
+ [existingHarness]: {
12725
+ ...overrideHarnessProfile(matchingOverride, existingHarness),
12726
+ launchArgs: nextLaunchArgs
12727
+ }
12728
+ }
12729
+ };
12730
+ await writeRelayAgentOverrides(overrides);
12731
+ }
12343
12732
  targetAgentId = matchingAgentId;
12344
12733
  }
12345
12734
  await ensureLocalAgentBindingOnline(targetAgentId, process.env.OPENSCOUT_NODE_ID ?? "local", {
@@ -12427,13 +12816,38 @@ async function restartAllLocalAgents(options = {}) {
12427
12816
  }));
12428
12817
  return restarted.filter((agent) => Boolean(agent));
12429
12818
  }
12819
+ function readPersistedClaudeSessionId(agentName) {
12820
+ try {
12821
+ const runtimeDir = relayAgentRuntimeDirectory(agentName);
12822
+ const catalogPath = join16(runtimeDir, "session-catalog.json");
12823
+ if (existsSync11(catalogPath)) {
12824
+ const raw2 = readFileSync7(catalogPath, "utf8").trim();
12825
+ if (raw2) {
12826
+ const catalog = JSON.parse(raw2);
12827
+ if (typeof catalog.activeSessionId === "string" && catalog.activeSessionId.trim()) {
12828
+ return catalog.activeSessionId.trim();
12829
+ }
12830
+ }
12831
+ }
12832
+ const legacyPath = join16(runtimeDir, "claude-session-id.txt");
12833
+ if (existsSync11(legacyPath)) {
12834
+ const value = readFileSync7(legacyPath, "utf8").trim();
12835
+ return value || null;
12836
+ }
12837
+ return null;
12838
+ } catch {
12839
+ return null;
12840
+ }
12841
+ }
12430
12842
  function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
12431
12843
  const normalizedRecord = normalizeLocalAgentRecord(agentId, record);
12844
+ const configuredModel = readLaunchModelForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs);
12432
12845
  const definitionId = normalizedRecord.definitionId ?? agentId;
12433
12846
  const displayName = titleCaseLocalAgentName(definitionId);
12434
12847
  const projectRoot = normalizedRecord.projectRoot ?? normalizedRecord.cwd;
12435
12848
  const instance = buildRelayAgentInstance(definitionId, projectRoot);
12436
12849
  const actorId = instance.id;
12850
+ const externalSessionId = normalizedRecord.transport === "claude_stream_json" ? readPersistedClaudeSessionId(definitionId) : null;
12437
12851
  return {
12438
12852
  actor: {
12439
12853
  id: actorId,
@@ -12452,7 +12866,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
12452
12866
  defaultSelector: instance.defaultSelector,
12453
12867
  nodeQualifier: instance.nodeQualifier,
12454
12868
  workspaceQualifier: instance.workspaceQualifier,
12455
- branch: instance.branch
12869
+ branch: instance.branch,
12870
+ ...configuredModel ? { model: configuredModel } : {}
12456
12871
  }
12457
12872
  },
12458
12873
  agent: {
@@ -12479,7 +12894,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
12479
12894
  defaultSelector: instance.defaultSelector,
12480
12895
  nodeQualifier: instance.nodeQualifier,
12481
12896
  workspaceQualifier: instance.workspaceQualifier,
12482
- branch: instance.branch
12897
+ branch: instance.branch,
12898
+ ...configuredModel ? { model: configuredModel } : {}
12483
12899
  },
12484
12900
  agentClass: "general",
12485
12901
  capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
@@ -12511,7 +12927,9 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
12511
12927
  selector: instance.selector,
12512
12928
  nodeQualifier: instance.nodeQualifier,
12513
12929
  workspaceQualifier: instance.workspaceQualifier,
12514
- branch: instance.branch
12930
+ branch: instance.branch,
12931
+ ...configuredModel ? { model: configuredModel } : {},
12932
+ ...externalSessionId ? { externalSessionId } : {}
12515
12933
  }
12516
12934
  }
12517
12935
  };
@@ -12711,12 +13129,12 @@ var init_local_agents = __esm(async () => {
12711
13129
  });
12712
13130
 
12713
13131
  // packages/web/server/index.ts
12714
- import { existsSync as existsSync14 } from "fs";
12715
- import { dirname as dirname13, join as join21, resolve as resolve10 } from "path";
12716
- import { fileURLToPath as fileURLToPath8 } from "url";
13132
+ import { existsSync as existsSync15 } from "fs";
13133
+ import { dirname as dirname14, join as join20, resolve as resolve11 } from "path";
13134
+ import { fileURLToPath as fileURLToPath9 } from "url";
12717
13135
 
12718
13136
  // packages/web/server/create-openscout-web-server.ts
12719
- import { existsSync as existsSync12, rmSync as rmSync3 } from "fs";
13137
+ import { existsSync as existsSync13, rmSync as rmSync3 } from "fs";
12720
13138
  import { dirname as dirname12, resolve as resolve9 } from "path";
12721
13139
  import { fileURLToPath as fileURLToPath7 } from "url";
12722
13140
 
@@ -15487,19 +15905,15 @@ function resolveDbPath() {
15487
15905
  return join12(controlHome, "control-plane.sqlite");
15488
15906
  }
15489
15907
  var _db = null;
15490
- var _dbOpenedAt = 0;
15491
- var DB_REOPEN_MS = 2000;
15908
+ var DB_BUSY_TIMEOUT_MS = 250;
15909
+ function configureReadonlyDb(db) {
15910
+ db.exec(`PRAGMA busy_timeout = ${DB_BUSY_TIMEOUT_MS}`);
15911
+ db.exec("PRAGMA query_only = ON");
15912
+ }
15492
15913
  function db() {
15493
- const now = Date.now();
15494
- if (_db && now - _dbOpenedAt > DB_REOPEN_MS) {
15495
- _db.close();
15496
- _db = null;
15497
- }
15498
15914
  if (!_db) {
15499
15915
  _db = new Database(resolveDbPath(), { readonly: true });
15500
- _db.exec("PRAGMA busy_timeout = 5000");
15501
- _db.exec("PRAGMA journal_mode = WAL");
15502
- _dbOpenedAt = now;
15916
+ configureReadonlyDb(_db);
15503
15917
  }
15504
15918
  return _db;
15505
15919
  }
@@ -15536,7 +15950,7 @@ function resolveHarnessSessionId(transport, endpointSessionId, metadata) {
15536
15950
  return metadataString(metadata, "threadId") ?? endpointSessionId;
15537
15951
  }
15538
15952
  if (transport === "claude_stream_json") {
15539
- return endpointSessionId ?? metadataString(metadata, "externalSessionId");
15953
+ return metadataString(metadata, "externalSessionId") ?? endpointSessionId;
15540
15954
  }
15541
15955
  return null;
15542
15956
  }
@@ -15620,6 +16034,12 @@ function transientBrokerWorkingStatusPredicate(alias) {
15620
16034
  AND ${alias}.body LIKE '% is working.'
15621
16035
  )`;
15622
16036
  }
16037
+ function staleFlightActivityPredicate(alias) {
16038
+ return `NOT (
16039
+ ${alias}.kind = 'flight_updated'
16040
+ AND COALESCE(${alias}.summary, '') LIKE 'Stale running flight reconciled:%'
16041
+ )`;
16042
+ }
15623
16043
  function workPhaseFromFlightState(state) {
15624
16044
  switch (state) {
15625
16045
  case "queued":
@@ -15783,10 +16203,7 @@ function queryActivity(limit = 60) {
15783
16203
  FROM activity_items ai
15784
16204
  LEFT JOIN actors ac ON ac.id = ai.actor_id
15785
16205
  WHERE ai.kind != 'ask_replied'
15786
- AND NOT (
15787
- ai.kind = 'flight_updated'
15788
- AND COALESCE(ai.summary, '') LIKE 'Stale running flight reconciled:%'
15789
- )
16206
+ AND ${staleFlightActivityPredicate("ai")}
15790
16207
  ORDER BY ai.ts DESC
15791
16208
  LIMIT ?`).all(limit);
15792
16209
  const items = rows.map((r) => ({
@@ -16512,6 +16929,7 @@ function queryFleetActivity(opts) {
16512
16929
  filters.push("ai.conversation_id = ?");
16513
16930
  params.push(opts.conversationId);
16514
16931
  }
16932
+ const scopedFilters = sqlJoinClauses(filters, "OR");
16515
16933
  const sql = `SELECT
16516
16934
  ai.id,
16517
16935
  ai.kind,
@@ -16530,7 +16948,10 @@ function queryFleetActivity(opts) {
16530
16948
  ai.session_id
16531
16949
  FROM activity_items ai
16532
16950
  LEFT JOIN actors ac ON ac.id = ai.actor_id
16533
- ${sqlWhereClause(filters, "OR")}
16951
+ ${sqlWhereClause([
16952
+ staleFlightActivityPredicate("ai"),
16953
+ scopedFilters ? `(${scopedFilters})` : null
16954
+ ])}
16534
16955
  ORDER BY ai.ts DESC
16535
16956
  LIMIT ?`;
16536
16957
  const rows = db().prepare(sql).all(...params, opts?.limit ?? 80);
@@ -16613,6 +17034,10 @@ function queryFleetAskRows(requesterIds, limit) {
16613
17034
  )
16614
17035
  LEFT JOIN collaboration_records cr ON cr.id = inv.collaboration_record_id
16615
17036
  WHERE inv.requester_id IN (${requesterClause})
17037
+ AND NOT (
17038
+ COALESCE(f.state, '') = 'failed'
17039
+ AND COALESCE(f.error, '') LIKE 'Stale running flight reconciled:%'
17040
+ )
16616
17041
  ORDER BY COALESCE(f.completed_at, f.started_at, inv.created_at) DESC
16617
17042
  LIMIT ?`).all(...requesterIds, limit);
16618
17043
  }
@@ -16714,125 +17139,67 @@ function queryFleet(opts) {
16714
17139
  activity
16715
17140
  };
16716
17141
  }
16717
- var WINDOW_TIERS = [
16718
- { ms: 30 * 60000, label: "30m" },
16719
- { ms: 60 * 60000, label: "1h" },
16720
- { ms: 6 * 60 * 60000, label: "6h" },
16721
- { ms: 24 * 60 * 60000, label: "24h" },
16722
- { ms: 7 * 24 * 60 * 60000, label: "7d" }
16723
- ];
16724
- var MIN_FILLED_BUCKETS = 3;
16725
- var _sealedCounts = new Map;
16726
- var _cachedTier = null;
16727
- function queryHeartrate(numBuckets = 48) {
16728
- const nowMs = Date.now();
16729
- let chosenTier = WINDOW_TIERS[WINDOW_TIERS.length - 1];
16730
- for (const tier of WINDOW_TIERS) {
16731
- const bucketMs2 = tier.ms / numBuckets;
16732
- const currentBucketStart2 = Math.floor(nowMs / bucketMs2) * bucketMs2;
16733
- const alignedStart2 = currentBucketStart2 - (numBuckets - 1) * bucketMs2;
16734
- if (_cachedTier === tier.label) {
16735
- let sealedHits = 0;
16736
- let filledSealed = 0;
16737
- for (let i = 0;i < numBuckets - 1; i++) {
16738
- const key = String(alignedStart2 + i * bucketMs2);
16739
- if (_sealedCounts.has(key)) {
16740
- sealedHits++;
16741
- if (_sealedCounts.get(key) > 0)
16742
- filledSealed++;
16743
- }
16744
- }
16745
- if (sealedHits === numBuckets - 1 && filledSealed >= MIN_FILLED_BUCKETS) {
16746
- chosenTier = tier;
16747
- break;
16748
- }
16749
- }
16750
- const rows2 = db().prepare(`SELECT ts FROM activity_items WHERE ts >= ? ORDER BY ts ASC`).all(alignedStart2);
16751
- const counts2 = new Array(numBuckets).fill(0);
16752
- for (const row of rows2) {
16753
- const ms = row.ts < 1000000000000 ? row.ts * 1000 : row.ts;
16754
- if (ms < alignedStart2)
17142
+ var HEARTRATE_WINDOW_MS = 7 * 24 * 60 * 60000;
17143
+ var DEFAULT_HEARTRATE_BUCKETS = 56;
17144
+ var SQLITE_MILLISECONDS_THRESHOLD = 1000000000000;
17145
+ function normalizeActivityTimestampMs(ts) {
17146
+ return ts < SQLITE_MILLISECONDS_THRESHOLD ? ts * 1000 : ts;
17147
+ }
17148
+ function smoothHeartrateCounts(counts) {
17149
+ const energy = counts.map((count) => Math.sqrt(count));
17150
+ const weights = [0.56, 0.28, 0.11, 0.05];
17151
+ return energy.map((_, index) => {
17152
+ let total = 0;
17153
+ let weightTotal = 0;
17154
+ for (let offset = -3;offset <= 3; offset++) {
17155
+ const nextIndex = index + offset;
17156
+ if (nextIndex < 0 || nextIndex >= energy.length)
16755
17157
  continue;
16756
- const idx = Math.min(numBuckets - 1, Math.floor((ms - alignedStart2) / bucketMs2));
16757
- if (idx >= 0)
16758
- counts2[idx]++;
16759
- }
16760
- const filled = counts2.filter((c) => c > 0).length;
16761
- if (filled >= MIN_FILLED_BUCKETS) {
16762
- if (_cachedTier !== tier.label) {
16763
- _sealedCounts.clear();
16764
- _cachedTier = tier.label;
16765
- }
16766
- for (let i = 0;i < numBuckets - 1; i++) {
16767
- _sealedCounts.set(String(alignedStart2 + i * bucketMs2), counts2[i]);
16768
- }
16769
- const peak2 = Math.max(1, ...counts2);
16770
- return {
16771
- windowLabel: tier.label,
16772
- buckets: counts2.map((count, i) => ({
16773
- ts: Math.round(alignedStart2 + i * bucketMs2),
16774
- count,
16775
- value: count / peak2
16776
- }))
16777
- };
17158
+ const weight = weights[Math.abs(offset)] ?? 0;
17159
+ total += energy[nextIndex] * weight;
17160
+ weightTotal += weight;
16778
17161
  }
17162
+ return weightTotal > 0 ? total / weightTotal : 0;
17163
+ });
17164
+ }
17165
+ function formatHeartrateBucketLabel(bucketMs) {
17166
+ const minutes = Math.round(bucketMs / 60000);
17167
+ if (minutes % (24 * 60) === 0) {
17168
+ return `${minutes / (24 * 60)}d buckets`;
17169
+ }
17170
+ if (minutes % 60 === 0) {
17171
+ return `${minutes / 60}h buckets`;
16779
17172
  }
16780
- const bucketMs = chosenTier.ms / numBuckets;
17173
+ return `${minutes}m buckets`;
17174
+ }
17175
+ function queryHeartrate(numBuckets = DEFAULT_HEARTRATE_BUCKETS, nowMs = Date.now()) {
17176
+ const bucketMs = HEARTRATE_WINDOW_MS / numBuckets;
16781
17177
  const currentBucketStart = Math.floor(nowMs / bucketMs) * bucketMs;
16782
17178
  const alignedStart = currentBucketStart - (numBuckets - 1) * bucketMs;
16783
- if (_cachedTier === chosenTier.label) {
16784
- const counts2 = new Array(numBuckets).fill(0);
16785
- let allSealed = true;
16786
- for (let i = 0;i < numBuckets - 1; i++) {
16787
- const key = String(alignedStart + i * bucketMs);
16788
- if (_sealedCounts.has(key)) {
16789
- counts2[i] = _sealedCounts.get(key);
16790
- } else {
16791
- allSealed = false;
16792
- break;
16793
- }
16794
- }
16795
- if (allSealed) {
16796
- const liveStart = alignedStart + (numBuckets - 1) * bucketMs;
16797
- const row = db().prepare(`SELECT COUNT(*) AS c FROM activity_items WHERE ts >= ?`).get(liveStart);
16798
- counts2[numBuckets - 1] = row.c;
16799
- const peak2 = Math.max(1, ...counts2);
16800
- return {
16801
- windowLabel: chosenTier.label,
16802
- buckets: counts2.map((count, i) => ({
16803
- ts: Math.round(alignedStart + i * bucketMs),
16804
- count,
16805
- value: count / peak2
16806
- }))
16807
- };
16808
- }
16809
- }
16810
- _sealedCounts.clear();
16811
- _cachedTier = chosenTier.label;
16812
- const rows = db().prepare(`SELECT ts FROM activity_items WHERE ts >= ? ORDER BY ts ASC`).all(alignedStart);
17179
+ const alignedStartSeconds = Math.floor(alignedStart / 1000);
17180
+ const rows = db().prepare(`SELECT ts
17181
+ FROM activity_items
17182
+ WHERE ts >= ?1
17183
+ OR (ts < ?2 AND ts >= ?3)
17184
+ ORDER BY ts ASC`).all(alignedStart, SQLITE_MILLISECONDS_THRESHOLD, alignedStartSeconds);
16813
17185
  const counts = new Array(numBuckets).fill(0);
16814
17186
  for (const row of rows) {
16815
- const ms = row.ts < 1000000000000 ? row.ts * 1000 : row.ts;
16816
- if (ms < alignedStart)
17187
+ const ms = normalizeActivityTimestampMs(row.ts);
17188
+ if (ms < alignedStart || ms > nowMs)
16817
17189
  continue;
16818
17190
  const idx = Math.min(numBuckets - 1, Math.floor((ms - alignedStart) / bucketMs));
16819
17191
  if (idx >= 0)
16820
17192
  counts[idx]++;
16821
17193
  }
16822
- for (let i = 0;i < numBuckets - 1; i++) {
16823
- _sealedCounts.set(String(alignedStart + i * bucketMs), counts[i]);
16824
- }
16825
- for (const key of _sealedCounts.keys()) {
16826
- if (Number(key) < alignedStart)
16827
- _sealedCounts.delete(key);
16828
- }
16829
- const peak = Math.max(1, ...counts);
17194
+ const smoothed = smoothHeartrateCounts(counts);
17195
+ const peak = Math.max(1, ...smoothed);
16830
17196
  return {
16831
- windowLabel: chosenTier.label,
17197
+ windowLabel: "trailing 7d",
17198
+ bucketLabel: formatHeartrateBucketLabel(bucketMs),
16832
17199
  buckets: counts.map((count, i) => ({
16833
17200
  ts: Math.round(alignedStart + i * bucketMs),
16834
17201
  count,
16835
- value: count / peak
17202
+ value: smoothed[i] / peak
16836
17203
  }))
16837
17204
  };
16838
17205
  }
@@ -17761,7 +18128,7 @@ function whoEntryState(endpoints, registrationKind) {
17761
18128
 
17762
18129
  // packages/web/server/core/observe/service.ts
17763
18130
  init_src();
17764
- import { existsSync as existsSync11, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
18131
+ import { existsSync as existsSync12, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
17765
18132
  import { homedir as homedir12 } from "os";
17766
18133
  import { join as join18, resolve as resolve8 } from "path";
17767
18134
 
@@ -19749,6 +20116,9 @@ function parseSelf(status) {
19749
20116
  magicDnsSuffix: status.CurrentTailnet?.MagicDNSSuffix
19750
20117
  };
19751
20118
  }
20119
+ function isBackendRunning(status) {
20120
+ return (status.BackendState ?? "").trim().toLowerCase() === "running";
20121
+ }
19752
20122
  async function readStatusJsonFromFile(filePath) {
19753
20123
  const raw2 = await readFile7(filePath, "utf8");
19754
20124
  return parseStatusJson(raw2);
@@ -19766,19 +20136,25 @@ async function readStatusJson() {
19766
20136
  return null;
19767
20137
  }
19768
20138
  }
19769
- async function readTailscalePeers() {
19770
- const status = await readStatusJson();
19771
- if (!status) {
19772
- return [];
20139
+ async function readTailscaleSelf() {
20140
+ const summary = await readTailscaleStatusSummary();
20141
+ if (!summary) {
20142
+ return null;
19773
20143
  }
19774
- return parsePeers(status);
20144
+ return summary.self;
19775
20145
  }
19776
- async function readTailscaleSelf() {
20146
+ async function readTailscaleStatusSummary() {
19777
20147
  const status = await readStatusJson();
19778
20148
  if (!status) {
19779
20149
  return null;
19780
20150
  }
19781
- return parseSelf(status);
20151
+ return {
20152
+ backendState: status.BackendState ?? null,
20153
+ running: isBackendRunning(status),
20154
+ health: status.Health ?? [],
20155
+ peers: parsePeers(status),
20156
+ self: parseSelf(status)
20157
+ };
19782
20158
  }
19783
20159
 
19784
20160
  // packages/runtime/src/index.ts
@@ -20075,11 +20451,37 @@ function encodeClaudeProjectsSlug2(absolutePath) {
20075
20451
  }
20076
20452
  function resolveClaudeHistoryPath(cwd, sessionId) {
20077
20453
  const normalizedCwd = expandHome(cwd)?.trim();
20454
+ if (!normalizedCwd) {
20455
+ return null;
20456
+ }
20457
+ const projectDir = join18(homedir12(), ".claude", "projects", encodeClaudeProjectsSlug2(normalizedCwd));
20078
20458
  const normalizedSessionId = sessionId?.trim().replace(/\.jsonl$/u, "") || "";
20079
- if (!normalizedCwd || !normalizedSessionId) {
20459
+ if (normalizedSessionId) {
20460
+ const exactPath = join18(projectDir, `${normalizedSessionId}.jsonl`);
20461
+ if (existsSync12(exactPath)) {
20462
+ return exactPath;
20463
+ }
20464
+ }
20465
+ return findMostRecentJsonl(projectDir);
20466
+ }
20467
+ function findMostRecentJsonl(dir) {
20468
+ if (!existsSync12(dir))
20469
+ return null;
20470
+ try {
20471
+ let best = null;
20472
+ for (const entry of readdirSync2(dir)) {
20473
+ if (!entry.endsWith(".jsonl"))
20474
+ continue;
20475
+ const full = join18(dir, entry);
20476
+ const st = statSync4(full);
20477
+ if (!best || st.mtimeMs > best.mtime) {
20478
+ best = { path: full, mtime: st.mtimeMs };
20479
+ }
20480
+ }
20481
+ return best?.path ?? null;
20482
+ } catch {
20080
20483
  return null;
20081
20484
  }
20082
- return join18(homedir12(), ".claude", "projects", encodeClaudeProjectsSlug2(normalizedCwd), `${normalizedSessionId}.jsonl`);
20083
20485
  }
20084
20486
  function historyAdapterAlias(value) {
20085
20487
  const normalized = value?.trim().toLowerCase();
@@ -20143,7 +20545,7 @@ function readHistorySnapshot(candidate) {
20143
20545
  if (!supportsHistorySessionSnapshotForPath(candidate.path, candidate.adapterType)) {
20144
20546
  return null;
20145
20547
  }
20146
- if (!existsSync11(candidate.path)) {
20548
+ if (!existsSync12(candidate.path)) {
20147
20549
  return null;
20148
20550
  }
20149
20551
  const stat5 = statSync4(candidate.path);
@@ -20157,7 +20559,7 @@ function readHistorySnapshot(candidate) {
20157
20559
  }
20158
20560
  const replay = createHistorySessionSnapshot({
20159
20561
  path: candidate.path,
20160
- content: readFileSync7(candidate.path, "utf8"),
20562
+ content: readFileSync8(candidate.path, "utf8"),
20161
20563
  adapterType: candidate.adapterType,
20162
20564
  baseTimestampMs: stat5.mtimeMs
20163
20565
  });
@@ -20659,6 +21061,9 @@ async function loadAgentObservePayload(agentId) {
20659
21061
 
20660
21062
  // packages/web/server/core/mesh/service.ts
20661
21063
  await init_broker_service();
21064
+ import { execFile as execFile2 } from "child_process";
21065
+ import { promisify as promisify2 } from "util";
21066
+ var execFileAsync2 = promisify2(execFile2);
20662
21067
  function normalizeHost(host) {
20663
21068
  return host.trim().replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
20664
21069
  }
@@ -20689,9 +21094,13 @@ function stripTrailingDot(value) {
20689
21094
  return trimmed.replace(/\.$/, "");
20690
21095
  }
20691
21096
  async function readTailscaleStatus() {
20692
- const peers = await readTailscalePeers();
21097
+ const summary = await readTailscaleStatusSummary();
21098
+ const peers = summary?.peers ?? [];
20693
21099
  return {
20694
- available: peers.length > 0,
21100
+ available: summary !== null,
21101
+ running: summary?.running ?? false,
21102
+ backendState: summary?.backendState ?? null,
21103
+ health: summary?.health ?? [],
20695
21104
  peers,
20696
21105
  onlineCount: peers.filter((p) => p.online).length
20697
21106
  };
@@ -20712,6 +21121,16 @@ function computeIssues(health, localNode, nodes, tailscale2) {
20712
21121
  });
20713
21122
  return issues;
20714
21123
  }
21124
+ if (tailscale2.available && !tailscale2.running) {
21125
+ issues.push({
21126
+ code: "tailscale_stopped",
21127
+ severity: "warning",
21128
+ title: "Tailscale is stopped",
21129
+ summary: "This machine can still show cached Tailnet peers, but the local Tailscale backend is not running, so the broker cannot reach them.",
21130
+ action: "Start Tailscale on this machine, then refresh mesh discovery.",
21131
+ actionCommand: null
21132
+ });
21133
+ }
20715
21134
  if (localNode?.advertiseScope === "local") {
20716
21135
  issues.push({
20717
21136
  code: "local_only",
@@ -20879,6 +21298,24 @@ async function announceMeshVisibility() {
20879
21298
  } catch {}
20880
21299
  return loadMeshStatus();
20881
21300
  }
21301
+ async function openTailscaleApp() {
21302
+ if (process.platform !== "darwin") {
21303
+ throw new Error("Opening Tailscale from Scout is only supported on macOS right now.");
21304
+ }
21305
+ try {
21306
+ await execFileAsync2("open", ["-a", "Tailscale"]);
21307
+ } catch (error) {
21308
+ const detail = error instanceof Error ? error.message : String(error);
21309
+ throw new Error(`Scout could not open Tailscale.app on this machine. ${detail}`);
21310
+ }
21311
+ }
21312
+ async function controlTailscale(action) {
21313
+ if (action === "open_app") {
21314
+ await openTailscaleApp();
21315
+ return loadMeshStatus();
21316
+ }
21317
+ throw new Error(`Unsupported Tailscale action: ${action}`);
21318
+ }
20882
21319
 
20883
21320
  // packages/web/server/runtime-summary.ts
20884
21321
  async function loadOpenScoutWebShellState() {
@@ -20915,6 +21352,9 @@ async function loadOpenScoutWebShellState() {
20915
21352
 
20916
21353
  // packages/web/server/create-openscout-web-server.ts
20917
21354
  init_setup();
21355
+ init_support_paths();
21356
+ init_claude_stream_json();
21357
+ init_harness_catalog();
20918
21358
  function parseOptionalPositiveInt(value, fallback) {
20919
21359
  if (typeof value !== "string" || value.trim().length === 0) {
20920
21360
  return fallback;
@@ -20958,10 +21398,10 @@ function resolveConversationRouting(conversationId) {
20958
21398
  return { directAgentId, senderId };
20959
21399
  }
20960
21400
  function resolveBundledStaticClientRoot(moduleUrl = import.meta.url) {
20961
- return resolve9(dirname12(fileURLToPath7(moduleUrl)), "client");
21401
+ return resolve9(dirname12(fileURLToPath7(moduleUrl.toString())), "client");
20962
21402
  }
20963
21403
  function resolveSourceStaticClientRoot(moduleUrl = import.meta.url) {
20964
- return resolve9(dirname12(fileURLToPath7(moduleUrl)), "../dist/client");
21404
+ return resolve9(dirname12(fileURLToPath7(moduleUrl.toString())), "../dist/client");
20965
21405
  }
20966
21406
  function resolveStaticRoot(staticRoot) {
20967
21407
  const configured = staticRoot?.trim();
@@ -20969,7 +21409,7 @@ function resolveStaticRoot(staticRoot) {
20969
21409
  return configured;
20970
21410
  }
20971
21411
  const bundled = resolveBundledStaticClientRoot(import.meta.url);
20972
- if (existsSync12(resolve9(bundled, "index.html"))) {
21412
+ if (existsSync13(resolve9(bundled, "index.html"))) {
20973
21413
  return bundled;
20974
21414
  }
20975
21415
  return resolveSourceStaticClientRoot(import.meta.url);
@@ -21015,6 +21455,20 @@ async function createOpenScoutWebServer(options) {
21015
21455
  const payload = await loadAgentObservePayload(c.req.param("id"));
21016
21456
  return payload ? c.json(payload) : c.json({ error: "not found" }, 404);
21017
21457
  });
21458
+ app.get("/api/agents/:id/session-catalog", (c) => {
21459
+ const agentId = c.req.param("id");
21460
+ const agents = queryAgents();
21461
+ const agent = agents.find((a) => a.id === agentId);
21462
+ if (!agent)
21463
+ return c.json({ error: "agent not found" }, 404);
21464
+ const runtimeDir = relayAgentRuntimeDirectory(agent.id);
21465
+ const catalog = readSessionCatalogSync(runtimeDir);
21466
+ const cwd = agent.cwd ?? agent.projectRoot ?? ".";
21467
+ const sessionId = catalog.activeSessionId;
21468
+ const harnessEntry = findHarnessEntry(agent.harness);
21469
+ const resumeCommand = sessionId && harnessEntry ? buildHarnessResumeCommand(harnessEntry, sessionId, cwd) : null;
21470
+ return c.json({ ...catalog, agentId, harness: agent.harness, resumeCommand });
21471
+ });
21018
21472
  app.get("/api/activity", (c) => c.json(queryActivity()));
21019
21473
  app.get("/api/heartrate", (c) => c.json(queryHeartrate()));
21020
21474
  app.get("/api/fleet", (c) => c.json(queryFleet({
@@ -21075,6 +21529,15 @@ async function createOpenScoutWebServer(options) {
21075
21529
  return c.json({ error: message }, 500);
21076
21530
  }
21077
21531
  });
21532
+ app.post("/api/mesh/tailscale", async (c) => {
21533
+ try {
21534
+ const { action } = await c.req.json();
21535
+ return c.json(await controlTailscale(action));
21536
+ } catch (err) {
21537
+ const message = err instanceof Error ? err.message : String(err);
21538
+ return c.json({ error: message }, 500);
21539
+ }
21540
+ });
21078
21541
  app.get("/api/user", (c) => {
21079
21542
  const config = loadUserConfig();
21080
21543
  return c.json({
@@ -21215,6 +21678,21 @@ async function createOpenScoutWebServer(options) {
21215
21678
  quietHours: config.quietHours ?? "22:00 \u2013 07:00"
21216
21679
  });
21217
21680
  });
21681
+ app.post("/api/terminal/run", async (c) => {
21682
+ const body = await c.req.json();
21683
+ if (!body.command)
21684
+ return c.json({ error: "missing command" }, 400);
21685
+ if (!options.runTerminalCommand) {
21686
+ return c.json({ error: "terminal relay is unavailable" }, 503);
21687
+ }
21688
+ try {
21689
+ await options.runTerminalCommand(body.command);
21690
+ } catch (error) {
21691
+ const message = error instanceof Error ? error.message : "failed to queue command";
21692
+ return c.json({ error: message }, 503);
21693
+ }
21694
+ return c.json({ ok: true });
21695
+ });
21218
21696
  app.post("/api/agents/:agentId/interrupt", async (c) => {
21219
21697
  const agentId = c.req.param("agentId");
21220
21698
  const { interruptLocalAgent: interruptLocalAgent2 } = await init_local_agents().then(() => exports_local_agents);
@@ -21308,387 +21786,252 @@ async function createOpenScoutWebServer(options) {
21308
21786
  return { app, warmupCaches };
21309
21787
  }
21310
21788
 
21311
- // ../hudson/packages/hudson-relay/src/relay/session.ts
21312
- import { createRequire } from "module";
21313
- import { execSync as execSync2 } from "child_process";
21314
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
21315
- import { join as join19, dirname as pathDirname } from "path";
21316
- var require2 = createRequire(import.meta.url);
21317
- var pty = require2("node-pty");
21318
- var DEFAULT_ORPHAN_TTL_MS = 30 * 60 * 1000;
21319
- var MAX_BUFFER_SIZE = 512 * 1024;
21320
- var sessions3 = new Map;
21321
- function generateId() {
21322
- return Math.random().toString(36).slice(2, 10);
21323
- }
21324
- function send(ws, data) {
21325
- if (ws.readyState === 1) {
21326
- ws.send(JSON.stringify(data));
21327
- }
21328
- }
21329
- function resolveCwd(raw2) {
21330
- const home = process.env.HOME || "/tmp";
21331
- const expanded = (raw2 || home).replace(/^~/, home);
21332
- if (existsSync13(expanded))
21333
- return expanded;
21334
- try {
21335
- mkdirSync6(expanded, { recursive: true });
21336
- console.log(`[relay] Created missing cwd: ${expanded}`);
21337
- return expanded;
21338
- } catch {
21339
- console.warn(`[relay] Could not create cwd ${expanded}, falling back to ${home}`);
21340
- return home;
21341
- }
21342
- }
21343
- function findBin(name, envOverride) {
21344
- if (envOverride && process.env[envOverride])
21345
- return process.env[envOverride];
21346
- try {
21347
- return execSync2(`which ${name}`, { encoding: "utf8" }).trim() || null;
21348
- } catch {
21349
- return null;
21350
- }
21351
- }
21352
- function findClaudeBin() {
21353
- return findBin("claude", "CLAUDE_BIN");
21354
- }
21355
- function findPiBin() {
21356
- return findBin("pi", "PI_BIN");
21357
- }
21358
- function tmuxSessionExists2(name) {
21359
- try {
21360
- execSync2(`tmux has-session -t ${name} 2>/dev/null`, { encoding: "utf8" });
21361
- return true;
21362
- } catch {
21363
- return false;
21789
+ // packages/web/server/relay.ts
21790
+ import { mkdir as mkdir7 } from "fs/promises";
21791
+ import { join as join19 } from "path";
21792
+ import { randomUUID as randomUUID3 } from "crypto";
21793
+ var UPLOAD_DIR = "/tmp/scout-uploads";
21794
+ async function handleRelayUpload(req) {
21795
+ const { name, data } = await req.json();
21796
+ if (!name || !data) {
21797
+ return Response.json({ error: "Missing name or data" }, { status: 400 });
21364
21798
  }
21799
+ await mkdir7(UPLOAD_DIR, { recursive: true });
21800
+ const filename = `${randomUUID3()}-${name}`;
21801
+ const filepath = join19(UPLOAD_DIR, filename);
21802
+ await Bun.write(filepath, Buffer.from(data, "base64"));
21803
+ return Response.json({ path: filepath });
21365
21804
  }
21366
- function bootstrapFiles(cwd, files, sessionId) {
21367
- for (const [relPath, content] of Object.entries(files)) {
21368
- const absPath = join19(cwd, relPath);
21369
- if (!existsSync13(absPath)) {
21370
- try {
21371
- const dir = pathDirname(absPath);
21372
- if (!existsSync13(dir))
21373
- mkdirSync6(dir, { recursive: true });
21374
- writeFileSync6(absPath, content, "utf-8");
21375
- console.log(`[relay] Session ${sessionId}: bootstrapped ${relPath}`);
21376
- } catch (err) {
21377
- console.warn(`[relay] Session ${sessionId}: failed to bootstrap ${relPath}:`, err);
21378
- }
21379
- }
21805
+ function flushPending(ws) {
21806
+ const upstream = ws.data.upstream;
21807
+ if (!upstream || upstream.readyState !== WebSocket.OPEN) {
21808
+ return;
21380
21809
  }
21381
- }
21382
- function spawnTmuxSession(tmuxName, cols, rows, cwd, claudeBin, claudeArgs, env) {
21383
- const exists = tmuxSessionExists2(tmuxName);
21384
- if (!exists) {
21385
- const shellCmd = [claudeBin, ...claudeArgs].map((a) => a.includes(" ") ? `'${a}'` : a).join(" ");
21386
- execSync2(`tmux new-session -d -s ${tmuxName} -x ${cols} -y ${rows} -c '${cwd}' '${shellCmd}'`, { env });
21387
- console.log(`[relay] Created tmux session: ${tmuxName}`);
21388
- } else {
21389
- try {
21390
- execSync2(`tmux resize-window -t ${tmuxName} -x ${cols} -y ${rows} 2>/dev/null`);
21391
- } catch {}
21392
- console.log(`[relay] Attaching to existing tmux session: ${tmuxName}`);
21810
+ for (const payload of ws.data.pending) {
21811
+ upstream.send(payload);
21393
21812
  }
21394
- return pty.spawn("tmux", ["attach", "-t", tmuxName], {
21395
- name: "xterm-256color",
21396
- cols,
21397
- rows,
21398
- cwd,
21399
- env
21400
- });
21813
+ ws.data.pending.length = 0;
21401
21814
  }
21402
- function createSession(ws, msg) {
21403
- const id = generateId();
21404
- const cols = Math.max(msg.cols || 80, 20);
21405
- const rows = Math.max(msg.rows || 24, 4);
21406
- const backend = msg.backend || "pty";
21407
- const tmuxName = msg.tmuxSession || `hudson-${id}`;
21408
- const agent = msg.agent || "claude";
21409
- let agentBin;
21410
- if (agent === "pi") {
21411
- agentBin = findPiBin();
21412
- if (!agentBin) {
21413
- const reason = "pi CLI not found. Install it with: npm install -g @mariozechner/pi-coding-agent";
21414
- console.error(`[relay] Session ${id} failed: ${reason}`);
21415
- send(ws, { type: "session:error", error: reason });
21416
- return null;
21417
- }
21418
- } else {
21419
- agentBin = findClaudeBin();
21420
- if (!agentBin) {
21421
- const reason = "Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code";
21422
- console.error(`[relay] Session ${id} failed: ${reason}`);
21423
- send(ws, { type: "session:error", error: reason });
21424
- return null;
21425
- }
21426
- }
21427
- if (!existsSync13(agentBin)) {
21428
- const reason = `${agent} binary not found at ${agentBin}`;
21429
- console.error(`[relay] Session ${id} failed: ${reason}`);
21430
- send(ws, { type: "session:error", error: reason });
21431
- return null;
21815
+ function forwardToClient(ws, data) {
21816
+ if (ws.readyState !== WebSocket.OPEN) {
21817
+ return;
21432
21818
  }
21433
- if (backend === "tmux" && !findBin("tmux")) {
21434
- const reason = "tmux not found. Install it with: brew install tmux";
21435
- console.error(`[relay] Session ${id} failed: ${reason}`);
21436
- send(ws, { type: "session:error", error: reason });
21437
- return null;
21819
+ if (typeof data === "string") {
21820
+ ws.send(data);
21821
+ return;
21438
21822
  }
21439
- const cwd = resolveCwd(msg.cwd);
21440
- if (msg.workspaceFiles) {
21441
- bootstrapFiles(cwd, msg.workspaceFiles, id);
21442
- }
21443
- let agentArgs;
21444
- if (agent === "pi") {
21445
- agentArgs = ["--verbose"];
21446
- if (msg.provider)
21447
- agentArgs.push("--provider", msg.provider);
21448
- if (msg.model)
21449
- agentArgs.push("--model", msg.model);
21450
- if (msg.systemPrompt)
21451
- agentArgs.push("--system-prompt", msg.systemPrompt);
21452
- } else {
21453
- agentArgs = ["--verbose"];
21454
- if (msg.systemPrompt)
21455
- agentArgs.push("--system-prompt", msg.systemPrompt);
21823
+ if (data instanceof Blob) {
21824
+ data.arrayBuffer().then((buffer) => {
21825
+ if (ws.readyState === WebSocket.OPEN) {
21826
+ ws.send(buffer);
21827
+ }
21828
+ }).catch(() => {});
21829
+ return;
21456
21830
  }
21457
- const env = { ...process.env, TERM: "xterm-256color", FORCE_COLOR: "1" };
21458
- delete env.CLAUDECODE;
21459
- let ptyProcess;
21460
- try {
21461
- if (backend === "tmux") {
21462
- console.log(`[relay] Session ${id}: tmux backend (session: ${tmuxName}) in ${cwd} [agent: ${agent}]`);
21463
- ptyProcess = spawnTmuxSession(tmuxName, cols, rows, cwd, agentBin, agentArgs, env);
21464
- } else {
21465
- console.log(`[relay] Session ${id}: pty backend, spawning ${agentBin} in ${cwd} [agent: ${agent}]`);
21466
- ptyProcess = pty.spawn(agentBin, agentArgs, {
21467
- name: "xterm-256color",
21468
- cols,
21469
- rows,
21470
- cwd,
21471
- env
21472
- });
21831
+ ws.send(data);
21832
+ }
21833
+ function createRelayWebSocketProxy(targetUrl) {
21834
+ return {
21835
+ open(ws) {
21836
+ ws.data.pending = [];
21837
+ const upstream = new WebSocket(targetUrl);
21838
+ upstream.binaryType = "arraybuffer";
21839
+ ws.data.upstream = upstream;
21840
+ upstream.onopen = () => {
21841
+ flushPending(ws);
21842
+ };
21843
+ upstream.onmessage = (event) => {
21844
+ forwardToClient(ws, event.data);
21845
+ };
21846
+ upstream.onerror = () => {
21847
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
21848
+ ws.close();
21849
+ }
21850
+ };
21851
+ upstream.onclose = () => {
21852
+ ws.data.upstream = null;
21853
+ ws.data.pending.length = 0;
21854
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
21855
+ ws.close();
21856
+ }
21857
+ };
21858
+ },
21859
+ message(ws, raw2) {
21860
+ const upstream = ws.data.upstream;
21861
+ const payload = raw2;
21862
+ if (!upstream || upstream.readyState === WebSocket.CONNECTING) {
21863
+ ws.data.pending.push(payload);
21864
+ return;
21865
+ }
21866
+ if (upstream.readyState === WebSocket.OPEN) {
21867
+ upstream.send(payload);
21868
+ }
21869
+ },
21870
+ close(ws) {
21871
+ const upstream = ws.data.upstream;
21872
+ ws.data.upstream = null;
21873
+ ws.data.pending.length = 0;
21874
+ if (!upstream) {
21875
+ return;
21876
+ }
21877
+ if (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING) {
21878
+ upstream.close();
21879
+ }
21473
21880
  }
21474
- } catch (err) {
21475
- const message = err instanceof Error ? err.message : String(err);
21476
- console.error(`[relay] Session ${id}: failed to spawn PTY \u2014 ${message}`);
21477
- send(ws, { type: "session:error", error: `Failed to spawn terminal: ${message}` });
21478
- return null;
21479
- }
21480
- const orphanTTL = msg.orphanTTL && msg.orphanTTL > 0 ? msg.orphanTTL : DEFAULT_ORPHAN_TTL_MS;
21481
- const session = {
21482
- id,
21483
- pty: ptyProcess,
21484
- ws,
21485
- outputBuffer: "",
21486
- cols,
21487
- rows,
21488
- reapTimer: null,
21489
- orphanTTL,
21490
- backend,
21491
- ...backend === "tmux" ? { tmuxSession: tmuxName } : {},
21492
- exited: false,
21493
- exitCode: null
21494
21881
  };
21495
- const startTime = Date.now();
21496
- ptyProcess.onData((data) => {
21497
- session.outputBuffer += data;
21498
- if (session.outputBuffer.length > MAX_BUFFER_SIZE) {
21499
- session.outputBuffer = session.outputBuffer.slice(-MAX_BUFFER_SIZE);
21500
- }
21501
- if (session.ws && session.ws.readyState === 1) {
21502
- send(session.ws, { type: "terminal:data", data });
21503
- }
21504
- });
21505
- ptyProcess.onExit(({ exitCode }) => {
21506
- session.exited = true;
21507
- session.exitCode = exitCode;
21508
- const uptime = Date.now() - startTime;
21509
- const crashed = exitCode !== 0 && uptime < 5000;
21510
- let reason;
21511
- if (crashed) {
21512
- const clean = session.outputBuffer.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").trim();
21513
- const lines = clean.split(`
21514
- `).filter((l) => l.trim()).slice(-5);
21515
- reason = lines.join(`
21516
- `) || `Process exited with code ${exitCode}`;
21517
- console.error(`[relay] Session ${id} crashed after ${uptime}ms (code ${exitCode}): ${reason}`);
21518
- }
21519
- if (session.ws) {
21520
- send(session.ws, { type: "session:exit", exitCode, ...reason ? { reason } : {} });
21521
- }
21522
- scheduleReap(session, 1e4);
21523
- });
21524
- sessions3.set(id, session);
21525
- console.log(`[relay] Session ${id} created (${cols}x${rows})`);
21526
- return session;
21527
21882
  }
21528
- function attachSession(session, ws, cols, rows) {
21529
- if (session.reapTimer) {
21530
- clearTimeout(session.reapTimer);
21531
- session.reapTimer = null;
21532
- }
21533
- session.ws = ws;
21534
- if (cols && rows) {
21535
- const c = Math.max(cols, 20);
21536
- const r = Math.max(rows, 4);
21537
- if (c !== session.cols || r !== session.rows) {
21538
- session.pty.resize(c, r);
21539
- session.cols = c;
21540
- session.rows = r;
21883
+
21884
+ // packages/web/server/managed-terminal-relay.ts
21885
+ import { existsSync as existsSync14 } from "fs";
21886
+ import { spawn as spawn5, spawnSync as spawnSync2 } from "child_process";
21887
+ import { dirname as dirname13, resolve as resolve10 } from "path";
21888
+ import { fileURLToPath as fileURLToPath8 } from "url";
21889
+ function relayPortForWebPort(webPort) {
21890
+ const configured = process.env.OPENSCOUT_WEB_TERMINAL_RELAY_PORT?.trim();
21891
+ if (configured) {
21892
+ const parsed = Number.parseInt(configured, 10);
21893
+ if (Number.isFinite(parsed) && parsed > 0) {
21894
+ return parsed;
21541
21895
  }
21542
21896
  }
21543
- if (session.exited) {
21544
- send(ws, { type: "session:exit", exitCode: session.exitCode });
21545
- } else if (session.outputBuffer.length > 0) {
21546
- send(ws, { type: "terminal:data", data: session.outputBuffer });
21547
- }
21548
- console.log(`[relay] Session ${session.id} reconnected`);
21897
+ return webPort + 1;
21549
21898
  }
21550
- function detachSession(session) {
21551
- session.ws = null;
21552
- if (session.exited) {
21553
- scheduleReap(session, 5000);
21554
- } else {
21555
- scheduleReap(session, session.orphanTTL);
21556
- console.log(`[relay] Session ${session.id} detached (orphaned for ${session.orphanTTL / 1000}s)`);
21557
- }
21899
+ function relayHostForWebHost(hostname2) {
21900
+ return process.env.OPENSCOUT_WEB_TERMINAL_RELAY_HOST?.trim() || hostname2;
21558
21901
  }
21559
- function scheduleReap(session, delay) {
21560
- if (session.reapTimer)
21561
- clearTimeout(session.reapTimer);
21562
- session.reapTimer = setTimeout(() => {
21563
- if (!session.ws) {
21564
- destroy(session.id);
21565
- }
21566
- }, delay);
21902
+ function relayLoopbackHost(hostname2) {
21903
+ if (hostname2 === "0.0.0.0" || hostname2 === "::") {
21904
+ return "127.0.0.1";
21905
+ }
21906
+ return hostname2;
21567
21907
  }
21568
- function destroy(sessionId) {
21569
- const session = sessions3.get(sessionId);
21570
- if (!session)
21571
- return;
21572
- if (session.reapTimer)
21573
- clearTimeout(session.reapTimer);
21908
+ async function isHealthy(targetHttpUrl) {
21574
21909
  try {
21575
- session.pty.kill();
21576
- } catch {}
21577
- sessions3.delete(sessionId);
21578
- if (session.backend === "tmux") {
21579
- console.log(`[relay] Session ${sessionId} bridge destroyed (tmux session '${session.tmuxSession}' still alive)`);
21580
- } else {
21581
- console.log(`[relay] Session ${sessionId} destroyed`);
21910
+ const response = await fetch(`${targetHttpUrl}/health`, {
21911
+ signal: AbortSignal.timeout(1000)
21912
+ });
21913
+ return response.ok;
21914
+ } catch {
21915
+ return false;
21582
21916
  }
21583
21917
  }
21584
-
21585
- // packages/web/server/relay.ts
21586
- import { mkdir as mkdir7 } from "fs/promises";
21587
- import { join as join20 } from "path";
21588
- import { randomUUID as randomUUID3 } from "crypto";
21589
- var UPLOAD_DIR = "/tmp/scout-uploads";
21590
- async function handleRelayUpload(req) {
21591
- const { name, data } = await req.json();
21592
- if (!name || !data) {
21593
- return Response.json({ error: "Missing name or data" }, { status: 400 });
21918
+ async function waitForHealthy(targetHttpUrl, timeoutMs) {
21919
+ const deadline = Date.now() + timeoutMs;
21920
+ while (Date.now() < deadline) {
21921
+ if (await isHealthy(targetHttpUrl)) {
21922
+ return true;
21923
+ }
21924
+ await new Promise((resolveTimer) => setTimeout(resolveTimer, 100));
21594
21925
  }
21595
- await mkdir7(UPLOAD_DIR, { recursive: true });
21596
- const filename = `${randomUUID3()}-${name}`;
21597
- const filepath = join20(UPLOAD_DIR, filename);
21598
- await Bun.write(filepath, Buffer.from(data, "base64"));
21599
- return Response.json({ path: filepath });
21600
- }
21601
- function asRelay(ws) {
21602
- return ws;
21926
+ return false;
21603
21927
  }
21604
- function parseMessage(raw2) {
21605
- try {
21606
- const msg = JSON.parse(raw2);
21607
- if (typeof msg.type === "string")
21608
- return msg;
21609
- } catch {}
21610
- return null;
21928
+ function resolveRuntimeEntry() {
21929
+ const selfDir = dirname13(fileURLToPath8(import.meta.url));
21930
+ const sourceEntry = resolve10(selfDir, "terminal-relay-node.ts");
21931
+ const bundledEntry = resolve10(selfDir, "openscout-terminal-relay.mjs");
21932
+ const sourceBundle = resolve10(selfDir, "../dist/openscout-terminal-relay.mjs");
21933
+ if (existsSync14(sourceEntry)) {
21934
+ const build = spawnSync2("bun", [
21935
+ "build",
21936
+ sourceEntry,
21937
+ "--target=node",
21938
+ "--format=esm",
21939
+ "--outfile",
21940
+ sourceBundle,
21941
+ "--external",
21942
+ "node-pty"
21943
+ ], {
21944
+ cwd: resolve10(selfDir, ".."),
21945
+ stdio: "inherit"
21946
+ });
21947
+ if ((build.status ?? 1) !== 0) {
21948
+ throw new Error("Failed to build terminal relay bundle");
21949
+ }
21950
+ return sourceBundle;
21951
+ }
21952
+ if (existsSync14(bundledEntry)) {
21953
+ return bundledEntry;
21954
+ }
21955
+ throw new Error("Could not locate the terminal relay runtime entry");
21611
21956
  }
21612
- var relayWebSocket = {
21613
- open(_ws) {},
21614
- message(ws, raw2) {
21615
- const msg = parseMessage(typeof raw2 === "string" ? raw2 : raw2.toString());
21616
- if (!msg)
21617
- return;
21618
- const rws = asRelay(ws);
21619
- switch (msg.type) {
21620
- case "session:init": {
21621
- if (ws.data.sessionId) {
21622
- const prev = sessions3.get(ws.data.sessionId);
21623
- if (prev)
21624
- detachSession(prev);
21625
- }
21626
- const session = createSession(rws, msg);
21627
- if (!session)
21628
- break;
21629
- ws.data.sessionId = session.id;
21630
- send(rws, { type: "session:ready", sessionId: session.id });
21631
- break;
21632
- }
21633
- case "session:reconnect": {
21634
- const existing = sessions3.get(msg.sessionId);
21635
- if (existing && !existing.exited) {
21636
- if (ws.data.sessionId && ws.data.sessionId !== msg.sessionId) {
21637
- const prev = sessions3.get(ws.data.sessionId);
21638
- if (prev)
21639
- detachSession(prev);
21640
- }
21641
- if (existing.ws && existing.ws !== rws) {
21642
- send(existing.ws, { type: "session:detached" });
21643
- }
21644
- ws.data.sessionId = existing.id;
21645
- attachSession(existing, rws, msg.cols, msg.rows);
21646
- send(rws, {
21647
- type: "session:ready",
21648
- sessionId: existing.id,
21649
- reconnected: true
21650
- });
21651
- } else {
21652
- send(rws, { type: "session:expired", sessionId: msg.sessionId });
21653
- }
21654
- break;
21655
- }
21656
- case "terminal:input": {
21657
- if (!ws.data.sessionId)
21658
- return;
21659
- const session = sessions3.get(ws.data.sessionId);
21660
- if (session && !session.exited) {
21661
- session.pty.write(msg.data);
21662
- }
21663
- break;
21664
- }
21665
- case "terminal:resize": {
21666
- if (!ws.data.sessionId)
21667
- return;
21668
- const session = sessions3.get(ws.data.sessionId);
21669
- if (session && !session.exited) {
21670
- const cols = Math.max(msg.cols || 80, 20);
21671
- const rows = Math.max(msg.rows || 24, 4);
21672
- session.pty.resize(cols, rows);
21673
- session.cols = cols;
21674
- session.rows = rows;
21957
+ async function startManagedTerminalRelay(args) {
21958
+ const relayPort = relayPortForWebPort(args.webPort);
21959
+ const bindHost = relayHostForWebHost(args.hostname);
21960
+ const loopbackHost = relayLoopbackHost(bindHost);
21961
+ const targetHttpUrl = `http://${loopbackHost}:${relayPort}`;
21962
+ const targetWebSocketUrl = `ws://${loopbackHost}:${relayPort}`;
21963
+ if (await isHealthy(targetHttpUrl)) {
21964
+ return {
21965
+ healthcheck: () => isHealthy(targetHttpUrl),
21966
+ queueCommand: async (command) => {
21967
+ const response = await fetch(`${targetHttpUrl}/api/terminal/run`, {
21968
+ method: "POST",
21969
+ headers: { "Content-Type": "application/json" },
21970
+ body: JSON.stringify({ command })
21971
+ });
21972
+ if (!response.ok) {
21973
+ throw new Error("Terminal relay rejected queued command");
21675
21974
  }
21676
- break;
21677
- }
21975
+ },
21976
+ shutdown() {},
21977
+ targetHttpUrl,
21978
+ targetWebSocketUrl
21979
+ };
21980
+ }
21981
+ const runtimeEntry = resolveRuntimeEntry();
21982
+ const child = spawn5("node", [runtimeEntry], {
21983
+ cwd: resolve10(dirname13(runtimeEntry), ".."),
21984
+ env: {
21985
+ ...process.env,
21986
+ OPENSCOUT_WEB_TERMINAL_RELAY_HOST: bindHost,
21987
+ OPENSCOUT_WEB_TERMINAL_RELAY_PORT: String(relayPort)
21988
+ },
21989
+ stdio: "inherit"
21990
+ });
21991
+ let spawnError = null;
21992
+ child.once("error", (error) => {
21993
+ spawnError = error;
21994
+ });
21995
+ child.on("exit", (code, signal) => {
21996
+ if (signal === "SIGTERM" || signal === "SIGINT") {
21997
+ return;
21678
21998
  }
21679
- },
21680
- close(ws) {
21681
- if (ws.data.sessionId) {
21682
- const session = sessions3.get(ws.data.sessionId);
21683
- if (session)
21684
- detachSession(session);
21685
- ws.data.sessionId = null;
21999
+ console.error(`[relay] Managed terminal relay exited unexpectedly (code: ${code ?? "null"}, signal: ${signal ?? "none"})`);
22000
+ });
22001
+ const ready = await waitForHealthy(targetHttpUrl, 5000);
22002
+ if (!ready) {
22003
+ if (!child.killed) {
22004
+ child.kill("SIGTERM");
22005
+ }
22006
+ if (spawnError) {
22007
+ throw spawnError;
21686
22008
  }
22009
+ throw new Error(`Terminal relay did not become healthy at ${targetHttpUrl}`);
21687
22010
  }
21688
- };
21689
- function destroyAllRelaySessions() {
21690
- for (const [id] of sessions3)
21691
- destroy(id);
22011
+ return {
22012
+ healthcheck: () => isHealthy(targetHttpUrl),
22013
+ queueCommand: async (command) => {
22014
+ const response = await fetch(`${targetHttpUrl}/api/terminal/run`, {
22015
+ method: "POST",
22016
+ headers: { "Content-Type": "application/json" },
22017
+ body: JSON.stringify({ command })
22018
+ });
22019
+ if (!response.ok) {
22020
+ throw new Error("Terminal relay rejected queued command");
22021
+ }
22022
+ },
22023
+ shutdown() {
22024
+ terminateChild(child);
22025
+ },
22026
+ targetHttpUrl,
22027
+ targetWebSocketUrl
22028
+ };
22029
+ }
22030
+ function terminateChild(child) {
22031
+ if (child.killed) {
22032
+ return;
22033
+ }
22034
+ child.kill("SIGTERM");
21692
22035
  }
21693
22036
 
21694
22037
  // packages/web/server/index.ts
@@ -21700,13 +22043,13 @@ function resolveStaticRoot2() {
21700
22043
  if (process.env.OPENSCOUT_WEB_STATIC_ROOT?.trim()) {
21701
22044
  return process.env.OPENSCOUT_WEB_STATIC_ROOT.trim();
21702
22045
  }
21703
- const selfDir = dirname13(fileURLToPath8(import.meta.url));
21704
- const siblingClientRoot = join21(selfDir, "client");
21705
- if (existsSync14(join21(siblingClientRoot, "index.html"))) {
22046
+ const selfDir = dirname14(fileURLToPath9(import.meta.url));
22047
+ const siblingClientRoot = join20(selfDir, "client");
22048
+ if (existsSync15(join20(siblingClientRoot, "index.html"))) {
21706
22049
  return siblingClientRoot;
21707
22050
  }
21708
- const sourceDistClientRoot = resolve10(selfDir, "../dist/client");
21709
- if (existsSync14(join21(sourceDistClientRoot, "index.html"))) {
22051
+ const sourceDistClientRoot = resolve11(selfDir, "../dist/client");
22052
+ if (existsSync15(join20(sourceDistClientRoot, "index.html"))) {
21710
22053
  return sourceDistClientRoot;
21711
22054
  }
21712
22055
  return;
@@ -21715,26 +22058,47 @@ var staticRoot = resolveStaticRoot2();
21715
22058
  var viteDevUrl = process.env.OPENSCOUT_WEB_VITE_URL?.trim() || undefined;
21716
22059
  var useViteProxy = Boolean(viteDevUrl) || !staticRoot;
21717
22060
  var idleTimeoutSeconds = Number.parseInt(process.env.OPENSCOUT_WEB_IDLE_TIMEOUT_SECONDS?.trim() || (useViteProxy ? "180" : "30"), 10);
22061
+ var terminalRelay = null;
22062
+ try {
22063
+ terminalRelay = await startManagedTerminalRelay({
22064
+ hostname: hostname2,
22065
+ webPort: port
22066
+ });
22067
+ } catch (error) {
22068
+ const message = error instanceof Error ? error.message : String(error);
22069
+ console.warn(`[scout] Terminal relay unavailable: ${message}`);
22070
+ }
21718
22071
  var { app, warmupCaches } = await createOpenScoutWebServer({
21719
22072
  currentDirectory,
21720
22073
  shellStateCacheTtlMs,
21721
22074
  assetMode: useViteProxy ? "vite-proxy" : "static",
21722
22075
  viteDevUrl,
21723
- staticRoot
22076
+ staticRoot,
22077
+ runTerminalCommand: terminalRelay?.queueCommand
21724
22078
  });
21725
22079
  var honoFetch = app.fetch;
22080
+ var relayWebSocket = terminalRelay ? createRelayWebSocketProxy(terminalRelay.targetWebSocketUrl) : {
22081
+ open(ws) {
22082
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
22083
+ ws.close(1013, "Terminal relay unavailable");
22084
+ }
22085
+ },
22086
+ message() {},
22087
+ close() {}
22088
+ };
21726
22089
  var server = Bun.serve({
21727
22090
  port,
21728
22091
  hostname: hostname2,
21729
22092
  idleTimeout: idleTimeoutSeconds,
21730
- fetch(req, server2) {
22093
+ async fetch(req, server2) {
21731
22094
  const url = new URL(req.url);
21732
22095
  if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
21733
- const ok = server2.upgrade(req, { data: { sessionId: null } });
22096
+ const ok = server2.upgrade(req, { data: { upstream: null, pending: [] } });
21734
22097
  return ok ? undefined : new Response("WebSocket upgrade failed", { status: 500 });
21735
22098
  }
21736
22099
  if (req.method === "GET" && url.pathname === "/health") {
21737
- return Response.json({ ok: true });
22100
+ const relayOk = terminalRelay ? await terminalRelay.healthcheck() : false;
22101
+ return Response.json({ ok: relayOk, relay: relayOk ? "up" : "down" }, { status: relayOk ? 200 : 503 });
21738
22102
  }
21739
22103
  if (req.method === "POST" && (url.pathname === "/api/upload" || url.pathname === "/api/relay/upload")) {
21740
22104
  return handleRelayUpload(req);
@@ -21745,8 +22109,8 @@ var server = Bun.serve({
21745
22109
  });
21746
22110
  var shutdown = () => {
21747
22111
  console.log(`
21748
- [scout] Shutting down relay sessions...`);
21749
- destroyAllRelaySessions();
22112
+ [scout] Shutting down terminal relay...`);
22113
+ terminalRelay?.shutdown();
21750
22114
  server.stop();
21751
22115
  process.exit(0);
21752
22116
  };