@evident-ai/cli 3.5.2-dev.58beb33 → 3.5.2-dev.659ea21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { createRequire as createRequire2 } from "module";
5
4
  import { Command } from "commander";
6
5
 
7
6
  // src/commands/login.ts
@@ -1063,7 +1062,7 @@ async function claudeUsage() {
1063
1062
 
1064
1063
  // src/commands/run.ts
1065
1064
  import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
1066
- import { homedir as homedir6 } from "node:os";
1065
+ import { homedir as homedir7 } from "node:os";
1067
1066
  import { isAbsolute as isAbsolute3, join as join11, parse, resolve as resolvePath2 } from "node:path";
1068
1067
  import chalk7 from "chalk";
1069
1068
 
@@ -1158,7 +1157,7 @@ import ora4 from "ora";
1158
1157
  import { select as select4 } from "@inquirer/prompts";
1159
1158
 
1160
1159
  // src/lib/telemetry.ts
1161
- var CLI_VERSION = (true ? "3.5.2-dev.58beb33" : void 0) ?? process.env.npm_package_version ?? "unknown";
1160
+ var CLI_VERSION = (true ? "3.5.2-dev.659ea21" : void 0) ?? process.env.npm_package_version ?? "unknown";
1162
1161
  function getCliVersion() {
1163
1162
  return CLI_VERSION;
1164
1163
  }
@@ -1172,6 +1171,23 @@ var authProvider = null;
1172
1171
  function setTelemetryAuthProvider(provider) {
1173
1172
  authProvider = provider;
1174
1173
  }
1174
+ var diagnosticsReporter = null;
1175
+ function setTelemetryDiagnosticsReporter(reporter) {
1176
+ diagnosticsReporter = reporter;
1177
+ }
1178
+ function reportTelemetryDiagnostic(message) {
1179
+ if (!diagnosticsReporter) {
1180
+ console.error(message);
1181
+ return;
1182
+ }
1183
+ try {
1184
+ diagnosticsReporter(message);
1185
+ } catch (error2) {
1186
+ console.error(
1187
+ `Telemetry diagnostics reporter failed: ${error2 instanceof Error ? error2.message : String(error2)}; original diagnostic: ${message}`
1188
+ );
1189
+ }
1190
+ }
1175
1191
  var FLUSH_FAILURE_LOG_INTERVAL_MS = 6e4;
1176
1192
  var lastFlushFailureLoggedAt = 0;
1177
1193
  var suppressedFlushFailureCount = 0;
@@ -1237,7 +1253,9 @@ async function flushEvents() {
1237
1253
  signal: controller.signal
1238
1254
  });
1239
1255
  if (!response.ok) {
1240
- console.error(`Telemetry flush failed: ${response.status}`);
1256
+ reportTelemetryDiagnostic(
1257
+ `Telemetry could not send events (HTTP ${response.status}); runner and OpenCode are unaffected`
1258
+ );
1241
1259
  }
