@evident-ai/cli 3.5.1-dev.bf9e537 → 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-dev.bf9e537" : 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
  }
@@ -1620,6 +1951,9 @@ async function checkOpenCodeHealth(port) {
1620
1951
  signal: AbortSignal.timeout(2e3)
1621
1952
  // 2 second timeout
1622
1953
  });
1954
+ if (response.status === 401) {
1955
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
1956
+ }
1623
1957
  if (!response.ok) {
1624
1958
  return { healthy: false, error: `HTTP ${response.status}` };
1625
1959
  }
@@ -1684,7 +2018,7 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1684
2018
  const startTime = Date.now();
1685
2019
  while (Date.now() - startTime < timeoutMs) {
1686
2020
  const health = await checkOpenCodeHealth(port);
1687
- if (health.healthy) {
2021
+ if (health.healthy || health.authFailed) {
1688
2022
  return health;
1689
2023
  }
1690
2024
  await new Promise((resolve4) => setTimeout(resolve4, 1e3));
@@ -1705,8 +2039,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1705
2039
 
1706
2040
  // src/lib/http-timeout.ts
1707
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
+ }
1708
2066
  function withRequestTimeout(fetchImpl, timeoutMs) {
1709
- 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
+ });
1710
2082
  }
1711
2083
 
1712
2084
  // src/lib/opencode/client.ts
@@ -1860,6 +2232,7 @@ function isSessionActivelyGenerating(messages) {
1860
2232
  if (roleOf(last) !== "assistant") return false;
1861
2233
  return completedOf(last) == null;
1862
2234
  }
2235
+ var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
1863
2236
  function sessionLastActivityMs(session) {
1864
2237
  const candidates = [
1865
2238
  session.time?.updated,
@@ -1874,30 +2247,60 @@ function sessionLastActivityMs(session) {
1874
2247
  }
1875
2248
  return null;
1876
2249
  }
1877
- async function listSessions(port, client) {
1878
- 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);
1879
2258
  try {
1880
- const res = await requestWithClient(port, client, "/session");
2259
+ const res = await requestWithClient(port, client, path);
1881
2260
  if (!res.ok) return null;
1882
2261
  const body = await res.json();
1883
2262
  return Array.isArray(body) ? body : null;
1884
2263
  } catch (error2) {
1885
2264
  console.error(
1886
- `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2265
+ `[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
1887
2266
  );
1888
2267
  return null;
1889
2268
  }
1890
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
+ }
1891
2288
  async function deleteSession(port, id, client) {
1892
2289
  if (client?.version === "v2") return deleteV2Session(client, id);
1893
2290
  try {
1894
- const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
1895
- 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 };
1896
2299
  } catch (error2) {
1897
2300
  console.error(
1898
2301
  `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
1899
2302
  );
1900
- return false;
2303
+ return { ok: false, status: "threw" };
1901
2304
  }
1902
2305
  }
1903
2306
  async function sessionExists(port, id, client) {
@@ -3030,13 +3433,17 @@ async function listV2Sessions(client) {
3030
3433
  }
3031
3434
  async function deleteV2Session(client, sessionId) {
3032
3435
  try {
3033
- await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3034
- 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 };
3035
3442
  } catch (error2) {
3036
3443
  console.error(
3037
3444
  `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3038
3445
  );
3039
- return false;
3446
+ return { ok: false, status: "threw" };
3040
3447
  }
3041
3448
  }
