@wrenai/genbi 0.0.2 → 0.0.4

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/README.md CHANGED
@@ -25,7 +25,7 @@ the rest:
25
25
 
26
26
  ## Developer preview
27
27
 
28
- `@wrenai/genbi` is published at `0.0.2`. Nothing here is under a compatibility
28
+ `@wrenai/genbi` is published at `0.0.4`. Nothing here is under a compatibility
29
29
  commitment yet — commands, environment variables and interfaces move without
30
30
  notice. The Codex runtime is supported in code but has not been exercised end
31
31
  to end; Claude is the one that is. See
@@ -31,6 +31,16 @@ import { isWarbleSiblingCheckoutDevModeEnabled, resolveInstalledPackageBin } fro
31
31
  */
32
32
  export async function resolveWarbleBinary(explicit) {
33
33
  if (explicit !== undefined) {
34
+ // A value with no path separator is a command name, not a file path, and is
35
+ // resolved on PATH — the same reading the native-session preflight gives it.
36
+ // Requiring it to exist relative to the working directory made `"warble"` an
37
+ // explicit value that could never resolve, while the preflight found it and
38
+ // reported the producer healthy: one string, two answers.
39
+ if (!explicit.includes(path.sep) && !path.isAbsolute(explicit)) {
40
+ if (await isExecutableOnPath(explicit))
41
+ return explicit;
42
+ throw new WarbleBinaryNotFoundError([`explicit warbleBin "${explicit}" is not on PATH`]);
43
+ }
34
44
  if (!existsSync(explicit)) {
35
45
  throw new WarbleBinaryNotFoundError([`explicit warbleBin "${explicit}" does not exist`]);
36
46
  }
@@ -105,6 +105,7 @@ import { createApp } from "./app.js";
105
105
  import { recoverBootstrapProjectBinding } from "./bootstrap-project-binding.js";
106
106
  import { createHarnessProfileSources } from "./harness-profile-sources.js";
107
107
  import { compileProfile, compileRawProfile } from "../harness/compile/index.js";
108
+ import { resolveWarbleBinary } from "../harness/compile/resolve-binary.js";
108
109
  import { toAuthChoiceFromRuntimeSettings } from "./auth-choice.js";
109
110
  import { Store } from "./db.js";
110
111
  import { resolveEnrichmentBinding, resolveProjectIdentity } from "./enrichment.js";
@@ -170,6 +171,22 @@ async function main() {
170
171
  // (server/turn.ts) and `server/spa.ts`.
171
172
  const candidateStaticDir = path.join(packageRoot, "dist");
172
173
  const staticDir = existsSync(path.join(candidateStaticDir, "index.html")) ? candidateStaticDir : undefined;
174
+ // Resolve the Warble binary once, here, and hand the resolved path to every
175
+ // consumer. Passing the bare name `"warble"` looked like a harmless default and
176
+ // was not: `resolveWarbleBinary` treats any value it is given as an explicit
177
+ // path and only searches when given nothing, so that default disabled the one
178
+ // lookup that finds the `@warble/cli` an install actually ships. It also made
179
+ // two resolvers disagree about the same string — the native preflight resolves
180
+ // a bare name through PATH and reported the producer healthy, while every
181
+ // compile failed.
182
+ const resolvedWarbleBin = await resolveWarbleBinary(flags.warbleBin).catch((error) => {
183
+ // Not fatal: the app still starts, and readiness reports the reason per
184
+ // vendor rather than the process dying at boot over a feature the user may
185
+ // not reach.
186
+ process.stderr.write(`warning: ${error instanceof Error ? error.message : String(error)}\n`);
187
+ return undefined;
188
+ });
189
+ const warbleBinOption = resolvedWarbleBin !== undefined ? { warbleBin: resolvedWarbleBin } : {};
173
190
  const profileSource = flags.profile ?? resolveDefaultProfileSource();
174
191
  // Frozen at boot: GET /api/harness accepts only a purpose and must never
175
192
  // inherit a caller-supplied RouteOptions.profileSource.
@@ -459,7 +476,7 @@ async function main() {
459
476
  binding,
460
477
  artifactRoot,
461
478
  materializationState: nativeMaterializationState,
462
- materialize: () => dispatchInteractiveArtifacts({ warbleBin: flags.warbleBin ?? "warble", irPath: enrichIrPath, target, cwd: artifactRoot, boundProject: binding.path, materializationState: nativeMaterializationState }),
479
+ materialize: () => dispatchInteractiveArtifacts({ warbleBin: resolvedWarbleBin ?? "warble", irPath: enrichIrPath, target, cwd: artifactRoot, boundProject: binding.path, materializationState: nativeMaterializationState }),
463
480
  }, { getCurrentBinding: currentInteractiveBinding });
464
481
  }
465
482
  const nativeMcpUrl = `http://127.0.0.1:${port}/api/native-sessions/mcp`;
@@ -489,11 +506,13 @@ async function main() {
489
506
  resolveDispatchIr: async (purpose, binding) => {
490
507
  const profileSource = harnessProfileSources[purpose];
491
508
  const compiled = purpose === "setup" || binding === undefined
492
- ? await compileRawProfile({ profileSource, mode: "native", warbleBin: flags.warbleBin ?? "warble" })
493
- : await compileProfile({ profileSource, userProject: binding.path, mode: "native", warbleBin: flags.warbleBin ?? "warble" });
509
+ ? await compileRawProfile({ profileSource, mode: "native", ...warbleBinOption })
510
+ : await compileProfile({ profileSource, userProject: binding.path, mode: "native", ...warbleBinOption });
494
511
  return compiled.irPath;
495
512
  },
496
- warbleBin: flags.warbleBin ?? "warble",
513
+ // The service's option is a required string; when resolution failed the bare
514
+ // name is what the preflight will report as unresolvable, with its reason.
515
+ warbleBin: resolvedWarbleBin ?? "warble",
497
516
  ...(process.env["WREN_HARNESS_WREN_SHIM"] !== undefined ? { wrenShim: process.env["WREN_HARNESS_WREN_SHIM"] } : {}),
498
517
  terminalHostAvailable: async () => {
499
518
  try {
@@ -95,6 +95,17 @@ export interface NativeVendorReadiness {
95
95
  export interface NativeProducerReadiness {
96
96
  readonly available: boolean;
97
97
  readonly category?: "native_session_producer_incompatible";
98
+ /**
99
+ * Why the preflight rejected the producer. Safe to send: every field is a
100
+ * fixed string plus a content digest of the resolved binary — the phase name,
101
+ * a `result=` token, and (for a Wren runtime rejection) one of that resolver's
102
+ * fixed sentences. None of them interpolate a path or process output.
103
+ *
104
+ * It is here because the category alone named a state without a cause, so an
105
+ * operator reading the Sessions view could not tell a missing binary from an
106
+ * incompatible one, and neither could a bug report.
107
+ */
108
+ readonly diagnostic?: string;
98
109
  }
99
110
  export interface NativePurposeReadiness {
100
111
  readonly scopeKind: "bootstrap" | "bound_project";
@@ -196,13 +207,30 @@ export interface NativeProducerIdentity {
196
207
  readonly executable: string;
197
208
  readonly identity: string;
198
209
  }
210
+ /** One vendor's own preflight outcome. A vendor nobody selected must not speak for one that was. */
211
+ export interface NativeProducerVendorProbe {
212
+ readonly available: boolean;
213
+ /** Browser-safe: a fixed sentence, never a path or process output. */
214
+ readonly reason?: string;
215
+ /** Fixed fields plus a content digest; never a command path or process output. */
216
+ readonly diagnostic: string;
217
+ }
199
218
  export interface NativeProducerPreflightResult {
219
+ /** True when at least one vendor is usable — never a verdict on the vendor in use. */
200
220
  readonly available: boolean;
201
221
  readonly category?: typeof NATIVE_PRODUCER_CATEGORY;
202
222
  /** Server-only resolved executable and content identity for the eventual launch. */
203
223
  readonly producer?: NativeProducerIdentity;
204
224
  /** Server-only: fixed fields plus a content digest; never a command path or process output. */
205
225
  readonly diagnostic: string;
226
+ /**
227
+ * Per-vendor outcomes. The preflight probes every vendor so that readiness can
228
+ * answer for the one actually configured: the Codex leg requires a native Wren
229
+ * runtime the resolver can trace to the server's own repository, which an
230
+ * installed package can never satisfy, and a single boolean over both vendors
231
+ * let that failure take Claude down with it.
232
+ */
233
+ readonly vendors: Readonly<Record<NativeVendor, NativeProducerVendorProbe>>;
206
234
  }
207
235
  /**
208
236
  * Exercises the exact, file-only native dispatch boundary against the
@@ -7,6 +7,7 @@ import { execFileSync, spawn } from "node:child_process";
7
7
  import { accessSync, chmodSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import os from "node:os";
9
9
  import path from "node:path";
10
+ import { WarbleBinaryNotFoundError } from "../harness/compile/errors.js";
10
11
  import { InteractiveLaunchError, NO_INTERACTIVE_TERMINAL_LEASES, interactiveExecutableAvailable } from "./interactive-terminal.js";
11
12
  import { newId } from "./db.js";
12
13
  import { NATIVE_MCP_CREDENTIAL_ENV_VAR } from "./native-artifacts.js";
@@ -39,11 +40,13 @@ const CODEX_NATIVE_PERMISSION_HEADER = "[permissions.warble_native_wren.filesyst
39
40
  const PROVIDER_SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
40
41
  const CODEX_RESUME_HANDLE_FILE = path.join(".warble", "codex-thread-id");
41
42
  function logNativeLaunchFailure(purpose, vendor, phase, error) {
42
- const category = error instanceof InteractiveLaunchError ? "rejected" : "unexpected";
43
- // Keep the precise boundary server-side without logging exception text,
44
- // paths, credentials, argv, or other launch material. Browser/API callers
45
- // continue through nativeSessionLaunchFailure's closed public projection.
46
- console.error(`[native-sessions] launch failed purpose=${purpose} vendor=${vendor} phase=${phase} category=${category}`);
43
+ const rejected = error instanceof InteractiveLaunchError;
44
+ // Exception text, paths, credentials and argv stay out of the log for an
45
+ // unexpected error, which may carry any of them. A rejection carries a
46
+ // sentence authored here, and suppressing that turned every diagnosis into a
47
+ // guess: an operator saw a phase and a category and no cause.
48
+ const detail = rejected ? ` reason=${error.message}` : "";
49
+ console.error(`[native-sessions] launch failed purpose=${purpose} vendor=${vendor} phase=${phase} category=${rejected ? "rejected" : "unexpected"}${detail}`);
47
50
  }
48
51
  /**
49
52
  * The producer deliberately sees only a sealed binding identity, never a caller-supplied path.
@@ -310,6 +313,24 @@ function configuredIrPath(irPath) {
310
313
  return false;
311
314
  }
312
315
  }
316
+ /**
317
+ * Why a vendor's preflight rejected the producer. Each is a fixed sentence
318
+ * chosen here — none is built from a path, a command, or process output — so
319
+ * `nativeSessionLaunchFailure` may pass them through unchanged.
320
+ */
321
+ const NATIVE_PRODUCER_REASONS = new Set([
322
+ "the Warble binary could not be resolved",
323
+ "the Warble binary could not be identified",
324
+ "the Warble binary could not be started",
325
+ "the Warble binary did not respond",
326
+ "the Warble binary rejected the dispatch probe",
327
+ "the Warble binary does not support the native session flags this build requires",
328
+ "the Warble binary could not be started for this vendor",
329
+ "the Warble binary did not respond for this vendor",
330
+ "Warble could not dispatch this profile for this vendor",
331
+ "Warble emitted a launch spec this build cannot run",
332
+ "the preflight could not create its temporary workspace",
333
+ ]);
313
334
  /** A deliberately small public classification; never expose process or filesystem detail. */
314
335
  export function nativeSessionLaunchFailure(error) {
315
336
  const message = error instanceof Error ? error.message : "";
@@ -319,14 +340,31 @@ export function nativeSessionLaunchFailure(error) {
319
340
  return message;
320
341
  if (message === "native session materialization prerequisites are unavailable")
321
342
  return message;
322
- if (message === "native Wren runtime is unavailable")
323
- return "native session materialization prerequisites are unavailable";
343
+ // Every NativeWrenRuntimeError message is `native Wren runtime is unavailable: `
344
+ // followed by one of that resolver's fixed literals; not one of them
345
+ // interpolates a path or process output, so the whole family may pass through.
346
+ // Matching the bare sentence, as this did, stopped matching anything once the
347
+ // resolver started naming which link in the chain broke.
348
+ if (message.startsWith("native Wren runtime is unavailable"))
349
+ return message;
350
+ if (NATIVE_PRODUCER_REASONS.has(message))
351
+ return message;
324
352
  if (message === "native session producer is incompatible")
325
353
  return message;
326
354
  if (message === "native session launch artifacts are unavailable" || message === "native session launch specification is invalid" || message === "native session launch specification is incompatible")
327
355
  return "native session launch artifacts are unavailable";
328
356
  if (message === "native session launch paths are unavailable" || message === "native session launch paths are outside its server-owned scope")
329
357
  return "native session launch artifacts are unavailable";
358
+ // Every InteractiveLaunchError message is authored in this file: a fixed
359
+ // sentence, a fixed profile name, or a reason readiness already publishes.
360
+ // None is built from a path, argv, or process output. Falling through to the
361
+ // generic string threw away the cause of eighteen distinct failures, which is
362
+ // what left "native session launch failed" as the only thing a user could see.
363
+ if (error instanceof InteractiveLaunchError)
364
+ return message;
365
+ // This one is NOT safe to forward: its text embeds the configured binary path.
366
+ if (error instanceof WarbleBinaryNotFoundError)
367
+ return "the Warble binary could not be resolved";
330
368
  return "native session launch failed";
331
369
  }
332
370
  const NATIVE_PRODUCER_CATEGORY = "native_session_producer_incompatible";
@@ -348,12 +386,25 @@ const NATIVE_HELP_MARKERS = [
348
386
  "--native-mcp",
349
387
  "--out",
350
388
  ];
351
- function nativeProducerResult(available, identity, phase, detail, producer) {
389
+ function producerDiagnostic(identity, phase, detail) {
390
+ return `identity=${identity ?? "unavailable"} phase=${phase} ${detail}`;
391
+ }
392
+ /** A failure before any vendor-specific work rules out every vendor, for the same reason. */
393
+ function everyVendorUnavailable(probe) {
394
+ const vendors = {};
395
+ for (const vendor of NATIVE_PROBE_TARGETS)
396
+ vendors[vendor] = probe;
397
+ return vendors;
398
+ }
399
+ /** A shared-phase outcome: it decided nothing vendor-specific, so it speaks for all of them. */
400
+ function nativeProducerResult(available, identity, phase, detail, reason, producer) {
401
+ const diagnostic = producerDiagnostic(identity, phase, detail);
352
402
  return {
353
403
  available,
354
404
  ...(available ? {} : { category: NATIVE_PRODUCER_CATEGORY }),
355
- ...(available && producer ? { producer } : {}),
356
- diagnostic: `identity=${identity ?? "unavailable"} phase=${phase} ${detail}`,
405
+ ...(producer ? { producer } : {}),
406
+ diagnostic,
407
+ vendors: everyVendorUnavailable(available ? { available: true, diagnostic } : { available: false, reason, diagnostic }),
357
408
  };
358
409
  }
359
410
  function executableIdentity(executable) {
@@ -439,26 +490,34 @@ function runNativeProbe(command, args) {
439
490
  export async function probeNativeSessionProducer(input) {
440
491
  const executable = localExecutablePath(input.warbleBin);
441
492
  if (!executable)
442
- return nativeProducerResult(false, undefined, "resolve", "result=binary_not_found");
493
+ return nativeProducerResult(false, undefined, "resolve", "result=binary_not_found", "the Warble binary could not be resolved");
443
494
  const identity = executableIdentity(executable);
444
495
  if (!identity)
445
- return nativeProducerResult(false, undefined, "identity", "result=identity_unavailable");
496
+ return nativeProducerResult(false, undefined, "identity", "result=identity_unavailable", "the Warble binary could not be identified");
446
497
  const producer = { executable, identity };
447
498
  const help = await runNativeProbe(executable, ["dispatch", "--help"]);
448
499
  if (help.spawnFailed)
449
- return nativeProducerResult(false, identity, "dispatch_help", "result=spawn_failed");
500
+ return nativeProducerResult(false, identity, "dispatch_help", "result=spawn_failed", "the Warble binary could not be started");
450
501
  if (help.timedOut)
451
- return nativeProducerResult(false, identity, "dispatch_help", "result=timed_out");
502
+ return nativeProducerResult(false, identity, "dispatch_help", "result=timed_out", "the Warble binary did not respond");
452
503
  if (help.exitCode !== 0)
453
- return nativeProducerResult(false, identity, "dispatch_help", `result=exit_code_${help.exitCode ?? "signal"}`);
504
+ return nativeProducerResult(false, identity, "dispatch_help", `result=exit_code_${help.exitCode ?? "signal"}`, "the Warble binary rejected the dispatch probe");
454
505
  const missingMarkers = NATIVE_HELP_MARKERS.filter((marker) => !help.output.includes(marker));
455
506
  if (missingMarkers.length > 0)
456
- return nativeProducerResult(false, identity, "dispatch_help", `result=missing_markers_${missingMarkers.join(",")}`);
457
- for (const purpose of NATIVE_PURPOSES) {
458
- const irPath = input.irPaths[purpose];
459
- if (!irPath || !configuredIrPath(irPath))
460
- continue;
461
- for (const vendor of NATIVE_PROBE_TARGETS) {
507
+ return nativeProducerResult(false, identity, "dispatch_help", `result=missing_markers_${missingMarkers.join(",")}`, "the Warble binary does not support the native session flags this build requires");
508
+ // Vendor is the outer loop on purpose. It used to be the inner one, and the
509
+ // first failing pair returned for the whole preflight — so a Codex rejection
510
+ // reported the producer incompatible even for a Claude-only install, which is
511
+ // exactly what an installed package produces.
512
+ const vendors = {};
513
+ for (const vendor of NATIVE_PROBE_TARGETS) {
514
+ let outcome = { available: true, diagnostic: producerDiagnostic(identity, `complete_${vendor}`, "result=compatible") };
515
+ for (const purpose of NATIVE_PURPOSES) {
516
+ const irPath = input.irPaths[purpose];
517
+ if (!irPath || !configuredIrPath(irPath))
518
+ continue;
519
+ const phase = `dispatch_${purpose}_${vendor}`;
520
+ const fail = (detail, reason) => ({ available: false, reason, diagnostic: producerDiagnostic(identity, phase, detail) });
462
521
  let root;
463
522
  try {
464
523
  root = mkdtempSync(path.join(os.tmpdir(), "genbi-native-preflight-"));
@@ -478,32 +537,54 @@ export async function probeNativeSessionProducer(input) {
478
537
  writeFileSync(scopePath, JSON.stringify({ version: "3", kind: NATIVE_DISPATCH_REGISTRY[purpose].scopeKind, scope_id: scopeId, cwd: outputRoot, entry: nativeEntryFor(purpose, vendor), ...(purpose === "setup" ? { bootstrap_root: bootstrapRoot } : {}), ...(wrenRuntime ? { wren_runtime: wrenRuntime } : {}), ...(binding ? { binding } : {}) }), { encoding: "utf8", mode: 0o600, flag: "wx" });
479
538
  writeFileSync(mcpPath, JSON.stringify({ version: "1", url: "http://127.0.0.1:0/api/native-sessions/mcp", credential: "native-preflight-nonsecret" }), { encoding: "utf8", mode: 0o600, flag: "wx" });
480
539
  const dispatched = await runNativeProbe(executable, ["dispatch", irPath, "--target", targetForProvider(vendor), "--purpose", purpose, "--native-scope", scopePath, "--native-mcp", mcpPath, "--out", outputRoot]);
481
- const phase = `dispatch_${purpose}_${vendor}`;
482
- if (dispatched.spawnFailed)
483
- return nativeProducerResult(false, identity, phase, "result=spawn_failed");
484
- if (dispatched.timedOut)
485
- return nativeProducerResult(false, identity, phase, "result=timed_out");
486
- if (dispatched.exitCode !== 0)
487
- return nativeProducerResult(false, identity, phase, `result=exit_code_${dispatched.exitCode ?? "signal"}`);
540
+ if (dispatched.spawnFailed) {
541
+ outcome = fail("result=spawn_failed", "the Warble binary could not be started for this vendor");
542
+ break;
543
+ }
544
+ if (dispatched.timedOut) {
545
+ outcome = fail("result=timed_out", "the Warble binary did not respond for this vendor");
546
+ break;
547
+ }
548
+ if (dispatched.exitCode !== 0) {
549
+ outcome = fail(`result=exit_code_${dispatched.exitCode ?? "signal"}`, "Warble could not dispatch this profile for this vendor");
550
+ break;
551
+ }
488
552
  try {
489
553
  readNativeLaunchSpec(outputRoot, purpose, vendor, scopeId, undefined, { version: "1", url: "http://127.0.0.1:0/api/native-sessions/mcp", credential: "native-preflight-nonsecret" });
490
554
  }
491
555
  catch {
492
- return nativeProducerResult(false, identity, phase, "result=launch_spec_incompatible");
556
+ outcome = fail("result=launch_spec_incompatible", "Warble emitted a launch spec this build cannot run");
557
+ break;
493
558
  }
494
559
  }
495
560
  catch (error) {
496
- if (error instanceof NativeWrenRuntimeError)
497
- return nativeProducerResult(false, identity, `dispatch_${purpose}_${vendor}`, `result=wren_runtime_unavailable (${error.message})`);
498
- return nativeProducerResult(false, identity, `dispatch_${purpose}_${vendor}`, "result=preflight_workspace_unavailable");
561
+ outcome = error instanceof NativeWrenRuntimeError
562
+ ? fail(`result=wren_runtime_unavailable (${error.message})`, error.message)
563
+ : fail("result=preflight_workspace_unavailable", "the preflight could not create its temporary workspace");
564
+ break;
499
565
  }
500
566
  finally {
501
567
  if (root)
502
568
  rmSync(root, { recursive: true, force: true });
503
569
  }
504
570
  }
505
- }
506
- return nativeProducerResult(true, identity, "complete", "result=compatible", producer);
571
+ vendors[vendor] = outcome;
572
+ }
573
+ const usable = NATIVE_PROBE_TARGETS.filter((vendor) => vendors[vendor].available);
574
+ const available = usable.length > 0;
575
+ // When nothing is usable the summary must still carry each vendor's own cause;
576
+ // collapsing them to "no_compatible_vendor" would lose the only line that says
577
+ // what actually broke.
578
+ const vendorResults = NATIVE_PROBE_TARGETS
579
+ .map((vendor) => `${vendor}=${vendors[vendor].diagnostic.split(" result=")[1] ?? "unknown"}`)
580
+ .join(" ");
581
+ return {
582
+ available,
583
+ ...(available ? {} : { category: NATIVE_PRODUCER_CATEGORY }),
584
+ producer,
585
+ diagnostic: producerDiagnostic(identity, "complete", available ? `result=compatible_${usable.join("+")}` : `result=no_compatible_vendor ${vendorResults}`),
586
+ vendors,
587
+ };
507
588
  }
508
589
  function parseNativeSetupRecoveryReport(value) {
509
590
  const report = record(value);
@@ -1139,6 +1220,10 @@ export class NativeSessionService {
1139
1220
  ? nativeSessionStateBaseAvailableForProject(this.materializationState(), configuredRoot)
1140
1221
  : purpose === "setup" && projectAvailable && nativeSessionStateBaseAvailable(this.materializationState());
1141
1222
  const workspaceUnavailable = !projectAvailable || !stateAvailable;
1223
+ // Only the configured vendor's own probe may block this purpose. Probing
1224
+ // both and collapsing them into one boolean let an unselected vendor veto
1225
+ // the selected one.
1226
+ const selectedProducer = producer && dispatch ? producer.vendors[dispatch.provider] : undefined;
1142
1227
  const reason = runtimeCorrection
1143
1228
  ?? (!dispatch && !legacyFixture
1144
1229
  ? "native sessions require a saved Runtime & authentication binding"
@@ -1146,8 +1231,8 @@ export class NativeSessionService {
1146
1231
  ? purpose === "setup" ? "native setup sessions require a workspace root" : "native sessions require a current bound project"
1147
1232
  : workspaceUnavailable
1148
1233
  ? "native session workspace is unavailable"
1149
- : producer && !producer.available
1150
- ? "native session producer is incompatible"
1234
+ : selectedProducer && !selectedProducer.available
1235
+ ? selectedProducer.reason ?? "native session producer is incompatible"
1151
1236
  : !irPath || !(this.options.dispatch !== undefined || configuredIrPath(irPath)) || !legacyProducerAvailable()
1152
1237
  ? "native session materialization prerequisites are unavailable"
1153
1238
  : mcpReadiness && !mcpReadiness.available
@@ -1160,6 +1245,17 @@ export class NativeSessionService {
1160
1245
  ? `the ${executable} interactive CLI is not available on this machine`
1161
1246
  : undefined);
1162
1247
  const vendorReadiness = {};
1248
+ // Production readiness now reports every vendor, so the UI can say which
1249
+ // one is unusable and why instead of only that "the producer" is.
1250
+ if (!legacyFixture && producer)
1251
+ for (const vendor of NATIVE_VENDORS) {
1252
+ const probe = producer.vendors[vendor];
1253
+ vendorReadiness[vendor] = probe?.available === false
1254
+ ? { available: false, reason: probe.reason ?? "native session producer is incompatible" }
1255
+ : !executableAvailable(vendor)
1256
+ ? { available: false, reason: `the ${vendor === "claude" ? "claude" : "codex"} interactive CLI is not available on this machine` }
1257
+ : { available: true };
1258
+ }
1163
1259
  if (legacyFixture)
1164
1260
  for (const vendor of NATIVE_VENDORS) {
1165
1261
  const legacyExecutable = vendor === "claude" ? "claude" : "codex";
@@ -1174,7 +1270,16 @@ export class NativeSessionService {
1174
1270
  scopeKind,
1175
1271
  profile: definition.profile,
1176
1272
  ...(dispatch ? { target: dispatch.target, targetLabel: dispatch.targetLabel } : {}),
1177
- ...(producer ? { producer: producer.available ? { available: true } : { available: false, category: NATIVE_PRODUCER_CATEGORY } } : {}),
1273
+ // With a vendor selected this answers for that vendor; with none selected
1274
+ // it falls back to "is any vendor usable", rather than claiming health
1275
+ // no vendor could back.
1276
+ ...(producer
1277
+ ? {
1278
+ producer: (selectedProducer ? selectedProducer.available : producer.available)
1279
+ ? { available: true, diagnostic: (selectedProducer ?? producer).diagnostic }
1280
+ : { available: false, category: NATIVE_PRODUCER_CATEGORY, diagnostic: (selectedProducer ?? producer).diagnostic },
1281
+ }
1282
+ : {}),
1178
1283
  available: legacyAvailable,
1179
1284
  ...(unavailable ? { reason: unavailable } : {}),
1180
1285
  vendors: vendorReadiness,
@@ -1218,12 +1323,25 @@ export class NativeSessionService {
1218
1323
  // Compile for this binding rather than dispatching the profile's golden.
1219
1324
  // Falls back to the configured path only where no resolver is wired (the
1220
1325
  // fixture-mode tests), never silently in production — see `bin.ts`.
1221
- const irPath = this.options.resolveDispatchIr === undefined
1222
- ? configuredIr
1223
- : await this.options.resolveDispatchIr(purpose, binding);
1326
+ // Compiling for this binding runs before the launch try/catch below, so a
1327
+ // failure here reached the HTTP route unlogged and unclassified — the case
1328
+ // where the server printed nothing at all and the browser said only "native
1329
+ // session launch failed".
1330
+ let irPath;
1331
+ try {
1332
+ irPath = this.options.resolveDispatchIr === undefined
1333
+ ? configuredIr
1334
+ : await this.options.resolveDispatchIr(purpose, binding);
1335
+ }
1336
+ catch (error) {
1337
+ logNativeLaunchFailure(purpose, dispatchDefinition.provider, "dispatch_ir", error);
1338
+ throw error;
1339
+ }
1224
1340
  const producer = this.legacyFixtureMode() ? undefined : await this.productionProducerPreflight();
1225
- if (producer && (!producer.available || !producer.producer))
1226
- throw new InteractiveLaunchError("native session producer is incompatible");
1341
+ const launchProducer = producer ? producer.vendors[dispatchDefinition.provider] : undefined;
1342
+ if (producer && (!launchProducer?.available || !producer.producer)) {
1343
+ throw new InteractiveLaunchError(launchProducer?.reason ?? "native session producer is incompatible");
1344
+ }
1227
1345
  if (producer?.producer)
1228
1346
  assertNativeProducerIdentity(producer.producer);
1229
1347
  const scopeId = resumed?.row.scopeId ?? newId("native-scope");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrenai/genbi",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Wren GenBI — the web UI for WrenAI: ask questions of your data and get dashboards back",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [