@evident-ai/cli 3.5.2-dev.c2c9f26 → 3.5.2-dev.df1ae49

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