@wrenai/genbi 0.0.4 → 0.0.6
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 +3 -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 +43 -45
- 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/describe.js +45 -6
- package/dist-server/harness/route/dispatched.d.ts +20 -0
- package/dist-server/harness/route/dispatched.js +122 -24
- package/dist-server/harness/route/host-query.d.ts +93 -0
- package/dist-server/harness/route/host-query.js +257 -0
- 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 +8 -2
- package/dist-server/harness/route/types.d.ts +47 -1
- package/dist-server/harness/route/wren-host-proxy.d.ts +33 -0
- package/dist-server/harness/route/wren-host-proxy.js +228 -0
- package/dist-server/harness/session/run.js +30 -10
- package/dist-server/harness/setup/runner.d.ts +17 -3
- package/dist-server/harness/setup/runner.js +272 -89
- package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
- package/dist-server/harness/setup/yaml-scalar.js +62 -0
- package/dist-server/harness/tools/setup-native.d.ts +7 -0
- package/dist-server/harness/tools/setup-native.js +7 -4
- package/dist-server/server/adopt.js +1 -17
- package/dist-server/server/app.d.ts +1 -0
- package/dist-server/server/app.js +175 -17
- 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 +185 -25
- 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 +23 -4
- package/dist-server/server/fold.js +42 -5
- package/dist-server/server/harness.d.ts +2 -2
- package/dist-server/server/harness.js +33 -7
- package/dist-server/server/host-evidence-grounding.d.ts +21 -0
- package/dist-server/server/host-evidence-grounding.js +93 -0
- 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 +112 -7
- package/dist-server/server/native-artifacts.js +194 -14
- 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 +839 -244
- 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 +86 -26
- 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,21 +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";
|
|
14
|
+
import { isDeepStrictEqual } from "node:util";
|
|
10
15
|
import { WarbleBinaryNotFoundError } from "../harness/compile/errors.js";
|
|
11
|
-
import { InteractiveLaunchError, NO_INTERACTIVE_TERMINAL_LEASES
|
|
16
|
+
import { InteractiveLaunchError, NO_INTERACTIVE_TERMINAL_LEASES } from "./interactive-terminal.js";
|
|
12
17
|
import { newId } from "./db.js";
|
|
13
18
|
import { NATIVE_MCP_CREDENTIAL_ENV_VAR } from "./native-artifacts.js";
|
|
14
19
|
import { dispatchForPurpose, NATIVE_DISPATCH_REGISTRY, NATIVE_PURPOSES, nativeTargetLabel, sameNativeRuntimeBinding, targetForProvider } from "./native-dispatch-registry.js";
|
|
15
20
|
import { createNativeSessionWorkspace, initializeNativeSessionStateBase, nativeSessionStateBaseAvailable, nativeSessionStateBaseAvailableForProject, validateNativeSessionWorkspace } from "./native-session-workspace.js";
|
|
16
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";
|
|
17
25
|
import { runtimeSettingsCorrection } from "./runtime-binding.js";
|
|
18
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";
|
|
19
29
|
export const NATIVE_VENDORS = ["claude", "codex"];
|
|
20
30
|
export { NATIVE_PURPOSES };
|
|
21
31
|
export const NATIVE_SETUP_RECOVERY_MCP_TOOL_NAME = "report_setup_recovery";
|
|
@@ -39,6 +49,7 @@ const START_SEPARATE_REPLAY_LIMIT = 128;
|
|
|
39
49
|
const CODEX_NATIVE_PERMISSION_HEADER = "[permissions.warble_native_wren.filesystem]";
|
|
40
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;
|
|
41
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");
|
|
42
53
|
function logNativeLaunchFailure(purpose, vendor, phase, error) {
|
|
43
54
|
const rejected = error instanceof InteractiveLaunchError;
|
|
44
55
|
// Exception text, paths, credentials and argv stay out of the log for an
|
|
@@ -74,60 +85,6 @@ export function grantCodexBoundProjectRead(cwd, projectPath, dataRoots = []) {
|
|
|
74
85
|
writeFileSync(configPath, updated, { encoding: "utf8", mode: 0o600 });
|
|
75
86
|
chmodSync(configPath, 0o600);
|
|
76
87
|
}
|
|
77
|
-
const MATERIALIZE_WREN_HOME_SCRIPT = String.raw `
|
|
78
|
-
import os
|
|
79
|
-
import pathlib
|
|
80
|
-
import shutil
|
|
81
|
-
import sys
|
|
82
|
-
import yaml
|
|
83
|
-
from wren.profile import expand_profile_secrets, resolve_profile_for_project
|
|
84
|
-
|
|
85
|
-
project = pathlib.Path(sys.argv[1]).resolve(strict=True)
|
|
86
|
-
destination = pathlib.Path(sys.argv[2])
|
|
87
|
-
name, profile = resolve_profile_for_project(project, strict=True)
|
|
88
|
-
if not name or not profile:
|
|
89
|
-
raise SystemExit(2)
|
|
90
|
-
destination.mkdir(mode=0o700, parents=False, exist_ok=False)
|
|
91
|
-
profile_path = destination / "profiles.yml"
|
|
92
|
-
profile_path.write_text(yaml.safe_dump({"active": name, "profiles": {name: profile}}, sort_keys=False), encoding="utf-8")
|
|
93
|
-
os.chmod(profile_path, 0o600)
|
|
94
|
-
project_env = project / ".env"
|
|
95
|
-
if project_env.exists():
|
|
96
|
-
if project_env.is_symlink() or not project_env.is_file():
|
|
97
|
-
raise SystemExit(3)
|
|
98
|
-
session_env = destination / ".env"
|
|
99
|
-
shutil.copyfile(project_env, session_env)
|
|
100
|
-
os.chmod(session_env, 0o600)
|
|
101
|
-
|
|
102
|
-
expanded = expand_profile_secrets(profile)
|
|
103
|
-
data_source = expanded.get("datasource")
|
|
104
|
-
data_root = None
|
|
105
|
-
if data_source in ("duckdb", "local_file"):
|
|
106
|
-
data_root = expanded.get("url")
|
|
107
|
-
elif data_source == "datafusion":
|
|
108
|
-
data_root = expanded.get("source")
|
|
109
|
-
if data_root:
|
|
110
|
-
root = pathlib.Path(data_root).expanduser()
|
|
111
|
-
if not root.is_absolute():
|
|
112
|
-
root = project / root
|
|
113
|
-
print(root.resolve(strict=True))
|
|
114
|
-
`;
|
|
115
|
-
function materializeCodexWrenHome(runtime, projectPath, cwd) {
|
|
116
|
-
const destination = path.join(cwd, ".wren");
|
|
117
|
-
try {
|
|
118
|
-
const output = execFileSync(runtime.venv_python, ["-c", MATERIALIZE_WREN_HOME_SCRIPT, projectPath, destination], {
|
|
119
|
-
cwd: projectPath,
|
|
120
|
-
env: process.env,
|
|
121
|
-
encoding: "utf8",
|
|
122
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
123
|
-
timeout: 10_000,
|
|
124
|
-
});
|
|
125
|
-
return { home: destination, dataRoots: output.trim() ? [output.trim()] : [] };
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
throw new InteractiveLaunchError("native Wren project profile is unavailable");
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
88
|
function nativeSessionRestartReason(target, cause) {
|
|
132
89
|
switch (cause) {
|
|
133
90
|
case "not_terminal":
|
|
@@ -175,6 +132,13 @@ export class NativeSetupRecoveryError extends Error {
|
|
|
175
132
|
* whichever verb the scope declares, after checking the declaration against the compiled IR.
|
|
176
133
|
*/
|
|
177
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
|
+
}
|
|
178
142
|
const agentFor = (purpose, vendor) => vendor === "claude" ? entryVerbFor(purpose) : `genbi-${purpose === "context_enrichment" ? "enrich-context" : purpose}`;
|
|
179
143
|
/**
|
|
180
144
|
* Closed v4 Claude entry contract. This host is the only place the mapping exists: Warble used to
|
|
@@ -203,9 +167,9 @@ const claudeScopeEntry = (purpose, vendor) => vendor === "claude" && !PINNED_CLA
|
|
|
203
167
|
* most by pinning: its profile mounts four selectable components, and a pinned session can only
|
|
204
168
|
* reach the other three by delegating out of the one it was forced to start as.
|
|
205
169
|
*/
|
|
206
|
-
const nativeEntryFor = (purpose, vendor) => claudeScopeEntry(purpose, vendor)
|
|
170
|
+
const nativeEntryFor = (purpose, vendor, entryVerb) => claudeScopeEntry(purpose, vendor)
|
|
207
171
|
? { kind: "scope", prompt: welcomePromptFor(purpose) }
|
|
208
|
-
: { verb: entryVerbFor(purpose), prompt: welcomePromptFor(purpose) };
|
|
172
|
+
: { verb: selectedNativeEntry(purpose, vendor, entryVerb) ?? entryVerbFor(purpose), prompt: welcomePromptFor(purpose) };
|
|
209
173
|
/** Closed producer/host contract for the one initial native TUI prompt. */
|
|
210
174
|
const welcomePromptFor = (purpose) => {
|
|
211
175
|
switch (purpose) {
|
|
@@ -217,6 +181,50 @@ const welcomePromptFor = (purpose) => {
|
|
|
217
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.";
|
|
218
182
|
}
|
|
219
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
|
+
};
|
|
220
228
|
function sameBinding(a, b) {
|
|
221
229
|
return b !== undefined && a.path === b.path && a.identity === b.identity && a.generation === b.generation && a.revision === b.revision;
|
|
222
230
|
}
|
|
@@ -225,6 +233,7 @@ function resumeScopeFingerprint(row) {
|
|
|
225
233
|
row.id, row.purpose, row.vendor, row.scopeKind, row.projectIdentity,
|
|
226
234
|
row.bindingGeneration, row.projectRevision, row.dispatchProfile,
|
|
227
235
|
row.dispatchTarget, row.runtimeGeneration,
|
|
236
|
+
...(row.entryVerb && row.entryVerb !== "answer_query" ? [row.entryVerb] : []),
|
|
228
237
|
])).digest("hex");
|
|
229
238
|
}
|
|
230
239
|
function record(value) {
|
|
@@ -237,25 +246,6 @@ function contained(root, candidate) {
|
|
|
237
246
|
const relative = path.relative(root, candidate);
|
|
238
247
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
239
248
|
}
|
|
240
|
-
function localExecutableAvailable(executable, pathValue = process.env["PATH"]) {
|
|
241
|
-
return localExecutablePath(executable, pathValue) !== undefined;
|
|
242
|
-
}
|
|
243
|
-
function localExecutablePath(executable, pathValue = process.env["PATH"]) {
|
|
244
|
-
const candidates = path.isAbsolute(executable) || executable.includes(path.sep)
|
|
245
|
-
? [path.resolve(executable)]
|
|
246
|
-
: (pathValue ?? "").split(path.delimiter).filter(Boolean).map((directory) => path.join(directory, executable));
|
|
247
|
-
for (const candidate of candidates) {
|
|
248
|
-
try {
|
|
249
|
-
const stat = statSync(candidate);
|
|
250
|
-
if (stat.isFile() && (stat.mode & 0o111) !== 0)
|
|
251
|
-
return realpathSync(candidate);
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
// Try the next PATH entry. The public readiness response never learns this path.
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return undefined;
|
|
258
|
-
}
|
|
259
249
|
function configuredWorkspace(cwd, create) {
|
|
260
250
|
try {
|
|
261
251
|
if (create)
|
|
@@ -407,25 +397,14 @@ function nativeProducerResult(available, identity, phase, detail, reason, produc
|
|
|
407
397
|
vendors: everyVendorUnavailable(available ? { available: true, diagnostic } : { available: false, reason, diagnostic }),
|
|
408
398
|
};
|
|
409
399
|
}
|
|
410
|
-
function executableIdentity(executable) {
|
|
411
|
-
try {
|
|
412
|
-
const metadata = statSync(executable);
|
|
413
|
-
if (!metadata.isFile() || (metadata.mode & 0o111) === 0)
|
|
414
|
-
return undefined;
|
|
415
|
-
return `sha256:${createHash("sha256").update(readFileSync(executable)).digest("hex")}`;
|
|
416
|
-
}
|
|
417
|
-
catch {
|
|
418
|
-
return undefined;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
400
|
/**
|
|
422
401
|
* The configured spelling may be a PATH lookup or retargetable symlink. A
|
|
423
402
|
* preflight result instead owns the resolved executable and its digest. Check
|
|
424
403
|
* both again before creating a durable row and directly before spawning.
|
|
425
404
|
*/
|
|
426
405
|
function assertNativeProducerIdentity(producer) {
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
406
|
+
const current = attestNativeExecutable("producer", producer.executable);
|
|
407
|
+
if (current.executable !== producer.executable || current.digest !== producer.identity) {
|
|
429
408
|
throw new InteractiveLaunchError("native session producer is incompatible");
|
|
430
409
|
}
|
|
431
410
|
}
|
|
@@ -434,7 +413,7 @@ function assertNativeProducerIdentity(producer) {
|
|
|
434
413
|
* output is used only for local help-marker parsing and is never logged or
|
|
435
414
|
* returned: a third-party executable may print arbitrary sensitive text.
|
|
436
415
|
*/
|
|
437
|
-
function runNativeProbe(command, args) {
|
|
416
|
+
function runNativeProbe(command, args, env) {
|
|
438
417
|
return new Promise((resolve) => {
|
|
439
418
|
let output = "";
|
|
440
419
|
let timedOut = false;
|
|
@@ -451,9 +430,10 @@ function runNativeProbe(command, args) {
|
|
|
451
430
|
};
|
|
452
431
|
let child;
|
|
453
432
|
try {
|
|
454
|
-
|
|
433
|
+
assertNativeExecutableIdentity(command);
|
|
434
|
+
child = spawn(command.executable, [...args], {
|
|
455
435
|
cwd: os.tmpdir(),
|
|
456
|
-
env:
|
|
436
|
+
env: nativeProcessEnvironment(env),
|
|
457
437
|
shell: false,
|
|
458
438
|
stdio: ["ignore", "pipe", "pipe"],
|
|
459
439
|
});
|
|
@@ -488,14 +468,23 @@ function runNativeProbe(command, args) {
|
|
|
488
468
|
* validator used just before a PTY starts.
|
|
489
469
|
*/
|
|
490
470
|
export async function probeNativeSessionProducer(input) {
|
|
491
|
-
const
|
|
492
|
-
if (!
|
|
471
|
+
const producerExecutable = resolveNativeExecutable("producer", input.warbleBin, input.pathValue);
|
|
472
|
+
if (!producerExecutable)
|
|
493
473
|
return nativeProducerResult(false, undefined, "resolve", "result=binary_not_found", "the Warble binary could not be resolved");
|
|
494
|
-
const identity =
|
|
495
|
-
|
|
496
|
-
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;
|
|
497
476
|
const producer = { executable, identity };
|
|
498
|
-
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);
|
|
499
488
|
if (help.spawnFailed)
|
|
500
489
|
return nativeProducerResult(false, identity, "dispatch_help", "result=spawn_failed", "the Warble binary could not be started");
|
|
501
490
|
if (help.timedOut)
|
|
@@ -510,65 +499,97 @@ export async function probeNativeSessionProducer(input) {
|
|
|
510
499
|
// reported the producer incompatible even for a Claude-only install, which is
|
|
511
500
|
// exactly what an installed package produces.
|
|
512
501
|
const vendors = {};
|
|
502
|
+
const purposes = Object.fromEntries(NATIVE_PURPOSES.map((purpose) => [purpose, {}]));
|
|
513
503
|
for (const vendor of NATIVE_PROBE_TARGETS) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const outputRoot = path.join(root, "out");
|
|
527
|
-
mkdirSync(outputRoot, { mode: 0o700 });
|
|
528
|
-
const bootstrapRoot = path.join(root, "bootstrap");
|
|
529
|
-
mkdirSync(bootstrapRoot, { mode: 0o700 });
|
|
530
|
-
const scopeId = `preflight-${purpose}-${vendor}`;
|
|
531
|
-
const scopePath = path.join(root, "scope.json");
|
|
532
|
-
const mcpPath = path.join(root, "mcp.json");
|
|
533
|
-
const wrenRuntime = vendor === "codex" ? resolveNativeWrenRuntime(input.wrenShim) : undefined;
|
|
534
|
-
const binding = NATIVE_DISPATCH_REGISTRY[purpose].scopeKind === "bound_project"
|
|
535
|
-
? { project_identity: "native-preflight-project", generation: "1", revision: "native-preflight-revision" }
|
|
536
|
-
: undefined;
|
|
537
|
-
writeFileSync(scopePath, JSON.stringify({ version: "3", kind: NATIVE_DISPATCH_REGISTRY[purpose].scopeKind, scope_id: scopeId, cwd: outputRoot, entry: nativeEntryFor(purpose, vendor), ...(purpose === "setup" ? { bootstrap_root: bootstrapRoot } : {}), ...(wrenRuntime ? { wren_runtime: wrenRuntime } : {}), ...(binding ? { binding } : {}) }), { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
538
|
-
writeFileSync(mcpPath, JSON.stringify({ version: "1", url: "http://127.0.0.1:0/api/native-sessions/mcp", credential: "native-preflight-nonsecret" }), { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
539
|
-
const dispatched = await runNativeProbe(executable, ["dispatch", irPath, "--target", targetForProvider(vendor), "--purpose", purpose, "--native-scope", scopePath, "--native-mcp", mcpPath, "--out", outputRoot]);
|
|
540
|
-
if (dispatched.spawnFailed) {
|
|
541
|
-
outcome = fail("result=spawn_failed", "the Warble binary could not be started for this vendor");
|
|
542
|
-
break;
|
|
543
|
-
}
|
|
544
|
-
if (dispatched.timedOut) {
|
|
545
|
-
outcome = fail("result=timed_out", "the Warble binary did not respond for this vendor");
|
|
546
|
-
break;
|
|
547
|
-
}
|
|
548
|
-
if (dispatched.exitCode !== 0) {
|
|
549
|
-
outcome = fail(`result=exit_code_${dispatched.exitCode ?? "signal"}`, "Warble could not dispatch this profile for this vendor");
|
|
550
|
-
break;
|
|
551
|
-
}
|
|
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;
|
|
552
516
|
try {
|
|
553
|
-
|
|
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
|
+
}
|
|
554
577
|
}
|
|
555
|
-
catch {
|
|
556
|
-
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");
|
|
557
582
|
break;
|
|
558
583
|
}
|
|
584
|
+
finally {
|
|
585
|
+
if (root)
|
|
586
|
+
rmSync(root, { recursive: true, force: true });
|
|
587
|
+
}
|
|
559
588
|
}
|
|
560
|
-
|
|
561
|
-
outcome = error instanceof NativeWrenRuntimeError
|
|
562
|
-
? fail(`result=wren_runtime_unavailable (${error.message})`, error.message)
|
|
563
|
-
: fail("result=preflight_workspace_unavailable", "the preflight could not create its temporary workspace");
|
|
564
|
-
break;
|
|
565
|
-
}
|
|
566
|
-
finally {
|
|
567
|
-
if (root)
|
|
568
|
-
rmSync(root, { recursive: true, force: true });
|
|
569
|
-
}
|
|
589
|
+
purposes[selectedPurpose][vendor] = outcome;
|
|
570
590
|
}
|
|
571
|
-
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") };
|
|
572
593
|
}
|
|
573
594
|
const usable = NATIVE_PROBE_TARGETS.filter((vendor) => vendors[vendor].available);
|
|
574
595
|
const available = usable.length > 0;
|
|
@@ -584,6 +605,7 @@ export async function probeNativeSessionProducer(input) {
|
|
|
584
605
|
producer,
|
|
585
606
|
diagnostic: producerDiagnostic(identity, "complete", available ? `result=compatible_${usable.join("+")}` : `result=no_compatible_vendor ${vendorResults}`),
|
|
586
607
|
vendors,
|
|
608
|
+
purposes,
|
|
587
609
|
};
|
|
588
610
|
}
|
|
589
611
|
function parseNativeSetupRecoveryReport(value) {
|
|
@@ -613,7 +635,7 @@ function parseNativeSetupRecoveryReport(value) {
|
|
|
613
635
|
* omits the host-issued scope; Setup exposes only its separately authorized
|
|
614
636
|
* bootstrap root, which is revalidated against the durable host configuration.
|
|
615
637
|
*/
|
|
616
|
-
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) {
|
|
617
639
|
const ownershipRoot = binding?.path ?? authorizedWorkspace;
|
|
618
640
|
const root = materializationState && ownershipRoot
|
|
619
641
|
? validateNativeSessionWorkspace(materializationState, ownershipRoot, cwd)
|
|
@@ -629,19 +651,28 @@ export function readNativeLaunchSpec(cwd, purpose, vendor, scopeId, binding, mcp
|
|
|
629
651
|
throw new InteractiveLaunchError("native session launch specification is invalid");
|
|
630
652
|
}
|
|
631
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");
|
|
632
657
|
const isV4 = mcp !== undefined;
|
|
633
658
|
const isSetupV4 = isV4 && purpose === "setup";
|
|
634
|
-
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"];
|
|
635
660
|
const target = targetForProvider(vendor);
|
|
636
661
|
const executable = vendor === "claude" ? "claude" : "codex";
|
|
637
|
-
const expectedAgent =
|
|
638
|
-
const scopeEntry =
|
|
662
|
+
const expectedAgent = expectedAgentFor(purpose, vendor, selection);
|
|
663
|
+
const scopeEntry = scopeEntryFor(purpose, vendor, selection);
|
|
639
664
|
if (!value || !hasExactlyKeys(value, keys) ||
|
|
640
|
-
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 ||
|
|
641
666
|
!Array.isArray(value.argv) || !value.argv.every((arg) => typeof arg === "string") || !record(value.agent) ||
|
|
642
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") {
|
|
643
668
|
throw new InteractiveLaunchError("native session launch specification is incompatible");
|
|
644
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
|
+
}
|
|
645
676
|
const welcome = welcomePromptFor(purpose);
|
|
646
677
|
const baseArgv = scopeEntry
|
|
647
678
|
? isV4 ? [welcome] : []
|
|
@@ -661,7 +692,7 @@ export function readNativeLaunchSpec(cwd, purpose, vendor, scopeId, binding, mcp
|
|
|
661
692
|
// "agent" descriptor is a distinct kind (`claude_scope`) carrying the profile id rather than
|
|
662
693
|
// `claude_agent`/`expectedAgent`. See `claudeScopeEntry` above.
|
|
663
694
|
const expectedAgentKind = scopeEntry ? "claude_scope" : vendor === "claude" ? "claude_agent" : "codex_skill";
|
|
664
|
-
const expectedAgentName = scopeEntry ? NATIVE_DISPATCH_REGISTRY[purpose].profile : expectedAgent;
|
|
695
|
+
const expectedAgentName = scopeEntry ? (customSelection(purpose, selection)?.id ?? NATIVE_DISPATCH_REGISTRY[purpose].profile) : expectedAgent;
|
|
665
696
|
if (!hasExactlyKeys(agent, ["kind", "name"]) || (!isV4 && !validV2Scope) ||
|
|
666
697
|
JSON.stringify(value.argv) !== JSON.stringify(baseArgv) || agent.kind !== expectedAgentKind || agent.name !== expectedAgentName ||
|
|
667
698
|
(isV4 && (!launchMcp || !hasExactlyKeys(launchMcp, ["server_name", "credential_env_var"]) || launchMcp.server_name !== "genbi_session" || launchMcp.credential_env_var !== NATIVE_MCP_CREDENTIAL_ENV_VAR)))
|
|
@@ -716,7 +747,7 @@ export function readNativeLaunchSpec(cwd, purpose, vendor, scopeId, binding, mcp
|
|
|
716
747
|
? [...(scopeEntry ? [] : ["--agent", expectedAgent]), "--resume", providerLaunch.handle, ...(isV4 ? [welcome] : [])]
|
|
717
748
|
: ["--dangerously-bypass-hook-trust", "-c", 'tui.resume_cwd="current"', "resume", providerLaunch.handle, ...(isV4 ? [welcome] : [])]
|
|
718
749
|
: spawnBaseArgv;
|
|
719
|
-
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 } : {}) };
|
|
720
751
|
}
|
|
721
752
|
/**
|
|
722
753
|
* Materializes only host-owned, short-lived producer inputs. The scope and
|
|
@@ -735,6 +766,7 @@ export async function dispatchNativeArtifacts(input) {
|
|
|
735
766
|
const dir = mkdtempSync(path.join(tempRoot, "genbi-native-dispatch-"));
|
|
736
767
|
const scopePath = path.join(dir, "scope.json");
|
|
737
768
|
const mcpPath = path.join(dir, "mcp.json");
|
|
769
|
+
const hostPath = path.join(dir, "host.json");
|
|
738
770
|
try {
|
|
739
771
|
const directory = lstatSync(dir);
|
|
740
772
|
if (!directory.isDirectory() || directory.isSymbolicLink() || !contained(tempRoot, dir))
|
|
@@ -750,18 +782,20 @@ export async function dispatchNativeArtifacts(input) {
|
|
|
750
782
|
if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o777) !== 0o600)
|
|
751
783
|
throw new InteractiveLaunchError("native session materialization failed");
|
|
752
784
|
};
|
|
785
|
+
if (input.componentHost) {
|
|
786
|
+
writeFileSync(hostPath, JSON.stringify(input.componentHost), { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
787
|
+
validateDescriptorFile(hostPath);
|
|
788
|
+
}
|
|
753
789
|
validateDescriptorFile(scopePath);
|
|
754
790
|
if (input.mcp)
|
|
755
791
|
validateDescriptorFile(mcpPath);
|
|
756
|
-
|
|
757
|
-
if (input.producer)
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
child.once("exit", (code) => code === 0 ? resolve() : reject(new InteractiveLaunchError("native session materialization failed")));
|
|
764
|
-
});
|
|
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"); });
|
|
765
799
|
}
|
|
766
800
|
finally {
|
|
767
801
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -769,7 +803,28 @@ export async function dispatchNativeArtifacts(input) {
|
|
|
769
803
|
}
|
|
770
804
|
export class NativeSessionService {
|
|
771
805
|
options;
|
|
806
|
+
runtimeHost;
|
|
807
|
+
nativeHome;
|
|
808
|
+
wrenShim;
|
|
809
|
+
sourceWrenHome;
|
|
810
|
+
nodeExecutable;
|
|
811
|
+
producerExecutable;
|
|
812
|
+
vendorExecutables;
|
|
813
|
+
childToolDirectories;
|
|
814
|
+
conversations = new Map();
|
|
772
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();
|
|
773
828
|
leaseTimers = new Map();
|
|
774
829
|
/** Open/reconnect coalescing only lasts while a matching launch is in flight. */
|
|
775
830
|
launches = new Map();
|
|
@@ -786,6 +841,41 @@ export class NativeSessionService {
|
|
|
786
841
|
fixtureMaterializationState;
|
|
787
842
|
constructor(options) {
|
|
788
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
|
+
])]);
|
|
789
879
|
this.idleTtlMs = options.idleTtlMs ?? options.postClaimDetachGraceMs ?? NATIVE_SESSION_IDLE_TTL_MS;
|
|
790
880
|
if (!Number.isSafeInteger(this.idleTtlMs) || this.idleTtlMs <= 0)
|
|
791
881
|
throw new RangeError("native session idle TTL must be a positive integer");
|
|
@@ -823,11 +913,27 @@ export class NativeSessionService {
|
|
|
823
913
|
async productionProducerPreflight() {
|
|
824
914
|
if (this.legacyFixtureMode())
|
|
825
915
|
return undefined;
|
|
826
|
-
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
|
+
});
|
|
827
927
|
const log = result.available ? console.info : console.warn;
|
|
828
928
|
log(`[native-sessions] producer preflight ${result.diagnostic}`);
|
|
829
929
|
return result;
|
|
830
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
|
+
}
|
|
831
937
|
/**
|
|
832
938
|
* SQLite is the durable source of truth, but a native PTY and its attach
|
|
833
939
|
* capability are deliberately process-local. A BFF restart (or a lease
|
|
@@ -850,7 +956,7 @@ export class NativeSessionService {
|
|
|
850
956
|
// yield while the durable row is still being initialized. It has never
|
|
851
957
|
// owned an attachable capability, so treating it as a missing terminal
|
|
852
958
|
// would race creation and could terminalize a PTY that is about to start.
|
|
853
|
-
if ((row.status !== "running" && row.status !== "detached") || this.
|
|
959
|
+
if ((row.status !== "running" && row.status !== "detached") || this.liveCapability(row.id))
|
|
854
960
|
return row;
|
|
855
961
|
// Commit the durable fence first. The cleanup below is best effort and
|
|
856
962
|
// cannot leave a browser-facing row claiming that it can reattach.
|
|
@@ -863,10 +969,13 @@ export class NativeSessionService {
|
|
|
863
969
|
this.recoveryCapabilities.delete(row.id);
|
|
864
970
|
this.options.artifactService?.revoke(this.artifactCredentials.get(row.id));
|
|
865
971
|
this.artifactCredentials.delete(row.id);
|
|
972
|
+
this.cleanupRuntime(row.id);
|
|
866
973
|
return stopped;
|
|
867
974
|
}
|
|
868
975
|
/** A legacy persisted Runtime must be repaired before it can authorize a PTY. */
|
|
869
976
|
assertRuntimeDispatchable() {
|
|
977
|
+
if (this.shuttingDown)
|
|
978
|
+
throw new InteractiveLaunchError("native session host is shutting down");
|
|
870
979
|
if (!this.options.store.hasExplicitRuntimeSettings())
|
|
871
980
|
return;
|
|
872
981
|
const correction = runtimeSettingsCorrection(this.options.store.getRuntimeSettings());
|
|
@@ -890,14 +999,17 @@ export class NativeSessionService {
|
|
|
890
999
|
if (!dispatch)
|
|
891
1000
|
throw new InteractiveLaunchError("native sessions require a saved Runtime & authentication binding");
|
|
892
1001
|
const binding = input.purpose === "setup" ? undefined : this.options.getBinding();
|
|
893
|
-
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);
|
|
894
1005
|
if (reusable) {
|
|
895
1006
|
const recoveryCapability = this.recoveryCapabilities.get(reusable.id);
|
|
896
|
-
return { row: reusable, capability: this.
|
|
1007
|
+
return { row: reusable, capability: this.liveCapability(reusable.id), ...(recoveryCapability ? { recoveryCapability } : {}) };
|
|
897
1008
|
}
|
|
1009
|
+
const selectedEntry = selectedNativeEntry(input.purpose, dispatch.provider, input.entryVerb);
|
|
898
1010
|
const scopeKey = input.purpose === "setup"
|
|
899
1011
|
? JSON.stringify([input.purpose, dispatch.target, runtime.generation, "bootstrap"])
|
|
900
|
-
: 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]);
|
|
901
1013
|
return this.singleFlight(`open:${scopeKey}`, scopeKey, input, binding, runtime);
|
|
902
1014
|
}
|
|
903
1015
|
/** Opens the exact row selected by the browser after revalidating its scope. */
|
|
@@ -915,7 +1027,7 @@ export class NativeSessionService {
|
|
|
915
1027
|
if (!row || !this.matchesOpenScope(row, input.purpose, dispatch.target, runtime.generation, binding))
|
|
916
1028
|
throw new InteractiveLaunchError("native session is unavailable");
|
|
917
1029
|
const recoveryCapability = this.recoveryCapabilities.get(row.id);
|
|
918
|
-
return { row, capability: this.
|
|
1030
|
+
return { row, capability: this.liveCapability(row.id), ...(recoveryCapability ? { recoveryCapability } : {}) };
|
|
919
1031
|
}
|
|
920
1032
|
/**
|
|
921
1033
|
* Starts a separate TUI even when a matching session is live. The caller's
|
|
@@ -933,9 +1045,11 @@ export class NativeSessionService {
|
|
|
933
1045
|
if (!dispatch)
|
|
934
1046
|
throw new InteractiveLaunchError("native sessions require a saved Runtime & authentication binding");
|
|
935
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);
|
|
936
1050
|
const scopeKey = input.purpose === "setup"
|
|
937
1051
|
? JSON.stringify([input.purpose, dispatch.target, runtime.generation, "bootstrap"])
|
|
938
|
-
: 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]);
|
|
939
1053
|
return this.replayStartSeparate(`start:${input.idempotencyKey}`, scopeKey, input, binding, runtime);
|
|
940
1054
|
}
|
|
941
1055
|
/**
|
|
@@ -974,6 +1088,8 @@ export class NativeSessionService {
|
|
|
974
1088
|
}
|
|
975
1089
|
/** Browser-visible resume state uses the same current-scope fence as launch, plus *why* when it fails. */
|
|
976
1090
|
resumeAvailability(row) {
|
|
1091
|
+
if (row.transport === "conversation" || (row.vendor === "codex" && this.options.directCodex?.terminal))
|
|
1092
|
+
return { available: false, cause: "no_resume_handle" };
|
|
977
1093
|
if (row.status !== "exited" && row.status !== "stopped" && row.status !== "interrupted" && row.status !== "failed" && row.status !== "stale") {
|
|
978
1094
|
return { available: false, cause: "not_terminal" };
|
|
979
1095
|
}
|
|
@@ -1000,7 +1116,7 @@ export class NativeSessionService {
|
|
|
1000
1116
|
* no global Codex state and persists only a provider-bound encrypted envelope.
|
|
1001
1117
|
*/
|
|
1002
1118
|
captureCodexResumeHandle(row) {
|
|
1003
|
-
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"))
|
|
1004
1120
|
return;
|
|
1005
1121
|
const state = this.materializationState();
|
|
1006
1122
|
if (!state)
|
|
@@ -1029,7 +1145,12 @@ export class NativeSessionService {
|
|
|
1029
1145
|
const runtime = !storedRuntime.configured && this.legacyFixtureMode()
|
|
1030
1146
|
? { configured: true, generation: 0, provider: row.vendor, target: targetForProvider(row.vendor), targetLabel: nativeTargetLabel(row.vendor) }
|
|
1031
1147
|
: storedRuntime;
|
|
1032
|
-
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;
|
|
1033
1154
|
const binding = row.purpose === "setup" ? undefined : this.options.getBinding();
|
|
1034
1155
|
const matches = dispatch?.provider === row.vendor && row.dispatchProfile === dispatch.profile && row.dispatchTarget === dispatch.target && row.runtimeGeneration === runtime.generation
|
|
1035
1156
|
&& (row.purpose === "setup"
|
|
@@ -1072,7 +1193,7 @@ export class NativeSessionService {
|
|
|
1072
1193
|
const reserved = this.options.store.reserveNativeSessionResume({
|
|
1073
1194
|
sourceSessionId: source.id, idempotencyKey, scopeFingerprint: fingerprint, sealedHandle: sealedChild,
|
|
1074
1195
|
child: {
|
|
1075
|
-
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,
|
|
1076
1197
|
dispatchProfile: scope.dispatch.profile, dispatchTarget: scope.dispatch.target, runtimeGeneration: scope.runtime.generation,
|
|
1077
1198
|
...(scope.binding ? { projectIdentity: scope.binding.identity, bindingGeneration: scope.binding.generation, projectRevision: scope.binding.revision } : {}),
|
|
1078
1199
|
},
|
|
@@ -1081,7 +1202,7 @@ export class NativeSessionService {
|
|
|
1081
1202
|
throw new InteractiveLaunchError("native session resume is unavailable");
|
|
1082
1203
|
if (!reserved.created)
|
|
1083
1204
|
return this.recoverReservedResume(reserved.row.id, scope);
|
|
1084
|
-
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 });
|
|
1085
1206
|
}
|
|
1086
1207
|
async recoverReservedResume(id, scope) {
|
|
1087
1208
|
const row = this.get(id);
|
|
@@ -1109,7 +1230,7 @@ export class NativeSessionService {
|
|
|
1109
1230
|
this.failReservedResume(row.id, "native session resume reservation cannot be recovered");
|
|
1110
1231
|
throw error;
|
|
1111
1232
|
}
|
|
1112
|
-
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 });
|
|
1113
1234
|
}
|
|
1114
1235
|
failReservedResume(id, failure) {
|
|
1115
1236
|
const row = this.options.store.getNativeSession(id);
|
|
@@ -1117,8 +1238,11 @@ export class NativeSessionService {
|
|
|
1117
1238
|
this.options.store.transitionNativeSession(id, "failed", { failure, ended: true });
|
|
1118
1239
|
this.options.store.invalidateNativeSessionResume(id);
|
|
1119
1240
|
}
|
|
1120
|
-
matchesOpenScope(row, purpose, target, generation, binding) {
|
|
1121
|
-
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)
|
|
1122
1246
|
return false;
|
|
1123
1247
|
if (row.status !== "creating" && row.status !== "running" && row.status !== "detached")
|
|
1124
1248
|
return false;
|
|
@@ -1154,7 +1278,7 @@ export class NativeSessionService {
|
|
|
1154
1278
|
// A successful response may contain an attach capability. Never replay it
|
|
1155
1279
|
// after the owning PTY/capability has gone away; failures carry no terminal
|
|
1156
1280
|
// capability and remain safely replayable within the same fenced scope.
|
|
1157
|
-
return !replay.result?.capability || this.
|
|
1281
|
+
return !replay.result?.capability || this.liveCapability(replay.result.row.id) === replay.result.capability;
|
|
1158
1282
|
}
|
|
1159
1283
|
trimStartSeparateReplays() {
|
|
1160
1284
|
for (const [key, replay] of this.startSeparateReplays) {
|
|
@@ -1190,12 +1314,110 @@ export class NativeSessionService {
|
|
|
1190
1314
|
this.launches.delete(launchKey);
|
|
1191
1315
|
}
|
|
1192
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
|
+
}
|
|
1193
1384
|
async readiness() {
|
|
1194
1385
|
const terminalHostAvailable = this.options.terminalHostAvailable ?? (async () => true);
|
|
1195
1386
|
const executableAvailable = (vendor) => this.options.executableAvailable
|
|
1196
|
-
? this.options.executableAvailable(vendor)
|
|
1197
|
-
:
|
|
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
|
+
}
|
|
1198
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;
|
|
1199
1421
|
const mcpReadiness = this.options.artifactService?.readiness();
|
|
1200
1422
|
const runtime = this.options.store.getNativeRuntimeBinding();
|
|
1201
1423
|
const runtimeCorrection = this.options.store.hasExplicitRuntimeSettings()
|
|
@@ -1203,7 +1425,7 @@ export class NativeSessionService {
|
|
|
1203
1425
|
: undefined;
|
|
1204
1426
|
const legacyFixture = !runtime.configured && this.legacyFixtureMode();
|
|
1205
1427
|
const producer = this.legacyFixtureMode() ? undefined : await this.productionProducerPreflight();
|
|
1206
|
-
const legacyProducerAvailable = this.options.producerAvailable ?? (() => this.options.dispatch !== undefined ||
|
|
1428
|
+
const legacyProducerAvailable = this.options.producerAvailable ?? (() => this.options.dispatch !== undefined || this.producerExecutable !== undefined);
|
|
1207
1429
|
const result = {};
|
|
1208
1430
|
for (const purpose of NATIVE_PURPOSES) {
|
|
1209
1431
|
const definition = NATIVE_DISPATCH_REGISTRY[purpose];
|
|
@@ -1223,21 +1445,22 @@ export class NativeSessionService {
|
|
|
1223
1445
|
// Only the configured vendor's own probe may block this purpose. Probing
|
|
1224
1446
|
// both and collapsing them into one boolean let an unselected vendor veto
|
|
1225
1447
|
// the selected one.
|
|
1226
|
-
const selectedProducer = producer && dispatch ? producer.vendors[dispatch.provider] : undefined;
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
:
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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;
|
|
1241
1464
|
const executable = dispatch?.provider === "claude" ? "claude" : "codex";
|
|
1242
1465
|
const unavailable = reason ?? (!hostAvailable
|
|
1243
1466
|
? "native terminal host cannot spawn local processes on this machine"
|
|
@@ -1249,12 +1472,14 @@ export class NativeSessionService {
|
|
|
1249
1472
|
// one is unusable and why instead of only that "the producer" is.
|
|
1250
1473
|
if (!legacyFixture && producer)
|
|
1251
1474
|
for (const vendor of NATIVE_VENDORS) {
|
|
1252
|
-
const probe = producer.vendors[vendor];
|
|
1253
|
-
vendorReadiness[vendor] =
|
|
1254
|
-
? { available: false, reason:
|
|
1255
|
-
:
|
|
1256
|
-
? { available: false, reason:
|
|
1257
|
-
:
|
|
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 };
|
|
1258
1483
|
}
|
|
1259
1484
|
if (legacyFixture)
|
|
1260
1485
|
for (const vendor of NATIVE_VENDORS) {
|
|
@@ -1292,9 +1517,12 @@ export class NativeSessionService {
|
|
|
1292
1517
|
}));
|
|
1293
1518
|
return legacy;
|
|
1294
1519
|
}
|
|
1520
|
+
const profiles = await this.conversationProfileReadiness(result.analysis, runtime.configured ? runtime.provider : undefined);
|
|
1295
1521
|
return {
|
|
1296
1522
|
runtime,
|
|
1523
|
+
runtimeHost: runtimeHostReadiness,
|
|
1297
1524
|
purposes: result,
|
|
1525
|
+
...(profiles ? { profiles } : {}),
|
|
1298
1526
|
mcp: this.options.artifactService?.health() ?? {
|
|
1299
1527
|
server: "GenBI MCP",
|
|
1300
1528
|
tool: "save_dashboard",
|
|
@@ -1307,19 +1535,55 @@ export class NativeSessionService {
|
|
|
1307
1535
|
}
|
|
1308
1536
|
async create(input, binding = input.purpose === "setup" ? undefined : this.options.getBinding(), capturedRuntime = this.options.store.getNativeRuntimeBinding(), resumed) {
|
|
1309
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();
|
|
1310
1546
|
const purpose = input.purpose;
|
|
1311
1547
|
const legacyRuntime = !capturedRuntime.configured && input.vendor
|
|
1312
1548
|
? { configured: true, generation: 0, provider: input.vendor, target: targetForProvider(input.vendor), targetLabel: nativeTargetLabel(input.vendor) }
|
|
1313
1549
|
: capturedRuntime;
|
|
1314
|
-
const
|
|
1315
|
-
if (!
|
|
1550
|
+
const registryDispatch = dispatchForPurpose(purpose, legacyRuntime);
|
|
1551
|
+
if (!registryDispatch)
|
|
1316
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
|
+
}
|
|
1317
1581
|
if (purpose !== "setup" && !binding)
|
|
1318
1582
|
throw new InteractiveLaunchError("native session requires a current bound project");
|
|
1319
1583
|
const configuredCwd = binding?.path ?? this.options.workspaceRoot;
|
|
1320
1584
|
const configuredIr = this.options.irPaths[purpose];
|
|
1321
1585
|
if (!configuredCwd || !configuredIr)
|
|
1322
|
-
throw new InteractiveLaunchError(`native ${
|
|
1586
|
+
throw new InteractiveLaunchError(`native ${selection.id} session is not configured`);
|
|
1323
1587
|
// Compile for this binding rather than dispatching the profile's golden.
|
|
1324
1588
|
// Falls back to the configured path only where no resolver is wired (the
|
|
1325
1589
|
// fixture-mode tests), never silently in production — see `bin.ts`.
|
|
@@ -1331,14 +1595,14 @@ export class NativeSessionService {
|
|
|
1331
1595
|
try {
|
|
1332
1596
|
irPath = this.options.resolveDispatchIr === undefined
|
|
1333
1597
|
? configuredIr
|
|
1334
|
-
: await this.options.resolveDispatchIr(purpose, binding);
|
|
1598
|
+
: await this.options.resolveDispatchIr(purpose, binding, selection.sourceDir);
|
|
1335
1599
|
}
|
|
1336
1600
|
catch (error) {
|
|
1337
1601
|
logNativeLaunchFailure(purpose, dispatchDefinition.provider, "dispatch_ir", error);
|
|
1338
1602
|
throw error;
|
|
1339
1603
|
}
|
|
1340
1604
|
const producer = this.legacyFixtureMode() ? undefined : await this.productionProducerPreflight();
|
|
1341
|
-
const launchProducer = producer ? producer.vendors[dispatchDefinition.provider] : undefined;
|
|
1605
|
+
const launchProducer = producer ? (producer.purposes?.[purpose][dispatchDefinition.provider] ?? producer.vendors[dispatchDefinition.provider]) : undefined;
|
|
1342
1606
|
if (producer && (!launchProducer?.available || !producer.producer)) {
|
|
1343
1607
|
throw new InteractiveLaunchError(launchProducer?.reason ?? "native session producer is incompatible");
|
|
1344
1608
|
}
|
|
@@ -1358,7 +1622,8 @@ export class NativeSessionService {
|
|
|
1358
1622
|
initialSealedHandle = sealNativeClaudeResumeHandle(state, id, sessionId);
|
|
1359
1623
|
providerLaunch = { kind: "initial", sessionId };
|
|
1360
1624
|
}
|
|
1361
|
-
|
|
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,
|
|
1362
1627
|
dispatchProfile: dispatchDefinition.profile, dispatchTarget: dispatchDefinition.target, runtimeGeneration: legacyRuntime.generation,
|
|
1363
1628
|
...(binding ? { projectIdentity: binding.identity, bindingGeneration: binding.generation, projectRevision: binding.revision } : {}) });
|
|
1364
1629
|
if (initialSealedHandle)
|
|
@@ -1384,7 +1649,7 @@ export class NativeSessionService {
|
|
|
1384
1649
|
if (!authorizedWorkspace || !materializationState)
|
|
1385
1650
|
throw new InteractiveLaunchError("native session workspace is unavailable");
|
|
1386
1651
|
const cwd = createNativeSessionWorkspace(materializationState, authorizedWorkspace, id);
|
|
1387
|
-
const producerAvailable = this.options.producerAvailable ?? (() => this.options.dispatch !== undefined ||
|
|
1652
|
+
const producerAvailable = this.options.producerAvailable ?? (() => this.options.dispatch !== undefined || this.producerExecutable !== undefined);
|
|
1388
1653
|
if (!(this.options.dispatch !== undefined || configuredIrPath(irPath)) || !producerAvailable())
|
|
1389
1654
|
throw new InteractiveLaunchError("native session materialization prerequisites are unavailable");
|
|
1390
1655
|
const mcpReadiness = artifactService?.readiness();
|
|
@@ -1394,19 +1659,58 @@ export class NativeSessionService {
|
|
|
1394
1659
|
if (mcp)
|
|
1395
1660
|
this.artifactCredentials.set(id, mcp.credential);
|
|
1396
1661
|
let wrenRuntime;
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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");
|
|
1404
1693
|
}
|
|
1405
|
-
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;
|
|
1406
1698
|
const dispatch = this.options.dispatch ?? dispatchNativeArtifacts;
|
|
1407
1699
|
validateNativeSessionWorkspace(materializationState, authorizedWorkspace, cwd);
|
|
1408
1700
|
launchPhase = "dispatch";
|
|
1409
|
-
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
|
+
});
|
|
1410
1714
|
if (!input.vendor && !sameNativeRuntimeBinding(capturedRuntime, this.options.store.getNativeRuntimeBinding())) {
|
|
1411
1715
|
this.options.store.transitionNativeSession(id, "stale", { failure: "native runtime binding changed before launch", ended: true });
|
|
1412
1716
|
throw new InteractiveLaunchError("native session is stale because the runtime binding changed");
|
|
@@ -1416,20 +1720,75 @@ export class NativeSessionService {
|
|
|
1416
1720
|
throw new InteractiveLaunchError("native session is stale because the bound project changed");
|
|
1417
1721
|
}
|
|
1418
1722
|
launchPhase = "launch_spec";
|
|
1419
|
-
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
|
+
}
|
|
1420
1743
|
let codexWrenHome;
|
|
1421
|
-
|
|
1422
|
-
|
|
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) {
|
|
1423
1749
|
launchPhase = "codex_wren_home";
|
|
1424
|
-
codexWrenHome = (this.options.prepareCodexWrenHome ??
|
|
1750
|
+
codexWrenHome = (this.options.prepareCodexWrenHome ?? materializeCodexWrenHome)({
|
|
1425
1751
|
runtime: wrenRuntime,
|
|
1426
1752
|
projectPath: binding.path,
|
|
1427
1753
|
cwd,
|
|
1754
|
+
sourceWrenHome: this.sourceWrenHome(),
|
|
1755
|
+
home: this.nativeHome,
|
|
1756
|
+
toolDirectories,
|
|
1428
1757
|
});
|
|
1429
1758
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
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
|
+
}
|
|
1432
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));
|
|
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
|
+
});
|
|
1433
1792
|
launchPhase = "terminal_manager";
|
|
1434
1793
|
const terminalManager = await this.options.terminalManager();
|
|
1435
1794
|
if (binding && !sameBinding(binding, this.options.getBinding())) {
|
|
@@ -1441,20 +1800,13 @@ export class NativeSessionService {
|
|
|
1441
1800
|
throw new InteractiveLaunchError("native session is stale because the runtime binding changed");
|
|
1442
1801
|
}
|
|
1443
1802
|
validateNativeSessionWorkspace(materializationState, authorizedWorkspace, cwd);
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
...(binding ? { WREN_PROJECT_HOME: binding.path } : {}),
|
|
1448
|
-
...(codexWrenHome ? { WREN_HOME: codexWrenHome.home } : {}),
|
|
1449
|
-
};
|
|
1450
|
-
// Never inherit a caller/operator value: only the launch spec's already
|
|
1451
|
-
// revalidated Setup root may reach the native TUI.
|
|
1452
|
-
delete terminalEnv[NATIVE_SETUP_BOOTSTRAP_ROOT_ENV_VAR];
|
|
1453
|
-
if (spec.bootstrap_root)
|
|
1454
|
-
terminalEnv[NATIVE_SETUP_BOOTSTRAP_ROOT_ENV_VAR] = spec.bootstrap_root;
|
|
1803
|
+
codexWrenHome?.assertActive?.();
|
|
1804
|
+
assertNativeRuntimeSpec(runtimeSpec);
|
|
1805
|
+
this.assertRuntimeDispatchable();
|
|
1455
1806
|
launchPhase = "terminal_start";
|
|
1456
|
-
const terminal = terminalManager.start(
|
|
1807
|
+
const terminal = terminalManager.start({ ...launchSpec, hostExecutable: vendorExecutable.executable }, { id, capability }, runtimeSpec.childEnvironment, NO_INTERACTIVE_TERMINAL_LEASES);
|
|
1457
1808
|
this.sessions.set(id, terminal);
|
|
1809
|
+
this.runtimeSpecs.set(id, runtimeSpec);
|
|
1458
1810
|
// Mark running before registering a replaying exit listener. A PTY can
|
|
1459
1811
|
// exit between start() and this registration; `onExit` immediately
|
|
1460
1812
|
// replays that state and must win over this transition, never be
|
|
@@ -1469,6 +1821,7 @@ export class NativeSessionService {
|
|
|
1469
1821
|
this.recoveryCapabilities.delete(id);
|
|
1470
1822
|
artifactService?.revoke(this.artifactCredentials.get(id));
|
|
1471
1823
|
this.artifactCredentials.delete(id);
|
|
1824
|
+
this.cleanupRuntime(id);
|
|
1472
1825
|
const current = this.options.store.getNativeSession(id);
|
|
1473
1826
|
if (current && (current.status === "running" || current.status === "detached"))
|
|
1474
1827
|
this.options.store.transitionNativeSession(id, "exited", { exitCode, ended: true });
|
|
@@ -1479,6 +1832,7 @@ export class NativeSessionService {
|
|
|
1479
1832
|
}
|
|
1480
1833
|
catch (error) {
|
|
1481
1834
|
logNativeLaunchFailure(purpose, dispatchDefinition.provider, launchPhase, error);
|
|
1835
|
+
this.cleanupRuntime(id);
|
|
1482
1836
|
this.recoveryCapabilities.delete(id);
|
|
1483
1837
|
artifactService?.revoke(this.artifactCredentials.get(id));
|
|
1484
1838
|
this.artifactCredentials.delete(id);
|
|
@@ -1500,6 +1854,153 @@ export class NativeSessionService {
|
|
|
1500
1854
|
throw error;
|
|
1501
1855
|
}
|
|
1502
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
|
+
}
|
|
1503
2004
|
attach(id, capability) {
|
|
1504
2005
|
const row = this.get(id);
|
|
1505
2006
|
const runtime = this.options.store.getNativeRuntimeBinding();
|
|
@@ -1517,6 +2018,7 @@ export class NativeSessionService {
|
|
|
1517
2018
|
return terminal;
|
|
1518
2019
|
}
|
|
1519
2020
|
detach(id) {
|
|
2021
|
+
this.componentAdmissions.get(id)?.cancelActive();
|
|
1520
2022
|
this.sessions.get(id)?.detach();
|
|
1521
2023
|
if (this.sessions.has(id)) {
|
|
1522
2024
|
this.options.store.transitionNativeSession(id, "detached");
|
|
@@ -1544,12 +2046,25 @@ export class NativeSessionService {
|
|
|
1544
2046
|
this.options.artifactService?.revoke(this.artifactCredentials.get(id));
|
|
1545
2047
|
this.artifactCredentials.delete(id);
|
|
1546
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);
|
|
1547
2057
|
try {
|
|
1548
2058
|
terminal?.close();
|
|
2059
|
+
if (terminal)
|
|
2060
|
+
this.pendingTerminalCleanup.delete(terminal);
|
|
1549
2061
|
}
|
|
1550
2062
|
catch {
|
|
1551
2063
|
// A process kill may fail after the durable session fence committed.
|
|
1552
2064
|
// Capabilities are already unreachable; continue revoking every id.
|
|
2065
|
+
this.runtimeCleanupFailed = true;
|
|
2066
|
+
if (terminal)
|
|
2067
|
+
this.pendingTerminalCleanup.add(terminal);
|
|
1553
2068
|
console.warn("[native-sessions] terminal close failed during capability revocation");
|
|
1554
2069
|
}
|
|
1555
2070
|
}
|
|
@@ -1558,6 +2073,81 @@ export class NativeSessionService {
|
|
|
1558
2073
|
revokeRuntimeCapabilities(ids) { this.revokeCapabilities(ids); }
|
|
1559
2074
|
/** Called after the store advances the bound-project generation. */
|
|
1560
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
|
+
}
|
|
1561
2151
|
/**
|
|
1562
2152
|
* Records only the producer's closed redacted report. Completion remains a
|
|
1563
2153
|
* host decision supplied by the canonical-artifact validator at the route.
|
|
@@ -1622,14 +2212,19 @@ export class NativeSessionService {
|
|
|
1622
2212
|
}
|
|
1623
2213
|
armLease(id, reason, timeoutMs) {
|
|
1624
2214
|
this.clearLease(id);
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
2215
|
+
const timer = setTimeout(() => {
|
|
2216
|
+
if (this.options.store.isClosed()) {
|
|
2217
|
+
this.leaseTimers.delete(id);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (!this.liveCapability(id)) {
|
|
1628
2221
|
this.leaseTimers.delete(id);
|
|
1629
2222
|
return;
|
|
1630
2223
|
}
|
|
1631
2224
|
this.options.store.transitionNativeSession(id, "stopped", { failure: reason, ended: true });
|
|
1632
2225
|
this.revokeCapabilities([id]);
|
|
1633
|
-
}, timeoutMs)
|
|
2226
|
+
}, timeoutMs);
|
|
2227
|
+
timer.unref();
|
|
2228
|
+
this.leaseTimers.set(id, timer);
|
|
1634
2229
|
}
|
|
1635
2230
|
}
|