@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.1ff37038366e
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 +165 -1
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +9 -1
- package/dist/bridge/bridge.service.d.ts +129 -3
- package/dist/bridge/bridge.service.js +2427 -70
- package/dist/bridge/bridge.types.d.ts +218 -2
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +14 -2
- package/dist/cleanup/cleanup.service.js +496 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +55 -7
- package/dist/cli/cli.service.js +429 -219
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
- package/dist/runtime/attempt-process-supervisor.js +397 -0
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +20 -0
- package/dist/runtime/generated-output-reset.js +157 -0
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +191 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +51 -0
- package/dist/runtime/host-listener-inspector.js +385 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +58 -0
- package/dist/runtime/host-runtime-contract.js +813 -0
- package/dist/runtime/host-runtime-lease.d.ts +217 -0
- package/dist/runtime/host-runtime-lease.js +2487 -0
- package/dist/runtime/host-runtime-promotion.d.ts +32 -0
- package/dist/runtime/host-runtime-promotion.js +206 -0
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +285 -0
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +161 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +6 -0
- package/dist/sandbox/repository-lock.js +128 -0
- package/dist/sandbox/runtime.d.ts +302 -0
- package/dist/sandbox/runtime.js +2367 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +74 -0
- package/dist/session/managed-attempt-tools.d.ts +28 -0
- package/dist/session/managed-attempt-tools.js +1015 -0
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +27 -0
- package/dist/session/phase-completion.artifacts.js +829 -0
- package/dist/session/phase-completion.d.ts +53 -42
- package/dist/session/phase-completion.helpers.d.ts +117 -0
- package/dist/session/phase-completion.helpers.js +707 -0
- package/dist/session/phase-completion.js +125 -3007
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +233 -0
- package/dist/session/phase-completion.runtime.d.ts +49 -0
- package/dist/session/phase-completion.runtime.js +840 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +859 -0
- package/dist/session/phase-completion.validation.d.ts +38 -0
- package/dist/session/phase-completion.validation.js +918 -0
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
- package/dist/session/provider-cli/provider-cli.process.js +874 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +16 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +81 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
- package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
- package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
- package/dist/session/provider-cli/provider-output-state.js +817 -0
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +448 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +586 -59
- package/dist/session/session.types.d.ts +324 -3
- package/dist/session/session.types.js +939 -10
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +21 -1
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +330 -0
- package/dist/shared/process/process.d.ts +3 -0
- package/dist/shared/process/process.js +13 -7
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +4 -0
- package/dist/shared/provider-capabilities.js +88 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +47 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +82 -10
- package/dist/worktree/dependency-warmer.d.ts +53 -1
- package/dist/worktree/dependency-warmer.js +619 -11
- package/dist/worktree/index.d.ts +2 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +16 -3
- package/dist/worktree/worktree.service.js +334 -54
- package/dist/worktree/worktree.types.d.ts +9 -1
- package/package.json +12 -7
- package/runtime/windows-job-supervisor.ps1 +722 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import readline from "node:readline";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
|
|
9
|
+
import { persistHostProcessSupervisorMetadata, resolveHostProcessSupervisedLaunch, terminateHostProcessTree, } from "../../shared/process/host-process-supervisor.js";
|
|
10
|
+
import { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
|
|
11
|
+
import { assertHostDatabaseCapabilityBoundary } from "../../runtime/host-database-capability-boundary.js";
|
|
12
|
+
import { ForgeFailureError } from "../session.types.js";
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
// Provider processes can emit many gigabytes over a long-running session. The
|
|
15
|
+
// line handlers stream that output to Forge as it arrives; the completed result
|
|
16
|
+
// only needs a bounded diagnostic tail for failure classification.
|
|
17
|
+
export const PROVIDER_OUTPUT_CAPTURE_LIMIT_CHARS = 128 * 1024;
|
|
18
|
+
const PROVIDER_TERMINAL_EXIT_GRACE_MS = 15_000;
|
|
19
|
+
const maximumRecoverableCodexModelsCacheBytes = 16 * 1024 * 1024;
|
|
20
|
+
/** Recovery must wrap the attempt coordinator, never its raw OS runner. Each
|
|
21
|
+
* relaunch needs a new fenced child generation and exclusive launch files. */
|
|
22
|
+
export function recoveringProviderProcessRunner(inner) {
|
|
23
|
+
return new CodexResumeRecoveringProcessRunner(new CodexModelsCacheRecoveringProcessRunner(inner));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Codex Desktop and an independently installed Codex CLI can briefly disagree
|
|
27
|
+
* about the shared models-cache schema during a rolling upgrade. Recover only
|
|
28
|
+
* that exact failure by quarantining the bounded cache file and retrying once;
|
|
29
|
+
* auth, config, sessions, and every other CODEX_HOME entry remain untouched.
|
|
30
|
+
*/
|
|
31
|
+
export class CodexModelsCacheRecoveringProcessRunner {
|
|
32
|
+
inner;
|
|
33
|
+
constructor(inner) {
|
|
34
|
+
this.inner = inner;
|
|
35
|
+
}
|
|
36
|
+
async run(request, handlers, signal) {
|
|
37
|
+
const first = await this.inner.run(request, handlers, signal);
|
|
38
|
+
if (first.exitCode === 0
|
|
39
|
+
|| signal?.aborted
|
|
40
|
+
|| !isCodexCommand(request.command)
|
|
41
|
+
|| !isCodexModelsCacheSchemaFailure(first)) {
|
|
42
|
+
return first;
|
|
43
|
+
}
|
|
44
|
+
const recovered = await quarantineIncompatibleCodexModelsCache(request.env).catch(() => false);
|
|
45
|
+
if (!recovered || signal?.aborted)
|
|
46
|
+
return first;
|
|
47
|
+
return await this.inner.run(request, handlers, signal);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Some Codex CLI builds can start new turns but cannot resume an older thread
|
|
52
|
+
* because the backing app does not implement the paginated thread lookup RPC.
|
|
53
|
+
* Preserve the prepared worktree and prompt, then retry once as a fresh turn.
|
|
54
|
+
*/
|
|
55
|
+
export class CodexResumeRecoveringProcessRunner {
|
|
56
|
+
inner;
|
|
57
|
+
constructor(inner) {
|
|
58
|
+
this.inner = inner;
|
|
59
|
+
}
|
|
60
|
+
async run(request, handlers, signal) {
|
|
61
|
+
const first = await this.inner.run(request, handlers, signal);
|
|
62
|
+
if (first.exitCode === 0
|
|
63
|
+
|| signal?.aborted
|
|
64
|
+
|| !isCodexCommand(request.command)
|
|
65
|
+
|| !isRecoverableCodexResumeFailure(request.command, first)) {
|
|
66
|
+
return first;
|
|
67
|
+
}
|
|
68
|
+
const fallback = codexFreshTurnRequest(request);
|
|
69
|
+
return fallback ? await this.inner.run(fallback, handlers, signal) : first;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function isRecoverableCodexResumeFailure(command, result) {
|
|
73
|
+
const output = `${result.stderr}\n${result.stdout}`.toLowerCase();
|
|
74
|
+
return (output.includes("thread/resume failed")
|
|
75
|
+
&& output.includes("paginated_threads is not supported yet"))
|
|
76
|
+
|| isTransientCodexProviderNotFoundFailure(command, result);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Recognizes the exact nonzero Codex service response at the provider-process
|
|
80
|
+
* boundary. Provider stdout is deliberately excluded because it can contain
|
|
81
|
+
* arbitrary tool/command output and therefore cannot mint retry authority.
|
|
82
|
+
*/
|
|
83
|
+
export function isTransientCodexProviderNotFoundFailure(command, result) {
|
|
84
|
+
if (result.exitCode === 0 || !isCodexCommand(command))
|
|
85
|
+
return false;
|
|
86
|
+
return result.stderr
|
|
87
|
+
.split(/\r?\n/u)
|
|
88
|
+
.map(line => line.trim().toLowerCase())
|
|
89
|
+
.some(line => line === "not found: status code '404'"
|
|
90
|
+
|| line === 'not found: status code "404"');
|
|
91
|
+
}
|
|
92
|
+
function codexFreshTurnRequest(request) {
|
|
93
|
+
const args = [...request.args];
|
|
94
|
+
const resumeIndex = args.indexOf("resume");
|
|
95
|
+
if (resumeIndex < 0 || args.length < 2)
|
|
96
|
+
return null;
|
|
97
|
+
// buildProviderCliCommand always places the thread id immediately before
|
|
98
|
+
// the prompt. Remove it before moving exec-level --cd back out of the
|
|
99
|
+
// resume subcommand shape.
|
|
100
|
+
args.splice(args.length - 2, 1);
|
|
101
|
+
args.splice(resumeIndex, 1);
|
|
102
|
+
const cdIndex = args.indexOf("--cd");
|
|
103
|
+
if (cdIndex >= 0 && cdIndex + 1 < args.length) {
|
|
104
|
+
const cd = args.splice(cdIndex, 2);
|
|
105
|
+
args.splice(args.length - 1, 0, ...cd);
|
|
106
|
+
}
|
|
107
|
+
return { ...request, args };
|
|
108
|
+
}
|
|
109
|
+
function isCodexCommand(command) {
|
|
110
|
+
return path.parse(command).name.toLowerCase() === "codex";
|
|
111
|
+
}
|
|
112
|
+
function isCodexModelsCacheSchemaFailure(result) {
|
|
113
|
+
const output = `${result.stderr}\n${result.stdout}`.toLowerCase();
|
|
114
|
+
return output.includes("base_instructions")
|
|
115
|
+
&& output.includes("missing field")
|
|
116
|
+
&& (output.includes("models_cache") || output.includes("models cache") || output.includes("model cache"));
|
|
117
|
+
}
|
|
118
|
+
async function quarantineIncompatibleCodexModelsCache(requestEnv) {
|
|
119
|
+
const configuredHome = readEnvironmentValue(requestEnv ?? {}, "CODEX_HOME")
|
|
120
|
+
?? readEnvironmentValue(process.env, "CODEX_HOME")
|
|
121
|
+
?? readEnvironmentValue(process.env, "FORGE_CODEX_HOME");
|
|
122
|
+
const codexHome = path.resolve(configuredHome ?? path.join(os.homedir(), ".codex"));
|
|
123
|
+
if (!path.isAbsolute(codexHome) || codexHome === path.parse(codexHome).root)
|
|
124
|
+
return false;
|
|
125
|
+
const cachePath = path.join(codexHome, "models_cache.json");
|
|
126
|
+
const cacheStat = await fs.promises.lstat(cachePath).catch(error => {
|
|
127
|
+
if (error.code === "ENOENT")
|
|
128
|
+
return null;
|
|
129
|
+
throw error;
|
|
130
|
+
});
|
|
131
|
+
if (!cacheStat
|
|
132
|
+
|| !cacheStat.isFile()
|
|
133
|
+
|| cacheStat.isSymbolicLink()
|
|
134
|
+
|| cacheStat.nlink !== 1
|
|
135
|
+
|| cacheStat.size <= 0
|
|
136
|
+
|| cacheStat.size > maximumRecoverableCodexModelsCacheBytes) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const quarantinePath = path.join(codexHome, `models_cache.forge-incompatible-${Date.now()}-${randomUUID()}.json`);
|
|
140
|
+
await fs.promises.rename(cachePath, quarantinePath);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const allowedProviderParentEnvNames = [
|
|
144
|
+
"PATH",
|
|
145
|
+
"HOME",
|
|
146
|
+
"USERPROFILE",
|
|
147
|
+
"TMPDIR",
|
|
148
|
+
"TEMP",
|
|
149
|
+
"TMP",
|
|
150
|
+
"LANG",
|
|
151
|
+
"CLAUDE_CONFIG_DIR"
|
|
152
|
+
];
|
|
153
|
+
const windowsAllowedProviderParentEnvNames = [
|
|
154
|
+
"SystemRoot",
|
|
155
|
+
"SystemDrive",
|
|
156
|
+
"ComSpec",
|
|
157
|
+
"PATHEXT",
|
|
158
|
+
"APPDATA",
|
|
159
|
+
"LOCALAPPDATA",
|
|
160
|
+
"USERNAME",
|
|
161
|
+
"USERDOMAIN",
|
|
162
|
+
"HOMEDRIVE",
|
|
163
|
+
"HOMEPATH",
|
|
164
|
+
"ProgramData",
|
|
165
|
+
"ProgramFiles",
|
|
166
|
+
"ProgramFiles(x86)",
|
|
167
|
+
"ProgramW6432",
|
|
168
|
+
"DOTNET_ROOT",
|
|
169
|
+
"DOTNET_ROOT(x86)",
|
|
170
|
+
"DOTNET_CLI_HOME",
|
|
171
|
+
"NUGET_PACKAGES",
|
|
172
|
+
"NUGET_HTTP_CACHE_PATH",
|
|
173
|
+
"CLAUDE_CODE_GIT_BASH_PATH"
|
|
174
|
+
];
|
|
175
|
+
export function buildProviderChildEnvironment(requestEnv, options = {}) {
|
|
176
|
+
const parentEnv = options.parentEnv ?? process.env;
|
|
177
|
+
const platform = options.platform ?? process.platform;
|
|
178
|
+
const env = {};
|
|
179
|
+
for (const [key, value] of Object.entries(parentEnv)) {
|
|
180
|
+
if (typeof value === "string" && isAllowedProviderParentEnv(key, platform)) {
|
|
181
|
+
env[key] = value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (requestEnv) {
|
|
185
|
+
for (const [key, value] of Object.entries(requestEnv)) {
|
|
186
|
+
const trimmedKey = key.trim();
|
|
187
|
+
if (trimmedKey.length > 0) {
|
|
188
|
+
env[trimmedKey] = value;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (platform === "win32") {
|
|
193
|
+
normalizeWindowsDotNetEnvironment(env, parentEnv, options.homeDir ?? os.homedir());
|
|
194
|
+
}
|
|
195
|
+
normalizeProviderBuildEnvironment(env);
|
|
196
|
+
return env;
|
|
197
|
+
}
|
|
198
|
+
function normalizeProviderBuildEnvironment(env) {
|
|
199
|
+
// Provider sessions are long lived and commonly run many dotnet builds. Persistent
|
|
200
|
+
// MSBuild and Roslyn compiler servers otherwise survive individual commands and can
|
|
201
|
+
// retain more memory than the active Forge lane. Keep those caches process-scoped by
|
|
202
|
+
// default on every runner OS; an explicitly supplied request value still wins.
|
|
203
|
+
setCanonicalProviderBuildEnvironmentValue(env, "DOTNET_CLI_USE_MSBUILD_SERVER", "0");
|
|
204
|
+
setCanonicalProviderBuildEnvironmentValue(env, "MSBUILDDISABLENODEREUSE", "1");
|
|
205
|
+
setCanonicalProviderBuildEnvironmentValue(env, "UseSharedCompilation", "false");
|
|
206
|
+
}
|
|
207
|
+
function setCanonicalProviderBuildEnvironmentValue(env, name, value) {
|
|
208
|
+
// The dotnet CLI uses case-sensitive environment lookup on POSIX, while MSBuild
|
|
209
|
+
// property names are case-insensitive on every OS. Collapse request aliases to
|
|
210
|
+
// one canonical key so the explicit value wins without producing ambiguity.
|
|
211
|
+
const exact = env[name];
|
|
212
|
+
const alias = Object.entries(env).find(([key, candidate]) => key !== name
|
|
213
|
+
&& key.toLowerCase() === name.toLowerCase()
|
|
214
|
+
&& typeof candidate === "string"
|
|
215
|
+
&& candidate.trim().length > 0)?.[1];
|
|
216
|
+
const resolved = typeof exact === "string" && exact.trim().length > 0
|
|
217
|
+
? exact
|
|
218
|
+
: alias ?? value;
|
|
219
|
+
for (const key of Object.keys(env)) {
|
|
220
|
+
if (key !== name && key.toLowerCase() === name.toLowerCase()) {
|
|
221
|
+
delete env[key];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
env[name] = resolved;
|
|
225
|
+
}
|
|
226
|
+
function isAllowedProviderParentEnv(key, platform = process.platform) {
|
|
227
|
+
return isAllowedProviderParentEnvName(key, allowedProviderParentEnvNames)
|
|
228
|
+
|| key.toUpperCase().startsWith("LC_")
|
|
229
|
+
|| (platform === "win32" && isAllowedProviderParentEnvName(key, windowsAllowedProviderParentEnvNames));
|
|
230
|
+
}
|
|
231
|
+
function isAllowedProviderParentEnvName(key, allowedNames) {
|
|
232
|
+
return allowedNames.some((allowed) => allowed.toLowerCase() === key.toLowerCase());
|
|
233
|
+
}
|
|
234
|
+
function normalizeWindowsDotNetEnvironment(env, parentEnv, homeDir) {
|
|
235
|
+
const systemRoot = readEnvironmentValue(env, "SystemRoot") ?? readEnvironmentValue(parentEnv, "SystemRoot") ?? "C:\\Windows";
|
|
236
|
+
const systemDrive = path.win32.parse(systemRoot).root.replace(/\\$/, "") || "C:";
|
|
237
|
+
setEnvironmentValueIfMissing(env, "SystemRoot", systemRoot);
|
|
238
|
+
setEnvironmentValueIfMissing(env, "SystemDrive", readEnvironmentValue(parentEnv, "SystemDrive") ?? systemDrive);
|
|
239
|
+
setEnvironmentValueIfMissing(env, "ComSpec", readEnvironmentValue(parentEnv, "ComSpec") ?? path.win32.join(systemRoot, "System32", "cmd.exe"));
|
|
240
|
+
setEnvironmentValueIfMissing(env, "PATHEXT", readEnvironmentValue(parentEnv, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL");
|
|
241
|
+
const resolvedSystemDrive = readEnvironmentValue(env, "SystemDrive") ?? systemDrive;
|
|
242
|
+
const defaultProgramFiles = path.win32.join(`${resolvedSystemDrive}\\`, "Program Files");
|
|
243
|
+
const defaultProgramFilesX86 = path.win32.join(`${resolvedSystemDrive}\\`, "Program Files (x86)");
|
|
244
|
+
const defaultProgramData = path.win32.join(`${resolvedSystemDrive}\\`, "ProgramData");
|
|
245
|
+
const programFiles = readEnvironmentValue(env, "ProgramFiles")
|
|
246
|
+
?? readEnvironmentValue(parentEnv, "ProgramFiles")
|
|
247
|
+
?? defaultProgramFiles;
|
|
248
|
+
setEnvironmentValueIfMissing(env, "ProgramFiles", programFiles);
|
|
249
|
+
setEnvironmentValueIfMissing(env, "ProgramW6432", readEnvironmentValue(parentEnv, "ProgramW6432") ?? defaultProgramFiles);
|
|
250
|
+
setEnvironmentValueIfMissing(env, "ProgramFiles(x86)", readEnvironmentValue(parentEnv, "ProgramFiles(x86)") ?? defaultProgramFilesX86);
|
|
251
|
+
setEnvironmentValueIfMissing(env, "ProgramData", readEnvironmentValue(parentEnv, "ProgramData") ?? defaultProgramData);
|
|
252
|
+
const userProfile = resolveWindowsUserProfile(env, parentEnv, homeDir);
|
|
253
|
+
if (userProfile) {
|
|
254
|
+
setEnvironmentValueIfMissing(env, "USERPROFILE", userProfile);
|
|
255
|
+
setEnvironmentValueIfMissing(env, "HOME", userProfile);
|
|
256
|
+
setEnvironmentValueIfMissing(env, "DOTNET_CLI_HOME", userProfile);
|
|
257
|
+
setEnvironmentValueIfMissing(env, "NUGET_PACKAGES", path.win32.join(userProfile, ".nuget", "packages"));
|
|
258
|
+
const parsed = path.win32.parse(userProfile);
|
|
259
|
+
if (parsed.root) {
|
|
260
|
+
setEnvironmentValueIfMissing(env, "HOMEDRIVE", parsed.root.replace(/\\$/, ""));
|
|
261
|
+
setEnvironmentValueIfMissing(env, "HOMEPATH", userProfile.slice(parsed.root.length - (parsed.root.endsWith("\\") ? 1 : 0)) || "\\");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const appData = readEnvironmentValue(env, "APPDATA")
|
|
265
|
+
?? readEnvironmentValue(parentEnv, "APPDATA")
|
|
266
|
+
?? (userProfile ? path.win32.join(userProfile, "AppData", "Roaming") : undefined);
|
|
267
|
+
const localAppData = readEnvironmentValue(env, "LOCALAPPDATA")
|
|
268
|
+
?? readEnvironmentValue(parentEnv, "LOCALAPPDATA")
|
|
269
|
+
?? (userProfile ? path.win32.join(userProfile, "AppData", "Local") : undefined);
|
|
270
|
+
if (appData) {
|
|
271
|
+
setEnvironmentValueIfMissing(env, "APPDATA", appData);
|
|
272
|
+
}
|
|
273
|
+
if (localAppData) {
|
|
274
|
+
setEnvironmentValueIfMissing(env, "LOCALAPPDATA", localAppData);
|
|
275
|
+
setEnvironmentValueIfMissing(env, "NUGET_HTTP_CACHE_PATH", path.win32.join(localAppData, "NuGet", "v3-cache"));
|
|
276
|
+
}
|
|
277
|
+
setEnvironmentValueIfMissing(env, "DOTNET_NOLOGO", "1");
|
|
278
|
+
setEnvironmentValueIfMissing(env, "DOTNET_CLI_TELEMETRY_OPTOUT", "1");
|
|
279
|
+
setEnvironmentValueIfMissing(env, "DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "1");
|
|
280
|
+
setEnvironmentValueIfMissing(env, "NUGET_XMLDOC_MODE", "skip");
|
|
281
|
+
}
|
|
282
|
+
function resolveWindowsUserProfile(env, parentEnv, homeDir) {
|
|
283
|
+
const existing = readEnvironmentValue(env, "USERPROFILE") ?? readEnvironmentValue(parentEnv, "USERPROFILE");
|
|
284
|
+
if (existing) {
|
|
285
|
+
return existing;
|
|
286
|
+
}
|
|
287
|
+
const home = readEnvironmentValue(env, "HOME") ?? readEnvironmentValue(parentEnv, "HOME");
|
|
288
|
+
if (home && path.win32.isAbsolute(home)) {
|
|
289
|
+
return home;
|
|
290
|
+
}
|
|
291
|
+
const homeDrive = readEnvironmentValue(env, "HOMEDRIVE") ?? readEnvironmentValue(parentEnv, "HOMEDRIVE");
|
|
292
|
+
const homePath = readEnvironmentValue(env, "HOMEPATH") ?? readEnvironmentValue(parentEnv, "HOMEPATH");
|
|
293
|
+
if (homeDrive && homePath) {
|
|
294
|
+
return `${homeDrive}${homePath}`;
|
|
295
|
+
}
|
|
296
|
+
if (homeDir && path.win32.isAbsolute(homeDir)) {
|
|
297
|
+
return homeDir;
|
|
298
|
+
}
|
|
299
|
+
const systemDrive = readEnvironmentValue(env, "SystemDrive") ?? readEnvironmentValue(parentEnv, "SystemDrive") ?? "C:";
|
|
300
|
+
const username = readEnvironmentValue(env, "USERNAME") ?? readEnvironmentValue(parentEnv, "USERNAME");
|
|
301
|
+
return username ? path.win32.join(systemDrive, "Users", username) : undefined;
|
|
302
|
+
}
|
|
303
|
+
function readEnvironmentValue(env, name) {
|
|
304
|
+
const entry = Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase());
|
|
305
|
+
const value = entry?.[1];
|
|
306
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
307
|
+
}
|
|
308
|
+
function setEnvironmentValueIfMissing(env, name, value) {
|
|
309
|
+
if (readEnvironmentValue(env, name)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
env[name] = value;
|
|
313
|
+
}
|
|
314
|
+
export class StreamingProviderProcessRunner {
|
|
315
|
+
terminalExitGraceMs;
|
|
316
|
+
constructor(terminalExitGraceMs = PROVIDER_TERMINAL_EXIT_GRACE_MS) {
|
|
317
|
+
this.terminalExitGraceMs = terminalExitGraceMs;
|
|
318
|
+
}
|
|
319
|
+
async run(request, handlers = {}, signal) {
|
|
320
|
+
assertHostDatabaseCapabilityBoundary();
|
|
321
|
+
const providerLaunch = await resolveProviderProcessLaunch(request.command, request.args);
|
|
322
|
+
const supervisedLaunch = resolveHostProcessSupervisedLaunch(request, providerLaunch.command, providerLaunch.args);
|
|
323
|
+
// An unsupervised Unix provider still gets a dedicated process group so a
|
|
324
|
+
// cancellation can stop descendants instead of only the CLI root.
|
|
325
|
+
const launch = supervisedLaunch ?? { ...providerLaunch, detached: process.platform !== "win32" };
|
|
326
|
+
return await new Promise((resolve, reject) => {
|
|
327
|
+
const rootCreatedAt = new Date().toISOString();
|
|
328
|
+
const child = spawn(launch.command, launch.args, {
|
|
329
|
+
cwd: request.cwd,
|
|
330
|
+
env: buildProviderChildEnvironment(request.env),
|
|
331
|
+
stdio: [request.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
332
|
+
windowsHide: true,
|
|
333
|
+
detached: launch.detached,
|
|
334
|
+
});
|
|
335
|
+
const memoryObserver = supervisedLaunch
|
|
336
|
+
? new HostProcessTreeMemoryObserver(supervisedLaunch, child.pid ?? 0)
|
|
337
|
+
: null;
|
|
338
|
+
let termination = null;
|
|
339
|
+
const terminate = () => {
|
|
340
|
+
termination ??= terminateHostProcessTree(child, supervisedLaunch);
|
|
341
|
+
return termination;
|
|
342
|
+
};
|
|
343
|
+
const childStdout = child.stdout;
|
|
344
|
+
const childStderr = child.stderr;
|
|
345
|
+
if (!childStdout || !childStderr) {
|
|
346
|
+
const pipeError = new Error("Provider process stdout/stderr streams were not piped.");
|
|
347
|
+
void terminate().then(() => reject(pipeError), terminationError => reject(new AggregateError([pipeError, terminationError], "Provider launch failed and its process tree could not be proven stopped.")));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const stdout = new BoundedOutputTail(PROVIDER_OUTPUT_CAPTURE_LIMIT_CHARS);
|
|
351
|
+
const stderr = new BoundedOutputTail(PROVIDER_OUTPUT_CAPTURE_LIMIT_CHARS);
|
|
352
|
+
let settled = false;
|
|
353
|
+
let closed = false;
|
|
354
|
+
let cancellationRequested = false;
|
|
355
|
+
let terminalTerminationStarted = false;
|
|
356
|
+
let terminalExitTimer = null;
|
|
357
|
+
let readiness = null;
|
|
358
|
+
const cleanup = () => {
|
|
359
|
+
if (terminalExitTimer) {
|
|
360
|
+
clearTimeout(terminalExitTimer);
|
|
361
|
+
terminalExitTimer = null;
|
|
362
|
+
}
|
|
363
|
+
signal?.removeEventListener("abort", abort);
|
|
364
|
+
};
|
|
365
|
+
const fail = (error) => {
|
|
366
|
+
if (settled) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
settled = true;
|
|
370
|
+
cleanup();
|
|
371
|
+
// Do not release the caller's reservation/worktree/port ownership until
|
|
372
|
+
// descendant termination has completed (or failed with explicit proof).
|
|
373
|
+
void terminate().then(() => reject(error), terminationError => reject(new AggregateError([error, terminationError], "Provider process failed and its process tree could not be proven stopped.")));
|
|
374
|
+
};
|
|
375
|
+
const abort = () => {
|
|
376
|
+
cancellationRequested = true;
|
|
377
|
+
// Cancellation is not a launch failure when the supervised tree can be
|
|
378
|
+
// proven stopped. Let the close handler settle the command after exact
|
|
379
|
+
// termination; only a failed termination proof rejects the run.
|
|
380
|
+
void terminate().catch(fail);
|
|
381
|
+
};
|
|
382
|
+
if (signal?.aborted) {
|
|
383
|
+
abort();
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
387
|
+
}
|
|
388
|
+
child.on("error", fail);
|
|
389
|
+
child.once("spawn", () => {
|
|
390
|
+
readiness = (async () => {
|
|
391
|
+
const providerReady = supervisedLaunch?.strategy === "windows_job_object"
|
|
392
|
+
? await waitForWindowsProviderReady(supervisedLaunch, signal, () => closed)
|
|
393
|
+
: undefined;
|
|
394
|
+
if (supervisedLaunch) {
|
|
395
|
+
persistHostProcessSupervisorMetadata(supervisedLaunch, request, child.pid ?? 0, rootCreatedAt, providerReady);
|
|
396
|
+
}
|
|
397
|
+
await handlers.onStarted?.();
|
|
398
|
+
if (supervisedLaunch?.strategy === "windows_job_object") {
|
|
399
|
+
grantWindowsProviderStart(supervisedLaunch);
|
|
400
|
+
}
|
|
401
|
+
})();
|
|
402
|
+
void readiness.catch(error => {
|
|
403
|
+
if (!closed && !signal?.aborted)
|
|
404
|
+
fail(error);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
const writeProviderInput = () => {
|
|
408
|
+
if (request.stdin === undefined)
|
|
409
|
+
return;
|
|
410
|
+
if (!child.stdin) {
|
|
411
|
+
void terminate();
|
|
412
|
+
fail(new Error("Provider process stdin stream was not piped."));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
child.stdin.on("error", (error) => {
|
|
416
|
+
void terminate();
|
|
417
|
+
fail(error);
|
|
418
|
+
});
|
|
419
|
+
child.stdin.end(request.stdin, "utf8");
|
|
420
|
+
};
|
|
421
|
+
if (request.stdin !== undefined) {
|
|
422
|
+
if (supervisedLaunch?.strategy === "windows_job_object") {
|
|
423
|
+
// The outer supervisor must receive stdin before it can launch the
|
|
424
|
+
// inner provider. Its start-grant file prevents prompt delivery.
|
|
425
|
+
writeProviderInput();
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
child.once("spawn", () => {
|
|
429
|
+
void readiness?.then(writeProviderInput).catch(fail);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const readers = [
|
|
434
|
+
readProcessLines(childStdout, (line) => {
|
|
435
|
+
if (request.captureOutput !== false)
|
|
436
|
+
stdout.appendLine(line);
|
|
437
|
+
const handled = handlers.onStdoutLine?.(line);
|
|
438
|
+
if (!terminalExitTimer && isProviderTerminalSuccess(line)) {
|
|
439
|
+
terminalExitTimer = setTimeout(() => {
|
|
440
|
+
if (closed || settled || cancellationRequested)
|
|
441
|
+
return;
|
|
442
|
+
terminalTerminationStarted = true;
|
|
443
|
+
void terminate().catch(fail);
|
|
444
|
+
}, this.terminalExitGraceMs);
|
|
445
|
+
terminalExitTimer.unref?.();
|
|
446
|
+
}
|
|
447
|
+
return handled;
|
|
448
|
+
}),
|
|
449
|
+
readProcessLines(childStderr, (line) => {
|
|
450
|
+
if (request.captureOutput !== false)
|
|
451
|
+
stderr.appendLine(line);
|
|
452
|
+
return handlers.onStderrLine?.(line);
|
|
453
|
+
})
|
|
454
|
+
];
|
|
455
|
+
for (const reader of readers) {
|
|
456
|
+
void reader.catch(fail);
|
|
457
|
+
}
|
|
458
|
+
child.on("close", (code) => {
|
|
459
|
+
closed = true;
|
|
460
|
+
Promise.allSettled(readers).then(async (readerResults) => {
|
|
461
|
+
await readiness?.catch(() => undefined);
|
|
462
|
+
const rejected = readerResults.find((result) => result.status === "rejected");
|
|
463
|
+
if (rejected?.status === "rejected") {
|
|
464
|
+
fail(rejected.reason);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (settled) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
memoryObserver?.sample();
|
|
472
|
+
if (supervisedLaunch?.strategy === "unix_process_group" || !supervisedLaunch) {
|
|
473
|
+
await terminate();
|
|
474
|
+
}
|
|
475
|
+
else if (termination) {
|
|
476
|
+
await termination;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
fail(error);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
settled = true;
|
|
484
|
+
cleanup();
|
|
485
|
+
const memory = memoryObserver?.complete();
|
|
486
|
+
resolve({
|
|
487
|
+
// A structured terminal-success event is the provider protocol's
|
|
488
|
+
// authoritative completion signal. If the CLI stays resident after
|
|
489
|
+
// that event, terminate the supervised tree after a short grace
|
|
490
|
+
// period and preserve the successful handoff so the existing
|
|
491
|
+
// artifact, commit, and publication gates can run.
|
|
492
|
+
exitCode: terminalTerminationStarted && !cancellationRequested ? 0 : code ?? 1,
|
|
493
|
+
stdout: stdout.toString(),
|
|
494
|
+
stderr: stderr.toString(),
|
|
495
|
+
...(memory ?? {}),
|
|
496
|
+
});
|
|
497
|
+
}).catch(fail);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function isProviderTerminalSuccess(line) {
|
|
503
|
+
const trimmed = line.trim();
|
|
504
|
+
if (!trimmed.startsWith("{"))
|
|
505
|
+
return false;
|
|
506
|
+
try {
|
|
507
|
+
const event = JSON.parse(trimmed);
|
|
508
|
+
return event.type === "turn.completed"
|
|
509
|
+
|| (event.type === "result" && event.subtype === "success" && event.is_error !== true);
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
class HostProcessTreeMemoryObserver {
|
|
516
|
+
launch;
|
|
517
|
+
processGroupId;
|
|
518
|
+
peakMemoryBytes = 0;
|
|
519
|
+
sampleCount = 0;
|
|
520
|
+
observedAt = null;
|
|
521
|
+
timer;
|
|
522
|
+
constructor(launch, processGroupId) {
|
|
523
|
+
this.launch = launch;
|
|
524
|
+
this.processGroupId = processGroupId;
|
|
525
|
+
if (launch.strategy === "unix_process_group" && processGroupId > 0) {
|
|
526
|
+
this.sample();
|
|
527
|
+
this.timer = setInterval(() => this.sample(), 1_000);
|
|
528
|
+
this.timer.unref?.();
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
this.timer = null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
sample() {
|
|
535
|
+
if (this.launch.strategy !== "unix_process_group" || this.processGroupId <= 0)
|
|
536
|
+
return;
|
|
537
|
+
const bytes = processGroupResidentMemoryBytes(this.processGroupId);
|
|
538
|
+
if (bytes === null)
|
|
539
|
+
return;
|
|
540
|
+
this.sampleCount += 1;
|
|
541
|
+
this.peakMemoryBytes = Math.max(this.peakMemoryBytes, bytes);
|
|
542
|
+
this.observedAt = new Date().toISOString();
|
|
543
|
+
}
|
|
544
|
+
complete() {
|
|
545
|
+
if (this.timer)
|
|
546
|
+
clearInterval(this.timer);
|
|
547
|
+
if (this.launch.strategy === "windows_job_object") {
|
|
548
|
+
return readWindowsJobMemoryTelemetry(this.launch.memoryTelemetryPath);
|
|
549
|
+
}
|
|
550
|
+
if (this.peakMemoryBytes <= 0 || this.sampleCount <= 0 || !this.observedAt)
|
|
551
|
+
return undefined;
|
|
552
|
+
return {
|
|
553
|
+
peakMemoryBytes: this.peakMemoryBytes,
|
|
554
|
+
memorySampleCount: this.sampleCount,
|
|
555
|
+
memorySource: process.platform === "linux"
|
|
556
|
+
? "linux_process_group_rss"
|
|
557
|
+
: "unix_process_group_rss",
|
|
558
|
+
memoryObservedAt: this.observedAt,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/** Returns aggregate resident bytes for the exact supervised Unix process group. */
|
|
563
|
+
export function processGroupResidentMemoryBytes(processGroupId, platform = process.platform, procRoot = "/proc") {
|
|
564
|
+
if (!Number.isSafeInteger(processGroupId) || processGroupId <= 0)
|
|
565
|
+
return null;
|
|
566
|
+
if (platform === "linux") {
|
|
567
|
+
let totalKilobytes = 0;
|
|
568
|
+
let matched = 0;
|
|
569
|
+
try {
|
|
570
|
+
for (const entry of fs.readdirSync(procRoot, { withFileTypes: true })) {
|
|
571
|
+
if (!entry.isDirectory() || !/^\d+$/u.test(entry.name))
|
|
572
|
+
continue;
|
|
573
|
+
try {
|
|
574
|
+
const stat = fs.readFileSync(path.join(procRoot, entry.name, "stat"), "utf8");
|
|
575
|
+
const close = stat.lastIndexOf(")");
|
|
576
|
+
const fields = close >= 0 ? stat.slice(close + 2).trim().split(/\s+/u) : [];
|
|
577
|
+
if (Number.parseInt(fields[2] ?? "", 10) !== processGroupId)
|
|
578
|
+
continue;
|
|
579
|
+
const status = fs.readFileSync(path.join(procRoot, entry.name, "status"), "utf8");
|
|
580
|
+
const rss = /^VmRSS:\s+(\d+)\s+kB$/mu.exec(status);
|
|
581
|
+
if (!rss)
|
|
582
|
+
continue;
|
|
583
|
+
totalKilobytes += Number.parseInt(rss[1], 10);
|
|
584
|
+
matched += 1;
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// Processes can exit between the directory, stat, and status reads.
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return matched > 0 ? totalKilobytes * 1024 : null;
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (platform !== "win32") {
|
|
597
|
+
try {
|
|
598
|
+
const output = execFileSync("ps", ["-axo", "pgid=,rss="], {
|
|
599
|
+
encoding: "utf8",
|
|
600
|
+
timeout: 5_000,
|
|
601
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
602
|
+
});
|
|
603
|
+
let totalKilobytes = 0;
|
|
604
|
+
let matched = 0;
|
|
605
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
606
|
+
const fields = line.trim().split(/\s+/u);
|
|
607
|
+
if (Number.parseInt(fields[0] ?? "", 10) !== processGroupId)
|
|
608
|
+
continue;
|
|
609
|
+
const rss = Number.parseInt(fields[1] ?? "", 10);
|
|
610
|
+
if (!Number.isSafeInteger(rss) || rss < 0)
|
|
611
|
+
continue;
|
|
612
|
+
totalKilobytes += rss;
|
|
613
|
+
matched += 1;
|
|
614
|
+
}
|
|
615
|
+
return matched > 0 ? totalKilobytes * 1024 : null;
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
function readWindowsJobMemoryTelemetry(target) {
|
|
624
|
+
if (!target)
|
|
625
|
+
return undefined;
|
|
626
|
+
try {
|
|
627
|
+
const value = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
628
|
+
const peakMemoryBytes = value.peakMemoryBytes;
|
|
629
|
+
const sampleCount = value.sampleCount;
|
|
630
|
+
const source = value.source;
|
|
631
|
+
const observedAt = value.observedAt;
|
|
632
|
+
if (!Number.isSafeInteger(peakMemoryBytes) || peakMemoryBytes <= 0
|
|
633
|
+
|| !Number.isSafeInteger(sampleCount) || sampleCount <= 0
|
|
634
|
+
|| typeof source !== "string" || source.length === 0
|
|
635
|
+
|| typeof observedAt !== "string" || !Number.isFinite(Date.parse(observedAt))) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
peakMemoryBytes: peakMemoryBytes,
|
|
640
|
+
memorySampleCount: sampleCount,
|
|
641
|
+
memorySource: source,
|
|
642
|
+
memoryObservedAt: observedAt,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
class BoundedOutputTail {
|
|
650
|
+
maxChars;
|
|
651
|
+
value = "";
|
|
652
|
+
constructor(maxChars) {
|
|
653
|
+
this.maxChars = maxChars;
|
|
654
|
+
}
|
|
655
|
+
appendLine(line) {
|
|
656
|
+
const chunk = `${line}\n`;
|
|
657
|
+
if (chunk.length >= this.maxChars) {
|
|
658
|
+
this.value = chunk.slice(-this.maxChars);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const retainedPrefixChars = this.maxChars - chunk.length;
|
|
662
|
+
this.value = `${this.value.slice(-retainedPrefixChars)}${chunk}`;
|
|
663
|
+
}
|
|
664
|
+
toString() {
|
|
665
|
+
return this.value;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function resolveProviderProcessLaunch(command, args) {
|
|
669
|
+
if (process.platform !== "win32") {
|
|
670
|
+
return { command, args };
|
|
671
|
+
}
|
|
672
|
+
const cursorDirect = await resolveCursorAgentDirectLaunch(command, args);
|
|
673
|
+
if (cursorDirect) {
|
|
674
|
+
return cursorDirect;
|
|
675
|
+
}
|
|
676
|
+
const resolved = await resolveWindowsCommand(command);
|
|
677
|
+
if (!resolved) {
|
|
678
|
+
return { command, args };
|
|
679
|
+
}
|
|
680
|
+
return resolveCodexNpmDirectLaunch(resolved, args)
|
|
681
|
+
?? resolveClaudeNpmDirectLaunch(resolved, args)
|
|
682
|
+
?? resolvePackageManagerDirectLaunch(resolved, args)
|
|
683
|
+
?? buildWindowsLaunchCommand(resolved, args);
|
|
684
|
+
}
|
|
685
|
+
function grantWindowsProviderStart(launch) {
|
|
686
|
+
if (!launch.startGrantPath) {
|
|
687
|
+
throw new Error("host_process_supervisor_invalid: Windows provider start-grant path was not assigned.");
|
|
688
|
+
}
|
|
689
|
+
fs.writeFileSync(launch.startGrantPath, "granted\n", {
|
|
690
|
+
encoding: "utf8",
|
|
691
|
+
flag: "wx",
|
|
692
|
+
mode: 0o600,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async function waitForWindowsProviderReady(launch, signal, isClosed) {
|
|
696
|
+
const readyPath = launch.readyPath;
|
|
697
|
+
if (!readyPath) {
|
|
698
|
+
throw new Error("host_process_supervisor_invalid: Windows provider-ready path was not assigned.");
|
|
699
|
+
}
|
|
700
|
+
while (true) {
|
|
701
|
+
try {
|
|
702
|
+
const parsed = JSON.parse(await fs.promises.readFile(readyPath, "utf8"));
|
|
703
|
+
if (parsed.schemaVersion !== 1
|
|
704
|
+
|| parsed.supervisorId !== launch.supervisorId
|
|
705
|
+
|| !Number.isSafeInteger(parsed.rootPid)
|
|
706
|
+
|| (parsed.rootPid ?? 0) <= 0
|
|
707
|
+
|| typeof parsed.rootIdentity !== "string"
|
|
708
|
+
|| !/^win32:\d+$/u.test(parsed.rootIdentity)
|
|
709
|
+
|| !Number.isSafeInteger(parsed.providerPid)
|
|
710
|
+
|| (parsed.providerPid ?? 0) <= 0
|
|
711
|
+
|| typeof parsed.providerCreatedAt !== "string"
|
|
712
|
+
|| Number.isNaN(Date.parse(parsed.providerCreatedAt))
|
|
713
|
+
|| (parsed.providerIdentity !== undefined
|
|
714
|
+
&& (typeof parsed.providerIdentity !== "string" || !/^win32:\d+$/u.test(parsed.providerIdentity)))
|
|
715
|
+
|| !Number.isSafeInteger(parsed.startAttempt)
|
|
716
|
+
|| (parsed.startAttempt ?? 0) < 1) {
|
|
717
|
+
throw new Error("host_process_supervisor_invalid: provider-ready receipt was invalid.");
|
|
718
|
+
}
|
|
719
|
+
return parsed;
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
if (error.code !== "ENOENT")
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
if (isClosed()) {
|
|
726
|
+
throw new ForgeFailureError("attempt_process_supervisor_start_failed", "host_process_supervisor_start_failed: supervisor exited before the provider became ready.", "bridge");
|
|
727
|
+
}
|
|
728
|
+
if (signal?.aborted) {
|
|
729
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Provider process launch was aborted.");
|
|
730
|
+
}
|
|
731
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
export function resolveCodexNpmDirectLaunch(commandPath, args) {
|
|
735
|
+
const extension = path.extname(commandPath).toLowerCase();
|
|
736
|
+
if ((extension !== ".cmd" && extension !== ".ps1")
|
|
737
|
+
|| path.basename(commandPath, extension).toLowerCase() !== "codex") {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
const entryPoint = path.join(path.dirname(commandPath), "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
741
|
+
return fs.existsSync(entryPoint)
|
|
742
|
+
? { command: process.execPath, args: [entryPoint, ...args] }
|
|
743
|
+
: null;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* npm installs of Claude Code (`npm i -g @anthropic-ai/claude-code`) put a `claude.cmd` shim on
|
|
747
|
+
* PATH; launch its cli.js directly for the same Job Object reasons as Codex. The native installer
|
|
748
|
+
* ships `claude.exe`, which is preferred by PATH resolution and needs no rewrite.
|
|
749
|
+
*/
|
|
750
|
+
export function resolveClaudeNpmDirectLaunch(commandPath, args) {
|
|
751
|
+
const extension = path.extname(commandPath).toLowerCase();
|
|
752
|
+
if ((extension !== ".cmd" && extension !== ".ps1")
|
|
753
|
+
|| path.basename(commandPath, extension).toLowerCase() !== "claude") {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
const entryPoint = path.join(path.dirname(commandPath), "node_modules", "@anthropic-ai", "claude-code", "cli.js");
|
|
757
|
+
return fs.existsSync(entryPoint)
|
|
758
|
+
? { command: process.execPath, args: [entryPoint, ...args] }
|
|
759
|
+
: null;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Avoids cmd.exe's nested-quote handling for package-manager shims. That
|
|
763
|
+
* quoting loses executable paths containing spaces after the command is
|
|
764
|
+
* wrapped by the Windows Job Object supervisor.
|
|
765
|
+
*/
|
|
766
|
+
export function resolvePackageManagerDirectLaunch(commandPath, args) {
|
|
767
|
+
const extension = path.extname(commandPath).toLowerCase();
|
|
768
|
+
if (extension !== ".cmd" && extension !== ".ps1")
|
|
769
|
+
return null;
|
|
770
|
+
const packageManager = path.basename(commandPath, extension).toLowerCase();
|
|
771
|
+
if (!["corepack", "npm", "npx", "pnpm", "pnpx", "yarn", "yarnpkg"].includes(packageManager))
|
|
772
|
+
return null;
|
|
773
|
+
const root = path.dirname(commandPath);
|
|
774
|
+
const candidates = [
|
|
775
|
+
path.join(root, "node_modules", "corepack", "dist", packageManager === "corepack" ? "corepack.js" : `${packageManager}.js`),
|
|
776
|
+
...(packageManager === "npm" || packageManager === "npx"
|
|
777
|
+
? [path.join(root, "node_modules", "npm", "bin", `${packageManager}-cli.js`)]
|
|
778
|
+
: []),
|
|
779
|
+
...(packageManager === "pnpm" || packageManager === "pnpx"
|
|
780
|
+
? [path.join(root, "node_modules", "pnpm", "bin", `${packageManager}.cjs`)]
|
|
781
|
+
: []),
|
|
782
|
+
...(packageManager === "yarn" || packageManager === "yarnpkg"
|
|
783
|
+
? [path.join(root, "node_modules", "yarn", "bin", "yarn.js")]
|
|
784
|
+
: []),
|
|
785
|
+
];
|
|
786
|
+
const entryPoint = candidates.find(candidate => fs.existsSync(candidate));
|
|
787
|
+
if (!entryPoint)
|
|
788
|
+
return null;
|
|
789
|
+
const adjacentNode = path.join(root, "node.exe");
|
|
790
|
+
return {
|
|
791
|
+
command: fs.existsSync(adjacentNode) ? adjacentNode : process.execPath,
|
|
792
|
+
args: [entryPoint, ...args],
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
async function resolveWindowsCommand(command) {
|
|
796
|
+
if (path.isAbsolute(command)) {
|
|
797
|
+
return command;
|
|
798
|
+
}
|
|
799
|
+
const resolvedFromPath = resolveWindowsCommandFromPath(command);
|
|
800
|
+
if (resolvedFromPath) {
|
|
801
|
+
return resolvedFromPath;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
const result = await execFileAsync("where.exe", [command], { timeout: 2000 });
|
|
805
|
+
return chooseWindowsCommandCandidate(result.stdout.split(/\r?\n/));
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Resolves Windows commands without spawning where.exe. Runtime launches can
|
|
813
|
+
* happen while the machine is under enough process pressure for where.exe to
|
|
814
|
+
* miss its timeout; returning the unresolved package-manager name in that case
|
|
815
|
+
* makes Node fall back to a shell shim that cannot survive Job Object wrapping.
|
|
816
|
+
*/
|
|
817
|
+
export function resolveWindowsCommandFromPath(command, pathValue = process.env.PATH ?? "", pathExtValue = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
|
|
818
|
+
if (!command || path.isAbsolute(command) || command.includes("/") || command.includes("\\")) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
const hasExtension = path.extname(command).length > 0;
|
|
822
|
+
const extensions = hasExtension
|
|
823
|
+
? [""]
|
|
824
|
+
: [...new Set([
|
|
825
|
+
"",
|
|
826
|
+
".exe",
|
|
827
|
+
".ps1",
|
|
828
|
+
".cmd",
|
|
829
|
+
".bat",
|
|
830
|
+
...pathExtValue.split(";").map(value => value.trim().toLowerCase()).filter(Boolean),
|
|
831
|
+
])];
|
|
832
|
+
for (const rawDirectory of pathValue.split(path.delimiter)) {
|
|
833
|
+
const directory = rawDirectory.trim().replace(/^"|"$/gu, "");
|
|
834
|
+
if (!directory)
|
|
835
|
+
continue;
|
|
836
|
+
const candidates = [];
|
|
837
|
+
for (const extension of extensions) {
|
|
838
|
+
const candidate = path.join(directory, `${command}${extension}`);
|
|
839
|
+
if (fs.existsSync(candidate))
|
|
840
|
+
candidates.push(candidate);
|
|
841
|
+
}
|
|
842
|
+
const resolved = chooseWindowsCommandCandidate(candidates);
|
|
843
|
+
if (resolved)
|
|
844
|
+
return resolved;
|
|
845
|
+
}
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
export function chooseWindowsCommandCandidate(candidates) {
|
|
849
|
+
const normalized = candidates
|
|
850
|
+
.map((candidate) => candidate.trim())
|
|
851
|
+
.filter((candidate) => candidate.length > 0);
|
|
852
|
+
if (normalized.length === 0) {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
const preferredExtensions = [".exe", ".ps1", ".cmd", ".bat", ""];
|
|
856
|
+
for (const extension of preferredExtensions) {
|
|
857
|
+
const candidate = normalized.find((entry) => path.extname(entry).toLowerCase() === extension);
|
|
858
|
+
if (candidate) {
|
|
859
|
+
return candidate;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return normalized[0];
|
|
863
|
+
}
|
|
864
|
+
async function readProcessLines(stream, onLine) {
|
|
865
|
+
const reader = readline.createInterface({ input: stream });
|
|
866
|
+
try {
|
|
867
|
+
for await (const line of reader) {
|
|
868
|
+
await onLine(line);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
finally {
|
|
872
|
+
reader.close();
|
|
873
|
+
}
|
|
874
|
+
}
|