@chrrxs/robloxstudio-mcp 3.0.0 → 3.0.2

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 (48) hide show
  1. package/dist/index.js +9401 -9047
  2. package/package.json +2 -2
  3. package/studio-plugin/MCPPlugin.rbxmx +400 -97
  4. package/studio-plugin/.Carbon.rbxm.lock +0 -0
  5. package/studio-plugin/Carbon.rbxm +0 -0
  6. package/studio-plugin/INSTALLATION.md +0 -170
  7. package/studio-plugin/MCPInspectorPlugin.rbxmx +0 -169759
  8. package/studio-plugin/default.project.json +0 -19
  9. package/studio-plugin/dev.project.json +0 -23
  10. package/studio-plugin/include/LibMP.lua +0 -156378
  11. package/studio-plugin/inspector-icon.png +0 -0
  12. package/studio-plugin/package-lock.json +0 -706
  13. package/studio-plugin/package.json +0 -19
  14. package/studio-plugin/plugin.json +0 -10
  15. package/studio-plugin/src/modules/AssetSanitizationPolicy.ts +0 -127
  16. package/studio-plugin/src/modules/ClientBroker.ts +0 -450
  17. package/studio-plugin/src/modules/Communication.ts +0 -601
  18. package/studio-plugin/src/modules/EvalBridges.ts +0 -255
  19. package/studio-plugin/src/modules/HttpDiagnostics.ts +0 -50
  20. package/studio-plugin/src/modules/LuauExec.ts +0 -403
  21. package/studio-plugin/src/modules/Recording.ts +0 -28
  22. package/studio-plugin/src/modules/RenderMonitor.ts +0 -60
  23. package/studio-plugin/src/modules/RuntimeLogBuffer.ts +0 -210
  24. package/studio-plugin/src/modules/ServerUrlSettings.ts +0 -117
  25. package/studio-plugin/src/modules/State.ts +0 -39
  26. package/studio-plugin/src/modules/StopPlayMonitor.ts +0 -267
  27. package/studio-plugin/src/modules/UI.ts +0 -597
  28. package/studio-plugin/src/modules/Utils.ts +0 -527
  29. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +0 -391
  30. package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +0 -460
  31. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +0 -170
  32. package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +0 -149
  33. package/studio-plugin/src/modules/handlers/GenerateModelHandlers.ts +0 -168
  34. package/studio-plugin/src/modules/handlers/InputHandlers.ts +0 -163
  35. package/studio-plugin/src/modules/handlers/LogHandlers.ts +0 -14
  36. package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +0 -44
  37. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +0 -96
  38. package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +0 -1263
  39. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +0 -62
  40. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -716
  41. package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +0 -216
  42. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +0 -531
  43. package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +0 -386
  44. package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +0 -172
  45. package/studio-plugin/src/modules/handlers/TestHandlers.ts +0 -350
  46. package/studio-plugin/src/server/index.server.ts +0 -135
  47. package/studio-plugin/src/types/index.d.ts +0 -57
  48. package/studio-plugin/tsconfig.json +0 -20
@@ -271,6 +271,7 @@ local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandl
271
271
  local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
272
272
  local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
273
273
  local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
274
+ local MetadataHandlers = TS.import(script, script.Parent, "handlers", "MetadataHandlers")
274
275
  local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
275
276
  local BreakpointHandlers = TS.import(script, script.Parent, "handlers", "BreakpointHandlers")
276
277
  local ScriptProfilerHandlers = TS.import(script, script.Parent, "handlers", "ScriptProfilerHandlers")
@@ -351,6 +352,7 @@ local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
351
352
  ["/api/capture-begin"] = true,
352
353
  ["/api/simulate-mouse-input"] = true,
353
354
  ["/api/simulate-keyboard-input"] = true,
355
+ ["/api/focus-viewport"] = true,
354
356
  }
355
357
  -- Throttle re-ready calls per proxyId so a brief window of unknownInstance
356
358
  -- polls doesn't cause a re-register stampede.
@@ -559,6 +561,9 @@ local function setupClientBroker()
559
561
  if payload and payload.endpoint == "/api/simulate-keyboard-input" then
560
562
  return InputHandlers.simulateKeyboardInput(payload.data or {})
561
563
  end
564
+ if payload and payload.endpoint == "/api/focus-viewport" then
565
+ return MetadataHandlers.focusViewport(payload.data or {})
566
+ end
562
567
  if payload and payload.endpoint == "/api/execute-luau" then
563
568
  return handleExecuteLuau(payload.data)
564
569
  end
@@ -611,15 +616,33 @@ local function pollProxy(proxyId, player, rf)
611
616
  unregisterProxy(player)
612
617
  break
613
618
  end
619
+ local nextPollDelay = 0.5
614
620
  local ok, res = pcall(function()
615
- return HttpService:RequestAsync({
616
- Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
621
+ local requestOptions = {
622
+ Url = `{mcpUrl}/poll?pluginSessionId={proxyId}&pollMode=long`,
617
623
  Method = "GET",
618
624
  Headers = {
619
625
  ["Content-Type"] = "application/json",
620
626
  },
621
- })
627
+ Timeout = State.POLL_REQUEST_TIMEOUT_SECONDS,
628
+ }
629
+ return HttpService:RequestAsync(requestOptions)
622
630
  end)
631
+ -- RequestAsync may yield for the full long-poll window. Never apply a
632
+ -- stale response to a player whose proxy was removed or replaced.
633
+ local _player = player
634
+ local currentProxy = proxyByPlayer[_player]
635
+ local _condition_1 = player.Parent == nil
636
+ if not _condition_1 then
637
+ local _result = currentProxy
638
+ if _result ~= nil then
639
+ _result = _result.pluginSessionId
640
+ end
641
+ _condition_1 = _result ~= proxyId
642
+ end
643
+ if _condition_1 then
644
+ break
645
+ end
623
646
  if ok and res and (res.Success or res.StatusCode == 503) then
624
647
  local okJson, body = pcall(function()
625
648
  return HttpService:JSONDecode(res.Body)
@@ -669,9 +692,17 @@ local function pollProxy(proxyId, player, rf)
669
692
  response = response,
670
693
  })
671
694
  end
