@ricsam/r5d-worker 0.0.88 → 0.0.90

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 CHANGED
@@ -41,6 +41,8 @@ Worker labels are mandatory and unique per user. Choose labels that describe hos
41
41
 
42
42
  The supervisor also reconnects automatically after server rollouts and transient network failures. A brief bounded delay prevents a tight retry loop while the server is unavailable. Pressing Ctrl+C still stops the worker, and agent-managed processes retain their existing in-process recovery path while disconnected.
43
43
 
44
+ The server registry is the sole heartbeat timeout authority. The worker answers server pings but does not run an independent connection watchdog, because synchronous bounded workspace operations can temporarily occupy its event loop. The server keeps those operations leased for their bounded lifetime and closes genuinely stale connections after the lease ends.
45
+
44
46
  Credential-generation restart fencing protects against stale or accidentally detached descendants. It is not a hostile same-UID sandbox: code running as the worker's OS user can read or tamper with that user's config and auth files, install a persistent user service, or return after reboot. Run untrusted agents in a disposable VM that is reset at the boundary, or use a separately privileged credential broker/service account. When rotating the worker's own local `r5dctl` credential, stop the worker and reboot/reset the host before writing the replacement credential so an old same-UID descendant cannot read the new config.
45
47
 
46
48
  One-click updates use the current user's global npm prefix and never invoke `sudo`. If that prefix is not writable, User Settings provides the exact command to run manually.
package/dist/cjs/main.cjs CHANGED
@@ -63,7 +63,6 @@ var import_node_child_process = require("node:child_process");
63
63
  var import_bun_sqlite = require("bun:sqlite");
64
64
  var import_cli_update = require("./cli-update.cjs");
65
65
  var import_git_process_environment = require("./git-process-environment.cjs");
66
- var import_heartbeat = require("./heartbeat.cjs");
67
66
  var import_process_tree = require("./process-tree.cjs");
68
67
  var import_pty_output_coalescer = require("./pty-output-coalescer.cjs");
69
68
  var import_port_forward_client = require("./port-forward-client.cjs");
@@ -150,6 +149,11 @@ class StaleWorkerAdmissionError extends Error {
150
149
  this.name = "StaleWorkerAdmissionError";
151
150
  }
152
151
  }
