@wrenai/genbi 0.0.2 → 0.0.3

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.3`. 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
@@ -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
@@ -310,6 +310,24 @@ function configuredIrPath(irPath) {
310
310
  return false;
311
311
  }
312
312
  }
313
+ /**
314
+ * Why a vendor's preflight rejected the producer. Each is a fixed sentence
315
+ * chosen here — none is built from a path, a command, or process output — so
316
+ * `nativeSessionLaunchFailure` may pass them through unchanged.
317
+ */
318
+ const NATIVE_PRODUCER_REASONS = new Set([
319
+ "the Warble binary could not be resolved",
320
+ "the Warble binary could not be identified",
321
+ "the Warble binary could not be started",
322
+ "the Warble binary did not respond",
323
+ "the Warble binary rejected the dispatch probe",
324
+ "the Warble binary does not support the native session flags this build requires",
325
+ "the Warble binary could not be started for this vendor",
326
+ "the Warble binary did not respond for this vendor",
327
+ "Warble could not dispatch this profile for this vendor",
328
+ "Warble emitted a launch spec this build cannot run",
329
+ "the preflight could not create its temporary workspace",
330
+ ]);
313
331
  /** A deliberately small public classification; never expose process or filesystem detail. */
314
332
  export function nativeSessionLaunchFailure(error) {
315
333
  const message = error instanceof Error ? error.message : "";
@@ -319,8 +337,15 @@ export function nativeSessionLaunchFailure(error) {
319
337
  return message;
320
338
  if (message === "native session materialization prerequisites are unavailable")
321
339
  return message;
322
- if (message === "native Wren runtime is unavailable")
323
- return "native session materialization prerequisites are unavailable";
340
+ // Every NativeWrenRuntimeError message is `native Wren runtime is unavailable: `
341
+ // followed by one of that resolver's fixed literals; not one of them
342
+ // interpolates a path or process output, so the whole family may pass through.
343
+ // Matching the bare sentence, as this did, stopped matching anything once the
344
+ // resolver started naming which link in the chain broke.
345
+ if (message.startsWith("native Wren runtime is unavailable"))
346
+ return message;
347
+ if (NATIVE_PRODUCER_REASONS.has(message))
348
+ return message;
324
349
  if (message === "native session producer is incompatible")
325
350
  return message;
326
351
  if (message === "native session launch artifacts are unavailable" || message === "native session launch specification is invalid" || message === "native session launch specification is incompatible")
@@ -348,12 +373,25 @@ const NATIVE_HELP_MARKERS = [
348
373
  "--native-mcp",
349
374
  "--out",
350
375
  ];
351
- function nativeProducerResult(available, identity, phase, detail, producer) {
376
+ function producerDiagnostic(identity, phase, detail) {
377
+ return `identity=${identity ?? "unavailable"} phase=${phase} ${detail}`;
378
+ }
379
+ /** A failure before any vendor-specific work rules out every vendor, for the same reason. */
380
+ function everyVendorUnavailable(probe) {
381
+ const vendors = {};
382
+ for (const vendor of NATIVE_PROBE_TARGETS)
383
+ vendors[vendor] = probe;
384
+ return vendors;
385
+ }
386
+ /** A shared-phase outcome: it decided nothing vendor-specific, so it speaks for all of them. */
387
+ function nativeProducerResult(available, identity, phase, detail, reason, producer) {
388
+ const diagnostic = producerDiagnostic(identity, phase, detail);
352
389
  return {
353
390
  available,
354
391
  ...(available ? {} : { category: NATIVE_PRODUCER_CATEGORY }),
355
- ...(available && producer ? { producer } : {}),
356
- diagnostic: `identity=${identity ?? "unavailable"} phase=${phase} ${detail}`,
392
+ ...(producer ? { producer } : {}),
393
+ diagnostic,
394
+ vendors: everyVendorUnavailable(available ? { available: true, diagnostic } : { available: false, reason, diagnostic }),
357
395
  };
358
396
  }
359
397
  function executableIdentity(executable) {
@@ -439,26 +477,34 @@ function runNativeProbe(command, args) {
439
477
  export async function probeNativeSessionProducer(input) {
440
478
  const executable = localExecutablePath(input.warbleBin);
441
479
  if (!executable)
442
- return nativeProducerResult(false, undefined, "resolve", "result=binary_not_found");
480
+ return nativeProducerResult(false, undefined, "resolve", "result=binary_not_found", "the Warble binary could not be resolved");
443
481
  const identity = executableIdentity(executable);
444
482
  if (!identity)
445
- return nativeProducerResult(false, undefined, "identity", "result=identity_unavailable");
483
+ return nativeProducerResult(false, undefined, "identity", "result=identity_unavailable", "the Warble binary could not be identified");
446
484
  const producer = { executable, identity };
447
485
  const help = await runNativeProbe(executable, ["dispatch", "--help"]);
448
486
  if (help.spawnFailed)
449
- return nativeProducerResult(false, identity, "dispatch_help", "result=spawn_failed");
487
+ return nativeProducerResult(false, identity, "dispatch_help", "result=spawn_failed", "the Warble binary could not be started");
450
488
  if (help.timedOut)
451
- return nativeProducerResult(false, identity, "dispatch_help", "result=timed_out");
489
+ return nativeProducerResult(false, identity, "dispatch_help", "result=timed_out", "the Warble binary did not respond");
452
490
  if (help.exitCode !== 0)
453
- return nativeProducerResult(false, identity, "dispatch_help", `result=exit_code_${help.exitCode ?? "signal"}`);
491
+ return nativeProducerResult(false, identity, "dispatch_help", `result=exit_code_${help.exitCode ?? "signal"}`, "the Warble binary rejected the dispatch probe");
454
492
  const missingMarkers = NATIVE_HELP_MARKERS.filter((marker) => !help.output.includes(marker));
455
493
  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) {
494
+ return nativeProducerResult(false, identity, "dispatch_help", `result=missing_markers_${missingMarkers.join(",")}`, "the Warble binary does not support the native session flags this build requires");
495
+ // Vendor is the outer loop on purpose. It used to be the inner one, and the
496
+ // first failing pair returned for the whole preflight — so a Codex rejection
497
+ // reported the producer incompatible even for a Claude-only install, which is
498
+ // exactly what an installed package produces.
499
+ const vendors = {};
500
+ for (const vendor of NATIVE_PROBE_TARGETS) {
501
+ let outcome = { available: true, diagnostic: producerDiagnostic(identity, `complete_${vendor}`, "result=compatible") };
502
+ for (const purpose of NATIVE_PURPOSES) {
503
+ const irPath = input.irPaths[purpose];
504
+ if (!irPath || !configuredIrPath(irPath))
505
+ continue;
506
+ const phase = `dispatch_${purpose}_${vendor}`;
507
+ const fail = (detail, reason) => ({ available: false, reason, diagnostic: producerDiagnostic(identity, phase, detail) });
462
508
  let root;
463
509
  try {
464
510
  root = mkdtempSync(path.join(os.tmpdir(), "genbi-native-preflight-"));
@@ -478,32 +524,54 @@ export async function probeNativeSessionProducer(input) {
478
524
  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
525
  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
526
  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"}`);
