@devrouter/cli 0.0.78 → 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");
@@ -1860,6 +1860,21 @@ function resolveWorkspaceRuntimeDetailed(repoPath, override) {
1860
1860
  function resolveWorkspaceRuntimeOrDefault(repoPath) {
1861
1861
  return resolveWorkspaceRuntimeDetailed(repoPath).runtime;
1862
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
+ }
1863
1878
  function resetWorkspaceRuntimeCaches() {
1864
1879
  cachedRuntime = void 0;
1865
1880
  cachedSnapshots = void 0;
@@ -2067,6 +2082,16 @@ function parseVersion(output) {
2067
2082
  /\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)\b/
2068
2083
  )?.[1];
2069
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
+ }
2070
2095
  function devsyAgentRepairSuggestion(inspection) {
2071
2096
  if (inspection.state === "stale") {
2072
2097
  return `Install Devsy ${SUPPORTED_DEVSY_VERSION} for a supported host, then run: ${DEVSY_AGENT_SETUP_COMMAND}`;
@@ -2138,7 +2163,7 @@ function inspectDevsyAgent(options = {}) {
2138
2163
  const installedVersion = parseVersion(resolved.versionOutput);
2139
2164
  const explicitPath = resolved.env.DEVSY_AGENT_BINARY?.trim();
2140
2165
  const source = explicitPath ? "explicit" : "managed";
2141
- if (installedVersion !== SUPPORTED_DEVSY_VERSION) {
2166
+ if (installedVersion === void 0 || installedVersion !== SUPPORTED_DEVSY_VERSION && compareVersionCore(installedVersion, SUPPORTED_DEVSY_VERSION) <= 0) {
2142
2167
  return {
2143
2168
  state: "stale",
2144
2169
  source,
@@ -2154,6 +2179,15 @@ function inspectDevsyAgent(options = {}) {
2154
2179
  installedVersion
2155
2180
  };
2156
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
+ }
2157
2191
  const expectedName = resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
2158
2192
  const asset = resolved.assets.find((candidate) => candidate.name === expectedName);
2159
2193
  if (!asset) {
@@ -2175,9 +2209,7 @@ function inspectDevsyAgent(options = {}) {
2175
2209
  }
2176
2210
  function requireReadyDevsyAgent(options = {}) {
2177
2211
  const inspection = inspectDevsyAgent(options);
2178
- if (inspection.state !== "ready" || !inspection.binaryPath || !inspection.asset) {
2179
- throw new DevsyAgentReadinessError(inspection);
2180
- }
2212
+ if (inspection.state !== "ready") throw new DevsyAgentReadinessError(inspection);
2181
2213
  return {
2182
2214
  binaryPath: inspection.binaryPath,
2183
2215
  source: inspection.source,
@@ -3982,6 +4014,45 @@ function proveReplacementStopAbsence(state, baseline, replacement) {
3982
4014
  );
3983
4015
  assertEmptyPopulation();
3984
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
+ }
3985
4056
  function proveManagedStop(state) {
3986
4057
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
3987
4058
  const owner = inspectDevsyWorkspaceOwnership(
@@ -3993,6 +4064,10 @@ function proveManagedStop(state) {
3993
4064
  const identityKeys = ["context", "providerId", "uid", "sourcePath", "sourceContainer"];
3994
4065
  const current = identityOf(owner.workspace);
3995
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
+ }
3996
4071
  if (changed.length === 1 && changed[0] === "uid" && !baseline.sourceContainer && !current.sourceContainer && state.workspace !== void 0 && isLinkedWorktree(state.repoPath)) {
3997
4072
  proveReplacementStopAbsence(state, baseline, { uid: current.uid || void 0 });
3998
4073
  return { status: "proven-absent", containers: [] };
@@ -4087,6 +4162,9 @@ function validOperation(value) {
4087
4162
  "COMPLETION_UNKNOWN"
4088
4163
  ]) && (value.status === "COMPLETED" ? isReliabilityCounter(value.exitCode) : value.exitCode === null);
4089
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
+ }
4090
4168
  function assertReliabilityState(value) {
4091
4169
  const intentRevision = record2(value) ? value.intentRevision : void 0;
4092
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(
@@ -4111,7 +4189,7 @@ function assertReliabilityState(value) {
4111
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
4112
4190
  ) && boundedArray(value.observations) && uniqueIds(value.observations, "capability") && value.observations.every(
4113
4191
  (observation) => record2(observation) && oneOf(observation.infrastructure, ["healthy", "failed", "unknown"]) && oneOf(observation.application, ["verified", "unready", "unverified"]) && isReliabilityCounter(observation.observedAtMs) && isReliabilityCounter(observation.validForMs)
4114
- ) && (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)));
4115
4193
  if (!valid) throw new Error("Invalid reliability state.");
4116
4194
  }
4117
4195
  function createReliabilityState(environmentId, controllerEpoch, executionPolicy = "capacity-managed") {
@@ -4214,6 +4292,15 @@ function evaluateCapacity(budgets, samples, charges, requested, nowMs, maxSample
4214
4292
 
4215
4293
  // src/core/capacity-store.ts
4216
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
+ };
4217
4304
  var CapacitySnapshotChangedError = class extends Error {
4218
4305
  constructor() {
4219
4306
  super("Capacity snapshot changed.");
@@ -4305,18 +4392,57 @@ function serializeSnapshot(snapshot) {
4305
4392
  return contents;
4306
4393
  }
4307
4394
  var CapacityStore = class {
4308
- constructor(directory) {
4395
+ constructor(directory, write = writeFileAtomically, minimumRevision = () => 0) {
4309
4396
  this.directory = directory;
4397
+ this.write = write;
4398
+ this.minimumRevision = minimumRevision;
4310
4399
  this.file = import_node_path19.default.join(directory, "capacity-reservations.json");
4400
+ this.establishedFile = import_node_path19.default.join(directory, "capacity-ledger.established");
4311
4401
  }
4312
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
+ }
4313
4431
  read() {
4432
+ const minimumRevision = this.minimumRevision();
4433
+ const established = this.readEstablished();
4314
4434
  let descriptor;
4315
4435
  try {
4316
- 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
+ );
4317
4440
  } catch (error) {
4318
- if (error.code === "ENOENT")
4441
+ if (error.code === "ENOENT") {
4442
+ if (established || this.observedLedger || minimumRevision > 0)
4443
+ throw new CapacityHistoryError("capacity-ledger-lost");
4319
4444
  return { version: 1, revision: 0, reservations: [] };
4445
+ }
4320
4446
  throw error;
4321
4447
  }
4322
4448
  try {
@@ -4328,11 +4454,40 @@ var CapacityStore = class {
4328
4454
  if (count > MAX_BYTES) throw new Error("Capacity reservation snapshot exceeds byte limit.");
4329
4455
  const value = JSON.parse(buffer.subarray(0, count).toString("utf8"));
4330
4456
  validate(value);
4457
+ if (value.revision < minimumRevision) throw new CapacityHistoryError("capacity-ledger-lost");
4458
+ this.observedLedger = true;
4331
4459
  return value;
4332
4460
  } finally {
4333
4461
  import_node_fs15.default.closeSync(descriptor);
4334
4462
  }
4335
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
+ }
4336
4491
  /**
4337
4492
  * Caller must revoke journal authority and prove exact physical cessation first.
4338
4493
  * An absent reservation still advances the revision to fence that proof.
@@ -4346,7 +4501,7 @@ var CapacityStore = class {
4346
4501
  `${this.file}.lock`,
4347
4502
  { activity: "capacity settlement", waitMs: 100 },
4348
4503
  () => {
4349
- const snapshot = this.read();
4504
+ const snapshot = this.readForMutation();
4350
4505
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4351
4506
  if (snapshot.revision === Number.MAX_SAFE_INTEGER)
4352
4507
  throw new Error("Capacity reservation revision exhausted.");
@@ -4356,7 +4511,7 @@ var CapacityStore = class {
4356
4511
  if (index >= 0) snapshot.reservations.splice(index, 1);
4357
4512
  snapshot.revision++;
4358
4513
  validate(snapshot);
4359
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4514
+ this.commit(snapshot);
4360
4515
  return { settled: index >= 0, revision: snapshot.revision };
4361
4516
  }
4362
4517
  );
@@ -4371,16 +4526,19 @@ var CapacityStore = class {
4371
4526
  `${this.file}.lock`,
4372
4527
  { activity: "capacity pool observation", waitMs: 100 },
4373
4528
  () => {
4374
- const snapshot = this.read();
4529
+ const snapshot = this.readForMutation();
4375
4530
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4376
4531
  const merged = structuredClone(snapshot.pools ?? []);
4377
- 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
+ }
4378
4536
  if (snapshot.revision === Number.MAX_SAFE_INTEGER)
4379
4537
  throw new Error("Capacity reservation revision exhausted.");
4380
4538
  snapshot.pools = merged;
4381
4539
  snapshot.revision++;
4382
4540
  validate(snapshot);
4383
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4541
+ this.commit(snapshot);
4384
4542
  return { changed: true, revision: snapshot.revision };
4385
4543
  }
4386
4544
  );
@@ -4399,7 +4557,7 @@ var CapacityStore = class {
4399
4557
  `${this.file}.lock`,
4400
4558
  { activity: "capacity pool settlement", waitMs: 100 },
4401
4559
  () => {
4402
- const snapshot = this.read();
4560
+ const snapshot = this.readForMutation();
4403
4561
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4404
4562
  const pools = snapshot.pools ?? [];
4405
4563
  const index = pools.findIndex((pool2) => pool2.daemonId === identity2.daemonId);
@@ -4413,7 +4571,7 @@ var CapacityStore = class {
4413
4571
  if (pool) pools.splice(index, 1);
4414
4572
  snapshot.revision++;
4415
4573
  validate(snapshot);
4416
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4574
+ this.commit(snapshot);
4417
4575
  return { settled: !!pool, revision: snapshot.revision };
4418
4576
  }
4419
4577
  );
@@ -4428,7 +4586,7 @@ var CapacityStore = class {
4428
4586
  `${this.file}.lock`,
4429
4587
  { activity: "capacity phase settlement", waitMs: 100 },
4430
4588
  () => {
4431
- const snapshot = this.read();
4589
+ const snapshot = this.readForMutation();
4432
4590
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4433
4591
  const index = snapshot.reservations.findIndex(
4434
4592
  (entry) => entry.environmentId === target.environmentId
@@ -4445,7 +4603,7 @@ var CapacityStore = class {
4445
4603
  snapshot.reservations[index] = structuredClone(target);
4446
4604
  snapshot.revision++;
4447
4605
  validate(snapshot);
4448
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4606
+ this.commit(snapshot);
4449
4607
  }
4450
4608
  );
4451
4609
  }
@@ -4463,7 +4621,7 @@ var CapacityStore = class {
4463
4621
  `${this.file}.lock`,
4464
4622
  { activity: "capacity reservation", waitMs: 100 },
4465
4623
  () => {
4466
- const snapshot = this.read();
4624
+ const snapshot = this.readForMutation();
4467
4625
  if (snapshot.revision !== expectedRevision) throw new CapacitySnapshotChangedError();
4468
4626
  const pools = structuredClone(snapshot.pools ?? []);
4469
4627
  const poolChanged = mergePools(pools, requestedPool === void 0 ? [] : [requestedPool]);
@@ -4508,7 +4666,7 @@ var CapacityStore = class {
4508
4666
  snapshot.reservations[snapshot.reservations.indexOf(existing)] = structuredClone(request);
4509
4667
  else snapshot.reservations.push(structuredClone(request));
4510
4668
  validate(snapshot);
4511
- writeFileAtomically(this.file, serializeSnapshot(snapshot));
4669
+ this.commit(snapshot);
4512
4670
  return { admitted: true, revision: snapshot.revision, joined: false };
4513
4671
  }
4514
4672
  );
@@ -4520,6 +4678,28 @@ var import_node_crypto7 = require("crypto");
4520
4678
  var import_node_fs16 = __toESM(require("fs"));
4521
4679
  var import_node_path20 = __toESM(require("path"));
4522
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
+ ];
4523
4703
  function fail() {
4524
4704
  throw new Error("Invalid controller snapshot.");
4525
4705
  }
@@ -4537,38 +4717,150 @@ function id3(value) {
4537
4717
  function counter(value) {
4538
4718
  return Number.isSafeInteger(value) && Number(value) >= 0;
4539
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
+ }
4540
4773
  function validateControllerSnapshot(value) {
4541
4774
  fields(value, [
4542
4775
  "version",
4543
4776
  "store",
4544
4777
  "epoch",
4545
4778
  "revision",
4779
+ "parkingRevision",
4780
+ "history",
4546
4781
  "nextSequence",
4547
4782
  "environments",
4548
4783
  "sessions",
4549
- "events"
4784
+ "retainedSessions",
4785
+ "events",
4786
+ ...value?.version === 3 ? ["fingerprint"] : []
4550
4787
  ]);
4551
- 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)
4552
4789
  fail();
4553
- const environments = /* @__PURE__ */ new Set();
4554
- const checkouts = /* @__PURE__ */ new Set();
4555
- for (const env of value.environments) {
4556
- 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, [
4557
4805
  "id",
4558
- "repoPath",
4559
- "workspace",
4560
- "provider",
4561
- "providerId",
4562
- "profile",
4563
- "fingerprint"
4806
+ "environmentId",
4807
+ "generation",
4808
+ "requirements",
4809
+ "renewedAtMs",
4810
+ "parkingConsent",
4811
+ "consentRevision",
4812
+ ...Object.hasOwn(session, "observation") ? ["observation"] : []
4564
4813
  ]);
4565
- 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))
4566
4816
  fail();
4567
- environments.add(env.id);
4568
- checkouts.add(env.repoPath);
4817
+ activeSessions.add(session.id);
4818
+ usedEnvironments.add(session.environmentId);
4819
+ tuples.add(tuple);
4820
+ checkProjection(session);
4569
4821
  }
4570
- 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());
4571
4862
  const usedEnvironments = /* @__PURE__ */ new Set();
4863
+ const sessions = /* @__PURE__ */ new Set();
4572
4864
  for (const session of value.sessions) {
4573
4865
  fields(session, [
4574
4866
  "id",
@@ -4578,60 +4870,66 @@ function validateControllerSnapshot(value) {
4578
4870
  "renewedAtMs",
4579
4871
  ...Object.hasOwn(session, "observation") ? ["observation"] : []
4580
4872
  ]);
4581
- 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(
4582
- (r) => !text2(r, 132) || !/^(runtime|app:[a-z0-9][a-z0-9-]*)$/.test(r)
4583
- ))
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))
4584
4874
  fail();
4585
- if (session.observation !== void 0) {
4586
- const observation = session.observation;
4587
- fields(observation, [
4588
- "status",
4589
- "sampledAtMs",
4590
- "validUntilMs",
4591
- "journalRevision",
4592
- "runtimeFingerprint"
4593
- ]);
4594
- if (![
4595
- "READY",
4596
- "APP_ERROR",
4597
- "BLOCKED",
4598
- "STOPPED",
4599
- "STARTING",
4600
- "RECOVERING",
4601
- "WAITING_CAPACITY",
4602
- "PARKED_CAPACITY",
4603
- "UNKNOWN"
4604
- ].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))
4605
- fail();
4606
- }
4607
4875
  sessions.add(session.id);
4608
4876
  usedEnvironments.add(session.environmentId);
4877
+ checkProjection(session);
4609
4878
  }
4610
- if (usedEnvironments.size !== environments.size) fail();
4611
- let previous = 0;
4612
- for (const event of value.events) {
4613
- fields(event, ["sequence", "session", "generation", "kind"]);
4614
- if (!counter(event.sequence) || event.sequence <= previous || event.sequence >= value.nextSequence || !id3(event.session) || !id3(event.generation) || !["acquired", "renewed", "released", "expired", "invalidated", "observed"].includes(
4615
- String(event.kind)
4616
- ))
4617
- fail();
4618
- 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);
4619
4911
  }
4620
- if (Buffer.byteLength(JSON.stringify(value.events)) > 262144 || Buffer.byteLength(JSON.stringify(value)) + 1 > CONTROLLER_SNAPSHOT_BYTES)
4621
- fail();
4912
+ validateControllerSnapshot(value);
4913
+ return value;
4622
4914
  }
4623
4915
  var ControllerStore = class {
4624
- constructor(directory, write = writeFileAtomically) {
4916
+ constructor(directory, write = writeFileAtomically, ownsLifetimeLock = false) {
4625
4917
  this.directory = directory;
4626
4918
  this.write = write;
4919
+ this.ownsLifetimeLock = ownsLifetimeLock;
4627
4920
  this.file = import_node_path20.default.join(directory, "snapshot.json");
4921
+ this.identityFile = import_node_path20.default.join(directory, "store-identity.json");
4628
4922
  }
4629
4923
  file;
4630
- read() {
4924
+ identityFile;
4925
+ identity;
4926
+ observedStore;
4927
+ observedFingerprint;
4928
+ readBytes(file = this.file, limit = CONTROLLER_SNAPSHOT_BYTES) {
4631
4929
  let fd;
4632
4930
  try {
4633
4931
  fd = import_node_fs16.default.openSync(
4634
- this.file,
4932
+ file,
4635
4933
  import_node_fs16.default.constants.O_RDONLY | import_node_fs16.default.constants.O_NOFOLLOW | import_node_fs16.default.constants.O_NONBLOCK
4636
4934
  );
4637
4935
  } catch (error) {
@@ -4640,39 +4938,150 @@ var ControllerStore = class {
4640
4938
  }
4641
4939
  try {
4642
4940
  const stat = import_node_fs16.default.fstatSync(fd);
4643
- 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)
4644
4942
  fail();
4645
- const bytes2 = Buffer.alloc(CONTROLLER_SNAPSHOT_BYTES + 1);
4943
+ const bytes2 = Buffer.alloc(limit + 1);
4646
4944
  let length = 0;
4647
4945
  while (length < bytes2.length) {
4648
4946
  const count = import_node_fs16.default.readSync(fd, bytes2, length, bytes2.length - length, null);
4649
4947
  if (!count) break;
4650
4948
  length += count;
4651
4949
  }
4652
- if (length > CONTROLLER_SNAPSHOT_BYTES) fail();
4653
- const value = JSON.parse(bytes2.subarray(0, length).toString("utf8"));
4654
- validateControllerSnapshot(value);
4655
- return value;
4950
+ if (length > limit) fail();
4951
+ return bytes2.subarray(0, length);
4656
4952
  } catch {
4657
4953
  throw new Error("Controller snapshot is unreadable or invalid.");
4658
4954
  } finally {
4659
4955
  import_node_fs16.default.closeSync(fd);
4660
4956
  }
4661
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
+ }
4662
5065
  persist(snapshot) {
4663
5066
  validateControllerSnapshot(snapshot);
4664
- 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());
4665
5071
  const contents = `${JSON.stringify(snapshot)}
4666
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) {
4667
5077
  try {
4668
- this.write(this.file, contents);
5078
+ this.write(file, contents);
4669
5079
  } catch {
4670
- const current = this.read();
4671
- if (!current || `${JSON.stringify(current)}
4672
- ` !== contents)
5080
+ const stored = this.readBytes(file, limit);
5081
+ if (!stored || stored.toString("utf8") !== contents)
4673
5082
  throw new Error("Controller snapshot commit failed.");
4674
- for (const file of [this.file, this.directory]) {
4675
- 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);
4676
5085
  try {
4677
5086
  import_node_fs16.default.fsyncSync(fd);
4678
5087
  } finally {
@@ -4682,27 +5091,66 @@ var ControllerStore = class {
4682
5091
  }
4683
5092
  }
4684
5093
  startIncarnation() {
4685
- const previous = this.read();
4686
- 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))
4687
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);
4688
5116
  const snapshot = {
4689
- version: 1,
4690
- store: previous?.store ?? (0, import_node_crypto7.randomUUID)(),
5117
+ version: 3,
5118
+ fingerprint,
5119
+ store: previous.store,
4691
5120
  epoch: (previous?.epoch ?? 0) + 1,
4692
5121
  revision: (previous?.revision ?? 0) + 1,
5122
+ parkingRevision: pristine ? 0 : previous.parkingRevision + 1,
5123
+ history: previous?.history ?? "complete",
4693
5124
  nextSequence: 1,
4694
5125
  environments: [],
4695
5126
  sessions: [],
5127
+ retainedSessions: previous ? [...previous.retainedSessions, ...previous.sessions.map((s) => retain(s, previous))] : [],
4696
5128
  events: []
4697
5129
  };
4698
5130
  this.persist(snapshot);
4699
5131
  return snapshot;
4700
5132
  }
4701
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
+ }
4702
5150
 
4703
5151
  // src/core/reliability-operation-store.ts
4704
5152
  function installedCliVersion() {
4705
- return true ? "0.0.78" : "0.0.0-dev";
5153
+ return true ? "0.0.79" : "0.0.0-dev";
4706
5154
  }
4707
5155
  function compareCliVersions(left, right) {
4708
5156
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -4717,6 +5165,10 @@ function compareCliVersions(left, right) {
4717
5165
  var MAX_RECORD_BYTES = 1048576;
4718
5166
  var MAX_RELIABILITY_JOURNALS = 256;
4719
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$/;
4720
5172
  function identityKey(identity2) {
4721
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)) {
4722
5174
  throw new Error("Invalid reliability workspace identity.");
@@ -4736,6 +5188,17 @@ function exactKeys(value, expected, message) {
4736
5188
  throw new Error(message);
4737
5189
  }
4738
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
+ }
4739
5202
  function validateCapacityPhaseSettlement(record5, state, enrollment) {
4740
5203
  const settlement = record5.phaseSettlement;
4741
5204
  if (settlement === void 0 || settlement === null) return;
@@ -4831,6 +5294,7 @@ function validate2(record5, identity2) {
4831
5294
  "outcome",
4832
5295
  "result",
4833
5296
  "writtenByVersion",
5297
+ "consumerProtection",
4834
5298
  ...record5.version === 2 ? [
4835
5299
  "capacity",
4836
5300
  "enrollment",
@@ -4840,6 +5304,7 @@ function validate2(record5, identity2) {
4840
5304
  "startupWitness"
4841
5305
  ] : []
4842
5306
  ]);
5307
+ validateConsumerProtection(record5);
4843
5308
  if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
4844
5309
  throw new Error(
4845
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.`
@@ -5007,6 +5472,19 @@ function validate2(record5, identity2) {
5007
5472
  }
5008
5473
  }
5009
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
+ }
5010
5488
  function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_path21.default.join(DEVROUTER_HOME, "controller")) {
5011
5489
  if (record5.version === 1) return;
5012
5490
  const binding = record5.capacity;
@@ -5017,7 +5495,7 @@ function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_
5017
5495
  if (!binding.controller || !controller || controller.store !== binding.controller.store || controller.epoch !== binding.controller.epoch)
5018
5496
  throw new Error("Capacity controller incarnation changed.");
5019
5497
  }
5020
- const snapshot = new CapacityStore(directory).read();
5498
+ const snapshot = createLifecycleCapacityStore(directory).read();
5021
5499
  if (binding.snapshotRevision === void 0 || snapshot.revision !== binding.snapshotRevision)
5022
5500
  throw new Error("Capacity snapshot authority is absent or stale.");
5023
5501
  const reservation = snapshot.reservations.find(
@@ -5058,6 +5536,79 @@ function readBoundedPrivateJson(file, label) {
5058
5536
  import_node_fs17.default.closeSync(descriptor);
5059
5537
  }
5060
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
+ }
5061
5612
  function persist(record5) {
5062
5613
  validate2(record5, record5.identity);
5063
5614
  const contents = `${JSON.stringify(record5)}
@@ -5101,12 +5652,15 @@ function updateReliabilityOperation(identity2, operation) {
5101
5652
  throw new Error("Reliability record revision is exhausted.");
5102
5653
  const previousVersion = record5.version;
5103
5654
  const previousEnrollment = record5.enrollment && { ...record5.enrollment };
5655
+ const previousProtection = structuredClone(record5.consumerProtection);
5104
5656
  const result = operation(record5);
5105
5657
  if (result && typeof result.then === "function") {
5106
5658
  throw new Error("Reliability record updates must be synchronous.");
5107
5659
  }
5108
5660
  if (record5.version < previousVersion || previousEnrollment && !(0, import_node_util4.isDeepStrictEqual)(record5.enrollment, previousEnrollment))
5109
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.");
5110
5664
  record5.writtenByVersion = installedCliVersion();
5111
5665
  validate2(record5, identity2);
5112
5666
  record5.revision += 1;
@@ -6402,7 +6956,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs18.default.rea
6402
6956
  const config = parseConfig(parsed ?? {}, configPath);
6403
6957
  const requiredVersion = config.devrouter?.version;
6404
6958
  if (requiredVersion && !hasWarnedVersionMismatch) {
6405
- const cliVersion = true ? "0.0.78" : "0.0.0-dev";
6959
+ const cliVersion = true ? "0.0.79" : "0.0.0-dev";
6406
6960
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
6407
6961
  hasWarnedVersionMismatch = true;
6408
6962
  process.stderr.write(
@@ -9334,7 +9888,8 @@ async function startDevsyWorkspace(options) {
9334
9888
  }
9335
9889
  if (options.recreate) args.push("--recreate");
9336
9890
  const env = network ? networkProviderEnvironment(network.binding, process.env) : { ...process.env };
9337
- env.DEVSY_AGENT_BINARY = agent.binaryPath;
9891
+ if (agent.binaryPath) env.DEVSY_AGENT_BINARY = agent.binaryPath;
9892
+ else delete env.DEVSY_AGENT_BINARY;
9338
9893
  if (options.workspace) {
9339
9894
  env.WORKSPACE = options.workspace.token;
9340
9895
  env.DEVROUTER_WORKSPACE = options.workspace.token;
@@ -9358,7 +9913,7 @@ async function startDevsyWorkspace(options) {
9358
9913
  let message = `devsy workspace up failed for '${devsyId ?? options.repoPath}'.`;
9359
9914
  if (result.error?.message) message += ` ${result.error.message}`;
9360
9915
  if (AGENT_ACQUISITION_RE.test(result.stderrTail)) {
9361
- 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.";
9362
9917
  }
9363
9918
  if (failedStartMayHaveAttached(devsyId, options.repoPath)) {
9364
9919
  throw new DevsyStartPostconditionError(message);
@@ -9398,9 +9953,9 @@ var ExecutionOutcomeError = class extends Error {
9398
9953
  };
9399
9954
 
9400
9955
  // src/core/reliability-lifecycle.ts
9401
- var import_node_crypto20 = require("crypto");
9956
+ var import_node_crypto21 = require("crypto");
9402
9957
  var import_node_path40 = __toESM(require("path"));
9403
- var import_node_util6 = require("util");
9958
+ var import_node_util7 = require("util");
9404
9959
 
9405
9960
  // src/core/capacity-policy.ts
9406
9961
  var import_node_fs26 = __toESM(require("fs"));
@@ -9421,6 +9976,76 @@ var CONTROLLER_PROFILE_RE = new RegExp(
9421
9976
  `^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
9422
9977
  );
9423
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
+
9424
10049
  // src/core/reliability-model.ts
9425
10050
  var INFRASTRUCTURE_VALUES = ["healthy", "failed", "unknown"];
9426
10051
  var APPLICATION_VALUES = ["verified", "unready", "unverified"];
@@ -9493,7 +10118,7 @@ function assertReliabilityEvent(value) {
9493
10118
  valid = isReliabilityCounter(value.type === "epoch" ? value.nextEpoch : value.nextGeneration);
9494
10119
  break;
9495
10120
  case "recover":
9496
- 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));
9497
10122
  break;
9498
10123
  case "rearm":
9499
10124
  valid = isReliabilityId(value.incidentId);
@@ -9513,7 +10138,10 @@ function cloneOperation(operation) {
9513
10138
  return { ...operation };
9514
10139
  }
9515
10140
  function cloneIncident(incident) {
9516
- return { ...incident };
10141
+ return {
10142
+ ...incident,
10143
+ ...incident.units ? { units: incident.units.map((unit) => ({ ...unit })) } : {}
10144
+ };
9517
10145
  }
9518
10146
  function cloneState(state) {
9519
10147
  return {
@@ -9565,6 +10193,12 @@ function possibleDispatch(operation) {
9565
10193
  function pendingCommand(operation) {
9566
10194
  return operation !== null && !["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(operation.status);
9567
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
+ }
9568
10202
  function setUnknown(operation) {
9569
10203
  if (!operation || !possibleDispatch(operation)) return operation;
9570
10204
  return { ...operation, status: "COMPLETION_UNKNOWN", exitCode: null };
@@ -9684,10 +10318,7 @@ function handleOperationRequest(state, event) {
9684
10318
  const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
9685
10319
  let retired = -1;
9686
10320
  if (rollover) {
9687
- const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
9688
- retired = state.operationHistory.findIndex(
9689
- (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
9690
- );
10321
+ retired = retirableEntryIndex(state);
9691
10322
  if (retired === -1)
9692
10323
  return blocked(
9693
10324
  state,
@@ -9740,7 +10371,7 @@ function handleAdmission(state, event) {
9740
10371
  return unchanged(state, "blocked");
9741
10372
  }
9742
10373
  if (state.admission === event.result) return unchanged(state, "joined");
9743
- 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")
9744
10375
  return unchanged(state, "blocked");
9745
10376
  state.admission = event.result;
9746
10377
  if (event.result === "admitted") {
@@ -9775,16 +10406,7 @@ function handleDispatchPersisted(state, event) {
9775
10406
  `dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
9776
10407
  );
9777
10408
  }
9778
- if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
9779
- return unchanged(state, "blocked");
9780
- }
9781
10409
  state.operation = { ...state.operation, status: "DISPATCH_RECORDED", exitCode: null };
9782
- if (state.phase === "recovering" && state.incident !== null) {
9783
- state.incident = {
9784
- ...state.incident,
9785
- correctiveActionsTaken: state.incident.correctiveActionsTaken + 1
9786
- };
9787
- }
9788
10410
  return transition(state, "accepted", [effect(state, "launch", event.operationId)]);
9789
10411
  }
9790
10412
  function handleLaunched(state, event) {
@@ -9917,9 +10539,18 @@ function handlePark(state) {
9917
10539
  return transition(state, "accepted", [effect(state, "stop", operationId)]);
9918
10540
  }
9919
10541
  function handleResume(state, event) {
9920
- 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) {
9921
10543
  return unchanged(state, "blocked");
9922
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
+ );
9923
10554
  const intentRevision = advance(state.intentRevision);
9924
10555
  const runtimeGeneration = advance(state.runtimeGeneration);
9925
10556
  if (intentRevision === null || runtimeGeneration === null) return unchanged(state, "blocked");
@@ -9927,9 +10558,10 @@ function handleResume(state, event) {
9927
10558
  state.runtimeGeneration = runtimeGeneration;
9928
10559
  state.desired = "running";
9929
10560
  state.phase = "queued";
10561
+ state.admission = "waiting";
9930
10562
  state.stopProof = { workloadsStopped: false, routesRemoved: false };
9931
10563
  state.observations = [];
9932
- state.chargeHeld = true;
10564
+ state.chargeHeld = false;
9933
10565
  state.operation = {
9934
10566
  id: event.operationId,
9935
10567
  kind: "ensure",
@@ -9942,6 +10574,13 @@ function handleResume(state, event) {
9942
10574
  operationId: event.operationId,
9943
10575
  intentRevision
9944
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
+ });
9945
10584
  return transition(state, "accepted");
9946
10585
  }
9947
10586
  function handleGenerationChange(state, kind, nextValue) {
@@ -9960,9 +10599,10 @@ function handleGenerationChange(state, kind, nextValue) {
9960
10599
  }
9961
10600
  return transition(state, "accepted");
9962
10601
  }
9963
- function handleRecover(state, event) {
10602
+ function handleRecover(state, event, nowMs) {
9964
10603
  if (state.executionPolicy === "manual" || state.desired !== "running")
9965
10604
  return unchanged(state, "blocked");
10605
+ if (state.phase === "stopping") return unchanged(state, "blocked");
9966
10606
  if (possibleDispatch(state.operation)) return unchanged(state, "blocked");
9967
10607
  const stranded = state.operation?.drained === true && ["NOT_STARTED", "NOT_LAUNCHED"].includes(state.operation.status);
9968
10608
  const operation = stranded ? null : state.operation;
@@ -9980,37 +10620,58 @@ function handleRecover(state, event) {
9980
10620
  return unchanged(state, "blocked");
9981
10621
  }
9982
10622
  }
9983
- if (state.incident === null) {
9984
- if (operation?.status === "NOT_STARTED") return unchanged(state, "blocked");
9985
- if (operation?.id === event.operationId) return unchanged(state, "conflict");
9986
- state.incident = {
9987
- id: event.incidentId,
9988
- correctiveActionsTaken: 0,
9989
- actionLimit: event.actionLimit
9990
- };
9991
- }
9992
- if (!operation || operation.status === "COMPLETED" || operation.status === "INTERRUPTED") {
9993
- const runtimeGeneration = advance(state.runtimeGeneration);
9994
- if (runtimeGeneration === null) return unchanged(state, "blocked");
9995
- const consumer = state.consumers[0];
9996
- if (state.profile === null || consumer === void 0) return unchanged(state, "blocked");
9997
- if (state.operationHistory.length >= RELIABILITY_MAX_ITEMS) return unchanged(state, "blocked");
9998
- state.runtimeGeneration = runtimeGeneration;
9999
- state.observations = [];
10000
- state.operation = {
10001
- id: event.operationId,
10002
- kind: "ensure",
10003
- drained: false,
10004
- status: "NOT_STARTED",
10005
- exitCode: null
10006
- };
10007
- state.operationHistory.push({
10008
- ...state.operation,
10009
- key: event.operationId,
10010
- profile: state.profile,
10011
- consumer: cloneConsumer(consumer)
10012
- });
10013
- }
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
+ });
10014
10675
  state.phase = "recovering";
10015
10676
  return transition(state, "accepted");
10016
10677
  }
@@ -10081,7 +10742,7 @@ function applyReliabilityEvent(state, event, nowMs) {
10081
10742
  case "runtime":
10082
10743
  return handleGenerationChange(next, "runtime", event.nextGeneration);
10083
10744
  case "recover":
10084
- return handleRecover(next, event);
10745
+ return handleRecover(next, event, nowMs);
10085
10746
  case "rearm":
10086
10747
  return handleRearm(next, event);
10087
10748
  }
@@ -10105,9 +10766,64 @@ function stepReliability(state, event, nowMs) {
10105
10766
 
10106
10767
  // src/core/reliability-worker.ts
10107
10768
  var import_node_child_process17 = require("child_process");
10108
- var import_node_crypto15 = require("crypto");
10769
+ var import_node_crypto16 = require("crypto");
10109
10770
  var import_node_fs28 = __toESM(require("fs"));
10110
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
10111
10827
  var DEFAULT_OUTPUT_PAGE_BYTES = 48 * 1024;
10112
10828
  function workerGroupAbsent(pid) {
10113
10829
  try {
@@ -10815,7 +11531,7 @@ async function runManagedHostPreparation(repoPath, config) {
10815
11531
 
10816
11532
  // src/core/managed-post-start.ts
10817
11533
  var import_node_child_process22 = require("child_process");
10818
- var import_node_crypto16 = require("crypto");
11534
+ var import_node_crypto17 = require("crypto");
10819
11535
  var import_node_fs31 = __toESM(require("fs"));
10820
11536
  var import_node_path36 = __toESM(require("path"));
10821
11537
  var MANAGED_MARKER = "devrouter:managed devcontainer";
@@ -10872,7 +11588,7 @@ function readRegularFile(filePath) {
10872
11588
  return readRegularFileBytes(filePath)?.toString("utf-8");
10873
11589
  }
10874
11590
  function adapterFingerprint(adapter) {
10875
- return (0, import_node_crypto16.createHash)("sha256").update(adapter).digest("hex");
11591
+ return (0, import_node_crypto17.createHash)("sha256").update(adapter).digest("hex");
10876
11592
  }
10877
11593
  function resolveProcessHelperPath() {
10878
11594
  const candidates = [
@@ -11025,6 +11741,56 @@ function runManagedProcessAction(options) {
11025
11741
  }
11026
11742
 
11027
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
+ }
11028
11794
  var EMPTY_RESOURCES = {
11029
11795
  apps: [],
11030
11796
  services: [],
@@ -11424,10 +12190,15 @@ function collectManagedRuntimeStatus(options) {
11424
12190
  services: inspection.activeServices,
11425
12191
  processes: inspection.activeProcesses
11426
12192
  };
12193
+ const reliability = readReliabilityStatus({
12194
+ repoPath: options.repoPath,
12195
+ ...options.workspace !== void 0 ? { workspace: options.workspace } : {}
12196
+ });
11427
12197
  return {
11428
12198
  mode: "managed",
11429
12199
  status,
11430
12200
  profile: options.profile,
12201
+ ...reliability ? { reliability } : {},
11431
12202
  ...state?.profile ? { activeProfile: state.profile } : {},
11432
12203
  ...options.workspace !== void 0 ? { workspace: options.workspace } : {},
11433
12204
  ...state?.devpodId ? { devpodId: state.devpodId } : {},
@@ -11725,10 +12496,10 @@ function inspectLegacyNetworkCapacity(input) {
11725
12496
  }
11726
12497
 
11727
12498
  // src/core/network-managed.ts
11728
- var import_node_crypto19 = require("crypto");
12499
+ var import_node_crypto20 = require("crypto");
11729
12500
 
11730
12501
  // src/core/network-compose.ts
11731
- var import_node_crypto17 = require("crypto");
12502
+ var import_node_crypto18 = require("crypto");
11732
12503
  var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
11733
12504
  function isRecord5(value) {
11734
12505
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -12534,12 +13305,12 @@ function stableValue(value) {
12534
13305
  }
12535
13306
  function fingerprintComposeNetworkModel(input) {
12536
13307
  const projection = normalizedFingerprintProjection(input);
12537
- 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");
12538
13309
  }
12539
13310
 
12540
13311
  // src/core/network-compose-files.ts
12541
13312
  var import_node_child_process25 = require("child_process");
12542
- var import_node_crypto18 = require("crypto");
13313
+ var import_node_crypto19 = require("crypto");
12543
13314
  var import_node_fs33 = __toESM(require("fs"));
12544
13315
  var import_node_path38 = __toESM(require("path"));
12545
13316
  var import_yaml4 = __toESM(require("yaml"));
@@ -12640,7 +13411,7 @@ ${JSON.stringify(extended.config, null, 2)}
12640
13411
  ...plan,
12641
13412
  contents,
12642
13413
  composeFiles: [...plan.composeFiles, overlayPath],
12643
- effectiveConfigSha256: (0, import_node_crypto18.createHash)("sha256").update(contents).digest("hex")
13414
+ effectiveConfigSha256: (0, import_node_crypto19.createHash)("sha256").update(contents).digest("hex")
12644
13415
  };
12645
13416
  return {
12646
13417
  plan: result,
@@ -12958,7 +13729,7 @@ function createManagedNetworkSession(input) {
12958
13729
  "Insufficient endpoint headroom; request an allowed larger network before first allocation."
12959
13730
  );
12960
13731
  const authority = readNetworkOperationAuthority(identity2);
12961
- 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");
12962
13733
  if (!saved) {
12963
13734
  if (policy.status !== "valid")
12964
13735
  throw new Error("New network allocation requires valid machine policy.");
@@ -13614,6 +14385,15 @@ function resolveRepairTarget(repoPath, linked) {
13614
14385
  }
13615
14386
  return { kind: "primary", devpodId: state.devpodId, hadExactDevpod: true };
13616
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
+ };
13617
14397
  function assertRepairBaseline(options) {
13618
14398
  const { repoPath, target, state, plan, config, profile, processes } = options;
13619
14399
  const desired = {
@@ -13621,14 +14401,23 @@ function assertRepairBaseline(options) {
13621
14401
  services: plan.desiredProfileServices,
13622
14402
  processes
13623
14403
  };
13624
- if (state.profile !== profile || state.sourceConfigSha256 !== plan.sourceConfigSha256 || state.effectiveConfigSha256 !== plan.effectiveConfigSha256 || Object.keys(desired).some((key) => {
13625
- const resource = key;
13626
- return JSON.stringify([...desired[resource]].sort()) !== JSON.stringify([...state.desired[resource]].sort());
13627
- }) || inspectManagedDevcontainerGeneratedConfig(plan).status !== "valid") {
13628
- throw new Error(
13629
- "Repair requires the recorded profile, resource sets, and unchanged managed configuration."
13630
- );
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
+ }
13631
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}`]);
13632
14421
  if (options.routes.some(
13633
14422
  (route) => route.workspace !== target.workspace || !desired.apps.includes(route.name)
13634
14423
  )) {
@@ -13785,6 +14574,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
13785
14574
  ...options.repair ? { allowRestart: false } : {}
13786
14575
  };
13787
14576
  try {
14577
+ reportLifecycleProgress("validation");
13788
14578
  const target = options.repair ? resolveRepairTarget(repoPath, linked) : linked ? resolveLinkedTarget(repoPath) : resolvePrimaryTarget(repoPath);
13789
14579
  let devpodId = target.devpodId;
13790
14580
  if (target.kind === "linked") {
@@ -13845,6 +14635,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
13845
14635
  });
13846
14636
  managedWorkspaceEnv = target.kind === "linked" ? { token: target.workspace, gitCommonDir: target.gitCommonDir } : void 0;
13847
14637
  if (previousManagedState && isWarmWorkspaceActive(repoPath) && previousManagedState.sourceConfigSha256 !== managedPlan.sourceConfigSha256) {
14638
+ if (options.repair) throw new RepairBaselineMismatchError(["source-config"]);
13848
14639
  throw new Error(
13849
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.`
13850
14641
  );
@@ -13955,6 +14746,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
13955
14746
  const upstreamHosts = parsedUpstreams.map((upstream) => upstream.host);
13956
14747
  const currentTarget = () => target.kind === "linked" ? target : { ...target, devpodId };
13957
14748
  const startAndProveAttachment = async (recreate = false) => {
14749
+ reportLifecycleProgress("provider");
13958
14750
  const requestedTarget = currentTarget();
13959
14751
  try {
13960
14752
  devpodId = await startDevpodWorkspace({
@@ -14010,6 +14802,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14010
14802
  if (stopped.length > 0) {
14011
14803
  repairMutationStarted = true;
14012
14804
  transitionPhase = "service-start";
14805
+ reportLifecycleProgress("service-start");
14013
14806
  claimLifecycleEffect();
14014
14807
  const result = (0, import_node_child_process27.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
14015
14808
  ...networkDockerOptions(),
@@ -14047,6 +14840,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14047
14840
  managedContainer = container;
14048
14841
  if (managedPlan) {
14049
14842
  transitionPhase = "service-start";
14843
+ reportLifecycleProgress("service-start");
14050
14844
  const initialContainers = inspectWorkspaceContainers();
14051
14845
  managedComposeProject = resolveComposeProject(
14052
14846
  initialContainers,
@@ -14182,6 +14976,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14182
14976
  if (options.repair && managedPlan)
14183
14977
  assertRetainedRepairContainers(repoPath, managedPlan, retainedRepairContainers);
14184
14978
  transitionPhase = "process-start";
14979
+ reportLifecycleProgress("process-start");
14185
14980
  claimLifecycleEffect();
14186
14981
  runManagedPostStart({
14187
14982
  plan: managedPostStart,
@@ -14206,6 +15001,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14206
15001
  }
14207
15002
  continue;
14208
15003
  }
15004
+ reportLifecycleProgress("process-stop");
14209
15005
  claimLifecycleEffect();
14210
15006
  runManagedProcessAction({
14211
15007
  container,
@@ -14226,6 +15022,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14226
15022
  }
14227
15023
  }
14228
15024
  transitionPhase = "service-stop";
15025
+ reportLifecycleProgress("service-stop");
14229
15026
  if (!options.repair)
14230
15027
  stopDroppedManagedServices(managedPlan, repoPath, managedComposeProject);
14231
15028
  assertDroppedManagedServicesStopped(managedPlan, repoPath, managedComposeProject);
@@ -14239,6 +15036,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14239
15036
  );
14240
15037
  }
14241
15038
  transitionPhase = "route-publication";
15039
+ reportLifecycleProgress("route-publication");
14242
15040
  claimLifecycleEffect();
14243
15041
  const publication = await replacePublishedProxyRoutes(
14244
15042
  repoPath,
@@ -14256,6 +15054,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14256
15054
  );
14257
15055
  let applicationReadiness;
14258
15056
  try {
15057
+ reportLifecycleProgress("readiness");
14259
15058
  applicationReadiness = await waitForHttpRoutes(
14260
15059
  repoPath,
14261
15060
  apps,
@@ -14483,6 +15282,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14483
15282
  const rollbackErrors = [];
14484
15283
  const failedPhase = transitionPhase;
14485
15284
  transitionPhase = "rollback";
15285
+ reportLifecycleProgress("rollback");
14486
15286
  const previousServices = previousManagedState?.desired.services ?? firstTransitionBaseline?.services ?? [];
14487
15287
  const previousProcesses = previousManagedState?.desired.processes ?? firstTransitionBaseline?.processes ?? [];
14488
15288
  const previousServiceSet = new Set(previousServices);
@@ -14714,6 +15514,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
14714
15514
  repair: Boolean(options.repair)
14715
15515
  });
14716
15516
  if (preparationConfig.managedRuntime?.devcontainer.prepareCommand) {
15517
+ reportLifecycleProgress("preparation");
14717
15518
  claimLifecycleEffect();
14718
15519
  await runManagedHostPreparation(repoPath, preparationConfig);
14719
15520
  claimLifecycleEffect();
@@ -15022,7 +15823,7 @@ function proveLifecycleStopped() {
15022
15823
  throw new Error("Full stop proof requires an explicit stop worker.");
15023
15824
  const settle = () => {
15024
15825
  claimLifecycleEffect();
15025
- if (initialStopAbsence && !(0, import_node_util6.isDeepStrictEqual)(
15826
+ if (initialStopAbsence && !(0, import_node_util7.isDeepStrictEqual)(
15026
15827
  proveInitialManagedDevsyAbsence(request.repoPath, initialStopAbsence.record.devpodId),
15027
15828
  initialStopAbsence
15028
15829
  ))
@@ -15071,7 +15872,7 @@ function proveLifecycleStopped() {
15071
15872
  return void 0;
15072
15873
  });
15073
15874
  if (settlement) {
15074
- const capacity = new CapacityStore(import_node_path40.default.join(DEVROUTER_HOME, "controller"));
15875
+ const capacity = createLifecycleCapacityStore(import_node_path40.default.join(DEVROUTER_HOME, "controller"));
15075
15876
  for (let attempt = 0; ; attempt++) {
15076
15877
  try {
15077
15878
  capacity.settleEnvironmentAfterStop(settlement, capacity.read().revision);
@@ -15232,7 +16033,7 @@ async function devpodExecOutcome(repoPath, command) {
15232
16033
  }
15233
16034
  assertRunningDevpod(devpod.id, repoPath);
15234
16035
  const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
15235
- const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto21.randomUUID)()}__:`;
16036
+ const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto22.randomUUID)()}__:`;
15236
16037
  const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
15237
16038
  const literalCommand = command.map(quotePosixArg).join(" ");
15238
16039
  const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
@@ -15548,6 +16349,7 @@ async function workspaceDeleteOwnedPath(worktreePath, opts = {}) {
15548
16349
 
15549
16350
  // src/core/environment-stop.ts
15550
16351
  async function environmentStop(repoPath, options = {}) {
16352
+ reportLifecycleProgress("stop");
15551
16353
  const linked = isLinkedWorktree(repoPath);
15552
16354
  const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
15553
16355
  if (linked && !workspace) {
@@ -15598,6 +16400,7 @@ async function environmentStop(repoPath, options = {}) {
15598
16400
  const removedRoutes = removeHostRoutesWhere(
15599
16401
  (route) => sameWorkspacePath(route.repoPath, repoPath)
15600
16402
  );
16403
+ reportLifecycleProgress("route-removal");
15601
16404
  await ensureTraefikRoutesRemoved(removedRoutes);
15602
16405
  return {
15603
16406
  kind: linked ? "linked" : "primary",
@@ -15623,6 +16426,10 @@ process.on("disconnect", cancel);
15623
16426
  process.once("message", async (message) => {
15624
16427
  started = true;
15625
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
+ });
15626
16433
  try {
15627
16434
  const request = message.request;
15628
16435
  const value = await executeLifecycleWorker(request, async () => {
@@ -15660,6 +16467,8 @@ process.once("message", async (message) => {
15660
16467
  } catch {
15661
16468
  }
15662
16469
  result = { ok: false, message: error instanceof Error ? error.message : String(error) };
16470
+ } finally {
16471
+ stopProgress?.();
15663
16472
  }
15664
16473
  if (process.connected && process.send) process.send(result, () => process.disconnect());
15665
16474
  });