@evident-ai/cli 3.5.2-dev.da3340e → 3.5.2-dev.e67eb8b

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
@@ -1158,7 +1158,7 @@ import ora4 from "ora";
1158
1158
  import { select as select4 } from "@inquirer/prompts";
1159
1159
 
1160
1160
  // src/lib/telemetry.ts
1161
- var CLI_VERSION = (true ? "3.5.2-dev.da3340e" : void 0) ?? process.env.npm_package_version ?? "unknown";
1161
+ var CLI_VERSION = (true ? "3.5.2-dev.e67eb8b" : void 0) ?? process.env.npm_package_version ?? "unknown";
1162
1162
  function getCliVersion() {
1163
1163
  return CLI_VERSION;
1164
1164
  }
@@ -1172,6 +1172,23 @@ var authProvider = null;
1172
1172
  function setTelemetryAuthProvider(provider) {
1173
1173
  authProvider = provider;
1174
1174
  }
1175
+ var diagnosticsReporter = null;
1176
+ function setTelemetryDiagnosticsReporter(reporter) {
1177
+ diagnosticsReporter = reporter;
1178
+ }
1179
+ function reportTelemetryDiagnostic(message) {
1180
+ if (!diagnosticsReporter) {
1181
+ console.error(message);
1182
+ return;
1183
+ }
1184
+ try {
1185
+ diagnosticsReporter(message);
1186
+ } catch (error2) {
1187
+ console.error(
1188
+ `Telemetry diagnostics reporter failed: ${error2 instanceof Error ? error2.message : String(error2)}; original diagnostic: ${message}`
1189
+ );
1190
+ }
1191
+ }
1175
1192
  var FLUSH_FAILURE_LOG_INTERVAL_MS = 6e4;
1176
1193
  var lastFlushFailureLoggedAt = 0;
1177
1194
  var suppressedFlushFailureCount = 0;
@@ -1237,7 +1254,9 @@ async function flushEvents() {
1237
1254
  signal: controller.signal
1238
1255
  });
1239
1256
  if (!response.ok) {
1240
- console.error(`Telemetry flush failed: ${response.status}`);
1257
+ reportTelemetryDiagnostic(
1258
+ `Telemetry could not send events (HTTP ${response.status}); runner and OpenCode are unaffected`
1259
+ );
1241
1260
  }
