rivet-design 0.14.9 → 0.14.11

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 (76) hide show
  1. package/dist/agent-variants/SessionStore.d.ts +11 -1
  2. package/dist/agent-variants/SessionStore.d.ts.map +1 -1
  3. package/dist/agent-variants/SessionStore.js +18 -0
  4. package/dist/agent-variants/SessionStore.js.map +1 -1
  5. package/dist/agent-variants/WorkerExecutorService.d.ts +77 -9
  6. package/dist/agent-variants/WorkerExecutorService.d.ts.map +1 -1
  7. package/dist/agent-variants/WorkerExecutorService.js +202 -104
  8. package/dist/agent-variants/WorkerExecutorService.js.map +1 -1
  9. package/dist/agent-variants/WorktreeOrchestrator.d.ts +13 -0
  10. package/dist/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  11. package/dist/agent-variants/WorktreeOrchestrator.js +25 -0
  12. package/dist/agent-variants/WorktreeOrchestrator.js.map +1 -1
  13. package/dist/agent-variants/contracts.d.ts +22 -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/workItemBriefing.d.ts.map +1 -1
  20. package/dist/agent-variants/workItemBriefing.js +48 -16
  21. package/dist/agent-variants/workItemBriefing.js.map +1 -1
  22. package/dist/agent-variants/workerRuntime.d.ts +230 -0
  23. package/dist/agent-variants/workerRuntime.d.ts.map +1 -0
  24. package/dist/agent-variants/workerRuntime.js +705 -0
  25. package/dist/agent-variants/workerRuntime.js.map +1 -0
  26. package/dist/cli/commands/mcpServe.d.ts.map +1 -1
  27. package/dist/cli/commands/mcpServe.js +16 -1
  28. package/dist/cli/commands/mcpServe.js.map +1 -1
  29. package/dist/cli/commands/variants.d.ts.map +1 -1
  30. package/dist/cli/commands/variants.js +26 -0
  31. package/dist/cli/commands/variants.js.map +1 -1
  32. package/dist/cli/hostWorkNextAction.d.ts +4 -0
  33. package/dist/cli/hostWorkNextAction.d.ts.map +1 -1
  34. package/dist/cli/hostWorkNextAction.js.map +1 -1
  35. package/dist/cli/router.js +1 -1
  36. package/dist/cli/router.js.map +1 -1
  37. package/dist/routes/agentVariants.d.ts.map +1 -1
  38. package/dist/routes/agentVariants.js +15 -0
  39. package/dist/routes/agentVariants.js.map +1 -1
  40. package/dist/services/ConfigManager.d.ts +18 -0
  41. package/dist/services/ConfigManager.d.ts.map +1 -1
  42. package/dist/services/ConfigManager.js +15 -0
  43. package/dist/services/ConfigManager.js.map +1 -1
  44. package/dist/services/VariantGenerationService.d.ts +5 -0
  45. package/dist/services/VariantGenerationService.d.ts.map +1 -1
  46. package/dist/services/VariantGenerationService.js +42 -11
  47. package/dist/services/VariantGenerationService.js.map +1 -1
  48. package/dist/services/VariantHistoryService.d.ts +7 -0
  49. package/dist/services/VariantHistoryService.d.ts.map +1 -1
  50. package/dist/services/VariantHistoryService.js +1 -0
  51. package/dist/services/VariantHistoryService.js.map +1 -1
  52. package/dist/services/VariantRunService.d.ts.map +1 -1
  53. package/dist/services/VariantRunService.js +17 -3
  54. package/dist/services/VariantRunService.js.map +1 -1
  55. package/dist/services/VariantsRuntime.d.ts.map +1 -1
  56. package/dist/services/VariantsRuntime.js +6 -0
  57. package/dist/services/VariantsRuntime.js.map +1 -1
  58. package/dist/types/change-request-types.d.ts +22 -0
  59. package/dist/types/change-request-types.d.ts.map +1 -1
  60. package/dist/utils/hostAgentEnv.d.ts +19 -0
  61. package/dist/utils/hostAgentEnv.d.ts.map +1 -0
  62. package/dist/utils/hostAgentEnv.js +32 -0
  63. package/dist/utils/hostAgentEnv.js.map +1 -0
  64. package/dist/utils/skills/claude-skill.d.ts +2 -2
  65. package/dist/utils/skills/claude-skill.d.ts.map +1 -1
  66. package/dist/utils/skills/claude-skill.js +1 -1
  67. package/dist/utils/skills/cli-guidance.d.ts +2 -2
  68. package/dist/utils/skills/cli-guidance.d.ts.map +1 -1
  69. package/dist/utils/skills/cli-guidance.js +4 -2
  70. package/dist/utils/skills/cli-guidance.js.map +1 -1
  71. package/dist/utils/skills/cursor-rules.d.ts +2 -2
  72. package/dist/utils/skills/cursor-rules.d.ts.map +1 -1
  73. package/dist/utils/skills/cursor-rules.js +1 -1
  74. package/package.json +1 -1
  75. package/src/ui/dist/assets/{main-CQho_sDZ.js → main-EGX1lAi3.js} +2 -2
  76. package/src/ui/dist/index.html +1 -1
