@alpsckr/unitycli 0.4.3 → 0.4.4

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 (420) hide show
  1. package/README.md +5 -5
  2. package/contracts/command-contract.json +109 -217
  3. package/contracts/package-files.json +374 -294
  4. package/contracts/stable-session/direct-call-semantics.json +8 -2
  5. package/contracts/tool-completion-contract.json +148 -672
  6. package/dist/bridge/admission.d.ts +17 -1
  7. package/dist/bridge/admission.js +2 -0
  8. package/dist/bridge/admission.js.map +1 -1
  9. package/dist/bridge/domains/asset.js +3 -1
  10. package/dist/bridge/domains/asset.js.map +1 -1
  11. package/dist/bridge/domains/camera.js +39 -7
  12. package/dist/bridge/domains/camera.js.map +1 -1
  13. package/dist/bridge/domains/console.js +2 -1
  14. package/dist/bridge/domains/console.js.map +1 -1
  15. package/dist/bridge/domains/doctor.d.ts +3 -0
  16. package/dist/bridge/domains/doctor.js +8 -0
  17. package/dist/bridge/domains/doctor.js.map +1 -0
  18. package/dist/bridge/domains/editor.js +0 -4
  19. package/dist/bridge/domains/editor.js.map +1 -1
  20. package/dist/bridge/domains/game-object.js +4 -12
  21. package/dist/bridge/domains/game-object.js.map +1 -1
  22. package/dist/bridge/domains/graphics.js +1 -16
  23. package/dist/bridge/domains/graphics.js.map +1 -1
  24. package/dist/bridge/domains/object.js +3 -0
  25. package/dist/bridge/domains/object.js.map +1 -1
  26. package/dist/bridge/domains/physics.js +1 -2
  27. package/dist/bridge/domains/physics.js.map +1 -1
  28. package/dist/bridge/domains/profiler.js +18 -7
  29. package/dist/bridge/domains/profiler.js.map +1 -1
  30. package/dist/bridge/domains/project-tool.js +4 -0
  31. package/dist/bridge/domains/project-tool.js.map +1 -1
  32. package/dist/bridge/domains/reflection.js +10 -2
  33. package/dist/bridge/domains/reflection.js.map +1 -1
  34. package/dist/bridge/domains/runtime-tool.js +3 -0
  35. package/dist/bridge/domains/runtime-tool.js.map +1 -1
  36. package/dist/bridge/domains/scene.js +3 -8
  37. package/dist/bridge/domains/scene.js.map +1 -1
  38. package/dist/bridge/domains/screenshot.js +30 -6
  39. package/dist/bridge/domains/screenshot.js.map +1 -1
  40. package/dist/bridge/domains/ui.js +3 -1
  41. package/dist/bridge/domains/ui.js.map +1 -1
  42. package/dist/bridge/index.d.ts +5 -5
  43. package/dist/bridge/index.js +6 -9
  44. package/dist/bridge/index.js.map +1 -1
  45. package/dist/bridge/registry.js +3 -5
  46. package/dist/bridge/registry.js.map +1 -1
  47. package/dist/bridge/session-supervisor.d.ts +6 -3
  48. package/dist/bridge/session-supervisor.js +122 -34
  49. package/dist/bridge/session-supervisor.js.map +1 -1
  50. package/dist/bridge/types.d.ts +6 -5
  51. package/dist/bridge/types.js +16 -2
  52. package/dist/bridge/types.js.map +1 -1
  53. package/dist/catalog-protocol.d.ts +64 -0
  54. package/dist/catalog-protocol.js +460 -0
  55. package/dist/catalog-protocol.js.map +1 -0
  56. package/dist/cli-commands/command.js +84 -61
  57. package/dist/cli-commands/command.js.map +1 -1
  58. package/dist/cli-commands/extensions.js +70 -50
  59. package/dist/cli-commands/extensions.js.map +1 -1
  60. package/dist/cli-commands/instances.js +1 -1
  61. package/dist/cli-commands/instances.js.map +1 -1
  62. package/dist/cli-process-owners.d.ts +6 -0
  63. package/dist/cli-process-owners.js +27 -0
  64. package/dist/cli-process-owners.js.map +1 -0
  65. package/dist/cli.d.ts +1 -1
  66. package/dist/cli.js +20 -5
  67. package/dist/cli.js.map +1 -1
  68. package/dist/contracts/command-contract.js +14 -4
  69. package/dist/contracts/command-contract.js.map +1 -1
  70. package/dist/doctor.d.ts +4 -6
  71. package/dist/doctor.js +429 -398
  72. package/dist/doctor.js.map +1 -1
  73. package/dist/editor-blocker-observer.d.ts +2 -1
  74. package/dist/editor-blocker-observer.js +40 -13
  75. package/dist/editor-blocker-observer.js.map +1 -1
  76. package/dist/extension-availability.d.ts +27 -9
  77. package/dist/extension-availability.js +96 -56
  78. package/dist/extension-availability.js.map +1 -1
  79. package/dist/extensions/descriptor-loader.d.ts +2 -2
  80. package/dist/extensions/descriptor-loader.js +3 -4
  81. package/dist/extensions/descriptor-loader.js.map +1 -1
  82. package/dist/extensions/index.d.ts +1 -1
  83. package/dist/extensions/index.js +1 -1
  84. package/dist/extensions/index.js.map +1 -1
  85. package/dist/extensions/installer.d.ts +24 -7
  86. package/dist/extensions/installer.js +142 -66
  87. package/dist/extensions/installer.js.map +1 -1
  88. package/dist/extensions/registry.js +3 -5
  89. package/dist/extensions/registry.js.map +1 -1
  90. package/dist/extensions/state-contract.d.ts +11 -0
  91. package/dist/extensions/state-contract.js +50 -0
  92. package/dist/extensions/state-contract.js.map +1 -0
  93. package/dist/instances.d.ts +13 -1
  94. package/dist/instances.js +26 -0
  95. package/dist/instances.js.map +1 -1
  96. package/dist/package-manager.d.ts +0 -24
  97. package/dist/package-manager.js +5 -58
  98. package/dist/package-manager.js.map +1 -1
  99. package/dist/project-tool-discovery.d.ts +5 -2
  100. package/dist/project-tool-discovery.js +22 -18
  101. package/dist/project-tool-discovery.js.map +1 -1
  102. package/dist/runtime-code.js +1 -1
  103. package/dist/runtime-code.js.map +1 -1
  104. package/dist/tool-name.d.ts +4 -0
  105. package/dist/tool-name.js +15 -0
  106. package/dist/tool-name.js.map +1 -0
  107. package/dist/tools/.generated-completion-registry.d.ts +2 -0
  108. package/dist/tools/.generated-completion-registry.js +763 -325
  109. package/dist/tools/.generated-completion-registry.js.map +1 -1
  110. package/dist/tools/.generated-registry.d.ts +6 -11
  111. package/dist/tools/.generated-registry.js +6 -11
  112. package/dist/tools/.generated-registry.js.map +1 -1
  113. package/dist/tools/.generated-schemas.js +11 -16
  114. package/dist/tools/.generated-schemas.js.map +1 -1
  115. package/dist/tools/asset/refresh-status.d.ts +3 -0
  116. package/dist/tools/asset/refresh-status.js +12 -0
  117. package/dist/tools/asset/refresh-status.js.map +1 -0
  118. package/dist/tools/asset/refresh.js +7 -3
  119. package/dist/tools/asset/refresh.js.map +1 -1
  120. package/dist/tools/build/player-status.d.ts +3 -0
  121. package/dist/tools/build/player-status.js +12 -0
  122. package/dist/tools/build/player-status.js.map +1 -0
  123. package/dist/tools/build/run.js +1 -1
  124. package/dist/tools/build/run.js.map +1 -1
  125. package/dist/tools/catalog-control.d.ts +22 -0
  126. package/dist/tools/catalog-control.js +335 -0
  127. package/dist/tools/catalog-control.js.map +1 -0
  128. package/dist/tools/catalog.d.ts +7 -9
  129. package/dist/tools/catalog.js +66 -45
  130. package/dist/tools/catalog.js.map +1 -1
  131. package/dist/tools/commands.d.ts +1 -1
  132. package/dist/tools/commands.js +44 -11
  133. package/dist/tools/commands.js.map +1 -1
  134. package/dist/tools/define.js +3 -2
  135. package/dist/tools/define.js.map +1 -1
  136. package/dist/tools/dispatcher.d.ts +53 -6
  137. package/dist/tools/dispatcher.js +277 -108
  138. package/dist/tools/dispatcher.js.map +1 -1
  139. package/dist/tools/editor/compile.js +1 -1
  140. package/dist/tools/editor/compile.js.map +1 -1
  141. package/dist/tools/editor/play-mode-status.d.ts +3 -0
  142. package/dist/tools/editor/play-mode-status.js +12 -0
  143. package/dist/tools/editor/play-mode-status.js.map +1 -0
  144. package/dist/tools/editor/readiness.d.ts +0 -1
  145. package/dist/tools/editor/readiness.js +0 -1
  146. package/dist/tools/editor/readiness.js.map +1 -1
  147. package/dist/tools/editor/refresh.d.ts +7 -3
  148. package/dist/tools/editor/refresh.js +20 -15
  149. package/dist/tools/editor/refresh.js.map +1 -1
  150. package/dist/tools/editor/state.d.ts +2 -2
  151. package/dist/tools/editor/state.js +13 -12
  152. package/dist/tools/editor/state.js.map +1 -1
  153. package/dist/tools/metadata/derived.js +1 -7
  154. package/dist/tools/metadata/derived.js.map +1 -1
  155. package/dist/tools/metadata/status.js +1 -12
  156. package/dist/tools/metadata/status.js.map +1 -1
  157. package/dist/tools/metadata/types.d.ts +0 -2
  158. package/dist/tools/navmesh/live.js +1 -1
  159. package/dist/tools/navmesh/live.js.map +1 -1
  160. package/dist/tools/occlusion/live.js +1 -1
  161. package/dist/tools/occlusion/live.js.map +1 -1
  162. package/dist/tools/package/deploy.js +2 -2
  163. package/dist/tools/package/deploy.js.map +1 -1
  164. package/dist/tools/package/embed.js +1 -1
  165. package/dist/tools/package/embed.js.map +1 -1
  166. package/dist/tools/package/install-status.d.ts +3 -0
  167. package/dist/tools/package/install-status.js +12 -0
  168. package/dist/tools/package/install-status.js.map +1 -0
  169. package/dist/tools/package/install.js +3 -3
  170. package/dist/tools/package/install.js.map +1 -1
  171. package/dist/tools/package/live-resolve.js +45 -12
  172. package/dist/tools/package/live-resolve.js.map +1 -1
  173. package/dist/tools/package/registry-clear.js.map +1 -1
  174. package/dist/tools/package/registry-set.js.map +1 -1
  175. package/dist/tools/package/remove-status.d.ts +3 -0
  176. package/dist/tools/package/remove-status.js +12 -0
  177. package/dist/tools/package/remove-status.js.map +1 -0
  178. package/dist/tools/package/remove.js +3 -3
  179. package/dist/tools/package/remove.js.map +1 -1
  180. package/dist/tools/package/resolve.js +3 -3
  181. package/dist/tools/package/resolve.js.map +1 -1
  182. package/dist/tools/package/restore.js +2 -2
  183. package/dist/tools/package/restore.js.map +1 -1
  184. package/dist/tools/package/scoped-registry-add.js.map +1 -1
  185. package/dist/tools/status/support.d.ts +13 -0
  186. package/dist/tools/status/support.js +28 -0
  187. package/dist/tools/status/support.js.map +1 -0
  188. package/dist/tools/test/cancel.js +2 -2
  189. package/dist/tools/test/cancel.js.map +1 -1
  190. package/dist/tools/test/run-status.d.ts +3 -0
  191. package/dist/tools/test/run-status.js +12 -0
  192. package/dist/tools/test/run-status.js.map +1 -0
  193. package/dist/tools/test/run.js +7 -4
  194. package/dist/tools/test/run.js.map +1 -1
  195. package/dist/tools/tools/group-summaries.js +10 -14
  196. package/dist/tools/tools/group-summaries.js.map +1 -1
  197. package/docs/architecture.md +4 -4
  198. package/docs/availability-differences.md +4 -4
  199. package/docs/command-execution.md +4 -3
  200. package/docs/error-codes.md +13 -5
  201. package/docs/extensions.md +1 -7
  202. package/docs/risk-and-side-effects.md +4 -3
  203. package/docs/safety-boundaries.md +3 -1
  204. package/docs/target-and-instance.md +1 -1
  205. package/docs/tool-discovery.md +17 -10
  206. package/docs/tool-execution.md +18 -7
  207. package/extensions/cinemachine/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  208. package/extensions/hdrp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  209. package/extensions/probuilder/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  210. package/extensions/urp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  211. package/extensions/vfx-graph/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  212. package/package.json +2 -4
  213. package/skills/unitycli/SKILL.md +11 -2
  214. package/skills/unitycli/manifest.json +1 -1
  215. package/skills/unitycli/references/operation-protocol.md +3 -3
  216. package/unitypkg/Editor/AssemblyInfo.cs +5 -0
  217. package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +49 -14
  218. package/unitypkg/Editor/Bridge/BridgeHttpServer.cs +9 -35
  219. package/unitypkg/Editor/Bridge/BridgeQueueState.cs +672 -0
  220. package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +970 -120
  221. package/unitypkg/Editor/BridgeStartupStateMachine.cs +59 -1
  222. package/unitypkg/Editor/Commands/ProjectToolCommands.cs +4 -0
  223. package/unitypkg/Editor/Commands/RuntimeToolCommands.cs +3 -0
  224. package/unitypkg/Editor/Generated/UnityCliToolCompletionRegistry.g.cs +379 -14
  225. package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +28 -13
  226. package/unitypkg/Editor/Infrastructure/RegistryOwnershipResolver.cs +132 -105
  227. package/unitypkg/Editor/Infrastructure/UnityCliActiveOperation.cs +138 -29
  228. package/unitypkg/Editor/Infrastructure/UnityCliAdmission.cs +1856 -237
  229. package/unitypkg/Editor/Infrastructure/UnityCliDoctorDiagnostics.cs +220 -0
  230. package/unitypkg/Editor/Infrastructure/UnityCliDurableStatusCoordinator.cs +118 -0
  231. package/unitypkg/Editor/Infrastructure/UnityCliEditorStateSnapshot.cs +2 -1
  232. package/unitypkg/Editor/Infrastructure/UnityCliHierarchyPager.cs +203 -0
  233. package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +2 -1
  234. package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +201 -63
  235. package/unitypkg/Editor/Infrastructure/UnityCliJson.cs +2 -3
  236. package/unitypkg/Editor/Infrastructure/UnityCliLiveBuildCoordinator.cs +289 -46
  237. package/unitypkg/Editor/Infrastructure/UnityCliLiveRefreshCoordinator.cs +87 -53
  238. package/unitypkg/Editor/Infrastructure/UnityCliNavMeshBakeCoordinator.cs +4 -4
  239. package/unitypkg/Editor/Infrastructure/UnityCliObjectRef.cs +128 -25
  240. package/unitypkg/Editor/Infrastructure/UnityCliObjectSerializer.cs +435 -327
  241. package/unitypkg/Editor/Infrastructure/UnityCliOcclusionBakeCoordinator.cs +4 -4
  242. package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +351 -53
  243. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveCoordinator.cs +118 -18
  244. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveState.cs +20 -1
  245. package/unitypkg/Editor/Infrastructure/UnityCliReadGeneration.cs +97 -0
  246. package/unitypkg/Editor/Infrastructure/UnityCliReflection.cs +644 -133
  247. package/unitypkg/Editor/Infrastructure/UnityCliReloadOperation.cs +128 -41
  248. package/unitypkg/Editor/Infrastructure/UnityCliRuntimeExecutor.cs +14 -84
  249. package/unitypkg/Editor/Infrastructure/UnityCliSerializer.cs +220 -0
  250. package/unitypkg/Editor/Infrastructure/UnityCliSessionMutationLease.cs +69 -29
  251. package/unitypkg/Editor/Infrastructure/UnityCliTestRunCoordinator.cs +440 -54
  252. package/unitypkg/Editor/Infrastructure/UnityCliWaiter.cs +128 -35
  253. package/unitypkg/Editor/ProjectTools/IUnityCliTool.cs +9 -1
  254. package/unitypkg/Editor/ProjectTools/OfficialAttributeToolRegistry.cs +32 -31
  255. package/unitypkg/Editor/ProjectTools/ProjectToolRegistry.cs +607 -458
  256. package/unitypkg/Editor/ProjectTools/ProjectToolSchemaValidator.cs +35 -7
  257. package/unitypkg/Editor/ProjectTools/RegisteredTool.cs +501 -60
  258. package/unitypkg/Editor/ProjectTools/UnityCliProjectToolContext.cs +21 -0
  259. package/unitypkg/Editor/ProjectTools/UnityCliSchemaDeriver.cs +36 -7
  260. package/unitypkg/Editor/ProjectTools/UnityCliToolName.cs +30 -0
  261. package/unitypkg/Editor/Properties/AssemblyInfo.cs +3 -0
  262. package/unitypkg/Editor/Tools/asset/get-data.cs +16 -7
  263. package/unitypkg/Editor/Tools/build/BuildSceneStateTool.cs +23 -3
  264. package/unitypkg/Editor/Tools/build/BuildToolDtos.cs +2 -1
  265. package/unitypkg/Editor/Tools/build/scene/add.cs +6 -3
  266. package/unitypkg/Editor/Tools/build/scene/disable.cs +1 -1
  267. package/unitypkg/Editor/Tools/build/scene/enable.cs +1 -1
  268. package/unitypkg/Editor/Tools/build/scene/list.cs +1 -1
  269. package/unitypkg/Editor/Tools/build/scene/remove.cs +13 -3
  270. package/unitypkg/Editor/Tools/camera/CameraToolSupport.cs +38 -38
  271. package/unitypkg/Editor/Tools/camera/multiview.cs +13 -6
  272. package/unitypkg/Editor/Tools/camera/screenshot.cs +14 -7
  273. package/unitypkg/Editor/Tools/component/get.cs +16 -9
  274. package/unitypkg/Editor/Tools/component/modify.cs +6 -4
  275. package/unitypkg/Editor/Tools/console/clear.cs +7 -6
  276. package/unitypkg/Editor/Tools/console/get.cs +48 -16
  277. package/unitypkg/Editor/Tools/editor/EditorToolSupport.cs +143 -28
  278. package/unitypkg/Editor/Tools/editor/menu-items.cs +1 -1
  279. package/unitypkg/Editor/Tools/editor/play.cs +2 -11
  280. package/unitypkg/Editor/Tools/editor/selection.cs +1 -1
  281. package/unitypkg/Editor/Tools/editor/stop.cs +2 -11
  282. package/unitypkg/Editor/Tools/editor/windows.cs +1 -1
  283. package/unitypkg/Editor/Tools/go/GameObjectToolSupport.cs +54 -4
  284. package/unitypkg/Editor/Tools/go/delete.cs +1 -1
  285. package/unitypkg/Editor/Tools/go/find.cs +167 -70
  286. package/unitypkg/Editor/Tools/graphics/GraphicsToolSupport.cs +25 -0
  287. package/unitypkg/Editor/Tools/graphics/light-probe-group.cs +9 -3
  288. package/unitypkg/Editor/Tools/graphics/pipeline-info.cs +1 -1
  289. package/unitypkg/Editor/Tools/graphics/pipeline-settings.cs +1 -1
  290. package/unitypkg/Editor/Tools/graphics/quality.cs +2 -2
  291. package/unitypkg/Editor/Tools/graphics/reflection-probe.cs +3 -3
  292. package/unitypkg/Editor/Tools/graphics/stats.cs +1 -1
  293. package/unitypkg/Editor/Tools/object/ObjectToolSupport.cs +4 -4
  294. package/unitypkg/Editor/Tools/object/get-data.cs +9 -7
  295. package/unitypkg/Editor/Tools/object/modify.cs +7 -5
  296. package/unitypkg/Editor/Tools/physics/raycast.cs +24 -6
  297. package/unitypkg/Editor/Tools/prefab/stage.cs +1 -1
  298. package/unitypkg/Editor/Tools/profiler/ProfilerFrameDebuggerSupport.cs +94 -21
  299. package/unitypkg/Editor/Tools/profiler/ProfilerMemorySnapshotSupport.cs +397 -10
  300. package/unitypkg/Editor/Tools/profiler/ProfilerToolSupport.cs +18 -4
  301. package/unitypkg/Editor/Tools/profiler/frame-debugger/enable.cs +1 -1
  302. package/unitypkg/Editor/Tools/profiler/frame-debugger/events.cs +88 -87
  303. package/unitypkg/Editor/Tools/profiler/hierarchy.cs +278 -112
  304. package/unitypkg/Editor/Tools/profiler/memory-snapshot/compare.cs +9 -5
  305. package/unitypkg/Editor/Tools/profiler/memory-snapshot/list.cs +37 -35
  306. package/unitypkg/Editor/Tools/profiler/memory-snapshot/take.cs +99 -97
  307. package/unitypkg/Editor/Tools/project/info.cs +1 -1
  308. package/unitypkg/Editor/Tools/project/layers.cs +1 -1
  309. package/unitypkg/Editor/Tools/project/tags.cs +1 -1
  310. package/unitypkg/Editor/Tools/reflection/method/call.cs +9 -7
  311. package/unitypkg/Editor/Tools/reflection/method/find.cs +27 -37
  312. package/unitypkg/Editor/Tools/reflection/type/schema.cs +24 -28
  313. package/unitypkg/Editor/Tools/scene/gameobject/component.cs +8 -5
  314. package/unitypkg/Editor/Tools/scene/gameobject/components.cs +10 -7
  315. package/unitypkg/Editor/Tools/scene/gameobject-support.cs +104 -8
  316. package/unitypkg/Editor/Tools/scene/gameobject.cs +37 -16
  317. package/unitypkg/Editor/Tools/scene/hierarchy.cs +146 -142
  318. package/unitypkg/Editor/Tools/screenshot/EditorElementCaptureToolSupport.cs +77 -25
  319. package/unitypkg/Editor/Tools/screenshot/ScreenshotToolSupport.cs +315 -95
  320. package/unitypkg/Editor/Tools/screenshot/editor-element.cs +7 -5
  321. package/unitypkg/Editor/Tools/screenshot/game-view.cs +35 -21
  322. package/unitypkg/Editor/Tools/screenshot/isolated-object.cs +29 -23
  323. package/unitypkg/Editor/Tools/screenshot/orbit.cs +37 -25
  324. package/unitypkg/Editor/Tools/screenshot/scene-view.cs +25 -18
  325. package/unitypkg/Editor/Tools/screenshot/screen.cs +177 -17
  326. package/unitypkg/Editor/Tools/screenshot/surround.cs +83 -79
  327. package/unitypkg/Editor/Tools/ui/render-preview.cs +16 -11
  328. package/unitypkg/Editor/UnityCliBridge.cs +97 -34
  329. package/unitypkg/Runtime/Infrastructure/UnityCliRuntimeBridge.cs +753 -122
  330. package/unitypkg/Runtime/Properties/AssemblyInfo.cs +5 -0
  331. package/unitypkg/Runtime/UnityCli.Runtime.asmdef +1 -1
  332. package/unitypkg/Shared/AssemblyInfo.cs +4 -0
  333. package/unitypkg/Shared/UnityCliAdmissionPool.cs +265 -75
  334. package/unitypkg/Shared/UnityCliBridgeQueuePolicy.cs +410 -0
  335. package/unitypkg/Shared/UnityCliBridgeWorkItemState.cs +47 -0
  336. package/unitypkg/Shared/UnityCliExceptionProjection.cs +151 -0
  337. package/unitypkg/Shared/UnityCliQueuePublication.cs +30 -0
  338. package/unitypkg/Shared/UnityCliRequestBudget.cs +352 -0
  339. package/unitypkg/Shared/UnityCliRuntimeRequestPublication.cs +107 -0
  340. package/unitypkg/Tests/Editor/UnityCli.Editor.Tests.asmdef +2 -2
  341. package/unitypkg/Tests/Editor/UnityCliAdmissionCoordinatorIntegrationTests.cs +1329 -0
  342. package/unitypkg/Tests/Editor/UnityCliAdmissionPoolTests.cs +311 -114
  343. package/unitypkg/Tests/Editor/UnityCliAdmissionRepairStateTests.cs +94 -0
  344. package/unitypkg/Tests/Editor/UnityCliBridgeAdmissionIntegrationTests.cs +1229 -0
  345. package/unitypkg/Tests/Editor/UnityCliBridgeQueuePolicyTests.cs +412 -0
  346. package/unitypkg/Tests/Editor/UnityCliContinuationIntegrationTests.cs +292 -0
  347. package/unitypkg/Tests/Editor/UnityCliDiagnosticBoundaryTests.cs +120 -0
  348. package/unitypkg/Tests/Editor/UnityCliDurableStatusTests.cs +84 -0
  349. package/unitypkg/Tests/Editor/UnityCliExceptionProjectionTests.cs +72 -0
  350. package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +180 -97
  351. package/unitypkg/Tests/Editor/UnityCliMemoryReflectionIntegrationTests.cs +434 -0
  352. package/unitypkg/Tests/Editor/UnityCliRequestBudgetTests.cs +615 -0
  353. package/unitypkg/Tests/Editor/UnityCliRuntimeBridgeIntegrationTests.cs +520 -0
  354. package/unitypkg/Tests/Editor/UnityCliToolCatalogTests.cs +562 -0
  355. package/unitypkg/Tests/Runtime/UnityCli.Runtime.Tests.asmdef +23 -0
  356. package/unitypkg/Tests/Runtime/UnityCliRuntimeCatalogValidationTests.cs +29 -0
  357. package/unitypkg/package.json +1 -1
  358. package/contracts/static-capability-catalog.json +0 -48226
  359. package/dist/bridge/domains/diagnose.d.ts +0 -2
  360. package/dist/bridge/domains/diagnose.js +0 -11
  361. package/dist/bridge/domains/diagnose.js.map +0 -1
  362. package/dist/bridge/domains/extension.d.ts +0 -8
  363. package/dist/bridge/domains/extension.js +0 -20
  364. package/dist/bridge/domains/extension.js.map +0 -1
  365. package/dist/capabilities.d.ts +0 -5
  366. package/dist/capabilities.js +0 -84
  367. package/dist/capabilities.js.map +0 -1
  368. package/dist/static-capability-catalog.d.ts +0 -13
  369. package/dist/static-capability-catalog.js +0 -41
  370. package/dist/static-capability-catalog.js.map +0 -1
  371. package/dist/tools/diagnose/capabilities.d.ts +0 -13
  372. package/dist/tools/diagnose/capabilities.js +0 -34
  373. package/dist/tools/diagnose/capabilities.js.map +0 -1
  374. package/dist/tools/package/info.d.ts +0 -19
  375. package/dist/tools/package/info.js +0 -27
  376. package/dist/tools/package/info.js.map +0 -1
  377. package/dist/tools/package/list.d.ts +0 -27
  378. package/dist/tools/package/list.js +0 -21
  379. package/dist/tools/package/list.js.map +0 -1
  380. package/dist/tools/package/registry-get.d.ts +0 -10
  381. package/dist/tools/package/registry-get.js +0 -22
  382. package/dist/tools/package/registry-get.js.map +0 -1
  383. package/dist/tools/package/scoped-registry-list.d.ts +0 -14
  384. package/dist/tools/package/scoped-registry-list.js +0 -22
  385. package/dist/tools/package/scoped-registry-list.js.map +0 -1
  386. package/dist/tools/package/search.d.ts +0 -17
  387. package/dist/tools/package/search.js +0 -39
  388. package/dist/tools/package/search.js.map +0 -1
  389. package/dist/tools/package/status.d.ts +0 -21
  390. package/dist/tools/package/status.js +0 -37
  391. package/dist/tools/package/status.js.map +0 -1
  392. package/dist/tools/tools/describe.d.ts +0 -27
  393. package/dist/tools/tools/describe.js +0 -35
  394. package/dist/tools/tools/describe.js.map +0 -1
  395. package/dist/tools/tools/group-list.d.ts +0 -16
  396. package/dist/tools/tools/group-list.js +0 -38
  397. package/dist/tools/tools/group-list.js.map +0 -1
  398. package/dist/tools/tools/group.d.ts +0 -13
  399. package/dist/tools/tools/group.js +0 -34
  400. package/dist/tools/tools/group.js.map +0 -1
  401. package/dist/tools/tools/pagination.d.ts +0 -17
  402. package/dist/tools/tools/pagination.js +0 -28
  403. package/dist/tools/tools/pagination.js.map +0 -1
  404. package/dist/tools/tools/search.d.ts +0 -15
  405. package/dist/tools/tools/search.js +0 -42
  406. package/dist/tools/tools/search.js.map +0 -1
  407. package/unitypkg/Editor/Tools/diagnose/preflight.cs +0 -269
  408. package/unitypkg/Editor/Tools/editor/console.cs +0 -26
  409. package/unitypkg/Editor/Tools/extension/status.cs +0 -87
  410. package/unitypkg/Editor/Tools/go/inspect.cs +0 -49
  411. package/unitypkg/Editor/Tools/graphics/pipeline-quality.cs +0 -24
  412. package/unitypkg/Editor/Tools/graphics/probe-positions.cs +0 -58
  413. package/unitypkg/Editor/Tools/graphics/settings.cs +0 -104
  414. package/unitypkg/Editor/Tools/graphics/stats-counters.cs +0 -28
  415. package/unitypkg/Editor/Tools/graphics/stats-memory.cs +0 -21
  416. package/unitypkg/Editor/Tools/instances/list.cs +0 -71
  417. package/unitypkg/Editor/Tools/physics/raycast-all.cs +0 -67
  418. package/unitypkg/Editor/Tools/scene/build-settings.cs +0 -69
  419. package/unitypkg/Editor/Tools/scene/data.cs +0 -125
  420. package/unitypkg/Editor/Tools/tests/list.cs +0 -98
