droid-patch 0.8.1 → 0.8.2

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
@@ -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
- if (
713
- line.includes('[Compaction] End') ||
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
- const factoryConfig = readJsonFile(CONFIG_PATH) || {};
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
- const sessionIdLower = String(sessionId).toLowerCase();
1235
-
1236
- const { settingsPath, settings } = resolveSessionSettings(workspaceDir, sessionId);
1237
- const modelId =
1238
- (settings && typeof settings.model === 'string' ? settings.model : null) || resolveGlobalSettingsModel();
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
- const provider = resolveProvider(modelId, factoryConfig);
1241
- const underlyingModel = resolveUnderlyingModelId(modelId, factoryConfig) || modelId || 'unknown';
1294
+ if (changed) renderNow();
1295
+ }
1242
1296
 
1243
1297
  let last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
1244
- let sessionUsage = settings && typeof settings.tokenUsage === 'object' && settings.tokenUsage ? settings.tokenUsage : {};
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 seeded = false;
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 && !seeded) {
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
- updateLastFromContext(ctx, isStreaming);
1365
- seeded = true;
1366
- break;
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
- // Equivalent to Python: re.search(b"\\x1b\\\\[[0-9]*;?[0-9]*r", detect_buf)
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() {