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 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
- let ctxPart = 'Ctx: ' + formatTokens(usedTokens);
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 modelId =
1257
- (sessionSettings && typeof sessionSettings.model === 'string' ? sessionSettings.model : null) ||
1258
- globalSettingsModel ||
1259
- null;
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 usedTokens = (last.cacheReadInputTokens || 0) + (last.contextCount || 0);
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: last.cacheReadInputTokens || 0,
1319
- deltaInput: last.contextCount || 0,
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
- if (Number.isFinite(cacheRead)) last.cacheReadInputTokens = cacheRead;
1352
- if (Number.isFinite(contextCount)) last.contextCount = contextCount;
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
- compacting = false;
1564
- settingsMtimeMs = 0;
1565
- lastCtxPollMs = 0;
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
- const tsMs = parseLineTimestampMs(line);
1589
- const lineSessionId = extractSessionIdFromLine(line);
1590
- const isSessionLine =
1591
- lineSessionId && String(lineSessionId).toLowerCase() === sessionIdLower;
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) {
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
- 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
- }
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
- if (isSessionLine || (compacting && !lineSessionId)) {
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
- const { row, col } = cursor.position();
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("if(R&&!B.supportedReasoningEfforts.includes(R))"),
3217
- replacement: Buffer.from("if(0&&!B.supportedReasoningEfforts.includes(R))")
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("if(R&&!B.supportedReasoningEfforts.includes(R))"),
3454
- replacement: Buffer.from("if(0&&!B.supportedReasoningEfforts.includes(R))")
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) {