@@ -28,20 +28,77 @@ namespace UnityCli
28
28
  {
29
29
  public static partial class UnityCliBridge
30
30
  {
31
- private static readonly Queue<BridgeWorkItem> WorkQueue = new Queue<BridgeWorkItem>();
32
- private static readonly HashSet<BridgeWorkItem> RunningWork = new HashSet<BridgeWorkItem>();
33
- private static readonly Dictionary<string, BridgeWorkItem> ActiveRequestWork = new Dictionary<string, BridgeWorkItem>(StringComparer.Ordinal);
34
- private const int MaxWorkStartsPerTick = 16;
31
+ private static readonly BridgeQueueState ProductionQueueState = CreateProductionQueueState();
35
32
 
36
- private static void CancelQueuedWork()
33
+ private static BridgeQueueState CreateProductionQueueState()
37
34
  {
38
- List<BridgeWorkItem> pending;
39
- lock (QueueLock)
35
+ return new BridgeQueueState(
36
+ null,
37
+ null,
38
+ new BridgeQueueSinks(WriteLog, UnityCliTransactionLog.Append, true),
39
+ new BridgeQueueTestHooks(false),
40
+ PersistProductionAdmissionPoolState,
41
+ identity => PersistProductionAdmissionCompletionIntent(identity, "admission-completion"),
42
+ HasProductionAdmissionCompletionIntent,
43
+ ClearProductionAdmissionCompletionIntent,
44
+ null);
45
+ }
46
+
47
+ internal static BridgeQueueState CreateQueueStateForTests(UnityCliAdmissionPool pool, Func<long> monotonicNowMs = null)
48
+ {
49
+ if (pool == null) throw new ArgumentNullException("pool");
50
+ var intentSync = new object();
51
+ var intents = new HashSet<string>(StringComparer.Ordinal);
52
+ var state = new BridgeQueueState(
53
+ pool,
54
+ monotonicNowMs,
55
+ new BridgeQueueSinks(_ => { }, (_, __, ___, ____) => { }, false),
56
+ new BridgeQueueTestHooks(true),
57
+ () => true,
58
+ identity => { lock (intentSync) return intents.Add(CompletionIntentKey(identity)); },
59
+ identity => { lock (intentSync) return intents.Contains(CompletionIntentKey(identity)); },
60
+ identity => { lock (intentSync) return intents.Remove(CompletionIntentKey(identity)); },
61
+ new BridgeIsolatedExecutionState())
62
+ {
63
+ QueuePublicationOpen = true,
64
+ QueueSessionStopping = false,
65
+ ListenerId = "test-listener-" + Guid.NewGuid().ToString("N"),
66
+ ListenerGeneration = 1
67
+ };
68
+ return state;
69
+ }
70
+
71
+ /// <summary>关闭publication与摘除全部未启动WorkItem共享同一状态锁线性化点。</summary>
72
+ private static List<BridgeWorkItem> CloseQueuePublicationAndTakeQueuedWork(bool sessionStopping = false)
73
+ {
74
+ return CloseQueuePublicationAndTakeQueuedWork(ProductionQueueState, sessionStopping);
75
+ }
76
+
77
+ private static List<BridgeWorkItem> CloseQueuePublicationAndTakeQueuedWork(BridgeQueueState state, bool sessionStopping)
78
+ {
79
+ lock (state.QueueLock)
40
80
  {
41
- pending = new List<BridgeWorkItem>(WorkQueue);
42
- WorkQueue.Clear();
81
+ if (sessionStopping) state.TestHooks.Hit("before-queue-shutdown-close");
82
+ state.QueuePublicationOpen = false;
83
+ if (sessionStopping) state.QueueSessionStopping = true;
84
+ var pending = new List<BridgeWorkItem>(state.WorkQueue);
85
+ foreach (var item in pending)
86
+ {
87
+ item.QueueNode = null;
88
+ item.SafeStatusNode = null;
89
+ item.ControlNode = null;
90
+ }
91
+ state.WorkQueue.Clear();
92
+ state.SafeStatusQueue.Clear();
93
+ state.ControlQueue.Clear();
94
+ state.ControlScanCursor = null;
95
+ return pending;
43
96
  }
44
- foreach (var item in pending)
97
+ }
98
+
99
+ private static void CancelQueuedWork(IEnumerable<BridgeWorkItem> pending)
100
+ {
101
+ foreach (var item in pending ?? Array.Empty<BridgeWorkItem>())
45
102
  {
46
103
  item.CancelBeforeStart();
47
104
  item.Response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
@@ -55,8 +112,65 @@ namespace UnityCli
55
112
  }
56
113
  }
57
114
 
115
+ /// <summary>Editor退出时不再依赖后续update;只按captured pool完整身份隔离完成未启动WorkItem。</summary>
116
+ private static void CancelQueuedWorkForShutdown(IEnumerable<BridgeWorkItem> pending)
117
+ {
118
+ foreach (var item in pending ?? Array.Empty<BridgeWorkItem>())
119
+ {
120
+ if (!item.CancelBeforeStart()) continue;
121
+ item.Response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
122
+ item.Request.RequestId,
123
+ "E_UNITY_QUITTING",
124
+ "Unity quit before the queued request began any effect.",
125
+ nextActions: new[] { "Restart Unity and submit a new request." }));
126
+ CompleteQueuedWorkForShutdown(item);
127
+ }
128
+ }
129
+
130
+ private static bool CompleteQueuedWorkForShutdown(BridgeWorkItem item)
131
+ {
132
+ if (item == null || !item.TryBeginCompletion()) return false;
133
+ var outcome = item.AdmissionOwner == null
134
+ ? UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent
135
+ : item.CapturedAdmissionPool == null
136
+ ? UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked
137
+ : item.CapturedAdmissionPool.CompleteWorkItemForShutdown(ToPoolIdentity(item.AdmissionOwner));
138
+ if (outcome != UnityCliAdmissionPool.CompletionOutcome.ExactCurrentCompleted
139
+ && outcome != UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent)
140
+ {
141
+ item.AllowCompletionRetry();
142
+ return false;
143
+ }
144
+ var state = item.QueueState;
145
+ lock (state.QueueLock)
146
+ {
147
+ state.RunningWork.Remove(item);
148
+ state.PendingSettlementWork.Remove(item);
149
+ RemoveActiveWorkItem(state, item);
150
+ }
151
+ CompleteAndLogWorkItem(item);
152
+ return true;
153
+ }
154
+
155
+ internal static void SweepQueuedWorkForTests(BridgeQueueState state)
156
+ {
157
+ if (state == null) throw new ArgumentNullException("state");
158
+ CancelQueuedWork(CloseQueuePublicationAndTakeQueuedWork(state, false));
159
+ }
160
+
161
+ internal static void ShutdownQueuedWorkForTests(BridgeQueueState state)
162
+ {
163
+ if (state == null) throw new ArgumentNullException("state");
164
+ CancelQueuedWorkForShutdown(CloseQueuePublicationAndTakeQueuedWork(state, true));
165
+ ShutdownAdmissionPool(state);
166
+ }
167
+
58
168
  private static void ProcessQueuedWork()
