rivet-design 0.14.8 → 0.14.10

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 (44) hide show
  1. package/dist/agent-variants/SessionStore.d.ts +1 -0
  2. package/dist/agent-variants/SessionStore.d.ts.map +1 -1
  3. package/dist/agent-variants/SessionStore.js +16 -0
  4. package/dist/agent-variants/SessionStore.js.map +1 -1
  5. package/dist/agent-variants/WorkerExecutorService.d.ts +65 -9
  6. package/dist/agent-variants/WorkerExecutorService.d.ts.map +1 -1
  7. package/dist/agent-variants/WorkerExecutorService.js +180 -103
  8. package/dist/agent-variants/WorkerExecutorService.js.map +1 -1
  9. package/dist/agent-variants/WorktreeOrchestrator.d.ts +1 -0
  10. package/dist/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  11. package/dist/agent-variants/WorktreeOrchestrator.js +5 -0
  12. package/dist/agent-variants/WorktreeOrchestrator.js.map +1 -1
  13. package/dist/agent-variants/contracts.d.ts +11 -0
  14. package/dist/agent-variants/contracts.d.ts.map +1 -1
  15. package/dist/agent-variants/contracts.js.map +1 -1
  16. package/dist/agent-variants/directionAuthoring.d.ts.map +1 -1
  17. package/dist/agent-variants/directionAuthoring.js +12 -0
  18. package/dist/agent-variants/directionAuthoring.js.map +1 -1
  19. package/dist/agent-variants/workerRuntime.d.ts +216 -0
  20. package/dist/agent-variants/workerRuntime.d.ts.map +1 -0
  21. package/dist/agent-variants/workerRuntime.js +652 -0
  22. package/dist/agent-variants/workerRuntime.js.map +1 -0
  23. package/dist/services/ConfigManager.d.ts +18 -0
  24. package/dist/services/ConfigManager.d.ts.map +1 -1
  25. package/dist/services/ConfigManager.js +15 -0
  26. package/dist/services/ConfigManager.js.map +1 -1
  27. package/dist/services/VariantGenerationService.d.ts +5 -0
  28. package/dist/services/VariantGenerationService.d.ts.map +1 -1
  29. package/dist/services/VariantGenerationService.js +36 -11
  30. package/dist/services/VariantGenerationService.js.map +1 -1
  31. package/dist/services/VariantRunService.d.ts.map +1 -1
  32. package/dist/services/VariantRunService.js +13 -3
  33. package/dist/services/VariantRunService.js.map +1 -1
  34. package/dist/services/VariantsRuntime.d.ts.map +1 -1
  35. package/dist/services/VariantsRuntime.js +6 -0
  36. package/dist/services/VariantsRuntime.js.map +1 -1
  37. package/dist/utils/hostAgentEnv.d.ts +19 -0
  38. package/dist/utils/hostAgentEnv.d.ts.map +1 -0
  39. package/dist/utils/hostAgentEnv.js +32 -0
  40. package/dist/utils/hostAgentEnv.js.map +1 -0
  41. package/package.json +1 -1
  42. package/src/ui/dist/assets/main-QevbGOvt.js +422 -0
  43. package/src/ui/dist/index.html +1 -1
  44. package/src/ui/dist/assets/main-Be9y99Zr.js +0 -422
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workerRuntime.d.ts","sourceRoot":"","sources":["../../src/agent-variants/workerRuntime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,652 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.describeWorkerRuntimeDecision = exports.WorkerRuntimeResolver = exports.adapterForVendor = exports.makeCursorAdapter = exports.parseCursorWorkerResult = exports.makeCodexAdapter = exports.makeClaudeAdapter = exports.parseCodexWorkerResult = exports.parseClaudeWorkerResult = exports.PREFLIGHT_TIERS = exports.EXECUTOR_FIDELITY_TIERS = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const os_1 = __importDefault(require("os"));
9
+ const logger_1 = require("../utils/logger");
10
+ const hostAgentEnv_1 = require("../utils/hostAgentEnv");
11
+ const runnerSelection_1 = require("../cli/runnerSelection");
12
+ const workerDefaults_1 = require("../hosts/workerDefaults");
13
+ const log = (0, logger_1.createLogger)('WorkerRuntime');
14
+ /**
15
+ * Fidelity → tier, per vendor. Claude mirrors the host registry (one model,
16
+ * effort varies — PR #965). Codex maps fidelity onto reasoning effort, its
17
+ * native analogue. Cursor cannot vary quality per invocation, so fidelity is
18
+ * intentionally a no-op there until a per-model benchmark says otherwise.
19
+ */
20
+ exports.EXECUTOR_FIDELITY_TIERS = {
21
+ claude: {
22
+ low: { vendor: 'claude', ...workerDefaults_1.FIDELITY_WORKER_TIERS.low },
23
+ medium: { vendor: 'claude', ...workerDefaults_1.FIDELITY_WORKER_TIERS.medium },
24
+ high: { vendor: 'claude', ...workerDefaults_1.FIDELITY_WORKER_TIERS.high },
25
+ },
26
+ codex: {
27
+ low: { vendor: 'codex', reasoningEffort: 'low' },
28
+ medium: { vendor: 'codex', reasoningEffort: 'medium' },
29
+ high: { vendor: 'codex', reasoningEffort: 'high' },
30
+ },
31
+ cursor: {
32
+ low: { vendor: 'cursor' },
33
+ medium: { vendor: 'cursor' },
34
+ high: { vendor: 'cursor' },
35
+ },
36
+ };
37
+ /** Cheapest valid tier per vendor — what the preflight probe runs with. Same
38
+ * flags as a real worker (the probe must test the exact invocation), minimum
39
+ * spend. */
40
+ exports.PREFLIGHT_TIERS = {
41
+ claude: exports.EXECUTOR_FIDELITY_TIERS.claude.low,
42
+ codex: exports.EXECUTOR_FIDELITY_TIERS.codex.low,
43
+ cursor: exports.EXECUTOR_FIDELITY_TIERS.cursor.low,
44
+ };
45
+ const requireVendorTier = (tier, vendor) => {
46
+ if (tier.vendor !== vendor) {
47
+ throw new Error(`executor tier/adapter mismatch: ${tier.vendor} tier handed to ${vendor} adapter`);
48
+ }
49
+ return tier;
50
+ };
51
+ /** Claude's `--output-format json`: one trailing JSON result object. */
52
+ const parseClaudeWorkerResult = (stdout) => {
53
+ try {
54
+ const trimmed = stdout.trim();
55
+ const start = trimmed.indexOf('{');
56
+ if (start === -1)
57
+ return {};
58
+ const parsed = JSON.parse(trimmed.slice(start));
59
+ const sum = (...values) => values.reduce((total, value) => total + (typeof value === 'number' ? value : 0), 0);
60
+ const usage = parsed.usage;
61
+ const errorParts = [parsed.subtype, parsed.result, parsed.error]
62
+ .filter((value) => typeof value === 'string')
63
+ .join(': ');
64
+ return {
65
+ ...(usage || typeof parsed.total_cost_usd === 'number'
66
+ ? {
67
+ usage: {
68
+ ...(usage
69
+ ? {
70
+ tokensIn: sum(usage.input_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens),
71
+ tokensOut: sum(usage.output_tokens),
72
+ }
73
+ : {}),
74
+ ...(typeof parsed.total_cost_usd === 'number'
75
+ ? { costUsd: parsed.total_cost_usd }
76
+ : {}),
77
+ },
78
+ }
79
+ : {}),
80
+ ...(errorParts.length > 0 ? { resultText: errorParts } : {}),
81
+ ...(typeof parsed.api_error_status === 'number'
82
+ ? { apiErrorStatus: parsed.api_error_status }
83
+ : {}),
84
+ };
85
+ }
86
+ catch {
87
+ return {};
88
+ }
89
+ };
90
+ exports.parseClaudeWorkerResult = parseClaudeWorkerResult;
91
+ /** Codex's `exec --json`: JSONL events. Usage rides `turn.completed`; failures
92
+ * surface as `item.completed` items of type `error` (and `turn.failed`). */
93
+ const parseCodexWorkerResult = (stdout) => {
94
+ const info = {};
95
+ const errors = [];
96
+ for (const line of stdout.split('\n')) {
97
+ const trimmed = line.trim();
98
+ if (!trimmed.startsWith('{'))
99
+ continue;
100
+ try {
101
+ const event = JSON.parse(trimmed);
102
+ if (event.type === 'turn.completed' && event.usage) {
103
+ info.usage = {
104
+ tokensIn: event.usage.input_tokens ?? 0,
105
+ tokensOut: (event.usage.output_tokens ?? 0) +
106
+ (event.usage.reasoning_output_tokens ?? 0),
107
+ };
108
+ }
109
+ if (event.type === 'item.completed' &&
110
+ event.item?.type === 'error' &&
111
+ typeof event.item.message === 'string') {
112
+ errors.push(event.item.message);
113
+ }
114
+ if (event.type === 'turn.failed' || event.type === 'error') {
115
+ const message = typeof event.error === 'string' ? event.error : event.error?.message;
116
+ if (message)
117
+ errors.push(message);
118
+ }
119
+ }
120
+ catch {
121
+ // Interleaved non-JSON log lines are expected; skip them.
122
+ }
123
+ }
124
+ if (errors.length > 0)
125
+ info.resultText = errors.join(': ');
126
+ return info;
127
+ };
128
+ exports.parseCodexWorkerResult = parseCodexWorkerResult;
129
+ const makeClaudeAdapter = (binary = 'claude') => ({
130
+ vendor: 'claude',
131
+ binary,
132
+ promptDelivery: 'stdin',
133
+ buildArgs(tier) {
134
+ const claudeTier = requireVendorTier(tier, 'claude');
135
+ return [
136
+ '--print',
137
+ '--output-format',
138
+ 'json',
139
+ '--model',
140
+ claudeTier.model,
141
+ '--effort',
142
+ claudeTier.effort,
143
+ '--dangerously-skip-permissions',
144
+ '--allowed-tools',
145
+ 'Read,Write,Edit,Bash,Glob,Grep',
146
+ '--exclude-dynamic-system-prompt-sections',
147
+ ];
148
+ },
149
+ parseResult: exports.parseClaudeWorkerResult,
150
+ supportsModelFallback: true,
151
+ // Strip nested-session markers: a `claude` worker spawned from a server
152
+ // that was itself started inside a Claude Code session would otherwise hit
153
+ // the refuse-to-nest guard (see hostAgentEnv.ts).
154
+ spawnEnv: () => (0, hostAgentEnv_1.sanitizeHostAgentEnv)(process.env),
155
+ });
156
+ exports.makeClaudeAdapter = makeClaudeAdapter;
157
+ const makeCodexAdapter = (binary = runnerSelection_1.RUNNER_VENDOR_BINARIES.codex) => ({
158
+ vendor: 'codex',
159
+ binary,
160
+ promptDelivery: 'stdin',
161
+ buildArgs(tier) {
162
+ const codexTier = requireVendorTier(tier, 'codex');
163
+ return [
164
+ 'exec',
165
+ '--json',
166
+ '--skip-git-repo-check',
167
+ // Same posture as claude's --dangerously-skip-permissions; the worker
168
+ // is already confined to its own workspace by the briefing contract.
169
+ '--dangerously-bypass-approvals-and-sandbox',
170
+ ...(codexTier.model ? ['--model', codexTier.model] : []),
171
+ '-c',
172
+ `model_reasoning_effort=${codexTier.reasoningEffort}`,
173
+ '-',
174
+ ];
175
+ },
176
+ parseResult: exports.parseCodexWorkerResult,
177
+ supportsModelFallback: false,
178
+ spawnEnv: () => process.env,
179
+ });
180
+ exports.makeCodexAdapter = makeCodexAdapter;
181
+ /** Cursor's `-p --output-format json`: one trailing JSON result object, like
182
+ * claude's — but usage keys are camelCase (`inputTokens`) and `subtype`/
183
+ * `result` are populated on success too ("success"/"ok"), so error text is
184
+ * taken only when `is_error` is set. Shape live-verified 2026-07-23 against
185
+ * cursor-agent 2026.07.23-e383d2b. */
186
+ const parseCursorWorkerResult = (stdout) => {
187
+ try {
188
+ const trimmed = stdout.trim();
189
+ const start = trimmed.indexOf('{');
190
+ if (start === -1)
191
+ return {};
192
+ const parsed = JSON.parse(trimmed.slice(start));
193
+ const sum = (...values) => values.reduce((total, value) => total + (typeof value === 'number' ? value : 0), 0);
194
+ const usage = parsed.usage;
195
+ const errorParts = parsed.is_error === true
196
+ ? [parsed.subtype, parsed.result]
197
+ .filter((value) => typeof value === 'string')
198
+ .join(': ')
199
+ : '';
200
+ return {
201
+ ...(usage
202
+ ? {
203
+ usage: {
204
+ tokensIn: sum(usage.inputTokens, usage.cacheReadTokens, usage.cacheWriteTokens),
205
+ tokensOut: sum(usage.outputTokens),
206
+ },
207
+ }
208
+ : {}),
209
+ ...(errorParts.length > 0 ? { resultText: errorParts } : {}),
210
+ };
211
+ }
212
+ catch {
213
+ return {};
214
+ }
215
+ };
216
+ exports.parseCursorWorkerResult = parseCursorWorkerResult;
217
+ const makeCursorAdapter = (binary = runnerSelection_1.RUNNER_VENDOR_BINARIES.cursor) => ({
218
+ vendor: 'cursor',
219
+ binary,
220
+ // cursor-agent's print mode does not read the prompt from stdin — it goes
221
+ // in argv. Briefings are ~16-20KB, well inside ARG_MAX.
222
+ promptDelivery: 'argv',
223
+ buildArgs(tier) {
224
+ const cursorTier = requireVendorTier(tier, 'cursor');
225
+ return [
226
+ '-p',
227
+ '--output-format',
228
+ 'json',
229
+ ...(cursorTier.model ? ['-m', cursorTier.model] : []),
230
+ '--force',
231
+ ];
232
+ },
233
+ parseResult: exports.parseCursorWorkerResult,
234
+ supportsModelFallback: false,
235
+ spawnEnv: () => process.env,
236
+ });
237
+ exports.makeCursorAdapter = makeCursorAdapter;
238
+ const adapterForVendor = (vendor, binaryOverride) => {
239
+ switch (vendor) {
240
+ case 'claude':
241
+ return (0, exports.makeClaudeAdapter)(binaryOverride);
242
+ case 'codex':
243
+ return (0, exports.makeCodexAdapter)(binaryOverride);
244
+ case 'cursor':
245
+ return (0, exports.makeCursorAdapter)(binaryOverride);
246
+ }
247
+ };
248
+ exports.adapterForVendor = adapterForVendor;
249
+ const PREFLIGHT_PROMPT = 'Reply with the single word: ok. Do not use any tools.';
250
+ const DEFAULT_PREFLIGHT_TIMEOUT_MS = 30_000;
251
+ /** Vendor selections arrive as user-typed strings (config file, env var) —
252
+ * trim and case-fold before matching so `"Claude "` or `CODEX` still select
253
+ * the vendor instead of silently falling through to PATH probing. */
254
+ const asExecutableVendor = (value) => {
255
+ if (typeof value !== 'string')
256
+ return undefined;
257
+ const normalized = value.trim().toLowerCase();
258
+ return runnerSelection_1.EXECUTABLE_RUNNER_VENDORS.includes(normalized)
259
+ ? normalized
260
+ : undefined;
261
+ };
262
+ /** Strip ANSI escapes / control characters so a TUI splash (cursor-agent's
263
+ * sign-in banner renders box art to stdout) reads as one diagnosable line. */
264
+ const sanitizeProbeText = (text) => text
265
+ // eslint-disable-next-line no-control-regex
266
+ .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, '')
267
+ // eslint-disable-next-line no-control-regex
268
+ .replace(/\u001b\[[0-9;?]*[a-zA-Z]/g, '')
269
+ // eslint-disable-next-line no-control-regex
270
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, ' ')
271
+ .replace(/\s+/g, ' ')
272
+ .trim();
273
+ /** An installed-but-logged-out CLI is the common first-run state (the CLI
274
+ * login is separate from the IDE's). Every vendor prints some "sign in"
275
+ * variant; match it so the degrade message says what to actually do. */
276
+ const LOGIN_MARKERS = /sign[ -]?in|not logged in|not signed in|unauthenticated|not authenticated|authentication required|invalid api key|please run \/login|401/i;
277
+ const VENDOR_LOGIN_HINTS = {
278
+ claude: 'not logged in — run /login inside `claude`',
279
+ codex: 'not logged in — run `codex login`',
280
+ cursor: 'not logged in — run `cursor-agent login`',
281
+ };
282
+ /** Health-cache identity of one probe: same vendor with a different binary or
283
+ * resolved model is a different runtime and must preflight independently. */
284
+ const probeCacheKey = (adapter, tier) => `${adapter.vendor}|${adapter.binary}|${'model' in tier && tier.model ? tier.model : 'default'}`;
285
+ const defaultPreflightSpawn = ({ command, args, cwd, env }) => (0, child_process_1.spawn)(command, args, {
286
+ cwd,
287
+ env,
288
+ stdio: ['pipe', 'pipe', 'pipe'],
289
+ detached: process.platform !== 'win32',
290
+ });
291
+ const killProcessTree = (pid) => {
292
+ if (!pid)
293
+ return;
294
+ try {
295
+ if (process.platform === 'win32') {
296
+ process.kill(pid, 'SIGKILL');
297
+ return;
298
+ }
299
+ process.kill(-pid, 'SIGKILL');
300
+ }
301
+ catch {
302
+ try {
303
+ process.kill(pid, 'SIGKILL');
304
+ }
305
+ catch {
306
+ // Already gone.
307
+ }
308
+ }
309
+ };
310
+ /**
311
+ * Resolves and preflights the executor's worker runtime, per request.
312
+ *
313
+ * Precedence: env override (`RIVET_EXECUTOR_VENDOR`, or legacy
314
+ * `RIVET_EXECUTOR_COMMAND` = claude-grammar custom binary) > user config
315
+ * `workerVendor` > the request initiator's agent > (human / no preference)
316
+ * probe installed vendors in registry order.
317
+ *
318
+ * Note this is deliberately PER-REQUEST, unlike apply-runner resolution
319
+ * (`resolveRunner`, fixed for the server's lifetime): one live session can
320
+ * serve requests initiated by different agents, and each request's workers
321
+ * should run on its initiator's runtime.
322
+ *
323
+ * Preflight lifecycle: one in-flight probe per runtime identity (vendor +
324
+ * binary + resolved model — concurrent resolves join it); a healthy result
325
+ * is cached until a runtime-shaped worker failure invalidates it
326
+ * (`markUnhealthy`) or a config edit changes the resolved model; an
327
+ * unhealthy result is re-probed on the next resolve — so a user who fixes
328
+ * their auth is not stuck on the host path, and each new run retries a
329
+ * broken vendor at most once.
330
+ */
331
+ class WorkerRuntimeResolver {
332
+ options;
333
+ health = new Map();
334
+ inFlight = new Map();
335
+ constructor(options = {}) {
336
+ this.options = options;
337
+ }
338
+ /** Invalidate a cached-healthy vendor after a runtime-shaped worker failure
339
+ * (spawn error, unknown CLI option, auth rejection) so the next run
340
+ * re-probes instead of leasing four more items to a dead runtime. */
341
+ markUnhealthy(vendor, reason) {
342
+ for (const key of this.health.keys()) {
343
+ if (key.startsWith(`${vendor}|`)) {
344
+ this.health.set(key, {
345
+ healthy: false,
346
+ reason,
347
+ category: 'worker_failure',
348
+ });
349
+ }
350
+ }
351
+ log.warn(`worker runtime ${vendor} marked unhealthy: ${reason}`);
352
+ }
353
+ async resolve(args) {
354
+ const { candidates, source, strict } = this.candidatesFor(args?.initiator);
355
+ const diagnostics = [];
356
+ for (const candidate of candidates) {
357
+ const outcome = await this.preflight(candidate, args?.signal);
358
+ if (outcome.healthy) {
359
+ return {
360
+ mode: 'executor',
361
+ adapter: candidate,
362
+ // A strict resolution that lands on a non-primary candidate got
363
+ // there via the cross-vendor fallback opt-in — label it as such.
364
+ source: strict && candidate !== candidates[0] ? 'fallback' : source,
365
+ };
366
+ }
367
+ diagnostics.push({
368
+ vendor: candidate.vendor,
369
+ binary: candidate.binary,
370
+ reason: outcome.reason,
371
+ });
372
+ if (strict && !(this.options.crossVendorFallback?.() ?? false))
373
+ break;
374
+ }
375
+ return { mode: 'host', diagnostics };
376
+ }
377
+ /** Candidate adapters in trial order, plus whether the choice was explicit
378
+ * (strict = unhealthy means host path, not another vendor). */
379
+ candidatesFor(initiator) {
380
+ const env = this.options.env ?? process.env;
381
+ const probe = this.options.binaryProbe ?? runnerSelection_1.isBinaryOnPath;
382
+ // Legacy ops escape hatch: a custom command is assumed claude-grammar.
383
+ const legacyCommand = env.RIVET_EXECUTOR_COMMAND;
384
+ if (legacyCommand) {
385
+ return {
386
+ candidates: [(0, exports.makeClaudeAdapter)(legacyCommand)],
387
+ source: 'env',
388
+ strict: true,
389
+ };
390
+ }
391
+ const envVendor = asExecutableVendor(env.RIVET_EXECUTOR_VENDOR);
392
+ if (envVendor) {
393
+ return {
394
+ candidates: this.withFallbackCandidates(envVendor, probe),
395
+ source: 'env',
396
+ strict: true,
397
+ };
398
+ }
399
+ const configured = asExecutableVendor(this.options.configuredVendor?.());
400
+ if (configured) {
401
+ return {
402
+ candidates: this.withFallbackCandidates(configured, probe),
403
+ source: 'user_config',
404
+ strict: true,
405
+ };
406
+ }
407
+ const initiatorVendor = initiator?.kind === 'agent'
408
+ ? asExecutableVendor(initiator.agent)
409
+ : undefined;
410
+ if (initiatorVendor) {
411
+ // Strict even when the initiator's CLI is not installed: the missing
412
+ // binary fails preflight and the request degrades to the host path —
413
+ // never to another vendor's CLI without the fallback opt-in.
414
+ return {
415
+ candidates: this.withFallbackCandidates(initiatorVendor, probe),
416
+ source: 'initiator',
417
+ strict: true,
418
+ };
419
+ }
420
+ // Human request / unknown agent: no prior vendor expectation exists, so
421
+ // probing across installed vendors is fine.
422
+ return {
423
+ candidates: runnerSelection_1.EXECUTABLE_RUNNER_VENDORS.filter((vendor) => probe(runnerSelection_1.RUNNER_VENDOR_BINARIES[vendor])).map((vendor) => this.adapterFor(vendor)),
424
+ source: 'probe',
425
+ strict: false,
426
+ };
427
+ }
428
+ withFallbackCandidates(vendor, probe) {
429
+ const primary = this.adapterFor(vendor);
430
+ if (!(this.options.crossVendorFallback?.() ?? false))
431
+ return [primary];
432
+ const rest = runnerSelection_1.EXECUTABLE_RUNNER_VENDORS.filter((candidate) => candidate !== vendor && probe(runnerSelection_1.RUNNER_VENDOR_BINARIES[candidate])).map((candidate) => this.adapterFor(candidate));
433
+ return [primary, ...rest];
434
+ }
435
+ adapterFor(vendor) {
436
+ return (0, exports.adapterForVendor)(vendor);
437
+ }
438
+ /** Tier the probe runs with — cheapest per vendor, with the same model
439
+ * override precedence a real worker gets (`RIVET_EXECUTOR_MODEL` env >
440
+ * `workerModels.<vendor>` config) so the probe exercises the exact model
441
+ * a real worker would run, not the CLI default. */
442
+ probeTierFor(adapter) {
443
+ const base = exports.PREFLIGHT_TIERS[adapter.vendor];
444
+ const env = this.options.env ?? process.env;
445
+ const model = env.RIVET_EXECUTOR_MODEL ??
446
+ this.options.configuredModelFor?.(adapter.vendor);
447
+ return model ? { ...base, model } : base;
448
+ }
449
+ async preflight(adapter, signal) {
450
+ const tier = this.probeTierFor(adapter);
451
+ // Cache key includes the binary and resolved model, not just the vendor:
452
+ // `workerModels.<vendor>` is read lazily, so a config edit while the
453
+ // server is live must re-probe rather than ride a stale healthy result
454
+ // for a model preflight never exercised.
455
+ const key = probeCacheKey(adapter, tier);
456
+ const cached = this.health.get(key);
457
+ // Healthy results are reused; unhealthy results are re-probed (each new
458
+ // resolve gives a fixed vendor one fresh chance).
459
+ if (cached?.healthy)
460
+ return cached;
461
+ const inFlight = this.inFlight.get(key);
462
+ if (inFlight)
463
+ return inFlight;
464
+ const startedAt = Date.now();
465
+ const probePromise = this.probeWithModelFallback(adapter, tier, signal)
466
+ .then((outcome) => {
467
+ this.health.set(key, outcome);
468
+ // Telemetry gets the coarse category only: the raw reason is built
469
+ // from stderr/spawn errors and can carry local paths or account
470
+ // fragments. Full detail stays in server.log and run feedback.
471
+ this.options.telemetry?.track('agent_variants.executor_preflight', {
472
+ source: 'mcp',
473
+ vendor: adapter.vendor,
474
+ healthy: outcome.healthy,
475
+ durationMs: Date.now() - startedAt,
476
+ reason: outcome.healthy ? null : outcome.category,
477
+ });
478
+ if (!outcome.healthy) {
479
+ log.warn(`worker runtime preflight failed for ${adapter.vendor} ` +
480
+ `(${adapter.binary}): ${outcome.reason}`);
481
+ }
482
+ return outcome;
483
+ })
484
+ .finally(() => {
485
+ this.inFlight.delete(key);
486
+ });
487
+ this.inFlight.set(key, probePromise);
488
+ return probePromise;
489
+ }
490
+ /** One probe — plus, for claude (the only vendor with a structured
491
+ * fallback-model rung), a second probe on the fallback model when the
492
+ * primary MODEL is what failed. A healthy fallback probe marks the
493
+ * runtime healthy under the primary identity: real workers still start
494
+ * on the primary tier and degrade per item via the executor's one-shot
495
+ * fallback retry, exactly the pre-preflight behavior. */
496
+ async probeWithModelFallback(adapter, tier, signal) {
497
+ const outcome = await this.runProbe(adapter, tier, signal);
498
+ const fallback = this.options.probeModelFallback;
499
+ if (outcome.healthy ||
500
+ !fallback ||
501
+ !adapter.supportsModelFallback ||
502
+ outcome.category !== 'exit_nonzero') {
503
+ return outcome;
504
+ }
505
+ const primaryModel = 'model' in tier ? tier.model : undefined;
506
+ const fallbackModel = fallback.fallbackModel();
507
+ if (!primaryModel ||
508
+ !fallbackModel ||
509
+ fallbackModel === primaryModel ||
510
+ !fallback.isModelAvailabilityFailure({
511
+ message: outcome.reason,
512
+ ...(outcome.apiErrorStatus !== undefined
513
+ ? { apiErrorStatus: outcome.apiErrorStatus }
514
+ : {}),
515
+ model: primaryModel,
516
+ })) {
517
+ return outcome;
518
+ }
519
+ log.warn(`worker runtime preflight: ${adapter.vendor} model '${primaryModel}' ` +
520
+ `unavailable — probing fallback model '${fallbackModel}'`);
521
+ const fallbackOutcome = await this.runProbe(adapter, { ...tier, model: fallbackModel }, signal);
522
+ return fallbackOutcome.healthy ? fallbackOutcome : outcome;
523
+ }
524
+ runProbe(adapter, tier, signal) {
525
+ const timeoutMs = this.options.probeTimeoutMs ??
526
+ Number((this.options.env ?? process.env)
527
+ .RIVET_EXECUTOR_PREFLIGHT_TIMEOUT_MS ?? DEFAULT_PREFLIGHT_TIMEOUT_MS);
528
+ const spawnProbe = this.options.spawnProbe ?? defaultPreflightSpawn;
529
+ const args = adapter.buildArgs(tier);
530
+ if (adapter.promptDelivery === 'argv')
531
+ args.push(PREFLIGHT_PROMPT);
532
+ return new Promise((resolve) => {
533
+ // A signal that aborted before we attached would otherwise leave the
534
+ // probe running to its full timeout — check up front.
535
+ if (signal?.aborted) {
536
+ resolve({
537
+ healthy: false,
538
+ reason: 'probe aborted (run cancelled)',
539
+ category: 'aborted',
540
+ });
541
+ return;
542
+ }
543
+ let child;
544
+ try {
545
+ child = spawnProbe({
546
+ command: adapter.binary,
547
+ args,
548
+ cwd: os_1.default.tmpdir(),
549
+ env: adapter.spawnEnv(),
550
+ });
551
+ }
552
+ catch (error) {
553
+ resolve({
554
+ healthy: false,
555
+ reason: error instanceof Error ? error.message : String(error),
556
+ category: 'spawn_error',
557
+ });
558
+ return;
559
+ }
560
+ let settled = false;
561
+ let stderrTail = '';
562
+ let stdoutTail = '';
563
+ const settle = (outcome) => {
564
+ if (settled)
565
+ return;
566
+ settled = true;
567
+ clearTimeout(timer);
568
+ signal?.removeEventListener('abort', onAbort);
569
+ resolve(outcome);
570
+ };
571
+ const onAbort = () => {
572
+ killProcessTree(child.pid);
573
+ settle({
574
+ healthy: false,
575
+ reason: 'probe aborted (run cancelled)',
576
+ category: 'aborted',
577
+ });
578
+ };
579
+ const timer = setTimeout(() => {
580
+ killProcessTree(child.pid);
581
+ settle({
582
+ healthy: false,
583
+ reason: `probe timed out after ${timeoutMs}ms`,
584
+ category: 'timeout',
585
+ });
586
+ }, timeoutMs);
587
+ // Note: a dedup-shared probe can be aborted by the first joiner's
588
+ // signal. Accepted: the outcome is cached as unhealthy and the next
589
+ // resolve re-probes.
590
+ signal?.addEventListener('abort', onAbort, { once: true });
591
+ child.stderr?.on('data', (chunk) => {
592
+ stderrTail = (stderrTail + chunk.toString()).slice(-2000);
593
+ });
594
+ child.stdout?.on('data', (chunk) => {
595
+ stdoutTail = (stdoutTail + chunk.toString()).slice(-16384);
596
+ });
597
+ child.on('error', (error) => {
598
+ settle({ healthy: false, reason: error.message, category: 'spawn_error' });
599
+ });
600
+ child.on('close', (code) => {
601
+ if (code === 0) {
602
+ settle({ healthy: true });
603
+ return;
604
+ }
605
+ const { resultText, apiErrorStatus } = adapter.parseResult(stdoutTail);
606
+ const stderrDetail = sanitizeProbeText(stderrTail);
607
+ // A logged-out CLI often reports nothing on stderr and no parseable
608
+ // result — just a sign-in splash on stdout. Fall back to that tail
609
+ // so the reason is never an empty "probe exited 1: ".
610
+ const stdoutDetail = sanitizeProbeText(stdoutTail);
611
+ const detail = stderrDetail || (resultText ? '' : stdoutDetail);
612
+ const loginRequired = LOGIN_MARKERS.test(`${stderrDetail}\n${resultText ?? ''}\n${stdoutDetail}`);
613
+ // Once diagnosed as a login problem the hint IS the reason — the raw
614
+ // output is a sign-in banner (ASCII art on cursor-agent) that only
615
+ // buries the actionable part.
616
+ settle(loginRequired
617
+ ? {
618
+ healthy: false,
619
+ category: 'login_required',
620
+ reason: `${adapter.binary} is ${VENDOR_LOGIN_HINTS[adapter.vendor]} (probe exited ${code})`,
621
+ }
622
+ : {
623
+ healthy: false,
624
+ category: 'exit_nonzero',
625
+ ...(apiErrorStatus !== undefined ? { apiErrorStatus } : {}),
626
+ reason: `probe exited ${code}: ${detail.slice(-400)}${resultText ? ` | result: ${resultText.slice(-500)}` : ''}`,
627
+ });
628
+ });
629
+ // Always close stdin: argv-delivery CLIs (cursor-agent print mode) can
630
+ // block on an open stdin pipe — the real worker spawn ends stdin
631
+ // unconditionally for the same reason.
632
+ if (adapter.promptDelivery === 'stdin') {
633
+ child.stdin?.write(PREFLIGHT_PROMPT);
634
+ }
635
+ child.stdin?.end();
636
+ });
637
+ }
638
+ }
639
+ exports.WorkerRuntimeResolver = WorkerRuntimeResolver;
640
+ /** One-line human summary of a decision, for run feedback and server.log:
641
+ * `codex` / `host path (claude: probe exited 1: …)`. */
642
+ const describeWorkerRuntimeDecision = (decision) => {
643
+ if (decision.mode === 'executor') {
644
+ return decision.adapter.vendor;
645
+ }
646
+ const reasons = decision.diagnostics
647
+ .map((d) => `${d.vendor}: ${d.reason}`)
648
+ .join('; ');
649
+ return `host path${reasons ? ` (${reasons})` : ' (no worker runtime installed)'}`;
650
+ };
651
+ exports.describeWorkerRuntimeDecision = describeWorkerRuntimeDecision;
652
+ //# sourceMappingURL=workerRuntime.js.map