695
+ if res.Success and body.pollMode == "long" and body.knownInstance ~= false and body.request == nil then
696
+ nextPollDelay = 0
697
+ end
672
698
  end
673
699
  end
674
- task.wait(0.5)
700
+ if nextPollDelay > 0 then
701
+ task.wait(nextPollDelay)
702
+ else
703
+ -- Yield one scheduler turn before replacing a completed long poll.
704
+ task.wait()
705
+ end
675
706
  end
676
707
  end
677
708
  local function registerProxy(player, rf)
@@ -826,6 +857,7 @@ local lastVersionMismatchWarningKey
826
857
  local lastReadyInstanceId
827
858
  local readyFailureLogKeys = {}
828
859
  local retryingDuplicateReady = false
860
+ local pollEpoch = 0
829
861
  -- Cache the published place name from MarketplaceService:GetProductInfo so
830
862
  -- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
831
863
  -- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
@@ -885,6 +917,8 @@ local routeMap = {
885
917
  ["/api/delete-script-lines"] = ScriptHandlers.deleteScriptLines,
886
918
  ["/api/get-attributes"] = MetadataHandlers.getAttributes,
887
919
  ["/api/get-selection"] = MetadataHandlers.getSelection,
920
+ ["/api/set-selection"] = MetadataHandlers.setSelection,
921
+ ["/api/focus-viewport"] = MetadataHandlers.focusViewport,
888
922
  ["/api/execute-luau"] = MetadataHandlers.executeLuau,
889
923
  ["/api/eval-runtime"] = EvalRuntimeHandlers.evalRuntime,
890
924
  ["/api/start-playtest"] = TestHandlers.startPlaytest,
@@ -1108,16 +1142,24 @@ local function pollForRequests()
1108
1142
  if conn.isPolling then
1109
1143
  return nil
1110
1144
  end
1145
+ local epoch = pollEpoch
1111
1146
  conn.isPolling = true
1112
1147
  local success, result = pcall(function()
1113
- return HttpService:RequestAsync({
1114
- Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}`,
1148
+ local requestOptions = {
1149
+ Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}&pollMode=long`,
1115
1150
  Method = "GET",
1116
1151
  Headers = {
1117
1152
  ["Content-Type"] = "application/json",
1118
1153
  },
1119
- })
1154
+ Timeout = State.POLL_REQUEST_TIMEOUT_SECONDS,
1155
+ }
1156
+ return HttpService:RequestAsync(requestOptions)
1120
1157
  end)
1158
+ -- A held request can finish after deactivate/reactivate. Only the current
1159
+ -- lifecycle epoch may mutate connection state or clear a newer poll's lock.
1160
+ if epoch ~= pollEpoch or not conn.isActive then
1161
+ return nil
1162
+ end
1121
1163
  conn.isPolling = false
1122
1164
  local ui = UI.getElements()
1123
1165
  UI.updateToolbarIcon()
@@ -1126,6 +1168,12 @@ local function pollForRequests()
1126
1168
  conn.currentRetryDelay = 0.5
1127
1169
  conn.lastSuccessfulConnection = tick()
1128
1170
  local data = HttpService:JSONDecode(result.Body)
1171
+ conn.lastPoll = tick()
1172
+ if result.Success and data.pollMode == "long" and data.knownInstance ~= false and data.request == nil then
1173
+ -- The server held this request until work or timeout, so immediately
1174
+ -- replace it instead of reintroducing the legacy 500 ms idle gap.
1175
+ conn.lastPoll = 0
1176
+ end
1129
1177
  local mcpConnected = data.mcpConnected == true
1130
1178
  conn.lastHttpOk = true
1131
1179
  conn.lastMcpOk = mcpConnected
@@ -1216,6 +1264,7 @@ local function pollForRequests()
1216
1264
  end)
1217
1265
  end
1218
1266
  elseif conn.isActive then
1267
+ conn.lastPoll = tick()
1219
1268
  conn.consecutiveFailures += 1
1220
1269
  if conn.consecutiveFailures > 1 then
1221
1270
  conn.currentRetryDelay = math.min(conn.currentRetryDelay * conn.retryBackoffMultiplier, conn.maxRetryDelay)
@@ -1280,6 +1329,9 @@ local deactivatePlugin
1280
1329
  local function activatePlugin()
1281
1330
  local conn = State.getActiveConnection()
1282
1331
  local ui = UI.getElements()
1332
+ pollEpoch += 1
1333
+ conn.isPolling = false
1334
+ conn.lastPoll = 0
1283
1335
  conn.isActive = true
1284
1336
  conn.consecutiveFailures = 0
1285
1337
  conn.currentRetryDelay = 0.5
@@ -1328,6 +1380,8 @@ local function activatePlugin()
1328
1380
  end
1329
1381
  function deactivatePlugin()
1330
1382
  local conn = State.getActiveConnection()
1383
+ pollEpoch += 1
1384
+ conn.isPolling = false
1331
1385
  conn.isActive = false
1332
1386
  conn.lastMcpOk = false
1333
1387
  UI.updateUIState()
@@ -1521,9 +1575,9 @@ local function computeBridgeStamp()
1521
1575
  for i = 1, #combined do
1522
1576
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1523
1577
  end
1524
- -- "3.0.0" is replaced with the package version at package time
1578
+ -- "3.0.2" is replaced with the package version at package time
1525
1579
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1526
- return `{tostring(h)}-3.0.0`
1580
+ return `{tostring(h)}-3.0.2`
1527
1581
  end
1528
1582
  local BRIDGE_STAMP = computeBridgeStamp()
1529
1583
  local function setSource(scriptInst, source)
@@ -3593,11 +3647,14 @@ local function getAttributes(requestData)
3593
3647
  }
3594
3648
  count += 1
3595
3649
  end
3596
- return {
3650
+ local response = {
3597
3651
  instancePath = instancePath,
3598
- attributes = serializedAttributes,
3599
3652
  count = count,
3600
3653
  }
3654
+ if count > 0 then
3655
+ response.attributes = serializedAttributes
3656
+ end
3657
+ return response
3601
3658
  end)
3602
3659
  if success then
3603
3660
  return result
@@ -3638,6 +3695,180 @@ local function getSelection(_requestData)
3638
3695
  message = `{#selection} object(s) selected`,
