@evident-ai/cli 3.5.2-dev.8d3aa06 → 3.5.2-dev.afae02b

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,13 +1062,83 @@ 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
 
1070
1069
  // ../../packages/types/src/agents/index.ts
1071
1070
  var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
1072
1071
 
1072
+ // ../../packages/types/src/opencode/index.ts
1073
+ function roleOf(m) {
1074
+ if (!m || typeof m !== "object") return void 0;
1075
+ if (typeof m.role === "string") return m.role;
1076
+ const infoRole = m.info?.role;
1077
+ return typeof infoRole === "string" ? infoRole : void 0;
1078
+ }
1079
+ function idOf(m) {
1080
+ if (!m || typeof m !== "object") return void 0;
1081
+ if (typeof m.id === "string") return m.id;
1082
+ const infoId = m.info?.id;
1083
+ return typeof infoId === "string" ? infoId : void 0;
1084
+ }
1085
+ function parentIdOf(m) {
1086
+ if (!m || typeof m !== "object") return void 0;
1087
+ if (typeof m.parentID === "string") return m.parentID;
1088
+ const infoParent = m.info?.parentID;
1089
+ return typeof infoParent === "string" ? infoParent : void 0;
1090
+ }
1091
+ function textPartsOf(m) {
1092
+ if (!m || typeof m !== "object") return [];
1093
+ const parts = Array.isArray(m.parts) ? m.parts : [];
1094
+ return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text ?? "").filter(Boolean);
1095
+ }
1096
+ function correlatedAssistantRun(messages, userMessageId) {
1097
+ if (!messages || messages.length === 0) return [];
1098
+ const byParent = messages.filter(
1099
+ (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
1100
+ );
1101
+ if (byParent.length > 0) return byParent;
1102
+ const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
1103
+ if (userIndex === -1) return [];
1104
+ const run2 = [];
1105
+ for (let i = userIndex + 1; i < messages.length; i++) {
1106
+ const role = roleOf(messages[i]);
1107
+ if (role === "user") break;
1108
+ if (role === "assistant") run2.push(messages[i]);
1109
+ }
1110
+ return run2;
1111
+ }
1112
+ function lastText(assistantRun) {
1113
+ for (let i = assistantRun.length - 1; i >= 0; i--) {
1114
+ const textParts = textPartsOf(assistantRun[i]);
1115
+ if (textParts.length > 0) return textParts.join("\n").trim() || null;
1116
+ }
1117
+ return null;
1118
+ }
1119
+ function effectiveCorrelatedRun(messages, userMessageId) {
1120
+ const run2 = correlatedAssistantRun(messages, userMessageId);
1121
+ if (lastText(run2) !== null) return run2;
1122
+ const userIndex = messages?.findIndex((m) => idOf(m) === userMessageId) ?? -1;
1123
+ if (userIndex === -1) return run2;
1124
+ let continuation;
1125
+ for (let i = userIndex + 1; messages && i < messages.length; i++) {
1126
+ if (roleOf(messages[i]) === "user") {
1127
+ continuation = messages[i];
1128
+ break;
1129
+ }
1130
+ }
1131
+ const continuationId = idOf(continuation);
1132
+ if (roleOf(continuation) === "user" && textPartsOf(continuation).join("\n").trim().toLowerCase() === "continue" && continuationId) {
1133
+ const continuationRun = correlatedAssistantRun(messages, continuationId);
1134
+ if (continuationRun.length > 0) return continuationRun;
1135
+ }
1136
+ return run2;
1137
+ }
1138
+ function extractReplyTextForMessage(messages, opencodeUserMessageId) {
1139
+ return lastText(effectiveCorrelatedRun(messages, opencodeUserMessageId));
1140
+ }
1141
+
1073
1142
  // ../../packages/types/src/telemetry/index.ts
1074
1143
  var TelemetryEventTypes = {
1075
1144
  // Agent activity events (shown in web UI activity log)
@@ -1158,7 +1227,7 @@ import ora4 from "ora";
1158
1227
  import { select as select4 } from "@inquirer/prompts";
1159
1228
 
1160
1229
  // src/lib/telemetry.ts
1161
- var CLI_VERSION = (true ? "3.5.2-dev.8d3aa06" : void 0) ?? process.env.npm_package_version ?? "unknown";
1230
+ var CLI_VERSION = (true ? "3.5.2-dev.afae02b" : void 0) ?? process.env.npm_package_version ?? "unknown";
1162
1231
  function getCliVersion() {
1163
1232
  return CLI_VERSION;
1164
1233
  }
@@ -1172,6 +1241,23 @@ var authProvider = null;
1172
1241
  function setTelemetryAuthProvider(provider) {
1173
1242
  authProvider = provider;
1174
1243
  }
1244
+ var diagnosticsReporter = null;
1245
+ function setTelemetryDiagnosticsReporter(reporter) {
1246
+ diagnosticsReporter = reporter;
1247
+ }
1248
+ function reportTelemetryDiagnostic(message) {
1249
+ if (!diagnosticsReporter) {
1250
+ console.error(message);
1251
+ return;
1252
+ }
1253
+ try {
1254
+ diagnosticsReporter(message);
1255
+ } catch (error2) {
1256
+ console.error(
1257
+ `Telemetry diagnostics reporter failed: ${error2 instanceof Error ? error2.message : String(error2)}; original diagnostic: ${message}`
1258
+ );
1259
+ }
1260
+ }
1175
1261
  var FLUSH_FAILURE_LOG_INTERVAL_MS = 6e4;
1176
1262
  var lastFlushFailureLoggedAt = 0;
1177
1263
  var suppressedFlushFailureCount = 0;
@@ -1237,7 +1323,9 @@ async function flushEvents() {
1237
1323
  signal: controller.signal
1238
1324
  });
1239
1325
  if (!response.ok) {
1240
- console.error(`Telemetry flush failed: ${response.status}`);
1326
+ reportTelemetryDiagnostic(
1327
+ `Telemetry could not send events (HTTP ${response.status}); runner and OpenCode are unaffected`
1328
+ );
1241
1329
  }
1242
1330
  } finally {
1243
1331
  clearTimeout(timeout);
@@ -1247,7 +1335,9 @@ async function flushEvents() {
1247
1335
  if (now - lastFlushFailureLoggedAt >= FLUSH_FAILURE_LOG_INTERVAL_MS) {
1248
1336
  const message = error2 instanceof Error ? error2.message : String(error2);
1249
1337
  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}`);
1338
+ reportTelemetryDiagnostic(
1339
+ `Telemetry flush failed: ${message}; runner and OpenCode are unaffected${suffix}`
1340
+ );
1251
1341
  lastFlushFailureLoggedAt = now;
1252
1342
  suppressedFlushFailureCount = 0;
1253
1343
  } else {
@@ -1299,8 +1389,227 @@ var EventTypes = {
1299
1389
  };
1300
1390
 
1301
1391
  // src/lib/runner-activity-telemetry.ts
1302
- var FORWARDED_LEVELS = /* @__PURE__ */ new Set(["warn", "error"]);
1392
+ import { homedir as homedir2 } from "node:os";
1393
+
1394
+ // src/lib/opencode/session-db-reclaim.ts
1395
+ import { statSync as statSync2, statfsSync } from "node:fs";
1396
+ import { dirname as dirname2 } from "node:path";
1397
+ var SESSION_DB_RECLAIM_CHUNK_PAGES = 2e3;
1398
+ var SESSION_DB_RECLAIM_TIME_BUDGET_MS = 500;
1399
+ var SESSION_DB_RECLAIM_PAGE_CEILING = 4e4;
1400
+ function errorMessage(error2) {
1401
+ if (!(error2 instanceof Error)) return String(error2);
1402
+ return error2.cause instanceof Error ? error2.cause.message : error2.message;
1403
+ }
1404
+ function isSessionStoreWriteFailure(error2) {
1405
+ const messages = [];
1406
+ const visited = /* @__PURE__ */ new Set();
1407
+ let current = error2;
1408
+ while (current !== null && current !== void 0 && !visited.has(current)) {
1409
+ visited.add(current);
1410
+ if (typeof current === "string") {
1411
+ messages.push(current);
1412
+ break;
1413
+ }
1414
+ if (current instanceof Error) {
1415
+ messages.push(current.name, current.message);
1416
+ current = current.cause;
1417
+ continue;
1418
+ }
1419
+ if (typeof current === "object") {
1420
+ const record = current;
1421
+ if (typeof record.code === "string") messages.push(record.code);
1422
+ if (typeof record.message === "string") messages.push(record.message);
1423
+ current = record.cause;
1424
+ continue;
1425
+ }
1426
+ break;
1427
+ }
1428
+ return messages.some(
1429
+ (message) => /(?:database or disk is full|disk is full|no space left on device|SQLITE_(?:FULL|IOERR)(?:\b|_))/i.test(
1430
+ message
1431
+ )
1432
+ );
1433
+ }
1434
+ function insufficientSpaceReason(dbPath, requiredBytes) {
1435
+ try {
1436
+ const fsStats = statfsSync(dirname2(dbPath));
1437
+ const availableBytes = fsStats.bavail * fsStats.bsize;
1438
+ if (availableBytes < requiredBytes) {
1439
+ return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
1440
+ }
1441
+ return null;
1442
+ } catch (err) {
1443
+ return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
1444
+ }
1445
+ }
1446
+ function readLogicalBytes(db) {
1447
+ const pageCount = db.prepare("PRAGMA page_count").get().page_count;
1448
+ const pageSize = db.prepare("PRAGMA page_size").get().page_size;
1449
+ return pageCount * pageSize;
1450
+ }
1451
+ function readFreelistCount(db) {
1452
+ return db.prepare("PRAGMA freelist_count").get().freelist_count;
1453
+ }
1454
+ function readCheckpointResult(db) {
1455
+ const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
1456
+ return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
1457
+ }
1458
+ async function probeReclaimAvailability(input) {
1459
+ const { dbPath, requiredBytes } = input;
1460
+ let sqlite;
1461
+ try {
1462
+ sqlite = await import("node:sqlite");
1463
+ } catch (err) {
1464
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
1465
+ console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
1466
+ return { reason: "sqlite-unavailable", detail };
1467
+ }
1468
+ let autoVacuum = null;
1469
+ try {
1470
+ const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
1471
+ try {
1472
+ db.exec("PRAGMA busy_timeout=5000");
1473
+ autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
1474
+ } finally {
1475
+ db.close();
1476
+ }
1477
+ } catch (err) {
1478
+ console.warn(
1479
+ `[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
1480
+ );
1481
+ }
1482
+ if (autoVacuum !== 0) return null;
1483
+ return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
1484
+ }
1485
+ async function reclaimSessionDbSpace(input) {
1486
+ const {
1487
+ dbPath,
1488
+ pageCeiling = SESSION_DB_RECLAIM_PAGE_CEILING,
1489
+ timeBudgetMs = SESSION_DB_RECLAIM_TIME_BUDGET_MS,
1490
+ chunkPages = SESSION_DB_RECLAIM_CHUNK_PAGES,
1491
+ now = () => performance.now(),
1492
+ allowFullVacuum = true
1493
+ } = input;
1494
+ let sqlite;
1495
+ try {
1496
+ sqlite = await import("node:sqlite");
1497
+ } catch (err) {
1498
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
1499
+ console.warn(
1500
+ `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
1501
+ );
1502
+ return { ok: false, skipped: "sqlite-unavailable", detail };
1503
+ }
1504
+ const { DatabaseSync } = sqlite;
1505
+ let db;
1506
+ try {
1507
+ db = new DatabaseSync(dbPath);
1508
+ db.exec("PRAGMA busy_timeout=5000");
1509
+ const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
1510
+ if (autoVacuum === 0) {
1511
+ if (!allowFullVacuum) {
1512
+ console.warn(
1513
+ `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
1514
+ );
1515
+ return {
1516
+ ok: false,
1517
+ skipped: "full-vacuum-blocked",
1518
+ detail: "a session turn is live"
1519
+ };
1520
+ }
1521
+ const fileBytesForGuard = statSync2(dbPath).size;
1522
+ const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
1523
+ if (skipReason !== null) {
1524
+ console.warn(
1525
+ `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
1526
+ );
1527
+ return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
1528
+ }
1529
+ const beforeBytes = readLogicalBytes(db);
1530
+ const freelistBefore = readFreelistCount(db);
1531
+ const startedAt = now();
1532
+ db.exec("PRAGMA auto_vacuum=INCREMENTAL");
1533
+ db.exec("VACUUM");
1534
+ const afterBytes = readLogicalBytes(db);
1535
+ const freelistAfter = readFreelistCount(db);
1536
+ const checkpoint = readCheckpointResult(db);
1537
+ return {
1538
+ ok: true,
1539
+ mode: "convert",
1540
+ beforeBytes,
1541
+ afterBytes,
1542
+ freelistBefore,
1543
+ freelistAfter,
1544
+ pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
1545
+ stoppedBy: "freelist-drained",
1546
+ elapsedMs: Math.max(0, now() - startedAt),
1547
+ checkpoint
1548
+ };
1549
+ }
1550
+ if (autoVacuum === 2) {
1551
+ const beforeBytes = readLogicalBytes(db);
1552
+ const freelistBefore = readFreelistCount(db);
1553
+ const startedAt = now();
1554
+ const pageLimit = Math.max(0, Math.trunc(pageCeiling));
1555
+ const chunkLimit = Math.max(1, Math.trunc(chunkPages));
1556
+ const budget = Math.max(0, timeBudgetMs);
1557
+ let pagesAttempted = 0;
1558
+ let freelistAfter = freelistBefore;
1559
+ let stoppedBy = "freelist-drained";
1560
+ while (freelistAfter > 0) {
1561
+ if (pagesAttempted >= pageLimit) {
1562
+ stoppedBy = "page-ceiling";
1563
+ break;
1564
+ }
1565
+ if (now() - startedAt >= budget) {
1566
+ stoppedBy = "time-budget";
1567
+ break;
1568
+ }
1569
+ const pages = Math.min(chunkLimit, pageLimit - pagesAttempted, freelistAfter);
1570
+ db.exec(`PRAGMA incremental_vacuum(${pages})`);
1571
+ pagesAttempted += pages;
1572
+ freelistAfter = readFreelistCount(db);
1573
+ }
1574
+ const afterBytes = readLogicalBytes(db);
1575
+ const checkpoint = readCheckpointResult(db);
1576
+ return {
1577
+ ok: true,
1578
+ mode: "incremental",
1579
+ beforeBytes,
1580
+ afterBytes,
1581
+ freelistBefore,
1582
+ freelistAfter,
1583
+ pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
1584
+ stoppedBy,
1585
+ elapsedMs: Math.max(0, now() - startedAt),
1586
+ checkpoint
1587
+ };
1588
+ }
1589
+ console.warn(
1590
+ `[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
1591
+ );
1592
+ return { ok: false, skipped: "auto-vacuum-not-applicable" };
1593
+ } catch (err) {
1594
+ console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
1595
+ return {
1596
+ ok: false,
1597
+ skipped: isSessionStoreWriteFailure(err) ? "disk-exhaustion" : "reclaim-error",
1598
+ detail: errorMessage(err)
1599
+ };
1600
+ } finally {
1601
+ db?.close();
1602
+ }
1603
+ }
1604
+
1605
+ // src/lib/runner-activity-telemetry.ts
1606
+ var FORWARDED_LEVELS_BY_SOURCE = {
1607
+ "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
1608
+ opencode: /* @__PURE__ */ new Set(["warn", "error"]),
1609
+ "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"])
1610
+ };
1303
1611
  var SEVERITY_BY_LEVEL = {
1612
+ info: "info",
1304
1613
  warn: "warning",
1305
1614
  error: "error"
1306
1615
  };