527
+ if (dispatched.spawnFailed) {
528
+ outcome = fail("result=spawn_failed", "the Warble binary could not be started for this vendor");
529
+ break;
530
+ }
531
+ if (dispatched.timedOut) {
532
+ outcome = fail("result=timed_out", "the Warble binary did not respond for this vendor");
533
+ break;
534
+ }
535
+ if (dispatched.exitCode !== 0) {
536
+ outcome = fail(`result=exit_code_${dispatched.exitCode ?? "signal"}`, "Warble could not dispatch this profile for this vendor");
537
+ break;
538
+ }
488
539
  try {
489
540
  readNativeLaunchSpec(outputRoot, purpose, vendor, scopeId, undefined, { version: "1", url: "http://127.0.0.1:0/api/native-sessions/mcp", credential: "native-preflight-nonsecret" });
490
541
  }
491
542
  catch {
492
- return nativeProducerResult(false, identity, phase, "result=launch_spec_incompatible");
543
+ outcome = fail("result=launch_spec_incompatible", "Warble emitted a launch spec this build cannot run");
544
+ break;
493
545
  }
494
546
  }
495
547
  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");
548
+ outcome = error instanceof NativeWrenRuntimeError
549
+ ? fail(`result=wren_runtime_unavailable (${error.message})`, error.message)
550
+ : fail("result=preflight_workspace_unavailable", "the preflight could not create its temporary workspace");
551
+ break;
499
552
  }
500
553
  finally {
501
554
  if (root)
502
555
  rmSync(root, { recursive: true, force: true });
503
556
  }
504
557
  }
505
- }
506
- return nativeProducerResult(true, identity, "complete", "result=compatible", producer);
558
+ vendors[vendor] = outcome;
559
+ }
560
+ const usable = NATIVE_PROBE_TARGETS.filter((vendor) => vendors[vendor].available);
561
+ const available = usable.length > 0;
562
+ // When nothing is usable the summary must still carry each vendor's own cause;
563
+ // collapsing them to "no_compatible_vendor" would lose the only line that says
564
+ // what actually broke.
565
+ const vendorResults = NATIVE_PROBE_TARGETS
566
+ .map((vendor) => `${vendor}=${vendors[vendor].diagnostic.split(" result=")[1] ?? "unknown"}`)
567
+ .join(" ");
568
+ return {
569
+ available,
570
+ ...(available ? {} : { category: NATIVE_PRODUCER_CATEGORY }),
571
+ producer,
572
+ diagnostic: producerDiagnostic(identity, "complete", available ? `result=compatible_${usable.join("+")}` : `result=no_compatible_vendor ${vendorResults}`),
573
+ vendors,
574
+ };
507
575
  }
