@the-open-engine/zeroshot 6.36.0 → 6.38.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.
Files changed (81) hide show
  1. package/cli/index.js +99 -29
  2. package/cli/lib/setup-scanner-worker.js +2 -9
  3. package/lib/agent-cli-provider/adapters/claude-recovery.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/claude-recovery.js +17 -3
  5. package/lib/agent-cli-provider/adapters/claude-recovery.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  7. package/lib/agent-cli-provider/adapters/index.js +7 -0
  8. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  9. package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/adapters/pi.js +86 -97
  11. package/lib/agent-cli-provider/adapters/pi.js.map +1 -1
  12. package/lib/agent-cli-provider/contract-parse.d.ts.map +1 -1
  13. package/lib/agent-cli-provider/contract-parse.js +5 -1
  14. package/lib/agent-cli-provider/contract-parse.js.map +1 -1
  15. package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/invoke-evidence.js +8 -1
  17. package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
  18. package/lib/agent-cli-provider/pi/credentials.d.ts +2 -0
  19. package/lib/agent-cli-provider/pi/credentials.d.ts.map +1 -0
  20. package/lib/agent-cli-provider/pi/credentials.js +51 -0
  21. package/lib/agent-cli-provider/pi/credentials.js.map +1 -0
  22. package/lib/agent-cli-provider/pi/events.d.ts +5 -0
  23. package/lib/agent-cli-provider/pi/events.d.ts.map +1 -0
  24. package/lib/agent-cli-provider/pi/events.js +124 -0
  25. package/lib/agent-cli-provider/pi/events.js.map +1 -0
  26. package/lib/agent-cli-provider/pi/protocol-state.d.ts +6 -0
  27. package/lib/agent-cli-provider/pi/protocol-state.d.ts.map +1 -0
  28. package/lib/agent-cli-provider/pi/protocol-state.js +138 -0
  29. package/lib/agent-cli-provider/pi/protocol-state.js.map +1 -0
  30. package/lib/agent-cli-provider/pi/release.d.ts +4 -0
  31. package/lib/agent-cli-provider/pi/release.d.ts.map +1 -0
  32. package/lib/agent-cli-provider/pi/release.js +7 -0
  33. package/lib/agent-cli-provider/pi/release.js.map +1 -0
  34. package/lib/agent-cli-provider/probe-availability.d.ts +5 -0
  35. package/lib/agent-cli-provider/probe-availability.d.ts.map +1 -0
  36. package/lib/agent-cli-provider/probe-availability.js +11 -0
  37. package/lib/agent-cli-provider/probe-availability.js.map +1 -0
  38. package/lib/agent-cli-provider/provider-registry.d.ts +13 -9
  39. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  40. package/lib/agent-cli-provider/provider-registry.js +42 -8
  41. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  42. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  43. package/lib/agent-cli-provider/single-agent-runtime.js +15 -5
  44. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  45. package/lib/agent-cli-provider/types.d.ts +3 -0
  46. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  47. package/lib/agent-cli-provider/types.js.map +1 -1
  48. package/lib/cluster-worker/engine-adapter.js +9 -1
  49. package/lib/docker-config.js +6 -1
  50. package/lib/provider-credential-path.js +36 -0
  51. package/lib/start-cluster.js +8 -0
  52. package/npm-shrinkwrap.json +2 -2
  53. package/package.json +1 -1
  54. package/src/agent/agent-lifecycle.js +25 -76
  55. package/src/agent/agent-liveness-poll.js +93 -0
  56. package/src/agent/agent-task-executor.js +98 -23
  57. package/src/agent/output-extraction.js +115 -0
  58. package/src/agent/pi-terminal-lifecycle.js +242 -0
  59. package/src/agent/provider-control-plane.js +26 -0
  60. package/src/agent/provider-terminal-failure.js +49 -24
  61. package/src/agent-cli-provider/adapters/claude-recovery.ts +15 -2
  62. package/src/agent-cli-provider/adapters/index.ts +6 -0
  63. package/src/agent-cli-provider/adapters/pi.ts +105 -125
  64. package/src/agent-cli-provider/contract-parse.ts +8 -1
  65. package/src/agent-cli-provider/invoke-evidence.ts +11 -1
  66. package/src/agent-cli-provider/pi/credentials.ts +47 -0
  67. package/src/agent-cli-provider/pi/events.ts +148 -0
  68. package/src/agent-cli-provider/pi/protocol-state.ts +163 -0
  69. package/src/agent-cli-provider/pi/release.ts +4 -0
  70. package/src/agent-cli-provider/probe-availability.ts +19 -0
  71. package/src/agent-cli-provider/provider-registry.ts +48 -9
  72. package/src/agent-cli-provider/single-agent-runtime.ts +27 -6
  73. package/src/agent-cli-provider/types.ts +3 -0
  74. package/src/config-validator.js +1 -3
  75. package/src/foreground-benchmark-files.js +81 -0
  76. package/src/foreground-benchmark-result.js +233 -0
  77. package/src/foreground-benchmark-run.js +72 -0
  78. package/src/isolation-manager.js +43 -29
  79. package/src/orchestrator.js +33 -1
  80. package/task-lib/runner.js +7 -0
  81. package/task-lib/watcher-output-runtime.js +16 -5
