labgate 0.5.31 → 0.5.33
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 +50 -2
- package/dist/cli.js +533 -0
- package/dist/cli.js.map +1 -1
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.js +45 -4
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/container.d.ts +3 -3
- package/dist/lib/container.js +144 -12
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/display-mcp.d.ts +10 -0
- package/dist/lib/display-mcp.js +160 -0
- package/dist/lib/display-mcp.js.map +1 -0
- package/dist/lib/display-store.d.ts +24 -0
- package/dist/lib/display-store.js +150 -0
- package/dist/lib/display-store.js.map +1 -0
- package/dist/lib/explorer-autopilot.d.ts +16 -0
- package/dist/lib/explorer-autopilot.js +573 -0
- package/dist/lib/explorer-autopilot.js.map +1 -0
- package/dist/lib/explorer-claude.d.ts +16 -0
- package/dist/lib/explorer-claude.js +361 -0
- package/dist/lib/explorer-claude.js.map +1 -0
- package/dist/lib/explorer-compare.d.ts +9 -0
- package/dist/lib/explorer-compare.js +190 -0
- package/dist/lib/explorer-compare.js.map +1 -0
- package/dist/lib/explorer-eval.d.ts +23 -0
- package/dist/lib/explorer-eval.js +161 -0
- package/dist/lib/explorer-eval.js.map +1 -0
- package/dist/lib/explorer-gc.d.ts +11 -0
- package/dist/lib/explorer-gc.js +304 -0
- package/dist/lib/explorer-gc.js.map +1 -0
- package/dist/lib/explorer-git.d.ts +14 -0
- package/dist/lib/explorer-git.js +136 -0
- package/dist/lib/explorer-git.js.map +1 -0
- package/dist/lib/explorer-lock.d.ts +5 -0
- package/dist/lib/explorer-lock.js +100 -0
- package/dist/lib/explorer-lock.js.map +1 -0
- package/dist/lib/explorer-mcp.d.ts +11 -0
- package/dist/lib/explorer-mcp.js +611 -0
- package/dist/lib/explorer-mcp.js.map +1 -0
- package/dist/lib/explorer-retention.d.ts +4 -0
- package/dist/lib/explorer-retention.js +58 -0
- package/dist/lib/explorer-retention.js.map +1 -0
- package/dist/lib/explorer-store.d.ts +77 -0
- package/dist/lib/explorer-store.js +950 -0
- package/dist/lib/explorer-store.js.map +1 -0
- package/dist/lib/explorer-types.d.ts +161 -0
- package/dist/lib/explorer-types.js +3 -0
- package/dist/lib/explorer-types.js.map +1 -0
- package/dist/lib/explorer.d.ts +31 -0
- package/dist/lib/explorer.js +247 -0
- package/dist/lib/explorer.js.map +1 -0
- package/dist/lib/results-store.js +37 -3
- package/dist/lib/results-store.js.map +1 -1
- package/dist/lib/test/integration-harness.js +1 -1
- package/dist/lib/test/integration-harness.js.map +1 -1
- package/dist/lib/ui.html +5115 -2052
- package/dist/lib/ui.js +906 -39
- package/dist/lib/ui.js.map +1 -1
- package/dist/lib/web-terminal.js +4 -3
- package/dist/lib/web-terminal.js.map +1 -1
- package/dist/mcp-bundles/dataset-mcp.bundle.mjs +0 -8
- package/dist/mcp-bundles/display-mcp.bundle.mjs +30209 -0
- package/dist/mcp-bundles/explorer-mcp.bundle.mjs +40036 -0
- package/dist/mcp-bundles/results-mcp.bundle.mjs +30 -4
- package/package.json +3 -2
- package/templates/tsp-lab/API_CONTRACT.md +20 -0
- package/templates/tsp-lab/EVAL.md +20 -0
- package/templates/tsp-lab/PROBLEM.md +18 -0
- package/templates/tsp-lab/data/generate_instances.py +51 -0
- package/templates/tsp-lab/data/instances.jsonl +12 -0
- package/templates/tsp-lab/eval.py +148 -0
- package/templates/tsp-lab/solver.py +88 -0
- package/templates/tsp-lab/stub-patches/enable_two_opt.patch +14 -0
package/dist/lib/ui.js
CHANGED
|
@@ -50,9 +50,13 @@ const audit_js_1 = require("./audit.js");
|
|
|
50
50
|
const slurm_db_js_1 = require("./slurm-db.js");
|
|
51
51
|
const slurm_poller_js_1 = require("./slurm-poller.js");
|
|
52
52
|
const results_store_js_1 = require("./results-store.js");
|
|
53
|
+
const display_store_js_1 = require("./display-store.js");
|
|
53
54
|
const policy_js_1 = require("./policy.js");
|
|
54
55
|
const license_js_1 = require("./license.js");
|
|
55
56
|
const web_terminal_js_1 = require("./web-terminal.js");
|
|
57
|
+
const explorer_js_1 = require("./explorer.js");
|
|
58
|
+
const explorer_eval_js_1 = require("./explorer-eval.js");
|
|
59
|
+
const explorer_store_js_1 = require("./explorer-store.js");
|
|
56
60
|
const log = __importStar(require("./log.js"));
|
|
57
61
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
58
62
|
const HTML_PATH = (0, path_1.resolve)(__dirname, '..', 'lib', 'ui.html');
|
|
@@ -72,6 +76,9 @@ const IRIS_SAMPLE_DATASET_NAME = 'flowers-iris';
|
|
|
72
76
|
const IRIS_SAMPLE_SOURCE_URL = 'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv';
|
|
73
77
|
const PODMAN_SETUP_TIMEOUT_MS = 30 * 60 * 1000;
|
|
74
78
|
const PODMAN_SETUP_MAX_BUFFER = 16 * 1024 * 1024;
|
|
79
|
+
const EXPLORER_TSP_TEMPLATE_DIR = (0, path_1.resolve)(__dirname, '..', '..', 'templates', 'tsp-lab');
|
|
80
|
+
const EXPLORER_TSP_TEMPLATE_SOURCE_REPO = (0, path_1.join)((0, config_js_1.getExplorerRootDir)(), 'templates', 'tsp-lab-source');
|
|
81
|
+
const EXPLORER_ARTIFACT_READ_MAX_BYTES = 2 * 1024 * 1024;
|
|
75
82
|
const IRIS_SAMPLE_README = '# Iris Flowers Dataset (Sample)\n' +
|
|
76
83
|
'\n' +
|
|
77
84
|
'Source: https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv\n' +
|
|
@@ -88,6 +95,7 @@ function resolveIrisSampleSourceUrl() {
|
|
|
88
95
|
let slurmDB = null;
|
|
89
96
|
let slurmPoller = null;
|
|
90
97
|
let resultsStore = null;
|
|
98
|
+
let displayStore = null;
|
|
91
99
|
const REQUIRED_SLURM_COMMANDS = ['sbatch', 'squeue', 'sacct', 'scancel'];
|
|
92
100
|
const webTerminalBridges = new Map();
|
|
93
101
|
const WEB_TERMINAL_INIT_RETENTION_MS = 60 * 60 * 1000;
|
|
@@ -116,6 +124,12 @@ function getResultsStore() {
|
|
|
116
124
|
}
|
|
117
125
|
return resultsStore;
|
|
118
126
|
}
|
|
127
|
+
function getDisplayStore() {
|
|
128
|
+
if (!displayStore) {
|
|
129
|
+
displayStore = new display_store_js_1.DisplayStore((0, config_js_1.getDisplayDbPath)());
|
|
130
|
+
}
|
|
131
|
+
return displayStore;
|
|
132
|
+
}
|
|
119
133
|
function hasCommandInPath(command) {
|
|
120
134
|
const pathValue = (process.env.PATH || '').trim();
|
|
121
135
|
if (!pathValue)
|
|
@@ -1017,6 +1031,13 @@ function readRecordString(record, key) {
|
|
|
1017
1031
|
const value = record[key];
|
|
1018
1032
|
return typeof value === 'string' ? value : '';
|
|
1019
1033
|
}
|
|
1034
|
+
function normalizeToolUseId(value) {
|
|
1035
|
+
if (typeof value === 'string')
|
|
1036
|
+
return value.trim();
|
|
1037
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
1038
|
+
return String(value);
|
|
1039
|
+
return '';
|
|
1040
|
+
}
|
|
1020
1041
|
function collectClaudeTextFromContent(content) {
|
|
1021
1042
|
if (!Array.isArray(content))
|
|
1022
1043
|
return '';
|
|
@@ -1069,6 +1090,15 @@ function buildClaudeHeadlessApptainerArgs(config, workdir, prompt, resumeSession
|
|
|
1069
1090
|
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
1070
1091
|
const sifPath = (0, path_1.join)((0, config_js_1.getImagesDir)(), (0, container_js_1.imageToSifName)(config.image));
|
|
1071
1092
|
const resume = resumeSessionId.trim();
|
|
1093
|
+
// Ensure display.json exists before bind-mounting it
|
|
1094
|
+
const displayDbPath = (0, config_js_1.getDisplayDbPath)();
|
|
1095
|
+
if (!(0, fs_1.existsSync)(displayDbPath)) {
|
|
1096
|
+
(0, config_js_1.ensurePrivateDir)((0, path_1.dirname)(displayDbPath));
|
|
1097
|
+
(0, fs_1.writeFileSync)(displayDbPath, JSON.stringify({ version: 1, events: [] }, null, 2) + '\n', {
|
|
1098
|
+
encoding: 'utf-8',
|
|
1099
|
+
mode: config_js_1.PRIVATE_FILE_MODE,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1072
1102
|
return [
|
|
1073
1103
|
'exec',
|
|
1074
1104
|
'--containall',
|
|
@@ -1088,6 +1118,7 @@ function buildClaudeHeadlessApptainerArgs(config, workdir, prompt, resumeSession
|
|
|
1088
1118
|
return ['--bind', bindSpec];
|
|
1089
1119
|
}),
|
|
1090
1120
|
...((0, fs_1.existsSync)((0, config_js_1.getConfigPath)()) ? ['--bind', `${(0, config_js_1.getConfigPath)()}:/labgate-config/config.json`] : []),
|
|
1121
|
+
'--bind', `${(0, config_js_1.getDisplayDbPath)()}:/labgate-config/display.json`,
|
|
1091
1122
|
'--env', 'HOME=/home/sandbox',
|
|
1092
1123
|
'--env', 'ANTHROPIC_API_KEY=',
|
|
1093
1124
|
sifPath,
|
|
@@ -1164,27 +1195,32 @@ function closeWebTerminalBridgeClients(bridge, code = 4001, reason = 'labgate-br
|
|
|
1164
1195
|
}
|
|
1165
1196
|
async function ensureWebTerminalBridge(record) {
|
|
1166
1197
|
const existing = webTerminalBridges.get(record.id);
|
|
1167
|
-
if (existing && existing.pty)
|
|
1168
|
-
return existing;
|
|
1169
|
-
const ptyModule = await loadNodePtyModule();
|
|
1170
|
-
if (!ptyModule) {
|
|
1171
|
-
return null;
|
|
1172
|
-
}
|
|
1173
1198
|
let tmuxBin = 'tmux';
|
|
1174
1199
|
try {
|
|
1175
1200
|
tmuxBin = await (0, web_terminal_js_1.getTmuxBinary)();
|
|
1176
1201
|
}
|
|
1177
1202
|
catch (err) {
|
|
1203
|
+
if (existing && existing.pty) {
|
|
1204
|
+
log.warn(`Could not resolve tmux binary for web terminal bridge ${record.id}: ${err?.message ?? String(err)}`);
|
|
1205
|
+
return existing;
|
|
1206
|
+
}
|
|
1178
1207
|
log.warn(`Could not resolve tmux binary for web terminal bridge ${record.id}: ${err?.message ?? String(err)}`);
|
|
1179
1208
|
return null;
|
|
1180
1209
|
}
|
|
1181
1210
|
try {
|
|
1182
|
-
// Keep
|
|
1183
|
-
|
|
1211
|
+
// Keep web terminal copy/selection behavior reliable by letting xterm own
|
|
1212
|
+
// mouse selection instead of tmux copy-mode selection.
|
|
1213
|
+
await execFileAsync(tmuxBin, ['set-option', '-t', record.tmuxSession, 'mouse', 'off'], { timeout: 10_000 });
|
|
1184
1214
|
}
|
|
1185
1215
|
catch {
|
|
1186
1216
|
// Best effort only; attach should still proceed.
|
|
1187
1217
|
}
|
|
1218
|
+
if (existing && existing.pty)
|
|
1219
|
+
return existing;
|
|
1220
|
+
const ptyModule = await loadNodePtyModule();
|
|
1221
|
+
if (!ptyModule) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1188
1224
|
const env = {};
|
|
1189
1225
|
for (const [k, v] of Object.entries(process.env)) {
|
|
1190
1226
|
if (v !== undefined)
|
|
@@ -1345,7 +1381,6 @@ async function startClaudeHeadlessWsRun(ws, record, prompt, resumeSessionId) {
|
|
|
1345
1381
|
return () => { };
|
|
1346
1382
|
}
|
|
1347
1383
|
const args = buildClaudeHeadlessApptainerArgs(config, record.workdir, trimmedPrompt, resumeSessionId);
|
|
1348
|
-
send({ type: 'status', stage: 'run', message: 'Running Claude in headless mode...' });
|
|
1349
1384
|
const child = (0, child_process_1.spawn)('apptainer', args, {
|
|
1350
1385
|
cwd: record.workdir,
|
|
1351
1386
|
env: process.env,
|
|
@@ -1356,6 +1391,7 @@ async function startClaudeHeadlessWsRun(ws, record, prompt, resumeSessionId) {
|
|
|
1356
1391
|
let latestClaudeSessionId = resumeSessionId.trim();
|
|
1357
1392
|
let emittedAssistantText = '';
|
|
1358
1393
|
let doneSent = false;
|
|
1394
|
+
let syntheticToolUseSeq = 0;
|
|
1359
1395
|
const sendDone = (exitCode) => {
|
|
1360
1396
|
if (doneSent)
|
|
1361
1397
|
return;
|
|
@@ -1406,6 +1442,53 @@ async function startClaudeHeadlessWsRun(ws, record, prompt, resumeSessionId) {
|
|
|
1406
1442
|
send({ type: 'delta', text: delta });
|
|
1407
1443
|
}
|
|
1408
1444
|
}
|
|
1445
|
+
// Forward tool_use events from assistant messages
|
|
1446
|
+
const eventType = readRecordString(event, 'type').trim().toLowerCase();
|
|
1447
|
+
if (eventType === 'assistant') {
|
|
1448
|
+
const msgContent = event.message?.content;
|
|
1449
|
+
if (Array.isArray(msgContent)) {
|
|
1450
|
+
for (const block of msgContent) {
|
|
1451
|
+
if (block && typeof block === 'object' && !Array.isArray(block) && block.type === 'tool_use') {
|
|
1452
|
+
const toolBlock = block;
|
|
1453
|
+
const toolName = String(toolBlock.name || 'tool');
|
|
1454
|
+
const detail = extractToolDetailFromToolUseBlock(toolBlock);
|
|
1455
|
+
const toolUseId = normalizeToolUseId(toolBlock.id) || `tool-${Date.now().toString(36)}-${(++syntheticToolUseSeq).toString(36)}`;
|
|
1456
|
+
// Intercept display_widget calls and forward rich content payload
|
|
1457
|
+
if (toolName === 'display_widget') {
|
|
1458
|
+
const input = toolBlock.input;
|
|
1459
|
+
if (input && typeof input.widget === 'string') {
|
|
1460
|
+
send({
|
|
1461
|
+
type: 'rich_content',
|
|
1462
|
+
widget: String(input.widget),
|
|
1463
|
+
title: input.title ? String(input.title) : undefined,
|
|
1464
|
+
data: (input.data && typeof input.data === 'object') ? input.data : {},
|
|
1465
|
+
id: toolUseId,
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
// Always also send the normal tool_use card
|
|
1470
|
+
send({ type: 'tool_use', tool_use_id: toolUseId, name: toolName, detail });
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
// Forward tool_result events (indicates tool execution completed)
|
|
1476
|
+
if (eventType === 'user') {
|
|
1477
|
+
const msgContent = event.message?.content;
|
|
1478
|
+
if (Array.isArray(msgContent)) {
|
|
1479
|
+
for (const block of msgContent) {
|
|
1480
|
+
if (block && typeof block === 'object' && !Array.isArray(block) && block.type === 'tool_result') {
|
|
1481
|
+
const resultBlock = block;
|
|
1482
|
+
const toolUseId = normalizeToolUseId(resultBlock.tool_use_id);
|
|
1483
|
+
send({
|
|
1484
|
+
type: 'tool_result',
|
|
1485
|
+
tool_use_id: toolUseId || undefined,
|
|
1486
|
+
is_error: !!resultBlock.is_error,
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1409
1492
|
if (isClaudeAuthenticationFailure(event, snapshot, stderrBuffer)) {
|
|
1410
1493
|
send({
|
|
1411
1494
|
type: 'auth_required',
|
|
@@ -2248,6 +2331,39 @@ function findProjectJsonlFiles(agent) {
|
|
|
2248
2331
|
/**
|
|
2249
2332
|
* Extract a human-readable detail string from a JSONL entry's tool_use blocks.
|
|
2250
2333
|
*/
|
|
2334
|
+
function extractToolDetailFromToolUseBlock(block) {
|
|
2335
|
+
const name = String(block.name || '');
|
|
2336
|
+
const inputRaw = block.input;
|
|
2337
|
+
const input = inputRaw && typeof inputRaw === 'object' && !Array.isArray(inputRaw)
|
|
2338
|
+
? inputRaw
|
|
2339
|
+
: {};
|
|
2340
|
+
if (name === 'Bash' || name === 'bash') {
|
|
2341
|
+
const cmd = String(input.command || '').slice(0, 60);
|
|
2342
|
+
return cmd ? `Ran \`${cmd}\`` : 'Running Bash';
|
|
2343
|
+
}
|
|
2344
|
+
if (name === 'Edit' || name === 'edit') {
|
|
2345
|
+
const file = String(input.file_path || '').split('/').pop() || '';
|
|
2346
|
+
return file ? `Edited ${file}` : 'Editing a file';
|
|
2347
|
+
}
|
|
2348
|
+
if (name === 'Read' || name === 'read') {
|
|
2349
|
+
const file = String(input.file_path || '').split('/').pop() || '';
|
|
2350
|
+
return file ? `Read ${file}` : 'Reading a file';
|
|
2351
|
+
}
|
|
2352
|
+
if (name === 'Write' || name === 'write') {
|
|
2353
|
+
const file = String(input.file_path || '').split('/').pop() || '';
|
|
2354
|
+
return file ? `Wrote ${file}` : 'Writing a file';
|
|
2355
|
+
}
|
|
2356
|
+
if (name === 'Grep' || name === 'grep') {
|
|
2357
|
+
return `Searching for "${String(input.pattern || '').slice(0, 40)}"`;
|
|
2358
|
+
}
|
|
2359
|
+
if (name === 'Glob' || name === 'glob') {
|
|
2360
|
+
return `Finding files: ${String(input.pattern || '').slice(0, 40)}`;
|
|
2361
|
+
}
|
|
2362
|
+
if (name === 'Task' || name === 'task') {
|
|
2363
|
+
return 'Spawned subagent';
|
|
2364
|
+
}
|
|
2365
|
+
return `Using ${name}`;
|
|
2366
|
+
}
|
|
2251
2367
|
function extractToolDetail(entry) {
|
|
2252
2368
|
if (!entry.message?.content)
|
|
2253
2369
|
return '';
|
|
@@ -2255,35 +2371,8 @@ function extractToolDetail(entry) {
|
|
|
2255
2371
|
if (!Array.isArray(content))
|
|
2256
2372
|
return '';
|
|
2257
2373
|
for (const block of content) {
|
|
2258
|
-
if (block.type === 'tool_use') {
|
|
2259
|
-
|
|
2260
|
-
const input = block.input || {};
|
|
2261
|
-
if (name === 'Bash' || name === 'bash') {
|
|
2262
|
-
const cmd = (input.command || '').slice(0, 60);
|
|
2263
|
-
return cmd ? `Ran \`${cmd}\`` : `Running Bash`;
|
|
2264
|
-
}
|
|
2265
|
-
if (name === 'Edit' || name === 'edit') {
|
|
2266
|
-
const file = (input.file_path || '').split('/').pop() || '';
|
|
2267
|
-
return file ? `Edited ${file}` : 'Editing a file';
|
|
2268
|
-
}
|
|
2269
|
-
if (name === 'Read' || name === 'read') {
|
|
2270
|
-
const file = (input.file_path || '').split('/').pop() || '';
|
|
2271
|
-
return file ? `Read ${file}` : 'Reading a file';
|
|
2272
|
-
}
|
|
2273
|
-
if (name === 'Write' || name === 'write') {
|
|
2274
|
-
const file = (input.file_path || '').split('/').pop() || '';
|
|
2275
|
-
return file ? `Wrote ${file}` : 'Writing a file';
|
|
2276
|
-
}
|
|
2277
|
-
if (name === 'Grep' || name === 'grep') {
|
|
2278
|
-
return `Searching for "${(input.pattern || '').slice(0, 40)}"`;
|
|
2279
|
-
}
|
|
2280
|
-
if (name === 'Glob' || name === 'glob') {
|
|
2281
|
-
return `Finding files: ${(input.pattern || '').slice(0, 40)}`;
|
|
2282
|
-
}
|
|
2283
|
-
if (name === 'Task' || name === 'task') {
|
|
2284
|
-
return `Spawned subagent`;
|
|
2285
|
-
}
|
|
2286
|
-
return `Using ${name}`;
|
|
2374
|
+
if (block && typeof block === 'object' && !Array.isArray(block) && block.type === 'tool_use') {
|
|
2375
|
+
return extractToolDetailFromToolUseBlock(block);
|
|
2287
2376
|
}
|
|
2288
2377
|
}
|
|
2289
2378
|
return '';
|
|
@@ -3588,6 +3677,8 @@ function mapContainerPathToHost(path, sandboxHome) {
|
|
|
3588
3677
|
return (0, config_js_1.getSlurmDbPath)();
|
|
3589
3678
|
if (path === '/labgate-config/results.json')
|
|
3590
3679
|
return (0, config_js_1.getResultsDbPath)();
|
|
3680
|
+
if (path === '/labgate-config/display.json')
|
|
3681
|
+
return (0, config_js_1.getDisplayDbPath)();
|
|
3591
3682
|
return path;
|
|
3592
3683
|
}
|
|
3593
3684
|
function readMcpConfigData() {
|
|
@@ -4424,6 +4515,88 @@ function startResultsWatcher() {
|
|
|
4424
4515
|
resultsWatcher = null;
|
|
4425
4516
|
}
|
|
4426
4517
|
}
|
|
4518
|
+
// ── Display (widgets) file watcher ──
|
|
4519
|
+
const DISPLAY_WATCH_DEBOUNCE_MS = 120;
|
|
4520
|
+
let lastDisplaySignature = getDisplayFileSignature();
|
|
4521
|
+
let displayWatcher = null;
|
|
4522
|
+
let displayWatchDebounce = null;
|
|
4523
|
+
function getDisplayFileSignature() {
|
|
4524
|
+
const displayPath = (0, config_js_1.getDisplayDbPath)();
|
|
4525
|
+
try {
|
|
4526
|
+
if (!(0, fs_1.existsSync)(displayPath))
|
|
4527
|
+
return 'missing';
|
|
4528
|
+
const st = (0, fs_1.statSync)(displayPath);
|
|
4529
|
+
return `${st.size}:${Math.floor(st.mtimeMs)}`;
|
|
4530
|
+
}
|
|
4531
|
+
catch {
|
|
4532
|
+
return 'error';
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
function maybeBroadcastWidgetsChanged() {
|
|
4536
|
+
const signature = getDisplayFileSignature();
|
|
4537
|
+
if (signature === lastDisplaySignature)
|
|
4538
|
+
return;
|
|
4539
|
+
if (sseClients.size === 0)
|
|
4540
|
+
return;
|
|
4541
|
+
lastDisplaySignature = signature;
|
|
4542
|
+
broadcastSSE('widgets_changed', {
|
|
4543
|
+
changed_at: new Date().toISOString(),
|
|
4544
|
+
signature,
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
function scheduleDisplayChangeCheck(delayMs = DISPLAY_WATCH_DEBOUNCE_MS) {
|
|
4548
|
+
if (displayWatchDebounce) {
|
|
4549
|
+
clearTimeout(displayWatchDebounce);
|
|
4550
|
+
}
|
|
4551
|
+
displayWatchDebounce = setTimeout(() => {
|
|
4552
|
+
displayWatchDebounce = null;
|
|
4553
|
+
maybeBroadcastWidgetsChanged();
|
|
4554
|
+
}, delayMs);
|
|
4555
|
+
displayWatchDebounce.unref?.();
|
|
4556
|
+
}
|
|
4557
|
+
function stopDisplayWatcher() {
|
|
4558
|
+
if (displayWatchDebounce) {
|
|
4559
|
+
clearTimeout(displayWatchDebounce);
|
|
4560
|
+
displayWatchDebounce = null;
|
|
4561
|
+
}
|
|
4562
|
+
if (displayWatcher) {
|
|
4563
|
+
try {
|
|
4564
|
+
displayWatcher.close();
|
|
4565
|
+
}
|
|
4566
|
+
catch {
|
|
4567
|
+
// Best effort.
|
|
4568
|
+
}
|
|
4569
|
+
displayWatcher = null;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
function startDisplayWatcher() {
|
|
4573
|
+
if (displayWatcher)
|
|
4574
|
+
return;
|
|
4575
|
+
lastDisplaySignature = getDisplayFileSignature();
|
|
4576
|
+
const displayPath = (0, config_js_1.getDisplayDbPath)();
|
|
4577
|
+
const watchDir = (0, path_1.dirname)(displayPath);
|
|
4578
|
+
const watchFile = (0, path_1.basename)(displayPath);
|
|
4579
|
+
try {
|
|
4580
|
+
(0, config_js_1.ensurePrivateDir)(watchDir);
|
|
4581
|
+
}
|
|
4582
|
+
catch {
|
|
4583
|
+
// Best effort.
|
|
4584
|
+
}
|
|
4585
|
+
try {
|
|
4586
|
+
displayWatcher = (0, fs_1.watch)(watchDir, (_eventType, filename) => {
|
|
4587
|
+
const changed = filename ? String(filename) : '';
|
|
4588
|
+
if (changed && changed !== watchFile)
|
|
4589
|
+
return;
|
|
4590
|
+
scheduleDisplayChangeCheck();
|
|
4591
|
+
});
|
|
4592
|
+
displayWatcher.on('error', () => {
|
|
4593
|
+
stopDisplayWatcher();
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
catch {
|
|
4597
|
+
displayWatcher = null;
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4427
4600
|
function handleSSE(_req, res) {
|
|
4428
4601
|
res.writeHead(200, {
|
|
4429
4602
|
'Content-Type': 'text/event-stream',
|
|
@@ -4452,6 +4625,7 @@ function startSSEBroadcast() {
|
|
|
4452
4625
|
if (sseInterval)
|
|
4453
4626
|
return;
|
|
4454
4627
|
startResultsWatcher();
|
|
4628
|
+
startDisplayWatcher();
|
|
4455
4629
|
sseInterval = setInterval(async () => {
|
|
4456
4630
|
if (sseClients.size === 0)
|
|
4457
4631
|
return;
|
|
@@ -4727,6 +4901,648 @@ function handleGetAdminLicense(_req, res) {
|
|
|
4727
4901
|
const status = (0, license_js_1.validateLicense)();
|
|
4728
4902
|
json(res, { ok: true, license: status });
|
|
4729
4903
|
}
|
|
4904
|
+
// ── Display file endpoint ────────────────────────────────
|
|
4905
|
+
// Serves files from the container filesystem to the browser for display widgets.
|
|
4906
|
+
// Maps container paths to host paths using the same mount logic as the session.
|
|
4907
|
+
function resolveDisplayFilePath(containerPath) {
|
|
4908
|
+
const config = (0, config_js_1.loadConfig)();
|
|
4909
|
+
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
4910
|
+
// /home/sandbox/... → sandbox home
|
|
4911
|
+
if (containerPath.startsWith('/home/sandbox/')) {
|
|
4912
|
+
return (0, path_1.join)(sandboxHome, containerPath.slice('/home/sandbox/'.length));
|
|
4913
|
+
}
|
|
4914
|
+
// /datasets/<name>/... → dataset host path
|
|
4915
|
+
const datasetMatch = containerPath.match(/^\/datasets\/([^/]+)\/(.+)$/);
|
|
4916
|
+
if (datasetMatch) {
|
|
4917
|
+
const [, dsName, rest] = datasetMatch;
|
|
4918
|
+
const ds = (config.datasets || []).find((d) => d.name === dsName);
|
|
4919
|
+
if (ds) {
|
|
4920
|
+
const resolved = ds.path.replace(/^~/, (0, os_1.homedir)());
|
|
4921
|
+
return (0, path_1.join)(resolved, rest);
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
// /work/... → workdir from active sessions
|
|
4925
|
+
if (containerPath.startsWith('/work/')) {
|
|
4926
|
+
const rest = containerPath.slice('/work/'.length);
|
|
4927
|
+
// Check active session workdirs
|
|
4928
|
+
try {
|
|
4929
|
+
const sessionDir = (0, config_js_1.getSessionsDir)();
|
|
4930
|
+
if ((0, fs_1.existsSync)(sessionDir)) {
|
|
4931
|
+
const files = (0, fs_1.readdirSync)(sessionDir).filter((f) => f.endsWith('.json'));
|
|
4932
|
+
for (const f of files) {
|
|
4933
|
+
try {
|
|
4934
|
+
const data = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(sessionDir, f), 'utf-8'));
|
|
4935
|
+
if (data.workdir) {
|
|
4936
|
+
const candidate = (0, path_1.join)(data.workdir, rest);
|
|
4937
|
+
if ((0, fs_1.existsSync)(candidate))
|
|
4938
|
+
return candidate;
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
catch { /* skip */ }
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
catch { /* skip */ }
|
|
4946
|
+
}
|
|
4947
|
+
// /mnt/<basename>/... → extra_paths
|
|
4948
|
+
const mntMatch = containerPath.match(/^\/mnt\/([^/]+)\/(.+)$/);
|
|
4949
|
+
if (mntMatch) {
|
|
4950
|
+
const [, mountBase, rest] = mntMatch;
|
|
4951
|
+
const ep = config.filesystem.extra_paths.find((p) => {
|
|
4952
|
+
const resolved = p.path.replace(/^~/, (0, os_1.homedir)());
|
|
4953
|
+
return (0, path_1.basename)(resolved) === mountBase;
|
|
4954
|
+
});
|
|
4955
|
+
if (ep) {
|
|
4956
|
+
const resolved = ep.path.replace(/^~/, (0, os_1.homedir)());
|
|
4957
|
+
return (0, path_1.join)(resolved, rest);
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
return null;
|
|
4961
|
+
}
|
|
4962
|
+
function getContentTypeForFile(filePath) {
|
|
4963
|
+
const ext = filePath.split('.').pop()?.toLowerCase() || '';
|
|
4964
|
+
const mimeMap = {
|
|
4965
|
+
png: 'image/png',
|
|
4966
|
+
jpg: 'image/jpeg',
|
|
4967
|
+
jpeg: 'image/jpeg',
|
|
4968
|
+
gif: 'image/gif',
|
|
4969
|
+
svg: 'image/svg+xml',
|
|
4970
|
+
webp: 'image/webp',
|
|
4971
|
+
pdf: 'application/pdf',
|
|
4972
|
+
csv: 'text/csv',
|
|
4973
|
+
tsv: 'text/tab-separated-values',
|
|
4974
|
+
txt: 'text/plain',
|
|
4975
|
+
json: 'application/json',
|
|
4976
|
+
pdb: 'chemical/x-pdb',
|
|
4977
|
+
cif: 'chemical/x-cif',
|
|
4978
|
+
mmcif: 'chemical/x-mmcif',
|
|
4979
|
+
fasta: 'text/plain',
|
|
4980
|
+
fa: 'text/plain',
|
|
4981
|
+
fastq: 'text/plain',
|
|
4982
|
+
fq: 'text/plain',
|
|
4983
|
+
html: 'text/html',
|
|
4984
|
+
xml: 'application/xml',
|
|
4985
|
+
};
|
|
4986
|
+
return mimeMap[ext] || 'application/octet-stream';
|
|
4987
|
+
}
|
|
4988
|
+
function handleDisplayFile(reqUrl, res) {
|
|
4989
|
+
const containerPath = reqUrl.searchParams.get('path');
|
|
4990
|
+
if (!containerPath) {
|
|
4991
|
+
json(res, { ok: false, error: 'Missing path parameter' }, 400);
|
|
4992
|
+
return;
|
|
4993
|
+
}
|
|
4994
|
+
// Prevent directory traversal
|
|
4995
|
+
if (containerPath.includes('..') || containerPath.includes('\0')) {
|
|
4996
|
+
json(res, { ok: false, error: 'Invalid path' }, 400);
|
|
4997
|
+
return;
|
|
4998
|
+
}
|
|
4999
|
+
const hostPath = resolveDisplayFilePath(containerPath);
|
|
5000
|
+
if (!hostPath) {
|
|
5001
|
+
json(res, { ok: false, error: 'Path not within any allowed mount' }, 404);
|
|
5002
|
+
return;
|
|
5003
|
+
}
|
|
5004
|
+
// Verify the resolved path doesn't escape via symlinks
|
|
5005
|
+
let realPath;
|
|
5006
|
+
try {
|
|
5007
|
+
realPath = (0, fs_1.realpathSync)(hostPath);
|
|
5008
|
+
}
|
|
5009
|
+
catch {
|
|
5010
|
+
json(res, { ok: false, error: 'File not found' }, 404);
|
|
5011
|
+
return;
|
|
5012
|
+
}
|
|
5013
|
+
try {
|
|
5014
|
+
const stat = (0, fs_1.statSync)(realPath);
|
|
5015
|
+
if (!stat.isFile()) {
|
|
5016
|
+
json(res, { ok: false, error: 'Not a file' }, 400);
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
// Limit to 50MB
|
|
5020
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
5021
|
+
json(res, { ok: false, error: 'File too large (max 50MB)' }, 413);
|
|
5022
|
+
return;
|
|
5023
|
+
}
|
|
5024
|
+
const contentType = getContentTypeForFile(realPath);
|
|
5025
|
+
const data = (0, fs_1.readFileSync)(realPath);
|
|
5026
|
+
res.writeHead(200, {
|
|
5027
|
+
'Content-Type': contentType,
|
|
5028
|
+
'Content-Length': data.length,
|
|
5029
|
+
'Cache-Control': 'no-cache',
|
|
5030
|
+
});
|
|
5031
|
+
res.end(data);
|
|
5032
|
+
}
|
|
5033
|
+
catch {
|
|
5034
|
+
json(res, { ok: false, error: 'Failed to read file' }, 500);
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
function handleGetWidgets(res) {
|
|
5038
|
+
try {
|
|
5039
|
+
const store = getDisplayStore();
|
|
5040
|
+
const widgets = store.listEvents();
|
|
5041
|
+
json(res, { ok: true, widgets });
|
|
5042
|
+
}
|
|
5043
|
+
catch (err) {
|
|
5044
|
+
json(res, { ok: false, error: err.message || 'Failed to list widgets' }, 500);
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
function handleClearWidgets(res) {
|
|
5048
|
+
try {
|
|
5049
|
+
const store = getDisplayStore();
|
|
5050
|
+
store.clearEvents();
|
|
5051
|
+
json(res, { ok: true });
|
|
5052
|
+
}
|
|
5053
|
+
catch (err) {
|
|
5054
|
+
json(res, { ok: false, error: err.message || 'Failed to clear widgets' }, 500);
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
function parseExplorerExperimentId(reqUrl) {
|
|
5058
|
+
return String(reqUrl.searchParams.get('experiment_id') || '').trim();
|
|
5059
|
+
}
|
|
5060
|
+
function parseExplorerRunId(reqUrl) {
|
|
5061
|
+
return String(reqUrl.searchParams.get('run_id') || '').trim();
|
|
5062
|
+
}
|
|
5063
|
+
function parseExplorerListInt(raw, fallback, min, max) {
|
|
5064
|
+
const parsed = Number(raw);
|
|
5065
|
+
if (!Number.isFinite(parsed))
|
|
5066
|
+
return fallback;
|
|
5067
|
+
return Math.max(min, Math.min(max, Math.floor(parsed)));
|
|
5068
|
+
}
|
|
5069
|
+
async function ensureExplorerQuickstartSourceRepo() {
|
|
5070
|
+
if (!(0, fs_1.existsSync)(EXPLORER_TSP_TEMPLATE_DIR)) {
|
|
5071
|
+
throw new Error(`Bundled template not found: ${EXPLORER_TSP_TEMPLATE_DIR}`);
|
|
5072
|
+
}
|
|
5073
|
+
const gitDir = (0, path_1.join)(EXPLORER_TSP_TEMPLATE_SOURCE_REPO, '.git');
|
|
5074
|
+
if ((0, fs_1.existsSync)(gitDir))
|
|
5075
|
+
return EXPLORER_TSP_TEMPLATE_SOURCE_REPO;
|
|
5076
|
+
(0, config_js_1.ensurePrivateDir)((0, path_1.dirname)(EXPLORER_TSP_TEMPLATE_SOURCE_REPO));
|
|
5077
|
+
(0, fs_1.mkdirSync)(EXPLORER_TSP_TEMPLATE_SOURCE_REPO, { recursive: true, mode: config_js_1.PRIVATE_DIR_MODE });
|
|
5078
|
+
(0, fs_1.cpSync)(EXPLORER_TSP_TEMPLATE_DIR, EXPLORER_TSP_TEMPLATE_SOURCE_REPO, { recursive: true, force: true });
|
|
5079
|
+
await execFileAsync('git', ['init', '.'], { cwd: EXPLORER_TSP_TEMPLATE_SOURCE_REPO });
|
|
5080
|
+
await execFileAsync('git', ['add', '.'], { cwd: EXPLORER_TSP_TEMPLATE_SOURCE_REPO });
|
|
5081
|
+
try {
|
|
5082
|
+
await execFileAsync('git', ['-c', 'user.name=labgate-ui', '-c', 'user.email=labgate@local', 'commit', '-m', 'template baseline'], { cwd: EXPLORER_TSP_TEMPLATE_SOURCE_REPO });
|
|
5083
|
+
}
|
|
5084
|
+
catch (err) {
|
|
5085
|
+
const detail = commandErrorDetail(err);
|
|
5086
|
+
if (!/nothing to commit/i.test(detail)) {
|
|
5087
|
+
throw err;
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
return EXPLORER_TSP_TEMPLATE_SOURCE_REPO;
|
|
5091
|
+
}
|
|
5092
|
+
async function parseExplorerQuickstartInput(body) {
|
|
5093
|
+
const nameInput = String(body.name || '').trim();
|
|
5094
|
+
const experimentName = nameInput || `TSP Demo ${new Date().toISOString().slice(0, 19).replace('T', ' ')}`;
|
|
5095
|
+
const modeRaw = String(body.agent_mode || 'stub').trim().toLowerCase();
|
|
5096
|
+
const agentMode = modeRaw === 'claude' ? 'claude_headless' : (modeRaw || 'stub');
|
|
5097
|
+
if (agentMode !== 'stub' && agentMode !== 'claude_headless') {
|
|
5098
|
+
throw new Error('agent_mode must be stub or claude_headless');
|
|
5099
|
+
}
|
|
5100
|
+
const claudeResumeSessionId = String(body.claude_resume_session_id || '').trim();
|
|
5101
|
+
const claudeTimeoutRaw = body.claude_timeout_sec;
|
|
5102
|
+
const claudeTimeout = Number(claudeTimeoutRaw);
|
|
5103
|
+
if (claudeTimeoutRaw !== undefined &&
|
|
5104
|
+
(!Number.isFinite(claudeTimeout) || claudeTimeout < 60 || claudeTimeout > 14_400)) {
|
|
5105
|
+
throw new Error('claude_timeout_sec must be between 60 and 14400');
|
|
5106
|
+
}
|
|
5107
|
+
const sourceRepoInput = String(body.source_repo_path || '').trim();
|
|
5108
|
+
const sourceRepoPath = sourceRepoInput
|
|
5109
|
+
? (0, path_1.resolve)(sourceRepoInput)
|
|
5110
|
+
: await ensureExplorerQuickstartSourceRepo();
|
|
5111
|
+
if (!(0, fs_1.existsSync)(sourceRepoPath)) {
|
|
5112
|
+
throw new Error(`source_repo_path not found: ${sourceRepoPath}`);
|
|
5113
|
+
}
|
|
5114
|
+
const evalCommand = String(body.eval_command || 'python3 eval.py').trim() || 'python3 eval.py';
|
|
5115
|
+
const timeoutRaw = body.eval_timeout_sec;
|
|
5116
|
+
const evalTimeoutSec = timeoutRaw === undefined ? 30 : Number(timeoutRaw);
|
|
5117
|
+
if (!Number.isFinite(evalTimeoutSec) || evalTimeoutSec < 5 || evalTimeoutSec > 86_400) {
|
|
5118
|
+
throw new Error('eval_timeout_sec must be between 5 and 86400');
|
|
5119
|
+
}
|
|
5120
|
+
const policy = {
|
|
5121
|
+
epsilon: 0.15,
|
|
5122
|
+
top_n: 5,
|
|
5123
|
+
agent_mode: agentMode,
|
|
5124
|
+
};
|
|
5125
|
+
if (agentMode === 'stub') {
|
|
5126
|
+
policy.stub_patch_file = 'stub-patches/enable_two_opt.patch';
|
|
5127
|
+
}
|
|
5128
|
+
else {
|
|
5129
|
+
if (claudeResumeSessionId)
|
|
5130
|
+
policy.claude_resume_session_id = claudeResumeSessionId;
|
|
5131
|
+
if (claudeTimeoutRaw !== undefined)
|
|
5132
|
+
policy.claude_timeout_sec = Math.floor(claudeTimeout);
|
|
5133
|
+
}
|
|
5134
|
+
return {
|
|
5135
|
+
experimentName,
|
|
5136
|
+
sourceRepoPath,
|
|
5137
|
+
evalCommand,
|
|
5138
|
+
evalTimeoutSec: Math.floor(evalTimeoutSec),
|
|
5139
|
+
policy,
|
|
5140
|
+
};
|
|
5141
|
+
}
|
|
5142
|
+
function handleGetExplorerExperiments(reqUrl, res) {
|
|
5143
|
+
const limit = parseExplorerListInt(reqUrl.searchParams.get('limit'), 20, 1, 500);
|
|
5144
|
+
const offset = parseExplorerListInt(reqUrl.searchParams.get('offset'), 0, 0, 100_000);
|
|
5145
|
+
const store = new explorer_store_js_1.ExplorerStore();
|
|
5146
|
+
try {
|
|
5147
|
+
const experiments = store.listExperiments(limit, offset);
|
|
5148
|
+
json(res, {
|
|
5149
|
+
ok: true,
|
|
5150
|
+
experiments,
|
|
5151
|
+
returned: experiments.length,
|
|
5152
|
+
limit,
|
|
5153
|
+
offset,
|
|
5154
|
+
});
|
|
5155
|
+
}
|
|
5156
|
+
catch (err) {
|
|
5157
|
+
json(res, { ok: false, error: err?.message || String(err) }, 500);
|
|
5158
|
+
}
|
|
5159
|
+
finally {
|
|
5160
|
+
store.close();
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
function handleGetExplorerOverview(reqUrl, res) {
|
|
5164
|
+
const experimentId = parseExplorerExperimentId(reqUrl);
|
|
5165
|
+
if (!experimentId) {
|
|
5166
|
+
json(res, { ok: false, error: 'Missing experiment_id' }, 400);
|
|
5167
|
+
return;
|
|
5168
|
+
}
|
|
5169
|
+
try {
|
|
5170
|
+
const overview = (0, explorer_js_1.getExperimentOverview)(experimentId);
|
|
5171
|
+
if (!overview) {
|
|
5172
|
+
json(res, { ok: false, error: 'Experiment not found' }, 404);
|
|
5173
|
+
return;
|
|
5174
|
+
}
|
|
5175
|
+
json(res, { ok: true, overview });
|
|
5176
|
+
}
|
|
5177
|
+
catch (err) {
|
|
5178
|
+
json(res, { ok: false, error: err?.message || String(err) }, 500);
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
function handleGetExplorerTree(reqUrl, res) {
|
|
5182
|
+
const experimentId = parseExplorerExperimentId(reqUrl);
|
|
5183
|
+
if (!experimentId) {
|
|
5184
|
+
json(res, { ok: false, error: 'Missing experiment_id' }, 400);
|
|
5185
|
+
return;
|
|
5186
|
+
}
|
|
5187
|
+
const mode = String(reqUrl.searchParams.get('mode') || 'best_path').trim() === 'full' ? 'full' : 'best_path';
|
|
5188
|
+
try {
|
|
5189
|
+
const tree = (0, explorer_js_1.getExperimentTree)(experimentId, mode);
|
|
5190
|
+
json(res, { ok: true, tree });
|
|
5191
|
+
}
|
|
5192
|
+
catch (err) {
|
|
5193
|
+
json(res, { ok: false, error: err?.message || String(err) }, 500);
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
function handleGetExplorerRuns(reqUrl, res) {
|
|
5197
|
+
const experimentId = parseExplorerExperimentId(reqUrl);
|
|
5198
|
+
if (!experimentId) {
|
|
5199
|
+
json(res, { ok: false, error: 'Missing experiment_id' }, 400);
|
|
5200
|
+
return;
|
|
5201
|
+
}
|
|
5202
|
+
const limit = parseExplorerListInt(reqUrl.searchParams.get('limit'), 20, 1, 500);
|
|
5203
|
+
const offset = parseExplorerListInt(reqUrl.searchParams.get('offset'), 0, 0, 100_000);
|
|
5204
|
+
const store = new explorer_store_js_1.ExplorerStore();
|
|
5205
|
+
try {
|
|
5206
|
+
const runs = store.listRuns(experimentId, { limit, offset });
|
|
5207
|
+
const total = store.getRunCount(experimentId);
|
|
5208
|
+
json(res, { ok: true, runs, total, returned: runs.length, limit, offset });
|
|
5209
|
+
}
|
|
5210
|
+
catch (err) {
|
|
5211
|
+
json(res, { ok: false, error: err?.message || String(err) }, 500);
|
|
5212
|
+
}
|
|
5213
|
+
finally {
|
|
5214
|
+
store.close();
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
function handleGetExplorerRun(reqUrl, res) {
|
|
5218
|
+
const runId = parseExplorerRunId(reqUrl);
|
|
5219
|
+
if (!runId) {
|
|
5220
|
+
json(res, { ok: false, error: 'Missing run_id' }, 400);
|
|
5221
|
+
return;
|
|
5222
|
+
}
|
|
5223
|
+
try {
|
|
5224
|
+
const details = (0, explorer_js_1.getRunDetails)(runId);
|
|
5225
|
+
if (!details) {
|
|
5226
|
+
json(res, { ok: false, error: 'Run not found' }, 404);
|
|
5227
|
+
return;
|
|
5228
|
+
}
|
|
5229
|
+
json(res, { ok: true, details });
|
|
5230
|
+
}
|
|
5231
|
+
catch (err) {
|
|
5232
|
+
json(res, { ok: false, error: err?.message || String(err) }, 500);
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
function handleGetExplorerCompare(reqUrl, res) {
|
|
5236
|
+
const experimentId = parseExplorerExperimentId(reqUrl);
|
|
5237
|
+
const runId = parseExplorerRunId(reqUrl);
|
|
5238
|
+
if (!experimentId || !runId) {
|
|
5239
|
+
json(res, { ok: false, error: 'Missing experiment_id or run_id' }, 400);
|
|
5240
|
+
return;
|
|
5241
|
+
}
|
|
5242
|
+
const compareTo = String(reqUrl.searchParams.get('to') || 'best').trim() || 'best';
|
|
5243
|
+
const includePatch = reqUrl.searchParams.get('include_patch') === '1';
|
|
5244
|
+
try {
|
|
5245
|
+
const comparison = (0, explorer_js_1.compareRun)({
|
|
5246
|
+
experiment_id: experimentId,
|
|
5247
|
+
run_id: runId,
|
|
5248
|
+
compare_to: compareTo,
|
|
5249
|
+
include_patch: includePatch,
|
|
5250
|
+
});
|
|
5251
|
+
json(res, { ok: true, comparison });
|
|
5252
|
+
}
|
|
5253
|
+
catch (err) {
|
|
5254
|
+
json(res, { ok: false, error: err?.message || String(err) }, 500);
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
function handleGetExplorerArtifact(reqUrl, res) {
|
|
5258
|
+
const runId = parseExplorerRunId(reqUrl);
|
|
5259
|
+
if (!runId) {
|
|
5260
|
+
json(res, { ok: false, error: 'Missing run_id' }, 400);
|
|
5261
|
+
return;
|
|
5262
|
+
}
|
|
5263
|
+
const kind = String(reqUrl.searchParams.get('kind') || 'summary').trim().toLowerCase();
|
|
5264
|
+
if (!['summary', 'diff', 'stdout', 'stderr', 'eval', 'agent', 'claude_stdout', 'claude_stderr'].includes(kind)) {
|
|
5265
|
+
json(res, { ok: false, error: 'Invalid kind (expected summary|diff|stdout|stderr|eval|agent|claude_stdout|claude_stderr)' }, 400);
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5268
|
+
const details = (0, explorer_js_1.getRunDetails)(runId);
|
|
5269
|
+
if (!details) {
|
|
5270
|
+
json(res, { ok: false, error: 'Run not found' }, 404);
|
|
5271
|
+
return;
|
|
5272
|
+
}
|
|
5273
|
+
const artifacts = details.artifacts;
|
|
5274
|
+
const pathByKind = {
|
|
5275
|
+
summary: artifacts.summary_path,
|
|
5276
|
+
diff: artifacts.diff_path,
|
|
5277
|
+
stdout: artifacts.stdout_path,
|
|
5278
|
+
stderr: artifacts.stderr_path,
|
|
5279
|
+
eval: artifacts.eval_json_path,
|
|
5280
|
+
agent: artifacts.agent_log_path,
|
|
5281
|
+
claude_stdout: artifacts.claude_stdout_path,
|
|
5282
|
+
claude_stderr: artifacts.claude_stderr_path,
|
|
5283
|
+
};
|
|
5284
|
+
const availableByKind = {
|
|
5285
|
+
summary: artifacts.available.summary,
|
|
5286
|
+
diff: artifacts.available.diff,
|
|
5287
|
+
stdout: artifacts.available.stdout,
|
|
5288
|
+
stderr: artifacts.available.stderr,
|
|
5289
|
+
eval: artifacts.available.eval_json,
|
|
5290
|
+
agent: artifacts.available.agent_log,
|
|
5291
|
+
claude_stdout: artifacts.available.claude_stdout,
|
|
5292
|
+
claude_stderr: artifacts.available.claude_stderr,
|
|
5293
|
+
};
|
|
5294
|
+
const filePath = pathByKind[kind];
|
|
5295
|
+
if (!filePath || !availableByKind[kind] || !(0, fs_1.existsSync)(filePath)) {
|
|
5296
|
+
json(res, {
|
|
5297
|
+
ok: false,
|
|
5298
|
+
error: 'Artifact missing (possibly pruned)',
|
|
5299
|
+
artifacts_pruned: artifacts.artifacts_pruned,
|
|
5300
|
+
worktree_pruned: artifacts.worktree_pruned,
|
|
5301
|
+
}, 404);
|
|
5302
|
+
return;
|
|
5303
|
+
}
|
|
5304
|
+
try {
|
|
5305
|
+
const st = (0, fs_1.statSync)(filePath);
|
|
5306
|
+
if (!st.isFile()) {
|
|
5307
|
+
json(res, { ok: false, error: 'Artifact is not a file' }, 400);
|
|
5308
|
+
return;
|
|
5309
|
+
}
|
|
5310
|
+
const readBytes = Math.min(st.size, EXPLORER_ARTIFACT_READ_MAX_BYTES);
|
|
5311
|
+
const offset = Math.max(0, st.size - readBytes);
|
|
5312
|
+
const fd = (0, fs_1.openSync)(filePath, 'r');
|
|
5313
|
+
const buf = Buffer.alloc(readBytes);
|
|
5314
|
+
try {
|
|
5315
|
+
(0, fs_1.readSync)(fd, buf, 0, readBytes, offset);
|
|
5316
|
+
}
|
|
5317
|
+
finally {
|
|
5318
|
+
(0, fs_1.closeSync)(fd);
|
|
5319
|
+
}
|
|
5320
|
+
let text = buf.toString('utf-8');
|
|
5321
|
+
if (offset > 0) {
|
|
5322
|
+
const firstNewline = text.indexOf('\n');
|
|
5323
|
+
if (firstNewline >= 0)
|
|
5324
|
+
text = text.slice(firstNewline + 1);
|
|
5325
|
+
}
|
|
5326
|
+
if (kind === 'eval') {
|
|
5327
|
+
let parsedEval = null;
|
|
5328
|
+
try {
|
|
5329
|
+
parsedEval = JSON.parse(text);
|
|
5330
|
+
}
|
|
5331
|
+
catch {
|
|
5332
|
+
parsedEval = null;
|
|
5333
|
+
}
|
|
5334
|
+
json(res, {
|
|
5335
|
+
ok: true,
|
|
5336
|
+
kind,
|
|
5337
|
+
path: filePath,
|
|
5338
|
+
size: st.size,
|
|
5339
|
+
truncated: st.size > readBytes,
|
|
5340
|
+
eval: parsedEval,
|
|
5341
|
+
raw: text,
|
|
5342
|
+
});
|
|
5343
|
+
return;
|
|
5344
|
+
}
|
|
5345
|
+
json(res, {
|
|
5346
|
+
ok: true,
|
|
5347
|
+
kind,
|
|
5348
|
+
path: filePath,
|
|
5349
|
+
size: st.size,
|
|
5350
|
+
truncated: st.size > readBytes,
|
|
5351
|
+
text,
|
|
5352
|
+
});
|
|
5353
|
+
}
|
|
5354
|
+
catch (err) {
|
|
5355
|
+
json(res, { ok: false, error: err?.message || String(err) }, 500);
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
async function handlePostExplorerQuickstart(req, res) {
|
|
5359
|
+
try {
|
|
5360
|
+
let body = {};
|
|
5361
|
+
try {
|
|
5362
|
+
body = JSON.parse(await readBody(req) || '{}');
|
|
5363
|
+
}
|
|
5364
|
+
catch {
|
|
5365
|
+
body = {};
|
|
5366
|
+
}
|
|
5367
|
+
const input = await parseExplorerQuickstartInput(body);
|
|
5368
|
+
const experiment = (0, explorer_js_1.createExplorerExperiment)({
|
|
5369
|
+
name: input.experimentName,
|
|
5370
|
+
source_repo_path: input.sourceRepoPath,
|
|
5371
|
+
eval_command: input.evalCommand,
|
|
5372
|
+
eval_timeout_sec: input.evalTimeoutSec,
|
|
5373
|
+
policy: input.policy,
|
|
5374
|
+
retention: {
|
|
5375
|
+
keep_worktrees: false,
|
|
5376
|
+
artifacts: 'minimal',
|
|
5377
|
+
keep_last_n: 50,
|
|
5378
|
+
keep_best: true,
|
|
5379
|
+
keep_failed_last_n: 20,
|
|
5380
|
+
max_delete_runs: 200,
|
|
5381
|
+
},
|
|
5382
|
+
});
|
|
5383
|
+
const baselineArtifactDir = (0, config_js_1.getExplorerArtifactDir)(experiment.id, 'baseline');
|
|
5384
|
+
const baseline = (0, explorer_eval_js_1.runEvaluation)({
|
|
5385
|
+
worktree_path: experiment.repo_path,
|
|
5386
|
+
eval_command: experiment.eval_command,
|
|
5387
|
+
timeout_sec: experiment.eval_timeout_sec,
|
|
5388
|
+
artifact_dir: baselineArtifactDir,
|
|
5389
|
+
});
|
|
5390
|
+
const store = new explorer_store_js_1.ExplorerStore();
|
|
5391
|
+
try {
|
|
5392
|
+
store.createEvent(experiment.id, 'note', {
|
|
5393
|
+
message: 'baseline evaluation',
|
|
5394
|
+
status: baseline.status,
|
|
5395
|
+
score: baseline.score ?? null,
|
|
5396
|
+
artifact_dir: baselineArtifactDir,
|
|
5397
|
+
error: baseline.error || null,
|
|
5398
|
+
});
|
|
5399
|
+
}
|
|
5400
|
+
finally {
|
|
5401
|
+
store.close();
|
|
5402
|
+
}
|
|
5403
|
+
const overview = (0, explorer_js_1.getExperimentOverview)(experiment.id);
|
|
5404
|
+
json(res, {
|
|
5405
|
+
ok: true,
|
|
5406
|
+
experiment,
|
|
5407
|
+
baseline: {
|
|
5408
|
+
status: baseline.status,
|
|
5409
|
+
score: baseline.score ?? null,
|
|
5410
|
+
error: baseline.error || null,
|
|
5411
|
+
artifact_dir: baselineArtifactDir,
|
|
5412
|
+
},
|
|
5413
|
+
overview,
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
5416
|
+
catch (err) {
|
|
5417
|
+
const message = err?.message || String(err);
|
|
5418
|
+
const status = /must be|not found/i.test(message) ? 400 : 500;
|
|
5419
|
+
json(res, { ok: false, error: message }, status);
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
async function handlePostExplorerRegister(req, res) {
|
|
5423
|
+
try {
|
|
5424
|
+
let body = {};
|
|
5425
|
+
try {
|
|
5426
|
+
body = JSON.parse(await readBody(req) || '{}');
|
|
5427
|
+
}
|
|
5428
|
+
catch {
|
|
5429
|
+
body = {};
|
|
5430
|
+
}
|
|
5431
|
+
const input = await parseExplorerQuickstartInput(body);
|
|
5432
|
+
const experiment = (0, explorer_js_1.createExplorerExperiment)({
|
|
5433
|
+
name: input.experimentName,
|
|
5434
|
+
source_repo_path: input.sourceRepoPath,
|
|
5435
|
+
eval_command: input.evalCommand,
|
|
5436
|
+
eval_timeout_sec: input.evalTimeoutSec,
|
|
5437
|
+
policy: input.policy,
|
|
5438
|
+
retention: {
|
|
5439
|
+
keep_worktrees: false,
|
|
5440
|
+
artifacts: 'minimal',
|
|
5441
|
+
keep_last_n: 50,
|
|
5442
|
+
keep_best: true,
|
|
5443
|
+
keep_failed_last_n: 20,
|
|
5444
|
+
max_delete_runs: 200,
|
|
5445
|
+
},
|
|
5446
|
+
});
|
|
5447
|
+
const overview = (0, explorer_js_1.getExperimentOverview)(experiment.id);
|
|
5448
|
+
json(res, {
|
|
5449
|
+
ok: true,
|
|
5450
|
+
experiment,
|
|
5451
|
+
overview,
|
|
5452
|
+
flow: {
|
|
5453
|
+
tool: 'experiment_register',
|
|
5454
|
+
initialized: false,
|
|
5455
|
+
},
|
|
5456
|
+
});
|
|
5457
|
+
}
|
|
5458
|
+
catch (err) {
|
|
5459
|
+
const message = err?.message || String(err);
|
|
5460
|
+
const status = /must be|not found/i.test(message) ? 400 : 500;
|
|
5461
|
+
json(res, { ok: false, error: message }, status);
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
async function handlePostExplorerInit(req, res) {
|
|
5465
|
+
try {
|
|
5466
|
+
let body = {};
|
|
5467
|
+
try {
|
|
5468
|
+
body = JSON.parse(await readBody(req) || '{}');
|
|
5469
|
+
}
|
|
5470
|
+
catch {
|
|
5471
|
+
body = {};
|
|
5472
|
+
}
|
|
5473
|
+
const experimentId = String(body.experiment_id || '').trim();
|
|
5474
|
+
if (!experimentId) {
|
|
5475
|
+
json(res, { ok: false, error: 'Missing experiment_id' }, 400);
|
|
5476
|
+
return;
|
|
5477
|
+
}
|
|
5478
|
+
const store = new explorer_store_js_1.ExplorerStore();
|
|
5479
|
+
try {
|
|
5480
|
+
const experiment = store.getExperiment(experimentId);
|
|
5481
|
+
if (!experiment) {
|
|
5482
|
+
json(res, { ok: false, error: 'Experiment not found' }, 404);
|
|
5483
|
+
return;
|
|
5484
|
+
}
|
|
5485
|
+
const baselineArtifactDir = (0, config_js_1.getExplorerArtifactDir)(experiment.id, 'baseline');
|
|
5486
|
+
const baseline = (0, explorer_eval_js_1.runEvaluation)({
|
|
5487
|
+
worktree_path: experiment.repo_path,
|
|
5488
|
+
eval_command: experiment.eval_command,
|
|
5489
|
+
timeout_sec: experiment.eval_timeout_sec,
|
|
5490
|
+
artifact_dir: baselineArtifactDir,
|
|
5491
|
+
});
|
|
5492
|
+
store.createEvent(experiment.id, 'note', {
|
|
5493
|
+
message: 'experiment initialized with baseline evaluation',
|
|
5494
|
+
status: baseline.status,
|
|
5495
|
+
score: baseline.score ?? null,
|
|
5496
|
+
artifact_dir: baselineArtifactDir,
|
|
5497
|
+
error: baseline.error || null,
|
|
5498
|
+
});
|
|
5499
|
+
const overview = (0, explorer_js_1.getExperimentOverview)(experiment.id, store);
|
|
5500
|
+
json(res, {
|
|
5501
|
+
ok: true,
|
|
5502
|
+
experiment,
|
|
5503
|
+
baseline: {
|
|
5504
|
+
status: baseline.status,
|
|
5505
|
+
score: baseline.score ?? null,
|
|
5506
|
+
error: baseline.error || null,
|
|
5507
|
+
artifact_dir: baselineArtifactDir,
|
|
5508
|
+
},
|
|
5509
|
+
overview,
|
|
5510
|
+
flow: {
|
|
5511
|
+
tool: 'experiment_init',
|
|
5512
|
+
initialized: true,
|
|
5513
|
+
},
|
|
5514
|
+
});
|
|
5515
|
+
}
|
|
5516
|
+
finally {
|
|
5517
|
+
store.close();
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
catch (err) {
|
|
5521
|
+
json(res, { ok: false, error: err?.message || String(err) }, 500);
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
async function handlePostExplorerTick(req, res) {
|
|
5525
|
+
try {
|
|
5526
|
+
let body = {};
|
|
5527
|
+
try {
|
|
5528
|
+
body = JSON.parse(await readBody(req) || '{}');
|
|
5529
|
+
}
|
|
5530
|
+
catch {
|
|
5531
|
+
body = {};
|
|
5532
|
+
}
|
|
5533
|
+
const experimentId = String(body.experiment_id || '').trim();
|
|
5534
|
+
if (!experimentId) {
|
|
5535
|
+
json(res, { ok: false, error: 'Missing experiment_id' }, 400);
|
|
5536
|
+
return;
|
|
5537
|
+
}
|
|
5538
|
+
const result = (0, explorer_js_1.runAutopilotTick)(experimentId);
|
|
5539
|
+
const runDetails = result.run_id ? (0, explorer_js_1.getRunDetails)(result.run_id) : null;
|
|
5540
|
+
json(res, { ok: true, result, run_details: runDetails });
|
|
5541
|
+
}
|
|
5542
|
+
catch (err) {
|
|
5543
|
+
json(res, { ok: false, error: err?.message || String(err) }, 500);
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
4730
5546
|
function upgradeUnauthorized(socket) {
|
|
4731
5547
|
try {
|
|
4732
5548
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
@@ -5123,6 +5939,54 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
5123
5939
|
else if (pathname === '/api/admin/license' && method === 'GET') {
|
|
5124
5940
|
handleGetAdminLicense(req, res);
|
|
5125
5941
|
}
|
|
5942
|
+
else if (pathname === '/api/explorer/experiments' && method === 'GET') {
|
|
5943
|
+
handleGetExplorerExperiments(reqUrl, res);
|
|
5944
|
+
}
|
|
5945
|
+
else if (pathname === '/api/explorer/overview' && method === 'GET') {
|
|
5946
|
+
handleGetExplorerOverview(reqUrl, res);
|
|
5947
|
+
}
|
|
5948
|
+
else if (pathname === '/api/explorer/tree' && method === 'GET') {
|
|
5949
|
+
handleGetExplorerTree(reqUrl, res);
|
|
5950
|
+
}
|
|
5951
|
+
else if (pathname === '/api/explorer/runs' && method === 'GET') {
|
|
5952
|
+
handleGetExplorerRuns(reqUrl, res);
|
|
5953
|
+
}
|
|
5954
|
+
else if (pathname === '/api/explorer/run' && method === 'GET') {
|
|
5955
|
+
handleGetExplorerRun(reqUrl, res);
|
|
5956
|
+
}
|
|
5957
|
+
else if (pathname === '/api/explorer/compare' && method === 'GET') {
|
|
5958
|
+
handleGetExplorerCompare(reqUrl, res);
|
|
5959
|
+
}
|
|
5960
|
+
else if (pathname === '/api/explorer/artifact' && method === 'GET') {
|
|
5961
|
+
handleGetExplorerArtifact(reqUrl, res);
|
|
5962
|
+
}
|
|
5963
|
+
else if (pathname === '/api/explorer/register' && method === 'POST') {
|
|
5964
|
+
await handlePostExplorerRegister(req, res);
|
|
5965
|
+
}
|
|
5966
|
+
else if (pathname === '/api/explorer/init' && method === 'POST') {
|
|
5967
|
+
await handlePostExplorerInit(req, res);
|
|
5968
|
+
}
|
|
5969
|
+
else if (pathname === '/api/explorer/quickstart' && method === 'POST') {
|
|
5970
|
+
await handlePostExplorerQuickstart(req, res);
|
|
5971
|
+
}
|
|
5972
|
+
else if (pathname === '/api/explorer/tick' && method === 'POST') {
|
|
5973
|
+
await handlePostExplorerTick(req, res);
|
|
5974
|
+
}
|
|
5975
|
+
else if (pathname === '/api/explorer/step' && method === 'POST') {
|
|
5976
|
+
await handlePostExplorerTick(req, res);
|
|
5977
|
+
}
|
|
5978
|
+
else if (pathname === '/api/explorer/go' && method === 'POST') {
|
|
5979
|
+
await handlePostExplorerTick(req, res);
|
|
5980
|
+
}
|
|
5981
|
+
else if (pathname === '/api/display/file' && method === 'GET') {
|
|
5982
|
+
handleDisplayFile(reqUrl, res);
|
|
5983
|
+
}
|
|
5984
|
+
else if (pathname === '/api/widgets' && method === 'GET') {
|
|
5985
|
+
handleGetWidgets(res);
|
|
5986
|
+
}
|
|
5987
|
+
else if (pathname === '/api/widgets/clear' && method === 'POST') {
|
|
5988
|
+
handleClearWidgets(res);
|
|
5989
|
+
}
|
|
5126
5990
|
else if (pathname.startsWith('/fonts/') && method === 'GET') {
|
|
5127
5991
|
serveFontFile(pathname, res);
|
|
5128
5992
|
}
|
|
@@ -5278,7 +6142,8 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
5278
6142
|
}
|
|
5279
6143
|
if (useTcp) {
|
|
5280
6144
|
const actualPort = server.address()?.port ?? listenPort;
|
|
5281
|
-
|
|
6145
|
+
// Use an explicit IPv4 loopback host to avoid `localhost` IPv6 collisions on macOS.
|
|
6146
|
+
dashboardQuickLink = `http://127.0.0.1:${actualPort}${UI_SHORT_LINK_PREFIX}${uiShortCode}`;
|
|
5282
6147
|
log.step(`Settings: ${formatTerminalHyperlink(dashboardQuickLink)}`);
|
|
5283
6148
|
try {
|
|
5284
6149
|
writeDashboardLink(dashboardQuickLink);
|
|
@@ -5387,6 +6252,7 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
5387
6252
|
}
|
|
5388
6253
|
sseClients.clear();
|
|
5389
6254
|
stopResultsWatcher();
|
|
6255
|
+
stopDisplayWatcher();
|
|
5390
6256
|
// Cleanup SLURM resources
|
|
5391
6257
|
if (slurmPoller) {
|
|
5392
6258
|
slurmPoller.stop();
|
|
@@ -5400,6 +6266,7 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
5400
6266
|
slurmDB = null;
|
|
5401
6267
|
}
|
|
5402
6268
|
resultsStore = null;
|
|
6269
|
+
displayStore = null;
|
|
5403
6270
|
});
|
|
5404
6271
|
return server;
|
|
5405
6272
|
}
|