152
+ class WorkspaceSyncAdmissionChangedError extends Error {
153
+ name = "WorkspaceSyncAdmissionChangedError";
154
+ }
155
+ let unsafeWorkspaceSyncRecoveryAttempts = 0;
156
+ let unsafeWorkspaceSyncBouncePending = false;
153
157
  function advanceWorkerAdmissionGeneration() {
154
158
  workerAdmissionGeneration += 1;
155
159
  return workerAdmissionGeneration;
@@ -1633,16 +1637,33 @@ function workspaceSyncFailureHydrationIsSafe(input) {
1633
1637
  if (input.error instanceof import_workspace_git_sync.WorkspaceRemediationAncestryError) return true;
1634
1638
  if (input.resetToCanonical) return false;
1635
1639
  try {
1636
- return input.inspectCurrentHydration();
1640
+ return input.inspectRecoverableHydration();
1637
1641
  } catch {
1638
1642
  return false;
1639
1643
  }
1640
1644
  }
1645
+ const WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS = 3;
1646
+ const WORKSPACE_UNSAFE_SYNC_RECOVERY_CLOSE_CODE = 4008;
1647
+ function unsafeWorkspaceSyncRecoveryBounceDelayMs(attempt) {
1648
+ return Math.min(1e3 * 2 ** Math.max(0, attempt - 1), 3e4);
1649
+ }
1650
+ function unsafeWorkspaceSyncFailureResponse(input) {
1651
+ if (input.resetToCanonical) return "fatal_exit";
1652
+ if (!input.hasRetainableWork) return "fatal_exit";
1653
+ if (input.recoveryAttempts >= input.maxRecoveryAttempts) return "fatal_exit";
1654
+ if (input.bouncePending) return "recovery_pending";
1655
+ return "recovery_bounce";
1656
+ }
1641
1657
  function fenceUnsafeWorkspaceSyncFailure(input) {
1642
- if (input.hydrationCurrent) return false;
1658
+ if (input.hydrationRecoverable) return "none";
1643
1659
  input.invalidateExecution();
1644
- requireCredentialGenerationRestart(true, input.fatalExit, input.exitCode);
1645
- return true;
1660
+ const response = unsafeWorkspaceSyncFailureResponse(input);
1661
+ if (response === "fatal_exit") {
1662
+ requireCredentialGenerationRestart(true, input.fatalExit, input.exitCode);
1663
+ throw new Error("Unsafe workspace synchronization fence continued past its fatal exit");
1664
+ }
1665
+ if (response === "recovery_bounce") input.scheduleRecoveryBounce();
1666
+ return response;
1646
1667
  }
1647
1668
  const workerGitSecurityTestHarness = {
1648
1669
  commandArgs: workerGitCommand.commandArgs,
@@ -1670,6 +1691,11 @@ const workerGitSecurityTestHarness = {
1670
1691
  credentialReapContractStatus,
1671
1692
  verifiedCredentialReapContract,
1672
1693
  fenceUnsafeWorkspaceSyncFailure,
1694
+ unsafeWorkspaceSyncFailureResponse,
1695
+ unsafeWorkspaceSyncRecoveryBounceDelayMs,
1696
+ WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
1697
+ WORKSPACE_UNSAFE_SYNC_RECOVERY_CLOSE_CODE,
1698
+ WorkspaceSyncAdmissionChangedError,
1673
1699
  workspaceConfigurationIncidentDeferral,
1674
1700
  workspaceIncidentTerminalClearDisposition,
1675
1701
  workspaceIncidentTerminalRefreshFence,
@@ -4286,6 +4312,28 @@ async function startWorker(options) {
4286
4312
  );
4287
4313
  }
4288
4314
  };
4315
+ const scheduleUnsafeWorkspaceSyncRecoveryBounce = (cause) => {
4316
+ unsafeWorkspaceSyncBouncePending = true;
4317
+ const attempt = unsafeWorkspaceSyncRecoveryAttempts;
4318
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4319
+ process.stderr.write(
4320
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; awaiting reconnect recovery (attempt ${attempt} of ${WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS}): ${cause}
4321
+ `
4322
+ );
4323
+ return;
4324
+ }
4325
+ process.stderr.write(
4326
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; bouncing the worker socket for in-process recovery (attempt ${attempt} of ${WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS}): ${cause}
4327
+ `
4328
+ );
4329
+ const timer = setTimeout(() => {
4330
+ if (!unsafeWorkspaceSyncBouncePending) return;
4331
+ if (currentWorkerSocket === ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
4332
+ ws.close(WORKSPACE_UNSAFE_SYNC_RECOVERY_CLOSE_CODE, "Workspace synchronization failed without a recoverable hydration basis");
4333
+ }
4334
+ }, unsafeWorkspaceSyncRecoveryBounceDelayMs(attempt));
4335
+ timer.unref();
4336
+ };
4289
4337
  const runWorkspaceSync = async (input) => {
4290
4338
  const attemptId = input.attemptId ?? crypto.randomUUID();
4291
4339
  const requestedAt = Date.now();
@@ -4294,7 +4342,7 @@ async function startWorker(options) {
4294
4342
  const admittedDeferredIncidentId = deferredWorkspaceConfiguration?.incidentId ?? null;
4295
4343
  const assertStillAdmitted = () => {
4296
4344
  if (admittedGeneration !== workerAdmissionGeneration || admittedActiveIncidentId !== activeWorkspaceIncidentId || admittedDeferredIncidentId !== (deferredWorkspaceConfiguration?.incidentId ?? null) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4297
- throw new Error("Workspace synchronization admission changed while asynchronous work was in flight");
4345
+ throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
4298
4346
  }
4299
4347
  };
4300
4348
  if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
@@ -4346,31 +4394,62 @@ async function startWorker(options) {
4346
4394
  assertStillAdmitted
4347
4395
  });
4348
4396
  } catch (error) {
4349
- const hydrationCurrent = workspaceSyncFailureHydrationIsSafe({
4397
+ const errorMessage = error instanceof Error ? error.message : String(error);
4398
+ const hydrationRecoverable = workspaceSyncFailureHydrationIsSafe({
4350
4399
  error,
4351
4400
  resetToCanonical: input.resetToCanonical === true,
4352
- inspectCurrentHydration: () => (0, import_workspace_git_sync.workspaceGitHydrationIsCurrent)(workspaceShadowRoot, buildWorkspaceMounts())
4401
+ inspectRecoverableHydration: () => (0, import_workspace_git_sync.workspaceGitHydrationIsRecoverable)(workspaceShadowRoot, buildWorkspaceMounts())
4353
4402
  });
4354
- if (!hydrationCurrent) {
4355
- process.stderr.write(
4356
- `[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
4403
+ if (hydrationRecoverable) {
4404
+ if (error instanceof WorkspaceSyncAdmissionChangedError) {
4405
+ process.stderr.write(
4406
+ `[r5d-worker] workspace synchronization was cancelled with every mount at a recorded basis; the next cycle converges it: ${errorMessage}
4357
4407
  `
4358
- );
4408
+ );
4409
+ }
4410
+ return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
4359
4411
  }
4360
- fenceUnsafeWorkspaceSyncFailure({
4361
- hydrationCurrent,
4412
+ const fenced = fenceUnsafeWorkspaceSyncFailure({
4413
+ hydrationRecoverable,
4414
+ resetToCanonical: input.resetToCanonical === true,
4415
+ recoveryAttempts: unsafeWorkspaceSyncRecoveryAttempts,
4416
+ maxRecoveryAttempts: WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
4417
+ hasRetainableWork: activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0,
4418
+ bouncePending: unsafeWorkspaceSyncBouncePending,
4362
4419
  invalidateExecution: () => {
4363
4420
  workspaceConfigured = false;
4364
4421
  advanceWorkerAdmissionGeneration();
4365
4422
  },
4423
+ scheduleRecoveryBounce: () => {
4424
+ unsafeWorkspaceSyncRecoveryAttempts += 1;
4425
+ scheduleUnsafeWorkspaceSyncRecoveryBounce(errorMessage);
4426
+ },
4427
+ fatalExit: (exitCode) => {
4428
+ process.stderr.write(
4429
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4430
+ `
4431
+ );
4432
+ process.exit(exitCode);
4433
+ },
4366
4434
  exitCode: 1
4367
4435
  });
4368
- return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
4436
+ if (fenced === "recovery_pending") {
4437
+ process.stderr.write(
4438
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis while in-process recovery is already pending: ${errorMessage}
4439
+ `
4440
+ );
4441
+ }
4442
+ return {
4443
+ ...await failedWorkspaceSyncResult(attemptId, input.trigger, error),
4444
+ failureReason: "unsafe_hydration",
4445
+ recoveryAttempt: unsafeWorkspaceSyncRecoveryAttempts
4446
+ };
4369
4447
  }
4370
4448
  });
4371
4449
  } finally {
4372
4450
  workspaceSyncRequestsInFlight -= 1;
4373
4451
  }
4452
+ if (result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
4374
4453
  const finishedAt = Date.now();
4375
4454
  result = {
4376
4455
  ...result,
@@ -4421,7 +4500,6 @@ async function startWorker(options) {
4421
4500
  );
4422
4501
  }).finally(() => {
4423
4502
  automaticSyncInFlight = false;
4424
- heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
4425
4503
  if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4426
4504
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
4427
4505
  }
@@ -4825,7 +4903,9 @@ async function startWorker(options) {
4825
4903
  const configurationSyncAdmissionGeneration = workerAdmissionGeneration;
4826
4904
  const assertConfigurationSyncStillAdmitted = () => {
4827
4905
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration || configurationSyncAdmissionGeneration !== workerAdmissionGeneration || !workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4828
- throw new Error("Workspace configuration synchronization admission changed while asynchronous work was in flight");
4906
+ throw new WorkspaceSyncAdmissionChangedError(
4907
+ "Workspace configuration synchronization admission changed while asynchronous work was in flight"
4908
+ );
4829
4909
  }
4830
4910
  };
4831
4911
  const result = await performWorkspaceSync({
@@ -4918,10 +4998,6 @@ async function startWorker(options) {
4918
4998
  Authorization: `Bearer ${token}`
4919
4999
  }
4920
5000
  });
4921
- let lastServerHeartbeatAt = null;
4922
- let heartbeatBusyGrace = null;
4923
- let heartbeatTimedOut = false;
4924
- let heartbeatWatchdog;
4925
5001
  const handleGracefulShutdown = () => {
4926
5002
  if (shutdownAfterClose) return;
4927
5003
  shutdownAfterClose = true;
@@ -4949,23 +5025,8 @@ async function startWorker(options) {
4949
5025
  };
4950
5026
  process.on("SIGTERM", handleGracefulShutdown);
4951
5027
  process.on("SIGINT", handleGracefulShutdown);
4952
- const stopHeartbeatWatchdog = () => {
4953
- if (heartbeatWatchdog) {
4954
- clearInterval(heartbeatWatchdog);
4955
- heartbeatWatchdog = void 0;
4956
- }
4957
- };
4958
5028
  ws.addEventListener("open", () => {
4959
5029
  currentWorkerSocket = ws;
4960
- heartbeatWatchdog = setInterval(() => {
4961
- if (heartbeatTimedOut || !(0, import_heartbeat.hasWorkerHeartbeatTimedOutWithBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace)) {
4962
- return;
4963
- }
4964
- heartbeatTimedOut = true;
4965
- stopHeartbeatWatchdog();
4966
- process.stderr.write("[r5d-worker] server heartbeat timed out; reconnecting\n");
4967
- ws.terminate();
4968
- }, import_heartbeat.WORKER_HEARTBEAT_INTERVAL_MS);
4969
5030
  const hello = {
4970
5031
  type: "hello",
4971
5032
  hostInfo: {
@@ -5012,7 +5073,7 @@ async function startWorker(options) {
5012
5073
  sourceSocketIsCurrent: currentWorkerSocket === ws,
5013
5074
  sourceSocketOpen: ws.readyState === WebSocket.OPEN,
5014
5075
  workspaceConfigured,
5015
- runtimeHealthy: !shutdownAfterClose && !heartbeatTimedOut
5076
+ runtimeHealthy: !shutdownAfterClose
5016
5077
  });
5017
5078
  if (message.type === "connected") {
5018
5079
  return;
@@ -5022,6 +5083,8 @@ async function startWorker(options) {
5022
5083
  const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
5023
5084
  advanceWorkerAdmissionGeneration();
5024
5085
  const configured = await configureWorkerWorkspace(message, receiptGeneration);
5086
+ unsafeWorkspaceSyncBouncePending = false;
5087
+ if (configured.result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
5025
5088
  sendWorkerMessageFromCurrentSource(ws, {
5026
5089
  type: "workspace_configured",
5027
5090
  requestId: message.requestId,
@@ -5369,8 +5432,6 @@ async function startWorker(options) {
5369
5432
  return;
5370
5433
  }
5371
5434
  if (message.type === "ping") {
5372
- lastServerHeartbeatAt = Date.now();
5373
- heartbeatBusyGrace = null;
5374
5435
  ws.send(JSON.stringify({ type: "pong" }));
5375
5436
  return;
5376
5437
  }
@@ -5784,15 +5845,12 @@ async function startWorker(options) {
5784
5845
  })().catch((error) => {
5785
5846
  process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
5786
5847
  `);
5787
- }).finally(() => {
5788
- heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
5789
5848
  });
5790
5849
  });