59
169
  {
170
+ var state = ProductionQueueState;
171
+ state.ListenerId = ListenerId;
172
+ state.ListenerGeneration = Math.Max(1, CurrentListenerGeneration());
173
+ state.AdmissionPool = admissionPool;
60
174
  // pulse只证明EditorApplication.update最近推进;具体请求仍由TryStart证明开始。
61
175
  Volatile.Write(ref mainThreadPulseTimestamp, System.Diagnostics.Stopwatch.GetTimestamp());
62
176
  Interlocked.Increment(ref mainThreadPulseSequence);
@@ -78,58 +192,306 @@ namespace UnityCli
78
192
  AdvanceBridgeStartup();
79
193
  UnityCliSessionMutationLease.TryFinalizeOwnedPlayModeRoot();
80
194
  UnityCliWaitScheduler.Tick();
195
+ RetryDirectMutationTerminalCleanup();
196
+ RetryPendingWorkItemSettlements();
81
197
 
82
198
  // 新listener先保留至少一个主线程update的transitioning状态;后续生命周期变化即时发布,30秒刷新只修复漏事件。
83
- if (listenerWasStarted && started
199
+ if (listenerWasStarted && started && InitialPublicationGate.Published
84
200
  && (readyPublicationPending || lifecycleChanged || EditorApplication.timeSinceStartup >= nextRegistryRefreshAt))
85
201
  {
86
202
  readyPublicationPending = false;
87
203
  RegisterInstance("ready");
88
204
  nextRegistryRefreshAt = EditorApplication.timeSinceStartup + RegistryRefreshSeconds;
89
205
  }
90
- TickInstancePublicationRetry();
91
- var startedThisTick = 0;
92
- while (startedThisTick < MaxWorkStartsPerTick)
93
- {
94
- BridgeWorkItem item;
95
- var queueDepth = 0;
96
- lock (QueueLock)
97
- {
98
- if (WorkQueue.Count == 0) return;
99
- item = WorkQueue.Peek();
100
- var mutation = CoordinatedRequestIsMutation(item.Request);
101
- if (mutation && RunningWork.Any(running => running.IsMutation)) return;
102
- item = WorkQueue.Dequeue();
103
- item.IsMutation = mutation;
104
- queueDepth = WorkQueue.Count;
105
- }
106
- WriteLog("bridge_request_dequeued requestId=" + item.Request.RequestId + " command=" + item.Request.Command + " queueDepth=" + queueDepth.ToString(CultureInfo.InvariantCulture));
206
+ HandleInitialPublicationOutcome(TickInstancePublicationRetry());
207
+ int queueDepth;
208
+ List<ControlQueueRejection> rejected;
209
+ var selected = SelectQueuedStarts(state, CurrentDurableMutationBarrier(state), true, out queueDepth, out rejected);
107
210
 
211
+ foreach (var rejection in rejected) CompleteRejectedControlWork(rejection);
212
+ foreach (var item in selected)
213
+ {
214
+ state.Sinks.WriteLog("bridge_request_dequeued requestId=" + item.Request.RequestId + " command=" + item.Request.Command + " queueDepth=" + queueDepth.ToString(CultureInfo.InvariantCulture));
108
215
  if (!item.TryStart())
109
216
  {
110
- WriteLog("bridge_request_skipped_expired requestId=" + item.Request.RequestId + " command=" + item.Request.Command);
217
+ state.Sinks.WriteLog("bridge_request_skipped_expired requestId=" + item.Request.RequestId + " command=" + item.Request.Command);
111
218
  item.Response = item.Response ?? UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
112
219
  item.Request.RequestId,
113
- "E_BRIDGE_REQUEST_EXPIRED",
114
- "Bridge request expired before Unity started executing it.",
220
+ "E_EXECUTION_TIMEOUT",
221
+ "Bridge request deadline elapsed before Unity started executing it.",
115
222
  nextActions: new[] { "Retry after Unity is no longer busy." }));
116
223
  CompleteWorkItem(item);
117
224
  continue;
118
225
  }
119
-
120
- lock (QueueLock) RunningWork.Add(item);
226
+ lock (state.QueueLock) state.RunningWork.Add(item);
121
227
  _ = ExecuteWorkItemAsync(item);
122
- startedThisTick += 1;
123
228
  }
124
229
  }
