@chrrxs/robloxstudio-mcp 3.0.3 → 3.0.5

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.
@@ -16,6 +16,7 @@ local cleanupEditBridgeArtifacts = _EvalBridges.cleanupEditBridgeArtifacts
16
16
  local ensureRuntimeBridgeInstalled = _EvalBridges.ensureRuntimeBridgeInstalled
17
17
  local RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer")
18
18
  local StopPlayMonitor = TS.import(script, script, "modules", "StopPlayMonitor")
19
+ local StudioEventStream = TS.import(script, script, "modules", "StudioEventStream")
19
20
  local BreakpointHandlers = TS.import(script, script, "modules", "handlers", "BreakpointHandlers")
20
21
  local RenderMonitor = TS.import(script, script, "modules", "RenderMonitor")
21
22
  -- Track render-loop liveness so input/screenshot tools can report "window
@@ -32,8 +33,9 @@ RuntimeLogBuffer.install()
32
33
  StopPlayMonitor.init(plugin)
33
34
  BreakpointHandlers.init(plugin)
34
35
  ServerUrlSettings.init(plugin)
36
+ local startupRole = ClientBroker.forkRole()
35
37
  local function applyRememberedServerUrl()
36
- if ClientBroker.forkRole() == "client" then
38
+ if startupRole == "client" then
37
39
  return nil
38
40
  end
39
41
  local rememberedServerUrl = ServerUrlSettings.readServerUrl()
@@ -49,6 +51,15 @@ local function applyRememberedServerUrl()
49
51
  ClientBroker.setServerUrl(rememberedServerUrl)
50
52
  end
51
53
  applyRememberedServerUrl()
54
+ -- Play DataModels do not reliably fire Plugin.Unloading before Studio tears
55
+ -- down their VM. Close the persistent WebStreamClient while BindToClose still
56
+ -- gives this server peer a live execution context; otherwise each play cycle
57
+ -- can retain one native event stream until Studio itself is restarted. The
58
+ -- transport releases that native stream before yielding to unregister the
59
+ -- logical server peer.
60
+ if startupRole == "server" then
61
+ game:BindToClose(StudioEventStream.suspendForShutdown)
62
+ end
52
63
  UI.init(plugin)
53
64
  local elements = UI.getElements()
54
65
  local ICON_DISCONNECTED = "rbxassetid://75876056391496"
@@ -91,7 +102,7 @@ task.delay(TOOLBAR_REGISTRATION_DELAY_SECONDS, registerToolbarButton)
91
102
  -- play peers' plugin instances load without ever registering. Run after a
92
103
  -- short delay so the UI/State have a chance to initialize first.
93
104
  task.delay(2, function()
94
- local role = ClientBroker.forkRole()
105
+ local role = startupRole
95
106
  if role == "edit" then
96
107
  cleanupEditBridgeArtifacts()
97
108
  else
@@ -133,7 +144,10 @@ task.delay(2, function()
133
144
  -- The play-server DM is the only one where StudioTestService:EndTest is
134
145
  -- legal, so the stop-play monitor lives here. It consumes tokenized
135
146
  -- stop requests from plugin settings and acknowledges EndTest results.
136
- StopPlayMonitor.startMonitor()
147
+ StopPlayMonitor.startMonitor({
148
+ beforeEndTest = StudioEventStream.suspendForShutdown,
149
+ afterEndTestFailure = StudioEventStream.resumeAfterShutdownFailure,
150
+ })
137
151
  elseif role == "client" then
138
152
  ClientBroker.setupClientBroker()
139
153
  end
@@ -880,12 +894,12 @@ local routeMap = {
880
894
  ["/api/get-memory-breakdown"] = MemoryHandlers.getMemoryBreakdown,
881
895
  ["/api/get-scene-analysis"] = SceneAnalysisHandlers.getSceneAnalysis,
882
896
  }
883
- local function processRequest(request)
897
+ local function processRequest(request, context)
884
898
  local endpoint = request.endpoint
885
899
  local data = request.data or {}
886
900
  local handler = routeMap[endpoint]
887
901
  if handler then
888
- return handler(data)
902
+ return handler(data, context)
889
903
  else
890
904
  return {
891
905
  error = `Unknown endpoint: {endpoint}`,
@@ -905,7 +919,7 @@ local function getConnectionStatus()
905
919
  end
906
920
  return "connecting"
907
921
  end
908
- local function dispatchStreamRequest(request)
922
+ local function dispatchStreamRequest(request, context)
909
923
  if request.logicalSessionId ~= PluginSession.id then
910
924
  return ClientBroker.dispatchClientRequest(request.logicalSessionId, request.target, request.endpoint, request.data)
911
925
  end
@@ -922,7 +936,7 @@ local function dispatchStreamRequest(request)
922
936
  return processRequest({
923
937
  endpoint = request.endpoint,
924
938
  data = request.data,
925
- })
939
+ }, context)
926
940
  end
927
941
  local function handleReady(response)
928
942
  local conn = State.getActiveConnection()
@@ -1154,6 +1168,81 @@ return {
1154
1168
  </Properties>
1155
1169
  </Item>
1156
1170
  <Item class="ModuleScript" referent="5">
1171
+ <Properties>
1172
+ <string name="Name">CooperativeJobRunner</string>
1173
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
1174
+ local DEADLINE_EXCEEDED = "__RSMCP_COOPERATIVE_JOB_DEADLINE_EXCEEDED__"
1175
+ local CANCELLED = "__RSMCP_COOPERATIVE_JOB_CANCELLED__"
1176
+ local MAX_SLICE_SECONDS = 0.008
1177
+ local CLOCK_CHECK_INTERVAL = 64
1178
+ local activeJobs = {}
1179
+ local function runExclusive(key, execution, work)
1180
+ local _key = key
1181
+ local activeRequestId = activeJobs[_key]
1182
+ if activeRequestId ~= nil then
1183
+ return {
1184
+ error = "plugin_busy",
1185
+ activeRequestId = activeRequestId,
1186
+ }
1187
+ end
1188
+ local _key_1 = key
1189
+ local _requestId = execution.requestId
1190
+ activeJobs[_key_1] = _requestId
1191
+ local sliceStartedAt = os.clock()
1192
+ local operationsUntilClockCheck = 0
1193
+ local control = {
1194
+ checkpoint = function(self)
1195
+ local _result = execution.isCancelled
1196
+ if _result ~= nil then
1197
+ _result = _result()
1198
+ end
1199
+ if _result then
1200
+ error(CANCELLED, 0)
1201
+ end
1202
+ if operationsUntilClockCheck > 0 then
1203
+ operationsUntilClockCheck -= 1
1204
+ return nil
1205
+ end
1206
+ operationsUntilClockCheck = CLOCK_CHECK_INTERVAL - 1
1207
+ local now = os.clock()
1208
+ if execution.deadlineAt ~= nil and now >= execution.deadlineAt then
1209
+ error(DEADLINE_EXCEEDED, 0)
1210
+ end
1211
+ if now - sliceStartedAt >= MAX_SLICE_SECONDS then
1212
+ task.wait()
1213
+ sliceStartedAt = os.clock()
1214
+ end
1215
+ end,
1216
+ }
1217
+ local ok, result = pcall(function()
1218
+ return work(control)
1219
+ end)
1220
+ local _key_2 = key
1221
+ activeJobs[_key_2] = nil
1222
+ if not ok then
1223
+ if result == DEADLINE_EXCEEDED then
1224
+ return {
1225
+ error = "deadline_exceeded",
1226
+ requestId = execution.requestId,
1227
+ }
1228
+ end
1229
+ if result == CANCELLED then
1230
+ return {
1231
+ error = "cancelled",
1232
+ requestId = execution.requestId,
1233
+ }
1234
+ end
1235
+ error(result, 0)
1236
+ end
1237
+ return result
1238
+ end
1239
+ return {
1240
+ runExclusive = runExclusive,
1241
+ }
1242
+ ]]></string>
1243
+ </Properties>
1244
+ </Item>
1245
+ <Item class="ModuleScript" referent="6">
1157
1246
  <Properties>