5791
5850
  ws.addEventListener("close", (event) => {
5792
5851
  const processEvents = process;
5793
5852
  processEvents.off("SIGTERM", handleGracefulShutdown);
5794
5853
  processEvents.off("SIGINT", handleGracefulShutdown);
5795
- stopHeartbeatWatchdog();
5796
5854
  if (workspaceAutomaticTimer) {
5797
5855
  clearTimeout(workspaceAutomaticTimer);
5798
5856
  workspaceAutomaticTimer = void 0;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.88",
3
+ "version": "0.0.90",
4
4
  "type": "commonjs"
5
5
  }
@@ -42,6 +42,7 @@ __export(workspace_git_sync_exports, {
42
42
  resetWorkspaceGit: () => resetWorkspaceGit,
43
43
  synchronizeWorkspaceGit: () => synchronizeWorkspaceGit,
44
44
  workspaceGitHydrationIsCurrent: () => workspaceGitHydrationIsCurrent,
45
+ workspaceGitHydrationIsRecoverable: () => workspaceGitHydrationIsRecoverable,
45
46
  workspaceGitMountsForRemediation: () => workspaceGitMountsForRemediation,
46
47
  workspaceGitSyncTestHarness: () => workspaceGitSyncTestHarness
47
48
  });
@@ -420,18 +421,25 @@ function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
420
421
  }
421
422
  }
422
423
  function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
