@evident-ai/cli 3.5.2-dev.817a271 → 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
@@ -1069,6 +1069,76 @@ import chalk7 from "chalk";
1069
1069
  // ../../packages/types/src/agents/index.ts
1070
1070
  var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
1071
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
+
1072
1142
  // ../../packages/types/src/telemetry/index.ts
1073
1143
  var TelemetryEventTypes = {
1074
1144
  // Agent activity events (shown in web UI activity log)
@@ -1157,7 +1227,7 @@ import ora4 from "ora";
1157
1227
  import { select as select4 } from "@inquirer/prompts";
1158
1228
 
1159
1229
  // src/lib/telemetry.ts
1160
- var CLI_VERSION = (true ? "3.5.2-dev.817a271" : 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";
1161
1231
  function getCliVersion() {
1162
1232
  return CLI_VERSION;
1163
1233
  }
@@ -1320,6 +1390,219 @@ var EventTypes = {
1320
1390
 
1321
1391
  // src/lib/runner-activity-telemetry.ts
1322
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
1323
1606
  var FORWARDED_LEVELS_BY_SOURCE = {
1324
1607
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
1325
1608
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
@@ -1336,11 +1619,13 @@ function parseOpenCodeLogLine(line) {
1336
1619
  if (!levelMatch) return null;
1337
1620
  const level = levelMatch[1].toUpperCase();
1338
1621
  if (level !== "WARN" && level !== "ERROR") return null;
1339
- 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+)/);
1340
1624
  const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
1341
1625
  const parsed = {
1342
1626
  level: level === "WARN" ? "warn" : "error",
1343
- sessionID: sessionMatch?.[1]
1627
+ sessionID: sessionMatch?.[1],
1628
+ messageID: messageIDMatch?.[1]
1344
1629
  };
1345
1630
  const message = messageMatch?.[1] ?? messageMatch?.[2];
1346
1631
  if (message !== void 0) parsed.message = message;
@@ -1348,12 +1633,13 @@ function parseOpenCodeLogLine(line) {
1348
1633
  }
1349
1634
  var MAX_LINE_BUFFER_BYTES = 16 * 1024;
1350
1635
  var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
1351
- function createOpenCodeActivityForwarder(getContext) {
1636
+ function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
1352
1637
  let buffer = Buffer.alloc(0);
1353
1638
  const repeatStates = /* @__PURE__ */ new Map();
1354
1639
  const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
1355
1640
  const metadata = {
1356
1641
  ...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
1642
+ ...parsed.messageID ? { messageID: parsed.messageID } : {},
1357
1643
  ...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
1358
1644
  };
1359
1645
  forwardRunnerActivity(
@@ -1378,10 +1664,19 @@ function createOpenCodeActivityForwarder(getContext) {
1378
1664
  }
1379
1665
  };
1380
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
+ }
1381
1676
  const parsed = parseOpenCodeLogLine(line);
1382
1677
  if (!parsed) return;
1383
1678
  const now = Date.now();
1384
- 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 ?? ""}`;
1385
1680
  expireRepeatStates(now, key);
1386
1681
  if (key === void 0) {
1387
1682
  forwardLine(line, parsed);
@@ -1919,7 +2214,7 @@ async function getOpenCodeDirectory(port, client) {
1919
2214
  return null;
1920
2215
  }
1921
2216
  }
1922
- function roleOf(m) {
2217
+ function roleOf2(m) {
1923
2218
  if (!m || typeof m !== "object") return void 0;
1924
2219
  if (typeof m.role === "string") return m.role;
1925
2220
  const infoRole = m.info?.role;
@@ -1933,13 +2228,13 @@ function createdOf(m) {
1933
2228
  if (!m || typeof m !== "object") return void 0;
1934
2229
  return m.info?.time?.created ?? m.time?.created;
1935
2230
  }
1936
- function idOf(m) {
2231
+ function idOf2(m) {
1937
2232
  if (!m || typeof m !== "object") return void 0;
1938
2233
  if (typeof m.id === "string") return m.id;
1939
2234
  const infoId = m.info?.id;
1940
2235
  return typeof infoId === "string" ? infoId : void 0;
1941
2236
  }
1942
- function parentIdOf(m) {
2237
+ function parentIdOf2(m) {
1943
2238
  if (!m || typeof m !== "object") return void 0;
1944
2239
  if (typeof m.parentID === "string") return m.parentID;
1945
2240
  const infoParent = m.info?.parentID;
@@ -2004,9 +2299,10 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
2004
2299
  function isSessionActivelyGenerating(messages) {
2005
2300
  if (!messages || messages.length === 0) return false;
2006
2301
  const last = messages[messages.length - 1];
2007
- if (roleOf(last) !== "assistant") return false;
2302
+ if (roleOf2(last) !== "assistant") return false;
2008
2303
  return completedOf(last) == null;
2009
2304
  }
2305
+ var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
2010
2306
  function sessionLastActivityMs(session) {
2011
2307
  const candidates = [
2012
2308
  session.time?.updated,
@@ -2021,30 +2317,60 @@ function sessionLastActivityMs(session) {
2021
2317
  }
2022
2318
  return null;
2023
2319
  }
2024
- async function listSessions(port, client) {
2025
- 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);
2026
2328
  try {
2027
- const res = await requestWithClient(port, client, "/session");
2329
+ const res = await requestWithClient(port, client, path);
2028
2330
  if (!res.ok) return null;
2029
2331
  const body = await res.json();
2030
2332
  return Array.isArray(body) ? body : null;
2031
2333
  } catch (error2) {
2032
2334
  console.error(
2033
- `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2335
+ `[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2034
2336
  );
2035
2337
  return null;
2036
2338
  }
2037
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
+ }
2038
2358
  async function deleteSession(port, id, client) {
2039
2359
  if (client?.version === "v2") return deleteV2Session(client, id);
2040
2360
  try {
2041
- const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2042
- 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 };
2043
2369
  } catch (error2) {
2044
2370
  console.error(
2045
2371
  `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2046
2372
  );
2047
- return false;
2373
+ return { ok: false, status: "threw" };
2048
2374
  }
