@devrouter/cli 0.0.77 → 0.0.79

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.
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
 
25
25
  // src/core/devpod-exec.ts
26
26
  var import_node_child_process29 = require("child_process");
27
- var import_node_crypto21 = require("crypto");
27
+ var import_node_crypto22 = require("crypto");
28
28
 
29
29
  // src/core/devpod-environment.ts
30
30
  var import_node_child_process3 = require("child_process");
@@ -350,9 +350,25 @@ function acquireFileLock(lockPath, options) {
350
350
  }
351
351
  if (options.onWait && now - lastProgressAt >= progressIntervalMs) {
352
352
  lastProgressAt = now;
353
+ let holder;
354
+ try {
355
+ const bytes2 = import_node_fs2.default.readFileSync(lockPath, "utf-8");
356
+ const record6 = parseLockRecord(bytes2);
357
+ if (record6.owner && isLockOwnerLive(record6.owner, livenessCache) && import_node_fs2.default.readFileSync(lockPath, "utf-8") === bytes2) {
358
+ holder = {
359
+ kind: "live",
360
+ pid: record6.owner.pid,
361
+ acquiredAtMs: record6.acquiredAtMs
362
+ };
363
+ }
364
+ } catch {
365
+ }
353
366
  options.onWait({
354
367
  waitingMs: now - waitStartedAt,
355
- holderPid: queueState.leaderPid,
368
+ remainingWaitMs: Math.max(0, deadline - now),
369
+ queueLeaderPid: queueState.leaderPid,
370
+ holderPid: holder?.pid,
371
+ holderHeldMs: holder?.acquiredAtMs !== void 0 ? Math.max(0, now - holder.acquiredAtMs) : void 0,
356
372
  queuePosition: queueState.position,
357
373
  waitingOn: "queue"
358
374
  });
@@ -390,6 +406,7 @@ function acquireFileLock(lockPath, options) {
390
406
  lastProgressAt = now;
391
407
  options.onWait({
392
408
  waitingMs: now - waitStartedAt,
409
+ remainingWaitMs: Math.max(0, deadline - now),
393
410
  holderPid: state.pid,
394
411
  holderHeldMs: state.acquiredAtMs !== void 0 ? Math.max(0, now - state.acquiredAtMs) : void 0,
395
412
  queuePosition: queueState?.position,
@@ -444,9 +461,11 @@ async function withFileLock(lockPath, options, operation) {
444
461
  function createStderrWaitReporter(activity, target) {
445
462
  return (progress) => {
446
463
  const heldSeconds = progress.holderHeldMs !== void 0 ? `, held for ${Math.round(progress.holderHeldMs / 1e3)}s` : "";
447
- const status = progress.waitingOn === "queue" ? `waiting in provider queue position ${progress.queuePosition} led by PID ${progress.holderPid}` : progress.queuePosition !== void 0 && progress.queuePosition > 1 ? `waiting in provider queue position ${progress.queuePosition}; provider lock held by PID ${progress.holderPid}${heldSeconds}` : `waiting for the provider lock held by PID ${progress.holderPid}${heldSeconds}`;
464
+ const holder = progress.holderPid !== void 0 ? `provider lock held by PID ${progress.holderPid}${heldSeconds}` : "provider lock holder unknown";
465
+ const remaining = progress.remainingWaitMs !== void 0 ? `; queue acquisition timeout in ${Math.ceil(progress.remainingWaitMs / 1e3)}s (does not stop the holder)` : "";
466
+ const status = progress.waitingOn === "queue" ? `waiting in provider queue position ${progress.queuePosition}; first waiter PID ${progress.queueLeaderPid}; ${holder}` : progress.queuePosition !== void 0 && progress.queuePosition > 1 ? `waiting in provider queue position ${progress.queuePosition}; provider lock held by PID ${progress.holderPid}${heldSeconds}` : `waiting for the provider lock held by PID ${progress.holderPid}${heldSeconds}`;
448
467
  process.stderr.write(
449
- `${activity} for ${target}: ${status}; waited ${Math.round(progress.waitingMs / 1e3)}s so far
468
+ `${activity} for ${target}: ${status}; waited ${Math.round(progress.waitingMs / 1e3)}s so far${remaining}
450
469
  `
451
470
  );
452
471
  };
@@ -1841,6 +1860,21 @@ function resolveWorkspaceRuntimeDetailed(repoPath, override) {
1841
1860
  function resolveWorkspaceRuntimeOrDefault(repoPath) {
1842
1861
  return resolveWorkspaceRuntimeDetailed(repoPath).runtime;
1843
1862
  }
1863
+ function resolveWorkspaceRuntimeForReport(repoPath) {
1864
+ const snapshots = getWorkspaceRegistrySnapshots();
1865
+ const envRaw = process.env.DEVROUTER_WORKSPACE_RUNTIME?.trim();
1866
+ if (envRaw) {
1867
+ const runtime2 = parseWorkspaceRuntime(envRaw);
1868
+ return snapshots[runtime2] ? runtime2 : void 0;
1869
+ }
1870
+ if (repoPath) {
1871
+ const owner = pathOwnerRuntime(repoPath, snapshots);
1872
+ if (owner.status === "conflict" || owner.status === "unavailable") return void 0;
1873
+ if (owner.status === "owner") return owner.runtime;
1874
+ }
1875
+ const runtime = resolveWorkspaceRuntimeDetailed().runtime;
1876
+ return snapshots[runtime] ? runtime : void 0;
1877
+ }
1844
1878
  function resetWorkspaceRuntimeCaches() {
1845
1879
  cachedRuntime = void 0;
1846
1880
  cachedSnapshots = void 0;
@@ -2048,6 +2082,16 @@ function parseVersion(output) {
2048
2082
  /\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)\b/
2049
2083
  )?.[1];
2050
2084
  }
2085
+ function compareVersionCore(left, right) {
2086
+ const core = (value) => value.split(/[-+]/, 1)[0].split(".").map((part) => Number.parseInt(part, 10));
2087
+ const leftParts = core(left);
2088
+ const rightParts = core(right);
2089
+ for (let index = 0; index < 3; index += 1) {
2090
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
2091
+ if (difference !== 0) return difference > 0 ? 1 : -1;
2092
+ }
2093
+ return 0;
2094
+ }
2051
2095
  function devsyAgentRepairSuggestion(inspection) {
2052
2096
  if (inspection.state === "stale") {
2053
2097
  return `Install Devsy ${SUPPORTED_DEVSY_VERSION} for a supported host, then run: ${DEVSY_AGENT_SETUP_COMMAND}`;
@@ -2119,7 +2163,7 @@ function inspectDevsyAgent(options = {}) {
2119
2163
  const installedVersion = parseVersion(resolved.versionOutput);
2120
2164
  const explicitPath = resolved.env.DEVSY_AGENT_BINARY?.trim();
2121
2165
  const source = explicitPath ? "explicit" : "managed";
2122
- if (installedVersion !== SUPPORTED_DEVSY_VERSION) {
2166
+ if (installedVersion === void 0 || installedVersion !== SUPPORTED_DEVSY_VERSION && compareVersionCore(installedVersion, SUPPORTED_DEVSY_VERSION) <= 0) {
2123
2167
  return {
2124
2168
  state: "stale",
2125
2169
  source,
@@ -2135,6 +2179,15 @@ function inspectDevsyAgent(options = {}) {
2135
2179
  installedVersion
2136
2180
  };
2137
2181
  }
2182
+ if (installedVersion !== SUPPORTED_DEVSY_VERSION) {
2183
+ return {
2184
+ state: "ready",
2185
+ source: "host",
2186
+ reason: `installed Devsy ${installedVersion} is newer than the verified ${SUPPORTED_DEVSY_VERSION} agent; the host CLI governs its own agent`,
2187
+ installedVersion,
2188
+ drift: { installed: installedVersion, supported: SUPPORTED_DEVSY_VERSION }
2189
+ };
2190
+ }
2138
2191
  const expectedName = resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
2139
2192
  const asset = resolved.assets.find((candidate) => candidate.name === expectedName);
2140
2193
  if (!asset) {
@@ -2156,9 +2209,7 @@ function inspectDevsyAgent(options = {}) {
2156
2209
  }
2157
2210
  function requireReadyDevsyAgent(options = {}) {
2158
2211
  const inspection = inspectDevsyAgent(options);
2159
- if (inspection.state !== "ready" || !inspection.binaryPath || !inspection.asset) {
2160
- throw new DevsyAgentReadinessError(inspection);
2161
- }
2212
+ if (inspection.state !== "ready") throw new DevsyAgentReadinessError(inspection);
2162
2213
  return {
2163
2214
  binaryPath: inspection.binaryPath,
2164
2215
  source: inspection.source,
@@ -3963,6 +4014,45 @@ function proveReplacementStopAbsence(state, baseline, replacement) {
3963
4014
  );
3964
4015
  assertEmptyPopulation();
3965
4016
  }
4017
+ function proveAbsentPopulationStop(state, baseline, expected) {
4018
+ const observation = () => {
4019
+ const owner = inspectDevsyWorkspaceOwnership(
4020
+ listDevsyWorkspaces(),
4021
+ state.devpodId,
4022
+ state.repoPath
4023
+ );
4024
+ if (owner.status !== "owned")
4025
+ throw new Error("Absent stop registration changed during inspection.");
4026
+ const current = identityOf(owner.workspace);
4027
+ if (!(0, import_node_util3.isDeepStrictEqual)(current, expected))
4028
+ throw new Error("Absent stop registration identity changed during inspection.");
4029
+ if (resolveManagedStopEndpoint() !== baseline.endpoint)
4030
+ throw new Error("Stop endpoint changed.");
4031
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
4032
+ throw new Error("Stop daemon changed.");
4033
+ proveLocalDockerSelection(owner.workspace);
4034
+ return current;
4035
+ };
4036
+ const assertAbsentPopulation = () => {
4037
+ assertManagedStopContainersAbsent(
4038
+ baseline.endpoint,
4039
+ baseline.containers.map((container) => container.id)
4040
+ );
4041
+ if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(
4042
+ baseline.endpoint,
4043
+ managedRunnerId(baseline.uid, baseline.providerId)
4044
+ ).length !== 0)
4045
+ throw new Error("Absent stop observed a remaining workspace population.");
4046
+ };
4047
+ const before = observation();
4048
+ if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
4049
+ throw new Error("Retained stop generation changed.");
4050
+ assertAbsentPopulation();
4051
+ const after = observation();
4052
+ if (!(0, import_node_util3.isDeepStrictEqual)(after, before) || !(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
4053
+ throw new Error("Absent stop authority changed during inspection.");
4054
+ assertAbsentPopulation();
4055
+ }
3966
4056
  function proveManagedStop(state) {
3967
4057
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
3968
4058
  const owner = inspectDevsyWorkspaceOwnership(
@@ -3974,6 +4064,10 @@ function proveManagedStop(state) {
3974
4064
  const identityKeys = ["context", "providerId", "uid", "sourcePath", "sourceContainer"];
3975
4065
  const current = identityOf(owner.workspace);
3976
4066
  const changed = identityKeys.filter((key) => baseline[key] !== current[key]);
4067
+ if (changed.length === 0 && state.workspace !== void 0 && isLinkedWorktree(state.repoPath) && inspectManagedStopContainers(baseline.project, baseline.endpoint).length === 0) {
4068
+ proveAbsentPopulationStop(state, baseline, current);
4069
+ return { status: "proven-absent", containers: [] };
4070
+ }
3977
4071
  if (changed.length === 1 && changed[0] === "uid" && !baseline.sourceContainer && !current.sourceContainer && state.workspace !== void 0 && isLinkedWorktree(state.repoPath)) {
3978
4072
  proveReplacementStopAbsence(state, baseline, { uid: current.uid || void 0 });
3979
4073
  return { status: "proven-absent", containers: [] };
@@ -4068,6 +4162,9 @@ function validOperation(value) {
4068
4162
  "COMPLETION_UNKNOWN"
4069
4163
  ]) && (value.status === "COMPLETED" ? isReliabilityCounter(value.exitCode) : value.exitCode === null);
4070
4164
  }
4165
+ function validIncidentUnit(value) {
4166
+ return record2(value) && isReliabilityId(value.key) && (value.kind === "process" || value.kind === "service") && isReliabilityCounter(value.actions) && isReliabilityCounter(value.lastActionAtMs);
4167
+ }
4071
4168
  function assertReliabilityState(value) {
4072
4169
  const intentRevision = record2(value) ? value.intentRevision : void 0;
4073
4170
  const valid = record2(value) && value.contractVersion === RELIABILITY_CONTRACT_VERSION && oneOf(value.executionPolicy, ["manual", "capacity-managed"]) && (value.executionPolicy === "manual" ? value.admission === "not-applicable" && value.chargeHeld === false : value.admission !== "not-applicable") && boundedArray(value.operationHistory) && uniqueIds(value.operationHistory, "key") && uniqueIds(value.operationHistory, "id") && value.operationHistory.every(
@@ -4092,7 +4189,7 @@ function assertReliabilityState(value) {
4092
4189
  (request) => record2(request) && oneOf(request.mode, ["start", "attach"]) && isReliabilityId(request.consumerId) && isReliabilityId(request.operationId) && isReliabilityProfile(request.profile) && isReliabilityCounter(request.intentRevision) && isReliabilityCounter(intentRevision) && request.intentRevision <= intentRevision
4093
4190
  ) && boundedArray(value.observations) && uniqueIds(value.observations, "capability") && value.observations.every(
4094
4191
  (observation) => record2(observation) && oneOf(observation.infrastructure, ["healthy", "failed", "unknown"]) && oneOf(observation.application, ["verified", "unready", "unverified"]) && isReliabilityCounter(observation.observedAtMs) && isReliabilityCounter(observation.validForMs)
4095
- ) && (value.operation === null || validOperation(value.operation)) && (value.executionPolicy !== "manual" || value.desired !== "parked-for-capacity" && value.incident === null) && (value.incident === null || record2(value.incident) && isReliabilityId(value.incident.id) && isReliabilityCounter(value.incident.correctiveActionsTaken) && isReliabilityCounter(value.incident.actionLimit) && value.incident.actionLimit > 0 && value.incident.correctiveActionsTaken <= value.incident.actionLimit);
4192
+ ) && (value.operation === null || validOperation(value.operation)) && (value.executionPolicy !== "manual" || value.desired !== "parked-for-capacity" && value.incident === null) && (value.incident === null || record2(value.incident) && isReliabilityId(value.incident.id) && isReliabilityCounter(value.incident.correctiveActionsTaken) && isReliabilityCounter(value.incident.actionLimit) && value.incident.actionLimit > 0 && value.incident.correctiveActionsTaken <= value.incident.actionLimit && (value.incident.startedAtMs === void 0 || isReliabilityCounter(value.incident.startedAtMs)) && (value.incident.units === void 0 || boundedArray(value.incident.units) && uniqueIds(value.incident.units, "key") && value.incident.units.every(validIncidentUnit)));
4096
4193
  if (!valid) throw new Error("Invalid reliability state.");
4097
4194
  }
4098
4195
  function createReliabilityState(environmentId, controllerEpoch, executionPolicy = "capacity-managed") {
@@ -4195,6 +4292,15 @@ function evaluateCapacity(budgets, samples, charges, requested, nowMs, maxSample
4195
4292
 
4196
4293
  // src/core/capacity-store.ts
4197
4294
  var MAX_BYTES = 1048576;
4295
+ var CapacityHistoryError = class extends Error {
4296
+ constructor(code) {
4297
+ super(
4298
+ code === "capacity-ledger-lost" ? "Capacity ledger history is lost; restore verified capacity-reservations.json." : "Capacity history cannot be proven; inspect the private lifecycle journals."
4299
+ );
4300
+ this.code = code;
4301
+ this.name = "CapacityHistoryError";
4302
+ }
4303
+ };
4198
4304
  var CapacitySnapshotChangedError = class extends Error {
4199
4305
  constructor() {
4200
4306
  super("Capacity snapshot changed.");
@@ -4286,18 +4392,57 @@ function serializeSnapshot(snapshot) {
4286
4392
  return contents;
4287
4393
  }
4288
4394
  var CapacityStore = class {
4289
- constructor(directory) {
4395
+ constructor(directory, write = writeFileAtomically, minimumRevision = () => 0) {
4290
4396
  this.directory = directory;
4397
+ this.write = write;
4398
+ this.minimumRevision = minimumRevision;
4291
4399
  this.file = import_node_path19.default.join(directory, "capacity-reservations.json");
4400
+ this.establishedFile = import_node_path19.default.join(directory, "capacity-ledger.established");
4292
4401
  }
4293
4402
  file;
4403
+ establishedFile;
4404
+ observedLedger = false;
4405
+ readEstablished() {
4406
+ let descriptor;
4407
+ try {
4408
+ descriptor = import_node_fs15.default.openSync(
4409
+ this.establishedFile,
4410
+ import_node_fs15.default.constants.O_RDONLY | import_node_fs15.default.constants.O_NOFOLLOW | import_node_fs15.default.constants.O_NONBLOCK
4411
+ );
4412
+ } catch (error) {
4413
+ if (error.code === "ENOENT") return false;
4414
+ throw error;
4415
+ }
4416
+ try {
4417
+ const stat = import_node_fs15.default.fstatSync(descriptor);
4418
+ if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > 4096)
4419
+ throw new Error("Unsafe capacity ledger marker.");
4420
+ const bytes2 = Buffer.alloc(4097);
4421
+ const count = import_node_fs15.default.readSync(descriptor, bytes2, 0, bytes2.length, 0);
4422
+ if (count > 4096) throw new Error("Capacity ledger marker exceeds byte limit.");
4423
+ const marker = JSON.parse(bytes2.subarray(0, count).toString("utf8"));
4424
+ object(marker, ["version"]);
4425
+ if (marker.version !== 1) throw new Error("Invalid capacity ledger marker.");
4426
+ return true;
4427
+ } finally {
4428
+ import_node_fs15.default.closeSync(descriptor);
4429
+ }
4430
+ }
4294
4431
  read() {
4432
+ const minimumRevision = this.minimumRevision();
4433
+ const established = this.readEstablished();
4295
4434
  let descriptor;
4296
4435
  try {
4297
- descriptor = import_node_fs15.default.openSync(this.file, import_node_fs15.default.constants.O_RDONLY | import_node_fs15.default.constants.O_NOFOLLOW);
4436
+ descriptor = import_node_fs15.default.openSync(
4437
+ this.file,
4438
+ import_node_fs15.default.constants.O_RDONLY | import_node_fs15.default.constants.O_NOFOLLOW | import_node_fs15.default.constants.O_NONBLOCK
4439
+ );
4298
4440
  } catch (error) {
4299
- if (error.code === "ENOENT")
4441
+ if (error.code === "ENOENT") {
4442
+ if (established || this.observedLedger || minimumRevision > 0)
4443
+ throw new CapacityHistoryError("capacity-ledger-lost");
4300
4444
  return { version: 1, revision: 0, reservations: [] };
4445
+ }
4301
4446
  throw error;
4302
4447
  }
4303
4448
  try {
@@ -4309,11 +4454,40 @@ var CapacityStore = class {
4309
4454
  if (count > MAX_BYTES) throw new Error("Capacity reservation snapshot exceeds byte limit.");
4310
4455
  const value = JSON.parse(buffer.subarray(0, count).toString("utf8"));
4311
4456
  validate(value);
4457
+ if (value.revision < minimumRevision) throw new CapacityHistoryError("capacity-ledger-lost");
4458
+ this.observedLedger = true;
4312
4459
  return value;
4313
4460
  } finally {
4314
4461
  import_node_fs15.default.closeSync(descriptor);
4315
4462
  }
4316
4463
  }
4464
+ establish() {
4465
+ if (!this.readEstablished()) this.write(this.establishedFile, '{"version":1}\n');
4466
+ for (const file of [this.file, this.establishedFile, this.directory]) {
4467
+ const descriptor = import_node_fs15.default.openSync(
4468
+ file,
4469
+ import_node_fs15.default.constants.O_RDONLY | import_node_fs15.default.constants.O_NOFOLLOW | import_node_fs15.default.constants.O_NONBLOCK
4470
+ );
4471
+ try {
4472
+ const stat = import_node_fs15.default.fstatSync(descriptor);
4473
+ if ((file === this.directory ? !stat.isDirectory() : !stat.isFile()) || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0)
4474
+ throw new Error("Unsafe capacity ledger durability artifact.");
4475
+ import_node_fs15.default.fsyncSync(descriptor);
4476
+ } finally {
4477
+ import_node_fs15.default.closeSync(descriptor);
4478
+ }
4479
+ }
4480
+ }
4481
+ readForMutation() {
4482
+ const snapshot = this.read();
4483
+ if (this.observedLedger) this.establish();
4484
+ return snapshot;
4485
+ }
4486
+ commit(snapshot) {
4487
+ this.write(this.file, serializeSnapshot(snapshot));
4488
+ this.observedLedger = true;
4489
+ this.establish();
4490
+ }
4317
4491
  /**
4318
4492
  * Caller must revoke journal authority and prove exact physical cessation first.
4319
4493
  * An absent reservation still advances the revision to fence that proof.
@@ -4327,7 +4501,7 @@ var CapacityStore = class {
4327
4501
  `${this.file}.lock`,
4328
4502
  { activity: "capacity settlement", waitMs: 100 },
4329
4503
  () => {
4330
- const snapshot = this.read();
4504
+ const snapshot = this.readForMutation();
4331
4505
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4332
4506
  if (snapshot.revision === Number.MAX_SAFE_INTEGER)
4333
4507
  throw new Error("Capacity reservation revision exhausted.");
@@ -4337,7 +4511,7 @@ var CapacityStore = class {
4337
4511
  if (index >= 0) snapshot.reservations.splice(index, 1);
4338
4512
  snapshot.revision++;
4339
4513
  validate(snapshot);
4340
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4514
+ this.commit(snapshot);
4341
4515
  return { settled: index >= 0, revision: snapshot.revision };
4342
4516
  }
4343
4517
  );
@@ -4352,16 +4526,19 @@ var CapacityStore = class {
4352
4526
  `${this.file}.lock`,
4353
4527
  { activity: "capacity pool observation", waitMs: 100 },
4354
4528
  () => {
4355
- const snapshot = this.read();
4529
+ const snapshot = this.readForMutation();
4356
4530
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4357
4531
  const merged = structuredClone(snapshot.pools ?? []);
4358
- if (!mergePools(merged, pools)) return { changed: false, revision: snapshot.revision };
4532
+ if (!mergePools(merged, pools)) {
4533
+ if (!this.observedLedger) this.commit(snapshot);
4534
+ return { changed: false, revision: snapshot.revision };
4535
+ }
4359
4536
  if (snapshot.revision === Number.MAX_SAFE_INTEGER)
4360
4537
  throw new Error("Capacity reservation revision exhausted.");
4361
4538
  snapshot.pools = merged;
4362
4539
  snapshot.revision++;
4363
4540
  validate(snapshot);
4364
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4541
+ this.commit(snapshot);
4365
4542
  return { changed: true, revision: snapshot.revision };
4366
4543
  }
4367
4544
  );
@@ -4380,7 +4557,7 @@ var CapacityStore = class {
4380
4557
  `${this.file}.lock`,
4381
4558
  { activity: "capacity pool settlement", waitMs: 100 },
4382
4559
  () => {
4383
- const snapshot = this.read();
4560
+ const snapshot = this.readForMutation();
4384
4561
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4385
4562
  const pools = snapshot.pools ?? [];
4386
4563
  const index = pools.findIndex((pool2) => pool2.daemonId === identity2.daemonId);
@@ -4394,7 +4571,7 @@ var CapacityStore = class {
4394
4571
  if (pool) pools.splice(index, 1);
4395
4572
  snapshot.revision++;
4396
4573
  validate(snapshot);
4397
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4574
+ this.commit(snapshot);
4398
4575
  return { settled: !!pool, revision: snapshot.revision };
4399
4576
  }
4400
4577
  );
@@ -4409,7 +4586,7 @@ var CapacityStore = class {
4409
4586
  `${this.file}.lock`,
4410
4587
  { activity: "capacity phase settlement", waitMs: 100 },
4411
4588
  () => {
4412
- const snapshot = this.read();
4589
+ const snapshot = this.readForMutation();
4413
4590
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4414
4591
  const index = snapshot.reservations.findIndex(
4415
4592
  (entry) => entry.environmentId === target.environmentId
@@ -4426,7 +4603,7 @@ var CapacityStore = class {
4426
4603
  snapshot.reservations[index] = structuredClone(target);
4427
4604
  snapshot.revision++;
4428
4605
  validate(snapshot);
4429
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4606
+ this.commit(snapshot);
4430
4607
  }
4431
4608
  );
4432
4609
  }
@@ -4444,7 +4621,7 @@ var CapacityStore = class {
4444
4621
  `${this.file}.lock`,
4445
4622
  { activity: "capacity reservation", waitMs: 100 },
4446
4623
  () => {
4447
- const snapshot = this.read();
4624
+ const snapshot = this.readForMutation();
4448
4625
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4449
4626
  const pools = structuredClone(snapshot.pools ?? []);
4450
4627
  const poolChanged = mergePools(pools, requestedPool === void 0 ? [] : [requestedPool]);
@@ -4489,7 +4666,7 @@ var CapacityStore = class {
4489
4666
  snapshot.reservations[snapshot.reservations.indexOf(existing)] = structuredClone(request);
4490
4667
  else snapshot.reservations.push(structuredClone(request));
4491
4668
  validate(snapshot);
4492
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4669
+ this.commit(snapshot);
4493
4670
  return { admitted: true, revision: snapshot.revision, joined: false };
4494
4671
  }
4495
4672
  );
@@ -4501,6 +4678,28 @@ var import_node_crypto7 = require("crypto");
4501
4678
  var import_node_fs16 = __toESM(require("fs"));
4502
4679
  var import_node_path20 = __toESM(require("path"));
4503
4680
  var CONTROLLER_SNAPSHOT_BYTES = 1048576;
4681
+ var EVENT_KINDS = [
4682
+ "acquired",
4683
+ "renewed",
4684
+ "released",
4685
+ "expired",
4686
+ "invalidated",
4687
+ "observed",
4688
+ "consent",
4689
+ "reconnected"
4690
+ ];
4691
+ var LEGACY_EVENT_KINDS = EVENT_KINDS.slice(0, 6);
4692
+ var PROJECTION_STATUSES = [
4693
+ "READY",
4694
+ "APP_ERROR",
4695
+ "BLOCKED",
4696
+ "STOPPED",
4697
+ "STARTING",
4698
+ "RECOVERING",
4699
+ "WAITING_CAPACITY",
4700
+ "PARKED_CAPACITY",
4701
+ "UNKNOWN"
4702
+ ];
4504
4703
  function fail() {
4505
4704
  throw new Error("Invalid controller snapshot.");
4506
4705
  }
@@ -4518,38 +4717,150 @@ function id3(value) {
4518
4717
  function counter(value) {
4519
4718
  return Number.isSafeInteger(value) && Number(value) >= 0;
4520
4719
  }
4720
+ function requirements(value) {
4721
+ return Array.isArray(value) && value.length >= 1 && value.length <= 16 && new Set(value).size === value.length && value.every(
4722
+ (entry) => text2(entry, 132) && /^(runtime|app:[a-z0-9][a-z0-9-]*)$/.test(entry)
4723
+ );
4724
+ }
4725
+ function parkingConsent(value) {
4726
+ return value === "protected" || value === "allow-unusable";
4727
+ }
4728
+ function retainedReason(value) {
4729
+ return value === "expired" || value === "restart" || value === "discontinuity" || value === "binding-changed";
4730
+ }
4731
+ function checkEnvironment(value, ids2, paths) {
4732
+ fields(value, [
4733
+ "id",
4734
+ "repoPath",
4735
+ "workspace",
4736
+ "provider",
4737
+ "providerId",
4738
+ "profile",
4739
+ "fingerprint"
4740
+ ]);
4741
+ if (!id3(value.id) || !text2(value.repoPath, 4096) || !import_node_path20.default.isAbsolute(value.repoPath) || import_node_path20.default.resolve(value.repoPath) !== value.repoPath || !id3(value.workspace) || !id3(value.providerId) || !text2(value.profile) || !/^[a-z0-9-]+(?:,[a-z0-9-]+)*$/.test(value.profile) || !text2(value.fingerprint, 128) || !/^[a-f0-9]{64}$/.test(value.fingerprint) || !["devpod", "devsy"].includes(String(value.provider)))
4742
+ fail();
4743
+ const knownPath = ids2.get(value.id);
4744
+ if (knownPath !== void 0 && knownPath !== value.repoPath) fail();
4745
+ const knownId = paths.get(value.repoPath);
4746
+ if (knownId !== void 0 && knownId !== value.id) fail();
4747
+ ids2.set(value.id, value.repoPath);
4748
+ paths.set(value.repoPath, value.id);
4749
+ }
4750
+ function checkProjection(session) {
4751
+ if (session.observation === void 0) return;
4752
+ const observation = session.observation;
4753
+ fields(observation, [
4754
+ "status",
4755
+ "sampledAtMs",
4756
+ "validUntilMs",
4757
+ "journalRevision",
4758
+ "runtimeFingerprint"
4759
+ ]);
4760
+ if (!PROJECTION_STATUSES.includes(String(observation.status)) || !counter(observation.sampledAtMs) || !counter(observation.validUntilMs) || observation.validUntilMs < observation.sampledAtMs || !counter(observation.journalRevision) || !text2(observation.runtimeFingerprint, 128) || !/^[a-f0-9]{64}$/.test(observation.runtimeFingerprint))
4761
+ fail();
4762
+ }
4763
+ function checkEvents(events, nextSequence, kinds) {
4764
+ let previous = 0;
4765
+ for (const event of events) {
4766
+ fields(event, ["sequence", "session", "generation", "kind"]);
4767
+ if (!counter(event.sequence) || event.sequence <= previous || event.sequence >= nextSequence || !id3(event.session) || !id3(event.generation) || typeof event.kind !== "string" || !kinds.includes(event.kind))
4768
+ fail();
4769
+ previous = event.sequence;
4770
+ }
4771
+ if (Buffer.byteLength(JSON.stringify(events)) > 262144) fail();
4772
+ }
4521
4773
  function validateControllerSnapshot(value) {
4522
4774
  fields(value, [
4523
4775
  "version",
4524
4776
  "store",
4525
4777
  "epoch",
4526
4778
  "revision",
4779
+ "parkingRevision",
4780
+ "history",
4527
4781
  "nextSequence",
4528
4782
  "environments",
4529
4783
  "sessions",
4530
- "events"
4784
+ "retainedSessions",
4785
+ "events",
4786
+ ...value?.version === 3 ? ["fingerprint"] : []
4531
4787
  ]);
4532
- if (value.version !== 1 || !id3(value.store) || !counter(value.epoch) || !counter(value.revision) || !counter(value.nextSequence) || value.nextSequence < 1 || !Array.isArray(value.environments) || value.environments.length > 32 || !Array.isArray(value.sessions) || value.sessions.length > 128 || !Array.isArray(value.events) || value.events.length > 256)
4788
+ if (value.version !== 2 && value.version !== 3 || !id3(value.store) || !counter(value.epoch) || !counter(value.revision) || !counter(value.parkingRevision) || value.history !== "complete" && value.history !== "legacy-unknown" || !counter(value.nextSequence) || value.nextSequence < 1 || !Array.isArray(value.environments) || !Array.isArray(value.sessions) || !Array.isArray(value.retainedSessions) || !Array.isArray(value.events) || value.events.length > 256 || value.sessions.length + value.retainedSessions.length > 128)
4533
4789
  fail();
4534
- const environments = /* @__PURE__ */ new Set();
4535
- const checkouts = /* @__PURE__ */ new Set();
4536
- for (const env of value.environments) {
4537
- fields(env, [
4790
+ if (value.version === 3) {
4791
+ fields(value.fingerprint, ["digest", "enrolledEpoch"]);
4792
+ if (typeof value.fingerprint.digest !== "string" || !/^[a-f0-9]{64}$/.test(value.fingerprint.digest) || !counter(value.fingerprint.enrolledEpoch) || value.fingerprint.enrolledEpoch < 1 || value.fingerprint.enrolledEpoch > Number(value.epoch))
4793
+ fail();
4794
+ }
4795
+ const ids2 = /* @__PURE__ */ new Map();
4796
+ const paths = /* @__PURE__ */ new Map();
4797
+ for (const environment of value.environments) checkEnvironment(environment, ids2, paths);
4798
+ if (ids2.size !== value.environments.length) fail();
4799
+ const activeEnvironments = new Set(ids2.keys());
4800
+ const usedEnvironments = /* @__PURE__ */ new Set();
4801
+ const activeSessions = /* @__PURE__ */ new Set();
4802
+ const tuples = /* @__PURE__ */ new Set();
4803
+ for (const session of value.sessions) {
4804
+ fields(session, [
4538
4805
  "id",
4539
- "repoPath",
4540
- "workspace",
4541
- "provider",
4542
- "providerId",
4543
- "profile",
4544
- "fingerprint"
4806
+ "environmentId",
4807
+ "generation",
4808
+ "requirements",
4809
+ "renewedAtMs",
4810
+ "parkingConsent",
4811
+ "consentRevision",
4812
+ ...Object.hasOwn(session, "observation") ? ["observation"] : []
4545
4813
  ]);
4546
- if (!id3(env.id) || environments.has(env.id) || !text2(env.repoPath, 4096) || !import_node_path20.default.isAbsolute(env.repoPath) || import_node_path20.default.resolve(env.repoPath) !== env.repoPath || checkouts.has(env.repoPath) || !id3(env.workspace) || !id3(env.providerId) || !text2(env.profile) || !/^[a-z0-9-]+(?:,[a-z0-9-]+)*$/.test(env.profile) || !text2(env.fingerprint, 128) || !/^[a-f0-9]{64}$/.test(env.fingerprint) || !["devpod", "devsy"].includes(String(env.provider)))
4814
+ const tuple = `${session.id}\0${value.epoch}\0${session.generation}`;
4815
+ if (!id3(session.id) || activeSessions.has(session.id) || !id3(session.environmentId) || !activeEnvironments.has(session.environmentId) || !id3(session.generation) || !counter(session.renewedAtMs) || !requirements(session.requirements) || !parkingConsent(session.parkingConsent) || !counter(session.consentRevision) || tuples.has(tuple))
4547
4816
  fail();
4548
- environments.add(env.id);
4549
- checkouts.add(env.repoPath);
4817
+ activeSessions.add(session.id);
4818
+ usedEnvironments.add(session.environmentId);
4819
+ tuples.add(tuple);
4820
+ checkProjection(session);
4550
4821
  }
4551
- const sessions = /* @__PURE__ */ new Set();
4822
+ if (usedEnvironments.size !== value.environments.length) fail();
4823
+ for (const retainedSession of value.retainedSessions) {
4824
+ fields(retainedSession, [
4825
+ "id",
4826
+ "generation",
4827
+ "epoch",
4828
+ "requirements",
4829
+ "parkingConsent",
4830
+ "consentRevision",
4831
+ "reason",
4832
+ "environment"
4833
+ ]);
4834
+ const tuple = `${retainedSession.id}\0${retainedSession.epoch}\0${retainedSession.generation}`;
4835
+ if (!id3(retainedSession.id) || !id3(retainedSession.generation) || !counter(retainedSession.epoch) || retainedSession.epoch > value.epoch || !requirements(retainedSession.requirements) || !parkingConsent(retainedSession.parkingConsent) || !counter(retainedSession.consentRevision) || !retainedReason(retainedSession.reason) || tuples.has(tuple))
4836
+ fail();
4837
+ tuples.add(tuple);
4838
+ checkEnvironment(retainedSession.environment, ids2, paths);
4839
+ }
4840
+ if (ids2.size > 32) fail();
4841
+ checkEvents(value.events, value.nextSequence, EVENT_KINDS);
4842
+ if (Buffer.byteLength(JSON.stringify(value)) + 1 > CONTROLLER_SNAPSHOT_BYTES) fail();
4843
+ }
4844
+ function validateLegacySnapshot(value) {
4845
+ fields(value, [
4846
+ "version",
4847
+ "store",
4848
+ "epoch",
4849
+ "revision",
4850
+ "nextSequence",
4851
+ "environments",
4852
+ "sessions",
4853
+ "events"
4854
+ ]);
4855
+ if (value.version !== 1 || !id3(value.store) || !counter(value.epoch) || !counter(value.revision) || !counter(value.nextSequence) || value.nextSequence < 1 || !Array.isArray(value.environments) || value.environments.length > 32 || !Array.isArray(value.sessions) || value.sessions.length > 128 || !Array.isArray(value.events) || value.events.length > 256)
4856
+ fail();
4857
+ const ids2 = /* @__PURE__ */ new Map();
4858
+ const paths = /* @__PURE__ */ new Map();
4859
+ for (const environment of value.environments) checkEnvironment(environment, ids2, paths);
4860
+ if (ids2.size !== value.environments.length) fail();
4861
+ const activeEnvironments = new Set(ids2.keys());
4552
4862
  const usedEnvironments = /* @__PURE__ */ new Set();
4863
+ const sessions = /* @__PURE__ */ new Set();
4553
4864
  for (const session of value.sessions) {
4554
4865
  fields(session, [
4555
4866
  "id",
@@ -4559,60 +4870,66 @@ function validateControllerSnapshot(value) {
4559
4870
  "renewedAtMs",
4560
4871
  ...Object.hasOwn(session, "observation") ? ["observation"] : []
4561
4872
  ]);
4562
- if (!id3(session.id) || sessions.has(session.id) || !id3(session.environmentId) || !environments.has(session.environmentId) || !id3(session.generation) || !counter(session.renewedAtMs) || !Array.isArray(session.requirements) || session.requirements.length < 1 || session.requirements.length > 16 || new Set(session.requirements).size !== session.requirements.length || session.requirements.some(
4563
- (r) => !text2(r, 132) || !/^(runtime|app:[a-z0-9][a-z0-9-]*)$/.test(r)
4564
- ))
4873
+ if (!id3(session.id) || sessions.has(session.id) || !id3(session.environmentId) || !activeEnvironments.has(session.environmentId) || !id3(session.generation) || !counter(session.renewedAtMs) || !requirements(session.requirements))
4565
4874
  fail();
4566
- if (session.observation !== void 0) {
4567
- const observation = session.observation;
4568
- fields(observation, [
4569
- "status",
4570
- "sampledAtMs",
4571
- "validUntilMs",
4572
- "journalRevision",
4573
- "runtimeFingerprint"
4574
- ]);
4575
- if (![
4576
- "READY",
4577
- "APP_ERROR",
4578
- "BLOCKED",
4579
- "STOPPED",
4580
- "STARTING",
4581
- "RECOVERING",
4582
- "WAITING_CAPACITY",
4583
- "PARKED_CAPACITY",
4584
- "UNKNOWN"
4585
- ].includes(String(observation.status)) || !counter(observation.sampledAtMs) || !counter(observation.validUntilMs) || observation.validUntilMs < observation.sampledAtMs || !counter(observation.journalRevision) || !text2(observation.runtimeFingerprint, 128) || !/^[a-f0-9]{64}$/.test(observation.runtimeFingerprint))
4586
- fail();
4587
- }
4588
4875
  sessions.add(session.id);
4589
4876
  usedEnvironments.add(session.environmentId);
4877
+ checkProjection(session);
4590
4878
  }
4591
- if (usedEnvironments.size !== environments.size) fail();
4592
- let previous = 0;
4593
- for (const event of value.events) {
4594
- fields(event, ["sequence", "session", "generation", "kind"]);
4595
- if (!counter(event.sequence) || event.sequence <= previous || event.sequence >= value.nextSequence || !id3(event.session) || !id3(event.generation) || !["acquired", "renewed", "released", "expired", "invalidated", "observed"].includes(
4596
- String(event.kind)
4597
- ))
4598
- fail();
4599
- previous = event.sequence;
4879
+ if (usedEnvironments.size !== value.environments.length) fail();
4880
+ checkEvents(value.events, value.nextSequence, LEGACY_EVENT_KINDS);
4881
+ if (Buffer.byteLength(JSON.stringify(value)) + 1 > CONTROLLER_SNAPSHOT_BYTES) fail();
4882
+ }
4883
+ function normalizeLegacySnapshot(value) {
4884
+ return {
4885
+ version: 2,
4886
+ store: value.store,
4887
+ epoch: value.epoch,
4888
+ revision: value.revision,
4889
+ parkingRevision: 0,
4890
+ history: "legacy-unknown",
4891
+ nextSequence: value.nextSequence,
4892
+ environments: value.environments.map((environment) => ({ ...environment })),
4893
+ sessions: value.sessions.map((session) => ({
4894
+ id: session.id,
4895
+ environmentId: session.environmentId,
4896
+ generation: session.generation,
4897
+ requirements: [...session.requirements],
4898
+ renewedAtMs: session.renewedAtMs,
4899
+ parkingConsent: "protected",
4900
+ consentRevision: 0,
4901
+ ...session.observation ? { observation: { ...session.observation } } : {}
4902
+ })),
4903
+ retainedSessions: [],
4904
+ events: value.events.map((event) => ({ ...event }))
4905
+ };
4906
+ }
4907
+ function parseStoredSnapshot(value) {
4908
+ if (value && typeof value === "object" && !Array.isArray(value) && value.version === 1) {
4909
+ validateLegacySnapshot(value);
4910
+ return normalizeLegacySnapshot(value);
4600
4911
  }
4601
- if (Buffer.byteLength(JSON.stringify(value.events)) > 262144 || Buffer.byteLength(JSON.stringify(value)) + 1 > CONTROLLER_SNAPSHOT_BYTES)
4602
- fail();
4912
+ validateControllerSnapshot(value);
4913
+ return value;
4603
4914
  }
4604
4915
  var ControllerStore = class {
4605
- constructor(directory, write = writeFileAtomically) {
4916
+ constructor(directory, write = writeFileAtomically, ownsLifetimeLock = false) {
4606
4917
  this.directory = directory;
4607
4918
  this.write = write;
4919
+ this.ownsLifetimeLock = ownsLifetimeLock;
4608
4920
  this.file = import_node_path20.default.join(directory, "snapshot.json");
4921
+ this.identityFile = import_node_path20.default.join(directory, "store-identity.json");
4609
4922
  }
4610
4923
  file;
4611
- read() {
4924
+ identityFile;
4925
+ identity;
4926
+ observedStore;
4927
+ observedFingerprint;
4928
+ readBytes(file = this.file, limit = CONTROLLER_SNAPSHOT_BYTES) {
4612
4929
  let fd;
4613
4930
  try {
4614
4931
  fd = import_node_fs16.default.openSync(
4615
- this.file,
4932
+ file,
4616
4933
  import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NOFOLLOW | import_node_fs16.default.constants.O_NONBLOCK
4617
4934
  );
4618
4935
  } catch (error) {
@@ -4621,39 +4938,150 @@ var ControllerStore = class {
4621
4938
  }
4622
4939
  try {
4623
4940
  const stat = import_node_fs16.default.fstatSync(fd);
4624
- if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > CONTROLLER_SNAPSHOT_BYTES)
4941
+ if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > limit)
4625
4942
  fail();
4626
- const bytes2 = Buffer.alloc(CONTROLLER_SNAPSHOT_BYTES + 1);
4943
+ const bytes2 = Buffer.alloc(limit + 1);
4627
4944
  let length = 0;
4628
4945
  while (length < bytes2.length) {
4629
4946
  const count = import_node_fs16.default.readSync(fd, bytes2, length, bytes2.length - length, null);
4630
4947
  if (!count) break;
4631
4948
  length += count;
4632
4949
  }
4633
- if (length > CONTROLLER_SNAPSHOT_BYTES) fail();
4634
- const value = JSON.parse(bytes2.subarray(0, length).toString("utf8"));
4635
- validateControllerSnapshot(value);
4636
- return value;
4950
+ if (length > limit) fail();
4951
+ return bytes2.subarray(0, length);
4637
4952
  } catch {
4638
4953
  throw new Error("Controller snapshot is unreadable or invalid.");
4639
4954
  } finally {
4640
4955
  import_node_fs16.default.closeSync(fd);
4641
4956
  }
4642
4957
  }
4958
+ readIdentity() {
4959
+ try {
4960
+ const bytes2 = this.readBytes(this.identityFile, 4096);
4961
+ if (!bytes2) return void 0;
4962
+ const value = JSON.parse(bytes2.toString("utf8"));
4963
+ fields(value, [
4964
+ "version",
4965
+ "store",
4966
+ ...value?.version === 2 ? ["key", "enrolledEpoch"] : []
4967
+ ]);
4968
+ if (value.version !== 1 && value.version !== 2 || !id3(value.store)) fail();
4969
+ if (value.version === 2 && (typeof value.key !== "string" || !/^[a-f0-9]{64}$/.test(value.key) || !counter(value.enrolledEpoch) || Number(value.enrolledEpoch) < 1))
4970
+ fail();
4971
+ return value;
4972
+ } catch {
4973
+ throw new Error("Controller store-identity.json is unreadable or invalid.");
4974
+ }
4975
+ }
4976
+ provenance(identity2) {
4977
+ if (identity2.version !== 2) throw new Error("Controller binding provenance is unavailable.");
4978
+ return {
4979
+ digest: (0, import_node_crypto7.createHash)("sha256").update(JSON.stringify(["devrouter-controller-binding-v1", identity2.store, identity2.key])).digest("hex"),
4980
+ enrolledEpoch: identity2.enrolledEpoch
4981
+ };
4982
+ }
4983
+ checkProvenance(snapshot, identity2) {
4984
+ if (snapshot.version === 3) {
4985
+ if (!identity2 || identity2.store !== snapshot.store || JSON.stringify(this.provenance(identity2)) !== JSON.stringify(snapshot.fingerprint))
4986
+ throw new Error("Controller binding provenance changed.");
4987
+ } else if (identity2?.version === 2 && identity2.enrolledEpoch !== snapshot.epoch + 1) {
4988
+ throw new Error("Controller binding enrollment epoch changed.");
4989
+ }
4990
+ if (this.observedFingerprint && (snapshot.version !== 3 || JSON.stringify(snapshot.fingerprint) !== this.observedFingerprint))
4991
+ throw new Error("Controller binding provenance disappeared or changed.");
4992
+ }
4993
+ /** Return an incarnation-fenced capability; private key bytes never leave this store. */
4994
+ bindingFingerprint() {
4995
+ const captured = this.read();
4996
+ if (captured?.version !== 3) throw new Error("Controller binding provenance is unavailable.");
4997
+ return (owner, config) => {
4998
+ const current = this.read();
4999
+ const identity2 = this.readIdentity();
5000
+ if (!current || current.store !== captured.store || current.epoch !== captured.epoch || JSON.stringify(current.fingerprint) !== JSON.stringify(captured.fingerprint) || identity2?.version !== 2)
5001
+ throw new Error("Controller binding incarnation changed.");
5002
+ this.checkProvenance(current, identity2);
5003
+ return (0, import_node_crypto7.createHmac)("sha256", Buffer.from(identity2.key, "hex")).update(JSON.stringify({ owner, config })).digest("hex");
5004
+ };
5005
+ }
5006
+ read() {
5007
+ const identity2 = this.readIdentity();
5008
+ if (this.identity && JSON.stringify(identity2) !== this.identity)
5009
+ throw new Error("Controller store-identity.json disappeared or changed.");
5010
+ const bytes2 = this.readBytes();
5011
+ if (!bytes2) {
5012
+ if (identity2 || this.observedStore)
5013
+ throw new Error(
5014
+ "Controller snapshot.json is missing; restore verified history matching store-identity.json."
5015
+ );
5016
+ return void 0;
5017
+ }
5018
+ try {
5019
+ const snapshot = parseStoredSnapshot(JSON.parse(bytes2.toString("utf8")));
5020
+ if (identity2 && identity2.store !== snapshot.store || this.observedStore && this.observedStore !== snapshot.store)
5021
+ fail();
5022
+ this.checkProvenance(snapshot, identity2);
5023
+ this.observedStore = snapshot.store;
5024
+ this.identity = identity2 ? JSON.stringify(identity2) : void 0;
5025
+ if (snapshot.version === 3) this.observedFingerprint = JSON.stringify(snapshot.fingerprint);
5026
+ return snapshot;
5027
+ } catch {
5028
+ throw new Error("Controller snapshot is unreadable or invalid.");
5029
+ }
5030
+ }
5031
+ assertEmptyDirectory(ignoreOwnerLock) {
5032
+ let entries;
5033
+ try {
5034
+ entries = import_node_fs16.default.readdirSync(this.directory);
5035
+ } catch (error) {
5036
+ if (error.code === "ENOENT") return;
5037
+ throw error;
5038
+ }
5039
+ if (entries.some(
5040
+ (entry) => entry !== "capacity-policy.json" && !(ignoreOwnerLock && entry === "owner.lock")
5041
+ ))
5042
+ throw new Error(
5043
+ "Controller history is missing with surviving artifacts; restore verified history."
5044
+ );
5045
+ }
5046
+ /** Refuse before acquiring a lock could consume the last surviving history evidence. */
5047
+ assertStartup() {
5048
+ if (!this.read()) this.assertEmptyDirectory(false);
5049
+ }
5050
+ enroll(store, epoch) {
5051
+ const existing = this.readIdentity();
5052
+ if (existing && existing.store !== store) throw new Error("Controller store identity changed.");
5053
+ const identity2 = existing?.version === 2 ? existing : {
5054
+ version: 2,
5055
+ store,
5056
+ key: (0, import_node_crypto7.randomBytes)(32).toString("hex"),
5057
+ enrolledEpoch: epoch
5058
+ };
5059
+ if (existing?.version !== 2)
5060
+ this.commitBytes(this.identityFile, `${JSON.stringify(identity2)}
5061
+ `, 4096);
5062
+ this.identity = JSON.stringify(identity2);
5063
+ return this.provenance(identity2);
5064
+ }
4643
5065
  persist(snapshot) {
4644
5066
  validateControllerSnapshot(snapshot);
4645
- this.read();
5067
+ const previous = this.read();
5068
+ if (previous && previous.store !== snapshot.store)
5069
+ throw new Error("Controller store identity changed.");
5070
+ this.checkProvenance(snapshot, this.readIdentity());
4646
5071
  const contents = `${JSON.stringify(snapshot)}
4647
5072
  `;
5073
+ this.commitBytes(this.file, contents, CONTROLLER_SNAPSHOT_BYTES);
5074
+ if (snapshot.version === 3) this.observedFingerprint = JSON.stringify(snapshot.fingerprint);
5075
+ }
5076
+ commitBytes(file, contents, limit) {
4648
5077
  try {
4649
- this.write(this.file, contents);
5078
+ this.write(file, contents);
4650
5079
  } catch {
4651
- const current = this.read();
4652
- if (!current || `${JSON.stringify(current)}
4653
- ` !== contents)
5080
+ const stored = this.readBytes(file, limit);
5081
+ if (!stored || stored.toString("utf8") !== contents)
4654
5082
  throw new Error("Controller snapshot commit failed.");
4655
- for (const file of [this.file, this.directory]) {
4656
- const fd = import_node_fs16.default.openSync(file, import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NOFOLLOW);
5083
+ for (const target of [file, this.directory]) {
5084
+ const fd = import_node_fs16.default.openSync(target, import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NOFOLLOW);
4657
5085
  try {
4658
5086
  import_node_fs16.default.fsyncSync(fd);
4659
5087
  } finally {
@@ -4663,27 +5091,66 @@ var ControllerStore = class {
4663
5091
  }
4664
5092
  }
4665
5093
  startIncarnation() {
4666
- const previous = this.read();
4667
- if (previous && (previous.epoch === Number.MAX_SAFE_INTEGER || previous.revision === Number.MAX_SAFE_INTEGER))
5094
+ let previous = this.read();
5095
+ const pristine = !previous;
5096
+ if (!previous) this.assertEmptyDirectory(this.ownsLifetimeLock);
5097
+ if (previous && (previous.epoch === Number.MAX_SAFE_INTEGER || previous.revision === Number.MAX_SAFE_INTEGER || previous.parkingRevision === Number.MAX_SAFE_INTEGER))
4668
5098
  throw new Error("Controller snapshot counters exhausted.");
5099
+ if (!previous) {
5100
+ previous = {
5101
+ version: 2,
5102
+ store: (0, import_node_crypto7.randomUUID)(),
5103
+ epoch: 0,
5104
+ revision: 0,
5105
+ parkingRevision: 0,
5106
+ history: "complete",
5107
+ nextSequence: 1,
5108
+ environments: [],
5109
+ sessions: [],
5110
+ retainedSessions: [],
5111
+ events: []
5112
+ };
5113
+ this.persist(previous);
5114
+ }
5115
+ const fingerprint = this.enroll(previous.store, previous.epoch + 1);
4669
5116
  const snapshot = {
4670
- version: 1,
4671
- store: previous?.store ?? (0, import_node_crypto7.randomUUID)(),
5117
+ version: 3,
5118
+ fingerprint,
5119
+ store: previous.store,
4672
5120
  epoch: (previous?.epoch ?? 0) + 1,
4673
5121
  revision: (previous?.revision ?? 0) + 1,
5122
+ parkingRevision: pristine ? 0 : previous.parkingRevision + 1,
5123
+ history: previous?.history ?? "complete",
4674
5124
  nextSequence: 1,
4675
5125
  environments: [],
4676
5126
  sessions: [],
5127
+ retainedSessions: previous ? [...previous.retainedSessions, ...previous.sessions.map((s) => retain(s, previous))] : [],
4677
5128
  events: []
4678
5129
  };
4679
5130
  this.persist(snapshot);
4680
5131
  return snapshot;
4681
5132
  }
4682
5133
  };
5134
+ function retain(session, previous) {
5135
+ const environment = previous.environments.find(
5136
+ (candidate) => candidate.id === session.environmentId
5137
+ );
5138
+ if (!environment) fail();
5139
+ return {
5140
+ id: session.id,
5141
+ generation: session.generation,
5142
+ epoch: previous.epoch,
5143
+ requirements: [...session.requirements],
5144
+ parkingConsent: session.parkingConsent,
5145
+ consentRevision: session.consentRevision,
5146
+ reason: "restart",
5147
+ environment: { ...environment }
5148
+ };
5149
+ }
4683
5150
 
4684
5151
  // src/core/reliability-operation-store.ts
4685
5152
  function installedCliVersion() {
4686
- return true ? "0.0.77" : "0.0.0-dev";
5153
+ return true ? "0.0.79" : "0.0.0-dev";
4687
5154
  }
4688
5155
  function compareCliVersions(left, right) {
4689
5156
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -4698,6 +5165,10 @@ function compareCliVersions(left, right) {
4698
5165
  var MAX_RECORD_BYTES = 1048576;
4699
5166
  var MAX_RELIABILITY_JOURNALS = 256;
4700
5167
  var MAX_RELIABILITY_DIRECTORY_ENTRIES = MAX_RELIABILITY_JOURNALS * 2;
5168
+ var MAX_RELIABILITY_ENUMERATION_MS = 2e3;
5169
+ var RELIABILITY_JOURNAL_NAME_RE = /^([0-9a-f]{64})\.json$/;
5170
+ var RELIABILITY_LOCK_NAME_RE = /^[0-9a-f]{64}\.json\.lock(?:\.[0-9]+\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.candidate(?:\.stale)?|\.queue\.[0-9]+\.[0-9]+\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(?:\.candidate)?)?$/;
5171
+ var RELIABILITY_ATOMIC_TEMP_NAME_RE = /^\.[0-9a-f]{64}\.json\.[0-9]+\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/;
4701
5172
  function identityKey(identity2) {
4702
5173
  if (!import_node_path21.default.isAbsolute(identity2.repoPath) || import_node_path21.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
4703
5174
  throw new Error("Invalid reliability workspace identity.");
@@ -4717,6 +5188,17 @@ function exactKeys(value, expected, message) {
4717
5188
  throw new Error(message);
4718
5189
  }
4719
5190
  }
5191
+ function validateConsumerProtection(record5) {
5192
+ const protection = record5.consumerProtection;
5193
+ if (protection === void 0) return;
5194
+ exactKeys(
5195
+ protection,
5196
+ ["version", "revision", "humanPinned"],
5197
+ "Invalid consumer protection fields."
5198
+ );
5199
+ if (protection.version !== 1 || !isReliabilityCounter(protection.revision) || typeof protection.humanPinned !== "boolean")
5200
+ throw new Error("Invalid consumer protection.");
5201
+ }
4720
5202
  function validateCapacityPhaseSettlement(record5, state, enrollment) {
4721
5203
  const settlement = record5.phaseSettlement;
4722
5204
  if (settlement === void 0 || settlement === null) return;
@@ -4812,6 +5294,7 @@ function validate2(record5, identity2) {
4812
5294
  "outcome",
4813
5295
  "result",
4814
5296
  "writtenByVersion",
5297
+ "consumerProtection",
4815
5298
  ...record5.version === 2 ? [
4816
5299
  "capacity",
4817
5300
  "enrollment",
@@ -4821,6 +5304,7 @@ function validate2(record5, identity2) {
4821
5304
  "startupWitness"
4822
5305
  ] : []
4823
5306
  ]);
5307
+ validateConsumerProtection(record5);
4824
5308
  if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
4825
5309
  throw new Error(
4826
5310
  `Reliability record was last written by devrouter ${record5.writtenByVersion}, which is newer than the installed ${installedCliVersion()}; upgrade devrouter (devrouter upgrade) before running lifecycle commands.`
@@ -4988,6 +5472,19 @@ function validate2(record5, identity2) {
4988
5472
  }
4989
5473
  }
4990
5474
  }
5475
+ function createLifecycleCapacityStore(directory = import_node_path21.default.join(DEVROUTER_HOME, "controller")) {
5476
+ return new CapacityStore(directory, void 0, () => {
5477
+ try {
5478
+ let revision = 0;
5479
+ for (const record5 of listReliabilityOperations()) {
5480
+ if (record5.capacity) revision = Math.max(revision, record5.capacity.snapshotRevision ?? 1);
5481
+ }
5482
+ return revision;
5483
+ } catch {
5484
+ throw new CapacityHistoryError("capacity-history-unprovable");
5485
+ }
5486
+ });
5487
+ }
4991
5488
  function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_path21.default.join(DEVROUTER_HOME, "controller")) {
4992
5489
  if (record5.version === 1) return;
4993
5490
  const binding = record5.capacity;
@@ -4998,7 +5495,7 @@ function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_
4998
5495
  if (!binding.controller || !controller || controller.store !== binding.controller.store || controller.epoch !== binding.controller.epoch)
4999
5496
  throw new Error("Capacity controller incarnation changed.");
5000
5497
  }
5001
- const snapshot = new CapacityStore(directory).read();
5498
+ const snapshot = createLifecycleCapacityStore(directory).read();
5002
5499
  if (binding.snapshotRevision === void 0 || snapshot.revision !== binding.snapshotRevision)
5003
5500
  throw new Error("Capacity snapshot authority is absent or stale.");
5004
5501
  const reservation = snapshot.reservations.find(
@@ -5039,6 +5536,79 @@ function readBoundedPrivateJson(file, label) {
5039
5536
  import_node_fs17.default.closeSync(descriptor);
5040
5537
  }
5041
5538
  }
5539
+ function assertReliabilityEnumerationTime(startedAtMs) {
5540
+ if (performance.now() - startedAtMs > MAX_RELIABILITY_ENUMERATION_MS)
5541
+ throw new Error("Reliability journal enumeration exceeded its time limit.");
5542
+ }
5543
+ function reliabilityDirectory() {
5544
+ return import_node_path21.default.join(DEVROUTER_HOME, "reliability");
5545
+ }
5546
+ function journalIdentity(value) {
5547
+ if (!value || typeof value !== "object" || Array.isArray(value))
5548
+ throw new Error("Reliability journal identity is invalid.");
5549
+ const identity2 = value.identity;
5550
+ if (!identity2 || typeof identity2 !== "object" || Array.isArray(identity2))
5551
+ throw new Error("Reliability journal identity is invalid.");
5552
+ return identity2;
5553
+ }
5554
+ function listReliabilityOperations() {
5555
+ const startedAtMs = performance.now();
5556
+ const directory = reliabilityDirectory();
5557
+ let stat;
5558
+ try {
5559
+ stat = import_node_fs17.default.lstatSync(directory);
5560
+ } catch (error) {
5561
+ if (error.code === "ENOENT") return [];
5562
+ throw error;
5563
+ }
5564
+ if (!stat.isDirectory() || stat.isSymbolicLink())
5565
+ throw new Error("Reliability journal directory is unsafe.");
5566
+ if (stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0)
5567
+ throw new Error("Reliability journal directory is not private.");
5568
+ const entries = [];
5569
+ const handle = import_node_fs17.default.opendirSync(directory);
5570
+ try {
5571
+ for (; ; ) {
5572
+ assertReliabilityEnumerationTime(startedAtMs);
5573
+ const entry = handle.readSync();
5574
+ if (!entry) break;
5575
+ entries.push(entry);
5576
+ if (entries.length > MAX_RELIABILITY_DIRECTORY_ENTRIES)
5577
+ throw new Error("Reliability journal directory exceeds its entry limit.");
5578
+ }
5579
+ } finally {
5580
+ handle.closeSync();
5581
+ }
5582
+ const journals = [];
5583
+ for (const entry of entries) {
5584
+ assertReliabilityEnumerationTime(startedAtMs);
5585
+ if (entry.isSymbolicLink())
5586
+ throw new Error("Reliability journal directory contains a symlink.");
5587
+ if (!entry.isFile())
5588
+ throw new Error("Reliability journal directory contains an unsupported entry.");
5589
+ if (RELIABILITY_LOCK_NAME_RE.test(entry.name) || RELIABILITY_ATOMIC_TEMP_NAME_RE.test(entry.name))
5590
+ continue;
5591
+ const match = RELIABILITY_JOURNAL_NAME_RE.exec(entry.name);
5592
+ if (!match) throw new Error("Reliability journal directory contains an unsupported entry.");
5593
+ journals.push({ name: entry.name, key: match[1] });
5594
+ }
5595
+ if (journals.length > MAX_RELIABILITY_JOURNALS)
5596
+ throw new Error("Reliability journal directory exceeds its journal limit.");
5597
+ journals.sort((left, right) => left.name.localeCompare(right.name));
5598
+ const records = [];
5599
+ for (const journal of journals) {
5600
+ assertReliabilityEnumerationTime(startedAtMs);
5601
+ const file = import_node_path21.default.join(directory, journal.name);
5602
+ const identity2 = journalIdentity(readBoundedPrivateJson(file, "Reliability journal"));
5603
+ if (identityKey(identity2) !== journal.key)
5604
+ throw new Error("Reliability journal filename does not match its identity.");
5605
+ const record5 = readReliabilityOperation(identity2);
5606
+ if (!record5) throw new Error("Reliability journal disappeared during enumeration.");
5607
+ records.push(record5);
5608
+ assertReliabilityEnumerationTime(startedAtMs);
5609
+ }
5610
+ return records;
5611
+ }
5042
5612
  function persist(record5) {
5043
5613
  validate2(record5, record5.identity);
5044
5614
  const contents = `${JSON.stringify(record5)}
@@ -5082,12 +5652,15 @@ function updateReliabilityOperation(identity2, operation) {
5082
5652
  throw new Error("Reliability record revision is exhausted.");
5083
5653
  const previousVersion = record5.version;
5084
5654
  const previousEnrollment = record5.enrollment && { ...record5.enrollment };
5655
+ const previousProtection = structuredClone(record5.consumerProtection);
5085
5656
  const result = operation(record5);
5086
5657
  if (result && typeof result.then === "function") {
5087
5658
  throw new Error("Reliability record updates must be synchronous.");
5088
5659
  }
5089
5660
  if (record5.version < previousVersion || previousEnrollment && !(0, import_node_util4.isDeepStrictEqual)(record5.enrollment, previousEnrollment))
5090
5661
  throw new Error("Durable capacity enrollment cannot be downgraded or replaced.");
5662
+ if (!(0, import_node_util4.isDeepStrictEqual)(record5.consumerProtection, previousProtection))
5663
+ throw new Error("Consumer protection is only writable through the dedicated pin authority.");
5091
5664
  record5.writtenByVersion = installedCliVersion();
5092
5665
  validate2(record5, identity2);
5093
5666
  record5.revision += 1;
@@ -6383,7 +6956,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs18.default.rea
6383
6956
  const config = parseConfig(parsed ?? {}, configPath);
6384
6957
  const requiredVersion = config.devrouter?.version;
6385
6958
  if (requiredVersion && !hasWarnedVersionMismatch) {
6386
- const cliVersion = true ? "0.0.77" : "0.0.0-dev";
6959
+ const cliVersion = true ? "0.0.79" : "0.0.0-dev";
6387
6960
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
6388
6961
  hasWarnedVersionMismatch = true;
6389
6962
  process.stderr.write(
@@ -9315,7 +9888,8 @@ async function startDevsyWorkspace(options) {
9315
9888
  }
9316
9889
  if (options.recreate) args.push("--recreate");
9317
9890
  const env = network ? networkProviderEnvironment(network.binding, process.env) : { ...process.env };
9318
- env.DEVSY_AGENT_BINARY = agent.binaryPath;
9891
+ if (agent.binaryPath) env.DEVSY_AGENT_BINARY = agent.binaryPath;
9892
+ else delete env.DEVSY_AGENT_BINARY;
9319
9893
  if (options.workspace) {
9320
9894
  env.WORKSPACE = options.workspace.token;
9321
9895
  env.DEVROUTER_WORKSPACE = options.workspace.token;
@@ -9339,7 +9913,7 @@ async function startDevsyWorkspace(options) {
9339
9913
  let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
9340
9914
  if (result.error?.message) message += ` ${result.error.message}`;
9341
9915
  if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
9342
- message += ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
9916
+ message += agent.binaryPath ? ` Devsy rejected the verified agent source. Run: ${DEVSY_AGENT_SETUP_COMMAND}` : " Devsy could not acquire an agent binary for this host CLI.";
9343
9917
  }
9344
9918
  if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
9345
9919
  throw new DevsyStartPostconditionError(message);
@@ -9379,9 +9953,9 @@ var ExecutionOutcomeError = class extends Error {
9379
9953
  };
9380
9954
 
9381
9955
  // src/core/reliability-lifecycle.ts
9382
- var import_node_crypto20 = require("crypto");
9956
+ var import_node_crypto21 = require("crypto");
9383
9957
  var import_node_path40 = __toESM(require("path"));
9384
- var import_node_util6 = require("util");
9958
+ var import_node_util7 = require("util");
9385
9959
 
9386
9960
  // src/core/capacity-policy.ts
9387
9961
  var import_node_fs26 = __toESM(require("fs"));
@@ -9402,6 +9976,76 @@ var CONTROLLER_PROFILE_RE = new RegExp(
9402
9976
  `^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
9403
9977
  );
9404
9978
 
9979
+ // src/core/controller-monitor.ts
9980
+ var import_node_crypto15 = require("crypto");
9981
+ var import_node_util6 = require("util");
9982
+
9983
+ // src/core/reliability-output.ts
9984
+ function reliabilityAttention(state) {
9985
+ assertReliabilityState(state);
9986
+ const completeStop = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
9987
+ if (state.desired === "stopped-by-user") return completeStop ? void 0 : "stop-incomplete";
9988
+ if (state.operation?.status === "COMPLETION_UNKNOWN" || state.operation?.status === "INTERRUPTED")
9989
+ return "operation-unknown";
9990
+ if (state.desired === "parked-for-capacity")
9991
+ return completeStop ? "capacity-parked" : "stop-incomplete";
9992
+ if (state.admission === "denied-unadmittable") return "unadmittable";
9993
+ if (state.admission === "waiting") return "capacity-waiting";
9994
+ if (state.phase === "recovering") return "recovering";
9995
+ if (state.phase === "queued" || state.phase === "starting" || state.phase === "verifying")
9996
+ return "starting";
9997
+ return void 0;
9998
+ }
9999
+
10000
+ // src/core/recovery-budget.ts
10001
+ function findRecoveryUnit(incident, unit) {
10002
+ return (incident.units ?? []).find((record5) => record5.key === unit.key);
10003
+ }
10004
+ function restartsFor(kind, limits) {
10005
+ return kind === "service" ? limits.maxServiceRestarts : limits.maxProcessRestarts;
10006
+ }
10007
+ function recoveryBudgetRefusal(input) {
10008
+ const startedAtMs = input.incident.startedAtMs ?? input.nowMs;
10009
+ const elapsed = input.activeElapsedMs ?? Math.max(0, input.nowMs - startedAtMs);
10010
+ if (elapsed > input.limits.windowSeconds * 1e3) return "window-closed";
10011
+ if (input.unit !== void 0) {
10012
+ const record5 = findRecoveryUnit(input.incident, input.unit);
10013
+ if (record5 !== void 0 && record5.actions >= restartsFor(input.unit.kind, input.limits))
10014
+ return "unit-exhausted";
10015
+ }
10016
+ if (input.incident.correctiveActionsTaken >= input.incident.actionLimit)
10017
+ return "budget-exhausted";
10018
+ return void 0;
10019
+ }
10020
+ function claimRecoveryUnit(input) {
10021
+ const reason2 = recoveryBudgetRefusal(input);
10022
+ if (reason2 !== void 0) return { ok: false, reason: reason2 };
10023
+ const units = (input.incident.units ?? []).map((record5) => ({ ...record5 }));
10024
+ if (input.unit !== void 0) {
10025
+ const index = units.findIndex((record5) => record5.key === input.unit?.key);
10026
+ if (index === -1) {
10027
+ if (units.length >= RELIABILITY_MAX_ITEMS) return { ok: false, reason: "unit-exhausted" };
10028
+ units.push({ ...input.unit, actions: 1, lastActionAtMs: input.nowMs });
10029
+ } else {
10030
+ const record5 = units[index];
10031
+ units[index] = {
10032
+ ...record5,
10033
+ actions: record5.actions + 1,
10034
+ lastActionAtMs: input.nowMs
10035
+ };
10036
+ }
10037
+ }
10038
+ return {
10039
+ ok: true,
10040
+ incident: {
10041
+ ...input.incident,
10042
+ startedAtMs: input.incident.startedAtMs ?? input.nowMs,
10043
+ correctiveActionsTaken: input.incident.correctiveActionsTaken + 1,
10044
+ units
10045
+ }
10046
+ };
10047
+ }
10048
+
9405
10049
  // src/core/reliability-model.ts
9406
10050
  var INFRASTRUCTURE_VALUES = ["healthy", "failed", "unknown"];
9407
10051
  var APPLICATION_VALUES = ["verified", "unready", "unverified"];
@@ -9474,7 +10118,7 @@ function assertReliabilityEvent(value) {
9474
10118
  valid = isReliabilityCounter(value.type === "epoch" ? value.nextEpoch : value.nextGeneration);
9475
10119
  break;
9476
10120
  case "recover":
9477
- valid = isReliabilityId(value.incidentId) && isReliabilityCounter(value.actionLimit) && value.actionLimit > 0 && isReliabilityId(value.operationId);
10121
+ valid = isReliabilityId(value.incidentId) && isReliabilityCounter(value.actionLimit) && value.actionLimit > 0 && isReliabilityId(value.operationId) && (value.unit === null || record3(value.unit) && isReliabilityId(value.unit.key) && (value.unit.kind === "process" || value.unit.kind === "service")) && record3(value.recoveryLimits) && isReliabilityCounter(value.recoveryLimits.maxProcessRestarts) && isReliabilityCounter(value.recoveryLimits.maxServiceRestarts) && isReliabilityCounter(value.recoveryLimits.windowSeconds) && value.recoveryLimits.windowSeconds > 0 && (value.activeElapsedMs === null || isReliabilityCounter(value.activeElapsedMs));
9478
10122
  break;
9479
10123
  case "rearm":
9480
10124
  valid = isReliabilityId(value.incidentId);
@@ -9494,7 +10138,10 @@ function cloneOperation(operation) {
9494
10138
  return { ...operation };
9495
10139
  }
9496
10140
  function cloneIncident(incident) {
9497
- return { ...incident };
10141
+ return {
10142
+ ...incident,
10143
+ ...incident.units ? { units: incident.units.map((unit) => ({ ...unit })) } : {}
10144
+ };
9498
10145
  }
9499
10146
  function cloneState(state) {
9500
10147
  return {
@@ -9546,6 +10193,12 @@ function possibleDispatch(operation) {
9546
10193
  function pendingCommand(operation) {
9547
10194
  return operation !== null && !["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(operation.status);
9548
10195
  }
10196
+ function retirableEntryIndex(state) {
10197
+ const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
10198
+ return state.operationHistory.findIndex(
10199
+ (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
10200
+ );
10201
+ }
9549
10202
  function setUnknown(operation) {
9550
10203
  if (!operation || !possibleDispatch(operation)) return operation;
9551
10204
  return { ...operation, status: "COMPLETION_UNKNOWN", exitCode: null };
@@ -9665,10 +10318,7 @@ function handleOperationRequest(state, event) {
9665
10318
  const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
9666
10319
  let retired = -1;
9667
10320
  if (rollover) {
9668
- const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
9669
- retired = state.operationHistory.findIndex(
9670
- (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
9671
- );
10321
+ retired = retirableEntryIndex(state);
9672
10322
  if (retired === -1)
9673
10323
  return blocked(
9674
10324
  state,
@@ -9721,7 +10371,7 @@ function handleAdmission(state, event) {
9721
10371
  return unchanged(state, "blocked");
9722
10372
  }
9723
10373
  if (state.admission === event.result) return unchanged(state, "joined");
9724
- if (state.desired === "parked-for-capacity" && event.result === "admitted" && (!state.stopProof.workloadsStopped || !state.stopProof.routesRemoved))
10374
+ if (state.desired === "parked-for-capacity" && event.result === "admitted")
9725
10375
  return unchanged(state, "blocked");
9726
10376
  state.admission = event.result;
9727
10377
  if (event.result === "admitted") {
@@ -9756,16 +10406,7 @@ function handleDispatchPersisted(state, event) {
9756
10406
  `dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
9757
10407
  );
9758
10408
  }
9759
- if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
9760
- return unchanged(state, "blocked");
9761
- }
9762
10409
  state.operation = { ...state.operation, status: "DISPATCH_RECORDED", exitCode: null };
9763
- if (state.phase === "recovering" && state.incident !== null) {
9764
- state.incident = {
9765
- ...state.incident,
9766
- correctiveActionsTaken: state.incident.correctiveActionsTaken + 1
9767
- };
9768
- }
9769
10410
  return transition(state, "accepted", [effect(state, "launch", event.operationId)]);
9770
10411
  }
9771
10412
  function handleLaunched(state, event) {
@@ -9898,9 +10539,18 @@ function handlePark(state) {
9898
10539
  return transition(state, "accepted", [effect(state, "stop", operationId)]);
9899
10540
  }
9900
10541
  function handleResume(state, event) {
9901
- if (state.executionPolicy === "manual" || state.desired !== "parked-for-capacity" || !event.pressureDwellSatisfied || !state.stopProof.workloadsStopped || !state.stopProof.routesRemoved || state.consumers.length === 0 || state.admission !== "admitted" || state.profile === null || state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit || state.operation === null || !["NOT_STARTED", "COMPLETED", "INTERRUPTED"].includes(state.operation.status) || ["COMPLETED", "INTERRUPTED"].includes(state.operation.status) && event.operationId === state.operation.id) {
10542
+ if (state.executionPolicy === "manual" || state.desired !== "parked-for-capacity" || !event.pressureDwellSatisfied || !state.stopProof.workloadsStopped || !state.stopProof.routesRemoved || state.consumers.length === 0 || state.profile === null || state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit || state.operation === null || !["NOT_STARTED", "COMPLETED", "INTERRUPTED"].includes(state.operation.status) || ["COMPLETED", "INTERRUPTED"].includes(state.operation.status) && event.operationId === state.operation.id) {
9902
10543
  return unchanged(state, "blocked");
9903
10544
  }
10545
+ if (state.operationHistory.some((entry) => entry.id === event.operationId))
10546
+ return unchanged(state, "conflict");
10547
+ const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
10548
+ const retired = rollover ? retirableEntryIndex(state) : -1;
10549
+ if (rollover && retired === -1)
10550
+ return blocked(
10551
+ state,
10552
+ `journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
10553
+ );
9904
10554
  const intentRevision = advance(state.intentRevision);
9905
10555
  const runtimeGeneration = advance(state.runtimeGeneration);
9906
10556
  if (intentRevision === null || runtimeGeneration === null) return unchanged(state, "blocked");
@@ -9908,9 +10558,10 @@ function handleResume(state, event) {
9908
10558
  state.runtimeGeneration = runtimeGeneration;
9909
10559
  state.desired = "running";
9910
10560
  state.phase = "queued";
10561
+ state.admission = "waiting";
9911
10562
  state.stopProof = { workloadsStopped: false, routesRemoved: false };
9912
10563
  state.observations = [];
9913
- state.chargeHeld = true;
10564
+ state.chargeHeld = false;
9914
10565
  state.operation = {
9915
10566
  id: event.operationId,
9916
10567
  kind: "ensure",
@@ -9923,6 +10574,13 @@ function handleResume(state, event) {
9923
10574
  operationId: event.operationId,
9924
10575
  intentRevision
9925
10576
  }));
10577
+ if (retired !== -1) state.operationHistory.splice(retired, 1);
10578
+ state.operationHistory.push({
10579
+ ...state.operation,
10580
+ key: event.operationId,
10581
+ profile: state.profile,
10582
+ consumer: cloneConsumer(state.consumers[0])
10583
+ });
9926
10584
  return transition(state, "accepted");
9927
10585
  }
9928
10586
  function handleGenerationChange(state, kind, nextValue) {
@@ -9941,9 +10599,10 @@ function handleGenerationChange(state, kind, nextValue) {
9941
10599
  }
9942
10600
  return transition(state, "accepted");
9943
10601
  }
9944
- function handleRecover(state, event) {
10602
+ function handleRecover(state, event, nowMs) {
9945
10603
  if (state.executionPolicy === "manual" || state.desired !== "running")
9946
10604
  return unchanged(state, "blocked");
10605
+ if (state.phase === "stopping") return unchanged(state, "blocked");
9947
10606
  if (possibleDispatch(state.operation)) return unchanged(state, "blocked");
9948
10607
  const stranded = state.operation?.drained === true && ["NOT_STARTED", "NOT_LAUNCHED"].includes(state.operation.status);
9949
10608
  const operation = stranded ? null : state.operation;
@@ -9961,37 +10620,58 @@ function handleRecover(state, event) {
9961
10620
  return unchanged(state, "blocked");
9962
10621
  }
9963
10622
  }
9964
- if (state.incident === null) {
9965
- if (operation?.status === "NOT_STARTED") return unchanged(state, "blocked");
9966
- if (operation?.id === event.operationId) return unchanged(state, "conflict");
9967
- state.incident = {
9968
- id: event.incidentId,
9969
- correctiveActionsTaken: 0,
9970
- actionLimit: event.actionLimit
9971
- };
9972
- }
9973
- if (!operation || operation.status === "COMPLETED" || operation.status === "INTERRUPTED") {
9974
- const runtimeGeneration = advance(state.runtimeGeneration);
9975
- if (runtimeGeneration === null) return unchanged(state, "blocked");
9976
- const consumer = state.consumers[0];
9977
- if (state.profile === null || consumer === void 0) return unchanged(state, "blocked");
9978
- if (state.operationHistory.length >= RELIABILITY_MAX_ITEMS) return unchanged(state, "blocked");
9979
- state.runtimeGeneration = runtimeGeneration;
9980
- state.observations = [];
9981
- state.operation = {
9982
- id: event.operationId,
9983
- kind: "ensure",
9984
- drained: false,
9985
- status: "NOT_STARTED",
9986
- exitCode: null
9987
- };
9988
- state.operationHistory.push({
9989
- ...state.operation,
9990
- key: event.operationId,
9991
- profile: state.profile,
9992
- consumer: cloneConsumer(consumer)
9993
- });
9994
- }
10623
+ const createsIncident = state.incident === null;
10624
+ const replaceable = operation === null || operation.drained && (operation.status === "COMPLETED" || operation.status === "INTERRUPTED" && operation.kind === "ensure");
10625
+ if (!replaceable) return unchanged(state, "blocked");
10626
+ const consumer = state.consumers[0];
10627
+ if (state.profile === null || consumer === void 0) return unchanged(state, "blocked");
10628
+ if (state.operation?.id === event.operationId || state.operationHistory.some(
10629
+ (entry) => entry.id === event.operationId || entry.key === event.operationId
10630
+ ))
10631
+ return unchanged(state, "conflict");
10632
+ const runtimeGeneration = advance(state.runtimeGeneration);
10633
+ if (runtimeGeneration === null) return unchanged(state, "blocked");
10634
+ const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
10635
+ const retired = rollover ? retirableEntryIndex(state) : -1;
10636
+ if (rollover && retired === -1)
10637
+ return blocked(
10638
+ state,
10639
+ `journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
10640
+ );
10641
+ const incident = createsIncident ? {
10642
+ id: event.incidentId,
10643
+ correctiveActionsTaken: 0,
10644
+ actionLimit: event.actionLimit
10645
+ } : state.incident;
10646
+ const claim2 = claimRecoveryUnit({
10647
+ incident,
10648
+ limits: event.recoveryLimits,
10649
+ unit: event.unit ?? void 0,
10650
+ nowMs,
10651
+ activeElapsedMs: event.activeElapsedMs
10652
+ });
10653
+ if (!claim2.ok)
10654
+ return blocked(
10655
+ state,
10656
+ `recovery budget refused the action (${claim2.reason})${event.unit ? ` for ${event.unit.kind} unit ${event.unit.key}` : ""}.`
10657
+ );
10658
+ state.incident = claim2.incident;
10659
+ if (retired !== -1) state.operationHistory.splice(retired, 1);
10660
+ state.runtimeGeneration = runtimeGeneration;
10661
+ state.observations = [];
10662
+ state.operation = {
10663
+ id: event.operationId,
10664
+ kind: "ensure",
10665
+ drained: false,
10666
+ status: "NOT_STARTED",
10667
+ exitCode: null
10668
+ };
10669
+ state.operationHistory.push({
10670
+ ...state.operation,
10671
+ key: event.operationId,
10672
+ profile: state.profile,
10673
+ consumer: cloneConsumer(consumer)
10674
+ });
9995
10675
  state.phase = "recovering";
9996
10676
  return transition(state, "accepted");
9997
10677
  }
@@ -10062,7 +10742,7 @@ function applyReliabilityEvent(state, event, nowMs) {
10062
10742
  case "runtime":
10063
10743
  return handleGenerationChange(next, "runtime", event.nextGeneration);
10064
10744
  case "recover":
10065
- return handleRecover(next, event);
10745
+ return handleRecover(next, event, nowMs);
10066
10746
  case "rearm":
10067
10747
  return handleRearm(next, event);
10068
10748
  }
@@ -10086,9 +10766,64 @@ function stepReliability(state, event, nowMs) {
10086
10766
 
10087
10767
  // src/core/reliability-worker.ts
10088
10768
  var import_node_child_process17 = require("child_process");
10089
- var import_node_crypto15 = require("crypto");
10769
+ var import_node_crypto16 = require("crypto");
10090
10770
  var import_node_fs28 = __toESM(require("fs"));
10091
10771
  var import_node_path33 = __toESM(require("path"));
10772
+
10773
+ // src/core/lifecycle-progress.ts
10774
+ var ROLES = {
10775
+ validation: "lifecycle-worker",
10776
+ preparation: "repository-preparation",
10777
+ provider: "provider-operation",
10778
+ "service-start": "service-reconciliation",
10779
+ "service-stop": "service-reconciliation",
10780
+ "process-start": "repository-process-adapter",
10781
+ "process-stop": "repository-process-adapter",
10782
+ "route-publication": "lifecycle-worker",
10783
+ "route-removal": "lifecycle-worker",
10784
+ readiness: "lifecycle-worker",
10785
+ rollback: "lifecycle-worker",
10786
+ stop: "lifecycle-stop"
10787
+ };
10788
+ function isLifecycleProgressPhase(value) {
10789
+ return typeof value === "string" && Object.hasOwn(ROLES, value);
10790
+ }
10791
+ var sender;
10792
+ function installLifecycleProgressSender(send) {
10793
+ let active = true;
10794
+ let pending = false;
10795
+ let latest;
10796
+ const report = (phase) => {
10797
+ if (!active || !isLifecycleProgressPhase(phase)) return;
10798
+ if (pending) {
10799
+ latest = phase;
10800
+ return;
10801
+ }
10802
+ pending = true;
10803
+ try {
10804
+ send({ lifecycleProgress: phase }, () => {
10805
+ pending = false;
10806
+ const next = latest;
10807
+ latest = void 0;
10808
+ if (active && next) report(next);
10809
+ });
10810
+ } catch {
10811
+ pending = false;
10812
+ latest = void 0;
10813
+ }
10814
+ };
10815
+ sender = report;
10816
+ return () => {
10817
+ active = false;
10818
+ latest = void 0;
10819
+ if (sender === report) sender = void 0;
10820
+ };
10821
+ }
10822
+ function reportLifecycleProgress(phase) {
10823
+ sender?.(phase);
10824
+ }
10825
+
10826
+ // src/core/reliability-worker.ts
10092
10827
  var DEFAULT_OUTPUT_PAGE_BYTES = 48 * 1024;
10093
10828
  function workerGroupAbsent(pid) {
10094
10829
  try {
@@ -10796,7 +11531,7 @@ async function runManagedHostPreparation(repoPath, config) {
10796
11531
 
10797
11532
  // src/core/managed-post-start.ts
10798
11533
  var import_node_child_process22 = require("child_process");
10799
- var import_node_crypto16 = require("crypto");
11534
+ var import_node_crypto17 = require("crypto");
10800
11535
  var import_node_fs31 = __toESM(require("fs"));
10801
11536
  var import_node_path36 = __toESM(require("path"));
10802
11537
  var MANAGED_MARKER = "devrouter:managed devcontainer";
@@ -10853,7 +11588,7 @@ function readRegularFile(filePath) {
10853
11588
  return readRegularFileBytes(filePath)?.toString("utf-8");
10854
11589
  }
10855
11590
  function adapterFingerprint(adapter) {
10856
- return (0, import_node_crypto16.createHash)("sha256").update(adapter).digest("hex");
11591
+ return (0, import_node_crypto17.createHash)("sha256").update(adapter).digest("hex");
10857
11592
  }
10858
11593
  function resolveProcessHelperPath() {
10859
11594
  const candidates = [
@@ -11006,6 +11741,56 @@ function runManagedProcessAction(options) {
11006
11741
  }
11007
11742
 
11008
11743
  // src/core/managed-runtime-status.ts
11744
+ function reliabilityRecovery(reason2, repoPath) {
11745
+ switch (reason2) {
11746
+ case "stop-incomplete":
11747
+ case "operation-unknown":
11748
+ return [`Run: devrouter stop ${repoPath}`, `Run: devrouter ensure ${repoPath} --repair`];
11749
+ case "unadmittable":
11750
+ return [
11751
+ `Run: devrouter stop ${repoPath}`,
11752
+ `Lower the requested capacity in .devrouter.yml, then run: devrouter ensure ${repoPath}`
11753
+ ];
11754
+ case "capacity-parked":
11755
+ return [`Run: devrouter ensure ${repoPath} (waits for capacity to free up)`];
11756
+ case "capacity-waiting":
11757
+ return [`Run: devrouter ensure ${repoPath} (still queued until capacity frees up)`];
11758
+ default:
11759
+ return [];
11760
+ }
11761
+ }
11762
+ function readReliabilityStatus(options) {
11763
+ let provider;
11764
+ try {
11765
+ provider = resolveWorkspaceRuntimeForReport(options.repoPath);
11766
+ } catch {
11767
+ return void 0;
11768
+ }
11769
+ if (!provider) return void 0;
11770
+ let record5;
11771
+ try {
11772
+ record5 = readReliabilityOperation({
11773
+ repoPath: options.repoPath,
11774
+ workspace: options.workspace ?? null,
11775
+ provider
11776
+ });
11777
+ } catch {
11778
+ return void 0;
11779
+ }
11780
+ if (!record5) return void 0;
11781
+ const reason2 = reliabilityAttention(record5.state);
11782
+ return {
11783
+ desired: record5.state.desired,
11784
+ phase: record5.state.phase,
11785
+ admission: record5.state.admission,
11786
+ chargeHeld: record5.state.chargeHeld,
11787
+ incident: record5.state.incident ? {
11788
+ correctiveActionsTaken: record5.state.incident.correctiveActionsTaken,
11789
+ actionLimit: record5.state.incident.actionLimit
11790
+ } : null,
11791
+ ...reason2 ? { attention: { reason: reason2, recovery: reliabilityRecovery(reason2, options.repoPath) } } : {}
11792
+ };
11793
+ }
11009
11794
  var EMPTY_RESOURCES = {
11010
11795
  apps: [],
11011
11796
  services: [],
@@ -11405,10 +12190,15 @@ function collectManagedRuntimeStatus(options) {
11405
12190
  services: inspection.activeServices,
11406
12191
  processes: inspection.activeProcesses
11407
12192
  };
12193
+ const reliability = readReliabilityStatus({
12194
+ repoPath: options.repoPath,
12195
+ ...options.workspace !== void 0 ? { workspace: options.workspace } : {}
12196
+ });
11408
12197
  return {
11409
12198
  mode: "managed",
11410
12199
  status,
11411
12200
  profile: options.profile,
12201
+ ...reliability ? { reliability } : {},
11412
12202
  ...state?.profile ? { activeProfile: state.profile } : {},
11413
12203
  ...options.workspace !== void 0 ? { workspace: options.workspace } : {},
11414
12204
  ...state?.devpodId ? { devpodId: state.devpodId } : {},
@@ -11706,10 +12496,10 @@ function inspectLegacyNetworkCapacity(input) {
11706
12496
  }
11707
12497
 
11708
12498
  // src/core/network-managed.ts
11709
- var import_node_crypto19 = require("crypto");
12499
+ var import_node_crypto20 = require("crypto");
11710
12500
 
11711
12501
  // src/core/network-compose.ts
11712
- var import_node_crypto17 = require("crypto");
12502
+ var import_node_crypto18 = require("crypto");
11713
12503
  var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
11714
12504
  function isRecord5(value) {
11715
12505
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -12515,12 +13305,12 @@ function stableValue(value) {
12515
13305
  }
12516
13306
  function fingerprintComposeNetworkModel(input) {
12517
13307
  const projection = normalizedFingerprintProjection(input);
12518
- return (0, import_node_crypto17.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
13308
+ return (0, import_node_crypto18.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
12519
13309
  }
12520
13310
 
12521
13311
  // src/core/network-compose-files.ts
12522
13312
  var import_node_child_process25 = require("child_process");
12523
- var import_node_crypto18 = require("crypto");
13313
+ var import_node_crypto19 = require("crypto");
12524
13314
  var import_node_fs33 = __toESM(require("fs"));
12525
13315
  var import_node_path38 = __toESM(require("path"));
12526
13316
  var import_yaml4 = __toESM(require("yaml"));
@@ -12621,7 +13411,7 @@ ${JSON.stringify(extended.config, null, 2)}
12621
13411
  ...plan,
12622
13412
  contents,
12623
13413
  composeFiles: [...plan.composeFiles, overlayPath],
12624
- effectiveConfigSha256: (0, import_node_crypto18.createHash)("sha256").update(contents).digest("hex")
13414
+ effectiveConfigSha256: (0, import_node_crypto19.createHash)("sha256").update(contents).digest("hex")
12625
13415
  };
12626
13416
  return {
12627
13417
  plan: result,
@@ -12939,7 +13729,7 @@ function createManagedNetworkSession(input) {
12939
13729
  "Insufficient endpoint headroom; request an allowed larger network before first allocation."
12940
13730
  );
12941
13731
  const authority = readNetworkOperationAuthority(identity2);
12942
- const fingerprint = (0, import_node_crypto19.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
13732
+ const fingerprint = (0, import_node_crypto20.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
12943
13733
  if (!saved) {
12944
13734
  if (policy.status !== "valid")
12945
13735
  throw new Error("New network allocation requires valid machine policy.");
@@ -13595,6 +14385,15 @@ function resolveRepairTarget(repoPath, linked) {
13595
14385
  }
13596
14386
  return { kind: "primary", devpodId: state.devpodId, hadExactDevpod: true };
13597
14387
  }
14388
+ var RepairBaselineMismatchError = class extends Error {
14389
+ constructor(reasons) {
14390
+ super(
14391
+ `Repair requires the recorded profile, resource sets, and unchanged managed configuration. [MANAGED_REPAIR_BASELINE_MISMATCH: ${reasons.join(", ")}] Inspect the retained baseline before choosing recovery; missing routes do not prove configuration ownership.`
14392
+ );
14393
+ this.reasons = reasons;
14394
+ }
14395
+ code = "MANAGED_REPAIR_BASELINE_MISMATCH";
14396
+ };
13598
14397
  function assertRepairBaseline(options) {
13599
14398
  const { repoPath, target, state, plan, config, profile, processes } = options;
13600
14399
  const desired = {
@@ -13602,14 +14401,23 @@ function assertRepairBaseline(options) {
13602
14401
  services: plan.desiredProfileServices,
13603
14402
  processes
13604
14403
  };
13605
- if (state.profile !== profile || state.sourceConfigSha256 !== plan.sourceConfigSha256 || state.effectiveConfigSha256 !== plan.effectiveConfigSha256 || Object.keys(desired).some((key) => {
13606
- const resource = key;
13607
- return JSON.stringify([...desired[resource]].sort()) !== JSON.stringify([...state.desired[resource]].sort());
13608
- }) || inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
13609
- throw new Error(
13610
- "Repair requires the recorded profile, resource sets, and unchanged managed configuration."
13611
- );
14404
+ const reasons = [];
14405
+ if (state.profile !== profile) reasons.push("profile");
14406
+ if (state.sourceConfigSha256 !== plan.sourceConfigSha256) reasons.push("source-config");
14407
+ if (state.effectiveConfigSha256 !== plan.effectiveConfigSha256) reasons.push("effective-config");
14408
+ for (const resource of ["apps", "services", "processes"]) {
14409
+ if (JSON.stringify([...desired[resource]].sort()) !== JSON.stringify([...state.desired[resource]].sort())) {
14410
+ reasons.push(resource);
14411
+ }
13612
14412
  }
14413
+ if (reasons.length > 0) throw new RepairBaselineMismatchError(reasons);
14414
+ let generated;
14415
+ try {
14416
+ generated = inspectManagedDevcontainerGeneratedConfig(plan).status;
14417
+ } catch {
14418
+ throw new RepairBaselineMismatchError(["generated-unavailable"]);
14419
+ }
14420
+ if (generated !== "valid") throw new RepairBaselineMismatchError([`generated-${generated}`]);
13613
14421
  if (options.routes.some(
13614
14422
  (route) => route.workspace !== target.workspace || !desired.apps.includes(route.name)
13615
14423
  )) {
@@ -13766,6 +14574,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
13766
14574
  ...options.repair ? { allowRestart: false } : {}
13767
14575
  };
13768
14576
  try {
14577
+ reportLifecycleProgress("validation");
13769
14578
  const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
13770
14579
  let devpodId = target.devpodId;
13771
14580
  if (target.kind === "linked") {
@@ -13826,6 +14635,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
13826
14635
  });
13827
14636
  managedWorkspaceEnv = target.kind === "linked" ? { token: target.workspace, gitCommonDir: target.gitCommonDir } : void 0;
13828
14637
  if (previousManagedState && isWarmWorkspaceActive(repoPath) && previousManagedState.sourceConfigSha256 !== managedPlan.sourceConfigSha256) {
14638
+ if (options.repair) throw new RepairBaselineMismatchError(["source-config"]);
13829
14639
  throw new Error(
13830
14640
  `Managed Dev Container source configuration changed since the last successful transition; a warm ensure cannot apply it to the existing runtime for '${repoPath}'. Stop and delete the exact runtime, then run ensure again to recreate it from the current source configuration.`
13831
14641
  );
@@ -13936,6 +14746,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
13936
14746
  const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
13937
14747
  const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
13938
14748
  const startAndProveAttachment = async (recreate = false) => {
14749
+ reportLifecycleProgress("provider");
13939
14750
  const requestedTarget = currentTarget();
13940
14751
  try {
13941
14752
  devpodId = await startDevpodWorkspace({
@@ -13991,6 +14802,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
13991
14802
  if (stopped.length > 0) {
13992
14803
  repairMutationStarted = true;
13993
14804
  transitionPhase = "service-start";
14805
+ reportLifecycleProgress("service-start");
13994
14806
  claimLifecycleEffect();
13995
14807
  const result = (0, import_node_child_process27.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
13996
14808
  ...networkDockerOptions(),
@@ -14028,6 +14840,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14028
14840
  managedContainer = container;
14029
14841
  if (managedPlan) {
14030
14842
  transitionPhase = "service-start";
14843
+ reportLifecycleProgress("service-start");
14031
14844
  const initialContainers = inspectWorkspaceContainers();
14032
14845
  managedComposeProject = resolveComposeProject(
14033
14846
  initialContainers,
@@ -14163,6 +14976,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14163
14976
  if (options.repair && managedPlan)
14164
14977
  assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
14165
14978
  transitionPhase = "process-start";
14979
+ reportLifecycleProgress("process-start");
14166
14980
  claimLifecycleEffect();
14167
14981
  runManagedPostStart({
14168
14982
  plan: managedPostStart,
@@ -14187,6 +15001,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14187
15001
  }
14188
15002
  continue;
14189
15003
  }
15004
+ reportLifecycleProgress("process-stop");
14190
15005
  claimLifecycleEffect();
14191
15006
  runManagedProcessAction({
14192
15007
  container,
@@ -14207,6 +15022,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14207
15022
  }
14208
15023
  }
14209
15024
  transitionPhase = "service-stop";
15025
+ reportLifecycleProgress("service-stop");
14210
15026
  if (!options.repair)
14211
15027
  stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
14212
15028
  assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
@@ -14220,6 +15036,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14220
15036
  );
14221
15037
  }
14222
15038
  transitionPhase = "route-publication";
15039
+ reportLifecycleProgress("route-publication");
14223
15040
  claimLifecycleEffect();
14224
15041
  const publication = await replacePublishedProxyRoutes(
14225
15042
  repoPath,
@@ -14237,6 +15054,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14237
15054
  );
14238
15055
  let applicationReadiness;
14239
15056
  try {
15057
+ reportLifecycleProgress("readiness");
14240
15058
  applicationReadiness = await waitForHttpRoutes(
14241
15059
  repoPath,
14242
15060
  apps,
@@ -14464,6 +15282,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14464
15282
  const rollbackErrors = [];
14465
15283
  const failedPhase = transitionPhase;
14466
15284
  transitionPhase = "rollback";
15285
+ reportLifecycleProgress("rollback");
14467
15286
  const previousServices = previousManagedState?.desired.services ?? firstTransitionBaseline?.services ?? [];
14468
15287
  const previousProcesses = previousManagedState?.desired.processes ?? firstTransitionBaseline?.processes ?? [];
14469
15288
  const previousServiceSet = new Set(previousServices);
@@ -14695,6 +15514,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14695
15514
  repair: Boolean(options.repair)
14696
15515
  });
14697
15516
  if (preparationConfig.managedRuntime?.devcontainer.prepareCommand) {
15517
+ reportLifecycleProgress("preparation");
14698
15518
  claimLifecycleEffect();
14699
15519
  await runManagedHostPreparation(repoPath, preparationConfig);
14700
15520
  claimLifecycleEffect();
@@ -15003,7 +15823,7 @@ function proveLifecycleStopped() {
15003
15823
  throw new Error("Full stop proof requires an explicit stop worker.");
15004
15824
  const settle = () => {
15005
15825
  claimLifecycleEffect();
15006
- if (initialStopAbsence && !(0, import_node_util6.isDeepStrictEqual)(
15826
+ if (initialStopAbsence && !(0, import_node_util7.isDeepStrictEqual)(
15007
15827
  proveInitialManagedDevsyAbsence(request.repoPath, initialStopAbsence.record.devpodId),
15008
15828
  initialStopAbsence
15009
15829
  ))
@@ -15052,7 +15872,7 @@ function proveLifecycleStopped() {
15052
15872
  return void 0;
15053
15873
  });
15054
15874
  if (settlement) {
15055
- const capacity = new CapacityStore(import_node_path40.default.join(DEVROUTER_HOME, "controller"));
15875
+ const capacity = createLifecycleCapacityStore(import_node_path40.default.join(DEVROUTER_HOME, "controller"));
15056
15876
  for (let attempt = 0; ; attempt++) {
15057
15877
  try {
15058
15878
  capacity.settleEnvironmentAfterStop(settlement, capacity.read().revision);
@@ -15213,7 +16033,7 @@ async function devpodExecOutcome(repoPath, command) {
15213
16033
  }
15214
16034
  assertRunningDevpod(devpod.id, repoPath);
15215
16035
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
15216
- const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto21.randomUUID)()}__:`;
16036
+ const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto22.randomUUID)()}__:`;
15217
16037
  const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
15218
16038
  const literalCommand = command.map(quotePosixArg).join(" ");
15219
16039
  const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
@@ -15529,6 +16349,7 @@ async function workspaceDeleteOwnedPath(worktreePath, opts = {}) {
15529
16349
 
15530
16350
  // src/core/environment-stop.ts
15531
16351
  async function environmentStop(repoPath, options = {}) {
16352
+ reportLifecycleProgress("stop");
15532
16353
  const linked = isLinkedWorktree(repoPath);
15533
16354
  const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
15534
16355
  if (linked && !workspace) {
@@ -15579,6 +16400,7 @@ async function environmentStop(repoPath, options = {}) {
15579
16400
  const removedRoutes = removeHostRoutesWhere(
15580
16401
  (route) => sameWorkspacePath(route.repoPath, repoPath)
15581
16402
  );
16403
+ reportLifecycleProgress("route-removal");
15582
16404
  await ensureTraefikRoutesRemoved(removedRoutes);
15583
16405
  return {
15584
16406
  kind: linked ? "linked" : "primary",
@@ -15604,6 +16426,10 @@ process.on("disconnect", cancel);
15604
16426
  process.once("message", async (message) => {
15605
16427
  started = true;
15606
16428
  let result;
16429
+ const stopProgress = message.request.kind === "exec" ? void 0 : installLifecycleProgressSender((progress, done) => {
16430
+ if (process.connected && process.send) process.send(progress, () => done());
16431
+ else done();
16432
+ });
15607
16433
  try {
15608
16434
  const request = message.request;
15609
16435
  const value = await executeLifecycleWorker(request, async () => {
@@ -15641,6 +16467,8 @@ process.once("message", async (message) => {
15641
16467
  } catch {
15642
16468
  }
15643
16469
  result = { ok: false, message: error instanceof Error ? error.message : String(error) };
16470
+ } finally {
16471
+ stopProgress?.();
15644
16472
  }
15645
16473
  if (process.connected && process.send) process.send(result, () => process.disconnect());
15646
16474
  });