1242
1261
  } finally {
1243
1262
  clearTimeout(timeout);
@@ -1247,7 +1266,9 @@ async function flushEvents() {
1247
1266
  if (now - lastFlushFailureLoggedAt >= FLUSH_FAILURE_LOG_INTERVAL_MS) {
1248
1267
  const message = error2 instanceof Error ? error2.message : String(error2);
1249
1268
  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}`);
1269
+ reportTelemetryDiagnostic(
1270
+ `Telemetry flush failed: ${message}; runner and OpenCode are unaffected${suffix}`
1271
+ );
1251
1272
  lastFlushFailureLoggedAt = now;
1252
1273
  suppressedFlushFailureCount = 0;
1253
1274
  } else {
@@ -1317,25 +1338,72 @@ function parseOpenCodeLogLine(line) {
1317
1338
  const level = levelMatch[1].toUpperCase();
1318
1339
  if (level !== "WARN" && level !== "ERROR") return null;
1319
1340
  const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
1320
- return { level: level === "WARN" ? "warn" : "error", sessionID: sessionMatch?.[1] };
1341
+ const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
1342
+ const parsed = {
1343
+ level: level === "WARN" ? "warn" : "error",
1344
+ sessionID: sessionMatch?.[1]
1345
+ };
1346
+ const message = messageMatch?.[1] ?? messageMatch?.[2];
1347
+ if (message !== void 0) parsed.message = message;
1348
+ return parsed;
1321
1349
  }
1322
1350
  var MAX_LINE_BUFFER_BYTES = 16 * 1024;
1351
+ var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
1323
1352
  function createOpenCodeActivityForwarder(getContext) {
1324
1353
  let buffer = Buffer.alloc(0);
1325
- const flushLine = (line) => {
1326
- const parsed = parseOpenCodeLogLine(line);
1327
- if (!parsed) return;
1354
+ const repeatStates = /* @__PURE__ */ new Map();
1355
+ const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
1356
+ const metadata = {
1357
+ ...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
1358
+ ...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
1359
+ };
1328
1360
  forwardRunnerActivity(
1329
1361
  {
1330
1362
  level: parsed.level,
1331
1363
  error: line,
1332
- metadata: parsed.sessionID ? { sessionID: parsed.sessionID } : void 0,
1364
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
1333
1365
  source: "opencode"
1334
1366
  },
1335
1367
  getContext()
1336
1368
  );
1337
1369
  };
1338
- return (chunk) => {
1370
+ const expireRepeatStates = (now, currentKey) => {
1371
+ for (const [key, state] of repeatStates) {
1372
+ if (key === currentKey || now - state.lastForwardedAt < REPEAT_SUPPRESSION_WINDOW_MS) {
1373
+ continue;
1374
+ }
1375
+ if (state.repeatsSuppressed > 0) {
1376
+ forwardLine(state.line, state.parsed, state.repeatsSuppressed);
1377
+ }
1378
+ repeatStates.delete(key);
1379
+ }
1380
+ };
1381
+ const flushLine = (line) => {
1382
+ const parsed = parseOpenCodeLogLine(line);
1383
+ if (!parsed) return;
1384
+ const now = Date.now();
1385
+ const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}`;
1386
+ expireRepeatStates(now, key);
1387
+ if (key === void 0) {
1388
+ forwardLine(line, parsed);
1389
+ return;
1390
+ }
1391
+ const previous = repeatStates.get(key);
1392
+ if (!previous || now - previous.lastForwardedAt >= REPEAT_SUPPRESSION_WINDOW_MS) {
1393
+ forwardLine(line, parsed, previous?.repeatsSuppressed ?? 0);
1394
+ repeatStates.set(key, {
1395
+ line,
1396
+ parsed,
1397
+ lastForwardedAt: now,
1398
+ repeatsSuppressed: 0
1399
+ });
1400
+ return;
1401
+ }
1402
+ previous.line = line;
1403
+ previous.parsed = parsed;
1404
+ previous.repeatsSuppressed++;
1405
+ };
1406
+ const forward = ((chunk) => {
1339
1407
  buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
1340
1408
  let newlineIndex;
1341
1409
  while ((newlineIndex = buffer.indexOf(10)) !== -1) {
@@ -1346,7 +1414,16 @@ function createOpenCodeActivityForwarder(getContext) {
1346
1414
  flushLine(buffer.toString("utf-8"));
1347
1415
  buffer = Buffer.alloc(0);
1348
1416
  }
1417
+ });
1418
+ forward.flush = () => {
1419
+ for (const state of repeatStates.values()) {
1420
+ if (state.repeatsSuppressed > 0) {
1421
+ forwardLine(state.line, state.parsed, state.repeatsSuppressed);
1422
+ }
1423
+ }
1424
+ repeatStates.clear();
1349
1425
  };
1426
+ return forward;
1350
1427
  }
1351
1428
  var MAX_MESSAGE_LENGTH = 500;
1352
1429
  var MAX_METADATA_VALUE_LENGTH = 200;
@@ -1372,7 +1449,7 @@ function redactHomeDirectory(message) {
1372
1449
  }
1373
1450
  return result + message.slice(searchFrom);
1374
1451
  }