1242
1260
  } finally {
1243
1261
  clearTimeout(timeout);
@@ -1247,7 +1265,9 @@ async function flushEvents() {
1247
1265
  if (now - lastFlushFailureLoggedAt >= FLUSH_FAILURE_LOG_INTERVAL_MS) {
1248
1266
  const message = error2 instanceof Error ? error2.message : String(error2);
1249
1267
  const suffix = suppressedFlushFailureCount > 0 ? ` (${suppressedFlushFailureCount} more suppressed in the last ${FLUSH_FAILURE_LOG_INTERVAL_MS / 1e3}s)` : "";
1250
- console.error(`Telemetry flush error: ${message}${suffix}`);
1268
+ reportTelemetryDiagnostic(
1269
+ `Telemetry flush failed: ${message}; runner and OpenCode are unaffected${suffix}`
1270
+ );
1251
1271
  lastFlushFailureLoggedAt = now;
1252
1272
  suppressedFlushFailureCount = 0;
1253
1273
  } else {
@@ -1299,8 +1319,14 @@ var EventTypes = {
1299
1319
  };
1300
1320
 
1301
1321
  // src/lib/runner-activity-telemetry.ts
1302
- var FORWARDED_LEVELS = /* @__PURE__ */ new Set(["warn", "error"]);
1322
+ import { homedir as homedir2 } from "node:os";
1323
+ var FORWARDED_LEVELS_BY_SOURCE = {
1324
+ "cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
1325
+ opencode: /* @__PURE__ */ new Set(["warn", "error"]),
1326
+ "cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"])
1327
+ };
1303
1328
  var SEVERITY_BY_LEVEL = {
1329
+ info: "info",
1304
1330
  warn: "warning",
1305
1331
  error: "error"
1306
1332
  };
@@ -1311,25 +1337,72 @@ function parseOpenCodeLogLine(line) {
1311
1337
  const level = levelMatch[1].toUpperCase();
1312
1338
  if (level !== "WARN" && level !== "ERROR") return null;
1313
1339
  const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
1314
- return { level: level === "WARN" ? "warn" : "error", sessionID: sessionMatch?.[1] };
1340
+ const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
1341
+ const parsed = {
1342
+ level: level === "WARN" ? "warn" : "error",
1343
+ sessionID: sessionMatch?.[1]
1344
+ };
1345
+ const message = messageMatch?.[1] ?? messageMatch?.[2];
1346
+ if (message !== void 0) parsed.message = message;
1347
+ return parsed;
1315
1348
  }
1316
1349
  var MAX_LINE_BUFFER_BYTES = 16 * 1024;
1350
+ var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
1317
1351
  function createOpenCodeActivityForwarder(getContext) {
1318
1352
  let buffer = Buffer.alloc(0);
1319
- const flushLine = (line) => {
1320
- const parsed = parseOpenCodeLogLine(line);
1321
- if (!parsed) return;
1353
+ const repeatStates = /* @__PURE__ */ new Map();
1354
+ const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
1355
+ const metadata = {
1356
+ ...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
1357
+ ...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
1358
+ };
1322
1359
  forwardRunnerActivity(
1323
1360
  {
1324
1361
  level: parsed.level,
1325
1362
  error: line,
1326
- metadata: parsed.sessionID ? { sessionID: parsed.sessionID } : void 0,
1363
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
1327
1364
  source: "opencode"
1328
1365
  },
1329
1366
  getContext()
1330
1367
  );
1331
1368
  };
1332
- return (chunk) => {
1369
+ const expireRepeatStates = (now, currentKey) => {
1370
+ for (const [key, state] of repeatStates) {
1371
+ if (key === currentKey || now - state.lastForwardedAt < REPEAT_SUPPRESSION_WINDOW_MS) {
1372
+ continue;
1373
+ }
1374
+ if (state.repeatsSuppressed > 0) {
1375
+ forwardLine(state.line, state.parsed, state.repeatsSuppressed);
1376
+ }
1377
+ repeatStates.delete(key);
1378
+ }
1379
+ };
1380
+ const flushLine = (line) => {
1381
+ const parsed = parseOpenCodeLogLine(line);
1382
+ if (!parsed) return;
1383
+ const now = Date.now();
1384
+ const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}`;
1385
+ expireRepeatStates(now, key);
1386
+ if (key === void 0) {
1387
+ forwardLine(line, parsed);
1388
+ return;
1389
+ }
1390
+ const previous = repeatStates.get(key);
1391
+ if (!previous || now - previous.lastForwardedAt >= REPEAT_SUPPRESSION_WINDOW_MS) {
1392
+ forwardLine(line, parsed, previous?.repeatsSuppressed ?? 0);
1393
+ repeatStates.set(key, {
1394
+ line,
1395
+ parsed,
1396
+ lastForwardedAt: now,
1397
+ repeatsSuppressed: 0
1398
+ });
1399
+ return;
1400
+ }
1401
+ previous.line = line;
1402
+ previous.parsed = parsed;
1403
+ previous.repeatsSuppressed++;
1404
+ };
1405
+ const forward = ((chunk) => {
1333
1406
  buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
1334
1407
  let newlineIndex;
1335
1408
  while ((newlineIndex = buffer.indexOf(10)) !== -1) {
@@ -1340,14 +1413,44 @@ function createOpenCodeActivityForwarder(getContext) {
1340
1413
  flushLine(buffer.toString("utf-8"));
1341
1414
  buffer = Buffer.alloc(0);
1342
1415
  }
1416
+ });
1417
+ forward.flush = () => {
1418
+ for (const state of repeatStates.values()) {
1419
+ if (state.repeatsSuppressed > 0) {
1420
+ forwardLine(state.line, state.parsed, state.repeatsSuppressed);
1421
+ }
1422
+ }
1423
+ repeatStates.clear();
1343
1424
  };
1425
+ return forward;
1344
1426
  }
1345
1427
  var MAX_MESSAGE_LENGTH = 500;
1346
1428
  var MAX_METADATA_VALUE_LENGTH = 200;
1347
1429
  var MAX_METADATA_ENTRIES = 20;
1348
1430
  var TRUNCATION_MARKER = "\u2026";
1349
- function redact(message) {
1350
- return message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-ant-***").replace(/(?<![A-Za-z0-9_-])sk-proj-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-proj-***").replace(/(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "sk-***").replace(/(?<![A-Za-z0-9_-])(gh[oprsu])_[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "$1_***").replace(/(?<![A-Za-z0-9_-])github_pat_[A-Za-z0-9_]{20,}(?![A-Za-z0-9_-])/g, "github_pat_***").replace(/https?:\/\/\S+/g, "<url>");
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);
1351
1454
  }
1352
1455
  function truncate(message) {
1353
1456
  if (message.length <= MAX_MESSAGE_LENGTH) return message;
@@ -1357,14 +1460,17 @@ function sanitiseMetadata(metadata) {
1357
1460
  if (!metadata) return {};
1358
1461
  const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
1359
1462
  if (Object.keys(metadata).length > entries.length) {
1360
- console.error(
1361
- `[runner-activity-telemetry] dropped ${Object.keys(metadata).length - entries.length} metadata entries`
1463
+ reportTelemetryDiagnostic(
1464
+ `Runner activity telemetry dropped ${Object.keys(metadata).length - entries.length} metadata entries; those entries were not sent`
1362
1465
  );
1363
1466
  }
1364
1467
  const sanitised = [];
1365
1468
  for (const [key, value] of entries) {
1366
1469
  if (typeof value === "string") {
1367
- sanitised.push([key, truncate(redact(value).slice(0, MAX_METADATA_VALUE_LENGTH))]);
1470
+ sanitised.push([
1471
+ key,
1472
+ truncate(redactDiagnosticText(value).slice(0, MAX_METADATA_VALUE_LENGTH))
1473
+ ]);
1368
1474
  } else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
1369
1475
  sanitised.push([key, value]);
1370
1476
  }
@@ -1382,8 +1488,8 @@ function admitUnderRateLimit(source, now) {
1382
1488
  }
1383
1489
  if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1384
1490
  if (window.windowDroppedCount > 0) {
1385
- console.error(
1386
- `[runner-activity-telemetry] rate cap reached: dropped ${window.windowDroppedCount} ${window.windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (cap ${RATE_LIMIT_MAX_EVENTS}/min) for source "${source}"`
1491
+ reportTelemetryDiagnostic(
1492
+ `Runner activity telemetry rate cap reached: dropped ${window.windowDroppedCount} ${window.windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (limit ${RATE_LIMIT_MAX_EVENTS}/min); those entries were not sent for source "${source}"`
1387
1493
  );
1388
1494
  }
1389
1495
  window.windowStartedAt = now;
@@ -1393,8 +1499,8 @@ function admitUnderRateLimit(source, now) {
1393
1499
  if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
1394
1500
  window.windowDroppedCount++;
1395
1501
  if (window.windowDroppedCount === 1) {
1396
- console.error(
1397
- `[runner-activity-telemetry] rate cap reached (${RATE_LIMIT_MAX_EVENTS} per ${RATE_LIMIT_WINDOW_MS / 1e3}s) \u2014 dropping further entries this window for source "${source}"`
1502
+ reportTelemetryDiagnostic(
1503
+ `Runner activity telemetry rate cap reached (${RATE_LIMIT_MAX_EVENTS} entries per ${RATE_LIMIT_WINDOW_MS / 1e3}s limit; further entries were not sent for source "${source}"`
1398
1504
  );
1399
1505
  }
1400
1506
  return false;
@@ -1404,12 +1510,12 @@ function admitUnderRateLimit(source, now) {
1404
1510
  }
1405
1511
  function forwardRunnerActivity(entry, context) {
1406
1512
  try {
1407
- if (!FORWARDED_LEVELS.has(entry.level)) return;
1408
- if (!context.agentId || !context.authHeader) return;
1409
1513
  const source = entry.source ?? "cli.run";
1514
+ if (!FORWARDED_LEVELS_BY_SOURCE[source].has(entry.level)) return;
1515
+ if (!context.agentId || !context.authHeader) return;
1410
1516
  if (!admitUnderRateLimit(source, Date.now())) return;
1411
1517
  const rawMessage = entry.error ?? entry.message ?? "";
1412
- const message = truncate(redact(rawMessage));
1518
+ const message = truncate(redactDiagnosticText(rawMessage));
1413
1519
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1414
1520
  severity: SEVERITY_BY_LEVEL[entry.level],
1415
1521
  message,
@@ -1417,8 +1523,8 @@ function forwardRunnerActivity(entry, context) {
1417
1523
  agentId: context.agentId
1418
1524
  });
1419
1525
  } catch (err) {
1420
- console.error(
1421
- `[runner-activity-telemetry] failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
1526
+ reportTelemetryDiagnostic(
1527
+ `Runner activity telemetry failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
1422
1528
  );
1423
1529
  }
1424
1530
  }
@@ -1708,8 +1814,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1708
1814
 
1709
1815
  // src/lib/http-timeout.ts
1710
1816
  var REQUEST_TIMEOUT_MS = 6e4;
1817
+ var RequestTimeoutError = class extends Error {
1818
+ constructor(operation, timeoutMs, elapsedMs) {
1819
+ super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
1820
+ this.operation = operation;
1821
+ this.timeoutMs = timeoutMs;
1822
+ this.elapsedMs = elapsedMs;
1823
+ this.name = "TimeoutError";
1824
+ }
1825
+ };
1826
+ function describeRequestOperation(input, init) {
1827
+ const method = (init?.method ?? input.method ?? "GET").toUpperCase();
1828
+ const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
1829
+ let path;
1830
+ try {
1831
+ path = new URL(rawUrl).pathname;
1832
+ } catch (error2) {
1833
+ if (!(error2 instanceof TypeError)) throw error2;
1834
+ path = rawUrl.split(/[?#]/, 1)[0];
1835
+ }
1836
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1837
+ const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
1838
+ const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
1839
+ return `${method} ${maskedPath}`;
1840
+ }
1711
1841
  function withRequestTimeout(fetchImpl, timeoutMs) {
1712
- return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
1842
+ return (async (input, init) => {
1843
+ const startedAt = Date.now();
1844
+ try {
1845
+ return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
1846
+ } catch (error2) {
1847
+ if (error2?.name === "TimeoutError") {
1848
+ throw new RequestTimeoutError(
1849
+ describeRequestOperation(input, init),
1850
+ timeoutMs,
1851
+ Date.now() - startedAt
1852
+ );
1853
+ }
1854
+ throw error2;
1855
+ }
1856
+ });
1713
1857
  }
1714
1858
 
1715
1859
  // src/lib/opencode/client.ts
@@ -3214,7 +3358,7 @@ import { join as join4 } from "node:path";
3214
3358
 
3215
3359
  // src/lib/openai-usage.ts
3216
3360
  import { readFileSync as readFileSync3 } from "node:fs";
3217
- import { homedir as homedir2 } from "node:os";
3361
+ import { homedir as homedir3 } from "node:os";
3218
3362
  import { join as join3 } from "node:path";
3219
3363
  var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
3220
3364
  var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
@@ -3229,7 +3373,7 @@ function isLocalCredentialProblem2(err) {
3229
3373
  }
3230
3374
  function readOpenCodeChatGptCredentials() {
3231
3375
  try {
3232
- const raw = readFileSync3(join3(homedir2(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3376
+ const raw = readFileSync3(join3(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
3233
3377
  let parsed;
3234
3378
  try {
3235
3379
  parsed = JSON.parse(raw);
@@ -3370,16 +3514,17 @@ function hasPrimaryHeaders(headers) {
3370
3514
  ].some((name) => headers.has(name));
3371
3515
  }
3372
3516
  async function getOpenAiUsage(port, client) {
3517
+ const openCodeVersion = client?.version ?? "v1";
3373
3518
  const credentials2 = readOpenCodeChatGptCredentials();
3374
3519
  if (!credentials2) {
3375
3520
  throw new OpenAiUsageError(
3376
- "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
3521
+ openCodeVersion === "v2" ? "OpenAI usage reporting is not available on OpenCode V2 because V2 keeps a connected ChatGPT login in a store Evident cannot read." : "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
3377
3522
  "no_credentials"
3378
3523
  );
3379
3524
  }
3380
3525
  if (credentials2.expiresAt < Date.now()) {
3381
3526
  throw new OpenAiUsageError(
3382
- "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
3527
+ openCodeVersion === "v2" ? "The delivered ChatGPT login has expired. OpenCode V2 cannot be given a replacement by file; connect the account again from the runner's page in Evident." : "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
3383
3528
  "credentials_expired"
3384
3529
  );
3385
3530
  }
@@ -3661,7 +3806,7 @@ async function importOpenCodeV2Credentials({
3661
3806
  // src/lib/opencode/session-db-boot.ts
3662
3807
  import { spawn as spawn2 } from "node:child_process";
3663
3808
  import { mkdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
3664
- import { homedir as homedir3 } from "node:os";
3809
+ import { homedir as homedir4 } from "node:os";
3665
3810
  import { dirname as dirname2, resolve as resolvePath } from "node:path";
3666
3811
 
3667
3812
  // src/lib/runner-synchroniser.ts
@@ -4017,14 +4162,14 @@ async function restoreSessionDb(options, configPath, env) {
4017
4162
  case 0:
4018
4163
  return;
4019
4164
  case 31:
4020
- acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir3(), env));
4165
+ acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
4021
4166
  options.log(
4022
4167
  "SESSION-DB-REPLICA-UNUSABLE: booting with a fresh opencode.db and replicating into the existing prefix this boot",
4023
4168
  "warn"
4024
4169
  );
4025
4170
  return;
4026
4171
  case 32:
4027
- acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir3(), env));
4172
+ acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
4028
4173
  discardSessionDbDebris(options);
4029
4174
  markNoReplicate(
4030
4175
  options,
@@ -4113,7 +4258,7 @@ function fileExists(path) {
4113
4258
  async function restoreAndVerifySessionDb(options) {
4114
4259
  const env = options.env ?? process.env;
4115
4260
  clearMarker(options);
4116
- acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir3(), env));
4261
+ acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
4117
4262
  const synchroniserEnv = await runSynchroniser(["env"], {
4118
4263
  timeoutMs: SESSION_DB_SYNCHRONISER_TIMEOUT_MS,
4119
4264
  env
@@ -4932,11 +5077,11 @@ function buildSessionStoreSizeWarning(input) {
4932
5077
 
4933
5078
  // src/lib/opencode/log-tail.ts
4934
5079
  import { statSync as statSync4 } from "node:fs";
4935
- import { homedir as homedir4 } from "node:os";
5080
+ import { homedir as homedir5 } from "node:os";
4936
5081
  import { join as join7 } from "node:path";
4937
5082
  import { open as open2, stat } from "node:fs/promises";
4938
5083
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
4939
- function resolveOpenCodeLogPath(homeDir = homedir4(), env = process.env) {
5084
+ function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
4940
5085
  const dataDir = env.XDG_DATA_HOME || join7(homeDir, ".local", "share");
4941
5086
  return join7(dataDir, "opencode", "log", "opencode.log");
4942
5087
  }
@@ -5091,7 +5236,11 @@ async function reclaimSessionDbSpace(input) {
5091
5236
  console.warn(
5092
5237
  `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
5093
5238
  );
5094
- return { ok: false, skipped: "full-vacuum-blocked" };
5239
+ return {
5240
+ ok: false,
5241
+ skipped: "full-vacuum-blocked",
5242
+ detail: "a session turn is live"
5243
+ };
5095
5244
  }
5096
5245
  const fileBytesForGuard = statSync5(dbPath).size;
5097
5246
  const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
@@ -5099,7 +5248,7 @@ async function reclaimSessionDbSpace(input) {
5099
5248
  console.warn(
5100
5249
  `[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
5101
5250
  );
5102
- return { ok: false, skipped: "insufficient-disk-space" };
5251
+ return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
5103
5252
  }
5104
5253
  const beforeBytes = readLogicalBytes(db);
5105
5254
  db.exec("PRAGMA auto_vacuum=INCREMENTAL");
@@ -5361,6 +5510,8 @@ var StreamForwarder = class {
5361
5510
  if (done) break;
5362
5511
  const chunk = Buffer.from(value);
5363
5512
  for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
5513
+ await stream.pausePromise;
5514
+ if (ac.signal.aborted) break;
5364
5515
  const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
5365
5516
  if (this.binaryFramesSupported) {
5366
5517
  if (this.ws.readyState === WebSocket.OPEN) {
@@ -5586,6 +5737,7 @@ function connectTunnel(options) {
5586
5737
  var RunnerConnection = class {
5587
5738
  opts;
5588
5739
  sleep;
5740
+ now;
5589
5741
  connection = null;
5590
5742
  resolvedAgentId;
5591
5743
  /** True while a (re)connect loop is in flight. */
@@ -5594,10 +5746,13 @@ var RunnerConnection = class {
5594
5746
  reconnectPromise = null;
5595
5747
  /** 1-based count of the current reconnect attempt streak. */
5596
5748
  reconnectAttempt = 0;
5749
+ generation = 0;
5750
+ disconnectedAtMs = null;
5597
5751
  constructor(opts) {
5598
5752
  this.opts = opts;
5599
5753
  this.resolvedAgentId = opts.agentId;
5600
5754
  this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5755
+ this.now = opts.now ?? Date.now;
5601
5756
  }
5602
5757
  get agentId() {
5603
5758
  return this.resolvedAgentId;
@@ -5633,13 +5788,22 @@ var RunnerConnection = class {
5633
5788
  port: this.opts.port,
5634
5789
  openCodePassword: this.opts.openCodePassword,
5635
5790
  onConnected: (agentId) => {
5791
+ const attempt = this.reconnectAttempt + 1;
5636
5792
  this.reconnectAttempt = 0;
5637
5793
  this.reconnecting = false;
5638
5794
  this.resolvedAgentId = agentId;
5639
- events.onConnected(agentId, isReconnect);
5795
+ this.generation++;
5796
+ const downtimeMs = this.disconnectedAtMs === null ? null : this.now() - this.disconnectedAtMs;
5797
+ this.disconnectedAtMs = null;
5798
+ events.onConnected(agentId, isReconnect, {
5799
+ generation: this.generation,
5800
+ attempt,
5801
+ downtimeMs
5802
+ });
5640
5803
  },
5641
5804
  onDisconnected: (code, reason) => {
5642
- events.onDisconnected(code, reason);
5805
+ this.disconnectedAtMs = this.now();
5806
+ events.onDisconnected(code, reason, { generation: this.generation });
5643
5807
  if (this.opts.isRunning() && code !== 1e3 && !this.reconnecting) {
5644
5808
  this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
5645
5809
  events.onError?.(`Reconnection failed: ${err.message}`);
@@ -5980,7 +6144,7 @@ function createResourceUsageCollector(homeDir) {
5980
6144
  }
5981
6145
 
5982
6146
  // src/lib/channels/driver.ts
5983
- import { homedir as homedir5 } from "node:os";
6147
+ import { homedir as homedir6 } from "node:os";
5984
6148
 
5985
6149
  // src/lib/runner-file-sync.ts
5986
6150
  import { join as join9 } from "node:path";
@@ -6863,7 +7027,7 @@ var ChannelDriver = class _ChannelDriver {
6863
7027
  this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
6864
7028
  this.now = config.now ?? (() => Date.now());
6865
7029
  this.fileSyncDirectories = config.fileSyncDirectories ?? [];
6866
- this.homeDir = config.homeDir ?? homedir5();
7030
+ this.homeDir = config.homeDir ?? homedir6();
6867
7031
  this.maxActiveSessions = config.maxActiveSessions;
6868
7032
  this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
6869
7033
  this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
@@ -12116,6 +12280,7 @@ function createCredentialSync({
12116
12280
 
12117
12281
  // src/commands/run.ts
12118
12282
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
12283
+ var MAX_DISCONNECT_REASON_LENGTH = 200;
12119
12284
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
12120
12285
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
12121
12286
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
@@ -12271,7 +12436,13 @@ function logActivity(state, entry) {
12271
12436
  const level = entry.level ?? (entry.type === "error" ? "error" : "info");
12272
12437
  if (!meetsThreshold(state, level)) return;
12273
12438
  forwardRunnerActivity(
12274
- { level, message: entry.message, error: entry.error, metadata: entry.metadata },
12439
+ {
12440
+ level,
12441
+ message: entry.message,
12442
+ error: entry.error,
12443
+ metadata: entry.metadata,
12444
+ ...entry.source ? { source: entry.source } : {}
12445
+ },
12275
12446
  { agentId: state.agentId, authHeader: state.authHeader }
12276
12447
  );
12277
12448
  const fullEntry = {
@@ -12293,7 +12464,7 @@ function logActivity(state, entry) {
12293
12464
  }
12294
12465
  function reportSessionDbRecovery(state) {
12295
12466
  try {
12296
- const report = drainSessionDbRecoveryReport({ homeDir: homedir6(), env: process.env });
12467
+ const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
12297
12468
  for (const record of report.records) {
12298
12469
  const activity = buildSessionDbRecoveryActivity(record);
12299
12470
  if (!activity) throw new Error("could not map session-DB recovery record");
@@ -12328,7 +12499,9 @@ function displayStatus(state) {
12328
12499
  const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
12329
12500
  const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
12330
12501
  const last = state.activityLog[state.activityLog.length - 1];
12331
- const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
12502
+ const detail = last ? chalk7.dim(
12503
+ ` \xB7 ${last.timestamp.toISOString()} ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`
12504
+ ) : "";
12332
12505
  const agent = state.agentName ?? state.agentId;
12333
12506
  console.log(
12334
12507
  `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
@@ -12347,7 +12520,7 @@ function reportOpenCodeV2CredentialImport(state, result) {
12347
12520
  logActivity(state, {
12348
12521
  type: "info",
12349
12522
  level: "warn",
12350
- message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them. See #2251 for importing existing logins.`
12523
+ message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them. Connect the provider again from the runner's page in Evident; no shell on this machine is needed.`
12351
12524
  });
12352
12525
  }
12353
12526
  if (result.imported.length === 0 && result.unsupported.length === 0 && result.failed.length === 0) {
@@ -12372,7 +12545,7 @@ async function importOpenCodeV2CredentialsForState(state) {
12372
12545
  }
12373
12546
  const result = await importOpenCodeV2Credentials({
12374
12547
  request: client.request,
12375
- homeDir: homedir6(),
12548
+ homeDir: homedir7(),
12376
12549
  directory,
12377
12550
  guard: state.opencodeV2CredentialImportGuard,
12378
12551
  log: (message, level = "info") => logActivity(state, { type: "info", level, message })
@@ -12591,7 +12764,14 @@ function shouldWarnForReclaimSkip(reason) {
12591
12764
  return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
12592
12765
  }
12593
12766
  function sessionDbPath() {
12594
- return join11(homedir6(), ".local", "share", "opencode", "opencode.db");
12767
+ return join11(homedir7(), ".local", "share", "opencode", "opencode.db");
12768
+ }
12769
+ function logSessionCleanupActivity(state, phase, entry) {
12770
+ logActivity(state, {
12771
+ ...entry,
12772
+ source: "cli.session-cleanup",
12773
+ metadata: { ...entry.metadata, phase }
12774
+ });
12595
12775
  }
12596
12776
  function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
12597
12777
  const record = {
@@ -12634,9 +12814,13 @@ async function runSweep(state, driver, config) {
12634
12814
  try {
12635
12815
  const sessions = await listSessions(state.port, state.opencodeClient ?? void 0);
12636
12816
  if (sessions === null) {
12637
- logActivity(state, {
12817
+ logSessionCleanupActivity(state, "sweep", {
12638
12818
  type: "info",
12639
- message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`
12819
+ message: `Session cleanup: could not list sessions (opencode unreachable); skipping this sweep (${mode})`,
12820
+ metadata: {
12821
+ outcome: "sweep-skipped",
12822
+ reason: "opencode-unreachable"
12823
+ }
12640
12824
  });
12641
12825
  return;
12642
12826
  }
@@ -12680,21 +12864,47 @@ async function runSweep(state, driver, config) {
12680
12864
  const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
12681
12865
  const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
12682
12866
  const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
12683
- logActivity(state, {
12867
+ logSessionCleanupActivity(state, "sweep", {
12684
12868
  type: "info",
12685
- message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`
12869
+ message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`,
12870
+ metadata: {
12871
+ outcome: "reclaimed",
12872
+ mode: reclaimResult.mode,
12873
+ before_bytes: reclaimResult.beforeBytes,
12874
+ after_bytes: reclaimResult.afterBytes,
12875
+ checkpoint_busy: reclaimResult.checkpoint.busy,
12876
+ wal_frames_pending: reclaimResult.checkpoint.log,
12877
+ sessions_inspected: sessions.length,
12878
+ sessions_deleted: deleted,
12879
+ sessions_failed: failed,
12880
+ sessions_skipped_active: skippedNewlyActive
12881
+ }
12686
12882
  });
12687
12883
  } else {
12688
- logActivity(state, {
12884
+ logSessionCleanupActivity(state, "sweep", {
12689
12885
  type: "info",
12690
- message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : "")
12886
+ level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
12887
+ message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
12888
+ metadata: {
12889
+ outcome: "reclaim-skipped",
12890
+ reason: reclaimResult.skipped,
12891
+ ...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
12892
+ sessions_inspected: sessions.length,
12893
+ sessions_deleted: deleted,
12894
+ sessions_failed: failed,
12895
+ sessions_skipped_active: skippedNewlyActive
12896
+ }
12691
12897
  });
12692
12898
  }
12693
12899
  } catch (error2) {
12694
12900
  const message = error2 instanceof Error ? error2.message : String(error2);
12695
- logActivity(state, {
12901
+ logSessionCleanupActivity(state, "sweep", {
12696
12902
  type: "error",
12697
- error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`
12903
+ error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`,
12904
+ metadata: {
12905
+ outcome: "sweep-failed",
12906
+ reason: "sweep-error"
12907
+ }
12698
12908
  });
12699
12909
  }
12700
12910
  }
@@ -12710,14 +12920,19 @@ function scheduleSessionCleanup(state, driver, options) {
12710
12920
  for (const warning2 of config.warnings) {
12711
12921
  logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
12712
12922
  }
12713
- const dbBytes = statSessionDbBytes(homedir6());
12923
+ const dbBytes = statSessionDbBytes(homedir7());
12714
12924
  void (async () => {
12715
12925
  const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
12716
12926
  if (reclaimAvailability !== null) {
12717
- logActivity(state, {
12927
+ logSessionCleanupActivity(state, "startup", {
12718
12928
  type: "info",
12719
12929
  level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
12720
- message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
12930
+ message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : ""),
12931
+ metadata: {
12932
+ outcome: "preflight-skipped",
12933
+ reason: reclaimAvailability.reason,
12934
+ ...reclaimAvailability.detail !== void 0 ? { detail: reclaimAvailability.detail } : {}
12935
+ }
12721
12936
  });
12722
12937
  }
12723
12938
  const sizeWarning = buildSessionStoreSizeWarning({
@@ -12734,9 +12949,15 @@ function scheduleSessionCleanup(state, driver, options) {
12734
12949
  );
12735
12950
  });
12736
12951
  if (!config.enabled) return;
12737
- logActivity(state, {
12952
+ logSessionCleanupActivity(state, "startup", {
12738
12953
  type: "info",
12739
- message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`
12954
+ message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`,
12955
+ metadata: {
12956
+ outcome: "armed",
12957
+ interval_ms: config.intervalMs,
12958
+ max_age_ms: config.maxAgeMs ?? null,
12959
+ max_count: config.maxCount ?? null
12960
+ }
12740
12961
  });
12741
12962
  const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
12742
12963
  const firstSweep = setTimeout(
@@ -12915,7 +13136,7 @@ function scheduleResourceUsageReporting(state, options) {
12915
13136
  });
12916
13137
  return;
12917
13138
  }
12918
- const { collect, stop } = createResourceUsageCollector(homedir6());
13139
+ const { collect, stop } = createResourceUsageCollector(homedir7());
12919
13140
  state.stopResourceUsageSampling = stop;
12920
13141
  let consecutiveFailures = 0;
12921
13142
  const tick = async () => {
@@ -13140,7 +13361,7 @@ async function run(options) {
13140
13361
  let fileSyncDirectories;
13141
13362
  try {
13142
13363
  logLevel = resolveLogLevel(options);
13143
- fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir6());
13364
+ fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
13144
13365
  if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
13145
13366
  throw new Error(
13146
13367
  "--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
@@ -13195,6 +13416,7 @@ async function run(options) {
13195
13416
  authHeader: ""
13196
13417
  };
13197
13418
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
13419
+ setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
13198
13420
  if (options.credentialSyncMarker) {
13199
13421
  state.credentialSync = createCredentialSync({
13200
13422
  markerPath: options.credentialSyncMarker,
@@ -13380,13 +13602,18 @@ async function run(options) {
13380
13602
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13381
13603
  }
13382
13604
  state.credentialSync?.arm();
13383
- state.stopOpenCodeLogTail = tailOpenCodeLogFile(
13384
- resolveOpenCodeLogPath(homedir6(), process.env),
13385
- createOpenCodeActivityForwarder(() => ({
13386
- agentId: state.agentId,
13387
- authHeader: state.authHeader
13388
- }))
13389
- ).stop;
13605
+ const openCodeActivityForwarder = createOpenCodeActivityForwarder(() => ({
13606
+ agentId: state.agentId,
13607
+ authHeader: state.authHeader
13608
+ }));
13609
+ const openCodeLogTail = tailOpenCodeLogFile(
13610
+ resolveOpenCodeLogPath(homedir7(), process.env),
13611
+ openCodeActivityForwarder
13612
+ );
13613
+ state.stopOpenCodeLogTail = () => {
13614
+ openCodeActivityForwarder.flush();
13615
+ openCodeLogTail.stop();
13616
+ };
13390
13617
  let sessionDbVerifyFatal = false;
13391
13618
  if (!options.restoreSessionDb) {
13392
13619
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -13503,7 +13730,7 @@ async function run(options) {
13503
13730
  const provenance = checkSessionDbProvenance({
13504
13731
  dbPath: sessionDbPath(),
13505
13732
  currentVersion: state.opencodeVersion,
13506
- homeDir: homedir6(),
13733
+ homeDir: homedir7(),
13507
13734
  env: process.env
13508
13735
  });
13509
13736
  if (provenance.anomaly) {
@@ -13673,7 +13900,7 @@ async function run(options) {
13673
13900
  // #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
13674
13901
  // REJECTED with `file_sync_disabled` on the ack, not silently ignored.
13675
13902
  fileSyncDirectories,
13676
- homeDir: homedir6(),
13903
+ homeDir: homedir7(),
13677
13904
  maxActiveSessions,
13678
13905
  log: (entry) => (
13679
13906
  // Thread the driver's real level straight through so `debug`/`warn`
@@ -13695,12 +13922,13 @@ async function run(options) {
13695
13922
  openCodePassword: state.opencodePassword,
13696
13923
  isRunning: () => state.running,
13697
13924
  events: {
13698
- onConnected: (agentId, isReconnect) => {
13925
+ onConnected: (agentId, isReconnect, transition) => {
13699
13926
  state.connected = true;
13700
13927
  state.agentId = agentId;
13928
+ const connectionMessage = isReconnect ? `Tunnel reconnected (runner: ${agentId}, generation: ${transition.generation}, attempt: ${transition.attempt}, offline for ${transition.downtimeMs}ms)` : `Tunnel connected (runner: ${agentId}, generation: ${transition.generation})`;
13701
13929
  logActivity(state, {
13702
13930
  type: "info",
13703
- message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (runner: ${agentId})`
13931
+ message: connectionMessage
13704
13932
  });
13705
13933
  if (options.tunnelReadyFile) {
13706
13934
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
@@ -13721,7 +13949,12 @@ async function run(options) {
13721
13949
  version: state.opencodeVersion,
13722
13950
  major: state.opencodeApiVersion,
13723
13951
  connected: state.opencodeConnected
13724
- })
13952
+ }),
13953
+ connection_generation: transition.generation,
13954
+ ...isReconnect && transition.downtimeMs !== null ? {
13955
+ reconnect_attempt: transition.attempt,
13956
+ downtime_ms: transition.downtimeMs
13957
+ } : {}
13725
13958
  });
13726
13959
  if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
13727
13960
  if (state.interactive) displayStatus(state);
@@ -13743,13 +13976,18 @@ async function run(options) {
13743
13976
  if (state.interactive) displayStatus(state);
13744
13977
  });
13745
13978
  },
13746
- onDisconnected: (code, reason) => {
13979
+ onDisconnected: (code, reason, transition) => {
13747
13980
  state.connected = false;
13981
+ const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
13748
13982
  logActivity(state, {
13749
13983
  type: "info",
13750
- message: `Tunnel disconnected (code: ${code}, reason: ${reason})`
13984
+ message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
13985
+ });
13986
+ emitAgentDisconnected(state.agentId, {
13987
+ code,
13988
+ reason: safeReason,
13989
+ connection_generation: transition.generation
13751
13990
  });
13752
- emitAgentDisconnected(state.agentId, { code, reason });
13753
13991
  if (state.interactive) displayStatus(state);
13754
13992
  },
13755
13993
  onError: (error2) => {
@@ -13849,7 +14087,7 @@ async function run(options) {
13849
14087
  },
13850
14088
  report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
13851
14089
  isLocalCredentialProblem: isLocalCredentialProblem2,
13852
- forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
14090
+ forcedOnHint: state.opencodeApiVersion === "v2" ? "OpenCode V2 keeps its connected ChatGPT login in a store Evident cannot read" : "connect a ChatGPT account to this runner, or run `opencode auth login`",
13853
14091
  firstDelayMs: firstReportDelayMs,
13854
14092
  nextDelayMs: usageReportDelayMs,
13855
14093
  failureLogLevel: usageReportFailureLogLevel
@@ -13892,7 +14130,7 @@ async function run(options) {
13892
14130
  }
13893
14131
 
13894
14132
  // src/index.ts
13895
- var { version } = createRequire2(import.meta.url)("../package.json");
14133
+ var version = getCliVersion();
13896
14134
  var program = new Command();
13897
14135
  program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
13898
14136
  "--endpoint <url>",