@zq-silk/yui 0.15.12 → 0.16.0

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.
Files changed (236) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/ARCHITECTURE.zh-CN.md +3 -2
  3. package/dist/artifacts/artifactCommitLock.js +16 -55
  4. package/dist/artifacts/taskArtifactRepository.js +3 -3
  5. package/dist/cli/agentConfigurationPicker.js +1 -1
  6. package/dist/cli/commandCatalog.js +57 -41
  7. package/dist/cli/completion.js +18 -18
  8. package/dist/cli/completionWizard.js +1 -1
  9. package/dist/cli/dynamicCompletion.js +1 -1
  10. package/dist/cli/interactionPolicy.js +7 -3
  11. package/dist/cli/invocationAuthority.js +64 -0
  12. package/dist/cli/managedDiagnostics.js +1 -1
  13. package/dist/cli/parseRepeatable.js +35 -0
  14. package/dist/cli/updatePorts.js +26 -5
  15. package/dist/cli.js +1280 -1243
  16. package/dist/commands/agentCommands.js +4 -4
  17. package/dist/commands/capabilityCommands.js +1 -1
  18. package/dist/commands/configCommands.js +6 -57
  19. package/dist/commands/configOverview.js +2 -2
  20. package/dist/commands/durableJobCommands.js +12 -6
  21. package/dist/commands/globalRoleCommands.js +53 -29
  22. package/dist/commands/grantCommands.js +0 -3
  23. package/dist/commands/jobCommands.js +1 -4
  24. package/dist/commands/operatorCommands.js +3 -3
  25. package/dist/commands/projectCommands.js +1 -10
  26. package/dist/commands/resourcesCommands.js +10 -40
  27. package/dist/commands/roleConfiguration.js +2 -6
  28. package/dist/commands/taskActivationCommands.js +4 -8
  29. package/dist/commands/taskAgentCapabilities.js +9 -0
  30. package/dist/commands/taskCommandSupport.js +155 -0
  31. package/dist/commands/taskCommandTypes.js +1 -0
  32. package/dist/commands/taskCommands.js +271 -710
  33. package/dist/commands/taskCompletionGate.js +1 -1
  34. package/dist/commands/taskExecutionCommands.js +1 -1
  35. package/dist/commands/taskFactCommands.js +325 -0
  36. package/dist/commands/taskInputCommands.js +12 -42
  37. package/dist/commands/taskIntegrationCommands.js +17 -57
  38. package/dist/commands/taskNextActionCommand.js +7 -6
  39. package/dist/commands/taskPublicationAdoptCommand.js +3 -3
  40. package/dist/commands/taskPublicationCommands.js +1 -1
  41. package/dist/commands/taskPublicationVerifyCommand.js +1 -1
  42. package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
  43. package/dist/commands/taskUpstreamCommands.js +19 -8
  44. package/dist/commands/workflowCommands.js +6 -1
  45. package/dist/completion/completionInstaller.js +26 -26
  46. package/dist/completion/completionState.js +26 -26
  47. package/dist/completion/fileCompletionManager.js +6 -11
  48. package/dist/config/configCatalog.js +0 -2
  49. package/dist/config/timeZone.js +14 -0
  50. package/dist/config/yuiConfig.js +0 -29
  51. package/dist/context/runContextPack.js +2 -3
  52. package/dist/context/taskCatalog.js +3 -5
  53. package/dist/context/taskContext.js +139 -151
  54. package/dist/context/wakeRunReferences.js +11 -0
  55. package/dist/controller/agentCapabilities.js +58 -0
  56. package/dist/controller/agentRuntimeObserver.js +6 -11
  57. package/dist/controller/clientRuntime.js +22 -14
  58. package/dist/controller/controller.js +9 -22
  59. package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
  60. package/dist/controller/jobClient.js +4 -4
  61. package/dist/controller/jobControl.js +60 -25
  62. package/dist/controller/jobSupervisor.js +4 -4
  63. package/dist/controller/providerRetryDelivery.js +7 -7
  64. package/dist/controller/runtime.js +51 -44
  65. package/dist/controller/runtimeEventProcessor.js +0 -5
  66. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  67. package/dist/controller/taskAgentError.js +35 -0
  68. package/dist/coordination/workMailboxQueue.js +2 -24
  69. package/dist/core/controllerClient.js +2 -2
  70. package/dist/core/fileLockOwner.js +74 -0
  71. package/dist/decision/decision.js +17 -0
  72. package/dist/doctor/doctor.js +18 -9
  73. package/dist/event/taskEvent.js +12 -0
  74. package/dist/execution/workItemExecutionProjection.js +2 -2
  75. package/dist/executor/agentCapabilityConfig.js +43 -0
  76. package/dist/executor/agentConfigurationCatalog.js +49 -11
  77. package/dist/executor/agentExecutor.js +5 -8
  78. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  79. package/dist/executor/taskAgentCapabilities.js +64 -0
  80. package/dist/integration/deliveryObligation.js +1 -69
  81. package/dist/integration/gitIntegrationService.js +60 -84
  82. package/dist/integration/integrationAttempt.js +3 -14
  83. package/dist/integration/integrationSourceApplication.js +4 -12
  84. package/dist/integration/manifestTags.js +2 -2
  85. package/dist/job/durableJob.js +4 -25
  86. package/dist/job/jobAssignmentScope.js +22 -0
  87. package/dist/job/jobOperation.js +16 -0
  88. package/dist/job/jobRunner.js +2 -1
  89. package/dist/job/stepDirectory.js +15 -0
  90. package/dist/kernel/builtinCapabilities.js +15 -11
  91. package/dist/kernel/kernelPorts.js +1 -17
  92. package/dist/lifecycle/exactRunTerminalization.js +1 -9
  93. package/dist/message/inputControlResolution.js +3 -4
  94. package/dist/message/message.js +17 -20
  95. package/dist/message/messageContinuation.js +1 -1
  96. package/dist/milestone/milestone.js +11 -0
  97. package/dist/observability/orchestrationMetrics.js +24 -1
  98. package/dist/observability/runtimeIdentity.js +1 -17
  99. package/dist/output/agentConfigurationPresentation.js +2 -0
  100. package/dist/output/timePresentation.js +1 -14
  101. package/dist/plugins/pluginService.js +12 -4
  102. package/dist/release/releaseWorkflowPorts.js +45 -96
  103. package/dist/release/runtimeRelease.js +9 -15
  104. package/dist/repository/projectMaintenanceLock.js +23 -64
  105. package/dist/repository/taskBaseFreshness.js +1 -1
  106. package/dist/repository/taskWorkspaceCoordinator.js +13 -22
  107. package/dist/repository/taskWorkspacePreparer.js +29 -25
  108. package/dist/repository/workspaceCleanupInspection.js +5 -5
  109. package/dist/resources/autoResourceGc.js +4 -77
  110. package/dist/resources/liveReferences.js +7 -54
  111. package/dist/resources/resourceDiscovery.js +5 -118
  112. package/dist/resources/resourceGc.js +233 -181
  113. package/dist/resources/resourceRegistrar.js +5 -4
  114. package/dist/resources/resourceRegistry.js +3 -37
  115. package/dist/resources/resourceRegistryStore.js +13 -36
  116. package/dist/resources/sqliteResourceRegistry.js +43 -26
  117. package/dist/review/deltaRecheck.js +1 -1
  118. package/dist/review/reviewAcceptance.js +1 -1
  119. package/dist/review/reviewDecision.js +1 -1
  120. package/dist/review/reviewRound.js +8 -7
  121. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  122. package/dist/runtime/acpProtocol.js +4 -51
  123. package/dist/runtime/acpSession.js +10 -75
  124. package/dist/runtime/acpSessionConfiguration.js +1 -8
  125. package/dist/runtime/agentEndpoint.js +3 -2
  126. package/dist/runtime/agentFailureContext.js +43 -0
  127. package/dist/runtime/agentHost.js +15 -15
  128. package/dist/runtime/codexInteractiveHost.js +2 -2
  129. package/dist/runtime/index.js +0 -1
  130. package/dist/runtime/managedCaller.js +21 -1
  131. package/dist/runtime/providerErrors.js +38 -0
  132. package/dist/runtime/runtimeObservation.js +0 -5
  133. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  134. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  135. package/dist/runtime/structuredProviderHost.js +4 -41
  136. package/dist/runtime/tmuxAdapters.js +6 -8
  137. package/dist/scheduler/activeRoleRunDelivery.js +7 -7
  138. package/dist/scheduler/leaderWakeupProcessor.js +19 -6
  139. package/dist/scheduler/operatorEvent.js +12 -20
  140. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  141. package/dist/scheduler/ports.js +5 -8
  142. package/dist/scheduler/roleRunLiveness.js +4 -4
  143. package/dist/scheduler/roleRunStall.js +16 -23
  144. package/dist/scheduler/taskExecutionProjection.js +25 -33
  145. package/dist/scheduler/taskObservabilityProjection.js +0 -1
  146. package/dist/scheduler/taskWake.js +5 -10
  147. package/dist/scheduler/wakeReason.js +2 -0
  148. package/dist/scheduler/wakeupQueue.js +2 -4
  149. package/dist/setup/setupCommand.js +1 -1
  150. package/dist/storage/contextRecords.js +106 -0
  151. package/dist/storage/currentTaskStore.js +1 -1
  152. package/dist/storage/homeLayout.js +13 -17
  153. package/dist/storage/migrations/agentFailureContext.js +22 -0
  154. package/dist/storage/migrations/currentInputContract.js +86 -0
  155. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  156. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  157. package/dist/storage/migrations/integrationContinuation.js +5 -4
  158. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  159. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  160. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  161. package/dist/storage/migrations/verificationPolicy.js +74 -0
  162. package/dist/storage/migrations/workItemHistory.js +46 -0
  163. package/dist/storage/persistenceWorker.js +12 -4
  164. package/dist/storage/recordValidation.js +87 -0
  165. package/dist/storage/sqliteSchema.js +142 -1
  166. package/dist/storage/sqliteStore.js +156 -162
  167. package/dist/storage/storageSchema.js +2 -14
  168. package/dist/storage/storageVersions.js +1 -1
  169. package/dist/storage/storeRpc.js +15 -7
  170. package/dist/storage/taskStore.js +3 -7
  171. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  172. package/dist/task/completionReadiness.js +5 -24
  173. package/dist/task/draftPlan.js +0 -53
  174. package/dist/task/nextAction.js +7 -13
  175. package/dist/task/remoteDeliveryService.js +89 -0
  176. package/dist/task/taskActivation.js +2 -25
  177. package/dist/task/taskActivationService.js +0 -2
  178. package/dist/task/taskRecordReference.js +0 -1
  179. package/dist/task/taskSubmission.js +3 -14
  180. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  181. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  182. package/dist/telemetry/telemetryWiring.js +4 -3
  183. package/dist/tmux/tmuxManager.js +29 -20
  184. package/dist/verification/gateArtifact.js +15 -24
  185. package/dist/verification/gateArtifactStore.js +14 -7
  186. package/dist/verification/verificationGateService.js +29 -88
  187. package/dist/verification/verificationPlan.js +27 -72
  188. package/dist/web/assets/client/app.js +1 -1
  189. package/dist/web/assets/client/taskSurface.js +4 -4
  190. package/dist/web/webServer.js +4 -6
  191. package/dist/web/webSnapshot.js +3 -83
  192. package/dist/web/webTaskSurface.js +17 -46
  193. package/dist/workItem/workItem.js +1 -12
  194. package/docs/architecture/capabilities-and-resources.md +14 -2
  195. package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
  196. package/docs/managed-turn-and-session-runtime.md +83 -10
  197. package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
  198. package/docs/plugin-sdk.md +4 -2
  199. package/docs/provider-runtime.md +3 -1
  200. package/docs/provider-runtime.zh-CN.md +4 -2
  201. package/docs/release-workflow.md +190 -7
  202. package/docs/release-workflow.zh-CN.md +139 -5
  203. package/docs/roles-and-configuration.md +29 -0
  204. package/docs/roles-and-configuration.zh-CN.md +21 -0
  205. package/docs/sqlite-control-plane-design.md +16 -3
  206. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  207. package/docs/task-dag-semantics.md +3 -3
  208. package/docs/task-dag-semantics.zh-CN.md +2 -2
  209. package/docs/task-delivery.md +91 -5
  210. package/docs/task-delivery.zh-CN.md +72 -4
  211. package/docs/task-discovery.md +10 -11
  212. package/docs/task-discovery.zh-CN.md +8 -9
  213. package/docs/testing/verification-levels.md +145 -9
  214. package/docs/testing/verification-levels.zh-CN.md +103 -8
  215. package/package.json +1 -1
  216. package/skills/yui-leader/SKILL.md +5 -0
  217. package/skills/yui-leader/references/integration.md +9 -5
  218. package/skills/yui-leader/references/planning.md +9 -3
  219. package/skills/yui-operator/SKILL.md +15 -10
  220. package/skills/yui-runtime/SKILL.md +6 -6
  221. package/skills/yui-runtime/references/recovery.md +47 -0
  222. package/dist/agent/agentRegistry.js +0 -10
  223. package/dist/agentRun/runIdentity.js +0 -22
  224. package/dist/commands/deliveryGuardPreflight.js +0 -30
  225. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  226. package/dist/commands/taskOverviewCommand.js +0 -377
  227. package/dist/completion/completionWizard.js +0 -125
  228. package/dist/context/dispatchContext.js +0 -110
  229. package/dist/context/wakeNotification.js +0 -144
  230. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  231. package/dist/integration/integrationQueueEntry.js +0 -191
  232. package/dist/integration/integrationQueueService.js +0 -810
  233. package/dist/release/fakeReleasePorts.js +0 -55
  234. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  235. package/dist/task/deliveryGuard.js +0 -226
  236. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -1,7 +1,8 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { performance } from "node:perf_hooks";
