@evident-ai/cli 3.5.2-dev.c2c9f26 → 3.5.2-dev.df1ae49
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/index.js +881 -255
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire as createRequire2 } from "module";
|
|
5
4
|
import { Command } from "commander";
|
|
6
5
|
|
|
7
6
|
// src/commands/login.ts
|
|
@@ -1063,7 +1062,7 @@ async function claudeUsage() {
|
|
|
1063
1062
|
|
|
1064
1063
|
// src/commands/run.ts
|
|
1065
1064
|
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1066
|
-
import { homedir as
|
|
1065
|
+
import { homedir as homedir7 } from "node:os";
|
|
1067
1066
|
import { isAbsolute as isAbsolute3, join as join11, parse, resolve as resolvePath2 } from "node:path";
|
|
1068
1067
|
import chalk7 from "chalk";
|
|
1069
1068
|
|
|
@@ -1158,7 +1157,7 @@ import ora4 from "ora";
|
|
|
1158
1157
|
import { select as select4 } from "@inquirer/prompts";
|
|
1159
1158
|
|
|
1160
1159
|
// src/lib/telemetry.ts
|
|
1161
|
-
var CLI_VERSION = (true ? "3.5.2-dev.
|
|
1160
|
+
var CLI_VERSION = (true ? "3.5.2-dev.df1ae49" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1162
1161
|
function getCliVersion() {
|
|
1163
1162
|
return CLI_VERSION;
|
|
1164
1163
|
}
|
|
@@ -1172,6 +1171,23 @@ var authProvider = null;
|
|
|
1172
1171
|
function setTelemetryAuthProvider(provider) {
|
|
1173
1172
|
authProvider = provider;
|
|
1174
1173
|
}
|
|
1174
|
+
var diagnosticsReporter = null;
|
|
1175
|
+
function setTelemetryDiagnosticsReporter(reporter) {
|
|
1176
|
+
diagnosticsReporter = reporter;
|
|
1177
|
+
}
|
|
1178
|
+
function reportTelemetryDiagnostic(message) {
|
|
1179
|
+
if (!diagnosticsReporter) {
|
|
1180
|
+
console.error(message);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
diagnosticsReporter(message);
|
|
1185
|
+
} catch (error2) {
|
|
1186
|
+
console.error(
|
|
1187
|
+
`Telemetry diagnostics reporter failed: ${error2 instanceof Error ? error2.message : String(error2)}; original diagnostic: ${message}`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1175
1191
|
var FLUSH_FAILURE_LOG_INTERVAL_MS = 6e4;
|
|
1176
1192
|
var lastFlushFailureLoggedAt = 0;
|
|
1177
1193
|
var suppressedFlushFailureCount = 0;
|
|
@@ -1237,7 +1253,9 @@ async function flushEvents() {
|
|
|
1237
1253
|
signal: controller.signal
|
|
1238
1254
|
});
|
|
1239
1255
|
if (!response.ok) {
|
|
1240
|
-
|
|
1256
|
+
reportTelemetryDiagnostic(
|
|
1257
|
+
`Telemetry could not send events (HTTP ${response.status}); runner and OpenCode are unaffected`
|
|
1258
|
+
);
|
|
1241
1259
|
}
|
|
1242
1260
|
} finally {
|
|
1243
1261
|
clearTimeout(timeout);
|
|
@@ -1247,7 +1265,9 @@ async function flushEvents() {
|
|
|
1247
1265
|
if (now - lastFlushFailureLoggedAt >= FLUSH_FAILURE_LOG_INTERVAL_MS) {
|
|
1248
1266
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1249
1267
|
const suffix = suppressedFlushFailureCount > 0 ? ` (${suppressedFlushFailureCount} more suppressed in the last ${FLUSH_FAILURE_LOG_INTERVAL_MS / 1e3}s)` : "";
|
|
1250
|
-
|
|
1268
|
+
reportTelemetryDiagnostic(
|
|
1269
|
+
`Telemetry flush failed: ${message}; runner and OpenCode are unaffected${suffix}`
|
|
1270
|
+
);
|
|
1251
1271
|
lastFlushFailureLoggedAt = now;
|
|
1252
1272
|
suppressedFlushFailureCount = 0;
|
|
1253
1273
|
} else {
|
|
@@ -1299,8 +1319,227 @@ var EventTypes = {
|
|
|
1299
1319
|
};
|
|
1300
1320
|
|
|
1301
1321
|
// src/lib/runner-activity-telemetry.ts
|
|
1302
|
-
|
|
1322
|
+
import { homedir as homedir2 } from "node:os";
|
|
1323
|
+
|
|
1324
|
+
// src/lib/opencode/session-db-reclaim.ts
|
|
1325
|
+
import { statSync as statSync2, statfsSync } from "node:fs";
|
|
1326
|
+
import { dirname as dirname2 } from "node:path";
|
|
1327
|
+
var SESSION_DB_RECLAIM_CHUNK_PAGES = 2e3;
|
|
1328
|
+
var SESSION_DB_RECLAIM_TIME_BUDGET_MS = 500;
|
|
1329
|
+
var SESSION_DB_RECLAIM_PAGE_CEILING = 4e4;
|
|
1330
|
+
function errorMessage(error2) {
|
|
1331
|
+
if (!(error2 instanceof Error)) return String(error2);
|
|
1332
|
+
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
1333
|
+
}
|
|
1334
|
+
function isSessionStoreWriteFailure(error2) {
|
|
1335
|
+
const messages = [];
|
|
1336
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1337
|
+
let current = error2;
|
|
1338
|
+
while (current !== null && current !== void 0 && !visited.has(current)) {
|
|
1339
|
+
visited.add(current);
|
|
1340
|
+
if (typeof current === "string") {
|
|
1341
|
+
messages.push(current);
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
if (current instanceof Error) {
|
|
1345
|
+
messages.push(current.name, current.message);
|
|
1346
|
+
current = current.cause;
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
if (typeof current === "object") {
|
|
1350
|
+
const record = current;
|
|
1351
|
+
if (typeof record.code === "string") messages.push(record.code);
|
|
1352
|
+
if (typeof record.message === "string") messages.push(record.message);
|
|
1353
|
+
current = record.cause;
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
return messages.some(
|
|
1359
|
+
(message) => /(?:database or disk is full|disk is full|no space left on device|SQLITE_(?:FULL|IOERR)(?:\b|_))/i.test(
|
|
1360
|
+
message
|
|
1361
|
+
)
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
1365
|
+
try {
|
|
1366
|
+
const fsStats = statfsSync(dirname2(dbPath));
|
|
1367
|
+
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
1368
|
+
if (availableBytes < requiredBytes) {
|
|
1369
|
+
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
1370
|
+
}
|
|
1371
|
+
return null;
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function readLogicalBytes(db) {
|
|
1377
|
+
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
1378
|
+
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
1379
|
+
return pageCount * pageSize;
|
|
1380
|
+
}
|
|
1381
|
+
function readFreelistCount(db) {
|
|
1382
|
+
return db.prepare("PRAGMA freelist_count").get().freelist_count;
|
|
1383
|
+
}
|
|
1384
|
+
function readCheckpointResult(db) {
|
|
1385
|
+
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
1386
|
+
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
1387
|
+
}
|
|
1388
|
+
async function probeReclaimAvailability(input) {
|
|
1389
|
+
const { dbPath, requiredBytes } = input;
|
|
1390
|
+
let sqlite;
|
|
1391
|
+
try {
|
|
1392
|
+
sqlite = await import("node:sqlite");
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
1395
|
+
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
1396
|
+
return { reason: "sqlite-unavailable", detail };
|
|
1397
|
+
}
|
|
1398
|
+
let autoVacuum = null;
|
|
1399
|
+
try {
|
|
1400
|
+
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
1401
|
+
try {
|
|
1402
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
1403
|
+
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
1404
|
+
} finally {
|
|
1405
|
+
db.close();
|
|
1406
|
+
}
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
console.warn(
|
|
1409
|
+
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
if (autoVacuum !== 0) return null;
|
|
1413
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
1414
|
+
}
|
|
1415
|
+
async function reclaimSessionDbSpace(input) {
|
|
1416
|
+
const {
|
|
1417
|
+
dbPath,
|
|
1418
|
+
pageCeiling = SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
1419
|
+
timeBudgetMs = SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
1420
|
+
chunkPages = SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
1421
|
+
now = () => performance.now(),
|
|
1422
|
+
allowFullVacuum = true
|
|
1423
|
+
} = input;
|
|
1424
|
+
let sqlite;
|
|
1425
|
+
try {
|
|
1426
|
+
sqlite = await import("node:sqlite");
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
1429
|
+
console.warn(
|
|
1430
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
1431
|
+
);
|
|
1432
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
1433
|
+
}
|
|
1434
|
+
const { DatabaseSync } = sqlite;
|
|
1435
|
+
let db;
|
|
1436
|
+
try {
|
|
1437
|
+
db = new DatabaseSync(dbPath);
|
|
1438
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
1439
|
+
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
1440
|
+
if (autoVacuum === 0) {
|
|
1441
|
+
if (!allowFullVacuum) {
|
|
1442
|
+
console.warn(
|
|
1443
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
1444
|
+
);
|
|
1445
|
+
return {
|
|
1446
|
+
ok: false,
|
|
1447
|
+
skipped: "full-vacuum-blocked",
|
|
1448
|
+
detail: "a session turn is live"
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
const fileBytesForGuard = statSync2(dbPath).size;
|
|
1452
|
+
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
1453
|
+
if (skipReason !== null) {
|
|
1454
|
+
console.warn(
|
|
1455
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
1456
|
+
);
|
|
1457
|
+
return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
|
|
1458
|
+
}
|
|
1459
|
+
const beforeBytes = readLogicalBytes(db);
|
|
1460
|
+
const freelistBefore = readFreelistCount(db);
|
|
1461
|
+
const startedAt = now();
|
|
1462
|
+
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
1463
|
+
db.exec("VACUUM");
|
|
1464
|
+
const afterBytes = readLogicalBytes(db);
|
|
1465
|
+
const freelistAfter = readFreelistCount(db);
|
|
1466
|
+
const checkpoint = readCheckpointResult(db);
|
|
1467
|
+
return {
|
|
1468
|
+
ok: true,
|
|
1469
|
+
mode: "convert",
|
|
1470
|
+
beforeBytes,
|
|
1471
|
+
afterBytes,
|
|
1472
|
+
freelistBefore,
|
|
1473
|
+
freelistAfter,
|
|
1474
|
+
pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
|
|
1475
|
+
stoppedBy: "freelist-drained",
|
|
1476
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
1477
|
+
checkpoint
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
if (autoVacuum === 2) {
|
|
1481
|
+
const beforeBytes = readLogicalBytes(db);
|
|
1482
|
+
const freelistBefore = readFreelistCount(db);
|
|
1483
|
+
const startedAt = now();
|
|
1484
|
+
const pageLimit = Math.max(0, Math.trunc(pageCeiling));
|
|
1485
|
+
const chunkLimit = Math.max(1, Math.trunc(chunkPages));
|
|
1486
|
+
const budget = Math.max(0, timeBudgetMs);
|
|
1487
|
+
let pagesAttempted = 0;
|
|
1488
|
+
let freelistAfter = freelistBefore;
|
|
1489
|
+
let stoppedBy = "freelist-drained";
|
|
1490
|
+
while (freelistAfter > 0) {
|
|
1491
|
+
if (pagesAttempted >= pageLimit) {
|
|
1492
|
+
stoppedBy = "page-ceiling";
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
if (now() - startedAt >= budget) {
|
|
1496
|
+
stoppedBy = "time-budget";
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
const pages = Math.min(chunkLimit, pageLimit - pagesAttempted, freelistAfter);
|
|
1500
|
+
db.exec(`PRAGMA incremental_vacuum(${pages})`);
|
|
1501
|
+
pagesAttempted += pages;
|
|
1502
|
+
freelistAfter = readFreelistCount(db);
|
|
1503
|
+
}
|
|
1504
|
+
const afterBytes = readLogicalBytes(db);
|
|
1505
|
+
const checkpoint = readCheckpointResult(db);
|
|
1506
|
+
return {
|
|
1507
|
+
ok: true,
|
|
1508
|
+
mode: "incremental",
|
|
1509
|
+
beforeBytes,
|
|
1510
|
+
afterBytes,
|
|
1511
|
+
freelistBefore,
|
|
1512
|
+
freelistAfter,
|
|
1513
|
+
pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
|
|
1514
|
+
stoppedBy,
|
|
1515
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
1516
|
+
checkpoint
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
console.warn(
|
|
1520
|
+
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
1521
|
+
);
|
|
1522
|
+
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
1525
|
+
return {
|
|
1526
|
+
ok: false,
|
|
1527
|
+
skipped: isSessionStoreWriteFailure(err) ? "disk-exhaustion" : "reclaim-error",
|
|
1528
|
+
detail: errorMessage(err)
|
|
1529
|
+
};
|
|
1530
|
+
} finally {
|
|
1531
|
+
db?.close();
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// src/lib/runner-activity-telemetry.ts
|
|
1536
|
+
var FORWARDED_LEVELS_BY_SOURCE = {
|
|
1537
|
+
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
1538
|
+
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
1539
|
+
"cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"])
|
|
1540
|
+
};
|
|
1303
1541
|
var SEVERITY_BY_LEVEL = {
|
|
1542
|
+
info: "info",
|
|
1304
1543
|
warn: "warning",
|
|
1305
1544
|
error: "error"
|
|
1306
1545
|
};
|
|
@@ -1311,25 +1550,81 @@ function parseOpenCodeLogLine(line) {
|
|
|
1311
1550
|
const level = levelMatch[1].toUpperCase();
|
|
1312
1551
|
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1313
1552
|
const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
|
|
1314
|
-
|
|
1553
|
+
const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
|
|
1554
|
+
const parsed = {
|
|
1555
|
+
level: level === "WARN" ? "warn" : "error",
|
|
1556
|
+
sessionID: sessionMatch?.[1]
|
|
1557
|
+
};
|
|
1558
|
+
const message = messageMatch?.[1] ?? messageMatch?.[2];
|
|
1559
|
+
if (message !== void 0) parsed.message = message;
|
|
1560
|
+
return parsed;
|
|
1315
1561
|
}
|
|
1316
1562
|
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1317
|
-
|
|
1563
|
+
var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
|
|
1564
|
+
function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
|
|
1318
1565
|
let buffer = Buffer.alloc(0);
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1566
|
+
const repeatStates = /* @__PURE__ */ new Map();
|
|
1567
|
+
const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
|
|
1568
|
+
const metadata = {
|
|
1569
|
+
...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
|
|
1570
|
+
...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
|
|
1571
|
+
};
|
|
1322
1572
|
forwardRunnerActivity(
|
|
1323
1573
|
{
|
|
1324
1574
|
level: parsed.level,
|
|
1325
1575
|
error: line,
|
|
1326
|
-
metadata:
|
|
1576
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
|
|
1327
1577
|
source: "opencode"
|
|
1328
1578
|
},
|
|
1329
1579
|
getContext()
|
|
1330
1580
|
);
|
|
1331
1581
|
};
|
|
1332
|
-
|
|
1582
|
+
const expireRepeatStates = (now, currentKey) => {
|
|
1583
|
+
for (const [key, state] of repeatStates) {
|
|
1584
|
+
if (key === currentKey || now - state.lastForwardedAt < REPEAT_SUPPRESSION_WINDOW_MS) {
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
if (state.repeatsSuppressed > 0) {
|
|
1588
|
+
forwardLine(state.line, state.parsed, state.repeatsSuppressed);
|
|
1589
|
+
}
|
|
1590
|
+
repeatStates.delete(key);
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
const flushLine = (line) => {
|
|
1594
|
+
if (onSessionStoreWriteFailure && isSessionStoreWriteFailure(line)) {
|
|
1595
|
+
try {
|
|
1596
|
+
onSessionStoreWriteFailure();
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
reportTelemetryDiagnostic(
|
|
1599
|
+
`Runner activity telemetry could not record session-store write failure: ${err instanceof Error ? err.message : String(err)}`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const parsed = parseOpenCodeLogLine(line);
|
|
1604
|
+
if (!parsed) return;
|
|
1605
|
+
const now = Date.now();
|
|
1606
|
+
const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}`;
|
|
1607
|
+
expireRepeatStates(now, key);
|
|
1608
|
+
if (key === void 0) {
|
|
1609
|
+
forwardLine(line, parsed);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const previous = repeatStates.get(key);
|
|
1613
|
+
if (!previous || now - previous.lastForwardedAt >= REPEAT_SUPPRESSION_WINDOW_MS) {
|
|
1614
|
+
forwardLine(line, parsed, previous?.repeatsSuppressed ?? 0);
|
|
1615
|
+
repeatStates.set(key, {
|
|
1616
|
+
line,
|
|
1617
|
+
parsed,
|
|
1618
|
+
lastForwardedAt: now,
|
|
1619
|
+
repeatsSuppressed: 0
|
|
1620
|
+
});
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
previous.line = line;
|
|
1624
|
+
previous.parsed = parsed;
|
|
1625
|
+
previous.repeatsSuppressed++;
|
|
1626
|
+
};
|
|
1627
|
+
const forward = ((chunk) => {
|
|
1333
1628
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
|
|
1334
1629
|
let newlineIndex;
|
|
1335
1630
|
while ((newlineIndex = buffer.indexOf(10)) !== -1) {
|
|
@@ -1340,14 +1635,44 @@ function createOpenCodeActivityForwarder(getContext) {
|
|
|
1340
1635
|
flushLine(buffer.toString("utf-8"));
|
|
1341
1636
|
buffer = Buffer.alloc(0);
|
|
1342
1637
|
}
|
|
1638
|
+
});
|
|
1639
|
+
forward.flush = () => {
|
|
1640
|
+
for (const state of repeatStates.values()) {
|
|
1641
|
+
if (state.repeatsSuppressed > 0) {
|
|
1642
|
+
forwardLine(state.line, state.parsed, state.repeatsSuppressed);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
repeatStates.clear();
|
|
1343
1646
|
};
|
|
1647
|
+
return forward;
|
|
1344
1648
|
}
|
|
1345
1649
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1346
1650
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1347
1651
|
var MAX_METADATA_ENTRIES = 20;
|
|
1348
1652
|
var TRUNCATION_MARKER = "\u2026";
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1653
|
+
function redactHomeDirectory(message) {
|
|
1654
|
+
const home = homedir2();
|
|
1655
|
+
if (!home || home === "/") {
|
|
1656
|
+
return message;
|
|
1657
|
+
}
|
|
1658
|
+
const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
|
|
1659
|
+
let result = "";
|
|
1660
|
+
let searchFrom = 0;
|
|
1661
|
+
let homeIndex;
|
|
1662
|
+
while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
|
|
1663
|
+
const homeEnd = homeIndex + home.length;
|
|
1664
|
+
const previousCharacter = message[homeIndex - 1];
|
|
1665
|
+
const nextCharacter = message[homeEnd];
|
|
1666
|
+
const atPathBoundary = home === "/" || (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
|
|
1667
|
+
result += message.slice(searchFrom, homeIndex);
|
|
1668
|
+
result += atPathBoundary ? "~" : home;
|
|
1669
|
+
searchFrom = homeEnd;
|
|
1670
|
+
}
|
|
1671
|
+
return result + message.slice(searchFrom);
|
|
1672
|
+
}
|
|
1673
|
+
function redactDiagnosticText(message) {
|
|
1674
|
+
const redacted = message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-ant-***").replace(/(?<![A-Za-z0-9_-])sk-proj-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-proj-***").replace(/(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "sk-***").replace(/(?<![A-Za-z0-9_-])(gh[oprsu])_[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "$1_***").replace(/(?<![A-Za-z0-9_-])github_pat_[A-Za-z0-9_]{20,}(?![A-Za-z0-9_-])/g, "github_pat_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
1675
|
+
return redactHomeDirectory(redacted);
|
|
1351
1676
|
}
|
|
1352
1677
|
function truncate(message) {
|
|
1353
1678
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1357,14 +1682,17 @@ function sanitiseMetadata(metadata) {
|
|
|
1357
1682
|
if (!metadata) return {};
|
|
1358
1683
|
const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
|
|
1359
1684
|
if (Object.keys(metadata).length > entries.length) {
|
|
1360
|
-
|
|
1361
|
-
`
|
|
1685
|
+
reportTelemetryDiagnostic(
|
|
1686
|
+
`Runner activity telemetry dropped ${Object.keys(metadata).length - entries.length} metadata entries; those entries were not sent`
|
|
1362
1687
|
);
|
|
1363
1688
|
}
|
|
1364
1689
|
const sanitised = [];
|
|
1365
1690
|
for (const [key, value] of entries) {
|
|
1366
1691
|
if (typeof value === "string") {
|
|
1367
|
-
sanitised.push([
|
|
1692
|
+
sanitised.push([
|
|
1693
|
+
key,
|
|
1694
|
+
truncate(redactDiagnosticText(value).slice(0, MAX_METADATA_VALUE_LENGTH))
|
|
1695
|
+
]);
|
|
1368
1696
|
} else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
1369
1697
|
sanitised.push([key, value]);
|
|
1370
1698
|
}
|
|
@@ -1382,8 +1710,8 @@ function admitUnderRateLimit(source, now) {
|
|
|
1382
1710
|
}
|
|
1383
1711
|
if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
|
|
1384
1712
|
if (window.windowDroppedCount > 0) {
|
|
1385
|
-
|
|
1386
|
-
`
|
|
1713
|
+
reportTelemetryDiagnostic(
|
|
1714
|
+
`Runner activity telemetry rate cap reached: dropped ${window.windowDroppedCount} ${window.windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (limit ${RATE_LIMIT_MAX_EVENTS}/min); those entries were not sent for source "${source}"`
|
|
1387
1715
|
);
|
|
1388
1716
|
}
|
|
1389
1717
|
window.windowStartedAt = now;
|
|
@@ -1393,8 +1721,8 @@ function admitUnderRateLimit(source, now) {
|
|
|
1393
1721
|
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1394
1722
|
window.windowDroppedCount++;
|
|
1395
1723
|
if (window.windowDroppedCount === 1) {
|
|
1396
|
-
|
|
1397
|
-
`
|
|
1724
|
+
reportTelemetryDiagnostic(
|
|
1725
|
+
`Runner activity telemetry rate cap reached (${RATE_LIMIT_MAX_EVENTS} entries per ${RATE_LIMIT_WINDOW_MS / 1e3}s limit; further entries were not sent for source "${source}"`
|
|
1398
1726
|
);
|
|
1399
1727
|
}
|
|
1400
1728
|
return false;
|
|
@@ -1404,12 +1732,12 @@ function admitUnderRateLimit(source, now) {
|
|
|
1404
1732
|
}
|
|
1405
1733
|
function forwardRunnerActivity(entry, context) {
|
|
1406
1734
|
try {
|
|
1407
|
-
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1408
|
-
if (!context.agentId || !context.authHeader) return;
|
|
1409
1735
|
const source = entry.source ?? "cli.run";
|
|
1736
|
+
if (!FORWARDED_LEVELS_BY_SOURCE[source].has(entry.level)) return;
|
|
1737
|
+
if (!context.agentId || !context.authHeader) return;
|
|
1410
1738
|
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1411
1739
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1412
|
-
const message = truncate(
|
|
1740
|
+
const message = truncate(redactDiagnosticText(rawMessage));
|
|
1413
1741
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1414
1742
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1415
1743
|
message,
|
|
@@ -1417,8 +1745,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1417
1745
|
agentId: context.agentId
|
|
1418
1746
|
});
|
|
1419
1747
|
} catch (err) {
|
|
1420
|
-
|
|
1421
|
-
`
|
|
1748
|
+
reportTelemetryDiagnostic(
|
|
1749
|
+
`Runner activity telemetry failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
|
|
1422
1750
|
);
|
|
1423
1751
|
}
|
|
1424
1752
|
}
|
|
@@ -1708,8 +2036,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
|
1708
2036
|
|
|
1709
2037
|
// src/lib/http-timeout.ts
|
|
1710
2038
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
2039
|
+
var RequestTimeoutError = class extends Error {
|
|
2040
|
+
constructor(operation, timeoutMs, elapsedMs) {
|
|
2041
|
+
super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
|
|
2042
|
+
this.operation = operation;
|
|
2043
|
+
this.timeoutMs = timeoutMs;
|
|
2044
|
+
this.elapsedMs = elapsedMs;
|
|
2045
|
+
this.name = "TimeoutError";
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
function describeRequestOperation(input, init) {
|
|
2049
|
+
const method = (init?.method ?? input.method ?? "GET").toUpperCase();
|
|
2050
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
2051
|
+
let path;
|
|
2052
|
+
try {
|
|
2053
|
+
path = new URL(rawUrl).pathname;
|
|
2054
|
+
} catch (error2) {
|
|
2055
|
+
if (!(error2 instanceof TypeError)) throw error2;
|
|
2056
|
+
path = rawUrl.split(/[?#]/, 1)[0];
|
|
2057
|
+
}
|
|
2058
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2059
|
+
const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
|
|
2060
|
+
const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
|
|
2061
|
+
return `${method} ${maskedPath}`;
|
|
2062
|
+
}
|
|
1711
2063
|
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1712
|
-
return ((input, init) =>
|
|
2064
|
+
return (async (input, init) => {
|
|
2065
|
+
const startedAt = Date.now();
|
|
2066
|
+
try {
|
|
2067
|
+
return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
2068
|
+
} catch (error2) {
|
|
2069
|
+
if (error2?.name === "TimeoutError") {
|
|
2070
|
+
throw new RequestTimeoutError(
|
|
2071
|
+
describeRequestOperation(input, init),
|
|
2072
|
+
timeoutMs,
|
|
2073
|
+
Date.now() - startedAt
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
throw error2;
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
1713
2079
|
}
|
|
1714
2080
|
|
|
1715
2081
|
// src/lib/opencode/client.ts
|
|
@@ -1863,6 +2229,7 @@ function isSessionActivelyGenerating(messages) {
|
|
|
1863
2229
|
if (roleOf(last) !== "assistant") return false;
|
|
1864
2230
|
return completedOf(last) == null;
|
|
1865
2231
|
}
|
|
2232
|
+
var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
|
|
1866
2233
|
function sessionLastActivityMs(session) {
|
|
1867
2234
|
const candidates = [
|
|
1868
2235
|
session.time?.updated,
|
|
@@ -1877,30 +2244,60 @@ function sessionLastActivityMs(session) {
|
|
|
1877
2244
|
}
|
|
1878
2245
|
return null;
|
|
1879
2246
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
2247
|
+
function sessionListPath(options) {
|
|
2248
|
+
const params = new URLSearchParams();
|
|
2249
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
2250
|
+
const query = params.toString();
|
|
2251
|
+
return query ? `/session?${query}` : "/session";
|
|
2252
|
+
}
|
|
2253
|
+
async function listSessionPage(port, client, options) {
|
|
2254
|
+
const path = sessionListPath(options);
|
|
1882
2255
|
try {
|
|
1883
|
-
const res = await requestWithClient(port, client,
|
|
2256
|
+
const res = await requestWithClient(port, client, path);
|
|
1884
2257
|
if (!res.ok) return null;
|
|
1885
2258
|
const body = await res.json();
|
|
1886
2259
|
return Array.isArray(body) ? body : null;
|
|
1887
2260
|
} catch (error2) {
|
|
1888
2261
|
console.error(
|
|
1889
|
-
`[
|
|
2262
|
+
`[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1890
2263
|
);
|
|
1891
2264
|
return null;
|
|
1892
2265
|
}
|
|
1893
2266
|
}
|
|
2267
|
+
async function listSessions(port, client, options) {
|
|
2268
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2269
|
+
return listSessionPage(port, client, options);
|
|
2270
|
+
}
|
|
2271
|
+
async function listAllSessions(port, client) {
|
|
2272
|
+
if (client?.version === "v2") {
|
|
2273
|
+
const sessions2 = await listV2Sessions(client);
|
|
2274
|
+
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
2275
|
+
}
|
|
2276
|
+
const sessions = await listSessionPage(port, client, {
|
|
2277
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
2278
|
+
});
|
|
2279
|
+
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
2280
|
+
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
2281
|
+
return { sessions: null, reason: "session-enumeration-limit-bound" };
|
|
2282
|
+
}
|
|
2283
|
+
return { sessions };
|
|
2284
|
+
}
|
|
1894
2285
|
async function deleteSession(port, id, client) {
|
|
1895
2286
|
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
1896
2287
|
try {
|
|
1897
|
-
const res = await requestWithClient(
|
|
1898
|
-
|
|
2288
|
+
const res = await requestWithClient(
|
|
2289
|
+
port,
|
|
2290
|
+
client,
|
|
2291
|
+
`/session/${id}`,
|
|
2292
|
+
{ method: "DELETE" },
|
|
2293
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
2294
|
+
);
|
|
2295
|
+
return { ok: res.status >= 200 && res.status < 300, status: res.status };
|
|
1899
2296
|
} catch (error2) {
|
|
1900
2297
|
console.error(
|
|
1901
2298
|
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1902
2299
|
);
|
|
1903
|
-
return false;
|
|
2300
|
+
return { ok: false, status: "threw" };
|
|
1904
2301
|
}
|
|
1905
2302
|
}
|
|
1906
2303
|
async function sessionExists(port, id, client) {
|
|
@@ -3033,13 +3430,17 @@ async function listV2Sessions(client) {
|
|
|
3033
3430
|
}
|
|
3034
3431
|
async function deleteV2Session(client, sessionId) {
|
|
3035
3432
|
try {
|
|
3036
|
-
await client.request(
|
|
3037
|
-
|
|
3433
|
+
const response = await client.request(
|
|
3434
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
3435
|
+
{ method: "DELETE" },
|
|
3436
|
+
{ allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
|
|
3437
|
+
);
|
|
3438
|
+
return { ok: response.status >= 200 && response.status < 300, status: response.status };
|
|
3038
3439
|
} catch (error2) {
|
|
3039
3440
|
console.error(
|
|
3040
3441
|
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3041
3442
|
);
|
|
3042
|
-
return false;
|
|
3443
|
+
return { ok: false, status: "threw" };
|
|
3043
3444
|
}
|
|
3044
3445
|
}
|
|
3045
3446
|
async function v2SessionExists(client, sessionId) {
|
|
@@ -3214,7 +3615,7 @@ import { join as join4 } from "node:path";
|
|
|
3214
3615
|
|
|
3215
3616
|
// src/lib/openai-usage.ts
|
|
3216
3617
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3217
|
-
import { homedir as
|
|
3618
|
+
import { homedir as homedir3 } from "node:os";
|
|
3218
3619
|
import { join as join3 } from "node:path";
|
|
3219
3620
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3220
3621
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
@@ -3229,7 +3630,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
3229
3630
|
}
|
|
3230
3631
|
function readOpenCodeChatGptCredentials() {
|
|
3231
3632
|
try {
|
|
3232
|
-
const raw = readFileSync3(join3(
|
|
3633
|
+
const raw = readFileSync3(join3(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3233
3634
|
let parsed;
|
|
3234
3635
|
try {
|
|
3235
3636
|
parsed = JSON.parse(raw);
|
|
@@ -3370,16 +3771,17 @@ function hasPrimaryHeaders(headers) {
|
|
|
3370
3771
|
].some((name) => headers.has(name));
|
|
3371
3772
|
}
|
|
3372
3773
|
async function getOpenAiUsage(port, client) {
|
|
3774
|
+
const openCodeVersion = client?.version ?? "v1";
|
|
3373
3775
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
3374
3776
|
if (!credentials2) {
|
|
3375
3777
|
throw new OpenAiUsageError(
|
|
3376
|
-
"No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3778
|
+
openCodeVersion === "v2" ? "OpenAI usage reporting is not available on OpenCode V2 because V2 keeps a connected ChatGPT login in a store Evident cannot read." : "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3377
3779
|
"no_credentials"
|
|
3378
3780
|
);
|
|
3379
3781
|
}
|
|
3380
3782
|
if (credentials2.expiresAt < Date.now()) {
|
|
3381
3783
|
throw new OpenAiUsageError(
|
|
3382
|
-
"ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3784
|
+
openCodeVersion === "v2" ? "The delivered ChatGPT login has expired. OpenCode V2 cannot be given a replacement by file; connect the account again from the runner's page in Evident." : "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3383
3785
|
"credentials_expired"
|
|
3384
3786
|
);
|
|
3385
3787
|
}
|
|
@@ -3660,9 +4062,9 @@ async function importOpenCodeV2Credentials({
|
|
|
3660
4062
|
|
|
3661
4063
|
// src/lib/opencode/session-db-boot.ts
|
|
3662
4064
|
import { spawn as spawn2 } from "node:child_process";
|
|
3663
|
-
import { mkdirSync, statSync as
|
|
3664
|
-
import { homedir as
|
|
3665
|
-
import { dirname as
|
|
4065
|
+
import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
4066
|
+
import { homedir as homedir4 } from "node:os";
|
|
4067
|
+
import { dirname as dirname3, resolve as resolvePath } from "node:path";
|
|
3666
4068
|
|
|
3667
4069
|
// src/lib/runner-synchroniser.ts
|
|
3668
4070
|
import { spawn } from "node:child_process";
|
|
@@ -3787,7 +4189,7 @@ function clearMarker(options) {
|
|
|
3787
4189
|
function markNoReplicate(options, message) {
|
|
3788
4190
|
if (options.noReplicateMarker) {
|
|
3789
4191
|
try {
|
|
3790
|
-
mkdirSync(
|
|
4192
|
+
mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
|
|
3791
4193
|
writeFileSync(options.noReplicateMarker, "");
|
|
3792
4194
|
} catch (error2) {
|
|
3793
4195
|
options.log(
|
|
@@ -3912,7 +4314,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
3912
4314
|
return null;
|
|
3913
4315
|
}
|
|
3914
4316
|
try {
|
|
3915
|
-
if (
|
|
4317
|
+
if (statSync3(configPath).size > 0) return configPath;
|
|
3916
4318
|
} catch (error2) {
|
|
3917
4319
|
if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
|
|
3918
4320
|
options.log(
|
|
@@ -3942,7 +4344,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
3942
4344
|
return null;
|
|
3943
4345
|
}
|
|
3944
4346
|
try {
|
|
3945
|
-
mkdirSync(
|
|
4347
|
+
mkdirSync(dirname3(configPath), { recursive: true });
|
|
3946
4348
|
writeFileSync(configPath, rendered.stdout);
|
|
3947
4349
|
} catch (error2) {
|
|
3948
4350
|
options.log(
|
|
@@ -4017,14 +4419,14 @@ async function restoreSessionDb(options, configPath, env) {
|
|
|
4017
4419
|
case 0:
|
|
4018
4420
|
return;
|
|
4019
4421
|
case 31:
|
|
4020
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4422
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4021
4423
|
options.log(
|
|
4022
4424
|
"SESSION-DB-REPLICA-UNUSABLE: booting with a fresh opencode.db and replicating into the existing prefix this boot",
|
|
4023
4425
|
"warn"
|
|
4024
4426
|
);
|
|
4025
4427
|
return;
|
|
4026
4428
|
case 32:
|
|
4027
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4429
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4028
4430
|
discardSessionDbDebris(options);
|
|
4029
4431
|
markNoReplicate(
|
|
4030
4432
|
options,
|
|
@@ -4103,7 +4505,7 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4103
4505
|
}
|
|
4104
4506
|
function fileExists(path) {
|
|
4105
4507
|
try {
|
|
4106
|
-
|
|
4508
|
+
statSync3(path);
|
|
4107
4509
|
return true;
|
|
4108
4510
|
} catch (error2) {
|
|
4109
4511
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
|
|
@@ -4113,7 +4515,7 @@ function fileExists(path) {
|
|
|
4113
4515
|
async function restoreAndVerifySessionDb(options) {
|
|
4114
4516
|
const env = options.env ?? process.env;
|
|
4115
4517
|
clearMarker(options);
|
|
4116
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4518
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4117
4519
|
const synchroniserEnv = await runSynchroniser(["env"], {
|
|
4118
4520
|
timeoutMs: SESSION_DB_SYNCHRONISER_TIMEOUT_MS,
|
|
4119
4521
|
env
|
|
@@ -4192,7 +4594,7 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
4192
4594
|
// src/lib/opencode/session-db-provenance.ts
|
|
4193
4595
|
import { createRequire } from "node:module";
|
|
4194
4596
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4195
|
-
import { dirname as
|
|
4597
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
4196
4598
|
var require2 = createRequire(import.meta.url);
|
|
4197
4599
|
function readSessionDbMigrationIds(dbPath) {
|
|
4198
4600
|
let db;
|
|
@@ -4258,7 +4660,7 @@ function loadSessionDbProvenanceState(path) {
|
|
|
4258
4660
|
}
|
|
4259
4661
|
function saveSessionDbProvenanceState(path, state) {
|
|
4260
4662
|
try {
|
|
4261
|
-
mkdirSync2(
|
|
4663
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
4262
4664
|
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
4263
4665
|
`, "utf8");
|
|
4264
4666
|
} catch (error2) {
|
|
@@ -4839,7 +5241,10 @@ function selectSessionsToDelete(sessions, opts) {
|
|
|
4839
5241
|
toDelete.push(s.id);
|
|
4840
5242
|
}
|
|
4841
5243
|
}
|
|
4842
|
-
|
|
5244
|
+
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
5245
|
+
return toDelete.sort(
|
|
5246
|
+
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
5247
|
+
);
|
|
4843
5248
|
}
|
|
4844
5249
|
var DEFAULT_INTERVAL = "1h";
|
|
4845
5250
|
function resolve(flag, envValue, fallback) {
|
|
@@ -4899,13 +5304,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
4899
5304
|
}
|
|
4900
5305
|
|
|
4901
5306
|
// src/lib/opencode/session-db-size.ts
|
|
4902
|
-
import { statSync as
|
|
5307
|
+
import { statSync as statSync4 } from "node:fs";
|
|
4903
5308
|
import { join as join6 } from "node:path";
|
|
4904
5309
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
4905
5310
|
function statSessionDbBytes(homeDir) {
|
|
4906
5311
|
const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
4907
5312
|
try {
|
|
4908
|
-
return
|
|
5313
|
+
return statSync4(dbPath).size;
|
|
4909
5314
|
} catch (err) {
|
|
4910
5315
|
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
4911
5316
|
if (!isMissingFile) {
|
|
@@ -4931,12 +5336,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
4931
5336
|
}
|
|
4932
5337
|
|
|
4933
5338
|
// src/lib/opencode/log-tail.ts
|
|
4934
|
-
import { statSync as
|
|
4935
|
-
import { homedir as
|
|
5339
|
+
import { statSync as statSync5 } from "node:fs";
|
|
5340
|
+
import { homedir as homedir5 } from "node:os";
|
|
4936
5341
|
import { join as join7 } from "node:path";
|
|
4937
5342
|
import { open as open2, stat } from "node:fs/promises";
|
|
4938
5343
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
4939
|
-
function resolveOpenCodeLogPath(homeDir =
|
|
5344
|
+
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
4940
5345
|
const dataDir = env.XDG_DATA_HOME || join7(homeDir, ".local", "share");
|
|
4941
5346
|
return join7(dataDir, "opencode", "log", "opencode.log");
|
|
4942
5347
|
}
|
|
@@ -4954,7 +5359,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
4954
5359
|
let inode = null;
|
|
4955
5360
|
let baselineReady = true;
|
|
4956
5361
|
try {
|
|
4957
|
-
const initial =
|
|
5362
|
+
const initial = statSync5(logPath);
|
|
4958
5363
|
offset = initial.size;
|
|
4959
5364
|
inode = initial.ino;
|
|
4960
5365
|
} catch (error2) {
|
|
@@ -5013,125 +5418,6 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
5013
5418
|
};
|
|
5014
5419
|
}
|
|
5015
5420
|
|
|
5016
|
-
// src/lib/opencode/session-db-reclaim.ts
|
|
5017
|
-
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
5018
|
-
import { dirname as dirname4 } from "node:path";
|
|
5019
|
-
function errorMessage(error2) {
|
|
5020
|
-
if (!(error2 instanceof Error)) return String(error2);
|
|
5021
|
-
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
5022
|
-
}
|
|
5023
|
-
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
5024
|
-
try {
|
|
5025
|
-
const fsStats = statfsSync(dirname4(dbPath));
|
|
5026
|
-
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
5027
|
-
if (availableBytes < requiredBytes) {
|
|
5028
|
-
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
5029
|
-
}
|
|
5030
|
-
return null;
|
|
5031
|
-
} catch (err) {
|
|
5032
|
-
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
5033
|
-
}
|
|
5034
|
-
}
|
|
5035
|
-
function readLogicalBytes(db) {
|
|
5036
|
-
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
5037
|
-
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
5038
|
-
return pageCount * pageSize;
|
|
5039
|
-
}
|
|
5040
|
-
function readCheckpointResult(db) {
|
|
5041
|
-
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
5042
|
-
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
5043
|
-
}
|
|
5044
|
-
async function probeReclaimAvailability(input) {
|
|
5045
|
-
const { dbPath, requiredBytes } = input;
|
|
5046
|
-
let sqlite;
|
|
5047
|
-
try {
|
|
5048
|
-
sqlite = await import("node:sqlite");
|
|
5049
|
-
} catch (err) {
|
|
5050
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5051
|
-
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
5052
|
-
return { reason: "sqlite-unavailable", detail };
|
|
5053
|
-
}
|
|
5054
|
-
let autoVacuum = null;
|
|
5055
|
-
try {
|
|
5056
|
-
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
5057
|
-
try {
|
|
5058
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5059
|
-
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5060
|
-
} finally {
|
|
5061
|
-
db.close();
|
|
5062
|
-
}
|
|
5063
|
-
} catch (err) {
|
|
5064
|
-
console.warn(
|
|
5065
|
-
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
5066
|
-
);
|
|
5067
|
-
}
|
|
5068
|
-
if (autoVacuum !== 0) return null;
|
|
5069
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
5070
|
-
}
|
|
5071
|
-
async function reclaimSessionDbSpace(input) {
|
|
5072
|
-
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
5073
|
-
let sqlite;
|
|
5074
|
-
try {
|
|
5075
|
-
sqlite = await import("node:sqlite");
|
|
5076
|
-
} catch (err) {
|
|
5077
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5078
|
-
console.warn(
|
|
5079
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
5080
|
-
);
|
|
5081
|
-
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
5082
|
-
}
|
|
5083
|
-
const { DatabaseSync } = sqlite;
|
|
5084
|
-
let db;
|
|
5085
|
-
try {
|
|
5086
|
-
db = new DatabaseSync(dbPath);
|
|
5087
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5088
|
-
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5089
|
-
if (autoVacuum === 0) {
|
|
5090
|
-
if (!allowFullVacuum) {
|
|
5091
|
-
console.warn(
|
|
5092
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
5093
|
-
);
|
|
5094
|
-
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
5095
|
-
}
|
|
5096
|
-
const fileBytesForGuard = statSync5(dbPath).size;
|
|
5097
|
-
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
5098
|
-
if (skipReason !== null) {
|
|
5099
|
-
console.warn(
|
|
5100
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
5101
|
-
);
|
|
5102
|
-
return { ok: false, skipped: "insufficient-disk-space" };
|
|
5103
|
-
}
|
|
5104
|
-
const beforeBytes = readLogicalBytes(db);
|
|
5105
|
-
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
5106
|
-
db.exec("VACUUM");
|
|
5107
|
-
const afterBytes = readLogicalBytes(db);
|
|
5108
|
-
const checkpoint = readCheckpointResult(db);
|
|
5109
|
-
return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
|
|
5110
|
-
}
|
|
5111
|
-
if (autoVacuum === 2) {
|
|
5112
|
-
const beforeBytes = readLogicalBytes(db);
|
|
5113
|
-
const bound = Math.max(0, Math.trunc(maxPages));
|
|
5114
|
-
db.exec(`PRAGMA incremental_vacuum(${bound})`);
|
|
5115
|
-
const afterBytes = readLogicalBytes(db);
|
|
5116
|
-
const checkpoint = readCheckpointResult(db);
|
|
5117
|
-
return { ok: true, mode: "incremental", beforeBytes, afterBytes, checkpoint };
|
|
5118
|
-
}
|
|
5119
|
-
console.warn(
|
|
5120
|
-
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
5121
|
-
);
|
|
5122
|
-
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
5123
|
-
} catch (err) {
|
|
5124
|
-
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
5125
|
-
return {
|
|
5126
|
-
ok: false,
|
|
5127
|
-
skipped: "reclaim-error",
|
|
5128
|
-
detail: errorMessage(err)
|
|
5129
|
-
};
|
|
5130
|
-
} finally {
|
|
5131
|
-
db?.close();
|
|
5132
|
-
}
|
|
5133
|
-
}
|
|
5134
|
-
|
|
5135
5421
|
// src/lib/tunnel/connection.ts
|
|
5136
5422
|
import WebSocket2 from "ws";
|
|
5137
5423
|
|
|
@@ -5361,6 +5647,8 @@ var StreamForwarder = class {
|
|
|
5361
5647
|
if (done) break;
|
|
5362
5648
|
const chunk = Buffer.from(value);
|
|
5363
5649
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
5650
|
+
await stream.pausePromise;
|
|
5651
|
+
if (ac.signal.aborted) break;
|
|
5364
5652
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
5365
5653
|
if (this.binaryFramesSupported) {
|
|
5366
5654
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
@@ -5403,6 +5691,7 @@ function classifyUpgradeRejection(headers) {
|
|
|
5403
5691
|
}
|
|
5404
5692
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
5405
5693
|
var BASE_RECONNECT_DELAY = 500;
|
|
5694
|
+
var RELAY_SILENCE_TIMEOUT_MS = 9e4;
|
|
5406
5695
|
function getReconnectDelay(attempt) {
|
|
5407
5696
|
const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
|
|
5408
5697
|
const jitter = Math.random() * 1e3;
|
|
@@ -5474,10 +5763,27 @@ function connectTunnel(options) {
|
|
|
5474
5763
|
ws.close();
|
|
5475
5764
|
reject(new Error("Connection timeout"));
|
|
5476
5765
|
}, 3e4);
|
|
5766
|
+
let relaySilenceTimeout;
|
|
5767
|
+
let relaySilenceTimedOut = false;
|
|
5768
|
+
const clearRelaySilenceTimeout = () => {
|
|
5769
|
+
if (relaySilenceTimeout === void 0) return;
|
|
5770
|
+
clearTimeout(relaySilenceTimeout);
|
|
5771
|
+
relaySilenceTimeout = void 0;
|
|
5772
|
+
};
|
|
5773
|
+
const armRelaySilenceTimeout = () => {
|
|
5774
|
+
clearRelaySilenceTimeout();
|
|
5775
|
+
relaySilenceTimeout = setTimeout(() => {
|
|
5776
|
+
relaySilenceTimeout = void 0;
|
|
5777
|
+
relaySilenceTimedOut = true;
|
|
5778
|
+
ws.terminate();
|
|
5779
|
+
}, RELAY_SILENCE_TIMEOUT_MS);
|
|
5780
|
+
relaySilenceTimeout.unref?.();
|
|
5781
|
+
};
|
|
5477
5782
|
let upgradeRejection = null;
|
|
5478
5783
|
let upgradeRejectionReason = null;
|
|
5479
5784
|
ws.on("unexpected-response", (_req, res) => {
|
|
5480
5785
|
clearTimeout(connectionTimeout);
|
|
5786
|
+
clearRelaySilenceTimeout();
|
|
5481
5787
|
const reason = classifyUpgradeRejection(res.headers);
|
|
5482
5788
|
upgradeRejectionReason = reason;
|
|
5483
5789
|
const chunks = [];
|
|
@@ -5507,6 +5813,7 @@ function connectTunnel(options) {
|
|
|
5507
5813
|
onInfo?.("WebSocket connection established");
|
|
5508
5814
|
});
|
|
5509
5815
|
ws.on("message", (data, isBinary) => {
|
|
5816
|
+
if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
|
|
5510
5817
|
if (isBinary) {
|
|
5511
5818
|
try {
|
|
5512
5819
|
const frame = decodeBinaryBodyFrame(toUint8Array(data));
|
|
@@ -5540,6 +5847,7 @@ function connectTunnel(options) {
|
|
|
5540
5847
|
switch (message.type) {
|
|
5541
5848
|
case "connected": {
|
|
5542
5849
|
clearTimeout(connectionTimeout);
|
|
5850
|
+
armRelaySilenceTimeout();
|
|
5543
5851
|
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
5544
5852
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
5545
5853
|
onConnected?.(connectedAgentId);
|
|
@@ -5564,6 +5872,7 @@ function connectTunnel(options) {
|
|
|
5564
5872
|
});
|
|
5565
5873
|
ws.on("error", (error2) => {
|
|
5566
5874
|
clearTimeout(connectionTimeout);
|
|
5875
|
+
clearRelaySilenceTimeout();
|
|
5567
5876
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
5568
5877
|
if (upgradeRejectionReason === "do_code_updated") {
|
|
5569
5878
|
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
@@ -5575,7 +5884,8 @@ function connectTunnel(options) {
|
|
|
5575
5884
|
);
|
|
5576
5885
|
});
|
|
5577
5886
|
ws.on("close", (code, reason) => {
|
|
5578
|
-
|
|
5887
|
+
clearRelaySilenceTimeout();
|
|
5888
|
+
const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
5579
5889
|
forwarder.abortAll();
|
|
5580
5890
|
onDisconnected?.(code, reasonStr);
|
|
5581
5891
|
});
|
|
@@ -5583,9 +5893,10 @@ function connectTunnel(options) {
|
|
|
5583
5893
|
}
|
|
5584
5894
|
|
|
5585
5895
|
// src/lib/tunnel/runner-connection.ts
|
|
5586
|
-
var RunnerConnection = class {
|
|
5896
|
+
var RunnerConnection = class _RunnerConnection {
|
|
5587
5897
|
opts;
|
|
5588
5898
|
sleep;
|
|
5899
|
+
now;
|
|
5589
5900
|
connection = null;
|
|
5590
5901
|
resolvedAgentId;
|
|
5591
5902
|
/** True while a (re)connect loop is in flight. */
|
|
@@ -5594,10 +5905,17 @@ var RunnerConnection = class {
|
|
|
5594
5905
|
reconnectPromise = null;
|
|
5595
5906
|
/** 1-based count of the current reconnect attempt streak. */
|
|
5596
5907
|
reconnectAttempt = 0;
|
|
5908
|
+
generation = 0;
|
|
5909
|
+
disconnectedAtMs = null;
|
|
5910
|
+
static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
|
|
5911
|
+
"No pong received",
|
|
5912
|
+
"Orphaned socket"
|
|
5913
|
+
]);
|
|
5597
5914
|
constructor(opts) {
|
|
5598
5915
|
this.opts = opts;
|
|
5599
5916
|
this.resolvedAgentId = opts.agentId;
|
|
5600
5917
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5918
|
+
this.now = opts.now ?? Date.now;
|
|
5601
5919
|
}
|
|
5602
5920
|
get agentId() {
|
|
5603
5921
|
return this.resolvedAgentId;
|
|
@@ -5633,14 +5951,24 @@ var RunnerConnection = class {
|
|
|
5633
5951
|
port: this.opts.port,
|
|
5634
5952
|
openCodePassword: this.opts.openCodePassword,
|
|
5635
5953
|
onConnected: (agentId) => {
|
|
5954
|
+
const attempt = this.reconnectAttempt + 1;
|
|
5636
5955
|
this.reconnectAttempt = 0;
|
|
5637
5956
|
this.reconnecting = false;
|
|
5638
5957
|
this.resolvedAgentId = agentId;
|
|
5639
|
-
|
|
5958
|
+
this.generation++;
|
|
5959
|
+
const downtimeMs = this.disconnectedAtMs === null ? null : this.now() - this.disconnectedAtMs;
|
|
5960
|
+
this.disconnectedAtMs = null;
|
|
5961
|
+
events.onConnected(agentId, isReconnect, {
|
|
5962
|
+
generation: this.generation,
|
|
5963
|
+
attempt,
|
|
5964
|
+
downtimeMs
|
|
5965
|
+
});
|
|
5640
5966
|
},
|
|
5641
5967
|
onDisconnected: (code, reason) => {
|
|
5642
|
-
|
|
5643
|
-
|
|
5968
|
+
this.disconnectedAtMs = this.now();
|
|
5969
|
+
events.onDisconnected(code, reason, { generation: this.generation });
|
|
5970
|
+
const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
|
|
5971
|
+
if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
|
|
5644
5972
|
this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
|
|
5645
5973
|
events.onError?.(`Reconnection failed: ${err.message}`);
|
|
5646
5974
|
});
|
|
@@ -5979,8 +6307,85 @@ function createResourceUsageCollector(homeDir) {
|
|
|
5979
6307
|
};
|
|
5980
6308
|
}
|
|
5981
6309
|
|
|
6310
|
+
// src/lib/resource-usage-low-disk.ts
|
|
6311
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_BYTES = 4 * 1024 ** 3;
|
|
6312
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION = 0.25;
|
|
6313
|
+
var RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES = 1 * 1024 ** 3;
|
|
6314
|
+
var RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES = 256 * 1024 ** 2;
|
|
6315
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES = 6 * 1024 ** 3;
|
|
6316
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION = 0.375;
|
|
6317
|
+
var LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
6318
|
+
var BAND_RANK = {
|
|
6319
|
+
low: 1,
|
|
6320
|
+
very_low: 2,
|
|
6321
|
+
critical: 3
|
|
6322
|
+
};
|
|
6323
|
+
function bandFor(freeBytes) {
|
|
6324
|
+
if (freeBytes < RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES) return "critical";
|
|
6325
|
+
if (freeBytes < RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES) return "very_low";
|
|
6326
|
+
return "low";
|
|
6327
|
+
}
|
|
6328
|
+
function formatBytes(bytes) {
|
|
6329
|
+
if (bytes === null) return "unavailable";
|
|
6330
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
6331
|
+
const unitIndex = Math.min(Math.floor(Math.log2(Math.max(bytes, 1)) / 10), units.length - 1);
|
|
6332
|
+
const value = bytes / 1024 ** unitIndex;
|
|
6333
|
+
const digits = unitIndex === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 2;
|
|
6334
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
6335
|
+
}
|
|
6336
|
+
function warningMessage(band, diskFreeBytes, diskTotalBytes, opencodeDbBytes) {
|
|
6337
|
+
return `Low disk warning (${band} severity): ${formatBytes(diskFreeBytes)} free of ${formatBytes(diskTotalBytes)}; opencode.db is ${formatBytes(opencodeDbBytes)}. Clear space on the machine running this runner before session history or new work can no longer be saved.`;
|
|
6338
|
+
}
|
|
6339
|
+
function evaluateLowDiskWarning(input) {
|
|
6340
|
+
const { diskFreeBytes, diskTotalBytes, opencodeDbBytes, nowMs, state } = input;
|
|
6341
|
+
if (diskFreeBytes === null || diskTotalBytes === null || diskTotalBytes <= 0 || diskFreeBytes >= diskTotalBytes) {
|
|
6342
|
+
return { warning: null, state };
|
|
6343
|
+
}
|
|
6344
|
+
const recovered = diskFreeBytes >= RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES || diskFreeBytes >= diskTotalBytes * RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION;
|
|
6345
|
+
if (recovered) {
|
|
6346
|
+
return { warning: null, state: { worstBand: null, warnedAtMs: null } };
|
|
6347
|
+
}
|
|
6348
|
+
const low = diskFreeBytes < RUNNER_DISK_ALERT_ENTRY_FREE_BYTES && diskFreeBytes < diskTotalBytes * RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION;
|
|
6349
|
+
if (!low) return { warning: null, state };
|
|
6350
|
+
const currentBand = bandFor(diskFreeBytes);
|
|
6351
|
+
const currentRank = BAND_RANK[currentBand];
|
|
6352
|
+
const previousRank = state.worstBand === null ? 0 : BAND_RANK[state.worstBand];
|
|
6353
|
+
const worse = currentRank > previousRank;
|
|
6354
|
+
const reassert = state.warnedAtMs !== null && nowMs - state.warnedAtMs >= LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS;
|
|
6355
|
+
const nextState = {
|
|
6356
|
+
worstBand: worse ? currentBand : state.worstBand ?? currentBand,
|
|
6357
|
+
warnedAtMs: worse || state.warnedAtMs === null || reassert ? nowMs : state.warnedAtMs
|
|
6358
|
+
};
|
|
6359
|
+
return {
|
|
6360
|
+
warning: worse || state.warnedAtMs === null || reassert ? warningMessage(nextState.worstBand, diskFreeBytes, diskTotalBytes, opencodeDbBytes) : null,
|
|
6361
|
+
state: nextState
|
|
6362
|
+
};
|
|
6363
|
+
}
|
|
6364
|
+
|
|
6365
|
+
// src/lib/opencode/session-store-health.ts
|
|
6366
|
+
var SESSION_STORE_HEALTH_STALENESS_MS = 5 * 6e4;
|
|
6367
|
+
function createSessionStoreHealth(options = {}) {
|
|
6368
|
+
const now = options.now ?? (() => Date.now());
|
|
6369
|
+
const stalenessMs = Math.max(0, options.stalenessMs ?? SESSION_STORE_HEALTH_STALENESS_MS);
|
|
6370
|
+
let lastUnwritableAt = null;
|
|
6371
|
+
let lastWritableAt = null;
|
|
6372
|
+
return {
|
|
6373
|
+
observeUnwritable: () => {
|
|
6374
|
+
lastUnwritableAt = now();
|
|
6375
|
+
},
|
|
6376
|
+
observeWritable: () => {
|
|
6377
|
+
lastWritableAt = now();
|
|
6378
|
+
},
|
|
6379
|
+
isUnwritable: () => {
|
|
6380
|
+
if (lastUnwritableAt === null) return false;
|
|
6381
|
+
if (lastWritableAt !== null && lastWritableAt >= lastUnwritableAt) return false;
|
|
6382
|
+
return now() - lastUnwritableAt < stalenessMs;
|
|
6383
|
+
}
|
|
6384
|
+
};
|
|
6385
|
+
}
|
|
6386
|
+
|
|
5982
6387
|
// src/lib/channels/driver.ts
|
|
5983
|
-
import { homedir as
|
|
6388
|
+
import { homedir as homedir6 } from "node:os";
|
|
5984
6389
|
|
|
5985
6390
|
// src/lib/runner-file-sync.ts
|
|
5986
6391
|
import { join as join9 } from "node:path";
|
|
@@ -6437,6 +6842,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
|
6437
6842
|
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
6438
6843
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
6439
6844
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
6845
|
+
var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
|
|
6440
6846
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
6441
6847
|
var HEARTBEAT_MS = 6e4;
|
|
6442
6848
|
var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
@@ -6498,6 +6904,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6498
6904
|
maxActiveSessions;
|
|
6499
6905
|
watcherStallMs;
|
|
6500
6906
|
wedgeWarningIntervalMs;
|
|
6907
|
+
sessionStoreHealth;
|
|
6501
6908
|
openCodeClient;
|
|
6502
6909
|
/** Cache of conversationId → opencode sessionId. */
|
|
6503
6910
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -6863,10 +7270,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6863
7270
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
6864
7271
|
this.now = config.now ?? (() => Date.now());
|
|
6865
7272
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
6866
|
-
this.homeDir = config.homeDir ??
|
|
7273
|
+
this.homeDir = config.homeDir ?? homedir6();
|
|
6867
7274
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
6868
7275
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
6869
7276
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
7277
|
+
this.sessionStoreHealth = config.sessionStoreHealth;
|
|
6870
7278
|
}
|
|
6871
7279
|
get isV2() {
|
|
6872
7280
|
return this.openCodeClient.version === "v2";
|
|
@@ -6882,7 +7290,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6882
7290
|
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6883
7291
|
}
|
|
6884
7292
|
async listSessions() {
|
|
6885
|
-
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient
|
|
7293
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient, {
|
|
7294
|
+
limit: DRIVER_SESSION_ENUMERATION_LIMIT
|
|
7295
|
+
});
|
|
6886
7296
|
}
|
|
6887
7297
|
async sessionExists(sessionId) {
|
|
6888
7298
|
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
@@ -8338,6 +8748,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8338
8748
|
b2AbandonedSignalled: false,
|
|
8339
8749
|
ambiguousPinnedSinceMs: 0,
|
|
8340
8750
|
ambiguousResolved: false,
|
|
8751
|
+
sessionStoreFailurePending: false,
|
|
8341
8752
|
neverStartedResolved: false
|
|
8342
8753
|
});
|
|
8343
8754
|
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
@@ -8456,6 +8867,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8456
8867
|
b2AbandonedSignalled: false,
|
|
8457
8868
|
ambiguousPinnedSinceMs: 0,
|
|
8458
8869
|
ambiguousResolved: false,
|
|
8870
|
+
sessionStoreFailurePending: false,
|
|
8459
8871
|
neverStartedResolved: false
|
|
8460
8872
|
});
|
|
8461
8873
|
}
|
|
@@ -8869,6 +9281,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8869
9281
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8870
9282
|
this.neverStartedAttempts.delete(id);
|
|
8871
9283
|
}
|
|
9284
|
+
if (inFlight.sessionStoreFailurePending) {
|
|
9285
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9286
|
+
return;
|
|
9287
|
+
}
|
|
8872
9288
|
if (inFlight.neverStartedResolved) {
|
|
8873
9289
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8874
9290
|
inFlight.neverStartedResolved = false;
|
|
@@ -9048,6 +9464,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9048
9464
|
descendantOngoing,
|
|
9049
9465
|
rootOngoing
|
|
9050
9466
|
})) {
|
|
9467
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
9468
|
+
inFlight.sessionStoreFailurePending = true;
|
|
9469
|
+
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9470
|
+
watched_for_ms: pinnedForMs,
|
|
9471
|
+
run_state: "failed",
|
|
9472
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9473
|
+
});
|
|
9474
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9475
|
+
return;
|
|
9476
|
+
}
|
|
9051
9477
|
inFlight.b2AbandonedSignalled = true;
|
|
9052
9478
|
this.log({
|
|
9053
9479
|
level: "warn",
|
|
@@ -9106,6 +9532,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9106
9532
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
9107
9533
|
sessionOngoing: ongoing
|
|
9108
9534
|
})) {
|
|
9535
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
9536
|
+
inFlight.sessionStoreFailurePending = true;
|
|
9537
|
+
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
9538
|
+
watched_for_ms: pinnedForMs,
|
|
9539
|
+
run_state: "failed",
|
|
9540
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9541
|
+
});
|
|
9542
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9543
|
+
return;
|
|
9544
|
+
}
|
|
9109
9545
|
inFlight.ambiguousResolved = true;
|
|
9110
9546
|
this.log({
|
|
9111
9547
|
level: "warn",
|
|
@@ -9385,6 +9821,66 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9385
9821
|
this.neverStartedAttempts.delete(inFlight.evidentMessageId);
|
|
9386
9822
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9387
9823
|
}
|
|
9824
|
+
/**
|
|
9825
|
+
* Report a turn as failed when the session database was observed unwritable.
|
|
9826
|
+
* The queue row is retained as failed, while the OpenCode copy stays fenced so
|
|
9827
|
+
* a later drain cannot blindly prompt the same turn again.
|
|
9828
|
+
*/
|
|
9829
|
+
async settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight) {
|
|
9830
|
+
const conv = watcher.conv;
|
|
9831
|
+
const error2 = "The runner session database could not be written, so this turn did not finish. Nothing was lost from the queue. Clear disk space and retry.";
|
|
9832
|
+
this.log({
|
|
9833
|
+
level: "error",
|
|
9834
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was not resolved as done: ${error2}`,
|
|
9835
|
+
conversation_id: conv.id,
|
|
9836
|
+
message_id: inFlight.evidentMessageId
|
|
9837
|
+
});
|
|
9838
|
+
this.anchorDeliveryDeadline(inFlight);
|
|
9839
|
+
try {
|
|
9840
|
+
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
|
|
9841
|
+
} catch (err) {
|
|
9842
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
9843
|
+
if (err instanceof ChannelTerminalError) {
|
|
9844
|
+
this.log({
|
|
9845
|
+
level: "warn",
|
|
9846
|
+
message: `Failed to report session-store failure for message ${inFlight.evidentMessageId.slice(0, 8)} as failed (terminal HTTP ${err.status}) \u2014 keeping its OpenCode copy fenced: ${err.message}`,
|
|
9847
|
+
conversation_id: conv.id,
|
|
9848
|
+
message_id: inFlight.evidentMessageId
|
|
9849
|
+
});
|
|
9850
|
+
this.recordReleasedOpencodeId(
|
|
9851
|
+
inFlight.evidentMessageId,
|
|
9852
|
+
sessionId,
|
|
9853
|
+
inFlight.opencodeMessageId
|
|
9854
|
+
);
|
|
9855
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9856
|
+
return;
|
|
9857
|
+
}
|
|
9858
|
+
if (this.now() >= inFlight.deadline) {
|
|
9859
|
+
this.log({
|
|
9860
|
+
level: "warn",
|
|
9861
|
+
message: `Failed to report session-store failure for message ${inFlight.evidentMessageId.slice(0, 8)} as failed within the delivery window \u2014 keeping its OpenCode copy fenced: ${err instanceof Error ? err.message : String(err)}`,
|
|
9862
|
+
conversation_id: conv.id,
|
|
9863
|
+
message_id: inFlight.evidentMessageId
|
|
9864
|
+
});
|
|
9865
|
+
this.recordReleasedOpencodeId(
|
|
9866
|
+
inFlight.evidentMessageId,
|
|
9867
|
+
sessionId,
|
|
9868
|
+
inFlight.opencodeMessageId
|
|
9869
|
+
);
|
|
9870
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9871
|
+
return;
|
|
9872
|
+
}
|
|
9873
|
+
this.log({
|
|
9874
|
+
level: "warn",
|
|
9875
|
+
message: `Failed to report session-store failure for message ${inFlight.evidentMessageId.slice(0, 8)} as failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
9876
|
+
conversation_id: conv.id,
|
|
9877
|
+
message_id: inFlight.evidentMessageId
|
|
9878
|
+
});
|
|
9879
|
+
return;
|
|
9880
|
+
}
|
|
9881
|
+
inFlight.done = true;
|
|
9882
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9883
|
+
}
|
|
9388
9884
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
9389
9885
|
/**
|
|
9390
9886
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
@@ -12116,6 +12612,7 @@ function createCredentialSync({
|
|
|
12116
12612
|
|
|
12117
12613
|
// src/commands/run.ts
|
|
12118
12614
|
var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
12615
|
+
var MAX_DISCONNECT_REASON_LENGTH = 200;
|
|
12119
12616
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
12120
12617
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
12121
12618
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
@@ -12271,7 +12768,13 @@ function logActivity(state, entry) {
|
|
|
12271
12768
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
12272
12769
|
if (!meetsThreshold(state, level)) return;
|
|
12273
12770
|
forwardRunnerActivity(
|
|
12274
|
-
{
|
|
12771
|
+
{
|
|
12772
|
+
level,
|
|
12773
|
+
message: entry.message,
|
|
12774
|
+
error: entry.error,
|
|
12775
|
+
metadata: entry.metadata,
|
|
12776
|
+
...entry.source ? { source: entry.source } : {}
|
|
12777
|
+
},
|
|
12275
12778
|
{ agentId: state.agentId, authHeader: state.authHeader }
|
|
12276
12779
|
);
|
|
12277
12780
|
const fullEntry = {
|
|
@@ -12293,7 +12796,7 @@ function logActivity(state, entry) {
|
|
|
12293
12796
|
}
|
|
12294
12797
|
function reportSessionDbRecovery(state) {
|
|
12295
12798
|
try {
|
|
12296
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
12799
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
|
|
12297
12800
|
for (const record of report.records) {
|
|
12298
12801
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
12299
12802
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -12328,7 +12831,9 @@ function displayStatus(state) {
|
|
|
12328
12831
|
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
12329
12832
|
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
12330
12833
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
12331
|
-
const detail = last ? chalk7.dim(
|
|
12834
|
+
const detail = last ? chalk7.dim(
|
|
12835
|
+
` \xB7 ${last.timestamp.toISOString()} ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`
|
|
12836
|
+
) : "";
|
|
12332
12837
|
const agent = state.agentName ?? state.agentId;
|
|
12333
12838
|
console.log(
|
|
12334
12839
|
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
@@ -12347,7 +12852,7 @@ function reportOpenCodeV2CredentialImport(state, result) {
|
|
|
12347
12852
|
logActivity(state, {
|
|
12348
12853
|
type: "info",
|
|
12349
12854
|
level: "warn",
|
|
12350
|
-
message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them.
|
|
12855
|
+
message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them. Connect the provider again from the runner's page in Evident; no shell on this machine is needed.`
|
|
12351
12856
|
});
|
|
12352
12857
|
}
|
|
12353
12858
|
if (result.imported.length === 0 && result.unsupported.length === 0 && result.failed.length === 0) {
|
|
@@ -12372,7 +12877,7 @@ async function importOpenCodeV2CredentialsForState(state) {
|
|
|
12372
12877
|
}
|
|
12373
12878
|
const result = await importOpenCodeV2Credentials({
|
|
12374
12879
|
request: client.request,
|
|
12375
|
-
homeDir:
|
|
12880
|
+
homeDir: homedir7(),
|
|
12376
12881
|
directory,
|
|
12377
12882
|
guard: state.opencodeV2CredentialImportGuard,
|
|
12378
12883
|
log: (message, level = "info") => logActivity(state, { type: "info", level, message })
|
|
@@ -12580,9 +13085,9 @@ async function driveChannels(state, driver) {
|
|
|
12580
13085
|
}
|
|
12581
13086
|
}
|
|
12582
13087
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
12583
|
-
var
|
|
13088
|
+
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
12584
13089
|
function shouldWarnForReclaimSkip(reason) {
|
|
12585
|
-
if (reason !== "sqlite-unavailable") return false;
|
|
13090
|
+
if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
|
|
12586
13091
|
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
12587
13092
|
if (!version2) return false;
|
|
12588
13093
|
const major = Number(version2[1]);
|
|
@@ -12591,7 +13096,14 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
12591
13096
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
12592
13097
|
}
|
|
12593
13098
|
function sessionDbPath() {
|
|
12594
|
-
return join11(
|
|
13099
|
+
return join11(homedir7(), ".local", "share", "opencode", "opencode.db");
|
|
13100
|
+
}
|
|
13101
|
+
function logSessionCleanupActivity(state, phase, entry) {
|
|
13102
|
+
logActivity(state, {
|
|
13103
|
+
...entry,
|
|
13104
|
+
source: "cli.session-cleanup",
|
|
13105
|
+
metadata: { ...entry.metadata, phase }
|
|
13106
|
+
});
|
|
12595
13107
|
}
|
|
12596
13108
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
12597
13109
|
const record = {
|
|
@@ -12632,69 +13144,136 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
12632
13144
|
async function runSweep(state, driver, config) {
|
|
12633
13145
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
12634
13146
|
try {
|
|
12635
|
-
const
|
|
12636
|
-
|
|
13147
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0);
|
|
13148
|
+
const sessions = enumeration.sessions;
|
|
13149
|
+
const enumerationReason = sessions === null ? enumeration.reason : null;
|
|
13150
|
+
if (enumerationReason !== null) {
|
|
12637
13151
|
logActivity(state, {
|
|
12638
13152
|
type: "info",
|
|
12639
|
-
|
|
13153
|
+
level: "warn",
|
|
13154
|
+
message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
|
|
12640
13155
|
});
|
|
12641
|
-
return;
|
|
12642
13156
|
}
|
|
12643
|
-
const toDelete = selectSessionsToDelete(
|
|
12644
|
-
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
12645
|
-
{
|
|
12646
|
-
maxAgeMs: config.maxAgeMs,
|
|
12647
|
-
maxCount: config.maxCount,
|
|
12648
|
-
nowMs: Date.now(),
|
|
12649
|
-
protectedIds: driver.protectedSessionIds()
|
|
12650
|
-
}
|
|
12651
|
-
);
|
|
12652
|
-
const protectedNow = driver.protectedSessionIds();
|
|
12653
13157
|
let deleted = 0;
|
|
12654
13158
|
let failed = 0;
|
|
13159
|
+
let failed4xx = 0;
|
|
13160
|
+
let failed5xx = 0;
|
|
13161
|
+
let failedOtherStatus = 0;
|
|
13162
|
+
let failedThrew = 0;
|
|
12655
13163
|
let skippedNewlyActive = 0;
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
13164
|
+
let remaining = 0;
|
|
13165
|
+
let protectedNow = driver.protectedSessionIds();
|
|
13166
|
+
if (sessions !== null) {
|
|
13167
|
+
const toDelete = selectSessionsToDelete(
|
|
13168
|
+
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
13169
|
+
{
|
|
13170
|
+
maxAgeMs: config.maxAgeMs,
|
|
13171
|
+
maxCount: config.maxCount,
|
|
13172
|
+
nowMs: Date.now(),
|
|
13173
|
+
protectedIds: protectedNow
|
|
13174
|
+
}
|
|
13175
|
+
);
|
|
13176
|
+
const deleteCandidates = toDelete.slice(0, SESSION_CLEANUP_MAX_DELETES_PER_SWEEP);
|
|
13177
|
+
remaining = toDelete.length - deleteCandidates.length;
|
|
13178
|
+
protectedNow = driver.protectedSessionIds();
|
|
13179
|
+
for (const id of deleteCandidates) {
|
|
13180
|
+
if (protectedNow.has(id)) {
|
|
13181
|
+
skippedNewlyActive++;
|
|
13182
|
+
logActivity(state, {
|
|
13183
|
+
type: "info",
|
|
13184
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
|
|
13185
|
+
});
|
|
13186
|
+
continue;
|
|
13187
|
+
}
|
|
13188
|
+
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
13189
|
+
if (outcome.ok) {
|
|
13190
|
+
deleted++;
|
|
13191
|
+
} else if (outcome.status === "threw") {
|
|
13192
|
+
failed++;
|
|
13193
|
+
failedThrew++;
|
|
13194
|
+
} else {
|
|
13195
|
+
failed++;
|
|
13196
|
+
const statusClass = Math.floor(outcome.status / 100);
|
|
13197
|
+
if (statusClass === 4) failed4xx++;
|
|
13198
|
+
else if (statusClass === 5) failed5xx++;
|
|
13199
|
+
else failedOtherStatus++;
|
|
13200
|
+
}
|
|
12664
13201
|
}
|
|
12665
|
-
|
|
12666
|
-
|
|
13202
|
+
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
13203
|
+
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
13204
|
+
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
13205
|
+
logActivity(state, {
|
|
13206
|
+
type: "info",
|
|
13207
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
13208
|
+
});
|
|
12667
13209
|
}
|
|
12668
|
-
const
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
13210
|
+
const sessionMetadata = {
|
|
13211
|
+
sessions_inspected: sessions?.length ?? null,
|
|
13212
|
+
sessions_deleted: deleted,
|
|
13213
|
+
sessions_failed: failed,
|
|
13214
|
+
sessions_failed_4xx: failed4xx,
|
|
13215
|
+
sessions_failed_5xx: failed5xx,
|
|
13216
|
+
sessions_failed_other: failedOtherStatus,
|
|
13217
|
+
sessions_failed_threw: failedThrew,
|
|
13218
|
+
sessions_skipped_active: skippedNewlyActive,
|
|
13219
|
+
sessions_remaining: remaining,
|
|
13220
|
+
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
13221
|
+
};
|
|
12674
13222
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
12675
13223
|
dbPath: sessionDbPath(),
|
|
12676
|
-
|
|
13224
|
+
chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
13225
|
+
pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
13226
|
+
timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
12677
13227
|
allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
|
|
12678
13228
|
});
|
|
12679
13229
|
if (reclaimResult.ok) {
|
|
13230
|
+
state.sessionStoreHealth.observeWritable();
|
|
12680
13231
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
12681
13232
|
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
12682
13233
|
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
12683
|
-
|
|
13234
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12684
13235
|
type: "info",
|
|
12685
|
-
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${
|
|
13236
|
+
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB (stopped by ${reclaimResult.stoppedBy}; freelist ${reclaimResult.freelistBefore} -> ${reclaimResult.freelistAfter} pages)` + checkpointNote,
|
|
13237
|
+
metadata: {
|
|
13238
|
+
outcome: "reclaimed",
|
|
13239
|
+
mode: reclaimResult.mode,
|
|
13240
|
+
before_bytes: reclaimResult.beforeBytes,
|
|
13241
|
+
after_bytes: reclaimResult.afterBytes,
|
|
13242
|
+
checkpoint_busy: reclaimResult.checkpoint.busy,
|
|
13243
|
+
wal_frames_pending: reclaimResult.checkpoint.log,
|
|
13244
|
+
stopped_by: reclaimResult.stoppedBy,
|
|
13245
|
+
freelist_before: reclaimResult.freelistBefore,
|
|
13246
|
+
freelist_after: reclaimResult.freelistAfter,
|
|
13247
|
+
pages_reclaimed: reclaimResult.pagesReclaimed,
|
|
13248
|
+
reclaim_elapsed_ms: reclaimResult.elapsedMs,
|
|
13249
|
+
...sessionMetadata
|
|
13250
|
+
}
|
|
12686
13251
|
});
|
|
12687
13252
|
} else {
|
|
12688
|
-
|
|
13253
|
+
if (reclaimResult.skipped === "disk-exhaustion") {
|
|
13254
|
+
state.sessionStoreHealth.observeUnwritable();
|
|
13255
|
+
}
|
|
13256
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12689
13257
|
type: "info",
|
|
12690
|
-
|
|
13258
|
+
level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
|
|
13259
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
|
|
13260
|
+
metadata: {
|
|
13261
|
+
outcome: "reclaim-skipped",
|
|
13262
|
+
reason: reclaimResult.skipped,
|
|
13263
|
+
...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
|
|
13264
|
+
...sessionMetadata
|
|
13265
|
+
}
|
|
12691
13266
|
});
|
|
12692
13267
|
}
|
|
12693
13268
|
} catch (error2) {
|
|
12694
13269
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
12695
|
-
|
|
13270
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12696
13271
|
type: "error",
|
|
12697
|
-
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}
|
|
13272
|
+
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`,
|
|
13273
|
+
metadata: {
|
|
13274
|
+
outcome: "sweep-failed",
|
|
13275
|
+
reason: "sweep-error"
|
|
13276
|
+
}
|
|
12698
13277
|
});
|
|
12699
13278
|
}
|
|
12700
13279
|
}
|
|
@@ -12710,14 +13289,19 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12710
13289
|
for (const warning2 of config.warnings) {
|
|
12711
13290
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
12712
13291
|
}
|
|
12713
|
-
const dbBytes = statSessionDbBytes(
|
|
13292
|
+
const dbBytes = statSessionDbBytes(homedir7());
|
|
12714
13293
|
void (async () => {
|
|
12715
13294
|
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
12716
13295
|
if (reclaimAvailability !== null) {
|
|
12717
|
-
|
|
13296
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12718
13297
|
type: "info",
|
|
12719
13298
|
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
12720
|
-
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
13299
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : ""),
|
|
13300
|
+
metadata: {
|
|
13301
|
+
outcome: "preflight-skipped",
|
|
13302
|
+
reason: reclaimAvailability.reason,
|
|
13303
|
+
...reclaimAvailability.detail !== void 0 ? { detail: reclaimAvailability.detail } : {}
|
|
13304
|
+
}
|
|
12721
13305
|
});
|
|
12722
13306
|
}
|
|
12723
13307
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
@@ -12734,9 +13318,15 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12734
13318
|
);
|
|
12735
13319
|
});
|
|
12736
13320
|
if (!config.enabled) return;
|
|
12737
|
-
|
|
13321
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12738
13322
|
type: "info",
|
|
12739
|
-
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)
|
|
13323
|
+
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`,
|
|
13324
|
+
metadata: {
|
|
13325
|
+
outcome: "armed",
|
|
13326
|
+
interval_ms: config.intervalMs,
|
|
13327
|
+
max_age_ms: config.maxAgeMs ?? null,
|
|
13328
|
+
max_count: config.maxCount ?? null
|
|
13329
|
+
}
|
|
12740
13330
|
});
|
|
12741
13331
|
const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
|
|
12742
13332
|
const firstSweep = setTimeout(
|
|
@@ -12915,7 +13505,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
12915
13505
|
});
|
|
12916
13506
|
return;
|
|
12917
13507
|
}
|
|
12918
|
-
const { collect, stop } = createResourceUsageCollector(
|
|
13508
|
+
const { collect, stop } = createResourceUsageCollector(homedir7());
|
|
12919
13509
|
state.stopResourceUsageSampling = stop;
|
|
12920
13510
|
let consecutiveFailures = 0;
|
|
12921
13511
|
const tick = async () => {
|
|
@@ -12928,6 +13518,19 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
12928
13518
|
message: `Resource usage collection: ${warning2}`
|
|
12929
13519
|
});
|
|
12930
13520
|
}
|
|
13521
|
+
const lowDiskDecision = evaluateLowDiskWarning({
|
|
13522
|
+
...usage,
|
|
13523
|
+
nowMs: Date.now(),
|
|
13524
|
+
state: state.lowDiskWarningState
|
|
13525
|
+
});
|
|
13526
|
+
state.lowDiskWarningState = lowDiskDecision.state;
|
|
13527
|
+
if (lowDiskDecision.warning !== null) {
|
|
13528
|
+
logActivity(state, {
|
|
13529
|
+
type: "info",
|
|
13530
|
+
level: "warn",
|
|
13531
|
+
message: lowDiskDecision.warning
|
|
13532
|
+
});
|
|
13533
|
+
}
|
|
12931
13534
|
const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
|
|
12932
13535
|
if (result.ok) {
|
|
12933
13536
|
if (consecutiveFailures > 0) {
|
|
@@ -13140,7 +13743,7 @@ async function run(options) {
|
|
|
13140
13743
|
let fileSyncDirectories;
|
|
13141
13744
|
try {
|
|
13142
13745
|
logLevel = resolveLogLevel(options);
|
|
13143
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
13746
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
|
|
13144
13747
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
13145
13748
|
throw new Error(
|
|
13146
13749
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -13174,6 +13777,7 @@ async function run(options) {
|
|
|
13174
13777
|
opencodeClient: null,
|
|
13175
13778
|
opencodeV2CredentialImportGuard: { inFlight: false },
|
|
13176
13779
|
sessionDbProvenanceAnomaly: false,
|
|
13780
|
+
sessionStoreHealth: createSessionStoreHealth(),
|
|
13177
13781
|
opencodeProcess: null,
|
|
13178
13782
|
stopOpenCodeLogTail: null,
|
|
13179
13783
|
litestreamProcess: null,
|
|
@@ -13191,10 +13795,12 @@ async function run(options) {
|
|
|
13191
13795
|
openaiUsageRearm: null,
|
|
13192
13796
|
resourceUsageTimer: null,
|
|
13193
13797
|
stopResourceUsageSampling: null,
|
|
13798
|
+
lowDiskWarningState: { worstBand: null, warnedAtMs: null },
|
|
13194
13799
|
credentialSync: null,
|
|
13195
13800
|
authHeader: ""
|
|
13196
13801
|
};
|
|
13197
13802
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
13803
|
+
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
13198
13804
|
if (options.credentialSyncMarker) {
|
|
13199
13805
|
state.credentialSync = createCredentialSync({
|
|
13200
13806
|
markerPath: options.credentialSyncMarker,
|
|
@@ -13380,13 +13986,21 @@ async function run(options) {
|
|
|
13380
13986
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
13381
13987
|
}
|
|
13382
13988
|
state.credentialSync?.arm();
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
createOpenCodeActivityForwarder(() => ({
|
|
13989
|
+
const openCodeActivityForwarder = createOpenCodeActivityForwarder(
|
|
13990
|
+
() => ({
|
|
13386
13991
|
agentId: state.agentId,
|
|
13387
13992
|
authHeader: state.authHeader
|
|
13388
|
-
})
|
|
13389
|
-
|
|
13993
|
+
}),
|
|
13994
|
+
state.sessionStoreHealth.observeUnwritable
|
|
13995
|
+
);
|
|
13996
|
+
const openCodeLogTail = tailOpenCodeLogFile(
|
|
13997
|
+
resolveOpenCodeLogPath(homedir7(), process.env),
|
|
13998
|
+
openCodeActivityForwarder
|
|
13999
|
+
);
|
|
14000
|
+
state.stopOpenCodeLogTail = () => {
|
|
14001
|
+
openCodeActivityForwarder.flush();
|
|
14002
|
+
openCodeLogTail.stop();
|
|
14003
|
+
};
|
|
13390
14004
|
let sessionDbVerifyFatal = false;
|
|
13391
14005
|
if (!options.restoreSessionDb) {
|
|
13392
14006
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -13503,7 +14117,7 @@ async function run(options) {
|
|
|
13503
14117
|
const provenance = checkSessionDbProvenance({
|
|
13504
14118
|
dbPath: sessionDbPath(),
|
|
13505
14119
|
currentVersion: state.opencodeVersion,
|
|
13506
|
-
homeDir:
|
|
14120
|
+
homeDir: homedir7(),
|
|
13507
14121
|
env: process.env
|
|
13508
14122
|
});
|
|
13509
14123
|
if (provenance.anomaly) {
|
|
@@ -13670,10 +14284,11 @@ async function run(options) {
|
|
|
13670
14284
|
getAuthHeader: () => state.authHeader,
|
|
13671
14285
|
conversationFilter: state.conversationFilter,
|
|
13672
14286
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
14287
|
+
sessionStoreHealth: state.sessionStoreHealth,
|
|
13673
14288
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
13674
14289
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
13675
14290
|
fileSyncDirectories,
|
|
13676
|
-
homeDir:
|
|
14291
|
+
homeDir: homedir7(),
|
|
13677
14292
|
maxActiveSessions,
|
|
13678
14293
|
log: (entry) => (
|
|
13679
14294
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -13695,12 +14310,13 @@ async function run(options) {
|
|
|
13695
14310
|
openCodePassword: state.opencodePassword,
|
|
13696
14311
|
isRunning: () => state.running,
|
|
13697
14312
|
events: {
|
|
13698
|
-
onConnected: (agentId, isReconnect) => {
|
|
14313
|
+
onConnected: (agentId, isReconnect, transition) => {
|
|
13699
14314
|
state.connected = true;
|
|
13700
14315
|
state.agentId = agentId;
|
|
14316
|
+
const connectionMessage = isReconnect ? `Tunnel reconnected (runner: ${agentId}, generation: ${transition.generation}, attempt: ${transition.attempt}, offline for ${transition.downtimeMs}ms)` : `Tunnel connected (runner: ${agentId}, generation: ${transition.generation})`;
|
|
13701
14317
|
logActivity(state, {
|
|
13702
14318
|
type: "info",
|
|
13703
|
-
message:
|
|
14319
|
+
message: connectionMessage
|
|
13704
14320
|
});
|
|
13705
14321
|
if (options.tunnelReadyFile) {
|
|
13706
14322
|
const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
|
|
@@ -13721,7 +14337,12 @@ async function run(options) {
|
|
|
13721
14337
|
version: state.opencodeVersion,
|
|
13722
14338
|
major: state.opencodeApiVersion,
|
|
13723
14339
|
connected: state.opencodeConnected
|
|
13724
|
-
})
|
|
14340
|
+
}),
|
|
14341
|
+
connection_generation: transition.generation,
|
|
14342
|
+
...isReconnect && transition.downtimeMs !== null ? {
|
|
14343
|
+
reconnect_attempt: transition.attempt,
|
|
14344
|
+
downtime_ms: transition.downtimeMs
|
|
14345
|
+
} : {}
|
|
13725
14346
|
});
|
|
13726
14347
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
13727
14348
|
if (state.interactive) displayStatus(state);
|
|
@@ -13743,13 +14364,18 @@ async function run(options) {
|
|
|
13743
14364
|
if (state.interactive) displayStatus(state);
|
|
13744
14365
|
});
|
|
13745
14366
|
},
|
|
13746
|
-
onDisconnected: (code, reason) => {
|
|
14367
|
+
onDisconnected: (code, reason, transition) => {
|
|
13747
14368
|
state.connected = false;
|
|
14369
|
+
const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
|
|
13748
14370
|
logActivity(state, {
|
|
13749
14371
|
type: "info",
|
|
13750
|
-
message: `Tunnel disconnected (code: ${code}, reason: ${
|
|
14372
|
+
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
|
|
14373
|
+
});
|
|
14374
|
+
emitAgentDisconnected(state.agentId, {
|
|
14375
|
+
code,
|
|
14376
|
+
reason: safeReason,
|
|
14377
|
+
connection_generation: transition.generation
|
|
13751
14378
|
});
|
|
13752
|
-
emitAgentDisconnected(state.agentId, { code, reason });
|
|
13753
14379
|
if (state.interactive) displayStatus(state);
|
|
13754
14380
|
},
|
|
13755
14381
|
onError: (error2) => {
|
|
@@ -13849,7 +14475,7 @@ async function run(options) {
|
|
|
13849
14475
|
},
|
|
13850
14476
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
13851
14477
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
13852
|
-
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
14478
|
+
forcedOnHint: state.opencodeApiVersion === "v2" ? "OpenCode V2 keeps its connected ChatGPT login in a store Evident cannot read" : "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
13853
14479
|
firstDelayMs: firstReportDelayMs,
|
|
13854
14480
|
nextDelayMs: usageReportDelayMs,
|
|
13855
14481
|
failureLogLevel: usageReportFailureLogLevel
|
|
@@ -13892,7 +14518,7 @@ async function run(options) {
|
|
|
13892
14518
|
}
|
|
13893
14519
|
|
|
13894
14520
|
// src/index.ts
|
|
13895
|
-
var
|
|
14521
|
+
var version = getCliVersion();
|
|
13896
14522
|
var program = new Command();
|
|
13897
14523
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
13898
14524
|
"--endpoint <url>",
|