labgate 0.5.43 → 0.5.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/lib/container.d.ts +7 -0
- package/dist/lib/container.js +330 -29
- package/dist/lib/container.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/container.js
CHANGED
|
@@ -42,7 +42,9 @@ exports.imageToSifName = imageToSifName;
|
|
|
42
42
|
exports.isUsableApptainerSif = isUsableApptainerSif;
|
|
43
43
|
exports.ensureSifImage = ensureSifImage;
|
|
44
44
|
exports.resolveSlurmProxyPathToHost = resolveSlurmProxyPathToHost;
|
|
45
|
+
exports.listenOnUnixSocket = listenOnUnixSocket;
|
|
45
46
|
exports.buildCodexOauthPublishSpec = buildCodexOauthPublishSpec;
|
|
47
|
+
exports.describeCliStartupPhase = describeCliStartupPhase;
|
|
46
48
|
exports.buildEntrypoint = buildEntrypoint;
|
|
47
49
|
exports.setupBrowserHook = setupBrowserHook;
|
|
48
50
|
exports.getAgentTokenEnv = getAgentTokenEnv;
|
|
@@ -1018,7 +1020,32 @@ function mapSlurmProxyArgsToHost(session, args, containerCwd) {
|
|
|
1018
1020
|
}
|
|
1019
1021
|
return { ok: true, args: mappedArgs };
|
|
1020
1022
|
}
|
|
1021
|
-
function
|
|
1023
|
+
async function listenOnUnixSocket(server, socketHostPath) {
|
|
1024
|
+
await new Promise((resolve, reject) => {
|
|
1025
|
+
const cleanup = () => {
|
|
1026
|
+
server.off('error', onError);
|
|
1027
|
+
server.off('listening', onListening);
|
|
1028
|
+
};
|
|
1029
|
+
const onError = (err) => {
|
|
1030
|
+
cleanup();
|
|
1031
|
+
reject(err);
|
|
1032
|
+
};
|
|
1033
|
+
const onListening = () => {
|
|
1034
|
+
cleanup();
|
|
1035
|
+
resolve();
|
|
1036
|
+
};
|
|
1037
|
+
server.once('error', onError);
|
|
1038
|
+
server.once('listening', onListening);
|
|
1039
|
+
try {
|
|
1040
|
+
server.listen(socketHostPath);
|
|
1041
|
+
}
|
|
1042
|
+
catch (err) {
|
|
1043
|
+
cleanup();
|
|
1044
|
+
reject(err);
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
async function startSlurmHostProxy(session) {
|
|
1022
1049
|
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
1023
1050
|
const socketHostDir = (0, path_1.join)(sandboxHome, '.labgate', 'slurm', 'host-proxy');
|
|
1024
1051
|
const socketHostPath = (0, path_1.join)(socketHostDir, 'slurm.sock');
|
|
@@ -1141,10 +1168,14 @@ function startSlurmHostProxy(session) {
|
|
|
1141
1168
|
log.warn(`SLURM host proxy server error: ${err?.message ?? String(err)}`);
|
|
1142
1169
|
});
|
|
1143
1170
|
try {
|
|
1144
|
-
server
|
|
1171
|
+
await listenOnUnixSocket(server, socketHostPath);
|
|
1145
1172
|
}
|
|
1146
1173
|
catch (err) {
|
|
1147
1174
|
log.warn(`Could not start SLURM host proxy: ${err?.message ?? String(err)}`);
|
|
1175
|
+
try {
|
|
1176
|
+
server.close();
|
|
1177
|
+
}
|
|
1178
|
+
catch { /* ignore */ }
|
|
1148
1179
|
try {
|
|
1149
1180
|
(0, fs_1.unlinkSync)(socketHostPath);
|
|
1150
1181
|
}
|
|
@@ -1276,7 +1307,9 @@ const DEFAULT_CODEX_OAUTH_CALLBACK_PORT = 1455;
|
|
|
1276
1307
|
const CODEX_OAUTH_STARTUP_HEARTBEAT_MS = 10_000;
|
|
1277
1308
|
const CODEX_OAUTH_STARTUP_HEARTBEAT_MAX_MS = 50_000;
|
|
1278
1309
|
const CODEX_OAUTH_STARTUP_SPINNER_MS = 125;
|
|
1279
|
-
const
|
|
1310
|
+
const CLI_STARTUP_SPINNER_FRAMES = ['|', '/', '-', '\\'];
|
|
1311
|
+
const CLI_STARTUP_SPINNER_MS = 120;
|
|
1312
|
+
const CLI_STARTUP_HEARTBEAT_MS = 5_000;
|
|
1280
1313
|
const DEFERRED_SLURM_PASSTHROUGH_DELAY_MS = 1_500;
|
|
1281
1314
|
function getCodexOauthCallbackPort() {
|
|
1282
1315
|
const raw = (process.env.LABGATE_CODEX_OAUTH_CALLBACK_PORT || '').trim();
|
|
@@ -1305,6 +1338,64 @@ function buildCodexOauthPublishSpec(port) {
|
|
|
1305
1338
|
// Publishing without an explicit host IP gives dual-stack localhost reachability.
|
|
1306
1339
|
return `${port}:${port}`;
|
|
1307
1340
|
}
|
|
1341
|
+
function describeCliStartupPhase(agent, phase, runtime) {
|
|
1342
|
+
const agentLabel = String(agent || '').trim().toLowerCase() === 'codex' ? 'Codex' : 'Claude';
|
|
1343
|
+
if (phase === 'image') {
|
|
1344
|
+
if (runtime === 'apptainer')
|
|
1345
|
+
return `Preparing ${agentLabel} Apptainer sandbox`;
|
|
1346
|
+
if (runtime === 'podman')
|
|
1347
|
+
return `Preparing ${agentLabel} container sandbox`;
|
|
1348
|
+
return `Preparing ${agentLabel} sandbox`;
|
|
1349
|
+
}
|
|
1350
|
+
return `Launching ${agentLabel} inside sandbox`;
|
|
1351
|
+
}
|
|
1352
|
+
function startCliStartupHeartbeat(agent, phase, runtime) {
|
|
1353
|
+
const startedAt = Date.now();
|
|
1354
|
+
let stopped = false;
|
|
1355
|
+
let sawOutput = false;
|
|
1356
|
+
let interval = null;
|
|
1357
|
+
let spinnerFrame = 0;
|
|
1358
|
+
let currentPhase = phase;
|
|
1359
|
+
const useSpinner = !!(process.stderr.isTTY && process.stdout.isTTY);
|
|
1360
|
+
const render = () => {
|
|
1361
|
+
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
|
|
1362
|
+
const message = describeCliStartupPhase(agent, currentPhase, runtime);
|
|
1363
|
+
if (useSpinner) {
|
|
1364
|
+
const frame = CLI_STARTUP_SPINNER_FRAMES[spinnerFrame];
|
|
1365
|
+
spinnerFrame = (spinnerFrame + 1) % CLI_STARTUP_SPINNER_FRAMES.length;
|
|
1366
|
+
process.stderr.write(`\r\x1b[2K${log.dim('›')} ${message}... ${frame} ${elapsedSeconds}s`);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
log.step(`${message}... (${elapsedSeconds}s elapsed)`);
|
|
1370
|
+
};
|
|
1371
|
+
const stop = () => {
|
|
1372
|
+
if (stopped)
|
|
1373
|
+
return;
|
|
1374
|
+
stopped = true;
|
|
1375
|
+
if (interval) {
|
|
1376
|
+
clearInterval(interval);
|
|
1377
|
+
interval = null;
|
|
1378
|
+
}
|
|
1379
|
+
if (useSpinner)
|
|
1380
|
+
process.stderr.write('\r\x1b[2K');
|
|
1381
|
+
};
|
|
1382
|
+
const noteOutput = (data) => {
|
|
1383
|
+
if (stopped || sawOutput)
|
|
1384
|
+
return;
|
|
1385
|
+
if (String(data || '').length === 0)
|
|
1386
|
+
return;
|
|
1387
|
+
sawOutput = true;
|
|
1388
|
+
stop();
|
|
1389
|
+
};
|
|
1390
|
+
render();
|
|
1391
|
+
interval = setInterval(() => {
|
|
1392
|
+
if (stopped || sawOutput)
|
|
1393
|
+
return;
|
|
1394
|
+
render();
|
|
1395
|
+
}, useSpinner ? CLI_STARTUP_SPINNER_MS : CLI_STARTUP_HEARTBEAT_MS);
|
|
1396
|
+
interval.unref();
|
|
1397
|
+
return { noteOutput, stop };
|
|
1398
|
+
}
|
|
1308
1399
|
function startCodexOauthStartupHeartbeat() {
|
|
1309
1400
|
const startedAt = Date.now();
|
|
1310
1401
|
let stopped = false;
|
|
@@ -1316,8 +1407,8 @@ function startCodexOauthStartupHeartbeat() {
|
|
|
1316
1407
|
const useSpinner = !!(process.stderr.isTTY && process.stdout.isTTY);
|
|
1317
1408
|
const renderSpinner = () => {
|
|
1318
1409
|
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
|
|
1319
|
-
const frame =
|
|
1320
|
-
spinnerFrame = (spinnerFrame + 1) %
|
|
1410
|
+
const frame = CLI_STARTUP_SPINNER_FRAMES[spinnerFrame];
|
|
1411
|
+
spinnerFrame = (spinnerFrame + 1) % CLI_STARTUP_SPINNER_FRAMES.length;
|
|
1321
1412
|
process.stderr.write(`\r\x1b[2K${log.dim('›')} Waiting for Codex login output... ${frame} ${elapsedSeconds}s`);
|
|
1322
1413
|
spinnerActive = true;
|
|
1323
1414
|
};
|
|
@@ -2040,6 +2131,45 @@ function logStartupTimings(entries, totalMs) {
|
|
|
2040
2131
|
const prefix = summary ? `${summary}, ` : '';
|
|
2041
2132
|
log.step(`[labgate] startup timings: ${prefix}total=${formatStartupDuration(totalMs)}`);
|
|
2042
2133
|
}
|
|
2134
|
+
function createStartupReportData(session, runtime, image) {
|
|
2135
|
+
const now = new Date().toISOString();
|
|
2136
|
+
const warnings = [];
|
|
2137
|
+
if (session.uiDetected === false)
|
|
2138
|
+
warnings.push('ui_not_detected');
|
|
2139
|
+
return {
|
|
2140
|
+
schema_version: 1,
|
|
2141
|
+
kind: 'labgate-startup-report',
|
|
2142
|
+
generated_at: now,
|
|
2143
|
+
updated_at: now,
|
|
2144
|
+
pid: process.pid,
|
|
2145
|
+
node: (0, os_1.hostname)(),
|
|
2146
|
+
agent: session.agent,
|
|
2147
|
+
runtime,
|
|
2148
|
+
workdir: session.workdir,
|
|
2149
|
+
image,
|
|
2150
|
+
dry_run: session.dryRun,
|
|
2151
|
+
status: session.dryRun ? 'dry-run' : 'starting',
|
|
2152
|
+
launch_mode: session.dryRun ? 'dry-run' : null,
|
|
2153
|
+
ui_detected: session.uiDetected ?? null,
|
|
2154
|
+
total_ms: 0,
|
|
2155
|
+
first_output_ms: null,
|
|
2156
|
+
timings_ms: {},
|
|
2157
|
+
cold_start_hints: [],
|
|
2158
|
+
warnings,
|
|
2159
|
+
slurm: {
|
|
2160
|
+
enabled: session.config.slurm.enabled,
|
|
2161
|
+
host_proxy_enabled: null,
|
|
2162
|
+
passthrough_mode: null,
|
|
2163
|
+
host_commands_found: null,
|
|
2164
|
+
staged_commands: null,
|
|
2165
|
+
reused_stage: null,
|
|
2166
|
+
},
|
|
2167
|
+
exit_code: null,
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
function writeStartupReportFile(path, report) {
|
|
2171
|
+
(0, startup_stage_lock_js_1.writeTextFileAtomic)(path, JSON.stringify(report, null, 2) + '\n', { mode: 0o600 });
|
|
2172
|
+
}
|
|
2043
2173
|
// ── Shared session helpers ─────────────────────────────────
|
|
2044
2174
|
function logSessionStart(session, sessionId) {
|
|
2045
2175
|
if (!session.config.audit.enabled)
|
|
@@ -2137,15 +2267,94 @@ function printSessionInfo(session, sessionId, runtime) {
|
|
|
2137
2267
|
async function startSession(session) {
|
|
2138
2268
|
const startupStartedAt = Date.now();
|
|
2139
2269
|
const startupTimings = [];
|
|
2140
|
-
const recordStartupTiming = (label, startedAt) => {
|
|
2141
|
-
startupTimings.push([label, Math.max(0, Date.now() - startedAt)]);
|
|
2142
|
-
};
|
|
2143
2270
|
const preferred = session.config.runtime;
|
|
2144
2271
|
const runtime = session.dryRun ? getDryRunRuntime(preferred) : (0, runtime_js_1.getRuntime)(preferred);
|
|
2145
2272
|
const image = session.imageOverride ?? session.config.image;
|
|
2146
2273
|
const sessionId = (0, crypto_1.randomBytes)(4).toString('hex');
|
|
2147
2274
|
const footerMode = session.footerMode ?? 'sticky';
|
|
2148
2275
|
const footerLine = formatStatusFooter(session, runtime, sessionId, image);
|
|
2276
|
+
const startupReport = session.startupReportPath
|
|
2277
|
+
? createStartupReportData(session, runtime, image)
|
|
2278
|
+
: null;
|
|
2279
|
+
let startupReportWriteFailed = false;
|
|
2280
|
+
const flushStartupReport = () => {
|
|
2281
|
+
if (!startupReport || !session.startupReportPath || startupReportWriteFailed)
|
|
2282
|
+
return;
|
|
2283
|
+
startupReport.updated_at = new Date().toISOString();
|
|
2284
|
+
startupReport.total_ms = Math.max(0, Date.now() - startupStartedAt);
|
|
2285
|
+
try {
|
|
2286
|
+
writeStartupReportFile(session.startupReportPath, startupReport);
|
|
2287
|
+
}
|
|
2288
|
+
catch (err) {
|
|
2289
|
+
startupReportWriteFailed = true;
|
|
2290
|
+
log.warn(`Could not write startup report to ${session.startupReportPath}: ${err?.message ?? String(err)}`);
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
const noteStartupWarning = (warning) => {
|
|
2294
|
+
if (!startupReport)
|
|
2295
|
+
return;
|
|
2296
|
+
if (!startupReport.warnings.includes(warning)) {
|
|
2297
|
+
startupReport.warnings.push(warning);
|
|
2298
|
+
flushStartupReport();
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
const noteColdStartHint = (hint) => {
|
|
2302
|
+
if (!startupReport)
|
|
2303
|
+
return;
|
|
2304
|
+
if (!startupReport.cold_start_hints.includes(hint)) {
|
|
2305
|
+
startupReport.cold_start_hints.push(hint);
|
|
2306
|
+
flushStartupReport();
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
const setStartupStatus = (status) => {
|
|
2310
|
+
if (!startupReport)
|
|
2311
|
+
return;
|
|
2312
|
+
startupReport.status = status;
|
|
2313
|
+
flushStartupReport();
|
|
2314
|
+
};
|
|
2315
|
+
const setStartupLaunchMode = (mode) => {
|
|
2316
|
+
if (!startupReport)
|
|
2317
|
+
return;
|
|
2318
|
+
startupReport.launch_mode = mode;
|
|
2319
|
+
flushStartupReport();
|
|
2320
|
+
};
|
|
2321
|
+
const noteStartupFirstOutput = (data) => {
|
|
2322
|
+
if (!startupReport)
|
|
2323
|
+
return;
|
|
2324
|
+
if (startupReport.first_output_ms !== null)
|
|
2325
|
+
return;
|
|
2326
|
+
if (String(data || '').length === 0)
|
|
2327
|
+
return;
|
|
2328
|
+
const firstOutputMs = Math.max(0, Date.now() - startupStartedAt);
|
|
2329
|
+
startupReport.first_output_ms = firstOutputMs;
|
|
2330
|
+
startupReport.timings_ms.launch_first_output = firstOutputMs;
|
|
2331
|
+
startupReport.status = 'running';
|
|
2332
|
+
flushStartupReport();
|
|
2333
|
+
};
|
|
2334
|
+
const updateSlurmStartupReport = (payload) => {
|
|
2335
|
+
if (!startupReport)
|
|
2336
|
+
return;
|
|
2337
|
+
if (payload.hostProxyEnabled !== undefined)
|
|
2338
|
+
startupReport.slurm.host_proxy_enabled = payload.hostProxyEnabled;
|
|
2339
|
+
if (payload.passthroughMode !== undefined)
|
|
2340
|
+
startupReport.slurm.passthrough_mode = payload.passthroughMode;
|
|
2341
|
+
if (payload.hostCommandsFound !== undefined)
|
|
2342
|
+
startupReport.slurm.host_commands_found = payload.hostCommandsFound;
|
|
2343
|
+
if (payload.stagedCommands !== undefined)
|
|
2344
|
+
startupReport.slurm.staged_commands = payload.stagedCommands;
|
|
2345
|
+
if (payload.reusedStage !== undefined)
|
|
2346
|
+
startupReport.slurm.reused_stage = payload.reusedStage;
|
|
2347
|
+
flushStartupReport();
|
|
2348
|
+
};
|
|
2349
|
+
const recordStartupTiming = (label, startedAt) => {
|
|
2350
|
+
const elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
2351
|
+
startupTimings.push([label, elapsedMs]);
|
|
2352
|
+
if (startupReport) {
|
|
2353
|
+
startupReport.timings_ms[label] = elapsedMs;
|
|
2354
|
+
flushStartupReport();
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
flushStartupReport();
|
|
2149
2358
|
// Extract agent auth token (CLI flag → env var)
|
|
2150
2359
|
const tokenEnv = session.dryRun ? [] : getAgentTokenEnv(session.agent, session.apiKey);
|
|
2151
2360
|
const bridgeCodexOauthForPodman = runtime === 'podman' && shouldBridgeCodexOauthForPodman(session.agent, session.config.network.mode);
|
|
@@ -2181,6 +2390,7 @@ async function startSession(session) {
|
|
|
2181
2390
|
let cleanupSlurmHostProxy = () => { };
|
|
2182
2391
|
let cleanupDeferredSlurmPassthrough = () => { };
|
|
2183
2392
|
let startDeferredSlurmPassthrough = () => { };
|
|
2393
|
+
let startupHeartbeat = null;
|
|
2184
2394
|
// If the agent isn't installed in the persistent sandbox home yet, warn that first run can be slow.
|
|
2185
2395
|
if (!session.dryRun) {
|
|
2186
2396
|
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
@@ -2188,18 +2398,28 @@ async function startSession(session) {
|
|
|
2188
2398
|
const installedBin = (0, path_1.join)(sandboxHome, '.npm-global', 'bin', agentBin);
|
|
2189
2399
|
try {
|
|
2190
2400
|
if (!(0, fs_1.existsSync)(installedBin)) {
|
|
2401
|
+
noteColdStartHint('agent_missing_in_sandbox');
|
|
2191
2402
|
log.step('First run: preparing sandbox (pulling image + installing agent). This can take a minute...');
|
|
2192
2403
|
}
|
|
2193
2404
|
}
|
|
2194
2405
|
catch { /* ignore */ }
|
|
2406
|
+
if (runtime === 'apptainer') {
|
|
2407
|
+
try {
|
|
2408
|
+
const cachedSifPath = (0, path_1.join)((0, config_js_1.getImagesDir)(), imageToSifName(image));
|
|
2409
|
+
if (!(0, fs_1.existsSync)(cachedSifPath))
|
|
2410
|
+
noteColdStartHint('sif_cache_missing');
|
|
2411
|
+
}
|
|
2412
|
+
catch { /* ignore */ }
|
|
2413
|
+
}
|
|
2195
2414
|
}
|
|
2196
2415
|
// For Apptainer sessions, always try SLURM CLI passthrough so sbatch/squeue
|
|
2197
2416
|
// can work out of the box when present on the host. Full SLURM tracking/MCP
|
|
2198
2417
|
// remains controlled by slurm.enabled.
|
|
2199
2418
|
if (!session.dryRun && runtime === 'apptainer') {
|
|
2200
2419
|
const hostProxyStartedAt = Date.now();
|
|
2201
|
-
const hostProxy = startSlurmHostProxy(session);
|
|
2420
|
+
const hostProxy = await startSlurmHostProxy(session);
|
|
2202
2421
|
recordStartupTiming('slurm_host_proxy', hostProxyStartedAt);
|
|
2422
|
+
updateSlurmStartupReport({ hostProxyEnabled: !!hostProxy });
|
|
2203
2423
|
const trackingEnabled = session.config.slurm.enabled;
|
|
2204
2424
|
const runSlurmPassthroughStage = (mode) => {
|
|
2205
2425
|
const stageStartedAt = Date.now();
|
|
@@ -2212,8 +2432,15 @@ async function startSession(session) {
|
|
|
2212
2432
|
preferHostProxy: false,
|
|
2213
2433
|
});
|
|
2214
2434
|
recordStartupTiming(mode === 'startup' ? 'slurm_passthrough_stage' : 'slurm_passthrough_stage_background', stageStartedAt);
|
|
2435
|
+
updateSlurmStartupReport({
|
|
2436
|
+
passthroughMode: staged.ok ? 'staged' : 'unavailable',
|
|
2437
|
+
hostCommandsFound: staged.hostCommands.length,
|
|
2438
|
+
stagedCommands: staged.staged.length,
|
|
2439
|
+
reusedStage: staged.reused,
|
|
2440
|
+
});
|
|
2215
2441
|
if (trackingEnabled) {
|
|
2216
2442
|
if (!staged.ok) {
|
|
2443
|
+
noteStartupWarning('slurm_commands_unavailable');
|
|
2217
2444
|
log.warn('SLURM is enabled but no SLURM commands were found on the host PATH. ' +
|
|
2218
2445
|
'Inside-sandbox SLURM commands (squeue/sbatch/...) will be unavailable.');
|
|
2219
2446
|
}
|
|
@@ -2236,6 +2463,13 @@ async function startSession(session) {
|
|
|
2236
2463
|
}
|
|
2237
2464
|
catch (err) {
|
|
2238
2465
|
recordStartupTiming(mode === 'startup' ? 'slurm_passthrough_stage' : 'slurm_passthrough_stage_background', stageStartedAt);
|
|
2466
|
+
updateSlurmStartupReport({
|
|
2467
|
+
passthroughMode: 'unavailable',
|
|
2468
|
+
reusedStage: false,
|
|
2469
|
+
});
|
|
2470
|
+
noteStartupWarning(mode === 'startup'
|
|
2471
|
+
? 'slurm_passthrough_stage_failed'
|
|
2472
|
+
: 'slurm_passthrough_stage_background_failed');
|
|
2239
2473
|
log.warn(mode === 'startup'
|
|
2240
2474
|
? `SLURM passthrough staging failed: ${err?.message ?? String(err)}`
|
|
2241
2475
|
: `Deferred SLURM passthrough staging failed: ${err?.message ?? String(err)}`);
|
|
@@ -2253,8 +2487,17 @@ async function startSession(session) {
|
|
|
2253
2487
|
preferHostProxy: true,
|
|
2254
2488
|
});
|
|
2255
2489
|
recordStartupTiming('slurm_passthrough_stage', stageStartedAt);
|
|
2490
|
+
updateSlurmStartupReport({
|
|
2491
|
+
passthroughMode: staged.ok
|
|
2492
|
+
? (staged.mode === 'proxy-only' ? 'proxy-only' : 'staged')
|
|
2493
|
+
: 'unavailable',
|
|
2494
|
+
hostCommandsFound: staged.hostCommands.length,
|
|
2495
|
+
stagedCommands: staged.staged.length,
|
|
2496
|
+
reusedStage: staged.reused,
|
|
2497
|
+
});
|
|
2256
2498
|
if (trackingEnabled) {
|
|
2257
2499
|
if (!staged.ok) {
|
|
2500
|
+
noteStartupWarning('slurm_commands_unavailable');
|
|
2258
2501
|
log.warn('SLURM is enabled but no SLURM commands were found on the host PATH. ' +
|
|
2259
2502
|
'Inside-sandbox SLURM commands (squeue/sbatch/...) will be unavailable.');
|
|
2260
2503
|
}
|
|
@@ -2275,12 +2518,19 @@ async function startSession(session) {
|
|
|
2275
2518
|
}
|
|
2276
2519
|
}
|
|
2277
2520
|
else {
|
|
2521
|
+
noteStartupWarning('slurm_host_proxy_unavailable');
|
|
2278
2522
|
if (trackingEnabled) {
|
|
2279
2523
|
log.step('SLURM host proxy unavailable. Staging SLURM passthrough before startup.');
|
|
2280
2524
|
runSlurmPassthroughStage('startup');
|
|
2281
2525
|
}
|
|
2282
2526
|
else {
|
|
2283
2527
|
log.step('SLURM host proxy unavailable. Deferring SLURM passthrough staging until after startup.');
|
|
2528
|
+
updateSlurmStartupReport({
|
|
2529
|
+
passthroughMode: 'deferred-stage',
|
|
2530
|
+
hostCommandsFound: null,
|
|
2531
|
+
stagedCommands: null,
|
|
2532
|
+
reusedStage: null,
|
|
2533
|
+
});
|
|
2284
2534
|
let cancelled = false;
|
|
2285
2535
|
let timer = null;
|
|
2286
2536
|
let started = false;
|
|
@@ -2308,32 +2558,45 @@ async function startSession(session) {
|
|
|
2308
2558
|
}
|
|
2309
2559
|
}
|
|
2310
2560
|
let args;
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
if (
|
|
2314
|
-
|
|
2561
|
+
const imagePrepareStartedAt = Date.now();
|
|
2562
|
+
try {
|
|
2563
|
+
if (runtime === 'apptainer') {
|
|
2564
|
+
startupHeartbeat = startCliStartupHeartbeat(session.agent, 'image', runtime);
|
|
2565
|
+
let sifPath;
|
|
2566
|
+
if (session.dryRun) {
|
|
2567
|
+
sifPath = (0, path_1.join)((0, config_js_1.getImagesDir)(), imageToSifName(image));
|
|
2568
|
+
}
|
|
2569
|
+
else {
|
|
2570
|
+
sifPath = await ensureSifImage(runtime, image);
|
|
2571
|
+
}
|
|
2572
|
+
args = buildApptainerArgs(session, sifPath, sessionId, runtimeEnvArgs);
|
|
2315
2573
|
}
|
|
2316
2574
|
else {
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
log.step(`Codex OAuth callback bridge enabled on localhost:${port} ` +
|
|
2325
|
-
'(Podman macOS uses bridge networking for callback compatibility).');
|
|
2326
|
-
if (needsCodexOauthStartupHeartbeat) {
|
|
2327
|
-
log.step('Launching Codex OAuth flow. Login output may take ~30s to appear.');
|
|
2575
|
+
if (bridgeCodexOauthForPodman) {
|
|
2576
|
+
const port = getCodexOauthCallbackPort();
|
|
2577
|
+
log.step(`Codex OAuth callback bridge enabled on localhost:${port} ` +
|
|
2578
|
+
'(Podman macOS uses bridge networking for callback compatibility).');
|
|
2579
|
+
if (needsCodexOauthStartupHeartbeat) {
|
|
2580
|
+
log.step('Launching Codex OAuth flow. Login output may take ~30s to appear.');
|
|
2581
|
+
}
|
|
2328
2582
|
}
|
|
2583
|
+
startupHeartbeat = startCliStartupHeartbeat(session.agent, 'image', runtime);
|
|
2584
|
+
if (!session.dryRun) {
|
|
2585
|
+
ensurePodmanImage(runtime, image);
|
|
2586
|
+
}
|
|
2587
|
+
args = buildPodmanArgs(session, image, sessionId, runtimeEnvArgs, { tty: !!(process.stdout.isTTY && process.stdin.isTTY) });
|
|
2329
2588
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2589
|
+
}
|
|
2590
|
+
finally {
|
|
2591
|
+
recordStartupTiming('image_prepare', imagePrepareStartedAt);
|
|
2592
|
+
startupHeartbeat?.stop();
|
|
2593
|
+
startupHeartbeat = null;
|
|
2334
2594
|
}
|
|
2335
2595
|
if (session.dryRun) {
|
|
2596
|
+
setStartupLaunchMode('dry-run');
|
|
2597
|
+
setStartupStatus('dry-run');
|
|
2336
2598
|
prettyPrintCommand(runtime, args);
|
|
2599
|
+
flushStartupReport();
|
|
2337
2600
|
return;
|
|
2338
2601
|
}
|
|
2339
2602
|
// Create OAuth URL interceptor as a fallback when BROWSER hook does not fire.
|
|
@@ -2372,6 +2635,7 @@ async function startSession(session) {
|
|
|
2372
2635
|
sessionSlurmPoller.start();
|
|
2373
2636
|
}
|
|
2374
2637
|
catch (err) {
|
|
2638
|
+
noteStartupWarning('slurm_tracking_unavailable');
|
|
2375
2639
|
log.warn(`SLURM tracking unavailable: ${err?.message ?? String(err)}`);
|
|
2376
2640
|
cleanupSlurm();
|
|
2377
2641
|
}
|
|
@@ -2389,7 +2653,11 @@ async function startSession(session) {
|
|
|
2389
2653
|
const wantsSticky = footerMode === 'sticky';
|
|
2390
2654
|
const needsOAuthPtyFallback = !!oauthInterceptor;
|
|
2391
2655
|
const hasTty = !!(process.stdout.isTTY && process.stdin.isTTY);
|
|
2392
|
-
const
|
|
2656
|
+
const wantsStartupOutputCapture = !!startupReport;
|
|
2657
|
+
const shouldUsePty = hasTty && (wantsSticky
|
|
2658
|
+
|| needsOAuthPtyFallback
|
|
2659
|
+
|| needsCodexOauthStartupHeartbeat
|
|
2660
|
+
|| wantsStartupOutputCapture);
|
|
2393
2661
|
if (shouldUsePty) {
|
|
2394
2662
|
const pty = await loadPty();
|
|
2395
2663
|
if (!pty) {
|
|
@@ -2402,6 +2670,10 @@ async function startSession(session) {
|
|
|
2402
2670
|
else if (needsCodexOauthStartupHeartbeat) {
|
|
2403
2671
|
log.step('Codex startup heartbeat unavailable (node-pty missing).');
|
|
2404
2672
|
}
|
|
2673
|
+
else if (wantsStartupOutputCapture) {
|
|
2674
|
+
noteStartupWarning('startup_output_capture_unavailable');
|
|
2675
|
+
log.step('Startup output capture unavailable (node-pty missing).');
|
|
2676
|
+
}
|
|
2405
2677
|
}
|
|
2406
2678
|
else {
|
|
2407
2679
|
let runtimePath;
|
|
@@ -2419,6 +2691,9 @@ async function startSession(session) {
|
|
|
2419
2691
|
const cols = process.stdout.columns || 80;
|
|
2420
2692
|
const rows = process.stdout.rows || 24;
|
|
2421
2693
|
let child;
|
|
2694
|
+
const launchHeartbeat = startCliStartupHeartbeat(session.agent, 'launch', runtime);
|
|
2695
|
+
setStartupStatus('launching');
|
|
2696
|
+
setStartupLaunchMode('pty');
|
|
2422
2697
|
try {
|
|
2423
2698
|
child = pty.spawn(runtimePath, args, {
|
|
2424
2699
|
name: 'xterm-256color',
|
|
@@ -2429,6 +2704,8 @@ async function startSession(session) {
|
|
|
2429
2704
|
});
|
|
2430
2705
|
}
|
|
2431
2706
|
catch (err) {
|
|
2707
|
+
launchHeartbeat.stop();
|
|
2708
|
+
noteStartupWarning('pty_spawn_failed');
|
|
2432
2709
|
log.step(`PTY spawn failed (${err?.message ?? String(err)}). Falling back to standard spawn.`);
|
|
2433
2710
|
// Fall through to standard spawn path below.
|
|
2434
2711
|
child = null;
|
|
@@ -2452,7 +2729,9 @@ async function startSession(session) {
|
|
|
2452
2729
|
renderStickyFooter(footerLine);
|
|
2453
2730
|
}
|
|
2454
2731
|
child.onData((data) => {
|
|
2732
|
+
launchHeartbeat.noteOutput(data);
|
|
2455
2733
|
codexOauthHeartbeat?.noteOutput(data);
|
|
2734
|
+
noteStartupFirstOutput(data);
|
|
2456
2735
|
if (oauthInterceptor)
|
|
2457
2736
|
oauthInterceptor.feed(data);
|
|
2458
2737
|
process.stdout.write(data);
|
|
@@ -2475,7 +2754,13 @@ async function startSession(session) {
|
|
|
2475
2754
|
const timeoutHandle = setupSessionTimeout(session, sessionId, runtime, () => exited, () => child.kill('SIGTERM'));
|
|
2476
2755
|
child.onExit((event) => {
|
|
2477
2756
|
exited = true;
|
|
2757
|
+
launchHeartbeat.stop();
|
|
2478
2758
|
codexOauthHeartbeat?.stop();
|
|
2759
|
+
if (startupReport) {
|
|
2760
|
+
startupReport.exit_code = event.exitCode ?? 0;
|
|
2761
|
+
startupReport.status = 'exited';
|
|
2762
|
+
flushStartupReport();
|
|
2763
|
+
}
|
|
2479
2764
|
if (timeoutHandle)
|
|
2480
2765
|
clearTimeout(timeoutHandle);
|
|
2481
2766
|
browserHook?.cleanup();
|
|
@@ -2505,17 +2790,33 @@ async function startSession(session) {
|
|
|
2505
2790
|
else if (needsOAuthPtyFallback && !hasTty) {
|
|
2506
2791
|
log.step('OAuth URL fallback interceptor requires a TTY; relying on BROWSER hook only.');
|
|
2507
2792
|
}
|
|
2793
|
+
else if (wantsStartupOutputCapture && !hasTty) {
|
|
2794
|
+
noteStartupWarning('startup_output_capture_unavailable');
|
|
2795
|
+
log.step('Startup output capture requires a TTY; report will omit first-output timing.');
|
|
2796
|
+
}
|
|
2508
2797
|
if (footerMode === 'sticky') {
|
|
2509
2798
|
console.log(footerLine);
|
|
2510
2799
|
}
|
|
2800
|
+
log.step(`${describeCliStartupPhase(session.agent, 'launch', runtime)}...`);
|
|
2511
2801
|
console.log('');
|
|
2512
2802
|
// Spawn path: must use stdio:'inherit' to preserve TTY for Claude Code.
|
|
2513
2803
|
// OAuth interception relies on the BROWSER hook + file watcher here
|
|
2514
2804
|
// (the PTY path above uses onData for direct interception).
|
|
2805
|
+
setStartupStatus('launching');
|
|
2806
|
+
setStartupLaunchMode('inherit');
|
|
2515
2807
|
const child = (0, child_process_1.spawn)(runtime, args, { stdio: 'inherit' });
|
|
2808
|
+
if (startupReport && startupReport.first_output_ms === null) {
|
|
2809
|
+
startupReport.status = 'running';
|
|
2810
|
+
flushStartupReport();
|
|
2811
|
+
}
|
|
2516
2812
|
startDeferredSlurmPassthrough();
|
|
2517
2813
|
const timeoutHandle = setupSessionTimeout(session, sessionId, runtime, () => child.exitCode !== null, () => child.kill('SIGTERM'));
|
|
2518
2814
|
child.on('close', (code) => {
|
|
2815
|
+
if (startupReport) {
|
|
2816
|
+
startupReport.exit_code = code ?? 0;
|
|
2817
|
+
startupReport.status = 'exited';
|
|
2818
|
+
flushStartupReport();
|
|
2819
|
+
}
|
|
2519
2820
|
if (timeoutHandle)
|
|
2520
2821
|
clearTimeout(timeoutHandle);
|
|
2521
2822
|
browserHook?.cleanup();
|