@@ -1310,26 +1619,85 @@ function parseOpenCodeLogLine(line) {
1310
1619
  if (!levelMatch) return null;
1311
1620
  const level = levelMatch[1].toUpperCase();
1312
1621
  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] };
1622
+ const sessionMatch = normalisedLine.match(/(?:^|\s)(?:sessionID|session\.id)=(\S+)/);
1623
+ const messageIDMatch = normalisedLine.match(/(?:^|\s)messageID=(\S+)/);
1624
+ const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
1625
+ const parsed = {
1626
+ level: level === "WARN" ? "warn" : "error",
1627
+ sessionID: sessionMatch?.[1],
1628
+ messageID: messageIDMatch?.[1]
1629
+ };
1630
+ const message = messageMatch?.[1] ?? messageMatch?.[2];
1631
+ if (message !== void 0) parsed.message = message;
1632
+ return parsed;
1315
1633
  }
1316
1634
  var MAX_LINE_BUFFER_BYTES = 16 * 1024;
1317
- function createOpenCodeActivityForwarder(getContext) {
1635
+ var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
1636
+ function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
1318
1637
  let buffer = Buffer.alloc(0);
1319
- const flushLine = (line) => {
1320
- const parsed = parseOpenCodeLogLine(line);
1321
- if (!parsed) return;
1638
+ const repeatStates = /* @__PURE__ */ new Map();
1639
+ const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
1640
+ const metadata = {
1641
+ ...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
1642
+ ...parsed.messageID ? { messageID: parsed.messageID } : {},
1643
+ ...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
1644
+ };
1322
1645
  forwardRunnerActivity(
1323
1646
  {
1324
1647
  level: parsed.level,
1325
1648
  error: line,
1326
- metadata: parsed.sessionID ? { sessionID: parsed.sessionID } : void 0,
1649
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
1327
1650
  source: "opencode"
1328
1651
  },
1329
1652
  getContext()
1330
1653
  );
1331
1654
  };
1332
- return (chunk) => {
1655
+ const expireRepeatStates = (now, currentKey) => {
1656
+ for (const [key, state] of repeatStates) {
1657
+ if (key === currentKey || now - state.lastForwardedAt < REPEAT_SUPPRESSION_WINDOW_MS) {
1658
+ continue;
1659
+ }
1660
+ if (state.repeatsSuppressed > 0) {
1661
+ forwardLine(state.line, state.parsed, state.repeatsSuppressed);
1662
+ }
1663
+ repeatStates.delete(key);
1664
+ }
1665
+ };
1666
+ const flushLine = (line) => {
1667
+ if (onSessionStoreWriteFailure && isSessionStoreWriteFailure(line)) {
1668
+ try {
1669
+ onSessionStoreWriteFailure();
1670
+ } catch (err) {
1671
+ reportTelemetryDiagnostic(
1672
+ `Runner activity telemetry could not record session-store write failure: ${err instanceof Error ? err.message : String(err)}`
1673
+ );
1674
+ }
1675
+ }
1676
+ const parsed = parseOpenCodeLogLine(line);
1677
+ if (!parsed) return;
1678
+ const now = Date.now();
1679
+ const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}\0${parsed.sessionID ?? ""}\0${parsed.messageID ?? ""}`;
1680
+ expireRepeatStates(now, key);
1681
+ if (key === void 0) {
1682
+ forwardLine(line, parsed);
1683
+ return;
1684
+ }
1685
+ const previous = repeatStates.get(key);
1686
+ if (!previous || now - previous.lastForwardedAt >= REPEAT_SUPPRESSION_WINDOW_MS) {
1687
+ forwardLine(line, parsed, previous?.repeatsSuppressed ?? 0);
1688
+ repeatStates.set(key, {
1689
+ line,
1690
+ parsed,
1691
+ lastForwardedAt: now,
1692
+ repeatsSuppressed: 0
1693
+ });
1694
+ return;
1695
+ }
1696
+ previous.line = line;
1697
+ previous.parsed = parsed;
1698
+ previous.repeatsSuppressed++;
1699
+ };
1700
+ const forward = ((chunk) => {
1333
1701
  buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
1334
1702
  let newlineIndex;
1335
1703
  while ((newlineIndex = buffer.indexOf(10)) !== -1) {
@@ -1340,14 +1708,44 @@ function createOpenCodeActivityForwarder(getContext) {
1340
1708
  flushLine(buffer.toString("utf-8"));
1341
1709
  buffer = Buffer.alloc(0);
1342
1710
  }
1711
+ });
1712
+ forward.flush = () => {
1713
+ for (const state of repeatStates.values()) {
1714
+ if (state.repeatsSuppressed > 0) {
1715
+ forwardLine(state.line, state.parsed, state.repeatsSuppressed);
1716
+ }
1717
+ }
1718
+ repeatStates.clear();
1343
1719
  };
1720
+ return forward;
1344
1721
  }
1345
1722
  var MAX_MESSAGE_LENGTH = 500;
1346
1723
  var MAX_METADATA_VALUE_LENGTH = 200;
1347
1724
  var MAX_METADATA_ENTRIES = 20;
1348
1725
  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>");
1726
+ function redactHomeDirectory(message) {
1727
+ const home = homedir2();
1728
+ if (!home || home === "/") {
1729
+ return message;
1730
+ }
1731
+ const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
1732
+ let result = "";
1733
+ let searchFrom = 0;
1734
+ let homeIndex;
1735
+ while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
1736
+ const homeEnd = homeIndex + home.length;
1737
+ const previousCharacter = message[homeIndex - 1];
1738
+ const nextCharacter = message[homeEnd];
1739
+ const atPathBoundary = home === "/" || (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
1740
+ result += message.slice(searchFrom, homeIndex);
1741
+ result += atPathBoundary ? "~" : home;
1742
+ searchFrom = homeEnd;
1743
+ }
1744
+ return result + message.slice(searchFrom);
1745
+ }
1746
+ function redactDiagnosticText(message) {
1747
+ 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>");
1748
+ return redactHomeDirectory(redacted);
1351
1749
  }
1352
1750
  function truncate(message) {
1353
1751
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
@@ -1357,14 +1755,17 @@ function sanitiseMetadata(metadata) {
1357
1755
  if (!metadata) return {};
1358
1756
  const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
1359
1757
  if (Object.keys(metadata).length > entries.length) {
1360
- console.error(
1361
- `[runner-activity-telemetry] dropped ${Object.keys(metadata).length - entries.length} metadata entries`
1758
+ reportTelemetryDiagnostic(
1759
+ `Runner activity telemetry dropped ${Object.keys(metadata).length - entries.length} metadata entries; those entries were not sent`
1362
1760
  );
1363
1761
  }
1364
1762
  const sanitised = [];
1365
1763
  for (const [key, value] of entries) {
1366
1764
  if (typeof value === "string") {
1367
- sanitised.push([key, truncate(redact(value).slice(0, MAX_METADATA_VALUE_LENGTH))]);
1765
+ sanitised.push([
1766
+ key,
1767
+ truncate(redactDiagnosticText(value).slice(0, MAX_METADATA_VALUE_LENGTH))
1768
+ ]);
1368
1769
  } else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
1369
1770
  sanitised.push([key, value]);
1370
1771
  }
@@ -1382,8 +1783,8 @@ function admitUnderRateLimit(source, now) {
1382
1783
  }
1383
1784
  if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1384
1785
  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}"`
1786
+ reportTelemetryDiagnostic(
1787
+ `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
1788
  );
1388
1789
  }
1389
1790
  window.windowStartedAt = now;
@@ -1393,8 +1794,8 @@ function admitUnderRateLimit(source, now) {
1393
1794
  if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
1394
1795
  window.windowDroppedCount++;
1395
1796
  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}"`
1797
+ reportTelemetryDiagnostic(
1798
+ `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
1799
  );
1399
1800
  }
1400
1801
  return false;
@@ -1404,12 +1805,12 @@ function admitUnderRateLimit(source, now) {
1404
1805
  }
1405
1806
  function forwardRunnerActivity(entry, context) {
1406
1807
  try {
1407
- if (!FORWARDED_LEVELS.has(entry.level)) return;
1408
- if (!context.agentId || !context.authHeader) return;
1409
1808
  const source = entry.source ?? "cli.run";
1809
+ if (!FORWARDED_LEVELS_BY_SOURCE[source].has(entry.level)) return;
1810
+ if (!context.agentId || !context.authHeader) return;
1410
1811
  if (!admitUnderRateLimit(source, Date.now())) return;
1411
1812
  const rawMessage = entry.error ?? entry.message ?? "";
1412
- const message = truncate(redact(rawMessage));
1813
+ const message = truncate(redactDiagnosticText(rawMessage));
1413
1814
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1414
1815
  severity: SEVERITY_BY_LEVEL[entry.level],
1415
1816
  message,
@@ -1417,8 +1818,8 @@ function forwardRunnerActivity(entry, context) {
1417
1818
  agentId: context.agentId
1418
1819
  });
1419
1820
  } catch (err) {
1420
- console.error(
1421
- `[runner-activity-telemetry] failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
1821
+ reportTelemetryDiagnostic(
1822
+ `Runner activity telemetry failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
1422
1823
  );
1423
1824
  }
1424
1825
  }
@@ -1708,8 +2109,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1708
2109
 
1709
2110
  // src/lib/http-timeout.ts
1710
2111
  var REQUEST_TIMEOUT_MS = 6e4;
2112
+ var RequestTimeoutError = class extends Error {
2113
+ constructor(operation, timeoutMs, elapsedMs) {
2114
+ super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
2115
+ this.operation = operation;
2116
+ this.timeoutMs = timeoutMs;
2117
+ this.elapsedMs = elapsedMs;
2118
+ this.name = "TimeoutError";
2119
+ }
2120
+ };
2121
+ function describeRequestOperation(input, init) {
2122
+ const method = (init?.method ?? input.method ?? "GET").toUpperCase();
2123
+ const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
2124
+ let path;
2125
+ try {
2126
+ path = new URL(rawUrl).pathname;
2127
+ } catch (error2) {
2128
+ if (!(error2 instanceof TypeError)) throw error2;
2129
+ path = rawUrl.split(/[?#]/, 1)[0];
2130
+ }
2131
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2132
+ const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
2133
+ const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
2134
+ return `${method} ${maskedPath}`;
2135
+ }
1711
2136
  function withRequestTimeout(fetchImpl, timeoutMs) {
1712
- return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
2137
+ return (async (input, init) => {
2138
+ const startedAt = Date.now();
2139
+ try {
2140
+ return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
2141
+ } catch (error2) {
2142
+ if (error2?.name === "TimeoutError") {
2143
+ throw new RequestTimeoutError(
2144
+ describeRequestOperation(input, init),
2145
+ timeoutMs,
2146
+ Date.now() - startedAt
2147
+ );
2148
+ }
2149
+ throw error2;
2150
+ }
2151
+ });
1713
2152
  }
1714
2153
 
1715
2154
  // src/lib/opencode/client.ts
@@ -1775,7 +2214,7 @@ async function getOpenCodeDirectory(port, client) {
1775
2214
  return null;
1776
2215
  }
1777
2216
  }
1778
- function roleOf(m) {
2217
+ function roleOf2(m) {
1779
2218
  if (!m || typeof m !== "object") return void 0;
1780
2219
  if (typeof m.role === "string") return m.role;
1781
2220
  const infoRole = m.info?.role;
@@ -1789,13 +2228,13 @@ function createdOf(m) {
1789
2228
  if (!m || typeof m !== "object") return void 0;
1790
2229
  return m.info?.time?.created ?? m.time?.created;
1791
2230
  }
1792
- function idOf(m) {
2231
+ function idOf2(m) {
1793
2232
  if (!m || typeof m !== "object") return void 0;
1794
2233
  if (typeof m.id === "string") return m.id;
1795
2234
  const infoId = m.info?.id;
1796
2235
  return typeof infoId === "string" ? infoId : void 0;
1797
2236
  }
1798
- function parentIdOf(m) {
2237
+ function parentIdOf2(m) {
1799
2238
  if (!m || typeof m !== "object") return void 0;
1800
2239
  if (typeof m.parentID === "string") return m.parentID;
1801
2240
  const infoParent = m.info?.parentID;
@@ -1860,9 +2299,10 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
1860
2299
  function isSessionActivelyGenerating(messages) {
1861
2300
  if (!messages || messages.length === 0) return false;
1862
2301
  const last = messages[messages.length - 1];
1863
- if (roleOf(last) !== "assistant") return false;
2302
+ if (roleOf2(last) !== "assistant") return false;
1864
2303
  return completedOf(last) == null;
1865
2304
  }
2305
+ var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
1866
2306
  function sessionLastActivityMs(session) {
1867
2307
  const candidates = [
1868
2308
  session.time?.updated,
@@ -1877,30 +2317,60 @@ function sessionLastActivityMs(session) {
1877
2317
  }
1878
2318
  return null;
1879
2319
  }
1880
- async function listSessions(port, client) {
1881
- if (client?.version === "v2") return listV2Sessions(client);
2320
+ function sessionListPath(options) {
2321
+ const params = new URLSearchParams();
2322
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
2323
+ const query = params.toString();
2324
+ return query ? `/session?${query}` : "/session";
2325
+ }
2326
+ async function listSessionPage(port, client, options) {
2327
+ const path = sessionListPath(options);
1882
2328
  try {
1883
- const res = await requestWithClient(port, client, "/session");
2329
+ const res = await requestWithClient(port, client, path);
1884
2330
  if (!res.ok) return null;
1885
2331
  const body = await res.json();
1886
2332
  return Array.isArray(body) ? body : null;
1887
2333
  } catch (error2) {
1888
2334
  console.error(
1889
- `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2335
+ `[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
1890
2336
  );
1891
2337
  return null;
1892
2338
  }
1893
2339
  }
2340
+ async function listSessions(port, client, options) {
2341
+ if (client?.version === "v2") return listV2Sessions(client);
2342
+ return listSessionPage(port, client, options);
2343
+ }
2344
+ async function listAllSessions(port, client) {
2345
+ if (client?.version === "v2") {
2346
+ const sessions2 = await listV2Sessions(client);
2347
+ return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
2348
+ }
2349
+ const sessions = await listSessionPage(port, client, {
2350
+ limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
2351
+ });
2352
+ if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
2353
+ if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
2354
+ return { sessions: null, reason: "session-enumeration-limit-bound" };
2355
+ }
2356
+ return { sessions };
2357
+ }
1894
2358
  async function deleteSession(port, id, client) {
1895
2359
  if (client?.version === "v2") return deleteV2Session(client, id);
1896
2360
  try {
1897
- const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
1898
- return res.status >= 200 && res.status < 300;
2361
+ const res = await requestWithClient(
2362
+ port,
2363
+ client,
2364
+ `/session/${id}`,
2365
+ { method: "DELETE" },
2366
+ { allowStatuses: ALL_HTTP_STATUSES }
2367
+ );
2368
+ return { ok: res.status >= 200 && res.status < 300, status: res.status };
1899
2369
  } catch (error2) {
1900
2370
  console.error(
1901
2371
  `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
1902
2372
  );
1903
- return false;
2373
+ return { ok: false, status: "threw" };
1904
2374
  }
1905
2375
  }
1906
2376
  async function sessionExists(port, id, client) {
@@ -2141,7 +2611,7 @@ function messageText(m) {
2141
2611
  async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
2142
2612
  const before = await getSessionMessages(port, sessionId, client);
2143
2613
  const knownUserIds = new Set(
2144
- (before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
2614
+ (before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
2145
2615
  );
2146
2616
  const parts = [{ type: "text", text: content }];
2147
2617
  let pendingOutcomes = null;
@@ -2184,8 +2654,8 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2184
2654
  if (after) {
2185
2655
  let best = null;
2186
2656
  for (const m of after) {
2187
- if (roleOf(m) !== "user") continue;
2188
- const id = idOf(m);
2657
+ if (roleOf2(m) !== "user") continue;
2658
+ const id = idOf2(m);
2189
2659
  if (typeof id !== "string" || knownUserIds.has(id)) continue;
2190
2660
  if (messageText(m) !== content) continue;
2191
2661
  const created = createdOf(m) ?? 0;
@@ -2207,13 +2677,13 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2207
2677
  function findAssistantReplyAfter(messages, userMessageId) {
2208
2678
  if (!messages || messages.length === 0) return null;
2209
2679
  const byParent = messages.find(
2210
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
2680
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2211
2681
  );
2212
2682
  if (byParent) return byParent;
2213
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
2683
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2214
2684
  if (userIndex === -1) return null;
2215
2685
  for (let i = userIndex + 1; i < messages.length; i++) {
2216
- if (roleOf(messages[i]) === "assistant") return messages[i];
2686
+ if (roleOf2(messages[i]) === "assistant") return messages[i];
2217
2687
  }
2218
2688
  return null;
2219
2689
  }
@@ -2223,7 +2693,7 @@ function findLastAssistantReplyFor(messages, userMessageId) {
2223
2693
  let lastNonErrored = null;
2224
2694
  for (let i = messages.length - 1; i >= 0; i--) {
2225
2695
  const m = messages[i];
2226
- if (roleOf(m) !== "assistant" || parentIdOf(m) !== userMessageId) continue;
2696
+ if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
2227
2697
  if (lastCorrelated === null) lastCorrelated = m;
2228
2698
  if (errorOf(m) == null) {
2229
2699
  lastNonErrored = m;
@@ -2231,12 +2701,12 @@ function findLastAssistantReplyFor(messages, userMessageId) {
2231
2701
  }
2232
2702
  }
2233
2703
  if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
2234
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
2704
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2235
2705
  if (userIndex === -1) return null;
2236
2706
  let last = null;
2237
2707
  let lastOk = null;
2238
2708
  for (let i = userIndex + 1; i < messages.length; i++) {
2239
- const role = roleOf(messages[i]);
2709
+ const role = roleOf2(messages[i]);
2240
2710
  if (role === "user") break;
2241
2711
  if (role === "assistant") {
2242
2712
  last = messages[i];
@@ -2248,16 +2718,16 @@ function findLastAssistantReplyFor(messages, userMessageId) {
2248
2718
  function collectSubagentSessions(messages, userMessageId) {
2249
2719
  if (!messages || messages.length === 0) return [];
2250
2720
  const byParent = messages.filter(
2251
- (message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
2721
+ (message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
2252
2722
  );
2253
2723
  const assistants = byParent.length > 0 ? byParent : [];
2254
2724
  if (assistants.length === 0) {
2255
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
2725
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2256
2726
  if (userIndex === -1) return [];
2257
2727
  for (let i = userIndex + 1; i < messages.length; i++) {
2258
2728
  const message = messages[i];
2259
- if (roleOf(message) === "user") break;
2260
- if (roleOf(message) === "assistant") assistants.push(message);
2729
+ if (roleOf2(message) === "user") break;
2730
+ if (roleOf2(message) === "assistant") assistants.push(message);
2261
2731
  }
2262
2732
  }
2263
2733
  const refs = [];
@@ -2297,7 +2767,7 @@ function collectTaskCalls(messages, userMessageId) {
2297
2767
  if (!messages || messages.length === 0) return [];
2298
2768
  const calls = [];
2299
2769
  for (const message of messages) {
2300
- if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
2770
+ if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
2301
2771
  for (const part of message.parts ?? []) {
2302
2772
  if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
2303
2773
  continue;
@@ -2327,7 +2797,7 @@ function attributeTaskCallUsage(messages, windows) {
2327
2797
  for (const window of eligibleWindows) assignments.set(window.callID, []);
2328
2798
  const unattributed = [];
2329
2799
  for (const message of messages ?? []) {
2330
- if (roleOf(message) !== "assistant") continue;
2800
+ if (roleOf2(message) !== "assistant") continue;
2331
2801
  const created = finiteNumber(createdOf(message));
2332
2802
  const matching = created === null ? [] : eligibleWindows.filter(
2333
2803
  (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
@@ -2404,7 +2874,7 @@ function sumAssistantUsage(messages) {
2404
2874
  function messageUsage(messages, userMessageId) {
2405
2875
  if (!messages || messages.length === 0) return null;
2406
2876
  const byParentAll = messages.filter(
2407
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
2877
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2408
2878
  );
2409
2879
  const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
2410
2880
  const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
@@ -2420,7 +2890,7 @@ function messageUsage(messages, userMessageId) {
2420
2890
  }
2421
2891
  function messageRunState(messages, userMessageId) {
2422
2892
  if (!messages || messages.length === 0) return "unknown";
2423
- const hasUser = messages.some((m) => idOf(m) === userMessageId);
2893
+ const hasUser = messages.some((m) => idOf2(m) === userMessageId);
2424
2894
  const reply = findLastAssistantReplyFor(messages, userMessageId);
2425
2895
  if (!hasUser) {
2426
2896
  if (!reply) return "unknown";
@@ -2512,7 +2982,7 @@ function findLatestSubagentAuthOutcome(messages, sinceMs) {
2512
2982
  if (!messages || messages.length === 0) return null;
2513
2983
  for (let i = messages.length - 1; i >= 0; i--) {
2514
2984
  const message = messages[i];
2515
- if (roleOf(message) !== "assistant") continue;
2985
+ if (roleOf2(message) !== "assistant") continue;
2516
2986
  const created = createdOf(message);
2517
2987
  if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
2518
2988
  const failure = classifyReplyAuthError(message);
@@ -2544,25 +3014,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
2544
3014
  function hasRunningAssistantExcept(messages, exceptUserMessageId) {
2545
3015
  if (!messages || messages.length === 0) return false;
2546
3016
  return messages.some(
2547
- (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3017
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
2548
3018
  );
2549
3019
  }
2550
3020
  function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
2551
3021
  if (!messages || messages.length === 0) return false;
2552
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3022
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2553
3023
  if (userIndex === -1) return false;
2554
3024
  let hasLaterUser = false;
2555
3025
  let hasStartedLaterUser = false;
2556
3026
  for (let i = userIndex + 1; i < messages.length; i++) {
2557
3027
  const message = messages[i];
2558
- if (roleOf(message) !== "user") continue;
3028
+ if (roleOf2(message) !== "user") continue;
2559
3029
  hasLaterUser = true;
2560
- const laterUserMessageId = idOf(message);
3030
+ const laterUserMessageId = idOf2(message);
2561
3031
  if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
2562
3032
  return false;
2563
3033
  }
2564
3034
  if (messages.some(
2565
- (candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
3035
+ (candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
2566
3036
  )) {
2567
3037
  hasStartedLaterUser = true;
2568
3038
  }
@@ -3033,13 +3503,17 @@ async function listV2Sessions(client) {
3033
3503
  }
3034
3504
  async function deleteV2Session(client, sessionId) {
3035
3505
  try {
3036
- await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3037
- return true;
3506
+ const response = await client.request(
3507
+ `/api/session/${encodeURIComponent(sessionId)}`,
3508
+ { method: "DELETE" },
3509
+ { allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
3510
+ );
3511
+ return { ok: response.status >= 200 && response.status < 300, status: response.status };
3038
3512
  } catch (error2) {
3039
3513
  console.error(
3040
3514
  `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3041
3515
  );
3042
- return false;
3516
+ return { ok: false, status: "threw" };
3043
3517
  }
3044
3518
  }
3045
3519
  async function v2SessionExists(client, sessionId) {
@@ -3214,7 +3688,7 @@ import { join as join4 } from "node:path";
3214
3688
 
3215
3689
  // src/lib/openai-usage.ts
3216
3690
  import { readFileSync as readFileSync3 } from "node:fs";
3217
- import { homedir as homedir2 } from "node:os";
3691
+ import { homedir as homedir3 } from "node:os";
3218
3692
  import { join as join3 } from "node:path";
3219
3693
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3220
3694
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
@@ -3229,7 +3703,7 @@ function isLocalCredentialProblem2(err) {
3229
3703
  }
3230
3704
  function readOpenCodeChatGptCredentials() {
3231
3705
  try {
3232
- const raw = readFileSync3(join3(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3706
+ const raw = readFileSync3(join3(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3233
3707
  let parsed;
3234
3708
  try {
3235
3709
  parsed = JSON.parse(raw);
@@ -3370,16 +3844,17 @@ function hasPrimaryHeaders(headers) {
3370
3844
  ].some((name) => headers.has(name));
3371
3845
  }
3372
3846
  async function getOpenAiUsage(port, client) {
3847
+ const openCodeVersion = client?.version ?? "v1";
3373
3848
  const credentials2 = readOpenCodeChatGptCredentials();
3374
3849
  if (!credentials2) {
3375
3850
  throw new OpenAiUsageError(
3376
- "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
3851
+ openCodeVersion === "v2" ? "OpenAI usage reporting is not available on OpenCode V2 because V2 keeps a connected ChatGPT login in a store Evident cannot read." : "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
3377
3852
  "no_credentials"
3378
3853
  );
3379
3854
  }
3380
3855
  if (credentials2.expiresAt < Date.now()) {
3381
3856
  throw new OpenAiUsageError(
3382
- "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
3857
+ openCodeVersion === "v2" ? "The delivered ChatGPT login has expired. OpenCode V2 cannot be given a replacement by file; connect the account again from the runner's page in Evident." : "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
3383
3858
  "credentials_expired"
3384
3859
  );
3385
3860
  }
@@ -3660,9 +4135,9 @@ async function importOpenCodeV2Credentials({
3660
4135
 
3661
4136
  // src/lib/opencode/session-db-boot.ts
3662
4137
  import { spawn as spawn2 } from "node:child_process";
3663
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
3664
- import { homedir as homedir3 } from "node:os";
3665
- import { dirname as dirname2, resolve as resolvePath } from "node:path";
4138
+ import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
4139
+ import { homedir as homedir4 } from "node:os";
4140
+ import { dirname as dirname3, resolve as resolvePath } from "node:path";
3666
4141
 
3667
4142
  // src/lib/runner-synchroniser.ts
3668
4143
  import { spawn } from "node:child_process";
@@ -3787,7 +4262,7 @@ function clearMarker(options) {
3787
4262
  function markNoReplicate(options, message) {
3788
4263
  if (options.noReplicateMarker) {
3789
4264
  try {
3790
- mkdirSync(dirname2(options.noReplicateMarker), { recursive: true });
4265
+ mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
3791
4266
  writeFileSync(options.noReplicateMarker, "");
3792
4267
  } catch (error2) {
3793
4268
  options.log(
@@ -3912,7 +4387,7 @@ async function ensureLitestreamConfig(options, env) {
3912
4387
  return null;
3913
4388
  }
3914
4389
  try {
3915
- if (statSync2(configPath).size > 0) return configPath;
4390
+ if (statSync3(configPath).size > 0) return configPath;
3916
4391
  } catch (error2) {
3917
4392
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
3918
4393
  options.log(
@@ -3942,7 +4417,7 @@ async function ensureLitestreamConfig(options, env) {
3942
4417
  return null;
3943
4418
  }
3944
4419
  try {
3945
- mkdirSync(dirname2(configPath), { recursive: true });
4420
+ mkdirSync(dirname3(configPath), { recursive: true });
3946
4421
  writeFileSync(configPath, rendered.stdout);
3947
4422
  } catch (error2) {
3948
4423
  options.log(
@@ -4017,14 +4492,14 @@ async function restoreSessionDb(options, configPath, env) {
4017
4492
  case 0:
4018
4493
  return;
4019
4494
  case 31:
4020
- acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir3(), env));
4495
+ acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
4021
4496
  options.log(
4022
4497
  "SESSION-DB-REPLICA-UNUSABLE: booting with a fresh opencode.db and replicating into the existing prefix this boot",
4023
4498
  "warn"
4024
4499
  );
4025
4500
  return;
4026
4501
  case 32:
4027
- acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir3(), env));
4502
+ acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
4028
4503
  discardSessionDbDebris(options);
4029
4504
  markNoReplicate(
4030
4505
  options,
@@ -4103,7 +4578,7 @@ async function verifySessionDb(options, configPath, env) {
4103
4578
  }
4104
4579
  function fileExists(path) {
4105
4580
  try {
4106
- statSync2(path);
4581
+ statSync3(path);
4107
4582
  return true;
4108
4583
  } catch (error2) {
4109
4584
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
@@ -4113,7 +4588,7 @@ function fileExists(path) {
4113
4588
  async function restoreAndVerifySessionDb(options) {
4114
4589
  const env = options.env ?? process.env;
4115
4590
  clearMarker(options);
4116
- acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir3(), env));
4591
+ acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
4117
4592
  const synchroniserEnv = await runSynchroniser(["env"], {
4118
4593
  timeoutMs: SESSION_DB_SYNCHRONISER_TIMEOUT_MS,
4119
4594
  env
@@ -4192,7 +4667,7 @@ async function restoreAndVerifySessionDb(options) {
4192
4667
  // src/lib/opencode/session-db-provenance.ts
4193
4668
  import { createRequire } from "node:module";
4194
4669
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
4195
- import { dirname as dirname3, join as join5 } from "node:path";
4670
+ import { dirname as dirname4, join as join5 } from "node:path";
4196
4671
  var require2 = createRequire(import.meta.url);
4197
4672
  function readSessionDbMigrationIds(dbPath) {
4198
4673
  let db;
@@ -4258,7 +4733,7 @@ function loadSessionDbProvenanceState(path) {
4258
4733
  }
4259
4734
  function saveSessionDbProvenanceState(path, state) {
4260
4735
  try {
4261
- mkdirSync2(dirname3(path), { recursive: true });
4736
+ mkdirSync2(dirname4(path), { recursive: true });
4262
4737
  writeFileSync2(path, `${JSON.stringify(state, null, 2)}
4263
4738
  `, "utf8");
4264
4739
  } catch (error2) {
@@ -4839,7 +5314,10 @@ function selectSessionsToDelete(sessions, opts) {
4839
5314
  toDelete.push(s.id);
4840
5315
  }
4841
5316
  }
4842
- return toDelete;
5317
+ const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
5318
+ return toDelete.sort(
5319
+ (a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
5320
+ );
4843
5321
  }
4844
5322
  var DEFAULT_INTERVAL = "1h";
4845
5323
  function resolve(flag, envValue, fallback) {
@@ -4899,13 +5377,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
4899
5377
  }
4900
5378
 
4901
5379
  // src/lib/opencode/session-db-size.ts
4902
- import { statSync as statSync3 } from "node:fs";
5380
+ import { statSync as statSync4 } from "node:fs";
4903
5381
  import { join as join6 } from "node:path";
4904
5382
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
4905
5383
  function statSessionDbBytes(homeDir) {
4906
5384
  const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
4907
5385
  try {
4908
- return statSync3(dbPath).size;
5386
+ return statSync4(dbPath).size;
4909
5387
  } catch (err) {
4910
5388
  const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
4911
5389
  if (!isMissingFile) {
@@ -4931,12 +5409,12 @@ function buildSessionStoreSizeWarning(input) {
4931
5409
  }
4932
5410
 
4933
5411
  // src/lib/opencode/log-tail.ts
4934
- import { statSync as statSync4 } from "node:fs";
4935
- import { homedir as homedir4 } from "node:os";
5412
+ import { statSync as statSync5 } from "node:fs";
5413
+ import { homedir as homedir5 } from "node:os";
4936
5414
  import { join as join7 } from "node:path";
4937
5415
  import { open as open2, stat } from "node:fs/promises";
4938
5416
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
4939
- function resolveOpenCodeLogPath(homeDir = homedir4(), env = process.env) {
5417
+ function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
4940
5418
  const dataDir = env.XDG_DATA_HOME || join7(homeDir, ".local", "share");
4941
5419
  return join7(dataDir, "opencode", "log", "opencode.log");
4942
5420
  }
@@ -4954,7 +5432,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
4954
5432
  let inode = null;
4955
5433
  let baselineReady = true;
4956
5434
  try {
4957
- const initial = statSync4(logPath);
5435
+ const initial = statSync5(logPath);
4958
5436
  offset = initial.size;
4959
5437
  inode = initial.ino;
4960
5438
  } catch (error2) {
@@ -4980,156 +5458,37 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
4980
5458
  offset = current.size;
4981
5459
  inode = current.ino;
4982
5460
  baselineReady = true;
4983
- return;
4984
- }
4985
- if (inode !== null && current.ino !== inode || current.size < offset) {
4986
- offset = 0;
4987
- }
4988
- inode = current.ino;
4989
- if (current.size === offset) return;
4990
- const length = current.size - offset;
4991
- const fh = await open2(logPath, "r");
4992
- try {
4993
- const buf = Buffer.alloc(length);
4994
- const { bytesRead } = await fh.read(buf, 0, length, offset);
4995
- offset += bytesRead;
4996
- if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
4997
- } finally {
4998
- await fh.close();
4999
- }
5000
- } catch (error2) {
5001
- if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
5002
- } finally {
5003
- polling = false;
5004
- }
5005
- };
5006
- const interval = setInterval(() => void poll(), pollIntervalMs);
5007
- void poll();
5008
- return {
5009
- stop: () => {
5010
- stopped = true;
5011
- clearInterval(interval);
5012
- }
5013
- };
5014
- }
5015
-
5016
- // src/lib/opencode/session-db-reclaim.ts
5017
- import { statSync as statSync5, statfsSync } from "node:fs";
5018
- import { dirname as dirname4 } from "node:path";
5019
- function errorMessage(error2) {
5020
- if (!(error2 instanceof Error)) return String(error2);
5021
- return error2.cause instanceof Error ? error2.cause.message : error2.message;
5022
- }
5023
- function insufficientSpaceReason(dbPath, requiredBytes) {
5024
- try {
5025
- const fsStats = statfsSync(dirname4(dbPath));
5026
- const availableBytes = fsStats.bavail * fsStats.bsize;
5027
- if (availableBytes < requiredBytes) {
5028
- return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
5029
- }
5030
- return null;
5031
- } catch (err) {
5032
- return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
5033
- }
5034
- }
5035
- function readLogicalBytes(db) {
5036
- const pageCount = db.prepare("PRAGMA page_count").get().page_count;
5037
- const pageSize = db.prepare("PRAGMA page_size").get().page_size;
5038
- return pageCount * pageSize;
5039
- }
5040
- function readCheckpointResult(db) {
5041
- const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
5042
- return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
5043
- }
5044
- async function probeReclaimAvailability(input) {
5045
- const { dbPath, requiredBytes } = input;
5046
- let sqlite;
5047
- try {
5048
- sqlite = await import("node:sqlite");
5049
- } catch (err) {
5050
- const detail = `Node ${process.version}: ${errorMessage(err)}`;
5051
- console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
5052
- return { reason: "sqlite-unavailable", detail };
5053
- }
5054
- let autoVacuum = null;
5055
- try {
5056
- const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
5057
- try {
5058
- db.exec("PRAGMA busy_timeout=5000");
5059
- autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
5060
- } finally {
5061
- db.close();
5062
- }
5063
- } catch (err) {
5064
- console.warn(
5065
- `[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
5066
- );
5067
- }
5068
- if (autoVacuum !== 0) return null;
5069
- return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
5070
- }
5071
- async function reclaimSessionDbSpace(input) {
5072
- const { dbPath, maxPages, allowFullVacuum = true } = input;
5073
- let sqlite;
5074
- try {
5075
- sqlite = await import("node:sqlite");
5076
- } catch (err) {
5077
- const detail = `Node ${process.version}: ${errorMessage(err)}`;
5078
- console.warn(
5079
- `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
5080
- );
5081
- return { ok: false, skipped: "sqlite-unavailable", detail };
5082
- }
5083
- const { DatabaseSync } = sqlite;
5084
- let db;
5085
- try {
5086
- db = new DatabaseSync(dbPath);
5087
- db.exec("PRAGMA busy_timeout=5000");
5088
- const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
5089
- if (autoVacuum === 0) {
5090
- if (!allowFullVacuum) {
5091
- console.warn(
5092
- `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
5093
- );
5094
- return { ok: false, skipped: "full-vacuum-blocked" };
5461
+ return;
5095
5462
  }
5096
- const fileBytesForGuard = statSync5(dbPath).size;
5097
- const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
5098
- if (skipReason !== null) {
5099
- console.warn(
5100
- `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
5101
- );
5102
- return { ok: false, skipped: "insufficient-disk-space" };
5463
+ if (inode !== null && current.ino !== inode || current.size < offset) {
5464
+ offset = 0;
5103
5465
  }
5104
- const beforeBytes = readLogicalBytes(db);
5105
- db.exec("PRAGMA auto_vacuum=INCREMENTAL");
5106
- db.exec("VACUUM");
5107
- const afterBytes = readLogicalBytes(db);
5108
- const checkpoint = readCheckpointResult(db);
5109
- return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
5466
+ inode = current.ino;
5467
+ if (current.size === offset) return;
5468
+ const length = current.size - offset;
5469
+ const fh = await open2(logPath, "r");
5470
+ try {
5471
+ const buf = Buffer.alloc(length);
5472
+ const { bytesRead } = await fh.read(buf, 0, length, offset);
5473
+ offset += bytesRead;
5474
+ if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
5475
+ } finally {
5476
+ await fh.close();
5477
+ }
5478
+ } catch (error2) {
5479
+ if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
5480
+ } finally {
5481
+ polling = false;
5110
5482
  }
5111
- if (autoVacuum === 2) {
5112
- const beforeBytes = readLogicalBytes(db);
5113
- const bound = Math.max(0, Math.trunc(maxPages));
5114
- db.exec(`PRAGMA incremental_vacuum(${bound})`);
5115
- const afterBytes = readLogicalBytes(db);
5116
- const checkpoint = readCheckpointResult(db);
5117
- return { ok: true, mode: "incremental", beforeBytes, afterBytes, checkpoint };
5483
+ };
5484
+ const interval = setInterval(() => void poll(), pollIntervalMs);
5485
+ void poll();
5486
+ return {
5487
+ stop: () => {
5488
+ stopped = true;
5489
+ clearInterval(interval);
5118
5490
  }
5119
- console.warn(
5120
- `[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
5121
- );
5122
- return { ok: false, skipped: "auto-vacuum-not-applicable" };
5123
- } catch (err) {
5124
- console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
5125
- return {
5126
- ok: false,
5127
- skipped: "reclaim-error",
5128
- detail: errorMessage(err)
5129
- };
5130
- } finally {
5131
- db?.close();
5132
- }
5491
+ };
5133
5492
  }
5134
5493
 
5135
5494
  // src/lib/tunnel/connection.ts
@@ -5361,6 +5720,8 @@ var StreamForwarder = class {
5361
5720
  if (done) break;
5362
5721
  const chunk = Buffer.from(value);
5363
5722
  for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
5723
+ await stream.pausePromise;
5724
+ if (ac.signal.aborted) break;
5364
5725
  const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
5365
5726
  if (this.binaryFramesSupported) {
5366
5727
  if (this.ws.readyState === WebSocket.OPEN) {
@@ -5403,6 +5764,7 @@ function classifyUpgradeRejection(headers) {
5403
5764
  }
5404
5765
  var MAX_RECONNECT_DELAY = 3e4;
5405
5766
  var BASE_RECONNECT_DELAY = 500;
5767
+ var RELAY_SILENCE_TIMEOUT_MS = 9e4;
5406
5768
  function getReconnectDelay(attempt) {
5407
5769
  const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
5408
5770
  const jitter = Math.random() * 1e3;
@@ -5474,10 +5836,27 @@ function connectTunnel(options) {
5474
5836
  ws.close();
5475
5837
  reject(new Error("Connection timeout"));
5476
5838
  }, 3e4);
5839
+ let relaySilenceTimeout;
5840
+ let relaySilenceTimedOut = false;
5841
+ const clearRelaySilenceTimeout = () => {
5842
+ if (relaySilenceTimeout === void 0) return;
5843
+ clearTimeout(relaySilenceTimeout);
5844
+ relaySilenceTimeout = void 0;
5845
+ };
5846
+ const armRelaySilenceTimeout = () => {
5847
+ clearRelaySilenceTimeout();
5848
+ relaySilenceTimeout = setTimeout(() => {
5849
+ relaySilenceTimeout = void 0;
5850
+ relaySilenceTimedOut = true;
5851
+ ws.terminate();
5852
+ }, RELAY_SILENCE_TIMEOUT_MS);
5853
+ relaySilenceTimeout.unref?.();
5854
+ };
5477
5855
  let upgradeRejection = null;
5478
5856
  let upgradeRejectionReason = null;
5479
5857
  ws.on("unexpected-response", (_req, res) => {
5480
5858
  clearTimeout(connectionTimeout);
5859
+ clearRelaySilenceTimeout();
5481
5860
  const reason = classifyUpgradeRejection(res.headers);
5482
5861
  upgradeRejectionReason = reason;
5483
5862
  const chunks = [];
@@ -5507,6 +5886,7 @@ function connectTunnel(options) {
5507
5886
  onInfo?.("WebSocket connection established");
5508
5887
  });
5509
5888
  ws.on("message", (data, isBinary) => {
5889
+ if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
5510
5890
  if (isBinary) {
5511
5891
  try {
5512
5892
  const frame = decodeBinaryBodyFrame(toUint8Array(data));
@@ -5540,6 +5920,7 @@ function connectTunnel(options) {
5540
5920
  switch (message.type) {
5541
5921
  case "connected": {
5542
5922
  clearTimeout(connectionTimeout);
5923
+ armRelaySilenceTimeout();
5543
5924
  forwarder.setBinaryFramesSupported(message.binary_frames === true);
5544
5925
  const connectedAgentId = message.agent_id ?? agentId;
5545
5926
  onConnected?.(connectedAgentId);
@@ -5564,6 +5945,7 @@ function connectTunnel(options) {
5564
5945
  });
5565
5946
  ws.on("error", (error2) => {
5566
5947
  clearTimeout(connectionTimeout);
5948
+ clearRelaySilenceTimeout();
5567
5949
  const detail = upgradeRejection ?? describeSocketError(error2, url);
5568
5950
  if (upgradeRejectionReason === "do_code_updated") {
5569
5951
  onWarning?.("Relay redeployed \u2014 reconnecting");
@@ -5575,7 +5957,8 @@ function connectTunnel(options) {
5575
5957
  );
5576
5958
  });
5577
5959
  ws.on("close", (code, reason) => {
5578
- const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
5960
+ clearRelaySilenceTimeout();
5961
+ const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
5579
5962
  forwarder.abortAll();
5580
5963
  onDisconnected?.(code, reasonStr);
5581
5964
  });
@@ -5583,9 +5966,10 @@ function connectTunnel(options) {
5583
5966
  }
5584
5967
 
5585
5968
  // src/lib/tunnel/runner-connection.ts
5586
- var RunnerConnection = class {
5969
+ var RunnerConnection = class _RunnerConnection {
5587
5970
  opts;
5588
5971
  sleep;
5972
+ now;
5589
5973
  connection = null;
5590
5974
  resolvedAgentId;
5591
5975
  /** True while a (re)connect loop is in flight. */
@@ -5594,10 +5978,17 @@ var RunnerConnection = class {
5594
5978
  reconnectPromise = null;
5595
5979
  /** 1-based count of the current reconnect attempt streak. */
5596
5980
  reconnectAttempt = 0;
5981
+ generation = 0;
5982
+ disconnectedAtMs = null;
5983
+ static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
5984
+ "No pong received",
5985
+ "Orphaned socket"
5986
+ ]);
5597
5987
  constructor(opts) {
5598
5988
  this.opts = opts;
5599
5989
  this.resolvedAgentId = opts.agentId;
5600
5990
  this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5991
+ this.now = opts.now ?? Date.now;
5601
5992
  }
5602
5993
  get agentId() {
5603
5994
  return this.resolvedAgentId;
@@ -5633,14 +6024,24 @@ var RunnerConnection = class {
5633
6024
  port: this.opts.port,
5634
6025
  openCodePassword: this.opts.openCodePassword,
5635
6026
  onConnected: (agentId) => {
6027
+ const attempt = this.reconnectAttempt + 1;
5636
6028
  this.reconnectAttempt = 0;
5637
6029
  this.reconnecting = false;
5638
6030
  this.resolvedAgentId = agentId;
5639
- events.onConnected(agentId, isReconnect);
6031
+ this.generation++;
6032
+ const downtimeMs = this.disconnectedAtMs === null ? null : this.now() - this.disconnectedAtMs;
6033
+ this.disconnectedAtMs = null;
6034
+ events.onConnected(agentId, isReconnect, {
6035
+ generation: this.generation,
6036
+ attempt,
6037
+ downtimeMs
6038
+ });
5640
6039
  },
5641
6040
  onDisconnected: (code, reason) => {
5642
- events.onDisconnected(code, reason);
5643
- if (this.opts.isRunning() && code !== 1e3 && !this.reconnecting) {
6041
+ this.disconnectedAtMs = this.now();
6042
+ events.onDisconnected(code, reason, { generation: this.generation });
6043
+ const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
6044
+ if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
5644
6045
  this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
5645
6046
  events.onError?.(`Reconnection failed: ${err.message}`);
5646
6047
  });
@@ -5979,8 +6380,85 @@ function createResourceUsageCollector(homeDir) {
5979
6380
  };
5980
6381
  }
5981
6382
 
6383
+ // src/lib/resource-usage-low-disk.ts
6384
+ var RUNNER_DISK_ALERT_ENTRY_FREE_BYTES = 4 * 1024 ** 3;
6385
+ var RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION = 0.25;
6386
+ var RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES = 1 * 1024 ** 3;
6387
+ var RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES = 256 * 1024 ** 2;
6388
+ var RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES = 6 * 1024 ** 3;
6389
+ var RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION = 0.375;
6390
+ var LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
6391
+ var BAND_RANK = {
6392
+ low: 1,
6393
+ very_low: 2,
6394
+ critical: 3
6395
+ };
6396
+ function bandFor(freeBytes) {
6397
+ if (freeBytes < RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES) return "critical";
6398
+ if (freeBytes < RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES) return "very_low";
6399
+ return "low";
6400
+ }
6401
+ function formatBytes(bytes) {
6402
+ if (bytes === null) return "unavailable";
6403
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
6404
+ const unitIndex = Math.min(Math.floor(Math.log2(Math.max(bytes, 1)) / 10), units.length - 1);
6405
+ const value = bytes / 1024 ** unitIndex;
6406
+ const digits = unitIndex === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 2;
6407
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
6408
+ }
6409
+ function warningMessage(band, diskFreeBytes, diskTotalBytes, opencodeDbBytes) {
6410
+ 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.`;
6411
+ }
6412
+ function evaluateLowDiskWarning(input) {
6413
+ const { diskFreeBytes, diskTotalBytes, opencodeDbBytes, nowMs, state } = input;
6414
+ if (diskFreeBytes === null || diskTotalBytes === null || diskTotalBytes <= 0 || diskFreeBytes >= diskTotalBytes) {
6415
+ return { warning: null, state };
6416
+ }
6417
+ const recovered = diskFreeBytes >= RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES || diskFreeBytes >= diskTotalBytes * RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION;
6418
+ if (recovered) {
6419
+ return { warning: null, state: { worstBand: null, warnedAtMs: null } };
6420
+ }
6421
+ const low = diskFreeBytes < RUNNER_DISK_ALERT_ENTRY_FREE_BYTES && diskFreeBytes < diskTotalBytes * RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION;
6422
+ if (!low) return { warning: null, state };
6423
+ const currentBand = bandFor(diskFreeBytes);
6424
+ const currentRank = BAND_RANK[currentBand];
6425
+ const previousRank = state.worstBand === null ? 0 : BAND_RANK[state.worstBand];
6426
+ const worse = currentRank > previousRank;
6427
+ const reassert = state.warnedAtMs !== null && nowMs - state.warnedAtMs >= LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS;
6428
+ const nextState = {
6429
+ worstBand: worse ? currentBand : state.worstBand ?? currentBand,
6430
+ warnedAtMs: worse || state.warnedAtMs === null || reassert ? nowMs : state.warnedAtMs
6431
+ };
6432
+ return {
6433
+ warning: worse || state.warnedAtMs === null || reassert ? warningMessage(nextState.worstBand, diskFreeBytes, diskTotalBytes, opencodeDbBytes) : null,
6434
+ state: nextState
6435
+ };
6436
+ }
6437
+
6438
+ // src/lib/opencode/session-store-health.ts
6439
+ var SESSION_STORE_HEALTH_STALENESS_MS = 5 * 6e4;
6440
+ function createSessionStoreHealth(options = {}) {
6441
+ const now = options.now ?? (() => Date.now());
6442
+ const stalenessMs = Math.max(0, options.stalenessMs ?? SESSION_STORE_HEALTH_STALENESS_MS);
6443
+ let lastUnwritableAt = null;
6444
+ let lastWritableAt = null;
6445
+ return {
6446
+ observeUnwritable: () => {
6447
+ lastUnwritableAt = now();
6448
+ },
6449
+ observeWritable: () => {
6450
+ lastWritableAt = now();
6451
+ },
6452
+ isUnwritable: () => {
6453
+ if (lastUnwritableAt === null) return false;
6454
+ if (lastWritableAt !== null && lastWritableAt >= lastUnwritableAt) return false;
6455
+ return now() - lastUnwritableAt < stalenessMs;
6456
+ }
6457
+ };
6458
+ }
6459
+
5982
6460
  // src/lib/channels/driver.ts
5983
- import { homedir as homedir5 } from "node:os";
6461
+ import { homedir as homedir6 } from "node:os";
5984
6462
 
5985
6463
  // src/lib/runner-file-sync.ts
5986
6464
  import { join as join9 } from "node:path";
@@ -6437,6 +6915,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
6437
6915
  var MAX_BUFFERED_SESSION_ERRORS = 256;
6438
6916
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
6439
6917
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
6918
+ var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
6440
6919
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
6441
6920
  var HEARTBEAT_MS = 6e4;
6442
6921
  var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
@@ -6498,6 +6977,7 @@ var ChannelDriver = class _ChannelDriver {
6498
6977
  maxActiveSessions;
6499
6978
  watcherStallMs;
6500
6979
  wedgeWarningIntervalMs;
6980
+ sessionStoreHealth;
6501
6981
  openCodeClient;
6502
6982
  /** Cache of conversationId → opencode sessionId. */
6503
6983
  sessions = /* @__PURE__ */ new Map();
@@ -6863,10 +7343,11 @@ var ChannelDriver = class _ChannelDriver {
6863
7343
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
6864
7344
  this.now = config.now ?? (() => Date.now());
6865
7345
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
6866
- this.homeDir = config.homeDir ?? homedir5();
7346
+ this.homeDir = config.homeDir ?? homedir6();
6867
7347
  this.maxActiveSessions = config.maxActiveSessions;
6868
7348
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
6869
7349
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
7350
+ this.sessionStoreHealth = config.sessionStoreHealth;
6870
7351
  }
6871
7352
  get isV2() {
6872
7353
  return this.openCodeClient.version === "v2";
@@ -6882,7 +7363,9 @@ var ChannelDriver = class _ChannelDriver {
6882
7363
  return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6883
7364
  }
6884
7365
  async listSessions() {
6885
- return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
7366
+ return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient, {
7367
+ limit: DRIVER_SESSION_ENUMERATION_LIMIT
7368
+ });
6886
7369
  }
6887
7370
  async sessionExists(sessionId) {
6888
7371
  return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
@@ -8336,8 +8819,10 @@ var ChannelDriver = class _ChannelDriver {
8336
8819
  b2LastDescendantCheckMs: 0,
8337
8820
  b2RootOngoingHeldLogged: false,
8338
8821
  b2AbandonedSignalled: false,
8822
+ b2AbandonedWithoutAnswer: false,
8339
8823
  ambiguousPinnedSinceMs: 0,
8340
8824
  ambiguousResolved: false,
8825
+ sessionStoreFailurePending: false,
8341
8826
  neverStartedResolved: false
8342
8827
  });
8343
8828
  const buffered = this.bufferedSessionErrors.get(sessionId);
@@ -8454,8 +8939,10 @@ var ChannelDriver = class _ChannelDriver {
8454
8939
  b2LastDescendantCheckMs: 0,
8455
8940
  b2RootOngoingHeldLogged: false,
8456
8941
  b2AbandonedSignalled: false,
8942
+ b2AbandonedWithoutAnswer: false,
8457
8943
  ambiguousPinnedSinceMs: 0,
8458
8944
  ambiguousResolved: false,
8945
+ sessionStoreFailurePending: false,
8459
8946
  neverStartedResolved: false
8460
8947
  });
8461
8948
  }
@@ -8869,6 +9356,10 @@ var ChannelDriver = class _ChannelDriver {
8869
9356
  if (state === "running" || state === "done" || state === "failed") {
8870
9357
  this.neverStartedAttempts.delete(id);
8871
9358
  }
9359
+ if (inFlight.sessionStoreFailurePending) {
9360
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
9361
+ return;
9362
+ }
8872
9363
  if (inFlight.neverStartedResolved) {
8873
9364
  if (state === "running" || state === "done" || state === "failed") {
8874
9365
  inFlight.neverStartedResolved = false;
@@ -9028,10 +9519,22 @@ var ChannelDriver = class _ChannelDriver {
9028
9519
  inFlight.b2LastDescendantCheckMs = 0;
9029
9520
  inFlight.b2RootOngoingHeldLogged = false;
9030
9521
  inFlight.b2AbandonedSignalled = false;
9522
+ inFlight.b2AbandonedWithoutAnswer = false;
9031
9523
  }
9032
9524
  } else {
9033
9525
  if (inFlight.b2AbandonedSignalled) {
9034
- await this.settleMessageDone(sessionId, watcher, inFlight, messages);
9526
+ if (inFlight.b2AbandonedWithoutAnswer) {
9527
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
9528
+ await this.settleMessageAbandonedWithoutAnswer(
9529
+ sessionId,
9530
+ watcher,
9531
+ inFlight,
9532
+ messages,
9533
+ reply?.info?.finish ?? reply?.finish
9534
+ );
9535
+ } else {
9536
+ await this.settleMessageDone(sessionId, watcher, inFlight, messages);
9537
+ }
9035
9538
  return;
9036
9539
  }
9037
9540
  if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
@@ -9048,17 +9551,47 @@ var ChannelDriver = class _ChannelDriver {
9048
9551
  descendantOngoing,
9049
9552
  rootOngoing
9050
9553
  })) {
9554
+ if (this.sessionStoreHealth?.isUnwritable()) {
9555
+ inFlight.sessionStoreFailurePending = true;
9556
+ void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
9557
+ watched_for_ms: pinnedForMs,
9558
+ run_state: "failed",
9559
+ opencode_message_id: inFlight.opencodeMessageId
9560
+ });
9561
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
9562
+ return;
9563
+ }
9051
9564
  inFlight.b2AbandonedSignalled = true;
9565
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
9566
+ const finish = reply?.info?.finish ?? reply?.finish;
9567
+ if (extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null) {
9568
+ inFlight.b2AbandonedWithoutAnswer = true;
9569
+ void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
9570
+ watched_for_ms: pinnedForMs,
9571
+ run_state: "failed",
9572
+ finish,
9573
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
9574
+ ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
9575
+ opencode_message_id: inFlight.opencodeMessageId
9576
+ });
9577
+ await this.settleMessageAbandonedWithoutAnswer(
9578
+ sessionId,
9579
+ watcher,
9580
+ inFlight,
9581
+ messages,
9582
+ finish
9583
+ );
9584
+ return;
9585
+ }
9052
9586
  this.log({
9053
9587
  level: "warn",
9054
9588
  message: `Message ${id.slice(0, 8)} b2-pinned for ${Math.round(pinnedForMs / 1e3)}s with no ongoing descendant sub-agent session (status-map confirmed) \u2014 treating the delegated/tool turn as abandoned, resolving done`,
9055
9589
  conversation_id: conv.id,
9056
9590
  message_id: id
9057
9591
  });
9058
- const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
9059
9592
  void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
9060
9593
  watched_for_ms: pinnedForMs,
9061
- finish: reply?.info?.finish ?? reply?.finish,
9594
+ finish,
9062
9595
  ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
9063
9596
  ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
9064
9597
  opencode_message_id: inFlight.opencodeMessageId
@@ -9106,6 +9639,16 @@ var ChannelDriver = class _ChannelDriver {
9106
9639
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
9107
9640
  sessionOngoing: ongoing
9108
9641
  })) {
9642
+ if (this.sessionStoreHealth?.isUnwritable()) {
9643
+ inFlight.sessionStoreFailurePending = true;
9644
+ void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
9645
+ watched_for_ms: pinnedForMs,
9646
+ run_state: "failed",
9647
+ opencode_message_id: inFlight.opencodeMessageId
9648
+ });
9649
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
9650
+ return;
9651
+ }
9109
9652
  inFlight.ambiguousResolved = true;
9110
9653
  this.log({
9111
9654
  level: "warn",
@@ -9330,6 +9873,74 @@ var ChannelDriver = class _ChannelDriver {
9330
9873
  }
9331
9874
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9332
9875
  }
9876
+ /** Report a confirmed b2 abandonment that produced no answer text. */
9877
+ async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, finish) {
9878
+ const conv = watcher.conv;
9879
+ const error2 = `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs.`;
9880
+ this.anchorDeliveryDeadline(inFlight);
9881
+ this.log({
9882
+ level: "warn",
9883
+ message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was abandoned without an answer \u2014 ${error2}`,
9884
+ conversation_id: conv.id,
9885
+ message_id: inFlight.evidentMessageId
9886
+ });
9887
+ const usage = messageUsage(messages, inFlight.opencodeMessageId);
9888
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
9889
+ const subagentInvocations = await this.resolveSubagentInvocations(
9890
+ messages,
9891
+ inFlight.opencodeMessageId,
9892
+ inFlight.evidentMessageId
9893
+ );
9894
+ try {
9895
+ await this.markFailed(
9896
+ conv.id,
9897
+ inFlight.evidentMessageId,
9898
+ sessionId,
9899
+ error2,
9900
+ usage,
9901
+ void 0,
9902
+ usageAgentName,
9903
+ subagentInvocations
9904
+ );
9905
+ } catch (err) {
9906
+ if (err instanceof ChannelAuthError) throw err;
9907
+ if (err instanceof ChannelTerminalError) {
9908
+ this.log({
9909
+ level: "warn",
9910
+ message: `Failed to report abandoned no-answer message ${inFlight.evidentMessageId.slice(0, 8)} as failed (terminal HTTP ${err.status}) \u2014 leaving it for the cron safety net: ${err.message}`,
9911
+ conversation_id: conv.id,
9912
+ message_id: inFlight.evidentMessageId
9913
+ });
9914
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
9915
+ return;
9916
+ }
9917
+ if (this.now() >= inFlight.deadline) {
9918
+ this.log({
9919
+ level: "warn",
9920
+ message: `Failed to report abandoned no-answer message ${inFlight.evidentMessageId.slice(0, 8)} as failed within the delivery window \u2014 leaving it for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
9921
+ conversation_id: conv.id,
9922
+ message_id: inFlight.evidentMessageId
9923
+ });
9924
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
9925
+ return;
9926
+ }
9927
+ this.log({
9928
+ level: "warn",
9929
+ message: `Failed to report abandoned no-answer message ${inFlight.evidentMessageId.slice(0, 8)} as failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
9930
+ conversation_id: conv.id,
9931
+ message_id: inFlight.evidentMessageId
9932
+ });
9933
+ return;
9934
+ }
9935
+ inFlight.done = true;
9936
+ await this.reportSubagentAuthFailures(
9937
+ watcher.conv.id,
9938
+ inFlight.opencodeMessageId,
9939
+ inFlight.evidentMessageId,
9940
+ messages
9941
+ );
9942
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
9943
+ }
9333
9944
  /**
9334
9945
  * Report a queued message as failed after the never-started bound. The accepted
9335
9946
  * OpenCode copy is not cancelled, so the local fence remains on unsuccessful
@@ -9385,6 +9996,66 @@ var ChannelDriver = class _ChannelDriver {
9385
9996
  this.neverStartedAttempts.delete(inFlight.evidentMessageId);
9386
9997
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9387
9998
  }
9999
+ /**
10000
+ * Report a turn as failed when the session database was observed unwritable.
10001
+ * The queue row is retained as failed, while the OpenCode copy stays fenced so
10002
+ * a later drain cannot blindly prompt the same turn again.
10003
+ */
10004
+ async settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight) {
10005
+ const conv = watcher.conv;
10006
+ 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.";
10007
+ this.log({
10008
+ level: "error",
10009
+ message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was not resolved as done: ${error2}`,
10010
+ conversation_id: conv.id,
10011
+ message_id: inFlight.evidentMessageId
10012
+ });
10013
+ this.anchorDeliveryDeadline(inFlight);
10014
+ try {
10015
+ await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
10016
+ } catch (err) {
10017
+ if (err instanceof ChannelAuthError) throw err;
10018
+ if (err instanceof ChannelTerminalError) {
10019
+ this.log({
10020
+ level: "warn",
10021
+ 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}`,
10022
+ conversation_id: conv.id,
10023
+ message_id: inFlight.evidentMessageId
10024
+ });
10025
+ this.recordReleasedOpencodeId(
10026
+ inFlight.evidentMessageId,
10027
+ sessionId,
10028
+ inFlight.opencodeMessageId
10029
+ );
10030
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10031
+ return;
10032
+ }
10033
+ if (this.now() >= inFlight.deadline) {
10034
+ this.log({
10035
+ level: "warn",
10036
+ 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)}`,
10037
+ conversation_id: conv.id,
10038
+ message_id: inFlight.evidentMessageId
10039
+ });
10040
+ this.recordReleasedOpencodeId(
10041
+ inFlight.evidentMessageId,
10042
+ sessionId,
10043
+ inFlight.opencodeMessageId
10044
+ );
10045
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10046
+ return;
10047
+ }
10048
+ this.log({
10049
+ level: "warn",
10050
+ 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)}`,
10051
+ conversation_id: conv.id,
10052
+ message_id: inFlight.evidentMessageId
10053
+ });
10054
+ return;
10055
+ }
10056
+ inFlight.done = true;
10057
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10058
+ }
9388
10059
  // Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
9389
10060
  /**
9390
10061
  * Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
@@ -12116,6 +12787,7 @@ function createCredentialSync({
12116
12787
 
12117
12788
  // src/commands/run.ts
12118
12789
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
12790
+ var MAX_DISCONNECT_REASON_LENGTH = 200;
12119
12791
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
12120
12792
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
12121
12793
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
@@ -12271,7 +12943,13 @@ function logActivity(state, entry) {
12271
12943
  const level = entry.level ?? (entry.type === "error" ? "error" : "info");
12272
12944
  if (!meetsThreshold(state, level)) return;
12273
12945
  forwardRunnerActivity(
12274
- { level, message: entry.message, error: entry.error, metadata: entry.metadata },
12946
+ {
12947
+ level,
12948
+ message: entry.message,
12949
+ error: entry.error,
12950
+ metadata: entry.metadata,
12951
+ ...entry.source ? { source: entry.source } : {}
12952
+ },
12275
12953
  { agentId: state.agentId, authHeader: state.authHeader }
12276
12954
  );
12277
12955
  const fullEntry = {
@@ -12293,7 +12971,7 @@ function logActivity(state, entry) {
12293
12971
  }
12294
12972
  function reportSessionDbRecovery(state) {
12295
12973
  try {
12296
- const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
12974
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
12297
12975
  for (const record of report.records) {
12298
12976
  const activity = buildSessionDbRecoveryActivity(record);
12299
12977
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -12328,7 +13006,9 @@ function displayStatus(state) {
12328
13006
  const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
12329
13007
  const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
12330
13008
  const last = state.activityLog[state.activityLog.length - 1];
12331
- const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
13009
+ const detail = last ? chalk7.dim(
13010
+ ` \xB7 ${last.timestamp.toISOString()} ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`
13011
+ ) : "";
12332
13012
  const agent = state.agentName ?? state.agentId;
12333
13013
  console.log(
12334
13014
  `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
@@ -12347,7 +13027,7 @@ function reportOpenCodeV2CredentialImport(state, result) {
12347
13027
  logActivity(state, {
12348
13028
  type: "info",
12349
13029
  level: "warn",
12350
- message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them. See #2251 for importing existing logins.`
13030
+ message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them. Connect the provider again from the runner's page in Evident; no shell on this machine is needed.`
12351
13031
  });
12352
13032
  }
12353
13033
  if (result.imported.length === 0 && result.unsupported.length === 0 && result.failed.length === 0) {
@@ -12372,7 +13052,7 @@ async function importOpenCodeV2CredentialsForState(state) {
12372
13052
  }
12373
13053
  const result = await importOpenCodeV2Credentials({
12374
13054
  request: client.request,
12375
- homeDir: homedir6(),
13055
+ homeDir: homedir7(),
12376
13056
  directory,
12377
13057
  guard: state.opencodeV2CredentialImportGuard,
12378
13058
  log: (message, level = "info") => logActivity(state, { type: "info", level, message })
@@ -12580,9 +13260,9 @@ async function driveChannels(state, driver) {
12580
13260
  }
12581
13261
  }
12582
13262
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
12583
- var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
13263
+ var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
12584
13264
  function shouldWarnForReclaimSkip(reason) {
12585
- if (reason !== "sqlite-unavailable") return false;
13265
+ if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
12586
13266
  const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
12587
13267
  if (!version2) return false;
12588
13268
  const major = Number(version2[1]);
@@ -12591,7 +13271,14 @@ function shouldWarnForReclaimSkip(reason) {
12591
13271
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
12592
13272
  }
12593
13273
  function sessionDbPath() {
12594
- return join11(homedir6(), ".local", "share", "opencode", "opencode.db");
13274
+ return join11(homedir7(), ".local", "share", "opencode", "opencode.db");
13275
+ }
13276
+ function logSessionCleanupActivity(state, phase, entry) {
13277
+ logActivity(state, {
13278
+ ...entry,
13279
+ source: "cli.session-cleanup",
13280
+ metadata: { ...entry.metadata, phase }
13281
+ });
12595
13282
  }
12596
13283
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
12597
13284
  const record = {
@@ -12632,69 +13319,136 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
12632
13319
  async function runSweep(state, driver, config) {
12633
13320
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
12634
13321
  try {
12635
- const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
12636
- if (sessions === null) {
13322
+ const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0);
13323
+ const sessions = enumeration.sessions;
13324
+ const enumerationReason = sessions === null ? enumeration.reason : null;
13325
+ if (enumerationReason !== null) {
12637
13326
  logActivity(state, {
12638
13327
  type: "info",
12639
- message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`
13328
+ level: "warn",
13329
+ message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
12640
13330
  });
12641
- return;
12642
13331
  }
12643
- const toDelete = selectSessionsToDelete(
12644
- sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
12645
- {
12646
- maxAgeMs: config.maxAgeMs,
12647
- maxCount: config.maxCount,
12648
- nowMs: Date.now(),
12649
- protectedIds: driver.protectedSessionIds()
12650
- }
12651
- );
12652
- const protectedNow = driver.protectedSessionIds();
12653
13332
  let deleted = 0;
12654
13333
  let failed = 0;
13334
+ let failed4xx = 0;
13335
+ let failed5xx = 0;
13336
+ let failedOtherStatus = 0;
13337
+ let failedThrew = 0;
12655
13338
  let skippedNewlyActive = 0;
12656
- for (const id of toDelete) {
12657
- if (protectedNow.has(id)) {
12658
- skippedNewlyActive++;
12659
- logActivity(state, {
12660
- type: "info",
12661
- message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
12662
- });
12663
- continue;
13339
+ let remaining = 0;
13340
+ let protectedNow = driver.protectedSessionIds();
13341
+ if (sessions !== null) {
13342
+ const toDelete = selectSessionsToDelete(
13343
+ sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
13344
+ {
13345
+ maxAgeMs: config.maxAgeMs,
13346
+ maxCount: config.maxCount,
13347
+ nowMs: Date.now(),
13348
+ protectedIds: protectedNow
13349
+ }
13350
+ );
13351
+ const deleteCandidates = toDelete.slice(0, SESSION_CLEANUP_MAX_DELETES_PER_SWEEP);
13352
+ remaining = toDelete.length - deleteCandidates.length;
13353
+ protectedNow = driver.protectedSessionIds();
13354
+ for (const id of deleteCandidates) {
13355
+ if (protectedNow.has(id)) {
13356
+ skippedNewlyActive++;
13357
+ logActivity(state, {
13358
+ type: "info",
13359
+ message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
13360
+ });
13361
+ continue;
13362
+ }
13363
+ const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
13364
+ if (outcome.ok) {
13365
+ deleted++;
13366
+ } else if (outcome.status === "threw") {
13367
+ failed++;
13368
+ failedThrew++;
13369
+ } else {
13370
+ failed++;
13371
+ const statusClass = Math.floor(outcome.status / 100);
13372
+ if (statusClass === 4) failed4xx++;
13373
+ else if (statusClass === 5) failed5xx++;
13374
+ else failedOtherStatus++;
13375
+ }
12664
13376
  }
12665
- if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
12666
- else failed++;
13377
+ const failedNote = failed > 0 ? `, failed ${failed}` : "";
13378
+ const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
13379
+ const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
13380
+ logActivity(state, {
13381
+ type: "info",
13382
+ message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
13383
+ });
12667
13384
  }
12668
- const failedNote = failed > 0 ? `, failed ${failed}` : "";
12669
- const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
12670
- logActivity(state, {
12671
- type: "info",
12672
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`
12673
- });
13385
+ const sessionMetadata = {
13386
+ sessions_inspected: sessions?.length ?? null,
13387
+ sessions_deleted: deleted,
13388
+ sessions_failed: failed,
13389
+ sessions_failed_4xx: failed4xx,
13390
+ sessions_failed_5xx: failed5xx,
13391
+ sessions_failed_other: failedOtherStatus,
13392
+ sessions_failed_threw: failedThrew,
13393
+ sessions_skipped_active: skippedNewlyActive,
13394
+ sessions_remaining: remaining,
13395
+ ...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
13396
+ };
12674
13397
  const reclaimResult = await reclaimSessionDbSpace({
12675
13398
  dbPath: sessionDbPath(),
12676
- maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
13399
+ chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
13400
+ pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
13401
+ timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
12677
13402
  allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
12678
13403
  });
12679
13404
  if (reclaimResult.ok) {
13405
+ state.sessionStoreHealth.observeWritable();
12680
13406
  const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
12681
13407
  const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
12682
13408
  const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
12683
- logActivity(state, {
13409
+ logSessionCleanupActivity(state, "sweep", {
12684
13410
  type: "info",
12685
- message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`
13411
+ message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB (stopped by ${reclaimResult.stoppedBy}; freelist ${reclaimResult.freelistBefore} -> ${reclaimResult.freelistAfter} pages)` + checkpointNote,
13412
+ metadata: {
13413
+ outcome: "reclaimed",
13414
+ mode: reclaimResult.mode,
13415
+ before_bytes: reclaimResult.beforeBytes,
13416
+ after_bytes: reclaimResult.afterBytes,
13417
+ checkpoint_busy: reclaimResult.checkpoint.busy,
13418
+ wal_frames_pending: reclaimResult.checkpoint.log,
13419
+ stopped_by: reclaimResult.stoppedBy,
13420
+ freelist_before: reclaimResult.freelistBefore,
13421
+ freelist_after: reclaimResult.freelistAfter,
13422
+ pages_reclaimed: reclaimResult.pagesReclaimed,
13423
+ reclaim_elapsed_ms: reclaimResult.elapsedMs,
13424
+ ...sessionMetadata
13425
+ }
12686
13426
  });
12687
13427
  } else {
12688
- logActivity(state, {
13428
+ if (reclaimResult.skipped === "disk-exhaustion") {
13429
+ state.sessionStoreHealth.observeUnwritable();
13430
+ }
13431
+ logSessionCleanupActivity(state, "sweep", {
12689
13432
  type: "info",
12690
- message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
13433
+ level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
13434
+ message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
13435
+ metadata: {
13436
+ outcome: "reclaim-skipped",
13437
+ reason: reclaimResult.skipped,
13438
+ ...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
13439
+ ...sessionMetadata
13440
+ }
12691
13441
  });
12692
13442
  }
12693
13443
  } catch (error2) {
12694
13444
  const message = error2 instanceof Error ? error2.message : String(error2);
12695
- logActivity(state, {
13445
+ logSessionCleanupActivity(state, "sweep", {
12696
13446
  type: "error",
12697
- error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`
13447
+ error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`,
13448
+ metadata: {
13449
+ outcome: "sweep-failed",
13450
+ reason: "sweep-error"
13451
+ }
12698
13452
  });
12699
13453
  }
12700
13454
  }
@@ -12710,14 +13464,19 @@ function scheduleSessionCleanup(state, driver, options) {
12710
13464
  for (const warning2 of config.warnings) {
12711
13465
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
12712
13466
  }
12713
- const dbBytes = statSessionDbBytes(homedir6());
13467
+ const dbBytes = statSessionDbBytes(homedir7());
12714
13468
  void (async () => {
12715
13469
  const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
12716
13470
  if (reclaimAvailability !== null) {
12717
- logActivity(state, {
13471
+ logSessionCleanupActivity(state, "startup", {
12718
13472
  type: "info",
12719
13473
  level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
12720
- message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
13474
+ message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : ""),
13475
+ metadata: {
13476
+ outcome: "preflight-skipped",
13477
+ reason: reclaimAvailability.reason,
13478
+ ...reclaimAvailability.detail !== void 0 ? { detail: reclaimAvailability.detail } : {}
13479
+ }
12721
13480
  });
12722
13481
  }
12723
13482
  const sizeWarning = buildSessionStoreSizeWarning({
@@ -12734,9 +13493,15 @@ function scheduleSessionCleanup(state, driver, options) {
12734
13493
  );
12735
13494
  });
12736
13495
  if (!config.enabled) return;
12737
- logActivity(state, {
13496
+ logSessionCleanupActivity(state, "startup", {
12738
13497
  type: "info",
12739
- message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`
13498
+ message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`,
13499
+ metadata: {
13500
+ outcome: "armed",
13501
+ interval_ms: config.intervalMs,
13502
+ max_age_ms: config.maxAgeMs ?? null,
13503
+ max_count: config.maxCount ?? null
13504
+ }
12740
13505
  });
12741
13506
  const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
12742
13507
  const firstSweep = setTimeout(
@@ -12915,7 +13680,7 @@ function scheduleResourceUsageReporting(state, options) {
12915
13680
  });
12916
13681
  return;
12917
13682
  }
12918
- const { collect, stop } = createResourceUsageCollector(homedir6());
13683
+ const { collect, stop } = createResourceUsageCollector(homedir7());
12919
13684
  state.stopResourceUsageSampling = stop;
12920
13685
  let consecutiveFailures = 0;
12921
13686
  const tick = async () => {
@@ -12928,6 +13693,19 @@ function scheduleResourceUsageReporting(state, options) {
12928
13693
  message: `Resource usage collection: ${warning2}`
12929
13694
  });
12930
13695
  }
13696
+ const lowDiskDecision = evaluateLowDiskWarning({
13697
+ ...usage,
13698
+ nowMs: Date.now(),
13699
+ state: state.lowDiskWarningState
13700
+ });
13701
+ state.lowDiskWarningState = lowDiskDecision.state;
13702
+ if (lowDiskDecision.warning !== null) {
13703
+ logActivity(state, {
13704
+ type: "info",
13705
+ level: "warn",
13706
+ message: lowDiskDecision.warning
13707
+ });
13708
+ }
12931
13709
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
12932
13710
  if (result.ok) {
12933
13711
  if (consecutiveFailures > 0) {
@@ -13140,7 +13918,7 @@ async function run(options) {
13140
13918
  let fileSyncDirectories;
13141
13919
  try {
13142
13920
  logLevel = resolveLogLevel(options);
13143
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
13921
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
13144
13922
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
13145
13923
  throw new Error(
13146
13924
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -13174,6 +13952,7 @@ async function run(options) {
13174
13952
  opencodeClient: null,
13175
13953
  opencodeV2CredentialImportGuard: { inFlight: false },
13176
13954
  sessionDbProvenanceAnomaly: false,
13955
+ sessionStoreHealth: createSessionStoreHealth(),
13177
13956
  opencodeProcess: null,
13178
13957
  stopOpenCodeLogTail: null,
13179
13958
  litestreamProcess: null,
@@ -13191,10 +13970,12 @@ async function run(options) {
13191
13970
  openaiUsageRearm: null,
13192
13971
  resourceUsageTimer: null,
13193
13972
  stopResourceUsageSampling: null,
13973
+ lowDiskWarningState: { worstBand: null, warnedAtMs: null },
13194
13974
  credentialSync: null,
13195
13975
  authHeader: ""
13196
13976
  };
13197
13977
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
13978
+ setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
13198
13979
  if (options.credentialSyncMarker) {
13199
13980
  state.credentialSync = createCredentialSync({
13200
13981
  markerPath: options.credentialSyncMarker,
@@ -13380,13 +14161,21 @@ async function run(options) {
13380
14161
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13381
14162
  }
13382
14163
  state.credentialSync?.arm();
13383
- state.stopOpenCodeLogTail = tailOpenCodeLogFile(
13384
- resolveOpenCodeLogPath(homedir6(), process.env),
13385
- createOpenCodeActivityForwarder(() => ({
14164
+ const openCodeActivityForwarder = createOpenCodeActivityForwarder(
14165
+ () => ({
13386
14166
  agentId: state.agentId,
13387
14167
  authHeader: state.authHeader
13388
- }))
13389
- ).stop;
14168
+ }),
14169
+ state.sessionStoreHealth.observeUnwritable
14170
+ );
14171
+ const openCodeLogTail = tailOpenCodeLogFile(
14172
+ resolveOpenCodeLogPath(homedir7(), process.env),
14173
+ openCodeActivityForwarder
14174
+ );
14175
+ state.stopOpenCodeLogTail = () => {
14176
+ openCodeActivityForwarder.flush();
14177
+ openCodeLogTail.stop();
14178
+ };
13390
14179
  let sessionDbVerifyFatal = false;
13391
14180
  if (!options.restoreSessionDb) {
13392
14181
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -13503,7 +14292,7 @@ async function run(options) {
13503
14292
  const provenance = checkSessionDbProvenance({
13504
14293
  dbPath: sessionDbPath(),
13505
14294
  currentVersion: state.opencodeVersion,
13506
- homeDir: homedir6(),
14295
+ homeDir: homedir7(),
13507
14296
  env: process.env
13508
14297
  });
13509
14298
  if (provenance.anomaly) {
@@ -13670,10 +14459,11 @@ async function run(options) {
13670
14459
  getAuthHeader: () => state.authHeader,
13671
14460
  conversationFilter: state.conversationFilter,
13672
14461
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
14462
+ sessionStoreHealth: state.sessionStoreHealth,
13673
14463
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
13674
14464
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
13675
14465
  fileSyncDirectories,
13676
- homeDir: homedir6(),
14466
+ homeDir: homedir7(),
13677
14467
  maxActiveSessions,
13678
14468
  log: (entry) => (
13679
14469
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -13695,12 +14485,13 @@ async function run(options) {
13695
14485
  openCodePassword: state.opencodePassword,
13696
14486
  isRunning: () => state.running,
13697
14487
  events: {
13698
- onConnected: (agentId, isReconnect) => {
14488
+ onConnected: (agentId, isReconnect, transition) => {
13699
14489
  state.connected = true;
13700
14490
  state.agentId = agentId;
14491
+ const connectionMessage = isReconnect ? `Tunnel reconnected (runner: ${agentId}, generation: ${transition.generation}, attempt: ${transition.attempt}, offline for ${transition.downtimeMs}ms)` : `Tunnel connected (runner: ${agentId}, generation: ${transition.generation})`;
13701
14492
  logActivity(state, {
13702
14493
  type: "info",
13703
- message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (runner: ${agentId})`
14494
+ message: connectionMessage
13704
14495
  });
13705
14496
  if (options.tunnelReadyFile) {
13706
14497
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
@@ -13721,7 +14512,12 @@ async function run(options) {
13721
14512
  version: state.opencodeVersion,
13722
14513
  major: state.opencodeApiVersion,
13723
14514
  connected: state.opencodeConnected
13724
- })
14515
+ }),
14516
+ connection_generation: transition.generation,
14517
+ ...isReconnect && transition.downtimeMs !== null ? {
14518
+ reconnect_attempt: transition.attempt,
14519
+ downtime_ms: transition.downtimeMs
14520
+ } : {}
13725
14521
  });
13726
14522
  if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
13727
14523
  if (state.interactive) displayStatus(state);
@@ -13743,13 +14539,18 @@ async function run(options) {
13743
14539
  if (state.interactive) displayStatus(state);
13744
14540
  });
13745
14541
  },
13746
- onDisconnected: (code, reason) => {
14542
+ onDisconnected: (code, reason, transition) => {
13747
14543
  state.connected = false;
14544
+ const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
13748
14545
  logActivity(state, {
13749
14546
  type: "info",
13750
- message: `Tunnel disconnected (code: ${code}, reason: ${reason})`
14547
+ message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
14548
+ });
14549
+ emitAgentDisconnected(state.agentId, {
14550
+ code,
14551
+ reason: safeReason,
14552
+ connection_generation: transition.generation
13751
14553
  });
13752
- emitAgentDisconnected(state.agentId, { code, reason });
13753
14554
  if (state.interactive) displayStatus(state);
13754
14555
  },
13755
14556
  onError: (error2) => {
@@ -13849,7 +14650,7 @@ async function run(options) {
13849
14650
  },
13850
14651
  report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
13851
14652
  isLocalCredentialProblem: isLocalCredentialProblem2,
13852
- forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
14653
+ forcedOnHint: state.opencodeApiVersion === "v2" ? "OpenCode V2 keeps its connected ChatGPT login in a store Evident cannot read" : "connect a ChatGPT account to this runner, or run `opencode auth login`",
13853
14654
  firstDelayMs: firstReportDelayMs,
13854
14655
  nextDelayMs: usageReportDelayMs,
13855
14656
  failureLogLevel: usageReportFailureLogLevel
@@ -13892,7 +14693,7 @@ async function run(options) {
13892
14693
  }
13893
14694
 
13894
14695
  // src/index.ts
13895
- var { version } = createRequire2(import.meta.url)("../package.json");
14696
+ var version = getCliVersion();
13896
14697
  var program = new Command();
13897
14698
  program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
13898
14699
  "--endpoint <url>",