4
4
  import { setTimeout as delay } from "node:timers/promises";
5
+ import { currentFileLockOwner, fileLockOwnerIsLive } from "../core/fileLockOwner.js";
5
6
  /**
6
7
  * Per-Project maintenance fence.
7
8
  *
@@ -87,6 +88,7 @@ export function acquireProjectMaintenanceLock(home, projectId, options = {}) {
87
88
  async function acquireWithBudget(home, projectId, budget, firstProject = true) {
88
89
  const lock = projectMaintenanceLockPath(home, projectId);
89
90
  assertNotCancelled(projectId, budget.signal);
91
+ const ownerIdentity = currentFileLockOwner();
90
92
  mkdirSync(join(home, "locks", "projects"), { recursive: true, mode: 0o700 });
91
93
  let firstAttempt = firstProject;
92
94
  while (true) {
@@ -99,7 +101,7 @@ async function acquireWithBudget(home, projectId, budget, firstProject = true) {
99
101
  firstAttempt = false;
100
102
  try {
101
103
  mkdirSync(lock, { mode: 0o700 });
102
- const ownerIdentity = writeOwnerIdentity(lock);
104
+ writeOwnerIdentity(lock, ownerIdentity);
103
105
  let released = false;
104
106
  return () => {
105
107
  if (released)
@@ -134,8 +136,7 @@ async function acquireWithBudget(home, projectId, budget, firstProject = true) {
134
136
  }
135
137
  }
136
138
  /** Write this process's owner identity and return the exact bytes recorded. */
