droid-patch 0.8.1 → 0.8.3
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/cli.mjs +211 -37
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -706,19 +706,31 @@ function extractSessionIdFromLine(line) {
|
|
|
706
706
|
return m ? m[1] : null;
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
+
function parseLineTimestampMs(line) {
|
|
710
|
+
const s = String(line || '');
|
|
711
|
+
if (!s || s[0] !== '[') return null;
|
|
712
|
+
const end = s.indexOf(']');
|
|
713
|
+
if (end <= 1) return null;
|
|
714
|
+
const raw = s.slice(1, end);
|
|
715
|
+
const ms = Date.parse(raw);
|
|
716
|
+
return Number.isFinite(ms) ? ms : null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function safeStatMtimeMs(p) {
|
|
720
|
+
try {
|
|
721
|
+
const stat = fs.statSync(p);
|
|
722
|
+
const ms = Number(stat?.mtimeMs ?? 0);
|
|
723
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
724
|
+
} catch {
|
|
725
|
+
return 0;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
709
729
|
function nextCompactionState(line, current) {
|
|
710
730
|
if (!line) return current;
|
|
711
731
|
if (line.includes('[Compaction] Start')) return true;
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
line.includes('[Compaction] Done') ||
|
|
715
|
-
line.includes('[Compaction] Finish') ||
|
|
716
|
-
line.includes('[Compaction] Finished') ||
|
|
717
|
-
line.includes('[Compaction] Complete') ||
|
|
718
|
-
line.includes('[Compaction] Completed')
|
|
719
|
-
) {
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
732
|
+
const endMarkers = ['End', 'Done', 'Finish', 'Finished', 'Complete', 'Completed'];
|
|
733
|
+
if (endMarkers.some(m => line.includes('[Compaction] ' + m))) return false;
|
|
722
734
|
return current;
|
|
723
735
|
}
|
|
724
736
|
|
|
@@ -1184,7 +1196,7 @@ function buildLine(params) {
|
|
|
1184
1196
|
}
|
|
1185
1197
|
|
|
1186
1198
|
async function main() {
|
|
1187
|
-
|
|
1199
|
+
let factoryConfig = readJsonFile(CONFIG_PATH) || {};
|
|
1188
1200
|
|
|
1189
1201
|
const cwd = process.cwd();
|
|
1190
1202
|
const cwdBase = path.basename(cwd) || cwd;
|
|
@@ -1231,22 +1243,68 @@ async function main() {
|
|
|
1231
1243
|
}
|
|
1232
1244
|
|
|
1233
1245
|
if (!sessionId || !workspaceDir) return;
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1246
|
+
let sessionIdLower = String(sessionId).toLowerCase();
|
|
1247
|
+
|
|
1248
|
+
let settingsPath = '';
|
|
1249
|
+
let sessionSettings = {};
|
|
1250
|
+
({ settingsPath, settings: sessionSettings } = resolveSessionSettings(workspaceDir, sessionId));
|
|
1251
|
+
|
|
1252
|
+
let configMtimeMs = safeStatMtimeMs(CONFIG_PATH);
|
|
1253
|
+
let globalSettingsMtimeMs = safeStatMtimeMs(GLOBAL_SETTINGS_PATH);
|
|
1254
|
+
let globalSettingsModel = resolveGlobalSettingsModel();
|
|
1255
|
+
|
|
1256
|
+
let modelId =
|
|
1257
|
+
(sessionSettings && typeof sessionSettings.model === 'string' ? sessionSettings.model : null) ||
|
|
1258
|
+
globalSettingsModel ||
|
|
1259
|
+
null;
|
|
1260
|
+
|
|
1261
|
+
let provider =
|
|
1262
|
+
sessionSettings && typeof sessionSettings.providerLock === 'string'
|
|
1263
|
+
? sessionSettings.providerLock
|
|
1264
|
+
: resolveProvider(modelId, factoryConfig);
|
|
1265
|
+
let underlyingModel = resolveUnderlyingModelId(modelId, factoryConfig) || modelId || 'unknown';
|
|
1266
|
+
|
|
1267
|
+
function refreshModel() {
|
|
1268
|
+
const nextModelId =
|
|
1269
|
+
(sessionSettings && typeof sessionSettings.model === 'string' ? sessionSettings.model : null) ||
|
|
1270
|
+
globalSettingsModel ||
|
|
1271
|
+
null;
|
|
1272
|
+
|
|
1273
|
+
// Use providerLock if set, otherwise resolve from model/config (same logic as initialization)
|
|
1274
|
+
const nextProvider =
|
|
1275
|
+
sessionSettings && typeof sessionSettings.providerLock === 'string'
|
|
1276
|
+
? sessionSettings.providerLock
|
|
1277
|
+
: resolveProvider(nextModelId, factoryConfig);
|
|
1278
|
+
const nextUnderlying = resolveUnderlyingModelId(nextModelId, factoryConfig) || nextModelId || 'unknown';
|
|
1279
|
+
|
|
1280
|
+
let changed = false;
|
|
1281
|
+
if (nextModelId !== modelId) {
|
|
1282
|
+
modelId = nextModelId;
|
|
1283
|
+
changed = true;
|
|
1284
|
+
}
|
|
1285
|
+
if (nextProvider !== provider) {
|
|
1286
|
+
provider = nextProvider;
|
|
1287
|
+
changed = true;
|
|
1288
|
+
}
|
|
1289
|
+
if (nextUnderlying !== underlyingModel) {
|
|
1290
|
+
underlyingModel = nextUnderlying;
|
|
1291
|
+
changed = true;
|
|
1292
|
+
}
|
|
1239
1293
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1294
|
+
if (changed) renderNow();
|
|
1295
|
+
}
|
|
1242
1296
|
|
|
1243
1297
|
let last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
|
|
1244
|
-
let sessionUsage =
|
|
1298
|
+
let sessionUsage =
|
|
1299
|
+
sessionSettings && typeof sessionSettings.tokenUsage === 'object' && sessionSettings.tokenUsage
|
|
1300
|
+
? sessionSettings.tokenUsage
|
|
1301
|
+
: {};
|
|
1245
1302
|
let compacting = false;
|
|
1246
1303
|
let lastRenderAt = 0;
|
|
1247
1304
|
let lastRenderedLine = '';
|
|
1248
1305
|
let gitBranch = '';
|
|
1249
1306
|
let gitDiff = '';
|
|
1307
|
+
let lastContextMs = 0;
|
|
1250
1308
|
|
|
1251
1309
|
function renderNow() {
|
|
1252
1310
|
const usedTokens = (last.cacheReadInputTokens || 0) + (last.contextCount || 0);
|
|
@@ -1284,19 +1342,25 @@ async function main() {
|
|
|
1284
1342
|
let reseedInProgress = false;
|
|
1285
1343
|
let reseedQueued = false;
|
|
1286
1344
|
|
|
1287
|
-
function updateLastFromContext(ctx, updateOutputTokens) {
|
|
1345
|
+
function updateLastFromContext(ctx, updateOutputTokens, tsMs) {
|
|
1346
|
+
const ts = Number.isFinite(tsMs) ? tsMs : null;
|
|
1347
|
+
if (ts != null && lastContextMs && ts < lastContextMs) return false;
|
|
1288
1348
|
const cacheRead = Number(ctx?.cacheReadInputTokens);
|
|
1289
1349
|
const contextCount = Number(ctx?.contextCount);
|
|
1290
1350
|
const out = Number(ctx?.outputTokens);
|
|
1291
1351
|
if (Number.isFinite(cacheRead)) last.cacheReadInputTokens = cacheRead;
|
|
1292
1352
|
if (Number.isFinite(contextCount)) last.contextCount = contextCount;
|
|
1293
1353
|
if (updateOutputTokens && Number.isFinite(out)) last.outputTokens = out;
|
|
1354
|
+
if (ts != null) lastContextMs = ts;
|
|
1355
|
+
return true;
|
|
1294
1356
|
}
|
|
1295
1357
|
|
|
1296
1358
|
function seedLastContextFromLog(options) {
|
|
1297
1359
|
const opts = options || {};
|
|
1298
1360
|
const maxScanBytes = Number.isFinite(opts.maxScanBytes) ? opts.maxScanBytes : 64 * 1024 * 1024;
|
|
1299
1361
|
const preferStreaming = !!opts.preferStreaming;
|
|
1362
|
+
const minTimestampMs = Number.isFinite(lastContextMs) && lastContextMs > 0 ? lastContextMs : 0;
|
|
1363
|
+
const earlyStopAfterBestBytes = Math.min(2 * 1024 * 1024, Math.max(256 * 1024, maxScanBytes));
|
|
1300
1364
|
|
|
1301
1365
|
if (reseedInProgress) {
|
|
1302
1366
|
reseedQueued = true;
|
|
@@ -1318,15 +1382,20 @@ async function main() {
|
|
|
1318
1382
|
let pos = size;
|
|
1319
1383
|
let scanned = 0;
|
|
1320
1384
|
let remainder = '';
|
|
1321
|
-
let
|
|
1385
|
+
let bestCtx = null;
|
|
1386
|
+
let bestIsStreaming = false;
|
|
1387
|
+
let bestTs = null;
|
|
1388
|
+
let bestHasTs = false;
|
|
1389
|
+
let bytesSinceBest = 0;
|
|
1322
1390
|
|
|
1323
|
-
while (pos > 0 && scanned < maxScanBytes && !
|
|
1391
|
+
while (pos > 0 && scanned < maxScanBytes && (!bestHasTs || bytesSinceBest < earlyStopAfterBestBytes)) {
|
|
1324
1392
|
const readSize = Math.min(CHUNK_BYTES, pos);
|
|
1325
1393
|
const start = pos - readSize;
|
|
1326
1394
|
const buf = Buffer.alloc(readSize);
|
|
1327
1395
|
fs.readSync(fd, buf, 0, readSize, start);
|
|
1328
1396
|
pos = start;
|
|
1329
1397
|
scanned += readSize;
|
|
1398
|
+
bytesSinceBest += readSize;
|
|
1330
1399
|
|
|
1331
1400
|
let text = buf.toString('utf8') + remainder;
|
|
1332
1401
|
let lines = String(text).split('\\n');
|
|
@@ -1361,13 +1430,34 @@ async function main() {
|
|
|
1361
1430
|
const hasUsage = Number.isFinite(cacheRead) || Number.isFinite(contextCount);
|
|
1362
1431
|
if (!hasUsage) continue;
|
|
1363
1432
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1433
|
+
const ts = parseLineTimestampMs(line);
|
|
1434
|
+
if (ts != null && minTimestampMs && ts < minTimestampMs) {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (ts != null) {
|
|
1439
|
+
if (!bestHasTs || ts > bestTs) {
|
|
1440
|
+
bestCtx = ctx;
|
|
1441
|
+
bestIsStreaming = isStreaming;
|
|
1442
|
+
bestTs = ts;
|
|
1443
|
+
bestHasTs = true;
|
|
1444
|
+
bytesSinceBest = 0;
|
|
1445
|
+
}
|
|
1446
|
+
} else if (!bestHasTs && !bestCtx) {
|
|
1447
|
+
// No timestamps available yet: the first match when scanning backward
|
|
1448
|
+
// is the most recent in file order.
|
|
1449
|
+
bestCtx = ctx;
|
|
1450
|
+
bestIsStreaming = isStreaming;
|
|
1451
|
+
bestTs = null;
|
|
1452
|
+
}
|
|
1367
1453
|
}
|
|
1368
1454
|
|
|
1369
1455
|
if (remainder.length > 8192) remainder = remainder.slice(-8192);
|
|
1370
1456
|
}
|
|
1457
|
+
|
|
1458
|
+
if (bestCtx) {
|
|
1459
|
+
updateLastFromContext(bestCtx, bestIsStreaming, bestTs);
|
|
1460
|
+
}
|
|
1371
1461
|
} finally {
|
|
1372
1462
|
try {
|
|
1373
1463
|
fs.closeSync(fd);
|
|
@@ -1399,17 +1489,36 @@ async function main() {
|
|
|
1399
1489
|
let settingsMtimeMs = 0;
|
|
1400
1490
|
let lastCtxPollMs = 0;
|
|
1401
1491
|
setInterval(() => {
|
|
1492
|
+
// Refresh config/global settings if they changed (model display depends on these).
|
|
1493
|
+
const configMtime = safeStatMtimeMs(CONFIG_PATH);
|
|
1494
|
+
if (configMtime && configMtime !== configMtimeMs) {
|
|
1495
|
+
configMtimeMs = configMtime;
|
|
1496
|
+
factoryConfig = readJsonFile(CONFIG_PATH) || {};
|
|
1497
|
+
refreshModel();
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const globalMtime = safeStatMtimeMs(GLOBAL_SETTINGS_PATH);
|
|
1501
|
+
if (globalMtime && globalMtime !== globalSettingsMtimeMs) {
|
|
1502
|
+
globalSettingsMtimeMs = globalMtime;
|
|
1503
|
+
globalSettingsModel = resolveGlobalSettingsModel();
|
|
1504
|
+
refreshModel();
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1402
1507
|
try {
|
|
1403
1508
|
const stat = fs.statSync(settingsPath);
|
|
1404
1509
|
if (stat.mtimeMs === settingsMtimeMs) return;
|
|
1405
1510
|
settingsMtimeMs = stat.mtimeMs;
|
|
1406
1511
|
const next = readJsonFile(settingsPath) || {};
|
|
1512
|
+
sessionSettings = next;
|
|
1407
1513
|
|
|
1408
1514
|
// Keep session token usage in sync (used by /status).
|
|
1409
1515
|
if (next && typeof next.tokenUsage === 'object' && next.tokenUsage) {
|
|
1410
1516
|
sessionUsage = next.tokenUsage;
|
|
1411
1517
|
}
|
|
1412
1518
|
|
|
1519
|
+
// Keep model/provider in sync (model can change during a running session).
|
|
1520
|
+
refreshModel();
|
|
1521
|
+
|
|
1413
1522
|
const now = Date.now();
|
|
1414
1523
|
if (now - lastRenderAt >= MIN_RENDER_INTERVAL_MS) {
|
|
1415
1524
|
lastRenderAt = now;
|
|
@@ -1431,6 +1540,37 @@ async function main() {
|
|
|
1431
1540
|
seedLastContextFromLog({ maxScanBytes: 4 * 1024 * 1024, preferStreaming: false });
|
|
1432
1541
|
}, 2000).unref();
|
|
1433
1542
|
|
|
1543
|
+
function switchToSession(nextSessionId) {
|
|
1544
|
+
if (!nextSessionId || !isUuid(nextSessionId)) return;
|
|
1545
|
+
const nextLower = String(nextSessionId).toLowerCase();
|
|
1546
|
+
if (nextLower === sessionIdLower) return;
|
|
1547
|
+
|
|
1548
|
+
sessionId = nextSessionId;
|
|
1549
|
+
sessionIdLower = nextLower;
|
|
1550
|
+
|
|
1551
|
+
const resolved = resolveSessionSettings(workspaceDir, nextSessionId);
|
|
1552
|
+
settingsPath = resolved.settingsPath;
|
|
1553
|
+
sessionSettings = resolved.settings || {};
|
|
1554
|
+
|
|
1555
|
+
sessionUsage =
|
|
1556
|
+
sessionSettings && typeof sessionSettings.tokenUsage === 'object' && sessionSettings.tokenUsage
|
|
1557
|
+
? sessionSettings.tokenUsage
|
|
1558
|
+
: {};
|
|
1559
|
+
|
|
1560
|
+
// Reset cached state for the new session.
|
|
1561
|
+
last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
|
|
1562
|
+
lastContextMs = 0;
|
|
1563
|
+
compacting = false;
|
|
1564
|
+
settingsMtimeMs = 0;
|
|
1565
|
+
lastCtxPollMs = 0;
|
|
1566
|
+
|
|
1567
|
+
refreshModel();
|
|
1568
|
+
renderNow();
|
|
1569
|
+
|
|
1570
|
+
// Best-effort: if the new session already has Context lines in the log, seed quickly.
|
|
1571
|
+
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1434
1574
|
// Follow the Factory log and update based on session-scoped events.
|
|
1435
1575
|
const tail = spawn('tail', ['-n', '0', '-F', LOG_PATH], {
|
|
1436
1576
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -1445,10 +1585,35 @@ async function main() {
|
|
|
1445
1585
|
const line = buffer.slice(0, idx).trimEnd();
|
|
1446
1586
|
buffer = buffer.slice(idx + 1);
|
|
1447
1587
|
|
|
1588
|
+
const tsMs = parseLineTimestampMs(line);
|
|
1448
1589
|
const lineSessionId = extractSessionIdFromLine(line);
|
|
1449
1590
|
const isSessionLine =
|
|
1450
1591
|
lineSessionId && String(lineSessionId).toLowerCase() === sessionIdLower;
|
|
1451
1592
|
|
|
1593
|
+
// /compress (aka /compact) can create a new session ID. Follow it so ctx/model keep updating.
|
|
1594
|
+
if (line.includes('oldSessionId') && line.includes('newSessionId') && line.includes('Context:')) {
|
|
1595
|
+
const ctxIndex = line.indexOf('Context: ');
|
|
1596
|
+
if (ctxIndex !== -1) {
|
|
1597
|
+
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1598
|
+
try {
|
|
1599
|
+
const meta = JSON.parse(jsonStr);
|
|
1600
|
+
const oldId = meta?.oldSessionId;
|
|
1601
|
+
const newId = meta?.newSessionId;
|
|
1602
|
+
if (
|
|
1603
|
+
isUuid(oldId) &&
|
|
1604
|
+
isUuid(newId) &&
|
|
1605
|
+
String(oldId).toLowerCase() === sessionIdLower &&
|
|
1606
|
+
String(newId).toLowerCase() !== sessionIdLower
|
|
1607
|
+
) {
|
|
1608
|
+
switchToSession(String(newId));
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
} catch {
|
|
1612
|
+
// ignore
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1452
1617
|
let compactionChanged = false;
|
|
1453
1618
|
let compactionEnded = false;
|
|
1454
1619
|
if (line.includes('[Compaction]')) {
|
|
@@ -1464,6 +1629,20 @@ async function main() {
|
|
|
1464
1629
|
}
|
|
1465
1630
|
}
|
|
1466
1631
|
|
|
1632
|
+
if (compactionChanged && compacting) {
|
|
1633
|
+
// Compaction can start after a context-limit error. Ensure we display the latest
|
|
1634
|
+
// pre-compaction ctx by reseeding from log (tail can miss bursts).
|
|
1635
|
+
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: true });
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (compactionEnded) {
|
|
1639
|
+
// ctx usage changes dramatically after compaction, but the next Context line
|
|
1640
|
+
// can be delayed. Clear displayed ctx immediately to avoid showing stale numbers.
|
|
1641
|
+
last.cacheReadInputTokens = 0;
|
|
1642
|
+
last.contextCount = 0;
|
|
1643
|
+
if (tsMs != null) lastContextMs = tsMs;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1467
1646
|
if (!line.includes('Context:')) {
|
|
1468
1647
|
if (compactionChanged) {
|
|
1469
1648
|
lastRenderAt = Date.now();
|
|
@@ -1507,7 +1686,7 @@ async function main() {
|
|
|
1507
1686
|
|
|
1508
1687
|
// Context usage can appear on multiple session-scoped log lines; update whenever present.
|
|
1509
1688
|
// (Streaming is still the best source for outputTokens / LastOut.)
|
|
1510
|
-
updateLastFromContext(ctx, false);
|
|
1689
|
+
updateLastFromContext(ctx, false, tsMs);
|
|
1511
1690
|
|
|
1512
1691
|
// For new sessions: if this is the first valid Context line and ctx is still 0,
|
|
1513
1692
|
// trigger a reseed to catch any earlier log entries we might have missed.
|
|
@@ -1519,7 +1698,7 @@ async function main() {
|
|
|
1519
1698
|
}
|
|
1520
1699
|
|
|
1521
1700
|
if (line.includes('[Agent] Streaming result')) {
|
|
1522
|
-
updateLastFromContext(ctx, true);
|
|
1701
|
+
updateLastFromContext(ctx, true, tsMs);
|
|
1523
1702
|
}
|
|
1524
1703
|
|
|
1525
1704
|
const now = Date.now();
|
|
@@ -2276,12 +2455,7 @@ async function main() {
|
|
|
2276
2455
|
}
|
|
2277
2456
|
|
|
2278
2457
|
function includesScrollRegionCSI() {
|
|
2279
|
-
|
|
2280
|
-
try {
|
|
2281
|
-
return /\\x1b\\[[0-9]*;?[0-9]*r/.test(detectStr);
|
|
2282
|
-
} catch {
|
|
2283
|
-
return false;
|
|
2284
|
-
}
|
|
2458
|
+
return /\\x1b\\[[0-9]*;?[0-9]*r/.test(detectStr);
|
|
2285
2459
|
}
|
|
2286
2460
|
|
|
2287
2461
|
function updateCursorVisibility() {
|
|
@@ -3039,8 +3213,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3039
3213
|
patches.push({
|
|
3040
3214
|
name: "reasoningEffortValidationBypass",
|
|
3041
3215
|
description: "Bypass reasoning effort validation (allows xhigh in settings.json)",
|
|
3042
|
-
pattern: Buffer.from("
|
|
3043
|
-
replacement: Buffer.from("
|
|
3216
|
+
pattern: Buffer.from("T!==\"none\"&&T!==\"off\"&&!W.supportedReasoningEfforts.includes(T)"),
|
|
3217
|
+
replacement: Buffer.from("T!=\"none\"&&T!=\"off\"&&0&&W.supportedReasoningEfforts.includes(T)")
|
|
3044
3218
|
});
|
|
3045
3219
|
}
|
|
3046
3220
|
if (noTelemetry) {
|
|
@@ -3276,8 +3450,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3276
3450
|
patches.push({
|
|
3277
3451
|
name: "reasoningEffortValidationBypass",
|
|
3278
3452
|
description: "Bypass reasoning effort validation (allows xhigh in settings.json)",
|
|
3279
|
-
pattern: Buffer.from("
|
|
3280
|
-
replacement: Buffer.from("
|
|
3453
|
+
pattern: Buffer.from("T!==\"none\"&&T!==\"off\"&&!W.supportedReasoningEfforts.includes(T)"),
|
|
3454
|
+
replacement: Buffer.from("T!=\"none\"&&T!=\"off\"&&0&&W.supportedReasoningEfforts.includes(T)")
|
|
3281
3455
|
});
|
|
3282
3456
|
}
|
|
3283
3457
|
if (meta.patches.noTelemetry) {
|