3639
3696
  }
3640
3697
  end
3698
+ -- The counterpart to getSelection: this sets it. Selecting what the agent just
3699
+ -- built shows the user the result and puts the instance under Studio's own
3700
+ -- move/scale handles, which is why it is a tool and not left to execute_luau.
3701
+ local function setSelection(requestData)
3702
+ local rawPaths = requestData.paths
3703
+ if not (type(rawPaths) == "table") then
3704
+ return {
3705
+ error = "paths is required (an empty array clears in set mode)",
3706
+ }
3707
+ end
3708
+ local _condition = (requestData.mode)
3709
+ if _condition == nil then
3710
+ _condition = "set"
3711
+ end
3712
+ local mode = _condition
3713
+ if mode ~= "set" and mode ~= "add" and mode ~= "remove" then
3714
+ return {
3715
+ error = `mode must be "set", "add" or "remove" (got: {mode})`,
3716
+ }
3717
+ end
3718
+ local paths = rawPaths
3719
+ if #paths == 0 and mode ~= "set" then
3720
+ return {
3721
+ error = `paths cannot be empty in {mode} mode`,
3722
+ }
3723
+ end
3724
+ local targets = {}
3725
+ local missing = {}
3726
+ for _, entry in paths do
3727
+ if not (type(entry) == "string") or entry == "" then
3728
+ return {
3729
+ error = "paths must contain non-empty instance path strings",
3730
+ }
3731
+ end
3732
+ local instance = getInstanceByPath(entry)
3733
+ if instance then
3734
+ table.insert(targets, instance)
3735
+ else
3736
+ table.insert(missing, entry)
3737
+ end
3738
+ end
3739
+ local clearsSelection = mode == "set" and #paths == 0
3740
+ if #targets == 0 and not clearsSelection then
3741
+ return {
3742
+ error = "No matching instances found for any path",
3743
+ missingPaths = missing,
3744
+ }
3745
+ end
3746
+ local ok, err = pcall(function()
3747
+ if mode == "add" then
3748
+ Selection:Add(targets)
3749
+ elseif mode == "remove" then
3750
+ Selection:Remove(targets)
3751
+ else
3752
+ Selection:Set(targets)
3753
+ end
3754
+ end)
3755
+ if not ok then
3756
+ return {
3757
+ error = `Selection.{mode} failed: {tostring(err)}`,
3758
+ }
3759
+ end
3760
+ return {
3761
+ success = true,
3762
+ mode = mode,
3763
+ selected = #targets,
3764
+ missingPaths = missing,
3765
+ message = if clearsSelection then "Selection cleared" elseif mode == "remove" then `Deselected {#targets} object(s)` else `{#targets} object(s) {if mode == "add" then "added to" else "in"} the selection`,
3766
+ }
3767
+ end
3768
+ -- Aims the Studio camera at an instance and frames it from a sensible angle.
3769
+ -- This completes the screenshot loop capture_screenshot documents: build,
3770
+ -- focus, screenshot. Framing distance comes from the bounding box and the
3771
+ -- camera's own field of view so any subject fills a similar share of frame.
3772
+ local function focusViewport(requestData)
3773
+ local instancePath = requestData.path
3774
+ if not (instancePath ~= "" and instancePath) then
3775
+ return {
3776
+ error = "path is required (the instance to frame)",
3777
+ }
3778
+ end
3779
+ local instance = getInstanceByPath(instancePath)
3780
+ if not instance then
3781
+ return {
3782
+ error = `Instance not found: {instancePath}`,
3783
+ }
3784
+ end
3785
+ local workspace = game:GetService("Workspace")
3786
+ local camera = workspace.CurrentCamera
3787
+ if not camera then
3788
+ return {
3789
+ error = "Workspace.CurrentCamera is unavailable right now",
3790
+ }
3791
+ end
3792
+ local _condition = tonumber(requestData.padding)
3793
+ if _condition == nil then
3794
+ _condition = 1
3795
+ end
3796
+ local padding = _condition
3797
+ if padding <= 0 or padding > 10 then
3798
+ return {
3799
+ error = "padding must be greater than 0 and at most 10",
3800
+ }
3801
+ end
3802
+ local compassOverride = if requestData.from == nil then nil else tonumber(requestData.from)
3803
+ if requestData.from ~= nil and compassOverride == nil then
3804
+ return {
3805
+ error = "from must be a compass angle in degrees",
3806
+ }
3807
+ end
3808
+ local elevationOverride = if requestData.angleY == nil then nil else tonumber(requestData.angleY)
3809
+ if requestData.angleY ~= nil and (elevationOverride == nil or elevationOverride < -89 or elevationOverride > 89) then
3810
+ return {
3811
+ error = "angleY must be between -89 and 89",
3812
+ }
3813
+ end
3814
+ local ok, err = pcall(function()
3815
+ local boundsCF
3816
+ local boundsSize
3817
+ if instance:IsA("Model") then
3818
+ boundsCF, boundsSize = instance:GetBoundingBox()
3819
+ elseif instance:IsA("BasePart") then
3820
+ boundsCF = instance.CFrame
3821
+ boundsSize = instance.Size
3822
+ else
3823
+ return error(`Cannot frame a {instance.ClassName}: it has no 3D bounding box. Frame a part or model instead.`)
3824
+ end
3825
+ local center = boundsCF.Position
3826
+ local direction = camera.CFrame.LookVector * (-1)
3827
+ local currentHorizontal = Vector3.new(direction.X, 0, direction.Z)
3828
+ local horizontalDirection = if currentHorizontal.Magnitude < 0.001 then Vector3.new(1, 0, 0) else currentHorizontal.Unit
3829
+ if compassOverride ~= nil then
3830
+ local yaw = math.rad(compassOverride)
3831
+ horizontalDirection = Vector3.new(math.cos(yaw), 0, math.sin(yaw))
3832
+ end
3833
+ local vertical = direction.Y
3834
+ local horizontalMagnitude = math.sqrt(math.max(0, 1 - vertical * vertical))
3835
+ if elevationOverride ~= nil then
3836
+ local pitch = math.rad(elevationOverride)
3837
+ vertical = math.sin(pitch)
3838
+ horizontalMagnitude = math.cos(pitch)
3839
+ end
3840
+ local _horizontalDirection = horizontalDirection
3841
+ local _horizontalMagnitude = horizontalMagnitude
3842
+ local _vector3 = Vector3.new(0, vertical, 0)
3843
+ direction = (_horizontalDirection * _horizontalMagnitude + _vector3).Unit
3844
+ camera.CameraType = Enum.CameraType.Scriptable
3845
+ local _direction = direction
3846
+ local _arg0 = math.max(boundsSize.Magnitude, 1)
3847
+ camera.CFrame = CFrame.lookAt(center + (_direction * _arg0), center)
3848
+ camera.Focus = CFrame.new(center)
3849
+ camera:ZoomToExtents(boundsCF, boundsSize)
3850
+ if padding ~= 1 then
3851
+ local fittedOffset = camera.CFrame.Position - center
3852
+ local _arg0_1 = fittedOffset * padding
3853
+ camera.CFrame = CFrame.lookAt(center + _arg0_1, center)
3854
+ end
3855
+ camera.Focus = CFrame.new(center)
3856
+ end)
3857
+ if not ok then
3858
+ return {
3859
+ error = `focus failed: {tostring(err)}`,
3860
+ }
3861
+ end
3862
+ return {
3863
+ success = true,
3864
+ path = getInstancePath(instance),
3865
+ cameraPosition = {
3866
+ X = camera.CFrame.Position.X,
3867
+ Y = camera.CFrame.Position.Y,
3868
+ Z = camera.CFrame.Position.Z,
3869
+ },
3870
+ }
3871
+ end
3641
3872
  local function executeLuau(requestData)