3042
3449
  async function v2SessionExists(client, sessionId) {
@@ -3211,7 +3618,7 @@ import { join as join4 } from "node:path";
3211
3618
 
3212
3619
  // src/lib/openai-usage.ts
3213
3620
  import { readFileSync as readFileSync3 } from "node:fs";
3214
- import { homedir as homedir2 } from "node:os";
3621
+ import { homedir as homedir3 } from "node:os";
3215
3622
  import { join as join3 } from "node:path";
3216
3623
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3217
3624
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
@@ -3226,7 +3633,7 @@ function isLocalCredentialProblem2(err) {
3226
3633
  }
3227
3634
  function readOpenCodeChatGptCredentials() {
3228
3635
  try {
3229
- const raw = readFileSync3(join3(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3636
+ const raw = readFileSync3(join3(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3230
3637
  let parsed;
3231
3638
  try {
3232
3639
  parsed = JSON.parse(raw);
@@ -3367,16 +3774,17 @@ function hasPrimaryHeaders(headers) {
3367
3774
  ].some((name) => headers.has(name));
3368
3775
  }
3369
3776
  async function getOpenAiUsage(port, client) {
3777
+ const openCodeVersion = client?.version ?? "v1";
3370
3778
  const credentials2 = readOpenCodeChatGptCredentials();
3371
3779
  if (!credentials2) {
3372
3780
  throw new OpenAiUsageError(
3373
- "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`.",
3374
3782
  "no_credentials"
3375
3783
  );
3376
3784
  }
3377
3785
  if (credentials2.expiresAt < Date.now()) {
3378
3786
  throw new OpenAiUsageError(
3379
- "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.",
3380
3788
  "credentials_expired"
3381
3789
  );
3382
3790
  }
@@ -3657,9 +4065,9 @@ async function importOpenCodeV2Credentials({
3657
4065
 
3658
4066
  // src/lib/opencode/session-db-boot.ts
3659
4067
  import { spawn as spawn2 } from "node:child_process";
3660
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
3661
- import { homedir as homedir3 } from "node:os";
3662
- 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";
3663
4071
 
3664
4072
  // src/lib/runner-synchroniser.ts
3665
4073
  import { spawn } from "node:child_process";
@@ -3784,7 +4192,7 @@ function clearMarker(options) {
3784
4192
  function markNoReplicate(options, message) {
3785
4193
  if (options.noReplicateMarker) {
3786
4194
  try {
3787
- mkdirSync(dirname2(options.noReplicateMarker), { recursive: true });
4195
+ mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
3788
4196
  writeFileSync(options.noReplicateMarker, "");
3789
4197
  } catch (error2) {
3790
4198
  options.log(
@@ -3909,7 +4317,7 @@ async function ensureLitestreamConfig(options, env) {
3909
4317
  return null;
3910
4318
  }
3911
4319
  try {
3912
- if (statSync2(configPath).size > 0) return configPath;
4320
+ if (statSync3(configPath).size > 0) return configPath;
3913
4321
  } catch (error2) {
3914
4322
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
3915
4323
  options.log(
@@ -3939,7 +4347,7 @@ async function ensureLitestreamConfig(options, env) {
3939
4347
  return null;
3940
4348
  }
3941
4349
  try {
3942
- mkdirSync(dirname2(configPath), { recursive: true });
4350
+ mkdirSync(dirname3(configPath), { recursive: true });
3943
4351
  writeFileSync(configPath, rendered.stdout);
3944
4352
  } catch (error2) {
3945
4353
  options.log(
@@ -4014,14 +4422,14 @@ async function restoreSessionDb(options, configPath, env) {
4014
4422
  case 0:
4015
4423
  return;
4016
4424
  case 31:
4017
- acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir3(), env));
4425
+ acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
4018
4426
  options.log(
4019
4427
  "SESSION-DB-REPLICA-UNUSABLE: booting with a fresh opencode.db and replicating into the existing prefix this boot",
4020
4428
  "warn"
4021
4429
  );
4022
4430
  return;
4023
4431
  case 32:
4024
- acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir3(), env));
4432
+ acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
4025
4433
  discardSessionDbDebris(options);
4026
4434
  markNoReplicate(
4027
4435
  options,
@@ -4100,7 +4508,7 @@ async function verifySessionDb(options, configPath, env) {
4100
4508
  }
4101
4509
  function fileExists(path) {
4102
4510
  try {
4103
- statSync2(path);
4511
+ statSync3(path);
4104
4512
  return true;
4105
4513
  } catch (error2) {
4106
4514
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
@@ -4110,7 +4518,7 @@ function fileExists(path) {
4110
4518
  async function restoreAndVerifySessionDb(options) {
4111
4519
  const env = options.env ?? process.env;
4112
4520
  clearMarker(options);
4113
- acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir3(), env));
4521
+ acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
4114
4522
  const synchroniserEnv = await runSynchroniser(["env"], {
4115
4523
  timeoutMs: SESSION_DB_SYNCHRONISER_TIMEOUT_MS,
4116
4524
  env
@@ -4189,7 +4597,7 @@ async function restoreAndVerifySessionDb(options) {
4189
4597
  // src/lib/opencode/session-db-provenance.ts
4190
4598
  import { createRequire } from "node:module";
4191
4599
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
4192
- import { dirname as dirname3, join as join5 } from "node:path";
4600
+ import { dirname as dirname4, join as join5 } from "node:path";
4193
4601
  var require2 = createRequire(import.meta.url);
4194
4602
  function readSessionDbMigrationIds(dbPath) {
4195
4603
  let db;
@@ -4255,7 +4663,7 @@ function loadSessionDbProvenanceState(path) {
4255
4663
  }
4256
4664
  function saveSessionDbProvenanceState(path, state) {
4257
4665
  try {
4258
- mkdirSync2(dirname3(path), { recursive: true });
4666
+ mkdirSync2(dirname4(path), { recursive: true });
4259
4667
  writeFileSync2(path, `${JSON.stringify(state, null, 2)}
4260
4668
  `, "utf8");
4261
4669
  } catch (error2) {
@@ -4311,6 +4719,9 @@ function isSessionDbProvenanceRecord(value) {
4311
4719
  return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
4312
4720
  }
4313
4721
 
4722
+ // src/lib/opencode/opencode2-package.ts
4723
+ var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
4724
+
4314
4725
  // src/lib/opencode/opencode-version-gate.ts
4315
4726
  var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
4316
4727
  function isQueueValidatedVersion(version2) {
@@ -4487,12 +4898,15 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
4487
4898
  }
4488
4899
  } catch {
4489
4900
  try {
4490
- const psOutput = execSync(`ps aux | grep -E "${psPattern}" | grep -v grep`, {
4901
+ const psOutput = execSync("ps aux", {
4491
4902
  encoding: "utf-8",
4492
- 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
4493
4906
  }).trim();
4494
4907
  if (psOutput) {
4495
4908
  for (const line of psOutput.split("\n")) {
4909
+ if (!psPattern.test(line)) continue;
4496
4910
  const parts = line.trim().split(/\s+/);
4497
4911
  if (parts.length >= 2) {
4498
4912
  const pid = parseInt(parts[1], 10);
@@ -4501,8 +4915,9 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
4501
4915
  }
4502
4916
  }
4503
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";
4504
4919
  console.warn(
4505
- `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}`
4506
4921
  );
4507
4922
  }
4508
4923
  }
@@ -4534,7 +4949,7 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
4534
4949
  return instances;
4535
4950
  }
4536
4951
  function findOpenCodeProcesses() {
4537
- return findProcessesByPattern("opencode serve|opencode-serve", "opencode (serve|--port)");
4952
+ return findProcessesByPattern("opencode serve|opencode-serve", /opencode (serve|--port)/);
4538
4953
  }
4539
4954
  async function scanPortsForOpenCode() {
4540
4955
  const instances = [];
@@ -4601,7 +5016,8 @@ async function startOpenCode(port, options = {}) {
4601
5016
  const child = spawn3(command, args, {
4602
5017
  detached: true,
4603
5018
  stdio: options.inheritStdio ? "inherit" : "ignore",
4604
- cwd: process.cwd()
5019
+ cwd: process.cwd(),
5020
+ env: { ...process.env, OPENCODE_SERVER_PASSWORD: void 0 }
4605
5021
  });
4606
5022
  return child;
4607
5023
  }
@@ -4617,7 +5033,7 @@ async function startOpenCode2(port, options = {}) {
4617
5033
  args = [
4618
5034
  "-y",
4619
5035
  "-p",
4620
- "@opencode-ai/cli@beta",
5036
+ OPENCODE2_PACKAGE_SPEC,
4621
5037
  "--",
4622
5038
  "opencode2",
4623
5039
  "serve",
@@ -4694,7 +5110,7 @@ async function promptOpenCodeInstall(interactive) {
4694
5110
  npm: "npm install -g opencode-ai",
4695
5111
  curl: "curl -fsSL https://opencode.ai/install.sh | sh",
4696
5112
  v2: {
4697
- npm: "npm install -g @opencode-ai/cli@beta",
5113
+ npm: `npm install -g ${OPENCODE2_PACKAGE_SPEC}`,
4698
5114
  curl: "curl -fsSL https://opencode.ai/v2/install | bash"
4699
5115
  }
4700
5116
  }
@@ -4828,7 +5244,10 @@ function selectSessionsToDelete(sessions, opts) {
4828
5244
  toDelete.push(s.id);
4829
5245
  }
4830
5246
  }
4831
- 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
+ );
4832
5251
  }
4833
5252
  var DEFAULT_INTERVAL = "1h";
4834
5253
  function resolve(flag, envValue, fallback) {
@@ -4888,13 +5307,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
4888
5307
  }
4889
5308
 
4890
5309
  // src/lib/opencode/session-db-size.ts
4891
- import { statSync as statSync3 } from "node:fs";
5310
+ import { statSync as statSync4 } from "node:fs";
4892
5311
  import { join as join6 } from "node:path";
4893
5312
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
4894
5313
  function statSessionDbBytes(homeDir) {
4895
5314
  const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
4896
5315
  try {
4897
- return statSync3(dbPath).size;
5316
+ return statSync4(dbPath).size;
4898
5317
  } catch (err) {
4899
5318
  const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
4900
5319
  if (!isMissingFile) {
@@ -4920,12 +5339,12 @@ function buildSessionStoreSizeWarning(input) {
4920
5339
  }
4921
5340
 
4922
5341
  // src/lib/opencode/log-tail.ts
4923
- import { statSync as statSync4 } from "node:fs";
4924
- import { homedir as homedir4 } from "node:os";
5342
+ import { statSync as statSync5 } from "node:fs";
5343
+ import { homedir as homedir5 } from "node:os";
4925
5344
  import { join as join7 } from "node:path";
4926
5345
  import { open as open2, stat } from "node:fs/promises";
4927
5346
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
4928
- function resolveOpenCodeLogPath(homeDir = homedir4(), env = process.env) {
5347
+ function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
4929
5348
  const dataDir = env.XDG_DATA_HOME || join7(homeDir, ".local", "share");
4930
5349
  return join7(dataDir, "opencode", "log", "opencode.log");
4931
5350
  }
@@ -4943,7 +5362,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
4943
5362
  let inode = null;
4944
5363
  let baselineReady = true;
4945
5364
  try {
4946
- const initial = statSync4(logPath);
5365
+ const initial = statSync5(logPath);
4947
5366
  offset = initial.size;
4948
5367
  inode = initial.ino;
4949
5368
  } catch (error2) {
@@ -5002,125 +5421,6 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
5002
5421
  };
5003
5422
  }
5004
5423
 
5005
- // src/lib/opencode/session-db-reclaim.ts
5006
- import { statSync as statSync5, statfsSync } from "node:fs";
5007
- import { dirname as dirname4 } from "node:path";
5008
- function errorMessage(error2) {
5009
- if (!(error2 instanceof Error)) return String(error2);
5010
- return error2.cause instanceof Error ? error2.cause.message : error2.message;
5011
- }
5012
- function insufficientSpaceReason(dbPath, requiredBytes) {
5013
- try {
5014
- const fsStats = statfsSync(dirname4(dbPath));
5015
- const availableBytes = fsStats.bavail * fsStats.bsize;
5016
- if (availableBytes < requiredBytes) {
5017
- return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
5018
- }
5019
- return null;
5020
- } catch (err) {
5021
- return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
5022
- }
5023
- }
5024
- function readLogicalBytes(db) {
5025
- const pageCount = db.prepare("PRAGMA page_count").get().page_count;
5026
- const pageSize = db.prepare("PRAGMA page_size").get().page_size;
5027
- return pageCount * pageSize;
5028
- }
5029
- function readCheckpointResult(db) {
5030
- const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
5031
- return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
5032
- }
5033
- async function probeReclaimAvailability(input) {
5034
- const { dbPath, requiredBytes } = input;
5035
- let sqlite;
5036
- try {
5037
- sqlite = await import("node:sqlite");
5038
- } catch (err) {
5039
- const detail = `Node ${process.version}: ${errorMessage(err)}`;
5040
- console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
5041
- return { reason: "sqlite-unavailable", detail };
5042
- }
5043
- let autoVacuum = null;
5044
- try {
5045
- const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
5046
- try {
5047
- db.exec("PRAGMA busy_timeout=5000");
5048
- autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
5049
- } finally {
5050
- db.close();
5051
- }
5052
- } catch (err) {
5053
- console.warn(
5054
- `[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
5055
- );
5056
- }
5057
- if (autoVacuum !== 0) return null;
5058
- return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
5059
- }
5060
- async function reclaimSessionDbSpace(input) {
5061
- const { dbPath, maxPages, allowFullVacuum = true } = input;
5062
- let sqlite;
5063
- try {
5064
- sqlite = await import("node:sqlite");
5065
- } catch (err) {
5066
- const detail = `Node ${process.version}: ${errorMessage(err)}`;
5067
- console.warn(
5068
- `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
5069
- );
5070
- return { ok: false, skipped: "sqlite-unavailable", detail };
5071
- }
5072
- const { DatabaseSync } = sqlite;
5073
- let db;
5074
- try {
5075
- db = new DatabaseSync(dbPath);
5076
- db.exec("PRAGMA busy_timeout=5000");
5077
- const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
5078
- if (autoVacuum === 0) {
5079
- if (!allowFullVacuum) {
5080
- console.warn(
5081
- `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
5082
- );
5083
- return { ok: false, skipped: "full-vacuum-blocked" };
5084
- }
5085
- const fileBytesForGuard = statSync5(dbPath).size;
5086
- const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
5087
- if (skipReason !== null) {
5088
- console.warn(
5089
- `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
5090
- );
5091
- return { ok: false, skipped: "insufficient-disk-space" };
5092
- }
5093
- const beforeBytes = readLogicalBytes(db);
5094
- db.exec("PRAGMA auto_vacuum=INCREMENTAL");
5095
- db.exec("VACUUM");
5096
- const afterBytes = readLogicalBytes(db);
5097
- const checkpoint = readCheckpointResult(db);
5098
- return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
5099
- }
5100
- if (autoVacuum === 2) {
5101
- const beforeBytes = readLogicalBytes(db);
5102
- const bound = Math.max(0, Math.trunc(maxPages));
5103
- db.exec(`PRAGMA incremental_vacuum(${bound})`);
5104
- const afterBytes = readLogicalBytes(db);
5105
- const checkpoint = readCheckpointResult(db);
5106
- return { ok: true, mode: "incremental", beforeBytes, afterBytes, checkpoint };
5107
- }
5108
- console.warn(
5109
- `[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
5110
- );
5111
- return { ok: false, skipped: "auto-vacuum-not-applicable" };
5112
- } catch (err) {
5113
- console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
5114
- return {
5115
- ok: false,
5116
- skipped: "reclaim-error",
5117
- detail: errorMessage(err)
5118
- };
5119
- } finally {
5120
- db?.close();
5121
- }
5122
- }
5123
-
5124
5424
  // src/lib/tunnel/connection.ts
5125
5425
  import WebSocket2 from "ws";
5126
5426
 
@@ -5350,6 +5650,8 @@ var StreamForwarder = class {
5350
5650
  if (done) break;
5351
5651
  const chunk = Buffer.from(value);
5352
5652
  for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
5653
+ await stream.pausePromise;
5654
+ if (ac.signal.aborted) break;
5353
5655
  const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
5354
5656
  if (this.binaryFramesSupported) {
5355
5657
  if (this.ws.readyState === WebSocket.OPEN) {
@@ -5392,6 +5694,7 @@ function classifyUpgradeRejection(headers) {
5392
5694
  }
5393
5695
  var MAX_RECONNECT_DELAY = 3e4;
5394
5696
  var BASE_RECONNECT_DELAY = 500;
5697
+ var RELAY_SILENCE_TIMEOUT_MS = 9e4;
5395
5698
  function getReconnectDelay(attempt) {
5396
5699
  const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
5397
5700
  const jitter = Math.random() * 1e3;
@@ -5463,10 +5766,27 @@ function connectTunnel(options) {
5463
5766
  ws.close();
5464
5767
  reject(new Error("Connection timeout"));
5465
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
+ };
5466
5785
  let upgradeRejection = null;
5467
5786
  let upgradeRejectionReason = null;
5468
5787
  ws.on("unexpected-response", (_req, res) => {
5469
5788
  clearTimeout(connectionTimeout);
5789
+ clearRelaySilenceTimeout();
5470
5790
  const reason = classifyUpgradeRejection(res.headers);
5471
5791
  upgradeRejectionReason = reason;
5472
5792
  const chunks = [];
@@ -5496,6 +5816,7 @@ function connectTunnel(options) {
5496
5816
  onInfo?.("WebSocket connection established");
5497
5817
  });
5498
5818
  ws.on("message", (data, isBinary) => {
5819
+ if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
5499
5820
  if (isBinary) {
5500
5821
  try {
5501
5822
  const frame = decodeBinaryBodyFrame(toUint8Array(data));
@@ -5529,6 +5850,7 @@ function connectTunnel(options) {
5529
5850
  switch (message.type) {
5530
5851
  case "connected": {
5531
5852
  clearTimeout(connectionTimeout);
5853
+ armRelaySilenceTimeout();
5532
5854
  forwarder.setBinaryFramesSupported(message.binary_frames === true);
5533
5855
  const connectedAgentId = message.agent_id ?? agentId;
5534
5856
  onConnected?.(connectedAgentId);
@@ -5553,6 +5875,7 @@ function connectTunnel(options) {
5553
5875
  });
5554
5876
  ws.on("error", (error2) => {
5555
5877
  clearTimeout(connectionTimeout);
5878
+ clearRelaySilenceTimeout();
5556
5879
  const detail = upgradeRejection ?? describeSocketError(error2, url);
5557
5880
  if (upgradeRejectionReason === "do_code_updated") {
5558
5881
  onWarning?.("Relay redeployed \u2014 reconnecting");
@@ -5564,7 +5887,8 @@ function connectTunnel(options) {
5564
5887
  );
5565
5888
  });
5566
5889
  ws.on("close", (code, reason) => {
5567
- 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");
5568
5892
  forwarder.abortAll();
5569
5893
  onDisconnected?.(code, reasonStr);
5570
5894
  });
@@ -5572,9 +5896,10 @@ function connectTunnel(options) {
5572
5896
  }
5573
5897
 
5574
5898
  // src/lib/tunnel/runner-connection.ts
5575
- var RunnerConnection = class {
5899
+ var RunnerConnection = class _RunnerConnection {
5576
5900
  opts;
5577
5901
  sleep;
5902
+ now;
5578
5903
  connection = null;
5579
5904
  resolvedAgentId;
5580
5905
  /** True while a (re)connect loop is in flight. */
@@ -5583,10 +5908,17 @@ var RunnerConnection = class {
5583
5908
  reconnectPromise = null;
5584
5909
  /** 1-based count of the current reconnect attempt streak. */
5585
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
+ ]);
5586
5917
  constructor(opts) {
5587
5918
  this.opts = opts;
5588
5919
  this.resolvedAgentId = opts.agentId;
5589
5920
  this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5921
+ this.now = opts.now ?? Date.now;
5590
5922
  }
5591
5923
  get agentId() {
5592
5924
  return this.resolvedAgentId;
@@ -5622,14 +5954,24 @@ var RunnerConnection = class {
5622
5954
  port: this.opts.port,
5623
5955
  openCodePassword: this.opts.openCodePassword,
5624
5956
  onConnected: (agentId) => {
5957
+ const attempt = this.reconnectAttempt + 1;
5625
5958
  this.reconnectAttempt = 0;
5626
5959
  this.reconnecting = false;
5627
5960
  this.resolvedAgentId = agentId;
5628
- 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
+ });
5629
5969
  },
5630
5970
  onDisconnected: (code, reason) => {
5631
- events.onDisconnected(code, reason);
5632
- 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) {
5633
5975
  this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
5634
5976
  events.onError?.(`Reconnection failed: ${err.message}`);
5635
5977
  });
@@ -5968,8 +6310,85 @@ function createResourceUsageCollector(homeDir) {
5968
6310
  };
5969
6311
  }
5970
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
+
5971
6390
  // src/lib/channels/driver.ts
5972
- import { homedir as homedir5 } from "node:os";
6391
+ import { homedir as homedir6 } from "node:os";
5973
6392
 
5974
6393
  // src/lib/runner-file-sync.ts
5975
6394
  import { join as join9 } from "node:path";
@@ -6426,6 +6845,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
6426
6845
  var MAX_BUFFERED_SESSION_ERRORS = 256;
6427
6846
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
6428
6847
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
6848
+ var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
6429
6849
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
6430
6850
  var HEARTBEAT_MS = 6e4;
6431
6851
  var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
@@ -6487,6 +6907,7 @@ var ChannelDriver = class _ChannelDriver {
6487
6907
  maxActiveSessions;
6488
6908
  watcherStallMs;
6489
6909
  wedgeWarningIntervalMs;
6910
+ sessionStoreHealth;
6490
6911
  openCodeClient;
6491
6912
  /** Cache of conversationId → opencode sessionId. */
6492
6913
  sessions = /* @__PURE__ */ new Map();
@@ -6852,10 +7273,11 @@ var ChannelDriver = class _ChannelDriver {
6852
7273
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
6853
7274
  this.now = config.now ?? (() => Date.now());
6854
7275
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
6855
- this.homeDir = config.homeDir ?? homedir5();
7276
+ this.homeDir = config.homeDir ?? homedir6();
6856
7277
  this.maxActiveSessions = config.maxActiveSessions;
6857
7278
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
6858
7279
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
7280
+ this.sessionStoreHealth = config.sessionStoreHealth;
6859
7281
  }
6860
7282
  get isV2() {
6861
7283
  return this.openCodeClient.version === "v2";
@@ -6871,7 +7293,9 @@ var ChannelDriver = class _ChannelDriver {
6871
7293
  return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6872
7294
  }
6873
7295
  async listSessions() {
6874
- 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
+ });
6875
7299
  }
6876
7300
  async sessionExists(sessionId) {
6877
7301
  return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
@@ -8327,6 +8751,7 @@ var ChannelDriver = class _ChannelDriver {
8327
8751
  b2AbandonedSignalled: false,
8328
8752
  ambiguousPinnedSinceMs: 0,
8329
8753
  ambiguousResolved: false,
8754
+ sessionStoreFailurePending: false,
8330
8755
  neverStartedResolved: false
8331
8756
  });
8332
8757
  const buffered = this.bufferedSessionErrors.get(sessionId);
@@ -8445,6 +8870,7 @@ var ChannelDriver = class _ChannelDriver {
8445
8870
  b2AbandonedSignalled: false,
8446
8871
  ambiguousPinnedSinceMs: 0,
8447
8872
  ambiguousResolved: false,
8873
+ sessionStoreFailurePending: false,
8448
8874
  neverStartedResolved: false
8449
8875
  });
8450
8876
  }
@@ -8858,6 +9284,10 @@ var ChannelDriver = class _ChannelDriver {
8858
9284
  if (state === "running" || state === "done" || state === "failed") {
8859
9285
  this.neverStartedAttempts.delete(id);
8860
9286
  }
9287
+ if (inFlight.sessionStoreFailurePending) {
9288
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
9289
+ return;
9290
+ }
8861
9291
  if (inFlight.neverStartedResolved) {
8862
9292
  if (state === "running" || state === "done" || state === "failed") {
8863
9293
  inFlight.neverStartedResolved = false;
@@ -9037,6 +9467,16 @@ var ChannelDriver = class _ChannelDriver {
9037
9467
  descendantOngoing,
9038
9468
  rootOngoing
9039
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
+ }
9040
9480
  inFlight.b2AbandonedSignalled = true;
9041
9481
  this.log({
9042
9482
  level: "warn",
@@ -9095,6 +9535,16 @@ var ChannelDriver = class _ChannelDriver {
9095
9535
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
9096
9536
  sessionOngoing: ongoing
9097
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
+ }
9098
9548
  inFlight.ambiguousResolved = true;
9099
9549
  this.log({
9100
9550
  level: "warn",
@@ -9374,6 +9824,66 @@ var ChannelDriver = class _ChannelDriver {
9374
9824
  this.neverStartedAttempts.delete(inFlight.evidentMessageId);
9375
9825
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9376
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
+ }
9377
9887
  // Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
9378
9888
  /**
9379
9889
  * Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
@@ -11279,8 +11789,16 @@ function checkNonInteractivePortConflict(port, isPortInUseFn) {
11279
11789
  );
11280
11790
  }
11281
11791
  }
11792
+ function unknownPasswordError(port) {
11793
+ return new Error(
11794
+ `OpenCode is already running on port ${port}, but this runner cannot authenticate to it. It may have been started with OPENCODE_SERVER_PASSWORD set in its environment. Free the port or pass --port.`
11795
+ );
11796
+ }
11282
11797
  async function ensureOpenCodeRunning(ctx) {
11283
11798
  const healthCheck = await checkOpenCodeHealth(ctx.port);
11799
+ if (healthCheck.authFailed) {
11800
+ throw unknownPasswordError(ctx.port);
11801
+ }
11284
11802
  if (healthCheck.healthy) {
11285
11803
  return {
11286
11804
  port: ctx.port,
@@ -11331,6 +11849,14 @@ async function ensureOpenCodeRunning(ctx) {
11331
11849
  const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
11332
11850
  const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
11333
11851
  if (!health.healthy) {
11852
+ if (health.authFailed) {
11853
+ return {
11854
+ port: ctx.port,
11855
+ process: proc,
11856
+ version: null,
11857
+ notReadyReason: "it is answering with an authentication challenge, likely because OPENCODE_SERVER_PASSWORD is set in the environment of the process that started it; a longer start timeout will not resolve this"
11858
+ };
11859
+ }
11334
11860
  return {
11335
11861
  port: ctx.port,
11336
11862
  process: proc,
@@ -11400,6 +11926,9 @@ Port ${port} is already in use.`));
11400
11926
  const health = await waitForOpenCodeHealth(port, INTERACTIVE_START_TIMEOUT_MS);
11401
11927
  if (!health.healthy) {
11402
11928
  spinner.fail("Failed to start OpenCode");
11929
+ if (health.authFailed) {
11930
+ throw unknownPasswordError(port);
11931
+ }
11403
11932
  throw new Error("OpenCode failed to start");
11404
11933
  }
11405
11934
  spinner.stop();
@@ -11412,7 +11941,7 @@ Port ${port} is already in use.`));
11412
11941
  import chalk6 from "chalk";
11413
11942
  import ora3 from "ora";
11414
11943
  import { select as select3 } from "@inquirer/prompts";
11415
- function unknownPasswordError(port) {
11944
+ function unknownPasswordError2(port) {
11416
11945
  return new Error(
11417
11946
  `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
11418
11947
  );
@@ -11421,7 +11950,7 @@ var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
11421
11950
  async function ensureOpenCode2Running(ctx) {
11422
11951
  const initialHealth = await checkOpenCode2Health(ctx.port);
11423
11952
  if (initialHealth.authFailed) {
11424
- throw unknownPasswordError(ctx.port);
11953
+ throw unknownPasswordError2(ctx.port);
11425
11954
  }
11426
11955
  if (initialHealth.healthy) {
11427
11956
  return {
@@ -11434,7 +11963,7 @@ async function ensureOpenCode2Running(ctx) {
11434
11963
  }
11435
11964
  if (!isOpenCode2Installed()) {
11436
11965
  throw new Error(
11437
- "OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
11966
+ `OpenCode V2 (opencode2) is not installed. Install it with: npm install -g ${OPENCODE2_PACKAGE_SPEC}`
11438
11967
  );
11439
11968
  }
11440
11969
  let port = ctx.port;
@@ -12086,6 +12615,7 @@ function createCredentialSync({
12086
12615
 
12087
12616
  // src/commands/run.ts
12088
12617
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
12618
+ var MAX_DISCONNECT_REASON_LENGTH = 200;
12089
12619
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
12090
12620
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
12091
12621
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
@@ -12241,7 +12771,13 @@ function logActivity(state, entry) {
12241
12771
  const level = entry.level ?? (entry.type === "error" ? "error" : "info");
12242
12772
  if (!meetsThreshold(state, level)) return;
12243
12773
  forwardRunnerActivity(
12244
- { 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
+ },
12245
12781
  { agentId: state.agentId, authHeader: state.authHeader }
12246
12782
  );
12247
12783
  const fullEntry = {
@@ -12263,7 +12799,7 @@ function logActivity(state, entry) {
12263
12799
  }
12264
12800
  function reportSessionDbRecovery(state) {
12265
12801
  try {
12266
- const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
12802
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
12267
12803
  for (const record of report.records) {
12268
12804
  const activity = buildSessionDbRecoveryActivity(record);
12269
12805
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -12298,7 +12834,9 @@ function displayStatus(state) {
12298
12834
  const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
12299
12835
  const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
12300
12836
  const last = state.activityLog[state.activityLog.length - 1];
12301
- 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
+ ) : "";
12302
12840
  const agent = state.agentName ?? state.agentId;
12303
12841
  console.log(
12304
12842
  `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
@@ -12317,7 +12855,7 @@ function reportOpenCodeV2CredentialImport(state, result) {
12317
12855
  logActivity(state, {
12318
12856
  type: "info",
12319
12857
  level: "warn",
12320
- 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.`
12321
12859
  });
12322
12860
  }
12323
12861
  if (result.imported.length === 0 && result.unsupported.length === 0 && result.failed.length === 0) {
@@ -12342,7 +12880,7 @@ async function importOpenCodeV2CredentialsForState(state) {
12342
12880
  }
12343
12881
  const result = await importOpenCodeV2Credentials({
12344
12882
  request: client.request,
12345
- homeDir: homedir6(),
12883
+ homeDir: homedir7(),
12346
12884
  directory,
12347
12885
  guard: state.opencodeV2CredentialImportGuard,
12348
12886
  log: (message, level = "info") => logActivity(state, { type: "info", level, message })
@@ -12550,9 +13088,9 @@ async function driveChannels(state, driver) {
12550
13088
  }
12551
13089
  }
12552
13090
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
12553
- var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
13091
+ var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
12554
13092
  function shouldWarnForReclaimSkip(reason) {
12555
- if (reason !== "sqlite-unavailable") return false;
13093
+ if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
12556
13094
  const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
12557
13095
  if (!version2) return false;
12558
13096
  const major = Number(version2[1]);
@@ -12561,7 +13099,14 @@ function shouldWarnForReclaimSkip(reason) {
12561
13099
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
12562
13100
  }
12563
13101
  function sessionDbPath() {
12564
- 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
+ });
12565
13110
  }
12566
13111
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
12567
13112
  const record = {
@@ -12602,69 +13147,136 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
12602
13147
  async function runSweep(state, driver, config) {
12603
13148
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
12604
13149
  try {
12605
- const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
12606
- 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) {
12607
13154
  logActivity(state, {
12608
13155
  type: "info",
12609
- 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})`
12610
13158
  });
12611
- return;
12612
13159
  }
12613
- const toDelete = selectSessionsToDelete(
12614
- sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
12615
- {
12616
- maxAgeMs: config.maxAgeMs,
12617
- maxCount: config.maxCount,
12618
- nowMs: Date.now(),
12619
- protectedIds: driver.protectedSessionIds()
12620
- }
12621
- );
12622
- const protectedNow = driver.protectedSessionIds();
12623
13160
  let deleted = 0;
12624
13161
  let failed = 0;
13162
+ let failed4xx = 0;
13163
+ let failed5xx = 0;
13164
+ let failedOtherStatus = 0;
13165
+ let failedThrew = 0;
12625
13166
  let skippedNewlyActive = 0;
12626
- for (const id of toDelete) {
12627
- if (protectedNow.has(id)) {
12628
- skippedNewlyActive++;
12629
- logActivity(state, {
12630
- type: "info",
12631
- message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
12632
- });
12633
- 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
+ }
12634
13204
  }
12635
- if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
12636
- 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
+ });
12637
13212
  }
12638
- const failedNote = failed > 0 ? `, failed ${failed}` : "";
12639
- const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
12640
- logActivity(state, {
12641
- type: "info",
12642
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`
12643
- });
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
+ };
12644
13225
  const reclaimResult = await reclaimSessionDbSpace({
12645
13226
  dbPath: sessionDbPath(),
12646
- 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,
12647
13230
  allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
12648
13231
  });
12649
13232
  if (reclaimResult.ok) {
13233
+ state.sessionStoreHealth.observeWritable();
12650
13234
  const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
12651
13235
  const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
12652
13236
  const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
12653
- logActivity(state, {
13237
+ logSessionCleanupActivity(state, "sweep", {
12654
13238
  type: "info",
12655
- 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
+ }
12656
13254
  });
12657
13255
  } else {
12658
- logActivity(state, {
13256
+ if (reclaimResult.skipped === "disk-exhaustion") {
13257
+ state.sessionStoreHealth.observeUnwritable();
13258
+ }
13259
+ logSessionCleanupActivity(state, "sweep", {
12659
13260
  type: "info",
12660
- 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
+ }
12661
13269
  });
12662
13270
  }
12663
13271
  } catch (error2) {
12664
13272
  const message = error2 instanceof Error ? error2.message : String(error2);
12665
- logActivity(state, {
13273
+ logSessionCleanupActivity(state, "sweep", {
12666
13274
  type: "error",
12667
- 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
+ }
12668
13280
  });
12669
13281
  }
12670
13282
  }
@@ -12680,14 +13292,19 @@ function scheduleSessionCleanup(state, driver, options) {
12680
13292
  for (const warning2 of config.warnings) {
12681
13293
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
12682
13294
  }
12683
- const dbBytes = statSessionDbBytes(homedir6());
13295
+ const dbBytes = statSessionDbBytes(homedir7());
12684
13296
  void (async () => {
12685
13297
  const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
12686
13298
  if (reclaimAvailability !== null) {
12687
- logActivity(state, {
13299
+ logSessionCleanupActivity(state, "startup", {
12688
13300
  type: "info",
12689
13301
  level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
12690
- 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
+ }
12691
13308
  });
12692
13309
  }
12693
13310
  const sizeWarning = buildSessionStoreSizeWarning({
@@ -12704,9 +13321,15 @@ function scheduleSessionCleanup(state, driver, options) {
12704
13321
  );
12705
13322
  });
12706
13323
  if (!config.enabled) return;
12707
- logActivity(state, {
13324
+ logSessionCleanupActivity(state, "startup", {
12708
13325
  type: "info",
12709
- 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
+ }
12710
13333
  });
12711
13334
  const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
12712
13335
  const firstSweep = setTimeout(
@@ -12885,7 +13508,7 @@ function scheduleResourceUsageReporting(state, options) {
12885
13508
  });
12886
13509
  return;
12887
13510
  }
12888
- const { collect, stop } = createResourceUsageCollector(homedir6());
13511
+ const { collect, stop } = createResourceUsageCollector(homedir7());
12889
13512
  state.stopResourceUsageSampling = stop;
12890
13513
  let consecutiveFailures = 0;
12891
13514
  const tick = async () => {
@@ -12898,6 +13521,19 @@ function scheduleResourceUsageReporting(state, options) {
12898
13521
  message: `Resource usage collection: ${warning2}`
12899
13522
  });
12900
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
+ }
12901
13537
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
12902
13538
  if (result.ok) {
12903
13539
  if (consecutiveFailures > 0) {
@@ -13110,7 +13746,7 @@ async function run(options) {
13110
13746
  let fileSyncDirectories;
13111
13747
  try {
13112
13748
  logLevel = resolveLogLevel(options);
13113
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
13749
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
13114
13750
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
13115
13751
  throw new Error(
13116
13752
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -13144,6 +13780,7 @@ async function run(options) {
13144
13780
  opencodeClient: null,
13145
13781
  opencodeV2CredentialImportGuard: { inFlight: false },
13146
13782
  sessionDbProvenanceAnomaly: false,
13783
+ sessionStoreHealth: createSessionStoreHealth(),
13147
13784
  opencodeProcess: null,
13148
13785
  stopOpenCodeLogTail: null,
13149
13786
  litestreamProcess: null,
@@ -13161,10 +13798,12 @@ async function run(options) {
13161
13798
  openaiUsageRearm: null,
13162
13799
  resourceUsageTimer: null,
13163
13800
  stopResourceUsageSampling: null,
13801
+ lowDiskWarningState: { worstBand: null, warnedAtMs: null },
13164
13802
  credentialSync: null,
13165
13803
  authHeader: ""
13166
13804
  };
13167
13805
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
13806
+ setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
13168
13807
  if (options.credentialSyncMarker) {
13169
13808
  state.credentialSync = createCredentialSync({
13170
13809
  markerPath: options.credentialSyncMarker,
@@ -13350,13 +13989,21 @@ async function run(options) {
13350
13989
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13351
13990
  }
13352
13991
  state.credentialSync?.arm();
13353
- state.stopOpenCodeLogTail = tailOpenCodeLogFile(
13354
- resolveOpenCodeLogPath(homedir6(), process.env),
13355
- createOpenCodeActivityForwarder(() => ({
13992
+ const openCodeActivityForwarder = createOpenCodeActivityForwarder(
13993
+ () => ({
13356
13994
  agentId: state.agentId,
13357
13995
  authHeader: state.authHeader
13358
- }))
13359
- ).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
+ };
13360
14007
  let sessionDbVerifyFatal = false;
13361
14008
  if (!options.restoreSessionDb) {
13362
14009
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -13368,6 +14015,12 @@ async function run(options) {
13368
14015
  "Skipping session-DB restore: OpenCode is already serving this database",
13369
14016
  "debug"
13370
14017
  );
14018
+ } else if (health.authFailed) {
14019
+ log2(
14020
+ state,
14021
+ "Skipping session-DB restore: OpenCode answered with an authentication challenge; assuming this database is in use",
14022
+ "debug"
14023
+ );
13371
14024
  } else {
13372
14025
  const result = await restoreAndVerifySessionDb({
13373
14026
  dbPath: sessionDbPath(),
@@ -13467,7 +14120,7 @@ async function run(options) {
13467
14120
  const provenance = checkSessionDbProvenance({
13468
14121
  dbPath: sessionDbPath(),
13469
14122
  currentVersion: state.opencodeVersion,
13470
- homeDir: homedir6(),
14123
+ homeDir: homedir7(),
13471
14124
  env: process.env
13472
14125
  });
13473
14126
  if (provenance.anomaly) {
@@ -13634,10 +14287,11 @@ async function run(options) {
13634
14287
  getAuthHeader: () => state.authHeader,
13635
14288
  conversationFilter: state.conversationFilter,
13636
14289
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
14290
+ sessionStoreHealth: state.sessionStoreHealth,
13637
14291
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
13638
14292
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
13639
14293
  fileSyncDirectories,
13640
- homeDir: homedir6(),
14294
+ homeDir: homedir7(),
13641
14295
  maxActiveSessions,
13642
14296
  log: (entry) => (
13643
14297
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -13659,12 +14313,13 @@ async function run(options) {
13659
14313
  openCodePassword: state.opencodePassword,
13660
14314
  isRunning: () => state.running,
13661
14315
  events: {
13662
- onConnected: (agentId, isReconnect) => {
14316
+ onConnected: (agentId, isReconnect, transition) => {
13663
14317
  state.connected = true;
13664
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})`;
13665
14320
  logActivity(state, {
13666
14321
  type: "info",
13667
- message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (runner: ${agentId})`
14322
+ message: connectionMessage
13668
14323
  });
13669
14324
  if (options.tunnelReadyFile) {
13670
14325
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
@@ -13685,7 +14340,12 @@ async function run(options) {
13685
14340
  version: state.opencodeVersion,
13686
14341
  major: state.opencodeApiVersion,
13687
14342
  connected: state.opencodeConnected
13688
- })
14343
+ }),
14344
+ connection_generation: transition.generation,
14345
+ ...isReconnect && transition.downtimeMs !== null ? {
14346
+ reconnect_attempt: transition.attempt,
14347
+ downtime_ms: transition.downtimeMs
14348
+ } : {}
13689
14349
  });
13690
14350
  if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
13691
14351
  if (state.interactive) displayStatus(state);
@@ -13707,13 +14367,18 @@ async function run(options) {
13707
14367
  if (state.interactive) displayStatus(state);
13708
14368
  });
13709
14369
  },
13710
- onDisconnected: (code, reason) => {
14370
+ onDisconnected: (code, reason, transition) => {
13711
14371
  state.connected = false;
14372
+ const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
13712
14373
  logActivity(state, {
13713
14374
  type: "info",
13714
- 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
13715
14381
  });
13716
- emitAgentDisconnected(state.agentId, { code, reason });
13717
14382
  if (state.interactive) displayStatus(state);
13718
14383
  },
13719
14384
  onError: (error2) => {
@@ -13813,7 +14478,7 @@ async function run(options) {
13813
14478
  },
13814
14479
  report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
13815
14480
  isLocalCredentialProblem: isLocalCredentialProblem2,
13816
- 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`",
13817
14482
  firstDelayMs: firstReportDelayMs,
13818
14483
  nextDelayMs: usageReportDelayMs,
13819
14484
  failureLogLevel: usageReportFailureLogLevel
@@ -13856,7 +14521,7 @@ async function run(options) {
13856
14521
  }
13857
14522
 
13858
14523
  // src/index.ts
13859
- var { version } = createRequire2(import.meta.url)("../package.json");
14524
+ var version = getCliVersion();
13860
14525
  var program = new Command();
13861
14526
  program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
13862
14527
  "--endpoint <url>",