1158
1247
  <string name="Name">EvalBridges</string>
1159
1248
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -1273,9 +1362,9 @@ local function computeBridgeStamp()
1273
1362
  for i = 1, #combined do
1274
1363
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1275
1364
  end
1276
- -- "3.0.3" is replaced with the package version at package time
1365
+ -- "3.0.5" is replaced with the package version at package time
1277
1366
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1278
- return `{tostring(h)}-3.0.3`
1367
+ return `{tostring(h)}-3.0.5`
1279
1368
  end
1280
1369
  local BRIDGE_STAMP = computeBridgeStamp()
1281
1370
  local function setSource(scriptInst, source)
@@ -1428,11 +1517,11 @@ return {
1428
1517
  ]]></string>
1429
1518
  </Properties>
1430
1519
  </Item>
1431
- <Item class="Folder" referent="6">
1520
+ <Item class="Folder" referent="7">
1432
1521
  <Properties>
1433
1522
  <string name="Name">handlers</string>
1434
1523
  </Properties>
1435
- <Item class="ModuleScript" referent="7">
1524
+ <Item class="ModuleScript" referent="8">
1436
1525
  <Properties>
1437
1526
  <string name="Name">AssetHandlers</string>
1438
1527
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -1873,7 +1962,7 @@ return {
1873
1962
  ]]></string>
1874
1963
  </Properties>
1875
1964
  </Item>
1876
- <Item class="ModuleScript" referent="8">
1965
+ <Item class="ModuleScript" referent="9">
1877
1966
  <Properties>
1878
1967
  <string name="Name">BreakpointHandlers</string>
1879
1968
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -2391,7 +2480,7 @@ return {
2391
2480
  ]]></string>
2392
2481
  </Properties>
2393
2482
  </Item>
2394
- <Item class="ModuleScript" referent="9">
2483
+ <Item class="ModuleScript" referent="10">
2395
2484
  <Properties>
2396
2485
  <string name="Name">CaptureHandlers</string>
2397
2486
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -2635,7 +2724,7 @@ return {
2635
2724
  ]]></string>
2636
2725
  </Properties>
2637
2726
  </Item>
2638
- <Item class="ModuleScript" referent="10">
2727
+ <Item class="ModuleScript" referent="11">
2639
2728
  <Properties>
2640
2729
  <string name="Name">EvalRuntimeHandlers</string>
2641
2730
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -2789,7 +2878,7 @@ return {
2789
2878
  ]]></string>
2790
2879
  </Properties>
2791
2880
  </Item>
2792
- <Item class="ModuleScript" referent="11">
2881
+ <Item class="ModuleScript" referent="12">
2793
2882
  <Properties>
2794
2883
  <string name="Name">GenerateModelHandlers</string>
2795
2884
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -2994,7 +3083,7 @@ return {
2994
3083
  ]]></string>
2995
3084
  </Properties>
2996
3085
  </Item>
2997
- <Item class="ModuleScript" referent="12">
3086
+ <Item class="ModuleScript" referent="13">
2998
3087
  <Properties>
2999
3088
  <string name="Name">InputHandlers</string>
3000
3089
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3194,7 +3283,7 @@ return {
3194
3283
  ]]></string>
3195
3284
  </Properties>
3196
3285
  </Item>
3197
- <Item class="ModuleScript" referent="13">
3286
+ <Item class="ModuleScript" referent="14">
3198
3287
  <Properties>
3199
3288
  <string name="Name">LogHandlers</string>
3200
3289
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3220,7 +3309,7 @@ return {
3220
3309
  ]]></string>
3221
3310
  </Properties>
3222
3311
  </Item>
3223
- <Item class="ModuleScript" referent="14">
3312
+ <Item class="ModuleScript" referent="15">
3224
3313
  <Properties>
3225
3314
  <string name="Name">MemoryHandlers</string>
3226
3315
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3288,7 +3377,7 @@ return {
3288
3377
  ]]></string>
3289
3378
  </Properties>
3290
3379
  </Item>
3291
- <Item class="ModuleScript" referent="15">
3380
+ <Item class="ModuleScript" referent="16">
3292
3381
  <Properties>
3293
3382
  <string name="Name">MetadataHandlers</string>
3294
3383
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3619,7 +3708,7 @@ return {
3619
3708
  ]]></string>
3620
3709
  </Properties>
3621
3710
  </Item>
3622
- <Item class="ModuleScript" referent="16">
3711
+ <Item class="ModuleScript" referent="17">
3623
3712
  <Properties>
3624
3713
  <string name="Name">MicroProfilerHandlers</string>
3625
3714
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -5372,7 +5461,7 @@ return {
5372
5461
  ]]></string>
5373
5462
  </Properties>
5374
5463
  </Item>
5375
- <Item class="ModuleScript" referent="17">
5464
+ <Item class="ModuleScript" referent="18">
5376
5465
  <Properties>
5377
5466
  <string name="Name">PropertyHandlers</string>
5378
5467
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -5457,12 +5546,14 @@ return {
5457
5546
  ]]></string>
5458
5547
  </Properties>
5459
5548
  </Item>
5460
- <Item class="ModuleScript" referent="18">
5549
+ <Item class="ModuleScript" referent="19">
5461
5550
  <Properties>
5462
5551
  <string name="Name">QueryHandlers</string>
5463
5552
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
5464
5553
  local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
5465
5554
  local Utils = TS.import(script, script.Parent.Parent, "Utils")
5555
+ local CooperativeJobRunner = TS.import(script, script.Parent.Parent, "CooperativeJobRunner")
5556
+ local ScriptSearch = TS.import(script, script.Parent.Parent, "ScriptSearch")
5466
5557
  local _binding = Utils
5467
5558
  local getInstancePath = _binding.getInstancePath
5468
5559
  local getInstanceByPath = _binding.getInstanceByPath
@@ -6055,265 +6146,46 @@ local function getProjectStructure(requestData)
6055
6146
  result.timestamp = tick()
6056
6147
  return result
6057
6148
  end
