@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.e7b62d8fdd33
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 +83 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +55 -3
- package/dist/bridge/bridge.service.js +834 -87
- package/dist/bridge/bridge.types.d.ts +89 -1
- 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.js +37 -3
- package/dist/cli/cli.service.d.ts +33 -2
- package/dist/cli/cli.service.js +209 -27
- package/dist/cli/local-config.d.ts +5 -0
- package/dist/cli/local-config.js +45 -1
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +105 -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 +386 -0
- package/dist/runtime/docker-engine-probe.d.ts +83 -0
- package/dist/runtime/docker-engine-probe.js +300 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +46 -0
- package/dist/runtime/host-listener-inspector.js +349 -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 +47 -0
- package/dist/runtime/host-runtime-contract.js +700 -0
- package/dist/runtime/host-runtime-lease.d.ts +197 -0
- package/dist/runtime/host-runtime-lease.js +2203 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/memory-capacity.d.ts +68 -0
- package/dist/runtime/memory-capacity.js +121 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +284 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +45 -1
- package/dist/sandbox/preflight.js +299 -34
- package/dist/sandbox/process.js +125 -40
- package/dist/sandbox/repository-lock.js +41 -3
- package/dist/sandbox/runtime.d.ts +66 -3
- package/dist/sandbox/runtime.js +670 -105
- package/dist/sandbox/sandbox.types.d.ts +10 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.js +1 -1
- package/dist/sandbox/worker.js +2 -5
- package/dist/sandbox/workspace.service.d.ts +1 -0
- package/dist/sandbox/workspace.service.js +28 -5
- package/dist/session/managed-attempt-tools.d.ts +21 -0
- package/dist/session/managed-attempt-tools.js +912 -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 +335 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +233 -24
- package/dist/session/phase-completion.d.ts +12 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +51 -10
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +24 -8
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +189 -8
- package/dist/session/phase-completion.schemas.js +117 -4
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +153 -22
- package/dist/session/pre-publication-validation.d.ts +46 -0
- package/dist/session/pre-publication-validation.js +208 -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 +3 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
- package/dist/session/provider-cli/provider-cli.deterministic.js +49 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +25 -0
- package/dist/session/provider-cli/provider-cli.process.js +463 -34
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
- package/dist/session/provider-cli/provider-cli.prompts.js +108 -25
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -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 +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +22 -2
- package/dist/session/provider-cli/provider-cli.service.js +524 -100
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +91 -16
- 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 +38 -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 +522 -41
- package/dist/session/session.types.d.ts +72 -0
- package/dist/session/session.types.js +74 -3
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- 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 +14 -0
- package/dist/shared/process/dotnet-toolchain.js +179 -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 +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/redaction/redaction.js +12 -2
- 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 +43 -4
- package/dist/worktree/dependency-warmer.d.ts +28 -1
- package/dist/worktree/dependency-warmer.js +308 -9
- 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/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 +13 -2
- package/dist/worktree/worktree.service.js +238 -47
- package/dist/worktree/worktree.types.d.ts +5 -1
- package/package.json +5 -3
- package/runtime/windows-job-supervisor.ps1 +444 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LoadedHostRuntimeContract } from "./host-runtime-contract.js";
|
|
2
|
+
export interface HostParallelPromotionRecord {
|
|
3
|
+
readonly schemaVersion: 1;
|
|
4
|
+
readonly repoId: string;
|
|
5
|
+
readonly runtimeContractFingerprint: string;
|
|
6
|
+
readonly runtimeContractSource?: "project" | "derived";
|
|
7
|
+
readonly observedAt: string;
|
|
8
|
+
readonly observedDispatchAttemptId: string;
|
|
9
|
+
readonly verifiedPortNames: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/** Persists only observed, contract-bound host parallel-safety evidence. */
|
|
12
|
+
export declare class HostParallelPromotionStore {
|
|
13
|
+
private readonly stateRoot;
|
|
14
|
+
constructor(stateRoot: string);
|
|
15
|
+
load(repoId: string): Promise<HostParallelPromotionRecord | null>;
|
|
16
|
+
list(): Promise<readonly HostParallelPromotionRecord[]>;
|
|
17
|
+
observe(repoId: string, runtimeContract: LoadedHostRuntimeContract, dispatchAttemptId: string, verifiedPortNames: readonly string[]): Promise<HostParallelPromotionRecord>;
|
|
18
|
+
invalidate(repoId: string): Promise<void>;
|
|
19
|
+
private recordPath;
|
|
20
|
+
}
|
|
21
|
+
export declare function hostRuntimeContractFingerprint(runtimeContract: LoadedHostRuntimeContract): string;
|
|
22
|
+
export declare function hostRuntimeParallelEligible(runtimeContract: LoadedHostRuntimeContract): boolean;
|
|
23
|
+
export declare function configuredLaunchPortNames(runtimeContract: LoadedHostRuntimeContract): readonly string[];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Persists only observed, contract-bound host parallel-safety evidence. */
|
|
5
|
+
export class HostParallelPromotionStore {
|
|
6
|
+
stateRoot;
|
|
7
|
+
constructor(stateRoot) {
|
|
8
|
+
this.stateRoot = stateRoot;
|
|
9
|
+
}
|
|
10
|
+
async load(repoId) {
|
|
11
|
+
const normalizedRepoId = normalizeRepoId(repoId);
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(await fs.readFile(this.recordPath(normalizedRepoId), "utf8"));
|
|
14
|
+
return validRecord(parsed, normalizedRepoId) ? parsed : null;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError)
|
|
18
|
+
return null;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async list() {
|
|
23
|
+
const root = path.join(this.stateRoot, "runtime-promotion", "host");
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = await fs.readdir(root);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === "ENOENT")
|
|
30
|
+
return [];
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
const records = [];
|
|
34
|
+
for (const entry of entries.filter(value => /^[0-9a-f]{64}\.json$/u.test(value)).slice(0, 4096)) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(await fs.readFile(path.join(root, entry), "utf8"));
|
|
37
|
+
const repoId = typeof parsed.repoId === "string" ? normalizeRepoId(parsed.repoId) : "";
|
|
38
|
+
if (repoId && validRecord(parsed, repoId))
|
|
39
|
+
records.push(parsed);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!(error instanceof SyntaxError))
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return records.sort((left, right) => left.repoId.localeCompare(right.repoId));
|
|
47
|
+
}
|
|
48
|
+
async observe(repoId, runtimeContract, dispatchAttemptId, verifiedPortNames) {
|
|
49
|
+
if (!hostRuntimeParallelEligible(runtimeContract)) {
|
|
50
|
+
throw new Error("host_parallel_promotion_ineligible: the runtime contract is not eligible for parallel host execution.");
|
|
51
|
+
}
|
|
52
|
+
const requiredPorts = configuredLaunchPortNames(runtimeContract);
|
|
53
|
+
const observedPorts = [...new Set(verifiedPortNames)].sort((left, right) => left.localeCompare(right));
|
|
54
|
+
if (requiredPorts.length !== observedPorts.length
|
|
55
|
+
|| requiredPorts.some((name, index) => name !== observedPorts[index])) {
|
|
56
|
+
throw new Error("host_parallel_promotion_unverified: every configured runtime listener must pass its assigned-port probe.");
|
|
57
|
+
}
|
|
58
|
+
const normalizedRepoId = normalizeRepoId(repoId);
|
|
59
|
+
const record = {
|
|
60
|
+
schemaVersion: 1,
|
|
61
|
+
repoId: normalizedRepoId,
|
|
62
|
+
runtimeContractFingerprint: hostRuntimeContractFingerprint(runtimeContract),
|
|
63
|
+
runtimeContractSource: runtimeContract.source === "project" ? "project" : "derived",
|
|
64
|
+
observedAt: new Date().toISOString(),
|
|
65
|
+
observedDispatchAttemptId: normalizedUuid(dispatchAttemptId, "dispatch attempt"),
|
|
66
|
+
verifiedPortNames: observedPorts,
|
|
67
|
+
};
|
|
68
|
+
const target = this.recordPath(normalizedRepoId);
|
|
69
|
+
const root = path.dirname(target);
|
|
70
|
+
await ensurePrivateDirectory(root);
|
|
71
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
72
|
+
try {
|
|
73
|
+
const handle = await fs.open(temporary, "wx", 0o600);
|
|
74
|
+
try {
|
|
75
|
+
await handle.writeFile(`${JSON.stringify(record)}\n`, "utf8");
|
|
76
|
+
await handle.sync();
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await handle.close();
|
|
80
|
+
}
|
|
81
|
+
await fs.rename(temporary, target);
|
|
82
|
+
return record;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async invalidate(repoId) {
|
|
90
|
+
await fs.rm(this.recordPath(normalizeRepoId(repoId)), { force: true });
|
|
91
|
+
}
|
|
92
|
+
recordPath(normalizedRepoId) {
|
|
93
|
+
const key = createHash("sha256").update(normalizedRepoId).digest("hex");
|
|
94
|
+
return path.join(this.stateRoot, "runtime-promotion", "host", `${key}.json`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function hostRuntimeContractFingerprint(runtimeContract) {
|
|
98
|
+
const canonical = JSON.stringify({
|
|
99
|
+
schemaVersion: runtimeContract.contract.schemaVersion,
|
|
100
|
+
runtime: runtimeContract.contract.runtime,
|
|
101
|
+
derivedCandidateFingerprint: runtimeContract.source === "derived"
|
|
102
|
+
? runtimeContract.discovery?.candidateFingerprint ?? null
|
|
103
|
+
: null,
|
|
104
|
+
});
|
|
105
|
+
return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
|
|
106
|
+
}
|
|
107
|
+
export function hostRuntimeParallelEligible(runtimeContract) {
|
|
108
|
+
const runtime = runtimeContract.contract.runtime;
|
|
109
|
+
const launchedPorts = runtime.ports.filter(port => port.launch !== null);
|
|
110
|
+
return (runtimeContract.source === "project" || runtimeContract.source === "derived")
|
|
111
|
+
&& runtime.hostConcurrency === "parallel"
|
|
112
|
+
&& runtime.browserProfile === "isolated"
|
|
113
|
+
&& runtime.singletonResources.length === 0
|
|
114
|
+
&& launchedPorts.length > 0
|
|
115
|
+
&& launchedPorts.length === runtime.ports.length
|
|
116
|
+
&& launchedPorts.every(port => port.adapter !== null);
|
|
117
|
+
}
|
|
118
|
+
export function configuredLaunchPortNames(runtimeContract) {
|
|
119
|
+
return runtimeContract.contract.runtime.ports
|
|
120
|
+
.filter(port => port.launch !== null)
|
|
121
|
+
.map(port => port.name)
|
|
122
|
+
.sort((left, right) => left.localeCompare(right));
|
|
123
|
+
}
|
|
124
|
+
function validRecord(value, repoId) {
|
|
125
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
126
|
+
return false;
|
|
127
|
+
const record = value;
|
|
128
|
+
return record.schemaVersion === 1
|
|
129
|
+
&& record.repoId === repoId
|
|
130
|
+
&& /^sha256:[0-9a-f]{64}$/u.test(record.runtimeContractFingerprint ?? "")
|
|
131
|
+
&& Number.isFinite(Date.parse(record.observedAt ?? ""))
|
|
132
|
+
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(record.observedDispatchAttemptId ?? "")
|
|
133
|
+
&& Array.isArray(record.verifiedPortNames)
|
|
134
|
+
&& record.verifiedPortNames.length > 0
|
|
135
|
+
&& record.verifiedPortNames.length <= 16
|
|
136
|
+
&& record.verifiedPortNames.every(name => typeof name === "string" && /^[a-z][a-z0-9_-]{0,63}$/u.test(name));
|
|
137
|
+
}
|
|
138
|
+
function normalizeRepoId(repoId) {
|
|
139
|
+
const normalized = repoId.trim().toLowerCase();
|
|
140
|
+
if (!normalized || normalized.length > 512 || Array.from(normalized).some(character => {
|
|
141
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
142
|
+
return codePoint < 32 || codePoint === 127;
|
|
143
|
+
})) {
|
|
144
|
+
throw new Error("host_parallel_promotion_invalid: repository identity is invalid.");
|
|
145
|
+
}
|
|
146
|
+
return normalized;
|
|
147
|
+
}
|
|
148
|
+
function normalizedUuid(value, label) {
|
|
149
|
+
const normalized = value.trim().toLowerCase();
|
|
150
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(normalized)) {
|
|
151
|
+
throw new Error(`host_parallel_promotion_invalid: ${label} must be a full UUID.`);
|
|
152
|
+
}
|
|
153
|
+
return normalized;
|
|
154
|
+
}
|
|
155
|
+
async function ensurePrivateDirectory(target) {
|
|
156
|
+
await fs.mkdir(target, { recursive: true, mode: 0o700 });
|
|
157
|
+
if (process.platform !== "win32")
|
|
158
|
+
await fs.chmod(target, 0o700);
|
|
159
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export declare const memoryAdmissionPolicyVersion = "memory-admission-v1";
|
|
2
|
+
export declare const memoryAdmissionEquation = "max(0, min(stabilized_available - reserve - pending_commitment, total - reserve - committed))";
|
|
3
|
+
export interface MemoryAdmissionDiagnostics {
|
|
4
|
+
readonly policyVersion: string;
|
|
5
|
+
readonly sampledAt: string;
|
|
6
|
+
readonly totalMemoryBytes: number;
|
|
7
|
+
readonly rawAvailableMemoryBytes: number;
|
|
8
|
+
readonly stabilizedAvailableMemoryBytes: number;
|
|
9
|
+
readonly reclaimableMemoryBytes: number | null;
|
|
10
|
+
readonly reclaimableMemorySource: "not_sampled_separately";
|
|
11
|
+
readonly controlPlaneMemoryBytes: number;
|
|
12
|
+
readonly reserveMemoryBytes: number;
|
|
13
|
+
readonly committedMemoryBytes: number;
|
|
14
|
+
readonly pendingCommitmentMemoryBytes: number;
|
|
15
|
+
readonly availableForAdmissionBytes: number;
|
|
16
|
+
readonly requestedMemoryBytes: number | null;
|
|
17
|
+
readonly shortfallMemoryBytes: number | null;
|
|
18
|
+
readonly decision: "snapshot" | "admitted" | "denied";
|
|
19
|
+
readonly equation: string;
|
|
20
|
+
}
|
|
21
|
+
export interface MemoryAdmissionInputs {
|
|
22
|
+
readonly sampledAt: string;
|
|
23
|
+
readonly totalMemoryBytes: number;
|
|
24
|
+
readonly rawAvailableMemoryBytes: number;
|
|
25
|
+
readonly stabilizedAvailableMemoryBytes: number;
|
|
26
|
+
readonly controlPlaneMemoryBytes: number;
|
|
27
|
+
readonly reserveMemoryBytes: number;
|
|
28
|
+
/** Ambient host use plus every committed Forge request, including pending requests. */
|
|
29
|
+
readonly committedMemoryBytes: number;
|
|
30
|
+
/** Commitments not yet reflected in the OS available-memory sample. */
|
|
31
|
+
readonly pendingCommitmentMemoryBytes: number;
|
|
32
|
+
readonly requestedMemoryBytes?: number | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One memory admission equation for Docker and managed-host capacity. The raw
|
|
36
|
+
* OS/daemon sample already reflects resident active children, so only pending
|
|
37
|
+
* commitments are charged against sampled headroom. The total-memory ceiling
|
|
38
|
+
* independently charges every commitment and prevents reservation overcommit.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildMemoryAdmissionDiagnostics(inputs: MemoryAdmissionInputs): MemoryAdmissionDiagnostics;
|
|
41
|
+
export interface AdaptiveMemoryReserveOptions {
|
|
42
|
+
readonly explicitBytes?: number | null;
|
|
43
|
+
readonly floorBytes?: number | null;
|
|
44
|
+
readonly ceilingBytes?: number | null;
|
|
45
|
+
readonly percent?: number | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Derives a host safety reserve from the capacity domain it protects. An
|
|
49
|
+
* explicit operator value remains authoritative; otherwise the reserve scales
|
|
50
|
+
* with the sampled total inside conservative floors and ceilings.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveAdaptiveMemoryReserveBytes(totalMemoryBytes: number, options?: AdaptiveMemoryReserveOptions): number;
|
|
53
|
+
/**
|
|
54
|
+
* Applies pressure immediately while requiring durable recovery samples before
|
|
55
|
+
* increasing advertised capacity. This prevents one GC/cache sample from
|
|
56
|
+
* waking queued work and then immediately withdrawing the same capacity.
|
|
57
|
+
*/
|
|
58
|
+
export declare class RecoveryStabilizedMemory {
|
|
59
|
+
private readonly requiredRecoverySamples;
|
|
60
|
+
private readonly minimumRecoverySampleIntervalMs;
|
|
61
|
+
private stableBytes;
|
|
62
|
+
private recoveryFloorBytes;
|
|
63
|
+
private recoverySamples;
|
|
64
|
+
private lastRecoverySampleAt;
|
|
65
|
+
constructor(requiredRecoverySamples?: number, minimumRecoverySampleIntervalMs?: number);
|
|
66
|
+
observe(rawAvailableBytes: number, sampledAt?: number): number;
|
|
67
|
+
private resetRecovery;
|
|
68
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const GiB = 1024 ** 3;
|
|
2
|
+
export const memoryAdmissionPolicyVersion = "memory-admission-v1";
|
|
3
|
+
export const memoryAdmissionEquation = "max(0, min(stabilized_available - reserve - pending_commitment, total - reserve - committed))";
|
|
4
|
+
/**
|
|
5
|
+
* One memory admission equation for Docker and managed-host capacity. The raw
|
|
6
|
+
* OS/daemon sample already reflects resident active children, so only pending
|
|
7
|
+
* commitments are charged against sampled headroom. The total-memory ceiling
|
|
8
|
+
* independently charges every commitment and prevents reservation overcommit.
|
|
9
|
+
*/
|
|
10
|
+
export function buildMemoryAdmissionDiagnostics(inputs) {
|
|
11
|
+
const totalMemoryBytes = nonNegativeInteger(inputs.totalMemoryBytes);
|
|
12
|
+
const rawAvailableMemoryBytes = nonNegativeInteger(inputs.rawAvailableMemoryBytes);
|
|
13
|
+
const stabilizedAvailableMemoryBytes = nonNegativeInteger(inputs.stabilizedAvailableMemoryBytes);
|
|
14
|
+
const reserveMemoryBytes = nonNegativeInteger(inputs.reserveMemoryBytes);
|
|
15
|
+
const committedMemoryBytes = nonNegativeInteger(inputs.committedMemoryBytes);
|
|
16
|
+
const pendingCommitmentMemoryBytes = Math.min(committedMemoryBytes, nonNegativeInteger(inputs.pendingCommitmentMemoryBytes));
|
|
17
|
+
const requestedMemoryBytes = inputs.requestedMemoryBytes == null
|
|
18
|
+
? null
|
|
19
|
+
: nonNegativeInteger(inputs.requestedMemoryBytes);
|
|
20
|
+
const availableForAdmissionBytes = Math.max(0, Math.min(stabilizedAvailableMemoryBytes - reserveMemoryBytes - pendingCommitmentMemoryBytes, totalMemoryBytes - reserveMemoryBytes - committedMemoryBytes));
|
|
21
|
+
const shortfallMemoryBytes = requestedMemoryBytes === null
|
|
22
|
+
? null
|
|
23
|
+
: Math.max(0, requestedMemoryBytes - availableForAdmissionBytes);
|
|
24
|
+
return {
|
|
25
|
+
policyVersion: memoryAdmissionPolicyVersion,
|
|
26
|
+
sampledAt: inputs.sampledAt,
|
|
27
|
+
totalMemoryBytes,
|
|
28
|
+
rawAvailableMemoryBytes,
|
|
29
|
+
stabilizedAvailableMemoryBytes,
|
|
30
|
+
reclaimableMemoryBytes: null,
|
|
31
|
+
reclaimableMemorySource: "not_sampled_separately",
|
|
32
|
+
controlPlaneMemoryBytes: nonNegativeInteger(inputs.controlPlaneMemoryBytes),
|
|
33
|
+
reserveMemoryBytes,
|
|
34
|
+
committedMemoryBytes,
|
|
35
|
+
pendingCommitmentMemoryBytes,
|
|
36
|
+
availableForAdmissionBytes,
|
|
37
|
+
requestedMemoryBytes,
|
|
38
|
+
shortfallMemoryBytes,
|
|
39
|
+
decision: requestedMemoryBytes === null
|
|
40
|
+
? "snapshot"
|
|
41
|
+
: shortfallMemoryBytes === 0 ? "admitted" : "denied",
|
|
42
|
+
equation: memoryAdmissionEquation,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Derives a host safety reserve from the capacity domain it protects. An
|
|
47
|
+
* explicit operator value remains authoritative; otherwise the reserve scales
|
|
48
|
+
* with the sampled total inside conservative floors and ceilings.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveAdaptiveMemoryReserveBytes(totalMemoryBytes, options = {}) {
|
|
51
|
+
const explicit = positiveInteger(options.explicitBytes);
|
|
52
|
+
if (explicit !== null)
|
|
53
|
+
return explicit;
|
|
54
|
+
const total = Math.max(0, finiteInteger(totalMemoryBytes));
|
|
55
|
+
const floor = positiveInteger(options.floorBytes) ?? GiB;
|
|
56
|
+
const ceiling = Math.max(floor, positiveInteger(options.ceilingBytes) ?? 4 * GiB);
|
|
57
|
+
const percent = positiveNumber(options.percent) ?? 12.5;
|
|
58
|
+
const proportional = Math.floor(total * Math.min(50, percent) / 100);
|
|
59
|
+
return Math.max(floor, Math.min(ceiling, proportional));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Applies pressure immediately while requiring durable recovery samples before
|
|
63
|
+
* increasing advertised capacity. This prevents one GC/cache sample from
|
|
64
|
+
* waking queued work and then immediately withdrawing the same capacity.
|
|
65
|
+
*/
|
|
66
|
+
export class RecoveryStabilizedMemory {
|
|
67
|
+
requiredRecoverySamples;
|
|
68
|
+
minimumRecoverySampleIntervalMs;
|
|
69
|
+
stableBytes = null;
|
|
70
|
+
recoveryFloorBytes = null;
|
|
71
|
+
recoverySamples = 0;
|
|
72
|
+
lastRecoverySampleAt = null;
|
|
73
|
+
constructor(requiredRecoverySamples = 2, minimumRecoverySampleIntervalMs = 1_000) {
|
|
74
|
+
this.requiredRecoverySamples = requiredRecoverySamples;
|
|
75
|
+
this.minimumRecoverySampleIntervalMs = minimumRecoverySampleIntervalMs;
|
|
76
|
+
}
|
|
77
|
+
observe(rawAvailableBytes, sampledAt = Date.now()) {
|
|
78
|
+
const observed = Math.max(0, finiteInteger(rawAvailableBytes));
|
|
79
|
+
if (this.stableBytes === null) {
|
|
80
|
+
this.stableBytes = observed;
|
|
81
|
+
return observed;
|
|
82
|
+
}
|
|
83
|
+
if (observed <= this.stableBytes) {
|
|
84
|
+
this.stableBytes = observed;
|
|
85
|
+
this.resetRecovery();
|
|
86
|
+
return observed;
|
|
87
|
+
}
|
|
88
|
+
this.recoveryFloorBytes = this.recoveryFloorBytes === null
|
|
89
|
+
? observed
|
|
90
|
+
: Math.min(this.recoveryFloorBytes, observed);
|
|
91
|
+
if (this.lastRecoverySampleAt !== null
|
|
92
|
+
&& sampledAt - this.lastRecoverySampleAt < Math.max(0, this.minimumRecoverySampleIntervalMs)) {
|
|
93
|
+
return this.stableBytes;
|
|
94
|
+
}
|
|
95
|
+
this.recoverySamples += 1;
|
|
96
|
+
this.lastRecoverySampleAt = sampledAt;
|
|
97
|
+
if (this.recoverySamples < Math.max(1, this.requiredRecoverySamples)) {
|
|
98
|
+
return this.stableBytes;
|
|
99
|
+
}
|
|
100
|
+
this.stableBytes = this.recoveryFloorBytes ?? this.stableBytes;
|
|
101
|
+
this.resetRecovery();
|
|
102
|
+
return this.stableBytes;
|
|
103
|
+
}
|
|
104
|
+
resetRecovery() {
|
|
105
|
+
this.recoveryFloorBytes = null;
|
|
106
|
+
this.recoverySamples = 0;
|
|
107
|
+
this.lastRecoverySampleAt = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function finiteInteger(value) {
|
|
111
|
+
return Number.isFinite(value) ? Math.floor(value) : 0;
|
|
112
|
+
}
|
|
113
|
+
function positiveInteger(value) {
|
|
114
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : null;
|
|
115
|
+
}
|
|
116
|
+
function positiveNumber(value) {
|
|
117
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
118
|
+
}
|
|
119
|
+
function nonNegativeInteger(value) {
|
|
120
|
+
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
121
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { HostPortBroker, type HostPortLaunch, type HostPortLeaseOwner, type HostPortReservation } from "./host-port-broker.js";
|
|
2
|
+
import { type HostRuntimePortAdapter, type HostRuntimePortConfig } from "./host-runtime-contract.js";
|
|
3
|
+
export type HostListenerOwnership = "unbound" | "owned" | "foreign";
|
|
4
|
+
export interface NodeRuntimeCommand {
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly args: readonly string[];
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly environment?: Readonly<Record<string, string>>;
|
|
9
|
+
}
|
|
10
|
+
export interface NodeRuntimeLaunchRequest extends NodeRuntimeCommand {
|
|
11
|
+
readonly framework: HostRuntimePortAdapter;
|
|
12
|
+
readonly port: HostPortReservation;
|
|
13
|
+
readonly baseUrl: string;
|
|
14
|
+
}
|
|
15
|
+
export interface NodeRuntimeCommandLauncher<T> {
|
|
16
|
+
readonly launch: (request: NodeRuntimeLaunchRequest, signal?: AbortSignal) => Promise<T>;
|
|
17
|
+
readonly inspectListener: (value: T, request: NodeRuntimeLaunchRequest, signal?: AbortSignal) => Promise<HostListenerOwnership>;
|
|
18
|
+
readonly readinessDiagnostics?: (value: T) => Promise<NodeRuntimeReadinessDiagnostics>;
|
|
19
|
+
readonly stop: (value: T) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface NodeRuntimeReadinessDiagnostics {
|
|
22
|
+
readonly exited: boolean;
|
|
23
|
+
readonly exitCode?: number;
|
|
24
|
+
readonly stdoutTail?: string;
|
|
25
|
+
readonly stderrTail?: string;
|
|
26
|
+
readonly outputRevision?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface NodeHostRuntimeAdapterOptions {
|
|
29
|
+
readonly readinessTimeoutMs?: number;
|
|
30
|
+
readonly hardReadinessTimeoutMs?: number;
|
|
31
|
+
readonly readinessPollIntervalMs?: number;
|
|
32
|
+
readonly httpProbeTimeoutMs?: number;
|
|
33
|
+
readonly probeHttp?: (url: string, timeoutMs: number, signal?: AbortSignal) => Promise<boolean>;
|
|
34
|
+
}
|
|
35
|
+
export interface NodeRuntimeAdapterLaunch<T> extends HostPortLaunch<T> {
|
|
36
|
+
readonly request: NodeRuntimeLaunchRequest;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Launches an explicitly configured HTTP project command beneath the caller's
|
|
40
|
+
* attempt supervisor. It never shells a command and never promotes a project
|
|
41
|
+
* by framework detection alone.
|
|
42
|
+
*/
|
|
43
|
+
export declare class NodeHostRuntimeAdapter {
|
|
44
|
+
private readonly broker;
|
|
45
|
+
private readonly readinessTimeoutMs;
|
|
46
|
+
private readonly hardReadinessTimeoutMs;
|
|
47
|
+
private readonly readinessPollIntervalMs;
|
|
48
|
+
private readonly httpProbeTimeoutMs;
|
|
49
|
+
private readonly probeHttp;
|
|
50
|
+
constructor(broker: HostPortBroker, options?: NodeHostRuntimeAdapterOptions);
|
|
51
|
+
launch<T>(owner: HostPortLeaseOwner, portConfig: HostRuntimePortConfig, command: NodeRuntimeCommand, launcher: NodeRuntimeCommandLauncher<T>, signal?: AbortSignal): Promise<NodeRuntimeAdapterLaunch<T>>;
|
|
52
|
+
private waitUntilReady;
|
|
53
|
+
}
|
|
54
|
+
export declare function buildNodeRuntimeLaunchRequest(framework: HostRuntimePortAdapter, portConfig: HostRuntimePortConfig, command: NodeRuntimeCommand, port: HostPortReservation): NodeRuntimeLaunchRequest;
|