424
+ return workspaceGitHydrationHolds(workspacePath, mounts, false);
425
+ }
426
+ function workspaceGitHydrationIsRecoverable(workspacePath, mounts) {
427
+ return workspaceGitHydrationHolds(workspacePath, mounts, true);
428
+ }
429
+ function workspaceGitHydrationHolds(workspacePath, mounts, tolerateIdleRecordedBasis) {
423
430
  const resolvedWorkspacePath = import_node_path.default.resolve(workspacePath);
424
431
  if (!import_node_fs.default.existsSync(import_node_path.default.join(resolvedWorkspacePath, ".git"))) return true;
425
432
  if (mounts) validateMounts(resolvedWorkspacePath, mounts);
426
433
  const head = revParse(resolvedWorkspacePath, "HEAD");
427
434
  if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
435
+ if (tolerateIdleRecordedBasis && lstatIfExists(hydrationTransactionPath(resolvedWorkspacePath)) !== null) return false;
428
436
  const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
429
437
  if (!workspaceBasisRefsAreCurrent(resolvedWorkspacePath, receipt ?? { version: 3, mounts: [] })) return false;
430
438
  if (head === null) return true;
431
439
  if (!receipt) return false;
432
440
  return mounts ? configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts).every(
433
- (mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean(mount.busy?.() && workspaceHydrationReceiptMountBasisHead(receipt, mount))
434
- ) : Boolean(receipt && receipt.mounts.every((mount) => mount.head === head));
441
+ (mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean((tolerateIdleRecordedBasis || mount.busy?.()) && workspaceHydrationReceiptMountBasisHead(receipt, mount))
442
+ ) : Boolean(receipt && receipt.mounts.every((mount) => tolerateIdleRecordedBasis || mount.head === head));
435
443
  }
436
444
  const WORKSPACE_GIT_BASIS_REF_PREFIX = "refs/r5d/workspace-basis/";
437
445
  const WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX = "refs/r5d/workspace-basis-staging/";
@@ -2190,6 +2198,7 @@ const workspaceGitSyncTestHarness = {
2190
2198
  resetWorkspaceGit,
2191
2199
  synchronizeWorkspaceGit,
2192
2200
  workspaceGitHydrationIsCurrent,
2201
+ workspaceGitHydrationIsRecoverable,
2193
2202
  workspaceGitMountsForRemediation,
2194
2203
  workspaceGitSyncTestHarness
2195
2204
  });
package/dist/mjs/main.mjs CHANGED
@@ -7,11 +7,6 @@ import { spawn as spawnChildProcess } from "node:child_process";
7
7
  import { Database } from "bun:sqlite";
8
8
  import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
9
9
  import { gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
10
- import {
11
- grantWorkerHeartbeatBusyGrace,
12
- hasWorkerHeartbeatTimedOutWithBusyGrace,
13
- WORKER_HEARTBEAT_INTERVAL_MS
14
- } from "./heartbeat.mjs";
15
10
  import { terminateProcessTree } from "./process-tree.mjs";
16
11
  import { createPtyOutputCoalescer } from "./pty-output-coalescer.mjs";
17
12
  import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
@@ -88,7 +83,7 @@ import {
88
83
  recoverWorkspaceGitHydration,
89
84
  resetWorkspaceGit,
90
85
  synchronizeWorkspaceGit,
91
- workspaceGitHydrationIsCurrent,
86
+ workspaceGitHydrationIsRecoverable,
92
87
  workspaceGitMountsForRemediation,
93
88
  WorkspaceRemediationAncestryError
94
89
  } from "./workspace-git-sync.mjs";
@@ -158,6 +153,11 @@ class StaleWorkerAdmissionError extends Error {
158
153
  this.name = "StaleWorkerAdmissionError";
159
154
  }
160
155
  }