6058
- -- Split a Lua pattern on TOP-LEVEL "|" into alternatives. Lua patterns have no
6059
- -- alternation operator, so "foo|bar" would otherwise be matched as the literal
6060
- -- text "foo|bar" and silently never hit. "%|" stays a literal pipe, and "%bxy"
6061
- -- keeps both balanced-match delimiter characters.
6062
- local function splitLuaAlternation(pattern)
6063
- local parts = {}
6064
- local current = ""
6065
- local i = 1
6066
- local n = #pattern
6067
- local inCharClass = false
6068
- while i <= n do
6069
- local c = string.sub(pattern, i, i)
6070
- if c == "%" then
6071
- if string.sub(pattern, i + 1, i + 1) == "b" then
6072
- current ..= string.sub(pattern, i, math.min(i + 3, n))
6073
- i += 4
6074
- continue
6075
- end
6076
- -- Preserve an escape pair (e.g. %|, %., %d) intact.
6077
- current ..= string.sub(pattern, i, i + 1)
6078
- i += 2
6079
- elseif c == "[" then
6080
- inCharClass = true
6081
- current ..= c
6082
- i += 1
6083
- elseif c == "]" then
6084
- inCharClass = false
6085
- current ..= c
6086
- i += 1
6087
- elseif c == "|" and not inCharClass then
6088
- local _current = current
6089
- table.insert(parts, _current)
6090
- current = ""
6091
- i += 1
6092
- else
6093
- current ..= c
6094
- i += 1
6095
- end
6096
- end
6097
- local _current = current
6098
- table.insert(parts, _current)
6099
- return parts
6100
- end
6101
- -- Return the earliest match across alternatives (mirrors regex alternation).
6102
- local function findFirstPattern(line, alternatives)
6103
- local bestStart
6104
- local bestEnd
6105
- for _, alt in alternatives do
6106
- if alt == "" then
6107
- continue
6108
- end
6109
- local s, e = string.find(line, alt)
6110
- if s ~= nil and (bestStart == nil or s < bestStart) then
6111
- bestStart = s
6112
- bestEnd = e
6149
+ local scriptSearch = ScriptSearch.createScriptSearch({
6150
+ resolveRoot = function(self, path)
6151
+ return getInstanceByPath(path)
6152
+ end,
6153
+ getChildren = function(self, instance)
6154
+ return instance:GetChildren()
6155
+ end,
6156
+ readScript = function(self, instance, classFilter)
6157
+ if not instance:IsA("LuaSourceContainer") or (classFilter ~= nil and instance.ClassName ~= classFilter) then
6158
+ return nil
6113
6159
  end
6114
- end
6115
- return { bestStart, bestEnd }
6116
- end
6117
- local function grepScripts(requestData)
6118
- local pattern = requestData.pattern
6119
- if not (pattern ~= "" and pattern) then
6120
- return {
6121
- error = "pattern is required",
6122
- }
6123
- end
6124
- local _condition = (requestData.usePattern)
6125
- if _condition == nil then
6126
- _condition = false
6127
- end
6128
- local usePattern = _condition
6129
- if usePattern and requestData.caseSensitive == false then
6130
- return {
6131
- error = "Case-insensitive Lua pattern search is not supported. Omit caseSensitive or pass caseSensitive: true with usePattern: true, or use literal search.",
6160
+ local snapshot = {
6161
+ instancePath = getInstancePath(instance),
6162
+ name = instance.Name,
6163
+ className = instance.ClassName,
6164
+ source = readScriptSource(instance),
6132
6165
  }
6133
- end
6134
- local _result
6135
- if usePattern then
6136
- _result = true
6137
- else
6138
- local _condition_1 = (requestData.caseSensitive)
6139
- if _condition_1 == nil then
6140
- _condition_1 = false
6166
+ if instance:IsA("BaseScript") then
6167
+ snapshot.enabled = instance.Enabled
6141
6168
  end
6142
- _result = _condition_1
6143
- end
6144
- local caseSensitive = _result
6145
- local _condition_1 = (requestData.contextLines)
6146
- if _condition_1 == nil then
6147
- _condition_1 = 0
6148
- end
6149
- local contextLines = _condition_1
6150
- local _condition_2 = (requestData.maxResults)
6151
- if _condition_2 == nil then
6152
- _condition_2 = 100
6153
- end
6154
- local maxResults = _condition_2
6155
- local _condition_3 = (requestData.maxResultsPerScript)
6156
- if _condition_3 == nil then
6157
- _condition_3 = 0
6158
- end
6159
- local maxResultsPerScript = _condition_3
6160
- local _condition_4 = (requestData.filesOnly)
6161
- if _condition_4 == nil then
6162
- _condition_4 = false
6163
- end
6164
- local filesOnly = _condition_4
6165
- local _condition_5 = (requestData.path)
6166
- if _condition_5 == nil then
6167
- _condition_5 = ""
6168
- end
6169
- local searchPath = _condition_5
6170
- local classFilter = requestData.classFilter
6171
- local startInstance = if searchPath ~= "" then getInstanceByPath(searchPath) else game
6172
- if not startInstance then
6173
- return {
6174
- error = `Path not found: {searchPath}`,
6175
- }
6169
+ return snapshot
6170
+ end,
6171
+ })
6172
+ local function grepScripts(requestData, execution)
6173
+ local result = CooperativeJobRunner.runExclusive("script-source-search", execution, function(control)
6174
+ return scriptSearch.search(requestData, control)
6175
+ end)
6176
+ if result.error == "plugin_busy" then
6177
+ local _object = table.clone(result)
6178
+ setmetatable(_object, nil)
6179
+ _object.message = "Another grep_scripts request is already running in this Studio DataModel. Retry after it completes."
6180
+ return _object
6176
6181
  end
6177
- -- Prepare pattern for matching
6178
- local searchPattern = if caseSensitive then pattern else string.lower(pattern)
6179
- -- Pre-split top-level "|" alternation once (pattern mode only).
6180
- local patternAlternatives = if usePattern then splitLuaAlternation(searchPattern) else nil
6181
- local results = {}
6182
- local totalMatches = 0
6183
- local scriptsSearched = 0
6184
- local hitLimit = false
6185
- local function searchInstance(instance)
6186
- if hitLimit then
6187
- return nil
6188
- end
6189
- if instance:IsA("LuaSourceContainer") then
6190
- -- Apply class filter
6191
- if classFilter ~= "" and classFilter then
6192
- local _exp = string.lower(instance.ClassName)
6193
- local _arg0 = string.lower(classFilter)
6194
- local _value = (string.find(_exp, _arg0))
6195
- if not (_value ~= 0 and _value == _value and _value) then
6196
- return nil
6197
- end
6198
- end
6199
- scriptsSearched += 1
6200
- local source = readScriptSource(instance)
6201
- local lines = Utils.splitLines(source)
6202
- local scriptMatches = {}
6203
- local scriptMatchCount = 0
6204
- for i = 0, #lines - 1 do
6205
- if hitLimit then
6206
- break
6207
- end
6208
- if maxResultsPerScript > 0 and scriptMatchCount >= maxResultsPerScript then
6209
- break
6210
- end
6211
- local line = lines[i + 1]
6212
- local searchLine = if caseSensitive then line else string.lower(line)
6213
- local matchStart
6214
- local matchEnd
6215
- if usePattern then
6216
- local _binding_1 = findFirstPattern(searchLine, patternAlternatives)
6217
- matchStart = _binding_1[1]
6218
- matchEnd = _binding_1[2]
6219
- else
6220
- matchStart, matchEnd = string.find(searchLine, searchPattern, 1, true)
6221
- end
6222
- if matchStart ~= nil then
6223
- scriptMatchCount += 1
6224
- totalMatches += 1
6225
- if totalMatches > maxResults then
6226
- hitLimit = true
6227
- break
6228
- end
6229
- if not filesOnly then
6230
- -- Gather context lines
6231
- local before = {}
6232
- local after = {}
6233
- if contextLines > 0 then
6234
- local beforeStart = math.max(0, i - contextLines)
6235
- do
6236
- local j = beforeStart
6237
- local _shouldIncrement = false
6238
- while true do
6239
- if _shouldIncrement then
6240
- j += 1
6241
- else
6242
- _shouldIncrement = true
6243
- end
6244
- if not (j < i) then
6245
- break
6246
- end
6247
- local _arg0 = lines[j + 1]
6248
- table.insert(before, _arg0)
6249
- end
6250
- end
6251
- local afterEnd = math.min(#lines - 1, i + contextLines)
6252
- do
6253
- local j = i + 1
6254
- local _shouldIncrement = false
6255
- while true do
6256
- if _shouldIncrement then
6257
- j += 1
6258
- else
6259
- _shouldIncrement = true
6260
- end
6261
- if not (j <= afterEnd) then
6262
- break
6263
- end
6264
- local _arg0 = lines[j + 1]
6265
- table.insert(after, _arg0)
6266
- end
6267
- end
6268
- end
6269
- local _arg0 = {
6270
- line = i + 1,
6271
- column = matchStart,
6272
- text = line,
6273
- before = before,
6274
- after = after,
6275
- }
6276
- table.insert(scriptMatches, _arg0)
6277
- end
6278
- end
6279
- end
6280
- if scriptMatchCount > 0 then
6281
- local scriptResult = {
6282
- instancePath = getInstancePath(instance),
6283
- name = instance.Name,
6284
- className = instance.ClassName,
6285
- matches = scriptMatches,
6286
- }
6287
- if instance:IsA("BaseScript") then
6288
- scriptResult.enabled = instance.Enabled
6289
- end
6290
- table.insert(results, scriptResult)
6291
- end
6292
- end
6293
- for _, child in instance:GetChildren() do
6294
- if hitLimit then
6295
- return nil
6296
- end
6297
- searchInstance(child)
6298
- end
6182
+ if result.error == "deadline_exceeded" then
6183
+ local _object = table.clone(result)
6184
+ setmetatable(_object, nil)
6185
+ _object.message = "grep_scripts exceeded its bridge deadline before the scan completed."
6186
+ return _object
6299
6187
  end
6300
- searchInstance(startInstance)
6301
- return {
6302
- results = results,
6303
- pattern = pattern,
6304
- totalMatches = if hitLimit then `>{maxResults}` else totalMatches,
6305
- scriptsSearched = scriptsSearched,
6306
- scriptsMatched = #results,
6307
- truncated = hitLimit,
6308
- options = {
6309
- caseSensitive = caseSensitive,
6310
- contextLines = contextLines,
6311
- usePattern = usePattern,
6312
- filesOnly = filesOnly,
6313
- maxResults = maxResults,
6314
- maxResultsPerScript = maxResultsPerScript,
6315
- },
6316
- }
6188
+ return result
6317
6189
  end
6318
6190
  return {
6319
6191
  getFileTree = getFileTree,
@@ -6329,7 +6201,7 @@ return {
6329
6201
  ]]></string>
6330
6202
  </Properties>
6331
6203
  </Item>
6332
- <Item class="ModuleScript" referent="19">
6204
+ <Item class="ModuleScript" referent="20">
6333
6205
  <Properties>
6334
6206
  <string name="Name">SceneAnalysisHandlers</string>
6335
6207
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -6578,7 +6450,7 @@ return {
6578
6450
  ]]></string>
6579
6451
  </Properties>
6580
6452
  </Item>
6581
- <Item class="ModuleScript" referent="20">
6453
+ <Item class="ModuleScript" referent="21">
6582
6454
  <Properties>
6583
6455
  <string name="Name">ScriptHandlers</string>
6584
6456
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -7250,7 +7122,7 @@ return {
7250
7122
  ]]></string>
