@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.
|
|
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:
|
|
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",
|
|
493
|
-
: await compileProfile({ profileSource, userProject: binding.path, mode: "native",
|
|
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
|
-
|
|
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
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
|
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
|
-
...(
|
|
356
|
-
diagnostic
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
1226
|
-
|
|
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");
|