@evident-ai/cli 3.5.1 → 3.5.2-dev.07822ab
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 +893 -260
- 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.
|
|
1160
|
+
var CLI_VERSION = (true ? "3.5.2-dev.07822ab" : 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
|
};
|
|
@@ -1310,26 +1549,85 @@ function parseOpenCodeLogLine(line) {
|
|
|
1310
1549
|
if (!levelMatch) return null;
|
|
1311
1550
|
const level = levelMatch[1].toUpperCase();
|
|
1312
1551
|
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1313
|
-
const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
|
|
1314
|
-
|
|
1552
|
+
const sessionMatch = normalisedLine.match(/(?:^|\s)(?:sessionID|session\.id)=(\S+)/);
|
|
1553
|
+
const messageIDMatch = normalisedLine.match(/(?:^|\s)messageID=(\S+)/);
|
|
1554
|
+
const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
|
|
1555
|
+
const parsed = {
|
|
1556
|
+
level: level === "WARN" ? "warn" : "error",
|
|
1557
|
+
sessionID: sessionMatch?.[1],
|
|
1558
|
+
messageID: messageIDMatch?.[1]
|
|
1559
|
+
};
|
|
1560
|
+
const message = messageMatch?.[1] ?? messageMatch?.[2];
|
|
1561
|
+
if (message !== void 0) parsed.message = message;
|
|
1562
|
+
return parsed;
|
|
1315
1563
|
}
|
|
1316
1564
|
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1317
|
-
|
|
1565
|
+
var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
|
|
1566
|
+
function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
|
|
1318
1567
|
let buffer = Buffer.alloc(0);
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1568
|
+
const repeatStates = /* @__PURE__ */ new Map();
|
|
1569
|
+
const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
|
|
1570
|
+
const metadata = {
|
|
1571
|
+
...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
|
|
1572
|
+
...parsed.messageID ? { messageID: parsed.messageID } : {},
|
|
1573
|
+
...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
|
|
1574
|
+
};
|
|
1322
1575
|
forwardRunnerActivity(
|
|
1323
1576
|
{
|
|
1324
1577
|
level: parsed.level,
|
|
1325
1578
|
error: line,
|
|
1326
|
-
metadata:
|
|
1579
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
|
|
1327
1580
|
source: "opencode"
|
|
1328
1581
|
},
|
|
1329
1582
|
getContext()
|
|
1330
1583
|
);
|
|
1331
1584
|
};
|
|
1332
|
-
|
|
1585
|
+
const expireRepeatStates = (now, currentKey) => {
|
|
1586
|
+
for (const [key, state] of repeatStates) {
|
|
1587
|
+
if (key === currentKey || now - state.lastForwardedAt < REPEAT_SUPPRESSION_WINDOW_MS) {
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (state.repeatsSuppressed > 0) {
|
|
1591
|
+
forwardLine(state.line, state.parsed, state.repeatsSuppressed);
|
|
1592
|
+
}
|
|
1593
|
+
repeatStates.delete(key);
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
const flushLine = (line) => {
|
|
1597
|
+
if (onSessionStoreWriteFailure && isSessionStoreWriteFailure(line)) {
|
|
1598
|
+
try {
|
|
1599
|
+
onSessionStoreWriteFailure();
|
|
1600
|
+
} catch (err) {
|
|
1601
|
+
reportTelemetryDiagnostic(
|
|
1602
|
+
`Runner activity telemetry could not record session-store write failure: ${err instanceof Error ? err.message : String(err)}`
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
const parsed = parseOpenCodeLogLine(line);
|
|
1607
|
+
if (!parsed) return;
|
|
1608
|
+
const now = Date.now();
|
|
1609
|
+
const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}\0${parsed.sessionID ?? ""}\0${parsed.messageID ?? ""}`;
|
|
1610
|
+
expireRepeatStates(now, key);
|
|
1611
|
+
if (key === void 0) {
|
|
1612
|
+
forwardLine(line, parsed);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
const previous = repeatStates.get(key);
|
|
1616
|
+
if (!previous || now - previous.lastForwardedAt >= REPEAT_SUPPRESSION_WINDOW_MS) {
|
|
1617
|
+
forwardLine(line, parsed, previous?.repeatsSuppressed ?? 0);
|
|
1618
|
+
repeatStates.set(key, {
|
|
1619
|
+
line,
|
|
1620
|
+
parsed,
|
|
1621
|
+
lastForwardedAt: now,
|
|
1622
|
+
repeatsSuppressed: 0
|
|
1623
|
+
});
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
previous.line = line;
|
|
1627
|
+
previous.parsed = parsed;
|
|
1628
|
+
previous.repeatsSuppressed++;
|
|
1629
|
+
};
|
|
1630
|
+
const forward = ((chunk) => {
|
|
1333
1631
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
|
|
1334
1632
|
let newlineIndex;
|
|
1335
1633
|
while ((newlineIndex = buffer.indexOf(10)) !== -1) {
|
|
@@ -1340,14 +1638,44 @@ function createOpenCodeActivityForwarder(getContext) {
|
|
|
1340
1638
|
flushLine(buffer.toString("utf-8"));
|
|
1341
1639
|
buffer = Buffer.alloc(0);
|
|
1342
1640
|
}
|
|
1641
|
+
});
|
|
1642
|
+
forward.flush = () => {
|
|
1643
|
+
for (const state of repeatStates.values()) {
|
|
1644
|
+
if (state.repeatsSuppressed > 0) {
|
|
1645
|
+
forwardLine(state.line, state.parsed, state.repeatsSuppressed);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
repeatStates.clear();
|
|
1343
1649
|
};
|
|
1650
|
+
return forward;
|
|
1344
1651
|
}
|
|
1345
1652
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1346
1653
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1347
1654
|
var MAX_METADATA_ENTRIES = 20;
|
|
1348
1655
|
var TRUNCATION_MARKER = "\u2026";
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1656
|
+
function redactHomeDirectory(message) {
|
|
1657
|
+
const home = homedir2();
|
|
1658
|
+
if (!home || home === "/") {
|
|
1659
|
+
return message;
|
|
1660
|
+
}
|
|
1661
|
+
const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
|
|
1662
|
+
let result = "";
|
|
1663
|
+
let searchFrom = 0;
|
|
1664
|
+
let homeIndex;
|
|
1665
|
+
while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
|
|
1666
|
+
const homeEnd = homeIndex + home.length;
|
|
1667
|
+
const previousCharacter = message[homeIndex - 1];
|
|
1668
|
+
const nextCharacter = message[homeEnd];
|
|
1669
|
+
const atPathBoundary = home === "/" || (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
|
|
1670
|
+
result += message.slice(searchFrom, homeIndex);
|
|
1671
|
+
result += atPathBoundary ? "~" : home;
|
|
1672
|
+
searchFrom = homeEnd;
|
|
1673
|
+
}
|
|
1674
|
+
return result + message.slice(searchFrom);
|
|
1675
|
+
}
|
|
1676
|
+
function redactDiagnosticText(message) {
|
|
1677
|
+
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>");
|
|
1678
|
+
return redactHomeDirectory(redacted);
|
|
1351
1679
|
}
|
|
1352
1680
|
function truncate(message) {
|
|
1353
1681
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1357,14 +1685,17 @@ function sanitiseMetadata(metadata) {
|
|
|
1357
1685
|
if (!metadata) return {};
|
|
1358
1686
|
const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
|
|
1359
1687
|
if (Object.keys(metadata).length > entries.length) {
|
|
1360
|
-
|
|
1361
|
-
`
|
|
1688
|
+
reportTelemetryDiagnostic(
|
|
1689
|
+
`Runner activity telemetry dropped ${Object.keys(metadata).length - entries.length} metadata entries; those entries were not sent`
|
|
1362
1690
|
);
|
|
1363
1691
|
}
|
|
1364
1692
|
const sanitised = [];
|
|
1365
1693
|
for (const [key, value] of entries) {
|
|
1366
1694
|
if (typeof value === "string") {
|
|
1367
|
-
sanitised.push([
|
|
1695
|
+
sanitised.push([
|
|
1696
|
+
key,
|
|
1697
|
+
truncate(redactDiagnosticText(value).slice(0, MAX_METADATA_VALUE_LENGTH))
|
|
1698
|
+
]);
|
|
1368
1699
|
} else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
1369
1700
|
sanitised.push([key, value]);
|
|
1370
1701
|
}
|
|
@@ -1382,8 +1713,8 @@ function admitUnderRateLimit(source, now) {
|
|
|
1382
1713
|
}
|
|
1383
1714
|
if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
|
|
1384
1715
|
if (window.windowDroppedCount > 0) {
|
|
1385
|
-
|
|
1386
|
-
`
|
|
1716
|
+
reportTelemetryDiagnostic(
|
|
1717
|
+
`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
1718
|
);
|
|
1388
1719
|
}
|
|
1389
1720
|
window.windowStartedAt = now;
|
|
@@ -1393,8 +1724,8 @@ function admitUnderRateLimit(source, now) {
|
|
|
1393
1724
|
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1394
1725
|
window.windowDroppedCount++;
|
|
1395
1726
|
if (window.windowDroppedCount === 1) {
|
|
1396
|
-
|
|
1397
|
-
`
|
|
1727
|
+
reportTelemetryDiagnostic(
|
|
1728
|
+
`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
1729
|
);
|
|
1399
1730
|
}
|
|
1400
1731
|
return false;
|
|
@@ -1404,12 +1735,12 @@ function admitUnderRateLimit(source, now) {
|
|
|
1404
1735
|
}
|
|
1405
1736
|
function forwardRunnerActivity(entry, context) {
|
|
1406
1737
|
try {
|
|
1407
|
-
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1408
|
-
if (!context.agentId || !context.authHeader) return;
|
|
1409
1738
|
const source = entry.source ?? "cli.run";
|
|
1739
|
+
if (!FORWARDED_LEVELS_BY_SOURCE[source].has(entry.level)) return;
|
|
1740
|
+
if (!context.agentId || !context.authHeader) return;
|
|
1410
1741
|
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1411
1742
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1412
|
-
const message = truncate(
|
|
1743
|
+
const message = truncate(redactDiagnosticText(rawMessage));
|
|
1413
1744
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1414
1745
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1415
1746
|
message,
|
|
@@ -1417,8 +1748,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1417
1748
|
agentId: context.agentId
|
|
1418
1749
|
});
|
|
1419
1750
|
} catch (err) {
|
|
1420
|
-
|
|
1421
|
-
`
|
|
1751
|
+
reportTelemetryDiagnostic(
|
|
1752
|
+
`Runner activity telemetry failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
|
|
1422
1753
|
);
|
|
1423
1754
|
}
|
|
1424
1755
|
}
|
|
@@ -1708,8 +2039,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
|
1708
2039
|
|
|
1709
2040
|
// src/lib/http-timeout.ts
|
|
1710
2041
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
2042
|
+
var RequestTimeoutError = class extends Error {
|
|
2043
|
+
constructor(operation, timeoutMs, elapsedMs) {
|
|
2044
|
+
super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
|
|
2045
|
+
this.operation = operation;
|
|
2046
|
+
this.timeoutMs = timeoutMs;
|
|
2047
|
+
this.elapsedMs = elapsedMs;
|
|
2048
|
+
this.name = "TimeoutError";
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
function describeRequestOperation(input, init) {
|
|
2052
|
+
const method = (init?.method ?? input.method ?? "GET").toUpperCase();
|
|
2053
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
2054
|
+
let path;
|
|
2055
|
+
try {
|
|
2056
|
+
path = new URL(rawUrl).pathname;
|
|
2057
|
+
} catch (error2) {
|
|
2058
|
+
if (!(error2 instanceof TypeError)) throw error2;
|
|
2059
|
+
path = rawUrl.split(/[?#]/, 1)[0];
|
|
2060
|
+
}
|
|
2061
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2062
|
+
const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
|
|
2063
|
+
const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
|
|
2064
|
+
return `${method} ${maskedPath}`;
|
|
2065
|
+
}
|
|
1711
2066
|
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1712
|
-
return ((input, init) =>
|
|
2067
|
+
return (async (input, init) => {
|
|
2068
|
+
const startedAt = Date.now();
|
|
2069
|
+
try {
|
|
2070
|
+
return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
2071
|
+
} catch (error2) {
|
|
2072
|
+
if (error2?.name === "TimeoutError") {
|
|
2073
|
+
throw new RequestTimeoutError(
|
|
2074
|
+
describeRequestOperation(input, init),
|
|
2075
|
+
timeoutMs,
|
|
2076
|
+
Date.now() - startedAt
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
throw error2;
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
1713
2082
|
}
|
|
1714
2083
|
|
|
1715
2084
|
// src/lib/opencode/client.ts
|
|
@@ -1863,6 +2232,7 @@ function isSessionActivelyGenerating(messages) {
|
|
|
1863
2232
|
if (roleOf(last) !== "assistant") return false;
|
|
1864
2233
|
return completedOf(last) == null;
|
|
1865
2234
|
}
|
|
2235
|
+
var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
|
|
1866
2236
|
function sessionLastActivityMs(session) {
|
|
1867
2237
|
const candidates = [
|
|
1868
2238
|
session.time?.updated,
|
|
@@ -1877,30 +2247,60 @@ function sessionLastActivityMs(session) {
|
|
|
1877
2247
|
}
|
|
1878
2248
|
return null;
|
|
1879
2249
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
2250
|
+
function sessionListPath(options) {
|
|
2251
|
+
const params = new URLSearchParams();
|
|
2252
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
2253
|
+
const query = params.toString();
|
|
2254
|
+
return query ? `/session?${query}` : "/session";
|
|
2255
|
+
}
|
|
2256
|
+
async function listSessionPage(port, client, options) {
|
|
2257
|
+
const path = sessionListPath(options);
|
|
1882
2258
|
try {
|
|
1883
|
-
const res = await requestWithClient(port, client,
|
|
2259
|
+
const res = await requestWithClient(port, client, path);
|
|
1884
2260
|
if (!res.ok) return null;
|
|
1885
2261
|
const body = await res.json();
|
|
1886
2262
|
return Array.isArray(body) ? body : null;
|
|
1887
2263
|
} catch (error2) {
|
|
1888
2264
|
console.error(
|
|
1889
|
-
`[
|
|
2265
|
+
`[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1890
2266
|
);
|
|
1891
2267
|
return null;
|
|
1892
2268
|
}
|
|
1893
2269
|
}
|
|
2270
|
+
async function listSessions(port, client, options) {
|
|
2271
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2272
|
+
return listSessionPage(port, client, options);
|
|
2273
|
+
}
|
|
2274
|
+
async function listAllSessions(port, client) {
|
|
2275
|
+
if (client?.version === "v2") {
|
|
2276
|
+
const sessions2 = await listV2Sessions(client);
|
|
2277
|
+
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
2278
|
+
}
|
|
2279
|
+
const sessions = await listSessionPage(port, client, {
|
|
2280
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
2281
|
+
});
|
|
2282
|
+
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
2283
|
+
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
2284
|
+
return { sessions: null, reason: "session-enumeration-limit-bound" };
|
|
2285
|
+
}
|
|
2286
|
+
return { sessions };
|
|
2287
|
+
}
|
|
1894
2288
|
async function deleteSession(port, id, client) {
|
|
1895
2289
|
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
1896
2290
|
try {
|
|
1897
|
-
const res = await requestWithClient(
|
|
1898
|
-
|
|
2291
|
+
const res = await requestWithClient(
|
|
2292
|
+
port,
|
|
2293
|
+
client,
|
|
2294
|
+
`/session/${id}`,
|
|
2295
|
+
{ method: "DELETE" },
|
|
2296
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
2297
|
+
);
|
|
2298
|
+
return { ok: res.status >= 200 && res.status < 300, status: res.status };
|
|
1899
2299
|
} catch (error2) {
|
|
1900
2300
|
console.error(
|
|
1901
2301
|
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1902
2302
|
);
|
|
1903
|
-
return false;
|
|
2303
|
+
return { ok: false, status: "threw" };
|
|
1904
2304
|
}
|
|
1905
2305
|
}
|
|
1906
2306
|
async function sessionExists(port, id, client) {
|
|
@@ -3033,13 +3433,17 @@ async function listV2Sessions(client) {
|
|
|
3033
3433
|
}
|
|
3034
3434
|
async function deleteV2Session(client, sessionId) {
|
|
3035
3435
|
try {
|
|
3036
|
-
await client.request(
|
|
3037
|
-
|
|
3436
|
+
const response = await client.request(
|
|
3437
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
3438
|
+
{ method: "DELETE" },
|
|
3439
|
+
{ allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
|
|
3440
|
+
);
|
|
3441
|
+
return { ok: response.status >= 200 && response.status < 300, status: response.status };
|
|
3038
3442
|
} catch (error2) {
|
|
3039
3443
|
console.error(
|
|
3040
3444
|
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3041
3445
|
);
|
|
3042
|
-
return false;
|
|
3446
|
+
return { ok: false, status: "threw" };
|
|
3043
3447
|
}
|
|
3044
3448
|
}
|
|
3045
3449
|
async function v2SessionExists(client, sessionId) {
|
|
@@ -3214,7 +3618,7 @@ import { join as join4 } from "node:path";
|
|
|
3214
3618
|
|
|
3215
3619
|
// src/lib/openai-usage.ts
|
|
3216
3620
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3217
|
-
import { homedir as
|
|
3621
|
+
import { homedir as homedir3 } from "node:os";
|
|
3218
3622
|
import { join as join3 } from "node:path";
|
|
3219
3623
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3220
3624
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
@@ -3229,7 +3633,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
3229
3633
|
}
|
|
3230
3634
|
function readOpenCodeChatGptCredentials() {
|
|
3231
3635
|
try {
|
|
3232
|
-
const raw = readFileSync3(join3(
|
|
3636
|
+
const raw = readFileSync3(join3(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3233
3637
|
let parsed;
|
|
3234
3638
|
try {
|
|
3235
3639
|
parsed = JSON.parse(raw);
|
|
@@ -3370,16 +3774,17 @@ function hasPrimaryHeaders(headers) {
|
|
|
3370
3774
|
].some((name) => headers.has(name));
|
|
3371
3775
|
}
|
|
3372
3776
|
async function getOpenAiUsage(port, client) {
|
|
3777
|
+
const openCodeVersion = client?.version ?? "v1";
|
|
3373
3778
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
3374
3779
|
if (!credentials2) {
|
|
3375
3780
|
throw new OpenAiUsageError(
|
|
3376
|
-
"No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3781
|
+
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
3782
|
"no_credentials"
|
|
3378
3783
|
);
|
|
3379
3784
|
}
|
|
3380
3785
|
if (credentials2.expiresAt < Date.now()) {
|
|
3381
3786
|
throw new OpenAiUsageError(
|
|
3382
|
-
"ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3787
|
+
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
3788
|
"credentials_expired"
|
|
3384
3789
|
);
|
|
3385
3790
|
}
|
|
@@ -3660,9 +4065,9 @@ async function importOpenCodeV2Credentials({
|
|
|
3660
4065
|
|
|
3661
4066
|
// src/lib/opencode/session-db-boot.ts
|
|
3662
4067
|
import { spawn as spawn2 } from "node:child_process";
|
|
3663
|
-
import { mkdirSync, statSync as
|
|
3664
|
-
import { homedir as
|
|
3665
|
-
import { dirname as
|
|
4068
|
+
import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
4069
|
+
import { homedir as homedir4 } from "node:os";
|
|
4070
|
+
import { dirname as dirname3, resolve as resolvePath } from "node:path";
|
|
3666
4071
|
|
|
3667
4072
|
// src/lib/runner-synchroniser.ts
|
|
3668
4073
|
import { spawn } from "node:child_process";
|
|
@@ -3787,7 +4192,7 @@ function clearMarker(options) {
|
|
|
3787
4192
|
function markNoReplicate(options, message) {
|
|
3788
4193
|
if (options.noReplicateMarker) {
|
|
3789
4194
|
try {
|
|
3790
|
-
mkdirSync(
|
|
4195
|
+
mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
|
|
3791
4196
|
writeFileSync(options.noReplicateMarker, "");
|
|
3792
4197
|
} catch (error2) {
|
|
3793
4198
|
options.log(
|
|
@@ -3912,7 +4317,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
3912
4317
|
return null;
|
|
3913
4318
|
}
|
|
3914
4319
|
try {
|
|
3915
|
-
if (
|
|
4320
|
+
if (statSync3(configPath).size > 0) return configPath;
|
|
3916
4321
|
} catch (error2) {
|
|
3917
4322
|
if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
|
|
3918
4323
|
options.log(
|
|
@@ -3942,7 +4347,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
3942
4347
|
return null;
|
|
3943
4348
|
}
|
|
3944
4349
|
try {
|
|
3945
|
-
mkdirSync(
|
|
4350
|
+
mkdirSync(dirname3(configPath), { recursive: true });
|
|
3946
4351
|
writeFileSync(configPath, rendered.stdout);
|
|
3947
4352
|
} catch (error2) {
|
|
3948
4353
|
options.log(
|
|
@@ -4017,14 +4422,14 @@ async function restoreSessionDb(options, configPath, env) {
|
|
|
4017
4422
|
case 0:
|
|
4018
4423
|
return;
|
|
4019
4424
|
case 31:
|
|
4020
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4425
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4021
4426
|
options.log(
|
|
4022
4427
|
"SESSION-DB-REPLICA-UNUSABLE: booting with a fresh opencode.db and replicating into the existing prefix this boot",
|
|
4023
4428
|
"warn"
|
|
4024
4429
|
);
|
|
4025
4430
|
return;
|
|
4026
4431
|
case 32:
|
|
4027
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4432
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4028
4433
|
discardSessionDbDebris(options);
|
|
4029
4434
|
markNoReplicate(
|
|
4030
4435
|
options,
|
|
@@ -4103,7 +4508,7 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4103
4508
|
}
|
|
4104
4509
|
function fileExists(path) {
|
|
4105
4510
|
try {
|
|
4106
|
-
|
|
4511
|
+
statSync3(path);
|
|
4107
4512
|
return true;
|
|
4108
4513
|
} catch (error2) {
|
|
4109
4514
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
|
|
@@ -4113,7 +4518,7 @@ function fileExists(path) {
|
|
|
4113
4518
|
async function restoreAndVerifySessionDb(options) {
|
|
4114
4519
|
const env = options.env ?? process.env;
|
|
4115
4520
|
clearMarker(options);
|
|
4116
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4521
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4117
4522
|
const synchroniserEnv = await runSynchroniser(["env"], {
|
|
4118
4523
|
timeoutMs: SESSION_DB_SYNCHRONISER_TIMEOUT_MS,
|
|
4119
4524
|
env
|
|
@@ -4192,7 +4597,7 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
4192
4597
|
// src/lib/opencode/session-db-provenance.ts
|
|
4193
4598
|
import { createRequire } from "node:module";
|
|
4194
4599
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4195
|
-
import { dirname as
|
|
4600
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
4196
4601
|
var require2 = createRequire(import.meta.url);
|
|
4197
4602
|
function readSessionDbMigrationIds(dbPath) {
|
|
4198
4603
|
let db;
|
|
@@ -4258,7 +4663,7 @@ function loadSessionDbProvenanceState(path) {
|
|
|
4258
4663
|
}
|
|
4259
4664
|
function saveSessionDbProvenanceState(path, state) {
|
|
4260
4665
|
try {
|
|
4261
|
-
mkdirSync2(
|
|
4666
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
4262
4667
|
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
4263
4668
|
`, "utf8");
|
|
4264
4669
|
} catch (error2) {
|
|
@@ -4493,12 +4898,15 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
|
4493
4898
|
}
|
|
4494
4899
|
} catch {
|
|
4495
4900
|
try {
|
|
4496
|
-
const psOutput = execSync(
|
|
4901
|
+
const psOutput = execSync("ps aux", {
|
|
4497
4902
|
encoding: "utf-8",
|
|
4498
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4903
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4904
|
+
// The whole table reaches Node; the 1 MiB default would surface a spurious failure.
|
|
4905
|
+
maxBuffer: 16 * 1024 * 1024
|
|
4499
4906
|
}).trim();
|
|
4500
4907
|
if (psOutput) {
|
|
4501
4908
|
for (const line of psOutput.split("\n")) {
|
|
4909
|
+
if (!psPattern.test(line)) continue;
|
|
4502
4910
|
const parts = line.trim().split(/\s+/);
|
|
4503
4911
|
if (parts.length >= 2) {
|
|
4504
4912
|
const pid = parseInt(parts[1], 10);
|
|
@@ -4507,8 +4915,9 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
|
4507
4915
|
}
|
|
4508
4916
|
}
|
|
4509
4917
|
} catch (err) {
|
|
4918
|
+
const reason = (typeof err === "object" && err !== null && "stderr" in err && typeof err.stderr === "string" ? err.stderr : "").split("\n").map((line) => line.trim()).find(Boolean) ?? (err instanceof Error ? err.message : String(err)).split("\n").map((line) => line.trim()).find(Boolean) ?? "no error detail available";
|
|
4510
4919
|
console.warn(
|
|
4511
|
-
`
|
|
4920
|
+
`Could not list running processes, so an already-running OpenCode may not be detected: ${reason}`
|
|
4512
4921
|
);
|
|
4513
4922
|
}
|
|
4514
4923
|
}
|
|
@@ -4540,7 +4949,7 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
|
4540
4949
|
return instances;
|
|
4541
4950
|
}
|
|
4542
4951
|
function findOpenCodeProcesses() {
|
|
4543
|
-
return findProcessesByPattern("opencode serve|opencode-serve",
|
|
4952
|
+
return findProcessesByPattern("opencode serve|opencode-serve", /opencode (serve|--port)/);
|
|
4544
4953
|
}
|
|
4545
4954
|
async function scanPortsForOpenCode() {
|
|
4546
4955
|
const instances = [];
|
|
@@ -4835,7 +5244,10 @@ function selectSessionsToDelete(sessions, opts) {
|
|
|
4835
5244
|
toDelete.push(s.id);
|
|
4836
5245
|
}
|
|
4837
5246
|
}
|
|
4838
|
-
|
|
5247
|
+
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
5248
|
+
return toDelete.sort(
|
|
5249
|
+
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
5250
|
+
);
|
|
4839
5251
|
}
|
|
4840
5252
|
var DEFAULT_INTERVAL = "1h";
|
|
4841
5253
|
function resolve(flag, envValue, fallback) {
|
|
@@ -4895,13 +5307,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
4895
5307
|
}
|
|
4896
5308
|
|
|
4897
5309
|
// src/lib/opencode/session-db-size.ts
|
|
4898
|
-
import { statSync as
|
|
5310
|
+
import { statSync as statSync4 } from "node:fs";
|
|
4899
5311
|
import { join as join6 } from "node:path";
|
|
4900
5312
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
4901
5313
|
function statSessionDbBytes(homeDir) {
|
|
4902
5314
|
const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
4903
5315
|
try {
|
|
4904
|
-
return
|
|
5316
|
+
return statSync4(dbPath).size;
|
|
4905
5317
|
} catch (err) {
|
|
4906
5318
|
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
4907
5319
|
if (!isMissingFile) {
|
|
@@ -4927,12 +5339,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
4927
5339
|
}
|
|
4928
5340
|
|
|
4929
5341
|
// src/lib/opencode/log-tail.ts
|
|
4930
|
-
import { statSync as
|
|
4931
|
-
import { homedir as
|
|
5342
|
+
import { statSync as statSync5 } from "node:fs";
|
|
5343
|
+
import { homedir as homedir5 } from "node:os";
|
|
4932
5344
|
import { join as join7 } from "node:path";
|
|
4933
5345
|
import { open as open2, stat } from "node:fs/promises";
|
|
4934
5346
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
4935
|
-
function resolveOpenCodeLogPath(homeDir =
|
|
5347
|
+
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
4936
5348
|
const dataDir = env.XDG_DATA_HOME || join7(homeDir, ".local", "share");
|
|
4937
5349
|
return join7(dataDir, "opencode", "log", "opencode.log");
|
|
4938
5350
|
}
|
|
@@ -4950,7 +5362,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
4950
5362
|
let inode = null;
|
|
4951
5363
|
let baselineReady = true;
|
|
4952
5364
|
try {
|
|
4953
|
-
const initial =
|
|
5365
|
+
const initial = statSync5(logPath);
|
|
4954
5366
|
offset = initial.size;
|
|
4955
5367
|
inode = initial.ino;
|
|
4956
5368
|
} catch (error2) {
|
|
@@ -5009,125 +5421,6 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
5009
5421
|
};
|
|
5010
5422
|
}
|
|
5011
5423
|
|
|
5012
|
-
// src/lib/opencode/session-db-reclaim.ts
|
|
5013
|
-
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
5014
|
-
import { dirname as dirname4 } from "node:path";
|
|
5015
|
-
function errorMessage(error2) {
|
|
5016
|
-
if (!(error2 instanceof Error)) return String(error2);
|
|
5017
|
-
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
5018
|
-
}
|
|
5019
|
-
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
5020
|
-
try {
|
|
5021
|
-
const fsStats = statfsSync(dirname4(dbPath));
|
|
5022
|
-
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
5023
|
-
if (availableBytes < requiredBytes) {
|
|
5024
|
-
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
5025
|
-
}
|
|
5026
|
-
return null;
|
|
5027
|
-
} catch (err) {
|
|
5028
|
-
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
5029
|
-
}
|
|
5030
|
-
}
|
|
5031
|
-
function readLogicalBytes(db) {
|
|
5032
|
-
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
5033
|
-
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
5034
|
-
return pageCount * pageSize;
|
|
5035
|
-
}
|
|
5036
|
-
function readCheckpointResult(db) {
|
|
5037
|
-
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
5038
|
-
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
5039
|
-
}
|
|
5040
|
-
async function probeReclaimAvailability(input) {
|
|
5041
|
-
const { dbPath, requiredBytes } = input;
|
|
5042
|
-
let sqlite;
|
|
5043
|
-
try {
|
|
5044
|
-
sqlite = await import("node:sqlite");
|
|
5045
|
-
} catch (err) {
|
|
5046
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5047
|
-
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
5048
|
-
return { reason: "sqlite-unavailable", detail };
|
|
5049
|
-
}
|
|
5050
|
-
let autoVacuum = null;
|
|
5051
|
-
try {
|
|
5052
|
-
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
5053
|
-
try {
|
|
5054
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5055
|
-
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5056
|
-
} finally {
|
|
5057
|
-
db.close();
|
|
5058
|
-
}
|
|
5059
|
-
} catch (err) {
|
|
5060
|
-
console.warn(
|
|
5061
|
-
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
5062
|
-
);
|
|
5063
|
-
}
|
|
5064
|
-
if (autoVacuum !== 0) return null;
|
|
5065
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
5066
|
-
}
|
|
5067
|
-
async function reclaimSessionDbSpace(input) {
|
|
5068
|
-
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
5069
|
-
let sqlite;
|
|
5070
|
-
try {
|
|
5071
|
-
sqlite = await import("node:sqlite");
|
|
5072
|
-
} catch (err) {
|
|
5073
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5074
|
-
console.warn(
|
|
5075
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
5076
|
-
);
|
|
5077
|
-
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
5078
|
-
}
|
|
5079
|
-
const { DatabaseSync } = sqlite;
|
|
5080
|
-
let db;
|
|
5081
|
-
try {
|
|
5082
|
-
db = new DatabaseSync(dbPath);
|
|
5083
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5084
|
-
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5085
|
-
if (autoVacuum === 0) {
|
|
5086
|
-
if (!allowFullVacuum) {
|
|
5087
|
-
console.warn(
|
|
5088
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
5089
|
-
);
|
|
5090
|
-
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
5091
|
-
}
|
|
5092
|
-
const fileBytesForGuard = statSync5(dbPath).size;
|
|
5093
|
-
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
5094
|
-
if (skipReason !== null) {
|
|
5095
|
-
console.warn(
|
|
5096
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
5097
|
-
);
|
|
5098
|
-
return { ok: false, skipped: "insufficient-disk-space" };
|
|
5099
|
-
}
|
|
5100
|
-
const beforeBytes = readLogicalBytes(db);
|
|
5101
|
-
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
5102
|
-
db.exec("VACUUM");
|
|
5103
|
-
const afterBytes = readLogicalBytes(db);
|
|
5104
|
-
const checkpoint = readCheckpointResult(db);
|
|
5105
|
-
return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
|
|
5106
|
-
}
|
|
5107
|
-
if (autoVacuum === 2) {
|
|
5108
|
-
const beforeBytes = readLogicalBytes(db);
|
|
5109
|
-
const bound = Math.max(0, Math.trunc(maxPages));
|
|
5110
|
-
db.exec(`PRAGMA incremental_vacuum(${bound})`);
|
|
5111
|
-
const afterBytes = readLogicalBytes(db);
|
|
5112
|
-
const checkpoint = readCheckpointResult(db);
|
|
5113
|
-
return { ok: true, mode: "incremental", beforeBytes, afterBytes, checkpoint };
|
|
5114
|
-
}
|
|
5115
|
-
console.warn(
|
|
5116
|
-
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
5117
|
-
);
|
|
5118
|
-
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
5119
|
-
} catch (err) {
|
|
5120
|
-
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
5121
|
-
return {
|
|
5122
|
-
ok: false,
|
|
5123
|
-
skipped: "reclaim-error",
|
|
5124
|
-
detail: errorMessage(err)
|
|
5125
|
-
};
|
|
5126
|
-
} finally {
|
|
5127
|
-
db?.close();
|
|
5128
|
-
}
|
|
5129
|
-
}
|
|
5130
|
-
|
|
5131
5424
|
// src/lib/tunnel/connection.ts
|
|
5132
5425
|
import WebSocket2 from "ws";
|
|
5133
5426
|
|
|
@@ -5357,6 +5650,8 @@ var StreamForwarder = class {
|
|
|
5357
5650
|
if (done) break;
|
|
5358
5651
|
const chunk = Buffer.from(value);
|
|
5359
5652
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
5653
|
+
await stream.pausePromise;
|
|
5654
|
+
if (ac.signal.aborted) break;
|
|
5360
5655
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
5361
5656
|
if (this.binaryFramesSupported) {
|
|
5362
5657
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
@@ -5399,6 +5694,7 @@ function classifyUpgradeRejection(headers) {
|
|
|
5399
5694
|
}
|
|
5400
5695
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
5401
5696
|
var BASE_RECONNECT_DELAY = 500;
|
|
5697
|
+
var RELAY_SILENCE_TIMEOUT_MS = 9e4;
|
|
5402
5698
|
function getReconnectDelay(attempt) {
|
|
5403
5699
|
const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
|
|
5404
5700
|
const jitter = Math.random() * 1e3;
|
|
@@ -5470,10 +5766,27 @@ function connectTunnel(options) {
|
|
|
5470
5766
|
ws.close();
|
|
5471
5767
|
reject(new Error("Connection timeout"));
|
|
5472
5768
|
}, 3e4);
|
|
5769
|
+
let relaySilenceTimeout;
|
|
5770
|
+
let relaySilenceTimedOut = false;
|
|
5771
|
+
const clearRelaySilenceTimeout = () => {
|
|
5772
|
+
if (relaySilenceTimeout === void 0) return;
|
|
5773
|
+
clearTimeout(relaySilenceTimeout);
|
|
5774
|
+
relaySilenceTimeout = void 0;
|
|
5775
|
+
};
|
|
5776
|
+
const armRelaySilenceTimeout = () => {
|
|
5777
|
+
clearRelaySilenceTimeout();
|
|
5778
|
+
relaySilenceTimeout = setTimeout(() => {
|
|
5779
|
+
relaySilenceTimeout = void 0;
|
|
5780
|
+
relaySilenceTimedOut = true;
|
|
5781
|
+
ws.terminate();
|
|
5782
|
+
}, RELAY_SILENCE_TIMEOUT_MS);
|
|
5783
|
+
relaySilenceTimeout.unref?.();
|
|
5784
|
+
};
|
|
5473
5785
|
let upgradeRejection = null;
|
|
5474
5786
|
let upgradeRejectionReason = null;
|
|
5475
5787
|
ws.on("unexpected-response", (_req, res) => {
|
|
5476
5788
|
clearTimeout(connectionTimeout);
|
|
5789
|
+
clearRelaySilenceTimeout();
|
|
5477
5790
|
const reason = classifyUpgradeRejection(res.headers);
|
|
5478
5791
|
upgradeRejectionReason = reason;
|
|
5479
5792
|
const chunks = [];
|
|
@@ -5503,6 +5816,7 @@ function connectTunnel(options) {
|
|
|
5503
5816
|
onInfo?.("WebSocket connection established");
|
|
5504
5817
|
});
|
|
5505
5818
|
ws.on("message", (data, isBinary) => {
|
|
5819
|
+
if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
|
|
5506
5820
|
if (isBinary) {
|
|
5507
5821
|
try {
|
|
5508
5822
|
const frame = decodeBinaryBodyFrame(toUint8Array(data));
|
|
@@ -5536,6 +5850,7 @@ function connectTunnel(options) {
|
|
|
5536
5850
|
switch (message.type) {
|
|
5537
5851
|
case "connected": {
|
|
5538
5852
|
clearTimeout(connectionTimeout);
|
|
5853
|
+
armRelaySilenceTimeout();
|
|
5539
5854
|
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
5540
5855
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
5541
5856
|
onConnected?.(connectedAgentId);
|
|
@@ -5560,6 +5875,7 @@ function connectTunnel(options) {
|
|
|
5560
5875
|
});
|
|
5561
5876
|
ws.on("error", (error2) => {
|
|
5562
5877
|
clearTimeout(connectionTimeout);
|
|
5878
|
+
clearRelaySilenceTimeout();
|
|
5563
5879
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
5564
5880
|
if (upgradeRejectionReason === "do_code_updated") {
|
|
5565
5881
|
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
@@ -5571,7 +5887,8 @@ function connectTunnel(options) {
|
|
|
5571
5887
|
);
|
|
5572
5888
|
});
|
|
5573
5889
|
ws.on("close", (code, reason) => {
|
|
5574
|
-
|
|
5890
|
+
clearRelaySilenceTimeout();
|
|
5891
|
+
const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
5575
5892
|
forwarder.abortAll();
|
|
5576
5893
|
onDisconnected?.(code, reasonStr);
|
|
5577
5894
|
});
|
|
@@ -5579,9 +5896,10 @@ function connectTunnel(options) {
|
|
|
5579
5896
|
}
|
|
5580
5897
|
|
|
5581
5898
|
// src/lib/tunnel/runner-connection.ts
|
|
5582
|
-
var RunnerConnection = class {
|
|
5899
|
+
var RunnerConnection = class _RunnerConnection {
|
|
5583
5900
|
opts;
|
|
5584
5901
|
sleep;
|
|
5902
|
+
now;
|
|
5585
5903
|
connection = null;
|
|
5586
5904
|
resolvedAgentId;
|
|
5587
5905
|
/** True while a (re)connect loop is in flight. */
|
|
@@ -5590,10 +5908,17 @@ var RunnerConnection = class {
|
|
|
5590
5908
|
reconnectPromise = null;
|
|
5591
5909
|
/** 1-based count of the current reconnect attempt streak. */
|
|
5592
5910
|
reconnectAttempt = 0;
|
|
5911
|
+
generation = 0;
|
|
5912
|
+
disconnectedAtMs = null;
|
|
5913
|
+
static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
|
|
5914
|
+
"No pong received",
|
|
5915
|
+
"Orphaned socket"
|
|
5916
|
+
]);
|
|
5593
5917
|
constructor(opts) {
|
|
5594
5918
|
this.opts = opts;
|
|
5595
5919
|
this.resolvedAgentId = opts.agentId;
|
|
5596
5920
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5921
|
+
this.now = opts.now ?? Date.now;
|
|
5597
5922
|
}
|
|
5598
5923
|
get agentId() {
|
|
5599
5924
|
return this.resolvedAgentId;
|
|
@@ -5629,14 +5954,24 @@ var RunnerConnection = class {
|
|
|
5629
5954
|
port: this.opts.port,
|
|
5630
5955
|
openCodePassword: this.opts.openCodePassword,
|
|
5631
5956
|
onConnected: (agentId) => {
|
|
5957
|
+
const attempt = this.reconnectAttempt + 1;
|
|
5632
5958
|
this.reconnectAttempt = 0;
|
|
5633
5959
|
this.reconnecting = false;
|
|
5634
5960
|
this.resolvedAgentId = agentId;
|
|
5635
|
-
|
|
5961
|
+
this.generation++;
|
|
5962
|
+
const downtimeMs = this.disconnectedAtMs === null ? null : this.now() - this.disconnectedAtMs;
|
|
5963
|
+
this.disconnectedAtMs = null;
|
|
5964
|
+
events.onConnected(agentId, isReconnect, {
|
|
5965
|
+
generation: this.generation,
|
|
5966
|
+
attempt,
|
|
5967
|
+
downtimeMs
|
|
5968
|
+
});
|
|
5636
5969
|
},
|
|
5637
5970
|
onDisconnected: (code, reason) => {
|
|
5638
|
-
|
|
5639
|
-
|
|
5971
|
+
this.disconnectedAtMs = this.now();
|
|
5972
|
+
events.onDisconnected(code, reason, { generation: this.generation });
|
|
5973
|
+
const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
|
|
5974
|
+
if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
|
|
5640
5975
|
this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
|
|
5641
5976
|
events.onError?.(`Reconnection failed: ${err.message}`);
|
|
5642
5977
|
});
|
|
@@ -5975,8 +6310,85 @@ function createResourceUsageCollector(homeDir) {
|
|
|
5975
6310
|
};
|
|
5976
6311
|
}
|
|
5977
6312
|
|
|
6313
|
+
// src/lib/resource-usage-low-disk.ts
|
|
6314
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_BYTES = 4 * 1024 ** 3;
|
|
6315
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION = 0.25;
|
|
6316
|
+
var RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES = 1 * 1024 ** 3;
|
|
6317
|
+
var RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES = 256 * 1024 ** 2;
|
|
6318
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES = 6 * 1024 ** 3;
|
|
6319
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION = 0.375;
|
|
6320
|
+
var LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
6321
|
+
var BAND_RANK = {
|
|
6322
|
+
low: 1,
|
|
6323
|
+
very_low: 2,
|
|
6324
|
+
critical: 3
|
|
6325
|
+
};
|
|
6326
|
+
function bandFor(freeBytes) {
|
|
6327
|
+
if (freeBytes < RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES) return "critical";
|
|
6328
|
+
if (freeBytes < RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES) return "very_low";
|
|
6329
|
+
return "low";
|
|
6330
|
+
}
|
|
6331
|
+
function formatBytes(bytes) {
|
|
6332
|
+
if (bytes === null) return "unavailable";
|
|
6333
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
6334
|
+
const unitIndex = Math.min(Math.floor(Math.log2(Math.max(bytes, 1)) / 10), units.length - 1);
|
|
6335
|
+
const value = bytes / 1024 ** unitIndex;
|
|
6336
|
+
const digits = unitIndex === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 2;
|
|
6337
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
6338
|
+
}
|
|
6339
|
+
function warningMessage(band, diskFreeBytes, diskTotalBytes, opencodeDbBytes) {
|
|
6340
|
+
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.`;
|
|
6341
|
+
}
|
|
6342
|
+
function evaluateLowDiskWarning(input) {
|
|
6343
|
+
const { diskFreeBytes, diskTotalBytes, opencodeDbBytes, nowMs, state } = input;
|
|
6344
|
+
if (diskFreeBytes === null || diskTotalBytes === null || diskTotalBytes <= 0 || diskFreeBytes >= diskTotalBytes) {
|
|
6345
|
+
return { warning: null, state };
|
|
6346
|
+
}
|
|
6347
|
+
const recovered = diskFreeBytes >= RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES || diskFreeBytes >= diskTotalBytes * RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION;
|
|
6348
|
+
if (recovered) {
|
|
6349
|
+
return { warning: null, state: { worstBand: null, warnedAtMs: null } };
|
|
6350
|
+
}
|
|
6351
|
+
const low = diskFreeBytes < RUNNER_DISK_ALERT_ENTRY_FREE_BYTES && diskFreeBytes < diskTotalBytes * RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION;
|
|
6352
|
+
if (!low) return { warning: null, state };
|
|
6353
|
+
const currentBand = bandFor(diskFreeBytes);
|
|
6354
|
+
const currentRank = BAND_RANK[currentBand];
|
|
6355
|
+
const previousRank = state.worstBand === null ? 0 : BAND_RANK[state.worstBand];
|
|
6356
|
+
const worse = currentRank > previousRank;
|
|
6357
|
+
const reassert = state.warnedAtMs !== null && nowMs - state.warnedAtMs >= LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS;
|
|
6358
|
+
const nextState = {
|
|
6359
|
+
worstBand: worse ? currentBand : state.worstBand ?? currentBand,
|
|
6360
|
+
warnedAtMs: worse || state.warnedAtMs === null || reassert ? nowMs : state.warnedAtMs
|
|
6361
|
+
};
|
|
6362
|
+
return {
|
|
6363
|
+
warning: worse || state.warnedAtMs === null || reassert ? warningMessage(nextState.worstBand, diskFreeBytes, diskTotalBytes, opencodeDbBytes) : null,
|
|
6364
|
+
state: nextState
|
|
6365
|
+
};
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
// src/lib/opencode/session-store-health.ts
|
|
6369
|
+
var SESSION_STORE_HEALTH_STALENESS_MS = 5 * 6e4;
|
|
6370
|
+
function createSessionStoreHealth(options = {}) {
|
|
6371
|
+
const now = options.now ?? (() => Date.now());
|
|
6372
|
+
const stalenessMs = Math.max(0, options.stalenessMs ?? SESSION_STORE_HEALTH_STALENESS_MS);
|
|
6373
|
+
let lastUnwritableAt = null;
|
|
6374
|
+
let lastWritableAt = null;
|
|
6375
|
+
return {
|
|
6376
|
+
observeUnwritable: () => {
|
|
6377
|
+
lastUnwritableAt = now();
|
|
6378
|
+
},
|
|
6379
|
+
observeWritable: () => {
|
|
6380
|
+
lastWritableAt = now();
|
|
6381
|
+
},
|
|
6382
|
+
isUnwritable: () => {
|
|
6383
|
+
if (lastUnwritableAt === null) return false;
|
|
6384
|
+
if (lastWritableAt !== null && lastWritableAt >= lastUnwritableAt) return false;
|
|
6385
|
+
return now() - lastUnwritableAt < stalenessMs;
|
|
6386
|
+
}
|
|
6387
|
+
};
|
|
6388
|
+
}
|
|
6389
|
+
|
|
5978
6390
|
// src/lib/channels/driver.ts
|
|
5979
|
-
import { homedir as
|
|
6391
|
+
import { homedir as homedir6 } from "node:os";
|
|
5980
6392
|
|
|
5981
6393
|
// src/lib/runner-file-sync.ts
|
|
5982
6394
|
import { join as join9 } from "node:path";
|
|
@@ -6433,6 +6845,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
|
6433
6845
|
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
6434
6846
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
6435
6847
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
6848
|
+
var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
|
|
6436
6849
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
6437
6850
|
var HEARTBEAT_MS = 6e4;
|
|
6438
6851
|
var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
@@ -6494,6 +6907,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6494
6907
|
maxActiveSessions;
|
|
6495
6908
|
watcherStallMs;
|
|
6496
6909
|
wedgeWarningIntervalMs;
|
|
6910
|
+
sessionStoreHealth;
|
|
6497
6911
|
openCodeClient;
|
|
6498
6912
|
/** Cache of conversationId → opencode sessionId. */
|
|
6499
6913
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -6859,10 +7273,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6859
7273
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
6860
7274
|
this.now = config.now ?? (() => Date.now());
|
|
6861
7275
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
6862
|
-
this.homeDir = config.homeDir ??
|
|
7276
|
+
this.homeDir = config.homeDir ?? homedir6();
|
|
6863
7277
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
6864
7278
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
6865
7279
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
7280
|
+
this.sessionStoreHealth = config.sessionStoreHealth;
|
|
6866
7281
|
}
|
|
6867
7282
|
get isV2() {
|
|
6868
7283
|
return this.openCodeClient.version === "v2";
|
|
@@ -6878,7 +7293,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6878
7293
|
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6879
7294
|
}
|
|
6880
7295
|
async listSessions() {
|
|
6881
|
-
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient
|
|
7296
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient, {
|
|
7297
|
+
limit: DRIVER_SESSION_ENUMERATION_LIMIT
|
|
7298
|
+
});
|
|
6882
7299
|
}
|
|
6883
7300
|
async sessionExists(sessionId) {
|
|
6884
7301
|
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
@@ -8334,6 +8751,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8334
8751
|
b2AbandonedSignalled: false,
|
|
8335
8752
|
ambiguousPinnedSinceMs: 0,
|
|
8336
8753
|
ambiguousResolved: false,
|
|
8754
|
+
sessionStoreFailurePending: false,
|
|
8337
8755
|
neverStartedResolved: false
|
|
8338
8756
|
});
|
|
8339
8757
|
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
@@ -8452,6 +8870,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8452
8870
|
b2AbandonedSignalled: false,
|
|
8453
8871
|
ambiguousPinnedSinceMs: 0,
|
|
8454
8872
|
ambiguousResolved: false,
|
|
8873
|
+
sessionStoreFailurePending: false,
|
|
8455
8874
|
neverStartedResolved: false
|
|
8456
8875
|
});
|
|
8457
8876
|
}
|
|
@@ -8865,6 +9284,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8865
9284
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8866
9285
|
this.neverStartedAttempts.delete(id);
|
|
8867
9286
|
}
|
|
9287
|
+
if (inFlight.sessionStoreFailurePending) {
|
|
9288
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9289
|
+
return;
|
|
9290
|
+
}
|
|
8868
9291
|
if (inFlight.neverStartedResolved) {
|
|
8869
9292
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8870
9293
|
inFlight.neverStartedResolved = false;
|
|
@@ -9044,6 +9467,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9044
9467
|
descendantOngoing,
|
|
9045
9468
|
rootOngoing
|
|
9046
9469
|
})) {
|
|
9470
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
9471
|
+
inFlight.sessionStoreFailurePending = true;
|
|
9472
|
+
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9473
|
+
watched_for_ms: pinnedForMs,
|
|
9474
|
+
run_state: "failed",
|
|
9475
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9476
|
+
});
|
|
9477
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9478
|
+
return;
|
|
9479
|
+
}
|
|
9047
9480
|
inFlight.b2AbandonedSignalled = true;
|
|
9048
9481
|
this.log({
|
|
9049
9482
|
level: "warn",
|
|
@@ -9102,6 +9535,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9102
9535
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
9103
9536
|
sessionOngoing: ongoing
|
|
9104
9537
|
})) {
|
|
9538
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
9539
|
+
inFlight.sessionStoreFailurePending = true;
|
|
9540
|
+
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
9541
|
+
watched_for_ms: pinnedForMs,
|
|
9542
|
+
run_state: "failed",
|
|
9543
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9544
|
+
});
|
|
9545
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9546
|
+
return;
|
|
9547
|
+
}
|
|
9105
9548
|
inFlight.ambiguousResolved = true;
|
|
9106
9549
|
this.log({
|
|
9107
9550
|
level: "warn",
|
|
@@ -9381,6 +9824,66 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9381
9824
|
this.neverStartedAttempts.delete(inFlight.evidentMessageId);
|
|
9382
9825
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9383
9826
|
}
|
|
9827
|
+
/**
|
|
9828
|
+
* Report a turn as failed when the session database was observed unwritable.
|
|
9829
|
+
* The queue row is retained as failed, while the OpenCode copy stays fenced so
|
|
9830
|
+
* a later drain cannot blindly prompt the same turn again.
|
|
9831
|
+
*/
|
|
9832
|
+
async settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight) {
|
|
9833
|
+
const conv = watcher.conv;
|
|
9834
|
+
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.";
|
|
9835
|
+
this.log({
|
|
9836
|
+
level: "error",
|
|
9837
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was not resolved as done: ${error2}`,
|
|
9838
|
+
conversation_id: conv.id,
|
|
9839
|
+
message_id: inFlight.evidentMessageId
|
|
9840
|
+
});
|
|
9841
|
+
this.anchorDeliveryDeadline(inFlight);
|
|
9842
|
+
try {
|
|
9843
|
+
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
|
|
9844
|
+
} catch (err) {
|
|
9845
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
9846
|
+
if (err instanceof ChannelTerminalError) {
|
|
9847
|
+
this.log({
|
|
9848
|
+
level: "warn",
|
|
9849
|
+
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}`,
|
|
9850
|
+
conversation_id: conv.id,
|
|
9851
|
+
message_id: inFlight.evidentMessageId
|
|
9852
|
+
});
|
|
9853
|
+
this.recordReleasedOpencodeId(
|
|
9854
|
+
inFlight.evidentMessageId,
|
|
9855
|
+
sessionId,
|
|
9856
|
+
inFlight.opencodeMessageId
|
|
9857
|
+
);
|
|
9858
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9859
|
+
return;
|
|
9860
|
+
}
|
|
9861
|
+
if (this.now() >= inFlight.deadline) {
|
|
9862
|
+
this.log({
|
|
9863
|
+
level: "warn",
|
|
9864
|
+
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)}`,
|
|
9865
|
+
conversation_id: conv.id,
|
|
9866
|
+
message_id: inFlight.evidentMessageId
|
|
9867
|
+
});
|
|
9868
|
+
this.recordReleasedOpencodeId(
|
|
9869
|
+
inFlight.evidentMessageId,
|
|
9870
|
+
sessionId,
|
|
9871
|
+
inFlight.opencodeMessageId
|
|
9872
|
+
);
|
|
9873
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9874
|
+
return;
|
|
9875
|
+
}
|
|
9876
|
+
this.log({
|
|
9877
|
+
level: "warn",
|
|
9878
|
+
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)}`,
|
|
9879
|
+
conversation_id: conv.id,
|
|
9880
|
+
message_id: inFlight.evidentMessageId
|
|
9881
|
+
});
|
|
9882
|
+
return;
|
|
9883
|
+
}
|
|
9884
|
+
inFlight.done = true;
|
|
9885
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9886
|
+
}
|
|
9384
9887
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
9385
9888
|
/**
|
|
9386
9889
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
@@ -12112,6 +12615,7 @@ function createCredentialSync({
|
|
|
12112
12615
|
|
|
12113
12616
|
// src/commands/run.ts
|
|
12114
12617
|
var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
12618
|
+
var MAX_DISCONNECT_REASON_LENGTH = 200;
|
|
12115
12619
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
12116
12620
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
12117
12621
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
@@ -12267,7 +12771,13 @@ function logActivity(state, entry) {
|
|
|
12267
12771
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
12268
12772
|
if (!meetsThreshold(state, level)) return;
|
|
12269
12773
|
forwardRunnerActivity(
|
|
12270
|
-
{
|
|
12774
|
+
{
|
|
12775
|
+
level,
|
|
12776
|
+
message: entry.message,
|
|
12777
|
+
error: entry.error,
|
|
12778
|
+
metadata: entry.metadata,
|
|
12779
|
+
...entry.source ? { source: entry.source } : {}
|
|
12780
|
+
},
|
|
12271
12781
|
{ agentId: state.agentId, authHeader: state.authHeader }
|
|
12272
12782
|
);
|
|
12273
12783
|
const fullEntry = {
|
|
@@ -12289,7 +12799,7 @@ function logActivity(state, entry) {
|
|
|
12289
12799
|
}
|
|
12290
12800
|
function reportSessionDbRecovery(state) {
|
|
12291
12801
|
try {
|
|
12292
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
12802
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
|
|
12293
12803
|
for (const record of report.records) {
|
|
12294
12804
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
12295
12805
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -12324,7 +12834,9 @@ function displayStatus(state) {
|
|
|
12324
12834
|
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
12325
12835
|
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
12326
12836
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
12327
|
-
const detail = last ? chalk7.dim(
|
|
12837
|
+
const detail = last ? chalk7.dim(
|
|
12838
|
+
` \xB7 ${last.timestamp.toISOString()} ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`
|
|
12839
|
+
) : "";
|
|
12328
12840
|
const agent = state.agentName ?? state.agentId;
|
|
12329
12841
|
console.log(
|
|
12330
12842
|
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
@@ -12343,7 +12855,7 @@ function reportOpenCodeV2CredentialImport(state, result) {
|
|
|
12343
12855
|
logActivity(state, {
|
|
12344
12856
|
type: "info",
|
|
12345
12857
|
level: "warn",
|
|
12346
|
-
message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them.
|
|
12858
|
+
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.`
|
|
12347
12859
|
});
|
|
12348
12860
|
}
|
|
12349
12861
|
if (result.imported.length === 0 && result.unsupported.length === 0 && result.failed.length === 0) {
|
|
@@ -12368,7 +12880,7 @@ async function importOpenCodeV2CredentialsForState(state) {
|
|
|
12368
12880
|
}
|
|
12369
12881
|
const result = await importOpenCodeV2Credentials({
|
|
12370
12882
|
request: client.request,
|
|
12371
|
-
homeDir:
|
|
12883
|
+
homeDir: homedir7(),
|
|
12372
12884
|
directory,
|
|
12373
12885
|
guard: state.opencodeV2CredentialImportGuard,
|
|
12374
12886
|
log: (message, level = "info") => logActivity(state, { type: "info", level, message })
|
|
@@ -12576,9 +13088,9 @@ async function driveChannels(state, driver) {
|
|
|
12576
13088
|
}
|
|
12577
13089
|
}
|
|
12578
13090
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
12579
|
-
var
|
|
13091
|
+
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
12580
13092
|
function shouldWarnForReclaimSkip(reason) {
|
|
12581
|
-
if (reason !== "sqlite-unavailable") return false;
|
|
13093
|
+
if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
|
|
12582
13094
|
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
12583
13095
|
if (!version2) return false;
|
|
12584
13096
|
const major = Number(version2[1]);
|
|
@@ -12587,7 +13099,14 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
12587
13099
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
12588
13100
|
}
|
|
12589
13101
|
function sessionDbPath() {
|
|
12590
|
-
return join11(
|
|
13102
|
+
return join11(homedir7(), ".local", "share", "opencode", "opencode.db");
|
|
13103
|
+
}
|
|
13104
|
+
function logSessionCleanupActivity(state, phase, entry) {
|
|
13105
|
+
logActivity(state, {
|
|
13106
|
+
...entry,
|
|
13107
|
+
source: "cli.session-cleanup",
|
|
13108
|
+
metadata: { ...entry.metadata, phase }
|
|
13109
|
+
});
|
|
12591
13110
|
}
|
|
12592
13111
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
12593
13112
|
const record = {
|
|
@@ -12628,69 +13147,136 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
12628
13147
|
async function runSweep(state, driver, config) {
|
|
12629
13148
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
12630
13149
|
try {
|
|
12631
|
-
const
|
|
12632
|
-
|
|
13150
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0);
|
|
13151
|
+
const sessions = enumeration.sessions;
|
|
13152
|
+
const enumerationReason = sessions === null ? enumeration.reason : null;
|
|
13153
|
+
if (enumerationReason !== null) {
|
|
12633
13154
|
logActivity(state, {
|
|
12634
13155
|
type: "info",
|
|
12635
|
-
|
|
13156
|
+
level: "warn",
|
|
13157
|
+
message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
|
|
12636
13158
|
});
|
|
12637
|
-
return;
|
|
12638
13159
|
}
|
|
12639
|
-
const toDelete = selectSessionsToDelete(
|
|
12640
|
-
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
12641
|
-
{
|
|
12642
|
-
maxAgeMs: config.maxAgeMs,
|
|
12643
|
-
maxCount: config.maxCount,
|
|
12644
|
-
nowMs: Date.now(),
|
|
12645
|
-
protectedIds: driver.protectedSessionIds()
|
|
12646
|
-
}
|
|
12647
|
-
);
|
|
12648
|
-
const protectedNow = driver.protectedSessionIds();
|
|
12649
13160
|
let deleted = 0;
|
|
12650
13161
|
let failed = 0;
|
|
13162
|
+
let failed4xx = 0;
|
|
13163
|
+
let failed5xx = 0;
|
|
13164
|
+
let failedOtherStatus = 0;
|
|
13165
|
+
let failedThrew = 0;
|
|
12651
13166
|
let skippedNewlyActive = 0;
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
|
|
12655
|
-
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
13167
|
+
let remaining = 0;
|
|
13168
|
+
let protectedNow = driver.protectedSessionIds();
|
|
13169
|
+
if (sessions !== null) {
|
|
13170
|
+
const toDelete = selectSessionsToDelete(
|
|
13171
|
+
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
13172
|
+
{
|
|
13173
|
+
maxAgeMs: config.maxAgeMs,
|
|
13174
|
+
maxCount: config.maxCount,
|
|
13175
|
+
nowMs: Date.now(),
|
|
13176
|
+
protectedIds: protectedNow
|
|
13177
|
+
}
|
|
13178
|
+
);
|
|
13179
|
+
const deleteCandidates = toDelete.slice(0, SESSION_CLEANUP_MAX_DELETES_PER_SWEEP);
|
|
13180
|
+
remaining = toDelete.length - deleteCandidates.length;
|
|
13181
|
+
protectedNow = driver.protectedSessionIds();
|
|
13182
|
+
for (const id of deleteCandidates) {
|
|
13183
|
+
if (protectedNow.has(id)) {
|
|
13184
|
+
skippedNewlyActive++;
|
|
13185
|
+
logActivity(state, {
|
|
13186
|
+
type: "info",
|
|
13187
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
|
|
13188
|
+
});
|
|
13189
|
+
continue;
|
|
13190
|
+
}
|
|
13191
|
+
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
13192
|
+
if (outcome.ok) {
|
|
13193
|
+
deleted++;
|
|
13194
|
+
} else if (outcome.status === "threw") {
|
|
13195
|
+
failed++;
|
|
13196
|
+
failedThrew++;
|
|
13197
|
+
} else {
|
|
13198
|
+
failed++;
|
|
13199
|
+
const statusClass = Math.floor(outcome.status / 100);
|
|
13200
|
+
if (statusClass === 4) failed4xx++;
|
|
13201
|
+
else if (statusClass === 5) failed5xx++;
|
|
13202
|
+
else failedOtherStatus++;
|
|
13203
|
+
}
|
|
12660
13204
|
}
|
|
12661
|
-
|
|
12662
|
-
|
|
13205
|
+
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
13206
|
+
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
13207
|
+
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
13208
|
+
logActivity(state, {
|
|
13209
|
+
type: "info",
|
|
13210
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
13211
|
+
});
|
|
12663
13212
|
}
|
|
12664
|
-
const
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
13213
|
+
const sessionMetadata = {
|
|
13214
|
+
sessions_inspected: sessions?.length ?? null,
|
|
13215
|
+
sessions_deleted: deleted,
|
|
13216
|
+
sessions_failed: failed,
|
|
13217
|
+
sessions_failed_4xx: failed4xx,
|
|
13218
|
+
sessions_failed_5xx: failed5xx,
|
|
13219
|
+
sessions_failed_other: failedOtherStatus,
|
|
13220
|
+
sessions_failed_threw: failedThrew,
|
|
13221
|
+
sessions_skipped_active: skippedNewlyActive,
|
|
13222
|
+
sessions_remaining: remaining,
|
|
13223
|
+
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
13224
|
+
};
|
|
12670
13225
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
12671
13226
|
dbPath: sessionDbPath(),
|
|
12672
|
-
|
|
13227
|
+
chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
13228
|
+
pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
13229
|
+
timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
12673
13230
|
allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
|
|
12674
13231
|
});
|
|
12675
13232
|
if (reclaimResult.ok) {
|
|
13233
|
+
state.sessionStoreHealth.observeWritable();
|
|
12676
13234
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
12677
13235
|
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
12678
13236
|
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
12679
|
-
|
|
13237
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12680
13238
|
type: "info",
|
|
12681
|
-
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${
|
|
13239
|
+
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB (stopped by ${reclaimResult.stoppedBy}; freelist ${reclaimResult.freelistBefore} -> ${reclaimResult.freelistAfter} pages)` + checkpointNote,
|
|
13240
|
+
metadata: {
|
|
13241
|
+
outcome: "reclaimed",
|
|
13242
|
+
mode: reclaimResult.mode,
|
|
13243
|
+
before_bytes: reclaimResult.beforeBytes,
|
|
13244
|
+
after_bytes: reclaimResult.afterBytes,
|
|
13245
|
+
checkpoint_busy: reclaimResult.checkpoint.busy,
|
|
13246
|
+
wal_frames_pending: reclaimResult.checkpoint.log,
|
|
13247
|
+
stopped_by: reclaimResult.stoppedBy,
|
|
13248
|
+
freelist_before: reclaimResult.freelistBefore,
|
|
13249
|
+
freelist_after: reclaimResult.freelistAfter,
|
|
13250
|
+
pages_reclaimed: reclaimResult.pagesReclaimed,
|
|
13251
|
+
reclaim_elapsed_ms: reclaimResult.elapsedMs,
|
|
13252
|
+
...sessionMetadata
|
|
13253
|
+
}
|
|
12682
13254
|
});
|
|
12683
13255
|
} else {
|
|
12684
|
-
|
|
13256
|
+
if (reclaimResult.skipped === "disk-exhaustion") {
|
|
13257
|
+
state.sessionStoreHealth.observeUnwritable();
|
|
13258
|
+
}
|
|
13259
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12685
13260
|
type: "info",
|
|
12686
|
-
|
|
13261
|
+
level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
|
|
13262
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
|
|
13263
|
+
metadata: {
|
|
13264
|
+
outcome: "reclaim-skipped",
|
|
13265
|
+
reason: reclaimResult.skipped,
|
|
13266
|
+
...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
|
|
13267
|
+
...sessionMetadata
|
|
13268
|
+
}
|
|
12687
13269
|
});
|
|
12688
13270
|
}
|
|
12689
13271
|
} catch (error2) {
|
|
12690
13272
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
12691
|
-
|
|
13273
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12692
13274
|
type: "error",
|
|
12693
|
-
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}
|
|
13275
|
+
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`,
|
|
13276
|
+
metadata: {
|
|
13277
|
+
outcome: "sweep-failed",
|
|
13278
|
+
reason: "sweep-error"
|
|
13279
|
+
}
|
|
12694
13280
|
});
|
|
12695
13281
|
}
|
|
12696
13282
|
}
|
|
@@ -12706,14 +13292,19 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12706
13292
|
for (const warning2 of config.warnings) {
|
|
12707
13293
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
12708
13294
|
}
|
|
12709
|
-
const dbBytes = statSessionDbBytes(
|
|
13295
|
+
const dbBytes = statSessionDbBytes(homedir7());
|
|
12710
13296
|
void (async () => {
|
|
12711
13297
|
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
12712
13298
|
if (reclaimAvailability !== null) {
|
|
12713
|
-
|
|
13299
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12714
13300
|
type: "info",
|
|
12715
13301
|
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
12716
|
-
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
13302
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : ""),
|
|
13303
|
+
metadata: {
|
|
13304
|
+
outcome: "preflight-skipped",
|
|
13305
|
+
reason: reclaimAvailability.reason,
|
|
13306
|
+
...reclaimAvailability.detail !== void 0 ? { detail: reclaimAvailability.detail } : {}
|
|
13307
|
+
}
|
|
12717
13308
|
});
|
|
12718
13309
|
}
|
|
12719
13310
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
@@ -12730,9 +13321,15 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12730
13321
|
);
|
|
12731
13322
|
});
|
|
12732
13323
|
if (!config.enabled) return;
|
|
12733
|
-
|
|
13324
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12734
13325
|
type: "info",
|
|
12735
|
-
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)
|
|
13326
|
+
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`,
|
|
13327
|
+
metadata: {
|
|
13328
|
+
outcome: "armed",
|
|
13329
|
+
interval_ms: config.intervalMs,
|
|
13330
|
+
max_age_ms: config.maxAgeMs ?? null,
|
|
13331
|
+
max_count: config.maxCount ?? null
|
|
13332
|
+
}
|
|
12736
13333
|
});
|
|
12737
13334
|
const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
|
|
12738
13335
|
const firstSweep = setTimeout(
|
|
@@ -12911,7 +13508,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
12911
13508
|
});
|
|
12912
13509
|
return;
|
|
12913
13510
|
}
|
|
12914
|
-
const { collect, stop } = createResourceUsageCollector(
|
|
13511
|
+
const { collect, stop } = createResourceUsageCollector(homedir7());
|
|
12915
13512
|
state.stopResourceUsageSampling = stop;
|
|
12916
13513
|
let consecutiveFailures = 0;
|
|
12917
13514
|
const tick = async () => {
|
|
@@ -12924,6 +13521,19 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
12924
13521
|
message: `Resource usage collection: ${warning2}`
|
|
12925
13522
|
});
|
|
12926
13523
|
}
|
|
13524
|
+
const lowDiskDecision = evaluateLowDiskWarning({
|
|
13525
|
+
...usage,
|
|
13526
|
+
nowMs: Date.now(),
|
|
13527
|
+
state: state.lowDiskWarningState
|
|
13528
|
+
});
|
|
13529
|
+
state.lowDiskWarningState = lowDiskDecision.state;
|
|
13530
|
+
if (lowDiskDecision.warning !== null) {
|
|
13531
|
+
logActivity(state, {
|
|
13532
|
+
type: "info",
|
|
13533
|
+
level: "warn",
|
|
13534
|
+
message: lowDiskDecision.warning
|
|
13535
|
+
});
|
|
13536
|
+
}
|
|
12927
13537
|
const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
|
|
12928
13538
|
if (result.ok) {
|
|
12929
13539
|
if (consecutiveFailures > 0) {
|
|
@@ -13136,7 +13746,7 @@ async function run(options) {
|
|
|
13136
13746
|
let fileSyncDirectories;
|
|
13137
13747
|
try {
|
|
13138
13748
|
logLevel = resolveLogLevel(options);
|
|
13139
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
13749
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
|
|
13140
13750
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
13141
13751
|
throw new Error(
|
|
13142
13752
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -13170,6 +13780,7 @@ async function run(options) {
|
|
|
13170
13780
|
opencodeClient: null,
|
|
13171
13781
|
opencodeV2CredentialImportGuard: { inFlight: false },
|
|
13172
13782
|
sessionDbProvenanceAnomaly: false,
|
|
13783
|
+
sessionStoreHealth: createSessionStoreHealth(),
|
|
13173
13784
|
opencodeProcess: null,
|
|
13174
13785
|
stopOpenCodeLogTail: null,
|
|
13175
13786
|
litestreamProcess: null,
|
|
@@ -13187,10 +13798,12 @@ async function run(options) {
|
|
|
13187
13798
|
openaiUsageRearm: null,
|
|
13188
13799
|
resourceUsageTimer: null,
|
|
13189
13800
|
stopResourceUsageSampling: null,
|
|
13801
|
+
lowDiskWarningState: { worstBand: null, warnedAtMs: null },
|
|
13190
13802
|
credentialSync: null,
|
|
13191
13803
|
authHeader: ""
|
|
13192
13804
|
};
|
|
13193
13805
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
13806
|
+
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
13194
13807
|
if (options.credentialSyncMarker) {
|
|
13195
13808
|
state.credentialSync = createCredentialSync({
|
|
13196
13809
|
markerPath: options.credentialSyncMarker,
|
|
@@ -13376,13 +13989,21 @@ async function run(options) {
|
|
|
13376
13989
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
13377
13990
|
}
|
|
13378
13991
|
state.credentialSync?.arm();
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
createOpenCodeActivityForwarder(() => ({
|
|
13992
|
+
const openCodeActivityForwarder = createOpenCodeActivityForwarder(
|
|
13993
|
+
() => ({
|
|
13382
13994
|
agentId: state.agentId,
|
|
13383
13995
|
authHeader: state.authHeader
|
|
13384
|
-
})
|
|
13385
|
-
|
|
13996
|
+
}),
|
|
13997
|
+
state.sessionStoreHealth.observeUnwritable
|
|
13998
|
+
);
|
|
13999
|
+
const openCodeLogTail = tailOpenCodeLogFile(
|
|
14000
|
+
resolveOpenCodeLogPath(homedir7(), process.env),
|
|
14001
|
+
openCodeActivityForwarder
|
|
14002
|
+
);
|
|
14003
|
+
state.stopOpenCodeLogTail = () => {
|
|
14004
|
+
openCodeActivityForwarder.flush();
|
|
14005
|
+
openCodeLogTail.stop();
|
|
14006
|
+
};
|
|
13386
14007
|
let sessionDbVerifyFatal = false;
|
|
13387
14008
|
if (!options.restoreSessionDb) {
|
|
13388
14009
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -13499,7 +14120,7 @@ async function run(options) {
|
|
|
13499
14120
|
const provenance = checkSessionDbProvenance({
|
|
13500
14121
|
dbPath: sessionDbPath(),
|
|
13501
14122
|
currentVersion: state.opencodeVersion,
|
|
13502
|
-
homeDir:
|
|
14123
|
+
homeDir: homedir7(),
|
|
13503
14124
|
env: process.env
|
|
13504
14125
|
});
|
|
13505
14126
|
if (provenance.anomaly) {
|
|
@@ -13666,10 +14287,11 @@ async function run(options) {
|
|
|
13666
14287
|
getAuthHeader: () => state.authHeader,
|
|
13667
14288
|
conversationFilter: state.conversationFilter,
|
|
13668
14289
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
14290
|
+
sessionStoreHealth: state.sessionStoreHealth,
|
|
13669
14291
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
13670
14292
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
13671
14293
|
fileSyncDirectories,
|
|
13672
|
-
homeDir:
|
|
14294
|
+
homeDir: homedir7(),
|
|
13673
14295
|
maxActiveSessions,
|
|
13674
14296
|
log: (entry) => (
|
|
13675
14297
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -13691,12 +14313,13 @@ async function run(options) {
|
|
|
13691
14313
|
openCodePassword: state.opencodePassword,
|
|
13692
14314
|
isRunning: () => state.running,
|
|
13693
14315
|
events: {
|
|
13694
|
-
onConnected: (agentId, isReconnect) => {
|
|
14316
|
+
onConnected: (agentId, isReconnect, transition) => {
|
|
13695
14317
|
state.connected = true;
|
|
13696
14318
|
state.agentId = agentId;
|
|
14319
|
+
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})`;
|
|
13697
14320
|
logActivity(state, {
|
|
13698
14321
|
type: "info",
|
|
13699
|
-
message:
|
|
14322
|
+
message: connectionMessage
|
|
13700
14323
|
});
|
|
13701
14324
|
if (options.tunnelReadyFile) {
|
|
13702
14325
|
const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
|
|
@@ -13717,7 +14340,12 @@ async function run(options) {
|
|
|
13717
14340
|
version: state.opencodeVersion,
|
|
13718
14341
|
major: state.opencodeApiVersion,
|
|
13719
14342
|
connected: state.opencodeConnected
|
|
13720
|
-
})
|
|
14343
|
+
}),
|
|
14344
|
+
connection_generation: transition.generation,
|
|
14345
|
+
...isReconnect && transition.downtimeMs !== null ? {
|
|
14346
|
+
reconnect_attempt: transition.attempt,
|
|
14347
|
+
downtime_ms: transition.downtimeMs
|
|
14348
|
+
} : {}
|
|
13721
14349
|
});
|
|
13722
14350
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
13723
14351
|
if (state.interactive) displayStatus(state);
|
|
@@ -13739,13 +14367,18 @@ async function run(options) {
|
|
|
13739
14367
|
if (state.interactive) displayStatus(state);
|
|
13740
14368
|
});
|
|
13741
14369
|
},
|
|
13742
|
-
onDisconnected: (code, reason) => {
|
|
14370
|
+
onDisconnected: (code, reason, transition) => {
|
|
13743
14371
|
state.connected = false;
|
|
14372
|
+
const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
|
|
13744
14373
|
logActivity(state, {
|
|
13745
14374
|
type: "info",
|
|
13746
|
-
message: `Tunnel disconnected (code: ${code}, reason: ${
|
|
14375
|
+
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
|
|
14376
|
+
});
|
|
14377
|
+
emitAgentDisconnected(state.agentId, {
|
|
14378
|
+
code,
|
|
14379
|
+
reason: safeReason,
|
|
14380
|
+
connection_generation: transition.generation
|
|
13747
14381
|
});
|
|
13748
|
-
emitAgentDisconnected(state.agentId, { code, reason });
|
|
13749
14382
|
if (state.interactive) displayStatus(state);
|
|
13750
14383
|
},
|
|
13751
14384
|
onError: (error2) => {
|
|
@@ -13845,7 +14478,7 @@ async function run(options) {
|
|
|
13845
14478
|
},
|
|
13846
14479
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
13847
14480
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
13848
|
-
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
14481
|
+
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`",
|
|
13849
14482
|
firstDelayMs: firstReportDelayMs,
|
|
13850
14483
|
nextDelayMs: usageReportDelayMs,
|
|
13851
14484
|
failureLogLevel: usageReportFailureLogLevel
|
|
@@ -13888,7 +14521,7 @@ async function run(options) {
|
|
|
13888
14521
|
}
|
|
13889
14522
|
|
|
13890
14523
|
// src/index.ts
|
|
13891
|
-
var
|
|
14524
|
+
var version = getCliVersion();
|
|
13892
14525
|
var program = new Command();
|
|
13893
14526
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
13894
14527
|
"--endpoint <url>",
|