7251
7123
  </Properties>
7252
7124
  </Item>
7253
- <Item class="ModuleScript" referent="21">
7125
+ <Item class="ModuleScript" referent="22">
7254
7126
  <Properties>
7255
7127
  <string name="Name">ScriptProfilerHandlers</string>
7256
7128
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -7713,7 +7585,7 @@ return {
7713
7585
  ]]></string>
7714
7586
  </Properties>
7715
7587
  </Item>
7716
- <Item class="ModuleScript" referent="22">
7588
+ <Item class="ModuleScript" referent="23">
7717
7589
  <Properties>
7718
7590
  <string name="Name">SerializationHandlers</string>
7719
7591
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -7899,7 +7771,7 @@ return {
7899
7771
  ]]></string>
7900
7772
  </Properties>
7901
7773
  </Item>
7902
- <Item class="ModuleScript" referent="23">
7774
+ <Item class="ModuleScript" referent="24">
7903
7775
  <Properties>
7904
7776
  <string name="Name">TestHandlers</string>
7905
7777
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8278,7 +8150,7 @@ return {
8278
8150
  </Properties>
8279
8151
  </Item>
8280
8152
  </Item>
8281
- <Item class="ModuleScript" referent="24">
8153
+ <Item class="ModuleScript" referent="25">
8282
8154
  <Properties>
8283
8155
  <string name="Name">HttpDiagnostics</string>
8284
8156
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8359,7 +8231,7 @@ return {
8359
8231
  ]]></string>
8360
8232
  </Properties>
8361
8233
  </Item>
8362
- <Item class="ModuleScript" referent="25">
8234
+ <Item class="ModuleScript" referent="26">
8363
8235
  <Properties>
8364
8236
  <string name="Name">LuauExec</string>
8365
8237
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8793,7 +8665,7 @@ return {
8793
8665
  ]]></string>
8794
8666
  </Properties>
8795
8667
  </Item>
8796
- <Item class="ModuleScript" referent="26">
8668
+ <Item class="ModuleScript" referent="27">
8797
8669
  <Properties>
8798
8670
  <string name="Name">PluginSession</string>
8799
8671
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8885,7 +8757,7 @@ return {
8885
8757
  ]]></string>
8886
8758
  </Properties>
8887
8759
  </Item>
8888
- <Item class="ModuleScript" referent="27">
8760
+ <Item class="ModuleScript" referent="28">
8889
8761
  <Properties>
8890
8762
  <string name="Name">Recording</string>
8891
8763
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8915,7 +8787,7 @@ return {
8915
8787
  ]]></string>
8916
8788
  </Properties>
8917
8789
  </Item>
8918
- <Item class="ModuleScript" referent="28">
8790
+ <Item class="ModuleScript" referent="29">
8919
8791
  <Properties>
8920
8792
  <string name="Name">RenderMonitor</string>
8921
8793
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -8983,7 +8855,7 @@ return {
8983
8855
  ]]></string>
8984
8856
  </Properties>
8985
8857
  </Item>
8986
- <Item class="ModuleScript" referent="29">
8858
+ <Item class="ModuleScript" referent="30">
8987
8859
  <Properties>
8988
8860
  <string name="Name">RuntimeLogBuffer</string>
8989
8861
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9238,7 +9110,334 @@ return {
9238
9110
  ]]></string>
9239
9111
  </Properties>
9240
9112
  </Item>