@@ -0,0 +1,163 @@
1
+ import { parseTurnEndResult } from '../assistant-stream';
2
+ import { getNumber, getRecord, getString } from '../json';
3
+ import type { ProviderParserState, ResultEvent } from '../types';
4
+
5
+ const STOP_REASONS = new Set(['stop', 'length', 'toolUse', 'error', 'aborted', 'deferred']);
6
+ const REQUIRED_USAGE_FIELDS = [
7
+ 'input',
8
+ 'output',
9
+ 'cacheRead',
10
+ 'cacheWrite',
11
+ 'totalTokens',
12
+ ] as const;
13
+ const OPTIONAL_USAGE_FIELDS = ['cacheWrite1h', 'reasoning'] as const;
14
+ const COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'total'] as const;
15
+
16
+ interface PiProtocolState {
17
+ latestAssistant?: Record<string, unknown>;
18
+ invalid?: string;
19
+ usage: Record<string, number>;
20
+ cost: Record<string, number>;
21
+ }
22
+
23
+ const protocolStates = new WeakMap<ProviderParserState, PiProtocolState>();
24
+
25
+ function protocolState(state: ProviderParserState): PiProtocolState {
26
+ const current = protocolStates.get(state);
27
+ if (current !== undefined) return current;
28
+ const created = { usage: {}, cost: {} };
29
+ protocolStates.set(state, created);
30
+ return created;
31
+ }
32
+
33
+ export function invalidatePiStream(state: ProviderParserState, error: string): void {
34
+ protocolState(state).invalid = error;
35
+ }
36
+
37
+ function validateUsage(usage: Record<string, unknown>, label: string): string | null {
38
+ for (const field of REQUIRED_USAGE_FIELDS) {
39
+ if (getNumber(usage, field) === null) return `${label}.${field} is missing.`;
40
+ }
41
+ for (const field of OPTIONAL_USAGE_FIELDS) {
42
+ if (usage[field] !== undefined && getNumber(usage, field) === null) {
43
+ return `${label}.${field} is invalid.`;
44
+ }
45
+ }
46
+ const cost = getRecord(usage, 'cost');
47
+ if (cost === null) return `${label}.cost is missing.`;
48
+ for (const field of COST_FIELDS) {
49
+ if (getNumber(cost, field) === null) return `${label}.cost.${field} is missing.`;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function validateAssistant(message: Record<string, unknown>): string | null {
55
+ if (!Array.isArray(message.content)) return 'Pi assistant message content is missing.';
56
+ const stopReason = getString(message, 'stopReason');
57
+ if (stopReason === null || !STOP_REASONS.has(stopReason)) {
58
+ return 'Pi assistant message stopReason is missing or unsupported.';
59
+ }
60
+ const usage = getRecord(message, 'usage');
61
+ return usage === null
62
+ ? 'Pi assistant message usage is missing.'
63
+ : validateUsage(usage, 'Pi assistant usage');
64
+ }
65
+
66
+ function addNumbers(
67
+ target: Record<string, number>,
68
+ source: Record<string, unknown>,
69
+ fields: readonly string[]
70
+ ): void {
71
+ for (const field of fields) {
72
+ const value = getNumber(source, field);
73
+ if (value !== null) target[field] = (target[field] ?? 0) + value;
74
+ }
75
+ }
76
+
77
+ export function rememberPiAssistant(
78
+ message: Record<string, unknown> | null,
79
+ state: ProviderParserState
80
+ ): void {
81
+ if (message === null) return invalidatePiStream(state, 'Pi message_end was missing its message.');
82
+ const role = getString(message, 'role');
83
+ if (role === null) return invalidatePiStream(state, 'Pi message_end was missing its role.');
84
+ if (role !== 'assistant') return;
85
+ const error = validateAssistant(message);
86
+ if (error !== null) return invalidatePiStream(state, error);
87
+ const current = protocolState(state);
88
+ current.latestAssistant = message;
89
+ rememberPiUsage(getRecord(message, 'usage'), state, 'Pi assistant usage');
90
+ }
91
+
92
+ export function rememberPiUsage(
93
+ usage: Record<string, unknown> | null,
94
+ state: ProviderParserState,
95
+ label: string
96
+ ): void {
97
+ if (usage === null) return invalidatePiStream(state, `${label} is invalid.`);
98
+ const error = validateUsage(usage, label);
99
+ if (error !== null) return invalidatePiStream(state, error);
100
+ const current = protocolState(state);
101
+ addNumbers(current.usage, usage, [...REQUIRED_USAGE_FIELDS, ...OPTIONAL_USAGE_FIELDS]);
102
+ const cost = getRecord(usage, 'cost');
103
+ if (cost !== null) addNumbers(current.cost, cost, COST_FIELDS);
104
+ }
105
+
106
+ function usageResult(
107
+ current: PiProtocolState
108
+ ): Pick<
109
+ ResultEvent,
110
+ | 'inputTokens'
111
+ | 'outputTokens'
112
+ | 'cacheReadInputTokens'
113
+ | 'cacheCreationInputTokens'
114
+ | 'cost'
115
+ | 'modelUsage'
116
+ > {
117
+ const cost = Object.keys(current.cost).length === 0 ? null : current.cost;
118
+ return {
119
+ inputTokens: current.usage.input ?? 0,
120
+ outputTokens: current.usage.output ?? 0,
121
+ cacheReadInputTokens: current.usage.cacheRead ?? 0,
122
+ cacheCreationInputTokens: current.usage.cacheWrite ?? 0,
123
+ cost: cost?.total ?? null,
124
+ modelUsage: { ...current.usage, ...(cost === null ? {} : { cost }) },
125
+ };
126
+ }
127
+
128
+ function failedResult(error: string, current: PiProtocolState): ResultEvent {
129
+ return {
130
+ type: 'result',
131
+ success: false,
132
+ result: null,
133
+ error,
134
+ ...usageResult(current),
135
+ };
136
+ }
137
+
138
+ export function settlePiStream(state: ProviderParserState): ResultEvent {
139
+ const current = protocolState(state);
140
+ if (current.invalid) {
141
+ return failedResult(`Invalid Pi JSON stream: ${current.invalid}`, current);
142
+ }
143
+ if (!current.latestAssistant) {
144
+ return failedResult('Pi settled without an assistant message_end.', current);
145
+ }
146
+ const parsedResult = parseTurnEndResult(
147
+ { message: current.latestAssistant },
148
+ { failureMessage: 'Pi turn failed' }
149
+ );
150
+ const result =
151
+ getString(current.latestAssistant, 'stopReason') === 'deferred'
152
+ ? {
153
+ ...parsedResult,
154
+ success: false,
155
+ result: null,
156
+ error: 'Pi turn deferred without completing.',
157
+ }
158
+ : parsedResult;
159
+ return {
160
+ ...result,
161
+ ...usageResult(current),
162
+ };
163
+ }
@@ -0,0 +1,4 @@
1
+ export const PI_SUPPORTED_VERSION = '0.84.1' as const;
2
+ export const PI_PACKAGE_NAME = '@earendil-works/pi-coding-agent' as const;
3
+ export const PI_INSTALL_COMMAND =
4
+ `npm install -g --ignore-scripts ${PI_PACKAGE_NAME}@${PI_SUPPORTED_VERSION}` as const;
@@ -0,0 +1,19 @@
1
+ import type { ProviderCliFeatures } from './types';
2
+
3
+ type AvailabilityProbe = 'command' | 'help-or-version' | 'supported-version' | undefined;
4
+
5
+ export function runtimeProbeIsAvailable(
6
+ availabilityProbe: AvailabilityProbe,
7
+ evidenceAvailable: boolean | undefined,
8
+ helpText: string,
9
+ versionText: string,
10
+ capabilities: ProviderCliFeatures
11
+ ): boolean {
12
+ const probeAvailable =
13
+ evidenceAvailable ??
14
+ (availabilityProbe === 'command' ? true : Boolean(helpText || versionText));
15
+ const versionSupported =
16
+ availabilityProbe !== 'supported-version' ||
17
+ ('versionMatches' in capabilities && capabilities.versionMatches === true);
18
+ return probeAvailable && versionSupported;
19
+ }
@@ -18,6 +18,7 @@ import {
18
18
  OMP_INSTALL_COMMAND,
19
19
  } from './omp/release';
20
20
  import { OMP_SDK_SETTINGS_DEFAULTS, validateOmpSdkSettings } from './omp/sdk-settings';
21
+ import { PI_INSTALL_COMMAND } from './pi/release';
21
22
  import type { OmpSettingsValidationContext } from './omp/sdk-settings';
22
23
  import type { ModelLevel, ProviderAdapter, StructuredOutputRecoveryAdapter } from './types';
23
24
 
@@ -73,6 +74,9 @@ export interface ProviderDocsMetadata {
73
74
 
74
75
  export interface ProviderDockerMountPreset {
75
76
  readonly host: string;
77
+ // Optional host-only config-root override. Its value selects the mount source but is never
78
+ // forwarded into the container, where `container` remains the canonical provider path.
79
+ readonly hostEnv?: string;
76
80
  readonly container: string;
77
81
  readonly readonly: boolean;
78
82
  }
@@ -130,7 +134,7 @@ interface ProviderRegistryEntryBase {
130
134
  settings: Record<string, unknown>,
131
135
  context?: OmpSettingsValidationContext
132
136
  ) => string | null;
133
- readonly availabilityProbe?: 'command' | 'help-or-version';
137
+ readonly availabilityProbe?: 'command' | 'help-or-version' | 'supported-version';
134
138
  readonly docs: ProviderDocsMetadata;
135
139
  readonly docker: ProviderDockerMetadata;
136
140
  readonly defaultLevels: Readonly<{
@@ -186,6 +190,35 @@ const CLAUDE_DOCKER_ENV_PASSTHROUGH = [
186
190
  'CLAUDE_CODE_USE_BEDROCK',
187
191
  ] as const;
188
192
 
193
+ const PI_EXPLICIT_DOCKER_CREDENTIALS = new Set([
194
+ 'AWS_CONTAINER_CREDENTIALS_FULL_URI',
195
+ 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI',
196
+ 'AWS_PROFILE',
197
+ 'AWS_WEB_IDENTITY_TOKEN_FILE',
198
+ 'GOOGLE_APPLICATION_CREDENTIALS',
199
+ ]);
200
+
201
+ const PI_DOCKER_CONFIGURATION_ENV = [
202
+ 'AWS_BEDROCK_FORCE_CACHE',
203
+ 'AWS_BEDROCK_FORCE_HTTP1',
204
+ 'AWS_BEDROCK_SKIP_AUTH',
205
+ 'AWS_DEFAULT_REGION',
206
+ 'AWS_ENDPOINT_URL_BEDROCK_RUNTIME',
207
+ 'AWS_REGION',
208
+ 'AZURE_OPENAI_API_VERSION',
209
+ 'AZURE_OPENAI_BASE_URL',
210
+ 'AZURE_OPENAI_DEPLOYMENT_NAME_MAP',
211
+ 'AZURE_OPENAI_RESOURCE_NAME',
212
+ 'CLOUDFLARE_ACCOUNT_ID',
213
+ 'CLOUDFLARE_GATEWAY_ID',
214
+ 'GCLOUD_PROJECT',
215
+ 'GOOGLE_CLOUD_LOCATION',
216
+ 'GOOGLE_CLOUD_PROJECT',
217
+ 'KIMI_CODE_OAUTH_HOST',
218
+ 'KIMI_OAUTH_HOST',
219
+ 'PI_CACHE_RETENTION',
220
+ ] as const;
221
+
189
222
  const SPAWN_INVOKE = Object.freeze({ lane: 'spawn' }) as SpawnProviderInvokeSpec;
190
223
  const ACP_STDIO_INVOKE = Object.freeze({
191
224
  lane: 'acp-stdio',
@@ -444,17 +477,17 @@ export const providerRegistry = [
444
477
  binary: 'pi',
445
478
  command: { kind: 'fixed', command: 'pi', args: [] },
446
479
  invoke: SPAWN_INVOKE,
447
- installInstructions: 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.80.3',
480
+ installInstructions: PI_INSTALL_COMMAND,
448
481
  authInstructions: 'pi\n/login',
449
- credentialPaths: ['~/.pi'],
482
+ credentialPaths: ['$PI_CODING_AGENT_DIR/auth.json', '~/.pi/agent/auth.json'],
450
483
  credentialEnvKeys: piAdapter.credentialEnvKeys,
451
484
  settingsFields: [],
452
- availabilityProbe: 'help-or-version',
485
+ availabilityProbe: 'supported-version',
453
486
  capabilities: {
454
487
  ...STANDARD_CAPABILITIES,
455
488
  mcpServers: false,
456
489
  jsonSchema: false,
457
- reasoningEffort: false,
490
+ reasoningEffort: true,
458
491
  },
459
492
  docs: {
460
493
  label: 'Pi',
@@ -462,11 +495,17 @@ export const providerRegistry = [
462
495
  },
463
496
  docker: {
464
497
  mount: {
465
- host: '~/.pi',
466
- container: '$HOME/.pi',
467
- readonly: true,
498
+ host: '~/.pi/agent',
499
+ hostEnv: 'PI_CODING_AGENT_DIR',
500
+ container: '$HOME/.pi/agent',
501
+ readonly: false,
468
502
  },
469
- envPassthrough: [],
503
+ install: PI_INSTALL_COMMAND,
504
+ configRoots: ['$HOME/.pi/agent'],
505
+ envPassthrough: [
506
+ ...piAdapter.credentialEnvKeys.filter((name) => !PI_EXPLICIT_DOCKER_CREDENTIALS.has(name)),
507
+ ...PI_DOCKER_CONFIGURATION_ENV,
508
+ ],
470
509
  },
471
510
  defaultLevels: {
472
511
  min: piAdapter.defaultMinLevel,
@@ -29,6 +29,7 @@ import {
29
29
  supportsProviderCapability,
30
30
  supportsProviderOutputReformatting,
31
31
  } from './provider-registry';
32
+ import { runtimeProbeIsAvailable } from './probe-availability';
32
33
  import type {
33
34
  BuildProviderCommandOptions,
34
35
  CliFeatureOverrides,
@@ -577,6 +578,10 @@ function resolveRuntimeCliFeatures(
577
578
  supportsWebSearch: false,
578
579
  };
579
580
  }
581
+ if (provider === 'pi') {
582
+ const detected = detectRuntimeProviderCliFeatures(provider, runtimeSettings);
583
+ return overrides === undefined ? detected : mergePiFailClosedCliFeatures(detected, overrides);
584
+ }
580
585
  if (getProviderRegistryEntry(provider).invoke.lane === 'acp-stdio') {
581
586
  const detected = detectRuntimeProviderCliFeatures(provider, runtimeSettings);
582
587
  if (overrides === undefined) return detected;
@@ -595,6 +600,18 @@ function resolveRuntimeCliFeatures(
595
600
  };
596
601
  }
597
602
 
603
+ function mergePiFailClosedCliFeatures(
604
+ detected: ProviderCliFeatures,
605
+ overrides: CliFeatureOverrides
606
+ ): CliFeatureOverrides {
607
+ return Object.fromEntries(
608
+ Object.entries(detected).map(([field, value]) => [
609
+ field,
610
+ typeof value === 'boolean' ? value && Reflect.get(overrides, field) !== false : value,
611
+ ])
612
+ );
613
+ }
614
+
598
615
  function mergeAcpFailClosedCliFeatures(
599
616
  detected: ProviderCliFeatures,
600
617
  overrides: CliFeatureOverrides
@@ -679,7 +696,9 @@ export function probeRuntimeProviderCli(
679
696
 
680
697
  const helpText =
681
698
  evidence?.helpText ??
682
- stringResult(getHelpOutputFn(helpCommand.command, helpCommand.args)).trim();
699
+ (adapter.id === 'pi'
700
+ ? ''
701
+ : stringResult(getHelpOutputFn(helpCommand.command, helpCommand.args)).trim());
683
702
  const versionText =
684
703
  evidence?.versionText ??
685
704
  stringResult(
@@ -691,11 +710,13 @@ export function probeRuntimeProviderCli(
691
710
  const capabilities = attestedCliFeatures(adapter, helpText, versionText);
692
711
 
693
712
  return {
694
- available:
695
- evidence?.available ??
696
- (registryEntry.availabilityProbe === 'help-or-version'
697
- ? Boolean(helpText || versionText)
698
- : true),
713
+ available: runtimeProbeIsAvailable(
714
+ registryEntry.availabilityProbe,
715
+ evidence?.available,
716
+ helpText,
717
+ versionText,
718
+ capabilities
719
+ ),
699
720
  helpText,
700
721
  versionText,
701
722
  capabilities,
@@ -158,8 +158,10 @@ export interface OpencodeCliFeatures extends BaseCliFeatures {
158
158
 
159
159
  export interface PiCliFeatures extends BaseCliFeatures {
160
160
  readonly provider: 'pi';
161
+ readonly versionMatches: boolean;
161
162
  readonly supportsJsonMode: boolean;
162
163
  readonly supportsModel: boolean;
164
+ readonly supportsThinking: boolean;
163
165
  readonly supportsNoSession: boolean;
164
166
  readonly supportsNoExtensions: boolean;
165
167
  readonly supportsNoSkills: boolean;
@@ -573,6 +575,7 @@ export interface ProviderAdapter {
573
575
  buildCommand(context: string, options?: BuildProviderCommandOptions): CommandSpec;
574
576
  extractSessionId?(line: string): string | null;
575
577
  parseEvent(line: string, state: ProviderParserState): ProviderParseResult;
578
+ finishParsing?(state: ProviderParserState): ProviderParseResult;
576
579
  createParserState(): ProviderParserState;
577
580
  resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec;
578
581
  validateModelId(modelId: string | null | undefined): string | null | undefined;
@@ -2058,9 +2058,7 @@ function validateProviderSettings(provider, providerSettings) {
2058
2058
  providerModule.resolveModelSpec(level, { [level]: override });
2059
2059
  }
2060
2060
  if (override?.reasoningEffort && !providerSupportsCapability(provider, 'reasoningEffort')) {
2061
- throw new Error(
2062
- `reasoningEffort overrides are only supported for Claude, Codex, and Opencode`
2063
- );
2061
+ throw new Error(`Provider "${provider}" does not support reasoningEffort overrides`);
2064
2062
  }
2065
2063
  if (override?.reasoningEffort && !REASONING_EFFORTS.includes(override.reasoningEffort)) {
2066
2064
  throw new Error(
@@ -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 };