508
576
  function parseNativeSetupRecoveryReport(value) {
509
577
  const report = record(value);
@@ -1139,6 +1207,10 @@ export class NativeSessionService {
1139
1207
  ? nativeSessionStateBaseAvailableForProject(this.materializationState(), configuredRoot)
1140
1208
  : purpose === "setup" && projectAvailable && nativeSessionStateBaseAvailable(this.materializationState());
1141
1209
  const workspaceUnavailable = !projectAvailable || !stateAvailable;
1210
+ // Only the configured vendor's own probe may block this purpose. Probing
1211
+ // both and collapsing them into one boolean let an unselected vendor veto
1212
+ // the selected one.
1213
+ const selectedProducer = producer && dispatch ? producer.vendors[dispatch.provider] : undefined;
1142
1214
  const reason = runtimeCorrection
1143
1215
  ?? (!dispatch && !legacyFixture
1144
1216
  ? "native sessions require a saved Runtime & authentication binding"
@@ -1146,8 +1218,8 @@ export class NativeSessionService {
1146
1218
  ? purpose === "setup" ? "native setup sessions require a workspace root" : "native sessions require a current bound project"
1147
1219
  : workspaceUnavailable
1148
1220
  ? "native session workspace is unavailable"
1149
- : producer && !producer.available
1150
- ? "native session producer is incompatible"
1221
+ : selectedProducer && !selectedProducer.available
1222
+ ? selectedProducer.reason ?? "native session producer is incompatible"
1151
1223
  : !irPath || !(this.options.dispatch !== undefined || configuredIrPath(irPath)) || !legacyProducerAvailable()
1152
1224
  ? "native session materialization prerequisites are unavailable"
1153
1225
  : mcpReadiness && !mcpReadiness.available
@@ -1160,6 +1232,17 @@ export class NativeSessionService {
1160
1232
  ? `the ${executable} interactive CLI is not available on this machine`
1161
1233
  : undefined);
1162
1234
  const vendorReadiness = {};
1235
+ // Production readiness now reports every vendor, so the UI can say which
1236
+ // one is unusable and why instead of only that "the producer" is.
1237
+ if (!legacyFixture && producer)
1238
+ for (const vendor of NATIVE_VENDORS) {
1239
+ const probe = producer.vendors[vendor];
1240
+ vendorReadiness[vendor] = probe?.available === false
1241
+ ? { available: false, reason: probe.reason ?? "native session producer is incompatible" }
1242
+ : !executableAvailable(vendor)
1243
+ ? { available: false, reason: `the ${vendor === "claude" ? "claude" : "codex"} interactive CLI is not available on this machine` }
1244
+ : { available: true };
1245
+ }
1163
1246
  if (legacyFixture)
1164
1247
  for (const vendor of NATIVE_VENDORS) {
1165
1248
  const legacyExecutable = vendor === "claude" ? "claude" : "codex";
@@ -1174,7 +1257,16 @@ export class NativeSessionService {
1174
1257
  scopeKind,
1175
1258
  profile: definition.profile,
1176
1259
  ...(dispatch ? { target: dispatch.target, targetLabel: dispatch.targetLabel } : {}),
1177
- ...(producer ? { producer: producer.available ? { available: true } : { available: false, category: NATIVE_PRODUCER_CATEGORY } } : {}),
1260
+ // With a vendor selected this answers for that vendor; with none selected
1261
+ // it falls back to "is any vendor usable", rather than claiming health
1262
+ // no vendor could back.
1263
+ ...(producer
1264
+ ? {
1265
+ producer: (selectedProducer ? selectedProducer.available : producer.available)
1266
+ ? { available: true, diagnostic: (selectedProducer ?? producer).diagnostic }
1267
+ : { available: false, category: NATIVE_PRODUCER_CATEGORY, diagnostic: (selectedProducer ?? producer).diagnostic },
1268
+ }
1269
+ : {}),
1178
1270
  available: legacyAvailable,
1179
1271
  ...(unavailable ? { reason: unavailable } : {}),
1180
1272
  vendors: vendorReadiness,
@@ -1222,8 +1314,10 @@ export class NativeSessionService {
1222
1314
  ? configuredIr
1223
1315
  : await this.options.resolveDispatchIr(purpose, binding);
1224
1316
  const producer = this.legacyFixtureMode() ? undefined : await this.productionProducerPreflight();
1225
- if (producer && (!producer.available || !producer.producer))
1226
- throw new InteractiveLaunchError("native session producer is incompatible");
1317
+ const launchProducer = producer ? producer.vendors[dispatchDefinition.provider] : undefined;
1318
+ if (producer && (!launchProducer?.available || !producer.producer)) {
1319
+ throw new InteractiveLaunchError(launchProducer?.reason ?? "native session producer is incompatible");
1320
+ }
1227
1321
  if (producer?.producer)
1228
1322
  assertNativeProducerIdentity(producer.producer);
1229
1323
  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.3",
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": [