@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 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 setupForegroundSigintHandler({ orchestrator, clusterId, cleanup, stopChecking }) {
609
- const handler = async () => {
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
- stopChecking();
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
- console.error(chalk.red(`Failed to stop cluster: ${stopErr.message}`));
638
+ settle(null, stopErr);
620
639
  }
621
-
622
- process.exit(0);
623
640
  };
624
641
 
625
- process.on('SIGINT', handler);
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', handler);
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 removeSigint = setupForegroundSigintHandler({
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
- stopChecking,
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.state !== 'running') {
657
- finish(cleanup.stopWithFlush);
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
- finish(cleanup.stop);
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
- console.log(chalk.dim(`\nCluster ${clusterId} completed.`));
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 streamClusterInForeground(cluster, orchestrator, clusterId, effectiveRunPlan);
2815
- orchestrator.close();
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) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.36.0",
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.36.0",
9
+ "version": "6.37.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.36.0",
3
+ "version": "6.37.0",
4
4
  "description": "Independent executor–verifier orchestration for software changes.",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -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 { isPlatformSupported } = require('./agent-stuck-detector');
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
- // Wait for in-flight execution to complete (up to 5 seconds)
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
- agent._currentExecution,
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
- } catch {
258
- // Ignore errors from cancelled execution
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
- agent._currentExecution = null;
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
- agent.livenessCheckInterval = setInterval(() => {
1272
- const hasRecoverableTask =
1273
- Boolean(agent.currentTask) ||
1274
- Boolean(agent.isolation?.enabled && agent.currentTaskId) ||
1275
- agent.nestedExecutions?.hasActive === true;
1276
- if (!hasRecoverableTask || agent.livenessTerminationStarted) {
1277
- return;
1278
- }
1279
-
1280
- const now = Date.now();
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
- finalizeLogFollow(agent, state);
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
+ };
@@ -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') {