@evident-ai/cli 3.5.2-dev.2828873 → 3.5.2-dev.2bb04b2

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
@@ -1070,6 +1069,76 @@ import chalk7 from "chalk";
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.2828873" : void 0) ?? process.env.npm_package_version ?? "unknown";
1230
+ var CLI_VERSION = (true ? "3.5.2-dev.2bb04b2" : void 0) ?? process.env.npm_package_version ?? "unknown";
1162
1231
  function getCliVersion() {
1163
1232
  return CLI_VERSION;
1164
1233
  }
@@ -1321,6 +1390,219 @@ var EventTypes = {
1321
1390
 
1322
1391
  // src/lib/runner-activity-telemetry.ts
1323
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
1324
1606
  var FORWARDED_LEVELS_BY_SOURCE = {
1325
1607
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
1326
1608
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
@@ -1337,11 +1619,13 @@ function parseOpenCodeLogLine(line) {
1337
1619
  if (!levelMatch) return null;
1338
1620
  const level = levelMatch[1].toUpperCase();
1339
1621
  if (level !== "WARN" && level !== "ERROR") return null;
1340
- const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
1622
+ const sessionMatch = normalisedLine.match(/(?:^|\s)(?:sessionID|session\.id)=(\S+)/);
1623
+ const messageIDMatch = normalisedLine.match(/(?:^|\s)messageID=(\S+)/);
1341
1624
  const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
1342
1625
  const parsed = {
1343
1626
  level: level === "WARN" ? "warn" : "error",
1344
- sessionID: sessionMatch?.[1]
1627
+ sessionID: sessionMatch?.[1],
1628
+ messageID: messageIDMatch?.[1]
1345
1629
  };
1346
1630
  const message = messageMatch?.[1] ?? messageMatch?.[2];
1347
1631
  if (message !== void 0) parsed.message = message;
@@ -1349,12 +1633,13 @@ function parseOpenCodeLogLine(line) {
1349
1633
  }
1350
1634
  var MAX_LINE_BUFFER_BYTES = 16 * 1024;
1351
1635
  var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
1352
- function createOpenCodeActivityForwarder(getContext) {
1636
+ function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
1353
1637
  let buffer = Buffer.alloc(0);
1354
1638
  const repeatStates = /* @__PURE__ */ new Map();
1355
1639
  const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
1356
1640
  const metadata = {
1357
1641
  ...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
1642
+ ...parsed.messageID ? { messageID: parsed.messageID } : {},
1358
1643
  ...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
1359
1644
  };
1360
1645
  forwardRunnerActivity(
@@ -1379,10 +1664,19 @@ function createOpenCodeActivityForwarder(getContext) {
1379
1664
  }
1380
1665
  };
1381
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
+ }
1382
1676
  const parsed = parseOpenCodeLogLine(line);
1383
1677
  if (!parsed) return;
1384
1678
  const now = Date.now();
1385
- const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}`;
1679
+ const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}\0${parsed.sessionID ?? ""}\0${parsed.messageID ?? ""}`;
1386
1680
  expireRepeatStates(now, key);
1387
1681
  if (key === void 0) {
1388
1682
  forwardLine(line, parsed);
@@ -1449,7 +1743,7 @@ function redactHomeDirectory(message) {
1449
1743
  }
1450
1744
  return result + message.slice(searchFrom);
1451
1745
  }
1452
- function redact(message) {
1746
+ function redactDiagnosticText(message) {
1453
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>");
1454
1748
  return redactHomeDirectory(redacted);
1455
1749
  }
@@ -1468,7 +1762,10 @@ function sanitiseMetadata(metadata) {
1468
1762
  const sanitised = [];
1469
1763
  for (const [key, value] of entries) {
1470
1764
  if (typeof value === "string") {
1471
- 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
+ ]);
1472
1769
  } else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
1473
1770
  sanitised.push([key, value]);
1474
1771
  }
@@ -1513,7 +1810,7 @@ function forwardRunnerActivity(entry, context) {
1513
1810
  if (!context.agentId || !context.authHeader) return;
1514
1811
  if (!admitUnderRateLimit(source, Date.now())) return;
1515
1812
  const rawMessage = entry.error ?? entry.message ?? "";
1516
- const message = truncate(redact(rawMessage));
1813
+ const message = truncate(redactDiagnosticText(rawMessage));
1517
1814
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1518
1815
  severity: SEVERITY_BY_LEVEL[entry.level],
1519
1816
  message,
@@ -1812,8 +2109,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1812
2109
 
1813
2110
  // src/lib/http-timeout.ts
1814
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
+ }
1815
2136
  function withRequestTimeout(fetchImpl, timeoutMs) {
1816
- 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
+ });
1817
2152
  }
1818
2153
 
1819
2154
  // src/lib/opencode/client.ts
@@ -1879,7 +2214,7 @@ async function getOpenCodeDirectory(port, client) {
1879
2214
  return null;
1880
2215
  }
1881
2216
  }