3642
3873
  local code = requestData.code
3643
3874
  if not (code ~= "" and code) or code == "" then
@@ -3654,6 +3885,8 @@ end
3654
3885
  return {
3655
3886
  getAttributes = getAttributes,
3656
3887
  getSelection = getSelection,
3888
+ setSelection = setSelection,
3889
+ focusViewport = focusViewport,
3657
3890
  executeLuau = executeLuau,
3658
3891
  }
3659
3892
  ]]></string>
@@ -3674,6 +3907,8 @@ local DEFAULT_MAX_TIMERS_PER_GROUP = 5
3674
3907
  local DEFAULT_MAX_RELATED_TIMERS = 3
3675
3908
  local DEFAULT_MAX_EVENTS = 250000
3676
3909
  local MAX_EVENTS = 1000000
3910
+ local COOPERATIVE_CLOCK_CHECK_INTERVAL = 128
3911
+ local MAX_COOPERATIVE_SLICE_SECONDS = 0.008
3677
3912
  local DEFAULT_FRAME_WINDOW = 240
3678
3913
  local BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
3679
3914
  local PAD_BYTE = (string.byte("="))
@@ -3690,6 +3925,87 @@ local FOCUS_GROUP_MASKS = {
3690
3925
  jobs = { "Jobs" },
3691
3926
  }
3692
3927
  local cachedLibMP
3928
+ -- Sampling the clock avoids a timing call for every event while still bounding
3929
+ -- each VM resumption. Sorting uses the same checkpoint so no full sort is atomic.
3930
+ local function createCooperativeCheckpoint()
3931
+ local operationsSinceClockCheck = 0
3932
+ local sliceStartedAt = os.clock()
3933
+ return function()
3934
+ operationsSinceClockCheck += 1
3935
+ if operationsSinceClockCheck < COOPERATIVE_CLOCK_CHECK_INTERVAL then
3936
+ return nil
3937
+ end
3938
+ operationsSinceClockCheck = 0
3939
+ if os.clock() - sliceStartedAt < MAX_COOPERATIVE_SLICE_SECONDS then
3940
+ return nil
3941
+ end
3942
+ task.wait()
3943
+ sliceStartedAt = os.clock()
3944
+ end
3945
+ end
3946
+ local function cooperativeSort(values, comesBefore, checkpoint)
3947
+ local count = #values
3948
+ if count < 2 then
3949
+ return nil
3950
+ end
3951
+ local function siftDown(start, lastIndex)
3952
+ local root = start
3953
+ while root * 2 + 1 <= lastIndex do
3954
+ checkpoint()
3955
+ local candidate = root
3956
+ local left = root * 2 + 1
3957
+ if comesBefore(values[candidate + 1], values[left + 1]) then
3958
+ candidate = left
3959
+ end
3960
+ local right = left + 1
3961
+ if right <= lastIndex and comesBefore(values[candidate + 1], values[right + 1]) then
3962
+ candidate = right
3963
+ end
3964
+ if candidate == root then
3965
+ return nil
3966
+ end
3967
+ local value = values[root + 1]
3968
+ values[root + 1] = values[candidate + 1]
3969
+ values[candidate + 1] = value
3970
+ root = candidate
3971
+ end
3972
+ end
3973
+ do
3974
+ local start = math.floor((count - 2) / 2)
3975
+ local _shouldIncrement = false
3976
+ while true do
3977
+ if _shouldIncrement then
3978
+ start -= 1
3979
+ else
3980
+ _shouldIncrement = true
3981
+ end
3982
+ if not (start >= 0) then
3983
+ break
3984
+ end
3985
+ siftDown(start, count - 1)
3986
+ checkpoint()
3987
+ end
3988
+ end
3989
+ do
3990
+ local lastIndex = count - 1
3991
+ local _shouldIncrement = false
3992
+ while true do
3993
+ if _shouldIncrement then
3994
+ lastIndex -= 1
3995
+ else
3996
+ _shouldIncrement = true
3997
+ end
3998
+ if not (lastIndex > 0) then
3999
+ break
4000
+ end
4001
+ local value = values[1]
4002
+ values[1] = values[lastIndex + 1]
4003
+ values[lastIndex + 1] = value
4004
+ checkpoint()
4005
+ siftDown(0, lastIndex - 1)
4006
+ end
4007
+ end
4008
+ end
3693
4009
  local function normalizeDurationMs(value)
3694
4010
  local _value = value
3695
4011
  if not (type(_value) == "number") then
@@ -3817,7 +4133,7 @@ local function addFrameRaw(map, frameId, raw)
3817
4133
  local _arg1 = _condition + raw
