@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.41da3780c451

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.
Files changed (165) hide show
  1. package/README.md +88 -2
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +4 -1
  5. package/dist/bridge/bridge.service.d.ts +72 -3
  6. package/dist/bridge/bridge.service.js +1563 -152
  7. package/dist/bridge/bridge.types.d.ts +46 -1
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +1 -0
  21. package/dist/cleanup/cleanup.service.js +70 -9
  22. package/dist/cli/cli.help.js +2 -2
  23. package/dist/cli/cli.service.d.ts +14 -6
  24. package/dist/cli/cli.service.js +75 -16
  25. package/dist/cli/local-config.d.ts +1 -0
  26. package/dist/cli/local-config.js +4 -0
  27. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  28. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  29. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  30. package/dist/runtime/attempt-database-broker.js +390 -0
  31. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  32. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  33. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  34. package/dist/runtime/attempt-path-operation.js +94 -0
  35. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  36. package/dist/runtime/attempt-private-directory.js +54 -0
  37. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  38. package/dist/runtime/attempt-process-supervisor.js +70 -22
  39. package/dist/runtime/canonical-source-file.d.ts +3 -0
  40. package/dist/runtime/canonical-source-file.js +54 -0
  41. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  42. package/dist/runtime/docker-engine-probe.js +333 -0
  43. package/dist/runtime/generated-output-reset.d.ts +10 -0
  44. package/dist/runtime/generated-output-reset.js +62 -8
  45. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  46. package/dist/runtime/host-database-capability-boundary.js +62 -0
  47. package/dist/runtime/host-diagnostics.d.ts +11 -0
  48. package/dist/runtime/host-diagnostics.js +192 -0
  49. package/dist/runtime/host-execution-root.d.ts +4 -0
  50. package/dist/runtime/host-execution-root.js +39 -0
  51. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  52. package/dist/runtime/host-listener-inspector.js +121 -60
  53. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  54. package/dist/runtime/host-runtime-contract.js +168 -55
  55. package/dist/runtime/host-runtime-lease.d.ts +92 -8
  56. package/dist/runtime/host-runtime-lease.js +1141 -114
  57. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  58. package/dist/runtime/host-runtime-promotion.js +59 -12
  59. package/dist/runtime/memory-capacity.d.ts +70 -0
  60. package/dist/runtime/memory-capacity.js +123 -0
  61. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  62. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  63. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  64. package/dist/runtime/node-host-runtime-adapter.js +29 -11
  65. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  66. package/dist/runtime/pinned-directory-process.js +61 -0
  67. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  68. package/dist/runtime/pinned-source-directory.js +94 -0
  69. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  70. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  71. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  72. package/dist/runtime/prepared-runtime-identity.js +218 -0
  73. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  74. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  75. package/dist/runtime/runtime-isolation-lease.js +4 -1
  76. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  77. package/dist/runtime/workspace-resource-estimator.js +174 -0
  78. package/dist/sandbox/preflight.d.ts +31 -1
  79. package/dist/sandbox/preflight.js +237 -51
  80. package/dist/sandbox/repository-lock.d.ts +2 -1
  81. package/dist/sandbox/repository-lock.js +19 -9
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +137 -107
  84. package/dist/sandbox/sandbox.types.d.ts +7 -0
  85. package/dist/sandbox/workspace.service.js +2 -0
  86. package/dist/session/managed-attempt-tools.d.ts +17 -1
  87. package/dist/session/managed-attempt-tools.js +489 -34
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +106 -0
  90. package/dist/session/managed-build-receipt.d.ts +21 -0
  91. package/dist/session/managed-build-receipt.js +337 -0
  92. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  93. package/dist/session/managed-verification-timeouts.js +17 -0
  94. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  95. package/dist/session/phase-completion.artifacts.js +224 -22
  96. package/dist/session/phase-completion.d.ts +11 -1
  97. package/dist/session/phase-completion.helpers.d.ts +5 -0
  98. package/dist/session/phase-completion.helpers.js +30 -7
  99. package/dist/session/phase-completion.js +14 -3
  100. package/dist/session/phase-completion.prompts.js +17 -18
  101. package/dist/session/phase-completion.runtime.d.ts +3 -1
  102. package/dist/session/phase-completion.runtime.js +126 -17
  103. package/dist/session/phase-completion.schemas.js +49 -2
  104. package/dist/session/phase-completion.validation.d.ts +1 -0
  105. package/dist/session/phase-completion.validation.js +111 -21
  106. package/dist/session/pre-publication-validation.d.ts +75 -0
  107. package/dist/session/pre-publication-validation.js +661 -0
  108. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  109. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  110. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  111. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  112. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  113. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  114. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  115. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  116. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  117. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  118. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  119. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  120. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  121. package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +773 -69
  125. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  126. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  127. package/dist/session/provider-cli/provider-output-state.js +361 -17
  128. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  129. package/dist/session/provider-cli/provider-usage.js +28 -5
  130. package/dist/session/session-redaction.js +434 -9
  131. package/dist/session/session.service.js +145 -23
  132. package/dist/session/session.types.d.ts +147 -2
  133. package/dist/session/session.types.js +587 -7
  134. package/dist/session/unreferenced-exports.d.ts +12 -0
  135. package/dist/session/unreferenced-exports.js +59 -0
  136. package/dist/shared/format/format-bytes.d.ts +1 -0
  137. package/dist/shared/format/format-bytes.js +9 -0
  138. package/dist/shared/git/git.js +2 -1
  139. package/dist/shared/path/path.d.ts +1 -0
  140. package/dist/shared/path/path.js +18 -0
  141. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  142. package/dist/shared/process/dotnet-toolchain.js +307 -0
  143. package/dist/shared/process/host-process-recovery.js +3 -2
  144. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  145. package/dist/shared/process/host-process-supervisor.js +60 -11
  146. package/dist/shared/process/process.d.ts +2 -0
  147. package/dist/shared/process/process.js +12 -7
  148. package/dist/shared/provider-capabilities.d.ts +3 -0
  149. package/dist/shared/provider-capabilities.js +85 -0
  150. package/dist/shared/redaction/redaction.d.ts +1 -0
  151. package/dist/shared/redaction/redaction.js +39 -3
  152. package/dist/shared/time/monotonic.d.ts +1 -0
  153. package/dist/shared/time/monotonic.js +8 -0
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/dist/worktree/dependency-warmer.d.ts +41 -9
  157. package/dist/worktree/dependency-warmer.js +497 -22
  158. package/dist/worktree/index.d.ts +1 -1
  159. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  160. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  161. package/dist/worktree/worktree.service.d.ts +7 -0
  162. package/dist/worktree/worktree.service.js +253 -21
  163. package/dist/worktree/worktree.types.d.ts +7 -1
  164. package/package.json +7 -5
  165. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -3,19 +3,28 @@ export interface HostParallelPromotionRecord {
3
3
  readonly schemaVersion: 1;
4
4
  readonly repoId: string;
5
5
  readonly runtimeContractFingerprint: string;
6
+ readonly inputCommitSha?: string | null;
7
+ readonly sourceFingerprint?: string;
6
8
  readonly runtimeContractSource?: "project" | "derived";
7
9
  readonly observedAt: string;
8
10
  readonly observedDispatchAttemptId: string;
9
11
  readonly verifiedPortNames: readonly string[];
10
12
  }