9241
- <Item class="ModuleScript" referent="30">
9113
+ <Item class="ModuleScript" referent="31">
9114
+ <Properties>
9115
+ <string name="Name">ScriptSearch</string>
9116
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
9117
+ local function splitLuaAlternation(pattern)
9118
+ local parts = {}
9119
+ local current = ""
9120
+ local index = 1
9121
+ local inCharacterClass = false
9122
+ while index <= #pattern do
9123
+ local character = string.sub(pattern, index, index)
9124
+ if character == "%" then
9125
+ if string.sub(pattern, index + 1, index + 1) == "b" then
9126
+ current ..= string.sub(pattern, index, math.min(index + 3, #pattern))
9127
+ index += 4
9128
+ else
9129
+ current ..= string.sub(pattern, index, index + 1)
9130
+ index += 2
9131
+ end
9132
+ elseif character == "[" then
9133
+ inCharacterClass = true
9134
+ current ..= character
9135
+ index += 1
9136
+ elseif character == "]" then
9137
+ inCharacterClass = false
9138
+ current ..= character
9139
+ index += 1
9140
+ elseif character == "|" and not inCharacterClass then
9141
+ local _current = current
9142
+ table.insert(parts, _current)
9143
+ current = ""
9144
+ index += 1
9145
+ else
9146
+ current ..= character
9147
+ index += 1
9148
+ end
9149
+ end
9150
+ local _current = current
9151
+ table.insert(parts, _current)
9152
+ return parts
9153
+ end
9154
+ local function findFirstPattern(line, alternatives, control)
9155
+ local earliest
9156
+ for _, alternative in alternatives do
9157
+ control:checkpoint()
9158
+ if alternative == "" then
9159
+ continue
9160
+ end
9161
+ local start = string.find(line, alternative)
9162
+ if start ~= nil and (earliest == nil or start < earliest) then
9163
+ earliest = start
9164
+ end
9165
+ end
9166
+ return earliest
9167
+ end
9168
+ local function readLine(source, start)
9169
+ local boundary = string.find(source, "[\r\n]", start)
9170
+ if boundary == nil then
9171
+ return { string.sub(source, start), nil }
9172
+ end
9173
+ local nextStart = boundary + 1
9174
+ if string.sub(source, boundary, boundary) == "\r" and string.sub(source, nextStart, nextStart) == "\n" then
9175
+ nextStart += 1
9176
+ end
9177
+ return { string.sub(source, start, boundary - 1), if nextStart <= #source then nextStart else nil }
9178
+ end
9179
+ local MAX_RESULTS = 10_000
9180
+ local LITERAL_CHUNK_BYTES = 64 * 1024
9181
+ local MAX_PATTERN_BYTES = 4096
9182
+ local MAX_CONTEXT_LINES = 100
9183
+ local function sourceCanMatchLiteral(source, pattern, caseSensitive, control)
9184
+ if #source == 0 then
9185
+ return false
9186
+ end
9187
+ local overlapBytes = math.max(#pattern - 1, 0)
9188
+ local chunkStart = 1
9189
+ while chunkStart <= #source do
9190
+ control:checkpoint()
9191
+ local chunkEnd = math.min(#source, chunkStart + LITERAL_CHUNK_BYTES + overlapBytes - 1)
9192
+ local chunk = string.sub(source, chunkStart, chunkEnd)
9193
+ local candidate = if caseSensitive then chunk else string.lower(chunk)
9194
+ if (string.find(candidate, pattern, 1, true)) ~= nil then
9195
+ return true
9196
+ end
9197
+ if chunkEnd >= #source then
9198
+ return false
9199
+ end
9200
+ chunkStart += LITERAL_CHUNK_BYTES
9201
+ end
9202
+ return false
9203
+ end
9204
+ local function createScriptSearch(corpus)
9205
+ local function search(requestData, control)
9206
+ local requestedPattern = requestData.pattern
9207
+ if not (type(requestedPattern) == "string") or requestedPattern == "" then
9208
+ return {
9209
+ error = "pattern is required",
9210
+ }
9211
+ end
9212
+ if #requestedPattern > MAX_PATTERN_BYTES then
9213
+ return {
9214
+ error = "invalid_request",
9215
+ message = `pattern must contain between 1 and {MAX_PATTERN_BYTES} bytes`,
9216
+ }
9217
+ end
9218
+ local pattern = requestedPattern
9219
+ for _, optionName in { "usePattern", "caseSensitive", "filesOnly" } do
9220
+ local option = requestData[optionName]
9221
+ if option ~= nil and not (type(option) == "boolean") then
9222
+ return {
9223
+ error = "invalid_request",
9224
+ message = `{optionName} must be a boolean`,
9225
+ }
9226
+ end
9227
+ end
9228
+ local _condition = (requestData.usePattern)
9229
+ if _condition == nil then
9230
+ _condition = false
9231
+ end
9232
+ local usePattern = _condition
9233
+ if usePattern and requestData.caseSensitive == false then
9234
+ return {
9235
+ error = "Case-insensitive Lua pattern search is not supported.",
9236
+ }
9237
+ end
9238
+ local _result
9239
+ if usePattern then
9240
+ _result = true
9241
+ else
9242
+ local _condition_1 = (requestData.caseSensitive)
9243
+ if _condition_1 == nil then
9244
+ _condition_1 = false
9245
+ end
9246
+ _result = _condition_1
9247
+ end
9248
+ local caseSensitive = _result
9249
+ local _condition_1 = (requestData.filesOnly)
9250
+ if _condition_1 == nil then
9251
+ _condition_1 = false
9252
+ end
9253
+ local filesOnly = _condition_1
9254
+ local requestedContextLines = requestData.contextLines
9255
+ if requestedContextLines ~= nil and (not (type(requestedContextLines) == "number") or math.floor(requestedContextLines) ~= requestedContextLines or requestedContextLines < 0 or requestedContextLines > MAX_CONTEXT_LINES) then
9256
+ return {
9257
+ error = "invalid_request",
9258
+ message = `contextLines must be an integer between 0 and {MAX_CONTEXT_LINES}`,
9259
+ }
9260
+ end
9261
+ local _condition_2 = requestedContextLines
9262
+ if _condition_2 == nil then
9263
+ _condition_2 = 0
9264
+ end
9265
+ local contextLines = _condition_2
9266
+ local requestedMaxResults = requestData.maxResults
9267
+ if requestedMaxResults ~= nil and (not (type(requestedMaxResults) == "number") or math.floor(requestedMaxResults) ~= requestedMaxResults or requestedMaxResults < 1 or requestedMaxResults > MAX_RESULTS) then
9268
+ return {
9269
+ error = "invalid_request",
9270
+ message = `maxResults must be an integer between 1 and {MAX_RESULTS}`,
9271
+ }
9272
+ end
9273
+ local _condition_3 = requestedMaxResults
9274
+ if _condition_3 == nil then
9275
+ _condition_3 = 100
9276
+ end
9277
+ local maxResults = _condition_3
9278
+ local requestedMaxResultsPerScript = requestData.maxResultsPerScript
9279
+ if requestedMaxResultsPerScript ~= nil and (not (type(requestedMaxResultsPerScript) == "number") or math.floor(requestedMaxResultsPerScript) ~= requestedMaxResultsPerScript or requestedMaxResultsPerScript < 0 or requestedMaxResultsPerScript > MAX_RESULTS) then
9280
+ return {
9281
+ error = "invalid_request",
9282
+ message = `maxResultsPerScript must be an integer between 0 and {MAX_RESULTS}`,
9283
+ }
9284
+ end
9285
+ local _condition_4 = requestedMaxResultsPerScript
9286
+ if _condition_4 == nil then
9287
+ _condition_4 = 0
9288
+ end
9289
+ local maxResultsPerScript = _condition_4
9290
+ local classFilter = requestData.classFilter
9291
+ local _condition_5 = (requestData.path)
9292
+ if _condition_5 == nil then
9293
+ _condition_5 = ""
9294
+ end
9295
+ local searchPath = _condition_5
9296
+ local root = corpus:resolveRoot(searchPath)
9297
+ if root == nil then
9298
+ return {
9299
+ error = `Path not found: {searchPath}`,
9300
+ }
9301
+ end
9302
+ local searchPattern = if caseSensitive then pattern else string.lower(pattern)
9303
+ local patternAlternatives = if usePattern then splitLuaAlternation(searchPattern) else nil
9304
+ local results = {}
9305
+ local stack = { root }
9306
+ local totalMatches = 0
9307
+ local scriptsSearched = 0
9308
+ local hitLimit = false
9309
+ while #stack > 0 and not hitLimit do
9310
+ control:checkpoint()
9311
+ -- ▼ Array.pop ▼
9312
+ local _length = #stack
9313
+ local _result_1 = stack[_length]
9314
+ stack[_length] = nil
9315
+ -- ▲ Array.pop ▲
9316
+ local instance = _result_1
9317
+ local snapshot = corpus:readScript(instance, classFilter)
9318
+ if snapshot ~= nil and (classFilter == nil or snapshot.className == classFilter) then
9319
+ scriptsSearched += 1
9320
+ local scriptMatches = {}
9321
+ local before = {}
9322
+ local pendingAfter = {}
9323
+ local scriptMatchCount = 0
9324
+ local lineNumber = 1
9325
+ local lineStart = 1
9326
+ local canMatch = usePattern or sourceCanMatchLiteral(snapshot.source, searchPattern, caseSensitive, control)
9327
+ if canMatch then
9328
+ while lineStart ~= nil and not hitLimit do
9329
+ control:checkpoint()
9330
+ local _binding = readLine(snapshot.source, lineStart)
9331
+ local line = _binding[1]
9332
+ local nextLineStart = _binding[2]
9333
+ for _, pending in pendingAfter do
9334
+ if pending.remaining > 0 then
9335
+ local _exp = pending.match.after
9336
+ table.insert(_exp, line)
9337
+ pending.remaining -= 1
9338
+ end
9339
+ end
9340
+ while #pendingAfter > 0 and pendingAfter[1].remaining == 0 do
9341
+ table.remove(pendingAfter, 1)
9342
+ end
9343
+ local candidate = if caseSensitive then line else string.lower(line)
9344
+ local matchStart = if usePattern then findFirstPattern(candidate, patternAlternatives, control) else (string.find(candidate, searchPattern, 1, true))
9345
+ if matchStart ~= nil and (maxResultsPerScript == 0 or scriptMatchCount < maxResultsPerScript) then
9346
+ scriptMatchCount += 1
9347
+ totalMatches += 1
9348
+ if totalMatches > maxResults then
9349
+ hitLimit = true
9350
+ break
9351
+ end
9352
+ if not filesOnly then
9353
+ local _object = {
9354
+ line = lineNumber,
9355
+ column = matchStart,
9356
+ text = line,
9357
+ }
9358
+ local _left = "before"
9359
+ local _array = {}
9360
+ local _length_1 = #_array
9361
+ table.move(before, 1, #before, _length_1 + 1, _array)
9362
+ _object[_left] = _array
9363
+ _object.after = {}
9364
+ local lineMatch = _object
9365
+ table.insert(scriptMatches, lineMatch)
9366
+ if contextLines > 0 then
9367
+ local _arg0 = {
9368
+ match = lineMatch,
9369
+ remaining = contextLines,
9370
+ }
9371
+ table.insert(pendingAfter, _arg0)
9372
+ end
9373
+ end
9374
+ end
9375
+ if maxResultsPerScript > 0 and scriptMatchCount >= maxResultsPerScript and #pendingAfter == 0 then
9376
+ break
9377
+ end
9378
+ if contextLines > 0 then
9379
+ table.insert(before, line)
9380
+ while #before > contextLines do
9381
+ table.remove(before, 1)
9382
+ end
9383
+ end
9384
+ lineStart = nextLineStart
9385
+ if nextLineStart == nil then
9386
+ break
9387
+ end
9388
+ lineNumber += 1
9389
+ end
9390
+ end
9391
+ if scriptMatchCount > 0 then
9392
+ local scriptResult = {
9393
+ instancePath = snapshot.instancePath,
9394
+ name = snapshot.name,
9395
+ className = snapshot.className,
9396
+ matches = scriptMatches,
9397
+ }
9398
+ if snapshot.enabled ~= nil then
9399
+ scriptResult.enabled = snapshot.enabled
9400
+ end
9401
+ table.insert(results, scriptResult)
9402
+ end
9403
+ end
9404
+ local children = {}
9405
+ for _, child in corpus:getChildren(instance) do
9406
+ table.insert(children, child)
9407
+ end
9408
+ for index = #children - 1, 0, -1 do
9409
+ local _arg0 = children[index + 1]
9410
+ table.insert(stack, _arg0)
9411
+ end
9412
+ end
9413
+ return {
9414
+ results = results,
9415
+ pattern = pattern,
9416
+ totalMatches = if hitLimit then `>{maxResults}` else totalMatches,
9417
+ scriptsSearched = scriptsSearched,
9418
+ scriptsMatched = #results,
9419
+ truncated = hitLimit,
9420
+ options = {
9421
+ caseSensitive = caseSensitive,
9422
+ contextLines = contextLines,
9423
+ usePattern = usePattern,
9424
+ filesOnly = filesOnly,
9425
+ maxResults = maxResults,
9426
+ maxResultsPerScript = maxResultsPerScript,
9427
+ },
9428
+ }
9429
+ end
9430
+ return {
9431
+ search = search,
9432
+ }
9433
+ end
9434
+ return {
9435
+ createScriptSearch = createScriptSearch,
9436
+ }
9437
+ ]]></string>
9438
+ </Properties>
9439
+ </Item>
9440
+ <Item class="ModuleScript" referent="32">
9242
9441
  <Properties>
9243
9442
  <string name="Name">ServerUrlSettings</string>
9244
9443
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9377,11 +9576,11 @@ return {
9377
9576
  ]]></string>
9378
9577
  </Properties>
9379
9578
  </Item>
9380
- <Item class="ModuleScript" referent="31">
9579
+ <Item class="ModuleScript" referent="33">
9381
9580
  <Properties>
9382
9581
  <string name="Name">State</string>
9383
9582
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
9384
- local CURRENT_VERSION = "3.0.3"
9583
+ local CURRENT_VERSION = "3.0.5"
9385
9584
  local PLUGIN_VARIANT = "main"
9386
9585
  local BASE_PORT = 58741
9387
9586
  local function createConnection(port)
@@ -9411,7 +9610,7 @@ return {
9411
9610
  ]]></string>
9412
9611
  </Properties>
9413
9612
  </Item>
9414
- <Item class="ModuleScript" referent="32">
9613
+ <Item class="ModuleScript" referent="34">
9415
9614
  <Properties>
9416
9615
  <string name="Name">StopPlayMonitor</string>
9417
9616
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9461,6 +9660,7 @@ local WAIT_POLL_SEC = 0.1
9461
9660
  local REQUEST_TTL_SEC = 12.0
9462
9661
  local pluginRef
9463
9662
  local endTestIssued = false
9663
+ local transportLifecycle
9464
9664
  local function init(p)
9465
9665
  pluginRef = p
9466
9666
  end
@@ -9604,15 +9804,29 @@ local function handleStopRequest(key, request)
9604
9804
  return nil
9605
9805
  end
9606
9806
  endTestIssued = true
9807
+ local lifecycle = transportLifecycle
9808
+ if lifecycle ~= nil then
9809
+ local closeOk, closeError = pcall(lifecycle.beforeEndTest)
9810
+ if not closeOk then
9811
+ warn(`[robloxstudio-mcp] Failed to close the play-server transport before EndTest: {tostring(closeError)}`)
9812
+ end
9813
+ end
9607
9814
  local endOk, endErr = pcall(function()
9608
9815
  return StudioTestService:EndTest("stopped_by_mcp")
9609
9816
  end)
9610
9817
  writeResult(key, request, endOk, if endOk then nil else tostring(endErr))
9611
9818
  if not endOk then
9612
9819
  endTestIssued = false
9820
+ if lifecycle ~= nil then
9821
+ local restoreOk, restoreError = pcall(lifecycle.afterEndTestFailure)
9822
+ if not restoreOk then
9823
+ warn(`[robloxstudio-mcp] Failed to restore the play-server transport after EndTest failed: {tostring(restoreError)}`)
9824
+ end
9825
+ end
9613
9826
  end
9614
9827
  end
9615
- local function startMonitor()
9828
+ local function startMonitor(lifecycle)
9829
+ transportLifecycle = lifecycle
9616
9830
  if not pluginRef then
9617
9831
  warn("[robloxstudio-mcp] StopPlayMonitor.startMonitor called before init; skipping")
9618
9832
  return nil
@@ -9702,7 +9916,7 @@ return {
9702
9916
  ]]></string>
9703
9917
  </Properties>
9704
9918
  </Item>
9705
- <Item class="ModuleScript" referent="33">
9919
+ <Item class="ModuleScript" referent="35">
9706
9920
  <Properties>
9707
9921
  <string name="Name">StudioEventStream</string>
9708
9922
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -9718,12 +9932,13 @@ local MAX_TERMINAL_RESPONSES = 256
9718
9932
  local STREAM_SILENCE_TIMEOUT_SECONDS = 20
9719
9933
  local options
9720
9934
  local active = false
9935
+ local shutdownSuspended = false
9721
9936
  local generation = 0
9722
9937
  local reconnectAttempt = 0
9723
9938
  local streamClient
9724
9939
  local streamConnections = {}
9725
9940
  local lastValidEventAt = 0
9726
- local inFlightRequestIds = {}
9941
+ local inFlightRequests = {}
9727
9942
  local pendingResponses = {}
9728
9943
  local terminalResponseIds = {}
9729
9944
  local terminalResponseOrder = {}
@@ -9780,6 +9995,21 @@ local function decodeMessage(message)
9780
9995
  _object[_left_2] = if type(_pluginVariant) == "string" then envelope.pluginVariant else nil
9781
9996
  return _object
9782
9997
  end
9998
+ if envelope.kind == "cancel" then
9999
+ local _requestId = envelope.requestId
10000
+ local _condition = not (type(_requestId) == "string")
10001
+ if not _condition then
10002
+ _condition = (envelope.reason ~= "timeout" and envelope.reason ~= "aborted" and envelope.reason ~= "connection_closed")
10003
+ end
10004
+ if _condition then
10005
+ return nil
10006
+ end
10007
+ return {
10008
+ kind = "cancel",
10009
+ requestId = envelope.requestId,
10010
+ reason = envelope.reason,
10011
+ }
10012
+ end
9783
10013
  if envelope.kind == "request" then
9784
10014
  local _requestId = envelope.requestId
9785
10015
  local _condition = not (type(_requestId) == "string")
@@ -9792,6 +10022,13 @@ local function decodeMessage(message)
9792
10022
  if not _condition then
9793
10023
  local _endpoint = envelope.endpoint
9794
10024
  _condition = not (type(_endpoint) == "string")
10025
+ if not _condition then
10026
+ local _remainingMs = envelope.remainingMs
10027
+ _condition = not (type(_remainingMs) == "number")
10028
+ if not _condition then
10029
+ _condition = envelope.remainingMs < 0
10030
+ end
10031
+ end
9795
10032
  end
9796
10033
  end
9797
10034
  end
@@ -9810,6 +10047,7 @@ local function decodeMessage(message)
9810
10047
  target = envelope.target,
9811
10048
  endpoint = envelope.endpoint,
9812
10049
  data = data,
10050
+ remainingMs = envelope.remainingMs,
9813
10051
  }
