@evident-ai/cli 3.5.2-dev.659ea21 → 3.5.2-dev.678bfa4

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
@@ -315,14 +315,14 @@ function blank() {
315
315
  console.log();
316
316
  }
317
317
  function waitForEnter(prompt = "Press Enter to continue...") {
318
- return new Promise((resolve4) => {
318
+ return new Promise((resolve5) => {
319
319
  process.stdout.write(chalk.dim(prompt));
320
320
  const handler = () => {
321
321
  process.stdin.removeListener("data", handler);
322
322
  process.stdin.setRawMode?.(false);
323
323
  process.stdin.pause();
324
324
  console.log();
325
- resolve4();
325
+ resolve5();
326
326
  };
327
327
  if (process.stdin.isTTY) {
328
328
  process.stdin.setRawMode?.(true);
@@ -332,7 +332,7 @@ function waitForEnter(prompt = "Press Enter to continue...") {
332
332
  });
333
333
  }
334
334
  function sleep(ms) {
335
- return new Promise((resolve4) => setTimeout(resolve4, ms));
335
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
336
336
  }
337
337
 
338
338
  // src/commands/login.ts
@@ -410,19 +410,19 @@ async function tokenLogin() {
410
410
  );
411
411
  blank();
412
412
  process.stdout.write("Paste token: ");
413
- const token = await new Promise((resolve4) => {
413
+ const token = await new Promise((resolve5) => {
414
414
  let data = "";
415
415
  process.stdin.setEncoding("utf8");
416
416
  process.stdin.on("data", (chunk) => {
417
417
  data += chunk;
418
418
  });
419
419
  process.stdin.on("end", () => {
420
- resolve4(data.trim());
420
+ resolve5(data.trim());
421
421
  });
422
422
  if (process.stdin.isTTY) {
423
423
  process.stdin.once("data", (chunk) => {
424
424
  process.stdin.pause();
425
- resolve4(chunk.toString().trim());
425
+ resolve5(chunk.toString().trim());
426
426
  });
427
427
  process.stdin.resume();
428
428
  }
@@ -703,6 +703,31 @@ async function reportResourceUsage(agentId, authHeader, usage) {
703
703
  opencode_db_bytes: usage.opencodeDbBytes
704
704
  });
705
705
  }
706
+ async function getRunnerCredentialVault(agentId, authHeader) {
707
+ const empty = { prefix: null, vaultId: null, source: null };
708
+ try {
709
+ const apiUrl = getApiUrlConfig();
710
+ const response = await fetch(`${apiUrl}/runners/${agentId}/credential-vault`, {
711
+ headers: { Authorization: authHeader },
712
+ signal: AbortSignal.timeout(1e4)
713
+ });
714
+ if (!response.ok) {
715
+ const serverMessage = await readErrorMessage(response);
716
+ return {
717
+ ...empty,
718
+ error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
719
+ };
720
+ }
721
+ const data = await response.json();
722
+ return {
723
+ prefix: data.credential_vault_prefix,
724
+ vaultId: data.vault_id,
725
+ source: data.source
726
+ };
727
+ } catch (error2) {
728
+ return { ...empty, error: describeTimeoutError(error2, 1e4) };
729
+ }
730
+ }
706
731
  async function getAgentInfo(agentId, authHeader) {
707
732
  const apiUrl = getApiUrlConfig();
708
733
  try {
@@ -877,6 +902,76 @@ async function status(options = {}) {
877
902
  process.exit(result.exitCode);
878
903
  }
879
904
 
905
+ // src/commands/credential-vault-env.ts
906
+ function shellQuote(value) {
907
+ return `'${value.replaceAll("'", "'\\''")}'`;
908
+ }
909
+ function printAssignment(prefix) {
910
+ console.log(`CREDENTIAL_VAULT_PREFIX=${shellQuote(prefix ?? "")}`);
911
+ }
912
+ function safeReason(reason, credentials2, authHeader) {
913
+ let safe = reason;
914
+ for (const secret of [
915
+ authHeader,
916
+ credentials2?.token,
917
+ process.env.EVIDENT_RUNNER_KEY,
918
+ process.env.EVIDENT_AGENT_KEY,
919
+ process.env.EVIDENT_TOKEN
920
+ ]) {
921
+ if (secret) safe = safe.replaceAll(secret, "[redacted]");
922
+ }
923
+ return safe.replace(/[\r\n]+/g, " ");
924
+ }
925
+ function printFallback(reason, credentials2, authHeader) {
926
+ printAssignment(null);
927
+ console.error(
928
+ `WARNING: ${safeReason(reason, credentials2, authHeader)}; using this runner's own credential prefix.`
929
+ );
930
+ }
931
+ async function credentialVaultEnv() {
932
+ let credentials2 = null;
933
+ let authHeader;
934
+ try {
935
+ credentials2 = await getAuthCredentials();
936
+ if (!credentials2) {
937
+ printFallback("no runner credentials are configured");
938
+ return;
939
+ }
940
+ authHeader = getAuthHeader(credentials2);
941
+ const runner = await resolveAgentIdFromKey(authHeader);
942
+ if (!runner.agent_id) {
943
+ printFallback(
944
+ runner.error ?? "the runner identity could not be resolved",
945
+ credentials2,
946
+ authHeader
947
+ );
948
+ return;
949
+ }
950
+ const lookup = await getRunnerCredentialVault(runner.agent_id, authHeader);
951
+ if (lookup.error) {
952
+ printFallback(`credential vault lookup failed: ${lookup.error}`, credentials2, authHeader);
953
+ return;
954
+ }
955
+ if (lookup.prefix === null) {
956
+ printFallback("this runner is not linked to a credential vault", credentials2, authHeader);
957
+ return;
958
+ }
959
+ if (/[\r\n]/.test(lookup.prefix)) {
960
+ printFallback("credential vault lookup returned an invalid prefix", credentials2, authHeader);
961
+ return;
962
+ }
963
+ printAssignment(lookup.prefix);
964
+ console.error(
965
+ `VAULT-MODE: credential stores use vault=${lookup.vaultId} source=${lookup.source} prefix=${lookup.prefix}.`
966
+ );
967
+ } catch (error2) {
968
+ const detail = credentials2 === null ? "credential resolution failed" : `credential vault lookup could not be completed: ${error2 instanceof Error ? error2.message : String(error2)}`;
969
+ printFallback(detail, credentials2, authHeader);
970
+ } finally {
971
+ process.exitCode = 0;
972
+ }
973
+ }
974
+
880
975
  // src/lib/claude-usage.ts
881
976
  import { execFileSync } from "node:child_process";
882
977
  import { readFileSync } from "node:fs";
@@ -1063,12 +1158,82 @@ async function claudeUsage() {
1063
1158
  // src/commands/run.ts
1064
1159
  import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
1065
1160
  import { homedir as homedir7 } from "node:os";
1066
- import { isAbsolute as isAbsolute3, join as join11, parse, resolve as resolvePath2 } from "node:path";
1161
+ import { isAbsolute as isAbsolute4, join as join12, parse, resolve as resolvePath2 } from "node:path";
1067
1162
  import chalk7 from "chalk";
1068
1163
 
1069
1164
  // ../../packages/types/src/agents/index.ts
1070
1165
  var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
1071
1166
 
1167
+ // ../../packages/types/src/opencode/index.ts
1168
+ function roleOf(m) {
1169
+ if (!m || typeof m !== "object") return void 0;
1170
+ if (typeof m.role === "string") return m.role;
1171
+ const infoRole = m.info?.role;
1172
+ return typeof infoRole === "string" ? infoRole : void 0;
1173
+ }
1174
+ function idOf(m) {
1175
+ if (!m || typeof m !== "object") return void 0;
1176
+ if (typeof m.id === "string") return m.id;
1177
+ const infoId = m.info?.id;
1178
+ return typeof infoId === "string" ? infoId : void 0;
1179
+ }
1180
+ function parentIdOf(m) {
1181
+ if (!m || typeof m !== "object") return void 0;
1182
+ if (typeof m.parentID === "string") return m.parentID;
1183
+ const infoParent = m.info?.parentID;
1184
+ return typeof infoParent === "string" ? infoParent : void 0;
1185
+ }
1186
+ function textPartsOf(m) {
1187
+ if (!m || typeof m !== "object") return [];
1188
+ const parts = Array.isArray(m.parts) ? m.parts : [];
1189
+ return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text ?? "").filter(Boolean);
1190
+ }
1191
+ function correlatedAssistantRun(messages, userMessageId) {
1192
+ if (!messages || messages.length === 0) return [];
1193
+ const byParent = messages.filter(
1194
+ (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
1195
+ );
1196
+ if (byParent.length > 0) return byParent;
1197
+ const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
1198
+ if (userIndex === -1) return [];
1199
+ const run2 = [];
1200
+ for (let i = userIndex + 1; i < messages.length; i++) {
1201
+ const role = roleOf(messages[i]);
1202
+ if (role === "user") break;
1203
+ if (role === "assistant") run2.push(messages[i]);
1204
+ }
1205
+ return run2;
1206
+ }
1207
+ function lastText(assistantRun) {
1208
+ for (let i = assistantRun.length - 1; i >= 0; i--) {
1209
+ const textParts = textPartsOf(assistantRun[i]);
1210
+ if (textParts.length > 0) return textParts.join("\n").trim() || null;
1211
+ }
1212
+ return null;
1213
+ }
1214
+ function effectiveCorrelatedRun(messages, userMessageId) {
1215
+ const run2 = correlatedAssistantRun(messages, userMessageId);
1216
+ if (lastText(run2) !== null) return run2;
1217
+ const userIndex = messages?.findIndex((m) => idOf(m) === userMessageId) ?? -1;
1218
+ if (userIndex === -1) return run2;
1219
+ let continuation;
1220
+ for (let i = userIndex + 1; messages && i < messages.length; i++) {
1221
+ if (roleOf(messages[i]) === "user") {
1222
+ continuation = messages[i];
1223
+ break;
1224
+ }
1225
+ }
1226
+ const continuationId = idOf(continuation);
1227
+ if (roleOf(continuation) === "user" && textPartsOf(continuation).join("\n").trim().toLowerCase() === "continue" && continuationId) {
1228
+ const continuationRun = correlatedAssistantRun(messages, continuationId);
1229
+ if (continuationRun.length > 0) return continuationRun;
1230
+ }
1231
+ return run2;
1232
+ }
1233
+ function extractReplyTextForMessage(messages, opencodeUserMessageId) {
1234
+ return lastText(effectiveCorrelatedRun(messages, opencodeUserMessageId));
1235
+ }
1236
+
1072
1237
  // ../../packages/types/src/telemetry/index.ts
1073
1238
  var TelemetryEventTypes = {
1074
1239
  // Agent activity events (shown in web UI activity log)
@@ -1157,7 +1322,7 @@ import ora4 from "ora";
1157
1322
  import { select as select4 } from "@inquirer/prompts";
1158
1323
 
1159
1324
  // src/lib/telemetry.ts
1160
- var CLI_VERSION = (true ? "3.5.2-dev.659ea21" : void 0) ?? process.env.npm_package_version ?? "unknown";
1325
+ var CLI_VERSION = (true ? "3.5.2-dev.678bfa4" : void 0) ?? process.env.npm_package_version ?? "unknown";
1161
1326
  function getCliVersion() {
1162
1327
  return CLI_VERSION;
1163
1328
  }
@@ -1318,8 +1483,591 @@ var EventTypes = {
1318
1483
  DEPRECATED_AGENT_KEY_ENV_USED: "cli.deprecated_agent_key_env_used"
1319
1484
  };
1320
1485
 
1321
- // src/lib/runner-activity-telemetry.ts
1486
+ // src/lib/opencode/session-db-reclaim.ts
1487
+ import { statSync as statSync2, statfsSync } from "node:fs";
1488
+ import { dirname as dirname2 } from "node:path";
1489
+
1490
+ // src/lib/diagnostic-redaction.ts
1322
1491
  import { homedir as homedir2 } from "node:os";
1492
+ function redactHomeDirectory(message) {
1493
+ const home = homedir2();
1494
+ if (!home || home === "/") {
1495
+ return message;
1496
+ }
1497
+ const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
1498
+ let result = "";
1499
+ let searchFrom = 0;
1500
+ let homeIndex;
1501
+ while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
1502
+ const homeEnd = homeIndex + home.length;
1503
+ const previousCharacter = message[homeIndex - 1];
1504
+ const nextCharacter = message[homeEnd];
1505
+ const atPathBoundary = (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
1506
+ result += message.slice(searchFrom, homeIndex);
1507
+ result += atPathBoundary ? "~" : home;
1508
+ searchFrom = homeEnd;
1509
+ }
1510
+ return result + message.slice(searchFrom);
1511
+ }
1512
+ function redactDiagnosticText(message) {
1513
+ 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>");
1514
+ return redactHomeDirectory(redacted);
1515
+ }
1516
+
1517
+ // src/lib/opencode/session-db-reclaim.ts
1518
+ var SESSION_DB_RECLAIM_CHUNK_PAGES = 100;
1519
+ var SESSION_DB_RECLAIM_TIME_BUDGET_MS = 500;
1520
+ var SESSION_DB_RECLAIM_PAGE_CEILING = 4e4;
1521
+ var SESSION_DB_RECLAIM_BUSY_TIMEOUT_MS = 5e3;
1522
+ function errorMessage(error2) {
1523
+ if (!(error2 instanceof Error)) return String(error2);
1524
+ return error2.cause instanceof Error ? error2.cause.message : error2.message;
1525
+ }
1526
+ function errorMessages(error2) {
1527
+ const messages = [];
1528
+ const visited = /* @__PURE__ */ new Set();
1529
+ let current = error2;
1530
+ while (current !== null && current !== void 0 && !visited.has(current)) {
1531
+ visited.add(current);
1532
+ if (typeof current === "string") {
1533
+ messages.push(current);
1534
+ break;
1535
+ }
1536
+ if (current instanceof Error) {
1537
+ messages.push(current.name, current.message);
1538
+ current = current.cause;
1539
+ continue;
1540
+ }
1541
+ if (typeof current === "object") {
1542
+ const record = current;
1543
+ if (typeof record.code === "string") messages.push(record.code);
1544
+ if (typeof record.message === "string") messages.push(record.message);
1545
+ current = record.cause;
1546
+ continue;
1547
+ }
1548
+ break;
1549
+ }
1550
+ return messages;
1551
+ }
1552
+ function isSessionStoreWriteFailure(error2) {
1553
+ return errorMessages(error2).some(
1554
+ (message) => /(?:database or disk is full|disk is full|no space left on device|SQLITE_(?:FULL|IOERR)(?:\b|_))/i.test(
1555
+ message
1556
+ )
1557
+ );
1558
+ }
1559
+ var SQLITE_CAUSE_TOKEN = /(?:^|cause:\s+)(SQLiteError:\s+[A-Za-z][A-Za-z0-9]*(?:\s+[A-Za-z][A-Za-z0-9]*){0,7})(?=\s*(?:[)\]"',;]|$))/i;
1560
+ var EFFECT_SQL_CAUSE_TOKEN = /(?:^|cause:\s+)(effect\/sql\/SqlError\/[A-Za-z][A-Za-z0-9_]*)/i;
1561
+ var PARAMS_FIELD = /\bparams\s*(?:=|:)/i;
1562
+ function isCauseBearingText(text) {
1563
+ return /^(?:SQLiteError:\s+|effect\/sql\/SqlError\/)/i.test(text.trim()) || /(?:^|\s)cause:\s+(?:SQLiteError:\s+|effect\/sql\/SqlError\/)/i.test(text);
1564
+ }
1565
+ function structuredLogFields(line) {
1566
+ const fields = [];
1567
+ for (let index = 0; index < line.length; index++) {
1568
+ if (line[index] === '"') {
1569
+ index++;
1570
+ while (index < line.length) {
1571
+ if (line[index] === "\\") {
1572
+ index++;
1573
+ } else if (line[index] === '"') {
1574
+ break;
1575
+ }
1576
+ index++;
1577
+ }
1578
+ continue;
1579
+ }
1580
+ const atFieldBoundary = index === 0 || /\s/.test(line[index - 1]);
1581
+ const key = line.startsWith("error=", index) ? "error=" : line.startsWith("cause=", index) ? "cause=" : null;
1582
+ if (!atFieldBoundary || key === null) continue;
1583
+ const valueStart = index + key.length;
1584
+ if (line[valueStart] !== '"') {
1585
+ const valueEnd2 = line.indexOf(" ", valueStart);
1586
+ fields.push(line.slice(valueStart, valueEnd2 === -1 ? line.length : valueEnd2));
1587
+ index = valueEnd2 === -1 ? line.length : valueEnd2;
1588
+ continue;
1589
+ }
1590
+ let valueEnd = valueStart + 1;
1591
+ for (; valueEnd < line.length; valueEnd++) {
1592
+ if (line[valueEnd] === "\\") {
1593
+ valueEnd++;
1594
+ } else if (line[valueEnd] === '"') {
1595
+ break;
1596
+ }
1597
+ }
1598
+ fields.push(line.slice(valueStart + 1, valueEnd));
1599
+ index = valueEnd;
1600
+ }
1601
+ return fields;
1602
+ }
1603
+ function structuredCauseMessages(text) {
1604
+ const fields = structuredLogFields(text);
1605
+ if (fields.length > 0) return fields;
1606
+ return isCauseBearingText(text) ? [text] : [];
1607
+ }
1608
+ function causeTokenFromText(text) {
1609
+ const paramsIndex = text.search(PARAMS_FIELD);
1610
+ for (const pattern of [SQLITE_CAUSE_TOKEN, EFFECT_SQL_CAUSE_TOKEN]) {
1611
+ const match = pattern.exec(text);
1612
+ if (match?.[1] && (paramsIndex === -1 || match.index < paramsIndex)) {
1613
+ return match[1];
1614
+ }
1615
+ }
1616
+ return null;
1617
+ }
1618
+ function sessionStoreCauseToken(value) {
1619
+ for (const message of structuredCauseMessages(value)) {
1620
+ const token = causeTokenFromText(message);
1621
+ if (token !== null) return redactDiagnosticText(token).slice(0, 120);
1622
+ }
1623
+ return null;
1624
+ }
1625
+ function insufficientSpaceReason(dbPath, requiredBytes) {
1626
+ try {
1627
+ const fsStats = statfsSync(dirname2(dbPath));
1628
+ const availableBytes = fsStats.bavail * fsStats.bsize;
1629
+ if (availableBytes < requiredBytes) {
1630
+ return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
1631
+ }
1632
+ return null;
1633
+ } catch (err) {
1634
+ return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
1635
+ }
1636
+ }
1637
+ function readLogicalBytes(db) {
1638
+ const pageCount = db.prepare("PRAGMA page_count").get().page_count;
1639
+ const pageSize = db.prepare("PRAGMA page_size").get().page_size;
1640
+ return pageCount * pageSize;
1641
+ }
1642
+ function readFreelistCount(db) {
1643
+ return db.prepare("PRAGMA freelist_count").get().freelist_count;
1644
+ }
1645
+ function readCheckpointResult(db) {
1646
+ db.exec("PRAGMA busy_timeout=0");
1647
+ const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
1648
+ return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
1649
+ }
1650
+ async function probeReclaimAvailability(input) {
1651
+ const { dbPath, requiredBytes } = input;
1652
+ let sqlite;
1653
+ try {
1654
+ sqlite = await import("node:sqlite");
1655
+ } catch (err) {
1656
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
1657
+ console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
1658
+ return { reason: "sqlite-unavailable", detail };
1659
+ }
1660
+ let autoVacuum = null;
1661
+ try {
1662
+ const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
1663
+ try {
1664
+ db.exec(`PRAGMA busy_timeout=${SESSION_DB_RECLAIM_BUSY_TIMEOUT_MS}`);
1665
+ autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
1666
+ } finally {
1667
+ db.close();
1668
+ }
1669
+ } catch (err) {
1670
+ console.warn(
1671
+ `[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
1672
+ );
1673
+ }
1674
+ if (autoVacuum !== 0) return null;
1675
+ return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
1676
+ }
1677
+ async function reclaimSessionDbSpace(input) {
1678
+ const {
1679
+ dbPath,
1680
+ pageCeiling = SESSION_DB_RECLAIM_PAGE_CEILING,
1681
+ timeBudgetMs = SESSION_DB_RECLAIM_TIME_BUDGET_MS,
1682
+ chunkPages = SESSION_DB_RECLAIM_CHUNK_PAGES,
1683
+ now = () => performance.now(),
1684
+ hasLiveTurn = false,
1685
+ hasProvenanceAnomaly = false
1686
+ } = input;
1687
+ let sqlite;
1688
+ try {
1689
+ sqlite = await import("node:sqlite");
1690
+ } catch (err) {
1691
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
1692
+ console.warn(
1693
+ `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
1694
+ );
1695
+ return { ok: false, skipped: "sqlite-unavailable", detail };
1696
+ }
1697
+ const { DatabaseSync } = sqlite;
1698
+ let db;
1699
+ try {
1700
+ db = new DatabaseSync(dbPath);
1701
+ db.exec(`PRAGMA busy_timeout=${SESSION_DB_RECLAIM_BUSY_TIMEOUT_MS}`);
1702
+ const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
1703
+ if (autoVacuum === 0) {
1704
+ if (hasLiveTurn || hasProvenanceAnomaly) {
1705
+ const detail = [
1706
+ ...hasLiveTurn ? ["a session turn is live"] : [],
1707
+ ...hasProvenanceAnomaly ? ["session-DB provenance is anomalous"] : []
1708
+ ].join(" and ");
1709
+ console.warn(`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${detail}`);
1710
+ return {
1711
+ ok: false,
1712
+ skipped: "full-vacuum-blocked",
1713
+ detail
1714
+ };
1715
+ }
1716
+ const fileBytesForGuard = statSync2(dbPath).size;
1717
+ const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
1718
+ if (skipReason !== null) {
1719
+ console.warn(
1720
+ `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
1721
+ );
1722
+ return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
1723
+ }
1724
+ const beforeBytes = readLogicalBytes(db);
1725
+ const freelistBefore = readFreelistCount(db);
1726
+ const startedAt = now();
1727
+ db.exec("PRAGMA auto_vacuum=INCREMENTAL");
1728
+ db.exec("VACUUM");
1729
+ const afterBytes = readLogicalBytes(db);
1730
+ const freelistAfter = readFreelistCount(db);
1731
+ const checkpointStartedAt = now();
1732
+ const checkpoint = readCheckpointResult(db);
1733
+ const checkpointElapsedMs = Math.max(0, now() - checkpointStartedAt);
1734
+ return {
1735
+ ok: true,
1736
+ mode: "convert",
1737
+ beforeBytes,
1738
+ afterBytes,
1739
+ freelistBefore,
1740
+ freelistAfter,
1741
+ pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
1742
+ stoppedBy: "freelist-drained",
1743
+ elapsedMs: Math.max(0, now() - startedAt),
1744
+ checkpointElapsedMs,
1745
+ checkpoint
1746
+ };
1747
+ }
1748
+ if (autoVacuum === 2) {
1749
+ if (hasLiveTurn) {
1750
+ const detail = "a session turn is live";
1751
+ console.warn(
1752
+ `[reclaimSessionDbSpace] skipping incremental reclaim of ${dbPath}: ${detail}`
1753
+ );
1754
+ return { ok: false, skipped: "live-turn", detail };
1755
+ }
1756
+ const beforeBytes = readLogicalBytes(db);
1757
+ const freelistBefore = readFreelistCount(db);
1758
+ const startedAt = now();
1759
+ const pageLimit = Math.max(0, Math.trunc(pageCeiling));
1760
+ const chunkLimit = Math.max(1, Math.trunc(chunkPages));
1761
+ const budget = Math.max(0, timeBudgetMs);
1762
+ let pagesAttempted = 0;
1763
+ let freelistAfter = freelistBefore;
1764
+ let stoppedBy = "freelist-drained";
1765
+ while (freelistAfter > 0) {
1766
+ if (pagesAttempted >= pageLimit) {
1767
+ stoppedBy = "page-ceiling";
1768
+ break;
1769
+ }
1770
+ if (now() - startedAt >= budget) {
1771
+ stoppedBy = "time-budget";
1772
+ break;
1773
+ }
1774
+ const pages = Math.min(chunkLimit, pageLimit - pagesAttempted, freelistAfter);
1775
+ db.exec(`PRAGMA incremental_vacuum(${pages})`);
1776
+ pagesAttempted += pages;
1777
+ freelistAfter = readFreelistCount(db);
1778
+ }
1779
+ const afterBytes = readLogicalBytes(db);
1780
+ const checkpointStartedAt = now();
1781
+ const checkpoint = readCheckpointResult(db);
1782
+ const checkpointElapsedMs = Math.max(0, now() - checkpointStartedAt);
1783
+ return {
1784
+ ok: true,
1785
+ mode: "incremental",
1786
+ beforeBytes,
1787
+ afterBytes,
1788
+ freelistBefore,
1789
+ freelistAfter,
1790
+ pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
1791
+ stoppedBy,
1792
+ elapsedMs: Math.max(0, now() - startedAt),
1793
+ checkpointElapsedMs,
1794
+ checkpoint
1795
+ };
1796
+ }
1797
+ console.warn(
1798
+ `[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
1799
+ );
1800
+ return { ok: false, skipped: "auto-vacuum-not-applicable" };
1801
+ } catch (err) {
1802
+ console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
1803
+ return {
1804
+ ok: false,
1805
+ skipped: isSessionStoreWriteFailure(err) ? "disk-exhaustion" : "reclaim-error",
1806
+ detail: errorMessage(err)
1807
+ };
1808
+ } finally {
1809
+ db?.close();
1810
+ }
1811
+ }
1812
+
1813
+ // src/lib/opencode/dependency-install-diagnostic.ts
1814
+ import { lstatSync, realpathSync } from "node:fs";
1815
+ import { basename, dirname as dirname3, isAbsolute, join as join2, relative, resolve, sep } from "node:path";
1816
+ var DEPENDENCY_INSTALL_FAILURE_MESSAGE = "background dependency install failed";
1817
+ var MAX_DISTINCT_DIAGNOSTICS = 32;
1818
+ var EFFECT = "OpenCode could not install background dependencies for this scope; its plugins and tools may be unavailable";
1819
+ function readOpenCodeLogField(line, key) {
1820
+ const match = line.match(new RegExp(`(?:^|\\s)${key}=(?:"((?:\\\\.|[^"])*)"|(\\S+))`));
1821
+ if (!match) return void 0;
1822
+ if (match[1] === void 0) return match[2];
1823
+ return match[1].replace(/\\(["\\n])/g, (_match, escaped) => {
1824
+ return escaped === "n" ? "\n" : escaped;
1825
+ });
1826
+ }
1827
+ function parseTrigger(line) {
1828
+ if (readOpenCodeLogField(line, "message") !== DEPENDENCY_INSTALL_FAILURE_MESSAGE) {
1829
+ return null;
1830
+ }
1831
+ const error2 = readOpenCodeLogField(line, "error");
1832
+ if (!error2 || !/\bEACCES\b/.test(error2)) return null;
1833
+ const errnoMatch = error2.match(/\bEACCES:\s*permission denied,\s*([a-z_]+)\s+'([^']*)'/);
1834
+ const capturedPath = errnoMatch?.[2];
1835
+ return {
1836
+ dir: readOpenCodeLogField(line, "dir"),
1837
+ op: errnoMatch?.[1],
1838
+ path: capturedPath && isAbsolute(capturedPath) ? capturedPath : void 0
1839
+ };
1840
+ }
1841
+ function getErrorCode(error2) {
1842
+ if (typeof error2 === "object" && error2 !== null && "code" in error2) {
1843
+ const code = error2.code;
1844
+ if (typeof code === "string") return code;
1845
+ }
1846
+ return "UNKNOWN";
1847
+ }
1848
+ function isWithin(path, root) {
1849
+ const candidate = resolve(path);
1850
+ const resolvedRoot = resolve(root);
1851
+ return candidate === resolvedRoot || candidate.startsWith(`${resolvedRoot}${sep}`);
1852
+ }
1853
+ function rootSpecs(context) {
1854
+ return [
1855
+ { token: "~", configured: resolve(context.homeDir) },
1856
+ { token: "$WORKSPACE", configured: resolve(context.cwd) }
1857
+ ];
1858
+ }
1859
+ function resolveRoots(context) {
1860
+ const resolved = [];
1861
+ const unresolved = [];
1862
+ for (const spec of rootSpecs(context)) {
1863
+ try {
1864
+ resolved.push({ ...spec, real: realpathSync(spec.configured) });
1865
+ } catch (_error) {
1866
+ unresolved.push(spec);
1867
+ }
1868
+ }
1869
+ return { resolved, unresolved };
1870
+ }
1871
+ function resolveNearestAncestor(failingPath, remaining, error2) {
1872
+ let ancestor = dirname3(failingPath);
1873
+ const suffix = [basename(failingPath), ...remaining];
1874
+ while (ancestor) {
1875
+ try {
1876
+ return {
1877
+ path: resolve(realpathSync(ancestor), ...suffix),
1878
+ failure: { path: resolve(failingPath), error: error2 }
1879
+ };
1880
+ } catch (ancestorError) {
1881
+ if (ancestor === dirname3(ancestor) || !getErrorCode(ancestorError).startsWith("E")) {
1882
+ return null;
1883
+ }
1884
+ suffix.unshift(basename(ancestor));
1885
+ ancestor = dirname3(ancestor);
1886
+ }
1887
+ }
1888
+ return null;
1889
+ }
1890
+ function resolveExistingPath(absolutePath) {
1891
+ if (!isAbsolute(absolutePath)) return null;
1892
+ const candidate = resolve(absolutePath);
1893
+ const remaining = [];
1894
+ let current = candidate;
1895
+ while (current) {
1896
+ try {
1897
+ const stat2 = lstatSync(current);
1898
+ if (stat2.isSymbolicLink()) {
1899
+ if (remaining.length === 0) {
1900
+ return { path: join2(realpathSync(dirname3(current)), basename(current)) };
1901
+ }
1902
+ return { path: resolve(realpathSync(current), ...remaining) };
1903
+ }
1904
+ return { path: resolve(realpathSync(current), ...remaining) };
1905
+ } catch (error2) {
1906
+ const code = getErrorCode(error2);
1907
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
1908
+ return resolveNearestAncestor(current, remaining, error2);
1909
+ }
1910
+ }
1911
+ if (current === dirname3(current)) return null;
1912
+ remaining.unshift(basename(current));
1913
+ current = dirname3(current);
1914
+ }
1915
+ return null;
1916
+ }
1917
+ function tokenise(absolutePath, roots) {
1918
+ const pathResolution = resolveExistingPath(absolutePath);
1919
+ if (!pathResolution) return null;
1920
+ const root = roots.reduce((selected, candidate) => {
1921
+ if (!isWithin(pathResolution.path, candidate.real)) return selected;
1922
+ if (!selected || candidate.real.length > selected.real.length || candidate.real.length === selected.real.length && candidate.token === "$WORKSPACE") {
1923
+ return candidate;
1924
+ }
1925
+ return selected;
1926
+ }, null);
1927
+ if (!root) return null;
1928
+ const suffix = relative(root.real, pathResolution.path);
1929
+ return {
1930
+ value: suffix ? `${root.token}/${suffix.split(sep).join("/")}` : root.token,
1931
+ root,
1932
+ resolutionFailure: pathResolution.failure
1933
+ };
1934
+ }
1935
+ function classifyScope(dir, roots) {
1936
+ if (!dir) return "other";
1937
+ const tokenisedDir = tokenise(dir, roots.resolved);
1938
+ if (tokenisedDir) return tokenisedDir.root.token === "$WORKSPACE" ? "workspace" : "global";
1939
+ for (const spec of roots.unresolved) {
1940
+ if (isWithin(dir, spec.configured)) {
1941
+ return spec.token === "$WORKSPACE" ? "workspace" : "global";
1942
+ }
1943
+ }
1944
+ return "other";
1945
+ }
1946
+ function unresolvedRootForPath(path, unresolved) {
1947
+ return unresolved.find((spec) => isWithin(path, spec.configured));
1948
+ }
1949
+ function statType(stat2) {
1950
+ if (stat2.isDirectory()) return "d";
1951
+ if (stat2.isFile()) return "f";
1952
+ if (stat2.isSymbolicLink()) return "l";
1953
+ return "o";
1954
+ }
1955
+ function renderStat(name, stat2) {
1956
+ const mode = (stat2.mode & 4095).toString(8).padStart(4, "0");
1957
+ return `${name}:${statType(stat2)}:${stat2.uid}:${stat2.gid}:${mode}`;
1958
+ }
1959
+ function renderStatError(name, error2) {
1960
+ const code = getErrorCode(error2);
1961
+ return `${name}:${renderErrorCode(code)}`;
1962
+ }
1963
+ function renderErrorCode(code) {
1964
+ if (code === "ENOENT") return "missing";
1965
+ return code.startsWith("E") ? code : `E${code}`;
1966
+ }
1967
+ function inspectChain(rootReal, rootToken, absolutePath, resolutionFailure) {
1968
+ const target = resolve(absolutePath);
1969
+ const relativePath = relative(rootReal, target);
1970
+ const components = relativePath ? relativePath.split(sep) : [];
1971
+ const entries = [];
1972
+ let current = rootReal;
1973
+ let status2 = "complete";
1974
+ let finalStat;
1975
+ for (const name of [rootToken, ...components]) {
1976
+ if (name !== rootToken) current = join2(current, name);
1977
+ if (resolutionFailure && resolve(current) === resolve(resolutionFailure.path)) {
1978
+ entries.push(renderStatError(name, resolutionFailure.error));
1979
+ status2 = "stat_failed";
1980
+ break;
1981
+ }
1982
+ try {
1983
+ const stat2 = lstatSync(current);
1984
+ entries.push(renderStat(name, stat2));
1985
+ finalStat = stat2;
1986
+ } catch (error2) {
1987
+ entries.push(renderStatError(name, error2));
1988
+ if (getErrorCode(error2) !== "ENOENT") status2 = "stat_failed";
1989
+ break;
1990
+ }
1991
+ }
1992
+ let symlinkTarget;
1993
+ let symlinkMeta;
1994
+ if (finalStat?.isSymbolicLink() && entries.length === components.length + 1) {
1995
+ try {
1996
+ symlinkTarget = realpathSync(current);
1997
+ try {
1998
+ symlinkMeta = renderStat("", lstatSync(symlinkTarget)).slice(1);
1999
+ } catch (error2) {
2000
+ symlinkMeta = renderErrorCode(getErrorCode(error2));
2001
+ status2 = "stat_failed";
2002
+ }
2003
+ } catch (error2) {
2004
+ symlinkTarget = "<unresolved>";
2005
+ symlinkMeta = renderErrorCode(getErrorCode(error2));
2006
+ status2 = "stat_failed";
2007
+ }
2008
+ }
2009
+ return { status: status2, chain: entries.join(","), symlinkTarget, symlinkMeta };
2010
+ }
2011
+ function buildDiagnostic(line, context) {
2012
+ const trigger = parseTrigger(line);
2013
+ if (!trigger) return null;
2014
+ const roots = resolveRoots(context);
2015
+ const scope = classifyScope(trigger.dir, roots);
2016
+ const pathSource = trigger.path ? "error" : trigger.dir ? "dir" : "none";
2017
+ const inspectedPath = trigger.path ?? trigger.dir;
2018
+ const microvmId = context.microvmId?.trim() || "unknown";
2019
+ const euid = context.euid === void 0 ? "unavailable" : String(context.euid);
2020
+ const egid = context.egid === void 0 ? "unavailable" : String(context.egid);
2021
+ const op = trigger.op ?? "unknown";
2022
+ if (!inspectedPath) {
2023
+ return {
2024
+ event: `OPENCODE-DEPENDENCY-INSTALL-EACCES: status=no_path path_source=${pathSource} scope=${scope} microvm_id=${microvmId} op=${op} path=unknown cli_euid=${euid} cli_egid=${egid} effect="${EFFECT}" chain=unavailable`,
2025
+ localDetail: null
2026
+ };
2027
+ }
2028
+ const tokenisedPath = tokenise(inspectedPath, roots.resolved);
2029
+ if (!tokenisedPath) {
2030
+ const unresolvedRoot = unresolvedRootForPath(inspectedPath, roots.unresolved);
2031
+ if (unresolvedRoot) {
2032
+ return {
2033
+ event: `OPENCODE-DEPENDENCY-INSTALL-EACCES: status=root_unresolved path_source=${pathSource} scope=${scope} microvm_id=${microvmId} op=${op} path=unknown cli_euid=${euid} cli_egid=${egid} effect="${EFFECT}" chain=unavailable`,
2034
+ localDetail: null
2035
+ };
2036
+ }
2037
+ return {
2038
+ event: `OPENCODE-DEPENDENCY-INSTALL-EACCES: status=outside_roots path_source=${pathSource} scope=${scope} microvm_id=${microvmId} op=${op} path=<outside-approved-roots> cli_euid=${euid} cli_egid=${egid} effect="${EFFECT}" chain=unavailable`,
2039
+ localDetail: `OPENCODE-DEPENDENCY-INSTALL-EACCES (local only): outside approved roots: ${inspectedPath}`
2040
+ };
2041
+ }
2042
+ const inspection = inspectChain(
2043
+ tokenisedPath.root.real,
2044
+ tokenisedPath.root.token,
2045
+ inspectedPath,
2046
+ tokenisedPath.resolutionFailure
2047
+ );
2048
+ const optionalSymlinkFields = inspection.symlinkTarget ? ` symlink_target=${tokenise(inspection.symlinkTarget, roots.resolved)?.value ?? "<outside-approved-roots>"}` + (inspection.symlinkMeta ? ` symlink_meta=${inspection.symlinkMeta}` : "") : "";
2049
+ return {
2050
+ event: `OPENCODE-DEPENDENCY-INSTALL-EACCES: status=${inspection.status} path_source=${pathSource} scope=${scope} microvm_id=${microvmId} op=${op} path=${tokenisedPath.value} cli_euid=${euid} cli_egid=${egid} effect="${EFFECT}" chain=${inspection.chain}${optionalSymlinkFields}`,
2051
+ localDetail: null
2052
+ };
2053
+ }
2054
+ function createDependencyInstallDiagnosticReporter({
2055
+ context,
2056
+ emitEvent: emitEvent2,
2057
+ emitLocal
2058
+ }) {
2059
+ const emittedEvents = /* @__PURE__ */ new Set();
2060
+ return (line) => {
2061
+ const diagnostic = buildDiagnostic(line, context);
2062
+ if (!diagnostic || emittedEvents.has(diagnostic.event)) return;
2063
+ if (emittedEvents.size >= MAX_DISTINCT_DIAGNOSTICS) return;
2064
+ emittedEvents.add(diagnostic.event);
2065
+ emitEvent2(diagnostic.event);
2066
+ if (diagnostic.localDetail) emitLocal(diagnostic.localDetail);
2067
+ };
2068
+ }
2069
+
2070
+ // src/lib/runner-activity-telemetry.ts
1323
2071
  var FORWARDED_LEVELS_BY_SOURCE = {
1324
2072
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
1325
2073
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
@@ -1336,11 +2084,13 @@ function parseOpenCodeLogLine(line) {
1336
2084
  if (!levelMatch) return null;
1337
2085
  const level = levelMatch[1].toUpperCase();
1338
2086
  if (level !== "WARN" && level !== "ERROR") return null;
1339
- const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
2087
+ const sessionMatch = normalisedLine.match(/(?:^|\s)(?:sessionID|session\.id)=(\S+)/);
2088
+ const messageIDMatch = normalisedLine.match(/(?:^|\s)messageID=(\S+)/);
1340
2089
  const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
1341
2090
  const parsed = {
1342
2091
  level: level === "WARN" ? "warn" : "error",
1343
- sessionID: sessionMatch?.[1]
2092
+ sessionID: sessionMatch?.[1],
2093
+ messageID: messageIDMatch?.[1]
1344
2094
  };
1345
2095
  const message = messageMatch?.[1] ?? messageMatch?.[2];
1346
2096
  if (message !== void 0) parsed.message = message;
@@ -1348,18 +2098,29 @@ function parseOpenCodeLogLine(line) {
1348
2098
  }
1349
2099
  var MAX_LINE_BUFFER_BYTES = 16 * 1024;
1350
2100
  var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
1351
- function createOpenCodeActivityForwarder(getContext) {
2101
+ function sanitiseDependencyInstallWarning(line) {
2102
+ const withoutDirectory = line.replace(
2103
+ /((?:^|\s)dir=)(?:"(?:\\.|[^"])*"|\/[^\s)"']+|\S+)/g,
2104
+ "$1<path>"
2105
+ );
2106
+ return withoutDirectory.replace(
2107
+ /((?:^|\s)error=)"((?:\\.|[^"])*)"/,
2108
+ (_match, prefix, error2) => `${prefix}"${error2.replace(/'\/[^']*'/g, "'<path>'")}"`
2109
+ );
2110
+ }
2111
+ function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure, onDependencyInstallFailure) {
1352
2112
  let buffer = Buffer.alloc(0);
1353
2113
  const repeatStates = /* @__PURE__ */ new Map();
1354
2114
  const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
1355
2115
  const metadata = {
1356
2116
  ...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
2117
+ ...parsed.messageID ? { messageID: parsed.messageID } : {},
1357
2118
  ...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
1358
2119
  };
1359
2120
  forwardRunnerActivity(
1360
2121
  {
1361
2122
  level: parsed.level,
1362
- error: line,
2123
+ error: parsed.message === DEPENDENCY_INSTALL_FAILURE_MESSAGE ? sanitiseDependencyInstallWarning(line) : line,
1363
2124
  metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
1364
2125
  source: "opencode"
1365
2126
  },
@@ -1377,11 +2138,9 @@ function createOpenCodeActivityForwarder(getContext) {
1377
2138
  repeatStates.delete(key);
1378
2139
  }
1379
2140
  };
1380
- const flushLine = (line) => {
1381
- const parsed = parseOpenCodeLogLine(line);
1382
- if (!parsed) return;
2141
+ const forwardWithRepeatSuppression = (line, parsed) => {
1383
2142
  const now = Date.now();
1384
- const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}`;
2143
+ const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}\0${parsed.sessionID ?? ""}\0${parsed.messageID ?? ""}`;
1385
2144
  expireRepeatStates(now, key);
1386
2145
  if (key === void 0) {
1387
2146
  forwardLine(line, parsed);
@@ -1398,9 +2157,38 @@ function createOpenCodeActivityForwarder(getContext) {
1398
2157
  });
1399
2158
  return;
1400
2159
  }
1401
- previous.line = line;
1402
- previous.parsed = parsed;
1403
- previous.repeatsSuppressed++;
2160
+ previous.line = line;
2161
+ previous.parsed = parsed;
2162
+ previous.repeatsSuppressed++;
2163
+ };
2164
+ const flushLine = (line) => {
2165
+ const parsed = parseOpenCodeLogLine(line);
2166
+ const isDiskFailure = isSessionStoreWriteFailure(line);
2167
+ const cause = sessionStoreCauseToken(line);
2168
+ if (onSessionStoreWriteFailure && (isDiskFailure || cause !== null)) {
2169
+ try {
2170
+ onSessionStoreWriteFailure({
2171
+ isDiskFailure,
2172
+ sessionId: parsed?.sessionID ?? null,
2173
+ cause
2174
+ });
2175
+ } catch (err) {
2176
+ reportTelemetryDiagnostic(
2177
+ `Runner activity telemetry could not record session-store write failure: ${err instanceof Error ? err.message : String(err)}`
2178
+ );
2179
+ }
2180
+ }
2181
+ if (!parsed) return;
2182
+ forwardWithRepeatSuppression(line, parsed);
2183
+ if (onDependencyInstallFailure && parsed.message === DEPENDENCY_INSTALL_FAILURE_MESSAGE) {
2184
+ try {
2185
+ onDependencyInstallFailure(line);
2186
+ } catch (err) {
2187
+ reportTelemetryDiagnostic(
2188
+ `Runner activity telemetry could not record dependency-install failure diagnostic: ${err instanceof Error ? err.message : String(err)}`
2189
+ );
2190
+ }
2191
+ }
1404
2192
  };
1405
2193
  const forward = ((chunk) => {
1406
2194
  buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
@@ -1427,34 +2215,38 @@ function createOpenCodeActivityForwarder(getContext) {
1427
2215
  var MAX_MESSAGE_LENGTH = 500;
1428
2216
  var MAX_METADATA_VALUE_LENGTH = 200;
1429
2217
  var MAX_METADATA_ENTRIES = 20;
1430
- var TRUNCATION_MARKER = "\u2026";
1431
- function redactHomeDirectory(message) {
1432
- const home = homedir2();
1433
- if (!home || home === "/") {
1434
- return message;
1435
- }
1436
- const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
1437
- let result = "";
1438
- let searchFrom = 0;
1439
- let homeIndex;
1440
- while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
1441
- const homeEnd = homeIndex + home.length;
1442
- const previousCharacter = message[homeIndex - 1];
1443
- const nextCharacter = message[homeEnd];
1444
- const atPathBoundary = home === "/" || (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
1445
- result += message.slice(searchFrom, homeIndex);
1446
- result += atPathBoundary ? "~" : home;
1447
- searchFrom = homeEnd;
1448
- }
1449
- return result + message.slice(searchFrom);
1450
- }
1451
- function redactDiagnosticText(message) {
1452
- 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>");
1453
- return redactHomeDirectory(redacted);
1454
- }
1455
2218
  function truncate(message) {
1456
2219
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
1457
- return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
2220
+ let dropped = message.length - MAX_MESSAGE_LENGTH;
2221
+ let marker = `\u2026${dropped} chars omitted\u2026`;
2222
+ let previousDropped = -1;
2223
+ while (dropped !== previousDropped) {
2224
+ previousDropped = dropped;
2225
+ const nextDropped = message.length - MAX_MESSAGE_LENGTH + marker.length;
2226
+ dropped = nextDropped;
2227
+ marker = `\u2026${dropped} chars omitted\u2026`;
2228
+ }
2229
+ const remaining = MAX_MESSAGE_LENGTH - marker.length;
2230
+ const headLength = Math.ceil(remaining * 0.6);
2231
+ const tailLength = remaining - headLength;
2232
+ const effectMatch = message.startsWith("OPENCODE-DEPENDENCY-INSTALL-EACCES:") ? message.match(/ effect="[^"]*"/) : null;
2233
+ if (effectMatch?.index !== void 0) {
2234
+ const effectEnd = effectMatch.index + effectMatch[0].length;
2235
+ if (effectEnd > headLength && effectEnd < message.length) {
2236
+ const effectLength = effectMatch[0].length;
2237
+ const nonEffectLength = remaining - effectLength;
2238
+ const preservedTailLength = Math.min(
2239
+ message.length - effectEnd,
2240
+ nonEffectLength - Math.min(effectMatch.index, Math.ceil(nonEffectLength * 0.6))
2241
+ );
2242
+ const preservedHeadLength = Math.min(
2243
+ effectMatch.index,
2244
+ nonEffectLength - preservedTailLength
2245
+ );
2246
+ return message.slice(0, preservedHeadLength) + marker + message.slice(effectMatch.index, effectEnd) + message.slice(message.length - preservedTailLength);
2247
+ }
2248
+ }
2249
+ return message.slice(0, headLength) + marker + message.slice(message.length - tailLength);
1458
2250
  }
1459
2251
  function sanitiseMetadata(metadata) {
1460
2252
  if (!metadata) return {};
@@ -1531,10 +2323,10 @@ function forwardRunnerActivity(entry, context) {
1531
2323
 
1532
2324
  // src/lib/opencode/session-db-recovery-report.ts
1533
2325
  import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
1534
- import { join as join2 } from "node:path";
2326
+ import { join as join3 } from "node:path";
1535
2327
  function sessionDbRecoveryReportPath(homeDir, env) {
1536
2328
  const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
1537
- return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
2329
+ return override || join3(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
1538
2330
  }
1539
2331
  function drainSessionDbRecoveryReport({
1540
2332
  homeDir,
@@ -1664,12 +2456,23 @@ function buildSessionDbRecoveryActivity(record) {
1664
2456
  metadata: withoutContractFields(record),
1665
2457
  message: `This runner started without restoring session history because it could not verify where its session history is backed up. Nothing in the backup was changed; check the runner backup configuration and permissions.${replication}`
1666
2458
  };
1667
- case "session_db_boot_refused":
1668
- return {
1669
- level,
1670
- metadata: withoutContractFields(record),
1671
- message: `This runner did not come online because its damaged session database could not be safely separated from its active backup or proven removed. Backed-up session history remains readable at ${record.quarantine_destination ?? "its original location or the quarantine destination named in the boot logs"}; any local session-database files that remain were left in place and nothing opened or wrote them. See the runner boot logs for SESSION-DB-LOCAL-DISCARD-FAILED details.`
1672
- };
2459
+ case "session_db_boot_refused": {
2460
+ const message = (() => {
2461
+ switch (record.reason) {
2462
+ case "local_discard_failed":
2463
+ return `This runner did not come online because its damaged session database could not be safely separated from its active backup or proven removed. Backed-up session history remains readable at ${record.quarantine_destination ?? "its original location or the quarantine destination named in the boot logs"}; any local session-database files that remain were left in place and nothing opened or wrote them. See the runner boot logs for SESSION-DB-LOCAL-DISCARD-FAILED details.`;
2464
+ case "replica_separation_unproven":
2465
+ return `This runner did not come online because its damaged session database could not be safely separated from its active backup. The SESSION-DB-REPLICA-SEPARATION-UNVERIFIED boot log contains the partial-quarantine moved and remaining counts. Backed-up session history remains readable at ${record.quarantine_destination ?? "its original location or the quarantine destination named in the boot logs"}; any local session-database files that remain were left in place and nothing opened or wrote them.`;
2466
+ case "verification_timeout_before_separation":
2467
+ return "This runner did not come online because session database verification timed out after detecting corruption, before replica separation was attempted. No replica progress is claimed. See SESSION-DB-VERIFY-TIMEOUT in the runner boot logs before retrying.";
2468
+ case "verification_timeout_during_separation":
2469
+ return "This runner did not come online because session database verification timed out while replica separation was in progress. Separation completion and object removal were not proven. See SESSION-DB-VERIFY-TIMEOUT in the runner boot logs before retrying.";
2470
+ default:
2471
+ return "This runner did not come online because session database recovery could not be completed. Inspect the runner boot logs for the refusal reason before retrying.";
2472
+ }
2473
+ })();
2474
+ return { level, metadata: withoutContractFields(record), message };
2475
+ }
1673
2476
  case "schema_provenance_mismatch":
1674
2477
  return {
1675
2478
  level,
@@ -1796,7 +2599,7 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1796
2599
  if (health.healthy || health.authFailed) {
1797
2600
  return health;
1798
2601
  }
1799
- await new Promise((resolve4) => setTimeout(resolve4, 1e3));
2602
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
1800
2603
  }
1801
2604
  return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1802
2605
  }
@@ -1807,7 +2610,7 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1807
2610
  if (health.healthy || health.authFailed) {
1808
2611
  return health;
1809
2612
  }
1810
- await new Promise((resolve4) => setTimeout(resolve4, 1e3));
2613
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
1811
2614
  }
1812
2615
  return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1813
2616
  }
@@ -1919,7 +2722,7 @@ async function getOpenCodeDirectory(port, client) {
1919
2722
  return null;
1920
2723
  }
1921
2724
  }
1922
- function roleOf(m) {
2725
+ function roleOf2(m) {
1923
2726
  if (!m || typeof m !== "object") return void 0;
1924
2727
  if (typeof m.role === "string") return m.role;
1925
2728
  const infoRole = m.info?.role;
@@ -1933,13 +2736,13 @@ function createdOf(m) {
1933
2736
  if (!m || typeof m !== "object") return void 0;
1934
2737
  return m.info?.time?.created ?? m.time?.created;
1935
2738
  }
1936
- function idOf(m) {
2739
+ function idOf2(m) {
1937
2740
  if (!m || typeof m !== "object") return void 0;
1938
2741
  if (typeof m.id === "string") return m.id;
1939
2742
  const infoId = m.info?.id;
1940
2743
  return typeof infoId === "string" ? infoId : void 0;
1941
2744
  }
1942
- function parentIdOf(m) {
2745
+ function parentIdOf2(m) {
1943
2746
  if (!m || typeof m !== "object") return void 0;
1944
2747
  if (typeof m.parentID === "string") return m.parentID;
1945
2748
  const infoParent = m.info?.parentID;
@@ -1955,8 +2758,12 @@ function errorOf(m) {
1955
2758
  if (!m || typeof m !== "object") return void 0;
1956
2759
  return m.info?.error ?? m.error;
1957
2760
  }
2761
+ function isTerminalStopAnswer(m) {
2762
+ return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
2763
+ }
1958
2764
  function isAssistantInFlight(m) {
1959
2765
  if (completedOf(m) == null) return true;
2766
+ if (errorOf(m) != null) return false;
1960
2767
  return finishOf(m) === "tool-calls";
1961
2768
  }
1962
2769
  async function getSessionMessages(port, sessionId, client) {
@@ -2004,9 +2811,10 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
2004
2811
  function isSessionActivelyGenerating(messages) {
2005
2812
  if (!messages || messages.length === 0) return false;
2006
2813
  const last = messages[messages.length - 1];
2007
- if (roleOf(last) !== "assistant") return false;
2814
+ if (roleOf2(last) !== "assistant") return false;
2008
2815
  return completedOf(last) == null;
2009
2816
  }
2817
+ var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
2010
2818
  function sessionLastActivityMs(session) {
2011
2819
  const candidates = [
2012
2820
  session.time?.updated,
@@ -2021,30 +2829,71 @@ function sessionLastActivityMs(session) {
2021
2829
  }
2022
2830
  return null;
2023
2831
  }
2024
- async function listSessions(port, client) {
2025
- if (client?.version === "v2") return listV2Sessions(client);
2832
+ function sessionListPath(options) {
2833
+ const params = new URLSearchParams();
2834
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
2835
+ const query = params.toString();
2836
+ return query ? `/session?${query}` : "/session";
2837
+ }
2838
+ async function listSessionPage(port, client, options) {
2839
+ const path = sessionListPath(options);
2026
2840
  try {
2027
- const res = await requestWithClient(port, client, "/session");
2841
+ const res = await requestWithClient(port, client, path);
2028
2842
  if (!res.ok) return null;
2029
2843
  const body = await res.json();
2030
2844
  return Array.isArray(body) ? body : null;
2031
2845
  } catch (error2) {
2032
2846
  console.error(
2033
- `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2847
+ `[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2034
2848
  );
2035
2849
  return null;
2036
2850
  }
2037
2851
  }
2852
+ async function listSessions(port, client, options) {
2853
+ if (client?.version === "v2") return listV2Sessions(client);
2854
+ return listSessionPage(port, client, options);
2855
+ }
2856
+ async function listAllSessions(port, client) {
2857
+ if (client?.version === "v2") {
2858
+ const sessions2 = await listV2Sessions(client);
2859
+ return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
2860
+ }
2861
+ const sessions = await listSessionPage(port, client, {
2862
+ limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
2863
+ });
2864
+ if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
2865
+ if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
2866
+ return { sessions: null, reason: "session-enumeration-limit-bound" };
2867
+ }
2868
+ return { sessions };
2869
+ }
2038
2870
  async function deleteSession(port, id, client) {
2039
2871
  if (client?.version === "v2") return deleteV2Session(client, id);
2040
2872
  try {
2041
- const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2042
- return res.status >= 200 && res.status < 300;
2873
+ const res = await requestWithClient(
2874
+ port,
2875
+ client,
2876
+ `/session/${id}`,
2877
+ { method: "DELETE" },
2878
+ { allowStatuses: ALL_HTTP_STATUSES }
2879
+ );
2880
+ if (res.status >= 200 && res.status < 300) {
2881
+ return { ok: true, status: res.status };
2882
+ }
2883
+ try {
2884
+ const body = await res.text();
2885
+ return body ? { ok: false, status: res.status, body } : { ok: false, status: res.status };
2886
+ } catch (error2) {
2887
+ console.error(
2888
+ `[deleteSession] DELETE /session/${id} response body read failed for HTTP ${res.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
2889
+ );
2890
+ return { ok: false, status: res.status };
2891
+ }
2043
2892
  } catch (error2) {
2044
2893
  console.error(
2045
2894
  `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2046
2895
  );
2047
- return false;
2896
+ return { ok: false, status: "threw" };
2048
2897
  }
2049
2898
  }
2050
2899
  async function sessionExists(port, id, client) {
@@ -2282,11 +3131,31 @@ function messageText(m) {
2282
3131
  if (!m || !Array.isArray(m.parts)) return "";
2283
3132
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
2284
3133
  }
2285
- async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
3134
+ function findDispatchedUserMessageId(messages, content, excludeIds) {
3135
+ if (!messages || messages.length === 0) return null;
3136
+ let best = null;
3137
+ for (const message of messages) {
3138
+ if (roleOf2(message) !== "user") continue;
3139
+ const id = idOf2(message);
3140
+ if (typeof id !== "string" || excludeIds?.has(id)) continue;
3141
+ if (messageText(message) !== content) continue;
3142
+ const created = createdOf(message) ?? 0;
3143
+ if (best === null || created > best.created) best = { id, created };
3144
+ }
3145
+ return best?.id ?? null;
3146
+ }
3147
+ async function sendPromptAsync(port, sessionId, content, options, attachments, client, callbacks) {
2286
3148
  const before = await getSessionMessages(port, sessionId, client);
2287
3149
  const knownUserIds = new Set(
2288
- (before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
3150
+ (before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
2289
3151
  );
3152
+ if (callbacks) {
3153
+ try {
3154
+ callbacks.onKnownUserIds(before === null ? null : knownUserIds);
3155
+ } catch (error2) {
3156
+ callbacks.onKnownUserIdsError(error2);
3157
+ }
3158
+ }
2290
3159
  const parts = [{ type: "text", text: content }];
2291
3160
  let pendingOutcomes = null;
2292
3161
  if (attachments && attachments.inputs.length > 0) {
@@ -2325,25 +3194,13 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2325
3194
  const READ_BACK_DELAY_MS = 150;
2326
3195
  for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
2327
3196
  const after = await getSessionMessages(port, sessionId, client);
2328
- if (after) {
2329
- let best = null;
2330
- for (const m of after) {
2331
- if (roleOf(m) !== "user") continue;
2332
- const id = idOf(m);
2333
- if (typeof id !== "string" || knownUserIds.has(id)) continue;
2334
- if (messageText(m) !== content) continue;
2335
- const created = createdOf(m) ?? 0;
2336
- if (best === null || created > best.created) {
2337
- best = { id, created };
2338
- }
2339
- }
2340
- if (best) {
2341
- if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
2342
- return best.id;
2343
- }
3197
+ const dispatchedUserMessageId = findDispatchedUserMessageId(after, content, knownUserIds);
3198
+ if (dispatchedUserMessageId !== null) {
3199
+ if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
3200
+ return dispatchedUserMessageId;
2344
3201
  }
2345
3202
  if (attempt < READ_BACK_ATTEMPTS - 1) {
2346
- await new Promise((resolve4) => setTimeout(resolve4, READ_BACK_DELAY_MS));
3203
+ await new Promise((resolve5) => setTimeout(resolve5, READ_BACK_DELAY_MS));
2347
3204
  }
2348
3205
  }
2349
3206
  return null;
@@ -2351,57 +3208,68 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2351
3208
  function findAssistantReplyAfter(messages, userMessageId) {
2352
3209
  if (!messages || messages.length === 0) return null;
2353
3210
  const byParent = messages.find(
2354
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3211
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2355
3212
  );
2356
3213
  if (byParent) return byParent;
2357
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
3214
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2358
3215
  if (userIndex === -1) return null;
2359
3216
  for (let i = userIndex + 1; i < messages.length; i++) {
2360
- if (roleOf(messages[i]) === "assistant") return messages[i];
3217
+ if (roleOf2(messages[i]) === "assistant") return messages[i];
2361
3218
  }
2362
3219
  return null;
2363
3220
  }
2364
3221
  function findLastAssistantReplyFor(messages, userMessageId) {
2365
3222
  if (!messages || messages.length === 0) return null;
2366
3223
  let lastCorrelated = null;
2367
- let lastNonErrored = null;
2368
3224
  for (let i = messages.length - 1; i >= 0; i--) {
2369
3225
  const m = messages[i];
2370
- if (roleOf(m) !== "assistant" || parentIdOf(m) !== userMessageId) continue;
2371
- if (lastCorrelated === null) lastCorrelated = m;
2372
- if (errorOf(m) == null) {
2373
- lastNonErrored = m;
2374
- break;
3226
+ if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
3227
+ lastCorrelated = m;
3228
+ break;
3229
+ }
3230
+ if (lastCorrelated) {
3231
+ if (errorOf(lastCorrelated) == null) return lastCorrelated;
3232
+ for (let i = messages.length - 1; i >= 0; i--) {
3233
+ const m = messages[i];
3234
+ if (roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId && isTerminalStopAnswer(m)) {
3235
+ return m;
3236
+ }
2375
3237
  }
3238
+ return lastCorrelated;
2376
3239
  }
2377
- if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
2378
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
3240
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2379
3241
  if (userIndex === -1) return null;
2380
3242
  let last = null;
2381
- let lastOk = null;
3243
+ let lastAssistantIndex = -1;
2382
3244
  for (let i = userIndex + 1; i < messages.length; i++) {
2383
- const role = roleOf(messages[i]);
3245
+ const role = roleOf2(messages[i]);
2384
3246
  if (role === "user") break;
2385
3247
  if (role === "assistant") {
2386
3248
  last = messages[i];
2387
- if (errorOf(messages[i]) == null) lastOk = messages[i];
3249
+ lastAssistantIndex = i;
2388
3250
  }
2389
3251
  }
2390
- return lastOk ?? last;
3252
+ if (last === null || errorOf(last) == null) return last;
3253
+ for (let i = lastAssistantIndex; i > userIndex; i--) {
3254
+ const m = messages[i];
3255
+ if (roleOf2(m) === "user") break;
3256
+ if (roleOf2(m) === "assistant" && isTerminalStopAnswer(m)) return m;
3257
+ }
3258
+ return last;
2391
3259
  }
2392
3260
  function collectSubagentSessions(messages, userMessageId) {
2393
3261
  if (!messages || messages.length === 0) return [];
2394
3262
  const byParent = messages.filter(
2395
- (message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
3263
+ (message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
2396
3264
  );
2397
3265
  const assistants = byParent.length > 0 ? byParent : [];
2398
3266
  if (assistants.length === 0) {
2399
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3267
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2400
3268
  if (userIndex === -1) return [];
2401
3269
  for (let i = userIndex + 1; i < messages.length; i++) {
2402
3270
  const message = messages[i];
2403
- if (roleOf(message) === "user") break;
2404
- if (roleOf(message) === "assistant") assistants.push(message);
3271
+ if (roleOf2(message) === "user") break;
3272
+ if (roleOf2(message) === "assistant") assistants.push(message);
2405
3273
  }
2406
3274
  }
2407
3275
  const refs = [];
@@ -2441,7 +3309,7 @@ function collectTaskCalls(messages, userMessageId) {
2441
3309
  if (!messages || messages.length === 0) return [];
2442
3310
  const calls = [];
2443
3311
  for (const message of messages) {
2444
- if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
3312
+ if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
2445
3313
  for (const part of message.parts ?? []) {
2446
3314
  if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
2447
3315
  continue;
@@ -2471,7 +3339,7 @@ function attributeTaskCallUsage(messages, windows) {
2471
3339
  for (const window of eligibleWindows) assignments.set(window.callID, []);
2472
3340
  const unattributed = [];
2473
3341
  for (const message of messages ?? []) {
2474
- if (roleOf(message) !== "assistant") continue;
3342
+ if (roleOf2(message) !== "assistant") continue;
2475
3343
  const created = finiteNumber(createdOf(message));
2476
3344
  const matching = created === null ? [] : eligibleWindows.filter(
2477
3345
  (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
@@ -2493,8 +3361,9 @@ function attributeTaskCallUsage(messages, windows) {
2493
3361
  }
2494
3362
  function sumAssistantUsage(messages) {
2495
3363
  if (!messages || messages.length === 0) return null;
2496
- const nonErrored = messages.filter((message) => errorOf(message) == null);
2497
- const selected = nonErrored.length > 0 ? nonErrored : messages;
3364
+ const hasTrailingError = errorOf(messages[messages.length - 1]) != null;
3365
+ const hasTerminalStopAnswer = messages.some(isTerminalStopAnswer);
3366
+ const selected = hasTrailingError && hasTerminalStopAnswer ? messages.filter((message) => errorOf(message) == null) : messages;
2498
3367
  let sawAnyUsage = false;
2499
3368
  let inputSum = 0;
2500
3369
  let outputSum = 0;
@@ -2503,9 +3372,11 @@ function sumAssistantUsage(messages) {
2503
3372
  let cacheWriteSum = 0;
2504
3373
  let costSum = 0;
2505
3374
  let sawCost = false;
3375
+ let toolCallCount = 0;
2506
3376
  let modelId = null;
2507
3377
  let providerId = null;
2508
3378
  for (const m of selected) {
3379
+ toolCallCount += m.parts?.filter((part) => part.type === "tool").length ?? 0;
2509
3380
  const info = m.info;
2510
3381
  if (!info) continue;
2511
3382
  const tokens = info.tokens;
@@ -2542,19 +3413,18 @@ function sumAssistantUsage(messages) {
2542
3413
  usage_tokens_cache_write: cacheWriteSum,
2543
3414
  // NULL means OpenCode never reported a cost; it is distinct from a genuine
2544
3415
  // zero-cost message, which sets `sawCost` with `costSum === 0`.
2545
- usage_cost_usd: sawCost ? costSum : null
3416
+ usage_cost_usd: sawCost ? costSum : null,
3417
+ turn_tool_calls: toolCallCount
2546
3418
  };
2547
3419
  }
2548
3420
  function messageUsage(messages, userMessageId) {
2549
3421
  if (!messages || messages.length === 0) return null;
2550
3422
  const byParentAll = messages.filter(
2551
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3423
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2552
3424
  );
2553
- const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
2554
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
2555
3425
  let correlated;
2556
- if (byParent.length > 0) {
2557
- correlated = byParent;
3426
+ if (byParentAll.length > 0) {
3427
+ correlated = byParentAll;
2558
3428
  } else {
2559
3429
  const reply = findAssistantReplyAfter(messages, userMessageId);
2560
3430
  correlated = reply ? [reply] : [];
@@ -2564,7 +3434,7 @@ function messageUsage(messages, userMessageId) {
2564
3434
  }
2565
3435
  function messageRunState(messages, userMessageId) {
2566
3436
  if (!messages || messages.length === 0) return "unknown";
2567
- const hasUser = messages.some((m) => idOf(m) === userMessageId);
3437
+ const hasUser = messages.some((m) => idOf2(m) === userMessageId);
2568
3438
  const reply = findLastAssistantReplyFor(messages, userMessageId);
2569
3439
  if (!hasUser) {
2570
3440
  if (!reply) return "unknown";
@@ -2600,14 +3470,38 @@ function messageError(messages, userMessageId) {
2600
3470
  const reply = findLastAssistantReplyFor(messages, userMessageId);
2601
3471
  const error2 = errorOf(reply);
2602
3472
  if (error2 == null) return null;
2603
- if (typeof error2 === "string") return error2;
2604
- if (typeof error2 === "object") {
3473
+ let rendered;
3474
+ if (typeof error2 === "string") {
3475
+ rendered = error2;
3476
+ } else if (typeof error2 === "object") {
2605
3477
  const e = error2;
2606
3478
  const dataMessage = e.data?.message;
2607
- if (typeof dataMessage === "string") return dataMessage;
2608
- if (typeof e.message === "string") return e.message;
3479
+ if (typeof dataMessage === "string") {
3480
+ rendered = dataMessage;
3481
+ } else if (typeof e.message === "string") {
3482
+ rendered = e.message;
3483
+ } else {
3484
+ rendered = "The agent run failed.";
3485
+ }
3486
+ } else {
3487
+ rendered = "The agent run failed.";
2609
3488
  }
2610
- return "The agent run failed.";
3489
+ const toolPart = Array.isArray(reply?.parts) ? reply.parts.find(
3490
+ (part) => part && typeof part === "object" && part.type === "tool" && part.state?.status === "error"
3491
+ ) : void 0;
3492
+ return typeof toolPart?.tool === "string" && toolPart.tool.length > 0 ? `${rendered} (tool "${toolPart.tool}")` : rendered;
3493
+ }
3494
+ function describeAbandonedReplyShape(messages, userMessageId) {
3495
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
3496
+ if (!reply) return null;
3497
+ const parts = Array.isArray(reply.parts) ? reply.parts : [];
3498
+ const lastTool = [...parts].reverse().find((part) => part?.type === "tool");
3499
+ return {
3500
+ partTypes: parts.slice(-12).map((part) => typeof part?.type === "string" ? part.type : "(unknown)"),
3501
+ droppedPartCount: Math.max(0, parts.length - 12),
3502
+ lastToolName: typeof lastTool?.tool === "string" && lastTool.tool.length > 0 ? lastTool.tool : null,
3503
+ lastToolStatus: typeof lastTool?.state?.status === "string" && lastTool.state.status.length > 0 ? lastTool.state.status : null
3504
+ };
2611
3505
  }
2612
3506
  function isAbortedTerminalReply(messages, userMessageId) {
2613
3507
  const reply = findLastAssistantReplyFor(messages, userMessageId);
@@ -2624,19 +3518,32 @@ function isAbortedTerminalReply(messages, userMessageId) {
2624
3518
  }
2625
3519
  return false;
2626
3520
  }
3521
+ var CLAUDE_AUTH_EXPIRY_MESSAGES = Object.freeze([
3522
+ "Claude Code credentials are unavailable or expired. Run `claude` to refresh them."
3523
+ ]);
3524
+ function extractReplyErrorShape(reply) {
3525
+ const error2 = errorOf(reply);
3526
+ if (error2 == null || typeof error2 !== "object") {
3527
+ return { name: null, statusCode: null };
3528
+ }
3529
+ const e = error2;
3530
+ const name = typeof e.name === "string" ? e.name.slice(0, 100) : null;
3531
+ const data = e.data;
3532
+ const statusCode = typeof data?.statusCode === "number" && Number.isFinite(data.statusCode) ? data.statusCode : null;
3533
+ return { name, statusCode };
3534
+ }
2627
3535
  function classifyReplyAuthError(reply) {
2628
3536
  const error2 = errorOf(reply);
2629
3537
  if (error2 == null || typeof error2 !== "object") return null;
2630
3538
  const e = error2;
2631
3539
  const replyProviderId = reply?.info?.providerID ?? null;
2632
3540
  const replyModelId = reply?.info?.modelID ?? null;
3541
+ const data = e.data;
2633
3542
  if (e.name === "ProviderAuthError") {
2634
- const data = e.data;
2635
3543
  const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
2636
3544
  return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
2637
3545
  }
2638
3546
  if (e.name === "APIError") {
2639
- const data = e.data;
2640
3547
  const statusCode = data?.statusCode;
2641
3548
  if (statusCode === 401 || statusCode === 403) {
2642
3549
  return {
@@ -2647,16 +3554,28 @@ function classifyReplyAuthError(reply) {
2647
3554
  };
2648
3555
  }
2649
3556
  }
3557
+ const errorMessage3 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
3558
+ if (errorMessage3 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage3.includes(knownMessage))) {
3559
+ return {
3560
+ kind: "model_auth",
3561
+ providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
3562
+ modelId: replyModelId,
3563
+ reason: "rejected"
3564
+ };
3565
+ }
2650
3566
  return null;
2651
3567
  }
2652
3568
  function messageFailure(messages, userMessageId) {
2653
3569
  return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
2654
3570
  }
3571
+ function isProviderModelNotFoundError(reason) {
3572
+ return reason.includes("Model not found:");
3573
+ }
2655
3574
  function findLatestSubagentAuthOutcome(messages, sinceMs) {
2656
3575
  if (!messages || messages.length === 0) return null;
2657
3576
  for (let i = messages.length - 1; i >= 0; i--) {
2658
3577
  const message = messages[i];
2659
- if (roleOf(message) !== "assistant") continue;
3578
+ if (roleOf2(message) !== "assistant") continue;
2660
3579
  const created = createdOf(message);
2661
3580
  if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
2662
3581
  const failure = classifyReplyAuthError(message);
@@ -2688,25 +3607,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
2688
3607
  function hasRunningAssistantExcept(messages, exceptUserMessageId) {
2689
3608
  if (!messages || messages.length === 0) return false;
2690
3609
  return messages.some(
2691
- (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3610
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
2692
3611
  );
2693
3612
  }
2694
3613
  function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
2695
3614
  if (!messages || messages.length === 0) return false;
2696
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3615
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2697
3616
  if (userIndex === -1) return false;
2698
3617
  let hasLaterUser = false;
2699
3618
  let hasStartedLaterUser = false;
2700
3619
  for (let i = userIndex + 1; i < messages.length; i++) {
2701
3620
  const message = messages[i];
2702
- if (roleOf(message) !== "user") continue;
3621
+ if (roleOf2(message) !== "user") continue;
2703
3622
  hasLaterUser = true;
2704
- const laterUserMessageId = idOf(message);
3623
+ const laterUserMessageId = idOf2(message);
2705
3624
  if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
2706
3625
  return false;
2707
3626
  }
2708
3627
  if (messages.some(
2709
- (candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
3628
+ (candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
2710
3629
  )) {
2711
3630
  hasStartedLaterUser = true;
2712
3631
  }
@@ -3177,13 +4096,28 @@ async function listV2Sessions(client) {
3177
4096
  }
3178
4097
  async function deleteV2Session(client, sessionId) {
3179
4098
  try {
3180
- await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3181
- return true;
4099
+ const response = await client.request(
4100
+ `/api/session/${encodeURIComponent(sessionId)}`,
4101
+ { method: "DELETE" },
4102
+ { allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
4103
+ );
4104
+ if (response.status >= 200 && response.status < 300) {
4105
+ return { ok: true, status: response.status };
4106
+ }
4107
+ try {
4108
+ const body = await response.text();
4109
+ return body ? { ok: false, status: response.status, body } : { ok: false, status: response.status };
4110
+ } catch (error2) {
4111
+ console.error(
4112
+ `[deleteV2Session] DELETE /api/session/${sessionId} response body read failed for HTTP ${response.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
4113
+ );
4114
+ return { ok: false, status: response.status };
4115
+ }
3182
4116
  } catch (error2) {
3183
4117
  console.error(
3184
4118
  `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3185
4119
  );
3186
- return false;
4120
+ return { ok: false, status: "threw" };
3187
4121
  }
3188
4122
  }
3189
4123
  async function v2SessionExists(client, sessionId) {
@@ -3354,12 +4288,12 @@ async function readV2SessionErrorStream(client, options) {
3354
4288
 
3355
4289
  // src/lib/opencode/credential-import-v2.ts
3356
4290
  import { readFileSync as readFileSync4 } from "node:fs";
3357
- import { join as join4 } from "node:path";
4291
+ import { join as join5 } from "node:path";
3358
4292
 
3359
4293
  // src/lib/openai-usage.ts
3360
4294
  import { readFileSync as readFileSync3 } from "node:fs";
3361
4295
  import { homedir as homedir3 } from "node:os";
3362
- import { join as join3 } from "node:path";
4296
+ import { join as join4 } from "node:path";
3363
4297
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3364
4298
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
3365
4299
  var OpenAiUsageError = class extends Error {
@@ -3373,7 +4307,7 @@ function isLocalCredentialProblem2(err) {
3373
4307
  }
3374
4308
  function readOpenCodeChatGptCredentials() {
3375
4309
  try {
3376
- const raw = readFileSync3(join3(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4310
+ const raw = readFileSync3(join4(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3377
4311
  let parsed;
3378
4312
  try {
3379
4313
  parsed = JSON.parse(raw);
@@ -3599,13 +4533,13 @@ var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
3599
4533
  function isRecord3(value) {
3600
4534
  return typeof value === "object" && value !== null && !Array.isArray(value);
3601
4535
  }
3602
- var sleep2 = (milliseconds) => new Promise((resolve4) => setTimeout(resolve4, milliseconds));
4536
+ var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
3603
4537
  function emptyResult(unsupported = [], outcome = "completed") {
3604
4538
  return { outcome, imported: [], alreadyConnected: [], unsupported, failed: [] };
3605
4539
  }
3606
4540
  function readOpenCodeCredentials(homeDir, log3) {
3607
4541
  try {
3608
- const raw = readFileSync4(join4(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
4542
+ const raw = readFileSync4(join5(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
3609
4543
  let parsed;
3610
4544
  try {
3611
4545
  parsed = JSON.parse(raw);
@@ -3805,9 +4739,9 @@ async function importOpenCodeV2Credentials({
3805
4739
 
3806
4740
  // src/lib/opencode/session-db-boot.ts
3807
4741
  import { spawn as spawn2 } from "node:child_process";
3808
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
4742
+ import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
3809
4743
  import { homedir as homedir4 } from "node:os";
3810
- import { dirname as dirname2, resolve as resolvePath } from "node:path";
4744
+ import { dirname as dirname4, resolve as resolvePath } from "node:path";
3811
4745
 
3812
4746
  // src/lib/runner-synchroniser.ts
3813
4747
  import { spawn } from "node:child_process";
@@ -3817,7 +4751,7 @@ function appendError(stderr, error2) {
3817
4751
  ${message}`;
3818
4752
  }
3819
4753
  function runSynchroniser(args, opts) {
3820
- return new Promise((resolve4) => {
4754
+ return new Promise((resolve5) => {
3821
4755
  let child;
3822
4756
  let stdout = "";
3823
4757
  let stderr = "";
@@ -3831,7 +4765,7 @@ function runSynchroniser(args, opts) {
3831
4765
  if (timer.handle) clearTimeout(timer.handle);
3832
4766
  if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
3833
4767
  if (spawnListener) child.removeListener("spawn", spawnListener);
3834
- resolve4(result);
4768
+ resolve5(result);
3835
4769
  };
3836
4770
  try {
3837
4771
  child = spawn("runner-synchroniser", args, {
@@ -3887,9 +4821,12 @@ function runSynchroniser(args, opts) {
3887
4821
 
3888
4822
  // src/lib/opencode/session-db-boot.ts
3889
4823
  var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
3890
- var SESSION_DB_VERIFY_TIMEOUT_MS = 12e4;
4824
+ var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
3891
4825
  var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
3892
- var SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS = 120;
4826
+ var SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS = 40;
4827
+ var SESSION_DB_VERIFY_RESTORE_POINTS_TIMEOUT_SECONDS = 30;
4828
+ var SESSION_DB_VERIFY_RESTORE_CANDIDATE_TIMEOUT_SECONDS = 120;
4829
+ var SESSION_DB_VERIFY_DISPOSAL_BUDGET_SECONDS = 90;
3893
4830
  function commandError(result) {
3894
4831
  return result.stderr.trim() || `process exited with code ${result.code ?? "null"}`;
3895
4832
  }
@@ -3932,7 +4869,7 @@ function clearMarker(options) {
3932
4869
  function markNoReplicate(options, message) {
3933
4870
  if (options.noReplicateMarker) {
3934
4871
  try {
3935
- mkdirSync(dirname2(options.noReplicateMarker), { recursive: true });
4872
+ mkdirSync(dirname4(options.noReplicateMarker), { recursive: true });
3936
4873
  writeFileSync(options.noReplicateMarker, "");
3937
4874
  } catch (error2) {
3938
4875
  options.log(
@@ -3991,7 +4928,7 @@ function parseSynchroniserEnv(stdout) {
3991
4928
  return values;
3992
4929
  }
3993
4930
  function runCommand(command, args, options) {
3994
- return new Promise((resolve4) => {
4931
+ return new Promise((resolve5) => {
3995
4932
  let child;
3996
4933
  let stdout = "";
3997
4934
  let stderr = "";
@@ -4000,7 +4937,7 @@ function runCommand(command, args, options) {
4000
4937
  if (settled) return;
4001
4938
  settled = true;
4002
4939
  if (timer) clearTimeout(timer);
4003
- resolve4(result);
4940
+ resolve5(result);
4004
4941
  };
4005
4942
  try {
4006
4943
  child = spawn2(command, args, {
@@ -4008,7 +4945,7 @@ function runCommand(command, args, options) {
4008
4945
  stdio: ["ignore", "pipe", "pipe"]
4009
4946
  });
4010
4947
  } catch (error2) {
4011
- resolve4({
4948
+ resolve5({
4012
4949
  code: null,
4013
4950
  stdout,
4014
4951
  stderr: error2 instanceof Error ? error2.message : String(error2),
@@ -4057,7 +4994,7 @@ async function ensureLitestreamConfig(options, env) {
4057
4994
  return null;
4058
4995
  }
4059
4996
  try {
4060
- if (statSync2(configPath).size > 0) return configPath;
4997
+ if (statSync3(configPath).size > 0) return configPath;
4061
4998
  } catch (error2) {
4062
4999
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
4063
5000
  options.log(
@@ -4087,7 +5024,7 @@ async function ensureLitestreamConfig(options, env) {
4087
5024
  return null;
4088
5025
  }
4089
5026
  try {
4090
- mkdirSync(dirname2(configPath), { recursive: true });
5027
+ mkdirSync(dirname4(configPath), { recursive: true });
4091
5028
  writeFileSync(configPath, rendered.stdout);
4092
5029
  } catch (error2) {
4093
5030
  options.log(
@@ -4200,20 +5137,37 @@ async function verifySessionDb(options, configPath, env) {
4200
5137
  timeoutMs: SESSION_DB_VERIFY_TIMEOUT_MS,
4201
5138
  env: {
4202
5139
  ...env,
4203
- // The synchroniser reads this value in SECONDS. Keep this at 120, not
4204
- // 120_000, so the walkback gives up before the outer process bound.
5140
+ // The synchroniser reads all four values in SECONDS.
4205
5141
  EVIDENT_SESSION_DB_WALKBACK_BUDGET_SECONDS: String(
4206
5142
  SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS
5143
+ ),
5144
+ EVIDENT_SESSION_DB_RESTORE_POINTS_TIMEOUT_SECONDS: String(
5145
+ SESSION_DB_VERIFY_RESTORE_POINTS_TIMEOUT_SECONDS
5146
+ ),
5147
+ EVIDENT_SESSION_DB_RESTORE_CANDIDATE_TIMEOUT_SECONDS: String(
5148
+ SESSION_DB_VERIFY_RESTORE_CANDIDATE_TIMEOUT_SECONDS
5149
+ ),
5150
+ EVIDENT_SESSION_DB_DISPOSAL_BUDGET_SECONDS: String(
5151
+ SESSION_DB_VERIFY_DISPOSAL_BUDGET_SECONDS
4207
5152
  )
4208
5153
  }
4209
5154
  });
4210
5155
  logSynchroniserDiagnostics(result, options);
4211
5156
  if (result.timedOut || result.code === 124 || result.code === 137) {
5157
+ if (result.stderr.includes("SESSION-DB-INTEGRITY-FAILED")) {
5158
+ const reason = result.stderr.includes("SESSION-DB-REPLICA-SIZE") ? "verification_timeout_during_separation" : "verification_timeout_before_separation";
5159
+ options.log(
5160
+ `SESSION-DB-VERIFY-TIMEOUT: verification did not finish within its ${SESSION_DB_VERIFY_TIMEOUT_MS}ms deadline; refusing to start with the restored opencode.db (${reason})`,
5161
+ "error"
5162
+ );
5163
+ reportRecord("verify", "session_db_boot_refused", reason, null, options);
5164
+ return { verifyFatal: true, verifyFatalReason: reason };
5165
+ }
4212
5166
  options.log(
4213
5167
  `SESSION-DB-VERIFY-TIMEOUT: verification did not finish within its ${SESSION_DB_VERIFY_TIMEOUT_MS}ms deadline; continuing with the restored opencode.db as-is, unverified`,
4214
5168
  "warn"
4215
5169
  );
4216
- return false;
5170
+ return { verifyFatal: false };
4217
5171
  }
4218
5172
  if (result.code === 34) {
4219
5173
  reportRecord(
@@ -4223,14 +5177,14 @@ async function verifySessionDb(options, configPath, env) {
4223
5177
  null,
4224
5178
  options
4225
5179
  );
4226
- return true;
5180
+ return { verifyFatal: true };
4227
5181
  }
4228
5182
  if (result.code === 33) {
4229
5183
  options.log(
4230
5184
  "SESSION-DB-INTEGRITY-EXHAUSTED: booting continues and litestream still replicates, starting an empty backup chain after the corrupt replica was separated.",
4231
5185
  "warn"
4232
5186
  );
4233
- return false;
5187
+ return { verifyFatal: false };
4234
5188
  }
4235
5189
  if (result.code !== 0) {
4236
5190
  options.log(
@@ -4238,17 +5192,17 @@ async function verifySessionDb(options, configPath, env) {
4238
5192
  "warn"
4239
5193
  );
4240
5194
  }
4241
- return false;
5195
+ return { verifyFatal: false };
4242
5196
  }
4243
5197
  options.log(
4244
5198
  "skipping session-DB verification: this boot's session DB was not proven safe to replicate",
4245
5199
  "debug"
4246
5200
  );
4247
- return false;
5201
+ return { verifyFatal: false };
4248
5202
  }
4249
5203
  function fileExists(path) {
4250
5204
  try {
4251
- statSync2(path);
5205
+ statSync3(path);
4252
5206
  return true;
4253
5207
  } catch (error2) {
4254
5208
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
@@ -4331,13 +5285,13 @@ async function restoreAndVerifySessionDb(options) {
4331
5285
  if (options.noReplicateMarker && fileExists(options.noReplicateMarker)) {
4332
5286
  return { verifyFatal: false };
4333
5287
  }
4334
- return { verifyFatal: await verifySessionDb(options, configPath, env) };
5288
+ return verifySessionDb(options, configPath, env);
4335
5289
  }
4336
5290
 
4337
5291
  // src/lib/opencode/session-db-provenance.ts
4338
5292
  import { createRequire } from "node:module";
4339
5293
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
4340
- import { dirname as dirname3, join as join5 } from "node:path";
5294
+ import { dirname as dirname5, join as join6 } from "node:path";
4341
5295
  var require2 = createRequire(import.meta.url);
4342
5296
  function readSessionDbMigrationIds(dbPath) {
4343
5297
  let db;
@@ -4374,7 +5328,7 @@ function readSessionDbMigrationIds(dbPath) {
4374
5328
  }
4375
5329
  function sessionDbProvenanceStatePath(homeDir, env) {
4376
5330
  const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
4377
- return override || join5(homeDir, ".local", "state", "evident", "session-db-provenance.json");
5331
+ return override || join6(homeDir, ".local", "state", "evident", "session-db-provenance.json");
4378
5332
  }
4379
5333
  function loadSessionDbProvenanceState(path) {
4380
5334
  let value;
@@ -4403,7 +5357,7 @@ function loadSessionDbProvenanceState(path) {
4403
5357
  }
4404
5358
  function saveSessionDbProvenanceState(path, state) {
4405
5359
  try {
4406
- mkdirSync2(dirname3(path), { recursive: true });
5360
+ mkdirSync2(dirname5(path), { recursive: true });
4407
5361
  writeFileSync2(path, `${JSON.stringify(state, null, 2)}
4408
5362
  `, "utf8");
4409
5363
  } catch (error2) {
@@ -4494,7 +5448,7 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
4494
5448
  if (child.exitCode !== null || child.signalCode !== null) {
4495
5449
  return { outcome: "exited", code: child.exitCode, signal: child.signalCode };
4496
5450
  }
4497
- return new Promise((resolve4, reject) => {
5451
+ return new Promise((resolve5, reject) => {
4498
5452
  let forced = false;
4499
5453
  let settled = false;
4500
5454
  const timer = setTimeout(() => {
@@ -4514,7 +5468,7 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
4514
5468
  settled = true;
4515
5469
  clearTimeout(timer);
4516
5470
  child.removeListener("exit", onExit);
4517
- resolve4(result);
5471
+ resolve5(result);
4518
5472
  };
4519
5473
  const fail = (error2) => {
4520
5474
  if (settled) return;
@@ -4984,10 +5938,13 @@ function selectSessionsToDelete(sessions, opts) {
4984
5938
  toDelete.push(s.id);
4985
5939
  }
4986
5940
  }
4987
- return toDelete;
5941
+ const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
5942
+ return toDelete.sort(
5943
+ (a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
5944
+ );
4988
5945
  }
4989
5946
  var DEFAULT_INTERVAL = "1h";
4990
- function resolve(flag, envValue, fallback) {
5947
+ function resolve2(flag, envValue, fallback) {
4991
5948
  return flag ?? envValue ?? fallback;
4992
5949
  }
4993
5950
  function parseMaxCount(input) {
@@ -5003,9 +5960,9 @@ function parseMaxCount(input) {
5003
5960
  }
5004
5961
  function resolveSessionCleanupConfig(flags, env = process.env) {
5005
5962
  const warnings = [];
5006
- const maxAgeRaw = resolve(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
5007
- const maxCountRaw = resolve(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
5008
- const intervalRaw = resolve(
5963
+ const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
5964
+ const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
5965
+ const intervalRaw = resolve2(
5009
5966
  flags.interval,
5010
5967
  env.EVIDENT_SESSION_CLEANUP_INTERVAL,
5011
5968
  DEFAULT_INTERVAL
@@ -5044,13 +6001,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
5044
6001
  }
5045
6002
 
5046
6003
  // src/lib/opencode/session-db-size.ts
5047
- import { statSync as statSync3 } from "node:fs";
5048
- import { join as join6 } from "node:path";
6004
+ import { statSync as statSync4 } from "node:fs";
6005
+ import { join as join7 } from "node:path";
5049
6006
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
5050
6007
  function statSessionDbBytes(homeDir) {
5051
- const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
6008
+ const dbPath = join7(homeDir, ".local", "share", "opencode", "opencode.db");
5052
6009
  try {
5053
- return statSync3(dbPath).size;
6010
+ return statSync4(dbPath).size;
5054
6011
  } catch (err) {
5055
6012
  const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
5056
6013
  if (!isMissingFile) {
@@ -5076,14 +6033,14 @@ function buildSessionStoreSizeWarning(input) {
5076
6033
  }
5077
6034
 
5078
6035
  // src/lib/opencode/log-tail.ts
5079
- import { statSync as statSync4 } from "node:fs";
6036
+ import { statSync as statSync5 } from "node:fs";
5080
6037
  import { homedir as homedir5 } from "node:os";
5081
- import { join as join7 } from "node:path";
6038
+ import { join as join8 } from "node:path";
5082
6039
  import { open as open2, stat } from "node:fs/promises";
5083
6040
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
5084
6041
  function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
5085
- const dataDir = env.XDG_DATA_HOME || join7(homeDir, ".local", "share");
5086
- return join7(dataDir, "opencode", "log", "opencode.log");
6042
+ const dataDir = env.XDG_DATA_HOME || join8(homeDir, ".local", "share");
6043
+ return join8(dataDir, "opencode", "log", "opencode.log");
5087
6044
  }
5088
6045
  function isEnoent(error2) {
5089
6046
  return error2?.code === "ENOENT";
@@ -5099,7 +6056,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
5099
6056
  let inode = null;
5100
6057
  let baselineReady = true;
5101
6058
  try {
5102
- const initial = statSync4(logPath);
6059
+ const initial = statSync5(logPath);
5103
6060
  offset = initial.size;
5104
6061
  inode = initial.ino;
5105
6062
  } catch (error2) {
@@ -5158,129 +6115,6 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
5158
6115
  };
5159
6116
  }
5160
6117
 
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
6118
  // src/lib/tunnel/connection.ts
5285
6119
  import WebSocket2 from "ws";
5286
6120
 
@@ -5361,8 +6195,8 @@ var StreamForwarder = class {
5361
6195
  pauseStream(sid, stream) {
5362
6196
  if (!stream.paused) {
5363
6197
  stream.paused = true;
5364
- stream.pausePromise = new Promise((resolve4) => {
5365
- stream.resume = resolve4;
6198
+ stream.pausePromise = new Promise((resolve5) => {
6199
+ stream.resume = resolve5;
5366
6200
  });
5367
6201
  }
5368
6202
  if (stream.pauseTimer !== null) clearTimeout(stream.pauseTimer);
@@ -5429,12 +6263,12 @@ var StreamForwarder = class {
5429
6263
  let endBody;
5430
6264
  if (has_body) {
5431
6265
  const chunks = [];
5432
- bodyPromise = new Promise((resolve4) => {
6266
+ bodyPromise = new Promise((resolve5) => {
5433
6267
  pushBody = (buf) => {
5434
6268
  chunks.push(buf);
5435
6269
  };
5436
6270
  endBody = () => {
5437
- resolve4(Buffer.concat(chunks));
6271
+ resolve5(Buffer.concat(chunks));
5438
6272
  };
5439
6273
  });
5440
6274
  }
@@ -5554,6 +6388,7 @@ function classifyUpgradeRejection(headers) {
5554
6388
  }
5555
6389
  var MAX_RECONNECT_DELAY = 3e4;
5556
6390
  var BASE_RECONNECT_DELAY = 500;
6391
+ var RELAY_SILENCE_TIMEOUT_MS = 9e4;
5557
6392
  function getReconnectDelay(attempt) {
5558
6393
  const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
5559
6394
  const jitter = Math.random() * 1e3;
@@ -5604,7 +6439,7 @@ function connectTunnel(options) {
5604
6439
  } = options;
5605
6440
  const tunnelUrl = getTunnelUrlConfig();
5606
6441
  const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
5607
- return new Promise((resolve4, reject) => {
6442
+ return new Promise((resolve5, reject) => {
5608
6443
  const ws = new WebSocket2(url, {
5609
6444
  headers: {
5610
6445
  Authorization: authHeader,
@@ -5625,10 +6460,27 @@ function connectTunnel(options) {
5625
6460
  ws.close();
5626
6461
  reject(new Error("Connection timeout"));
5627
6462
  }, 3e4);
6463
+ let relaySilenceTimeout;
6464
+ let relaySilenceTimedOut = false;
6465
+ const clearRelaySilenceTimeout = () => {
6466
+ if (relaySilenceTimeout === void 0) return;
6467
+ clearTimeout(relaySilenceTimeout);
6468
+ relaySilenceTimeout = void 0;
6469
+ };
6470
+ const armRelaySilenceTimeout = () => {
6471
+ clearRelaySilenceTimeout();
6472
+ relaySilenceTimeout = setTimeout(() => {
6473
+ relaySilenceTimeout = void 0;
6474
+ relaySilenceTimedOut = true;
6475
+ ws.terminate();
6476
+ }, RELAY_SILENCE_TIMEOUT_MS);
6477
+ relaySilenceTimeout.unref?.();
6478
+ };
5628
6479
  let upgradeRejection = null;
5629
6480
  let upgradeRejectionReason = null;
5630
6481
  ws.on("unexpected-response", (_req, res) => {
5631
6482
  clearTimeout(connectionTimeout);
6483
+ clearRelaySilenceTimeout();
5632
6484
  const reason = classifyUpgradeRejection(res.headers);
5633
6485
  upgradeRejectionReason = reason;
5634
6486
  const chunks = [];
@@ -5658,6 +6510,7 @@ function connectTunnel(options) {
5658
6510
  onInfo?.("WebSocket connection established");
5659
6511
  });
5660
6512
  ws.on("message", (data, isBinary) => {
6513
+ if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
5661
6514
  if (isBinary) {
5662
6515
  try {
5663
6516
  const frame = decodeBinaryBodyFrame(toUint8Array(data));
@@ -5691,10 +6544,11 @@ function connectTunnel(options) {
5691
6544
  switch (message.type) {
5692
6545
  case "connected": {
5693
6546
  clearTimeout(connectionTimeout);
6547
+ armRelaySilenceTimeout();
5694
6548
  forwarder.setBinaryFramesSupported(message.binary_frames === true);
5695
6549
  const connectedAgentId = message.agent_id ?? agentId;
5696
6550
  onConnected?.(connectedAgentId);
5697
- resolve4({
6551
+ resolve5({
5698
6552
  ws,
5699
6553
  close: () => ws.close(1e3, "CLI shutdown")
5700
6554
  });
@@ -5715,6 +6569,7 @@ function connectTunnel(options) {
5715
6569
  });
5716
6570
  ws.on("error", (error2) => {
5717
6571
  clearTimeout(connectionTimeout);
6572
+ clearRelaySilenceTimeout();
5718
6573
  const detail = upgradeRejection ?? describeSocketError(error2, url);
5719
6574
  if (upgradeRejectionReason === "do_code_updated") {
5720
6575
  onWarning?.("Relay redeployed \u2014 reconnecting");
@@ -5726,7 +6581,8 @@ function connectTunnel(options) {
5726
6581
  );
5727
6582
  });
5728
6583
  ws.on("close", (code, reason) => {
5729
- const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
6584
+ clearRelaySilenceTimeout();
6585
+ const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
5730
6586
  forwarder.abortAll();
5731
6587
  onDisconnected?.(code, reasonStr);
5732
6588
  });
@@ -5734,7 +6590,7 @@ function connectTunnel(options) {
5734
6590
  }
5735
6591
 
5736
6592
  // src/lib/tunnel/runner-connection.ts
5737
- var RunnerConnection = class {
6593
+ var RunnerConnection = class _RunnerConnection {
5738
6594
  opts;
5739
6595
  sleep;
5740
6596
  now;
@@ -5748,6 +6604,10 @@ var RunnerConnection = class {
5748
6604
  reconnectAttempt = 0;
5749
6605
  generation = 0;
5750
6606
  disconnectedAtMs = null;
6607
+ static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
6608
+ "No pong received",
6609
+ "Orphaned socket"
6610
+ ]);
5751
6611
  constructor(opts) {
5752
6612
  this.opts = opts;
5753
6613
  this.resolvedAgentId = opts.agentId;
@@ -5804,7 +6664,8 @@ var RunnerConnection = class {
5804
6664
  onDisconnected: (code, reason) => {
5805
6665
  this.disconnectedAtMs = this.now();
5806
6666
  events.onDisconnected(code, reason, { generation: this.generation });
5807
- if (this.opts.isRunning() && code !== 1e3 && !this.reconnecting) {
6667
+ const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
6668
+ if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
5808
6669
  this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
5809
6670
  events.onError?.(`Reconnection failed: ${err.message}`);
5810
6671
  });
@@ -6139,7 +7000,102 @@ function createResourceUsageCollector(homeDir) {
6139
7000
  warnings
6140
7001
  };
6141
7002
  },
6142
- stop: cpuPeakSampler.stop
7003
+ stop: cpuPeakSampler.stop
7004
+ };
7005
+ }
7006
+
7007
+ // src/lib/resource-usage-low-disk.ts
7008
+ var RUNNER_DISK_ALERT_ENTRY_FREE_BYTES = 4 * 1024 ** 3;
7009
+ var RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION = 0.25;
7010
+ var RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES = 1 * 1024 ** 3;
7011
+ var RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES = 256 * 1024 ** 2;
7012
+ var RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES = 6 * 1024 ** 3;
7013
+ var RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION = 0.375;
7014
+ var LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
7015
+ var BAND_RANK = {
7016
+ low: 1,
7017
+ very_low: 2,
7018
+ critical: 3
7019
+ };
7020
+ function bandFor(freeBytes) {
7021
+ if (freeBytes < RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES) return "critical";
7022
+ if (freeBytes < RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES) return "very_low";
7023
+ return "low";
7024
+ }
7025
+ function formatBytes(bytes) {
7026
+ if (bytes === null) return "unavailable";
7027
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
7028
+ const unitIndex = Math.min(Math.floor(Math.log2(Math.max(bytes, 1)) / 10), units.length - 1);
7029
+ const value = bytes / 1024 ** unitIndex;
7030
+ const digits = unitIndex === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 2;
7031
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
7032
+ }
7033
+ function warningMessage(band, diskFreeBytes, diskTotalBytes, opencodeDbBytes) {
7034
+ 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.`;
7035
+ }
7036
+ function evaluateLowDiskWarning(input) {
7037
+ const { diskFreeBytes, diskTotalBytes, opencodeDbBytes, nowMs, state } = input;
7038
+ if (diskFreeBytes === null || diskTotalBytes === null || diskTotalBytes <= 0 || diskFreeBytes >= diskTotalBytes) {
7039
+ return { warning: null, state };
7040
+ }
7041
+ const recovered = diskFreeBytes >= RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES || diskFreeBytes >= diskTotalBytes * RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION;
7042
+ if (recovered) {
7043
+ return { warning: null, state: { worstBand: null, warnedAtMs: null } };
7044
+ }
7045
+ const low = diskFreeBytes < RUNNER_DISK_ALERT_ENTRY_FREE_BYTES && diskFreeBytes < diskTotalBytes * RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION;
7046
+ if (!low) return { warning: null, state };
7047
+ const currentBand = bandFor(diskFreeBytes);
7048
+ const currentRank = BAND_RANK[currentBand];
7049
+ const previousRank = state.worstBand === null ? 0 : BAND_RANK[state.worstBand];
7050
+ const worse = currentRank > previousRank;
7051
+ const reassert = state.warnedAtMs !== null && nowMs - state.warnedAtMs >= LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS;
7052
+ const nextState = {
7053
+ worstBand: worse ? currentBand : state.worstBand ?? currentBand,
7054
+ warnedAtMs: worse || state.warnedAtMs === null || reassert ? nowMs : state.warnedAtMs
7055
+ };
7056
+ return {
7057
+ warning: worse || state.warnedAtMs === null || reassert ? warningMessage(nextState.worstBand, diskFreeBytes, diskTotalBytes, opencodeDbBytes) : null,
7058
+ state: nextState
7059
+ };
7060
+ }
7061
+
7062
+ // src/lib/opencode/session-store-health.ts
7063
+ var SESSION_STORE_HEALTH_STALENESS_MS = 5 * 6e4;
7064
+ function createSessionStoreHealth(options = {}) {
7065
+ const now = options.now ?? (() => Date.now());
7066
+ const stalenessMs = Math.max(0, options.stalenessMs ?? SESSION_STORE_HEALTH_STALENESS_MS);
7067
+ let lastUnwritableAt = null;
7068
+ let lastWritableAt = null;
7069
+ const causes = /* @__PURE__ */ new Map();
7070
+ return {
7071
+ observeUnwritable: () => {
7072
+ lastUnwritableAt = now();
7073
+ },
7074
+ observeWritable: () => {
7075
+ lastWritableAt = now();
7076
+ },
7077
+ isUnwritable: () => {
7078
+ if (lastUnwritableAt === null) return false;
7079
+ if (lastWritableAt !== null && lastWritableAt >= lastUnwritableAt) return false;
7080
+ return now() - lastUnwritableAt < stalenessMs;
7081
+ },
7082
+ observeCause: (sessionId, cause) => {
7083
+ if (sessionId === null) return;
7084
+ causes.delete(sessionId);
7085
+ causes.set(sessionId, { cause, observedAt: now() });
7086
+ while (causes.size > 256) {
7087
+ const oldest = causes.keys().next().value;
7088
+ if (typeof oldest !== "string") break;
7089
+ causes.delete(oldest);
7090
+ }
7091
+ },
7092
+ readCause: (sessionId) => {
7093
+ const observed = causes.get(sessionId);
7094
+ if (!observed) return null;
7095
+ causes.delete(sessionId);
7096
+ if (now() - observed.observedAt >= stalenessMs) return null;
7097
+ return observed.cause;
7098
+ }
6143
7099
  };
6144
7100
  }
6145
7101
 
@@ -6147,12 +7103,12 @@ function createResourceUsageCollector(homeDir) {
6147
7103
  import { homedir as homedir6 } from "node:os";
6148
7104
 
6149
7105
  // src/lib/runner-file-sync.ts
6150
- import { join as join9 } from "node:path";
7106
+ import { join as join10 } from "node:path";
6151
7107
 
6152
7108
  // src/lib/file-push.ts
6153
7109
  import { randomUUID } from "node:crypto";
6154
7110
  import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
6155
- import { basename, dirname as dirname5, isAbsolute, join as join8, relative, resolve as resolve2, sep } from "node:path";
7111
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as join9, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
6156
7112
  var FILE_MODE = 384;
6157
7113
  var DIRECTORY_MODE = 448;
6158
7114
  async function writePushedFile(request2) {
@@ -6183,9 +7139,9 @@ async function writePushedFile(request2) {
6183
7139
  }
6184
7140
  try {
6185
7141
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
6186
- dirname5(candidate)
7142
+ dirname6(candidate)
6187
7143
  );
6188
- const realTarget = join8(existingAncestor, ...missingSegments, basename(candidate));
7144
+ const realTarget = join9(existingAncestor, ...missingSegments, basename2(candidate));
6189
7145
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
6190
7146
  if (allowedDirectory === null) {
6191
7147
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -6195,8 +7151,8 @@ async function writePushedFile(request2) {
6195
7151
  }
6196
7152
  if (missingSegments.length > 0) {
6197
7153
  await createMissingDirectories(existingAncestor, missingSegments);
6198
- const realParent = await realpath(dirname5(realTarget));
6199
- if (realParent !== dirname5(realTarget) || !contains(allowedDirectory, realTarget)) {
7154
+ const realParent = await realpath(dirname6(realTarget));
7155
+ if (realParent !== dirname6(realTarget) || !contains(allowedDirectory, realTarget)) {
6200
7156
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
6201
7157
  path: realTarget,
6202
7158
  bytes,
@@ -6221,15 +7177,15 @@ function expandAndValidate(requestedPath, homeDir) {
6221
7177
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
6222
7178
  return null;
6223
7179
  }
6224
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
7180
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
6225
7181
  if (expanded.split(/[/\\]/).includes("..")) {
6226
7182
  return null;
6227
7183
  }
6228
- if (!isAbsolute(expanded)) {
7184
+ if (!isAbsolute2(expanded)) {
6229
7185
  return null;
6230
7186
  }
6231
- const candidate = resolve2(expanded);
6232
- const name = basename(candidate);
7187
+ const candidate = resolve3(expanded);
7188
+ const name = basename2(candidate);
6233
7189
  return name === "" || name === "." || name === ".." ? null : candidate;
6234
7190
  }
6235
7191
  async function resolveNearestExistingAncestor(directory) {
@@ -6239,18 +7195,18 @@ async function resolveNearestExistingAncestor(directory) {
6239
7195
  try {
6240
7196
  return { existingAncestor: await realpath(current), missingSegments };
6241
7197
  } catch (err) {
6242
- const parent = dirname5(current);
7198
+ const parent = dirname6(current);
6243
7199
  if (err.code !== "ENOENT" || parent === current) {
6244
7200
  throw err;
6245
7201
  }
6246
- missingSegments.unshift(basename(current));
7202
+ missingSegments.unshift(basename2(current));
6247
7203
  current = parent;
6248
7204
  }
6249
7205
  }
6250
7206
  }
6251
7207
  async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
6252
7208
  for (const directory of allowedDirectories) {
6253
- if (!isAbsolute(directory)) {
7209
+ if (!isAbsolute2(directory)) {
6254
7210
  log("warn", "file_push_allowed_directory_skipped", { directory, reason: "not_absolute" });
6255
7211
  continue;
6256
7212
  }
@@ -6288,19 +7244,19 @@ async function realpathCreatingIfMissing(directory) {
6288
7244
  }
6289
7245
  }
6290
7246
  function contains(realDirectory, realTarget) {
6291
- const rel = relative(realDirectory, realTarget);
6292
- return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
7247
+ const rel = relative2(realDirectory, realTarget);
7248
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute2(rel);
6293
7249
  }
6294
7250
  async function createMissingDirectories(existingAncestor, missingSegments) {
6295
7251
  let current = existingAncestor;
6296
7252
  for (const segment of missingSegments) {
6297
- current = join8(current, segment);
7253
+ current = join9(current, segment);
6298
7254
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
6299
7255
  await chmod(current, DIRECTORY_MODE);
6300
7256
  }
6301
7257
  }
6302
7258
  async function writeAtomically(realTarget, content) {
6303
- const temporaryPath = join8(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
7259
+ const temporaryPath = join9(dirname6(realTarget), `.evident-push-${randomUUID()}.tmp`);
6304
7260
  let handle;
6305
7261
  try {
6306
7262
  handle = await open3(temporaryPath, "wx", FILE_MODE);
@@ -6430,12 +7386,12 @@ var NOT_APPLIED = {
6430
7386
  opencodeAuthApplied: false
6431
7387
  };
6432
7388
  function isClaudeCredentialPath(requestedPath, homeDir) {
6433
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
6434
- return expanded === join9(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
7389
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7390
+ return expanded === join10(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
6435
7391
  }
6436
7392
  function isOpenCodeAuthPath(requestedPath, homeDir) {
6437
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
6438
- return expanded === join9(homeDir, ...OPENCODE_AUTH_SEGMENTS);
7393
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7394
+ return expanded === join10(homeDir, ...OPENCODE_AUTH_SEGMENTS);
6439
7395
  }
6440
7396
  async function applyOne(options, file) {
6441
7397
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -6579,6 +7535,11 @@ function messageIdOf(m) {
6579
7535
  const infoId = m.info?.id;
6580
7536
  return typeof infoId === "string" ? infoId : void 0;
6581
7537
  }
7538
+ function composeAbandonmentNote(finish, replyShape, outcome) {
7539
+ const base = `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") ` + (outcome === "without-answer" ? "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." : "after writing only part of an answer, and confirmed it is no longer working on this conversation. Evident delivered what was written and is reporting the run as finished.");
7540
+ if (replyShape === null) return base;
7541
+ return `${base} ${replyShape.lastToolName === null ? "No tool call was recorded" : `The last tool call was "${replyShape.lastToolName}" with status "${replyShape.lastToolStatus ?? "(unknown)"}"`}; the reply's final part types were [${replyShape.partTypes.join(", ")}]${replyShape.droppedPartCount > 0 ? ` (${replyShape.droppedPartCount} earlier part types omitted)` : ""}.`;
7542
+ }
6582
7543
  function cleanImageMime(contentType) {
6583
7544
  if (!contentType) return null;
6584
7545
  const media = contentType.split(";")[0].trim().toLowerCase();
@@ -6599,8 +7560,13 @@ var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
6599
7560
  var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
6600
7561
  var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
6601
7562
  var MAX_BUFFERED_SESSION_ERRORS = 256;
7563
+ var SESSION_ERROR_RETENTION_TTL_MS = 6e4;
7564
+ var SESSION_STORE_UNWRITABLE_ERROR = "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.";
7565
+ var SESSION_ERROR_RETAINED_DETAIL = "Runner-observed OpenCode session error: ";
7566
+ var SESSION_ERROR_OBSERVED_CAUSE = "Runner-observed OpenCode session-store cause: ";
6602
7567
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
6603
7568
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
7569
+ var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
6604
7570
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
6605
7571
  var HEARTBEAT_MS = 6e4;
6606
7572
  var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
@@ -6662,6 +7628,7 @@ var ChannelDriver = class _ChannelDriver {
6662
7628
  maxActiveSessions;
6663
7629
  watcherStallMs;
6664
7630
  wedgeWarningIntervalMs;
7631
+ sessionStoreHealth;
6665
7632
  openCodeClient;
6666
7633
  /** Cache of conversationId → opencode sessionId. */
6667
7634
  sessions = /* @__PURE__ */ new Map();
@@ -6754,6 +7721,7 @@ var ChannelDriver = class _ChannelDriver {
6754
7721
  * with a short TTL so an unmatched session cannot retain an event indefinitely.
6755
7722
  */
6756
7723
  bufferedSessionErrors = /* @__PURE__ */ new Map();
7724
+ retainedSessionErrors = /* @__PURE__ */ new Map();
6757
7725
  /**
6758
7726
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
6759
7727
  * dispatched and are still in-flight. A message in this set is never
@@ -6786,10 +7754,18 @@ var ChannelDriver = class _ChannelDriver {
6786
7754
  * row is `processing` or `pending`; clear it only when absent from both lists.
6787
7755
  */
6788
7756
  untrackableAck = /* @__PURE__ */ new Map();
7757
+ /** Session binding used by the first untrackable-ack report, for retries. */
7758
+ untrackableAckSessionIds = /* @__PURE__ */ new Map();
6789
7759
  /** Messages whose untrackable-ack failure signal was emitted while fenced. */
6790
7760
  untrackableAckSignalled = /* @__PURE__ */ new Set();
6791
7761
  /** Messages whose first untrackable-ack fence warning was already logged. */
6792
7762
  untrackableAckWarned = /* @__PURE__ */ new Set();
7763
+ /**
7764
+ * Accepted V1 prompts whose assigned ids were not read back; these rows are
7765
+ * re-read instead of POSTed again. A null snapshot cannot safely exclude an older
7766
+ * identical-text turn, so it never produces a correlation.
7767
+ */
7768
+ awaitingCorrelation = /* @__PURE__ */ new Map();
6793
7769
  /** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
6794
7770
  pendingMessageIds = /* @__PURE__ */ new Set();
6795
7771
  /**
@@ -6875,21 +7851,6 @@ var ChannelDriver = class _ChannelDriver {
6875
7851
  * re-signal on every one of the 15h of re-dispatch attempts #1110 made.
6876
7852
  */
6877
7853
  dispatchNotStartedSignalled = /* @__PURE__ */ new Map();
6878
- /**
6879
- * Consecutive-UNCONFIRMED-dispatch streak for a `pending` row with NO stored
6880
- * `opencode_message_id` yet — i.e. one that has never even reached the
6881
- * re-drive fence above. `sendPromptAsync`'s POST may 2xx, but its own
6882
- * read-back retries can never confirm the assigned id when the session's
6883
- * message list is PERMANENTLY unreadable (e.g. a corrupted local opencode
6884
- * SQLite DB, #1345/#1348's exact fault, just hit BEFORE the row is ever
6885
- * dispatched instead of after). Unlike an already-dispatched row, THIS row has
6886
- * no other safety net at all: the lifecycle cron only reclaims `status =
6887
- * 'processing'` rows, and a row stuck here never reaches `processing`. Keyed
6888
- * by message id, carrying `sessionId` so a session change (a fresh one bound
6889
- * after abandonment) starts a new streak rather than inheriting the old
6890
- * session's count — same shape as `redrivePollFailures` above.
6891
- */
6892
- unconfirmedDispatchFailures = /* @__PURE__ */ new Map();
6893
7854
  /**
6894
7855
  * Consecutive give-ups for a message whose OpenCode user row remains queued.
6895
7856
  * The session id scopes the count: a fresh session is a new attempt after a
@@ -6907,8 +7868,8 @@ var ChannelDriver = class _ChannelDriver {
6907
7868
  * short-circuits while it is present, so a null-id row is re-dispatched AT MOST
6908
7869
  * ONCE per outstanding read-back. Cleared on a SUCCESSFUL dispatch+read-back (the
6909
7870
  * row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
6910
- * re-entry) OR on a failed/unresolved dispatch (the message is genuinely un-sent,
6911
- * so the NEXT tick may retry exactly once more).
7871
+ * re-entry) OR on a failed/unresolved dispatch (the accepted prompt is handed to
7872
+ * the terminal-report fence and is never sent again).
6912
7873
  */
6913
7874
  awaitingReadopt = /* @__PURE__ */ new Set();
6914
7875
  /**
@@ -7031,6 +7992,7 @@ var ChannelDriver = class _ChannelDriver {
7031
7992
  this.maxActiveSessions = config.maxActiveSessions;
7032
7993
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
7033
7994
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
7995
+ this.sessionStoreHealth = config.sessionStoreHealth;
7034
7996
  }
7035
7997
  get isV2() {
7036
7998
  return this.openCodeClient.version === "v2";
@@ -7046,7 +8008,9 @@ var ChannelDriver = class _ChannelDriver {
7046
8008
  return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
7047
8009
  }
7048
8010
  async listSessions() {
7049
- return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
8011
+ return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient, {
8012
+ limit: DRIVER_SESSION_ENUMERATION_LIMIT
8013
+ });
7050
8014
  }
7051
8015
  async sessionExists(sessionId) {
7052
8016
  return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
@@ -7223,6 +8187,49 @@ var ChannelDriver = class _ChannelDriver {
7223
8187
  }
7224
8188
  return ids;
7225
8189
  }
8190
+ /** Find an accepted prompt's user row without adopting an older or concurrent turn. */
8191
+ findAwaitingCorrelationId(entry, sessionId, messages) {
8192
+ if (entry.sessionId !== sessionId || entry.knownUserIds === null) return null;
8193
+ const excludeIds = new Set(entry.knownUserIds);
8194
+ for (const inFlight of this.watchers.get(entry.sessionId)?.inFlight.values() ?? []) {
8195
+ excludeIds.add(inFlight.opencodeMessageId);
8196
+ }
8197
+ return findDispatchedUserMessageId(messages, entry.content, excludeIds);
8198
+ }
8199
+ /** Hand an exhausted accepted-but-untrackable prompt to the terminal-report fence. */
8200
+ async handoffUntrackableCorrelation(conversationId, messageId, sessionId, attempts, source) {
8201
+ this.awaitingCorrelation.delete(messageId);
8202
+ this.awaitingReadopt.delete(messageId);
8203
+ this.sessions.delete(conversationId);
8204
+ this.supersede(conversationId, sessionId);
8205
+ const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
8206
+ const errorMessage3 = `OpenCode accepted this ${dispatchDescription} once, but its assigned id was never confirmed after ${attempts} attempts to match it up \u2014 the message was deliberately not sent again, so no duplicate turn was created; session ${sessionId.slice(0, 8)} was abandoned; a fresh one is used for further messages.`;
8207
+ this.untrackableAck.set(messageId, errorMessage3);
8208
+ this.untrackableAckSessionIds.set(messageId, null);
8209
+ this.log({
8210
+ level: "error",
8211
+ message: errorMessage3,
8212
+ conversation_id: conversationId,
8213
+ message_id: messageId
8214
+ });
8215
+ const reportError = await this.reportUntrackableAck(
8216
+ conversationId,
8217
+ messageId,
8218
+ null,
8219
+ errorMessage3
8220
+ );
8221
+ if (reportError !== null) {
8222
+ this.log({
8223
+ level: "warn",
8224
+ message: `markFailed PATCH for message ${messageId.slice(0, 8)} (conversation ${conversationId.slice(0, 8)}) failed: ${reportError.message}`,
8225
+ conversation_id: conversationId,
8226
+ message_id: messageId
8227
+ });
8228
+ }
8229
+ if (source === "readopt") {
8230
+ void this.postSignal(conversationId, messageId, "readopt_orphan_unsent");
8231
+ }
8232
+ }
7226
8233
  /**
7227
8234
  * File-pull work, for `run.ts`'s idle accounting (#559).
7228
8235
  *
@@ -7402,6 +8409,56 @@ var ChannelDriver = class _ChannelDriver {
7402
8409
  }
7403
8410
  break;
7404
8411
  }
8412
+ const correlation = this.awaitingCorrelation.get(message.id);
8413
+ if (correlation) {
8414
+ correlation.attempts += 1;
8415
+ let correlatedId = null;
8416
+ if (correlation.knownUserIds !== null && correlation.sessionId === sessionId) {
8417
+ const polled = await this.pollSessionMessagesForRedrive(
8418
+ conv,
8419
+ message,
8420
+ correlation.sessionId
8421
+ );
8422
+ if (polled.ok)
8423
+ correlatedId = this.findAwaitingCorrelationId(correlation, sessionId, polled.messages);
8424
+ } else if (correlation.knownUserIds === null) {
8425
+ this.log({
8426
+ level: "debug",
8427
+ message: `Message ${message.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
8428
+ conversation_id: conv.id,
8429
+ message_id: message.id
8430
+ });
8431
+ }
8432
+ if (correlatedId !== null) {
8433
+ this.awaitingCorrelation.delete(message.id);
8434
+ this.dispatchNotStartedSignalled.delete(message.id);
8435
+ this.releasedOpencodeIds.delete(message.id);
8436
+ this.dispatched.add(message.id);
8437
+ this.registerInFlight(conv, correlation.sessionId, message, correlatedId);
8438
+ dispatched += 1;
8439
+ this.log({
8440
+ level: "info",
8441
+ message: `Matched accepted message ${message.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
8442
+ conversation_id: conv.id,
8443
+ message_id: message.id
8444
+ });
8445
+ void this.postSignal(conv.id, message.id, "ack_correlated", {
8446
+ opencode_message_id: correlatedId
8447
+ });
8448
+ continue;
8449
+ }
8450
+ if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
8451
+ await this.handoffUntrackableCorrelation(
8452
+ conv.id,
8453
+ message.id,
8454
+ correlation.sessionId,
8455
+ correlation.attempts,
8456
+ "fresh"
8457
+ );
8458
+ break;
8459
+ }
8460
+ break;
8461
+ }
7405
8462
  const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
7406
8463
  if (effectiveOpencodeMessageId) {
7407
8464
  const outcome = await this.resolveRedrive(
@@ -7423,6 +8480,7 @@ var ChannelDriver = class _ChannelDriver {
7423
8480
  model: message.opencode_model ?? void 0
7424
8481
  };
7425
8482
  let opencodeMessageId;
8483
+ let preDispatchUserIds = null;
7426
8484
  try {
7427
8485
  this.log({
7428
8486
  level: "info",
@@ -7452,7 +8510,20 @@ var ChannelDriver = class _ChannelDriver {
7452
8510
  message.content,
7453
8511
  options,
7454
8512
  sendAttachments,
7455
- this.openCodeClient
8513
+ this.openCodeClient,
8514
+ {
8515
+ onKnownUserIds: (ids) => {
8516
+ preDispatchUserIds = ids;
8517
+ },
8518
+ onKnownUserIdsError: (error2) => {
8519
+ this.log({
8520
+ level: "warn",
8521
+ message: `Pre-dispatch snapshot callback failed for message ${message.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
8522
+ conversation_id: conv.id,
8523
+ message_id: message.id
8524
+ });
8525
+ }
8526
+ }
7456
8527
  )
7457
8528
  );
7458
8529
  } catch (err) {
@@ -7460,6 +8531,7 @@ var ChannelDriver = class _ChannelDriver {
7460
8531
  if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
7461
8532
  const errorMessage4 = err instanceof Error ? err.message : String(err);
7462
8533
  this.untrackableAck.set(message.id, errorMessage4);
8534
+ this.untrackableAckSessionIds.set(message.id, null);
7463
8535
  this.log({
7464
8536
  level: "error",
7465
8537
  message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
@@ -7535,39 +8607,21 @@ var ChannelDriver = class _ChannelDriver {
7535
8607
  if (this.isV2) {
7536
8608
  throw new Error("V2 prompt dispatch completed without an acknowledged message id");
7537
8609
  }
7538
- const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
7539
- if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
7540
- this.log({
7541
- level: "warn",
7542
- message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) \u2014 leaving un-tracked to retry next tick`,
7543
- conversation_id: conv.id,
7544
- message_id: message.id
7545
- });
7546
- this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
7547
- continue;
7548
- }
7549
- this.unconfirmedDispatchFailures.delete(message.id);
7550
- this.sessions.delete(conv.id);
7551
- this.supersede(conv.id, sessionId);
7552
- const errorMessage3 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
8610
+ this.awaitingCorrelation.set(message.id, {
8611
+ sessionId,
8612
+ content: message.content,
8613
+ attempts: 0,
8614
+ knownUserIds: preDispatchUserIds
8615
+ });
7553
8616
  this.log({
7554
- level: "error",
7555
- message: errorMessage3,
8617
+ level: "warn",
8618
+ message: `OpenCode accepted message ${message.id.slice(0, 8)}, but its assigned id could not be read back \u2014 the message will not be sent again while the runner matches it to the turn already running`,
7556
8619
  conversation_id: conv.id,
7557
8620
  message_id: message.id
7558
8621
  });
7559
- await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
7560
- this.log({
7561
- level: "warn",
7562
- message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
7563
- conversation_id: conv.id,
7564
- message_id: message.id
7565
- });
7566
- this.signalDispatchNotStarted(conv, message, "abandon_unreported");
7567
- });
8622
+ this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
7568
8623
  break;
7569
8624
  }
7570
- this.unconfirmedDispatchFailures.delete(message.id);
7571
8625
  this.dispatchNotStartedSignalled.delete(message.id);
7572
8626
  this.releasedOpencodeIds.delete(message.id);
7573
8627
  this.dispatched.add(message.id);
@@ -8172,23 +9226,6 @@ var ChannelDriver = class _ChannelDriver {
8172
9226
  this.redrivePollFailures.set(messageId, { sessionId, signature, count: 1 });
8173
9227
  return 1;
8174
9228
  }
8175
- /**
8176
- * Record one UNCONFIRMED-dispatch outcome (a `pending` row with no stored
8177
- * `opencode_message_id` whose `sendPromptAsync` returned `null`) toward the
8178
- * bound in `processConversation`'s dispatch loop, and return the resulting
8179
- * count. Mirrors `recordRedrivePollFailure`'s session-scoping: a session
8180
- * change starts a fresh streak at `1` rather than inheriting the old one's
8181
- * count, since a new session is a genuinely different attempt.
8182
- */
8183
- recordUnconfirmedDispatch(messageId, sessionId) {
8184
- const existing = this.unconfirmedDispatchFailures.get(messageId);
8185
- if (existing && existing.sessionId === sessionId) {
8186
- existing.count += 1;
8187
- return existing.count;
8188
- }
8189
- this.unconfirmedDispatchFailures.set(messageId, { sessionId, count: 1 });
8190
- return 1;
8191
- }
8192
9229
  /** Record one queued give-up toward the never-started bound. */
8193
9230
  recordNeverStartedAttempt(messageId, sessionId) {
8194
9231
  const existing = this.neverStartedAttempts.get(messageId);
@@ -8500,8 +9537,10 @@ var ChannelDriver = class _ChannelDriver {
8500
9537
  b2LastDescendantCheckMs: 0,
8501
9538
  b2RootOngoingHeldLogged: false,
8502
9539
  b2AbandonedSignalled: false,
9540
+ b2AbandonedWithoutAnswer: false,
8503
9541
  ambiguousPinnedSinceMs: 0,
8504
9542
  ambiguousResolved: false,
9543
+ sessionStoreFailurePending: false,
8505
9544
  neverStartedResolved: false
8506
9545
  });
8507
9546
  const buffered = this.bufferedSessionErrors.get(sessionId);
@@ -8523,6 +9562,28 @@ var ChannelDriver = class _ChannelDriver {
8523
9562
  this.bufferedSessionErrors.delete(oldest);
8524
9563
  }
8525
9564
  }
9565
+ retainSessionError(event) {
9566
+ this.retainedSessionErrors.delete(event.sessionId);
9567
+ this.retainedSessionErrors.set(event.sessionId, {
9568
+ reason: event.reason,
9569
+ observedAt: this.now()
9570
+ });
9571
+ while (this.retainedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
9572
+ const oldest = this.retainedSessionErrors.keys().next().value;
9573
+ if (typeof oldest !== "string") break;
9574
+ this.retainedSessionErrors.delete(oldest);
9575
+ }
9576
+ }
9577
+ readRetainedSessionError(sessionId) {
9578
+ const retained = this.retainedSessionErrors.get(sessionId);
9579
+ if (!retained) return null;
9580
+ if (this.now() - retained.observedAt >= SESSION_ERROR_RETENTION_TTL_MS) {
9581
+ this.retainedSessionErrors.delete(sessionId);
9582
+ return null;
9583
+ }
9584
+ this.retainedSessionErrors.delete(sessionId);
9585
+ return retained.reason;
9586
+ }
8526
9587
  /**
8527
9588
  * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
8528
9589
  * EQUAL (#1618) so a watcher whose loop has not started ticking yet is never
@@ -8618,8 +9679,10 @@ var ChannelDriver = class _ChannelDriver {
8618
9679
  b2LastDescendantCheckMs: 0,
8619
9680
  b2RootOngoingHeldLogged: false,
8620
9681
  b2AbandonedSignalled: false,
9682
+ b2AbandonedWithoutAnswer: false,
8621
9683
  ambiguousPinnedSinceMs: 0,
8622
9684
  ambiguousResolved: false,
9685
+ sessionStoreFailurePending: false,
8623
9686
  neverStartedResolved: false
8624
9687
  });
8625
9688
  }
@@ -8847,9 +9910,10 @@ var ChannelDriver = class _ChannelDriver {
8847
9910
  return;
8848
9911
  }
8849
9912
  if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
9913
+ this.retainSessionError(event);
8850
9914
  this.log({
8851
- level: "debug",
8852
- message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
9915
+ level: "warn",
9916
+ message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 retaining the session error and deferring to transcript polling: ${event.reason}`,
8853
9917
  conversation_id: watcher.conv.id
8854
9918
  });
8855
9919
  return;
@@ -8879,9 +9943,10 @@ var ChannelDriver = class _ChannelDriver {
8879
9943
  const messages = await this.getSessionMessages(event.sessionId);
8880
9944
  const state = messageRunState(messages, inFlight.opencodeMessageId);
8881
9945
  if (state !== "queued") {
9946
+ this.retainSessionError(event);
8882
9947
  this.log({
8883
- level: "debug",
8884
- message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
9948
+ level: "warn",
9949
+ message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)} observed state ${state}; retaining the reason for transcript polling: ${event.reason}`,
8885
9950
  conversation_id: watcher.conv.id,
8886
9951
  message_id: inFlight.evidentMessageId
8887
9952
  });
@@ -8893,14 +9958,53 @@ var ChannelDriver = class _ChannelDriver {
8893
9958
  conversation_id: watcher.conv.id,
8894
9959
  message_id: inFlight.evidentMessageId
8895
9960
  });
8896
- await this.markFailed(
8897
- watcher.conv.id,
8898
- inFlight.evidentMessageId,
8899
- event.sessionId,
8900
- `OpenCode could not run this turn: ${event.reason}`
8901
- );
9961
+ const isMissingProvider = isProviderModelNotFoundError(event.reason);
9962
+ if (isMissingProvider) {
9963
+ const dispatchedModel = inFlight.message.opencode_model;
9964
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
9965
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
9966
+ await this.markFailed(
9967
+ watcher.conv.id,
9968
+ inFlight.evidentMessageId,
9969
+ event.sessionId,
9970
+ `OpenCode could not run this turn: ${event.reason}`,
9971
+ void 0,
9972
+ {
9973
+ classification: {
9974
+ kind: "model_auth",
9975
+ providerId: hasProviderAndModel ? dispatchedModel.slice(0, separator) : null,
9976
+ modelId: hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null,
9977
+ reason: "missing"
9978
+ },
9979
+ errorShape: { name: null, statusCode: null }
9980
+ }
9981
+ );
9982
+ } else {
9983
+ await this.markFailed(
9984
+ watcher.conv.id,
9985
+ inFlight.evidentMessageId,
9986
+ event.sessionId,
9987
+ `OpenCode could not run this turn: ${event.reason}`
9988
+ );
9989
+ }
8902
9990
  inFlight.done = true;
8903
9991
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9992
+ if (isMissingProvider) {
9993
+ if (this.isV2) {
9994
+ void this.postSignal(
9995
+ watcher.conv.id,
9996
+ inFlight.evidentMessageId,
9997
+ "provider_registry_reload_unsupported"
9998
+ );
9999
+ } else {
10000
+ await reloadProviderCache(this.port, this.openCodeClient);
10001
+ void this.postSignal(
10002
+ watcher.conv.id,
10003
+ inFlight.evidentMessageId,
10004
+ "provider_registry_reload_issued"
10005
+ );
10006
+ }
10007
+ }
8904
10008
  } catch (err) {
8905
10009
  if (err instanceof ChannelAuthError) {
8906
10010
  this.log({
@@ -9033,6 +10137,10 @@ var ChannelDriver = class _ChannelDriver {
9033
10137
  if (state === "running" || state === "done" || state === "failed") {
9034
10138
  this.neverStartedAttempts.delete(id);
9035
10139
  }
10140
+ if (inFlight.sessionStoreFailurePending) {
10141
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
10142
+ return;
10143
+ }
9036
10144
  if (inFlight.neverStartedResolved) {
9037
10145
  if (state === "running" || state === "done" || state === "failed") {
9038
10146
  inFlight.neverStartedResolved = false;
@@ -9106,7 +10214,16 @@ var ChannelDriver = class _ChannelDriver {
9106
10214
  if (state === "failed") {
9107
10215
  this.anchorDeliveryDeadline(inFlight);
9108
10216
  if (!inFlight.done) {
9109
- const error2 = messageError(messages, inFlight.opencodeMessageId) ?? void 0;
10217
+ const messageErrorText = messageError(messages, inFlight.opencodeMessageId);
10218
+ const retainedReason = this.readRetainedSessionError(sessionId);
10219
+ const retainedError = retainedReason === null || messageErrorText !== null && messageErrorText.includes(retainedReason) ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10220
+ const observedCause = this.sessionStoreHealth?.readCause(sessionId);
10221
+ const causeError = observedCause === null || observedCause === void 0 ? null : `${SESSION_ERROR_OBSERVED_CAUSE}${observedCause}`;
10222
+ const storeError = this.sessionStoreHealth?.isUnwritable() ? SESSION_STORE_UNWRITABLE_ERROR : null;
10223
+ const runnerDetails = [retainedError, causeError, storeError].filter(
10224
+ (detail) => detail !== null
10225
+ );
10226
+ const error2 = messageErrorText === null ? runnerDetails.length === 0 ? void 0 : runnerDetails.join(" ") : [messageErrorText, ...runnerDetails].join(" ");
9110
10227
  this.log({
9111
10228
  level: "error",
9112
10229
  message: `Message ${inFlight.evidentMessageId.slice(0, 8)} errored \u2014 marking failed: ${error2 ?? "(no error text)"}`,
@@ -9192,10 +10309,33 @@ var ChannelDriver = class _ChannelDriver {
9192
10309
  inFlight.b2LastDescendantCheckMs = 0;
9193
10310
  inFlight.b2RootOngoingHeldLogged = false;
9194
10311
  inFlight.b2AbandonedSignalled = false;
10312
+ inFlight.b2AbandonedWithoutAnswer = false;
9195
10313
  }
9196
10314
  } else {
9197
10315
  if (inFlight.b2AbandonedSignalled) {
9198
- await this.settleMessageDone(sessionId, watcher, inFlight, messages);
10316
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
10317
+ const finish = reply?.info?.finish ?? reply?.finish;
10318
+ if (inFlight.b2AbandonedWithoutAnswer) {
10319
+ await this.settleMessageAbandonedWithoutAnswer(
10320
+ sessionId,
10321
+ watcher,
10322
+ inFlight,
10323
+ messages,
10324
+ finish
10325
+ );
10326
+ } else {
10327
+ await this.settleMessageDone(
10328
+ sessionId,
10329
+ watcher,
10330
+ inFlight,
10331
+ messages,
10332
+ composeAbandonmentNote(
10333
+ finish,
10334
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
10335
+ "with-answer"
10336
+ )
10337
+ );
10338
+ }
9199
10339
  return;
9200
10340
  }
9201
10341
  if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
@@ -9212,22 +10352,62 @@ var ChannelDriver = class _ChannelDriver {
9212
10352
  descendantOngoing,
9213
10353
  rootOngoing
9214
10354
  })) {
10355
+ if (this.sessionStoreHealth?.isUnwritable()) {
10356
+ inFlight.sessionStoreFailurePending = true;
10357
+ void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
10358
+ watched_for_ms: pinnedForMs,
10359
+ run_state: "failed",
10360
+ opencode_message_id: inFlight.opencodeMessageId
10361
+ });
10362
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
10363
+ return;
10364
+ }
9215
10365
  inFlight.b2AbandonedSignalled = true;
10366
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
10367
+ const finish = reply?.info?.finish ?? reply?.finish;
10368
+ if (extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null) {
10369
+ inFlight.b2AbandonedWithoutAnswer = true;
10370
+ void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
10371
+ watched_for_ms: pinnedForMs,
10372
+ run_state: "failed",
10373
+ finish,
10374
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
10375
+ ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
10376
+ opencode_message_id: inFlight.opencodeMessageId
10377
+ });
10378
+ await this.settleMessageAbandonedWithoutAnswer(
10379
+ sessionId,
10380
+ watcher,
10381
+ inFlight,
10382
+ messages,
10383
+ finish
10384
+ );
10385
+ return;
10386
+ }
9216
10387
  this.log({
9217
10388
  level: "warn",
9218
10389
  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`,
9219
10390
  conversation_id: conv.id,
9220
10391
  message_id: id
9221
10392
  });
9222
- const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
9223
10393
  void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
9224
10394
  watched_for_ms: pinnedForMs,
9225
- finish: reply?.info?.finish ?? reply?.finish,
10395
+ finish,
9226
10396
  ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
9227
10397
  ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
9228
10398
  opencode_message_id: inFlight.opencodeMessageId
9229
10399
  });
9230
- await this.settleMessageDone(sessionId, watcher, inFlight, messages);
10400
+ await this.settleMessageDone(
10401
+ sessionId,
10402
+ watcher,
10403
+ inFlight,
10404
+ messages,
10405
+ composeAbandonmentNote(
10406
+ finish,
10407
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
10408
+ "with-answer"
10409
+ )
10410
+ );
9231
10411
  return;
9232
10412
  }
9233
10413
  if (rootOngoing === true && !inFlight.b2RootOngoingHeldLogged) {
@@ -9270,6 +10450,16 @@ var ChannelDriver = class _ChannelDriver {
9270
10450
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
9271
10451
  sessionOngoing: ongoing
9272
10452
  })) {
10453
+ if (this.sessionStoreHealth?.isUnwritable()) {
10454
+ inFlight.sessionStoreFailurePending = true;
10455
+ void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
10456
+ watched_for_ms: pinnedForMs,
10457
+ run_state: "failed",
10458
+ opencode_message_id: inFlight.opencodeMessageId
10459
+ });
10460
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
10461
+ return;
10462
+ }
9273
10463
  inFlight.ambiguousResolved = true;
9274
10464
  this.log({
9275
10465
  level: "warn",
@@ -9417,15 +10607,11 @@ var ChannelDriver = class _ChannelDriver {
9417
10607
  }
9418
10608
  }
9419
10609
  /**
9420
- * Settle a message whose run-state has resolved `'done'` — extracted verbatim
9421
- * (pure refactor, no behavior change) from `serviceInFlightMessage`'s former
9422
- * inline `state === 'done'` branch body, so a SECOND caller (the #721
9423
- * b2-abandonment resolution) can reach the exact same completion behavior
9424
- * (delivery-deadline anchoring, title resolution, usage extraction, and
9425
- * `markDone`'s auth/terminal/transient-retry discipline) without duplicating it
9426
- * and risking the two copies silently drifting apart.
10610
+ * Settle a message whose run-state has resolved `'done'`. Normal completion and
10611
+ * confirmed b2 abandonment share the same delivery, usage, and terminal handling.
10612
+ * Normal completions omit the optional terminal diagnostic.
9427
10613
  */
9428
- async settleMessageDone(sessionId, watcher, inFlight, messages) {
10614
+ async settleMessageDone(sessionId, watcher, inFlight, messages, note) {
9429
10615
  const conv = watcher.conv;
9430
10616
  this.anchorDeliveryDeadline(inFlight);
9431
10617
  if (!inFlight.done) {
@@ -9452,7 +10638,8 @@ var ChannelDriver = class _ChannelDriver {
9452
10638
  title,
9453
10639
  usage,
9454
10640
  usageAgentName,
9455
- subagentInvocations
10641
+ subagentInvocations,
10642
+ note
9456
10643
  );
9457
10644
  } catch (err) {
9458
10645
  if (err instanceof ChannelAuthError) throw err;
@@ -9484,24 +10671,157 @@ var ChannelDriver = class _ChannelDriver {
9484
10671
  });
9485
10672
  return;
9486
10673
  }
9487
- inFlight.done = true;
9488
- await this.reportSubagentAuthFailures(
9489
- watcher.conv.id,
9490
- inFlight.opencodeMessageId,
9491
- inFlight.evidentMessageId,
9492
- messages
9493
- );
10674
+ inFlight.done = true;
10675
+ await this.reportSubagentAuthFailures(
10676
+ watcher.conv.id,
10677
+ inFlight.opencodeMessageId,
10678
+ inFlight.evidentMessageId,
10679
+ messages
10680
+ );
10681
+ }
10682
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10683
+ }
10684
+ /** Report a confirmed b2 abandonment that produced no answer text. */
10685
+ async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, finish) {
10686
+ const conv = watcher.conv;
10687
+ const error2 = composeAbandonmentNote(
10688
+ finish,
10689
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
10690
+ "without-answer"
10691
+ );
10692
+ this.anchorDeliveryDeadline(inFlight);
10693
+ this.log({
10694
+ level: "warn",
10695
+ message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was abandoned without an answer \u2014 ${error2}`,
10696
+ conversation_id: conv.id,
10697
+ message_id: inFlight.evidentMessageId
10698
+ });
10699
+ const usage = messageUsage(messages, inFlight.opencodeMessageId);
10700
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
10701
+ const subagentInvocations = await this.resolveSubagentInvocations(
10702
+ messages,
10703
+ inFlight.opencodeMessageId,
10704
+ inFlight.evidentMessageId
10705
+ );
10706
+ try {
10707
+ await this.markFailed(
10708
+ conv.id,
10709
+ inFlight.evidentMessageId,
10710
+ sessionId,
10711
+ error2,
10712
+ usage,
10713
+ void 0,
10714
+ usageAgentName,
10715
+ subagentInvocations
10716
+ );
10717
+ } catch (err) {
10718
+ if (err instanceof ChannelAuthError) throw err;
10719
+ if (err instanceof ChannelTerminalError) {
10720
+ this.log({
10721
+ level: "warn",
10722
+ 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}`,
10723
+ conversation_id: conv.id,
10724
+ message_id: inFlight.evidentMessageId
10725
+ });
10726
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10727
+ return;
10728
+ }
10729
+ if (this.now() >= inFlight.deadline) {
10730
+ this.log({
10731
+ level: "warn",
10732
+ 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)}`,
10733
+ conversation_id: conv.id,
10734
+ message_id: inFlight.evidentMessageId
10735
+ });
10736
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10737
+ return;
10738
+ }
10739
+ this.log({
10740
+ level: "warn",
10741
+ 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)}`,
10742
+ conversation_id: conv.id,
10743
+ message_id: inFlight.evidentMessageId
10744
+ });
10745
+ return;
10746
+ }
10747
+ inFlight.done = true;
10748
+ await this.reportSubagentAuthFailures(
10749
+ watcher.conv.id,
10750
+ inFlight.opencodeMessageId,
10751
+ inFlight.evidentMessageId,
10752
+ messages
10753
+ );
10754
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10755
+ }
10756
+ /**
10757
+ * Report a queued message as failed after the never-started bound. The accepted
10758
+ * OpenCode copy is not cancelled, so the local fence remains on unsuccessful
10759
+ * terminal-report attempts to prevent a blind re-prompt.
10760
+ */
10761
+ async settleMessageNeverStarted(sessionId, watcher, inFlight, attempts) {
10762
+ const conv = watcher.conv;
10763
+ const error2 = `OpenCode accepted this message but never started running it; after ${attempts} attempts it was still queued. Evident is reporting the run as failed. The copy OpenCode accepted has not been cancelled and may still run if OpenCode recovers.`;
10764
+ this.anchorDeliveryDeadline(inFlight);
10765
+ try {
10766
+ await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
10767
+ } catch (err) {
10768
+ if (err instanceof ChannelAuthError) throw err;
10769
+ if (err instanceof ChannelTerminalError) {
10770
+ this.log({
10771
+ level: "warn",
10772
+ message: `Failed to report never-started message ${inFlight.evidentMessageId.slice(0, 8)} as failed (terminal HTTP ${err.status}) \u2014 keeping its OpenCode copy fenced: ${err.message}`,
10773
+ conversation_id: conv.id,
10774
+ message_id: inFlight.evidentMessageId
10775
+ });
10776
+ this.recordReleasedOpencodeId(
10777
+ inFlight.evidentMessageId,
10778
+ sessionId,
10779
+ inFlight.opencodeMessageId
10780
+ );
10781
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10782
+ return;
10783
+ }
10784
+ if (this.now() >= inFlight.deadline) {
10785
+ this.log({
10786
+ level: "warn",
10787
+ message: `Failed to report never-started 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)}`,
10788
+ conversation_id: conv.id,
10789
+ message_id: inFlight.evidentMessageId
10790
+ });
10791
+ this.recordReleasedOpencodeId(
10792
+ inFlight.evidentMessageId,
10793
+ sessionId,
10794
+ inFlight.opencodeMessageId
10795
+ );
10796
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10797
+ return;
10798
+ }
10799
+ this.log({
10800
+ level: "warn",
10801
+ message: `Failed to report never-started message ${inFlight.evidentMessageId.slice(0, 8)} as failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
10802
+ conversation_id: conv.id,
10803
+ message_id: inFlight.evidentMessageId
10804
+ });
10805
+ return;
9494
10806
  }
10807
+ inFlight.done = true;
10808
+ this.neverStartedAttempts.delete(inFlight.evidentMessageId);
9495
10809
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9496
10810
  }
9497
10811
  /**
9498
- * Report a queued message as failed after the never-started bound. The accepted
9499
- * OpenCode copy is not cancelled, so the local fence remains on unsuccessful
9500
- * terminal-report attempts to prevent a blind re-prompt.
10812
+ * Report a turn as failed when the session database was observed unwritable.
10813
+ * The queue row is retained as failed, while the OpenCode copy stays fenced so
10814
+ * a later drain cannot blindly prompt the same turn again.
9501
10815
  */
9502
- async settleMessageNeverStarted(sessionId, watcher, inFlight, attempts) {
10816
+ async settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight) {
9503
10817
  const conv = watcher.conv;
9504
- const error2 = `OpenCode accepted this message but never started running it; after ${attempts} attempts it was still queued. Evident is reporting the run as failed. The copy OpenCode accepted has not been cancelled and may still run if OpenCode recovers.`;
10818
+ const error2 = SESSION_STORE_UNWRITABLE_ERROR;
10819
+ this.log({
10820
+ level: "error",
10821
+ message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was not resolved as done: ${error2}`,
10822
+ conversation_id: conv.id,
10823
+ message_id: inFlight.evidentMessageId
10824
+ });
9505
10825
  this.anchorDeliveryDeadline(inFlight);
9506
10826
  try {
9507
10827
  await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
@@ -9510,7 +10830,7 @@ var ChannelDriver = class _ChannelDriver {
9510
10830
  if (err instanceof ChannelTerminalError) {
9511
10831
  this.log({
9512
10832
  level: "warn",
9513
- message: `Failed to report never-started message ${inFlight.evidentMessageId.slice(0, 8)} as failed (terminal HTTP ${err.status}) \u2014 keeping its OpenCode copy fenced: ${err.message}`,
10833
+ 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}`,
9514
10834
  conversation_id: conv.id,
9515
10835
  message_id: inFlight.evidentMessageId
9516
10836
  });
@@ -9525,7 +10845,7 @@ var ChannelDriver = class _ChannelDriver {
9525
10845
  if (this.now() >= inFlight.deadline) {
9526
10846
  this.log({
9527
10847
  level: "warn",
9528
- message: `Failed to report never-started 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)}`,
10848
+ 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)}`,
9529
10849
  conversation_id: conv.id,
9530
10850
  message_id: inFlight.evidentMessageId
9531
10851
  });
@@ -9539,14 +10859,13 @@ var ChannelDriver = class _ChannelDriver {
9539
10859
  }
9540
10860
  this.log({
9541
10861
  level: "warn",
9542
- message: `Failed to report never-started message ${inFlight.evidentMessageId.slice(0, 8)} as failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
10862
+ 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)}`,
9543
10863
  conversation_id: conv.id,
9544
10864
  message_id: inFlight.evidentMessageId
9545
10865
  });
9546
10866
  return;
9547
10867
  }
9548
10868
  inFlight.done = true;
9549
- this.neverStartedAttempts.delete(inFlight.evidentMessageId);
9550
10869
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9551
10870
  }
9552
10871
  // Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
@@ -9587,11 +10906,22 @@ var ChannelDriver = class _ChannelDriver {
9587
10906
  }
9588
10907
  }
9589
10908
  }
9590
- if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0) {
10909
+ if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0 || this.awaitingCorrelation.size > 0) {
9591
10910
  const stillProcessing = new Set(rows.map((r) => r.id));
10911
+ for (const id of this.awaitingCorrelation.keys()) {
10912
+ if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
10913
+ this.awaitingCorrelation.delete(id);
10914
+ this.log({
10915
+ level: "debug",
10916
+ message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared awaiting-correlation fence`,
10917
+ message_id: id
10918
+ });
10919
+ }
10920
+ }
9592
10921
  for (const id of [...this.untrackableAck.keys()]) {
9593
10922
  if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
9594
10923
  this.untrackableAck.delete(id);
10924
+ this.untrackableAckSessionIds.delete(id);
9595
10925
  this.untrackableAckSignalled.delete(id);
9596
10926
  this.untrackableAckWarned.delete(id);
9597
10927
  this.log({
@@ -9631,7 +10961,7 @@ var ChannelDriver = class _ChannelDriver {
9631
10961
  bySession.set(row.opencode_session_id, list);
9632
10962
  }
9633
10963
  for (const [sessionId, sessionRows] of bySession) {
9634
- let messages;
10964
+ let messages = null;
9635
10965
  try {
9636
10966
  if (this.isV2) {
9637
10967
  const snapshot = await this.getSessionMessages(sessionId);
@@ -9640,9 +10970,9 @@ var ChannelDriver = class _ChannelDriver {
9640
10970
  level: "warn",
9641
10971
  message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
9642
10972
  });
9643
- continue;
10973
+ } else {
10974
+ messages = snapshot;
9644
10975
  }
9645
- messages = snapshot;
9646
10976
  } else {
9647
10977
  const polled = await pollSessionMessagesForRedrive(
9648
10978
  this.port,
@@ -9655,15 +10985,22 @@ var ChannelDriver = class _ChannelDriver {
9655
10985
  level: "warn",
9656
10986
  message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned ${polled.malformed ? "a non-array message body" : `HTTP ${polled.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 skipping this session this tick`
9657
10987
  });
9658
- continue;
10988
+ } else {
10989
+ messages = polled.messages;
9659
10990
  }
9660
- messages = polled.messages;
9661
10991
  }
9662
10992
  } catch (err) {
9663
10993
  this.log({
9664
10994
  level: "warn",
9665
10995
  message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
9666
10996
  });
10997
+ }
10998
+ if (messages === null) {
10999
+ for (const row of sessionRows) {
11000
+ if (this.awaitingCorrelation.has(row.id)) {
11001
+ await this.advanceReadoptCorrelation(sessionId, row, null);
11002
+ }
11003
+ }
9667
11004
  continue;
9668
11005
  }
9669
11006
  const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
@@ -9673,6 +11010,59 @@ var ChannelDriver = class _ChannelDriver {
9673
11010
  }
9674
11011
  }
9675
11012
  }
11013
+ /** Advance a re-adopted accepted prompt through the shared correlation fence. */
11014
+ async advanceReadoptCorrelation(sessionId, row, messages) {
11015
+ const correlation = this.awaitingCorrelation.get(row.id);
11016
+ if (!correlation) return false;
11017
+ correlation.attempts += 1;
11018
+ const correlatedId = messages === null ? null : this.findAwaitingCorrelationId(correlation, sessionId, messages);
11019
+ if (correlatedId !== null) {
11020
+ this.awaitingCorrelation.delete(row.id);
11021
+ const conv = this.convForRow(sessionId, row);
11022
+ const message = this.queuedMessageForRow(row);
11023
+ const processedAtMs = this.processedAtMs(row);
11024
+ this.registerReadopted(conv, sessionId, message, correlatedId, processedAtMs, processedAtMs);
11025
+ this.dispatched.add(row.id);
11026
+ this.readopted.add(row.id);
11027
+ this.ensureWatcherRunning(sessionId);
11028
+ this.log({
11029
+ level: "info",
11030
+ message: `Re-adopt: matched accepted message ${row.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
11031
+ conversation_id: row.conversation_id,
11032
+ message_id: row.id
11033
+ });
11034
+ void this.postSignal(row.conversation_id, row.id, "readopt_redispatched");
11035
+ void this.postSignal(row.conversation_id, row.id, "ack_correlated", {
11036
+ opencode_message_id: correlatedId
11037
+ });
11038
+ return true;
11039
+ }
11040
+ if (correlation.knownUserIds === null) {
11041
+ this.log({
11042
+ level: "debug",
11043
+ message: `Re-adopt: message ${row.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
11044
+ conversation_id: row.conversation_id,
11045
+ message_id: row.id
11046
+ });
11047
+ } else if (correlation.sessionId !== sessionId) {
11048
+ this.log({
11049
+ level: "warn",
11050
+ message: `Re-adopt: message ${row.id.slice(0, 8)} moved from original session ${correlation.sessionId.slice(0, 8)} to ${sessionId.slice(0, 8)} while awaiting correlation \u2014 refusing to adopt a turn from the rebound session`,
11051
+ conversation_id: row.conversation_id,
11052
+ message_id: row.id
11053
+ });
11054
+ }
11055
+ if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
11056
+ await this.handoffUntrackableCorrelation(
11057
+ row.conversation_id,
11058
+ row.id,
11059
+ correlation.sessionId,
11060
+ correlation.attempts,
11061
+ "readopt"
11062
+ );
11063
+ }
11064
+ return true;
11065
+ }
9676
11066
  /**
9677
11067
  * Re-adopt ONE `processing` row against the tick's session message snapshot
9678
11068
  * (ADR-0046 Decision §1/§2). Idempotent: skips a row already being driven.
@@ -9680,8 +11070,10 @@ var ChannelDriver = class _ChannelDriver {
9680
11070
  * Branches on `messageRunState(messages, row.opencode_message_id)` — the
9681
11071
  * opencode-assigned user-message id persisted on the first `processing` PATCH
9682
11072
  * (#218). A row with a NULL stored id (dispatched but the read-back never landed
9683
- * before the restart) has no id to correlate → treated as an orphan and
9684
- * re-dispatched (at most once, see `forceReadoptRun`):
11073
+ * before the restart) is first offered to an awaiting-correlation fence recorded
11074
+ * by this process; without that fence, it has no id to correlate after restart
11075
+ * and is treated as an orphan and re-dispatched (at most once, see
11076
+ * `forceReadoptRun`):
9685
11077
  * - `done` → `markDone` now (guarded like the watcher's done branch);
9686
11078
  * - `failed` → `markFailed` with the surfaced error (issue #182), so an
9687
11079
  * errored turn is reported failed on restart, NOT re-dispatched —
@@ -9690,7 +11082,8 @@ var ChannelDriver = class _ChannelDriver {
9690
11082
  * re-dispatched instead (issue #1310, see the branch below);
9691
11083
  * - `running`/`queued` → re-attach a watcher via `registerReadopted` (no re-dispatch),
9692
11084
  * tracking the stored id so the reply correlates by it;
9693
- * - `unknown`/null id → re-dispatch (opencode assigns a fresh id) + attach a watcher.
11085
+ * - `unknown`/null id → correlate through an awaiting fence when present;
11086
+ * otherwise re-dispatch (opencode assigns a fresh id) + attach a watcher.
9694
11087
  *
9695
11088
  * Only `ChannelAuthError` propagates.
9696
11089
  */
@@ -9704,6 +11097,7 @@ var ChannelDriver = class _ChannelDriver {
9704
11097
  });
9705
11098
  return;
9706
11099
  }
11100
+ if (await this.advanceReadoptCorrelation(sessionId, row, messages)) return;
9707
11101
  const ocId = row.opencode_message_id;
9708
11102
  const state = messageRunState(messages, ocId ?? "");
9709
11103
  if (state === "done") {
@@ -9770,12 +11164,48 @@ var ChannelDriver = class _ChannelDriver {
9770
11164
  await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
9771
11165
  this.dontRedispatch.delete(row.id);
9772
11166
  this.untrackableAck.delete(row.id);
11167
+ this.untrackableAckSessionIds.delete(row.id);
9773
11168
  this.untrackableAckSignalled.delete(row.id);
9774
11169
  this.untrackableAckWarned.delete(row.id);
9775
11170
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
9776
11171
  return;
9777
11172
  }
9778
- if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
11173
+ if (this.untrackableAck.has(row.id)) {
11174
+ if (this.untrackableAckSignalled.has(row.id)) {
11175
+ const firstFenceWarning = !this.untrackableAckWarned.has(row.id);
11176
+ this.untrackableAckWarned.add(row.id);
11177
+ this.log({
11178
+ level: firstFenceWarning ? "warn" : "debug",
11179
+ message: `Re-adopt: message ${row.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch and retrying the terminal report`,
11180
+ conversation_id: row.conversation_id,
11181
+ message_id: row.id
11182
+ });
11183
+ const errorMessage3 = this.untrackableAck.get(row.id);
11184
+ const reportError = await this.reportUntrackableAck(
11185
+ row.conversation_id,
11186
+ row.id,
11187
+ this.untrackableAckSessionIds.get(row.id) ?? null,
11188
+ errorMessage3
11189
+ );
11190
+ if (reportError !== null) {
11191
+ this.log({
11192
+ level: "warn",
11193
+ message: `Re-adopt: failed to report untrackable message ${row.id.slice(0, 8)} (will retry next drain): ${reportError.message}`,
11194
+ conversation_id: row.conversation_id,
11195
+ message_id: row.id
11196
+ });
11197
+ }
11198
+ } else {
11199
+ this.log({
11200
+ level: "debug",
11201
+ message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
11202
+ conversation_id: row.conversation_id,
11203
+ message_id: row.id
11204
+ });
11205
+ }
11206
+ return;
11207
+ }
11208
+ if (this.dontRedispatch.has(row.id)) {
9779
11209
  this.log({
9780
11210
  level: "debug",
9781
11211
  message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
@@ -9953,6 +11383,7 @@ var ChannelDriver = class _ChannelDriver {
9953
11383
  }
9954
11384
  this.dontRedispatch.delete(row.id);
9955
11385
  this.untrackableAck.delete(row.id);
11386
+ this.untrackableAckSessionIds.delete(row.id);
9956
11387
  this.untrackableAckSignalled.delete(row.id);
9957
11388
  this.untrackableAckWarned.delete(row.id);
9958
11389
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
@@ -9972,8 +11403,9 @@ var ChannelDriver = class _ChannelDriver {
9972
11403
  * latch makes a null-id row re-dispatched AT MOST ONCE per outstanding read-back:
9973
11404
  * short-circuit while the row is latched; clear it on a successful dispatch (the
9974
11405
  * row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
9975
- * re-entry) OR on a failed/unresolved dispatch (genuinely un-sent → the next tick
9976
- * may retry exactly once more).
11406
+ * re-entry) or when the dispatch itself failed before acceptance. An accepted V1
11407
+ * dispatch whose id is not readable enters the bounded correlation fence instead
11408
+ * of retrying; exhaustion hands it to `untrackableAck`.
9977
11409
  *
9978
11410
  * `evidentMessageId = row.id` addresses the SERVER row. Deadline anchored to
9979
11411
  * `processed_at` (Invariant 1).
@@ -10036,6 +11468,7 @@ var ChannelDriver = class _ChannelDriver {
10036
11468
  );
10037
11469
  }
10038
11470
  let ocId;
11471
+ let preDispatchUserIds = null;
10039
11472
  try {
10040
11473
  ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
10041
11474
  sessionId,
@@ -10045,7 +11478,20 @@ var ChannelDriver = class _ChannelDriver {
10045
11478
  row.content,
10046
11479
  options,
10047
11480
  sendAttachments,
10048
- this.openCodeClient
11481
+ this.openCodeClient,
11482
+ {
11483
+ onKnownUserIds: (ids) => {
11484
+ preDispatchUserIds = ids;
11485
+ },
11486
+ onKnownUserIdsError: (error2) => {
11487
+ this.log({
11488
+ level: "warn",
11489
+ message: `Pre-dispatch snapshot callback failed for message ${row.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
11490
+ conversation_id: row.conversation_id,
11491
+ message_id: row.id
11492
+ });
11493
+ }
11494
+ }
10049
11495
  )
10050
11496
  );
10051
11497
  } catch (err) {
@@ -10078,6 +11524,7 @@ var ChannelDriver = class _ChannelDriver {
10078
11524
  }
10079
11525
  } else {
10080
11526
  this.untrackableAck.set(row.id, errorMessage3);
11527
+ this.untrackableAckSessionIds.set(row.id, sessionId);
10081
11528
  }
10082
11529
  this.log({
10083
11530
  level: "error",
@@ -10126,39 +11573,20 @@ var ChannelDriver = class _ChannelDriver {
10126
11573
  }
10127
11574
  if (ocId === null) {
10128
11575
  this.awaitingReadopt.delete(row.id);
10129
- const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
10130
- if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
10131
- this.unconfirmedDispatchFailures.delete(row.id);
10132
- this.sessions.delete(readoptConv.id);
10133
- this.supersede(readoptConv.id, sessionId);
10134
- const errorMessage3 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
10135
- this.log({
10136
- level: "error",
10137
- message: errorMessage3,
10138
- conversation_id: row.conversation_id,
10139
- message_id: row.id
10140
- });
10141
- await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
10142
- this.log({
10143
- level: "warn",
10144
- message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
10145
- conversation_id: row.conversation_id,
10146
- message_id: row.id
10147
- });
10148
- });
10149
- void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
10150
- return;
10151
- }
11576
+ this.awaitingCorrelation.set(row.id, {
11577
+ sessionId,
11578
+ content: row.content,
11579
+ attempts: 0,
11580
+ knownUserIds: preDispatchUserIds
11581
+ });
10152
11582
  this.log({
10153
11583
  level: "warn",
10154
- message: `Re-adopt: message ${row.id.slice(0, 8)} re-dispatched but its opencode id could not be read back (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) \u2014 leaving un-tracked to retry next drain`,
11584
+ message: `Re-adopt: OpenCode accepted message ${row.id.slice(0, 8)}, but its assigned id could not be read back \u2014 the message will not be sent again while the runner matches it to the turn already running`,
10155
11585
  conversation_id: row.conversation_id,
10156
11586
  message_id: row.id
10157
11587
  });
10158
- void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
10159
11588
  return;
10160
11589
  }
10161
- this.unconfirmedDispatchFailures.delete(row.id);
10162
11590
  const processedAtMs = this.processedAtMs(row);
10163
11591
  this.registerReadopted(
10164
11592
  readoptConv,
@@ -10497,14 +11925,14 @@ var ChannelDriver = class _ChannelDriver {
10497
11925
  const fetchChildWithoutBlocking = async (sessionId) => {
10498
11926
  const pending = fetchChildMessages(sessionId);
10499
11927
  let timer;
10500
- const timeout = new Promise((resolve4) => {
11928
+ const timeout = new Promise((resolve5) => {
10501
11929
  timer = setTimeout(() => {
10502
11930
  this.log({
10503
11931
  level: "warn",
10504
11932
  message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was slow \u2014 omitting invocation telemetry without delaying completion`,
10505
11933
  message_id: messageId
10506
11934
  });
10507
- resolve4(null);
11935
+ resolve5(null);
10508
11936
  }, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
10509
11937
  });
10510
11938
  try {
@@ -11064,7 +12492,7 @@ var ChannelDriver = class _ChannelDriver {
11064
12492
  * watcher retries next tick within the
11065
12493
  * deadline, Finding 4).
11066
12494
  */
11067
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
12495
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations, error2) {
11068
12496
  const res = await this.fetchImpl(
11069
12497
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
11070
12498
  {
@@ -11082,7 +12510,8 @@ var ChannelDriver = class _ChannelDriver {
11082
12510
  ...title ? { title } : {},
11083
12511
  ...usage ? usage : {},
11084
12512
  ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
11085
- ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
12513
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {},
12514
+ ...error2 ? { error: error2 } : {}
11086
12515
  })
11087
12516
  }
11088
12517
  );
@@ -11142,11 +12571,17 @@ var ChannelDriver = class _ChannelDriver {
11142
12571
  if (subagentInvocations && subagentInvocations.length > 0) {
11143
12572
  body.subagent_invocations = subagentInvocations;
11144
12573
  }
11145
- if (failure) {
11146
- body.failure_kind = failure.kind;
11147
- body.failure_provider_id = failure.providerId;
11148
- body.failure_model_id = failure.modelId;
11149
- body.failure_reason = failure.reason;
12574
+ if (failure && failure.errorShape.name !== null) {
12575
+ body.failure_error_name = failure.errorShape.name;
12576
+ }
12577
+ if (failure && failure.errorShape.statusCode !== null) {
12578
+ body.failure_status_code = failure.errorShape.statusCode;
12579
+ }
12580
+ if (failure?.classification) {
12581
+ body.failure_kind = failure.classification.kind;
12582
+ body.failure_provider_id = failure.classification.providerId;
12583
+ body.failure_model_id = failure.classification.modelId;
12584
+ body.failure_reason = failure.classification.reason;
11150
12585
  }
11151
12586
  try {
11152
12587
  await this.callWithRetry(
@@ -11167,27 +12602,28 @@ var ChannelDriver = class _ChannelDriver {
11167
12602
  this.clearSubagentInvocationCaches(messageId);
11168
12603
  }
11169
12604
  /**
11170
- * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
12605
+ * Classify an errored turn and extract its bounded error shape for `markFailed`.
11171
12606
  *
11172
12607
  * `messageFailure` alone (structured OpenCode error → `model_auth`) covers
11173
12608
  * most cases; when it returns `null` on this ALREADY-FAILED turn, fall back
11174
12609
  * to the P1-2b zero-provider check — one extra loopback call to
11175
12610
  * `hasAnyConfiguredProvider`, only reached when the structured classifier
11176
12611
  * couldn't place it. Fails open (never throws): a fallback probe failure
11177
- * (`null`/indeterminate) leaves the classification `null`, which produces
11178
- * today's byte-identical PATCH body via `markFailed`'s `if (failure)` guard.
12612
+ * (`null`/indeterminate) leaves the classification `null`; the raw error shape
12613
+ * is still retained for diagnostics.
11179
12614
  */
11180
12615
  async classifyModelAuthFailure(messages, userMessageId) {
11181
- const classified = messageFailure(messages, userMessageId);
11182
- if (classified != null) return classified;
11183
12616
  const reply = findLastAssistantReplyFor(messages, userMessageId);
11184
- const hasProvider = await this.hasAnyConfiguredProvider();
11185
- return applyZeroProviderFallback(
12617
+ const errorShape = extractReplyErrorShape(reply);
12618
+ const classified = messageFailure(messages, userMessageId);
12619
+ if (classified != null) return { classification: classified, errorShape };
12620
+ const classification = applyZeroProviderFallback(
11186
12621
  classified,
11187
- hasProvider,
12622
+ await this.hasAnyConfiguredProvider(),
11188
12623
  reply?.info?.providerID ?? null,
11189
12624
  reply?.info?.modelID ?? null
11190
12625
  );
12626
+ return { classification, errorShape };
11191
12627
  }
11192
12628
  async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
11193
12629
  const providerId = failure.providerId ?? "(unknown)";
@@ -11745,7 +13181,7 @@ function commandError2(result) {
11745
13181
  return result.stderr.trim() || `process exited with code ${result.code ?? "null"}`;
11746
13182
  }
11747
13183
  var runCommand2 = (command, args, opts) => {
11748
- return new Promise((resolve4) => {
13184
+ return new Promise((resolve5) => {
11749
13185
  let child;
11750
13186
  let stdout = "";
11751
13187
  let stderr = "";
@@ -11755,7 +13191,7 @@ var runCommand2 = (command, args, opts) => {
11755
13191
  if (settled) return;
11756
13192
  settled = true;
11757
13193
  if (timer.handle) clearTimeout(timer.handle);
11758
- resolve4(result);
13194
+ resolve5(result);
11759
13195
  };
11760
13196
  try {
11761
13197
  child = spawn5(command, args, { env: opts.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -11810,7 +13246,9 @@ async function installRunnerSecret({
11810
13246
  }) {
11811
13247
  const arn = env.RUNNER_SECRET_ARN?.trim();
11812
13248
  if (!arn) {
11813
- log3("runner secret is not configured; continuing without GitHub and MCP credentials");
13249
+ if (!env.GH_TOKEN) {
13250
+ log3("runner secret is not configured; continuing without GitHub and MCP credentials");
13251
+ }
11814
13252
  return false;
11815
13253
  }
11816
13254
  const result = await (commandRunner ?? runCommand2)(
@@ -12009,7 +13447,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
12009
13447
  // src/lib/opencode/config-overlay.ts
12010
13448
  import { execFileSync as execFileSync2 } from "node:child_process";
12011
13449
  import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
12012
- import { isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
13450
+ import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
12013
13451
  function isFile(filePath) {
12014
13452
  return existsSync2(filePath) && statSync6(filePath).isFile();
12015
13453
  }
@@ -12022,8 +13460,8 @@ function applyRunnerOpenCodeConfig({
12022
13460
  log3("runner OpenCode config is not configured; using the baked project config", "debug");
12023
13461
  return;
12024
13462
  }
12025
- const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
12026
- const target = isFile(join10(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
13463
+ const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
13464
+ const target = isFile(join11(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
12027
13465
  if (!isFile(source)) {
12028
13466
  log3(
12029
13467
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -12031,7 +13469,7 @@ function applyRunnerOpenCodeConfig({
12031
13469
  );
12032
13470
  return;
12033
13471
  }
12034
- copyFileSync(source, join10(cwd, target));
13472
+ copyFileSync(source, join11(cwd, target));
12035
13473
  try {
12036
13474
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
12037
13475
  stdio: "ignore"
@@ -12040,7 +13478,7 @@ function applyRunnerOpenCodeConfig({
12040
13478
  const detail = error2 instanceof Error ? error2.message : String(error2);
12041
13479
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
12042
13480
  }
12043
- log3(`Applied runner OpenCode config ${source} to ${join10(cwd, target)}`);
13481
+ log3(`Applied runner OpenCode config ${source} to ${join11(cwd, target)}`);
12044
13482
  }
12045
13483
 
12046
13484
  // src/lib/credential-sync.ts
@@ -12058,14 +13496,14 @@ function errorMessage2(error2) {
12058
13496
  return error2 instanceof Error ? error2.message : String(error2);
12059
13497
  }
12060
13498
  function waitForSettlement(promise, timeoutMs) {
12061
- return new Promise((resolve4) => {
13499
+ return new Promise((resolve5) => {
12062
13500
  let settled = false;
12063
13501
  const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
12064
13502
  const finish = (value) => {
12065
13503
  if (settled) return;
12066
13504
  settled = true;
12067
13505
  clearTimeout(timer);
12068
- resolve4(value);
13506
+ resolve5(value);
12069
13507
  };
12070
13508
  promise.then(
12071
13509
  () => finish(true),
@@ -12316,8 +13754,8 @@ function resolveFileSyncDirectories(raw, homeDir) {
12316
13754
  if (trimmed === "") {
12317
13755
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
12318
13756
  }
12319
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join11(homeDir, trimmed.slice(2)) : trimmed;
12320
- if (!isAbsolute3(expanded)) {
13757
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join12(homeDir, trimmed.slice(2)) : trimmed;
13758
+ if (!isAbsolute4(expanded)) {
12321
13759
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
12322
13760
  }
12323
13761
  const normalized = resolvePath2(expanded);
@@ -12417,8 +13855,7 @@ function resolveMaxActiveSessions(options, env = process.env) {
12417
13855
  function meetsThreshold(state, level) {
12418
13856
  return LOG_LEVELS[level] >= LOG_LEVELS[state.logLevel];
12419
13857
  }
12420
- function log2(state, message, level = "info") {
12421
- if (!meetsThreshold(state, level)) return;
13858
+ function writeConsoleLog(state, message, level) {
12422
13859
  if (state.json) {
12423
13860
  console.log(
12424
13861
  JSON.stringify({
@@ -12427,10 +13864,18 @@ function log2(state, message, level = "info") {
12427
13864
  message
12428
13865
  })
12429
13866
  );
12430
- } else if (!state.interactive) {
12431
- const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
12432
- console.log(`${prefix} ${message}`);
13867
+ return;
12433
13868
  }
13869
+ const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
13870
+ console.log(`${prefix} ${message}`);
13871
+ }
13872
+ function log2(state, message, level = "info") {
13873
+ if (!meetsThreshold(state, level)) return;
13874
+ if (state.json || !state.interactive) writeConsoleLog(state, message, level);
13875
+ }
13876
+ function logLocalOnly(state, message, level = "info") {
13877
+ if (!meetsThreshold(state, level)) return;
13878
+ writeConsoleLog(state, message, level);
12434
13879
  }
12435
13880
  function logActivity(state, entry) {
12436
13881
  const level = entry.level ?? (entry.type === "error" ? "error" : "info");
@@ -12631,6 +14076,8 @@ async function driveChannels(state, driver) {
12631
14076
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
12632
14077
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
12633
14078
  let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
14079
+ const stayAwakeHolds = () => state.stayAwakeUntilMs !== null && Date.now() < state.stayAwakeUntilMs;
14080
+ let stayAwakeSuppressionLogged = false;
12634
14081
  while (state.running) {
12635
14082
  const cycleStartedAtMs = performance.now();
12636
14083
  let idleThisCycle = false;
@@ -12732,7 +14179,7 @@ async function driveChannels(state, driver) {
12732
14179
  }
12733
14180
  }
12734
14181
  }
12735
- await new Promise((resolve4) => setTimeout(resolve4, CHANNEL_POLL_INTERVAL_MS));
14182
+ await new Promise((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
12736
14183
  const cycleMs = performance.now() - cycleStartedAtMs;
12737
14184
  if (idleThisCycle) idleMs += cycleMs;
12738
14185
  if (unreachableThisCycle) unreachableMs += cycleMs;
@@ -12745,17 +14192,29 @@ async function driveChannels(state, driver) {
12745
14192
  if (state.interactive) displayStatus(state);
12746
14193
  break;
12747
14194
  }
12748
- if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3) {
14195
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && !stayAwakeHolds()) {
12749
14196
  logActivity(state, { type: "info", message: "Idle timeout reached" });
12750
14197
  if (state.interactive) displayStatus(state);
12751
14198
  break;
12752
14199
  }
14200
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && stayAwakeHolds()) {
14201
+ if (!stayAwakeSuppressionLogged) {
14202
+ stayAwakeSuppressionLogged = true;
14203
+ logActivity(state, {
14204
+ type: "info",
14205
+ message: "Idle, but staying awake until the requested minimum passes"
14206
+ });
14207
+ }
14208
+ }
12753
14209
  }
12754
14210
  }
12755
14211
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
12756
- var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
14212
+ var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
14213
+ var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
14214
+ var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
14215
+ var MAX_SESSION_DELETE_BODY_LENGTH = 200;
12757
14216
  function shouldWarnForReclaimSkip(reason) {
12758
- if (reason !== "sqlite-unavailable") return false;
14217
+ if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
12759
14218
  const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
12760
14219
  if (!version2) return false;
12761
14220
  const major = Number(version2[1]);
@@ -12764,7 +14223,7 @@ function shouldWarnForReclaimSkip(reason) {
12764
14223
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
12765
14224
  }
12766
14225
  function sessionDbPath() {
12767
- return join11(homedir7(), ".local", "share", "opencode", "opencode.db");
14226
+ return join12(homedir7(), ".local", "share", "opencode", "opencode.db");
12768
14227
  }
12769
14228
  function logSessionCleanupActivity(state, phase, entry) {
12770
14229
  logActivity(state, {
@@ -12812,61 +14271,146 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
12812
14271
  async function runSweep(state, driver, config) {
12813
14272
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
12814
14273
  try {
12815
- const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
12816
- if (sessions === null) {
12817
- logSessionCleanupActivity(state, "sweep", {
14274
+ const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0);
14275
+ const sessions = enumeration.sessions;
14276
+ const enumerationReason = sessions === null ? enumeration.reason : null;
14277
+ if (enumerationReason !== null) {
14278
+ logActivity(state, {
12818
14279
  type: "info",
12819
- message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`,
12820
- metadata: {
12821
- outcome: "sweep-skipped",
12822
- reason: "opencode-unreachable"
12823
- }
14280
+ level: "warn",
14281
+ message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
12824
14282
  });
12825
- return;
12826
14283
  }
12827
- const toDelete = selectSessionsToDelete(
12828
- sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
12829
- {
12830
- maxAgeMs: config.maxAgeMs,
12831
- maxCount: config.maxCount,
12832
- nowMs: Date.now(),
12833
- protectedIds: driver.protectedSessionIds()
12834
- }
12835
- );
12836
- const protectedNow = driver.protectedSessionIds();
12837
14284
  let deleted = 0;
12838
14285
  let failed = 0;
14286
+ let failed4xx = 0;
14287
+ let failed5xx = 0;
14288
+ let failedOtherStatus = 0;
14289
+ let failedThrew = 0;
12839
14290
  let skippedNewlyActive = 0;
12840
- for (const id of toDelete) {
12841
- if (protectedNow.has(id)) {
12842
- skippedNewlyActive++;
12843
- logActivity(state, {
14291
+ let remaining = 0;
14292
+ let protectedNow = driver.protectedSessionIds();
14293
+ if (sessions !== null) {
14294
+ const toDelete = selectSessionsToDelete(
14295
+ sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
14296
+ {
14297
+ maxAgeMs: config.maxAgeMs,
14298
+ maxCount: config.maxCount,
14299
+ nowMs: Date.now(),
14300
+ protectedIds: protectedNow
14301
+ }
14302
+ );
14303
+ const eligibleToDelete = toDelete.filter(
14304
+ (id) => (state.sessionDeleteFailures.get(id)?.isolatedFailures ?? 0) < SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES
14305
+ );
14306
+ const deleteCandidates = eligibleToDelete.slice(0, SESSION_CLEANUP_MAX_DELETES_PER_SWEEP);
14307
+ remaining = eligibleToDelete.length - deleteCandidates.length;
14308
+ const answeredFailures = [];
14309
+ protectedNow = driver.protectedSessionIds();
14310
+ for (const id of deleteCandidates) {
14311
+ if (protectedNow.has(id)) {
14312
+ skippedNewlyActive++;
14313
+ logActivity(state, {
14314
+ type: "info",
14315
+ message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
14316
+ });
14317
+ continue;
14318
+ }
14319
+ const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
14320
+ if (outcome.ok) {
14321
+ deleted++;
14322
+ } else if (outcome.status === "threw") {
14323
+ failed++;
14324
+ failedThrew++;
14325
+ } else {
14326
+ failed++;
14327
+ const statusClass = Math.floor(outcome.status / 100);
14328
+ if (statusClass === 4) failed4xx++;
14329
+ else if (statusClass === 5) failed5xx++;
14330
+ else failedOtherStatus++;
14331
+ answeredFailures.push({
14332
+ id,
14333
+ status: outcome.status,
14334
+ ...outcome.body !== void 0 ? { body: outcome.body } : {}
14335
+ });
14336
+ }
14337
+ }
14338
+ const givenUpFailures = [];
14339
+ if (failed > SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES) {
14340
+ for (const { id } of answeredFailures) state.sessionDeleteFailures.delete(id);
14341
+ } else {
14342
+ for (const failure of answeredFailures) {
14343
+ const previous = state.sessionDeleteFailures.get(failure.id);
14344
+ const record = previous?.status === failure.status ? { status: failure.status, isolatedFailures: previous.isolatedFailures + 1 } : { status: failure.status, isolatedFailures: 1 };
14345
+ state.sessionDeleteFailures.set(failure.id, record);
14346
+ if (record.isolatedFailures >= SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES) {
14347
+ givenUpFailures.push(failure);
14348
+ }
14349
+ }
14350
+ }
14351
+ for (const failure of givenUpFailures) {
14352
+ const excerpt = redactDiagnosticText(failure.body ?? "(no response body)").slice(
14353
+ 0,
14354
+ MAX_SESSION_DELETE_BODY_LENGTH
14355
+ );
14356
+ logSessionCleanupActivity(state, "sweep", {
12844
14357
  type: "info",
12845
- message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
14358
+ level: "warn",
14359
+ message: `Session cleanup: gave up on session ${failure.id} after ${SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES} isolated delete failures (status ${failure.status}): ${excerpt}`,
14360
+ metadata: {
14361
+ outcome: "session-delete-given-up",
14362
+ session_id: failure.id,
14363
+ status: failure.status,
14364
+ body_excerpt: excerpt,
14365
+ given_up_count: givenUpFailures.length
14366
+ }
12846
14367
  });
12847
- continue;
12848
14368
  }
12849
- if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
12850
- else failed++;
14369
+ const failedNote = failed > 0 ? `, failed ${failed}` : "";
14370
+ const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
14371
+ const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
14372
+ logActivity(state, {
14373
+ type: "info",
14374
+ message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
14375
+ });
12851
14376
  }
12852
- const failedNote = failed > 0 ? `, failed ${failed}` : "";
12853
- const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
12854
- logActivity(state, {
12855
- type: "info",
12856
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`
12857
- });
14377
+ const sessionMetadata = {
14378
+ sessions_inspected: sessions?.length ?? null,
14379
+ sessions_deleted: deleted,
14380
+ sessions_failed: failed,
14381
+ sessions_failed_4xx: failed4xx,
14382
+ sessions_failed_5xx: failed5xx,
14383
+ sessions_failed_other: failedOtherStatus,
14384
+ sessions_failed_threw: failedThrew,
14385
+ sessions_skipped_active: skippedNewlyActive,
14386
+ sessions_remaining: remaining,
14387
+ ...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
14388
+ };
14389
+ const protectedForReclaim = driver.protectedSessionIds();
12858
14390
  const reclaimResult = await reclaimSessionDbSpace({
12859
14391
  dbPath: sessionDbPath(),
12860
- maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
12861
- allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
14392
+ chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
14393
+ pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
14394
+ timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
14395
+ hasLiveTurn: protectedForReclaim.size > 0,
14396
+ hasProvenanceAnomaly: state.sessionDbProvenanceAnomaly
12862
14397
  });
12863
14398
  if (reclaimResult.ok) {
14399
+ state.sessionStoreHealth.observeWritable();
12864
14400
  const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
12865
14401
  const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
12866
14402
  const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
14403
+ const loopElapsedMs = Math.max(
14404
+ 0,
14405
+ reclaimResult.elapsedMs - reclaimResult.checkpointElapsedMs
14406
+ );
14407
+ const reclaimIsSlow = reclaimResult.elapsedMs >= 4 * SESSION_DB_RECLAIM_TIME_BUDGET_MS;
14408
+ const perPageNote = reclaimResult.pagesReclaimed > 0 ? `, per-page ${(loopElapsedMs / reclaimResult.pagesReclaimed).toFixed(2)} ms/page` : "";
14409
+ const slowReclaimNote = reclaimIsSlow ? ` (slow reclaim: total elapsed ${reclaimResult.elapsedMs} ms, checkpoint ${reclaimResult.checkpointElapsedMs} ms, loop ${loopElapsedMs} ms, budget ${SESSION_DB_RECLAIM_TIME_BUDGET_MS} ms${perPageNote})` : "";
12867
14410
  logSessionCleanupActivity(state, "sweep", {
12868
14411
  type: "info",
12869
- message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`,
14412
+ level: reclaimIsSlow ? "warn" : "info",
14413
+ message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB (stopped by ${reclaimResult.stoppedBy}; freelist ${reclaimResult.freelistBefore} -> ${reclaimResult.freelistAfter} pages)` + checkpointNote + slowReclaimNote,
12870
14414
  metadata: {
12871
14415
  outcome: "reclaimed",
12872
14416
  mode: reclaimResult.mode,
@@ -12874,25 +14418,28 @@ async function runSweep(state, driver, config) {
12874
14418
  after_bytes: reclaimResult.afterBytes,
12875
14419
  checkpoint_busy: reclaimResult.checkpoint.busy,
12876
14420
  wal_frames_pending: reclaimResult.checkpoint.log,
12877
- sessions_inspected: sessions.length,
12878
- sessions_deleted: deleted,
12879
- sessions_failed: failed,
12880
- sessions_skipped_active: skippedNewlyActive
14421
+ stopped_by: reclaimResult.stoppedBy,
14422
+ freelist_before: reclaimResult.freelistBefore,
14423
+ freelist_after: reclaimResult.freelistAfter,
14424
+ pages_reclaimed: reclaimResult.pagesReclaimed,
14425
+ reclaim_elapsed_ms: reclaimResult.elapsedMs,
14426
+ checkpoint_elapsed_ms: reclaimResult.checkpointElapsedMs,
14427
+ ...sessionMetadata
12881
14428
  }
12882
14429
  });
12883
14430
  } else {
14431
+ if (reclaimResult.skipped === "disk-exhaustion") {
14432
+ state.sessionStoreHealth.observeUnwritable();
14433
+ }
12884
14434
  logSessionCleanupActivity(state, "sweep", {
12885
14435
  type: "info",
12886
- level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
14436
+ level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
12887
14437
  message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
12888
14438
  metadata: {
12889
14439
  outcome: "reclaim-skipped",
12890
14440
  reason: reclaimResult.skipped,
12891
14441
  ...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
12892
- sessions_inspected: sessions.length,
12893
- sessions_deleted: deleted,
12894
- sessions_failed: failed,
12895
- sessions_skipped_active: skippedNewlyActive
14442
+ ...sessionMetadata
12896
14443
  }
12897
14444
  });
12898
14445
  }
@@ -12987,10 +14534,12 @@ function scheduleUsageReporting(state, params) {
12987
14534
  let phase = "dormant";
12988
14535
  let rearmRequested = false;
12989
14536
  const armProbe = () => {
14537
+ if (!state.running) return;
12990
14538
  phase = "probe-pending";
12991
14539
  params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
12992
14540
  };
12993
14541
  const scheduleNextTick = () => {
14542
+ if (!state.running) return;
12994
14543
  if (rearmRequested) {
12995
14544
  rearmRequested = false;
12996
14545
  armProbe();
@@ -13024,6 +14573,7 @@ function scheduleUsageReporting(state, params) {
13024
14573
  phase = "tick-in-flight";
13025
14574
  try {
13026
14575
  const usage = await params.fetchUsage();
14576
+ if (!state.running) return;
13027
14577
  const result = await params.report(usage);
13028
14578
  if (result.ok) {
13029
14579
  if (consecutiveFailures > 0) {
@@ -13149,6 +14699,20 @@ function scheduleResourceUsageReporting(state, options) {
13149
14699
  message: `Resource usage collection: ${warning2}`
13150
14700
  });
13151
14701
  }
14702
+ const lowDiskDecision = evaluateLowDiskWarning({
14703
+ ...usage,
14704
+ nowMs: Date.now(),
14705
+ state: state.lowDiskWarningState
14706
+ });
14707
+ state.lowDiskWarningState = lowDiskDecision.state;
14708
+ if (lowDiskDecision.warning !== null) {
14709
+ logActivity(state, {
14710
+ type: "info",
14711
+ level: "warn",
14712
+ message: lowDiskDecision.warning
14713
+ });
14714
+ }
14715
+ if (!state.running) return;
13152
14716
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
13153
14717
  if (result.ok) {
13154
14718
  if (consecutiveFailures > 0) {
@@ -13181,7 +14745,9 @@ function scheduleResourceUsageReporting(state, options) {
13181
14745
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
13182
14746
  });
13183
14747
  } finally {
13184
- state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
14748
+ if (state.running) {
14749
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
14750
+ }
13185
14751
  }
13186
14752
  };
13187
14753
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -13343,8 +14909,8 @@ async function beginGracefulShutdown(state, trigger) {
13343
14909
  return true;
13344
14910
  }
13345
14911
  );
13346
- const timedOut = new Promise((resolve4) => {
13347
- timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
14912
+ const timedOut = new Promise((resolve5) => {
14913
+ timer = setTimeout(() => resolve5(false), telemetryBudgetMs);
13348
14914
  });
13349
14915
  if (!await Promise.race([flushed, timedOut])) {
13350
14916
  log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
@@ -13384,6 +14950,8 @@ async function run(options) {
13384
14950
  port: options.port ?? 4096,
13385
14951
  conversationFilter: options.conversation ?? null,
13386
14952
  idleTimeout: options.idleTimeout ?? null,
14953
+ stayAwakeSeconds: null,
14954
+ stayAwakeUntilMs: null,
13387
14955
  json: options.json ?? false,
13388
14956
  interactive,
13389
14957
  logLevel,
@@ -13395,6 +14963,7 @@ async function run(options) {
13395
14963
  opencodeClient: null,
13396
14964
  opencodeV2CredentialImportGuard: { inFlight: false },
13397
14965
  sessionDbProvenanceAnomaly: false,
14966
+ sessionStoreHealth: createSessionStoreHealth(),
13398
14967
  opencodeProcess: null,
13399
14968
  stopOpenCodeLogTail: null,
13400
14969
  litestreamProcess: null,
@@ -13406,12 +14975,14 @@ async function run(options) {
13406
14975
  messageCount: 0,
13407
14976
  lastProxiedActivityAt: null,
13408
14977
  sessionCleanupTimers: [],
14978
+ sessionDeleteFailures: /* @__PURE__ */ new Map(),
13409
14979
  claudeUsageTimer: null,
13410
14980
  claudeUsageRearm: null,
13411
14981
  openaiUsageTimer: null,
13412
14982
  openaiUsageRearm: null,
13413
14983
  resourceUsageTimer: null,
13414
14984
  stopResourceUsageSampling: null,
14985
+ lowDiskWarningState: { worstBand: null, warnedAtMs: null },
13415
14986
  credentialSync: null,
13416
14987
  authHeader: ""
13417
14988
  };
@@ -13570,6 +15141,17 @@ async function run(options) {
13570
15141
  }
13571
15142
  spinner?.succeed(`Runner: ${validation.agent.name || state.agentId}`);
13572
15143
  state.agentName = validation.agent.name;
15144
+ const requestedStayAwakeSeconds = validation.agent?.wake_stay_awake_seconds;
15145
+ if (typeof requestedStayAwakeSeconds === "number" && Number.isFinite(requestedStayAwakeSeconds) && requestedStayAwakeSeconds > 0) {
15146
+ state.stayAwakeSeconds = requestedStayAwakeSeconds;
15147
+ } else if (requestedStayAwakeSeconds !== void 0 && requestedStayAwakeSeconds !== null) {
15148
+ state.stayAwakeSeconds = null;
15149
+ log2(
15150
+ state,
15151
+ `Ignoring invalid wake_stay_awake_seconds (type: ${typeof requestedStayAwakeSeconds})`,
15152
+ "warn"
15153
+ );
15154
+ }
13573
15155
  const microvmId = process.env.MICROVM_ID?.trim();
13574
15156
  if (microvmId) {
13575
15157
  const reported = await reportMicrovmId(state.agentId, state.authHeader, microvmId);
@@ -13585,27 +15167,74 @@ async function run(options) {
13585
15167
  } else {
13586
15168
  log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
13587
15169
  }
13588
- if (options.restoreRunnerCredentials) {
13589
- log2(state, "Restoring runner credentials before starting OpenCode");
13590
- const credentialContext = {
13591
- env: process.env,
13592
- log: (message, level = "info") => {
13593
- if (level === "error") {
13594
- logActivity(state, { type: "error", error: message });
13595
- } else {
13596
- logActivity(state, { type: "info", level, message });
13597
- }
15170
+ if (options.restoreRunnerCredentials || options.credentialSyncMarker) {
15171
+ const logCredentialVaultResolution = (message, level) => {
15172
+ log2(state, message, level);
15173
+ if (state.interactive && !state.json) {
15174
+ logActivity(state, { type: "info", level, message });
13598
15175
  }
13599
15176
  };
13600
- const githubTokenPopulated = await installRunnerSecret(credentialContext);
15177
+ const vaultResolution = await getRunnerCredentialVault(state.agentId, state.authHeader);
15178
+ if (vaultResolution.error) {
15179
+ delete process.env.CREDENTIAL_VAULT_PREFIX;
15180
+ logCredentialVaultResolution(
15181
+ `CREDENTIAL-VAULT-LOOKUP-FAILED: ${vaultResolution.error}; credentials will be read from and written to this runner's own prefix this boot`,
15182
+ "warn"
15183
+ );
15184
+ } else if (vaultResolution.prefix !== null && vaultResolution.vaultId !== null && vaultResolution.source !== null) {
15185
+ process.env.CREDENTIAL_VAULT_PREFIX = vaultResolution.prefix;
15186
+ logCredentialVaultResolution(
15187
+ `CREDENTIAL-VAULT: source=${vaultResolution.source} vault=${vaultResolution.vaultId} prefix=${vaultResolution.prefix}`,
15188
+ "info"
15189
+ );
15190
+ } else {
15191
+ delete process.env.CREDENTIAL_VAULT_PREFIX;
15192
+ logCredentialVaultResolution(
15193
+ "CREDENTIAL-VAULT-NONE: this runner is not linked to a credential vault; using its own prefix",
15194
+ "debug"
15195
+ );
15196
+ }
15197
+ }
15198
+ const credentialContext = {
15199
+ env: process.env,
15200
+ log: (message, level = "info") => {
15201
+ if (level === "error") {
15202
+ logActivity(state, { type: "error", error: message });
15203
+ } else {
15204
+ logActivity(state, { type: "info", level, message });
15205
+ }
15206
+ }
15207
+ };
15208
+ const githubTokenPopulated = await installRunnerSecret(credentialContext);
15209
+ if (options.restoreRunnerCredentials) {
13601
15210
  await restoreCredentialStores(credentialContext);
13602
- if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13603
15211
  }
15212
+ if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13604
15213
  state.credentialSync?.arm();
13605
- const openCodeActivityForwarder = createOpenCodeActivityForwarder(() => ({
13606
- agentId: state.agentId,
13607
- authHeader: state.authHeader
13608
- }));
15214
+ const dependencyInstallDiagnosticReporter = createDependencyInstallDiagnosticReporter({
15215
+ context: {
15216
+ homeDir: homedir7(),
15217
+ cwd: process.cwd(),
15218
+ microvmId: process.env.MICROVM_ID?.trim(),
15219
+ euid: process.getuid?.(),
15220
+ egid: process.getgid?.()
15221
+ },
15222
+ emitEvent: (message) => logActivity(state, { type: "info", level: "warn", message }),
15223
+ emitLocal: (message) => logLocalOnly(state, message, "warn")
15224
+ });
15225
+ const openCodeActivityForwarder = createOpenCodeActivityForwarder(
15226
+ () => ({
15227
+ agentId: state.agentId,
15228
+ authHeader: state.authHeader
15229
+ }),
15230
+ ({ isDiskFailure, sessionId, cause }) => {
15231
+ if (isDiskFailure) state.sessionStoreHealth.observeUnwritable();
15232
+ if (cause !== null && sessionId !== null) {
15233
+ state.sessionStoreHealth.observeCause(sessionId, cause);
15234
+ }
15235
+ },
15236
+ dependencyInstallDiagnosticReporter
15237
+ );
13609
15238
  const openCodeLogTail = tailOpenCodeLogFile(
13610
15239
  resolveOpenCodeLogPath(homedir7(), process.env),
13611
15240
  openCodeActivityForwarder
@@ -13615,22 +15244,27 @@ async function run(options) {
13615
15244
  openCodeLogTail.stop();
13616
15245
  };
13617
15246
  let sessionDbVerifyFatal = false;
15247
+ let sessionDbVerifyFatalReason;
13618
15248
  if (!options.restoreSessionDb) {
13619
- log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
15249
+ logActivity(state, {
15250
+ type: "info",
15251
+ level: "warn",
15252
+ message: "SESSION-DB-VERIFY-SKIPPED: reason=flag-absent \u2014 Skipping session-DB restore: --restore-session-db was not passed"
15253
+ });
13620
15254
  } else {
13621
15255
  const health = await checkOpenCodeHealth(state.port);
13622
15256
  if (health.healthy) {
13623
- log2(
13624
- state,
13625
- "Skipping session-DB restore: OpenCode is already serving this database",
13626
- "debug"
13627
- );
15257
+ logActivity(state, {
15258
+ type: "info",
15259
+ level: "warn",
15260
+ message: "SESSION-DB-VERIFY-SKIPPED: reason=opencode-serving \u2014 Skipping session-DB restore: OpenCode is already serving this database"
15261
+ });
13628
15262
  } else if (health.authFailed) {
13629
- log2(
13630
- state,
13631
- "Skipping session-DB restore: OpenCode answered with an authentication challenge; assuming this database is in use",
13632
- "debug"
13633
- );
15263
+ logActivity(state, {
15264
+ type: "info",
15265
+ level: "warn",
15266
+ message: "SESSION-DB-VERIFY-SKIPPED: reason=opencode-auth-challenge \u2014 Skipping session-DB restore: OpenCode answered with an authentication challenge; assuming this database is in use"
15267
+ });
13634
15268
  } else {
13635
15269
  const result = await restoreAndVerifySessionDb({
13636
15270
  dbPath: sessionDbPath(),
@@ -13647,12 +15281,13 @@ async function run(options) {
13647
15281
  reportRecovery: (record) => reportSessionDbRecoveryRecord(state, record)
13648
15282
  });
13649
15283
  sessionDbVerifyFatal = result.verifyFatal;
15284
+ sessionDbVerifyFatalReason = result.verifyFatalReason;
13650
15285
  }
13651
15286
  }
13652
15287
  reportSessionDbRecovery(state);
13653
15288
  if (sessionDbVerifyFatal) {
13654
15289
  throw new Error(
13655
- "SESSION-DB-INTEGRITY-EXHAUSTED: the corrupt session DB could not be proven separated from the active backup prefix or removed from disk"
15290
+ sessionDbVerifyFatalReason === void 0 ? "SESSION-DB-INTEGRITY-EXHAUSTED: the corrupt session DB could not be proven separated from the active backup prefix or removed from disk" : `SESSION-DB-VERIFY-TIMEOUT: session DB verification timed out (${sessionDbVerifyFatalReason}); refusing to start`
13656
15291
  );
13657
15292
  }
13658
15293
  applyRunnerOpenCodeConfig({
@@ -13897,6 +15532,7 @@ async function run(options) {
13897
15532
  getAuthHeader: () => state.authHeader,
13898
15533
  conversationFilter: state.conversationFilter,
13899
15534
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
15535
+ sessionStoreHealth: state.sessionStoreHealth,
13900
15536
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
13901
15537
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
13902
15538
  fileSyncDirectories,
@@ -13930,6 +15566,13 @@ async function run(options) {
13930
15566
  type: "info",
13931
15567
  message: connectionMessage
13932
15568
  });
15569
+ if (!isReconnect && state.stayAwakeSeconds !== null && state.stayAwakeUntilMs === null) {
15570
+ state.stayAwakeUntilMs = Date.now() + state.stayAwakeSeconds * 1e3;
15571
+ logActivity(state, {
15572
+ type: "info",
15573
+ message: `Staying awake for at least ${state.stayAwakeSeconds}s as requested by the wake`
15574
+ });
15575
+ }
13933
15576
  if (options.tunnelReadyFile) {
13934
15577
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
13935
15578
  if (marker.ok) {
@@ -13978,14 +15621,14 @@ async function run(options) {
13978
15621
  },
13979
15622
  onDisconnected: (code, reason, transition) => {
13980
15623
  state.connected = false;
13981
- const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
15624
+ const safeReason2 = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
13982
15625
  logActivity(state, {
13983
15626
  type: "info",
13984
- message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
15627
+ message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason2})`
13985
15628
  });
13986
15629
  emitAgentDisconnected(state.agentId, {
13987
15630
  code,
13988
- reason: safeReason,
15631
+ reason: safeReason2,
13989
15632
  connection_generation: transition.generation
13990
15633
  });
13991
15634
  if (state.interactive) displayStatus(state);
@@ -14064,6 +15707,7 @@ async function run(options) {
14064
15707
  if (error2.message === "Unauthorized") tunnelSpinner?.fail("Unauthorized");
14065
15708
  throw error2;
14066
15709
  }
15710
+ if (!state.running) return;
14067
15711
  scheduleSessionCleanup(state, channelDriver, options);
14068
15712
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
14069
15713
  state.openaiUsageRearm = scheduleUsageReporting(state, {
@@ -14148,6 +15792,7 @@ program.command("login").description("Authenticate with Evident").option("--toke
14148
15792
  program.command("logout").description("Remove stored credentials for the current endpoint").option("--all", "Remove stored credentials for all endpoints").action((options) => logout({ all: options.all }));
14149
15793
  program.command("whoami").description("Show the currently logged in user").action(whoami);
14150
15794
  program.command("status").description("Check whether the configured credentials can reach Evident").option("--json", "Output in JSON format").action((options) => status({ json: options.json }));
15795
+ program.command("credential-vault-env").description("Print the shell assignment a runner image evals before starting the synchroniser").action(credentialVaultEnv);
14151
15796
  program.command("claude-usage").description("[spike] Show Claude subscription usage (requires a local `claude login`)").action(claudeUsage);
14152
15797
  program.command("run").description("Connect to Evident and process messages").option("--runner [id]", "Runner ID to connect to (optional when EVIDENT_RUNNER_KEY is set)").option(
14153
15798
  "-a, --agent [id]",