137
- function writeOwnerIdentity(lock) {
138
- const identity = `${process.pid}:${processStartIdentity() ?? ""}`;
139
+ function writeOwnerIdentity(lock, identity = currentFileLockOwner()) {
139
140
  writeFileSync(join(lock, "owner"), `${identity}\n`, { mode: 0o600 });
140
141
  return identity;
141
142
  }
@@ -188,14 +189,17 @@ export async function acquireProjectMaintenanceLocks(home, projectIds, options =
188
189
  }
189
190
  /**
190
191
  * Non-blocking fence check for the Controller: a Project is fenced only
191
- * while a live process holds its lock. A stale (dead-owner) lock reads as
192
- * unfenced, so a crashed maintenance process never wedges preparation.
192
+ * while a live or unverified process holds its lock. Only proven stale owners
193
+ * read as unfenced; incomplete evidence never authorizes preparation.
193
194
  */
194
195
  export function isProjectMaintenanceFenced(home, projectId) {
195
196
  const lock = projectMaintenanceLockPath(home, projectId);
196
- if (!existsSync(lock))
197
- return false;
198
- return lockOwnerIsAlive(lock);
197
+ try {
198
+ return fileLockOwnerIsLive(lock);
199
+ }
200
+ catch {
201
+ return true;
202
+ }
199
203
  }
200
204
  /**
201
205
  * Reclaim a stale (dead-owner) lock through a crash-safe compare-and-delete
@@ -213,9 +217,8 @@ function reclaimStaleProjectMaintenanceLock(lock) {
213
217
  catch (error) {
214
218
  if (!isEnoent(error))
215
219
  throw error;
216
- // Ownerless lock: a crash between mkdir and owner publication. Reclaim
217
- // only when the directory is old enough that the creator is not still
218
- // initializing; a fresh lock may have its owner file written imminently.
220
+ // Allow publication its initial grace period; an older missing owner is
221
+ // diagnosed as unverified below, never treated as a dead creator.
219
222
  try {
220
223
  if (Date.now() - statSync(lock).mtimeMs < STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS)
221
224
  return;
@@ -236,7 +239,7 @@ function reclaimStaleProjectMaintenanceLock(lock) {
236
239
  return;
237
240
  throw error;
238
241
  }
239
- if (expectedOwner !== null && lockOwnerIsAlive(lock))
242
+ if (fileLockOwnerIsLive(lock))
240
243
  return;
241
244
  const reclaimLock = `${lock}.reclaim`;
242
245
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -285,7 +288,7 @@ function reclaimStaleProjectMaintenanceLock(lock) {
285
288
  }
286
289
  if (currentOwner !== expectedOwner)
287
290
  return;
288
- if (currentOwner !== null && lockOwnerIsAlive(lock))
291
+ if (fileLockOwnerIsLive(lock))
289
292
  return;
290
293
  rmSync(lock, { recursive: true, force: true });
291
294
  }
@@ -297,70 +300,26 @@ function reclaimStaleProjectMaintenanceLock(lock) {
297
300
  }
298
301
  /**
299
302
  * Reclaim a reclaim-lock directory only when it is provably orphaned: its
300
- * owner is dead (or unrecorded) AND it is older than the age bound, so a
303
+ * owner is provably dead AND it is older than the age bound, so a
301
304
  * lock whose owner just mkdir'ed but has not written its owner is not stolen.
302
305
  * Uses the same PID + start-time identity check as the main fence, so a
303
306
  * recycled PID with a different start identity does not keep an orphaned
304
- * reclaim lock alive. An ownerless (ENOENT owner) lock is still reclaimed;
305
- * an unreadable owner fails closed (treated as live).
307
+ * reclaim lock alive. An ownerless or unreadable lock remains unverified.
306
308
  * Returns true when it removed the lock (caller retries), false otherwise.
307
309
  */
308
310
  function reclaimOrphanedReclaimLock(reclaimLock) {
309
311
  try {
310
312
  if (Date.now() - statSync(reclaimLock).mtimeMs < STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS)
311
313
  return false;
312
- if (lockOwnerIsAlive(reclaimLock))
314
+ if (fileLockOwnerIsLive(reclaimLock))
313
315
  return false;
314
316
  rmSync(reclaimLock, { recursive: true, force: true });
315
317
  return true;
316
318
  }
317
319
  catch (error) {
318
- return isEnoent(error);
319
- }
320
- }
321
- function lockOwnerIsAlive(lock) {
322
- let owner;
323
- try {
324
- owner = readFileSync(join(lock, "owner"), "utf8").trim();
325
- }
326
- catch (error) {
327
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
328
- return false;
329
- // An unreadable owner file fails closed: treat the fence as held.
330
- return true;
331
- }
332
- const separator = owner.indexOf(":");
333
- const pid = Number.parseInt(separator < 0 ? owner : owner.slice(0, separator), 10);
334
- if (!Number.isInteger(pid) || pid <= 0)
335
- return false;
336
- const recordedIdentity = separator < 0 ? "" : owner.slice(separator + 1);
337
- // PID + start-time identity: a recycled PID can never match the holder.
338
- const currentIdentity = processStartIdentity(pid);
339
- if (currentIdentity !== undefined) {
340
- return recordedIdentity !== "" && currentIdentity === recordedIdentity;
341
- }
342
- // Off /proc (non-Linux): fall back to liveness.
343
- return processIsAlive(pid);
344
- }
345
- /** Linux process start time (clock ticks since boot) for a PID; undefined off /proc. */
346
- function processStartIdentity(pid = process.pid) {
347
- try {
348
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
349
- const close = stat.lastIndexOf(")");
350
- const fields = stat.slice(close + 2).split(" ");
351
- return fields[19] ?? "0"; // field 22: starttime
352
- }
353
- catch {
354
- return undefined;
355
- }
356
- }
357
- function processIsAlive(pid) {
358
- try {
359
- process.kill(pid, 0);
360
- return true;
361
- }
362
- catch (error) {
363
- return error instanceof Error && "code" in error && error.code === "EPERM";
320
+ if (isEnoent(error))
321
+ return true;
322
+ throw error;
364
323
  }
365
324
  }
366
325
  function isEnoent(error) {
@@ -95,7 +95,7 @@ export async function inspectTaskBaseFreshness(taskId, store, options = {}) {
95
95
  baseRef: binding.baseRef,
96
96
  baseCommit,
97
97
  status,
98
- source: tracked?.source ?? (project.remoteUrl === undefined ? "not-applicable" : "compatibility-projection"),
98
+ source: tracked?.source ?? (project.remoteUrl === undefined ? "not-applicable" : "unknown"),
99
99
  workspacePath,
100
100
  workspaceClean,
101
101
  ...(physicalHead === undefined ? {} : { physicalHead }),
@@ -1,10 +1,10 @@
1
- import { isDeepStrictEqual } from "node:util";
2
1
  import { lstat } from "node:fs/promises";
2
+ import { isDeepStrictEqual } from "node:util";
3
3
  import { isArchivePersistenceFailure, recordArchiveCleanup } from "../task/archiveDiagnostics.js";
4
- import { projectTaskRemoteDeliveryFromStore } from "../commands/taskRemoteDeliveryCommand.js";
5
- import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
6
- import { archiveSettlementChecks, archiveExecutionChecks } from "../task/archivePreflight.js";
4
+ import { archiveExecutionChecks, archiveSettlementChecks } from "../task/archivePreflight.js";
5
+ import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
7
6
  import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
7
+ import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
8
8
  import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
9
9
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
10
10
  import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
@@ -235,7 +235,7 @@ export class TaskWorkspaceCoordinator {
235
235
  throw new CleanupInspectionError(checks);
236
236
  const roleNames = this.store.listRoles(taskId).map(({ name }) => name);
237
237
  await this.#stopLiveRoles(taskId, roleNames);
238
- await this.runtime.assertTaskPhysicalResourcesReleased?.(task.id);
238
+ await this.runtime.releaseTaskTerminals(task.id);
239
239
  this.#assertTaskArchiveSnapshot(snapshot);
240
240
  for (const workspace of laneWorkspaces) {
241
241
  this.#assertTaskArchiveLifecycle(task);
@@ -336,10 +336,7 @@ export class TaskWorkspaceCoordinator {
336
336
  throw new CleanupInspectionError(checks);
337
337
  if (!runtimeReleased)
338
338
  throw new Error("One or more Role runtimes could not be safely released.");
339
- if (this.runtime.assertTaskPhysicalResourcesReleased === undefined) {
340
- throw new Error("Physical resource release inspection is unavailable; workspace references retained.");
341
- }
342
- await this.runtime.assertTaskPhysicalResourcesReleased(taskId);
339
+ await this.runtime.releaseTaskTerminals(taskId);
343
340
  return "released";
344
341
  });
345
342
  if (physicalReleased) {
@@ -447,9 +444,7 @@ export class TaskWorkspaceCoordinator {
447
444
  * human/Leader has taken responsibility for the outcome.
448
445
  */
449
446
  #assertNoActiveWorkItemDurableJobs(item) {
450
- const jobs = this.store.listDurableJobs?.(item.taskId);
451
- if (jobs === undefined)
452
- return;
447
+ const jobs = this.store.listDurableJobs(item.taskId);
453
448
  const blocking = jobs.filter((job) => (job.owner.kind === "work-item"
454
449
  && job.owner.workItemId === item.id
455
450
  && (job.status === "queued"
@@ -470,18 +465,16 @@ export class TaskWorkspaceCoordinator {
470
465
  #reviewRoundRoleNames(round) {
471
466
  return [
472
467
  round.reviewerRoleName,
473
- ...(round.executionGroup?.lanes.map(({ roleName }) => roleName) ?? []),
474
468
  ...(round.executionGroup?.lanes.map(({ roleName }) => roleName) ?? [])
475
469
  ];
476
470
  }
477
471
  async #stopLiveRoles(taskId, roleNames) {
478
472
  const targets = [...new Set(roleNames)];
479
- const getActiveRun = this.store.getActiveRun?.bind(this.store);
480
473
  for (const roleName of targets) {
481
- if (getActiveRun !== undefined && getActiveRun(taskId, roleName) !== null) {
474
+ if (this.store.getActiveRun(taskId, roleName) !== null) {
482
475
  throw new Error(`Role has an active AgentRun: ${taskId}/${roleName}.`);
483
476
  }
484
- if (this.store.getWorkMailbox !== undefined && hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
477
+ if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
485
478
  throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
486
479
  }
487
480
  }
@@ -498,11 +491,9 @@ export class TaskWorkspaceCoordinator {
498
491
  });
499
492
  if (live.length > 0)
500
493
  await this.runtime.stopTaskRoleSessions(taskId, live);
501
- // The aggregate-16 dormant Claude placeholder is the sole exception to
502
- // strict workspace-session retirement. The synchronous pane inspection is
503
- // performed while holding the Task store transaction so a normal launch
504
- // cannot reserve a AgentRun/Session between absence proof and terminalization.
505
- if (inspect === undefined || this.store.transaction === undefined)
494
+ // Recheck exact runtime quiescence under the Task transaction when this
495
+ // runtime exposes physical panes. Store reads are always authoritative.
496
+ if (inspect === undefined)
506
497
  return;
507
498
  this.store.transaction((tx) => {
508
499
  const panes = inspect(taskId);
@@ -513,7 +504,7 @@ export class TaskWorkspaceCoordinator {
513
504
  if (tx.getActiveRun(taskId, roleName) !== null) {
514
505
  throw new Error(`Role has an active AgentRun: ${taskId}/${roleName}.`);
515
506
  }
516
- if (tx.getWorkMailbox !== undefined && hasRuntimeLifecycleWork(tx.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
507
+ if (hasRuntimeLifecycleWork(tx.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName })))) {
517
508
  throw new Error(`Role has unsettled runtime lifecycle state: ${taskId}/${roleName}.`);
518
509
  }
519
510
  }
@@ -1,31 +1,31 @@
1
- import { lstat, mkdir, readlink, readdir, rm, rmdir, symlink, unlink } from "node:fs/promises";
1
+ import { lstat, mkdir, readdir, readlink, rm, rmdir, symlink, unlink } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { isDeepStrictEqual } from "node:util";
4
4
  import { recordArchiveCleanup } from "../task/archiveDiagnostics.js";
5
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
6
+ import { createTaskEvent } from "../event/taskEvent.js";
5
7
  import { retireTaskRoleSessionsForWorkspace, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
6
8
  import { formatWorkspacePreflightError } from "../executor/workspacePreflightClassification.js";
9
+ import { createProjectResources } from "../resources/projectResourceService.js";
10
+ import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
11
+ import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
7
12
  import { updateRole } from "../role/role.js";
13
+ import { hasRuntimeCleanupObligation, RUNTIME_HOST_DETACH_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
8
14
  import { taskRoleRuntimeIdentity } from "../runtime/managedCaller.js";
9
- import { hasRuntimeCleanupObligation, runtimeLifecycleTarget, RUNTIME_HOST_DETACH_REQUIRED_REASON } from "../runtime/lifecycleReservation.js";
10
- import { taskLocalActor } from "../commands/taskActor.js";
11
- import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
15
+ import { managedTaskRoot } from "../storage/homeLayout.js";
12
16
  import { StorageConflictError } from "../storage/taskStore.js";
13
- import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits, taskOwnsManagedWorkspace } from "../task/task.js";
14
- import { admitStoredTaskActivation, adoptTaskActivationResources, recordFailedTaskActivation, recordAdoptedTaskActivation } from "../task/taskActivationService.js";
15
17
  import { validateDraftTaskForActivation } from "../task/draftPlan.js";
16
- import { createProjectResources } from "../resources/projectResourceService.js";
17
- import { createTaskEvent } from "../event/taskEvent.js";
18
- import { enqueueWork } from "../coordination/workMailboxQueue.js";
19
- import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
18
+ import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits, taskOwnsManagedWorkspace } from "../task/task.js";
19
+ import { admitStoredTaskActivation, adoptTaskActivationResources, recordAdoptedTaskActivation, recordFailedTaskActivation } from "../task/taskActivationService.js";
20
+ import { taskLocalActor } from "../task/taskAuthority.js";
21
+ import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, recordWorkItemWorkspaceDisposition, workItemExecutionGroupById } from "../workItem/workItem.js";
22
+ import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
20
23
  import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
21
24
  import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
22
- import { acquireProjectMaintenanceLocks, ProjectMaintenanceLockedError, ProjectMaintenanceLockCancelledError } from "./projectMaintenanceLock.js";
23
- import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
24
- import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
25
+ import { acquireProjectMaintenanceLocks, ProjectMaintenanceLockCancelledError, ProjectMaintenanceLockedError } from "./projectMaintenanceLock.js";
25
26
  import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
26
- import { managedTaskRoot } from "../storage/homeLayout.js";
27
+ import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
27
28
  import { inspectWorkspaceCleanup } from "./workspaceCleanupInspection.js";
28
- import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
29
29
  const MAIN_WORKTREE = "main";
30
30
  const LEADER_ROLE = "leader";
31
31
  /**
@@ -154,12 +154,16 @@ export class FileTaskWorkspacePreparer {
154
154
  changed: false
155
155
  };
156
156
  }
157
+ const admission = admitStoredTaskActivation(this.store, task.id);
158
+ if (admission.disposition === "absent") {
159
+ throw new Error(`Task activation request is required: ${task.id}. `
160
+ + `Use yui task activation request ${task.id} --request-id <id> --environment <plan> first.`);
161
+ }
157
162
  const provider = this.store.getTaskRoleSessionSet(task.id, LEADER_ROLE)?.providerBinding;
158
163
  if (provider?.run != null
159
164
  && ["submitting", "accepted", "delivery-unknown"].includes(provider.run.status)) {
160
165
  throw new Error("Planning native input is unsettled; preserve activation intent and retry after its exact terminal.");
161
166
  }
162
- const admission = admitStoredTaskActivation(this.store, task.id);
163
167
  if (admission.disposition === "ready")
164
168
  attemptedRequest = admission.request;
165
169
  validateDraftTaskForActivation(this.store, task);
@@ -218,9 +222,8 @@ export class FileTaskWorkspacePreparer {
218
222
  * Prepares and adopts the resource configuration an explicit activation
219
223
  * request named, before any status change.
220
224
  *
221
- * A Task with no request keeps the historical behavior: its managed workspace
222
- * is the whole environment. A request whose plan is empty and whose Task
223
- * binds no Project owns nothing physical, and is reported as workspace-free
225
+ * A request whose plan is empty and whose Task binds no Project owns nothing
226
+ * physical, and is reported as workspace-free
224
227
  * so activation creates no directory at all.
225
228
  *
226
229
  * The exact request that was adopted travels with the result, so the later
@@ -229,8 +232,6 @@ export class FileTaskWorkspacePreparer {
229
232
  */
230
233
  async #adoptActivationEnvironment(task) {
231
234
  const admission = admitStoredTaskActivation(this.store, task.id);
232
- if (admission.disposition === "absent")
233
- return { workspaceFree: false };
234
235
  if (admission.disposition !== "ready") {
235
236
  throw new Error(`Task activation is not adoptable: ${task.id}/${admission.disposition}`
236
237
  + (admission.disposition === "unavailable" ? ` (${admission.reason})` : "")
@@ -1467,7 +1468,7 @@ export class FileTaskWorkspacePreparer {
1467
1468
  if (round.status !== "pending") {
1468
1469
  throw new Error(`ReviewRound workspace can only prepare while pending: ${round.id}.`);
1469
1470
  }
1470
- const taskScope = (round.scope ?? "work-item") === "task";
1471
+ const taskScope = round.scope === "task";
1471
1472
  const item = taskScope || round.workItemId === undefined
1472
1473
  ? undefined
1473
1474
  : requireWorkItem(this.store, task.id, round.workItemId);
@@ -1770,7 +1771,7 @@ export class FileTaskWorkspacePreparer {
1770
1771
  async #reassignTaskReviewWorkspace(task, round, reviewer, reviewRoot, frozenEntries) {
1771
1772
  const previous = this.store.listReviewRounds(task.id)
1772
1773
  .filter((candidate) => (candidate.id !== round.id
1773
- && (candidate.scope ?? "work-item") === "task"
1774
+ && candidate.scope === "task"
1774
1775
  && candidate.reviewerRoleName === round.reviewerRoleName
1775
1776
  && (candidate.status === "completed" || candidate.status === "failed")
1776
1777
  && candidate.workspaceDisposition?.kind !== "removed"
@@ -2128,6 +2129,8 @@ export class FileTaskWorkspacePreparer {
2128
2129
  if (main.owner.type !== "task") {
2129
2130
  throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
2130
2131
  }
2132
+ if (main.entries.length === 0)
2133
+ return "missing";
2131
2134
  return this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries);
2132
2135
  }
2133
2136
  async cleanupTaskForArchive(taskId, disposition = "abandoned") {
@@ -2147,7 +2150,8 @@ export class FileTaskWorkspacePreparer {
2147
2150
  if (main !== null) {
2148
2151
  await this.#assertWorkspaceCleanup(main, disposition);
2149
2152
  assertTaskArchiveState(requireTask(this.store, task.id), task);
2150
- if (await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
2153
+ // Scratch-only workspaces have no Git entries or Git workspace identity.
2154
+ if (main.entries.length > 0 && await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
2151
2155
  return {
2152
2156
  taskId,
2153
2157
  status: "retained-dirty",
@@ -2228,7 +2232,7 @@ export class FileTaskWorkspacePreparer {
2228
2232
  return integrationWorkspaceRoot(this.home, taskId, integrationId);
2229
2233
  }
2230
2234
  #reviewWorktreeName(round) {
2231
- return (round.scope ?? "work-item") === "task"
2235
+ return round.scope === "task"
2232
2236
  ? `reviewer-${round.reviewerRoleName}`
2233
2237
  : round.id;
2234
2238
  }
@@ -1,11 +1,11 @@
1
- import { dirname, join, relative, isAbsolute } from "node:path";
1
+ import { dirname, isAbsolute, join, relative } from "node:path";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { managedWorkspaceKey, managedWorktreeName } from "../worktree/managedWorkspace.js";
4
- import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
3
+ import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
5
4
  import { cleanupCheckFromError } from "../workspace/cleanupInspection.js";
6
- import { projectTaskRemoteDeliveryFromStore } from "../commands/taskRemoteDeliveryCommand.js";
7
- import { taskWorkspaceRefSegment } from "./taskWorkspaceIdentity.js";
5
+ import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
6
+ import { managedWorkspaceKey, managedWorktreeName } from "../worktree/managedWorkspace.js";
8
7
  import { integrationWorktreeIdentity, worktreeIdentity } from "./gitWorkspace.js";
8
+ import { taskWorkspaceRefSegment } from "./taskWorkspaceIdentity.js";
9
9
  /** Current owner facts + the same Git inspection used immediately before removal.
10
10
  * No preparation, locks, Git refresh, runtime stop, DB writes or repair.
11
11
  */
@@ -10,7 +10,7 @@
10
10
  * so every safety guarantee (live-ref re-scan, cleanliness proof, fail-closed
11
11
  * sources) applies identically.
12
12
  */
13
- import { applyResourceGc, planResourceGc } from "./resourceGc.js";
13
+ import { applyResourceGc, planResourceGc, readResourceGcState, } from "./resourceGc.js";
14
14
  import { resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours } from "../config/yuiConfig.js";
15
15
  /**
16
16
  * Create the Controller's automatic Resource GC hook. The hook self-skips
@@ -25,22 +25,16 @@ export function createResourceAutoGc(options) {
25
25
  return { skipped: true, applied: 0, failed: 0, restored: 0 };
26
26
  }
27
27
  const now = new Date();
28
- const projects = store.listProjects();
29
- const managedWorkspaces = collectManagedWorkspaces(store);
30
- const taskStatusById = collectTaskStatuses(store);
31
28
  const input = {
32
29
  home,
33
- projects,
34
- managedWorkspaces,
35
- taskStatusById,
30
+ ...readResourceGcState(store),
36
31
  mode: "quarantine",
37
32
  now,
38
33
  quarantineTtlHours: resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours),
39
- environment,
40
- activeWorkspaceOwnerPaths: collectActiveWorkspaceOwnerPaths(store)
34
+ environment
41
35
  };
42
36
  const plan = await planResourceGc(input);
43
- const result = await applyResourceGc(input, plan);
37
+ const result = await applyResourceGc(input, plan, store);
44
38
  return {
45
39
  skipped: false,
46
40
  applied: result.applied.length,
@@ -49,70 +43,3 @@ export function createResourceAutoGc(options) {
49
43
  };
50
44
  };
51
45
  }
52
- /**
53
- * Run one automatic GC pass. Returns `ran: false` when auto-GC is disabled or
54
- * the GC mode is not `quarantine`. The pass never purges: permanent deletion
55
- * is always manual.
56
- */
57
- export async function runAutoResourceGc(store, options = {}) {
58
- const config = store.getConfig();
59
- const autoQuarantine = resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine);
60
- if (!autoQuarantine) {
61
- return { ran: false, reason: "auto-quarantine is disabled" };
62
- }
63
- const gcMode = resolveResourcesGcMode(config.resourcesGcMode);
64
- if (gcMode !== "quarantine") {
65
- return { ran: false, reason: "resources.gcMode is not quarantine" };
66
- }
67
- const now = options.now ?? new Date();
68
- const home = store.rootDirectory();
69
- const projects = store.listProjects();
70
- const managedWorkspaces = collectManagedWorkspaces(store);
71
- const taskStatusById = collectTaskStatuses(store);
72
- const input = {
73
- home,
74
- projects,
75
- managedWorkspaces,
76
- taskStatusById,
77
- mode: "quarantine",
78
- now,
79
- quarantineTtlHours: resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours),
80
- activeWorkspaceOwnerPaths: collectActiveWorkspaceOwnerPaths(store)
81
- };
82
- const plan = await planResourceGc(input);
83
- const result = await applyResourceGc(input, plan);
84
- return { ran: true, result };
85
- }
86
- function collectManagedWorkspaces(store) {
87
- const workspaces = [];
88
- for (const task of store.listTasks()) {
89
- workspaces.push(...store.listManagedWorkspaces(task.id));
90
- }
91
- return workspaces;
92
- }
93
- function collectTaskStatuses(store) {
94
- const statuses = new Map();
95
- for (const task of store.listTasks()) {
96
- statuses.set(task.id, task.status);
97
- }
98
- return statuses;
99
- }
100
- /**
101
- * Workspace paths claimed by active durable Jobs. An active AgentRun still
102
- * holds its workspace even after the managed workspace record is gone, so
103
- * those paths stay protected until the AgentRun finishes.
104
- */
105
- function collectActiveWorkspaceOwnerPaths(store) {
106
- const paths = [];
107
- for (const task of store.listTasks()) {
108
- for (const run of store.listRuns(task.id)) {
109
- if (run.status !== "active")
110
- continue;
111
- const workspace = run.workspace;
112
- if (workspace === undefined)
113
- continue;
114
- paths.push(workspace.root, ...workspace.entries.map((entry) => entry.path));
115
- }
116
- }
117
- return paths;
118
- }
@@ -112,7 +112,7 @@ export async function scanLiveReferences(input) {
112
112
  diagnostics.push(...runtimeClaims.diagnostics);
113
113
  const activeReleaseClaims = readActiveReleaseClaims(home);
114
114
  diagnostics.push(...activeReleaseClaims.diagnostics);
115
- const sessionOwnerClaims = readSessionOwnerClaims(home);
115
+ const sessionOwnerClaims = readSessionOwnerClaims(input.sessionOwners);
116
116
  diagnostics.push(...sessionOwnerClaims.diagnostics);
117
117
  for (const claim of [
118
118
  ...runtimeClaims.claims,
@@ -231,62 +231,15 @@ function readActiveReleaseClaims(home) {
231
231
  });
232
232
  }
233
233
  }
234
- /**
235
- * Read Session owner records (`runtime/session-owners/<pid-startIdentity>.json`). A
236
- * record whose Provider root is physically alive with a matching start
237
- * identity protects its runtime root. A dead PID is stale (reconciliation
238
- * removes those records); an identity conflict or unreadable record fails
239
- * closed because liveness can no longer be proven.
240
- */
241
- function readSessionOwnerClaims(home) {
242
- const directory = join(home, "runtime", "session-owners");
243
- let entries;
244
- try {
245
- entries = readdirSync(directory, { withFileTypes: true });
246
- }
247
- catch (error) {
248
- if (error.code === "ENOENT") {
249
- return Object.freeze({ claims: Object.freeze([]), diagnostics: Object.freeze([]) });
250
- }
251
- return Object.freeze({
252
- claims: Object.freeze([]),
253
- diagnostics: Object.freeze([{
254
- source: "controller",
255
- severity: "error",
256
- message: `session owner records are unreadable: ${error instanceof Error ? error.message : "unknown error"}`
257
- }])
258
- });
259
- }
234
+ /** SQLite supplies durable custody; exact OS generation proves current life.
235
+ * The same check is used during scanning and under the GC writer fence. */
236
+ export function readSessionOwnerClaims(records) {
260
237
  const claims = [];
261
238
  const diagnostics = [];
262
- for (const entry of entries) {
263
- if (!entry.isFile() || !entry.name.endsWith(".json"))
264
- continue;
265
- const path = join(directory, entry.name);
266
- let record;
267
- try {
268
- record = JSON.parse(readFileSync(path, "utf8"));
269
- }
270
- catch (error) {
271
- diagnostics.push({
272
- source: "controller",
273
- severity: "error",
274
- message: `session owner record is unreadable at ${path}: ${error instanceof Error ? error.message : "unknown error"}`
275
- });
276
- continue;
277
- }
278
- const pid = record.providerRoot?.pid;
279
- const startIdentity = record.providerRoot?.startIdentity;
280
- if (typeof pid !== "number" || pid <= 0 || typeof startIdentity !== "string") {
281
- diagnostics.push({
282
- source: "controller",
283
- severity: "error",
284
- message: `session owner record at ${path} has no valid provider root identity`
285
- });
286
- continue;
287
- }
239
+ for (const record of records) {
240
+ const { pid, startIdentity } = record.providerRoot;
288
241
  if (!isProcessAlive(pid)) {
289
- // Dead Provider root: the record is stale; reconciliation removes it.
242
+ // The normal reconciliation owner releases dead custody records.
290
243
  continue;
291
244
  }
292
245
  const currentIdentity = readLinuxProcessStartIdentity(pid);