@zq-silk/yui 0.15.11 → 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 (280) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/ARCHITECTURE.zh-CN.md +8 -4
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/artifacts/artifactCommitLock.js +16 -55
  6. package/dist/artifacts/taskArtifactRepository.js +3 -3
  7. package/dist/cli/agentConfigurationPicker.js +1 -1
  8. package/dist/cli/commandCatalog.js +92 -48
  9. package/dist/cli/completion.js +18 -18
  10. package/dist/cli/completionWizard.js +1 -1
  11. package/dist/cli/dynamicCompletion.js +1 -1
  12. package/dist/cli/interactionPolicy.js +11 -3
  13. package/dist/cli/invocationAuthority.js +64 -0
  14. package/dist/cli/managedDiagnostics.js +2 -2
  15. package/dist/cli/parseRepeatable.js +35 -0
  16. package/dist/cli/updatePorts.js +26 -5
  17. package/dist/cli.js +1303 -1235
  18. package/dist/commands/agentCommands.js +4 -4
  19. package/dist/commands/capabilityCommands.js +1 -1
  20. package/dist/commands/configCommands.js +6 -57
  21. package/dist/commands/configOverview.js +2 -2
  22. package/dist/commands/durableJobCommands.js +12 -6
  23. package/dist/commands/executionAuditCommands.js +10 -0
  24. package/dist/commands/globalRoleCommands.js +78 -29
  25. package/dist/commands/grantCommands.js +0 -3
  26. package/dist/commands/jobCommands.js +1 -4
  27. package/dist/commands/operatorCommands.js +3 -3
  28. package/dist/commands/projectCommands.js +45 -25
  29. package/dist/commands/resourcesCommands.js +10 -40
  30. package/dist/commands/roleConfiguration.js +2 -6
  31. package/dist/commands/taskActivationCommands.js +4 -8
  32. package/dist/commands/taskAgentCapabilities.js +9 -0
  33. package/dist/commands/taskCommandSupport.js +155 -0
  34. package/dist/commands/taskCommandTypes.js +1 -0
  35. package/dist/commands/taskCommands.js +355 -732
  36. package/dist/commands/taskCompletionGate.js +1 -1
  37. package/dist/commands/taskExecutionCommands.js +1 -1
  38. package/dist/commands/taskFactCommands.js +325 -0
  39. package/dist/commands/taskInputCommands.js +12 -42
  40. package/dist/commands/taskIntegrationCommands.js +20 -58
  41. package/dist/commands/taskNextActionCommand.js +7 -6
  42. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  43. package/dist/commands/taskPublicationCommands.js +12 -3
  44. package/dist/commands/taskPublicationVerifyCommand.js +24 -40
  45. package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
  46. package/dist/commands/taskUpstreamCommands.js +19 -8
  47. package/dist/commands/workflowCommands.js +6 -1
  48. package/dist/completion/completionInstaller.js +26 -26
  49. package/dist/completion/completionState.js +26 -26
  50. package/dist/completion/fileCompletionManager.js +6 -11
  51. package/dist/config/configCatalog.js +0 -2
  52. package/dist/config/timeZone.js +14 -0
  53. package/dist/config/yuiConfig.js +0 -29
  54. package/dist/context/runContextPack.js +5 -3
  55. package/dist/context/taskCatalog.js +185 -0
  56. package/dist/context/taskContext.js +152 -151
  57. package/dist/context/wakeRunReferences.js +11 -0
  58. package/dist/controller/agentCapabilities.js +58 -0
  59. package/dist/controller/agentRuntimeObserver.js +6 -11
  60. package/dist/controller/clientRuntime.js +22 -14
  61. package/dist/controller/controller.js +20 -24
  62. package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
  63. package/dist/controller/globalInputDelivery.js +13 -0
  64. package/dist/controller/jobClient.js +4 -4
  65. package/dist/controller/jobControl.js +60 -25
  66. package/dist/controller/jobSupervisor.js +4 -4
  67. package/dist/controller/providerRetryAdmission.js +100 -0
  68. package/dist/controller/providerRetryDelivery.js +218 -0
  69. package/dist/controller/runtime.js +85 -43
  70. package/dist/controller/runtimeEventProcessor.js +0 -5
  71. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  72. package/dist/controller/taskAgentError.js +35 -0
  73. package/dist/coordination/workMailboxQueue.js +2 -24
  74. package/dist/core/controllerClient.js +2 -2
  75. package/dist/core/fileLockOwner.js +74 -0
  76. package/dist/decision/decision.js +17 -0
  77. package/dist/doctor/doctor.js +18 -9
  78. package/dist/event/taskEvent.js +12 -0
  79. package/dist/execution/workItemExecutionProjection.js +2 -2
  80. package/dist/executor/agentCapabilityConfig.js +43 -0
  81. package/dist/executor/agentConfigurationCatalog.js +49 -11
  82. package/dist/executor/agentExecutor.js +5 -8
  83. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  84. package/dist/executor/taskAgentCapabilities.js +64 -0
  85. package/dist/integration/deliveryObligation.js +1 -69
  86. package/dist/integration/gitIntegrationService.js +79 -90
  87. package/dist/integration/integrationAttempt.js +3 -14
  88. package/dist/integration/integrationSourceApplication.js +4 -12
  89. package/dist/integration/manifestTags.js +2 -2
  90. package/dist/job/durableJob.js +4 -25
  91. package/dist/job/jobAssignmentScope.js +22 -0
  92. package/dist/job/jobOperation.js +16 -0
  93. package/dist/job/jobRunner.js +2 -1
  94. package/dist/job/stepDirectory.js +15 -0
  95. package/dist/kernel/builtinCapabilities.js +15 -11
  96. package/dist/kernel/kernelPorts.js +1 -17
  97. package/dist/lifecycle/exactRunTerminalization.js +5 -10
  98. package/dist/message/globalProviderRetry.js +15 -0
  99. package/dist/message/inputControlResolution.js +3 -4
  100. package/dist/message/message.js +17 -20
  101. package/dist/message/messageContinuation.js +1 -1
  102. package/dist/milestone/milestone.js +11 -0
  103. package/dist/observability/executionAudit.js +19 -0
  104. package/dist/observability/orchestrationMetrics.js +24 -1
  105. package/dist/observability/runtimeIdentity.js +1 -17
  106. package/dist/output/agentConfigurationPresentation.js +2 -0
  107. package/dist/output/timePresentation.js +1 -14
  108. package/dist/plugins/pluginService.js +12 -4
  109. package/dist/release/releaseWorkflowPorts.js +45 -96
  110. package/dist/release/runtimeRelease.js +9 -15
  111. package/dist/repository/gitWorkspace.js +371 -105
  112. package/dist/repository/projectMaintenanceLock.js +98 -82
  113. package/dist/repository/taskBaseFreshness.js +1 -1
  114. package/dist/repository/taskWorkspaceCoordinator.js +82 -144
  115. package/dist/repository/taskWorkspacePreparer.js +113 -47
  116. package/dist/repository/workspaceCleanupInspection.js +187 -0
  117. package/dist/resources/autoResourceGc.js +4 -77
  118. package/dist/resources/liveReferences.js +7 -54
  119. package/dist/resources/resourceDiscovery.js +5 -118
  120. package/dist/resources/resourceGc.js +233 -181
  121. package/dist/resources/resourceRegistrar.js +5 -4
  122. package/dist/resources/resourceRegistry.js +3 -37
  123. package/dist/resources/resourceRegistryStore.js +13 -36
  124. package/dist/resources/sqliteResourceRegistry.js +43 -26
  125. package/dist/review/deltaRecheck.js +1 -1
  126. package/dist/review/reviewAcceptance.js +1 -1
  127. package/dist/review/reviewDecision.js +1 -1
  128. package/dist/review/reviewRound.js +8 -7
  129. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  130. package/dist/runtime/acpProtocol.js +4 -51
  131. package/dist/runtime/acpSession.js +10 -75
  132. package/dist/runtime/acpSessionConfiguration.js +1 -8
  133. package/dist/runtime/agentEndpoint.js +3 -2
  134. package/dist/runtime/agentError.js +5 -3
  135. package/dist/runtime/agentFailureContext.js +43 -0
  136. package/dist/runtime/agentHost.js +42 -25
  137. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  138. package/dist/runtime/codexAppServerRuntime.js +34 -3
  139. package/dist/runtime/codexInteractiveHost.js +2 -2
  140. package/dist/runtime/index.js +0 -1
  141. package/dist/runtime/managedCaller.js +21 -1
  142. package/dist/runtime/providerControl.js +5 -1
  143. package/dist/runtime/providerErrors.js +38 -0
  144. package/dist/runtime/providerRetry.js +198 -0
  145. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  146. package/dist/runtime/runtimeObservation.js +0 -5
  147. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  148. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  149. package/dist/runtime/sessionTokenMetrics.js +15 -5
  150. package/dist/runtime/structuredProviderHost.js +10 -43
  151. package/dist/runtime/taskUsageMetrics.js +275 -0
  152. package/dist/runtime/tmuxAdapters.js +6 -8
  153. package/dist/scheduler/activeRoleRunDelivery.js +19 -7
  154. package/dist/scheduler/leaderWakeupProcessor.js +24 -6
  155. package/dist/scheduler/operatorEvent.js +12 -20
  156. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  157. package/dist/scheduler/ports.js +5 -8
  158. package/dist/scheduler/roleRunLiveness.js +4 -4
  159. package/dist/scheduler/roleRunStall.js +16 -23
  160. package/dist/scheduler/taskExecutionProjection.js +49 -36
  161. package/dist/scheduler/taskObservabilityProjection.js +6 -45
  162. package/dist/scheduler/taskWake.js +5 -10
  163. package/dist/scheduler/wakeReason.js +2 -0
  164. package/dist/scheduler/wakeupQueue.js +2 -4
  165. package/dist/setup/setupCommand.js +1 -1
  166. package/dist/storage/contextRecords.js +106 -0
  167. package/dist/storage/currentTaskStore.js +1 -1
  168. package/dist/storage/homeLayout.js +13 -17
  169. package/dist/storage/migrations/agentFailureContext.js +22 -0
  170. package/dist/storage/migrations/currentInputContract.js +86 -0
  171. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  172. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  173. package/dist/storage/migrations/integrationContinuation.js +5 -4
  174. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  175. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  176. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  177. package/dist/storage/migrations/verificationPolicy.js +74 -0
  178. package/dist/storage/migrations/workItemHistory.js +46 -0
  179. package/dist/storage/persistenceWorker.js +12 -4
  180. package/dist/storage/recordValidation.js +87 -0
  181. package/dist/storage/sqliteSchema.js +173 -1
  182. package/dist/storage/sqliteStore.js +172 -161
  183. package/dist/storage/storageSchema.js +2 -14
  184. package/dist/storage/storageVersions.js +1 -1
  185. package/dist/storage/storeRpc.js +16 -7
  186. package/dist/storage/taskCatalog.js +123 -0
  187. package/dist/storage/taskStore.js +5 -7
  188. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  189. package/dist/task/archiveDiagnostics.js +1 -0
  190. package/dist/task/archivePreflight.js +124 -0
  191. package/dist/task/completionReadiness.js +5 -24
  192. package/dist/task/draftPlan.js +0 -53
  193. package/dist/task/nextAction.js +7 -13
  194. package/dist/task/publicationAdoption.js +56 -0
  195. package/dist/task/publicationReference.js +10 -0
  196. package/dist/task/remoteDelivery.js +31 -16
  197. package/dist/task/remoteDeliveryService.js +89 -0
  198. package/dist/task/taskActivation.js +2 -25
  199. package/dist/task/taskActivationService.js +0 -2
  200. package/dist/task/taskRecordReference.js +0 -1
  201. package/dist/task/taskSubmission.js +3 -14
  202. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  203. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  204. package/dist/telemetry/telemetryWiring.js +4 -3
  205. package/dist/tmux/tmuxManager.js +29 -20
  206. package/dist/verification/gateArtifact.js +15 -24
  207. package/dist/verification/gateArtifactStore.js +14 -7
  208. package/dist/verification/verificationGateService.js +29 -88
  209. package/dist/verification/verificationPlan.js +27 -72
  210. package/dist/web/assets/client/app.js +92 -17
  211. package/dist/web/assets/client/components.js +55 -13
  212. package/dist/web/assets/client/i18n.js +72 -4
  213. package/dist/web/assets/client/taskSurface.js +6 -5
  214. package/dist/web/assets/client/view.js +35 -7
  215. package/dist/web/assets/shell.js +6 -0
  216. package/dist/web/assets/styles/layout.js +7 -0
  217. package/dist/web/webServer.js +12 -3
  218. package/dist/web/webSnapshot.js +14 -85
  219. package/dist/web/webTaskSurface.js +17 -46
  220. package/dist/workItem/workItem.js +1 -12
  221. package/dist/workspace/cleanupInspection.js +63 -0
  222. package/dist/workspace/workItemChangeSetManager.js +110 -50
  223. package/docs/agent-result-consumption.md +4 -0
  224. package/docs/agent-result-consumption.zh-CN.md +3 -0
  225. package/docs/agent-runtime-drivers.md +7 -0
  226. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  227. package/docs/architecture/README.md +2 -0
  228. package/docs/architecture/README.zh-CN.md +3 -1
  229. package/docs/architecture/capabilities-and-resources.md +42 -5
  230. package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
  231. package/docs/managed-turn-and-session-runtime.md +121 -1
  232. package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
  233. package/docs/observability/README.md +62 -0
  234. package/docs/observability/README.zh-CN.md +47 -0
  235. package/docs/plugin-sdk.md +4 -2
  236. package/docs/project-refresh.md +77 -0
  237. package/docs/project-refresh.zh-CN.md +59 -0
  238. package/docs/provider-retry.md +70 -0
  239. package/docs/provider-runtime.md +3 -1
  240. package/docs/provider-runtime.zh-CN.md +4 -2
  241. package/docs/release-workflow.md +190 -7
  242. package/docs/release-workflow.zh-CN.md +139 -5
  243. package/docs/roles-and-configuration.md +29 -0
  244. package/docs/roles-and-configuration.zh-CN.md +21 -0
  245. package/docs/sqlite-control-plane-design.md +16 -3
  246. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  247. package/docs/task-dag-semantics.md +3 -3
  248. package/docs/task-dag-semantics.zh-CN.md +2 -2
  249. package/docs/task-delivery.md +223 -17
  250. package/docs/task-delivery.zh-CN.md +171 -14
  251. package/docs/task-discovery.md +101 -0
  252. package/docs/task-discovery.zh-CN.md +85 -0
  253. package/docs/testing/verification-levels.md +161 -9
  254. package/docs/testing/verification-levels.zh-CN.md +116 -9
  255. package/i18n/README.zh-CN.md +13 -7
  256. package/package.json +1 -1
  257. package/skills/yui-leader/SKILL.md +5 -0
  258. package/skills/yui-leader/references/execution.md +3 -2
  259. package/skills/yui-leader/references/integration.md +9 -5
  260. package/skills/yui-leader/references/planning.md +9 -3
  261. package/skills/yui-operator/SKILL.md +24 -8
  262. package/skills/yui-reviewer/SKILL.md +4 -0
  263. package/skills/yui-runtime/SKILL.md +17 -2
  264. package/skills/yui-runtime/references/publication.md +26 -4
  265. package/skills/yui-runtime/references/recovery.md +71 -0
  266. package/dist/agent/agentRegistry.js +0 -10
  267. package/dist/agentRun/runIdentity.js +0 -22
  268. package/dist/commands/deliveryGuardPreflight.js +0 -30
  269. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  270. package/dist/commands/taskOverviewCommand.js +0 -376
  271. package/dist/completion/completionWizard.js +0 -125
  272. package/dist/context/dispatchContext.js +0 -110
  273. package/dist/context/wakeNotification.js +0 -144
  274. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  275. package/dist/integration/integrationQueueEntry.js +0 -191
  276. package/dist/integration/integrationQueueService.js +0 -810
  277. package/dist/release/fakeReleasePorts.js +0 -55
  278. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  279. package/dist/task/deliveryGuard.js +0 -226
  280. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -1,5 +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