125
230
 
231
+ /// <summary>control proof在锁外冻结;每个linked-list节点在lane访问当下完成唯一policy检查。</summary>
232
+ private static List<BridgeWorkItem> SelectQueuedStarts(BridgeQueueState state, DurableMutationBarrierProof barrier, bool remove, out int queueDepth, out List<ControlQueueRejection> rejections)
233
+ {
234
+ bool hasControlWork;
235
+ lock (state.QueueLock) hasControlWork = state.ControlQueue.Count > 0;
236
+ var controlContext = LoadControlDecisionContext(state, barrier, hasControlWork);
237
+ var barrierState = barrier == null ? UnityCliBridgeQueuePolicy.DurableMutationBarrier.Unreadable : barrier.State;
238
+ var snapshotItems = new List<BridgeWorkItem>(UnityCliBridgeQueuePolicy.MaxNodesExaminedPerTick);
239
+ var selected = new List<BridgeWorkItem>(UnityCliBridgeQueuePolicy.MaxStartsPerTick);
240
+ rejections = new List<ControlQueueRejection>();
241
+ var safeStatusVisits = 0;
242
+ var controlVisits = 0;
243
+ var fifoVisits = 0;
244
+ lock (state.QueueLock)
245
+ {
246
+ var runningOrdinaryReads = state.RunningWork.Count(item => item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.OrdinaryRead);
247
+ var mutationRunning = state.RunningWork.Any(item => item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.Mutation
248
+ || item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.Control);
249
+ var accumulator = UnityCliBridgeQueuePolicy.BeginSelection(runningOrdinaryReads, mutationRunning, barrierState);
250
+ var seen = new HashSet<BridgeWorkItem>();
251
+
252
+ var fifoCursor = state.WorkQueue.First;
253
+ while (fifoCursor != null && fifoVisits < UnityCliBridgeQueuePolicy.MaxFifoNodesExaminedPerTick)
254
+ {
255
+ ExamineQueueCandidate(fifoCursor.Value, controlContext, seen, snapshotItems, accumulator);
256
+ fifoVisits += 1;
257
+ fifoCursor = fifoCursor.Next;
258
+ }
259
+
260
+ var controlLimit = Math.Min(state.ControlQueue.Count, UnityCliBridgeQueuePolicy.MaxControlNodesExaminedPerTick);
261
+ var controlCursor = state.ControlScanCursor;
262
+ if (controlCursor == null || controlCursor.List != state.ControlQueue) controlCursor = state.ControlQueue.First;
263
+ while (controlCursor != null && controlVisits < controlLimit)
264
+ {
265
+ ExamineQueueCandidate(controlCursor.Value, controlContext, seen, snapshotItems, accumulator);
266
+ controlVisits += 1;
267
+ controlCursor = controlCursor.Next ?? state.ControlQueue.First;
268
+ }
269
+ state.ControlScanCursor = state.ControlQueue.Count == 0 ? null : controlCursor;
270
+
271
+ var safeCursor = state.SafeStatusQueue.First;
272
+ while (safeCursor != null && safeStatusVisits < UnityCliBridgeQueuePolicy.MaxSafeStatusNodesExaminedPerTick)
273
+ {
274
+ ExamineQueueCandidate(safeCursor.Value, controlContext, seen, snapshotItems, accumulator);
275
+ safeStatusVisits += 1;
276
+ safeCursor = safeCursor.Next;
277
+ }
278
+
279
+ state.RecordQueueNodesExamined(safeStatusVisits, controlVisits, fifoVisits);
280
+ var selection = accumulator.Complete();
281
+ foreach (var decision in selection.Rejects)
282
+ {
283
+ var item = snapshotItems[decision.QueueIndex];
284
+ if (item.QueueNode == null || item.QueueNode.List != state.WorkQueue) continue;
285
+ rejections.Add(new ControlQueueRejection(item, decision.Disposition));
286
+ if (remove) RemoveQueuedItemUnderLock(state, item);
287
+ }
288
+ foreach (var decision in selection.Starts)
289
+ {
290
+ var item = snapshotItems[decision.QueueIndex];
291
+ if (item.QueueNode == null || item.QueueNode.List != state.WorkQueue) continue;
292
+ selected.Add(item);
293
+ if (remove) RemoveQueuedItemUnderLock(state, item);
294
+ }
295
+ queueDepth = state.WorkQueue.Count;
296
+ }
297
+ return selected;
298
+ }
299
+
300
+ /// <summary>同一WorkItem跨lane只做一次policy检查,lane节点访问仍按各自预算计数。</summary>
301
+ private static void ExamineQueueCandidate(
302
+ BridgeWorkItem item,
303
+ ControlDecisionContext controlContext,
304
+ ISet<BridgeWorkItem> seen,
305
+ ICollection<BridgeWorkItem> snapshotItems,
306
+ UnityCliBridgeQueuePolicy.TickSelectionAccumulator accumulator)
307
+ {
308
+ if (!seen.Add(item)) return;
309
+ var node = item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.Control
310
+ ? item.PolicyNode.WithControlDisposition(ControlDisposition(item, controlContext))
311
+ : item.PolicyNode;
312
+ accumulator.Examine(node);
313
+ snapshotItems.Add(item);
314
+ }
315
+
316
+ internal static string[] PreviewQueuedStartRequestIdsForTests(BridgeQueueState state)
317
+ {
318
+ if (state == null || state.IsolatedExecution == null) throw new ArgumentException("Queue preview test state must be isolated.", "state");
319
+ int queueDepth;
320
+ List<ControlQueueRejection> ignored;
321
+ return SelectQueuedStarts(state, CurrentDurableMutationBarrier(state), false, out queueDepth, out ignored)
322
+ .Select(item => item.Request.RequestId).ToArray();
323
+ }
324
+
325
+ /// <summary>测试以显式barrier调用production移除算法;不读取production lease或固定状态。</summary>
326
+ internal static BridgeWorkItem[] TakeQueuedStartsForTests(BridgeQueueState state, UnityCliBridgeQueuePolicy.DurableMutationBarrier barrier)
327
+ {
328
+ if (state == null || state.IsolatedExecution == null) throw new ArgumentException("Queue take test state must be isolated.", "state");
329
+ int queueDepth;
330
+ List<ControlQueueRejection> ignored;
331
+ return SelectQueuedStarts(state, new DurableMutationBarrierProof { State = barrier }, true, out queueDepth, out ignored).ToArray();
332
+ }
333
+
334
+ internal static string[] RejectTerminalControlWorkForTests(BridgeQueueState state)
335
+ {
336
+ if (state == null || state.IsolatedExecution == null) throw new ArgumentException("Control rejection test state must be isolated.", "state");
337
+ int queueDepth;
338
+ List<ControlQueueRejection> rejections;
339
+ SelectQueuedStarts(state, CurrentDurableMutationBarrier(state), false, out queueDepth, out rejections);
340
+ var removed = new List<ControlQueueRejection>();
341
+ lock (state.QueueLock)
342
+ {
343
+ foreach (var rejection in rejections)
344
+ {
345
+ var item = rejection.Item;
346
+ if (item.QueueNode == null || item.QueueNode.List != state.WorkQueue) continue;
347
+ RemoveQueuedItemUnderLock(state, item);
348
+ removed.Add(rejection);
349
+ }
350
+ }
351
+ foreach (var rejection in removed) CompleteRejectedControlWork(rejection);
352
+ return removed.Select(value => value.Item.Request.RequestId).ToArray();
353
+ }
354
+
355
+ private static readonly string[] DurableControlRootScopes =
356
+ {
357
+ "test-run", "package-resolve", "navmesh-bake", "occlusion-bake"
358
+ };
359
+
360
+ /// <summary>固定scope的durable root与lease proof在QueueLock外一次冻结,lane访问只做纯裁决。</summary>
361
+ private static ControlDecisionContext LoadControlDecisionContext(BridgeQueueState state, DurableMutationBarrierProof barrier, bool hasControlWork)
362
+ {
363
+ var roots = new Dictionary<string, ControlRootObservation>(StringComparer.Ordinal);
364
+ if (!hasControlWork || barrier == null || barrier.State == UnityCliBridgeQueuePolicy.DurableMutationBarrier.Unreadable)
365
+ return new ControlDecisionContext(barrier, roots, false, false);
366
+ foreach (var scope in DurableControlRootScopes)
367
+ {
368
+ try { roots[scope] = new ControlRootObservation(true, state.LoadOperation(scope)); }
369
+ catch { roots[scope] = new ControlRootObservation(false, null); }
370
+ }
371
+ var leaseReadable = false;
372
+ var leaseOwned = false;
373
+ if (barrier.State == UnityCliBridgeQueuePolicy.DurableMutationBarrier.Held)
374
+ leaseReadable = state.TryIsLeaseOwned(barrier.RequestId, barrier.RequestDigest, out leaseOwned);
375
+ return new ControlDecisionContext(barrier, roots, leaseReadable, leaseOwned);
376
+ }
377
+
378
+ private static UnityCliBridgeQueuePolicy.ControlDisposition ControlDisposition(BridgeWorkItem item, ControlDecisionContext context)
379
+ {
380
+ int remainingMs;
381
+ if (item == null || item.Request == null || !item.Request.TryDeadlineRemainingMs(out remainingMs) || remainingMs <= 0)
382
+ return UnityCliBridgeQueuePolicy.ControlDisposition.ExpireDeadline;
383
+ var barrier = context == null ? null : context.Barrier;
384
+ if (barrier == null || barrier.State == UnityCliBridgeQueuePolicy.DurableMutationBarrier.Unreadable)
385
+ return UnityCliBridgeQueuePolicy.ControlDisposition.Pending;
386
+ var scope = ControlRootScope(item.Request.Command);
387
+ ControlRootObservation observation;
388
+ if (scope == null || !context.Roots.TryGetValue(scope, out observation) || !observation.Readable)
389
+ return UnityCliBridgeQueuePolicy.ControlDisposition.Pending;
390
+ if (barrier.State == UnityCliBridgeQueuePolicy.DurableMutationBarrier.Clear)
391
+ return UnityCliBridgeQueuePolicy.ControlDisposition.RejectParentUnavailable;
392
+ if (!context.LeaseReadable || !context.LeaseOwned)
393
+ return UnityCliBridgeQueuePolicy.ControlDisposition.Pending;
394
+ var root = observation.Root;
395
+ if (root == null
396
+ || !string.Equals(AdmissionText(root, "requestId"), barrier.RequestId, StringComparison.Ordinal)
397
+ || !string.Equals(AdmissionText(root, "requestDigest"), barrier.RequestDigest, StringComparison.Ordinal)
398
+ || !string.Equals(AdmissionText(root, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)
399
+ || !ControlSelectorMatchesParent(item.Request, root, barrier))
400
+ return UnityCliBridgeQueuePolicy.ControlDisposition.RejectIdentityMismatch;
401
+ return UnityCliBridgeQueuePolicy.ControlDisposition.Authorized;
402
+ }
403
+
404
+ private static bool ControlSelectorMatchesParent(UnityCli.Protocol.BridgeRequest request, IDictionary<string, object> root, DurableMutationBarrierProof barrier)
405
+ {
406
+ var scope = ControlRootScope(request.Command);
407
+ var input = request.Input as IDictionary<string, object>;
408
+ if (scope == "package-resolve")
409
+ return string.Equals(ControlInputText(input, "rootRequestId"), barrier.RequestId, StringComparison.Ordinal);
410
+ if (scope == "test-run")
411
+ {
412
+ object rawOperation;
413
+ var operation = root.TryGetValue("testOperation", out rawOperation) ? rawOperation as IDictionary<string, object> : null;
414
+ return operation != null && Guid.TryParse(AdmissionText(operation, "runId"), out _)
415
+ && string.Equals(ControlInputText(input, "expectedEditorSessionId"), AdmissionText(root, "editorSessionId"), StringComparison.Ordinal);
416
+ }
417
+ return string.Equals(ControlInputText(input, "operationId"), AdmissionText(root, "operationId"), StringComparison.Ordinal);
418
+ }
419
+
420
+ private static void CompleteRejectedControlWork(ControlQueueRejection rejection)
421
+ {
422
+ if (rejection == null || rejection.Item == null || !rejection.Item.CancelBeforeStart()) return;
423
+ var item = rejection.Item;
424
+ var identityMismatch = rejection.Disposition == UnityCliBridgeQueuePolicy.ControlDisposition.RejectIdentityMismatch;
425
+ var deadlineElapsed = rejection.Disposition == UnityCliBridgeQueuePolicy.ControlDisposition.ExpireDeadline;
426
+ item.Response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
427
+ item.Request.RequestId,
428
+ identityMismatch ? "E_ADMISSION_IDENTITY_MISMATCH" : "E_EXECUTION_TIMEOUT",
429
+ identityMismatch ? "Control request identity does not match the exact active parent."
430
+ : deadlineElapsed ? "Control request deadline elapsed before exact parent authorization."
431
+ : "Control request parent is no longer active.",
432
+ nextActions: new[] { "Inspect the exact parent status before submitting another control request." }));
433
+ item.MarkNoEffectRejected();
434
+ CompleteWorkItem(item);
435
+ }
436
+
437
+ /// <summary>一次tick在QueueLock外冻结的control durable proof。</summary>
438
+ private sealed class ControlDecisionContext
439
+ {
440
+ internal ControlDecisionContext(
441
+ DurableMutationBarrierProof barrier,
442
+ IDictionary<string, ControlRootObservation> roots,
443
+ bool leaseReadable,
444
+ bool leaseOwned)
445
+ {
446
+ Barrier = barrier;
447
+ Roots = roots;
448
+ LeaseReadable = leaseReadable;
449
+ LeaseOwned = leaseOwned;
450
+ }
451
+
452
+ internal DurableMutationBarrierProof Barrier;
453
+ internal IDictionary<string, ControlRootObservation> Roots;
454
+ internal bool LeaseReadable;
455
+ internal bool LeaseOwned;
456
+ }
457
+
458
+ private sealed class ControlRootObservation
459
+ {
460
+ internal ControlRootObservation(bool readable, Dictionary<string, object> root) { Readable = readable; Root = root; }
461
+ internal bool Readable;
462
+ internal Dictionary<string, object> Root;
463
+ }
464
+
465
+ private sealed class ControlQueueRejection
466
+ {
467
+ internal ControlQueueRejection(BridgeWorkItem item, UnityCliBridgeQueuePolicy.ControlDisposition disposition) { Item = item; Disposition = disposition; }
468
+ internal BridgeWorkItem Item;
469
+ internal UnityCliBridgeQueuePolicy.ControlDisposition Disposition;
470
+ }
471
+
472
+ private static string ControlRootScope(string command)
473
+ {
474
+ if (command == "internal.test.run.cancel.plan" || command == "internal.test.run.cancel") return "test-run";
475
+ if (command == "internal.package.files.claim" || command == "internal.package.files.commit"
476
+ || command == "internal.package.files.prove" || command == "internal.package.files.fail") return "package-resolve";
477
+ if (command == "internal.navmesh.bake.cancel") return "navmesh-bake";
478
+ if (command == "internal.occlusion.bake.cancel") return "occlusion-bake";
479
+ return null;
480
+ }
481
+
482
+ private static string ControlInputText(IDictionary<string, object> input, string key)
483
+ {
484
+ object value;
485
+ return input != null && input.TryGetValue(key, out value) ? value as string : null;
486
+ }
487
+
126
488
  private static async Task ExecuteWorkItemAsync(BridgeWorkItem item)
