@the-open-engine/zeroshot 6.36.0 → 6.37.0
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/cli/index.js +99 -29
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +25 -76
- package/src/agent/agent-liveness-poll.js +93 -0
- package/src/agent/agent-task-executor.js +19 -3
- package/src/foreground-benchmark-files.js +81 -0
- package/src/foreground-benchmark-result.js +233 -0
- package/src/foreground-benchmark-run.js +72 -0
- package/task-lib/runner.js +7 -0
package/cli/index.js
CHANGED
|
@@ -73,6 +73,11 @@ const {
|
|
|
73
73
|
resolveEffectiveRunPlan,
|
|
74
74
|
} = require('../lib/start-cluster');
|
|
75
75
|
const { requirePreflight } = require('../src/preflight');
|
|
76
|
+
const {
|
|
77
|
+
exitCodeForResult,
|
|
78
|
+
isForegroundStatusSettled,
|
|
79
|
+
writeForegroundResult,
|
|
80
|
+
} = require('../src/foreground-benchmark-run');
|
|
76
81
|
const {
|
|
77
82
|
createUnsupportedProviderCapabilityError,
|
|
78
83
|
serializeTaskStartupError,
|
|
@@ -251,6 +256,17 @@ function shouldRunDetached(options) {
|
|
|
251
256
|
return options.detach && !process.env.ZEROSHOT_DAEMON;
|
|
252
257
|
}
|
|
253
258
|
|
|
259
|
+
function requireForegroundResultMode(options) {
|
|
260
|
+
if (!options.resultFile) return;
|
|
261
|
+
if (shouldRunDetached(options) || process.env.ZEROSHOT_DAEMON) {
|
|
262
|
+
throw new Error('--result-file requires foreground execution without --detach');
|
|
263
|
+
}
|
|
264
|
+
if (options.pr || options.ship) {
|
|
265
|
+
throw new Error('--result-file is incompatible with --pr and --ship delivery');
|
|
266
|
+
}
|
|
267
|
+
process.env.ZEROSHOT_TASK_EXECUTION_CONTEXT = 'benchmark';
|
|
268
|
+
}
|
|
269
|
+
|
|
254
270
|
function printDetachedClusterStart(plan, clusterId, logPath) {
|
|
255
271
|
const runMode = runModeFromPlan(plan);
|
|
256
272
|
console.log(runMode ? `Started ${clusterId} (${runMode})` : `Started ${clusterId}`);
|
|
@@ -605,65 +621,83 @@ function createForegroundCleanup({
|
|
|
605
621
|
return { stop, stopWithFlush };
|
|
606
622
|
}
|
|
607
623
|
|
|
608
|
-
function
|
|
609
|
-
|
|
624
|
+
function setupForegroundSignalHandler({ orchestrator, clusterId, cleanup, settle, handleSigterm }) {
|
|
625
|
+
let handling = false;
|
|
626
|
+
const handler = async (signal) => {
|
|
627
|
+
if (handling) return;
|
|
628
|
+
handling = true;
|
|
610
629
|
cleanup.stop();
|
|
611
|
-
|
|
612
|
-
console.log(chalk.dim('\n\n--- Interrupted ---'));
|
|
630
|
+
console.log(chalk.dim(`\n\n--- Interrupted (${signal}) ---`));
|
|
613
631
|
|
|
614
632
|
try {
|
|
615
633
|
console.log(chalk.dim(`Stopping cluster ${clusterId}...`));
|
|
616
634
|
await orchestrator.stop(clusterId);
|
|
617
635
|
console.log(chalk.dim(`Cluster ${clusterId} stopped.`));
|
|
636
|
+
settle({ cancelled: true });
|
|
618
637
|
} catch (stopErr) {
|
|
619
|
-
|
|
638
|
+
settle(null, stopErr);
|
|
620
639
|
}
|
|
621
|
-
|
|
622
|
-
process.exit(0);
|
|
623
640
|
};
|
|
624
641
|
|
|
625
|
-
|
|
642
|
+
const onSigint = () => handler('SIGINT');
|
|
643
|
+
const onSigterm = () => handler('SIGTERM');
|
|
644
|
+
process.on('SIGINT', onSigint);
|
|
645
|
+
if (handleSigterm) process.on('SIGTERM', onSigterm);
|
|
626
646
|
return () => {
|
|
627
|
-
process.off('SIGINT',
|
|
647
|
+
process.off('SIGINT', onSigint);
|
|
648
|
+
if (handleSigterm) process.off('SIGTERM', onSigterm);
|
|
628
649
|
};
|
|
629
650
|
}
|
|
630
651
|
|
|
631
|
-
function waitForClusterCompletion(orchestrator, clusterId, cleanup) {
|
|
632
|
-
return new Promise((resolve) => {
|
|
652
|
+
function waitForClusterCompletion(orchestrator, clusterId, cleanup, handleSigterm) {
|
|
653
|
+
return new Promise((resolve, reject) => {
|
|
633
654
|
let checkInterval;
|
|
655
|
+
let settled = false;
|
|
656
|
+
let terminalObservedAt = null;
|
|
657
|
+
let removeSignals = () => {};
|
|
634
658
|
const stopChecking = () => {
|
|
635
659
|
if (checkInterval) {
|
|
636
660
|
clearInterval(checkInterval);
|
|
637
661
|
}
|
|
638
662
|
};
|
|
639
|
-
const
|
|
663
|
+
const settle = (result, error) => {
|
|
664
|
+
if (settled) return;
|
|
665
|
+
settled = true;
|
|
666
|
+
stopChecking();
|
|
667
|
+
removeSignals();
|
|
668
|
+
if (error) reject(error);
|
|
669
|
+
else resolve(result);
|
|
670
|
+
};
|
|
671
|
+
removeSignals = setupForegroundSignalHandler({
|
|
640
672
|
orchestrator,
|
|
641
673
|
clusterId,
|
|
642
674
|
cleanup,
|
|
643
|
-
|
|
675
|
+
settle,
|
|
676
|
+
handleSigterm,
|
|
644
677
|
});
|
|
645
678
|
|
|
646
|
-
const finish = (finalizer) => {
|
|
647
|
-
stopChecking();
|
|
648
|
-
removeSigint();
|
|
649
|
-
finalizer();
|
|
650
|
-
resolve();
|
|
651
|
-
};
|
|
652
|
-
|
|
653
679
|
checkInterval = setInterval(() => {
|
|
654
680
|
try {
|
|
655
681
|
const status = orchestrator.getStatus(clusterId);
|
|
656
|
-
if (status
|
|
657
|
-
|
|
682
|
+
if (isForegroundStatusSettled(status)) {
|
|
683
|
+
cleanup.stopWithFlush();
|
|
684
|
+
settle({ cancelled: false });
|
|
685
|
+
} else if (['stopped', 'killed'].includes(status.state)) {
|
|
686
|
+
terminalObservedAt ??= Date.now();
|
|
687
|
+
if (Date.now() - terminalObservedAt > 30_000) {
|
|
688
|
+
cleanup.stop();
|
|
689
|
+
settle(null, new Error('foreground agent processes did not settle after terminal'));
|
|
690
|
+
}
|
|
658
691
|
}
|
|
659
|
-
} catch {
|
|
660
|
-
|
|
692
|
+
} catch (error) {
|
|
693
|
+
cleanup.stop();
|
|
694
|
+
settle(null, error);
|
|
661
695
|
}
|
|
662
696
|
}, 500);
|
|
663
697
|
});
|
|
664
698
|
}
|
|
665
699
|
|
|
666
|
-
async function streamClusterInForeground(cluster, orchestrator, clusterId, plan) {
|
|
700
|
+
async function streamClusterInForeground(cluster, orchestrator, clusterId, plan, handleSigterm) {
|
|
667
701
|
const sendersWithOutput = new Set();
|
|
668
702
|
const processedMessageIds = new Set();
|
|
669
703
|
|
|
@@ -696,8 +730,34 @@ async function streamClusterInForeground(cluster, orchestrator, clusterId, plan)
|
|
|
696
730
|
sendersWithOutput,
|
|
697
731
|
});
|
|
698
732
|
|
|
699
|
-
await waitForClusterCompletion(orchestrator, clusterId, cleanup);
|
|
700
|
-
|
|
733
|
+
const result = await waitForClusterCompletion(orchestrator, clusterId, cleanup, handleSigterm);
|
|
734
|
+
const label = result.cancelled ? 'cancelled' : 'finished';
|
|
735
|
+
console.log(chalk.dim(`\nCluster ${clusterId} ${label}.`));
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function finishForegroundRun({ cluster, orchestrator, clusterId, plan, resultPath }) {
|
|
740
|
+
try {
|
|
741
|
+
const foreground = await streamClusterInForeground(
|
|
742
|
+
cluster,
|
|
743
|
+
orchestrator,
|
|
744
|
+
clusterId,
|
|
745
|
+
plan,
|
|
746
|
+
Boolean(resultPath)
|
|
747
|
+
);
|
|
748
|
+
if (!resultPath) return;
|
|
749
|
+
const receipt = writeForegroundResult({
|
|
750
|
+
orchestrator,
|
|
751
|
+
cluster,
|
|
752
|
+
clusterId,
|
|
753
|
+
resultPath,
|
|
754
|
+
cancelled: foreground.cancelled,
|
|
755
|
+
});
|
|
756
|
+
process.exitCode = exitCodeForResult(receipt);
|
|
757
|
+
console.log(chalk.dim(`Result ${receipt.outcome} committed to ${resultPath}`));
|
|
758
|
+
} finally {
|
|
759
|
+
orchestrator.close();
|
|
760
|
+
}
|
|
701
761
|
}
|
|
702
762
|
|
|
703
763
|
function setupDaemonCleanup(orchestrator, clusterId) {
|
|
@@ -2662,6 +2722,10 @@ program
|
|
|
2662
2722
|
'fast'
|
|
2663
2723
|
)
|
|
2664
2724
|
.option('-d, --detach', 'Run in background (default: attach to first agent)')
|
|
2725
|
+
.option(
|
|
2726
|
+
'--result-file <path>',
|
|
2727
|
+
'Atomically write a closed foreground result receipt (incompatible with --detach)'
|
|
2728
|
+
)
|
|
2665
2729
|
.addHelpText(
|
|
2666
2730
|
'after',
|
|
2667
2731
|
`
|
|
@@ -2768,6 +2832,7 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
|
|
|
2768
2832
|
}
|
|
2769
2833
|
|
|
2770
2834
|
const { generateName } = require('../src/name-generator');
|
|
2835
|
+
requireForegroundResultMode(effectiveOptions);
|
|
2771
2836
|
if (shouldRunDetached(effectiveOptions)) {
|
|
2772
2837
|
const clusterId = generateName('cluster');
|
|
2773
2838
|
await spawnDetachedCluster(effectiveOptions, effectiveRunPlan, clusterId, stdinText);
|
|
@@ -2811,8 +2876,13 @@ Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Line
|
|
|
2811
2876
|
}
|
|
2812
2877
|
|
|
2813
2878
|
if (!process.env.ZEROSHOT_DAEMON) {
|
|
2814
|
-
await
|
|
2815
|
-
|
|
2879
|
+
await finishForegroundRun({
|
|
2880
|
+
cluster,
|
|
2881
|
+
orchestrator,
|
|
2882
|
+
clusterId,
|
|
2883
|
+
plan: effectiveRunPlan,
|
|
2884
|
+
resultPath: effectiveOptions.resultFile,
|
|
2885
|
+
});
|
|
2816
2886
|
}
|
|
2817
2887
|
setupDaemonCleanup(orchestrator, clusterId);
|
|
2818
2888
|
} catch (error) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-open-engine/zeroshot",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.37.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@the-open-engine/zeroshot",
|
|
9
|
-
"version": "6.
|
|
9
|
+
"version": "6.37.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@ const { executeHook } = require('./agent-hook-executor');
|
|
|
17
17
|
const IsolationManager = require('../isolation-manager');
|
|
18
18
|
const crypto = require('crypto');
|
|
19
19
|
const { bufferMessage, scheduleDrain, drainBufferedMessages } = require('../message-buffer');
|
|
20
|
-
const {
|
|
20
|
+
const { createLivenessPoll } = require('./agent-liveness-poll');
|
|
21
21
|
const { normalizeProviderName, getDefaultProviderId } = require('../../lib/provider-names');
|
|
22
22
|
const { loadSettings } = require('../../lib/settings');
|
|
23
23
|
const { findPlatformMismatchReason } = require('./validation-platform');
|
|
@@ -223,7 +223,7 @@ async function stop(agent) {
|
|
|
223
223
|
stopLivenessCheck(agent);
|
|
224
224
|
|
|
225
225
|
const hasNestedExecutions = agent.nestedExecutions?.hasActive === true;
|
|
226
|
-
if (!agent.running && !agent.currentTask && !hasNestedExecutions) {
|
|
226
|
+
if (!agent.running && !agent.currentTask && !hasNestedExecutions && !agent._currentExecution) {
|
|
227
227
|
return;
|
|
228
228
|
}
|
|
229
229
|
|
|
@@ -242,26 +242,26 @@ async function stop(agent) {
|
|
|
242
242
|
throw new Error(`Task shutdown could not confirm termination: ${termination.reason}`);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// This prevents write-after-close race conditions
|
|
248
|
-
if (agent._currentExecution) {
|
|
245
|
+
const currentExecution = agent._currentExecution;
|
|
246
|
+
if (currentExecution) {
|
|
249
247
|
let executionTimeout = null;
|
|
250
248
|
try {
|
|
251
|
-
await Promise.race([
|
|
252
|
-
|
|
249
|
+
const outcome = await Promise.race([
|
|
250
|
+
Promise.resolve(currentExecution).then(
|
|
251
|
+
() => 'settled',
|
|
252
|
+
() => 'settled'
|
|
253
|
+
),
|
|
253
254
|
new Promise((resolve) => {
|
|
254
|
-
executionTimeout = setTimeout(resolve, 5000);
|
|
255
|
+
executionTimeout = setTimeout(() => resolve('timeout'), 5000);
|
|
255
256
|
}),
|
|
256
257
|
]);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
} finally {
|
|
260
|
-
if (executionTimeout) {
|
|
261
|
-
clearTimeout(executionTimeout);
|
|
258
|
+
if (outcome === 'timeout') {
|
|
259
|
+
throw new Error(`Agent ${agent.id} execution did not settle after task termination`);
|
|
262
260
|
}
|
|
263
|
-
|
|
261
|
+
} finally {
|
|
262
|
+
if (executionTimeout) clearTimeout(executionTimeout);
|
|
264
263
|
}
|
|
264
|
+
if (agent._currentExecution === currentExecution) agent._currentExecution = null;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
agent._log(`Agent ${agent.id} stopped`);
|
|
@@ -1268,67 +1268,16 @@ function startLivenessCheck(agent) {
|
|
|
1268
1268
|
agent.livenessTerminationAttempts = 0;
|
|
1269
1269
|
agent.livenessTerminationRetryAt = 0;
|
|
1270
1270
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
if (agent.livenessTerminationContext) {
|
|
1282
|
-
if (now >= agent.livenessTerminationRetryAt) {
|
|
1283
|
-
attemptLivenessTermination(agent, settings);
|
|
1284
|
-
}
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
const taskStartedAt = agent.taskStartedAt || agent.lastOutputTime || now;
|
|
1289
|
-
const lastOutputTime = agent.lastOutputTime || taskStartedAt;
|
|
1290
|
-
const taskRuntime = now - taskStartedAt;
|
|
1291
|
-
const timeSinceLastOutput = now - lastOutputTime;
|
|
1292
|
-
|
|
1293
|
-
if (configuredTimeout && taskRuntime >= configuredTimeout) {
|
|
1294
|
-
const reason = `Task timed out after ${configuredTimeout}ms`;
|
|
1295
|
-
beginLivenessTermination(agent, settings, reason, 'AGENT_TASK_TIMEOUT', {
|
|
1296
|
-
taskId: agent.currentTaskId,
|
|
1297
|
-
taskRuntime,
|
|
1298
|
-
timeout: configuredTimeout,
|
|
1299
|
-
});
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
if (timeSinceLastOutput < staleDuration) {
|
|
1304
|
-
agent.consecutiveStaleWarnings = 0;
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
agent.consecutiveStaleWarnings += 1;
|
|
1309
|
-
agent._publishLifecycle('AGENT_STALE_WARNING', {
|
|
1310
|
-
taskId: agent.currentTaskId,
|
|
1311
|
-
timeSinceLastOutput,
|
|
1312
|
-
staleDuration,
|
|
1313
|
-
lastOutputTime,
|
|
1314
|
-
consecutiveWarnings: agent.consecutiveStaleWarnings,
|
|
1315
|
-
warningsBeforeKill,
|
|
1316
|
-
processDiagnosticsAvailable: isPlatformSupported(),
|
|
1317
|
-
analysis: `Provider produced no output for ${timeSinceLastOutput}ms`,
|
|
1318
|
-
});
|
|
1319
|
-
|
|
1320
|
-
if (agent.consecutiveStaleWarnings < warningsBeforeKill) {
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
const reason = `Provider produced no output for ${timeSinceLastOutput}ms`;
|
|
1325
|
-
beginLivenessTermination(agent, settings, reason, 'PROVIDER_INACTIVITY_TIMEOUT', {
|
|
1326
|
-
taskId: agent.currentTaskId,
|
|
1327
|
-
timeSinceLastOutput,
|
|
1328
|
-
staleDuration,
|
|
1329
|
-
consecutiveWarnings: agent.consecutiveStaleWarnings,
|
|
1330
|
-
});
|
|
1331
|
-
}, checkIntervalMs);
|
|
1271
|
+
const poll = createLivenessPoll({
|
|
1272
|
+
agent,
|
|
1273
|
+
settings,
|
|
1274
|
+
configuredTimeout,
|
|
1275
|
+
staleDuration,
|
|
1276
|
+
warningsBeforeKill,
|
|
1277
|
+
attemptTermination: attemptLivenessTermination,
|
|
1278
|
+
beginTermination: beginLivenessTermination,
|
|
1279
|
+
});
|
|
1280
|
+
agent.livenessCheckInterval = setInterval(poll, checkIntervalMs);
|
|
1332
1281
|
}
|
|
1333
1282
|
|
|
1334
1283
|
const MAX_LIVENESS_TERMINATION_ATTEMPTS = 3;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const { isPlatformSupported } = require('./agent-stuck-detector');
|
|
2
|
+
|
|
3
|
+
function hasRecoverableTask(agent) {
|
|
4
|
+
return (
|
|
5
|
+
Boolean(agent.currentTask) ||
|
|
6
|
+
Boolean(agent.isolation?.enabled && agent.currentTaskId) ||
|
|
7
|
+
agent.nestedExecutions?.hasActive === true
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function handlePendingTermination(agent, settings, now, attemptTermination) {
|
|
12
|
+
if (!agent.livenessTerminationContext) return false;
|
|
13
|
+
if (now >= agent.livenessTerminationRetryAt) attemptTermination(agent, settings);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function taskTiming(agent, now) {
|
|
18
|
+
const taskStartedAt = agent.taskStartedAt || agent.lastOutputTime || now;
|
|
19
|
+
const lastOutputTime = agent.lastOutputTime || taskStartedAt;
|
|
20
|
+
return {
|
|
21
|
+
taskRuntime: now - taskStartedAt,
|
|
22
|
+
timeSinceLastOutput: now - lastOutputTime,
|
|
23
|
+
lastOutputTime,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function handleTaskTimeout(context, timing) {
|
|
28
|
+
const { agent, settings, configuredTimeout, beginTermination } = context;
|
|
29
|
+
if (!configuredTimeout || timing.taskRuntime < configuredTimeout) return false;
|
|
30
|
+
beginTermination(
|
|
31
|
+
agent,
|
|
32
|
+
settings,
|
|
33
|
+
`Task timed out after ${configuredTimeout}ms`,
|
|
34
|
+
'AGENT_TASK_TIMEOUT',
|
|
35
|
+
{
|
|
36
|
+
taskId: agent.currentTaskId,
|
|
37
|
+
taskRuntime: timing.taskRuntime,
|
|
38
|
+
timeout: configuredTimeout,
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function publishStaleWarning(context, timing) {
|
|
45
|
+
const { agent, staleDuration, warningsBeforeKill } = context;
|
|
46
|
+
agent.consecutiveStaleWarnings += 1;
|
|
47
|
+
agent._publishLifecycle('AGENT_STALE_WARNING', {
|
|
48
|
+
taskId: agent.currentTaskId,
|
|
49
|
+
timeSinceLastOutput: timing.timeSinceLastOutput,
|
|
50
|
+
staleDuration,
|
|
51
|
+
lastOutputTime: timing.lastOutputTime,
|
|
52
|
+
consecutiveWarnings: agent.consecutiveStaleWarnings,
|
|
53
|
+
warningsBeforeKill,
|
|
54
|
+
processDiagnosticsAvailable: isPlatformSupported(),
|
|
55
|
+
analysis: `Provider produced no output for ${timing.timeSinceLastOutput}ms`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function terminateForInactivity(context, timing) {
|
|
60
|
+
const { agent, settings, staleDuration, beginTermination } = context;
|
|
61
|
+
beginTermination(
|
|
62
|
+
agent,
|
|
63
|
+
settings,
|
|
64
|
+
`Provider produced no output for ${timing.timeSinceLastOutput}ms`,
|
|
65
|
+
'PROVIDER_INACTIVITY_TIMEOUT',
|
|
66
|
+
{
|
|
67
|
+
taskId: agent.currentTaskId,
|
|
68
|
+
timeSinceLastOutput: timing.timeSinceLastOutput,
|
|
69
|
+
staleDuration,
|
|
70
|
+
consecutiveWarnings: agent.consecutiveStaleWarnings,
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createLivenessPoll(context) {
|
|
76
|
+
const { agent, settings, staleDuration, warningsBeforeKill, attemptTermination } = context;
|
|
77
|
+
return () => {
|
|
78
|
+
if (!hasRecoverableTask(agent) || agent.livenessTerminationStarted) return;
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
if (handlePendingTermination(agent, settings, now, attemptTermination)) return;
|
|
81
|
+
const timing = taskTiming(agent, now);
|
|
82
|
+
if (handleTaskTimeout(context, timing)) return;
|
|
83
|
+
if (timing.timeSinceLastOutput < staleDuration) {
|
|
84
|
+
agent.consecutiveStaleWarnings = 0;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
publishStaleWarning(context, timing);
|
|
88
|
+
if (agent.consecutiveStaleWarnings < warningsBeforeKill) return;
|
|
89
|
+
terminateForInactivity(context, timing);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { createLivenessPoll };
|
|
@@ -1866,6 +1866,19 @@ function finalizeLogFollow(agent, state) {
|
|
|
1866
1866
|
}
|
|
1867
1867
|
}
|
|
1868
1868
|
|
|
1869
|
+
function finishHostLogCapture(agent, state, pollLogFile, processNewContent, broadcastLine) {
|
|
1870
|
+
// A task status can become terminal before the watcher has flushed its final
|
|
1871
|
+
// filesystem write (observed on Modal). Stop the timers first, then take one
|
|
1872
|
+
// authoritative catch-up read and finish any final UTF-8/non-newline record.
|
|
1873
|
+
finalizeLogFollow(agent, state);
|
|
1874
|
+
pollLogFile();
|
|
1875
|
+
const decoderTail = state.logDecoder.end();
|
|
1876
|
+
if (decoderTail) processNewContent(decoderTail);
|
|
1877
|
+
if (state.lineBuffer.byteLength > 0) {
|
|
1878
|
+
completeLogRecord(state.lineBuffer, broadcastLine, true);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1869
1882
|
function settleHostStatusFailure({ agent, providerName, state, resolve, text, data, error }) {
|
|
1870
1883
|
if (state.resolved) return;
|
|
1871
1884
|
state.resolved = true;
|
|
@@ -1986,6 +1999,7 @@ function handleStatusCompletion({
|
|
|
1986
1999
|
state,
|
|
1987
2000
|
stdout,
|
|
1988
2001
|
pollLogFile,
|
|
2002
|
+
finishLogCapture,
|
|
1989
2003
|
resolve,
|
|
1990
2004
|
reject,
|
|
1991
2005
|
}) {
|
|
@@ -2012,7 +2026,7 @@ function handleStatusCompletion({
|
|
|
2012
2026
|
if (state.resolved) return;
|
|
2013
2027
|
state.resolved = true;
|
|
2014
2028
|
|
|
2015
|
-
|
|
2029
|
+
finishLogCapture();
|
|
2016
2030
|
flushAgentOutput(agent, providerName, state);
|
|
2017
2031
|
|
|
2018
2032
|
buildCompletionResult({
|
|
@@ -2066,14 +2080,12 @@ function createLogFollower({
|
|
|
2066
2080
|
const state = createLogFollowState();
|
|
2067
2081
|
state.skipStructuredResultCheck = skipStructuredResultCheck;
|
|
2068
2082
|
state.nested = nested;
|
|
2069
|
-
|
|
2070
2083
|
state.logFilePath = lookupLogFilePath(ctPath, taskId);
|
|
2071
2084
|
if (state.logFilePath) {
|
|
2072
2085
|
agent._log(`📋 Agent ${agent.id}: Following ct logs for ${taskId}`);
|
|
2073
2086
|
} else {
|
|
2074
2087
|
agent._log(`⏳ Agent ${agent.id}: Waiting for log file...`);
|
|
2075
2088
|
}
|
|
2076
|
-
|
|
2077
2089
|
const broadcastLine = (line) => broadcastAgentLine({ agent, providerName, state, line });
|
|
2078
2090
|
const processNewContent = (content) => appendContentToBuffer(state, content, broadcastLine);
|
|
2079
2091
|
const pollLogFile = () =>
|
|
@@ -2085,6 +2097,8 @@ function createLogFollower({
|
|
|
2085
2097
|
state,
|
|
2086
2098
|
onNewContent: processNewContent,
|
|
2087
2099
|
});
|
|
2100
|
+
const finishLogCapture = () =>
|
|
2101
|
+
finishHostLogCapture(agent, state, pollLogFile, processNewContent, broadcastLine);
|
|
2088
2102
|
|
|
2089
2103
|
state.pollInterval = setInterval(pollLogFile, 300);
|
|
2090
2104
|
|
|
@@ -2120,6 +2134,7 @@ function createLogFollower({
|
|
|
2120
2134
|
state,
|
|
2121
2135
|
stdout,
|
|
2122
2136
|
pollLogFile,
|
|
2137
|
+
finishLogCapture,
|
|
2123
2138
|
resolve,
|
|
2124
2139
|
reject,
|
|
2125
2140
|
});
|
|
@@ -3546,6 +3561,7 @@ module.exports = {
|
|
|
3546
3561
|
consumeIsolatedTailChunk,
|
|
3547
3562
|
flushAgentOutput,
|
|
3548
3563
|
flushIsolatedOutput,
|
|
3564
|
+
finishHostLogCapture,
|
|
3549
3565
|
CONTROL_PLANE_OUTPUT_LIMITS: Object.freeze({
|
|
3550
3566
|
maxBytes: MAX_CONTROL_PLANE_OUTPUT_BYTES,
|
|
3551
3567
|
maxRecords: MAX_CONTROL_PLANE_OUTPUT_RECORDS,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { buildTelemetry } = require('./foreground-benchmark-result');
|
|
6
|
+
|
|
7
|
+
function serialized(value) {
|
|
8
|
+
return Buffer.from(`${JSON.stringify(value)}\n`, 'utf8');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function atomicWriteNew(targetPath, content) {
|
|
12
|
+
const directory = path.dirname(targetPath);
|
|
13
|
+
const temporary = path.join(
|
|
14
|
+
directory,
|
|
15
|
+
`.${path.basename(targetPath)}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`
|
|
16
|
+
);
|
|
17
|
+
let descriptor;
|
|
18
|
+
let published = false;
|
|
19
|
+
try {
|
|
20
|
+
descriptor = fs.openSync(temporary, 'wx', 0o600);
|
|
21
|
+
fs.writeFileSync(descriptor, content);
|
|
22
|
+
fs.fsyncSync(descriptor);
|
|
23
|
+
fs.closeSync(descriptor);
|
|
24
|
+
descriptor = undefined;
|
|
25
|
+
fs.linkSync(temporary, targetPath);
|
|
26
|
+
published = true;
|
|
27
|
+
fs.unlinkSync(temporary);
|
|
28
|
+
const directoryDescriptor = fs.openSync(directory, fs.constants.O_RDONLY);
|
|
29
|
+
try {
|
|
30
|
+
fs.fsyncSync(directoryDescriptor);
|
|
31
|
+
} finally {
|
|
32
|
+
fs.closeSync(directoryDescriptor);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
error.atomicTargetPublished = published;
|
|
36
|
+
throw error;
|
|
37
|
+
} finally {
|
|
38
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
39
|
+
try {
|
|
40
|
+
fs.unlinkSync(temporary);
|
|
41
|
+
} catch {
|
|
42
|
+
// Preserve the primary write error; a leftover randomized temp is non-authoritative.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeOrphanTelemetry(telemetryPath, primaryError) {
|
|
48
|
+
try {
|
|
49
|
+
fs.unlinkSync(telemetryPath);
|
|
50
|
+
} catch (cleanupError) {
|
|
51
|
+
if (cleanupError.code !== 'ENOENT') primaryError.cleanupError = cleanupError;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeBenchmarkResultBundle(resultPath, result, snapshot) {
|
|
56
|
+
if (typeof resultPath !== 'string' || resultPath.length === 0) {
|
|
57
|
+
throw new Error('result path must be non-empty text');
|
|
58
|
+
}
|
|
59
|
+
const resolvedResult = path.resolve(resultPath);
|
|
60
|
+
const telemetryPath = `${resolvedResult}.telemetry.json`;
|
|
61
|
+
const telemetry = buildTelemetry(result.runId, snapshot);
|
|
62
|
+
const telemetryBytes = serialized(telemetry);
|
|
63
|
+
atomicWriteNew(telemetryPath, telemetryBytes);
|
|
64
|
+
const receipt = {
|
|
65
|
+
...result,
|
|
66
|
+
telemetry: {
|
|
67
|
+
artifact: path.basename(telemetryPath),
|
|
68
|
+
byteLength: telemetryBytes.length,
|
|
69
|
+
sha256: crypto.createHash('sha256').update(telemetryBytes).digest('hex'),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
atomicWriteNew(resolvedResult, serialized(receipt));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!error.atomicTargetPublished) removeOrphanTelemetry(telemetryPath, error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
return receipt;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { writeBenchmarkResultBundle };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const { VALID_PROVIDERS } = require('../lib/provider-names');
|
|
3
|
+
|
|
4
|
+
const RESULT_SCHEMA = 'zeroshot-benchmark-result/v1';
|
|
5
|
+
const TELEMETRY_SCHEMA = 'zeroshot-benchmark-telemetry/v1';
|
|
6
|
+
const EMPTY_DIAGNOSTIC = Object.freeze({
|
|
7
|
+
byteLength: 0,
|
|
8
|
+
sha256: crypto.createHash('sha256').update('').digest('hex'),
|
|
9
|
+
});
|
|
10
|
+
const TASK_FAILURE_REASONS = new Set(['max_iterations', 'structured_output_invalid']);
|
|
11
|
+
const PROVIDER_CODES = new Set(['crash', 'refusal']);
|
|
12
|
+
const PROVIDERS = new Set(VALID_PROVIDERS);
|
|
13
|
+
const PROVIDER_EVENTS = new Set(['terminal_error', 'turn.failed']);
|
|
14
|
+
const PROVIDER_CATEGORIES = new Set([
|
|
15
|
+
'authentication',
|
|
16
|
+
'permanent',
|
|
17
|
+
'quota',
|
|
18
|
+
'transient',
|
|
19
|
+
'unknown',
|
|
20
|
+
]);
|
|
21
|
+
const PROVIDER_KINDS = new Set([
|
|
22
|
+
'permanent-pattern',
|
|
23
|
+
'rate-limit',
|
|
24
|
+
'retryable-pattern',
|
|
25
|
+
'status-permanent',
|
|
26
|
+
'status-retryable',
|
|
27
|
+
'code-retryable',
|
|
28
|
+
'unknown-retryable',
|
|
29
|
+
]);
|
|
30
|
+
const TOKEN_FIELDS = [
|
|
31
|
+
'inputTokens',
|
|
32
|
+
'outputTokens',
|
|
33
|
+
'cacheReadInputTokens',
|
|
34
|
+
'cacheCreationInputTokens',
|
|
35
|
+
'totalCostUsd',
|
|
36
|
+
'count',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function requireObject(value, label) {
|
|
40
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
41
|
+
throw new Error(`${label} must be an object`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireClosedText(value, allowed, label) {
|
|
47
|
+
if (typeof value !== 'string' || !allowed.has(value)) {
|
|
48
|
+
throw new Error(`${label} is outside the closed result contract`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requireDiagnostic(value) {
|
|
54
|
+
if (
|
|
55
|
+
value &&
|
|
56
|
+
Number.isSafeInteger(value.byteLength) &&
|
|
57
|
+
value.byteLength >= 0 &&
|
|
58
|
+
typeof value.sha256 === 'string' &&
|
|
59
|
+
/^[a-f0-9]{64}$/.test(value.sha256)
|
|
60
|
+
) {
|
|
61
|
+
return { byteLength: value.byteLength, sha256: value.sha256 };
|
|
62
|
+
}
|
|
63
|
+
throw new Error('provider diagnostic is outside the closed result contract');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function terminalData(message) {
|
|
67
|
+
return requireObject(requireObject(message.content, 'terminal content').data, 'terminal data');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isExplicitTaskFailure(message, data) {
|
|
71
|
+
if (message.sender === 'orchestrator' || !TASK_FAILURE_REASONS.has(data.reason)) return false;
|
|
72
|
+
if (data.reason === 'max_iterations') return message.receiver === 'system';
|
|
73
|
+
return message.receiver === 'broadcast' && data.code === 'STRUCTURED_OUTPUT_INVALID';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function classifyTerminal(message) {
|
|
77
|
+
if (message.topic === 'CLUSTER_COMPLETE') {
|
|
78
|
+
return {
|
|
79
|
+
outcome: 'completed',
|
|
80
|
+
terminalOwner: 'task',
|
|
81
|
+
code: 'ok',
|
|
82
|
+
kind: 'workflow_complete',
|
|
83
|
+
retryable: false,
|
|
84
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
85
|
+
provider: null,
|
|
86
|
+
event: null,
|
|
87
|
+
category: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (message.topic !== 'CLUSTER_FAILED') {
|
|
92
|
+
throw new Error(`unsupported terminal topic: ${message.topic}`);
|
|
93
|
+
}
|
|
94
|
+
const data = terminalData(message);
|
|
95
|
+
if (isExplicitTaskFailure(message, data)) {
|
|
96
|
+
return {
|
|
97
|
+
outcome: 'task_failure',
|
|
98
|
+
terminalOwner: 'task',
|
|
99
|
+
code: data.reason,
|
|
100
|
+
kind: 'declared_failure',
|
|
101
|
+
retryable: false,
|
|
102
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
103
|
+
provider: null,
|
|
104
|
+
event: null,
|
|
105
|
+
category: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (data.reason === 'provider_execution_failed') {
|
|
109
|
+
if (typeof data.retryable !== 'boolean') {
|
|
110
|
+
throw new Error('provider retryable must be boolean');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
outcome: 'provider_failure',
|
|
114
|
+
terminalOwner: 'provider',
|
|
115
|
+
code: requireClosedText(data.code, PROVIDER_CODES, 'provider code'),
|
|
116
|
+
kind: requireClosedText(data.kind, PROVIDER_KINDS, 'provider kind'),
|
|
117
|
+
retryable: data.retryable,
|
|
118
|
+
diagnostic: requireDiagnostic(data.diagnostic),
|
|
119
|
+
provider: requireClosedText(data.provider, PROVIDERS, 'provider'),
|
|
120
|
+
event: requireClosedText(data.event, PROVIDER_EVENTS, 'provider event'),
|
|
121
|
+
category: requireClosedText(data.category, PROVIDER_CATEGORIES, 'provider category'),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
outcome: 'engine_failure',
|
|
126
|
+
terminalOwner: 'engine',
|
|
127
|
+
code: 'engine_failed',
|
|
128
|
+
kind: 'declared_failure',
|
|
129
|
+
retryable: false,
|
|
130
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
131
|
+
provider: null,
|
|
132
|
+
event: null,
|
|
133
|
+
category: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateStoppedAgents(agents) {
|
|
138
|
+
if (!Array.isArray(agents)) throw new Error('agents must be an array');
|
|
139
|
+
for (const agent of agents) {
|
|
140
|
+
const state = requireObject(agent, 'agent state');
|
|
141
|
+
if (state.pid !== null && state.pid !== undefined) {
|
|
142
|
+
throw new Error(`agent ${String(state.id)} still has a live process identity`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateRunId(runId) {
|
|
148
|
+
const parts = typeof runId === 'string' ? runId.split('-') : [];
|
|
149
|
+
const valid =
|
|
150
|
+
parts.length >= 2 &&
|
|
151
|
+
parts.every(
|
|
152
|
+
(part) => part.length > 0 && [...part].every((character) => /[a-z0-9]/.test(character))
|
|
153
|
+
);
|
|
154
|
+
if (!valid) {
|
|
155
|
+
throw new Error('runId must be a canonical cluster id');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildBenchmarkResult({ runId, terminalMessages, agents }) {
|
|
160
|
+
validateRunId(runId);
|
|
161
|
+
if (!Array.isArray(terminalMessages) || terminalMessages.length !== 1) {
|
|
162
|
+
throw new Error('foreground run must have exactly one terminal event');
|
|
163
|
+
}
|
|
164
|
+
validateStoppedAgents(agents);
|
|
165
|
+
return {
|
|
166
|
+
schema: RESULT_SCHEMA,
|
|
167
|
+
runId,
|
|
168
|
+
...classifyTerminal(terminalMessages[0]),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildCancelledResult({ runId, agents }) {
|
|
173
|
+
validateRunId(runId);
|
|
174
|
+
validateStoppedAgents(agents);
|
|
175
|
+
return {
|
|
176
|
+
schema: RESULT_SCHEMA,
|
|
177
|
+
runId,
|
|
178
|
+
outcome: 'cancelled',
|
|
179
|
+
terminalOwner: 'controller',
|
|
180
|
+
code: 'cancelled',
|
|
181
|
+
kind: 'controlled_cancellation',
|
|
182
|
+
retryable: false,
|
|
183
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
184
|
+
provider: null,
|
|
185
|
+
event: null,
|
|
186
|
+
category: null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeTokenEntry(value, label) {
|
|
191
|
+
const source = requireObject(value, label);
|
|
192
|
+
const entry = {};
|
|
193
|
+
for (const field of TOKEN_FIELDS) {
|
|
194
|
+
const amount = source[field] ?? 0;
|
|
195
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0) {
|
|
196
|
+
throw new Error(`${label}.${field} must be a finite non-negative number`);
|
|
197
|
+
}
|
|
198
|
+
entry[field] = amount;
|
|
199
|
+
}
|
|
200
|
+
return entry;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildTelemetry(runId, snapshot) {
|
|
204
|
+
validateRunId(runId);
|
|
205
|
+
const source = requireObject(snapshot, 'telemetry snapshot');
|
|
206
|
+
if (!Number.isSafeInteger(source.messageCount) || source.messageCount < 0) {
|
|
207
|
+
throw new Error('telemetry messageCount must be a non-negative safe integer');
|
|
208
|
+
}
|
|
209
|
+
const roles = requireObject(source.tokensByRole, 'tokensByRole');
|
|
210
|
+
const names = Object.keys(roles).sort();
|
|
211
|
+
if (names.length > 64) throw new Error('telemetry role count exceeds 64');
|
|
212
|
+
const tokensByRole = {};
|
|
213
|
+
for (const name of names) {
|
|
214
|
+
const validRoleName =
|
|
215
|
+
name === '_total' ||
|
|
216
|
+
(name.length <= 128 &&
|
|
217
|
+
/^[a-zA-Z0-9]$/.test(name[0] || '') &&
|
|
218
|
+
[...name].every((character) => /[a-zA-Z0-9_.-]/.test(character)));
|
|
219
|
+
if (!validRoleName) {
|
|
220
|
+
throw new Error('telemetry role name is invalid');
|
|
221
|
+
}
|
|
222
|
+
tokensByRole[name] = normalizeTokenEntry(roles[name], `tokensByRole.${name}`);
|
|
223
|
+
}
|
|
224
|
+
return { schema: TELEMETRY_SCHEMA, runId, messageCount: source.messageCount, tokensByRole };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
RESULT_SCHEMA,
|
|
229
|
+
TELEMETRY_SCHEMA,
|
|
230
|
+
buildBenchmarkResult,
|
|
231
|
+
buildCancelledResult,
|
|
232
|
+
buildTelemetry,
|
|
233
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { buildBenchmarkResult, buildCancelledResult } = require('./foreground-benchmark-result');
|
|
2
|
+
const { writeBenchmarkResultBundle } = require('./foreground-benchmark-files');
|
|
3
|
+
|
|
4
|
+
const TERMINAL_TOPICS = ['CLUSTER_COMPLETE', 'CLUSTER_FAILED'];
|
|
5
|
+
const VERIFIER_ELIGIBLE = new Set(['completed', 'task_failure']);
|
|
6
|
+
const EXIT_CODES = Object.freeze({
|
|
7
|
+
provider_failure: 20,
|
|
8
|
+
engine_failure: 21,
|
|
9
|
+
cancelled: 22,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function isForegroundStatusSettled(status) {
|
|
13
|
+
return (
|
|
14
|
+
status &&
|
|
15
|
+
['stopped', 'killed'].includes(status.state) &&
|
|
16
|
+
status.isZombie === false &&
|
|
17
|
+
Array.isArray(status.agents) &&
|
|
18
|
+
status.agents.every((agent) => agent && (agent.pid === null || agent.pid === undefined))
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function terminalMessages(cluster, clusterId) {
|
|
23
|
+
const messages = TERMINAL_TOPICS.flatMap((topic) =>
|
|
24
|
+
cluster.messageBus.query({ cluster_id: clusterId, topic })
|
|
25
|
+
);
|
|
26
|
+
return messages.sort((left, right) => {
|
|
27
|
+
const a = BigInt(left.sequence);
|
|
28
|
+
const b = BigInt(right.sequence);
|
|
29
|
+
if (a < b) return -1;
|
|
30
|
+
if (a > b) return 1;
|
|
31
|
+
return 0;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requireSettledStatus(orchestrator, clusterId) {
|
|
36
|
+
const status = orchestrator.getStatus(clusterId);
|
|
37
|
+
if (!isForegroundStatusSettled(status)) {
|
|
38
|
+
throw new Error(`foreground cluster is not settled: ${String(status?.state || 'unavailable')}`);
|
|
39
|
+
}
|
|
40
|
+
return status;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeForegroundResult({ orchestrator, cluster, clusterId, resultPath, cancelled }) {
|
|
44
|
+
const status = requireSettledStatus(orchestrator, clusterId);
|
|
45
|
+
const terminals = terminalMessages(cluster, clusterId);
|
|
46
|
+
let result;
|
|
47
|
+
if (cancelled && terminals.length === 0) {
|
|
48
|
+
result = buildCancelledResult({ runId: clusterId, agents: status.agents });
|
|
49
|
+
} else {
|
|
50
|
+
result = buildBenchmarkResult({
|
|
51
|
+
runId: clusterId,
|
|
52
|
+
terminalMessages: terminals,
|
|
53
|
+
agents: status.agents,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const snapshot = cluster.messageBus.readSnapshot(clusterId);
|
|
57
|
+
return writeBenchmarkResultBundle(resultPath, result, snapshot);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function exitCodeForResult(result) {
|
|
61
|
+
if (VERIFIER_ELIGIBLE.has(result.outcome)) return 0;
|
|
62
|
+
const exitCode = EXIT_CODES[result.outcome];
|
|
63
|
+
if (exitCode === undefined) throw new Error(`unsupported result outcome: ${result.outcome}`);
|
|
64
|
+
return exitCode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
exitCodeForResult,
|
|
69
|
+
isForegroundStatusSettled,
|
|
70
|
+
terminalMessages,
|
|
71
|
+
writeForegroundResult,
|
|
72
|
+
};
|
package/task-lib/runner.js
CHANGED
|
@@ -625,6 +625,13 @@ export function shouldUseAttachableWatcher(options, providerName) {
|
|
|
625
625
|
return false;
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
+
// Benchmark runs are non-interactive and keep the cluster process in the foreground. Use the
|
|
629
|
+
// pipe watcher so task completion is observed only after stdout/stderr close; a PTY exit can
|
|
630
|
+
// race its final buffered output on remote runtimes and lose the terminal structured result.
|
|
631
|
+
if (resolveTaskExecutionContext() === 'benchmark') {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
|
|
628
635
|
// The rpc-stdio lane owns bidirectional correlated RPC over stdio itself (see
|
|
629
636
|
// omp-rpc-driver.ts) and always uses rpc-watcher.js instead of the attachable PTY watcher.
|
|
630
637
|
if (getProviderRegistryEntry(providerName).invoke.lane === 'rpc-stdio') {
|