2049
2375
  }
2050
2376
  async function sessionExists(port, id, client) {
@@ -2285,7 +2611,7 @@ function messageText(m) {
2285
2611
  async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
2286
2612
  const before = await getSessionMessages(port, sessionId, client);
2287
2613
  const knownUserIds = new Set(
2288
- (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")
2289
2615
  );
2290
2616
  const parts = [{ type: "text", text: content }];
2291
2617
  let pendingOutcomes = null;
@@ -2328,8 +2654,8 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2328
2654
  if (after) {
2329
2655
  let best = null;
2330
2656
  for (const m of after) {
2331
- if (roleOf(m) !== "user") continue;
2332
- const id = idOf(m);
2657
+ if (roleOf2(m) !== "user") continue;
2658
+ const id = idOf2(m);
2333
2659
  if (typeof id !== "string" || knownUserIds.has(id)) continue;
2334
2660
  if (messageText(m) !== content) continue;
2335
2661
  const created = createdOf(m) ?? 0;
@@ -2351,13 +2677,13 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2351
2677
  function findAssistantReplyAfter(messages, userMessageId) {
2352
2678
  if (!messages || messages.length === 0) return null;
2353
2679
  const byParent = messages.find(
2354
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
2680
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2355
2681
  );
2356
2682
  if (byParent) return byParent;
2357
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
2683
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2358
2684
  if (userIndex === -1) return null;
2359
2685
  for (let i = userIndex + 1; i < messages.length; i++) {
2360
- if (roleOf(messages[i]) === "assistant") return messages[i];
2686
+ if (roleOf2(messages[i]) === "assistant") return messages[i];
2361
2687
  }
2362
2688
  return null;
2363
2689
  }
@@ -2367,7 +2693,7 @@ function findLastAssistantReplyFor(messages, userMessageId) {
2367
2693
  let lastNonErrored = null;
2368
2694
  for (let i = messages.length - 1; i >= 0; i--) {
2369
2695
  const m = messages[i];
2370
- if (roleOf(m) !== "assistant" || parentIdOf(m) !== userMessageId) continue;
2696
+ if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
2371
2697
  if (lastCorrelated === null) lastCorrelated = m;
2372
2698
  if (errorOf(m) == null) {
2373
2699
  lastNonErrored = m;
@@ -2375,12 +2701,12 @@ function findLastAssistantReplyFor(messages, userMessageId) {
2375
2701
  }
2376
2702
  }
2377
2703
  if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
2378
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
2704
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2379
2705
  if (userIndex === -1) return null;
2380
2706
  let last = null;
2381
2707
  let lastOk = null;
2382
2708
  for (let i = userIndex + 1; i < messages.length; i++) {
2383
- const role = roleOf(messages[i]);
2709
+ const role = roleOf2(messages[i]);
2384
2710
  if (role === "user") break;
2385
2711
  if (role === "assistant") {
2386
2712
  last = messages[i];
@@ -2392,16 +2718,16 @@ function findLastAssistantReplyFor(messages, userMessageId) {
2392
2718
  function collectSubagentSessions(messages, userMessageId) {
2393
2719
  if (!messages || messages.length === 0) return [];
2394
2720
  const byParent = messages.filter(
2395
- (message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
2721
+ (message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
2396
2722
  );
2397
2723
  const assistants = byParent.length > 0 ? byParent : [];
2398
2724
  if (assistants.length === 0) {
2399
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
2725
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2400
2726
  if (userIndex === -1) return [];
2401
2727
  for (let i = userIndex + 1; i < messages.length; i++) {
2402
2728
  const message = messages[i];
2403
- if (roleOf(message) === "user") break;
2404
- if (roleOf(message) === "assistant") assistants.push(message);
2729
+ if (roleOf2(message) === "user") break;
2730
+ if (roleOf2(message) === "assistant") assistants.push(message);
2405
2731
  }
2406
2732
  }
2407
2733
  const refs = [];
@@ -2441,7 +2767,7 @@ function collectTaskCalls(messages, userMessageId) {
2441
2767
  if (!messages || messages.length === 0) return [];
2442
2768
  const calls = [];
2443
2769
  for (const message of messages) {
2444
- if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
2770
+ if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
2445
2771
  for (const part of message.parts ?? []) {
2446
2772
  if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
2447
2773
  continue;
@@ -2471,7 +2797,7 @@ function attributeTaskCallUsage(messages, windows) {
2471
2797
  for (const window of eligibleWindows) assignments.set(window.callID, []);
2472
2798
  const unattributed = [];
2473
2799
  for (const message of messages ?? []) {
2474
- if (roleOf(message) !== "assistant") continue;
2800
+ if (roleOf2(message) !== "assistant") continue;
2475
2801
  const created = finiteNumber(createdOf(message));
2476
2802
  const matching = created === null ? [] : eligibleWindows.filter(
2477
2803
  (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
@@ -2548,7 +2874,7 @@ function sumAssistantUsage(messages) {
2548
2874
  function messageUsage(messages, userMessageId) {
2549
2875
  if (!messages || messages.length === 0) return null;
2550
2876
  const byParentAll = messages.filter(
2551
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
2877
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2552
2878
  );
2553
2879
  const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
2554
2880
  const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
@@ -2564,7 +2890,7 @@ function messageUsage(messages, userMessageId) {
2564
2890
  }
2565
2891
  function messageRunState(messages, userMessageId) {
2566
2892
  if (!messages || messages.length === 0) return "unknown";
2567
- const hasUser = messages.some((m) => idOf(m) === userMessageId);
2893
+ const hasUser = messages.some((m) => idOf2(m) === userMessageId);
2568
2894
  const reply = findLastAssistantReplyFor(messages, userMessageId);
2569
2895
  if (!hasUser) {
2570
2896
  if (!reply) return "unknown";
@@ -2656,7 +2982,7 @@ function findLatestSubagentAuthOutcome(messages, sinceMs) {
2656
2982
  if (!messages || messages.length === 0) return null;
2657
2983
  for (let i = messages.length - 1; i >= 0; i--) {
2658
2984
  const message = messages[i];
2659
- if (roleOf(message) !== "assistant") continue;
2985
+ if (roleOf2(message) !== "assistant") continue;
2660
2986
  const created = createdOf(message);
2661
2987
  if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
2662
2988
  const failure = classifyReplyAuthError(message);
@@ -2688,25 +3014,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
2688
3014
  function hasRunningAssistantExcept(messages, exceptUserMessageId) {
2689
3015
  if (!messages || messages.length === 0) return false;
2690
3016
  return messages.some(
2691
- (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3017
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
2692
3018
  );
2693
3019
  }
2694
3020
  function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
2695
3021
  if (!messages || messages.length === 0) return false;
2696
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3022
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2697
3023
  if (userIndex === -1) return false;
2698
3024
  let hasLaterUser = false;
2699
3025
  let hasStartedLaterUser = false;
2700
3026
  for (let i = userIndex + 1; i < messages.length; i++) {
2701
3027
  const message = messages[i];
2702
- if (roleOf(message) !== "user") continue;
3028
+ if (roleOf2(message) !== "user") continue;
2703
3029
  hasLaterUser = true;
2704
- const laterUserMessageId = idOf(message);
3030
+ const laterUserMessageId = idOf2(message);
2705
3031
  if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
2706
3032
  return false;
2707
3033
  }
2708
3034
  if (messages.some(
2709
- (candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
3035
+ (candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
2710
3036
  )) {
2711
3037
  hasStartedLaterUser = true;
2712
3038
  }
@@ -3177,13 +3503,17 @@ async function listV2Sessions(client) {
3177
3503
  }
3178
3504
  async function deleteV2Session(client, sessionId) {
3179
3505
  try {
3180
- await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3181
- 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 };
3182
3512
  } catch (error2) {
3183
3513
  console.error(
3184
3514
  `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3185
3515
  );
3186
- return false;
3516
+ return { ok: false, status: "threw" };
3187
3517
  }
3188
3518
  }
3189
3519
  async function v2SessionExists(client, sessionId) {
@@ -3805,9 +4135,9 @@ async function importOpenCodeV2Credentials({
3805
4135
 
3806
4136
  // src/lib/opencode/session-db-boot.ts
3807
4137
  import { spawn as spawn2 } from "node:child_process";
3808
- 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";
3809
4139
  import { homedir as homedir4 } from "node:os";
3810
- import { dirname as dirname2, resolve as resolvePath } from "node:path";
4140
+ import { dirname as dirname3, resolve as resolvePath } from "node:path";
3811
4141
 
3812
4142
  // src/lib/runner-synchroniser.ts
3813
4143
  import { spawn } from "node:child_process";
@@ -3932,7 +4262,7 @@ function clearMarker(options) {
3932
4262
  function markNoReplicate(options, message) {
3933
4263
  if (options.noReplicateMarker) {
3934
4264
  try {
3935
- mkdirSync(dirname2(options.noReplicateMarker), { recursive: true });
4265
+ mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
3936
4266
  writeFileSync(options.noReplicateMarker, "");
3937
4267
  } catch (error2) {
3938
4268
  options.log(
@@ -4057,7 +4387,7 @@ async function ensureLitestreamConfig(options, env) {
4057
4387
  return null;
4058
4388
  }
4059
4389
  try {
4060
- if (statSync2(configPath).size > 0) return configPath;
4390
+ if (statSync3(configPath).size > 0) return configPath;
4061
4391
  } catch (error2) {
4062
4392
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
4063
4393
  options.log(
@@ -4087,7 +4417,7 @@ async function ensureLitestreamConfig(options, env) {
4087
4417
  return null;
4088
4418
  }
4089
4419
  try {
4090
- mkdirSync(dirname2(configPath), { recursive: true });
4420
+ mkdirSync(dirname3(configPath), { recursive: true });
4091
4421
  writeFileSync(configPath, rendered.stdout);
4092
4422
  } catch (error2) {
4093
4423
  options.log(
@@ -4248,7 +4578,7 @@ async function verifySessionDb(options, configPath, env) {
4248
4578
  }
4249
4579
  function fileExists(path) {
4250
4580
  try {
4251
- statSync2(path);
4581
+ statSync3(path);
4252
4582
  return true;
4253
4583
  } catch (error2) {
4254
4584
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
@@ -4337,7 +4667,7 @@ async function restoreAndVerifySessionDb(options) {
4337
4667
  // src/lib/opencode/session-db-provenance.ts
4338
4668
  import { createRequire } from "node:module";
4339
4669
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
4340
- import { dirname as dirname3, join as join5 } from "node:path";
4670
+ import { dirname as dirname4, join as join5 } from "node:path";
4341
4671
  var require2 = createRequire(import.meta.url);
4342
4672
  function readSessionDbMigrationIds(dbPath) {
4343
4673
  let db;
@@ -4403,7 +4733,7 @@ function loadSessionDbProvenanceState(path) {
4403
4733
  }
4404
4734
  function saveSessionDbProvenanceState(path, state) {
4405
4735
  try {
4406
- mkdirSync2(dirname3(path), { recursive: true });
4736
+ mkdirSync2(dirname4(path), { recursive: true });
4407
4737
  writeFileSync2(path, `${JSON.stringify(state, null, 2)}
4408
4738
  `, "utf8");
4409
4739
  } catch (error2) {
@@ -4984,7 +5314,10 @@ function selectSessionsToDelete(sessions, opts) {
4984
5314
  toDelete.push(s.id);
4985
5315
  }
4986
5316
  }
4987
- 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
+ );
4988
5321
  }
4989
5322
  var DEFAULT_INTERVAL = "1h";
4990
5323
  function resolve(flag, envValue, fallback) {
@@ -5044,13 +5377,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
5044
5377
  }
5045
5378
 
5046
5379
  // src/lib/opencode/session-db-size.ts
5047
- import { statSync as statSync3 } from "node:fs";
5380
+ import { statSync as statSync4 } from "node:fs";
5048
5381
  import { join as join6 } from "node:path";
5049
5382
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
5050
5383
  function statSessionDbBytes(homeDir) {
5051
5384
  const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
5052
5385
  try {
5053
- return statSync3(dbPath).size;
5386
+ return statSync4(dbPath).size;
5054
5387
  } catch (err) {
5055
5388
  const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
5056
5389
  if (!isMissingFile) {
@@ -5076,7 +5409,7 @@ function buildSessionStoreSizeWarning(input) {
5076
5409
  }
5077
5410
 
5078
5411
  // src/lib/opencode/log-tail.ts
5079
- import { statSync as statSync4 } from "node:fs";
5412
+ import { statSync as statSync5 } from "node:fs";
5080
5413
  import { homedir as homedir5 } from "node:os";
5081
5414
  import { join as join7 } from "node:path";
5082
5415
  import { open as open2, stat } from "node:fs/promises";
@@ -5099,7 +5432,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
5099
5432
  let inode = null;
5100
5433
  let baselineReady = true;
5101
5434
  try {
5102
- const initial = statSync4(logPath);
5435
+ const initial = statSync5(logPath);
5103
5436
  offset = initial.size;
5104
5437
  inode = initial.ino;
5105
5438
  } catch (error2) {
@@ -5158,129 +5491,6 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
5158
5491
  };
5159
5492
  }
5160
5493
 
5161
- // src/lib/opencode/session-db-reclaim.ts
5162
- import { statSync as statSync5, statfsSync } from "node:fs";
5163
- import { dirname as dirname4 } from "node:path";
5164
- function errorMessage(error2) {
5165
- if (!(error2 instanceof Error)) return String(error2);
5166
- return error2.cause instanceof Error ? error2.cause.message : error2.message;
5167
- }
5168
- function insufficientSpaceReason(dbPath, requiredBytes) {
5169
- try {
5170
- const fsStats = statfsSync(dirname4(dbPath));
5171
- const availableBytes = fsStats.bavail * fsStats.bsize;
5172
- if (availableBytes < requiredBytes) {
5173
- return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
5174
- }
5175
- return null;
5176
- } catch (err) {
5177
- return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
5178
- }
5179
- }
5180
- function readLogicalBytes(db) {
5181
- const pageCount = db.prepare("PRAGMA page_count").get().page_count;
5182
- const pageSize = db.prepare("PRAGMA page_size").get().page_size;
5183
- return pageCount * pageSize;
5184
- }
5185
- function readCheckpointResult(db) {
5186
- const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
5187
- return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
5188
- }
5189
- async function probeReclaimAvailability(input) {
5190
- const { dbPath, requiredBytes } = input;
5191
- let sqlite;
5192
- try {
5193
- sqlite = await import("node:sqlite");
5194
- } catch (err) {
5195
- const detail = `Node ${process.version}: ${errorMessage(err)}`;
5196
- console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
5197
- return { reason: "sqlite-unavailable", detail };
5198
- }
5199
- let autoVacuum = null;
5200
- try {
5201
- const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
5202
- try {
5203
- db.exec("PRAGMA busy_timeout=5000");
5204
- autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
5205
- } finally {
5206
- db.close();
5207
- }
5208
- } catch (err) {
5209
- console.warn(
5210
- `[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
5211
- );
5212
- }
5213
- if (autoVacuum !== 0) return null;
5214
- return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
5215
- }
5216
- async function reclaimSessionDbSpace(input) {
5217
- const { dbPath, maxPages, allowFullVacuum = true } = input;
5218
- let sqlite;
5219
- try {
5220
- sqlite = await import("node:sqlite");
5221
- } catch (err) {
5222
- const detail = `Node ${process.version}: ${errorMessage(err)}`;
5223
- console.warn(
5224
- `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
5225
- );
5226
- return { ok: false, skipped: "sqlite-unavailable", detail };
5227
- }
5228
- const { DatabaseSync } = sqlite;
5229
- let db;
5230
- try {
5231
- db = new DatabaseSync(dbPath);
5232
- db.exec("PRAGMA busy_timeout=5000");
5233
- const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
5234
- if (autoVacuum === 0) {
5235
- if (!allowFullVacuum) {
5236
- console.warn(
5237
- `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
5238
- );
5239
- return {
5240
- ok: false,
5241
- skipped: "full-vacuum-blocked",
5242
- detail: "a session turn is live"
5243
- };
5244
- }
5245
- const fileBytesForGuard = statSync5(dbPath).size;
5246
- const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
5247
- if (skipReason !== null) {
5248
- console.warn(
5249
- `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
5250
- );
5251
- return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
5252
- }
5253
- const beforeBytes = readLogicalBytes(db);
5254
- db.exec("PRAGMA auto_vacuum=INCREMENTAL");
5255
- db.exec("VACUUM");
5256
- const afterBytes = readLogicalBytes(db);
5257
- const checkpoint = readCheckpointResult(db);
5258
- return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
5259
- }
5260
- if (autoVacuum === 2) {
5261
- const beforeBytes = readLogicalBytes(db);
5262
- const bound = Math.max(0, Math.trunc(maxPages));
5263
- db.exec(`PRAGMA incremental_vacuum(${bound})`);
5264
- const afterBytes = readLogicalBytes(db);
5265
- const checkpoint = readCheckpointResult(db);
5266
- return { ok: true, mode: "incremental", beforeBytes, afterBytes, checkpoint };
5267
- }
5268
- console.warn(
5269
- `[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
5270
- );
5271
- return { ok: false, skipped: "auto-vacuum-not-applicable" };
5272
- } catch (err) {
5273
- console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
5274
- return {
5275
- ok: false,
5276
- skipped: "reclaim-error",
5277
- detail: errorMessage(err)
5278
- };
5279
- } finally {
5280
- db?.close();
5281
- }
5282
- }
5283
-
5284
5494
  // src/lib/tunnel/connection.ts
5285
5495
  import WebSocket2 from "ws";
5286
5496
 
@@ -5510,6 +5720,8 @@ var StreamForwarder = class {
5510
5720
  if (done) break;
5511
5721
  const chunk = Buffer.from(value);
5512
5722
  for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
5723
+ await stream.pausePromise;
5724
+ if (ac.signal.aborted) break;
5513
5725
  const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
5514
5726
  if (this.binaryFramesSupported) {
5515
5727
  if (this.ws.readyState === WebSocket.OPEN) {
@@ -5552,6 +5764,7 @@ function classifyUpgradeRejection(headers) {
5552
5764
  }
5553
5765
  var MAX_RECONNECT_DELAY = 3e4;
5554
5766
  var BASE_RECONNECT_DELAY = 500;
5767
+ var RELAY_SILENCE_TIMEOUT_MS = 9e4;
5555
5768
  function getReconnectDelay(attempt) {
5556
5769
  const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
5557
5770
  const jitter = Math.random() * 1e3;
@@ -5623,10 +5836,27 @@ function connectTunnel(options) {
5623
5836
  ws.close();
5624
5837
  reject(new Error("Connection timeout"));
5625
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
+ };
5626
5855
  let upgradeRejection = null;
5627
5856
  let upgradeRejectionReason = null;
5628
5857
  ws.on("unexpected-response", (_req, res) => {
5629
5858
  clearTimeout(connectionTimeout);
5859
+ clearRelaySilenceTimeout();
5630
5860
  const reason = classifyUpgradeRejection(res.headers);
5631
5861
  upgradeRejectionReason = reason;
5632
5862
  const chunks = [];
@@ -5656,6 +5886,7 @@ function connectTunnel(options) {
5656
5886
  onInfo?.("WebSocket connection established");
5657
5887
  });
5658
5888
  ws.on("message", (data, isBinary) => {
5889
+ if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
5659
5890
  if (isBinary) {
5660
5891
  try {
5661
5892
  const frame = decodeBinaryBodyFrame(toUint8Array(data));
@@ -5689,6 +5920,7 @@ function connectTunnel(options) {
5689
5920
  switch (message.type) {
5690
5921
  case "connected": {
5691
5922
  clearTimeout(connectionTimeout);
5923
+ armRelaySilenceTimeout();
5692
5924
  forwarder.setBinaryFramesSupported(message.binary_frames === true);
5693
5925
  const connectedAgentId = message.agent_id ?? agentId;
5694
5926
  onConnected?.(connectedAgentId);
@@ -5713,6 +5945,7 @@ function connectTunnel(options) {
5713
5945
  });
5714
5946
  ws.on("error", (error2) => {
5715
5947
  clearTimeout(connectionTimeout);
5948
+ clearRelaySilenceTimeout();
5716
5949
  const detail = upgradeRejection ?? describeSocketError(error2, url);
5717
5950
  if (upgradeRejectionReason === "do_code_updated") {
5718
5951
  onWarning?.("Relay redeployed \u2014 reconnecting");
@@ -5724,7 +5957,8 @@ function connectTunnel(options) {
5724
5957
  );
5725
5958
  });
5726
5959
  ws.on("close", (code, reason) => {
5727
- 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");
5728
5962
  forwarder.abortAll();
5729
5963
  onDisconnected?.(code, reasonStr);
5730
5964
  });
@@ -5732,7 +5966,7 @@ function connectTunnel(options) {
5732
5966
  }
5733
5967
 
5734
5968
  // src/lib/tunnel/runner-connection.ts
5735
- var RunnerConnection = class {
5969
+ var RunnerConnection = class _RunnerConnection {
5736
5970
  opts;
5737
5971
  sleep;
5738
5972
  now;
@@ -5746,6 +5980,10 @@ var RunnerConnection = class {
5746
5980
  reconnectAttempt = 0;
5747
5981
  generation = 0;
5748
5982
  disconnectedAtMs = null;
5983
+ static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
5984
+ "No pong received",
5985
+ "Orphaned socket"
5986
+ ]);
5749
5987
  constructor(opts) {
5750
5988
  this.opts = opts;
5751
5989
  this.resolvedAgentId = opts.agentId;
@@ -5802,7 +6040,8 @@ var RunnerConnection = class {
5802
6040
  onDisconnected: (code, reason) => {
5803
6041
  this.disconnectedAtMs = this.now();
5804
6042
  events.onDisconnected(code, reason, { generation: this.generation });
5805
- if (this.opts.isRunning() && code !== 1e3 && !this.reconnecting) {
6043
+ const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
6044
+ if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
5806
6045
  this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
5807
6046
  events.onError?.(`Reconnection failed: ${err.message}`);
5808
6047
  });
@@ -6141,6 +6380,83 @@ function createResourceUsageCollector(homeDir) {
6141
6380
  };
6142
6381
  }
6143
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
+
6144
6460
  // src/lib/channels/driver.ts
6145
6461
  import { homedir as homedir6 } from "node:os";
6146
6462
 
@@ -6599,6 +6915,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
6599
6915
  var MAX_BUFFERED_SESSION_ERRORS = 256;
6600
6916
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
6601
6917
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
6918
+ var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
6602
6919
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
6603
6920
  var HEARTBEAT_MS = 6e4;
6604
6921
  var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
@@ -6660,6 +6977,7 @@ var ChannelDriver = class _ChannelDriver {
6660
6977
  maxActiveSessions;
6661
6978
  watcherStallMs;
6662
6979
  wedgeWarningIntervalMs;
6980
+ sessionStoreHealth;
6663
6981
  openCodeClient;
6664
6982
  /** Cache of conversationId → opencode sessionId. */
6665
6983
  sessions = /* @__PURE__ */ new Map();
@@ -7029,6 +7347,7 @@ var ChannelDriver = class _ChannelDriver {
7029
7347
  this.maxActiveSessions = config.maxActiveSessions;
7030
7348
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
7031
7349
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
7350
+ this.sessionStoreHealth = config.sessionStoreHealth;
7032
7351
  }
7033
7352
  get isV2() {
7034
7353
  return this.openCodeClient.version === "v2";
@@ -7044,7 +7363,9 @@ var ChannelDriver = class _ChannelDriver {
7044
7363
  return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
7045
7364
  }
7046
7365
  async listSessions() {
7047
- 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
+ });
7048
7369
  }
7049
7370
  async sessionExists(sessionId) {
7050
7371
  return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
@@ -8498,8 +8819,10 @@ var ChannelDriver = class _ChannelDriver {
8498
8819
  b2LastDescendantCheckMs: 0,
8499
8820
  b2RootOngoingHeldLogged: false,
8500
8821
  b2AbandonedSignalled: false,
8822
+ b2AbandonedWithoutAnswer: false,
8501
8823
  ambiguousPinnedSinceMs: 0,
8502
8824
  ambiguousResolved: false,
8825
+ sessionStoreFailurePending: false,
8503
8826
  neverStartedResolved: false
8504
8827
  });
8505
8828
  const buffered = this.bufferedSessionErrors.get(sessionId);
@@ -8616,8 +8939,10 @@ var ChannelDriver = class _ChannelDriver {
8616
8939
  b2LastDescendantCheckMs: 0,
8617
8940
  b2RootOngoingHeldLogged: false,
8618
8941
  b2AbandonedSignalled: false,
8942
+ b2AbandonedWithoutAnswer: false,
8619
8943
  ambiguousPinnedSinceMs: 0,
8620
8944
  ambiguousResolved: false,
8945
+ sessionStoreFailurePending: false,
8621
8946
  neverStartedResolved: false
8622
8947
  });
8623
8948
  }
@@ -9031,6 +9356,10 @@ var ChannelDriver = class _ChannelDriver {
9031
9356
  if (state === "running" || state === "done" || state === "failed") {
9032
9357
  this.neverStartedAttempts.delete(id);
9033
9358
  }
9359
+ if (inFlight.sessionStoreFailurePending) {
9360
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
9361
+ return;
9362
+ }
9034
9363
  if (inFlight.neverStartedResolved) {
9035
9364
  if (state === "running" || state === "done" || state === "failed") {
9036
9365
  inFlight.neverStartedResolved = false;
@@ -9190,10 +9519,22 @@ var ChannelDriver = class _ChannelDriver {
9190
9519
  inFlight.b2LastDescendantCheckMs = 0;
9191
9520
  inFlight.b2RootOngoingHeldLogged = false;
9192
9521
  inFlight.b2AbandonedSignalled = false;
9522
+ inFlight.b2AbandonedWithoutAnswer = false;
9193
9523
  }
9194
9524
  } else {
9195
9525
  if (inFlight.b2AbandonedSignalled) {
9196
- 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
+ }
9197
9538
  return;
9198
9539
  }
9199
9540
  if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
@@ -9210,17 +9551,47 @@ var ChannelDriver = class _ChannelDriver {
9210
9551
  descendantOngoing,
9211
9552
  rootOngoing
9212
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
+ }
9213
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
+ }
9214
9586
  this.log({
9215
9587
  level: "warn",
9216
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`,
9217
9589
  conversation_id: conv.id,
9218
9590
  message_id: id
9219
9591
  });
9220
- const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
9221
9592
  void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
9222
9593
  watched_for_ms: pinnedForMs,
9223
- finish: reply?.info?.finish ?? reply?.finish,
9594
+ finish,
9224
9595
  ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
9225
9596
  ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
9226
9597
  opencode_message_id: inFlight.opencodeMessageId
@@ -9268,6 +9639,16 @@ var ChannelDriver = class _ChannelDriver {
9268
9639
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
9269
9640
  sessionOngoing: ongoing
9270
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
+ }
9271
9652
  inFlight.ambiguousResolved = true;
9272
9653
  this.log({
9273
9654
  level: "warn",
@@ -9492,6 +9873,74 @@ var ChannelDriver = class _ChannelDriver {
9492
9873
  }
9493
9874
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9494
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
+ }
9495
9944
  /**
9496
9945
  * Report a queued message as failed after the never-started bound. The accepted
9497
9946
  * OpenCode copy is not cancelled, so the local fence remains on unsuccessful
@@ -9547,6 +9996,66 @@ var ChannelDriver = class _ChannelDriver {
9547
9996
  this.neverStartedAttempts.delete(inFlight.evidentMessageId);
9548
9997
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9549
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
+ }
9550
10059
  // Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
9551
10060
  /**
9552
10061
  * Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
@@ -12751,9 +13260,9 @@ async function driveChannels(state, driver) {
12751
13260
  }
12752
13261
  }
12753
13262
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
12754
- var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
13263
+ var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
12755
13264
  function shouldWarnForReclaimSkip(reason) {
12756
- if (reason !== "sqlite-unavailable") return false;
13265
+ if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
12757
13266
  const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
12758
13267
  if (!version2) return false;
12759
13268
  const major = Number(version2[1]);
@@ -12810,61 +13319,96 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
12810
13319
  async function runSweep(state, driver, config) {
12811
13320
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
12812
13321
  try {
12813
- const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
12814
- if (sessions === null) {
12815
- 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, {
12816
13327
  type: "info",
12817
- message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`,
12818
- metadata: {
12819
- outcome: "sweep-skipped",
12820
- reason: "opencode-unreachable"
12821
- }
13328
+ level: "warn",
13329
+ message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
12822
13330
  });
12823
- return;
12824
13331
  }
12825
- const toDelete = selectSessionsToDelete(
12826
- sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
12827
- {
12828
- maxAgeMs: config.maxAgeMs,
12829
- maxCount: config.maxCount,
12830
- nowMs: Date.now(),
12831
- protectedIds: driver.protectedSessionIds()
12832
- }
12833
- );
12834
- const protectedNow = driver.protectedSessionIds();
12835
13332
  let deleted = 0;
12836
13333
  let failed = 0;
13334
+ let failed4xx = 0;
13335
+ let failed5xx = 0;
13336
+ let failedOtherStatus = 0;
13337
+ let failedThrew = 0;
12837
13338
  let skippedNewlyActive = 0;
12838
- for (const id of toDelete) {
12839
- if (protectedNow.has(id)) {
12840
- skippedNewlyActive++;
12841
- logActivity(state, {
12842
- type: "info",
12843
- message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
12844
- });
12845
- 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
+ }
12846
13376
  }
12847
- if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
12848
- 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
+ });
12849
13384
  }
12850
- const failedNote = failed > 0 ? `, failed ${failed}` : "";
12851
- const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
12852
- logActivity(state, {
12853
- type: "info",
12854
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`
12855
- });
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
+ };
12856
13397
  const reclaimResult = await reclaimSessionDbSpace({
12857
13398
  dbPath: sessionDbPath(),
12858
- 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,
12859
13402
  allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
12860
13403
  });
12861
13404
  if (reclaimResult.ok) {
13405
+ state.sessionStoreHealth.observeWritable();
12862
13406
  const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
12863
13407
  const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
12864
13408
  const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
12865
13409
  logSessionCleanupActivity(state, "sweep", {
12866
13410
  type: "info",
12867
- 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,
12868
13412
  metadata: {
12869
13413
  outcome: "reclaimed",
12870
13414
  mode: reclaimResult.mode,
@@ -12872,25 +13416,27 @@ async function runSweep(state, driver, config) {
12872
13416
  after_bytes: reclaimResult.afterBytes,
12873
13417
  checkpoint_busy: reclaimResult.checkpoint.busy,
12874
13418
  wal_frames_pending: reclaimResult.checkpoint.log,
12875
- sessions_inspected: sessions.length,
12876
- sessions_deleted: deleted,
12877
- sessions_failed: failed,
12878
- 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
12879
13425
  }
12880
13426
  });
12881
13427
  } else {
13428
+ if (reclaimResult.skipped === "disk-exhaustion") {
13429
+ state.sessionStoreHealth.observeUnwritable();
13430
+ }
12882
13431
  logSessionCleanupActivity(state, "sweep", {
12883
13432
  type: "info",
12884
- 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",
12885
13434
  message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
12886
13435
  metadata: {
12887
13436
  outcome: "reclaim-skipped",
12888
13437
  reason: reclaimResult.skipped,
12889
13438
  ...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
12890
- sessions_inspected: sessions.length,
12891
- sessions_deleted: deleted,
12892
- sessions_failed: failed,
12893
- sessions_skipped_active: skippedNewlyActive
13439
+ ...sessionMetadata
12894
13440
  }
12895
13441
  });
12896
13442
  }
@@ -13147,6 +13693,19 @@ function scheduleResourceUsageReporting(state, options) {
13147
13693
  message: `Resource usage collection: ${warning2}`
13148
13694
  });
13149
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
+ }
13150
13709
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
13151
13710
  if (result.ok) {
13152
13711
  if (consecutiveFailures > 0) {
@@ -13393,6 +13952,7 @@ async function run(options) {
13393
13952
  opencodeClient: null,
13394
13953
  opencodeV2CredentialImportGuard: { inFlight: false },
13395
13954
  sessionDbProvenanceAnomaly: false,
13955
+ sessionStoreHealth: createSessionStoreHealth(),
13396
13956
  opencodeProcess: null,
13397
13957
  stopOpenCodeLogTail: null,
13398
13958
  litestreamProcess: null,
@@ -13410,6 +13970,7 @@ async function run(options) {
13410
13970
  openaiUsageRearm: null,
13411
13971
  resourceUsageTimer: null,
13412
13972
  stopResourceUsageSampling: null,
13973
+ lowDiskWarningState: { worstBand: null, warnedAtMs: null },
13413
13974
  credentialSync: null,
13414
13975
  authHeader: ""
13415
13976
  };
@@ -13600,10 +14161,13 @@ async function run(options) {
13600
14161
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13601
14162
  }
13602
14163
  state.credentialSync?.arm();
13603
- const openCodeActivityForwarder = createOpenCodeActivityForwarder(() => ({
13604
- agentId: state.agentId,
13605
- authHeader: state.authHeader
13606
- }));
14164
+ const openCodeActivityForwarder = createOpenCodeActivityForwarder(
14165
+ () => ({
14166
+ agentId: state.agentId,
14167
+ authHeader: state.authHeader
14168
+ }),
14169
+ state.sessionStoreHealth.observeUnwritable
14170
+ );
13607
14171
  const openCodeLogTail = tailOpenCodeLogFile(
13608
14172
  resolveOpenCodeLogPath(homedir7(), process.env),
13609
14173
  openCodeActivityForwarder
@@ -13895,6 +14459,7 @@ async function run(options) {
13895
14459
  getAuthHeader: () => state.authHeader,
13896
14460
  conversationFilter: state.conversationFilter,
13897
14461
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
14462
+ sessionStoreHealth: state.sessionStoreHealth,
13898
14463
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
13899
14464
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
13900
14465
  fileSyncDirectories,