127
489
  {
128
490
  try
129
491
  {
130
- WriteLog("bridge_request_started requestId=" + item.Request.RequestId + " command=" + item.Request.Command);
492
+ item.QueueState.Sinks.WriteLog("bridge_request_started requestId=" + item.Request.RequestId + " command=" + item.Request.Command);
131
493
  item.Response = await ExecuteCoordinatedRequest(item.Request, item.RawRequestBytes, item.TryAccept, item.Token);
132
- WriteLog("bridge_request_finished requestId=" + item.Request.RequestId + " command=" + item.Request.Command + " ok=" + Bool(item.Response != null && item.Response.Contains("\"ok\":true")));
494
+ item.QueueState.Sinks.WriteLog("bridge_request_finished requestId=" + item.Request.RequestId + " command=" + item.Request.Command + " ok=" + Bool(item.Response != null && item.Response.Contains("\"ok\":true")));
133
495
  }
134
496
  catch (BridgeCommandException exception)
135
497
  {
@@ -157,88 +519,497 @@ namespace UnityCli
157
519
  }
158
520
  }
159
521
 
160
- /// <summary>同 root request 的并发 redelivery 只附着一个 work item;不同 bytes 立即 fail closed。</summary>
161
- private static BridgeWorkItem EnqueueOrAttach(UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string admissionListenerId, long admissionGeneration, AdmissionOwnerIdentity admissionOwner, out bool created, out int queueDepth)
522
+ /// <summary>immutable输入在锁外准备;existing attach与首次consume/publication共用QueueLock→pool线性化边界。</summary>
523
+ private static BridgeWorkItem EnqueueOrAttach(HttpListenerRequest httpRequest, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string admissionListenerId, long admissionGeneration, out bool created, out int queueDepth)
524
+ {
525
+ var reservation = PrepareAdmissionForQueue(httpRequest, request, rawRequestBytes);
526
+ return EnqueuePrepared(ProductionQueueState, request, rawRequestBytes, admissionListenerId, admissionGeneration, reservation, out created, out queueDepth);
527
+ }
528
+
529
+ internal static BridgeWorkItem EnqueuePreparedForTests(UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, AdmissionQueueReservation reservation, BridgeQueueState state, out bool created, out int queueDepth)
530
+ {
531
+ if (state == null) throw new ArgumentNullException("state");
532
+ return EnqueuePrepared(state, request, rawRequestBytes, state.ListenerId ?? ListenerId, state.ListenerGeneration, reservation, out created, out queueDepth);
533
+ }
534
+
535
+ private static BridgeWorkItem EnqueuePrepared(BridgeQueueState state, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string admissionListenerId, long admissionGeneration, AdmissionQueueReservation reservation, out bool created, out int queueDepth)
162
536
  {
163
537
  var digest = UnityCliRequestDigest.Compute(rawRequestBytes, request.InstanceId);
164
- lock (QueueLock)
538
+ var workClass = QueueRequestWorkClass(request);
539
+ if (state == null) throw new ArgumentNullException("state");
540
+ if (reservation == null) throw new ArgumentNullException("reservation");
541
+ BridgeWorkItem item = null;
542
+ try
165
543
  {
166
- BridgeWorkItem existing;
167
- if (ActiveRequestWork.TryGetValue(request.RequestId, out existing))
544
+ HitQueueTestBoundary(state, "before-queue-lock");
545
+ lock (state.QueueLock)
546
+ {
547
+ try
548
+ {
549
+ if (!state.QueuePublicationOpen)
550
+ {
551
+ if (state.QueueSessionStopping)
552
+ throw new BridgeCommandException("E_RUNTIME_SESSION_STALE", "The Unity session is stopping and no longer accepts admission or business work.", null, new[] { "Start a new Unity session and submit a new request." });
553
+ throw new BridgeCommandException("E_UNITY_DOMAIN_RELOAD", "Bridge queue admission is closed for the current Unity lifecycle.", null, new[] { "Wait for Unity to finish reloading and retry." }, true, 1000);
554
+ }
555
+ HitQueueTestBoundary(state, "before-existing-lookup");
556
+ BridgeWorkItem existing;
557
+ if (state.ActiveRequestWork.TryGetValue(request.RequestId, out existing))
558
+ {
559
+ if (!string.Equals(existing.RequestDigest, digest, StringComparison.Ordinal))
560
+ throw new BridgeCommandException("E_REQUEST_ID_REUSED", "requestId is already bound to different request bytes.", "Generate a new requestId before retrying this command.");
561
+ if (state.HasCompensationTombstone(request.RequestId, digest))
562
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Exact pre-queue admission compensation is still pending; duplicate attachment is forbidden.");
563
+ if (!TryAuthorizeCompletionPendingAttach(state, reservation, existing))
564
+ AuthorizeAdmissionForQueue(state, reservation, request);
565
+ if (!SameAdmissionOwner(existing.AdmissionOwner, reservation.Owner))
566
+ throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "requestId is already attached to another admission root.");
567
+ MarkAdmissionQueuePublished(reservation);
568
+ created = false;
569
+ queueDepth = state.WorkQueue.Count;
570
+ return existing;
571
+ }
572
+ AuthorizeAdmissionForQueue(state, reservation, request);
573
+ if (state.HasCompensationTombstone(request.RequestId, digest))
574
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Exact pre-queue admission compensation is still pending; duplicate publication is forbidden.");
575
+ var bypass = reservation.IsBypass && reservation.Owner == null;
576
+ UnityCliBridgeQueuePolicy.BypassAdmissionDecision bypassDecision = null;
577
+ if (bypass)
578
+ {
579
+ var identities = state.ActiveRequestWork.Values.Where(active => active.AdmissionBypass)
580
+ .Select(active => new UnityCliBridgeQueuePolicy.BypassIdentity(active.Request.RequestId, active.RequestDigest)).ToArray();
581
+ bypassDecision = UnityCliBridgeQueuePolicy.DecideBypassAdmission(identities, new UnityCliBridgeQueuePolicy.BypassIdentity(request.RequestId, digest));
582
+ if (bypassDecision.Outcome == UnityCliBridgeQueuePolicy.BypassAdmissionOutcome.RejectCapacity)
583
+ throw new BridgeCommandException("E_UNITY_BUSY", "Admission-bypass Bridge capacity is full.", "Wait for an active status or package-control request to complete before retrying.");
584
+ if (bypassDecision.Outcome == UnityCliBridgeQueuePolicy.BypassAdmissionOutcome.RejectIdentityMismatch)
585
+ throw new BridgeCommandException("E_REQUEST_ID_REUSED", "Admission-bypass requestId identity is inconsistent.");
586
+ if (bypassDecision.Outcome == UnityCliBridgeQueuePolicy.BypassAdmissionOutcome.Attach)
587
+ throw new BridgeCommandException("E_REQUEST_ID_REUSED", "Admission-bypass attach target disappeared before publication.");
588
+ }
589
+
590
+ item = new BridgeWorkItem(state, request, rawRequestBytes, digest, admissionListenerId, admissionGeneration,
591
+ reservation.Owner, workClass, bypass, reservation.NewlyConsumed);
592
+ HitQueueTestBoundary(state, "before-queue-publication");
593
+ UnityCliQueuePublication.Publish(
594
+ () => state.ActiveRequestWork.Add(request.RequestId, item),
595
+ () =>
596
+ {
597
+ item.QueueNode = state.WorkQueue.AddLast(item);
598
+ try
599
+ {
600
+ if (item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.SafeStatus)
601
+ item.SafeStatusNode = state.SafeStatusQueue.AddLast(item);
602
+ if (item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.Control)
603
+ {
604
+ item.ControlNode = state.ControlQueue.AddLast(item);
605
+ if (state.ControlScanCursor == null) state.ControlScanCursor = item.ControlNode;
606
+ }
607
+ }
608
+ catch
609
+ {
610
+ RemoveQueuedItemUnderLock(state, item);
611
+ throw;
612
+ }
613
+ },
614
+ () => { BridgeWorkItem current; if (state.ActiveRequestWork.TryGetValue(request.RequestId, out current) && object.ReferenceEquals(current, item)) state.ActiveRequestWork.Remove(request.RequestId); },
615
+ () => RemoveQueuedItemUnderLock(state, item),
616
+ () => MarkAdmissionQueuePublished(reservation));
617
+ created = true;
618
+ queueDepth = state.WorkQueue.Count;
619
+ return item;
620
+ }
621
+ catch
622
+ {
623
+ if (reservation.NewlyConsumed && reservation.Owner != null && !reservation.QueuePublished)
624
+ {
625
+ try { state.TrackCompensationContinuation(reservation.Owner); }
626
+ catch
627
+ {
628
+ state.QueuePublicationOpen = false;
629
+ throw;
630
+ }
631
+ HitQueueTestBoundary(state, "after-publication-compensation-tombstone");
632
+ }
633
+ throw;
634
+ }
635
+ }
636
+ }
637
+ catch
638
+ {
639
+ if (item != null && !reservation.QueuePublished) item.DisposeUnpublished();
640
+ var compensationRequired = reservation.NewlyConsumed && reservation.Owner != null && !reservation.QueuePublished;
641
+ try
642
+ {
643
+ if (compensationRequired && !PersistAdmissionCompletionIntent(state, reservation.Owner))
644
+ throw new IOException("Admission compensation intent could not be durably recorded before pre-queue cleanup.");
645
+ var compensated = CompensateAdmissionBeforeQueuePublication(state, reservation);
646
+ if (!compensated)
647
+ throw new IOException("Admission compensation could not prove exact pre-queue cleanup.");
648
+ if (compensationRequired)
649
+ {
650
+ HitQueueTestBoundary(state, "before-admission-compensation-persist");
651
+ if (!PersistAdmissionPoolState(state)) throw new IOException("Admission pool compensation persistence was not confirmed.");
652
+ if (!ClearAdmissionCompletionIntent(state, reservation.Owner)) throw new IOException("Admission pool compensation intent cleanup was not confirmed.");
653
+ HitQueueTestBoundary(state, "before-admission-compensation-log");
654
+ state.Sinks.WriteLog("admission_prequeue_compensated requestId=" + reservation.Owner.RequestId + " active=" + state.AdmissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
655
+ }
656
+ }
657
+ catch (Exception exception)
168
658
  {
169
- if (!string.Equals(existing.RequestDigest, digest, StringComparison.Ordinal))
170
- throw new BridgeCommandException("E_REQUEST_ID_REUSED", "requestId is already bound to different request bytes.", "Generate a new requestId before retrying this command.");
171
- if (!SameAdmissionOwner(existing.AdmissionOwner, admissionOwner))
172
- throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "requestId is already attached to another admission root.");
173
- created = false;
174
- queueDepth = WorkQueue.Count;
175
- return existing;
659
+ if (compensationRequired && state.DeferTrackedCompletionIntent(reservation.Owner, state.NowMs()))
660
+ state.Sinks.WriteLog("admission_prequeue_compensation_failed requestId=" + reservation.Owner.RequestId + " " + exception.GetType().Name);
661
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission compensation could not be durably recorded after queue publication failed.");
176
662
  }
177
- var item = new BridgeWorkItem(request, rawRequestBytes, digest, admissionListenerId, admissionGeneration, admissionOwner);
178
- ActiveRequestWork.Add(request.RequestId, item);
179
- WorkQueue.Enqueue(item);
180
- created = true;
181
- queueDepth = WorkQueue.Count;
182
- return item;
663
+ throw;
183
664
  }
