@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.
- package/README.md +2 -2
- package/dist/client/assets/{arc.es-Ccqmb9Pq.js → arc.es-DgeMPL-I.js} +1 -1
- package/dist/client/assets/index-25TJx0iN.js +140 -0
- package/dist/client/assets/index-B1kglsyi.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/main.mjs +1560 -223
- package/dist/pair-supervisor.mjs +1230 -115
- package/dist/scout-control-plane-web.mjs +935 -571
- package/package.json +2 -2
- package/dist/client/assets/index-CN8PCsrs.css +0 -1
- package/dist/client/assets/index-X8QwrbtL.js +0 -140
|
@@ -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,
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8798
|
-
const [rawLog, persistedSessionId] = await Promise.all([
|
|
8875
|
+
const [rawLog, catalog] = await Promise.all([
|
|
8799
8876
|
readOptionalFile2(stdoutLogPath),
|
|
8800
|
-
|
|
8877
|
+
readSessionCatalog(options.runtimeDirectory)
|
|
8801
8878
|
]);
|
|
8802
8879
|
if (!rawLog) {
|
|
8803
8880
|
return null;
|
|
8804
8881
|
}
|
|
8805
|
-
return buildClaudeStreamJsonSessionSnapshot(rawLog, options,
|
|
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
|
|
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 {
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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 (
|
|
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") &&
|
|
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 (
|
|
10743
|
+
if (existsSync10(candidate)) {
|
|
10529
10744
|
return candidate;
|
|
10530
10745
|
}
|
|
10531
10746
|
}
|
|
10532
10747
|
const homeBun = join15(homedir11(), ".bun", "bin", "bun");
|
|
10533
|
-
if (
|
|
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 (!
|
|
10938
|
+
if (!existsSync10(path)) {
|
|
10724
10939
|
return [];
|
|
10725
10940
|
}
|
|
10726
|
-
return
|
|
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 =
|
|
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 (
|
|
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
|
|
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 (!
|
|
11230
|
+
if (!existsSync11(supportPaths.settingsPath)) {
|
|
11015
11231
|
return dirname11(projectPath);
|
|
11016
11232
|
}
|
|
11017
|
-
const raw2 = JSON.parse(
|
|
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 `
|
|
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 (
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ??
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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 =
|
|
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:
|
|
12331
|
-
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
|
|
12715
|
-
import { dirname as
|
|
12716
|
-
import { fileURLToPath as
|
|
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
|
|
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
|
|
15491
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
|
|
16726
|
-
|
|
16727
|
-
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
|
|
16731
|
-
|
|
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
|
|
16757
|
-
|
|
16758
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
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
|
|
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
|
-
|
|
16823
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
19770
|
-
const
|
|
19771
|
-
if (!
|
|
19772
|
-
return
|
|
20139
|
+
async function readTailscaleSelf() {
|
|
20140
|
+
const summary = await readTailscaleStatusSummary();
|
|
20141
|
+
if (!summary) {
|
|
20142
|
+
return null;
|
|
19773
20143
|
}
|
|
19774
|
-
return
|
|
20144
|
+
return summary.self;
|
|
19775
20145
|
}
|
|
19776
|
-
async function
|
|
20146
|
+
async function readTailscaleStatusSummary() {
|
|
19777
20147
|
const status = await readStatusJson();
|
|
19778
20148
|
if (!status) {
|
|
19779
20149
|
return null;
|
|
19780
20150
|
}
|
|
19781
|
-
return
|
|
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 (
|
|
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 (!
|
|
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:
|
|
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
|
|
21097
|
+
const summary = await readTailscaleStatusSummary();
|
|
21098
|
+
const peers = summary?.peers ?? [];
|
|
20693
21099
|
return {
|
|
20694
|
-
available:
|
|
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 (
|
|
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
|
-
//
|
|
21312
|
-
import {
|
|
21313
|
-
import {
|
|
21314
|
-
import {
|
|
21315
|
-
|
|
21316
|
-
|
|
21317
|
-
|
|
21318
|
-
|
|
21319
|
-
|
|
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
|
|
21367
|
-
|
|
21368
|
-
|
|
21369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21395
|
-
name: "xterm-256color",
|
|
21396
|
-
cols,
|
|
21397
|
-
rows,
|
|
21398
|
-
cwd,
|
|
21399
|
-
env
|
|
21400
|
-
});
|
|
21813
|
+
ws.data.pending.length = 0;
|
|
21401
21814
|
}
|
|
21402
|
-
function
|
|
21403
|
-
|
|
21404
|
-
|
|
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 (
|
|
21434
|
-
|
|
21435
|
-
|
|
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
|
-
|
|
21440
|
-
|
|
21441
|
-
|
|
21442
|
-
|
|
21443
|
-
|
|
21444
|
-
|
|
21445
|
-
|
|
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
|
-
|
|
21458
|
-
|
|
21459
|
-
|
|
21460
|
-
|
|
21461
|
-
|
|
21462
|
-
|
|
21463
|
-
|
|
21464
|
-
|
|
21465
|
-
|
|
21466
|
-
|
|
21467
|
-
|
|
21468
|
-
|
|
21469
|
-
|
|
21470
|
-
|
|
21471
|
-
|
|
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
|
-
|
|
21529
|
-
|
|
21530
|
-
|
|
21531
|
-
|
|
21532
|
-
|
|
21533
|
-
|
|
21534
|
-
|
|
21535
|
-
|
|
21536
|
-
|
|
21537
|
-
|
|
21538
|
-
|
|
21539
|
-
|
|
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
|
-
|
|
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
|
|
21551
|
-
|
|
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
|
|
21560
|
-
if (
|
|
21561
|
-
|
|
21562
|
-
|
|
21563
|
-
|
|
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
|
|
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
|
-
|
|
21576
|
-
|
|
21577
|
-
|
|
21578
|
-
|
|
21579
|
-
|
|
21580
|
-
|
|
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
|
-
|
|
21586
|
-
|
|
21587
|
-
|
|
21588
|
-
|
|
21589
|
-
|
|
21590
|
-
|
|
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
|
-
|
|
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
|
|
21605
|
-
|
|
21606
|
-
|
|
21607
|
-
|
|
21608
|
-
|
|
21609
|
-
|
|
21610
|
-
|
|
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
|
-
|
|
21613
|
-
|
|
21614
|
-
|
|
21615
|
-
|
|
21616
|
-
|
|
21617
|
-
|
|
21618
|
-
|
|
21619
|
-
|
|
21620
|
-
|
|
21621
|
-
|
|
21622
|
-
|
|
21623
|
-
|
|
21624
|
-
|
|
21625
|
-
|
|
21626
|
-
|
|
21627
|
-
if (!
|
|
21628
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21681
|
-
|
|
21682
|
-
|
|
21683
|
-
|
|
21684
|
-
|
|
21685
|
-
|
|
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
|
-
|
|
21690
|
-
|
|
21691
|
-
|
|
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 =
|
|
21704
|
-
const siblingClientRoot =
|
|
21705
|
-
if (
|
|
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 =
|
|
21709
|
-
if (
|
|
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: {
|
|
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
|
-
|
|
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
|
|
21749
|
-
|
|
22112
|
+
[scout] Shutting down terminal relay...`);
|
|
22113
|
+
terminalRelay?.shutdown();
|
|
21750
22114
|
server.stop();
|
|
21751
22115
|
process.exit(0);
|
|
21752
22116
|
};
|