@alpsckr/unitycli 0.4.2 → 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 +35 -42
  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
@@ -3,8 +3,10 @@ using System;
3
3
  using System.Collections;
4
4
  using System.Collections.Generic;
5
5
  using System.Globalization;
6
+ using System.IO;
6
7
  using System.Linq;
7
8
  using System.Net;
9
+ using System.Text;
8
10
  using UnityEditor;
9
11
 
10
12
  namespace UnityCli
@@ -12,294 +14,1767 @@ namespace UnityCli
12
14
  public static partial class UnityCliBridge
13
15
  {
14
16
  private const string AdmissionSessionStateKey = "UnityCli.AdmissionPool.State";
15
- private static UnityCliAdmissionPool admissionPool;
17
+ private const string AdmissionSettlementScope = "admission-settlement";
18
+ private const int AdmissionPublicationMutexTimeoutMs = 5000;
19
+ private const int MaxCancelRecoveryEnvelopeBytes = 1536 * 1024;
20
+ private static long admissionPoolPublicationRevision;
21
+ private static string admissionPoolPublicationObserved = string.Empty;
22
+ private static readonly string[] AdmissionDurableScopes = { "compile", "package-resolve", "refresh", "asset-refresh", "package-embed", "test-run", "test-cancel", "build", "navmesh-bake", "occlusion-bake", "direct-mutation" };
23
+ private static UnityCliAdmissionPool admissionPool
24
+ {
25
+ get { return ProductionQueueState.AdmissionPool; }
26
+ set { ProductionQueueState.AdmissionPool = value; }
27
+ }
16
28
 
17
29
  internal sealed class AdmissionOwnerIdentity
18
30
  {
19
- internal string RequestId;
20
- internal string RootFingerprint;
21
- internal string Token;
22
- internal string SessionId;
23
- internal UnityCliAdmissionPool.AdmissionOwnerKind OwnerKind;
31
+ internal string RequestId;
32
+ internal string RequestDigest;
33
+ internal string TopRequestDigest;
34
+ internal string RootFingerprint;
35
+ internal string Token;
36
+ internal string SessionId;
37
+ internal string ListenerId;
38
+ internal long ListenerGeneration;
39
+ internal long ExecutionDeadlineMs;
40
+ internal string EditorSessionId;
41
+ internal UnityCliAdmissionPool.AdmissionOwnerState OwnerState;
42
+ }
43
+
44
+ private enum AdmissionTransferResult
45
+ {
46
+ NotAccepted,
47
+ Transferred,
48
+ TransferPending
49
+ }
50
+
51
+ /// <summary>队列publication失败时只补偿本次新消费;attach不拥有撤销权。</summary>
52
+ internal sealed class AdmissionQueueReservation
53
+ {
54
+ internal string RequestId;
55
+ internal string RequestDigest;
56
+ internal string RootFingerprint;
57
+ internal string Token;
58
+ internal long ExecutionDeadlineMs;
59
+ internal bool HasAdmissionHeaders;
60
+ internal bool HasExactTerminal;
61
+ internal bool IsBypass;
62
+ internal AdmissionOwnerIdentity Owner;
63
+ internal bool NewlyConsumed;
64
+ internal bool QueuePublished;
65
+ }
66
+
67
+ /// <summary>按当前Editor session与项目配置建立唯一准入池,并恢复同启动内的consumed roots。</summary>
68
+ private static void InitializeAdmissionPool()
69
+ {
70
+ if (admissionPool != null) return;
71
+ var settings = UnityCliProjectSettings.Current;
72
+ admissionPool = new UnityCliAdmissionPool(InstanceId(), EditorSessionId(), settings.TaskPoolMaxActive, settings.TaskPoolReopenAt);
73
+ ProductionQueueState.ListenerId = ListenerId;
74
+ ProductionQueueState.ListenerGeneration = Math.Max(1, CurrentListenerGeneration());
75
+ string serialized;
76
+ try { serialized = LoadProductionAdmissionPoolState(); }
77
+ catch (Exception exception)
78
+ {
79
+ admissionPool.BlockRecovery();
80
+ try { EraseProductionAdmissionPoolState(); } catch { }
81
+ WriteLog("admission_publication_load_failed " + exception.GetType().Name + ": " + exception.Message);
82
+ ReconcileOrphanAdmissionOwnerMarkers();
83
+ ReconcilePendingAdmissionSettlements();
84
+ return;
85
+ }
86
+ if (string.IsNullOrEmpty(serialized))
87
+ {
88
+ ReconcileOrphanAdmissionOwnerMarkers();
89
+ ReconcilePendingAdmissionSettlements();
90
+ return;
91
+ }
92
+ try
93
+ {
94
+ var state = AsDictionary(UnityCliSerializer.Parse(serialized));
95
+ if (state == null || AdmissionText(state, "editorSessionId") != EditorSessionId())
96
+ {
97
+ EraseProductionAdmissionPoolState();
98
+ ReconcileOrphanAdmissionOwnerMarkers();
99
+ ReconcilePendingAdmissionSettlements();
100
+ return;
101
+ }
102
+ var records = new List<UnityCliAdmissionPool.AdmissionRecord>();
103
+ object rawRecords;
104
+ var values = state.TryGetValue("records", out rawRecords) ? rawRecords as IEnumerable : null;
105
+ if (values == null) throw new InvalidOperationException("Admission SessionState records are missing.");
106
+ foreach (var value in values)
107
+ {
108
+ var record = AsDictionary(value);
109
+ if (record == null) throw new InvalidOperationException("Admission SessionState record is invalid.");
110
+ records.Add(new UnityCliAdmissionPool.AdmissionRecord
111
+ {
112
+ InstanceId = AdmissionText(record, "instanceId"),
113
+ SessionId = AdmissionText(record, "sessionId"),
114
+ RequestId = AdmissionText(record, "requestId"),
115
+ RootFingerprint = AdmissionText(record, "rootFingerprint"),
116
+ RequestDigest = AdmissionText(record, "requestDigest"),
117
+ Token = AdmissionText(record, "token"),
118
+ ListenerId = AdmissionText(record, "listenerId"),
119
+ ListenerGeneration = AdmissionLong(record, "listenerGeneration"),
120
+ GrantExpiresAtMs = AdmissionLong(record, "grantExpiresAtMs"),
121
+ ExecutionDeadlineMs = AdmissionLong(record, "executionDeadlineMs"),
122
+ OwnerState = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(record, "ownerState"),
123
+ IntendedOwnerState = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(record, "intendedOwnerState"),
124
+ TransferCommitted = AdmissionBool(record, "transferCommitted")
125
+ });
126
+ }
127
+ var revision = AdmissionLong(state, "publicationRevision");
128
+ if (revision < 1L || revision != admissionPoolPublicationRevision)
129
+ throw new InvalidOperationException("Admission SessionState publication revision is invalid.");
130
+ admissionPool.Restore(records, checked((int)AdmissionLong(state, "activeCount")), AdmissionBool(state, "gateClosed"), RequiredText(state, "recoveryDigest", 64), checked((int)AdmissionLong(state, "configuredMaxActive")), checked((int)AdmissionLong(state, "configuredReopenAt")));
131
+ ReconcileOrphanAdmissionOwnerMarkers();
132
+ ReconcilePendingAdmissionSettlements();
133
+ }
134
+ catch (Exception exception)
135
+ {
136
+ admissionPool.BlockRecovery();
137
+ try { EraseProductionAdmissionPoolState(); } catch { }
138
+ WriteLog("admission_restore_failed " + exception.GetType().Name + ": " + exception.Message);
139
+ ReconcileOrphanAdmissionOwnerMarkers();
140
+ ReconcilePendingAdmissionSettlements();
141
+ }
142
+ }
143
+
144
+ /// <summary>reload前撤销未消费grant,只持久化同Editor启动内的有界owner state。</summary>
145
+ private static void SaveAdmissionPoolBeforeReload()
146
+ {
147
+ if (admissionPool == null) return;
148
+ admissionPool.RevokeUnconsumed(ListenerId, CurrentListenerGeneration());
149
+ PersistAdmissionPoolState();
150
+ }
151
+
152
+ /// <summary>单个SessionState键保存最多maxActive条;新Editor启动因session不匹配整体作废。</summary>
153
+ private static bool PersistAdmissionPoolState(BridgeQueueState state = null)
154
+ {
155
+ state = state ?? ProductionQueueState;
156
+ if (state == null || state.AdmissionPool == null) return true;
157
+ if (!object.ReferenceEquals(state, ProductionQueueState)) return state.PersistPool();
158
+ return PersistProductionAdmissionPoolState();
159
+ }
160
+
161
+ private static bool PersistProductionAdmissionPoolState()
162
+ {
163
+ try
164
+ {
165
+ return WithAdmissionPublicationLock(() =>
166
+ {
167
+ if (admissionPool == null) return true;
168
+ var current = SessionState.GetString(AdmissionSessionStateKey, string.Empty);
169
+ var currentRevision = AdmissionPoolPublicationRevision(current);
170
+ if (currentRevision != admissionPoolPublicationRevision
171
+ || !string.Equals(current, admissionPoolPublicationObserved, StringComparison.Ordinal)) return false;
172
+ var snapshot = admissionPool.CaptureRecovery();
173
+ var records = snapshot.Records.Select(record => (object)new Dictionary<string, object>
174
+ {
175
+ { "instanceId", record.InstanceId }, { "sessionId", record.SessionId },
176
+ { "requestId", record.RequestId }, { "rootFingerprint", record.RootFingerprint }, { "requestDigest", record.RequestDigest }, { "token", record.Token },
177
+ { "listenerId", record.ListenerId }, { "listenerGeneration", record.ListenerGeneration },
178
+ { "grantExpiresAtMs", record.GrantExpiresAtMs }, { "executionDeadlineMs", record.ExecutionDeadlineMs },
179
+ { "ownerState", (long)record.OwnerState }, { "intendedOwnerState", (long)record.IntendedOwnerState },
180
+ { "transferCommitted", record.TransferCommitted }
181
+ }).ToList();
182
+ var nextRevision = checked(currentRevision + 1L);
183
+ var serialized = UnityCliSerializer.Serialize(new Dictionary<string, object>
184
+ {
185
+ { "editorSessionId", EditorSessionId() }, { "publicationRevision", nextRevision },
186
+ { "gateClosed", snapshot.GateClosed }, { "records", records },
187
+ { "activeCount", (long)records.Count }, { "recoveryDigest", snapshot.RecoveryDigest },
188
+ { "configuredMaxActive", (long)snapshot.ConfiguredMaxActive }, { "configuredReopenAt", (long)snapshot.ReopenAt }
189
+ });
190
+ SessionState.SetString(AdmissionSessionStateKey, serialized);
191
+ var confirmed = SessionState.GetString(AdmissionSessionStateKey, string.Empty);
192
+ if (!string.Equals(confirmed, serialized, StringComparison.Ordinal)) return false;
193
+ admissionPoolPublicationRevision = nextRevision;
194
+ admissionPoolPublicationObserved = serialized;
195
+ return true;
196
+ });
197
+ }
198
+ catch { return false; }
199
+ }
200
+
201
+ /// <summary>pool snapshot与completion journal共享同一current-user mutex;锁内不进入QueueLock。</summary>
202
+ private static T WithAdmissionPublicationLock<T>(Func<T> action)
203
+ {
204
+ if (action == null) throw new ArgumentNullException("action");
205
+ T result = default(T);
206
+ WithCurrentUserMutex("Local\\UnityCli.AdmissionPublication." + InstanceId(), AdmissionPublicationMutexTimeoutMs, () => { result = action(); });
207
+ return result;
208
+ }
209
+
210
+ private static string LoadProductionAdmissionPoolState()
211
+ {
212
+ return WithAdmissionPublicationLock(() =>
213
+ {
214
+ var serialized = SessionState.GetString(AdmissionSessionStateKey, string.Empty);
215
+ admissionPoolPublicationRevision = AdmissionPoolPublicationRevision(serialized);
216
+ admissionPoolPublicationObserved = serialized;
217
+ return serialized;
218
+ });
219
+ }
220
+
221
+ private static void EraseProductionAdmissionPoolState()
222
+ {
223
+ WithAdmissionPublicationLock(() =>
224
+ {
225
+ SessionState.EraseString(AdmissionSessionStateKey);
226
+ admissionPoolPublicationRevision = 0L;
227
+ admissionPoolPublicationObserved = string.Empty;
228
+ return true;
229
+ });
230
+ }
231
+
232
+ private static long AdmissionPoolPublicationRevision(string serialized)
233
+ {
234
+ if (string.IsNullOrEmpty(serialized)) return 0L;
235
+ var value = AsDictionary(UnityCliSerializer.Parse(serialized));
236
+ if (value == null || !string.Equals(AdmissionText(value, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) return 0L;
237
+ var revision = AdmissionLong(value, "publicationRevision");
238
+ if (revision < 1L) throw new InvalidDataException("Admission publication revision is missing or invalid.");
239
+ return revision;
240
+ }
241
+
242
+ /// <summary>重启后把固定journal恢复进同session在线settlement;只完成准入,不重新执行业务effect。</summary>
243
+ private static void ReconcilePendingAdmissionSettlements()
244
+ {
245
+ if (admissionPool == null) return;
246
+ Dictionary<string, object> root;
247
+ try { root = LoadProductionAdmissionSettlement(); }
248
+ catch { admissionPool.BlockRecovery(); return; }
249
+ if (root == null) return;
250
+ var entries = root.ContainsKey("entries") ? root["entries"] as List<object> : null;
251
+ if (entries == null || entries.Count > 256)
252
+ {
253
+ admissionPool.BlockRecovery();
254
+ return;
255
+ }
256
+ foreach (var raw in entries.ToArray())
257
+ {
258
+ var owner = ReadCompletionIntentOwner(AsDictionary(raw));
259
+ if (owner == null || !HasAdmissionCompletionIntent(ProductionQueueState, owner))
260
+ {
261
+ admissionPool.BlockRecovery();
262
+ return;
263
+ }
264
+ ProductionQueueState.TrackCompletionIntent(owner);
265
+ }
266
+ RetryPendingAdmissionCompletionIntents(ProductionQueueState);
267
+ }
268
+
269
+ /// <summary>同session每tick固定批次推进pool、marker与intent三者收敛;失败保持journal并固定退避。</summary>
270
+ internal static void RetryPendingAdmissionCompletionIntents(BridgeQueueState state)
271
+ {
272
+ if (state == null || state.AdmissionPool == null) return;
273
+ var now = state.NowMs();
274
+ var due = state.DueCompletionIntents(now, UnityCliBridgeQueuePolicy.MaxStartsPerTick);
275
+ foreach (var pending in due)
276
+ {
277
+ var owner = pending.Owner;
278
+ var pool = state.AdmissionPool;
279
+ if (pending.Compensation)
280
+ {
281
+ try
282
+ {
283
+ if ((!pending.IntentPersisted && !PersistAdmissionCompletionIntent(state, owner))
284
+ || !pool.CompensateAuthorization(ToPoolIdentity(owner))
285
+ || !state.PersistPool()
286
+ || !ClearAdmissionCompletionIntent(state, owner))
287
+ throw new IOException("Admission compensation continuation is not durable yet.");
288
+ state.Sinks.WriteLog("admission_prequeue_compensation_replayed requestId=" + owner.RequestId);
289
+ continue;
290
+ }
291
+ catch { }
292
+ }
293
+ else
294
+ {
295
+ UnityCliAdmissionPool.CompletionOutcome outcome;
296
+ var activeWorkOwnsAdmission = false;
297
+ lock (state.QueueLock)
298
+ {
299
+ // 同一root可先describe再attach业务WorkItem;旧intent必须等活动WorkItem完成后再收敛。
300
+ activeWorkOwnsAdmission = ActiveWorkOwnsAdmissionUnderQueueLock(state, owner);
301
+ if (activeWorkOwnsAdmission)
302
+ {
303
+ outcome = UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
304
+ }
305
+ else
306
+ {
307
+ var exact = pool.InspectExactIdentityFields(ToPoolIdentity(owner));
308
+ outcome = exact == UnityCliAdmissionPool.ExactIdentityOutcome.ExactCurrent
309
+ ? pool.CompleteTrusted(ToPoolIdentity(owner))
310
+ : exact == UnityCliAdmissionPool.ExactIdentityOutcome.DefinitelyAbsent
311
+ ? UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent
312
+ : UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
313
+ }
314
+ }
315
+ if (activeWorkOwnsAdmission)
316
+ {
317
+ state.DeferCompletionIntent(pending, now);
318
+ continue;
319
+ }
320
+ if (outcome == UnityCliAdmissionPool.CompletionOutcome.ExactCurrentCompleted
321
+ || outcome == UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent)
322
+ {
323
+ try
324
+ {
325
+ if (state.PersistPool() && state.ClearMarkers(owner) && ClearAdmissionCompletionIntent(state, owner))
326
+ {
327
+ state.Sinks.WriteLog("admission_settlement_replayed requestId=" + owner.RequestId + " outcome=" + outcome.ToString());
328
+ continue;
329
+ }
330
+ }
331
+ catch { }
332
+ }
333
+ }
334
+ state.DeferCompletionIntent(pending, now);
335
+ if (pending.FailureLogged) continue;
336
+ state.MarkCompletionIntentFailureLogged(pending);
337
+ state.Sinks.WriteLog("admission_settlement_retry_pending requestId=" + owner.RequestId);
338
+ }
339
+ }
340
+
341
+ private static AdmissionOwnerIdentity ReadCompletionIntentOwner(IDictionary<string, object> entry)
342
+ {
343
+ if (entry == null || !AdmissionDigestIsValid(AdmissionText(entry, "requestDigest"))) return null;
344
+ var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(entry, "admissionOwnerState");
345
+ if (!Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), source) || source == UnityCliAdmissionPool.AdmissionOwnerState.Granted) return null;
346
+ var owner = new AdmissionOwnerIdentity
347
+ {
348
+ RequestId = AdmissionText(entry, "requestId"), RequestDigest = AdmissionText(entry, "admissionRequestDigest"), TopRequestDigest = AdmissionText(entry, "requestDigest"),
349
+ RootFingerprint = AdmissionText(entry, "admissionRootFingerprint"), Token = AdmissionText(entry, "admissionToken"), SessionId = AdmissionText(entry, "admissionSessionId"),
350
+ ListenerId = AdmissionText(entry, "admissionListenerId"), ListenerGeneration = AdmissionLong(entry, "admissionListenerGeneration"),
351
+ ExecutionDeadlineMs = AdmissionLong(entry, "admissionExecutionDeadlineMs"), EditorSessionId = AdmissionText(entry, "editorSessionId"), OwnerState = source
352
+ };
353
+ if (!string.Equals(owner.EditorSessionId, EditorSessionId(), StringComparison.Ordinal)) return null;
354
+ try { ToPoolIdentity(owner); }
355
+ catch { return null; }
356
+ return owner;
357
+ }
358
+
359
+ private static void ShutdownAdmissionPool()
360
+ {
361
+ ShutdownAdmissionPool(ProductionQueueState);
362
+ }
363
+
364
+ /// <summary>publication关闭后在同一QueueLock内撤销未消费grant并裁决pool是否可分离。</summary>
365
+ private static void ShutdownAdmissionPool(BridgeQueueState state)
366
+ {
367
+ if (state == null) return;
368
+ lock (state.QueueLock)
369
+ {
370
+ state.QueuePublicationOpen = false;
371
+ state.QueueSessionStopping = true;
372
+ var pool = state.AdmissionPool;
373
+ if (pool != null)
374
+ {
375
+ foreach (var grant in pool.Capture().Records.Where(record => record.OwnerState == UnityCliAdmissionPool.AdmissionOwnerState.Granted).ToArray())
376
+ if (!pool.Revoke(grant.RequestId, grant.RootFingerprint, grant.Token)) pool.BlockRecovery();
377
+ }
378
+ if (state.RunningWork.Count != 0 || state.PendingSettlementWork.Count != 0)
379
+ {
380
+ state.AdmissionPoolShutdownPending = true;
381
+ return;
382
+ }
383
+ }
384
+ FinalizeAdmissionPoolShutdown(state);
385
+ }
386
+
387
+ internal static void TryFinalizeAdmissionPoolShutdown(BridgeQueueState state)
388
+ {
389
+ if (state == null || !state.AdmissionPoolShutdownPending) return;
390
+ FinalizeAdmissionPoolShutdown(state);
391
+ }
392
+
393
+ private static void FinalizeAdmissionPoolShutdown(BridgeQueueState state)
394
+ {
395
+ var detached = false;
396
+ lock (state.QueueLock)
397
+ {
398
+ var pool = state.AdmissionPool;
399
+ if (state.RunningWork.Count != 0 || state.PendingSettlementWork.Count != 0
400
+ || (pool != null && pool.Capture().Active != 0))
401
+ {
402
+ state.AdmissionPoolShutdownPending = true;
403
+ return;
404
+ }
405
+ if (pool != null) pool.Reset();
406
+ state.AdmissionPool = null;
407
+ state.AdmissionPoolShutdownPending = false;
408
+ detached = true;
409
+ }
410
+ if (detached && object.ReferenceEquals(state, ProductionQueueState))
411
+ WithAdmissionPublicationLock(() => { SessionState.EraseString(AdmissionSessionStateKey); admissionPoolPublicationRevision = 0L; admissionPoolPublicationObserved = string.Empty; return true; });
412
+ }
413
+
414
+ /// <summary>HTTP后台线程原子发grant;不进入主线程或transaction log。</summary>
415
+ private static string ExecuteAdmissionAcquire(byte[] rawBody, out string token)
416
+ {
417
+ token = null;
418
+ var value = StrictAdmissionObject(rawBody, "acquire", "protocolVersion", "requestId", "instanceId", "target", "command", "rootFingerprint");
419
+ var requestId = RequiredText(value, "requestId", 200);
420
+ if (AdmissionLong(value, "protocolVersion") != 1L || AdmissionText(value, "instanceId") != InstanceId() || AdmissionText(value, "target") != "editor")
421
+ throw new BridgeCommandException("E_INSTANCE_IDENTITY_MISMATCH", "Admission acquire identity does not match this Editor session.");
422
+ RequiredText(value, "command", 256);
423
+ var fingerprint = RequiredText(value, "rootFingerprint", 64);
424
+ var result = AcquireAdmissionAtEndpoint(ProductionQueueState, requestId, fingerprint);
425
+ var granted = result.Outcome == UnityCliAdmissionPool.AcquireOutcome.Granted;
426
+ if (granted) token = result.Token;
427
+ if (granted) WriteLog((result.NewlyGranted ? "admission_granted" : "admission_attached") + " requestId=" + requestId + " active=" + AdmissionActiveCount(ProductionQueueState).ToString(CultureInfo.InvariantCulture));
428
+ return UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Success(requestId, new Dictionary<string, object>
429
+ {
430
+ { "protocolVersion", 1L }, { "granted", granted },
431
+ { "grantExpiresInMs", (long)(granted ? result.GrantExpiresInMs : 0) },
432
+ { "retryAfterMs", (long)result.RetryAfterMs }
433
+ }));
434
+ }
435
+
436
+ private static string ExecuteAdmissionRevoke(byte[] rawBody, HttpListenerRequest httpRequest)
437
+ {
438
+ var value = StrictAdmissionObject(rawBody, "revoke", "protocolVersion", "requestId", "instanceId", "target", "rootFingerprint");
439
+ var requestId = RequiredText(value, "requestId", 200);
440
+ if (AdmissionLong(value, "protocolVersion") != 1L || AdmissionText(value, "instanceId") != InstanceId() || AdmissionText(value, "target") != "editor")
441
+ throw new BridgeCommandException("E_INSTANCE_IDENTITY_MISMATCH", "Admission revoke identity does not match this Editor session.");
442
+ var fingerprint = RequiredText(value, "rootFingerprint", 64);
443
+ var revoked = RevokeAdmissionAtEndpoint(ProductionQueueState, requestId, fingerprint, RequiredReleaseToken(httpRequest, requestId, fingerprint));
444
+ if (!revoked) throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission revoke identity does not match an unconsumed root.");
445
+ WriteLog("admission_revoked requestId=" + requestId + " active=" + AdmissionActiveCount(ProductionQueueState).ToString(CultureInfo.InvariantCulture));
446
+ return UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Success(requestId, new Dictionary<string, object> { { "revoked", true } }));
447
+ }
448
+
449
+ /// <summary>Editor admission端点与publication shutdown共享QueueLock,锁内只做pool内存裁决。</summary>
450
+ private static UnityCliAdmissionPool.AcquireResult AcquireAdmissionAtEndpoint(BridgeQueueState state, string requestId, string fingerprint)
451
+ {
452
+ if (state == null) throw new ArgumentNullException("state");
453
+ lock (state.QueueLock)
454
+ {
455
+ if (!state.QueuePublicationOpen || state.AdmissionPool == null)
456
+ throw ClosedAdmissionEndpoint(state);
457
+ state.TestHooks.Hit("before-admission-endpoint-acquire");
458
+ var result = state.AdmissionPool.Acquire(requestId, fingerprint, state.ListenerId ?? ListenerId, state.ListenerGeneration);
459
+ if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.Invalid)
460
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Admission acquire fields are invalid.");
461
+ if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.IdentityMismatch)
462
+ throw new BridgeCommandException("E_REQUEST_ID_REUSED", "Admission requestId is already bound to different business input.");
463
+ if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.AlreadyConsumed)
464
+ throw new BridgeCommandException("E_ADMISSION_ALREADY_CONSUMED", "Admission root is already consumed and must use exact attach or terminal recovery.");
465
+ return result;
466
+ }
467
+ }
468
+
469
+ private static bool RevokeAdmissionAtEndpoint(BridgeQueueState state, string requestId, string fingerprint, string token)
470
+ {
471
+ if (state == null) throw new ArgumentNullException("state");
472
+ lock (state.QueueLock)
473
+ {
474
+ if (!state.QueuePublicationOpen || state.AdmissionPool == null)
475
+ throw ClosedAdmissionEndpoint(state);
476
+ return state.AdmissionPool.Revoke(requestId, fingerprint, token);
477
+ }
478
+ }
479
+
480
+ private static BridgeCommandException ClosedAdmissionEndpoint(BridgeQueueState state)
481
+ {
482
+ return state != null && state.QueueSessionStopping
483
+ ? new BridgeCommandException("E_RUNTIME_SESSION_STALE", "The Unity session is stopping and no longer accepts admission work.", null, new[] { "Start a new Unity session and submit a new request." })
484
+ : new BridgeCommandException("E_UNITY_DOMAIN_RELOAD", "Unity admission is closed during lifecycle transition.", null, new[] { "Wait for Unity to finish reloading and retry." }, true, 1000);
485
+ }
486
+
487
+ private static int AdmissionActiveCount(BridgeQueueState state)
488
+ {
489
+ lock (state.QueueLock) return state.AdmissionPool == null ? 0 : state.AdmissionPool.Capture().Active;
490
+ }
491
+
492
+ internal static UnityCliAdmissionPool.AcquireResult AcquireAdmissionForTests(BridgeQueueState state, string requestId, string fingerprint)
493
+ {
494
+ if (state == null || state.IsolatedExecution == null) throw new ArgumentException("Admission endpoint test state must be isolated.", "state");
495
+ return AcquireAdmissionAtEndpoint(state, requestId, fingerprint);
496
+ }
497
+
498
+ /// <summary>只解析HTTP immutable admission输入;调用方在QueueLock内调用AuthorizeAdmissionForQueue完成pool线性化。</summary>
499
+ internal static AdmissionQueueReservation PrepareAdmissionForQueue(HttpListenerRequest httpRequest, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes)
500
+ {
501
+ var rootId = httpRequest.Headers["x-unity-cli-admission-root-id"];
502
+ var fingerprint = httpRequest.Headers["x-unity-cli-admission-fingerprint"];
503
+ var token = httpRequest.Headers["x-unity-cli-admission-token"];
504
+ var deadlineText = httpRequest.Headers["x-unity-cli-admission-deadline-utc"];
505
+ var reservation = new AdmissionQueueReservation
506
+ {
507
+ RequestId = request.RequestId,
508
+ RequestDigest = UnityCliRequestDigest.Compute(rawRequestBytes, request.InstanceId),
509
+ RootFingerprint = fingerprint,
510
+ Token = token,
511
+ HasAdmissionHeaders = !string.IsNullOrEmpty(rootId) || !string.IsNullOrEmpty(fingerprint) || !string.IsNullOrEmpty(token) || !string.IsNullOrEmpty(deadlineText),
512
+ IsBypass = AdmissionBypassCommand(request.Command),
513
+ HasExactTerminal = AdmissionHasExactTerminal(request, rawRequestBytes)
514
+ };
515
+ if (!reservation.HasAdmissionHeaders) return reservation;
516
+ if (string.IsNullOrEmpty(rootId) || string.IsNullOrEmpty(fingerprint) || string.IsNullOrEmpty(deadlineText))
517
+ throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission root headers are incomplete.");
518
+ if (!string.Equals(rootId, request.RequestId, StringComparison.Ordinal))
519
+ throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission root requestId does not match the Bridge request body.");
520
+ DateTimeOffset deadline;
521
+ if (!DateTimeOffset.TryParse(deadlineText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out deadline))
522
+ throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission execution deadline is invalid.");
523
+ reservation.ExecutionDeadlineMs = deadline.ToUnixTimeMilliseconds();
524
+ if (reservation.ExecutionDeadlineMs <= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())
525
+ throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission execution deadline has elapsed.");
526
+ reservation.RootFingerprint = fingerprint;
527
+ reservation.Token = token;
528
+ return reservation;
529
+ }
530
+
531
+ /// <summary>
532
+ /// QueueLock是外层锁,admission pool锁是内层锁;本方法只操作内存pool,不读取文件或SessionState。
533
+ /// </summary>
534
+ internal static void AuthorizeAdmissionForQueue(AdmissionQueueReservation reservation, UnityCli.Protocol.BridgeRequest request)
535
+ {
536
+ AuthorizeAdmissionForQueue(ProductionQueueState, reservation, request);
537
+ }
538
+
539
+ internal static void AuthorizeAdmissionForQueue(BridgeQueueState state, AdmissionQueueReservation reservation, UnityCli.Protocol.BridgeRequest request)
540
+ {
541
+ if (state == null || reservation == null || request == null) throw new ArgumentNullException("reservation");
542
+ if (!reservation.HasAdmissionHeaders)
543
+ {
544
+ if (reservation.IsBypass) return;
545
+ throw new BridgeCommandException("E_ADMISSION_REQUIRED", "A valid Unity task-pool admission is required before this business request can enter the queue.");
546
+ }
547
+ var pool = state.AdmissionPool;
548
+ if (pool == null) throw new BridgeCommandException("E_RUNTIME_SESSION_STALE", "The Unity session ended before admission completed.");
549
+ if (!string.IsNullOrEmpty(reservation.Token))
550
+ {
551
+ var result = pool.AuthorizeAndCapture(reservation.RequestId, reservation.RootFingerprint, reservation.RequestDigest, reservation.Token, state.ListenerId ?? ListenerId, state.ListenerGeneration, reservation.ExecutionDeadlineMs);
552
+ if (result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.Authorized
553
+ || result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.AlreadyConsumed)
554
+ {
555
+ reservation.Owner = ToAdmissionOwner(result.Identity, reservation.RequestDigest);
556
+ reservation.NewlyConsumed = result.NewlyConsumed;
557
+ return;
558
+ }
559
+ if ((result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.Invalid
560
+ || result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.RecoveryBlocked)
561
+ && !pool.Revoke(reservation.RequestId, reservation.RootFingerprint, reservation.Token))
562
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission rejection could not prove exact unconsumed cleanup.");
563
+ throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission token is missing, expired, reused, or bound to another root.");
564
+ }
565
+ if (pool.AttachConsumed(reservation.RequestId, reservation.RootFingerprint, reservation.RequestDigest, state.ListenerId ?? ListenerId, state.ListenerGeneration, reservation.ExecutionDeadlineMs))
566
+ {
567
+ reservation.Owner = CaptureAdmissionOwner(state, reservation.RequestId, reservation.RequestDigest);
568
+ return;
569
+ }
570
+ if (AdmissionRecoveryCommand(request.Command) && pool.RecoverConsumed(reservation.RequestId, reservation.RootFingerprint, reservation.RequestDigest, state.ListenerId ?? ListenerId, state.ListenerGeneration, reservation.ExecutionDeadlineMs))
571
+ {
572
+ reservation.Owner = CaptureAdmissionOwner(state, reservation.RequestId, reservation.RequestDigest);
573
+ return;
574
+ }
575
+ if (reservation.HasExactTerminal) return;
576
+ throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Consumed admission root cannot attach to this listener generation.");
577
+ }
578
+
579
+ /// <summary>durable settlement已失败并等待恢复时,同字节duplicate只附着原WorkItem。</summary>
580
+ private static bool TryAuthorizeCompletionPendingAttach(BridgeQueueState state, AdmissionQueueReservation reservation, BridgeWorkItem existing)
581
+ {
582
+ var owner = existing == null ? null : existing.AdmissionOwner;
583
+ if (state == null || reservation == null || owner == null || !existing.CompletionIntentReady
584
+ || !existing.SettlementFailureLogged || !reservation.HasAdmissionHeaders
585
+ || !string.Equals(reservation.RequestId, owner.RequestId, StringComparison.Ordinal)
586
+ || !string.Equals(reservation.RequestDigest, owner.RequestDigest, StringComparison.Ordinal)
587
+ || !string.Equals(reservation.RootFingerprint, owner.RootFingerprint, StringComparison.Ordinal)
588
+ || reservation.ExecutionDeadlineMs != owner.ExecutionDeadlineMs
589
+ || (!string.IsNullOrEmpty(reservation.Token) && !string.Equals(reservation.Token, owner.Token, StringComparison.Ordinal))) return false;
590
+ reservation.Owner = owner;
591
+ reservation.NewlyConsumed = false;
592
+ return true;
593
+ }
594
+
595
+ internal static void MarkAdmissionQueuePublished(AdmissionQueueReservation reservation)
596
+ {
597
+ if (reservation != null) reservation.QueuePublished = true;
598
+ }
599
+
600
+ /// <summary>publication前补偿只修改pool内存;持久化与日志由QueueLock外调用方完成。</summary>
601
+ internal static bool CompensateAdmissionBeforeQueuePublication(AdmissionQueueReservation reservation)
602
+ {
603
+ return CompensateAdmissionBeforeQueuePublication(ProductionQueueState, reservation);
604
+ }
605
+
606
+ internal static bool CompensateAdmissionBeforeQueuePublication(BridgeQueueState state, AdmissionQueueReservation reservation)
607
+ {
608
+ if (state == null || reservation == null || reservation.QueuePublished || !reservation.NewlyConsumed || reservation.Owner == null) return true;
609
+ var pool = state.AdmissionPool;
610
+ var compensated = pool != null && pool.CompensateAuthorization(ToPoolIdentity(reservation.Owner));
611
+ if (compensated) reservation.NewlyConsumed = false;
612
+ return compensated;
613
+ }
614
+
615
+ /// <summary>只按当前pool中的完整digest冻结exact owner;缺失与identity冲突保持不同结果。</summary>
616
+ internal static bool TryCaptureExactAdmissionOwner(string requestId, string requestDigest, out AdmissionOwnerIdentity identity)
617
+ {
618
+ return TryCaptureExactAdmissionOwner(ProductionQueueState, requestId, requestDigest, out identity);
619
+ }
620
+
621
+ internal static bool TryCaptureExactAdmissionOwner(BridgeQueueState state, string requestId, string requestDigest, out AdmissionOwnerIdentity identity)
622
+ {
623
+ identity = null;
624
+ UnityCliAdmissionPool.ConsumedIdentity current;
625
+ var pool = state == null ? null : state.AdmissionPool;
626
+ if (pool == null || !pool.TryGetConsumedIdentity(requestId, out current)) return false;
627
+ identity = ToAdmissionOwner(current, requestDigest);
628
+ return true;
629
+ }
630
+
631
+ internal static AdmissionOwnerIdentity CaptureAdmissionOwner(string requestId, string requestDigest = null)
632
+ {
633
+ return CaptureAdmissionOwner(ProductionQueueState, requestId, requestDigest);
634
+ }
635
+
636
+ internal static AdmissionOwnerIdentity CaptureAdmissionOwner(BridgeQueueState state, string requestId, string requestDigest = null)
637
+ {
638
+ UnityCliAdmissionPool.ConsumedIdentity identity;
639
+ var pool = state == null ? null : state.AdmissionPool;
640
+ if (pool == null || !pool.TryGetConsumedIdentity(requestId, out identity))
641
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner identity cannot be frozen after consumption.");
642
+ return ToAdmissionOwner(identity, requestDigest);
643
+ }
644
+
645
+ /// <summary>先落exact completion intent,再在state锁内完成pool memory与map裁决。</summary>
646
+ internal static bool CompleteAdmissionRoot(AdmissionOwnerIdentity identity)
647
+ {
648
+ return CompleteAdmissionRoot(ProductionQueueState, identity);
649
+ }
650
+
651
+ internal static bool CompleteAdmissionRoot(BridgeQueueState state, AdmissionOwnerIdentity identity)
652
+ {
653
+ if (state == null || identity == null) return false;
654
+ if (!HasAdmissionCompletionIntent(state, identity) && !PersistAdmissionCompletionIntent(state, identity)) return false;
655
+ var pool = state.AdmissionPool;
656
+ if (pool == null) return false;
657
+ UnityCliAdmissionPool.CompletionOutcome outcome;
658
+ lock (state.QueueLock)
659
+ {
660
+ if (ActiveWorkOwnsAdmissionUnderQueueLock(state, identity)) return false;
661
+ outcome = pool.CompleteTrusted(ToPoolIdentity(identity));
662
+ }
663
+ if (outcome != UnityCliAdmissionPool.CompletionOutcome.ExactCurrentCompleted
664
+ && outcome != UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent) return false;
665
+ try
666
+ {
667
+ if (!state.PersistPool()) return false;
668
+ if (!state.ClearMarkers(identity)) return false;
669
+ if (!ClearAdmissionCompletionIntent(state, identity)) return false;
670
+ }
671
+ catch { return false; }
672
+ state.Sinks.WriteLog("admission_terminal requestId=" + identity.RequestId + " outcome=" + outcome.ToString() + " active=" + pool.Capture().Active.ToString(CultureInfo.InvariantCulture));
673
+ TryFinalizeAdmissionPoolShutdown(state);
674
+ return true;
675
+ }
676
+
677
+ // 调用方必须持有QueueLock,使活动WorkItem检查与owner完成形成同一原子裁决。
678
+ private static bool ActiveWorkOwnsAdmissionUnderQueueLock(BridgeQueueState state, AdmissionOwnerIdentity identity)
679
+ {
680
+ BridgeWorkItem existing;
681
+ return state.ActiveRequestWork.TryGetValue(identity.RequestId, out existing)
682
+ && existing.AdmissionOwner != null
683
+ && SameAdmissionOwner(existing.AdmissionOwner, identity);
684
+ }
685
+
686
+ /// <summary>仅为将随父root落盘的continuation写入完整intent;pool transfer由队列terminal owner提交。</summary>
687
+ internal static void AddAdmissionOwnerFields(IDictionary<string, object> state, string requestId, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Continuation) { AddAdmissionOwnerFields(state, CaptureAdmissionOwner(requestId), intendedOwner); }
688
+
689
+ internal static void AddAdmissionOwnerFields(IDictionary<string, object> state, AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Continuation)
690
+ {
691
+ if (state == null || identity == null) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner marker identity is unavailable.");
692
+ WriteAdmissionOwnerFields(state, identity, intendedOwner, identity.OwnerState == intendedOwner);
693
+ }
694
+
695
+ internal static AdmissionOwnerIdentity ReadAdmissionOwnerFields(IDictionary<string, object> state, string requestId)
696
+ {
697
+ if (state == null || string.IsNullOrEmpty(requestId)) return null;
698
+ var committed = AdmissionBool(state, "admissionTransferCommitted");
699
+ var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(state, "admissionOwnerState");
700
+ var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(state, "admissionIntendedOwnerState");
701
+ var topDigest = AdmissionText(state, "requestDigest");
702
+ if (!AdmissionDigestIsValid(topDigest)) topDigest = AdmissionText(state, "cancelRootRequestDigest");
703
+ if (!AdmissionDigestIsValid(topDigest)) topDigest = AdmissionText(state, "admissionRequestDigest");
704
+ return new AdmissionOwnerIdentity
705
+ {
706
+ RequestId = requestId,
707
+ RequestDigest = AdmissionText(state, "admissionRequestDigest"),
708
+ TopRequestDigest = topDigest,
709
+ RootFingerprint = AdmissionText(state, "admissionRootFingerprint"),
710
+ Token = AdmissionText(state, "admissionToken"),
711
+ SessionId = AdmissionText(state, "admissionSessionId"),
712
+ ListenerId = AdmissionText(state, "admissionListenerId"),
713
+ ListenerGeneration = AdmissionLong(state, "admissionListenerGeneration"),
714
+ ExecutionDeadlineMs = AdmissionLong(state, "admissionExecutionDeadlineMs"),
715
+ EditorSessionId = AdmissionText(state, "editorSessionId"),
716
+ OwnerState = committed ? intended : source
717
+ };
718
+ }
719
+
720
+ /// <summary>三段式owner transfer:先落完整intent,再exact转pool,最后落committed marker。</summary>
721
+ internal static AdmissionOwnerIdentity PersistAdmissionOwnerTransfer(string markerScope, IDictionary<string, object> marker, AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner)
722
+ {
723
+ return PersistAdmissionOwnerTransfer(ProductionQueueState, markerScope, marker, identity, intendedOwner);
724
+ }
725
+
726
+ internal static AdmissionOwnerIdentity PersistAdmissionOwnerTransfer(BridgeQueueState state, string markerScope, IDictionary<string, object> marker, AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner)
727
+ {
728
+ if (state == null || marker == null || identity == null || string.IsNullOrEmpty(markerScope)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission transfer marker is unavailable.");
729
+ var pool = state.AdmissionPool;
730
+ if (pool == null) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission pool is unavailable during owner transfer.");
731
+ var existing = state.LoadOperation(markerScope);
732
+ if (existing != null && !string.Equals(AdmissionText(existing, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal))
733
+ {
734
+ state.ClearOperation(markerScope);
735
+ existing = null;
736
+ }
737
+ if (existing != null && !AdmissionMarkerMatchesIdentity(existing, identity) && !AdmissionMarkerCanAcquireOwner(existing, identity))
738
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission transfer marker is owned by another exact request.");
739
+ marker["requestId"] = identity.RequestId;
740
+ marker["requestDigest"] = AdmissionTopDigest(identity);
741
+ marker["editorSessionId"] = identity.EditorSessionId ?? EditorSessionId();
742
+ WriteAdmissionOwnerFields(marker, identity, intendedOwner, false);
743
+ state.SaveOperation(markerScope, new Dictionary<string, object>(marker));
744
+ if (!pool.PrepareOwnerTransfer(ToPoolIdentity(identity), intendedOwner))
745
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner transfer intent does not match the current exact owner.");
746
+ if (!PersistAdmissionPoolState(state)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner transfer intent could not be durably persisted.");
747
+ UnityCliAdmissionPool.ConsumedIdentity successor;
748
+ if (!pool.CommitOwnerTransfer(ToPoolIdentity(identity), intendedOwner, out successor))
749
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner transfer could not be committed.");
750
+ var updated = ToAdmissionOwner(successor, identity.RequestDigest);
751
+ updated.TopRequestDigest = AdmissionText(marker, "requestDigest");
752
+ updated.EditorSessionId = AdmissionText(marker, "editorSessionId");
753
+ marker["admissionTransferCommitted"] = true;
754
+ state.SaveOperation(markerScope, new Dictionary<string, object>(marker));
755
+ if (!PersistAdmissionPoolState(state)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner transfer commit could not be durably persisted.");
756
+ return updated;
757
+ }
758
+
759
+ /// <summary>重试修复transfer窗口;pool确定缺失时仍先持久化当前SessionState再交给fixed marker清理。</summary>
760
+ internal static AdmissionOwnerIdentity ReconcileAdmissionTransferForLiveMarker(string markerScope, Dictionary<string, object> marker, string requestId, string requestDigest)
761
+ {
762
+ return ReconcileAdmissionTransferForLiveMarker(ProductionQueueState, markerScope, marker, requestId, requestDigest);
763
+ }
764
+
765
+ internal static AdmissionOwnerIdentity ReconcileAdmissionTransferForLiveMarker(BridgeQueueState state, string markerScope, Dictionary<string, object> marker, string requestId, string requestDigest)
766
+ {
767
+ if (string.Equals(markerScope, "test-cancel-admission-owner", StringComparison.Ordinal)
768
+ || string.Equals(markerScope, "test-cancel-terminal-cleanup", StringComparison.Ordinal))
769
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Test cancellation admission markers require dedicated parent and cancel identity reconciliation.");
770
+ var markerOwner = ReadAdmissionOwnerFields(marker, requestId);
771
+ var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
772
+ var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
773
+ var pool = state == null ? null : state.AdmissionPool;
774
+ if (!AdmissionMarkerHasImmutableIdentity(marker, markerOwner, AdmissionTopDigest(markerOwner)) || pool == null)
775
+ {
776
+ if (pool != null) pool.BlockRecovery();
777
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker identity is invalid.");
778
+ }
779
+ var exact = pool.InspectExactIdentityFields(ToPoolIdentity(markerOwner));
780
+ if (exact == UnityCliAdmissionPool.ExactIdentityOutcome.DefinitelyAbsent)
781
+ {
782
+ try
783
+ {
784
+ if (!RestoreAbsentAdmissionOwner(state, markerScope, marker, markerOwner)) return null;
785
+ exact = UnityCliAdmissionPool.ExactIdentityOutcome.ExactCurrent;
786
+ }
787
+ catch
788
+ {
789
+ pool.BlockRecovery();
790
+ throw;
791
+ }
792
+ }
793
+ if (exact == UnityCliAdmissionPool.ExactIdentityOutcome.IdentityMismatchOrBlocked)
794
+ {
795
+ UnityCliAdmissionPool.ConsumedIdentity rebound;
796
+ if (!pool.TryGetConsumedIdentity(requestId, out rebound))
797
+ {
798
+ pool.BlockRecovery();
799
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker conflicts with the current requestId owner.");
800
+ }
801
+ var reboundOwner = ToAdmissionOwner(rebound, markerOwner.RequestDigest);
802
+ reboundOwner.TopRequestDigest = AdmissionText(marker, "requestDigest");
803
+ reboundOwner.EditorSessionId = AdmissionText(marker, "editorSessionId");
804
+ if (!SameAdmissionTransferIdentityExceptListener(reboundOwner, markerOwner)
805
+ || (reboundOwner.OwnerState != source && reboundOwner.OwnerState != intended))
806
+ {
807
+ pool.BlockRecovery();
808
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker conflicts with the current requestId owner.");
809
+ }
810
+ markerOwner = reboundOwner;
811
+ }
812
+ else if (exact != UnityCliAdmissionPool.ExactIdentityOutcome.ExactCurrent)
813
+ {
814
+ pool.BlockRecovery();
815
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker conflicts with the current requestId owner.");
816
+ }
817
+ var current = CaptureAdmissionOwner(state, requestId, markerOwner.RequestDigest);
818
+ current.TopRequestDigest = AdmissionText(marker, "requestDigest");
819
+ current.EditorSessionId = AdmissionText(marker, "editorSessionId");
820
+ if (current.OwnerState != source && current.OwnerState != intended)
821
+ {
822
+ pool.BlockRecovery();
823
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker owner state is outside its exact transfer window.");
824
+ }
825
+ if (current.OwnerState != intended) return PersistAdmissionOwnerTransfer(state, markerScope, marker, current, intended);
826
+ WriteAdmissionOwnerFields(marker, current, intended, true);
827
+ state.SaveOperation(markerScope, marker);
828
+ if (!PersistAdmissionPoolState(state)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission marker reconciliation persistence was not confirmed.");
829
+ return current;
830
+ }
831
+
832
+ /// <summary>启动时按marker、对应root与lease三者重建或清理owner;pool缺失不能单独证明终态。</summary>
833
+ private static void ReconcileOrphanAdmissionOwnerMarkers()
834
+ {
835
+ var pool = admissionPool;
836
+ if (pool == null) return;
837
+ foreach (var scope in AdmissionDurableScopes)
838
+ {
839
+ if (scope == "test-cancel") continue;
840
+ var markerScope = scope + "-admission-owner";
841
+ if (!UnityCliActiveOperation.Exists(markerScope)) continue;
842
+ Dictionary<string, object> marker;
843
+ try { marker = UnityCliActiveOperation.Load(markerScope); }
844
+ catch { pool.BlockRecovery(); return; }
845
+ if (marker == null) continue;
846
+ if (!string.Equals(AdmissionText(marker, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal))
847
+ {
848
+ try { UnityCliActiveOperation.Clear(markerScope); } catch { pool.BlockRecovery(); return; }
849
+ continue;
850
+ }
851
+ AdmissionOwnerIdentity markerOwner;
852
+ try { markerOwner = ReadAdmissionOwnerFields(marker, AdmissionText(marker, "requestId")); }
853
+ catch { pool.BlockRecovery(); return; }
854
+ if (!AdmissionMarkerHasImmutableIdentity(marker, markerOwner, AdmissionTopDigest(markerOwner))) { pool.BlockRecovery(); return; }
855
+ var exact = pool.InspectExactIdentityFields(ToPoolIdentity(markerOwner));
856
+ if (exact == UnityCliAdmissionPool.ExactIdentityOutcome.IdentityMismatchOrBlocked) { pool.BlockRecovery(); return; }
857
+ if (exact != UnityCliAdmissionPool.ExactIdentityOutcome.DefinitelyAbsent) continue;
858
+ try { RestoreAbsentAdmissionOwner(markerScope, marker, markerOwner); }
859
+ catch { pool.BlockRecovery(); return; }
860
+ }
861
+ }
862
+
863
+ /// <summary>pool缺失时,cleanup marker可覆盖root/lease清理窗口;普通owner marker只接受完整root+lease重建。</summary>
864
+ private static bool RestoreAbsentAdmissionOwner(string markerScope, Dictionary<string, object> marker, AdmissionOwnerIdentity markerOwner)
865
+ {
866
+ return RestoreAbsentAdmissionOwner(ProductionQueueState, markerScope, marker, markerOwner);
867
+ }
868
+
869
+ private static bool RestoreAbsentAdmissionOwner(BridgeQueueState state, string markerScope, Dictionary<string, object> marker, AdmissionOwnerIdentity markerOwner)
870
+ {
871
+ bool rootReadable;
872
+ var rootExists = HasExactAdmissionMarkerRoot(state, markerScope, markerOwner, out rootReadable);
873
+ if (!rootReadable) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker root cannot be verified.");
874
+ bool leaseOwned;
875
+ if (!state.TryIsLeaseOwned(markerOwner.RequestId, markerOwner.RequestDigest, out leaseOwned))
876
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker lease ownership cannot be verified.");
877
+ var cleanupMarker = markerScope.EndsWith("-terminal-cleanup", StringComparison.Ordinal)
878
+ || markerScope.EndsWith("-pre-effect-cleanup", StringComparison.Ordinal);
879
+ if (rootExists || leaseOwned)
880
+ {
881
+ if (!cleanupMarker && (!rootExists || !leaseOwned))
882
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner marker has only a partial root and lease proof.");
883
+ var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
884
+ var pool = state == null ? null : state.AdmissionPool;
885
+ if (pool == null || !pool.RestoreConsumed(ToPoolIdentity(markerOwner), intended, AdmissionBool(marker, "admissionTransferCommitted")))
886
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker could not rebuild its exact consumed owner.");
887
+ if (!PersistAdmissionPoolState(state)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker restore was not durably persisted.");
888
+ return true;
889
+ }
890
+ var preEffectCleanupMarker = markerScope.EndsWith("-pre-effect-cleanup", StringComparison.Ordinal);
891
+ if (!preEffectCleanupMarker && !AdmissionOwnerTerminalExists(state, markerScope, markerOwner))
892
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner marker has no exact cleanup completion proof.");
893
+ state.ClearOperation(markerScope);
894
+ if (state.OperationExists(markerScope))
895
+ throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Completed admission cleanup marker could not be cleared.");
896
+ return false;
897
+ }
898
+
899
+ private static bool AdmissionOwnerTerminalExists(BridgeQueueState state, string markerScope, AdmissionOwnerIdentity owner)
900
+ {
901
+ try
902
+ {
903
+ var terminalScope = markerScope.EndsWith("-admission-owner", StringComparison.Ordinal)
904
+ ? markerScope.Substring(0, markerScope.Length - "-admission-owner".Length)
905
+ : markerScope;
906
+ if (terminalScope.EndsWith("-terminal-cleanup", StringComparison.Ordinal))
907
+ terminalScope = terminalScope.Substring(0, terminalScope.Length - "-terminal-cleanup".Length);
908
+ if (terminalScope.EndsWith("-pre-effect-cleanup", StringComparison.Ordinal))
909
+ terminalScope = terminalScope.Substring(0, terminalScope.Length - "-pre-effect-cleanup".Length);
910
+ object terminal;
911
+ return state.TryGetTerminal(terminalScope, owner.RequestId, AdmissionTopDigest(owner), out terminal);
912
+ }
913
+ catch { return false; }
914
+ }
915
+
916
+ private static bool HasExactAdmissionMarkerRoot(BridgeQueueState state, string markerScope, AdmissionOwnerIdentity owner, out bool readable)
917
+ {
918
+ readable = true;
919
+ try
920
+ {
921
+ var rootScope = markerScope.EndsWith("-admission-owner", StringComparison.Ordinal)
922
+ ? markerScope.Substring(0, markerScope.Length - "-admission-owner".Length)
923
+ : markerScope;
924
+ if (rootScope.EndsWith("-terminal-cleanup", StringComparison.Ordinal)) rootScope = rootScope.Substring(0, rootScope.Length - "-terminal-cleanup".Length);
925
+ if (rootScope.EndsWith("-pre-effect-cleanup", StringComparison.Ordinal)) rootScope = rootScope.Substring(0, rootScope.Length - "-pre-effect-cleanup".Length);
926
+ var root = state.LoadOperation(rootScope);
927
+ if (root == null || !string.Equals(AdmissionText(root, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) return false;
928
+ return string.Equals(AdmissionText(root, "requestId"), owner.RequestId, StringComparison.Ordinal)
929
+ && string.Equals(AdmissionText(root, "requestDigest"), owner.RequestDigest, StringComparison.Ordinal);
930
+ }
931
+ catch { readable = false; return false; }
932
+ }
933
+
934
+ /// <summary>reload只按完整marker identity修复intent/commit;requestId单字段从不构成owner证明。</summary>
935
+ private static void ReconcileRestoredAdmissionRoots()
936
+ {
937
+ if (admissionPool == null) return;
938
+ foreach (var captured in admissionPool.Capture().Records)
939
+ {
940
+ var record = captured;
941
+ var cancelOwnership = DedicatedCancelMarkerOwnershipForState(ProductionQueueState, record);
942
+ if (cancelOwnership == DedicatedCancelMarkerOwnership.ExactOwner) continue;
943
+ if (cancelOwnership == DedicatedCancelMarkerOwnership.Blocked)
944
+ {
945
+ admissionPool.BlockRecovery();
946
+ return;
947
+ }
948
+ Dictionary<string, object> marker;
949
+ string markerScope;
950
+ try
951
+ {
952
+ if (TryFindExactAdmissionMarker(record, out markerScope, out marker))
953
+ {
954
+ ReconcileAdmissionMarker(record, markerScope, marker);
955
+ continue;
956
+ }
957
+ if (record.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem
958
+ && record.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.Recovery)
959
+ {
960
+ admissionPool.BlockRecovery();
961
+ return;
962
+ }
963
+ var owner = RecordIdentity(record);
964
+ string durableScope;
965
+ UnityCliAdmissionPool.AdmissionOwnerState durableOwner;
966
+ if (TryFindExactDurableBusinessRoot(record, out durableScope, out durableOwner))
967
+ {
968
+ if (durableScope == "direct-mutation")
969
+ {
970
+ var direct = ReconcileAcceptedDirectMutationAfterReload(ProductionQueueState, record.RequestId, record.RequestDigest);
971
+ if (direct == DirectMutationReloadOutcome.SettledEffectBefore) continue;
972
+ if (direct == DirectMutationReloadOutcome.IdentityBlocked)
973
+ throw new InvalidOperationException("Direct mutation reload identity is blocked.");
974
+ }
975
+ var markerState = new Dictionary<string, object>
976
+ {
977
+ { "requestId", record.RequestId }, { "requestDigest", record.RequestDigest }, { "editorSessionId", EditorSessionId() }
978
+ };
979
+ PersistAdmissionOwnerTransfer(durableScope + "-admission-owner", markerState, owner,
980
+ owner.OwnerState == UnityCliAdmissionPool.AdmissionOwnerState.WorkItem ? durableOwner : UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
981
+ continue;
982
+ }
983
+ if (owner.OwnerState == UnityCliAdmissionPool.AdmissionOwnerState.WorkItem)
984
+ owner = TransitionWithoutMarker(owner, UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
985
+ owner = TransitionWithoutMarker(owner, UnityCliAdmissionPool.AdmissionOwnerState.TerminalCleanup);
986
+ if (!CompleteAdmissionRoot(owner)) { admissionPool.BlockRecovery(); return; }
987
+ }
988
+ catch (Exception exception)
989
+ {
990
+ admissionPool.BlockRecovery();
991
+ WriteLog("admission_reconcile_failed requestId=" + record.RequestId + " " + exception.GetType().Name + ": " + exception.Message);
992
+ return;
993
+ }
994
+ }
995
+ }
996
+
997
+ private enum DedicatedCancelMarkerOwnership { NoMarkerOrDifferentOwner, ExactOwner, Blocked }
998
+
999
+ /// <summary>cancel owner只有marker、parent root与run identity全部精确一致时才从通用recovery分流。</summary>
1000
+ private static DedicatedCancelMarkerOwnership DedicatedCancelMarkerOwnershipForState(BridgeQueueState state, UnityCliAdmissionPool.AdmissionRecord record)
1001
+ {
1002
+ const string markerScope = "test-cancel-admission-owner";
1003
+ if (state == null || record == null) return DedicatedCancelMarkerOwnership.NoMarkerOrDifferentOwner;
1004
+ try
1005
+ {
1006
+ if (!state.OperationExists(markerScope)) return DedicatedCancelMarkerOwnership.NoMarkerOrDifferentOwner;
1007
+ var marker = state.LoadOperation(markerScope);
1008
+ if (marker == null) return DedicatedCancelMarkerOwnership.Blocked;
1009
+ if (!string.Equals(AdmissionText(marker, "requestId"), record.RequestId, StringComparison.Ordinal))
1010
+ return DedicatedCancelMarkerOwnership.NoMarkerOrDifferentOwner;
1011
+ var rootRequestId = AdmissionText(marker, "rootRequestId");
1012
+ var rootRequestDigest = AdmissionText(marker, "rootRequestDigest");
1013
+ var runId = AdmissionText(marker, "runId");
1014
+ string continuationState;
1015
+ string repairEffectState;
1016
+ object candidateEnvelope;
1017
+ if (!AdmissionMarkerMatchesRecord(marker, record)
1018
+ || !IsSafeId(rootRequestId) || !AdmissionDigestIsValid(rootRequestDigest) || !Guid.TryParse(runId, out _)
1019
+ || !string.Equals(AdmissionText(marker, "requestDigest"), rootRequestDigest, StringComparison.Ordinal)
1020
+ || !TryReadCancelAdmissionMarkerState(marker, record.RequestId, record.RequestDigest, rootRequestDigest, runId,
1021
+ out continuationState, out repairEffectState, out _, out candidateEnvelope))
1022
+ return DedicatedCancelMarkerOwnership.Blocked;
1023
+ var parent = state.LoadOperation("test-run");
1024
+ if (parent == null)
1025
+ {
1026
+ object parentTerminal;
1027
+ object cancelTerminal;
1028
+ object selectedCancelTerminal;
1029
+ return state.TryGetTerminal("test-run", rootRequestId, rootRequestDigest, out parentTerminal)
1030
+ && state.TryGetTerminal("test-cancel", record.RequestId, record.RequestDigest, out cancelTerminal)
1031
+ && state.TryGetTerminalBySelector("test-cancel", record.RequestId, rootRequestDigest, out selectedCancelTerminal)
1032
+ && string.Equals(UnityCliSerializer.Serialize(cancelTerminal), UnityCliSerializer.Serialize(selectedCancelTerminal), StringComparison.Ordinal)
1033
+ && (!string.Equals(continuationState, "continuation_pending", StringComparison.Ordinal)
1034
+ || string.Equals(UnityCliSerializer.Serialize(cancelTerminal), UnityCliSerializer.Serialize(candidateEnvelope), StringComparison.Ordinal))
1035
+ ? DedicatedCancelMarkerOwnership.ExactOwner
1036
+ : DedicatedCancelMarkerOwnership.Blocked;
1037
+ }
1038
+ object rawOperation;
1039
+ var operation = parent.TryGetValue("testOperation", out rawOperation) ? AsDictionary(rawOperation) : null;
1040
+ if (operation == null
1041
+ || !string.Equals(AdmissionText(parent, "requestId"), rootRequestId, StringComparison.Ordinal)
1042
+ || !string.Equals(AdmissionText(parent, "requestDigest"), rootRequestDigest, StringComparison.Ordinal)
1043
+ || !string.Equals(AdmissionText(parent, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)
1044
+ || !string.Equals(AdmissionText(operation, "runId"), runId, StringComparison.Ordinal))
1045
+ return DedicatedCancelMarkerOwnership.Blocked;
1046
+ if (string.Equals(continuationState, "dispatched", StringComparison.Ordinal))
1047
+ {
1048
+ return AdmissionBool(operation, "cancelRequested") && AdmissionBool(operation, "cancelNeedsContinuation")
1049
+ && string.Equals(AdmissionText(operation, "cancelEffectState"), "dispatched", StringComparison.Ordinal)
1050
+ && string.Equals(AdmissionText(operation, "cancelRunId"), runId, StringComparison.Ordinal)
1051
+ && string.Equals(AdmissionText(operation, "cancelRequestId"), record.RequestId, StringComparison.Ordinal)
1052
+ && string.Equals(AdmissionText(operation, "cancelRequestDigest"), record.RequestDigest, StringComparison.Ordinal)
1053
+ && string.Equals(AdmissionText(operation, "cancelRootRequestDigest"), rootRequestDigest, StringComparison.Ordinal)
1054
+ ? DedicatedCancelMarkerOwnership.ExactOwner
1055
+ : DedicatedCancelMarkerOwnership.Blocked;
1056
+ }
1057
+ if (string.Equals(repairEffectState, "not_invoked", StringComparison.Ordinal))
1058
+ {
1059
+ object exact;
1060
+ object selected;
1061
+ object stored;
1062
+ return !AdmissionBool(operation, "cancelRequested")
1063
+ && string.Equals(AdmissionText(operation, "cancelEffectState"), "not_invoked", StringComparison.Ordinal)
1064
+ && string.IsNullOrEmpty(AdmissionText(operation, "cancelRequestId"))
1065
+ && operation.TryGetValue("cancelTerminalEnvelope", out stored)
1066
+ && state.TryGetTerminal("test-cancel", record.RequestId, record.RequestDigest, out exact)
1067
+ && state.TryGetTerminalBySelector("test-cancel", record.RequestId, rootRequestDigest, out selected)
1068
+ && string.Equals(UnityCliSerializer.Serialize(exact), UnityCliSerializer.Serialize(selected), StringComparison.Ordinal)
1069
+ && string.Equals(UnityCliSerializer.Serialize(exact), UnityCliSerializer.Serialize(stored), StringComparison.Ordinal)
1070
+ ? DedicatedCancelMarkerOwnership.ExactOwner
1071
+ : DedicatedCancelMarkerOwnership.Blocked;
1072
+ }
1073
+ if (!string.Equals(AdmissionText(operation, "cancelRunId"), runId, StringComparison.Ordinal)
1074
+ || !string.Equals(AdmissionText(operation, "cancelRequestId"), record.RequestId, StringComparison.Ordinal)
1075
+ || !string.Equals(AdmissionText(operation, "cancelRequestDigest"), record.RequestDigest, StringComparison.Ordinal)
1076
+ || !string.Equals(AdmissionText(operation, "cancelRootRequestDigest"), rootRequestDigest, StringComparison.Ordinal))
1077
+ return DedicatedCancelMarkerOwnership.Blocked;
1078
+ return DedicatedCancelMarkerOwnership.ExactOwner;
1079
+ }
1080
+ catch { return DedicatedCancelMarkerOwnership.Blocked; }
1081
+ }
1082
+
1083
+ internal static string DedicatedCancelMarkerOwnershipForTests(BridgeQueueState state, UnityCliAdmissionPool.AdmissionRecord record)
1084
+ {
1085
+ return DedicatedCancelMarkerOwnershipForState(state, record).ToString();
1086
+ }
1087
+
1088
+ private static AdmissionOwnerIdentity TransitionWithoutMarker(AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState target)
1089
+ {
1090
+ if (!admissionPool.PrepareOwnerTransfer(ToPoolIdentity(identity), target)) throw new InvalidOperationException("Admission recovery transfer intent failed.");
1091
+ PersistAdmissionPoolState();
1092
+ UnityCliAdmissionPool.ConsumedIdentity successor;
1093
+ if (!admissionPool.CommitOwnerTransfer(ToPoolIdentity(identity), target, out successor)) throw new InvalidOperationException("Admission recovery transfer commit failed.");
1094
+ PersistAdmissionPoolState();
1095
+ return ToAdmissionOwner(successor, identity.RequestDigest);
1096
+ }
1097
+
1098
+ private static bool TryFindExactDurableBusinessRoot(UnityCliAdmissionPool.AdmissionRecord record, out string markerScope, out UnityCliAdmissionPool.AdmissionOwnerState intendedOwner)
1099
+ {
1100
+ markerScope = null;
1101
+ intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
1102
+ foreach (var scope in AdmissionDurableScopes)
1103
+ {
1104
+ if (scope == "test-cancel") continue;
1105
+ var state = UnityCliActiveOperation.Load(scope);
1106
+ if (state == null || !string.Equals(AdmissionText(state, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) continue;
1107
+ if (string.Equals(AdmissionText(state, "requestId"), record.RequestId, StringComparison.Ordinal)
1108
+ && string.Equals(AdmissionText(state, "requestDigest"), record.RequestDigest, StringComparison.Ordinal))
1109
+ {
1110
+ markerScope = scope;
1111
+ intendedOwner = scope == "direct-mutation" ? UnityCliAdmissionPool.AdmissionOwnerState.Recovery : UnityCliAdmissionPool.AdmissionOwnerState.Coordinator;
1112
+ return true;
1113
+ }
1114
+ }
1115
+ return false;
1116
+ }
1117
+
1118
+ private static bool TryFindExactAdmissionMarker(UnityCliAdmissionPool.AdmissionRecord record, out string markerScope, out Dictionary<string, object> marker)
1119
+ {
1120
+ markerScope = null;
1121
+ marker = null;
1122
+ foreach (var scope in AdmissionDurableScopes)
1123
+ {
1124
+ if (scope == "test-cancel") continue;
1125
+ foreach (var candidate in new[] { scope + "-terminal-cleanup", scope + "-pre-effect-cleanup", scope + "-admission-owner" })
1126
+ {
1127
+ if (!UnityCliActiveOperation.Exists(candidate)) continue;
1128
+ var value = UnityCliActiveOperation.Load(candidate);
1129
+ if (!AdmissionMarkerMatchesRecord(value, record)) continue;
1130
+ markerScope = candidate;
1131
+ marker = value;
1132
+ return true;
1133
+ }
1134
+ }
1135
+ return false;
1136
+ }
1137
+
1138
+ private static void ReconcileAdmissionMarker(UnityCliAdmissionPool.AdmissionRecord record, string markerScope, Dictionary<string, object> marker)
1139
+ {
1140
+ var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
1141
+ var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
1142
+ var current = RecordIdentity(record);
1143
+ if (current.OwnerState != intended)
1144
+ {
1145
+ if (current.OwnerState != source || !admissionPool.PrepareOwnerTransfer(ToPoolIdentity(current), intended))
1146
+ throw new InvalidOperationException("Admission marker source does not match the restored owner.");
1147
+ PersistAdmissionPoolState();
1148
+ UnityCliAdmissionPool.ConsumedIdentity successor;
1149
+ if (!admissionPool.CommitOwnerTransfer(ToPoolIdentity(current), intended, out successor))
1150
+ throw new InvalidOperationException("Admission marker transfer cannot be repaired.");
1151
+ current = ToAdmissionOwner(successor, AdmissionText(marker, "admissionRequestDigest"));
1152
+ }
1153
+ current.TopRequestDigest = AdmissionText(marker, "requestDigest");
1154
+ current.EditorSessionId = AdmissionText(marker, "editorSessionId");
1155
+ marker["admissionTransferCommitted"] = true;
1156
+ UnityCliActiveOperation.Save(markerScope, marker);
1157
+ PersistAdmissionPoolState();
1158
+ }
1159
+
1160
+ private static bool AdmissionMarkerCanAcquireOwner(IDictionary<string, object> marker, AdmissionOwnerIdentity identity)
1161
+ {
1162
+ return marker != null && identity != null && string.IsNullOrEmpty(AdmissionText(marker, "admissionRootFingerprint"))
1163
+ && string.Equals(AdmissionText(marker, "requestId"), identity.RequestId, StringComparison.Ordinal)
1164
+ && string.Equals(AdmissionText(marker, "requestDigest"), AdmissionTopDigest(identity), StringComparison.Ordinal)
1165
+ && string.Equals(AdmissionText(marker, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal);
1166
+ }
1167
+
1168
+ private static bool AdmissionMarkerMatchesIdentity(IDictionary<string, object> marker, AdmissionOwnerIdentity identity)
1169
+ {
1170
+ return AdmissionMarkerIdentityIsValid(marker, identity, AdmissionTopDigest(identity));
1171
+ }
1172
+
1173
+ /// <summary>marker与pool snapshot只按完整immutable identity及合法三阶段状态匹配。</summary>
1174
+ private static bool AdmissionMarkerMatchesRecord(IDictionary<string, object> marker, UnityCliAdmissionPool.AdmissionRecord record)
1175
+ {
1176
+ if (marker == null || record == null || !AdmissionDigestIsValid(AdmissionText(marker, "requestDigest"))) return false;
1177
+ var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
1178
+ var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
1179
+ object committedValue;
1180
+ if (!marker.TryGetValue("admissionTransferCommitted", out committedValue) || !(committedValue is bool)
1181
+ || source == UnityCliAdmissionPool.AdmissionOwnerState.Granted || intended == UnityCliAdmissionPool.AdmissionOwnerState.Granted
1182
+ || !Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), source)
1183
+ || !Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), intended)
1184
+ || !UnityCliAdmissionPool.IsLegalOwnerTransfer(source, intended)) return false;
1185
+ if (!string.Equals(AdmissionText(marker, "requestId"), record.RequestId, StringComparison.Ordinal)
1186
+ || !string.Equals(AdmissionText(marker, "admissionRequestDigest"), record.RequestDigest, StringComparison.Ordinal)
1187
+ || !string.Equals(AdmissionText(marker, "admissionRootFingerprint"), record.RootFingerprint, StringComparison.Ordinal)
1188
+ || !string.Equals(AdmissionText(marker, "admissionToken"), record.Token, StringComparison.Ordinal)
1189
+ || !string.Equals(AdmissionText(marker, "admissionSessionId"), record.SessionId, StringComparison.Ordinal)
1190
+ || !string.Equals(AdmissionText(marker, "admissionListenerId"), record.ListenerId, StringComparison.Ordinal)
1191
+ || AdmissionLong(marker, "admissionListenerGeneration") != record.ListenerGeneration
1192
+ || AdmissionLong(marker, "admissionExecutionDeadlineMs") != record.ExecutionDeadlineMs
1193
+ || !string.Equals(AdmissionText(marker, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) return false;
1194
+ var sourceCommitted = record.TransferCommitted && record.OwnerState == source && record.IntendedOwnerState == source;
1195
+ var sourcePending = !record.TransferCommitted && record.OwnerState == source && record.IntendedOwnerState == intended;
1196
+ var intendedCommitted = record.TransferCommitted && record.OwnerState == intended && record.IntendedOwnerState == intended;
1197
+ return sourceCommitted || sourcePending || intendedCommitted;
1198
+ }
1199
+
1200
+ private static bool AdmissionMarkerIdentityIsValid(IDictionary<string, object> marker, AdmissionOwnerIdentity identity, string topRequestDigest)
1201
+ {
1202
+ if (!AdmissionMarkerHasImmutableIdentity(marker, identity, topRequestDigest)) return false;
1203
+ var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
1204
+ var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
1205
+ return identity.OwnerState == source || identity.OwnerState == intended;
1206
+ }
1207
+
1208
+ private static bool AdmissionMarkerHasImmutableIdentity(IDictionary<string, object> marker, AdmissionOwnerIdentity identity, string topRequestDigest)
1209
+ {
1210
+ if (marker == null || identity == null || !AdmissionDigestIsValid(topRequestDigest)
1211
+ || !AdmissionDigestIsValid(identity.RequestDigest)
1212
+ || !Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), identity.OwnerState)) return false;
1213
+ var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
1214
+ var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
1215
+ object committed;
1216
+ return marker.TryGetValue("admissionTransferCommitted", out committed) && committed is bool
1217
+ && source != UnityCliAdmissionPool.AdmissionOwnerState.Granted
1218
+ && intended != UnityCliAdmissionPool.AdmissionOwnerState.Granted
1219
+ && Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), source)
1220
+ && Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), intended)
1221
+ && UnityCliAdmissionPool.IsLegalOwnerTransfer(source, intended)
1222
+ && string.Equals(AdmissionText(marker, "requestId"), identity.RequestId, StringComparison.Ordinal)
1223
+ && string.Equals(AdmissionText(marker, "requestDigest"), topRequestDigest, StringComparison.Ordinal)
1224
+ && string.Equals(AdmissionText(marker, "admissionRequestDigest"), identity.RequestDigest, StringComparison.Ordinal)
1225
+ && string.Equals(AdmissionText(marker, "admissionRootFingerprint"), identity.RootFingerprint, StringComparison.Ordinal)
1226
+ && string.Equals(AdmissionText(marker, "admissionToken"), identity.Token, StringComparison.Ordinal)
1227
+ && string.Equals(AdmissionText(marker, "admissionSessionId"), identity.SessionId, StringComparison.Ordinal)
1228
+ && AdmissionLong(marker, "admissionExecutionDeadlineMs") == identity.ExecutionDeadlineMs
1229
+ && string.Equals(AdmissionText(marker, "editorSessionId"), identity.EditorSessionId ?? EditorSessionId(), StringComparison.Ordinal);
1230
+ }
1231
+
1232
+ /// <summary>pool删除前先落固定有界completion intent;该intent只记录identity,不重新执行业务effect。</summary>
1233
+ internal static bool PersistAdmissionCompletionIntent(BridgeQueueState state, AdmissionOwnerIdentity identity)
1234
+ {
1235
+ if (state == null || identity == null) return false;
1236
+ return state.PersistIntent(identity);
1237
+ }
1238
+
1239
+ internal static bool HasAdmissionCompletionIntent(BridgeQueueState state, AdmissionOwnerIdentity identity)
1240
+ {
1241
+ if (state == null || identity == null) return false;
1242
+ return state.HasIntent(identity);
1243
+ }
1244
+
1245
+ internal static bool ClearAdmissionCompletionIntent(BridgeQueueState state, AdmissionOwnerIdentity identity)
1246
+ {
1247
+ if (state == null || identity == null) return false;
1248
+ return state.ClearIntent(identity);
24
1249
  }
25
1250
 
26
- /// <summary>按当前Editor session与项目配置建立唯一准入池,并恢复同启动内的consumed roots。</summary>
27
- private static void InitializeAdmissionPool()
1251
+ private static bool PersistProductionAdmissionCompletionIntent(AdmissionOwnerIdentity identity, string kind)
28
1252
  {
29
- if (admissionPool != null) return;
30
- var settings = UnityCliProjectSettings.Current;
31
- admissionPool = new UnityCliAdmissionPool(InstanceId(), EditorSessionId(), settings.TaskPoolMaxActive, settings.TaskPoolReopenAt);
32
- var serialized = SessionState.GetString(AdmissionSessionStateKey, string.Empty);
33
- if (string.IsNullOrEmpty(serialized)) return;
34
1253
  try
35
1254
  {
36
- var state = AsDictionary(UnityCliSerializer.Parse(serialized));
37
- if (state == null || AdmissionText(state, "editorSessionId") != EditorSessionId()) { SessionState.EraseString(AdmissionSessionStateKey); return; }
38
- var records = new List<UnityCliAdmissionPool.AdmissionRecord>();
39
- object rawRecords;
40
- var values = state.TryGetValue("records", out rawRecords) ? rawRecords as IEnumerable : null;
41
- if (values == null) throw new InvalidOperationException("Admission SessionState records are missing.");
42
- foreach (var value in values)
1255
+ return WithAdmissionPublicationLock(() =>
43
1256
  {
44
- var record = AsDictionary(value);
45
- if (record == null) throw new InvalidOperationException("Admission SessionState record is invalid.");
46
- records.Add(new UnityCliAdmissionPool.AdmissionRecord
47
- {
48
- InstanceId = AdmissionText(record, "instanceId"),
49
- SessionId = AdmissionText(record, "sessionId"),
50
- RequestId = AdmissionText(record, "requestId"),
51
- RootFingerprint = AdmissionText(record, "rootFingerprint"),
52
- Token = AdmissionText(record, "token"),
53
- ListenerId = AdmissionText(record, "listenerId"),
54
- ListenerGeneration = AdmissionLong(record, "listenerGeneration"),
55
- GrantExpiresAtMs = AdmissionLong(record, "grantExpiresAtMs"),
56
- ExecutionDeadlineMs = AdmissionLong(record, "executionDeadlineMs"),
57
- State = AdmissionText(record, "state") == "consumed" ? UnityCliAdmissionPool.AdmissionState.Consumed : UnityCliAdmissionPool.AdmissionState.Granted,
58
- OwnerKind = (UnityCliAdmissionPool.AdmissionOwnerKind)AdmissionLong(record, "ownerKind")
59
- });
60
- }
61
- admissionPool.Restore(records, checked((int)AdmissionLong(state, "activeCount")), AdmissionBool(state, "gateClosed"), RequiredText(state, "recoveryDigest", 64), checked((int)AdmissionLong(state, "configuredMaxActive")), checked((int)AdmissionLong(state, "configuredReopenAt")));
1257
+ var root = LoadAdmissionSettlementLocked(true);
1258
+ var entries = root["entries"] as List<object>;
1259
+ if (entries == null || entries.Cast<object>().Select(AsDictionary).Any(value => value == null)) return false;
1260
+ if (entries.Cast<object>().Select(AsDictionary).Any(value => CompletionIntentMatches(value, identity))) return true;
1261
+ if (entries.Count >= 256) return false;
1262
+ entries.Add(CompletionIntent(identity, kind));
1263
+ root["publicationRevision"] = checked(AdmissionLong(root, "publicationRevision") + 1L);
1264
+ UnityCliActiveOperation.Save(AdmissionSettlementScope, root);
1265
+ var confirmed = LoadAdmissionSettlementLocked(false);
1266
+ return confirmed != null && CompletionIntentEntries(confirmed).Any(value => CompletionIntentMatches(value, identity));
1267
+ });
62
1268
  }
63
- catch (Exception exception)
1269
+ catch { return false; }
1270
+ }
1271
+
1272
+ private static bool HasProductionAdmissionCompletionIntent(AdmissionOwnerIdentity identity)
1273
+ {
1274
+ try
64
1275
  {
65
- admissionPool.BlockRecovery();
66
- SessionState.EraseString(AdmissionSessionStateKey);
67
- WriteLog("admission_restore_failed " + exception.GetType().Name + ": " + exception.Message);
1276
+ return WithAdmissionPublicationLock(() =>
1277
+ {
1278
+ var root = LoadAdmissionSettlementLocked(false);
1279
+ return root != null && CompletionIntentEntries(root).Any(value => CompletionIntentMatches(value, identity));
1280
+ });
68
1281
  }
1282
+ catch { return false; }
69
1283
  }
70
1284
 
71
- /// <summary>reload前撤销未消费grant,只持久化同Editor启动内的有界consumed identity。</summary>
72
- private static void SaveAdmissionPoolBeforeReload()
1285
+ private static bool ClearProductionAdmissionCompletionIntent(AdmissionOwnerIdentity identity)
73
1286
  {
74
- if (admissionPool == null) return;
75
- admissionPool.RevokeUnconsumed(ListenerId, CurrentListenerGeneration());
76
- var snapshot = admissionPool.Capture();
77
- var records = snapshot.Records.Where(record => record.Consumed).Select(record => (object)new Dictionary<string, object>
78
- {
79
- { "instanceId", record.InstanceId }, { "sessionId", record.SessionId },
80
- { "requestId", record.RequestId }, { "rootFingerprint", record.RootFingerprint }, { "token", record.Token },
81
- { "listenerId", record.ListenerId }, { "listenerGeneration", record.ListenerGeneration },
82
- { "grantExpiresAtMs", record.GrantExpiresAtMs }, { "executionDeadlineMs", record.ExecutionDeadlineMs }, { "state", "consumed" }, { "ownerKind", (long)record.OwnerKind }
83
- }).ToList();
84
- SessionState.SetString(AdmissionSessionStateKey, UnityCliSerializer.Serialize(new Dictionary<string, object>
85
- {
86
- { "editorSessionId", EditorSessionId() }, { "gateClosed", snapshot.GateClosed }, { "records", records },
87
- { "activeCount", (long)snapshot.Active }, { "recoveryDigest", snapshot.RecoveryDigest },
88
- { "configuredMaxActive", (long)snapshot.ConfiguredMaxActive }, { "configuredReopenAt", (long)snapshot.ReopenAt }
89
- }));
1287
+ try
1288
+ {
1289
+ return WithAdmissionPublicationLock(() =>
1290
+ {
1291
+ var root = LoadAdmissionSettlementLocked(false);
1292
+ if (root == null) return true;
1293
+ var entries = root["entries"] as List<object>;
1294
+ if (entries == null || entries.Cast<object>().Select(AsDictionary).Any(value => value == null)) return false;
1295
+ entries.RemoveAll(value => CompletionIntentMatches(AsDictionary(value), identity));
1296
+ if (entries.Count == 0)
1297
+ {
1298
+ UnityCliActiveOperation.Clear(AdmissionSettlementScope);
1299
+ return UnityCliActiveOperation.Load(AdmissionSettlementScope) == null;
1300
+ }
1301
+ root["publicationRevision"] = checked(AdmissionLong(root, "publicationRevision") + 1L);
1302
+ UnityCliActiveOperation.Save(AdmissionSettlementScope, root);
1303
+ var confirmed = LoadAdmissionSettlementLocked(false);
1304
+ return confirmed != null && !CompletionIntentEntries(confirmed).Any(value => CompletionIntentMatches(value, identity));
1305
+ });
1306
+ }
1307
+ catch { return false; }
90
1308
  }
91
1309
 
92
- private static void ShutdownAdmissionPool()
1310
+ private static Dictionary<string, object> LoadProductionAdmissionSettlement()
93
1311
  {
94
- if (admissionPool != null) admissionPool.Reset();
95
- admissionPool = null;
96
- SessionState.EraseString(AdmissionSessionStateKey);
1312
+ return WithAdmissionPublicationLock(() => LoadAdmissionSettlementLocked(false));
97
1313
  }
98
1314
 
99
- /// <summary>HTTP后台线程原子发grant;不进入主线程或transaction log。</summary>
100
- private static string ExecuteAdmissionAcquire(byte[] rawBody, out string token)
1315
+ private static Dictionary<string, object> LoadAdmissionSettlementLocked(bool create)
101
1316
  {
102
- token = null;
103
- var value = StrictAdmissionObject(rawBody, "acquire", "protocolVersion", "requestId", "instanceId", "target", "command", "rootFingerprint");
104
- var requestId = RequiredText(value, "requestId", 200);
105
- if (AdmissionLong(value, "protocolVersion") != 1L || AdmissionText(value, "instanceId") != InstanceId() || AdmissionText(value, "target") != "editor")
106
- throw new BridgeCommandException("E_INSTANCE_IDENTITY_MISMATCH", "Admission acquire identity does not match this Editor session.");
107
- RequiredText(value, "command", 256);
108
- var fingerprint = RequiredText(value, "rootFingerprint", 64);
109
- EnsureAdmissionPool();
110
- var result = admissionPool.Acquire(requestId, fingerprint, ListenerId, CurrentListenerGeneration());
111
- if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.Invalid)
112
- throw new BridgeCommandException("E_SCHEMA_INVALID", "Admission acquire fields are invalid.");
113
- if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.IdentityMismatch)
114
- throw new BridgeCommandException("E_REQUEST_ID_REUSED", "Admission requestId is already bound to different business input.");
115
- if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.AlreadyConsumed)
116
- throw new BridgeCommandException("E_ADMISSION_ALREADY_CONSUMED", "Admission root is already consumed and must use exact attach or terminal recovery.");
117
- var granted = result.Outcome == UnityCliAdmissionPool.AcquireOutcome.Granted;
118
- if (granted) token = result.Token;
119
- if (granted) WriteLog((result.NewlyGranted ? "admission_granted" : "admission_attached") + " requestId=" + requestId + " active=" + admissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
120
- return UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Success(requestId, new Dictionary<string, object>
1317
+ var root = UnityCliActiveOperation.Load(AdmissionSettlementScope);
1318
+ if (root == null)
121
1319
  {
122
- { "protocolVersion", 1L }, { "granted", granted },
123
- { "grantExpiresInMs", (long)(granted ? result.GrantExpiresInMs : 0) },
124
- { "retryAfterMs", (long)result.RetryAfterMs }
125
- }));
1320
+ return create
1321
+ ? new Dictionary<string, object> { { "editorSessionId", EditorSessionId() }, { "publicationRevision", 0L }, { "entries", new List<object>() } }
1322
+ : null;
1323
+ }
1324
+ if (!string.Equals(AdmissionText(root, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal))
1325
+ {
1326
+ return create
1327
+ ? new Dictionary<string, object> { { "editorSessionId", EditorSessionId() }, { "publicationRevision", 0L }, { "entries", new List<object>() } }
1328
+ : null;
1329
+ }
1330
+ if (AdmissionLong(root, "publicationRevision") < 1L || !(root.ContainsKey("entries") && root["entries"] is List<object>))
1331
+ throw new InvalidDataException("Admission completion journal publication is invalid.");
1332
+ return root;
126
1333
  }
127
1334
 
128
- private static string ExecuteAdmissionRevoke(byte[] rawBody, HttpListenerRequest httpRequest)
1335
+ private static IEnumerable<IDictionary<string, object>> CompletionIntentEntries(Dictionary<string, object> root)
129
1336
  {
130
- var value = StrictAdmissionObject(rawBody, "revoke", "protocolVersion", "requestId", "instanceId", "target", "rootFingerprint");
131
- var requestId = RequiredText(value, "requestId", 200);
132
- if (AdmissionLong(value, "protocolVersion") != 1L || AdmissionText(value, "instanceId") != InstanceId() || AdmissionText(value, "target") != "editor")
133
- throw new BridgeCommandException("E_INSTANCE_IDENTITY_MISMATCH", "Admission revoke identity does not match this Editor session.");
134
- EnsureAdmissionPool();
135
- var fingerprint = RequiredText(value, "rootFingerprint", 64);
136
- var revoked = admissionPool.Revoke(requestId, fingerprint, RequiredReleaseToken(httpRequest, requestId, fingerprint));
137
- if (!revoked) throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission revoke identity does not match an unconsumed root.");
138
- WriteLog("admission_revoked requestId=" + requestId + " active=" + admissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
139
- return UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Success(requestId, new Dictionary<string, object> { { "revoked", true } }));
1337
+ var entries = root == null ? null : root["entries"] as List<object>;
1338
+ if (entries == null || entries.Count > 256) throw new InvalidDataException("Admission completion journal entries are invalid.");
1339
+ foreach (var raw in entries)
1340
+ {
1341
+ var entry = AsDictionary(raw);
1342
+ if (entry == null) throw new InvalidDataException("Admission completion journal entry is invalid.");
1343
+ yield return entry;
1344
+ }
140
1345
  }
141
1346
 
142
- /// <summary>任何带admission headers的请求都必须授权;无headers只允许内部控制面。</summary>
143
- private static AdmissionOwnerIdentity AuthorizeAdmission(HttpListenerRequest httpRequest, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes)
1347
+ private static Dictionary<string, object> CompletionIntent(AdmissionOwnerIdentity identity, string kind)
144
1348
  {
145
- var rootId = httpRequest.Headers["x-unity-cli-admission-root-id"];
146
- var fingerprint = httpRequest.Headers["x-unity-cli-admission-fingerprint"];
147
- var token = httpRequest.Headers["x-unity-cli-admission-token"];
148
- var deadlineText = httpRequest.Headers["x-unity-cli-admission-deadline-utc"];
149
- var any = !string.IsNullOrEmpty(rootId) || !string.IsNullOrEmpty(fingerprint) || !string.IsNullOrEmpty(token) || !string.IsNullOrEmpty(deadlineText);
150
- if (!any)
151
- {
152
- if (AdmissionBypassCommand(request.Command)) return null;
153
- throw new BridgeCommandException("E_ADMISSION_REQUIRED", "A valid Unity task-pool admission is required before this business request can enter the queue.");
154
- }
155
- if (string.IsNullOrEmpty(rootId) || string.IsNullOrEmpty(fingerprint) || string.IsNullOrEmpty(deadlineText))
156
- throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission root headers are incomplete.");
157
- if (!string.Equals(rootId, request.RequestId, StringComparison.Ordinal))
158
- throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission root requestId does not match the Bridge request body.");
159
- DateTimeOffset deadline;
160
- if (!DateTimeOffset.TryParse(deadlineText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out deadline))
161
- throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission execution deadline is invalid.");
162
- EnsureAdmissionPool();
163
- var deadlineMs = deadline.ToUnixTimeMilliseconds();
164
- if (!string.IsNullOrEmpty(token))
1349
+ return new Dictionary<string, object>
165
1350
  {
166
- var outcome = admissionPool.Authorize(rootId, fingerprint, token, ListenerId, CurrentListenerGeneration(), deadlineMs);
167
- if (outcome == UnityCliAdmissionPool.AuthorizeOutcome.Authorized) return CaptureAdmissionOwner(rootId);
168
- throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission token is missing, expired, reused, or bound to another root.");
169
- }
170
- if (admissionPool.AttachConsumed(rootId, fingerprint, ListenerId, CurrentListenerGeneration(), deadlineMs)) return CaptureAdmissionOwner(rootId);
171
- if (AdmissionRecoveryCommand(request.Command) && admissionPool.RecoverConsumed(rootId, fingerprint, ListenerId, CurrentListenerGeneration(), deadlineMs)) return CaptureAdmissionOwner(rootId);
172
- if (AdmissionHasExactTerminal(request, rawRequestBytes)) return null;
173
- throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Consumed admission root cannot attach to this listener generation.");
1351
+ { "requestId", identity.RequestId }, { "requestDigest", AdmissionTopDigest(identity) },
1352
+ { "kind", kind ?? "admission-completion" }, { "admissionRequestDigest", identity.RequestDigest },
1353
+ { "admissionRootFingerprint", identity.RootFingerprint }, { "admissionToken", identity.Token },
1354
+ { "admissionSessionId", identity.SessionId }, { "admissionListenerId", identity.ListenerId },
1355
+ { "admissionListenerGeneration", identity.ListenerGeneration }, { "admissionExecutionDeadlineMs", identity.ExecutionDeadlineMs },
1356
+ { "admissionOwnerState", (long)identity.OwnerState }, { "editorSessionId", identity.EditorSessionId ?? EditorSessionId() }
1357
+ };
174
1358
  }
175
1359
 
176
- private static AdmissionOwnerIdentity CaptureAdmissionOwner(string requestId)
1360
+ private static bool CompletionIntentMatches(IDictionary<string, object> entry, AdmissionOwnerIdentity identity)
177
1361
  {
178
- UnityCliAdmissionPool.ConsumedIdentity identity;
179
- if (admissionPool == null || !admissionPool.TryGetConsumedIdentity(requestId, out identity))
180
- throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner identity cannot be frozen after consumption.");
181
- return new AdmissionOwnerIdentity { RequestId = identity.RequestId, RootFingerprint = identity.RootFingerprint, Token = identity.Token, SessionId = identity.SessionId, OwnerKind = identity.OwnerKind };
1362
+ return entry != null && identity != null
1363
+ && string.Equals(AdmissionText(entry, "requestId"), identity.RequestId, StringComparison.Ordinal)
1364
+ && string.Equals(AdmissionText(entry, "requestDigest"), AdmissionTopDigest(identity), StringComparison.Ordinal)
1365
+ && string.Equals(AdmissionText(entry, "admissionRequestDigest"), identity.RequestDigest, StringComparison.Ordinal)
1366
+ && string.Equals(AdmissionText(entry, "admissionRootFingerprint"), identity.RootFingerprint, StringComparison.Ordinal)
1367
+ && string.Equals(AdmissionText(entry, "admissionToken"), identity.Token, StringComparison.Ordinal)
1368
+ && string.Equals(AdmissionText(entry, "admissionSessionId"), identity.SessionId, StringComparison.Ordinal)
1369
+ && string.Equals(AdmissionText(entry, "admissionListenerId"), identity.ListenerId, StringComparison.Ordinal)
1370
+ && AdmissionLong(entry, "admissionListenerGeneration") == identity.ListenerGeneration
1371
+ && AdmissionLong(entry, "admissionExecutionDeadlineMs") == identity.ExecutionDeadlineMs
1372
+ && AdmissionLong(entry, "admissionOwnerState") == (long)identity.OwnerState
1373
+ && string.Equals(AdmissionText(entry, "editorSessionId"), identity.EditorSessionId ?? EditorSessionId(), StringComparison.Ordinal);
182
1374
  }
183
1375
 
184
- internal static bool CompleteAdmissionRoot(AdmissionOwnerIdentity identity)
1376
+ /// <summary>accepted durable transfer失败时只使用WorkItem冻结的消费角色与cancel候选建立可执行marker。</summary>
1377
+ private static AdmissionTransferResult SettleAdmissionAfterWorkItem(BridgeQueueState state, BridgeWorkItem item)
185
1378
  {
186
- if (admissionPool == null || identity == null) return false;
1379
+ var identity = item == null ? null : item.AdmissionOwner;
1380
+ var command = item == null || item.Request == null ? null : item.Request.Command;
1381
+ var response = item == null ? null : item.Response;
1382
+ if (identity == null || string.IsNullOrEmpty(identity.RequestId)) return AdmissionTransferResult.NotAccepted;
1383
+ var transfer = TransferAdmissionAfterAcceptedWork(state, item, identity, command, response);
1384
+ if (transfer == AdmissionTransferResult.TransferPending)
1385
+ transfer = PersistWorkItemRecoveryAfterTransferFailure(state, item, identity, command, response);
1386
+ if (transfer == AdmissionTransferResult.TransferPending) return transfer;
1387
+ var pool = state.AdmissionPool;
187
1388
  UnityCliAdmissionPool.ConsumedIdentity current;
188
- var existed = admissionPool.TryGetConsumedIdentity(identity.RequestId, out current);
189
- var completed = admissionPool.CompleteTrusted(identity.RequestId, identity.RootFingerprint, identity.Token, identity.SessionId, identity.OwnerKind);
190
- if (existed && completed) WriteLog("admission_terminal requestId=" + identity.RequestId + " active=" + admissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
191
- return completed;
1389
+ if (pool == null || !pool.TryGetConsumedIdentity(identity.RequestId, out current)) return transfer;
1390
+ AdmissionOwnerIdentity frozen;
1391
+ try { frozen = ToAdmissionOwner(current, identity.RequestDigest); }
1392
+ catch (BridgeCommandException) { return AdmissionTransferResult.TransferPending; }
1393
+ if (!SameAdmissionImmutableIdentity(frozen, identity)) return AdmissionTransferResult.TransferPending;
1394
+ identity.OwnerState = frozen.OwnerState;
1395
+ return transfer;
192
1396
  }
193
1397
 
194
- internal static void AddAdmissionOwnerFields(IDictionary<string, object> state, string requestId, UnityCliAdmissionPool.AdmissionOwnerKind ownerKind = UnityCliAdmissionPool.AdmissionOwnerKind.Coordinator) { AddAdmissionOwnerFields(state, CaptureAdmissionOwner(requestId), ownerKind); }
1398
+ /// <summary>root观察不确定时用队列冻结身份建立可执行Recovery marker;候选缺失时继续保留WorkItem。</summary>
1399
+ private static AdmissionTransferResult PersistWorkItemRecoveryAfterTransferFailure(BridgeQueueState state, BridgeWorkItem item, AdmissionOwnerIdentity identity, string command, string response)
1400
+ {
1401
+ if (identity == null || (!AdmissionDurableRootCommand(command) && command != "project-tool.run" && command != "code.exec"))
1402
+ return AdmissionTransferResult.TransferPending;
1403
+ try
1404
+ {
1405
+ var current = CaptureAdmissionOwner(state, identity.RequestId, identity.RequestDigest);
1406
+ if (current.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem)
1407
+ return TryConfirmDurableTransferredOwner(state, identity, command, current)
1408
+ ? AdmissionTransferResult.Transferred
1409
+ : AdmissionTransferResult.TransferPending;
1410
+ string markerScope;
1411
+ string rootScope;
1412
+ AdmissionOwnerScopes(command, out markerScope, out rootScope);
1413
+ var cancelCandidate = command == "internal.test.run.cancel" ? item.CaptureCancelRecoveryCandidate() : null;
1414
+ current.TopRequestDigest = cancelCandidate != null && AdmissionDigestIsValid(cancelCandidate.RootRequestDigest)
1415
+ ? cancelCandidate.RootRequestDigest
1416
+ : AdmissionAcceptedTopDigest(state, response, current, command);
1417
+ var marker = new Dictionary<string, object>
1418
+ {
1419
+ { "requestId", current.RequestId }, { "requestDigest", AdmissionTopDigest(current) }, { "editorSessionId", EditorSessionId() }
1420
+ };
1421
+ var cancelOwner = UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
1422
+ if (command == "internal.test.run.cancel"
1423
+ && !TryAddCancelAdmissionRootIdentity(state, marker, current, cancelCandidate, out cancelOwner))
1424
+ return AdmissionTransferResult.TransferPending;
1425
+ var recovered = PersistAdmissionOwnerTransfer(state, markerScope + "-admission-owner", marker, current,
1426
+ command == "internal.test.run.cancel" ? cancelOwner : UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
1427
+ identity.OwnerState = recovered.OwnerState;
1428
+ identity.TopRequestDigest = recovered.TopRequestDigest;
1429
+ return AdmissionTransferResult.Transferred;
1430
+ }
1431
+ catch (Exception exception)
1432
+ {
1433
+ state.Sinks.WriteLog("admission_workitem_recovery_pending requestId=" + identity.RequestId + " " + exception.GetType().Name + ": " + exception.Message);
1434
+ return AdmissionTransferResult.TransferPending;
1435
+ }
1436
+ }
195
1437
 
196
- internal static void AddAdmissionOwnerFields(IDictionary<string, object> state, AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerKind ownerKind = UnityCliAdmissionPool.AdmissionOwnerKind.Coordinator)
1438
+ /// <summary>accepted coordinator/cancel及仍持有exact direct durable root的请求只通过固定marker移交owner。</summary>
1439
+ private static AdmissionTransferResult TransferAdmissionAfterAcceptedWork(BridgeQueueState state, BridgeWorkItem item, AdmissionOwnerIdentity identity, string command, string response)
197
1440
  {
198
- if (state == null || identity == null) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner marker identity is unavailable.");
199
- if (identity.OwnerKind != ownerKind)
1441
+ if (identity == null) return AdmissionTransferResult.NotAccepted;
1442
+ var directMutation = command == "project-tool.run" || command == "code.exec";
1443
+ if (!AdmissionDurableRootCommand(command) && !directMutation) return AdmissionTransferResult.NotAccepted;
1444
+ string markerScope;
1445
+ string rootScope;
1446
+ AdmissionOwnerScopes(command, out markerScope, out rootScope);
1447
+ try
1448
+ {
1449
+ var current = CaptureAdmissionOwner(state, identity.RequestId, identity.RequestDigest);
1450
+ if (current.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem)
1451
+ return TryConfirmDurableTransferredOwner(state, identity, command, current)
1452
+ ? AdmissionTransferResult.Transferred
1453
+ : AdmissionTransferResult.TransferPending;
1454
+ bool durable;
1455
+ var readable = TryHasExactDurableBusinessRoot(state, rootScope, command, identity, out durable);
1456
+ if (!readable) return AdmissionTransferResult.TransferPending;
1457
+ var accepted = AdmissionResponseIsCoordinatorAccepted(response) || durable;
1458
+ if (!accepted) return AdmissionTransferResult.NotAccepted;
1459
+ var cancelCandidate = command == "internal.test.run.cancel" ? item.CaptureCancelRecoveryCandidate() : null;
1460
+ identity.TopRequestDigest = cancelCandidate != null && AdmissionDigestIsValid(cancelCandidate.RootRequestDigest)
1461
+ ? cancelCandidate.RootRequestDigest
1462
+ : AdmissionAcceptedTopDigest(state, response, identity, command);
1463
+ var intended = durable
1464
+ ? UnityCliAdmissionPool.AdmissionOwnerState.Coordinator
1465
+ : UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
1466
+ if (directMutation) intended = UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
1467
+ var marker = new Dictionary<string, object>
1468
+ {
1469
+ { "requestId", identity.RequestId }, { "requestDigest", AdmissionTopDigest(identity) }, { "editorSessionId", EditorSessionId() }
1470
+ };
1471
+ if (command == "internal.test.run.cancel"
1472
+ && !TryAddCancelAdmissionRootIdentity(state, marker, identity, cancelCandidate, out intended))
1473
+ return AdmissionTransferResult.TransferPending;
1474
+ var updated = PersistAdmissionOwnerTransfer(state, markerScope + "-admission-owner", marker, identity, intended);
1475
+ identity.OwnerState = updated.OwnerState;
1476
+ identity.TopRequestDigest = updated.TopRequestDigest;
1477
+ return AdmissionTransferResult.Transferred;
1478
+ }
1479
+ catch (Exception exception)
200
1480
  {
201
- if (admissionPool == null || !admissionPool.TransferOwner(identity.RequestId, identity.RootFingerprint, identity.Token, identity.SessionId, ownerKind))
202
- throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner transfer could not be proven.");
203
- identity.OwnerKind = ownerKind;
1481
+ state.Sinks.WriteLog("admission_durable_transfer_pending requestId=" + identity.RequestId + " " + exception.GetType().Name + ": " + exception.Message);
1482
+ return AdmissionTransferResult.TransferPending;
204
1483
  }
205
- state["admissionRootFingerprint"] = identity.RootFingerprint;
206
- state["admissionToken"] = identity.Token;
207
- state["admissionSessionId"] = identity.SessionId;
208
- state["admissionOwnerKind"] = (long)identity.OwnerKind;
209
1484
  }
210
1485
 
211
- internal static AdmissionOwnerIdentity ReadAdmissionOwnerFields(IDictionary<string, object> state, string requestId)
1486
+ /// <summary>pool owner已变化时仍须由exact marker reconcile证明commit与pool persistence均已完成。</summary>
1487
+ private static bool TryConfirmDurableTransferredOwner(BridgeQueueState state, AdmissionOwnerIdentity identity, string command, AdmissionOwnerIdentity current)
212
1488
  {
213
- if (state == null || string.IsNullOrEmpty(requestId)) return null;
214
- return new AdmissionOwnerIdentity
1489
+ if (command == "internal.test.run.cancel")
1490
+ return TryConfirmCancelTransferredOwner(state, identity, current);
1491
+ string markerScope;
1492
+ string rootScope;
1493
+ AdmissionOwnerScopes(command, out markerScope, out rootScope);
1494
+ foreach (var candidate in new[]
215
1495
  {
216
- RequestId = requestId,
217
- RootFingerprint = AdmissionText(state, "admissionRootFingerprint"),
218
- Token = AdmissionText(state, "admissionToken"),
219
- SessionId = AdmissionText(state, "admissionSessionId"),
220
- OwnerKind = (UnityCliAdmissionPool.AdmissionOwnerKind)AdmissionLong(state, "admissionOwnerKind")
221
- };
1496
+ markerScope + "-admission-owner",
1497
+ rootScope + "-terminal-cleanup",
1498
+ rootScope + "-pre-effect-cleanup"
1499
+ }.Distinct(StringComparer.Ordinal))
1500
+ {
1501
+ Dictionary<string, object> marker;
1502
+ try { marker = state.LoadOperation(candidate); }
1503
+ catch { return false; }
1504
+ if (marker == null) continue;
1505
+ current.TopRequestDigest = AdmissionText(marker, "requestDigest");
1506
+ current.EditorSessionId = AdmissionText(marker, "editorSessionId");
1507
+ if (!AdmissionMarkerMatchesIdentity(marker, current)) continue;
1508
+ AdmissionOwnerIdentity reconciled;
1509
+ try { reconciled = ReconcileAdmissionTransferForLiveMarker(state, candidate, marker, current.RequestId, current.RequestDigest); }
1510
+ catch { return false; }
1511
+ if (reconciled == null || reconciled.OwnerState != current.OwnerState) return false;
1512
+ identity.OwnerState = reconciled.OwnerState;
1513
+ identity.TopRequestDigest = reconciled.TopRequestDigest;
1514
+ return true;
1515
+ }
1516
+ return false;
222
1517
  }
223
1518
 
224
- /// <summary>reload恢复后只保留由固定active root证明仍在执行的consumed admission。</summary>
225
- private static void ReconcileRestoredAdmissionRoots()
1519
+ /// <summary>cancel queue settlement只确认cancel owner marker,不进入通用root/lease reconciliation。</summary>
1520
+ private static bool TryConfirmCancelTransferredOwner(BridgeQueueState state, AdmissionOwnerIdentity identity, AdmissionOwnerIdentity current)
226
1521
  {
227
- if (admissionPool == null) return;
228
- foreach (var record in admissionPool.Capture().Records)
1522
+ const string markerScope = "test-cancel-admission-owner";
1523
+ Dictionary<string, object> marker;
1524
+ try { marker = state.LoadOperation(markerScope); }
1525
+ catch { return false; }
1526
+ var rootRequestId = AdmissionText(marker, "rootRequestId");
1527
+ var rootRequestDigest = AdmissionText(marker, "rootRequestDigest");
1528
+ var runId = AdmissionText(marker, "runId");
1529
+ if (!IsSafeId(rootRequestId) || !AdmissionDigestIsValid(rootRequestDigest) || !Guid.TryParse(runId, out _)
1530
+ || !string.Equals(AdmissionText(marker, "requestId"), current.RequestId, StringComparison.Ordinal)
1531
+ || !string.Equals(AdmissionText(marker, "requestDigest"), rootRequestDigest, StringComparison.Ordinal)
1532
+ || !TryReadCancelAdmissionMarkerState(marker, current.RequestId, current.RequestDigest, rootRequestDigest, runId,
1533
+ out _, out _, out _, out _)) return false;
1534
+ current.TopRequestDigest = rootRequestDigest;
1535
+ current.EditorSessionId = AdmissionText(marker, "editorSessionId");
1536
+ if (!AdmissionMarkerMatchesIdentity(marker, current)) return false;
1537
+ var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
1538
+ try
229
1539
  {
230
- bool durable;
231
- if (!TryHasDurableAdmissionRoot(record.RequestId, out durable)) { admissionPool.BlockRecovery(); return; }
232
- if (!durable)
1540
+ var reconciled = current.OwnerState == intended
1541
+ ? current
1542
+ : PersistAdmissionOwnerTransfer(state, markerScope, marker, current, intended);
1543
+ if (current.OwnerState == intended)
233
1544
  {
234
- var identity = new AdmissionOwnerIdentity { RequestId = record.RequestId, RootFingerprint = record.RootFingerprint, Token = record.Token, SessionId = record.SessionId, OwnerKind = record.OwnerKind };
235
- if (identity.OwnerKind == UnityCliAdmissionPool.AdmissionOwnerKind.WorkItem)
236
- {
237
- if (!admissionPool.TransferOwner(identity.RequestId, identity.RootFingerprint, identity.Token, identity.SessionId, UnityCliAdmissionPool.AdmissionOwnerKind.Recovery)) { admissionPool.BlockRecovery(); return; }
238
- identity.OwnerKind = UnityCliAdmissionPool.AdmissionOwnerKind.Recovery;
239
- }
240
- CompleteAdmissionRoot(identity);
1545
+ marker["admissionTransferCommitted"] = true;
1546
+ state.SaveOperation(markerScope, marker);
1547
+ if (!state.PersistPool()) return false;
241
1548
  }
1549
+ identity.OwnerState = reconciled.OwnerState;
1550
+ identity.TopRequestDigest = rootRequestDigest;
1551
+ return true;
242
1552
  }
1553
+ catch { return false; }
243
1554
  }
244
1555
 
245
- private static bool TryHasDurableAdmissionRoot(string requestId, out bool durable)
1556
+ /// <summary>cancel首次转移冻结完整parent、cancel、continuation与repair schema;不可执行的半marker不接管WorkItem。</summary>
1557
+ private static bool TryAddCancelAdmissionRootIdentity(
1558
+ BridgeQueueState state,
1559
+ IDictionary<string, object> marker,
1560
+ AdmissionOwnerIdentity identity,
1561
+ BridgeWorkItem.CancelRecoveryCandidate candidate,
1562
+ out UnityCliAdmissionPool.AdmissionOwnerState intendedOwner)
246
1563
  {
247
- durable = false;
248
- string leaseRequestId;
249
- string leaseDigest;
250
- if (!UnityCliSessionMutationLease.TryGetCurrentOwner(out leaseRequestId, out leaseDigest)) return false;
251
- if (string.Equals(leaseRequestId, requestId, StringComparison.Ordinal)) { durable = true; return true; }
252
-
253
- foreach (var scope in new[] { "compile", "package-resolve", "refresh", "asset-refresh", "package-embed", "test-run", "build", "navmesh-bake", "occlusion-bake", "direct-mutation" })
1564
+ intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
1565
+ try
254
1566
  {
255
- foreach (var stateScope in new[] { scope, scope + "-pre-effect-cleanup", scope + "-terminal-cleanup" })
1567
+ var root = state.LoadOperation("test-run");
1568
+ if (root == null)
1569
+ return TryAddCancelAdmissionIdentityFromTerminalProof(state, marker, identity, candidate);
1570
+ object rawOperation;
1571
+ var operation = root.TryGetValue("testOperation", out rawOperation) ? AsDictionary(rawOperation) : null;
1572
+ var rootRequestId = AdmissionText(root, "requestId");
1573
+ var rootRequestDigest = AdmissionText(root, "requestDigest");
1574
+ var runId = AdmissionText(operation, "cancelRunId");
1575
+ if (!IsSafeId(rootRequestId) || !AdmissionDigestIsValid(rootRequestDigest) || !Guid.TryParse(runId, out _)
1576
+ || !string.Equals(AdmissionText(operation, "cancelRequestId"), identity.RequestId, StringComparison.Ordinal)
1577
+ || !string.Equals(AdmissionText(operation, "cancelRequestDigest"), identity.RequestDigest, StringComparison.Ordinal)
1578
+ || !string.Equals(AdmissionText(operation, "cancelRootRequestDigest"), rootRequestDigest, StringComparison.Ordinal)
1579
+ || !string.Equals(AdmissionTopDigest(identity), rootRequestDigest, StringComparison.Ordinal)) return false;
1580
+ marker["rootRequestId"] = rootRequestId;
1581
+ marker["rootRequestDigest"] = rootRequestDigest;
1582
+ marker["runId"] = runId;
1583
+ marker["cancelRequestId"] = identity.RequestId;
1584
+ marker["cancelRequestDigest"] = identity.RequestDigest;
1585
+ if (candidate == null)
256
1586
  {
257
- try
258
- {
259
- if (!UnityCliActiveOperation.Exists(stateScope)) continue;
260
- var state = UnityCliActiveOperation.Load(stateScope);
261
- object rootSession;
262
- if (state == null || !state.TryGetValue("editorSessionId", out rootSession)) return false;
263
- if (!string.Equals(rootSession as string, EditorSessionId(), StringComparison.Ordinal)) continue;
264
- if (AdmissionStateOwnsRequest(state, requestId)) { durable = true; return true; }
265
- }
266
- catch { return false; }
1587
+ if (!string.Equals(AdmissionText(operation, "cancelEffectState"), "dispatched", StringComparison.Ordinal)
1588
+ || !AdmissionBool(operation, "cancelNeedsContinuation")) return false;
1589
+ marker["cancelContinuationState"] = "dispatched";
1590
+ marker["parentRepairEffectState"] = "dispatched";
1591
+ marker["parentRepairPending"] = false;
1592
+ intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Continuation;
1593
+ return true;
267
1594
  }
1595
+ if (!string.Equals(candidate.RootRequestId, rootRequestId, StringComparison.Ordinal)
1596
+ || !string.Equals(candidate.RootRequestDigest, rootRequestDigest, StringComparison.Ordinal)
1597
+ || !string.Equals(candidate.RunId, runId, StringComparison.Ordinal)
1598
+ || !CancelTerminalEffectState(candidate.EffectState)
1599
+ || !CancelTerminalCandidateIsValid(candidate.Envelope, identity.RequestId)) return false;
1600
+ AddCancelRecoveryCandidateFields(marker, identity, candidate);
1601
+ return true;
268
1602
  }
1603
+ catch { return false; }
1604
+ }
1605
+
1606
+ /// <summary>parent root已清理时,只用WorkItem候选与exact双terminal重建同一恢复marker。</summary>
1607
+ private static bool TryAddCancelAdmissionIdentityFromTerminalProof(
1608
+ BridgeQueueState state,
1609
+ IDictionary<string, object> marker,
1610
+ AdmissionOwnerIdentity identity,
1611
+ BridgeWorkItem.CancelRecoveryCandidate candidate)
1612
+ {
1613
+ if (candidate == null || !IsSafeId(candidate.RootRequestId) || !AdmissionDigestIsValid(candidate.RootRequestDigest)
1614
+ || !Guid.TryParse(candidate.RunId, out _) || !string.Equals(AdmissionTopDigest(identity), candidate.RootRequestDigest, StringComparison.Ordinal)
1615
+ || (candidate.EffectState != "cancelled" && candidate.EffectState != "outcome_unknown")
1616
+ || !CancelTerminalCandidateIsValid(candidate.Envelope, identity.RequestId)) return false;
1617
+ object childTerminal;
1618
+ object selectedChildTerminal;
1619
+ object parentTerminal;
1620
+ if (!state.TryGetTerminal("test-cancel", identity.RequestId, identity.RequestDigest, out childTerminal)
1621
+ || !state.TryGetTerminalBySelector("test-cancel", identity.RequestId, candidate.RootRequestDigest, out selectedChildTerminal)
1622
+ || !state.TryGetTerminal("test-run", candidate.RootRequestId, candidate.RootRequestDigest, out parentTerminal)
1623
+ || !SerializedEqual(childTerminal, selectedChildTerminal)
1624
+ || !SerializedEqual(childTerminal, candidate.Envelope)
1625
+ || !CancelParentTerminalCandidateIsValid(parentTerminal, candidate.RootRequestId, candidate.EffectState)) return false;
1626
+ AddCancelRecoveryCandidateFields(marker, identity, candidate);
269
1627
  return true;
270
1628
  }
271
1629
 
272
- private static bool AdmissionStateOwnsRequest(IDictionary<string, object> state, string requestId)
1630
+ private static void AddCancelRecoveryCandidateFields(
1631
+ IDictionary<string, object> marker,
1632
+ AdmissionOwnerIdentity identity,
1633
+ BridgeWorkItem.CancelRecoveryCandidate candidate)
273
1634
  {
274
- if (string.Equals(AdmissionText(state, "requestId"), requestId, StringComparison.Ordinal)) return true;
275
- object rawOperation;
276
- var operation = state != null && state.TryGetValue("testOperation", out rawOperation) ? AsDictionary(rawOperation) : null;
277
- return operation != null && string.Equals(AdmissionText(operation, "cancelRequestId"), requestId, StringComparison.Ordinal);
1635
+ marker["rootRequestId"] = candidate.RootRequestId;
1636
+ marker["rootRequestDigest"] = candidate.RootRequestDigest;
1637
+ marker["runId"] = candidate.RunId;
1638
+ marker["cancelRequestId"] = identity.RequestId;
1639
+ marker["cancelRequestDigest"] = identity.RequestDigest;
1640
+ marker["cancelContinuationState"] = "continuation_pending";
1641
+ marker["parentRepairEffectState"] = candidate.EffectState;
1642
+ marker["parentRepairPending"] = true;
1643
+ marker["cancelTerminalEnvelope"] = candidate.Envelope;
278
1644
  }
279
1645
 
280
- private static bool AdmissionCoordinatorOwns(AdmissionOwnerIdentity identity, string command, string response)
1646
+ private static bool CancelParentTerminalCandidateIsValid(object envelope, string rootRequestId, string effectState)
281
1647
  {
282
- if (identity == null || !AdmissionDurableRootCommand(command)) return false;
283
- UnityCliAdmissionPool.ConsumedIdentity current;
284
- if (admissionPool == null || !admissionPool.TryGetConsumedIdentity(identity.RequestId, out current)) return true;
285
- if (!string.Equals(current.RootFingerprint, identity.RootFingerprint, StringComparison.Ordinal)
286
- || !string.Equals(current.Token, identity.Token, StringComparison.Ordinal)
287
- || !string.Equals(current.SessionId, identity.SessionId, StringComparison.Ordinal)) return true;
288
- if (current.OwnerKind == UnityCliAdmissionPool.AdmissionOwnerKind.Coordinator) return true;
289
- bool durable;
290
- if (!TryHasDurableAdmissionRoot(identity.RequestId, out durable)) return true;
291
- if (!durable && !AdmissionResponseIsCoordinatorAccepted(response)) return false;
292
- if (!admissionPool.TransferOwner(identity.RequestId, identity.RootFingerprint, identity.Token, identity.SessionId, UnityCliAdmissionPool.AdmissionOwnerKind.Coordinator)) return true;
1648
+ try
1649
+ {
1650
+ var value = AsDictionary(UnityCliSerializer.Parse(UnityCliSerializer.Serialize(envelope)));
1651
+ object ok;
1652
+ object rawError;
1653
+ var error = value != null && value.TryGetValue("error", out rawError) ? AsDictionary(rawError) : null;
1654
+ var expectedCode = effectState == "cancelled" ? "E_TEST_RUN_CANCELLED" : "E_EXECUTION_TIMEOUT";
1655
+ return value != null && string.Equals(AdmissionText(value, "requestId"), rootRequestId, StringComparison.Ordinal)
1656
+ && value.TryGetValue("ok", out ok) && ok is bool && !(bool)ok
1657
+ && error != null && string.Equals(AdmissionText(error, "code"), expectedCode, StringComparison.Ordinal);
1658
+ }
1659
+ catch { return false; }
1660
+ }
1661
+
1662
+ private static bool SerializedEqual(object left, object right)
1663
+ {
1664
+ try { return string.Equals(UnityCliSerializer.Serialize(left), UnityCliSerializer.Serialize(right), StringComparison.Ordinal); }
1665
+ catch { return false; }
1666
+ }
1667
+
1668
+ private static bool CancelTerminalEffectState(string value)
1669
+ {
1670
+ return string.Equals(value, "not_invoked", StringComparison.Ordinal)
1671
+ || string.Equals(value, "outcome_unknown", StringComparison.Ordinal)
1672
+ || string.Equals(value, "cancelled", StringComparison.Ordinal);
1673
+ }
1674
+
1675
+ /// <summary>候选terminal必须是exact request envelope且受terminal单条上限约束。</summary>
1676
+ private static bool CancelTerminalCandidateIsValid(object envelope, string cancelRequestId)
1677
+ {
1678
+ try
1679
+ {
1680
+ var serialized = UnityCliSerializer.Serialize(envelope);
1681
+ if (Encoding.UTF8.GetByteCount(serialized) > MaxCancelRecoveryEnvelopeBytes) return false;
1682
+ var value = AsDictionary(UnityCliSerializer.Parse(serialized));
1683
+ object ok;
1684
+ return value != null && string.Equals(AdmissionText(value, "requestId"), cancelRequestId, StringComparison.Ordinal)
1685
+ && value.TryGetValue("ok", out ok) && ok is bool;
1686
+ }
1687
+ catch { return false; }
1688
+ }
1689
+
1690
+ /// <summary>reload与在线retry共用的cancel marker状态闭集。</summary>
1691
+ private static bool TryReadCancelAdmissionMarkerState(
1692
+ IDictionary<string, object> marker,
1693
+ string cancelRequestId,
1694
+ string cancelRequestDigest,
1695
+ string rootRequestDigest,
1696
+ string runId,
1697
+ out string continuationState,
1698
+ out string repairEffectState,
1699
+ out bool repairPending,
1700
+ out object candidateEnvelope)
1701
+ {
1702
+ continuationState = AdmissionText(marker, "cancelContinuationState");
1703
+ repairEffectState = AdmissionText(marker, "parentRepairEffectState");
1704
+ repairPending = false;
1705
+ candidateEnvelope = null;
1706
+ object repairPendingValue;
1707
+ if (marker == null || !marker.TryGetValue("parentRepairPending", out repairPendingValue) || !(repairPendingValue is bool)
1708
+ || !string.Equals(AdmissionText(marker, "cancelRequestId"), cancelRequestId, StringComparison.Ordinal)
1709
+ || !string.Equals(AdmissionText(marker, "cancelRequestDigest"), cancelRequestDigest, StringComparison.Ordinal)
1710
+ || !string.Equals(AdmissionText(marker, "rootRequestDigest"), rootRequestDigest, StringComparison.Ordinal)
1711
+ || !string.Equals(AdmissionText(marker, "runId"), runId, StringComparison.Ordinal)) return false;
1712
+ repairPending = (bool)repairPendingValue;
1713
+ object rawCandidate;
1714
+ if (string.Equals(continuationState, "dispatched", StringComparison.Ordinal))
1715
+ return string.Equals(repairEffectState, "dispatched", StringComparison.Ordinal) && !repairPending
1716
+ && !marker.TryGetValue("cancelTerminalEnvelope", out rawCandidate);
1717
+ if (!string.Equals(continuationState, "continuation_pending", StringComparison.Ordinal)
1718
+ || !CancelTerminalEffectState(repairEffectState)
1719
+ || !marker.TryGetValue("cancelTerminalEnvelope", out rawCandidate)
1720
+ || !CancelTerminalCandidateIsValid(rawCandidate, cancelRequestId)) return false;
1721
+ candidateEnvelope = rawCandidate;
293
1722
  return true;
294
1723
  }
295
1724
 
1725
+ private static bool TryHasExactDurableBusinessRoot(BridgeQueueState queueState, string rootScope, string command, AdmissionOwnerIdentity identity, out bool durable)
1726
+ {
1727
+ durable = false;
1728
+ try
1729
+ {
1730
+ var operationRoot = queueState.LoadOperation(rootScope);
1731
+ if (operationRoot == null)
1732
+ {
1733
+ if (command == "internal.test.run.cancel") return false;
1734
+ bool ownsLease;
1735
+ if (!queueState.TryIsLeaseOwned(identity.RequestId, identity.RequestDigest, out ownsLease)) return false;
1736
+ if (command == "project-tool.run" || command == "code.exec")
1737
+ {
1738
+ durable = ownsLease;
1739
+ return true;
1740
+ }
1741
+ // durable coordinator无root但仍持有exact lease时,不能把错误响应归约为未接受。
1742
+ return !ownsLease;
1743
+ }
1744
+ if (!string.Equals(AdmissionText(operationRoot, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) return true;
1745
+ if (command == "internal.test.run.cancel")
1746
+ {
1747
+ object rawOperation;
1748
+ var operation = operationRoot.TryGetValue("testOperation", out rawOperation) ? AsDictionary(rawOperation) : null;
1749
+ var exactCancel = operation != null && string.Equals(AdmissionText(operation, "cancelRequestId"), identity.RequestId, StringComparison.Ordinal)
1750
+ && string.Equals(AdmissionText(operation, "cancelRequestDigest"), identity.RequestDigest, StringComparison.Ordinal);
1751
+ durable = exactCancel && (AdmissionBool(operation, "cancelNeedsContinuation")
1752
+ || new[] { "dispatching", "dispatched", "outcome_unknown", "terminal_cleanup" }.Contains(AdmissionText(operation, "cancelEffectState")));
1753
+ return true;
1754
+ }
1755
+ durable = string.Equals(AdmissionText(operationRoot, "requestId"), identity.RequestId, StringComparison.Ordinal)
1756
+ && string.Equals(AdmissionText(operationRoot, "requestDigest"), identity.RequestDigest, StringComparison.Ordinal);
1757
+ return true;
1758
+ }
1759
+ catch { return false; }
1760
+ }
1761
+
1762
+ private static void AdmissionOwnerScopes(string command, out string markerScope, out string rootScope)
1763
+ {
1764
+ markerScope = AdmissionTerminalScope(command);
1765
+ rootScope = markerScope == "test-cancel" ? "test-run" : markerScope;
1766
+ if (string.IsNullOrEmpty(markerScope)) throw new InvalidOperationException("Admission durable command has no owner scope.");
1767
+ }
1768
+
296
1769
  /// <summary>deadline之后仍允许同字节direct request读取已发布terminal,不入队也不重新占slot。</summary>
297
1770
  private static string AdmissionRecoveredDirectTerminal(UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes)
298
1771
  {
299
1772
  if (request == null || rawRequestBytes == null || (request.Command != "code.exec" && request.Command != "project-tool.run")) return null;
300
1773
  var digest = UnityCliRequestDigest.Compute(rawRequestBytes, request.InstanceId);
301
1774
  object terminal;
302
- return UnityCliActiveOperation.TryGetTerminal("direct-mutation", request.RequestId, digest, out terminal) ? UnityCliSerializer.Serialize(terminal) : null;
1775
+ if (!UnityCliActiveOperation.TryGetTerminal("direct-mutation", request.RequestId, digest, out terminal)) return null;
1776
+ CleanupPublishedDirectMutation(request.RequestId, digest);
1777
+ return UnityCliSerializer.Serialize(terminal);
303
1778
  }
304
1779
 
305
1780
  /// <summary>active admission已收敛后,只允许同字节请求读取现有exact durable terminal。</summary>
@@ -351,49 +1826,64 @@ namespace UnityCli
351
1826
  catch { return false; }
352
1827
  }
353
1828
 
354
- private static bool AdmissionResponseHasTrustedTerminal(string response)
355
- {
356
- try
357
- {
358
- var envelope = AsDictionary(UnityCliSerializer.Parse(response));
359
- object ok;
360
- if (envelope == null || !envelope.TryGetValue("ok", out ok) || !(ok is bool)) return false;
361
- if ((bool)ok) return true;
362
- object rawError;
363
- var error = envelope.TryGetValue("error", out rawError) ? AsDictionary(rawError) : null;
364
- var code = AdmissionText(error, "code");
365
- return error != null && code != "E_EXECUTION_TIMEOUT" && code != "E_UNITY_DOMAIN_RELOAD"
366
- && code != "E_UNITY_COMPILING" && code != "E_UNITY_IMPORTING" && code != "E_UNITY_PLAY_MODE"
367
- && code != "E_QUEUE_START_TIMEOUT";
368
- }
369
- catch { return false; }
370
- }
371
-
372
1829
  private static bool AdmissionDurableRootCommand(string command)
373
1830
  {
374
1831
  return command == "internal.build.run.start" || command == "internal.editor.compile.start"
375
1832
  || command == "internal.editor.refresh.start" || command == "internal.asset.refresh.start"
376
1833
  || command == "internal.package.embed.start" || command == "internal.package.resolve.start"
377
1834
  || command == "internal.test.run.start" || command == "internal.test.run.cancel"
378
- || command == "internal.navmesh.bake.start" || command == "internal.occlusion.bake.start";
1835
+ || command == "internal.navmesh.bake.start" || command == "internal.occlusion.bake.start"
1836
+ || command == "project-tool.run" || command == "code.exec";
1837
+ }
1838
+
1839
+ private static string AdmissionAcceptedTopDigest(BridgeQueueState state, string response, AdmissionOwnerIdentity identity, string command)
1840
+ {
1841
+ try
1842
+ {
1843
+ var envelope = AsDictionary(UnityCliSerializer.Parse(response));
1844
+ object rawData;
1845
+ var data = envelope != null && envelope.TryGetValue("data", out rawData) ? AsDictionary(rawData) : null;
1846
+ var digest = AdmissionText(data, "rootRequestDigest");
1847
+ if (AdmissionDigestIsValid(digest)) return digest;
1848
+ if (command == "internal.test.run.cancel")
1849
+ {
1850
+ var root = state.LoadOperation("test-run");
1851
+ object rawOperation;
1852
+ var operation = root != null && root.TryGetValue("testOperation", out rawOperation) ? AsDictionary(rawOperation) : null;
1853
+ digest = AdmissionText(operation, "cancelRootRequestDigest");
1854
+ if (AdmissionDigestIsValid(digest)) return digest;
1855
+ }
1856
+ return identity.RequestDigest;
1857
+ }
1858
+ catch { return identity.RequestDigest; }
379
1859
  }
380
1860
 
381
1861
  private static bool AdmissionRecoveryCommand(string command)
382
1862
  {
383
- return command == "project-tool.list" || command == "internal.editor.state.snapshot"
1863
+ return IsProjectToolCatalogReadCommand(command) || command == "internal.editor.state.snapshot"
1864
+ || IsDoctorAdmissionBypassCommand(command) || UnityCliDurableStatusCoordinator.IsCommand(command)
384
1865
  || UnityCliCompileCoordinator.IsCommand(command) || UnityCliLiveBuildCoordinator.IsCommand(command)
385
1866
  || UnityCliLiveRefreshCoordinator.IsCommand(command) || UnityCliAssetRefreshCoordinator.IsCommand(command)
386
1867
  || UnityCliPackageEmbedCoordinator.IsCommand(command) || UnityCliPackageResolveCoordinator.IsCommand(command)
387
1868
  || UnityCliTestRunCoordinator.IsCommand(command) || UnityCliNavMeshBakeCoordinator.IsCommand(command)
388
- || UnityCliOcclusionBakeCoordinator.IsCommand(command);
1869
+ || UnityCliOcclusionBakeCoordinator.IsCommand(command)
1870
+ || command == "project-tool.run" || command == "code.exec";
389
1871
  }
390
1872
 
391
1873
  private static bool AdmissionBypassCommand(string command)
392
1874
  {
1875
+ if (IsDoctorAdmissionBypassCommand(command)) return true;
393
1876
  switch (command)
394
1877
  {
1878
+ case "project-tool.health":
1879
+ case "project-tool.groups":
1880
+ case "project-tool.summary":
1881
+ case "project-tool.describe":
395
1882
  case "project-tool.list":
396
- case "diagnose.preflight":
1883
+ case "runtime-tool.groups":
1884
+ case "runtime-tool.summary":
1885
+ case "runtime-tool.describe":
1886
+ case "runtime-tool.list":
397
1887
  case "internal.editor.state.snapshot":
398
1888
  case "internal.editor.compile.status":
399
1889
  case "internal.editor.refresh.status":
@@ -406,7 +1896,6 @@ namespace UnityCli
406
1896
  case "internal.package.files.fail":
407
1897
  case "internal.test.run.status":
408
1898
  case "internal.test.run.cancel.plan":
409
- case "internal.test.run.cancel":
410
1899
  case "internal.test.run.cancel.status":
411
1900
  case "internal.navmesh.bake.status":
412
1901
  case "internal.navmesh.bake.plan":
@@ -422,9 +1911,139 @@ namespace UnityCli
422
1911
  }
423
1912
  }
424
1913
 
425
- private static void EnsureAdmissionPool()
1914
+ /// <summary>已完成transfer的marker更新业务字段时复制原source→intended证据,禁止伪造同态transfer。</summary>
1915
+ private static bool CopyCommittedAdmissionOwnerFields(IDictionary<string, object> source, IDictionary<string, object> target, AdmissionOwnerIdentity current, string topRequestDigest)
1916
+ {
1917
+ if (!AdmissionMarkerHasImmutableIdentity(source, current, topRequestDigest)
1918
+ || (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(source, "admissionIntendedOwnerState") != current.OwnerState)
1919
+ return false;
1920
+ foreach (var key in new[]
1921
+ {
1922
+ "admissionRootFingerprint", "admissionToken", "admissionSessionId", "admissionListenerId",
1923
+ "admissionListenerGeneration", "admissionExecutionDeadlineMs", "admissionRequestDigest",
1924
+ "admissionOwnerState", "admissionIntendedOwnerState"
1925
+ })
1926
+ {
1927
+ object value;
1928
+ if (!source.TryGetValue(key, out value)) return false;
1929
+ target[key] = value;
1930
+ }
1931
+ target["admissionTransferCommitted"] = true;
1932
+ return true;
1933
+ }
1934
+
1935
+ private static void WriteAdmissionOwnerFields(IDictionary<string, object> state, AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner, bool committed)
1936
+ {
1937
+ state["admissionRootFingerprint"] = identity.RootFingerprint;
1938
+ state["admissionToken"] = identity.Token;
1939
+ state["admissionSessionId"] = identity.SessionId;
1940
+ state["admissionListenerId"] = identity.ListenerId;
1941
+ state["admissionListenerGeneration"] = identity.ListenerGeneration;
1942
+ state["admissionExecutionDeadlineMs"] = identity.ExecutionDeadlineMs;
1943
+ state["admissionRequestDigest"] = identity.RequestDigest;
1944
+ state["admissionOwnerState"] = (long)identity.OwnerState;
1945
+ state["admissionIntendedOwnerState"] = (long)intendedOwner;
1946
+ state["admissionTransferCommitted"] = committed;
1947
+ }
1948
+
1949
+ private static AdmissionOwnerIdentity ToAdmissionOwner(UnityCliAdmissionPool.ConsumedIdentity identity, string requestDigest)
1950
+ {
1951
+ if (identity == null) return null;
1952
+ if (!string.IsNullOrEmpty(requestDigest) && !string.Equals(identity.RequestDigest, requestDigest, StringComparison.Ordinal))
1953
+ throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission owner request digest does not match the frozen WorkItem.");
1954
+ return new AdmissionOwnerIdentity { RequestId = identity.RequestId, RequestDigest = identity.RequestDigest, TopRequestDigest = identity.RequestDigest, RootFingerprint = identity.RootFingerprint, Token = identity.Token, SessionId = identity.SessionId, ListenerId = identity.ListenerId, ListenerGeneration = identity.ListenerGeneration, ExecutionDeadlineMs = identity.ExecutionDeadlineMs, EditorSessionId = EditorSessionId(), OwnerState = identity.OwnerState };
1955
+ }
1956
+
1957
+ private static UnityCliAdmissionPool.ConsumedIdentity ToPoolIdentity(AdmissionOwnerIdentity identity)
1958
+ {
1959
+ return identity == null ? null : UnityCliAdmissionPool.CreateConsumedIdentity(identity.RequestId, identity.RootFingerprint, identity.RequestDigest, identity.Token, identity.SessionId, identity.OwnerState, identity.ListenerId, identity.ListenerGeneration, identity.ExecutionDeadlineMs);
1960
+ }
1961
+
1962
+ private static AdmissionOwnerIdentity RecordIdentity(UnityCliAdmissionPool.AdmissionRecord record)
1963
+ {
1964
+ return new AdmissionOwnerIdentity { RequestId = record.RequestId, RequestDigest = record.RequestDigest, TopRequestDigest = record.RequestDigest, RootFingerprint = record.RootFingerprint, Token = record.Token, SessionId = record.SessionId, ListenerId = record.ListenerId, ListenerGeneration = record.ListenerGeneration, ExecutionDeadlineMs = record.ExecutionDeadlineMs, EditorSessionId = EditorSessionId(), OwnerState = record.OwnerState };
1965
+ }
1966
+
1967
+ private static bool SameAdmissionTransferIdentityExceptListener(AdmissionOwnerIdentity left, AdmissionOwnerIdentity right)
1968
+ {
1969
+ return left != null && right != null
1970
+ && string.Equals(left.RequestId, right.RequestId, StringComparison.Ordinal)
1971
+ && string.Equals(left.RequestDigest, right.RequestDigest, StringComparison.Ordinal)
1972
+ && string.Equals(left.RootFingerprint, right.RootFingerprint, StringComparison.Ordinal)
1973
+ && string.Equals(left.Token, right.Token, StringComparison.Ordinal)
1974
+ && string.Equals(left.SessionId, right.SessionId, StringComparison.Ordinal)
1975
+ && left.ExecutionDeadlineMs == right.ExecutionDeadlineMs
1976
+ && string.Equals(left.EditorSessionId ?? EditorSessionId(), right.EditorSessionId ?? EditorSessionId(), StringComparison.Ordinal);
1977
+ }
1978
+
1979
+ private static bool SameAdmissionImmutableIdentity(AdmissionOwnerIdentity left, AdmissionOwnerIdentity right)
1980
+ {
1981
+ return left != null && right != null
1982
+ && string.Equals(left.RequestId, right.RequestId, StringComparison.Ordinal)
1983
+ && string.Equals(left.RequestDigest, right.RequestDigest, StringComparison.Ordinal)
1984
+ && string.Equals(left.RootFingerprint, right.RootFingerprint, StringComparison.Ordinal)
1985
+ && string.Equals(left.Token, right.Token, StringComparison.Ordinal)
1986
+ && string.Equals(left.SessionId, right.SessionId, StringComparison.Ordinal)
1987
+ && string.Equals(left.ListenerId, right.ListenerId, StringComparison.Ordinal)
1988
+ && left.ListenerGeneration == right.ListenerGeneration
1989
+ && left.ExecutionDeadlineMs == right.ExecutionDeadlineMs
1990
+ && string.Equals(left.EditorSessionId ?? EditorSessionId(), right.EditorSessionId ?? EditorSessionId(), StringComparison.Ordinal);
1991
+ }
1992
+
1993
+ private static string AdmissionTopDigest(AdmissionOwnerIdentity identity)
1994
+ {
1995
+ return identity == null || !AdmissionDigestIsValid(identity.TopRequestDigest) ? identity == null ? string.Empty : identity.RequestDigest : identity.TopRequestDigest;
1996
+ }
1997
+
1998
+ private static bool AdmissionDigestIsValid(string value)
1999
+ {
2000
+ if (string.IsNullOrEmpty(value) || value.Length != 64) return false;
2001
+ foreach (var character in value) if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) return false;
2002
+ return true;
2003
+ }
2004
+
2005
+ internal static bool ClearExactAdmissionOwnerMarkers(AdmissionOwnerIdentity identity)
2006
+ {
2007
+ return ClearExactAdmissionOwnerMarkers(ProductionQueueState, identity);
2008
+ }
2009
+
2010
+ internal static bool ClearExactAdmissionOwnerMarkers(BridgeQueueState state, AdmissionOwnerIdentity identity)
2011
+ {
2012
+ return ClearAdmissionOwnerMarkersForState(state, identity);
2013
+ }
2014
+
2015
+ internal static bool ClearAdmissionOwnerMarkersForState(BridgeQueueState state, AdmissionOwnerIdentity identity)
2016
+ {
2017
+ if (state == null || identity == null) return false;
2018
+ return ClearAdmissionOwnerMarkers(state, identity);
2019
+ }
2020
+
2021
+ private static bool ClearProductionAdmissionOwnerMarkers(AdmissionOwnerIdentity identity)
2022
+ {
2023
+ return ClearAdmissionOwnerMarkers(ProductionQueueState, identity);
2024
+ }
2025
+
2026
+ private static bool ClearAdmissionOwnerMarkers(BridgeQueueState state, AdmissionOwnerIdentity identity)
426
2027
  {
427
- if (admissionPool == null) InitializeAdmissionPool();
2028
+ if (state == null || identity == null) return false;
2029
+ var cleared = true;
2030
+ foreach (var scope in AdmissionDurableScopes)
2031
+ {
2032
+ var markerScope = scope + "-admission-owner";
2033
+ try
2034
+ {
2035
+ var marker = state.LoadOperation(markerScope);
2036
+ if (marker == null || !AdmissionMarkerHasImmutableIdentity(marker, identity, AdmissionTopDigest(identity))) continue;
2037
+ state.ClearOperation(markerScope);
2038
+ if (state.OperationExists(markerScope)) cleared = false;
2039
+ }
2040
+ catch (Exception exception)
2041
+ {
2042
+ cleared = false;
2043
+ state.Sinks.WriteLog("admission_owner_marker_cleanup_pending requestId=" + identity.RequestId + " scope=" + markerScope + " " + exception.GetType().Name);
2044
+ }
2045
+ }
2046
+ return cleared;
428
2047
  }
429
2048
 
430
2049
  private static Dictionary<string, object> StrictAdmissionObject(byte[] rawBody, string operation, params string[] allowed)