1882
- function roleOf(m) {
2217
+ function roleOf2(m) {
1883
2218
  if (!m || typeof m !== "object") return void 0;
1884
2219
  if (typeof m.role === "string") return m.role;
1885
2220
  const infoRole = m.info?.role;
@@ -1893,13 +2228,13 @@ function createdOf(m) {
1893
2228
  if (!m || typeof m !== "object") return void 0;
1894
2229
  return m.info?.time?.created ?? m.time?.created;
1895
2230
  }
1896
- function idOf(m) {
2231
+ function idOf2(m) {
1897
2232
  if (!m || typeof m !== "object") return void 0;
1898
2233
  if (typeof m.id === "string") return m.id;
1899
2234
  const infoId = m.info?.id;
1900
2235
  return typeof infoId === "string" ? infoId : void 0;
1901
2236
  }
1902
- function parentIdOf(m) {
2237
+ function parentIdOf2(m) {
1903
2238
  if (!m || typeof m !== "object") return void 0;
1904
2239
  if (typeof m.parentID === "string") return m.parentID;
1905
2240
  const infoParent = m.info?.parentID;
@@ -1964,9 +2299,10 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
1964
2299
  function isSessionActivelyGenerating(messages) {
1965
2300
  if (!messages || messages.length === 0) return false;
1966
2301
  const last = messages[messages.length - 1];
1967
- if (roleOf(last) !== "assistant") return false;
2302
+ if (roleOf2(last) !== "assistant") return false;
1968
2303
  return completedOf(last) == null;
1969
2304
  }
2305
+ var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
1970
2306
  function sessionLastActivityMs(session) {
1971
2307
  const candidates = [
1972
2308
  session.time?.updated,
@@ -1981,30 +2317,60 @@ function sessionLastActivityMs(session) {
1981
2317
  }
1982
2318
  return null;
1983
2319
  }
1984
- async function listSessions(port, client) {
1985
- 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);
1986
2328
  try {
1987
- const res = await requestWithClient(port, client, "/session");
2329
+ const res = await requestWithClient(port, client, path);
1988
2330
  if (!res.ok) return null;
1989
2331
  const body = await res.json();
1990
2332
  return Array.isArray(body) ? body : null;
1991
2333
  } catch (error2) {
1992
2334
  console.error(
1993
- `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2335
+ `[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
1994
2336
  );
1995
2337
  return null;
1996
2338
  }
1997
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
+ }
1998
2358
  async function deleteSession(port, id, client) {
1999
2359
  if (client?.version === "v2") return deleteV2Session(client, id);
2000
2360
  try {
2001
- const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2002
- 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 };
2003
2369
  } catch (error2) {
2004
2370
  console.error(
2005
2371
  `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2006
2372
  );
2007
- return false;
2373
+ return { ok: false, status: "threw" };
2008
2374
  }
2009
2375
  }
2010
2376
  async function sessionExists(port, id, client) {
@@ -2245,7 +2611,7 @@ function messageText(m) {
2245
2611
  async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
2246
2612
  const before = await getSessionMessages(port, sessionId, client);
2247
2613
  const knownUserIds = new Set(
2248
- (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")
2249
2615
  );
2250
2616
  const parts = [{ type: "text", text: content }];
2251
2617
  let pendingOutcomes = null;
@@ -2288,8 +2654,8 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2288
2654
  if (after) {
2289
2655
  let best = null;
2290
2656
  for (const m of after) {
2291
- if (roleOf(m) !== "user") continue;
2292
- const id = idOf(m);
2657
+ if (roleOf2(m) !== "user") continue;
2658
+ const id = idOf2(m);
2293
2659
  if (typeof id !== "string" || knownUserIds.has(id)) continue;
2294
2660
  if (messageText(m) !== content) continue;
2295
2661
  const created = createdOf(m) ?? 0;
@@ -2311,13 +2677,13 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2311
2677
  function findAssistantReplyAfter(messages, userMessageId) {
2312
2678
  if (!messages || messages.length === 0) return null;
2313
2679
  const byParent = messages.find(
2314
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
2680
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2315
2681
  );
2316
2682
  if (byParent) return byParent;
2317
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
2683
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2318
2684
  if (userIndex === -1) return null;
2319
2685
  for (let i = userIndex + 1; i < messages.length; i++) {
2320
- if (roleOf(messages[i]) === "assistant") return messages[i];
2686
+ if (roleOf2(messages[i]) === "assistant") return messages[i];
2321
2687
  }
2322
2688
  return null;
2323
2689
  }
@@ -2327,7 +2693,7 @@ function findLastAssistantReplyFor(messages, userMessageId) {
2327
2693
  let lastNonErrored = null;
2328
2694
  for (let i = messages.length - 1; i >= 0; i--) {
2329
2695
  const m = messages[i];
2330
- if (roleOf(m) !== "assistant" || parentIdOf(m) !== userMessageId) continue;
2696
+ if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
2331
2697
  if (lastCorrelated === null) lastCorrelated = m;
2332
2698
  if (errorOf(m) == null) {
2333
2699
  lastNonErrored = m;
@@ -2335,12 +2701,12 @@ function findLastAssistantReplyFor(messages, userMessageId) {
2335
2701
  }
2336
2702
  }
2337
2703
  if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
2338
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
2704
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2339
2705
  if (userIndex === -1) return null;
2340
2706
  let last = null;
2341
2707
  let lastOk = null;
2342
2708
  for (let i = userIndex + 1; i < messages.length; i++) {
2343
- const role = roleOf(messages[i]);
2709
+ const role = roleOf2(messages[i]);
2344
2710
  if (role === "user") break;
2345
2711
  if (role === "assistant") {
2346
2712
  last = messages[i];
@@ -2352,16 +2718,16 @@ function findLastAssistantReplyFor(messages, userMessageId) {
2352
2718
  function collectSubagentSessions(messages, userMessageId) {
2353
2719
  if (!messages || messages.length === 0) return [];
2354
2720
  const byParent = messages.filter(
2355
- (message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
2721
+ (message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
2356
2722
  );
2357
2723
  const assistants = byParent.length > 0 ? byParent : [];
2358
2724
  if (assistants.length === 0) {
2359
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
2725
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2360
2726
  if (userIndex === -1) return [];
2361
2727
  for (let i = userIndex + 1; i < messages.length; i++) {
2362
2728
  const message = messages[i];
2363
- if (roleOf(message) === "user") break;
2364
- if (roleOf(message) === "assistant") assistants.push(message);
2729
+ if (roleOf2(message) === "user") break;
2730
+ if (roleOf2(message) === "assistant") assistants.push(message);
2365
2731
  }
2366
2732
  }
2367
2733
  const refs = [];
@@ -2401,7 +2767,7 @@ function collectTaskCalls(messages, userMessageId) {
2401
2767
  if (!messages || messages.length === 0) return [];
2402
2768
  const calls = [];
2403
2769
  for (const message of messages) {
2404
- if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
2770
+ if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
2405
2771
  for (const part of message.parts ?? []) {
2406
2772
  if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
2407
2773
  continue;
@@ -2431,7 +2797,7 @@ function attributeTaskCallUsage(messages, windows) {
2431
2797
  for (const window of eligibleWindows) assignments.set(window.callID, []);
2432
2798
  const unattributed = [];
2433
2799
  for (const message of messages ?? []) {
2434
- if (roleOf(message) !== "assistant") continue;
2800
+ if (roleOf2(message) !== "assistant") continue;
2435
2801
  const created = finiteNumber(createdOf(message));
2436
2802
  const matching = created === null ? [] : eligibleWindows.filter(
2437
2803
  (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
@@ -2508,7 +2874,7 @@ function sumAssistantUsage(messages) {
2508
2874
  function messageUsage(messages, userMessageId) {
2509
2875
  if (!messages || messages.length === 0) return null;
2510
2876
  const byParentAll = messages.filter(
2511
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
2877
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2512
2878
  );
2513
2879
  const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
2514
2880
  const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
@@ -2524,7 +2890,7 @@ function messageUsage(messages, userMessageId) {
2524
2890
  }
2525
2891
  function messageRunState(messages, userMessageId) {
2526
2892
  if (!messages || messages.length === 0) return "unknown";
2527
- const hasUser = messages.some((m) => idOf(m) === userMessageId);
2893
+ const hasUser = messages.some((m) => idOf2(m) === userMessageId);
2528
2894
  const reply = findLastAssistantReplyFor(messages, userMessageId);
2529
2895
  if (!hasUser) {
2530
2896
  if (!reply) return "unknown";
@@ -2616,7 +2982,7 @@ function findLatestSubagentAuthOutcome(messages, sinceMs) {
2616
2982
  if (!messages || messages.length === 0) return null;
2617
2983
  for (let i = messages.length - 1; i >= 0; i--) {
2618
2984
  const message = messages[i];
2619
- if (roleOf(message) !== "assistant") continue;
2985
+ if (roleOf2(message) !== "assistant") continue;
2620
2986
  const created = createdOf(message);
2621
2987
  if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
2622
2988
  const failure = classifyReplyAuthError(message);
@@ -2648,25 +3014,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
2648
3014
  function hasRunningAssistantExcept(messages, exceptUserMessageId) {
2649
3015
  if (!messages || messages.length === 0) return false;
2650
3016
  return messages.some(
2651
- (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3017
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
2652
3018
  );
2653
3019
  }
2654
3020
  function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
2655
3021
  if (!messages || messages.length === 0) return false;
2656
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3022
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2657
3023
  if (userIndex === -1) return false;
2658
3024
  let hasLaterUser = false;
2659
3025
  let hasStartedLaterUser = false;
2660
3026
  for (let i = userIndex + 1; i < messages.length; i++) {
2661
3027
  const message = messages[i];
2662
- if (roleOf(message) !== "user") continue;
3028
+ if (roleOf2(message) !== "user") continue;
2663
3029
  hasLaterUser = true;
2664
- const laterUserMessageId = idOf(message);
3030
+ const laterUserMessageId = idOf2(message);
2665
3031
  if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
2666
3032
  return false;
2667
3033
  }
2668
3034
  if (messages.some(
2669
- (candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
3035
+ (candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
2670
3036
  )) {
2671
3037
  hasStartedLaterUser = true;
2672
3038
  }
@@ -3137,13 +3503,17 @@ async function listV2Sessions(client) {
3137
3503
  }
3138
3504
  async function deleteV2Session(client, sessionId) {
3139
3505
  try {
3140
- await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3141
- 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 };
3142
3512
  } catch (error2) {
3143
3513
  console.error(
3144
3514
  `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3145
3515
  );
3146
- return false;
3516
+ return { ok: false, status: "threw" };
3147
3517
  }
3148
3518
  }
3149
3519
  async function v2SessionExists(client, sessionId) {
@@ -3765,9 +4135,9 @@ async function importOpenCodeV2Credentials({
3765
4135
 
3766
4136
  // src/lib/opencode/session-db-boot.ts
3767
4137
  import { spawn as spawn2 } from "node:child_process";
3768
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
4138
+ import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
3769
4139
  import { homedir as homedir4 } from "node:os";
3770
- import { dirname as dirname2, resolve as resolvePath } from "node:path";
4140
+ import { dirname as dirname3, resolve as resolvePath } from "node:path";
3771
4141
 
3772
4142
  // src/lib/runner-synchroniser.ts
3773
4143
  import { spawn } from "node:child_process";
@@ -3892,7 +4262,7 @@ function clearMarker(options) {
3892
4262
  function markNoReplicate(options, message) {
3893
4263
  if (options.noReplicateMarker) {
3894
4264
  try {
3895
- mkdirSync(dirname2(options.noReplicateMarker), { recursive: true });
4265
+ mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
3896
4266
  writeFileSync(options.noReplicateMarker, "");
3897
4267
  } catch (error2) {
3898
4268
  options.log(
@@ -4017,7 +4387,7 @@ async function ensureLitestreamConfig(options, env) {
4017
4387
  return null;
4018
4388
  }
4019
4389
  try {
4020
- if (statSync2(configPath).size > 0) return configPath;
4390
+ if (statSync3(configPath).size > 0) return configPath;
4021
4391
  } catch (error2) {
4022
4392
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
4023
4393
  options.log(
@@ -4047,7 +4417,7 @@ async function ensureLitestreamConfig(options, env) {
4047
4417
  return null;
4048
4418
  }
4049
4419
  try {
4050
- mkdirSync(dirname2(configPath), { recursive: true });
4420
+ mkdirSync(dirname3(configPath), { recursive: true });
4051
4421
  writeFileSync(configPath, rendered.stdout);
4052
4422
  } catch (error2) {
4053
4423
  options.log(
@@ -4208,7 +4578,7 @@ async function verifySessionDb(options, configPath, env) {
4208
4578
  }
4209
4579
  function fileExists(path) {
4210
4580
  try {
4211
- statSync2(path);
4581
+ statSync3(path);
4212
4582
  return true;
4213
4583
  } catch (error2) {
4214
4584
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
@@ -4297,7 +4667,7 @@ async function restoreAndVerifySessionDb(options) {
4297
4667
  // src/lib/opencode/session-db-provenance.ts
4298
4668
  import { createRequire } from "node:module";
4299
4669
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
4300
- import { dirname as dirname3, join as join5 } from "node:path";
4670
+ import { dirname as dirname4, join as join5 } from "node:path";
4301
4671
  var require2 = createRequire(import.meta.url);
4302
4672
  function readSessionDbMigrationIds(dbPath) {
4303
4673
  let db;
@@ -4363,7 +4733,7 @@ function loadSessionDbProvenanceState(path) {
4363
4733
  }
4364
4734
  function saveSessionDbProvenanceState(path, state) {
4365
4735
  try {
4366
- mkdirSync2(dirname3(path), { recursive: true });
4736
+ mkdirSync2(dirname4(path), { recursive: true });
4367
4737
  writeFileSync2(path, `${JSON.stringify(state, null, 2)}
4368
4738
  `, "utf8");
4369
4739
  } catch (error2) {
@@ -4944,7 +5314,10 @@ function selectSessionsToDelete(sessions, opts) {
4944
5314
  toDelete.push(s.id);
4945
5315
  }
4946
5316
  }
4947
- 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
+ );
4948
5321
  }
4949
5322
  var DEFAULT_INTERVAL = "1h";
4950
5323
  function resolve(flag, envValue, fallback) {
@@ -5004,13 +5377,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
5004
5377
  }
5005
5378
 
5006
5379
  // src/lib/opencode/session-db-size.ts
5007
- import { statSync as statSync3 } from "node:fs";
5380
+ import { statSync as statSync4 } from "node:fs";
5008
5381
  import { join as join6 } from "node:path";
5009
5382
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
5010
5383
  function statSessionDbBytes(homeDir) {
5011
5384
  const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
5012
5385
  try {
5013
- return statSync3(dbPath).size;
5386
+ return statSync4(dbPath).size;
5014
5387
  } catch (err) {
5015
5388
  const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
5016
5389
  if (!isMissingFile) {
@@ -5036,7 +5409,7 @@ function buildSessionStoreSizeWarning(input) {
5036
5409
  }
5037
5410
 
5038
5411
  // src/lib/opencode/log-tail.ts
5039
- import { statSync as statSync4 } from "node:fs";
5412
+ import { statSync as statSync5 } from "node:fs";
5040
5413
  import { homedir as homedir5 } from "node:os";
5041
5414
  import { join as join7 } from "node:path";
5042
5415
  import { open as open2, stat } from "node:fs/promises";
@@ -5059,7 +5432,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
5059
5432
  let inode = null;
5060
5433
  let baselineReady = true;
5061
5434
  try {
5062
- const initial = statSync4(logPath);
5435
+ const initial = statSync5(logPath);
5063
5436
  offset = initial.size;
5064
5437
  inode = initial.ino;
5065
5438
  } catch (error2) {
@@ -5072,173 +5445,50 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
5072
5445
  let stopped = false;
5073
5446
  const poll = async () => {
5074
5447
  if (polling || stopped) return;
5075
- polling = true;
5076
- try {
5077
- let current;
5078
- try {
5079
- current = await stat(logPath);
5080
- } catch (error2) {
5081
- if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
5082
- return;
5083
- }
5084
- if (!baselineReady) {
5085
- offset = current.size;
5086
- inode = current.ino;
5087
- baselineReady = true;
5088
- return;
5089
- }
5090
- if (inode !== null && current.ino !== inode || current.size < offset) {
5091
- offset = 0;
5092
- }
5093
- inode = current.ino;
5094
- if (current.size === offset) return;
5095
- const length = current.size - offset;
5096
- const fh = await open2(logPath, "r");
5097
- try {
5098
- const buf = Buffer.alloc(length);
5099
- const { bytesRead } = await fh.read(buf, 0, length, offset);
5100
- offset += bytesRead;
5101
- if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
5102
- } finally {
5103
- await fh.close();
5104
- }
5105
- } catch (error2) {
5106
- if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
5107
- } finally {
5108
- polling = false;
5109
- }
5110
- };
5111
- const interval = setInterval(() => void poll(), pollIntervalMs);
5112
- void poll();
5113
- return {
5114
- stop: () => {
5115
- stopped = true;
5116
- clearInterval(interval);
5117
- }
5118
- };
5119
- }
5120
-
5121
- // src/lib/opencode/session-db-reclaim.ts
5122
- import { statSync as statSync5, statfsSync } from "node:fs";
5123
- import { dirname as dirname4 } from "node:path";
5124
- function errorMessage(error2) {
5125
- if (!(error2 instanceof Error)) return String(error2);
5126
- return error2.cause instanceof Error ? error2.cause.message : error2.message;
5127
- }
5128
- function insufficientSpaceReason(dbPath, requiredBytes) {
5129
- try {
5130
- const fsStats = statfsSync(dirname4(dbPath));
5131
- const availableBytes = fsStats.bavail * fsStats.bsize;
5132
- if (availableBytes < requiredBytes) {
5133
- return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
5134
- }
5135
- return null;
5136
- } catch (err) {
5137
- return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
5138
- }
5139
- }
5140
- function readLogicalBytes(db) {
5141
- const pageCount = db.prepare("PRAGMA page_count").get().page_count;
5142
- const pageSize = db.prepare("PRAGMA page_size").get().page_size;
5143
- return pageCount * pageSize;
5144
- }
5145
- function readCheckpointResult(db) {
5146
- const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
5147
- return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
5148
- }
5149
- async function probeReclaimAvailability(input) {
5150
- const { dbPath, requiredBytes } = input;
5151
- let sqlite;
5152
- try {
5153
- sqlite = await import("node:sqlite");
5154
- } catch (err) {
5155
- const detail = `Node ${process.version}: ${errorMessage(err)}`;
5156
- console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
5157
- return { reason: "sqlite-unavailable", detail };
5158
- }
5159
- let autoVacuum = null;
5160
- try {
5161
- const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
5162
- try {
5163
- db.exec("PRAGMA busy_timeout=5000");
5164
- autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
5165
- } finally {
5166
- db.close();
5167
- }
5168
- } catch (err) {
5169
- console.warn(
5170
- `[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
5171
- );
5172
- }
5173
- if (autoVacuum !== 0) return null;
5174
- return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
5175
- }
5176
- async function reclaimSessionDbSpace(input) {
5177
- const { dbPath, maxPages, allowFullVacuum = true } = input;
5178
- let sqlite;
5179
- try {
5180
- sqlite = await import("node:sqlite");
5181
- } catch (err) {
5182
- const detail = `Node ${process.version}: ${errorMessage(err)}`;
5183
- console.warn(
5184
- `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
5185
- );
5186
- return { ok: false, skipped: "sqlite-unavailable", detail };
5187
- }
5188
- const { DatabaseSync } = sqlite;
5189
- let db;
5190
- try {
5191
- db = new DatabaseSync(dbPath);
5192
- db.exec("PRAGMA busy_timeout=5000");
5193
- const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
5194
- if (autoVacuum === 0) {
5195
- if (!allowFullVacuum) {
5196
- console.warn(
5197
- `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
5198
- );
5199
- return {
5200
- ok: false,
5201
- skipped: "full-vacuum-blocked",
5202
- detail: "a session turn is live"
5203
- };
5448
+ polling = true;
5449
+ try {
5450
+ let current;
5451
+ try {
5452
+ current = await stat(logPath);
5453
+ } catch (error2) {
5454
+ if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
5455
+ return;
5204
5456
  }
5205
- const fileBytesForGuard = statSync5(dbPath).size;
5206
- const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
5207
- if (skipReason !== null) {
5208
- console.warn(
5209
- `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
5210
- );
5211
- return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
5457
+ if (!baselineReady) {
5458
+ offset = current.size;
5459
+ inode = current.ino;
5460
+ baselineReady = true;
5461
+ return;
5212
5462
  }
5213
- const beforeBytes = readLogicalBytes(db);
5214
- db.exec("PRAGMA auto_vacuum=INCREMENTAL");
5215
- db.exec("VACUUM");
5216
- const afterBytes = readLogicalBytes(db);
5217
- const checkpoint = readCheckpointResult(db);
5218
- return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
5463
+ if (inode !== null && current.ino !== inode || current.size < offset) {
5464
+ offset = 0;
5465
+ }
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;
5219
5482
  }
5220
- if (autoVacuum === 2) {
5221
- const beforeBytes = readLogicalBytes(db);
5222
- const bound = Math.max(0, Math.trunc(maxPages));
5223
- db.exec(`PRAGMA incremental_vacuum(${bound})`);
5224
- const afterBytes = readLogicalBytes(db);
5225
- const checkpoint = readCheckpointResult(db);
5226
- 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);
5227
5490
  }
5228
- console.warn(
5229
- `[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
5230
- );
5231
- return { ok: false, skipped: "auto-vacuum-not-applicable" };
5232
- } catch (err) {
5233
- console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
5234
- return {
5235
- ok: false,
5236
- skipped: "reclaim-error",
5237
- detail: errorMessage(err)
5238
- };
5239
- } finally {
5240
- db?.close();
5241
- }
5491
+ };
5242
5492
  }
5243
5493
 
5244
5494
  // src/lib/tunnel/connection.ts
@@ -5470,6 +5720,8 @@ var StreamForwarder = class {
5470
5720
  if (done) break;
5471
5721
  const chunk = Buffer.from(value);
5472
5722
  for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
5723
+ await stream.pausePromise;
5724
+ if (ac.signal.aborted) break;
5473
5725
  const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
5474
5726
  if (this.binaryFramesSupported) {
5475
5727
  if (this.ws.readyState === WebSocket.OPEN) {
@@ -5512,6 +5764,7 @@ function classifyUpgradeRejection(headers) {
5512
5764
  }
5513
5765
  var MAX_RECONNECT_DELAY = 3e4;
5514
5766
  var BASE_RECONNECT_DELAY = 500;
5767
+ var RELAY_SILENCE_TIMEOUT_MS = 9e4;
5515
5768
  function getReconnectDelay(attempt) {
5516
5769
  const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
5517
5770
  const jitter = Math.random() * 1e3;
@@ -5583,10 +5836,27 @@ function connectTunnel(options) {
5583
5836
  ws.close();
5584
5837
  reject(new Error("Connection timeout"));
5585
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
+ };
5586
5855
  let upgradeRejection = null;
5587
5856
  let upgradeRejectionReason = null;
5588
5857
  ws.on("unexpected-response", (_req, res) => {
5589
5858
  clearTimeout(connectionTimeout);
5859
+ clearRelaySilenceTimeout();
5590
5860
  const reason = classifyUpgradeRejection(res.headers);
5591
5861
  upgradeRejectionReason = reason;
5592
5862
  const chunks = [];
@@ -5616,6 +5886,7 @@ function connectTunnel(options) {
5616
5886
  onInfo?.("WebSocket connection established");
5617
5887
  });
5618
5888
  ws.on("message", (data, isBinary) => {
5889
+ if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
5619
5890
  if (isBinary) {
5620
5891
  try {
5621
5892
  const frame = decodeBinaryBodyFrame(toUint8Array(data));
@@ -5649,6 +5920,7 @@ function connectTunnel(options) {
5649
5920
  switch (message.type) {
5650
5921
  case "connected": {
5651
5922
  clearTimeout(connectionTimeout);
5923
+ armRelaySilenceTimeout();
5652
5924
  forwarder.setBinaryFramesSupported(message.binary_frames === true);
5653
5925
  const connectedAgentId = message.agent_id ?? agentId;
5654
5926
  onConnected?.(connectedAgentId);
@@ -5673,6 +5945,7 @@ function connectTunnel(options) {
5673
5945
  });
5674
5946
  ws.on("error", (error2) => {
5675
5947
  clearTimeout(connectionTimeout);
5948
+ clearRelaySilenceTimeout();
5676
5949
  const detail = upgradeRejection ?? describeSocketError(error2, url);
5677
5950
  if (upgradeRejectionReason === "do_code_updated") {
5678
5951
  onWarning?.("Relay redeployed \u2014 reconnecting");
@@ -5684,7 +5957,8 @@ function connectTunnel(options) {
5684
5957
  );
5685
5958
  });
5686
5959
  ws.on("close", (code, reason) => {
5687
- 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");
5688
5962
  forwarder.abortAll();
5689
5963
  onDisconnected?.(code, reasonStr);
5690
5964
  });
@@ -5692,9 +5966,10 @@ function connectTunnel(options) {
5692
5966
  }
5693
5967
 
5694
5968
  // src/lib/tunnel/runner-connection.ts
5695
- var RunnerConnection = class {
5969
+ var RunnerConnection = class _RunnerConnection {
5696
5970
  opts;
5697
5971
  sleep;
5972
+ now;
5698
5973
  connection = null;
5699
5974
  resolvedAgentId;
5700
5975
  /** True while a (re)connect loop is in flight. */
@@ -5703,10 +5978,17 @@ var RunnerConnection = class {
5703
5978
  reconnectPromise = null;
5704
5979
  /** 1-based count of the current reconnect attempt streak. */
5705
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
+ ]);
5706
5987
  constructor(opts) {
5707
5988
  this.opts = opts;
5708
5989
  this.resolvedAgentId = opts.agentId;
5709
5990
  this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5991
+ this.now = opts.now ?? Date.now;
5710
5992
  }
5711
5993
  get agentId() {
5712
5994
  return this.resolvedAgentId;
@@ -5742,14 +6024,24 @@ var RunnerConnection = class {
5742
6024
  port: this.opts.port,
5743
6025
  openCodePassword: this.opts.openCodePassword,
5744
6026
  onConnected: (agentId) => {
6027
+ const attempt = this.reconnectAttempt + 1;
5745
6028
  this.reconnectAttempt = 0;
5746
6029
  this.reconnecting = false;
5747
6030
  this.resolvedAgentId = agentId;
5748
- 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
+ });
5749
6039
  },
5750
6040
  onDisconnected: (code, reason) => {
5751
- events.onDisconnected(code, reason);
5752
- 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) {
5753
6045
  this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
5754
6046
  events.onError?.(`Reconnection failed: ${err.message}`);
5755
6047
  });
@@ -6088,6 +6380,83 @@ function createResourceUsageCollector(homeDir) {
6088
6380
  };
6089
6381
  }
6090
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
+
6091
6460
  // src/lib/channels/driver.ts
6092
6461
  import { homedir as homedir6 } from "node:os";
6093
6462
 
@@ -6546,6 +6915,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
6546
6915
  var MAX_BUFFERED_SESSION_ERRORS = 256;
6547
6916
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
6548
6917
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
6918
+ var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
6549
6919
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
6550
6920
  var HEARTBEAT_MS = 6e4;
6551
6921
  var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
@@ -6607,6 +6977,7 @@ var ChannelDriver = class _ChannelDriver {
6607
6977
  maxActiveSessions;
6608
6978
  watcherStallMs;
6609
6979
  wedgeWarningIntervalMs;
6980
+ sessionStoreHealth;
6610
6981
  openCodeClient;
6611
6982
  /** Cache of conversationId → opencode sessionId. */
6612
6983
  sessions = /* @__PURE__ */ new Map();
@@ -6976,6 +7347,7 @@ var ChannelDriver = class _ChannelDriver {
6976
7347
  this.maxActiveSessions = config.maxActiveSessions;
6977
7348
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
6978
7349
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
7350
+ this.sessionStoreHealth = config.sessionStoreHealth;
6979
7351
  }
6980
7352
  get isV2() {
6981
7353
  return this.openCodeClient.version === "v2";
@@ -6991,7 +7363,9 @@ var ChannelDriver = class _ChannelDriver {
6991
7363
  return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
6992
7364
  }
6993
7365
  async listSessions() {
6994
- 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
+ });
6995
7369
  }
6996
7370
  async sessionExists(sessionId) {
6997
7371
  return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
@@ -8445,8 +8819,10 @@ var ChannelDriver = class _ChannelDriver {
8445
8819
  b2LastDescendantCheckMs: 0,
8446
8820
  b2RootOngoingHeldLogged: false,
8447
8821
  b2AbandonedSignalled: false,
8822
+ b2AbandonedWithoutAnswer: false,
8448
8823
  ambiguousPinnedSinceMs: 0,
8449
8824
  ambiguousResolved: false,
8825
+ sessionStoreFailurePending: false,
8450
8826
  neverStartedResolved: false
8451
8827
  });
8452
8828
  const buffered = this.bufferedSessionErrors.get(sessionId);
@@ -8563,8 +8939,10 @@ var ChannelDriver = class _ChannelDriver {
8563
8939
  b2LastDescendantCheckMs: 0,
8564
8940
  b2RootOngoingHeldLogged: false,
8565
8941
  b2AbandonedSignalled: false,
8942
+ b2AbandonedWithoutAnswer: false,
8566
8943
  ambiguousPinnedSinceMs: 0,
8567
8944
  ambiguousResolved: false,
8945
+ sessionStoreFailurePending: false,
8568
8946
  neverStartedResolved: false
8569
8947
  });
8570
8948
  }
@@ -8978,6 +9356,10 @@ var ChannelDriver = class _ChannelDriver {
8978
9356
  if (state === "running" || state === "done" || state === "failed") {
8979
9357
  this.neverStartedAttempts.delete(id);
8980
9358
  }
9359
+ if (inFlight.sessionStoreFailurePending) {
9360
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
9361
+ return;
9362
+ }
8981
9363
  if (inFlight.neverStartedResolved) {
8982
9364
  if (state === "running" || state === "done" || state === "failed") {
8983
9365
  inFlight.neverStartedResolved = false;
@@ -9137,10 +9519,22 @@ var ChannelDriver = class _ChannelDriver {
9137
9519
  inFlight.b2LastDescendantCheckMs = 0;
9138
9520
  inFlight.b2RootOngoingHeldLogged = false;
9139
9521
  inFlight.b2AbandonedSignalled = false;
9522
+ inFlight.b2AbandonedWithoutAnswer = false;
9140
9523
  }
9141
9524
  } else {
9142
9525
  if (inFlight.b2AbandonedSignalled) {
9143
- 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
+ }
9144
9538
  return;
9145
9539
  }
9146
9540
  if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
@@ -9157,17 +9551,47 @@ var ChannelDriver = class _ChannelDriver {
9157
9551
  descendantOngoing,
9158
9552
  rootOngoing
9159
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
+ }
9160
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
+ }
9161
9586
  this.log({
9162
9587
  level: "warn",
9163
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`,
9164
9589
  conversation_id: conv.id,
9165
9590
  message_id: id
9166
9591
  });
9167
- const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
9168
9592
  void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
9169
9593
  watched_for_ms: pinnedForMs,
9170
- finish: reply?.info?.finish ?? reply?.finish,
9594
+ finish,
9171
9595
  ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
9172
9596
  ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
9173
9597
  opencode_message_id: inFlight.opencodeMessageId
@@ -9215,6 +9639,16 @@ var ChannelDriver = class _ChannelDriver {
9215
9639
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
9216
9640
  sessionOngoing: ongoing
9217
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
+ }
9218
9652
  inFlight.ambiguousResolved = true;
9219
9653
  this.log({
9220
9654
  level: "warn",
@@ -9439,6 +9873,74 @@ var ChannelDriver = class _ChannelDriver {
9439
9873
  }
9440
9874
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9441
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
+ }
9442
9944
  /**
9443
9945
  * Report a queued message as failed after the never-started bound. The accepted
9444
9946
  * OpenCode copy is not cancelled, so the local fence remains on unsuccessful
@@ -9494,6 +9996,66 @@ var ChannelDriver = class _ChannelDriver {
9494
9996
  this.neverStartedAttempts.delete(inFlight.evidentMessageId);
9495
9997
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9496
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
+ }
9497
10059
  // Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
9498
10060
  /**
9499
10061
  * Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
@@ -12225,6 +12787,7 @@ function createCredentialSync({
12225
12787
 
12226
12788
  // src/commands/run.ts
12227
12789
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
12790
+ var MAX_DISCONNECT_REASON_LENGTH = 200;
12228
12791
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
12229
12792
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
12230
12793
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
@@ -12443,7 +13006,9 @@ function displayStatus(state) {
12443
13006
  const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
12444
13007
  const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
12445
13008
  const last = state.activityLog[state.activityLog.length - 1];
12446
- 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
+ ) : "";
12447
13012
  const agent = state.agentName ?? state.agentId;
12448
13013
  console.log(
12449
13014
  `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
@@ -12695,9 +13260,9 @@ async function driveChannels(state, driver) {
12695
13260
  }
12696
13261
  }
12697
13262
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
12698
- var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
13263
+ var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
12699
13264
  function shouldWarnForReclaimSkip(reason) {
12700
- if (reason !== "sqlite-unavailable") return false;
13265
+ if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
12701
13266
  const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
12702
13267
  if (!version2) return false;
12703
13268
  const major = Number(version2[1]);
@@ -12754,61 +13319,96 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
12754
13319
  async function runSweep(state, driver, config) {
12755
13320
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
12756
13321
  try {
12757
- const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
12758
- if (sessions === null) {
12759
- logSessionCleanupActivity(state, "sweep", {
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) {
13326
+ logActivity(state, {
12760
13327
  type: "info",
12761
- message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`,
12762
- metadata: {
12763
- outcome: "sweep-skipped",
12764
- reason: "opencode-unreachable"
12765
- }
13328
+ level: "warn",
13329
+ message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
12766
13330
  });
12767
- return;
12768
13331
  }
12769
- const toDelete = selectSessionsToDelete(
12770
- sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
12771
- {
12772
- maxAgeMs: config.maxAgeMs,
12773
- maxCount: config.maxCount,
12774
- nowMs: Date.now(),
12775
- protectedIds: driver.protectedSessionIds()
12776
- }
12777
- );
12778
- const protectedNow = driver.protectedSessionIds();
12779
13332
  let deleted = 0;
12780
13333
  let failed = 0;
13334
+ let failed4xx = 0;
13335
+ let failed5xx = 0;
13336
+ let failedOtherStatus = 0;
13337
+ let failedThrew = 0;
12781
13338
  let skippedNewlyActive = 0;
12782
- for (const id of toDelete) {
12783
- if (protectedNow.has(id)) {
12784
- skippedNewlyActive++;
12785
- logActivity(state, {
12786
- type: "info",
12787
- message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
12788
- });
12789
- 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
+ }
12790
13376
  }
12791
- if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
12792
- 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
+ });
12793
13384
  }
12794
- const failedNote = failed > 0 ? `, failed ${failed}` : "";
12795
- const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
12796
- logActivity(state, {
12797
- type: "info",
12798
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`
12799
- });
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
+ };
12800
13397
  const reclaimResult = await reclaimSessionDbSpace({
12801
13398
  dbPath: sessionDbPath(),
12802
- 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,
12803
13402
  allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
12804
13403
  });
12805
13404
  if (reclaimResult.ok) {
13405
+ state.sessionStoreHealth.observeWritable();
12806
13406
  const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
12807
13407
  const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
12808
13408
  const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
12809
13409
  logSessionCleanupActivity(state, "sweep", {
12810
13410
  type: "info",
12811
- 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,
12812
13412
  metadata: {
12813
13413
  outcome: "reclaimed",
12814
13414
  mode: reclaimResult.mode,
@@ -12816,25 +13416,27 @@ async function runSweep(state, driver, config) {
12816
13416
  after_bytes: reclaimResult.afterBytes,
12817
13417
  checkpoint_busy: reclaimResult.checkpoint.busy,
12818
13418
  wal_frames_pending: reclaimResult.checkpoint.log,
12819
- sessions_inspected: sessions.length,
12820
- sessions_deleted: deleted,
12821
- sessions_failed: failed,
12822
- sessions_skipped_active: skippedNewlyActive
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
12823
13425
  }
12824
13426
  });
12825
13427
  } else {
13428
+ if (reclaimResult.skipped === "disk-exhaustion") {
13429
+ state.sessionStoreHealth.observeUnwritable();
13430
+ }
12826
13431
  logSessionCleanupActivity(state, "sweep", {
12827
13432
  type: "info",
12828
- level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
13433
+ level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
12829
13434
  message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
12830
13435
  metadata: {
12831
13436
  outcome: "reclaim-skipped",
12832
13437
  reason: reclaimResult.skipped,
12833
13438
  ...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
12834
- sessions_inspected: sessions.length,
12835
- sessions_deleted: deleted,
12836
- sessions_failed: failed,
12837
- sessions_skipped_active: skippedNewlyActive
13439
+ ...sessionMetadata
12838
13440
  }
12839
13441
  });
12840
13442
  }
@@ -13091,6 +13693,19 @@ function scheduleResourceUsageReporting(state, options) {
13091
13693
  message: `Resource usage collection: ${warning2}`
13092
13694
  });
13093
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
+ }
13094
13709
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
13095
13710
  if (result.ok) {
13096
13711
  if (consecutiveFailures > 0) {
@@ -13337,6 +13952,7 @@ async function run(options) {
13337
13952
  opencodeClient: null,
13338
13953
  opencodeV2CredentialImportGuard: { inFlight: false },
13339
13954
  sessionDbProvenanceAnomaly: false,
13955
+ sessionStoreHealth: createSessionStoreHealth(),
13340
13956
  opencodeProcess: null,
13341
13957
  stopOpenCodeLogTail: null,
13342
13958
  litestreamProcess: null,
@@ -13354,6 +13970,7 @@ async function run(options) {
13354
13970
  openaiUsageRearm: null,
13355
13971
  resourceUsageTimer: null,
13356
13972
  stopResourceUsageSampling: null,
13973
+ lowDiskWarningState: { worstBand: null, warnedAtMs: null },
13357
13974
  credentialSync: null,
13358
13975
  authHeader: ""
13359
13976
  };
@@ -13544,10 +14161,13 @@ async function run(options) {
13544
14161
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13545
14162
  }
13546
14163
  state.credentialSync?.arm();
13547
- const openCodeActivityForwarder = createOpenCodeActivityForwarder(() => ({
13548
- agentId: state.agentId,
13549
- authHeader: state.authHeader
13550
- }));
14164
+ const openCodeActivityForwarder = createOpenCodeActivityForwarder(
14165
+ () => ({
14166
+ agentId: state.agentId,
14167
+ authHeader: state.authHeader
14168
+ }),
14169
+ state.sessionStoreHealth.observeUnwritable
14170
+ );
13551
14171
  const openCodeLogTail = tailOpenCodeLogFile(
13552
14172
  resolveOpenCodeLogPath(homedir7(), process.env),
13553
14173
  openCodeActivityForwarder
@@ -13839,6 +14459,7 @@ async function run(options) {
13839
14459
  getAuthHeader: () => state.authHeader,
13840
14460
  conversationFilter: state.conversationFilter,
13841
14461
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
14462
+ sessionStoreHealth: state.sessionStoreHealth,
13842
14463
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
13843
14464
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
13844
14465
  fileSyncDirectories,
@@ -13864,12 +14485,13 @@ async function run(options) {
13864
14485
  openCodePassword: state.opencodePassword,
13865
14486
  isRunning: () => state.running,
13866
14487
  events: {
13867
- onConnected: (agentId, isReconnect) => {
14488
+ onConnected: (agentId, isReconnect, transition) => {
13868
14489
  state.connected = true;
13869
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})`;
13870
14492
  logActivity(state, {
13871
14493
  type: "info",
13872
- message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (runner: ${agentId})`
14494
+ message: connectionMessage
13873
14495
  });
13874
14496
  if (options.tunnelReadyFile) {
13875
14497
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
@@ -13890,7 +14512,12 @@ async function run(options) {
13890
14512
  version: state.opencodeVersion,
13891
14513
  major: state.opencodeApiVersion,
13892
14514
  connected: state.opencodeConnected
13893
- })
14515
+ }),
14516
+ connection_generation: transition.generation,
14517
+ ...isReconnect && transition.downtimeMs !== null ? {
14518
+ reconnect_attempt: transition.attempt,
14519
+ downtime_ms: transition.downtimeMs
14520
+ } : {}
13894
14521
  });
13895
14522
  if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
13896
14523
  if (state.interactive) displayStatus(state);
@@ -13912,13 +14539,18 @@ async function run(options) {
13912
14539
  if (state.interactive) displayStatus(state);
13913
14540
  });
13914
14541
  },
13915
- onDisconnected: (code, reason) => {
14542
+ onDisconnected: (code, reason, transition) => {
13916
14543
  state.connected = false;
14544
+ const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
13917
14545
  logActivity(state, {
13918
14546
  type: "info",
13919
- 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
13920
14553
  });
13921
- emitAgentDisconnected(state.agentId, { code, reason });
13922
14554
  if (state.interactive) displayStatus(state);
13923
14555
  },
13924
14556
  onError: (error2) => {
@@ -14061,7 +14693,7 @@ async function run(options) {
14061
14693
  }
14062
14694
 
14063
14695
  // src/index.ts
14064
- var { version } = createRequire2(import.meta.url)("../package.json");
14696
+ var version = getCliVersion();
14065
14697
  var program = new Command();
14066
14698
  program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
14067
14699
  "--endpoint <url>",