184
665
  }
185
666
 
186
667
  private static bool SameAdmissionOwner(AdmissionOwnerIdentity left, AdmissionOwnerIdentity right)
187
668
  {
188
669
  if (left == null || right == null) return left == null && right == null;
189
- return string.Equals(left.RequestId, right.RequestId, StringComparison.Ordinal)
190
- && string.Equals(left.RootFingerprint, right.RootFingerprint, StringComparison.Ordinal)
191
- && string.Equals(left.Token, right.Token, StringComparison.Ordinal)
192
- && string.Equals(left.SessionId, right.SessionId, StringComparison.Ordinal);
670
+ return SameAdmissionImmutableIdentity(left, right);
193
671
  }
194
672
 
195
- /// <summary>完成信号与 active-request map 在同一锁内更新,attach 不会观察到半完成 item。</summary>
196
- private static void CompleteWorkItem(BridgeWorkItem item)
673
+ /// <summary>completion只作为production算法入口;WorkItem携带其所属state,测试不替换production state。</summary>
674
+ internal static bool CompleteWorkItemForTests(BridgeWorkItem item, string response)
197
675
  {
198
- UnityCliTransactionLog.Append(item.Request.RequestId, item.Request.Command, Encoding.UTF8.GetString(item.RawRequestBytes), item.Response ?? UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
199
- item.Request.RequestId,
200
- "E_BRIDGE_COMMAND_FAILED",
201
- "Bridge request completed without a response.")));
202
- var rootAdmission = item.AdmissionOwner != null && string.Equals(item.AdmissionOwner.RequestId, item.Request.RequestId, StringComparison.Ordinal);
203
- var coordinatorAccepted = rootAdmission && item.HasStarted && AdmissionCoordinatorOwns(item.AdmissionOwner, item.Request.Command, item.Response);
204
- var workItemTerminal = rootAdmission && item.HasStarted && !coordinatorAccepted && AdmissionResponseHasTrustedTerminal(item.Response);
205
- if (rootAdmission && (!item.HasStarted || workItemTerminal)) CompleteAdmissionRoot(item.AdmissionOwner);
206
- lock (QueueLock)
676
+ if (item == null || item.QueueState == null || item.QueueState.IsolatedExecution == null)
677
+ throw new ArgumentException("WorkItem completion test state must be isolated.", "item");
678
+ if (item.Completed.IsSet) return false;
679
+ lock (item.QueueState.QueueLock)
680
+ {
681
+ if (item.QueueNode != null && item.QueueNode.List == item.QueueState.WorkQueue)
682
+ RemoveQueuedItemUnderLock(item.QueueState, item);
683
+ item.QueueState.RunningWork.Add(item);
684
+ }
685
+ item.SetResponseForTests(response);
686
+ return CompleteWorkItem(item);
687
+ }
688
+
689
+ /// <summary>按单调时钟和固定退避重试settlement;一个tick不超过准入容量。</summary>
690
+ private static void RetryPendingWorkItemSettlements()
691
+ {
692
+ RetryPendingWorkItemSettlements(ProductionQueueState);
693
+ }
694
+
695
+ internal static void RetryPendingWorkItemSettlementsForTests(BridgeQueueState state)
696
+ {
697
+ RetryPendingWorkItemSettlements(state);
698
+ }
699
+
700
+ private static void RetryPendingWorkItemSettlements(BridgeQueueState state)
701
+ {
702
+ if (state == null) return;
703
+ var now = state.NowMs();
704
+ var limit = UnityCliBridgeQueuePolicy.MaxStartsPerTick;
705
+ if (state.AdmissionPool != null)
706
+ {
707
+ var snapshot = state.AdmissionPool.Capture();
708
+ limit = Math.Max(1, Math.Min(limit, snapshot.ConfiguredMaxActive));
709
+ }
710
+ BridgeWorkItem[] pending;
711
+ lock (state.QueueLock)
712
+ {
713
+ pending = state.PendingSettlementWork.Where(item => item.NextSettlementRetryAtMs <= now)
714
+ .OrderBy(item => item.NextSettlementRetryAtMs).ThenBy(item => item.Request.RequestId, StringComparer.Ordinal)
715
+ .Take(limit).ToArray();
716
+ }
717
+ foreach (var item in pending) CompleteWorkItem(item);
718
+ RetryPendingAdmissionCompletionIntents(state);
719
+ }
720
+
721
+ internal static int ActiveRequestWorkCountForTests(BridgeQueueState state)
722
+ {
723
+ if (state == null) throw new ArgumentNullException("state");
724
+ lock (state.QueueLock) return state.ActiveRequestWork.Count;
725
+ }
726
+
727
+ internal static bool QueueLockHeldForTests(BridgeQueueState state)
728
+ {
729
+ return state != null && Monitor.IsEntered(state.QueueLock);
730
+ }
731
+
732
+ private static bool CompleteWorkItem(BridgeWorkItem item)
733
+ {
734
+ if (item == null) return false;
735
+ var state = item.QueueState;
736
+ if (!item.TryBeginCompletion()) return false;
737
+ if (item.NoEffectRejected && !item.NoEffectRejectionOwnsAdmission)
738
+ {
739
+ lock (state.QueueLock)
740
+ {
741
+ state.RunningWork.Remove(item);
742
+ state.PendingSettlementWork.Remove(item);
743
+ RemoveActiveWorkItem(state, item);
744
+ }
745
+ CompleteAndLogWorkItem(item);
746
+ TryFinalizeAdmissionPoolShutdown(state);
747
+ return true;
748
+ }
749
+
750
+ var transfer = AdmissionTransferResult.TransferPending;
751
+ Exception settlementException = null;
752
+ Exception boundaryException = null;
753
+ try
754
+ {
755
+ // selection 已证明该 WorkItem 未启动时,禁止按命令名推断 durable coordinator 接管。
756
+ if (item.CompletionIntentReady || item.NoEffectRejected)
757
+ transfer = AdmissionTransferResult.NotAccepted;
758
+ else
759
+ transfer = SettleAdmissionAfterWorkItem(state, item);
760
+ }
761
+ catch (Exception exception) { settlementException = exception; }
762
+
763
+ if (transfer == AdmissionTransferResult.TransferPending)
764
+ {
765
+ HoldPendingSettlement(state, item, settlementException == null ? "recovery-marker-pending" : settlementException.GetType().Name);
766
+ return false;
767
+ }
768
+
769
+ // 已被Coordinator/Continuation/Recovery/TerminalCleanup durable接管的owner不再由WorkItem完成。
770
+ if (item.AdmissionOwner == null || transfer == AdmissionTransferResult.Transferred)
771
+ {
772
+ lock (state.QueueLock)
773
+ {
774
+ state.RunningWork.Remove(item);
775
+ state.PendingSettlementWork.Remove(item);
776
+ RemoveActiveWorkItem(state, item);
777
+ }
778
+ CompleteAndLogWorkItem(item);
779
+ TryFinalizeAdmissionPoolShutdown(state);
780
+ return true;
781
+ }
782
+
783
+ // 仍由WorkItem负责时,先在QueueLock外落completion intent,再锁内删除pool内存。
784
+ UnityCliAdmissionPool.ConsumedIdentity current;
785
+ AdmissionOwnerIdentity frozen;
786
+ try
787
+ {
788
+ if (item.CompletionIntentReady) frozen = item.AdmissionOwner;
789
+ else
790
+ {
791
+ frozen = CaptureAdmissionOwner(state, item.AdmissionOwner.RequestId, item.AdmissionOwner.RequestDigest);
792
+ if (!SameAdmissionOwner(frozen, item.AdmissionOwner)
793
+ || frozen.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem)
794
+ {
795
+ HoldPendingSettlement(state, item, "completion-owner-changed");
796
+ return false;
797
+ }
798
+ if (!HasAdmissionCompletionIntent(state, frozen)
799
+ && !PersistAdmissionCompletionIntent(state, frozen))
800
+ {
801
+ HoldPendingSettlement(state, item, "completion-intent-pending");
802
+ return false;
803
+ }
804
+ item.CompletionIntentReady = true;
805
+ }
806
+ }
807
+ catch (Exception exception)
808
+ {
809
+ HoldPendingSettlement(state, item, exception.GetType().Name);
810
+ return false;
811
+ }
812
+
813
+ UnityCliAdmissionPool.CompletionOutcome completionOutcome;
814
+ try
815
+ {
816
+ lock (state.QueueLock)
817
+ {
818
+ try { HitQueueTestBoundary(state, "before-completion-memory"); }
819
+ catch (Exception exception) { boundaryException = exception; }
820
+ if (boundaryException != null)
821
+ {
822
+ completionOutcome = UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
823
+ }
824
+ else
825
+ {
826
+ var pool = state.AdmissionPool;
827
+ if (pool == null)
828
+ completionOutcome = UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
829
+ else if (!pool.TryGetConsumedIdentity(item.AdmissionOwner.RequestId, out current))
830
+ completionOutcome = item.CompletionIntentReady
831
+ ? UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent
832
+ : UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
833
+ else
834
+ {
835
+ frozen = ToAdmissionOwner(current, item.AdmissionOwner.RequestDigest);
836
+ completionOutcome = SameAdmissionOwner(frozen, item.AdmissionOwner)
837
+ ? pool.CompleteTrusted(ToPoolIdentity(frozen))
838
+ : UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
839
+ }
840
+ }
841
+ state.RunningWork.Remove(item);
842
+ state.PendingSettlementWork.Add(item);
843
+ }
844
+ }
845
+ catch (Exception exception)
846
+ {
847
+ if (state.AdmissionPool != null) state.AdmissionPool.BlockRecovery();
848
+ HoldPendingSettlement(state, item, exception.GetType().Name);
849
+ return false;
850
+ }
851
+
852
+ if (boundaryException != null)
853
+ {
854
+ if (!item.SettlementFailureLogged)
855
+ {
856
+ try { HitQueueTestBoundary(state, "before-queue-boundary-fault-log"); } catch { }
857
+ state.Sinks.WriteLog("queue_test_boundary_failed " + boundaryException.GetType().Name);
858
+ }
859
+ HoldPendingSettlement(state, item, boundaryException.GetType().Name);
860
+ return false;
861
+ }
862
+ if (completionOutcome == UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked)
863
+ {
864
+ HoldPendingSettlement(state, item, "completion-identity-blocked");
865
+ return false;
866
+ }
867
+
868
+ try
869
+ {
870
+ HitQueueTestBoundary(state, "before-admission-persist");
871
+ if (!state.PersistPool()) throw new IOException("Admission pool persistence was not confirmed.");
872
+ if (!state.ClearMarkers(item.AdmissionOwner)) throw new IOException("Admission owner marker cleanup was not confirmed.");
873
+ if (!ClearAdmissionCompletionIntent(state, item.AdmissionOwner)) throw new IOException("Admission completion intent cleanup was not confirmed.");
874
+ }
875
+ catch (Exception exception)
876
+ {
877
+ HoldPendingSettlement(state, item, exception.GetType().Name);
878
+ return false;
879
+ }
880
+
881
+ lock (state.QueueLock)
882
+ {
883
+ state.PendingSettlementWork.Remove(item);
884
+ RemoveActiveWorkItem(state, item);
885
+ }
886
+ CompleteAndLogWorkItem(item);
887
+ TryFinalizeAdmissionPoolShutdown(state);
888
+ return true;
889
+ }
890
+
891
+ private static void HoldPendingSettlement(BridgeQueueState state, BridgeWorkItem item, string reason)
892
+ {
893
+ lock (state.QueueLock)
894
+ {
895
+ state.RunningWork.Remove(item);
896
+ state.PendingSettlementWork.Add(item);
897
+ }
898
+ item.ScheduleSettlementRetry(state.NowMs());
899
+ item.AllowCompletionRetry();
900
+ if (item.SettlementFailureLogged) return;
901
+ item.SettlementFailureLogged = true;
902
+ state.Sinks.WriteLog("admission_settlement_deferred requestId=" + item.Request.RequestId + " reason=" + reason);
903
+ }
904
+
905
+ /// <summary>QueueLock内同时摘除主FIFO与两个独立索引,任何入口都不得留下失效节点。</summary>
906
+ private static void RemoveQueuedItemUnderLock(BridgeQueueState state, BridgeWorkItem item)
907
+ {
908
+ if (item.QueueNode != null && item.QueueNode.List == state.WorkQueue)
909
+ state.WorkQueue.Remove(item.QueueNode);
910
+ if (item.SafeStatusNode != null && item.SafeStatusNode.List == state.SafeStatusQueue)
911
+ state.SafeStatusQueue.Remove(item.SafeStatusNode);
912
+ if (item.ControlNode != null && item.ControlNode.List == state.ControlQueue)
913
+ {
914
+ if (object.ReferenceEquals(state.ControlScanCursor, item.ControlNode))
915
+ state.ControlScanCursor = item.ControlNode.Next ?? state.ControlQueue.First;
916
+ state.ControlQueue.Remove(item.ControlNode);
917
+ if (state.ControlQueue.Count == 0) state.ControlScanCursor = null;
918
+ }
919
+ item.QueueNode = null;
920
+ item.SafeStatusNode = null;
921
+ item.ControlNode = null;
922
+ }
923
+
924
+ private static void RemoveActiveWorkItem(BridgeQueueState state, BridgeWorkItem item)
925
+ {
926
+ BridgeWorkItem current;
927
+ if (state.ActiveRequestWork.TryGetValue(item.Request.RequestId, out current) && object.ReferenceEquals(current, item))
928
+ state.ActiveRequestWork.Remove(item.Request.RequestId);
929
+ }
930
+
931
+ private static string CompletionIntentKey(AdmissionOwnerIdentity identity)
932
+ {
933
+ return identity == null ? string.Empty : identity.RequestId + "\0" + identity.RequestDigest + "\0" + (identity.TopRequestDigest ?? string.Empty)
934
+ + "\0" + identity.RootFingerprint + "\0" + identity.Token + "\0" + identity.SessionId
935
+ + "\0" + identity.ListenerId + "\0" + identity.ListenerGeneration.ToString(CultureInfo.InvariantCulture)
936
+ + "\0" + identity.ExecutionDeadlineMs.ToString(CultureInfo.InvariantCulture) + "\0" + ((int)identity.OwnerState).ToString(CultureInfo.InvariantCulture);
937
+ }
938
+
939
+ private static void CompleteAndLogWorkItem(BridgeWorkItem item)
940
+ {
941
+ item.Completed.Set();
942
+ try
207
943
  {
208
- RunningWork.Remove(item);
209
- BridgeWorkItem current;
210
- if (ActiveRequestWork.TryGetValue(item.Request.RequestId, out current) && object.ReferenceEquals(current, item))
211
- ActiveRequestWork.Remove(item.Request.RequestId);
212
- item.Completed.Set();
944
+ item.QueueState.Sinks.AppendTransaction(item.Request.RequestId, item.Request.Command, Encoding.UTF8.GetString(item.RawRequestBytes), item.Response ?? UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
945
+ item.Request.RequestId, "E_BRIDGE_COMMAND_FAILED", "Bridge request completed without a response.")));
213
946
  }
