@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.
@@ -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 = Date.now() + this.metadataReadyTimeoutMs;
252
- while (Date.now() <= deadline) {
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 = Date.now() + configuredDeadlineMs;
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 - Date.now();
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 (Date.now() >= retentionDeadline)
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 = Date.now();
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 (Date.now() <= idleDeadline && Date.now() <= hardDeadline) {
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, Date.now() + this.readinessTimeoutMs);
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 = Date.now() >= hardDeadline;
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 = Date.now();
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 (Date.now() - started >= this.lockTimeoutMs) {
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);
@@ -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 = Date.now();
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: Date.now() - waitStartedAt,
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 = Date.now() + 60_000;
893
- while (Date.now() < deadline) {
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 = Date.now() + waitTimeoutMs;
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 (Date.now() < deadline) {
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 - Date.now())));
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 = Date.now();
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 (Date.now() - startedAt >= acquisitionTimeoutMs) {
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(Date.now() - startedAt);
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, environment, this.processRunner, preparedHeadSha));
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 = Date.now() + timeoutMs;
75
- while (Date.now() < deadline && processExists(pid)) {
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 = Date.now() + timeoutMs;
281
- while (Date.now() < deadline && unixProcessGroupExists(processGroupId)) {
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 = Date.now() + timeoutMs;
311
- while (Date.now() < deadline && exactProcessExists(pid, identity, platform)) {
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.1ff37038366e";
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.1ff37038366e";
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.1ff37038366e",
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",