13
+ export interface HostParallelProofIdentity {
14
+ readonly runtimeContractFingerprint: string;
15
+ readonly inputCommitSha: string;
16
+ readonly sourceFingerprint?: string;
17
+ }
11
18
  /** Persists only observed, contract-bound host parallel-safety evidence. */
12
19
  export declare class HostParallelPromotionStore {
13
20
  private readonly stateRoot;
14
21
  constructor(stateRoot: string);
15
- load(repoId: string): Promise<HostParallelPromotionRecord | null>;
22
+ load(repoId: string, identity?: HostParallelProofIdentity): Promise<HostParallelPromotionRecord | null>;
16
23
  list(): Promise<readonly HostParallelPromotionRecord[]>;
17
- observe(repoId: string, runtimeContract: LoadedHostRuntimeContract, dispatchAttemptId: string, verifiedPortNames: readonly string[]): Promise<HostParallelPromotionRecord>;
18
- invalidate(repoId: string): Promise<void>;
24
+ observe(repoId: string, runtimeContract: LoadedHostRuntimeContract, dispatchAttemptId: string, verifiedPortNames: readonly string[], inputCommitSha?: string, sourceFingerprint?: string): Promise<HostParallelPromotionRecord>;
25
+ private writeRecord;
26
+ private pruneVersions;
27
+ invalidate(repoId: string, scope?: "all" | "discovery"): Promise<void>;
19
28
  private recordPath;
20
29
  }