156
+ class WorkspaceSyncAdmissionChangedError extends Error {
157
+ name = "WorkspaceSyncAdmissionChangedError";
158
+ }
159
+ let unsafeWorkspaceSyncRecoveryAttempts = 0;
160
+ let unsafeWorkspaceSyncBouncePending = false;
161
161
  function advanceWorkerAdmissionGeneration() {
162
162
  workerAdmissionGeneration += 1;
163
163
  return workerAdmissionGeneration;
@@ -1641,16 +1641,33 @@ function workspaceSyncFailureHydrationIsSafe(input) {
1641
1641
  if (input.error instanceof WorkspaceRemediationAncestryError) return true;
1642
1642
  if (input.resetToCanonical) return false;
1643
1643
  try {
1644
- return input.inspectCurrentHydration();
1644
+ return input.inspectRecoverableHydration();
1645
1645
  } catch {
1646
1646
  return false;
1647
1647
  }
1648
1648
  }
1649
+ const WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS = 3;
1650
+ const WORKSPACE_UNSAFE_SYNC_RECOVERY_CLOSE_CODE = 4008;
1651
+ function unsafeWorkspaceSyncRecoveryBounceDelayMs(attempt) {
1652
+ return Math.min(1e3 * 2 ** Math.max(0, attempt - 1), 3e4);
1653
+ }
1654
+ function unsafeWorkspaceSyncFailureResponse(input) {
1655
+ if (input.resetToCanonical) return "fatal_exit";
1656
+ if (!input.hasRetainableWork) return "fatal_exit";
1657
+ if (input.recoveryAttempts >= input.maxRecoveryAttempts) return "fatal_exit";
1658
+ if (input.bouncePending) return "recovery_pending";
1659
+ return "recovery_bounce";
1660
+ }
1649
1661
  function fenceUnsafeWorkspaceSyncFailure(input) {
1650
- if (input.hydrationCurrent) return false;
1662
+ if (input.hydrationRecoverable) return "none";
1651
1663
  input.invalidateExecution();
1652
- requireCredentialGenerationRestart(true, input.fatalExit, input.exitCode);
1653
- return true;
1664
+ const response = unsafeWorkspaceSyncFailureResponse(input);
1665
+ if (response === "fatal_exit") {
1666
+ requireCredentialGenerationRestart(true, input.fatalExit, input.exitCode);
1667
+ throw new Error("Unsafe workspace synchronization fence continued past its fatal exit");
1668
+ }
1669
+ if (response === "recovery_bounce") input.scheduleRecoveryBounce();
1670
+ return response;
1654
1671
  }
1655
1672
  const workerGitSecurityTestHarness = {
1656
1673
  commandArgs: workerGitCommand.commandArgs,
@@ -1678,6 +1695,11 @@ const workerGitSecurityTestHarness = {
1678
1695
  credentialReapContractStatus,
1679
1696
  verifiedCredentialReapContract,
1680
1697
  fenceUnsafeWorkspaceSyncFailure,
1698
+ unsafeWorkspaceSyncFailureResponse,
1699
+ unsafeWorkspaceSyncRecoveryBounceDelayMs,
1700
+ WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
1701
+ WORKSPACE_UNSAFE_SYNC_RECOVERY_CLOSE_CODE,
1702
+ WorkspaceSyncAdmissionChangedError,
1681
1703
  workspaceConfigurationIncidentDeferral,
1682
1704
  workspaceIncidentTerminalClearDisposition,
1683
1705
  workspaceIncidentTerminalRefreshFence,
@@ -4294,6 +4316,28 @@ async function startWorker(options) {
4294
4316
  );
4295
4317
  }
4296
4318
  };
4319
+ const scheduleUnsafeWorkspaceSyncRecoveryBounce = (cause) => {
4320
+ unsafeWorkspaceSyncBouncePending = true;
4321
+ const attempt = unsafeWorkspaceSyncRecoveryAttempts;
4322
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4323
+ process.stderr.write(
4324
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; awaiting reconnect recovery (attempt ${attempt} of ${WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS}): ${cause}
4325
+ `
4326
+ );
4327
+ return;
4328
+ }
4329
+ process.stderr.write(
4330
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; bouncing the worker socket for in-process recovery (attempt ${attempt} of ${WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS}): ${cause}
4331
+ `
4332
+ );
4333
+ const timer = setTimeout(() => {
4334
+ if (!unsafeWorkspaceSyncBouncePending) return;
4335
+ if (currentWorkerSocket === ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
4336
+ ws.close(WORKSPACE_UNSAFE_SYNC_RECOVERY_CLOSE_CODE, "Workspace synchronization failed without a recoverable hydration basis");
4337
+ }
4338
+ }, unsafeWorkspaceSyncRecoveryBounceDelayMs(attempt));
4339
+ timer.unref();
4340
+ };
4297
4341
  const runWorkspaceSync = async (input) => {
4298
4342
  const attemptId = input.attemptId ?? crypto.randomUUID();
4299
4343
  const requestedAt = Date.now();
@@ -4302,7 +4346,7 @@ async function startWorker(options) {
4302
4346
  const admittedDeferredIncidentId = deferredWorkspaceConfiguration?.incidentId ?? null;
4303
4347
  const assertStillAdmitted = () => {
4304
4348
  if (admittedGeneration !== workerAdmissionGeneration || admittedActiveIncidentId !== activeWorkspaceIncidentId || admittedDeferredIncidentId !== (deferredWorkspaceConfiguration?.incidentId ?? null) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4305
- throw new Error("Workspace synchronization admission changed while asynchronous work was in flight");
4349
+ throw new WorkspaceSyncAdmissionChangedError("Workspace synchronization admission changed while asynchronous work was in flight");
4306
4350
  }
4307
4351
  };
4308
4352
  if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
@@ -4354,31 +4398,62 @@ async function startWorker(options) {
4354
4398
  assertStillAdmitted
4355
4399
  });
4356
4400
  } catch (error) {
4357
- const hydrationCurrent = workspaceSyncFailureHydrationIsSafe({
4401
+ const errorMessage = error instanceof Error ? error.message : String(error);
4402
+ const hydrationRecoverable = workspaceSyncFailureHydrationIsSafe({
4358
4403
  error,
4359
4404
  resetToCanonical: input.resetToCanonical === true,
4360
- inspectCurrentHydration: () => workspaceGitHydrationIsCurrent(workspaceShadowRoot, buildWorkspaceMounts())
4405
+ inspectRecoverableHydration: () => workspaceGitHydrationIsRecoverable(workspaceShadowRoot, buildWorkspaceMounts())
4361
4406
  });
4362
- if (!hydrationCurrent) {
4363
- process.stderr.write(
4364
- `[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
4407
+ if (hydrationRecoverable) {
4408
+ if (error instanceof WorkspaceSyncAdmissionChangedError) {
4409
+ process.stderr.write(
4410
+ `[r5d-worker] workspace synchronization was cancelled with every mount at a recorded basis; the next cycle converges it: ${errorMessage}
4365
4411
  `
4366
- );
4412
+ );
4413
+ }
4414
+ return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
4367
4415
  }
4368
- fenceUnsafeWorkspaceSyncFailure({
4369
- hydrationCurrent,
4416
+ const fenced = fenceUnsafeWorkspaceSyncFailure({
4417
+ hydrationRecoverable,
4418
+ resetToCanonical: input.resetToCanonical === true,
4419
+ recoveryAttempts: unsafeWorkspaceSyncRecoveryAttempts,
4420
+ maxRecoveryAttempts: WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS,
4421
+ hasRetainableWork: activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0,
4422
+ bouncePending: unsafeWorkspaceSyncBouncePending,
4370
4423
  invalidateExecution: () => {
4371
4424
  workspaceConfigured = false;
4372
4425
  advanceWorkerAdmissionGeneration();
4373
4426
  },
4427
+ scheduleRecoveryBounce: () => {
4428
+ unsafeWorkspaceSyncRecoveryAttempts += 1;
4429
+ scheduleUnsafeWorkspaceSyncRecoveryBounce(errorMessage);
4430
+ },
4431
+ fatalExit: (exitCode) => {
4432
+ process.stderr.write(
4433
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis; exiting for exact recovery after ${unsafeWorkspaceSyncRecoveryAttempts} in-process recovery attempt(s): ${errorMessage}
4434
+ `
4435
+ );
4436
+ process.exit(exitCode);
4437
+ },
4374
4438
  exitCode: 1
4375
4439
  });
4376
- return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
4440
+ if (fenced === "recovery_pending") {
4441
+ process.stderr.write(
4442
+ `[r5d-worker] workspace synchronization failed without a recoverable hydration basis while in-process recovery is already pending: ${errorMessage}
4443
+ `
4444
+ );
4445
+ }
4446
+ return {
4447
+ ...await failedWorkspaceSyncResult(attemptId, input.trigger, error),
4448
+ failureReason: "unsafe_hydration",
4449
+ recoveryAttempt: unsafeWorkspaceSyncRecoveryAttempts
4450
+ };
4377
4451
  }
4378
4452
  });
