droid-patch 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +408 -48
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1147,12 +1147,18 @@ function buildLine(params) {
|
|
|
1147
1147
|
lastOutputTokens,
|
|
1148
1148
|
sessionUsage,
|
|
1149
1149
|
compacting,
|
|
1150
|
+
ctxAvailable,
|
|
1151
|
+
ctxApprox,
|
|
1152
|
+
ctxOverflow,
|
|
1150
1153
|
} = params;
|
|
1151
1154
|
|
|
1152
|
-
|
|
1155
|
+
const ctxValue = !ctxAvailable
|
|
1156
|
+
? '--'
|
|
1157
|
+
: (ctxApprox ? '~' : '') + formatTokens(usedTokens) + (ctxOverflow ? '+' : '');
|
|
1158
|
+
let ctxPart = 'Ctx: ' + ctxValue;
|
|
1153
1159
|
|
|
1154
1160
|
const cachePart =
|
|
1155
|
-
cacheRead > 0 || deltaInput > 0
|
|
1161
|
+
ctxAvailable && !ctxApprox && !ctxOverflow && (cacheRead > 0 || deltaInput > 0)
|
|
1156
1162
|
? ' c' + formatTokens(cacheRead) + '+n' + formatTokens(deltaInput)
|
|
1157
1163
|
: '';
|
|
1158
1164
|
|
|
@@ -1253,10 +1259,18 @@ async function main() {
|
|
|
1253
1259
|
let globalSettingsMtimeMs = safeStatMtimeMs(GLOBAL_SETTINGS_PATH);
|
|
1254
1260
|
let globalSettingsModel = resolveGlobalSettingsModel();
|
|
1255
1261
|
|
|
1256
|
-
let
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1262
|
+
let modelIdFromLog = null;
|
|
1263
|
+
|
|
1264
|
+
function resolveActiveModelId() {
|
|
1265
|
+
const fromSession =
|
|
1266
|
+
sessionSettings && typeof sessionSettings.model === 'string' ? sessionSettings.model : null;
|
|
1267
|
+
if (fromSession && String(fromSession).startsWith('custom:')) return fromSession;
|
|
1268
|
+
const fromLog = typeof modelIdFromLog === 'string' ? modelIdFromLog : null;
|
|
1269
|
+
if (fromLog) return fromLog;
|
|
1270
|
+
return fromSession || globalSettingsModel || null;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
let modelId = resolveActiveModelId();
|
|
1260
1274
|
|
|
1261
1275
|
let provider =
|
|
1262
1276
|
sessionSettings && typeof sessionSettings.providerLock === 'string'
|
|
@@ -1265,10 +1279,7 @@ async function main() {
|
|
|
1265
1279
|
let underlyingModel = resolveUnderlyingModelId(modelId, factoryConfig) || modelId || 'unknown';
|
|
1266
1280
|
|
|
1267
1281
|
function refreshModel() {
|
|
1268
|
-
const nextModelId =
|
|
1269
|
-
(sessionSettings && typeof sessionSettings.model === 'string' ? sessionSettings.model : null) ||
|
|
1270
|
-
globalSettingsModel ||
|
|
1271
|
-
null;
|
|
1282
|
+
const nextModelId = resolveActiveModelId();
|
|
1272
1283
|
|
|
1273
1284
|
// Use providerLock if set, otherwise resolve from model/config (same logic as initialization)
|
|
1274
1285
|
const nextProvider =
|
|
@@ -1305,9 +1316,22 @@ async function main() {
|
|
|
1305
1316
|
let gitBranch = '';
|
|
1306
1317
|
let gitDiff = '';
|
|
1307
1318
|
let lastContextMs = 0;
|
|
1319
|
+
let ctxAvailable = false;
|
|
1320
|
+
let ctxApprox = false;
|
|
1321
|
+
let ctxOverflow = false;
|
|
1322
|
+
let ctxOverrideUsedTokens = null;
|
|
1323
|
+
|
|
1324
|
+
let baselineCacheReadInputTokens = 0;
|
|
1325
|
+
let knownContextMaxTokens = 0;
|
|
1326
|
+
let pendingCompactionSuffixTokens = null;
|
|
1327
|
+
let pendingCompactionSummaryOutputTokens = null;
|
|
1328
|
+
let pendingCompactionSummaryTsMs = null;
|
|
1308
1329
|
|
|
1309
1330
|
function renderNow() {
|
|
1310
|
-
const
|
|
1331
|
+
const override = Number.isFinite(ctxOverrideUsedTokens) && ctxOverrideUsedTokens > 0 ? ctxOverrideUsedTokens : null;
|
|
1332
|
+
const usedTokens = override != null ? override : (last.cacheReadInputTokens || 0) + (last.contextCount || 0);
|
|
1333
|
+
const cacheRead = override != null ? 0 : last.cacheReadInputTokens || 0;
|
|
1334
|
+
const deltaInput = override != null ? 0 : last.contextCount || 0;
|
|
1311
1335
|
const line = buildLine({
|
|
1312
1336
|
provider,
|
|
1313
1337
|
model: underlyingModel,
|
|
@@ -1315,11 +1339,14 @@ async function main() {
|
|
|
1315
1339
|
gitBranch,
|
|
1316
1340
|
gitDiff,
|
|
1317
1341
|
usedTokens,
|
|
1318
|
-
cacheRead
|
|
1319
|
-
deltaInput
|
|
1342
|
+
cacheRead,
|
|
1343
|
+
deltaInput,
|
|
1320
1344
|
lastOutputTokens: last.outputTokens || 0,
|
|
1321
1345
|
sessionUsage,
|
|
1322
1346
|
compacting,
|
|
1347
|
+
ctxAvailable: override != null ? true : ctxAvailable,
|
|
1348
|
+
ctxApprox,
|
|
1349
|
+
ctxOverflow,
|
|
1323
1350
|
});
|
|
1324
1351
|
if (line !== lastRenderedLine) {
|
|
1325
1352
|
lastRenderedLine = line;
|
|
@@ -1339,22 +1366,275 @@ async function main() {
|
|
|
1339
1366
|
} catch {}
|
|
1340
1367
|
}, 0).unref();
|
|
1341
1368
|
|
|
1369
|
+
// Seed known context max tokens from recent log failures (some providers omit explicit counts).
|
|
1370
|
+
setTimeout(() => {
|
|
1371
|
+
try {
|
|
1372
|
+
seedKnownContextMaxTokensFromLog(8 * 1024 * 1024);
|
|
1373
|
+
} catch {}
|
|
1374
|
+
}, 0).unref();
|
|
1375
|
+
|
|
1342
1376
|
let reseedInProgress = false;
|
|
1343
1377
|
let reseedQueued = false;
|
|
1344
1378
|
|
|
1379
|
+
function extractModelIdFromContext(ctx) {
|
|
1380
|
+
const tagged = ctx?.tags?.modelId;
|
|
1381
|
+
if (typeof tagged === 'string') return tagged;
|
|
1382
|
+
const direct = ctx?.modelId;
|
|
1383
|
+
return typeof direct === 'string' ? direct : null;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1345
1386
|
function updateLastFromContext(ctx, updateOutputTokens, tsMs) {
|
|
1346
1387
|
const ts = Number.isFinite(tsMs) ? tsMs : null;
|
|
1347
1388
|
if (ts != null && lastContextMs && ts < lastContextMs) return false;
|
|
1348
1389
|
const cacheRead = Number(ctx?.cacheReadInputTokens);
|
|
1349
1390
|
const contextCount = Number(ctx?.contextCount);
|
|
1350
1391
|
const out = Number(ctx?.outputTokens);
|
|
1351
|
-
|
|
1352
|
-
|
|
1392
|
+
const hasTokens =
|
|
1393
|
+
(Number.isFinite(cacheRead) && cacheRead > 0) ||
|
|
1394
|
+
(Number.isFinite(contextCount) && contextCount > 0);
|
|
1395
|
+
if (hasTokens) {
|
|
1396
|
+
// Treat 0/0 as "not reported" (some providers log zeros even when prompt exists).
|
|
1397
|
+
// If at least one field is >0, accept both fields (including zero) as reliable.
|
|
1398
|
+
if (Number.isFinite(cacheRead)) last.cacheReadInputTokens = cacheRead;
|
|
1399
|
+
if (Number.isFinite(contextCount)) last.contextCount = contextCount;
|
|
1400
|
+
ctxAvailable = true;
|
|
1401
|
+
ctxOverrideUsedTokens = null;
|
|
1402
|
+
ctxApprox = false;
|
|
1403
|
+
ctxOverflow = false;
|
|
1404
|
+
if (Number.isFinite(cacheRead) && cacheRead > 0) {
|
|
1405
|
+
baselineCacheReadInputTokens = baselineCacheReadInputTokens
|
|
1406
|
+
? Math.min(baselineCacheReadInputTokens, cacheRead)
|
|
1407
|
+
: cacheRead;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1353
1410
|
if (updateOutputTokens && Number.isFinite(out)) last.outputTokens = out;
|
|
1354
|
-
if (ts != null) lastContextMs = ts;
|
|
1411
|
+
if (hasTokens && ts != null) lastContextMs = ts;
|
|
1412
|
+
|
|
1413
|
+
const nextModelIdFromLog = extractModelIdFromContext(ctx);
|
|
1414
|
+
if (nextModelIdFromLog && nextModelIdFromLog !== modelIdFromLog) {
|
|
1415
|
+
modelIdFromLog = nextModelIdFromLog;
|
|
1416
|
+
refreshModel();
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1355
1419
|
return true;
|
|
1356
1420
|
}
|
|
1357
1421
|
|
|
1422
|
+
function setCtxOverride(usedTokens, options) {
|
|
1423
|
+
const opts = options || {};
|
|
1424
|
+
const v = Number(usedTokens);
|
|
1425
|
+
if (!Number.isFinite(v) || v <= 0) return false;
|
|
1426
|
+
const prevUsed = ctxOverrideUsedTokens;
|
|
1427
|
+
const prevApprox = ctxApprox;
|
|
1428
|
+
const prevOverflow = ctxOverflow;
|
|
1429
|
+
ctxOverrideUsedTokens = v;
|
|
1430
|
+
ctxAvailable = true;
|
|
1431
|
+
ctxApprox = !!opts.approx;
|
|
1432
|
+
ctxOverflow = !!opts.overflow;
|
|
1433
|
+
const ts = Number.isFinite(opts.tsMs) ? opts.tsMs : null;
|
|
1434
|
+
if (ts != null && (!lastContextMs || ts > lastContextMs)) lastContextMs = ts;
|
|
1435
|
+
if (prevUsed !== ctxOverrideUsedTokens || prevApprox !== ctxApprox || prevOverflow !== ctxOverflow) {
|
|
1436
|
+
renderNow();
|
|
1437
|
+
}
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function parseContextLimitFromMessage(message) {
|
|
1442
|
+
const s = String(message || '');
|
|
1443
|
+
let promptTokens = null;
|
|
1444
|
+
let maxTokens = null;
|
|
1445
|
+
|
|
1446
|
+
const pair = s.match(/(\\d+)\\s*tokens\\s*>\\s*(\\d+)\\s*maximum/i);
|
|
1447
|
+
if (pair) {
|
|
1448
|
+
const prompt = Number(pair[1]);
|
|
1449
|
+
const max = Number(pair[2]);
|
|
1450
|
+
if (Number.isFinite(prompt)) promptTokens = prompt;
|
|
1451
|
+
if (Number.isFinite(max)) maxTokens = max;
|
|
1452
|
+
return { promptTokens, maxTokens };
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const promptMatch = s.match(/prompt\\s+is\\s+too\\s+long:\\s*(\\d+)\\s*tokens/i);
|
|
1456
|
+
if (promptMatch) {
|
|
1457
|
+
const prompt = Number(promptMatch[1]);
|
|
1458
|
+
if (Number.isFinite(prompt)) promptTokens = prompt;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const maxMatch = s.match(/>\\s*(\\d+)\\s*maximum/i);
|
|
1462
|
+
if (maxMatch) {
|
|
1463
|
+
const max = Number(maxMatch[1]);
|
|
1464
|
+
if (Number.isFinite(max)) maxTokens = max;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
return { promptTokens, maxTokens };
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function seedKnownContextMaxTokensFromLog(maxScanBytes = 4 * 1024 * 1024) {
|
|
1471
|
+
try {
|
|
1472
|
+
const stat = fs.statSync(LOG_PATH);
|
|
1473
|
+
const size = Number(stat?.size ?? 0);
|
|
1474
|
+
if (!(size > 0)) return;
|
|
1475
|
+
|
|
1476
|
+
const scan = Math.max(256 * 1024, maxScanBytes);
|
|
1477
|
+
const readSize = Math.min(size, scan);
|
|
1478
|
+
const start = Math.max(0, size - readSize);
|
|
1479
|
+
|
|
1480
|
+
const buf = Buffer.alloc(readSize);
|
|
1481
|
+
const fd = fs.openSync(LOG_PATH, 'r');
|
|
1482
|
+
try {
|
|
1483
|
+
fs.readSync(fd, buf, 0, readSize, start);
|
|
1484
|
+
} finally {
|
|
1485
|
+
try {
|
|
1486
|
+
fs.closeSync(fd);
|
|
1487
|
+
} catch {}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const text = buf.toString('utf8');
|
|
1491
|
+
const re = /(\\d+)\\s*tokens\\s*>\\s*(\\d+)\\s*maximum/gi;
|
|
1492
|
+
let m;
|
|
1493
|
+
while ((m = re.exec(text))) {
|
|
1494
|
+
const max = Number(m[2]);
|
|
1495
|
+
if (Number.isFinite(max) && max > 0) knownContextMaxTokens = max;
|
|
1496
|
+
}
|
|
1497
|
+
} catch {}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function maybeUpdateCtxFromContextLimitFailure(line, ctx, tsMs) {
|
|
1501
|
+
if (!line || !ctx) return false;
|
|
1502
|
+
if (!String(line).includes('[Chat route failure]')) return false;
|
|
1503
|
+
const reason = ctx?.reason;
|
|
1504
|
+
if (reason !== 'llmContextExceeded') return false;
|
|
1505
|
+
|
|
1506
|
+
const msg = ctx?.error?.message;
|
|
1507
|
+
if (typeof msg !== 'string' || !msg) return false;
|
|
1508
|
+
const parsed = parseContextLimitFromMessage(msg);
|
|
1509
|
+
const max = Number(parsed?.maxTokens);
|
|
1510
|
+
if (Number.isFinite(max) && max > 0) knownContextMaxTokens = max;
|
|
1511
|
+
|
|
1512
|
+
const prompt = Number(parsed?.promptTokens);
|
|
1513
|
+
if (Number.isFinite(prompt) && prompt > 0) {
|
|
1514
|
+
return setCtxOverride(prompt, { tsMs, approx: false, overflow: false });
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
if (Number.isFinite(max) && max > 0) {
|
|
1518
|
+
return setCtxOverride(max, { tsMs, approx: false, overflow: true });
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
if (knownContextMaxTokens > 0) {
|
|
1522
|
+
return setCtxOverride(knownContextMaxTokens, { tsMs, approx: false, overflow: true });
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function maybeCaptureCompactionSuffix(line, ctx) {
|
|
1529
|
+
if (!line || !ctx) return;
|
|
1530
|
+
if (!String(line).includes('[Compaction] Suffix selection')) return;
|
|
1531
|
+
const suffix = Number(ctx?.suffixTokens);
|
|
1532
|
+
if (Number.isFinite(suffix) && suffix >= 0) pendingCompactionSuffixTokens = suffix;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function maybeApplyPostCompactionEstimate(line, ctx, tsMs) {
|
|
1536
|
+
if (!line || !ctx) return false;
|
|
1537
|
+
if (!String(line).includes('[Compaction] End')) return false;
|
|
1538
|
+
if (ctx?.eventType !== 'compaction' || ctx?.state !== 'end') return false;
|
|
1539
|
+
const reason = ctx?.reason || ctx?.tags?.compactionReason || null;
|
|
1540
|
+
if (!(reason === 'context_limit' || reason === 'manual')) return false;
|
|
1541
|
+
|
|
1542
|
+
const summaryOut = Number(ctx?.summaryOutputTokens);
|
|
1543
|
+
if (!Number.isFinite(summaryOut) || summaryOut < 0) return false;
|
|
1544
|
+
|
|
1545
|
+
const prefix = baselineCacheReadInputTokens > 0 ? baselineCacheReadInputTokens : 0;
|
|
1546
|
+
const suffix = Number.isFinite(pendingCompactionSuffixTokens) ? pendingCompactionSuffixTokens : 0;
|
|
1547
|
+
pendingCompactionSuffixTokens = null;
|
|
1548
|
+
|
|
1549
|
+
const est = prefix + suffix + summaryOut;
|
|
1550
|
+
if (est <= 0) return false;
|
|
1551
|
+
return setCtxOverride(est, { tsMs, approx: true, overflow: false });
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function maybeApplyCompactSessionEstimate(sessionIdToEstimate, options, attempt = 0) {
|
|
1555
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
1556
|
+
const id = String(sessionIdToEstimate || '');
|
|
1557
|
+
if (!isUuid(id)) return;
|
|
1558
|
+
if (!workspaceDir) return;
|
|
1559
|
+
|
|
1560
|
+
// forceApply allows overriding even if ctx is already set (useful for manual /compress)
|
|
1561
|
+
const forceApply = !!opts?.forceApply;
|
|
1562
|
+
if (!forceApply) {
|
|
1563
|
+
if (ctxAvailable) return;
|
|
1564
|
+
if (Number.isFinite(ctxOverrideUsedTokens) && ctxOverrideUsedTokens > 0) return;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const suffixVal = opts?.suffixTokens;
|
|
1568
|
+
const suffixTokens =
|
|
1569
|
+
typeof suffixVal === 'number' && Number.isFinite(suffixVal) && suffixVal >= 0 ? suffixVal : 0;
|
|
1570
|
+
const tsVal = opts?.tsMs;
|
|
1571
|
+
const ts = typeof tsVal === 'number' && Number.isFinite(tsVal) ? tsVal : null;
|
|
1572
|
+
|
|
1573
|
+
const jsonlPath = path.join(workspaceDir, id + '.jsonl');
|
|
1574
|
+
let head = null;
|
|
1575
|
+
try {
|
|
1576
|
+
const fd = fs.openSync(jsonlPath, 'r');
|
|
1577
|
+
try {
|
|
1578
|
+
const maxBytes = 2 * 1024 * 1024;
|
|
1579
|
+
const buf = Buffer.alloc(maxBytes);
|
|
1580
|
+
const bytes = fs.readSync(fd, buf, 0, maxBytes, 0);
|
|
1581
|
+
head = buf.slice(0, Math.max(0, bytes)).toString('utf8');
|
|
1582
|
+
} finally {
|
|
1583
|
+
try {
|
|
1584
|
+
fs.closeSync(fd);
|
|
1585
|
+
} catch {}
|
|
1586
|
+
}
|
|
1587
|
+
} catch {
|
|
1588
|
+
head = null;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
if (!head) {
|
|
1592
|
+
if (attempt < 40) {
|
|
1593
|
+
setTimeout(() => {
|
|
1594
|
+
maybeApplyCompactSessionEstimate(id, opts, attempt + 1);
|
|
1595
|
+
}, 150).unref();
|
|
1596
|
+
}
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
let summaryText = null;
|
|
1601
|
+
for (const raw of head.split('\\n')) {
|
|
1602
|
+
if (!raw) continue;
|
|
1603
|
+
let obj;
|
|
1604
|
+
try {
|
|
1605
|
+
obj = JSON.parse(raw);
|
|
1606
|
+
} catch {
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
if (obj && obj.type === 'compaction_state' && typeof obj.summaryText === 'string') {
|
|
1610
|
+
summaryText = obj.summaryText;
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (!summaryText) {
|
|
1615
|
+
if (attempt < 40) {
|
|
1616
|
+
setTimeout(() => {
|
|
1617
|
+
maybeApplyCompactSessionEstimate(id, opts, attempt + 1);
|
|
1618
|
+
}, 150).unref();
|
|
1619
|
+
}
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Rough token estimate (no tokenizer deps): English-like text averages ~4 chars/token.
|
|
1624
|
+
// Non-ASCII tends to be denser; use a smaller divisor.
|
|
1625
|
+
let ascii = 0;
|
|
1626
|
+
let other = 0;
|
|
1627
|
+
for (let i = 0; i < summaryText.length; i++) {
|
|
1628
|
+
const code = summaryText.charCodeAt(i);
|
|
1629
|
+
if (code <= 0x7f) ascii += 1;
|
|
1630
|
+
else other += 1;
|
|
1631
|
+
}
|
|
1632
|
+
const summaryTokens = Math.max(1, Math.ceil(ascii / 4 + other / 1.5));
|
|
1633
|
+
const prefix = baselineCacheReadInputTokens > 0 ? baselineCacheReadInputTokens : 0;
|
|
1634
|
+
const est = prefix + suffixTokens + summaryTokens;
|
|
1635
|
+
if (est > 0) setCtxOverride(est, { tsMs: ts, approx: true, overflow: false });
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1358
1638
|
function seedLastContextFromLog(options) {
|
|
1359
1639
|
const opts = options || {};
|
|
1360
1640
|
const maxScanBytes = Number.isFinite(opts.maxScanBytes) ? opts.maxScanBytes : 64 * 1024 * 1024;
|
|
@@ -1560,9 +1840,17 @@ async function main() {
|
|
|
1560
1840
|
// Reset cached state for the new session.
|
|
1561
1841
|
last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
|
|
1562
1842
|
lastContextMs = 0;
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1843
|
+
ctxAvailable = false;
|
|
1844
|
+
ctxApprox = false;
|
|
1845
|
+
ctxOverflow = false;
|
|
1846
|
+
ctxOverrideUsedTokens = null;
|
|
1847
|
+
pendingCompactionSuffixTokens = null;
|
|
1848
|
+
pendingCompactionSummaryOutputTokens = null;
|
|
1849
|
+
pendingCompactionSummaryTsMs = null;
|
|
1850
|
+
modelIdFromLog = null;
|
|
1851
|
+
compacting = false;
|
|
1852
|
+
settingsMtimeMs = 0;
|
|
1853
|
+
lastCtxPollMs = 0;
|
|
1566
1854
|
|
|
1567
1855
|
refreshModel();
|
|
1568
1856
|
renderNow();
|
|
@@ -1585,29 +1873,81 @@ async function main() {
|
|
|
1585
1873
|
const line = buffer.slice(0, idx).trimEnd();
|
|
1586
1874
|
buffer = buffer.slice(idx + 1);
|
|
1587
1875
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1876
|
+
const tsMs = parseLineTimestampMs(line);
|
|
1877
|
+
const lineSessionId = extractSessionIdFromLine(line);
|
|
1878
|
+
const isSessionLine =
|
|
1879
|
+
lineSessionId && String(lineSessionId).toLowerCase() === sessionIdLower;
|
|
1880
|
+
|
|
1881
|
+
if (compacting && line.includes('[Compaction] End') && line.includes('Context:')) {
|
|
1882
|
+
const ctxIndex = line.indexOf('Context: ');
|
|
1883
|
+
if (ctxIndex !== -1) {
|
|
1884
|
+
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1885
|
+
try {
|
|
1886
|
+
const meta = JSON.parse(jsonStr);
|
|
1887
|
+
const summaryOut = Number(meta?.summaryOutputTokens);
|
|
1888
|
+
if (
|
|
1889
|
+
meta?.eventType === 'compaction' &&
|
|
1890
|
+
meta?.state === 'end' &&
|
|
1891
|
+
Number.isFinite(summaryOut) &&
|
|
1892
|
+
summaryOut >= 0
|
|
1893
|
+
) {
|
|
1894
|
+
pendingCompactionSummaryOutputTokens = summaryOut;
|
|
1895
|
+
if (tsMs != null) pendingCompactionSummaryTsMs = tsMs;
|
|
1896
|
+
}
|
|
1897
|
+
} catch {
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// /compress (aka /compact) can create a new session ID. Follow it so ctx/model keep updating.
|
|
1903
|
+
if (line.includes('oldSessionId') && line.includes('newSessionId') && line.includes('Context:')) {
|
|
1904
|
+
const ctxIndex = line.indexOf('Context: ');
|
|
1905
|
+
if (ctxIndex !== -1) {
|
|
1597
1906
|
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1598
1907
|
try {
|
|
1599
1908
|
const meta = JSON.parse(jsonStr);
|
|
1600
1909
|
const oldId = meta?.oldSessionId;
|
|
1601
1910
|
const newId = meta?.newSessionId;
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1911
|
+
if (
|
|
1912
|
+
isUuid(oldId) &&
|
|
1913
|
+
isUuid(newId) &&
|
|
1914
|
+
String(oldId).toLowerCase() === sessionIdLower &&
|
|
1915
|
+
String(newId).toLowerCase() !== sessionIdLower
|
|
1916
|
+
) {
|
|
1917
|
+
const suffixTokens = Number.isFinite(pendingCompactionSuffixTokens)
|
|
1918
|
+
? pendingCompactionSuffixTokens
|
|
1919
|
+
: 0;
|
|
1920
|
+
const summaryOutTokens = Number.isFinite(pendingCompactionSummaryOutputTokens)
|
|
1921
|
+
? pendingCompactionSummaryOutputTokens
|
|
1922
|
+
: null;
|
|
1923
|
+
const summaryTsMs = Number.isFinite(pendingCompactionSummaryTsMs) ? pendingCompactionSummaryTsMs : null;
|
|
1924
|
+
|
|
1925
|
+
// Save baseline before switching session (it persists across sessions)
|
|
1926
|
+
const savedBaseline = baselineCacheReadInputTokens;
|
|
1927
|
+
|
|
1928
|
+
switchToSession(String(newId));
|
|
1929
|
+
|
|
1930
|
+
// For manual /compress, immediately set an estimated ctx value
|
|
1931
|
+
// This ensures the statusline shows a reasonable value right after compression
|
|
1932
|
+
if (summaryOutTokens != null && summaryOutTokens > 0) {
|
|
1933
|
+
const prefix = savedBaseline > 0 ? savedBaseline : 0;
|
|
1934
|
+
const est = prefix + suffixTokens + summaryOutTokens;
|
|
1935
|
+
if (est > 0) {
|
|
1936
|
+
setCtxOverride(est, { tsMs: summaryTsMs != null ? summaryTsMs : tsMs, approx: true, overflow: false });
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// Always attempt to get a more accurate estimate from the new session's jsonl
|
|
1941
|
+
// This will read the compaction_state and estimate tokens from summaryText
|
|
1942
|
+
// Note: we pass forceApply=true to override even if ctxOverrideUsedTokens is set,
|
|
1943
|
+
// because the jsonl-based estimate may be more accurate
|
|
1944
|
+
maybeApplyCompactSessionEstimate(String(newId), {
|
|
1945
|
+
suffixTokens,
|
|
1946
|
+
tsMs: summaryTsMs != null ? summaryTsMs : tsMs,
|
|
1947
|
+
forceApply: true,
|
|
1948
|
+
});
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1611
1951
|
} catch {
|
|
1612
1952
|
// ignore
|
|
1613
1953
|
}
|
|
@@ -1619,7 +1959,13 @@ async function main() {
|
|
|
1619
1959
|
if (line.includes('[Compaction]')) {
|
|
1620
1960
|
// Accept session-scoped compaction lines; allow end markers to clear even
|
|
1621
1961
|
// if the line lacks a session id (some builds omit Context on end lines).
|
|
1622
|
-
|
|
1962
|
+
// For manual /compress, [Compaction] End uses the NEW session ID, so we need
|
|
1963
|
+
// to also accept End markers when compacting is true and it's an End line.
|
|
1964
|
+
const isManualCompactionEnd = compacting &&
|
|
1965
|
+
line.includes('[Compaction] End') &&
|
|
1966
|
+
lineSessionId &&
|
|
1967
|
+
String(lineSessionId).toLowerCase() !== sessionIdLower;
|
|
1968
|
+
if (isSessionLine || (compacting && !lineSessionId) || isManualCompactionEnd) {
|
|
1623
1969
|
const next = nextCompactionState(line, compacting);
|
|
1624
1970
|
if (next !== compacting) {
|
|
1625
1971
|
compacting = next;
|
|
@@ -1630,6 +1976,9 @@ async function main() {
|
|
|
1630
1976
|
}
|
|
1631
1977
|
|
|
1632
1978
|
if (compactionChanged && compacting) {
|
|
1979
|
+
pendingCompactionSuffixTokens = null;
|
|
1980
|
+
pendingCompactionSummaryOutputTokens = null;
|
|
1981
|
+
pendingCompactionSummaryTsMs = null;
|
|
1633
1982
|
// Compaction can start after a context-limit error. Ensure we display the latest
|
|
1634
1983
|
// pre-compaction ctx by reseeding from log (tail can miss bursts).
|
|
1635
1984
|
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: true });
|
|
@@ -1640,6 +1989,10 @@ async function main() {
|
|
|
1640
1989
|
// can be delayed. Clear displayed ctx immediately to avoid showing stale numbers.
|
|
1641
1990
|
last.cacheReadInputTokens = 0;
|
|
1642
1991
|
last.contextCount = 0;
|
|
1992
|
+
ctxAvailable = false;
|
|
1993
|
+
ctxOverrideUsedTokens = null;
|
|
1994
|
+
ctxApprox = false;
|
|
1995
|
+
ctxOverflow = false;
|
|
1643
1996
|
if (tsMs != null) lastContextMs = tsMs;
|
|
1644
1997
|
}
|
|
1645
1998
|
|
|
@@ -1688,6 +2041,10 @@ async function main() {
|
|
|
1688
2041
|
// (Streaming is still the best source for outputTokens / LastOut.)
|
|
1689
2042
|
updateLastFromContext(ctx, false, tsMs);
|
|
1690
2043
|
|
|
2044
|
+
maybeCaptureCompactionSuffix(line, ctx);
|
|
2045
|
+
maybeUpdateCtxFromContextLimitFailure(line, ctx, tsMs);
|
|
2046
|
+
maybeApplyPostCompactionEstimate(line, ctx, tsMs);
|
|
2047
|
+
|
|
1691
2048
|
// For new sessions: if this is the first valid Context line and ctx is still 0,
|
|
1692
2049
|
// trigger a reseed to catch any earlier log entries we might have missed.
|
|
1693
2050
|
if (!initialSeedDone && last.contextCount === 0) {
|
|
@@ -2498,9 +2855,11 @@ async function main() {
|
|
|
2498
2855
|
writeStdout(Buffer.from(rewritten));
|
|
2499
2856
|
}
|
|
2500
2857
|
|
|
2858
|
+
let cleanupCalled = false;
|
|
2501
2859
|
function onChildExit(exitCode, signal) {
|
|
2502
|
-
if (shouldStop) return;
|
|
2503
2860
|
shouldStop = true;
|
|
2861
|
+
if (cleanupCalled) return;
|
|
2862
|
+
cleanupCalled = true;
|
|
2504
2863
|
const code = exitCode ?? (signal != null ? 128 + signal : 0);
|
|
2505
2864
|
cleanup().finally(() => process.exit(code));
|
|
2506
2865
|
}
|
|
@@ -2585,6 +2944,9 @@ async function main() {
|
|
|
2585
2944
|
|
|
2586
2945
|
// Forward signals to child's process group when possible.
|
|
2587
2946
|
const forward = (sig) => {
|
|
2947
|
+
// Stop processing child output before forwarding signal
|
|
2948
|
+
// This prevents the child's cleanup/clear screen sequences from being written
|
|
2949
|
+
shouldStop = true;
|
|
2588
2950
|
try {
|
|
2589
2951
|
process.kill(-pgid, sig);
|
|
2590
2952
|
} catch {
|
|
@@ -2646,12 +3008,10 @@ async function main() {
|
|
|
2646
3008
|
try {
|
|
2647
3009
|
process.stdin.setRawMode(false);
|
|
2648
3010
|
} catch {}
|
|
3011
|
+
// Don't clear screen or reset scroll region on exit - preserve session ID and logs
|
|
3012
|
+
// Only reset colors and show cursor
|
|
2649
3013
|
try {
|
|
2650
|
-
|
|
2651
|
-
renderer.clearReservedArea(physicalRows, physicalCols, effectiveReservedRows, row, col);
|
|
2652
|
-
} catch {}
|
|
2653
|
-
try {
|
|
2654
|
-
writeStdout("\\x1b[r\\x1b[0m\\x1b[?25h");
|
|
3014
|
+
writeStdout("\\x1b[0m\\x1b[?25h");
|
|
2655
3015
|
} catch {}
|
|
2656
3016
|
try {
|
|
2657
3017
|
monitor.kill();
|
|
@@ -3213,8 +3573,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3213
3573
|
patches.push({
|
|
3214
3574
|
name: "reasoningEffortValidationBypass",
|
|
3215
3575
|
description: "Bypass reasoning effort validation (allows xhigh in settings.json)",
|
|
3216
|
-
pattern: Buffer.from("
|
|
3217
|
-
replacement: Buffer.from("
|
|
3576
|
+
pattern: Buffer.from("T!==\"none\"&&T!==\"off\"&&!W.supportedReasoningEfforts.includes(T)"),
|
|
3577
|
+
replacement: Buffer.from("T!=\"none\"&&T!=\"off\"&&0&&W.supportedReasoningEfforts.includes(T)")
|
|
3218
3578
|
});
|
|
3219
3579
|
}
|
|
3220
3580
|
if (noTelemetry) {
|
|
@@ -3450,8 +3810,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3450
3810
|
patches.push({
|
|
3451
3811
|
name: "reasoningEffortValidationBypass",
|
|
3452
3812
|
description: "Bypass reasoning effort validation (allows xhigh in settings.json)",
|
|
3453
|
-
pattern: Buffer.from("
|
|
3454
|
-
replacement: Buffer.from("
|
|
3813
|
+
pattern: Buffer.from("T!==\"none\"&&T!==\"off\"&&!W.supportedReasoningEfforts.includes(T)"),
|
|
3814
|
+
replacement: Buffer.from("T!=\"none\"&&T!=\"off\"&&0&&W.supportedReasoningEfforts.includes(T)")
|
|
3455
3815
|
});
|
|
3456
3816
|
}
|
|
3457
3817
|
if (meta.patches.noTelemetry) {
|