@evident-ai/cli 3.5.2-dev.3431f9f → 3.5.2-dev.3c6c52c

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)
@@ -1128,7 +1293,11 @@ var FORWARD_FAILURE_REASON_HEADER = "X-Evident-Failure-Reason";
1128
1293
  function log(level, event, fields) {
1129
1294
  const method = level === "debug" ? "log" : level;
1130
1295
  try {
1131
- console[method]("[evident]", JSON.stringify({ level, event, ...fields }));
1296
+ const payload = Object.fromEntries(
1297
+ Object.entries({ level, event, ...fields }).filter(([, value]) => value !== void 0)
1298
+ );
1299
+ const secondArgument = globalThis.navigator?.userAgent === "Cloudflare-Workers" ? payload : JSON.stringify(payload);
1300
+ console[method]("[evident]", secondArgument);
1132
1301
  } catch (err) {
1133
1302
  console.error(
1134
1303
  "[evident] log_serialize_failed",
@@ -1157,7 +1326,7 @@ import ora4 from "ora";
1157
1326
  import { select as select4 } from "@inquirer/prompts";
1158
1327
 
1159
1328
  // src/lib/telemetry.ts
1160
- var CLI_VERSION = (true ? "3.5.2-dev.3431f9f" : void 0) ?? process.env.npm_package_version ?? "unknown";
1329
+ var CLI_VERSION = (true ? "3.5.2-dev.3c6c52c" : void 0) ?? process.env.npm_package_version ?? "unknown";
1161
1330
  function getCliVersion() {
1162
1331
  return CLI_VERSION;
1163
1332
  }
@@ -1318,8 +1487,591 @@ var EventTypes = {
1318
1487
  DEPRECATED_AGENT_KEY_ENV_USED: "cli.deprecated_agent_key_env_used"
1319
1488
  };
1320
1489
 
1321
- // src/lib/runner-activity-telemetry.ts
1490
+ // src/lib/opencode/session-db-reclaim.ts
1491
+ import { statSync as statSync2, statfsSync } from "node:fs";
1492
+ import { dirname as dirname2 } from "node:path";
1493
+
1494
+ // src/lib/diagnostic-redaction.ts
1322
1495
  import { homedir as homedir2 } from "node:os";
1496
+ function redactHomeDirectory(message) {
1497
+ const home = homedir2();
1498
+ if (!home || home === "/") {
1499
+ return message;
1500
+ }
1501
+ const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
1502
+ let result = "";
1503
+ let searchFrom = 0;
1504
+ let homeIndex;
1505
+ while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
1506
+ const homeEnd = homeIndex + home.length;
1507
+ const previousCharacter = message[homeIndex - 1];
1508
+ const nextCharacter = message[homeEnd];
1509
+ const atPathBoundary = (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
1510
+ result += message.slice(searchFrom, homeIndex);
1511
+ result += atPathBoundary ? "~" : home;
1512
+ searchFrom = homeEnd;
1513
+ }
1514
+ return result + message.slice(searchFrom);
1515
+ }
1516
+ function redactDiagnosticText(message) {
1517
+ 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>");
1518
+ return redactHomeDirectory(redacted);
1519
+ }
1520
+
1521
+ // src/lib/opencode/session-db-reclaim.ts
1522
+ var SESSION_DB_RECLAIM_CHUNK_PAGES = 100;
1523
+ var SESSION_DB_RECLAIM_TIME_BUDGET_MS = 500;
1524
+ var SESSION_DB_RECLAIM_PAGE_CEILING = 4e4;
1525
+ var SESSION_DB_RECLAIM_BUSY_TIMEOUT_MS = 5e3;
1526
+ function errorMessage(error2) {
1527
+ if (!(error2 instanceof Error)) return String(error2);
1528
+ return error2.cause instanceof Error ? error2.cause.message : error2.message;
1529
+ }
1530
+ function errorMessages(error2) {
1531
+ const messages = [];
1532
+ const visited = /* @__PURE__ */ new Set();
1533
+ let current = error2;
1534
+ while (current !== null && current !== void 0 && !visited.has(current)) {
1535
+ visited.add(current);
1536
+ if (typeof current === "string") {
1537
+ messages.push(current);
1538
+ break;
1539
+ }
1540
+ if (current instanceof Error) {
1541
+ messages.push(current.name, current.message);
1542
+ current = current.cause;
1543
+ continue;
1544
+ }
1545
+ if (typeof current === "object") {
1546
+ const record = current;
1547
+ if (typeof record.code === "string") messages.push(record.code);
1548
+ if (typeof record.message === "string") messages.push(record.message);
1549
+ current = record.cause;
1550
+ continue;
1551
+ }
1552
+ break;
1553
+ }
1554
+ return messages;
1555
+ }
1556
+ function isSessionStoreWriteFailure(error2) {
1557
+ return errorMessages(error2).some(
1558
+ (message) => /(?:database or disk is full|disk is full|no space left on device|SQLITE_(?:FULL|IOERR)(?:\b|_))/i.test(
1559
+ message
1560
+ )
1561
+ );
1562
+ }
1563
+ 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;
1564
+ var EFFECT_SQL_CAUSE_TOKEN = /(?:^|cause:\s+)(effect\/sql\/SqlError\/[A-Za-z][A-Za-z0-9_]*)/i;
1565
+ var PARAMS_FIELD = /\bparams\s*(?:=|:)/i;
1566
+ function isCauseBearingText(text) {
1567
+ return /^(?:SQLiteError:\s+|effect\/sql\/SqlError\/)/i.test(text.trim()) || /(?:^|\s)cause:\s+(?:SQLiteError:\s+|effect\/sql\/SqlError\/)/i.test(text);
1568
+ }
1569
+ function structuredLogFields(line) {
1570
+ const fields = [];
1571
+ for (let index = 0; index < line.length; index++) {
1572
+ if (line[index] === '"') {
1573
+ index++;
1574
+ while (index < line.length) {
1575
+ if (line[index] === "\\") {
1576
+ index++;
1577
+ } else if (line[index] === '"') {
1578
+ break;
1579
+ }
1580
+ index++;
1581
+ }
1582
+ continue;
1583
+ }
1584
+ const atFieldBoundary = index === 0 || /\s/.test(line[index - 1]);
1585
+ const key = line.startsWith("error=", index) ? "error=" : line.startsWith("cause=", index) ? "cause=" : null;
1586
+ if (!atFieldBoundary || key === null) continue;
1587
+ const valueStart = index + key.length;
1588
+ if (line[valueStart] !== '"') {
1589
+ const valueEnd2 = line.indexOf(" ", valueStart);
1590
+ fields.push(line.slice(valueStart, valueEnd2 === -1 ? line.length : valueEnd2));
1591
+ index = valueEnd2 === -1 ? line.length : valueEnd2;
1592
+ continue;
1593
+ }
1594
+ let valueEnd = valueStart + 1;
1595
+ for (; valueEnd < line.length; valueEnd++) {
1596
+ if (line[valueEnd] === "\\") {
1597
+ valueEnd++;
1598
+ } else if (line[valueEnd] === '"') {
1599
+ break;
1600
+ }
1601
+ }
1602
+ fields.push(line.slice(valueStart + 1, valueEnd));
1603
+ index = valueEnd;
1604
+ }
1605
+ return fields;
1606
+ }
1607
+ function structuredCauseMessages(text) {
1608
+ const fields = structuredLogFields(text);
1609
+ if (fields.length > 0) return fields;
1610
+ return isCauseBearingText(text) ? [text] : [];
1611
+ }
1612
+ function causeTokenFromText(text) {
1613
+ const paramsIndex = text.search(PARAMS_FIELD);
1614
+ for (const pattern of [SQLITE_CAUSE_TOKEN, EFFECT_SQL_CAUSE_TOKEN]) {
1615
+ const match = pattern.exec(text);
1616
+ if (match?.[1] && (paramsIndex === -1 || match.index < paramsIndex)) {
1617
+ return match[1];
1618
+ }
1619
+ }
1620
+ return null;
1621
+ }
1622
+ function sessionStoreCauseToken(value) {
1623
+ for (const message of structuredCauseMessages(value)) {
1624
+ const token = causeTokenFromText(message);
1625
+ if (token !== null) return redactDiagnosticText(token).slice(0, 120);
1626
+ }
1627
+ return null;
1628
+ }
1629
+ function insufficientSpaceReason(dbPath, requiredBytes) {
1630
+ try {
1631
+ const fsStats = statfsSync(dirname2(dbPath));
1632
+ const availableBytes = fsStats.bavail * fsStats.bsize;
1633
+ if (availableBytes < requiredBytes) {
1634
+ return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
1635
+ }
1636
+ return null;
1637
+ } catch (err) {
1638
+ return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
1639
+ }
1640
+ }
1641
+ function readLogicalBytes(db) {
1642
+ const pageCount = db.prepare("PRAGMA page_count").get().page_count;
1643
+ const pageSize = db.prepare("PRAGMA page_size").get().page_size;
1644
+ return pageCount * pageSize;
1645
+ }
1646
+ function readFreelistCount(db) {
1647
+ return db.prepare("PRAGMA freelist_count").get().freelist_count;
1648
+ }
1649
+ function readCheckpointResult(db) {
1650
+ db.exec("PRAGMA busy_timeout=0");
1651
+ const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
1652
+ return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
1653
+ }
1654
+ async function probeReclaimAvailability(input) {
1655
+ const { dbPath, requiredBytes } = input;
1656
+ let sqlite;
1657
+ try {
1658
+ sqlite = await import("node:sqlite");
1659
+ } catch (err) {
1660
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
1661
+ console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
1662
+ return { reason: "sqlite-unavailable", detail };
1663
+ }
1664
+ let autoVacuum = null;
1665
+ try {
1666
+ const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
1667
+ try {
1668
+ db.exec(`PRAGMA busy_timeout=${SESSION_DB_RECLAIM_BUSY_TIMEOUT_MS}`);
1669
+ autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
1670
+ } finally {
1671
+ db.close();
1672
+ }
1673
+ } catch (err) {
1674
+ console.warn(
1675
+ `[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
1676
+ );
1677
+ }
1678
+ if (autoVacuum !== 0) return null;
1679
+ return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
1680
+ }
1681
+ async function reclaimSessionDbSpace(input) {
1682
+ const {
1683
+ dbPath,
1684
+ pageCeiling = SESSION_DB_RECLAIM_PAGE_CEILING,
1685
+ timeBudgetMs = SESSION_DB_RECLAIM_TIME_BUDGET_MS,
1686
+ chunkPages = SESSION_DB_RECLAIM_CHUNK_PAGES,
1687
+ now = () => performance.now(),
1688
+ hasLiveTurn = false,
1689
+ hasProvenanceAnomaly = false
1690
+ } = input;
1691
+ let sqlite;
1692
+ try {
1693
+ sqlite = await import("node:sqlite");
1694
+ } catch (err) {
1695
+ const detail = `Node ${process.version}: ${errorMessage(err)}`;
1696
+ console.warn(
1697
+ `[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
1698
+ );
1699
+ return { ok: false, skipped: "sqlite-unavailable", detail };
1700
+ }
1701
+ const { DatabaseSync } = sqlite;
1702
+ let db;
1703
+ try {
1704
+ db = new DatabaseSync(dbPath);
1705
+ db.exec(`PRAGMA busy_timeout=${SESSION_DB_RECLAIM_BUSY_TIMEOUT_MS}`);
1706
+ const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
1707
+ if (autoVacuum === 0) {
1708
+ if (hasLiveTurn || hasProvenanceAnomaly) {
1709
+ const detail = [
1710
+ ...hasLiveTurn ? ["a session turn is live"] : [],
1711
+ ...hasProvenanceAnomaly ? ["session-DB provenance is anomalous"] : []
1712
+ ].join(" and ");
1713
+ console.warn(`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${detail}`);
1714
+ return {
1715
+ ok: false,
1716
+ skipped: "full-vacuum-blocked",
1717
+ detail
1718
+ };
1719
+ }
1720
+ const fileBytesForGuard = statSync2(dbPath).size;
1721
+ const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
1722
+ if (skipReason !== null) {
1723
+ console.warn(
1724
+ `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
1725
+ );
1726
+ return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
1727
+ }
1728
+ const beforeBytes = readLogicalBytes(db);
1729
+ const freelistBefore = readFreelistCount(db);
1730
+ const startedAt = now();
1731
+ db.exec("PRAGMA auto_vacuum=INCREMENTAL");
1732
+ db.exec("VACUUM");
1733
+ const afterBytes = readLogicalBytes(db);
1734
+ const freelistAfter = readFreelistCount(db);
1735
+ const checkpointStartedAt = now();
1736
+ const checkpoint = readCheckpointResult(db);
1737
+ const checkpointElapsedMs = Math.max(0, now() - checkpointStartedAt);
1738
+ return {
1739
+ ok: true,
1740
+ mode: "convert",
1741
+ beforeBytes,
1742
+ afterBytes,
1743
+ freelistBefore,
1744
+ freelistAfter,
1745
+ pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
1746
+ stoppedBy: "freelist-drained",
1747
+ elapsedMs: Math.max(0, now() - startedAt),
1748
+ checkpointElapsedMs,
1749
+ checkpoint
1750
+ };
1751
+ }
1752
+ if (autoVacuum === 2) {
1753
+ if (hasLiveTurn) {
1754
+ const detail = "a session turn is live";
1755
+ console.warn(
1756
+ `[reclaimSessionDbSpace] skipping incremental reclaim of ${dbPath}: ${detail}`
1757
+ );
1758
+ return { ok: false, skipped: "live-turn", detail };
1759
+ }
1760
+ const beforeBytes = readLogicalBytes(db);
1761
+ const freelistBefore = readFreelistCount(db);
1762
+ const startedAt = now();
1763
+ const pageLimit = Math.max(0, Math.trunc(pageCeiling));
1764
+ const chunkLimit = Math.max(1, Math.trunc(chunkPages));
1765
+ const budget = Math.max(0, timeBudgetMs);
1766
+ let pagesAttempted = 0;
1767
+ let freelistAfter = freelistBefore;
1768
+ let stoppedBy = "freelist-drained";
1769
+ while (freelistAfter > 0) {
1770
+ if (pagesAttempted >= pageLimit) {
1771
+ stoppedBy = "page-ceiling";
1772
+ break;
1773
+ }
1774
+ if (now() - startedAt >= budget) {
1775
+ stoppedBy = "time-budget";
1776
+ break;
1777
+ }
1778
+ const pages = Math.min(chunkLimit, pageLimit - pagesAttempted, freelistAfter);
1779
+ db.exec(`PRAGMA incremental_vacuum(${pages})`);
1780
+ pagesAttempted += pages;
1781
+ freelistAfter = readFreelistCount(db);
1782
+ }
1783
+ const afterBytes = readLogicalBytes(db);
1784
+ const checkpointStartedAt = now();
1785
+ const checkpoint = readCheckpointResult(db);
1786
+ const checkpointElapsedMs = Math.max(0, now() - checkpointStartedAt);
1787
+ return {
1788
+ ok: true,
1789
+ mode: "incremental",
1790
+ beforeBytes,
1791
+ afterBytes,
1792
+ freelistBefore,
1793
+ freelistAfter,
1794
+ pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
1795
+ stoppedBy,
1796
+ elapsedMs: Math.max(0, now() - startedAt),
1797
+ checkpointElapsedMs,
1798
+ checkpoint
1799
+ };
1800
+ }
1801
+ console.warn(
1802
+ `[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
1803
+ );
1804
+ return { ok: false, skipped: "auto-vacuum-not-applicable" };
1805
+ } catch (err) {
1806
+ console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
1807
+ return {
1808
+ ok: false,
1809
+ skipped: isSessionStoreWriteFailure(err) ? "disk-exhaustion" : "reclaim-error",
1810
+ detail: errorMessage(err)
1811
+ };
1812
+ } finally {
1813
+ db?.close();
1814
+ }
1815
+ }
1816
+
1817
+ // src/lib/opencode/dependency-install-diagnostic.ts
1818
+ import { lstatSync, realpathSync } from "node:fs";
1819
+ import { basename, dirname as dirname3, isAbsolute, join as join2, relative, resolve, sep } from "node:path";
1820
+ var DEPENDENCY_INSTALL_FAILURE_MESSAGE = "background dependency install failed";
1821
+ var MAX_DISTINCT_DIAGNOSTICS = 32;
1822
+ var EFFECT = "OpenCode could not install background dependencies for this scope; its plugins and tools may be unavailable";
1823
+ function readOpenCodeLogField(line, key) {
1824
+ const match = line.match(new RegExp(`(?:^|\\s)${key}=(?:"((?:\\\\.|[^"])*)"|(\\S+))`));
1825
+ if (!match) return void 0;
1826
+ if (match[1] === void 0) return match[2];
1827
+ return match[1].replace(/\\(["\\n])/g, (_match, escaped) => {
1828
+ return escaped === "n" ? "\n" : escaped;
1829
+ });
1830
+ }
1831
+ function parseTrigger(line) {
1832
+ if (readOpenCodeLogField(line, "message") !== DEPENDENCY_INSTALL_FAILURE_MESSAGE) {
1833
+ return null;
1834
+ }
1835
+ const error2 = readOpenCodeLogField(line, "error");
1836
+ if (!error2 || !/\bEACCES\b/.test(error2)) return null;
1837
+ const errnoMatch = error2.match(/\bEACCES:\s*permission denied,\s*([a-z_]+)\s+'([^']*)'/);
1838
+ const capturedPath = errnoMatch?.[2];
1839
+ return {
1840
+ dir: readOpenCodeLogField(line, "dir"),
1841
+ op: errnoMatch?.[1],
1842
+ path: capturedPath && isAbsolute(capturedPath) ? capturedPath : void 0
1843
+ };
1844
+ }
1845
+ function getErrorCode(error2) {
1846
+ if (typeof error2 === "object" && error2 !== null && "code" in error2) {
1847
+ const code = error2.code;
1848
+ if (typeof code === "string") return code;
1849
+ }
1850
+ return "UNKNOWN";
1851
+ }
1852
+ function isWithin(path, root) {
1853
+ const candidate = resolve(path);
1854
+ const resolvedRoot = resolve(root);
1855
+ return candidate === resolvedRoot || candidate.startsWith(`${resolvedRoot}${sep}`);
1856
+ }
1857
+ function rootSpecs(context) {
1858
+ return [
1859
+ { token: "~", configured: resolve(context.homeDir) },
1860
+ { token: "$WORKSPACE", configured: resolve(context.cwd) }
1861
+ ];
1862
+ }
1863
+ function resolveRoots(context) {
1864
+ const resolved = [];
1865
+ const unresolved = [];
1866
+ for (const spec of rootSpecs(context)) {
1867
+ try {
1868
+ resolved.push({ ...spec, real: realpathSync(spec.configured) });
1869
+ } catch (_error) {
1870
+ unresolved.push(spec);
1871
+ }
1872
+ }
1873
+ return { resolved, unresolved };
1874
+ }
1875
+ function resolveNearestAncestor(failingPath, remaining, error2) {
1876
+ let ancestor = dirname3(failingPath);
1877
+ const suffix = [basename(failingPath), ...remaining];
1878
+ while (ancestor) {
1879
+ try {
1880
+ return {
1881
+ path: resolve(realpathSync(ancestor), ...suffix),
1882
+ failure: { path: resolve(failingPath), error: error2 }
1883
+ };
1884
+ } catch (ancestorError) {
1885
+ if (ancestor === dirname3(ancestor) || !getErrorCode(ancestorError).startsWith("E")) {
1886
+ return null;
1887
+ }
1888
+ suffix.unshift(basename(ancestor));
1889
+ ancestor = dirname3(ancestor);
1890
+ }
1891
+ }
1892
+ return null;
1893
+ }
1894
+ function resolveExistingPath(absolutePath) {
1895
+ if (!isAbsolute(absolutePath)) return null;
1896
+ const candidate = resolve(absolutePath);
1897
+ const remaining = [];
1898
+ let current = candidate;
1899
+ while (current) {
1900
+ try {
1901
+ const stat2 = lstatSync(current);
1902
+ if (stat2.isSymbolicLink()) {
1903
+ if (remaining.length === 0) {
1904
+ return { path: join2(realpathSync(dirname3(current)), basename(current)) };
1905
+ }
1906
+ return { path: resolve(realpathSync(current), ...remaining) };
1907
+ }
1908
+ return { path: resolve(realpathSync(current), ...remaining) };
1909
+ } catch (error2) {
1910
+ const code = getErrorCode(error2);
1911
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
1912
+ return resolveNearestAncestor(current, remaining, error2);
1913
+ }
1914
+ }
1915
+ if (current === dirname3(current)) return null;
1916
+ remaining.unshift(basename(current));
1917
+ current = dirname3(current);
1918
+ }
1919
+ return null;
1920
+ }
1921
+ function tokenise(absolutePath, roots) {
1922
+ const pathResolution = resolveExistingPath(absolutePath);
1923
+ if (!pathResolution) return null;
1924
+ const root = roots.reduce((selected, candidate) => {
1925
+ if (!isWithin(pathResolution.path, candidate.real)) return selected;
1926
+ if (!selected || candidate.real.length > selected.real.length || candidate.real.length === selected.real.length && candidate.token === "$WORKSPACE") {
1927
+ return candidate;
1928
+ }
1929
+ return selected;
1930
+ }, null);
1931
+ if (!root) return null;
1932
+ const suffix = relative(root.real, pathResolution.path);
1933
+ return {
1934
+ value: suffix ? `${root.token}/${suffix.split(sep).join("/")}` : root.token,
1935
+ root,
1936
+ resolutionFailure: pathResolution.failure
1937
+ };
1938
+ }
1939
+ function classifyScope(dir, roots) {
1940
+ if (!dir) return "other";
1941
+ const tokenisedDir = tokenise(dir, roots.resolved);
1942
+ if (tokenisedDir) return tokenisedDir.root.token === "$WORKSPACE" ? "workspace" : "global";
1943
+ for (const spec of roots.unresolved) {
1944
+ if (isWithin(dir, spec.configured)) {
1945
+ return spec.token === "$WORKSPACE" ? "workspace" : "global";
1946
+ }
1947
+ }
1948
+ return "other";
1949
+ }
1950
+ function unresolvedRootForPath(path, unresolved) {
1951
+ return unresolved.find((spec) => isWithin(path, spec.configured));
1952
+ }
1953
+ function statType(stat2) {
1954
+ if (stat2.isDirectory()) return "d";
1955
+ if (stat2.isFile()) return "f";
1956
+ if (stat2.isSymbolicLink()) return "l";
1957
+ return "o";
1958
+ }
1959
+ function renderStat(name, stat2) {
1960
+ const mode = (stat2.mode & 4095).toString(8).padStart(4, "0");
1961
+ return `${name}:${statType(stat2)}:${stat2.uid}:${stat2.gid}:${mode}`;
1962
+ }
1963
+ function renderStatError(name, error2) {
1964
+ const code = getErrorCode(error2);
1965
+ return `${name}:${renderErrorCode(code)}`;
1966
+ }
1967
+ function renderErrorCode(code) {
1968
+ if (code === "ENOENT") return "missing";
1969
+ return code.startsWith("E") ? code : `E${code}`;
1970
+ }
1971
+ function inspectChain(rootReal, rootToken, absolutePath, resolutionFailure) {
1972
+ const target = resolve(absolutePath);
1973
+ const relativePath = relative(rootReal, target);
1974
+ const components = relativePath ? relativePath.split(sep) : [];
1975
+ const entries = [];
1976
+ let current = rootReal;
1977
+ let status2 = "complete";
1978
+ let finalStat;
1979
+ for (const name of [rootToken, ...components]) {
1980
+ if (name !== rootToken) current = join2(current, name);
1981
+ if (resolutionFailure && resolve(current) === resolve(resolutionFailure.path)) {
1982
+ entries.push(renderStatError(name, resolutionFailure.error));
1983
+ status2 = "stat_failed";
1984
+ break;
1985
+ }
1986
+ try {
1987
+ const stat2 = lstatSync(current);
1988
+ entries.push(renderStat(name, stat2));
1989
+ finalStat = stat2;
1990
+ } catch (error2) {
1991
+ entries.push(renderStatError(name, error2));
1992
+ if (getErrorCode(error2) !== "ENOENT") status2 = "stat_failed";
1993
+ break;
1994
+ }
1995
+ }
1996
+ let symlinkTarget;
1997
+ let symlinkMeta;
1998
+ if (finalStat?.isSymbolicLink() && entries.length === components.length + 1) {
1999
+ try {
2000
+ symlinkTarget = realpathSync(current);
2001
+ try {
2002
+ symlinkMeta = renderStat("", lstatSync(symlinkTarget)).slice(1);
2003
+ } catch (error2) {
2004
+ symlinkMeta = renderErrorCode(getErrorCode(error2));
2005
+ status2 = "stat_failed";
2006
+ }
2007
+ } catch (error2) {
2008
+ symlinkTarget = "<unresolved>";
2009
+ symlinkMeta = renderErrorCode(getErrorCode(error2));
2010
+ status2 = "stat_failed";
2011
+ }
2012
+ }
2013
+ return { status: status2, chain: entries.join(","), symlinkTarget, symlinkMeta };
2014
+ }
2015
+ function buildDiagnostic(line, context) {
2016
+ const trigger = parseTrigger(line);
2017
+ if (!trigger) return null;
2018
+ const roots = resolveRoots(context);
2019
+ const scope = classifyScope(trigger.dir, roots);
2020
+ const pathSource = trigger.path ? "error" : trigger.dir ? "dir" : "none";
2021
+ const inspectedPath = trigger.path ?? trigger.dir;
2022
+ const microvmId = context.microvmId?.trim() || "unknown";
2023
+ const euid = context.euid === void 0 ? "unavailable" : String(context.euid);
2024
+ const egid = context.egid === void 0 ? "unavailable" : String(context.egid);
2025
+ const op = trigger.op ?? "unknown";
2026
+ if (!inspectedPath) {
2027
+ return {
2028
+ 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`,
2029
+ localDetail: null
2030
+ };
2031
+ }
2032
+ const tokenisedPath = tokenise(inspectedPath, roots.resolved);
2033
+ if (!tokenisedPath) {
2034
+ const unresolvedRoot = unresolvedRootForPath(inspectedPath, roots.unresolved);
2035
+ if (unresolvedRoot) {
2036
+ return {
2037
+ 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`,
2038
+ localDetail: null
2039
+ };
2040
+ }
2041
+ return {
2042
+ 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`,
2043
+ localDetail: `OPENCODE-DEPENDENCY-INSTALL-EACCES (local only): outside approved roots: ${inspectedPath}`
2044
+ };
2045
+ }
2046
+ const inspection = inspectChain(
2047
+ tokenisedPath.root.real,
2048
+ tokenisedPath.root.token,
2049
+ inspectedPath,
2050
+ tokenisedPath.resolutionFailure
2051
+ );
2052
+ const optionalSymlinkFields = inspection.symlinkTarget ? ` symlink_target=${tokenise(inspection.symlinkTarget, roots.resolved)?.value ?? "<outside-approved-roots>"}` + (inspection.symlinkMeta ? ` symlink_meta=${inspection.symlinkMeta}` : "") : "";
2053
+ return {
2054
+ 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}`,
2055
+ localDetail: null
2056
+ };
2057
+ }
2058
+ function createDependencyInstallDiagnosticReporter({
2059
+ context,
2060
+ emitEvent: emitEvent2,
2061
+ emitLocal
2062
+ }) {
2063
+ const emittedEvents = /* @__PURE__ */ new Set();
2064
+ return (line) => {
2065
+ const diagnostic = buildDiagnostic(line, context);
2066
+ if (!diagnostic || emittedEvents.has(diagnostic.event)) return;
2067
+ if (emittedEvents.size >= MAX_DISTINCT_DIAGNOSTICS) return;
2068
+ emittedEvents.add(diagnostic.event);
2069
+ emitEvent2(diagnostic.event);
2070
+ if (diagnostic.localDetail) emitLocal(diagnostic.localDetail);
2071
+ };
2072
+ }
2073
+
2074
+ // src/lib/runner-activity-telemetry.ts
1323
2075
  var FORWARDED_LEVELS_BY_SOURCE = {
1324
2076
  "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
1325
2077
  opencode: /* @__PURE__ */ new Set(["warn", "error"]),
@@ -1336,11 +2088,13 @@ function parseOpenCodeLogLine(line) {
1336
2088
  if (!levelMatch) return null;
1337
2089
  const level = levelMatch[1].toUpperCase();
1338
2090
  if (level !== "WARN" && level !== "ERROR") return null;
1339
- const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
2091
+ const sessionMatch = normalisedLine.match(/(?:^|\s)(?:sessionID|session\.id)=(\S+)/);
2092
+ const messageIDMatch = normalisedLine.match(/(?:^|\s)messageID=(\S+)/);
1340
2093
  const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
1341
2094
  const parsed = {
1342
2095
  level: level === "WARN" ? "warn" : "error",
1343
- sessionID: sessionMatch?.[1]
2096
+ sessionID: sessionMatch?.[1],
2097
+ messageID: messageIDMatch?.[1]
1344
2098
  };
1345
2099
  const message = messageMatch?.[1] ?? messageMatch?.[2];
1346
2100
  if (message !== void 0) parsed.message = message;
@@ -1348,18 +2102,29 @@ function parseOpenCodeLogLine(line) {
1348
2102
  }
1349
2103
  var MAX_LINE_BUFFER_BYTES = 16 * 1024;
1350
2104
  var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
1351
- function createOpenCodeActivityForwarder(getContext) {
2105
+ function sanitiseDependencyInstallWarning(line) {
2106
+ const withoutDirectory = line.replace(
2107
+ /((?:^|\s)dir=)(?:"(?:\\.|[^"])*"|\/[^\s)"']+|\S+)/g,
2108
+ "$1<path>"
2109
+ );
2110
+ return withoutDirectory.replace(
2111
+ /((?:^|\s)error=)"((?:\\.|[^"])*)"/,
2112
+ (_match, prefix, error2) => `${prefix}"${error2.replace(/'\/[^']*'/g, "'<path>'")}"`
2113
+ );
2114
+ }
2115
+ function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure, onDependencyInstallFailure) {
1352
2116
  let buffer = Buffer.alloc(0);
1353
2117
  const repeatStates = /* @__PURE__ */ new Map();
1354
2118
  const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
1355
2119
  const metadata = {
1356
2120
  ...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
2121
+ ...parsed.messageID ? { messageID: parsed.messageID } : {},
1357
2122
  ...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
1358
2123
  };
1359
2124
  forwardRunnerActivity(
1360
2125
  {
1361
2126
  level: parsed.level,
1362
- error: line,
2127
+ error: parsed.message === DEPENDENCY_INSTALL_FAILURE_MESSAGE ? sanitiseDependencyInstallWarning(line) : line,
1363
2128
  metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
1364
2129
  source: "opencode"
1365
2130
  },
@@ -1377,11 +2142,9 @@ function createOpenCodeActivityForwarder(getContext) {
1377
2142
  repeatStates.delete(key);
1378
2143
  }
1379
2144
  };
1380
- const flushLine = (line) => {
1381
- const parsed = parseOpenCodeLogLine(line);
1382
- if (!parsed) return;
2145
+ const forwardWithRepeatSuppression = (line, parsed) => {
1383
2146
  const now = Date.now();
1384
- const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}`;
2147
+ const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}\0${parsed.sessionID ?? ""}\0${parsed.messageID ?? ""}`;
1385
2148
  expireRepeatStates(now, key);
1386
2149
  if (key === void 0) {
1387
2150
  forwardLine(line, parsed);
@@ -1398,9 +2161,38 @@ function createOpenCodeActivityForwarder(getContext) {
1398
2161
  });
1399
2162
  return;
1400
2163
  }
1401
- previous.line = line;
1402
- previous.parsed = parsed;
1403
- previous.repeatsSuppressed++;
2164
+ previous.line = line;
2165
+ previous.parsed = parsed;
2166
+ previous.repeatsSuppressed++;
2167
+ };
2168
+ const flushLine = (line) => {
2169
+ const parsed = parseOpenCodeLogLine(line);
2170
+ const isDiskFailure = isSessionStoreWriteFailure(line);
2171
+ const cause = sessionStoreCauseToken(line);
2172
+ if (onSessionStoreWriteFailure && (isDiskFailure || cause !== null)) {
2173
+ try {
2174
+ onSessionStoreWriteFailure({
2175
+ isDiskFailure,
2176
+ sessionId: parsed?.sessionID ?? null,
2177
+ cause
2178
+ });
2179
+ } catch (err) {
2180
+ reportTelemetryDiagnostic(
2181
+ `Runner activity telemetry could not record session-store write failure: ${err instanceof Error ? err.message : String(err)}`
2182
+ );
2183
+ }
2184
+ }
2185
+ if (!parsed) return;
2186
+ forwardWithRepeatSuppression(line, parsed);
2187
+ if (onDependencyInstallFailure && parsed.message === DEPENDENCY_INSTALL_FAILURE_MESSAGE) {
2188
+ try {
2189
+ onDependencyInstallFailure(line);
2190
+ } catch (err) {
2191
+ reportTelemetryDiagnostic(
2192
+ `Runner activity telemetry could not record dependency-install failure diagnostic: ${err instanceof Error ? err.message : String(err)}`
2193
+ );
2194
+ }
2195
+ }
1404
2196
  };
1405
2197
  const forward = ((chunk) => {
1406
2198
  buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
@@ -1427,34 +2219,38 @@ function createOpenCodeActivityForwarder(getContext) {
1427
2219
  var MAX_MESSAGE_LENGTH = 500;
1428
2220
  var MAX_METADATA_VALUE_LENGTH = 200;
1429
2221
  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
2222
  function truncate(message) {
1456
2223
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
1457
- return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
2224
+ let dropped = message.length - MAX_MESSAGE_LENGTH;
2225
+ let marker = `\u2026${dropped} chars omitted\u2026`;
2226
+ let previousDropped = -1;
2227
+ while (dropped !== previousDropped) {
2228
+ previousDropped = dropped;
2229
+ const nextDropped = message.length - MAX_MESSAGE_LENGTH + marker.length;
2230
+ dropped = nextDropped;
2231
+ marker = `\u2026${dropped} chars omitted\u2026`;
2232
+ }
2233
+ const remaining = MAX_MESSAGE_LENGTH - marker.length;
2234
+ const headLength = Math.ceil(remaining * 0.6);
2235
+ const tailLength = remaining - headLength;
2236
+ const effectMatch = message.startsWith("OPENCODE-DEPENDENCY-INSTALL-EACCES:") ? message.match(/ effect="[^"]*"/) : null;
2237
+ if (effectMatch?.index !== void 0) {
2238
+ const effectEnd = effectMatch.index + effectMatch[0].length;
2239
+ if (effectEnd > headLength && effectEnd < message.length) {
2240
+ const effectLength = effectMatch[0].length;
2241
+ const nonEffectLength = remaining - effectLength;
2242
+ const preservedTailLength = Math.min(
2243
+ message.length - effectEnd,
2244
+ nonEffectLength - Math.min(effectMatch.index, Math.ceil(nonEffectLength * 0.6))
2245
+ );
2246
+ const preservedHeadLength = Math.min(
2247
+ effectMatch.index,
2248
+ nonEffectLength - preservedTailLength
2249
+ );
2250
+ return message.slice(0, preservedHeadLength) + marker + message.slice(effectMatch.index, effectEnd) + message.slice(message.length - preservedTailLength);
2251
+ }
2252
+ }
2253
+ return message.slice(0, headLength) + marker + message.slice(message.length - tailLength);
1458
2254
  }
1459
2255
  function sanitiseMetadata(metadata) {
1460
2256
  if (!metadata) return {};
@@ -1531,10 +2327,10 @@ function forwardRunnerActivity(entry, context) {
1531
2327
 
1532
2328
  // src/lib/opencode/session-db-recovery-report.ts
1533
2329
  import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
1534
- import { join as join2 } from "node:path";
2330
+ import { join as join3 } from "node:path";
1535
2331
  function sessionDbRecoveryReportPath(homeDir, env) {
1536
2332
  const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
1537
- return override || join2(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
2333
+ return override || join3(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
1538
2334
  }
1539
2335
  function drainSessionDbRecoveryReport({
1540
2336
  homeDir,
@@ -1664,12 +2460,23 @@ function buildSessionDbRecoveryActivity(record) {
1664
2460
  metadata: withoutContractFields(record),
1665
2461
  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
2462
  };
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
- };
2463
+ case "session_db_boot_refused": {
2464
+ const message = (() => {
2465
+ switch (record.reason) {
2466
+ case "local_discard_failed":
2467
+ 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.`;
2468
+ case "replica_separation_unproven":
2469
+ 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.`;
2470
+ case "verification_timeout_before_separation":
2471
+ 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.";
2472
+ case "verification_timeout_during_separation":
2473
+ 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.";
2474
+ default:
2475
+ 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.";
2476
+ }
2477
+ })();
2478
+ return { level, metadata: withoutContractFields(record), message };
2479
+ }
1673
2480
  case "schema_provenance_mismatch":
1674
2481
  return {
1675
2482
  level,
@@ -1796,7 +2603,7 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1796
2603
  if (health.healthy || health.authFailed) {
1797
2604
  return health;
1798
2605
  }
1799
- await new Promise((resolve4) => setTimeout(resolve4, 1e3));
2606
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
1800
2607
  }
1801
2608
  return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1802
2609
  }
@@ -1807,7 +2614,7 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1807
2614
  if (health.healthy || health.authFailed) {
1808
2615
  return health;
1809
2616
  }
1810
- await new Promise((resolve4) => setTimeout(resolve4, 1e3));
2617
+ await new Promise((resolve5) => setTimeout(resolve5, 1e3));
1811
2618
  }
1812
2619
  return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
1813
2620
  }
@@ -1919,7 +2726,7 @@ async function getOpenCodeDirectory(port, client) {
1919
2726
  return null;
1920
2727
  }
1921
2728
  }
1922
- function roleOf(m) {
2729
+ function roleOf2(m) {
1923
2730
  if (!m || typeof m !== "object") return void 0;
1924
2731
  if (typeof m.role === "string") return m.role;
1925
2732
  const infoRole = m.info?.role;
@@ -1933,13 +2740,13 @@ function createdOf(m) {
1933
2740
  if (!m || typeof m !== "object") return void 0;
1934
2741
  return m.info?.time?.created ?? m.time?.created;
1935
2742
  }
1936
- function idOf(m) {
2743
+ function idOf2(m) {
1937
2744
  if (!m || typeof m !== "object") return void 0;
1938
2745
  if (typeof m.id === "string") return m.id;
1939
2746
  const infoId = m.info?.id;
1940
2747
  return typeof infoId === "string" ? infoId : void 0;
1941
2748
  }
1942
- function parentIdOf(m) {
2749
+ function parentIdOf2(m) {
1943
2750
  if (!m || typeof m !== "object") return void 0;
1944
2751
  if (typeof m.parentID === "string") return m.parentID;
1945
2752
  const infoParent = m.info?.parentID;
@@ -1955,8 +2762,12 @@ function errorOf(m) {
1955
2762
  if (!m || typeof m !== "object") return void 0;
1956
2763
  return m.info?.error ?? m.error;
1957
2764
  }
2765
+ function isTerminalStopAnswer(m) {
2766
+ return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
2767
+ }
1958
2768
  function isAssistantInFlight(m) {
1959
2769
  if (completedOf(m) == null) return true;
2770
+ if (errorOf(m) != null) return false;
1960
2771
  return finishOf(m) === "tool-calls";
1961
2772
  }
1962
2773
  async function getSessionMessages(port, sessionId, client) {
@@ -2004,9 +2815,10 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
2004
2815
  function isSessionActivelyGenerating(messages) {
2005
2816
  if (!messages || messages.length === 0) return false;
2006
2817
  const last = messages[messages.length - 1];
2007
- if (roleOf(last) !== "assistant") return false;
2818
+ if (roleOf2(last) !== "assistant") return false;
2008
2819
  return completedOf(last) == null;
2009
2820
  }
2821
+ var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
2010
2822
  function sessionLastActivityMs(session) {
2011
2823
  const candidates = [
2012
2824
  session.time?.updated,
@@ -2021,30 +2833,71 @@ function sessionLastActivityMs(session) {
2021
2833
  }
2022
2834
  return null;
2023
2835
  }
2024
- async function listSessions(port, client) {
2025
- if (client?.version === "v2") return listV2Sessions(client);
2836
+ function sessionListPath(options) {
2837
+ const params = new URLSearchParams();
2838
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
2839
+ const query = params.toString();
2840
+ return query ? `/session?${query}` : "/session";
2841
+ }
2842
+ async function listSessionPage(port, client, options) {
2843
+ const path = sessionListPath(options);
2026
2844
  try {
2027
- const res = await requestWithClient(port, client, "/session");
2845
+ const res = await requestWithClient(port, client, path);
2028
2846
  if (!res.ok) return null;
2029
2847
  const body = await res.json();
2030
2848
  return Array.isArray(body) ? body : null;
2031
2849
  } catch (error2) {
2032
2850
  console.error(
2033
- `[listSessions] GET /session failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2851
+ `[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2034
2852
  );
2035
2853
  return null;
2036
2854
  }
2037
2855
  }
2856
+ async function listSessions(port, client, options) {
2857
+ if (client?.version === "v2") return listV2Sessions(client);
2858
+ return listSessionPage(port, client, options);
2859
+ }
2860
+ async function listAllSessions(port, client) {
2861
+ if (client?.version === "v2") {
2862
+ const sessions2 = await listV2Sessions(client);
2863
+ return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
2864
+ }
2865
+ const sessions = await listSessionPage(port, client, {
2866
+ limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
2867
+ });
2868
+ if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
2869
+ if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
2870
+ return { sessions: null, reason: "session-enumeration-limit-bound" };
2871
+ }
2872
+ return { sessions };
2873
+ }
2038
2874
  async function deleteSession(port, id, client) {
2039
2875
  if (client?.version === "v2") return deleteV2Session(client, id);
2040
2876
  try {
2041
- const res = await requestWithClient(port, client, `/session/${id}`, { method: "DELETE" });
2042
- return res.status >= 200 && res.status < 300;
2877
+ const res = await requestWithClient(
2878
+ port,
2879
+ client,
2880
+ `/session/${id}`,
2881
+ { method: "DELETE" },
2882
+ { allowStatuses: ALL_HTTP_STATUSES }
2883
+ );
2884
+ if (res.status >= 200 && res.status < 300) {
2885
+ return { ok: true, status: res.status };
2886
+ }
2887
+ try {
2888
+ const body = await res.text();
2889
+ return body ? { ok: false, status: res.status, body } : { ok: false, status: res.status };
2890
+ } catch (error2) {
2891
+ console.error(
2892
+ `[deleteSession] DELETE /session/${id} response body read failed for HTTP ${res.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
2893
+ );
2894
+ return { ok: false, status: res.status };
2895
+ }
2043
2896
  } catch (error2) {
2044
2897
  console.error(
2045
2898
  `[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
2046
2899
  );
2047
- return false;
2900
+ return { ok: false, status: "threw" };
2048
2901
  }
2049
2902
  }
2050
2903
  async function sessionExists(port, id, client) {
@@ -2282,11 +3135,31 @@ function messageText(m) {
2282
3135
  if (!m || !Array.isArray(m.parts)) return "";
2283
3136
  return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
2284
3137
  }
2285
- async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
3138
+ function findDispatchedUserMessageId(messages, content, excludeIds) {
3139
+ if (!messages || messages.length === 0) return null;
3140
+ let best = null;
3141
+ for (const message of messages) {
3142
+ if (roleOf2(message) !== "user") continue;
3143
+ const id = idOf2(message);
3144
+ if (typeof id !== "string" || excludeIds?.has(id)) continue;
3145
+ if (messageText(message) !== content) continue;
3146
+ const created = createdOf(message) ?? 0;
3147
+ if (best === null || created > best.created) best = { id, created };
3148
+ }
3149
+ return best?.id ?? null;
3150
+ }
3151
+ async function sendPromptAsync(port, sessionId, content, options, attachments, client, callbacks) {
2286
3152
  const before = await getSessionMessages(port, sessionId, client);
2287
3153
  const knownUserIds = new Set(
2288
- (before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
3154
+ (before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
2289
3155
  );
3156
+ if (callbacks) {
3157
+ try {
3158
+ callbacks.onKnownUserIds(before === null ? null : knownUserIds);
3159
+ } catch (error2) {
3160
+ callbacks.onKnownUserIdsError(error2);
3161
+ }
3162
+ }
2290
3163
  const parts = [{ type: "text", text: content }];
2291
3164
  let pendingOutcomes = null;
2292
3165
  if (attachments && attachments.inputs.length > 0) {
@@ -2325,25 +3198,13 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2325
3198
  const READ_BACK_DELAY_MS = 150;
2326
3199
  for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
2327
3200
  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
- }
3201
+ const dispatchedUserMessageId = findDispatchedUserMessageId(after, content, knownUserIds);
3202
+ if (dispatchedUserMessageId !== null) {
3203
+ if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
3204
+ return dispatchedUserMessageId;
2344
3205
  }
2345
3206
  if (attempt < READ_BACK_ATTEMPTS - 1) {
2346
- await new Promise((resolve4) => setTimeout(resolve4, READ_BACK_DELAY_MS));
3207
+ await new Promise((resolve5) => setTimeout(resolve5, READ_BACK_DELAY_MS));
2347
3208
  }
2348
3209
  }
2349
3210
  return null;
@@ -2351,57 +3212,68 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
2351
3212
  function findAssistantReplyAfter(messages, userMessageId) {
2352
3213
  if (!messages || messages.length === 0) return null;
2353
3214
  const byParent = messages.find(
2354
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3215
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2355
3216
  );
2356
3217
  if (byParent) return byParent;
2357
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
3218
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2358
3219
  if (userIndex === -1) return null;
2359
3220
  for (let i = userIndex + 1; i < messages.length; i++) {
2360
- if (roleOf(messages[i]) === "assistant") return messages[i];
3221
+ if (roleOf2(messages[i]) === "assistant") return messages[i];
2361
3222
  }
2362
3223
  return null;
2363
3224
  }
2364
3225
  function findLastAssistantReplyFor(messages, userMessageId) {
2365
3226
  if (!messages || messages.length === 0) return null;
2366
3227
  let lastCorrelated = null;
2367
- let lastNonErrored = null;
2368
3228
  for (let i = messages.length - 1; i >= 0; i--) {
2369
3229
  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;
3230
+ if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
3231
+ lastCorrelated = m;
3232
+ break;
3233
+ }
3234
+ if (lastCorrelated) {
3235
+ if (errorOf(lastCorrelated) == null) return lastCorrelated;
3236
+ for (let i = messages.length - 1; i >= 0; i--) {
3237
+ const m = messages[i];
3238
+ if (roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId && isTerminalStopAnswer(m)) {
3239
+ return m;
3240
+ }
2375
3241
  }
3242
+ return lastCorrelated;
2376
3243
  }
2377
- if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
2378
- const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
3244
+ const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
2379
3245
  if (userIndex === -1) return null;
2380
3246
  let last = null;
2381
- let lastOk = null;
3247
+ let lastAssistantIndex = -1;
2382
3248
  for (let i = userIndex + 1; i < messages.length; i++) {
2383
- const role = roleOf(messages[i]);
3249
+ const role = roleOf2(messages[i]);
2384
3250
  if (role === "user") break;
2385
3251
  if (role === "assistant") {
2386
3252
  last = messages[i];
2387
- if (errorOf(messages[i]) == null) lastOk = messages[i];
3253
+ lastAssistantIndex = i;
2388
3254
  }
2389
3255
  }
2390
- return lastOk ?? last;
3256
+ if (last === null || errorOf(last) == null) return last;
3257
+ for (let i = lastAssistantIndex; i > userIndex; i--) {
3258
+ const m = messages[i];
3259
+ if (roleOf2(m) === "user") break;
3260
+ if (roleOf2(m) === "assistant" && isTerminalStopAnswer(m)) return m;
3261
+ }
3262
+ return last;
2391
3263
  }
2392
3264
  function collectSubagentSessions(messages, userMessageId) {
2393
3265
  if (!messages || messages.length === 0) return [];
2394
3266
  const byParent = messages.filter(
2395
- (message) => roleOf(message) === "assistant" && parentIdOf(message) === userMessageId
3267
+ (message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
2396
3268
  );
2397
3269
  const assistants = byParent.length > 0 ? byParent : [];
2398
3270
  if (assistants.length === 0) {
2399
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3271
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2400
3272
  if (userIndex === -1) return [];
2401
3273
  for (let i = userIndex + 1; i < messages.length; i++) {
2402
3274
  const message = messages[i];
2403
- if (roleOf(message) === "user") break;
2404
- if (roleOf(message) === "assistant") assistants.push(message);
3275
+ if (roleOf2(message) === "user") break;
3276
+ if (roleOf2(message) === "assistant") assistants.push(message);
2405
3277
  }
2406
3278
  }
2407
3279
  const refs = [];
@@ -2441,7 +3313,7 @@ function collectTaskCalls(messages, userMessageId) {
2441
3313
  if (!messages || messages.length === 0) return [];
2442
3314
  const calls = [];
2443
3315
  for (const message of messages) {
2444
- if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
3316
+ if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
2445
3317
  for (const part of message.parts ?? []) {
2446
3318
  if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
2447
3319
  continue;
@@ -2471,7 +3343,7 @@ function attributeTaskCallUsage(messages, windows) {
2471
3343
  for (const window of eligibleWindows) assignments.set(window.callID, []);
2472
3344
  const unattributed = [];
2473
3345
  for (const message of messages ?? []) {
2474
- if (roleOf(message) !== "assistant") continue;
3346
+ if (roleOf2(message) !== "assistant") continue;
2475
3347
  const created = finiteNumber(createdOf(message));
2476
3348
  const matching = created === null ? [] : eligibleWindows.filter(
2477
3349
  (window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
@@ -2493,8 +3365,9 @@ function attributeTaskCallUsage(messages, windows) {
2493
3365
  }
2494
3366
  function sumAssistantUsage(messages) {
2495
3367
  if (!messages || messages.length === 0) return null;
2496
- const nonErrored = messages.filter((message) => errorOf(message) == null);
2497
- const selected = nonErrored.length > 0 ? nonErrored : messages;
3368
+ const hasTrailingError = errorOf(messages[messages.length - 1]) != null;
3369
+ const hasTerminalStopAnswer = messages.some(isTerminalStopAnswer);
3370
+ const selected = hasTrailingError && hasTerminalStopAnswer ? messages.filter((message) => errorOf(message) == null) : messages;
2498
3371
  let sawAnyUsage = false;
2499
3372
  let inputSum = 0;
2500
3373
  let outputSum = 0;
@@ -2503,9 +3376,11 @@ function sumAssistantUsage(messages) {
2503
3376
  let cacheWriteSum = 0;
2504
3377
  let costSum = 0;
2505
3378
  let sawCost = false;
3379
+ let toolCallCount = 0;
2506
3380
  let modelId = null;
2507
3381
  let providerId = null;
2508
3382
  for (const m of selected) {
3383
+ toolCallCount += m.parts?.filter((part) => part.type === "tool").length ?? 0;
2509
3384
  const info = m.info;
2510
3385
  if (!info) continue;
2511
3386
  const tokens = info.tokens;
@@ -2542,19 +3417,18 @@ function sumAssistantUsage(messages) {
2542
3417
  usage_tokens_cache_write: cacheWriteSum,
2543
3418
  // NULL means OpenCode never reported a cost; it is distinct from a genuine
2544
3419
  // zero-cost message, which sets `sawCost` with `costSum === 0`.
2545
- usage_cost_usd: sawCost ? costSum : null
3420
+ usage_cost_usd: sawCost ? costSum : null,
3421
+ turn_tool_calls: toolCallCount
2546
3422
  };
2547
3423
  }
2548
3424
  function messageUsage(messages, userMessageId) {
2549
3425
  if (!messages || messages.length === 0) return null;
2550
3426
  const byParentAll = messages.filter(
2551
- (m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
3427
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
2552
3428
  );
2553
- const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
2554
- const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
2555
3429
  let correlated;
2556
- if (byParent.length > 0) {
2557
- correlated = byParent;
3430
+ if (byParentAll.length > 0) {
3431
+ correlated = byParentAll;
2558
3432
  } else {
2559
3433
  const reply = findAssistantReplyAfter(messages, userMessageId);
2560
3434
  correlated = reply ? [reply] : [];
@@ -2564,7 +3438,7 @@ function messageUsage(messages, userMessageId) {
2564
3438
  }
2565
3439
  function messageRunState(messages, userMessageId) {
2566
3440
  if (!messages || messages.length === 0) return "unknown";
2567
- const hasUser = messages.some((m) => idOf(m) === userMessageId);
3441
+ const hasUser = messages.some((m) => idOf2(m) === userMessageId);
2568
3442
  const reply = findLastAssistantReplyFor(messages, userMessageId);
2569
3443
  if (!hasUser) {
2570
3444
  if (!reply) return "unknown";
@@ -2600,14 +3474,38 @@ function messageError(messages, userMessageId) {
2600
3474
  const reply = findLastAssistantReplyFor(messages, userMessageId);
2601
3475
  const error2 = errorOf(reply);
2602
3476
  if (error2 == null) return null;
2603
- if (typeof error2 === "string") return error2;
2604
- if (typeof error2 === "object") {
3477
+ let rendered;
3478
+ if (typeof error2 === "string") {
3479
+ rendered = error2;
3480
+ } else if (typeof error2 === "object") {
2605
3481
  const e = error2;
2606
3482
  const dataMessage = e.data?.message;
2607
- if (typeof dataMessage === "string") return dataMessage;
2608
- if (typeof e.message === "string") return e.message;
3483
+ if (typeof dataMessage === "string") {
3484
+ rendered = dataMessage;
3485
+ } else if (typeof e.message === "string") {
3486
+ rendered = e.message;
3487
+ } else {
3488
+ rendered = "The agent run failed.";
3489
+ }
3490
+ } else {
3491
+ rendered = "The agent run failed.";
2609
3492
  }
2610
- return "The agent run failed.";
3493
+ const toolPart = Array.isArray(reply?.parts) ? reply.parts.find(
3494
+ (part) => part && typeof part === "object" && part.type === "tool" && part.state?.status === "error"
3495
+ ) : void 0;
3496
+ return typeof toolPart?.tool === "string" && toolPart.tool.length > 0 ? `${rendered} (tool "${toolPart.tool}")` : rendered;
3497
+ }
3498
+ function describeAbandonedReplyShape(messages, userMessageId) {
3499
+ const reply = findLastAssistantReplyFor(messages, userMessageId);
3500
+ if (!reply) return null;
3501
+ const parts = Array.isArray(reply.parts) ? reply.parts : [];
3502
+ const lastTool = [...parts].reverse().find((part) => part?.type === "tool");
3503
+ return {
3504
+ partTypes: parts.slice(-12).map((part) => typeof part?.type === "string" ? part.type : "(unknown)"),
3505
+ droppedPartCount: Math.max(0, parts.length - 12),
3506
+ lastToolName: typeof lastTool?.tool === "string" && lastTool.tool.length > 0 ? lastTool.tool : null,
3507
+ lastToolStatus: typeof lastTool?.state?.status === "string" && lastTool.state.status.length > 0 ? lastTool.state.status : null
3508
+ };
2611
3509
  }
2612
3510
  function isAbortedTerminalReply(messages, userMessageId) {
2613
3511
  const reply = findLastAssistantReplyFor(messages, userMessageId);
@@ -2624,19 +3522,32 @@ function isAbortedTerminalReply(messages, userMessageId) {
2624
3522
  }
2625
3523
  return false;
2626
3524
  }
3525
+ var CLAUDE_AUTH_EXPIRY_MESSAGES = Object.freeze([
3526
+ "Claude Code credentials are unavailable or expired. Run `claude` to refresh them."
3527
+ ]);
3528
+ function extractReplyErrorShape(reply) {
3529
+ const error2 = errorOf(reply);
3530
+ if (error2 == null || typeof error2 !== "object") {
3531
+ return { name: null, statusCode: null };
3532
+ }
3533
+ const e = error2;
3534
+ const name = typeof e.name === "string" ? e.name.slice(0, 100) : null;
3535
+ const data = e.data;
3536
+ const statusCode = typeof data?.statusCode === "number" && Number.isFinite(data.statusCode) ? data.statusCode : null;
3537
+ return { name, statusCode };
3538
+ }
2627
3539
  function classifyReplyAuthError(reply) {
2628
3540
  const error2 = errorOf(reply);
2629
3541
  if (error2 == null || typeof error2 !== "object") return null;
2630
3542
  const e = error2;
2631
3543
  const replyProviderId = reply?.info?.providerID ?? null;
2632
3544
  const replyModelId = reply?.info?.modelID ?? null;
3545
+ const data = e.data;
2633
3546
  if (e.name === "ProviderAuthError") {
2634
- const data = e.data;
2635
3547
  const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
2636
3548
  return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
2637
3549
  }
2638
3550
  if (e.name === "APIError") {
2639
- const data = e.data;
2640
3551
  const statusCode = data?.statusCode;
2641
3552
  if (statusCode === 401 || statusCode === 403) {
2642
3553
  return {
@@ -2647,16 +3558,28 @@ function classifyReplyAuthError(reply) {
2647
3558
  };
2648
3559
  }
2649
3560
  }
3561
+ const errorMessage3 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
3562
+ if (errorMessage3 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage3.includes(knownMessage))) {
3563
+ return {
3564
+ kind: "model_auth",
3565
+ providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
3566
+ modelId: replyModelId,
3567
+ reason: "rejected"
3568
+ };
3569
+ }
2650
3570
  return null;
2651
3571
  }
2652
3572
  function messageFailure(messages, userMessageId) {
2653
3573
  return classifyReplyAuthError(findLastAssistantReplyFor(messages, userMessageId));
2654
3574
  }
3575
+ function isProviderModelNotFoundError(reason) {
3576
+ return reason.includes("Model not found:");
3577
+ }
2655
3578
  function findLatestSubagentAuthOutcome(messages, sinceMs) {
2656
3579
  if (!messages || messages.length === 0) return null;
2657
3580
  for (let i = messages.length - 1; i >= 0; i--) {
2658
3581
  const message = messages[i];
2659
- if (roleOf(message) !== "assistant") continue;
3582
+ if (roleOf2(message) !== "assistant") continue;
2660
3583
  const created = createdOf(message);
2661
3584
  if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
2662
3585
  const failure = classifyReplyAuthError(message);
@@ -2688,25 +3611,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
2688
3611
  function hasRunningAssistantExcept(messages, exceptUserMessageId) {
2689
3612
  if (!messages || messages.length === 0) return false;
2690
3613
  return messages.some(
2691
- (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
3614
+ (m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
2692
3615
  );
2693
3616
  }
2694
3617
  function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
2695
3618
  if (!messages || messages.length === 0) return false;
2696
- const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
3619
+ const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
2697
3620
  if (userIndex === -1) return false;
2698
3621
  let hasLaterUser = false;
2699
3622
  let hasStartedLaterUser = false;
2700
3623
  for (let i = userIndex + 1; i < messages.length; i++) {
2701
3624
  const message = messages[i];
2702
- if (roleOf(message) !== "user") continue;
3625
+ if (roleOf2(message) !== "user") continue;
2703
3626
  hasLaterUser = true;
2704
- const laterUserMessageId = idOf(message);
3627
+ const laterUserMessageId = idOf2(message);
2705
3628
  if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
2706
3629
  return false;
2707
3630
  }
2708
3631
  if (messages.some(
2709
- (candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
3632
+ (candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
2710
3633
  )) {
2711
3634
  hasStartedLaterUser = true;
2712
3635
  }
@@ -3177,13 +4100,28 @@ async function listV2Sessions(client) {
3177
4100
  }
3178
4101
  async function deleteV2Session(client, sessionId) {
3179
4102
  try {
3180
- await client.request(`/api/session/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
3181
- return true;
4103
+ const response = await client.request(
4104
+ `/api/session/${encodeURIComponent(sessionId)}`,
4105
+ { method: "DELETE" },
4106
+ { allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
4107
+ );
4108
+ if (response.status >= 200 && response.status < 300) {
4109
+ return { ok: true, status: response.status };
4110
+ }
4111
+ try {
4112
+ const body = await response.text();
4113
+ return body ? { ok: false, status: response.status, body } : { ok: false, status: response.status };
4114
+ } catch (error2) {
4115
+ console.error(
4116
+ `[deleteV2Session] DELETE /api/session/${sessionId} response body read failed for HTTP ${response.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
4117
+ );
4118
+ return { ok: false, status: response.status };
4119
+ }
3182
4120
  } catch (error2) {
3183
4121
  console.error(
3184
4122
  `[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
3185
4123
  );
3186
- return false;
4124
+ return { ok: false, status: "threw" };
3187
4125
  }
3188
4126
  }
3189
4127
  async function v2SessionExists(client, sessionId) {
@@ -3354,12 +4292,12 @@ async function readV2SessionErrorStream(client, options) {
3354
4292
 
3355
4293
  // src/lib/opencode/credential-import-v2.ts
3356
4294
  import { readFileSync as readFileSync4 } from "node:fs";
3357
- import { join as join4 } from "node:path";
4295
+ import { join as join5 } from "node:path";
3358
4296
 
3359
4297
  // src/lib/openai-usage.ts
3360
4298
  import { readFileSync as readFileSync3 } from "node:fs";
3361
4299
  import { homedir as homedir3 } from "node:os";
3362
- import { join as join3 } from "node:path";
4300
+ import { join as join4 } from "node:path";
3363
4301
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3364
4302
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
3365
4303
  var OpenAiUsageError = class extends Error {
@@ -3373,7 +4311,7 @@ function isLocalCredentialProblem2(err) {
3373
4311
  }
3374
4312
  function readOpenCodeChatGptCredentials() {
3375
4313
  try {
3376
- const raw = readFileSync3(join3(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
4314
+ const raw = readFileSync3(join4(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3377
4315
  let parsed;
3378
4316
  try {
3379
4317
  parsed = JSON.parse(raw);
@@ -3599,13 +4537,13 @@ var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
3599
4537
  function isRecord3(value) {
3600
4538
  return typeof value === "object" && value !== null && !Array.isArray(value);
3601
4539
  }
3602
- var sleep2 = (milliseconds) => new Promise((resolve4) => setTimeout(resolve4, milliseconds));
4540
+ var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
3603
4541
  function emptyResult(unsupported = [], outcome = "completed") {
3604
4542
  return { outcome, imported: [], alreadyConnected: [], unsupported, failed: [] };
3605
4543
  }
3606
4544
  function readOpenCodeCredentials(homeDir, log3) {
3607
4545
  try {
3608
- const raw = readFileSync4(join4(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
4546
+ const raw = readFileSync4(join5(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
3609
4547
  let parsed;
3610
4548
  try {
3611
4549
  parsed = JSON.parse(raw);
@@ -3805,9 +4743,9 @@ async function importOpenCodeV2Credentials({
3805
4743
 
3806
4744
  // src/lib/opencode/session-db-boot.ts
3807
4745
  import { spawn as spawn2 } from "node:child_process";
3808
- import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
4746
+ import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
3809
4747
  import { homedir as homedir4 } from "node:os";
3810
- import { dirname as dirname2, resolve as resolvePath } from "node:path";
4748
+ import { dirname as dirname4, resolve as resolvePath } from "node:path";
3811
4749
 
3812
4750
  // src/lib/runner-synchroniser.ts
3813
4751
  import { spawn } from "node:child_process";
@@ -3817,7 +4755,7 @@ function appendError(stderr, error2) {
3817
4755
  ${message}`;
3818
4756
  }
3819
4757
  function runSynchroniser(args, opts) {
3820
- return new Promise((resolve4) => {
4758
+ return new Promise((resolve5) => {
3821
4759
  let child;
3822
4760
  let stdout = "";
3823
4761
  let stderr = "";
@@ -3831,7 +4769,7 @@ function runSynchroniser(args, opts) {
3831
4769
  if (timer.handle) clearTimeout(timer.handle);
3832
4770
  if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
3833
4771
  if (spawnListener) child.removeListener("spawn", spawnListener);
3834
- resolve4(result);
4772
+ resolve5(result);
3835
4773
  };
3836
4774
  try {
3837
4775
  child = spawn("runner-synchroniser", args, {
@@ -3887,9 +4825,12 @@ function runSynchroniser(args, opts) {
3887
4825
 
3888
4826
  // src/lib/opencode/session-db-boot.ts
3889
4827
  var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
3890
- var SESSION_DB_VERIFY_TIMEOUT_MS = 12e4;
4828
+ var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
3891
4829
  var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
3892
- var SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS = 120;
4830
+ var SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS = 40;
4831
+ var SESSION_DB_VERIFY_RESTORE_POINTS_TIMEOUT_SECONDS = 30;
4832
+ var SESSION_DB_VERIFY_RESTORE_CANDIDATE_TIMEOUT_SECONDS = 120;
4833
+ var SESSION_DB_VERIFY_DISPOSAL_BUDGET_SECONDS = 90;
3893
4834
  function commandError(result) {
3894
4835
  return result.stderr.trim() || `process exited with code ${result.code ?? "null"}`;
3895
4836
  }
@@ -3932,7 +4873,7 @@ function clearMarker(options) {
3932
4873
  function markNoReplicate(options, message) {
3933
4874
  if (options.noReplicateMarker) {
3934
4875
  try {
3935
- mkdirSync(dirname2(options.noReplicateMarker), { recursive: true });
4876
+ mkdirSync(dirname4(options.noReplicateMarker), { recursive: true });
3936
4877
  writeFileSync(options.noReplicateMarker, "");
3937
4878
  } catch (error2) {
3938
4879
  options.log(
@@ -3991,7 +4932,7 @@ function parseSynchroniserEnv(stdout) {
3991
4932
  return values;
3992
4933
  }
3993
4934
  function runCommand(command, args, options) {
3994
- return new Promise((resolve4) => {
4935
+ return new Promise((resolve5) => {
3995
4936
  let child;
3996
4937
  let stdout = "";
3997
4938
  let stderr = "";
@@ -4000,7 +4941,7 @@ function runCommand(command, args, options) {
4000
4941
  if (settled) return;
4001
4942
  settled = true;
4002
4943
  if (timer) clearTimeout(timer);
4003
- resolve4(result);
4944
+ resolve5(result);
4004
4945
  };
4005
4946
  try {
4006
4947
  child = spawn2(command, args, {
@@ -4008,7 +4949,7 @@ function runCommand(command, args, options) {
4008
4949
  stdio: ["ignore", "pipe", "pipe"]
4009
4950
  });
4010
4951
  } catch (error2) {
4011
- resolve4({
4952
+ resolve5({
4012
4953
  code: null,
4013
4954
  stdout,
4014
4955
  stderr: error2 instanceof Error ? error2.message : String(error2),
@@ -4057,7 +4998,7 @@ async function ensureLitestreamConfig(options, env) {
4057
4998
  return null;
4058
4999
  }
4059
5000
  try {
4060
- if (statSync2(configPath).size > 0) return configPath;
5001
+ if (statSync3(configPath).size > 0) return configPath;
4061
5002
  } catch (error2) {
4062
5003
  if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
4063
5004
  options.log(
@@ -4087,7 +5028,7 @@ async function ensureLitestreamConfig(options, env) {
4087
5028
  return null;
4088
5029
  }
4089
5030
  try {
4090
- mkdirSync(dirname2(configPath), { recursive: true });
5031
+ mkdirSync(dirname4(configPath), { recursive: true });
4091
5032
  writeFileSync(configPath, rendered.stdout);
4092
5033
  } catch (error2) {
4093
5034
  options.log(
@@ -4200,20 +5141,37 @@ async function verifySessionDb(options, configPath, env) {
4200
5141
  timeoutMs: SESSION_DB_VERIFY_TIMEOUT_MS,
4201
5142
  env: {
4202
5143
  ...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.
5144
+ // The synchroniser reads all four values in SECONDS.
4205
5145
  EVIDENT_SESSION_DB_WALKBACK_BUDGET_SECONDS: String(
4206
5146
  SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS
5147
+ ),
5148
+ EVIDENT_SESSION_DB_RESTORE_POINTS_TIMEOUT_SECONDS: String(
5149
+ SESSION_DB_VERIFY_RESTORE_POINTS_TIMEOUT_SECONDS
5150
+ ),
5151
+ EVIDENT_SESSION_DB_RESTORE_CANDIDATE_TIMEOUT_SECONDS: String(
5152
+ SESSION_DB_VERIFY_RESTORE_CANDIDATE_TIMEOUT_SECONDS
5153
+ ),
5154
+ EVIDENT_SESSION_DB_DISPOSAL_BUDGET_SECONDS: String(
5155
+ SESSION_DB_VERIFY_DISPOSAL_BUDGET_SECONDS
4207
5156
  )
4208
5157
  }
4209
5158
  });
4210
5159
  logSynchroniserDiagnostics(result, options);
4211
5160
  if (result.timedOut || result.code === 124 || result.code === 137) {
5161
+ if (result.stderr.includes("SESSION-DB-INTEGRITY-FAILED")) {
5162
+ const reason = result.stderr.includes("SESSION-DB-REPLICA-SIZE") ? "verification_timeout_during_separation" : "verification_timeout_before_separation";
5163
+ options.log(
5164
+ `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})`,
5165
+ "error"
5166
+ );
5167
+ reportRecord("verify", "session_db_boot_refused", reason, null, options);
5168
+ return { verifyFatal: true, verifyFatalReason: reason };
5169
+ }
4212
5170
  options.log(
4213
5171
  `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
5172
  "warn"
4215
5173
  );
4216
- return false;
5174
+ return { verifyFatal: false };
4217
5175
  }
4218
5176
  if (result.code === 34) {
4219
5177
  reportRecord(
@@ -4223,14 +5181,14 @@ async function verifySessionDb(options, configPath, env) {
4223
5181
  null,
4224
5182
  options
4225
5183
  );
4226
- return true;
5184
+ return { verifyFatal: true };
4227
5185
  }
4228
5186
  if (result.code === 33) {
4229
5187
  options.log(
4230
5188
  "SESSION-DB-INTEGRITY-EXHAUSTED: booting continues and litestream still replicates, starting an empty backup chain after the corrupt replica was separated.",
4231
5189
  "warn"
4232
5190
  );
4233
- return false;
5191
+ return { verifyFatal: false };
4234
5192
  }
4235
5193
  if (result.code !== 0) {
4236
5194
  options.log(
@@ -4238,17 +5196,17 @@ async function verifySessionDb(options, configPath, env) {
4238
5196
  "warn"
4239
5197
  );
4240
5198
  }
4241
- return false;
5199
+ return { verifyFatal: false };
4242
5200
  }
4243
5201
  options.log(
4244
5202
  "skipping session-DB verification: this boot's session DB was not proven safe to replicate",
4245
5203
  "debug"
4246
5204
  );
4247
- return false;
5205
+ return { verifyFatal: false };
4248
5206
  }
4249
5207
  function fileExists(path) {
4250
5208
  try {
4251
- statSync2(path);
5209
+ statSync3(path);
4252
5210
  return true;
4253
5211
  } catch (error2) {
4254
5212
  if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
@@ -4331,13 +5289,13 @@ async function restoreAndVerifySessionDb(options) {
4331
5289
  if (options.noReplicateMarker && fileExists(options.noReplicateMarker)) {
4332
5290
  return { verifyFatal: false };
4333
5291
  }
4334
- return { verifyFatal: await verifySessionDb(options, configPath, env) };
5292
+ return verifySessionDb(options, configPath, env);
4335
5293
  }
4336
5294
 
4337
5295
  // src/lib/opencode/session-db-provenance.ts
4338
5296
  import { createRequire } from "node:module";
4339
5297
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
4340
- import { dirname as dirname3, join as join5 } from "node:path";
5298
+ import { dirname as dirname5, join as join6 } from "node:path";
4341
5299
  var require2 = createRequire(import.meta.url);
4342
5300
  function readSessionDbMigrationIds(dbPath) {
4343
5301
  let db;
@@ -4374,7 +5332,7 @@ function readSessionDbMigrationIds(dbPath) {
4374
5332
  }
4375
5333
  function sessionDbProvenanceStatePath(homeDir, env) {
4376
5334
  const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
4377
- return override || join5(homeDir, ".local", "state", "evident", "session-db-provenance.json");
5335
+ return override || join6(homeDir, ".local", "state", "evident", "session-db-provenance.json");
4378
5336
  }
4379
5337
  function loadSessionDbProvenanceState(path) {
4380
5338
  let value;
@@ -4403,7 +5361,7 @@ function loadSessionDbProvenanceState(path) {
4403
5361
  }
4404
5362
  function saveSessionDbProvenanceState(path, state) {
4405
5363
  try {
4406
- mkdirSync2(dirname3(path), { recursive: true });
5364
+ mkdirSync2(dirname5(path), { recursive: true });
4407
5365
  writeFileSync2(path, `${JSON.stringify(state, null, 2)}
4408
5366
  `, "utf8");
4409
5367
  } catch (error2) {
@@ -4494,7 +5452,7 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
4494
5452
  if (child.exitCode !== null || child.signalCode !== null) {
4495
5453
  return { outcome: "exited", code: child.exitCode, signal: child.signalCode };
4496
5454
  }
4497
- return new Promise((resolve4, reject) => {
5455
+ return new Promise((resolve5, reject) => {
4498
5456
  let forced = false;
4499
5457
  let settled = false;
4500
5458
  const timer = setTimeout(() => {
@@ -4514,7 +5472,7 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
4514
5472
  settled = true;
4515
5473
  clearTimeout(timer);
4516
5474
  child.removeListener("exit", onExit);
4517
- resolve4(result);
5475
+ resolve5(result);
4518
5476
  };
4519
5477
  const fail = (error2) => {
4520
5478
  if (settled) return;
@@ -4984,10 +5942,13 @@ function selectSessionsToDelete(sessions, opts) {
4984
5942
  toDelete.push(s.id);
4985
5943
  }
4986
5944
  }
4987
- return toDelete;
5945
+ const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
5946
+ return toDelete.sort(
5947
+ (a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
5948
+ );
4988
5949
  }
4989
5950
  var DEFAULT_INTERVAL = "1h";
4990
- function resolve(flag, envValue, fallback) {
5951
+ function resolve2(flag, envValue, fallback) {
4991
5952
  return flag ?? envValue ?? fallback;
4992
5953
  }
4993
5954
  function parseMaxCount(input) {
@@ -5003,9 +5964,9 @@ function parseMaxCount(input) {
5003
5964
  }
5004
5965
  function resolveSessionCleanupConfig(flags, env = process.env) {
5005
5966
  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(
5967
+ const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
5968
+ const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
5969
+ const intervalRaw = resolve2(
5009
5970
  flags.interval,
5010
5971
  env.EVIDENT_SESSION_CLEANUP_INTERVAL,
5011
5972
  DEFAULT_INTERVAL
@@ -5044,13 +6005,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
5044
6005
  }
5045
6006
 
5046
6007
  // src/lib/opencode/session-db-size.ts
5047
- import { statSync as statSync3 } from "node:fs";
5048
- import { join as join6 } from "node:path";
6008
+ import { statSync as statSync4 } from "node:fs";
6009
+ import { join as join7 } from "node:path";
5049
6010
  var LARGE_DB_THRESHOLD_BYTES = 268435456;
5050
6011
  function statSessionDbBytes(homeDir) {
5051
- const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
6012
+ const dbPath = join7(homeDir, ".local", "share", "opencode", "opencode.db");
5052
6013
  try {
5053
- return statSync3(dbPath).size;
6014
+ return statSync4(dbPath).size;
5054
6015
  } catch (err) {
5055
6016
  const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
5056
6017
  if (!isMissingFile) {
@@ -5076,14 +6037,14 @@ function buildSessionStoreSizeWarning(input) {
5076
6037
  }
5077
6038
 
5078
6039
  // src/lib/opencode/log-tail.ts
5079
- import { statSync as statSync4 } from "node:fs";
6040
+ import { statSync as statSync5 } from "node:fs";
5080
6041
  import { homedir as homedir5 } from "node:os";
5081
- import { join as join7 } from "node:path";
6042
+ import { join as join8 } from "node:path";
5082
6043
  import { open as open2, stat } from "node:fs/promises";
5083
6044
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
5084
6045
  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");
6046
+ const dataDir = env.XDG_DATA_HOME || join8(homeDir, ".local", "share");
6047
+ return join8(dataDir, "opencode", "log", "opencode.log");
5087
6048
  }
5088
6049
  function isEnoent(error2) {
5089
6050
  return error2?.code === "ENOENT";
@@ -5099,7 +6060,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
5099
6060
  let inode = null;
5100
6061
  let baselineReady = true;
5101
6062
  try {
5102
- const initial = statSync4(logPath);
6063
+ const initial = statSync5(logPath);
5103
6064
  offset = initial.size;
5104
6065
  inode = initial.ino;
5105
6066
  } catch (error2) {
@@ -5158,129 +6119,6 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
5158
6119
  };
5159
6120
  }
5160
6121
 
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
6122
  // src/lib/tunnel/connection.ts
5285
6123
  import WebSocket2 from "ws";
5286
6124
 
@@ -5361,8 +6199,8 @@ var StreamForwarder = class {
5361
6199
  pauseStream(sid, stream) {
5362
6200
  if (!stream.paused) {
5363
6201
  stream.paused = true;
5364
- stream.pausePromise = new Promise((resolve4) => {
5365
- stream.resume = resolve4;
6202
+ stream.pausePromise = new Promise((resolve5) => {
6203
+ stream.resume = resolve5;
5366
6204
  });
5367
6205
  }
5368
6206
  if (stream.pauseTimer !== null) clearTimeout(stream.pauseTimer);
@@ -5429,12 +6267,12 @@ var StreamForwarder = class {
5429
6267
  let endBody;
5430
6268
  if (has_body) {
5431
6269
  const chunks = [];
5432
- bodyPromise = new Promise((resolve4) => {
6270
+ bodyPromise = new Promise((resolve5) => {
5433
6271
  pushBody = (buf) => {
5434
6272
  chunks.push(buf);
5435
6273
  };
5436
6274
  endBody = () => {
5437
- resolve4(Buffer.concat(chunks));
6275
+ resolve5(Buffer.concat(chunks));
5438
6276
  };
5439
6277
  });
5440
6278
  }
@@ -5605,7 +6443,7 @@ function connectTunnel(options) {
5605
6443
  } = options;
5606
6444
  const tunnelUrl = getTunnelUrlConfig();
5607
6445
  const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
5608
- return new Promise((resolve4, reject) => {
6446
+ return new Promise((resolve5, reject) => {
5609
6447
  const ws = new WebSocket2(url, {
5610
6448
  headers: {
5611
6449
  Authorization: authHeader,
@@ -5714,7 +6552,7 @@ function connectTunnel(options) {
5714
6552
  forwarder.setBinaryFramesSupported(message.binary_frames === true);
5715
6553
  const connectedAgentId = message.agent_id ?? agentId;
5716
6554
  onConnected?.(connectedAgentId);
5717
- resolve4({
6555
+ resolve5({
5718
6556
  ws,
5719
6557
  close: () => ws.close(1e3, "CLI shutdown")
5720
6558
  });
@@ -6166,7 +7004,102 @@ function createResourceUsageCollector(homeDir) {
6166
7004
  warnings
6167
7005
  };
6168
7006
  },
6169
- stop: cpuPeakSampler.stop
7007
+ stop: cpuPeakSampler.stop
7008
+ };
7009
+ }
7010
+
7011
+ // src/lib/resource-usage-low-disk.ts
7012
+ var RUNNER_DISK_ALERT_ENTRY_FREE_BYTES = 4 * 1024 ** 3;
7013
+ var RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION = 0.25;
7014
+ var RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES = 1 * 1024 ** 3;
7015
+ var RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES = 256 * 1024 ** 2;
7016
+ var RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES = 6 * 1024 ** 3;
7017
+ var RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION = 0.375;
7018
+ var LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
7019
+ var BAND_RANK = {
7020
+ low: 1,
7021
+ very_low: 2,
7022
+ critical: 3
7023
+ };
7024
+ function bandFor(freeBytes) {
7025
+ if (freeBytes < RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES) return "critical";
7026
+ if (freeBytes < RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES) return "very_low";
7027
+ return "low";
7028
+ }
7029
+ function formatBytes(bytes) {
7030
+ if (bytes === null) return "unavailable";
7031
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
7032
+ const unitIndex = Math.min(Math.floor(Math.log2(Math.max(bytes, 1)) / 10), units.length - 1);
7033
+ const value = bytes / 1024 ** unitIndex;
7034
+ const digits = unitIndex === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 2;
7035
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
7036
+ }
7037
+ function warningMessage(band, diskFreeBytes, diskTotalBytes, opencodeDbBytes) {
7038
+ 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.`;
7039
+ }
7040
+ function evaluateLowDiskWarning(input) {
7041
+ const { diskFreeBytes, diskTotalBytes, opencodeDbBytes, nowMs, state } = input;
7042
+ if (diskFreeBytes === null || diskTotalBytes === null || diskTotalBytes <= 0 || diskFreeBytes >= diskTotalBytes) {
7043
+ return { warning: null, state };
7044
+ }
7045
+ const recovered = diskFreeBytes >= RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES || diskFreeBytes >= diskTotalBytes * RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION;
7046
+ if (recovered) {
7047
+ return { warning: null, state: { worstBand: null, warnedAtMs: null } };
7048
+ }
7049
+ const low = diskFreeBytes < RUNNER_DISK_ALERT_ENTRY_FREE_BYTES && diskFreeBytes < diskTotalBytes * RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION;
7050
+ if (!low) return { warning: null, state };
7051
+ const currentBand = bandFor(diskFreeBytes);
7052
+ const currentRank = BAND_RANK[currentBand];
7053
+ const previousRank = state.worstBand === null ? 0 : BAND_RANK[state.worstBand];
7054
+ const worse = currentRank > previousRank;
7055
+ const reassert = state.warnedAtMs !== null && nowMs - state.warnedAtMs >= LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS;
7056
+ const nextState = {
7057
+ worstBand: worse ? currentBand : state.worstBand ?? currentBand,
7058
+ warnedAtMs: worse || state.warnedAtMs === null || reassert ? nowMs : state.warnedAtMs
7059
+ };
7060
+ return {
7061
+ warning: worse || state.warnedAtMs === null || reassert ? warningMessage(nextState.worstBand, diskFreeBytes, diskTotalBytes, opencodeDbBytes) : null,
7062
+ state: nextState
7063
+ };
7064
+ }
7065
+
7066
+ // src/lib/opencode/session-store-health.ts
7067
+ var SESSION_STORE_HEALTH_STALENESS_MS = 5 * 6e4;
7068
+ function createSessionStoreHealth(options = {}) {
7069
+ const now = options.now ?? (() => Date.now());
7070
+ const stalenessMs = Math.max(0, options.stalenessMs ?? SESSION_STORE_HEALTH_STALENESS_MS);
7071
+ let lastUnwritableAt = null;
7072
+ let lastWritableAt = null;
7073
+ const causes = /* @__PURE__ */ new Map();
7074
+ return {
7075
+ observeUnwritable: () => {
7076
+ lastUnwritableAt = now();
7077
+ },
7078
+ observeWritable: () => {
7079
+ lastWritableAt = now();
7080
+ },
7081
+ isUnwritable: () => {
7082
+ if (lastUnwritableAt === null) return false;
7083
+ if (lastWritableAt !== null && lastWritableAt >= lastUnwritableAt) return false;
7084
+ return now() - lastUnwritableAt < stalenessMs;
7085
+ },
7086
+ observeCause: (sessionId, cause) => {
7087
+ if (sessionId === null) return;
7088
+ causes.delete(sessionId);
7089
+ causes.set(sessionId, { cause, observedAt: now() });
7090
+ while (causes.size > 256) {
7091
+ const oldest = causes.keys().next().value;
7092
+ if (typeof oldest !== "string") break;
7093
+ causes.delete(oldest);
7094
+ }
7095
+ },
7096
+ readCause: (sessionId) => {
7097
+ const observed = causes.get(sessionId);
7098
+ if (!observed) return null;
7099
+ causes.delete(sessionId);
7100
+ if (now() - observed.observedAt >= stalenessMs) return null;
7101
+ return observed.cause;
7102
+ }
6170
7103
  };
6171
7104
  }
6172
7105
 
@@ -6174,12 +7107,12 @@ function createResourceUsageCollector(homeDir) {
6174
7107
  import { homedir as homedir6 } from "node:os";
6175
7108
 
6176
7109
  // src/lib/runner-file-sync.ts
6177
- import { join as join9 } from "node:path";
7110
+ import { join as join10 } from "node:path";
6178
7111
 
6179
7112
  // src/lib/file-push.ts
6180
7113
  import { randomUUID } from "node:crypto";
6181
7114
  import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
6182
- import { basename, dirname as dirname5, isAbsolute, join as join8, relative, resolve as resolve2, sep } from "node:path";
7115
+ 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";
6183
7116
  var FILE_MODE = 384;
6184
7117
  var DIRECTORY_MODE = 448;
6185
7118
  async function writePushedFile(request2) {
@@ -6210,9 +7143,9 @@ async function writePushedFile(request2) {
6210
7143
  }
6211
7144
  try {
6212
7145
  const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
6213
- dirname5(candidate)
7146
+ dirname6(candidate)
6214
7147
  );
6215
- const realTarget = join8(existingAncestor, ...missingSegments, basename(candidate));
7148
+ const realTarget = join9(existingAncestor, ...missingSegments, basename2(candidate));
6216
7149
  const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
6217
7150
  if (allowedDirectory === null) {
6218
7151
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
@@ -6222,8 +7155,8 @@ async function writePushedFile(request2) {
6222
7155
  }
6223
7156
  if (missingSegments.length > 0) {
6224
7157
  await createMissingDirectories(existingAncestor, missingSegments);
6225
- const realParent = await realpath(dirname5(realTarget));
6226
- if (realParent !== dirname5(realTarget) || !contains(allowedDirectory, realTarget)) {
7158
+ const realParent = await realpath(dirname6(realTarget));
7159
+ if (realParent !== dirname6(realTarget) || !contains(allowedDirectory, realTarget)) {
6227
7160
  return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
6228
7161
  path: realTarget,
6229
7162
  bytes,
@@ -6248,15 +7181,15 @@ function expandAndValidate(requestedPath, homeDir) {
6248
7181
  if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
6249
7182
  return null;
6250
7183
  }
6251
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join8(homeDir, requestedPath.slice(2)) : requestedPath;
7184
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
6252
7185
  if (expanded.split(/[/\\]/).includes("..")) {
6253
7186
  return null;
6254
7187
  }
6255
- if (!isAbsolute(expanded)) {
7188
+ if (!isAbsolute2(expanded)) {
6256
7189
  return null;
6257
7190
  }
6258
- const candidate = resolve2(expanded);
6259
- const name = basename(candidate);
7191
+ const candidate = resolve3(expanded);
7192
+ const name = basename2(candidate);
6260
7193
  return name === "" || name === "." || name === ".." ? null : candidate;
6261
7194
  }
6262
7195
  async function resolveNearestExistingAncestor(directory) {
@@ -6266,18 +7199,18 @@ async function resolveNearestExistingAncestor(directory) {
6266
7199
  try {
6267
7200
  return { existingAncestor: await realpath(current), missingSegments };
6268
7201
  } catch (err) {
6269
- const parent = dirname5(current);
7202
+ const parent = dirname6(current);
6270
7203
  if (err.code !== "ENOENT" || parent === current) {
6271
7204
  throw err;
6272
7205
  }
6273
- missingSegments.unshift(basename(current));
7206
+ missingSegments.unshift(basename2(current));
6274
7207
  current = parent;
6275
7208
  }
6276
7209
  }
6277
7210
  }
6278
7211
  async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
6279
7212
  for (const directory of allowedDirectories) {
6280
- if (!isAbsolute(directory)) {
7213
+ if (!isAbsolute2(directory)) {
6281
7214
  log("warn", "file_push_allowed_directory_skipped", { directory, reason: "not_absolute" });
6282
7215
  continue;
6283
7216
  }
@@ -6315,19 +7248,19 @@ async function realpathCreatingIfMissing(directory) {
6315
7248
  }
6316
7249
  }
6317
7250
  function contains(realDirectory, realTarget) {
6318
- const rel = relative(realDirectory, realTarget);
6319
- return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
7251
+ const rel = relative2(realDirectory, realTarget);
7252
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute2(rel);
6320
7253
  }
6321
7254
  async function createMissingDirectories(existingAncestor, missingSegments) {
6322
7255
  let current = existingAncestor;
6323
7256
  for (const segment of missingSegments) {
6324
- current = join8(current, segment);
7257
+ current = join9(current, segment);
6325
7258
  await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
6326
7259
  await chmod(current, DIRECTORY_MODE);
6327
7260
  }
6328
7261
  }
6329
7262
  async function writeAtomically(realTarget, content) {
6330
- const temporaryPath = join8(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
7263
+ const temporaryPath = join9(dirname6(realTarget), `.evident-push-${randomUUID()}.tmp`);
6331
7264
  let handle;
6332
7265
  try {
6333
7266
  handle = await open3(temporaryPath, "wx", FILE_MODE);
@@ -6457,12 +7390,12 @@ var NOT_APPLIED = {
6457
7390
  opencodeAuthApplied: false
6458
7391
  };
6459
7392
  function isClaudeCredentialPath(requestedPath, homeDir) {
6460
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
6461
- return expanded === join9(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
7393
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7394
+ return expanded === join10(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
6462
7395
  }
6463
7396
  function isOpenCodeAuthPath(requestedPath, homeDir) {
6464
- const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
6465
- return expanded === join9(homeDir, ...OPENCODE_AUTH_SEGMENTS);
7397
+ const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
7398
+ return expanded === join10(homeDir, ...OPENCODE_AUTH_SEGMENTS);
6466
7399
  }
6467
7400
  async function applyOne(options, file) {
6468
7401
  const label = `${file.id.slice(0, 8)} (${file.path})`;
@@ -6606,6 +7539,11 @@ function messageIdOf(m) {
6606
7539
  const infoId = m.info?.id;
6607
7540
  return typeof infoId === "string" ? infoId : void 0;
6608
7541
  }
7542
+ function composeAbandonmentNote(finish, replyShape, outcome) {
7543
+ 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.");
7544
+ if (replyShape === null) return base;
7545
+ 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)` : ""}.`;
7546
+ }
6609
7547
  function cleanImageMime(contentType) {
6610
7548
  if (!contentType) return null;
6611
7549
  const media = contentType.split(";")[0].trim().toLowerCase();
@@ -6626,8 +7564,13 @@ var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
6626
7564
  var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
6627
7565
  var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
6628
7566
  var MAX_BUFFERED_SESSION_ERRORS = 256;
7567
+ var SESSION_ERROR_RETENTION_TTL_MS = 6e4;
7568
+ 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.";
7569
+ var SESSION_ERROR_RETAINED_DETAIL = "Runner-observed OpenCode session error: ";
7570
+ var SESSION_ERROR_OBSERVED_CAUSE = "Runner-observed OpenCode session-store cause: ";
6629
7571
  var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
6630
7572
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
7573
+ var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
6631
7574
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
6632
7575
  var HEARTBEAT_MS = 6e4;
6633
7576
  var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
@@ -6689,6 +7632,7 @@ var ChannelDriver = class _ChannelDriver {
6689
7632
  maxActiveSessions;
6690
7633
  watcherStallMs;
6691
7634
  wedgeWarningIntervalMs;
7635
+ sessionStoreHealth;
6692
7636
  openCodeClient;
6693
7637
  /** Cache of conversationId → opencode sessionId. */
6694
7638
  sessions = /* @__PURE__ */ new Map();
@@ -6781,6 +7725,7 @@ var ChannelDriver = class _ChannelDriver {
6781
7725
  * with a short TTL so an unmatched session cannot retain an event indefinitely.
6782
7726
  */
6783
7727
  bufferedSessionErrors = /* @__PURE__ */ new Map();
7728
+ retainedSessionErrors = /* @__PURE__ */ new Map();
6784
7729
  /**
6785
7730
  * AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
6786
7731
  * dispatched and are still in-flight. A message in this set is never
@@ -6813,10 +7758,18 @@ var ChannelDriver = class _ChannelDriver {
6813
7758
  * row is `processing` or `pending`; clear it only when absent from both lists.
6814
7759
  */
6815
7760
  untrackableAck = /* @__PURE__ */ new Map();
7761
+ /** Session binding used by the first untrackable-ack report, for retries. */
7762
+ untrackableAckSessionIds = /* @__PURE__ */ new Map();
6816
7763
  /** Messages whose untrackable-ack failure signal was emitted while fenced. */
6817
7764
  untrackableAckSignalled = /* @__PURE__ */ new Set();
6818
7765
  /** Messages whose first untrackable-ack fence warning was already logged. */
6819
7766
  untrackableAckWarned = /* @__PURE__ */ new Set();
7767
+ /**
7768
+ * Accepted V1 prompts whose assigned ids were not read back; these rows are
7769
+ * re-read instead of POSTed again. A null snapshot cannot safely exclude an older
7770
+ * identical-text turn, so it never produces a correlation.
7771
+ */
7772
+ awaitingCorrelation = /* @__PURE__ */ new Map();
6820
7773
  /** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
6821
7774
  pendingMessageIds = /* @__PURE__ */ new Set();
6822
7775
  /**
@@ -6902,21 +7855,6 @@ var ChannelDriver = class _ChannelDriver {
6902
7855
  * re-signal on every one of the 15h of re-dispatch attempts #1110 made.
6903
7856
  */
6904
7857
  dispatchNotStartedSignalled = /* @__PURE__ */ new Map();
6905
- /**
6906
- * Consecutive-UNCONFIRMED-dispatch streak for a `pending` row with NO stored
6907
- * `opencode_message_id` yet — i.e. one that has never even reached the
6908
- * re-drive fence above. `sendPromptAsync`'s POST may 2xx, but its own
6909
- * read-back retries can never confirm the assigned id when the session's
6910
- * message list is PERMANENTLY unreadable (e.g. a corrupted local opencode
6911
- * SQLite DB, #1345/#1348's exact fault, just hit BEFORE the row is ever
6912
- * dispatched instead of after). Unlike an already-dispatched row, THIS row has
6913
- * no other safety net at all: the lifecycle cron only reclaims `status =
6914
- * 'processing'` rows, and a row stuck here never reaches `processing`. Keyed
6915
- * by message id, carrying `sessionId` so a session change (a fresh one bound
6916
- * after abandonment) starts a new streak rather than inheriting the old
6917
- * session's count — same shape as `redrivePollFailures` above.
6918
- */
6919
- unconfirmedDispatchFailures = /* @__PURE__ */ new Map();
6920
7858
  /**
6921
7859
  * Consecutive give-ups for a message whose OpenCode user row remains queued.
6922
7860
  * The session id scopes the count: a fresh session is a new attempt after a
@@ -6934,8 +7872,8 @@ var ChannelDriver = class _ChannelDriver {
6934
7872
  * short-circuits while it is present, so a null-id row is re-dispatched AT MOST
6935
7873
  * ONCE per outstanding read-back. Cleared on a SUCCESSFUL dispatch+read-back (the
6936
7874
  * row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
6937
- * re-entry) OR on a failed/unresolved dispatch (the message is genuinely un-sent,
6938
- * so the NEXT tick may retry exactly once more).
7875
+ * re-entry) OR on a failed/unresolved dispatch (the accepted prompt is handed to
7876
+ * the terminal-report fence and is never sent again).
6939
7877
  */
6940
7878
  awaitingReadopt = /* @__PURE__ */ new Set();
6941
7879
  /**
@@ -7058,6 +7996,7 @@ var ChannelDriver = class _ChannelDriver {
7058
7996
  this.maxActiveSessions = config.maxActiveSessions;
7059
7997
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
7060
7998
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
7999
+ this.sessionStoreHealth = config.sessionStoreHealth;
7061
8000
  }
7062
8001
  get isV2() {
7063
8002
  return this.openCodeClient.version === "v2";
@@ -7073,7 +8012,9 @@ var ChannelDriver = class _ChannelDriver {
7073
8012
  return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
7074
8013
  }
7075
8014
  async listSessions() {
7076
- return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient);
8015
+ return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient, {
8016
+ limit: DRIVER_SESSION_ENUMERATION_LIMIT
8017
+ });
7077
8018
  }
7078
8019
  async sessionExists(sessionId) {
7079
8020
  return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
@@ -7250,6 +8191,49 @@ var ChannelDriver = class _ChannelDriver {
7250
8191
  }
7251
8192
  return ids;
7252
8193
  }
8194
+ /** Find an accepted prompt's user row without adopting an older or concurrent turn. */
8195
+ findAwaitingCorrelationId(entry, sessionId, messages) {
8196
+ if (entry.sessionId !== sessionId || entry.knownUserIds === null) return null;
8197
+ const excludeIds = new Set(entry.knownUserIds);
8198
+ for (const inFlight of this.watchers.get(entry.sessionId)?.inFlight.values() ?? []) {
8199
+ excludeIds.add(inFlight.opencodeMessageId);
8200
+ }
8201
+ return findDispatchedUserMessageId(messages, entry.content, excludeIds);
8202
+ }
8203
+ /** Hand an exhausted accepted-but-untrackable prompt to the terminal-report fence. */
8204
+ async handoffUntrackableCorrelation(conversationId, messageId, sessionId, attempts, source) {
8205
+ this.awaitingCorrelation.delete(messageId);
8206
+ this.awaitingReadopt.delete(messageId);
8207
+ this.sessions.delete(conversationId);
8208
+ this.supersede(conversationId, sessionId);
8209
+ const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
8210
+ 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.`;
8211
+ this.untrackableAck.set(messageId, errorMessage3);
8212
+ this.untrackableAckSessionIds.set(messageId, null);
8213
+ this.log({
8214
+ level: "error",
8215
+ message: errorMessage3,
8216
+ conversation_id: conversationId,
8217
+ message_id: messageId
8218
+ });
8219
+ const reportError = await this.reportUntrackableAck(
8220
+ conversationId,
8221
+ messageId,
8222
+ null,
8223
+ errorMessage3
8224
+ );
8225
+ if (reportError !== null) {
8226
+ this.log({
8227
+ level: "warn",
8228
+ message: `markFailed PATCH for message ${messageId.slice(0, 8)} (conversation ${conversationId.slice(0, 8)}) failed: ${reportError.message}`,
8229
+ conversation_id: conversationId,
8230
+ message_id: messageId
8231
+ });
8232
+ }
8233
+ if (source === "readopt") {
8234
+ void this.postSignal(conversationId, messageId, "readopt_orphan_unsent");
8235
+ }
8236
+ }
7253
8237
  /**
7254
8238
  * File-pull work, for `run.ts`'s idle accounting (#559).
7255
8239
  *
@@ -7429,6 +8413,56 @@ var ChannelDriver = class _ChannelDriver {
7429
8413
  }
7430
8414
  break;
7431
8415
  }
8416
+ const correlation = this.awaitingCorrelation.get(message.id);
8417
+ if (correlation) {
8418
+ correlation.attempts += 1;
8419
+ let correlatedId = null;
8420
+ if (correlation.knownUserIds !== null && correlation.sessionId === sessionId) {
8421
+ const polled = await this.pollSessionMessagesForRedrive(
8422
+ conv,
8423
+ message,
8424
+ correlation.sessionId
8425
+ );
8426
+ if (polled.ok)
8427
+ correlatedId = this.findAwaitingCorrelationId(correlation, sessionId, polled.messages);
8428
+ } else if (correlation.knownUserIds === null) {
8429
+ this.log({
8430
+ level: "debug",
8431
+ message: `Message ${message.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
8432
+ conversation_id: conv.id,
8433
+ message_id: message.id
8434
+ });
8435
+ }
8436
+ if (correlatedId !== null) {
8437
+ this.awaitingCorrelation.delete(message.id);
8438
+ this.dispatchNotStartedSignalled.delete(message.id);
8439
+ this.releasedOpencodeIds.delete(message.id);
8440
+ this.dispatched.add(message.id);
8441
+ this.registerInFlight(conv, correlation.sessionId, message, correlatedId);
8442
+ dispatched += 1;
8443
+ this.log({
8444
+ level: "info",
8445
+ message: `Matched accepted message ${message.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
8446
+ conversation_id: conv.id,
8447
+ message_id: message.id
8448
+ });
8449
+ void this.postSignal(conv.id, message.id, "ack_correlated", {
8450
+ opencode_message_id: correlatedId
8451
+ });
8452
+ continue;
8453
+ }
8454
+ if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
8455
+ await this.handoffUntrackableCorrelation(
8456
+ conv.id,
8457
+ message.id,
8458
+ correlation.sessionId,
8459
+ correlation.attempts,
8460
+ "fresh"
8461
+ );
8462
+ break;
8463
+ }
8464
+ break;
8465
+ }
7432
8466
  const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
7433
8467
  if (effectiveOpencodeMessageId) {
7434
8468
  const outcome = await this.resolveRedrive(
@@ -7450,6 +8484,7 @@ var ChannelDriver = class _ChannelDriver {
7450
8484
  model: message.opencode_model ?? void 0
7451
8485
  };
7452
8486
  let opencodeMessageId;
8487
+ let preDispatchUserIds = null;
7453
8488
  try {
7454
8489
  this.log({
7455
8490
  level: "info",
@@ -7479,7 +8514,20 @@ var ChannelDriver = class _ChannelDriver {
7479
8514
  message.content,
7480
8515
  options,
7481
8516
  sendAttachments,
7482
- this.openCodeClient
8517
+ this.openCodeClient,
8518
+ {
8519
+ onKnownUserIds: (ids) => {
8520
+ preDispatchUserIds = ids;
8521
+ },
8522
+ onKnownUserIdsError: (error2) => {
8523
+ this.log({
8524
+ level: "warn",
8525
+ message: `Pre-dispatch snapshot callback failed for message ${message.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
8526
+ conversation_id: conv.id,
8527
+ message_id: message.id
8528
+ });
8529
+ }
8530
+ }
7483
8531
  )
7484
8532
  );
7485
8533
  } catch (err) {
@@ -7487,6 +8535,7 @@ var ChannelDriver = class _ChannelDriver {
7487
8535
  if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
7488
8536
  const errorMessage4 = err instanceof Error ? err.message : String(err);
7489
8537
  this.untrackableAck.set(message.id, errorMessage4);
8538
+ this.untrackableAckSessionIds.set(message.id, null);
7490
8539
  this.log({
7491
8540
  level: "error",
7492
8541
  message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
@@ -7562,39 +8611,21 @@ var ChannelDriver = class _ChannelDriver {
7562
8611
  if (this.isV2) {
7563
8612
  throw new Error("V2 prompt dispatch completed without an acknowledged message id");
7564
8613
  }
7565
- const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
7566
- if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
7567
- this.log({
7568
- level: "warn",
7569
- 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`,
7570
- conversation_id: conv.id,
7571
- message_id: message.id
7572
- });
7573
- this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
7574
- continue;
7575
- }
7576
- this.unconfirmedDispatchFailures.delete(message.id);
7577
- this.sessions.delete(conv.id);
7578
- this.supersede(conv.id, sessionId);
7579
- 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.`;
8614
+ this.awaitingCorrelation.set(message.id, {
8615
+ sessionId,
8616
+ content: message.content,
8617
+ attempts: 0,
8618
+ knownUserIds: preDispatchUserIds
8619
+ });
7580
8620
  this.log({
7581
- level: "error",
7582
- message: errorMessage3,
8621
+ level: "warn",
8622
+ 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`,
7583
8623
  conversation_id: conv.id,
7584
8624
  message_id: message.id
7585
8625
  });
7586
- await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
7587
- this.log({
7588
- level: "warn",
7589
- 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)}`,
7590
- conversation_id: conv.id,
7591
- message_id: message.id
7592
- });
7593
- this.signalDispatchNotStarted(conv, message, "abandon_unreported");
7594
- });
8626
+ this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
7595
8627
  break;
7596
8628
  }
7597
- this.unconfirmedDispatchFailures.delete(message.id);
7598
8629
  this.dispatchNotStartedSignalled.delete(message.id);
7599
8630
  this.releasedOpencodeIds.delete(message.id);
7600
8631
  this.dispatched.add(message.id);
@@ -8199,23 +9230,6 @@ var ChannelDriver = class _ChannelDriver {
8199
9230
  this.redrivePollFailures.set(messageId, { sessionId, signature, count: 1 });
8200
9231
  return 1;
8201
9232
  }
8202
- /**
8203
- * Record one UNCONFIRMED-dispatch outcome (a `pending` row with no stored
8204
- * `opencode_message_id` whose `sendPromptAsync` returned `null`) toward the
8205
- * bound in `processConversation`'s dispatch loop, and return the resulting
8206
- * count. Mirrors `recordRedrivePollFailure`'s session-scoping: a session
8207
- * change starts a fresh streak at `1` rather than inheriting the old one's
8208
- * count, since a new session is a genuinely different attempt.
8209
- */
8210
- recordUnconfirmedDispatch(messageId, sessionId) {
8211
- const existing = this.unconfirmedDispatchFailures.get(messageId);
8212
- if (existing && existing.sessionId === sessionId) {
8213
- existing.count += 1;
8214
- return existing.count;
8215
- }
8216
- this.unconfirmedDispatchFailures.set(messageId, { sessionId, count: 1 });
8217
- return 1;
8218
- }
8219
9233
  /** Record one queued give-up toward the never-started bound. */
8220
9234
  recordNeverStartedAttempt(messageId, sessionId) {
8221
9235
  const existing = this.neverStartedAttempts.get(messageId);
@@ -8527,8 +9541,10 @@ var ChannelDriver = class _ChannelDriver {
8527
9541
  b2LastDescendantCheckMs: 0,
8528
9542
  b2RootOngoingHeldLogged: false,
8529
9543
  b2AbandonedSignalled: false,
9544
+ b2AbandonedWithoutAnswer: false,
8530
9545
  ambiguousPinnedSinceMs: 0,
8531
9546
  ambiguousResolved: false,
9547
+ sessionStoreFailurePending: false,
8532
9548
  neverStartedResolved: false
8533
9549
  });
8534
9550
  const buffered = this.bufferedSessionErrors.get(sessionId);
@@ -8550,6 +9566,28 @@ var ChannelDriver = class _ChannelDriver {
8550
9566
  this.bufferedSessionErrors.delete(oldest);
8551
9567
  }
8552
9568
  }
9569
+ retainSessionError(event) {
9570
+ this.retainedSessionErrors.delete(event.sessionId);
9571
+ this.retainedSessionErrors.set(event.sessionId, {
9572
+ reason: event.reason,
9573
+ observedAt: this.now()
9574
+ });
9575
+ while (this.retainedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
9576
+ const oldest = this.retainedSessionErrors.keys().next().value;
9577
+ if (typeof oldest !== "string") break;
9578
+ this.retainedSessionErrors.delete(oldest);
9579
+ }
9580
+ }
9581
+ readRetainedSessionError(sessionId) {
9582
+ const retained = this.retainedSessionErrors.get(sessionId);
9583
+ if (!retained) return null;
9584
+ if (this.now() - retained.observedAt >= SESSION_ERROR_RETENTION_TTL_MS) {
9585
+ this.retainedSessionErrors.delete(sessionId);
9586
+ return null;
9587
+ }
9588
+ this.retainedSessionErrors.delete(sessionId);
9589
+ return retained.reason;
9590
+ }
8553
9591
  /**
8554
9592
  * Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
8555
9593
  * EQUAL (#1618) so a watcher whose loop has not started ticking yet is never
@@ -8645,8 +9683,10 @@ var ChannelDriver = class _ChannelDriver {
8645
9683
  b2LastDescendantCheckMs: 0,
8646
9684
  b2RootOngoingHeldLogged: false,
8647
9685
  b2AbandonedSignalled: false,
9686
+ b2AbandonedWithoutAnswer: false,
8648
9687
  ambiguousPinnedSinceMs: 0,
8649
9688
  ambiguousResolved: false,
9689
+ sessionStoreFailurePending: false,
8650
9690
  neverStartedResolved: false
8651
9691
  });
8652
9692
  }
@@ -8874,9 +9914,10 @@ var ChannelDriver = class _ChannelDriver {
8874
9914
  return;
8875
9915
  }
8876
9916
  if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
9917
+ this.retainSessionError(event);
8877
9918
  this.log({
8878
- level: "debug",
8879
- message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
9919
+ level: "warn",
9920
+ 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}`,
8880
9921
  conversation_id: watcher.conv.id
8881
9922
  });
8882
9923
  return;
@@ -8906,9 +9947,10 @@ var ChannelDriver = class _ChannelDriver {
8906
9947
  const messages = await this.getSessionMessages(event.sessionId);
8907
9948
  const state = messageRunState(messages, inFlight.opencodeMessageId);
8908
9949
  if (state !== "queued") {
9950
+ this.retainSessionError(event);
8909
9951
  this.log({
8910
- level: "debug",
8911
- message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
9952
+ level: "warn",
9953
+ 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}`,
8912
9954
  conversation_id: watcher.conv.id,
8913
9955
  message_id: inFlight.evidentMessageId
8914
9956
  });
@@ -8920,14 +9962,53 @@ var ChannelDriver = class _ChannelDriver {
8920
9962
  conversation_id: watcher.conv.id,
8921
9963
  message_id: inFlight.evidentMessageId
8922
9964
  });
8923
- await this.markFailed(
8924
- watcher.conv.id,
8925
- inFlight.evidentMessageId,
8926
- event.sessionId,
8927
- `OpenCode could not run this turn: ${event.reason}`
8928
- );
9965
+ const isMissingProvider = isProviderModelNotFoundError(event.reason);
9966
+ if (isMissingProvider) {
9967
+ const dispatchedModel = inFlight.message.opencode_model;
9968
+ const separator = dispatchedModel?.indexOf("/") ?? -1;
9969
+ const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
9970
+ await this.markFailed(
9971
+ watcher.conv.id,
9972
+ inFlight.evidentMessageId,
9973
+ event.sessionId,
9974
+ `OpenCode could not run this turn: ${event.reason}`,
9975
+ void 0,
9976
+ {
9977
+ classification: {
9978
+ kind: "model_auth",
9979
+ providerId: hasProviderAndModel ? dispatchedModel.slice(0, separator) : null,
9980
+ modelId: hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null,
9981
+ reason: "missing"
9982
+ },
9983
+ errorShape: { name: null, statusCode: null }
9984
+ }
9985
+ );
9986
+ } else {
9987
+ await this.markFailed(
9988
+ watcher.conv.id,
9989
+ inFlight.evidentMessageId,
9990
+ event.sessionId,
9991
+ `OpenCode could not run this turn: ${event.reason}`
9992
+ );
9993
+ }
8929
9994
  inFlight.done = true;
8930
9995
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9996
+ if (isMissingProvider) {
9997
+ if (this.isV2) {
9998
+ void this.postSignal(
9999
+ watcher.conv.id,
10000
+ inFlight.evidentMessageId,
10001
+ "provider_registry_reload_unsupported"
10002
+ );
10003
+ } else {
10004
+ await reloadProviderCache(this.port, this.openCodeClient);
10005
+ void this.postSignal(
10006
+ watcher.conv.id,
10007
+ inFlight.evidentMessageId,
10008
+ "provider_registry_reload_issued"
10009
+ );
10010
+ }
10011
+ }
8931
10012
  } catch (err) {
8932
10013
  if (err instanceof ChannelAuthError) {
8933
10014
  this.log({
@@ -9060,6 +10141,10 @@ var ChannelDriver = class _ChannelDriver {
9060
10141
  if (state === "running" || state === "done" || state === "failed") {
9061
10142
  this.neverStartedAttempts.delete(id);
9062
10143
  }
10144
+ if (inFlight.sessionStoreFailurePending) {
10145
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
10146
+ return;
10147
+ }
9063
10148
  if (inFlight.neverStartedResolved) {
9064
10149
  if (state === "running" || state === "done" || state === "failed") {
9065
10150
  inFlight.neverStartedResolved = false;
@@ -9133,7 +10218,16 @@ var ChannelDriver = class _ChannelDriver {
9133
10218
  if (state === "failed") {
9134
10219
  this.anchorDeliveryDeadline(inFlight);
9135
10220
  if (!inFlight.done) {
9136
- const error2 = messageError(messages, inFlight.opencodeMessageId) ?? void 0;
10221
+ const messageErrorText = messageError(messages, inFlight.opencodeMessageId);
10222
+ const retainedReason = this.readRetainedSessionError(sessionId);
10223
+ const retainedError = retainedReason === null || messageErrorText !== null && messageErrorText.includes(retainedReason) ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
10224
+ const observedCause = this.sessionStoreHealth?.readCause(sessionId);
10225
+ const causeError = observedCause === null || observedCause === void 0 ? null : `${SESSION_ERROR_OBSERVED_CAUSE}${observedCause}`;
10226
+ const storeError = this.sessionStoreHealth?.isUnwritable() ? SESSION_STORE_UNWRITABLE_ERROR : null;
10227
+ const runnerDetails = [retainedError, causeError, storeError].filter(
10228
+ (detail) => detail !== null
10229
+ );
10230
+ const error2 = messageErrorText === null ? runnerDetails.length === 0 ? void 0 : runnerDetails.join(" ") : [messageErrorText, ...runnerDetails].join(" ");
9137
10231
  this.log({
9138
10232
  level: "error",
9139
10233
  message: `Message ${inFlight.evidentMessageId.slice(0, 8)} errored \u2014 marking failed: ${error2 ?? "(no error text)"}`,
@@ -9219,10 +10313,33 @@ var ChannelDriver = class _ChannelDriver {
9219
10313
  inFlight.b2LastDescendantCheckMs = 0;
9220
10314
  inFlight.b2RootOngoingHeldLogged = false;
9221
10315
  inFlight.b2AbandonedSignalled = false;
10316
+ inFlight.b2AbandonedWithoutAnswer = false;
9222
10317
  }
9223
10318
  } else {
9224
10319
  if (inFlight.b2AbandonedSignalled) {
9225
- await this.settleMessageDone(sessionId, watcher, inFlight, messages);
10320
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
10321
+ const finish = reply?.info?.finish ?? reply?.finish;
10322
+ if (inFlight.b2AbandonedWithoutAnswer) {
10323
+ await this.settleMessageAbandonedWithoutAnswer(
10324
+ sessionId,
10325
+ watcher,
10326
+ inFlight,
10327
+ messages,
10328
+ finish
10329
+ );
10330
+ } else {
10331
+ await this.settleMessageDone(
10332
+ sessionId,
10333
+ watcher,
10334
+ inFlight,
10335
+ messages,
10336
+ composeAbandonmentNote(
10337
+ finish,
10338
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
10339
+ "with-answer"
10340
+ )
10341
+ );
10342
+ }
9226
10343
  return;
9227
10344
  }
9228
10345
  if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
@@ -9239,22 +10356,62 @@ var ChannelDriver = class _ChannelDriver {
9239
10356
  descendantOngoing,
9240
10357
  rootOngoing
9241
10358
  })) {
10359
+ if (this.sessionStoreHealth?.isUnwritable()) {
10360
+ inFlight.sessionStoreFailurePending = true;
10361
+ void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
10362
+ watched_for_ms: pinnedForMs,
10363
+ run_state: "failed",
10364
+ opencode_message_id: inFlight.opencodeMessageId
10365
+ });
10366
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
10367
+ return;
10368
+ }
9242
10369
  inFlight.b2AbandonedSignalled = true;
10370
+ const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
10371
+ const finish = reply?.info?.finish ?? reply?.finish;
10372
+ if (extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null) {
10373
+ inFlight.b2AbandonedWithoutAnswer = true;
10374
+ void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
10375
+ watched_for_ms: pinnedForMs,
10376
+ run_state: "failed",
10377
+ finish,
10378
+ ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
10379
+ ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
10380
+ opencode_message_id: inFlight.opencodeMessageId
10381
+ });
10382
+ await this.settleMessageAbandonedWithoutAnswer(
10383
+ sessionId,
10384
+ watcher,
10385
+ inFlight,
10386
+ messages,
10387
+ finish
10388
+ );
10389
+ return;
10390
+ }
9243
10391
  this.log({
9244
10392
  level: "warn",
9245
10393
  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`,
9246
10394
  conversation_id: conv.id,
9247
10395
  message_id: id
9248
10396
  });
9249
- const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
9250
10397
  void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
9251
10398
  watched_for_ms: pinnedForMs,
9252
- finish: reply?.info?.finish ?? reply?.finish,
10399
+ finish,
9253
10400
  ...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
9254
10401
  ...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
9255
10402
  opencode_message_id: inFlight.opencodeMessageId
9256
10403
  });
9257
- await this.settleMessageDone(sessionId, watcher, inFlight, messages);
10404
+ await this.settleMessageDone(
10405
+ sessionId,
10406
+ watcher,
10407
+ inFlight,
10408
+ messages,
10409
+ composeAbandonmentNote(
10410
+ finish,
10411
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
10412
+ "with-answer"
10413
+ )
10414
+ );
9258
10415
  return;
9259
10416
  }
9260
10417
  if (rootOngoing === true && !inFlight.b2RootOngoingHeldLogged) {
@@ -9297,6 +10454,16 @@ var ChannelDriver = class _ChannelDriver {
9297
10454
  maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
9298
10455
  sessionOngoing: ongoing
9299
10456
  })) {
10457
+ if (this.sessionStoreHealth?.isUnwritable()) {
10458
+ inFlight.sessionStoreFailurePending = true;
10459
+ void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
10460
+ watched_for_ms: pinnedForMs,
10461
+ run_state: "failed",
10462
+ opencode_message_id: inFlight.opencodeMessageId
10463
+ });
10464
+ await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
10465
+ return;
10466
+ }
9300
10467
  inFlight.ambiguousResolved = true;
9301
10468
  this.log({
9302
10469
  level: "warn",
@@ -9444,15 +10611,11 @@ var ChannelDriver = class _ChannelDriver {
9444
10611
  }
9445
10612
  }
9446
10613
  /**
9447
- * Settle a message whose run-state has resolved `'done'` — extracted verbatim
9448
- * (pure refactor, no behavior change) from `serviceInFlightMessage`'s former
9449
- * inline `state === 'done'` branch body, so a SECOND caller (the #721
9450
- * b2-abandonment resolution) can reach the exact same completion behavior
9451
- * (delivery-deadline anchoring, title resolution, usage extraction, and
9452
- * `markDone`'s auth/terminal/transient-retry discipline) without duplicating it
9453
- * and risking the two copies silently drifting apart.
10614
+ * Settle a message whose run-state has resolved `'done'`. Normal completion and
10615
+ * confirmed b2 abandonment share the same delivery, usage, and terminal handling.
10616
+ * Normal completions omit the optional terminal diagnostic.
9454
10617
  */
9455
- async settleMessageDone(sessionId, watcher, inFlight, messages) {
10618
+ async settleMessageDone(sessionId, watcher, inFlight, messages, note) {
9456
10619
  const conv = watcher.conv;
9457
10620
  this.anchorDeliveryDeadline(inFlight);
9458
10621
  if (!inFlight.done) {
@@ -9479,7 +10642,8 @@ var ChannelDriver = class _ChannelDriver {
9479
10642
  title,
9480
10643
  usage,
9481
10644
  usageAgentName,
9482
- subagentInvocations
10645
+ subagentInvocations,
10646
+ note
9483
10647
  );
9484
10648
  } catch (err) {
9485
10649
  if (err instanceof ChannelAuthError) throw err;
@@ -9511,24 +10675,157 @@ var ChannelDriver = class _ChannelDriver {
9511
10675
  });
9512
10676
  return;
9513
10677
  }
9514
- inFlight.done = true;
9515
- await this.reportSubagentAuthFailures(
9516
- watcher.conv.id,
9517
- inFlight.opencodeMessageId,
9518
- inFlight.evidentMessageId,
9519
- messages
9520
- );
10678
+ inFlight.done = true;
10679
+ await this.reportSubagentAuthFailures(
10680
+ watcher.conv.id,
10681
+ inFlight.opencodeMessageId,
10682
+ inFlight.evidentMessageId,
10683
+ messages
10684
+ );
10685
+ }
10686
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10687
+ }
10688
+ /** Report a confirmed b2 abandonment that produced no answer text. */
10689
+ async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, finish) {
10690
+ const conv = watcher.conv;
10691
+ const error2 = composeAbandonmentNote(
10692
+ finish,
10693
+ describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
10694
+ "without-answer"
10695
+ );
10696
+ this.anchorDeliveryDeadline(inFlight);
10697
+ this.log({
10698
+ level: "warn",
10699
+ message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was abandoned without an answer \u2014 ${error2}`,
10700
+ conversation_id: conv.id,
10701
+ message_id: inFlight.evidentMessageId
10702
+ });
10703
+ const usage = messageUsage(messages, inFlight.opencodeMessageId);
10704
+ const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
10705
+ const subagentInvocations = await this.resolveSubagentInvocations(
10706
+ messages,
10707
+ inFlight.opencodeMessageId,
10708
+ inFlight.evidentMessageId
10709
+ );
10710
+ try {
10711
+ await this.markFailed(
10712
+ conv.id,
10713
+ inFlight.evidentMessageId,
10714
+ sessionId,
10715
+ error2,
10716
+ usage,
10717
+ void 0,
10718
+ usageAgentName,
10719
+ subagentInvocations
10720
+ );
10721
+ } catch (err) {
10722
+ if (err instanceof ChannelAuthError) throw err;
10723
+ if (err instanceof ChannelTerminalError) {
10724
+ this.log({
10725
+ level: "warn",
10726
+ 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}`,
10727
+ conversation_id: conv.id,
10728
+ message_id: inFlight.evidentMessageId
10729
+ });
10730
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10731
+ return;
10732
+ }
10733
+ if (this.now() >= inFlight.deadline) {
10734
+ this.log({
10735
+ level: "warn",
10736
+ 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)}`,
10737
+ conversation_id: conv.id,
10738
+ message_id: inFlight.evidentMessageId
10739
+ });
10740
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10741
+ return;
10742
+ }
10743
+ this.log({
10744
+ level: "warn",
10745
+ 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)}`,
10746
+ conversation_id: conv.id,
10747
+ message_id: inFlight.evidentMessageId
10748
+ });
10749
+ return;
10750
+ }
10751
+ inFlight.done = true;
10752
+ await this.reportSubagentAuthFailures(
10753
+ watcher.conv.id,
10754
+ inFlight.opencodeMessageId,
10755
+ inFlight.evidentMessageId,
10756
+ messages
10757
+ );
10758
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10759
+ }
10760
+ /**
10761
+ * Report a queued message as failed after the never-started bound. The accepted
10762
+ * OpenCode copy is not cancelled, so the local fence remains on unsuccessful
10763
+ * terminal-report attempts to prevent a blind re-prompt.
10764
+ */
10765
+ async settleMessageNeverStarted(sessionId, watcher, inFlight, attempts) {
10766
+ const conv = watcher.conv;
10767
+ 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.`;
10768
+ this.anchorDeliveryDeadline(inFlight);
10769
+ try {
10770
+ await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
10771
+ } catch (err) {
10772
+ if (err instanceof ChannelAuthError) throw err;
10773
+ if (err instanceof ChannelTerminalError) {
10774
+ this.log({
10775
+ level: "warn",
10776
+ 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}`,
10777
+ conversation_id: conv.id,
10778
+ message_id: inFlight.evidentMessageId
10779
+ });
10780
+ this.recordReleasedOpencodeId(
10781
+ inFlight.evidentMessageId,
10782
+ sessionId,
10783
+ inFlight.opencodeMessageId
10784
+ );
10785
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10786
+ return;
10787
+ }
10788
+ if (this.now() >= inFlight.deadline) {
10789
+ this.log({
10790
+ level: "warn",
10791
+ 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)}`,
10792
+ conversation_id: conv.id,
10793
+ message_id: inFlight.evidentMessageId
10794
+ });
10795
+ this.recordReleasedOpencodeId(
10796
+ inFlight.evidentMessageId,
10797
+ sessionId,
10798
+ inFlight.opencodeMessageId
10799
+ );
10800
+ this.removeInFlight(watcher, inFlight.evidentMessageId);
10801
+ return;
10802
+ }
10803
+ this.log({
10804
+ level: "warn",
10805
+ 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)}`,
10806
+ conversation_id: conv.id,
10807
+ message_id: inFlight.evidentMessageId
10808
+ });
10809
+ return;
9521
10810
  }
10811
+ inFlight.done = true;
10812
+ this.neverStartedAttempts.delete(inFlight.evidentMessageId);
9522
10813
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9523
10814
  }
9524
10815
  /**
9525
- * Report a queued message as failed after the never-started bound. The accepted
9526
- * OpenCode copy is not cancelled, so the local fence remains on unsuccessful
9527
- * terminal-report attempts to prevent a blind re-prompt.
10816
+ * Report a turn as failed when the session database was observed unwritable.
10817
+ * The queue row is retained as failed, while the OpenCode copy stays fenced so
10818
+ * a later drain cannot blindly prompt the same turn again.
9528
10819
  */
9529
- async settleMessageNeverStarted(sessionId, watcher, inFlight, attempts) {
10820
+ async settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight) {
9530
10821
  const conv = watcher.conv;
9531
- 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.`;
10822
+ const error2 = SESSION_STORE_UNWRITABLE_ERROR;
10823
+ this.log({
10824
+ level: "error",
10825
+ message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was not resolved as done: ${error2}`,
10826
+ conversation_id: conv.id,
10827
+ message_id: inFlight.evidentMessageId
10828
+ });
9532
10829
  this.anchorDeliveryDeadline(inFlight);
9533
10830
  try {
9534
10831
  await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
@@ -9537,7 +10834,7 @@ var ChannelDriver = class _ChannelDriver {
9537
10834
  if (err instanceof ChannelTerminalError) {
9538
10835
  this.log({
9539
10836
  level: "warn",
9540
- 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}`,
10837
+ 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}`,
9541
10838
  conversation_id: conv.id,
9542
10839
  message_id: inFlight.evidentMessageId
9543
10840
  });
@@ -9552,7 +10849,7 @@ var ChannelDriver = class _ChannelDriver {
9552
10849
  if (this.now() >= inFlight.deadline) {
9553
10850
  this.log({
9554
10851
  level: "warn",
9555
- 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)}`,
10852
+ 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)}`,
9556
10853
  conversation_id: conv.id,
9557
10854
  message_id: inFlight.evidentMessageId
9558
10855
  });
@@ -9566,14 +10863,13 @@ var ChannelDriver = class _ChannelDriver {
9566
10863
  }
9567
10864
  this.log({
9568
10865
  level: "warn",
9569
- 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)}`,
10866
+ 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)}`,
9570
10867
  conversation_id: conv.id,
9571
10868
  message_id: inFlight.evidentMessageId
9572
10869
  });
9573
10870
  return;
9574
10871
  }
9575
10872
  inFlight.done = true;
9576
- this.neverStartedAttempts.delete(inFlight.evidentMessageId);
9577
10873
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9578
10874
  }
9579
10875
  // Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
@@ -9614,11 +10910,22 @@ var ChannelDriver = class _ChannelDriver {
9614
10910
  }
9615
10911
  }
9616
10912
  }
9617
- if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0) {
10913
+ if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0 || this.awaitingCorrelation.size > 0) {
9618
10914
  const stillProcessing = new Set(rows.map((r) => r.id));
10915
+ for (const id of this.awaitingCorrelation.keys()) {
10916
+ if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
10917
+ this.awaitingCorrelation.delete(id);
10918
+ this.log({
10919
+ level: "debug",
10920
+ message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared awaiting-correlation fence`,
10921
+ message_id: id
10922
+ });
10923
+ }
10924
+ }
9619
10925
  for (const id of [...this.untrackableAck.keys()]) {
9620
10926
  if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
9621
10927
  this.untrackableAck.delete(id);
10928
+ this.untrackableAckSessionIds.delete(id);
9622
10929
  this.untrackableAckSignalled.delete(id);
9623
10930
  this.untrackableAckWarned.delete(id);
9624
10931
  this.log({
@@ -9658,7 +10965,7 @@ var ChannelDriver = class _ChannelDriver {
9658
10965
  bySession.set(row.opencode_session_id, list);
9659
10966
  }
9660
10967
  for (const [sessionId, sessionRows] of bySession) {
9661
- let messages;
10968
+ let messages = null;
9662
10969
  try {
9663
10970
  if (this.isV2) {
9664
10971
  const snapshot = await this.getSessionMessages(sessionId);
@@ -9667,9 +10974,9 @@ var ChannelDriver = class _ChannelDriver {
9667
10974
  level: "warn",
9668
10975
  message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
9669
10976
  });
9670
- continue;
10977
+ } else {
10978
+ messages = snapshot;
9671
10979
  }
9672
- messages = snapshot;
9673
10980
  } else {
9674
10981
  const polled = await pollSessionMessagesForRedrive(
9675
10982
  this.port,
@@ -9682,15 +10989,22 @@ var ChannelDriver = class _ChannelDriver {
9682
10989
  level: "warn",
9683
10990
  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`
9684
10991
  });
9685
- continue;
10992
+ } else {
10993
+ messages = polled.messages;
9686
10994
  }
9687
- messages = polled.messages;
9688
10995
  }
9689
10996
  } catch (err) {
9690
10997
  this.log({
9691
10998
  level: "warn",
9692
10999
  message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
9693
11000
  });
11001
+ }
11002
+ if (messages === null) {
11003
+ for (const row of sessionRows) {
11004
+ if (this.awaitingCorrelation.has(row.id)) {
11005
+ await this.advanceReadoptCorrelation(sessionId, row, null);
11006
+ }
11007
+ }
9694
11008
  continue;
9695
11009
  }
9696
11010
  const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
@@ -9700,6 +11014,59 @@ var ChannelDriver = class _ChannelDriver {
9700
11014
  }
9701
11015
  }
9702
11016
  }
11017
+ /** Advance a re-adopted accepted prompt through the shared correlation fence. */
11018
+ async advanceReadoptCorrelation(sessionId, row, messages) {
11019
+ const correlation = this.awaitingCorrelation.get(row.id);
11020
+ if (!correlation) return false;
11021
+ correlation.attempts += 1;
11022
+ const correlatedId = messages === null ? null : this.findAwaitingCorrelationId(correlation, sessionId, messages);
11023
+ if (correlatedId !== null) {
11024
+ this.awaitingCorrelation.delete(row.id);
11025
+ const conv = this.convForRow(sessionId, row);
11026
+ const message = this.queuedMessageForRow(row);
11027
+ const processedAtMs = this.processedAtMs(row);
11028
+ this.registerReadopted(conv, sessionId, message, correlatedId, processedAtMs, processedAtMs);
11029
+ this.dispatched.add(row.id);
11030
+ this.readopted.add(row.id);
11031
+ this.ensureWatcherRunning(sessionId);
11032
+ this.log({
11033
+ level: "info",
11034
+ message: `Re-adopt: matched accepted message ${row.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
11035
+ conversation_id: row.conversation_id,
11036
+ message_id: row.id
11037
+ });
11038
+ void this.postSignal(row.conversation_id, row.id, "readopt_redispatched");
11039
+ void this.postSignal(row.conversation_id, row.id, "ack_correlated", {
11040
+ opencode_message_id: correlatedId
11041
+ });
11042
+ return true;
11043
+ }
11044
+ if (correlation.knownUserIds === null) {
11045
+ this.log({
11046
+ level: "debug",
11047
+ message: `Re-adopt: message ${row.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
11048
+ conversation_id: row.conversation_id,
11049
+ message_id: row.id
11050
+ });
11051
+ } else if (correlation.sessionId !== sessionId) {
11052
+ this.log({
11053
+ level: "warn",
11054
+ 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`,
11055
+ conversation_id: row.conversation_id,
11056
+ message_id: row.id
11057
+ });
11058
+ }
11059
+ if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
11060
+ await this.handoffUntrackableCorrelation(
11061
+ row.conversation_id,
11062
+ row.id,
11063
+ correlation.sessionId,
11064
+ correlation.attempts,
11065
+ "readopt"
11066
+ );
11067
+ }
11068
+ return true;
11069
+ }
9703
11070
  /**
9704
11071
  * Re-adopt ONE `processing` row against the tick's session message snapshot
9705
11072
  * (ADR-0046 Decision §1/§2). Idempotent: skips a row already being driven.
@@ -9707,8 +11074,10 @@ var ChannelDriver = class _ChannelDriver {
9707
11074
  * Branches on `messageRunState(messages, row.opencode_message_id)` — the
9708
11075
  * opencode-assigned user-message id persisted on the first `processing` PATCH
9709
11076
  * (#218). A row with a NULL stored id (dispatched but the read-back never landed
9710
- * before the restart) has no id to correlate → treated as an orphan and
9711
- * re-dispatched (at most once, see `forceReadoptRun`):
11077
+ * before the restart) is first offered to an awaiting-correlation fence recorded
11078
+ * by this process; without that fence, it has no id to correlate after restart
11079
+ * and is treated as an orphan and re-dispatched (at most once, see
11080
+ * `forceReadoptRun`):
9712
11081
  * - `done` → `markDone` now (guarded like the watcher's done branch);
9713
11082
  * - `failed` → `markFailed` with the surfaced error (issue #182), so an
9714
11083
  * errored turn is reported failed on restart, NOT re-dispatched —
@@ -9717,7 +11086,8 @@ var ChannelDriver = class _ChannelDriver {
9717
11086
  * re-dispatched instead (issue #1310, see the branch below);
9718
11087
  * - `running`/`queued` → re-attach a watcher via `registerReadopted` (no re-dispatch),
9719
11088
  * tracking the stored id so the reply correlates by it;
9720
- * - `unknown`/null id → re-dispatch (opencode assigns a fresh id) + attach a watcher.
11089
+ * - `unknown`/null id → correlate through an awaiting fence when present;
11090
+ * otherwise re-dispatch (opencode assigns a fresh id) + attach a watcher.
9721
11091
  *
9722
11092
  * Only `ChannelAuthError` propagates.
9723
11093
  */
@@ -9731,6 +11101,7 @@ var ChannelDriver = class _ChannelDriver {
9731
11101
  });
9732
11102
  return;
9733
11103
  }
11104
+ if (await this.advanceReadoptCorrelation(sessionId, row, messages)) return;
9734
11105
  const ocId = row.opencode_message_id;
9735
11106
  const state = messageRunState(messages, ocId ?? "");
9736
11107
  if (state === "done") {
@@ -9797,12 +11168,48 @@ var ChannelDriver = class _ChannelDriver {
9797
11168
  await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
9798
11169
  this.dontRedispatch.delete(row.id);
9799
11170
  this.untrackableAck.delete(row.id);
11171
+ this.untrackableAckSessionIds.delete(row.id);
9800
11172
  this.untrackableAckSignalled.delete(row.id);
9801
11173
  this.untrackableAckWarned.delete(row.id);
9802
11174
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
9803
11175
  return;
9804
11176
  }
9805
- if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
11177
+ if (this.untrackableAck.has(row.id)) {
11178
+ if (this.untrackableAckSignalled.has(row.id)) {
11179
+ const firstFenceWarning = !this.untrackableAckWarned.has(row.id);
11180
+ this.untrackableAckWarned.add(row.id);
11181
+ this.log({
11182
+ level: firstFenceWarning ? "warn" : "debug",
11183
+ 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`,
11184
+ conversation_id: row.conversation_id,
11185
+ message_id: row.id
11186
+ });
11187
+ const errorMessage3 = this.untrackableAck.get(row.id);
11188
+ const reportError = await this.reportUntrackableAck(
11189
+ row.conversation_id,
11190
+ row.id,
11191
+ this.untrackableAckSessionIds.get(row.id) ?? null,
11192
+ errorMessage3
11193
+ );
11194
+ if (reportError !== null) {
11195
+ this.log({
11196
+ level: "warn",
11197
+ message: `Re-adopt: failed to report untrackable message ${row.id.slice(0, 8)} (will retry next drain): ${reportError.message}`,
11198
+ conversation_id: row.conversation_id,
11199
+ message_id: row.id
11200
+ });
11201
+ }
11202
+ } else {
11203
+ this.log({
11204
+ level: "debug",
11205
+ message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
11206
+ conversation_id: row.conversation_id,
11207
+ message_id: row.id
11208
+ });
11209
+ }
11210
+ return;
11211
+ }
11212
+ if (this.dontRedispatch.has(row.id)) {
9806
11213
  this.log({
9807
11214
  level: "debug",
9808
11215
  message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
@@ -9980,6 +11387,7 @@ var ChannelDriver = class _ChannelDriver {
9980
11387
  }
9981
11388
  this.dontRedispatch.delete(row.id);
9982
11389
  this.untrackableAck.delete(row.id);
11390
+ this.untrackableAckSessionIds.delete(row.id);
9983
11391
  this.untrackableAckSignalled.delete(row.id);
9984
11392
  this.untrackableAckWarned.delete(row.id);
9985
11393
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
@@ -9999,8 +11407,9 @@ var ChannelDriver = class _ChannelDriver {
9999
11407
  * latch makes a null-id row re-dispatched AT MOST ONCE per outstanding read-back:
10000
11408
  * short-circuit while the row is latched; clear it on a successful dispatch (the
10001
11409
  * row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
10002
- * re-entry) OR on a failed/unresolved dispatch (genuinely un-sent → the next tick
10003
- * may retry exactly once more).
11410
+ * re-entry) or when the dispatch itself failed before acceptance. An accepted V1
11411
+ * dispatch whose id is not readable enters the bounded correlation fence instead
11412
+ * of retrying; exhaustion hands it to `untrackableAck`.
10004
11413
  *
10005
11414
  * `evidentMessageId = row.id` addresses the SERVER row. Deadline anchored to
10006
11415
  * `processed_at` (Invariant 1).
@@ -10063,6 +11472,7 @@ var ChannelDriver = class _ChannelDriver {
10063
11472
  );
10064
11473
  }
10065
11474
  let ocId;
11475
+ let preDispatchUserIds = null;
10066
11476
  try {
10067
11477
  ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
10068
11478
  sessionId,
@@ -10072,7 +11482,20 @@ var ChannelDriver = class _ChannelDriver {
10072
11482
  row.content,
10073
11483
  options,
10074
11484
  sendAttachments,
10075
- this.openCodeClient
11485
+ this.openCodeClient,
11486
+ {
11487
+ onKnownUserIds: (ids) => {
11488
+ preDispatchUserIds = ids;
11489
+ },
11490
+ onKnownUserIdsError: (error2) => {
11491
+ this.log({
11492
+ level: "warn",
11493
+ message: `Pre-dispatch snapshot callback failed for message ${row.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
11494
+ conversation_id: row.conversation_id,
11495
+ message_id: row.id
11496
+ });
11497
+ }
11498
+ }
10076
11499
  )
10077
11500
  );
10078
11501
  } catch (err) {
@@ -10105,6 +11528,7 @@ var ChannelDriver = class _ChannelDriver {
10105
11528
  }
10106
11529
  } else {
10107
11530
  this.untrackableAck.set(row.id, errorMessage3);
11531
+ this.untrackableAckSessionIds.set(row.id, sessionId);
10108
11532
  }
10109
11533
  this.log({
10110
11534
  level: "error",
@@ -10153,39 +11577,20 @@ var ChannelDriver = class _ChannelDriver {
10153
11577
  }
10154
11578
  if (ocId === null) {
10155
11579
  this.awaitingReadopt.delete(row.id);
10156
- const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
10157
- if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
10158
- this.unconfirmedDispatchFailures.delete(row.id);
10159
- this.sessions.delete(readoptConv.id);
10160
- this.supersede(readoptConv.id, sessionId);
10161
- 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.`;
10162
- this.log({
10163
- level: "error",
10164
- message: errorMessage3,
10165
- conversation_id: row.conversation_id,
10166
- message_id: row.id
10167
- });
10168
- await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
10169
- this.log({
10170
- level: "warn",
10171
- 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)}`,
10172
- conversation_id: row.conversation_id,
10173
- message_id: row.id
10174
- });
10175
- });
10176
- void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
10177
- return;
10178
- }
11580
+ this.awaitingCorrelation.set(row.id, {
11581
+ sessionId,
11582
+ content: row.content,
11583
+ attempts: 0,
11584
+ knownUserIds: preDispatchUserIds
11585
+ });
10179
11586
  this.log({
10180
11587
  level: "warn",
10181
- 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`,
11588
+ 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`,
10182
11589
  conversation_id: row.conversation_id,
10183
11590
  message_id: row.id
10184
11591
  });
10185
- void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
10186
11592
  return;
10187
11593
  }
10188
- this.unconfirmedDispatchFailures.delete(row.id);
10189
11594
  const processedAtMs = this.processedAtMs(row);
10190
11595
  this.registerReadopted(
10191
11596
  readoptConv,
@@ -10524,14 +11929,14 @@ var ChannelDriver = class _ChannelDriver {
10524
11929
  const fetchChildWithoutBlocking = async (sessionId) => {
10525
11930
  const pending = fetchChildMessages(sessionId);
10526
11931
  let timer;
10527
- const timeout = new Promise((resolve4) => {
11932
+ const timeout = new Promise((resolve5) => {
10528
11933
  timer = setTimeout(() => {
10529
11934
  this.log({
10530
11935
  level: "warn",
10531
11936
  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`,
10532
11937
  message_id: messageId
10533
11938
  });
10534
- resolve4(null);
11939
+ resolve5(null);
10535
11940
  }, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
10536
11941
  });
10537
11942
  try {
@@ -11091,7 +12496,7 @@ var ChannelDriver = class _ChannelDriver {
11091
12496
  * watcher retries next tick within the
11092
12497
  * deadline, Finding 4).
11093
12498
  */
11094
- async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
12499
+ async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations, error2) {
11095
12500
  const res = await this.fetchImpl(
11096
12501
  `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
11097
12502
  {
@@ -11109,7 +12514,8 @@ var ChannelDriver = class _ChannelDriver {
11109
12514
  ...title ? { title } : {},
11110
12515
  ...usage ? usage : {},
11111
12516
  ...usageAgentName ? { usage_agent_name: usageAgentName } : {},
11112
- ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
12517
+ ...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {},
12518
+ ...error2 ? { error: error2 } : {}
11113
12519
  })
11114
12520
  }
11115
12521
  );
@@ -11169,11 +12575,17 @@ var ChannelDriver = class _ChannelDriver {
11169
12575
  if (subagentInvocations && subagentInvocations.length > 0) {
11170
12576
  body.subagent_invocations = subagentInvocations;
11171
12577
  }
11172
- if (failure) {
11173
- body.failure_kind = failure.kind;
11174
- body.failure_provider_id = failure.providerId;
11175
- body.failure_model_id = failure.modelId;
11176
- body.failure_reason = failure.reason;
12578
+ if (failure && failure.errorShape.name !== null) {
12579
+ body.failure_error_name = failure.errorShape.name;
12580
+ }
12581
+ if (failure && failure.errorShape.statusCode !== null) {
12582
+ body.failure_status_code = failure.errorShape.statusCode;
12583
+ }
12584
+ if (failure?.classification) {
12585
+ body.failure_kind = failure.classification.kind;
12586
+ body.failure_provider_id = failure.classification.providerId;
12587
+ body.failure_model_id = failure.classification.modelId;
12588
+ body.failure_reason = failure.classification.reason;
11177
12589
  }
11178
12590
  try {
11179
12591
  await this.callWithRetry(
@@ -11194,27 +12606,28 @@ var ChannelDriver = class _ChannelDriver {
11194
12606
  this.clearSubagentInvocationCaches(messageId);
11195
12607
  }
11196
12608
  /**
11197
- * Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
12609
+ * Classify an errored turn and extract its bounded error shape for `markFailed`.
11198
12610
  *
11199
12611
  * `messageFailure` alone (structured OpenCode error → `model_auth`) covers
11200
12612
  * most cases; when it returns `null` on this ALREADY-FAILED turn, fall back
11201
12613
  * to the P1-2b zero-provider check — one extra loopback call to
11202
12614
  * `hasAnyConfiguredProvider`, only reached when the structured classifier
11203
12615
  * couldn't place it. Fails open (never throws): a fallback probe failure
11204
- * (`null`/indeterminate) leaves the classification `null`, which produces
11205
- * today's byte-identical PATCH body via `markFailed`'s `if (failure)` guard.
12616
+ * (`null`/indeterminate) leaves the classification `null`; the raw error shape
12617
+ * is still retained for diagnostics.
11206
12618
  */
11207
12619
  async classifyModelAuthFailure(messages, userMessageId) {
11208
- const classified = messageFailure(messages, userMessageId);
11209
- if (classified != null) return classified;
11210
12620
  const reply = findLastAssistantReplyFor(messages, userMessageId);
11211
- const hasProvider = await this.hasAnyConfiguredProvider();
11212
- return applyZeroProviderFallback(
12621
+ const errorShape = extractReplyErrorShape(reply);
12622
+ const classified = messageFailure(messages, userMessageId);
12623
+ if (classified != null) return { classification: classified, errorShape };
12624
+ const classification = applyZeroProviderFallback(
11213
12625
  classified,
11214
- hasProvider,
12626
+ await this.hasAnyConfiguredProvider(),
11215
12627
  reply?.info?.providerID ?? null,
11216
12628
  reply?.info?.modelID ?? null
11217
12629
  );
12630
+ return { classification, errorShape };
11218
12631
  }
11219
12632
  async recordSubagentModelAuthFailure(failure, conversationId, messageId) {
11220
12633
  const providerId = failure.providerId ?? "(unknown)";
@@ -11772,7 +13185,7 @@ function commandError2(result) {
11772
13185
  return result.stderr.trim() || `process exited with code ${result.code ?? "null"}`;
11773
13186
  }
11774
13187
  var runCommand2 = (command, args, opts) => {
11775
- return new Promise((resolve4) => {
13188
+ return new Promise((resolve5) => {
11776
13189
  let child;
11777
13190
  let stdout = "";
11778
13191
  let stderr = "";
@@ -11782,7 +13195,7 @@ var runCommand2 = (command, args, opts) => {
11782
13195
  if (settled) return;
11783
13196
  settled = true;
11784
13197
  if (timer.handle) clearTimeout(timer.handle);
11785
- resolve4(result);
13198
+ resolve5(result);
11786
13199
  };
11787
13200
  try {
11788
13201
  child = spawn5(command, args, { env: opts.env, stdio: ["ignore", "pipe", "pipe"] });
@@ -11837,7 +13250,9 @@ async function installRunnerSecret({
11837
13250
  }) {
11838
13251
  const arn = env.RUNNER_SECRET_ARN?.trim();
11839
13252
  if (!arn) {
11840
- log3("runner secret is not configured; continuing without GitHub and MCP credentials");
13253
+ if (!env.GH_TOKEN) {
13254
+ log3("runner secret is not configured; continuing without GitHub and MCP credentials");
13255
+ }
11841
13256
  return false;
11842
13257
  }
11843
13258
  const result = await (commandRunner ?? runCommand2)(
@@ -12036,7 +13451,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
12036
13451
  // src/lib/opencode/config-overlay.ts
12037
13452
  import { execFileSync as execFileSync2 } from "node:child_process";
12038
13453
  import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
12039
- import { isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
13454
+ import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
12040
13455
  function isFile(filePath) {
12041
13456
  return existsSync2(filePath) && statSync6(filePath).isFile();
12042
13457
  }
@@ -12049,8 +13464,8 @@ function applyRunnerOpenCodeConfig({
12049
13464
  log3("runner OpenCode config is not configured; using the baked project config", "debug");
12050
13465
  return;
12051
13466
  }
12052
- const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
12053
- const target = isFile(join10(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
13467
+ const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
13468
+ const target = isFile(join11(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
12054
13469
  if (!isFile(source)) {
12055
13470
  log3(
12056
13471
  `RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
@@ -12058,7 +13473,7 @@ function applyRunnerOpenCodeConfig({
12058
13473
  );
12059
13474
  return;
12060
13475
  }
12061
- copyFileSync(source, join10(cwd, target));
13476
+ copyFileSync(source, join11(cwd, target));
12062
13477
  try {
12063
13478
  execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
12064
13479
  stdio: "ignore"
@@ -12067,7 +13482,7 @@ function applyRunnerOpenCodeConfig({
12067
13482
  const detail = error2 instanceof Error ? error2.message : String(error2);
12068
13483
  log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
12069
13484
  }
12070
- log3(`Applied runner OpenCode config ${source} to ${join10(cwd, target)}`);
13485
+ log3(`Applied runner OpenCode config ${source} to ${join11(cwd, target)}`);
12071
13486
  }
12072
13487
 
12073
13488
  // src/lib/credential-sync.ts
@@ -12085,14 +13500,14 @@ function errorMessage2(error2) {
12085
13500
  return error2 instanceof Error ? error2.message : String(error2);
12086
13501
  }
12087
13502
  function waitForSettlement(promise, timeoutMs) {
12088
- return new Promise((resolve4) => {
13503
+ return new Promise((resolve5) => {
12089
13504
  let settled = false;
12090
13505
  const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
12091
13506
  const finish = (value) => {
12092
13507
  if (settled) return;
12093
13508
  settled = true;
12094
13509
  clearTimeout(timer);
12095
- resolve4(value);
13510
+ resolve5(value);
12096
13511
  };
12097
13512
  promise.then(
12098
13513
  () => finish(true),
@@ -12343,8 +13758,8 @@ function resolveFileSyncDirectories(raw, homeDir) {
12343
13758
  if (trimmed === "") {
12344
13759
  throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
12345
13760
  }
12346
- const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join11(homeDir, trimmed.slice(2)) : trimmed;
12347
- if (!isAbsolute3(expanded)) {
13761
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join12(homeDir, trimmed.slice(2)) : trimmed;
13762
+ if (!isAbsolute4(expanded)) {
12348
13763
  throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
12349
13764
  }
12350
13765
  const normalized = resolvePath2(expanded);
@@ -12444,8 +13859,7 @@ function resolveMaxActiveSessions(options, env = process.env) {
12444
13859
  function meetsThreshold(state, level) {
12445
13860
  return LOG_LEVELS[level] >= LOG_LEVELS[state.logLevel];
12446
13861
  }
12447
- function log2(state, message, level = "info") {
12448
- if (!meetsThreshold(state, level)) return;
13862
+ function writeConsoleLog(state, message, level) {
12449
13863
  if (state.json) {
12450
13864
  console.log(
12451
13865
  JSON.stringify({
@@ -12454,10 +13868,18 @@ function log2(state, message, level = "info") {
12454
13868
  message
12455
13869
  })
12456
13870
  );
12457
- } else if (!state.interactive) {
12458
- const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
12459
- console.log(`${prefix} ${message}`);
13871
+ return;
12460
13872
  }
13873
+ const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
13874
+ console.log(`${prefix} ${message}`);
13875
+ }
13876
+ function log2(state, message, level = "info") {
13877
+ if (!meetsThreshold(state, level)) return;
13878
+ if (state.json || !state.interactive) writeConsoleLog(state, message, level);
13879
+ }
13880
+ function logLocalOnly(state, message, level = "info") {
13881
+ if (!meetsThreshold(state, level)) return;
13882
+ writeConsoleLog(state, message, level);
12461
13883
  }
12462
13884
  function logActivity(state, entry) {
12463
13885
  const level = entry.level ?? (entry.type === "error" ? "error" : "info");
@@ -12658,6 +14080,8 @@ async function driveChannels(state, driver) {
12658
14080
  let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
12659
14081
  let lastSeenClaudeApplies = driver.fileSyncActivity().claudeCredentialApplies;
12660
14082
  let lastSeenOpencodeAuthApplies = driver.fileSyncActivity().opencodeAuthApplyCount;
14083
+ const stayAwakeHolds = () => state.stayAwakeUntilMs !== null && Date.now() < state.stayAwakeUntilMs;
14084
+ let stayAwakeSuppressionLogged = false;
12661
14085
  while (state.running) {
12662
14086
  const cycleStartedAtMs = performance.now();
12663
14087
  let idleThisCycle = false;
@@ -12759,7 +14183,7 @@ async function driveChannels(state, driver) {
12759
14183
  }
12760
14184
  }
12761
14185
  }
12762
- await new Promise((resolve4) => setTimeout(resolve4, CHANNEL_POLL_INTERVAL_MS));
14186
+ await new Promise((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
12763
14187
  const cycleMs = performance.now() - cycleStartedAtMs;
12764
14188
  if (idleThisCycle) idleMs += cycleMs;
12765
14189
  if (unreachableThisCycle) unreachableMs += cycleMs;
@@ -12772,17 +14196,29 @@ async function driveChannels(state, driver) {
12772
14196
  if (state.interactive) displayStatus(state);
12773
14197
  break;
12774
14198
  }
12775
- if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3) {
14199
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && !stayAwakeHolds()) {
12776
14200
  logActivity(state, { type: "info", message: "Idle timeout reached" });
12777
14201
  if (state.interactive) displayStatus(state);
12778
14202
  break;
12779
14203
  }
14204
+ if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3 && stayAwakeHolds()) {
14205
+ if (!stayAwakeSuppressionLogged) {
14206
+ stayAwakeSuppressionLogged = true;
14207
+ logActivity(state, {
14208
+ type: "info",
14209
+ message: "Idle, but staying awake until the requested minimum passes"
14210
+ });
14211
+ }
14212
+ }
12780
14213
  }
12781
14214
  }
12782
14215
  var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
12783
- var SESSION_DB_RECLAIM_MAX_PAGES = 2e4;
14216
+ var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
14217
+ var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
14218
+ var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
14219
+ var MAX_SESSION_DELETE_BODY_LENGTH = 200;
12784
14220
  function shouldWarnForReclaimSkip(reason) {
12785
- if (reason !== "sqlite-unavailable") return false;
14221
+ if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
12786
14222
  const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
12787
14223
  if (!version2) return false;
12788
14224
  const major = Number(version2[1]);
@@ -12791,7 +14227,7 @@ function shouldWarnForReclaimSkip(reason) {
12791
14227
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
12792
14228
  }
12793
14229
  function sessionDbPath() {
12794
- return join11(homedir7(), ".local", "share", "opencode", "opencode.db");
14230
+ return join12(homedir7(), ".local", "share", "opencode", "opencode.db");
12795
14231
  }
12796
14232
  function logSessionCleanupActivity(state, phase, entry) {
12797
14233
  logActivity(state, {
@@ -12839,61 +14275,146 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
12839
14275
  async function runSweep(state, driver, config) {
12840
14276
  const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
12841
14277
  try {
12842
- const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
12843
- if (sessions === null) {
12844
- logSessionCleanupActivity(state, "sweep", {
14278
+ const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0);
14279
+ const sessions = enumeration.sessions;
14280
+ const enumerationReason = sessions === null ? enumeration.reason : null;
14281
+ if (enumerationReason !== null) {
14282
+ logActivity(state, {
12845
14283
  type: "info",
12846
- message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`,
12847
- metadata: {
12848
- outcome: "sweep-skipped",
12849
- reason: "opencode-unreachable"
12850
- }
14284
+ level: "warn",
14285
+ message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
12851
14286
  });
12852
- return;
12853
14287
  }
12854
- const toDelete = selectSessionsToDelete(
12855
- sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
12856
- {
12857
- maxAgeMs: config.maxAgeMs,
12858
- maxCount: config.maxCount,
12859
- nowMs: Date.now(),
12860
- protectedIds: driver.protectedSessionIds()
12861
- }
12862
- );
12863
- const protectedNow = driver.protectedSessionIds();
12864
14288
  let deleted = 0;
12865
14289
  let failed = 0;
14290
+ let failed4xx = 0;
14291
+ let failed5xx = 0;
14292
+ let failedOtherStatus = 0;
14293
+ let failedThrew = 0;
12866
14294
  let skippedNewlyActive = 0;
12867
- for (const id of toDelete) {
12868
- if (protectedNow.has(id)) {
12869
- skippedNewlyActive++;
12870
- logActivity(state, {
14295
+ let remaining = 0;
14296
+ let protectedNow = driver.protectedSessionIds();
14297
+ if (sessions !== null) {
14298
+ const toDelete = selectSessionsToDelete(
14299
+ sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
14300
+ {
14301
+ maxAgeMs: config.maxAgeMs,
14302
+ maxCount: config.maxCount,
14303
+ nowMs: Date.now(),
14304
+ protectedIds: protectedNow
14305
+ }
14306
+ );
14307
+ const eligibleToDelete = toDelete.filter(
14308
+ (id) => (state.sessionDeleteFailures.get(id)?.isolatedFailures ?? 0) < SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES
14309
+ );
14310
+ const deleteCandidates = eligibleToDelete.slice(0, SESSION_CLEANUP_MAX_DELETES_PER_SWEEP);
14311
+ remaining = eligibleToDelete.length - deleteCandidates.length;
14312
+ const answeredFailures = [];
14313
+ protectedNow = driver.protectedSessionIds();
14314
+ for (const id of deleteCandidates) {
14315
+ if (protectedNow.has(id)) {
14316
+ skippedNewlyActive++;
14317
+ logActivity(state, {
14318
+ type: "info",
14319
+ message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
14320
+ });
14321
+ continue;
14322
+ }
14323
+ const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
14324
+ if (outcome.ok) {
14325
+ deleted++;
14326
+ } else if (outcome.status === "threw") {
14327
+ failed++;
14328
+ failedThrew++;
14329
+ } else {
14330
+ failed++;
14331
+ const statusClass = Math.floor(outcome.status / 100);
14332
+ if (statusClass === 4) failed4xx++;
14333
+ else if (statusClass === 5) failed5xx++;
14334
+ else failedOtherStatus++;
14335
+ answeredFailures.push({
14336
+ id,
14337
+ status: outcome.status,
14338
+ ...outcome.body !== void 0 ? { body: outcome.body } : {}
14339
+ });
14340
+ }
14341
+ }
14342
+ const givenUpFailures = [];
14343
+ if (failed > SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES) {
14344
+ for (const { id } of answeredFailures) state.sessionDeleteFailures.delete(id);
14345
+ } else {
14346
+ for (const failure of answeredFailures) {
14347
+ const previous = state.sessionDeleteFailures.get(failure.id);
14348
+ const record = previous?.status === failure.status ? { status: failure.status, isolatedFailures: previous.isolatedFailures + 1 } : { status: failure.status, isolatedFailures: 1 };
14349
+ state.sessionDeleteFailures.set(failure.id, record);
14350
+ if (record.isolatedFailures >= SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES) {
14351
+ givenUpFailures.push(failure);
14352
+ }
14353
+ }
14354
+ }
14355
+ for (const failure of givenUpFailures) {
14356
+ const excerpt = redactDiagnosticText(failure.body ?? "(no response body)").slice(
14357
+ 0,
14358
+ MAX_SESSION_DELETE_BODY_LENGTH
14359
+ );
14360
+ logSessionCleanupActivity(state, "sweep", {
12871
14361
  type: "info",
12872
- message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
14362
+ level: "warn",
14363
+ message: `Session cleanup: gave up on session ${failure.id} after ${SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES} isolated delete failures (status ${failure.status}): ${excerpt}`,
14364
+ metadata: {
14365
+ outcome: "session-delete-given-up",
14366
+ session_id: failure.id,
14367
+ status: failure.status,
14368
+ body_excerpt: excerpt,
14369
+ given_up_count: givenUpFailures.length
14370
+ }
12873
14371
  });
12874
- continue;
12875
14372
  }
12876
- if (await deleteSession(state.port, id, state.opencodeClient ?? void 0)) deleted++;
12877
- else failed++;
14373
+ const failedNote = failed > 0 ? `, failed ${failed}` : "";
14374
+ const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
14375
+ const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
14376
+ logActivity(state, {
14377
+ type: "info",
14378
+ message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
14379
+ });
12878
14380
  }
12879
- const failedNote = failed > 0 ? `, failed ${failed}` : "";
12880
- const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
12881
- logActivity(state, {
12882
- type: "info",
12883
- message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`
12884
- });
14381
+ const sessionMetadata = {
14382
+ sessions_inspected: sessions?.length ?? null,
14383
+ sessions_deleted: deleted,
14384
+ sessions_failed: failed,
14385
+ sessions_failed_4xx: failed4xx,
14386
+ sessions_failed_5xx: failed5xx,
14387
+ sessions_failed_other: failedOtherStatus,
14388
+ sessions_failed_threw: failedThrew,
14389
+ sessions_skipped_active: skippedNewlyActive,
14390
+ sessions_remaining: remaining,
14391
+ ...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
14392
+ };
14393
+ const protectedForReclaim = driver.protectedSessionIds();
12885
14394
  const reclaimResult = await reclaimSessionDbSpace({
12886
14395
  dbPath: sessionDbPath(),
12887
- maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
12888
- allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
14396
+ chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
14397
+ pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
14398
+ timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
14399
+ hasLiveTurn: protectedForReclaim.size > 0,
14400
+ hasProvenanceAnomaly: state.sessionDbProvenanceAnomaly
12889
14401
  });
12890
14402
  if (reclaimResult.ok) {
14403
+ state.sessionStoreHealth.observeWritable();
12891
14404
  const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
12892
14405
  const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
12893
14406
  const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
14407
+ const loopElapsedMs = Math.max(
14408
+ 0,
14409
+ reclaimResult.elapsedMs - reclaimResult.checkpointElapsedMs
14410
+ );
14411
+ const reclaimIsSlow = reclaimResult.elapsedMs >= 4 * SESSION_DB_RECLAIM_TIME_BUDGET_MS;
14412
+ const perPageNote = reclaimResult.pagesReclaimed > 0 ? `, per-page ${(loopElapsedMs / reclaimResult.pagesReclaimed).toFixed(2)} ms/page` : "";
14413
+ 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})` : "";
12894
14414
  logSessionCleanupActivity(state, "sweep", {
12895
14415
  type: "info",
12896
- message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`,
14416
+ level: reclaimIsSlow ? "warn" : "info",
14417
+ 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,
12897
14418
  metadata: {
12898
14419
  outcome: "reclaimed",
12899
14420
  mode: reclaimResult.mode,
@@ -12901,25 +14422,28 @@ async function runSweep(state, driver, config) {
12901
14422
  after_bytes: reclaimResult.afterBytes,
12902
14423
  checkpoint_busy: reclaimResult.checkpoint.busy,
12903
14424
  wal_frames_pending: reclaimResult.checkpoint.log,
12904
- sessions_inspected: sessions.length,
12905
- sessions_deleted: deleted,
12906
- sessions_failed: failed,
12907
- sessions_skipped_active: skippedNewlyActive
14425
+ stopped_by: reclaimResult.stoppedBy,
14426
+ freelist_before: reclaimResult.freelistBefore,
14427
+ freelist_after: reclaimResult.freelistAfter,
14428
+ pages_reclaimed: reclaimResult.pagesReclaimed,
14429
+ reclaim_elapsed_ms: reclaimResult.elapsedMs,
14430
+ checkpoint_elapsed_ms: reclaimResult.checkpointElapsedMs,
14431
+ ...sessionMetadata
12908
14432
  }
12909
14433
  });
12910
14434
  } else {
14435
+ if (reclaimResult.skipped === "disk-exhaustion") {
14436
+ state.sessionStoreHealth.observeUnwritable();
14437
+ }
12911
14438
  logSessionCleanupActivity(state, "sweep", {
12912
14439
  type: "info",
12913
- level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
14440
+ level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
12914
14441
  message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
12915
14442
  metadata: {
12916
14443
  outcome: "reclaim-skipped",
12917
14444
  reason: reclaimResult.skipped,
12918
14445
  ...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
12919
- sessions_inspected: sessions.length,
12920
- sessions_deleted: deleted,
12921
- sessions_failed: failed,
12922
- sessions_skipped_active: skippedNewlyActive
14446
+ ...sessionMetadata
12923
14447
  }
12924
14448
  });
12925
14449
  }
@@ -13014,10 +14538,12 @@ function scheduleUsageReporting(state, params) {
13014
14538
  let phase = "dormant";
13015
14539
  let rearmRequested = false;
13016
14540
  const armProbe = () => {
14541
+ if (!state.running) return;
13017
14542
  phase = "probe-pending";
13018
14543
  params.setTimer(setTimeout(() => void tick(true), params.firstDelayMs()));
13019
14544
  };
13020
14545
  const scheduleNextTick = () => {
14546
+ if (!state.running) return;
13021
14547
  if (rearmRequested) {
13022
14548
  rearmRequested = false;
13023
14549
  armProbe();
@@ -13051,6 +14577,7 @@ function scheduleUsageReporting(state, params) {
13051
14577
  phase = "tick-in-flight";
13052
14578
  try {
13053
14579
  const usage = await params.fetchUsage();
14580
+ if (!state.running) return;
13054
14581
  const result = await params.report(usage);
13055
14582
  if (result.ok) {
13056
14583
  if (consecutiveFailures > 0) {
@@ -13176,6 +14703,20 @@ function scheduleResourceUsageReporting(state, options) {
13176
14703
  message: `Resource usage collection: ${warning2}`
13177
14704
  });
13178
14705
  }
14706
+ const lowDiskDecision = evaluateLowDiskWarning({
14707
+ ...usage,
14708
+ nowMs: Date.now(),
14709
+ state: state.lowDiskWarningState
14710
+ });
14711
+ state.lowDiskWarningState = lowDiskDecision.state;
14712
+ if (lowDiskDecision.warning !== null) {
14713
+ logActivity(state, {
14714
+ type: "info",
14715
+ level: "warn",
14716
+ message: lowDiskDecision.warning
14717
+ });
14718
+ }
14719
+ if (!state.running) return;
13179
14720
  const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
13180
14721
  if (result.ok) {
13181
14722
  if (consecutiveFailures > 0) {
@@ -13208,7 +14749,9 @@ function scheduleResourceUsageReporting(state, options) {
13208
14749
  message: `Resource usage reporting failed: ${message}${failureStreakSuffix(consecutiveFailures)}`
13209
14750
  });
13210
14751
  } finally {
13211
- state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
14752
+ if (state.running) {
14753
+ state.resourceUsageTimer = setTimeout(() => void tick(), usageReportDelayMs());
14754
+ }
13212
14755
  }
13213
14756
  };
13214
14757
  state.resourceUsageTimer = setTimeout(() => void tick(), firstReportDelayMs());
@@ -13370,8 +14913,8 @@ async function beginGracefulShutdown(state, trigger) {
13370
14913
  return true;
13371
14914
  }
13372
14915
  );
13373
- const timedOut = new Promise((resolve4) => {
13374
- timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
14916
+ const timedOut = new Promise((resolve5) => {
14917
+ timer = setTimeout(() => resolve5(false), telemetryBudgetMs);
13375
14918
  });
13376
14919
  if (!await Promise.race([flushed, timedOut])) {
13377
14920
  log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
@@ -13411,6 +14954,8 @@ async function run(options) {
13411
14954
  port: options.port ?? 4096,
13412
14955
  conversationFilter: options.conversation ?? null,
13413
14956
  idleTimeout: options.idleTimeout ?? null,
14957
+ stayAwakeSeconds: null,
14958
+ stayAwakeUntilMs: null,
13414
14959
  json: options.json ?? false,
13415
14960
  interactive,
13416
14961
  logLevel,
@@ -13422,6 +14967,7 @@ async function run(options) {
13422
14967
  opencodeClient: null,
13423
14968
  opencodeV2CredentialImportGuard: { inFlight: false },
13424
14969
  sessionDbProvenanceAnomaly: false,
14970
+ sessionStoreHealth: createSessionStoreHealth(),
13425
14971
  opencodeProcess: null,
13426
14972
  stopOpenCodeLogTail: null,
13427
14973
  litestreamProcess: null,
@@ -13433,12 +14979,14 @@ async function run(options) {
13433
14979
  messageCount: 0,
13434
14980
  lastProxiedActivityAt: null,
13435
14981
  sessionCleanupTimers: [],
14982
+ sessionDeleteFailures: /* @__PURE__ */ new Map(),
13436
14983
  claudeUsageTimer: null,
13437
14984
  claudeUsageRearm: null,
13438
14985
  openaiUsageTimer: null,
13439
14986
  openaiUsageRearm: null,
13440
14987
  resourceUsageTimer: null,
13441
14988
  stopResourceUsageSampling: null,
14989
+ lowDiskWarningState: { worstBand: null, warnedAtMs: null },
13442
14990
  credentialSync: null,
13443
14991
  authHeader: ""
13444
14992
  };
@@ -13597,6 +15145,17 @@ async function run(options) {
13597
15145
  }
13598
15146
  spinner?.succeed(`Runner: ${validation.agent.name || state.agentId}`);
13599
15147
  state.agentName = validation.agent.name;
15148
+ const requestedStayAwakeSeconds = validation.agent?.wake_stay_awake_seconds;
15149
+ if (typeof requestedStayAwakeSeconds === "number" && Number.isFinite(requestedStayAwakeSeconds) && requestedStayAwakeSeconds > 0) {
15150
+ state.stayAwakeSeconds = requestedStayAwakeSeconds;
15151
+ } else if (requestedStayAwakeSeconds !== void 0 && requestedStayAwakeSeconds !== null) {
15152
+ state.stayAwakeSeconds = null;
15153
+ log2(
15154
+ state,
15155
+ `Ignoring invalid wake_stay_awake_seconds (type: ${typeof requestedStayAwakeSeconds})`,
15156
+ "warn"
15157
+ );
15158
+ }
13600
15159
  const microvmId = process.env.MICROVM_ID?.trim();
13601
15160
  if (microvmId) {
13602
15161
  const reported = await reportMicrovmId(state.agentId, state.authHeader, microvmId);
@@ -13612,27 +15171,74 @@ async function run(options) {
13612
15171
  } else {
13613
15172
  log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
13614
15173
  }
13615
- if (options.restoreRunnerCredentials) {
13616
- log2(state, "Restoring runner credentials before starting OpenCode");
13617
- const credentialContext = {
13618
- env: process.env,
13619
- log: (message, level = "info") => {
13620
- if (level === "error") {
13621
- logActivity(state, { type: "error", error: message });
13622
- } else {
13623
- logActivity(state, { type: "info", level, message });
13624
- }
15174
+ if (options.restoreRunnerCredentials || options.credentialSyncMarker) {
15175
+ const logCredentialVaultResolution = (message, level) => {
15176
+ log2(state, message, level);
15177
+ if (state.interactive && !state.json) {
15178
+ logActivity(state, { type: "info", level, message });
13625
15179
  }
13626
15180
  };
13627
- const githubTokenPopulated = await installRunnerSecret(credentialContext);
15181
+ const vaultResolution = await getRunnerCredentialVault(state.agentId, state.authHeader);
15182
+ if (vaultResolution.error) {
15183
+ delete process.env.CREDENTIAL_VAULT_PREFIX;
15184
+ logCredentialVaultResolution(
15185
+ `CREDENTIAL-VAULT-LOOKUP-FAILED: ${vaultResolution.error}; credentials will be read from and written to this runner's own prefix this boot`,
15186
+ "warn"
15187
+ );
15188
+ } else if (vaultResolution.prefix !== null && vaultResolution.vaultId !== null && vaultResolution.source !== null) {
15189
+ process.env.CREDENTIAL_VAULT_PREFIX = vaultResolution.prefix;
15190
+ logCredentialVaultResolution(
15191
+ `CREDENTIAL-VAULT: source=${vaultResolution.source} vault=${vaultResolution.vaultId} prefix=${vaultResolution.prefix}`,
15192
+ "info"
15193
+ );
15194
+ } else {
15195
+ delete process.env.CREDENTIAL_VAULT_PREFIX;
15196
+ logCredentialVaultResolution(
15197
+ "CREDENTIAL-VAULT-NONE: this runner is not linked to a credential vault; using its own prefix",
15198
+ "debug"
15199
+ );
15200
+ }
15201
+ }
15202
+ const credentialContext = {
15203
+ env: process.env,
15204
+ log: (message, level = "info") => {
15205
+ if (level === "error") {
15206
+ logActivity(state, { type: "error", error: message });
15207
+ } else {
15208
+ logActivity(state, { type: "info", level, message });
15209
+ }
15210
+ }
15211
+ };
15212
+ const githubTokenPopulated = await installRunnerSecret(credentialContext);
15213
+ if (options.restoreRunnerCredentials) {
13628
15214
  await restoreCredentialStores(credentialContext);
13629
- if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13630
15215
  }
15216
+ if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13631
15217
  state.credentialSync?.arm();
13632
- const openCodeActivityForwarder = createOpenCodeActivityForwarder(() => ({
13633
- agentId: state.agentId,
13634
- authHeader: state.authHeader
13635
- }));
15218
+ const dependencyInstallDiagnosticReporter = createDependencyInstallDiagnosticReporter({
15219
+ context: {
15220
+ homeDir: homedir7(),
15221
+ cwd: process.cwd(),
15222
+ microvmId: process.env.MICROVM_ID?.trim(),
15223
+ euid: process.getuid?.(),
15224
+ egid: process.getgid?.()
15225
+ },
15226
+ emitEvent: (message) => logActivity(state, { type: "info", level: "warn", message }),
15227
+ emitLocal: (message) => logLocalOnly(state, message, "warn")
15228
+ });
15229
+ const openCodeActivityForwarder = createOpenCodeActivityForwarder(
15230
+ () => ({
15231
+ agentId: state.agentId,
15232
+ authHeader: state.authHeader
15233
+ }),
15234
+ ({ isDiskFailure, sessionId, cause }) => {
15235
+ if (isDiskFailure) state.sessionStoreHealth.observeUnwritable();
15236
+ if (cause !== null && sessionId !== null) {
15237
+ state.sessionStoreHealth.observeCause(sessionId, cause);
15238
+ }
15239
+ },
15240
+ dependencyInstallDiagnosticReporter
15241
+ );
13636
15242
  const openCodeLogTail = tailOpenCodeLogFile(
13637
15243
  resolveOpenCodeLogPath(homedir7(), process.env),
13638
15244
  openCodeActivityForwarder
@@ -13642,22 +15248,27 @@ async function run(options) {
13642
15248
  openCodeLogTail.stop();
13643
15249
  };
13644
15250
  let sessionDbVerifyFatal = false;
15251
+ let sessionDbVerifyFatalReason;
13645
15252
  if (!options.restoreSessionDb) {
13646
- log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
15253
+ logActivity(state, {
15254
+ type: "info",
15255
+ level: "warn",
15256
+ message: "SESSION-DB-VERIFY-SKIPPED: reason=flag-absent \u2014 Skipping session-DB restore: --restore-session-db was not passed"
15257
+ });
13647
15258
  } else {
13648
15259
  const health = await checkOpenCodeHealth(state.port);
13649
15260
  if (health.healthy) {
13650
- log2(
13651
- state,
13652
- "Skipping session-DB restore: OpenCode is already serving this database",
13653
- "debug"
13654
- );
15261
+ logActivity(state, {
15262
+ type: "info",
15263
+ level: "warn",
15264
+ message: "SESSION-DB-VERIFY-SKIPPED: reason=opencode-serving \u2014 Skipping session-DB restore: OpenCode is already serving this database"
15265
+ });
13655
15266
  } else if (health.authFailed) {
13656
- log2(
13657
- state,
13658
- "Skipping session-DB restore: OpenCode answered with an authentication challenge; assuming this database is in use",
13659
- "debug"
13660
- );
15267
+ logActivity(state, {
15268
+ type: "info",
15269
+ level: "warn",
15270
+ 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"
15271
+ });
13661
15272
  } else {
13662
15273
  const result = await restoreAndVerifySessionDb({
13663
15274
  dbPath: sessionDbPath(),
@@ -13674,12 +15285,13 @@ async function run(options) {
13674
15285
  reportRecovery: (record) => reportSessionDbRecoveryRecord(state, record)
13675
15286
  });
13676
15287
  sessionDbVerifyFatal = result.verifyFatal;
15288
+ sessionDbVerifyFatalReason = result.verifyFatalReason;
13677
15289
  }
13678
15290
  }
13679
15291
  reportSessionDbRecovery(state);
13680
15292
  if (sessionDbVerifyFatal) {
13681
15293
  throw new Error(
13682
- "SESSION-DB-INTEGRITY-EXHAUSTED: the corrupt session DB could not be proven separated from the active backup prefix or removed from disk"
15294
+ 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`
13683
15295
  );
13684
15296
  }
13685
15297
  applyRunnerOpenCodeConfig({
@@ -13924,6 +15536,7 @@ async function run(options) {
13924
15536
  getAuthHeader: () => state.authHeader,
13925
15537
  conversationFilter: state.conversationFilter,
13926
15538
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
15539
+ sessionStoreHealth: state.sessionStoreHealth,
13927
15540
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
13928
15541
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
13929
15542
  fileSyncDirectories,
@@ -13957,6 +15570,13 @@ async function run(options) {
13957
15570
  type: "info",
13958
15571
  message: connectionMessage
13959
15572
  });
15573
+ if (!isReconnect && state.stayAwakeSeconds !== null && state.stayAwakeUntilMs === null) {
15574
+ state.stayAwakeUntilMs = Date.now() + state.stayAwakeSeconds * 1e3;
15575
+ logActivity(state, {
15576
+ type: "info",
15577
+ message: `Staying awake for at least ${state.stayAwakeSeconds}s as requested by the wake`
15578
+ });
15579
+ }
13960
15580
  if (options.tunnelReadyFile) {
13961
15581
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
13962
15582
  if (marker.ok) {
@@ -14005,14 +15625,14 @@ async function run(options) {
14005
15625
  },
14006
15626
  onDisconnected: (code, reason, transition) => {
14007
15627
  state.connected = false;
14008
- const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
15628
+ const safeReason2 = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
14009
15629
  logActivity(state, {
14010
15630
  type: "info",
14011
- message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
15631
+ message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason2})`
14012
15632
  });
14013
15633
  emitAgentDisconnected(state.agentId, {
14014
15634
  code,
14015
- reason: safeReason,
15635
+ reason: safeReason2,
14016
15636
  connection_generation: transition.generation
14017
15637
  });
14018
15638
  if (state.interactive) displayStatus(state);
@@ -14091,6 +15711,7 @@ async function run(options) {
14091
15711
  if (error2.message === "Unauthorized") tunnelSpinner?.fail("Unauthorized");
14092
15712
  throw error2;
14093
15713
  }
15714
+ if (!state.running) return;
14094
15715
  scheduleSessionCleanup(state, channelDriver, options);
14095
15716
  state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
14096
15717
  state.openaiUsageRearm = scheduleUsageReporting(state, {
@@ -14175,6 +15796,7 @@ program.command("login").description("Authenticate with Evident").option("--toke
14175
15796
  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 }));
14176
15797
  program.command("whoami").description("Show the currently logged in user").action(whoami);
14177
15798
  program.command("status").description("Check whether the configured credentials can reach Evident").option("--json", "Output in JSON format").action((options) => status({ json: options.json }));
15799
+ program.command("credential-vault-env").description("Print the shell assignment a runner image evals before starting the synchroniser").action(credentialVaultEnv);
14178
15800
  program.command("claude-usage").description("[spike] Show Claude subscription usage (requires a local `claude login`)").action(claudeUsage);
14179
15801
  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(
14180
15802
  "-a, --agent [id]",