@@ -0,0 +1,705 @@
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
+ * Request-PINNED probes get their own identity too: they run without the
285
+ * fallback-model rung, so their verdicts and ordinary (fallback-eligible)
286
+ * verdicts are not interchangeable in either direction — a pinned request
287
+ * must not ride a fallback-earned healthy, and an ordinary resolve must not
288
+ * join a pinned probe and lose its fallback rung. Distinct keys make both
289
+ * impossible and remove any in-flight sharing between the two modes. */
290
+ const probeCacheKey = (adapter, tier, requestPinnedModel) => `${adapter.vendor}|${adapter.binary}|${'model' in tier && tier.model ? tier.model : 'default'}${requestPinnedModel ? '|pinned' : ''}`;
291
+ const defaultPreflightSpawn = ({ command, args, cwd, env }) => (0, child_process_1.spawn)(command, args, {
292
+ cwd,
293
+ env,
294
+ stdio: ['pipe', 'pipe', 'pipe'],
295
+ detached: process.platform !== 'win32',
296
+ });
297
+ const killProcessTree = (pid) => {
298
+ if (!pid)
299
+ return;
300
+ try {
301
+ if (process.platform === 'win32') {
302
+ process.kill(pid, 'SIGKILL');
303
+ return;
304
+ }
305
+ process.kill(-pid, 'SIGKILL');
306
+ }
307
+ catch {
308
+ try {
309
+ process.kill(pid, 'SIGKILL');
310
+ }
311
+ catch {
312
+ // Already gone.
313
+ }
314
+ }
315
+ };
316
+ /**
317
+ * Resolves and preflights the executor's worker runtime, per request.
318
+ *
319
+ * Precedence: env override (`RIVET_EXECUTOR_VENDOR`, or legacy
320
+ * `RIVET_EXECUTOR_COMMAND` = claude-grammar custom binary) > user config
321
+ * `workerVendor` > the request initiator's agent > (human / no preference)
322
+ * probe installed vendors in registry order.
323
+ *
324
+ * Note this is deliberately PER-REQUEST, unlike apply-runner resolution
325
+ * (`resolveRunner`, fixed for the server's lifetime): one live session can
326
+ * serve requests initiated by different agents, and each request's workers
327
+ * should run on its initiator's runtime.
328
+ *
329
+ * Preflight lifecycle: one in-flight probe per runtime identity (vendor +
330
+ * binary + resolved model — concurrent resolves join it); a healthy result
331
+ * is cached until a runtime-shaped worker failure invalidates it
332
+ * (`markUnhealthy`) or a config edit changes the resolved model; an
333
+ * unhealthy result is re-probed on the next resolve — so a user who fixes
334
+ * their auth is not stuck on the host path, and each new run retries a
335
+ * broken vendor at most once.
336
+ */
337
+ class WorkerRuntimeResolver {
338
+ options;
339
+ health = new Map();
340
+ inFlight = new Map();
341
+ constructor(options = {}) {
342
+ this.options = options;
343
+ }
344
+ /** Invalidate a cached-healthy vendor after a runtime-shaped worker failure
345
+ * (spawn error, unknown CLI option, auth rejection) so the next run
346
+ * re-probes instead of leasing four more items to a dead runtime. */
347
+ markUnhealthy(vendor, reason) {
348
+ for (const key of this.health.keys()) {
349
+ if (key.startsWith(`${vendor}|`)) {
350
+ this.health.set(key, {
351
+ healthy: false,
352
+ reason,
353
+ category: 'worker_failure',
354
+ });
355
+ }
356
+ }
357
+ log.warn(`worker runtime ${vendor} marked unhealthy: ${reason}`);
358
+ }
359
+ async resolve(args) {
360
+ const { candidates, source, strict } = this.candidatesFor(args?.initiator, args?.selection);
361
+ const diagnostics = [];
362
+ for (const candidate of candidates) {
363
+ // The request model pin binds to ONE runtime: the requested provider,
364
+ // or — for a model-only request — the primary candidate the request
365
+ // resolved against. Fallback / later probe-path candidates are probed
366
+ // (and would run) without it: another CLI's model id is meaningless
367
+ // there, and a silent re-target would hand the user a runtime they
368
+ // didn't pick. When the RIVET_EXECUTOR_MODEL ops escape hatch is set,
369
+ // the pin is inert (the env model is what preflights and runs), so it
370
+ // must not travel as modelOverride — that would disable the ops
371
+ // model-fallback rung over a pin that is not even in effect.
372
+ const envModel = (this.options.env ?? process.env).RIVET_EXECUTOR_MODEL;
373
+ const modelPinApplies = Boolean(!envModel &&
374
+ args?.selection?.model &&
375
+ (args.selection.provider
376
+ ? args.selection.provider === candidate.vendor
377
+ : candidate === candidates[0]));
378
+ const outcome = await this.preflight(candidate, args?.signal, modelPinApplies ? args?.selection : undefined);
379
+ if (outcome.healthy) {
380
+ return {
381
+ mode: 'executor',
382
+ adapter: candidate,
383
+ // A strict resolution that lands on a non-primary candidate got
384
+ // there via the cross-vendor fallback opt-in — label it as such.
385
+ source: strict && candidate !== candidates[0] ? 'fallback' : source,
386
+ ...(modelPinApplies ? { modelOverride: args.selection.model } : {}),
387
+ };
388
+ }
389
+ diagnostics.push({
390
+ vendor: candidate.vendor,
391
+ binary: candidate.binary,
392
+ reason: outcome.reason,
393
+ });
394
+ if (strict && !(this.options.crossVendorFallback?.() ?? false))
395
+ break;
396
+ }
397
+ return { mode: 'host', diagnostics };
398
+ }
399
+ /** Candidate adapters in trial order, plus whether the choice was explicit
400
+ * (strict = unhealthy means host path, not another vendor). */
401
+ candidatesFor(initiator, selection) {
402
+ const env = this.options.env ?? process.env;
403
+ const probe = this.options.binaryProbe ?? runnerSelection_1.isBinaryOnPath;
404
+ // Legacy ops escape hatch: a custom command is assumed claude-grammar.
405
+ const legacyCommand = env.RIVET_EXECUTOR_COMMAND;
406
+ if (legacyCommand) {
407
+ return {
408
+ candidates: [(0, exports.makeClaudeAdapter)(legacyCommand)],
409
+ source: 'env',
410
+ strict: true,
411
+ };
412
+ }
413
+ const envVendor = asExecutableVendor(env.RIVET_EXECUTOR_VENDOR);
414
+ if (envVendor) {
415
+ return {
416
+ candidates: this.withFallbackCandidates(envVendor, probe),
417
+ source: 'env',
418
+ strict: true,
419
+ };
420
+ }
421
+ // Explicit per-request choice: the user's live intent ("--worker-provider
422
+ // claude" relayed by the host agent) beats a sticky config default, and
423
+ // only the ops env escape hatches beat it. Strict like every explicit
424
+ // choice — an unhealthy selection lands on the host path with the reason.
425
+ const requested = asExecutableVendor(selection?.provider);
426
+ if (requested) {
427
+ return {
428
+ candidates: this.withFallbackCandidates(requested, probe),
429
+ source: 'request',
430
+ strict: true,
431
+ };
432
+ }
433
+ const configured = asExecutableVendor(this.options.configuredVendor?.());
434
+ if (configured) {
435
+ return {
436
+ candidates: this.withFallbackCandidates(configured, probe),
437
+ source: 'user_config',
438
+ strict: true,
439
+ };
440
+ }
441
+ const initiatorVendor = initiator?.kind === 'agent'
442
+ ? asExecutableVendor(initiator.agent)
443
+ : undefined;
444
+ if (initiatorVendor) {
445
+ // Strict even when the initiator's CLI is not installed: the missing
446
+ // binary fails preflight and the request degrades to the host path —
447
+ // never to another vendor's CLI without the fallback opt-in.
448
+ return {
449
+ candidates: this.withFallbackCandidates(initiatorVendor, probe),
450
+ source: 'initiator',
451
+ strict: true,
452
+ };
453
+ }
454
+ // Human request / unknown agent: no prior vendor expectation exists, so
455
+ // probing across installed vendors is fine.
456
+ return {
457
+ candidates: runnerSelection_1.EXECUTABLE_RUNNER_VENDORS.filter((vendor) => probe(runnerSelection_1.RUNNER_VENDOR_BINARIES[vendor])).map((vendor) => this.adapterFor(vendor)),
458
+ source: 'probe',
459
+ strict: false,
460
+ };
461
+ }
462
+ withFallbackCandidates(vendor, probe) {
463
+ const primary = this.adapterFor(vendor);
464
+ if (!(this.options.crossVendorFallback?.() ?? false))
465
+ return [primary];
466
+ const rest = runnerSelection_1.EXECUTABLE_RUNNER_VENDORS.filter((candidate) => candidate !== vendor && probe(runnerSelection_1.RUNNER_VENDOR_BINARIES[candidate])).map((candidate) => this.adapterFor(candidate));
467
+ return [primary, ...rest];
468
+ }
469
+ adapterFor(vendor) {
470
+ return (0, exports.adapterForVendor)(vendor);
471
+ }
472
+ /** Tier the probe runs with — cheapest per vendor, with the same model
473
+ * override precedence a real worker gets (`RIVET_EXECUTOR_MODEL` env >
474
+ * request `--worker-model` > `workerModels.<vendor>` config) so the probe
475
+ * exercises the exact model a real worker would run, not the CLI
476
+ * default. */
477
+ probeTierFor(adapter, selection) {
478
+ const base = exports.PREFLIGHT_TIERS[adapter.vendor];
479
+ const env = this.options.env ?? process.env;
480
+ // The request model applies only to the vendor it was requested with —
481
+ // probing a cross-vendor fallback candidate with another CLI's model id
482
+ // would wrongly fail the fallback.
483
+ const requestModel = selection?.model &&
484
+ (!selection.provider || selection.provider === adapter.vendor)
485
+ ? selection.model
486
+ : undefined;
487
+ const model = env.RIVET_EXECUTOR_MODEL ??
488
+ requestModel ??
489
+ this.options.configuredModelFor?.(adapter.vendor);
490
+ return model ? { ...base, model } : base;
491
+ }
492
+ async preflight(adapter, signal, selection) {
493
+ const tier = this.probeTierFor(adapter, selection);
494
+ // Cache key includes the binary and resolved model, not just the vendor:
495
+ // `workerModels.<vendor>` is read lazily, so a config edit while the
496
+ // server is live must re-probe rather than ride a stale healthy result
497
+ // for a model preflight never exercised.
498
+ // A request-pinned model must never silently degrade: if the user asked
499
+ // for a specific model and it fails preflight, the honest outcome is
500
+ // unhealthy (→ host path with the reason), not a fallback-model run the
501
+ // user didn't pick. resolve() only passes `selection` through when the
502
+ // pin applies to this adapter, so its presence is the signal. Pinned
503
+ // probes carry their own cache identity (see probeCacheKey), so pinned
504
+ // and ordinary resolves never share verdicts or in-flight probes.
505
+ const requestPinnedModel = Boolean(selection?.model);
506
+ const key = probeCacheKey(adapter, tier, requestPinnedModel);
507
+ const cached = this.health.get(key);
508
+ // Healthy results are reused; unhealthy results are re-probed (each new
509
+ // resolve gives a fixed vendor one fresh chance).
510
+ if (cached?.healthy)
511
+ return cached;
512
+ const inFlight = this.inFlight.get(key);
513
+ if (inFlight)
514
+ return inFlight;
515
+ const startedAt = Date.now();
516
+ const probePromise = (requestPinnedModel
517
+ ? this.runProbe(adapter, tier, signal)
518
+ : this.probeWithModelFallback(adapter, tier, signal))
519
+ .then((outcome) => {
520
+ this.health.set(key, outcome);
521
+ // Telemetry gets the coarse category only: the raw reason is built
522
+ // from stderr/spawn errors and can carry local paths or account
523
+ // fragments. Full detail stays in server.log and run feedback.
524
+ this.options.telemetry?.track('agent_variants.executor_preflight', {
525
+ source: 'mcp',
526
+ vendor: adapter.vendor,
527
+ healthy: outcome.healthy,
528
+ durationMs: Date.now() - startedAt,
529
+ reason: outcome.healthy ? null : outcome.category,
530
+ });
531
+ if (!outcome.healthy) {
532
+ log.warn(`worker runtime preflight failed for ${adapter.vendor} ` +
533
+ `(${adapter.binary}): ${outcome.reason}`);
534
+ }
535
+ return outcome;
536
+ })
537
+ .finally(() => {
538
+ this.inFlight.delete(key);
539
+ });
540
+ this.inFlight.set(key, probePromise);
541
+ return probePromise;
542
+ }
543
+ /** One probe — plus, for claude (the only vendor with a structured
544
+ * fallback-model rung), a second probe on the fallback model when the
545
+ * primary MODEL is what failed. A healthy fallback probe marks the
546
+ * runtime healthy under the primary identity: real workers still start
547
+ * on the primary tier and degrade per item via the executor's one-shot
548
+ * fallback retry, exactly the pre-preflight behavior. */
549
+ async probeWithModelFallback(adapter, tier, signal) {
550
+ const outcome = await this.runProbe(adapter, tier, signal);
551
+ const fallback = this.options.probeModelFallback;
552
+ if (outcome.healthy ||
553
+ !fallback ||
554
+ !adapter.supportsModelFallback ||
555
+ outcome.category !== 'exit_nonzero') {
556
+ return outcome;
557
+ }
558
+ const primaryModel = 'model' in tier ? tier.model : undefined;
559
+ const fallbackModel = fallback.fallbackModel();
560
+ if (!primaryModel ||
561
+ !fallbackModel ||
562
+ fallbackModel === primaryModel ||
563
+ !fallback.isModelAvailabilityFailure({
564
+ message: outcome.reason,
565
+ ...(outcome.apiErrorStatus !== undefined
566
+ ? { apiErrorStatus: outcome.apiErrorStatus }
567
+ : {}),
568
+ model: primaryModel,
569
+ })) {
570
+ return outcome;
571
+ }
572
+ log.warn(`worker runtime preflight: ${adapter.vendor} model '${primaryModel}' ` +
573
+ `unavailable — probing fallback model '${fallbackModel}'`);
574
+ const fallbackOutcome = await this.runProbe(adapter, { ...tier, model: fallbackModel }, signal);
575
+ return fallbackOutcome.healthy ? fallbackOutcome : outcome;
576
+ }
577
+ runProbe(adapter, tier, signal) {
578
+ const timeoutMs = this.options.probeTimeoutMs ??
579
+ Number((this.options.env ?? process.env)
580
+ .RIVET_EXECUTOR_PREFLIGHT_TIMEOUT_MS ?? DEFAULT_PREFLIGHT_TIMEOUT_MS);
581
+ const spawnProbe = this.options.spawnProbe ?? defaultPreflightSpawn;
582
+ const args = adapter.buildArgs(tier);
583
+ if (adapter.promptDelivery === 'argv')
584
+ args.push(PREFLIGHT_PROMPT);
585
+ return new Promise((resolve) => {
586
+ // A signal that aborted before we attached would otherwise leave the
587
+ // probe running to its full timeout — check up front.
588
+ if (signal?.aborted) {
589
+ resolve({
590
+ healthy: false,
591
+ reason: 'probe aborted (run cancelled)',
592
+ category: 'aborted',
593
+ });
594
+ return;
595
+ }
596
+ let child;
597
+ try {
598
+ child = spawnProbe({
599
+ command: adapter.binary,
600
+ args,
601
+ cwd: os_1.default.tmpdir(),
602
+ env: adapter.spawnEnv(),
603
+ });
604
+ }
605
+ catch (error) {
606
+ resolve({
607
+ healthy: false,
608
+ reason: error instanceof Error ? error.message : String(error),
609
+ category: 'spawn_error',
610
+ });
611
+ return;
612
+ }
613
+ let settled = false;
614
+ let stderrTail = '';
615
+ let stdoutTail = '';
616
+ const settle = (outcome) => {
617
+ if (settled)
618
+ return;
619
+ settled = true;
620
+ clearTimeout(timer);
621
+ signal?.removeEventListener('abort', onAbort);
622
+ resolve(outcome);
623
+ };
624
+ const onAbort = () => {
625
+ killProcessTree(child.pid);
626
+ settle({
627
+ healthy: false,
628
+ reason: 'probe aborted (run cancelled)',
629
+ category: 'aborted',
630
+ });
631
+ };
632
+ const timer = setTimeout(() => {
633
+ killProcessTree(child.pid);
634
+ settle({
635
+ healthy: false,
636
+ reason: `probe timed out after ${timeoutMs}ms`,
637
+ category: 'timeout',
638
+ });
639
+ }, timeoutMs);
640
+ // Note: a dedup-shared probe can be aborted by the first joiner's
641
+ // signal. Accepted: the outcome is cached as unhealthy and the next
642
+ // resolve re-probes.
643
+ signal?.addEventListener('abort', onAbort, { once: true });
644
+ child.stderr?.on('data', (chunk) => {
645
+ stderrTail = (stderrTail + chunk.toString()).slice(-2000);
646
+ });
647
+ child.stdout?.on('data', (chunk) => {
648
+ stdoutTail = (stdoutTail + chunk.toString()).slice(-16384);
649
+ });
650
+ child.on('error', (error) => {
651
+ settle({ healthy: false, reason: error.message, category: 'spawn_error' });
652
+ });
653
+ child.on('close', (code) => {
654
+ if (code === 0) {
655
+ settle({ healthy: true });
656
+ return;
657
+ }
658
+ const { resultText, apiErrorStatus } = adapter.parseResult(stdoutTail);
659
+ const stderrDetail = sanitizeProbeText(stderrTail);
660
+ // A logged-out CLI often reports nothing on stderr and no parseable
661
+ // result — just a sign-in splash on stdout. Fall back to that tail
662
+ // so the reason is never an empty "probe exited 1: ".
663
+ const stdoutDetail = sanitizeProbeText(stdoutTail);
664
+ const detail = stderrDetail || (resultText ? '' : stdoutDetail);
665
+ const loginRequired = LOGIN_MARKERS.test(`${stderrDetail}\n${resultText ?? ''}\n${stdoutDetail}`);
666
+ // Once diagnosed as a login problem the hint IS the reason — the raw
667
+ // output is a sign-in banner (ASCII art on cursor-agent) that only
668
+ // buries the actionable part.
669
+ settle(loginRequired
670
+ ? {
671
+ healthy: false,
672
+ category: 'login_required',
673
+ reason: `${adapter.binary} is ${VENDOR_LOGIN_HINTS[adapter.vendor]} (probe exited ${code})`,
674
+ }
675
+ : {
676
+ healthy: false,
677
+ category: 'exit_nonzero',
678
+ ...(apiErrorStatus !== undefined ? { apiErrorStatus } : {}),
679
+ reason: `probe exited ${code}: ${detail.slice(-400)}${resultText ? ` | result: ${resultText.slice(-500)}` : ''}`,
680
+ });
681
+ });
682
+ // Always close stdin: argv-delivery CLIs (cursor-agent print mode) can
683
+ // block on an open stdin pipe — the real worker spawn ends stdin
684
+ // unconditionally for the same reason.
685
+ if (adapter.promptDelivery === 'stdin') {
686
+ child.stdin?.write(PREFLIGHT_PROMPT);
687
+ }
688
+ child.stdin?.end();
689
+ });
690
+ }
691
+ }
692
+ exports.WorkerRuntimeResolver = WorkerRuntimeResolver;
693
+ /** One-line human summary of a decision, for run feedback and server.log:
694
+ * `codex` / `host path (claude: probe exited 1: …)`. */
695
+ const describeWorkerRuntimeDecision = (decision) => {
696
+ if (decision.mode === 'executor') {
697
+ return decision.adapter.vendor;
698
+ }
699
+ const reasons = decision.diagnostics
700
+ .map((d) => `${d.vendor}: ${d.reason}`)
701
+ .join('; ');
702
+ return `host path${reasons ? ` (${reasons})` : ' (no worker runtime installed)'}`;
703
+ };
704
+ exports.describeWorkerRuntimeDecision = describeWorkerRuntimeDecision;
705
+ //# sourceMappingURL=workerRuntime.js.map