droid-patch 0.8.3 → 0.9.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/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-
|
|
2
|
+
import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-DkFWCjWn.mjs";
|
|
3
3
|
import bin from "tiny-bin";
|
|
4
4
|
import { styleText } from "node:util";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -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();
|
|
@@ -3023,7 +3383,7 @@ function findDefaultDroidPath() {
|
|
|
3023
3383
|
for (const p of paths) if (existsSync(p)) return p;
|
|
3024
3384
|
return join(home, ".droid", "bin", "droid");
|
|
3025
3385
|
}
|
|
3026
|
-
bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy (each instance runs own proxy, auto-cleanup on exit)").option("--statusline", "Enable a Claude-style statusline (terminal UI)").option("--sessions", "Enable sessions browser (--sessions flag in alias)").option("--standalone", "Standalone mode: mock non-LLM Factory APIs (use with --websearch)").option("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").option("--disable-telemetry", "Disable telemetry and Sentry error reporting (block data uploads)").option("--dry-run", "Verify patches without actually modifying the binary").option("-p, --path <path>", "Path to the droid binary").option("-o, --output <dir>", "Output directory for patched binary").option("--no-backup", "Do not create backup of original binary").option("-v, --verbose", "Enable verbose output").argument("[alias]", "Alias name for the patched binary").action(async (options, args) => {
|
|
3386
|
+
bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy (each instance runs own proxy, auto-cleanup on exit)").option("--statusline", "Enable a Claude-style statusline (terminal UI)").option("--sessions", "Enable sessions browser (--sessions flag in alias)").option("--standalone", "Standalone mode: mock non-LLM Factory APIs (use with --websearch)").option("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").option("--disable-telemetry", "Disable telemetry and Sentry error reporting (block data uploads)").option("--auto-high", "Set default autonomy mode to auto-high (bypass settings.json race condition)").option("--dry-run", "Verify patches without actually modifying the binary").option("-p, --path <path>", "Path to the droid binary").option("-o, --output <dir>", "Output directory for patched binary").option("--no-backup", "Do not create backup of original binary").option("-v, --verbose", "Enable verbose output").argument("[alias]", "Alias name for the patched binary").action(async (options, args) => {
|
|
3027
3387
|
const alias = args?.[0];
|
|
3028
3388
|
const isCustom = options["is-custom"];
|
|
3029
3389
|
const skipLogin = options["skip-login"];
|
|
@@ -3035,13 +3395,14 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3035
3395
|
const websearchTarget = websearch ? apiBase || "https://api.factory.ai" : void 0;
|
|
3036
3396
|
const reasoningEffort = options["reasoning-effort"];
|
|
3037
3397
|
const noTelemetry = options["disable-telemetry"];
|
|
3398
|
+
const autoHigh = options["auto-high"];
|
|
3038
3399
|
const dryRun = options["dry-run"];
|
|
3039
3400
|
const path = options.path || findDefaultDroidPath();
|
|
3040
3401
|
const outputDir = options.output;
|
|
3041
3402
|
const backup = options.backup !== false;
|
|
3042
3403
|
const verbose = options.verbose;
|
|
3043
3404
|
const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
|
|
3044
|
-
const needsBinaryPatch = !!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!apiBase && !websearch;
|
|
3405
|
+
const needsBinaryPatch = !!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!autoHigh || !!apiBase && !websearch;
|
|
3045
3406
|
const statuslineEnabled = statusline;
|
|
3046
3407
|
if (!needsBinaryPatch && (websearch || statuslineEnabled)) {
|
|
3047
3408
|
if (!alias) {
|
|
@@ -3117,7 +3478,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3117
3478
|
}
|
|
3118
3479
|
return;
|
|
3119
3480
|
}
|
|
3120
|
-
if (!isCustom && !skipLogin && !apiBase && !websearch && !statuslineEnabled && !reasoningEffort && !noTelemetry) {
|
|
3481
|
+
if (!isCustom && !skipLogin && !apiBase && !websearch && !statuslineEnabled && !reasoningEffort && !noTelemetry && !autoHigh) {
|
|
3121
3482
|
console.log(styleText("yellow", "No patch flags specified. Available patches:"));
|
|
3122
3483
|
console.log(styleText("gray", " --is-custom Patch isCustom for custom models"));
|
|
3123
3484
|
console.log(styleText("gray", " --skip-login Bypass login by injecting a fake API key"));
|
|
@@ -3126,6 +3487,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3126
3487
|
console.log(styleText("gray", " --statusline Enable Claude-style statusline"));
|
|
3127
3488
|
console.log(styleText("gray", " --reasoning-effort Set reasoning effort level for custom models"));
|
|
3128
3489
|
console.log(styleText("gray", " --disable-telemetry Disable telemetry and Sentry error reporting"));
|
|
3490
|
+
console.log(styleText("gray", " --auto-high Set default autonomy mode to auto-high"));
|
|
3129
3491
|
console.log(styleText("gray", " --standalone Standalone mode: mock non-LLM Factory APIs"));
|
|
3130
3492
|
console.log();
|
|
3131
3493
|
console.log("Usage examples:");
|
|
@@ -3237,6 +3599,12 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3237
3599
|
replacement: Buffer.from("!0||this.webEvents.length")
|
|
3238
3600
|
});
|
|
3239
3601
|
}
|
|
3602
|
+
if (autoHigh) patches.push({
|
|
3603
|
+
name: "autoHighAutonomy",
|
|
3604
|
+
description: "Hardcode getCurrentAutonomyMode() to return \"auto-high\"",
|
|
3605
|
+
pattern: Buffer.from("getCurrentAutonomyMode(){return this.autonomyMode}"),
|
|
3606
|
+
replacement: Buffer.from("getCurrentAutonomyMode(){return\"auto-high\" }")
|
|
3607
|
+
});
|
|
3240
3608
|
try {
|
|
3241
3609
|
const result = await patchDroid({
|
|
3242
3610
|
inputPath: path,
|
|
@@ -3298,7 +3666,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3298
3666
|
sessions: !!sessions,
|
|
3299
3667
|
reasoningEffort: !!reasoningEffort,
|
|
3300
3668
|
noTelemetry: !!noTelemetry,
|
|
3301
|
-
standalone: !!standalone
|
|
3669
|
+
standalone: !!standalone,
|
|
3670
|
+
autoHigh: !!autoHigh
|
|
3302
3671
|
}, {
|
|
3303
3672
|
droidPatchVersion: version,
|
|
3304
3673
|
droidVersion,
|
|
@@ -3474,6 +3843,12 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3474
3843
|
replacement: Buffer.from("!0||this.webEvents.length")
|
|
3475
3844
|
});
|
|
3476
3845
|
}
|
|
3846
|
+
if (meta.patches.autoHigh) patches.push({
|
|
3847
|
+
name: "autoHighAutonomy",
|
|
3848
|
+
description: "Hardcode getCurrentAutonomyMode() to return \"auto-high\"",
|
|
3849
|
+
pattern: Buffer.from("getCurrentAutonomyMode(){return this.autonomyMode}"),
|
|
3850
|
+
replacement: Buffer.from("getCurrentAutonomyMode(){return\"auto-high\" }")
|
|
3851
|
+
});
|
|
3477
3852
|
const outputPath = join(join(homedir(), ".droid-patch", "bins"), `${meta.name}-patched`);
|
|
3478
3853
|
if (patches.length > 0) {
|
|
3479
3854
|
if (!(await patchDroid({
|