3818
4134
  _map[_exp] = _arg1
3819
4135
  end
3820
- local function summarizeFrameImpact(frameRawById, analyzedFrames)
4136
+ local function summarizeFrameImpact(frameRawById, analyzedFrames, checkpoint)
3821
4137
  local _condition = frameRawById == nil
3822
4138
  if not _condition then
3823
4139
  -- ▼ ReadonlyMap.size ▼
@@ -3842,6 +4158,7 @@ local function summarizeFrameImpact(frameRawById, analyzedFrames)
3842
4158
  local maxUs = 0
3843
4159
  local maxFrameId = 0
3844
4160
  for frameId, raw in frameRawById do
4161
+ checkpoint()
3845
4162
  local us = rawToUs(raw)
3846
4163
  table.insert(values, us)
3847
4164
  totalUs += us
@@ -3850,9 +4167,9 @@ local function summarizeFrameImpact(frameRawById, analyzedFrames)
3850
4167
  maxFrameId = frameId
3851
4168
  end
3852
4169
  end
3853
- table.sort(values, function(a, b)
4170
+ cooperativeSort(values, function(a, b)
3854
4171
  return a < b
3855
- end)
4172
+ end, checkpoint)
3856
4173
  return {
3857
4174
  active_frame_count = #values,
3858
4175
  active_frame_pct = percent(#values, analyzedFrames),
@@ -3863,7 +4180,7 @@ local function summarizeFrameImpact(frameRawById, analyzedFrames)
3863
4180
  max_frame_id = maxFrameId,
3864
4181
  }
3865
4182
  end
3866
- local function encodeBase64(buf)
4183
+ local function encodeBase64(buf, checkpoint)
3867
4184
  local len = buffer.len(buf)
3868
4185
  local fullTriples = math.floor(len / 3)
3869
4186
  local remaining = len - fullTriples * 3
@@ -3892,6 +4209,7 @@ local function encodeBase64(buf)
3892
4209
  buffer.writeu8(out, di + 3, B64[bit32.band(b2, 63) + 1])
3893
4210
  si += 3
3894
4211
  di += 4
4212
+ checkpoint()
3895
4213
  end
3896
4214
  end
3897
4215
  if remaining == 2 then
@@ -4002,7 +4320,7 @@ local function recommendedToolsForGroups(groups, targetRole)
4002
4320
  end
4003
4321
  return tools
4004
4322
  end
4005
- local function collectFrameSummary(session, startFrame, frameMax)
4323
+ local function collectFrameSummary(session, startFrame, frameMax, checkpoint)
4006
4324
  local frameRows = {}
4007
4325
  local durations = {}
4008
4326
  local incompleteFrames = 0
@@ -4020,6 +4338,7 @@ local function collectFrameSummary(session, startFrame, frameMax)
4020
4338
  if not (frameId <= frameMax) then
4021
4339
  break
4022
4340
  end
4341
+ checkpoint()
4023
4342
  local ok, descOrErr = safeCall(function()
4024
4343
  return session:GetFrameDesc(frameId)
4025
4344
  end)
@@ -4059,15 +4378,16 @@ local function collectFrameSummary(session, startFrame, frameMax)
4059
4378
  _frameId = frameId
4060
4379
  end
4061
4380
  end
4062
- table.sort(durations, function(a, b)
4381
+ cooperativeSort(durations, function(a, b)
4063
4382
  return a < b
4064
- end)
4065
- table.sort(frameRows, function(a, b)
4383
+ end, checkpoint)
4384
+ cooperativeSort(frameRows, function(a, b)
4066
4385
  return (a.duration_us) > (b.duration_us)
4067
- end)
4386
+ end, checkpoint)
4068
4387
  local totalUs = 0
4069
4388
  for _, duration in durations do
4070
4389
  totalUs += duration
4390
+ checkpoint()
4071
4391
  end
4072
4392
  local topFrames = {}
4073
4393
  do
@@ -4381,6 +4701,7 @@ local function captureMicroProfiler(requestData)
4381
4701
  backend = backend,
4382
4702
  }
4383
4703
  end
4704
+ local checkpoint = createCooperativeCheckpoint()
4384
4705
  local stacks = {}
4385
4706
  local aggregates = {}
4386
4707
  local timerThreadAggregates = {}
@@ -4396,7 +4717,11 @@ local function captureMicroProfiler(requestData)
4396
4717
  local sampledFrameMax
4397
4718
  local lastProcessedFrameId
4398
4719
  local lastProcessedTimestampRaw
4399
- while eventsSampled < maxEvents and iterator:Step() do
4720
+ while eventsSampled < maxEvents do
4721
+ checkpoint()
4722
+ if not iterator:Step() then
4723
+ break
4724
+ end
4400
4725
  eventsSampled += 1
4401
4726
  local state = iterator:GetState()
4402
4727
  if not state then
@@ -4437,6 +4762,7 @@ local function captureMicroProfiler(requestData)
4437
4762
  exitEvents += 1
4438
4763
  local entry
4439
4764
  while #stack > 0 do
4765
+ checkpoint()
4440
4766
  -- ▼ Array.pop ▼
4441
4767
  local _length = #stack
4442
4768
  local _result = stack[_length]
@@ -4514,7 +4840,7 @@ local function captureMicroProfiler(requestData)
4514
4840
  _condition_2 = frameMax
4515
4841
  end
4516
4842
  local analysisFrameMax = _condition_2
4517
- local frameSummary = collectFrameSummary(session, analysisFrameMin, analysisFrameMax)
4843
+ local frameSummary = collectFrameSummary(session, analysisFrameMin, analysisFrameMax, checkpoint)
4518
4844
  local _frames = frameSummary.frames
4519
4845
  local _condition_3 = type(_frames) == "number"
4520
4846
  if _condition_3 then
@@ -4563,6 +4889,7 @@ local function captureMicroProfiler(requestData)
4563
4889
  local omittedBelowThreshold = 0
4564
4890
  local omittedByFilter = 0
4565
4891
  for timerId, aggregate in aggregates do
4892
+ checkpoint()
4566
4893
  local info = getTimerInfo(timerId)
4567
4894
  local totalUs = rawToUs(aggregate.inclusive_raw)
