@loucompanion/forge-bridge 0.1.4-dev.1ff37038366e → 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.
- package/dist/runtime/attempt-process-supervisor.js +3 -2
- package/dist/runtime/host-diagnostics.js +4 -3
- package/dist/runtime/node-host-runtime-adapter.js +5 -4
- package/dist/sandbox/repository-lock.js +3 -2
- package/dist/sandbox/runtime.js +5 -4
- package/dist/session/managed-attempt-tools.js +4 -3
- package/dist/session/managed-build-coordinator.js +4 -3
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +11 -0
- package/dist/session/provider-cli/provider-cli.service.js +3 -2
- package/dist/shared/process/host-process-recovery.js +3 -2
- package/dist/shared/process/host-process-supervisor.js +5 -4
- package/dist/shared/time/monotonic.d.ts +1 -0
- package/dist/shared/time/monotonic.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ForgeFailureError, sanitizeFailureError } from "../session/session.types.js";
|
|
5
|
+
import { monotonicNowMs } from "../shared/time/monotonic.js";
|
|
5
6
|
const terminalChildStatuses = new Set(["exited", "failed"]);
|
|
6
7
|
const defaultMetadataReadyTimeoutMs = process.platform === "win32" ? 60_000 : 10_000;
|
|
7
8
|
/**
|
|
@@ -248,8 +249,8 @@ export class AttemptProcessSupervisor {
|
|
|
248
249
|
};
|
|
249
250
|
}
|
|
250
251
|
async observeChildReady(record) {
|
|
251
|
-
const deadline =
|
|
252
|
-
while (
|
|
252
|
+
const deadline = monotonicNowMs() + this.metadataReadyTimeoutMs;
|
|
253
|
+
while (monotonicNowMs() <= deadline) {
|
|
253
254
|
if (await fileExists(record.metadataPath)) {
|
|
254
255
|
if (record.status === "starting") {
|
|
255
256
|
record.status = "running";
|
|
@@ -6,6 +6,7 @@ import { assertCanonicalDirectory } from "./attempt-private-directory.js";
|
|
|
6
6
|
import { runAttemptArchiveOperation, runAttemptPathOperation } from "./attempt-path-operation.js";
|
|
7
7
|
import { readPinnedSourceDirectory } from "./pinned-source-directory.js";
|
|
8
8
|
import { compactHostAttemptId } from "./host-execution-root.js";
|
|
9
|
+
import { monotonicNowMs } from "../shared/time/monotonic.js";
|
|
9
10
|
const maximumEvidenceBytes = 200 * 1024 * 1024;
|
|
10
11
|
const diagnosticsRetentionDeadlineMs = 30_000;
|
|
11
12
|
const diagnosticsHelperTerminationConfirmationMs = 2_000;
|
|
@@ -21,7 +22,7 @@ export async function retainHostDiagnostics(stateRoot, attemptRoot, attemptId, e
|
|
|
21
22
|
const configuredDeadlineMs = Math.max(1, Math.min(diagnosticsRetentionDeadlineMs, Number.isFinite(requestedDeadlineMs) && (requestedDeadlineMs ?? 0) > 0
|
|
22
23
|
? Math.floor(requestedDeadlineMs)
|
|
23
24
|
: diagnosticsRetentionDeadlineMs));
|
|
24
|
-
const retentionDeadline =
|
|
25
|
+
const retentionDeadline = monotonicNowMs() + configuredDeadlineMs;
|
|
25
26
|
const identity = attemptId.replaceAll("-", "").toLowerCase();
|
|
26
27
|
const compactAttemptRoot = path.resolve(attemptOwnerRoot, "a", compactHostAttemptId(attemptId));
|
|
27
28
|
const legacyAttemptRoot = path.resolve(stateRoot, "host-attempts", identity);
|
|
@@ -155,7 +156,7 @@ function remainingHelperTiming(retentionDeadline, configuredDeadlineMs) {
|
|
|
155
156
|
return { operationTimeoutMs, terminationConfirmationMs };
|
|
156
157
|
}
|
|
157
158
|
function remainingRetentionMs(retentionDeadline, configuredDeadlineMs) {
|
|
158
|
-
const remainingMs = retentionDeadline -
|
|
159
|
+
const remainingMs = retentionDeadline - monotonicNowMs();
|
|
159
160
|
if (remainingMs <= 0)
|
|
160
161
|
throw diagnosticsDeadlineError(configuredDeadlineMs);
|
|
161
162
|
return remainingMs;
|
|
@@ -181,7 +182,7 @@ async function runWithinRetentionHelperDeadline(operation, retentionDeadline, co
|
|
|
181
182
|
return await operation();
|
|
182
183
|
}
|
|
183
184
|
catch (error) {
|
|
184
|
-
if (
|
|
185
|
+
if (monotonicNowMs() >= retentionDeadline)
|
|
185
186
|
throw diagnosticsDeadlineError(configuredDeadlineMs);
|
|
186
187
|
throw error;
|
|
187
188
|
}
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { ForgeFailureError } from "../session/session.types.js";
|
|
3
3
|
import { withAspNetPrebuiltRunArguments } from "./aspnet-runtime-prebuild.js";
|
|
4
4
|
import { hostPortEnvironment, } from "./host-runtime-contract.js";
|
|
5
|
+
import { monotonicNowMs } from "../shared/time/monotonic.js";
|
|
5
6
|
/**
|
|
6
7
|
* Launches an explicitly configured HTTP project command beneath the caller's
|
|
7
8
|
* attempt supervisor. It never shells a command and never promotes a project
|
|
@@ -53,12 +54,12 @@ export class NodeHostRuntimeAdapter {
|
|
|
53
54
|
return { ...launched, request: activeRequest };
|
|
54
55
|
}
|
|
55
56
|
async waitUntilReady(value, request, launcher, signal) {
|
|
56
|
-
const startedAt =
|
|
57
|
+
const startedAt = monotonicNowMs();
|
|
57
58
|
const hardDeadline = startedAt + this.hardReadinessTimeoutMs;
|
|
58
59
|
let idleDeadline = startedAt + this.readinessTimeoutMs;
|
|
59
60
|
let outputRevision = -1;
|
|
60
61
|
let observedOwnership = "unbound";
|
|
61
|
-
while (
|
|
62
|
+
while (monotonicNowMs() <= idleDeadline && monotonicNowMs() <= hardDeadline) {
|
|
62
63
|
if (signal?.aborted)
|
|
63
64
|
throw cancellationError(signal);
|
|
64
65
|
const diagnostics = await launcher.readinessDiagnostics?.(value);
|
|
@@ -67,7 +68,7 @@ export class NodeHostRuntimeAdapter {
|
|
|
67
68
|
}
|
|
68
69
|
if (diagnostics?.outputRevision !== undefined && diagnostics.outputRevision > outputRevision) {
|
|
69
70
|
outputRevision = diagnostics.outputRevision;
|
|
70
|
-
idleDeadline = Math.min(hardDeadline,
|
|
71
|
+
idleDeadline = Math.min(hardDeadline, monotonicNowMs() + this.readinessTimeoutMs);
|
|
71
72
|
}
|
|
72
73
|
const ownership = await launcher.inspectListener(value, request, signal);
|
|
73
74
|
observedOwnership = ownership;
|
|
@@ -85,7 +86,7 @@ export class NodeHostRuntimeAdapter {
|
|
|
85
86
|
const code = observedOwnership === "owned"
|
|
86
87
|
? "host_runtime_http_readiness_timeout"
|
|
87
88
|
: "host_runtime_listener_timeout";
|
|
88
|
-
const reachedHardDeadline =
|
|
89
|
+
const reachedHardDeadline = monotonicNowMs() >= hardDeadline;
|
|
89
90
|
const detail = observedOwnership === "owned"
|
|
90
91
|
? reachedHardDeadline
|
|
91
92
|
? `the supervised process owned its assigned loopback port but HTTP readiness did not succeed within the ${this.hardReadinessTimeoutMs}ms hard limit`
|
|
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { resolvePathIdentity } from "../shared/path/path.js";
|
|
6
6
|
import { ForgeFailureError } from "../session/session.types.js";
|
|
7
|
+
import { monotonicNowMs } from "../shared/time/monotonic.js";
|
|
7
8
|
const LOCK_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
8
9
|
const LOCK_STALE_AFTER_MS = 2 * 60_000;
|
|
9
10
|
export class RepositoryCoordinatorLock {
|
|
@@ -21,7 +22,7 @@ export class RepositoryCoordinatorLock {
|
|
|
21
22
|
const target = path.join(root, `${key}.lock`);
|
|
22
23
|
const token = randomUUID();
|
|
23
24
|
await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
24
|
-
const started =
|
|
25
|
+
const started = monotonicNowMs();
|
|
25
26
|
while (true) {
|
|
26
27
|
if (signal?.aborted) {
|
|
27
28
|
throw new ForgeFailureError("sandbox_import_cancelled", "sandbox_import_cancelled: repository coordination was cancelled.", "sandbox");
|
|
@@ -44,7 +45,7 @@ export class RepositoryCoordinatorLock {
|
|
|
44
45
|
await fs.rm(target, { force: true });
|
|
45
46
|
continue;
|
|
46
47
|
}
|
|
47
|
-
if (
|
|
48
|
+
if (monotonicNowMs() - started >= this.lockTimeoutMs) {
|
|
48
49
|
throw new ForgeFailureError("sandbox_import_unavailable", "sandbox_import_unavailable: repository coordinator lock timed out.", "sandbox", { cause: error });
|
|
49
50
|
}
|
|
50
51
|
await abortableDelay(50, signal);
|
package/dist/sandbox/runtime.js
CHANGED
|
@@ -12,6 +12,7 @@ import { RepositoryCoordinatorLock } from "./repository-lock.js";
|
|
|
12
12
|
import { redactText } from "../shared/redaction/redaction.js";
|
|
13
13
|
import { createRuntimeIsolationLease, renewRuntimeIsolationLease, transitionRuntimeIsolationLease, validRuntimeIsolationLease, withRuntimeIsolationResources, } from "../runtime/runtime-isolation-lease.js";
|
|
14
14
|
import { sandboxProviderAuthAdapterFor, sandboxProviderAuthUnavailableMessage } from "./provider-auth/codex.js";
|
|
15
|
+
import { monotonicNowMs } from "../shared/time/monotonic.js";
|
|
15
16
|
const managedLabel = "dev.forge.managed=true";
|
|
16
17
|
const bridgeRuntimeDigestLabel = "dev.forge.bridge-runtime-digest";
|
|
17
18
|
export class DockerSandboxRuntime {
|
|
@@ -554,10 +555,10 @@ export class DockerSandboxRuntime {
|
|
|
554
555
|
const existing = this.builds.get(key);
|
|
555
556
|
if (existing) {
|
|
556
557
|
await sink.emit(event("sandbox_image_cache_miss", `Sandbox image ${key.slice(0, 12)} is being built by another dispatch.`, { cacheKey: key, singleflight: true }));
|
|
557
|
-
const waitStartedAt =
|
|
558
|
+
const waitStartedAt = monotonicNowMs();
|
|
558
559
|
const built = await existing;
|
|
559
560
|
await sink.emit(event("sandbox_build_singleflight_wait", `Waited for the existing sandbox image build ${key.slice(0, 12)}.`, {
|
|
560
|
-
durationMs:
|
|
561
|
+
durationMs: Math.round(monotonicNowMs() - waitStartedAt),
|
|
561
562
|
imageDigest: built.imageDigest
|
|
562
563
|
}));
|
|
563
564
|
return built;
|
|
@@ -889,8 +890,8 @@ export class DockerSandboxRuntime {
|
|
|
889
890
|
await sink.emit(event("sandbox_image_signature_warning", `Image signature enforcement is not configured for adoption-mode image ${image}.`));
|
|
890
891
|
}
|
|
891
892
|
async waitForServiceHealthy(name, signal) {
|
|
892
|
-
const deadline =
|
|
893
|
-
while (
|
|
893
|
+
const deadline = monotonicNowMs() + 60_000;
|
|
894
|
+
while (monotonicNowMs() < deadline) {
|
|
894
895
|
if (signal?.aborted)
|
|
895
896
|
throw new Error(String(signal.reason ?? "sandbox service startup cancelled"));
|
|
896
897
|
const status = (await this.docker(["container", "inspect", name, "--format", "{{.State.Health.Status}}"], 10_000, signal)).stdout.trim();
|
|
@@ -11,6 +11,7 @@ import { ManagedBuildCoordinator } from "./managed-build-coordinator.js";
|
|
|
11
11
|
import { managedBuildConfiguration, publishManagedBuildReceipt, verifyManagedBuildReceipt, } from "./managed-build-receipt.js";
|
|
12
12
|
import { managedLongTierTimeoutMs, managedMaximumCommandTimeoutMs, managedVerificationTierTimeoutMs, } from "./managed-verification-timeouts.js";
|
|
13
13
|
import { resolveWindowsCommandFromPath, StreamingProviderProcessRunner } from "./provider-cli/provider-cli.process.js";
|
|
14
|
+
import { monotonicNowMs } from "../shared/time/monotonic.js";
|
|
14
15
|
const managedCommandNames = new Set(["forge.run_managed_command", "run_managed_command"]);
|
|
15
16
|
const startManagedCommandNames = new Set(["forge.start_managed_command", "start_managed_command"]);
|
|
16
17
|
const getManagedCommandNames = new Set(["forge.get_managed_command", "get_managed_command"]);
|
|
@@ -612,7 +613,7 @@ async function waitManagedCommand(input) {
|
|
|
612
613
|
const commandId = validatedCommandId(record);
|
|
613
614
|
const requestedWaitTimeoutMs = optionalWaitTimeout(record.wait_timeout_ms);
|
|
614
615
|
const waitTimeoutMs = Math.min(requestedWaitTimeoutMs, maximumSynchronousWaitMs);
|
|
615
|
-
const deadline =
|
|
616
|
+
const deadline = monotonicNowMs() + waitTimeoutMs;
|
|
616
617
|
const receiptPath = managedCommandReceiptPath(requiredAttemptEnvironment("FORGE_LOG_DIR"), commandId);
|
|
617
618
|
const task = managedCommandTasks.get(commandId);
|
|
618
619
|
if (task) {
|
|
@@ -623,12 +624,12 @@ async function waitManagedCommand(input) {
|
|
|
623
624
|
if (result)
|
|
624
625
|
return result;
|
|
625
626
|
}
|
|
626
|
-
while (
|
|
627
|
+
while (monotonicNowMs() < deadline) {
|
|
627
628
|
const result = await readManagedCommand({ command_id: commandId });
|
|
628
629
|
if (terminalManagedCommandStatuses.has(String(result.structuredContent.status))) {
|
|
629
630
|
return result;
|
|
630
631
|
}
|
|
631
|
-
await delay(Math.min(managedCommandReceiptPollIntervalMs, Math.max(1, deadline -
|
|
632
|
+
await delay(Math.min(managedCommandReceiptPollIntervalMs, Math.max(1, deadline - monotonicNowMs())));
|
|
632
633
|
}
|
|
633
634
|
const receipt = await readManagedCommand({ command_id: commandId });
|
|
634
635
|
// Only a running receipt is refreshed on a heartbeat; queued or starting receipts are
|
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { defaultWorktreeCoordinatorStateRoot } from "../worktree/worktree-paths.js";
|
|
5
|
+
import { monotonicNowMs } from "../shared/time/monotonic.js";
|
|
5
6
|
const heartbeatIntervalMs = 30_000;
|
|
6
7
|
const staleAfterMs = 2 * 60_000;
|
|
7
8
|
const acquisitionTimeoutMs = 2 * 60 * 60_000;
|
|
@@ -17,7 +18,7 @@ export class ManagedBuildCoordinator {
|
|
|
17
18
|
const root = path.join(this.stateRoot, "managed-build-locks");
|
|
18
19
|
const target = path.join(root, `${key}.lock`);
|
|
19
20
|
const token = randomUUID();
|
|
20
|
-
const startedAt =
|
|
21
|
+
const startedAt = monotonicNowMs();
|
|
21
22
|
let waitingReported = false;
|
|
22
23
|
await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
23
24
|
while (true) {
|
|
@@ -49,7 +50,7 @@ export class ManagedBuildCoordinator {
|
|
|
49
50
|
waitingReported = true;
|
|
50
51
|
await onWaiting?.();
|
|
51
52
|
}
|
|
52
|
-
if (
|
|
53
|
+
if (monotonicNowMs() - startedAt >= acquisitionTimeoutMs) {
|
|
53
54
|
throw new Error("managed_build_slot_timeout: another build held this worktree for more than two hours.", { cause: error });
|
|
54
55
|
}
|
|
55
56
|
await delay(pollIntervalMs);
|
|
@@ -58,7 +59,7 @@ export class ManagedBuildCoordinator {
|
|
|
58
59
|
const heartbeat = setInterval(() => void refreshHeartbeat(target, token), heartbeatIntervalMs);
|
|
59
60
|
heartbeat.unref?.();
|
|
60
61
|
try {
|
|
61
|
-
return await action(
|
|
62
|
+
return await action(Math.round(monotonicNowMs() - startedAt));
|
|
62
63
|
}
|
|
63
64
|
finally {
|
|
64
65
|
clearInterval(heartbeat);
|
|
@@ -3,6 +3,12 @@ import type { ProviderCliRunContext } from "./provider-cli.types.js";
|
|
|
3
3
|
import type { ProviderOutputState } from "./provider-output-state.js";
|
|
4
4
|
import { type ProviderUsage } from "./provider-usage.js";
|
|
5
5
|
export declare function providerProcessEnv(dispatch: RunOnceDispatchPacket, runtimeEnvironment?: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
|
|
6
|
+
/**
|
|
7
|
+
* Pre-publication validation runs the attempt's own checks, whose E2E fixtures log in to the
|
|
8
|
+
* brokered attempt database. Unlike the provider's shell, it receives that capability; the
|
|
9
|
+
* validator redacts it from every command's output.
|
|
10
|
+
*/
|
|
11
|
+
export declare function prePublicationValidationEnv(dispatch: RunOnceDispatchPacket, runtimeEnvironment?: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
|
|
6
12
|
export declare function providerRuntimeEnvironment(runtimeEnvironment?: Readonly<Record<string, string>>): Readonly<Record<string, string>>;
|
|
7
13
|
export declare function providerCredentialEnv(dispatch: RunOnceDispatchPacket): Readonly<Record<string, string>>;
|
|
8
14
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attemptDatabaseCapabilityEnvironment } from "../../runtime/attempt-database-broker.js";
|
|
1
2
|
import { completionRedactionEnv } from "../phase-completion.js";
|
|
2
3
|
import { addUsage, buildProviderUsageEvent, emptyUsage, hasUsage } from "./provider-usage.js";
|
|
3
4
|
export function providerProcessEnv(dispatch, runtimeEnvironment) {
|
|
@@ -12,6 +13,16 @@ export function providerProcessEnv(dispatch, runtimeEnvironment) {
|
|
|
12
13
|
// Bridge environment, including credentials outside the provider contract.
|
|
13
14
|
return env;
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Pre-publication validation runs the attempt's own checks, whose E2E fixtures log in to the
|
|
18
|
+
* brokered attempt database. Unlike the provider's shell, it receives that capability; the
|
|
19
|
+
* validator redacts it from every command's output.
|
|
20
|
+
*/
|
|
21
|
+
export function prePublicationValidationEnv(dispatch, runtimeEnvironment) {
|
|
22
|
+
const env = providerProcessEnv(dispatch, runtimeEnvironment);
|
|
23
|
+
const capability = runtimeEnvironment?.[attemptDatabaseCapabilityEnvironment]?.trim();
|
|
24
|
+
return capability ? { ...env, [attemptDatabaseCapabilityEnvironment]: capability } : env;
|
|
25
|
+
}
|
|
15
26
|
export function providerRuntimeEnvironment(runtimeEnvironment) {
|
|
16
27
|
if (!runtimeEnvironment)
|
|
17
28
|
return {};
|
|
@@ -10,7 +10,7 @@ import { cleanupCompletionTools, commitWorktreeChanges, completionPhaseFromArtif
|
|
|
10
10
|
import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
|
|
11
11
|
import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
|
|
12
12
|
import { isTransientCodexProviderNotFoundFailure, recoveringProviderProcessRunner, StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
13
|
-
import { emitProviderUsageEvent, providerProcessEnv } from "./provider-cli.runtime.js";
|
|
13
|
+
import { emitProviderUsageEvent, prePublicationValidationEnv, providerProcessEnv } from "./provider-cli.runtime.js";
|
|
14
14
|
import { clip, createProviderOutputState, parseJsonLine } from "./provider-output-state.js";
|
|
15
15
|
import { writeCiRepairReadinessManifest } from "./ci-repair-readiness.js";
|
|
16
16
|
import { assertRepairHeadEpoch } from "./provider-cli.repair-epoch.js";
|
|
@@ -1489,9 +1489,10 @@ export class ProcessBackedProviderCliService {
|
|
|
1489
1489
|
const { dispatch, worktree } = context;
|
|
1490
1490
|
const processRunner = recoveringProviderProcessRunner(context.processRunner ?? this.processRunner);
|
|
1491
1491
|
const environment = providerProcessEnv(dispatch, repairEnvironment);
|
|
1492
|
+
const validationEnvironment = prePublicationValidationEnv(dispatch, repairEnvironment);
|
|
1492
1493
|
for (let repairTurn = 0;; repairTurn += 1) {
|
|
1493
1494
|
try {
|
|
1494
|
-
await this.prePublicationValidator.validate(worktree.worktreePath, validationProfile, publicationCapabilities(context, repairEnvironment), context.emit, context.signal, prePublicationExecution(context,
|
|
1495
|
+
await this.prePublicationValidator.validate(worktree.worktreePath, validationProfile, publicationCapabilities(context, repairEnvironment), context.emit, context.signal, prePublicationExecution(context, validationEnvironment, this.processRunner, preparedHeadSha));
|
|
1495
1496
|
if (repairTurn > 0) {
|
|
1496
1497
|
await context.emit({
|
|
1497
1498
|
role: "runner",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { captureHostProcessIdentity } from "./host-process-supervisor.js";
|
|
2
|
+
import { monotonicNowMs } from "../time/monotonic.js";
|
|
2
3
|
/** Exact, birth-marker-fenced recovery for a process tree left by an old bridge. */
|
|
3
4
|
export class ExactHostProcessRecovery {
|
|
4
5
|
platform;
|
|
@@ -71,8 +72,8 @@ function signalExactProcessGroup(pid, signal) {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
async function waitForExit(pid, timeoutMs) {
|
|
74
|
-
const deadline =
|
|
75
|
-
while (
|
|
75
|
+
const deadline = monotonicNowMs() + timeoutMs;
|
|
76
|
+
while (monotonicNowMs() < deadline && processExists(pid)) {
|
|
76
77
|
await new Promise(resolve => setTimeout(resolve, 25));
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { monotonicNowMs } from "../time/monotonic.js";
|
|
6
7
|
const windowsJobSupervisorPath = fileURLToPath(new URL("../../../runtime/windows-job-supervisor.ps1", import.meta.url));
|
|
7
8
|
export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, resolvedArgs, platform = process.platform) {
|
|
8
9
|
const env = request.env;
|
|
@@ -277,8 +278,8 @@ function unixProcessGroupExists(processGroupId) {
|
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
async function waitForUnixProcessGroupExit(processGroupId, timeoutMs) {
|
|
280
|
-
const deadline =
|
|
281
|
-
while (
|
|
281
|
+
const deadline = monotonicNowMs() + timeoutMs;
|
|
282
|
+
while (monotonicNowMs() < deadline && unixProcessGroupExists(processGroupId)) {
|
|
282
283
|
await new Promise(resolve => setTimeout(resolve, 25));
|
|
283
284
|
}
|
|
284
285
|
}
|
|
@@ -307,8 +308,8 @@ function processExists(pid) {
|
|
|
307
308
|
}
|
|
308
309
|
}
|
|
309
310
|
async function waitForExactProcessExit(pid, identity, timeoutMs, platform) {
|
|
310
|
-
const deadline =
|
|
311
|
-
while (
|
|
311
|
+
const deadline = monotonicNowMs() + timeoutMs;
|
|
312
|
+
while (monotonicNowMs() < deadline && exactProcessExists(pid, identity, platform)) {
|
|
312
313
|
await new Promise(resolve => setTimeout(resolve, 25));
|
|
313
314
|
}
|
|
314
315
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function monotonicNowMs(): number;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Elapsed time and local deadlines use the monotonic clock. The wall clock can step:
|
|
2
|
+
// a WSL VM's clock is corrected in multi-second jumps, which would end a local wait
|
|
3
|
+
// early or count time that never passed. Timestamps that are persisted or compared
|
|
4
|
+
// across processes (lease expiry, receipts, file mtimes) stay on the wall clock.
|
|
5
|
+
// Read through the global so test fake timers that replace `performance` apply.
|
|
6
|
+
export function monotonicNowMs() {
|
|
7
|
+
return performance.now();
|
|
8
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const forgeBridgeVersion = "0.1.4-dev.
|
|
1
|
+
export declare const forgeBridgeVersion = "0.1.4-dev.41da3780c451";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.4-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.4-dev.41da3780c451";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loucompanion/forge-bridge",
|
|
3
|
-
"version": "0.1.4-dev.
|
|
3
|
+
"version": "0.1.4-dev.41da3780c451",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|