+ import { performance } from "node:perf_hooks";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { currentFileLockOwner, fileLockOwnerIsLive } from "../core/fileLockOwner.js";
3
6
  /**
4
7
  * Per-Project maintenance fence.
5
8
  *
@@ -12,35 +15,66 @@ import { join } from "node:path";
12
15
  * gone; the owner file records PID + process start time, so a recycled PID
13
16
  * can never pass for the original holder.
14
17
  *
15
- * Maintenance operations are long-lived, so a contended fence fails fast
16
- * with a retryable error instead of blocking the CLI for seconds.
18
+ * Contenders wait asynchronously within one monotonic budget. Only lock
19
+ * acquisition is retried; the caller's protected operation is never replayed.
17
20
  */
18
- const PROJECT_MAINTENANCE_LOCK_TIMEOUT_MS = 250;
19
- const PROJECT_MAINTENANCE_LOCK_RETRY_MS = 10;
21
+ const PROJECT_MAINTENANCE_LOCK_TIMEOUT_MS = 60_000;
22
+ const PROJECT_MAINTENANCE_LOCK_RETRY_MIN_MS = 200;
23
+ const PROJECT_MAINTENANCE_LOCK_RETRY_MAX_MS = 500;
20
24
  /** A lock older than this with a dead owner is reclaimed. */