947
+ catch (Exception exception) { item.QueueState.Sinks.WriteLog("transaction_log_append_failed requestId=" + item.Request.RequestId + " " + exception.GetType().Name); }
214
948
  }
215
949
 
216
- private sealed class BridgeWorkItem
950
+ internal sealed class BridgeWorkItem
217
951
  {
218
952
  private readonly object StateLock = new object();
219
953
  private bool cancelled;
220
954
  private bool started;
221
- private bool accepted;
222
- private bool acceptanceCancelled;
955
+ private bool completionStarted;
956
+ private CancelRecoveryCandidate cancelRecoveryCandidate;
957
+ private readonly UnityCliBridgeWorkItemState lifecycle = new UnityCliBridgeWorkItemState();
223
958
  private readonly CancellationTokenSource cancellation;
224
959
 
225
- public BridgeWorkItem(UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string requestDigest, string admissionListenerId, long admissionGeneration, AdmissionOwnerIdentity admissionOwner)
960
+ public BridgeWorkItem(BridgeQueueState queueState, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string requestDigest, string admissionListenerId, long admissionGeneration, AdmissionOwnerIdentity admissionOwner, UnityCliBridgeQueuePolicy.WorkClass workClass, bool admissionBypass, bool admissionNewlyConsumed)
226
961
  {
962
+ QueueState = queueState;
227
963
  Request = request;
228
964
  RawRequestBytes = rawRequestBytes;
229
965
  RequestDigest = requestDigest;
230
966
  AdmissionListenerId = admissionListenerId;
231
967
  AdmissionGeneration = admissionGeneration;
232
968
  AdmissionOwner = admissionOwner;
969
+ CapturedAdmissionPool = queueState == null ? null : queueState.AdmissionPool;
970
+ WorkClass = workClass;
971
+ AdmissionBypass = admissionBypass;
972
+ AdmissionNewlyConsumed = admissionNewlyConsumed;
973
+ AdmissionOwnerStateAtPublication = admissionOwner == null
974
+ ? UnityCliAdmissionPool.AdmissionOwnerState.Granted
975
+ : admissionOwner.OwnerState;
976
+ PolicyNode = new UnityCliBridgeQueuePolicy.QueueNode(request.RequestId, request.Command, workClass);
233
977
  cancellation = CreateExecutionLifetime(request);
234
978
  }
235
979
 
980
+ public BridgeQueueState QueueState { get; private set; }
236
981
  public UnityCli.Protocol.BridgeRequest Request { get; private set; }
237
982
  public byte[] RawRequestBytes { get; private set; }
238
983
  public string RequestDigest { get; private set; }
239
984
  public string AdmissionListenerId { get; private set; }
240
985
  public long AdmissionGeneration { get; private set; }
241
986
  public AdmissionOwnerIdentity AdmissionOwner { get; private set; }
987
+ internal UnityCliAdmissionPool CapturedAdmissionPool { get; private set; }
988
+ public UnityCliBridgeQueuePolicy.WorkClass WorkClass { get; private set; }
989
+ public UnityCliBridgeQueuePolicy.QueueNode PolicyNode { get; private set; }
990
+ public bool AdmissionBypass { get; private set; }
991
+ internal bool AdmissionNewlyConsumed { get; private set; }
992
+ internal UnityCliAdmissionPool.AdmissionOwnerState AdmissionOwnerStateAtPublication { get; private set; }
993
+ internal bool NoEffectRejectionOwnsAdmission
994
+ {
995
+ get
996
+ {
997
+ return AdmissionOwner != null && AdmissionNewlyConsumed
998
+ && AdmissionOwnerStateAtPublication == UnityCliAdmissionPool.AdmissionOwnerState.WorkItem;
999
+ }
1000
+ }
1001
+ internal bool AttachedToExistingDurableOwner
1002
+ {
1003
+ get
1004
+ {
1005
+ return AdmissionOwner != null && !AdmissionNewlyConsumed
1006
+ && AdmissionOwnerStateAtPublication != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem
1007
+ && AdmissionOwnerStateAtPublication != UnityCliAdmissionPool.AdmissionOwnerState.Granted;
1008
+ }
1009
+ }
1010
+ public LinkedListNode<BridgeWorkItem> QueueNode { get; set; }
1011
+ internal LinkedListNode<BridgeWorkItem> SafeStatusNode { get; set; }
1012
+ internal LinkedListNode<BridgeWorkItem> ControlNode { get; set; }
242
1013
  public bool HasStarted { get { lock (StateLock) return started; } }
243
1014
  public CancellationToken Token { get { return cancellation.Token; } }
244
1015
  public ManualResetEventSlim Started { get; private set; } = new ManualResetEventSlim(false);
@@ -249,38 +1020,129 @@ namespace UnityCli
249
1020
  get { lock (StateLock) return response; }
250
1021
  set { lock (StateLock) response = value; }
251
1022
  }