4568
4895
  if not includeIdle and isIdleTimer(info) then
@@ -4627,7 +4954,7 @@ local function captureMicroProfiler(requestData)
4627
4954
  if maxToAvg ~= nil then
4628
4955
  row.max_to_avg = maxToAvg
4629
4956
  end
4630
- local timerFrameImpact = summarizeFrameImpact(timerFrameAggregates[timerId], analysisFrameCount)
4957
+ local timerFrameImpact = summarizeFrameImpact(timerFrameAggregates[timerId], analysisFrameCount, checkpoint)
4631
4958
  for key, value in pairs(timerFrameImpact) do
4632
4959
  row[key] = value
4633
4960
  end
@@ -4653,12 +4980,14 @@ local function captureMicroProfiler(requestData)
4653
4980
  end
4654
4981
  for frameId, raw in timerFrames do
4655
4982
  addFrameRaw(groupFrames, frameId, raw)
4983
+ checkpoint()
4656
4984
  end
4657
4985
  end
4658
4986
  local perThread = timerThreadAggregates[timerId]
4659
4987
  if perThread ~= nil then
4660
4988
  local timerThreadSummaryRows = {}
4661
4989
  for threadId, threadAggregate in perThread do
4990
+ checkpoint()
4662
4991
  local threadAggregateRow = threadAggregates[threadId]
4663
4992
  if threadAggregateRow == nil then
4664
4993
  threadAggregateRow = {
@@ -4717,9 +5046,9 @@ local function captureMicroProfiler(requestData)
4717
5046
  }
4718
5047
  table.insert(timerThreadSummaryRows, _arg0)
4719
5048
  end
4720
- table.sort(timerThreadSummaryRows, function(a, b)
5049
+ cooperativeSort(timerThreadSummaryRows, function(a, b)
4721
5050
  return (a.inclusive_us) > (b.inclusive_us)
4722
- end)
5051
+ end, checkpoint)
4723
5052
  if maxRelatedTimers > 0 and #timerThreadSummaryRows > 0 then
4724
5053
  local topTimerThreads = {}
4725
5054
  do
@@ -4742,13 +5071,14 @@ local function captureMicroProfiler(requestData)
4742
5071
  end
4743
5072
  end
4744
5073
  end
4745
- table.sort(rows, function(a, b)
5074
+ cooperativeSort(rows, function(a, b)
4746
5075
  return (a.inclusive_us) > (b.inclusive_us)
4747
- end)
5076
+ end, checkpoint)
4748
5077
  local edgeRows = {}
4749
5078
  local parentRelationsByChild = {}
4750
5079
  local childRelationsByParent = {}
4751
5080
  for _, edge in edgeAggregates do
5081
+ checkpoint()
4752
5082
  local _parent_timer_id = edge.parent_timer_id
4753
5083
  local parentRow = rowsByTimerId[_parent_timer_id]
4754
5084
  local _child_timer_id = edge.child_timer_id
@@ -4819,21 +5149,24 @@ local function captureMicroProfiler(requestData)
4819
5149
  }
4820
5150
  table.insert(_childRelations, _arg0_1)
4821
5151
  end
4822
- table.sort(edgeRows, function(a, b)
5152
+ cooperativeSort(edgeRows, function(a, b)
4823
5153
  return (a.inclusive_us) > (b.inclusive_us)
4824
- end)
5154
+ end, checkpoint)
4825
5155
  if maxRelatedTimers > 0 then
4826
5156
  for _, relationRows in parentRelationsByChild do
4827
- table.sort(relationRows, function(a, b)
5157
+ checkpoint()
5158
+ cooperativeSort(relationRows, function(a, b)
4828
5159
  return (a.inclusive_us) > (b.inclusive_us)
4829
- end)
5160
+ end, checkpoint)
4830
5161
  end
4831
5162
  for _, relationRows in childRelationsByParent do
4832
- table.sort(relationRows, function(a, b)
5163
+ checkpoint()
5164
+ cooperativeSort(relationRows, function(a, b)
4833
5165
  return (a.inclusive_us) > (b.inclusive_us)
4834
- end)
5166
+ end, checkpoint)
4835
5167
  end
4836
5168
  for _, row in rows do
5169
+ checkpoint()
4837
5170
  local timerId = row.timer_id
4838
5171
  local parents = parentRelationsByChild[timerId]
4839
5172
  local children = childRelationsByParent[timerId]
@@ -4900,10 +5233,11 @@ local function captureMicroProfiler(requestData)
4900
5233
  local rowsByExclusive = {}
4901
5234
  for _, row in rows do
4902
5235
  table.insert(rowsByExclusive, row)
5236
+ checkpoint()
4903
5237
  end
4904
- table.sort(rowsByExclusive, function(a, b)
5238
+ cooperativeSort(rowsByExclusive, function(a, b)
4905
5239
  return (a.exclusive_us) > (b.exclusive_us)
4906
- end)
5240
+ end, checkpoint)
4907
5241
  local topTimersByExclusive = {}
4908
5242
  do
4909
5243
  local i = 0
@@ -4942,6 +5276,7 @@ local function captureMicroProfiler(requestData)
4942
5276
  end
4943
5277
  local threadRows = {}
4944
5278
  for threadId, aggregate in threadAggregates do
5279
+ checkpoint()
4945
5280
  local info = getThreadInfo(threadId)
4946
5281
  local inclusiveUs = rawToUs(aggregate.inclusive_raw)
4947
5282
  local exclusiveUs = rawToUs(aggregate.exclusive_raw)
@@ -4959,9 +5294,9 @@ local function captureMicroProfiler(requestData)
4959
5294
  pct_of_analyzed_wall = percent(inclusiveUs, analysisDurationUs),
4960
5295
  }
4961
5296
  local timerRows = threadTimerRows[threadId] or {}
4962
- table.sort(timerRows, function(a, b)
5297
+ cooperativeSort(timerRows, function(a, b)
4963
5298
  return (a.inclusive_us) > (b.inclusive_us)
4964
- end)
5299
+ end, checkpoint)
4965
5300
  local topThreadTimers = {}
4966
5301
  do
4967
5302
  local i = 0