9814
10052
  end
9815
10053
  return nil
@@ -9827,6 +10065,21 @@ local function closeCurrentStream()
9827
10065
  end)
9828
10066
  end
9829
10067
  end
10068
+ local function disconnectSession(currentOptions)
10069
+ pcall(function()
10070
+ return HttpService:RequestAsync({
10071
+ Url = `{currentOptions.serverUrl}/disconnect`,
10072
+ Method = "POST",
10073
+ Headers = {
10074
+ ["Content-Type"] = "application/json",
10075
+ },
10076
+ Body = HttpService:JSONEncode({
10077
+ pluginSessionId = PluginSession.id,
10078
+ timestamp = tick(),
10079
+ }),
10080
+ })
10081
+ end)
10082
+ end
9830
10083
  local function responseRetryDelay(attempt)
9831
10084
  return math.min(INITIAL_RESPONSE_RETRY_DELAY_SECONDS * math.pow(2, math.max(attempt - 1, 0)), MAX_RESPONSE_RETRY_DELAY_SECONDS)
9832
10085
  end
@@ -9977,6 +10230,21 @@ local function encodeResponse(requestId, response)
9977
10230
  error = `Plugin response serialization failed: {tostring(encoded)}`,
9978
10231
  })
9979
10232
  end
10233
+ local function cancelRequest(event)
10234
+ local _requestId = event.requestId
10235
+ local inFlight = inFlightRequests[_requestId]
10236
+ if inFlight ~= nil then
10237
+ inFlight.cancelled = true
10238
+ end
10239
+ local _requestId_1 = event.requestId
10240
+ local pending = pendingResponses[_requestId_1]
10241
+ if pending ~= nil then
10242
+ pending.retryToken += 1
10243
+ local _requestId_2 = event.requestId
10244
+ pendingResponses[_requestId_2] = nil
10245
+ end
10246
+ rememberTerminalResponse(event.requestId)
10247
+ end
9980
10248
  local function dispatchRequest(request)
