@openscout/scout 0.2.58 → 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.
@@ -8902,8 +8902,107 @@ var init_claude_stream_json = __esm(() => {
8902
8902
 
8903
8903
  // packages/runtime/src/codex-app-server.ts
8904
8904
  import { spawn as spawn4 } from "child_process";
8905
- 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";
8906
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
+ }
8907
9006
  function isCodexThreadGlobalMessage(message) {
8908
9007
  const result = message.result;
8909
9008
  const thread = result?.thread;
@@ -9706,6 +9805,13 @@ function isMissingCodexRolloutError2(error) {
9706
9805
  const message = errorMessage3(error).toLowerCase();
9707
9806
  return message.includes("no rollout found for thread id");
9708
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
+ }
9709
9815
 
9710
9816
  class CodexAppServerSession {
9711
9817
  options;
@@ -9895,7 +10001,7 @@ class CodexAppServerSession {
9895
10001
  systemPrompt: options.systemPrompt,
9896
10002
  threadId: options.threadId ?? null,
9897
10003
  requireExistingThread: options.requireExistingThread === true,
9898
- launchArgs: Array.isArray(options.launchArgs) ? options.launchArgs : []
10004
+ launchArgs: normalizeCodexAppServerLaunchArgs(options.launchArgs)
9899
10005
  });
9900
10006
  }
9901
10007
  async ensureStarted() {
@@ -9917,6 +10023,7 @@ class CodexAppServerSession {
9917
10023
  await mkdir5(this.options.logsDirectory, { recursive: true });
9918
10024
  await writeFile5(join14(this.options.runtimeDirectory, "prompt.txt"), this.options.systemPrompt);
9919
10025
  const codexExecutable = await resolveCodexExecutable2();
10026
+ const launchArgs = normalizeCodexAppServerLaunchArgs(this.options.launchArgs);
9920
10027
  const env = buildManagedAgentEnvironment({
9921
10028
  agentName: this.options.agentName,
9922
10029
  currentDirectory: this.options.cwd,
@@ -9927,7 +10034,8 @@ class CodexAppServerSession {
9927
10034
  ...buildScoutMcpCodexLaunchArgs({
9928
10035
  currentDirectory: this.options.cwd,
9929
10036
  env
9930
- })
10037
+ }),
10038
+ ...launchArgs
9931
10039
  ], {
9932
10040
  cwd: this.options.cwd,
9933
10041
  env,
@@ -10025,6 +10133,8 @@ class CodexAppServerSession {
10025
10133
  turnId: "",
10026
10134
  startedAt: Date.now(),
10027
10135
  timer: null,
10136
+ graceTimer: null,
10137
+ timedOutAt: null,
10028
10138
  messageOrder: [],
10029
10139
  messageByItemId: new Map,
10030
10140
  resolve: resolve5,
@@ -10032,20 +10142,43 @@ class CodexAppServerSession {
10032
10142
  watchers: []
10033
10143
  };
10034
10144
  turn.timer = setTimeout(() => {
10035
- this.interrupt().catch(() => {
10036
- return;
10037
- });
10038
- if (this.activeTurn === turn) {
10039
- this.activeTurn = null;
10040
- }
10041
- for (const watcher of this.drainTurnWatchers(turn)) {
10042
- watcher.reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
10043
- }
10044
- reject(new Error(`Timed out after ${timeoutMs}ms waiting for ${this.options.agentName}.`));
10145
+ this.scheduleTurnTimeout(turn, timeoutMs);
10045
10146
  }, timeoutMs);
10046
10147
  this.activeTurn = turn;
10047
10148
  return turn;
10048
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
+ }
10049
10182
  addTurnWatcher(turn, resolve5, reject, timeoutMs) {
10050
10183
  const watcher = {
10051
10184
  resolve: resolve5,
@@ -10079,6 +10212,9 @@ class CodexAppServerSession {
10079
10212
  if (turn.timer) {
10080
10213
  clearTimeout(turn.timer);
10081
10214
  }
10215
+ if (turn.graceTimer) {
10216
+ clearTimeout(turn.graceTimer);
10217
+ }
10082
10218
  if (this.activeTurn === turn) {
10083
10219
  this.activeTurn = null;
10084
10220
  }
@@ -11454,6 +11590,128 @@ function normalizeLocalAgentCapabilities(value) {
11454
11590
  function normalizeLocalAgentLaunchArgs(value) {
11455
11591
  return Array.isArray(value) ? value.map((entry) => String(entry).trim()).filter(Boolean) : [];
11456
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
+ }
11457
11715
  function normalizeManagedHarness2(value, fallback) {
11458
11716
  return value === "codex" ? "codex" : value === "claude" ? "claude" : fallback;
11459
11717
  }
@@ -11469,7 +11727,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
11469
11727
  cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
11470
11728
  transport: normalizeLocalAgentTransport(profile.transport, harness),
11471
11729
  sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
11472
- launchArgs: normalizeLocalAgentLaunchArgs(profile.launchArgs)
11730
+ launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
11473
11731
  };
11474
11732
  }
11475
11733
  const runtimeHarness = normalizeManagedHarness2(record.harness, defaultHarness);
@@ -11478,7 +11736,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
11478
11736
  cwd: normalizeProjectPath(record.cwd || process.cwd()),
11479
11737
  transport: normalizeLocalAgentTransport(record.transport, runtimeHarness),
11480
11738
  sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${runtimeHarness}`),
11481
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
11739
+ launchArgs: normalizeLaunchArgsForHarness(runtimeHarness, record.launchArgs)
11482
11740
  };
11483
11741
  }
11484
11742
  if (!nextProfiles[defaultHarness]) {
@@ -11486,7 +11744,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
11486
11744
  cwd: normalizeProjectPath(record.cwd || process.cwd()),
11487
11745
  transport: normalizeLocalAgentTransport(record.transport, defaultHarness),
11488
11746
  sessionId: normalizeTmuxSessionName2(record.tmuxSession, `${agentId}-${defaultHarness}`),
11489
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
11747
+ launchArgs: normalizeLaunchArgsForHarness(defaultHarness, record.launchArgs)
11490
11748
  };
11491
11749
  }
11492
11750
  for (const harness of ["claude", "codex"]) {
@@ -11498,7 +11756,7 @@ function normalizeLocalHarnessProfiles(agentId, record) {
11498
11756
  cwd: normalizeProjectPath(profile.cwd || record.cwd || process.cwd()),
11499
11757
  transport: normalizeLocalAgentTransport(profile.transport, harness),
11500
11758
  sessionId: normalizeTmuxSessionName2(profile.sessionId, `${agentId}-${harness}`),
11501
- launchArgs: normalizeLocalAgentLaunchArgs(profile.launchArgs)
11759
+ launchArgs: normalizeLaunchArgsForHarness(harness, profile.launchArgs)
11502
11760
  };
11503
11761
  }
11504
11762
  return nextProfiles;
@@ -11522,14 +11780,14 @@ function recordForHarness(record, harnessOverride) {
11522
11780
  tmuxSession: fallbackSessionId,
11523
11781
  cwd: fallbackCwd,
11524
11782
  transport: fallbackTransport,
11525
- launchArgs: normalizeLocalAgentLaunchArgs(normalized.launchArgs),
11783
+ launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs),
11526
11784
  harnessProfiles: {
11527
11785
  ...normalized.harnessProfiles,
11528
11786
  [selectedHarness]: {
11529
11787
  cwd: fallbackCwd,
11530
11788
  transport: fallbackTransport,
11531
11789
  sessionId: fallbackSessionId,
11532
- launchArgs: normalizeLocalAgentLaunchArgs(normalized.launchArgs)
11790
+ launchArgs: normalizeLaunchArgsForHarness(selectedHarness, normalized.launchArgs)
11533
11791
  }
11534
11792
  }
11535
11793
  };
@@ -11571,7 +11829,7 @@ function normalizeLocalAgentRecord(agentId, record) {
11571
11829
  harnessProfiles,
11572
11830
  transport: activeProfile?.transport ?? normalizeLocalAgentTransport(record.transport, harness),
11573
11831
  capabilities: normalizeLocalAgentCapabilities(record.capabilities),
11574
- launchArgs: activeProfile?.launchArgs ?? normalizeLocalAgentLaunchArgs(record.launchArgs)
11832
+ launchArgs: activeProfile?.launchArgs ?? normalizeLaunchArgsForHarness(harness, record.launchArgs)
11575
11833
  };
11576
11834
  }
11577
11835
  function localAgentStatusFromRecord(agentId, record, source) {
@@ -11640,7 +11898,7 @@ function relayAgentOverrideFromLocalAgentRecord(agentId, record, existing) {
11640
11898
  source: existing?.source && existing.source !== "inferred" ? existing.source : "manual",
11641
11899
  startedAt: normalizedRecord.startedAt,
11642
11900
  systemPrompt: normalizedRecord.systemPrompt,
11643
- launchArgs: normalizeLocalAgentLaunchArgs(normalizedRecord.launchArgs),
11901
+ launchArgs: normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs),
11644
11902
  capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
11645
11903
  defaultHarness: normalizeManagedHarness2(normalizedRecord.defaultHarness, "claude"),
11646
11904
  harnessProfiles: normalizedRecord.harnessProfiles,
@@ -11665,7 +11923,7 @@ function buildLocalAgentConfigState(agentId, record) {
11665
11923
  sessionId: record.tmuxSession,
11666
11924
  wakePolicy: "on_demand"
11667
11925
  },
11668
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs),
11926
+ launchArgs: normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(record.harness), record.launchArgs),
11669
11927
  capabilities: normalizeLocalAgentCapabilities(record.capabilities),
11670
11928
  applyMode: "restart",
11671
11929
  templateHint: LOCAL_AGENT_SYSTEM_PROMPT_TEMPLATE_HINT
@@ -11791,7 +12049,7 @@ function buildCodexAgentSessionOptions(agentName, record, systemPrompt) {
11791
12049
  systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "codex_app_server" }),
11792
12050
  runtimeDirectory: relayAgentRuntimeDirectory(agentName),
11793
12051
  logsDirectory: relayAgentLogsDirectory(agentName),
11794
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
12052
+ launchArgs: normalizeLaunchArgsForHarness("codex", record.launchArgs)
11795
12053
  };
11796
12054
  }
11797
12055
  function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
@@ -11802,7 +12060,7 @@ function buildClaudeAgentSessionOptions(agentName, record, systemPrompt) {
11802
12060
  systemPrompt: systemPrompt ?? buildLocalAgentSystemPrompt(agentName, record.project, record.cwd, { transport: "claude_stream_json" }),
11803
12061
  runtimeDirectory: relayAgentRuntimeDirectory(agentName),
11804
12062
  logsDirectory: relayAgentLogsDirectory(agentName),
11805
- launchArgs: normalizeLocalAgentLaunchArgs(record.launchArgs)
12063
+ launchArgs: normalizeLaunchArgsForHarness("claude", record.launchArgs)
11806
12064
  };
11807
12065
  }
11808
12066
  function endpointMetadataString(endpoint, key) {
@@ -12007,7 +12265,7 @@ function buildLocalAgentBootstrapPrompt(_harness, _systemPrompt, initialMessage)
12007
12265
  return initialMessage;
12008
12266
  }
12009
12267
  function buildLocalAgentLaunchCommand(agentName, record, projectPath, promptFile, workerScript) {
12010
- const extraArgs = shellQuoteArguments(normalizeLocalAgentLaunchArgs(record.launchArgs));
12268
+ const extraArgs = shellQuoteArguments(normalizeLaunchArgsForHarness(normalizeLocalAgentHarness(record.harness), record.launchArgs));
12011
12269
  if (normalizeLocalAgentHarness(record.harness) === "codex") {
12012
12270
  return `exec bash ${JSON.stringify(workerScript ?? join16(relayAgentRuntimeDirectory(agentName), "codex-worker.sh"))}`;
12013
12271
  }
@@ -12379,6 +12637,7 @@ async function startLocalAgent(input) {
12379
12637
  const effectiveHarness = normalizeManagedHarness2(preferredHarness ?? configDefaultHarness, "claude");
12380
12638
  const transport = normalizeLocalAgentTransport(undefined, effectiveHarness);
12381
12639
  const sessionId = normalizeTmuxSessionName2(undefined, `${instance.id}-${effectiveHarness}`);
12640
+ const launchArgs = applyRequestedModelToLaunchArgs(effectiveHarness, [], input.model);
12382
12641
  const existingForInstance = overrides[instance.id];
12383
12642
  if (existingForInstance && normalizeProjectPath(existingForInstance.projectRoot) !== projectRoot) {
12384
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.`);
@@ -12392,13 +12651,14 @@ async function startLocalAgent(input) {
12392
12651
  projectConfigPath: coldProjectConfigPath,
12393
12652
  source: "manual",
12394
12653
  startedAt: nowSeconds2(),
12654
+ launchArgs,
12395
12655
  defaultHarness: effectiveHarness,
12396
12656
  harnessProfiles: {
12397
12657
  [effectiveHarness]: {
12398
12658
  cwd: effectiveCwd ?? projectRoot,
12399
12659
  transport,
12400
12660
  sessionId,
12401
- launchArgs: []
12661
+ launchArgs
12402
12662
  }
12403
12663
  },
12404
12664
  runtime: {
@@ -12419,6 +12679,9 @@ async function startLocalAgent(input) {
12419
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.`);
12420
12680
  }
12421
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);
12422
12685
  overrides[instance.id] = {
12423
12686
  agentId: instance.id,
12424
12687
  definitionId: requestedDefinitionId,
@@ -12429,10 +12692,16 @@ async function startLocalAgent(input) {
12429
12692
  source: "manual",
12430
12693
  startedAt: matchingOverride.startedAt ?? nowSeconds2(),
12431
12694
  systemPrompt: matchingOverride.systemPrompt,
12432
- launchArgs: matchingOverride.launchArgs,
12695
+ launchArgs: nextHarness === nextDefaultHarness ? nextLaunchArgs : matchingOverride.launchArgs,
12433
12696
  capabilities: matchingOverride.capabilities,
12434
- defaultHarness: normalizeManagedHarness2(preferredHarness ?? matchingOverride.defaultHarness, "claude"),
12435
- harnessProfiles: matchingOverride.harnessProfiles,
12697
+ defaultHarness: nextDefaultHarness,
12698
+ harnessProfiles: {
12699
+ ...matchingOverride.harnessProfiles ?? {},
12700
+ [nextHarness]: {
12701
+ ...overrideHarnessProfile(matchingOverride, nextHarness),
12702
+ launchArgs: nextLaunchArgs
12703
+ }
12704
+ },
12436
12705
  runtime: {
12437
12706
  cwd: effectiveCwd ?? matchingOverride.runtime?.cwd ?? matchingProjectRoot,
12438
12707
  harness: resolvedHarness,
@@ -12444,6 +12713,22 @@ async function startLocalAgent(input) {
12444
12713
  await writeRelayAgentOverrides(overrides);
12445
12714
  targetAgentId = instance.id;
12446
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
+ }
12447
12732
  targetAgentId = matchingAgentId;
12448
12733
  }
12449
12734
  await ensureLocalAgentBindingOnline(targetAgentId, process.env.OPENSCOUT_NODE_ID ?? "local", {
@@ -12556,6 +12841,7 @@ function readPersistedClaudeSessionId(agentName) {
12556
12841
  }
12557
12842
  function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
12558
12843
  const normalizedRecord = normalizeLocalAgentRecord(agentId, record);
12844
+ const configuredModel = readLaunchModelForHarness(normalizeLocalAgentHarness(normalizedRecord.harness), normalizedRecord.launchArgs);
12559
12845
  const definitionId = normalizedRecord.definitionId ?? agentId;
12560
12846
  const displayName = titleCaseLocalAgentName(definitionId);
12561
12847
  const projectRoot = normalizedRecord.projectRoot ?? normalizedRecord.cwd;
@@ -12580,7 +12866,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
12580
12866
  defaultSelector: instance.defaultSelector,
12581
12867
  nodeQualifier: instance.nodeQualifier,
12582
12868
  workspaceQualifier: instance.workspaceQualifier,
12583
- branch: instance.branch
12869
+ branch: instance.branch,
12870
+ ...configuredModel ? { model: configuredModel } : {}
12584
12871
  }
12585
12872
  },
12586
12873
  agent: {
@@ -12607,7 +12894,8 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
12607
12894
  defaultSelector: instance.defaultSelector,
12608
12895
  nodeQualifier: instance.nodeQualifier,
12609
12896
  workspaceQualifier: instance.workspaceQualifier,
12610
- branch: instance.branch
12897
+ branch: instance.branch,
12898
+ ...configuredModel ? { model: configuredModel } : {}
12611
12899
  },
12612
12900
  agentClass: "general",
12613
12901
  capabilities: normalizeLocalAgentCapabilities(normalizedRecord.capabilities),
@@ -12640,6 +12928,7 @@ function buildLocalAgentBinding(agentId, record, alive, nodeId, source) {
12640
12928
  nodeQualifier: instance.nodeQualifier,
12641
12929
  workspaceQualifier: instance.workspaceQualifier,
12642
12930
  branch: instance.branch,
12931
+ ...configuredModel ? { model: configuredModel } : {},
12643
12932
  ...externalSessionId ? { externalSessionId } : {}
12644
12933
  }
12645
12934
  }
@@ -12841,11 +13130,11 @@ var init_local_agents = __esm(async () => {
12841
13130
 
12842
13131
  // packages/web/server/index.ts
12843
13132
  import { existsSync as existsSync15 } from "fs";
12844
- import { dirname as dirname13, join as join21, resolve as resolve10 } from "path";
12845
- import { fileURLToPath as fileURLToPath8 } from "url";
13133
+ import { dirname as dirname14, join as join20, resolve as resolve11 } from "path";
13134
+ import { fileURLToPath as fileURLToPath9 } from "url";
12846
13135
 
12847
13136
  // packages/web/server/create-openscout-web-server.ts
12848
- import { existsSync as existsSync14, rmSync as rmSync3 } from "fs";
13137
+ import { existsSync as existsSync13, rmSync as rmSync3 } from "fs";
12849
13138
  import { dirname as dirname12, resolve as resolve9 } from "path";
12850
13139
  import { fileURLToPath as fileURLToPath7 } from "url";
12851
13140
 
@@ -15745,6 +16034,12 @@ function transientBrokerWorkingStatusPredicate(alias) {
15745
16034
  AND ${alias}.body LIKE '% is working.'
15746
16035
  )`;
15747
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
+ }
15748
16043
  function workPhaseFromFlightState(state) {
15749
16044
  switch (state) {
15750
16045
  case "queued":
@@ -15908,10 +16203,7 @@ function queryActivity(limit = 60) {
15908
16203
  FROM activity_items ai
15909
16204
  LEFT JOIN actors ac ON ac.id = ai.actor_id
15910
16205
  WHERE ai.kind != 'ask_replied'
15911
- AND NOT (
15912
- ai.kind = 'flight_updated'
15913
- AND COALESCE(ai.summary, '') LIKE 'Stale running flight reconciled:%'
15914
- )
16206
+ AND ${staleFlightActivityPredicate("ai")}
15915
16207
  ORDER BY ai.ts DESC
15916
16208
  LIMIT ?`).all(limit);
15917
16209
  const items = rows.map((r) => ({
@@ -16637,6 +16929,7 @@ function queryFleetActivity(opts) {
16637
16929
  filters.push("ai.conversation_id = ?");
16638
16930
  params.push(opts.conversationId);
16639
16931
  }
16932
+ const scopedFilters = sqlJoinClauses(filters, "OR");
16640
16933
  const sql = `SELECT
16641
16934
  ai.id,
16642
16935
  ai.kind,
@@ -16655,7 +16948,10 @@ function queryFleetActivity(opts) {
16655
16948
  ai.session_id
16656
16949
  FROM activity_items ai
16657
16950
  LEFT JOIN actors ac ON ac.id = ai.actor_id
16658
- ${sqlWhereClause(filters, "OR")}
16951
+ ${sqlWhereClause([
16952
+ staleFlightActivityPredicate("ai"),
16953
+ scopedFilters ? `(${scopedFilters})` : null
16954
+ ])}
16659
16955
  ORDER BY ai.ts DESC
16660
16956
  LIMIT ?`;
16661
16957
  const rows = db().prepare(sql).all(...params, opts?.limit ?? 80);
@@ -16738,6 +17034,10 @@ function queryFleetAskRows(requesterIds, limit) {
16738
17034
  )
16739
17035
  LEFT JOIN collaboration_records cr ON cr.id = inv.collaboration_record_id
16740
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
+ )
16741
17041
  ORDER BY COALESCE(f.completed_at, f.started_at, inv.created_at) DESC
16742
17042
  LIMIT ?`).all(...requesterIds, limit);
16743
17043
  }
@@ -21055,415 +21355,6 @@ init_setup();
21055
21355
  init_support_paths();
21056
21356
  init_claude_stream_json();
21057
21357
  init_harness_catalog();
21058
-
21059
- // ../hudson/packages/hudson-relay/src/relay/session.ts
21060
- import { createRequire } from "module";
21061
- import { execSync as execSync2 } from "child_process";
21062
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
21063
- import { join as join19, dirname as pathDirname } from "path";
21064
- var require2 = createRequire(import.meta.url);
21065
- var pty = require2("node-pty");
21066
- var DEFAULT_ORPHAN_TTL_MS = 30 * 60 * 1000;
21067
- var MAX_BUFFER_SIZE = 512 * 1024;
21068
- var sessions3 = new Map;
21069
- function generateId() {
21070
- return Math.random().toString(36).slice(2, 10);
21071
- }
21072
- function send(ws, data) {
21073
- if (ws.readyState === 1) {
21074
- ws.send(JSON.stringify(data));
21075
- }
21076
- }
21077
- function resolveCwd(raw2) {
21078
- const home = process.env.HOME || "/tmp";
21079
- const expanded = (raw2 || home).replace(/^~/, home);
21080
- if (existsSync13(expanded))
21081
- return expanded;
21082
- try {
21083
- mkdirSync6(expanded, { recursive: true });
21084
- console.log(`[relay] Created missing cwd: ${expanded}`);
21085
- return expanded;
21086
- } catch {
21087
- console.warn(`[relay] Could not create cwd ${expanded}, falling back to ${home}`);
21088
- return home;
21089
- }
21090
- }
21091
- function findBin(name, envOverride) {
21092
- if (envOverride && process.env[envOverride])
21093
- return process.env[envOverride];
21094
- try {
21095
- return execSync2(`which ${name}`, { encoding: "utf8" }).trim() || null;
21096
- } catch {
21097
- return null;
21098
- }
21099
- }
21100
- function findClaudeBin() {
21101
- return findBin("claude", "CLAUDE_BIN");
21102
- }
21103
- function findPiBin() {
21104
- return findBin("pi", "PI_BIN");
21105
- }
21106
- function tmuxSessionExists2(name) {
21107
- try {
21108
- execSync2(`tmux has-session -t ${name} 2>/dev/null`, { encoding: "utf8" });
21109
- return true;
21110
- } catch {
21111
- return false;
21112
- }
21113
- }
21114
- function bootstrapFiles(cwd, files, sessionId) {
21115
- for (const [relPath, content] of Object.entries(files)) {
21116
- const absPath = join19(cwd, relPath);
21117
- if (!existsSync13(absPath)) {
21118
- try {
21119
- const dir = pathDirname(absPath);
21120
- if (!existsSync13(dir))
21121
- mkdirSync6(dir, { recursive: true });
21122
- writeFileSync6(absPath, content, "utf-8");
21123
- console.log(`[relay] Session ${sessionId}: bootstrapped ${relPath}`);
21124
- } catch (err) {
21125
- console.warn(`[relay] Session ${sessionId}: failed to bootstrap ${relPath}:`, err);
21126
- }
21127
- }
21128
- }
21129
- }
21130
- function spawnTmuxSession(tmuxName, cols, rows, cwd, claudeBin, claudeArgs, env) {
21131
- const exists = tmuxSessionExists2(tmuxName);
21132
- if (!exists) {
21133
- const shellCmd = [claudeBin, ...claudeArgs].map((a) => a.includes(" ") ? `'${a}'` : a).join(" ");
21134
- execSync2(`tmux new-session -d -s ${tmuxName} -x ${cols} -y ${rows} -c '${cwd}' '${shellCmd}'`, { env });
21135
- console.log(`[relay] Created tmux session: ${tmuxName}`);
21136
- } else {
21137
- try {
21138
- execSync2(`tmux resize-window -t ${tmuxName} -x ${cols} -y ${rows} 2>/dev/null`);
21139
- } catch {}
21140
- console.log(`[relay] Attaching to existing tmux session: ${tmuxName}`);
21141
- }
21142
- return pty.spawn("tmux", ["attach", "-t", tmuxName], {
21143
- name: "xterm-256color",
21144
- cols,
21145
- rows,
21146
- cwd,
21147
- env
21148
- });
21149
- }
21150
- function createSession(ws, msg) {
21151
- const id = generateId();
21152
- const cols = Math.max(msg.cols || 80, 20);
21153
- const rows = Math.max(msg.rows || 24, 4);
21154
- const backend = msg.backend || "pty";
21155
- const tmuxName = msg.tmuxSession || `hudson-${id}`;
21156
- const agent = msg.agent || "claude";
21157
- let agentBin;
21158
- if (agent === "pi") {
21159
- agentBin = findPiBin();
21160
- if (!agentBin) {
21161
- const reason = "pi CLI not found. Install it with: npm install -g @mariozechner/pi-coding-agent";
21162
- console.error(`[relay] Session ${id} failed: ${reason}`);
21163
- send(ws, { type: "session:error", error: reason });
21164
- return null;
21165
- }
21166
- } else {
21167
- agentBin = findClaudeBin();
21168
- if (!agentBin) {
21169
- const reason = "Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code";
21170
- console.error(`[relay] Session ${id} failed: ${reason}`);
21171
- send(ws, { type: "session:error", error: reason });
21172
- return null;
21173
- }
21174
- }
21175
- if (!existsSync13(agentBin)) {
21176
- const reason = `${agent} binary not found at ${agentBin}`;
21177
- console.error(`[relay] Session ${id} failed: ${reason}`);
21178
- send(ws, { type: "session:error", error: reason });
21179
- return null;
21180
- }
21181
- if (backend === "tmux" && !findBin("tmux")) {
21182
- const reason = "tmux not found. Install it with: brew install tmux";
21183
- console.error(`[relay] Session ${id} failed: ${reason}`);
21184
- send(ws, { type: "session:error", error: reason });
21185
- return null;
21186
- }
21187
- const cwd = resolveCwd(msg.cwd);
21188
- if (msg.workspaceFiles) {
21189
- bootstrapFiles(cwd, msg.workspaceFiles, id);
21190
- }
21191
- let agentArgs;
21192
- if (agent === "pi") {
21193
- agentArgs = ["--verbose"];
21194
- if (msg.provider)
21195
- agentArgs.push("--provider", msg.provider);
21196
- if (msg.model)
21197
- agentArgs.push("--model", msg.model);
21198
- if (msg.systemPrompt)
21199
- agentArgs.push("--system-prompt", msg.systemPrompt);
21200
- } else {
21201
- agentArgs = ["--verbose"];
21202
- if (msg.systemPrompt)
21203
- agentArgs.push("--system-prompt", msg.systemPrompt);
21204
- }
21205
- const env = { ...process.env, TERM: "xterm-256color", FORCE_COLOR: "1" };
21206
- delete env.CLAUDECODE;
21207
- let ptyProcess;
21208
- try {
21209
- if (backend === "tmux") {
21210
- console.log(`[relay] Session ${id}: tmux backend (session: ${tmuxName}) in ${cwd} [agent: ${agent}]`);
21211
- ptyProcess = spawnTmuxSession(tmuxName, cols, rows, cwd, agentBin, agentArgs, env);
21212
- } else {
21213
- console.log(`[relay] Session ${id}: pty backend, spawning ${agentBin} in ${cwd} [agent: ${agent}]`);
21214
- ptyProcess = pty.spawn(agentBin, agentArgs, {
21215
- name: "xterm-256color",
21216
- cols,
21217
- rows,
21218
- cwd,
21219
- env
21220
- });
21221
- }
21222
- } catch (err) {
21223
- const message = err instanceof Error ? err.message : String(err);
21224
- console.error(`[relay] Session ${id}: failed to spawn PTY \u2014 ${message}`);
21225
- send(ws, { type: "session:error", error: `Failed to spawn terminal: ${message}` });
21226
- return null;
21227
- }
21228
- const orphanTTL = msg.orphanTTL && msg.orphanTTL > 0 ? msg.orphanTTL : DEFAULT_ORPHAN_TTL_MS;
21229
- const session = {
21230
- id,
21231
- pty: ptyProcess,
21232
- ws,
21233
- outputBuffer: "",
21234
- cols,
21235
- rows,
21236
- reapTimer: null,
21237
- orphanTTL,
21238
- backend,
21239
- ...backend === "tmux" ? { tmuxSession: tmuxName } : {},
21240
- exited: false,
21241
- exitCode: null
21242
- };
21243
- const startTime = Date.now();
21244
- ptyProcess.onData((data) => {
21245
- session.outputBuffer += data;
21246
- if (session.outputBuffer.length > MAX_BUFFER_SIZE) {
21247
- session.outputBuffer = session.outputBuffer.slice(-MAX_BUFFER_SIZE);
21248
- }
21249
- if (session.ws && session.ws.readyState === 1) {
21250
- send(session.ws, { type: "terminal:data", data });
21251
- }
21252
- });
21253
- ptyProcess.onExit(({ exitCode }) => {
21254
- session.exited = true;
21255
- session.exitCode = exitCode;
21256
- const uptime = Date.now() - startTime;
21257
- const crashed = exitCode !== 0 && uptime < 5000;
21258
- let reason;
21259
- if (crashed) {
21260
- const clean = session.outputBuffer.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").trim();
21261
- const lines = clean.split(`
21262
- `).filter((l) => l.trim()).slice(-5);
21263
- reason = lines.join(`
21264
- `) || `Process exited with code ${exitCode}`;
21265
- console.error(`[relay] Session ${id} crashed after ${uptime}ms (code ${exitCode}): ${reason}`);
21266
- }
21267
- if (session.ws) {
21268
- send(session.ws, { type: "session:exit", exitCode, ...reason ? { reason } : {} });
21269
- }
21270
- scheduleReap(session, 1e4);
21271
- });
21272
- sessions3.set(id, session);
21273
- console.log(`[relay] Session ${id} created (${cols}x${rows})`);
21274
- return session;
21275
- }
21276
- function attachSession(session, ws, cols, rows) {
21277
- if (session.reapTimer) {
21278
- clearTimeout(session.reapTimer);
21279
- session.reapTimer = null;
21280
- }
21281
- session.ws = ws;
21282
- if (cols && rows) {
21283
- const c = Math.max(cols, 20);
21284
- const r = Math.max(rows, 4);
21285
- if (c !== session.cols || r !== session.rows) {
21286
- session.pty.resize(c, r);
21287
- session.cols = c;
21288
- session.rows = r;
21289
- }
21290
- }
21291
- if (session.exited) {
21292
- send(ws, { type: "session:exit", exitCode: session.exitCode });
21293
- } else if (session.outputBuffer.length > 0) {
21294
- send(ws, { type: "terminal:data", data: session.outputBuffer });
21295
- }
21296
- console.log(`[relay] Session ${session.id} reconnected`);
21297
- }
21298
- function detachSession(session) {
21299
- session.ws = null;
21300
- if (session.exited) {
21301
- scheduleReap(session, 5000);
21302
- } else {
21303
- scheduleReap(session, session.orphanTTL);
21304
- console.log(`[relay] Session ${session.id} detached (orphaned for ${session.orphanTTL / 1000}s)`);
21305
- }
21306
- }
21307
- function scheduleReap(session, delay) {
21308
- if (session.reapTimer)
21309
- clearTimeout(session.reapTimer);
21310
- session.reapTimer = setTimeout(() => {
21311
- if (!session.ws) {
21312
- destroy(session.id);
21313
- }
21314
- }, delay);
21315
- }
21316
- function destroy(sessionId) {
21317
- const session = sessions3.get(sessionId);
21318
- if (!session)
21319
- return;
21320
- if (session.reapTimer)
21321
- clearTimeout(session.reapTimer);
21322
- try {
21323
- session.pty.kill();
21324
- } catch {}
21325
- sessions3.delete(sessionId);
21326
- if (session.backend === "tmux") {
21327
- console.log(`[relay] Session ${sessionId} bridge destroyed (tmux session '${session.tmuxSession}' still alive)`);
21328
- } else {
21329
- console.log(`[relay] Session ${sessionId} destroyed`);
21330
- }
21331
- }
21332
-
21333
- // packages/web/server/relay.ts
21334
- import { mkdir as mkdir7 } from "fs/promises";
21335
- import { join as join20 } from "path";
21336
- import { randomUUID as randomUUID3 } from "crypto";
21337
- var UPLOAD_DIR = "/tmp/scout-uploads";
21338
- async function handleRelayUpload(req) {
21339
- const { name, data } = await req.json();
21340
- if (!name || !data) {
21341
- return Response.json({ error: "Missing name or data" }, { status: 400 });
21342
- }
21343
- await mkdir7(UPLOAD_DIR, { recursive: true });
21344
- const filename = `${randomUUID3()}-${name}`;
21345
- const filepath = join20(UPLOAD_DIR, filename);
21346
- await Bun.write(filepath, Buffer.from(data, "base64"));
21347
- return Response.json({ path: filepath });
21348
- }
21349
- var _pendingCommand = null;
21350
- function queueTerminalCommand(cmd) {
21351
- for (const [, session] of sessions3) {
21352
- if (!session.exited) {
21353
- session.pty.write(cmd + `
21354
- `);
21355
- return;
21356
- }
21357
- }
21358
- _pendingCommand = cmd;
21359
- }
21360
- function drainPendingCommand() {
21361
- const cmd = _pendingCommand;
21362
- _pendingCommand = null;
21363
- return cmd;
21364
- }
21365
- function asRelay(ws) {
21366
- return ws;
21367
- }
21368
- function parseMessage(raw2) {
21369
- try {
21370
- const msg = JSON.parse(raw2);
21371
- if (typeof msg.type === "string")
21372
- return msg;
21373
- } catch {}
21374
- return null;
21375
- }
21376
- var relayWebSocket = {
21377
- open(_ws) {},
21378
- message(ws, raw2) {
21379
- const msg = parseMessage(typeof raw2 === "string" ? raw2 : raw2.toString());
21380
- if (!msg)
21381
- return;
21382
- const rws = asRelay(ws);
21383
- switch (msg.type) {
21384
- case "session:init": {
21385
- if (ws.data.sessionId) {
21386
- const prev = sessions3.get(ws.data.sessionId);
21387
- if (prev)
21388
- detachSession(prev);
21389
- }
21390
- const session = createSession(rws, msg);
21391
- if (!session)
21392
- break;
21393
- ws.data.sessionId = session.id;
21394
- send(rws, { type: "session:ready", sessionId: session.id });
21395
- const pending = drainPendingCommand();
21396
- if (pending)
21397
- setTimeout(() => session.pty.write(pending + `
21398
- `), 400);
21399
- break;
21400
- }
21401
- case "session:reconnect": {
21402
- const existing = sessions3.get(msg.sessionId);
21403
- if (existing && !existing.exited) {
21404
- if (ws.data.sessionId && ws.data.sessionId !== msg.sessionId) {
21405
- const prev = sessions3.get(ws.data.sessionId);
21406
- if (prev)
21407
- detachSession(prev);
21408
- }
21409
- if (existing.ws && existing.ws !== rws) {
21410
- send(existing.ws, { type: "session:detached" });
21411
- }
21412
- ws.data.sessionId = existing.id;
21413
- attachSession(existing, rws, msg.cols, msg.rows);
21414
- send(rws, {
21415
- type: "session:ready",
21416
- sessionId: existing.id,
21417
- reconnected: true
21418
- });
21419
- const pending = drainPendingCommand();
21420
- if (pending)
21421
- setTimeout(() => existing.pty.write(pending + `
21422
- `), 400);
21423
- } else {
21424
- send(rws, { type: "session:expired", sessionId: msg.sessionId });
21425
- }
21426
- break;
21427
- }
21428
- case "terminal:input": {
21429
- if (!ws.data.sessionId)
21430
- return;
21431
- const session = sessions3.get(ws.data.sessionId);
21432
- if (session && !session.exited) {
21433
- session.pty.write(msg.data);
21434
- }
21435
- break;
21436
- }
21437
- case "terminal:resize": {
21438
- if (!ws.data.sessionId)
21439
- return;
21440
- const session = sessions3.get(ws.data.sessionId);
21441
- if (session && !session.exited) {
21442
- const cols = Math.max(msg.cols || 80, 20);
21443
- const rows = Math.max(msg.rows || 24, 4);
21444
- session.pty.resize(cols, rows);
21445
- session.cols = cols;
21446
- session.rows = rows;
21447
- }
21448
- break;
21449
- }
21450
- }
21451
- },
21452
- close(ws) {
21453
- if (ws.data.sessionId) {
21454
- const session = sessions3.get(ws.data.sessionId);
21455
- if (session)
21456
- detachSession(session);
21457
- ws.data.sessionId = null;
21458
- }
21459
- }
21460
- };
21461
- function destroyAllRelaySessions() {
21462
- for (const [id] of sessions3)
21463
- destroy(id);
21464
- }
21465
-
21466
- // packages/web/server/create-openscout-web-server.ts
21467
21358
  function parseOptionalPositiveInt(value, fallback) {
21468
21359
  if (typeof value !== "string" || value.trim().length === 0) {
21469
21360
  return fallback;
@@ -21518,7 +21409,7 @@ function resolveStaticRoot(staticRoot) {
21518
21409
  return configured;
21519
21410
  }
21520
21411
  const bundled = resolveBundledStaticClientRoot(import.meta.url);
21521
- if (existsSync14(resolve9(bundled, "index.html"))) {
21412
+ if (existsSync13(resolve9(bundled, "index.html"))) {
21522
21413
  return bundled;
21523
21414
  }
21524
21415
  return resolveSourceStaticClientRoot(import.meta.url);
@@ -21791,7 +21682,15 @@ async function createOpenScoutWebServer(options) {
21791
21682
  const body = await c.req.json();
21792
21683
  if (!body.command)
21793
21684
  return c.json({ error: "missing command" }, 400);
21794
- queueTerminalCommand(body.command);
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
+ }
21795
21694
  return c.json({ ok: true });
21796
21695
  });
21797
21696
  app.post("/api/agents/:agentId/interrupt", async (c) => {
@@ -21887,6 +21786,254 @@ async function createOpenScoutWebServer(options) {
21887
21786
  return { app, warmupCaches };
21888
21787
  }
21889
21788
 
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 });
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 });
21804
+ }
21805
+ function flushPending(ws) {
21806
+ const upstream = ws.data.upstream;
21807
+ if (!upstream || upstream.readyState !== WebSocket.OPEN) {
21808
+ return;
21809
+ }
21810
+ for (const payload of ws.data.pending) {
21811
+ upstream.send(payload);
21812
+ }
21813
+ ws.data.pending.length = 0;
21814
+ }
21815
+ function forwardToClient(ws, data) {
21816
+ if (ws.readyState !== WebSocket.OPEN) {
21817
+ return;
21818
+ }
21819
+ if (typeof data === "string") {
21820
+ ws.send(data);
21821
+ return;
21822
+ }
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;
21830
+ }
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
+ }
21880
+ }
21881
+ };
21882
+ }
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;
21895
+ }
21896
+ }
21897
+ return webPort + 1;
21898
+ }
21899
+ function relayHostForWebHost(hostname2) {
21900
+ return process.env.OPENSCOUT_WEB_TERMINAL_RELAY_HOST?.trim() || hostname2;
21901
+ }
21902
+ function relayLoopbackHost(hostname2) {
21903
+ if (hostname2 === "0.0.0.0" || hostname2 === "::") {
21904
+ return "127.0.0.1";
21905
+ }
21906
+ return hostname2;
21907
+ }
21908
+ async function isHealthy(targetHttpUrl) {
21909
+ try {
21910
+ const response = await fetch(`${targetHttpUrl}/health`, {
21911
+ signal: AbortSignal.timeout(1000)
21912
+ });
21913
+ return response.ok;
21914
+ } catch {
21915
+ return false;
21916
+ }
21917
+ }
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));
21925
+ }
21926
+ return false;
21927
+ }
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");
21956
+ }
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");
21974
+ }
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;
21998
+ }
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;
22008
+ }
22009
+ throw new Error(`Terminal relay did not become healthy at ${targetHttpUrl}`);
22010
+ }
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");
22035
+ }
22036
+
21890
22037
  // packages/web/server/index.ts
