@princeofscale/bloxforge-inspector 2.20.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1489 -375
- package/package.json +5 -2
- package/studio-plugin/MCPInspectorPlugin.rbxmx +460 -101
- package/studio-plugin/MCPPlugin.rbxmx +460 -101
- package/studio-plugin/package-lock.json +9 -9
- package/studio-plugin/src/modules/ClientBroker.ts +67 -14
- package/studio-plugin/src/modules/Communication.ts +177 -28
- package/studio-plugin/src/modules/LuauExec.ts +6 -3
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +31 -2
- package/studio-plugin/src/modules/State.ts +1 -1
- package/studio-plugin/src/modules/handlers/JobHandlers.ts +2 -1
- package/studio-plugin/src/types/index.d.ts +7 -0
|
@@ -240,11 +240,41 @@ local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
|
240
240
|
-- Throttle re-ready calls per proxyId so a brief window of unknownInstance
|
|
241
241
|
-- polls doesn't cause a re-register stampede.
|
|
242
242
|
local lastReadyByProxy = {}
|
|
243
|
-
local
|
|
244
|
-
local
|
|
243
|
+
local completedProxyRequests = {}
|
|
244
|
+
local completedProxyRequestOrder = {}
|
|
245
|
+
local COMPLETED_PROXY_REQUEST_LIMIT = 500
|
|
246
|
+
local function rememberCompletedProxyRequest(requestId, response)
|
|
247
|
+
local _requestId = requestId
|
|
248
|
+
local _arg1 = {
|
|
249
|
+
response = response,
|
|
250
|
+
}
|
|
251
|
+
completedProxyRequests[_requestId] = _arg1
|
|
252
|
+
local _requestId_1 = requestId
|
|
253
|
+
table.insert(completedProxyRequestOrder, _requestId_1)
|
|
254
|
+
if #completedProxyRequestOrder > COMPLETED_PROXY_REQUEST_LIMIT then
|
|
255
|
+
local _arg0 = table.remove(completedProxyRequestOrder, 1)
|
|
256
|
+
completedProxyRequests[_arg0] = nil
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
local postJson, formatPostJsonFailure
|
|
260
|
+
local function sendProxyResponse(requestId, response, proxy, fence)
|
|
261
|
+
local _object = {
|
|
262
|
+
requestId = requestId,
|
|
263
|
+
pluginSessionId = proxy.pluginSessionId,
|
|
264
|
+
response = response,
|
|
265
|
+
}
|
|
266
|
+
for _k, _v in fence do
|
|
267
|
+
_object[_k] = _v
|
|
268
|
+
end
|
|
269
|
+
local ok, result = postJson("/response", _object, proxy.sessionToken)
|
|
270
|
+
if not ok or not result or not result.Success then
|
|
271
|
+
warn(`[BloxForge] proxy response delivery failed: {formatPostJsonFailure("/response", ok, result)}`)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
local function reRegisterProxy(proxy, role)
|
|
275
|
+
local proxyId = proxy.pluginSessionId
|
|
245
276
|
local now = tick()
|
|
246
|
-
local
|
|
247
|
-
local _condition = lastReadyByProxy[_proxyId]
|
|
277
|
+
local _condition = lastReadyByProxy[proxyId]
|
|
248
278
|
if _condition == nil then
|
|
249
279
|
_condition = 0
|
|
250
280
|
end
|
|
@@ -252,10 +282,9 @@ local function reRegisterProxy(proxyId, role)
|
|
|
252
282
|
if now - last < 2 then
|
|
253
283
|
return nil
|
|
254
284
|
end
|
|
255
|
-
|
|
256
|
-
lastReadyByProxy[_proxyId_1] = now
|
|
285
|
+
lastReadyByProxy[proxyId] = now
|
|
257
286
|
pcall(function()
|
|
258
|
-
|
|
287
|
+
local ok, result = postJson("/ready", {
|
|
259
288
|
pluginSessionId = proxyId,
|
|
260
289
|
instanceId = computeInstanceId(),
|
|
261
290
|
role = role,
|
|
@@ -267,6 +296,12 @@ local function reRegisterProxy(proxyId, role)
|
|
|
267
296
|
pluginVariant = State.PLUGIN_VARIANT,
|
|
268
297
|
protocolVersion = State.PROTOCOL_VERSION,
|
|
269
298
|
})
|
|
299
|
+
if ok and result and result.Success then
|
|
300
|
+
local body = HttpService:JSONDecode(result.Body)
|
|
301
|
+
if body.sessionToken ~= nil then
|
|
302
|
+
proxy.sessionToken = body.sessionToken
|
|
303
|
+
end
|
|
304
|
+
end
|
|
270
305
|
end)
|
|
271
306
|
end
|
|
272
307
|
local function forkRole()
|
|
@@ -278,19 +313,23 @@ local function forkRole()
|
|
|
278
313
|
end
|
|
279
314
|
return "client"
|
|
280
315
|
end
|
|
281
|
-
function postJson(endpoint, body)
|
|
316
|
+
function postJson(endpoint, body, sessionToken)
|
|
317
|
+
local headers = {
|
|
318
|
+
["Content-Type"] = "application/json",
|
|
319
|
+
}
|
|
320
|
+
if sessionToken ~= nil and sessionToken ~= "" then
|
|
321
|
+
headers.Authorization = `Bearer {sessionToken}`
|
|
322
|
+
end
|
|
282
323
|
return pcall(function()
|
|
283
324
|
return HttpService:RequestAsync({
|
|
284
325
|
Url = `{mcpUrl}{endpoint}`,
|
|
285
326
|
Method = "POST",
|
|
286
|
-
Headers =
|
|
287
|
-
["Content-Type"] = "application/json",
|
|
288
|
-
},
|
|
327
|
+
Headers = headers,
|
|
289
328
|
Body = HttpService:JSONEncode(body),
|
|
290
329
|
})
|
|
291
330
|
end)
|
|
292
331
|
end
|
|
293
|
-
|
|
332
|
+
function formatPostJsonFailure(endpoint, ok, res)
|
|
294
333
|
return HttpDiagnostics.formatRequestFailure(`{mcpUrl}{endpoint}`, ok, res)
|
|
295
334
|
end
|
|
296
335
|
local function setServerUrl(serverUrl)
|
|
@@ -471,7 +510,7 @@ local function unregisterProxy(player, entry)
|
|
|
471
510
|
proxyRegisterFailuresByPlayer[_player_1] = nil
|
|
472
511
|
postJson("/disconnect", {
|
|
473
512
|
pluginSessionId = proxy.pluginSessionId,
|
|
474
|
-
})
|
|
513
|
+
}, proxy.sessionToken)
|
|
475
514
|
end
|
|
476
515
|
local function disconnectAllProxies()
|
|
477
516
|
for player, entry in proxyByPlayer do
|
|
@@ -480,7 +519,8 @@ local function disconnectAllProxies()
|
|
|
480
519
|
table.clear(proxyByPlayer)
|
|
481
520
|
table.clear(proxyRegisterFailuresByPlayer)
|
|
482
521
|
end
|
|
483
|
-
local function pollProxy(
|
|
522
|
+
local function pollProxy(proxy, player, rf)
|
|
523
|
+
local proxyId = proxy.pluginSessionId
|
|
484
524
|
while true do
|
|
485
525
|
local _condition = player.Parent ~= nil
|
|
486
526
|
if _condition then
|
|
@@ -498,11 +538,17 @@ local function pollProxy(proxyId, player, rf)
|
|
|
498
538
|
return HttpService:RequestAsync({
|
|
499
539
|
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
|
|
500
540
|
Method = "GET",
|
|
501
|
-
Headers = {
|
|
541
|
+
Headers = if proxy.sessionToken ~= "" then {
|
|
542
|
+
["Content-Type"] = "application/json",
|
|
543
|
+
Authorization = `Bearer {proxy.sessionToken}`,
|
|
544
|
+
} else {
|
|
502
545
|
["Content-Type"] = "application/json",
|
|
503
546
|
},
|
|
504
547
|
})
|
|
505
548
|
end)
|
|
549
|
+
if ok and res and res.StatusCode == 401 then
|
|
550
|
+
reRegisterProxy(proxy, "client")
|
|
551
|
+
end
|
|
506
552
|
if ok and res and (res.Success or res.StatusCode == 503) then
|
|
507
553
|
local okJson, body = pcall(function()
|
|
508
554
|
return HttpService:JSONDecode(res.Body)
|
|
@@ -511,9 +557,41 @@ local function pollProxy(proxyId, player, rf)
|
|
|
511
557
|
-- Server lost our proxy registration (process restart, etc.) -
|
|
512
558
|
-- re-register so the next poll cycle starts routing again.
|
|
513
559
|
if body.knownInstance == false then
|
|
514
|
-
reRegisterProxy(
|
|
560
|
+
reRegisterProxy(proxy, "client")
|
|
515
561
|
end
|
|
516
562
|
if body.request and body.requestId ~= nil then
|
|
563
|
+
local _value = body.serverEpoch
|
|
564
|
+
local _condition_1 = not (_value ~= "" and _value)
|
|
565
|
+
if not _condition_1 then
|
|
566
|
+
_condition_1 = body.deliveryAttempt == nil
|
|
567
|
+
if not _condition_1 then
|
|
568
|
+
local _value_1 = body.leaseToken
|
|
569
|
+
_condition_1 = not (_value_1 ~= "" and _value_1)
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
if _condition_1 then
|
|
573
|
+
continue
|
|
574
|
+
end
|
|
575
|
+
local fence = {
|
|
576
|
+
serverEpoch = body.serverEpoch,
|
|
577
|
+
deliveryAttempt = body.deliveryAttempt,
|
|
578
|
+
leaseToken = body.leaseToken,
|
|
579
|
+
}
|
|
580
|
+
local _requestId = body.requestId
|
|
581
|
+
local cached = completedProxyRequests[_requestId]
|
|
582
|
+
if cached ~= nil then
|
|
583
|
+
sendProxyResponse(body.requestId, cached.response, proxy, fence)
|
|
584
|
+
task.wait(0.5)
|
|
585
|
+
continue
|
|
586
|
+
end
|
|
587
|
+
local _object = {
|
|
588
|
+
requestId = body.requestId,
|
|
589
|
+
pluginSessionId = proxyId,
|
|
590
|
+
}
|
|
591
|
+
for _k, _v in fence do
|
|
592
|
+
_object[_k] = _v
|
|
593
|
+
end
|
|
594
|
+
postJson("/ack", _object, proxy.sessionToken)
|
|
517
595
|
local request = body.request
|
|
518
596
|
local response
|
|
519
597
|
local _endpoint = request.endpoint
|
|
@@ -547,10 +625,8 @@ local function pollProxy(proxyId, player, rf)
|
|
|
547
625
|
error = `Client-proxy does not forward {tostring(request.endpoint)}. ` .. `Allowed: {table.concat(allowed, ", ")}.`,
|
|
548
626
|
}
|
|
549
627
|
end
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
response = response,
|
|
553
|
-
})
|
|
628
|
+
rememberCompletedProxyRequest(body.requestId, response)
|
|
629
|
+
sendProxyResponse(body.requestId, response, proxy, fence)
|
|
554
630
|
end
|
|
555
631
|
end
|
|
556
632
|
end
|
|
@@ -587,19 +663,26 @@ local function registerProxy(player, rf)
|
|
|
587
663
|
_condition = "client"
|
|
588
664
|
end
|
|
589
665
|
local assigned = _condition
|
|
590
|
-
local
|
|
591
|
-
local _arg1 = {
|
|
666
|
+
local _object = {
|
|
592
667
|
pluginSessionId = proxyId,
|
|
593
668
|
role = assigned,
|
|
594
669
|
}
|
|
595
|
-
|
|
670
|
+
local _left = "sessionToken"
|
|
671
|
+
local _condition_1 = body.sessionToken
|
|
672
|
+
if _condition_1 == nil then
|
|
673
|
+
_condition_1 = ""
|
|
674
|
+
end
|
|
675
|
+
_object[_left] = _condition_1
|
|
676
|
+
local entry = _object
|
|
677
|
+
local _player_1 = player
|
|
678
|
+
proxyByPlayer[_player_1] = entry
|
|
596
679
|
local _player_2 = player
|
|
597
680
|
if proxyRegisterFailuresByPlayer[_player_2] ~= nil then
|
|
598
681
|
local _player_3 = player
|
|
599
682
|
proxyRegisterFailuresByPlayer[_player_3] = nil
|
|
600
683
|
print(`[BloxForge] proxy registered for {player.Name} as {assigned} via {mcpUrl}`)
|
|
601
684
|
end
|
|
602
|
-
task.spawn(pollProxy,
|
|
685
|
+
task.spawn(pollProxy, entry, player, rf)
|
|
603
686
|
end
|
|
604
687
|
-- (Removed: startEditProxyLoop. The play-server DM no longer registers an
|
|
605
688
|
-- "edit-proxy" peer with the MCP server. stop_playtest now uses a cross-DM
|
|
@@ -708,8 +791,10 @@ local function computeInstanceId()
|
|
|
708
791
|
end
|
|
709
792
|
local assignedRole
|
|
710
793
|
local duplicateInstanceRole = false
|
|
711
|
-
local
|
|
794
|
+
local versionMismatchConnections = {}
|
|
712
795
|
local lastVersionMismatchWarningKey
|
|
796
|
+
local protocolMismatchConnections = {}
|
|
797
|
+
local lastProtocolMismatchWarningKey
|
|
713
798
|
local lastReadyInstanceId
|
|
714
799
|
local readyFailureLogKeys = {}
|
|
715
800
|
-- Cache the published place name from MarketplaceService:GetProductInfo so
|
|
@@ -753,6 +838,20 @@ local function detectRole()
|
|
|
753
838
|
return "client"
|
|
754
839
|
end
|
|
755
840
|
local initialRole = detectRole()
|
|
841
|
+
local activeRequests = {}
|
|
842
|
+
local completedRequests = {}
|
|
843
|
+
local completedRequestOrder = {}
|
|
844
|
+
local COMPLETED_REQUEST_LIMIT = 500
|
|
845
|
+
local syncThreadRequests = {}
|
|
846
|
+
local cancelledSyncRequests = {}
|
|
847
|
+
local function isCancelledForThread(co)
|
|
848
|
+
local _co = co
|
|
849
|
+
local reqId = syncThreadRequests[_co]
|
|
850
|
+
if reqId == nil then
|
|
851
|
+
return false
|
|
852
|
+
end
|
|
853
|
+
return cancelledSyncRequests[reqId] ~= nil
|
|
854
|
+
end
|
|
756
855
|
local routeMap = {
|
|
757
856
|
["/api/file-tree"] = QueryHandlers.getFileTree,
|
|
758
857
|
["/api/search-files"] = QueryHandlers.searchFiles,
|
|
@@ -841,19 +940,119 @@ local function processRequest(request)
|
|
|
841
940
|
}
|
|
842
941
|
end
|
|
843
942
|
end
|
|
844
|
-
local function sendResponse(conn, requestId, responseData)
|
|
845
|
-
|
|
846
|
-
|
|
943
|
+
local function sendResponse(conn, requestId, responseData, serverEpoch, deliveryAttempt, leaseToken)
|
|
944
|
+
local body = {
|
|
945
|
+
requestId = requestId,
|
|
946
|
+
response = responseData,
|
|
947
|
+
}
|
|
948
|
+
if serverEpoch ~= nil then
|
|
949
|
+
body.serverEpoch = serverEpoch
|
|
950
|
+
end
|
|
951
|
+
if deliveryAttempt ~= nil then
|
|
952
|
+
body.deliveryAttempt = deliveryAttempt
|
|
953
|
+
end
|
|
954
|
+
if leaseToken ~= nil then
|
|
955
|
+
body.leaseToken = leaseToken
|
|
956
|
+
end
|
|
957
|
+
local ok, result = pcall(function()
|
|
958
|
+
local _object = {
|
|
847
959
|
Url = `{conn.serverUrl}/response`,
|
|
848
960
|
Method = "POST",
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
961
|
+
}
|
|
962
|
+
local _left = "Headers"
|
|
963
|
+
local _object_1 = {
|
|
964
|
+
["Content-Type"] = "application/json",
|
|
965
|
+
}
|
|
966
|
+
local _value = conn.sessionToken
|
|
967
|
+
for _k, _v in (if _value ~= "" and _value then {
|
|
968
|
+
Authorization = `Bearer {conn.sessionToken}`,
|
|
969
|
+
} else {}) do
|
|
970
|
+
_object_1[_k] = _v
|
|
971
|
+
end
|
|
972
|
+
_object[_left] = _object_1
|
|
973
|
+
_object.Body = HttpService:JSONEncode(body)
|
|
974
|
+
return HttpService:RequestAsync(_object)
|
|
975
|
+
end)
|
|
976
|
+
if not ok then
|
|
977
|
+
warn(`[BloxForge] Plugin response serialization failed for {requestId}: {tostring(result)}`)
|
|
978
|
+
elseif not result.Success then
|
|
979
|
+
warn(`[BloxForge] Failed to deliver response for {requestId}: HTTP {result.StatusCode}`)
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
local function handleRequestOnce(conn, requestId, request, serverEpoch, deliveryAttempt, leaseToken)
|
|
983
|
+
-- If we've already completed this request, just re-deliver the cached response
|
|
984
|
+
local _requestId = requestId
|
|
985
|
+
local completed = completedRequests[_requestId]
|
|
986
|
+
if completed then
|
|
987
|
+
sendResponse(conn, requestId, completed.response, serverEpoch, deliveryAttempt, leaseToken)
|
|
988
|
+
return nil
|
|
989
|
+
end
|
|
990
|
+
-- If it's actively being processed, do nothing. The running thread will deliver
|
|
991
|
+
-- the response when it finishes.
|
|
992
|
+
local _requestId_1 = requestId
|
|
993
|
+
if activeRequests[_requestId_1] ~= nil then
|
|
994
|
+
return nil
|
|
995
|
+
end
|
|
996
|
+
-- Acknowledge the fenced request so the server knows we've started work.
|
|
997
|
+
-- This prevents the lease from expiring while we're executing long-running operations.
|
|
998
|
+
if serverEpoch ~= nil and deliveryAttempt ~= nil and leaseToken ~= nil then
|
|
999
|
+
task.spawn(function()
|
|
1000
|
+
pcall(function()
|
|
1001
|
+
local _object = {
|
|
1002
|
+
Url = `{conn.serverUrl}/ack`,
|
|
1003
|
+
Method = "POST",
|
|
1004
|
+
}
|
|
1005
|
+
local _left = "Headers"
|
|
1006
|
+
local _object_1 = {
|
|
1007
|
+
["Content-Type"] = "application/json",
|
|
1008
|
+
}
|
|
1009
|
+
local _value = conn.sessionToken
|
|
1010
|
+
for _k, _v in (if _value ~= "" and _value then {
|
|
1011
|
+
Authorization = `Bearer {conn.sessionToken}`,
|
|
1012
|
+
} else {}) do
|
|
1013
|
+
_object_1[_k] = _v
|
|
1014
|
+
end
|
|
1015
|
+
_object[_left] = _object_1
|
|
1016
|
+
_object.Body = HttpService:JSONEncode({
|
|
1017
|
+
pluginSessionId = pluginSessionId,
|
|
1018
|
+
requestId = requestId,
|
|
1019
|
+
serverEpoch = serverEpoch,
|
|
1020
|
+
deliveryAttempt = deliveryAttempt,
|
|
1021
|
+
leaseToken = leaseToken,
|
|
1022
|
+
})
|
|
1023
|
+
HttpService:RequestAsync(_object)
|
|
1024
|
+
end)
|
|
1025
|
+
end)
|
|
1026
|
+
end
|
|
1027
|
+
local _requestId_2 = requestId
|
|
1028
|
+
activeRequests[_requestId_2] = true
|
|
1029
|
+
task.spawn(function()
|
|
1030
|
+
local co = coroutine.running()
|
|
1031
|
+
local _requestId_3 = requestId
|
|
1032
|
+
syncThreadRequests[co] = _requestId_3
|
|
1033
|
+
local ok, result = pcall(function()
|
|
1034
|
+
return processRequest(request)
|
|
1035
|
+
end)
|
|
1036
|
+
syncThreadRequests[co] = nil
|
|
1037
|
+
local response = if ok then result else {
|
|
1038
|
+
error = tostring(result),
|
|
1039
|
+
}
|
|
1040
|
+
local _requestId_4 = requestId
|
|
1041
|
+
activeRequests[_requestId_4] = nil
|
|
1042
|
+
if #completedRequestOrder >= COMPLETED_REQUEST_LIMIT then
|
|
1043
|
+
local oldest = table.remove(completedRequestOrder, 1)
|
|
1044
|
+
if oldest ~= nil then
|
|
1045
|
+
completedRequests[oldest] = nil
|
|
1046
|
+
end
|
|
1047
|
+
end
|
|
1048
|
+
local _requestId_5 = requestId
|
|
1049
|
+
local _arg1 = {
|
|
1050
|
+
response = response,
|
|
1051
|
+
}
|
|
1052
|
+
completedRequests[_requestId_5] = _arg1
|
|
1053
|
+
local _requestId_6 = requestId
|
|
1054
|
+
table.insert(completedRequestOrder, _requestId_6)
|
|
1055
|
+
sendResponse(conn, requestId, response, serverEpoch, deliveryAttempt, leaseToken)
|
|
857
1056
|
end)
|
|
858
1057
|
end
|
|
859
1058
|
local function getConnectionStatus(connIndex)
|
|
@@ -969,8 +1168,12 @@ function sendReady(conn)
|
|
|
969
1168
|
local parseOk, readyData = pcall(function()
|
|
970
1169
|
return HttpService:JSONDecode(readyResult.Body)
|
|
971
1170
|
end)
|
|
972
|
-
local _value = parseOk and readyData.
|
|
1171
|
+
local _value = parseOk and readyData.sessionToken
|
|
973
1172
|
if _value ~= "" and _value then
|
|
1173
|
+
conn.sessionToken = readyData.sessionToken
|
|
1174
|
+
end
|
|
1175
|
+
local _value_1 = parseOk and readyData.assignedRole
|
|
1176
|
+
if _value_1 ~= "" and _value_1 then
|
|
974
1177
|
assignedRole = readyData.assignedRole
|
|
975
1178
|
end
|
|
976
1179
|
local _condition = parseOk
|
|
@@ -982,12 +1185,51 @@ function sendReady(conn)
|
|
|
982
1185
|
end
|
|
983
1186
|
end
|
|
984
1187
|
lastReadyInstanceId = if _condition then readyData.instanceId else instanceId
|
|
1188
|
+
local _condition_1 = parseOk
|
|
1189
|
+
if _condition_1 then
|
|
1190
|
+
_condition_1 = readyData.serverEpoch
|
|
1191
|
+
if _condition_1 ~= "" and _condition_1 then
|
|
1192
|
+
-- ▼ ReadonlyMap.size ▼
|
|
1193
|
+
local _size = 0
|
|
1194
|
+
for _ in completedRequests do
|
|
1195
|
+
_size += 1
|
|
1196
|
+
end
|
|
1197
|
+
-- ▲ ReadonlyMap.size ▲
|
|
1198
|
+
_condition_1 = _size > 0
|
|
1199
|
+
end
|
|
1200
|
+
end
|
|
1201
|
+
if _condition_1 ~= "" and _condition_1 then
|
|
1202
|
+
local receipts = {}
|
|
1203
|
+
for id, _ in completedRequests do
|
|
1204
|
+
local _arg0 = {
|
|
1205
|
+
requestId = id,
|
|
1206
|
+
completedAt = tick() * 1000,
|
|
1207
|
+
}
|
|
1208
|
+
table.insert(receipts, _arg0)
|
|
1209
|
+
end
|
|
1210
|
+
task.spawn(function()
|
|
1211
|
+
pcall(function()
|
|
1212
|
+
return HttpService:RequestAsync({
|
|
1213
|
+
Url = `{conn.serverUrl}/reconcile`,
|
|
1214
|
+
Method = "POST",
|
|
1215
|
+
Headers = {
|
|
1216
|
+
["Content-Type"] = "application/json",
|
|
1217
|
+
},
|
|
1218
|
+
Body = HttpService:JSONEncode({
|
|
1219
|
+
pluginSessionId = pluginSessionId,
|
|
1220
|
+
serverEpoch = readyData.serverEpoch,
|
|
1221
|
+
receipts = receipts,
|
|
1222
|
+
}),
|
|
1223
|
+
})
|
|
1224
|
+
end)
|
|
1225
|
+
end)
|
|
1226
|
+
end
|
|
985
1227
|
ServerUrlSettings.rememberServerUrl(conn.serverUrl)
|
|
986
|
-
local
|
|
987
|
-
if
|
|
988
|
-
|
|
1228
|
+
local _condition_2 = assignedRole
|
|
1229
|
+
if _condition_2 == nil then
|
|
1230
|
+
_condition_2 = detectRole()
|
|
989
1231
|
end
|
|
990
|
-
local connectedRole =
|
|
1232
|
+
local connectedRole = _condition_2
|
|
991
1233
|
if readyFailureLogKeys[readyLogKey] ~= nil then
|
|
992
1234
|
readyFailureLogKeys[readyLogKey] = nil
|
|
993
1235
|
print(`[BloxForge] /ready connected for {instanceId}/{connectedRole} via {conn.serverUrl}`)
|
|
@@ -998,30 +1240,53 @@ local function streamUrl(serverUrl)
|
|
|
998
1240
|
local websocketUrl = string.gsub(serverUrl, "^http", "ws")
|
|
999
1241
|
return `{websocketUrl}/stream?pluginSessionId={pluginSessionId}`
|
|
1000
1242
|
end
|
|
1001
|
-
local function sendStreamResponse(conn, requestId, response)
|
|
1243
|
+
local function sendStreamResponse(conn, requestId, response, serverEpoch, deliveryAttempt, leaseToken)
|
|
1002
1244
|
if not conn.streamOpen or not conn.streamClient then
|
|
1003
1245
|
return nil
|
|
1004
1246
|
end
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1247
|
+
local body = {
|
|
1248
|
+
type = "response",
|
|
1249
|
+
requestId = requestId,
|
|
1250
|
+
response = response,
|
|
1251
|
+
}
|
|
1252
|
+
if serverEpoch ~= nil then
|
|
1253
|
+
body.serverEpoch = serverEpoch
|
|
1254
|
+
end
|
|
1255
|
+
if deliveryAttempt ~= nil then
|
|
1256
|
+
body.deliveryAttempt = deliveryAttempt
|
|
1257
|
+
end
|
|
1258
|
+
if leaseToken ~= nil then
|
|
1259
|
+
body.leaseToken = leaseToken
|
|
1260
|
+
end
|
|
1261
|
+
local ok, result = pcall(function()
|
|
1262
|
+
return conn.streamClient:Send(HttpService:JSONEncode(body))
|
|
1011
1263
|
end)
|
|
1264
|
+
if not ok then
|
|
1265
|
+
warn(`[BloxForge] Failed to send stream response for {requestId}: {tostring(result)}`)
|
|
1266
|
+
end
|
|
1012
1267
|
end
|
|
1013
1268
|
local function startRequestStream(conn)
|
|
1014
1269
|
if not conn.isActive or conn.streamClient then
|
|
1015
1270
|
return nil
|
|
1016
1271
|
end
|
|
1017
1272
|
local ok, stream = pcall(function()
|
|
1018
|
-
|
|
1273
|
+
local _exp = Enum.WebStreamClientType.WebSocket
|
|
1274
|
+
local _object = {
|
|
1019
1275
|
Url = streamUrl(conn.serverUrl),
|
|
1020
1276
|
Method = "GET",
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1277
|
+
}
|
|
1278
|
+
local _left = "Headers"
|
|
1279
|
+
local _object_1 = {
|
|
1280
|
+
["Content-Type"] = "application/json",
|
|
1281
|
+
}
|
|
1282
|
+
local _value = conn.sessionToken
|
|
1283
|
+
for _k, _v in (if _value ~= "" and _value then {
|
|
1284
|
+
Authorization = `Bearer {conn.sessionToken}`,
|
|
1285
|
+
} else {}) do
|
|
1286
|
+
_object_1[_k] = _v
|
|
1287
|
+
end
|
|
1288
|
+
_object[_left] = _object_1
|
|
1289
|
+
return HttpService:CreateWebStreamClient(_exp, _object)
|
|
1025
1290
|
end)
|
|
1026
1291
|
if not ok or not stream then
|
|
1027
1292
|
return nil
|
|
@@ -1053,14 +1318,7 @@ local function startRequestStream(conn)
|
|
|
1053
1318
|
if _condition then
|
|
1054
1319
|
return nil
|
|
1055
1320
|
end
|
|
1056
|
-
|
|
1057
|
-
local handled, response = pcall(function()
|
|
1058
|
-
return processRequest(frame.request)
|
|
1059
|
-
end)
|
|
1060
|
-
sendStreamResponse(conn, frame.requestId, if handled then response else {
|
|
1061
|
-
error = tostring(response),
|
|
1062
|
-
})
|
|
1063
|
-
end)
|
|
1321
|
+
handleRequestOnce(conn, frame.requestId, frame.request, frame.serverEpoch, frame.deliveryAttempt, frame.leaseToken)
|
|
1064
1322
|
end)
|
|
1065
1323
|
local close = function()
|
|
1066
1324
|
if conn.streamClient ~= stream then
|
|
@@ -1083,13 +1341,22 @@ local function pollForRequests(connIndex)
|
|
|
1083
1341
|
end
|
|
1084
1342
|
conn.isPolling = true
|
|
1085
1343
|
local success, result = pcall(function()
|
|
1086
|
-
|
|
1344
|
+
local _object = {
|
|
1087
1345
|
Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}`,
|
|
1088
1346
|
Method = "GET",
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1347
|
+
}
|
|
1348
|
+
local _left = "Headers"
|
|
1349
|
+
local _object_1 = {
|
|
1350
|
+
["Content-Type"] = "application/json",
|
|
1351
|
+
}
|
|
1352
|
+
local _value = conn.sessionToken
|
|
1353
|
+
for _k, _v in (if _value ~= "" and _value then {
|
|
1354
|
+
Authorization = `Bearer {conn.sessionToken}`,
|
|
1355
|
+
} else {}) do
|
|
1356
|
+
_object_1[_k] = _v
|
|
1357
|
+
end
|
|
1358
|
+
_object[_left] = _object_1
|
|
1359
|
+
return HttpService:RequestAsync(_object)
|
|
1093
1360
|
end)
|
|
1094
1361
|
conn.isPolling = false
|
|
1095
1362
|
local ui = UI.getElements()
|
|
@@ -1118,16 +1385,49 @@ local function pollForRequests(connIndex)
|
|
|
1118
1385
|
end
|
|
1119
1386
|
local serverVersion = _condition
|
|
1120
1387
|
if data.versionMismatch == true then
|
|
1121
|
-
|
|
1388
|
+
versionMismatchConnections[conn] = true
|
|
1122
1389
|
local warningKey = `{State.CURRENT_VERSION}:{serverVersion}`
|
|
1123
1390
|
if lastVersionMismatchWarningKey ~= warningKey then
|
|
1124
1391
|
lastVersionMismatchWarningKey = warningKey
|
|
1125
1392
|
warn(`[BloxForge] Version mismatch: Studio plugin v{State.CURRENT_VERSION} / MCP v{serverVersion}. Run npx -y @princeofscale/bloxforge@latest --auto-install-plugin and restart Studio.`)
|
|
1126
1393
|
end
|
|
1127
1394
|
UI.showBanner("version-mismatch", `Plugin v{State.CURRENT_VERSION} / MCP v{serverVersion} mismatch`)
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1395
|
+
else
|
|
1396
|
+
versionMismatchConnections[conn] = nil
|
|
1397
|
+
-- ▼ ReadonlySet.size ▼
|
|
1398
|
+
local _size = 0
|
|
1399
|
+
for _ in versionMismatchConnections do
|
|
1400
|
+
_size += 1
|
|
1401
|
+
end
|
|
1402
|
+
-- ▲ ReadonlySet.size ▲
|
|
1403
|
+
if _size == 0 then
|
|
1404
|
+
UI.hideBanner("version-mismatch")
|
|
1405
|
+
end
|
|
1406
|
+
end
|
|
1407
|
+
local _condition_1 = data.serverProtocolVersion
|
|
1408
|
+
if _condition_1 == nil then
|
|
1409
|
+
_condition_1 = 0
|
|
1410
|
+
end
|
|
1411
|
+
local serverProtocol = _condition_1
|
|
1412
|
+
if data.protocolMismatch == true then
|
|
1413
|
+
protocolMismatchConnections[conn] = true
|
|
1414
|
+
local warningKey = `{State.PROTOCOL_VERSION}:{serverProtocol}`
|
|
1415
|
+
if lastProtocolMismatchWarningKey ~= warningKey then
|
|
1416
|
+
lastProtocolMismatchWarningKey = warningKey
|
|
1417
|
+
warn(`[BloxForge] Protocol mismatch: Studio plugin protocol v{State.PROTOCOL_VERSION} / MCP protocol v{serverProtocol}. Run npx -y @princeofscale/bloxforge@latest --auto-install-plugin and restart Studio.`)
|
|
1418
|
+
end
|
|
1419
|
+
UI.showBanner("protocol-mismatch", `Protocol mismatch: Plugin protocol v{State.PROTOCOL_VERSION} / MCP protocol v{serverProtocol}`)
|
|
1420
|
+
else
|
|
1421
|
+
protocolMismatchConnections[conn] = nil
|
|
1422
|
+
-- ▼ ReadonlySet.size ▼
|
|
1423
|
+
local _size = 0
|
|
1424
|
+
for _ in protocolMismatchConnections do
|
|
1425
|
+
_size += 1
|
|
1426
|
+
end
|
|
1427
|
+
-- ▲ ReadonlySet.size ▲
|
|
1428
|
+
if _size == 0 then
|
|
1429
|
+
UI.hideBanner("protocol-mismatch")
|
|
1430
|
+
end
|
|
1131
1431
|
end
|
|
1132
1432
|
-- Server tells us when its in-memory instances map doesn't have us
|
|
1133
1433
|
-- (e.g. after an MCP process restart, or after it reaped us during a
|
|
@@ -1146,12 +1446,12 @@ local function pollForRequests(connIndex)
|
|
|
1146
1446
|
local el = ui
|
|
1147
1447
|
el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
1148
1448
|
el.step1Label.Text = "HTTP server (OK)"
|
|
1149
|
-
local
|
|
1150
|
-
if
|
|
1449
|
+
local _condition_2 = mcpConnected
|
|
1450
|
+
if _condition_2 then
|
|
1151
1451
|
local _value = (string.find(el.statusLabel.Text, "Connected"))
|
|
1152
|
-
|
|
1452
|
+
_condition_2 = not (_value ~= 0 and _value == _value and _value)
|
|
1153
1453
|
end
|
|
1154
|
-
if
|
|
1454
|
+
if _condition_2 then
|
|
1155
1455
|
el.statusLabel.Text = "Connected"
|
|
1156
1456
|
el.statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
|
|
1157
1457
|
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
@@ -1182,28 +1482,22 @@ local function pollForRequests(connIndex)
|
|
|
1182
1482
|
conn.mcpWaitStartTime = tick()
|
|
1183
1483
|
end
|
|
1184
1484
|
local _exp = tick()
|
|
1185
|
-
local
|
|
1186
|
-
if
|
|
1187
|
-
|
|
1485
|
+
local _condition_3 = conn.mcpWaitStartTime
|
|
1486
|
+
if _condition_3 == nil then
|
|
1487
|
+
_condition_3 = tick()
|
|
1188
1488
|
end
|
|
1189
|
-
local elapsed = _exp -
|
|
1489
|
+
local elapsed = _exp - _condition_3
|
|
1190
1490
|
el.troubleshootLabel.Visible = elapsed > 8
|
|
1191
1491
|
UI.startPulseAnimation()
|
|
1192
1492
|
end
|
|
1193
1493
|
end
|
|
1494
|
+
if data.cancellations then
|
|
1495
|
+
for _, c in data.cancellations do
|
|
1496
|
+
cancelledSyncRequests[c] = true
|
|
1497
|
+
end
|
|
1498
|
+
end
|
|
1194
1499
|
if data.request and mcpConnected then
|
|
1195
|
-
|
|
1196
|
-
local ok, response = pcall(function()
|
|
1197
|
-
return processRequest(data.request)
|
|
1198
|
-
end)
|
|
1199
|
-
if ok then
|
|
1200
|
-
sendResponse(conn, data.requestId, response)
|
|
1201
|
-
else
|
|
1202
|
-
sendResponse(conn, data.requestId, {
|
|
1203
|
-
error = tostring(response),
|
|
1204
|
-
})
|
|
1205
|
-
end
|
|
1206
|
-
end)
|
|
1500
|
+
handleRequestOnce(conn, data.requestId, data.request, data.serverEpoch, data.deliveryAttempt, data.leaseToken)
|
|
1207
1501
|
end
|
|
1208
1502
|
elseif conn.isActive then
|
|
1209
1503
|
conn.consecutiveFailures += 1
|
|
@@ -1356,6 +1650,26 @@ function deactivatePlugin(connIndex)
|
|
|
1356
1650
|
end
|
|
1357
1651
|
conn.isActive = false
|
|
1358
1652
|
conn.lastMcpOk = false
|
|
1653
|
+
versionMismatchConnections[conn] = nil
|
|
1654
|
+
protocolMismatchConnections[conn] = nil
|
|
1655
|
+
-- ▼ ReadonlySet.size ▼
|
|
1656
|
+
local _size = 0
|
|
1657
|
+
for _ in versionMismatchConnections do
|
|
1658
|
+
_size += 1
|
|
1659
|
+
end
|
|
1660
|
+
-- ▲ ReadonlySet.size ▲
|
|
1661
|
+
if _size == 0 then
|
|
1662
|
+
UI.hideBanner("version-mismatch")
|
|
1663
|
+
end
|
|
1664
|
+
-- ▼ ReadonlySet.size ▼
|
|
1665
|
+
local _size_1 = 0
|
|
1666
|
+
for _ in protocolMismatchConnections do
|
|
1667
|
+
_size_1 += 1
|
|
1668
|
+
end
|
|
1669
|
+
-- ▲ ReadonlySet.size ▲
|
|
1670
|
+
if _size_1 == 0 then
|
|
1671
|
+
UI.hideBanner("protocol-mismatch")
|
|
1672
|
+
end
|
|
1359
1673
|
if idx == State.getActiveTabIndex() then
|
|
1360
1674
|
UI.updateUIState()
|
|
1361
1675
|
end
|
|
@@ -1418,7 +1732,13 @@ local function checkForUpdates()
|
|
|
1418
1732
|
if _condition ~= "" and _condition then
|
|
1419
1733
|
local latestVersion = data.version
|
|
1420
1734
|
if Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0 then
|
|
1421
|
-
|
|
1735
|
+
-- ▼ ReadonlySet.size ▼
|
|
1736
|
+
local _size = 0
|
|
1737
|
+
for _ in versionMismatchConnections do
|
|
1738
|
+
_size += 1
|
|
1739
|
+
end
|
|
1740
|
+
-- ▲ ReadonlySet.size ▲
|
|
1741
|
+
if _size == 0 then
|
|
1422
1742
|
UI.showBanner("update", `v{latestVersion} available - github.com/princeofscale/bloxforge`)
|
|
1423
1743
|
end
|
|
1424
1744
|
end
|
|
@@ -1428,6 +1748,7 @@ local function checkForUpdates()
|
|
|
1428
1748
|
end
|
|
1429
1749
|
return {
|
|
1430
1750
|
getConnectionStatus = getConnectionStatus,
|
|
1751
|
+
isCancelledForThread = isCancelledForThread,
|
|
1431
1752
|
activatePlugin = activatePlugin,
|
|
1432
1753
|
deactivatePlugin = deactivatePlugin,
|
|
1433
1754
|
deactivateAll = deactivateAll,
|
|
@@ -1556,9 +1877,9 @@ local function computeBridgeStamp()
|
|
|
1556
1877
|
for i = 1, #combined do
|
|
1557
1878
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1558
1879
|
end
|
|
1559
|
-
-- "
|
|
1880
|
+
-- "3.0.0" is replaced with the package version at package time
|
|
1560
1881
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1561
|
-
return `{tostring(h)}-
|
|
1882
|
+
return `{tostring(h)}-3.0.0`
|
|
1562
1883
|
end
|
|
1563
1884
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1564
1885
|
local function setSource(scriptInst, source)
|
|
@@ -4239,6 +4560,7 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
|
4239
4560
|
-- discard its result on completion.
|
|
4240
4561
|
local LuauExec = TS.import(script, script.Parent.Parent, "LuauExec")
|
|
4241
4562
|
local JobRegistry = TS.import(script, script.Parent.Parent, "JobRegistry")
|
|
4563
|
+
local Communication = TS.import(script, script.Parent.Parent, "Communication")
|
|
4242
4564
|
-- Install a sanctioned progress/cancel API once. Server-generated long-running
|
|
4243
4565
|
-- Luau (terrain/template/batch builders) can call _G.__mcp.progress(done,total,msg)
|
|
4244
4566
|
-- and _G.__mcp.checkCancelled() to report progress and bail early. We do NOT
|
|
@@ -4252,7 +4574,7 @@ local function installMcpGlobal()
|
|
|
4252
4574
|
JobRegistry.reportProgress(coroutine.running(), done, total, message, stage)
|
|
4253
4575
|
end,
|
|
4254
4576
|
checkCancelled = function()
|
|
4255
|
-
return JobRegistry.isCancelledForThread(coroutine.running())
|
|
4577
|
+
return JobRegistry.isCancelledForThread(coroutine.running()) or Communication.isCancelledForThread(coroutine.running())
|
|
4256
4578
|
end,
|
|
4257
4579
|
}
|
|
4258
4580
|
end
|
|
@@ -10353,7 +10675,6 @@ local function computeWrapperLineOffset()
|
|
|
10353
10675
|
local before = string.sub(probe, 1, start - 1)
|
|
10354
10676
|
return countLines(before) - 1
|
|
10355
10677
|
end
|
|
10356
|
-
local WRAPPER_LINE_OFFSET = computeWrapperLineOffset()
|
|
10357
10678
|
-- Count source lines so the wrapper can filter traceback frames that fall
|
|
10358
10679
|
-- outside the user code range (the wrapper's own preamble/postamble lines).
|
|
10359
10680
|
function countLines(s)
|
|
@@ -10388,7 +10709,7 @@ end
|
|
|
10388
10709
|
-- instead of hand-maintained, so reordering preamble lines can never desync
|
|
10389
10710
|
-- __mcp_LINE_OFFSET / remapPayloadLines from the real line count.
|
|
10390
10711
|
function renderWrapper(lineOffset, userLines, code, payloadPattern)
|
|
10391
|
-
return `return (
|
|
10712
|
+
return `return (function()\
|
|
10392
10713
|
\tlocal __mcp_traceback\
|
|
10393
10714
|
\tlocal __mcp_remap\
|
|
10394
10715
|
\tlocal __mcp_LINE_OFFSET = {lineOffset}\
|
|
@@ -10505,7 +10826,7 @@ function renderWrapper(lineOffset, userLines, code, payloadPattern)
|
|
|
10505
10826
|
\t__mcp_remap = function(s)\
|
|
10506
10827
|
\t\t-- Two chunk-name formats can reference our payload:\
|
|
10507
10828
|
\t\t-- * "Workspace.__MCPExecLuauPayload:N" — ModuleScript:require fallback path\
|
|
10508
|
-
\t\t-- * "[string \\"return (
|
|
10829
|
+
\t\t-- * "[string \\"return (function()...\\"]:N" — loadstring() (default in plugin)\
|
|
10509
10830
|
\t\t-- Subtract LINE_OFFSET to get the user-relative number, then clamp.\
|
|
10510
10831
|
\t\t-- Clamping matters for unclosed constructs ("local x = (") where the\
|
|
10511
10832
|
\t\t-- parser keeps reading into wrapper postamble and reports a payload\
|
|
@@ -10568,6 +10889,9 @@ function renderWrapper(lineOffset, userLines, code, payloadPattern)
|
|
|
10568
10889
|
\treturn \{ ok = ok, value = errOrValue, output = __mcp_output \}\
|
|
10569
10890
|
end)()`
|
|
10570
10891
|
end
|
|
10892
|
+
-- roblox-ts emits function declarations as ordered local assignments, so this
|
|
10893
|
+
-- must run after every helper used by computeWrapperLineOffset is initialized.
|
|
10894
|
+
local WRAPPER_LINE_OFFSET = computeWrapperLineOffset()
|
|
10571
10895
|
local function buildWrapper(code, payloadInstanceName)
|
|
10572
10896
|
if payloadInstanceName == nil then
|
|
10573
10897
|
payloadInstanceName = PAYLOAD_INSTANCE_NAME
|
|
@@ -10930,6 +11254,36 @@ end
|
|
|
10930
11254
|
local function nowSec()
|
|
10931
11255
|
return DateTime.now().UnixTimestampMillis / 1000
|
|
10932
11256
|
end
|
|
11257
|
+
-- Studio can expose binary-bearing Output messages that JSONEncode rejects.
|
|
11258
|
+
-- Preserve valid UTF-8 and make only malformed bytes visible and JSON-safe.
|
|
11259
|
+
local function escapeInvalidUtf8(message)
|
|
11260
|
+
local valid = utf8.len(message)
|
|
11261
|
+
if type(valid) == "number" then
|
|
11262
|
+
return message
|
|
11263
|
+
end
|
|
11264
|
+
local parts = {}
|
|
11265
|
+
local cursor = 1
|
|
11266
|
+
while cursor <= #message do
|
|
11267
|
+
local suffixValid, invalidPosition = utf8.len(message, cursor)
|
|
11268
|
+
if type(suffixValid) == "number" then
|
|
11269
|
+
local _arg0 = string.sub(message, cursor)
|
|
11270
|
+
table.insert(parts, _arg0)
|
|
11271
|
+
break
|
|
11272
|
+
end
|
|
11273
|
+
if not (type(invalidPosition) == "number") then
|
|
11274
|
+
break
|
|
11275
|
+
end
|
|
11276
|
+
if invalidPosition > cursor then
|
|
11277
|
+
local _arg0 = string.sub(message, cursor, invalidPosition - 1)
|
|
11278
|
+
table.insert(parts, _arg0)
|
|
11279
|
+
end
|
|
11280
|
+
local invalidByte = string.byte(message, invalidPosition)
|
|
11281
|
+
local _arg0 = string.format("\\x%02X", invalidByte)
|
|
11282
|
+
table.insert(parts, _arg0)
|
|
11283
|
+
cursor = invalidPosition + 1
|
|
11284
|
+
end
|
|
11285
|
+
return table.concat(parts, "")
|
|
11286
|
+
end
|
|
10933
11287
|
local function dropOldestUntilFits(incomingBytes)
|
|
10934
11288
|
while #entries > 0 and (totalBytes + incomingBytes > MAX_BYTES or #entries >= HARD_ENTRY_CAP) do
|
|
10935
11289
|
local dropped = table.remove(entries, 1)
|
|
@@ -10938,13 +11292,18 @@ local function dropOldestUntilFits(incomingBytes)
|
|
|
10938
11292
|
end
|
|
10939
11293
|
end
|
|
10940
11294
|
local function pushEntry(message, level, ts)
|
|
10941
|
-
local
|
|
11295
|
+
local safeMessage = escapeInvalidUtf8(message)
|
|
11296
|
+
local bytes = #safeMessage
|
|
11297
|
+
if bytes > MAX_BYTES then
|
|
11298
|
+
totalDropped += 1
|
|
11299
|
+
return nil
|
|
11300
|
+
end
|
|
10942
11301
|
dropOldestUntilFits(bytes)
|
|
10943
11302
|
local _arg0 = {
|
|
10944
11303
|
seq = nextSeq,
|
|
10945
11304
|
ts = ts,
|
|
10946
11305
|
level = level,
|
|
10947
|
-
message =
|
|
11306
|
+
message = safeMessage,
|
|
10948
11307
|
}
|
|
10949
11308
|
table.insert(entries, _arg0)
|
|
10950
11309
|
nextSeq += 1
|
|
@@ -11236,9 +11595,9 @@ return {
|
|
|
11236
11595
|
<Properties>
|
|
11237
11596
|
<string name="Name">State</string>
|
|
11238
11597
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
11239
|
-
local CURRENT_VERSION = "
|
|
11598
|
+
local CURRENT_VERSION = "3.0.0"
|
|
11240
11599
|
local PLUGIN_VARIANT = "inspector"
|
|
11241
|
-
local PROTOCOL_VERSION =
|
|
11600
|
+
local PROTOCOL_VERSION = 3
|
|
11242
11601
|
local MAX_CONNECTIONS = 5
|
|
11243
11602
|
local BASE_PORT = 58741
|
|
11244
11603
|
local activeTabIndex = 0
|