9981
10249
  local _requestId = request.requestId
9982
10250
  local _condition = terminalResponseIds[_requestId] ~= nil
@@ -9985,7 +10253,7 @@ local function dispatchRequest(request)
9985
10253
  _condition = pendingResponses[_requestId_1] ~= nil
9986
10254
  if not _condition then
9987
10255
  local _requestId_2 = request.requestId
9988
- _condition = inFlightRequestIds[_requestId_2] ~= nil
10256
+ _condition = inFlightRequests[_requestId_2] ~= nil
9989
10257
  end
9990
10258
  end
9991
10259
  if _condition then
@@ -9995,12 +10263,48 @@ local function dispatchRequest(request)
9995
10263
  if not active or dispatchOptions == nil then
9996
10264
  return nil
9997
10265
  end
10266
+ local inFlight = {
10267
+ cancelled = false,
10268
+ }
10269
+ local context = {
10270
+ requestId = request.requestId,
10271
+ deadlineAt = os.clock() + request.remainingMs / 1000,
10272
+ isCancelled = function()
10273
+ return inFlight.cancelled
10274
+ end,
10275
+ }
9998
10276
  local _requestId_1 = request.requestId
9999
- inFlightRequestIds[_requestId_1] = true
10277
+ inFlightRequests[_requestId_1] = inFlight
10000
10278
  task.spawn(function()
10279
+ local _condition_1 = inFlight.cancelled
10280
+ if not _condition_1 then
10281
+ local _requestId_2 = request.requestId
10282
+ _condition_1 = terminalResponseIds[_requestId_2] ~= nil
10283
+ end
10284
+ if _condition_1 then
10285
+ local _requestId_2 = request.requestId
10286
+ if inFlightRequests[_requestId_2] == inFlight then
10287
+ local _requestId_3 = request.requestId
10288
+ inFlightRequests[_requestId_3] = nil
10289
+ end
10290
+ return nil
10291
+ end
10001
10292
  local dispatchOk, response = pcall(function()
10002
- return dispatchOptions.dispatchRequest(request)
10293
+ return dispatchOptions.dispatchRequest(request, context)
10003
10294
  end)
10295
+ local _condition_2 = inFlight.cancelled
10296
+ if not _condition_2 then
10297
+ local _requestId_2 = request.requestId
10298
+ _condition_2 = terminalResponseIds[_requestId_2] ~= nil
10299
+ end
10300
+ if _condition_2 then
10301
+ local _requestId_2 = request.requestId
10302
+ if inFlightRequests[_requestId_2] == inFlight then
10303
+ local _requestId_3 = request.requestId
10304
+ inFlightRequests[_requestId_3] = nil
10305
+ end
10306
+ return nil
10307
+ end
10004
10308
  local responseData = if dispatchOk then response else {
10005
10309
  error = tostring(response),
10006
10310
  }
@@ -10013,7 +10317,7 @@ local function dispatchRequest(request)
10013
10317
  local _requestId_2 = request.requestId
10014
10318
  pendingResponses[_requestId_2] = entry
10015
10319
  local _requestId_3 = request.requestId
10016
- inFlightRequestIds[_requestId_3] = nil
10320
+ inFlightRequests[_requestId_3] = nil
10017
10321
  postPendingResponse(request.requestId, entry)
10018
10322
  end)
10019
10323
  end
@@ -10232,6 +10536,10 @@ function connect(expectedGeneration)
10232
10536
  end)
10233
10537
  return nil
10234
10538
  end
10539
+ if event.kind == "cancel" then
10540
+ cancelRequest(event)
10541
+ return nil
10542
+ end
10235
10543
  if event.kind == "request" then
10236
10544
  dispatchRequest(event)
10237
10545
  return nil
@@ -10260,11 +10568,12 @@ function connect(expectedGeneration)
10260
10568
  end
10261
10569
  local stop
10262
10570
  local function start(newOptions)
10263
- if active then
10571
+ if active or shutdownSuspended then
10264
10572
  stop()
10265
10573
  end
10266
10574
  options = newOptions
10267
10575
  active = true
10576
+ shutdownSuspended = false
10268
10577
  reconnectAttempt = 0
10269
10578
  generation += 1
10270
10579
  connect(generation)
@@ -10278,42 +10587,64 @@ function refresh()
10278
10587
  reconnectAttempt = 0
