@wrenai/genbi 0.0.3 → 0.0.5
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 +65 -3
- package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
- package/dist/assets/index-A_Wrq7Ey.css +1 -0
- package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
- package/dist/index.html +2 -2
- package/dist-server/harness/bundle/schema.d.ts +38 -0
- package/dist-server/harness/bundle/schema.js +18 -10
- package/dist-server/harness/bundle/version.js +1 -1
- package/dist-server/harness/cli-args.d.ts +10 -2
- package/dist-server/harness/cli-args.js +24 -8
- package/dist-server/harness/cli.js +75 -9
- package/dist-server/harness/compile/cache.js +4 -4
- package/dist-server/harness/compile/compose-profile.d.ts +51 -14
- package/dist-server/harness/compile/compose-profile.js +85 -19
- package/dist-server/harness/compile/context-loader.d.ts +56 -0
- package/dist-server/harness/compile/context-loader.js +237 -0
- package/dist-server/harness/compile/errors.d.ts +19 -0
- package/dist-server/harness/compile/errors.js +35 -0
- package/dist-server/harness/compile/index.d.ts +4 -3
- package/dist-server/harness/compile/index.js +4 -3
- package/dist-server/harness/compile/pipeline.d.ts +15 -7
- package/dist-server/harness/compile/pipeline.js +62 -14
- package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
- package/dist-server/harness/compile/resolve-binary.js +13 -3
- package/dist-server/harness/compile/types.d.ts +29 -0
- package/dist-server/harness/compile/warble-identity.d.ts +11 -0
- package/dist-server/harness/compile/warble-identity.js +13 -0
- package/dist-server/harness/components/ai-step.d.ts +4 -0
- package/dist-server/harness/components/ai-step.js +40 -0
- package/dist-server/harness/components/broker.d.ts +60 -0
- package/dist-server/harness/components/broker.js +117 -0
- package/dist-server/harness/components/capability-card.d.ts +98 -0
- package/dist-server/harness/components/capability-card.js +169 -0
- package/dist-server/harness/components/context.d.ts +2 -0
- package/dist-server/harness/components/context.js +30 -0
- package/dist-server/harness/components/display.d.ts +6 -0
- package/dist-server/harness/components/display.js +29 -0
- package/dist-server/harness/components/egress-judge.d.ts +12 -0
- package/dist-server/harness/components/egress-judge.js +27 -0
- package/dist-server/harness/components/egress.d.ts +142 -0
- package/dist-server/harness/components/egress.js +469 -0
- package/dist-server/harness/components/in-process.d.ts +7 -0
- package/dist-server/harness/components/in-process.js +245 -0
- package/dist-server/harness/components/normalize.d.ts +35 -0
- package/dist-server/harness/components/normalize.js +245 -0
- package/dist-server/harness/components/plan.d.ts +31 -0
- package/dist-server/harness/components/plan.js +184 -0
- package/dist-server/harness/components/report.d.ts +207 -0
- package/dist-server/harness/components/report.js +309 -0
- package/dist-server/harness/components/runner.d.ts +193 -0
- package/dist-server/harness/components/runner.js +492 -0
- package/dist-server/harness/components/semantic-guards.d.ts +20 -0
- package/dist-server/harness/components/semantic-guards.js +42 -0
- package/dist-server/harness/components/terminal-json.d.ts +18 -0
- package/dist-server/harness/components/terminal-json.js +82 -0
- package/dist-server/harness/components/usage.d.ts +14 -0
- package/dist-server/harness/components/usage.js +19 -0
- package/dist-server/harness/components/wren-access.d.ts +29 -0
- package/dist-server/harness/components/wren-access.js +233 -0
- package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
- package/dist-server/harness/components/zone-dry-run.js +107 -0
- package/dist-server/harness/components/zone-gate.d.ts +76 -0
- package/dist-server/harness/components/zone-gate.js +144 -0
- package/dist-server/harness/events/index.d.ts +1 -1
- package/dist-server/harness/events/types.d.ts +52 -0
- package/dist-server/harness/index.d.ts +9 -2
- package/dist-server/harness/index.js +5 -1
- package/dist-server/harness/loop/executor.d.ts +1 -0
- package/dist-server/harness/loop/executor.js +37 -15
- package/dist-server/harness/npm-package-resolve.d.ts +2 -0
- package/dist-server/harness/npm-package-resolve.js +16 -4
- package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
- package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
- package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
- package/dist-server/harness/providers/adapters/openai.js +6 -0
- package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
- package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
- package/dist-server/harness/providers/binding.d.ts +45 -4
- package/dist-server/harness/providers/binding.js +44 -8
- package/dist-server/harness/providers/index.d.ts +9 -3
- package/dist-server/harness/providers/index.js +10 -3
- package/dist-server/harness/providers/zone.d.ts +96 -0
- package/dist-server/harness/providers/zone.js +112 -0
- package/dist-server/harness/render/answering-query.d.ts +29 -0
- package/dist-server/harness/render/answering-query.js +62 -0
- package/dist-server/harness/render/envelope.d.ts +6 -0
- package/dist-server/harness/render/envelope.js +10 -3
- package/dist-server/harness/route/adapter-spec.d.ts +8 -9
- package/dist-server/harness/route/adapter-spec.js +13 -14
- package/dist-server/harness/route/codex-ask-events.js +2 -1
- package/dist-server/harness/route/codex-ask.d.ts +4 -0
- package/dist-server/harness/route/codex-ask.js +69 -30
- package/dist-server/harness/route/codex-host-query.d.ts +52 -0
- package/dist-server/harness/route/codex-host-query.js +191 -0
- package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
- package/dist-server/harness/route/codex-local-manifest.js +96 -15
- package/dist-server/harness/route/codex-wren-proxy.d.ts +33 -0
- package/dist-server/harness/route/codex-wren-proxy.js +227 -0
- package/dist-server/harness/route/describe.js +45 -6
- package/dist-server/harness/route/in-process.d.ts +22 -1
- package/dist-server/harness/route/in-process.js +54 -4
- package/dist-server/harness/route/route.js +5 -2
- package/dist-server/harness/route/types.d.ts +30 -1
- package/dist-server/harness/session/run.js +30 -10
- package/dist-server/harness/setup/runner.d.ts +2 -0
- package/dist-server/harness/setup/runner.js +38 -17
- package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
- package/dist-server/harness/setup/yaml-scalar.js +62 -0
- package/dist-server/server/adopt.js +1 -17
- package/dist-server/server/app.d.ts +1 -0
- package/dist-server/server/app.js +163 -14
- package/dist-server/server/auth-choice.d.ts +13 -1
- package/dist-server/server/auth-choice.js +66 -1
- package/dist-server/server/bin.js +197 -26
- package/dist-server/server/codex-local-grounding.d.ts +18 -0
- package/dist-server/server/codex-local-grounding.js +92 -0
- package/dist-server/server/codex-native-session.d.ts +25 -0
- package/dist-server/server/codex-native-session.js +1 -0
- package/dist-server/server/codex-runtime-composition.d.ts +34 -0
- package/dist-server/server/codex-runtime-composition.js +250 -0
- package/dist-server/server/codex-structured-ask.d.ts +23 -0
- package/dist-server/server/codex-structured-ask.js +102 -0
- package/dist-server/server/conn-config.js +4 -16
- package/dist-server/server/conversation-socket.d.ts +16 -0
- package/dist-server/server/conversation-socket.js +65 -0
- package/dist-server/server/db.d.ts +46 -0
- package/dist-server/server/db.js +88 -9
- package/dist-server/server/env-detect.d.ts +6 -0
- package/dist-server/server/env-detect.js +7 -0
- package/dist-server/server/fold.d.ts +22 -4
- package/dist-server/server/fold.js +37 -5
- package/dist-server/server/harness.d.ts +2 -2
- package/dist-server/server/harness.js +33 -7
- package/dist-server/server/interactive-terminal.d.ts +10 -6
- package/dist-server/server/interactive-terminal.js +33 -8
- package/dist-server/server/managed-wren-runtime.d.ts +116 -0
- package/dist-server/server/managed-wren-runtime.js +670 -0
- package/dist-server/server/native-artifacts.d.ts +23 -7
- package/dist-server/server/native-artifacts.js +43 -12
- package/dist-server/server/native-component-context.d.ts +19 -0
- package/dist-server/server/native-component-context.js +76 -0
- package/dist-server/server/native-component-preparation.d.ts +20 -0
- package/dist-server/server/native-component-preparation.js +129 -0
- package/dist-server/server/native-component-runtime.d.ts +36 -0
- package/dist-server/server/native-component-runtime.js +92 -0
- package/dist-server/server/native-components.d.ts +84 -0
- package/dist-server/server/native-components.js +160 -0
- package/dist-server/server/native-dispatch-process.d.ts +8 -0
- package/dist-server/server/native-dispatch-process.js +65 -0
- package/dist-server/server/native-runtime-spec.d.ts +89 -0
- package/dist-server/server/native-runtime-spec.js +169 -0
- package/dist-server/server/native-sessions.d.ts +172 -19
- package/dist-server/server/native-sessions.js +870 -251
- package/dist-server/server/native-shutdown.d.ts +6 -0
- package/dist-server/server/native-shutdown.js +28 -0
- package/dist-server/server/native-wren-home.d.ts +18 -0
- package/dist-server/server/native-wren-home.js +262 -0
- package/dist-server/server/node-pty-host.d.ts +2 -1
- package/dist-server/server/node-pty-host.js +3 -2
- package/dist-server/server/profile-registry.d.ts +139 -0
- package/dist-server/server/profile-registry.js +620 -0
- package/dist-server/server/runtime-binding.d.ts +4 -1
- package/dist-server/server/runtime-binding.js +50 -2
- package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
- package/dist-server/server/runtime-host/claude-component-step.js +186 -0
- package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
- package/dist-server/server/runtime-host/codex-app-server.js +239 -0
- package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
- package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
- package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
- package/dist-server/server/runtime-host/codex-component-step.js +308 -0
- package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
- package/dist-server/server/runtime-host/codex-conversation.js +295 -0
- package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
- package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
- package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
- package/dist-server/server/runtime-host/codex-events.js +66 -0
- package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-policy.js +151 -0
- package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
- package/dist-server/server/runtime-host/codex-process.js +96 -0
- package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
- package/dist-server/server/runtime-host/codex-rpc.js +182 -0
- package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
- package/dist-server/server/runtime-host/codex-session.js +492 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
- package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-terminal.js +120 -0
- package/dist-server/server/runtime-host/index.d.ts +5 -0
- package/dist-server/server/runtime-host/index.js +3 -0
- package/dist-server/server/runtime-host/local.d.ts +29 -0
- package/dist-server/server/runtime-host/local.js +90 -0
- package/dist-server/server/runtime-host/policy.d.ts +8 -0
- package/dist-server/server/runtime-host/policy.js +78 -0
- package/dist-server/server/runtime-host/types.d.ts +52 -0
- package/dist-server/server/runtime-host/types.js +62 -0
- package/dist-server/server/structured-query-evidence.d.ts +7 -0
- package/dist-server/server/structured-query-evidence.js +47 -0
- package/dist-server/server/structured-runtime.d.ts +52 -0
- package/dist-server/server/structured-runtime.js +130 -0
- package/dist-server/server/structured-scope.d.ts +4 -0
- package/dist-server/server/structured-scope.js +12 -0
- package/dist-server/server/turn.d.ts +6 -1
- package/dist-server/server/turn.js +82 -25
- package/dist-server/server/wire-types.d.ts +67 -3
- package/dist-server/server/wren-profiles.js +1 -1
- package/managed-wren/README.md +60 -0
- package/managed-wren/dependency-wheels.json +282 -0
- package/managed-wren/manifest.json +373 -0
- package/managed-wren/release-inputs.json +45 -0
- package/managed-wren/runtime-tree.cjs +115 -0
- package/managed-wren/source-inputs.json +40 -0
- package/package.json +15 -6
- package/profiles/genbi-default/context/binding.yml +17 -7
- package/profiles/genbi-default/context/context.json +146 -0
- package/profiles/genbi-default/ir.golden.json +39 -43
- package/profiles/genbi-default/profile.yml +31 -0
- package/profiles/genbi-enrich-context/context/binding.yml +14 -3
- package/profiles/genbi-enrich-context/context/context.json +146 -0
- package/profiles/genbi-enrich-context/ir.golden.json +4 -5
- package/profiles/genbi-monitor/context/binding.yml +13 -5
- package/profiles/genbi-monitor/context/context.json +146 -0
- package/profiles/genbi-monitor/ir.golden.json +13 -7
- package/profiles/genbi-report/context/binding.yml +19 -0
- package/profiles/genbi-report/context/context.json +223 -0
- package/profiles/genbi-report/ir.golden.json +328 -0
- package/profiles/genbi-report/profile.yml +38 -0
- package/profiles/genbi-setup/ir.golden.json +3 -1
- package/dist/assets/index-Xs4q7pNo.css +0 -1
|
@@ -1,20 +1,31 @@
|
|
|
1
|
+
import { startCodexTerminal } from "./runtime-host/codex-terminal.js";
|
|
2
|
+
import { CodexConversation } from "./runtime-host/codex-conversation.js";
|
|
3
|
+
import { runNativeDispatchProcess } from "./native-dispatch-process.js";
|
|
4
|
+
import { NativeComponentAdmission, nativeComponentHostContract, readNativeComponentPlans } from "./native-components.js";
|
|
1
5
|
/**
|
|
2
6
|
* Durable native-session control plane. SQLite owns identity/lifecycle;
|
|
3
7
|
* the PTY remains deliberately process-local and contains the only replay.
|
|
4
8
|
*/
|
|
5
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import {
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
7
11
|
import { accessSync, chmodSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
12
|
import os from "node:os";
|
|
9
13
|
import path from "node:path";
|
|
10
|
-
import {
|
|
14
|
+
import { isDeepStrictEqual } from "node:util";
|
|
15
|
+
import { WarbleBinaryNotFoundError } from "../harness/compile/errors.js";
|
|
16
|
+
import { InteractiveLaunchError, NO_INTERACTIVE_TERMINAL_LEASES } from "./interactive-terminal.js";
|
|
11
17
|
import { newId } from "./db.js";
|
|
12
18
|
import { NATIVE_MCP_CREDENTIAL_ENV_VAR } from "./native-artifacts.js";
|
|
13
19
|
import { dispatchForPurpose, NATIVE_DISPATCH_REGISTRY, NATIVE_PURPOSES, nativeTargetLabel, sameNativeRuntimeBinding, targetForProvider } from "./native-dispatch-registry.js";
|
|
14
20
|
import { createNativeSessionWorkspace, initializeNativeSessionStateBase, nativeSessionStateBaseAvailable, nativeSessionStateBaseAvailableForProject, validateNativeSessionWorkspace } from "./native-session-workspace.js";
|
|
15
21
|
import { NativeWrenRuntimeError, resolveNativeWrenRuntime } from "./native-wren-runtime.js";
|
|
22
|
+
import { ManagedWrenRuntimeError } from "./managed-wren-runtime.js";
|
|
23
|
+
import { assertNativeExecutableIdentity, assertNativeRuntimeSpec, attestNativeExecutable, buildNativeChildEnvironment, buildNativeRuntimeSpec, nativeProcessEnvironment, resolveNativeExecutable } from "./native-runtime-spec.js";
|
|
24
|
+
import { createEmptyCodexWrenHome, materializeCodexWrenHome } from "./native-wren-home.js";
|
|
16
25
|
import { runtimeSettingsCorrection } from "./runtime-binding.js";
|
|
17
26
|
import { sealNativeClaudeResumeHandle, sealNativeResumeHandle, unsealNativeResumeHandle } from "./native-session-resume.js";
|
|
27
|
+
import { RuntimeHost } from "./runtime-host/local.js";
|
|
28
|
+
import { runtimeNotReady } from "./runtime-host/policy.js";
|
|
18
29
|
export const NATIVE_VENDORS = ["claude", "codex"];
|
|
19
30
|
export { NATIVE_PURPOSES };
|
|
20
31
|
export const NATIVE_SETUP_RECOVERY_MCP_TOOL_NAME = "report_setup_recovery";
|
|
@@ -38,12 +49,15 @@ const START_SEPARATE_REPLAY_LIMIT = 128;
|
|
|
38
49
|
const CODEX_NATIVE_PERMISSION_HEADER = "[permissions.warble_native_wren.filesystem]";
|
|
39
50
|
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
51
|
const CODEX_RESUME_HANDLE_FILE = path.join(".warble", "codex-thread-id");
|
|
52
|
+
const CODEX_WREN_RUNTIME_UNPROVISIONED = runtimeNotReady("codex-app-server", "unprovisioned", "codex_wren_runtime_unprovisioned");
|
|
41
53
|
function logNativeLaunchFailure(purpose, vendor, phase, error) {
|
|
42
|
-
const
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
54
|
+
const rejected = error instanceof InteractiveLaunchError;
|
|
55
|
+
// Exception text, paths, credentials and argv stay out of the log for an
|
|
56
|
+
// unexpected error, which may carry any of them. A rejection carries a
|
|
57
|
+
// sentence authored here, and suppressing that turned every diagnosis into a
|
|
58
|
+
// guess: an operator saw a phase and a category and no cause.
|
|
59
|
+
const detail = rejected ? ` reason=${error.message}` : "";
|
|
60
|
+
console.error(`[native-sessions] launch failed purpose=${purpose} vendor=${vendor} phase=${phase} category=${rejected ? "rejected" : "unexpected"}${detail}`);
|
|
47
61
|
}
|
|
48
62
|
/**
|
|
49
63
|
* The producer deliberately sees only a sealed binding identity, never a caller-supplied path.
|
|
@@ -71,60 +85,6 @@ export function grantCodexBoundProjectRead(cwd, projectPath, dataRoots = []) {
|
|
|
71
85
|
writeFileSync(configPath, updated, { encoding: "utf8", mode: 0o600 });
|
|
72
86
|
chmodSync(configPath, 0o600);
|
|
73
87
|
}
|
|
74
|
-
const MATERIALIZE_WREN_HOME_SCRIPT = String.raw `
|
|
75
|
-
import os
|
|
76
|
-
import pathlib
|
|
77
|
-
import shutil
|
|
78
|
-
import sys
|
|
79
|
-
import yaml
|
|
80
|
-
from wren.profile import expand_profile_secrets, resolve_profile_for_project
|
|
81
|
-
|
|
82
|
-
project = pathlib.Path(sys.argv[1]).resolve(strict=True)
|
|
83
|
-
destination = pathlib.Path(sys.argv[2])
|
|
84
|
-
name, profile = resolve_profile_for_project(project, strict=True)
|
|
85
|
-
if not name or not profile:
|
|
86
|
-
raise SystemExit(2)
|
|
87
|
-
destination.mkdir(mode=0o700, parents=False, exist_ok=False)
|
|
88
|
-
profile_path = destination / "profiles.yml"
|
|
89
|
-
profile_path.write_text(yaml.safe_dump({"active": name, "profiles": {name: profile}}, sort_keys=False), encoding="utf-8")
|
|
90
|
-
os.chmod(profile_path, 0o600)
|
|
91
|
-
project_env = project / ".env"
|
|
92
|
-
if project_env.exists():
|
|
93
|
-
if project_env.is_symlink() or not project_env.is_file():
|
|
94
|
-
raise SystemExit(3)
|
|
95
|
-
session_env = destination / ".env"
|
|
96
|
-
shutil.copyfile(project_env, session_env)
|
|
97
|
-
os.chmod(session_env, 0o600)
|
|
98
|
-
|
|
99
|
-
expanded = expand_profile_secrets(profile)
|
|
100
|
-
data_source = expanded.get("datasource")
|
|
101
|
-
data_root = None
|
|
102
|
-
if data_source in ("duckdb", "local_file"):
|
|
103
|
-
data_root = expanded.get("url")
|
|
104
|
-
elif data_source == "datafusion":
|
|
105
|
-
data_root = expanded.get("source")
|
|
106
|
-
if data_root:
|
|
107
|
-
root = pathlib.Path(data_root).expanduser()
|
|
108
|
-
if not root.is_absolute():
|
|
109
|
-
root = project / root
|
|
110
|
-
print(root.resolve(strict=True))
|
|
111
|
-
`;
|
|
112
|
-
function materializeCodexWrenHome(runtime, projectPath, cwd) {
|
|
113
|
-
const destination = path.join(cwd, ".wren");
|
|
114
|
-
try {
|
|
115
|
-
const output = execFileSync(runtime.venv_python, ["-c", MATERIALIZE_WREN_HOME_SCRIPT, projectPath, destination], {
|
|
116
|
-
cwd: projectPath,
|
|
117
|
-
env: process.env,
|
|
118
|
-
encoding: "utf8",
|
|
119
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
120
|
-
timeout: 10_000,
|
|
121
|
-
});
|
|
122
|
-
return { home: destination, dataRoots: output.trim() ? [output.trim()] : [] };
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
throw new InteractiveLaunchError("native Wren project profile is unavailable");
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
88
|
function nativeSessionRestartReason(target, cause) {
|
|
129
89
|
switch (cause) {
|
|
130
90
|
case "not_terminal":
|
|
@@ -172,6 +132,13 @@ export class NativeSetupRecoveryError extends Error {
|
|
|
172
132
|
* whichever verb the scope declares, after checking the declaration against the compiled IR.
|
|
173
133
|
*/
|
|
174
134
|
const entryVerbFor = (purpose) => purpose === "analysis" ? "answer_query" : purpose === "setup" ? "connect_source" : "draft_enrichment";
|
|
135
|
+
/** User-selected analysis entry is a single pin; it never widens Claude scope. */
|
|
136
|
+
export function selectedNativeEntry(purpose, vendor, entryVerb) {
|
|
137
|
+
if (entryVerb != null && (vendor !== "codex" || purpose !== "analysis" || !["answer_query", "generate_dashboard"].includes(entryVerb))) {
|
|
138
|
+
throw new InteractiveLaunchError("native session entry is invalid");
|
|
139
|
+
}
|
|
140
|
+
return vendor === "codex" && purpose === "analysis" ? entryVerb ?? "answer_query" : undefined;
|
|
141
|
+
}
|
|
175
142
|
const agentFor = (purpose, vendor) => vendor === "claude" ? entryVerbFor(purpose) : `genbi-${purpose === "context_enrichment" ? "enrich-context" : purpose}`;
|
|
176
143
|
/**
|
|
177
144
|
* Closed v4 Claude entry contract. This host is the only place the mapping exists: Warble used to
|
|
@@ -200,9 +167,9 @@ const claudeScopeEntry = (purpose, vendor) => vendor === "claude" && !PINNED_CLA
|
|
|
200
167
|
* most by pinning: its profile mounts four selectable components, and a pinned session can only
|
|
201
168
|
* reach the other three by delegating out of the one it was forced to start as.
|
|
202
169
|
*/
|
|
203
|
-
const nativeEntryFor = (purpose, vendor) => claudeScopeEntry(purpose, vendor)
|
|
170
|
+
const nativeEntryFor = (purpose, vendor, entryVerb) => claudeScopeEntry(purpose, vendor)
|
|
204
171
|
? { kind: "scope", prompt: welcomePromptFor(purpose) }
|
|
205
|
-
: { verb: entryVerbFor(purpose), prompt: welcomePromptFor(purpose) };
|
|
172
|
+
: { verb: selectedNativeEntry(purpose, vendor, entryVerb) ?? entryVerbFor(purpose), prompt: welcomePromptFor(purpose) };
|
|
206
173
|
/** Closed producer/host contract for the one initial native TUI prompt. */
|
|
207
174
|
const welcomePromptFor = (purpose) => {
|
|
208
175
|
switch (purpose) {
|
|
@@ -214,6 +181,50 @@ const welcomePromptFor = (purpose) => {
|
|
|
214
181
|
return "Help me inspect this project's context and draft a read-only enrichment proposal. Do not apply changes; ask what context I want to review.";
|
|
215
182
|
}
|
|
216
183
|
};
|
|
184
|
+
export const DEFAULT_CONVERSATION_PROFILE = NATIVE_DISPATCH_REGISTRY.analysis.profile;
|
|
185
|
+
const DEFAULT_PROFILE_SELECTION = Object.freeze({ id: DEFAULT_CONVERSATION_PROFILE, entry: { kind: "scope" } });
|
|
186
|
+
const PROFILE_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
187
|
+
const CODEX_CUSTOM_PROFILE_UNAVAILABLE = "conversation profiles other than the default are not available on the Codex CLI";
|
|
188
|
+
/**
|
|
189
|
+
* A selection that departs from the purpose's fixed profile, or `undefined` when the registry's
|
|
190
|
+
* rules for that purpose apply unchanged. System purposes never have one.
|
|
191
|
+
*/
|
|
192
|
+
const customSelection = (purpose, selection) => purpose === "analysis" && selection !== undefined && selection.id !== NATIVE_DISPATCH_REGISTRY[purpose].profile ? selection : undefined;
|
|
193
|
+
/**
|
|
194
|
+
* The profile id a launch request names, validated for shape and purpose. Only analysis sessions
|
|
195
|
+
* run inside a selectable profile; the system purposes keep their fixed ones.
|
|
196
|
+
*/
|
|
197
|
+
function requestedProfileId(purpose, profile) {
|
|
198
|
+
if (profile === undefined)
|
|
199
|
+
return purpose === "analysis" ? DEFAULT_CONVERSATION_PROFILE : NATIVE_DISPATCH_REGISTRY[purpose].profile;
|
|
200
|
+
if (purpose !== "analysis")
|
|
201
|
+
throw new InteractiveLaunchError("native session profile is only selectable for analysis");
|
|
202
|
+
if (!PROFILE_ID_PATTERN.test(profile))
|
|
203
|
+
throw new InteractiveLaunchError("native session profile is invalid");
|
|
204
|
+
return profile;
|
|
205
|
+
}
|
|
206
|
+
const scopeEntryFor = (purpose, vendor, selection) => {
|
|
207
|
+
const custom = customSelection(purpose, selection);
|
|
208
|
+
return custom ? custom.entry.kind === "scope" : claudeScopeEntry(purpose, vendor);
|
|
209
|
+
};
|
|
210
|
+
/** The agent a pinned launch names, or the component a scope session is recorded under. */
|
|
211
|
+
const expectedAgentFor = (purpose, vendor, selection) => {
|
|
212
|
+
const custom = customSelection(purpose, selection);
|
|
213
|
+
if (!custom)
|
|
214
|
+
return agentFor(purpose, vendor);
|
|
215
|
+
return custom.entry.kind === "agent" ? custom.entry.verb : custom.id;
|
|
216
|
+
};
|
|
217
|
+
/** The entry declared in the scope descriptor; a custom profile's form comes from admission. */
|
|
218
|
+
const entryFor = (purpose, vendor, entryVerb, selection) => {
|
|
219
|
+
const custom = customSelection(purpose, selection);
|
|
220
|
+
if (!custom)
|
|
221
|
+
return nativeEntryFor(purpose, vendor, entryVerb);
|
|
222
|
+
// The welcome text is purpose text, not profile text: every conversation profile is entered the
|
|
223
|
+
// same way, so the first turn stays the one string the launch gate already checks.
|
|
224
|
+
return custom.entry.kind === "scope"
|
|
225
|
+
? { kind: "scope", prompt: welcomePromptFor(purpose) }
|
|
226
|
+
: { verb: custom.entry.verb, prompt: welcomePromptFor(purpose) };
|
|
227
|
+
};
|
|
217
228
|
function sameBinding(a, b) {
|
|
218
229
|
return b !== undefined && a.path === b.path && a.identity === b.identity && a.generation === b.generation && a.revision === b.revision;
|
|
219
230
|
}
|
|
@@ -222,6 +233,7 @@ function resumeScopeFingerprint(row) {
|
|
|
222
233
|
row.id, row.purpose, row.vendor, row.scopeKind, row.projectIdentity,
|
|
223
234
|
row.bindingGeneration, row.projectRevision, row.dispatchProfile,
|
|
224
235
|
row.dispatchTarget, row.runtimeGeneration,
|
|
236
|
+
...(row.entryVerb && row.entryVerb !== "answer_query" ? [row.entryVerb] : []),
|
|
225
237
|
])).digest("hex");
|
|
226
238
|
}
|
|
227
239
|
function record(value) {
|
|
@@ -234,25 +246,6 @@ function contained(root, candidate) {
|
|
|
234
246
|
const relative = path.relative(root, candidate);
|
|
235
247
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
236
248
|
}
|
|
237
|
-
function localExecutableAvailable(executable, pathValue = process.env["PATH"]) {
|
|
238
|
-
return localExecutablePath(executable, pathValue) !== undefined;
|
|
239
|
-
}
|
|
240
|
-
function localExecutablePath(executable, pathValue = process.env["PATH"]) {
|
|
241
|
-
const candidates = path.isAbsolute(executable) || executable.includes(path.sep)
|
|
242
|
-
? [path.resolve(executable)]
|
|
243
|
-
: (pathValue ?? "").split(path.delimiter).filter(Boolean).map((directory) => path.join(directory, executable));
|
|
244
|
-
for (const candidate of candidates) {
|
|
245
|
-
try {
|
|
246
|
-
const stat = statSync(candidate);
|
|
247
|
-
if (stat.isFile() && (stat.mode & 0o111) !== 0)
|
|
248
|
-
return realpathSync(candidate);
|
|
249
|
-
}
|
|
250
|
-
catch {
|
|
251
|
-
// Try the next PATH entry. The public readiness response never learns this path.
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
256
249
|
function configuredWorkspace(cwd, create) {
|
|
257
250
|
try {
|
|
258
251
|
if (create)
|
|
@@ -352,6 +345,16 @@ export function nativeSessionLaunchFailure(error) {
|
|
|
352
345
|
return "native session launch artifacts are unavailable";
|
|
353
346
|
if (message === "native session launch paths are unavailable" || message === "native session launch paths are outside its server-owned scope")
|
|
354
347
|
return "native session launch artifacts are unavailable";
|
|
348
|
+
// Every InteractiveLaunchError message is authored in this file: a fixed
|
|
349
|
+
// sentence, a fixed profile name, or a reason readiness already publishes.
|
|
350
|
+
// None is built from a path, argv, or process output. Falling through to the
|
|
351
|
+
// generic string threw away the cause of eighteen distinct failures, which is
|
|
352
|
+
// what left "native session launch failed" as the only thing a user could see.
|
|
353
|
+
if (error instanceof InteractiveLaunchError)
|
|
354
|
+
return message;
|
|
355
|
+
// This one is NOT safe to forward: its text embeds the configured binary path.
|
|
356
|
+
if (error instanceof WarbleBinaryNotFoundError)
|
|
357
|
+
return "the Warble binary could not be resolved";
|
|
355
358
|
return "native session launch failed";
|
|
356
359
|
}
|
|
357
360
|
const NATIVE_PRODUCER_CATEGORY = "native_session_producer_incompatible";
|
|
@@ -394,25 +397,14 @@ function nativeProducerResult(available, identity, phase, detail, reason, produc
|
|
|
394
397
|
vendors: everyVendorUnavailable(available ? { available: true, diagnostic } : { available: false, reason, diagnostic }),
|
|
395
398
|
};
|
|
396
399
|
}
|
|
397
|
-
function executableIdentity(executable) {
|
|
398
|
-
try {
|
|
399
|
-
const metadata = statSync(executable);
|
|
400
|
-
if (!metadata.isFile() || (metadata.mode & 0o111) === 0)
|
|
401
|
-
return undefined;
|
|
402
|
-
return `sha256:${createHash("sha256").update(readFileSync(executable)).digest("hex")}`;
|
|
403
|
-
}
|
|
404
|
-
catch {
|
|
405
|
-
return undefined;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
400
|
/**
|
|
409
401
|
* The configured spelling may be a PATH lookup or retargetable symlink. A
|
|
410
402
|
* preflight result instead owns the resolved executable and its digest. Check
|
|
411
403
|
* both again before creating a durable row and directly before spawning.
|
|
412
404
|
*/
|
|
413
405
|
function assertNativeProducerIdentity(producer) {
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
406
|
+
const current = attestNativeExecutable("producer", producer.executable);
|
|
407
|
+
if (current.executable !== producer.executable || current.digest !== producer.identity) {
|
|
416
408
|
throw new InteractiveLaunchError("native session producer is incompatible");
|
|
417
409
|
}
|
|
418
410
|
}
|
|
@@ -421,7 +413,7 @@ function assertNativeProducerIdentity(producer) {
|
|
|
421
413
|
* output is used only for local help-marker parsing and is never logged or
|
|
422
414
|
* returned: a third-party executable may print arbitrary sensitive text.
|
|
423
415
|
*/
|
|
424
|
-
function runNativeProbe(command, args) {
|
|
416
|
+
function runNativeProbe(command, args, env) {
|
|
425
417
|
return new Promise((resolve) => {
|
|
426
418
|
let output = "";
|
|
427
419
|
let timedOut = false;
|
|
@@ -438,9 +430,10 @@ function runNativeProbe(command, args) {
|
|
|
438
430
|
};
|
|
439
431
|
let child;
|
|
440
432
|
try {
|
|
441
|
-
|
|
433
|
+
assertNativeExecutableIdentity(command);
|
|
434
|
+
child = spawn(command.executable, [...args], {
|
|
442
435
|
cwd: os.tmpdir(),
|
|
443
|
-
env:
|
|
436
|
+
env: nativeProcessEnvironment(env),
|
|
444
437
|
shell: false,
|
|
445
438
|
stdio: ["ignore", "pipe", "pipe"],
|
|
446
439
|
});
|
|
@@ -475,14 +468,23 @@ function runNativeProbe(command, args) {
|
|
|
475
468
|
* validator used just before a PTY starts.
|
|
476
469
|
*/
|
|
477
470
|
export async function probeNativeSessionProducer(input) {
|
|
478
|
-
const
|
|
479
|
-
if (!
|
|
471
|
+
const producerExecutable = resolveNativeExecutable("producer", input.warbleBin, input.pathValue);
|
|
472
|
+
if (!producerExecutable)
|
|
480
473
|
return nativeProducerResult(false, undefined, "resolve", "result=binary_not_found", "the Warble binary could not be resolved");
|
|
481
|
-
const identity =
|
|
482
|
-
|
|
483
|
-
return nativeProducerResult(false, undefined, "identity", "result=identity_unavailable", "the Warble binary could not be identified");
|
|
474
|
+
const identity = producerExecutable.digest;
|
|
475
|
+
const executable = producerExecutable.executable;
|
|
484
476
|
const producer = { executable, identity };
|
|
485
|
-
const
|
|
477
|
+
const nodeExecutable = input.nodeExecutable ?? attestNativeExecutable("node", process.execPath);
|
|
478
|
+
const childToolDirectories = [...new Set([
|
|
479
|
+
...(input.childToolDirectories ?? []),
|
|
480
|
+
path.dirname(nodeExecutable.executable),
|
|
481
|
+
path.dirname(executable),
|
|
482
|
+
])];
|
|
483
|
+
const probeEnv = buildNativeChildEnvironment({
|
|
484
|
+
toolDirectories: childToolDirectories,
|
|
485
|
+
home: input.nativeHome ?? os.homedir(),
|
|
486
|
+
});
|
|
487
|
+
const help = await runNativeProbe(producerExecutable, ["dispatch", "--help"], probeEnv);
|
|
486
488
|
if (help.spawnFailed)
|
|
487
489
|
return nativeProducerResult(false, identity, "dispatch_help", "result=spawn_failed", "the Warble binary could not be started");
|
|
488
490
|
if (help.timedOut)
|
|
@@ -497,65 +499,97 @@ export async function probeNativeSessionProducer(input) {
|
|
|
497
499
|
// reported the producer incompatible even for a Claude-only install, which is
|
|
498
500
|
// exactly what an installed package produces.
|
|
499
501
|
const vendors = {};
|
|
502
|
+
const purposes = Object.fromEntries(NATIVE_PURPOSES.map((purpose) => [purpose, {}]));
|
|
500
503
|
for (const vendor of NATIVE_PROBE_TARGETS) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const outputRoot = path.join(root, "out");
|
|
514
|
-
mkdirSync(outputRoot, { mode: 0o700 });
|
|
515
|
-
const bootstrapRoot = path.join(root, "bootstrap");
|
|
516
|
-
mkdirSync(bootstrapRoot, { mode: 0o700 });
|
|
517
|
-
const scopeId = `preflight-${purpose}-${vendor}`;
|
|
518
|
-
const scopePath = path.join(root, "scope.json");
|
|
519
|
-
const mcpPath = path.join(root, "mcp.json");
|
|
520
|
-
const wrenRuntime = vendor === "codex" ? resolveNativeWrenRuntime(input.wrenShim) : undefined;
|
|
521
|
-
const binding = NATIVE_DISPATCH_REGISTRY[purpose].scopeKind === "bound_project"
|
|
522
|
-
? { project_identity: "native-preflight-project", generation: "1", revision: "native-preflight-revision" }
|
|
523
|
-
: undefined;
|
|
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" });
|
|
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" });
|
|
526
|
-
const dispatched = await runNativeProbe(executable, ["dispatch", irPath, "--target", targetForProvider(vendor), "--purpose", purpose, "--native-scope", scopePath, "--native-mcp", mcpPath, "--out", outputRoot]);
|
|
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
|
-
}
|
|
504
|
+
for (const selectedPurpose of NATIVE_PURPOSES) {
|
|
505
|
+
let outcome = { available: true, diagnostic: producerDiagnostic(identity, `complete_${vendor}`, "result=compatible") };
|
|
506
|
+
const probes = [selectedPurpose].flatMap((purpose) => purpose === "analysis" && vendor === "codex" && input.prepareComponents
|
|
507
|
+
? [{ purpose, entryVerb: "answer_query" }, { purpose, entryVerb: "generate_dashboard" }]
|
|
508
|
+
: [{ purpose, entryVerb: undefined }]);
|
|
509
|
+
for (const { purpose, entryVerb } of probes) {
|
|
510
|
+
const irPath = input.irPaths[purpose];
|
|
511
|
+
if (!irPath || !configuredIrPath(irPath))
|
|
512
|
+
continue;
|
|
513
|
+
const phase = `dispatch_${purpose}_${vendor}${entryVerb ? `_${entryVerb}` : ""}`;
|
|
514
|
+
const fail = (detail, reason) => ({ available: false, reason, diagnostic: producerDiagnostic(identity, phase, detail) });
|
|
515
|
+
let root;
|
|
539
516
|
try {
|
|
540
|
-
|
|
517
|
+
root = mkdtempSync(path.join(os.tmpdir(), "genbi-native-preflight-"));
|
|
518
|
+
chmodSync(root, 0o700);
|
|
519
|
+
root = realpathSync(root);
|
|
520
|
+
const outputRoot = path.join(root, "out");
|
|
521
|
+
mkdirSync(outputRoot, { mode: 0o700 });
|
|
522
|
+
const bootstrapRoot = path.join(root, "bootstrap");
|
|
523
|
+
mkdirSync(bootstrapRoot, { mode: 0o700 });
|
|
524
|
+
const scopeId = `preflight-${purpose}-${vendor}`;
|
|
525
|
+
const scopePath = path.join(root, "scope.json");
|
|
526
|
+
const mcpPath = path.join(root, "mcp.json");
|
|
527
|
+
// Production passes the sealed managed record. The legacy resolver is a
|
|
528
|
+
// compatibility seam for deterministic fixture tests only; bin.ts does
|
|
529
|
+
// not provide a shim and so can never select an editable installation.
|
|
530
|
+
const wrenRuntime = vendor === "codex"
|
|
531
|
+
? input.resolveManagedCodexWrenRuntime
|
|
532
|
+
? input.resolveManagedCodexWrenRuntime()
|
|
533
|
+
: resolveNativeWrenRuntime(input.wrenShim)
|
|
534
|
+
: undefined;
|
|
535
|
+
const binding = NATIVE_DISPATCH_REGISTRY[purpose].scopeKind === "bound_project"
|
|
536
|
+
? { project_identity: "native-preflight-project", generation: "1", revision: "native-preflight-revision" }
|
|
537
|
+
: undefined;
|
|
538
|
+
const scope = { version: "3", kind: NATIVE_DISPATCH_REGISTRY[purpose].scopeKind, scope_id: scopeId, cwd: outputRoot, entry: nativeEntryFor(purpose, vendor, entryVerb), ...(purpose === "setup" ? { bootstrap_root: bootstrapRoot } : {}), ...(wrenRuntime ? { wren_runtime: wrenRuntime } : {}), ...(binding ? { binding } : {}) };
|
|
539
|
+
const scopeDocument = JSON.stringify(scope);
|
|
540
|
+
writeFileSync(scopePath, scopeDocument, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
541
|
+
const irDocument = input.prepareComponents && purpose === "analysis" ? readFileSync(irPath, "utf8") : undefined;
|
|
542
|
+
const prepared = irDocument ? input.prepareComponents?.({ vendor, purpose, irDocument, scope }) : undefined;
|
|
543
|
+
const hostPath = path.join(root, "component-host.json");
|
|
544
|
+
let receipt;
|
|
545
|
+
if (prepared && irDocument) {
|
|
546
|
+
if (prepared.identity.vendor !== vendor || !isDeepStrictEqual(prepared.identity.binding, binding))
|
|
547
|
+
throw new Error("Incompatible component probe identity");
|
|
548
|
+
writeFileSync(hostPath, JSON.stringify(nativeComponentHostContract(prepared)), { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
549
|
+
receipt = { identity: structuredClone(prepared.identity), irDocument, scopeDocument,
|
|
550
|
+
hostRoots: structuredClone(prepared.hostRoots), contexts: structuredClone(prepared.contexts) };
|
|
551
|
+
}
|
|
552
|
+
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" });
|
|
553
|
+
const dispatched = await runNativeProbe(producerExecutable, ["dispatch", irPath, "--target", targetForProvider(vendor), "--purpose", purpose, "--native-scope", scopePath, "--native-mcp", mcpPath, ...(prepared ? ["--native-host", hostPath] : []), "--out", outputRoot], probeEnv);
|
|
554
|
+
if (dispatched.spawnFailed) {
|
|
555
|
+
outcome = fail("result=spawn_failed", "the Warble binary could not be started for this vendor");
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
if (dispatched.timedOut) {
|
|
559
|
+
outcome = fail("result=timed_out", "the Warble binary did not respond for this vendor");
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
if (dispatched.exitCode !== 0) {
|
|
563
|
+
const missingHost = !prepared && dispatched.output.includes("component_invocation")
|
|
564
|
+
&& dispatched.output.includes("no trusted invocation handler is installed");
|
|
565
|
+
outcome = missingHost
|
|
566
|
+
? fail("result=component_runtime_unprovisioned", "Native component execution has not been provisioned for this profile.")
|
|
567
|
+
: fail(`result=exit_code_${dispatched.exitCode ?? "signal"}`, "Warble could not dispatch this profile for this vendor");
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
readNativeLaunchSpec(outputRoot, purpose, vendor, scopeId, undefined, { version: "1", url: "http://127.0.0.1:0/api/native-sessions/mcp", credential: "native-preflight-nonsecret" }, undefined, undefined, undefined, receipt);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
outcome = fail("result=launch_spec_incompatible", "Warble emitted a launch spec this build cannot run");
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
541
577
|
}
|
|
542
|
-
catch {
|
|
543
|
-
outcome =
|
|
578
|
+
catch (error) {
|
|
579
|
+
outcome = error instanceof NativeWrenRuntimeError || error instanceof ManagedWrenRuntimeError
|
|
580
|
+
? fail(`result=wren_runtime_unavailable (${error.message})`, error.message)
|
|
581
|
+
: fail("result=preflight_workspace_unavailable", "the preflight could not create its temporary workspace");
|
|
544
582
|
break;
|
|
545
583
|
}
|
|
584
|
+
finally {
|
|
585
|
+
if (root)
|
|
586
|
+
rmSync(root, { recursive: true, force: true });
|
|
587
|
+
}
|
|
546
588
|
}
|
|
547
|
-
|
|
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;
|
|
552
|
-
}
|
|
553
|
-
finally {
|
|
554
|
-
if (root)
|
|
555
|
-
rmSync(root, { recursive: true, force: true });
|
|
556
|
-
}
|
|
589
|
+
purposes[selectedPurpose][vendor] = outcome;
|
|
557
590
|
}
|
|
558
|
-
vendors[vendor] =
|
|
591
|
+
vendors[vendor] = NATIVE_PURPOSES.map((purpose) => purposes[purpose][vendor]).find((probe) => !probe.available)
|
|
592
|
+
?? { available: true, diagnostic: producerDiagnostic(identity, `complete_${vendor}`, "result=compatible") };
|
|
559
593
|
}
|
|
560
594
|
const usable = NATIVE_PROBE_TARGETS.filter((vendor) => vendors[vendor].available);
|
|
561
595
|
const available = usable.length > 0;
|
|
@@ -571,6 +605,7 @@ export async function probeNativeSessionProducer(input) {
|
|
|
571
605
|
producer,
|
|
572
606
|
diagnostic: producerDiagnostic(identity, "complete", available ? `result=compatible_${usable.join("+")}` : `result=no_compatible_vendor ${vendorResults}`),
|
|
573
607
|
vendors,
|
|
608
|
+
purposes,
|
|
574
609
|
};
|
|
575
610
|
}
|
|
576
611
|
function parseNativeSetupRecoveryReport(value) {
|
|
@@ -600,7 +635,7 @@ function parseNativeSetupRecoveryReport(value) {
|
|
|
600
635
|
* omits the host-issued scope; Setup exposes only its separately authorized
|
|
601
636
|
* bootstrap root, which is revalidated against the durable host configuration.
|
|
602
637
|
*/
|
|
603
|
-
export function readNativeLaunchSpec(cwd, purpose, vendor, scopeId, binding, mcp, materializationState, authorizedWorkspace, providerLaunch) {
|
|
638
|
+
export function readNativeLaunchSpec(cwd, purpose, vendor, scopeId, binding, mcp, materializationState, authorizedWorkspace, providerLaunch, componentReceipt, selection) {
|
|
604
639
|
const ownershipRoot = binding?.path ?? authorizedWorkspace;
|
|
605
640
|
const root = materializationState && ownershipRoot
|
|
606
641
|
? validateNativeSessionWorkspace(materializationState, ownershipRoot, cwd)
|
|
@@ -616,19 +651,28 @@ export function readNativeLaunchSpec(cwd, purpose, vendor, scopeId, binding, mcp
|
|
|
616
651
|
throw new InteractiveLaunchError("native session launch specification is invalid");
|
|
617
652
|
}
|
|
618
653
|
const value = record(raw);
|
|
654
|
+
const isV5 = componentReceipt !== undefined;
|
|
655
|
+
if (isV5 && (!mcp || purpose !== "analysis"))
|
|
656
|
+
throw new InteractiveLaunchError("native component launch requires a bound analysis session");
|
|
619
657
|
const isV4 = mcp !== undefined;
|
|
620
658
|
const isSetupV4 = isV4 && purpose === "setup";
|
|
621
|
-
const keys = ["version", "target", "purpose", "executable", "argv", "agent", ...(isV4 ? ["mcp", ...(isSetupV4 ? ["bootstrap_root"] : [])] : ["scope"]), "cwd", "artifact_root", "handoff_path"];
|
|
659
|
+
const keys = ["version", "target", "purpose", "executable", "argv", "agent", ...(isV5 ? ["component_host"] : []), ...(isV4 ? ["mcp", ...(isSetupV4 ? ["bootstrap_root"] : [])] : ["scope"]), "cwd", "artifact_root", "handoff_path"];
|
|
622
660
|
const target = targetForProvider(vendor);
|
|
623
661
|
const executable = vendor === "claude" ? "claude" : "codex";
|
|
624
|
-
const expectedAgent =
|
|
625
|
-
const scopeEntry =
|
|
662
|
+
const expectedAgent = expectedAgentFor(purpose, vendor, selection);
|
|
663
|
+
const scopeEntry = scopeEntryFor(purpose, vendor, selection);
|
|
626
664
|
if (!value || !hasExactlyKeys(value, keys) ||
|
|
627
|
-
value.version !== (isV4 ? "4" : "2") || value.target !== target || value.purpose !== purpose || value.executable !== executable ||
|
|
665
|
+
value.version !== (isV5 ? "5" : isV4 ? "4" : "2") || value.target !== target || value.purpose !== purpose || value.executable !== executable ||
|
|
628
666
|
!Array.isArray(value.argv) || !value.argv.every((arg) => typeof arg === "string") || !record(value.agent) ||
|
|
629
667
|
(!isV4 && !record(value.scope)) || (isV4 && !record(value.mcp)) || (isSetupV4 && typeof value.bootstrap_root !== "string") || typeof value.cwd !== "string" || typeof value.artifact_root !== "string" || typeof value.handoff_path !== "string") {
|
|
630
668
|
throw new InteractiveLaunchError("native session launch specification is incompatible");
|
|
631
669
|
}
|
|
670
|
+
if (componentReceipt) {
|
|
671
|
+
const planPath = path.join(root, ".warble", "component-plans.json");
|
|
672
|
+
if (lstatSync(planPath).isSymbolicLink() || !contained(root, realpathSync(planPath)))
|
|
673
|
+
throw new InteractiveLaunchError("native component plan is outside its scope");
|
|
674
|
+
readNativeComponentPlans(readFileSync(planPath, "utf8"), value.component_host, componentReceipt);
|
|
675
|
+
}
|
|
632
676
|
const welcome = welcomePromptFor(purpose);
|
|
633
677
|
const baseArgv = scopeEntry
|
|
634
678
|
? isV4 ? [welcome] : []
|
|
@@ -648,7 +692,7 @@ export function readNativeLaunchSpec(cwd, purpose, vendor, scopeId, binding, mcp
|
|
|
648
692
|
// "agent" descriptor is a distinct kind (`claude_scope`) carrying the profile id rather than
|
|
649
693
|
// `claude_agent`/`expectedAgent`. See `claudeScopeEntry` above.
|
|
650
694
|
const expectedAgentKind = scopeEntry ? "claude_scope" : vendor === "claude" ? "claude_agent" : "codex_skill";
|
|
651
|
-
const expectedAgentName = scopeEntry ? NATIVE_DISPATCH_REGISTRY[purpose].profile : expectedAgent;
|
|
695
|
+
const expectedAgentName = scopeEntry ? (customSelection(purpose, selection)?.id ?? NATIVE_DISPATCH_REGISTRY[purpose].profile) : expectedAgent;
|
|
652
696
|
if (!hasExactlyKeys(agent, ["kind", "name"]) || (!isV4 && !validV2Scope) ||
|
|
653
697
|
JSON.stringify(value.argv) !== JSON.stringify(baseArgv) || agent.kind !== expectedAgentKind || agent.name !== expectedAgentName ||
|
|
654
698
|
(isV4 && (!launchMcp || !hasExactlyKeys(launchMcp, ["server_name", "credential_env_var"]) || launchMcp.server_name !== "genbi_session" || launchMcp.credential_env_var !== NATIVE_MCP_CREDENTIAL_ENV_VAR)))
|
|
@@ -703,7 +747,7 @@ export function readNativeLaunchSpec(cwd, purpose, vendor, scopeId, binding, mcp
|
|
|
703
747
|
? [...(scopeEntry ? [] : ["--agent", expectedAgent]), "--resume", providerLaunch.handle, ...(isV4 ? [welcome] : [])]
|
|
704
748
|
: ["--dangerously-bypass-hook-trust", "-c", 'tui.resume_cwd="current"', "resume", providerLaunch.handle, ...(isV4 ? [welcome] : [])]
|
|
705
749
|
: spawnBaseArgv;
|
|
706
|
-
return { version: isV4 ? "4" : "2", target, executable, argv, cwd: root, artifact_root: root, handoff_path: handoffPath, ...(bootstrapRoot ? { bootstrap_root: bootstrapRoot } : {}) };
|
|
750
|
+
return { version: isV5 ? "5" : isV4 ? "4" : "2", target, executable, argv, cwd: root, artifact_root: root, handoff_path: handoffPath, ...(bootstrapRoot ? { bootstrap_root: bootstrapRoot } : {}) };
|
|
707
751
|
}
|
|
708
752
|
/**
|
|
709
753
|
* Materializes only host-owned, short-lived producer inputs. The scope and
|
|
@@ -722,6 +766,7 @@ export async function dispatchNativeArtifacts(input) {
|
|
|
722
766
|
const dir = mkdtempSync(path.join(tempRoot, "genbi-native-dispatch-"));
|
|
723
767
|
const scopePath = path.join(dir, "scope.json");
|
|
724
768
|
const mcpPath = path.join(dir, "mcp.json");
|
|
769
|
+
const hostPath = path.join(dir, "host.json");
|
|
725
770
|
try {
|
|
726
771
|
const directory = lstatSync(dir);
|
|
727
772
|
if (!directory.isDirectory() || directory.isSymbolicLink() || !contained(tempRoot, dir))
|
|
@@ -737,18 +782,20 @@ export async function dispatchNativeArtifacts(input) {
|
|
|
737
782
|
if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o777) !== 0o600)
|
|
738
783
|
throw new InteractiveLaunchError("native session materialization failed");
|
|
739
784
|
};
|
|
785
|
+
if (input.componentHost) {
|
|
786
|
+
writeFileSync(hostPath, JSON.stringify(input.componentHost), { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
787
|
+
validateDescriptorFile(hostPath);
|
|
788
|
+
}
|
|
740
789
|
validateDescriptorFile(scopePath);
|
|
741
790
|
if (input.mcp)
|
|
742
791
|
validateDescriptorFile(mcpPath);
|
|
743
|
-
|
|
744
|
-
if (input.producer)
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
child.once("exit", (code) => code === 0 ? resolve() : reject(new InteractiveLaunchError("native session materialization failed")));
|
|
751
|
-
});
|
|
792
|
+
assertNativeProducerIdentity(input.producer);
|
|
793
|
+
if (input.producer.executable !== input.warbleBin)
|
|
794
|
+
throw new InteractiveLaunchError("native session materialization failed");
|
|
795
|
+
const executable = input.producer.executable;
|
|
796
|
+
const args = ["dispatch", input.irPath, "--target", input.target, "--purpose", input.purpose, "--native-scope", scopePath, ...(input.mcp ? ["--native-mcp", mcpPath] : []), ...(input.componentHost ? ["--native-host", hostPath] : []), "--out", input.cwd];
|
|
797
|
+
await runNativeDispatchProcess({ executable, args, cwd: input.cwd, env: nativeProcessEnvironment(input.env),
|
|
798
|
+
...(input.signal ? { signal: input.signal } : {}) }).catch(() => { throw new InteractiveLaunchError("native session materialization failed"); });
|
|
752
799
|
}
|
|
753
800
|
finally {
|
|
754
801
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -756,7 +803,28 @@ export async function dispatchNativeArtifacts(input) {
|
|
|
756
803
|
}
|
|
757
804
|
export class NativeSessionService {
|
|
758
805
|
options;
|
|
806
|
+
runtimeHost;
|
|
807
|
+
nativeHome;
|
|
808
|
+
wrenShim;
|
|
809
|
+
sourceWrenHome;
|
|
810
|
+
nodeExecutable;
|
|
811
|
+
producerExecutable;
|
|
812
|
+
vendorExecutables;
|
|
813
|
+
childToolDirectories;
|
|
814
|
+
conversations = new Map();
|
|
759
815
|
sessions = new Map();
|
|
816
|
+
componentAdmissions = new Map();
|
|
817
|
+
componentCleanups = new Set();
|
|
818
|
+
componentCleanupFailed = false;
|
|
819
|
+
runtimeCleanupFailed = false;
|
|
820
|
+
pendingTerminalCleanup = new Set();
|
|
821
|
+
pendingHomeCleanup = new Set();
|
|
822
|
+
shuttingDown = false;
|
|
823
|
+
shutdownController = new AbortController();
|
|
824
|
+
shutdownPromise;
|
|
825
|
+
pendingCreates = new Set();
|
|
826
|
+
runtimeSpecs = new Map();
|
|
827
|
+
codexWrenHomes = new Map();
|
|
760
828
|
leaseTimers = new Map();
|
|
761
829
|
/** Open/reconnect coalescing only lasts while a matching launch is in flight. */
|
|
762
830
|
launches = new Map();
|
|
@@ -773,6 +841,41 @@ export class NativeSessionService {
|
|
|
773
841
|
fixtureMaterializationState;
|
|
774
842
|
constructor(options) {
|
|
775
843
|
this.options = options;
|
|
844
|
+
if (options.prepareComponents && !options.prepareComponentProbe)
|
|
845
|
+
throw new Error("Native component execution requires a producer probe");
|
|
846
|
+
this.runtimeHost = options.runtimeHost ?? new RuntimeHost({
|
|
847
|
+
// Compatibility default for server-side unit seams only. The real BFF
|
|
848
|
+
// composes and injects its one policy-owned host in `bin.ts`.
|
|
849
|
+
selected: "local",
|
|
850
|
+
deployment: "development",
|
|
851
|
+
localAvailable: options.terminalHostAvailable ?? (async () => true),
|
|
852
|
+
});
|
|
853
|
+
this.nativeHome = realpathSync(options.nativeHome ?? os.homedir());
|
|
854
|
+
this.wrenShim = options.wrenShim ?? path.join(this.nativeHome, ".local", "bin", "wren");
|
|
855
|
+
const capturedSourceWrenHome = options.sourceWrenHome
|
|
856
|
+
? undefined
|
|
857
|
+
: process.env["WREN_HOME"]?.trim() || path.join(os.homedir(), ".wren");
|
|
858
|
+
this.sourceWrenHome = options.sourceWrenHome ?? (() => capturedSourceWrenHome);
|
|
859
|
+
this.nodeExecutable = options.nodeExecutable ?? attestNativeExecutable("node", process.execPath);
|
|
860
|
+
const capturedPath = options.pathValue;
|
|
861
|
+
this.producerExecutable = resolveNativeExecutable("producer", options.warbleBin, capturedPath);
|
|
862
|
+
const capturedVendors = {
|
|
863
|
+
...options.vendorExecutables,
|
|
864
|
+
};
|
|
865
|
+
for (const vendor of NATIVE_VENDORS) {
|
|
866
|
+
if (!capturedVendors[vendor]) {
|
|
867
|
+
const resolved = resolveNativeExecutable("vendor", vendor, capturedPath)
|
|
868
|
+
?? (this.legacyFixtureMode() ? attestNativeExecutable("vendor", process.execPath) : undefined);
|
|
869
|
+
if (resolved)
|
|
870
|
+
capturedVendors[vendor] = resolved;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
this.vendorExecutables = Object.freeze(capturedVendors);
|
|
874
|
+
this.childToolDirectories = Object.freeze([...new Set([
|
|
875
|
+
...(options.childToolDirectories ?? []),
|
|
876
|
+
path.dirname(this.nodeExecutable.executable),
|
|
877
|
+
...Object.values(capturedVendors).filter((value) => value !== undefined).map((value) => path.dirname(value.executable)),
|
|
878
|
+
])]);
|
|
776
879
|
this.idleTtlMs = options.idleTtlMs ?? options.postClaimDetachGraceMs ?? NATIVE_SESSION_IDLE_TTL_MS;
|
|
777
880
|
if (!Number.isSafeInteger(this.idleTtlMs) || this.idleTtlMs <= 0)
|
|
778
881
|
throw new RangeError("native session idle TTL must be a positive integer");
|
|
@@ -810,11 +913,27 @@ export class NativeSessionService {
|
|
|
810
913
|
async productionProducerPreflight() {
|
|
811
914
|
if (this.legacyFixtureMode())
|
|
812
915
|
return undefined;
|
|
813
|
-
const result = await probeNativeSessionProducer({
|
|
916
|
+
const result = await probeNativeSessionProducer({
|
|
917
|
+
warbleBin: this.options.warbleBin,
|
|
918
|
+
irPaths: this.options.irPaths,
|
|
919
|
+
...(this.options.prepareComponents && this.options.prepareComponentProbe ? { prepareComponents: this.options.prepareComponentProbe } : {}),
|
|
920
|
+
nativeHome: this.nativeHome,
|
|
921
|
+
nodeExecutable: this.nodeExecutable,
|
|
922
|
+
childToolDirectories: this.childToolDirectories,
|
|
923
|
+
...(this.options.resolveManagedCodexWrenRuntime ? { resolveManagedCodexWrenRuntime: this.options.resolveManagedCodexWrenRuntime } : {}),
|
|
924
|
+
...(this.options.pathValue !== undefined ? { pathValue: this.options.pathValue } : {}),
|
|
925
|
+
wrenShim: this.wrenShim,
|
|
926
|
+
});
|
|
814
927
|
const log = result.available ? console.info : console.warn;
|
|
815
928
|
log(`[native-sessions] producer preflight ${result.diagnostic}`);
|
|
816
929
|
return result;
|
|
817
930
|
}
|
|
931
|
+
async assertSelectedRuntimeReady() {
|
|
932
|
+
const readiness = await this.runtimeHost.probe();
|
|
933
|
+
if (readiness.selectedReadiness.state !== "ready")
|
|
934
|
+
throw new InteractiveLaunchError(readiness.selectedReadiness.message);
|
|
935
|
+
return readiness;
|
|
936
|
+
}
|
|
818
937
|
/**
|
|
819
938
|
* SQLite is the durable source of truth, but a native PTY and its attach
|
|
820
939
|
* capability are deliberately process-local. A BFF restart (or a lease
|
|
@@ -837,7 +956,7 @@ export class NativeSessionService {
|
|
|
837
956
|
// yield while the durable row is still being initialized. It has never
|
|
838
957
|
// owned an attachable capability, so treating it as a missing terminal
|
|
839
958
|
// would race creation and could terminalize a PTY that is about to start.
|
|
840
|
-
if ((row.status !== "running" && row.status !== "detached") || this.
|
|
959
|
+
if ((row.status !== "running" && row.status !== "detached") || this.liveCapability(row.id))
|
|
841
960
|
return row;
|
|
842
961
|
// Commit the durable fence first. The cleanup below is best effort and
|
|
843
962
|
// cannot leave a browser-facing row claiming that it can reattach.
|
|
@@ -850,10 +969,13 @@ export class NativeSessionService {
|
|
|
850
969
|
this.recoveryCapabilities.delete(row.id);
|
|
851
970
|
this.options.artifactService?.revoke(this.artifactCredentials.get(row.id));
|
|
852
971
|
this.artifactCredentials.delete(row.id);
|
|
972
|
+
this.cleanupRuntime(row.id);
|
|
853
973
|
return stopped;
|
|
854
974
|
}
|
|
855
975
|
/** A legacy persisted Runtime must be repaired before it can authorize a PTY. */
|
|
856
976
|
assertRuntimeDispatchable() {
|
|
977
|
+
if (this.shuttingDown)
|
|
978
|
+
throw new InteractiveLaunchError("native session host is shutting down");
|
|
857
979
|
if (!this.options.store.hasExplicitRuntimeSettings())
|
|
858
980
|
return;
|
|
859
981
|
const correction = runtimeSettingsCorrection(this.options.store.getRuntimeSettings());
|
|
@@ -877,14 +999,17 @@ export class NativeSessionService {
|
|
|
877
999
|
if (!dispatch)
|
|
878
1000
|
throw new InteractiveLaunchError("native sessions require a saved Runtime & authentication binding");
|
|
879
1001
|
const binding = input.purpose === "setup" ? undefined : this.options.getBinding();
|
|
880
|
-
const
|
|
1002
|
+
const profileId = requestedProfileId(input.purpose, input.profile);
|
|
1003
|
+
const entryVerb = selectedNativeEntry(input.purpose, dispatch.provider, input.entryVerb);
|
|
1004
|
+
const reusable = this.list().find((row) => this.matchesOpenScope(row, input.purpose, dispatch.target, runtime.generation, binding, profileId) && selectedNativeEntry(row.purpose, row.vendor, row.entryVerb) === entryVerb);
|
|
881
1005
|
if (reusable) {
|
|
882
1006
|
const recoveryCapability = this.recoveryCapabilities.get(reusable.id);
|
|
883
|
-
return { row: reusable, capability: this.
|
|
1007
|
+
return { row: reusable, capability: this.liveCapability(reusable.id), ...(recoveryCapability ? { recoveryCapability } : {}) };
|
|
884
1008
|
}
|
|
1009
|
+
const selectedEntry = selectedNativeEntry(input.purpose, dispatch.provider, input.entryVerb);
|
|
885
1010
|
const scopeKey = input.purpose === "setup"
|
|
886
1011
|
? JSON.stringify([input.purpose, dispatch.target, runtime.generation, "bootstrap"])
|
|
887
|
-
: JSON.stringify([input.purpose, dispatch.target, runtime.generation, binding?.identity ?? "unbound", binding?.generation ?? null, binding?.revision ?? null]);
|
|
1012
|
+
: JSON.stringify([input.purpose, dispatch.target, runtime.generation, binding?.identity ?? "unbound", binding?.generation ?? null, binding?.revision ?? null, selectedEntry, profileId]);
|
|
888
1013
|
return this.singleFlight(`open:${scopeKey}`, scopeKey, input, binding, runtime);
|
|
889
1014
|
}
|
|
890
1015
|
/** Opens the exact row selected by the browser after revalidating its scope. */
|
|
@@ -902,7 +1027,7 @@ export class NativeSessionService {
|
|
|
902
1027
|
if (!row || !this.matchesOpenScope(row, input.purpose, dispatch.target, runtime.generation, binding))
|
|
903
1028
|
throw new InteractiveLaunchError("native session is unavailable");
|
|
904
1029
|
const recoveryCapability = this.recoveryCapabilities.get(row.id);
|
|
905
|
-
return { row, capability: this.
|
|
1030
|
+
return { row, capability: this.liveCapability(row.id), ...(recoveryCapability ? { recoveryCapability } : {}) };
|
|
906
1031
|
}
|
|
907
1032
|
/**
|
|
908
1033
|
* Starts a separate TUI even when a matching session is live. The caller's
|
|
@@ -920,9 +1045,11 @@ export class NativeSessionService {
|
|
|
920
1045
|
if (!dispatch)
|
|
921
1046
|
throw new InteractiveLaunchError("native sessions require a saved Runtime & authentication binding");
|
|
922
1047
|
const binding = input.purpose === "setup" ? undefined : this.options.getBinding();
|
|
1048
|
+
const profileId = requestedProfileId(input.purpose, input.profile);
|
|
1049
|
+
const selectedEntry = selectedNativeEntry(input.purpose, dispatch.provider, input.entryVerb);
|
|
923
1050
|
const scopeKey = input.purpose === "setup"
|
|
924
1051
|
? JSON.stringify([input.purpose, dispatch.target, runtime.generation, "bootstrap"])
|
|
925
|
-
: JSON.stringify([input.purpose, dispatch.target, runtime.generation, binding?.identity ?? "unbound", binding?.generation ?? null, binding?.revision ?? null]);
|
|
1052
|
+
: JSON.stringify([input.purpose, dispatch.target, runtime.generation, binding?.identity ?? "unbound", binding?.generation ?? null, binding?.revision ?? null, selectedEntry, profileId]);
|
|
926
1053
|
return this.replayStartSeparate(`start:${input.idempotencyKey}`, scopeKey, input, binding, runtime);
|
|
927
1054
|
}
|
|
928
1055
|
/**
|
|
@@ -961,6 +1088,8 @@ export class NativeSessionService {
|
|
|
961
1088
|
}
|
|
962
1089
|
/** Browser-visible resume state uses the same current-scope fence as launch, plus *why* when it fails. */
|
|
963
1090
|
resumeAvailability(row) {
|
|
1091
|
+
if (row.transport === "conversation" || (row.vendor === "codex" && this.options.directCodex?.terminal))
|
|
1092
|
+
return { available: false, cause: "no_resume_handle" };
|
|
964
1093
|
if (row.status !== "exited" && row.status !== "stopped" && row.status !== "interrupted" && row.status !== "failed" && row.status !== "stale") {
|
|
965
1094
|
return { available: false, cause: "not_terminal" };
|
|
966
1095
|
}
|
|
@@ -987,7 +1116,7 @@ export class NativeSessionService {
|
|
|
987
1116
|
* no global Codex state and persists only a provider-bound encrypted envelope.
|
|
988
1117
|
*/
|
|
989
1118
|
captureCodexResumeHandle(row) {
|
|
990
|
-
if (row.vendor !== "codex" || this.options.store.hasAvailableNativeSessionResume(row.id, "codex"))
|
|
1119
|
+
if (row.vendor !== "codex" || this.options.directCodex?.terminal || this.options.store.hasAvailableNativeSessionResume(row.id, "codex"))
|
|
991
1120
|
return;
|
|
992
1121
|
const state = this.materializationState();
|
|
993
1122
|
if (!state)
|
|
@@ -1016,7 +1145,12 @@ export class NativeSessionService {
|
|
|
1016
1145
|
const runtime = !storedRuntime.configured && this.legacyFixtureMode()
|
|
1017
1146
|
? { configured: true, generation: 0, provider: row.vendor, target: targetForProvider(row.vendor), targetLabel: nativeTargetLabel(row.vendor) }
|
|
1018
1147
|
: storedRuntime;
|
|
1019
|
-
const
|
|
1148
|
+
const registryDispatch = dispatchForPurpose(row.purpose, runtime);
|
|
1149
|
+
// An analysis row resumes inside the profile it ran in; whether that profile is still admitted is
|
|
1150
|
+
// the launch's question (it resolves the id again), not this scope fence's.
|
|
1151
|
+
const dispatch = registryDispatch && row.purpose === "analysis" && row.dispatchProfile !== null
|
|
1152
|
+
? { ...registryDispatch, profile: row.dispatchProfile }
|
|
1153
|
+
: registryDispatch;
|
|
1020
1154
|
const binding = row.purpose === "setup" ? undefined : this.options.getBinding();
|
|
1021
1155
|
const matches = dispatch?.provider === row.vendor && row.dispatchProfile === dispatch.profile && row.dispatchTarget === dispatch.target && row.runtimeGeneration === runtime.generation
|
|
1022
1156
|
&& (row.purpose === "setup"
|
|
@@ -1059,7 +1193,7 @@ export class NativeSessionService {
|
|
|
1059
1193
|
const reserved = this.options.store.reserveNativeSessionResume({
|
|
1060
1194
|
sourceSessionId: source.id, idempotencyKey, scopeFingerprint: fingerprint, sealedHandle: sealedChild,
|
|
1061
1195
|
child: {
|
|
1062
|
-
id: childId, purpose: source.purpose, vendor: source.vendor, agent:
|
|
1196
|
+
id: childId, purpose: source.purpose, vendor: source.vendor, agent: source.agent, entryVerb: selectedNativeEntry(source.purpose, source.vendor, source.entryVerb), scopeKind: source.scopeKind, scopeId: childScopeId,
|
|
1063
1197
|
dispatchProfile: scope.dispatch.profile, dispatchTarget: scope.dispatch.target, runtimeGeneration: scope.runtime.generation,
|
|
1064
1198
|
...(scope.binding ? { projectIdentity: scope.binding.identity, bindingGeneration: scope.binding.generation, projectRevision: scope.binding.revision } : {}),
|
|
1065
1199
|
},
|
|
@@ -1068,7 +1202,7 @@ export class NativeSessionService {
|
|
|
1068
1202
|
throw new InteractiveLaunchError("native session resume is unavailable");
|
|
1069
1203
|
if (!reserved.created)
|
|
1070
1204
|
return this.recoverReservedResume(reserved.row.id, scope);
|
|
1071
|
-
return this.create({ purpose: source.purpose, ...(this.legacyFixtureMode() ? { vendor: source.vendor } : {}) }, scope.binding, scope.runtime, { row: reserved.row, handle });
|
|
1205
|
+
return this.create({ purpose: source.purpose, entryVerb: selectedNativeEntry(source.purpose, source.vendor, source.entryVerb), ...(source.purpose === "analysis" && source.dispatchProfile !== null ? { profile: source.dispatchProfile } : {}), ...(this.legacyFixtureMode() ? { vendor: source.vendor } : {}) }, scope.binding, scope.runtime, { row: reserved.row, handle });
|
|
1072
1206
|
}
|
|
1073
1207
|
async recoverReservedResume(id, scope) {
|
|
1074
1208
|
const row = this.get(id);
|
|
@@ -1096,7 +1230,7 @@ export class NativeSessionService {
|
|
|
1096
1230
|
this.failReservedResume(row.id, "native session resume reservation cannot be recovered");
|
|
1097
1231
|
throw error;
|
|
1098
1232
|
}
|
|
1099
|
-
return this.create({ purpose: row.purpose, ...(this.legacyFixtureMode() ? { vendor: row.vendor } : {}) }, scope.binding, scope.runtime, { row, handle });
|
|
1233
|
+
return this.create({ purpose: row.purpose, entryVerb: selectedNativeEntry(row.purpose, row.vendor, row.entryVerb), ...(row.purpose === "analysis" && row.dispatchProfile !== null ? { profile: row.dispatchProfile } : {}), ...(this.legacyFixtureMode() ? { vendor: row.vendor } : {}) }, scope.binding, scope.runtime, { row, handle });
|
|
1100
1234
|
}
|
|
1101
1235
|
failReservedResume(id, failure) {
|
|
1102
1236
|
const row = this.options.store.getNativeSession(id);
|
|
@@ -1104,8 +1238,11 @@ export class NativeSessionService {
|
|
|
1104
1238
|
this.options.store.transitionNativeSession(id, "failed", { failure, ended: true });
|
|
1105
1239
|
this.options.store.invalidateNativeSessionResume(id);
|
|
1106
1240
|
}
|
|
1107
|
-
matchesOpenScope(row, purpose, target, generation, binding) {
|
|
1108
|
-
if (row.purpose !== purpose || row.dispatchTarget !== target || row.runtimeGeneration !== generation || !this.
|
|
1241
|
+
matchesOpenScope(row, purpose, target, generation, binding, profileId) {
|
|
1242
|
+
if (row.purpose !== purpose || row.dispatchTarget !== target || row.runtimeGeneration !== generation || !this.liveCapability(row.id))
|
|
1243
|
+
return false;
|
|
1244
|
+
// A live session inside another conversation profile is not this one, however its scope matches.
|
|
1245
|
+
if (profileId !== undefined && (row.dispatchProfile ?? NATIVE_DISPATCH_REGISTRY[purpose].profile) !== profileId)
|
|
1109
1246
|
return false;
|
|
1110
1247
|
if (row.status !== "creating" && row.status !== "running" && row.status !== "detached")
|
|
1111
1248
|
return false;
|
|
@@ -1141,7 +1278,7 @@ export class NativeSessionService {
|
|
|
1141
1278
|
// A successful response may contain an attach capability. Never replay it
|
|
1142
1279
|
// after the owning PTY/capability has gone away; failures carry no terminal
|
|
1143
1280
|
// capability and remain safely replayable within the same fenced scope.
|
|
1144
|
-
return !replay.result?.capability || this.
|
|
1281
|
+
return !replay.result?.capability || this.liveCapability(replay.result.row.id) === replay.result.capability;
|
|
1145
1282
|
}
|
|
1146
1283
|
trimStartSeparateReplays() {
|
|
1147
1284
|
for (const [key, replay] of this.startSeparateReplays) {
|
|
@@ -1177,12 +1314,110 @@ export class NativeSessionService {
|
|
|
1177
1314
|
this.launches.delete(launchKey);
|
|
1178
1315
|
}
|
|
1179
1316
|
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Resolves the profile a launch runs inside. The default needs no registry; any other id is
|
|
1319
|
+
* Claude-only (the Codex entry is a purpose skill with no scope form), accepts no entry verb
|
|
1320
|
+
* (its entry form was decided at admission), and must still be admitted right now.
|
|
1321
|
+
*/
|
|
1322
|
+
async selectProfile(purpose, profile, provider, entryVerb, resumedRow) {
|
|
1323
|
+
const id = requestedProfileId(purpose, profile);
|
|
1324
|
+
if (resumedRow && (resumedRow.dispatchProfile ?? NATIVE_DISPATCH_REGISTRY[purpose].profile) !== id)
|
|
1325
|
+
throw new InteractiveLaunchError("native session profile changed during resume");
|
|
1326
|
+
// A system purpose always runs its registry profile; the analysis default needs no registry either.
|
|
1327
|
+
if (purpose !== "analysis")
|
|
1328
|
+
return { id, entry: { kind: "scope" } };
|
|
1329
|
+
if (id === DEFAULT_CONVERSATION_PROFILE)
|
|
1330
|
+
return DEFAULT_PROFILE_SELECTION;
|
|
1331
|
+
if (provider !== "claude")
|
|
1332
|
+
throw new InteractiveLaunchError(CODEX_CUSTOM_PROFILE_UNAVAILABLE);
|
|
1333
|
+
// An entry verb beside a selected profile was already refused by `selectedNativeEntry`: Claude
|
|
1334
|
+
// never takes one, and the Codex vendor was refused just above. Nothing to re-check here.
|
|
1335
|
+
void entryVerb;
|
|
1336
|
+
if (!this.options.resolveConversationProfile)
|
|
1337
|
+
throw new InteractiveLaunchError("conversation profile selection is not configured");
|
|
1338
|
+
try {
|
|
1339
|
+
const resolved = await this.options.resolveConversationProfile(id);
|
|
1340
|
+
if (resolved.id !== id)
|
|
1341
|
+
throw new InteractiveLaunchError("conversation profile resolution is inconsistent");
|
|
1342
|
+
return resolved;
|
|
1343
|
+
}
|
|
1344
|
+
catch (error) {
|
|
1345
|
+
if (error instanceof InteractiveLaunchError)
|
|
1346
|
+
throw error;
|
|
1347
|
+
throw new InteractiveLaunchError(error instanceof Error ? error.message : "conversation profile is unavailable");
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/** Analysis readiness, narrowed per conversation profile by the registry's verdict and the vendor. */
|
|
1351
|
+
async conversationProfileReadiness(analysis, provider) {
|
|
1352
|
+
if (!this.options.listConversationProfiles)
|
|
1353
|
+
return undefined;
|
|
1354
|
+
let summaries;
|
|
1355
|
+
try {
|
|
1356
|
+
summaries = await this.options.listConversationProfiles();
|
|
1357
|
+
}
|
|
1358
|
+
catch {
|
|
1359
|
+
return undefined;
|
|
1360
|
+
}
|
|
1361
|
+
const result = {};
|
|
1362
|
+
for (const summary of summaries) {
|
|
1363
|
+
const custom = summary.id !== DEFAULT_CONVERSATION_PROFILE;
|
|
1364
|
+
// A profile the registry refused is refused for good; say that before any runtime reason,
|
|
1365
|
+
// which may come and go, so a picker can tell the two apart.
|
|
1366
|
+
const reason = (!summary.selectable ? summary.reason ?? "this profile is unavailable" : undefined)
|
|
1367
|
+
?? analysis.reason
|
|
1368
|
+
?? (custom && provider !== undefined && provider !== "claude" ? CODEX_CUSTOM_PROFILE_UNAVAILABLE : undefined);
|
|
1369
|
+
result[summary.id] = {
|
|
1370
|
+
id: summary.id,
|
|
1371
|
+
scopeKind: "bound_project",
|
|
1372
|
+
profile: summary.id,
|
|
1373
|
+
...(analysis.target !== undefined ? { target: analysis.target } : {}),
|
|
1374
|
+
...(analysis.targetLabel !== undefined ? { targetLabel: analysis.targetLabel } : {}),
|
|
1375
|
+
available: analysis.available && reason === undefined,
|
|
1376
|
+
...(reason !== undefined ? { reason } : {}),
|
|
1377
|
+
...(custom
|
|
1378
|
+
? { ...(summary.entryKind !== undefined ? { entryKind: summary.entryKind } : {}), ...(summary.entryVerb !== undefined ? { entryVerb: summary.entryVerb } : {}) }
|
|
1379
|
+
: { entryKind: provider === "codex" ? "agent" : "scope" }),
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
return result;
|
|
1383
|
+
}
|
|
1180
1384
|
async readiness() {
|
|
1181
1385
|
const terminalHostAvailable = this.options.terminalHostAvailable ?? (async () => true);
|
|
1182
1386
|
const executableAvailable = (vendor) => this.options.executableAvailable
|
|
1183
|
-
? this.options.executableAvailable(vendor)
|
|
1184
|
-
:
|
|
1387
|
+
? this.options.executableAvailable(vendor) && this.vendorExecutables[vendor] !== undefined
|
|
1388
|
+
: this.vendorExecutables[vendor] !== undefined;
|
|
1389
|
+
const runtimeHostReadiness = await this.runtimeHost.probe();
|
|
1390
|
+
if (runtimeHostReadiness.selected === "codex-app-server") {
|
|
1391
|
+
const runtime = this.options.store.getNativeRuntimeBinding();
|
|
1392
|
+
const binding = this.options.getBinding();
|
|
1393
|
+
const reason = runtimeHostReadiness.selectedReadiness.state !== "ready" ? runtimeHostReadiness.selectedReadiness.message
|
|
1394
|
+
: this.options.store.hasExplicitRuntimeSettings() && runtimeSettingsCorrection(this.options.store.getRuntimeSettings()) ? runtimeSettingsCorrection(this.options.store.getRuntimeSettings())
|
|
1395
|
+
: !this.options.directCodex ? "Direct Codex preparation is unavailable."
|
|
1396
|
+
: !runtime.configured || runtime.provider !== "codex" ? "Select a Codex Runtime binding."
|
|
1397
|
+
: !binding ? "Bind a project before starting a conversation."
|
|
1398
|
+
: this.options.directCodex.terminal && !this.options.directCodex.terminalCertified?.("answer_query") ? "Native terminal version is not certified."
|
|
1399
|
+
: this.options.directCodex.terminal && !(await terminalHostAvailable()) ? "Native terminal host is unavailable." : undefined;
|
|
1400
|
+
const purposes = Object.fromEntries(NATIVE_PURPOSES.map((purpose) => {
|
|
1401
|
+
const unavailable = reason ?? (purpose !== "analysis" ? "This purpose is not supported by direct Codex sessions." : undefined);
|
|
1402
|
+
return [purpose, { scopeKind: NATIVE_DISPATCH_REGISTRY[purpose].scopeKind, profile: NATIVE_DISPATCH_REGISTRY[purpose].profile,
|
|
1403
|
+
target: "codex:interactive", targetLabel: "Codex CLI", available: !unavailable, ...(unavailable ? { reason: unavailable } : {}),
|
|
1404
|
+
vendors: { codex: { available: !unavailable }, claude: { available: false } } }];
|
|
1405
|
+
}));
|
|
1406
|
+
const profiles = await this.conversationProfileReadiness(purposes.analysis, "codex");
|
|
1407
|
+
return { runtime, runtimeHost: runtimeHostReadiness, purposes, ...(profiles ? { profiles } : {}), ...purposes, mcp: { server: "GenBI MCP", tool: "save_dashboard", destination: "GenBI Artifacts", available: false, reason: "Direct conversations use host-owned tools." } };
|
|
1408
|
+
}
|
|
1185
1409
|
const hostAvailable = await terminalHostAvailable();
|
|
1410
|
+
const selectedRuntimeUnavailable = runtimeHostReadiness.selectedReadiness.state === "ready"
|
|
1411
|
+
? undefined
|
|
1412
|
+
: runtimeHostReadiness.selectedReadiness.message;
|
|
1413
|
+
const codexBackendReadiness = runtimeHostReadiness.backends["codex-app-server"];
|
|
1414
|
+
const codexWrenRuntimeUnavailable = this.legacyFixtureMode()
|
|
1415
|
+
? undefined
|
|
1416
|
+
: this.options.resolveManagedCodexWrenRuntime === undefined
|
|
1417
|
+
? CODEX_WREN_RUNTIME_UNPROVISIONED.message
|
|
1418
|
+
: codexBackendReadiness.state === "ready"
|
|
1419
|
+
? undefined
|
|
1420
|
+
: codexBackendReadiness.message;
|
|
1186
1421
|
const mcpReadiness = this.options.artifactService?.readiness();
|
|
1187
1422
|
const runtime = this.options.store.getNativeRuntimeBinding();
|
|
1188
1423
|
const runtimeCorrection = this.options.store.hasExplicitRuntimeSettings()
|
|
@@ -1190,7 +1425,7 @@ export class NativeSessionService {
|
|
|
1190
1425
|
: undefined;
|
|
1191
1426
|
const legacyFixture = !runtime.configured && this.legacyFixtureMode();
|
|
1192
1427
|
const producer = this.legacyFixtureMode() ? undefined : await this.productionProducerPreflight();
|
|
1193
|
-
const legacyProducerAvailable = this.options.producerAvailable ?? (() => this.options.dispatch !== undefined ||
|
|
1428
|
+
const legacyProducerAvailable = this.options.producerAvailable ?? (() => this.options.dispatch !== undefined || this.producerExecutable !== undefined);
|
|
1194
1429
|
const result = {};
|
|
1195
1430
|
for (const purpose of NATIVE_PURPOSES) {
|
|
1196
1431
|
const definition = NATIVE_DISPATCH_REGISTRY[purpose];
|
|
@@ -1210,21 +1445,22 @@ export class NativeSessionService {
|
|
|
1210
1445
|
// Only the configured vendor's own probe may block this purpose. Probing
|
|
1211
1446
|
// both and collapsing them into one boolean let an unselected vendor veto
|
|
1212
1447
|
// the selected one.
|
|
1213
|
-
const selectedProducer = producer && dispatch ? producer.vendors[dispatch.provider] : undefined;
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
:
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1448
|
+
const selectedProducer = producer && dispatch ? (producer.purposes?.[purpose][dispatch.provider] ?? producer.vendors[dispatch.provider]) : undefined;
|
|
1449
|
+
const sharedReason = !dispatch && !legacyFixture
|
|
1450
|
+
? "native sessions require a saved Runtime & authentication binding"
|
|
1451
|
+
: !cwd
|
|
1452
|
+
? purpose === "setup" ? "native setup sessions require a workspace root" : "native sessions require a current bound project"
|
|
1453
|
+
: workspaceUnavailable
|
|
1454
|
+
? "native session workspace is unavailable"
|
|
1455
|
+
: selectedProducer && !selectedProducer.available
|
|
1456
|
+
? selectedProducer.reason ?? "native session producer is incompatible"
|
|
1457
|
+
: !irPath || !(this.options.dispatch !== undefined || configuredIrPath(irPath)) || !legacyProducerAvailable()
|
|
1458
|
+
? "native session materialization prerequisites are unavailable"
|
|
1459
|
+
: mcpReadiness && !mcpReadiness.available
|
|
1460
|
+
? mcpReadiness.reason
|
|
1461
|
+
: undefined;
|
|
1462
|
+
const vendorRuntimeUnavailable = dispatch?.provider === "codex" ? codexWrenRuntimeUnavailable : undefined;
|
|
1463
|
+
const reason = runtimeCorrection ?? selectedRuntimeUnavailable ?? vendorRuntimeUnavailable ?? sharedReason;
|
|
1228
1464
|
const executable = dispatch?.provider === "claude" ? "claude" : "codex";
|
|
1229
1465
|
const unavailable = reason ?? (!hostAvailable
|
|
1230
1466
|
? "native terminal host cannot spawn local processes on this machine"
|
|
@@ -1236,12 +1472,14 @@ export class NativeSessionService {
|
|
|
1236
1472
|
// one is unusable and why instead of only that "the producer" is.
|
|
1237
1473
|
if (!legacyFixture && producer)
|
|
1238
1474
|
for (const vendor of NATIVE_VENDORS) {
|
|
1239
|
-
const probe = producer.vendors[vendor];
|
|
1240
|
-
vendorReadiness[vendor] =
|
|
1241
|
-
? { available: false, reason:
|
|
1242
|
-
:
|
|
1243
|
-
? { available: false, reason:
|
|
1244
|
-
:
|
|
1475
|
+
const probe = producer.purposes?.[purpose][vendor] ?? producer.vendors[vendor];
|
|
1476
|
+
vendorReadiness[vendor] = vendor === "codex" && codexWrenRuntimeUnavailable
|
|
1477
|
+
? { available: false, reason: codexWrenRuntimeUnavailable }
|
|
1478
|
+
: probe?.available === false
|
|
1479
|
+
? { available: false, reason: probe.reason ?? "native session producer is incompatible" }
|
|
1480
|
+
: !executableAvailable(vendor)
|
|
1481
|
+
? { available: false, reason: `the ${vendor === "claude" ? "claude" : "codex"} interactive CLI is not available on this machine` }
|
|
1482
|
+
: { available: true };
|
|
1245
1483
|
}
|
|
1246
1484
|
if (legacyFixture)
|
|
1247
1485
|
for (const vendor of NATIVE_VENDORS) {
|
|
@@ -1279,9 +1517,12 @@ export class NativeSessionService {
|
|
|
1279
1517
|
}));
|
|
1280
1518
|
return legacy;
|
|
1281
1519
|
}
|
|
1520
|
+
const profiles = await this.conversationProfileReadiness(result.analysis, runtime.configured ? runtime.provider : undefined);
|
|
1282
1521
|
return {
|
|
1283
1522
|
runtime,
|
|
1523
|
+
runtimeHost: runtimeHostReadiness,
|
|
1284
1524
|
purposes: result,
|
|
1525
|
+
...(profiles ? { profiles } : {}),
|
|
1285
1526
|
mcp: this.options.artifactService?.health() ?? {
|
|
1286
1527
|
server: "GenBI MCP",
|
|
1287
1528
|
tool: "save_dashboard",
|
|
@@ -1294,27 +1535,74 @@ export class NativeSessionService {
|
|
|
1294
1535
|
}
|
|
1295
1536
|
async create(input, binding = input.purpose === "setup" ? undefined : this.options.getBinding(), capturedRuntime = this.options.store.getNativeRuntimeBinding(), resumed) {
|
|
1296
1537
|
this.assertRuntimeDispatchable();
|
|
1538
|
+
const pending = this.createCaptured(input, binding, capturedRuntime, resumed);
|
|
1539
|
+
this.pendingCreates.add(pending);
|
|
1540
|
+
void pending.finally(() => this.pendingCreates.delete(pending)).catch(() => { });
|
|
1541
|
+
return pending;
|
|
1542
|
+
}
|
|
1543
|
+
async createCaptured(input, binding = input.purpose === "setup" ? undefined : this.options.getBinding(), capturedRuntime = this.options.store.getNativeRuntimeBinding(), resumed) {
|
|
1544
|
+
this.assertRuntimeDispatchable();
|
|
1545
|
+
const runtimeReadiness = await this.assertSelectedRuntimeReady();
|
|
1297
1546
|
const purpose = input.purpose;
|
|
1298
1547
|
const legacyRuntime = !capturedRuntime.configured && input.vendor
|
|
1299
1548
|
? { configured: true, generation: 0, provider: input.vendor, target: targetForProvider(input.vendor), targetLabel: nativeTargetLabel(input.vendor) }
|
|
1300
1549
|
: capturedRuntime;
|
|
1301
|
-
const
|
|
1302
|
-
if (!
|
|
1550
|
+
const registryDispatch = dispatchForPurpose(purpose, legacyRuntime);
|
|
1551
|
+
if (!registryDispatch)
|
|
1303
1552
|
throw new InteractiveLaunchError("native sessions require a saved Runtime & authentication binding");
|
|
1553
|
+
const entryVerb = selectedNativeEntry(purpose, registryDispatch.provider, input.entryVerb);
|
|
1554
|
+
if (resumed && selectedNativeEntry(purpose, registryDispatch.provider, resumed.row.entryVerb) !== entryVerb)
|
|
1555
|
+
throw new InteractiveLaunchError("native session entry changed during resume");
|
|
1556
|
+
const selection = await this.selectProfile(purpose, input.profile, registryDispatch.provider, input.entryVerb, resumed?.row);
|
|
1557
|
+
// The launch tuple names the selected profile; everything else is the registry's for the purpose.
|
|
1558
|
+
const dispatchDefinition = { ...registryDispatch, profile: selection.id };
|
|
1559
|
+
if (runtimeReadiness.selected === "codex-app-server") {
|
|
1560
|
+
if (dispatchDefinition.provider !== "codex" || purpose !== "analysis" || !binding || resumed || !this.options.directCodex || customSelection(purpose, selection)) {
|
|
1561
|
+
throw new InteractiveLaunchError("direct Codex session preparation is unavailable");
|
|
1562
|
+
}
|
|
1563
|
+
return this.createDirectCodex(binding, legacyRuntime, entryVerb);
|
|
1564
|
+
}
|
|
1565
|
+
if (runtimeReadiness.selected !== "local")
|
|
1566
|
+
throw new InteractiveLaunchError("native session backend is unavailable");
|
|
1567
|
+
let managedCodexWrenRuntime;
|
|
1568
|
+
if (dispatchDefinition.provider === "codex" && !this.legacyFixtureMode()) {
|
|
1569
|
+
const codexReadiness = runtimeReadiness.backends["codex-app-server"];
|
|
1570
|
+
if (codexReadiness.state !== "ready")
|
|
1571
|
+
throw new InteractiveLaunchError(codexReadiness.message);
|
|
1572
|
+
if (!this.options.resolveManagedCodexWrenRuntime)
|
|
1573
|
+
throw new InteractiveLaunchError(CODEX_WREN_RUNTIME_UNPROVISIONED.message);
|
|
1574
|
+
try {
|
|
1575
|
+
managedCodexWrenRuntime = this.options.resolveManagedCodexWrenRuntime();
|
|
1576
|
+
}
|
|
1577
|
+
catch {
|
|
1578
|
+
throw new InteractiveLaunchError(CODEX_WREN_RUNTIME_UNPROVISIONED.message);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1304
1581
|
if (purpose !== "setup" && !binding)
|
|
1305
1582
|
throw new InteractiveLaunchError("native session requires a current bound project");
|
|
1306
1583
|
const configuredCwd = binding?.path ?? this.options.workspaceRoot;
|
|
1307
1584
|
const configuredIr = this.options.irPaths[purpose];
|
|
1308
1585
|
if (!configuredCwd || !configuredIr)
|
|
1309
|
-
throw new InteractiveLaunchError(`native ${
|
|
1586
|
+
throw new InteractiveLaunchError(`native ${selection.id} session is not configured`);
|
|
1310
1587
|
// Compile for this binding rather than dispatching the profile's golden.
|
|
1311
1588
|
// Falls back to the configured path only where no resolver is wired (the
|
|
1312
1589
|
// fixture-mode tests), never silently in production — see `bin.ts`.
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1590
|
+
// Compiling for this binding runs before the launch try/catch below, so a
|
|
1591
|
+
// failure here reached the HTTP route unlogged and unclassified — the case
|
|
1592
|
+
// where the server printed nothing at all and the browser said only "native
|
|
1593
|
+
// session launch failed".
|
|
1594
|
+
let irPath;
|
|
1595
|
+
try {
|
|
1596
|
+
irPath = this.options.resolveDispatchIr === undefined
|
|
1597
|
+
? configuredIr
|
|
1598
|
+
: await this.options.resolveDispatchIr(purpose, binding, selection.sourceDir);
|
|
1599
|
+
}
|
|
1600
|
+
catch (error) {
|
|
1601
|
+
logNativeLaunchFailure(purpose, dispatchDefinition.provider, "dispatch_ir", error);
|
|
1602
|
+
throw error;
|
|
1603
|
+
}
|
|
1316
1604
|
const producer = this.legacyFixtureMode() ? undefined : await this.productionProducerPreflight();
|
|
1317
|
-
const launchProducer = producer ? producer.vendors[dispatchDefinition.provider] : undefined;
|
|
1605
|
+
const launchProducer = producer ? (producer.purposes?.[purpose][dispatchDefinition.provider] ?? producer.vendors[dispatchDefinition.provider]) : undefined;
|
|
1318
1606
|
if (producer && (!launchProducer?.available || !producer.producer)) {
|
|
1319
1607
|
throw new InteractiveLaunchError(launchProducer?.reason ?? "native session producer is incompatible");
|
|
1320
1608
|
}
|
|
@@ -1334,7 +1622,8 @@ export class NativeSessionService {
|
|
|
1334
1622
|
initialSealedHandle = sealNativeClaudeResumeHandle(state, id, sessionId);
|
|
1335
1623
|
providerLaunch = { kind: "initial", sessionId };
|
|
1336
1624
|
}
|
|
1337
|
-
|
|
1625
|
+
this.assertRuntimeDispatchable();
|
|
1626
|
+
const row = resumed?.row ?? this.options.store.createNativeSession({ id, purpose, vendor: dispatchDefinition.provider, agent: expectedAgentFor(purpose, dispatchDefinition.provider, selection), entryVerb, scopeKind: dispatchDefinition.scopeKind, scopeId,
|
|
1338
1627
|
dispatchProfile: dispatchDefinition.profile, dispatchTarget: dispatchDefinition.target, runtimeGeneration: legacyRuntime.generation,
|
|
1339
1628
|
...(binding ? { projectIdentity: binding.identity, bindingGeneration: binding.generation, projectRevision: binding.revision } : {}) });
|
|
1340
1629
|
if (initialSealedHandle)
|
|
@@ -1360,7 +1649,7 @@ export class NativeSessionService {
|
|
|
1360
1649
|
if (!authorizedWorkspace || !materializationState)
|
|
1361
1650
|
throw new InteractiveLaunchError("native session workspace is unavailable");
|
|
1362
1651
|
const cwd = createNativeSessionWorkspace(materializationState, authorizedWorkspace, id);
|
|
1363
|
-
const producerAvailable = this.options.producerAvailable ?? (() => this.options.dispatch !== undefined ||
|
|
1652
|
+
const producerAvailable = this.options.producerAvailable ?? (() => this.options.dispatch !== undefined || this.producerExecutable !== undefined);
|
|
1364
1653
|
if (!(this.options.dispatch !== undefined || configuredIrPath(irPath)) || !producerAvailable())
|
|
1365
1654
|
throw new InteractiveLaunchError("native session materialization prerequisites are unavailable");
|
|
1366
1655
|
const mcpReadiness = artifactService?.readiness();
|
|
@@ -1370,19 +1659,58 @@ export class NativeSessionService {
|
|
|
1370
1659
|
if (mcp)
|
|
1371
1660
|
this.artifactCredentials.set(id, mcp.credential);
|
|
1372
1661
|
let wrenRuntime;
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1662
|
+
wrenRuntime = managedCodexWrenRuntime;
|
|
1663
|
+
const vendorExecutable = this.vendorExecutables[dispatchDefinition.provider];
|
|
1664
|
+
if (!vendorExecutable)
|
|
1665
|
+
throw new InteractiveLaunchError(`the ${dispatchDefinition.provider} interactive CLI is not available on this machine`);
|
|
1666
|
+
const producerExecutable = producer?.producer
|
|
1667
|
+
? attestNativeExecutable("producer", producer.producer.executable)
|
|
1668
|
+
: this.options.dispatch !== undefined
|
|
1669
|
+
? attestNativeExecutable("producer", this.nodeExecutable.executable)
|
|
1670
|
+
: resolveNativeExecutable("producer", this.options.warbleBin, undefined);
|
|
1671
|
+
if (!producerExecutable)
|
|
1672
|
+
throw new InteractiveLaunchError("native session materialization prerequisites are unavailable");
|
|
1673
|
+
const toolDirectories = [...new Set([
|
|
1674
|
+
...this.childToolDirectories,
|
|
1675
|
+
path.dirname(producerExecutable.executable),
|
|
1676
|
+
...(wrenRuntime ? [path.join(wrenRuntime.tool_root, "bin"), path.dirname(wrenRuntime.interpreter)] : []),
|
|
1677
|
+
])];
|
|
1678
|
+
const dispatchEnvironment = buildNativeChildEnvironment({
|
|
1679
|
+
toolDirectories,
|
|
1680
|
+
home: this.nativeHome,
|
|
1681
|
+
...(binding ? { projectPath: binding.path } : {}),
|
|
1682
|
+
...(mcp ? { mcpCredential: mcp.credential } : {}),
|
|
1683
|
+
...(purpose === "setup" ? { setupBootstrapRoot: authorizedWorkspace } : {}),
|
|
1684
|
+
});
|
|
1685
|
+
const scope = { version: "3", kind: row.scopeKind, scope_id: scopeId, cwd, entry: entryFor(purpose, dispatchDefinition.provider, entryVerb, selection), ...(purpose === "setup" ? { bootstrap_root: authorizedWorkspace } : {}), ...(wrenRuntime ? { wren_runtime: wrenRuntime } : {}), ...(binding ? { binding: { project_identity: binding.identity, generation: String(binding.generation), revision: binding.revision } } : {}) };
|
|
1686
|
+
const componentIr = this.options.prepareComponents && binding && purpose === "analysis" ? readFileSync(irPath, "utf8") : undefined;
|
|
1687
|
+
const preparedComponents = componentIr && binding ? await this.options.prepareComponents({ session: row, binding, irDocument: componentIr, scope }) : undefined;
|
|
1688
|
+
if (preparedComponents) {
|
|
1689
|
+
const identity = preparedComponents.identity;
|
|
1690
|
+
if (!mcp || identity.session_id !== id || identity.vendor !== row.vendor || identity.runtime_generation !== String(row.runtimeGeneration)
|
|
1691
|
+
|| identity.binding.project_identity !== binding?.identity || identity.binding.generation !== String(binding?.generation) || identity.binding.revision !== binding?.revision)
|
|
1692
|
+
throw new InteractiveLaunchError("native component execution identity is incompatible");
|
|
1380
1693
|
}
|
|
1381
|
-
const
|
|
1694
|
+
const componentReceipt = preparedComponents && componentIr ? {
|
|
1695
|
+
identity: structuredClone(preparedComponents.identity), irDocument: componentIr, scopeDocument: JSON.stringify(scope),
|
|
1696
|
+
hostRoots: structuredClone(preparedComponents.hostRoots), contexts: structuredClone(preparedComponents.contexts),
|
|
1697
|
+
} : undefined;
|
|
1382
1698
|
const dispatch = this.options.dispatch ?? dispatchNativeArtifacts;
|
|
1383
1699
|
validateNativeSessionWorkspace(materializationState, authorizedWorkspace, cwd);
|
|
1384
1700
|
launchPhase = "dispatch";
|
|
1385
|
-
await dispatch({
|
|
1701
|
+
await dispatch({
|
|
1702
|
+
warbleBin: producerExecutable.executable,
|
|
1703
|
+
producer: producer?.producer ?? { executable: producerExecutable.executable, identity: producerExecutable.digest },
|
|
1704
|
+
irPath,
|
|
1705
|
+
target: dispatchDefinition.target,
|
|
1706
|
+
cwd,
|
|
1707
|
+
purpose,
|
|
1708
|
+
scope,
|
|
1709
|
+
...(preparedComponents ? { componentHost: nativeComponentHostContract(preparedComponents) } : {}),
|
|
1710
|
+
...(mcp ? { mcp } : {}),
|
|
1711
|
+
env: dispatchEnvironment,
|
|
1712
|
+
signal: this.shutdownController.signal,
|
|
1713
|
+
});
|
|
1386
1714
|
if (!input.vendor && !sameNativeRuntimeBinding(capturedRuntime, this.options.store.getNativeRuntimeBinding())) {
|
|
1387
1715
|
this.options.store.transitionNativeSession(id, "stale", { failure: "native runtime binding changed before launch", ended: true });
|
|
1388
1716
|
throw new InteractiveLaunchError("native session is stale because the runtime binding changed");
|
|
@@ -1392,20 +1720,75 @@ export class NativeSessionService {
|
|
|
1392
1720
|
throw new InteractiveLaunchError("native session is stale because the bound project changed");
|
|
1393
1721
|
}
|
|
1394
1722
|
launchPhase = "launch_spec";
|
|
1395
|
-
const
|
|
1723
|
+
const launchSpec = readNativeLaunchSpec(cwd, purpose, dispatchDefinition.provider, scopeId, binding, mcp, materializationState, authorizedWorkspace, providerLaunch, componentReceipt, selection);
|
|
1724
|
+
if (componentReceipt && preparedComponents) {
|
|
1725
|
+
const launch = JSON.parse(readFileSync(path.join(cwd, ".warble", "interactive-launch.json"), "utf8"));
|
|
1726
|
+
const plans = readNativeComponentPlans(readFileSync(path.join(cwd, ".warble", "component-plans.json"), "utf8"), launch.component_host, componentReceipt);
|
|
1727
|
+
const current = () => {
|
|
1728
|
+
const live = this.options.store.getNativeSession(id);
|
|
1729
|
+
if (!live || !["creating", "running", "detached"].includes(live.status) || !sameBinding(binding, this.options.getBinding())
|
|
1730
|
+
|| (!input.vendor && !sameNativeRuntimeBinding(capturedRuntime, this.options.store.getNativeRuntimeBinding())))
|
|
1731
|
+
return undefined;
|
|
1732
|
+
try {
|
|
1733
|
+
preparedComponents.assertCurrent();
|
|
1734
|
+
}
|
|
1735
|
+
catch {
|
|
1736
|
+
return undefined;
|
|
1737
|
+
}
|
|
1738
|
+
return componentReceipt.identity;
|
|
1739
|
+
};
|
|
1740
|
+
this.assertRuntimeDispatchable();
|
|
1741
|
+
this.componentAdmissions.set(id, new NativeComponentAdmission(plans, current, (root, plan, signal) => preparedComponents.host(root, plan, signal)));
|
|
1742
|
+
}
|
|
1396
1743
|
let codexWrenHome;
|
|
1397
|
-
|
|
1398
|
-
|
|
1744
|
+
// Hosted v5 entries route work through fixed MCP roots. Their outer Codex
|
|
1745
|
+
// session retains the producer's read-only, network-disabled profile; only
|
|
1746
|
+
// the direct v4 route receives a Wren home and bound-project read grant.
|
|
1747
|
+
if (launchSpec.version === "4" && dispatchDefinition.provider === "codex") {
|
|
1748
|
+
if (wrenRuntime && binding) {
|
|
1399
1749
|
launchPhase = "codex_wren_home";
|
|
1400
|
-
codexWrenHome = (this.options.prepareCodexWrenHome ??
|
|
1750
|
+
codexWrenHome = (this.options.prepareCodexWrenHome ?? materializeCodexWrenHome)({
|
|
1401
1751
|
runtime: wrenRuntime,
|
|
1402
1752
|
projectPath: binding.path,
|
|
1403
1753
|
cwd,
|
|
1754
|
+
sourceWrenHome: this.sourceWrenHome(),
|
|
1755
|
+
home: this.nativeHome,
|
|
1756
|
+
toolDirectories,
|
|
1404
1757
|
});
|
|
1405
1758
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1759
|
+
else if (wrenRuntime && purpose === "setup") {
|
|
1760
|
+
launchPhase = "codex_wren_home";
|
|
1761
|
+
codexWrenHome = createEmptyCodexWrenHome(cwd);
|
|
1762
|
+
}
|
|
1763
|
+
if (binding) {
|
|
1764
|
+
launchPhase = "codex_project_read";
|
|
1765
|
+
grantCodexBoundProjectRead(cwd, binding.path, codexWrenHome?.dataRoots);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
codexWrenHome?.assertActive?.();
|
|
1769
|
+
if (codexWrenHome)
|
|
1770
|
+
this.codexWrenHomes.set(id, codexWrenHome);
|
|
1771
|
+
const executableIdentities = [this.nodeExecutable, producerExecutable, vendorExecutable];
|
|
1772
|
+
if (wrenRuntime) {
|
|
1773
|
+
executableIdentities.push(attestNativeExecutable("wren", wrenRuntime.launcher), attestNativeExecutable("python", wrenRuntime.interpreter));
|
|
1408
1774
|
}
|
|
1775
|
+
// Legacy Claude tools use the same server-selected profile store as Setup.
|
|
1776
|
+
// Hosted v5 entries receive only their broker and must not inherit this store.
|
|
1777
|
+
const claudeWrenHome = launchSpec.version === "4" && dispatchDefinition.provider === "claude"
|
|
1778
|
+
&& binding && this.options.sourceWrenHome ? this.sourceWrenHome() : undefined;
|
|
1779
|
+
const runtimeSpec = buildNativeRuntimeSpec({
|
|
1780
|
+
backend: runtimeReadiness.selected,
|
|
1781
|
+
vendor: dispatchDefinition.provider,
|
|
1782
|
+
executables: executableIdentities,
|
|
1783
|
+
toolDirectories,
|
|
1784
|
+
workspace: cwd,
|
|
1785
|
+
home: this.nativeHome,
|
|
1786
|
+
...(binding ? { binding } : {}),
|
|
1787
|
+
...(codexWrenHome ? { sessionWrenHome: codexWrenHome.home } : claudeWrenHome ? { sessionWrenHome: claudeWrenHome } : {}),
|
|
1788
|
+
...(dispatchDefinition.provider === "codex" && this.options.codexHome ? { codexHome: this.options.codexHome } : {}),
|
|
1789
|
+
...(mcp ? { mcpCredential: mcp.credential } : {}),
|
|
1790
|
+
...(launchSpec.bootstrap_root ? { setupBootstrapRoot: launchSpec.bootstrap_root } : {}),
|
|
1791
|
+
});
|
|
1409
1792
|
launchPhase = "terminal_manager";
|
|
1410
1793
|
const terminalManager = await this.options.terminalManager();
|
|
1411
1794
|
if (binding && !sameBinding(binding, this.options.getBinding())) {
|
|
@@ -1417,20 +1800,13 @@ export class NativeSessionService {
|
|
|
1417
1800
|
throw new InteractiveLaunchError("native session is stale because the runtime binding changed");
|
|
1418
1801
|
}
|
|
1419
1802
|
validateNativeSessionWorkspace(materializationState, authorizedWorkspace, cwd);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
...(binding ? { WREN_PROJECT_HOME: binding.path } : {}),
|
|
1424
|
-
...(codexWrenHome ? { WREN_HOME: codexWrenHome.home } : {}),
|
|
1425
|
-
};
|
|
1426
|
-
// Never inherit a caller/operator value: only the launch spec's already
|
|
1427
|
-
// revalidated Setup root may reach the native TUI.
|
|
1428
|
-
delete terminalEnv[NATIVE_SETUP_BOOTSTRAP_ROOT_ENV_VAR];
|
|
1429
|
-
if (spec.bootstrap_root)
|
|
1430
|
-
terminalEnv[NATIVE_SETUP_BOOTSTRAP_ROOT_ENV_VAR] = spec.bootstrap_root;
|
|
1803
|
+
codexWrenHome?.assertActive?.();
|
|
1804
|
+
assertNativeRuntimeSpec(runtimeSpec);
|
|
1805
|
+
this.assertRuntimeDispatchable();
|
|
1431
1806
|
launchPhase = "terminal_start";
|
|
1432
|
-
const terminal = terminalManager.start(
|
|
1807
|
+
const terminal = terminalManager.start({ ...launchSpec, hostExecutable: vendorExecutable.executable }, { id, capability }, runtimeSpec.childEnvironment, NO_INTERACTIVE_TERMINAL_LEASES);
|
|
1433
1808
|
this.sessions.set(id, terminal);
|
|
1809
|
+
this.runtimeSpecs.set(id, runtimeSpec);
|
|
1434
1810
|
// Mark running before registering a replaying exit listener. A PTY can
|
|
1435
1811
|
// exit between start() and this registration; `onExit` immediately
|
|
1436
1812
|
// replays that state and must win over this transition, never be
|
|
@@ -1445,6 +1821,7 @@ export class NativeSessionService {
|
|
|
1445
1821
|
this.recoveryCapabilities.delete(id);
|
|
1446
1822
|
artifactService?.revoke(this.artifactCredentials.get(id));
|
|
1447
1823
|
this.artifactCredentials.delete(id);
|
|
1824
|
+
this.cleanupRuntime(id);
|
|
1448
1825
|
const current = this.options.store.getNativeSession(id);
|
|
1449
1826
|
if (current && (current.status === "running" || current.status === "detached"))
|
|
1450
1827
|
this.options.store.transitionNativeSession(id, "exited", { exitCode, ended: true });
|
|
@@ -1455,6 +1832,7 @@ export class NativeSessionService {
|
|
|
1455
1832
|
}
|
|
1456
1833
|
catch (error) {
|
|
1457
1834
|
logNativeLaunchFailure(purpose, dispatchDefinition.provider, launchPhase, error);
|
|
1835
|
+
this.cleanupRuntime(id);
|
|
1458
1836
|
this.recoveryCapabilities.delete(id);
|
|
1459
1837
|
artifactService?.revoke(this.artifactCredentials.get(id));
|
|
1460
1838
|
this.artifactCredentials.delete(id);
|
|
@@ -1476,6 +1854,153 @@ export class NativeSessionService {
|
|
|
1476
1854
|
throw error;
|
|
1477
1855
|
}
|
|
1478
1856
|
}
|
|
1857
|
+
liveCapability(id) {
|
|
1858
|
+
return this.sessions.get(id)?.capability ?? this.conversations.get(id)?.conversation.capability;
|
|
1859
|
+
}
|
|
1860
|
+
async createDirectCodex(binding, runtime, entryVerb) {
|
|
1861
|
+
const provisioner = this.options.directCodex;
|
|
1862
|
+
if (provisioner.terminal && !provisioner.terminalCertified?.(entryVerb))
|
|
1863
|
+
throw new InteractiveLaunchError("Native terminal version is not certified");
|
|
1864
|
+
const permit = provisioner.backend.prepareLaunch(); // before rows, credentials or workspace writes
|
|
1865
|
+
const capturedBinding = structuredClone(binding);
|
|
1866
|
+
const capturedRuntime = structuredClone(runtime);
|
|
1867
|
+
const id = newId("native-session");
|
|
1868
|
+
const controller = new AbortController();
|
|
1869
|
+
const signal = AbortSignal.any([controller.signal, this.shutdownController.signal]);
|
|
1870
|
+
let prepared;
|
|
1871
|
+
let conversation;
|
|
1872
|
+
let renderer;
|
|
1873
|
+
let closing;
|
|
1874
|
+
const assertActive = () => {
|
|
1875
|
+
signal.throwIfAborted();
|
|
1876
|
+
this.assertRuntimeDispatchable();
|
|
1877
|
+
const row = this.options.store.getNativeSession(id);
|
|
1878
|
+
if (!sameBinding(capturedBinding, this.options.getBinding()) || !sameNativeRuntimeBinding(capturedRuntime, this.options.store.getNativeRuntimeBinding())
|
|
1879
|
+
|| (row && !["creating", "running", "detached"].includes(row.status)))
|
|
1880
|
+
throw new InteractiveLaunchError("native session scope is stale");
|
|
1881
|
+
};
|
|
1882
|
+
const close = () => closing ??= (async () => {
|
|
1883
|
+
controller.abort();
|
|
1884
|
+
const results = await Promise.allSettled([renderer?.close(), conversation?.close(), prepared?.input.tools.close()]);
|
|
1885
|
+
// Keep materialized resources if process/tool cleanup could not be confirmed.
|
|
1886
|
+
if (results.some((result) => result.status === "rejected"))
|
|
1887
|
+
throw new InteractiveLaunchError("native conversation cleanup failed");
|
|
1888
|
+
await prepared?.dispose();
|
|
1889
|
+
})();
|
|
1890
|
+
try {
|
|
1891
|
+
assertActive();
|
|
1892
|
+
permit.assertActive();
|
|
1893
|
+
const row = this.options.store.createNativeSession({ id, ...(!provisioner.terminal ? { transport: "conversation" } : {}), purpose: "analysis", vendor: "codex", agent: agentFor("analysis", "codex"),
|
|
1894
|
+
entryVerb, scopeKind: "bound_project", scopeId: newId("native-scope"), dispatchProfile: NATIVE_DISPATCH_REGISTRY.analysis.profile,
|
|
1895
|
+
dispatchTarget: "codex:interactive", runtimeGeneration: capturedRuntime.generation,
|
|
1896
|
+
projectIdentity: capturedBinding.identity, bindingGeneration: capturedBinding.generation, projectRevision: capturedBinding.revision });
|
|
1897
|
+
prepared = await provisioner.prepare({ session: row, binding: capturedBinding, permit, signal, assertActive });
|
|
1898
|
+
assertActive();
|
|
1899
|
+
permit.assertActive();
|
|
1900
|
+
prepared.input.tools.assertCurrent();
|
|
1901
|
+
const hostGuard = prepared.input.assertScopeActive;
|
|
1902
|
+
conversation = new CodexConversation(provisioner.backend, permit, { ...prepared.input,
|
|
1903
|
+
assertScopeActive: () => { assertActive(); hostGuard(); },
|
|
1904
|
+
}, (state, failure) => {
|
|
1905
|
+
if (state !== "closed" && state !== "failed")
|
|
1906
|
+
return;
|
|
1907
|
+
const current = this.options.store.getNativeSession(id);
|
|
1908
|
+
if (current && ["creating", "running", "detached"].includes(current.status)) {
|
|
1909
|
+
this.options.store.transitionNativeSession(id, state === "failed" ? "failed" : "exited", { ended: true,
|
|
1910
|
+
...(failure ? { failure: `Codex conversation ${failure}` } : {}) });
|
|
1911
|
+
}
|
|
1912
|
+
// Schedule after transition delivers its terminal frame to the attachment.
|
|
1913
|
+
queueMicrotask(() => this.revokeCapabilities([id]));
|
|
1914
|
+
});
|
|
1915
|
+
this.conversations.set(id, { conversation, assertActive, close });
|
|
1916
|
+
await conversation.ready;
|
|
1917
|
+
assertActive();
|
|
1918
|
+
if (provisioner.terminal) {
|
|
1919
|
+
const vendor = prepared.input.spec.executables.vendor;
|
|
1920
|
+
if (!vendor || !prepared.input.model)
|
|
1921
|
+
throw new InteractiveLaunchError("Native terminal configuration is unavailable");
|
|
1922
|
+
renderer = await startCodexTerminal({ id, conversation, manager: await this.options.terminalManager(), vendor,
|
|
1923
|
+
cwd: prepared.input.spec.workspace, model: prepared.input.model, assertActive });
|
|
1924
|
+
this.sessions.set(id, renderer.terminal);
|
|
1925
|
+
}
|
|
1926
|
+
this.options.store.transitionNativeSession(id, "running", { started: true });
|
|
1927
|
+
if (renderer) {
|
|
1928
|
+
renderer.terminal.onExit((exitCode) => {
|
|
1929
|
+
const current = this.options.store.getNativeSession(id);
|
|
1930
|
+
if (current && ["creating", "running", "detached"].includes(current.status)) {
|
|
1931
|
+
this.options.store.transitionNativeSession(id, exitCode === 0 ? "exited" : "failed", { ended: true,
|
|
1932
|
+
...(exitCode !== 0 ? { failure: "Codex terminal exited" } : {}) });
|
|
1933
|
+
}
|
|
1934
|
+
queueMicrotask(() => this.revokeCapabilities([id]));
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
assertActive();
|
|
1938
|
+
this.armLease(id, "native session attachment timed out", NATIVE_SESSION_INITIAL_ATTACHMENT_GRACE_MS);
|
|
1939
|
+
return { row: this.options.store.getNativeSession(id), capability: conversation.capability };
|
|
1940
|
+
}
|
|
1941
|
+
catch (error) {
|
|
1942
|
+
const row = this.options.store.getNativeSession(id);
|
|
1943
|
+
if (row && ["creating", "running", "detached"].includes(row.status))
|
|
1944
|
+
this.options.store.transitionNativeSession(id, "failed", { ended: true, failure: "Direct Codex launch failed" });
|
|
1945
|
+
this.sessions.delete(id);
|
|
1946
|
+
this.conversations.delete(id);
|
|
1947
|
+
permit.release();
|
|
1948
|
+
try {
|
|
1949
|
+
await close();
|
|
1950
|
+
}
|
|
1951
|
+
catch {
|
|
1952
|
+
this.componentCleanupFailed = true;
|
|
1953
|
+
throw new InteractiveLaunchError("native conversation cleanup failed");
|
|
1954
|
+
}
|
|
1955
|
+
throw error;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
attachConversation(id, capability, listener, afterSequence = 0) {
|
|
1959
|
+
if (this.options.directCodex?.terminal)
|
|
1960
|
+
return undefined;
|
|
1961
|
+
const direct = this.conversations.get(id);
|
|
1962
|
+
if (!direct)
|
|
1963
|
+
return undefined;
|
|
1964
|
+
let attachment;
|
|
1965
|
+
try {
|
|
1966
|
+
direct.assertActive();
|
|
1967
|
+
attachment = direct.conversation.attach(capability, listener, afterSequence);
|
|
1968
|
+
}
|
|
1969
|
+
catch {
|
|
1970
|
+
return undefined;
|
|
1971
|
+
}
|
|
1972
|
+
if (!["ready", "running"].includes(direct.conversation.snapshot().state)) {
|
|
1973
|
+
attachment.detach();
|
|
1974
|
+
return undefined;
|
|
1975
|
+
}
|
|
1976
|
+
this.clearLease(id);
|
|
1977
|
+
this.options.store.transitionNativeSession(id, "running");
|
|
1978
|
+
let detached = false;
|
|
1979
|
+
const check = () => { if (detached)
|
|
1980
|
+
throw new InteractiveLaunchError("native attachment is unavailable"); direct.assertActive(); };
|
|
1981
|
+
return { submit: (text) => { check(); return attachment.submit(text); }, interrupt: () => { check(); return attachment.interrupt(); },
|
|
1982
|
+
detach: () => {
|
|
1983
|
+
if (detached)
|
|
1984
|
+
return;
|
|
1985
|
+
detached = true;
|
|
1986
|
+
attachment.detach();
|
|
1987
|
+
if (this.conversations.get(id) === direct && ["ready", "running"].includes(direct.conversation.snapshot().state)) {
|
|
1988
|
+
this.options.store.transitionNativeSession(id, "detached");
|
|
1989
|
+
this.armLease(id, "native session idle TTL expired", this.idleTtlMs);
|
|
1990
|
+
}
|
|
1991
|
+
} };
|
|
1992
|
+
}
|
|
1993
|
+
async stopAndWait(id, capability) {
|
|
1994
|
+
const direct = this.conversations.get(id);
|
|
1995
|
+
if (!direct)
|
|
1996
|
+
return this.stop(id, capability);
|
|
1997
|
+
if (direct.conversation.capability !== capability)
|
|
1998
|
+
return false;
|
|
1999
|
+
this.options.store.stopNativeSessionAndRevokeRecoveryAction(id);
|
|
2000
|
+
this.revokeCapabilities([id]);
|
|
2001
|
+
await direct.close();
|
|
2002
|
+
return true;
|
|
2003
|
+
}
|
|
1479
2004
|
attach(id, capability) {
|
|
1480
2005
|
const row = this.get(id);
|
|
1481
2006
|
const runtime = this.options.store.getNativeRuntimeBinding();
|
|
@@ -1493,6 +2018,7 @@ export class NativeSessionService {
|
|
|
1493
2018
|
return terminal;
|
|
1494
2019
|
}
|
|
1495
2020
|
detach(id) {
|
|
2021
|
+
this.componentAdmissions.get(id)?.cancelActive();
|
|
1496
2022
|
this.sessions.get(id)?.detach();
|
|
1497
2023
|
if (this.sessions.has(id)) {
|
|
1498
2024
|
this.options.store.transitionNativeSession(id, "detached");
|
|
@@ -1520,12 +2046,25 @@ export class NativeSessionService {
|
|
|
1520
2046
|
this.options.artifactService?.revoke(this.artifactCredentials.get(id));
|
|
1521
2047
|
this.artifactCredentials.delete(id);
|
|
1522
2048
|
this.sessions.delete(id);
|
|
2049
|
+
const direct = this.conversations.get(id);
|
|
2050
|
+
this.conversations.delete(id);
|
|
2051
|
+
if (direct) {
|
|
2052
|
+
const cleanup = direct.close();
|
|
2053
|
+
this.componentCleanups.add(cleanup);
|
|
2054
|
+
void cleanup.catch(() => { this.componentCleanupFailed = true; }).finally(() => this.componentCleanups.delete(cleanup));
|
|
2055
|
+
}
|
|
2056
|
+
this.cleanupRuntime(id);
|
|
1523
2057
|
try {
|
|
1524
2058
|
terminal?.close();
|
|
2059
|
+
if (terminal)
|
|
2060
|
+
this.pendingTerminalCleanup.delete(terminal);
|
|
1525
2061
|
}
|
|
1526
2062
|
catch {
|
|
1527
2063
|
// A process kill may fail after the durable session fence committed.
|
|
1528
2064
|
// Capabilities are already unreachable; continue revoking every id.
|
|
2065
|
+
this.runtimeCleanupFailed = true;
|
|
2066
|
+
if (terminal)
|
|
2067
|
+
this.pendingTerminalCleanup.add(terminal);
|
|
1529
2068
|
console.warn("[native-sessions] terminal close failed during capability revocation");
|
|
1530
2069
|
}
|
|
1531
2070
|
}
|
|
@@ -1534,6 +2073,81 @@ export class NativeSessionService {
|
|
|
1534
2073
|
revokeRuntimeCapabilities(ids) { this.revokeCapabilities(ids); }
|
|
1535
2074
|
/** Called after the store advances the bound-project generation. */
|
|
1536
2075
|
revokeBindingCapabilities(ids) { this.revokeCapabilities(ids); }
|
|
2076
|
+
componentTools(id) { return this.componentAdmissions.get(id)?.list() ?? []; }
|
|
2077
|
+
async callComponent(id, tool, input, requestId, signal) {
|
|
2078
|
+
const admission = this.componentAdmissions.get(id);
|
|
2079
|
+
if (!admission)
|
|
2080
|
+
throw new InteractiveLaunchError("native component execution is unavailable");
|
|
2081
|
+
return admission.call(tool, input, requestId, signal);
|
|
2082
|
+
}
|
|
2083
|
+
async drainComponentCleanup() {
|
|
2084
|
+
const cleanup = await Promise.allSettled([...this.componentCleanups]);
|
|
2085
|
+
if (this.componentCleanupFailed || cleanup.some((result) => result.status === "rejected"))
|
|
2086
|
+
throw new InteractiveLaunchError("native component cleanup failed");
|
|
2087
|
+
}
|
|
2088
|
+
shutdown() {
|
|
2089
|
+
this.shuttingDown = true;
|
|
2090
|
+
this.shutdownController.abort();
|
|
2091
|
+
return this.shutdownPromise ??= (async () => {
|
|
2092
|
+
const ids = [...new Set([...this.sessions.keys(), ...this.conversations.keys(), ...this.componentAdmissions.keys()])];
|
|
2093
|
+
for (const id of ids)
|
|
2094
|
+
this.options.store.stopNativeSessionAndRevokeRecoveryAction(id);
|
|
2095
|
+
this.revokeCapabilities(ids);
|
|
2096
|
+
let timer;
|
|
2097
|
+
try {
|
|
2098
|
+
await Promise.race([Promise.allSettled([...this.pendingCreates]), new Promise((_, reject) => {
|
|
2099
|
+
timer = setTimeout(() => reject(new InteractiveLaunchError("native launch cleanup timed out")), 5_000);
|
|
2100
|
+
})]);
|
|
2101
|
+
}
|
|
2102
|
+
finally {
|
|
2103
|
+
clearTimeout(timer);
|
|
2104
|
+
}
|
|
2105
|
+
this.revokeCapabilities([...this.componentAdmissions.keys()]);
|
|
2106
|
+
// Failed resources remain owned after capability revocation, for one final retry.
|
|
2107
|
+
for (const terminal of this.pendingTerminalCleanup) {
|
|
2108
|
+
try {
|
|
2109
|
+
terminal.close();
|
|
2110
|
+
this.pendingTerminalCleanup.delete(terminal);
|
|
2111
|
+
}
|
|
2112
|
+
catch {
|
|
2113
|
+
this.runtimeCleanupFailed = true;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
for (const cleanup of this.pendingHomeCleanup) {
|
|
2117
|
+
try {
|
|
2118
|
+
cleanup();
|
|
2119
|
+
this.pendingHomeCleanup.delete(cleanup);
|
|
2120
|
+
}
|
|
2121
|
+
catch {
|
|
2122
|
+
this.runtimeCleanupFailed = true;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
await this.drainComponentCleanup();
|
|
2126
|
+
if (this.runtimeCleanupFailed)
|
|
2127
|
+
throw new InteractiveLaunchError("native runtime cleanup failed");
|
|
2128
|
+
})();
|
|
2129
|
+
}
|
|
2130
|
+
cleanupRuntime(id) {
|
|
2131
|
+
const admission = this.componentAdmissions.get(id);
|
|
2132
|
+
this.componentAdmissions.delete(id);
|
|
2133
|
+
if (admission) {
|
|
2134
|
+
const cleanup = admission.close();
|
|
2135
|
+
this.componentCleanups.add(cleanup);
|
|
2136
|
+
void cleanup.catch(() => { this.componentCleanupFailed = true; console.warn("[native-sessions] component cleanup failed"); }).finally(() => this.componentCleanups.delete(cleanup));
|
|
2137
|
+
}
|
|
2138
|
+
const home = this.codexWrenHomes.get(id);
|
|
2139
|
+
this.codexWrenHomes.delete(id);
|
|
2140
|
+
this.runtimeSpecs.delete(id);
|
|
2141
|
+
try {
|
|
2142
|
+
home?.cleanup?.();
|
|
2143
|
+
}
|
|
2144
|
+
catch {
|
|
2145
|
+
this.runtimeCleanupFailed = true;
|
|
2146
|
+
if (home?.cleanup)
|
|
2147
|
+
this.pendingHomeCleanup.add(home.cleanup.bind(home));
|
|
2148
|
+
console.warn("[native-sessions] Wren session home cleanup failed");
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
1537
2151
|
/**
|
|
1538
2152
|
* Records only the producer's closed redacted report. Completion remains a
|
|
1539
2153
|
* host decision supplied by the canonical-artifact validator at the route.
|
|
@@ -1598,14 +2212,19 @@ export class NativeSessionService {
|
|
|
1598
2212
|
}
|
|
1599
2213
|
armLease(id, reason, timeoutMs) {
|
|
1600
2214
|
this.clearLease(id);
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
2215
|
+
const timer = setTimeout(() => {
|
|
2216
|
+
if (this.options.store.isClosed()) {
|
|
2217
|
+
this.leaseTimers.delete(id);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (!this.liveCapability(id)) {
|
|
1604
2221
|
this.leaseTimers.delete(id);
|
|
1605
2222
|
return;
|
|
1606
2223
|
}
|
|
1607
2224
|
this.options.store.transitionNativeSession(id, "stopped", { failure: reason, ended: true });
|
|
1608
2225
|
this.revokeCapabilities([id]);
|
|
1609
|
-
}, timeoutMs)
|
|
2226
|
+
}, timeoutMs);
|
|
2227
|
+
timer.unref();
|
|
2228
|
+
this.leaseTimers.set(id, timer);
|
|
1610
2229
|
}
|
|
1611
2230
|
}
|