252
- public bool TimedOut { get; private set; }
253
- public bool IsMutation { get; set; }
1023
+ internal long NextSettlementRetryAtMs { get; private set; }
1024
+ internal bool SettlementFailureLogged { get; set; }
1025
+ internal bool CompletionIntentReady { get; set; }
1026
+ internal bool NoEffectRejected { get; private set; }
254
1027
 
255
- public bool TryStart()
1028
+ internal string RequestDigestValue { get { return RequestDigest; } }
1029
+ internal void SetResponseForTests(string response) { Response = response; }
1030
+
1031
+ /// <summary>cancel terminal首次持久化双故障时,把有界候选冻结在仍存活的WorkItem供settlement重建marker。</summary>
1032
+ internal void FreezeCancelRecoveryCandidate(string rootRequestId, string rootRequestDigest, string runId, string effectState, object envelope)
256
1033
  {
1034
+ var serialized = UnityCliSerializer.Serialize(envelope);
1035
+ if (string.IsNullOrEmpty(rootRequestId) || string.IsNullOrEmpty(rootRequestDigest) || string.IsNullOrEmpty(runId)
1036
+ || string.IsNullOrEmpty(effectState) || envelope == null || Encoding.UTF8.GetByteCount(serialized) > MaxCancelRecoveryEnvelopeBytes)
1037
+ throw new InvalidOperationException("Cancel recovery candidate is incomplete or exceeds its fixed bound.");
257
1038
  lock (StateLock)
258
1039
  {
259
- if (cancelled)
1040
+ if (cancelRecoveryCandidate != null)
260
1041
  {
261
- return false;
1042
+ if (!string.Equals(cancelRecoveryCandidate.RootRequestId, rootRequestId, StringComparison.Ordinal)
1043
+ || !string.Equals(cancelRecoveryCandidate.RootRequestDigest, rootRequestDigest, StringComparison.Ordinal)
1044
+ || !string.Equals(cancelRecoveryCandidate.RunId, runId, StringComparison.Ordinal)
1045
+ || !string.Equals(cancelRecoveryCandidate.EffectState, effectState, StringComparison.Ordinal)
1046
+ || !string.Equals(UnityCliSerializer.Serialize(cancelRecoveryCandidate.Envelope), serialized, StringComparison.Ordinal))
1047
+ throw new InvalidOperationException("Cancel recovery candidate identity changed after it was frozen.");
1048
+ return;
262
1049
  }
1050
+ cancelRecoveryCandidate = new CancelRecoveryCandidate
1051
+ {
1052
+ RootRequestId = rootRequestId,
1053
+ RootRequestDigest = rootRequestDigest,
1054
+ RunId = runId,
1055
+ EffectState = effectState,
1056
+ Envelope = UnityCliSerializer.Parse(serialized)
1057
+ };
1058
+ }
1059
+ }
1060
+
1061
+ internal CancelRecoveryCandidate CaptureCancelRecoveryCandidate()
1062
+ {
1063
+ lock (StateLock)
1064
+ {
1065
+ if (cancelRecoveryCandidate == null) return null;
1066
+ return new CancelRecoveryCandidate
1067
+ {
1068
+ RootRequestId = cancelRecoveryCandidate.RootRequestId,
1069
+ RootRequestDigest = cancelRecoveryCandidate.RootRequestDigest,
1070
+ RunId = cancelRecoveryCandidate.RunId,
1071
+ EffectState = cancelRecoveryCandidate.EffectState,
1072
+ Envelope = UnityCliSerializer.Parse(UnityCliSerializer.Serialize(cancelRecoveryCandidate.Envelope))
1073
+ };
1074
+ }
1075
+ }
1076
+
1077
+ internal sealed class CancelRecoveryCandidate
1078
+ {
1079
+ internal string RootRequestId;
1080
+ internal string RootRequestDigest;
1081
+ internal string RunId;
1082
+ internal string EffectState;
1083
+ internal object Envelope;
1084
+ }
1085
+
1086
+ /// <summary>只允许已取消且从未启动的 selection reject 冻结 no-effect settlement。</summary>
1087
+ internal void MarkNoEffectRejected()
1088
+ {
1089
+ lock (StateLock)
1090
+ {
1091
+ if (started || !cancelled) throw new InvalidOperationException("No-effect rejection requires a cancelled, unstarted WorkItem.");
1092
+ NoEffectRejected = true;
1093
+ }
1094
+ }
1095
+
1096
+ internal void ScheduleSettlementRetry(long nowMs)
1097
+ {
1098
+ NextSettlementRetryAtMs = checked(nowMs + 100L);
1099
+ }
1100
+
1101
+ internal bool TryBeginCompletion()
1102
+ {
1103
+ lock (StateLock)
1104
+ {
1105
+ if (completionStarted) return false;
1106
+ completionStarted = true;
1107
+ return true;
1108
+ }
1109
+ }
1110
+
1111
+ internal void AllowCompletionRetry()
1112
+ {
1113
+ lock (StateLock) completionStarted = false;
1114
+ }
1115
+
1116
+ public bool TryStart()
1117
+ {
1118
+ lock (StateLock)
1119
+ {
263
1120
  // admission与主线程开始必须属于同一listener epoch;reload后的旧队列项不得执行。
264
- if (!string.Equals(AdmissionListenerId, ListenerId, StringComparison.Ordinal)
265
- || AdmissionGeneration != CurrentListenerGeneration())
1121
+ int remainingMs = 0;
1122
+ if (Request != null) Request.TryDeadlineRemainingMs(out remainingMs);
1123
+ var outcome = lifecycle.TryStart(
1124
+ QueueState != null && string.Equals(AdmissionListenerId, QueueState.ListenerId, StringComparison.Ordinal) && AdmissionGeneration == QueueState.ListenerGeneration,
1125
+ remainingMs);
1126
+ if (outcome != UnityCliBridgeWorkItemState.StartOutcome.Started)
266
1127
  {
267
- cancelled = true;
1128
+ if (outcome == UnityCliBridgeWorkItemState.StartOutcome.DeadlineElapsed) cancelled = true;
1129
+ if (outcome == UnityCliBridgeWorkItemState.StartOutcome.Cancelled) cancelled = true;
1130
+ if (outcome == UnityCliBridgeWorkItemState.StartOutcome.ListenerChanged)
1131
+ {
1132
+ cancelled = true;
1133
+ response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
1134
+ Request.RequestId,
1135
+ "E_UNITY_DOMAIN_RELOAD",
1136
+ "Unity listener generation changed before the queued request started.",
1137
+ nextActions: new[] { "Keep the same root requestId and immutable request bytes while the session supervisor rebinds." },
1138
+ transient: true,
1139
+ retryAfterMs: 1000));
1140
+ }
268
1141
  cancellation.Cancel();
269
- response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
270
- Request.RequestId,
271
- "E_UNITY_DOMAIN_RELOAD",
272
- "Unity listener generation changed before the queued request started.",
273
- nextActions: new[] { "Keep the same root requestId and immutable request bytes while the session supervisor rebinds." },
274
- transient: true,
275
- retryAfterMs: 1000));
276
1142
  return false;
277
1143
  }
1144
+ cancellation.CancelAfter(TimeSpan.FromMilliseconds(remainingMs));
278
1145
  started = true;
279
- int remainingMs;
280
- if (Request != null && Request.TryDeadlineRemainingMs(out remainingMs))
281
- cancellation.CancelAfter(TimeSpan.FromMilliseconds(remainingMs));
282
- else
283
- cancellation.Cancel();
284
1146
  Started.Set();
285
1147
  return true;
286
1148
  }
@@ -290,7 +1152,7 @@ namespace UnityCli
290
1152
  {
291
1153
  lock (StateLock)
292
1154
  {
293
- if (started)
1155
+ if (started || !lifecycle.CancelBeforeStart())
294
1156
  {
295
1157
  return false;
296
1158
  }
@@ -300,35 +1162,23 @@ namespace UnityCli
300
1162
  }
301
1163
  }
302
1164
 
303
- /// <summary>StateLock 使接受与 HTTP 超时取消使用同一内存裁决顺序。</summary>
1165
+ /// <summary>StateLock使HTTP接受与主线程启动使用同一内存裁决顺序。</summary>
304
1166
  public bool TryAccept()
305
1167
  {
306
- lock (StateLock)
307
- {
308
- if (acceptanceCancelled) return false;
309
- accepted = true;
310
- return true;
311
- }
1168
+ lock (StateLock) return lifecycle.TryAccept();
312
1169
  }
313
1170
 
314
- /// <summary>HTTP timeout 仅能取消尚未被 coordinator 接受的 mutation。</summary>
315
- public bool CancelBeforeAcceptance()
1171
+ public void DisposeUnpublished()
316
1172
  {
317
1173
  lock (StateLock)
318
1174
  {
319
- if (accepted) return false;
320
- acceptanceCancelled = true;
321
- cancellation.Cancel();
322
- return true;
323
- }
324
- }
325
-
326
- public void Cancel()
327
- {
328
- lock (StateLock)
329
- {
330
- TimedOut = true;
1175
+ cancelled = true;
1176
+ try { cancellation.Cancel(); } catch { }
1177
+ Completed.Set();
331
1178
  }
1179
+ cancellation.Dispose();
1180
+ Started.Dispose();
1181
+ Completed.Dispose();
332
1182
  }
333
1183
 
334
1184
  }