4379
4453
  } finally {
4380
4454
  workspaceSyncRequestsInFlight -= 1;
4381
4455
  }
4456
+ if (result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
4382
4457
  const finishedAt = Date.now();
4383
4458
  result = {
4384
4459
  ...result,
@@ -4429,7 +4504,6 @@ async function startWorker(options) {
4429
4504
  );
4430
4505
  }).finally(() => {
4431
4506
  automaticSyncInFlight = false;
4432
- heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
4433
4507
  if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4434
4508
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
4435
4509
  }
@@ -4833,7 +4907,9 @@ async function startWorker(options) {
4833
4907
  const configurationSyncAdmissionGeneration = workerAdmissionGeneration;
4834
4908
  const assertConfigurationSyncStillAdmitted = () => {
4835
4909
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration || configurationSyncAdmissionGeneration !== workerAdmissionGeneration || !workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4836
- throw new Error("Workspace configuration synchronization admission changed while asynchronous work was in flight");
4910
+ throw new WorkspaceSyncAdmissionChangedError(
4911
+ "Workspace configuration synchronization admission changed while asynchronous work was in flight"
4912
+ );
4837
4913
  }
4838
4914
  };
4839
4915
  const result = await performWorkspaceSync({
@@ -4926,10 +5002,6 @@ async function startWorker(options) {
4926
5002
  Authorization: `Bearer ${token}`
4927
5003
  }
4928
5004
  });
4929
- let lastServerHeartbeatAt = null;
4930
- let heartbeatBusyGrace = null;
4931
- let heartbeatTimedOut = false;
4932
- let heartbeatWatchdog;
4933
5005
  const handleGracefulShutdown = () => {
4934
5006
  if (shutdownAfterClose) return;
4935
5007
  shutdownAfterClose = true;
@@ -4957,23 +5029,8 @@ async function startWorker(options) {
4957
5029
  };
4958
5030
  process.on("SIGTERM", handleGracefulShutdown);
4959
5031
  process.on("SIGINT", handleGracefulShutdown);
4960
- const stopHeartbeatWatchdog = () => {
4961
- if (heartbeatWatchdog) {
4962
- clearInterval(heartbeatWatchdog);
4963
- heartbeatWatchdog = void 0;
4964
- }
4965
- };
4966
5032
  ws.addEventListener("open", () => {
4967
5033
  currentWorkerSocket = ws;
4968
- heartbeatWatchdog = setInterval(() => {
4969
- if (heartbeatTimedOut || !hasWorkerHeartbeatTimedOutWithBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace)) {
4970
- return;
4971
- }
4972
- heartbeatTimedOut = true;
4973
- stopHeartbeatWatchdog();
4974
- process.stderr.write("[r5d-worker] server heartbeat timed out; reconnecting\n");
4975
- ws.terminate();
4976
- }, WORKER_HEARTBEAT_INTERVAL_MS);
4977
5034
  const hello = {
4978
5035
  type: "hello",
4979
5036
  hostInfo: {
@@ -5020,7 +5077,7 @@ async function startWorker(options) {
5020
5077
  sourceSocketIsCurrent: currentWorkerSocket === ws,
5021
5078
  sourceSocketOpen: ws.readyState === WebSocket.OPEN,
5022
5079
  workspaceConfigured,
5023
- runtimeHealthy: !shutdownAfterClose && !heartbeatTimedOut
5080
+ runtimeHealthy: !shutdownAfterClose
5024
5081
  });
5025
5082
  if (message.type === "connected") {
5026
5083
  return;
@@ -5030,6 +5087,8 @@ async function startWorker(options) {
5030
5087
  const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
5031
5088
  advanceWorkerAdmissionGeneration();
5032
5089
  const configured = await configureWorkerWorkspace(message, receiptGeneration);
5090
+ unsafeWorkspaceSyncBouncePending = false;
5091
+ if (configured.result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
5033
5092
  sendWorkerMessageFromCurrentSource(ws, {
5034
5093
  type: "workspace_configured",
5035
5094
  requestId: message.requestId,
@@ -5377,8 +5436,6 @@ async function startWorker(options) {
5377
5436
  return;
5378
5437
  }
5379
5438
  if (message.type === "ping") {
5380
- lastServerHeartbeatAt = Date.now();
5381
- heartbeatBusyGrace = null;
5382
5439
  ws.send(JSON.stringify({ type: "pong" }));
5383
5440
  return;
5384
5441
  }
@@ -5792,15 +5849,12 @@ async function startWorker(options) {
5792
5849
  })().catch((error) => {
5793
5850
  process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
5794
5851
  `);
5795
- }).finally(() => {
5796
- heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
5797
5852
  });
5798
5853
  });
5799
5854
  ws.addEventListener("close", (event) => {
5800
5855
  const processEvents = process;
5801
5856
  processEvents.off("SIGTERM", handleGracefulShutdown);
5802
5857
  processEvents.off("SIGINT", handleGracefulShutdown);
5803
- stopHeartbeatWatchdog();
5804
5858
  if (workspaceAutomaticTimer) {
5805
5859
  clearTimeout(workspaceAutomaticTimer);
5806
5860
  workspaceAutomaticTimer = void 0;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.88",
3
+ "version": "0.0.90",
4
4
  "type": "module"
5
5
  }
@@ -376,18 +376,25 @@ function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
376
376
  }
377
377
  }
378
378
  function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
379
+ return workspaceGitHydrationHolds(workspacePath, mounts, false);
380
+ }
381
+ function workspaceGitHydrationIsRecoverable(workspacePath, mounts) {
382
+ return workspaceGitHydrationHolds(workspacePath, mounts, true);
383
+ }
384
+ function workspaceGitHydrationHolds(workspacePath, mounts, tolerateIdleRecordedBasis) {
379
385
  const resolvedWorkspacePath = path.resolve(workspacePath);
380
386
  if (!fs.existsSync(path.join(resolvedWorkspacePath, ".git"))) return true;
381
387
  if (mounts) validateMounts(resolvedWorkspacePath, mounts);
382
388
  const head = revParse(resolvedWorkspacePath, "HEAD");
383
389
  if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
390
+ if (tolerateIdleRecordedBasis && lstatIfExists(hydrationTransactionPath(resolvedWorkspacePath)) !== null) return false;
384
391
  const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
385
392
  if (!workspaceBasisRefsAreCurrent(resolvedWorkspacePath, receipt ?? { version: 3, mounts: [] })) return false;
386
393
  if (head === null) return true;
387
394
  if (!receipt) return false;
388
395
  return mounts ? configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts).every(
389
- (mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean(mount.busy?.() && workspaceHydrationReceiptMountBasisHead(receipt, mount))
390
- ) : Boolean(receipt && receipt.mounts.every((mount) => mount.head === head));
396
+ (mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean((tolerateIdleRecordedBasis || mount.busy?.()) && workspaceHydrationReceiptMountBasisHead(receipt, mount))
397
+ ) : Boolean(receipt && receipt.mounts.every((mount) => tolerateIdleRecordedBasis || mount.head === head));
391
398
  }
392
399
  const WORKSPACE_GIT_BASIS_REF_PREFIX = "refs/r5d/workspace-basis/";
393
400
  const WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX = "refs/r5d/workspace-basis-staging/";
@@ -2145,6 +2152,7 @@ export {
2145
2152
  resetWorkspaceGit,
2146
2153
  synchronizeWorkspaceGit,
2147
2154
  workspaceGitHydrationIsCurrent,
2155
+ workspaceGitHydrationIsRecoverable,
2148
2156
  workspaceGitMountsForRemediation,
2149
2157
  workspaceGitSyncTestHarness
2150
2158
  };
@@ -69,6 +69,10 @@ type WorkspaceSyncResult = {
69
69
  synchronizeMs: number;
70
70
  };
71
71
  error?: string;
72
+ /** Typed reason for a failed outcome the server should surface loudly. */
73
+ failureReason?: "unsafe_hydration";
74
+ /** Consecutive in-process recovery bounces taken for unsafe hydration failures. */
75
+ recoveryAttempt?: number;
72
76
  };
73
77
  export type WorkerSessionTarget = {
74
78
  type: "project";
@@ -356,6 +360,9 @@ type WorkerCodeReadResult = {
356
360
  mtime: string;
357
361
  base64: string;
358
362
  };
363
+ declare class WorkspaceSyncAdmissionChangedError extends Error {
364
+ readonly name = "WorkspaceSyncAdmissionChangedError";
365
+ }
359
366
  declare function assertWorkerChildAdmission(input: {
360
367
  capturedGeneration: number;
361
368
  currentGeneration: number;
@@ -609,14 +616,29 @@ declare function deferredIncidentWorkspaceConfigurationResult(input: {
609
616
  declare function workspaceSyncFailureHydrationIsSafe(input: {
610
617
  error: unknown;
611
618
  resetToCanonical: boolean;
612
- inspectCurrentHydration: () => boolean;
619
+ inspectRecoverableHydration: () => boolean;
613
620
  }): boolean;
621
+ declare function unsafeWorkspaceSyncRecoveryBounceDelayMs(attempt: number): number;
622
+ type UnsafeWorkspaceSyncFailureResponse = "fatal_exit" | "recovery_bounce" | "recovery_pending";
623
+ declare function unsafeWorkspaceSyncFailureResponse(input: {
624
+ resetToCanonical: boolean;
625
+ recoveryAttempts: number;
626
+ maxRecoveryAttempts: number;
627
+ hasRetainableWork: boolean;
628
+ bouncePending: boolean;
629
+ }): UnsafeWorkspaceSyncFailureResponse;
614
630
  declare function fenceUnsafeWorkspaceSyncFailure(input: {
615
- hydrationCurrent: boolean;
631
+ hydrationRecoverable: boolean;
632
+ resetToCanonical: boolean;
633
+ recoveryAttempts: number;
634
+ maxRecoveryAttempts: number;
635
+ hasRetainableWork: boolean;
636
+ bouncePending: boolean;
616
637
  invalidateExecution: () => void;
638
+ scheduleRecoveryBounce: () => void;
617
639
  fatalExit?: (exitCode: number) => void;
618
640
  exitCode?: number;
619
- }): boolean;
641
+ }): "none" | "recovery_bounce" | "recovery_pending";
620
642
  export declare const workerGitSecurityTestHarness: {
621
643
  commandArgs: (args: string[]) => string[];
622
644
  credentialHelperForRemotes: typeof credentialHelperForRemotes;
@@ -641,6 +663,11 @@ export declare const workerGitSecurityTestHarness: {
641
663
  credentialReapContractStatus: typeof credentialReapContractStatus;
642
664
  verifiedCredentialReapContract: typeof verifiedCredentialReapContract;
643
665
  fenceUnsafeWorkspaceSyncFailure: typeof fenceUnsafeWorkspaceSyncFailure;
666
+ unsafeWorkspaceSyncFailureResponse: typeof unsafeWorkspaceSyncFailureResponse;
667
+ unsafeWorkspaceSyncRecoveryBounceDelayMs: typeof unsafeWorkspaceSyncRecoveryBounceDelayMs;
668
+ WORKSPACE_UNSAFE_SYNC_MAX_RECOVERY_ATTEMPTS: number;
669
+ WORKSPACE_UNSAFE_SYNC_RECOVERY_CLOSE_CODE: number;
670
+ WorkspaceSyncAdmissionChangedError: typeof WorkspaceSyncAdmissionChangedError;
644
671
  workspaceConfigurationIncidentDeferral: typeof workspaceConfigurationIncidentDeferral;
645
672
  workspaceIncidentTerminalClearDisposition: typeof workspaceIncidentTerminalClearDisposition;
646
673
  workspaceIncidentTerminalRefreshFence: typeof workspaceIncidentTerminalRefreshFence;
@@ -71,6 +71,16 @@ export declare class WorkspaceRemediationAncestryError extends Error {
71
71
  declare function gitCommandArgs(args: string[]): string[];
72
72
  declare function workspaceCloneCommandArgs(args: string[], remoteUrl: string, credentialHelper?: string | null, credentialUsername?: string | null): string[];
73
73
  export declare function workspaceGitHydrationIsCurrent(workspacePath: string, mounts?: readonly WorkspaceGitMount[]): boolean;
74
+ /**
75
+ * Failure-time variant of the currency check. A mount pinned at a valid
76
+ * recorded basis is convergent whether or not it is busy right now: the next
77
+ * cycle merges it forward from that exact basis and a genuine collision
78
+ * surfaces as a projection_merge conflict, never a silent overwrite. Live
79
+ * busy-ness must not decide safety here because the same disconnect that
80
+ * aborts a sync also clears the process/PTY maps busy-ness is computed from.
81
+ * Configuration-time recovery keeps the strict predicate.
82
+ */
83
+ export declare function workspaceGitHydrationIsRecoverable(workspacePath: string, mounts?: readonly WorkspaceGitMount[]): boolean;
74
84
  declare function configureWorkspaceRepository(input: {
75
85
  workspacePath: string;
76
86
  remoteUrl: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.88",
3
+ "version": "0.0.90",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",
@@ -1,49 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
- var heartbeat_exports = {};
20
- __export(heartbeat_exports, {
21
- WORKER_HEARTBEAT_INTERVAL_MS: () => WORKER_HEARTBEAT_INTERVAL_MS,
22
- WORKER_HEARTBEAT_TIMEOUT_MS: () => WORKER_HEARTBEAT_TIMEOUT_MS,
23
- grantWorkerHeartbeatBusyGrace: () => grantWorkerHeartbeatBusyGrace,
24
- hasWorkerHeartbeatTimedOut: () => hasWorkerHeartbeatTimedOut,
25
- hasWorkerHeartbeatTimedOutWithBusyGrace: () => hasWorkerHeartbeatTimedOutWithBusyGrace
26
- });
27
- module.exports = __toCommonJS(heartbeat_exports);
28
- const WORKER_HEARTBEAT_INTERVAL_MS = 15e3;
29
- const WORKER_HEARTBEAT_TIMEOUT_MS = 45e3;
30
- function hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
31
- return lastHeartbeatAt !== null && now - lastHeartbeatAt >= timeoutMs;
32
- }
33
- function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt, currentGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS, graceMs = WORKER_HEARTBEAT_INTERVAL_MS) {
34
- if (!hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs)) return currentGrace;
35
- if (currentGrace?.heartbeatAt === lastHeartbeatAt) return currentGrace;
36
- return { heartbeatAt: lastHeartbeatAt, expiresAt: now + graceMs };
37
- }
38
- function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt, busyGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
39
- if (lastHeartbeatAt !== null && busyGrace?.heartbeatAt === lastHeartbeatAt && now < busyGrace.expiresAt) return false;
40
- return hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs);
41
- }
42
- // Annotate the CommonJS export names for ESM import in node:
43
- 0 && (module.exports = {
44
- WORKER_HEARTBEAT_INTERVAL_MS,
45
- WORKER_HEARTBEAT_TIMEOUT_MS,
46
- grantWorkerHeartbeatBusyGrace,
47
- hasWorkerHeartbeatTimedOut,
48
- hasWorkerHeartbeatTimedOutWithBusyGrace
49
- });
@@ -1,21 +0,0 @@
1
- const WORKER_HEARTBEAT_INTERVAL_MS = 15e3;
2
- const WORKER_HEARTBEAT_TIMEOUT_MS = 45e3;
3
- function hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
4
- return lastHeartbeatAt !== null && now - lastHeartbeatAt >= timeoutMs;
5
- }
6
- function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt, currentGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS, graceMs = WORKER_HEARTBEAT_INTERVAL_MS) {
7
- if (!hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs)) return currentGrace;
8
- if (currentGrace?.heartbeatAt === lastHeartbeatAt) return currentGrace;
9
- return { heartbeatAt: lastHeartbeatAt, expiresAt: now + graceMs };
10
- }
11
- function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt, busyGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
12
- if (lastHeartbeatAt !== null && busyGrace?.heartbeatAt === lastHeartbeatAt && now < busyGrace.expiresAt) return false;
13
- return hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs);
14
- }
15
- export {
16
- WORKER_HEARTBEAT_INTERVAL_MS,
17
- WORKER_HEARTBEAT_TIMEOUT_MS,
18
- grantWorkerHeartbeatBusyGrace,
19
- hasWorkerHeartbeatTimedOut,
20
- hasWorkerHeartbeatTimedOutWithBusyGrace
21
- };
@@ -1,10 +0,0 @@
1
- export declare const WORKER_HEARTBEAT_INTERVAL_MS = 15000;
2
- export declare const WORKER_HEARTBEAT_TIMEOUT_MS = 45000;
3
- export declare function hasWorkerHeartbeatTimedOut(lastHeartbeatAt: number | null, now?: number, timeoutMs?: number): boolean;
4
- export type WorkerHeartbeatBusyGrace = {
5
- heartbeatAt: number;
6
- expiresAt: number;
7
- };
8
- /** Grant one bounded grace period for one actual server-heartbeat generation. */
9
- export declare function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt: number | null, currentGrace: WorkerHeartbeatBusyGrace | null, now?: number, timeoutMs?: number, graceMs?: number): WorkerHeartbeatBusyGrace | null;
10
- export declare function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt: number | null, busyGrace: WorkerHeartbeatBusyGrace | null, now?: number, timeoutMs?: number): boolean;