1375
- function redact(message) {
1452
+ function redactDiagnosticText(message) {
1376
1453
  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>");
1377
1454
  return redactHomeDirectory(redacted);
1378
1455
  }
@@ -1384,14 +1461,17 @@ function sanitiseMetadata(metadata) {
1384
1461
  if (!metadata) return {};
1385
1462
  const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
1386
1463
  if (Object.keys(metadata).length > entries.length) {
1387
- console.error(
1388
- `[runner-activity-telemetry] dropped ${Object.keys(metadata).length - entries.length} metadata entries`
1464
+ reportTelemetryDiagnostic(
1465
+ `Runner activity telemetry dropped ${Object.keys(metadata).length - entries.length} metadata entries; those entries were not sent`
1389
1466
  );
1390
1467
  }
1391
1468
  const sanitised = [];
1392
1469
  for (const [key, value] of entries) {
1393
1470
  if (typeof value === "string") {
1394
- sanitised.push([key, truncate(redact(value).slice(0, MAX_METADATA_VALUE_LENGTH))]);
1471
+ sanitised.push([
1472
+ key,
1473
+ truncate(redactDiagnosticText(value).slice(0, MAX_METADATA_VALUE_LENGTH))
1474
+ ]);
1395
1475
  } else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
1396
1476
  sanitised.push([key, value]);
1397
1477
  }
@@ -1409,8 +1489,8 @@ function admitUnderRateLimit(source, now) {
1409
1489
  }
1410
1490
  if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
1411
1491
  if (window.windowDroppedCount > 0) {
1412
- console.error(
1413
- `[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}"`
1492
+ reportTelemetryDiagnostic(
1493
+ `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}"`
1414
1494
  );
1415
1495
  }
1416
1496
  window.windowStartedAt = now;
@@ -1420,8 +1500,8 @@ function admitUnderRateLimit(source, now) {
1420
1500
  if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
1421
1501
  window.windowDroppedCount++;
1422
1502
  if (window.windowDroppedCount === 1) {
1423
- console.error(
1424
- `[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}"`
1503
+ reportTelemetryDiagnostic(
1504
+ `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}"`
1425
1505
  );
1426
1506
  }
1427
1507
  return false;
@@ -1436,7 +1516,7 @@ function forwardRunnerActivity(entry, context) {
1436
1516
  if (!context.agentId || !context.authHeader) return;
1437
1517
  if (!admitUnderRateLimit(source, Date.now())) return;
1438
1518
  const rawMessage = entry.error ?? entry.message ?? "";
1439
- const message = truncate(redact(rawMessage));
1519
+ const message = truncate(redactDiagnosticText(rawMessage));
1440
1520
  logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
1441
1521
  severity: SEVERITY_BY_LEVEL[entry.level],
1442
1522
  message,
@@ -1444,8 +1524,8 @@ function forwardRunnerActivity(entry, context) {
1444
1524
  agentId: context.agentId
1445
1525
  });
1446
1526
  } catch (err) {
1447
- console.error(
1448
- `[runner-activity-telemetry] failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
1527
+ reportTelemetryDiagnostic(
1528
+ `Runner activity telemetry failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
1449
1529
  );
1450
1530
  }
1451
1531
  }
@@ -1735,8 +1815,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
1735
1815
 
1736
1816
  // src/lib/http-timeout.ts
1737
1817
  var REQUEST_TIMEOUT_MS = 6e4;
1818
+ var RequestTimeoutError = class extends Error {
1819
+ constructor(operation, timeoutMs, elapsedMs) {
1820
+ super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
1821
+ this.operation = operation;
1822
+ this.timeoutMs = timeoutMs;
1823
+ this.elapsedMs = elapsedMs;
1824
+ this.name = "TimeoutError";
1825
+ }
1826
+ };
1827
+ function describeRequestOperation(input, init) {
1828
+ const method = (init?.method ?? input.method ?? "GET").toUpperCase();
1829
+ const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
1830
+ let path;
1831
+ try {
1832
+ path = new URL(rawUrl).pathname;
1833
+ } catch (error2) {
1834
+ if (!(error2 instanceof TypeError)) throw error2;
1835
+ path = rawUrl.split(/[?#]/, 1)[0];
1836
+ }
1837
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1838
+ const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
1839
+ const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
1840
+ return `${method} ${maskedPath}`;
1841
+ }
1738
1842
  function withRequestTimeout(fetchImpl, timeoutMs) {
1739
- return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
1843
+ return (async (input, init) => {
1844
+ const startedAt = Date.now();
1845
+ try {
1846
+ return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
1847
+ } catch (error2) {
1848
+ if (error2?.name === "TimeoutError") {
1849
+ throw new RequestTimeoutError(
1850
+ describeRequestOperation(input, init),
1851
+ timeoutMs,
1852
+ Date.now() - startedAt
1853
+ );
1854
+ }
1855
+ throw error2;
1856
+ }
1857
+ });
1740
1858
  }
1741
1859
 
1742
1860
  // src/lib/opencode/client.ts
@@ -3397,16 +3515,17 @@ function hasPrimaryHeaders(headers) {
3397
3515
  ].some((name) => headers.has(name));
3398
3516
  }
3399
3517
  async function getOpenAiUsage(port, client) {
3518
+ const openCodeVersion = client?.version ?? "v1";
3400
3519
  const credentials2 = readOpenCodeChatGptCredentials();
3401
3520
  if (!credentials2) {
3402
3521
  throw new OpenAiUsageError(
3403
- "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
3522
+ 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`.",
3404
3523
  "no_credentials"
3405
3524
  );
3406
3525
  }
3407
3526
  if (credentials2.expiresAt < Date.now()) {
3408
3527
  throw new OpenAiUsageError(
3409
- "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
3528
+ 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.",
3410
3529
  "credentials_expired"
3411
3530
  );
3412
3531
  }
@@ -5617,6 +5736,7 @@ function connectTunnel(options) {
5617
5736
  var RunnerConnection = class {
5618
5737
  opts;
5619
5738
  sleep;
5739
+ now;
5620
5740
  connection = null;
5621
5741
  resolvedAgentId;
5622
5742
  /** True while a (re)connect loop is in flight. */
@@ -5625,10 +5745,13 @@ var RunnerConnection = class {
5625
5745
  reconnectPromise = null;
5626
5746
  /** 1-based count of the current reconnect attempt streak. */
5627
5747
  reconnectAttempt = 0;
5748
+ generation = 0;
5749
+ disconnectedAtMs = null;
5628
5750
  constructor(opts) {
5629
5751
  this.opts = opts;
5630
5752
  this.resolvedAgentId = opts.agentId;
5631
5753
  this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5754
+ this.now = opts.now ?? Date.now;
5632
5755
  }
5633
5756
  get agentId() {
5634
5757
  return this.resolvedAgentId;
@@ -5664,13 +5787,22 @@ var RunnerConnection = class {
5664
5787
  port: this.opts.port,
5665
5788
  openCodePassword: this.opts.openCodePassword,
5666
5789
  onConnected: (agentId) => {
5790
+ const attempt = this.reconnectAttempt + 1;
5667
5791
  this.reconnectAttempt = 0;
5668
5792
  this.reconnecting = false;
5669
5793
  this.resolvedAgentId = agentId;
5670
- events.onConnected(agentId, isReconnect);
5794
+ this.generation++;
5795
+ const downtimeMs = this.disconnectedAtMs === null ? null : this.now() - this.disconnectedAtMs;
5796
+ this.disconnectedAtMs = null;
5797
+ events.onConnected(agentId, isReconnect, {
5798
+ generation: this.generation,
5799
+ attempt,
5800
+ downtimeMs
5801
+ });
5671
5802
  },
5672
5803
  onDisconnected: (code, reason) => {
5673
- events.onDisconnected(code, reason);
5804
+ this.disconnectedAtMs = this.now();
5805
+ events.onDisconnected(code, reason, { generation: this.generation });
5674
5806
  if (this.opts.isRunning() && code !== 1e3 && !this.reconnecting) {
5675
5807
  this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
5676
5808
  events.onError?.(`Reconnection failed: ${err.message}`);
@@ -12147,6 +12279,7 @@ function createCredentialSync({
12147
12279
 
12148
12280
  // src/commands/run.ts
12149
12281
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
12282
+ var MAX_DISCONNECT_REASON_LENGTH = 200;
12150
12283
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
12151
12284
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
12152
12285
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
@@ -12365,7 +12498,9 @@ function displayStatus(state) {
12365
12498
  const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
12366
12499
  const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
12367
12500
  const last = state.activityLog[state.activityLog.length - 1];
12368
- const detail = last ? chalk7.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
12501
+ const detail = last ? chalk7.dim(
12502
+ ` \xB7 ${last.timestamp.toISOString()} ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`
12503
+ ) : "";
12369
12504
  const agent = state.agentName ?? state.agentId;
12370
12505
  console.log(
12371
12506
  `${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
@@ -12384,7 +12519,7 @@ function reportOpenCodeV2CredentialImport(state, result) {
12384
12519
  logActivity(state, {
12385
12520
  type: "info",
12386
12521
  level: "warn",
12387
- message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them. See #2251 for importing existing logins.`
12522
+ 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.`
12388
12523
  });
12389
12524
  }
12390
12525
  if (result.imported.length === 0 && result.unsupported.length === 0 && result.failed.length === 0) {
@@ -13280,6 +13415,7 @@ async function run(options) {
13280
13415
  authHeader: ""
13281
13416
  };
13282
13417
  setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
13418
+ setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
13283
13419
  if (options.credentialSyncMarker) {
13284
13420
  state.credentialSync = createCredentialSync({
13285
13421
  markerPath: options.credentialSyncMarker,
@@ -13465,13 +13601,18 @@ async function run(options) {
13465
13601
  if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
13466
13602
  }
13467
13603
  state.credentialSync?.arm();
13468
- state.stopOpenCodeLogTail = tailOpenCodeLogFile(
13604
+ const openCodeActivityForwarder = createOpenCodeActivityForwarder(() => ({
13605
+ agentId: state.agentId,
13606
+ authHeader: state.authHeader
13607
+ }));
13608
+ const openCodeLogTail = tailOpenCodeLogFile(
13469
13609
  resolveOpenCodeLogPath(homedir7(), process.env),
13470
- createOpenCodeActivityForwarder(() => ({
13471
- agentId: state.agentId,
13472
- authHeader: state.authHeader
13473
- }))
13474
- ).stop;
13610
+ openCodeActivityForwarder
13611
+ );
13612
+ state.stopOpenCodeLogTail = () => {
13613
+ openCodeActivityForwarder.flush();
13614
+ openCodeLogTail.stop();
13615
+ };
13475
13616
  let sessionDbVerifyFatal = false;
13476
13617
  if (!options.restoreSessionDb) {
13477
13618
  log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
@@ -13780,12 +13921,13 @@ async function run(options) {
13780
13921
  openCodePassword: state.opencodePassword,
13781
13922
  isRunning: () => state.running,
13782
13923
  events: {
13783
- onConnected: (agentId, isReconnect) => {
13924
+ onConnected: (agentId, isReconnect, transition) => {
13784
13925
  state.connected = true;
13785
13926
  state.agentId = agentId;
13927
+ 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})`;
13786
13928
  logActivity(state, {
13787
13929
  type: "info",
13788
- message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (runner: ${agentId})`
13930
+ message: connectionMessage
13789
13931
  });
13790
13932
  if (options.tunnelReadyFile) {
13791
13933
  const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
@@ -13806,7 +13948,12 @@ async function run(options) {
13806
13948
  version: state.opencodeVersion,
13807
13949
  major: state.opencodeApiVersion,
13808
13950
  connected: state.opencodeConnected
13809
- })
13951
+ }),
13952
+ connection_generation: transition.generation,
13953
+ ...isReconnect && transition.downtimeMs !== null ? {
13954
+ reconnect_attempt: transition.attempt,
13955
+ downtime_ms: transition.downtimeMs
13956
+ } : {}
13810
13957
  });
13811
13958
  if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
13812
13959
  if (state.interactive) displayStatus(state);
@@ -13828,13 +13975,18 @@ async function run(options) {
13828
13975
  if (state.interactive) displayStatus(state);
13829
13976
  });
13830
13977
  },
13831
- onDisconnected: (code, reason) => {
13978
+ onDisconnected: (code, reason, transition) => {
13832
13979
  state.connected = false;
13980
+ const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
13833
13981
  logActivity(state, {
13834
13982
  type: "info",
13835
- message: `Tunnel disconnected (code: ${code}, reason: ${reason})`
13983
+ message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
13984
+ });
13985
+ emitAgentDisconnected(state.agentId, {
13986
+ code,
13987
+ reason: safeReason,
13988
+ connection_generation: transition.generation
13836
13989
  });
13837
- emitAgentDisconnected(state.agentId, { code, reason });
13838
13990
  if (state.interactive) displayStatus(state);
13839
13991
  },
13840
13992
  onError: (error2) => {
@@ -13934,7 +14086,7 @@ async function run(options) {
13934
14086
  },
13935
14087
  report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
13936
14088
  isLocalCredentialProblem: isLocalCredentialProblem2,
13937
- forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
14089
+ 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`",
13938
14090
  firstDelayMs: firstReportDelayMs,
13939
14091
  nextDelayMs: usageReportDelayMs,
13940
14092
  failureLogLevel: usageReportFailureLogLevel