21
30
  export declare function hostRuntimeContractFingerprint(runtimeContract: LoadedHostRuntimeContract): string;
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  /** Persists only observed, contract-bound host parallel-safety evidence. */
@@ -7,11 +7,12 @@ export class HostParallelPromotionStore {
7
7
  constructor(stateRoot) {
8
8
  this.stateRoot = stateRoot;
9
9
  }
10
- async load(repoId) {
10
+ async load(repoId, identity) {
11
11
  const normalizedRepoId = normalizeRepoId(repoId);
12
12
  try {
13
- const parsed = JSON.parse(await fs.readFile(this.recordPath(normalizedRepoId), "utf8"));
14
- return validRecord(parsed, normalizedRepoId) ? parsed : null;
13
+ const parsed = JSON.parse(await fs.readFile(this.recordPath(normalizedRepoId, identity), "utf8"));
14
+ return validRecord(parsed, normalizedRepoId)
15
+ && (!identity || matchesIdentity(parsed, identity)) ? parsed : null;
15
16
  }
16
17
  catch (error) {
17
18
  if (error.code === "ENOENT" || error instanceof SyntaxError)
@@ -45,7 +46,7 @@ export class HostParallelPromotionStore {
45
46
  }
46
47
  return records.sort((left, right) => left.repoId.localeCompare(right.repoId));
47
48
  }
48
- async observe(repoId, runtimeContract, dispatchAttemptId, verifiedPortNames) {
49
+ async observe(repoId, runtimeContract, dispatchAttemptId, verifiedPortNames, inputCommitSha, sourceFingerprint) {
49
50
  if (!hostRuntimeParallelEligible(runtimeContract)) {
50
51
  throw new Error("host_parallel_promotion_ineligible: the runtime contract is not eligible for parallel host execution.");
51
52
  }
@@ -60,15 +61,26 @@ export class HostParallelPromotionStore {
60
61
  schemaVersion: 1,
61
62
  repoId: normalizedRepoId,
62
63
  runtimeContractFingerprint: hostRuntimeContractFingerprint(runtimeContract),
64
+ inputCommitSha: inputCommitSha ?? null,
65
+ ...(sourceFingerprint ? { sourceFingerprint } : {}),
63
66
  runtimeContractSource: runtimeContract.source === "project" ? "project" : "derived",
64
67
  observedAt: new Date().toISOString(),
65
68
  observedDispatchAttemptId: normalizedUuid(dispatchAttemptId, "dispatch attempt"),
66
69
  verifiedPortNames: observedPorts,
67
70
  };
68
- const target = this.recordPath(normalizedRepoId);
71
+ if (inputCommitSha) {
72
+ await this.writeRecord(this.recordPath(normalizedRepoId, {
73
+ runtimeContractFingerprint: record.runtimeContractFingerprint, inputCommitSha, sourceFingerprint,
74
+ }), record);
75
+ }
76
+ await this.writeRecord(this.recordPath(normalizedRepoId), record);
77
+ await this.pruneVersions(normalizedRepoId);
78
+ return record;
79
+ }
80
+ async writeRecord(target, record) {
69
81
  const root = path.dirname(target);
70
82
  await ensurePrivateDirectory(root);
71
- const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
83
+ const temporary = `${target}.${randomUUID()}.tmp`;
72
84
  try {
73
85
  const handle = await fs.open(temporary, "wx", 0o600);
74
86
  try {
@@ -79,21 +91,56 @@ export class HostParallelPromotionStore {
79
91
  await handle.close();
80
92
  }
81
93
  await fs.rename(temporary, target);
82
- return record;
83
94
  }
84
95
  catch (error) {
85
96
  await fs.rm(temporary, { force: true }).catch(() => undefined);
86
97
  throw error;
87
98
  }
88
99
  }
89
- async invalidate(repoId) {
90
- await fs.rm(this.recordPath(normalizeRepoId(repoId)), { force: true });
100
+ async pruneVersions(repoId) {
101
+ const target = this.recordPath(repoId);
102
+ const root = path.dirname(target);
103
+ const prefix = path.basename(target, ".json") + ".";
104
+ const versions = (await fs.readdir(root)).filter(name => name.startsWith(prefix)
105
+ && /^[0-9a-f]{64}\.[0-9a-f]{64}\.json$/u.test(name));
106
+ if (versions.length <= 128)
107
+ return;
108
+ const dated = await Promise.all(versions.map(async (name) => ({ name,
109
+ at: await fs.stat(path.join(root, name)).then(stat => stat.mtimeMs, () => Number.MAX_SAFE_INTEGER) })));
110
+ for (const entry of dated.sort((a, b) => b.at - a.at).slice(128)) {
111
+ await fs.rm(path.join(root, entry.name), { force: true });
112
+ }
113
+ }
114
+ async invalidate(repoId, scope = "all") {
115
+ const target = this.recordPath(normalizeRepoId(repoId));
116
+ await fs.rm(target, { force: true });
117
+ // Background/default-branch discovery must not erase proofs bound to other
118
+ // prepared sources. Actual containment/cleanup degradation invalidates all.
119
+ if (scope === "discovery")
120
+ return;
121
+ const prefix = path.basename(target, ".json") + ".";
122
+ const entries = await fs.readdir(path.dirname(target)).catch((error) => {
123
+ if (error.code === "ENOENT")
124
+ return [];
125
+ throw error;
126
+ });
127
+ for (const entry of entries.filter(name => name.startsWith(prefix) && /^[0-9a-f]{64}\.[0-9a-f]{64}\.json$/u.test(name))) {
128
+ await fs.rm(path.join(path.dirname(target), entry), { force: true });
129
+ }
91
130
  }
92
- recordPath(normalizedRepoId) {
131
+ recordPath(normalizedRepoId, identity) {
93
132
  const key = createHash("sha256").update(normalizedRepoId).digest("hex");
94
- return path.join(this.stateRoot, "runtime-promotion", "host", `${key}.json`);
133
+ const version = identity ? `.${createHash("sha256").update(JSON.stringify([
134
+ identity.runtimeContractFingerprint, identity.inputCommitSha, identity.sourceFingerprint ?? null,
135
+ ])).digest("hex")}` : "";
136
+ return path.join(this.stateRoot, "runtime-promotion", "host", `${key}${version}.json`);
95
137
  }
96
138
  }
139
+ function matchesIdentity(record, identity) {
140
+ return record.runtimeContractFingerprint === identity.runtimeContractFingerprint
141
+ && record.inputCommitSha === identity.inputCommitSha
142
+ && record.sourceFingerprint === identity.sourceFingerprint;
143
+ }
97
144
  export function hostRuntimeContractFingerprint(runtimeContract) {
98
145
  const canonical = JSON.stringify({
99
146
  schemaVersion: runtimeContract.contract.schemaVersion,
@@ -0,0 +1,70 @@
1
+ export declare const memoryAdmissionPolicyVersion = "memory-admission-v2";
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
+ /** 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. Ambient host use must not
38
+ * also be folded into committedMemoryBytes: it is already reflected in the OS
39
+ * sample, and doing both permanently double-charges long-lived host processes.
40
+ * The total-memory ceiling independently caps Forge's own reservations.
41
+ */
42
+ export declare function buildMemoryAdmissionDiagnostics(inputs: MemoryAdmissionInputs): MemoryAdmissionDiagnostics;
43
+ export interface AdaptiveMemoryReserveOptions {
44
+ readonly explicitBytes?: number | null;
45
+ readonly floorBytes?: number | null;
46
+ readonly ceilingBytes?: number | null;
47
+ readonly percent?: number | null;
48
+ }
49
+ /**
50
+ * Derives a host safety reserve from the capacity domain it protects. An
51
+ * explicit operator value remains authoritative; otherwise the reserve scales
52
+ * with the sampled total inside conservative floors and ceilings.
53
+ */
54
+ export declare function resolveAdaptiveMemoryReserveBytes(totalMemoryBytes: number, options?: AdaptiveMemoryReserveOptions): number;
55
+ /**
56
+ * Applies pressure immediately while requiring durable recovery samples before
57
+ * increasing advertised capacity. This prevents one GC/cache sample from
58
+ * waking queued work and then immediately withdrawing the same capacity.
59
+ */
60
+ export declare class RecoveryStabilizedMemory {
61
+ private readonly requiredRecoverySamples;
62
+ private readonly minimumRecoverySampleIntervalMs;
63
+ private stableBytes;
64
+ private recoveryFloorBytes;
65
+ private recoverySamples;
66
+ private lastRecoverySampleAt;
67
+ constructor(requiredRecoverySamples?: number, minimumRecoverySampleIntervalMs?: number);
68
+ observe(rawAvailableBytes: number, sampledAt?: number): number;
69
+ private resetRecovery;
70
+ }
@@ -0,0 +1,123 @@
1
+ const GiB = 1024 ** 3;
2
+ export const memoryAdmissionPolicyVersion = "memory-admission-v2";
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. Ambient host use must not
8
+ * also be folded into committedMemoryBytes: it is already reflected in the OS
9
+ * sample, and doing both permanently double-charges long-lived host processes.
10
+ * The total-memory ceiling independently caps Forge's own reservations.
11
+ */
12
+ export function buildMemoryAdmissionDiagnostics(inputs) {
13
+ const totalMemoryBytes = nonNegativeInteger(inputs.totalMemoryBytes);
14
+ const rawAvailableMemoryBytes = nonNegativeInteger(inputs.rawAvailableMemoryBytes);
15
+ const stabilizedAvailableMemoryBytes = nonNegativeInteger(inputs.stabilizedAvailableMemoryBytes);
16
+ const reserveMemoryBytes = nonNegativeInteger(inputs.reserveMemoryBytes);
17
+ const committedMemoryBytes = nonNegativeInteger(inputs.committedMemoryBytes);
18
+ const pendingCommitmentMemoryBytes = Math.min(committedMemoryBytes, nonNegativeInteger(inputs.pendingCommitmentMemoryBytes));
19
+ const requestedMemoryBytes = inputs.requestedMemoryBytes == null
20
+ ? null
21
+ : nonNegativeInteger(inputs.requestedMemoryBytes);
22
+ const availableForAdmissionBytes = Math.max(0, Math.min(stabilizedAvailableMemoryBytes - reserveMemoryBytes - pendingCommitmentMemoryBytes, totalMemoryBytes - reserveMemoryBytes - committedMemoryBytes));
23
+ const shortfallMemoryBytes = requestedMemoryBytes === null
24
+ ? null
25
+ : Math.max(0, requestedMemoryBytes - availableForAdmissionBytes);
26
+ return {
27
+ policyVersion: memoryAdmissionPolicyVersion,
28
+ sampledAt: inputs.sampledAt,
29
+ totalMemoryBytes,
30
+ rawAvailableMemoryBytes,
31
+ stabilizedAvailableMemoryBytes,
32
+ reclaimableMemoryBytes: null,
33
+ reclaimableMemorySource: "not_sampled_separately",
34
+ controlPlaneMemoryBytes: nonNegativeInteger(inputs.controlPlaneMemoryBytes),
35
+ reserveMemoryBytes,
36
+ committedMemoryBytes,
37
+ pendingCommitmentMemoryBytes,
38
+ availableForAdmissionBytes,
39
+ requestedMemoryBytes,
40
+ shortfallMemoryBytes,
41
+ decision: requestedMemoryBytes === null
42
+ ? "snapshot"
43
+ : shortfallMemoryBytes === 0 ? "admitted" : "denied",
44
+ equation: memoryAdmissionEquation,
45
+ };
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 function resolveAdaptiveMemoryReserveBytes(totalMemoryBytes, options = {}) {
53
+ const explicit = positiveInteger(options.explicitBytes);
54
+ if (explicit !== null)
55
+ return explicit;
56
+ const total = Math.max(0, finiteInteger(totalMemoryBytes));
57
+ const floor = positiveInteger(options.floorBytes) ?? GiB;
58
+ const ceiling = Math.max(floor, positiveInteger(options.ceilingBytes) ?? 4 * GiB);
59
+ const percent = positiveNumber(options.percent) ?? 12.5;
60
+ const proportional = Math.floor(total * Math.min(50, percent) / 100);
61
+ return Math.max(floor, Math.min(ceiling, proportional));
62
+ }
63
+ /**
64
+ * Applies pressure immediately while requiring durable recovery samples before
65
+ * increasing advertised capacity. This prevents one GC/cache sample from
66
+ * waking queued work and then immediately withdrawing the same capacity.
67
+ */
68
+ export class RecoveryStabilizedMemory {
69
+ requiredRecoverySamples;
70
+ minimumRecoverySampleIntervalMs;
71
+ stableBytes = null;
72
+ recoveryFloorBytes = null;
73
+ recoverySamples = 0;
74
+ lastRecoverySampleAt = null;
75
+ constructor(requiredRecoverySamples = 2, minimumRecoverySampleIntervalMs = 1_000) {
76
+ this.requiredRecoverySamples = requiredRecoverySamples;
77
+ this.minimumRecoverySampleIntervalMs = minimumRecoverySampleIntervalMs;
78
+ }
79
+ observe(rawAvailableBytes, sampledAt = Date.now()) {
80
+ const observed = Math.max(0, finiteInteger(rawAvailableBytes));
81
+ if (this.stableBytes === null) {
82
+ this.stableBytes = observed;
83
+ return observed;
84
+ }
85
+ if (observed <= this.stableBytes) {
86
+ this.stableBytes = observed;
87
+ this.resetRecovery();
88
+ return observed;
89
+ }
90
+ this.recoveryFloorBytes = this.recoveryFloorBytes === null
91
+ ? observed
92
+ : Math.min(this.recoveryFloorBytes, observed);
93
+ if (this.lastRecoverySampleAt !== null
94
+ && sampledAt - this.lastRecoverySampleAt < Math.max(0, this.minimumRecoverySampleIntervalMs)) {
95
+ return this.stableBytes;
96
+ }
97
+ this.recoverySamples += 1;
98
+ this.lastRecoverySampleAt = sampledAt;
99
+ if (this.recoverySamples < Math.max(1, this.requiredRecoverySamples)) {
100
+ return this.stableBytes;
101
+ }
102
+ this.stableBytes = this.recoveryFloorBytes ?? this.stableBytes;
103
+ this.resetRecovery();
104
+ return this.stableBytes;
105
+ }
106
+ resetRecovery() {
107
+ this.recoveryFloorBytes = null;
108
+ this.recoverySamples = 0;
109
+ this.lastRecoverySampleAt = null;
110
+ }
111
+ }
112
+ function finiteInteger(value) {
113
+ return Number.isFinite(value) ? Math.floor(value) : 0;
114
+ }
115
+ function positiveInteger(value) {
116
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : null;
117
+ }
118
+ function positiveNumber(value) {
119
+ return Number.isFinite(value) && value > 0 ? value : null;
120
+ }
121
+ function nonNegativeInteger(value) {
122
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
123
+ }
@@ -0,0 +1,25 @@
1
+ export declare const nativeTestPostgresFallbackConfigEnvironment = "FORGE_NATIVE_TEST_POSTGRES_CONFIG";
2
+ export declare const nativeTestPostgresFallbackFileName = "native-test-postgres.json";
3
+ export declare const nativeTestPostgresFallbackDatabaseMode = "native_fallback";
4
+ /**
5
+ * A dedicated, operator-provisioned PostgreSQL cluster that runner-authored .NET
6
+ * test tiers may use only while the Docker engine is unavailable. The cluster
7
+ * must hold nothing but disposable test databases, so its capability carries no
8
+ * more authority than the attempt-private container it replaces.
9
+ */
10
+ export interface NativeTestPostgresFallback {
11
+ readonly connectionString: string;
12
+ readonly endpoint: string;
13
+ }
14
+ export declare class NativeTestPostgresFallbackError extends Error {
15
+ readonly code = "native_test_postgres_fallback_invalid";
16
+ constructor(detail: string);
17
+ }
18
+ export declare function resolveNativeTestPostgresFallbackPath(): string;
19
+ /** The operator's fallback file when one has been provisioned on this host. */
20
+ export declare function configuredNativeTestPostgresFallbackPath(): string | null;
21
+ /** Returns null when no fallback is configured; throws when one is configured unsafely. */
22
+ export declare function readNativeTestPostgresFallback(configPath?: string): NativeTestPostgresFallback | null;
23
+ export declare function parseNativeTestPostgresFallback(connectionString: string): NativeTestPostgresFallback;
24
+ /** Environment for one runner-authored .NET test process using the fallback. */
25
+ export declare function nativeTestPostgresFallbackEnvironment(fallback: NativeTestPostgresFallback): Readonly<Record<string, string>>;
@@ -0,0 +1,91 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveLocalConfigPath } from "../cli/local-config.js";
4
+ import { hostRuntimeLegacyTestPostgresEnvironment, hostRuntimeTestPostgresAdminEnvironment, } from "./host-runtime-contract.js";
5
+ export const nativeTestPostgresFallbackConfigEnvironment = "FORGE_NATIVE_TEST_POSTGRES_CONFIG";
6
+ export const nativeTestPostgresFallbackFileName = "native-test-postgres.json";
7
+ export const nativeTestPostgresFallbackDatabaseMode = "native_fallback";
8
+ const defaultPostgresPort = 5432;
9
+ const loopbackHosts = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
10
+ export class NativeTestPostgresFallbackError extends Error {
11
+ code = "native_test_postgres_fallback_invalid";
12
+ constructor(detail) {
13
+ super(`native_test_postgres_fallback_invalid: ${detail}`);
14
+ this.name = "NativeTestPostgresFallbackError";
15
+ }
16
+ }
17
+ export function resolveNativeTestPostgresFallbackPath() {
18
+ const configured = process.env[nativeTestPostgresFallbackConfigEnvironment]?.trim();
19
+ return configured
20
+ ? path.resolve(configured)
21
+ : path.join(path.dirname(resolveLocalConfigPath()), nativeTestPostgresFallbackFileName);
22
+ }
23
+ /** The operator's fallback file when one has been provisioned on this host. */
24
+ export function configuredNativeTestPostgresFallbackPath() {
25
+ const configPath = resolveNativeTestPostgresFallbackPath();
26
+ return fs.existsSync(configPath) ? configPath : null;
27
+ }
28
+ /** Returns null when no fallback is configured; throws when one is configured unsafely. */
29
+ export function readNativeTestPostgresFallback(configPath = resolveNativeTestPostgresFallbackPath()) {
30
+ let raw;
31
+ try {
32
+ raw = fs.readFileSync(configPath, "utf8");
33
+ }
34
+ catch (error) {
35
+ if (error.code === "ENOENT")
36
+ return null;
37
+ throw error;
38
+ }
39
+ let document;
40
+ try {
41
+ document = JSON.parse(raw);
42
+ }
43
+ catch {
44
+ throw new NativeTestPostgresFallbackError(`${configPath} is not valid JSON.`);
45
+ }
46
+ if (document.schemaVersion !== 1 || typeof document.connectionString !== "string") {
47
+ throw new NativeTestPostgresFallbackError(`${configPath} must contain schemaVersion 1 and a connectionString.`);
48
+ }
49
+ return parseNativeTestPostgresFallback(document.connectionString);
50
+ }
51
+ export function parseNativeTestPostgresFallback(connectionString) {
52
+ const fields = new Map();
53
+ for (const segment of connectionString.split(";")) {
54
+ const separator = segment.indexOf("=");
55
+ if (separator <= 0)
56
+ continue;
57
+ fields.set(segment.slice(0, separator).trim().toLowerCase().replaceAll(" ", ""), segment.slice(separator + 1).trim());
58
+ }
59
+ const host = (fields.get("host") ?? fields.get("server") ?? "").toLowerCase();
60
+ if (!loopbackHosts.has(host)) {
61
+ throw new NativeTestPostgresFallbackError("the fallback must use a single loopback host.");
62
+ }
63
+ const database = (fields.get("database") ?? fields.get("db") ?? fields.get("initialcatalog") ?? "").toLowerCase();
64
+ if (database !== "postgres") {
65
+ throw new NativeTestPostgresFallbackError("the fallback must connect to the 'postgres' administrative database.");
66
+ }
67
+ if (fields.get("searchpath")) {
68
+ throw new NativeTestPostgresFallbackError("the fallback must not set a search path.");
69
+ }
70
+ const port = Number.parseInt(fields.get("port") ?? "", 10);
71
+ if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
72
+ throw new NativeTestPostgresFallbackError("the fallback must name an explicit port.");
73
+ }
74
+ if (port === defaultPostgresPort) {
75
+ // The default port is where developer databases live; a dedicated test
76
+ // cluster must be provisioned on its own port.
77
+ throw new NativeTestPostgresFallbackError(`the fallback must not use the default PostgreSQL port ${defaultPostgresPort}; provision a dedicated test cluster.`);
78
+ }
79
+ if (!(fields.get("username") ?? fields.get("userid") ?? fields.get("user"))) {
80
+ throw new NativeTestPostgresFallbackError("the fallback must name a dedicated test role.");
81
+ }
82
+ return { connectionString: connectionString.trim(), endpoint: `${host}:${port}` };
83
+ }
84
+ /** Environment for one runner-authored .NET test process using the fallback. */
85
+ export function nativeTestPostgresFallbackEnvironment(fallback) {
86
+ return {
87
+ [hostRuntimeTestPostgresAdminEnvironment]: fallback.connectionString,
88
+ [hostRuntimeLegacyTestPostgresEnvironment]: fallback.connectionString,
89
+ FORGE_MANAGED_VERIFICATION_DATABASE_MODE: nativeTestPostgresFallbackDatabaseMode,
90
+ };
91
+ }
@@ -23,9 +23,11 @@ export interface NodeRuntimeReadinessDiagnostics {
23
23
  readonly exitCode?: number;
24
24
  readonly stdoutTail?: string;
25
25
  readonly stderrTail?: string;
26
+ readonly outputRevision?: number;
26
27
  }
27
28
  export interface NodeHostRuntimeAdapterOptions {
28
29
  readonly readinessTimeoutMs?: number;
30
+ readonly hardReadinessTimeoutMs?: number;
29
31
  readonly readinessPollIntervalMs?: number;
30
32
  readonly httpProbeTimeoutMs?: number;
31
33
  readonly probeHttp?: (url: string, timeoutMs: number, signal?: AbortSignal) => Promise<boolean>;
@@ -41,6 +43,7 @@ export interface NodeRuntimeAdapterLaunch<T> extends HostPortLaunch<T> {
41
43
  export declare class NodeHostRuntimeAdapter {
42
44
  private readonly broker;
43
45
  private readonly readinessTimeoutMs;
46
+ private readonly hardReadinessTimeoutMs;
44
47
  private readonly readinessPollIntervalMs;
45
48
  private readonly httpProbeTimeoutMs;
46
49
  private readonly probeHttp;
@@ -1,5 +1,8 @@
1
1
  import path from "node:path";
2
+ import { ForgeFailureError } from "../session/session.types.js";
3
+ import { withAspNetPrebuiltRunArguments } from "./aspnet-runtime-prebuild.js";
2
4
  import { hostPortEnvironment, } from "./host-runtime-contract.js";
5
+ import { monotonicNowMs } from "../shared/time/monotonic.js";
3
6
  /**
4
7
  * Launches an explicitly configured HTTP project command beneath the caller's
5
8
  * attempt supervisor. It never shells a command and never promotes a project
@@ -8,12 +11,14 @@ import { hostPortEnvironment, } from "./host-runtime-contract.js";
8
11
  export class NodeHostRuntimeAdapter {
9
12
  broker;
10
13
  readinessTimeoutMs;
14
+ hardReadinessTimeoutMs;
11
15
  readinessPollIntervalMs;
12
16
  httpProbeTimeoutMs;
13
17
  probeHttp;
14
18
  constructor(broker, options = {}) {
15
19
  this.broker = broker;
16
20
  this.readinessTimeoutMs = boundedInteger(options.readinessTimeoutMs ?? 120_000, 100, 120_000, "readinessTimeoutMs");
21
+ this.hardReadinessTimeoutMs = boundedInteger(options.hardReadinessTimeoutMs ?? 600_000, this.readinessTimeoutMs, 900_000, "hardReadinessTimeoutMs");
17
22
  this.readinessPollIntervalMs = boundedInteger(options.readinessPollIntervalMs ?? 100, 10, 5_000, "readinessPollIntervalMs");
18
23
  this.httpProbeTimeoutMs = boundedInteger(options.httpProbeTimeoutMs ?? 1_000, 50, 10_000, "httpProbeTimeoutMs");
19
24
  this.probeHttp = options.probeHttp ?? probeHttpReadiness;
@@ -49,14 +54,21 @@ export class NodeHostRuntimeAdapter {
49
54
  return { ...launched, request: activeRequest };
50
55
  }
51
56
  async waitUntilReady(value, request, launcher, signal) {
52
- const deadline = Date.now() + this.readinessTimeoutMs;
57
+ const startedAt = monotonicNowMs();
58
+ const hardDeadline = startedAt + this.hardReadinessTimeoutMs;
59
+ let idleDeadline = startedAt + this.readinessTimeoutMs;
60
+ let outputRevision = -1;
53
61
  let observedOwnership = "unbound";
54
- while (Date.now() <= deadline) {
62
+ while (monotonicNowMs() <= idleDeadline && monotonicNowMs() <= hardDeadline) {
55
63
  if (signal?.aborted)
56
64
  throw cancellationError(signal);
57
65
  const diagnostics = await launcher.readinessDiagnostics?.(value);
58
66
  if (diagnostics?.exited) {
59
- throw new Error(readinessFailure("host_runtime_process_exited", `the supervised process exited with code ${diagnostics.exitCode ?? "unknown"} before its assigned runtime became ready`, request, diagnostics));
67
+ throw new ForgeFailureError("host_runtime_process_exited", readinessFailure("host_runtime_process_exited", `the supervised process exited with code ${diagnostics.exitCode ?? "unknown"} before its assigned runtime became ready`, request, diagnostics), "bridge");
68
+ }
69
+ if (diagnostics?.outputRevision !== undefined && diagnostics.outputRevision > outputRevision) {
70
+ outputRevision = diagnostics.outputRevision;
71
+ idleDeadline = Math.min(hardDeadline, monotonicNowMs() + this.readinessTimeoutMs);
60
72
  }
61
73
  const ownership = await launcher.inspectListener(value, request, signal);
62
74
  observedOwnership = ownership;
@@ -69,15 +81,20 @@ export class NodeHostRuntimeAdapter {
69
81
  }
70
82
  const diagnostics = await launcher.readinessDiagnostics?.(value);
71
83
  if (diagnostics?.exited) {
72
- throw new Error(readinessFailure("host_runtime_process_exited", `the supervised process exited with code ${diagnostics.exitCode ?? "unknown"} before its assigned runtime became ready`, request, diagnostics));
84
+ throw new ForgeFailureError("host_runtime_process_exited", readinessFailure("host_runtime_process_exited", `the supervised process exited with code ${diagnostics.exitCode ?? "unknown"} before its assigned runtime became ready`, request, diagnostics), "bridge");
73
85
  }
74
86
  const code = observedOwnership === "owned"
75
87
  ? "host_runtime_http_readiness_timeout"
76
88
  : "host_runtime_listener_timeout";
89
+ const reachedHardDeadline = monotonicNowMs() >= hardDeadline;
77
90
  const detail = observedOwnership === "owned"
78
- ? `the supervised process owned its assigned loopback port but HTTP readiness did not succeed within ${this.readinessTimeoutMs}ms`
79
- : `the supervised process did not bind its assigned loopback port within ${this.readinessTimeoutMs}ms`;
80
- throw new Error(readinessFailure(code, detail, request, diagnostics));
91
+ ? reachedHardDeadline
92
+ ? `the supervised process owned its assigned loopback port but HTTP readiness did not succeed within the ${this.hardReadinessTimeoutMs}ms hard limit`
93
+ : `the supervised process owned its assigned loopback port but made no readiness progress for ${this.readinessTimeoutMs}ms`
94
+ : reachedHardDeadline
95
+ ? `the supervised process did not bind its assigned loopback port within the ${this.hardReadinessTimeoutMs}ms hard limit`
96
+ : `the supervised process did not bind its assigned loopback port and made no output progress for ${this.readinessTimeoutMs}ms`;
97
+ throw new ForgeFailureError(code, readinessFailure(code, detail, request, diagnostics), "bridge");
81
98
  }
82
99
  }
83
100
  function readinessFailure(code, detail, request, diagnostics) {
@@ -195,15 +212,16 @@ function aspNetCoreArguments(command, args) {
195
212
  if (args[0]?.toLowerCase() !== "run") {
196
213
  throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires a dotnet run command.");
197
214
  }
198
- const applicationArguments = args.indexOf("--");
199
- const insertionIndex = applicationArguments < 0 ? args.length : applicationArguments;
215
+ const prebuiltArgs = withAspNetPrebuiltRunArguments(command, args);
216
+ const applicationArguments = prebuiltArgs.indexOf("--");
217
+ const insertionIndex = applicationArguments < 0 ? prebuiltArgs.length : applicationArguments;
200
218
  return [
201
- ...args.slice(0, insertionIndex),
219
+ ...prebuiltArgs.slice(0, insertionIndex),
202
220
  "--disable-build-servers",
203
221
  "-p:UseSharedCompilation=false",
204
222
  "-p:DefaultItemExcludesInProjectFolder=TestResults/**",
205
223
  "--no-launch-profile",
206
- ...args.slice(insertionIndex),
224
+ ...prebuiltArgs.slice(insertionIndex),
207
225
  ];
208
226
  }
209
227
  function validateCommand(command) {
@@ -0,0 +1,9 @@
1
+ import type { BigIntStats } from "node:fs";
2
+ export type PinnedDirectoryIdentity = Pick<BigIntStats, "dev" | "ino">;
3
+ export declare class PinnedDirectoryProcessError extends Error {
4
+ readonly failureKind: "failed" | "timeout";
5
+ readonly terminationConfirmed: boolean;
6
+ constructor(message: string, failureKind: "failed" | "timeout", options?: ErrorOptions, terminationConfirmed?: boolean);
7
+ }
8
+ /** Keep filesystem stalls off the Bridge event loop. The worker pins and checks cwd. */
9
+ export declare function runPinnedDirectoryProcess(parent: string, identity: PinnedDirectoryIdentity, worker: string, operation: object, bytes?: Buffer, maxBuffer?: number, timeoutMs?: number, terminationConfirmationMs?: number): Promise<string>;
@@ -0,0 +1,61 @@
1
+ import childProcess from "node:child_process";
2
+ export class PinnedDirectoryProcessError extends Error {
3
+ failureKind;
4
+ terminationConfirmed;
5
+ constructor(message, failureKind, options, terminationConfirmed = true) {
6
+ super(`pinned_directory_operation_${failureKind}: ${message}`, options);
7
+ this.failureKind = failureKind;
8
+ this.terminationConfirmed = terminationConfirmed;
9
+ this.name = "PinnedDirectoryProcessError";
10
+ }
11
+ }
12
+ /** Keep filesystem stalls off the Bridge event loop. The worker pins and checks cwd. */
13
+ export async function runPinnedDirectoryProcess(parent, identity, worker, operation, bytes, maxBuffer = 16 * 1024, timeoutMs = 30_000, terminationConfirmationMs = 2_000) {
14
+ const environment = { ...process.env };
15
+ delete environment.NODE_OPTIONS;
16
+ const header = `${JSON.stringify({ parent, dev: identity.dev.toString(), ino: identity.ino.toString(), ...operation })}\n`;
17
+ return await new Promise((resolve, reject) => {
18
+ let settled = false;
19
+ let timedOut = false;
20
+ const deadline = { timer: undefined };
21
+ const termination = { timer: undefined };
22
+ const child = childProcess.execFile(process.execPath, ["--input-type=commonjs", "-e", worker], {
23
+ cwd: parent, env: environment, windowsHide: true, maxBuffer,
24
+ }, (error, stdout, stderr) => {
25
+ if (settled)
26
+ return;
27
+ settled = true;
28
+ if (deadline.timer)
29
+ clearTimeout(deadline.timer);
30
+ if (termination.timer)
31
+ clearTimeout(termination.timer);
32
+ if (timedOut) {
33
+ reject(new PinnedDirectoryProcessError(`operation exceeded ${timeoutMs} ms`, "timeout", error ? { cause: error } : undefined));
34
+ return;
35
+ }
36
+ if (error) {
37
+ reject(new PinnedDirectoryProcessError(stderr.trim() || error.message, "failed", { cause: error }));
38
+ }
39
+ else
40
+ resolve(stdout);
41
+ });
42
+ deadline.timer = setTimeout(() => {
43
+ if (settled)
44
+ return;
45
+ timedOut = true;
46
+ child.kill("SIGKILL");
47
+ // Wait for execFile's callback so callers know the helper has released
48
+ // its pinned cwd before they remove attempt state. A mocked or wedged
49
+ // child still gets one bounded post-kill confirmation window.
50
+ termination.timer = setTimeout(() => {
51
+ if (settled)
52
+ return;
53
+ settled = true;
54
+ reject(new PinnedDirectoryProcessError(`operation exceeded ${timeoutMs} ms and termination was not confirmed`, "timeout", undefined, false));
55
+ }, terminationConfirmationMs);
56
+ }, timeoutMs);
57
+ child.stdin.on("error", () => undefined);
58
+ child.stdin.write(header);
59
+ child.stdin.end(bytes);
60
+ });
61
+ }