21
25
  const STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS = 1_000;
22
26
  /**
23
- * Raised when a Project's maintenance fence is already held. The caller
24
- * made no changes and is safe to retry once the holder finishes.
27
+ * Raised when acquisition exhausted its budget. This acquisition has not
28
+ * started the protected operation; callers retain responsibility for any
29
+ * earlier effects and must not replay the whole operation implicitly.
25
30
  */
26
31
  export class ProjectMaintenanceLockedError extends Error {
32
+ timeoutMs;
27
33
  projectId;
28
34
  retryable = true;
29
- constructor(projectId) {
30
- super(`Project maintenance is already in progress for ${projectId}; retry once it finishes.`);
35
+ constructor(projectId, timeoutMs = PROJECT_MAINTENANCE_LOCK_TIMEOUT_MS) {
36
+ super(`Timed out after ${timeoutMs}ms waiting for Project maintenance on ${projectId}; `
37
+ + "the lock was not acquired. Read current state before retrying.");
38
+ this.timeoutMs = timeoutMs;
31
39
  this.name = "ProjectMaintenanceLockedError";
32
40
  this.projectId = projectId;
33
41
  }
34
42
  }
43
+ export class ProjectMaintenanceLockCancelledError extends Error {
44
+ projectId;
45
+ constructor(projectId, cause) {
46
+ super(`Cancelled waiting for Project maintenance on ${projectId}; the lock was not acquired.`, { cause });
47
+ this.projectId = projectId;
48
+ this.name = "ProjectMaintenanceLockCancelledError";
49
+ }
50
+ }
51
+ function acquisitionBudget(options) {
52
+ const timeoutMs = options.timeoutMs ?? PROJECT_MAINTENANCE_LOCK_TIMEOUT_MS;
53
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
54
+ throw new RangeError("Project maintenance timeout must be a finite nonnegative number.");
55
+ }
56
+ const now = options.now ?? (() => performance.now());
57
+ return {
58
+ timeoutMs,
59
+ deadline: now() + timeoutMs,
60
+ now,
61
+ random: options.random ?? Math.random,
62
+ wait: options.wait ?? ((milliseconds, signal) => delay(milliseconds, undefined, { signal })),
63
+ signal: options.signal
64
+ };
65
+ }
66
+ function assertNotCancelled(projectId, signal) {
67
+ if (signal?.aborted)
68
+ throw new ProjectMaintenanceLockCancelledError(projectId, signal.reason);
69
+ }
35
70
  /** Directory of one Project's maintenance fence, below the Home's locks area. */
