@devrouter/cli 0.0.65 → 0.0.67

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.
@@ -1188,6 +1188,17 @@ function inspectDevsyRuntimeStatus(devsyId) {
1188
1188
  if (result.status !== 0 || result.error) return "unknown";
1189
1189
  return parseDevsyRuntimeStatus(result.stdout, devsyId);
1190
1190
  }
1191
+ function inspectDevsyRuntimeAbsence(devsyId) {
1192
+ const result = (0, import_node_child_process5.spawnSync)("devsy", ["workspace", "status", devsyId, "--result-format", "json"], {
1193
+ encoding: "utf-8",
1194
+ timeout: 1e4
1195
+ });
1196
+ if (!result.error && result.status === 0) {
1197
+ return parseDevsyRuntimeStatus(result.stdout, devsyId) === "not-found";
1198
+ }
1199
+ const output = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n");
1200
+ return /workspace not found/i.test(output);
1201
+ }
1191
1202
  function inspectDevsyWorkspaceOwnership(workspaces, devsyId, worktreePath) {
1192
1203
  const idOwners = workspaces.filter((workspace) => workspace.id === devsyId);
1193
1204
  const pathOwners = workspaces.filter(
@@ -1987,6 +1998,7 @@ function requireReadyDevsyAgent(options = {}) {
1987
1998
  // src/core/managed-devsy-stop.ts
1988
1999
  var import_node_os4 = __toESM(require("os"));
1989
2000
  var import_node_path20 = __toESM(require("path"));
2001
+ var import_node_util4 = require("util");
1990
2002
 
1991
2003
  // src/core/devcontainer-profile.ts
1992
2004
  var import_node_child_process10 = require("child_process");
@@ -3202,7 +3214,16 @@ function registration(state) {
3202
3214
  throw new Error("Stop workspace identity changed.");
3203
3215
  resolveGitCommonDir(state.repoPath);
3204
3216
  } else if (state.workspace !== void 0) throw new Error("Stop checkout identity changed.");
3205
- const value = owner.workspace;
3217
+ return identityOf(owner.workspace);
3218
+ }
3219
+ function managedRunnerId(uid, providerId) {
3220
+ if (uid === void 0) {
3221
+ return providerId;
3222
+ }
3223
+ const bytes = Buffer.byteLength(uid);
3224
+ return bytes === 16 || bytes === 40 ? uid : providerId;
3225
+ }
3226
+ function identityOf(value) {
3206
3227
  return {
3207
3228
  context: value.context ?? "",
3208
3229
  providerId: value.id,
@@ -3347,6 +3368,64 @@ function absentRegistrationIdentity(state) {
3347
3368
  }
3348
3369
  return { record: record5, gitCommonDir: resolveGitCommonDir(state.repoPath) };
3349
3370
  }
3371
+ function replacementObservation(state) {
3372
+ resetWorkspaceRuntimeCaches();
3373
+ if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
3374
+ throw new Error("Stop provider changed.");
3375
+ if (!state.workspace || !isLinkedWorktree(state.repoPath))
3376
+ throw new Error("Replacement stop requires a linked workspace.");
3377
+ const record5 = readWorkspaceOwnership(state.repoPath, state.workspace);
3378
+ if (!record5 || record5.workspace !== state.workspace || record5.devpodId !== state.devpodId || !sameWorkspacePath(record5.worktreePath, state.repoPath) || resolveWorktreeWorkspace(state.repoPath) !== state.workspace || inspectWorkspaceOwnership(record5, listGitWorktrees(state.repoPath), void 0).ownerStatus !== "present")
3379
+ throw new Error("Replacement stop workspace ownership changed.");
3380
+ const owner = inspectDevsyWorkspaceOwnership(
3381
+ listDevsyWorkspaces(),
3382
+ state.devpodId,
3383
+ state.repoPath
3384
+ );
3385
+ if (owner.status !== "owned")
3386
+ throw new Error("Replacement stop requires one exact provider registration.");
3387
+ if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
3388
+ (entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
3389
+ ))
3390
+ throw new Error("Replacement stop requires no competing provider registration.");
3391
+ proveLocalDockerSelection(owner.workspace);
3392
+ return owner.workspace;
3393
+ }
3394
+ function proveReplacementStopAbsence(state, baseline, replacement) {
3395
+ const observation = () => {
3396
+ const workspace = replacementObservation(state);
3397
+ if (workspace.uid !== replacement.uid)
3398
+ throw new Error("Replacement stop registration changed during inspection.");
3399
+ if (resolveManagedStopEndpoint() !== baseline.endpoint)
3400
+ throw new Error("Stop endpoint changed.");
3401
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
3402
+ throw new Error("Stop daemon changed.");
3403
+ return workspace;
3404
+ };
3405
+ const before = observation();
3406
+ if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
3407
+ throw new Error("Retained stop generation changed.");
3408
+ assertManagedStopContainersAbsent(
3409
+ baseline.endpoint,
3410
+ baseline.containers.map((container) => container.id)
3411
+ );
3412
+ const oldRunner = managedRunnerId(baseline.uid, baseline.providerId);
3413
+ const newRunner = managedRunnerId(before.uid, baseline.providerId);
3414
+ const runners = oldRunner === newRunner ? [oldRunner] : [oldRunner, newRunner];
3415
+ const assertEmptyPopulation = () => {
3416
+ if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || runners.some((id3) => inspectProviderRunnerContainers(baseline.endpoint, id3).length !== 0))
3417
+ throw new Error("Replacement stop observed a remaining workspace population.");
3418
+ };
3419
+ assertEmptyPopulation();
3420
+ const after = observation();
3421
+ if (after.uid !== before.uid || !(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
3422
+ throw new Error("Replacement stop authority changed during inspection.");
3423
+ assertManagedStopContainersAbsent(
3424
+ baseline.endpoint,
3425
+ baseline.containers.map((container) => container.id)
3426
+ );
3427
+ assertEmptyPopulation();
3428
+ }
3350
3429
  function proveManagedStop(state) {
3351
3430
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
3352
3431
  const owner = inspectDevsyWorkspaceOwnership(
@@ -3354,6 +3433,15 @@ function proveManagedStop(state) {
3354
3433
  state.devpodId,
3355
3434
  state.repoPath
3356
3435
  );
3436
+ if (owner.status === "owned") {
3437
+ const identityKeys = ["context", "providerId", "uid", "sourcePath", "sourceContainer"];
3438
+ const current = identityOf(owner.workspace);
3439
+ const changed = identityKeys.filter((key) => baseline[key] !== current[key]);
3440
+ if (changed.length === 1 && changed[0] === "uid" && !baseline.sourceContainer && !current.sourceContainer && state.workspace !== void 0 && isLinkedWorktree(state.repoPath)) {
3441
+ proveReplacementStopAbsence(state, baseline, { uid: current.uid || void 0 });
3442
+ return { status: "proven-absent", containers: [] };
3443
+ }
3444
+ }
3357
3445
  if (owner.status !== "absent")
3358
3446
  return { status: "retained", containers: proveRetainedManagedStop(state) };
3359
3447
  if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
@@ -4787,7 +4875,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs14.default.rea
4787
4875
  const config = parseConfig(parsed ?? {}, configPath);
4788
4876
  const requiredVersion = config.devrouter?.version;
4789
4877
  if (requiredVersion && !hasWarnedVersionMismatch) {
4790
- const cliVersion = true ? "0.0.65" : "0.0.0-dev";
4878
+ const cliVersion = true ? "0.0.67" : "0.0.0-dev";
4791
4879
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4792
4880
  hasWarnedVersionMismatch = true;
4793
4881
  process.stderr.write(
@@ -5342,6 +5430,66 @@ function stopRetainedManagedDevsyWorkspace(options) {
5342
5430
  }
5343
5431
  return owner.workspace;
5344
5432
  }
5433
+ function registrationStatus() {
5434
+ resetWorkspaceRuntimeCaches();
5435
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
5436
+ throw new Error("Managed stop provider selection changed.");
5437
+ }
5438
+ return inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
5439
+ }
5440
+ function proveAbsentRegistrationStop() {
5441
+ const observe = () => {
5442
+ const owner = registrationStatus();
5443
+ if (owner.status === "conflict") throw new Error(owner.reason);
5444
+ if (owner.status !== "absent") {
5445
+ throw new Error("Managed stop absence proof requires the registration to remain absent.");
5446
+ }
5447
+ if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
5448
+ (entry) => entry.id === devsyId || sameWorkspacePath(entry.source.localFolder, repoPath)
5449
+ )) {
5450
+ throw new Error(
5451
+ "Managed stop absence proof requires both provider registrations to remain absent."
5452
+ );
5453
+ }
5454
+ if (!inspectDevsyRuntimeAbsence(devsyId)) {
5455
+ throw new Error(
5456
+ "Managed stop absence proof requires Devsy to report the runtime not-found."
5457
+ );
5458
+ }
5459
+ if (inspectManagedStopContainers(state.composeProject).length !== 0) {
5460
+ throw new Error("Managed stop absence proof observed a remaining compose population.");
5461
+ }
5462
+ if (inspectProviderRunnerContainers(resolveManagedStopEndpoint(), devsyId).length !== 0) {
5463
+ throw new Error("Managed stop absence proof observed a remaining runner population.");
5464
+ }
5465
+ const owned = inspectWorkspaceContainers().filter(
5466
+ (container) => sameWorkspacePath(
5467
+ container.labels["com.docker.compose.project.working_dir"] ?? "",
5468
+ import_node_path20.default.join(repoPath, ".devcontainer")
5469
+ )
5470
+ );
5471
+ if (owned.length !== 0) {
5472
+ throw new Error("Managed stop absence proof observed a remaining workspace container.");
5473
+ }
5474
+ if (workspace) {
5475
+ const record5 = readWorkspaceOwnership(repoPath, workspace);
5476
+ if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present") {
5477
+ throw new Error("Managed stop workspace ownership changed during absence proof.");
5478
+ }
5479
+ }
5480
+ if (!(0, import_node_util4.isDeepStrictEqual)(readManagedRuntimeState(repoPath, workspace), state)) {
5481
+ throw new Error("Managed stop retained state changed during absence proof.");
5482
+ }
5483
+ };
5484
+ observe();
5485
+ observe();
5486
+ }
5487
+ const initialOwner = registrationStatus();
5488
+ if (initialOwner.status === "conflict") throw new Error(initialOwner.reason);
5489
+ if (initialOwner.status === "absent") {
5490
+ proveAbsentRegistrationStop();
5491
+ return "proven-absent";
5492
+ }
5345
5493
  const context = registration2().context;
5346
5494
  if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
5347
5495
  throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
@@ -7320,6 +7468,7 @@ function assertReliabilityEvent(value) {
7320
7468
  case "launched":
7321
7469
  case "interrupted":
7322
7470
  case "not-started":
7471
+ case "settle":
7323
7472
  valid = isReliabilityId(value.operationId);
7324
7473
  break;
7325
7474
  case "completion":
@@ -7389,8 +7538,13 @@ function transition(state, outcome, effects = []) {
7389
7538
  assertReliabilityState(state);
7390
7539
  return { state, effects, outcome };
7391
7540
  }
7392
- function unchanged(state, outcome) {
7393
- return transition(cloneState(state), outcome);
7541
+ function unchanged(state, outcome, reason2) {
7542
+ const result = transition(cloneState(state), outcome);
7543
+ if (reason2) result.reason = reason2;
7544
+ return result;
7545
+ }
7546
+ function blocked(state, reason2) {
7547
+ return unchanged(state, "blocked", reason2);
7394
7548
  }
7395
7549
  function advance(value) {
7396
7550
  return value === Number.MAX_SAFE_INTEGER ? null : value + 1;
@@ -7415,7 +7569,10 @@ function updateRunningPhase(state) {
7415
7569
  }
7416
7570
  function handleRequest(state, event) {
7417
7571
  if (state.executionPolicy === "manual" && event.mode === "start")
7418
- return unchanged(state, "blocked");
7572
+ return blocked(
7573
+ state,
7574
+ "start requests are not admitted under manual execution policy; use the ensure/stop commands."
7575
+ );
7419
7576
  const existing = state.requests.find((request) => request.key === event.key);
7420
7577
  if (existing) {
7421
7578
  const consumer = state.consumers.find((candidate) => candidate.id === existing.consumerId);
@@ -7482,7 +7639,8 @@ function handleRequest(state, event) {
7482
7639
  }
7483
7640
  function canExecAfterInterruptedEnsure(state) {
7484
7641
  const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
7485
- return state.executionPolicy === "manual" && state.desired === "running" && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && state.phase === "recovering" || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
7642
+ const quiescent = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
7643
+ return state.executionPolicy === "manual" && (state.desired === "running" || quiescent && state.desired === "stopped-by-user") && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && (state.phase === "recovering" || quiescent) || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
7486
7644
  }
7487
7645
  function handleOperationRequest(state, event) {
7488
7646
  if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
@@ -7495,25 +7653,39 @@ function handleOperationRequest(state, event) {
7495
7653
  if (state.operationHistory.some((entry) => entry.id === event.operationId))
7496
7654
  return unchanged(state, "conflict");
7497
7655
  if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
7498
- return unchanged(state, "blocked");
7656
+ return blocked(
7657
+ state,
7658
+ "the latest ensure is INTERRUPTED and drained; exec requires recovering it first (run ensure, or exec with recovery proof)."
7659
+ );
7499
7660
  const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
7500
7661
  const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
7501
7662
  if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure && !(event.kind === "exec" && event.recoverInterruptedEnsure === true && canExecAfterInterruptedEnsure(state))))
7502
- return unchanged(state, "blocked");
7663
+ return blocked(
7664
+ state,
7665
+ `operation ${state.operation.id} is ${state.operation.status} (drained=${state.operation.drained}) and no stop proof, ensure reconciliation, or exec recovery supersedes it.`
7666
+ );
7503
7667
  if (state.phase === "stopping" || state.desired === "parked-for-capacity")
7504
- return unchanged(state, "blocked");
7668
+ return blocked(
7669
+ state,
7670
+ `phase is '${state.phase}' with desired '${state.desired}'; wait for the running stop to finish.`
7671
+ );
7505
7672
  if (event.kind === "exec" && (!event.runtimeRunning || state.desired !== "running" && state.intentRevision !== 0))
7506
- return unchanged(state, "blocked");
7673
+ return blocked(
7674
+ state,
7675
+ `exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
7676
+ );
7507
7677
  const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
7508
7678
  let retired = -1;
7509
7679
  if (rollover) {
7510
- if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
7511
- return unchanged(state, "blocked");
7512
7680
  const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
7513
7681
  retired = state.operationHistory.findIndex(
7514
- (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
7682
+ (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
7515
7683
  );
7516
- if (retired === -1) return unchanged(state, "blocked");
7684
+ if (retired === -1)
7685
+ return blocked(
7686
+ state,
7687
+ `journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
7688
+ );
7517
7689
  }
7518
7690
  if (state.desired !== "running" || rollover) {
7519
7691
  const revision = advance(state.intentRevision);
@@ -7568,7 +7740,10 @@ function handleDispatch(state) {
7568
7740
  if (state.operation?.drained || state.operation?.status !== "NOT_STARTED")
7569
7741
  return unchanged(state, "stale");
7570
7742
  if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && state.admission !== "admitted" || state.consumers.length === 0) {
7571
- return unchanged(state, "blocked");
7743
+ return blocked(
7744
+ state,
7745
+ `dispatch requires desired 'running' with a registered consumer (desired='${state.desired}', consumers=${state.consumers.length}).`
7746
+ );
7572
7747
  }
7573
7748
  const corrective = state.phase === "recovering" && state.incident !== null;
7574
7749
  state.operation = { ...state.operation, status: "DISPATCH_PENDING", exitCode: null };
@@ -7583,7 +7758,10 @@ function handleDispatchPersisted(state, event) {
7583
7758
  if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && (state.admission !== "admitted" || !state.chargeHeld)) {
7584
7759
  state.operation = setUnknown(state.operation);
7585
7760
  state.chargeHeld = state.executionPolicy === "capacity-managed";
7586
- return transition(state, "blocked");
7761
+ return blocked(
7762
+ state,
7763
+ `dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
7764
+ );
7587
7765
  }
7588
7766
  if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
7589
7767
  return unchanged(state, "blocked");
@@ -7631,6 +7809,22 @@ function handleInterrupted(state, event) {
7631
7809
  if (state.desired === "running") state.phase = "recovering";
7632
7810
  return transition(state, "accepted");
7633
7811
  }
7812
+ function handleSettle(state, event) {
7813
+ if (state.executionPolicy !== "manual") return unchanged(state, "stale");
7814
+ if (!state.operation || state.operation.id !== event.operationId)
7815
+ return unchanged(state, "stale");
7816
+ if (state.operation.drained && state.operation.status === "INTERRUPTED")
7817
+ return unchanged(state, "joined");
7818
+ if (state.operation.status === "COMPLETED") return unchanged(state, "joined");
7819
+ state.operation = {
7820
+ ...state.operation,
7821
+ status: "INTERRUPTED",
7822
+ drained: true,
7823
+ exitCode: null
7824
+ };
7825
+ if (state.desired === "running") state.phase = "recovering";
7826
+ return transition(state, "accepted");
7827
+ }
7634
7828
  function handleObservation(state, event, nowMs) {
7635
7829
  if (state.desired !== "running") return unchanged(state, "stale");
7636
7830
  const previous = state.observations.find(
@@ -7640,7 +7834,10 @@ function handleObservation(state, event, nowMs) {
7640
7834
  return unchanged(state, "stale");
7641
7835
  }
7642
7836
  if (!previous && state.observations.length >= RELIABILITY_MAX_ITEMS)
7643
- return unchanged(state, "blocked");
7837
+ return blocked(
7838
+ state,
7839
+ `observation set holds ${state.observations.length} distinct capabilities; observations repeat per capability and never retire.`
7840
+ );
7644
7841
  state.observations = state.observations.filter(
7645
7842
  (observation) => observation.capability !== event.observation.capability
7646
7843
  );
@@ -7668,7 +7865,10 @@ function handleStop(state) {
7668
7865
  function handleStopProof(state, event) {
7669
7866
  if (state.desired === "running") return unchanged(state, "stale");
7670
7867
  if (state.executionPolicy === "manual" && state.operation && !state.operation.drained && event.workloadsStopped && event.routesRemoved)
7671
- return unchanged(state, "blocked");
7868
+ return blocked(
7869
+ state,
7870
+ `stop proof cannot settle while operation ${state.operation.id} is not drained; wait for its worker or settle the journal.`
7871
+ );
7672
7872
  const previous = { ...state.stopProof };
7673
7873
  if (previous.workloadsStopped === event.workloadsStopped && previous.routesRemoved === event.routesRemoved)
7674
7874
  return unchanged(state, "joined");
@@ -7847,6 +8047,8 @@ function applyReliabilityEvent(state, event, nowMs) {
7847
8047
  return handleCompletion(next, event);
7848
8048
  case "interrupted":
7849
8049
  return handleInterrupted(next, event);
8050
+ case "settle":
8051
+ return handleSettle(next, event);
7850
8052
  case "observation":
7851
8053
  return handleObservation(next, event, nowMs);
7852
8054
  case "stop-proof":
@@ -7882,6 +8084,19 @@ function stepReliability(state, event, nowMs) {
7882
8084
  var import_node_crypto11 = require("crypto");
7883
8085
  var import_node_fs20 = __toESM(require("fs"));
7884
8086
  var import_node_path25 = __toESM(require("path"));
8087
+ function installedCliVersion() {
8088
+ return true ? "0.0.67" : "0.0.0-dev";
8089
+ }
8090
+ function compareCliVersions(left, right) {
8091
+ const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
8092
+ const leftParts = parts(left);
8093
+ const rightParts = parts(right);
8094
+ for (let index = 0; index < 3; index += 1) {
8095
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
8096
+ if (difference !== 0) return difference;
8097
+ }
8098
+ return 0;
8099
+ }
7885
8100
  var MAX_RECORD_BYTES = 1048576;
7886
8101
  function identityKey(identity2) {
7887
8102
  if (!import_node_path25.default.isAbsolute(identity2.repoPath) || import_node_path25.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
@@ -7898,7 +8113,21 @@ function keys(value, expected) {
7898
8113
  }
7899
8114
  }
7900
8115
  function validate(record5, identity2) {
7901
- keys(record5, ["version", "identity", "revision", "state", "worker", "effectSequence", "outcome"]);
8116
+ keys(record5, [
8117
+ "version",
8118
+ "identity",
8119
+ "revision",
8120
+ "state",
8121
+ "worker",
8122
+ "effectSequence",
8123
+ "outcome",
8124
+ "writtenByVersion"
8125
+ ]);
8126
+ if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
8127
+ throw new Error(
8128
+ `Reliability record was last written by devrouter ${record5.writtenByVersion}, which is newer than the installed ${installedCliVersion()}; upgrade devrouter (devrouter upgrade) before running lifecycle commands.`
8129
+ );
8130
+ }
7902
8131
  keys(record5.identity, ["repoPath", "workspace", "provider"]);
7903
8132
  if (record5.version !== 1 || record5.identity.repoPath !== identity2.repoPath || record5.identity.workspace !== identity2.workspace || record5.identity.provider !== identity2.provider || !isReliabilityCounter(record5.revision) || !isReliabilityCounter(record5.effectSequence)) {
7904
8133
  throw new Error("Reliability record has invalid version, ownership, or counters.");
@@ -8039,6 +8268,7 @@ function updateReliabilityOperation(identity2, operation) {
8039
8268
  if (result && typeof result.then === "function") {
8040
8269
  throw new Error("Reliability record updates must be synchronous.");
8041
8270
  }
8271
+ record5.writtenByVersion = installedCliVersion();
8042
8272
  validate(record5, identity2);
8043
8273
  record5.revision += 1;
8044
8274
  persist(record5);
@@ -12785,7 +13015,9 @@ async function withLifecycleOperationLock(repoPath, operation) {
12785
13015
  function stepRecord(record5, event) {
12786
13016
  const transition2 = stepReliability(record5.state, event, Date.now());
12787
13017
  if (!["accepted", "joined"].includes(transition2.outcome))
12788
- throw new Error(`Lifecycle transition is ${transition2.outcome}.`);
13018
+ throw new Error(
13019
+ `Lifecycle transition is ${transition2.outcome}.${transition2.reason ? ` ${transition2.reason}` : ""}`
13020
+ );
12789
13021
  record5.state = transition2.state;
12790
13022
  }
12791
13023
  function reconcileDrained(record5) {