@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 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 homedir6 } from "node:os";
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.1" : void 0) ?? process.env.npm_package_version ?? "unknown";
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
- console.error(`Telemetry flush failed: ${response.status}`);
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
- console.error(`Telemetry flush error: ${message}${suffix}`);
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
- var FORWARDED_LEVELS = /* @__PURE__ */ new Set(["warn", "error"]);
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
- return { level: level === "WARN" ? "warn" : "error", sessionID: sessionMatch?.[1] };
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
- function createOpenCodeActivityForwarder(getContext) {
1565
+ var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
1566
+ function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
1318
1567
  let buffer = Buffer.alloc(0);
1319
- const flushLine = (line) => {
1320
- const parsed = parseOpenCodeLogLine(line);
1321
- if (!parsed) return;
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: parsed.sessionID ? { sessionID: parsed.sessionID } : void 0,
1579
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
1327
1580
  source: "opencode"
1328
1581
  },
1329
1582
  getContext()
1330
1583
  );
1331
1584
  };
1332
- return (chunk) => {
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 redact(message) {
1350
- return 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>");
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
- console.error(
1361
- `[runner-activity-telemetry] dropped ${Object.keys(metadata).length - entries.length} metadata entries`
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([key, truncate(redact(value).slice(0, MAX_METADATA_VALUE_LENGTH))]);
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
- console.error(
1386
- `[runner-activity-telemetry] rate cap reached: dropped ${window.windowDroppedCount} ${window.windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min) for source "${source}"`
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
- console.error(
1397
- `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window for source "${source}"`
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(redact(rawMessage));
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
- console.error(
1421
- `[runner-activity-telemetry] failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
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) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
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
- async function listSessions(port, client) {
1881
- if (client?.version === "v2") return listV2Sessions(client);
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, "/session");
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
- `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
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(port, client, `/session/${id}`, { method: "DELETE" });
1898
- return res.status >= 200 && res.status < 300;
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(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3037
- return true;
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 homedir2 } from "node:os";
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(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
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 statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
3664
- import { homedir as homedir3 } from "node:os";
3665
- import { dirname as dirname2, resolve as resolvePath } from "node:path";
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(dirname2(options.noReplicateMarker), { recursive: true });
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 (statSync2(configPath).size > 0) return configPath;
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(dirname2(configPath), { recursive: true });
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(homedir3(), env));
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(homedir3(), env));
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
- statSync2(path);
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(homedir3(), env));
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 dirname3, join as join5 } from "node:path";
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(dirname3(path), { recursive: true });
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(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
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
- `findOpenCodeProcesses: ps fallback failed: ${err instanceof Error ? err.message : String(err)}`
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", "opencode (serve|--port)");
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
- return toDelete;
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 statSync3 } from "node:fs";
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 statSync3(dbPath).size;
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 statSync4 } from "node:fs";
4931
- import { homedir as homedir4 } from "node:os";
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 = homedir4(), env = process.env) {
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 = statSync4(logPath);
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
- const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
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
- events.onConnected(agentId, isReconnect);
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
- events.onDisconnected(code, reason);
5639
- if (this.opts.isRunning() && code !== 1e3 && !this.reconnecting) {
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 homedir5 } from "node:os";
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 ?? homedir5();
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
- { level, message: entry.message, error: entry.error, metadata: entry.metadata },
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: homedir6(), env: process.env });
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(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
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. See #2251 for importing existing logins.`
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: homedir6(),
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 SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
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(homedir6(), ".local", "share", "opencode", "opencode.db");
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 sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
12632
- if (sessions === null) {
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
- message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`
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
- for (const id of toDelete) {
12653
- if (protectedNow.has(id)) {
12654
- skippedNewlyActive++;
12655
- logActivity(state, {
12656
- type: "info",
12657
- message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
12658
- });
12659
- continue;
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
- if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
12662
- else failed++;
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 failedNote = failed > 0 ? `, failed ${failed}` : "";
12665
- const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
12666
- logActivity(state, {
12667
- type: "info",
12668
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`
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
- maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
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
- logActivity(state, {
13237
+ logSessionCleanupActivity(state, "sweep", {
12680
13238
  type: "info",
12681
- message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`
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
- logActivity(state, {
13256
+ if (reclaimResult.skipped === "disk-exhaustion") {
13257
+ state.sessionStoreHealth.observeUnwritable();
13258
+ }
13259
+ logSessionCleanupActivity(state, "sweep", {
12685
13260
  type: "info",
12686
- message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
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
- logActivity(state, {
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(homedir6());
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
- logActivity(state, {
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
- logActivity(state, {
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(homedir6());
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, homedir6());
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
- state.stopOpenCodeLogTail = tailOpenCodeLogFile(
13380
- resolveOpenCodeLogPath(homedir6(), process.env),
13381
- createOpenCodeActivityForwarder(() => ({
13992
+ const openCodeActivityForwarder = createOpenCodeActivityForwarder(
13993
+ () => ({
13382
13994
  agentId: state.agentId,
13383
13995
  authHeader: state.authHeader
13384
- }))
13385
- ).stop;
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: homedir6(),
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: homedir6(),
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: `Tunnel ${isReconnect ? "reconnected" : "connected"} (runner: ${agentId})`
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: ${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 { version } = createRequire2(import.meta.url)("../package.json");
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>",