36
71
  export function projectMaintenanceLockPath(home, projectId) {
37
72
  return join(home, "locks", "projects", `${projectId}.lock`);
38
73
  }
39
74
  /**
40
75
  * Acquire one Project's maintenance fence. Returns the release function;
41
- * callers MUST release on every exit path (try/finally). A live holder
42
- * fails fast with {@link ProjectMaintenanceLockedError}; a stale (dead)
43
- * holder is reclaimed.
76
+ * callers MUST release on every exit path (try/finally). A live holder is
77
+ * never stolen, including at timeout; a stale (dead) holder is reclaimed.
44
78
  *
45
79
  * There is no in-process reentrancy: every acquisition contends, so two
46
80
  * independent operations in the same process are mutually exclusive just
@@ -48,14 +82,26 @@ export function projectMaintenanceLockPath(home, projectId) {
48
82
  * fence while its caller already holds it) goes through private
49
83
  * already-locked methods, never through a second acquisition.
50
84
  */
51
- export function acquireProjectMaintenanceLock(home, projectId) {
85
+ export function acquireProjectMaintenanceLock(home, projectId, options = {}) {
86
+ return acquireWithBudget(home, projectId, acquisitionBudget(options));
87
+ }
88
+ async function acquireWithBudget(home, projectId, budget, firstProject = true) {
52
89
  const lock = projectMaintenanceLockPath(home, projectId);
90
+ assertNotCancelled(projectId, budget.signal);
91
+ const ownerIdentity = currentFileLockOwner();
53
92
  mkdirSync(join(home, "locks", "projects"), { recursive: true, mode: 0o700 });
54
- const deadline = Date.now() + PROJECT_MAINTENANCE_LOCK_TIMEOUT_MS;
93
+ let firstAttempt = firstProject;
55
94
  while (true) {
95
+ assertNotCancelled(projectId, budget.signal);
96
+ // Always try the first Project immediately, even for a zero-budget probe.
97
+ // Every retry and subsequent Project shares the original deadline.
98
+ if (!firstAttempt && budget.now() >= budget.deadline) {
99
+ throw new ProjectMaintenanceLockedError(projectId, budget.timeoutMs);
100
+ }
101
+ firstAttempt = false;
56
102
  try {
57
103
  mkdirSync(lock, { mode: 0o700 });
58
- const ownerIdentity = writeOwnerIdentity(lock);
104
+ writeOwnerIdentity(lock, ownerIdentity);
59
105
  let released = false;
60
106
  return () => {
61
107
  if (released)
@@ -70,16 +116,27 @@ export function acquireProjectMaintenanceLock(home, projectId) {
70
116
  catch (error) {
71
117
  if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
72
118
  throw error;
119
+ if (budget.now() >= budget.deadline) {
120
+ throw new ProjectMaintenanceLockedError(projectId, budget.timeoutMs);
121
+ }
73
122
  reclaimStaleProjectMaintenanceLock(lock);
74
- if (Date.now() >= deadline)
75
- throw new ProjectMaintenanceLockedError(projectId);
76
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, PROJECT_MAINTENANCE_LOCK_RETRY_MS);
123
+ const remaining = budget.deadline - budget.now();
124
+ if (remaining <= 0)
125
+ throw new ProjectMaintenanceLockedError(projectId, budget.timeoutMs);
126
+ const jitter = PROJECT_MAINTENANCE_LOCK_RETRY_MIN_MS
127
+ + budget.random() * (PROJECT_MAINTENANCE_LOCK_RETRY_MAX_MS - PROJECT_MAINTENANCE_LOCK_RETRY_MIN_MS);
128
+ try {
129
+ await budget.wait(Math.min(jitter, remaining), budget.signal);
130
+ }
131
+ catch (waitError) {
132
+ assertNotCancelled(projectId, budget.signal);
133
+ throw waitError;
134
+ }
77
135
  }
78
136
  }
79
137
  }
80
138
  /** Write this process's owner identity and return the exact bytes recorded. */
81
- function writeOwnerIdentity(lock) {
82
- const identity = `${process.pid}:${processStartIdentity() ?? ""}`;
139
+ function writeOwnerIdentity(lock, identity = currentFileLockOwner()) {
83
140
  writeFileSync(join(lock, "owner"), `${identity}\n`, { mode: 0o600 });
84
141
  return identity;
85
142
  }
@@ -108,11 +165,12 @@ function releaseOwnedProjectMaintenanceLock(lock, ownerIdentity) {
108
165
  * including ones this process already holds: callers that need nesting must
109
166
  * use an already-locked private path instead of acquiring twice.
110
167
  */
111
- export function acquireProjectMaintenanceLocks(home, projectIds) {
168
+ export async function acquireProjectMaintenanceLocks(home, projectIds, options = {}) {
169
+ const budget = acquisitionBudget(options);
112
170
  const releases = [];
113
171
  try {
114
172
  for (const projectId of [...new Set(projectIds)].sort()) {
115
- releases.push(acquireProjectMaintenanceLock(home, projectId));
173
+ releases.push(await acquireWithBudget(home, projectId, budget, releases.length === 0));
116
174
  }
117
175
  }
118
176
  catch (error) {
@@ -131,14 +189,17 @@ export function acquireProjectMaintenanceLocks(home, projectIds) {
131
189
  }
132
190
  /**
133
191
  * Non-blocking fence check for the Controller: a Project is fenced only
134
- * while a live process holds its lock. A stale (dead-owner) lock reads as
135
- * 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.
136
194
  */
137
195
  export function isProjectMaintenanceFenced(home, projectId) {
138
196
  const lock = projectMaintenanceLockPath(home, projectId);
139
- if (!existsSync(lock))
140
- return false;
141
- return lockOwnerIsAlive(lock);
197
+ try {
198
+ return fileLockOwnerIsLive(lock);
199
+ }
200
+ catch {
201
+ return true;
202
+ }
142
203
  }
143
204
  /**
144
205
  * Reclaim a stale (dead-owner) lock through a crash-safe compare-and-delete
@@ -156,9 +217,8 @@ function reclaimStaleProjectMaintenanceLock(lock) {
156
217
  catch (error) {
157
218
  if (!isEnoent(error))
158
219
  throw error;
159
- // Ownerless lock: a crash between mkdir and owner publication. Reclaim
160
- // only when the directory is old enough that the creator is not still
161
- // 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.
162
222
  try {
163
223
  if (Date.now() - statSync(lock).mtimeMs < STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS)
164
224
  return;
@@ -179,7 +239,7 @@ function reclaimStaleProjectMaintenanceLock(lock) {
179
239
  return;
180
240
  throw error;
181
241
  }
182
- if (expectedOwner !== null && lockOwnerIsAlive(lock))
242
+ if (fileLockOwnerIsLive(lock))
183
243
  return;
184
244
  const reclaimLock = `${lock}.reclaim`;
185
245
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -228,7 +288,7 @@ function reclaimStaleProjectMaintenanceLock(lock) {
228
288
  }
229
289
  if (currentOwner !== expectedOwner)
230
290
  return;
231
- if (currentOwner !== null && lockOwnerIsAlive(lock))
291
+ if (fileLockOwnerIsLive(lock))
232
292
  return;
233
293
  rmSync(lock, { recursive: true, force: true });
234
294
  }
@@ -240,70 +300,26 @@ function reclaimStaleProjectMaintenanceLock(lock) {
240
300
  }
241
301
  /**
242
302
  * Reclaim a reclaim-lock directory only when it is provably orphaned: its
243
- * 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
244
304
  * lock whose owner just mkdir'ed but has not written its owner is not stolen.
245
305
  * Uses the same PID + start-time identity check as the main fence, so a
246
306
  * recycled PID with a different start identity does not keep an orphaned
247
- * reclaim lock alive. An ownerless (ENOENT owner) lock is still reclaimed;
248
- * an unreadable owner fails closed (treated as live).
307
+ * reclaim lock alive. An ownerless or unreadable lock remains unverified.
249
308
  * Returns true when it removed the lock (caller retries), false otherwise.
250
309
  */
251
310
  function reclaimOrphanedReclaimLock(reclaimLock) {
252
311
  try {
253
312
  if (Date.now() - statSync(reclaimLock).mtimeMs < STALE_PROJECT_MAINTENANCE_LOCK_AGE_MS)
254
313
  return false;
255
- if (lockOwnerIsAlive(reclaimLock))
314
+ if (fileLockOwnerIsLive(reclaimLock))
256
315
  return false;
257
316
  rmSync(reclaimLock, { recursive: true, force: true });
258
317
  return true;
259
318
  }
260
319
  catch (error) {
261
- return isEnoent(error);
262
- }
263
- }
264
- function lockOwnerIsAlive(lock) {
265
- let owner;
266
- try {
267
- owner = readFileSync(join(lock, "owner"), "utf8").trim();
268
- }
269
- catch (error) {
270
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
271
- return false;
272
- // An unreadable owner file fails closed: treat the fence as held.
273
- return true;
274
- }
275
- const separator = owner.indexOf(":");
276
- const pid = Number.parseInt(separator < 0 ? owner : owner.slice(0, separator), 10);
277
- if (!Number.isInteger(pid) || pid <= 0)
278
- return false;
279
- const recordedIdentity = separator < 0 ? "" : owner.slice(separator + 1);
280
- // PID + start-time identity: a recycled PID can never match the holder.
281
- const currentIdentity = processStartIdentity(pid);
282
- if (currentIdentity !== undefined) {
283
- return recordedIdentity !== "" && currentIdentity === recordedIdentity;
284
- }
285
- // Off /proc (non-Linux): fall back to liveness.
286
- return processIsAlive(pid);
287
- }
288
- /** Linux process start time (clock ticks since boot) for a PID; undefined off /proc. */
289
- function processStartIdentity(pid = process.pid) {
290
- try {
291
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
292
- const close = stat.lastIndexOf(")");
293
- const fields = stat.slice(close + 2).split(" ");
294
- return fields[19] ?? "0"; // field 22: starttime
295
- }
296
- catch {
297
- return undefined;
298
- }
299
- }
300
- function processIsAlive(pid) {
301
- try {
302
- process.kill(pid, 0);
303
- return true;
304
- }
305
- catch (error) {
306
- return error instanceof Error && "code" in error && error.code === "EPERM";
320
+ if (isEnoent(error))
321
+ return true;
322
+ throw error;
307
323
  }
308
324
  }
309
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 }),