@@ -4984,9 +5319,9 @@ local function captureMicroProfiler(requestData)
4984
5319
  end
4985
5320
  table.insert(threadRows, row)
4986
5321
  end
4987
- table.sort(threadRows, function(a, b)
5322
+ cooperativeSort(threadRows, function(a, b)
4988
5323
  return (a.inclusive_us) > (b.inclusive_us)
4989
- end)
5324
+ end, checkpoint)
4990
5325
  local topThreads = {}
4991
5326
  do
4992
5327
  local i = 0
@@ -5007,15 +5342,16 @@ local function captureMicroProfiler(requestData)
5007
5342
  end
5008
5343
  local groupRows = {}
5009
5344
  for _, aggregate in groupAggregates do
5345
+ checkpoint()
5010
5346
  local _group = aggregate.group
5011
5347
  local _condition_5 = groupTimerRows[_group]
5012
5348
  if _condition_5 == nil then
5013
5349
  _condition_5 = {}
5014
5350
  end
5015
5351
  local timerRows = _condition_5
5016
- table.sort(timerRows, function(a, b)
5352
+ cooperativeSort(timerRows, function(a, b)
5017
5353
  return (a.inclusive_us) > (b.inclusive_us)
5018
- end)
5354
+ end, checkpoint)
5019
5355
  local topGroupTimers = {}
5020
5356
  do
5021
5357
  local i = 0
@@ -5058,7 +5394,7 @@ local function captureMicroProfiler(requestData)
5058
5394
  pct_of_analyzed_wall = percent(inclusiveUs, analysisDurationUs),
5059
5395
  }
5060
5396
  local _group_1 = aggregate.group
5061
- local groupFrameImpact = summarizeFrameImpact(groupFrameAggregates[_group_1], analysisFrameCount)
5397
+ local groupFrameImpact = summarizeFrameImpact(groupFrameAggregates[_group_1], analysisFrameCount, checkpoint)
5062
5398
  for key, value in pairs(groupFrameImpact) do
5063
5399
  groupRow[key] = value
5064
5400
  end
@@ -5067,9 +5403,9 @@ local function captureMicroProfiler(requestData)
5067
5403
  end
5068
5404
  table.insert(groupRows, groupRow)
5069
5405
  end
5070
- table.sort(groupRows, function(a, b)
5406
+ cooperativeSort(groupRows, function(a, b)
5071
5407
  return (a.inclusive_us) > (b.inclusive_us)
5072
- end)
5408
+ end, checkpoint)
5073
5409
  local topGroups = {}
5074
5410
  do
5075
5411
  local i = 0
@@ -5091,10 +5427,11 @@ local function captureMicroProfiler(requestData)
5091
5427
  local groupRowsByExclusive = {}
5092
5428
  for _, row in groupRows do
5093
5429
  table.insert(groupRowsByExclusive, row)
5430
+ checkpoint()
5094
5431
  end
5095
- table.sort(groupRowsByExclusive, function(a, b)
5432
+ cooperativeSort(groupRowsByExclusive, function(a, b)
5096
5433
  return (a.exclusive_us) > (b.exclusive_us)
5097
- end)
5434
+ end, checkpoint)
5098
5435
  local topGroupsByExclusive = {}
5099
5436
  do
5100
5437
  local i = 0
@@ -5126,6 +5463,7 @@ local function captureMicroProfiler(requestData)
5126
5463
  local openStackEntriesAtEnd = 0
5127
5464
  for _, stack in stacks do
5128
5465
  openStackEntriesAtEnd += #stack
5466
+ checkpoint()
5129
5467
  end
5130
5468
  local iteratorFinished = eventsSampled < maxEvents
5131
5469
  local partialReasons = {}
@@ -5148,21 +5486,25 @@ local function captureMicroProfiler(requestData)
5148
5486
  for _, row in rows do
5149
5487
  local _arg0 = pickFields(row, timerFields)
5150
5488
  table.insert(timerIndex, _arg0)
5489
+ checkpoint()
5151
5490
  end
5152
5491
  local groupIndex = {}
5153
5492
  for _, row in groupRows do
5154
5493
  local _arg0 = pickFields(row, groupFields)
5155
5494
  table.insert(groupIndex, _arg0)
5495
+ checkpoint()
5156
5496
  end
5157
5497
  local threadIndex = {}
5158
5498
  for _, row in threadRows do
5159
5499
  local _arg0 = pickFields(row, threadFields)
5160
5500
  table.insert(threadIndex, _arg0)
5501
+ checkpoint()
5161
5502
  end
5162
5503
  local edgeIndex = {}
5163
5504
  for _, row in edgeRows do
5164
5505
  local _arg0 = pickFields(row, edgeFields)
5165
5506
  table.insert(edgeIndex, _arg0)
5507
+ checkpoint()
5166
5508
  end
5167
5509
  comparisonIndex = {
5168
5510
  timers = timerIndex,
@@ -5291,7 +5633,7 @@ local function captureMicroProfiler(requestData)
5291
5633
  result.snapshot_object_size_bytes = session:GetObjSize()
5292
5634
  end
5293
5635
  if includeRawBuffer then
5294
- result.raw_snapshot_base64 = encodeBase64(snapshot)
5636
+ result.raw_snapshot_base64 = encodeBase64(snapshot, checkpoint)
5295
5637
  end
5296
5638
  iterator:Dispose()
5297
5639
  session:Dispose()
@@ -5860,7 +6202,6 @@ local function getProjectStructure(requestData)
5860
6202
  name = instance.Name,
5861
6203
  className = instance.ClassName,
5862
6204
  path = getInstancePath(instance),
5863
- children = {},
5864
6205
  }
5865
6206
  if instance:IsA("LuaSourceContainer") then
5866
6207
  node.hasSource = true
@@ -5900,7 +6241,7 @@ local function getProjectStructure(requestData)
5900
6241
  -- ▲ ReadonlyArray.filter ▲
5901
6242
  children = _newValue
5902
6243
  end
5903
- local nodeChildren = node.children
6244
+ local nodeChildren = {}
5904
6245
  local childCount = #children
5905
6246
  if childCount > 20 and depth < maxDepth then
5906
6247
  local classGroups = {}