21891
22038
  var port = Number(process.env.OPENSCOUT_WEB_PORT ?? process.env.SCOUT_WEB_PORT ?? "3200");
21892
22039
  var hostname2 = process.env.OPENSCOUT_WEB_HOST?.trim() || process.env.SCOUT_WEB_HOST?.trim() || "127.0.0.1";
@@ -21896,13 +22043,13 @@ function resolveStaticRoot2() {
21896
22043
  if (process.env.OPENSCOUT_WEB_STATIC_ROOT?.trim()) {
21897
22044
  return process.env.OPENSCOUT_WEB_STATIC_ROOT.trim();
21898
22045
  }
21899
- const selfDir = dirname13(fileURLToPath8(import.meta.url));
21900
- const siblingClientRoot = join21(selfDir, "client");
21901
- if (existsSync15(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"))) {
21902
22049
  return siblingClientRoot;
21903
22050
  }
21904
- const sourceDistClientRoot = resolve10(selfDir, "../dist/client");
21905
- if (existsSync15(join21(sourceDistClientRoot, "index.html"))) {
22051
+ const sourceDistClientRoot = resolve11(selfDir, "../dist/client");
22052
+ if (existsSync15(join20(sourceDistClientRoot, "index.html"))) {
21906
22053
  return sourceDistClientRoot;
21907
22054
  }
21908
22055
  return;
@@ -21911,26 +22058,47 @@ var staticRoot = resolveStaticRoot2();
21911
22058
  var viteDevUrl = process.env.OPENSCOUT_WEB_VITE_URL?.trim() || undefined;
21912
22059
  var useViteProxy = Boolean(viteDevUrl) || !staticRoot;
21913
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
+ }
21914
22071
  var { app, warmupCaches } = await createOpenScoutWebServer({
21915
22072
  currentDirectory,
21916
22073
  shellStateCacheTtlMs,
21917
22074
  assetMode: useViteProxy ? "vite-proxy" : "static",
21918
22075
  viteDevUrl,
21919
- staticRoot
22076
+ staticRoot,
22077
+ runTerminalCommand: terminalRelay?.queueCommand
21920
22078
  });
21921
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
+ };
21922
22089
  var server = Bun.serve({
21923
22090
  port,
21924
22091
  hostname: hostname2,
21925
22092
  idleTimeout: idleTimeoutSeconds,
21926
- fetch(req, server2) {
22093
+ async fetch(req, server2) {
21927
22094
  const url = new URL(req.url);
21928
22095
  if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
21929
- const ok = server2.upgrade(req, { data: { sessionId: null } });
22096
+ const ok = server2.upgrade(req, { data: { upstream: null, pending: [] } });
21930
22097
  return ok ? undefined : new Response("WebSocket upgrade failed", { status: 500 });
21931
22098
  }
21932
22099
  if (req.method === "GET" && url.pathname === "/health") {
21933
- 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 });
21934
22102
  }
21935
22103
  if (req.method === "POST" && (url.pathname === "/api/upload" || url.pathname === "/api/relay/upload")) {
21936
22104
  return handleRelayUpload(req);
@@ -21941,8 +22109,8 @@ var server = Bun.serve({
21941
22109
  });
21942
22110
  var shutdown = () => {
21943
22111
  console.log(`
21944
- [scout] Shutting down relay sessions...`);
21945
- destroyAllRelaySessions();
22112
+ [scout] Shutting down terminal relay...`);
22113
+ terminalRelay?.shutdown();
21946
22114
  server.stop();
21947
22115
  process.exit(0);
21948
22116
  };