10279
10588
  connect(generation)
10280
10589
  end
10590
+ -- EndTest can tear down a play DataModel without giving Plugin.Unloading a
10591
+ -- usable execution window. Release the native WebStreamClient before the
10592
+ -- yielding unregister request, but retain local request state and connection
10593
+ -- options so a failed EndTest can register again and reconnect.
10594
+ local function suspendForShutdown()
10595
+ local currentOptions = options
10596
+ if not active or currentOptions == nil then
10597
+ return nil
10598
+ end
10599
+ active = false
10600
+ shutdownSuspended = true
10601
+ generation += 1
10602
+ reconnectAttempt = 0
10603
+ closeCurrentStream()
10604
+ disconnectSession(currentOptions)
10605
+ end
10606
+ local function resumeAfterShutdownFailure()
10607
+ if not shutdownSuspended or options == nil then
10608
+ return nil
10609
+ end
10610
+ shutdownSuspended = false
10611
+ active = true
10612
+ generation += 1
10613
+ reconnectAttempt = 0
10614
+ connect(generation)
10615
+ end
10281
10616
  function stop()
10282
- if not active then
10617
+ if not active and not shutdownSuspended then
10283
10618
  return nil
10284
10619
  end
10285
10620
  local currentOptions = options
10286
10621
  active = false
10622
+ shutdownSuspended = false
10287
10623
  generation += 1
10624
+ for _, inFlight in inFlightRequests do
10625
+ inFlight.cancelled = true
10626
+ end
10627
+ table.clear(inFlightRequests)
10628
+ table.clear(pendingResponses)
10288
10629
  closeCurrentStream()
10289
10630
  table.clear(readyFailureLogKeys)
10290
10631
  options = nil
10291
10632
  reconnectAttempt = 0
10292
10633
  if currentOptions ~= nil then
10293
- pcall(function()
10294
- return HttpService:RequestAsync({
10295
- Url = `{currentOptions.serverUrl}/disconnect`,
10296
- Method = "POST",
10297
- Headers = {
10298
- ["Content-Type"] = "application/json",
10299
- },
10300
- Body = HttpService:JSONEncode({
10301
- pluginSessionId = PluginSession.id,
10302
- timestamp = tick(),
10303
- }),
10304
- })
10305
- end)
10634
+ disconnectSession(currentOptions)
10306
10635
  end
10307
10636
  end
10308
10637
  return {
10309
10638
  start = start,
10310
10639
  refresh = refresh,
10640
+ suspendForShutdown = suspendForShutdown,
10641
+ resumeAfterShutdownFailure = resumeAfterShutdownFailure,
10311
10642
  stop = stop,
10312
10643
  }
10313
10644
  ]]></string>
10314
10645
  </Properties>
10315
10646
  </Item>
10316
- <Item class="ModuleScript" referent="34">
10647
+ <Item class="ModuleScript" referent="36">
10317
10648
  <Properties>
10318
10649
  <string name="Name">UI</string>
10319
10650
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -10865,7 +11196,7 @@ return {
10865
11196
  ]]></string>
10866
11197
  </Properties>
10867
11198
  </Item>
10868
- <Item class="ModuleScript" referent="35">
11199
+ <Item class="ModuleScript" referent="37">
10869
11200
  <Properties>
10870
11201
  <string name="Name">Utils</string>
10871
11202
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -11149,21 +11480,11 @@ local function splitLines(source)
11149
11480
  end
11150
11481
  local normalized = (string.gsub((string.gsub(_condition, "\r\n", "\n")), "\r", "\n"))
11151
11482
  local endsWithNewline = string.sub(normalized, -1) == "\n"
11152
- local lines = {}
11153
- local start = 1
11154
- while true do
11155
- local newlinePos = string.find(normalized, "\n", start, true)
11156
- if newlinePos ~= nil then
11157
- local _arg0 = string.sub(normalized, start, newlinePos - 1)
11158
- table.insert(lines, _arg0)
11159
- start = newlinePos + 1
11160
- else
11161
- local remainder = string.sub(normalized, start)
11162
- if remainder ~= "" or not endsWithNewline then
11163
- table.insert(lines, remainder)
11164
- end
11165
- break
11166
- end
11483
+ local lines = string.split(normalized, "\n")
11484
+ -- split() includes a final empty field for a trailing newline. Keep the
11485
+ -- existing splitLines contract, which tracks that newline separately.
11486
+ if endsWithNewline and lines[#lines] == "" then
11487
+ lines[#lines] = nil
11167
11488
  end
11168
11489
  if #lines == 0 then
11169
11490
  table.insert(lines, "")
@@ -11178,15 +11499,21 @@ local function joinLines(lines, hadTrailingNewline)
11178
11499
  return source
11179
11500
  end
11180
11501
  local function readScriptSource(instance)
11181
- local ok, result = pcall(function()
11502
+ local editorOk, editorSource = pcall(function()
11503
+ return ScriptEditorService:GetEditorSource(instance)
11504
+ end)
11505
+ if editorOk then
11506
+ return editorSource
11507
+ end
11508
+ local documentOk, documentSource = pcall(function()
11182
11509
  local doc = ScriptEditorService:FindScriptDocument(instance)
11183
11510
  if doc then
11184
11511
  return doc:GetText()
11185
11512
  end
11186
11513
  return nil
11187
11514
  end)
11188
- if ok and result ~= nil then
11189
- return result
11515
+ if documentOk and documentSource ~= nil then
11516
+ return documentSource
11190
11517
  end
11191
11518
  -- @rbxts/types does not expose PluginSecurity Source reads.
11192
11519
  local readableScript = instance
@@ -11710,11 +12037,11 @@ return {
11710
12037
  </Properties>
11711
12038
  </Item>
11712
12039
  </Item>
11713
- <Item class="Folder" referent="40">
12040
+ <Item class="Folder" referent="42">
11714
12041
  <Properties>
11715
12042
  <string name="Name">include</string>
11716
12043
  </Properties>
11717
- <Item class="ModuleScript" referent="36">
12044
+ <Item class="ModuleScript" referent="38">
11718
12045
  <Properties>
11719
12046
  <string name="Name">LibMP</string>
11720
12047
  <string name="Source"><![CDATA[-- =============================================================================
@@ -168098,7 +168425,7 @@ return LibMP
168098
168425
  ]]></string>
168099
168426
  </Properties>
168100
168427
  </Item>
168101
- <Item class="ModuleScript" referent="37">
168428
+ <Item class="ModuleScript" referent="39">
168102
168429
  <Properties>
168103
168430
  <string name="Name">Promise</string>
168104
168431
  <string name="Source"><![CDATA[--[[
@@ -170172,7 +170499,7 @@ return Promise
170172
170499
  ]]></string>
170173
170500
  </Properties>
170174
170501
  </Item>
170175
- <Item class="ModuleScript" referent="38">
170502
+ <Item class="ModuleScript" referent="40">
170176
170503
  <Properties>
170177
170504
  <string name="Name">RuntimeLib</string>
170178
170505
  <string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
@@ -170439,15 +170766,15 @@ return TS
170439
170766
  </Properties>
170440
170767
  </Item>
170441
170768
  </Item>
170442
- <Item class="Folder" referent="41">
170769
+ <Item class="Folder" referent="43">
170443
170770
  <Properties>
170444
170771
  <string name="Name">node_modules</string>
170445
170772
  </Properties>
170446
- <Item class="Folder" referent="42">
170773
+ <Item class="Folder" referent="44">
170447
170774
  <Properties>
170448
170775
  <string name="Name">@rbxts</string>
170449
170776
  </Properties>
170450
- <Item class="ModuleScript" referent="39">
170777
+ <Item class="ModuleScript" referent="41">
170451
170778
  <Properties>
170452
170779
  <string name="Name">services</string>
170453
170780
  <string name="Source"><![CDATA[return setmetatable({}, {