@@ -5975,6 +6316,9 @@ local function getProjectStructure(requestData)
5975
6316
  table.insert(nodeChildren, _arg0)
5976
6317
  end
5977
6318
  end
6319
+ if #nodeChildren > 0 then
6320
+ node.children = nodeChildren
6321
+ end
5978
6322
  return node
5979
6323
  end
5980
6324
  local result = getStructure(startInstance, 0)
@@ -6527,16 +6871,6 @@ local finishRecording = _binding_1.finishRecording
6527
6871
  local SOURCE_TRUNCATE_CHAR_BUDGET = 25000
6528
6872
  local SOURCE_TRUNCATE_LINE_BUDGET = 400
6529
6873
  local SOURCE_TRUNCATE_TO_LINES = 300
6530
- local function normalizeEscapes(s)
6531
- local result = s
6532
- result = (string.gsub(result, "\\\\", "\x01"))
6533
- result = (string.gsub(result, "\\n", "\n"))
6534
- result = (string.gsub(result, "\\t", "\t"))
6535
- result = (string.gsub(result, "\\r", "\r"))
6536
- result = (string.gsub(result, '\\"', '"'))
6537
- result = (string.gsub(result, "\x01", "\\"))
6538
- return result
6539
- end
6540
6874
  local function getTopServiceName(instance)
6541
6875
  local topServiceInst = instance
6542
6876
  while topServiceInst.Parent and topServiceInst.Parent ~= game do
@@ -6664,7 +6998,7 @@ end
6664
6998
  local function setScriptSource(requestData)
6665
6999
  local instancePath = requestData.instancePath
6666
7000
  local newSource = requestData.source
6667
- if not (instancePath ~= "" and instancePath) or not (newSource ~= "" and newSource) then
7001
+ if not (instancePath ~= "" and instancePath) or not (type(newSource) == "string") then
6668
7002
  return {
6669
7003
  error = "Instance path and source are required",
6670
7004
  }
@@ -6680,7 +7014,8 @@ local function setScriptSource(requestData)
6680
7014
  error = `Instance is not a script-like object: {instance.ClassName}`,
6681
7015
  }
6682
7016
  end
6683
- local sourceToSet = normalizeEscapes(newSource)
7017
+ -- Communication has already JSON-decoded the poll payload; source text is exact at this boundary.
7018
+ local sourceToSet = newSource
6684
7019
  local recordingId = beginRecording(`Set script source: {instance.Name}`)
6685
7020
  local readSuccess, readResult = pcall(function()
6686
7021
  return #readScriptSource(instance)
@@ -6704,40 +7039,9 @@ local function setScriptSource(requestData)
6704
7039
  message = `Script source updated successfully ({if applyResult.method == "UpdateSourceAsync" then "editor-safe" else "direct assignment"})`,
6705
7040
  }
6706
7041
  end
6707
- local replaceSuccess, replaceResult = pcall(function()
6708
- local parent = instance.Parent
6709
- local name = instance.Name
6710
- local className = instance.ClassName
6711
- local wasBaseScript = instance:IsA("BaseScript")
6712
- local enabled = if wasBaseScript then instance.Enabled else nil
6713
- local newScript = Instance.new(className)
6714
- newScript.Name = name
6715
- -- @rbxts/types does not expose PluginSecurity Source writes.
6716
- local writableNewScript = newScript
6717
- writableNewScript.Source = sourceToSet
6718
- if readScriptSource(newScript) ~= sourceToSet then
6719
- error("Replacement script source did not match the requested source")
6720
- end
6721
- if wasBaseScript and enabled ~= nil then
6722
- local newBaseScript = newScript
6723
- newBaseScript.Enabled = enabled
6724
- end
6725
- newScript.Parent = parent
6726
- instance:Destroy()
6727
- return {
6728
- success = true,
6729
- instancePath = getInstancePath(newScript),
6730
- method = "replace",
6731
- message = "Script replaced successfully with new source",
6732
- }
6733
- end)
6734
- if replaceSuccess then
6735
- finishRecording(recordingId, true)
6736
- return replaceResult
6737
- end
6738
7042
  finishRecording(recordingId, false)
6739
7043
  return {
6740
- error = `Failed to set script source. {applyResult.error} Replace method failed: {replaceResult}`,
7044
+ error = `Failed to set script source: {applyResult.error}`,
6741
7045
  }
6742
7046
  end
6743
7047
  local function editScriptLines(requestData)
@@ -6750,8 +7054,6 @@ local function editScriptLines(requestData)
6750
7054
  error = "Instance path, old_string, and new_string are required",
6751
7055
  }
6752
7056
  end
6753
- oldString = normalizeEscapes(oldString)
6754
- newString = normalizeEscapes(newString)
6755
7057
  local instance = getInstanceByPath(instancePath)
6756
7058
  if not instance then
6757
7059
  return {
@@ -6848,7 +7150,6 @@ local function insertScriptLines(requestData)
6848
7150
  error = "Instance path and newContent are required",
6849
7151
  }
6850
7152
  end
6851
- newContent = normalizeEscapes(newContent)
6852
7153
  local instance = getInstanceByPath(instancePath)
6853
7154
  if not instance then
6854
7155
  return {
@@ -9272,9 +9573,10 @@ return {
9272
9573
  <Properties>
9273
9574
  <string name="Name">State</string>
9274
9575
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
9275
- local CURRENT_VERSION = "3.0.0"
9576
+ local CURRENT_VERSION = "3.0.2"
9276
9577
  local PLUGIN_VARIANT = "main"
9277
9578
  local BASE_PORT = 58741
9579
+ local POLL_REQUEST_TIMEOUT_SECONDS = 20
9278
9580
  local function createConnection(port)
9279
9581
  return {
9280
9582
  port = port,
@@ -9303,6 +9605,7 @@ return {
9303
9605
  CURRENT_VERSION = CURRENT_VERSION,
9304
9606
  PLUGIN_VARIANT = PLUGIN_VARIANT,
9305
9607
  BASE_PORT = BASE_PORT,
9608
+ POLL_REQUEST_TIMEOUT_SECONDS = POLL_REQUEST_TIMEOUT_SECONDS,
9306
9609
  getActiveConnection = getActiveConnection,
9307
9610
  }
9308
9611
  ]]></string>